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
18e043baad
commit
2085e22688
4 changed files with 76 additions and 12 deletions
|
@ -33,7 +33,10 @@ import {
|
||||||
getActiveMarksAndBlocks,
|
getActiveMarksAndBlocks,
|
||||||
State,
|
State,
|
||||||
movementKeys,
|
movementKeys,
|
||||||
controlSequenceKeys
|
controlSequenceKeys,
|
||||||
|
isRootNode,
|
||||||
|
isTextNode,
|
||||||
|
moveCaretToEOL
|
||||||
} from '@/editor'
|
} from '@/editor'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -76,7 +79,6 @@ export default class DeckCardEditor extends Vue {
|
||||||
}
|
}
|
||||||
|
|
||||||
private editorAction (action: string) {
|
private editorAction (action: string) {
|
||||||
console.log('action', action)
|
|
||||||
const content = this.$refs.content as HTMLElement
|
const content = this.$refs.content as HTMLElement
|
||||||
content.focus()
|
content.focus()
|
||||||
|
|
||||||
|
@ -104,7 +106,27 @@ export default class DeckCardEditor extends Vue {
|
||||||
// arrow keys, enter, delete, etc
|
// arrow keys, enter, delete, etc
|
||||||
const isMove = movementKeys.indexOf(event.key) >= 0
|
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 () {
|
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'
|
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 type State = KV<boolean>
|
||||||
export {
|
export {
|
||||||
movementKeys,
|
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> = {
|
export const menuActionToCommand: KV<() => boolean> = {
|
||||||
paragraph: simpleAction('formatblock', 'P'),
|
paragraph: simpleAction('formatblock', 'P'),
|
||||||
heading1: simpleAction('formatblock', 'H1'),
|
heading1: simpleAction('formatblock', 'H1'),
|
||||||
|
@ -29,7 +34,7 @@ export const menuActionToCommand: KV<() => boolean> = {
|
||||||
heading3: simpleAction('formatblock', 'H3'),
|
heading3: simpleAction('formatblock', 'H3'),
|
||||||
bulletList: simpleAction('insertUnorderedList'),
|
bulletList: simpleAction('insertUnorderedList'),
|
||||||
numberedList: simpleAction('insertOrderedList'),
|
numberedList: simpleAction('insertOrderedList'),
|
||||||
separator: insertHorizontalRule(),
|
separator: simpleAction('insertHorizontalRule'),
|
||||||
bold: simpleAction('bold'),
|
bold: simpleAction('bold'),
|
||||||
italic: simpleAction('italic')
|
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…
Add table
Reference in a new issue