@@ -33,7 +37,6 @@
import { Component, Prop, Vue } from 'vue-property-decorator'
import { cardWHtoStyle, iconPath } from '@/lib'
import DeckCardEditor from '@/components/deck-card-editor.vue'
-import { selectLine } from '@/editor'
@Component({
components: { DeckCardEditor }
@@ -104,10 +107,6 @@ export default class DeckCard extends Vue {
return style
}
-
- private selectLine () {
- selectLine()
- }
}
@@ -201,6 +200,7 @@ export default class DeckCard extends Vue {
border-radius: 1rem;
font-size: 1.2rem;
color: black;
+ overflow: hidden;
}
.card-back {
diff --git a/src/editor/block-tool.ts b/src/editor/block-tool.ts
new file mode 100644
index 0000000..d2d6e6b
--- /dev/null
+++ b/src/editor/block-tool.ts
@@ -0,0 +1,45 @@
+import { BlockTool, BlockToolData, ToolConfig, ToolboxConfig, API } from '@editorjs/editorjs'
+
+export interface BlockToolArgs {
+ api: API;
+ config: ToolConfig;
+ data?: BlockToolData;
+}
+
+export class BlockToolExt implements BlockTool {
+ protected api: API
+ protected _element: HTMLElement
+ protected _data: object
+ protected _config: ToolConfig
+
+ constructor ({ data, config, api }: BlockToolArgs) {
+ this.api = api
+ this._config = config
+ this._data = data || {}
+ this._element = this._render()
+ }
+
+ protected get _CSS (): { [key: string]: string } {
+ return { block: this.api.styles.block }
+ }
+
+ protected _render (): HTMLElement {
+ const el = document.createElement('DIV')
+ el.classList.add(this._CSS.block)
+ return el
+ }
+
+ render (): HTMLElement {
+ return this._element
+ }
+
+ save (_toolsContent: HTMLElement): object {
+ return {}
+ }
+
+ static get toolbox (): ToolboxConfig {
+ return { icon: '', title: 'UnnamedPlugin' }
+ }
+}
+
+export default BlockToolExt
diff --git a/src/editor/caret.ts b/src/editor/caret.ts
deleted file mode 100644
index 04ab695..0000000
--- a/src/editor/caret.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { getFocussedNode } from './node'
-
-function applyRange (callback: (range: Range) => void) {
- const range = document.createRange()
- callback(range)
-
- const sel = window.getSelection()
- if (sel) {
- sel.removeAllRanges()
- sel.addRange(range)
- }
-}
-function collapseRange (node: Node, toStart = false) {
- applyRange(range => {
- range.selectNode(node)
- range.collapse(toStart)
- })
-}
-
-export function moveCaretToBOL () {
- const node = getFocussedNode()
- if (node) collapseRange(node, true)
-}
-export function moveCaretToEOL () {
- const node = getFocussedNode()
- if (node) collapseRange(node, false)
-}
-export function selectLine () {
- const node = getFocussedNode()
- if (node) {
- applyRange(range => range.selectNodeContents(node))
- }
-}
diff --git a/src/editor/constants.ts b/src/editor/constants.ts
deleted file mode 100644
index 2891990..0000000
--- a/src/editor/constants.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-export const movementKeys = [
- 'ArrowLeft',
- 'ArrowRight',
- 'ArrowUp',
- 'ArrowDown',
- 'Delete',
- 'Backspace',
- 'Enter',
- 'Home',
- 'End',
- 'PageUp',
- 'PageDown'
-]
-
-export const controlSequenceKeys = ['p', 'x', 'y', 'z', 'Z']
-
-export const elementNameToMenuState: KV = {
- B: 'bold',
- STRONG: 'bold',
- I: 'italic',
- EM: 'italic',
- P: 'paragraph',
- H1: 'heading1',
- H2: 'heading2',
- H3: 'heading3',
- UL: 'bulletList',
- OL: 'numberedList',
- HR: 'separator'
-}
-
-export const marks = ['bold', 'italic']
-export const blocks = [
- 'paragraph',
- 'heading1',
- 'heading2',
- 'heading3',
- 'bulletList',
- 'spacer',
- 'separator',
- 'statBlock'
-]
diff --git a/src/editor/delimiter.ts b/src/editor/delimiter.ts
new file mode 100644
index 0000000..4cb8c71
--- /dev/null
+++ b/src/editor/delimiter.ts
@@ -0,0 +1,29 @@
+import { ToolConstructable } from '@editorjs/editorjs'
+import BlockTool from './block-tool'
+import icon from '../assets/editor/delimiter.svg.txt'
+const title = 'Delimiter'
+
+export class Delimiter extends BlockTool {
+ static get contentless () {
+ return true
+ }
+
+ protected get _CSS () {
+ return {
+ block: this.api.styles.block,
+ wrapper: 'card-delimiter'
+ }
+ }
+
+ protected _render (): HTMLElement {
+ const el = document.createElement('HR')
+ el.classList.add(this._CSS.wrapper, this._CSS.block)
+ return el
+ }
+
+ static get toolbox () {
+ return { icon, title }
+ }
+}
+
+export default Delimiter as ToolConstructable
diff --git a/src/editor/heading.ts b/src/editor/heading.ts
new file mode 100644
index 0000000..7c1836c
--- /dev/null
+++ b/src/editor/heading.ts
@@ -0,0 +1,202 @@
+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'
+
+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 () {
+ 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 3bacda8..61f8ae3 100644
--- a/src/editor/index.ts
+++ b/src/editor/index.ts
@@ -1,81 +1,2 @@
-import { elementNameToMenuState, marks, blocks } from './constants'
-
-export {
- isRootNode,
- isRootChild,
- isElementNode,
- isTextNode,
- isEmptyTextNode,
- getFocussedNode
-} from './node'
-
-export {
- moveCaretToBOL,
- moveCaretToEOL,
- selectLine
-} from './caret'
-
-export type State = KV
-export {
- movementKeys,
- controlSequenceKeys,
- marks,
- blocks
-} from './constants'
-
-function simpleAction (cmd: string, arg?: string): () => boolean {
- return () => {
- return document.execCommand(cmd, false, arg)
- }
-}
-
-export const menuActionToCommand: KV<() => boolean> = {
- paragraph: simpleAction('formatblock', 'P'),
- heading1: simpleAction('formatblock', 'H1'),
- heading2: simpleAction('formatblock', 'H2'),
- heading3: simpleAction('formatblock', 'H3'),
- bulletList: simpleAction('insertUnorderedList'),
- numberedList: simpleAction('insertOrderedList'),
- separator: simpleAction('insertHorizontalRule'),
- bold: simpleAction('bold'),
- italic: simpleAction('italic')
-}
-
-export function getActiveMarksAndBlocks (el: HTMLElement): {
- marks: string[];
- block: string;
-} {
- let activeBlock = 'paragraph'
- const activeMarks: string[] = []
-
- const focussedEl = el.nodeName === '#text' ? el.parentElement : el
- if (!focussedEl) return { marks: activeMarks, block: activeBlock }
-
- const focussedState = elementNameToMenuState[focussedEl.nodeName]
- if (!focussedState) return { marks: activeMarks, block: activeBlock }
-
- if (blocks.indexOf(focussedState) >= 0) {
- activeBlock = focussedState
- return { marks: activeMarks, block: activeBlock }
- }
-
- let wrappingEl = focussedEl.parentElement
- let wrappingState: string
-
- if (marks.indexOf(focussedState) >= 0) {
- activeMarks.push(focussedState)
-
- while (wrappingEl) {
- wrappingState = elementNameToMenuState[wrappingEl.nodeName]
- if (marks.indexOf(wrappingState) < 0) {
- if (blocks.indexOf(wrappingState) >= 0) activeBlock = wrappingState
- break
- }
-
- activeMarks.push(wrappingState)
- wrappingEl = wrappingEl.parentElement
- }
- }
-
- return { marks: activeMarks, block: activeBlock }
-}
+export { default as Delimiter } from './delimiter'
+export { default as Heading } from './heading'
diff --git a/src/editor/node.ts b/src/editor/node.ts
deleted file mode 100644
index b7524b2..0000000
--- a/src/editor/node.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-const { TEXT_NODE, ELEMENT_NODE } = Node
-export function getFocussedNode (): Node | null {
- return window.getSelection()?.focusNode || null
-}
-export function isTextNode ({ nodeType }: Node): boolean {
- return nodeType === TEXT_NODE
-}
-export function isElementNode ({ nodeType }: Node): boolean {
- return nodeType === ELEMENT_NODE
-}
-export function isEmptyTextNode (node: Node): boolean {
- return isTextNode(node) && (node as CharacterData).data === ''
-}
-export function isRootNode (node: Node): boolean {
- return (node as HTMLElement).contentEditable === 'true'
-}
-export function isRootChild (node: Node): boolean {
- // TODO: maybe use a data attribute or something for saver identification
- return node.parentElement?.contentEditable === 'true'
-}
diff --git a/src/modules.d.ts b/src/modules.d.ts
new file mode 100644
index 0000000..a6d311e
--- /dev/null
+++ b/src/modules.d.ts
@@ -0,0 +1,12 @@
+declare module '*.vue' {
+ import Vue from 'vue'
+ export default Vue
+}
+
+declare module '*.txt' {
+ const content: string
+ export default content
+}
+
+declare module '@editorjs/paragraph'
+declare module '@editorjs/list'
diff --git a/src/shims-vue.d.ts b/src/shims-vue.d.ts
deleted file mode 100644
index d9f24fa..0000000
--- a/src/shims-vue.d.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-declare module '*.vue' {
- import Vue from 'vue'
- export default Vue
-}
diff --git a/src/shims.d.ts b/src/types.d.ts
similarity index 100%
rename from src/shims.d.ts
rename to src/types.d.ts
diff --git a/vue.config.js b/vue.config.js
new file mode 100644
index 0000000..8993484
--- /dev/null
+++ b/vue.config.js
@@ -0,0 +1,10 @@
+module.exports = {
+ chainWebpack: config => {
+ config.module
+ .rule('raw')
+ .test(/\.txt$/)
+ .use('raw-loader')
+ .loader('raw-loader')
+ .end()
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index 9c95d6b..7ae3188 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -770,6 +770,19 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
+"@editorjs/editorjs@^2.17.0":
+ version "2.17.0"
+ resolved "https://registry.yarnpkg.com/@editorjs/editorjs/-/editorjs-2.17.0.tgz#38f20d7f99bc21868904b6b937905b6daad5a2a2"
+ integrity sha512-5rMjZLdiFOiUGESe5MZagvuVaLggORXBEolbbDLLVWHslR+r4+TACOXBcN8A6m9hMmnpHIJsC3442MZEWdNfQA==
+ dependencies:
+ codex-notifier "^1.1.2"
+ codex-tooltip "^1.0.0"
+
+"@editorjs/list@^1.4.0":
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/@editorjs/list/-/list-1.4.0.tgz#e92459a8ac2305bc4385245e329c8b5c8437456a"
+ integrity sha512-iYDXGbVXvsAJbSxbjFMP4p7kS1zhQyNDqVNzkfMRhItulzKYlOMlFjTIGHqu5SxPy6NrcckhVFaWdfGDn5/gEA==
+
"@hapi/address@2.x.x":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
@@ -2359,6 +2372,16 @@ code-point-at@^1.0.0:
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
+codex-notifier@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/codex-notifier/-/codex-notifier-1.1.2.tgz#a733079185f4c927fa296f1d71eb8753fe080895"
+ integrity sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg==
+
+codex-tooltip@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/codex-tooltip/-/codex-tooltip-1.0.0.tgz#720353b27fadc40f2d054d171479b016ffcb63ea"
+ integrity sha512-Wa/p/om166GVjg+q436BERBZZz3yvTnCDDzMV2kjKIzsUkj6vCWphTSTo+M0QJRfwODKzhXYaw8+S4EXPW6r0g==
+
collection-visit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
@@ -6234,11 +6257,6 @@ ora@^3.4.0:
strip-ansi "^5.2.0"
wcwidth "^1.0.1"
-orderedmap@^1.1.0:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-1.1.1.tgz#c618e77611b3b21d0fe3edc92586265e0059c789"
- integrity sha512-3Ux8um0zXbVacKUkcytc0u3HgC0b0bBLT+I60r2J/En72cI0nZffqrA7Xtf2Hqs27j1g82llR5Mhbd0Z1XW4AQ==
-
original@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f"
@@ -7004,13 +7022,6 @@ promise-inflight@^1.0.1:
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
-prosemirror-model@1.8.2:
- version "1.8.2"
- resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.8.2.tgz#c74eaacb0bbfea49b59a6d89fef5516181666a56"
- integrity sha512-piffokzW7opZVCjf/9YaoXvTC0g7zMRWKJib1hpphPfC+4x6ZXe5CiExgycoWZJe59VxxP7uHX8aFiwg2i9mUQ==
- dependencies:
- orderedmap "^1.1.0"
-
proxy-addr@~2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
@@ -7154,6 +7165,14 @@ raw-body@2.4.0:
iconv-lite "0.4.24"
unpipe "1.0.0"
+raw-loader@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.0.tgz#d639c40fb9d72b5c7f8abc1fb2ddb25b29d3d540"
+ integrity sha512-iINUOYvl1cGEmfoaLjnZXt4bKfT2LJnZZib5N/LLyAphC+Dd11vNP9CNVb38j+SAJpFI1uo8j9frmih53ASy7Q==
+ dependencies:
+ loader-utils "^1.2.3"
+ schema-utils "^2.5.0"
+
read-pkg-up@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"