save card content, add charges component
parent
fb5b5add6e
commit
600af1679a
@ -0,0 +1 @@
|
|||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><circle r="7" cx="7" cy="7" stroke="black" stroke-width="2" fill="none" /></svg>
|
After Width: | Height: | Size: 164 B |
@ -0,0 +1 @@
|
|||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><rect width="14" height="14" stroke="black" stroke-width="2" fill="none" /></svg>
|
After Width: | Height: | Size: 165 B |
@ -0,0 +1 @@
|
|||||||
|
<svg width="17" height="13" viewBox="0 0 17 13" xmlns="http://www.w3.org/2000/svg"> <path d="M5.625 4.85h9.25a1.125 1.125 0 0 1 0 2.25h-9.25a1.125 1.125 0 0 1 0-2.25zm0-4.85h9.25a1.125 1.125 0 0 1 0 2.25h-9.25a1.125 1.125 0 0 1 0-2.25zm0 9.85h9.25a1.125 1.125 0 0 1 0 2.25h-9.25a1.125 1.125 0 0 1 0-2.25zm-4.5-5a1.125 1.125 0 1 1 0 2.25 1.125 1.125 0 0 1 0-2.25zm0-4.85a1.125 1.125 0 1 1 0 2.25 1.125 1.125 0 0 1 0-2.25zm0 9.85a1.125 1.125 0 1 1 0 2.25 1.125 1.125 0 0 1 0-2.25z"/></svg>
|
After Width: | Height: | Size: 488 B |
@ -0,0 +1 @@
|
|||||||
|
<svg width="17" height="13" viewBox="0 0 17 13" xmlns="http://www.w3.org/2000/svg"><path d="M5.819 4.607h9.362a1.069 1.069 0 0 1 0 2.138H5.82a1.069 1.069 0 1 1 0-2.138zm0-4.607h9.362a1.069 1.069 0 0 1 0 2.138H5.82a1.069 1.069 0 1 1 0-2.138zm0 9.357h9.362a1.069 1.069 0 0 1 0 2.138H5.82a1.069 1.069 0 0 1 0-2.137zM1.468 4.155V1.33c-.554.404-.926.606-1.118.606a.338.338 0 0 1-.244-.104A.327.327 0 0 1 0 1.59c0-.107.035-.184.105-.234.07-.05.192-.114.369-.192.264-.118.475-.243.633-.373.158-.13.298-.276.42-.438a3.94 3.94 0 0 1 .238-.298C1.802.019 1.872 0 1.975 0c.115 0 .208.042.277.127.07.085.105.202.105.351v3.556c0 .416-.15.624-.448.624a.421.421 0 0 1-.32-.127c-.08-.085-.121-.21-.121-.376zm-.283 6.664h1.572c.156 0 .275.03.358.091a.294.294 0 0 1 .123.25.323.323 0 0 1-.098.238c-.065.065-.164.097-.296.097H.629a.494.494 0 0 1-.353-.119.372.372 0 0 1-.126-.28c0-.068.027-.16.081-.273a.977.977 0 0 1 .178-.268c.267-.264.507-.49.722-.678.215-.188.368-.312.46-.371.165-.11.302-.222.412-.334.109-.112.192-.226.25-.344a.786.786 0 0 0 .085-.345.6.6 0 0 0-.341-.553.75.75 0 0 0-.345-.08c-.263 0-.47.11-.62.329-.02.029-.054.107-.101.235a.966.966 0 0 1-.16.295c-.059.069-.145.103-.26.103a.348.348 0 0 1-.25-.094.34.34 0 0 1-.099-.258c0-.132.031-.27.093-.413.063-.143.155-.273.279-.39.123-.116.28-.21.47-.282.189-.072.411-.107.666-.107.307 0 .569.045.786.137a1.182 1.182 0 0 1 .618.623 1.18 1.18 0 0 1-.096 1.083 2.03 2.03 0 0 1-.378.457c-.128.11-.344.282-.646.517-.302.235-.509.417-.621.547a1.637 1.637 0 0 0-.148.187z"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1 @@
|
|||||||
|
<svg width="17" height="13" viewBox="0 0 17 13" xmlns="http://www.w3.org/2000/svg"> <path d="M5.625 4.85h9.25a1.125 1.125 0 0 1 0 2.25h-9.25a1.125 1.125 0 0 1 0-2.25zm0-4.85h9.25a1.125 1.125 0 0 1 0 2.25h-9.25a1.125 1.125 0 0 1 0-2.25zm0 9.85h9.25a1.125 1.125 0 0 1 0 2.25h-9.25a1.125 1.125 0 0 1 0-2.25zm-4.5-5a1.125 1.125 0 1 1 0 2.25 1.125 1.125 0 0 1 0-2.25zm0-4.85a1.125 1.125 0 1 1 0 2.25 1.125 1.125 0 0 1 0-2.25zm0 9.85a1.125 1.125 0 1 1 0 2.25 1.125 1.125 0 0 1 0-2.25z"/></svg>
|
After Width: | Height: | Size: 488 B |
@ -0,0 +1,137 @@
|
|||||||
|
import { ContentlessBlock, BlockToolArgs } from './contentless-block'
|
||||||
|
import icon from '../assets/editor/charges.svg.txt'
|
||||||
|
import iconCircle from '../assets/editor/charges-circle.svg.txt'
|
||||||
|
|
||||||
|
const title = 'Charges'
|
||||||
|
|
||||||
|
interface ChargesData {
|
||||||
|
variant: string;
|
||||||
|
amount: number;
|
||||||
|
size: number;
|
||||||
|
stretch: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Charges extends ContentlessBlock {
|
||||||
|
static MIN_SIZE = 1
|
||||||
|
static MAX_SIZE = 5
|
||||||
|
private _variant: string
|
||||||
|
private _amount: number
|
||||||
|
private _size: number
|
||||||
|
private _stretch: boolean
|
||||||
|
|
||||||
|
constructor (args: BlockToolArgs) {
|
||||||
|
super(args)
|
||||||
|
console.log('new charges', args)
|
||||||
|
this._settingButtons = [
|
||||||
|
{ name: 'box', icon, action: (name: string) => this.setVariant(name) },
|
||||||
|
{ name: 'more', icon: icon, action: () => this.increaseAmount() },
|
||||||
|
{ name: 'bigger', icon: icon, action: () => this.increaseSize() },
|
||||||
|
{ name: 'circle', icon: iconCircle, action: (name: string) => this.setVariant(name) },
|
||||||
|
{ name: 'less', icon: icon, action: () => this.decreaseAmount() },
|
||||||
|
{ name: 'smaller', icon: icon, action: () => this.decreaseSize() },
|
||||||
|
{ name: 'toggle-stretch', icon: icon, action: () => this.toggleStretch() }
|
||||||
|
]
|
||||||
|
const { variant, amount, size, stretch } = (args.data || {}) as ChargesData
|
||||||
|
|
||||||
|
this._variant = variant || 'box'
|
||||||
|
this._amount = amount || 5
|
||||||
|
this._size = size || 1
|
||||||
|
this._stretch = !(stretch === false)
|
||||||
|
|
||||||
|
this._element = this._render()
|
||||||
|
}
|
||||||
|
|
||||||
|
private setVariant (variant: string) {
|
||||||
|
if (this._variant === variant) return
|
||||||
|
|
||||||
|
const charges = Array.from(this._element.children)
|
||||||
|
|
||||||
|
charges.forEach(charge => {
|
||||||
|
charge.classList.remove(`card-charge-${this._variant}`)
|
||||||
|
charge.classList.add(`card-charge-${variant}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
this._variant = variant
|
||||||
|
}
|
||||||
|
|
||||||
|
private toggleStretch () {
|
||||||
|
if (this._stretch) this._element.classList.remove('card-charges-stretch')
|
||||||
|
else this._element.classList.add('card-charges-stretch')
|
||||||
|
this._stretch = !this._stretch
|
||||||
|
}
|
||||||
|
|
||||||
|
private createCharge (): HTMLElement {
|
||||||
|
const charge = document.createElement('DIV')
|
||||||
|
charge.classList.add('card-charge', `card-charge-${this._variant}`, `card-charge-size-${this._size}`)
|
||||||
|
return charge
|
||||||
|
}
|
||||||
|
|
||||||
|
private increaseAmount () {
|
||||||
|
this._element.appendChild(this.createCharge())
|
||||||
|
this._amount++
|
||||||
|
}
|
||||||
|
|
||||||
|
private decreaseAmount () {
|
||||||
|
const child = this._element.lastElementChild
|
||||||
|
if (child) {
|
||||||
|
this._element.removeChild(child)
|
||||||
|
this._amount--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private increaseSize () {
|
||||||
|
if (this._size >= Charges.MAX_SIZE) return
|
||||||
|
|
||||||
|
const charges = Array.from(this._element.children)
|
||||||
|
|
||||||
|
charges.forEach(charge => {
|
||||||
|
charge.classList.remove(`card-charge-size-${this._size}`)
|
||||||
|
charge.classList.add(`card-charge-size-${this._size + 1}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
this._size++
|
||||||
|
}
|
||||||
|
|
||||||
|
private decreaseSize () {
|
||||||
|
if (this._size <= Charges.MIN_SIZE) return
|
||||||
|
|
||||||
|
const charges = Array.from(this._element.children)
|
||||||
|
|
||||||
|
charges.forEach(charge => {
|
||||||
|
charge.classList.remove(`card-charge-size-${this._size}`)
|
||||||
|
charge.classList.add(`card-charge-size-${this._size - 1}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
this._size--
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _render (): HTMLElement {
|
||||||
|
const el = document.createElement('DIV')
|
||||||
|
el.classList.add('card-charges-wrapper', this._CSS.block)
|
||||||
|
|
||||||
|
if (this._stretch) el.classList.add('card-charges-stretch')
|
||||||
|
|
||||||
|
for (let i = 0; i < this._amount; i++) {
|
||||||
|
el.appendChild(this.createCharge())
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('rendered', this._amount, 'charges', el)
|
||||||
|
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
public save (): ChargesData {
|
||||||
|
return {
|
||||||
|
variant: this._variant,
|
||||||
|
amount: this._amount,
|
||||||
|
size: this._size,
|
||||||
|
stretch: this._stretch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get toolbox () {
|
||||||
|
return { icon, title }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Charges
|
@ -1,207 +0,0 @@
|
|||||||
import { ToolConstructable, ToolConfig, HTMLPasteEvent } from '@editorjs/editorjs'
|
|
||||||
import { BlockToolExt, BlockToolArgs } from './block-tool'
|
|
||||||
|
|
||||||
import icon from '../assets/editor/header.svg.txt'
|
|
||||||
import icon1 from '../assets/editor/header1.svg.txt'
|
|
||||||
import icon2 from '../assets/editor/header2.svg.txt'
|
|
||||||
import icon3 from '../assets/editor/header3.svg.txt'
|
|
||||||
import icon4 from '../assets/editor/header4.svg.txt'
|
|
||||||
import icon5 from '../assets/editor/header5.svg.txt'
|
|
||||||
import icon6 from '../assets/editor/header6.svg.txt'
|
|
||||||
const title = 'Heading'
|
|
||||||
|
|
||||||
interface PasteConfig {
|
|
||||||
tags: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
enum HeadingLevel {
|
|
||||||
One = 1,
|
|
||||||
Two = 2,
|
|
||||||
Three = 3,
|
|
||||||
Four = 4,
|
|
||||||
Five = 5,
|
|
||||||
Six = 6
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HeaderConfig extends ToolConfig {
|
|
||||||
placeholder?: string;
|
|
||||||
levels?: HeadingLevel[];
|
|
||||||
defaultLevel?: HeadingLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HeaderData {
|
|
||||||
text?: string;
|
|
||||||
level?: HeadingLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Heading extends BlockToolExt {
|
|
||||||
protected _config: HeaderConfig
|
|
||||||
protected settingsButtons: HTMLElement[] = []
|
|
||||||
private levels: HeadingLevel[]
|
|
||||||
private defaultLevel: HeadingLevel
|
|
||||||
private currentLevel: HeadingLevel
|
|
||||||
private icons: Map<HeadingLevel, string> = new Map([
|
|
||||||
[HeadingLevel.One, icon1],
|
|
||||||
[HeadingLevel.Two, icon2],
|
|
||||||
[HeadingLevel.Three, icon3],
|
|
||||||
[HeadingLevel.Four, icon4],
|
|
||||||
[HeadingLevel.Five, icon5],
|
|
||||||
[HeadingLevel.Six, icon6]
|
|
||||||
])
|
|
||||||
|
|
||||||
constructor (args: BlockToolArgs) {
|
|
||||||
super(args)
|
|
||||||
this._config = args.config as HeaderConfig
|
|
||||||
|
|
||||||
if (this._config.levels === undefined) {
|
|
||||||
this._config.levels = [HeadingLevel.Two, HeadingLevel.Three]
|
|
||||||
}
|
|
||||||
if (this._config.defaultLevel === undefined) {
|
|
||||||
this._config.defaultLevel = HeadingLevel.Two
|
|
||||||
}
|
|
||||||
if (this._config.levels.indexOf(this._config.defaultLevel) === -1) {
|
|
||||||
console.warn('(ง\'̀-\'́)ง Heading Tool: the default level specified was not found in available levels')
|
|
||||||
}
|
|
||||||
this.levels = this._config.levels
|
|
||||||
this.defaultLevel = this._config.defaultLevel
|
|
||||||
this.currentLevel = this.defaultLevel
|
|
||||||
|
|
||||||
// setting data will rerender the element with the right settings
|
|
||||||
this.data = {
|
|
||||||
level: this.currentLevel,
|
|
||||||
text: (args.data as HeaderData).text || ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public renderSettings (): HTMLElement {
|
|
||||||
const wrapper = document.createElement('DIV')
|
|
||||||
|
|
||||||
this.levels.forEach(level => {
|
|
||||||
const { settingsButton, settingsButtonActive } = this._CSS
|
|
||||||
const btn = document.createElement('SPAN')
|
|
||||||
btn.classList.add(settingsButton)
|
|
||||||
btn.dataset.level = `${level}`
|
|
||||||
btn.innerHTML = this.icons.get(level) || icon
|
|
||||||
|
|
||||||
if (this.currentLevel === level) btn.classList.add(settingsButtonActive)
|
|
||||||
|
|
||||||
btn.addEventListener('click', () => this.setLevel(level))
|
|
||||||
wrapper.appendChild(btn)
|
|
||||||
this.settingsButtons.push(btn)
|
|
||||||
})
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
}
|
|
||||||
|
|
||||||
public get data (): HeaderData {
|
|
||||||
return this._data as HeaderData
|
|
||||||
}
|
|
||||||
|
|
||||||
public set data (data: HeaderData) {
|
|
||||||
const currentData = this._data as HeaderData
|
|
||||||
|
|
||||||
if (data.level === undefined) data.level = currentData.level || this.defaultLevel
|
|
||||||
if (data.text === undefined) data.text = currentData.text || ''
|
|
||||||
|
|
||||||
this._data = data
|
|
||||||
this.currentLevel = data.level
|
|
||||||
|
|
||||||
const newHeader = this._render()
|
|
||||||
if (this._element.parentNode) {
|
|
||||||
this._element.parentNode.replaceChild(newHeader, this._element)
|
|
||||||
}
|
|
||||||
this._element = newHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
private setLevel (level: HeadingLevel) {
|
|
||||||
this.data = { level, text: this._element.innerHTML }
|
|
||||||
}
|
|
||||||
|
|
||||||
protected get _CSS () {
|
|
||||||
return {
|
|
||||||
block: this.api.styles.block,
|
|
||||||
settingsButton: this.api.styles.settingsButton,
|
|
||||||
settingsButtonActive: this.api.styles.settingsButtonActive,
|
|
||||||
wrapper: 'card-heading'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _render (): HTMLElement {
|
|
||||||
console.log('render', `H${this.currentLevel}`, this.data)
|
|
||||||
|
|
||||||
const el = document.createElement(`H${this.currentLevel}`)
|
|
||||||
el.innerHTML = this.data.text || ''
|
|
||||||
el.classList.add(this._CSS.wrapper, this._CSS.block)
|
|
||||||
el.contentEditable = 'true'
|
|
||||||
el.dataset.placeholder = this._config.placeholder || ''
|
|
||||||
return el
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle pasted H1-H6 tags to substitute with header tool
|
|
||||||
onPaste (event: HTMLPasteEvent) {
|
|
||||||
const content = event.detail.data
|
|
||||||
const text = content.innerHTML
|
|
||||||
let level = this.defaultLevel
|
|
||||||
|
|
||||||
const tagMatch = content.tagName.match(/H(\d)/)
|
|
||||||
if (tagMatch) level = parseInt(tagMatch[1], 10)
|
|
||||||
|
|
||||||
// Fallback to nearest level when specified not available
|
|
||||||
if (this._config.levels) {
|
|
||||||
level = this._config.levels.reduce((prevLevel, currLevel) => {
|
|
||||||
return Math.abs(currLevel - level) < Math.abs(prevLevel - level) ? currLevel : prevLevel
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.data = { level, text }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used by Editor.js paste handling API.
|
|
||||||
// Provides configuration to handle H1-H6 tags.
|
|
||||||
static get pasteConfig (): PasteConfig {
|
|
||||||
return {
|
|
||||||
tags: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method that specified how to merge two Text blocks.
|
|
||||||
// Called by Editor.js by backspace at the beginning of the Block
|
|
||||||
public merge (data: HeaderData) {
|
|
||||||
this.data = {
|
|
||||||
text: this.data.text + (data.text || ''),
|
|
||||||
level: this.data.level
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate text block data
|
|
||||||
validate (blockData: HeaderData): boolean {
|
|
||||||
if (!blockData.text) return false
|
|
||||||
return blockData.text.trim() !== ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract tools data from view
|
|
||||||
save (toolsContent: HTMLElement): HeaderData {
|
|
||||||
return {
|
|
||||||
text: toolsContent.innerHTML,
|
|
||||||
level: this.currentLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow Heading to be converted to/from other blocks
|
|
||||||
static get conversionConfig () {
|
|
||||||
return {
|
|
||||||
export: 'text', // use 'text' property for other blocks
|
|
||||||
import: 'text' // fill 'text' property from other block's export string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static get sanitize (): object {
|
|
||||||
return { level: {} }
|
|
||||||
}
|
|
||||||
|
|
||||||
static get toolbox () {
|
|
||||||
return { icon, title }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Heading as ToolConstructable
|
|
@ -1,2 +1,4 @@
|
|||||||
export { default as Delimiter } from './delimiter'
|
export { default as Delimiter } from './delimiter'
|
||||||
export { default as Heading } from './heading'
|
export { default as Heading } from './heading'
|
||||||
|
export { default as List } from './list'
|
||||||
|
export { default as Charges } from './charges'
|
||||||
|
@ -0,0 +1,254 @@
|
|||||||
|
import {
|
||||||
|
ContentBlock,
|
||||||
|
ContentBlockArgs,
|
||||||
|
ContentBlockConfig,
|
||||||
|
ContentBlockData,
|
||||||
|
ConversionConfig
|
||||||
|
} from './content-block'
|
||||||
|
|
||||||
|
import icon from '../assets/editor/list.svg.txt'
|
||||||
|
import iconUL from '../assets/editor/list_unordered.svg.txt'
|
||||||
|
import iconOL from '../assets/editor/list_ordered.svg.txt'
|
||||||
|
|
||||||
|
const title = 'List'
|
||||||
|
|
||||||
|
enum ListStyle {
|
||||||
|
Ordered = 'OL',
|
||||||
|
Unordered = 'UL'
|
||||||
|
}
|
||||||
|
const icons = {
|
||||||
|
[ListStyle.Ordered]: iconOL,
|
||||||
|
[ListStyle.Unordered]: iconUL
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListConfig extends ContentBlockConfig {
|
||||||
|
placeholder?: string;
|
||||||
|
styles: ListStyle[];
|
||||||
|
defaultStyle: ListStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListData extends ContentBlockData {
|
||||||
|
style: ListStyle;
|
||||||
|
items: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListConversionConfig extends ConversionConfig {
|
||||||
|
import: (str: string) => ListData;
|
||||||
|
export: (data: ListData) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class List extends ContentBlock {
|
||||||
|
static _supportedTags = ['UL', 'OL', 'LI']
|
||||||
|
static _toolboxConfig = { icon, title }
|
||||||
|
|
||||||
|
protected _config: ListConfig
|
||||||
|
private defaultStyle: ListStyle
|
||||||
|
private currentStyle: ListStyle
|
||||||
|
|
||||||
|
constructor (args: ContentBlockArgs) {
|
||||||
|
super(args)
|
||||||
|
this._config = args.config as ListConfig
|
||||||
|
|
||||||
|
if (this._config.styles === undefined) {
|
||||||
|
this._config.styles = [ListStyle.Unordered, ListStyle.Ordered]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._config.defaultStyle === undefined) {
|
||||||
|
this._config.defaultStyle = ListStyle.Unordered
|
||||||
|
}
|
||||||
|
if (this._config.styles.indexOf(this._config.defaultStyle) === -1) {
|
||||||
|
console.warn('(ง\'̀-\'́)ง List Tool: the default style specified was not found in available styles')
|
||||||
|
}
|
||||||
|
this.defaultStyle = this._config.defaultStyle
|
||||||
|
this.currentStyle = this.defaultStyle
|
||||||
|
|
||||||
|
this.data = {
|
||||||
|
style: this.currentStyle,
|
||||||
|
items: (args.data as ListData).items
|
||||||
|
}
|
||||||
|
|
||||||
|
this._settingButtons = this._config.styles.map(listStyle => {
|
||||||
|
return {
|
||||||
|
name: listStyle,
|
||||||
|
icon: icons[listStyle] || icon,
|
||||||
|
action: (name: string) => this.setStyle(name as ListStyle),
|
||||||
|
isActive: (name: string): boolean => name === this.currentStyle
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public get data (): ListData {
|
||||||
|
// first render
|
||||||
|
if (!this._element) return this._data as ListData
|
||||||
|
|
||||||
|
const items = this.queryItems()
|
||||||
|
const data = this._data as ListData
|
||||||
|
data.items = Array.from(items).map(item => item.innerText)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
public set data (data: ListData) {
|
||||||
|
const currentData = this._data as ListData
|
||||||
|
|
||||||
|
if (data.style === undefined) data.style = currentData.style || this.defaultStyle
|
||||||
|
if (data.items === undefined) data.items = currentData.items || []
|
||||||
|
|
||||||
|
this._data = data
|
||||||
|
this.currentStyle = data.style
|
||||||
|
|
||||||
|
const newList = this._render()
|
||||||
|
if (this._element.parentNode) {
|
||||||
|
this._element.parentNode.replaceChild(newList, this._element)
|
||||||
|
}
|
||||||
|
this._element = newList
|
||||||
|
}
|
||||||
|
|
||||||
|
private setStyle (style: ListStyle) {
|
||||||
|
this.data = { style, items: this.data.items || [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _render (): HTMLElement {
|
||||||
|
const el = document.createElement(this.currentStyle)
|
||||||
|
el.classList.add('cdx-list', this._CSS.block)
|
||||||
|
el.contentEditable = 'true'
|
||||||
|
el.dataset.placeholder = this._config.placeholder || ''
|
||||||
|
|
||||||
|
const items = this.data.items
|
||||||
|
|
||||||
|
if (!items || !items.length) {
|
||||||
|
const li = document.createElement('LI')
|
||||||
|
li.innerText = ''
|
||||||
|
li.classList.add('cdx-list__item')
|
||||||
|
el.appendChild(li)
|
||||||
|
} else {
|
||||||
|
items.forEach(item => {
|
||||||
|
const li = document.createElement('LI')
|
||||||
|
li.innerText = item
|
||||||
|
li.classList.add('cdx-list__item')
|
||||||
|
el.appendChild(li)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
el.addEventListener('keydown', event => {
|
||||||
|
// on pressing Enter
|
||||||
|
if (event.keyCode === 13) return this.getOutOfList(event)
|
||||||
|
// on pressing Backspace
|
||||||
|
if (event.keyCode === 8) return this.backspace(event)
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
|
private queryItems (): NodeListOf<HTMLLIElement> {
|
||||||
|
return this._element.querySelectorAll('.cdx-list__item')
|
||||||
|
}
|
||||||
|
|
||||||
|
private get currentItem (): Element | undefined {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (selection === null) return
|
||||||
|
|
||||||
|
let currentNode = selection.anchorNode
|
||||||
|
if (currentNode === null) return
|
||||||
|
|
||||||
|
if (currentNode.nodeType !== Node.ELEMENT_NODE) {
|
||||||
|
currentNode = currentNode.parentNode
|
||||||
|
}
|
||||||
|
|
||||||
|
return (currentNode as HTMLElement).closest('.cdx-list__item') || undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// leave list by pressing enter on an empty list item
|
||||||
|
private getOutOfList (event: KeyboardEvent) {
|
||||||
|
const items = this.queryItems()
|
||||||
|
if (items.length < 2) return
|
||||||
|
|
||||||
|
const lastItem = items[items.length - 1]
|
||||||
|
const currentItem = this.currentItem
|
||||||
|
|
||||||
|
// prevent generation of new li if last li is empty
|
||||||
|
if (currentItem === lastItem && !lastItem.innerText.trim().length) {
|
||||||
|
if (!currentItem.parentElement) return // somethings really wrong
|
||||||
|
currentItem.parentElement.removeChild(currentItem)
|
||||||
|
this.api.blocks.insertNewBlock()
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public backspace (event: KeyboardEvent) {
|
||||||
|
const items = this.queryItems()
|
||||||
|
const firstItem = items[0]
|
||||||
|
|
||||||
|
if (firstItem === undefined) return
|
||||||
|
if (items.length < 2 && !firstItem.innerText.trim()) event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectItem (event: KeyboardEvent) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (selection === null) return // no selection, no problem
|
||||||
|
|
||||||
|
const currentNode = selection.anchorNode?.parentNode
|
||||||
|
if (currentNode === null) return
|
||||||
|
|
||||||
|
const currentItem = (currentNode as Element).closest('.cdx-list__item')
|
||||||
|
if (currentItem === null) return
|
||||||
|
|
||||||
|
const range = new Range()
|
||||||
|
|
||||||
|
range.selectNodeContents(currentItem)
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(range)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected pasteHandler (element: HTMLUListElement | HTMLOListElement | HTMLLIElement): ListData {
|
||||||
|
const tag = element.tagName
|
||||||
|
const style = tag === ListStyle.Ordered ? ListStyle.Ordered : ListStyle.Unordered
|
||||||
|
const data: ListData = { style, items: [] }
|
||||||
|
|
||||||
|
if (tag === 'LI') { // does this really happen?
|
||||||
|
data.items.push(element.innerText)
|
||||||
|
} else {
|
||||||
|
const items = Array.from(element.querySelectorAll('LI'))
|
||||||
|
data.items = items.map(item => (item as HTMLElement).innerText).filter(item => !!item.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow to use native enter behavior
|
||||||
|
static get enableLineBreaks (): boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
static get sanitize () {
|
||||||
|
return {
|
||||||
|
style: {},
|
||||||
|
items: {
|
||||||
|
br: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get conversionConfig (): ListConversionConfig {
|
||||||
|
return {
|
||||||
|
export: data => {
|
||||||
|
return data.items.join(' * ')
|
||||||
|
},
|
||||||
|
import: str => {
|
||||||
|
return {
|
||||||
|
items: [str],
|
||||||
|
style: ListStyle.Unordered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public save (): ListData {
|
||||||
|
console.log('saving list', this.data)
|
||||||
|
return this.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default List
|
Loading…
Reference in New Issue