Fixes problems with bogus root level text nodes
Depending on the browser in different situations the root node itself is selected and new text ends up in a text node on root level instead of a new paragraph. This happens in: * Firefox: after inserting a closed block like a horizontal rule * Chromium: after inserting or selecting such a closed block Now instead of inserting a paragraph directly after inserting an HR, the editor simply checks for normal text input inside the root node and wraps the newly written text with a paragraph (and moves the caret to the end of the paragraph because chromium moves it to the beginning of the line)
This commit is contained in:
parent
3a57abc423
commit
738dce4884
4 changed files with 76 additions and 12 deletions
|
@ -33,7 +33,10 @@ import {
|
|||
getActiveMarksAndBlocks,
|
||||
State,
|
||||
movementKeys,
|
||||
controlSequenceKeys
|
||||
controlSequenceKeys,
|
||||
isRootNode,
|
||||
isTextNode,
|
||||
moveCaretToEOL
|
||||
} from '@/editor'
|
||||
|
||||
@Component({
|
||||
|
@ -76,7 +79,6 @@ export default class DeckCardEditor extends Vue {
|
|||
}
|
||||
|
||||
private editorAction (action: string) {
|
||||
console.log('action', action)
|
||||
const content = this.$refs.content as HTMLElement
|
||||
content.focus()
|
||||
|
||||
|
@ -104,7 +106,27 @@ export default class DeckCardEditor extends Vue {
|
|||
// arrow keys, enter, delete, etc
|
||||
const isMove = movementKeys.indexOf(event.key) >= 0
|
||||
|
||||
if (isCtrlSq || isMove) this.syncMenuState()
|
||||
if (isCtrlSq || isMove) {
|
||||
return this.syncMenuState()
|
||||
} else if (!event.ctrlKey && event.key.length === 1) {
|
||||
// this should capture all normal typable letters and numbers
|
||||
// TODO: this needs to be done on text pasting as well
|
||||
|
||||
// some browsers create bogus root level text nodes, so whenever
|
||||
// something is typed in such a root level node, we simply wrap it with
|
||||
// a paragraph
|
||||
const sel = window.getSelection()?.focusNode
|
||||
if (sel && isTextNode(sel) && isRootNode(sel.parentElement as HTMLElement)) {
|
||||
console.debug(`Typed letter "${event.key} into root node, throwing a <p> at it!"`)
|
||||
document.execCommand('formatblock', false, 'P')
|
||||
|
||||
// Firefox behaves nicely and leaves the caret alone after surrounding
|
||||
// the text node with a <p>. Unlike Chromium that moves the caret to
|
||||
// the beginning of the new paragraph. To mitigate that, we set the
|
||||
// caret to end-of-line manually.
|
||||
moveCaretToEOL()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private start () {
|
||||
|
|
19
src/editor/caret.ts
Normal file
19
src/editor/caret.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
function collapseRange (node: Node, toStart = false) {
|
||||
const range = document.createRange()
|
||||
range.selectNode(node)
|
||||
range.collapse(toStart)
|
||||
const sel = window.getSelection()
|
||||
if (sel) {
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(range)
|
||||
}
|
||||
}
|
||||
|
||||
export function moveCaretToBOL () {
|
||||
const node = window.getSelection()?.focusNode
|
||||
if (node) collapseRange(node, true)
|
||||
}
|
||||
export function moveCaretToEOL () {
|
||||
const node = window.getSelection()?.focusNode
|
||||
if (node) collapseRange(node, false)
|
||||
}
|
|
@ -1,5 +1,18 @@
|
|||
import { elementNameToMenuState, marks, blocks } from './constants'
|
||||
|
||||
export {
|
||||
isRootNode,
|
||||
isRootChild,
|
||||
isElementNode,
|
||||
isTextNode,
|
||||
isEmptyTextNode
|
||||
} from './node'
|
||||
|
||||
export {
|
||||
moveCaretToBOL,
|
||||
moveCaretToEOL
|
||||
} from './caret'
|
||||
|
||||
export type State = KV<boolean>
|
||||
export {
|
||||
movementKeys,
|
||||
|
@ -14,14 +27,6 @@ function simpleAction (cmd: string, arg?: string): () => boolean {
|
|||
}
|
||||
}
|
||||
|
||||
function insertHorizontalRule (): () => boolean {
|
||||
return () => {
|
||||
const hr = document.execCommand('insertHorizontalRule')
|
||||
const p = document.execCommand('formatblock', false, 'P')
|
||||
return hr && p
|
||||
}
|
||||
}
|
||||
|
||||
export const menuActionToCommand: KV<() => boolean> = {
|
||||
paragraph: simpleAction('formatblock', 'P'),
|
||||
heading1: simpleAction('formatblock', 'H1'),
|
||||
|
@ -29,7 +34,7 @@ export const menuActionToCommand: KV<() => boolean> = {
|
|||
heading3: simpleAction('formatblock', 'H3'),
|
||||
bulletList: simpleAction('insertUnorderedList'),
|
||||
numberedList: simpleAction('insertOrderedList'),
|
||||
separator: insertHorizontalRule(),
|
||||
separator: simpleAction('insertHorizontalRule'),
|
||||
bold: simpleAction('bold'),
|
||||
italic: simpleAction('italic')
|
||||
}
|
||||
|
|
18
src/editor/node.ts
Normal file
18
src/editor/node.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
const { TEXT_NODE, ELEMENT_NODE } = Node
|
||||
|
||||
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'
|
||||
}
|
Loading…
Reference in a new issue