update_display.js (10173B)
1 import { sawCollapsedSpans } from "../line/saw_special_spans.js" 2 import { heightAtLine, visualLineEndNo, visualLineNo } from "../line/spans.js" 3 import { getLine, lineNumberFor } from "../line/utils_line.js" 4 import { displayHeight, displayWidth, getDimensions, paddingVert, scrollGap } from "../measurement/position_measurement.js" 5 import { mac, webkit } from "../util/browser.js" 6 import { activeElt, removeChildren, contains } from "../util/dom.js" 7 import { hasHandler, signal } from "../util/event.js" 8 import { indexOf } from "../util/misc.js" 9 10 import { buildLineElement, updateLineForChanges } from "./update_line.js" 11 import { startWorker } from "./highlight_worker.js" 12 import { maybeUpdateLineNumberWidth } from "./line_numbers.js" 13 import { measureForScrollbars, updateScrollbars } from "./scrollbars.js" 14 import { updateSelection } from "./selection.js" 15 import { updateHeightsInViewport, visibleLines } from "./update_lines.js" 16 import { adjustView, countDirtyView, resetView } from "./view_tracking.js" 17 18 // DISPLAY DRAWING 19 20 export class DisplayUpdate { 21 constructor(cm, viewport, force) { 22 let display = cm.display 23 24 this.viewport = viewport 25 // Store some values that we'll need later (but don't want to force a relayout for) 26 this.visible = visibleLines(display, cm.doc, viewport) 27 this.editorIsHidden = !display.wrapper.offsetWidth 28 this.wrapperHeight = display.wrapper.clientHeight 29 this.wrapperWidth = display.wrapper.clientWidth 30 this.oldDisplayWidth = displayWidth(cm) 31 this.force = force 32 this.dims = getDimensions(cm) 33 this.events = [] 34 } 35 36 signal(emitter, type) { 37 if (hasHandler(emitter, type)) 38 this.events.push(arguments) 39 } 40 finish() { 41 for (let i = 0; i < this.events.length; i++) 42 signal.apply(null, this.events[i]) 43 } 44 } 45 46 export function maybeClipScrollbars(cm) { 47 let display = cm.display 48 if (!display.scrollbarsClipped && display.scroller.offsetWidth) { 49 display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth 50 display.heightForcer.style.height = scrollGap(cm) + "px" 51 display.sizer.style.marginBottom = -display.nativeBarWidth + "px" 52 display.sizer.style.borderRightWidth = scrollGap(cm) + "px" 53 display.scrollbarsClipped = true 54 } 55 } 56 57 function selectionSnapshot(cm) { 58 if (cm.hasFocus()) return null 59 let active = activeElt() 60 if (!active || !contains(cm.display.lineDiv, active)) return null 61 let result = {activeElt: active} 62 if (window.getSelection) { 63 let sel = window.getSelection() 64 if (sel.anchorNode && sel.extend && contains(cm.display.lineDiv, sel.anchorNode)) { 65 result.anchorNode = sel.anchorNode 66 result.anchorOffset = sel.anchorOffset 67 result.focusNode = sel.focusNode 68 result.focusOffset = sel.focusOffset 69 } 70 } 71 return result 72 } 73 74 function restoreSelection(snapshot) { 75 if (!snapshot || !snapshot.activeElt || snapshot.activeElt == activeElt()) return 76 snapshot.activeElt.focus() 77 if (snapshot.anchorNode && contains(document.body, snapshot.anchorNode) && contains(document.body, snapshot.focusNode)) { 78 let sel = window.getSelection(), range = document.createRange() 79 range.setEnd(snapshot.anchorNode, snapshot.anchorOffset) 80 range.collapse(false) 81 sel.removeAllRanges() 82 sel.addRange(range) 83 sel.extend(snapshot.focusNode, snapshot.focusOffset) 84 } 85 } 86 87 // Does the actual updating of the line display. Bails out 88 // (returning false) when there is nothing to be done and forced is 89 // false. 90 export function updateDisplayIfNeeded(cm, update) { 91 let display = cm.display, doc = cm.doc 92 93 if (update.editorIsHidden) { 94 resetView(cm) 95 return false 96 } 97 98 // Bail out if the visible area is already rendered and nothing changed. 99 if (!update.force && 100 update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo && 101 (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) && 102 display.renderedView == display.view && countDirtyView(cm) == 0) 103 return false 104 105 if (maybeUpdateLineNumberWidth(cm)) { 106 resetView(cm) 107 update.dims = getDimensions(cm) 108 } 109 110 // Compute a suitable new viewport (from & to) 111 let end = doc.first + doc.size 112 let from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first) 113 let to = Math.min(end, update.visible.to + cm.options.viewportMargin) 114 if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom) 115 if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo) 116 if (sawCollapsedSpans) { 117 from = visualLineNo(cm.doc, from) 118 to = visualLineEndNo(cm.doc, to) 119 } 120 121 let different = from != display.viewFrom || to != display.viewTo || 122 display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth 123 adjustView(cm, from, to) 124 125 display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)) 126 // Position the mover div to align with the current scroll position 127 cm.display.mover.style.top = display.viewOffset + "px" 128 129 let toUpdate = countDirtyView(cm) 130 if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view && 131 (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo)) 132 return false 133 134 // For big changes, we hide the enclosing element during the 135 // update, since that speeds up the operations on most browsers. 136 let selSnapshot = selectionSnapshot(cm) 137 if (toUpdate > 4) display.lineDiv.style.display = "none" 138 patchDisplay(cm, display.updateLineNumbers, update.dims) 139 if (toUpdate > 4) display.lineDiv.style.display = "" 140 display.renderedView = display.view 141 // There might have been a widget with a focused element that got 142 // hidden or updated, if so re-focus it. 143 restoreSelection(selSnapshot) 144 145 // Prevent selection and cursors from interfering with the scroll 146 // width and height. 147 removeChildren(display.cursorDiv) 148 removeChildren(display.selectionDiv) 149 display.gutters.style.height = display.sizer.style.minHeight = 0 150 151 if (different) { 152 display.lastWrapHeight = update.wrapperHeight 153 display.lastWrapWidth = update.wrapperWidth 154 startWorker(cm, 400) 155 } 156 157 display.updateLineNumbers = null 158 159 return true 160 } 161 162 export function postUpdateDisplay(cm, update) { 163 let viewport = update.viewport 164 165 for (let first = true;; first = false) { 166 if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) { 167 // Clip forced viewport to actual scrollable area. 168 if (viewport && viewport.top != null) 169 viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)} 170 // Updated line heights might result in the drawn area not 171 // actually covering the viewport. Keep looping until it does. 172 update.visible = visibleLines(cm.display, cm.doc, viewport) 173 if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo) 174 break 175 } 176 if (!updateDisplayIfNeeded(cm, update)) break 177 updateHeightsInViewport(cm) 178 let barMeasure = measureForScrollbars(cm) 179 updateSelection(cm) 180 updateScrollbars(cm, barMeasure) 181 setDocumentHeight(cm, barMeasure) 182 update.force = false 183 } 184 185 update.signal(cm, "update", cm) 186 if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) { 187 update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo) 188 cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo 189 } 190 } 191 192 export function updateDisplaySimple(cm, viewport) { 193 let update = new DisplayUpdate(cm, viewport) 194 if (updateDisplayIfNeeded(cm, update)) { 195 updateHeightsInViewport(cm) 196 postUpdateDisplay(cm, update) 197 let barMeasure = measureForScrollbars(cm) 198 updateSelection(cm) 199 updateScrollbars(cm, barMeasure) 200 setDocumentHeight(cm, barMeasure) 201 update.finish() 202 } 203 } 204 205 // Sync the actual display DOM structure with display.view, removing 206 // nodes for lines that are no longer in view, and creating the ones 207 // that are not there yet, and updating the ones that are out of 208 // date. 209 function patchDisplay(cm, updateNumbersFrom, dims) { 210 let display = cm.display, lineNumbers = cm.options.lineNumbers 211 let container = display.lineDiv, cur = container.firstChild 212 213 function rm(node) { 214 let next = node.nextSibling 215 // Works around a throw-scroll bug in OS X Webkit 216 if (webkit && mac && cm.display.currentWheelTarget == node) 217 node.style.display = "none" 218 else 219 node.parentNode.removeChild(node) 220 return next 221 } 222 223 let view = display.view, lineN = display.viewFrom 224 // Loop over the elements in the view, syncing cur (the DOM nodes 225 // in display.lineDiv) with the view as we go. 226 for (let i = 0; i < view.length; i++) { 227 let lineView = view[i] 228 if (lineView.hidden) { 229 } else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet 230 let node = buildLineElement(cm, lineView, lineN, dims) 231 container.insertBefore(node, cur) 232 } else { // Already drawn 233 while (cur != lineView.node) cur = rm(cur) 234 let updateNumber = lineNumbers && updateNumbersFrom != null && 235 updateNumbersFrom <= lineN && lineView.lineNumber 236 if (lineView.changes) { 237 if (indexOf(lineView.changes, "gutter") > -1) updateNumber = false 238 updateLineForChanges(cm, lineView, lineN, dims) 239 } 240 if (updateNumber) { 241 removeChildren(lineView.lineNumber) 242 lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN))) 243 } 244 cur = lineView.node.nextSibling 245 } 246 lineN += lineView.size 247 } 248 while (cur) cur = rm(cur) 249 } 250 251 export function updateGutterSpace(cm) { 252 let width = cm.display.gutters.offsetWidth 253 cm.display.sizer.style.marginLeft = width + "px" 254 } 255 256 export function setDocumentHeight(cm, measure) { 257 cm.display.sizer.style.minHeight = measure.docHeight + "px" 258 cm.display.heightForcer.style.top = measure.docHeight + "px" 259 cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + "px" 260 }