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)
own-rich-text-solution
koehr 5 years ago
parent 18e043baad
commit 2085e22688

@ -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 () {

@ -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')
}

@ -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…
Cancel
Save