From ef8dacd89ec433fa652ed17d89cb5333889fcfc8 Mon Sep 17 00:00:00 2001 From: koehr Date: Sun, 12 Apr 2020 14:29:28 +0200 Subject: [PATCH] finishes generalized contentless and content block classes --- src/components/deck-card-editor.vue | 6 +- src/editor/content-block.ts | 60 ++++++-- src/editor/contentless-block.ts | 16 +-- src/editor/delimiter.ts | 3 +- src/editor/heading.ts | 126 ++++++----------- src/editor/heading.ts.bak | 207 ++++++++++++++++++++++++++++ src/editor/index.ts | 3 +- 7 files changed, 313 insertions(+), 108 deletions(-) create mode 100644 src/editor/heading.ts.bak diff --git a/src/components/deck-card-editor.vue b/src/components/deck-card-editor.vue index fd0928e..1d75551 100644 --- a/src/components/deck-card-editor.vue +++ b/src/components/deck-card-editor.vue @@ -7,7 +7,7 @@ import { Component, Prop, Vue } from 'vue-property-decorator' import Editor from '@editorjs/editorjs' import List from '@editorjs/list' -import { /* Heading, */ Delimiter, Boop } from '@/editor' +import { Heading, Delimiter } from '@/editor' @Component export default class DeckCardEditor extends Vue { @@ -28,8 +28,8 @@ export default class DeckCardEditor extends Vue { tools: { // header: Heading, list: { class: List, inlineToolbar: true }, - delimiter: { class: Delimiter, inlineToolbar: true }, - boop: { class: Boop, inlineToolbar: true } + delimiter: { class: Delimiter, inlineToolbar: false }, + heading: { class: Heading, inlineToolbar: true } }, // data: {}, placeholder: 'Click here to write your card.' diff --git a/src/editor/content-block.ts b/src/editor/content-block.ts index c37a5b0..d766cdd 100644 --- a/src/editor/content-block.ts +++ b/src/editor/content-block.ts @@ -1,15 +1,33 @@ -import { BlockTool, BlockToolData, ToolboxConfig, API, HTMLPasteEvent, ToolConstructable, ToolSettings } from '@editorjs/editorjs' -import icon from '@/assets/editor/text.svg.txt' +import { + BlockTool, + BlockToolData, + ToolboxConfig, + API, + HTMLPasteEvent, + ToolSettings, + SanitizerConfig +} from '@editorjs/editorjs' + +export { HTMLPasteEvent } from '@editorjs/editorjs' interface PasteConfig { tags: string[]; } -interface ContentBlockConfig extends ToolSettings { +export interface ContentBlockConfig extends ToolSettings { placeholder?: string; } -interface ContentBlockArgs { +export interface ContentBlockSettingButton { + name: string; + icon: string; + action: (name: string, event?: MouseEvent) => void; // action triggered by button + isActive?: (name: string) => boolean; // determine if current button is active +} + +export type ContentBlockSettings = ContentBlockSettingButton[] + +export interface ContentBlockArgs { api: API; config?: ContentBlockConfig; data?: BlockToolData; @@ -19,7 +37,7 @@ interface CSSClasses { [key: string]: string; } -interface ContentBlockData extends BlockToolData { +export interface ContentBlockData extends BlockToolData { text: string; } @@ -32,8 +50,7 @@ export class ContentBlock implements BlockTool { static _supportedTags: string[] = [] static _toolboxConfig: ToolboxConfig = { - // icon: '', - icon, + icon: '', title: 'UnnamedContentPlugin' } @@ -48,6 +65,7 @@ export class ContentBlock implements BlockTool { protected _placeholder: string protected _CSS: CSSClasses protected onKeyUp: (event: KeyboardEvent) => void + protected _settingButtons: ContentBlockSettings = [] constructor ({ data, config, api }: ContentBlockArgs) { this.api = api @@ -133,7 +151,7 @@ export class ContentBlock implements BlockTool { } // Sanitizer rules - static get sanitize () { + static get sanitize (): SanitizerConfig { return { text: { br: true } } @@ -151,6 +169,30 @@ export class ContentBlock implements BlockTool { this._element.innerHTML = this._data.text || '' } + public renderSettings (): HTMLElement { + const wrapper = document.createElement('DIV') + + this._settingButtons.forEach(tune => { + // make sure the settings button does something + if (!tune.icon || typeof tune.action !== 'function') return + + const { name, icon, action, isActive } = tune + + const btn = document.createElement('SPAN') + btn.classList.add(this.api.styles.settingsButton) + + if (typeof isActive === 'function' && isActive(name)) { + btn.classList.add(this.api.styles.settingsButtonActive) + } + btn.innerHTML = icon + btn.addEventListener('click', event => action(name, event)) + + wrapper.appendChild(btn) + }) + + return wrapper + } + // Used by Editor.js paste handling API. // Provides configuration to handle the tools tags. static get pasteConfig (): PasteConfig { @@ -165,4 +207,4 @@ export class ContentBlock implements BlockTool { } } -export default ContentBlock as ToolConstructable +export default ContentBlock diff --git a/src/editor/contentless-block.ts b/src/editor/contentless-block.ts index beb7872..d048a01 100644 --- a/src/editor/contentless-block.ts +++ b/src/editor/contentless-block.ts @@ -1,24 +1,24 @@ -import { BlockTool, BlockToolData, ToolConfig, ToolboxConfig, API } from '@editorjs/editorjs' - -interface BlockToolConfig extends ToolConfig { - [key: string]: string; -} +import { BlockTool, BlockToolData, ToolSettings, ToolboxConfig, API } from '@editorjs/editorjs' export interface BlockToolArgs { api: API; - config: BlockToolConfig; + config?: ToolSettings; data?: BlockToolData; } export class ContentlessBlock implements BlockTool { + static get contentless () { + return true + } + protected api: API protected _element: HTMLElement protected _data: object - protected _config: ToolConfig + protected _config: ToolSettings constructor ({ data, config, api }: BlockToolArgs) { this.api = api - this._config = config + this._config = config as ToolSettings this._data = data || {} this._element = this._render() } diff --git a/src/editor/delimiter.ts b/src/editor/delimiter.ts index 7bd9db6..324b1da 100644 --- a/src/editor/delimiter.ts +++ b/src/editor/delimiter.ts @@ -1,4 +1,3 @@ -import { ToolConstructable } from '@editorjs/editorjs' import ContentlessBlock from './contentless-block' import icon from '../assets/editor/delimiter.svg.txt' const title = 'Delimiter' @@ -22,4 +21,4 @@ export class Delimiter extends ContentlessBlock { } } -export default Delimiter as ToolConstructable +export default Delimiter diff --git a/src/editor/heading.ts b/src/editor/heading.ts index 4f857b2..a7daa70 100644 --- a/src/editor/heading.ts +++ b/src/editor/heading.ts @@ -1,5 +1,11 @@ -import { ToolConstructable, ToolConfig, HTMLPasteEvent } from '@editorjs/editorjs' -import { BlockToolExt, BlockToolArgs } from './block-tool' +import { + ContentBlock, + ContentBlockArgs, + ContentBlockConfig, + ContentBlockData, + HTMLPasteEvent +} from './content-block' + import icon from '../assets/editor/header.svg.txt' import icon1 from '../assets/editor/header1.svg.txt' import icon2 from '../assets/editor/header2.svg.txt' @@ -7,11 +13,8 @@ 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[]; -} +const title = 'Heading' enum HeadingLevel { One = 1, @@ -22,35 +25,31 @@ enum HeadingLevel { Six = 6 } -interface HeaderConfig extends ToolConfig { +const icons = [null, icon1, icon2, icon3, icon4, icon5, icon6] + +interface HeadingConfig extends ContentBlockConfig { placeholder?: string; levels?: HeadingLevel[]; defaultLevel?: HeadingLevel; } -interface HeaderData { - text?: string; +interface HeaderData extends ContentBlockData { + text: string; level?: HeadingLevel; } -class Heading extends BlockToolExt { - protected _config: HeaderConfig - protected settingsButtons: HTMLElement[] = [] - private levels: HeadingLevel[] +class Heading extends ContentBlock { + static _supportedTags = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'] + static _toolboxConfig = { icon, title } + + protected _config: HeadingConfig private defaultLevel: HeadingLevel private currentLevel: HeadingLevel - private icons: Map = new Map([ - [HeadingLevel.One, icon1], - [HeadingLevel.Two, icon2], - [HeadingLevel.Three, icon3], - [HeadingLevel.Four, icon4], - [HeadingLevel.Five, icon5], - [HeadingLevel.Six, icon6] - ]) - - constructor (args: BlockToolArgs) { + + constructor (args: ContentBlockArgs) { super(args) - this._config = args.config as HeaderConfig + this._config = args.config as HeadingConfig + this._CSS.wrapper = 'card-content-block' if (this._config.levels === undefined) { this._config.levels = [HeadingLevel.Two, HeadingLevel.Three] @@ -61,7 +60,6 @@ class Heading extends BlockToolExt { 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 @@ -70,30 +68,19 @@ class Heading extends BlockToolExt { 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) + this._settingButtons = this._config.levels.map(level => { + return { + name: `H${level}`, + icon: icons[level] || icon, + action: (name: string) => this.setLevel(name), + isActive: (name: string): boolean => this.isCurrentLevel(name) + } }) - - return wrapper } - public get data (): HeaderData { - return this._data as HeaderData + public get data () { + return this._data } public set data (data: HeaderData) { @@ -112,17 +99,14 @@ class Heading extends BlockToolExt { this._element = newHeader } - private setLevel (level: HeadingLevel) { - this.data = { level, text: this._element.innerHTML } + private isCurrentLevel (name: string): boolean { + const currentLevel = `H${this.currentLevel}` + return name === currentLevel } - protected get _CSS () { - return { - block: this.api.styles.block, - settingsButton: this.api.styles.settingsButton, - settingsButtonActive: this.api.styles.settingsButtonActive, - wrapper: 'card-heading' - } + private setLevel (name: string) { + const level = parseInt(name[1], 10) + this.data = { level, text: this._element.innerHTML } } protected _render (): HTMLElement { @@ -137,7 +121,7 @@ class Heading extends BlockToolExt { } // Handle pasted H1-H6 tags to substitute with header tool - onPaste (event: HTMLPasteEvent) { + public onPaste (event: HTMLPasteEvent) { const content = event.detail.data const text = content.innerHTML let level = this.defaultLevel @@ -155,14 +139,6 @@ class Heading extends BlockToolExt { 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) { @@ -172,35 +148,17 @@ class Heading extends BlockToolExt { } } - // 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 { + public 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 { + static get sanitize () { return { level: {} } } - - static get toolbox () { - return { icon, title } - } } -export default Heading as ToolConstructable +export default Heading diff --git a/src/editor/heading.ts.bak b/src/editor/heading.ts.bak new file mode 100644 index 0000000..22d5010 --- /dev/null +++ b/src/editor/heading.ts.bak @@ -0,0 +1,207 @@ +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 = 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 diff --git a/src/editor/index.ts b/src/editor/index.ts index 7b1ec63..61f8ae3 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -1,3 +1,2 @@ export { default as Delimiter } from './delimiter' -// export { default as Heading } from './heading' -export { default as Boop } from './content-block' +export { default as Heading } from './heading'