Index: third_party/WebKit/Source/devtools/front_end/cm/codemirror.js |
diff --git a/third_party/WebKit/Source/devtools/front_end/cm/codemirror.js b/third_party/WebKit/Source/devtools/front_end/cm/codemirror.js |
index 03a34dbbfb78b10557a43bfbb1504d56f36093b2..cd73b84a1253b395847152bbcf58d825dad519e1 100644 |
--- a/third_party/WebKit/Source/devtools/front_end/cm/codemirror.js |
+++ b/third_party/WebKit/Source/devtools/front_end/cm/codemirror.js |
@@ -13,7 +13,7 @@ |
else if (typeof define == "function" && define.amd) // AMD |
return define([], mod); |
else // Plain browser env |
- this.CodeMirror = mod(); |
+ (this || window).CodeMirror = mod(); |
})(function() { |
"use strict"; |
@@ -21,29 +21,30 @@ |
// Kludges for bugs and behavior differences that can't be feature |
// detected are enabled based on userAgent etc sniffing. |
+ var userAgent = navigator.userAgent; |
+ var platform = navigator.platform; |
- var gecko = /gecko\/\d/i.test(navigator.userAgent); |
- // ie_uptoN means Internet Explorer version N or lower |
- var ie_upto10 = /MSIE \d/.test(navigator.userAgent); |
- var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent); |
+ var gecko = /gecko\/\d/i.test(userAgent); |
+ var ie_upto10 = /MSIE \d/.test(userAgent); |
+ var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent); |
var ie = ie_upto10 || ie_11up; |
var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : ie_11up[1]); |
- var webkit = /WebKit\//.test(navigator.userAgent); |
- var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent); |
- var chrome = /Chrome\//.test(navigator.userAgent); |
- var presto = /Opera\//.test(navigator.userAgent); |
+ var webkit = /WebKit\//.test(userAgent); |
+ var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent); |
+ var chrome = /Chrome\//.test(userAgent); |
+ var presto = /Opera\//.test(userAgent); |
var safari = /Apple Computer/.test(navigator.vendor); |
- var khtml = /KHTML\//.test(navigator.userAgent); |
- var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent); |
- var phantom = /PhantomJS/.test(navigator.userAgent); |
+ var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent); |
+ var phantom = /PhantomJS/.test(userAgent); |
- var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent); |
+ var ios = /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent); |
// This is woefully incomplete. Suggestions for alternative methods welcome. |
- var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent); |
- var mac = ios || /Mac/.test(navigator.platform); |
- var windows = /win/i.test(navigator.platform); |
+ var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent); |
+ var mac = ios || /Mac/.test(platform); |
+ var chromeOS = /\bCrOS\b/.test(userAgent); |
+ var windows = /win/i.test(platform); |
- var presto_version = presto && navigator.userAgent.match(/Version\/(\d*\.\d*)/); |
+ var presto_version = presto && userAgent.match(/Version\/(\d*\.\d*)/); |
if (presto_version) presto_version = Number(presto_version[1]); |
if (presto_version && presto_version >= 15) { presto = false; webkit = true; } |
// Some browsers use the wrong event properties to signal cmd/ctrl on OS X |
@@ -67,33 +68,40 @@ |
setGuttersForLineNumbers(options); |
var doc = options.value; |
- if (typeof doc == "string") doc = new Doc(doc, options.mode); |
+ if (typeof doc == "string") doc = new Doc(doc, options.mode, null, options.lineSeparator); |
this.doc = doc; |
- var display = this.display = new Display(place, doc); |
+ var input = new CodeMirror.inputStyles[options.inputStyle](this); |
+ var display = this.display = new Display(place, doc, input); |
display.wrapper.CodeMirror = this; |
updateGutters(this); |
themeChanged(this); |
if (options.lineWrapping) |
this.display.wrapper.className += " CodeMirror-wrap"; |
- if (options.autofocus && !mobile) focusInput(this); |
+ if (options.autofocus && !mobile) display.input.focus(); |
initScrollbars(this); |
this.state = { |
keyMaps: [], // stores maps added by addKeyMap |
overlays: [], // highlighting overlays, as added by addOverlay |
modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info |
- overwrite: false, focused: false, |
+ overwrite: false, |
+ delayingBlurEvent: false, |
+ focused: false, |
suppressEdits: false, // used to disable editing during key handlers when in readOnly mode |
- pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in readInput |
+ pasteIncoming: false, cutIncoming: false, // help recognize paste/cut edits in input.poll |
+ selectingText: false, |
draggingText: false, |
highlight: new Delayed(), // stores highlight worker timeout |
- keySeq: null // Unfinished key sequence |
+ keySeq: null, // Unfinished key sequence |
+ specialChars: null |
}; |
+ var cm = this; |
+ |
// Override magic textarea content restore that IE sometimes does |
// on our hidden textarea on reload |
- if (ie && ie_version < 11) setTimeout(bind(resetInput, this, true), 20); |
+ if (ie && ie_version < 11) setTimeout(function() { cm.display.input.reset(true); }, 20); |
registerEventHandlers(this); |
ensureGlobalHandlers(); |
@@ -102,7 +110,7 @@ |
this.curOp.forceUpdate = true; |
attachDoc(this, doc); |
- if ((options.autofocus && !mobile) || activeElt() == display.input) |
+ if ((options.autofocus && !mobile) || cm.hasFocus()) |
setTimeout(bind(onFocus, this), 20); |
else |
onBlur(this); |
@@ -110,6 +118,7 @@ |
for (var opt in optionHandlers) if (optionHandlers.hasOwnProperty(opt)) |
optionHandlers[opt](this, options[opt], Init); |
maybeUpdateLineNumberWidth(this); |
+ if (options.finishInit) options.finishInit(this); |
for (var i = 0; i < initHooks.length; ++i) initHooks[i](this); |
endOperation(this); |
// Suppress optimizelegibility in Webkit, since it breaks text |
@@ -125,31 +134,17 @@ |
// and content drawing. It holds references to DOM nodes and |
// display-related state. |
- function Display(place, doc) { |
+ function Display(place, doc, input) { |
var d = this; |
+ this.input = input; |
- // The semihidden textarea that is focused when the editor is |
- // focused, and receives input. |
- var input = d.input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none"); |
- // The textarea is kept positioned near the cursor to prevent the |
- // fact that it'll be scrolled into view on input from scrolling |
- // our fake cursor out of view. On webkit, when wrap=off, paste is |
- // very slow. So make the area wide instead. |
- if (webkit) input.style.width = "1000px"; |
- else input.setAttribute("wrap", "off"); |
- // If border: 0; -- iOS fails to open keyboard (issue #1287) |
- if (ios) input.style.border = "1px solid black"; |
- input.setAttribute("autocorrect", "off"); input.setAttribute("autocapitalize", "off"); input.setAttribute("spellcheck", "false"); |
- |
- // Wraps and hides input textarea |
- d.inputDiv = elt("div", [input], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); |
// Covers bottom-right square when both scrollbars are present. |
d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); |
- d.scrollbarFiller.setAttribute("not-content", "true"); |
+ d.scrollbarFiller.setAttribute("cm-not-content", "true"); |
// Covers bottom of gutter when coverGutterNextToScrollbar is on |
// and h scrollbar is present. |
d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); |
- d.gutterFiller.setAttribute("not-content", "true"); |
+ d.gutterFiller.setAttribute("cm-not-content", "true"); |
// Will contain the actual code, positioned to cover the viewport. |
d.lineDiv = elt("div", null, "CodeMirror-code"); |
// Elements are added to these to represent selection and cursors. |
@@ -178,15 +173,11 @@ |
d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); |
d.scroller.setAttribute("tabIndex", "-1"); |
// The element in which the editor lives. |
- d.wrapper = elt("div", [d.inputDiv, d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); |
+ d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); |
// Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) |
if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } |
- // Needed to hide big blue blinking cursor on Mobile Safari |
- if (ios) input.style.width = "0px"; |
- if (!webkit) d.scroller.draggable = true; |
- // Needed to handle Tab key in KHTML |
- if (khtml) { d.inputDiv.style.height = "1px"; d.inputDiv.style.position = "absolute"; } |
+ if (!webkit && !(gecko && mobile)) d.scroller.draggable = true; |
if (place) { |
if (place.appendChild) place.appendChild(d.wrapper); |
@@ -213,25 +204,13 @@ |
// Used to only resize the line number gutter when necessary (when |
// the amount of lines crosses a boundary that makes its width change) |
d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; |
- // See readInput and resetInput |
- d.prevInput = ""; |
// Set to true when a non-horizontal-scrolling line widget is |
// added. As an optimization, line widget aligning is skipped when |
// this is false. |
d.alignWidgets = false; |
- // Flag that indicates whether we expect input to appear real soon |
- // now (after some event like 'keypress' or 'input') and are |
- // polling intensively. |
- d.pollingFast = false; |
- // Self-resetting timeout for the poller |
- d.poll = new Delayed(); |
d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; |
- // Tracks when resetInput has punted to just putting a short |
- // string into the textarea instead of the full selection. |
- d.inaccurateSelection = false; |
- |
// Tracks the maximum line length so that the horizontal scrollbar |
// can be kept static when scrolling. |
d.maxLine = null; |
@@ -247,6 +226,10 @@ |
// Used to track whether anything happened since the context menu |
// was opened. |
d.selForContextMenu = null; |
+ |
+ d.activeTouch = null; |
+ |
+ input.init(d); |
} |
// STATE UPDATES |
@@ -428,7 +411,7 @@ |
if (horiz.clientWidth) scroll(horiz.scrollLeft, "horizontal"); |
}); |
- this.checkedOverlay = false; |
+ this.checkedZeroWidth = false; |
// Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). |
if (ie && ie_version < 8) this.horiz.style.minHeight = this.vert.style.minWidth = "18px"; |
} |
@@ -463,29 +446,43 @@ |
this.horiz.firstChild.style.width = "0"; |
} |
- if (!this.checkedOverlay && measure.clientHeight > 0) { |
- if (sWidth == 0) this.overlayHack(); |
- this.checkedOverlay = true; |
+ if (!this.checkedZeroWidth && measure.clientHeight > 0) { |
+ if (sWidth == 0) this.zeroWidthHack(); |
+ this.checkedZeroWidth = true; |
} |
return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0}; |
}, |
setScrollLeft: function(pos) { |
if (this.horiz.scrollLeft != pos) this.horiz.scrollLeft = pos; |
+ if (this.disableHoriz) this.enableZeroWidthBar(this.horiz, this.disableHoriz); |
}, |
setScrollTop: function(pos) { |
if (this.vert.scrollTop != pos) this.vert.scrollTop = pos; |
+ if (this.disableVert) this.enableZeroWidthBar(this.vert, this.disableVert); |
}, |
- overlayHack: function() { |
+ zeroWidthHack: function() { |
var w = mac && !mac_geMountainLion ? "12px" : "18px"; |
- this.horiz.style.minHeight = this.vert.style.minWidth = w; |
- var self = this; |
- var barMouseDown = function(e) { |
- if (e_target(e) != self.vert && e_target(e) != self.horiz) |
- operation(self.cm, onMouseDown)(e); |
- }; |
- on(this.vert, "mousedown", barMouseDown); |
- on(this.horiz, "mousedown", barMouseDown); |
+ this.horiz.style.height = this.vert.style.width = w; |
+ this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none"; |
+ this.disableHoriz = new Delayed; |
+ this.disableVert = new Delayed; |
+ }, |
+ enableZeroWidthBar: function(bar, delay) { |
+ bar.style.pointerEvents = "auto"; |
+ function maybeDisable() { |
+ // To find out whether the scrollbar is still visible, we |
+ // check whether the element under the pixel in the bottom |
+ // left corner of the scrollbar box is the scrollbar box |
+ // itself (when the bar is still visible) or its filler child |
+ // (when the bar is hidden). If it is still visible, we keep |
+ // it enabled, if it's hidden, we disable pointer events. |
+ var box = bar.getBoundingClientRect(); |
+ var elt = document.elementFromPoint(box.left + 1, box.bottom - 1); |
+ if (elt != bar) bar.style.pointerEvents = "none"; |
+ else delay.set(1000, maybeDisable); |
+ } |
+ delay.set(1000, maybeDisable); |
}, |
clear: function() { |
var parent = this.horiz.parentNode; |
@@ -514,10 +511,11 @@ |
cm.display.scrollbars = new CodeMirror.scrollbarModel[cm.options.scrollbarStyle](function(node) { |
cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller); |
+ // Prevent clicks in the scrollbars from killing focus |
on(node, "mousedown", function() { |
- if (cm.state.focused) setTimeout(bind(focusInput, cm), 0); |
+ if (cm.state.focused) setTimeout(function() { cm.display.input.focus(); }, 0); |
}); |
- node.setAttribute("not-content", "true"); |
+ node.setAttribute("cm-not-content", "true"); |
}, function(pos, axis) { |
if (axis == "horizontal") setScrollLeft(cm, pos); |
else setScrollTop(cm, pos); |
@@ -546,6 +544,7 @@ |
d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px"; |
d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px"; |
+ d.heightForcer.style.borderBottom = sizes.bottom + "px solid transparent" |
if (sizes.right && sizes.bottom) { |
d.scrollbarFiller.style.display = "block"; |
@@ -614,7 +613,7 @@ |
"CodeMirror-linenumber CodeMirror-gutter-elt")); |
var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; |
display.lineGutter.style.width = ""; |
- display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding); |
+ display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1; |
display.lineNumWidth = display.lineNumInnerWidth + padding; |
display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; |
display.lineGutter.style.width = display.lineNumWidth + "px"; |
@@ -649,8 +648,18 @@ |
this.oldDisplayWidth = displayWidth(cm); |
this.force = force; |
this.dims = getDimensions(cm); |
+ this.events = []; |
} |
+ DisplayUpdate.prototype.signal = function(emitter, type) { |
+ if (hasHandler(emitter, type)) |
+ this.events.push(arguments); |
+ }; |
+ DisplayUpdate.prototype.finish = function() { |
+ for (var i = 0; i < this.events.length; i++) |
+ signal.apply(null, this.events[i]); |
+ }; |
+ |
function maybeClipScrollbars(cm) { |
var display = cm.display; |
if (!display.scrollbarsClipped && display.scroller.offsetWidth) { |
@@ -724,7 +733,7 @@ |
// width and height. |
removeChildren(display.cursorDiv); |
removeChildren(display.selectionDiv); |
- display.gutters.style.height = 0; |
+ display.gutters.style.height = display.sizer.style.minHeight = 0; |
if (different) { |
display.lastWrapHeight = update.wrapperHeight; |
@@ -738,12 +747,10 @@ |
} |
function postUpdateDisplay(cm, update) { |
- var force = update.force, viewport = update.viewport; |
+ var viewport = update.viewport; |
+ |
for (var first = true;; first = false) { |
- if (first && cm.options.lineWrapping && update.oldDisplayWidth != displayWidth(cm)) { |
- force = true; |
- } else { |
- force = 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)}; |
@@ -757,13 +764,13 @@ |
updateHeightsInViewport(cm); |
var barMeasure = measureForScrollbars(cm); |
updateSelection(cm); |
- setDocumentHeight(cm, barMeasure); |
updateScrollbars(cm, barMeasure); |
+ setDocumentHeight(cm, barMeasure); |
} |
- signalLater(cm, "update", cm); |
+ update.signal(cm, "update", cm); |
if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) { |
- signalLater(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); |
+ update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); |
cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo; |
} |
} |
@@ -775,16 +782,16 @@ |
postUpdateDisplay(cm, update); |
var barMeasure = measureForScrollbars(cm); |
updateSelection(cm); |
- setDocumentHeight(cm, barMeasure); |
updateScrollbars(cm, barMeasure); |
+ setDocumentHeight(cm, barMeasure); |
+ update.finish(); |
} |
} |
function setDocumentHeight(cm, measure) { |
cm.display.sizer.style.minHeight = measure.docHeight + "px"; |
- var total = measure.docHeight + cm.display.barHeight; |
- cm.display.heightForcer.style.top = total + "px"; |
- cm.display.gutters.style.height = Math.max(total + scrollGap(cm), measure.clientHeight) + "px"; |
+ cm.display.heightForcer.style.top = measure.docHeight + "px"; |
+ cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + "px"; |
} |
// Read the actual heights of the rendered lines, and update their |
@@ -818,7 +825,7 @@ |
// given line. |
function updateWidgetHeight(line) { |
if (line.widgets) for (var i = 0; i < line.widgets.length; ++i) |
- line.widgets[i].height = line.widgets[i].node.offsetHeight; |
+ line.widgets[i].height = line.widgets[i].node.parentNode.offsetHeight; |
} |
// Do a bulk-read of the DOM positions and sizes needed to draw the |
@@ -861,7 +868,7 @@ |
for (var i = 0; i < view.length; i++) { |
var lineView = view[i]; |
if (lineView.hidden) { |
- } else if (!lineView.node) { // Not drawn yet |
+ } else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet |
var node = buildLineElement(cm, lineView, lineN, dims); |
container.insertBefore(node, cur); |
} else { // Already drawn |
@@ -892,7 +899,7 @@ |
if (type == "text") updateLineText(cm, lineView); |
else if (type == "gutter") updateLineGutter(cm, lineView, lineN, dims); |
else if (type == "class") updateLineClasses(lineView); |
- else if (type == "widget") updateLineWidgets(lineView, dims); |
+ else if (type == "widget") updateLineWidgets(cm, lineView, dims); |
} |
lineView.changes = null; |
} |
@@ -967,14 +974,24 @@ |
lineView.node.removeChild(lineView.gutter); |
lineView.gutter = null; |
} |
+ if (lineView.gutterBackground) { |
+ lineView.node.removeChild(lineView.gutterBackground); |
+ lineView.gutterBackground = null; |
+ } |
+ if (lineView.line.gutterClass) { |
+ var wrap = ensureLineWrapped(lineView); |
+ lineView.gutterBackground = elt("div", null, "CodeMirror-gutter-background " + lineView.line.gutterClass, |
+ "left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + |
+ "px; width: " + dims.gutterTotalWidth + "px"); |
+ wrap.insertBefore(lineView.gutterBackground, lineView.text); |
+ } |
var markers = lineView.line.gutterMarkers; |
if (cm.options.lineNumbers || markers) { |
var wrap = ensureLineWrapped(lineView); |
- var gutterWrap = lineView.gutter = |
- wrap.insertBefore(elt("div", null, "CodeMirror-gutter-wrapper", "left: " + |
- (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + |
- "px; width: " + dims.gutterTotalWidth + "px"), |
- lineView.text); |
+ var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", "left: " + |
+ (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px"); |
+ cm.display.input.setUneditable(gutterWrap); |
+ wrap.insertBefore(gutterWrap, lineView.text); |
if (lineView.line.gutterClass) |
gutterWrap.className += " " + lineView.line.gutterClass; |
if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) |
@@ -992,14 +1009,14 @@ |
} |
} |
- function updateLineWidgets(lineView, dims) { |
+ function updateLineWidgets(cm, lineView, dims) { |
if (lineView.alignable) lineView.alignable = null; |
for (var node = lineView.node.firstChild, next; node; node = next) { |
var next = node.nextSibling; |
if (node.className == "CodeMirror-linewidget") |
lineView.node.removeChild(node); |
} |
- insertLineWidgets(lineView, dims); |
+ insertLineWidgets(cm, lineView, dims); |
} |
// Build a line's DOM representation from scratch |
@@ -1011,25 +1028,26 @@ |
updateLineClasses(lineView); |
updateLineGutter(cm, lineView, lineN, dims); |
- insertLineWidgets(lineView, dims); |
+ insertLineWidgets(cm, lineView, dims); |
return lineView.node; |
} |
// A lineView may contain multiple logical lines (when merged by |
// collapsed spans). The widgets for all of them need to be drawn. |
- function insertLineWidgets(lineView, dims) { |
- insertLineWidgetsFor(lineView.line, lineView, dims, true); |
+ function insertLineWidgets(cm, lineView, dims) { |
+ insertLineWidgetsFor(cm, lineView.line, lineView, dims, true); |
if (lineView.rest) for (var i = 0; i < lineView.rest.length; i++) |
- insertLineWidgetsFor(lineView.rest[i], lineView, dims, false); |
+ insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false); |
} |
- function insertLineWidgetsFor(line, lineView, dims, allowAbove) { |
+ function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) { |
if (!line.widgets) return; |
var wrap = ensureLineWrapped(lineView); |
for (var i = 0, ws = line.widgets; i < ws.length; ++i) { |
var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget"); |
if (!widget.handleMouseEvents) node.setAttribute("cm-ignore-events", "true"); |
positionLineWidget(widget, node, lineView, dims); |
+ cm.display.input.setUneditable(node); |
if (allowAbove && widget.above) |
wrap.insertBefore(node, lineView.gutter || lineView.text); |
else |
@@ -1072,6 +1090,924 @@ |
function maxPos(a, b) { return cmp(a, b) < 0 ? b : a; } |
function minPos(a, b) { return cmp(a, b) < 0 ? a : b; } |
+ // INPUT HANDLING |
+ |
+ function ensureFocus(cm) { |
+ if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm); } |
+ } |
+ |
+ // This will be set to a {lineWise: bool, text: [string]} object, so |
+ // that, when pasting, we know what kind of selections the copied |
+ // text was made out of. |
+ var lastCopied = null; |
+ |
+ function applyTextInput(cm, inserted, deleted, sel, origin) { |
+ var doc = cm.doc; |
+ cm.display.shift = false; |
+ if (!sel) sel = doc.sel; |
+ |
+ var paste = cm.state.pasteIncoming || origin == "paste"; |
+ var textLines = doc.splitLines(inserted), multiPaste = null |
+ // When pasing N lines into N selections, insert one line per selection |
+ if (paste && sel.ranges.length > 1) { |
+ if (lastCopied && lastCopied.text.join("\n") == inserted) { |
+ if (sel.ranges.length % lastCopied.text.length == 0) { |
+ multiPaste = []; |
+ for (var i = 0; i < lastCopied.text.length; i++) |
+ multiPaste.push(doc.splitLines(lastCopied.text[i])); |
+ } |
+ } else if (textLines.length == sel.ranges.length) { |
+ multiPaste = map(textLines, function(l) { return [l]; }); |
+ } |
+ } |
+ |
+ // Normal behavior is to insert the new text into every selection |
+ for (var i = sel.ranges.length - 1; i >= 0; i--) { |
+ var range = sel.ranges[i]; |
+ var from = range.from(), to = range.to(); |
+ if (range.empty()) { |
+ if (deleted && deleted > 0) // Handle deletion |
+ from = Pos(from.line, from.ch - deleted); |
+ else if (cm.state.overwrite && !paste) // Handle overwrite |
+ to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)); |
+ else if (lastCopied && lastCopied.lineWise && lastCopied.text.join("\n") == inserted) |
+ from = to = Pos(from.line, 0) |
+ } |
+ var updateInput = cm.curOp.updateInput; |
+ var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i % multiPaste.length] : textLines, |
+ origin: origin || (paste ? "paste" : cm.state.cutIncoming ? "cut" : "+input")}; |
+ makeChange(cm.doc, changeEvent); |
+ signalLater(cm, "inputRead", cm, changeEvent); |
+ } |
+ if (inserted && !paste) |
+ triggerElectric(cm, inserted); |
+ |
+ ensureCursorVisible(cm); |
+ cm.curOp.updateInput = updateInput; |
+ cm.curOp.typing = true; |
+ cm.state.pasteIncoming = cm.state.cutIncoming = false; |
+ } |
+ |
+ function handlePaste(e, cm) { |
+ var pasted = e.clipboardData && e.clipboardData.getData("text/plain"); |
+ if (pasted) { |
+ e.preventDefault(); |
+ if (!cm.isReadOnly() && !cm.options.disableInput) |
+ runInOp(cm, function() { applyTextInput(cm, pasted, 0, null, "paste"); }); |
+ return true; |
+ } |
+ } |
+ |
+ function triggerElectric(cm, inserted) { |
+ // When an 'electric' character is inserted, immediately trigger a reindent |
+ if (!cm.options.electricChars || !cm.options.smartIndent) return; |
+ var sel = cm.doc.sel; |
+ |
+ for (var i = sel.ranges.length - 1; i >= 0; i--) { |
+ var range = sel.ranges[i]; |
+ if (range.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range.head.line)) continue; |
+ var mode = cm.getModeAt(range.head); |
+ var indented = false; |
+ if (mode.electricChars) { |
+ for (var j = 0; j < mode.electricChars.length; j++) |
+ if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) { |
+ indented = indentLine(cm, range.head.line, "smart"); |
+ break; |
+ } |
+ } else if (mode.electricInput) { |
+ if (mode.electricInput.test(getLine(cm.doc, range.head.line).text.slice(0, range.head.ch))) |
+ indented = indentLine(cm, range.head.line, "smart"); |
+ } |
+ if (indented) signalLater(cm, "electricInput", cm, range.head.line); |
+ } |
+ } |
+ |
+ function copyableRanges(cm) { |
+ var text = [], ranges = []; |
+ for (var i = 0; i < cm.doc.sel.ranges.length; i++) { |
+ var line = cm.doc.sel.ranges[i].head.line; |
+ var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)}; |
+ ranges.push(lineRange); |
+ text.push(cm.getRange(lineRange.anchor, lineRange.head)); |
+ } |
+ return {text: text, ranges: ranges}; |
+ } |
+ |
+ function disableBrowserMagic(field) { |
+ field.setAttribute("autocorrect", "off"); |
+ field.setAttribute("autocapitalize", "off"); |
+ field.setAttribute("spellcheck", "false"); |
+ } |
+ |
+ // TEXTAREA INPUT STYLE |
+ |
+ function TextareaInput(cm) { |
+ this.cm = cm; |
+ // See input.poll and input.reset |
+ this.prevInput = ""; |
+ |
+ // Flag that indicates whether we expect input to appear real soon |
+ // now (after some event like 'keypress' or 'input') and are |
+ // polling intensively. |
+ this.pollingFast = false; |
+ // Self-resetting timeout for the poller |
+ this.polling = new Delayed(); |
+ // Tracks when input.reset has punted to just putting a short |
+ // string into the textarea instead of the full selection. |
+ this.inaccurateSelection = false; |
+ // Used to work around IE issue with selection being forgotten when focus moves away from textarea |
+ this.hasSelection = false; |
+ this.composing = null; |
+ }; |
+ |
+ function hiddenTextarea() { |
+ var te = elt("textarea", null, null, "position: absolute; bottom: -1em; padding: 0; width: 1px; height: 1em; outline: none"); |
+ var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); |
+ // The textarea is kept positioned near the cursor to prevent the |
+ // fact that it'll be scrolled into view on input from scrolling |
+ // our fake cursor out of view. On webkit, when wrap=off, paste is |
+ // very slow. So make the area wide instead. |
+ if (webkit) te.style.width = "1000px"; |
+ else te.setAttribute("wrap", "off"); |
+ // If border: 0; -- iOS fails to open keyboard (issue #1287) |
+ if (ios) te.style.border = "1px solid black"; |
+ disableBrowserMagic(te); |
+ return div; |
+ } |
+ |
+ TextareaInput.prototype = copyObj({ |
+ init: function(display) { |
+ var input = this, cm = this.cm; |
+ |
+ // Wraps and hides input textarea |
+ var div = this.wrapper = hiddenTextarea(); |
+ // The semihidden textarea that is focused when the editor is |
+ // focused, and receives input. |
+ var te = this.textarea = div.firstChild; |
+ display.wrapper.insertBefore(div, display.wrapper.firstChild); |
+ |
+ // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore) |
+ if (ios) te.style.width = "0px"; |
+ |
+ on(te, "input", function() { |
+ if (ie && ie_version >= 9 && input.hasSelection) input.hasSelection = null; |
+ input.poll(); |
+ }); |
+ |
+ on(te, "paste", function(e) { |
+ if (signalDOMEvent(cm, e) || handlePaste(e, cm)) return |
+ |
+ cm.state.pasteIncoming = true; |
+ input.fastPoll(); |
+ }); |
+ |
+ function prepareCopyCut(e) { |
+ if (signalDOMEvent(cm, e)) return |
+ if (cm.somethingSelected()) { |
+ lastCopied = {lineWise: false, text: cm.getSelections()}; |
+ if (input.inaccurateSelection) { |
+ input.prevInput = ""; |
+ input.inaccurateSelection = false; |
+ te.value = lastCopied.text.join("\n"); |
+ selectInput(te); |
+ } |
+ } else if (!cm.options.lineWiseCopyCut) { |
+ return; |
+ } else { |
+ var ranges = copyableRanges(cm); |
+ lastCopied = {lineWise: true, text: ranges.text}; |
+ if (e.type == "cut") { |
+ cm.setSelections(ranges.ranges, null, sel_dontScroll); |
+ } else { |
+ input.prevInput = ""; |
+ te.value = ranges.text.join("\n"); |
+ selectInput(te); |
+ } |
+ } |
+ if (e.type == "cut") cm.state.cutIncoming = true; |
+ } |
+ on(te, "cut", prepareCopyCut); |
+ on(te, "copy", prepareCopyCut); |
+ |
+ on(display.scroller, "paste", function(e) { |
+ if (eventInWidget(display, e) || signalDOMEvent(cm, e)) return; |
+ cm.state.pasteIncoming = true; |
+ input.focus(); |
+ }); |
+ |
+ // Prevent normal selection in the editor (we handle our own) |
+ on(display.lineSpace, "selectstart", function(e) { |
+ if (!eventInWidget(display, e)) e_preventDefault(e); |
+ }); |
+ |
+ on(te, "compositionstart", function() { |
+ var start = cm.getCursor("from"); |
+ if (input.composing) input.composing.range.clear() |
+ input.composing = { |
+ start: start, |
+ range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"}) |
+ }; |
+ }); |
+ on(te, "compositionend", function() { |
+ if (input.composing) { |
+ input.poll(); |
+ input.composing.range.clear(); |
+ input.composing = null; |
+ } |
+ }); |
+ }, |
+ |
+ prepareSelection: function() { |
+ // Redraw the selection and/or cursor |
+ var cm = this.cm, display = cm.display, doc = cm.doc; |
+ var result = prepareSelection(cm); |
+ |
+ // Move the hidden textarea near the cursor to prevent scrolling artifacts |
+ if (cm.options.moveInputWithCursor) { |
+ var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); |
+ var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); |
+ result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, |
+ headPos.top + lineOff.top - wrapOff.top)); |
+ result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, |
+ headPos.left + lineOff.left - wrapOff.left)); |
+ } |
+ |
+ return result; |
+ }, |
+ |
+ showSelection: function(drawn) { |
+ var cm = this.cm, display = cm.display; |
+ removeChildrenAndAdd(display.cursorDiv, drawn.cursors); |
+ removeChildrenAndAdd(display.selectionDiv, drawn.selection); |
+ if (drawn.teTop != null) { |
+ this.wrapper.style.top = drawn.teTop + "px"; |
+ this.wrapper.style.left = drawn.teLeft + "px"; |
+ } |
+ }, |
+ |
+ // Reset the input to correspond to the selection (or to be empty, |
+ // when not typing and nothing is selected) |
+ reset: function(typing) { |
+ if (this.contextMenuPending) return; |
+ var minimal, selected, cm = this.cm, doc = cm.doc; |
+ if (cm.somethingSelected()) { |
+ this.prevInput = ""; |
+ var range = doc.sel.primary(); |
+ minimal = hasCopyEvent && |
+ (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000); |
+ var content = minimal ? "-" : selected || cm.getSelection(); |
+ this.textarea.value = content; |
+ if (cm.state.focused) selectInput(this.textarea); |
+ if (ie && ie_version >= 9) this.hasSelection = content; |
+ } else if (!typing) { |
+ this.prevInput = this.textarea.value = ""; |
+ if (ie && ie_version >= 9) this.hasSelection = null; |
+ } |
+ this.inaccurateSelection = minimal; |
+ }, |
+ |
+ getField: function() { return this.textarea; }, |
+ |
+ supportsTouch: function() { return false; }, |
+ |
+ focus: function() { |
+ if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { |
+ try { this.textarea.focus(); } |
+ catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM |
+ } |
+ }, |
+ |
+ blur: function() { this.textarea.blur(); }, |
+ |
+ resetPosition: function() { |
+ this.wrapper.style.top = this.wrapper.style.left = 0; |
+ }, |
+ |
+ receivedFocus: function() { this.slowPoll(); }, |
+ |
+ // Poll for input changes, using the normal rate of polling. This |
+ // runs as long as the editor is focused. |
+ slowPoll: function() { |
+ var input = this; |
+ if (input.pollingFast) return; |
+ input.polling.set(this.cm.options.pollInterval, function() { |
+ input.poll(); |
+ if (input.cm.state.focused) input.slowPoll(); |
+ }); |
+ }, |
+ |
+ // When an event has just come in that is likely to add or change |
+ // something in the input textarea, we poll faster, to ensure that |
+ // the change appears on the screen quickly. |
+ fastPoll: function() { |
+ var missed = false, input = this; |
+ input.pollingFast = true; |
+ function p() { |
+ var changed = input.poll(); |
+ if (!changed && !missed) {missed = true; input.polling.set(60, p);} |
+ else {input.pollingFast = false; input.slowPoll();} |
+ } |
+ input.polling.set(20, p); |
+ }, |
+ |
+ // Read input from the textarea, and update the document to match. |
+ // When something is selected, it is present in the textarea, and |
+ // selected (unless it is huge, in which case a placeholder is |
+ // used). When nothing is selected, the cursor sits after previously |
+ // seen text (can be empty), which is stored in prevInput (we must |
+ // not reset the textarea when typing, because that breaks IME). |
+ poll: function() { |
+ var cm = this.cm, input = this.textarea, prevInput = this.prevInput; |
+ // Since this is called a *lot*, try to bail out as cheaply as |
+ // possible when it is clear that nothing happened. hasSelection |
+ // will be the case when there is a lot of text in the textarea, |
+ // in which case reading its value would be expensive. |
+ if (this.contextMenuPending || !cm.state.focused || |
+ (hasSelection(input) && !prevInput && !this.composing) || |
+ cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq) |
+ return false; |
+ |
+ var text = input.value; |
+ // If nothing changed, bail. |
+ if (text == prevInput && !cm.somethingSelected()) return false; |
+ // Work around nonsensical selection resetting in IE9/10, and |
+ // inexplicable appearance of private area unicode characters on |
+ // some key combos in Mac (#2689). |
+ if (ie && ie_version >= 9 && this.hasSelection === text || |
+ mac && /[\uf700-\uf7ff]/.test(text)) { |
+ cm.display.input.reset(); |
+ return false; |
+ } |
+ |
+ if (cm.doc.sel == cm.display.selForContextMenu) { |
+ var first = text.charCodeAt(0); |
+ if (first == 0x200b && !prevInput) prevInput = "\u200b"; |
+ if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo"); } |
+ } |
+ // Find the part of the input that is actually new |
+ var same = 0, l = Math.min(prevInput.length, text.length); |
+ while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same; |
+ |
+ var self = this; |
+ runInOp(cm, function() { |
+ applyTextInput(cm, text.slice(same), prevInput.length - same, |
+ null, self.composing ? "*compose" : null); |
+ |
+ // Don't leave long text in the textarea, since it makes further polling slow |
+ if (text.length > 1000 || text.indexOf("\n") > -1) input.value = self.prevInput = ""; |
+ else self.prevInput = text; |
+ |
+ if (self.composing) { |
+ self.composing.range.clear(); |
+ self.composing.range = cm.markText(self.composing.start, cm.getCursor("to"), |
+ {className: "CodeMirror-composing"}); |
+ } |
+ }); |
+ return true; |
+ }, |
+ |
+ ensurePolled: function() { |
+ if (this.pollingFast && this.poll()) this.pollingFast = false; |
+ }, |
+ |
+ onKeyPress: function() { |
+ if (ie && ie_version >= 9) this.hasSelection = null; |
+ this.fastPoll(); |
+ }, |
+ |
+ onContextMenu: function(e) { |
+ var input = this, cm = input.cm, display = cm.display, te = input.textarea; |
+ var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; |
+ if (!pos || presto) return; // Opera is difficult. |
+ |
+ // Reset the current text selection only if the click is done outside of the selection |
+ // and 'resetSelectionOnContextMenu' option is true. |
+ var reset = cm.options.resetSelectionOnContextMenu; |
+ if (reset && cm.doc.sel.contains(pos) == -1) |
+ operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll); |
+ |
+ var oldCSS = te.style.cssText, oldWrapperCSS = input.wrapper.style.cssText; |
+ input.wrapper.style.cssText = "position: absolute" |
+ var wrapperBox = input.wrapper.getBoundingClientRect() |
+ te.style.cssText = "position: absolute; width: 30px; height: 30px; top: " + (e.clientY - wrapperBox.top - 5) + |
+ "px; left: " + (e.clientX - wrapperBox.left - 5) + "px; z-index: 1000; background: " + |
+ (ie ? "rgba(255, 255, 255, .05)" : "transparent") + |
+ "; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; |
+ if (webkit) var oldScrollY = window.scrollY; // Work around Chrome issue (#2712) |
+ display.input.focus(); |
+ if (webkit) window.scrollTo(null, oldScrollY); |
+ display.input.reset(); |
+ // Adds "Select all" to context menu in FF |
+ if (!cm.somethingSelected()) te.value = input.prevInput = " "; |
+ input.contextMenuPending = true; |
+ display.selForContextMenu = cm.doc.sel; |
+ clearTimeout(display.detectingSelectAll); |
+ |
+ // Select-all will be greyed out if there's nothing to select, so |
+ // this adds a zero-width space so that we can later check whether |
+ // it got selected. |
+ function prepareSelectAllHack() { |
+ if (te.selectionStart != null) { |
+ var selected = cm.somethingSelected(); |
+ var extval = "\u200b" + (selected ? te.value : ""); |
+ te.value = "\u21da"; // Used to catch context-menu undo |
+ te.value = extval; |
+ input.prevInput = selected ? "" : "\u200b"; |
+ te.selectionStart = 1; te.selectionEnd = extval.length; |
+ // Re-set this, in case some other handler touched the |
+ // selection in the meantime. |
+ display.selForContextMenu = cm.doc.sel; |
+ } |
+ } |
+ function rehide() { |
+ input.contextMenuPending = false; |
+ input.wrapper.style.cssText = oldWrapperCSS |
+ te.style.cssText = oldCSS; |
+ if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos); |
+ |
+ // Try to detect the user choosing select-all |
+ if (te.selectionStart != null) { |
+ if (!ie || (ie && ie_version < 9)) prepareSelectAllHack(); |
+ var i = 0, poll = function() { |
+ if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 && |
+ te.selectionEnd > 0 && input.prevInput == "\u200b") |
+ operation(cm, commands.selectAll)(cm); |
+ else if (i++ < 10) display.detectingSelectAll = setTimeout(poll, 500); |
+ else display.input.reset(); |
+ }; |
+ display.detectingSelectAll = setTimeout(poll, 200); |
+ } |
+ } |
+ |
+ if (ie && ie_version >= 9) prepareSelectAllHack(); |
+ if (captureRightClick) { |
+ e_stop(e); |
+ var mouseup = function() { |
+ off(window, "mouseup", mouseup); |
+ setTimeout(rehide, 20); |
+ }; |
+ on(window, "mouseup", mouseup); |
+ } else { |
+ setTimeout(rehide, 50); |
+ } |
+ }, |
+ |
+ readOnlyChanged: function(val) { |
+ if (!val) this.reset(); |
+ }, |
+ |
+ setUneditable: nothing, |
+ |
+ needsContentAttribute: false |
+ }, TextareaInput.prototype); |
+ |
+ // CONTENTEDITABLE INPUT STYLE |
+ |
+ function ContentEditableInput(cm) { |
+ this.cm = cm; |
+ this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null; |
+ this.polling = new Delayed(); |
+ this.gracePeriod = false; |
+ } |
+ |
+ ContentEditableInput.prototype = copyObj({ |
+ init: function(display) { |
+ var input = this, cm = input.cm; |
+ var div = input.div = display.lineDiv; |
+ disableBrowserMagic(div); |
+ |
+ on(div, "paste", function(e) { |
+ if (!signalDOMEvent(cm, e)) handlePaste(e, cm); |
+ }) |
+ |
+ on(div, "compositionstart", function(e) { |
+ var data = e.data; |
+ input.composing = {sel: cm.doc.sel, data: data, startData: data}; |
+ if (!data) return; |
+ var prim = cm.doc.sel.primary(); |
+ var line = cm.getLine(prim.head.line); |
+ var found = line.indexOf(data, Math.max(0, prim.head.ch - data.length)); |
+ if (found > -1 && found <= prim.head.ch) |
+ input.composing.sel = simpleSelection(Pos(prim.head.line, found), |
+ Pos(prim.head.line, found + data.length)); |
+ }); |
+ on(div, "compositionupdate", function(e) { |
+ input.composing.data = e.data; |
+ }); |
+ on(div, "compositionend", function(e) { |
+ var ours = input.composing; |
+ if (!ours) return; |
+ if (e.data != ours.startData && !/\u200b/.test(e.data)) |
+ ours.data = e.data; |
+ // Need a small delay to prevent other code (input event, |
+ // selection polling) from doing damage when fired right after |
+ // compositionend. |
+ setTimeout(function() { |
+ if (!ours.handled) |
+ input.applyComposition(ours); |
+ if (input.composing == ours) |
+ input.composing = null; |
+ }, 50); |
+ }); |
+ |
+ on(div, "touchstart", function() { |
+ input.forceCompositionEnd(); |
+ }); |
+ |
+ on(div, "input", function() { |
+ if (input.composing) return; |
+ if (cm.isReadOnly() || !input.pollContent()) |
+ runInOp(input.cm, function() {regChange(cm);}); |
+ }); |
+ |
+ function onCopyCut(e) { |
+ if (signalDOMEvent(cm, e)) return |
+ if (cm.somethingSelected()) { |
+ lastCopied = {lineWise: false, text: cm.getSelections()}; |
+ if (e.type == "cut") cm.replaceSelection("", null, "cut"); |
+ } else if (!cm.options.lineWiseCopyCut) { |
+ return; |
+ } else { |
+ var ranges = copyableRanges(cm); |
+ lastCopied = {lineWise: true, text: ranges.text}; |
+ if (e.type == "cut") { |
+ cm.operation(function() { |
+ cm.setSelections(ranges.ranges, 0, sel_dontScroll); |
+ cm.replaceSelection("", null, "cut"); |
+ }); |
+ } |
+ } |
+ // iOS exposes the clipboard API, but seems to discard content inserted into it |
+ if (e.clipboardData && !ios) { |
+ e.preventDefault(); |
+ e.clipboardData.clearData(); |
+ e.clipboardData.setData("text/plain", lastCopied.text.join("\n")); |
+ } else { |
+ // Old-fashioned briefly-focus-a-textarea hack |
+ var kludge = hiddenTextarea(), te = kludge.firstChild; |
+ cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild); |
+ te.value = lastCopied.text.join("\n"); |
+ var hadFocus = document.activeElement; |
+ selectInput(te); |
+ setTimeout(function() { |
+ cm.display.lineSpace.removeChild(kludge); |
+ hadFocus.focus(); |
+ }, 50); |
+ } |
+ } |
+ on(div, "copy", onCopyCut); |
+ on(div, "cut", onCopyCut); |
+ }, |
+ |
+ prepareSelection: function() { |
+ var result = prepareSelection(this.cm, false); |
+ result.focus = this.cm.state.focused; |
+ return result; |
+ }, |
+ |
+ showSelection: function(info, takeFocus) { |
+ if (!info || !this.cm.display.view.length) return; |
+ if (info.focus || takeFocus) this.showPrimarySelection(); |
+ this.showMultipleSelections(info); |
+ }, |
+ |
+ showPrimarySelection: function() { |
+ var sel = window.getSelection(), prim = this.cm.doc.sel.primary(); |
+ var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset); |
+ var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset); |
+ if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad && |
+ cmp(minPos(curAnchor, curFocus), prim.from()) == 0 && |
+ cmp(maxPos(curAnchor, curFocus), prim.to()) == 0) |
+ return; |
+ |
+ var start = posToDOM(this.cm, prim.from()); |
+ var end = posToDOM(this.cm, prim.to()); |
+ if (!start && !end) return; |
+ |
+ var view = this.cm.display.view; |
+ var old = sel.rangeCount && sel.getRangeAt(0); |
+ if (!start) { |
+ start = {node: view[0].measure.map[2], offset: 0}; |
+ } else if (!end) { // FIXME dangerously hacky |
+ var measure = view[view.length - 1].measure; |
+ var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map; |
+ end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]}; |
+ } |
+ |
+ try { var rng = range(start.node, start.offset, end.offset, end.node); } |
+ catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible |
+ if (rng) { |
+ if (!gecko && this.cm.state.focused) { |
+ sel.collapse(start.node, start.offset); |
+ if (!rng.collapsed) sel.addRange(rng); |
+ } else { |
+ sel.removeAllRanges(); |
+ sel.addRange(rng); |
+ } |
+ if (old && sel.anchorNode == null) sel.addRange(old); |
+ else if (gecko) this.startGracePeriod(); |
+ } |
+ this.rememberSelection(); |
+ }, |
+ |
+ startGracePeriod: function() { |
+ var input = this; |
+ clearTimeout(this.gracePeriod); |
+ this.gracePeriod = setTimeout(function() { |
+ input.gracePeriod = false; |
+ if (input.selectionChanged()) |
+ input.cm.operation(function() { input.cm.curOp.selectionChanged = true; }); |
+ }, 20); |
+ }, |
+ |
+ showMultipleSelections: function(info) { |
+ removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors); |
+ removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection); |
+ }, |
+ |
+ rememberSelection: function() { |
+ var sel = window.getSelection(); |
+ this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset; |
+ this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset; |
+ }, |
+ |
+ selectionInEditor: function() { |
+ var sel = window.getSelection(); |
+ if (!sel.rangeCount) return false; |
+ var node = sel.getRangeAt(0).commonAncestorContainer; |
+ return contains(this.div, node); |
+ }, |
+ |
+ focus: function() { |
+ if (this.cm.options.readOnly != "nocursor") this.div.focus(); |
+ }, |
+ blur: function() { this.div.blur(); }, |
+ getField: function() { return this.div; }, |
+ |
+ supportsTouch: function() { return true; }, |
+ |
+ receivedFocus: function() { |
+ var input = this; |
+ if (this.selectionInEditor()) |
+ this.pollSelection(); |
+ else |
+ runInOp(this.cm, function() { input.cm.curOp.selectionChanged = true; }); |
+ |
+ function poll() { |
+ if (input.cm.state.focused) { |
+ input.pollSelection(); |
+ input.polling.set(input.cm.options.pollInterval, poll); |
+ } |
+ } |
+ this.polling.set(this.cm.options.pollInterval, poll); |
+ }, |
+ |
+ selectionChanged: function() { |
+ var sel = window.getSelection(); |
+ return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || |
+ sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset; |
+ }, |
+ |
+ pollSelection: function() { |
+ if (!this.composing && !this.gracePeriod && this.selectionChanged()) { |
+ var sel = window.getSelection(), cm = this.cm; |
+ this.rememberSelection(); |
+ var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); |
+ var head = domToPos(cm, sel.focusNode, sel.focusOffset); |
+ if (anchor && head) runInOp(cm, function() { |
+ setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll); |
+ if (anchor.bad || head.bad) cm.curOp.selectionChanged = true; |
+ }); |
+ } |
+ }, |
+ |
+ pollContent: function() { |
+ var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary(); |
+ var from = sel.from(), to = sel.to(); |
+ if (from.line < display.viewFrom || to.line > display.viewTo - 1) return false; |
+ |
+ var fromIndex; |
+ if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) { |
+ var fromLine = lineNo(display.view[0].line); |
+ var fromNode = display.view[0].node; |
+ } else { |
+ var fromLine = lineNo(display.view[fromIndex].line); |
+ var fromNode = display.view[fromIndex - 1].node.nextSibling; |
+ } |
+ var toIndex = findViewIndex(cm, to.line); |
+ if (toIndex == display.view.length - 1) { |
+ var toLine = display.viewTo - 1; |
+ var toNode = display.lineDiv.lastChild; |
+ } else { |
+ var toLine = lineNo(display.view[toIndex + 1].line) - 1; |
+ var toNode = display.view[toIndex + 1].node.previousSibling; |
+ } |
+ |
+ var newText = cm.doc.splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine)); |
+ var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length)); |
+ while (newText.length > 1 && oldText.length > 1) { |
+ if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; } |
+ else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; } |
+ else break; |
+ } |
+ |
+ var cutFront = 0, cutEnd = 0; |
+ var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length); |
+ while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront)) |
+ ++cutFront; |
+ var newBot = lst(newText), oldBot = lst(oldText); |
+ var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0), |
+ oldBot.length - (oldText.length == 1 ? cutFront : 0)); |
+ while (cutEnd < maxCutEnd && |
+ newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) |
+ ++cutEnd; |
+ |
+ newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd); |
+ newText[0] = newText[0].slice(cutFront); |
+ |
+ var chFrom = Pos(fromLine, cutFront); |
+ var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0); |
+ if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) { |
+ replaceRange(cm.doc, newText, chFrom, chTo, "+input"); |
+ return true; |
+ } |
+ }, |
+ |
+ ensurePolled: function() { |
+ this.forceCompositionEnd(); |
+ }, |
+ reset: function() { |
+ this.forceCompositionEnd(); |
+ }, |
+ forceCompositionEnd: function() { |
+ if (!this.composing || this.composing.handled) return; |
+ this.applyComposition(this.composing); |
+ this.composing.handled = true; |
+ this.div.blur(); |
+ this.div.focus(); |
+ }, |
+ applyComposition: function(composing) { |
+ if (this.cm.isReadOnly()) |
+ operation(this.cm, regChange)(this.cm) |
+ else if (composing.data && composing.data != composing.startData) |
+ operation(this.cm, applyTextInput)(this.cm, composing.data, 0, composing.sel); |
+ }, |
+ |
+ setUneditable: function(node) { |
+ node.contentEditable = "false" |
+ }, |
+ |
+ onKeyPress: function(e) { |
+ e.preventDefault(); |
+ if (!this.cm.isReadOnly()) |
+ operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); |
+ }, |
+ |
+ readOnlyChanged: function(val) { |
+ this.div.contentEditable = String(val != "nocursor") |
+ }, |
+ |
+ onContextMenu: nothing, |
+ resetPosition: nothing, |
+ |
+ needsContentAttribute: true |
+ }, ContentEditableInput.prototype); |
+ |
+ function posToDOM(cm, pos) { |
+ var view = findViewForLine(cm, pos.line); |
+ if (!view || view.hidden) return null; |
+ var line = getLine(cm.doc, pos.line); |
+ var info = mapFromLineView(view, line, pos.line); |
+ |
+ var order = getOrder(line), side = "left"; |
+ if (order) { |
+ var partPos = getBidiPartAt(order, pos.ch); |
+ side = partPos % 2 ? "right" : "left"; |
+ } |
+ var result = nodeAndOffsetInLineMap(info.map, pos.ch, side); |
+ result.offset = result.collapse == "right" ? result.end : result.start; |
+ return result; |
+ } |
+ |
+ function badPos(pos, bad) { if (bad) pos.bad = true; return pos; } |
+ |
+ function domToPos(cm, node, offset) { |
+ var lineNode; |
+ if (node == cm.display.lineDiv) { |
+ lineNode = cm.display.lineDiv.childNodes[offset]; |
+ if (!lineNode) return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true); |
+ node = null; offset = 0; |
+ } else { |
+ for (lineNode = node;; lineNode = lineNode.parentNode) { |
+ if (!lineNode || lineNode == cm.display.lineDiv) return null; |
+ if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) break; |
+ } |
+ } |
+ for (var i = 0; i < cm.display.view.length; i++) { |
+ var lineView = cm.display.view[i]; |
+ if (lineView.node == lineNode) |
+ return locateNodeInLineView(lineView, node, offset); |
+ } |
+ } |
+ |
+ function locateNodeInLineView(lineView, node, offset) { |
+ var wrapper = lineView.text.firstChild, bad = false; |
+ if (!node || !contains(wrapper, node)) return badPos(Pos(lineNo(lineView.line), 0), true); |
+ if (node == wrapper) { |
+ bad = true; |
+ node = wrapper.childNodes[offset]; |
+ offset = 0; |
+ if (!node) { |
+ var line = lineView.rest ? lst(lineView.rest) : lineView.line; |
+ return badPos(Pos(lineNo(line), line.text.length), bad); |
+ } |
+ } |
+ |
+ var textNode = node.nodeType == 3 ? node : null, topNode = node; |
+ if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) { |
+ textNode = node.firstChild; |
+ if (offset) offset = textNode.nodeValue.length; |
+ } |
+ while (topNode.parentNode != wrapper) topNode = topNode.parentNode; |
+ var measure = lineView.measure, maps = measure.maps; |
+ |
+ function find(textNode, topNode, offset) { |
+ for (var i = -1; i < (maps ? maps.length : 0); i++) { |
+ var map = i < 0 ? measure.map : maps[i]; |
+ for (var j = 0; j < map.length; j += 3) { |
+ var curNode = map[j + 2]; |
+ if (curNode == textNode || curNode == topNode) { |
+ var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]); |
+ var ch = map[j] + offset; |
+ if (offset < 0 || curNode != textNode) ch = map[j + (offset ? 1 : 0)]; |
+ return Pos(line, ch); |
+ } |
+ } |
+ } |
+ } |
+ var found = find(textNode, topNode, offset); |
+ if (found) return badPos(found, bad); |
+ |
+ // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems |
+ for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) { |
+ found = find(after, after.firstChild, 0); |
+ if (found) |
+ return badPos(Pos(found.line, found.ch - dist), bad); |
+ else |
+ dist += after.textContent.length; |
+ } |
+ for (var before = topNode.previousSibling, dist = offset; before; before = before.previousSibling) { |
+ found = find(before, before.firstChild, -1); |
+ if (found) |
+ return badPos(Pos(found.line, found.ch + dist), bad); |
+ else |
+ dist += after.textContent.length; |
+ } |
+ } |
+ |
+ function domTextBetween(cm, from, to, fromLine, toLine) { |
+ var text = "", closing = false, lineSep = cm.doc.lineSeparator(); |
+ function recognizeMarker(id) { return function(marker) { return marker.id == id; }; } |
+ function walk(node) { |
+ if (node.nodeType == 1) { |
+ var cmText = node.getAttribute("cm-text"); |
+ if (cmText != null) { |
+ if (cmText == "") cmText = node.textContent.replace(/\u200b/g, ""); |
+ text += cmText; |
+ return; |
+ } |
+ var markerID = node.getAttribute("cm-marker"), range; |
+ if (markerID) { |
+ var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID)); |
+ if (found.length && (range = found[0].find())) |
+ text += getBetween(cm.doc, range.from, range.to).join(lineSep); |
+ return; |
+ } |
+ if (node.getAttribute("contenteditable") == "false") return; |
+ for (var i = 0; i < node.childNodes.length; i++) |
+ walk(node.childNodes[i]); |
+ if (/^(pre|div|p)$/i.test(node.nodeName)) |
+ closing = true; |
+ } else if (node.nodeType == 3) { |
+ var val = node.nodeValue; |
+ if (!val) return; |
+ if (closing) { |
+ text += lineSep; |
+ closing = false; |
+ } |
+ text += val; |
+ } |
+ } |
+ for (;;) { |
+ walk(from); |
+ if (from == to) break; |
+ from = from.nextSibling; |
+ } |
+ return text; |
+ } |
+ |
+ CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; |
+ |
// SELECTION / CURSOR |
// Selection objects are immutable. A new one is created every time |
@@ -1228,7 +2164,7 @@ |
// Give beforeSelectionChange handlers a change to influence a |
// selection update. |
- function filterSelectionChange(doc, sel) { |
+ function filterSelectionChange(doc, sel, options) { |
var obj = { |
ranges: sel.ranges, |
update: function(ranges) { |
@@ -1236,7 +2172,8 @@ |
for (var i = 0; i < ranges.length; i++) |
this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), |
clipPos(doc, ranges[i].head)); |
- } |
+ }, |
+ origin: options && options.origin |
}; |
signal(doc, "beforeSelectionChange", doc, obj); |
if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj); |
@@ -1262,7 +2199,7 @@ |
function setSelectionNoUndo(doc, sel, options) { |
if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) |
- sel = filterSelectionChange(doc, sel); |
+ sel = filterSelectionChange(doc, sel, options); |
var bias = options && options.bias || |
(cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1); |
@@ -1296,8 +2233,9 @@ |
var out; |
for (var i = 0; i < sel.ranges.length; i++) { |
var range = sel.ranges[i]; |
- var newAnchor = skipAtomic(doc, range.anchor, bias, mayClear); |
- var newHead = skipAtomic(doc, range.head, bias, mayClear); |
+ var old = sel.ranges.length == doc.sel.ranges.length && doc.sel.ranges[i]; |
+ var newAnchor = skipAtomic(doc, range.anchor, old && old.anchor, bias, mayClear); |
+ var newHead = skipAtomic(doc, range.head, old && old.head, bias, mayClear); |
if (out || newAnchor != range.anchor || newHead != range.head) { |
if (!out) out = sel.ranges.slice(0, i); |
out[i] = new Range(newAnchor, newHead); |
@@ -1306,103 +2244,91 @@ |
return out ? normalizeSelection(out, sel.primIndex) : sel; |
} |
- // Ensure a given position is not inside an atomic range. |
- function skipAtomic(doc, pos, bias, mayClear) { |
- var flipped = false, curPos = pos; |
- var dir = bias || 1; |
- doc.cantEdit = false; |
- search: for (;;) { |
- var line = getLine(doc, curPos.line); |
- if (line.markedSpans) { |
- for (var i = 0; i < line.markedSpans.length; ++i) { |
- var sp = line.markedSpans[i], m = sp.marker; |
- if ((sp.from == null || (m.inclusiveLeft ? sp.from <= curPos.ch : sp.from < curPos.ch)) && |
- (sp.to == null || (m.inclusiveRight ? sp.to >= curPos.ch : sp.to > curPos.ch))) { |
- if (mayClear) { |
- signal(m, "beforeCursorEnter"); |
- if (m.explicitlyCleared) { |
- if (!line.markedSpans) break; |
- else {--i; continue;} |
- } |
- } |
- if (!m.atomic) continue; |
- var newPos = m.find(dir < 0 ? -1 : 1); |
- if (cmp(newPos, curPos) == 0) { |
- newPos.ch += dir; |
- if (newPos.ch < 0) { |
- if (newPos.line > doc.first) newPos = clipPos(doc, Pos(newPos.line - 1)); |
- else newPos = null; |
- } else if (newPos.ch > line.text.length) { |
- if (newPos.line < doc.first + doc.size - 1) newPos = Pos(newPos.line + 1, 0); |
- else newPos = null; |
- } |
- if (!newPos) { |
- if (flipped) { |
- // Driven in a corner -- no valid cursor position found at all |
- // -- try again *with* clearing, if we didn't already |
- if (!mayClear) return skipAtomic(doc, pos, bias, true); |
- // Otherwise, turn off editing until further notice, and return the start of the doc |
- doc.cantEdit = true; |
- return Pos(doc.first, 0); |
- } |
- flipped = true; newPos = pos; dir = -dir; |
- } |
- } |
- curPos = newPos; |
- continue search; |
+ function skipAtomicInner(doc, pos, oldPos, dir, mayClear) { |
+ var line = getLine(doc, pos.line); |
+ if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) { |
+ var sp = line.markedSpans[i], m = sp.marker; |
+ if ((sp.from == null || (m.inclusiveLeft ? sp.from <= pos.ch : sp.from < pos.ch)) && |
+ (sp.to == null || (m.inclusiveRight ? sp.to >= pos.ch : sp.to > pos.ch))) { |
+ if (mayClear) { |
+ signal(m, "beforeCursorEnter"); |
+ if (m.explicitlyCleared) { |
+ if (!line.markedSpans) break; |
+ else {--i; continue;} |
} |
} |
+ if (!m.atomic) continue; |
+ |
+ if (oldPos) { |
+ var near = m.find(dir < 0 ? 1 : -1), diff; |
+ if (dir < 0 ? m.inclusiveRight : m.inclusiveLeft) |
+ near = movePos(doc, near, -dir, near && near.line == pos.line ? line : null); |
+ if (near && near.line == pos.line && (diff = cmp(near, oldPos)) && (dir < 0 ? diff < 0 : diff > 0)) |
+ return skipAtomicInner(doc, near, pos, dir, mayClear); |
+ } |
+ |
+ var far = m.find(dir < 0 ? -1 : 1); |
+ if (dir < 0 ? m.inclusiveLeft : m.inclusiveRight) |
+ far = movePos(doc, far, dir, far.line == pos.line ? line : null); |
+ return far ? skipAtomicInner(doc, far, pos, dir, mayClear) : null; |
} |
- return curPos; |
+ } |
+ return pos; |
+ } |
+ |
+ // Ensure a given position is not inside an atomic range. |
+ function skipAtomic(doc, pos, oldPos, bias, mayClear) { |
+ var dir = bias || 1; |
+ var found = skipAtomicInner(doc, pos, oldPos, dir, mayClear) || |
+ (!mayClear && skipAtomicInner(doc, pos, oldPos, dir, true)) || |
+ skipAtomicInner(doc, pos, oldPos, -dir, mayClear) || |
+ (!mayClear && skipAtomicInner(doc, pos, oldPos, -dir, true)); |
+ if (!found) { |
+ doc.cantEdit = true; |
+ return Pos(doc.first, 0); |
+ } |
+ return found; |
+ } |
+ |
+ function movePos(doc, pos, dir, line) { |
+ if (dir < 0 && pos.ch == 0) { |
+ if (pos.line > doc.first) return clipPos(doc, Pos(pos.line - 1)); |
+ else return null; |
+ } else if (dir > 0 && pos.ch == (line || getLine(doc, pos.line)).text.length) { |
+ if (pos.line < doc.first + doc.size - 1) return Pos(pos.line + 1, 0); |
+ else return null; |
+ } else { |
+ return new Pos(pos.line, pos.ch + dir); |
} |
} |
// SELECTION DRAWING |
- // Redraw the selection and/or cursor |
- function drawSelection(cm) { |
- var display = cm.display, doc = cm.doc, result = {}; |
+ function updateSelection(cm) { |
+ cm.display.input.showSelection(cm.display.input.prepareSelection()); |
+ } |
+ |
+ function prepareSelection(cm, primary) { |
+ var doc = cm.doc, result = {}; |
var curFragment = result.cursors = document.createDocumentFragment(); |
var selFragment = result.selection = document.createDocumentFragment(); |
for (var i = 0; i < doc.sel.ranges.length; i++) { |
+ if (primary === false && i == doc.sel.primIndex) continue; |
var range = doc.sel.ranges[i]; |
+ if (range.from().line >= cm.display.viewTo || range.to().line < cm.display.viewFrom) continue; |
var collapsed = range.empty(); |
if (collapsed || cm.options.showCursorWhenSelecting) |
- drawSelectionCursor(cm, range, curFragment); |
+ drawSelectionCursor(cm, range.head, curFragment); |
if (!collapsed) |
drawSelectionRange(cm, range, selFragment); |
} |
- |
- // Move the hidden textarea near the cursor to prevent scrolling artifacts |
- if (cm.options.moveInputWithCursor) { |
- var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); |
- var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); |
- result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, |
- headPos.top + lineOff.top - wrapOff.top)); |
- result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, |
- headPos.left + lineOff.left - wrapOff.left)); |
- } |
- |
return result; |
} |
- function showSelection(cm, drawn) { |
- removeChildrenAndAdd(cm.display.cursorDiv, drawn.cursors); |
- removeChildrenAndAdd(cm.display.selectionDiv, drawn.selection); |
- if (drawn.teTop != null) { |
- cm.display.inputDiv.style.top = drawn.teTop + "px"; |
- cm.display.inputDiv.style.left = drawn.teLeft + "px"; |
- } |
- } |
- |
- function updateSelection(cm) { |
- showSelection(cm, drawSelection(cm)); |
- } |
- |
// Draws a cursor for the given range |
- function drawSelectionCursor(cm, range, output) { |
- var pos = cursorCoords(cm, range.head, "div", null, null, !cm.options.singleCursorHeightPerLine); |
+ function drawSelectionCursor(cm, head, output) { |
+ var pos = cursorCoords(cm, head, "div", null, null, !cm.options.singleCursorHeightPerLine); |
var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")); |
cursor.style.left = pos.left + "px"; |
@@ -1526,8 +2452,8 @@ |
doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function(line) { |
if (doc.frontier >= cm.display.viewFrom) { // Visible |
- var oldStyles = line.styles; |
- var highlighted = highlightLine(cm, line, state, true); |
+ var oldStyles = line.styles, tooLong = line.text.length > cm.options.maxHighlightLength; |
+ var highlighted = highlightLine(cm, line, tooLong ? copyState(doc.mode, state) : state, true); |
line.styles = highlighted.styles; |
var oldCls = line.styleClasses, newCls = highlighted.classes; |
if (newCls) line.styleClasses = newCls; |
@@ -1536,9 +2462,10 @@ |
oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass); |
for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i]; |
if (ischange) changedLines.push(doc.frontier); |
- line.stateAfter = copyState(doc.mode, state); |
+ line.stateAfter = tooLong ? state : copyState(doc.mode, state); |
} else { |
- processLine(cm, line.text, state); |
+ if (line.text.length <= cm.options.maxHighlightLength) |
+ processLine(cm, line.text, state); |
line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null; |
} |
++doc.frontier; |
@@ -1683,10 +2610,12 @@ |
function prepareMeasureForLine(cm, line) { |
var lineN = lineNo(line); |
var view = findViewForLine(cm, lineN); |
- if (view && !view.text) |
+ if (view && !view.text) { |
view = null; |
- else if (view && view.changes) |
+ } else if (view && view.changes) { |
updateLineForChanges(cm, view, lineN, getDimensions(cm)); |
+ cm.curOp.forceUpdate = true; |
+ } |
if (!view) |
view = updateExternalMeasurement(cm, line); |
@@ -1722,9 +2651,7 @@ |
var nullRect = {left: 0, right: 0, top: 0, bottom: 0}; |
- function measureCharInner(cm, prepared, ch, bias) { |
- var map = prepared.map; |
- |
+ function nodeAndOffsetInLineMap(map, ch, bias) { |
var node, start, end, collapse; |
// First, search the line map for the text node corresponding to, |
// or closest to, the target character. |
@@ -1758,23 +2685,32 @@ |
break; |
} |
} |
+ return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd}; |
+ } |
+ |
+ function getUsefulRect(rects, bias) { |
+ var rect = nullRect |
+ if (bias == "left") for (var i = 0; i < rects.length; i++) { |
+ if ((rect = rects[i]).left != rect.right) break |
+ } else for (var i = rects.length - 1; i >= 0; i--) { |
+ if ((rect = rects[i]).left != rect.right) break |
+ } |
+ return rect |
+ } |
+ |
+ function measureCharInner(cm, prepared, ch, bias) { |
+ var place = nodeAndOffsetInLineMap(prepared.map, ch, bias); |
+ var node = place.node, start = place.start, end = place.end, collapse = place.collapse; |
var rect; |
if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. |
for (var i = 0; i < 4; i++) { // Retry a maximum of 4 times when nonsense rectangles are returned |
- while (start && isExtendingChar(prepared.line.text.charAt(mStart + start))) --start; |
- while (mStart + end < mEnd && isExtendingChar(prepared.line.text.charAt(mStart + end))) ++end; |
- if (ie && ie_version < 9 && start == 0 && end == mEnd - mStart) { |
+ while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) --start; |
+ while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) ++end; |
+ if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) |
rect = node.parentNode.getBoundingClientRect(); |
- } else if (ie && cm.options.lineWrapping) { |
- var rects = range(node, start, end).getClientRects(); |
- if (rects.length) |
- rect = rects[bias == "right" ? rects.length - 1 : 0]; |
- else |
- rect = nullRect; |
- } else { |
- rect = range(node, start, end).getBoundingClientRect() || nullRect; |
- } |
+ else |
+ rect = getUsefulRect(range(node, start, end).getClientRects(), bias) |
if (rect.left || rect.right || start == 0) break; |
end = start; |
start = start - 1; |
@@ -2000,10 +2936,23 @@ |
for (;;) { |
if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) { |
var ch = x < fromX || x - fromX <= toX - x ? from : to; |
+ var outside = ch == from ? fromOutside : toOutside |
var xDiff = x - (ch == from ? fromX : toX); |
+ // This is a kludge to handle the case where the coordinates |
+ // are after a line-wrapped line. We should replace it with a |
+ // more general handling of cursor positions around line |
+ // breaks. (Issue #4078) |
+ if (toOutside && !bidi && !/\s/.test(lineObj.text.charAt(ch)) && xDiff > 0 && |
+ ch < lineObj.text.length && preparedMeasure.view.measure.heights.length > 1) { |
+ var charSize = measureCharPrepared(cm, preparedMeasure, ch, "right"); |
+ if (innerOff <= charSize.bottom && innerOff >= charSize.top && Math.abs(x - charSize.right) < xDiff) { |
+ outside = false |
+ ch++ |
+ xDiff = x - charSize.right |
+ } |
+ } |
while (isExtendingChar(lineObj.text.charAt(ch))) ++ch; |
- var pos = PosWithInfo(lineNo, ch, ch == from ? fromOutside : toOutside, |
- xDiff < -1 ? -1 : xDiff > 1 ? 1 : 0); |
+ var pos = PosWithInfo(lineNo, ch, outside, xDiff < -1 ? -1 : xDiff > 1 ? 1 : 0); |
return pos; |
} |
var step = Math.ceil(dist / 2), middle = from + step; |
@@ -2076,6 +3025,7 @@ |
updateMaxLine: false, // Set when the widest line needs to be determined anew |
scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet |
scrollToPos: null, // Used to scroll to a specific position |
+ focus: false, |
id: ++nextOpId // Unique ID |
}; |
if (operationGroup) { |
@@ -2094,12 +3044,12 @@ |
var callbacks = group.delayedCallbacks, i = 0; |
do { |
for (; i < callbacks.length; i++) |
- callbacks[i](); |
+ callbacks[i].call(null); |
for (var j = 0; j < group.ops.length; j++) { |
var op = group.ops[j]; |
if (op.cursorActivityHandlers) |
while (op.cursorActivityCalled < op.cursorActivityHandlers.length) |
- op.cursorActivityHandlers[op.cursorActivityCalled++](op.cm); |
+ op.cursorActivityHandlers[op.cursorActivityCalled++].call(null, op.cm); |
} |
} while (i < callbacks.length); |
} |
@@ -2169,7 +3119,7 @@ |
} |
if (op.updatedDisplay || op.selectionChanged) |
- op.newSelectionNodes = drawSelection(cm); |
+ op.preparedSelection = display.input.prepareSelection(op.focus); |
} |
function endOperation_W2(op) { |
@@ -2182,17 +3132,19 @@ |
cm.display.maxLineChanged = false; |
} |
- if (op.newSelectionNodes) |
- showSelection(cm, op.newSelectionNodes); |
- if (op.updatedDisplay) |
- setDocumentHeight(cm, op.barMeasure); |
+ var takeFocus = op.focus && op.focus == activeElt() && (!document.hasFocus || document.hasFocus()) |
+ if (op.preparedSelection) |
+ cm.display.input.showSelection(op.preparedSelection, takeFocus); |
if (op.updatedDisplay || op.startHeight != cm.doc.height) |
updateScrollbars(cm, op.barMeasure); |
+ if (op.updatedDisplay) |
+ setDocumentHeight(cm, op.barMeasure); |
if (op.selectionChanged) restartBlink(cm); |
if (cm.state.focused && op.updateInput) |
- resetInput(cm, op.typing); |
+ cm.display.input.reset(op.typing); |
+ if (takeFocus) ensureFocus(op.cm); |
} |
function endOperation_finish(op) { |
@@ -2211,7 +3163,7 @@ |
display.scroller.scrollTop = doc.scrollTop; |
} |
if (op.scrollLeft != null && (display.scroller.scrollLeft != op.scrollLeft || op.forceScroll)) { |
- doc.scrollLeft = Math.max(0, Math.min(display.scroller.scrollWidth - displayWidth(cm), op.scrollLeft)); |
+ doc.scrollLeft = Math.max(0, Math.min(display.scroller.scrollWidth - display.scroller.clientWidth, op.scrollLeft)); |
display.scrollbars.setScrollLeft(doc.scrollLeft); |
display.scroller.scrollLeft = doc.scrollLeft; |
alignHorizontally(cm); |
@@ -2237,6 +3189,8 @@ |
// Fire change events, and delayed event handlers |
if (op.changeObjs) |
signal(cm, "changes", cm, op.changeObjs); |
+ if (op.update) |
+ op.update.finish(); |
} |
// Run the given function in an operation |
@@ -2462,167 +3416,6 @@ |
return dirty; |
} |
- // INPUT HANDLING |
- |
- // Poll for input changes, using the normal rate of polling. This |
- // runs as long as the editor is focused. |
- function slowPoll(cm) { |
- if (cm.display.pollingFast) return; |
- cm.display.poll.set(cm.options.pollInterval, function() { |
- readInput(cm); |
- if (cm.state.focused) slowPoll(cm); |
- }); |
- } |
- |
- // When an event has just come in that is likely to add or change |
- // something in the input textarea, we poll faster, to ensure that |
- // the change appears on the screen quickly. |
- function fastPoll(cm) { |
- var missed = false; |
- cm.display.pollingFast = true; |
- function p() { |
- var changed = readInput(cm); |
- if (!changed && !missed) {missed = true; cm.display.poll.set(60, p);} |
- else {cm.display.pollingFast = false; slowPoll(cm);} |
- } |
- cm.display.poll.set(20, p); |
- } |
- |
- // This will be set to an array of strings when copying, so that, |
- // when pasting, we know what kind of selections the copied text |
- // was made out of. |
- var lastCopied = null; |
- |
- // Read input from the textarea, and update the document to match. |
- // When something is selected, it is present in the textarea, and |
- // selected (unless it is huge, in which case a placeholder is |
- // used). When nothing is selected, the cursor sits after previously |
- // seen text (can be empty), which is stored in prevInput (we must |
- // not reset the textarea when typing, because that breaks IME). |
- function readInput(cm) { |
- var input = cm.display.input, prevInput = cm.display.prevInput, doc = cm.doc; |
- // Since this is called a *lot*, try to bail out as cheaply as |
- // possible when it is clear that nothing happened. hasSelection |
- // will be the case when there is a lot of text in the textarea, |
- // in which case reading its value would be expensive. |
- if (!cm.state.focused || (hasSelection(input) && !prevInput) || isReadOnly(cm) || cm.options.disableInput || cm.state.keySeq) |
- return false; |
- // See paste handler for more on the fakedLastChar kludge |
- if (cm.state.pasteIncoming && cm.state.fakedLastChar) { |
- input.value = input.value.substring(0, input.value.length - 1); |
- cm.state.fakedLastChar = false; |
- } |
- var text = input.value; |
- // If nothing changed, bail. |
- if (text == prevInput && !cm.somethingSelected()) return false; |
- // Work around nonsensical selection resetting in IE9/10, and |
- // inexplicable appearance of private area unicode characters on |
- // some key combos in Mac (#2689). |
- if (ie && ie_version >= 9 && cm.display.inputHasSelection === text || |
- mac && /[\uf700-\uf7ff]/.test(text)) { |
- resetInput(cm); |
- return false; |
- } |
- |
- var withOp = !cm.curOp; |
- if (withOp) startOperation(cm); |
- cm.display.shift = false; |
- |
- if (text.charCodeAt(0) == 0x200b && doc.sel == cm.display.selForContextMenu && !prevInput) |
- prevInput = "\u200b"; |
- // Find the part of the input that is actually new |
- var same = 0, l = Math.min(prevInput.length, text.length); |
- while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same; |
- var inserted = text.slice(same), textLines = splitLines(inserted); |
- |
- // When pasing N lines into N selections, insert one line per selection |
- var multiPaste = null; |
- if (cm.state.pasteIncoming && doc.sel.ranges.length > 1) { |
- if (lastCopied && lastCopied.join("\n") == inserted) |
- multiPaste = doc.sel.ranges.length % lastCopied.length == 0 && map(lastCopied, splitLines); |
- else if (textLines.length == doc.sel.ranges.length) |
- multiPaste = map(textLines, function(l) { return [l]; }); |
- } |
- |
- // Normal behavior is to insert the new text into every selection |
- for (var i = doc.sel.ranges.length - 1; i >= 0; i--) { |
- var range = doc.sel.ranges[i]; |
- var from = range.from(), to = range.to(); |
- // Handle deletion |
- if (same < prevInput.length) |
- from = Pos(from.line, from.ch - (prevInput.length - same)); |
- // Handle overwrite |
- else if (cm.state.overwrite && range.empty() && !cm.state.pasteIncoming) |
- to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)); |
- var updateInput = cm.curOp.updateInput; |
- var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i % multiPaste.length] : textLines, |
- origin: cm.state.pasteIncoming ? "paste" : cm.state.cutIncoming ? "cut" : "+input"}; |
- makeChange(cm.doc, changeEvent); |
- signalLater(cm, "inputRead", cm, changeEvent); |
- // When an 'electric' character is inserted, immediately trigger a reindent |
- if (inserted && !cm.state.pasteIncoming && cm.options.electricChars && |
- cm.options.smartIndent && range.head.ch < 100 && |
- (!i || doc.sel.ranges[i - 1].head.line != range.head.line)) { |
- var mode = cm.getModeAt(range.head); |
- var end = changeEnd(changeEvent); |
- if (mode.electricChars) { |
- for (var j = 0; j < mode.electricChars.length; j++) |
- if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) { |
- indentLine(cm, end.line, "smart"); |
- break; |
- } |
- } else if (mode.electricInput) { |
- if (mode.electricInput.test(getLine(doc, end.line).text.slice(0, end.ch))) |
- indentLine(cm, end.line, "smart"); |
- } |
- } |
- } |
- ensureCursorVisible(cm); |
- cm.curOp.updateInput = updateInput; |
- cm.curOp.typing = true; |
- |
- // Don't leave long text in the textarea, since it makes further polling slow |
- if (text.length > 1000 || text.indexOf("\n") > -1) input.value = cm.display.prevInput = ""; |
- else cm.display.prevInput = text; |
- if (withOp) endOperation(cm); |
- cm.state.pasteIncoming = cm.state.cutIncoming = false; |
- return true; |
- } |
- |
- // Reset the input to correspond to the selection (or to be empty, |
- // when not typing and nothing is selected) |
- function resetInput(cm, typing) { |
- if (cm.display.contextMenuPending) return; |
- var minimal, selected, doc = cm.doc; |
- if (cm.somethingSelected()) { |
- cm.display.prevInput = ""; |
- var range = doc.sel.primary(); |
- minimal = hasCopyEvent && |
- (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000); |
- var content = minimal ? "-" : selected || cm.getSelection(); |
- cm.display.input.value = content; |
- if (cm.state.focused) selectInput(cm.display.input); |
- if (ie && ie_version >= 9) cm.display.inputHasSelection = content; |
- } else if (!typing) { |
- cm.display.prevInput = cm.display.input.value = ""; |
- if (ie && ie_version >= 9) cm.display.inputHasSelection = null; |
- } |
- cm.display.inaccurateSelection = minimal; |
- } |
- |
- function focusInput(cm) { |
- if (cm.options.readOnly != "nocursor" && (!mobile || activeElt() != cm.display.input)) |
- cm.display.input.focus(); |
- } |
- |
- function ensureFocus(cm) { |
- if (!cm.state.focused) { focusInput(cm); onFocus(cm); } |
- } |
- |
- function isReadOnly(cm) { |
- return cm.options.readOnly || cm.doc.cantEdit; |
- } |
- |
// EVENT HANDLERS |
// Attach the necessary event handlers when initializing the editor |
@@ -2641,15 +3434,64 @@ |
})); |
else |
on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); }); |
- // Prevent normal selection in the editor (we handle our own) |
- on(d.lineSpace, "selectstart", function(e) { |
- if (!eventInWidget(d, e)) e_preventDefault(e); |
- }); |
// Some browsers fire contextmenu *after* opening the menu, at |
// which point we can't mess with it anymore. Context menu is |
// handled in onMouseDown for these browsers. |
if (!captureRightClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);}); |
+ // Used to suppress mouse event handling when a touch happens |
+ var touchFinished, prevTouch = {end: 0}; |
+ function finishTouch() { |
+ if (d.activeTouch) { |
+ touchFinished = setTimeout(function() {d.activeTouch = null;}, 1000); |
+ prevTouch = d.activeTouch; |
+ prevTouch.end = +new Date; |
+ } |
+ }; |
+ function isMouseLikeTouchEvent(e) { |
+ if (e.touches.length != 1) return false; |
+ var touch = e.touches[0]; |
+ return touch.radiusX <= 1 && touch.radiusY <= 1; |
+ } |
+ function farAway(touch, other) { |
+ if (other.left == null) return true; |
+ var dx = other.left - touch.left, dy = other.top - touch.top; |
+ return dx * dx + dy * dy > 20 * 20; |
+ } |
+ on(d.scroller, "touchstart", function(e) { |
+ if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e)) { |
+ clearTimeout(touchFinished); |
+ var now = +new Date; |
+ d.activeTouch = {start: now, moved: false, |
+ prev: now - prevTouch.end <= 300 ? prevTouch : null}; |
+ if (e.touches.length == 1) { |
+ d.activeTouch.left = e.touches[0].pageX; |
+ d.activeTouch.top = e.touches[0].pageY; |
+ } |
+ } |
+ }); |
+ on(d.scroller, "touchmove", function() { |
+ if (d.activeTouch) d.activeTouch.moved = true; |
+ }); |
+ on(d.scroller, "touchend", function(e) { |
+ var touch = d.activeTouch; |
+ if (touch && !eventInWidget(d, e) && touch.left != null && |
+ !touch.moved && new Date - touch.start < 300) { |
+ var pos = cm.coordsChar(d.activeTouch, "page"), range; |
+ if (!touch.prev || farAway(touch, touch.prev)) // Single tap |
+ range = new Range(pos, pos); |
+ else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap |
+ range = cm.findWordAt(pos); |
+ else // Triple tap |
+ range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))); |
+ cm.setSelection(range.anchor, range.head); |
+ cm.focus(); |
+ e_preventDefault(e); |
+ } |
+ finishTouch(); |
+ }); |
+ on(d.scroller, "touchcancel", finishTouch); |
+ |
// Sync scrolling between fake scrollbars and real scrollable |
// area, ensure viewport is updated when scrolling. |
on(d.scroller, "scroll", function() { |
@@ -2667,86 +3509,33 @@ |
// Prevent wrapper from ever scrolling |
on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); |
- on(d.input, "keyup", function(e) { onKeyUp.call(cm, e); }); |
- on(d.input, "input", function() { |
- if (ie && ie_version >= 9 && cm.display.inputHasSelection) cm.display.inputHasSelection = null; |
- readInput(cm); |
- }); |
- on(d.input, "keydown", operation(cm, onKeyDown)); |
- on(d.input, "keypress", operation(cm, onKeyPress)); |
- on(d.input, "focus", bind(onFocus, cm)); |
- on(d.input, "blur", bind(onBlur, cm)); |
- |
- function drag_(e) { |
- if (!signalDOMEvent(cm, e)) e_stop(e); |
- } |
- if (cm.options.dragDrop) { |
- on(d.scroller, "dragstart", function(e){onDragStart(cm, e);}); |
- on(d.scroller, "dragenter", drag_); |
- on(d.scroller, "dragover", drag_); |
- on(d.scroller, "drop", operation(cm, onDrop)); |
- } |
- on(d.scroller, "paste", function(e) { |
- if (eventInWidget(d, e)) return; |
- cm.state.pasteIncoming = true; |
- focusInput(cm); |
- fastPoll(cm); |
- }); |
- on(d.input, "paste", function() { |
- // Workaround for webkit bug https://bugs.webkit.org/show_bug.cgi?id=90206 |
- // Add a char to the end of textarea before paste occur so that |
- // selection doesn't span to the end of textarea. |
- if (webkit && !cm.state.fakedLastChar && !(new Date - cm.state.lastMiddleDown < 200)) { |
- var start = d.input.selectionStart, end = d.input.selectionEnd; |
- d.input.value += "$"; |
- // The selection end needs to be set before the start, otherwise there |
- // can be an intermediate non-empty selection between the two, which |
- // can override the middle-click paste buffer on linux and cause the |
- // wrong thing to get pasted. |
- d.input.selectionEnd = end; |
- d.input.selectionStart = start; |
- cm.state.fakedLastChar = true; |
- } |
- cm.state.pasteIncoming = true; |
- fastPoll(cm); |
- }); |
+ d.dragFunctions = { |
+ enter: function(e) {if (!signalDOMEvent(cm, e)) e_stop(e);}, |
+ over: function(e) {if (!signalDOMEvent(cm, e)) { onDragOver(cm, e); e_stop(e); }}, |
+ start: function(e){onDragStart(cm, e);}, |
+ drop: operation(cm, onDrop), |
+ leave: function(e) {if (!signalDOMEvent(cm, e)) { clearDragCursor(cm); }} |
+ }; |
- function prepareCopyCut(e) { |
- if (cm.somethingSelected()) { |
- lastCopied = cm.getSelections(); |
- if (d.inaccurateSelection) { |
- d.prevInput = ""; |
- d.inaccurateSelection = false; |
- d.input.value = lastCopied.join("\n"); |
- selectInput(d.input); |
- } |
- } else { |
- var text = [], ranges = []; |
- for (var i = 0; i < cm.doc.sel.ranges.length; i++) { |
- var line = cm.doc.sel.ranges[i].head.line; |
- var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)}; |
- ranges.push(lineRange); |
- text.push(cm.getRange(lineRange.anchor, lineRange.head)); |
- } |
- if (e.type == "cut") { |
- cm.setSelections(ranges, null, sel_dontScroll); |
- } else { |
- d.prevInput = ""; |
- d.input.value = text.join("\n"); |
- selectInput(d.input); |
- } |
- lastCopied = text; |
- } |
- if (e.type == "cut") cm.state.cutIncoming = true; |
- } |
- on(d.input, "cut", prepareCopyCut); |
- on(d.input, "copy", prepareCopyCut); |
+ var inp = d.input.getField(); |
+ on(inp, "keyup", function(e) { onKeyUp.call(cm, e); }); |
+ on(inp, "keydown", operation(cm, onKeyDown)); |
+ on(inp, "keypress", operation(cm, onKeyPress)); |
+ on(inp, "focus", bind(onFocus, cm)); |
+ on(inp, "blur", bind(onBlur, cm)); |
+ } |
- // Needed to handle Tab key in KHTML |
- if (khtml) on(d.sizer, "mouseup", function() { |
- if (activeElt() == d.input) d.input.blur(); |
- focusInput(cm); |
- }); |
+ function dragDropChanged(cm, value, old) { |
+ var wasOn = old && old != CodeMirror.Init; |
+ if (!value != !wasOn) { |
+ var funcs = cm.display.dragFunctions; |
+ var toggle = value ? on : off; |
+ toggle(cm.display.scroller, "dragstart", funcs.start); |
+ toggle(cm.display.scroller, "dragenter", funcs.enter); |
+ toggle(cm.display.scroller, "dragover", funcs.over); |
+ toggle(cm.display.scroller, "dragleave", funcs.leave); |
+ toggle(cm.display.scroller, "drop", funcs.drop); |
+ } |
} |
// Called when the window resizes |
@@ -2778,7 +3567,7 @@ |
// coordinates beyond the right of the text. |
function posFromMouse(cm, e, liberal, forRect) { |
var display = cm.display; |
- if (!liberal && e_target(e).getAttribute("not-content") == "true") return null; |
+ if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") return null; |
var x, y, space = display.lineSpace.getBoundingClientRect(); |
// Fails unpredictably on IE[67] when mouse is dragged around quickly. |
@@ -2798,8 +3587,8 @@ |
// middle-click-paste. Or it might be a click on something we should |
// not interfere with, such as a scrollbar or widget. |
function onMouseDown(e) { |
- if (signalDOMEvent(this, e)) return; |
var cm = this, display = cm.display; |
+ if (signalDOMEvent(cm, e) || display.activeTouch && display.input.supportsTouch()) return; |
display.shift = e.shiftKey; |
if (eventInWidget(display, e)) { |
@@ -2817,7 +3606,10 @@ |
switch (e_button(e)) { |
case 1: |
- if (start) |
+ // #3261: make sure, that we're not starting a second selection |
+ if (cm.state.selectingText) |
+ cm.state.selectingText(e); |
+ else if (start) |
leftButtonDown(cm, e, start); |
else if (e_target(e) == display.scroller) |
e_preventDefault(e); |
@@ -2825,18 +3617,20 @@ |
case 2: |
if (webkit) cm.state.lastMiddleDown = +new Date; |
if (start) extendSelection(cm.doc, start); |
- setTimeout(bind(focusInput, cm), 20); |
+ setTimeout(function() {display.input.focus();}, 20); |
e_preventDefault(e); |
break; |
case 3: |
if (captureRightClick) onContextMenu(cm, e); |
+ else delayBlurEvent(cm); |
break; |
} |
} |
var lastClick, lastDoubleClick; |
function leftButtonDown(cm, e, start) { |
- setTimeout(bind(ensureFocus, cm), 0); |
+ if (ie) setTimeout(bind(ensureFocus, cm), 0); |
+ else cm.curOp.focus = activeElt(); |
var now = +new Date, type; |
if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) { |
@@ -2850,9 +3644,10 @@ |
} |
var sel = cm.doc.sel, modifier = mac ? e.metaKey : e.ctrlKey, contained; |
- if (cm.options.dragDrop && dragAndDrop && !isReadOnly(cm) && |
+ if (cm.options.dragDrop && dragAndDrop && !cm.isReadOnly() && |
type == "single" && (contained = sel.contains(start)) > -1 && |
- !sel.ranges[contained].empty()) |
+ (cmp((contained = sel.ranges[contained]).from(), start) < 0 || start.xRel > 0) && |
+ (cmp(contained.to(), start) > 0 || start.xRel < 0)) |
leftButtonStartDrag(cm, e, start, modifier); |
else |
leftButtonSelect(cm, e, start, type, modifier); |
@@ -2861,7 +3656,7 @@ |
// Start a text drag. When it ends, see if any dragging actually |
// happen, and treat as a click if it didn't. |
function leftButtonStartDrag(cm, e, start, modifier) { |
- var display = cm.display; |
+ var display = cm.display, startTime = +new Date; |
var dragEnd = operation(cm, function(e2) { |
if (webkit) display.scroller.draggable = false; |
cm.state.draggingText = false; |
@@ -2869,17 +3664,19 @@ |
off(display.scroller, "drop", dragEnd); |
if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { |
e_preventDefault(e2); |
- if (!modifier) |
+ if (!modifier && +new Date - 200 < startTime) |
extendSelection(cm.doc, start); |
- focusInput(cm); |
- // Work around unexplainable focus problem in IE9 (#2127) |
- if (ie && ie_version == 9) |
- setTimeout(function() {document.body.focus(); focusInput(cm);}, 20); |
+ // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081) |
+ if (webkit || ie && ie_version == 9) |
+ setTimeout(function() {document.body.focus(); display.input.focus();}, 20); |
+ else |
+ display.input.focus(); |
} |
}); |
// Let the drag handler handle this. |
if (webkit) display.scroller.draggable = true; |
cm.state.draggingText = dragEnd; |
+ dragEnd.copy = mac ? e.altKey : e.ctrlKey |
// IE's approach to draggable |
if (display.scroller.dragDrop) display.scroller.dragDrop(); |
on(document, "mouseup", dragEnd); |
@@ -2900,9 +3697,10 @@ |
ourRange = new Range(start, start); |
} else { |
ourRange = doc.sel.primary(); |
+ ourIndex = doc.sel.primIndex; |
} |
- if (e.altKey) { |
+ if (chromeOS ? e.shiftKey && e.metaKey : e.altKey) { |
type = "rect"; |
if (!addNew) ourRange = new Range(start, start); |
start = posFromMouse(cm, e, true, true); |
@@ -2931,8 +3729,9 @@ |
ourIndex = ranges.length; |
setSelection(doc, normalizeSelection(ranges.concat([ourRange]), ourIndex), |
{scroll: false, origin: "*mouse"}); |
- } else if (ranges.length > 1 && ranges[ourIndex].empty() && type == "single") { |
- setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0)); |
+ } else if (ranges.length > 1 && ranges[ourIndex].empty() && type == "single" && !e.shiftKey) { |
+ setSelection(doc, normalizeSelection(ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0), |
+ {scroll: false, origin: "*mouse"}); |
startSel = doc.sel; |
} else { |
replaceOneSelection(doc, ourIndex, ourRange, sel_mouse); |
@@ -2994,7 +3793,7 @@ |
var cur = posFromMouse(cm, e, true, type == "rect"); |
if (!cur) return; |
if (cmp(cur, lastPos) != 0) { |
- ensureFocus(cm); |
+ cm.curOp.focus = activeElt(); |
extendTo(cur); |
var visible = visibleLines(display, doc); |
if (cur.line >= visible.to || cur.line < visible.from) |
@@ -3010,9 +3809,10 @@ |
} |
function done(e) { |
+ cm.state.selectingText = false; |
counter = Infinity; |
e_preventDefault(e); |
- focusInput(cm); |
+ display.input.focus(); |
off(document, "mousemove", move); |
off(document, "mouseup", up); |
doc.history.lastSelOrigin = null; |
@@ -3023,13 +3823,14 @@ |
else extend(e); |
}); |
var up = operation(cm, done); |
+ cm.state.selectingText = up; |
on(document, "mousemove", move); |
on(document, "mouseup", up); |
} |
// Determines whether an event happened in the gutter, and fires the |
// handlers for the corresponding event. |
- function gutterEvent(cm, e, type, prevent, signalfn) { |
+ function gutterEvent(cm, e, type, prevent) { |
try { var mX = e.clientX, mY = e.clientY; } |
catch(e) { return false; } |
if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) return false; |
@@ -3046,14 +3847,14 @@ |
if (g && g.getBoundingClientRect().right >= mX) { |
var line = lineAtHeight(cm.doc, mY); |
var gutter = cm.options.gutters[i]; |
- signalfn(cm, type, cm, line, gutter, e); |
+ signal(cm, type, cm, line, gutter, e); |
return e_defaultPrevented(e); |
} |
} |
} |
function clickInGutter(cm, e) { |
- return gutterEvent(cm, e, "gutterClick", true, signalLater); |
+ return gutterEvent(cm, e, "gutterClick", true); |
} |
// Kludge to work around strange IE behavior where it'll sometimes |
@@ -3062,23 +3863,32 @@ |
function onDrop(e) { |
var cm = this; |
+ clearDragCursor(cm); |
if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) |
return; |
e_preventDefault(e); |
if (ie) lastDrop = +new Date; |
var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; |
- if (!pos || isReadOnly(cm)) return; |
+ if (!pos || cm.isReadOnly()) return; |
// Might be a file drop, in which case we simply extract the text |
// and insert it. |
if (files && files.length && window.FileReader && window.File) { |
var n = files.length, text = Array(n), read = 0; |
var loadFile = function(file, i) { |
+ if (cm.options.allowDropFileTypes && |
+ indexOf(cm.options.allowDropFileTypes, file.type) == -1) |
+ return; |
+ |
var reader = new FileReader; |
reader.onload = operation(cm, function() { |
- text[i] = reader.result; |
+ var content = reader.result; |
+ if (/[\x00-\x08\x0e-\x1f]{2}/.test(content)) content = ""; |
+ text[i] = content; |
if (++read == n) { |
pos = clipPos(cm.doc, pos); |
- var change = {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"}; |
+ var change = {from: pos, to: pos, |
+ text: cm.doc.splitLines(text.join(cm.doc.lineSeparator())), |
+ origin: "paste"}; |
makeChange(cm.doc, change); |
setSelectionReplaceHistory(cm.doc, simpleSelection(pos, changeEnd(change))); |
} |
@@ -3091,19 +3901,19 @@ |
if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) { |
cm.state.draggingText(e); |
// Ensure the editor is re-focused |
- setTimeout(bind(focusInput, cm), 20); |
+ setTimeout(function() {cm.display.input.focus();}, 20); |
return; |
} |
try { |
var text = e.dataTransfer.getData("Text"); |
if (text) { |
- if (cm.state.draggingText && !(mac ? e.metaKey : e.ctrlKey)) |
+ if (cm.state.draggingText && !cm.state.draggingText.copy) |
var selected = cm.listSelections(); |
setSelectionNoUndo(cm.doc, simpleSelection(pos, pos)); |
if (selected) for (var i = 0; i < selected.length; ++i) |
replaceRange(cm.doc, "", selected[i].anchor, selected[i].head, "drag"); |
cm.replaceSelection(text, "around", "paste"); |
- focusInput(cm); |
+ cm.display.input.focus(); |
} |
} |
catch(e){} |
@@ -3115,6 +3925,7 @@ |
if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return; |
e.dataTransfer.setData("Text", cm.getSelection()); |
+ e.dataTransfer.effectAllowed = "copyMove" |
// Use dummy image instead of default browsers image. |
// Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. |
@@ -3132,6 +3943,25 @@ |
} |
} |
+ function onDragOver(cm, e) { |
+ var pos = posFromMouse(cm, e); |
+ if (!pos) return; |
+ var frag = document.createDocumentFragment(); |
+ drawSelectionCursor(cm, pos, frag); |
+ if (!cm.display.dragCursor) { |
+ cm.display.dragCursor = elt("div", null, "CodeMirror-cursors CodeMirror-dragcursors"); |
+ cm.display.lineSpace.insertBefore(cm.display.dragCursor, cm.display.cursorDiv); |
+ } |
+ removeChildrenAndAdd(cm.display.dragCursor, frag); |
+ } |
+ |
+ function clearDragCursor(cm) { |
+ if (cm.display.dragCursor) { |
+ cm.display.lineSpace.removeChild(cm.display.dragCursor); |
+ cm.display.dragCursor = null; |
+ } |
+ } |
+ |
// SCROLL EVENTS |
// Sync the scrollable area and scrollbars, ensure the viewport |
@@ -3196,8 +4026,9 @@ |
var display = cm.display, scroll = display.scroller; |
// Quit if there's nothing to scroll here |
- if (!(dx && scroll.scrollWidth > scroll.clientWidth || |
- dy && scroll.scrollHeight > scroll.clientHeight)) return; |
+ var canScrollX = scroll.scrollWidth > scroll.clientWidth; |
+ var canScrollY = scroll.scrollHeight > scroll.clientHeight; |
+ if (!(dx && canScrollX || dy && canScrollY)) return; |
// Webkit browsers on OS X abort momentum scrolls when the target |
// of the scroll event is removed from the scrollable element. |
@@ -3221,10 +4052,15 @@ |
// scrolling entirely here. It'll be slightly off from native, but |
// better than glitching out. |
if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { |
- if (dy) |
+ if (dy && canScrollY) |
setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight))); |
setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth))); |
- e_preventDefault(e); |
+ // Only prevent default scrolling if vertical scrolling is |
+ // actually possible. Otherwise, it causes vertical scroll |
+ // jitter on OSX trackpads when deltaX is small and deltaY |
+ // is large (issue #3579) |
+ if (!dy || (dy && canScrollY)) |
+ e_preventDefault(e); |
display.wheelStartX = null; // Abort measurement, if in progress |
return; |
} |
@@ -3270,10 +4106,10 @@ |
} |
// Ensure previous input has been read, so that the handler sees a |
// consistent view of the document |
- if (cm.display.pollingFast && readInput(cm)) cm.display.pollingFast = false; |
+ cm.display.input.ensurePolled(); |
var prevShift = cm.display.shift, done = false; |
try { |
- if (isReadOnly(cm)) cm.state.suppressEdits = true; |
+ if (cm.isReadOnly()) cm.state.suppressEdits = true; |
if (dropShift) cm.display.shift = false; |
done = bound(cm) != Pass; |
} finally { |
@@ -3300,7 +4136,7 @@ |
stopSeq.set(50, function() { |
if (cm.state.keySeq == seq) { |
cm.state.keySeq = null; |
- resetInput(cm); |
+ cm.display.input.reset(); |
} |
}); |
name = seq + " " + name; |
@@ -3352,7 +4188,7 @@ |
var lastStoppedKey = null; |
function onKeyDown(e) { |
var cm = this; |
- ensureFocus(cm); |
+ cm.curOp.focus = activeElt(); |
if (signalDOMEvent(cm, e)) return; |
// IE does strange things with escape. |
if (ie && ie_version < 11 && e.keyCode == 27) e.returnValue = false; |
@@ -3393,36 +4229,49 @@ |
function onKeyPress(e) { |
var cm = this; |
- if (signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) return; |
+ if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) return; |
var keyCode = e.keyCode, charCode = e.charCode; |
if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} |
- if (((presto && (!e.which || e.which < 10)) || khtml) && handleKeyBinding(cm, e)) return; |
+ if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) return; |
var ch = String.fromCharCode(charCode == null ? keyCode : charCode); |
if (handleCharBinding(cm, e, ch)) return; |
- if (ie && ie_version >= 9) cm.display.inputHasSelection = null; |
- fastPoll(cm); |
+ cm.display.input.onKeyPress(e); |
} |
// FOCUS/BLUR EVENTS |
+ function delayBlurEvent(cm) { |
+ cm.state.delayingBlurEvent = true; |
+ setTimeout(function() { |
+ if (cm.state.delayingBlurEvent) { |
+ cm.state.delayingBlurEvent = false; |
+ onBlur(cm); |
+ } |
+ }, 100); |
+ } |
+ |
function onFocus(cm) { |
+ if (cm.state.delayingBlurEvent) cm.state.delayingBlurEvent = false; |
+ |
if (cm.options.readOnly == "nocursor") return; |
if (!cm.state.focused) { |
signal(cm, "focus", cm); |
cm.state.focused = true; |
addClass(cm.display.wrapper, "CodeMirror-focused"); |
- // The prevInput test prevents this from firing when a context |
- // menu is closed (since the resetInput would kill the |
+ // This test prevents this from firing when a context |
+ // menu is closed (since the input reset would kill the |
// select-all detection hack) |
if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) { |
- resetInput(cm); |
- if (webkit) setTimeout(bind(resetInput, cm, true), 0); // Issue #1730 |
+ cm.display.input.reset(); |
+ if (webkit) setTimeout(function() { cm.display.input.reset(true); }, 20); // Issue #1730 |
} |
+ cm.display.input.receivedFocus(); |
} |
- slowPoll(cm); |
restartBlink(cm); |
} |
function onBlur(cm) { |
+ if (cm.state.delayingBlurEvent) return; |
+ |
if (cm.state.focused) { |
signal(cm, "blur", cm); |
cm.state.focused = false; |
@@ -3438,85 +4287,14 @@ |
// textarea (making it as unobtrusive as possible) to let the |
// right-click take effect on it. |
function onContextMenu(cm, e) { |
+ if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) return; |
if (signalDOMEvent(cm, e, "contextmenu")) return; |
- var display = cm.display; |
- if (eventInWidget(display, e) || contextMenuInGutter(cm, e)) return; |
- |
- var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; |
- if (!pos || presto) return; // Opera is difficult. |
- |
- // Reset the current text selection only if the click is done outside of the selection |
- // and 'resetSelectionOnContextMenu' option is true. |
- var reset = cm.options.resetSelectionOnContextMenu; |
- if (reset && cm.doc.sel.contains(pos) == -1) |
- operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll); |
- |
- var oldCSS = display.input.style.cssText; |
- display.inputDiv.style.position = "absolute"; |
- display.input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + |
- "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: " + |
- (ie ? "rgba(255, 255, 255, .05)" : "transparent") + |
- "; outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; |
- if (webkit) var oldScrollY = window.scrollY; // Work around Chrome issue (#2712) |
- focusInput(cm); |
- if (webkit) window.scrollTo(null, oldScrollY); |
- resetInput(cm); |
- // Adds "Select all" to context menu in FF |
- if (!cm.somethingSelected()) display.input.value = display.prevInput = " "; |
- display.contextMenuPending = true; |
- display.selForContextMenu = cm.doc.sel; |
- clearTimeout(display.detectingSelectAll); |
- |
- // Select-all will be greyed out if there's nothing to select, so |
- // this adds a zero-width space so that we can later check whether |
- // it got selected. |
- function prepareSelectAllHack() { |
- if (display.input.selectionStart != null) { |
- var selected = cm.somethingSelected(); |
- var extval = display.input.value = "\u200b" + (selected ? display.input.value : ""); |
- display.prevInput = selected ? "" : "\u200b"; |
- display.input.selectionStart = 1; display.input.selectionEnd = extval.length; |
- // Re-set this, in case some other handler touched the |
- // selection in the meantime. |
- display.selForContextMenu = cm.doc.sel; |
- } |
- } |
- function rehide() { |
- display.contextMenuPending = false; |
- display.inputDiv.style.position = "relative"; |
- display.input.style.cssText = oldCSS; |
- if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos); |
- slowPoll(cm); |
- |
- // Try to detect the user choosing select-all |
- if (display.input.selectionStart != null) { |
- if (!ie || (ie && ie_version < 9)) prepareSelectAllHack(); |
- var i = 0, poll = function() { |
- if (display.selForContextMenu == cm.doc.sel && display.input.selectionStart == 0) |
- operation(cm, commands.selectAll)(cm); |
- else if (i++ < 10) display.detectingSelectAll = setTimeout(poll, 500); |
- else resetInput(cm); |
- }; |
- display.detectingSelectAll = setTimeout(poll, 200); |
- } |
- } |
- |
- if (ie && ie_version >= 9) prepareSelectAllHack(); |
- if (captureRightClick) { |
- e_stop(e); |
- var mouseup = function() { |
- off(window, "mouseup", mouseup); |
- setTimeout(rehide, 20); |
- }; |
- on(window, "mouseup", mouseup); |
- } else { |
- setTimeout(rehide, 50); |
- } |
+ cm.display.input.onContextMenu(e); |
} |
function contextMenuInGutter(cm, e) { |
if (!hasHandler(cm, "gutterContextMenu")) return false; |
- return gutterEvent(cm, e, "gutterContextMenu", false, signal); |
+ return gutterEvent(cm, e, "gutterContextMenu", false); |
} |
// UPDATING |
@@ -3644,7 +4422,7 @@ |
// Revert a change stored in a document's history. |
function makeChangeFromHistory(doc, type, allowSelectionOnly) { |
- if (doc.cm && doc.cm.state.suppressEdits) return; |
+ if (doc.cm && doc.cm.state.suppressEdits && !allowSelectionOnly) return; |
var hist = doc.history, event, selAfter = doc.sel; |
var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done; |
@@ -3820,7 +4598,7 @@ |
function replaceRange(doc, code, from, to, origin) { |
if (!to) to = from; |
if (cmp(to, from) < 0) { var tmp = to; to = from; from = tmp; } |
- if (typeof code == "string") code = splitLines(code); |
+ if (typeof code == "string") code = doc.splitLines(code); |
makeChange(doc, {from: from, to: to, text: code, origin: origin}); |
} |
@@ -3999,6 +4777,8 @@ |
if (indentString != curSpaceString) { |
replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); |
+ line.stateAfter = null; |
+ return true; |
} else { |
// Ensure that, if the cursor was in the whitespace at the start |
// of the line, it is moved to the end of that space. |
@@ -4011,7 +4791,6 @@ |
} |
} |
} |
- line.stateAfter = null; |
} |
// Utility for applying a change to a line by handle or number, |
@@ -4063,10 +4842,9 @@ |
function findPosH(doc, pos, dir, unit, visually) { |
var line = pos.line, ch = pos.ch, origDir = dir; |
var lineObj = getLine(doc, line); |
- var possible = true; |
function findNextLine() { |
var l = line + dir; |
- if (l < doc.first || l >= doc.first + doc.size) return (possible = false); |
+ if (l < doc.first || l >= doc.first + doc.size) return false |
line = l; |
return lineObj = getLine(doc, l); |
} |
@@ -4076,14 +4854,16 @@ |
if (!boundToLine && findNextLine()) { |
if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj); |
else ch = dir < 0 ? lineObj.text.length : 0; |
- } else return (possible = false); |
+ } else return false |
} else ch = next; |
return true; |
} |
- if (unit == "char") moveOnce(); |
- else if (unit == "column") moveOnce(true); |
- else if (unit == "word" || unit == "group") { |
+ if (unit == "char") { |
+ moveOnce() |
+ } else if (unit == "column") { |
+ moveOnce(true) |
+ } else if (unit == "word" || unit == "group") { |
var sawType = null, group = unit == "group"; |
var helper = doc.cm && doc.cm.getHelper(pos, "wordChars"); |
for (var first = true;; first = false) { |
@@ -4103,8 +4883,8 @@ |
if (dir > 0 && !moveOnce(!first)) break; |
} |
} |
- var result = skipAtomic(doc, Pos(line, ch), origDir, true); |
- if (!possible) result.hitSide = true; |
+ var result = skipAtomic(doc, Pos(line, ch), pos, origDir, true); |
+ if (!cmp(pos, result)) result.hitSide = true; |
return result; |
} |
@@ -4140,7 +4920,7 @@ |
CodeMirror.prototype = { |
constructor: CodeMirror, |
- focus: function(){window.focus(); focusInput(this); fastPoll(this);}, |
+ focus: function(){window.focus(); this.display.input.focus();}, |
setOption: function(option, value) { |
var options = this.options, old = options[option]; |
@@ -4251,7 +5031,7 @@ |
getHelpers: function(pos, type) { |
var found = []; |
- if (!helpers.hasOwnProperty(type)) return helpers; |
+ if (!helpers.hasOwnProperty(type)) return found; |
var help = helpers[type], mode = this.getModeAt(pos); |
if (typeof mode[type] == "string") { |
if (help[mode[type]]) found.push(help[mode[type]]); |
@@ -4301,10 +5081,15 @@ |
return lineAtHeight(this.doc, height + this.display.viewOffset); |
}, |
heightAtLine: function(line, mode) { |
- var end = false, last = this.doc.first + this.doc.size - 1; |
- if (line < this.doc.first) line = this.doc.first; |
- else if (line > last) { line = last; end = true; } |
- var lineObj = getLine(this.doc, line); |
+ var end = false, lineObj; |
+ if (typeof line == "number") { |
+ var last = this.doc.first + this.doc.size - 1; |
+ if (line < this.doc.first) line = this.doc.first; |
+ else if (line > last) { line = last; end = true; } |
+ lineObj = getLine(this.doc, line); |
+ } else { |
+ lineObj = line; |
+ } |
return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page").top + |
(end ? this.doc.height - heightAtLine(lineObj) : 0); |
}, |
@@ -4333,12 +5118,6 @@ |
}); |
}), |
- addLineWidget: methodOp(function(handle, node, options) { |
- return addLineWidget(this, handle, node, options); |
- }), |
- |
- removeLineWidget: function(widget) { widget.clear(); }, |
- |
lineInfo: function(line) { |
if (typeof line == "number") { |
if (!isLine(this.doc, line)) return null; |
@@ -4362,6 +5141,7 @@ |
var top = pos.bottom, left = pos.left; |
node.style.position = "absolute"; |
node.setAttribute("cm-ignore-events", "true"); |
+ this.display.input.setUneditable(node); |
display.sizer.appendChild(node); |
if (vert == "over") { |
top = pos.top; |
@@ -4396,9 +5176,11 @@ |
execCommand: function(cmd) { |
if (commands.hasOwnProperty(cmd)) |
- return commands[cmd](this); |
+ return commands[cmd].call(null, this); |
}, |
+ triggerElectric: methodOp(function(text) { triggerElectric(this, text); }), |
+ |
findPosH: function(from, amount, unit, visually) { |
var dir = 1; |
if (amount < 0) { dir = -1; amount = -amount; } |
@@ -4488,7 +5270,8 @@ |
signal(this, "overwriteToggle", this, this.state.overwrite); |
}, |
- hasFocus: function() { return activeElt() == this.display.input; }, |
+ hasFocus: function() { return this.display.input.getField() == activeElt(); }, |
+ isReadOnly: function() { return !!(this.options.readOnly || this.doc.cantEdit); }, |
scrollTo: methodOp(function(x, y) { |
if (x != null || y != null) resolveScrollToPos(this); |
@@ -4564,14 +5347,14 @@ |
old.cm = null; |
attachDoc(this, doc); |
clearCaches(this); |
- resetInput(this); |
+ this.display.input.reset(); |
this.scrollTo(doc.scrollLeft, doc.scrollTop); |
this.curOp.forceScroll = true; |
signalLater(this, "swapDoc", this, old); |
return old; |
}), |
- getInputField: function(){return this.display.input;}, |
+ getInputField: function(){return this.display.input.getField();}, |
getWrapperElement: function(){return this.display.wrapper;}, |
getScrollerElement: function(){return this.display.scroller;}, |
getGutterElement: function(){return this.display.gutters;} |
@@ -4612,12 +5395,31 @@ |
clearCaches(cm); |
regChange(cm); |
}, true); |
- option("specialChars", /[\t\u0000-\u0019\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g, function(cm, val) { |
- cm.options.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); |
- cm.refresh(); |
- }, true); |
+ option("lineSeparator", null, function(cm, val) { |
+ cm.doc.lineSep = val; |
+ if (!val) return; |
+ var newBreaks = [], lineNo = cm.doc.first; |
+ cm.doc.iter(function(line) { |
+ for (var pos = 0;;) { |
+ var found = line.text.indexOf(val, pos); |
+ if (found == -1) break; |
+ pos = found + val.length; |
+ newBreaks.push(Pos(lineNo, found)); |
+ } |
+ lineNo++; |
+ }); |
+ for (var i = newBreaks.length - 1; i >= 0; i--) |
+ replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length)) |
+ }); |
+ option("specialChars", /[\u0000-\u001f\u007f\u00ad\u200b-\u200f\u2028\u2029\ufeff]/g, function(cm, val, old) { |
+ cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); |
+ if (old != CodeMirror.Init) cm.refresh(); |
+ }); |
option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function(cm) {cm.refresh();}, true); |
option("electricChars", true); |
+ option("inputStyle", mobile ? "contenteditable" : "textarea", function() { |
+ throw new Error("inputStyle can not (yet) be changed in a running editor"); // FIXME |
+ }, true); |
option("rtlMoveVisually", !windows); |
option("wholeLineUpdateBefore", true); |
@@ -4658,6 +5460,7 @@ |
option("showCursorWhenSelecting", false, updateSelection, true); |
option("resetSelectionOnContextMenu", true); |
+ option("lineWiseCopyCut", true); |
option("readOnly", false, function(cm, val) { |
if (val == "nocursor") { |
@@ -4666,11 +5469,12 @@ |
cm.display.disabled = true; |
} else { |
cm.display.disabled = false; |
- if (!val) resetInput(cm); |
} |
+ cm.display.input.readOnlyChanged(val) |
}); |
- option("disableInput", false, function(cm, val) {if (!val) resetInput(cm);}, true); |
- option("dragDrop", true); |
+ option("disableInput", false, function(cm, val) {if (!val) cm.display.input.reset();}, true); |
+ option("dragDrop", true, dragDropChanged); |
+ option("allowDropFileTypes", null); |
option("cursorBlinkRate", 530); |
option("cursorScrollMargin", 0); |
@@ -4686,11 +5490,11 @@ |
option("viewportMargin", 10, function(cm){cm.refresh();}, true); |
option("maxHighlightLength", 10000, resetModeState, true); |
option("moveInputWithCursor", true, function(cm, val) { |
- if (!val) cm.display.inputDiv.style.top = cm.display.inputDiv.style.left = 0; |
+ if (!val) cm.display.input.resetPosition(); |
}); |
option("tabindex", null, function(cm, val) { |
- cm.display.input.tabIndex = val || ""; |
+ cm.display.input.getField().tabIndex = val || ""; |
}); |
option("autofocus", null); |
@@ -4936,7 +5740,7 @@ |
for (var i = 0; i < ranges.length; i++) { |
var pos = ranges[i].from(); |
var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize); |
- spaces.push(new Array(tabSize - col % tabSize + 1).join(" ")); |
+ spaces.push(spaceStr(tabSize - col % tabSize)); |
} |
cm.replaceSelections(spaces); |
}, |
@@ -4958,7 +5762,8 @@ |
} else if (cur.line > cm.doc.first) { |
var prev = getLine(cm.doc, cur.line - 1).text; |
if (prev) |
- cm.replaceRange(line.charAt(0) + "\n" + prev.charAt(prev.length - 1), |
+ cm.replaceRange(line.charAt(0) + cm.doc.lineSeparator() + |
+ prev.charAt(prev.length - 1), |
Pos(cur.line - 1, prev.length - 1), Pos(cur.line, 1), "+transpose"); |
} |
} |
@@ -4972,12 +5777,13 @@ |
var len = cm.listSelections().length; |
for (var i = 0; i < len; i++) { |
var range = cm.listSelections()[i]; |
- cm.replaceRange("\n", range.anchor, range.head, "+input"); |
+ cm.replaceRange(cm.doc.lineSeparator(), range.anchor, range.head, "+input"); |
cm.indentLine(range.from().line + 1, null, true); |
- ensureCursorVisible(cm); |
} |
+ ensureCursorVisible(cm); |
}); |
}, |
+ openLine: function(cm) {cm.replaceSelection("\n", "start")}, |
toggleOverwrite: function(cm) {cm.toggleOverwrite();} |
}; |
@@ -5012,7 +5818,8 @@ |
"Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", |
"Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", |
"Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", |
- "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars" |
+ "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars", |
+ "Ctrl-O": "openLine" |
}; |
keyMap.macDefault = { |
"Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", |
@@ -5062,7 +5869,7 @@ |
for (var i = 0; i < keys.length; i++) { |
var val, name; |
if (i == keys.length - 1) { |
- name = keyname; |
+ name = keys.join(" "); |
val = value; |
} else { |
name = keys.slice(0, i + 1).join(" "); |
@@ -5121,10 +5928,10 @@ |
// FROMTEXTAREA |
CodeMirror.fromTextArea = function(textarea, options) { |
- if (!options) options = {}; |
+ options = options ? copyObj(options) : {}; |
options.value = textarea.value; |
- if (!options.tabindex && textarea.tabindex) |
- options.tabindex = textarea.tabindex; |
+ if (!options.tabindex && textarea.tabIndex) |
+ options.tabindex = textarea.tabIndex; |
if (!options.placeholder && textarea.placeholder) |
options.placeholder = textarea.placeholder; |
// Set autofocus to true if this textarea is focused, or if it has |
@@ -5152,23 +5959,26 @@ |
} |
} |
+ options.finishInit = function(cm) { |
+ cm.save = save; |
+ cm.getTextArea = function() { return textarea; }; |
+ cm.toTextArea = function() { |
+ cm.toTextArea = isNaN; // Prevent this from being ran twice |
+ save(); |
+ textarea.parentNode.removeChild(cm.getWrapperElement()); |
+ textarea.style.display = ""; |
+ if (textarea.form) { |
+ off(textarea.form, "submit", save); |
+ if (typeof textarea.form.submit == "function") |
+ textarea.form.submit = realSubmit; |
+ } |
+ }; |
+ }; |
+ |
textarea.style.display = "none"; |
var cm = CodeMirror(function(node) { |
textarea.parentNode.insertBefore(node, textarea.nextSibling); |
}, options); |
- cm.save = save; |
- cm.getTextArea = function() { return textarea; }; |
- cm.toTextArea = function() { |
- cm.toTextArea = isNaN; // Prevent this from being ran twice |
- save(); |
- textarea.parentNode.removeChild(cm.getWrapperElement()); |
- textarea.style.display = ""; |
- if (textarea.form) { |
- off(textarea.form, "submit", save); |
- if (typeof textarea.form.submit == "function") |
- textarea.form.submit = realSubmit; |
- } |
- }; |
return cm; |
}; |
@@ -5261,10 +6071,13 @@ |
// marker continues beyond the start/end of the line. Markers have |
// links back to the lines they currently touch. |
+ var nextMarkerId = 0; |
+ |
var TextMarker = CodeMirror.TextMarker = function(doc, type) { |
this.lines = []; |
this.type = type; |
this.doc = doc; |
+ this.id = ++nextMarkerId; |
}; |
eventMixin(TextMarker); |
@@ -5768,8 +6581,8 @@ |
var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker); |
var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker); |
if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue; |
- if (fromCmp <= 0 && (cmp(found.to, from) > 0 || (sp.marker.inclusiveRight && marker.inclusiveLeft)) || |
- fromCmp >= 0 && (cmp(found.from, to) < 0 || (sp.marker.inclusiveLeft && marker.inclusiveRight))) |
+ if (fromCmp <= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.to, from) >= 0 : cmp(found.to, from) > 0) || |
+ fromCmp >= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.from, to) <= 0 : cmp(found.from, to) < 0)) |
return true; |
} |
} |
@@ -5848,10 +6661,10 @@ |
// Line widgets are block elements displayed above or below a line. |
- var LineWidget = CodeMirror.LineWidget = function(cm, node, options) { |
+ var LineWidget = CodeMirror.LineWidget = function(doc, node, options) { |
if (options) for (var opt in options) if (options.hasOwnProperty(opt)) |
this[opt] = options[opt]; |
- this.cm = cm; |
+ this.doc = doc; |
this.node = node; |
}; |
eventMixin(LineWidget); |
@@ -5862,52 +6675,55 @@ |
} |
LineWidget.prototype.clear = function() { |
- var cm = this.cm, ws = this.line.widgets, line = this.line, no = lineNo(line); |
+ var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line); |
if (no == null || !ws) return; |
for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1); |
if (!ws.length) line.widgets = null; |
var height = widgetHeight(this); |
- runInOp(cm, function() { |
+ updateLineHeight(line, Math.max(0, line.height - height)); |
+ if (cm) runInOp(cm, function() { |
adjustScrollWhenAboveVisible(cm, line, -height); |
regLineChange(cm, no, "widget"); |
- updateLineHeight(line, Math.max(0, line.height - height)); |
}); |
}; |
LineWidget.prototype.changed = function() { |
- var oldH = this.height, cm = this.cm, line = this.line; |
+ var oldH = this.height, cm = this.doc.cm, line = this.line; |
this.height = null; |
var diff = widgetHeight(this) - oldH; |
if (!diff) return; |
- runInOp(cm, function() { |
+ updateLineHeight(line, line.height + diff); |
+ if (cm) runInOp(cm, function() { |
cm.curOp.forceUpdate = true; |
adjustScrollWhenAboveVisible(cm, line, diff); |
- updateLineHeight(line, line.height + diff); |
}); |
}; |
function widgetHeight(widget) { |
if (widget.height != null) return widget.height; |
+ var cm = widget.doc.cm; |
+ if (!cm) return 0; |
if (!contains(document.body, widget.node)) { |
var parentStyle = "position: relative;"; |
if (widget.coverGutter) |
- parentStyle += "margin-left: -" + widget.cm.display.gutters.offsetWidth + "px;"; |
+ parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;"; |
if (widget.noHScroll) |
- parentStyle += "width: " + widget.cm.display.wrapper.clientWidth + "px;"; |
- removeChildrenAndAdd(widget.cm.display.measure, elt("div", [widget.node], null, parentStyle)); |
+ parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;"; |
+ removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle)); |
} |
- return widget.height = widget.node.offsetHeight; |
+ return widget.height = widget.node.parentNode.offsetHeight; |
} |
- function addLineWidget(cm, handle, node, options) { |
- var widget = new LineWidget(cm, node, options); |
- if (widget.noHScroll) cm.display.alignWidgets = true; |
- changeLine(cm.doc, handle, "widget", function(line) { |
+ function addLineWidget(doc, handle, node, options) { |
+ var widget = new LineWidget(doc, node, options); |
+ var cm = doc.cm; |
+ if (cm && widget.noHScroll) cm.display.alignWidgets = true; |
+ changeLine(doc, handle, "widget", function(line) { |
var widgets = line.widgets || (line.widgets = []); |
if (widget.insertAt == null) widgets.push(widget); |
else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget); |
widget.line = line; |
- if (!lineIsHidden(cm.doc, line)) { |
- var aboveVisible = heightAtLine(line) < cm.doc.scrollTop; |
+ if (cm && !lineIsHidden(doc, line)) { |
+ var aboveVisible = heightAtLine(line) < doc.scrollTop; |
updateLineHeight(line, line.height + widgetHeight(widget)); |
if (aboveVisible) addToScrollPos(cm, null, widget.height); |
cm.curOp.forceUpdate = true; |
@@ -6083,7 +6899,9 @@ |
function getLineStyles(cm, line, updateFrontier) { |
if (!line.styles || line.styles[0] != cm.state.modeGen) { |
- var result = highlightLine(cm, line, line.stateAfter = getStateBefore(cm, lineNo(line))); |
+ var state = getStateBefore(cm, lineNo(line)); |
+ var result = highlightLine(cm, line, line.text.length > cm.options.maxHighlightLength ? copyState(cm.doc.mode, state) : state); |
+ line.stateAfter = state; |
line.styles = result.styles; |
if (result.classes) line.styleClasses = result.classes; |
else if (line.styleClasses) line.styleClasses = null; |
@@ -6100,7 +6918,7 @@ |
var stream = new StringStream(text, cm.options.tabSize); |
stream.start = stream.pos = startAt || 0; |
if (text == "") callBlankLine(mode, state); |
- while (!stream.eol() && stream.pos <= cm.options.maxHighlightLength) { |
+ while (!stream.eol()) { |
readToken(mode, stream, state); |
stream.start = stream.pos; |
} |
@@ -6127,7 +6945,10 @@ |
// is needed on Webkit to be able to get line-level bounding |
// rectangles for it (in measureChar). |
var content = elt("span", null, null, webkit ? "padding-right: .1px" : null); |
- var builder = {pre: elt("pre", [content]), content: content, col: 0, pos: 0, cm: cm}; |
+ var builder = {pre: elt("pre", [content], "CodeMirror-line"), content: content, |
+ col: 0, pos: 0, cm: cm, |
+ trailingSpace: false, |
+ splitSpaces: (ie || webkit) && cm.getOption("lineWrapping")}; |
lineView.measure = {}; |
// Iterate over the logical lines that make up this visual line. |
@@ -6137,8 +6958,6 @@ |
builder.addToken = buildToken; |
// Optionally wire in some hacks into the token-rendering |
// algorithm, to deal with browser quirks. |
- if ((ie || webkit) && cm.getOption("lineWrapping")) |
- builder.addToken = buildTokenSplitSpaces(builder.addToken); |
if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line))) |
builder.addToken = buildTokenBadBidi(builder.addToken, order); |
builder.map = []; |
@@ -6166,8 +6985,11 @@ |
} |
// See issue #2901 |
- if (webkit && /\bcm-tab\b/.test(builder.content.lastChild.className)) |
- builder.content.className = "cm-tab-wrap-hack"; |
+ if (webkit) { |
+ var last = builder.content.lastChild |
+ if (/\bcm-tab\b/.test(last.className) || (last.querySelector && last.querySelector(".cm-tab"))) |
+ builder.content.className = "cm-tab-wrap-hack"; |
+ } |
signal(cm, "renderLine", cm, lineView.line, builder.pre); |
if (builder.pre.className) |
@@ -6179,6 +7001,7 @@ |
function defaultSpecialCharPlaceholder(ch) { |
var token = elt("span", "\u2022", "cm-invalidchar"); |
token.title = "\\u" + ch.charCodeAt(0).toString(16); |
+ token.setAttribute("aria-label", token.title); |
return token; |
} |
@@ -6186,10 +7009,11 @@ |
// the line map. Takes care to render special characters separately. |
function buildToken(builder, text, style, startStyle, endStyle, title, css) { |
if (!text) return; |
- var special = builder.cm.options.specialChars, mustWrap = false; |
+ var displayText = builder.splitSpaces ? splitSpaces(text, builder.trailingSpace) : text |
+ var special = builder.cm.state.specialChars, mustWrap = false; |
if (!special.test(text)) { |
builder.col += text.length; |
- var content = document.createTextNode(text); |
+ var content = document.createTextNode(displayText); |
builder.map.push(builder.pos, builder.pos + text.length, content); |
if (ie && ie_version < 9) mustWrap = true; |
builder.pos += text.length; |
@@ -6200,7 +7024,7 @@ |
var m = special.exec(text); |
var skipped = m ? m.index - pos : text.length - pos; |
if (skipped) { |
- var txt = document.createTextNode(text.slice(pos, pos + skipped)); |
+ var txt = document.createTextNode(displayText.slice(pos, pos + skipped)); |
if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); |
else content.appendChild(txt); |
builder.map.push(builder.pos, builder.pos + skipped, txt); |
@@ -6212,9 +7036,16 @@ |
if (m[0] == "\t") { |
var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; |
var txt = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); |
+ txt.setAttribute("role", "presentation"); |
+ txt.setAttribute("cm-text", "\t"); |
builder.col += tabWidth; |
+ } else if (m[0] == "\r" || m[0] == "\n") { |
+ var txt = content.appendChild(elt("span", m[0] == "\r" ? "\u240d" : "\u2424", "cm-invalidchar")); |
+ txt.setAttribute("cm-text", m[0]); |
+ builder.col += 1; |
} else { |
var txt = builder.cm.options.specialCharPlaceholder(m[0]); |
+ txt.setAttribute("cm-text", m[0]); |
if (ie && ie_version < 9) content.appendChild(elt("span", [txt])); |
else content.appendChild(txt); |
builder.col += 1; |
@@ -6223,6 +7054,7 @@ |
builder.pos++; |
} |
} |
+ builder.trailingSpace = displayText.charCodeAt(text.length - 1) == 32 |
if (style || startStyle || endStyle || mustWrap || css) { |
var fullStyle = style || ""; |
if (startStyle) fullStyle += startStyle; |
@@ -6234,22 +7066,23 @@ |
builder.content.appendChild(content); |
} |
- function buildTokenSplitSpaces(inner) { |
- function split(old) { |
- var out = " "; |
- for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0"; |
- out += " "; |
- return out; |
+ function splitSpaces(text, trailingBefore) { |
+ if (text.length > 1 && !/ /.test(text)) return text |
+ var spaceBefore = trailingBefore, result = "" |
+ for (var i = 0; i < text.length; i++) { |
+ var ch = text.charAt(i) |
+ if (ch == " " && spaceBefore && (i == text.length - 1 || text.charCodeAt(i + 1) == 32)) |
+ ch = "\u00a0" |
+ result += ch |
+ spaceBefore = ch == " " |
} |
- return function(builder, text, style, startStyle, endStyle, title) { |
- inner(builder, text.replace(/ {3,}/g, split), style, startStyle, endStyle, title); |
- }; |
+ return result |
} |
// Work around nonsense dimensions being reported for stretches of |
// right-to-left text. |
function buildTokenBadBidi(inner, order) { |
- return function(builder, text, style, startStyle, endStyle, title) { |
+ return function(builder, text, style, startStyle, endStyle, title, css) { |
style = style ? style + " cm-force-border" : "cm-force-border"; |
var start = builder.pos, end = start + text.length; |
for (;;) { |
@@ -6258,8 +7091,8 @@ |
var part = order[i]; |
if (part.to > start && part.from <= start) break; |
} |
- if (part.to >= end) return inner(builder, text, style, startStyle, endStyle, title); |
- inner(builder, text.slice(0, part.to - start), style, startStyle, null, title); |
+ if (part.to >= end) return inner(builder, text, style, startStyle, endStyle, title, css); |
+ inner(builder, text.slice(0, part.to - start), style, startStyle, null, title, css); |
startStyle = null; |
text = text.slice(part.to - start); |
start = part.to; |
@@ -6269,11 +7102,18 @@ |
function buildCollapsedSpan(builder, size, marker, ignoreWidget) { |
var widget = !ignoreWidget && marker.widgetNode; |
+ if (widget) builder.map.push(builder.pos, builder.pos + size, widget); |
+ if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) { |
+ if (!widget) |
+ widget = builder.content.appendChild(document.createElement("span")); |
+ widget.setAttribute("cm-marker", marker.id); |
+ } |
if (widget) { |
- builder.map.push(builder.pos, builder.pos + size, widget); |
+ builder.cm.display.input.setUneditable(widget); |
builder.content.appendChild(widget); |
} |
builder.pos += size; |
+ builder.trailingSpace = false |
} |
// Outputs a number of spans to make up a line, taking highlighting |
@@ -6292,30 +7132,38 @@ |
if (nextChange == pos) { // Update current marker set |
spanStyle = spanEndStyle = spanStartStyle = title = css = ""; |
collapsed = null; nextChange = Infinity; |
- var foundBookmarks = []; |
+ var foundBookmarks = [], endStyles |
for (var j = 0; j < spans.length; ++j) { |
var sp = spans[j], m = sp.marker; |
- if (sp.from <= pos && (sp.to == null || sp.to > pos)) { |
- if (sp.to != null && nextChange > sp.to) { nextChange = sp.to; spanEndStyle = ""; } |
+ if (m.type == "bookmark" && sp.from == pos && m.widgetNode) { |
+ foundBookmarks.push(m); |
+ } else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) { |
+ if (sp.to != null && sp.to != pos && nextChange > sp.to) { |
+ nextChange = sp.to; |
+ spanEndStyle = ""; |
+ } |
if (m.className) spanStyle += " " + m.className; |
- if (m.css) css = m.css; |
+ if (m.css) css = (css ? css + ";" : "") + m.css; |
if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle; |
- if (m.endStyle && sp.to == nextChange) spanEndStyle += " " + m.endStyle; |
+ if (m.endStyle && sp.to == nextChange) (endStyles || (endStyles = [])).push(m.endStyle, sp.to) |
if (m.title && !title) title = m.title; |
if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0)) |
collapsed = sp; |
} else if (sp.from > pos && nextChange > sp.from) { |
nextChange = sp.from; |
} |
- if (m.type == "bookmark" && sp.from == pos && m.widgetNode) foundBookmarks.push(m); |
} |
+ if (endStyles) for (var j = 0; j < endStyles.length; j += 2) |
+ if (endStyles[j + 1] == nextChange) spanEndStyle += " " + endStyles[j] |
+ |
+ if (!collapsed || collapsed.from == pos) for (var j = 0; j < foundBookmarks.length; ++j) |
+ buildCollapsedSpan(builder, 0, foundBookmarks[j]); |
if (collapsed && (collapsed.from || 0) == pos) { |
buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos, |
collapsed.marker, collapsed.from == null); |
if (collapsed.to == null) return; |
+ if (collapsed.to == pos) collapsed = false; |
} |
- if (!collapsed && foundBookmarks.length) for (var j = 0; j < foundBookmarks.length; ++j) |
- buildCollapsedSpan(builder, 0, foundBookmarks[j]); |
} |
if (pos >= len) break; |
@@ -6501,13 +7349,16 @@ |
if (at <= sz) { |
child.insertInner(at, lines, height); |
if (child.lines && child.lines.length > 50) { |
- while (child.lines.length > 50) { |
- var spilled = child.lines.splice(child.lines.length - 25, 25); |
- var newleaf = new LeafChunk(spilled); |
- child.height -= newleaf.height; |
- this.children.splice(i + 1, 0, newleaf); |
- newleaf.parent = this; |
+ // To avoid memory thrashing when child.lines is huge (e.g. first view of a large file), it's never spliced. |
+ // Instead, small slices are taken. They're taken in order because sequential memory accesses are fastest. |
+ var remaining = child.lines.length % 25 + 25 |
+ for (var pos = remaining; pos < child.lines.length;) { |
+ var leaf = new LeafChunk(child.lines.slice(pos, pos += 25)); |
+ child.height -= leaf.height; |
+ this.children.splice(++i, 0, leaf); |
+ leaf.parent = this; |
} |
+ child.lines = child.lines.slice(0, remaining); |
this.maybeSpill(); |
} |
break; |
@@ -6527,7 +7378,7 @@ |
copy.parent = me; |
me.children = [copy, sibling]; |
me = copy; |
- } else { |
+ } else { |
me.size -= sibling.size; |
me.height -= sibling.height; |
var myIndex = indexOf(me.parent.children, me); |
@@ -6551,8 +7402,8 @@ |
}; |
var nextDocId = 0; |
- var Doc = CodeMirror.Doc = function(text, mode, firstLine) { |
- if (!(this instanceof Doc)) return new Doc(text, mode, firstLine); |
+ var Doc = CodeMirror.Doc = function(text, mode, firstLine, lineSep) { |
+ if (!(this instanceof Doc)) return new Doc(text, mode, firstLine, lineSep); |
if (firstLine == null) firstLine = 0; |
BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); |
@@ -6566,8 +7417,10 @@ |
this.history = new History(null); |
this.id = ++nextDocId; |
this.modeOption = mode; |
+ this.lineSep = lineSep; |
+ this.extend = false; |
- if (typeof text == "string") text = splitLines(text); |
+ if (typeof text == "string") text = this.splitLines(text); |
updateDoc(this, {from: start, to: start, text: text}); |
setSelection(this, simpleSelection(start), sel_dontScroll); |
}; |
@@ -6597,12 +7450,12 @@ |
getValue: function(lineSep) { |
var lines = getLines(this, this.first, this.first + this.size); |
if (lineSep === false) return lines; |
- return lines.join(lineSep || "\n"); |
+ return lines.join(lineSep || this.lineSeparator()); |
}, |
setValue: docMethodOp(function(code) { |
var top = Pos(this.first, 0), last = this.first + this.size - 1; |
makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), |
- text: splitLines(code), origin: "setValue", full: true}, true); |
+ text: this.splitLines(code), origin: "setValue", full: true}, true); |
setSelection(this, simpleSelection(top)); |
}), |
replaceRange: function(code, from, to, origin) { |
@@ -6613,7 +7466,7 @@ |
getRange: function(from, to, lineSep) { |
var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); |
if (lineSep === false) return lines; |
- return lines.join(lineSep || "\n"); |
+ return lines.join(lineSep || this.lineSeparator()); |
}, |
getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;}, |
@@ -6653,10 +7506,11 @@ |
extendSelection(this, clipPos(this, head), other && clipPos(this, other), options); |
}), |
extendSelections: docMethodOp(function(heads, options) { |
- extendSelections(this, clipPosArray(this, heads, options)); |
+ extendSelections(this, clipPosArray(this, heads), options); |
}), |
extendSelectionsBy: docMethodOp(function(f, options) { |
- extendSelections(this, map(this.sel.ranges, f), options); |
+ var heads = map(this.sel.ranges, f); |
+ extendSelections(this, clipPosArray(this, heads), options); |
}), |
setSelections: docMethodOp(function(ranges, primary, options) { |
if (!ranges.length) return; |
@@ -6679,13 +7533,13 @@ |
lines = lines ? lines.concat(sel) : sel; |
} |
if (lineSep === false) return lines; |
- else return lines.join(lineSep || "\n"); |
+ else return lines.join(lineSep || this.lineSeparator()); |
}, |
getSelections: function(lineSep) { |
var parts = [], ranges = this.sel.ranges; |
for (var i = 0; i < ranges.length; i++) { |
var sel = getBetween(this, ranges[i].from(), ranges[i].to()); |
- if (lineSep !== false) sel = sel.join(lineSep || "\n"); |
+ if (lineSep !== false) sel = sel.join(lineSep || this.lineSeparator()); |
parts[i] = sel; |
} |
return parts; |
@@ -6700,7 +7554,7 @@ |
var changes = [], sel = this.sel; |
for (var i = 0; i < sel.ranges.length; i++) { |
var range = sel.ranges[i]; |
- changes[i] = {from: range.from(), to: range.to(), text: splitLines(code[i]), origin: origin}; |
+ changes[i] = {from: range.from(), to: range.to(), text: this.splitLines(code[i]), origin: origin}; |
} |
var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse); |
for (var i = changes.length - 1; i >= 0; i--) |
@@ -6775,13 +7629,19 @@ |
}); |
}), |
+ addLineWidget: docMethodOp(function(handle, node, options) { |
+ return addLineWidget(this, handle, node, options); |
+ }), |
+ removeLineWidget: function(widget) { widget.clear(); }, |
+ |
markText: function(from, to, options) { |
- return markText(this, clipPos(this, from), clipPos(this, to), options, "range"); |
+ return markText(this, clipPos(this, from), clipPos(this, to), options, options && options.type || "range"); |
}, |
setBookmark: function(pos, options) { |
var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), |
insertLeft: options && options.insertLeft, |
- clearWhenEmpty: false, shared: options && options.shared}; |
+ clearWhenEmpty: false, shared: options && options.shared, |
+ handleMouseEvents: options && options.handleMouseEvents}; |
pos = clipPos(this, pos); |
return markText(this, pos, pos, realOpts, "bookmark"); |
}, |
@@ -6803,9 +7663,9 @@ |
var spans = line.markedSpans; |
if (spans) for (var i = 0; i < spans.length; i++) { |
var span = spans[i]; |
- if (!(lineNo == from.line && from.ch > span.to || |
- span.from == null && lineNo != from.line|| |
- lineNo == to.line && span.from > to.ch) && |
+ if (!(span.to != null && lineNo == from.line && from.ch >= span.to || |
+ span.from == null && lineNo != from.line || |
+ span.from != null && lineNo == to.line && span.from >= to.ch) && |
(!filter || filter(span.marker))) |
found.push(span.marker.parent || span.marker); |
} |
@@ -6824,9 +7684,9 @@ |
}, |
posFromIndex: function(off) { |
- var ch, lineNo = this.first; |
+ var ch, lineNo = this.first, sepSize = this.lineSeparator().length; |
this.iter(function(line) { |
- var sz = line.text.length + 1; |
+ var sz = line.text.length + sepSize; |
if (sz > off) { ch = off; return true; } |
off -= sz; |
++lineNo; |
@@ -6837,14 +7697,16 @@ |
coords = clipPos(this, coords); |
var index = coords.ch; |
if (coords.line < this.first || coords.ch < 0) return 0; |
+ var sepSize = this.lineSeparator().length; |
this.iter(this.first, coords.line, function (line) { |
- index += line.text.length + 1; |
+ index += line.text.length + sepSize; |
}); |
return index; |
}, |
copy: function(copyHistory) { |
- var doc = new Doc(getLines(this, this.first, this.first + this.size), this.modeOption, this.first); |
+ var doc = new Doc(getLines(this, this.first, this.first + this.size), |
+ this.modeOption, this.first, this.lineSep); |
doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; |
doc.sel = this.sel; |
doc.extend = false; |
@@ -6860,7 +7722,7 @@ |
var from = this.first, to = this.first + this.size; |
if (options.from != null && options.from > from) from = options.from; |
if (options.to != null && options.to < to) to = options.to; |
- var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from); |
+ var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep); |
if (options.sharedHist) copy.history = this.history; |
(this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); |
copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; |
@@ -6889,14 +7751,20 @@ |
iterLinkedDocs: function(f) {linkedDocs(this, f);}, |
getMode: function() {return this.mode;}, |
- getEditor: function() {return this.cm;} |
+ getEditor: function() {return this.cm;}, |
+ |
+ splitLines: function(str) { |
+ if (this.lineSep) return str.split(this.lineSep); |
+ return splitLinesAuto(str); |
+ }, |
+ lineSeparator: function() { return this.lineSep || "\n"; } |
}); |
// Public alias. |
Doc.prototype.eachLine = Doc.prototype.iter; |
// Set up methods on CodeMirror's prototype to redirect to the editor's document. |
- var dontDelegate = "iter insert remove copy getEditor".split(" "); |
+ var dontDelegate = "iter insert remove copy getEditor constructor".split(" "); |
for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) |
CodeMirror.prototype[prop] = (function(method) { |
return function() {return method.apply(this.doc, arguments);}; |
@@ -7329,24 +8197,30 @@ |
} |
}; |
+ var noHandlers = [] |
+ function getHandlers(emitter, type, copy) { |
+ var arr = emitter._handlers && emitter._handlers[type] |
+ if (copy) return arr && arr.length > 0 ? arr.slice() : noHandlers |
+ else return arr || noHandlers |
+ } |
+ |
var off = CodeMirror.off = function(emitter, type, f) { |
if (emitter.removeEventListener) |
emitter.removeEventListener(type, f, false); |
else if (emitter.detachEvent) |
emitter.detachEvent("on" + type, f); |
else { |
- var arr = emitter._handlers && emitter._handlers[type]; |
- if (!arr) return; |
- for (var i = 0; i < arr.length; ++i) |
- if (arr[i] == f) { arr.splice(i, 1); break; } |
+ var handlers = getHandlers(emitter, type, false) |
+ for (var i = 0; i < handlers.length; ++i) |
+ if (handlers[i] == f) { handlers.splice(i, 1); break; } |
} |
}; |
var signal = CodeMirror.signal = function(emitter, type /*, values...*/) { |
- var arr = emitter._handlers && emitter._handlers[type]; |
- if (!arr) return; |
+ var handlers = getHandlers(emitter, type, true) |
+ if (!handlers.length) return; |
var args = Array.prototype.slice.call(arguments, 2); |
- for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args); |
+ for (var i = 0; i < handlers.length; ++i) handlers[i].apply(null, args); |
}; |
var orphanDelayedCallbacks = null; |
@@ -7359,8 +8233,8 @@ |
// them to be executed when the last operation ends, or, if no |
// operation is active, when a timeout fires. |
function signalLater(emitter, type /*, values...*/) { |
- var arr = emitter._handlers && emitter._handlers[type]; |
- if (!arr) return; |
+ var arr = getHandlers(emitter, type, false) |
+ if (!arr.length) return; |
var args = Array.prototype.slice.call(arguments, 2), list; |
if (operationGroup) { |
list = operationGroup.delayedCallbacks; |
@@ -7400,8 +8274,7 @@ |
} |
function hasHandler(emitter, type) { |
- var arr = emitter._handlers && emitter._handlers[type]; |
- return arr && arr.length > 0; |
+ return getHandlers(emitter, type).length > 0 |
} |
// Add on and off methods to a constructor's prototype, to make |
@@ -7448,7 +8321,7 @@ |
// The inverse of countColumn -- find the offset that corresponds to |
// a particular column. |
- function findColumn(string, goal, tabSize) { |
+ var findColumn = CodeMirror.findColumn = function(string, goal, tabSize) { |
for (var pos = 0, col = 0;;) { |
var nextTab = string.indexOf("\t", pos); |
if (nextTab == -1) nextTab = string.length; |
@@ -7488,14 +8361,15 @@ |
return out; |
} |
+ function nothing() {} |
+ |
function createObj(base, props) { |
var inst; |
if (Object.create) { |
inst = Object.create(base); |
} else { |
- var ctor = function() {}; |
- ctor.prototype = base; |
- inst = new ctor(); |
+ nothing.prototype = base; |
+ inst = new nothing(); |
} |
if (props) copyObj(props, inst); |
return inst; |
@@ -7514,7 +8388,7 @@ |
return function(){return f.apply(null, args);}; |
} |
- var nonASCIISingleCaseWordChar = /[\u00df\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; |
+ var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; |
var isWordCharBasic = CodeMirror.isWordChar = function(ch) { |
return /\w/.test(ch) || ch > "\x80" && |
(ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)); |
@@ -7550,9 +8424,9 @@ |
} |
var range; |
- if (document.createRange) range = function(node, start, end) { |
+ if (document.createRange) range = function(node, start, end, endNode) { |
var r = document.createRange(); |
- r.setEnd(node, end); |
+ r.setEnd(endNode || node, end); |
r.setStart(node, start); |
return r; |
}; |
@@ -7576,14 +8450,23 @@ |
return removeChildren(parent).appendChild(e); |
} |
- function contains(parent, child) { |
+ var contains = CodeMirror.contains = function(parent, child) { |
+ if (child.nodeType == 3) // Android browser always returns false when child is a textnode |
+ child = child.parentNode; |
if (parent.contains) |
return parent.contains(child); |
- while (child = child.parentNode) |
+ do { |
+ if (child.nodeType == 11) child = child.host; |
if (child == parent) return true; |
- } |
+ } while (child = child.parentNode); |
+ }; |
- function activeElt() { return document.activeElement; } |
+ function activeElt() { |
+ var activeElement = document.activeElement; |
+ while (activeElement && activeElement.root && activeElement.root.activeElement) |
+ activeElement = activeElement.root.activeElement; |
+ return activeElement; |
+ } |
// Older versions of IE throws unspecified error when touching |
// document.activeElement in some cases (during loading, in iframe) |
if (ie && ie_version < 11) activeElt = function() { |
@@ -7666,8 +8549,10 @@ |
if (measure.firstChild.offsetHeight != 0) |
zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8); |
} |
- if (zwspSupported) return elt("span", "\u200b"); |
- else return elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); |
+ var node = zwspSupported ? elt("span", "\u200b") : |
+ elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); |
+ node.setAttribute("cm-text", ""); |
+ return node; |
} |
// Feature-detect IE's crummy client rect reporting for bidi text |
@@ -7676,14 +8561,15 @@ |
if (badBidiRects != null) return badBidiRects; |
var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA")); |
var r0 = range(txt, 0, 1).getBoundingClientRect(); |
- if (!r0 || r0.left == r0.right) return false; // Safari returns null in some cases (#2780) |
var r1 = range(txt, 1, 2).getBoundingClientRect(); |
+ removeChildren(measure); |
+ if (!r0 || r0.left == r0.right) return false; // Safari returns null in some cases (#2780) |
return badBidiRects = (r1.right - r0.right < 3); |
} |
// See if "".split is the broken IE version, if so, provide an |
// alternative way to split lines. |
- var splitLines = CodeMirror.splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { |
+ var splitLinesAuto = CodeMirror.splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { |
var pos = 0, result = [], l = string.length; |
while (pos <= l) { |
var nl = string.indexOf("\n", pos); |
@@ -7729,14 +8615,16 @@ |
// KEY NAMES |
- var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", |
- 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", |
- 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", |
- 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", 107: "=", 109: "-", 127: "Delete", |
- 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", |
- 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", |
- 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"}; |
- CodeMirror.keyNames = keyNames; |
+ var keyNames = CodeMirror.keyNames = { |
+ 3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", |
+ 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", |
+ 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", |
+ 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", |
+ 106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 127: "Delete", |
+ 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", |
+ 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", |
+ 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert" |
+ }; |
(function() { |
// Number keys |
for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i); |
@@ -8030,6 +8918,8 @@ |
lst(order).to -= m[0].length; |
order.push(new BidiSpan(0, len - m[0].length, len)); |
} |
+ if (order[0].level == 2) |
+ order.unshift(new BidiSpan(1, order[0].to, order[0].to)); |
if (order[0].level != lst(order).level) |
order.push(new BidiSpan(order[0].level, len, len)); |
@@ -8039,7 +8929,7 @@ |
// THE END |
- CodeMirror.version = "4.12.0"; |
+ CodeMirror.version = "5.17.1"; |
return CodeMirror; |
-}); |
+}); |