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 b187914b68..fd11516609 100644 Binary files a/foreign dlls/ClientDependency.Core.dll and b/foreign dlls/ClientDependency.Core.dll differ 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