selection.ts (8979B)
1 import {TextSelection, NodeSelection, Selection} from "prosemirror-state" 2 import {ResolvedPos} from "prosemirror-model" 3 4 import * as browser from "./browser" 5 import {isEquivalentPosition, domIndex, isOnEdge, selectionCollapsed} from "./dom" 6 import {EditorView} from "./index" 7 import {NodeViewDesc} from "./viewdesc" 8 9 export function selectionFromDOM(view: EditorView, origin: string | null = null) { 10 let domSel = view.domSelectionRange(), doc = view.state.doc 11 if (!domSel.focusNode) return null 12 let nearestDesc = view.docView.nearestDesc(domSel.focusNode), inWidget = nearestDesc && nearestDesc.size == 0 13 let head = view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset, 1) 14 if (head < 0) return null 15 let $head = doc.resolve(head), anchor, selection 16 if (selectionCollapsed(domSel)) { 17 anchor = head 18 while (nearestDesc && !nearestDesc.node) nearestDesc = nearestDesc.parent 19 let nearestDescNode = (nearestDesc as NodeViewDesc).node 20 if (nearestDesc && nearestDescNode.isAtom && NodeSelection.isSelectable(nearestDescNode) && nearestDesc.parent 21 && !(nearestDescNode.isInline && isOnEdge(domSel.focusNode, domSel.focusOffset, nearestDesc.dom))) { 22 let pos = nearestDesc.posBefore 23 selection = new NodeSelection(head == pos ? $head : doc.resolve(pos)) 24 } 25 } else { 26 if (domSel instanceof view.dom.ownerDocument.defaultView!.Selection && domSel.rangeCount > 1) { 27 let min = head, max = head 28 for (let i = 0; i < domSel.rangeCount; i++) { 29 let range = domSel.getRangeAt(i) 30 min = Math.min(min, view.docView.posFromDOM(range.startContainer, range.startOffset, 1)) 31 max = Math.max(max, view.docView.posFromDOM(range.endContainer, range.endOffset, -1)) 32 } 33 if (min < 0) return null 34 ;[anchor, head] = max == view.state.selection.anchor ? [max, min] : [min, max] 35 $head = doc.resolve(head) 36 } else { 37 anchor = view.docView.posFromDOM(domSel.anchorNode!, domSel.anchorOffset, 1) 38 } 39 if (anchor < 0) return null 40 } 41 let $anchor = doc.resolve(anchor) 42 43 if (!selection) { 44 let bias = origin == "pointer" || (view.state.selection.head < $head.pos && !inWidget) ? 1 : -1 45 selection = selectionBetween(view, $anchor, $head, bias) 46 } 47 return selection 48 } 49 50 function editorOwnsSelection(view: EditorView) { 51 return view.editable ? view.hasFocus() : 52 hasSelection(view) && document.activeElement && document.activeElement.contains(view.dom) 53 } 54 55 export function selectionToDOM(view: EditorView, force = false) { 56 let sel = view.state.selection 57 syncNodeSelection(view, sel) 58 59 if (!editorOwnsSelection(view)) return 60 61 // The delayed drag selection causes issues with Cell Selections 62 // in Safari. And the drag selection delay is to workarond issues 63 // which only present in Chrome. 64 if (!force && view.input.mouseDown && view.input.mouseDown.allowDefault && browser.chrome) { 65 let domSel = view.domSelectionRange(), curSel = view.domObserver.currentSelection 66 if (domSel.anchorNode && curSel.anchorNode && 67 isEquivalentPosition(domSel.anchorNode, domSel.anchorOffset, 68 curSel.anchorNode, curSel.anchorOffset)) { 69 view.input.mouseDown.delayedSelectionSync = true 70 view.domObserver.setCurSelection() 71 return 72 } 73 } 74 75 view.domObserver.disconnectSelection() 76 77 if (view.cursorWrapper) { 78 selectCursorWrapper(view) 79 } else { 80 let {anchor, head} = sel, resetEditableFrom, resetEditableTo 81 if (brokenSelectBetweenUneditable && !(sel instanceof TextSelection)) { 82 if (!sel.$from.parent.inlineContent) 83 resetEditableFrom = temporarilyEditableNear(view, sel.from) 84 if (!sel.empty && !sel.$from.parent.inlineContent) 85 resetEditableTo = temporarilyEditableNear(view, sel.to) 86 } 87 view.docView.setSelection(anchor, head, view, force) 88 if (brokenSelectBetweenUneditable) { 89 if (resetEditableFrom) resetEditable(resetEditableFrom) 90 if (resetEditableTo) resetEditable(resetEditableTo) 91 } 92 if (sel.visible) { 93 view.dom.classList.remove("ProseMirror-hideselection") 94 } else { 95 view.dom.classList.add("ProseMirror-hideselection") 96 if ("onselectionchange" in document) removeClassOnSelectionChange(view) 97 } 98 } 99 100 view.domObserver.setCurSelection() 101 view.domObserver.connectSelection() 102 } 103 104 // Kludge to work around Webkit not allowing a selection to start/end 105 // between non-editable block nodes. We briefly make something 106 // editable, set the selection, then set it uneditable again. 107 108 const brokenSelectBetweenUneditable = browser.safari || browser.chrome && browser.chrome_version < 63 109 110 function temporarilyEditableNear(view: EditorView, pos: number) { 111 let {node, offset} = view.docView.domFromPos(pos, 0) 112 let after = offset < node.childNodes.length ? node.childNodes[offset] : null 113 let before = offset ? node.childNodes[offset - 1] : null 114 if (browser.safari && after && (after as HTMLElement).contentEditable == "false") return setEditable(after as HTMLElement) 115 if ((!after || (after as HTMLElement).contentEditable == "false") && 116 (!before || (before as HTMLElement).contentEditable == "false")) { 117 if (after) return setEditable(after as HTMLElement) 118 else if (before) return setEditable(before as HTMLElement) 119 } 120 } 121 122 function setEditable(element: HTMLElement) { 123 element.contentEditable = "true" 124 if (browser.safari && element.draggable) { element.draggable = false; (element as any).wasDraggable = true } 125 return element 126 } 127 128 function resetEditable(element: HTMLElement) { 129 element.contentEditable = "false" 130 if ((element as any).wasDraggable) { element.draggable = true; (element as any).wasDraggable = null } 131 } 132 133 function removeClassOnSelectionChange(view: EditorView) { 134 let doc = view.dom.ownerDocument 135 doc.removeEventListener("selectionchange", view.input.hideSelectionGuard!) 136 let domSel = view.domSelectionRange() 137 let node = domSel.anchorNode, offset = domSel.anchorOffset 138 doc.addEventListener("selectionchange", view.input.hideSelectionGuard = () => { 139 if (domSel.anchorNode != node || domSel.anchorOffset != offset) { 140 doc.removeEventListener("selectionchange", view.input.hideSelectionGuard!) 141 setTimeout(() => { 142 if (!editorOwnsSelection(view) || view.state.selection.visible) 143 view.dom.classList.remove("ProseMirror-hideselection") 144 }, 20) 145 } 146 }) 147 } 148 149 function selectCursorWrapper(view: EditorView) { 150 let domSel = view.domSelection() 151 if (!domSel) return 152 let node = view.cursorWrapper!.dom, img = node.nodeName == "IMG" 153 if (img) domSel.collapse(node.parentNode!, domIndex(node) + 1) 154 else domSel.collapse(node, 0) 155 // Kludge to kill 'control selection' in IE11 when selecting an 156 // invisible cursor wrapper, since that would result in those weird 157 // resize handles and a selection that considers the absolutely 158 // positioned wrapper, rather than the root editable node, the 159 // focused element. 160 if (!img && !view.state.selection.visible && browser.ie && browser.ie_version <= 11) { 161 ;(node as any).disabled = true 162 ;(node as any).disabled = false 163 } 164 } 165 166 export function syncNodeSelection(view: EditorView, sel: Selection) { 167 if (sel instanceof NodeSelection) { 168 let desc = view.docView.descAt(sel.from) 169 if (desc != view.lastSelectedViewDesc) { 170 clearNodeSelection(view) 171 if (desc) (desc as NodeViewDesc).selectNode() 172 view.lastSelectedViewDesc = desc 173 } 174 } else { 175 clearNodeSelection(view) 176 } 177 } 178 179 // Clear all DOM statefulness of the last node selection. 180 function clearNodeSelection(view: EditorView) { 181 if (view.lastSelectedViewDesc) { 182 if (view.lastSelectedViewDesc.parent) 183 (view.lastSelectedViewDesc as NodeViewDesc).deselectNode() 184 view.lastSelectedViewDesc = undefined 185 } 186 } 187 188 export function selectionBetween(view: EditorView, $anchor: ResolvedPos, $head: ResolvedPos, bias?: number) { 189 return view.someProp("createSelectionBetween", f => f(view, $anchor, $head)) 190 || TextSelection.between($anchor, $head, bias) 191 } 192 193 export function hasFocusAndSelection(view: EditorView) { 194 if (view.editable && !view.hasFocus()) return false 195 return hasSelection(view) 196 } 197 198 export function hasSelection(view: EditorView) { 199 let sel = view.domSelectionRange() 200 if (!sel.anchorNode) return false 201 try { 202 // Firefox will raise 'permission denied' errors when accessing 203 // properties of `sel.anchorNode` when it's in a generated CSS 204 // element. 205 return view.dom.contains(sel.anchorNode.nodeType == 3 ? sel.anchorNode.parentNode : sel.anchorNode) && 206 (view.editable || view.dom.contains(sel.focusNode!.nodeType == 3 ? sel.focusNode!.parentNode : sel.focusNode)) 207 } catch(_) { 208 return false 209 } 210 } 211 212 export function anchorInRightPlace(view: EditorView) { 213 let anchorDOM = view.docView.domFromPos(view.state.selection.anchor, 0) 214 let domSel = view.domSelectionRange() 215 return isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode!, domSel.anchorOffset) 216 }