From 0dc2189b43433e64799731c3265d49879263430a Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Wed, 8 Oct 2014 17:30:38 +0200 Subject: [PATCH 01/47] Make inputStyle an option, move relevant logic into TextareaInput Add a dummy ContentEditableInput --- addon/edit/matchbrackets.js | 2 +- lib/codemirror.js | 915 ++++++++++++++++++++++++-------------------- 2 files changed, 499 insertions(+), 418 deletions(-) diff --git a/addon/edit/matchbrackets.js b/addon/edit/matchbrackets.js index fa1ae030a5..70e1ae18c7 100644 --- a/addon/edit/matchbrackets.js +++ b/addon/edit/matchbrackets.js @@ -81,7 +81,7 @@ if (marks.length) { // Kludge to work around the IE bug from issue #1193, where text // input stops going to the textare whever this fires. - if (ie_lt8 && cm.state.focused) cm.display.input.focus(); + if (ie_lt8 && cm.state.focused) cm.focus(); var clear = function() { cm.operation(function() { diff --git a/lib/codemirror.js b/lib/codemirror.js index 0e3d019278..0e680db3ff 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -33,7 +33,6 @@ var chrome = /Chrome\//.test(navigator.userAgent); var presto = /Opera\//.test(navigator.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); @@ -70,13 +69,14 @@ if (typeof doc == "string") doc = new Doc(doc, options.mode); 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 = { @@ -85,15 +85,17 @@ modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info overwrite: 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 draggingText: false, highlight: new Delayed(), // stores highlight worker timeout keySeq: null // Unfinished key sequence }; + 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 +104,7 @@ this.curOp.forceUpdate = true; attachDoc(this, doc); - if ((options.autofocus && !mobile) || activeElt() == display.input) + if ((options.autofocus && !mobile) || display.input.hasFocus()) setTimeout(bind(onFocus, this), 20); else onBlur(this); @@ -126,24 +128,10 @@ // 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"); @@ -179,15 +167,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 (place) { if (place.appendChild) place.appendChild(d.wrapper); @@ -214,25 +198,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; @@ -248,6 +220,8 @@ // Used to track whether anything happened since the context menu // was opened. d.selForContextMenu = null; + + input.init(this); } // STATE UPDATES @@ -515,8 +489,9 @@ 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"); }, function(pos, axis) { @@ -1084,6 +1059,449 @@ 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); } + } + + function isReadOnly(cm) { + return cm.options.readOnly || cm.doc.cantEdit; + } + + // 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; + + var emptyInput = { + init: nothing, + drawSelection: nothing, + showSelection: nothing, + reset: nothing, + getField: nothing, + focus: nothing, + hasFocus: nothing, + blur: nothing, + setTabIndex: nothing, + resetPosition: nothing, + slowPoll: nothing, + fastPoll: nothing, + poll: nothing, + ensurePolled: nothing, + onKeyPress: nothing, + onContextMenu: nothing + }; + + // 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; + }; + + TextareaInput.prototype = createObj(emptyInput, { + init: function(display) { + var input = this, cm = this.cm; + // The semihidden textarea that is focused when the editor is + // focused, and receives input. + var te = this.textarea = 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) 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"; + te.setAttribute("autocorrect", "off"); te.setAttribute("autocapitalize", "off"); te.setAttribute("spellcheck", "false"); + + // Wraps and hides input textarea + var div = this.wrapper = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); + 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, "keyup", function(e) { onKeyUp.call(cm, e); }); + on(te, "input", function() { + if (ie && ie_version >= 9 && input.hasSelection) input.hasSelection = null; + input.poll(); + }); + on(te, "keydown", operation(cm, onKeyDown)); + on(te, "keypress", operation(cm, onKeyPress)); + on(te, "focus", bind(onFocus, cm)); + on(te, "blur", bind(onBlur, cm)); + + on(te, "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 = te.selectionStart, end = te.selectionEnd; + te.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. + te.selectionEnd = end; + te.selectionStart = start; + cm.state.fakedLastChar = true; + } + cm.state.pasteIncoming = true; + input.fastPoll(); + }); + + function prepareCopyCut(e) { + if (cm.somethingSelected()) { + lastCopied = cm.getSelections(); + if (this.inaccurateSelection) { + input.prevInput = ""; + input.inaccurateSelection = false; + te.value = lastCopied.join("\n"); + selectInput(te); + } + } 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 { + input.prevInput = ""; + te.value = text.join("\n"); + selectInput(te); + } + lastCopied = text; + } + if (e.type == "cut") cm.state.cutIncoming = true; + } + on(te, "cut", prepareCopyCut); + on(te, "copy", prepareCopyCut); + + // Prevent normal selection in the editor (we handle our own) + on(display.lineSpace, "selectstart", function(e) { + if (!eventInWidget(display, e)) e_preventDefault(e); + }); + }, + + drawSelection: function() { + // Redraw the selection and/or cursor + var cm = this.cm, display = cm.display, 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++) { + var range = doc.sel.ranges[i]; + var collapsed = range.empty(); + if (collapsed || cm.options.showCursorWhenSelecting) + drawSelectionCursor(cm, range, 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; + }, + + 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; + }, + + 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(); + }, + + hasFocus: function() { + return activeElt() == this.textarea; + }, + + setTabIndex: function(index) { + this.textarea.tabIndex = index; + }, + + resetPosition: function() { + this.wrapper.style.top = this.wrapper.style.left = 0; + }, + + // 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, 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 && this.hasSelection === text || + mac && /[\uf700-\uf7ff]/.test(text)) { + cm.display.input.reset(); + 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 = this.prevInput = ""; + else this.prevInput = text; + if (withOp) endOperation(cm); + cm.state.pasteIncoming = cm.state.cutIncoming = false; + 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; + input.wrapper.style.position = "absolute"; + te.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) + 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 = te.value = "\u200b" + (selected ? te.value : ""); + 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.position = "relative"; + te.style.cssText = oldCSS; + if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos); + display.input.slowPoll(); + + // 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) + 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); + } + } + }); + + // CONTENTEDITABLE INPUT STYLE + + function ContentEditableInput() {} + + ContentEditableInput.prototype = createObj(emptyInput, {}); + + CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; + // SELECTION / CURSOR // Selection objects are immutable. A new one is created every time @@ -1371,45 +1789,8 @@ // SELECTION DRAWING - // Redraw the selection and/or cursor - function drawSelection(cm) { - var display = cm.display, 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++) { - var range = doc.sel.ranges[i]; - var collapsed = range.empty(); - if (collapsed || cm.options.showCursorWhenSelecting) - drawSelectionCursor(cm, range, 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)); + cm.display.input.showSelection(cm.display.input.drawSelection()); } // Draws a cursor for the given range @@ -2181,7 +2562,7 @@ } if (op.updatedDisplay || op.selectionChanged) - op.newSelectionNodes = drawSelection(cm); + op.preparedSelection = display.input.drawSelection(); } function endOperation_W2(op) { @@ -2194,8 +2575,8 @@ cm.display.maxLineChanged = false; } - if (op.newSelectionNodes) - showSelection(cm, op.newSelectionNodes); + if (op.preparedSelection) + cm.display.input.showSelection(op.preparedSelection); if (op.updatedDisplay) setDocumentHeight(cm, op.barMeasure); if (op.updatedDisplay || op.startHeight != cm.doc.height) @@ -2204,7 +2585,7 @@ if (op.selectionChanged) restartBlink(cm); if (cm.state.focused && op.updateInput) - resetInput(cm, op.typing); + cm.display.input.reset(op.typing); } function endOperation_finish(op) { @@ -2476,169 +2857,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)) { - try { cm.display.input.focus(); } - catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM - } - } - - 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 @@ -2657,10 +2875,6 @@ })); 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. @@ -2683,16 +2897,6 @@ // 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); } @@ -2703,65 +2907,11 @@ on(d.scroller, "drop", operation(cm, onDrop)); } on(d.scroller, "paste", function(e) { + // FIXME adjust for configurable input model 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); - }); - - 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); - - // Needed to handle Tab key in KHTML - if (khtml) on(d.sizer, "mouseup", function() { - if (activeElt() == d.input) d.input.blur(); - focusInput(cm); + d.input.focus(); + d.input.fastPoll(); }); } @@ -2841,7 +2991,7 @@ 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: @@ -2887,10 +3037,10 @@ e_preventDefault(e2); if (!modifier) extendSelection(cm.doc, start); - focusInput(cm); + display.input.focus(); // Work around unexplainable focus problem in IE9 (#2127) if (ie && ie_version == 9) - setTimeout(function() {document.body.focus(); focusInput(cm);}, 20); + setTimeout(function() {document.body.focus(); display.input.focus();}, 20); } }); // Let the drag handler handle this. @@ -3028,7 +3178,7 @@ function done(e) { counter = Infinity; e_preventDefault(e); - focusInput(cm); + display.input.focus(); off(document, "mousemove", move); off(document, "mouseup", up); doc.history.lastSelOrigin = null; @@ -3107,7 +3257,7 @@ 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 { @@ -3119,7 +3269,7 @@ 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){} @@ -3286,7 +3436,7 @@ } // 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; @@ -3316,7 +3466,7 @@ stopSeq.set(50, function() { if (cm.state.keySeq == seq) { cm.state.keySeq = null; - resetInput(cm); + cm.display.input.reset(); } }); name = seq + " " + name; @@ -3412,11 +3562,10 @@ if (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 @@ -3427,15 +3576,15 @@ 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 } } - slowPoll(cm); + cm.display.input.slowPoll(); restartBlink(cm); } function onBlur(cm) { @@ -3454,80 +3603,8 @@ // textarea (making it as unobtrusive as possible) to let the // right-click take effect on it. function onContextMenu(cm, e) { - 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); - } + if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) return; + cm.display.input.onContextMenu(e); } function contextMenuInGutter(cm, e) { @@ -4156,7 +4233,7 @@ CodeMirror.prototype = { constructor: CodeMirror, - focus: function(){window.focus(); focusInput(this); fastPoll(this);}, + focus: function(){window.focus(); this.display.input.focus(); this.display.input.fastPoll();}, setOption: function(option, value) { var options = this.options, old = options[option]; @@ -4504,7 +4581,7 @@ signal(this, "overwriteToggle", this, this.state.overwrite); }, - hasFocus: function() { return activeElt() == this.display.input; }, + hasFocus: function() { return this.display.input.hasFocus(); }, scrollTo: methodOp(function(x, y) { if (x != null || y != null) resolveScrollToPos(this); @@ -4580,14 +4657,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;} @@ -4634,6 +4711,9 @@ }, true); 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); @@ -4682,10 +4762,10 @@ cm.display.disabled = true; } else { cm.display.disabled = false; - if (!val) resetInput(cm); + if (!val) cm.display.input.reset(); } }); - option("disableInput", false, function(cm, val) {if (!val) resetInput(cm);}, true); + option("disableInput", false, function(cm, val) {if (!val) cm.display.input.reset();}, true); option("dragDrop", true); option("cursorBlinkRate", 530); @@ -4702,11 +4782,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.setTabIndex(val || ""); }); option("autofocus", null); @@ -7509,14 +7589,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; From f2b49d192b1149f504200ecd5e50ef3e0c774b64 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Thu, 9 Oct 2014 17:20:49 +0200 Subject: [PATCH 02/47] Wire up some actual logic to ContentEditableInput Simple things work. --- lib/codemirror.css | 12 ++- lib/codemirror.js | 307 ++++++++++++++++++++++++++++++++++------------------- 2 files changed, 208 insertions(+), 111 deletions(-) diff --git a/lib/codemirror.css b/lib/codemirror.css index c56510e99a..0b53af6576 100644 --- a/lib/codemirror.css +++ b/lib/codemirror.css @@ -4,6 +4,7 @@ /* Set height, width, borders, and global font properties here */ font-family: monospace; height: 300px; + color: black; } /* PADDING */ @@ -139,11 +140,9 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} the editor. You probably shouldn't touch them. */ .CodeMirror { - line-height: 1; position: relative; overflow: hidden; background: white; - color: black; } .CodeMirror-scroll { @@ -215,6 +214,11 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} cursor: default; z-index: 4; } +.CodeMirror-gutter-wrapper { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} .CodeMirror-lines { cursor: text; @@ -256,6 +260,10 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} .CodeMirror-widget {} +.CodeMirror-code { + outline: none; +} + .CodeMirror-measure { position: absolute; width: 100%; diff --git a/lib/codemirror.js b/lib/codemirror.js index 0e680db3ff..3c089d72e9 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -23,7 +23,6 @@ // detected are enabled based on userAgent etc sniffing. 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 ie = ie_upto10 || ie_11up; @@ -221,7 +220,7 @@ // was opened. d.selForContextMenu = null; - input.init(this); + input.init(d); } // STATE UPDATES @@ -879,7 +878,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; } @@ -957,11 +956,11 @@ 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; width: " + 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"])) @@ -979,14 +978,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 @@ -998,25 +997,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(widget); if (allowAbove && widget.above) wrap.insertBefore(node, lineView.gutter || lineView.text); else @@ -1074,24 +1074,57 @@ // was made out of. var lastCopied = null; - var emptyInput = { - init: nothing, - drawSelection: nothing, - showSelection: nothing, - reset: nothing, - getField: nothing, - focus: nothing, - hasFocus: nothing, - blur: nothing, - setTabIndex: nothing, - resetPosition: nothing, - slowPoll: nothing, - fastPoll: nothing, - poll: nothing, - ensurePolled: nothing, - onKeyPress: nothing, - onContextMenu: nothing - }; + function applyTextInput(cm, inserted, deleted) { + var doc = cm.doc; + cm.display.shift = false; + + var textLines = splitLines(inserted), multiPaste = null; + // When pasing N lines into N selections, insert one line per selection + 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(); + if (range.empty()) { + if (deleted && deleted > 0) // Handle deletion + from = Pos(from.line, from.ch - deleted); + else if (cm.state.overwrite && !cm.state.pasteIncoming) // Handle overwrite + 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; + cm.state.pasteIncoming = cm.state.cutIncoming = false; + } // TEXTAREA INPUT STYLE @@ -1113,7 +1146,7 @@ this.hasSelection = false; }; - TextareaInput.prototype = createObj(emptyInput, { + TextareaInput.prototype = copyObj({ init: function(display) { var input = this, cm = this.cm; // The semihidden textarea that is focused when the editor is @@ -1202,7 +1235,7 @@ }); }, - drawSelection: function() { + prepareSelection: function() { // Redraw the selection and/or cursor var cm = this.cm, display = cm.display, doc = cm.doc, result = {}; var curFragment = result.cursors = document.createDocumentFragment(); @@ -1280,10 +1313,6 @@ return activeElt() == this.textarea; }, - setTabIndex: function(index) { - this.textarea.tabIndex = index; - }, - resetPosition: function() { this.wrapper.style.top = this.wrapper.style.left = 0; }, @@ -1320,7 +1349,7 @@ // 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, doc = cm.doc; + 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, @@ -1345,68 +1374,17 @@ 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) + if (text.charCodeAt(0) == 0x200b && cm.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; + operation(cm, applyTextInput)(cm, text.slice(same), prevInput.length - same); // Don't leave long text in the textarea, since it makes further polling slow if (text.length > 1000 || text.indexOf("\n") > -1) input.value = this.prevInput = ""; else this.prevInput = text; - if (withOp) endOperation(cm); - cm.state.pasteIncoming = cm.state.cutIncoming = false; return true; }, @@ -1491,14 +1469,119 @@ } else { setTimeout(rehide, 50); } - } - }); + }, + + setUneditable: nothing + }, TextareaInput.prototype); // CONTENTEDITABLE INPUT STYLE - function ContentEditableInput() {} + function ContentEditableInput(cm) { + this.cm = cm; + // FIXME eventually poll for selection changes + this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null; + } + + ContentEditableInput.prototype = copyObj({ + init: function(display) { + var input = this, cm = input.cm; + var div = input.div = display.lineDiv; + try { div.contentEditable = "plaintext-only"; } + catch(e) {} // It's nice if this works, since it guards against some weird editing possiblities, but not essential + if (div.contentEditable != "plaintext-only") div.contentEditable = "true"; + + // FIXME make configurable + div.setAttribute("autocorrect", "off"); div.setAttribute("autocapitalize", "off"); div.setAttribute("spellcheck", "false"); + + // FIXME maybe do centrally, on input.getField() + on(div, "keyup", function(e) { onKeyUp.call(cm, e); }); + on(div, "keydown", operation(cm, onKeyDown)); + on(div, "keypress", operation(cm, onKeyPress)); + on(div, "focus", bind(onFocus, cm)); + on(div, "blur", bind(onBlur, cm)); + + on(div, "input", function() { input.fastPoll(); }); + on(div, "selectstart", function() { input.fastPoll(); }); + on(div, "textInput", function(e) { + if (eventInWidget(cm.display, e)) return; + e.preventDefault(); + operation(cm, applyTextInput)(cm, e.data, 0); + }); + + // FIXME cut/copy/paste handlers + }, + prepareSelection: function(force) { + if (force || this.cm.hasFocus()) { + var sel = this.cm.doc.sel.primary(); + return {start: nodeAndOffsetForPos(this.cm, sel.from()), + end: nodeAndOffsetForPos(this.cm, sel.to())}; + } + }, + showSelection: function(nodes) { + var sel = window.getSelection(); + if (nodes) { + var old = sel.rangeCount && sel.getRangeAt(0); + try { var rng = range(nodes.start.node, nodes.start.offset, nodes.end.offset, nodes.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) { + sel.removeAllRanges(); + sel.addRange(rng); + if (sel.anchorNode == null) sel.addRange(old); + } + } + this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset; + this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset; + }, + focus: function() { + this.showSelection(this.prepareSelection(true)); + this.div.focus(); + }, + hasFocus: function() { + var sel = window.getSelection(); + if (!sel.rangeCount) return false; + var node = sel.getRangeAt(0).commonAncestorContainer; + return node == this.div || contains(this.div, node); + }, + blur: function() { + if (this.hasFocus()) + window.getSelection().removeAllRanges(); + this.div.blur(); + }, + getField: function() { + return this.div; + }, + reset: nothing, + resetPosition: nothing, + slowPoll: function() { + // FIXME + }, + fastPoll: function() { this.slowPoll(); }, // FIXME + poll: function() { // FIXME (poll just for selection?) + }, + + ensurePolled: nothing, + onContextMenu: nothing, + + setUneditable: function(node) { + node.setAttribute("contenteditable", "false"); + }, + + onKeyPress: function(e) { + e.preventDefault(); + operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); + } + }, ContentEditableInput.prototype); - ContentEditableInput.prototype = createObj(emptyInput, {}); + // FIXME assumes pos is in viewport + function nodeAndOffsetForPos(cm, pos) { + var view = findViewForLine(cm, pos.line); + var line = getLine(cm.doc, pos.line); + var info = mapFromLineView(view, line, pos.line); + // FIXME bidi + var result = nodeAndOffsetInLineMap(info.map, pos.ch, "left"); + result.offset = result.collapse == "right" ? result.end : result.start; + return result; + } CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; @@ -1790,7 +1873,7 @@ // SELECTION DRAWING function updateSelection(cm) { - cm.display.input.showSelection(cm.display.input.drawSelection()); + cm.display.input.showSelection(cm.display.input.prepareSelection()); } // Draws a cursor for the given range @@ -2115,9 +2198,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. @@ -2151,13 +2232,19 @@ break; } } + return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd}; + } + + 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(); @@ -2562,7 +2649,7 @@ } if (op.updatedDisplay || op.selectionChanged) - op.preparedSelection = display.input.drawSelection(); + op.preparedSelection = display.input.prepareSelection(); } function endOperation_W2(op) { @@ -3559,7 +3646,7 @@ 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)) && handleKeyBinding(cm, e)) return; @@ -4455,6 +4542,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; @@ -4786,7 +4874,7 @@ }); option("tabindex", null, function(cm, val) { - cm.display.input.setTabIndex(val || ""); + cm.display.input.getField().tabindex = val || ""; }); option("autofocus", null); @@ -6372,6 +6460,7 @@ var widget = !ignoreWidget && marker.widgetNode; if (widget) { builder.map.push(builder.pos, builder.pos + size, widget); + builder.cm.display.input.setUneditable(widget); builder.content.appendChild(widget); } builder.pos += size; @@ -7652,9 +7741,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; }; From a1664f64e7ff30846a992e293b1d2b5bda6ca849 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Fri, 10 Oct 2014 11:50:18 +0200 Subject: [PATCH 03/47] Suppress mouse event handling during touch --- lib/codemirror.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/codemirror.js b/lib/codemirror.js index 3c089d72e9..6929f2f4a9 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -220,6 +220,8 @@ // was opened. d.selForContextMenu = null; + d.touchActive = false; + input.init(d); } @@ -2967,6 +2969,16 @@ // 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; + on(d.scroller, "touchstart", function() { + clearTimeout(touchFinished); + d.touchActive = true; + }); + on(d.scroller, "touchend", function() { + touchFinished = setTimeout(function() {d.touchActive = false;}, 1000); + }); + // Sync scrolling between fake scrollbars and real scrollable // area, ensure viewport is updated when scrolling. on(d.scroller, "scroll", function() { @@ -3051,8 +3063,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 (display.touchActive || signalDOMEvent(cm, e)) return; display.shift = e.shiftKey; if (eventInWidget(display, e)) { From 8bec859269c22b31ff6e13e20b17b1b357eb9f65 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Fri, 10 Oct 2014 15:18:55 +0200 Subject: [PATCH 04/47] Support clipboard events, fix some selection issues --- lib/codemirror.js | 156 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 118 insertions(+), 38 deletions(-) diff --git a/lib/codemirror.js b/lib/codemirror.js index 6929f2f4a9..c852cb9107 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -103,7 +103,7 @@ this.curOp.forceUpdate = true; attachDoc(this, doc); - if ((options.autofocus && !mobile) || display.input.hasFocus()) + if ((options.autofocus && !mobile) || cm.hasFocus()) setTimeout(bind(onFocus, this), 20); else onBlur(this); @@ -1128,6 +1128,17 @@ cm.state.pasteIncoming = cm.state.cutIncoming = false; } + 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}; + } + // TEXTAREA INPUT STYLE function TextareaInput(cm) { @@ -1210,21 +1221,15 @@ selectInput(te); } } 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)); - } + var ranges = copyableRanges(cm); + lastCopied = ranges.text; if (e.type == "cut") { - cm.setSelections(ranges, null, sel_dontScroll); + cm.setSelections(ranges.ranges, null, sel_dontScroll); } else { input.prevInput = ""; - te.value = text.join("\n"); + te.value = ranges.text.join("\n"); selectInput(te); } - lastCopied = text; } if (e.type == "cut") cm.state.cutIncoming = true; } @@ -1311,14 +1316,12 @@ this.textarea.blur(); }, - hasFocus: function() { - return activeElt() == this.textarea; - }, - 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() { @@ -1394,6 +1397,9 @@ if (this.pollingFast && this.poll()) this.pollingFast = false; }, + rememberSelection: nothing, + pollSelection: nothing, + onKeyPress: function() { if (ie && ie_version >= 9) this.hasSelection = null; this.fastPoll(); @@ -1445,7 +1451,6 @@ input.wrapper.style.position = "relative"; te.style.cssText = oldCSS; if (ie && ie_version < 9) display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos); - display.input.slowPoll(); // Try to detect the user choosing select-all if (te.selectionStart != null) { @@ -1501,27 +1506,55 @@ on(div, "keypress", operation(cm, onKeyPress)); on(div, "focus", bind(onFocus, cm)); on(div, "blur", bind(onBlur, cm)); + on(div, "focus", bind(onFocus, cm)); + on(div, "blur", bind(onBlur, cm)); - on(div, "input", function() { input.fastPoll(); }); - on(div, "selectstart", function() { input.fastPoll(); }); on(div, "textInput", function(e) { if (eventInWidget(cm.display, e)) return; e.preventDefault(); operation(cm, applyTextInput)(cm, e.data, 0); }); - // FIXME cut/copy/paste handlers + // FIXME include per-line selection magic + on(div, "paste", function(e) { + if (e.clipboardData) { + e.preventDefault(); + cm.replaceSelection(e.clipboardData.getData("text/plain"), null, "paste"); + } + }); + function onCopyCut(e) { + if (e.clipboardData) { + e.preventDefault(); + if (cm.somethingSelected()) { + lastCopied = cm.getSelections(); + if (e.type == "cut") cm.replaceSelection("", null, "cut"); + } else { + var ranges = copyableRanges(cm); + lastCopied = ranges.text; + if (e.type == "cut") { + cm.operation(function() { + cm.setSelections(ranges.ranges, 0, sel_dontScroll); + cm.replaceSelection("", null, "cut"); + }); + } + } + e.clipboardData.clearData(); + e.clipboardData.setData("text/plain", lastCopied.join("\n")); + } + } + on(div, "copy", onCopyCut); + on(div, "cut", onCopyCut); }, prepareSelection: function(force) { - if (force || this.cm.hasFocus()) { + if (force || this.cm.state.focused) { var sel = this.cm.doc.sel.primary(); - return {start: nodeAndOffsetForPos(this.cm, sel.from()), - end: nodeAndOffsetForPos(this.cm, sel.to())}; + return {start: posNodeOffset(this.cm, sel.from()), + end: posNodeOffset(this.cm, sel.to())}; } }, showSelection: function(nodes) { - var sel = window.getSelection(); if (nodes) { + var sel = window.getSelection(); var old = sel.rangeCount && sel.getRangeAt(0); try { var rng = range(nodes.start.node, nodes.start.offset, nodes.end.offset, nodes.end.node); } catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible @@ -1530,35 +1563,50 @@ sel.addRange(rng); if (sel.anchorNode == null) sel.addRange(old); } + this.rememberSelection(); } + }, + rememberSelection: function() { + var sel = window.getSelection(); this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset; this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset; }, focus: function() { - this.showSelection(this.prepareSelection(true)); this.div.focus(); }, - hasFocus: function() { + selectionInEditor: function() { var sel = window.getSelection(); if (!sel.rangeCount) return false; var node = sel.getRangeAt(0).commonAncestorContainer; return node == this.div || contains(this.div, node); }, blur: function() { - if (this.hasFocus()) - window.getSelection().removeAllRanges(); this.div.blur(); }, getField: function() { return this.div; }, + reset: nothing, resetPosition: nothing, - slowPoll: function() { - // FIXME + + receivedFocus: function() { + if (!this.selectionInEditor()) + this.showSelection(this.prepareSelection(true)); }, - fastPoll: function() { this.slowPoll(); }, // FIXME - poll: function() { // FIXME (poll just for selection?) + + pollSelection: function() { + var sel = window.getSelection(), an = sel.anchorNode, ao = sel.anchorOffset, fn = sel.focusNode, fo = sel.focusOffset; + if (sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || + sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset) { + this.rememberSelection(); + window.ii = (window.ii || 0) + 1; + if (window.ii > 20) return; + var anchor = nodeOffsetPos(this.cm, sel.anchorNode, sel.anchorOffset); + var head = nodeOffsetPos(this.cm, sel.focusNode, sel.focusOffset); + if (anchor && head) + operation(this.cm, setSelection)(this.cm.doc, simpleSelection(anchor, head), sel_dontScroll); + } }, ensurePolled: nothing, @@ -1575,7 +1623,7 @@ }, ContentEditableInput.prototype); // FIXME assumes pos is in viewport - function nodeAndOffsetForPos(cm, pos) { + function posNodeOffset(cm, pos) { var view = findViewForLine(cm, pos.line); var line = getLine(cm.doc, pos.line); var info = mapFromLineView(view, line, pos.line); @@ -1585,6 +1633,35 @@ return result; } + function nodeOffsetPos(cm, node, offset) { + for (var 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) { + if (!contains(lineView.text, node)) return Pos(lineNo(lineView.line), 0); + + var textNode = node.nodeType == 3 ? node : null, topNode = node; + while (topNode.parentNode != lineView.text.firstChild) topNode = topNode.parentNode; + var measure = lineView.measure, maps = measure.maps; + 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) { + if (map[j + 2] == textNode || map[j + 2] == topNode) { + var line = lineNo(i < 0 ? lineView.line : lineView.other[i]); + return Pos(line, map[j] + (map[j + 2] == textNode ? offset : 0)); + } + } + } + } + CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; // SELECTION / CURSOR @@ -2972,10 +3049,12 @@ // Used to suppress mouse event handling when a touch happens var touchFinished; on(d.scroller, "touchstart", function() { + d.input.rememberSelection(); clearTimeout(touchFinished); d.touchActive = true; }); on(d.scroller, "touchend", function() { + d.input.pollSelection(); touchFinished = setTimeout(function() {d.touchActive = false;}, 1000); }); @@ -3010,7 +3089,6 @@ if (eventInWidget(d, e)) return; cm.state.pasteIncoming = true; d.input.focus(); - d.input.fastPoll(); }); } @@ -3683,7 +3761,7 @@ if (webkit) setTimeout(function() { cm.display.input.reset(true); }, 20); // Issue #1730 } } - cm.display.input.slowPoll(); + cm.display.input.receivedFocus(); restartBlink(cm); } function onBlur(cm) { @@ -4332,7 +4410,7 @@ CodeMirror.prototype = { constructor: CodeMirror, - focus: function(){window.focus(); this.display.input.focus(); this.display.input.fastPoll();}, + focus: function(){window.focus(); this.display.input.focus();}, setOption: function(option, value) { var options = this.options, old = options[option]; @@ -4681,7 +4759,7 @@ signal(this, "overwriteToggle", this, this.state.overwrite); }, - hasFocus: function() { return this.display.input.hasFocus(); }, + hasFocus: function() { return this.display.input.getField() == activeElt(); }, scrollTo: methodOp(function(x, y) { if (x != null || y != null) resolveScrollToPos(this); @@ -7780,12 +7858,14 @@ } 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; } From 3f45d4e54c7d055681845fca39a91db973614a0c Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Fri, 10 Oct 2014 15:44:18 +0200 Subject: [PATCH 05/47] Clean up some FIXMEs --- lib/codemirror.js | 53 ++++++++++++++++++++++++----------------------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/lib/codemirror.js b/lib/codemirror.js index c852cb9107..22e44344da 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -1139,6 +1139,12 @@ 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) { @@ -1173,7 +1179,7 @@ else te.setAttribute("wrap", "off"); // If border: 0; -- iOS fails to open keyboard (issue #1287) if (ios) te.style.border = "1px solid black"; - te.setAttribute("autocorrect", "off"); te.setAttribute("autocapitalize", "off"); te.setAttribute("spellcheck", "false"); + disableBrowserMagic(te); // Wraps and hides input textarea var div = this.wrapper = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); @@ -1182,15 +1188,10 @@ // 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, "keyup", function(e) { onKeyUp.call(cm, e); }); on(te, "input", function() { if (ie && ie_version >= 9 && input.hasSelection) input.hasSelection = null; input.poll(); }); - on(te, "keydown", operation(cm, onKeyDown)); - on(te, "keypress", operation(cm, onKeyPress)); - on(te, "focus", bind(onFocus, cm)); - on(te, "blur", bind(onBlur, cm)); on(te, "paste", function() { // Workaround for webkit bug https://bugs.webkit.org/show_bug.cgi?id=90206 @@ -1236,6 +1237,12 @@ on(te, "cut", prepareCopyCut); on(te, "copy", prepareCopyCut); + on(display.scroller, "paste", function(e) { + if (eventInWidget(display, 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); @@ -1485,7 +1492,6 @@ function ContentEditableInput(cm) { this.cm = cm; - // FIXME eventually poll for selection changes this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null; } @@ -1496,18 +1502,7 @@ try { div.contentEditable = "plaintext-only"; } catch(e) {} // It's nice if this works, since it guards against some weird editing possiblities, but not essential if (div.contentEditable != "plaintext-only") div.contentEditable = "true"; - - // FIXME make configurable - div.setAttribute("autocorrect", "off"); div.setAttribute("autocapitalize", "off"); div.setAttribute("spellcheck", "false"); - - // FIXME maybe do centrally, on input.getField() - on(div, "keyup", function(e) { onKeyUp.call(cm, e); }); - on(div, "keydown", operation(cm, onKeyDown)); - on(div, "keypress", operation(cm, onKeyPress)); - on(div, "focus", bind(onFocus, cm)); - on(div, "blur", bind(onBlur, cm)); - on(div, "focus", bind(onFocus, cm)); - on(div, "blur", bind(onBlur, cm)); + disableBrowserMagic(div); on(div, "textInput", function(e) { if (eventInWidget(cm.display, e)) return; @@ -1515,7 +1510,6 @@ operation(cm, applyTextInput)(cm, e.data, 0); }); - // FIXME include per-line selection magic on(div, "paste", function(e) { if (e.clipboardData) { e.preventDefault(); @@ -3084,12 +3078,13 @@ on(d.scroller, "dragover", drag_); on(d.scroller, "drop", operation(cm, onDrop)); } - on(d.scroller, "paste", function(e) { - // FIXME adjust for configurable input model - if (eventInWidget(d, e)) return; - cm.state.pasteIncoming = true; - d.input.focus(); - }); + + 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)); } // Called when the window resizes @@ -4964,7 +4959,7 @@ }); option("tabindex", null, function(cm, val) { - cm.display.input.getField().tabindex = val || ""; + cm.display.input.getField().tabIndex = val || ""; }); option("autofocus", null); @@ -5397,8 +5392,8 @@ CodeMirror.fromTextArea = function(textarea, 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 From 594a301d5e3f5fa71627d9283b4e00d96cd16b93 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Fri, 10 Oct 2014 16:04:27 +0200 Subject: [PATCH 06/47] Make selection drawing work when the viewport doesn't cover the selection --- lib/codemirror.js | 47 ++++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/lib/codemirror.js b/lib/codemirror.js index 22e44344da..4b022390b8 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -1308,9 +1308,7 @@ this.inaccurateSelection = minimal; }, - getField: function() { - return this.textarea; - }, + getField: function() { return this.textarea; }, focus: function() { if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { @@ -1319,9 +1317,7 @@ } }, - blur: function() { - this.textarea.blur(); - }, + blur: function() { this.textarea.blur(); }, resetPosition: function() { this.wrapper.style.top = this.wrapper.style.left = 0; @@ -1539,18 +1535,31 @@ on(div, "copy", onCopyCut); on(div, "cut", onCopyCut); }, + prepareSelection: function(force) { if (force || this.cm.state.focused) { var sel = this.cm.doc.sel.primary(); - return {start: posNodeOffset(this.cm, sel.from()), - end: posNodeOffset(this.cm, sel.to())}; + var start = posNodeOffset(this.cm, sel.from()); + var end = posNodeOffset(this.cm, sel.to()); + return (start || end) && {start: start, end: end}; } }, + + // FIXME draw multiple selections showSelection: function(nodes) { - if (nodes) { + var view = this.cm.display.view; + if (nodes && view.length) { var sel = window.getSelection(); var old = sel.rangeCount && sel.getRangeAt(0); - try { var rng = range(nodes.start.node, nodes.start.offset, nodes.end.offset, nodes.end.node); } + var start = nodes.start, end = nodes.end; + 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; + last = {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) { sel.removeAllRanges(); @@ -1560,26 +1569,25 @@ this.rememberSelection(); } }, + rememberSelection: function() { var sel = window.getSelection(); this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset; this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset; }, - focus: function() { - this.div.focus(); - }, + selectionInEditor: function() { var sel = window.getSelection(); if (!sel.rangeCount) return false; var node = sel.getRangeAt(0).commonAncestorContainer; return node == this.div || contains(this.div, node); }, - blur: function() { - this.div.blur(); - }, - getField: function() { - return this.div; + + focus: function() { + if (this.cm.options.readOnly != "nocursor") this.div.focus(); }, + blur: function() { this.div.blur(); }, + getField: function() { return this.div; }, reset: nothing, resetPosition: nothing, @@ -1610,15 +1618,16 @@ node.setAttribute("contenteditable", "false"); }, + // FIXME handle IME onKeyPress: function(e) { e.preventDefault(); operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); } }, ContentEditableInput.prototype); - // FIXME assumes pos is in viewport function posNodeOffset(cm, pos) { var view = findViewForLine(cm, pos.line); + if (!view) return null; var line = getLine(cm.doc, pos.line); var info = mapFromLineView(view, line, pos.line); // FIXME bidi From af059b4dcac2c7d9d9562f15d43d68304b54d6d2 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Mon, 13 Oct 2014 11:50:39 +0200 Subject: [PATCH 07/47] Further tweaks to selection polling Still doesn't work properly --- lib/codemirror.js | 121 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 71 insertions(+), 50 deletions(-) diff --git a/lib/codemirror.js b/lib/codemirror.js index 4b022390b8..a95613206a 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -170,7 +170,7 @@ // 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; } - if (!webkit) d.scroller.draggable = true; + if (!webkit && !(gecko && mobile)) d.scroller.draggable = true; if (place) { if (place.appendChild) place.appendChild(d.wrapper); @@ -1400,9 +1400,6 @@ if (this.pollingFast && this.poll()) this.pollingFast = false; }, - rememberSelection: nothing, - pollSelection: nothing, - onKeyPress: function() { if (ie && ie_version >= 9) this.hasSelection = null; this.fastPoll(); @@ -1489,6 +1486,7 @@ function ContentEditableInput(cm) { this.cm = cm; this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null; + this.polling = new Delayed(); } ContentEditableInput.prototype = copyObj({ @@ -1507,11 +1505,13 @@ }); on(div, "paste", function(e) { - if (e.clipboardData) { + var pasted = e.clipboardData && e.clipboardData.getData("text/plain"); + if (pasted) { e.preventDefault(); - cm.replaceSelection(e.clipboardData.getData("text/plain"), null, "paste"); + cm.replaceSelection(pasted, null, "paste"); } }); + function onCopyCut(e) { if (e.clipboardData) { e.preventDefault(); @@ -1536,38 +1536,43 @@ on(div, "cut", onCopyCut); }, - prepareSelection: function(force) { - if (force || this.cm.state.focused) { - var sel = this.cm.doc.sel.primary(); - var start = posNodeOffset(this.cm, sel.from()); - var end = posNodeOffset(this.cm, sel.to()); - return (start || end) && {start: start, end: end}; - } - }, + prepareSelection: function() { return true; }, // FIXME draw multiple selections - showSelection: function(nodes) { + showSelection: function(force) { + if (!this.cm.state.focused && !force || !this.cm.display.view.length) return; + + var sel = window.getSelection(), prim = this.cm.doc.sel.primary(); + var curAnchor = nodeOffsetPos(this.cm, sel.anchorNode, sel.anchorOffset); + var curFocus = nodeOffsetPos(this.cm, sel.focusNode, sel.focusOffset); + if (curAnchor && curFocus && + cmp(minPos(curAnchor, curFocus), prim.from()) == 0 && + cmp(maxPos(curAnchor, curFocus), prim.to()) == 0) + return; + + var start = posNodeOffset(this.cm, prim.from()); + var end = posNodeOffset(this.cm, prim.to()); + if (!start && !end) return; + var view = this.cm.display.view; - if (nodes && view.length) { - var sel = window.getSelection(); - var old = sel.rangeCount && sel.getRangeAt(0); - var start = nodes.start, end = nodes.end; - 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; - last = {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) { - sel.removeAllRanges(); - sel.addRange(rng); - if (sel.anchorNode == null) sel.addRange(old); - } - this.rememberSelection(); - } + 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; + last = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]}; + } + + console.log("selection redraw"); + 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) { + sel.removeAllRanges(); + sel.addRange(rng); + if (sel.anchorNode == null) sel.addRange(old); + } + this.rememberSelection(); }, rememberSelection: function() { @@ -1580,7 +1585,7 @@ var sel = window.getSelection(); if (!sel.rangeCount) return false; var node = sel.getRangeAt(0).commonAncestorContainer; - return node == this.div || contains(this.div, node); + return contains(this.div, node); }, focus: function() { @@ -1593,19 +1598,26 @@ resetPosition: nothing, receivedFocus: function() { - if (!this.selectionInEditor()) - this.showSelection(this.prepareSelection(true)); + this.showSelection(true); + + var input = this; + 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); }, pollSelection: function() { - var sel = window.getSelection(), an = sel.anchorNode, ao = sel.anchorOffset, fn = sel.focusNode, fo = sel.focusOffset; + var sel = window.getSelection(); if (sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset) { this.rememberSelection(); - window.ii = (window.ii || 0) + 1; - if (window.ii > 20) return; var anchor = nodeOffsetPos(this.cm, sel.anchorNode, sel.anchorOffset); var head = nodeOffsetPos(this.cm, sel.focusNode, sel.focusOffset); + console.log("selection noticed", anchor, head); if (anchor && head) operation(this.cm, setSelection)(this.cm.doc, simpleSelection(anchor, head), sel_dontScroll); } @@ -1637,8 +1649,10 @@ } function nodeOffsetPos(cm, node, offset) { + if (node == cm.display.lineDiv) + return Pos(offset + cm.display.viewOffset, 0); // FIXME collapsed lines, etc for (var lineNode = node;; lineNode = lineNode.parentNode) { - if (!lineNode || lineNode == cm.display.lineDiv) return null; + if (!lineNode || lineNode == cm.display.lineDiv) return console.log("OUT"); if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) break; } for (var i = 0; i < cm.display.view.length; i++) { @@ -1649,10 +1663,16 @@ } function locateNodeInLineView(lineView, node, offset) { - if (!contains(lineView.text, node)) return Pos(lineNo(lineView.line), 0); + var wrapper = lineView.text.firstChild; + if (!wrapper) console.log("wrapper is null", lineView.text.outerHTML, lineView.measure && lineView.measure.map); + if (!contains(wrapper, node)) return Pos(lineNo(lineView.line), 0); + if (node == wrapper) { + node = wrapper.childNodes[offset]; + offset = 0; + } var textNode = node.nodeType == 3 ? node : null, topNode = node; - while (topNode.parentNode != lineView.text.firstChild) topNode = topNode.parentNode; + while (topNode.parentNode != wrapper) topNode = topNode.parentNode; var measure = lineView.measure, maps = measure.maps; for (var i = -1; i < (maps ? maps.length : 0); i++) { var map = i < 0 ? measure.map : maps[i]; @@ -1663,6 +1683,7 @@ } } } + console.log("FELL", textNode && textNode.nodeValue, topNode, measure.map.indexOf(textNode), lineView.changes); } CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; @@ -3051,15 +3072,15 @@ // Used to suppress mouse event handling when a touch happens var touchFinished; + function finishTouch(e) { + touchFinished = setTimeout(function() {d.touchActive = false;}, 1000); + }; on(d.scroller, "touchstart", function() { - d.input.rememberSelection(); clearTimeout(touchFinished); d.touchActive = true; }); - on(d.scroller, "touchend", function() { - d.input.pollSelection(); - touchFinished = setTimeout(function() {d.touchActive = false;}, 1000); - }); + on(d.scroller, "touchend", finishTouch); + on(d.scroller, "touchcancel", finishTouch); // Sync scrolling between fake scrollbars and real scrollable // area, ensure viewport is updated when scrolling. @@ -3764,8 +3785,8 @@ cm.display.input.reset(); if (webkit) setTimeout(function() { cm.display.input.reset(true); }, 20); // Issue #1730 } + cm.display.input.receivedFocus(); } - cm.display.input.receivedFocus(); restartBlink(cm); } function onBlur(cm) { From 68b04f070efaead35a742caa40f5fe49d2964a4c Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Tue, 14 Oct 2014 13:15:13 +0200 Subject: [PATCH 08/47] Add scanning of surrounding dom nodes when looking for a selection node --- lib/codemirror.js | 69 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 21 deletions(-) diff --git a/lib/codemirror.js b/lib/codemirror.js index a95613206a..bed3c46b4e 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -1543,15 +1543,15 @@ if (!this.cm.state.focused && !force || !this.cm.display.view.length) return; var sel = window.getSelection(), prim = this.cm.doc.sel.primary(); - var curAnchor = nodeOffsetPos(this.cm, sel.anchorNode, sel.anchorOffset); - var curFocus = nodeOffsetPos(this.cm, sel.focusNode, sel.focusOffset); + var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset); + var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset); if (curAnchor && curFocus && cmp(minPos(curAnchor, curFocus), prim.from()) == 0 && cmp(maxPos(curAnchor, curFocus), prim.to()) == 0) return; - var start = posNodeOffset(this.cm, prim.from()); - var end = posNodeOffset(this.cm, prim.to()); + var start = posToDOM(this.cm, prim.from()); + var end = posToDOM(this.cm, prim.to()); if (!start && !end) return; var view = this.cm.display.view; @@ -1615,8 +1615,8 @@ if (sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset) { this.rememberSelection(); - var anchor = nodeOffsetPos(this.cm, sel.anchorNode, sel.anchorOffset); - var head = nodeOffsetPos(this.cm, sel.focusNode, sel.focusOffset); + var anchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset); + var head = domToPos(this.cm, sel.focusNode, sel.focusOffset); console.log("selection noticed", anchor, head); if (anchor && head) operation(this.cm, setSelection)(this.cm.doc, simpleSelection(anchor, head), sel_dontScroll); @@ -1637,7 +1637,7 @@ } }, ContentEditableInput.prototype); - function posNodeOffset(cm, pos) { + function posToDOM(cm, pos) { var view = findViewForLine(cm, pos.line); if (!view) return null; var line = getLine(cm.doc, pos.line); @@ -1648,12 +1648,17 @@ return result; } - function nodeOffsetPos(cm, node, offset) { - if (node == cm.display.lineDiv) - return Pos(offset + cm.display.viewOffset, 0); // FIXME collapsed lines, etc - for (var lineNode = node;; lineNode = lineNode.parentNode) { - if (!lineNode || lineNode == cm.display.lineDiv) return console.log("OUT"); - if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) break; + function domToPos(cm, node, offset) { + var lineNode; + if (node == cm.display.lineDiv) { + lineNode = cm.display.lineDiv.childNodes[offset]; + if (!lineNode) return cm.clipPos(Pos(cm.display.viewTo - 1)); + node = null; offset = 0; + } else { + for (lineNode = node;; lineNode = lineNode.parentNode) { + if (!lineNode || lineNode == cm.display.lineDiv) return; + 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]; @@ -1665,25 +1670,47 @@ function locateNodeInLineView(lineView, node, offset) { var wrapper = lineView.text.firstChild; if (!wrapper) console.log("wrapper is null", lineView.text.outerHTML, lineView.measure && lineView.measure.map); - if (!contains(wrapper, node)) return Pos(lineNo(lineView.line), 0); + if (!node || !contains(wrapper, node)) return Pos(lineNo(lineView.line), 0); if (node == wrapper) { node = wrapper.childNodes[offset]; offset = 0; + if (!node) return cm.clipPos(Pos(lineNo(lineView.rest ? lst(lineView.rest) : lineView.line))); } var textNode = node.nodeType == 3 ? node : null, topNode = node; while (topNode.parentNode != wrapper) topNode = topNode.parentNode; var measure = lineView.measure, maps = measure.maps; - 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) { - if (map[j + 2] == textNode || map[j + 2] == topNode) { - var line = lineNo(i < 0 ? lineView.line : lineView.other[i]); - return Pos(line, map[j] + (map[j + 2] == textNode ? offset : 0)); + + 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) { + if (map[j + 2] == textNode || map[j + 2] == topNode) { + var line = lineNo(i < 0 ? lineView.line : lineView.other[i]); + return Pos(line, offset < 0 ? map[j + 1] : map[j] + (map[j + 2] == textNode ? offset : 0)); + } } } } - console.log("FELL", textNode && textNode.nodeValue, topNode, measure.map.indexOf(textNode), lineView.changes); + var found = find(textNode, topNode, offset); + if (found) return found; + + // 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 Pos(found.line, found.ch - dist); + 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 Pos(found.line, found.ch + dist); + else + dist += after.textContent.length; + } + console.log("FELL THROUGH", textNode && textNode.nodeValue, topNode); } CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; From 3082017f6cb4d2a511be3abdb96fe6395ab04df1 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Tue, 14 Oct 2014 14:49:23 +0200 Subject: [PATCH 09/47] More selection nonsense --- lib/codemirror.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/codemirror.js b/lib/codemirror.js index bed3c46b4e..12fb076378 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -1536,15 +1536,15 @@ on(div, "cut", onCopyCut); }, - prepareSelection: function() { return true; }, + prepareSelection: function() { return this.cm.state.focused; }, // FIXME draw multiple selections - showSelection: function(force) { - if (!this.cm.state.focused && !force || !this.cm.display.view.length) return; + showSelection: function(doIt) { + if (!doIt || !this.cm.display.view.length) return; 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); + var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset, true); + var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset, true); if (curAnchor && curFocus && cmp(minPos(curAnchor, curFocus), prim.from()) == 0 && cmp(maxPos(curAnchor, curFocus), prim.to()) == 0) @@ -1564,7 +1564,6 @@ last = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]}; } - console.log("selection redraw"); 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) { @@ -1598,7 +1597,10 @@ resetPosition: nothing, receivedFocus: function() { - this.showSelection(true); + if (this.selectionInEditor()) + this.pollSelection(); + else + this.showSelection(true); var input = this; function poll() { @@ -1617,7 +1619,6 @@ this.rememberSelection(); var anchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset); var head = domToPos(this.cm, sel.focusNode, sel.focusOffset); - console.log("selection noticed", anchor, head); if (anchor && head) operation(this.cm, setSelection)(this.cm.doc, simpleSelection(anchor, head), sel_dontScroll); } @@ -1648,9 +1649,10 @@ return result; } - function domToPos(cm, node, offset) { + function domToPos(cm, node, offset, strict) { var lineNode; if (node == cm.display.lineDiv) { + if (strict) return null; lineNode = cm.display.lineDiv.childNodes[offset]; if (!lineNode) return cm.clipPos(Pos(cm.display.viewTo - 1)); node = null; offset = 0; @@ -1669,6 +1671,7 @@ function locateNodeInLineView(lineView, node, offset) { var wrapper = lineView.text.firstChild; + // FIXME debug if (!wrapper) console.log("wrapper is null", lineView.text.outerHTML, lineView.measure && lineView.measure.map); if (!node || !contains(wrapper, node)) return Pos(lineNo(lineView.line), 0); if (node == wrapper) { @@ -1710,7 +1713,6 @@ else dist += after.textContent.length; } - console.log("FELL THROUGH", textNode && textNode.nodeValue, topNode); } CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; From a3af04e3ebc40ec35973a1f84e72e461e00eb2cc Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Tue, 14 Oct 2014 15:06:39 +0200 Subject: [PATCH 10/47] Don't disable mouse events during touch when using textarea input --- lib/codemirror.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/codemirror.js b/lib/codemirror.js index 12fb076378..f4b877b072 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -1310,6 +1310,8 @@ 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(); } @@ -1593,6 +1595,8 @@ blur: function() { this.div.blur(); }, getField: function() { return this.div; }, + supportsTouch: function() { return true; }, + reset: nothing, resetPosition: nothing, @@ -3196,7 +3200,7 @@ // not interfere with, such as a scrollbar or widget. function onMouseDown(e) { var cm = this, display = cm.display; - if (display.touchActive || signalDOMEvent(cm, e)) return; + if (display.touchActive && display.input.supportsTouch() || signalDOMEvent(cm, e)) return; display.shift = e.shiftKey; if (eventInWidget(display, e)) { From 5df0ce750e6d393c41e3e2aafdf8d35502a82e55 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Tue, 14 Oct 2014 15:37:01 +0200 Subject: [PATCH 11/47] Make tests more or less run with contenteditable backend --- lib/codemirror.js | 19 +++++++++++-------- test/test.js | 7 +++++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/codemirror.js b/lib/codemirror.js index f4b877b072..6026d7157f 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -1018,7 +1018,7 @@ 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(widget); + cm.display.input.setUneditable(node); if (allowAbove && widget.above) wrap.insertBefore(node, lineView.gutter || lineView.text); else @@ -1563,7 +1563,7 @@ } 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; - last = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]}; + 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); } @@ -1571,7 +1571,7 @@ if (rng) { sel.removeAllRanges(); sel.addRange(rng); - if (sel.anchorNode == null) sel.addRange(old); + if (old && sel.anchorNode == null) sel.addRange(old); } this.rememberSelection(); }, @@ -1601,12 +1601,12 @@ resetPosition: nothing, receivedFocus: function() { + var input = this; if (this.selectionInEditor()) this.pollSelection(); else - this.showSelection(true); + runInOp(this.cm, function() { input.cm.curOp.selectionChanged = true; }); - var input = this; function poll() { if (input.cm.state.focused) { input.pollSelection(); @@ -1644,7 +1644,7 @@ function posToDOM(cm, pos) { var view = findViewForLine(cm, pos.line); - if (!view) return null; + if (!view || view.hidden) return null; var line = getLine(cm.doc, pos.line); var info = mapFromLineView(view, line, pos.line); // FIXME bidi @@ -1681,7 +1681,10 @@ if (node == wrapper) { node = wrapper.childNodes[offset]; offset = 0; - if (!node) return cm.clipPos(Pos(lineNo(lineView.rest ? lst(lineView.rest) : lineView.line))); + if (!node) { + var line = lineView.rest ? lst(lineView.rest) : lineView.line; + return Pos(lineNo(line), line.text.length); + } } var textNode = node.nodeType == 3 ? node : null, topNode = node; @@ -3105,7 +3108,7 @@ // Used to suppress mouse event handling when a touch happens var touchFinished; - function finishTouch(e) { + function finishTouch() { touchFinished = setTimeout(function() {d.touchActive = false;}, 1000); }; on(d.scroller, "touchstart", function() { diff --git a/test/test.js b/test/test.js index 6314598235..83d09ca5ac 100644 --- a/test/test.js +++ b/test/test.js @@ -681,7 +681,7 @@ testCM("selectAllNoScroll", function(cm) { }); testCM("selectionPos", function(cm) { - if (phantom) return; + if (phantom || cm.getOption("inputStyle") != "textarea") return; cm.setSize(100, 100); addDoc(cm, 200, 100); cm.setSelection(Pos(1, 100), Pos(98, 100)); @@ -811,6 +811,7 @@ testCM("collapsedRangeCoordsChar", function(cm) { }, {value: "123456\nabcdef\nghijkl\nmnopqr\n"}); testCM("collapsedRangeBetweenLinesSelected", function(cm) { + if (cm.getOption("inputStyle") != "textarea") return; var widget = document.createElement("span"); widget.textContent = "\u2194"; cm.markText(Pos(0, 3), Pos(1, 0), {replacedWith: widget}); @@ -1091,6 +1092,7 @@ testCM("measureEndOfLine", function(cm) { }, {mode: "text/html", value: "", lineWrapping: true}, ie_lt8 || opera_lt10); testCM("scrollVerticallyAndHorizontally", function(cm) { + if (cm.getOption("inputStyle") != "textarea") return; cm.setSize(100, 100); addDoc(cm, 40, 40); cm.setCursor(39); @@ -1319,6 +1321,7 @@ testCM("verticalMovementCommandsWrapping", function(cm) { lineWrapping: true}); testCM("rtlMovement", function(cm) { + if (cm.getOption("inputStyle") != "textarea") return; forEach(["خحج", "خحabcخحج", "abخحخحجcd", "abخde", "abخح2342خ1حج", "خ1ح2خح3حxج", "خحcd", "1خحcd", "abcdeح1ج", "خمرحبها مها!", "foobarر", "خ ة ق", ""], function(line) { @@ -1388,7 +1391,7 @@ testCM("lineChangeEvents", function(cm) { }); testCM("scrollEntirelyToRight", function(cm) { - if (phantom) return; + if (phantom || cm.getOption("inputStyle") != "textarea") return; addDoc(cm, 500, 2); cm.setCursor(Pos(0, 500)); var wrap = cm.getWrapperElement(), cur = byClassName(wrap, "CodeMirror-cursor")[0]; From 3ab057f4af1a8354ffa897797aad235a8aad5f35 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Wed, 29 Oct 2014 14:22:44 +0100 Subject: [PATCH 12/47] Don't poll for selection during composition --- lib/codemirror.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/codemirror.js b/lib/codemirror.js index 6026d7157f..771e237ff3 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -1514,6 +1514,13 @@ } }); + on(div, "compositionstart", function() { + this.composing = true; + }); + on(div, "compositionend", function() { + this.composing = false; + }); + function onCopyCut(e) { if (e.clipboardData) { e.preventDefault(); @@ -1617,6 +1624,8 @@ }, pollSelection: function() { + if (this.composing) return; + var sel = window.getSelection(); if (sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset) { From 5e834b99421d5974d26e08f17ca969cdfa2f14cb Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Wed, 29 Oct 2014 14:35:33 +0100 Subject: [PATCH 13/47] Draw secondary selections in contentEditable input model --- lib/codemirror.js | 51 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/lib/codemirror.js b/lib/codemirror.js index 771e237ff3..1dd777b868 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -1251,18 +1251,8 @@ prepareSelection: function() { // Redraw the selection and/or cursor - var cm = this.cm, display = cm.display, 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++) { - var range = doc.sel.ranges[i]; - var collapsed = range.empty(); - if (collapsed || cm.options.showCursorWhenSelecting) - drawSelectionCursor(cm, range, curFragment); - if (!collapsed) - drawSelectionRange(cm, range, selFragment); - } + 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) { @@ -1545,12 +1535,19 @@ on(div, "cut", onCopyCut); }, - prepareSelection: function() { return this.cm.state.focused; }, + prepareSelection: function() { + var result = prepareSelection(this.cm, false); + result.focus = this.cm.state.focused; + return result; + }, - // FIXME draw multiple selections - showSelection: function(doIt) { - if (!doIt || !this.cm.display.view.length) return; + showSelection: function(info) { + if (!info || !this.cm.display.view.length) return; + if (info.focus) 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, true); var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset, true); @@ -1583,6 +1580,11 @@ this.rememberSelection(); }, + 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; @@ -2024,6 +2026,23 @@ 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]; + var collapsed = range.empty(); + if (collapsed || cm.options.showCursorWhenSelecting) + drawSelectionCursor(cm, range, curFragment); + if (!collapsed) + drawSelectionRange(cm, range, selFragment); + } + return result; + } + // Draws a cursor for the given range function drawSelectionCursor(cm, range, output) { var pos = cursorCoords(cm, range.head, "div", null, null, !cm.options.singleCursorHeightPerLine); From 4891ba31f2fd2e8209d88f92f2a77f55df7fcf92 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Wed, 29 Oct 2014 14:46:13 +0100 Subject: [PATCH 14/47] Place cursor at correct offset in bidi text --- lib/codemirror.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/codemirror.js b/lib/codemirror.js index 1dd777b868..fb4ffb943f 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -1646,7 +1646,6 @@ node.setAttribute("contenteditable", "false"); }, - // FIXME handle IME onKeyPress: function(e) { e.preventDefault(); operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); @@ -1658,7 +1657,12 @@ if (!view || view.hidden) return null; var line = getLine(cm.doc, pos.line); var info = mapFromLineView(view, line, pos.line); - // FIXME bidi + + 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, "left"); result.offset = result.collapse == "right" ? result.end : result.start; return result; From 4f20778ac6219bb720a88a44b273d91fd2ea4fd2 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Wed, 12 Nov 2014 11:40:14 +0100 Subject: [PATCH 15/47] Improve handling of composition events --- lib/codemirror.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/codemirror.js b/lib/codemirror.js index fb4ffb943f..bda1107ccf 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -1490,12 +1490,6 @@ if (div.contentEditable != "plaintext-only") div.contentEditable = "true"; disableBrowserMagic(div); - on(div, "textInput", function(e) { - if (eventInWidget(cm.display, e)) return; - e.preventDefault(); - operation(cm, applyTextInput)(cm, e.data, 0); - }); - on(div, "paste", function(e) { var pasted = e.clipboardData && e.clipboardData.getData("text/plain"); if (pasted) { @@ -1505,10 +1499,14 @@ }); on(div, "compositionstart", function() { - this.composing = true; + input.composing = true; }); - on(div, "compositionend", function() { - this.composing = false; + on(div, "compositionend", function(e) { + input.composing = false; + var data = e.data; + setTimeout(function() { + operation(cm, applyTextInput)(cm, data, 0); + }, 50); }); function onCopyCut(e) { From 2ba1798be6f2c7fab8679054f3c20804e64ff15b Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Wed, 12 Nov 2014 11:48:13 +0100 Subject: [PATCH 16/47] Fix focus issue in Firefox --- lib/codemirror.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/codemirror.js b/lib/codemirror.js index bda1107ccf..22d512c78c 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -1688,8 +1688,6 @@ function locateNodeInLineView(lineView, node, offset) { var wrapper = lineView.text.firstChild; - // FIXME debug - if (!wrapper) console.log("wrapper is null", lineView.text.outerHTML, lineView.measure && lineView.measure.map); if (!node || !contains(wrapper, node)) return Pos(lineNo(lineView.line), 0); if (node == wrapper) { node = wrapper.childNodes[offset]; @@ -3270,7 +3268,8 @@ var lastClick, lastDoubleClick; function leftButtonDown(cm, e, start) { - setTimeout(bind(ensureFocus, cm), 0); + ensureFocus(cm); + if (ie) setTimeout(bind(ensureFocus, cm), 0); var now = +new Date, type; if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) { From 9ddfe1f2871631c467739576cf6da7440e65f8c1 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Mon, 24 Nov 2014 12:07:26 +0100 Subject: [PATCH 17/47] Don't treat single-pixel-radius touch events as touch events --- lib/codemirror.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/codemirror.js b/lib/codemirror.js index 22d512c78c..501c038202 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -3137,11 +3137,18 @@ // Used to suppress mouse event handling when a touch happens var touchFinished; function finishTouch() { - touchFinished = setTimeout(function() {d.touchActive = false;}, 1000); + if (d.touchActive) touchFinished = setTimeout(function() {d.touchActive = false;}, 1000); }; - on(d.scroller, "touchstart", function() { - clearTimeout(touchFinished); - d.touchActive = true; + function isMouseLikeTouchEvent(e) { + if (e.touches.length != 1) return false; + var touch = e.touches[0]; + return touch.radiusX <= 1 && touch.radiusY <= 1; + } + on(d.scroller, "touchstart", function(e) { + if (!isMouseLikeTouchEvent(e)) { + clearTimeout(touchFinished); + d.touchActive = true; + } }); on(d.scroller, "touchend", finishTouch); on(d.scroller, "touchcancel", finishTouch); From 45229e2af219c8b024785547f93f9846df210378 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Wed, 3 Dec 2014 13:35:21 +0100 Subject: [PATCH 18/47] [contenteditable input] React to input invent by reading text from DOM --- lib/codemirror.js | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/lib/codemirror.js b/lib/codemirror.js index 501c038202..f586a4c931 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -1502,13 +1502,19 @@ input.composing = true; }); on(div, "compositionend", function(e) { - input.composing = false; var data = e.data; setTimeout(function() { operation(cm, applyTextInput)(cm, data, 0); + input.composing = false; }, 50); }); + on(div, "input", function() { + if (input.composing) return; + if (!input.pollContent()) + runInOp(input.cm, function() {regChange(cm);}); + }); + function onCopyCut(e) { if (e.clipboardData) { e.preventDefault(); @@ -1637,6 +1643,50 @@ } }, + 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; + + if (from.line == display.viewFrom) { + var fromLine = from.line; + var fromNode = display.view[0].node; + } else { + var fromIndex = findViewIndex(cm, from.line); + 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.view[toIndex].node; + } else { + var toLine = lineNo(display.view[toIndex + 1].line) - 1; + var toNode = display.view[toIndex + 1].node.previousSibling; + } + var text = splitLines(domTextBetween(fromNode, toNode)); + + var beforeSel = getBetween(cm.doc, Pos(fromLine, 0), from); + while (beforeSel.length > 1) { + if (text.length <= 1 || beforeSel[0] != text[0]) return false; + beforeSel.shift(); text.shift(); + } + if (beforeSel[0] != text[0].slice(0, from.ch)) return false; + text[0] = text[0].slice(from.ch); + + var afterSel = getBetween(cm.doc, to, Pos(toLine, getLine(cm.doc, toLine).text.length)); + while (afterSel.length > 1) { + if (text.length <= 1 || lst(afterSel) != lst(text)) return false; + afterSel.pop(); text.pop(); + } + var textLst = lst(text); + if (afterSel[0] != textLst.slice(textLst.length - afterSel[0].length)) return false; + text[text.length - 1] = textLst.slice(0, textLst.length - afterSel[0].length); + + cm.replaceSelection(text.join("\n")); + return true; + }, + ensurePolled: nothing, onContextMenu: nothing, @@ -1733,6 +1783,33 @@ } } + function domTextBetween(from, to) { + var text = "", closing = false; + function walk(node) { + if (node.nodeType == 1) { + 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 += "\n"; + closing = false; + } + text += val; + } + } + for (;;) { + walk(from); + if (from == to) break; + from = from.nextSibling; + } + return text; + } + CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; // SELECTION / CURSOR From 0dc7c20b2f6656381fba2cf39c4b1710af17bd13 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Wed, 3 Dec 2014 15:02:31 +0100 Subject: [PATCH 19/47] Override tap events to avoid Chrome's magnifying behavior It apparently believes that if the editor's nodes are directly next to each other without margin, the user needs a magnifying glass to pick a cursor position. --- lib/codemirror.css | 1 + lib/codemirror.js | 21 +++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/codemirror.css b/lib/codemirror.css index 0b53af6576..626d890f10 100644 --- a/lib/codemirror.css +++ b/lib/codemirror.css @@ -239,6 +239,7 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} z-index: 2; position: relative; overflow: visible; + -webkit-tap-highlight-color: transparent; } .CodeMirror-wrap pre { word-wrap: break-word; diff --git a/lib/codemirror.js b/lib/codemirror.js index f586a4c931..1a0b4ba9c6 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -3224,10 +3224,27 @@ on(d.scroller, "touchstart", function(e) { if (!isMouseLikeTouchEvent(e)) { clearTimeout(touchFinished); - d.touchActive = true; + d.touchActive = {}; + if (e.touches.length == 1) { + d.touchActive.left = e.touches[0].pageX; + d.touchActive.top = e.touches[0].pageY; + d.touchActive.time = Date.now(); + } + } + }); + on(d.scroller, "touchmove", function() { + if (d.touchActive) d.touchActive.moved = true; + }); + on(d.scroller, "touchend", function(e) { + if (d.touchActive && !eventInWidget(d, e) && + !d.touchActive.moved && + d.touchActive.time != null && Date.now() - d.touchActive.time < 300) { + cm.setCursor(cm.coordsChar(d.touchActive, "page")); + cm.focus(); + e_preventDefault(e); } + finishTouch(); }); - on(d.scroller, "touchend", finishTouch); on(d.scroller, "touchcancel", finishTouch); // Sync scrolling between fake scrollbars and real scrollable From 72d5c41ea5b72bbceea72f70a072049da90302c3 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Wed, 3 Dec 2014 15:32:59 +0100 Subject: [PATCH 20/47] [contenteditable input] Properly handle compositionend events --- lib/codemirror.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/codemirror.js b/lib/codemirror.js index 1a0b4ba9c6..ea358a0f02 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -1076,22 +1076,23 @@ // was made out of. var lastCopied = null; - function applyTextInput(cm, inserted, deleted) { + function applyTextInput(cm, inserted, deleted, sel) { var doc = cm.doc; cm.display.shift = false; + if (!sel) sel = doc.sel; var textLines = splitLines(inserted), multiPaste = null; // When pasing N lines into N selections, insert one line per selection - if (cm.state.pasteIncoming && doc.sel.ranges.length > 1) { + if (cm.state.pasteIncoming && 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 = sel.ranges.length % lastCopied.length == 0 && map(lastCopied, splitLines); + 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 = doc.sel.ranges.length - 1; i >= 0; i--) { - var range = doc.sel.ranges[i]; + 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 @@ -1107,7 +1108,7 @@ // 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)) { + (!i || sel.ranges[i - 1].head.line != range.head.line)) { var mode = cm.getModeAt(range.head); var end = changeEnd(changeEvent); if (mode.electricChars) { @@ -1498,13 +1499,16 @@ } }); - on(div, "compositionstart", function() { - input.composing = true; + on(div, "compositionstart", function(e) { + input.composing = {sel: cm.doc.sel, value: e.data}; + }); + on(div, "compositionupdate", function(e) { + input.composing.value = e.data; }); on(div, "compositionend", function(e) { - var data = e.data; + var data = e.data || input.composing.value, sel = input.composing.sel; setTimeout(function() { - operation(cm, applyTextInput)(cm, data, 0); + operation(cm, applyTextInput)(cm, data, 0, sel); input.composing = false; }, 50); }); From 4734f2f579229db5139bbee69ffcf8bca9c34689 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Wed, 3 Dec 2014 15:46:43 +0100 Subject: [PATCH 21/47] [contenteditable input] Kill composition when key is handled or input is reset --- lib/codemirror.js | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/lib/codemirror.js b/lib/codemirror.js index ea358a0f02..07ce3ffa19 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -1506,10 +1506,11 @@ input.composing.value = e.data; }); on(div, "compositionend", function(e) { + if (!input.composing) return; var data = e.data || input.composing.value, sel = input.composing.sel; setTimeout(function() { operation(cm, applyTextInput)(cm, data, 0, sel); - input.composing = false; + input.composing = null; }, 50); }); @@ -1614,9 +1615,6 @@ supportsTouch: function() { return true; }, - reset: nothing, - resetPosition: nothing, - receivedFocus: function() { var input = this; if (this.selectionInEditor()) @@ -1691,8 +1689,18 @@ return true; }, - ensurePolled: nothing, - onContextMenu: nothing, + ensurePolled: function() { + if (this.composing) this.forceCompositionEnd(); + }, + reset: function() { + if (this.composing) this.forceCompositionEnd(); + }, + forceCompositionEnd: function() { + operation(this.cm, applyTextInput)(this.cm, this.composing.value, 0, this.composing.sel); + this.composing = null; + this.div.blur(); + this.div.focus(); + }, setUneditable: function(node) { node.setAttribute("contenteditable", "false"); @@ -1701,7 +1709,10 @@ onKeyPress: function(e) { e.preventDefault(); operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); - } + }, + + onContextMenu: nothing, + resetPosition: nothing }, ContentEditableInput.prototype); function posToDOM(cm, pos) { From e3748a29e86d6bcfea21e726319b770ef1a38ecb Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Wed, 3 Dec 2014 16:40:48 +0100 Subject: [PATCH 22/47] Style in-editor selections to match default selection style So that the contenteditable input style doesn't look silly on desktop. --- lib/codemirror.css | 2 ++ theme/3024-day.css | 2 ++ theme/3024-night.css | 2 ++ theme/ambiance.css | 10 ++++------ theme/base16-dark.css | 2 ++ theme/base16-light.css | 2 ++ theme/blackboard.css | 2 ++ theme/cobalt.css | 2 ++ theme/erlang-dark.css | 2 ++ theme/lesser-dark.css | 2 ++ theme/mbo.css | 2 ++ theme/mdn-like.css | 2 ++ theme/midnight.css | 2 ++ theme/monokai.css | 2 ++ theme/night.css | 2 ++ theme/paraiso-dark.css | 2 ++ theme/paraiso-light.css | 2 ++ theme/pastel-on-dark.css | 3 +++ theme/rubyblue.css | 2 ++ theme/solarized.css | 12 ++++++------ theme/the-matrix.css | 2 ++ theme/tomorrow-night-eighties.css | 2 ++ theme/twilight.css | 2 ++ theme/vibrant-ink.css | 2 ++ theme/xq-dark.css | 2 ++ 25 files changed, 57 insertions(+), 12 deletions(-) diff --git a/lib/codemirror.css b/lib/codemirror.css index 626d890f10..1902ba4abd 100644 --- a/lib/codemirror.css +++ b/lib/codemirror.css @@ -292,6 +292,8 @@ div.CodeMirror-cursors { .CodeMirror-selected { background: #d9d9d9; } .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } .CodeMirror-crosshair { cursor: crosshair; } +.CodeMirror ::selection { background: #d7d4f0; } +.CodeMirror ::-moz-selection { background: #d7d4f0; } .cm-searching { background: #ffa; diff --git a/theme/3024-day.css b/theme/3024-day.css index 3c01c2bf58..359281621b 100644 --- a/theme/3024-day.css +++ b/theme/3024-day.css @@ -10,6 +10,8 @@ .cm-s-3024-day.CodeMirror {background: #f7f7f7; color: #3a3432;} .cm-s-3024-day div.CodeMirror-selected {background: #d6d5d4 !important;} +.cm-s-3024-day.CodeMirror ::selection { background: #d6d5d4; } +.cm-s-3024-day.CodeMirror ::-moz-selection { background: #d9d9d9; } .cm-s-3024-day .CodeMirror-gutters {background: #f7f7f7; border-right: 0px;} .cm-s-3024-day .CodeMirror-guttermarker { color: #db2d20; } diff --git a/theme/3024-night.css b/theme/3024-night.css index 631757fc47..ccab9d50bf 100644 --- a/theme/3024-night.css +++ b/theme/3024-night.css @@ -10,6 +10,8 @@ .cm-s-3024-night.CodeMirror {background: #090300; color: #d6d5d4;} .cm-s-3024-night div.CodeMirror-selected {background: #3a3432 !important;} +.cm-s-3024-night.CodeMirror ::selection { background: rgba(58, 52, 50, .99); } +.cm-s-3024-night.CodeMirror ::-moz-selection { background: rgba(58, 52, 50, .99); } .cm-s-3024-night .CodeMirror-gutters {background: #090300; border-right: 0px;} .cm-s-3024-night .CodeMirror-guttermarker { color: #db2d20; } .cm-s-3024-night .CodeMirror-guttermarker-subtle { color: #5c5855; } diff --git a/theme/ambiance.css b/theme/ambiance.css index c844566eac..afcf15a37a 100644 --- a/theme/ambiance.css +++ b/theme/ambiance.css @@ -30,12 +30,10 @@ .cm-s-ambiance .CodeMirror-matchingbracket { color: #0f0; } .cm-s-ambiance .CodeMirror-nonmatchingbracket { color: #f22; } -.cm-s-ambiance .CodeMirror-selected { - background: rgba(255, 255, 255, 0.15); -} -.cm-s-ambiance.CodeMirror-focused .CodeMirror-selected { - background: rgba(255, 255, 255, 0.10); -} +.cm-s-ambiance .CodeMirror-selected { background: rgba(255, 255, 255, 0.15); } +.cm-s-ambiance.CodeMirror-focused .CodeMirror-selected { background: rgba(255, 255, 255, 0.10); } +.cm-s-ambiance.CodeMirror ::selection { background: rgba(255, 255, 255, 0.10); } +.cm-s-ambiance.CodeMirror ::-moz-selection { background: rgba(255, 255, 255, 0.10); } /* Editor styling */ diff --git a/theme/base16-dark.css b/theme/base16-dark.css index a46abdbbbd..b009d2b9d6 100644 --- a/theme/base16-dark.css +++ b/theme/base16-dark.css @@ -10,6 +10,8 @@ .cm-s-base16-dark.CodeMirror {background: #151515; color: #e0e0e0;} .cm-s-base16-dark div.CodeMirror-selected {background: #303030 !important;} +.cm-s-base16-dark.CodeMirror ::selection { background: rgba(48, 48, 48, .99); } +.cm-s-base16-dark.CodeMirror ::-moz-selection { background: rgba(48, 48, 48, .99); } .cm-s-base16-dark .CodeMirror-gutters {background: #151515; border-right: 0px;} .cm-s-base16-dark .CodeMirror-guttermarker { color: #ac4142; } .cm-s-base16-dark .CodeMirror-guttermarker-subtle { color: #505050; } diff --git a/theme/base16-light.css b/theme/base16-light.css index 12ff2eb06f..15df6d3807 100644 --- a/theme/base16-light.css +++ b/theme/base16-light.css @@ -10,6 +10,8 @@ .cm-s-base16-light.CodeMirror {background: #f5f5f5; color: #202020;} .cm-s-base16-light div.CodeMirror-selected {background: #e0e0e0 !important;} +.cm-s-base16-light.CodeMirror ::selection { background: #e0e0e0; } +.cm-s-base16-light.CodeMirror ::-moz-selection { background: #e0e0e0; } .cm-s-base16-light .CodeMirror-gutters {background: #f5f5f5; border-right: 0px;} .cm-s-base16-light .CodeMirror-guttermarker { color: #ac4142; } .cm-s-base16-light .CodeMirror-guttermarker-subtle { color: #b0b0b0; } diff --git a/theme/blackboard.css b/theme/blackboard.css index d7a2dc9695..02289b630b 100644 --- a/theme/blackboard.css +++ b/theme/blackboard.css @@ -2,6 +2,8 @@ .cm-s-blackboard.CodeMirror { background: #0C1021; color: #F8F8F8; } .cm-s-blackboard .CodeMirror-selected { background: #253B76 !important; } +.cm-s-blackboard.CodeMirror ::selection { background: rgba(37, 59, 118, .99); } +.cm-s-blackboard.CodeMirror ::-moz-selection { background: rgba(37, 59, 118, .99); } .cm-s-blackboard .CodeMirror-gutters { background: #0C1021; border-right: 0; } .cm-s-blackboard .CodeMirror-guttermarker { color: #FBDE2D; } .cm-s-blackboard .CodeMirror-guttermarker-subtle { color: #888; } diff --git a/theme/cobalt.css b/theme/cobalt.css index 47440531a0..3915589494 100644 --- a/theme/cobalt.css +++ b/theme/cobalt.css @@ -1,5 +1,7 @@ .cm-s-cobalt.CodeMirror { background: #002240; color: white; } .cm-s-cobalt div.CodeMirror-selected { background: #b36539 !important; } +.cm-s-cobalt.CodeMirror ::selection { background: rgba(179, 101, 57, .99); } +.cm-s-cobalt.CodeMirror ::-moz-selection { background: rgba(179, 101, 57, .99); } .cm-s-cobalt .CodeMirror-gutters { background: #002240; border-right: 1px solid #aaa; } .cm-s-cobalt .CodeMirror-guttermarker { color: #ffee80; } .cm-s-cobalt .CodeMirror-guttermarker-subtle { color: #d0d0d0; } diff --git a/theme/erlang-dark.css b/theme/erlang-dark.css index ff47d7f8de..25c7e0a2aa 100644 --- a/theme/erlang-dark.css +++ b/theme/erlang-dark.css @@ -1,5 +1,7 @@ .cm-s-erlang-dark.CodeMirror { background: #002240; color: white; } .cm-s-erlang-dark div.CodeMirror-selected { background: #b36539 !important; } +.cm-s-erlang-dark.CodeMirror ::selection { background: rgba(179, 101, 57, .99); } +.cm-s-erlang-dark.CodeMirror ::-moz-selection { background: rgba(179, 101, 57, .99); } .cm-s-erlang-dark .CodeMirror-gutters { background: #002240; border-right: 1px solid #aaa; } .cm-s-erlang-dark .CodeMirror-guttermarker { color: white; } .cm-s-erlang-dark .CodeMirror-guttermarker-subtle { color: #d0d0d0; } diff --git a/theme/lesser-dark.css b/theme/lesser-dark.css index a782474739..5af8b7f627 100644 --- a/theme/lesser-dark.css +++ b/theme/lesser-dark.css @@ -7,6 +7,8 @@ Ported to CodeMirror by Peter Kroon } .cm-s-lesser-dark.CodeMirror { background: #262626; color: #EBEFE7; text-shadow: 0 -1px 1px #262626; } .cm-s-lesser-dark div.CodeMirror-selected {background: #45443B !important;} /* 33322B*/ +.cm-s-lesser-dark.CodeMirror ::selection { background: rgba(69, 68, 59, .99); } +.cm-s-lesser-dark.CodeMirror ::-moz-selection { background: rgba(69, 68, 59, .99); } .cm-s-lesser-dark .CodeMirror-cursor { border-left: 1px solid white !important; } .cm-s-lesser-dark pre { padding: 0 8px; }/*editable code holder*/ diff --git a/theme/mbo.css b/theme/mbo.css index 0ad6360b50..e39879522e 100644 --- a/theme/mbo.css +++ b/theme/mbo.css @@ -6,6 +6,8 @@ .cm-s-mbo.CodeMirror {background: #2c2c2c; color: #ffffec;} .cm-s-mbo div.CodeMirror-selected {background: #716C62 !important;} +.cm-s-mbo.CodeMirror ::selection { background: rgba(113, 108, 98, .99); } +.cm-s-mbo.CodeMirror ::-moz-selection { background: rgba(113, 108, 98, .99); } .cm-s-mbo .CodeMirror-gutters {background: #4e4e4e; border-right: 0px;} .cm-s-mbo .CodeMirror-guttermarker { color: white; } .cm-s-mbo .CodeMirror-guttermarker-subtle { color: grey; } diff --git a/theme/mdn-like.css b/theme/mdn-like.css index 81b21772d2..93293c01c8 100644 --- a/theme/mdn-like.css +++ b/theme/mdn-like.css @@ -9,6 +9,8 @@ */ .cm-s-mdn-like.CodeMirror { color: #999; background-color: #fff; } .cm-s-mdn-like .CodeMirror-selected { background: #cfc !important; } +.cm-s-mdn-like.CodeMirror ::selection { background: #cfc; } +.cm-s-mdn-like.CodeMirror ::-moz-selection { background: #cfc; } .cm-s-mdn-like .CodeMirror-gutters { background: #f8f8f8; border-left: 6px solid rgba(0,83,159,0.65); color: #333; } .cm-s-mdn-like .CodeMirror-linenumber { color: #aaa; margin-left: 3px; } diff --git a/theme/midnight.css b/theme/midnight.css index 4567d29399..296af4f7d2 100644 --- a/theme/midnight.css +++ b/theme/midnight.css @@ -15,6 +15,8 @@ .cm-s-midnight.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;} .cm-s-midnight div.CodeMirror-selected {background: #314D67 !important;} +.cm-s-midnight.CodeMirror ::selection { background: rgba(49, 77, 103, .99); } +.cm-s-midnight.CodeMirror ::-moz-selection { background: rgba(49, 77, 103, .99); } .cm-s-midnight .CodeMirror-gutters {background: #0F192A; border-right: 1px solid;} .cm-s-midnight .CodeMirror-guttermarker { color: white; } .cm-s-midnight .CodeMirror-guttermarker-subtle { color: #d0d0d0; } diff --git a/theme/monokai.css b/theme/monokai.css index 548d2dfff6..6dfcc73ce4 100644 --- a/theme/monokai.css +++ b/theme/monokai.css @@ -2,6 +2,8 @@ .cm-s-monokai.CodeMirror {background: #272822; color: #f8f8f2;} .cm-s-monokai div.CodeMirror-selected {background: #49483E !important;} +.cm-s-monokai.CodeMirror ::selection { background: rgba(73, 72, 62, .99); } +.cm-s-monokai.CodeMirror ::-moz-selection { background: rgba(73, 72, 62, .99); } .cm-s-monokai .CodeMirror-gutters {background: #272822; border-right: 0px;} .cm-s-monokai .CodeMirror-guttermarker { color: white; } .cm-s-monokai .CodeMirror-guttermarker-subtle { color: #d0d0d0; } diff --git a/theme/night.css b/theme/night.css index a0bf8cfa78..6b2ac6c7cf 100644 --- a/theme/night.css +++ b/theme/night.css @@ -2,6 +2,8 @@ .cm-s-night.CodeMirror { background: #0a001f; color: #f8f8f8; } .cm-s-night div.CodeMirror-selected { background: #447 !important; } +.cm-s-night.CodeMirror ::selection { background: rgba(68, 68, 119, .99); } +.cm-s-night.CodeMirror ::-moz-selection { background: rgba(68, 68, 119, .99); } .cm-s-night .CodeMirror-gutters { background: #0a001f; border-right: 1px solid #aaa; } .cm-s-night .CodeMirror-guttermarker { color: white; } .cm-s-night .CodeMirror-guttermarker-subtle { color: #bbb; } diff --git a/theme/paraiso-dark.css b/theme/paraiso-dark.css index 53dcdf7a2a..af914b60bf 100644 --- a/theme/paraiso-dark.css +++ b/theme/paraiso-dark.css @@ -10,6 +10,8 @@ .cm-s-paraiso-dark.CodeMirror {background: #2f1e2e; color: #b9b6b0;} .cm-s-paraiso-dark div.CodeMirror-selected {background: #41323f !important;} +.cm-s-paraiso-dark.CodeMirror ::selection { background: rgba(65, 50, 63, .99); } +.cm-s-paraiso-dark.CodeMirror ::-moz-selection { background: rgba(65, 50, 63, .99); } .cm-s-paraiso-dark .CodeMirror-gutters {background: #2f1e2e; border-right: 0px;} .cm-s-paraiso-dark .CodeMirror-guttermarker { color: #ef6155; } .cm-s-paraiso-dark .CodeMirror-guttermarker-subtle { color: #776e71; } diff --git a/theme/paraiso-light.css b/theme/paraiso-light.css index 07ca325978..e198066faa 100644 --- a/theme/paraiso-light.css +++ b/theme/paraiso-light.css @@ -10,6 +10,8 @@ .cm-s-paraiso-light.CodeMirror {background: #e7e9db; color: #41323f;} .cm-s-paraiso-light div.CodeMirror-selected {background: #b9b6b0 !important;} +.cm-s-paraiso-light.CodeMirror ::selection { background: #b9b6b0; } +.cm-s-paraiso-light.CodeMirror ::-moz-selection { background: #b9b6b0; } .cm-s-paraiso-light .CodeMirror-gutters {background: #e7e9db; border-right: 0px;} .cm-s-paraiso-light .CodeMirror-guttermarker { color: black; } .cm-s-paraiso-light .CodeMirror-guttermarker-subtle { color: #8d8687; } diff --git a/theme/pastel-on-dark.css b/theme/pastel-on-dark.css index 7992ac7d2d..0d06f63284 100644 --- a/theme/pastel-on-dark.css +++ b/theme/pastel-on-dark.css @@ -14,6 +14,9 @@ font-size: 14px; } .cm-s-pastel-on-dark div.CodeMirror-selected { background: rgba(221,240,255,0.2) !important; } +.cm-s-pastel-on-dark.CodeMirror ::selection { background: rgba(221,240,255,0.2); } +.cm-s-pastel-on-dark.CodeMirror ::-moz-selection { background: rgba(221,240,255,0.2); } + .cm-s-pastel-on-dark .CodeMirror-gutters { background: #34302f; border-right: 0px; diff --git a/theme/rubyblue.css b/theme/rubyblue.css index 5349838940..d2fc0ecdbc 100644 --- a/theme/rubyblue.css +++ b/theme/rubyblue.css @@ -1,5 +1,7 @@ .cm-s-rubyblue.CodeMirror { background: #112435; color: white; } .cm-s-rubyblue div.CodeMirror-selected { background: #38566F !important; } +.cm-s-rubyblue.CodeMirror ::selection { background: rgba(56, 86, 111, 0.99); } +.cm-s-rubyblue.CodeMirror ::-moz-selection { background: rgba(56, 86, 111, 0.99); } .cm-s-rubyblue .CodeMirror-gutters { background: #1F4661; border-right: 7px solid #3E7087; } .cm-s-rubyblue .CodeMirror-guttermarker { color: white; } .cm-s-rubyblue .CodeMirror-guttermarker-subtle { color: #3E7087; } diff --git a/theme/solarized.css b/theme/solarized.css index 07ef406804..4a10b7c059 100644 --- a/theme/solarized.css +++ b/theme/solarized.css @@ -94,13 +94,13 @@ http://ethanschoonover.com/solarized/img/solarized-palette.png border-bottom: 1px dotted #dc322f; } -.cm-s-solarized.cm-s-dark .CodeMirror-selected { - background: #073642; -} +.cm-s-solarized.cm-s-dark .CodeMirror-selected { background: #073642; } +.cm-s-solarized.cm-s-dark.CodeMirror ::selection { background: rgba(7, 54, 66, 0.99); } +.cm-s-solarized.cm-s-dark.CodeMirror ::-moz-selection { background: rgba(7, 54, 66, 0.99); } -.cm-s-solarized.cm-s-light .CodeMirror-selected { - background: #eee8d5; -} +.cm-s-solarized.cm-s-light .CodeMirror-selected { background: #eee8d5; } +.cm-s-solarized.cm-s-light.CodeMirror ::selection { background: #eee8d5; } +.cm-s-solarized.cm-s-lightCodeMirror ::-moz-selection { background: #eee8d5; } /* Editor styling */ diff --git a/theme/the-matrix.css b/theme/the-matrix.css index 01474ca94d..f29b22b0da 100644 --- a/theme/the-matrix.css +++ b/theme/the-matrix.css @@ -1,5 +1,7 @@ .cm-s-the-matrix.CodeMirror { background: #000000; color: #00FF00; } .cm-s-the-matrix div.CodeMirror-selected { background: #2D2D2D !important; } +.cm-s-the-matrix.CodeMirror ::selection { background: rgba(45, 45, 45, 0.99); } +.cm-s-the-matrix.CodeMirror ::-moz-selection { background: rgba(45, 45, 45, 0.99); } .cm-s-the-matrix .CodeMirror-gutters { background: #060; border-right: 2px solid #00FF00; } .cm-s-the-matrix .CodeMirror-guttermarker { color: #0f0; } .cm-s-the-matrix .CodeMirror-guttermarker-subtle { color: white; } diff --git a/theme/tomorrow-night-eighties.css b/theme/tomorrow-night-eighties.css index 841413546c..5fca3cafbf 100644 --- a/theme/tomorrow-night-eighties.css +++ b/theme/tomorrow-night-eighties.css @@ -10,6 +10,8 @@ .cm-s-tomorrow-night-eighties.CodeMirror {background: #000000; color: #CCCCCC;} .cm-s-tomorrow-night-eighties div.CodeMirror-selected {background: #2D2D2D !important;} +.cm-s-tomorrow-night-eighties.CodeMirror ::selection { background: rgba(45, 45, 45, 0.99); } +.cm-s-tomorrow-night-eighties.CodeMirror ::-moz-selection { background: rgba(45, 45, 45, 0.99); } .cm-s-tomorrow-night-eighties .CodeMirror-gutters {background: #000000; border-right: 0px;} .cm-s-tomorrow-night-eighties .CodeMirror-guttermarker { color: #f2777a; } .cm-s-tomorrow-night-eighties .CodeMirror-guttermarker-subtle { color: #777; } diff --git a/theme/twilight.css b/theme/twilight.css index 9ca50576d8..889a83d799 100644 --- a/theme/twilight.css +++ b/theme/twilight.css @@ -1,5 +1,7 @@ .cm-s-twilight.CodeMirror { background: #141414; color: #f7f7f7; } /**/ .cm-s-twilight .CodeMirror-selected { background: #323232 !important; } /**/ +.cm-s-twilight.CodeMirror ::selection { background: rgba(50, 50, 50, 0.99); } +.cm-s-twilight.CodeMirror ::-moz-selection { background: rgba(50, 50, 50, 0.99); } .cm-s-twilight .CodeMirror-gutters { background: #222; border-right: 1px solid #aaa; } .cm-s-twilight .CodeMirror-guttermarker { color: white; } diff --git a/theme/vibrant-ink.css b/theme/vibrant-ink.css index 5177282325..8ea535973c 100644 --- a/theme/vibrant-ink.css +++ b/theme/vibrant-ink.css @@ -2,6 +2,8 @@ .cm-s-vibrant-ink.CodeMirror { background: black; color: white; } .cm-s-vibrant-ink .CodeMirror-selected { background: #35493c !important; } +.cm-s-vibrant-ink.CodeMirror ::selection { background: rgba(53, 73, 60, 0.99); } +.cm-s-vibrant-ink.CodeMirror ::-moz-selection { background: rgba(53, 73, 60, 0.99); } .cm-s-vibrant-ink .CodeMirror-gutters { background: #002240; border-right: 1px solid #aaa; } .cm-s-vibrant-ink .CodeMirror-guttermarker { color: white; } diff --git a/theme/xq-dark.css b/theme/xq-dark.css index 116eccf21b..d537993e89 100644 --- a/theme/xq-dark.css +++ b/theme/xq-dark.css @@ -22,6 +22,8 @@ THE SOFTWARE. */ .cm-s-xq-dark.CodeMirror { background: #0a001f; color: #f8f8f8; } .cm-s-xq-dark .CodeMirror-selected { background: #27007A !important; } +.cm-s-xq-dark.CodeMirror ::selection { background: rgba(39, 0, 122, 0.99); } +.cm-s-xq-dark.CodeMirror ::-moz-selection { background: rgba(39, 0, 122, 0.99); } .cm-s-xq-dark .CodeMirror-gutters { background: #0a001f; border-right: 1px solid #aaa; } .cm-s-xq-dark .CodeMirror-guttermarker { color: #FFBD40; } .cm-s-xq-dark .CodeMirror-guttermarker-subtle { color: #f8f8f8; } From 9c874be3b1d8c7616832311a03f1f2479835f3de Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Thu, 4 Dec 2014 15:32:39 +0100 Subject: [PATCH 23/47] [contenteditable input] Make dom poll hack work with tabs and special chars --- lib/codemirror.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/codemirror.js b/lib/codemirror.js index 07ce3ffa19..d5a13fb1e5 100644 --- a/lib/codemirror.js +++ b/lib/codemirror.js @@ -133,11 +133,11 @@ // 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. @@ -494,7 +494,7 @@ on(node, "mousedown", function() { 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); @@ -1803,6 +1803,8 @@ function walk(node) { if (node.nodeType == 1) { if (node.getAttribute("contenteditable") == "false") return; + var cmText = node.getAttribute("cm-text"); + if (cmText) { text += cmText; return; } for (var i = 0; i < node.childNodes.length; i++) walk(node.childNodes[i]); if (/^(pre|div|p)$/i.test(node.nodeName)) @@ -3326,7 +3328,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. @@ -6697,9 +6699,11 @@ 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 { 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; From 0f227d37511743355de99be4eaef7ff001b02ba7 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Wed, 10 Dec 2014 14:07:50 +0100 Subject: [PATCH 24/47] Force redraw of native selection when it is in a bogus state I.e. when the cursor is directly in the editor's top node or outside of a line's
 node.
---
 lib/codemirror.js | 40 ++++++++++++++++++++++------------------
 1 file changed, 22 insertions(+), 18 deletions(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index d5a13fb1e5..e3f7e84e06 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -1558,9 +1558,9 @@
 
     showPrimarySelection: function() {
       var sel = window.getSelection(), prim = this.cm.doc.sel.primary();
-      var curAnchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset, true);
-      var curFocus = domToPos(this.cm, sel.focusNode, sel.focusOffset, true);
-      if (curAnchor && curFocus &&
+      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;
@@ -1634,14 +1634,16 @@
     pollSelection: function() {
       if (this.composing) return;
 
-      var sel = window.getSelection();
+      var sel = window.getSelection(), cm = this.cm;
       if (sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset ||
           sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset) {
         this.rememberSelection();
-        var anchor = domToPos(this.cm, sel.anchorNode, sel.anchorOffset);
-        var head = domToPos(this.cm, sel.focusNode, sel.focusOffset);
-        if (anchor && head)
-          operation(this.cm, setSelection)(this.cm.doc, simpleSelection(anchor, head), sel_dontScroll);
+        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;
+        });
       }
     },
 
@@ -1731,16 +1733,17 @@
     return result;
   }
 
-  function domToPos(cm, node, offset, strict) {
+  function badPos(pos, bad) { if (bad) pos.bad = true; return pos; }
+
+  function domToPos(cm, node, offset) {
     var lineNode;
     if (node == cm.display.lineDiv) {
-      if (strict) return null;
       lineNode = cm.display.lineDiv.childNodes[offset];
-      if (!lineNode) return cm.clipPos(Pos(cm.display.viewTo - 1));
+      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;
+        if (!lineNode || lineNode == cm.display.lineDiv) return null;
         if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) break;
       }
     }
@@ -1752,14 +1755,15 @@
   }
 
   function locateNodeInLineView(lineView, node, offset) {
-    var wrapper = lineView.text.firstChild;
-    if (!node || !contains(wrapper, node)) return Pos(lineNo(lineView.line), 0);
+    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 Pos(lineNo(line), line.text.length);
+        return badPos(Pos(lineNo(line), line.text.length), bad);
       }
     }
 
@@ -1779,20 +1783,20 @@
       }
     }
     var found = find(textNode, topNode, offset);
-    if (found) return found;
+    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 Pos(found.line, found.ch - dist);
+        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 Pos(found.line, found.ch + dist);
+        return badPos(Pos(found.line, found.ch + dist), bad);
       else
         dist += after.textContent.length;
     }

From 8956ff560d10b6b0bb9637c474a6ff8b918cb2d1 Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Wed, 10 Dec 2014 14:37:54 +0100
Subject: [PATCH 25/47] [contenteditable input] Improve handling of forced
 completionend

---
 lib/codemirror.js | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index e3f7e84e06..de9b79b9ef 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -1508,8 +1508,12 @@
       on(div, "compositionend", function(e) {
         if (!input.composing) return;
         var data = e.data || input.composing.value, sel = input.composing.sel;
+        // Need a small delay to prevent other code (input event,
+        // selection polling) from doing damage when fired right after
+        // compositionend.
         setTimeout(function() {
-          operation(cm, applyTextInput)(cm, data, 0, sel);
+          if (input.composing && !input.composing.handled)
+            operation(cm, applyTextInput)(cm, data, 0, sel);
           input.composing = null;
         }, 50);
       });
@@ -1692,14 +1696,14 @@
     },
 
     ensurePolled: function() {
-      if (this.composing) this.forceCompositionEnd();
+      if (this.composing && !this.composing.handled) this.forceCompositionEnd();
     },
     reset: function() {
-      if (this.composing) this.forceCompositionEnd();
+      if (this.composing && !this.composing.handled) this.forceCompositionEnd();
     },
     forceCompositionEnd: function() {
       operation(this.cm, applyTextInput)(this.cm, this.composing.value, 0, this.composing.sel);
-      this.composing = null;
+      this.composing.handled = true;
       this.div.blur();
       this.div.focus();
     },

From 54df5999ec251dc84e943134dbde84344ccee078 Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Wed, 10 Dec 2014 14:42:27 +0100
Subject: [PATCH 26/47] [contenteditable input] Make sure zero-width elements
 aren't counted as text

---
 lib/codemirror.js | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index de9b79b9ef..a15d938514 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -1812,7 +1812,7 @@
       if (node.nodeType == 1) {
         if (node.getAttribute("contenteditable") == "false") return;
         var cmText = node.getAttribute("cm-text");
-        if (cmText) { text += cmText; return; }
+        if (cmText != null) { text += cmText; return; }
         for (var i = 0; i < node.childNodes.length; i++)
           walk(node.childNodes[i]);
         if (/^(pre|div|p)$/i.test(node.nodeName))
@@ -8169,8 +8169,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

From 6ed2b8673fcfe52787773e3ba8ab81a3d44e6299 Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Mon, 15 Dec 2014 12:56:09 +0100
Subject: [PATCH 27/47] [contenteditable input] Guard against wrapped or
 replaced nodes in patchDisplay

---
 lib/codemirror.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index a15d938514..49271390e5 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -849,7 +849,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

From f396f8fcb6fab17e6e09accb35522f22dde855e3 Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Tue, 16 Dec 2014 18:00:11 +0100
Subject: [PATCH 28/47] [contenteditable input] Try to handle Google Keyboard's
 weird composition events

---
 lib/codemirror.js | 59 +++++++++++++++++++++++++++++++++++++------------------
 1 file changed, 40 insertions(+), 19 deletions(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index 49271390e5..8bca14fc82 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -1500,20 +1500,28 @@
       });
 
       on(div, "compositionstart", function(e) {
-        input.composing = {sel: cm.doc.sel, value: e.data};
+        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 < data.length)
+          input.composing.sel = simpleSelection(Pos(prim.head.line, found),
+                                                Pos(prim.head.line, found + data.length));
       });
       on(div, "compositionupdate", function(e) {
         input.composing.value = e.data;
       });
       on(div, "compositionend", function(e) {
         if (!input.composing) return;
-        var data = e.data || input.composing.value, sel = input.composing.sel;
+        if (e.data) input.composing.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 (input.composing && !input.composing.handled)
-            operation(cm, applyTextInput)(cm, data, 0, sel);
+            input.applyComposition(input.composing);
           input.composing = null;
         }, 50);
       });
@@ -1672,26 +1680,35 @@
         var toLine = lineNo(display.view[toIndex + 1].line) - 1;
         var toNode = display.view[toIndex + 1].node.previousSibling;
       }
-      var text = splitLines(domTextBetween(fromNode, toNode));
 
-      var beforeSel = getBetween(cm.doc, Pos(fromLine, 0), from);
-      while (beforeSel.length > 1) {
-        if (text.length <= 1 || beforeSel[0] != text[0]) return false;
-        beforeSel.shift(); text.shift();
+      var newText = splitLines(domTextBetween(fromNode, toNode));
+      var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length));
+      while (newText.length && oldText.length) {
+        if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; }
+        else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; }
+        else break;
       }
-      if (beforeSel[0] != text[0].slice(0, from.ch)) return false;
-      text[0] = text[0].slice(from.ch);
 
-      var afterSel = getBetween(cm.doc, to, Pos(toLine, getLine(cm.doc, toLine).text.length));
-      while (afterSel.length > 1) {
-        if (text.length <= 1 || lst(afterSel) != lst(text)) return false;
-        afterSel.pop(); text.pop();
+      var cutFront = 0, cutEnd = 0;
+      if (newText.length && oldText.length) {
+        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.max(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] = newTop.slice(cutFront);
       }
-      var textLst = lst(text);
-      if (afterSel[0] != textLst.slice(textLst.length - afterSel[0].length)) return false;
-      text[text.length - 1] = textLst.slice(0, textLst.length - afterSel[0].length);
 
-      cm.replaceSelection(text.join("\n"));
+      var chFrom = Pos(fromLine, cutFront);
+      var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0);
+      if (newText.length || newText[0].length || cmp(chFrom, chTo))
+        replaceRange(cm.doc, newText, chFrom, chTo, "+input");
       return true;
     },
 
@@ -1702,11 +1719,15 @@
       if (this.composing && !this.composing.handled) this.forceCompositionEnd();
     },
     forceCompositionEnd: function() {
-      operation(this.cm, applyTextInput)(this.cm, this.composing.value, 0, this.composing.sel);
+      this.applyComposition(this.composing);
       this.composing.handled = true;
       this.div.blur();
       this.div.focus();
     },
+    applyComposition: function(composing) {
+      if (composing.data && composing.data != composing.startData)
+        operation(this.cm, applyTextInput)(this.cm, composing.data, 0, composing.sel);
+    },
 
     setUneditable: function(node) {
       node.setAttribute("contenteditable", "false");

From 42c3cadc7e65aa2715889b7f88e7d30421483b29 Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Mon, 5 Jan 2015 16:28:26 +0100
Subject: [PATCH 29/47] Work around broken iOS clipboard API

---
 lib/codemirror.js | 72 ++++++++++++++++++++++++++++++++++---------------------
 1 file changed, 45 insertions(+), 27 deletions(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index 8bca14fc82..3aed97603b 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -1166,24 +1166,30 @@
     this.hasSelection = false;
   };
 
+  function hiddenTextarea() {
+    var te = elt("textarea", null, null, "position: absolute; 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;
-      // The semihidden textarea that is focused when the editor is
-      // focused, and receives input.
-      var te = this.textarea = 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) 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);
 
       // Wraps and hides input textarea
-      var div = this.wrapper = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;");
+      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)
@@ -1533,23 +1539,35 @@
       });
 
       function onCopyCut(e) {
-        if (e.clipboardData) {
-          e.preventDefault();
-          if (cm.somethingSelected()) {
-            lastCopied = cm.getSelections();
-            if (e.type == "cut") cm.replaceSelection("", null, "cut");
-          } else {
-            var ranges = copyableRanges(cm);
-            lastCopied = ranges.text;
-            if (e.type == "cut") {
-              cm.operation(function() {
-                cm.setSelections(ranges.ranges, 0, sel_dontScroll);
-                cm.replaceSelection("", null, "cut");
-              });
-            }
+        if (cm.somethingSelected()) {
+          lastCopied = cm.getSelections();
+          if (e.type == "cut") cm.replaceSelection("", null, "cut");
+        } else {
+          var ranges = copyableRanges(cm);
+          lastCopied = 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.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.join("\n");
+          var hadFocus = document.activeElement;
+          selectInput(te);
+          setTimeout(function() {
+            cm.display.lineSpace.removeChild(kludge);
+            hadFocus.focus();
+          }, 50);
         }
       }
       on(div, "copy", onCopyCut);

From 3baa1c1269519e11225ac436296ccb5bdfb69b97 Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Mon, 5 Jan 2015 17:08:02 +0100
Subject: [PATCH 30/47] [contenteditable input] Fix empty array issues in
 pollContent

---
 lib/codemirror.js | 30 ++++++++++++++----------------
 1 file changed, 14 insertions(+), 16 deletions(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index 3aed97603b..e05aa1505f 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -1701,31 +1701,29 @@
 
       var newText = splitLines(domTextBetween(fromNode, toNode));
       var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length));
-      while (newText.length && oldText.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;
-      if (newText.length && oldText.length) {
-        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.max(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] = newTop.slice(cutFront);
-      }
+      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.max(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] = newTop.slice(cutFront);
 
       var chFrom = Pos(fromLine, cutFront);
       var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0);
-      if (newText.length || newText[0].length || cmp(chFrom, chTo))
+      if (newText.length > 1 || newText[0] || cmp(chFrom, chTo))
         replaceRange(cm.doc, newText, chFrom, chTo, "+input");
       return true;
     },

From 505f6963af5194fc73b7ade6910d22e07ba022d5 Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Mon, 5 Jan 2015 17:37:15 +0100
Subject: [PATCH 31/47] [contenteditable input] Resolve race condition in
 composition end

---
 lib/codemirror.js | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index e05aa1505f..e60dfa7e17 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -1517,18 +1517,20 @@
                                                 Pos(prim.head.line, found + data.length));
       });
       on(div, "compositionupdate", function(e) {
-        input.composing.value = e.data;
+        input.composing.data = e.data;
       });
       on(div, "compositionend", function(e) {
-        if (!input.composing) return;
-        if (e.data) input.composing.data = e.data;
+        var ours = input.composing;
+        if (!ours) return;
+        if (e.data != e.startData && !/\200b/.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 (input.composing && !input.composing.handled)
-            input.applyComposition(input.composing);
-          input.composing = null;
+          if (!ours.handled)
+            input.applyComposition(ours);
+          if (input.composing == ours)
+            input.composing = null;
         }, 50);
       });
 

From 3f60ed5101a38310f14f0d4d52e82f642185014c Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Wed, 7 Jan 2015 12:25:26 +0100
Subject: [PATCH 32/47] [contenteditable input] Fix pollContent when cursor
 ends up in a zero-width element

---
 lib/codemirror.js | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index e60dfa7e17..acc48cd8f6 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -1851,7 +1851,11 @@
       if (node.nodeType == 1) {
         if (node.getAttribute("contenteditable") == "false") return;
         var cmText = node.getAttribute("cm-text");
-        if (cmText != null) { text += cmText; return; }
+        if (cmText != null) {
+          if (cmText == "") cmText = node.textContent.replace(/\u200b/g, "");
+          text += cmText;
+          return;
+        }
         for (var i = 0; i < node.childNodes.length; i++)
           walk(node.childNodes[i]);
         if (/^(pre|div|p)$/i.test(node.nodeName))

From 60ef248a7739be24a118ece6e439a6e39c76a2b2 Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Fri, 23 Jan 2015 11:26:08 +0100
Subject: [PATCH 33/47] Don't immediately focus on click in IE

It can create a kind of phantom-focus state where the textarea
is focused but you can't type into it.

Issue #3041
---
 lib/codemirror.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index acc48cd8f6..03b42958ac 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -3437,8 +3437,8 @@
 
   var lastClick, lastDoubleClick;
   function leftButtonDown(cm, e, start) {
-    ensureFocus(cm);
     if (ie) setTimeout(bind(ensureFocus, cm), 0);
+    else ensureFocus(cm);
 
     var now = +new Date, type;
     if (lastDoubleClick && lastDoubleClick.time > now - 400 && cmp(lastDoubleClick.pos, start) == 0) {

From 0f5030a87a2ccffc5cce7d28019ff2d655199b84 Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Mon, 26 Jan 2015 14:20:34 +0100
Subject: [PATCH 34/47] When polling input, set prevInput before ending the
 operation

Since that might end up calling resetInput and creating an
inconsistent state.

Issue #3046
---
 lib/codemirror.js | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index 03b42958ac..cca917c278 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -1387,11 +1387,14 @@
       var same = 0, l = Math.min(prevInput.length, text.length);
       while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same;
 
-      operation(cm, applyTextInput)(cm, text.slice(same), prevInput.length - same);
+      var self = this;
+      runInOp(cm, function() {
+        applyTextInput(cm, text.slice(same), prevInput.length - same);
 
-      // Don't leave long text in the textarea, since it makes further polling slow
-      if (text.length > 1000 || text.indexOf("\n") > -1) input.value = this.prevInput = "";
-      else this.prevInput = text;
+        // 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;
+      });
       return true;
     },
 

From 6873df1e6b5f9cc0cf414e56dba81e18bf0cea97 Mon Sep 17 00:00:00 2001
From: mihailik 
Date: Wed, 28 Jan 2015 12:25:26 +0000
Subject: [PATCH 35/47] Fix for copying large chunks of text

Selecting and copying a large chunk of text places a single dash into clipboard.

This is related to line 1285, where the input's content is trimmed for performance:

https://github.com/codemirror/CodeMirror/blob/mobile/lib/codemirror.js#L1285

    minimal = hasCopyEvent &&
      (range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000);
    var content = minimal ? "-" : selected || cm.getSelection();

The problem is apparently a typo during refactoring/renaming from 'd' on the master branch to 'input' here.
---
 lib/codemirror.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index cca917c278..6033a69736 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -1222,7 +1222,7 @@
       function prepareCopyCut(e) {
         if (cm.somethingSelected()) {
           lastCopied = cm.getSelections();
-          if (this.inaccurateSelection) {
+          if (input.inaccurateSelection) {
             input.prevInput = "";
             input.inaccurateSelection = false;
             te.value = lastCopied.join("\n");

From 926ce1161dc56ffb4e9623c0b42f0c57bf2f652e Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Mon, 2 Feb 2015 22:29:04 +0100
Subject: [PATCH 36/47] Don't try to use contenteditable=plaintext-only

Since Mobile Chrome's clipboard API is broken and does not let us
intercept paste events, we need full contenteditable to be able
to paste code with newlines.

Issue #3062
---
 lib/codemirror.js | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index 6033a69736..4a15e8308e 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -1495,9 +1495,7 @@
     init: function(display) {
       var input = this, cm = input.cm;
       var div = input.div = display.lineDiv;
-      try { div.contentEditable = "plaintext-only"; }
-      catch(e) {} // It's nice if this works, since it guards against some weird editing possiblities, but not essential
-      if (div.contentEditable != "plaintext-only") div.contentEditable = "true";
+      div.contentEditable = "true";
       disableBrowserMagic(div);
 
       on(div, "paste", function(e) {

From 7ae24f7c6cc3880869da1f0c838d5def962b5047 Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Wed, 11 Feb 2015 16:52:13 +0100
Subject: [PATCH 37/47] [contenteditable input] Fix bug in pollContent

---
 lib/codemirror.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index 4a15e8308e..b200d66e81 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -1722,7 +1722,7 @@
         ++cutEnd;
 
       newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd);
-      newText[0] = newTop.slice(cutFront);
+      newText[0] = newText[0].slice(cutFront);
 
       var chFrom = Pos(fromLine, cutFront);
       var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0);

From 23486577016c5f0e040064fc5944ce7f78cc1dac Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Wed, 11 Feb 2015 16:52:51 +0100
Subject: [PATCH 38/47] [contenteditable input] Fix selection adjustment for
 compositionstart

---
 lib/codemirror.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index b200d66e81..889192d160 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -1513,7 +1513,7 @@
         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 < 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));
       });

From 6ef5f8be8a51784faada2384bba8f51a9132f64b Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Wed, 11 Feb 2015 17:04:47 +0100
Subject: [PATCH 39/47] [contenteditable input] Fix compositionend
 data-overwriting exception

---
 lib/codemirror.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index 889192d160..c478245335 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -1523,7 +1523,8 @@
       on(div, "compositionend", function(e) {
         var ours = input.composing;
         if (!ours) return;
-        if (e.data != e.startData && !/\200b/.test(e.data)) ours.data = e.data;
+        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.

From 5cf6e8718b3016c6946b77ca3c472318d7b1d37e Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Wed, 11 Feb 2015 17:27:44 +0100
Subject: [PATCH 40/47] [contenteditable input] Force composition end on
 touchstart event

---
 lib/codemirror.js | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index c478245335..46c0b2d105 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -1536,6 +1536,10 @@
         }, 50);
       });
 
+      on(div, "touchstart", function() {
+        input.forceCompositionEnd();
+      });
+
       on(div, "input", function() {
         if (input.composing) return;
         if (!input.pollContent())
@@ -1733,12 +1737,13 @@
     },
 
     ensurePolled: function() {
-      if (this.composing && !this.composing.handled) this.forceCompositionEnd();
+      this.forceCompositionEnd();
     },
     reset: function() {
-      if (this.composing && !this.composing.handled) this.forceCompositionEnd();
+      this.forceCompositionEnd();
     },
     forceCompositionEnd: function() {
+      if (!this.composing || this.composing.handled) return;
       this.applyComposition(this.composing);
       this.composing.handled = true;
       this.div.blur();

From 2ec77379cae678c7cedcda9a04d492eb7ae6e079 Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Thu, 12 Feb 2015 13:22:36 +0100
Subject: [PATCH 41/47] Handle double- and triple-tap events ourselves

Since preventDefaulting the first touchend event means the native
behavior won't happen anymore, this is seems to be the only way
to make multi-tap work for iOS.
---
 lib/codemirror.js | 43 ++++++++++++++++++++++++++++++-------------
 1 file changed, 30 insertions(+), 13 deletions(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index 46c0b2d105..d2dffd1eaa 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -220,7 +220,7 @@
     // was opened.
     d.selForContextMenu = null;
 
-    d.touchActive = false;
+    d.activeTouch = null;
 
     input.init(d);
   }
@@ -3287,34 +3287,51 @@
     if (!captureRightClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);});
 
     // Used to suppress mouse event handling when a touch happens
-    var touchFinished;
+    var touchFinished, prevTouch = {end: 0};
     function finishTouch() {
-      if (d.touchActive) touchFinished = setTimeout(function() {d.touchActive = false;}, 1000);
+      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 (!isMouseLikeTouchEvent(e)) {
         clearTimeout(touchFinished);
-        d.touchActive = {};
+        var now = +new Date;
+        d.activeTouch = {start: now, moved: false,
+                         prev: now - prevTouch.end <= 300 ? prevTouch : null};
         if (e.touches.length == 1) {
-          d.touchActive.left = e.touches[0].pageX;
-          d.touchActive.top = e.touches[0].pageY;
-          d.touchActive.time = Date.now();
+          d.activeTouch.left = e.touches[0].pageX;
+          d.activeTouch.top = e.touches[0].pageY;
         }
       }
     });
     on(d.scroller, "touchmove", function() {
-      if (d.touchActive) d.touchActive.moved = true;
+      if (d.activeTouch) d.activeTouch.moved = true;
     });
     on(d.scroller, "touchend", function(e) {
-      if (d.touchActive && !eventInWidget(d, e) &&
-          !d.touchActive.moved &&
-          d.touchActive.time != null && Date.now() - d.touchActive.time < 300) {
-        cm.setCursor(cm.coordsChar(d.touchActive, "page"));
+      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);
       }
@@ -3407,7 +3424,7 @@
   // not interfere with, such as a scrollbar or widget.
   function onMouseDown(e) {
     var cm = this, display = cm.display;
-    if (display.touchActive && display.input.supportsTouch() || signalDOMEvent(cm, e)) return;
+    if (display.activeTouch && display.input.supportsTouch() || signalDOMEvent(cm, e)) return;
     display.shift = e.shiftKey;
 
     if (eventInWidget(display, e)) {

From 025d1a8e5f39619580465e622a2b4408fde0bec7 Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Thu, 12 Feb 2015 13:23:32 +0100
Subject: [PATCH 42/47] [contenteditable input] Only return true from
 pollContent when an actual change was found

So that styling content causes a redraw, removing the style
---
 lib/codemirror.js | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index d2dffd1eaa..ba8ad3d01d 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -1731,9 +1731,10 @@
 
       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))
+      if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) {
         replaceRange(cm.doc, newText, chFrom, chTo, "+input");
-      return true;
+        return true;
+      }
     },
 
     ensurePolled: function() {

From b6fed250db7fec16e9802ab20cbea6867e107ac3 Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Thu, 12 Feb 2015 14:22:45 +0100
Subject: [PATCH 43/47] [contenteditable display] Fix polling for collapsed
 text

---
 lib/codemirror.js | 40 ++++++++++++++++++++++++++++++----------
 1 file changed, 30 insertions(+), 10 deletions(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index ba8ad3d01d..4ffe7e6f2d 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -1480,7 +1480,9 @@
       }
     },
 
-    setUneditable: nothing
+    setUneditable: nothing,
+
+    needsContentAttribute: false
   }, TextareaInput.prototype);
 
   // CONTENTEDITABLE INPUT STYLE
@@ -1690,11 +1692,11 @@
       var from = sel.from(), to = sel.to();
       if (from.line < display.viewFrom || to.line > display.viewTo - 1) return false;
 
-      if (from.line == display.viewFrom) {
-        var fromLine = from.line;
+      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 fromIndex = findViewIndex(cm, from.line);
         var fromLine = lineNo(display.view[fromIndex].line);
         var fromNode = display.view[fromIndex - 1].node.nextSibling;
       }
@@ -1707,7 +1709,7 @@
         var toNode = display.view[toIndex + 1].node.previousSibling;
       }
 
-      var newText = splitLines(domTextBetween(fromNode, toNode));
+      var newText = 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--; }
@@ -1765,7 +1767,9 @@
     },
 
     onContextMenu: nothing,
-    resetPosition: nothing
+    resetPosition: nothing,
+
+    needsContentAttribute: true
   }, ContentEditableInput.prototype);
 
   function posToDOM(cm, pos) {
@@ -1827,7 +1831,7 @@
         var map = i < 0 ? measure.map : maps[i];
         for (var j = 0; j < map.length; j += 3) {
           if (map[j + 2] == textNode || map[j + 2] == topNode) {
-            var line = lineNo(i < 0 ? lineView.line : lineView.other[i]);
+            var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]);
             return Pos(line, offset < 0 ? map[j + 1] : map[j] + (map[j + 2] == textNode ? offset : 0));
           }
         }
@@ -1853,17 +1857,25 @@
     }
   }
 
-  function domTextBetween(from, to) {
+  function domTextBetween(cm, from, to, fromLine, toLine) {
     var text = "", closing = false;
+    function recognizeMarker(id) { return function(marker) { return marker.id == id; }; }
     function walk(node) {
       if (node.nodeType == 1) {
-        if (node.getAttribute("contenteditable") == "false") return;
         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("\n");
+          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))
@@ -5822,10 +5834,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);
 
@@ -6834,8 +6849,13 @@
 
   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);
     }

From 9914eb78b1e5f8da7bf5f908f2c33d49273ef278 Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Fri, 20 Feb 2015 16:02:00 +0100
Subject: [PATCH 44/47] [contenteditable input] Fix another bug in pollContent

Typing the same character multiple times in a row would
cause a character to be deleted.
---
 lib/codemirror.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index 4ffe7e6f2d..795300f590 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -1722,7 +1722,7 @@
       while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront))
         ++cutFront;
       var newBot = lst(newText), oldBot = lst(oldText);
-      var maxCutEnd = Math.max(newBot.length - (newText.length == 1 ? cutFront : 0),
+      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))

From 8f74e74366a690825a259caaa10b90d616aa2763 Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Fri, 20 Feb 2015 17:30:12 +0100
Subject: [PATCH 45/47] [contenteditable input] Fix selection-to-pos hack when
 node is a span around a text node

---
 lib/codemirror.js | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/lib/codemirror.js b/lib/codemirror.js
index 795300f590..33356c8840 100644
--- a/lib/codemirror.js
+++ b/lib/codemirror.js
@@ -1823,6 +1823,10 @@
     }
 
     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;
 
@@ -1830,9 +1834,12 @@
       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) {
-          if (map[j + 2] == textNode || map[j + 2] == topNode) {
+          var curNode = map[j + 2];
+          if (curNode == textNode || curNode == topNode) {
             var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]);
-            return Pos(line, offset < 0 ? map[j + 1] : map[j] + (map[j + 2] == textNode ? offset : 0));
+            var ch = map[j] + offset;
+            if (offset < 0 || curNode != textNode) ch = map[j + (offset ? 1 : 0)];
+            return Pos(line, ch);
           }
         }
       }

From c1e30b8cc3ab741912f33b981ce00fbee15130cd Mon Sep 17 00:00:00 2001
From: Marijn Haverbeke 
Date: Fri, 20 Feb 2015 17:30:23 +0100
Subject: [PATCH 46/47] Document inputStyle option

---
 doc/manual.html | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/doc/manual.html b/doc/manual.html
index c77a71f9cc..9f3fa36763 100644
--- a/doc/manual.html
+++ b/doc/manual.html
@@ -63,7 +63,7 @@
 

User manual and reference guide - version 4.13.0 + version 5.0.0

CodeMirror is a code-editor component that can be embedded in @@ -333,6 +333,17 @@

Module loaders

option is set to true, it will be covered by an element with class CodeMirror-gutter-filler. +
inputStyle: string
+
Selects the way CodeMirror handles input and focus. The core + library defines the "textarea" + and "contenteditable" input models. On mobile + browsers, the default is "contenteditable". On + desktop browsers, the default is "textarea". + Support for IME and screen readers is better in + the "contenteditable" model. The intention is to + make it the default on modern desktop browsers in the + future.
+
readOnly: boolean|string
This disables editing of the editor content by the user. If the special value "nocursor" is given (instead of From 289b6472d4a0d41ea6028416d4942794c98856f7 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Fri, 20 Feb 2015 17:46:43 +0100 Subject: [PATCH 47/47] Mark release 5.0 --- bin/release | 2 +- bower.json | 2 +- doc/compress.html | 1 + doc/manual.html | 7 +++++-- doc/releases.html | 10 ++++++++++ index.html | 8 ++++---- lib/codemirror.js | 2 +- package.json | 2 +- 8 files changed, 24 insertions(+), 10 deletions(-) diff --git a/bin/release b/bin/release index c6b97c1c03..9b58b872d8 100755 --- a/bin/release +++ b/bin/release @@ -42,5 +42,5 @@ rewrite("doc/compress.html", function(cmp) { rewrite("index.html", function(index) { return index.replace(/\.zip">\d+\.\d+<\/a>/, - ".zip>" + simple + ""); + ".zip\">" + simple + ""); }); diff --git a/bower.json b/bower.json index dad8fe2e02..c59f2d9d27 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "codemirror", - "version":"4.13.0", + "version":"5.0.0", "main": ["lib/codemirror.js", "lib/codemirror.css"], "ignore": [ "**/.*", diff --git a/doc/compress.html b/doc/compress.html index 7d89eb4520..a376c86d2d 100644 --- a/doc/compress.html +++ b/doc/compress.html @@ -36,6 +36,7 @@

Version: