+++ /dev/null
-.editbox {
- margin: .4em;
- margin-top: 5px;
- margin-left: 5px;
- padding: 0;
- font-family:"Lucida Console", monospace;
- font-size: 13px;
- line-height: 18px;
- color: black;
-}
-
-.editbox p {
- margin: 0;
-}
-
-.editbox span {
- display: inline;
- font-size: 13px;
- line-height: 18px;
-}
-
-span.xml-tagname {
- color: #0e5f0e;
-}
-
-span.xml-attribute {
- color: #8b8b8b;
-}
-
-span.xml-punctuation {
- color: black;
-}
-
-span.xml-attname {
- color: #817aff;
-}
-
-span.xml-comment {
- color: #A70;
-}
-
-span.xml-cdata {
- color: #48A;
-}
-
-span.xml-processing {
- color: #999;
-}
-
-span.xml-entity {
- color: #A22;
-}
-
-span.xml-error {
- color: #F00;
- font-weight: bolder;
-}
-
-span.xml-text {
- color: black;
-}
--- /dev/null
+html {
+ cursor: text;
+}
+
+.editbox {
+ margin: .4em;
+ padding: 0;
+ /* fnp changes */
+ margin-top: 5px;
+ margin-left: 5px;
+ font-family: "Lucida Console", monospace;
+ font-size: 13px;
+ line-height: 18px;
+ /* end fnp changes */
+ color: black;
+}
+
+.editbox p {
+ margin: 0;
+}
+
+span.xml-tagname {
+ color: #A0B;
+}
+
+span.xml-attribute {
+ color: #281;
+}
+
+span.xml-punctuation {
+ color: black;
+}
+
+span.xml-attname {
+ color: #00F;
+}
+
+span.xml-comment {
+ color: #A70;
+}
+
+span.xml-cdata {
+ color: #48A;
+}
+
+span.xml-processing {
+ color: #999;
+}
+
+span.xml-entity {
+ color: #A22;
+}
+
+span.xml-error {
+ color: #F00 !important;
+ /* fnp changes */
+ font-weight: bolder;
+ /* end fnp changes */
+}
+
+span.xml-text {
+ color: black;
+}
// 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,
disableSpellcheck: true,
textWrapping: true,
readOnly: false,
- width: "100%",
+ width: "",
height: "300px",
+ minHeight: 100,
autoMatchParens: false,
parserConfig: null,
tabMode: "indent", // or "spaces", "default", "shift"
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 = "<div>1</div>";
+ 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 = ["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\"><html><head>"];
+ // Hack to work around a bunch of IE8-specific problems.
+ html.push("<meta http-equiv=\"X-UA-Compatible\" content=\"IE=EmulateIE7\"/>");
+ forEach(options.stylesheet, function(file) {
+ html.push("<link rel=\"stylesheet\" type=\"text/css\" href=\"" + file + "\"/>");
+ });
+ forEach(options.basefiles.concat(options.parserfile), function(file) {
+ if (!/^https?:/.test(file)) file = options.path + file;
+ html.push("<script type=\"text/javascript\" src=\"" + file + "\"><" + "/script>");
+ });
+ html.push("</head><body style=\"border-width: 0;\" class=\"editbox\" spellcheck=\"" +
+ (options.disableSpellcheck ? "false" : "true") + "\"></body></html>");
+ return html.join("");
}
- function CodeMirror(place, options) {
- // Backward compatibility for deprecated options.
- if (options.dumbTabs) options.tabMode = "spaces";
- else if (options.normalTab) options.tabMode = "default";
+ var internetExplorer = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent);
+ function CodeMirror(place, options) {
// Use passed options, if any, to override defaults.
this.options = options = options || {};
setDefaults(options, CodeMirrorConfig);
- var frame = this.frame = document.createElement("iframe");
+ // Backward compatibility for deprecated options.
+ if (options.dumbTabs) options.tabMode = "spaces";
+ else if (options.normalTab) options.tabMode = "default";
+
+ var frame = this.frame = document.createElement("IFRAME");
if (options.iframeClass) frame.className = options.iframeClass;
frame.frameBorder = 0;
- frame.src = "javascript:false;";
frame.style.border = "0";
- frame.style.width = options.width;
- frame.style.height = options.height;
+ frame.style.width = '100%';
+ frame.style.height = '100%';
// display: block occasionally suppresses some Firefox bugs, so we
// always add it, redundant as it sounds.
frame.style.display = "block";
- if (place.appendChild) {
- var node = place;
- place = function(n){node.appendChild(n);};
- }
- if (options.lineNumbers) place = wrapLineNumberDiv(place);
- place(frame);
+ var div = this.wrapping = document.createElement("DIV");
+ div.style.position = "relative";
+ div.className = "CodeMirror-wrapping";
+ div.style.width = options.width;
+ div.style.height = (options.height == "dynamic") ? options.minHeight + "px" : options.height;
+ // This is used by Editor.reroutePasteEvent
+ var teHack = this.textareaHack = document.createElement("TEXTAREA");
+ div.appendChild(teHack);
+ teHack.style.position = "absolute";
+ teHack.style.left = "-10000px";
+ teHack.style.width = "10px";
// Link back to this object, so that the editor can fetch options
// and add a reference to itself.
frame.CodeMirror = this;
- this.win = frame.contentWindow;
+ if (options.domain && internetExplorer) {
+ this.html = frameHTML(options);
+ frame.src = "javascript:(function(){document.open();" +
+ (options.domain ? "document.domain=\"" + options.domain + "\";" : "") +
+ "document.write(window.frameElement.CodeMirror.html);document.close();})()";
+ }
+ else {
+ frame.src = "javascript:false";
+ }
- if (typeof options.parserfile == "string")
- options.parserfile = [options.parserfile];
- if (typeof options.stylesheet == "string")
- options.stylesheet = [options.stylesheet];
+ if (place.appendChild) place.appendChild(div);
+ else place(div);
+ div.appendChild(frame);
+ if (options.lineNumbers) this.lineNumbers = addLineNumberDiv(div);
- var html = ["<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\"><html><head>"];
- // Hack to work around a bunch of IE8-specific problems.
- html.push("<meta http-equiv=\"X-UA-Compatible\" content=\"IE=EmulateIE7\"/>");
- forEach(options.stylesheet, function(file) {
- html.push("<link rel=\"stylesheet\" type=\"text/css\" href=\"" + file + "\"/>");
- });
- forEach(options.basefiles.concat(options.parserfile), function(file) {
- html.push("<script type=\"text/javascript\" src=\"" + options.path + file + "\"></script>");
- });
- html.push("</head><body style=\"border-width: 0;\" class=\"editbox\" spellcheck=\"" +
- (options.disableSpellcheck ? "false" : "true") + "\"></body></html>");
-
- 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();
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();},
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();
}
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);
+ };
}
};
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) {
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;
};
* plain sequences of <span> and <br> 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("");
}
// 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
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.
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"));
// 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
// <span> and <br> 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 = [];
}
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 <span>, and give that span a currentText
// property -- this is used to cache the nodeValue, because
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);
}
// 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 <span> element.
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;
}
// 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();
// 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}};
// 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},
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)
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) {
}
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
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;
}
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";
var accum = [];
select.markSelection(this.win);
forEach(traverseDOM(this.container.firstChild), method(accum, "push"));
- webkitLastLineHack(this.container);
select.selectMarked();
return cleanText(accum.join(""));
},
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;
},
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(""));
},
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") {
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);
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();
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;
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);
},
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
},
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;
// 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
}
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
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
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));
},
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 {
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)
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);
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);
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)))))
// 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)
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();
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;
}
}
// 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);
// 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;
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
// 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) {}
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;
}
},
+ 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
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 (<span> node) and the corresponding token
// match.
}
// 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;
}
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
// 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;
}
};
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;
parts.next();
}
else {
- if (part.nodeName != "SPAN")
+ if (!isSpan(part))
throw "Parser out of sync. Expected SPAN.";
if (part.dirty)
lineDirty = true;
}
});
maybeTouch(from);
- webkitLastLineHack(this.container);
// The function returns some status information that is used by
// hightlightDirty to determine whether and where it has to
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);
});
// 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]);
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);}
};
}
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 {
}
}
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) {
indentation: function() {return indented;},
next: function(){
- var token = tokens.next();
+ token = tokens.next();
if (token.style == "whitespace" && tokenNr == 0)
indented = token.value.length;
else
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;
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++;
// 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 <br> 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.
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'.
}
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
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
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();
}
// 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
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) {
}
}
- 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)
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
// 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
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);
}
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) {
if (!range) return;
var topNode = select.selectionTopNode(container, start);
- while (topNode && topNode.nodeName != "BR")
+ while (topNode && !isBR(topNode))
topNode = topNode.previousSibling;
range = range.cloneRange();
range.setStartAfter(topNode);
else
range.setStartBefore(container);
+
return {node: topNode, offset: range.toString().length};
};
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);
// 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.
source.next();
return this.take("whitespace");
}
-
+
if (source.applies(isWhiteSpace))
type = "whitespace";
else
* 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
// 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
this.history = []; this.redoHistory = []; this.touched = [];
}
-History.prototype = {
+UndoHistory.prototype = {
// Schedule a commit (if no other touches come in for commitDelay
// milliseconds).
scheduleCommit: function() {
// 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);
}
},
// 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);
}
},
from = end;
}
this.pushChains([chain], from == null && to == null);
+ this.notifyEnvironment();
},
pushChains: function(chains, doNotHighlight) {
// 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();
},
if (chains.length) {
this.addUndoLevel(this.updateTo(chains, "linkChain"));
this.redoHistory = [];
- if (this.onChange) this.onChange();
+ this.notifyEnvironment();
}
},
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) {
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(""))};
}
// 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;
}
/* 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);};
}
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";
+}
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
},
textWrapping: true,
lineNumbers: true,
width: "100%",
+ height: "100%",
tabMode: 'spaces',
indentUnit: 0,
readOnly: CurrentDocument.readonly || false,
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()) {
function xml2html(options) {
withStylesheets(function() {
- var xml = options.xml.replace(/\/\s+/g, '<br />');
+ var xml = options.xml.replace(/\/(\s+)/g, '<br />$1');
var parser = new DOMParser();
var serializer = new XMLSerializer();
var doc = parser.parseFromString(xml, 'text/xml');
if(xnode === 'wers') {
/* push children */
if(self._verseBefore(token.node))
- self.result += '/\n';
+ self.result += '/';
self._pushChildren(token.node);
break;
};