[ Index ] |
MailPress 7.2 |
[ Index ] [ Classes ] [ Functions ] [ Variables ] [ Constants ] [ Statistics ] |
[Summary view] [Print] [Text view]
1 /* The Editor object manages the content of the editable frame. It 2 * catches events, colours nodes, and indents lines. This file also 3 * holds some functions for transforming arbitrary DOM structures into 4 * plain sequences of <span> and <br> elements 5 */ 6 7 var internetExplorer = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent); 8 var webkit = /AppleWebKit/.test(navigator.userAgent); 9 var safari = /Apple Computers, Inc/.test(navigator.vendor); 10 var gecko = /gecko\/(\d{8})/i.test(navigator.userAgent); 11 var mac = /Mac/.test(navigator.platform); 12 13 // TODO this is related to the backspace-at-end-of-line bug. Remove 14 // this if Opera gets their act together, make the version check more 15 // broad if they don't. 16 var brokenOpera = window.opera && /Version\/10.[56]/.test(navigator.userAgent); 17 // TODO remove this once WebKit 533 becomes less common. 18 var slowWebkit = /AppleWebKit\/533/.test(navigator.userAgent); 19 20 // Make sure a string does not contain two consecutive 'collapseable' 21 // whitespace characters. 22 function makeWhiteSpace(n) { 23 var buffer = [], nb = true; 24 for (; n > 0; n--) { 25 buffer.push((nb || n == 1) ? nbsp : " "); 26 nb ^= true; 27 } 28 return buffer.join(""); 29 } 30 31 // Create a set of white-space characters that will not be collapsed 32 // by the browser, but will not break text-wrapping either. 33 function fixSpaces(string) { 34 if (string.charAt(0) == " ") string = nbsp + string.slice(1); 35 return string.replace(/\t/g, function() {return makeWhiteSpace(indentUnit);}) 36 .replace(/[ \u00a0]{2,}/g, function(s) {return makeWhiteSpace(s.length);}); 37 } 38 39 function cleanText(text) { 40 return text.replace(/\u00a0/g, " "); 41 } 42 43 // Create a SPAN node with the expected properties for document part 44 // spans. 45 function makePartSpan(value) { 46 var text = value; 47 if (value.nodeType == 3) text = value.nodeValue; 48 else value = document.createTextNode(text); 49 50 var span = document.createElement("SPAN"); 51 span.isPart = true; 52 span.appendChild(value); 53 span.currentText = text; 54 return span; 55 } 56 57 // On webkit, when the last BR of the document does not have text 58 // behind it, the cursor can not be put on the line after it. This 59 // makes pressing enter at the end of the document occasionally do 60 // nothing (or at least seem to do nothing). To work around it, this 61 // function makes sure the document ends with a span containing a 62 // zero-width space character. The traverseDOM iterator filters such 63 // character out again, so that the parsers won't see them. This 64 // function is called from a few strategic places to make sure the 65 // zwsp is restored after the highlighting process eats it. 66 var webkitLastLineHack = webkit ? 67 function(container) { 68 var last = container.lastChild; 69 if (!last || !last.hackBR) { 70 var br = document.createElement("BR"); 71 br.hackBR = true; 72 container.appendChild(br); 73 } 74 } : function() {}; 75 76 var Editor = (function(){ 77 // The HTML elements whose content should be suffixed by a newline 78 // when converting them to flat text. 79 var newlineElements = {"P": true, "DIV": true, "LI": true}; 80 81 function asEditorLines(string) { 82 var tab = makeWhiteSpace(indentUnit); 83 return map(string.replace(/\t/g, tab).replace(/\u00a0/g, " ").replace(/\r\n?/g, "\n").split("\n"), fixSpaces); 84 } 85 86 // Helper function for traverseDOM. Flattens an arbitrary DOM node 87 // into an array of textnodes and <br> tags. 88 function simplifyDOM(root, atEnd) { 89 var result = []; 90 var leaving = true; 91 92 function simplifyNode(node, top) { 93 if (node.nodeType == 3) { 94 var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/\r/g, "").replace(/\n/g, " ")); 95 if (text.length) leaving = false; 96 result.push(node); 97 } 98 else if (isBR(node) && node.childNodes.length == 0) { 99 leaving = true; 100 result.push(node); 101 } 102 else { 103 for (var n = node.firstChild; n; n = n.nextSibling) simplifyNode(n); 104 if (!leaving && newlineElements.hasOwnProperty(node.nodeName.toUpperCase())) { 105 leaving = true; 106 if (!atEnd || !top) 107 result.push(document.createElement("BR")); 108 } 109 } 110 } 111 112 simplifyNode(root, true); 113 return result; 114 } 115 116 // Creates a MochiKit-style iterator that goes over a series of DOM 117 // nodes. The values it yields are strings, the textual content of 118 // the nodes. It makes sure that all nodes up to and including the 119 // one whose text is being yielded have been 'normalized' to be just 120 // <span> and <br> elements. 121 function traverseDOM(start){ 122 var nodeQueue = []; 123 124 // Create a function that can be used to insert nodes after the 125 // one given as argument. 126 function pointAt(node){ 127 var parent = node.parentNode; 128 var next = node.nextSibling; 129 return function(newnode) { 130 parent.insertBefore(newnode, next); 131 }; 132 } 133 var point = null; 134 135 // This an Opera-specific hack -- always insert an empty span 136 // between two BRs, because Opera's cursor code gets terribly 137 // confused when the cursor is between two BRs. 138 var afterBR = true; 139 140 // Insert a normalized node at the current point. If it is a text 141 // node, wrap it in a <span>, and give that span a currentText 142 // property -- this is used to cache the nodeValue, because 143 // directly accessing nodeValue is horribly slow on some browsers. 144 // The dirty property is used by the highlighter to determine 145 // which parts of the document have to be re-highlighted. 146 function insertPart(part){ 147 var text = "\n"; 148 if (part.nodeType == 3) { 149 select.snapshotChanged(); 150 part = makePartSpan(part); 151 text = part.currentText; 152 afterBR = false; 153 } 154 else { 155 if (afterBR && window.opera) 156 point(makePartSpan("")); 157 afterBR = true; 158 } 159 part.dirty = true; 160 nodeQueue.push(part); 161 point(part); 162 return text; 163 } 164 165 // Extract the text and newlines from a DOM node, insert them into 166 // the document, and return the textual content. Used to replace 167 // non-normalized nodes. 168 function writeNode(node, end) { 169 var simplified = simplifyDOM(node, end); 170 for (var i = 0; i < simplified.length; i++) 171 simplified[i] = insertPart(simplified[i]); 172 return simplified.join(""); 173 } 174 175 // Check whether a node is a normalized <span> element. 176 function partNode(node){ 177 if (node.isPart && node.childNodes.length == 1 && node.firstChild.nodeType == 3) { 178 node.currentText = node.firstChild.nodeValue; 179 return !/[\n\t\r]/.test(node.currentText); 180 } 181 return false; 182 } 183 184 // Advance to next node, return string for current node. 185 function next() { 186 if (!start) throw StopIteration; 187 var node = start; 188 start = node.nextSibling; 189 190 if (partNode(node)){ 191 nodeQueue.push(node); 192 afterBR = false; 193 return node.currentText; 194 } 195 else if (isBR(node)) { 196 if (afterBR && window.opera) 197 node.parentNode.insertBefore(makePartSpan(""), node); 198 nodeQueue.push(node); 199 afterBR = true; 200 return "\n"; 201 } 202 else { 203 var end = !node.nextSibling; 204 point = pointAt(node); 205 removeElement(node); 206 return writeNode(node, end); 207 } 208 } 209 210 // MochiKit iterators are objects with a next function that 211 // returns the next value or throws StopIteration when there are 212 // no more values. 213 return {next: next, nodes: nodeQueue}; 214 } 215 216 // Determine the text size of a processed node. 217 function nodeSize(node) { 218 return isBR(node) ? 1 : node.currentText.length; 219 } 220 221 // Search backwards through the top-level nodes until the next BR or 222 // the start of the frame. 223 function startOfLine(node) { 224 while (node && !isBR(node)) node = node.previousSibling; 225 return node; 226 } 227 function endOfLine(node, container) { 228 if (!node) node = container.firstChild; 229 else if (isBR(node)) node = node.nextSibling; 230 231 while (node && !isBR(node)) node = node.nextSibling; 232 return node; 233 } 234 235 function time() {return new Date().getTime();} 236 237 // Client interface for searching the content of the editor. Create 238 // these by calling CodeMirror.getSearchCursor. To use, call 239 // findNext on the resulting object -- this returns a boolean 240 // indicating whether anything was found, and can be called again to 241 // skip to the next find. Use the select and replace methods to 242 // actually do something with the found locations. 243 function SearchCursor(editor, string, from, caseFold) { 244 this.editor = editor; 245 this.history = editor.history; 246 this.history.commit(); 247 this.valid = !!string; 248 this.atOccurrence = false; 249 if (caseFold == undefined) caseFold = string == string.toLowerCase(); 250 251 function getText(node){ 252 var line = cleanText(editor.history.textAfter(node)); 253 return (caseFold ? line.toLowerCase() : line); 254 } 255 256 var topPos = {node: null, offset: 0}; 257 if (from && typeof from == "object" && typeof from.character == "number") { 258 editor.checkLine(from.line); 259 var pos = {node: from.line, offset: from.character}; 260 this.pos = {from: pos, to: pos}; 261 } 262 else if (from) { 263 this.pos = {from: select.cursorPos(editor.container, true) || topPos, 264 to: select.cursorPos(editor.container, false) || topPos}; 265 } 266 else { 267 this.pos = {from: topPos, to: topPos}; 268 } 269 270 if (caseFold) string = string.toLowerCase(); 271 // Create a matcher function based on the kind of string we have. 272 var target = string.split("\n"); 273 this.matches = (target.length == 1) ? 274 // For one-line strings, searching can be done simply by calling 275 // indexOf or lastIndexOf on the current line. 276 function(reverse, node, offset) { 277 var line = getText(node), len = string.length, match; 278 if (reverse ? (offset >= len && (match = line.lastIndexOf(string, offset - len)) != -1) 279 : (match = line.indexOf(string, offset)) != -1) 280 return {from: {node: node, offset: match}, 281 to: {node: node, offset: match + len}}; 282 } : 283 // Multi-line strings require internal iteration over lines, and 284 // some clunky checks to make sure the first match ends at the 285 // end of the line and the last match starts at the start. 286 function(reverse, node, offset) { 287 var idx = (reverse ? target.length - 1 : 0), match = target[idx], line = getText(node); 288 var offsetA = (reverse ? line.indexOf(match) + match.length : line.lastIndexOf(match)); 289 if (reverse ? offsetA >= offset || offsetA != match.length 290 : offsetA <= offset || offsetA != line.length - match.length) 291 return; 292 293 var pos = node; 294 while (true) { 295 if (reverse && !pos) return; 296 pos = (reverse ? this.history.nodeBefore(pos) : this.history.nodeAfter(pos) ); 297 if (!reverse && !pos) return; 298 299 line = getText(pos); 300 match = target[reverse ? --idx : ++idx]; 301 302 if (idx > 0 && idx < target.length - 1) { 303 if (line != match) return; 304 else continue; 305 } 306 var offsetB = (reverse ? line.lastIndexOf(match) : line.indexOf(match) + match.length); 307 if (reverse ? offsetB != line.length - match.length : offsetB != match.length) 308 return; 309 return {from: {node: reverse ? pos : node, offset: reverse ? offsetB : offsetA}, 310 to: {node: reverse ? node : pos, offset: reverse ? offsetA : offsetB}}; 311 } 312 }; 313 } 314 315 SearchCursor.prototype = { 316 findNext: function() {return this.find(false);}, 317 findPrevious: function() {return this.find(true);}, 318 319 find: function(reverse) { 320 if (!this.valid) return false; 321 322 var self = this, pos = reverse ? this.pos.from : this.pos.to, 323 node = pos.node, offset = pos.offset; 324 // Reset the cursor if the current line is no longer in the DOM tree. 325 if (node && !node.parentNode) { 326 node = null; offset = 0; 327 } 328 function savePosAndFail() { 329 var pos = {node: node, offset: offset}; 330 self.pos = {from: pos, to: pos}; 331 self.atOccurrence = false; 332 return false; 333 } 334 335 while (true) { 336 if (this.pos = this.matches(reverse, node, offset)) { 337 this.atOccurrence = true; 338 return true; 339 } 340 341 if (reverse) { 342 if (!node) return savePosAndFail(); 343 node = this.history.nodeBefore(node); 344 offset = this.history.textAfter(node).length; 345 } 346 else { 347 var next = this.history.nodeAfter(node); 348 if (!next) { 349 offset = this.history.textAfter(node).length; 350 return savePosAndFail(); 351 } 352 node = next; 353 offset = 0; 354 } 355 } 356 }, 357 358 select: function() { 359 if (this.atOccurrence) { 360 select.setCursorPos(this.editor.container, this.pos.from, this.pos.to); 361 select.scrollToCursor(this.editor.container); 362 } 363 }, 364 365 replace: function(string) { 366 if (this.atOccurrence) { 367 var end = this.editor.replaceRange(this.pos.from, this.pos.to, string); 368 this.pos.to = end; 369 this.atOccurrence = false; 370 } 371 }, 372 373 position: function() { 374 if (this.atOccurrence) 375 return {line: this.pos.from.node, character: this.pos.from.offset}; 376 } 377 }; 378 379 // The Editor object is the main inside-the-iframe interface. 380 function Editor(options) { 381 this.options = options; 382 window.indentUnit = options.indentUnit; 383 this.parent = parent; 384 var container = this.container = document.body; 385 this.history = new UndoHistory(container, options.undoDepth, options.undoDelay, this); 386 var self = this; 387 388 if (!Editor.Parser) 389 throw "No parser loaded."; 390 if (options.parserConfig && Editor.Parser.configure) 391 Editor.Parser.configure(options.parserConfig); 392 393 if (!options.readOnly) 394 select.setCursorPos(container, {node: null, offset: 0}); 395 396 this.dirty = []; 397 this.importCode(options.content || ""); 398 this.history.onChange = options.onChange; 399 400 if (!options.readOnly) { 401 if (options.continuousScanning !== false) { 402 this.scanner = this.documentScanner(options.passTime); 403 this.delayScanning(); 404 } 405 406 function setEditable() { 407 // Use contentEditable instead of designMode on IE, since designMode frames 408 // can not run any scripts. It would be nice if we could use contentEditable 409 // everywhere, but it is significantly flakier than designMode on every 410 // single non-IE browser. 411 if (document.body.contentEditable != undefined && internetExplorer) 412 document.body.contentEditable = "true"; 413 else 414 document.designMode = "on"; 415 416 // Work around issue where you have to click on the actual 417 // body of the document to focus it in IE, making focusing 418 // hard when the document is small. 419 if (internetExplorer && options.height != "dynamic") 420 document.body.style.minHeight = (frameElement.clientHeight - 2 * document.body.offsetTop - 5) + "px"; 421 422 document.documentElement.style.borderWidth = "0"; 423 if (!options.textWrapping) 424 container.style.whiteSpace = "nowrap"; 425 } 426 427 // If setting the frame editable fails, try again when the user 428 // focus it (happens when the frame is not visible on 429 // initialisation, in Firefox). 430 try { 431 setEditable(); 432 } 433 catch(e) { 434 var focusEvent = addEventHandler(document, "focus", function() { 435 focusEvent(); 436 setEditable(); 437 }, true); 438 } 439 440 addEventHandler(document, "keydown", method(this, "keyDown")); 441 addEventHandler(document, "keypress", method(this, "keyPress")); 442 addEventHandler(document, "keyup", method(this, "keyUp")); 443 444 function cursorActivity() {self.cursorActivity(false);} 445 addEventHandler(document.body, "mouseup", cursorActivity); 446 addEventHandler(document.body, "cut", cursorActivity); 447 448 // workaround for a gecko bug [?] where going forward and then 449 // back again breaks designmode (no more cursor) 450 if (gecko) 451 addEventHandler(window, "pagehide", function(){self.unloaded = true;}); 452 453 addEventHandler(document.body, "paste", function(event) { 454 cursorActivity(); 455 var text = null; 456 try { 457 var clipboardData = event.clipboardData || window.clipboardData; 458 if (clipboardData) text = clipboardData.getData('Text'); 459 } 460 catch(e) {} 461 if (text !== null) { 462 event.stop(); 463 self.replaceSelection(text); 464 select.scrollToCursor(self.container); 465 } 466 }); 467 468 if (this.options.autoMatchParens) 469 addEventHandler(document.body, "click", method(this, "scheduleParenHighlight")); 470 } 471 else if (!options.textWrapping) { 472 container.style.whiteSpace = "nowrap"; 473 } 474 } 475 476 function isSafeKey(code) { 477 return (code >= 16 && code <= 18) || // shift, control, alt 478 (code >= 33 && code <= 40); // arrows, home, end 479 } 480 481 Editor.prototype = { 482 // Import a piece of code into the editor. 483 importCode: function(code) { 484 this.history.push(null, null, asEditorLines(code)); 485 this.history.reset(); 486 }, 487 488 // Extract the code from the editor. 489 getCode: function() { 490 if (!this.container.firstChild) 491 return ""; 492 493 var accum = []; 494 select.markSelection(); 495 forEach(traverseDOM(this.container.firstChild), method(accum, "push")); 496 webkitLastLineHack(this.container); 497 select.selectMarked(); 498 // On webkit, don't count last (empty) line if the webkitLastLineHack BR is present 499 if (webkit && this.container.lastChild.hackBR) 500 accum.pop(); 501 return cleanText(accum.join("")); 502 }, 503 504 checkLine: function(node) { 505 if (node === false || !(node == null || node.parentNode == this.container)) 506 throw parent.CodeMirror.InvalidLineHandle; 507 }, 508 509 cursorPosition: function(start) { 510 if (start == null) start = true; 511 var pos = select.cursorPos(this.container, start); 512 if (pos) return {line: pos.node, character: pos.offset}; 513 else return {line: null, character: 0}; 514 }, 515 516 firstLine: function() { 517 return null; 518 }, 519 520 lastLine: function() { 521 if (this.container.lastChild) return startOfLine(this.container.lastChild); 522 else return null; 523 }, 524 525 nextLine: function(line) { 526 this.checkLine(line); 527 var end = endOfLine(line, this.container); 528 return end || false; 529 }, 530 531 prevLine: function(line) { 532 this.checkLine(line); 533 if (line == null) return false; 534 return startOfLine(line.previousSibling); 535 }, 536 537 visibleLineCount: function() { 538 var line = this.container.firstChild; 539 while (line && isBR(line)) line = line.nextSibling; // BR heights are unreliable 540 if (!line) return false; 541 var innerHeight = (window.innerHeight 542 || document.documentElement.clientHeight 543 || document.body.clientHeight); 544 return Math.floor(innerHeight / line.offsetHeight); 545 }, 546 547 selectLines: function(startLine, startOffset, endLine, endOffset) { 548 this.checkLine(startLine); 549 var start = {node: startLine, offset: startOffset}, end = null; 550 if (endOffset !== undefined) { 551 this.checkLine(endLine); 552 end = {node: endLine, offset: endOffset}; 553 } 554 select.setCursorPos(this.container, start, end); 555 select.scrollToCursor(this.container); 556 }, 557 558 lineContent: function(line) { 559 var accum = []; 560 for (line = line ? line.nextSibling : this.container.firstChild; 561 line && !isBR(line); line = line.nextSibling) 562 accum.push(nodeText(line)); 563 return cleanText(accum.join("")); 564 }, 565 566 setLineContent: function(line, content) { 567 this.history.commit(); 568 this.replaceRange({node: line, offset: 0}, 569 {node: line, offset: this.history.textAfter(line).length}, 570 content); 571 this.addDirtyNode(line); 572 this.scheduleHighlight(); 573 }, 574 575 removeLine: function(line) { 576 var node = line ? line.nextSibling : this.container.firstChild; 577 while (node) { 578 var next = node.nextSibling; 579 removeElement(node); 580 if (isBR(node)) break; 581 node = next; 582 } 583 this.addDirtyNode(line); 584 this.scheduleHighlight(); 585 }, 586 587 insertIntoLine: function(line, position, content) { 588 var before = null; 589 if (position == "end") { 590 before = endOfLine(line, this.container); 591 } 592 else { 593 for (var cur = line ? line.nextSibling : this.container.firstChild; cur; cur = cur.nextSibling) { 594 if (position == 0) { 595 before = cur; 596 break; 597 } 598 var text = nodeText(cur); 599 if (text.length > position) { 600 before = cur.nextSibling; 601 content = text.slice(0, position) + content + text.slice(position); 602 removeElement(cur); 603 break; 604 } 605 position -= text.length; 606 } 607 } 608 609 var lines = asEditorLines(content); 610 for (var i = 0; i < lines.length; i++) { 611 if (i > 0) this.container.insertBefore(document.createElement("BR"), before); 612 this.container.insertBefore(makePartSpan(lines[i]), before); 613 } 614 this.addDirtyNode(line); 615 this.scheduleHighlight(); 616 }, 617 618 // Retrieve the selected text. 619 selectedText: function() { 620 var h = this.history; 621 h.commit(); 622 623 var start = select.cursorPos(this.container, true), 624 end = select.cursorPos(this.container, false); 625 if (!start || !end) return ""; 626 627 if (start.node == end.node) 628 return h.textAfter(start.node).slice(start.offset, end.offset); 629 630 var text = [h.textAfter(start.node).slice(start.offset)]; 631 for (var pos = h.nodeAfter(start.node); pos != end.node; pos = h.nodeAfter(pos)) 632 text.push(h.textAfter(pos)); 633 text.push(h.textAfter(end.node).slice(0, end.offset)); 634 return cleanText(text.join("\n")); 635 }, 636 637 // Replace the selection with another piece of text. 638 replaceSelection: function(text) { 639 this.history.commit(); 640 641 var start = select.cursorPos(this.container, true), 642 end = select.cursorPos(this.container, false); 643 if (!start || !end) return; 644 645 end = this.replaceRange(start, end, text); 646 select.setCursorPos(this.container, end); 647 webkitLastLineHack(this.container); 648 }, 649 650 cursorCoords: function(start) { 651 var sel = select.cursorPos(this.container, start); 652 if (!sel) return null; 653 var off = sel.offset, node = sel.node, self = this; 654 function measureFromNode(node, xOffset) { 655 var y = -(document.body.scrollTop || document.documentElement.scrollTop || 0), 656 x = -(document.body.scrollLeft || document.documentElement.scrollLeft || 0) + xOffset; 657 forEach([node, window.frameElement], function(n) { 658 while (n) {x += n.offsetLeft; y += n.offsetTop;n = n.offsetParent;} 659 }); 660 return {x: x, y: y, yBot: y + node.offsetHeight}; 661 } 662 function withTempNode(text, f) { 663 var node = document.createElement("SPAN"); 664 node.appendChild(document.createTextNode(text)); 665 try {return f(node);} 666 finally {if (node.parentNode) node.parentNode.removeChild(node);} 667 } 668 669 while (off) { 670 node = node ? node.nextSibling : this.container.firstChild; 671 var txt = nodeText(node); 672 if (off < txt.length) 673 return withTempNode(txt.substr(0, off), function(tmp) { 674 tmp.style.position = "absolute"; tmp.style.visibility = "hidden"; 675 tmp.className = node.className; 676 self.container.appendChild(tmp); 677 return measureFromNode(node, tmp.offsetWidth); 678 }); 679 off -= txt.length; 680 } 681 if (node && isSpan(node)) 682 return measureFromNode(node, node.offsetWidth); 683 else if (node && node.nextSibling && isSpan(node.nextSibling)) 684 return measureFromNode(node.nextSibling, 0); 685 else 686 return withTempNode("\u200b", function(tmp) { 687 if (node) node.parentNode.insertBefore(tmp, node.nextSibling); 688 else self.container.insertBefore(tmp, self.container.firstChild); 689 return measureFromNode(tmp, 0); 690 }); 691 }, 692 693 reroutePasteEvent: function() { 694 if (this.capturingPaste || window.opera) return; 695 this.capturingPaste = true; 696 var te = window.frameElement.CodeMirror.textareaHack; 697 parent.focus(); 698 te.value = ""; 699 te.focus(); 700 701 var self = this; 702 this.parent.setTimeout(function() { 703 self.capturingPaste = false; 704 window.focus(); 705 if (self.selectionSnapshot) // IE hack 706 window.select.setBookmark(self.container, self.selectionSnapshot); 707 var text = te.value; 708 if (text) { 709 self.replaceSelection(text); 710 select.scrollToCursor(self.container); 711 } 712 }, 10); 713 }, 714 715 replaceRange: function(from, to, text) { 716 var lines = asEditorLines(text); 717 lines[0] = this.history.textAfter(from.node).slice(0, from.offset) + lines[0]; 718 var lastLine = lines[lines.length - 1]; 719 lines[lines.length - 1] = lastLine + this.history.textAfter(to.node).slice(to.offset); 720 var end = this.history.nodeAfter(to.node); 721 this.history.push(from.node, end, lines); 722 return {node: this.history.nodeBefore(end), 723 offset: lastLine.length}; 724 }, 725 726 getSearchCursor: function(string, fromCursor, caseFold) { 727 return new SearchCursor(this, string, fromCursor, caseFold); 728 }, 729 730 // Re-indent the whole buffer 731 reindent: function() { 732 if (this.container.firstChild) 733 this.indentRegion(null, this.container.lastChild); 734 }, 735 736 reindentSelection: function(direction) { 737 if (!select.somethingSelected()) { 738 this.indentAtCursor(direction); 739 } 740 else { 741 var start = select.selectionTopNode(this.container, true), 742 end = select.selectionTopNode(this.container, false); 743 if (start === false || end === false) return; 744 this.indentRegion(start, end, direction); 745 } 746 }, 747 748 grabKeys: function(eventHandler, filter) { 749 this.frozen = eventHandler; 750 this.keyFilter = filter; 751 }, 752 ungrabKeys: function() { 753 this.frozen = "leave"; 754 }, 755 756 setParser: function(name, parserConfig) { 757 Editor.Parser = window[name]; 758 parserConfig = parserConfig || this.options.parserConfig; 759 if (parserConfig && Editor.Parser.configure) 760 Editor.Parser.configure(parserConfig); 761 762 if (this.container.firstChild) { 763 forEach(this.container.childNodes, function(n) { 764 if (n.nodeType != 3) n.dirty = true; 765 }); 766 this.addDirtyNode(this.firstChild); 767 this.scheduleHighlight(); 768 } 769 }, 770 771 // Intercept enter and tab, and assign their new functions. 772 keyDown: function(event) { 773 if (this.frozen == "leave") {this.frozen = null; this.keyFilter = null;} 774 if (this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode, event))) { 775 event.stop(); 776 this.frozen(event); 777 return; 778 } 779 780 var code = event.keyCode; 781 // Don't scan when the user is typing. 782 this.delayScanning(); 783 // Schedule a paren-highlight event, if configured. 784 if (this.options.autoMatchParens) 785 this.scheduleParenHighlight(); 786 787 // The various checks for !altKey are there because AltGr sets both 788 // ctrlKey and altKey to true, and should not be recognised as 789 // Control. 790 if (code == 13) { // enter 791 if (event.ctrlKey && !event.altKey) { 792 this.reparseBuffer(); 793 } 794 else { 795 select.insertNewlineAtCursor(); 796 var mode = this.options.enterMode; 797 if (mode != "flat") this.indentAtCursor(mode == "keep" ? "keep" : undefined); 798 select.scrollToCursor(this.container); 799 } 800 event.stop(); 801 } 802 else if (code == 9 && this.options.tabMode != "default" && !event.ctrlKey) { // tab 803 this.handleTab(!event.shiftKey); 804 event.stop(); 805 } 806 else if (code == 32 && event.shiftKey && this.options.tabMode == "default") { // space 807 this.handleTab(true); 808 event.stop(); 809 } 810 else if (code == 36 && !event.shiftKey && !event.ctrlKey) { // home 811 if (this.home()) event.stop(); 812 } 813 else if (code == 35 && !event.shiftKey && !event.ctrlKey) { // end 814 if (this.end()) event.stop(); 815 } 816 // Only in Firefox is the default behavior for PgUp/PgDn correct. 817 else if (code == 33 && !event.shiftKey && !event.ctrlKey && !gecko) { // PgUp 818 if (this.pageUp()) event.stop(); 819 } 820 else if (code == 34 && !event.shiftKey && !event.ctrlKey && !gecko) { // PgDn 821 if (this.pageDown()) event.stop(); 822 } 823 else if ((code == 219 || code == 221) && event.ctrlKey && !event.altKey) { // [, ] 824 this.highlightParens(event.shiftKey, true); 825 event.stop(); 826 } 827 else if (event.metaKey && !event.shiftKey && (code == 37 || code == 39)) { // Meta-left/right 828 var cursor = select.selectionTopNode(this.container); 829 if (cursor === false || !this.container.firstChild) return; 830 831 if (code == 37) select.focusAfterNode(startOfLine(cursor), this.container); 832 else { 833 var end = endOfLine(cursor, this.container); 834 select.focusAfterNode(end ? end.previousSibling : this.container.lastChild, this.container); 835 } 836 event.stop(); 837 } 838 else if ((event.ctrlKey || event.metaKey) && !event.altKey) { 839 if ((event.shiftKey && code == 90) || code == 89) { // shift-Z, Y 840 select.scrollToNode(this.history.redo()); 841 event.stop(); 842 } 843 else if (code == 90 || (safari && code == 8)) { // Z, backspace 844 select.scrollToNode(this.history.undo()); 845 event.stop(); 846 } 847 else if (code == 83 && this.options.saveFunction) { // S 848 this.options.saveFunction(); 849 event.stop(); 850 } 851 else if (code == 86 && !mac) { // V 852 this.reroutePasteEvent(); 853 } 854 } 855 }, 856 857 // Check for characters that should re-indent the current line, 858 // and prevent Opera from handling enter and tab anyway. 859 keyPress: function(event) { 860 var electric = this.options.electricChars && Editor.Parser.electricChars, self = this; 861 // Hack for Opera, and Firefox on OS X, in which stopping a 862 // keydown event does not prevent the associated keypress event 863 // from happening, so we have to cancel enter and tab again 864 // here. 865 if ((this.frozen && (!this.keyFilter || this.keyFilter(event.keyCode || event.code, event))) || 866 event.code == 13 || (event.code == 9 && this.options.tabMode != "default") || 867 (event.code == 32 && event.shiftKey && this.options.tabMode == "default")) 868 event.stop(); 869 else if (mac && (event.ctrlKey || event.metaKey) && event.character == "v") { 870 this.reroutePasteEvent(); 871 } 872 else if (electric && electric.indexOf(event.character) != -1) 873 this.parent.setTimeout(function(){self.indentAtCursor(null);}, 0); 874 // Work around a bug where pressing backspace at the end of a 875 // line, or delete at the start, often causes the cursor to jump 876 // to the start of the line in Opera 10.60. 877 else if (brokenOpera) { 878 if (event.code == 8) { // backspace 879 var sel = select.selectionTopNode(this.container), self = this, 880 next = sel ? sel.nextSibling : this.container.firstChild; 881 if (sel !== false && next && isBR(next)) 882 this.parent.setTimeout(function(){ 883 if (select.selectionTopNode(self.container) == next) 884 select.focusAfterNode(next.previousSibling, self.container); 885 }, 20); 886 } 887 else if (event.code == 46) { // delete 888 var sel = select.selectionTopNode(this.container), self = this; 889 if (sel && isBR(sel)) { 890 this.parent.setTimeout(function(){ 891 if (select.selectionTopNode(self.container) != sel) 892 select.focusAfterNode(sel, self.container); 893 }, 20); 894 } 895 } 896 } 897 // In 533.* WebKit versions, when the document is big, typing 898 // something at the end of a line causes the browser to do some 899 // kind of stupid heavy operation, creating delays of several 900 // seconds before the typed characters appear. This very crude 901 // hack inserts a temporary zero-width space after the cursor to 902 // make it not be at the end of the line. 903 else if (slowWebkit) { 904 var sel = select.selectionTopNode(this.container), 905 next = sel ? sel.nextSibling : this.container.firstChild; 906 // Doesn't work on empty lines, for some reason those always 907 // trigger the delay. 908 if (sel && next && isBR(next) && !isBR(sel)) { 909 var cheat = document.createTextNode("\u200b"); 910 this.container.insertBefore(cheat, next); 911 this.parent.setTimeout(function() { 912 if (cheat.nodeValue == "\u200b") removeElement(cheat); 913 else cheat.nodeValue = cheat.nodeValue.replace("\u200b", ""); 914 }, 20); 915 } 916 } 917 }, 918 919 // Mark the node at the cursor dirty when a non-safe key is 920 // released. 921 keyUp: function(event) { 922 this.cursorActivity(isSafeKey(event.keyCode)); 923 }, 924 925 // Indent the line following a given <br>, or null for the first 926 // line. If given a <br> element, this must have been highlighted 927 // so that it has an indentation method. Returns the whitespace 928 // element that has been modified or created (if any). 929 indentLineAfter: function(start, direction) { 930 function whiteSpaceAfter(node) { 931 var ws = node ? node.nextSibling : self.container.firstChild; 932 if (!ws || !hasClass(ws, "whitespace")) return null; 933 return ws; 934 } 935 936 // whiteSpace is the whitespace span at the start of the line, 937 // or null if there is no such node. 938 var self = this, whiteSpace = whiteSpaceAfter(start); 939 var newIndent = 0, curIndent = whiteSpace ? whiteSpace.currentText.length : 0; 940 941 if (direction == "keep") { 942 if (start) { 943 var prevWS = whiteSpaceAfter(startOfLine(start.previousSibling)) 944 if (prevWS) newIndent = prevWS.currentText.length; 945 } 946 } 947 else { 948 // Sometimes the start of the line can influence the correct 949 // indentation, so we retrieve it. 950 var firstText = whiteSpace ? whiteSpace.nextSibling : (start ? start.nextSibling : this.container.firstChild); 951 var nextChars = (start && firstText && firstText.currentText) ? firstText.currentText : ""; 952 953 // Ask the lexical context for the correct indentation, and 954 // compute how much this differs from the current indentation. 955 if (direction != null && this.options.tabMode == "shift") 956 newIndent = direction ? curIndent + indentUnit : Math.max(0, curIndent - indentUnit) 957 else if (start) 958 newIndent = start.indentation(nextChars, curIndent, direction); 959 else if (Editor.Parser.firstIndentation) 960 newIndent = Editor.Parser.firstIndentation(nextChars, curIndent, direction); 961 } 962 963 var indentDiff = newIndent - curIndent; 964 965 // If there is too much, this is just a matter of shrinking a span. 966 if (indentDiff < 0) { 967 if (newIndent == 0) { 968 if (firstText) select.snapshotMove(whiteSpace.firstChild, firstText.firstChild || firstText, 0); 969 removeElement(whiteSpace); 970 whiteSpace = null; 971 } 972 else { 973 select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true); 974 whiteSpace.currentText = makeWhiteSpace(newIndent); 975 whiteSpace.firstChild.nodeValue = whiteSpace.currentText; 976 } 977 } 978 // Not enough... 979 else if (indentDiff > 0) { 980 // If there is whitespace, we grow it. 981 if (whiteSpace) { 982 whiteSpace.currentText = makeWhiteSpace(newIndent); 983 whiteSpace.firstChild.nodeValue = whiteSpace.currentText; 984 select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true); 985 } 986 // Otherwise, we have to add a new whitespace node. 987 else { 988 whiteSpace = makePartSpan(makeWhiteSpace(newIndent)); 989 whiteSpace.className = "whitespace"; 990 if (start) insertAfter(whiteSpace, start); 991 else this.container.insertBefore(whiteSpace, this.container.firstChild); 992 select.snapshotMove(firstText && (firstText.firstChild || firstText), 993 whiteSpace.firstChild, newIndent, false, true); 994 } 995 } 996 // Make sure cursor ends up after the whitespace 997 else if (whiteSpace) { 998 select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, newIndent, false); 999 } 1000 if (indentDiff != 0) this.addDirtyNode(start); 1001 }, 1002 1003 // Re-highlight the selected part of the document. 1004 highlightAtCursor: function() { 1005 var pos = select.selectionTopNode(this.container, true); 1006 var to = select.selectionTopNode(this.container, false); 1007 if (pos === false || to === false) return false; 1008 1009 select.markSelection(); 1010 if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false) 1011 return false; 1012 select.selectMarked(); 1013 return true; 1014 }, 1015 1016 // When tab is pressed with text selected, the whole selection is 1017 // re-indented, when nothing is selected, the line with the cursor 1018 // is re-indented. 1019 handleTab: function(direction) { 1020 if (this.options.tabMode == "spaces") 1021 select.insertTabAtCursor(); 1022 else 1023 this.reindentSelection(direction); 1024 }, 1025 1026 // Custom home behaviour that doesn't land the cursor in front of 1027 // leading whitespace unless pressed twice. 1028 home: function() { 1029 var cur = select.selectionTopNode(this.container, true), start = cur; 1030 if (cur === false || !(!cur || cur.isPart || isBR(cur)) || !this.container.firstChild) 1031 return false; 1032 1033 while (cur && !isBR(cur)) cur = cur.previousSibling; 1034 var next = cur ? cur.nextSibling : this.container.firstChild; 1035 if (next && next != start && next.isPart && hasClass(next, "whitespace")) 1036 select.focusAfterNode(next, this.container); 1037 else 1038 select.focusAfterNode(cur, this.container); 1039 1040 select.scrollToCursor(this.container); 1041 return true; 1042 }, 1043 1044 // Some browsers (Opera) don't manage to handle the end key 1045 // properly in the face of vertical scrolling. 1046 end: function() { 1047 var cur = select.selectionTopNode(this.container, true); 1048 if (cur === false) return false; 1049 cur = endOfLine(cur, this.container); 1050 if (!cur) return false; 1051 select.focusAfterNode(cur.previousSibling, this.container); 1052 select.scrollToCursor(this.container); 1053 return true; 1054 }, 1055 1056 pageUp: function() { 1057 var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount(); 1058 if (line === false || scrollAmount === false) return false; 1059 // Try to keep one line on the screen. 1060 scrollAmount -= 2; 1061 for (var i = 0; i < scrollAmount; i++) { 1062 line = this.prevLine(line); 1063 if (line === false) break; 1064 } 1065 if (i == 0) return false; // Already at first line 1066 select.setCursorPos(this.container, {node: line, offset: 0}); 1067 select.scrollToCursor(this.container); 1068 return true; 1069 }, 1070 1071 pageDown: function() { 1072 var line = this.cursorPosition().line, scrollAmount = this.visibleLineCount(); 1073 if (line === false || scrollAmount === false) return false; 1074 // Try to move to the last line of the current page. 1075 scrollAmount -= 2; 1076 for (var i = 0; i < scrollAmount; i++) { 1077 var nextLine = this.nextLine(line); 1078 if (nextLine === false) break; 1079 line = nextLine; 1080 } 1081 if (i == 0) return false; // Already at last line 1082 select.setCursorPos(this.container, {node: line, offset: 0}); 1083 select.scrollToCursor(this.container); 1084 return true; 1085 }, 1086 1087 // Delay (or initiate) the next paren highlight event. 1088 scheduleParenHighlight: function() { 1089 if (this.parenEvent) this.parent.clearTimeout(this.parenEvent); 1090 var self = this; 1091 this.parenEvent = this.parent.setTimeout(function(){self.highlightParens();}, 300); 1092 }, 1093 1094 // Take the token before the cursor. If it contains a character in 1095 // '()[]{}', search for the matching paren/brace/bracket, and 1096 // highlight them in green for a moment, or red if no proper match 1097 // was found. 1098 highlightParens: function(jump, fromKey) { 1099 var self = this; 1100 // give the relevant nodes a colour. 1101 function highlight(node, ok) { 1102 if (!node) return; 1103 if (self.options.markParen) { 1104 self.options.markParen(node, ok); 1105 } 1106 else { 1107 node.style.fontWeight = "bold"; 1108 node.style.color = ok ? "#8F8" : "#F88"; 1109 } 1110 } 1111 function unhighlight(node) { 1112 if (!node) return; 1113 if (self.options.unmarkParen) { 1114 self.options.unmarkParen(node); 1115 } 1116 else { 1117 node.style.fontWeight = ""; 1118 node.style.color = ""; 1119 } 1120 } 1121 if (!fromKey && self.highlighted) { 1122 unhighlight(self.highlighted[0]); 1123 unhighlight(self.highlighted[1]); 1124 } 1125 1126 if (!window.parent || !window.select) return; 1127 // Clear the event property. 1128 if (this.parenEvent) this.parent.clearTimeout(this.parenEvent); 1129 this.parenEvent = null; 1130 1131 // Extract a 'paren' from a piece of text. 1132 function paren(node) { 1133 if (node.currentText) { 1134 var match = node.currentText.match(/^[\s\u00a0]*([\(\)\[\]{}])[\s\u00a0]*$/); 1135 return match && match[1]; 1136 } 1137 } 1138 // Determine the direction a paren is facing. 1139 function forward(ch) { 1140 return /[\(\[\{]/.test(ch); 1141 } 1142 1143 var ch, cursor = select.selectionTopNode(this.container, true); 1144 if (!cursor || !this.highlightAtCursor()) return; 1145 cursor = select.selectionTopNode(this.container, true); 1146 if (!(cursor && ((ch = paren(cursor)) || (cursor = cursor.nextSibling) && (ch = paren(cursor))))) 1147 return; 1148 // We only look for tokens with the same className. 1149 var className = cursor.className, dir = forward(ch), match = matching[ch]; 1150 1151 // Since parts of the document might not have been properly 1152 // highlighted, and it is hard to know in advance which part we 1153 // have to scan, we just try, and when we find dirty nodes we 1154 // abort, parse them, and re-try. 1155 function tryFindMatch() { 1156 var stack = [], ch, ok = true; 1157 for (var runner = cursor; runner; runner = dir ? runner.nextSibling : runner.previousSibling) { 1158 if (runner.className == className && isSpan(runner) && (ch = paren(runner))) { 1159 if (forward(ch) == dir) 1160 stack.push(ch); 1161 else if (!stack.length) 1162 ok = false; 1163 else if (stack.pop() != matching[ch]) 1164 ok = false; 1165 if (!stack.length) break; 1166 } 1167 else if (runner.dirty || !isSpan(runner) && !isBR(runner)) { 1168 return {node: runner, status: "dirty"}; 1169 } 1170 } 1171 return {node: runner, status: runner && ok}; 1172 } 1173 1174 while (true) { 1175 var found = tryFindMatch(); 1176 if (found.status == "dirty") { 1177 this.highlight(found.node, endOfLine(found.node)); 1178 // Needed because in some corner cases a highlight does not 1179 // reach a node. 1180 found.node.dirty = false; 1181 continue; 1182 } 1183 else { 1184 highlight(cursor, found.status); 1185 highlight(found.node, found.status); 1186 if (fromKey) 1187 self.parent.setTimeout(function() {unhighlight(cursor); unhighlight(found.node);}, 500); 1188 else 1189 self.highlighted = [cursor, found.node]; 1190 if (jump && found.node) 1191 select.focusAfterNode(found.node.previousSibling, this.container); 1192 break; 1193 } 1194 } 1195 }, 1196 1197 // Adjust the amount of whitespace at the start of the line that 1198 // the cursor is on so that it is indented properly. 1199 indentAtCursor: function(direction) { 1200 if (!this.container.firstChild) return; 1201 // The line has to have up-to-date lexical information, so we 1202 // highlight it first. 1203 if (!this.highlightAtCursor()) return; 1204 var cursor = select.selectionTopNode(this.container, false); 1205 // If we couldn't determine the place of the cursor, 1206 // there's nothing to indent. 1207 if (cursor === false) 1208 return; 1209 select.markSelection(); 1210 this.indentLineAfter(startOfLine(cursor), direction); 1211 select.selectMarked(); 1212 }, 1213 1214 // Indent all lines whose start falls inside of the current 1215 // selection. 1216 indentRegion: function(start, end, direction) { 1217 var current = (start = startOfLine(start)), before = start && startOfLine(start.previousSibling); 1218 if (!isBR(end)) end = endOfLine(end, this.container); 1219 this.addDirtyNode(start); 1220 1221 do { 1222 var next = endOfLine(current, this.container); 1223 if (current) this.highlight(before, next, true); 1224 this.indentLineAfter(current, direction); 1225 before = current; 1226 current = next; 1227 } while (current != end); 1228 select.setCursorPos(this.container, {node: start, offset: 0}, {node: end, offset: 0}); 1229 }, 1230 1231 // Find the node that the cursor is in, mark it as dirty, and make 1232 // sure a highlight pass is scheduled. 1233 cursorActivity: function(safe) { 1234 // pagehide event hack above 1235 if (this.unloaded) { 1236 window.document.designMode = "off"; 1237 window.document.designMode = "on"; 1238 this.unloaded = false; 1239 } 1240 1241 if (internetExplorer) { 1242 this.container.createTextRange().execCommand("unlink"); 1243 this.selectionSnapshot = select.getBookmark(this.container); 1244 } 1245 1246 var activity = this.options.cursorActivity; 1247 if (!safe || activity) { 1248 var cursor = select.selectionTopNode(this.container, false); 1249 if (cursor === false || !this.container.firstChild) return; 1250 cursor = cursor || this.container.firstChild; 1251 if (activity) activity(cursor); 1252 if (!safe) { 1253 this.scheduleHighlight(); 1254 this.addDirtyNode(cursor); 1255 } 1256 } 1257 }, 1258 1259 reparseBuffer: function() { 1260 forEach(this.container.childNodes, function(node) {node.dirty = true;}); 1261 if (this.container.firstChild) 1262 this.addDirtyNode(this.container.firstChild); 1263 }, 1264 1265 // Add a node to the set of dirty nodes, if it isn't already in 1266 // there. 1267 addDirtyNode: function(node) { 1268 node = node || this.container.firstChild; 1269 if (!node) return; 1270 1271 for (var i = 0; i < this.dirty.length; i++) 1272 if (this.dirty[i] == node) return; 1273 1274 if (node.nodeType != 3) 1275 node.dirty = true; 1276 this.dirty.push(node); 1277 }, 1278 1279 allClean: function() { 1280 return !this.dirty.length; 1281 }, 1282 1283 // Cause a highlight pass to happen in options.passDelay 1284 // milliseconds. Clear the existing timeout, if one exists. This 1285 // way, the passes do not happen while the user is typing, and 1286 // should as unobtrusive as possible. 1287 scheduleHighlight: function() { 1288 // Timeouts are routed through the parent window, because on 1289 // some browsers designMode windows do not fire timeouts. 1290 var self = this; 1291 this.parent.clearTimeout(this.highlightTimeout); 1292 this.highlightTimeout = this.parent.setTimeout(function(){self.highlightDirty();}, this.options.passDelay); 1293 }, 1294 1295 // Fetch one dirty node, and remove it from the dirty set. 1296 getDirtyNode: function() { 1297 while (this.dirty.length > 0) { 1298 var found = this.dirty.pop(); 1299 // IE8 sometimes throws an unexplainable 'invalid argument' 1300 // exception for found.parentNode 1301 try { 1302 // If the node has been coloured in the meantime, or is no 1303 // longer in the document, it should not be returned. 1304 while (found && found.parentNode != this.container) 1305 found = found.parentNode; 1306 if (found && (found.dirty || found.nodeType == 3)) 1307 return found; 1308 } catch (e) {} 1309 } 1310 return null; 1311 }, 1312 1313 // Pick dirty nodes, and highlight them, until options.passTime 1314 // milliseconds have gone by. The highlight method will continue 1315 // to next lines as long as it finds dirty nodes. It returns 1316 // information about the place where it stopped. If there are 1317 // dirty nodes left after this function has spent all its lines, 1318 // it shedules another highlight to finish the job. 1319 highlightDirty: function(force) { 1320 // Prevent FF from raising an error when it is firing timeouts 1321 // on a page that's no longer loaded. 1322 if (!window.parent || !window.select) return false; 1323 1324 if (!this.options.readOnly) select.markSelection(); 1325 var start, endTime = force ? null : time() + this.options.passTime; 1326 while ((time() < endTime || force) && (start = this.getDirtyNode())) { 1327 var result = this.highlight(start, endTime); 1328 if (result && result.node && result.dirty) 1329 this.addDirtyNode(result.node.nextSibling); 1330 } 1331 if (!this.options.readOnly) select.selectMarked(); 1332 if (start) this.scheduleHighlight(); 1333 return this.dirty.length == 0; 1334 }, 1335 1336 // Creates a function that, when called through a timeout, will 1337 // continuously re-parse the document. 1338 documentScanner: function(passTime) { 1339 var self = this, pos = null; 1340 return function() { 1341 // FF timeout weirdness workaround. 1342 if (!window.parent || !window.select) return; 1343 // If the current node is no longer in the document... oh 1344 // well, we start over. 1345 if (pos && pos.parentNode != self.container) 1346 pos = null; 1347 select.markSelection(); 1348 var result = self.highlight(pos, time() + passTime, true); 1349 select.selectMarked(); 1350 var newPos = result ? (result.node && result.node.nextSibling) : null; 1351 pos = (pos == newPos) ? null : newPos; 1352 self.delayScanning(); 1353 }; 1354 }, 1355 1356 // Starts the continuous scanning process for this document after 1357 // a given interval. 1358 delayScanning: function() { 1359 if (this.scanner) { 1360 this.parent.clearTimeout(this.documentScan); 1361 this.documentScan = this.parent.setTimeout(this.scanner, this.options.continuousScanning); 1362 } 1363 }, 1364 1365 // The function that does the actual highlighting/colouring (with 1366 // help from the parser and the DOM normalizer). Its interface is 1367 // rather overcomplicated, because it is used in different 1368 // situations: ensuring that a certain line is highlighted, or 1369 // highlighting up to X milliseconds starting from a certain 1370 // point. The 'from' argument gives the node at which it should 1371 // start. If this is null, it will start at the beginning of the 1372 // document. When a timestamp is given with the 'target' argument, 1373 // it will stop highlighting at that time. If this argument holds 1374 // a DOM node, it will highlight until it reaches that node. If at 1375 // any time it comes across two 'clean' lines (no dirty nodes), it 1376 // will stop, except when 'cleanLines' is true. maxBacktrack is 1377 // the maximum number of lines to backtrack to find an existing 1378 // parser instance. This is used to give up in situations where a 1379 // highlight would take too long and freeze the browser interface. 1380 highlight: function(from, target, cleanLines, maxBacktrack){ 1381 var container = this.container, self = this, active = this.options.activeTokens; 1382 var endTime = (typeof target == "number" ? target : null); 1383 1384 if (!container.firstChild) 1385 return false; 1386 // Backtrack to the first node before from that has a partial 1387 // parse stored. 1388 while (from && (!from.parserFromHere || from.dirty)) { 1389 if (maxBacktrack != null && isBR(from) && (--maxBacktrack) < 0) 1390 return false; 1391 from = from.previousSibling; 1392 } 1393 // If we are at the end of the document, do nothing. 1394 if (from && !from.nextSibling) 1395 return false; 1396 1397 // Check whether a part (<span> node) and the corresponding token 1398 // match. 1399 function correctPart(token, part){ 1400 return !part.reduced && part.currentText == token.value && part.className == token.style; 1401 } 1402 // Shorten the text associated with a part by chopping off 1403 // characters from the front. Note that only the currentText 1404 // property gets changed. For efficiency reasons, we leave the 1405 // nodeValue alone -- we set the reduced flag to indicate that 1406 // this part must be replaced. 1407 function shortenPart(part, minus){ 1408 part.currentText = part.currentText.substring(minus); 1409 part.reduced = true; 1410 } 1411 // Create a part corresponding to a given token. 1412 function tokenPart(token){ 1413 var part = makePartSpan(token.value); 1414 part.className = token.style; 1415 return part; 1416 } 1417 1418 function maybeTouch(node) { 1419 if (node) { 1420 var old = node.oldNextSibling; 1421 if (lineDirty || old === undefined || node.nextSibling != old) 1422 self.history.touch(node); 1423 node.oldNextSibling = node.nextSibling; 1424 } 1425 else { 1426 var old = self.container.oldFirstChild; 1427 if (lineDirty || old === undefined || self.container.firstChild != old) 1428 self.history.touch(null); 1429 self.container.oldFirstChild = self.container.firstChild; 1430 } 1431 } 1432 1433 // Get the token stream. If from is null, we start with a new 1434 // parser from the start of the frame, otherwise a partial parse 1435 // is resumed. 1436 var traversal = traverseDOM(from ? from.nextSibling : container.firstChild), 1437 stream = stringStream(traversal), 1438 parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream); 1439 1440 function surroundedByBRs(node) { 1441 return (node.previousSibling == null || isBR(node.previousSibling)) && 1442 (node.nextSibling == null || isBR(node.nextSibling)); 1443 } 1444 1445 // parts is an interface to make it possible to 'delay' fetching 1446 // the next DOM node until we are completely done with the one 1447 // before it. This is necessary because often the next node is 1448 // not yet available when we want to proceed past the current 1449 // one. 1450 var parts = { 1451 current: null, 1452 // Fetch current node. 1453 get: function(){ 1454 if (!this.current) 1455 this.current = traversal.nodes.shift(); 1456 return this.current; 1457 }, 1458 // Advance to the next part (do not fetch it yet). 1459 next: function(){ 1460 this.current = null; 1461 }, 1462 // Remove the current part from the DOM tree, and move to the 1463 // next. 1464 remove: function(){ 1465 container.removeChild(this.get()); 1466 this.current = null; 1467 }, 1468 // Advance to the next part that is not empty, discarding empty 1469 // parts. 1470 getNonEmpty: function(){ 1471 var part = this.get(); 1472 // Allow empty nodes when they are alone on a line, needed 1473 // for the FF cursor bug workaround (see select.js, 1474 // insertNewlineAtCursor). 1475 while (part && isSpan(part) && part.currentText == "") { 1476 // Leave empty nodes that are alone on a line alone in 1477 // Opera, since that browsers doesn't deal well with 1478 // having 2 BRs in a row. 1479 if (window.opera && surroundedByBRs(part)) { 1480 this.next(); 1481 part = this.get(); 1482 } 1483 else { 1484 var old = part; 1485 this.remove(); 1486 part = this.get(); 1487 // Adjust selection information, if any. See select.js for details. 1488 select.snapshotMove(old.firstChild, part && (part.firstChild || part), 0); 1489 } 1490 } 1491 1492 return part; 1493 } 1494 }; 1495 1496 var lineDirty = false, prevLineDirty = true, lineNodes = 0; 1497 1498 // This forEach loops over the tokens from the parsed stream, and 1499 // at the same time uses the parts object to proceed through the 1500 // corresponding DOM nodes. 1501 forEach(parsed, function(token){ 1502 var part = parts.getNonEmpty(); 1503 1504 if (token.value == "\n"){ 1505 // The idea of the two streams actually staying synchronized 1506 // is such a long shot that we explicitly check. 1507 if (!isBR(part)) 1508 throw "Parser out of sync. Expected BR."; 1509 1510 if (part.dirty || !part.indentation) lineDirty = true; 1511 maybeTouch(from); 1512 from = part; 1513 1514 // Every <br> gets a copy of the parser state and a lexical 1515 // context assigned to it. The first is used to be able to 1516 // later resume parsing from this point, the second is used 1517 // for indentation. 1518 part.parserFromHere = parsed.copy(); 1519 part.indentation = token.indentation; 1520 part.dirty = false; 1521 1522 // If the target argument wasn't an integer, go at least 1523 // until that node. 1524 if (endTime == null && part == target) throw StopIteration; 1525 1526 // A clean line with more than one node means we are done. 1527 // Throwing a StopIteration is the way to break out of a 1528 // MochiKit forEach loop. 1529 if ((endTime != null && time() >= endTime) || (!lineDirty && !prevLineDirty && lineNodes > 1 && !cleanLines)) 1530 throw StopIteration; 1531 prevLineDirty = lineDirty; lineDirty = false; lineNodes = 0; 1532 parts.next(); 1533 } 1534 else { 1535 if (!isSpan(part)) 1536 throw "Parser out of sync. Expected SPAN."; 1537 if (part.dirty) 1538 lineDirty = true; 1539 lineNodes++; 1540 1541 // If the part matches the token, we can leave it alone. 1542 if (correctPart(token, part)){ 1543 part.dirty = false; 1544 parts.next(); 1545 } 1546 // Otherwise, we have to fix it. 1547 else { 1548 lineDirty = true; 1549 // Insert the correct part. 1550 var newPart = tokenPart(token); 1551 container.insertBefore(newPart, part); 1552 if (active) active(newPart, token, self); 1553 var tokensize = token.value.length; 1554 var offset = 0; 1555 // Eat up parts until the text for this token has been 1556 // removed, adjusting the stored selection info (see 1557 // select.js) in the process. 1558 while (tokensize > 0) { 1559 part = parts.get(); 1560 var partsize = part.currentText.length; 1561 select.snapshotReplaceNode(part.firstChild, newPart.firstChild, tokensize, offset); 1562 if (partsize > tokensize){ 1563 shortenPart(part, tokensize); 1564 tokensize = 0; 1565 } 1566 else { 1567 tokensize -= partsize; 1568 offset += partsize; 1569 parts.remove(); 1570 } 1571 } 1572 } 1573 } 1574 }); 1575 maybeTouch(from); 1576 webkitLastLineHack(this.container); 1577 1578 // The function returns some status information that is used by 1579 // hightlightDirty to determine whether and where it has to 1580 // continue. 1581 return {node: parts.getNonEmpty(), 1582 dirty: lineDirty}; 1583 } 1584 }; 1585 1586 return Editor; 1587 })(); 1588 1589 addEventHandler(window, "load", function() { 1590 var CodeMirror = window.frameElement.CodeMirror; 1591 var e = CodeMirror.editor = new Editor(CodeMirror.options); 1592 this.parent.setTimeout(method(CodeMirror, "init"), 0); 1593 });
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Tue May 19 15:55:14 2020 | Cross-referenced by PHPXref 0.7.1 |