openrat-cms

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

operations.js (8025B)


      1 import { clipPos } from "../line/pos.js"
      2 import { findMaxLine } from "../line/spans.js"
      3 import { displayWidth, measureChar, scrollGap } from "../measurement/position_measurement.js"
      4 import { signal } from "../util/event.js"
      5 import { activeElt } from "../util/dom.js"
      6 import { finishOperation, pushOperation } from "../util/operation_group.js"
      7 
      8 import { ensureFocus } from "./focus.js"
      9 import { measureForScrollbars, updateScrollbars } from "./scrollbars.js"
     10 import { restartBlink } from "./selection.js"
     11 import { maybeScrollWindow, scrollPosIntoView, setScrollLeft, setScrollTop } from "./scrolling.js"
     12 import { DisplayUpdate, maybeClipScrollbars, postUpdateDisplay, setDocumentHeight, updateDisplayIfNeeded } from "./update_display.js"
     13 import { updateHeightsInViewport } from "./update_lines.js"
     14 
     15 // Operations are used to wrap a series of changes to the editor
     16 // state in such a way that each change won't have to update the
     17 // cursor and display (which would be awkward, slow, and
     18 // error-prone). Instead, display updates are batched and then all
     19 // combined and executed at once.
     20 
     21 let nextOpId = 0
     22 // Start a new operation.
     23 export function startOperation(cm) {
     24   cm.curOp = {
     25     cm: cm,
     26     viewChanged: false,      // Flag that indicates that lines might need to be redrawn
     27     startHeight: cm.doc.height, // Used to detect need to update scrollbar
     28     forceUpdate: false,      // Used to force a redraw
     29     updateInput: null,       // Whether to reset the input textarea
     30     typing: false,           // Whether this reset should be careful to leave existing text (for compositing)
     31     changeObjs: null,        // Accumulated changes, for firing change events
     32     cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on
     33     cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already
     34     selectionChanged: false, // Whether the selection needs to be redrawn
     35     updateMaxLine: false,    // Set when the widest line needs to be determined anew
     36     scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet
     37     scrollToPos: null,       // Used to scroll to a specific position
     38     focus: false,
     39     id: ++nextOpId           // Unique ID
     40   }
     41   pushOperation(cm.curOp)
     42 }
     43 
     44 // Finish an operation, updating the display and signalling delayed events
     45 export function endOperation(cm) {
     46   let op = cm.curOp
     47   finishOperation(op, group => {
     48     for (let i = 0; i < group.ops.length; i++)
     49       group.ops[i].cm.curOp = null
     50     endOperations(group)
     51   })
     52 }
     53 
     54 // The DOM updates done when an operation finishes are batched so
     55 // that the minimum number of relayouts are required.
     56 function endOperations(group) {
     57   let ops = group.ops
     58   for (let i = 0; i < ops.length; i++) // Read DOM
     59     endOperation_R1(ops[i])
     60   for (let i = 0; i < ops.length; i++) // Write DOM (maybe)
     61     endOperation_W1(ops[i])
     62   for (let i = 0; i < ops.length; i++) // Read DOM
     63     endOperation_R2(ops[i])
     64   for (let i = 0; i < ops.length; i++) // Write DOM (maybe)
     65     endOperation_W2(ops[i])
     66   for (let i = 0; i < ops.length; i++) // Read DOM
     67     endOperation_finish(ops[i])
     68 }
     69 
     70 function endOperation_R1(op) {
     71   let cm = op.cm, display = cm.display
     72   maybeClipScrollbars(cm)
     73   if (op.updateMaxLine) findMaxLine(cm)
     74 
     75   op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null ||
     76     op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom ||
     77                        op.scrollToPos.to.line >= display.viewTo) ||
     78     display.maxLineChanged && cm.options.lineWrapping
     79   op.update = op.mustUpdate &&
     80     new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate)
     81 }
     82 
     83 function endOperation_W1(op) {
     84   op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update)
     85 }
     86 
     87 function endOperation_R2(op) {
     88   let cm = op.cm, display = cm.display
     89   if (op.updatedDisplay) updateHeightsInViewport(cm)
     90 
     91   op.barMeasure = measureForScrollbars(cm)
     92 
     93   // If the max line changed since it was last measured, measure it,
     94   // and ensure the document's width matches it.
     95   // updateDisplay_W2 will use these properties to do the actual resizing
     96   if (display.maxLineChanged && !cm.options.lineWrapping) {
     97     op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3
     98     cm.display.sizerWidth = op.adjustWidthTo
     99     op.barMeasure.scrollWidth =
    100       Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth)
    101     op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm))
    102   }
    103 
    104   if (op.updatedDisplay || op.selectionChanged)
    105     op.preparedSelection = display.input.prepareSelection()
    106 }
    107 
    108 function endOperation_W2(op) {
    109   let cm = op.cm
    110 
    111   if (op.adjustWidthTo != null) {
    112     cm.display.sizer.style.minWidth = op.adjustWidthTo + "px"
    113     if (op.maxScrollLeft < cm.doc.scrollLeft)
    114       setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true)
    115     cm.display.maxLineChanged = false
    116   }
    117 
    118   let takeFocus = op.focus && op.focus == activeElt()
    119   if (op.preparedSelection)
    120     cm.display.input.showSelection(op.preparedSelection, takeFocus)
    121   if (op.updatedDisplay || op.startHeight != cm.doc.height)
    122     updateScrollbars(cm, op.barMeasure)
    123   if (op.updatedDisplay)
    124     setDocumentHeight(cm, op.barMeasure)
    125 
    126   if (op.selectionChanged) restartBlink(cm)
    127 
    128   if (cm.state.focused && op.updateInput)
    129     cm.display.input.reset(op.typing)
    130   if (takeFocus) ensureFocus(op.cm)
    131 }
    132 
    133 function endOperation_finish(op) {
    134   let cm = op.cm, display = cm.display, doc = cm.doc
    135 
    136   if (op.updatedDisplay) postUpdateDisplay(cm, op.update)
    137 
    138   // Abort mouse wheel delta measurement, when scrolling explicitly
    139   if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos))
    140     display.wheelStartX = display.wheelStartY = null
    141 
    142   // Propagate the scroll position to the actual DOM scroller
    143   if (op.scrollTop != null) setScrollTop(cm, op.scrollTop, op.forceScroll)
    144 
    145   if (op.scrollLeft != null) setScrollLeft(cm, op.scrollLeft, true, true)
    146   // If we need to scroll a specific position into view, do so.
    147   if (op.scrollToPos) {
    148     let rect = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from),
    149                                  clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin)
    150     maybeScrollWindow(cm, rect)
    151   }
    152 
    153   // Fire events for markers that are hidden/unidden by editing or
    154   // undoing
    155   let hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers
    156   if (hidden) for (let i = 0; i < hidden.length; ++i)
    157     if (!hidden[i].lines.length) signal(hidden[i], "hide")
    158   if (unhidden) for (let i = 0; i < unhidden.length; ++i)
    159     if (unhidden[i].lines.length) signal(unhidden[i], "unhide")
    160 
    161   if (display.wrapper.offsetHeight)
    162     doc.scrollTop = cm.display.scroller.scrollTop
    163 
    164   // Fire change events, and delayed event handlers
    165   if (op.changeObjs)
    166     signal(cm, "changes", cm, op.changeObjs)
    167   if (op.update)
    168     op.update.finish()
    169 }
    170 
    171 // Run the given function in an operation
    172 export function runInOp(cm, f) {
    173   if (cm.curOp) return f()
    174   startOperation(cm)
    175   try { return f() }
    176   finally { endOperation(cm) }
    177 }
    178 // Wraps a function in an operation. Returns the wrapped function.
    179 export function operation(cm, f) {
    180   return function() {
    181     if (cm.curOp) return f.apply(cm, arguments)
    182     startOperation(cm)
    183     try { return f.apply(cm, arguments) }
    184     finally { endOperation(cm) }
    185   }
    186 }
    187 // Used to add methods to editor and doc instances, wrapping them in
    188 // operations.
    189 export function methodOp(f) {
    190   return function() {
    191     if (this.curOp) return f.apply(this, arguments)
    192     startOperation(this)
    193     try { return f.apply(this, arguments) }
    194     finally { endOperation(this) }
    195   }
    196 }
    197 export function docMethodOp(f) {
    198   return function() {
    199     let cm = this.cm
    200     if (!cm || cm.curOp) return f.apply(this, arguments)
    201     startOperation(cm)
    202     try { return f.apply(this, arguments) }
    203     finally { endOperation(cm) }
    204   }
    205 }