261 lines
9.9 KiB
JavaScript
261 lines
9.9 KiB
JavaScript
|
import { sawCollapsedSpans } from "../line/saw_special_spans.js"
|
||
|
import { heightAtLine, visualLineEndNo, visualLineNo } from "../line/spans.js"
|
||
|
import { getLine, lineNumberFor } from "../line/utils_line.js"
|
||
|
import { displayHeight, displayWidth, getDimensions, paddingVert, scrollGap } from "../measurement/position_measurement.js"
|
||
|
import { mac, webkit } from "../util/browser.js"
|
||
|
import { activeElt, removeChildren, contains } from "../util/dom.js"
|
||
|
import { hasHandler, signal } from "../util/event.js"
|
||
|
import { indexOf } from "../util/misc.js"
|
||
|
|
||
|
import { buildLineElement, updateLineForChanges } from "./update_line.js"
|
||
|
import { startWorker } from "./highlight_worker.js"
|
||
|
import { maybeUpdateLineNumberWidth } from "./line_numbers.js"
|
||
|
import { measureForScrollbars, updateScrollbars } from "./scrollbars.js"
|
||
|
import { updateSelection } from "./selection.js"
|
||
|
import { updateHeightsInViewport, visibleLines } from "./update_lines.js"
|
||
|
import { adjustView, countDirtyView, resetView } from "./view_tracking.js"
|
||
|
|
||
|
// DISPLAY DRAWING
|
||
|
|
||
|
export class DisplayUpdate {
|
||
|
constructor(cm, viewport, force) {
|
||
|
let display = cm.display
|
||
|
|
||
|
this.viewport = viewport
|
||
|
// Store some values that we'll need later (but don't want to force a relayout for)
|
||
|
this.visible = visibleLines(display, cm.doc, viewport)
|
||
|
this.editorIsHidden = !display.wrapper.offsetWidth
|
||
|
this.wrapperHeight = display.wrapper.clientHeight
|
||
|
this.wrapperWidth = display.wrapper.clientWidth
|
||
|
this.oldDisplayWidth = displayWidth(cm)
|
||
|
this.force = force
|
||
|
this.dims = getDimensions(cm)
|
||
|
this.events = []
|
||
|
}
|
||
|
|
||
|
signal(emitter, type) {
|
||
|
if (hasHandler(emitter, type))
|
||
|
this.events.push(arguments)
|
||
|
}
|
||
|
finish() {
|
||
|
for (let i = 0; i < this.events.length; i++)
|
||
|
signal.apply(null, this.events[i])
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export function maybeClipScrollbars(cm) {
|
||
|
let display = cm.display
|
||
|
if (!display.scrollbarsClipped && display.scroller.offsetWidth) {
|
||
|
display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth
|
||
|
display.heightForcer.style.height = scrollGap(cm) + "px"
|
||
|
display.sizer.style.marginBottom = -display.nativeBarWidth + "px"
|
||
|
display.sizer.style.borderRightWidth = scrollGap(cm) + "px"
|
||
|
display.scrollbarsClipped = true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function selectionSnapshot(cm) {
|
||
|
if (cm.hasFocus()) return null
|
||
|
let active = activeElt()
|
||
|
if (!active || !contains(cm.display.lineDiv, active)) return null
|
||
|
let result = {activeElt: active}
|
||
|
if (window.getSelection) {
|
||
|
let sel = window.getSelection()
|
||
|
if (sel.anchorNode && sel.extend && contains(cm.display.lineDiv, sel.anchorNode)) {
|
||
|
result.anchorNode = sel.anchorNode
|
||
|
result.anchorOffset = sel.anchorOffset
|
||
|
result.focusNode = sel.focusNode
|
||
|
result.focusOffset = sel.focusOffset
|
||
|
}
|
||
|
}
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
function restoreSelection(snapshot) {
|
||
|
if (!snapshot || !snapshot.activeElt || snapshot.activeElt == activeElt()) return
|
||
|
snapshot.activeElt.focus()
|
||
|
if (snapshot.anchorNode && contains(document.body, snapshot.anchorNode) && contains(document.body, snapshot.focusNode)) {
|
||
|
let sel = window.getSelection(), range = document.createRange()
|
||
|
range.setEnd(snapshot.anchorNode, snapshot.anchorOffset)
|
||
|
range.collapse(false)
|
||
|
sel.removeAllRanges()
|
||
|
sel.addRange(range)
|
||
|
sel.extend(snapshot.focusNode, snapshot.focusOffset)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Does the actual updating of the line display. Bails out
|
||
|
// (returning false) when there is nothing to be done and forced is
|
||
|
// false.
|
||
|
export function updateDisplayIfNeeded(cm, update) {
|
||
|
let display = cm.display, doc = cm.doc
|
||
|
|
||
|
if (update.editorIsHidden) {
|
||
|
resetView(cm)
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// Bail out if the visible area is already rendered and nothing changed.
|
||
|
if (!update.force &&
|
||
|
update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo &&
|
||
|
(display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) &&
|
||
|
display.renderedView == display.view && countDirtyView(cm) == 0)
|
||
|
return false
|
||
|
|
||
|
if (maybeUpdateLineNumberWidth(cm)) {
|
||
|
resetView(cm)
|
||
|
update.dims = getDimensions(cm)
|
||
|
}
|
||
|
|
||
|
// Compute a suitable new viewport (from & to)
|
||
|
let end = doc.first + doc.size
|
||
|
let from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first)
|
||
|
let to = Math.min(end, update.visible.to + cm.options.viewportMargin)
|
||
|
if (display.viewFrom < from && from - display.viewFrom < 20) from = Math.max(doc.first, display.viewFrom)
|
||
|
if (display.viewTo > to && display.viewTo - to < 20) to = Math.min(end, display.viewTo)
|
||
|
if (sawCollapsedSpans) {
|
||
|
from = visualLineNo(cm.doc, from)
|
||
|
to = visualLineEndNo(cm.doc, to)
|
||
|
}
|
||
|
|
||
|
let different = from != display.viewFrom || to != display.viewTo ||
|
||
|
display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth
|
||
|
adjustView(cm, from, to)
|
||
|
|
||
|
display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom))
|
||
|
// Position the mover div to align with the current scroll position
|
||
|
cm.display.mover.style.top = display.viewOffset + "px"
|
||
|
|
||
|
let toUpdate = countDirtyView(cm)
|
||
|
if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view &&
|
||
|
(display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo))
|
||
|
return false
|
||
|
|
||
|
// For big changes, we hide the enclosing element during the
|
||
|
// update, since that speeds up the operations on most browsers.
|
||
|
let selSnapshot = selectionSnapshot(cm)
|
||
|
if (toUpdate > 4) display.lineDiv.style.display = "none"
|
||
|
patchDisplay(cm, display.updateLineNumbers, update.dims)
|
||
|
if (toUpdate > 4) display.lineDiv.style.display = ""
|
||
|
display.renderedView = display.view
|
||
|
// There might have been a widget with a focused element that got
|
||
|
// hidden or updated, if so re-focus it.
|
||
|
restoreSelection(selSnapshot)
|
||
|
|
||
|
// Prevent selection and cursors from interfering with the scroll
|
||
|
// width and height.
|
||
|
removeChildren(display.cursorDiv)
|
||
|
removeChildren(display.selectionDiv)
|
||
|
display.gutters.style.height = display.sizer.style.minHeight = 0
|
||
|
|
||
|
if (different) {
|
||
|
display.lastWrapHeight = update.wrapperHeight
|
||
|
display.lastWrapWidth = update.wrapperWidth
|
||
|
startWorker(cm, 400)
|
||
|
}
|
||
|
|
||
|
display.updateLineNumbers = null
|
||
|
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
export function postUpdateDisplay(cm, update) {
|
||
|
let viewport = update.viewport
|
||
|
|
||
|
for (let first = true;; first = false) {
|
||
|
if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) {
|
||
|
// Clip forced viewport to actual scrollable area.
|
||
|
if (viewport && viewport.top != null)
|
||
|
viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)}
|
||
|
// Updated line heights might result in the drawn area not
|
||
|
// actually covering the viewport. Keep looping until it does.
|
||
|
update.visible = visibleLines(cm.display, cm.doc, viewport)
|
||
|
if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo)
|
||
|
break
|
||
|
}
|
||
|
if (!updateDisplayIfNeeded(cm, update)) break
|
||
|
updateHeightsInViewport(cm)
|
||
|
let barMeasure = measureForScrollbars(cm)
|
||
|
updateSelection(cm)
|
||
|
updateScrollbars(cm, barMeasure)
|
||
|
setDocumentHeight(cm, barMeasure)
|
||
|
update.force = false
|
||
|
}
|
||
|
|
||
|
update.signal(cm, "update", cm)
|
||
|
if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) {
|
||
|
update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo)
|
||
|
cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export function updateDisplaySimple(cm, viewport) {
|
||
|
let update = new DisplayUpdate(cm, viewport)
|
||
|
if (updateDisplayIfNeeded(cm, update)) {
|
||
|
updateHeightsInViewport(cm)
|
||
|
postUpdateDisplay(cm, update)
|
||
|
let barMeasure = measureForScrollbars(cm)
|
||
|
updateSelection(cm)
|
||
|
updateScrollbars(cm, barMeasure)
|
||
|
setDocumentHeight(cm, barMeasure)
|
||
|
update.finish()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Sync the actual display DOM structure with display.view, removing
|
||
|
// nodes for lines that are no longer in view, and creating the ones
|
||
|
// that are not there yet, and updating the ones that are out of
|
||
|
// date.
|
||
|
function patchDisplay(cm, updateNumbersFrom, dims) {
|
||
|
let display = cm.display, lineNumbers = cm.options.lineNumbers
|
||
|
let container = display.lineDiv, cur = container.firstChild
|
||
|
|
||
|
function rm(node) {
|
||
|
let next = node.nextSibling
|
||
|
// Works around a throw-scroll bug in OS X Webkit
|
||
|
if (webkit && mac && cm.display.currentWheelTarget == node)
|
||
|
node.style.display = "none"
|
||
|
else
|
||
|
node.parentNode.removeChild(node)
|
||
|
return next
|
||
|
}
|
||
|
|
||
|
let view = display.view, lineN = display.viewFrom
|
||
|
// Loop over the elements in the view, syncing cur (the DOM nodes
|
||
|
// in display.lineDiv) with the view as we go.
|
||
|
for (let i = 0; i < view.length; i++) {
|
||
|
let lineView = view[i]
|
||
|
if (lineView.hidden) {
|
||
|
} else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet
|
||
|
let node = buildLineElement(cm, lineView, lineN, dims)
|
||
|
container.insertBefore(node, cur)
|
||
|
} else { // Already drawn
|
||
|
while (cur != lineView.node) cur = rm(cur)
|
||
|
let updateNumber = lineNumbers && updateNumbersFrom != null &&
|
||
|
updateNumbersFrom <= lineN && lineView.lineNumber
|
||
|
if (lineView.changes) {
|
||
|
if (indexOf(lineView.changes, "gutter") > -1) updateNumber = false
|
||
|
updateLineForChanges(cm, lineView, lineN, dims)
|
||
|
}
|
||
|
if (updateNumber) {
|
||
|
removeChildren(lineView.lineNumber)
|
||
|
lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN)))
|
||
|
}
|
||
|
cur = lineView.node.nextSibling
|
||
|
}
|
||
|
lineN += lineView.size
|
||
|
}
|
||
|
while (cur) cur = rm(cur)
|
||
|
}
|
||
|
|
||
|
export function updateGutterSpace(display) {
|
||
|
let width = display.gutters.offsetWidth
|
||
|
display.sizer.style.marginLeft = width + "px"
|
||
|
}
|
||
|
|
||
|
export function setDocumentHeight(cm, measure) {
|
||
|
cm.display.sizer.style.minHeight = measure.docHeight + "px"
|
||
|
cm.display.heightForcer.style.top = measure.docHeight + "px"
|
||
|
cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + "px"
|
||
|
}
|