X-Git-Url: https://git.mdrn.pl/redakcja.git/blobdiff_plain/9b25359a45e5498ac09b51b96919d515c1cd5d15..28cd732838488b94daa3ff66dbb8a22a11de459e:/redakcja/static/js/lib/codemirror/editor.js?ds=sidebyside diff --git a/redakcja/static/js/lib/codemirror/editor.js b/redakcja/static/js/lib/codemirror/editor.js index 4eb6d099..07410d22 100644 --- a/redakcja/static/js/lib/codemirror/editor.js +++ b/redakcja/static/js/lib/codemirror/editor.js @@ -4,13 +4,22 @@ * plain sequences of and
elements */ +var internetExplorer = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent); +var webkit = /AppleWebKit/.test(navigator.userAgent); +var safari = /Apple Computers, Inc/.test(navigator.vendor); +var gecko = /gecko\/(\d{8})/i.test(navigator.userAgent); +// TODO this is related to the backspace-at-end-of-line bug. Remove +// this if Opera gets their act together, make the version check more +// broad if they don't. +var brokenOpera = window.opera && /Version\/10.[56]/.test(navigator.userAgent); + // Make sure a string does not contain two consecutive 'collapseable' // whitespace characters. function makeWhiteSpace(n) { var buffer = [], nb = true; for (; n > 0; n--) { buffer.push((nb || n == 1) ? nbsp : " "); - nb = !nb; + nb ^= true; } return buffer.join(""); } @@ -19,12 +28,12 @@ function makeWhiteSpace(n) { // by the browser, but will not break text-wrapping either. function fixSpaces(string) { if (string.charAt(0) == " ") string = nbsp + string.slice(1); - return string.replace(/\t/g, function(){return makeWhiteSpace(indentUnit);}) + return string.replace(/\t/g, function() {return makeWhiteSpace(indentUnit);}) .replace(/[ \u00a0]{2,}/g, function(s) {return makeWhiteSpace(s.length);}); } function cleanText(text) { - return text.replace(/\u00a0/g, " ").replace(/\u200b/g, ""); + return text.replace(/\u00a0/g, " "); } // Create a SPAN node with the expected properties for document part @@ -41,22 +50,6 @@ function makePartSpan(value, doc) { return span; } -// On webkit, when the last BR of the document does not have text -// behind it, the cursor can not be put on the line after it. This -// makes pressing enter at the end of the document occasionally do -// nothing (or at least seem to do nothing). To work around it, this -// function makes sure the document ends with a span containing a -// zero-width space character. The traverseDOM iterator filters such -// character out again, so that the parsers won't see them. This -// function is called from a few strategic places to make sure the -// zwsp is restored after the highlighting process eats it. -var webkitLastLineHack = webkit ? - function(container) { - var last = container.lastChild; - if (!last || !last.isPart || last.textContent != "\u200b") - container.appendChild(makePartSpan("\u200b", container.ownerDocument)); - } : function() {}; - var Editor = (function(){ // The HTML elements whose content should be suffixed by a newline // when converting them to flat text. @@ -76,17 +69,17 @@ var Editor = (function(){ function simplifyNode(node, top) { if (node.nodeType == 3) { - var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/[\r\u200b]/g, "").replace(/\n/g, " ")); + var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/\r/g, "").replace(/\n/g, " ")); if (text.length) leaving = false; result.push(node); } - else if (node.nodeName == "BR" && node.childNodes.length == 0) { + else if (isBR(node) && node.childNodes.length == 0) { leaving = true; result.push(node); } else { - forEach(node.childNodes, simplifyNode); - if (!leaving && newlineElements.hasOwnProperty(node.nodeName)) { + for (var n = node.firstChild; n; n = n.nextSibling) simplifyNode(n); + if (!leaving && newlineElements.hasOwnProperty(node.nodeName.toUpperCase())) { leaving = true; if (!atEnd || !top) result.push(doc.createElement("BR")); @@ -103,13 +96,7 @@ var Editor = (function(){ // the nodes. It makes sure that all nodes up to and including the // one whose text is being yielded have been 'normalized' to be just // and
elements. - // See the story.html file for some short remarks about the use of - // continuation-passing style in this iterator. function traverseDOM(start){ - function yield(value, c){cc = c; return value;} - function push(fun, arg, c){return function(){return fun(arg, c);};} - function stop(){cc = stop; throw StopIteration;}; - var cc = push(scanNode, start, stop); var owner = start.ownerDocument; var nodeQueue = []; @@ -124,6 +111,11 @@ var Editor = (function(){ } var point = null; + // This an Opera-specific hack -- always insert an empty span + // between two BRs, because Opera's cursor code gets terribly + // confused when the cursor is between two BRs. + var afterBR = true; + // Insert a normalized node at the current point. If it is a text // node, wrap it in a , and give that span a currentText // property -- this is used to cache the nodeValue, because @@ -136,6 +128,12 @@ var Editor = (function(){ select.snapshotChanged(); part = makePartSpan(part, owner); text = part.currentText; + afterBR = false; + } + else { + if (afterBR && window.opera) + point(makePartSpan("", owner)); + afterBR = true; } part.dirty = true; nodeQueue.push(part); @@ -144,14 +142,13 @@ var Editor = (function(){ } // Extract the text and newlines from a DOM node, insert them into - // the document, and yield the textual content. Used to replace + // the document, and return the textual content. Used to replace // non-normalized nodes. - function writeNode(node, c, end) { - var toYield = []; - forEach(simplifyDOM(node, end), function(part) { - toYield.push(insertPart(part)); - }); - return yield(toYield.join(""), c); + function writeNode(node, end) { + var simplified = simplifyDOM(node, end); + for (var i = 0; i < simplified.length; i++) + simplified[i] = insertPart(simplified[i]); + return simplified.join(""); } // Check whether a node is a normalized element. @@ -163,55 +160,54 @@ var Editor = (function(){ return false; } - // Handle a node. Add its successor to the continuation if there - // is one, find out whether the node is normalized. If it is, - // yield its content, otherwise, normalize it (writeNode will take - // care of yielding). - function scanNode(node, c){ - if (node.nextSibling) - c = push(scanNode, node.nextSibling, c); + // Advance to next node, return string for current node. + function next() { + if (!start) throw StopIteration; + var node = start; + start = node.nextSibling; if (partNode(node)){ nodeQueue.push(node); - return yield(node.currentText, c); + afterBR = false; + return node.currentText; } - else if (node.nodeName == "BR") { + else if (isBR(node)) { + if (afterBR && window.opera) + node.parentNode.insertBefore(makePartSpan("", owner), node); nodeQueue.push(node); - return yield("\n", c); + afterBR = true; + return "\n"; } else { var end = !node.nextSibling; point = pointAt(node); removeElement(node); - return writeNode(node, c, end); + return writeNode(node, end); } } // MochiKit iterators are objects with a next function that // returns the next value or throws StopIteration when there are // no more values. - return {next: function(){return cc();}, nodes: nodeQueue}; + return {next: next, nodes: nodeQueue}; } // Determine the text size of a processed node. function nodeSize(node) { - if (node.nodeName == "BR") - return 1; - else - return node.currentText.length; + return isBR(node) ? 1 : node.currentText.length; } // Search backwards through the top-level nodes until the next BR or // the start of the frame. function startOfLine(node) { - while (node && node.nodeName != "BR") node = node.previousSibling; + while (node && !isBR(node)) node = node.previousSibling; return node; } function endOfLine(node, container) { if (!node) node = container.firstChild; - else if (node.nodeName == "BR") node = node.nextSibling; + else if (isBR(node)) node = node.nextSibling; - while (node && node.nodeName != "BR") node = node.nextSibling; + while (node && !isBR(node)) node = node.nextSibling; return node; } @@ -223,30 +219,13 @@ var Editor = (function(){ // indicating whether anything was found, and can be called again to // skip to the next find. Use the select and replace methods to // actually do something with the found locations. - function SearchCursor(editor, string, fromCursor, regexp, case_sensitive) { - - function casedIndexOf(hay, needle, case_sensitive) { - if (case_sensitive) - return hay.indexOf(needle); - else - return hay.toLowerCase().indexOf(needle.toLowerCase()) - } - - function casedLastIndexOf(hay, needle, case_sensitive) { - if (case_sensitive) - return hay.lastIndexOf(needle); - else - return hay.toLowerCase().lastIndexOf(needle.toLowerCase()); - } - - function casedEqual(a, b, case_sensitive) { - if (case_sensitive) - return a == b; - else - return a.toLowerCase() == b.toLowerCase(); - } - + function SearchCursor(editor, string, fromCursor, caseFold) { this.editor = editor; + if (caseFold == undefined) { + caseFold = (string == string.toLowerCase()); + } + this.caseFold = caseFold; + if (caseFold) string = string.toLowerCase(); this.history = editor.history; this.history.commit(); @@ -274,8 +253,8 @@ var Editor = (function(){ // For one-line strings, searching can be done simply by calling // indexOf on the current line. function() { - var match = casedIndexOf(cleanText(self.history.textAfter(self.line).slice(self.offset)), - string, case_sensitive); + var line = cleanText(self.history.textAfter(self.line).slice(self.offset)); + var match = (self.caseFold ? line.toLowerCase() : line).indexOf(string); if (match > -1) return {from: {node: self.line, offset: self.offset + match}, to: {node: self.line, offset: self.offset + match + string.length}}; @@ -285,19 +264,21 @@ var Editor = (function(){ // end of the line and the last match starts at the start. function() { var firstLine = cleanText(self.history.textAfter(self.line).slice(self.offset)); - var match = casedLastIndexOf(firstLine, target[0], case_sensitive); + var match = (self.caseFold ? firstLine.toLowerCase() : firstLine).lastIndexOf(target[0]); if (match == -1 || match != firstLine.length - target[0].length) return false; var startOffset = self.offset + match; var line = self.history.nodeAfter(self.line); for (var i = 1; i < target.length - 1; i++) { - if (!casedEqual(cleanText(self.history.textAfter(line)), target[i], case_sensitive)) + var lineText = cleanText(self.history.textAfter(line)); + if ((self.caseFold ? lineText.toLowerCase() : lineText) != target[i]) return false; line = self.history.nodeAfter(line); } - if (casedIndexOf(cleanText(self.history.textAfter(line)), target[target.length - 1], case_sensitive) != 0) + var lastLine = cleanText(self.history.textAfter(line)); + if ((self.caseFold ? lastLine.toLowerCase() : lastLine).indexOf(target[target.length - 1]) != 0) return false; return {from: {node: self.line, offset: startOffset}, @@ -374,8 +355,7 @@ var Editor = (function(){ this.doc = document; var container = this.container = this.doc.body; this.win = window; - this.history = new History(container, options.undoDepth, options.undoDelay, - this, options.onChange); + this.history = new UndoHistory(container, options.undoDepth, options.undoDelay, this); var self = this; if (!Editor.Parser) @@ -387,10 +367,8 @@ var Editor = (function(){ select.setCursorPos(container, {node: null, offset: 0}); this.dirty = []; - if (options.content) - this.importCode(options.content); - else // FF acts weird when the editable document is completely empty - container.appendChild(this.doc.createElement("BR")); + this.importCode(options.content || ""); + this.history.onChange = options.onChange; if (!options.readOnly) { if (options.continuousScanning !== false) { @@ -399,8 +377,10 @@ var Editor = (function(){ } function setEditable() { - // In IE, designMode frames can not run any scripts, so we use - // contentEditable instead. + // Use contentEditable instead of designMode on IE, since designMode frames + // can not run any scripts. It would be nice if we could use contentEditable + // everywhere, but it is significantly flakier than designMode on every + // single non-IE browser. if (document.body.contentEditable != undefined && internetExplorer) document.body.contentEditable = "true"; else @@ -432,6 +412,11 @@ var Editor = (function(){ addEventHandler(document.body, "mouseup", cursorActivity); addEventHandler(document.body, "cut", cursorActivity); + // workaround for a gecko bug [?] where going forward and then + // back again breaks designmode (no more cursor) + if (gecko) + addEventHandler(this.win, "pagehide", function(){self.unloaded = true;}); + addEventHandler(document.body, "paste", function(event) { cursorActivity(); var text = null; @@ -441,15 +426,14 @@ var Editor = (function(){ } catch(e) {} if (text !== null) { - self.replaceSelection(text); event.stop(); + self.replaceSelection(text); + select.scrollToCursor(self.container); } }); - addEventHandler(document.body, "beforepaste", method(this, "reroutePasteEvent")); - if (this.options.autoMatchParens) - addEventHandler(document.body, "click", method(this, "scheduleParenBlink")); + addEventHandler(document.body, "click", method(this, "scheduleParenHighlight")); } else if (!options.textWrapping) { container.style.whiteSpace = "nowrap"; @@ -476,7 +460,6 @@ var Editor = (function(){ var accum = []; select.markSelection(this.win); forEach(traverseDOM(this.container.firstChild), method(accum, "push")); - webkitLastLineHack(this.container); select.selectMarked(); return cleanText(accum.join("")); }, @@ -514,6 +497,16 @@ var Editor = (function(){ return startOfLine(line.previousSibling); }, + visibleLineCount: function() { + var line = this.container.firstChild; + while (line && isBR(line)) line = line.nextSibling; // BR heights are unreliable + if (!line) return false; + var innerHeight = (window.innerHeight + || document.documentElement.clientHeight + || document.body.clientHeight); + return Math.floor(innerHeight / line.offsetHeight); + }, + selectLines: function(startLine, startOffset, endLine, endOffset) { this.checkLine(startLine); var start = {node: startLine, offset: startOffset}, end = null; @@ -526,10 +519,9 @@ var Editor = (function(){ }, lineContent: function(line) { - this.checkLine(line); var accum = []; for (line = line ? line.nextSibling : this.container.firstChild; - line && line.nodeName != "BR"; line = line.nextSibling) + line && !isBR(line); line = line.nextSibling) accum.push(nodeText(line)); return cleanText(accum.join("")); }, @@ -543,6 +535,18 @@ var Editor = (function(){ this.scheduleHighlight(); }, + removeLine: function(line) { + var node = line ? line.nextSibling : this.container.firstChild; + while (node) { + var next = node.nextSibling; + removeElement(node); + if (isBR(node)) break; + node = next; + } + this.addDirtyNode(line); + this.scheduleHighlight(); + }, + insertIntoLine: function(line, position, content) { var before = null; if (position == "end") { @@ -554,7 +558,7 @@ var Editor = (function(){ before = cur; break; } - var text = (cur.innerText || cur.textContent || cur.nodeValue || ""); + var text = nodeText(cur); if (text.length > position) { before = cur.nextSibling; content = text.slice(0, position) + content + text.slice(position); @@ -593,8 +597,7 @@ var Editor = (function(){ return cleanText(text.join("\n")); }, - // Replace the selection with another p - // iece of text. + // Replace the selection with another piece of text. replaceSelection: function(text) { this.history.commit(); @@ -603,20 +606,58 @@ var Editor = (function(){ if (!start || !end) return; end = this.replaceRange(start, end, text); - // select.setCursorPos(this.container, end); - webkitLastLineHack(this.container); + select.setCursorPos(this.container, end); + }, + + cursorCoords: function(start) { + var sel = select.cursorPos(this.container, start); + if (!sel) return null; + var off = sel.offset, node = sel.node, doc = this.win.document, self = this; + function measureFromNode(node, xOffset) { + var y = -(self.win.document.body.scrollTop || self.win.document.documentElement.scrollTop || 0), + x = -(self.win.document.body.scrollLeft || self.win.document.documentElement.scrollLeft || 0) + xOffset; + forEach([node, self.win.frameElement], function(n) { + while (n) {x += n.offsetLeft; y += n.offsetTop;n = n.offsetParent;} + }); + return {x: x, y: y, yBot: y + node.offsetHeight}; + } + function withTempNode(text, f) { + var node = doc.createElement("SPAN"); + node.appendChild(doc.createTextNode(text)); + try {return f(node);} + finally {if (node.parentNode) node.parentNode.removeChild(node);} + } + + while (off) { + node = node ? node.nextSibling : this.container.firstChild; + var txt = nodeText(node); + if (off < txt.length) + return withTempNode(txt.substr(0, off), function(tmp) { + tmp.style.position = "absolute"; tmp.style.visibility = "hidden"; + tmp.className = node.className; + self.container.appendChild(tmp); + return measureFromNode(node, tmp.offsetWidth); + }); + off -= txt.length; + } + if (node && isSpan(node)) + return measureFromNode(node, node.offsetWidth); + else if (node && node.nextSibling && isSpan(node.nextSibling)) + return measureFromNode(node.nextSibling, 0); + else + return withTempNode("\u200b", function(tmp) { + if (node) node.parentNode.insertBefore(tmp, node.nextSibling); + else self.container.insertBefore(tmp, self.container.firstChild); + return measureFromNode(tmp, 0); + }); }, reroutePasteEvent: function() { if (this.capturingPaste || window.opera) return; this.capturingPaste = true; - var te = parent.document.createElement("TEXTAREA"); - te.style.position = "absolute"; - te.style.left = "-500px"; - te.style.width = "10px"; - te.style.top = nodeTop(frameElement) + "px"; - parent.document.body.appendChild(te); + var te = window.frameElement.CodeMirror.textareaHack; parent.focus(); + te.value = ""; te.focus(); var self = this; @@ -624,10 +665,12 @@ var Editor = (function(){ self.capturingPaste = false; self.win.focus(); if (self.selectionSnapshot) // IE hack - self.win.select.selectCoords(self.win, self.selectionSnapshot); + self.win.select.setBookmark(self.container, self.selectionSnapshot); var text = te.value; - if (text) self.replaceSelection(text); - removeElement(te); + if (text) { + self.replaceSelection(text); + select.scrollToCursor(self.container); + } }, 10); }, @@ -642,8 +685,8 @@ var Editor = (function(){ offset: lastLine.length}; }, - getSearchCursor: function(string, fromCursor, regexp, case_sensitive) { - return new SearchCursor(this, string, fromCursor, regexp, case_sensitive); + getSearchCursor: function(string, fromCursor, caseFold) { + return new SearchCursor(this, string, fromCursor, caseFold); }, // Re-indent the whole buffer @@ -670,11 +713,14 @@ var Editor = (function(){ }, ungrabKeys: function() { this.frozen = "leave"; - this.keyFilter = null; }, - setParser: function(name) { + setParser: function(name, parserConfig) { Editor.Parser = window[name]; + parserConfig = parserConfig || this.options.parserConfig; + if (parserConfig && Editor.Parser.configure) + Editor.Parser.configure(parserConfig); + if (this.container.firstChild) { forEach(this.container.childNodes, function(n) { if (n.nodeType != 3) n.dirty = true; @@ -686,19 +732,19 @@ var Editor = (function(){ // Intercept enter and tab, and assign their new functions. keyDown: function(event) { - if (this.frozen == "leave") this.frozen = null; - if (this.frozen && (!this.keyFilter || this.keyFilter(event))) { + if (this.frozen == "leave") {this.frozen = null; this.keyFilter = null;} + if (this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode, event))) { event.stop(); this.frozen(event); return; } - var code = event.keyCode; + var code = this.lastKeyDownCode = event.keyCode; // Don't scan when the user is typing. this.delayScanning(); // Schedule a paren-highlight event, if configured. if (this.options.autoMatchParens) - this.scheduleParenBlink(); + this.scheduleParenHighlight(); // The various checks for !altKey are there because AltGr sets both // ctrlKey and altKey to true, and should not be recognised as @@ -714,8 +760,8 @@ var Editor = (function(){ } event.stop(); } - else if (code == 9 && this.options.tabMode != "default") { // tab - this.handleTab(!event.ctrlKey && !event.shiftKey); + else if (code == 9 && this.options.tabMode != "default" && !event.ctrlKey) { // tab + this.handleTab(!event.shiftKey); event.stop(); } else if (code == 32 && event.shiftKey && this.options.tabMode == "default") { // space @@ -723,23 +769,32 @@ var Editor = (function(){ event.stop(); } else if (code == 36 && !event.shiftKey && !event.ctrlKey) { // home - if (this.home()) - event.stop(); + if (this.home()) event.stop(); + } + else if (code == 35 && !event.shiftKey && !event.ctrlKey) { // end + if (this.end()) event.stop(); + } + // Only in Firefox is the default behavior for PgUp/PgDn correct. + else if (code == 33 && !event.shiftKey && !event.ctrlKey && !gecko) { // PgUp + if (this.pageUp()) event.stop(); + } + else if (code == 34 && !event.shiftKey && !event.ctrlKey && !gecko) { // PgDn + if (this.pageDown()) event.stop(); } else if ((code == 219 || code == 221) && event.ctrlKey && !event.altKey) { // [, ] - this.blinkParens(event.shiftKey); + this.highlightParens(event.shiftKey, true); event.stop(); } else if (event.metaKey && !event.shiftKey && (code == 37 || code == 39)) { // Meta-left/right var cursor = select.selectionTopNode(this.container); - if (cursor === false || !this.container.firstChild) return; - - if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container); - else { - var end = endOfLine(cursor, this.container); - select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container); + if (cursor !== false && this.container.firstChild) { + if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container); + else { + var end = endOfLine(cursor, this.container); + select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container); + } + event.stop(); } - event.stop(); } else if ((event.ctrlKey || event.metaKey) && !event.altKey) { if ((event.shiftKey && code == 90) || code == 89) { // shift-Z, Y @@ -754,31 +809,49 @@ var Editor = (function(){ this.options.saveFunction(); event.stop(); } + else if (internetExplorer && code == 86) { + this.reroutePasteEvent(); + } } + this.keyUpOrPressAfterLastKeyDown = false; }, // Check for characters that should re-indent the current line, // and prevent Opera from handling enter and tab anyway. keyPress: function(event) { + this.keyUpOrPressAfterLastKeyDown = true; var electric = Editor.Parser.electricChars, self = this; // Hack for Opera, and Firefox on OS X, in which stopping a // keydown event does not prevent the associated keypress event // from happening, so we have to cancel enter and tab again // here. - if ((this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode))) || + if ((this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode || event.code, event))) || event.code == 13 || (event.code == 9 && this.options.tabMode != "default") || - (event.keyCode == 32 && event.shiftKey && this.options.tabMode == "default")) + (event.code == 32 && event.shiftKey && this.options.tabMode == "default")) event.stop(); else if (electric && electric.indexOf(event.character) != -1) this.parent.setTimeout(function(){self.indentAtCursor(null);}, 0); else if ((event.character == "v" || event.character == "V") && (event.ctrlKey || event.metaKey) && !event.altKey) // ctrl-V this.reroutePasteEvent(); + // Work around a bug where pressing backspace at the end of a + // line often causes the cursor to jump to the start of the line + // in Opera 10.60. + else if (brokenOpera && event.code == 8) { + var sel = select.selectionTopNode(this.container), self = this, + next = sel ? sel.nextSibling : this.container.firstChild; + if (sel !== false && next && isBR(next)) + this.parent.setTimeout(function(){ + if (select.selectionTopNode(self.container) == next) + select.focusAfterNode(next.previousSibling, self.container); + }, 20); + } }, // Mark the node at the cursor dirty when a non-safe key is // released. keyUp: function(event) { + this.keyUpOrPressAfterLastKeyDown = true; this.cursorActivity(isSafeKey(event.keyCode)); }, @@ -828,6 +901,7 @@ var Editor = (function(){ if (whiteSpace) { whiteSpace.currentText = makeWhiteSpace(newIndent); whiteSpace.firstChild.nodeValue = whiteSpace.currentText; + select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true); } // Otherwise, we have to add a new whitespace node. else { @@ -835,18 +909,18 @@ var Editor = (function(){ whiteSpace.className = "whitespace"; if (start) insertAfter(whiteSpace, start); else this.container.insertBefore(whiteSpace, this.container.firstChild); + select.snapshotMove(firstText && (firstText.firstChild || firstText), + whiteSpace.firstChild, newIndent, false, true); } - if (firstText) select.snapshotMove(firstText.firstChild, whiteSpace.firstChild, curIndent, false, true); } if (indentDiff != 0) this.addDirtyNode(start); - return whiteSpace; }, // Re-highlight the selected part of the document. highlightAtCursor: function() { var pos = select.selectionTopNode(this.container, true); var to = select.selectionTopNode(this.container, false); - if (pos === false || to === false) return; + if (pos === false || to === false) return false; select.markSelection(this.win); if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false) @@ -865,12 +939,14 @@ var Editor = (function(){ this.reindentSelection(direction); }, + // Custom home behaviour that doesn't land the cursor in front of + // leading whitespace unless pressed twice. home: function() { var cur = select.selectionTopNode(this.container, true), start = cur; - if (cur === false || !(!cur || cur.isPart || cur.nodeName == "BR") || !this.container.firstChild) + if (cur === false || !(!cur || cur.isPart || isBR(cur)) || !this.container.firstChild) return false; - while (cur && cur.nodeName != "BR") cur = cur.previousSibling; + while (cur && !isBR(cur)) cur = cur.previousSibling; var next = cur ? cur.nextSibling : this.container.firstChild; if (next && next != start && next.isPart && hasClass(next, "whitespace")) select.focusAfterNode(next, this.container); @@ -881,18 +957,88 @@ var Editor = (function(){ return true; }, - // Delay (or initiate) the next paren blink event. - scheduleParenBlink: function() { + // Some browsers (Opera) don't manage to handle the end key + // properly in the face of vertical scrolling. + end: function() { + var cur = select.selectionTopNode(this.container, true); + if (cur === false) return false; + cur = endOfLine(cur, this.container); + if (!cur) return false; + select.focusAfterNode(cur.previousSibling, this.container); + select.scrollToCursor(this.container); + return true; + }, + + pageUp: function() { + var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount(); + if (line === false || scrollAmount === false) return false; + // Try to keep one line on the screen. + scrollAmount -= 2; + for (var i = 0; i < scrollAmount; i++) { + line = this.prevLine(line); + if (line === false) break; + } + if (i == 0) return false; // Already at first line + select.setCursorPos(this.container, {node: line, offset: 0}); + select.scrollToCursor(this.container); + return true; + }, + + pageDown: function() { + var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount(); + if (line === false || scrollAmount === false) return false; + // Try to move to the last line of the current page. + scrollAmount -= 2; + for (var i = 0; i < scrollAmount; i++) { + var nextLine = this.nextLine(line); + if (nextLine === false) break; + line = nextLine; + } + if (i == 0) return false; // Already at last line + select.setCursorPos(this.container, {node: line, offset: 0}); + select.scrollToCursor(this.container); + return true; + }, + + // Delay (or initiate) the next paren highlight event. + scheduleParenHighlight: function() { if (this.parenEvent) this.parent.clearTimeout(this.parenEvent); var self = this; - this.parenEvent = this.parent.setTimeout(function(){self.blinkParens();}, 300); + this.parenEvent = this.parent.setTimeout(function(){self.highlightParens();}, 300); }, // Take the token before the cursor. If it contains a character in // '()[]{}', search for the matching paren/brace/bracket, and // highlight them in green for a moment, or red if no proper match // was found. - blinkParens: function(jump) { + highlightParens: function(jump, fromKey) { + var self = this; + // give the relevant nodes a colour. + function highlight(node, ok) { + if (!node) return; + if (self.options.markParen) { + self.options.markParen(node, ok); + } + else { + node.style.fontWeight = "bold"; + node.style.color = ok ? "#8F8" : "#F88"; + } + } + function unhighlight(node) { + if (!node) return; + if (self.options.unmarkParen) { + self.options.unmarkParen(node); + } + else { + node.style.fontWeight = ""; + node.style.color = ""; + } + } + if (!fromKey && self.highlighted) { + unhighlight(self.highlighted[0]); + unhighlight(self.highlighted[1]); + } + if (!window.select) return; // Clear the event property. if (this.parenEvent) this.parent.clearTimeout(this.parenEvent); @@ -910,7 +1056,7 @@ var Editor = (function(){ return /[\(\[\{]/.test(ch); } - var ch, self = this, cursor = select.selectionTopNode(this.container, true); + var ch, cursor = select.selectionTopNode(this.container, true); if (!cursor || !this.highlightAtCursor()) return; cursor = select.selectionTopNode(this.container, true); if (!(cursor && ((ch = paren(cursor)) || (cursor = cursor.nextSibling) && (ch = paren(cursor))))) @@ -923,9 +1069,9 @@ var Editor = (function(){ // have to scan, we just try, and when we find dirty nodes we // abort, parse them, and re-try. function tryFindMatch() { - var stack = [], ch, ok = true;; + var stack = [], ch, ok = true; for (var runner = cursor; runner; runner = dir ? runner.nextSibling : runner.previousSibling) { - if (runner.className == className && runner.nodeName == "SPAN" && (ch = paren(runner))) { + if (runner.className == className && isSpan(runner) && (ch = paren(runner))) { if (forward(ch) == dir) stack.push(ch); else if (!stack.length) @@ -934,18 +1080,12 @@ var Editor = (function(){ ok = false; if (!stack.length) break; } - else if (runner.dirty || runner.nodeName != "SPAN" && runner.nodeName != "BR") { + else if (runner.dirty || !isSpan(runner) && !isBR(runner)) { return {node: runner, status: "dirty"}; } } return {node: runner, status: runner && ok}; } - // Temporarily give the relevant nodes a colour. - function blink(node, ok) { - node.style.fontWeight = "bold"; - node.style.color = ok ? "#8F8" : "#F88"; - self.parent.setTimeout(function() {node.style.fontWeight = ""; node.style.color = "";}, 500); - } while (true) { var found = tryFindMatch(); @@ -957,11 +1097,14 @@ var Editor = (function(){ continue; } else { - blink(cursor, found.status); - if (found.node) { - blink(found.node, found.status); - if (jump) select.focusAfterNode(found.node.previousSibling, this.container); - } + highlight(cursor, found.status); + highlight(found.node, found.status); + if (fromKey) + self.parent.setTimeout(function() {unhighlight(cursor); unhighlight(found.node);}, 500); + else + self.highlighted = [cursor, found.node]; + if (jump && found.node) + select.focusAfterNode(found.node.previousSibling, this.container); break; } } @@ -979,20 +1122,17 @@ var Editor = (function(){ // there's nothing to indent. if (cursor === false) return; - var lineStart = startOfLine(cursor); - var whiteSpace = this.indentLineAfter(lineStart, direction); - if (cursor == lineStart && whiteSpace) - cursor = whiteSpace; - // This means the indentation has probably messed up the cursor. - if (cursor == whiteSpace) - select.focusAfterNode(cursor, this.container); + select.markSelection(this.win); + this.indentLineAfter(startOfLine(cursor), direction); + select.selectMarked(); }, // Indent all lines whose start falls inside of the current // selection. indentRegion: function(start, end, direction) { var current = (start = startOfLine(start)), before = start && startOfLine(start.previousSibling); - if (end.nodeName != "BR") end = endOfLine(end, this.container); + if (!isBR(end)) end = endOfLine(end, this.container); + this.addDirtyNode(start); do { var next = endOfLine(current, this.container); @@ -1007,9 +1147,16 @@ var Editor = (function(){ // Find the node that the cursor is in, mark it as dirty, and make // sure a highlight pass is scheduled. cursorActivity: function(safe) { + // pagehide event hack above + if (this.unloaded) { + this.win.document.designMode = "off"; + this.win.document.designMode = "on"; + this.unloaded = false; + } + if (internetExplorer) { this.container.createTextRange().execCommand("unlink"); - this.selectionSnapshot = select.selectionCoords(this.win); + this.selectionSnapshot = select.getBookmark(this.container); } var activity = this.options.cursorActivity; @@ -1045,6 +1192,10 @@ var Editor = (function(){ this.dirty.push(node); }, + allClean: function() { + return !this.dirty.length; + }, + // Cause a highlight pass to happen in options.passDelay // milliseconds. Clear the existing timeout, if one exists. This // way, the passes do not happen while the user is typing, and @@ -1067,7 +1218,7 @@ var Editor = (function(){ // If the node has been coloured in the meantime, or is no // longer in the document, it should not be returned. while (found && found.parentNode != this.container) - found = found.parentNode + found = found.parentNode; if (found && (found.dirty || found.nodeType == 3)) return found; } catch (e) {} @@ -1084,7 +1235,7 @@ var Editor = (function(){ highlightDirty: function(force) { // Prevent FF from raising an error when it is firing timeouts // on a page that's no longer loaded. - if (!window.select) return; + if (!window.select) return false; if (!this.options.readOnly) select.markSelection(this.win); var start, endTime = force ? null : time() + this.options.passTime; @@ -1127,6 +1278,12 @@ var Editor = (function(){ } }, + isIMEOn: function() { + // chrome: keyDown keyCode is 229 while IME on + // firefox: no keyUps or keyPresses fires after first keyDown while IME on + return this.lastKeyDownCode == 229 || this.keyUpOrPressAfterLastKeyDown === false; + }, + // The function that does the actual highlighting/colouring (with // help from the parser and the DOM normalizer). Its interface is // rather overcomplicated, because it is used in different @@ -1146,18 +1303,18 @@ var Editor = (function(){ var container = this.container, self = this, active = this.options.activeTokens; var endTime = (typeof target == "number" ? target : null); - if (!container.firstChild) - return; + if (!container.firstChild || this.isIMEOn()) + return false; // Backtrack to the first node before from that has a partial // parse stored. while (from && (!from.parserFromHere || from.dirty)) { - if (maxBacktrack != null && from.nodeName == "BR" && (--maxBacktrack) < 0) + if (maxBacktrack != null && isBR(from) && (--maxBacktrack) < 0) return false; from = from.previousSibling; } // If we are at the end of the document, do nothing. if (from && !from.nextSibling) - return; + return false; // Check whether a part ( node) and the corresponding token // match. @@ -1175,7 +1332,7 @@ var Editor = (function(){ } // Create a part corresponding to a given token. function tokenPart(token){ - var part = makePartSpan(token.value, self.doc); + var part = makePartSpan(token.value, self.doc); part.className = token.style; return part; } @@ -1202,6 +1359,11 @@ var Editor = (function(){ stream = stringStream(traversal), parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream); + function surroundedByBRs(node) { + return (node.previousSibling == null || isBR(node.previousSibling)) && + (node.nextSibling == null || isBR(node.nextSibling)); + } + // parts is an interface to make it possible to 'delay' fetching // the next DOM node until we are completely done with the one // before it. This is necessary because often the next node is @@ -1232,13 +1394,23 @@ var Editor = (function(){ // Allow empty nodes when they are alone on a line, needed // for the FF cursor bug workaround (see select.js, // insertNewlineAtCursor). - while (part && part.nodeName == "SPAN" && part.currentText == "") { - var old = part; - this.remove(); - part = this.get(); - // Adjust selection information, if any. See select.js for details. - select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0); + while (part && isSpan(part) && part.currentText == "") { + // Leave empty nodes that are alone on a line alone in + // Opera, since that browsers doesn't deal well with + // having 2 BRs in a row. + if (window.opera && surroundedByBRs(part)) { + this.next(); + part = this.get(); + } + else { + var old = part; + this.remove(); + part = this.get(); + // Adjust selection information, if any. See select.js for details. + select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0); + } } + return part; } }; @@ -1254,7 +1426,7 @@ var Editor = (function(){ if (token.value == "\n"){ // The idea of the two streams actually staying synchronized // is such a long shot that we explicitly check. - if (part.nodeName != "BR") + if (!isBR(part)) throw "Parser out of sync. Expected BR."; if (part.dirty || !part.indentation) lineDirty = true; @@ -1282,7 +1454,7 @@ var Editor = (function(){ parts.next(); } else { - if (part.nodeName != "SPAN") + if (!isSpan(part)) throw "Parser out of sync. Expected SPAN."; if (part.dirty) lineDirty = true; @@ -1323,7 +1495,6 @@ var Editor = (function(){ } }); maybeTouch(from); - webkitLastLineHack(this.container); // The function returns some status information that is used by // hightlightDirty to determine whether and where it has to @@ -1338,6 +1509,6 @@ var Editor = (function(){ addEventHandler(window, "load", function() { var CodeMirror = window.frameElement.CodeMirror; - CodeMirror.editor = new Editor(CodeMirror.options); + var e = CodeMirror.editor = new Editor(CodeMirror.options); this.parent.setTimeout(method(CodeMirror, "init"), 0); });