From 28cd732838488b94daa3ff66dbb8a22a11de459e Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Mon, 6 Sep 2010 11:12:07 +0200 Subject: [PATCH] Codemirror 0.8. fixes #816: editor and error msgs line numbers match, fixes #744: preserve whitespace after slash --- ...rs_15032010.css => xmlcolors_20100906.css} | 26 +- .../static/js/lib/codemirror/codemirror.js | 427 +++++++++++--- redakcja/static/js/lib/codemirror/editor.js | 539 ++++++++++++------ redakcja/static/js/lib/codemirror/parsexml.js | 44 +- redakcja/static/js/lib/codemirror/select.js | 199 ++++--- .../static/js/lib/codemirror/stringstream.js | 2 +- redakcja/static/js/lib/codemirror/tokenize.js | 2 +- redakcja/static/js/lib/codemirror/undo.js | 27 +- redakcja/static/js/lib/codemirror/util.js | 13 +- redakcja/static/js/wiki/view_editor_source.js | 3 +- redakcja/static/js/wiki/view_search.js | 3 +- redakcja/static/js/wiki/xslt.js | 4 +- 12 files changed, 882 insertions(+), 407 deletions(-) rename redakcja/static/css/{xmlcolors_15032010.css => xmlcolors_20100906.css} (69%) diff --git a/redakcja/static/css/xmlcolors_15032010.css b/redakcja/static/css/xmlcolors_20100906.css similarity index 69% rename from redakcja/static/css/xmlcolors_15032010.css rename to redakcja/static/css/xmlcolors_20100906.css index 17277f85..7446450a 100644 --- a/redakcja/static/css/xmlcolors_15032010.css +++ b/redakcja/static/css/xmlcolors_20100906.css @@ -1,11 +1,17 @@ +html { + cursor: text; +} + .editbox { margin: .4em; + padding: 0; + /* fnp changes */ margin-top: 5px; margin-left: 5px; - padding: 0; - font-family:"Lucida Console", monospace; + font-family: "Lucida Console", monospace; font-size: 13px; line-height: 18px; + /* end fnp changes */ color: black; } @@ -13,18 +19,12 @@ margin: 0; } -.editbox span { - display: inline; - font-size: 13px; - line-height: 18px; -} - span.xml-tagname { - color: #0e5f0e; + color: #A0B; } span.xml-attribute { - color: #8b8b8b; + color: #281; } span.xml-punctuation { @@ -32,7 +32,7 @@ span.xml-punctuation { } span.xml-attname { - color: #817aff; + color: #00F; } span.xml-comment { @@ -52,8 +52,10 @@ span.xml-entity { } span.xml-error { - color: #F00; + color: #F00 !important; + /* fnp changes */ font-weight: bolder; + /* end fnp changes */ } span.xml-text { diff --git a/redakcja/static/js/lib/codemirror/codemirror.js b/redakcja/static/js/lib/codemirror/codemirror.js index 8475989c..57e44be7 100644 --- a/redakcja/static/js/lib/codemirror/codemirror.js +++ b/redakcja/static/js/lib/codemirror/codemirror.js @@ -26,13 +26,15 @@ var CodeMirror = (function(){ // options to a specific CodeMirror constructor. See manual.html for // their meaning. setDefaults(CodeMirrorConfig, { - stylesheet: "", + stylesheet: [], path: "", parserfile: [], basefiles: ["util.js", "stringstream.js", "select.js", "undo.js", "editor.js", "tokenize.js"], iframeClass: null, passDelay: 200, passTime: 50, + lineNumberDelay: 200, + lineNumberTime: 50, continuousScanning: false, saveFunction: null, onChange: null, @@ -41,8 +43,9 @@ var CodeMirror = (function(){ disableSpellcheck: true, textWrapping: true, readOnly: false, - width: "100%", + width: "", height: "300px", + minHeight: 100, autoMatchParens: false, parserConfig: null, tabMode: "indent", // or "spaces", "default", "shift" @@ -50,124 +53,131 @@ var CodeMirror = (function(){ activeTokens: null, cursorActivity: null, lineNumbers: false, - indentUnit: 2 + indentUnit: 2, + domain: null }); - function wrapLineNumberDiv(place) { - return function(node) { - var container = document.createElement("div"), - nums = document.createElement("div"), - scroller = document.createElement("div"); - container.style.position = "relative"; - nums.style.position = "absolute"; - nums.style.height = "100%"; - if (nums.style.setExpression) { - try {nums.style.setExpression("height", "this.previousSibling.offsetHeight + 'px'");} - catch(e) {} // Seems to throw 'Not Implemented' on some IE8 versions - } - nums.style.top = "0px"; - nums.style.overflow = "hidden"; - place(container); - container.appendChild(node); - container.appendChild(nums); - scroller.className = "CodeMirror-line-numbers"; - nums.appendChild(scroller); + function addLineNumberDiv(container) { + var nums = document.createElement("DIV"), + scroller = document.createElement("DIV"); + nums.style.position = "absolute"; + nums.style.height = "100%"; + if (nums.style.setExpression) { + try {nums.style.setExpression("height", "this.previousSibling.offsetHeight + 'px'");} + catch(e) {} // Seems to throw 'Not Implemented' on some IE8 versions } + nums.style.top = "0px"; + nums.style.left = "0px"; + nums.style.overflow = "hidden"; + container.appendChild(nums); + scroller.className = "CodeMirror-line-numbers"; + nums.appendChild(scroller); + scroller.innerHTML = "
1
"; + return nums; } - function applyLineNumbers(frame) { - var win = frame.contentWindow, doc = win.document, - nums = frame.nextSibling, scroller = nums.firstChild; - - var nextNum = 1; - - function update() { - var diff = 20 + Math.max(doc.body.offsetHeight, frame.offsetHeight) - scroller.offsetHeight; - for (var n = Math.ceil(diff / 10); n > 0; n--) { - var div = document.createElement("div"); - div.appendChild(document.createTextNode(nextNum++)); - scroller.appendChild(div); - } - nums.scrollTop = doc.body.scrollTop || doc.documentElement.scrollTop || 0; - } + function frameHTML(options) { + if (typeof options.parserfile == "string") + options.parserfile = [options.parserfile]; + if (typeof options.stylesheet == "string") + options.stylesheet = [options.stylesheet]; - update(); - win.addEventHandler(win, "scroll", update); - win.addEventHandler(win, "resize", update); + var html = [""]; + // Hack to work around a bunch of IE8-specific problems. + html.push(""); + forEach(options.stylesheet, function(file) { + html.push(""); + }); + forEach(options.basefiles.concat(options.parserfile), function(file) { + if (!/^https?:/.test(file)) file = options.path + file; + html.push(""); - }); - html.push(""); - - var doc = this.win.document; - doc.open(); - doc.write(html.join("")); - doc.close(); + this.win = frame.contentWindow; + if (!options.domain || !internetExplorer) { + this.win.document.open(); + this.win.document.write(frameHTML(options)); + this.win.document.close(); + } } CodeMirror.prototype = { init: function() { if (this.options.initCallback) this.options.initCallback(this); - if (this.options.lineNumbers) applyLineNumbers(this.frame); + if (this.options.lineNumbers) this.activateLineNumbers(); if (this.options.reindentOnLoad) this.reindent(); + if (this.options.height == "dynamic") this.setDynamicHeight(); }, getCode: function() {return this.editor.getCode();}, setCode: function(code) {this.editor.importCode(code);}, - selection: function() {return this.editor.selectedText();}, + selection: function() {this.focusIfIE(); return this.editor.selectedText();}, reindent: function() {this.editor.reindent();}, - reindentSelection: function() {this.editor.reindentSelection(null);}, + reindentSelection: function() {this.focusIfIE(); this.editor.reindentSelection(null);}, + focusIfIE: function() { + // in IE, a lot of selection-related functionality only works when the frame is focused + if (this.win.select.ie_selection) this.focus(); + }, focus: function() { this.win.focus(); if (this.editor.selectionSnapshot) // IE hack - this.win.select.selectCoords(this.win, this.editor.selectionSnapshot); + this.win.select.setBookmark(this.win.document.body, this.editor.selectionSnapshot); }, replaceSelection: function(text) { this.focus(); @@ -177,8 +187,8 @@ var CodeMirror = (function(){ replaceChars: function(text, start, end) { this.editor.replaceChars(text, start, end); }, - getSearchCursor: function(string, fromCursor, regexp, case_sensitive) { - return this.editor.getSearchCursor(string, fromCursor, regexp, case_sensitive); + getSearchCursor: function(string, fromCursor, caseFold) { + return this.editor.getSearchCursor(string, fromCursor, caseFold); }, undo: function() {this.editor.history.undo();}, @@ -189,18 +199,76 @@ var CodeMirror = (function(){ grabKeys: function(callback, filter) {this.editor.grabKeys(callback, filter);}, ungrabKeys: function() {this.editor.ungrabKeys();}, - setParser: function(name) {this.editor.setParser(name);}, - - cursorPosition: function(start) { - if (this.win.select.ie_selection) this.focus(); - return this.editor.cursorPosition(start); + setParser: function(name, parserConfig) {this.editor.setParser(name, parserConfig);}, + setSpellcheck: function(on) {this.win.document.body.spellcheck = on;}, + setStylesheet: function(names) { + if (typeof names === "string") names = [names]; + var activeStylesheets = {}; + var matchedNames = {}; + var links = this.win.document.getElementsByTagName("link"); + // Create hashes of active stylesheets and matched names. + // This is O(n^2) but n is expected to be very small. + for (var x = 0, link; link = links[x]; x++) { + if (link.rel.indexOf("stylesheet") !== -1) { + for (var y = 0; y < names.length; y++) { + var name = names[y]; + if (link.href.substring(link.href.length - name.length) === name) { + activeStylesheets[link.href] = true; + matchedNames[name] = true; + } + } + } + } + // Activate the selected stylesheets and disable the rest. + for (var x = 0, link; link = links[x]; x++) { + if (link.rel.indexOf("stylesheet") !== -1) { + link.disabled = !(link.href in activeStylesheets); + } + } + // Create any new stylesheets. + for (var y = 0; y < names.length; y++) { + var name = names[y]; + if (!(name in matchedNames)) { + var link = this.win.document.createElement("link"); + link.rel = "stylesheet"; + link.type = "text/css"; + link.href = name; + this.win.document.getElementsByTagName('head')[0].appendChild(link); + } + } + }, + setTextWrapping: function(on) { + if (on == this.options.textWrapping) return; + this.win.document.body.style.whiteSpace = on ? "" : "nowrap"; + this.options.textWrapping = on; + if (this.lineNumbers) { + this.setLineNumbers(false); + this.setLineNumbers(true); + } }, + setIndentUnit: function(unit) {this.win.indentUnit = unit;}, + setUndoDepth: function(depth) {this.editor.history.maxDepth = depth;}, + setTabMode: function(mode) {this.options.tabMode = mode;}, + setLineNumbers: function(on) { + if (on && !this.lineNumbers) { + this.lineNumbers = addLineNumberDiv(this.wrapping); + this.activateLineNumbers(); + } + else if (!on && this.lineNumbers) { + this.wrapping.removeChild(this.lineNumbers); + this.wrapping.style.marginLeft = ""; + this.lineNumbers = null; + } + }, + + cursorPosition: function(start) {this.focusIfIE(); return this.editor.cursorPosition(start);}, firstLine: function() {return this.editor.firstLine();}, lastLine: function() {return this.editor.lastLine();}, nextLine: function(line) {return this.editor.nextLine(line);}, prevLine: function(line) {return this.editor.prevLine(line);}, lineContent: function(line) {return this.editor.lineContent(line);}, setLineContent: function(line, content) {this.editor.setLineContent(line, content);}, + removeLine: function(line){this.editor.removeLine(line);}, insertIntoLine: function(line, position, content) {this.editor.insertIntoLine(line, position, content);}, selectLines: function(startLine, startOffset, endLine, endOffset) { this.win.focus(); @@ -220,14 +288,168 @@ var CodeMirror = (function(){ } return num; }, - - // Old number-based line interface - jumpToLine: function(n) { - this.selectLines(this.nthLine(n), 0); + jumpToLine: function(line) { + if (typeof line == "number") line = this.nthLine(line); + this.selectLines(line, 0); this.win.focus(); }, - currentLine: function() { - return this.lineNumber(this.cursorPosition().line); + currentLine: function() { // Deprecated, but still there for backward compatibility + return this.lineNumber(this.cursorLine()); + }, + cursorLine: function() { + return this.cursorPosition().line; + }, + cursorCoords: function(start) {return this.editor.cursorCoords(start);}, + + activateLineNumbers: function() { + var frame = this.frame, win = frame.contentWindow, doc = win.document, body = doc.body, + nums = this.lineNumbers, scroller = nums.firstChild, self = this; + var barWidth = null; + + function sizeBar() { + if (frame.offsetWidth == 0) return; + for (var root = frame; root.parentNode; root = root.parentNode); + if (!nums.parentNode || root != document || !win.Editor) { + // Clear event handlers (their nodes might already be collected, so try/catch) + try{clear();}catch(e){} + clearInterval(sizeInterval); + return; + } + + /*if (nums.offsetWidth != barWidth) { + barWidth = nums.offsetWidth; + frame.parentNode.style.paddingLeft = barWidth + "px"; + }*/ + } + function doScroll() { + nums.scrollTop = body.scrollTop || doc.documentElement.scrollTop || 0; + } + // Cleanup function, registered by nonWrapping and wrapping. + var clear = function(){}; + sizeBar(); + var sizeInterval = setInterval(sizeBar, 500); + + function ensureEnoughLineNumbers(fill) { + var lineHeight = scroller.firstChild.offsetHeight; + if (lineHeight == 0) return; + var targetHeight = 50 + Math.max(body.offsetHeight, Math.max(frame.offsetHeight, body.scrollHeight || 0)), + lastNumber = Math.ceil(targetHeight / lineHeight); + for (var i = scroller.childNodes.length; i <= lastNumber; i++) { + var div = document.createElement("DIV"); + div.appendChild(document.createTextNode(fill ? String(i + 1) : "\u00a0")); + scroller.appendChild(div); + } + } + + function nonWrapping() { + function update() { + ensureEnoughLineNumbers(true); + doScroll(); + } + self.updateNumbers = update; + var onScroll = win.addEventHandler(win, "scroll", doScroll, true), + onResize = win.addEventHandler(win, "resize", update, true); + clear = function(){ + onScroll(); onResize(); + if (self.updateNumbers == update) self.updateNumbers = null; + }; + update(); + } + + function wrapping() { + var node, lineNum, next, pos, changes = [], styleNums = self.options.styleNumbers; + + function setNum(n, node) { + // Does not typically happen (but can, if you mess with the + // document during the numbering) + if (!lineNum) lineNum = scroller.appendChild(document.createElement("DIV")); + if (styleNums) styleNums(lineNum, node, n); + // Changes are accumulated, so that the document layout + // doesn't have to be recomputed during the pass + changes.push(lineNum); changes.push(n); + pos = lineNum.offsetHeight + lineNum.offsetTop; + lineNum = lineNum.nextSibling; + } + function commitChanges() { + for (var i = 0; i < changes.length; i += 2) + changes[i].innerHTML = changes[i + 1]; + changes = []; + } + function work() { + if (!scroller.parentNode || scroller.parentNode != self.lineNumbers) return; + + var endTime = new Date().getTime() + self.options.lineNumberTime; + while (node) { + setNum(next++, node.previousSibling); + for (; node && !win.isBR(node); node = node.nextSibling) { + var bott = node.offsetTop + node.offsetHeight; + while (scroller.offsetHeight && bott - 3 > pos) setNum(" "); + } + if (node) node = node.nextSibling; + if (new Date().getTime() > endTime) { + commitChanges(); + pending = setTimeout(work, self.options.lineNumberDelay); + return; + } + } + while (lineNum) setNum(next++); + commitChanges(); + doScroll(); + } + function start(firstTime) { + doScroll(); + ensureEnoughLineNumbers(firstTime); + node = body.firstChild; + lineNum = scroller.firstChild; + pos = 0; + next = 1; + work(); + } + + start(true); + var pending = null; + function update() { + if (pending) clearTimeout(pending); + if (self.editor.allClean()) start(); + else pending = setTimeout(update, 200); + } + self.updateNumbers = update; + var onScroll = win.addEventHandler(win, "scroll", doScroll, true), + onResize = win.addEventHandler(win, "resize", update, true); + clear = function(){ + if (pending) clearTimeout(pending); + if (self.updateNumbers == update) self.updateNumbers = null; + onScroll(); + onResize(); + }; + } + (this.options.textWrapping || this.options.styleNumbers ? wrapping : nonWrapping)(); + }, + + setDynamicHeight: function() { + var self = this, activity = self.options.cursorActivity, win = self.win, body = win.document.body, + lineHeight = null, timeout = null, vmargin = 2 * self.frame.offsetTop; + body.style.overflowY = "hidden"; + win.document.documentElement.style.overflowY = "hidden"; + this.frame.scrolling = "no"; + + function updateHeight() { + for (var span = body.firstChild, sawBR = false; span; span = span.nextSibling) + if (win.isSpan(span) && span.offsetHeight) { + lineHeight = span.offsetHeight; + if (!sawBR) vmargin = 2 * (self.frame.offsetTop + span.offsetTop + body.offsetTop + (internetExplorer ? 10 : 0)); + break; + } + if (lineHeight) + self.wrapping.style.height = Math.max(vmargin + lineHeight * (body.getElementsByTagName("BR").length + 1), + self.options.minHeight) + "px"; + } + setTimeout(updateHeight, 100); + self.options.cursorActivity = function(x) { + if (activity) activity(x); + clearTimeout(timeout); + timeout = setTimeout(updateHeight, 200); + }; } }; @@ -260,6 +482,15 @@ var CodeMirror = (function(){ area.form.addEventListener("submit", updateField, false); else area.form.attachEvent("onsubmit", updateField); + var realSubmit = area.form.submit; + function wrapSubmit() { + updateField(); + // Can't use realSubmit.apply because IE6 is too stupid + area.form.submit = realSubmit; + area.form.submit(); + area.form.submit = wrapSubmit; + } + area.form.submit = wrapSubmit; } function insert(frame) { @@ -271,6 +502,18 @@ var CodeMirror = (function(){ area.style.display = "none"; var mirror = new CodeMirror(insert, options); + mirror.toTextArea = function() { + area.parentNode.removeChild(mirror.wrapping); + area.style.display = ""; + if (area.form) { + area.form.submit = realSubmit; + if (typeof area.form.removeEventListener == "function") + area.form.removeEventListener("submit", updateField, false); + else + area.form.detachEvent("onsubmit", updateField); + } + }; + return mirror; }; 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); }); diff --git a/redakcja/static/js/lib/codemirror/parsexml.js b/redakcja/static/js/lib/codemirror/parsexml.js index 87fbf0c0..994efd3b 100644 --- a/redakcja/static/js/lib/codemirror/parsexml.js +++ b/redakcja/static/js/lib/codemirror/parsexml.js @@ -125,12 +125,12 @@ var XMLParser = Editor.Parser = (function() { // parseJavaScript in parsejavascript.js (there is actually a bit more // shared code than I'd like), but it is quite a bit simpler. function parseXML(source) { - var tokens = tokenizeXML(source); + var tokens = tokenizeXML(source), token; var cc = [base]; var tokenNr = 0, indented = 0; var currentTag = null, context = null; - var consume, marked; - + var consume; + function push(fs) { for (var i = fs.length - 1; i >= 0; i--) cc.push(fs[i]); @@ -144,13 +144,13 @@ var XMLParser = Editor.Parser = (function() { consume = false; } - function mark(style) { - marked = style; + function markErr() { + token.style += " xml-error"; } function expect(text) { return function(style, content) { if (content == text) cont(); - else mark("xml-error") || cont(arguments.callee); + else {markErr(); cont(arguments.callee);} }; } @@ -192,12 +192,12 @@ var XMLParser = Editor.Parser = (function() { cont(); } else if (harmlessTokens.hasOwnProperty(style)) cont(); - else mark("xml-error") || cont(); + else {markErr(); cont();} } function tagname(style, content) { if (style == "xml-name") { currentTag = content.toLowerCase(); - mark("xml-tagname"); + token.style = "xml-tagname"; cont(); } else { @@ -206,24 +206,22 @@ var XMLParser = Editor.Parser = (function() { } } function closetagname(style, content) { - if (style == "xml-name" && context && content.toLowerCase() == context.name) { - popContext(); - mark("xml-tagname"); - } - else { - mark("xml-error"); + if (style == "xml-name") { + token.style = "xml-tagname"; + if (context && content.toLowerCase() == context.name) popContext(); + else markErr(); } cont(); } function endtag(startOfLine) { return function(style, content) { if (content == "/>" || (content == ">" && UseKludges.autoSelfClosers.hasOwnProperty(currentTag))) cont(); - else if (content == ">") pushContext(currentTag, startOfLine) || cont(); - else mark("xml-error") || cont(arguments.callee); + else if (content == ">") {pushContext(currentTag, startOfLine); cont();} + else {markErr(); cont(arguments.callee);} }; } function attributes(style) { - if (style == "xml-name") mark("xml-attname") || cont(attribute, attributes); + if (style == "xml-name") {token.style = "xml-attname"; cont(attribute, attributes);} else pass(); } function attribute(style, content) { @@ -240,7 +238,7 @@ var XMLParser = Editor.Parser = (function() { indentation: function() {return indented;}, next: function(){ - var token = tokens.next(); + token = tokens.next(); if (token.style == "whitespace" && tokenNr == 0) indented = token.value.length; else @@ -254,20 +252,16 @@ var XMLParser = Editor.Parser = (function() { return token; while(true){ - consume = marked = false; + consume = false; cc.pop()(token.style, token.content); - if (consume){ - if (marked) - token.style = marked; - return token; - } + if (consume) return token; } }, copy: function(){ var _cc = cc.concat([]), _tokenState = tokens.state, _context = context; var parser = this; - + return function(input){ cc = _cc.concat([]); tokenNr = indented = 0; diff --git a/redakcja/static/js/lib/codemirror/select.js b/redakcja/static/js/lib/codemirror/select.js index d513ba5f..01fe9960 100644 --- a/redakcja/static/js/lib/codemirror/select.js +++ b/redakcja/static/js/lib/codemirror/select.js @@ -29,15 +29,16 @@ var select = {}; var fourSpaces = "\u00a0\u00a0\u00a0\u00a0"; - select.scrollToNode = function(element) { - if (!element) return; - var doc = element.ownerDocument, body = doc.body, + select.scrollToNode = function(node, cursor) { + if (!node) return; + var element = node, + doc = element.ownerDocument, body = doc.body, win = (doc.defaultView || doc.parentWindow), html = doc.documentElement, atEnd = !element.nextSibling || !element.nextSibling.nextSibling || !element.nextSibling.nextSibling.nextSibling; // In Opera (and recent Webkit versions), BR elements *always* - // have a scrollTop property of zero. + // have a offsetTop property of zero. var compensateHack = 0; while (element && !element.offsetTop) { compensateHack++; @@ -48,32 +49,47 @@ var select = {}; // offset, just scroll to the end. if (compensateHack == 0) atEnd = false; - var y = compensateHack * (element ? element.offsetHeight : 0), x = 0, pos = element; + // WebKit has a bad habit of (sometimes) happily returning bogus + // offsets when the document has just been changed. This seems to + // always be 5/5, so we don't use those. + if (webkit && element && element.offsetTop == 5 && element.offsetLeft == 5) + return; + + var y = compensateHack * (element ? element.offsetHeight : 0), x = 0, + width = (node ? node.offsetWidth : 0), pos = element; while (pos && pos.offsetParent) { y += pos.offsetTop; // Don't count X offset for
nodes - if (pos.nodeName != "BR") + if (!isBR(pos)) x += pos.offsetLeft; pos = pos.offsetParent; } var scroll_x = body.scrollLeft || html.scrollLeft || 0, scroll_y = body.scrollTop || html.scrollTop || 0, - screen_x = x - scroll_x, screen_y = y - scroll_y, scroll = false; + scroll = false, screen_width = win.innerWidth || html.clientWidth || 0; - if (screen_x < 0 || screen_x > (win.innerWidth || html.clientWidth || 0)) { - scroll_x = x; - scroll = true; + if (cursor || width < screen_width) { + if (cursor) { + var off = select.offsetInNode(win, node), size = nodeText(node).length; + if (size) x += width * (off / size); + } + var screen_x = x - scroll_x; + if (screen_x < 0 || screen_x > screen_width) { + scroll_x = x; + scroll = true; + } } + var screen_y = y - scroll_y; if (screen_y < 0 || atEnd || screen_y > (win.innerHeight || html.clientHeight || 0) - 50) { - scroll_y = atEnd ? 1e10 : y; + scroll_y = atEnd ? 1e6 : y; scroll = true; } if (scroll) win.scrollTo(scroll_x, scroll_y); }; select.scrollToCursor = function(container) { - select.scrollToNode(select.selectionTopNode(container, true) || container.firstChild); + select.scrollToNode(select.selectionTopNode(container, true) || container.firstChild, true); }; // Used to prevent restoring a selection when we do not need to. @@ -218,6 +234,15 @@ var select = {}; start.select(); }; + select.offsetInNode = function(win, node) { + var sel = win.document.selection; + if (!sel) return 0; + var range = sel.createRange(), range2 = range.duplicate(); + try {range2.moveToElementText(node);} catch(e){return 0;} + range.setEndPoint("StartToStart", range2); + return range.text.length; + }; + // Get the top-level node that one end of the cursor is inside or // after. Note that this returns false for 'no cursor', and null // for 'start of document'. @@ -247,13 +272,17 @@ var select = {}; } if (cur) { try{range.moveToElementText(cur);} - catch(e){} + catch(e){return false;} range.collapse(false); } else range.moveToElementText(node.parentNode); if (count) range.move("character", count); } - else range.moveToElementText(node); + else { + try{range.moveToElementText(node);} + catch(e){return false;} + } + return true; } // Do a binary search through the container object, comparing @@ -262,7 +291,7 @@ var select = {}; while (start < end) { var middle = Math.ceil((end + start) / 2), node = container.childNodes[middle]; if (!node) return false; // Don't ask. IE6 manages this sometimes. - moveToNodeStart(range2, node); + if (!moveToNodeStart(range2, node)) return false; if (range.compareEndPoints("StartToStart", range2) == 1) start = middle; else @@ -314,7 +343,7 @@ var select = {}; if (!selection) return null; var topNode = select.selectionTopNode(container, start); - while (topNode && topNode.nodeName != "BR") + while (topNode && !isBR(topNode)) topNode = topNode.previousSibling; var range = selection.createRange(), range2 = range.duplicate(); @@ -356,36 +385,36 @@ var select = {}; } // Some hacks for storing and re-storing the selection when the editor loses and regains focus. - select.selectionCoords = function (win) { - var selection = win.document.selection; - if (!selection) return null; - var start = selection.createRange(), end = start.duplicate(); - start.collapse(true); - end.collapse(false); - - var body = win.document.body; - return {start: {x: start.boundingLeft + body.scrollLeft - 1, - y: start.boundingTop + body.scrollTop}, - end: {x: end.boundingLeft + body.scrollLeft - 1, - y: end.boundingTop + body.scrollTop}}; + select.getBookmark = function (container) { + var from = select.cursorPos(container, true), to = select.cursorPos(container, false); + if (from && to) return {from: from, to: to}; }; // Restore a stored selection. - select.selectCoords = function(win, coords) { - if (!coords) return; - - var range1 = win.document.body.createTextRange(), range2 = range1.duplicate(); - // This can fail for various hard-to-handle reasons. - try { - range1.moveToPoint(coords.start.x, coords.start.y); - range2.moveToPoint(coords.end.x, coords.end.y); - range1.setEndPoint("EndToStart", range2); - range1.select(); - } catch(e) {} + select.setBookmark = function(container, mark) { + if (!mark) return; + select.setCursorPos(container, mark.from, mark.to); }; } // W3C model else { + // Find the node right at the cursor, not one of its + // ancestors with a suitable offset. This goes down the DOM tree + // until a 'leaf' is reached (or is it *up* the DOM tree?). + function innerNode(node, offset) { + while (node.nodeType != 3 && !isBR(node)) { + var newNode = node.childNodes[offset] || node.nextSibling; + offset = 0; + while (!newNode && node.parentNode) { + node = node.parentNode; + newNode = node.nextSibling; + } + node = newNode; + if (!newNode) break; + } + return {node: node, offset: offset}; + } + // Store start and end nodes, and offsets within these, and refer // back to the selection object from those nodes, so that this // object can be updated when the nodes are replaced before the @@ -397,36 +426,30 @@ var select = {}; var range = selection.getRangeAt(0); currentSelection = { - start: {node: range.startContainer, offset: range.startOffset}, - end: {node: range.endContainer, offset: range.endOffset}, + start: innerNode(range.startContainer, range.startOffset), + end: innerNode(range.endContainer, range.endOffset), window: win, changed: false }; - - // We want the nodes right at the cursor, not one of their - // ancestors with a suitable offset. This goes down the DOM tree - // until a 'leaf' is reached (or is it *up* the DOM tree?). - function normalize(point){ - while (point.node.nodeType != 3 && point.node.nodeName != "BR") { - var newNode = point.node.childNodes[point.offset] || point.node.nextSibling; - point.offset = 0; - while (!newNode && point.node.parentNode) { - point.node = point.node.parentNode; - newNode = point.node.nextSibling; - } - point.node = newNode; - if (!newNode) - break; - } - } - - normalize(currentSelection.start); - normalize(currentSelection.end); }; select.selectMarked = function () { - if (!currentSelection || !currentSelection.changed) return; - var win = currentSelection.window, range = win.document.createRange(); + var cs = currentSelection; + // on webkit-based browsers, it is apparently possible that the + // selection gets reset even when a node that is not one of the + // endpoints get messed with. the most common situation where + // this occurs is when a selection is deleted or overwitten. we + // check for that here. + function focusIssue() { + if (cs.start.node == cs.end.node && cs.start.offset == cs.end.offset) { + var selection = cs.window.getSelection(); + if (!selection || selection.rangeCount == 0) return true; + var range = selection.getRangeAt(0), point = innerNode(range.startContainer, range.startOffset); + return cs.start.node != point.node || cs.start.offset != point.offset; + } + } + if (!cs || !(cs.changed || (webkit && focusIssue()))) return; + var win = cs.window, range = win.document.createRange(); function setPoint(point, which) { if (point.node) { @@ -442,17 +465,18 @@ var select = {}; } } - setPoint(currentSelection.end, "End"); - setPoint(currentSelection.start, "Start"); + setPoint(cs.end, "End"); + setPoint(cs.start, "Start"); selectRange(range, win); }; // Helper for selecting a range object. function selectRange(range, window) { var selection = window.getSelection(); + if (!selection) return; selection.removeAllRanges(); selection.addRange(range); - }; + } function selectionRange(window) { var selection = window.getSelection(); if (!selection || selection.rangeCount == 0) @@ -471,7 +495,7 @@ var select = {}; var offset = start ? range.startOffset : range.endOffset; // Work around (yet another) bug in Opera's selection model. if (window.opera && !start && range.endContainer == container && range.endOffset == range.startOffset + 1 && - container.childNodes[range.startOffset] && container.childNodes[range.startOffset].nodeName == "BR") + container.childNodes[range.startOffset] && isBR(container.childNodes[range.startOffset])) offset--; // For text nodes, we look at the node itself if the cursor is @@ -486,7 +510,7 @@ var select = {}; // Occasionally, browsers will return the HTML node as // selection. If the offset is 0, we take the start of the frame // ('after null'), otherwise, we take the last node. - else if (node.nodeName == "HTML") { + else if (node.nodeName.toUpperCase() == "HTML") { return (offset == 1 ? null : container.lastChild); } // If the given node is our 'container', we just look up the @@ -531,13 +555,34 @@ var select = {}; return range && !range.collapsed; }; + select.offsetInNode = function(win, node) { + var range = selectionRange(win); + if (!range) return 0; + range = range.cloneRange(); + range.setStartBefore(node); + return range.toString().length; + }; + function insertNodeAtCursor(window, node) { var range = selectionRange(window); if (!range) return; range.deleteContents(); range.insertNode(node); - webkitLastLineHack(window.document.body); + + // work around weirdness where Opera will magically insert a new + // BR node when a BR node inside a span is moved around. makes + // sure the BR ends up outside of spans. + if (window.opera && isBR(node) && isSpan(node.parentNode)) { + var next = node.nextSibling, p = node.parentNode, outer = p.parentNode; + outer.insertBefore(node, p.nextSibling); + var textAfter = ""; + for (; next && next.nodeType == 3; next = next.nextSibling) { + textAfter += next.nodeValue; + removeElement(next); + } + outer.insertBefore(makePartSpan(textAfter, window.document), node.nextSibling); + } range = window.document.createRange(); range.selectNode(node); range.collapse(false); @@ -545,7 +590,10 @@ var select = {}; } select.insertNewlineAtCursor = function(window) { - insertNodeAtCursor(window, window.document.createElement("BR")); + if (webkit) + document.execCommand('insertLineBreak'); + else + insertNodeAtCursor(window, window.document.createElement("BR")); }; select.insertTabAtCursor = function(window) { @@ -557,7 +605,7 @@ var select = {}; if (!range) return; var topNode = select.selectionTopNode(container, start); - while (topNode && topNode.nodeName != "BR") + while (topNode && !isBR(topNode)) topNode = topNode.previousSibling; range = range.cloneRange(); @@ -566,6 +614,7 @@ var select = {}; range.setStartAfter(topNode); else range.setStartBefore(container); + return {node: topNode, offset: range.toString().length}; }; @@ -574,13 +623,17 @@ var select = {}; range = win.document.createRange(); function setPoint(node, offset, side) { + if (offset == 0 && node && !node.nextSibling) { + range["set" + side + "After"](node); + return true; + } + if (!node) node = container.firstChild; else node = node.nextSibling; - if (!node) - return; + if (!node) return; if (offset == 0) { range["set" + side + "Before"](node); diff --git a/redakcja/static/js/lib/codemirror/stringstream.js b/redakcja/static/js/lib/codemirror/stringstream.js index 8c1c0422..4f5bc611 100644 --- a/redakcja/static/js/lib/codemirror/stringstream.js +++ b/redakcja/static/js/lib/codemirror/stringstream.js @@ -14,7 +14,7 @@ // Make a stringstream stream out of an iterator that returns strings. // This is applied to the result of traverseDOM (see codemirror.js), // and the resulting stream is fed to the parser. -window.stringStream = function(source){ +var stringStream = function(source){ // String that's currently being iterated over. var current = ""; // Position in that string. diff --git a/redakcja/static/js/lib/codemirror/tokenize.js b/redakcja/static/js/lib/codemirror/tokenize.js index b0c9545f..071970ce 100644 --- a/redakcja/static/js/lib/codemirror/tokenize.js +++ b/redakcja/static/js/lib/codemirror/tokenize.js @@ -43,7 +43,7 @@ function tokenizer(source, state) { source.next(); return this.take("whitespace"); } - + if (source.applies(isWhiteSpace)) type = "whitespace"; else diff --git a/redakcja/static/js/lib/codemirror/undo.js b/redakcja/static/js/lib/codemirror/undo.js index 5f717a9e..4ea23859 100644 --- a/redakcja/static/js/lib/codemirror/undo.js +++ b/redakcja/static/js/lib/codemirror/undo.js @@ -9,7 +9,7 @@ * complexity and hackery. * * In short, the editor 'touches' BR elements as it parses them, and - * the History stores these. When nothing is touched in commitDelay + * the UndoHistory stores these. When nothing is touched in commitDelay * milliseconds, the changes are committed: It goes over all touched * nodes, throws out the ones that did not change since last commit or * are no longer in the document, and assembles the rest into zero or @@ -26,11 +26,10 @@ // delay (of no input) after which it commits a set of changes, and, // unfortunately, the 'parent' window -- a window that is not in // designMode, and on which setTimeout works in every browser. -function History(container, maxDepth, commitDelay, editor, onChange) { +function UndoHistory(container, maxDepth, commitDelay, editor) { this.container = container; this.maxDepth = maxDepth; this.commitDelay = commitDelay; this.editor = editor; this.parent = editor.parent; - this.onChange = onChange; // This line object represents the initial, empty editor. var initial = {text: "", from: null, to: null}; // As the borders between lines are represented by BR elements, the @@ -48,7 +47,7 @@ function History(container, maxDepth, commitDelay, editor, onChange) { this.history = []; this.redoHistory = []; this.touched = []; } -History.prototype = { +UndoHistory.prototype = { // Schedule a commit (if no other touches come in for commitDelay // milliseconds). scheduleCommit: function() { @@ -73,7 +72,7 @@ History.prototype = { // shadow in the redo history. var item = this.history.pop(); this.redoHistory.push(this.updateTo(item, "applyChain")); - if (this.onChange) this.onChange(); + this.notifyEnvironment(); return this.chainNode(item); } }, @@ -85,7 +84,7 @@ History.prototype = { // The inverse of undo, basically. var item = this.redoHistory.pop(); this.addUndoLevel(this.updateTo(item, "applyChain")); - if (this.onChange) this.onChange(); + this.notifyEnvironment(); return this.chainNode(item); } }, @@ -109,6 +108,7 @@ History.prototype = { from = end; } this.pushChains([chain], from == null && to == null); + this.notifyEnvironment(); }, pushChains: function(chains, doNotHighlight) { @@ -145,7 +145,7 @@ History.prototype = { // Commit unless there are pending dirty nodes. tryCommit: function() { - if (!window.History) return; // Stop when frame has been unloaded + if (!window.UndoHistory) return; // Stop when frame has been unloaded if (this.editor.highlightDirty()) this.commit(true); else this.scheduleCommit(); }, @@ -162,7 +162,7 @@ History.prototype = { if (chains.length) { this.addUndoLevel(this.updateTo(chains, "linkChain")); this.redoHistory = []; - if (this.onChange) this.onChange(); + this.notifyEnvironment(); } }, @@ -191,6 +191,13 @@ History.prototype = { this.editor.scheduleHighlight(); }, + notifyEnvironment: function() { + if (this.onChange) this.onChange(); + // Used by the line-wrapping line-numbering code. + if (window.frameElement && window.frameElement.CodeMirror.updateNumbers) + window.frameElement.CodeMirror.updateNumbers(); + }, + // Link a chain into the DOM nodes (or the first/last links for null // nodes). linkChain: function(chain) { @@ -250,7 +257,7 @@ History.prototype = { function buildLine(node) { var text = []; for (var cur = node ? node.nextSibling : self.container.firstChild; - cur && cur.nodeName != "BR"; cur = cur.nextSibling) + cur && !isBR(cur); cur = cur.nextSibling) if (cur.currentText) text.push(cur.currentText); return {from: node, to: cur, text: cleanText(text.join(""))}; } @@ -275,7 +282,7 @@ History.prototype = { // Get the BR element after/before the given node. function nextBR(node, dir) { var link = dir + "Sibling", search = node[link]; - while (search && search.nodeName != "BR") + while (search && !isBR(search)) search = search[link]; return search; } diff --git a/redakcja/static/js/lib/codemirror/util.js b/redakcja/static/js/lib/codemirror/util.js index 796025ee..c7021c24 100644 --- a/redakcja/static/js/lib/codemirror/util.js +++ b/redakcja/static/js/lib/codemirror/util.js @@ -1,9 +1,5 @@ /* A few useful utility functions. */ -var internetExplorer = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent); -var webkit = /AppleWebKit/.test(navigator.userAgent); -var safari = /Apple Computers, Inc/.test(navigator.vendor); - // Capture a method on an object. function method(obj, name) { return function() {obj[name].apply(obj, arguments);}; @@ -123,3 +119,12 @@ function nodeTop(node) { } return top; } + +function isBR(node) { + var nn = node.nodeName; + return nn == "BR" || nn == "br"; +} +function isSpan(node) { + var nn = node.nodeName; + return nn == "SPAN" || nn == "span"; +} diff --git a/redakcja/static/js/wiki/view_editor_source.js b/redakcja/static/js/wiki/view_editor_source.js index 0597140d..6975f9b2 100644 --- a/redakcja/static/js/wiki/view_editor_source.js +++ b/redakcja/static/js/wiki/view_editor_source.js @@ -10,7 +10,7 @@ this.codemirror = CodeMirror.fromTextArea('codemirror_placeholder', { parserfile: 'parsexml.js', path: STATIC_URL + "js/lib/codemirror/", - stylesheet: STATIC_URL + "css/xmlcolors_15032010.css", + stylesheet: STATIC_URL + "css/xmlcolors_20100906.css", parserConfig: { useHTMLKludges: false }, @@ -18,6 +18,7 @@ textWrapping: true, lineNumbers: true, width: "100%", + height: "100%", tabMode: 'spaces', indentUnit: 0, readOnly: CurrentDocument.readonly || false, diff --git a/redakcja/static/js/wiki/view_search.js b/redakcja/static/js/wiki/view_search.js index 34393dc4..740c9136 100644 --- a/redakcja/static/js/wiki/view_search.js +++ b/redakcja/static/js/wiki/view_search.js @@ -64,8 +64,7 @@ self.searchCursor = self.editor.getSearchCursor( self.$searchInput.val(), self.options['search-from-cursor'], - self.options['search-regexp'], - self.options['search-case-sensitive'] + !self.options['search-case-sensitive'] ); } if (self.searchCursor.findNext()) { diff --git a/redakcja/static/js/wiki/xslt.js b/redakcja/static/js/wiki/xslt.js index 86c6d362..791cf1a3 100644 --- a/redakcja/static/js/wiki/xslt.js +++ b/redakcja/static/js/wiki/xslt.js @@ -61,7 +61,7 @@ function withThemes(code_block, onError) function xml2html(options) { withStylesheets(function() { - var xml = options.xml.replace(/\/\s+/g, '
'); + var xml = options.xml.replace(/\/(\s+)/g, '
$1'); var parser = new DOMParser(); var serializer = new XMLSerializer(); var doc = parser.parseFromString(xml, 'text/xml'); @@ -271,7 +271,7 @@ HTMLSerializer.prototype.serialize = function(rootElement, stripOuter) if(xnode === 'wers') { /* push children */ if(self._verseBefore(token.node)) - self.result += '/\n'; + self.result += '/'; self._pushChildren(token.node); break; }; -- 2.20.1