diff --git a/src/editor/index.css b/src/editor/index.css index 8cf160cb..d10c51f3 100644 --- a/src/editor/index.css +++ b/src/editor/index.css @@ -24,6 +24,10 @@ a { pointer-events: none; } +hr { + cursor: default; +} + .ag-emoji-box { position: absolute; display: none; diff --git a/src/editor/index.js b/src/editor/index.js index 0b9e96e3..850ecae9 100644 --- a/src/editor/index.js +++ b/src/editor/index.js @@ -3,7 +3,8 @@ import { operateClassName, insertBefore, insertAfter, removeNode, isFirstChildElement, wrapperElementWithTag, nestElementWithTag, isOnlyChildElement, isLastChildElement, chopBlockQuote, removeAndInsertBefore, removeAndInsertPreList, replaceElement, - replacementLists, insertBeforeBlockQuote, isAganippeEditorElement + replacementLists, insertBeforeBlockQuote, isAganippeEditorElement, + findOutMostParagraph } from './utils/domManipulate' import { @@ -16,7 +17,7 @@ import { } from './utils' import { - CLASS_OR_ID, EVENT_KEYS, LOWERCASE_TAGS + CLASS_OR_ID, LOWERCASE_TAGS, EVENT_KEYS } from './config' import Selection from './selection' @@ -57,12 +58,11 @@ class Aganippe { eventCenter.subscribe('elementUpdate', this.subscribeElementUpdate.bind(this)) this.dispatchElementUpdate() - eventCenter.subscribe('arrow', this.subscribeArrow.bind(this)) - this.dispatchArrow() - eventCenter.bind('enter', this.enterKeyHandler.bind(this)) eventCenter.bind('backspace', this.backspaceKeyHandler.bind(this)) + this.handlerSelectHr() + this.generateLastEmptyParagraph() } /** @@ -103,6 +103,10 @@ class Aganippe { dispatchEditeEmoji () { const { container, eventCenter } = this const changeHandler = event => { + const target = event.target + if (event.type === 'click' && target.tagName.toLowerCase() === LOWERCASE_TAGS.hr) { + return false + } const node = selection.getSelectionStart() Promise.resolve() .then(() => { @@ -143,7 +147,10 @@ class Aganippe { dispatchMarkedText () { const { container, eventCenter } = this const changeHandler = event => { - // TODO: Handler hr + const target = event.target + if (event.type === 'click' && target.tagName.toLowerCase() === LOWERCASE_TAGS.hr) { + return false + } const node = selection.getSelectionStart() const paragraph = findNearestParagraph(node) const text = paragraph.textContent @@ -253,47 +260,59 @@ class Aganippe { paragraph: newElement } } - - dispatchArrow () { - const { eventCenter, container } = this - const changeHandler = event => { - if (event.key) { - if ( - event.key === EVENT_KEYS.ArrowLeft || - event.key === EVENT_KEYS.ArrowRight || - event.key === EVENT_KEYS.ArrowUp || - event.key === EVENT_KEYS.ArrowDown - ) { - eventCenter.dispatch('arrow', event) + // add handler to select hr element. and translate it to a p element + handlerSelectHr () { + const { container, eventCenter } = this + let newElement + const changeHr2P = (event, target, preParagraph) => { + newElement = updateBlock(target, LOWERCASE_TAGS.p) + newElement.textContent = '---' + selection.importSelection({ + start: 3, + end: 3 + }, newElement) + this.activeParagraph = { + id: newElement.id, + paragraph: newElement + } + eventCenter.dispatch('paragraphChange', newElement, preParagraph) + event.preventDefault() + } + const handler = event => { + switch (event.type) { + case 'click': { + const target = event.target + if (target.tagName.toLowerCase() === LOWERCASE_TAGS.hr) { + changeHr2P(event, target, this.activeParagraph.paragraph) + } + break + } + case 'keydown': { + const node = selection.getSelectionStart() + const outmostParagraph = findOutMostParagraph(node) + const preSibling = outmostParagraph.previousElementSibling + const nextSibling = outmostParagraph.nextElementSibling + if ( + event.key === EVENT_KEYS.ArrowUp && + preSibling && + preSibling.tagName.toLowerCase() === LOWERCASE_TAGS.hr + ) { + changeHr2P(event, preSibling, outmostParagraph) + } + if ( + event.key === EVENT_KEYS.ArrowDown && + nextSibling && + nextSibling.tagName.toLowerCase() === LOWERCASE_TAGS.hr + ) { + changeHr2P(event, nextSibling, outmostParagraph) + } + break } } } - eventCenter.attachDOMEvent(container, 'keydown', changeHandler) - } - subscribeArrow () { - // switch (event.key) { - // case EVENT_KEYS.ArrowUp: - // if (this.activeEmojiItem > 0) { - // event.preventDefault() - // this.activeEmojiItem = this.activeEmojiItem - 1 - // if (this.emojiList) { - // this.emoji.box.setOptions(this.emojiList, this.activeEmojiItem) - // } - // } - // break - // case EVENT_KEYS.ArrowDown: - // console.log(this.activeEmojiItem, this.emojiList.length) - // if (this.activeEmojiItem < this.emojiList.length - 1) { - // event.preventDefault() - // this.activeEmojiItem = this.activeEmojiItem + 1 - // if (this.emojiList) { - // this.emoji.box.setOptions(this.emojiList, this.activeEmojiItem) - // } - // } - // break - // } - // TODO + eventCenter.attachDOMEvent(container, 'keydown', handler) + eventCenter.attachDOMEvent(container, 'click', handler) } dispatchParagraphChange () { @@ -333,16 +352,19 @@ class Aganippe { break } case LOWERCASE_TAGS.hr: { - updateBlock(oldParagraph, LOWERCASE_TAGS.hr) + oldParagraph = updateBlock(oldParagraph, LOWERCASE_TAGS.hr) break } } } else { - // set and remove active className - operateClassName(oldParagraph, 'remove', CLASS_OR_ID['AG_ACTIVE']) - operateClassName(newParagraph, 'add', CLASS_OR_ID['AG_ACTIVE']) - oldParagraph.innerHTML = markedText2Html(oldParagraph.textContent) + if (oldContext) { + oldParagraph.innerHTML = markedText2Html(oldParagraph.textContent) + } } + console.log(newParagraph, oldParagraph) + // set and remove active className + operateClassName(oldParagraph, 'remove', CLASS_OR_ID['AG_ACTIVE']) + operateClassName(newParagraph, 'add', CLASS_OR_ID['AG_ACTIVE']) } enterKeyHandler (event) { diff --git a/src/editor/syntax/index.js b/src/editor/syntax/index.js index 5845ef0e..e8e43911 100644 --- a/src/editor/syntax/index.js +++ b/src/editor/syntax/index.js @@ -14,19 +14,20 @@ import { */ const fragments = [ - '^#{1,6}', // Header + '^#{1,6}[^#]?', // Header '(\\*{1,3}|_{1,3})[^*_]+\\1', // Emphasize '(`{1,3})([^`]+?|.{2,})\\2', // inline code '\\[[^\\[\\]]+\\]\\(.*?\\)', // link '\\[\\]\\([^\\(\\)]*?\\)', // no text link ':[^:]+?:', // emoji '~{2}[^~]+~{2}', // line through - 'https?://[^\\s]+(?=\\s|$)' // auto link + 'https?://[^\\s]+(?=\\s|$)', // auto link + '^\\*{3,}|^\\-{3,}|^\\_{3,}' // hr ] const CHOP_REG = new RegExp(fragments.join('|'), 'g') // eslint-disable-line no-useless-escape const HEAD_REG_G = /^(#{1,6})([^#]*)$/g -const HEAD_REG = /^(#{1,6})([^#]*)$/ +const HEAD_REG = /^#{1,6}[^#]*$/ const EMPHASIZE_REG_G = /(\*{1,3}|_{1,3})([^*]+)(\1)/g const EMPHASIZE_REG = /(\*{1,3}|_{1,3})([^*]+)(\1)/ const INLINE_CODE_REG_G = /(`{1,3})([^`]+?|.{2,})(\1)/g @@ -42,6 +43,8 @@ const LINE_THROUGH_REG_G = /(~{2})([^~]+?)(~{2})/g const LINE_THROUGH_REG = /~{2}[^~]+?~{2}/ const AUTO_LINK_G = /(https?:\/\/[^\\s]+)(?=\s|$)/g const AUTO_LINK = /https?:\/\/[^\s]+(?=\s|$)/ +const HR_REG_G = /(^\*{3,}|^-{3,}|^_{3,})/g +const HR_REG = /^\*{3,}|^-{3,}|^_{3,}/ // const SIMPLE_LINK_G = /(<)([^<>]+?)(>)/g // const SIMPLE_LINK = /<[^<>]+?>/g const LINE_BREAK_BLOCK_REG = /^(?:`{3,}([^`]*))|[\*\-\_]{3,}/ // eslint-disable-line no-useless-escape @@ -56,6 +59,8 @@ const conflict = (arr1, arr2) => { } const chunk2html = ({ chunk, index, lastIndex }, { start, end } = {}) => { + // `###h` should be corrected to `###` to judge the confliction. + if (/^#{1,6}[^#]/.test(chunk)) lastIndex = lastIndex - 1 // if no positionState provided, no conflict. const isConflicted = start !== undefined && end !== undefined ? conflict([index, lastIndex], [start, end]) @@ -65,7 +70,11 @@ const chunk2html = ({ chunk, index, lastIndex }, { start, end } = {}) => { // handle head mark symble if (HEAD_REG.test(chunk)) { return chunk.replace(HEAD_REG_G, (match, p1, p2) => { - return `${p1}${p2}` + if (p2) { + return `${p1}${p2}` + } else { + return `${p1}${p2}` + } }) } @@ -182,6 +191,12 @@ const chunk2html = ({ chunk, index, lastIndex }, { start, end } = {}) => { }) } + if (HR_REG.test(chunk)) { + return chunk.replace(HR_REG_G, (match, p1) => { + return `${p1}` + }) + } + // handle picture // TODO // handle auto link: markdown text: `` @@ -227,7 +242,7 @@ export const markedText2Html = (markedText, positionState) => { result = result.replace(c.chunk, c.html) }) } - + console.log(chunks) return result } @@ -316,7 +331,6 @@ export const checkBackspaceCase = (startNode, selection) => { } if (parTagName === LOWERCASE_TAGS.blockquote && inLeft === 0) { if (isOnlyChildElement(nearestParagraph)) { - console.log('xx') return { type: 'BLOCKQUOTE', info: 'REPLACEMENT' } } else if (isFirstChildElement(nearestParagraph)) { return { type: 'BLOCKQUOTE', info: 'INSERT_BEFORE' } @@ -335,7 +349,7 @@ export const checkLineBreakUpdate = text => { switch (true) { case /^`{3,}.*/.test(match): return { type: 'pre', info: token[1] } - case /^[*_-]{3,}/.test(match): + case HR_REG.test(match): return { type: 'hr' } default: return false diff --git a/src/editor/utils/domManipulate.js b/src/editor/utils/domManipulate.js index a7aa4d9e..8b1b5d49 100644 --- a/src/editor/utils/domManipulate.js +++ b/src/editor/utils/domManipulate.js @@ -347,7 +347,7 @@ export const updateBlock = (origin, tagName) => { const json = html2json(origin.outerHTML) json.child[0].tag = tagName - if (/^h/.test(tagName)) { + if (/^h\d$/.test(tagName)) { json.child[0].attr['data-head-level'] = tagName } else if (json.child[0].attr['data-head-level']) { delete json.child[0].attr['data-head-level']