jquery-ui实现一个多tab标签页可拖动编辑器记事本效果代码
代码语言:html
所属分类:其他
代码描述:jquery-ui实现一个多tab标签页可拖动编辑器效果代码
代码标签: jquery-ui tab 标签页 编辑器 记事本 拖动
下面为部分代码预览,完整代码请点击下载或在bfwstudio webide中打开
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <style> @font-face { font-family: 'icomoon'; src:url('data:application/font-woff;base64,d09GRk9UVE8AAAdMAAoAAAAABwQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRkYgAAAA9AAAA8AAAAPAUtNf6E9TLzIAAAS0AAAAYAAAAGAIQvy+Y21hcAAABRQAAABEAAAAROYaAIdnYXNwAAAFWAAAAAgAAAAIAAAAEGhlYWQAAAVgAAAANgAAADb+9fzjaGhlYQAABZgAAAAkAAAAJAPiAexobXR4AAAFvAAAACwAAAAsEwABAG1heHAAAAXoAAAABgAAAAYAC1AAbmFtZQAABfAAAAE5AAABOUQYtNZwb3N0AAAHLAAAACAAAAAgAAMAAAEABAQAAQEBCGljb21vb24AAQIAAQA8+BwC+BsD+BgEHgoAGVMSX4uLHgoAGVMSX4uLDAeLa/iU+JQFHQAAAKIPHQAAAKcRHQAAAAkdAAADtxIADAEBCA8SFxwhJiswNTo/aWNvbW9vbmljb21vb251MjB1RTYwMHVFNjAxdUU2MDJ1RTYwM3VFNjA0dUU2MDV1RTYwNnVFNjA3dUU2MDgAAAIBiQAJAAsCAAEABAAHAF0AtAEPAXYBzAH4AmICkQL2/JQO+5QOi/gUFfiUiwWLawX8lIsFi2sV9/SLBYtrBfv0iwWLaxX4lIsFi2sF/JSLBYtrFff0iwWLawX79IsFi2sV+JSLBYtrBfyUiwWLaxX39IsFi2sF+/SLBQ6L+BQV+JSLBYtrBfyUiwXraxX31IsFi2sF+9SLBYsrFffUiwWLawX71IsFiysV99SLBYtrBfvUiwUr93QV+JSLBYtrBfyUiwWLKxX4lIsFi2sF/JSLBQ6L+BQV+JSLBYtrBfyUiwX3NGsV9/SLBYtrBfv0iwX7NGsV+JSLBYtrBfyUiwX3NGsV9/SLBYtrBfv0iwX7NGsV+JSLBYtrBfyUiwX3NGsV9/SLBYtrBfv0iwUO9/b3ghWeoZaoi6oIi9JSxESLCPs0iwWL/FQF91SLBdKLxMSL0giLuXK0ZqEI+zb3JhW+iwWni6Jui2gIi2h0bm+LCFiLBYv3FAXb+9QVPIsFi/cUBduLBaiLo26LaAiLaHNubosIDvf0+FQVy4sFi/tkBYs7Q0sziwgzi0PLi9sIi/dkBcuLBYv7ZAWLd5R4nHwInXqkgqaLCKaLpJSdnAicmpSei58Ii/dkBfuU/BQV99SLBYtLBfvUiwUO+FT4VBWLawVLiwX7NPwUBcuLBYtrBft0iwWLqwXLiwX3NPgUBUuLBYurBQ74hPe0FftEiwWL90QFi5SEkoKLCCuLBYKLhISLggiL+0QF+0SLBYKLhISLggiLKwWLgpKElIsI90SLBYv7RAWLgpKElIsI64sFlIuSkouUCIv3RAX3RIsFlIuSkouUCIvrBYuUhJKCiwgOi/ekFYsrBYuCkoSUiwj4dIsFlIuSkouUCIvrBYuUhJKCiwj8dIsFgouEhIuCCA73VPdPFYuL8cjEcwiXnZeflp8IU5lBf4uLCIuL6sTFeQiXoJaglp4IXY5ehIuLCIuLx6/BkQicppuhmZYI+6yL+zv71Ev7VAiriwXr9zQFi4ura+urCKKToqChpwhTmUB+i4sIDviUFPiUFYsMCgADAgABkAAFAAABTAFmAAAARwFMAWYAAAD1ABkAhAAAAAAAAAAAAAAAAAAAAAEQAAAAAAAAAAAAAAAAAAAAAEAAIOYIAeD/4P/gAeAAIAAAAAEAAAAAAAAAAAAAACAAAAAAAAIAAAADAAAAFAADAAEAAAAUAAQAMAAAAAgACAACAAAAIOYI//3//wAAACDmAP/9////4RoCAAMAAQAAAAAAAAAAAAEAAf//AA8AAQAAAAEAAEkagQJfDzz1AAsCAAAAAADO614TAAAAAM7rXhMAAP/gAgAB4AAAAAgAAgAAAAAAAAABAAAB4P/gAAACAAAAAAACAAABAAAAAAAAAAAAAAAAAAAACwAAAAABAAAAAgAAAAIAAAACAAAAAgAAYAIAAGACAABAAgAAAAIAAAACAAAAAABQAAALAAAAAAAOAK4AAQAAAAAAAQAOAAAAAQAAAAAAAgAOAEcAAQAAAAAAAwAOACQAAQAAAAAABAAOAFUAAQAAAAAABQAWAA4AAQAAAAAABgAHADIAAQAAAAAACgAoAGMAAwABBAkAAQAOAAAAAwABBAkAAgAOAEcAAwABBAkAAwAOACQAAwABBAkABAAOAFUAAwABBAkABQAWAA4AAwABBAkABgAOADkAAwABBAkACgAoAGMAaQBjAG8AbQBvAG8AbgBWAGUAcgBzAGkAbwBuACAAMQAuADAAaQBjAG8AbQBvAG8Abmljb21vb24AaQBjAG8AbQBvAG8AbgBSAGUAZwB1AGwAYQByAGkAYwBvAG0AbwBvAG4ARwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=') format('woff'), url('') format('truetype'), url('') format('svg'); font-weight: normal; font-style: normal; } [class^="icon-"], [class*=" icon-"] { font-family: 'icomoon'; speak: none; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; /* Better Font Rendering =========== */ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .icon-paragraph-left:before { content: "\e600"; } .icon-paragraph-center:before { content: "\e601"; } .icon-paragraph-right:before { content: "\e602"; } .icon-bold:before { content: "\e603"; } .icon-underline:before { content: "\e604"; } .icon-italic:before { content: "\e605"; } .icon-plus:before { content: "\e606"; } .icon-minus:before { content: "\e607"; } .icon-quill:before { content: "\e608"; } @import url(https://fonts.googleapis.com/css?family=Telex); * { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; -webkit-font-smoothing: antialiased; } body { background: #5deaba; } .launch { font-family: "Telex", sans-serif; text-decoration: none; width: 240px; height: 60px; display: block; text-align: center; line-height: 38px; border-radius: 3px; font-size: 60px; color: #fff; position: absolute; left: 50%; top: 50%; margin: -30px 0 0 -120px; display: none; } .texteditor { width: 550px; height: 450px; position: absolute; left: 50%; top: 50%; background: #fff; margin: -205px 0 0 -275px; border-radius: 0 0 10px 10px; box-shadow: 0px -12px 200px #20b080; } .texteditor .ops { height: 288px; width: 50px; background: #ebebeb; position: absolute; right: 0px; border-radius: 0 10px 10px 0; z-index: 1; display: none; } .texteditor .ops ul { list-style: none; margin: 0; padding: 0; } .texteditor .ops ul li { text-align: center; padding: 10px 0; } .texteditor .ops ul li:first-child { border-radius: 0 10px 0 0; } .texteditor .ops ul li:last-child { border-radius: 0 0 10px 0; } .texteditor .ops ul li:hover { background: #31d687; } .texteditor .ops ul li:hover a { color: #fff; } .texteditor .ops ul li a { text-decoration: none; color: grey; display: block; } .texteditor .textareawrap .fontsize { background: #5deaba; position: absolute; z-index: 400; bottom: 5px; left: 5px; padding: 5px 10px; font-family: "Telex", sans-serif; color: #fff; font-size: 16px; border-radius: 0 0 0 5px; display: none; } .texteditor .textareawrap .textarea { display: none; margin: 0; padding: 10px; width: 100%; height: 100%; position: absolute; border: 0; outline: none; resize: none; overflow: auto; border-radius: 0 0 10px 10px; font-family: "Telex", sans-serif; font-size: 13px; color: #666; background: #fff; z-index: 2; } .texteditor .top { font-family: "Telex", sans-serif; position: absolute; height: 45px; background: #ebebeb; top: -45px; width: 100%; border-radius: 10px 10px 0 0; border-bottom: 1px solid #bababa; cursor: move; overflow: hidden; } .texteditor .top .icon-quill { font-size: 25px; float: right; margin: 0 10px 0 0; color: grey; text-decoration: none; } .texteditor .top .icon-quill:hover { color: #31d687; } .texteditor ul.controls { padding: 0; margin: 16px 0 0 15px; list-style: none; float: left; width: 65px; } .texteditor ul.controls li { width: 15px; height: 15px; float: left; margin: 0 4%; border-radius: 50%; cursor: pointer; } .texteditor ul.controls li:after { content: ""; background: linear-gradient(225deg, #ffffff 50%, transparent 50%); width: 15px; height: 15px; position: absolute; z-index: 2; border-radius: 50%; opacity: 0.4; } .texteditor ul.controls .close { background: #e57069; } .texteditor ul.controls .min { background: #f5ce56; } .texteditor ul.controls .expand { background: #31d687; } .texteditor ul.tabs { margin: 9px 0 0 100px; padding: 0; list-style: none; } .texteditor ul.tabs .addtab { font-size: 22px; z-index: 305; color: grey; position: relative; cursor: pointer; float: left; margin: 0 0 0 5px; } .texteditor ul.tabs .addtab:hover { color: #31d687; } .texteditor ul.tabs li { float: left; width: auto; margin: 0 -5px; padding: 0 15px; border-bottom: 35px solid #dad8d8; border-right: 20px solid transparent; border-left: 20px solid transparent; height: 0; line-height: 37px; position: relative; z-index: 2; cursor: pointer; } .texteditor ul.tabs li span { position: absolute; top: -12px; left: 2px; display: none; width: 17px; height: 17px; border-radius: 50%; top: -5px; left: -7px; text-align: center; line-height: 16px; background: #e57069; font-size: 20px; z-index: 3; } .texteditor ul.tabs li span:after { content: ""; background: linear-gradient(225deg, #ffffff 50%, transparent 50%); width: 15px; height: 15px; position: absolute; z-index: 2; border-radius: 50%; opacity: 0.2; left: 2px; } .texteditor ul.tabs li a { text-decoration: none; color: #fff; } .ui-resizable-e { right: 0; position: absolute; width: 5px; height: 100%; } .ui-resizable-s { bottom: 0; height: 5px; width: 100%; position: absolute; } .ui-icon-gripsmall-diagonal-se { z-index: 90; bottom: 0; right: 0; position: absolute; width: 10px; height: 10px; } .ui-resizable-s:hover { cursor: ns-resize; } .ui-resizable-e:hover { cursor: ew-resize; } .ui-icon-gripsmall-diagonal-se:hover { cursor: nwse-resize; } .active { border-bottom-color: #bababa !important; z-index: 300 !important; } .bold { font-weight: bold; } .italic { font-style: italic; } .underline { text-decoration: underline; } .leftalign { text-align: left; display: inline; } .rightalign { text-align: right; display: block; } .centeralign { text-align: center; display: block; } </style> </head> <body> <a href="#" class="launch"> LAUNCH </a> <div class="texteditor"> <div class="top"> <ul class="controls"> <li class="close"> <a href="#"></a> </li> <li class="min"> <a href="#"></a> </li> <li class="expand"> <a href="#"></a> </li> </ul> <ul class="tabs"> <span class="tabwrap"> </span> </ul> <a href="#" class="icon-quill"></a> </div> <div class="textareawrap"> <span class="fontsize"></span> </div> <aside class="ops"> <ul> <li> <a href="#" class="icon-bold"></a> </li> <li> <a class="icon-underline" href="#"></a> </li> <li> <a class="icon-italic" href="#"></a> </li> <li> <a class="icon-paragraph-left" href="#"></a> </li> <li> <a class="icon-paragraph-right" href="#"></a> </li> <li> <a class="icon-paragraph-center" href="#"></a> </li> <li> <a class="icon-plus" href="#"></a> </li> <li> <a class="icon-minus" href="#"></a> </li> </ul> </aside> </div> <script type="text/javascript" src="//repo.bfw.wiki/bfwrepo/js/jquery.2.11.js"></script> <script type="text/javascript" src="//repo.bfw.wiki/bfwrepo/js/jquery-ui-1.10.3.min.js"></script> <script> (function (factory, root) { if (typeof define == "function" && define.amd) { // AMD. Register as an anonymous module. define(factory); } else if (typeof module != "undefined" && typeof exports == "object") { // Node/CommonJS style module.exports = factory(); } else { // No AMD or CommonJS support so we place Rangy in (probably) the global variable root.rangy = factory(); } })(function () { var OBJECT = "object",FUNCTION = "function",UNDEFINED = "undefined"; // Minimal set of properties required for DOM Level 2 Range compliance. Comparison constants such as START_TO_START // are omitted because ranges in KHTML do not have them but otherwise work perfectly well. See issue 113. var domRangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", "commonAncestorContainer"]; // Minimal set of methods required for DOM Level 2 Range compliance var domRangeMethods = ["setStart", "setStartBefore", "setStartAfter", "setEnd", "setEndBefore", "setEndAfter", "collapse", "selectNode", "selectNodeContents", "compareBoundaryPoints", "deleteContents", "extractContents", "cloneContents", "insertNode", "surroundContents", "cloneRange", "toString", "detach"]; var textRangeProperties = ["boundingHeight", "boundingLeft", "boundingTop", "boundingWidth", "htmlText", "text"]; // Subset of TextRange's full set of methods that we're interested in var textRangeMethods = ["collapse", "compareEndPoints", "duplicate", "moveToElementText", "parentElement", "select", "setEndPoint", "getBoundingClientRect"]; /*----------------------------------------------------------------------------------------------------------------*/ // Trio of functions taken from Peter Michaux's article: // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting function isHostMethod(o, p) { var t = typeof o[p]; return t == FUNCTION || !!(t == OBJECT && o[p]) || t == "unknown"; } function isHostObject(o, p) { return !!(typeof o[p] == OBJECT && o[p]); } function isHostProperty(o, p) { return typeof o[p] != UNDEFINED; } // Creates a convenience function to save verbose repeated calls to tests functions function createMultiplePropertyTest(testFunc) { return function (o, props) { var i = props.length; while (i--) {if (window.CP.shouldStopExecution(0)) break; if (!testFunc(o, props[i])) { return false; } }window.CP.exitedLoop(0); return true; }; } // Next trio of functions are a convenience to save verbose repeated calls to previous two functions var areHostMethods = createMultiplePropertyTest(isHostMethod); var areHostObjects = createMultiplePropertyTest(isHostObject); var areHostProperties = createMultiplePropertyTest(isHostProperty); function isTextRange(range) { return range && areHostMethods(range, textRangeMethods) && areHostProperties(range, textRangeProperties); } function getBody(doc) { return isHostObject(doc, "body") ? doc.body : doc.getElementsByTagName("body")[0]; } var forEach = [].forEach ? function (arr, func) { arr.forEach(func); } : function (arr, func) { for (var i = 0, len = arr.length; i < len; ++i) {if (window.CP.shouldStopExecution(1)) break; func(arr[i], i); }window.CP.exitedLoop(1); }; var modules = {}; var isBrowser = typeof window != UNDEFINED && typeof document != UNDEFINED; var util = { isHostMethod: isHostMethod, isHostObject: isHostObject, isHostProperty: isHostProperty, areHostMethods: areHostMethods, areHostObjects: areHostObjects, areHostProperties: areHostProperties, isTextRange: isTextRange, getBody: getBody, forEach: forEach }; var api = { version: "1.3.1-dev", initialized: false, isBrowser: isBrowser, supported: true, util: util, features: {}, modules: modules, config: { alertOnFail: false, alertOnWarn: false, preferTextRange: false, autoInitialize: typeof rangyAutoInitialize == UNDEFINED ? true : rangyAutoInitialize } }; function consoleLog(msg) { if (typeof console != UNDEFINED && isHostMethod(console, "log")) { console.log(msg); } } function alertOrLog(msg, shouldAlert) { if (isBrowser && shouldAlert) { alert(msg); } else { consoleLog(msg); } } function fail(reason) { api.initialized = true; api.supported = false; alertOrLog("Rangy is not supported in this environment. Reason: " + reason, api.config.alertOnFail); } api.fail = fail; function warn(msg) { alertOrLog("Rangy warning: " + msg, api.config.alertOnWarn); } api.warn = warn; // Add utility extend() method var extend; if ({}.hasOwnProperty) { util.extend = extend = function (obj, props, deep) { var o, p; for (var i in props) { if (props.hasOwnProperty(i)) { o = obj[i]; p = props[i]; if (deep && o !== null && typeof o == "object" && p !== null && typeof p == "object") { extend(o, p, true); } obj[i] = p; } } // Special case for toString, which does not show up in for...in loops in IE <= 8 if (props.hasOwnProperty("toString")) { obj.toString = props.toString; } return obj; }; util.createOptions = function (optionsParam, defaults) { var options = {}; extend(options, defaults); if (optionsParam) { extend(options, optionsParam); } return options; }; } else { fail("hasOwnProperty not supported"); } // Test whether we're in a browser and bail out if not if (!isBrowser) { fail("Rangy can only run in a browser"); } // Test whether Array.prototype.slice can be relied on for NodeLists and use an alternative toArray() if not (function () { var toArray; if (isBrowser) { var el = document.createElement("div"); el.appendChild(document.createElement("span")); var slice = [].slice; try { if (slice.call(el.childNodes, 0)[0].nodeType == 1) { toArray = function (arrayLike) { return slice.call(arrayLike, 0); }; } } catch (e) {} } if (!toArray) { toArray = function (arrayLike) { var arr = []; for (var i = 0, len = arrayLike.length; i < len; ++i) {if (window.CP.shouldStopExecution(2)) break; arr[i] = arrayLike[i]; }window.CP.exitedLoop(2); return arr; }; } util.toArray = toArray; })(); // Very simple event handler wrapper function that doesn't attempt to solve issues such as "this" handling or // normalization of event properties var addListener; if (isBrowser) { if (isHostMethod(document, "addEventListener")) { addListener = function (obj, eventType, listener) { obj.addEventListener(eventType, listener, false); }; } else if (isHostMethod(document, "attachEvent")) { addListener = function (obj, eventType, listener) { obj.attachEvent("on" + eventType, listener); }; } else { fail("Document does not have required addEventListener or attachEvent method"); } util.addListener = addListener; } var initListeners = []; function getErrorDesc(ex) { return ex.message || ex.description || String(ex); } // Initialization function init() { if (!isBrowser || api.initialized) { return; } var testRange; var implementsDomRange = false,implementsTextRange = false; // First, perform basic feature tests if (isHostMethod(document, "createRange")) { testRange = document.createRange(); if (areHostMethods(testRange, domRangeMethods) && areHostProperties(testRange, domRangeProperties)) { implementsDomRange = true; } } var body = getBody(document); if (!body || body.nodeName.toLowerCase() != "body") { fail("No body element found"); return; } if (body && isHostMethod(body, "createTextRange")) { testRange = body.createTextRange(); if (isTextRange(testRange)) { implementsTextRange = true; } } if (!implementsDomRange && !implementsTextRange) { fail("Neither Range nor TextRange are available"); return; } api.initialized = true; api.features = { implementsDomRange: implementsDomRange, implementsTextRange: implementsTextRange }; // Initialize modules var module, errorMessage; for (var moduleName in modules) { if ((module = modules[moduleName]) instanceof Module) { module.init(module, api); } } // Call init listeners for (var i = 0, len = initListeners.length; i < len; ++i) {if (window.CP.shouldStopExecution(3)) break; try { initListeners[i](api); } catch (ex) { errorMessage = "Rangy init listener threw an exception. Continuing. Detail: " + getErrorDesc(ex); consoleLog(errorMessage); } }window.CP.exitedLoop(3); } function deprecationNotice(deprecated, replacement, module) { if (module) { deprecated += " in module " + module.name; } api.warn("DEPRECATED: " + deprecated + " is deprecated. Please use " + replacement + " instead."); } function createAliasForDeprecatedMethod(owner, deprecated, replacement, module) { owner[deprecated] = function () { deprecationNotice(deprecated, replacement, module); return owner[replacement].apply(owner, util.toArray(arguments)); }; } util.deprecationNotice = deprecationNotice; util.createAliasForDeprecatedMethod = createAliasForDeprecatedMethod; // Allow external scripts to initialize this library in case it's loaded after the document has loaded api.init = init; // Execute listener immediately if already initialized api.addInitListener = function (listener) { if (api.initialized) { listener(api); } else { initListeners.push(listener); } }; var shimListeners = []; api.addShimListener = function (listener) { shimListeners.push(listener); }; function shim(win) { win = win || window; init(); // Notify listeners for (var i = 0, len = shimListeners.length; i < len; ++i) {if (window.CP.shouldStopExecution(4)) break; shimListeners[i](win); }window.CP.exitedLoop(4); } if (isBrowser) { api.shim = api.createMissingNativeApi = shim; createAliasForDeprecatedMethod(api, "createMissingNativeApi", "shim"); } function Module(name, dependencies, initializer) { this.name = name; this.dependencies = dependencies; this.initialized = false; this.supported = false; this.initializer = initializer; } Module.prototype = { init: function () { var requiredModuleNames = this.dependencies || []; for (var i = 0, len = requiredModuleNames.length, requiredModule, moduleName; i < len; ++i) {if (window.CP.shouldStopExecution(5)) break; moduleName = requiredModuleNames[i]; requiredModule = modules[moduleName]; if (!requiredModule || !(requiredModule instanceof Module)) { throw new Error("required module '" + moduleName + "' not found"); } requiredModule.init(); if (!requiredModule.supported) { throw new Error("required module '" + moduleName + "' not supported"); } } // Now run initializer window.CP.exitedLoop(5);this.initializer(this); }, fail: function (reason) { this.initialized = true; this.supported = false; throw new Error(reason); }, warn: function (msg) { api.warn("Module " + this.name + ": " + msg); }, deprecationNotice: function (deprecated, replacement) { api.warn("DEPRECATED: " + deprecated + " in module " + this.name + " is deprecated. Please use " + replacement + " instead"); }, createError: function (msg) { return new Error("Error in Rangy " + this.name + " module: " + msg); } }; function createModule(name, dependencies, initFunc) { var newModule = new Module(name, dependencies, function (module) { if (!module.initialized) { module.initialized = true; try { initFunc(api, module); module.supported = true; } catch (ex) { var errorMessage = "Module '" + name + "' failed to load: " + getErrorDesc(ex); consoleLog(errorMessage); if (ex.stack) { consoleLog(ex.stack); } } } }); modules[name] = newModule; return newModule; } api.createModule = function (name) { // Allow 2 or 3 arguments (second argument is an optional array of dependencies) var initFunc, dependencies; if (arguments.length == 2) { initFunc = arguments[1]; dependencies = []; } else { initFunc = arguments[2]; dependencies = arguments[1]; } var module = createModule(name, dependencies, initFunc); // Initialize the module immediately if the core is already initialized if (api.initialized && api.supported) { module.init(); } }; api.createCoreModule = function (name, dependencies, initFunc) { createModule(name, dependencies, initFunc); }; /*----------------------------------------------------------------------------------------------------------------*/ // Ensure rangy.rangePrototype and rangy.selectionPrototype are available immediately function RangePrototype() {} api.RangePrototype = RangePrototype; api.rangePrototype = new RangePrototype(); function SelectionPrototype() {} api.selectionPrototype = new SelectionPrototype(); /*----------------------------------------------------------------------------------------------------------------*/ // DOM utility methods used by Rangy api.createCoreModule("DomUtil", [], function (api, module) { var UNDEF = "undefined"; var util = api.util; var getBody = util.getBody; // Perform feature tests if (!util.areHostMethods(document, ["createDocumentFragment", "createElement", "createTextNode"])) { module.fail("document missing a Node creation method"); } if (!util.isHostMethod(document, "getElementsByTagName")) { module.fail("document missing getElementsByTagName method"); } var el = document.createElement("div"); if (!util.areHostMethods(el, ["insertBefore", "appendChild", "cloneNode"] || !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]))) { module.fail("Incomplete Element implementation"); } // innerHTML is required for Range's createContextualFragment method if (!util.isHostProperty(el, "innerHTML")) { module.fail("Element is missing innerHTML property"); } var textNode = document.createTextNode("test"); if (!util.areHostMethods(textNode, ["splitText", "deleteData", "insertData", "appendData", "cloneNode"] || !util.areHostObjects(el, ["previousSibling", "nextSibling", "childNodes", "parentNode"]) || !util.areHostProperties(textNode, ["data"]))) { module.fail("Incomplete Text Node implementation"); } /*----------------------------------------------------------------------------------------------------------------*/ // Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been // able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that // contains just the document as a single element and the value searched for is the document. var arrayContains = /*Array.prototype.indexOf ? function(arr, val) { return arr.indexOf(val) > -1; }:*/ function (arr, val) { var i = arr.length; while (i--) {if (window.CP.shouldStopExecution(6)) break; if (arr[i] === val) { return true; } }window.CP.exitedLoop(6); return false; }; // Opera 11 puts HTML elements in the null namespace, it seems, and IE 7 has undefined namespaceURI function isHtmlNamespace(node) { var ns; return typeof node.namespaceURI == UNDEF || (ns = node.namespaceURI) === null || ns == "http://www.w3.org/1999/xhtml"; } function parentElement(node) { var parent = node.parentNode; return parent.nodeType == 1 ? parent : null; } function getNodeIndex(node) { var i = 0; while (node = node.previousSibling) {if (window.CP.shouldStopExecution(7)) break; ++i; }window.CP.exitedLoop(7); return i; } function getNodeLength(node) { switch (node.nodeType) { case 7: case 10: return 0; case 3: case 8: return node.length; default: return node.childNodes.length;} } function getCommonAncestor(node1, node2) { var ancestors = [],n; for (n = node1; n; n = n.parentNode) {if (window.CP.shouldStopExecution(8)) break; ancestors.push(n); }window.CP.exitedLoop(8); for (n = node2; n; n = n.parentNode) {if (window.CP.shouldStopExecution(9)) break; if (arrayContains(ancestors, n)) { return n; } }window.CP.exitedLoop(9); return null; } function isAncestorOf(ancestor, descendant, selfIsAncestor) { var n = selfIsAncestor ? descendant : descendant.parentNode; while (n) {if (window.CP.shouldStopExecution(10)) break; if (n === ancestor) { return true; } else { n = n.parentNode; } }window.CP.exitedLoop(10); return false; } function isOrIsAncestorOf(ancestor, descendant) { return isAncestorOf(ancestor, descendant, true); } function getClosestAncestorIn(node, ancestor, selfIsAncestor) { var p,n = selfIsAncestor ? node : node.parentNode; while (n) {if (window.CP.shouldStopExecution(11)) break; p = n.parentNode; if (p === ancestor) { return n; } n = p; }window.CP.exitedLoop(11); return null; } function isCharacterDataNode(node) { var t = node.nodeType; return t == 3 || t == 4 || t == 8; // Text, CDataSection or Comment } function isTextOrCommentNode(node) { if (!node) { return false; } var t = node.nodeType; return t == 3 || t == 8; // Text or Comment } function insertAfter(node, precedingNode) { var nextNode = precedingNode.nextSibling,parent = precedingNode.parentNode; if (nextNode) { parent.insertBefore(node, nextNode); } else { parent.appendChild(node); } return node; } // Note that we cannot use splitText() because it is bugridden in IE 9. function splitDataNode(node, index, positionsToPreserve) { var newNode = node.cloneNode(false); newNode.deleteData(0, index); node.deleteData(index, node.length - index); insertAfter(newNode, node); // Preserve positions if (positionsToPreserve) { for (var i = 0, position; position = positionsToPreserve[i++];) {if (window.CP.shouldStopExecution(12)) break; // Handle case where position was inside the portion of node after the split point if (position.node == node && position.offset > index) { position.node = newNode; position.offset -= index; } // Handle the case where the position is a node offset within node's parent else if (position.node == node.parentNode && position.offset > getNodeIndex(node)) { ++position.offset; } }window.CP.exitedLoop(12); } return newNode; } function getDocument(node) { if (node.nodeType == 9) { return node; } else if (typeof node.ownerDocument != UNDEF) { return node.ownerDocument; } else if (typeof node.document != UNDEF) { return node.document; } else if (node.parentNode) { return getDocument(node.parentNode); } else { throw module.createError("getDocument: no document found for node"); } } function getWindow(node) { var doc = getDocument(node); if (typeof doc.defaultView != UNDEF) { return doc.defaultView; } else if (typeof doc.parentWindow != UNDEF) { return doc.parentWindow; } else { throw module.createError("Cannot get a window object for node"); } } function getIframeDocument(iframeEl) { if (typeof iframeEl.contentDocument != UNDEF) { return iframeEl.contentDocument; } else if (typeof iframeEl.contentWindow != UNDEF) { return iframeEl.contentWindow.document; } else { throw module.createError("getIframeDocument: No Document object found for iframe element"); } } function getIframeWindow(iframeEl) { if (typeof iframeEl.contentWindow != UNDEF) { return iframeEl.contentWindow; } else if (typeof iframeEl.contentDocument != UNDEF) { return iframeEl.contentDocument.defaultView; } else { throw module.createError("getIframeWindow: No Window object found for iframe element"); } } // This looks bad. Is it worth it? function isWindow(obj) { return obj && util.isHostMethod(obj, "setTimeout") && util.isHostObject(obj, "document"); } function getContentDocument(obj, module, methodName) { var doc; if (!obj) { doc = document; } // Test if a DOM node has been passed and obtain a document object for it if so else if (util.isHostProperty(obj, "nodeType")) { doc = obj.nodeType == 1 && obj.tagName.toLowerCase() == "iframe" ? getIframeDocument(obj) : getDocument(obj); } // Test if the doc parameter appears to be a Window object else if (isWindow(obj)) { doc = obj.document; } if (!doc) { throw module.createError(methodName + "(): Parameter must be a Window object or DOM node"); } return doc; } function getRootContainer(node) { var parent; while (parent = node.parentNode) {if (window.CP.shouldStopExecution(13)) break; node = parent; }window.CP.exitedLoop(13); return node; } function comparePoints(nodeA, offsetA, nodeB, offsetB) { // See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing var nodeC, root, childA, childB, n; if (nodeA == nodeB) { // Case 1: nodes are the same return offsetA === offsetB ? 0 : offsetA < offsetB ? -1 : 1; } else if (nodeC = getClosestAncestorIn(nodeB, nodeA, true)) { // Case 2: node C (container B or an ancestor) is a child node of A return offsetA <= getNodeIndex(nodeC) ? -1 : 1; } else if (nodeC = getClosestAncestorIn(nodeA, nodeB, true)) { // Case 3: node C (container A or an ancestor) is a child node of B return getNodeIndex(nodeC) < offsetB ? -1 : 1; } else { root = getCommonAncestor(nodeA, nodeB); if (!root) { throw new Error("comparePoints error: nodes have no common ancestor"); } // Case 4: containers are siblings or descendants of siblings childA = nodeA === root ? root : getClosestAncestorIn(nodeA, root, true); childB = nodeB === root ? root : getClosestAncestorIn(nodeB, root, true); if (childA === childB) { // This shouldn't be possible throw module.createError("comparePoints got to case 4 and childA and childB are the same!"); } else { n = root.firstChild; while (n) {if (window.CP.shouldStopExecution(14)) break; if (n === childA) { return -1; } else if (n === childB) { return 1; } n = n.nextSibling; }window.CP.exitedLoop(14); } } } /*----------------------------------------------------------------------------------------------------------------*/ // Test for IE's crash (IE 6/7) or exception (IE >= 8) when a reference to garbage-collected text node is queried var crashyTextNodes = false; function isBrokenNode(node) { var n; try { n = node.parentNode; return false; } catch (e) { return true; } } (function () { var el = document.createElement("b"); el.innerHTML = "1"; var textNode = el.firstChild; el.innerHTML = "<br />"; crashyTextNodes = isBrokenNode(textNode); api.features.crashyTextNodes = crashyTextNodes; })(); /*----------------------------------------------------------------------------------------------------------------*/ function inspectNode(node) { if (!node) { return "[No node]"; } if (crashyTextNodes && isBrokenNode(node)) { return "[Broken node]"; } if (isCharacterDataNode(node)) { return '"' + node.data + '"'; } if (node.nodeType == 1) { var idAttr = node.id ? ' id="' + node.id + '"' : ""; return "<" + node.nodeName + idAttr + ">[index:" + getNodeIndex(node) + ",length:" + node.childNodes.length + "][" + (node.innerHTML || "[innerHTML not supported]").slice(0, 25) + "]"; } return node.nodeName; } function fragmentFromNodeChildren(node) { var fragment = getDocument(node).createDocumentFragment(),child; while (child = node.firstChild) {if (window.CP.shouldStopExecution(15)) break; fragment.appendChild(child); }window.CP.exitedLoop(15); return fragment; } var getComputedStyleProperty; if (typeof window.getComputedStyle != UNDEF) { getComputedStyleProperty = function (el, propName) { return getWindow(el).getComputedStyle(el, null)[propName]; }; } else if (typeof document.documentElement.currentStyle != UNDEF) { getComputedStyleProperty = function (el, propName) { return el.currentStyle ? el.currentStyle[propName] : ""; }; } else { module.fail("No means of obtaining computed style properties found"); } function createTestElement(doc, html, contentEditable) { var body = getBody(doc); var el = doc.createElement("div"); el.contentEditable = "" + !!contentEditable; if (html) { el.innerHTML = html; } // Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292) var bodyFirstChild = body.firstChild; if (bodyFirstChild) { body.insertBefore(el, bodyFirstChild); } else { body.appendChild(el); } return el; } function removeNode(node) { return node.parentNode.removeChild(node); } function NodeIterator(root) { this.root = root; this._next = root; } NodeIterator.prototype = { _current: null, hasNext: function () { return !!this._next; }, next: function () { var n = this._current = this._next; var child, next; if (this._current) { child = n.firstChild; if (child) { this._next = child; } else { next = null; while (n !== this.root && !(next = n.nextSibling)) {if (window.CP.shouldStopExecution(16)) break; n = n.parentNode; }window.CP.exitedLoop(16); this._next = next; } } return this._current; }, detach: function () { this._current = this._next = this.root = null; } }; function createIterator(root) { return new NodeIterator(root); } function DomPosition(node, offset) { this.node = node; this.offset = offset; } DomPosition.prototype = { equals: function (pos) { return !!pos && this.node === pos.node && this.offset == pos.offset; }, inspect: function () { return "[DomPosition(" + inspectNode(this.node) + ":" + this.offset + ")]"; }, toString: function () { return this.inspect(); } }; function DOMException(codeName) { this.code = this[codeName]; this.codeName = codeName; this.message = "DOMException: " + this.codeName; } DOMException.prototype = { INDEX_SIZE_ERR: 1, HIERARCHY_REQUEST_ERR: 3, WRONG_DOCUMENT_ERR: 4, NO_MODIFICATION_ALLOWED_ERR: 7, NOT_FOUND_ERR: 8, NOT_SUPPORTED_ERR: 9, INVALID_STATE_ERR: 11, INVALID_NODE_TYPE_ERR: 24 }; DOMException.prototype.toString = function () { return this.message; }; api.dom = { arrayContains: arrayContains, isHtmlNamespace: isHtmlNamespace, parentElement: parentElement, getNodeIndex: getNodeIndex, getNodeLength: getNodeLength, getCommonAncestor: getCommonAncestor, isAncestorOf: isAncestorOf, isOrIsAncestorOf: isOrIsAncestorOf, getClosestAncestorIn: getClosestAncestorIn, isCharacterDataNode: isCharacterDataNode, isTextOrCommentNode: isTextOrCommentNode, insertAfter: insertAfter, splitDataNode: splitDataNode, getDocument: getDocument, getWindow: getWindow, getIframeWindow: getIframeWindow, getIframeDocument: getIframeDocument, getBody: getBody, isWindow: isWindow, getContentDocument: getContentDocument, getRootContainer: getRootContainer, comparePoints: comparePoints, isBrokenNode: isBrokenNode, inspectNode: inspectNode, getComputedStyleProperty: getComputedStyleProperty, createTestElement: createTestElement, removeNode: removeNode, fragmentFromNodeChildren: fragmentFromNodeChildren, createIterator: createIterator, DomPosition: DomPosition }; api.DOMException = DOMException; }); /*----------------------------------------------------------------------------------------------------------------*/ // Pure JavaScript implementation of DOM Range api.createCoreModule("DomRange", ["DomUtil"], function (api, module) { var dom = api.dom; var util = api.util; var DomPosition = dom.DomPosition; var DOMException = api.DOMException; var isCharacterDataNode = dom.isCharacterDataNode; var getNodeIndex = dom.getNodeIndex; var isOrIsAncestorOf = dom.isOrIsAncestorOf; var getDocument = dom.getDocument; var comparePoints = dom.comparePoints; var splitDataNode = dom.splitDataNode; var getClosestAncestorIn = dom.getClosestAncestorIn; var getNodeLength = dom.getNodeLength; var arrayContains = dom.arrayContains; var getRootContainer = dom.getRootContainer; var crashyTextNodes = api.features.crashyTextNodes; var removeNode = dom.removeNode; /*----------------------------------------------------------------------------------------------------------------*/ // Utility functions function isNonTextPartiallySelected(node, range) { return node.nodeType != 3 && ( isOrIsAncestorOf(node, range.startContainer) || isOrIsAncestorOf(node, range.endContainer)); } function getRangeDocument(range) { return range.document || getDocument(range.startContainer); } function getRangeRoot(range) { return getRootContainer(range.startContainer); } function getBoundaryBeforeNode(node) { return new DomPosition(node.parentNode, getNodeIndex(node)); } function getBoundaryAfterNode(node) { return new DomPosition(node.parentNode, getNodeIndex(node) + 1); } function insertNodeAtPosition(node, n, o) { var firstNodeInserted = node.nodeType == 11 ? node.firstChild : node; if (isCharacterDataNode(n)) { if (o == n.length) { dom.insertAfter(node, n); } else { n.parentNode.insertBefore(node, o == 0 ? n : splitDataNode(n, o)); } } else if (o >= n.childNodes.length) { n.appendChild(node); } else { n.insertBefore(node, n.childNodes[o]); } return firstNodeInserted; } function rangesIntersect(rangeA, rangeB, touchingIsIntersecting) { assertRangeValid(rangeA); assertRangeValid(rangeB); if (getRangeDocument(rangeB) != getRangeDocument(rangeA)) { throw new DOMException("WRONG_DOCUMENT_ERR"); } var startComparison = comparePoints(rangeA.startContainer, rangeA.startOffset, rangeB.endContainer, rangeB.endOffset), endComparison = comparePoints(rangeA.endContainer, rangeA.endOffset, rangeB.startContainer, rangeB.startOffset); return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; } function cloneSubtree(iterator) { var partiallySelected; for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next();) {if (window.CP.shouldStopExecution(17)) break; partiallySelected = iterator.isPartiallySelectedSubtree(); node = node.cloneNode(!partiallySelected); if (partiallySelected) { subIterator = iterator.getSubtreeIterator(); node.appendChild(cloneSubtree(subIterator)); subIterator.detach(); } if (node.nodeType == 10) {// DocumentType throw new DOMException("HIERARCHY_REQUEST_ERR"); } frag.appendChild(node); }window.CP.exitedLoop(17); return frag; } function iterateSubtree(rangeIterator, func, iteratorState) { var it, n; iteratorState = iteratorState || { stop: false }; for (var node, subRangeIterator; node = rangeIterator.next();) {if (window.CP.shouldStopExecution(18)) break; if (rangeIterator.isPartiallySelectedSubtree()) { if (func(node) === false) { iteratorState.stop = true; return; } else { // The node is partially selected by the Range, so we can use a new RangeIterator on the portion of // the node selected by the Range. subRangeIterator = rangeIterator.getSubtreeIterator(); iterateSubtree(subRangeIterator, func, iteratorState); subRangeIterator.detach(); if (iteratorState.stop) { return; } } } else { // The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its // descendants it = dom.createIterator(node); while (n = it.next()) {if (window.CP.shouldStopExecution(19)) break; if (func(n) === false) { iteratorState.stop = true; return; } }window.CP.exitedLoop(19); } }window.CP.exitedLoop(18); } function deleteSubtree(iterator) { var subIterator; while (iterator.next()) {if (window.CP.shouldStopExecution(20)) break; if (iterator.isPartiallySelectedSubtree()) { subIterator = iterator.getSubtreeIterator(); deleteSubtree(subIterator); subIterator.detach(); } else { iterator.remove(); } }window.CP.exitedLoop(20); } function extractSubtree(iterator) { for (var node, frag = getRangeDocument(iterator.range).createDocumentFragment(), subIterator; node = iterator.next();) {if (window.CP.shouldStopExecution(21)) break; if (iterator.isPartiallySelectedSubtree()) { node = node.cloneNode(false); subIterator = iterator.getSubtreeIterator(); node.appendChild(extractSubtree(subIterator)); subIterator.detach(); } else { iterator.remove(); } if (node.nodeType == 10) {// DocumentType throw new DOMException("HIERARCHY_REQUEST_ERR"); } frag.appendChild(node); }window.CP.exitedLoop(21); return frag; } function getNodesInRange(range, nodeTypes, filter) { var filterNodeTypes = !!(nodeTypes && nodeTypes.length),regex; var filterExists = !!filter; if (filterNodeTypes) { regex = new RegExp("^(" + nodeTypes.join("|") + ")$"); } var nodes = []; iterateSubtree(new RangeIterator(range, false), function (node) { if (filterNodeTypes && !regex.test(node.nodeType)) { return; } if (filterExists && !filter(node)) { return; } // Don't include a boundary container if it is a character data node and the range does not contain any // of its character data. See issue 190. var sc = range.startContainer; if (node == sc && isCharacterDataNode(sc) && range.startOffset == sc.length) { return; } var ec = range.endContainer; if (node == ec && isCharacterDataNode(ec) && range.endOffset == 0) { return; } nodes.push(node); }); return nodes; } function inspect(range) { var name = typeof range.getName == "undefined" ? "Range" : range.getName(); return "[" + name + "(" + dom.inspectNode(range.startContainer) + ":" + range.startOffset + ", " + dom.inspectNode(range.endContainer) + ":" + range.endOffset + ")]"; } /*----------------------------------------------------------------------------------------------------------------*/ // RangeIterator code partially borrows from IERange by Tim Ryan (https://github.com/timcameronryan/IERange) function RangeIterator(range, clonePartiallySelectedTextNodes) { this.range = range; this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes; if (!range.collapsed) { this.sc = range.startContainer; this.so = range.startOffset; this.ec = range.endContainer; this.eo = range.endOffset; var root = range.commonAncestorContainer; if (this.sc === this.ec && isCharacterDataNode(this.sc)) { this.isSingleCharacterDataNode = true; this._first = this._last = this._next = this.sc; } else { this._first = this._next = this.sc === root && !isCharacterDataNode(this.sc) ? this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true); this._last = this.ec === root && !isCharacterDataNode(this.ec) ? this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true); } } } RangeIterator.prototype = { _current: null, _next: null, _first: null, _last: null, isSingleCharacterDataNode: false, reset: function () { this._current = null; this._next = this._first; }, hasNext: function () { return !!this._next; }, next: function () { // Move to next node var current = this._current = this._next; if (current) { this._next = current !== this._last ? current.nextSibling : null; // Check for partially selected text nodes if (isCharacterDataNode(current) && this.clonePartiallySelectedTextNodes) { if (current === this.ec) { (current = current.cloneNode(true)).deleteData(this.eo, current.length - this.eo); } if (this._current === this.sc) { (current = current.cloneNode(true)).deleteData(0, this.so); } } } return current; }, remove: function () { var current = this._current,start,end; if (isCharacterDataNode(current) && (current === this.sc || current === this.ec)) { start = current === this.sc ? this.so : 0; end = current === this.ec ? this.eo : current.length; if (start != end) { current.deleteData(start, end - start); } } else { if (current.parentNode) { removeNode(current); } else { } } }, // Checks if the current node is partially selected isPartiallySelectedSubtree: function () { var current = this._current; return isNonTextPartiallySelected(current, this.range); }, getSubtreeIterator: function () { var subRange; if (this.isSingleCharacterDataNode) { subRange = this.range.cloneRange(); subRange.collapse(false); } else { subRange = new Range(getRangeDocument(this.range)); var current = this._current; var startContainer = current,startOffset = 0,endContainer = current,endOffset = getNodeLength(current); if (isOrIsAncestorOf(current, this.sc)) { startContainer = this.sc; startOffset = this.so; } if (isOrIsAncestorOf(current, this.ec)) { endContainer = this.ec; endOffset = this.eo; } updateBoundaries(subRange, startContainer, startOffset, endContainer, endOffset); } return new RangeIterator(subRange, this.clonePartiallySelectedTextNodes); }, detach: function () { this.range = this._current = this._next = this._first = this._last = this.sc = this.so = this.ec = this.eo = null; } }; /*----------------------------------------------------------------------------------------------------------------*/ var beforeAfterNodeTypes = [1, 3, 4, 5, 7, 8, 10]; var rootContainerNodeTypes = [2, 9, 11]; var readonlyNodeTypes = [5, 6, 10, 12]; var insertableNodeTypes = [1, 3, 4, 5, 7, 8, 10, 11]; var surroundNodeTypes = [1, 3, 4, 5, 7, 8]; function createAncestorFinder(nodeTypes) { return function (node, selfIsAncestor) { var t,n = selfIsAncestor ? node : node.parentNode; while (n) {if (window.CP.shouldStopExecution(22)) break; t = n.nodeType; if (arrayContains(nodeTypes, t)) { return n; } n = n.parentNode; }window.CP.exitedLoop(22); return null; }; } var getDocumentOrFragmentContainer = createAncestorFinder([9, 11]); var getReadonlyAncestor = createAncestorFinder(readonlyNodeTypes); var getDocTypeNotationEntityAncestor = createAncestorFinder([6, 10, 12]); function assertNoDocTypeNotationEntityAncestor(node, allowSelf) { if (getDocTypeNotationEntityAncestor(node, allowSelf)) { throw new DOMException("INVALID_NODE_TYPE_ERR"); } } function assertValidNodeType(node, invalidTypes) { if (!arrayContains(invalidTypes, node.nodeType)) { throw new DOMException("INVALID_NODE_TYPE_ERR"); } } function assertValidOffset(node, offset) { if (offset < 0 || offset > (isCharacterDataNode(node) ? node.length : node.childNodes.length)) { throw new DOMException("INDEX_SIZE_ERR"); } } function assertSameDocumentOrFragment(node1, node2) { if (getDocumentOrFragmentContainer(node1, true) !== getDocumentOrFragmentContainer(node2, true)) { throw new DOMException("WRONG_DOCUMENT_ERR"); } } function assertNodeNotReadOnly(node) { if (getReadonlyAncestor(node, true)) { throw new DOMException("NO_MODIFICATION_ALLOWED_ERR"); } } function assertNode(node, codeName) { if (!node) { throw new DOMException(codeName); } } function isValidOffset(node, offset) { return offset <= (isCharacterDataNode(node) ? node.length : node.childNodes.length); } function isRangeValid(range) { return !!range.startContainer && !!range.endContainer && !(crashyTextNodes && (dom.isBrokenNode(range.startContainer) || dom.isBrokenNode(range.endContainer))) && getRootContainer(range.startContainer) == getRootContainer(range.endContainer) && isValidOffset(range.startContainer, range.startOffset) && isValidOffset(range.endContainer, range.endOffset); } function assertRangeValid(range) { if (!isRangeValid(range)) { throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: (" + range.inspect() + ")"); } } /*----------------------------------------------------------------------------------------------------------------*/ // Test the browser's innerHTML support to decide how to implement createContextualFragment var styleEl = document.createElement("style"); var htmlParsingConforms = false; try { styleEl.innerHTML = "<b>x</b>"; htmlParsingConforms = styleEl.firstChild.nodeType == 3; // Opera incorrectly creates an element node } catch (e) { // IE 6 and 7 throw } api.features.htmlParsingConforms = htmlParsingConforms; var createContextualFragment = htmlParsingConforms ? // Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See // discussion and base code for this implementation at issue 67. // Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface // Thanks to Aleks Williams. function (fragmentStr) { // "Let node the context object's start's node." var node = this.startContainer; var doc = getDocument(node); // "If the context object's start's node is null, raise an INVALID_STATE_ERR // exception and abort these steps." if (!node) { throw new DOMException("INVALID_STATE_ERR"); } // "Let element be as follows, depending on node's interface:" // Document, Document Fragment: null var el = null; // "Element: node" if (node.nodeType == 1) { el = node; // "Text, Comment: node's parentElement" } else if (isCharacterDataNode(node)) { el = dom.parentElement(node); } // "If either element is null or element's ownerDocument is an HTML document // and element's local name is "html" and element's namespace is the HTML // namespace" if (el === null || el.nodeName == "HTML" && dom.isHtmlNamespace(getDocument(el).documentElement) && dom.isHtmlNamespace(el)) { // "let element be a new Element with "body" as its local name and the HTML // namespace as its namespace."" el = doc.createElement("body"); } else { el = el.cloneNode(false); } // "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm." // "If the node's document is an XML document: Invoke the XML fragment parsing algorithm." // "In either case, the algorithm must be invoked with fragment as the input // and element as the context element." el.innerHTML = fragmentStr; // "If this raises an exception, then abort these steps. Otherwise, let new // children be the nodes returned." // "Let fragment be a new DocumentFragment." // "Append all new children to fragment." // "Return fragment." return dom.fragmentFromNodeChildren(el); } : // In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that // previous versions of Rangy used (with the exception of using a body element rather than a div) function (fragmentStr) { var doc = getRangeDocument(this); var el = doc.createElement("body"); el.innerHTML = fragmentStr; return dom.fragmentFromNodeChildren(el); }; function splitRangeBoundaries(range, positionsToPreserve) { assertRangeValid(range); var sc = range.startContainer,so = range.startOffset,ec = range.endContainer,eo = range.endOffset; var startEndSame = sc === ec; if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) { splitDataNode(ec, eo, positionsToPreserve); } if (isCharacterDataNode(sc) && so > 0 && so < sc.length) { sc = splitDataNode(sc, so, positionsToPreserve); if (startEndSame) { eo -= so; ec = sc; } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) { eo++; } so = 0; } range.setStartAndEnd(sc, so, ec, eo); } function rangeToHtml(range) { assertRangeValid(range); var container = range.commonAncestorContainer.parentNode.cloneNode(false); container.appendChild(range.cloneContents()); return container.innerHTML; } /*----------------------------------------------------------------------------------------------------------------*/ var rangeProperties = ["startContainer", "startOffset", "endContainer", "endOffset", "collapsed", "commonAncestorContainer"]; var s2s = 0,s2e = 1,e2e = 2,e2s = 3; var n_b = 0,n_a = 1,n_b_a = 2,n_i = 3; util.extend(api.rangePrototype, { compareBoundaryPoints: function (how, range) { assertRangeValid(this); assertSameDocumentOrFragment(this.startContainer, range.startContainer); var nodeA, offsetA, nodeB, offsetB; var prefixA = how == e2s || how == s2s ? "start" : "end"; var prefixB = how == s2e || how == s2s ? "start" : "end"; nodeA = this[prefixA + "Container"]; offsetA = this[prefixA + "Offset"]; nodeB = range[prefixB + "Container"]; offsetB = range[prefixB + "Offset"]; return comparePoints(nodeA, offsetA, nodeB, offsetB); }, insertNode: function (node) { assertRangeValid(this); assertValidNodeType(node, insertableNodeTypes); assertNodeNotReadOnly(this.startContainer); if (isOrIsAncestorOf(node, this.startContainer)) { throw new DOMException("HIERARCHY_REQUEST_ERR"); } // No check for whether the container of the start of the Range is of a type that does not allow // children of the type of node: the browser's DOM implementation should do this for us when we attempt // to add the node var firstNodeInserted = insertNodeAtPosition(node, this.startContainer, this.startOffset); this.setStartBefore(firstNodeInserted); }, cloneContents: function () { assertRangeValid(this); var clone, frag; if (this.collapsed) { return getRangeDocument(this).createDocumentFragment(); } else { if (this.startContainer === this.endContainer && isCharacterDataNode(this.startContainer)) { clone = this.startContainer.cloneNode(true); clone.data = clone.data.slice(this.startOffset, this.endOffset); frag = getRangeDocument(this).createDocumentFragment(); frag.appendChild(clone); return frag; } else { var iterator = new RangeIterator(this, true); clone = cloneSubtree(iterator); iterator.detach(); } return clone; } }, canSurroundContents: function () { assertRangeValid(this); assertNodeNotReadOnly(this.startContainer); assertNodeNotReadOnly(this.endContainer); // Check if the contents can be surrounded. Specifically, this means whether the range partially selects // no non-text nodes. var iterator = new RangeIterator(this, true); var boundariesInvalid = iterator._first && isNonTextPartiallySelected(iterator._first, this) || iterator._last && isNonTextPartiallySelected(iterator._last, this); iterator.detach(); return !boundariesInvalid; }, surroundContents: function (node) { assertValidNodeType(node, surroundNodeTypes); if (!this.canSurroundContents()) { throw new DOMException("INVALID_STATE_ERR"); } // Extract the contents var content = this.extractContents(); // Clear the children of the node if (node.hasChildNodes()) { while (node.lastChild) {if (window.CP.shouldStopExecution(23)) break; node.removeChild(node.lastChild); }window.CP.exitedLoop(23); } // Insert the new node and add the extracted contents insertNodeAtPosition(node, this.startContainer, this.startOffset); node.appendChild(content); this.selectNode(node); }, cloneRange: function () { assertRangeValid(this); var range = new Range(getRangeDocument(this)); var i = rangeProperties.length,prop; while (i--) {if (window.CP.shouldStopExecution(24)) break; prop = rangeProperties[i]; range[prop] = this[prop]; }window.CP.exitedLoop(24); return range; }, toString: function () { assertRangeValid(this); var sc = this.startContainer; if (sc === this.endContainer && isCharacterDataNode(sc)) { return sc.nodeType == 3 || sc.nodeType == 4 ? sc.data.slice(this.startOffset, this.endOffset) : ""; } else { var textParts = [],iterator = new RangeIterator(this, true); iterateSubtree(iterator, function (node) { // Accept only text or CDATA nodes, not comments if (node.nodeType == 3 || node.nodeType == 4) { textParts.push(node.data); } }); iterator.detach(); return textParts.join(""); } }, // The methods below are all non-standard. The following batch were introduced by Mozilla but have since // been removed from Mozilla. compareNode: function (node) { assertRangeValid(this); var parent = node.parentNode; var nodeIndex = getNodeIndex(node); if (!parent) { throw new DOMException("NOT_FOUND_ERR"); } var startComparison = this.comparePoint(parent, nodeIndex), endComparison = this.comparePoint(parent, nodeIndex + 1); if (startComparison < 0) {// Node starts before return endComparison > 0 ? n_b_a : n_b; } else { return endComparison > 0 ? n_a : n_i; } }, comparePoint: function (node, offset) { assertRangeValid(this); assertNode(node, "HIERARCHY_REQUEST_ERR"); assertSameDocumentOrFragment(node, this.startContainer); if (comparePoints(node, offset, this.startContainer, this.startOffset) < 0) { return -1; } else if (comparePoints(node, offset, this.endContainer, this.endOffset) > 0) { return 1; } return 0; }, createContextualFragment: createContextualFragment, toHtml: function () { return rangeToHtml(this); }, // touchingIsIntersecting determines whether this method considers a node that borders a range intersects // with it (as in WebKit) or not (as in Gecko pre-1.9, and the default) intersectsNode: function (node, touchingIsIntersecting) { assertRangeValid(this); if (getRootContainer(node) != getRangeRoot(this)) { return false; } var parent = node.parentNode,offset = getNodeIndex(node); if (!parent) { return true; } var startComparison = comparePoints(parent, offset, this.endContainer, this.endOffset), endComparison = comparePoints(parent, offset + 1, this.startContainer, this.startOffset); return touchingIsIntersecting ? startComparison <= 0 && endComparison >= 0 : startComparison < 0 && endComparison > 0; }, isPointInRange: function (node, offset) { assertRangeValid(this); assertNode(node, "HIERARCHY_REQUEST_ERR"); assertSameDocumentOrFragment(node, this.startContainer); return comparePoints(node, offset, this.startContainer, this.startOffset) >= 0 && comparePoints(node, offset, this.endContainer, this.endOffset) <= 0; }, // The methods below are non-standard and invented by me. // Sharing a boundary start-to-end or end-to-start does not count as intersection. intersectsRange: function (range) { return rangesIntersect(this, range, false); }, // Sharing a boundary start-to-end or end-to-start does count as intersection. intersectsOrTouchesRange: function (range) { return rangesIntersect(this, range, true); }, intersection: function (range) { if (this.intersectsRange(range)) { var startComparison = comparePoints(this.startContainer, this.startOffset, range.startContainer, range.startOffset), endComparison = comparePoints(this.endContainer, this.endOffset, range.endContainer, range.endOffset); var intersectionRange = this.cloneRange(); if (startComparison == -1) { intersectionRange.setStart(range.startContainer, range.startOffset); } if (endComparison == 1) { intersectionRange.setEnd(range.endContainer, range.endOffset); } return intersectionRange; } return null; }, union: function (range) { if (this.intersectsOrTouchesRange(range)) { var unionRange = this.cloneRange(); if (comparePoints(range.startContainer, range.startOffset, this.startContainer, this.startOffset) == -1) { unionRange.setStart(range.startContainer, range.startOffset); } if (comparePoints(range.endContainer, range.endOffset, this.endContainer, this.endOffset) == 1) { unionRange.setEnd(range.endContainer, range.endOffset); } return unionRange; } else { throw new DOMException("Ranges do not intersect"); } }, containsNode: function (node, allowPartial) { if (allowPartial) { return this.intersectsNode(node, false); } else { return this.compareNode(node) == n_i; } }, containsNodeContents: function (node) { return this.comparePoint(node, 0) >= 0 && this.comparePoint(node, getNodeLength(node)) <= 0; }, containsRange: function (range) { var intersection = this.intersection(range); return intersection !== null && range.equals(intersection); }, containsNodeText: function (node) { var nodeRange = this.cloneRange(); nodeRange.selectNode(node); var textNodes = nodeRange.getNodes([3]); if (textNodes.length > 0) { nodeRange.setStart(textNodes[0], 0); var lastTextNode = textNodes.pop(); nodeRange.setEnd(lastTextNode, lastTextNode.length); return this.containsRange(nodeRange); } else { return this.containsNodeContents(node); } }, getNodes: function (nodeTypes, filter) { assertRangeValid(this); return getNodesInRange(this, nodeTypes, filter); }, getDocument: function () { return getRangeDocument(this); }, collapseBefore: function (node) { this.setEndBefore(node); this.collapse(false); }, collapseAfter: function (node) { this.setStartAfter(node); this.collapse(true); }, getBookmark: function (containerNode) { var doc = getRangeDocument(this); var preSelectionRange = api.createRange(doc); containerNode = containerNode || dom.getBody(doc); preSelectionRange.selectNodeContents(containerNode); var range = this.intersection(preSelectionRange); var start = 0,end = 0; if (range) { preSelectionRange.setEnd(range.startContainer, range.startOffset); start = preSelectionRange.toString().length; end = start + range.toString().length; } return { start: start, end: end, containerNode: containerNode }; }, moveToBookmark: function (bookmark) { var containerNode = bookmark.containerNode; var charIndex = 0; this.setStart(containerNode, 0); this.collapse(true); var nodeStack = [containerNode],node,foundStart = false,stop = false; var nextCharIndex, i, childNodes; while (!stop && (node = nodeStack.pop())) {if (window.CP.shouldStopExecution(25)) break; if (node.nodeType == 3) { nextCharIndex = charIndex + node.length; if (!foundStart && bookmark.start >= charIndex && bookmark.start <= nextCharIndex) { this.setStart(node, bookmark.start - charIndex); foundStart = true; } if (foundStart && bookmark.end >= charIndex && bookmark.end <= nextCharIndex) { this.setEnd(node, bookmark.end - charIndex); stop = true; } charIndex = nextCharIndex; } else { childNodes = node.childNodes; i = childNodes.length; while (i--) {if (window.CP.shouldStopExecution(26)) break; nodeStack.push(childNodes[i]); }window.CP.exitedLoop(26); } }window.CP.exitedLoop(25); }, getName: function () { return "DomRange"; }, equals: function (range) { return Range.rangesEqual(this, range); }, isValid: function () { return isRangeValid(this); }, inspect: function () { return inspect(this); }, detach: function () { // In DOM4, detach() is now a no-op. } }); function copyComparisonConstantsToObject(obj) { obj.START_TO_START = s2s; obj.START_TO_END = s2e; obj.END_TO_END = e2e; obj.END_TO_START = e2s; obj.NODE_BEFORE = n_b; obj.NODE_AFTER = n_a; obj.NODE_BEFORE_AND_AFTER = n_b_a; obj.NODE_INSIDE = n_i; } function copyComparisonConstants(constructor) { copyComparisonConstantsToObject(constructor); copyComparisonConstantsToObject(constructor.prototype); } function createRangeContentRemover(remover, boundaryUpdater) { return function () { assertRangeValid(this); var sc = this.startContainer,so = this.startOffset,root = this.commonAncestorContainer; var iterator = new RangeIterator(this, true); // Work out where to position the range after content removal var node, boundary; if (sc !== root) { node = getClosestAncestorIn(sc, root, true); boundary = getBoundaryAfterNode(node); sc = boundary.node; so = boundary.offset; } // Check none of the range is read-only iterateSubtree(iterator, assertNodeNotReadOnly); iterator.reset(); // Remove the content var returnValue = remover(iterator); iterator.detach(); // Move to the new position boundaryUpdater(this, sc, so, sc, so); return returnValue; }; } function createPrototypeRange(constructor, boundaryUpdater) { function createBeforeAfterNodeSetter(isBefore, isStart) { return function (node) { assertValidNodeType(node, beforeAfterNodeTypes); assertValidNodeType(getRootContainer(node), rootContainerNodeTypes); var boundary = (isBefore ? getBoundaryBeforeNode : getBoundaryAfterNode)(node); (isStart ? setRangeStart : setRangeEnd)(this, boundary.node, boundary.offset); }; } function setRangeStart(range, node, offset) { var ec = range.endContainer,eo = range.endOffset; if (node !== range.startContainer || offset !== range.startOffset) { // Check the root containers of the range and the new boundary, and also check whether the new boundary // is after the current end. In either case, collapse the range to the new position if (getRootContainer(node) != getRootContainer(ec) || comparePoints(node, offset, ec, eo) == 1) { ec = node; eo = offset; } boundaryUpdater(range, node, offset, ec, eo); } } function setRangeEnd(range, node, offset) { var sc = range.startContainer,so = range.startOffset; if (node !== range.endContainer || offset !== range.endOffset) { // Check the root containers of the range and the new boundary, and also check whether the new boundary // is after the current end. In either case, collapse the range to the new position if (getRootContainer(node) != getRootContainer(sc) || comparePoints(node, offset, sc, so) == -1) { sc = node; so = offset; } boundaryUpdater(range, sc, so, node, offset); } } // Set up inheritance var F = function () {}; F.prototype = api.rangePrototype; constructor.prototype = new F(); util.extend(constructor.prototype, { setStart: function (node, offset) { assertNoDocTypeNotationEntityAncestor(node, true); assertValidOffset(node, offset); setRangeStart(this, node, offset); }, setEnd: function (node, offset) { assertNoDocTypeNotationEntityAncestor(node, true); assertValidOffset(node, offset); setRangeEnd(this, node, offset); }, /** * Convenience method to set a range's start and end boundaries. Overloaded as follows: * - Two parameters (node, offset) creates a collapsed range at that position * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at * startOffset and ending at endOffset * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in * startNode and ending at endOffset in endNode */ setStartAndEnd: function () { var args = arguments; var sc = args[0],so = args[1],ec = sc,eo = so; switch (args.length) { case 3: eo = args[2]; break; case 4: ec = args[2]; eo = args[3]; break;} boundaryUpdater(this, sc, so, ec, eo); }, setBoundary: function (node, offset, isStart) { this["set" + (isStart ? "Start" : "End")](node, offset); }, setStartBefore: createBeforeAfterNodeSetter(true, true), setStartAfter: createBeforeAfterNodeSetter(false, true), setEndBefore: createBeforeAfterNodeSetter(true, false), setEndAfter: createBeforeAfterNodeSetter(false, false), collapse: function (isStart) { assertRangeValid(this); if (isStart) { boundaryUpdater(this, this.startContainer, this.startOffset, this.startContainer, this.startOffset); } else { boundaryUpdater(this, this.endContainer, this.endOffset, this.endContainer, this.endOffset); } }, selectNodeContents: function (node) { assertNoDocTypeNotationEntityAncestor(node, true); boundaryUpdater(this, node, 0, node, getNodeLength(node)); }, selectNode: function (node) { assertNoDocTypeNotationEntityAncestor(node, false); assertValidNodeType(node, beforeAfterNodeTypes); var start = getBoundaryBeforeNode(node),end = getBoundaryAfterNode(node); boundaryUpdater(this, start.node, start.offset, end.node, end.offset); }, extractContents: createRangeContentRemover(extractSubtree, boundaryUpdater), deleteContents: createRangeContentRemover(deleteSubtree, boundaryUpdater), canSurroundContents: function () { assertRangeValid(this); assertNodeNotReadOnly(this.startContainer); assertNodeNotReadOnly(this.endContainer); // Check if the contents can be surrounded. Specifically, this means whether the range partially selects // no non-text nodes. var iterator = new RangeIterator(this, true); var boundariesInvalid = iterator._first && isNonTextPartiallySelected(iterator._first, this) || iterator._last && isNonTextPartiallySelected(iterator._last, this); iterator.detach(); return !boundariesInvalid; }, splitBoundaries: function () { splitRangeBoundaries(this); }, splitBoundariesPreservingPositions: function (positionsToPreserve) { splitRangeBoundaries(this, positionsToPreserve); }, normalizeBoundaries: function () { assertRangeValid(this); var sc = this.startContainer,so = this.startOffset,ec = this.endContainer,eo = this.endOffset; var mergeForward = function (node) { var sibling = node.nextSibling; if (sibling && sibling.nodeType == node.nodeType) { ec = node; eo = node.length; node.appendData(sibling.data); removeNode(sibling); } }; var mergeBackward = function (node) { var sibling = node.previousSibling; if (sibling && sibling.nodeType == node.nodeType) { sc = node; var nodeLength = node.length; so = sibling.length; node.insertData(0, sibling.data); removeNode(sibling); if (sc == ec) { eo += so; ec = sc; } else if (ec == node.parentNode) { var nodeIndex = getNodeIndex(node); if (eo == nodeIndex) { ec = node; eo = nodeLength; } else if (eo > nodeIndex) { eo--; } } } }; var normalizeStart = true; var sibling; if (isCharacterDataNode(ec)) { if (eo == ec.length) { mergeForward(ec); } else if (eo == 0) { sibling = ec.previousSibling; if (sibling && sibling.nodeType == ec.nodeType) { eo = sibling.length; if (sc == ec) { normalizeStart = false; } sibling.appendData(ec.data); removeNode(ec); ec = sibling; } } } else { if (eo > 0) { var endNode = ec.childNodes[eo - 1]; if (endNode && isCharacterDataNode(endNode)) { mergeForward(endNode); } } normalizeStart = !this.collapsed; } if (normalizeStart) { if (isCharacterDataNode(sc)) { if (so == 0) { mergeBackward(sc); } else if (so == sc.length) { sibling = sc.nextSibling; if (sibling && sibling.nodeType == sc.nodeType) { if (ec == sibling) { ec = sc; eo += sc.length; } sc.appendData(sibling.data); removeNode(sibling); } } } else { if (so < sc.childNodes.length) { var startNode = sc.childNodes[so]; if (startNode && isCharacterDataNode(startNode)) { mergeBackward(startNode); } } } } else { sc = ec; so = eo; } boundaryUpdater(this, sc, so, ec, eo); }, collapseToPoint: function (node, offset) { assertNoDocTypeNotationEntityAncestor(node, true); assertValidOffset(node, offset); this.setStartAndEnd(node, offset); } }); copyComparisonConstants(constructor); } /*----------------------------------------------------------------------------------------------------------------*/ // Updates commonAncestorContainer and collapsed after boundary change function updateCollapsedAndCommonAncestor(range) { range.collapsed = range.startContainer === range.endContainer && range.startOffset === range.endOffset; range.commonAncestorContainer = range.collapsed ? range.startContainer : dom.getCommonAncestor(range.startContainer, range.endContainer); } function updateBoundaries(range, startContainer, startOffset, endContainer, endOffset) { range.startContainer = startContainer; range.startOffset = startOffset; range.endContainer = endContainer; range.endOffset = endOffset; range.document = dom.getDocument(startContainer); updateCollapsedAndCommonAncestor(range); } function Range(doc) { this.startContainer = doc; this.startOffset = 0; this.endContainer = doc; this.endOffset = 0; this.document = doc; updateCollapsedAndCommonAncestor(this); } createPrototypeRange(Range, updateBoundaries); util.extend(Range, { rangeProperties: rangeProperties, RangeIterator: RangeIterator, copyComparisonConstants: copyComparisonConstants, createPrototypeRange: createPrototypeRange, inspect: inspect, toHtml: rangeToHtml, getRangeDocument: getRangeDocument, rangesEqual: function (r1, r2) { return r1.startContainer === r2.startContainer && r1.startOffset === r2.startOffset && r1.endContainer === r2.endContainer && r1.endOffset === r2.endOffset; } }); api.DomRange = Range; }); /*----------------------------------------------------------------------------------------------------------------*/ // Wrappers for the browser's native DOM Range and/or TextRange implementation api.createCoreModule("WrappedRange", ["DomRange"], function (api, module) { var WrappedRange, WrappedTextRange; var dom = api.dom; var util = api.util; var DomPosition = dom.DomPosition; var DomRange = api.DomRange; var getBody = dom.getBody; var getContentDocument = dom.getContentDocument; var isCharacterDataNode = dom.isCharacterDataNode; /*----------------------------------------------------------------------------------------------------------------*/ if (api.features.implementsDomRange) { // This is a wrapper around the browser's native DOM Range. It has two aims: // - Provide workarounds for specific browser bugs // - provide convenient extensions, which are inherited from Rangy's DomRange (function () { var rangeProto; var rangeProperties = DomRange.rangeProperties; function updateRangeProperties(range) { var i = rangeProperties.length,prop; while (i--) {if (window.CP.shouldStopExecution(27)) break; prop = rangeProperties[i]; range[prop] = range.nativeRange[prop]; } // Fix for broken collapsed property in IE 9. window.CP.exitedLoop(27);range.collapsed = range.startContainer === range.endContainer && range.startOffset === range.endOffset; } function updateNativeRange(range, startContainer, startOffset, endContainer, endOffset) { var startMoved = range.startContainer !== startContainer || range.startOffset != startOffset; var endMoved = range.endContainer !== endContainer || range.endOffset != endOffset; var nativeRangeDifferent = !range.equals(range.nativeRange); // Always set both boundaries for the benefit of IE9 (see issue 35) if (startMoved || endMoved || nativeRangeDifferent) { range.setEnd(endContainer, endOffset); range.setStart(startContainer, startOffset); } } var createBeforeAfterNodeSetter; WrappedRange = function (range) { if (!range) { throw module.createError("WrappedRange: Range must be specified"); } this.nativeRange = range; updateRangeProperties(this); }; DomRange.createPrototypeRange(WrappedRange, updateNativeRange); rangeProto = WrappedRange.prototype; rangeProto.selectNode = function (node) { this.nativeRange.selectNode(node); updateRangeProperties(this); }; rangeProto.cloneContents = function () { return this.nativeRange.cloneContents(); }; // Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect, // insertNode() is never delegated to the native range. rangeProto.surroundContents = function (node) { this.nativeRange.surroundContents(node); updateRangeProperties(this); }; rangeProto.collapse = function (isStart) { this.nativeRange.collapse(isStart); updateRangeProperties(this); }; rangeProto.cloneRange = function () { return new WrappedRange(this.nativeRange.cloneRange()); }; rangeProto.refresh = function () { updateRangeProperties(this); }; rangeProto.toString = function () { return this.nativeRange.toString(); }; // Create test range and node for feature detection var testTextNode = document.createTextNode("test"); getBody(document).appendChild(testTextNode); var range = document.createRange(); /*--------------------------------------------------------------------------------------------------------*/ // Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and // correct for it range.setStart(testTextNode, 0); range.setEnd(testTextNode, 0); try { range.setStart(testTextNode, 1); rangeProto.setStart = function (node, offset) { this.nativeRange.setStart(node, offset); updateRangeProperties(this); }; rangeProto.setEnd = function (node, offset) { this.nativeRange.setEnd(node, offset); updateRangeProperties(this); }; createBeforeAfterNodeSetter = function (name) { return function (node) { this.nativeRange[name](node); updateRangeProperties(this); }; }; } catch (ex) { rangeProto.setStart = function (node, offset) { try { this.nativeRange.setStart(node, offset); } catch (ex) { this.nativeRange.setEnd(node, offset); this.nativeRange.setStart(node, offset); } updateRangeProperties(this); }; rangeProto.setEnd = function (node, offset) { try { this.nativeRange.setEnd(node, offset); } catch (ex) { this.nativeRange.setStart(node, offset); this.nativeRange.setEnd(node, offset); } updateRangeProperties(this); }; createBeforeAfterNodeSetter = function (name, oppositeName) { return function (node) { try { this.nativeRange[name](node); } catch (ex) { this.nativeRange[oppositeName](node); this.nativeRange[name](node); } updateRangeProperties(this); }; }; } rangeProto.setStartBefore = createBeforeAfterNodeSetter("setStartBefore", "setEndBefore"); rangeProto.setStartAfter = createBeforeAfterNodeSetter("setStartAfter", "setEndAfter"); rangeProto.setEndBefore = createBeforeAfterNodeSetter("setEndBefore", "setStartBefore"); rangeProto.setEndAfter = createBeforeAfterNodeSetter("setEndAfter", "setStartAfter"); /*--------------------------------------------------------------------------------------------------------*/ // Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing // whether the native implementation can be trusted rangeProto.selectNodeContents = function (node) { this.setStartAndEnd(node, 0, dom.getNodeLength(node)); }; /*--------------------------------------------------------------------------------------------------------*/ // Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for // constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738 range.selectNodeContents(testTextNode); range.setEnd(testTextNode, 3); var range2 = document.createRange(); range2.selectNodeContents(testTextNode); range2.setEnd(testTextNode, 4); range2.setStart(testTextNode, 2); if (range.compareBoundaryPoints(range.START_TO_END, range2) == -1 && range.compareBoundaryPoints(range.END_TO_START, range2) == 1) { // This is the wrong way round, so correct for it rangeProto.compareBoundaryPoints = function (type, range) { range = range.nativeRange || range; if (type == range.START_TO_END) { type = range.END_TO_START; } else if (type == range.END_TO_START) { type = range.START_TO_END; } return this.nativeRange.compareBoundaryPoints(type, range); }; } else { rangeProto.compareBoundaryPoints = function (type, range) { return this.nativeRange.compareBoundaryPoints(type, range.nativeRange || range); }; } /*--------------------------------------------------------------------------------------------------------*/ // Test for IE deleteContents() and extractContents() bug and correct it. See issue 107. var el = document.createElement("div"); el.innerHTML = "123"; var textNode = el.firstChild; var body = getBody(document); body.appendChild(el); range.setStart(textNode, 1); range.setEnd(textNode, 2); range.deleteContents(); if (textNode.data == "13") { // Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and // extractContents() rangeProto.deleteContents = function () { this.nativeRange.deleteContents(); updateRangeProperties(this); }; rangeProto.extractContents = function () { var frag = this.nativeRange.extractContents(); updateRangeProperties(this); return frag; }; } else { } body.removeChild(el); body = null; /*--------------------------------------------------------------------------------------------------------*/ // Test for existence of createContextualFragment and delegate to it if it exists if (util.isHostMethod(range, "createContextualFragment")) { rangeProto.createContextualFragment = function (fragmentStr) { return this.nativeRange.createContextualFragment(fragmentStr); }; } /*--------------------------------------------------------------------------------------------------------*/ // Clean up getBody(document).removeChild(testTextNode); rangeProto.getName = function () { return "WrappedRange"; }; api.WrappedRange = WrappedRange; api.createNativeRange = function (doc) { doc = getContentDocument(doc, module, "createNativeRange"); return doc.createRange(); }; })(); } if (api.features.implementsTextRange) { /* This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement() method. For example, in the following (where pipes denote the selection boundaries): <ul id="ul"><li id="a">| a </li><li id="b"> b |</li></ul> var range = document.selection.createRange(); alert(range.parentElement().id); // Should alert "ul" but alerts "b" This method returns the common ancestor node of the following: - the parentElement() of the textRange - the parentElement() of the textRange after calling collapse(true) - the parentElement() of the textRange after calling collapse(false) */ var getTextRangeContainerElement = function (textRange) { var parentEl = textRange.parentElement(); var range = textRange.duplicate(); range.collapse(true); var startEl = range.parentElement(); range = textRange.duplicate(); range.collapse(false); var endEl = range.parentElement(); var startEndContainer = startEl == endEl ? startEl : dom.getCommonAncestor(startEl, endEl); return startEndContainer == parentEl ? startEndContainer : dom.getCommonAncestor(parentEl, startEndContainer); }; var textRangeIsCollapsed = function (textRange) { return textRange.compareEndPoints("StartToEnd", textRange) == 0; }; // Gets the boundary of a TextRange expressed as a node and an offset within that node. This function started // out as an improved version of code found in Tim Cameron Ryan's IERange (https://code.google.com/p/ierange/) // but has grown, fixing problems with line breaks in preformatted text, adding workaround for IE TextRange // bugs, handling for inputs and images, plus optimizations. var getTextRangeBoundaryPosition = function (textRange, wholeRangeContainerElement, isStart, isCollapsed, startInfo) { var workingRange = textRange.duplicate(); workingRange.collapse(isStart); var containerElement = workingRange.parentElement(); // Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so // check for that if (!dom.isOrIsAncestorOf(wholeRangeContainerElement, containerElement)) { containerElement = wholeRangeContainerElement; } // Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and // similar. See https://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx if (!containerElement.canHaveHTML) { var pos = new DomPosition(containerElement.parentNode, dom.getNodeIndex(containerElement)); return { boundaryPosition: pos, nodeInfo: { nodeIndex: pos.offset, containerElement: pos.node } }; } var workingNode = dom.getDocument(containerElement).createElement("span"); // Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5 // Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64 if (workingNode.parentNode) { dom.removeNode(workingNode); } var comparison,workingComparisonType = isStart ? "StartToStart" : "StartToEnd"; var previousNode, nextNode, boundaryPosition, boundaryNode; var start = startInfo && startInfo.containerElement == containerElement ? startInfo.nodeIndex : 0; var childNodeCount = containerElement.childNodes.length; var end = childNodeCount; // Check end first. Code within the loop assumes that the endth child node of the container is definitely // after the range boundary. var nodeIndex = end; while (true) {if (window.CP.shouldStopExecution(28)) break; if (nodeIndex == childNodeCount) { containerElement.appendChild(workingNode); } else { containerElement.insertBefore(workingNode, containerElement.childNodes[nodeIndex]); } workingRange.moveToElementText(workingNode); comparison = workingRange.compareEndPoints(workingComparisonType, textRange); if (comparison == 0 || start == end) { break; } else if (comparison == -1) { if (end == start + 1) { // We know the endth child node is after the range boundary, so we must be done. break; } else { start = nodeIndex; } } else { end = end == start + 1 ? start : nodeIndex; } nodeIndex = Math.floor((start + end) / 2); containerElement.removeChild(workingNode); } // We've now reached or gone past the boundary of the text range we're interested in // so have identified the node we want window.CP.exitedLoop(28);boundaryNode = workingNode.nextSibling; if (comparison == -1 && boundaryNode && isCharacterDataNode(boundaryNode)) { // This is a character data node (text, comment, cdata). The working range is collapsed at the start of // the node containing the text range's boundary, so we move the end of the working range to the // boundary point and measure the length of its text to get the boundary's offset within the node. workingRange.setEndPoint(isStart ? "EndToStart" : "EndToEnd", textRange); var offset; if (/[\r\n]/.test(boundaryNode.data)) { /* For the particular case of a boundary within a text node containing rendered line breaks (within a <pre> element, for example), we need a slightly co.........完整代码请登录后点击上方下载按钮下载查看
网友评论0