diff --git a/src/assets/editor/text.svg.txt b/src/assets/editor/text.svg.txt new file mode 100644 index 0000000..aeff875 --- /dev/null +++ b/src/assets/editor/text.svg.txt @@ -0,0 +1,3 @@ + + + diff --git a/src/components/deck-card-editor.vue b/src/components/deck-card-editor.vue index 4f42cfe..fd0928e 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 } from '@/editor' +import { /* Heading, */ Delimiter, Boop } from '@/editor' @Component export default class DeckCardEditor extends Vue { @@ -26,9 +26,10 @@ export default class DeckCardEditor extends Vue { holderId: this.id, autofocus: false, tools: { - header: Heading, - list: List, - delimiter: Delimiter + // header: Heading, + list: { class: List, inlineToolbar: true }, + delimiter: { class: Delimiter, inlineToolbar: true }, + boop: { class: Boop, inlineToolbar: true } }, // data: {}, placeholder: 'Click here to write your card.' diff --git a/src/editor/content-block.ts b/src/editor/content-block.ts new file mode 100644 index 0000000..c37a5b0 --- /dev/null +++ b/src/editor/content-block.ts @@ -0,0 +1,168 @@ +import { BlockTool, BlockToolData, ToolboxConfig, API, HTMLPasteEvent, ToolConstructable, ToolSettings } from '@editorjs/editorjs' +import icon from '@/assets/editor/text.svg.txt' + +interface PasteConfig { + tags: string[]; +} + +interface ContentBlockConfig extends ToolSettings { + placeholder?: string; +} + +interface ContentBlockArgs { + api: API; + config?: ContentBlockConfig; + data?: BlockToolData; +} + +interface CSSClasses { + [key: string]: string; +} + +interface ContentBlockData extends BlockToolData { + text: string; +} + +export class ContentBlock implements BlockTool { + // Default placeholder for Paragraph Tool + static get DEFAULT_PLACEHOLDER (): string { + return '' + } + + static _supportedTags: string[] = [] + + static _toolboxConfig: ToolboxConfig = { + // icon: '', + icon, + title: 'UnnamedContentPlugin' + } + + protected _defaultPlaceholder (): string { + return ContentBlock.DEFAULT_PLACEHOLDER + } + + protected api: API + protected _element: HTMLElement + protected _data: ContentBlockData + protected _config: ContentBlockConfig + protected _placeholder: string + protected _CSS: CSSClasses + protected onKeyUp: (event: KeyboardEvent) => void + + constructor ({ data, config, api }: ContentBlockArgs) { + this.api = api + this._config = config as ContentBlockConfig + + this._CSS = { + block: this.api.styles.block, + wrapper: 'card-content-block' + } + + this.onKeyUp = (event: KeyboardEvent) => this._onKeyUp(event) + + // Placeholder it is first Block + this._placeholder = config?.placeholder ? config.placeholder : this._defaultPlaceholder() + this._data = data as ContentBlockData + this._element = this._render() + } + + // Check if text content is empty and set empty string to inner html. + // We need this because some browsers (e.g. Safari) insert
into empty contenteditanle elements + _onKeyUp (event: KeyboardEvent) { + if (event.code !== 'Backspace' && event.code !== 'Delete') return + + if (this._element.textContent === '') { + this._element.innerHTML = '' + } + } + + // render tool view + // whenever a redraw is needed the result is saved in this._element + protected _render (): HTMLElement { + const el = document.createElement('DIV') + el.classList.add(this._CSS.wrapper, this._CSS.block) + el.dataset.placeholder = this._placeholder + el.addEventListener('keyup', this.onKeyUp) + el.innerHTML = this.data.text + el.contentEditable = 'true' + + return el + } + + // Return Tool's view + public render (): HTMLElement { + return this._element + } + + // Method that specified how to merge two Text blocks. + // Called by Editor.js by backspace at the beginning of the Block + public merge (data: ContentBlockData) { + this.data = { + text: this.data.text + data.text + } + } + + // Validate Paragraph block data (by default checks for emptiness) + public validate (savedData: ContentBlockData): boolean { + if (!savedData.text) return false + return savedData.text.trim() !== '' + } + + // Extract Tool's data from the view + public save (toolsContent: HTMLElement): ContentBlockData { + return { + text: toolsContent.innerHTML + } + } + + // On paste callback fired from Editor. + public onPaste (event: HTMLPasteEvent) { + this.data = { + text: event.detail.data.innerHTML + } + } + + /** + * Enable Conversion Toolbar. Paragraph can be converted to/from other tools + */ + static get conversionConfig () { + return { + export: 'text', // to convert Paragraph to other block, use 'text' property of saved data + import: 'text' // to covert other block's exported string to Paragraph, fill 'text' property of tool data + } + } + + // Sanitizer rules + static get sanitize () { + return { + text: { br: true } + } + } + + get data (): ContentBlockData { + const text = this._element?.innerHTML + if (text !== undefined) this._data.text = text + if (this._data.text === undefined) this._data.text = '' + return this._data + } + + set data (data: ContentBlockData) { + this._data = data || {} + this._element.innerHTML = this._data.text || '' + } + + // Used by Editor.js paste handling API. + // Provides configuration to handle the tools tags. + static get pasteConfig (): PasteConfig { + return { + tags: this._supportedTags + } + } + + // Icon and title for displaying at the Toolbox + static get toolbox (): ToolboxConfig { + return this._toolboxConfig + } +} + +export default ContentBlock as ToolConstructable diff --git a/src/editor/block-tool.ts b/src/editor/contentless-block.ts similarity index 83% rename from src/editor/block-tool.ts rename to src/editor/contentless-block.ts index d2d6e6b..beb7872 100644 --- a/src/editor/block-tool.ts +++ b/src/editor/contentless-block.ts @@ -1,12 +1,16 @@ import { BlockTool, BlockToolData, ToolConfig, ToolboxConfig, API } from '@editorjs/editorjs' +interface BlockToolConfig extends ToolConfig { + [key: string]: string; +} + export interface BlockToolArgs { api: API; - config: ToolConfig; + config: BlockToolConfig; data?: BlockToolData; } -export class BlockToolExt implements BlockTool { +export class ContentlessBlock implements BlockTool { protected api: API protected _element: HTMLElement protected _data: object @@ -42,4 +46,4 @@ export class BlockToolExt implements BlockTool { } } -export default BlockToolExt +export default ContentlessBlock diff --git a/src/editor/delimiter.ts b/src/editor/delimiter.ts index 4cb8c71..7bd9db6 100644 --- a/src/editor/delimiter.ts +++ b/src/editor/delimiter.ts @@ -1,13 +1,9 @@ import { ToolConstructable } from '@editorjs/editorjs' -import BlockTool from './block-tool' +import ContentlessBlock from './contentless-block' import icon from '../assets/editor/delimiter.svg.txt' const title = 'Delimiter' -export class Delimiter extends BlockTool { - static get contentless () { - return true - } - +export class Delimiter extends ContentlessBlock { protected get _CSS () { return { block: this.api.styles.block, diff --git a/src/editor/heading.ts b/src/editor/heading.ts index 7c1836c..4f857b2 100644 --- a/src/editor/heading.ts +++ b/src/editor/heading.ts @@ -9,6 +9,10 @@ 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, @@ -153,7 +157,7 @@ class Heading extends BlockToolExt { // Used by Editor.js paste handling API. // Provides configuration to handle H1-H6 tags. - static get pasteConfig () { + static get pasteConfig (): PasteConfig { return { tags: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'] } diff --git a/src/editor/index.ts b/src/editor/index.ts index 61f8ae3..7b1ec63 100644 --- a/src/editor/index.ts +++ b/src/editor/index.ts @@ -1,2 +1,3 @@ export { default as Delimiter } from './delimiter' -export { default as Heading } from './heading' +// export { default as Heading } from './heading' +export { default as Boop } from './content-block'