From 565fa7864d3b0d3aad3ccabd7757c573ec752216 Mon Sep 17 00:00:00 2001 From: Shandem Date: Mon, 28 Sep 2009 13:06:10 +0000 Subject: [PATCH] DO NOT DOWNLOAD, STABLE RELEASE AVAILABLE ON THE RELEASES TAB Fixed Python editor, Fixed last IE bug with Code editor [TFS Changeset #59521] --- components/umbraco.controls/CodeArea.cs | 26 +- foreign dlls/ClientDependency.Core.dll | Bin 51712 -> 52224 bytes .../config/ClientDependency.config | 2 +- .../presentation/umbraco.presentation.csproj | 1 + .../umbraco/controls/TreeControl.ascx | 4 +- .../controls/TreeControl.ascx.designer.cs | 2 +- .../umbraco/developer/Python/editPython.aspx | 29 +- .../developer/Python/editPython.aspx.cs | 4 +- .../Python/editPython.aspx.designer.cs | 6 +- .../umbraco/developer/Xslt/editXslt.aspx | 6 +- .../umbraco_client/CodeArea/UmbracoEditor.js | 29 +- .../CodeMirror/js/CoreCombined.js | 2687 +++++++++++++++++ .../CodeMirror/js/codemirror.js | 24 +- .../umbraco_client/Tree/UmbracoTree.js | 2 +- .../umbraco_client/Tree/UmbracoTree.min.js | 174 +- 15 files changed, 2795 insertions(+), 201 deletions(-) create mode 100644 umbraco/presentation/umbraco_client/CodeMirror/js/CoreCombined.js diff --git a/components/umbraco.controls/CodeArea.cs b/components/umbraco.controls/CodeArea.cs index b317139af7..a5e569e403 100644 --- a/components/umbraco.controls/CodeArea.cs +++ b/components/umbraco.controls/CodeArea.cs @@ -150,12 +150,7 @@ namespace umbraco.uicontrols { protected string RenderBasicEditor() { - string jsEventCode = @" - if (navigator.userAgent.match('MSIE')) { - //addEvent(m_textEditor, ""select"", function() { storeCaret(this); }); - //addEvent(m_textEditor, ""click"", function() { storeCaret(this); }); - //addEvent(m_textEditor, ""keyup"", function() { storeCaret(this); }); - } + string jsEventCode = @" var m_textEditor = document.getElementById('" + this.ClientID + @"'); tab.watch('" + this.ClientID + @"'); "; @@ -164,6 +159,9 @@ namespace umbraco.uicontrols { protected string RenderCodeEditor() { + //get the client dependency url for the file so that it's compressed + var baseFileSource = ClientDependencyLoader.GetCompositeUrl(new string[] + { GlobalSettings.ClientPath + "/CodeMirror/js/CoreCombined.js" }, ClientDependencyType.Javascript); string[] parserFiles = new string[] { "tokenizejavascript.js", "parsejavascript.js" }; string[] cssFile = new string[] { "jscolors.css", "umbracoCustom.css" }; @@ -188,28 +186,26 @@ namespace umbraco.uicontrols { break; case EditorType.HTML: parserFiles = new string[] { "parsexml.js", "parsecss.js", "tokenizejavascript.js", "parsejavascript.js", "parsehtmlmixed.js" }; - cssFile = new string[] { "xmlcolors.css", "jscolors.css", "csscolors", "umbracoCustom.css" }; + cssFile = new string[] { "xmlcolors.css", "jscolors.css", "csscolors.css", "umbracoCustom.css" }; break; } - + var jsEventCode = @" var textarea = document.getElementById('" + CodeTextBox.ClientID + @"'); var codeEditor = CodeMirror.fromTextArea(textarea, { + basefiles: ['" + baseFileSource + @"'], width: ""100%"", height: ""100%"", tabMode: ""shift"", textWrapping: false, lineNumbers: true, - parserfile: [" + string.Join(",", - parserFiles - .Select(x => string.Format(@"""{0}""", x)) - .ToArray()) + @"], - stylesheet: [" + string.Join(",", + parserfile: ['" + ClientDependencyLoader.GetCompositeUrl(parserFiles, ClientDependencyType.Javascript) + @"'], + stylesheet: ['" + ClientDependencyLoader.GetCompositeUrl( cssFile - .Select(x => string.Format(@"""{0}""", GlobalSettings.ClientPath + @"/CodeMirror/css/" + x)) - .ToArray()) + @"], + .Select(x => GlobalSettings.ClientPath + @"/CodeMirror/css/" + x) + .ToArray(), ClientDependencyType.Css) + @"'], path: """ + GlobalSettings.ClientPath + @"/CodeMirror/js/"", content: textarea.value, autoMatchParens: false," diff --git a/foreign dlls/ClientDependency.Core.dll b/foreign dlls/ClientDependency.Core.dll index b187914b68ba03ba4517ea71c12fb41e8f0e497c..fd115166097340f2bc6b69ecb4b549c80248e87a 100644 GIT binary patch literal 52224 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 literal 51712 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 diff --git a/umbraco/presentation/config/ClientDependency.config b/umbraco/presentation/config/ClientDependency.config index c5df7de364..5a4aff94ab 100644 --- a/umbraco/presentation/config/ClientDependency.config +++ b/umbraco/presentation/config/ClientDependency.config @@ -1,4 +1,4 @@ - + diff --git a/umbraco/presentation/umbraco.presentation.csproj b/umbraco/presentation/umbraco.presentation.csproj index d4acf2638e..ca5f908cbf 100644 --- a/umbraco/presentation/umbraco.presentation.csproj +++ b/umbraco/presentation/umbraco.presentation.csproj @@ -1465,6 +1465,7 @@ + diff --git a/umbraco/presentation/umbraco/controls/TreeControl.ascx b/umbraco/presentation/umbraco/controls/TreeControl.ascx index 34220a6360..8aca4e6991 100644 --- a/umbraco/presentation/umbraco/controls/TreeControl.ascx +++ b/umbraco/presentation/umbraco/controls/TreeControl.ascx @@ -15,8 +15,8 @@ <%----%> -<%----%> - + +<%----%> - - + + -
- - Hide error - - - + + +
diff --git a/umbraco/presentation/umbraco/developer/Python/editPython.aspx.cs b/umbraco/presentation/umbraco/developer/Python/editPython.aspx.cs index 59e7c51bc9..3cef663b11 100644 --- a/umbraco/presentation/umbraco/developer/Python/editPython.aspx.cs +++ b/umbraco/presentation/umbraco/developer/Python/editPython.aspx.cs @@ -47,11 +47,11 @@ namespace umbraco.cms.presentation.developer { base.speechBubble(speechBubbleIcon.error, ui.Text("errors", "pythonErrorHeader", base.getUser()), ui.Text("errors", "pythonErrorText", base.getUser())); errorHolder.Visible = true; - closeErrorMessage.Visible = true; + //closeErrorMessage.Visible = true; errorHolder.Attributes.Add("style", "height: 250px; overflow: auto; border: 1px solid CCC; padding: 5px;"); errorMessage = errorPython.ToString(); pythonError.Text = errorMessage.Replace("\n", "
\n"); - closeErrorMessage.Visible = true; + //closeErrorMessage.Visible = true; } } if (errorMessage == "") diff --git a/umbraco/presentation/umbraco/developer/Python/editPython.aspx.designer.cs b/umbraco/presentation/umbraco/developer/Python/editPython.aspx.designer.cs index 6f6ea55bf5..a1fbf4a7d7 100644 --- a/umbraco/presentation/umbraco/developer/Python/editPython.aspx.designer.cs +++ b/umbraco/presentation/umbraco/developer/Python/editPython.aspx.designer.cs @@ -68,13 +68,13 @@ namespace umbraco.cms.presentation.developer { protected global::System.Web.UI.WebControls.CheckBox SkipTesting; /// - /// closeErrorMessage control. + /// pp_error control. /// /// /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::System.Web.UI.WebControls.Literal closeErrorMessage; + protected global::umbraco.uicontrols.PropertyPanel pp_error; /// /// errorHolder control. @@ -83,7 +83,7 @@ namespace umbraco.cms.presentation.developer { /// Auto-generated field. /// To modify move field declaration from designer file to code-behind file. /// - protected global::System.Web.UI.WebControls.Panel errorHolder; + protected global::System.Web.UI.HtmlControls.HtmlGenericControl errorHolder; /// /// pythonError control. diff --git a/umbraco/presentation/umbraco/developer/Xslt/editXslt.aspx b/umbraco/presentation/umbraco/developer/Xslt/editXslt.aspx index a6a8692ad0..bfa6be9453 100644 --- a/umbraco/presentation/umbraco/developer/Xslt/editXslt.aspx +++ b/umbraco/presentation/umbraco/developer/Xslt/editXslt.aspx @@ -75,8 +75,7 @@ - + @@ -85,8 +84,7 @@ - + diff --git a/umbraco/presentation/umbraco_client/CodeArea/UmbracoEditor.js b/umbraco/presentation/umbraco_client/CodeArea/UmbracoEditor.js index c78a80ba86..1383875aae 100644 --- a/umbraco/presentation/umbraco_client/CodeArea/UmbracoEditor.js +++ b/umbraco/presentation/umbraco_client/CodeArea/UmbracoEditor.js @@ -52,17 +52,10 @@ Umbraco.Sys.registerNamespace("Umbraco.Controls.CodeEditor"); else { this._editor.win.document.body.focus(); //need to restore the focus to the editor body - //if the saved selection (IE only) is not null, then - //reselect the selection there is one, otherwise, expand the non-selection by 1 - //I know, this is wierd but it's an IE issue and this fixes it. + //if the saved selection (IE only) is not null, then if (this._cmSave != null) { - if (this._cmSave.text.length > 0) { - this._cmSave.select(); - } - else { - this._cmSave.expand("character"); - } - } + this._editor.selectLines(this._cmSave.start.line, this._cmSave.start.character, this._cmSave.end.line, this._cmSave.end.character); + } var selection = this._editor.selection(); var replace = (arg3) ? open + arg3 : open; //concat open and arg3, if arg3 specified @@ -100,7 +93,7 @@ Umbraco.Sys.registerNamespace("Umbraco.Controls.CodeEditor"); /// in the editors so that when the selections are lost (i.e. the user types in a different text box /// we'll need to restore the selection when they return focus /// - if (navigator.userAgent.match('MSIE')) { + if (document.all) { var _this = this; if (this._editor == null) { function storeCaret(editEl) { @@ -111,13 +104,13 @@ Umbraco.Sys.registerNamespace("Umbraco.Controls.CodeEditor"); this._control.click( function() {storeCaret(this)} ); this._control.keyup( function() {storeCaret(this)} ); } - else { - //when the editor loses focus, save the current selection - this._editor.win.document.body.onblur = function() - { - _this._cmSave = _this._editor.win.document.selection.createRange(); - return true; - }; + else { + this._editor.options.cursorActivity = function() { + _this._cmSave = { + start: _this._editor.cursorPosition(true), //save start position + end: _this._editor.cursorPosition(false) //save end position + } + } } } } diff --git a/umbraco/presentation/umbraco_client/CodeMirror/js/CoreCombined.js b/umbraco/presentation/umbraco_client/CodeMirror/js/CoreCombined.js new file mode 100644 index 0000000000..06a3e2b8f3 --- /dev/null +++ b/umbraco/presentation/umbraco_client/CodeMirror/js/CoreCombined.js @@ -0,0 +1,2687 @@ +/* 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);}; +} + +// The value used to signal the end of a sequence in iterators. +var StopIteration = {toString: function() {return "StopIteration"}}; + +// Apply a function to each element in a sequence. +function forEach(iter, f) { + if (iter.next) { + try {while (true) f(iter.next());} + catch (e) {if (e != StopIteration) throw e;} + } + else { + for (var i = 0; i < iter.length; i++) + f(iter[i]); + } +} + +// Map a function over a sequence, producing an array of results. +function map(iter, f) { + var accum = []; + forEach(iter, function(val) {accum.push(f(val));}); + return accum; +} + +// Create a predicate function that tests a string againsts a given +// regular expression. No longer used but might be used by 3rd party +// parsers. +function matcher(regexp){ + return function(value){return regexp.test(value);}; +} + +// Test whether a DOM node has a certain CSS class. Much faster than +// the MochiKit equivalent, for some reason. +function hasClass(element, className){ + var classes = element.className; + return classes && new RegExp("(^| )" + className + "($| )").test(classes); +} + +// Insert a DOM node after another node. +function insertAfter(newNode, oldNode) { + var parent = oldNode.parentNode; + parent.insertBefore(newNode, oldNode.nextSibling); + return newNode; +} + +function removeElement(node) { + if (node.parentNode) + node.parentNode.removeChild(node); +} + +function clearElement(node) { + while (node.firstChild) + node.removeChild(node.firstChild); +} + +// Check whether a node is contained in another one. +function isAncestor(node, child) { + while (child = child.parentNode) { + if (node == child) + return true; + } + return false; +} + +// The non-breaking space character. +var nbsp = "\u00a0"; +var matching = {"{": "}", "[": "]", "(": ")", + "}": "{", "]": "[", ")": "("}; + +// Standardize a few unportable event properties. +function normalizeEvent(event) { + if (!event.stopPropagation) { + event.stopPropagation = function() {this.cancelBubble = true;}; + event.preventDefault = function() {this.returnValue = false;}; + } + if (!event.stop) { + event.stop = function() { + this.stopPropagation(); + this.preventDefault(); + }; + } + + if (event.type == "keypress") { + event.code = (event.charCode == null) ? event.keyCode : event.charCode; + event.character = String.fromCharCode(event.code); + } + return event; +} + +// Portably register event handlers. +function addEventHandler(node, type, handler, removeFunc) { + function wrapHandler(event) { + handler(normalizeEvent(event || window.event)); + } + if (typeof node.addEventListener == "function") { + node.addEventListener(type, wrapHandler, false); + if (removeFunc) return function() {node.removeEventListener(type, wrapHandler, false);}; + } + else { + node.attachEvent("on" + type, wrapHandler); + if (removeFunc) return function() {node.detachEvent("on" + type, wrapHandler);}; + } +} + +function nodeText(node) { + return node.textContent || node.innerText || node.nodeValue || ""; +} + +function nodeTop(node) { + var top = 0; + while (node.offsetParent) { + top += node.offsetTop; + node = node.offsetParent; + } + 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"; +} + +;;;;; + +/* String streams are the things fed to parsers (which can feed them + * to a tokenizer if they want). They provide peek and next methods + * for looking at the current character (next 'consumes' this + * character, peek does not), and a get method for retrieving all the + * text that was consumed since the last time get was called. + * + * An easy mistake to make is to let a StopIteration exception finish + * the token stream while there are still characters pending in the + * string stream (hitting the end of the buffer while parsing a + * token). To make it easier to detect such errors, the stringstreams + * throw an exception when this happens. + */ + +// 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){ + // String that's currently being iterated over. + var current = ""; + // Position in that string. + var pos = 0; + // Accumulator for strings that have been iterated over but not + // get()-ed yet. + var accum = ""; + // Make sure there are more characters ready, or throw + // StopIteration. + function ensureChars() { + while (pos == current.length) { + accum += current; + current = ""; // In case source.next() throws + pos = 0; + try {current = source.next();} + catch (e) { + if (e != StopIteration) throw e; + else return false; + } + } + return true; + } + + return { + // Return the next character in the stream. + peek: function() { + if (!ensureChars()) return null; + return current.charAt(pos); + }, + // Get the next character, throw StopIteration if at end, check + // for unused content. + next: function() { + if (!ensureChars()) { + if (accum.length > 0) + throw "End of stringstream reached without emptying buffer ('" + accum + "')."; + else + throw StopIteration; + } + return current.charAt(pos++); + }, + // Return the characters iterated over since the last call to + // .get(). + get: function() { + var temp = accum; + accum = ""; + if (pos > 0){ + temp += current.slice(0, pos); + current = current.slice(pos); + pos = 0; + } + return temp; + }, + // Push a string back into the stream. + push: function(str) { + current = current.slice(0, pos) + str + current.slice(pos); + }, + lookAhead: function(str, consume, skipSpaces, caseInsensitive) { + function cased(str) {return caseInsensitive ? str.toLowerCase() : str;} + str = cased(str); + var found = false; + + var _accum = accum, _pos = pos; + if (skipSpaces) this.nextWhileMatches(/[\s\u00a0]/); + + while (true) { + var end = pos + str.length, left = current.length - pos; + if (end <= current.length) { + found = str == cased(current.slice(pos, end)); + pos = end; + break; + } + else if (str.slice(0, left) == cased(current.slice(pos))) { + accum += current; current = ""; + try {current = source.next();} + catch (e) {break;} + pos = 0; + str = str.slice(left); + } + else { + break; + } + } + + if (!(found && consume)) { + current = accum.slice(_accum.length) + current; + pos = _pos; + accum = _accum; + } + + return found; + }, + + // Utils built on top of the above + more: function() { + return this.peek() !== null; + }, + applies: function(test) { + var next = this.peek(); + return (next !== null && test(next)); + }, + nextWhile: function(test) { + var next; + while ((next = this.peek()) !== null && test(next)) + this.next(); + }, + matches: function(re) { + var next = this.peek(); + return (next !== null && re.test(next)); + }, + nextWhileMatches: function(re) { + var next; + while ((next = this.peek()) !== null && re.test(next)) + this.next(); + }, + equals: function(ch) { + return ch === this.peek(); + }, + endOfLine: function() { + var next = this.peek(); + return next == null || next == "\n"; + } + }; +}; + +;;;;; + +/* Functionality for finding, storing, and restoring selections + * + * This does not provide a generic API, just the minimal functionality + * required by the CodeMirror system. + */ + +// Namespace object. +var select = {}; + +(function() { + select.ie_selection = document.selection && document.selection.createRangeCollection; + + // Find the 'top-level' (defined as 'a direct child of the node + // passed as the top argument') node that the given node is + // contained in. Return null if the given node is not inside the top + // node. + function topLevelNodeAt(node, top) { + while (node && node.parentNode != top) + node = node.parentNode; + return node; + } + + // Find the top-level node that contains the node before this one. + function topLevelNodeBefore(node, top) { + while (!node.previousSibling && node.parentNode != top) + node = node.parentNode; + return topLevelNodeAt(node.previousSibling, top); + } + + var fourSpaces = "\u00a0\u00a0\u00a0\u00a0"; + + select.scrollToNode = function(element) { + if (!element) return; + var 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. + var compensateHack = 0; + while (element && !element.offsetTop) { + compensateHack++; + element = element.previousSibling; + } + // atEnd is another kludge for these browsers -- if the cursor is + // at the end of the document, and the node doesn't have an + // offset, just scroll to the end. + if (compensateHack == 0) atEnd = false; + + var y = compensateHack * (element ? element.offsetHeight : 0), x = 0, pos = element; + while (pos && pos.offsetParent) { + y += pos.offsetTop; + // Don't count X offset for
nodes + 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; + + if (screen_x < 0 || screen_x > (win.innerWidth || html.clientWidth || 0)) { + scroll_x = x; + scroll = true; + } + if (screen_y < 0 || atEnd || screen_y > (win.innerHeight || html.clientHeight || 0) - 50) { + scroll_y = atEnd ? 1e10 : y; + scroll = true; + } + if (scroll) win.scrollTo(scroll_x, scroll_y); + }; + + select.scrollToCursor = function(container) { + select.scrollToNode(select.selectionTopNode(container, true) || container.firstChild); + }; + + // Used to prevent restoring a selection when we do not need to. + var currentSelection = null; + + select.snapshotChanged = function() { + if (currentSelection) currentSelection.changed = true; + }; + + // This is called by the code in editor.js whenever it is replacing + // a text node. The function sees whether the given oldNode is part + // of the current selection, and updates this selection if it is. + // Because nodes are often only partially replaced, the length of + // the part that gets replaced has to be taken into account -- the + // selection might stay in the oldNode if the newNode is smaller + // than the selection's offset. The offset argument is needed in + // case the selection does move to the new object, and the given + // length is not the whole length of the new node (part of it might + // have been used to replace another node). + select.snapshotReplaceNode = function(from, to, length, offset) { + if (!currentSelection) return; + + function replace(point) { + if (from == point.node) { + currentSelection.changed = true; + if (length && point.offset > length) { + point.offset -= length; + } + else { + point.node = to; + point.offset += (offset || 0); + } + } + } + replace(currentSelection.start); + replace(currentSelection.end); + }; + + select.snapshotMove = function(from, to, distance, relative, ifAtStart) { + if (!currentSelection) return; + + function move(point) { + if (from == point.node && (!ifAtStart || point.offset == 0)) { + currentSelection.changed = true; + point.node = to; + if (relative) point.offset = Math.max(0, point.offset + distance); + else point.offset = distance; + } + } + move(currentSelection.start); + move(currentSelection.end); + }; + + // Most functions are defined in two ways, one for the IE selection + // model, one for the W3C one. + if (select.ie_selection) { + function selectionNode(win, start) { + var range = win.document.selection.createRange(); + range.collapse(start); + + function nodeAfter(node) { + var found = null; + while (!found && node) { + found = node.nextSibling; + node = node.parentNode; + } + return nodeAtStartOf(found); + } + + function nodeAtStartOf(node) { + while (node && node.firstChild) node = node.firstChild; + return {node: node, offset: 0}; + } + + var containing = range.parentElement(); + if (!isAncestor(win.document.body, containing)) return null; + if (!containing.firstChild) return nodeAtStartOf(containing); + + var working = range.duplicate(); + working.moveToElementText(containing); + working.collapse(true); + for (var cur = containing.firstChild; cur; cur = cur.nextSibling) { + if (cur.nodeType == 3) { + var size = cur.nodeValue.length; + working.move("character", size); + } + else { + working.moveToElementText(cur); + working.collapse(false); + } + + var dir = range.compareEndPoints("StartToStart", working); + if (dir == 0) return nodeAfter(cur); + if (dir == 1) continue; + if (cur.nodeType != 3) return nodeAtStartOf(cur); + + working.setEndPoint("StartToEnd", range); + return {node: cur, offset: size - working.text.length}; + } + return nodeAfter(containing); + } + + select.markSelection = function(win) { + currentSelection = null; + var sel = win.document.selection; + if (!sel) return; + var start = selectionNode(win, true), + end = selectionNode(win, false); + if (!start || !end) return; + currentSelection = {start: start, end: end, window: win, changed: false}; + }; + + select.selectMarked = function() { + if (!currentSelection || !currentSelection.changed) return; + var win = currentSelection.window, doc = win.document; + + function makeRange(point) { + var range = doc.body.createTextRange(), + node = point.node; + if (!node) { + range.moveToElementText(currentSelection.window.document.body); + range.collapse(false); + } + else if (node.nodeType == 3) { + range.moveToElementText(node.parentNode); + var offset = point.offset; + while (node.previousSibling) { + node = node.previousSibling; + offset += (node.innerText || "").length; + } + range.move("character", offset); + } + else { + range.moveToElementText(node); + range.collapse(true); + } + return range; + } + + var start = makeRange(currentSelection.start), end = makeRange(currentSelection.end); + start.setEndPoint("StartToEnd", end); + start.select(); + }; + + // 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'. + select.selectionTopNode = function(container, start) { + var selection = container.ownerDocument.selection; + if (!selection) return false; + + var range = selection.createRange(), range2 = range.duplicate(); + range.collapse(start); + var around = range.parentElement(); + if (around && isAncestor(container, around)) { + // Only use this node if the selection is not at its start. + range2.moveToElementText(around); + if (range.compareEndPoints("StartToStart", range2) == 1) + return topLevelNodeAt(around, container); + } + + // Move the start of a range to the start of a node, + // compensating for the fact that you can't call + // moveToElementText with text nodes. + function moveToNodeStart(range, node) { + if (node.nodeType == 3) { + var count = 0, cur = node.previousSibling; + while (cur && cur.nodeType == 3) { + count += cur.nodeValue.length; + cur = cur.previousSibling; + } + if (cur) { + try{range.moveToElementText(cur);} + catch(e){return false;} + range.collapse(false); + } + else range.moveToElementText(node.parentNode); + if (count) range.move("character", count); + } + else { + try{range.moveToElementText(node);} + catch(e){return false;} + } + return true; + } + + // Do a binary search through the container object, comparing + // the start of each node to the selection + var start = 0, end = container.childNodes.length - 1; + 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. + if (!moveToNodeStart(range2, node)) return false; + if (range.compareEndPoints("StartToStart", range2) == 1) + start = middle; + else + end = middle - 1; + } + return container.childNodes[start] || null; + }; + + // Place the cursor after this.start. This is only useful when + // manually moving the cursor instead of restoring it to its old + // position. + select.focusAfterNode = function(node, container) { + var range = container.ownerDocument.body.createTextRange(); + range.moveToElementText(node || container); + range.collapse(!node); + range.select(); + }; + + select.somethingSelected = function(win) { + var sel = win.document.selection; + return sel && (sel.createRange().text != ""); + }; + + function insertAtCursor(window, html) { + var selection = window.document.selection; + if (selection) { + var range = selection.createRange(); + range.pasteHTML(html); + range.collapse(false); + range.select(); + } + } + + // Used to normalize the effect of the enter key, since browsers + // do widely different things when pressing enter in designMode. + select.insertNewlineAtCursor = function(window) { + insertAtCursor(window, "
"); + }; + + select.insertTabAtCursor = function(window) { + insertAtCursor(window, fourSpaces); + }; + + // Get the BR node at the start of the line on which the cursor + // currently is, and the offset into the line. Returns null as + // node if cursor is on first line. + select.cursorPos = function(container, start) { + var selection = container.ownerDocument.selection; + if (!selection) return null; + + var topNode = select.selectionTopNode(container, start); + while (topNode && !isBR(topNode)) + topNode = topNode.previousSibling; + + var range = selection.createRange(), range2 = range.duplicate(); + range.collapse(start); + if (topNode) { + range2.moveToElementText(topNode); + range2.collapse(false); + } + else { + // When nothing is selected, we can get all kinds of funky errors here. + try { range2.moveToElementText(container); } + catch (e) { return null; } + range2.collapse(true); + } + range.setEndPoint("StartToStart", range2); + + return {node: topNode, offset: range.text.length}; + }; + + select.setCursorPos = function(container, from, to) { + function rangeAt(pos) { + var range = container.ownerDocument.body.createTextRange(); + if (!pos.node) { + range.moveToElementText(container); + range.collapse(true); + } + else { + range.moveToElementText(pos.node); + range.collapse(false); + } + range.move("character", pos.offset); + return range; + } + + var range = rangeAt(from); + if (to && to != from) + range.setEndPoint("EndToEnd", rangeAt(to)); + range.select(); + } + + // Some hacks for storing and re-storing the selection when the editor loses and regains focus. + select.selectionCoords = function (win) { + var selection = win.document.selection; + if (!selection) return null; + var start = selection.createRange(), end = start.duplicate(); + start.collapse(true); + end.collapse(false); + + var body = win.document.body; + return {start: {x: start.boundingLeft + body.scrollLeft - 1, + y: start.boundingTop + body.scrollTop}, + end: {x: end.boundingLeft + body.scrollLeft - 1, + y: end.boundingTop + body.scrollTop}}; + }; + + // 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) {} + }; + } + // W3C model + else { + // 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 + // selection is restored. + select.markSelection = function (win) { + var selection = win.getSelection(); + if (!selection || selection.rangeCount == 0) + return (currentSelection = null); + var range = selection.getRangeAt(0); + + currentSelection = { + start: {node: range.startContainer, offset: range.startOffset}, + end: {node: range.endContainer, offset: 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 && !isBR(point.node)) { + 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 () { + var cs = currentSelection; + if (!(cs && (cs.changed || (webkit && cs.start.node == cs.end.node)))) return; + var win = cs.window, range = win.document.createRange(); + + function setPoint(point, which) { + if (point.node) { + // Some magic to generalize the setting of the start and end + // of a range. + if (point.offset == 0) + range["set" + which + "Before"](point.node); + else + range["set" + which](point.node, point.offset); + } + else { + range.setStartAfter(win.document.body.lastChild || win.document.body); + } + } + + 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(); + selection.removeAllRanges(); + selection.addRange(range); + }; + function selectionRange(window) { + var selection = window.getSelection(); + if (!selection || selection.rangeCount == 0) + return false; + else + return selection.getRangeAt(0); + } + + // Finding the top-level node at the cursor in the W3C is, as you + // can see, quite an involved process. + select.selectionTopNode = function(container, start) { + var range = selectionRange(container.ownerDocument.defaultView); + if (!range) return false; + + var node = start ? range.startContainer : range.endContainer; + 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] && isBR(container.childNodes[range.startOffset])) + offset--; + + // For text nodes, we look at the node itself if the cursor is + // inside, or at the node before it if the cursor is at the + // start. + if (node.nodeType == 3){ + if (offset > 0) + return topLevelNodeAt(node, container); + else + return topLevelNodeBefore(node, container); + } + // 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.toUpperCase() == "HTML") { + return (offset == 1 ? null : container.lastChild); + } + // If the given node is our 'container', we just look up the + // correct node by using the offset. + else if (node == container) { + return (offset == 0) ? null : node.childNodes[offset - 1]; + } + // In any other case, we have a regular node. If the cursor is + // at the end of the node, we use the node itself, if it is at + // the start, we use the node before it, and in any other + // case, we look up the child before the cursor and use that. + else { + if (offset == node.childNodes.length) + return topLevelNodeAt(node, container); + else if (offset == 0) + return topLevelNodeBefore(node, container); + else + return topLevelNodeAt(node.childNodes[offset - 1], container); + } + }; + + select.focusAfterNode = function(node, container) { + var win = container.ownerDocument.defaultView, + range = win.document.createRange(); + range.setStartBefore(container.firstChild || container); + // In Opera, setting the end of a range at the end of a line + // (before a BR) will cause the cursor to appear on the next + // line, so we set the end inside of the start node when + // possible. + if (node && !node.firstChild) + range.setEndAfter(node); + else if (node) + range.setEnd(node, node.childNodes.length); + else + range.setEndBefore(container.firstChild || container); + range.collapse(false); + selectRange(range, win); + }; + + select.somethingSelected = function(win) { + var range = selectionRange(win); + return range && !range.collapsed; + }; + + function insertNodeAtCursor(window, node) { + var range = selectionRange(window); + if (!range) return; + + range.deleteContents(); + range.insertNode(node); + webkitLastLineHack(window.document.body); + range = window.document.createRange(); + range.selectNode(node); + range.collapse(false); + selectRange(range, window); + } + + select.insertNewlineAtCursor = function(window) { + insertNodeAtCursor(window, window.document.createElement("BR")); + }; + + select.insertTabAtCursor = function(window) { + insertNodeAtCursor(window, window.document.createTextNode(fourSpaces)); + }; + + select.cursorPos = function(container, start) { + var range = selectionRange(window); + if (!range) return; + + var topNode = select.selectionTopNode(container, start); + while (topNode && !isBR(topNode)) + topNode = topNode.previousSibling; + + range = range.cloneRange(); + range.collapse(start); + if (topNode) + range.setStartAfter(topNode); + else + range.setStartBefore(container); + return {node: topNode, offset: range.toString().length}; + }; + + select.setCursorPos = function(container, from, to) { + var win = container.ownerDocument.defaultView, + range = win.document.createRange(); + + function setPoint(node, offset, side) { + if (!node) + node = container.firstChild; + else + node = node.nextSibling; + + if (!node) + return; + + if (offset == 0) { + range["set" + side + "Before"](node); + return true; + } + + var backlog = [] + function decompose(node) { + if (node.nodeType == 3) + backlog.push(node); + else + forEach(node.childNodes, decompose); + } + while (true) { + while (node && !backlog.length) { + decompose(node); + node = node.nextSibling; + } + var cur = backlog.shift(); + if (!cur) return false; + + var length = cur.nodeValue.length; + if (length >= offset) { + range["set" + side](cur, offset); + return true; + } + offset -= length; + } + } + + to = to || from; + if (setPoint(to.node, to.offset, "End") && setPoint(from.node, from.offset, "Start")) + selectRange(range, win); + }; + } +})(); + +;;;;; + +/** + * Storage and control for undo information within a CodeMirror + * editor. 'Why on earth is such a complicated mess required for + * that?', I hear you ask. The goal, in implementing this, was to make + * the complexity of storing and reverting undo information depend + * only on the size of the edited or restored content, not on the size + * of the whole document. This makes it necessary to use a kind of + * 'diff' system, which, when applied to a DOM tree, causes some + * 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 + * 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 + * more 'chains' -- arrays of adjacent lines. Links back to these + * chains are added to the BR nodes, while the chain that previously + * spanned these nodes is added to the undo history. Undoing a change + * means taking such a chain off the undo history, restoring its + * content (text is saved per line) and linking it back into the + * document. + */ + +// A history object needs to know about the DOM container holding the +// document, the maximum amount of undo levels it should store, the +// 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) { + 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 + // start of the first line and the end of the last one are + // represented by null. Since you can not store any properties + // (links to line objects) in null, these properties are used in + // those cases. + this.first = initial; this.last = initial; + // Similarly, a 'historyTouched' property is added to the BR in + // front of lines that have already been touched, and 'firstTouched' + // is used for the first line. + this.firstTouched = false; + // History is the set of committed changes, touched is the set of + // nodes touched since the last commit. + this.history = []; this.redoHistory = []; this.touched = []; +} + +History.prototype = { + // Schedule a commit (if no other touches come in for commitDelay + // milliseconds). + scheduleCommit: function() { + var self = this; + this.parent.clearTimeout(this.commitTimeout); + this.commitTimeout = this.parent.setTimeout(function(){self.tryCommit();}, this.commitDelay); + }, + + // Mark a node as touched. Null is a valid argument. + touch: function(node) { + this.setTouched(node); + this.scheduleCommit(); + }, + + // Undo the last change. + undo: function() { + // Make sure pending changes have been committed. + this.commit(); + + if (this.history.length) { + // Take the top diff from the history, apply it, and store its + // shadow in the redo history. + var item = this.history.pop(); + this.redoHistory.push(this.updateTo(item, "applyChain")); + if (this.onChange) this.onChange(); + return this.chainNode(item); + } + }, + + // Redo the last undone change. + redo: function() { + this.commit(); + if (this.redoHistory.length) { + // The inverse of undo, basically. + var item = this.redoHistory.pop(); + this.addUndoLevel(this.updateTo(item, "applyChain")); + if (this.onChange) this.onChange(); + return this.chainNode(item); + } + }, + + clear: function() { + this.history = []; + this.redoHistory = []; + }, + + // Ask for the size of the un/redo histories. + historySize: function() { + return {undo: this.history.length, redo: this.redoHistory.length}; + }, + + // Push a changeset into the document. + push: function(from, to, lines) { + var chain = []; + for (var i = 0; i < lines.length; i++) { + var end = (i == lines.length - 1) ? to : this.container.ownerDocument.createElement("BR"); + chain.push({from: from, to: end, text: cleanText(lines[i])}); + from = end; + } + this.pushChains([chain], from == null && to == null); + }, + + pushChains: function(chains, doNotHighlight) { + this.commit(doNotHighlight); + this.addUndoLevel(this.updateTo(chains, "applyChain")); + this.redoHistory = []; + }, + + // Retrieve a DOM node from a chain (for scrolling to it after undo/redo). + chainNode: function(chains) { + for (var i = 0; i < chains.length; i++) { + var start = chains[i][0], node = start && (start.from || start.to); + if (node) return node; + } + }, + + // Clear the undo history, make the current document the start + // position. + reset: function() { + this.history = []; this.redoHistory = []; + }, + + textAfter: function(br) { + return this.after(br).text; + }, + + nodeAfter: function(br) { + return this.after(br).to; + }, + + nodeBefore: function(br) { + return this.before(br).from; + }, + + // Commit unless there are pending dirty nodes. + tryCommit: function() { + if (!window.History) return; // Stop when frame has been unloaded + if (this.editor.highlightDirty()) this.commit(true); + else this.scheduleCommit(); + }, + + // Check whether the touched nodes hold any changes, if so, commit + // them. + commit: function(doNotHighlight) { + this.parent.clearTimeout(this.commitTimeout); + // Make sure there are no pending dirty nodes. + if (!doNotHighlight) this.editor.highlightDirty(true); + // Build set of chains. + var chains = this.touchedChains(), self = this; + + if (chains.length) { + this.addUndoLevel(this.updateTo(chains, "linkChain")); + this.redoHistory = []; + if (this.onChange) this.onChange(); + } + }, + + // [ end of public interface ] + + // Update the document with a given set of chains, return its + // shadow. updateFunc should be "applyChain" or "linkChain". In the + // second case, the chains are taken to correspond the the current + // document, and only the state of the line data is updated. In the + // first case, the content of the chains is also pushed iinto the + // document. + updateTo: function(chains, updateFunc) { + var shadows = [], dirty = []; + for (var i = 0; i < chains.length; i++) { + shadows.push(this.shadowChain(chains[i])); + dirty.push(this[updateFunc](chains[i])); + } + if (updateFunc == "applyChain") + this.notifyDirty(dirty); + return shadows; + }, + + // Notify the editor that some nodes have changed. + notifyDirty: function(nodes) { + forEach(nodes, method(this.editor, "addDirtyNode")) + this.editor.scheduleHighlight(); + }, + + // Link a chain into the DOM nodes (or the first/last links for null + // nodes). + linkChain: function(chain) { + for (var i = 0; i < chain.length; i++) { + var line = chain[i]; + if (line.from) line.from.historyAfter = line; + else this.first = line; + if (line.to) line.to.historyBefore = line; + else this.last = line; + } + }, + + // Get the line object after/before a given node. + after: function(node) { + return node ? node.historyAfter : this.first; + }, + before: function(node) { + return node ? node.historyBefore : this.last; + }, + + // Mark a node as touched if it has not already been marked. + setTouched: function(node) { + if (node) { + if (!node.historyTouched) { + this.touched.push(node); + node.historyTouched = true; + } + } + else { + this.firstTouched = true; + } + }, + + // Store a new set of undo info, throw away info if there is more of + // it than allowed. + addUndoLevel: function(diffs) { + this.history.push(diffs); + if (this.history.length > this.maxDepth) + this.history.shift(); + }, + + // Build chains from a set of touched nodes. + touchedChains: function() { + var self = this; + + // The temp system is a crummy hack to speed up determining + // whether a (currently touched) node has a line object associated + // with it. nullTemp is used to store the object for the first + // line, other nodes get it stored in their historyTemp property. + var nullTemp = null; + function temp(node) {return node ? node.historyTemp : nullTemp;} + function setTemp(node, line) { + if (node) node.historyTemp = line; + else nullTemp = line; + } + + function buildLine(node) { + var text = []; + for (var cur = node ? node.nextSibling : self.container.firstChild; + cur && !isBR(cur); cur = cur.nextSibling) + if (cur.currentText) text.push(cur.currentText); + return {from: node, to: cur, text: cleanText(text.join(""))}; + } + + // Filter out unchanged lines and nodes that are no longer in the + // document. Build up line objects for remaining nodes. + var lines = []; + if (self.firstTouched) self.touched.push(null); + forEach(self.touched, function(node) { + if (node && node.parentNode != self.container) return; + + if (node) node.historyTouched = false; + else self.firstTouched = false; + + var line = buildLine(node), shadow = self.after(node); + if (!shadow || shadow.text != line.text || shadow.to != line.to) { + lines.push(line); + setTemp(node, line); + } + }); + + // Get the BR element after/before the given node. + function nextBR(node, dir) { + var link = dir + "Sibling", search = node[link]; + while (search && !isBR(search)) + search = search[link]; + return search; + } + + // Assemble line objects into chains by scanning the DOM tree + // around them. + var chains = []; self.touched = []; + forEach(lines, function(line) { + // Note that this makes the loop skip line objects that have + // been pulled into chains by lines before them. + if (!temp(line.from)) return; + + var chain = [], curNode = line.from, safe = true; + // Put any line objects (referred to by temp info) before this + // one on the front of the array. + while (true) { + var curLine = temp(curNode); + if (!curLine) { + if (safe) break; + else curLine = buildLine(curNode); + } + chain.unshift(curLine); + setTemp(curNode, null); + if (!curNode) break; + safe = self.after(curNode); + curNode = nextBR(curNode, "previous"); + } + curNode = line.to; safe = self.before(line.from); + // Add lines after this one at end of array. + while (true) { + if (!curNode) break; + var curLine = temp(curNode); + if (!curLine) { + if (safe) break; + else curLine = buildLine(curNode); + } + chain.push(curLine); + setTemp(curNode, null); + safe = self.before(curNode); + curNode = nextBR(curNode, "next"); + } + chains.push(chain); + }); + + return chains; + }, + + // Find the 'shadow' of a given chain by following the links in the + // DOM nodes at its start and end. + shadowChain: function(chain) { + var shadows = [], next = this.after(chain[0].from), end = chain[chain.length - 1].to; + while (true) { + shadows.push(next); + var nextNode = next.to; + if (!nextNode || nextNode == end) + break; + else + next = nextNode.historyAfter || this.before(end); + // (The this.before(end) is a hack -- FF sometimes removes + // properties from BR nodes, in which case the best we can hope + // for is to not break.) + } + return shadows; + }, + + // Update the DOM tree to contain the lines specified in a given + // chain, link this chain into the DOM nodes. + applyChain: function(chain) { + // Some attempt is made to prevent the cursor from jumping + // randomly when an undo or redo happens. It still behaves a bit + // strange sometimes. + var cursor = select.cursorPos(this.container, false), self = this; + + // Remove all nodes in the DOM tree between from and to (null for + // start/end of container). + function removeRange(from, to) { + var pos = from ? from.nextSibling : self.container.firstChild; + while (pos != to) { + var temp = pos.nextSibling; + removeElement(pos); + pos = temp; + } + } + + var start = chain[0].from, end = chain[chain.length - 1].to; + // Clear the space where this change has to be made. + removeRange(start, end); + + // Insert the content specified by the chain into the DOM tree. + for (var i = 0; i < chain.length; i++) { + var line = chain[i]; + // The start and end of the space are already correct, but BR + // tags inside it have to be put back. + if (i > 0) + self.container.insertBefore(line.from, end); + + // Add the text. + var node = makePartSpan(fixSpaces(line.text), this.container.ownerDocument); + self.container.insertBefore(node, end); + // See if the cursor was on this line. Put it back, adjusting + // for changed line length, if it was. + if (cursor && cursor.node == line.from) { + var cursordiff = 0; + var prev = this.after(line.from); + if (prev && i == chain.length - 1) { + // Only adjust if the cursor is after the unchanged part of + // the line. + for (var match = 0; match < cursor.offset && + line.text.charAt(match) == prev.text.charAt(match); match++); + if (cursor.offset > match) + cursordiff = line.text.length - prev.text.length; + } + select.setCursorPos(this.container, {node: line.from, offset: Math.max(0, cursor.offset + cursordiff)}); + } + // Cursor was in removed line, this is last new line. + else if (cursor && (i == chain.length - 1) && cursor.node && cursor.node.parentNode != this.container) { + select.setCursorPos(this.container, {node: line.from, offset: line.text.length}); + } + } + + // Anchor the chain in the DOM tree. + this.linkChain(chain); + return start; + } +}; + +;;;;; + +/* The Editor object manages the content of the editable frame. It + * catches events, colours nodes, and indents lines. This file also + * holds some functions for transforming arbitrary DOM structures into + * plain sequences of and
elements + */ + +// 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; + } + return buffer.join(""); +} + +// Create a set of white-space characters that will not be collapsed +// 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);}) + .replace(/[ \u00a0]{2,}/g, function(s) {return makeWhiteSpace(s.length);}); +} + +function cleanText(text) { + return text.replace(/\u00a0/g, " ").replace(/\u200b/g, ""); +} + +// Create a SPAN node with the expected properties for document part +// spans. +function makePartSpan(value, doc) { + var text = value; + if (value.nodeType == 3) text = value.nodeValue; + else value = doc.createTextNode(text); + + var span = doc.createElement("SPAN"); + span.isPart = true; + span.appendChild(value); + span.currentText = text; + 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. + var newlineElements = {"P": true, "DIV": true, "LI": true}; + + function asEditorLines(string) { + var tab = makeWhiteSpace(indentUnit); + return map(string.replace(/\t/g, tab).replace(/\u00a0/g, " ").replace(/\r\n?/g, "\n").split("\n"), fixSpaces); + } + + // Helper function for traverseDOM. Flattens an arbitrary DOM node + // into an array of textnodes and
tags. + function simplifyDOM(root, atEnd) { + var doc = root.ownerDocument; + var result = []; + var leaving = true; + + function simplifyNode(node, top) { + if (node.nodeType == 3) { + var text = node.nodeValue = fixSpaces(node.nodeValue.replace(/[\r\u200b]/g, "").replace(/\n/g, " ")); + if (text.length) leaving = false; + result.push(node); + } + else if (isBR(node) && node.childNodes.length == 0) { + leaving = true; + result.push(node); + } + else { + forEach(node.childNodes, simplifyNode); + if (!leaving && newlineElements.hasOwnProperty(node.nodeName.toUpperCase())) { + leaving = true; + if (!atEnd || !top) + result.push(doc.createElement("BR")); + } + } + } + + simplifyNode(root, true); + return result; + } + + // Creates a MochiKit-style iterator that goes over a series of DOM + // nodes. The values it yields are strings, the textual content of + // the nodes. It makes sure that all nodes up to and including the + // one whose text is being yielded have been 'normalized' to be just + // and
elements. + // See the story.html file for some short remarks about the use of + // continuation-passing style in this iterator. + function traverseDOM(start){ + function yield(value, c){cc = c; return value;} + function push(fun, arg, c){return function(){return fun(arg, c);};} + function stop(){cc = stop; throw StopIteration;}; + var cc = push(scanNode, start, stop); + var owner = start.ownerDocument; + var nodeQueue = []; + + // Create a function that can be used to insert nodes after the + // one given as argument. + function pointAt(node){ + var parent = node.parentNode; + var next = node.nextSibling; + return function(newnode) { + parent.insertBefore(newnode, next); + }; + } + var point = null; + + // Insert a normalized node at the current point. If it is a text + // node, wrap it in a , and give that span a currentText + // property -- this is used to cache the nodeValue, because + // directly accessing nodeValue is horribly slow on some browsers. + // The dirty property is used by the highlighter to determine + // which parts of the document have to be re-highlighted. + function insertPart(part){ + var text = "\n"; + if (part.nodeType == 3) { + select.snapshotChanged(); + part = makePartSpan(part, owner); + text = part.currentText; + } + part.dirty = true; + nodeQueue.push(part); + point(part); + return text; + } + + // Extract the text and newlines from a DOM node, insert them into + // the document, and yield 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); + } + + // Check whether a node is a normalized element. + function partNode(node){ + if (node.isPart && node.childNodes.length == 1 && node.firstChild.nodeType == 3) { + node.currentText = node.firstChild.nodeValue; + return !/[\n\t\r]/.test(node.currentText); + } + 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); + + if (partNode(node)){ + nodeQueue.push(node); + return yield(node.currentText, c); + } + else if (isBR(node)) { + nodeQueue.push(node); + return yield("\n", c); + } + else { + var end = !node.nextSibling; + point = pointAt(node); + removeElement(node); + return writeNode(node, c, 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}; + } + + // Determine the text size of a processed node. + function nodeSize(node) { + 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 && !isBR(node)) node = node.previousSibling; + return node; + } + function endOfLine(node, container) { + if (!node) node = container.firstChild; + else if (isBR(node)) node = node.nextSibling; + + while (node && !isBR(node)) node = node.nextSibling; + return node; + } + + function time() {return new Date().getTime();} + + // Client interface for searching the content of the editor. Create + // these by calling CodeMirror.getSearchCursor. To use, call + // findNext on the resulting object -- this returns a boolean + // 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) { + this.editor = editor; + this.history = editor.history; + this.history.commit(); + + // Are we currently at an occurrence of the search string? + this.atOccurrence = false; + // The object stores a set of nodes coming after its current + // position, so that when the current point is taken out of the + // DOM tree, we can still try to continue. + this.fallbackSize = 15; + var cursor; + // Start from the cursor when specified and a cursor can be found. + if (fromCursor && (cursor = select.cursorPos(this.editor.container))) { + this.line = cursor.node; + this.offset = cursor.offset; + } + else { + this.line = null; + this.offset = 0; + } + this.valid = !!string; + + // Create a matcher function based on the kind of string we have. + var target = string.split("\n"), self = this; + this.matches = (target.length == 1) ? + // For one-line strings, searching can be done simply by calling + // indexOf on the current line. + function() { + var match = cleanText(self.history.textAfter(self.line).slice(self.offset)).indexOf(string); + if (match > -1) + return {from: {node: self.line, offset: self.offset + match}, + to: {node: self.line, offset: self.offset + match + string.length}}; + } : + // Multi-line strings require internal iteration over lines, and + // some clunky checks to make sure the first match ends at the + // 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 = 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 (cleanText(self.history.textAfter(line)) != target[i]) + return false; + line = self.history.nodeAfter(line); + } + + if (cleanText(self.history.textAfter(line)).indexOf(target[target.length - 1]) != 0) + return false; + + return {from: {node: self.line, offset: startOffset}, + to: {node: line, offset: target[target.length - 1].length}}; + }; + } + + SearchCursor.prototype = { + findNext: function() { + if (!this.valid) return false; + this.atOccurrence = false; + var self = this; + + // Go back to the start of the document if the current line is + // no longer in the DOM tree. + if (this.line && !this.line.parentNode) { + this.line = null; + this.offset = 0; + } + + // Set the cursor's position one character after the given + // position. + function saveAfter(pos) { + if (self.history.textAfter(pos.node).length > pos.offset) { + self.line = pos.node; + self.offset = pos.offset + 1; + } + else { + self.line = self.history.nodeAfter(pos.node); + self.offset = 0; + } + } + + while (true) { + var match = this.matches(); + // Found the search string. + if (match) { + this.atOccurrence = match; + saveAfter(match.from); + return true; + } + this.line = this.history.nodeAfter(this.line); + this.offset = 0; + // End of document. + if (!this.line) { + this.valid = false; + return false; + } + } + }, + + select: function() { + if (this.atOccurrence) { + select.setCursorPos(this.editor.container, this.atOccurrence.from, this.atOccurrence.to); + select.scrollToCursor(this.editor.container); + } + }, + + replace: function(string) { + if (this.atOccurrence) { + var end = this.editor.replaceRange(this.atOccurrence.from, this.atOccurrence.to, string); + this.line = end.node; + this.offset = end.offset; + this.atOccurrence = false; + } + } + }; + + // The Editor object is the main inside-the-iframe interface. + function Editor(options) { + this.options = options; + window.indentUnit = options.indentUnit; + this.parent = parent; + 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); + var self = this; + + if (!Editor.Parser) + throw "No parser loaded."; + if (options.parserConfig && Editor.Parser.configure) + Editor.Parser.configure(options.parserConfig); + + if (!options.readOnly) + select.setCursorPos(container, {node: null, offset: 0}); + + this.dirty = []; + if (options.content) + this.importCode(options.content); + + if (!options.readOnly) { + if (options.continuousScanning !== false) { + this.scanner = this.documentScanner(options.passTime); + this.delayScanning(); + } + + function setEditable() { + // In IE, designMode frames can not run any scripts, so we use + // contentEditable instead. + if (document.body.contentEditable != undefined && internetExplorer) + document.body.contentEditable = "true"; + else + document.designMode = "on"; + + document.documentElement.style.borderWidth = "0"; + if (!options.textWrapping) + container.style.whiteSpace = "nowrap"; + } + + // If setting the frame editable fails, try again when the user + // focus it (happens when the frame is not visible on + // initialisation, in Firefox). + try { + setEditable(); + } + catch(e) { + var focusEvent = addEventHandler(document, "focus", function() { + focusEvent(); + setEditable(); + }, true); + } + + addEventHandler(document, "keydown", method(this, "keyDown")); + addEventHandler(document, "keypress", method(this, "keyPress")); + addEventHandler(document, "keyup", method(this, "keyUp")); + + function cursorActivity() {self.cursorActivity(false);} + addEventHandler(document.body, "mouseup", cursorActivity); + addEventHandler(document.body, "cut", cursorActivity); + + addEventHandler(document.body, "paste", function(event) { + cursorActivity(); + var text = null; + try { + var clipboardData = event.clipboardData || window.clipboardData; + if (clipboardData) text = clipboardData.getData('Text'); + } + catch(e) {} + if (text !== null) { + self.replaceSelection(text); + event.stop(); + } + }); + + addEventHandler(document.body, "beforepaste", method(this, "reroutePasteEvent")); + + if (this.options.autoMatchParens) + addEventHandler(document.body, "click", method(this, "scheduleParenBlink")); + } + else if (!options.textWrapping) { + container.style.whiteSpace = "nowrap"; + } + } + + function isSafeKey(code) { + return (code >= 16 && code <= 18) || // shift, control, alt + (code >= 33 && code <= 40); // arrows, home, end + } + + Editor.prototype = { + // Import a piece of code into the editor. + importCode: function(code) { + this.history.push(null, null, asEditorLines(code)); + this.history.reset(); + }, + + // Extract the code from the editor. + getCode: function() { + if (!this.container.firstChild) + return ""; + + var accum = []; + select.markSelection(this.win); + forEach(traverseDOM(this.container.firstChild), method(accum, "push")); + webkitLastLineHack(this.container); + select.selectMarked(); + return cleanText(accum.join("")); + }, + + checkLine: function(node) { + if (node === false || !(node == null || node.parentNode == this.container)) + throw parent.CodeMirror.InvalidLineHandle; + }, + + cursorPosition: function(start) { + if (start == null) start = true; + var pos = select.cursorPos(this.container, start); + if (pos) return {line: pos.node, character: pos.offset}; + else return {line: null, character: 0}; + }, + + firstLine: function() { + return null; + }, + + lastLine: function() { + if (this.container.lastChild) return startOfLine(this.container.lastChild); + else return null; + }, + + nextLine: function(line) { + this.checkLine(line); + var end = endOfLine(line, this.container); + return end || false; + }, + + prevLine: function(line) { + this.checkLine(line); + if (line == null) return false; + return startOfLine(line.previousSibling); + }, + + selectLines: function(startLine, startOffset, endLine, endOffset) { + this.checkLine(startLine); + var start = {node: startLine, offset: startOffset}, end = null; + if (endOffset !== undefined) { + this.checkLine(endLine); + end = {node: endLine, offset: endOffset}; + } + select.setCursorPos(this.container, start, end); + select.scrollToCursor(this.container); + }, + + lineContent: function(line) { + this.checkLine(line); + var accum = []; + for (line = line ? line.nextSibling : this.container.firstChild; + line && !isBR(line); line = line.nextSibling) + accum.push(nodeText(line)); + return cleanText(accum.join("")); + }, + + setLineContent: function(line, content) { + this.history.commit(); + this.replaceRange({node: line, offset: 0}, + {node: line, offset: this.history.textAfter(line).length}, + content); + this.addDirtyNode(line); + this.scheduleHighlight(); + }, + + insertIntoLine: function(line, position, content) { + var before = null; + if (position == "end") { + before = endOfLine(line, this.container); + } + else { + for (var cur = line ? line.nextSibling : this.container.firstChild; cur; cur = cur.nextSibling) { + if (position == 0) { + before = cur; + break; + } + var text = nodeText(cur); + if (text.length > position) { + before = cur.nextSibling; + content = text.slice(0, position) + content + text.slice(position); + removeElement(cur); + break; + } + position -= text.length; + } + } + + var lines = asEditorLines(content), doc = this.container.ownerDocument; + for (var i = 0; i < lines.length; i++) { + if (i > 0) this.container.insertBefore(doc.createElement("BR"), before); + this.container.insertBefore(makePartSpan(lines[i], doc), before); + } + this.addDirtyNode(line); + this.scheduleHighlight(); + }, + + // Retrieve the selected text. + selectedText: function() { + var h = this.history; + h.commit(); + + var start = select.cursorPos(this.container, true), + end = select.cursorPos(this.container, false); + if (!start || !end) return ""; + + if (start.node == end.node) + return h.textAfter(start.node).slice(start.offset, end.offset); + + var text = [h.textAfter(start.node).slice(start.offset)]; + for (var pos = h.nodeAfter(start.node); pos != end.node; pos = h.nodeAfter(pos)) + text.push(h.textAfter(pos)); + text.push(h.textAfter(end.node).slice(0, end.offset)); + return cleanText(text.join("\n")); + }, + + // Replace the selection with another piece of text. + replaceSelection: function(text) { + this.history.commit(); + + var start = select.cursorPos(this.container, true), + end = select.cursorPos(this.container, false); + if (!start || !end) return; + + end = this.replaceRange(start, end, text); + select.setCursorPos(this.container, end); + webkitLastLineHack(this.container); + }, + + 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); + parent.focus(); + te.focus(); + + var self = this; + this.parent.setTimeout(function() { + self.capturingPaste = false; + self.win.focus(); + if (self.selectionSnapshot) // IE hack + self.win.select.selectCoords(self.win, self.selectionSnapshot); + var text = te.value; + if (text) self.replaceSelection(text); + removeElement(te); + }, 10); + }, + + replaceRange: function(from, to, text) { + var lines = asEditorLines(text); + lines[0] = this.history.textAfter(from.node).slice(0, from.offset) + lines[0]; + var lastLine = lines[lines.length - 1]; + lines[lines.length - 1] = lastLine + this.history.textAfter(to.node).slice(to.offset); + var end = this.history.nodeAfter(to.node); + this.history.push(from.node, end, lines); + return {node: this.history.nodeBefore(end), + offset: lastLine.length}; + }, + + getSearchCursor: function(string, fromCursor) { + return new SearchCursor(this, string, fromCursor); + }, + + // Re-indent the whole buffer + reindent: function() { + if (this.container.firstChild) + this.indentRegion(null, this.container.lastChild); + }, + + reindentSelection: function(direction) { + if (!select.somethingSelected(this.win)) { + this.indentAtCursor(direction); + } + else { + var start = select.selectionTopNode(this.container, true), + end = select.selectionTopNode(this.container, false); + if (start === false || end === false) return; + this.indentRegion(start, end, direction); + } + }, + + grabKeys: function(eventHandler, filter) { + this.frozen = eventHandler; + this.keyFilter = filter; + }, + ungrabKeys: function() { + this.frozen = "leave"; + this.keyFilter = null; + }, + + setParser: function(name) { + Editor.Parser = window[name]; + if (this.container.firstChild) { + forEach(this.container.childNodes, function(n) { + if (n.nodeType != 3) n.dirty = true; + }); + this.addDirtyNode(this.firstChild); + this.scheduleHighlight(); + } + }, + + // 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.keyCode))) { + event.stop(); + this.frozen(event); + return; + } + + var code = 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(); + + // The various checks for !altKey are there because AltGr sets both + // ctrlKey and altKey to true, and should not be recognised as + // Control. + if (code == 13) { // enter + if (event.ctrlKey && !event.altKey) { + this.reparseBuffer(); + } + else { + select.insertNewlineAtCursor(this.win); + this.indentAtCursor(); + select.scrollToCursor(this.container); + } + event.stop(); + } + else if (code == 9 && this.options.tabMode != "default") { // tab + this.handleTab(!event.ctrlKey && !event.shiftKey); + event.stop(); + } + else if (code == 32 && event.shiftKey && this.options.tabMode == "default") { // space + this.handleTab(true); + event.stop(); + } + else if (code == 36 && !event.shiftKey && !event.ctrlKey) { // home + if (this.home()) + event.stop(); + } + else if ((code == 219 || code == 221) && event.ctrlKey && !event.altKey) { // [, ] + this.blinkParens(event.shiftKey); + 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); + } + event.stop(); + } + else if ((event.ctrlKey || event.metaKey) && !event.altKey) { + if ((event.shiftKey && code == 90) || code == 89) { // shift-Z, Y + select.scrollToNode(this.history.redo()); + event.stop(); + } + else if (code == 90 || (safari && code == 8)) { // Z, backspace + select.scrollToNode(this.history.undo()); + event.stop(); + } + else if (code == 83 && this.options.saveFunction) { // S + this.options.saveFunction(); + event.stop(); + } + } + }, + + // Check for characters that should re-indent the current line, + // and prevent Opera from handling enter and tab anyway. + keyPress: function(event) { + 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))) || + event.code == 13 || (event.code == 9 && this.options.tabMode != "default") || + (event.keyCode == 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(); + }, + + // Mark the node at the cursor dirty when a non-safe key is + // released. + keyUp: function(event) { + this.cursorActivity(isSafeKey(event.keyCode)); + }, + + // Indent the line following a given
, or null for the first + // line. If given a
element, this must have been highlighted + // so that it has an indentation method. Returns the whitespace + // element that has been modified or created (if any). + indentLineAfter: function(start, direction) { + // whiteSpace is the whitespace span at the start of the line, + // or null if there is no such node. + var whiteSpace = start ? start.nextSibling : this.container.firstChild; + if (whiteSpace && !hasClass(whiteSpace, "whitespace")) + whiteSpace = null; + + // Sometimes the start of the line can influence the correct + // indentation, so we retrieve it. + var firstText = whiteSpace ? whiteSpace.nextSibling : (start ? start.nextSibling : this.container.firstChild); + var nextChars = (start && firstText && firstText.currentText) ? firstText.currentText : ""; + + // Ask the lexical context for the correct indentation, and + // compute how much this differs from the current indentation. + var newIndent = 0, curIndent = whiteSpace ? whiteSpace.currentText.length : 0; + if (direction != null && this.options.tabMode == "shift") + newIndent = direction ? curIndent + indentUnit : Math.max(0, curIndent - indentUnit) + else if (start) + newIndent = start.indentation(nextChars, curIndent, direction); + else if (Editor.Parser.firstIndentation) + newIndent = Editor.Parser.firstIndentation(nextChars, curIndent, direction); + var indentDiff = newIndent - curIndent; + + // If there is too much, this is just a matter of shrinking a span. + if (indentDiff < 0) { + if (newIndent == 0) { + if (firstText) select.snapshotMove(whiteSpace.firstChild, firstText.firstChild, 0); + removeElement(whiteSpace); + whiteSpace = null; + } + else { + select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true); + whiteSpace.currentText = makeWhiteSpace(newIndent); + whiteSpace.firstChild.nodeValue = whiteSpace.currentText; + } + } + // Not enough... + else if (indentDiff > 0) { + // If there is whitespace, we grow it. + if (whiteSpace) { + whiteSpace.currentText = makeWhiteSpace(newIndent); + whiteSpace.firstChild.nodeValue = whiteSpace.currentText; + } + // Otherwise, we have to add a new whitespace node. + else { + whiteSpace = makePartSpan(makeWhiteSpace(newIndent), this.doc); + whiteSpace.className = "whitespace"; + if (start) insertAfter(whiteSpace, start); + else this.container.insertBefore(whiteSpace, this.container.firstChild); + } + 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; + + select.markSelection(this.win); + if (this.highlight(pos, endOfLine(to, this.container), true, 20) === false) + return false; + select.selectMarked(); + return true; + }, + + // When tab is pressed with text selected, the whole selection is + // re-indented, when nothing is selected, the line with the cursor + // is re-indented. + handleTab: function(direction) { + if (this.options.tabMode == "spaces") + select.insertTabAtCursor(this.win); + else + this.reindentSelection(direction); + }, + + home: function() { + var cur = select.selectionTopNode(this.container, true), start = cur; + if (cur === false || !(!cur || cur.isPart || isBR(cur)) || !this.container.firstChild) + return false; + + 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); + else + select.focusAfterNode(cur, this.container); + + select.scrollToCursor(this.container); + return true; + }, + + // Delay (or initiate) the next paren blink event. + scheduleParenBlink: function() { + if (this.parenEvent) this.parent.clearTimeout(this.parenEvent); + var self = this; + this.parenEvent = this.parent.setTimeout(function(){self.blinkParens();}, 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) { + if (!window.select) return; + // Clear the event property. + if (this.parenEvent) this.parent.clearTimeout(this.parenEvent); + this.parenEvent = null; + + // Extract a 'paren' from a piece of text. + function paren(node) { + if (node.currentText) { + var match = node.currentText.match(/^[\s\u00a0]*([\(\)\[\]{}])[\s\u00a0]*$/); + return match && match[1]; + } + } + // Determine the direction a paren is facing. + function forward(ch) { + return /[\(\[\{]/.test(ch); + } + + var ch, self = this, 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))))) + return; + // We only look for tokens with the same className. + var className = cursor.className, dir = forward(ch), match = matching[ch]; + + // Since parts of the document might not have been properly + // highlighted, and it is hard to know in advance which part we + // 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;; + for (var runner = cursor; runner; runner = dir ? runner.nextSibling : runner.previousSibling) { + if (runner.className == className && isSpan(runner) && (ch = paren(runner))) { + if (forward(ch) == dir) + stack.push(ch); + else if (!stack.length) + ok = false; + else if (stack.pop() != matching[ch]) + ok = false; + if (!stack.length) break; + } + 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(); + if (found.status == "dirty") { + this.highlight(found.node, endOfLine(found.node)); + // Needed because in some corner cases a highlight does not + // reach a node. + found.node.dirty = false; + continue; + } + else { + blink(cursor, found.status); + if (found.node) { + blink(found.node, found.status); + if (jump) select.focusAfterNode(found.node.previousSibling, this.container); + } + break; + } + } + }, + + // Adjust the amount of whitespace at the start of the line that + // the cursor is on so that it is indented properly. + indentAtCursor: function(direction) { + if (!this.container.firstChild) return; + // The line has to have up-to-date lexical information, so we + // highlight it first. + if (!this.highlightAtCursor()) return; + var cursor = select.selectionTopNode(this.container, false); + // If we couldn't determine the place of the cursor, + // 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); + }, + + // 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 (!isBR(end)) end = endOfLine(end, this.container); + + do { + var next = endOfLine(current, this.container); + if (current) this.highlight(before, next, true); + this.indentLineAfter(current, direction); + before = current; + current = next; + } while (current != end); + select.setCursorPos(this.container, {node: start, offset: 0}, {node: end, offset: 0}); + }, + + // Find the node that the cursor is in, mark it as dirty, and make + // sure a highlight pass is scheduled. + cursorActivity: function(safe) { + if (internetExplorer) { + this.container.createTextRange().execCommand("unlink"); + this.selectionSnapshot = select.selectionCoords(this.win); + } + + var activity = this.options.cursorActivity; + if (!safe || activity) { + var cursor = select.selectionTopNode(this.container, false); + if (cursor === false || !this.container.firstChild) return; + cursor = cursor || this.container.firstChild; + if (activity) activity(cursor); + if (!safe) { + this.scheduleHighlight(); + this.addDirtyNode(cursor); + } + } + }, + + reparseBuffer: function() { + forEach(this.container.childNodes, function(node) {node.dirty = true;}); + if (this.container.firstChild) + this.addDirtyNode(this.container.firstChild); + }, + + // Add a node to the set of dirty nodes, if it isn't already in + // there. + addDirtyNode: function(node) { + node = node || this.container.firstChild; + if (!node) return; + + for (var i = 0; i < this.dirty.length; i++) + if (this.dirty[i] == node) return; + + if (node.nodeType != 3) + node.dirty = true; + this.dirty.push(node); + }, + + // 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 + // should as unobtrusive as possible. + scheduleHighlight: function() { + // Timeouts are routed through the parent window, because on + // some browsers designMode windows do not fire timeouts. + var self = this; + this.parent.clearTimeout(this.highlightTimeout); + this.highlightTimeout = this.parent.setTimeout(function(){self.highlightDirty();}, this.options.passDelay); + }, + + // Fetch one dirty node, and remove it from the dirty set. + getDirtyNode: function() { + while (this.dirty.length > 0) { + var found = this.dirty.pop(); + // IE8 sometimes throws an unexplainable 'invalid argument' + // exception for found.parentNode + try { + // 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 + if (found && (found.dirty || found.nodeType == 3)) + return found; + } catch (e) {} + } + return null; + }, + + // Pick dirty nodes, and highlight them, until options.passTime + // milliseconds have gone by. The highlight method will continue + // to next lines as long as it finds dirty nodes. It returns + // information about the place where it stopped. If there are + // dirty nodes left after this function has spent all its lines, + // it shedules another highlight to finish the job. + 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 (!this.options.readOnly) select.markSelection(this.win); + var start, endTime = force ? null : time() + this.options.passTime; + while ((time() < endTime || force) && (start = this.getDirtyNode())) { + var result = this.highlight(start, endTime); + if (result && result.node && result.dirty) + this.addDirtyNode(result.node); + } + if (!this.options.readOnly) select.selectMarked(); + if (start) this.scheduleHighlight(); + return this.dirty.length == 0; + }, + + // Creates a function that, when called through a timeout, will + // continuously re-parse the document. + documentScanner: function(passTime) { + var self = this, pos = null; + return function() { + // FF timeout weirdness workaround. + if (!window.select) return; + // If the current node is no longer in the document... oh + // well, we start over. + if (pos && pos.parentNode != self.container) + pos = null; + select.markSelection(self.win); + var result = self.highlight(pos, time() + passTime, true); + select.selectMarked(); + var newPos = result ? (result.node && result.node.nextSibling) : null; + pos = (pos == newPos) ? null : newPos; + self.delayScanning(); + }; + }, + + // Starts the continuous scanning process for this document after + // a given interval. + delayScanning: function() { + if (this.scanner) { + this.parent.clearTimeout(this.documentScan); + this.documentScan = this.parent.setTimeout(this.scanner, this.options.continuousScanning); + } + }, + + // 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 + // situations: ensuring that a certain line is highlighted, or + // highlighting up to X milliseconds starting from a certain + // point. The 'from' argument gives the node at which it should + // start. If this is null, it will start at the beginning of the + // document. When a timestamp is given with the 'target' argument, + // it will stop highlighting at that time. If this argument holds + // a DOM node, it will highlight until it reaches that node. If at + // any time it comes across two 'clean' lines (no dirty nodes), it + // will stop, except when 'cleanLines' is true. maxBacktrack is + // the maximum number of lines to backtrack to find an existing + // parser instance. This is used to give up in situations where a + // highlight would take too long and freeze the browser interface. + highlight: function(from, target, cleanLines, maxBacktrack){ + var container = this.container, self = this, active = this.options.activeTokens; + var endTime = (typeof target == "number" ? target : null); + + if (!container.firstChild) + return; + // Backtrack to the first node before from that has a partial + // parse stored. + while (from && (!from.parserFromHere || from.dirty)) { + 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; + + // Check whether a part ( node) and the corresponding token + // match. + function correctPart(token, part){ + return !part.reduced && part.currentText == token.value && part.className == token.style; + } + // Shorten the text associated with a part by chopping off + // characters from the front. Note that only the currentText + // property gets changed. For efficiency reasons, we leave the + // nodeValue alone -- we set the reduced flag to indicate that + // this part must be replaced. + function shortenPart(part, minus){ + part.currentText = part.currentText.substring(minus); + part.reduced = true; + } + // Create a part corresponding to a given token. + function tokenPart(token){ + var part = makePartSpan(token.value, self.doc); + part.className = token.style; + return part; + } + + function maybeTouch(node) { + if (node) { + var old = node.oldNextSibling; + if (lineDirty || old === undefined || node.nextSibling != old) + self.history.touch(node); + node.oldNextSibling = node.nextSibling; + } + else { + var old = self.container.oldFirstChild; + if (lineDirty || old === undefined || self.container.firstChild != old) + self.history.touch(null); + self.container.oldFirstChild = self.container.firstChild; + } + } + + // Get the token stream. If from is null, we start with a new + // parser from the start of the frame, otherwise a partial parse + // is resumed. + var traversal = traverseDOM(from ? from.nextSibling : container.firstChild), + stream = stringStream(traversal), + parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream); + + // 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 + // not yet available when we want to proceed past the current + // one. + var parts = { + current: null, + // Fetch current node. + get: function(){ + if (!this.current) + this.current = traversal.nodes.shift(); + return this.current; + }, + // Advance to the next part (do not fetch it yet). + next: function(){ + this.current = null; + }, + // Remove the current part from the DOM tree, and move to the + // next. + remove: function(){ + container.removeChild(this.get()); + this.current = null; + }, + // Advance to the next part that is not empty, discarding empty + // parts. + getNonEmpty: function(){ + var part = this.get(); + // Allow empty nodes when they are alone on a line, needed + // for the FF cursor bug workaround (see select.js, + // insertNewlineAtCursor). + while (part && isSpan(part) && 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); + } + return part; + } + }; + + var lineDirty = false, prevLineDirty = true, lineNodes = 0; + + // This forEach loops over the tokens from the parsed stream, and + // at the same time uses the parts object to proceed through the + // corresponding DOM nodes. + forEach(parsed, function(token){ + var part = parts.getNonEmpty(); + + if (token.value == "\n"){ + // The idea of the two streams actually staying synchronized + // is such a long shot that we explicitly check. + if (!isBR(part)) + throw "Parser out of sync. Expected BR."; + + if (part.dirty || !part.indentation) lineDirty = true; + maybeTouch(from); + from = part; + + // Every
gets a copy of the parser state and a lexical + // context assigned to it. The first is used to be able to + // later resume parsing from this point, the second is used + // for indentation. + part.parserFromHere = parsed.copy(); + part.indentation = token.indentation; + part.dirty = false; + + // If the target argument wasn't an integer, go at least + // until that node. + if (endTime == null && part == target) throw StopIteration; + + // A clean line with more than one node means we are done. + // Throwing a StopIteration is the way to break out of a + // MochiKit forEach loop. + if ((endTime != null && time() >= endTime) || (!lineDirty && !prevLineDirty && lineNodes > 1 && !cleanLines)) + throw StopIteration; + prevLineDirty = lineDirty; lineDirty = false; lineNodes = 0; + parts.next(); + } + else { + if (!isSpan(part)) + throw "Parser out of sync. Expected SPAN."; + if (part.dirty) + lineDirty = true; + lineNodes++; + + // If the part matches the token, we can leave it alone. + if (correctPart(token, part)){ + part.dirty = false; + parts.next(); + } + // Otherwise, we have to fix it. + else { + lineDirty = true; + // Insert the correct part. + var newPart = tokenPart(token); + container.insertBefore(newPart, part); + if (active) active(newPart, token, self); + var tokensize = token.value.length; + var offset = 0; + // Eat up parts until the text for this token has been + // removed, adjusting the stored selection info (see + // select.js) in the process. + while (tokensize > 0) { + part = parts.get(); + var partsize = part.currentText.length; + select.snapshotReplaceNode(part.firstChild, newPart.firstChild, tokensize, offset); + if (partsize > tokensize){ + shortenPart(part, tokensize); + tokensize = 0; + } + else { + tokensize -= partsize; + offset += partsize; + parts.remove(); + } + } + } + } + }); + maybeTouch(from); + webkitLastLineHack(this.container); + + // The function returns some status information that is used by + // hightlightDirty to determine whether and where it has to + // continue. + return {node: parts.getNonEmpty(), + dirty: lineDirty}; + } + }; + + return Editor; +})(); + +addEventHandler(window, "load", function() { + var CodeMirror = window.frameElement.CodeMirror; + CodeMirror.editor = new Editor(CodeMirror.options); + this.parent.setTimeout(method(CodeMirror, "init"), 0); +}); + +;;;; + +// A framework for simple tokenizers. Takes care of newlines and +// white-space, and of getting the text from the source stream into +// the token object. A state is a function of two arguments -- a +// string stream and a setState function. The second can be used to +// change the tokenizer's state, and can be ignored for stateless +// tokenizers. This function should advance the stream over a token +// and return a string or object containing information about the next +// token, or null to pass and have the (new) state be called to finish +// the token. When a string is given, it is wrapped in a {style, type} +// object. In the resulting object, the characters consumed are stored +// under the content property. Any whitespace following them is also +// automatically consumed, and added to the value property. (Thus, +// content is the actual meaningful part of the token, while value +// contains all the text it spans.) + +function tokenizer(source, state) { + // Newlines are always a separate token. + function isWhiteSpace(ch) { + // The messy regexp is because IE's regexp matcher is of the + // opinion that non-breaking spaces are no whitespace. + return ch != "\n" && /^[\s\u00a0]*$/.test(ch); + } + + var tokenizer = { + state: state, + + take: function(type) { + if (typeof(type) == "string") + type = {style: type, type: type}; + + type.content = (type.content || "") + source.get(); + if (!/\n$/.test(type.content)) + source.nextWhile(isWhiteSpace); + type.value = type.content + source.get(); + return type; + }, + + next: function () { + if (!source.more()) throw StopIteration; + + var type; + if (source.equals("\n")) { + source.next(); + return this.take("whitespace"); + } + + if (source.applies(isWhiteSpace)) + type = "whitespace"; + else + while (!type) + type = this.state(source, function(s) {tokenizer.state = s;}); + + return this.take(type); + } + }; + return tokenizer; +} diff --git a/umbraco/presentation/umbraco_client/CodeMirror/js/codemirror.js b/umbraco/presentation/umbraco_client/CodeMirror/js/codemirror.js index 97e2657bd7..5567f66fb7 100644 --- a/umbraco/presentation/umbraco_client/CodeMirror/js/codemirror.js +++ b/umbraco/presentation/umbraco_client/CodeMirror/js/codemirror.js @@ -81,13 +81,10 @@ var CodeMirror = (function(){ var nextNum = 1, barWidth = null; function sizeBar() { - if (!frame.offsetWidth || !win.Editor) { - for (var cur = frame; cur.parentNode; cur = cur.parentNode) { - if (cur != document) { - clearInterval(sizeInterval); - return; - } - } + for (var root = frame; root.parentNode; root = root.parentNode); + if (root != document || !win.Editor) { + clearInterval(sizeInterval); + return; } if (nums.offsetWidth != barWidth) { @@ -175,10 +172,14 @@ var CodeMirror = (function(){ 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 @@ -206,10 +207,7 @@ var CodeMirror = (function(){ setParser: function(name) {this.editor.setParser(name);}, - cursorPosition: function(start) { - if (this.win.select.ie_selection) this.focus(); - return this.editor.cursorPosition(start); - }, + 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);}, diff --git a/umbraco/presentation/umbraco_client/Tree/UmbracoTree.js b/umbraco/presentation/umbraco_client/Tree/UmbracoTree.js index 3f0df72243..e79457fe02 100644 --- a/umbraco/presentation/umbraco_client/Tree/UmbracoTree.js +++ b/umbraco/presentation/umbraco_client/Tree/UmbracoTree.js @@ -53,7 +53,7 @@ Umbraco.Sys.registerNamespace("Umbraco.Controls"); _showContext: true, _isEditMode: false, _isDialog: false, - _isDebug: true, //set to true to enable alert debugging + _isDebug: false, //set to true to enable alert debugging _loadedApps: [], //stores the application names that have been loaded to track which JavaScript code has been inserted into the DOM _serviceUrl: "", //a path to the tree client service url _dataUrl: "", //a path to the tree data service url diff --git a/umbraco/presentation/umbraco_client/Tree/UmbracoTree.min.js b/umbraco/presentation/umbraco_client/Tree/UmbracoTree.min.js index a9be09f038..15e5dfce78 100644 --- a/umbraco/presentation/umbraco_client/Tree/UmbracoTree.min.js +++ b/umbraco/presentation/umbraco_client/Tree/UmbracoTree.min.js @@ -1,124 +1,50 @@ -Umbraco.Sys.registerNamespace("Umbraco.Controls"); (function($) { - $.fn.UmbracoTree = function(opts) { return this.each(function() { var conf = $.extend({ jsonFullMenu: null, jsonInitNode: null, appActions: null, uiKeys: null, app: "", showContext: true, isDialog: false, treeType: "standard", umb_clientFolderRoot: "/umbraco_client", recycleBinId: -20 }, opts); new Umbraco.Controls.UmbracoTree().init($(this), conf); }); }; $.fn.UmbracoTreeAPI = function() { return $(this).data("UmbracoTree") == null ? null : $(this).data("UmbracoTree"); }; Umbraco.Controls.UmbracoTree = function() { - return { _actionNode: new Umbraco.Controls.NodeDefinition(), _activeTreeType: "content", _recycleBinId: -20, _umb_clientFolderRoot: "/umbraco_client", _fullMenu: null, _initNode: null, _menuActions: null, _tree: null, _uiKeys: null, _container: null, _app: null, _showContext: true, _isDialog: false, _isDebug: false, _loadedApps: [], _serviceUrl: "", _dataUrl: "", _treeType: "standard", _treeClass: "umbTree", _currenAJAXRequest: false, addEventHandler: function(fnName, fn) { $(this).bind(fnName, fn); }, removeEventHandler: function(fnName, fn) { $(this).unbind(fnName, fn); }, init: function(jItem, opts) { this._init(opts.jsonFullMenu, opts.jsonInitNode, jItem, opts.appActions, opts.uiKeys, opts.app, opts.showContext, opts.isDialog, opts.treeType, opts.serviceUrl, opts.dataUrl, opts.umb_clientFolderRoot, opts.recycleBinId); jItem.addClass(this._treeClass); jItem.data("UmbracoTree", this); }, setRecycleBinNodeId: function(id) { this._recycleBinId = id; }, clearTreeCache: function() { this._debug("clearTreeCache..."); for (var a in this._loadedApps) { this._debug("clearTreeCache: " + this._loadedApps[a]); this._container.data("tree_" + this._loadedApps[a], null); } }, rebuildTree: function(app) { - this._debug("rebuildTree"); if (this._app == null || (this._app.toLowerCase() == app.toLowerCase())) { this._debug("not rebuilding"); return; } - else { this._app = app; } - $("div").remove(".tree-default-context"); this._tree.destroy(); this._container.hide(); var _this = this; var saveData = this._container.data("tree_" + app); if (saveData != null) { - this._debug("rebuildTree: rebuilding from cache: app = " + app); this._initNode = saveData.d; this._tree = $.tree_create(); this._tree.init(this._container, this._getInitOptions()); this._configureNodes(this._container.find("li"), true); var lastSelected = saveData.selected != null ? $(saveData.selected[0]).attr("id") : null; if (lastSelected != null) { var _this = this; var foundHandler = function(EV, node) { _this.removeEventHandler("syncFound", foundHandler); this._debug("rebuildTree: node synced, selecting node..."); _this.selectNode(node, false, true); this._container.show(); }; this._debug("rebuildTree: syncing to last selected: " + lastSelected); this.addEventHandler("syncFound", foundHandler); this.setActiveTreeType($(saveData.selected[0]).attr("umb:type")); this.syncTree(lastSelected); } - else { this._container.show(); } - return; - } - var parameters = "{'app':'" + app + "','showContextMenu':'" + this._showContext + "', 'isDialog':'" + this._isDialog + "'}" - this._currentAJAXRequest = true; $.ajax({ type: "POST", url: this._serviceUrl, data: parameters, contentType: "application/json; charset=utf-8", dataType: "json", success: function(msg) { - msg = msg.d; if ($.inArray(msg.app, _this._loadedApps) == -1) { _this._debug("loading js for app: " + msg.app); _this._loadedApps.push(msg.app); _this._container.after(""); } - _this._initNode = eval(msg.json); _this._tree = $.tree_create(); _this._tree.init(_this._container, _this._getInitOptions()); _this._container.show(); _this._loadChildNodes(_this._container.find("li:first"), null); _this._currentAJAXRequest = false; $(_this).trigger("rebuiltTree", [msg.app]); - }, error: function(e) { _this._debug("rebuildTree: AJAX error occurred"); _this._currentAJAXRequest = false; $(_this).trigger("ajaxError", [{ msg: "rebuildTree"}]); } - }); - }, saveTreeState: function(appAlias) { - this._debug("saveTreeState: " + appAlias + " : ajax request? " + this._currentAJAXRequest); if (this._currentAJAXRequest) { this._container.data("tree_" + appAlias, null); return; } - var treeData = this._tree.getJSON(null, ["id", "umb:type", "class"], ["umb:nodedata", "href", "class", "style"]); this._updateJSONNodeState(treeData); this._container.data("tree_" + appAlias, { selected: this._tree.selected, d: treeData }); - }, _updateJSONNodeState: function(obj) { - var node = $("li[id='" + obj.attributes.id + "']").filter(function() { return ($(this).attr("umb:type") == obj.attributes["umb:type"]); }); var c = node.attr("class"); if (c != null) { var state = c.indexOf("open") > -1 ? "open" : c.indexOf("closed") > -1 ? "closed" : null; if (state != null) obj.state = state; } - if (obj.children != null) { for (var x in obj.children) { this._updateJSONNodeState(obj.children[x]); } } - }, syncTree: function(path, forceReload) { this._debug("syncTree: " + path + ", " + forceReload); this._syncTree.call(this, path, forceReload, null, null); }, childNodeCreated: function() { - this._debug("childNodeCreated"); var childrenIds = new Array(); this._actionNode.jsNode.find("ul > li").each(function() { childrenIds.push($(this).attr("id")); }); var _this = this; var currId = this._actionNode.nodeId; this.reloadActionNode(true, false, function(success) { - if (success && childrenIds.length > 0) { - var found = false; var actionNode = _this.findNode(currId); if (actionNode) { actionNode.find("ul > li").each(function() { if ($.inArray($(this).attr("id"), childrenIds) == -1) { found = $(this); } }); } - if (found) { _this._debug("childNodeCreated: selecting new child node: " + found.attr("id")); _this.selectNode(found, true, true); $(_this).trigger("newChildNodeFound", [found]); return; } - } - _this._debug("childNodeCreated: could not select new child!"); - }); - }, moveNode: function(nodeId, parentPath) { this._debug("moveNode"); var old = this.findNode(nodeId); if (old) old.remove(); var newPath = parentPath + "," + nodeId; var _this = this; var foundHandler = function(EV, node) { _this.removeEventHandler("syncFound", foundHandler); _this.selectNode(node, false, true); $(_this).trigger("nodeMoved", [node]); }; this.addEventHandler("syncFound", foundHandler); this.syncTree(newPath); }, copyNode: function(nodeId, parentPath) { this._debug("copyNode"); var originalNode = this.findNode(nodeId); var _this = this; var foundHandler = function(EV, node) { _this.removeEventHandler("syncFound", foundHandler); _this._loadChildNodes(node, null); if (originalNode) _this.selectNode(originalNode, true); $(_this).trigger("nodeCopied", [node]); }; this.addEventHandler("syncFound", foundHandler); this.syncTree(parentPath); }, findNode: function(nodeId, findGlobal) { var _this = this; var branch = this._container.find("li[id='" + nodeId + "']"); if (!findGlobal) branch = branch.filter(function() { return ($(this).attr("umb:type") == _this._activeTreeType); }); var found = branch.length > 0 ? branch : false; this._debug("findNode: " + nodeId + " in '" + this._activeTreeType + "' tree. Found? " + found.length); return found; }, selectNode: function(node, supressEvent, reselect) { - this._debug("selectNode"); var selectedId = this._tree.selected != null ? $(this._tree.selected[0]).attr("id") : null; if (reselect || (selectedId == null || selectedId != node.attr("id"))) { - if (supressEvent) { this._tree.settings.callback.onselect = function() { }; } - this._tree.select_branch(node); var _this = this; this._tree.settings.callback.onselect = function(N, T) { _this.onSelect(N, T) }; - } - }, reloadActionNode: function(supressSelect, supressChildReload, callback) { - this._debug("reloadActionNode: supressSelect = " + supressSelect + ", supressChildReload = " + supressChildReload); if (this._actionNode != null && this._actionNode.jsNode != null) { - var nodeParent = this._actionNode.jsNode.parents("li:first"); this._debug("reloadActionNode: found " + nodeParent.length + " parent nodes"); if (nodeParent.length == 1) { - var nodeDef = this._getNodeDef(nodeParent); this._debug("reloadActionNode: loading ajax for node: " + nodeDef.nodeId); var _this = this; var toReplace = $("
  • " + (this._tree.settings.lang.loading || "Loading ...") + "
  • ").replaceAll(this._actionNode.jsNode); $.get(this._getUrl(nodeDef.sourceUrl), null, function(msg) { - if (!msg || msg.length == 0) { _this._debug("reloadActionNode: error loading ajax data, performing jsTree refresh"); _this._tree.refresh(); if (callback != null) callback.call(_this, false); return; } - var oFound = null; for (var o in msg) { - if (msg[o].attributes != null && msg[o].attributes.id == _this._actionNode.nodeId) { - oFound = _this._tree.parseJSON(msg[o]); if ($(oFound).attr("umb:type") == _this._actionNode.treeType) { break; } - else { oFound = null; } - } - } - if (oFound != null) { - _this._debug("reloadActionNode: node is refreshed!"); var reloaded = $(oFound).replaceAll(toReplace); _this._configureNodes(reloaded, true); if (!supressSelect) _this.selectNode(reloaded); if (!supressChildReload) { _this._loadChildNodes(reloaded, function() { if (callback != null) callback.call(_this, true); }); } - else { if (callback != null) callback.call(_this, true); } - } - else { _this._debug("reloadActionNode: error finding child node in ajax data, performing jsTree refresh"); _this._tree.refresh(); if (callback != null) callback.call(_this, false); } - }, "json"); return; - } - this._debug("reloadActionNode: error finding parent node, performing jsTree refresh"); this._tree.refresh(); if (callback != null) callback.call(this, false); - } - }, getActionNode: function() { return this._actionNode; }, setActiveTreeType: function(treeType) { this._activeTreeType = treeType; }, onNodeDeleting: function(EV) { - this._debug("onNodeDeleting") - this._tree.close_branch(this._actionNode.jsNode); this._actionNode.jsNode.find("a").attr("class", "loading"); this._actionNode.jsNode.find("a").css("background-image", ""); this._actionNode.jsNode.find("a").html(this._uiKeys['deleting']); - }, onNodeDeleted: function(EV) { this._debug("onNodeDeleted"); this._actionNode.jsNode.find("a").removeClass("loading"); this._tree.close_branch(this._actionNode.jsNode); this._actionNode.jsNode.hide("drop", { direction: "down" }, 400); this._updateRecycleBin(); }, onNodeRefresh: function(EV) { this._debug("onNodeRefresh"); this._loadChildNodes(this._actionNode.jsNode, null); }, onSelect: function(NODE, TREE_OBJ) { - this.setActiveTreeType($(NODE).attr("umb:type")); var js = $(NODE).children("a").attr("href").replace("javascript:", ""); this._debug("onSelect: js: " + js); try { var func = eval(js); if (func != null) { func.call(); } } catch (e) { } - return true; - }, onOpen: function(NODE, TREE_OBJ) { this._debug("onOpen: " + $(NODE).attr("id")); var nodes = $(NODE).find("ul > li"); this._configureNodes(nodes); return true; }, onBeforeOpen: function(NODE, TREE_OBJ) { var nodeDef = this._getNodeDef($(NODE)); this._debug("onBeforeOpen: " + nodeDef.nodeId); this._currentAJAXRequest = true; TREE_OBJ.settings.data.url = this._getUrl(nodeDef.sourceUrl); }, onJSONData: function(DATA, TREE_OBJ) { this._debug("onJSONData"); this._currentAJAXRequest = false; return DATA; }, onChange: function(NODE, TREE_OBJ) { - if (this._treeType == "checkbox") { - var $this = $(NODE).is("li") ? $(NODE) : $(NODE).parent(); if ($this.children("a").hasClass("checked")) $this.children("a").removeClass("checked") - else $this.children("a").addClass("checked"); - } - else if (this._treeType == "inheritedcheckbox") { - var $this = $(NODE).is("li") ? $(NODE) : $(NODE).parent(); if ($this.children("a.unchecked").size() == 0) { TREE_OBJ.container.find("a").addClass("unchecked"); } - $this.children("a").removeClass("clicked"); if ($this.children("a").hasClass("checked")) { $this.find("li").andSelf().children("a").removeClass("checked").removeClass("undetermined").addClass("unchecked"); var state = 0; } - else { $this.find("li").andSelf().children("a").removeClass("unchecked").removeClass("undetermined").addClass("checked"); var state = 1; } - $this.parents("li").each(function() { - if (state == 1) { - if ($(this).find("a.unchecked, a.undetermined").size() - 1 > 0) { $(this).parents("li").andSelf().children("a").removeClass("unchecked").removeClass("checked").addClass("undetermined"); return false; } - else $(this).children("a").removeClass("unchecked").removeClass("undetermined").addClass("checked"); - } - else { - if ($(this).find("a.checked, a.undetermined").size() - 1 > 0) { $(this).parents("li").andSelf().children("a").removeClass("unchecked").removeClass("checked").addClass("undetermined"); return false; } - else $(this).children("a").removeClass("checked").removeClass("undetermined").addClass("unchecked"); - } - }); - } - $(this).trigger("nodeClicked", [NODE]); - }, onRightClick: function(NODE, TREE_OBJ, EV) { this._actionNode = this._getNodeDef($(NODE)); this.setActiveTreeType($(NODE).attr("umb:type")); this._debug("onRightClick: menu = " + this._actionNode.menu); $("div").remove(".tree-default-context"); if (this._actionNode.menu != "") { TREE_OBJ.settings.ui.context = this._getContextMenu(this._actionNode.menu); TREE_OBJ.context_menu(); var timeout = null; TREE_OBJ.context.mouseenter(function() { clearTimeout(timeout); }); TREE_OBJ.context.mouseleave(function() { timeout = setTimeout(function() { TREE_OBJ.hide_context(); }, 400); }); this._checkContextMenu(TREE_OBJ); } }, _checkContextMenu: function(TREE_OBJ, count) { - if (count > 20) - return; var isVisible = TREE_OBJ.context.is(":visible"); if (!isVisible) { this._debug("_checkContextMenu - waiting for visible menu"); var _this = this; setTimeout(function() { _this._checkContextMenu(TREE_OBJ, ++count); }, 50); return; } - var offset = TREE_OBJ.context.offset(); var bodyHeight = $("body").innerHeight(); var ctxHeight = TREE_OBJ.context.height(); this._debug("_checkContextMenu - offset top: " + offset.top + ", bodyHeight: " + bodyHeight + ", ctxHeight: " + ctxHeight); var diff = (offset.top + ctxHeight) - bodyHeight; if (diff > 0) { this._debug("_checkContextMenu - Menu needs adjusting, new top: " + diff); TREE_OBJ.context.css("top", (offset.top - diff - 10) + "px"); } - }, _debug: function(strMsg) { if (this._isDebug) { Sys.Debug.trace("UmbracoTree: " + strMsg); } }, _configureNodes: function(nodes, reconfigure) { - var _this = this; if (!reconfigure) { nodes = nodes.not("li[class*='loaded']"); } - this._debug("_configureNodes: " + nodes.length); var rxInput = new RegExp("\\boverlay-\\w+\\b", "gi"); nodes.each(function() { - if (_this._treeType != "standard") { $(this).children("a:first").css("background", ""); return; } - $(this).children("div").remove(); var m = $(this).attr("class").match(rxInput); if (m != null) { for (i = 0; i < m.length; i++) { _this._debug("_configureNodes: adding overlay: " + m[i] + " for node: " + $(this).attr("id")); $(this).children("a:first").before("
    "); } } - var txt = $(this).children("a").html(); $(this).children("a").html("
    " + txt + "
    "); $(this).addClass("loaded"); - }); - }, _getNodeDef: function(NODE) { var nodedata = $(NODE).children("a").metadata({ type: 'attr', name: 'umb:nodedata' }); this._debug("_getNodeDef: " + $(NODE).attr("id") + ", " + nodedata.nodeType + ", " + nodedata.source); var def = new Umbraco.Controls.NodeDefinition(); def.updateDefinition(this._tree, $(NODE), $(NODE).attr("id"), $(NODE).find("a > div").html(), nodedata.nodeType, nodedata.source, nodedata.menu, $(NODE).attr("umb:type")); return def; }, _updateRecycleBin: function() { this._debug("_updateRecycleBin BinId: " + this._recycleBinId); var rNode = this.findNode(this._recycleBinId, true); if (rNode) { this._actionNode = this._getNodeDef(rNode); var _this = this; this.reloadActionNode(true, true, function(success) { if (success) { _this.findNode(_this._recycleBinId, true).effect("highlight", {}, 1000); } }); } }, _loadChildNodes: function(liNode, callback) { this._debug("_loadChildNodes: " + liNode); liNode.removeClass("leaf"); this._tree.close_branch(liNode, true); liNode.children("ul:eq(0)").html(""); this._tree.open_branch(liNode, false, callback); }, _syncTree: function(path, forceReload, numPaths, numAsync) { - this._debug("_syncTree"); var paths = path.split(","); var found = null; var foundIndex = null; if (numPaths == null) numPaths = (paths.length - 0); for (var i = 0; i < numPaths; i++) { foundIndex = paths.length - (1 + i); found = this.findNode(paths[foundIndex]); this._debug("_syncTree: finding... " + paths[foundIndex] + " found? " + found); if (found) break; } - if (!found) { this._debug("no node found in path: " + path + " : " + numPaths); $(this).trigger("syncNotFound", [path]); return; } - if (found.attr("id") != paths[paths.length - 1]) { - var _this = this; this._loadChildNodes(found, function(NODE, TREE_OBJ) { - var pathsToSearch = paths.length - (Number(foundIndex) + 1); if (_this.findNode(paths[foundIndex + 1])) { _this._syncTree(path, forceReload, pathsToSearch, (numAsync == null ? numAsync == 1 : ++numAsync)); } - else { _this._debug("node not found in children: " + path + " : " + numPaths); $(this).trigger("syncNotFound", [path]); } - }); - } - else { - var doReload = (forceReload && (numAsync == null || numAsync < 1)); this._debug("_syncTree: found! numAsync: " + numAsync + ", forceReload: " + forceReload); if (doReload) { this._actionNode = this._getNodeDef(found); this.reloadActionNode(true, true, null); } - else { if (found.attr("id") != "-1") this.selectNode(found, true); this._configureNodes(found, doReload); } - $(this).trigger("syncFound", [found]); - } - }, _getContextMenu: function(strMenu) { - this._debug("_getContextMenu: " + strMenu); var newMenu = new Array(); for (var i = 0; i < strMenu.length; i++) { var letter = strMenu.charAt(i); var menuItem = this._getMenuItemByLetter(letter); if (menuItem != null) newMenu.push(menuItem); } - return newMenu; - }, _getMenuItemByLetter: function(letter) { - if (letter == ",") return "separator"; for (var m in this._fullMenu) { if (this._fullMenu[m].id == letter) { return this._fullMenu[m]; } } - return null; - }, _init: function(jFullMenu, jInitNode, treeContainer, appActions, uiKeys, app, showContext, isDialog, treeType, serviceUrl, dataUrl, umbClientFolder, recycleBinId) { - this._debug("_init: creating new tree with class/id: " + treeContainer.attr("class") + " / " + treeContainer.attr("id")); this._fullMenu = jFullMenu; this._initNode = jInitNode; this._menuActions = appActions; this._uiKeys = uiKeys; this._app = app; this._showContext = showContext; this._isDialog = isDialog; this._treeType = treeType; this._serviceUrl = serviceUrl; this._dataUrl = dataUrl; this._umb_clientFolderRoot = umbClientFolder; this._recycleBinId = recycleBinId; if (this._menuActions != null) { var _this = this; this._menuActions.addEventHandler("nodeDeleting", function(E) { _this.onNodeDeleting(E) }); this._menuActions.addEventHandler("nodeDeleted", function(E) { _this.onNodeDeleted(E) }); this._menuActions.addEventHandler("nodeRefresh", function(E) { _this.onNodeRefresh(E) }); } - this._container = treeContainer; this._tree = $.tree_create(); this._tree.init(this._container, this._getInitOptions()); if ($.inArray(app, this._loadedApps) == -1) { this._loadedApps.push(app); } - this._loadChildNodes(this._container.find("li:first"), null); - }, _getUrl: function(nodeSource) { - if (nodeSource == null || nodeSource == "") { return this._dataUrl; } - var params = nodeSource.split("?")[1]; return this._dataUrl + "?" + params + "&rnd2=" + Umbraco.Utils.generateRandom(); - }, _getInitOptions: function() { this._debug("_getInitOptions"); var _this = this; var options = { data: { type: "json", async: true, url: "", json: this._initNode, async_data: function(NODE) { return null; } }, ui: { dots: false, rtl: false, animation: false, hover_mode: true, theme_path: this._umb_clientFolderRoot + "/Tree/Themes/", theme_name: "umbraco", context: null }, lang: { new_node: "New folder", loading: "
    " + (this._tree.settings.lang.loading || "Loading ...") + "
    " }, rules: { metadata: "umb:nodedata", creatable: "none", draggable: "none" }, callback: { onrgtclk: function(N, T, E) { _this.onRightClick(N, T, E) }, beforeopen: function(N, T) { _this.onBeforeOpen(N, T) }, onopen: function(N, T) { _this.onOpen(N, T) }, onselect: function(N, T) { _this.onSelect(N, T) }, onchange: function(N, T) { _this.onChange(N, T) }, onJSONdata: function(D, T) { return _this.onJSONData(D, T) } } }; return options; } - }; - } -})(jQuery); \ No newline at end of file +Umbraco.Sys.registerNamespace("Umbraco.Controls");(function($){$.fn.UmbracoTree=function(opts){return this.each(function(){var conf=$.extend({jsonFullMenu:null,jsonInitNode:null,appActions:null,uiKeys:null,app:"",showContext:true,isDialog:false,treeType:"standard",umb_clientFolderRoot:"/umbraco_client",recycleBinId:-20},opts);new Umbraco.Controls.UmbracoTree().init($(this),conf);});};$.fn.UmbracoTreeAPI=function(){return $(this).data("UmbracoTree")==null?null:$(this).data("UmbracoTree");};Umbraco.Controls.UmbracoTree=function(){return{_actionNode:new Umbraco.Controls.NodeDefinition(),_activeTreeType:"content",_recycleBinId:-20,_umb_clientFolderRoot:"/umbraco_client",_fullMenu:null,_initNode:null,_appActions:null,_tree:null,_uiKeys:null,_container:null,_app:null,_showContext:true,_isEditMode:false,_isDialog:false,_isDebug:false,_loadedApps:[],_serviceUrl:"",_dataUrl:"",_treeType:"standard",_treeClass:"umbTree",_currenAJAXRequest:false,addEventHandler:function(fnName,fn){$(this).bind(fnName,fn);},removeEventHandler:function(fnName,fn){$(this).unbind(fnName,fn);},init:function(jItem,opts){this._init(opts.jsonFullMenu,opts.jsonInitNode,jItem,opts.appActions,opts.uiKeys,opts.app,opts.showContext,opts.isDialog,opts.treeType,opts.serviceUrl,opts.dataUrl,opts.umb_clientFolderRoot,opts.recycleBinId);jItem.addClass(this._treeClass);jItem.data("UmbracoTree",this);},setRecycleBinNodeId:function(id){this._recycleBinId=id;},clearTreeCache:function(){this._debug("clearTreeCache...");for(var a in this._loadedApps){this._debug("clearTreeCache: "+this._loadedApps[a]);this._container.data("tree_"+this._loadedApps[a],null);}},toggleEditMode:function(enable){this._debug("Edit mode. Currently: "+this._tree.settings.rules.draggable);this._isEditMode=enable;this.saveTreeState(this._app);var app=this._app;this._app="temp";this.rebuildTree(app);this._debug("Edit mode. New Mode: "+this._tree.settings.rules.draggable);this._appActions.showSpeachBubble("info","Tree Edit Mode","The tree is now operating in edit mode");},rebuildTree:function(app){this._debug("rebuildTree");if(this._app==null||(this._app.toLowerCase()==app.toLowerCase())){this._debug("not rebuilding");return;} +else{this._app=app;} +$("div").remove(".tree-default-context");this._tree.destroy();this._container.hide();var _this=this;var saveData=this._container.data("tree_"+app);if(saveData!=null){this._debug("rebuildTree: rebuilding from cache: app = "+app);this._initNode=saveData.d;this._tree=$.tree_create();this._tree.init(this._container,this._getInitOptions());this._tree.rename=this._umbracoRename;this._configureNodes(this._container.find("li"),true);var lastSelected=saveData.selected!=null?$(saveData.selected[0]).attr("id"):null;if(lastSelected!=null){var _this=this;var foundHandler=function(EV,node){_this.removeEventHandler("syncFound",foundHandler);this._debug("rebuildTree: node synced, selecting node...");_this.selectNode(node,false,true);this._container.show();};this._debug("rebuildTree: syncing to last selected: "+lastSelected);this.addEventHandler("syncFound",foundHandler);this.setActiveTreeType($(saveData.selected[0]).attr("umb:type"));this.syncTree(lastSelected);} +else{this._container.show();} +return;} +var parameters="{'app':'"+app+"','showContextMenu':'"+this._showContext+"', 'isDialog':'"+this._isDialog+"'}" +this._currentAJAXRequest=true;$.ajax({type:"POST",url:this._serviceUrl,data:parameters,contentType:"application/json; charset=utf-8",dataType:"json",success:function(msg){msg=msg.d;if($.inArray(msg.app,_this._loadedApps)==-1){_this._debug("loading js for app: "+msg.app);_this._loadedApps.push(msg.app);_this._container.after("");} +_this._initNode=eval(msg.json);_this._tree=$.tree_create();_this._tree.init(_this._container,_this._getInitOptions());_this._tree.rename=_this._umbracoRename;_this._container.show();_this._loadChildNodes(_this._container.find("li:first"),null);_this._currentAJAXRequest=false;$(_this).trigger("rebuiltTree",[msg.app]);},error:function(e){_this._debug("rebuildTree: AJAX error occurred");_this._currentAJAXRequest=false;$(_this).trigger("ajaxError",[{msg:"rebuildTree"}]);}});},saveTreeState:function(appAlias){this._debug("saveTreeState: "+appAlias+" : ajax request? "+this._currentAJAXRequest);if(this._currentAJAXRequest){this._container.data("tree_"+appAlias,null);return;} +var treeData=this._tree.getJSON(null,["id","umb:type","class","rel"],["umb:nodedata","href","class","style"]);this._updateJSONNodeState(treeData);this._container.data("tree_"+appAlias,{selected:this._tree.selected,d:treeData});},_updateJSONNodeState:function(obj){var node=$("li[id='"+obj.attributes.id+"']").filter(function(){return($(this).attr("umb:type")==obj.attributes["umb:type"]);});var c=node.attr("class");if(c!=null){var state=c.indexOf("open")>-1?"open":c.indexOf("closed")>-1?"closed":null;if(state!=null)obj.state=state;} +if(obj.children!=null){for(var x in obj.children){this._updateJSONNodeState(obj.children[x]);}}},syncTree:function(path,forceReload){this._debug("syncTree: "+path+", "+forceReload);this._syncTree.call(this,path,forceReload,null,null);},childNodeCreated:function(){this._debug("childNodeCreated");var childrenIds=new Array();this._actionNode.jsNode.find("ul > li").each(function(){childrenIds.push($(this).attr("id"));});var _this=this;var currId=this._actionNode.nodeId;this.reloadActionNode(true,false,function(success){if(success&&childrenIds.length>0){var found=false;var actionNode=_this.findNode(currId);if(actionNode){actionNode.find("ul > li").each(function(){if($.inArray($(this).attr("id"),childrenIds)==-1){found=$(this);}});} +if(found){_this._debug("childNodeCreated: selecting new child node: "+found.attr("id"));_this.selectNode(found,true,true);$(_this).trigger("newChildNodeFound",[found]);return;}} +_this._debug("childNodeCreated: could not select new child!");});},moveNode:function(nodeId,parentPath){this._debug("moveNode");var old=this.findNode(nodeId);if(old)old.remove();var newPath=parentPath+","+nodeId;var _this=this;var foundHandler=function(EV,node){_this.removeEventHandler("syncFound",foundHandler);_this.selectNode(node,false,true);$(_this).trigger("nodeMoved",[node]);};this.addEventHandler("syncFound",foundHandler);this.syncTree(newPath);},copyNode:function(nodeId,parentPath){this._debug("copyNode");var originalNode=this.findNode(nodeId);var _this=this;var foundHandler=function(EV,node){_this.removeEventHandler("syncFound",foundHandler);_this._loadChildNodes(node,null);if(originalNode)_this.selectNode(originalNode,true);$(_this).trigger("nodeCopied",[node]);};this.addEventHandler("syncFound",foundHandler);this.syncTree(parentPath);},findNode:function(nodeId,findGlobal){var _this=this;var branch=this._container.find("li[id='"+nodeId+"']");if(!findGlobal)branch=branch.filter(function(){return($(this).attr("umb:type")==_this._activeTreeType);});var found=branch.length>0?branch:false;this._debug("findNode: "+nodeId+" in '"+this._activeTreeType+"' tree. Found? "+found.length);return found;},selectNode:function(node,supressEvent,reselect){this._debug("selectNode, edit mode? "+this._isEditMode);var selectedId=this._tree.selected!=null?$(this._tree.selected[0]).attr("id"):null;if(reselect||(selectedId==null||selectedId!=node.attr("id"))){if(supressEvent||this._isEditMode){this._tree.settings.callback.onselect=function(){};} +this._tree.select_branch(node);var _this=this;this._tree.settings.callback.onselect=function(N,T){_this.onSelect(N,T)};}},reloadActionNode:function(supressSelect,supressChildReload,callback){this._debug("reloadActionNode: supressSelect = "+supressSelect+", supressChildReload = "+supressChildReload);if(this._actionNode!=null&&this._actionNode.jsNode!=null){var nodeParent=this._actionNode.jsNode.parents("li:first");this._debug("reloadActionNode: found "+nodeParent.length+" parent nodes");if(nodeParent.length==1){var nodeDef=this._getNodeDef(nodeParent);this._debug("reloadActionNode: loading ajax for node: "+nodeDef.nodeId);var _this=this;var toReplace=$("
  • "+(this._tree.settings.lang.loading||"Loading ...")+"
  • ").replaceAll(this._actionNode.jsNode);$.get(this._getUrl(nodeDef.sourceUrl),null,function(msg){if(!msg||msg.length==0){_this._debug("reloadActionNode: error loading ajax data, performing jsTree refresh");_this._tree.refresh();if(callback!=null)callback.call(_this,false);return;} +var oFound=null;for(var o in msg){if(msg[o].attributes!=null&&msg[o].attributes.id==_this._actionNode.nodeId){oFound=_this._tree.parseJSON(msg[o]);if($(oFound).attr("umb:type")==_this._actionNode.treeType){break;} +else{oFound=null;}}} +if(oFound!=null){_this._debug("reloadActionNode: node is refreshed!");var reloaded=$(oFound).replaceAll(toReplace);_this._configureNodes(reloaded,true);if(!supressSelect)_this.selectNode(reloaded);if(!supressChildReload){_this._loadChildNodes(reloaded,function(){if(callback!=null)callback.call(_this,true);});} +else{if(callback!=null)callback.call(_this,true);}} +else{_this._debug("reloadActionNode: error finding child node in ajax data, performing jsTree refresh");_this._tree.refresh();if(callback!=null)callback.call(_this,false);}},"json");return;} +this._debug("reloadActionNode: error finding parent node, performing jsTree refresh");this._tree.refresh();if(callback!=null)callback.call(this,false);}},getActionNode:function(){return this._actionNode;},setActiveTreeType:function(treeType){this._activeTreeType=treeType;},onNodeDeleting:function(EV){this._debug("onNodeDeleting") +this._tree.close_branch(this._actionNode.jsNode);this._actionNode.jsNode.find("a").attr("class","loading");this._actionNode.jsNode.find("a").css("background-image","");this._actionNode.jsNode.find("a").html(this._uiKeys['deleting']);},onNodeDeleted:function(EV){this._debug("onNodeDeleted");this._actionNode.jsNode.find("a").removeClass("loading");this._tree.close_branch(this._actionNode.jsNode);this._actionNode.jsNode.hide("drop",{direction:"down"},400);this._updateRecycleBin();},onNodeRefresh:function(EV){this._debug("onNodeRefresh");this._loadChildNodes(this._actionNode.jsNode,null);},onSelect:function(NODE,TREE_OBJ){this._debug("onSelect, edit mode? "+this._isEditMode);if(this._isEditMode){this._tree.rename(NODE);return false;} +else{this.setActiveTreeType($(NODE).attr("umb:type"));var js=$(NODE).children("a").attr("href").replace("javascript:","");this._debug("onSelect: js: "+js);try{var func=eval(js);if(func!=null){func.call();}}catch(e){} +return true;}},onOpen:function(NODE,TREE_OBJ){this._debug("onOpen: "+$(NODE).attr("id"));var nodes=$(NODE).find("ul > li");this._configureNodes(nodes);return true;},onBeforeOpen:function(NODE,TREE_OBJ){var nodeDef=this._getNodeDef($(NODE));this._debug("onBeforeOpen: "+nodeDef.nodeId);this._currentAJAXRequest=true;TREE_OBJ.settings.data.url=this._getUrl(nodeDef.sourceUrl);},onJSONData:function(DATA,TREE_OBJ){this._debug("onJSONData");this._currentAJAXRequest=false;return DATA;},onChange:function(NODE,TREE_OBJ){if(this._treeType=="checkbox"){var $this=$(NODE).is("li")?$(NODE):$(NODE).parent();if($this.children("a").hasClass("checked"))$this.children("a").removeClass("checked") +else $this.children("a").addClass("checked");} +else if(this._treeType=="inheritedcheckbox"){var $this=$(NODE).is("li")?$(NODE):$(NODE).parent();if($this.children("a.unchecked").size()==0){TREE_OBJ.container.find("a").addClass("unchecked");} +$this.children("a").removeClass("clicked");if($this.children("a").hasClass("checked")){$this.find("li").andSelf().children("a").removeClass("checked").removeClass("undetermined").addClass("unchecked");var state=0;} +else{$this.find("li").andSelf().children("a").removeClass("unchecked").removeClass("undetermined").addClass("checked");var state=1;} +$this.parents("li").each(function(){if(state==1){if($(this).find("a.unchecked, a.undetermined").size()-1>0){$(this).parents("li").andSelf().children("a").removeClass("unchecked").removeClass("checked").addClass("undetermined");return false;} +else $(this).children("a").removeClass("unchecked").removeClass("undetermined").addClass("checked");} +else{if($(this).find("a.checked, a.undetermined").size()-1>0){$(this).parents("li").andSelf().children("a").removeClass("unchecked").removeClass("checked").addClass("undetermined");return false;} +else $(this).children("a").removeClass("checked").removeClass("undetermined").addClass("unchecked");}});} +$(this).trigger("nodeClicked",[NODE]);},onRightClick:function(NODE,TREE_OBJ,EV){this._actionNode=this._getNodeDef($(NODE));this.setActiveTreeType($(NODE).attr("umb:type"));this._debug("onRightClick: menu = "+this._actionNode.menu);$("div").remove(".tree-default-context");if(this._actionNode.menu!=""){TREE_OBJ.settings.ui.context=this._getContextMenu(this._actionNode.menu);TREE_OBJ.context_menu();var timeout=null;TREE_OBJ.context.mouseenter(function(){clearTimeout(timeout);});TREE_OBJ.context.mouseleave(function(){timeout=setTimeout(function(){TREE_OBJ.hide_context();},400);});this._checkContextMenu(TREE_OBJ);}},onBeforeMove:function(NODE,REF_NODE,TYPE,TREE_OBJ){var nodeDef=this._getNodeDef($(NODE));var nodeParent=nodeDef.jsNode.parents("li:first");var nodeParentDef=this._getNodeDef(nodeParent);var refNodeDef=this._getNodeDef($(REF_NODE));this._debug("onBeforeMove, TYPE: "+TYPE);this._debug("onBeforeMove, NODE ID: "+nodeDef.nodeId);this._debug("onBeforeMove, PARENT NODE ID: "+nodeParentDef.nodeId);this._debug("onBeforeMove, REF NODE ID: "+refNodeDef.nodeId);switch(TYPE){case"inside":if(nodeParentDef.nodeId==refNodeDef.nodeId){this._appActions.showSpeachBubble("warning","Tree Edit Mode","Cannot move a node to it's same parent node");return false;} +break;case"before":break;case"after":break;} +return false;},_checkContextMenu:function(TREE_OBJ,count){if(count>20) +return;var isVisible=TREE_OBJ.context.is(":visible");if(!isVisible){this._debug("_checkContextMenu - waiting for visible menu");var _this=this;setTimeout(function(){_this._checkContextMenu(TREE_OBJ,++count);},50);return;} +var offset=TREE_OBJ.context.offset();var bodyHeight=$("body").innerHeight();var ctxHeight=TREE_OBJ.context.height();this._debug("_checkContextMenu - offset top: "+offset.top+", bodyHeight: "+bodyHeight+", ctxHeight: "+ctxHeight);var diff=(offset.top+ctxHeight)-bodyHeight;if(diff>0){this._debug("_checkContextMenu - Menu needs adjusting, new top: "+diff);TREE_OBJ.context.css("top",(offset.top-diff-10)+"px");}},_debug:function(strMsg){if(this._isDebug){Sys.Debug.trace("UmbracoTree: "+strMsg);}},_configureNodes:function(nodes,reconfigure){var _this=this;if(!reconfigure){nodes=nodes.not("li[class*='loaded']");} +this._debug("_configureNodes: "+nodes.length);var rxInput=new RegExp("\\boverlay-\\w+\\b","gi");nodes.each(function(){if(_this._treeType!="standard"){$(this).children("a:first").css("background","");return;} +$(this).children("div").remove();var m=$(this).attr("class").match(rxInput);if(m!=null){for(i=0;i");}} +var txt=$(this).children("a").html();$(this).children("a").html("
    "+txt+"
    ");$(this).addClass("loaded");});},_getNodeDef:function(NODE){var nodedata=$(NODE).children("a").metadata({type:'attr',name:'umb:nodedata'});this._debug("_getNodeDef: "+$(NODE).attr("id")+", "+nodedata.nodeType+", "+nodedata.source);var def=new Umbraco.Controls.NodeDefinition();def.updateDefinition(this._tree,$(NODE),$(NODE).attr("id"),$(NODE).find("a > div").html(),nodedata.nodeType,nodedata.source,nodedata.menu,$(NODE).attr("umb:type"));return def;},_updateRecycleBin:function(){this._debug("_updateRecycleBin BinId: "+this._recycleBinId);var rNode=this.findNode(this._recycleBinId,true);if(rNode){this._actionNode=this._getNodeDef(rNode);var _this=this;this.reloadActionNode(true,true,function(success){if(success){_this.findNode(_this._recycleBinId,true).effect("highlight",{},1000);}});}},_loadChildNodes:function(liNode,callback){this._debug("_loadChildNodes: "+liNode);liNode.removeClass("leaf");this._tree.close_branch(liNode,true);liNode.children("ul:eq(0)").html("");this._tree.open_branch(liNode,false,callback);},_syncTree:function(path,forceReload,numPaths,numAsync){this._debug("_syncTree");var paths=path.split(",");var found=null;var foundIndex=null;if(numPaths==null)numPaths=(paths.length-0);for(var i=0;i");_this.inp.val(last_value.replace(/&/g,"&").replace(/>/g,">").replace(/").addClass($(obj).parent().attr("class")).addClass("renaming").append(_this.inp);spn.attr("style",$(obj).attr("style"));obj.parent().hide();obj.parents("li:first").prepend(spn);},_getUrl:function(nodeSource){if(nodeSource==null||nodeSource==""){return this._dataUrl;} +var params=nodeSource.split("?")[1];return this._dataUrl+"?"+params+"&rnd2="+Umbraco.Utils.generateRandom();},_getInitOptions:function(){this._debug("_getInitOptions");var _this=this;var options={data:{type:"json",async:true,url:"",json:this._initNode,async_data:function(NODE){return null;}},ui:{dots:false,rtl:false,animation:false,hover_mode:true,theme_path:this._umb_clientFolderRoot+"/Tree/Themes/",theme_name:"umbraco",context:null},lang:{new_node:"New folder",loading:"
    "+(this._tree.settings.lang.loading||"Loading ...")+"
    "},rules:{metadata:"umb:nodedata",creatable:"none",draggable:(!this._isEditMode?"none":["dataNode"]),},callback:{onrgtclk:function(N,T,E){_this.onRightClick(N,T,E)},beforemove:function(N,RN,TYPE,T){_this.onBeforeMove(N,RN,TYPE,T)},beforeopen:function(N,T){_this.onBeforeOpen(N,T)},onopen:function(N,T){_this.onOpen(N,T)},onselect:function(N,T){_this.onSelect(N,T)},onchange:function(N,T){_this.onChange(N,T)},onJSONdata:function(D,T){return _this.onJSONData(D,T)}}};return options;}};}})(jQuery); \ No newline at end of file