1/*! cargoEditor - v0.2.0 - 2015-05-05 */ 2 3 4// Copyright 2011 Google Inc. 5// 6// Licensed under the Apache License, Version 2.0 (the "License"); 7// you may not use this file except in compliance with the License. 8// You may obtain a copy of the License at 9// 10// http://www.apache.org/licenses/LICENSE-2.0 11// 12// Unless required by applicable law or agreed to in writing, software 13// distributed under the License is distributed on an "AS IS" BASIS, 14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15// See the License for the specific language governing permissions and 16// limitations under the License. 17var __extends = this.__extends || function (d, b) { 18 for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; 19 function __() { this.constructor = d; } 20 __.prototype = b.prototype; 21 d.prototype = new __(); 22}; 23var MutationObserverCtor; 24if (typeof WebKitMutationObserver !== 'undefined') 25 MutationObserverCtor = WebKitMutationObserver; 26else 27 MutationObserverCtor = MutationObserver; 28 29if (MutationObserverCtor === undefined) { 30 console.error('DOM Mutation Observers are required.'); 31 console.error('https://developer.mozilla.org/en-US/docs/DOM/MutationObserver'); 32 throw Error('DOM Mutation Observers are required'); 33} 34 35var NodeMap = (function () { 36 function NodeMap() { 37 this.nodes = []; 38 this.values = []; 39 } 40 NodeMap.prototype.isIndex = function (s) { 41 return +s === s >>> 0; 42 }; 43 44 NodeMap.prototype.nodeId = function (node) { 45 var id = node[NodeMap.ID_PROP]; 46 if (!id) 47 id = node[NodeMap.ID_PROP] = NodeMap.nextId_++; 48 return id; 49 }; 50 51 NodeMap.prototype.set = function (node, value) { 52 var id = this.nodeId(node); 53 this.nodes[id] = node; 54 this.values[id] = value; 55 }; 56 57 NodeMap.prototype.get = function (node) { 58 var id = this.nodeId(node); 59 return this.values[id]; 60 }; 61 62 NodeMap.prototype.has = function (node) { 63 return this.nodeId(node) in this.nodes; 64 }; 65 66 NodeMap.prototype.delete = function (node) { 67 var id = this.nodeId(node); 68 delete this.nodes[id]; 69 this.values[id] = undefined; 70 }; 71 72 NodeMap.prototype.keys = function () { 73 var nodes = []; 74 for (var id in this.nodes) { 75 if (!this.isIndex(id)) 76 continue; 77 nodes.push(this.nodes[id]); 78 } 79 80 return nodes; 81 }; 82 NodeMap.ID_PROP = '__mutation_summary_node_map_id__'; 83 NodeMap.nextId_ = 1; 84 return NodeMap; 85})(); 86 87/** 88* var reachableMatchableProduct = [ 89* // STAYED_OUT, ENTERED, STAYED_IN, EXITED 90* [ STAYED_OUT, STAYED_OUT, STAYED_OUT, STAYED_OUT ], // STAYED_OUT 91* [ STAYED_OUT, ENTERED, ENTERED, STAYED_OUT ], // ENTERED 92* [ STAYED_OUT, ENTERED, STAYED_IN, EXITED ], // STAYED_IN 93* [ STAYED_OUT, STAYED_OUT, EXITED, EXITED ] // EXITED 94* ]; 95*/ 96var Movement; 97(function (Movement) { 98 Movement[Movement["STAYED_OUT"] = 0] = "STAYED_OUT"; 99 Movement[Movement["ENTERED"] = 1] = "ENTERED"; 100 Movement[Movement["STAYED_IN"] = 2] = "STAYED_IN"; 101 Movement[Movement["REPARENTED"] = 3] = "REPARENTED"; 102 Movement[Movement["REORDERED"] = 4] = "REORDERED"; 103 Movement[Movement["EXITED"] = 5] = "EXITED"; 104})(Movement || (Movement = {})); 105 106function enteredOrExited(changeType) { 107 return changeType === 1 /* ENTERED */ || changeType === 5 /* EXITED */; 108} 109 110var NodeChange = (function () { 111 function NodeChange(node, childList, attributes, characterData, oldParentNode, added, attributeOldValues, characterDataOldValue) { 112 if (typeof childList === "undefined") { childList = false; } 113 if (typeof attributes === "undefined") { attributes = false; } 114 if (typeof characterData === "undefined") { characterData = false; } 115 if (typeof oldParentNode === "undefined") { oldParentNode = null; } 116 if (typeof added === "undefined") { added = false; } 117 if (typeof attributeOldValues === "undefined") { attributeOldValues = null; } 118 if (typeof characterDataOldValue === "undefined") { characterDataOldValue = null; } 119 this.node = node; 120 this.childList = childList; 121 this.attributes = attributes; 122 this.characterData = characterData; 123 this.oldParentNode = oldParentNode; 124 this.added = added; 125 this.attributeOldValues = attributeOldValues; 126 this.characterDataOldValue = characterDataOldValue; 127 this.isCaseInsensitive = this.node.nodeType === Node.ELEMENT_NODE && this.node instanceof HTMLElement && this.node.ownerDocument instanceof HTMLDocument; 128 } 129 NodeChange.prototype.getAttributeOldValue = function (name) { 130 if (!this.attributeOldValues) 131 return undefined; 132 if (this.isCaseInsensitive) 133 name = name.toLowerCase(); 134 return this.attributeOldValues[name]; 135 }; 136 137 NodeChange.prototype.getAttributeNamesMutated = function () { 138 var names = []; 139 if (!this.attributeOldValues) 140 return names; 141 for (var name in this.attributeOldValues) { 142 names.push(name); 143 } 144 return names; 145 }; 146 147 NodeChange.prototype.attributeMutated = function (name, oldValue) { 148 this.attributes = true; 149 this.attributeOldValues = this.attributeOldValues || {}; 150 151 if (name in this.attributeOldValues) 152 return; 153 154 this.attributeOldValues[name] = oldValue; 155 }; 156 157 NodeChange.prototype.characterDataMutated = function (oldValue) { 158 if (this.characterData) 159 return; 160 this.characterData = true; 161 this.characterDataOldValue = oldValue; 162 }; 163
164 // Note: is it possible to receive a removal followed by a removal. This 165 // can occur if the removed node is added to an non-observed node, that 166 // node is added to the observed area, and then the node removed from 167 // it. 168 NodeChange.prototype.removedFromParent = function (parent) { 169 this.childList = true; 170 if (this.added || this.oldParentNode) 171 this.added = false; 172 else 173 this.oldParentNode = parent; 174 }; 175 176 NodeChange.prototype.insertedIntoParent = function () { 177 this.childList = true; 178 this.added = true; 179 }; 180 181 // An node's oldParent is 182 // -its present parent, if its parentNode was not changed. 183 // -null if the first thing that happened to it was an add. 184 // -the node it was removed from if the first thing that happened to it 185 // was a remove. 186 NodeChange.prototype.getOldParent = function () { 187 if (this.childList) { 188 if (this.oldParentNode) 189 return this.oldParentNode; 190 if (this.added) 191 return null; 192 } 193 194 return this.node.parentNode; 195 }; 196 return NodeChange; 197})(); 198 199var ChildListChange = (function () { 200 function ChildListChange() { 201 this.added = new NodeMap(); 202 this.removed = new NodeMap(); 203 this.maybeMoved = new NodeMap(); 204 this.oldPrevious = new NodeMap(); 205 this.moved = undefined; 206 } 207 return ChildListChange; 208})(); 209 210var TreeChanges = (function (_super) { 211 __extends(TreeChanges, _super); 212 function TreeChanges(rootNode, mutations) { 213 _super.call(this); 214 215 this.rootNode = rootNode; 216 this.reachableCache = undefined; 217 this.wasReachableCache = undefined; 218 this.anyParentsChanged = false; 219 this.anyAttributesChanged = false; 220 this.anyCharacterDataChanged = false; 221 222 for (var m = 0; m < mutations.length; m++) { 223 var mutation = mutations[m]; 224 switch (mutation.type) { 225 case 'childList': 226 this.anyParentsChanged = true; 227 for (var i = 0; i < mutation.removedNodes.length; i++) { 228 var node = mutation.removedNodes[i]; 229 this.getChange(node).removedFromParent(mutation.target); 230 } 231 for (var i = 0; i < mutation.addedNodes.length; i++) { 232 var node = mutation.addedNodes[i]; 233 this.getChange(node).insertedIntoParent(); 234 } 235 break; 236 237 case 'attributes': 238 this.anyAttributesChanged = true; 239 var change = this.getChange(mutation.target); 240 change.attributeMutated(mutation.attributeName, mutation.oldValue); 241 break; 242 243 case 'characterData': 244 this.anyCharacterDataChanged = true; 245 var change = this.getChange(mutation.target); 246 change.characterDataMutated(mutation.oldValue); 247 break; 248 } 249 } 250 } 251 TreeChanges.prototype.getChange = function (node) { 252 var change = this.get(node); 253 if (!change) { 254 change = new NodeChange(node); 255 this.set(node, change); 256 } 257 return change; 258 }; 259 260 TreeChanges.prototype.getOldParent = function (node) { 261 var change = this.get(node); 262 return change ? change.getOldParent() : node.parentNode; 263 }; 264 265 TreeChanges.prototype.getIsReachable = function (node) { 266 if (node === this.rootNode) 267 return true; 268 if (!node) 269 return false; 270 271 this.reachableCache = this.reachableCache || new NodeMap(); 272 var isReachable = this.reachableCache.get(node); 273 if (isReachable === undefined) { 274 isReachable = this.getIsReachable(node.parentNode); 275 this.reachableCache.set(node, isReachable); 276 } 277 return isReachable; 278 }; 279 280 // A node wasReachable if its oldParent wasReachable. 281 TreeChanges.prototype.getWasReachable = function (node) { 282 if (node === this.rootNode) 283 return true; 284 if (!node) 285 return false;
286 287 this.wasReachableCache = this.wasReachableCache || new NodeMap(); 288 var wasReachable = this.wasReachableCache.get(node); 289 if (wasReachable === undefined) { 290 wasReachable = this.getWasReachable(this.getOldParent(node)); 291 this.wasReachableCache.set(node, wasReachable); 292 } 293 return wasReachable; 294 }; 295 296 TreeChanges.prototype.reachabilityChange = function (node) { 297 if (this.getIsReachable(node)) { 298 return this.getWasReachable(node) ? 2 /* STAYED_IN */ : 1 /* ENTERED */; 299 } 300 301 return this.getWasReachable(node) ? 5 /* EXITED */ : 0 /* STAYED_OUT */; 302 }; 303 return TreeChanges; 304})(NodeMap); 305 306var MutationProjection = (function () { 307 // TOOD(any) 308 function MutationProjection(rootNode, mutations, selectors, calcReordered, calcOldPreviousSibling) { 309 this.rootNode = rootNode; 310 this.mutations = mutations; 311 this.selectors = selectors; 312 this.calcReordered = calcReordered; 313 this.calcOldPreviousSibling = calcOldPreviousSibling; 314 this.treeChanges = new TreeChanges(rootNode, mutations); 315 this.entered = []; 316 this.exited = []; 317 this.stayedIn = new NodeMap(); 318 this.visited = new NodeMap(); 319 this.childListChangeMap = undefined; 320 this.characterDataOnly = undefined; 321 this.matchCache = undefined; 322 323 this.processMutations(); 324 } 325 MutationProjection.prototype.processMutations = function () { 326 if (!this.treeChanges.anyParentsChanged && !this.treeChanges.anyAttributesChanged) 327 return; 328 329 var changedNodes = this.treeChanges.keys(); 330 for (var i = 0; i < changedNodes.length; i++) { 331 this.visitNode(changedNodes[i], undefined); 332 } 333 }; 334 335 MutationProjection.prototype.visitNode = function (node, parentReachable) { 336 if (this.visited.has(node)) 337 return; 338 339 this.visited.set(node, true); 340 341 var change = this.treeChanges.get(node); 342 var reachable = parentReachable; 343 344 // node inherits its parent's reachability change unless 345 // its parentNode was mutated. 346 if ((change && change.childList) || reachable == undefined) 347 reachable = this.treeChanges.reachabilityChange(node); 348 349 if (reachable === 0 /* STAYED_OUT */) 350 return; 351 352 // Cache match results for sub-patterns. 353 this.matchabilityChange(node); 354 355 if (reachable === 1 /* ENTERED */) { 356 this.entered.push(node); 357 } else if (reachable === 5 /* EXITED */) { 358 this.exited.push(node); 359 this.ensureHasOldPreviousSiblingIfNeeded(node); 360 } else if (reachable === 2 /* STAYED_IN */) { 361 var movement = 2 /* STAYED_IN */; 362 363 if (change && change.childList) { 364 if (change.oldParentNode !== node.parentNode) { 365 movement = 3 /* REPARENTED */; 366 this.ensureHasOldPreviousSiblingIfNeeded(node); 367 } else if (this.calcReordered && this.wasReordered(node)) { 368 movement = 4 /* REORDERED */; 369 } 370 } 371 372 this.stayedIn.set(node, movement); 373 } 374 375 if (reachable === 2 /* STAYED_IN */) 376 return; 377 378 for (var child = node.firstChild; child; child = child.nextSibling) { 379 this.visitNode(child, reachable); 380 } 381 }; 382 383 MutationProjection.prototype.ensureHasOldPreviousSiblingIfNeeded = function (node) { 384 if (!this.calcOldPreviousSibling) 385 return; 386 387 this.processChildlistChanges(); 388 389 var parentNode = node.parentNode; 390 var nodeChange = this.treeChanges.get(node); 391 if (nodeChange && nodeChange.oldParentNode) 392 parentNode = nodeChange.oldParentNode; 393 394 var change = this.childListChangeMap.get(parentNode); 395 if (!change) { 396 change = new ChildListChange(); 397 this.childListChangeMap.set(parentNode, change); 398 } 399 400 if (!change.oldPrevious.has(node)) { 401 change.oldPrevious.set(node, node.previousSibling); 402 } 403 }; 404 405 MutationProjection.prototype.getChanged = function (summary, selectors, characterDataOnly) { 406 this.selectors = selectors; 407 this.characterDataOnly = characterDataOnly; 408 409 for (var i = 0; i < this.entered.length; i++) { 410 var node = this.entered[i]; 411 var matchable = this.matchabilityChange(node); 412 if (matchable === 1 /* ENTERED */ || matchable === 2 /* STAYED_IN */) 413 summary.added.push(node); 414 } 415 416 var stayedInNodes = this.stayedIn.keys(); 417 for (var i = 0; i < stayedInNodes.length; i++) { 418 var node = stayedInNodes[i]; 419 var matchable = this.matchabilityChange(node); 420 421 if (matchable === 1 /* ENTERED */) { 422 summary.added.push(node); 423 } else if (matchable === 5 /* EXITED */) { 424 summary.removed.push(node); 425 } else if (matchable === 2 /* STAYED_IN */ && (summary.reparented || summary.reordered)) { 426 var movement = this.stayedIn.get(node); 427 if (summary.reparented && movement === 3 /* REPARENTED */) 428 summary.reparented.push(node); 429 else if (summary.reordered && movement === 4 /* REORDERED */) 430 summary.reordered.push(node); 431 } 432 } 433 434 for (var i = 0; i < this.exited.length; i++) { 435 var node = this.exited[i]; 436 var matchable = this.matchabilityChange(node); 437 if (matchable === 5 /* EXITED */ || matchable === 2 /* STAYED_IN */) 438 summary.removed.push(node); 439 } 440 }; 441 442 MutationProjection.prototype.getOldParentNode = function (node) { 443 var change = this.treeChanges.get(node); 444 if (change && change.childList) 445 return change.oldParentNode ? change.oldParentNode : null; 446 447 var reachabilityChange = this.treeChanges.reachabilityChange(node); 448 if (reachabilityChange === 0 /* STAYED_OUT */ || reachabilityChange === 1 /* ENTERED */)
449 throw Error('getOldParentNode requested on invalid node.'); 450 451 return node.parentNode; 452 }; 453 454 MutationProjection.prototype.getOldPreviousSibling = function (node) { 455 var parentNode = node.parentNode; 456 var nodeChange = this.treeChanges.get(node); 457 if (nodeChange && nodeChange.oldParentNode) 458 parentNode = nodeChange.oldParentNode; 459 460 var change = this.childListChangeMap.get(parentNode); 461 if (!change) 462 throw Error('getOldPreviousSibling requested on invalid node.'); 463 464 return change.oldPrevious.get(node); 465 }; 466 467 MutationProjection.prototype.getOldAttribute = function (element, attrName) { 468 var change = this.treeChanges.get(element); 469 if (!change || !change.attributes) 470 throw Error('getOldAttribute requested on invalid node.'); 471 472 var value = change.getAttributeOldValue(attrName); 473 if (value === undefined) 474 throw Error('getOldAttribute requested for unchanged attribute name.'); 475 476 return value; 477 }; 478 479 MutationProjection.prototype.attributeChangedNodes = function (includeAttributes) { 480 if (!this.treeChanges.anyAttributesChanged) 481 return {}; 482 483 var attributeFilter; 484 var caseInsensitiveFilter; 485 if (includeAttributes) { 486 attributeFilter = {}; 487 caseInsensitiveFilter = {}; 488 for (var i = 0; i < includeAttributes.length; i++) { 489 var attrName = includeAttributes[i]; 490 attributeFilter[attrName] = true; 491 caseInsensitiveFilter[attrName.toLowerCase()] = attrName; 492 } 493 } 494 495 var result = {}; 496 var nodes = this.treeChanges.keys(); 497 498 for (var i = 0; i < nodes.length; i++) { 499 var node = nodes[i]; 500 501 var change = this.treeChanges.get(node); 502 if (!change.attributes) 503 continue; 504 505 if (2 /* STAYED_IN */ !== this.treeChanges.reachabilityChange(node) || 2 /* STAYED_IN */ !== this.matchabilityChange(node)) { 506 continue; 507 } 508 509 var element = node; 510 var changedAttrNames = change.getAttributeNamesMutated(); 511 for (var j = 0; j < changedAttrNames.length; j++) { 512 var attrName = changedAttrNames[j]; 513 514 if (attributeFilter && !attributeFilter[attrName] && !(change.
514isCaseInsensitive && caseInsensitiveFilter[attrName])) { 515 continue; 516 } 517 518 var oldValue = change.getAttributeOldValue(attrName); 519 if (oldValue === element.getAttribute(attrName)) 520 continue; 521 522 if (caseInsensitiveFilter && change.isCaseInsensitive) 523 attrName = caseInsensitiveFilter[attrName]; 524 525 result[attrName] = result[attrName] || []; 526 result[attrName].push(element); 527 } 528 } 529 530 return result; 531 }; 532 533 MutationProjection.prototype.getOldCharacterData = function (node) { 534 var change = this.treeChanges.get(node); 535 if (!change || !change.characterData) 536 throw Error('getOldCharacterData requested on invalid node.'); 537 538 return change.characterDataOldValue; 539 }; 540 541 MutationProjection.prototype.getCharacterDataChanged = function () { 542 if (!this.treeChanges.anyCharacterDataChanged) 543 return []; 544 545 var nodes = this.treeChanges.keys(); 546 var result = []; 547 for (var i = 0; i < nodes.length; i++) { 548 var target = nodes[i]; 549 if (2 /* STAYED_IN */ !== this.treeChanges.reachabilityChange(target)) 550 continue; 551 552 var change = this.treeChanges.get(target); 553 if (!change.characterData || target.textContent == change.characterDataOldValue) 554 continue; 555 556 result.push(target); 557 } 558 559 return result; 560 }; 561 562 MutationProjection.prototype.computeMatchabilityChange = function (selector, el) { 563 if (!this.matchCache) 564 this.matchCache = []; 565 if (!this.matchCache[selector.uid]) 566 this.matchCache[selector.uid] = new NodeMap(); 567 568 var cache = this.matchCache[selector.uid]; 569 var result = cache.get(el); 570 if (result === undefined) { 571 result = selector.matchabilityChange(el, this.treeChanges.get(el)); 572 cache.set(el, result); 573 } 574 return result; 575 }; 576 577 MutationProjection.prototype.matchabilityChange = function (node) { 578 var _this = this; 579 // TODO(rafaelw): Include PI, CDATA? 580 // Only include text nodes. 581 if (this.characterDataOnly) { 582 switch (node.nodeType) { 583 case Node.COMMENT_NODE: 584 case Node.TEXT_NODE: 585 return 2 /* STAYED_IN */; 586 default: 587 return 0 /* STAYED_OUT */; 588 } 589 } 590 591 // No element filter. Include all nodes. 592 if (!this.selectors) 593 return 2 /* STAYED_IN */; 594 595 // Element filter. Exclude non-elements. 596 if (node.nodeType !== Node.ELEMENT_NODE) 597 return 0 /* STAYED_OUT */; 598 599 var el = node; 600 601 var matchChanges = this.selectors.map(function (selector) { 602 return _this.computeMatchabilityChange(selector, el); 603 }); 604 605 var accum = 0 /* STAYED_OUT */; 606 var i = 0; 607 608 while (accum !== 2 /* STAYED_IN */ && i < matchChanges.length) { 609 switch (matchChanges[i]) { 610 case 2 /* STAYED_IN */: 611 accum = 2 /* STAYED_IN */; 612 break; 613 case 1 /* ENTERED */: 614 if (accum === 5 /* EXITED */) 615 accum = 2 /* STAYED_IN */; 616 else 617 accum = 1 /* ENTERED */; 618 break; 619 case 5 /* EXITED */: 620 if (accum === 1 /* ENTERED */) 621 accum = 2 /* STAYED_IN */; 622 else 623 accum = 5 /* EXITED */; 624 break; 625 } 626 627 i++; 628 } 629 630 return accum; 631 }; 632 633 MutationProjection.prototype.getChildlistChange = function (el) { 634 var change = this.childListChangeMap.get(el); 635 if (!change) { 636 change = new ChildListChange(); 637 this.childListChangeMap.set(el, change); 638 } 639 640 return change; 641 }; 642 643 MutationProjection.prototype.processChildlistChanges = function () { 644 if (this.childListChangeMap) 645 return; 646 647 this.childListChangeMap = new NodeMap(); 648 649 for (var i = 0; i < this.mutations.length; i++) { 650 var mutation = this.mutations[i]; 651 if (mutation.type != 'childList') 652 continue; 653 654 if (this.treeChanges.reachabilityChange(mutation.target) !== 2 /* STAYED_IN */ && !this.calcOldPreviousSibling) 655 continue; 656 657 var change = this.getChildlistChange(mutation.target); 658 659 var oldPrevious = mutation.previousSibling; 660 661 function recordOldPrevious(node, previous) { 662 if (!node || change.oldPrevious.has(node) || change.added.has(node) || change.maybeMoved.has(node)) 663 return; 664 665 if (previous && (change.added.has(previous) || change.maybeMoved.has(previous))) 666 return; 667 668 change.oldPrevious.set(node, previous); 669 } 670 671 for (var j = 0; j < mutation.removedNodes.length; j++) { 672 var node = mutation.removedNodes[j]; 673 recordOldPrevious(node, oldPrevious); 674 675 if (change.added.has(node)) { 676 change.added.delete(node); 677 } else { 678 change.removed.set(node, true); 679 change.maybeMoved.delete(node); 680 } 681 682 oldPrevious = node; 683 } 684 685 recordOldPrevious(mutation.nextSibling, oldPrevious); 686 687 for (var j = 0; j < mutation.addedNodes.length; j++) { 688 var node = mutation.addedNodes[j]; 689 if (change.removed.has(node)) { 690 change.removed.delete(node); 691 change.maybeMoved.set(node, true); 692 } else { 693 change.added.set(node, true); 694 } 695 } 696 } 697 }; 698 699 MutationProjection.prototype.wasReordered = function (node) { 700 if (!this.treeChanges.anyParentsChanged) 701 return false;
702 703 this.processChildlistChanges(); 704 705 var parentNode = node.parentNode; 706 var nodeChange = this.treeChanges.get(node); 707 if (nodeChange && nodeChange.oldParentNode) 708 parentNode = nodeChange.oldParentNode; 709 710 var change = this.childListChangeMap.get(parentNode); 711 if (!change) 712 return false; 713 714 if (change.moved) 715 return change.moved.get(node); 716 717 change.moved = new NodeMap(); 718 var pendingMoveDecision = new NodeMap(); 719 720 function isMoved(node) { 721 if (!node) 722 return false; 723 if (!change.maybeMoved.has(node)) 724 return false; 725 726 var didMove = change.moved.get(node); 727 if (didMove !== undefined) 728 return didMove; 729 730 if (pendingMoveDecision.has(node)) { 731 didMove = true; 732 } else { 733 pendingMoveDecision.set(node, true); 734 didMove = getPrevious(node) !== getOldPrevious(node); 735 } 736 737 if (pendingMoveDecision.has(node)) { 738 pendingMoveDecision.delete(node); 739 change.moved.set(node, didMove); 740 } else { 741 didMove = change.moved.get(node); 742 } 743 744 return didMove; 745 } 746 747 var oldPreviousCache = new NodeMap(); 748 function getOldPrevious(node) { 749 var oldPrevious = oldPreviousCache.get(node); 750 if (oldPrevious !== undefined) 751 return oldPrevious; 752 753 oldPrevious = change.oldPrevious.get(node); 754 while (oldPrevious && (change.removed.has(oldPrevious) || isMoved(oldPrevious))) { 755 oldPrevious = getOldPrevious(oldPrevious); 756 } 757 758 if (oldPrevious === undefined) 759 oldPrevious = node.previousSibling; 760 oldPreviousCache.set(node, oldPrevious); 761 762 return oldPrevious; 763 } 764 765 var previousCache = new NodeMap(); 766 function getPrevious(node) { 767 if (previousCache.has(node)) 768 return previousCache.get(node); 769 770 var previous = node.previousSibling; 771 while (previous && (change.added.has(previous) || isMoved(previous))) 772 previous = previous.previousSibling; 773 774 previousCache.set(node, previous); 775 return previous; 776 } 777 778 change.maybeMoved.keys().forEach(isMoved); 779 return change.moved.get(node); 780 }; 781 return MutationProjection; 782})(); 783 784var Summary = (function () { 785 function Summary(projection, query) { 786 var _this = this; 787 this.projection = projection; 788 this.added = []; 789 this.removed = []; 790 this.reparented = query.all || query.element ? [] : undefined; 791 this.reordered = query.all ? [] : undefined; 792 793 projection.getChanged(this, query.elementFilter, query.characterData); 794 795 if (query.all || query.attribute || query.attributeList) { 796 var filter = query.attribute ? [query.attribute] : query.attributeList; 797 var attributeChanged = projection.attributeChangedNodes(filter); 798 799 if (query.attribute) { 800 this.valueChanged = attributeChanged[query.attribute] || []; 801 } else { 802 this.attributeChanged = attributeChanged; 803 if (query.attributeList) { 804 query.attributeList.forEach(function (attrName) { 805 if (!_this.attributeChanged.hasOwnProperty(attrName)) 806 _this.attributeChanged[attrName] = []; 807 }); 808 } 809 } 810 } 811 812 if (query.all || query.characterData) { 813 var characterDataChanged = projection.getCharacterDataChanged(); 814 815 if (query.characterData) 816 this.valueChanged = characterDataChanged; 817 else 818 this.characterDataChanged = characterDataChanged; 819 } 820 821 if (this.reordered) 822 this.getOldPreviousSibling = projection.getOldPreviousSibling.bind(projection); 823 } 824 Summary.prototype.getOldParentNode = function (node) { 825 return this.projection.getOldParentNode(node); 826 }; 827 828 Summary.prototype.getOldAttribute = function (node, name) { 829 return this.projection.getOldAttribute(node, name); 830 }; 831 832 Summary.prototype.getOldCharacterData = function (node) { 833 return this.projection.getOldCharacterData(node); 834 }; 835 836 Summary.prototype.getOldPreviousSibling = function (node) { 837 return this.projection.getOldPreviousSibling(node); 838 }; 839 return Summary; 840})(); 841 842// TODO(rafaelw): Allow ':' and '.' as valid name characters. 843var validNameInitialChar = /[a-zA-Z_]+/; 844var validNameNonInitialChar = /[a-zA-Z0-9_\-]+/; 845 846// TODO(rafaelw): Consider allowing backslash in the attrValue. 847// TODO(rafaelw): There's got a to be way to represent this state machine 848// more compactly??? 849function escapeQuotes(value) { 850 return '"' + value.replace(/"/, '\\\"') + '"'; 851} 852 853var Qualifier = (function () { 854 function Qualifier() { 855 } 856 Qualifier.prototype.matches = function (oldValue) { 857 if (oldValue === null) 858 return false;
859 860 if (this.attrValue === undefined) 861 return true; 862 863 if (!this.contains) 864 return this.attrValue == oldValue; 865 866 var tokens = oldValue.split(' '); 867 for (var i = 0; i < tokens.length; i++) { 868 if (this.attrValue === tokens[i]) 869 return true; 870 } 871 872 return false; 873 }; 874 875 Qualifier.prototype.toString = function () { 876 if (this.attrName === 'class' && this.contains) 877 return '.' + this.attrValue; 878 879 if (this.attrName === 'id' && !this.contains) 880 return '#' + this.attrValue; 881 882 if (this.contains) 883 return '[' + this.attrName + '~=' + escapeQuotes(this.attrValue) + ']'; 884 885 if ('attrValue' in this) 886 return '[' + this.attrName + '=' + escapeQuotes(this.attrValue) + ']'; 887 888 return '[' + this.attrName + ']'; 889 }; 890 return Qualifier; 891})(); 892 893var Selector = (function () { 894 function Selector() { 895 this.uid = Selector.nextUid++; 896 this.qualifiers = []; 897 } 898 Object.defineProperty(Selector.prototype, "caseInsensitiveTagName", { 899 get: function () { 900 return this.tagName.toUpperCase(); 901 }, 902 enumerable: true, 903 configurable: true 904 }); 905 906 Object.defineProperty(Selector.prototype, "selectorString", { 907 get: function () { 908 return this.tagName + this.qualifiers.join(''); 909 }, 910 enumerable: true, 911 configurable: true 912 }); 913 914 Selector.prototype.isMatching = function (el) { 915 return el[Selector.matchesSelector](this.selectorString); 916 }; 917 918 Selector.prototype.wasMatching = function (el, change, isMatching) { 919 if (!change || !change.attributes) 920 return isMatching; 921 922 var tagName = change.isCaseInsensitive ? this.caseInsensitiveTagName : this.tagName; 923 if (tagName !== '*' && tagName !== el.tagName) 924 return false; 925 926 var attributeOldValues = []; 927 var anyChanged = false; 928 for (var i = 0; i < this.qualifiers.length; i++) { 929 var qualifier = this.qualifiers[i]; 930 var oldValue = change.getAttributeOldValue(qualifier.attrName); 931 attributeOldValues.push(oldValue); 932 anyChanged = anyChanged || (oldValue !== undefined); 933 } 934 935 if (!anyChanged) 936 return isMatching; 937 938 for (var i = 0; i < this.qualifiers.length; i++) { 939 var qualifier = this.qualifiers[i]; 940 var oldValue = attributeOldValues[i]; 941 if (oldValue === undefined) 942 oldValue = el.getAttribute(qualifier.attrName); 943 if (!qualifier.matches(oldValue)) 944 return false; 945 } 946 947 return true; 948 }; 949 950 Selector.prototype.matchabilityChange = function (el, change) { 951 var isMatching = this.isMatching(el); 952 if (isMatching) 953 return this.wasMatching(el, change, isMatching) ? 2 /* STAYED_IN */ : 1 /* ENTERED */; 954 else 955 return this.wasMatching(el, change, isMatching) ? 5 /* EXITED */ : 0 /* STAYED_OUT */; 956 }; 957 958 Selector.parseSelectors = function (input) { 959 var selectors = []; 960 var currentSelector; 961 var currentQualifier; 962 963 function newSelector() { 964 if (currentSelector) { 965 if (currentQualifier) { 966 currentSelector.qualifiers.push(currentQualifier); 967 currentQualifier = undefined; 968 } 969 970 selectors.push(currentSelector); 971 } 972 currentSelector = new Selector(); 973 } 974 975 function newQualifier() { 976 if (currentQualifier) 977 currentSelector.qualifiers.push(currentQualifier); 978 979 currentQualifier = new Qualifier(); 980 } 981 982 var WHITESPACE = /\s/; 983 var valueQuoteChar; 984 var SYNTAX_ERROR = 'Invalid or unsupported selector syntax.'; 985 986 var SELECTOR = 1; 987 var TAG_NAME = 2; 988 var QUALIFIER = 3; 989 var QUALIFIER_NAME_FIRST_CHAR = 4; 990 var QUALIFIER_NAME = 5; 991 var ATTR_NAME_FIRST_CHAR = 6; 992 var ATTR_NAME = 7; 993 var EQUIV_OR_ATTR_QUAL_END = 8; 994 var EQUAL = 9; 995 var ATTR_QUAL_END = 10; 996 var VALUE_FIRST_CHAR = 11; 997 var VALUE = 12; 998 var QUOTED_VALUE = 13; 999 var SELECTOR_SEPARATOR = 14; 1000 1001 var state = SELECTOR; 1002 var i = 0; 1003 while (i < input.length) {
1004 var c = input[i++]; 1005 1006 switch (state) { 1007 case SELECTOR: 1008 if (c.match(validNameInitialChar)) { 1009 newSelector(); 1010 currentSelector.tagName = c; 1011 state = TAG_NAME; 1012 break; 1013 } 1014 1015 if (c == '*') { 1016 newSelector(); 1017 currentSelector.tagName = '*'; 1018 state = QUALIFIER; 1019 break; 1020 } 1021 1022 if (c == '.') { 1023 newSelector(); 1024 newQualifier(); 1025 currentSelector.tagName = '*'; 1026 currentQualifier.attrName = 'class'; 1027 currentQualifier.contains = true; 1028 state = QUALIFIER_NAME_FIRST_CHAR; 1029 break; 1030 } 1031 if (c == '#') { 1032 newSelector(); 1033 newQualifier(); 1034 currentSelector.tagName = '*'; 1035 currentQualifier.attrName = 'id'; 1036 state = QUALIFIER_NAME_FIRST_CHAR; 1037 break; 1038 } 1039 if (c == '[') { 1040 newSelector(); 1041 newQualifier(); 1042 currentSelector.tagName = '*'; 1043 currentQualifier.attrName = ''; 1044 state = ATTR_NAME_FIRST_CHAR; 1045 break; 1046 } 1047 1048 if (c.match(WHITESPACE)) 1049 break; 1050 1051 throw Error(SYNTAX_ERROR); 1052 1053 case TAG_NAME: 1054 if (c.match(validNameNonInitialChar)) { 1055 currentSelector.tagName += c; 1056 break; 1057 } 1058 1059 if (c == '.') { 1060 newQualifier(); 1061 currentQualifier.attrName = 'class'; 1062 currentQualifier.contains = true; 1063 state = QUALIFIER_NAME_FIRST_CHAR; 1064 break; 1065 } 1066 if (c == '#') { 1067 newQualifier(); 1068 currentQualifier.attrName = 'id'; 1069 state = QUALIFIER_NAME_FIRST_CHAR; 1070 break; 1071 } 1072 if (c == '[') { 1073 newQualifier(); 1074 currentQualifier.attrName = ''; 1075 state = ATTR_NAME_FIRST_CHAR; 1076 break; 1077 } 1078 1079 if (c.match(WHITESPACE)) { 1080 state = SELECTOR_SEPARATOR; 1081 break; 1082 } 1083 1084 if (c == ',') { 1085 state = SELECTOR; 1086 break; 1087 } 1088 1089 throw Error(SYNTAX_ERROR); 1090 1091 case QUALIFIER: 1092 if (c == '.') { 1093 newQualifier(); 1094 currentQualifier.attrName = 'class'; 1095 currentQualifier.contains = true; 1096 state = QUALIFIER_NAME_FIRST_CHAR; 1097 break; 1098 } 1099 if (c == '#') { 1100 newQualifier(); 1101 currentQualifier.attrName = 'id'; 1102 state = QUALIFIER_NAME_FIRST_CHAR; 1103 break; 1104 } 1105 if (c == '[') { 1106 newQualifier(); 1107 currentQualifier.attrName = ''; 1108 state = ATTR_NAME_FIRST_CHAR; 1109 break; 1110 } 1111 1112 if (c.match(WHITESPACE)) { 1113 state = SELECTOR_SEPARATOR; 1114 break; 1115 } 1116 1117 if (c == ',') { 1118 state = SELECTOR; 1119 break; 1120 } 1121 1122 throw Error(SYNTAX_ERROR); 1123 1124 case QUALIFIER_NAME_FIRST_CHAR: 1125 if (c.match(validNameInitialChar)) { 1126 currentQualifier.attrValue = c; 1127 state = QUALIFIER_NAME; 1128 break; 1129 } 1130 1131 throw Error(SYNTAX_ERROR); 1132 1133 case QUALIFIER_NAME: 1134 if (c.match(validNameNonInitialChar)) { 1135 currentQualifier.attrValue += c; 1136 break; 1137 } 1138 1139 if (c == '.') { 1140 newQualifier(); 1141 currentQualifier.attrName = 'class'; 1142 currentQualifier.contains = true; 1143 state = QUALIFIER_NAME_FIRST_CHAR; 1144 break; 1145 } 1146 if (c == '#') { 1147 newQualifier(); 1148 currentQualifier.attrName = 'id'; 1149 state = QUALIFIER_NAME_FIRST_CHAR; 1150 break; 1151 } 1152 if (c == '[') { 1153 newQualifier(); 1154 state = ATTR_NAME_FIRST_CHAR; 1155 break; 1156 } 1157 1158 if (c.match(WHITESPACE)) { 1159 state = SELECTOR_SEPARATOR; 1160 break; 1161 } 1162 if (c == ',') { 1163 state = SELECTOR; 1164 break; 1165 } 1166 1167 throw Error(SYNTAX_ERROR); 1168 1169 case ATTR_NAME_FIRST_CHAR: 1170 if (c.match(validNameInitialChar)) { 1171 currentQualifier.attrName = c; 1172 state = ATTR_NAME; 1173 break; 1174 } 1175 1176 if (c.match(WHITESPACE)) 1177 break; 1178 1179 throw Error(SYNTAX_ERROR); 1180 1181 case ATTR_NAME: 1182 if (c.match(validNameNonInitialChar)) { 1183 currentQualifier.attrName += c; 1184 break; 1185 } 1186 1187 if (c.match(WHITESPACE)) { 1188 state = EQUIV_OR_ATTR_QUAL_END; 1189 break; 1190 } 1191 1192 if (c == '~') { 1193 currentQualifier.contains = true; 1194 state = EQUAL; 1195 break; 1196 } 1197 1198 if (c == '=') { 1199 currentQualifier.attrValue = ''; 1200 state = VALUE_FIRST_CHAR; 1201 break; 1202 } 1203 1204 if (c == ']') { 1205 state = QUALIFIER; 1206 break; 1207 } 1208 1209 throw Error(SYNTAX_ERROR); 1210 1211 case EQUIV_OR_ATTR_QUAL_END: 1212 if (c == '~') { 1213 currentQualifier.contains = true; 1214 state = EQUAL; 1215 break; 1216 } 1217 1218 if (c == '=') { 1219 currentQualifier.attrValue = ''; 1220 state = VALUE_FIRST_CHAR; 1221 break; 1222 } 1223 1224 if (c == ']') { 1225 state = QUALIFIER; 1226 break; 1227 } 1228 1229 if (c.match(WHITESPACE)) 1230 break; 1231 1232 throw Error(SYNTAX_ERROR); 1233 1234 case EQUAL: 1235 if (c == '=') { 1236 currentQualifier.attrValue = ''; 1237 state = VALUE_FIRST_CHAR; 1238 break; 1239 } 1240 1241 throw Error(SYNTAX_ERROR); 1242 1243 case ATTR_QUAL_END: 1244 if (c == ']') { 1245 state = QUALIFIER; 1246 break; 1247 } 1248 1249 if (c.match(WHITESPACE)) 1250 break; 1251 1252 throw Error(SYNTAX_ERROR); 1253 1254 case VALUE_FIRST_CHAR: 1255 if (c.match(WHITESPACE)) 1256 break; 1257 1258 if (c == '"' || c == "'") { 1259 valueQuoteChar = c; 1260 state = QUOTED_VALUE; 1261 break; 1262 } 1263 1264 currentQualifier.attrValue += c; 1265 state = VALUE; 1266 break; 1267 1268 case VALUE: 1269 if (c.match(WHITESPACE)) { 1270 state = ATTR_QUAL_END; 1271 break; 1272 } 1273 if (c == ']') { 1274 state = QUALIFIER; 1275 break; 1276 } 1277 if (c == "'" || c == '"') 1278 throw Error(SYNTAX_ERROR); 1279 1280 currentQualifier.attrValue += c; 1281 break; 1282 1283 case QUOTED_VALUE: 1284 if (c == valueQuoteChar) { 1285 state = ATTR_QUAL_END; 1286 break; 1287 } 1288 1289 currentQualifier.attrValue += c; 1290 break; 1291 1292 case SELECTOR_SEPARATOR: 1293 if (c.match(WHITESPACE)) 1294 break; 1295 1296 if (c == ',') { 1297 state = SELECTOR; 1298 break; 1299 } 1300 1301 throw Error(SYNTAX_ERROR); 1302 } 1303 } 1304 1305 switch (state) { 1306 case SELECTOR: 1307 case TAG_NAME: 1308 case QUALIFIER: 1309 case QUALIFIER_NAME: 1310 case SELECTOR_SEPARATOR: 1311 // Valid end states. 1312 newSelector(); 1313 break; 1314 default: 1315 throw Error(SYNTAX_ERROR); 1316 } 1317 1318 if (!selectors.length) 1319 throw Error(SYNTAX_ERROR); 1320 1321 return selectors; 1322 }; 1323 Selector.nextUid = 1; 1324 Selector.matchesSelector = (function () { 1325 var element = document.createElement('div'); 1326 if (typeof element['webkitMatchesSelector'] === 'function') 1327 return 'webkitMatchesSelector'; 1328 if (typeof element['mozMatchesSelector'] === 'function') 1329 return 'mozMatchesSelector';
1330 if (typeof element['msMatchesSelector'] === 'function') 1331 return 'msMatchesSelector'; 1332 1333 return 'matchesSelector'; 1334 })(); 1335 return Selector; 1336})(); 1337 1338var attributeFilterPattern = /^([a-zA-Z:_]+[a-zA-Z0-9_\-:\.]*)$/; 1339 1340function validateAttribute(attribute) { 1341 if (typeof attribute != 'string') 1342 throw Error('Invalid request opion. attribute must be a non-zero length string.'); 1343 1344 attribute = attribute.trim(); 1345 1346 if (!attribute) 1347 throw Error('Invalid request opion. attribute must be a non-zero length string.'); 1348 1349 if (!attribute.match(attributeFilterPattern)) 1350 throw Error('Invalid request option. invalid attribute name: ' + attribute); 1351 1352 return attribute; 1353} 1354 1355function validateElementAttributes(attribs) { 1356 if (!attribs.trim().length) 1357 throw Error('Invalid request option: elementAttributes must contain at least one attribute.'); 1358 1359 var lowerAttributes = {}; 1360 var attributes = {}; 1361 1362 var tokens = attribs.split(/\s+/); 1363 for (var i = 0; i < tokens.length; i++) { 1364 var name = tokens[i]; 1365 if (!name) 1366 continue; 1367 1368 var name = validateAttribute(name); 1369 var nameLower = name.toLowerCase(); 1370 if (lowerAttributes[nameLower]) 1371 throw Error('Invalid request option: observing multiple case variations of the same attribute is not supported.'); 1372 1373 attributes[name] = true; 1374 lowerAttributes[nameLower] = true; 1375 } 1376 1377 return Object.keys(attributes); 1378} 1379 1380function elementFilterAttributes(selectors) { 1381 var attributes = {}; 1382 1383 selectors.forEach(function (selector) { 1384 selector.qualifiers.forEach(function (qualifier) { 1385 attributes[qualifier.attrName] = true; 1386 }); 1387 }); 1388 1389 return Object.keys(attributes); 1390} 1391 1392var MutationSummary = (function () { 1393 function MutationSummary(opts) { 1394 var _this = this; 1395 this.connected = false; 1396 this.suspended = false; 1397 this.queue = []; 1398 this.options = MutationSummary.validateOptions(opts); 1399 this.observerOptions = MutationSummary.createObserverOptions(this.options.queries); 1400 this.root = this.options.rootNode; 1401 this.callback = this.options.callback; 1402 1403 this.elementFilter = Array.prototype.concat.apply([], this.options.queries.map(function (query) { 1404 return query.elementFilter ? query.elementFilter : []; 1405 })); 1406 if (!this.elementFilter.length) 1407 this.elementFilter = undefined; 1408 1409 this.calcReordered = this.options.queries.some(function (query) { 1410 return query.all; 1411 }); 1412 1413 this.queryValidators = []; // TODO(rafaelw): Shouldn't always define this. 1414 if (MutationSummary.createQueryValidator) { 1415 this.queryValidators = this.options.queries.map(function (query) { 1416 return MutationSummary.createQueryValidator(_this.root, query); 1417 }); 1418 } 1419 1420 this.observer = new MutationObserverCtor(function (mutations) { 1421 1422 if(_this.suspended){ 1423 _this.queue = _this.queue.concat(mutations); 1424 return; 1425 } 1426 1427 _this.observerCallback(mutations); 1428 }); 1429 1430 this.reconnect(); 1431 } 1432 MutationSummary.createObserverOptions = function (queries) { 1433 var observerOptions = { 1434 childList: true, 1435 subtree: true 1436 }; 1437 1438 var attributeFilter; 1439 function observeAttributes(attributes) { 1440 if (observerOptions.attributes && !attributeFilter) 1441 return; 1442 1443 observerOptions.attributes = true; 1444 observerOptions.attributeOldValue = true; 1445 1446 if (!attributes) { 1447 // observe all. 1448 attributeFilter = undefined; 1449 return; 1450 } 1451 1452 // add to observed. 1453 attributeFilter = attributeFilter || {}; 1454 attributes.forEach(function (attribute) { 1455 attributeFilter[attribute] = true; 1456 attributeFilter[attribute.toLowerCase()] = true; 1457 }); 1458 } 1459
1460 queries.forEach(function (query) { 1461 if (query.characterData) { 1462 observerOptions.characterData = true; 1463 observerOptions.characterDataOldValue = true; 1464 return; 1465 } 1466 1467 if (query.all) { 1468 observeAttributes(); 1469 observerOptions.characterData = true; 1470 observerOptions.characterDataOldValue = true; 1471 return; 1472 } 1473 1474 if (query.attribute) { 1475 observeAttributes([query.attribute.trim()]); 1476 return; 1477 } 1478 1479 var attributes = elementFilterAttributes(query.elementFilter).concat(query.attributeList || []); 1480 if (attributes.length) 1481 observeAttributes(attributes); 1482 }); 1483 1484 if (attributeFilter) 1485 observerOptions.attributeFilter = Object.keys(attributeFilter); 1486 1487 return observerOptions; 1488 }; 1489 1490 MutationSummary.validateOptions = function (options) { 1491 for (var prop in options) { 1492 if (!(prop in MutationSummary.optionKeys)) 1493 throw Error('Invalid option: ' + prop); 1494 } 1495 1496 if (typeof options.callback !== 'function') 1497 throw Error('Invalid options: callback is required and must be a function'); 1498 1499 if (!options.queries || !options.queries.length) 1500 throw Error('Invalid options: queries must contain at least one query request object.'); 1501 1502 var opts = { 1503 callback: options.callback, 1504 rootNode: options.rootNode || document, 1505 observeOwnChanges: !!options.observeOwnChanges, 1506 oldPreviousSibling: !!options.oldPreviousSibling, 1507 queries: [] 1508 }; 1509 1510 for (var i = 0; i < options.queries.length; i++) { 1511 var request = options.queries[i]; 1512 1513 // all 1514 if (request.all) { 1515 if (Object.keys(request).length > 1) 1516 throw Error('Invalid request option. all has no options.'); 1517 1518 opts.queries.push({ all: true }); 1519 continue; 1520 } 1521 1522 // attribute 1523 if ('attribute' in request) { 1524 var query = { 1525 attribute: validateAttribute(request.attribute) 1526 }; 1527 1528 query.elementFilter = Selector.parseSelectors('*[' + query.attribute + ']'); 1529 1530 if (Object.keys(request).length > 1) 1531 throw Error('Invalid request option. attribute has no options.'); 1532 1533 opts.queries.push(query); 1534 continue; 1535 } 1536 1537 // element 1538 if ('element' in request) { 1539 var requestOptionCount = Object.keys(request).length; 1540 var query = { 1541 element: request.element, 1542 elementFilter: Selector.parseSelectors(request.element) 1543 }; 1544 1545 if (request.hasOwnProperty('elementAttributes')) { 1546 query.attributeList = validateElementAttributes(request.elementAttributes); 1547 requestOptionCount--; 1548 } 1549 1550 if (requestOptionCount > 1) 1551 throw Error('Invalid request option. element only allows elementAttributes option.'); 1552 1553 opts.queries.push(query); 1554 continue; 1555 } 1556 1557 // characterData 1558 if (request.characterData) { 1559 if (Object.keys(request).length > 1) 1560 throw Error('Invalid request option. characterData has no options.'); 1561 1562 opts.queries.push({ characterData: true }); 1563 continue; 1564 } 1565 1566 throw Error('Invalid request option. Unknown query request.'); 1567 } 1568 1569 return opts; 1570 }; 1571 1572 MutationSummary.prototype.createSummaries = function (mutations) { 1573 if (!mutations || !mutations.length) 1574 return []; 1575 1576 var projection = new MutationProjection(this.root, mutations, this.elementFilter, this.calcReordered, this.options.oldPreviousSibling); 1577 1578 var summaries = []; 1579 for (var i = 0; i < this.options.queries.length; i++) { 1580 summaries.push(new Summary(projection, this.options.queries[i])
1580); 1581 } 1582 1583 return summaries; 1584 }; 1585 1586 MutationSummary.prototype.checkpointQueryValidators = function () { 1587 this.queryValidators.forEach(function (validator) { 1588 if (validator) 1589 validator.recordPreviousState(); 1590 }); 1591 }; 1592 1593 MutationSummary.prototype.runQueryValidators = function (summaries) { 1594 this.queryValidators.forEach(function (validator, index) { 1595 if (validator) 1596 validator.validate(summaries[index]); 1597 }); 1598 }; 1599 1600 MutationSummary.prototype.changesToReport = function (summaries) { 1601 return summaries.some(function (summary) { 1602 var summaryProps = [ 1603 'added', 'removed', 'reordered', 'reparented', 1604 'valueChanged', 'characterDataChanged']; 1605 if (summaryProps.some(function (prop) { 1606 return summary[prop] && summary[prop].length; 1607 })) 1608 return true; 1609 1610 if (summary.attributeChanged) { 1611 var attrNames = Object.keys(summary.attributeChanged); 1612 var attrsChanged = attrNames.some(function (attrName) { 1613 return !!summary.attributeChanged[attrName].length; 1614 }); 1615 if (attrsChanged) 1616 return true; 1617 } 1618 return false; 1619 }); 1620 }; 1621 1622 MutationSummary.prototype.observerCallback = function (mutations) { 1623 if (!this.options.observeOwnChanges) 1624 this.observer.disconnect(); 1625 1626 var summaries = this.createSummaries(mutations); 1627 this.runQueryValidators(summaries); 1628 1629 if (this.options.observeOwnChanges) 1630 this.checkpointQueryValidators(); 1631 1632 if (this.changesToReport(summaries)) 1633 this.callback(summaries); 1634 1635 // disconnect() may have been called during the callback. 1636 if (!this.options.observeOwnChanges && this.connected) { 1637 this.checkpointQueryValidators(); 1638 this.observer.observe(this.root, this.observerOptions); 1639 } 1640 }; 1641 1642 MutationSummary.prototype.reconnect = function () { 1643 if (this.connected) 1644 throw Error('Already connected'); 1645 1646 this.observer.observe(this.root, this.observerOptions); 1647 this.connected = true; 1648 this.checkpointQueryValidators(); 1649 }; 1650 1651 MutationSummary.prototype.takeSummaries = function () { 1652 if (!this.connected) 1653 throw Error('Not connected'); 1654 1655 this.queue = this.queue.concat(this.observer.takeRecords()); 1656 var summaries = this.createSummaries(this.queue); 1657 this.queue = []; 1658 1659 return this.changesToReport(summaries) ? summaries : undefined; 1660 }; 1661 1662 MutationSummary.prototype.suspendDelivery = function(){ 1663 this.suspended = true; 1664 } 1665 1666 MutationSummary.prototype.resumeDelivery = function(){ 1667 this.suspended = false; 1668 return this.takeSummaries(); 1669 } 1670 1671 MutationSummary.prototype.disconnect = function () { 1672 var summaries = this.takeSummaries(); 1673 this.observer.disconnect(); 1674 this.connected = false; 1675 return summaries; 1676 }; 1677 1678 MutationSummary.NodeMap = NodeMap; 1679 MutationSummary.parseElementFilter = Selector.parseSelectors; 1680 1681 MutationSummary.optionKeys = { 1682 'callback': true, 1683 'queries': true, 1684 'rootNode': true, 1685 'oldPreviousSibling': true, 1686 'observeOwnChanges': true 1687 }; 1688 return MutationSummary; 1689})(); 1690 1691 1692/*! cargoEditor - v0.2.0 - 2015-05-05 */ 1693 1694 1695/** 1696 * The namespace in which all CargoEditor methods and properties reside. 1697 * @name CargoEditor 1698 * @namespace 1699 * @property {CargoEditor.EventManager} events This is a reference to the EventManager singleton 1700 * @public 1701 * @author Aart Röst 1702 */ 1703 1704(function(CargoEditor) { 1705 1706 'use strict'; 1707 1708 // Some feature detection 1709 1710 if(typeof MutationObserver === 'undefined'){ 1711 throw 'Unable to init the editor. MutationObservers are not supported in this browser.'; 1712 } 1713 1714(function() { 1715 1716 /** 1717 * Standalone extraction of Backbone.Events, no external dependency required. 1718 * Degrades nicely when Backone/underscore are already available in the current 1719 * global context. 1720 * 1721 * Note that docs suggest to use underscore's `_.extend()` method to add Events 1722 * support to some given object. A `mixin()` method has been added to the Events 1723 * prototype to avoid using underscore for that sole purpose: 1724 * 1725 * var myEventEmitter = BackboneEvents.mixin({}); 1726 * 1727 * Or for a function constructor: 1728 * 1729 * function MyConstructor(){} 1730 * MyConstructor.prototype.foo = function(){} 1731 * BackboneEvents.mixin(MyConstructor.prototype); 1732 *
1733 * (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. 1734 * (c) 2013 Nicolas Perriault 1735 */ 1736 1737 /* global exports:true, define, module */ 1738 1739 var evts = {}; 1740 1741 (function() { 1742 1743 var root = this, 1744 nativeForEach = Array.prototype.forEach, 1745 hasOwnProperty = Object.prototype.hasOwnProperty, 1746 slice = Array.prototype.slice, 1747 idCounter = 0; 1748 1749 1750 // Returns a partial implementation matching the minimal API subset required 1751 // by Backbone.Events 1752 1753 function miniscore() { 1754 return { 1755 keys: Object.keys || function(obj) { 1756 if (typeof obj !== "object" && typeof obj !== "function" || obj === null) { 1757 throw new TypeError("keys() called on a non-object"); 1758 } 1759 var key, keys = []; 1760 for (key in obj) { 1761 if (obj.hasOwnProperty(key)) { 1762 keys[keys.length] = key; 1763 } 1764 } 1765 return keys; 1766 }, 1767 1768 uniqueId: function(prefix) { 1769 var id = ++idCounter + ''; 1770 return prefix ? prefix + id : id; 1771 }, 1772 1773 has: function(obj, key) { 1774 return hasOwnProperty.call(obj, key); 1775 }, 1776 1777 each: function(obj, iterator, context) { 1778 if (obj == null) return; 1779 if (nativeForEach && obj.forEach === nativeForEach) { 1780 obj.forEach(iterator, context); 1781 } else if (obj.length === +obj.length) { 1782 for (var i = 0, l = obj.length; i < l; i++) { 1783 iterator.call(context, obj[i], i, obj); 1784 } 1785 } else { 1786 for (var key in obj) { 1787 if (this.has(obj, key)) { 1788 iterator.call(context, obj[key], key, obj); 1789 } 1790 } 1791 } 1792 }, 1793 1794 once: function(func) { 1795 var ran = false, 1796 memo; 1797 return function() { 1798 if (ran) return memo; 1799 ran = true; 1800 memo = func.apply(this, arguments); 1801 func = null; 1802 return memo; 1803 }; 1804 } 1805 }; 1806 } 1807 1808 var _ = miniscore(), 1809 Events; 1810 1811 // Backbone.Events 1812 // --------------- 1813 1814 // A module that can be mixed in to *any object* in order to provide it with 1815 // custom events. You may bind with `on` or remove with `off` callback 1816 // functions to an event; `trigger`-ing an event fires all callbacks in 1817 // succession. 1818 // 1819 // var object = {}; 1820 // _.extend(object, Backbone.Events); 1821 // object.on('expand', function(){ alert('expanded'); }); 1822 // object.trigger('expand'); 1823 // 1824 Events = { 1825 1826 // Bind an event to a `callback` function. Passing `"all"` will bind 1827 // the callback to all events fired. 1828 on: function(name, callback, context) { 1829 if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; 1830 this._events || (this._events = {}); 1831 var events = this._events[name] || (this._events[name] = []); 1832 events.push({ 1833 callback: callback, 1834 context: context, 1835 ctx: context || this 1836 }); 1837 return this; 1838 }, 1839 1840 // Bind an event to only be triggered a single time. After the first time 1841 // the callback is invoked, it will be removed. 1842 once: function(name, callback, context) { 1843 if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; 1844 var self = this; 1845 var once = _.once(function() { 1846 self.off(name, once); 1847 callback.apply(this, arguments); 1848 }); 1849 once._callback = callback; 1850 return this.on(name, once, context); 1851 }, 1852 1853 // Remove one or many callbacks. If `context` is null, removes all 1854 // callbacks with that function. If `callback` is null, removes all 1855 // callbacks for the event. If `name` is null, removes all bound 1856 // callbacks for all events. 1857 off: function(name, callback, context) { 1858 var retain, ev, events, names, i, l, j, k; 1859 if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; 1860 if (!name && !callback && !context) { 1861 this._events = {}; 1862 return this; 1863 } 1864 1865 names = name ? [name] : _.keys(this._events); 1866 for (i = 0, l = names.length; i < l; i++) { 1867 name = names[i]; 1868 if (events = this._events[name]) { 1869 this._events[name] = retain = []; 1870 if (callback || context) { 1871 for (j = 0, k = events.length; j < k; j++) { 1872 ev = events[j]; 1873 if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || 1874 (context && context !== ev.context)) { 1875 retain.push(ev); 1876 } 1877 } 1878 } 1879 if (!retain.length) delete this._events[name]; 1880 } 1881 } 1882 1883 return this; 1884 }, 1885 1886 // Trigger one or many events, firing all bound callbacks. Callbacks are 1887 // passed the same arguments as `trigger` is, apart from the event name 1888 // (unless you're listening on `"all"`, which will cause your callback to 1889 // receive the true name of the event as the first argument). 1890 trigger: function(name) { 1891 if (!this._events) return this; 1892 var args = slice.call(arguments, 1); 1893 if (!eventsApi(this, 'trigger', name, args)) return this; 1894 var events = this._events[name]; 1895 var allEvents = this._events.all; 1896 if (events) triggerEvents(events, args); 1897 if (allEvents) triggerEvents(allEvents, arguments); 1898 return this; 1899 }, 1900 1901 // Tell this object to stop listening to either specific events ... or 1902 // to every object it's currently listening to. 1903 stopListening: function(obj, name, callback) { 1904 var listeners = this._listeners; 1905 if (!listeners) return this; 1906 var deleteListener = !name && !callback; 1907 if (typeof name === 'object') callback = this; 1908 if (obj)(listeners = {})[obj._listenerId] = obj; 1909 for (var id in listeners) { 1910 listeners[id].off(name, callback, this); 1911 if (deleteListener) delete this._listeners[id]; 1912 } 1913 return this; 1914 } 1915 1916 }; 1917 1918 // Regular expression used to split event strings. 1919 var eventSplitter = /\s+/; 1920 1921 // Implement fancy features of the Events API such as multiple event 1922 // names `"change blur"` and jQuery-style event maps `{change: action}` 1923 // in terms of the existing API. 1924 var eventsApi = function(obj, action, name, rest) { 1925 if (!name) return true; 1926 1927 // Handle event maps. 1928 if (typeof name === 'object') { 1929 for (var key in name) { 1930 obj[action].apply(obj, [key, name[key]].concat(rest)); 1931 } 1932 return false;
1933 } 1934 1935 // Handle space separated event names. 1936 if (eventSplitter.test(name)) { 1937 var names = name.split(eventSplitter); 1938 for (var i = 0, l = names.length; i < l; i++) { 1939 obj[action].apply(obj, [names[i]].concat(rest)); 1940 } 1941 return false; 1942 } 1943 1944 return true; 1945 }; 1946 1947 // A difficult-to-believe, but optimized internal dispatch function for 1948 // triggering events. Tries to keep the usual cases speedy (most internal 1949 // Backbone events have 3 arguments). 1950 var triggerEvents = function(events, args) { 1951 var ev, i = -1, 1952 l = events.length, 1953 a1 = args[0], 1954 a2 = args[1], 1955 a3 = args[2]; 1956 switch (args.length) { 1957 case 0: 1958 while (++i < l)(ev = events[i]).callback.call(ev.ctx); 1959 return; 1960 case 1: 1961 while (++i < l)(ev = events[i]).callback.call(ev.ctx, a1); 1962 return; 1963 case 2: 1964 while (++i < l)(ev = events[i]).callback.call(ev.ctx, a1, a2); 1965 return; 1966 case 3: 1967 while (++i < l)(ev = events[i]).callback.call(ev.ctx, a1, a2, a3); 1968 return; 1969 default: 1970 while (++i < l)(ev = events[i]).callback.apply(ev.ctx, args); 1971 } 1972 }; 1973 1974 var listenMethods = { 1975 listenTo: 'on', 1976 listenToOnce: 'once' 1977 }; 1978 1979 // Inversion-of-control versions of `on` and `once`. Tell *this* object to 1980 // listen to an event in another object ... keeping track of what it's 1981 // listening to. 1982 _.each(listenMethods, function(implementation, method) { 1983 Events[method] = function(obj, name, callback) { 1984 var listeners = this._listeners || (this._listeners = {}); 1985 var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); 1986 listeners[id] = obj; 1987 if (typeof name === 'object') callback = this; 1988 obj[implementation](name, callback, this); 1989 return this; 1990 }; 1991 }); 1992 1993 // Aliases for backwards compatibility. 1994 Events.bind = Events.on; 1995 Events.unbind = Events.off; 1996 Events.fire = function(name, args){ 1997
1998 console.warn('fire is deprecated', arguments, [name].concat(args)); 1999 2000 Events.trigger.apply(this, [name].concat(args)); 2001 } 2002 2003 // Mixin utility 2004 Events.mixin = function(proto) { 2005 var exports = ['on', 'once', 'off', 'trigger', 'stopListening', 'listenTo', 2006 'listenToOnce', 'bind', 'unbind', 'fire' 2007 ]; 2008 _.each(exports, function(name) { 2009 proto[name] = this[name]; 2010 }, this); 2011 return proto; 2012 }; 2013 2014 // Export Events as BackboneEvents depending on current context 2015 2016 this.BackboneEvents = Events; 2017 2018 }).call(evts); 2019 2020 this.events = evts.BackboneEvents.mixin({}); 2021 2022}).call(this); 2023 2024/* 2025* This is the root of the CargoEditor. Use this to define global public functions and methods. 2026*/ 2027 2028var _core = this, 2029 2030 // array to keep track of all EditorInstances. 2031 editorInstances = [], 2032 activeEditor; 2033 2034/** 2035 * Public method to create a private EditorInstance 2036 * 2037 * @method 2038 * @param {Element} element The element (Not a jQuery object!) that contains the HTML that needs to be edited. 2039 * 2040 * @memberOf CargoEditor 2041 */ 2042this.addEditor = function(element) { 2043 2044 var editorInstance = new Editor(element); 2045 2046 if (editorInstance !== false) { 2047 2048 editorInstances.push(editorInstance); 2049 2050 return editorInstance; 2051 2052 } 2053 2054 // if no editor Instance is returned: return false. 2055 return false; 2056} 2057 2058this.events.on('editor-destroyed', function(editor){ 2059 2060 if(editor === activeEditor) { 2061 activeEditor = undefined; 2062 } 2063 2064 var index = editorInstances.indexOf(editor); 2065 2066 if(index !== -1){ 2067 2068 editorInstances.splice(index, 1); 2069 editor = null; 2070 2071 } 2072 2073}); 2074 2075this.getActiveRange = function(){ 2076 2077 var selection = _core.rangy.getSelection(); 2078 2079 if(selection.rangeCount > 0){ 2080 return selection.getRangeAt(0); 2081 } 2082 2083 return undefined; 2084 2085} 2086 2087this.plugins = {}; 2088 2089/** 2090 * Adds a plugin to CargoEditor. Sets up a namespace for the plugin in CargoEditor.plugins and calls the plugin function. 2091 * 2092 * @method 2093 * @param {String} id The name of the plugin. Registers a namespace in CargoEditor.plugins.id 2094 * @param {Function} plugin The plugin. The this keyword refers to the plugin's namespace. The first parameter of the plugin is the CargoEditor object 2095 * 2096 * @memberOf CargoEditor 2097 */ 2098 2099this.addPlugin = function(name, plugin) { 2100 2101 if(typeof plugin === 'function') { 2102 2103 // extend plugin 2104 /*plugin.prototype.events = []; 2105 plugin.prototype.addEvent = function(){ 2106 this.events.push('test'); 2107 }*/ 2108 2109 plugin.prototype.events = _core.events; 2110 plugin.prototype.helpers = helpers; 2111 2112 plugin.prototype.pluginNS = _core.plugins; 2113 2114 // Init plugin 2115 this.plugins[name] = new plugin(this); 2116 2117 /** 2118 * Fires after a plugin has been added. Returns the plugin's name and namespace 2119 * 2120 * @event CargoEditor#plugin-initialized 2121 * @property {String} name The plugin's name 2122 * @property {Object} namespace The namespace the plugin has access to. Located at CargoEditor.plugins[name] 2123 */ 2124 2125 _core.events.trigger('plugin-initialized', name, this.plugins[name]); 2126 } 2127 2128}; 2129 2130/** 2131 * Public method to retrieve a list of active EditorInstances 2132 * 2133 * @returns {Array} An array of all editor instances. 2134 * 2135 * @memberOf CargoEditor 2136 */ 2137 2138this.getEditorInstances = function() { 2139 2140 return editorInstances; 2141 2142}; 2143 2144/** 2145 * Returns the EditorInstance that's currently focused 2146 * @method 2147 * @returns {CargoEditor.EditorInstance|undefined} The active instance or undefined if none of the editors is active. 2148 * @memberOf CargoEditor 2149 */ 2150 2151this.getActiveEditor = function() { 2152 2153 return activeEditor; 2154 2155}; 2156 2157document.addEventListener('keydown', function(event) { 2158 if (!event) event = window.event; 2159 2160 if(event.keyCode === 46 || event.keyCode === 8){ 2161 2162 event.preventDefault(); 2163 2164 _core.events.trigger('global-delete-pressed', event); 2165 2166 } 2167 2168}, false); 2169 2170var helpers = { 2171 2172 /** 2173 * [extend description] 2174 * @param {Object} obj [The original object] 2175 * @param {Object} source [The object that's going to be merged into the original object] 2176 * @return {Object} [The original object including the merged] 2177 */ 2178 extend: function(obj, source) { 2179 2180 if (Object.keys) { 2181 var keys = Object.keys(source); 2182 for (var i = 0, il = keys.length; i < il; i++) { 2183 var prop = keys[i]; 2184 Object.defineProperty(obj, prop, Object.getOwnPropertyDescriptor(source, prop)); 2185 } 2186 } else { 2187 var safeHasOwnProperty = {}.hasOwnProperty; 2188 for (var prop in source) { 2189 if (safeHasOwnProperty.call(source, prop)) { 2190 obj[prop] = source[prop]; 2191 } 2192 } 2193 } 2194 2195 return obj; 2196 2197 }, 2198 2199 loadScript: function(src, completeCallback){ 2200 var script = document.createElement('script'); 2201 script.async = false;
2202 script.src = src; 2203 script.onload = completeCallback; 2204 document.head.appendChild(script); 2205 }, 2206 2207 hasClass: function(el, name) { 2208 if(el === undefined || name === undefined) { 2209 return; 2210 } 2211 2212 return new RegExp('(\\s|^)' + name + '(\\s|$)').test(el.className); 2213 }, 2214 2215 addClass: function(el, name) { 2216 if(el === undefined || name === undefined) { 2217 return; 2218 } 2219 2220 if (!helpers.hasClass(el, name)) { 2221 el.className += (el.className ? ' ' : '') + name; 2222 } 2223 }, 2224 2225 removeClass: function(el, name) { 2226 if(el === undefined || name === undefined) { 2227 return; 2228 } 2229 2230 if (helpers.hasClass(el, name)) { 2231 el.className = el.className.replace(new RegExp('(\\s|^)' + name + '(\\s|$)'), ' ').replace(/^\s+|\s+$/g, ''); 2232 } 2233 }, 2234 2235 /** 2236 * @method 2237 * @param {Node} node The reference node. 2238 * @returns {Node} The reference node's next sibling. 2239 */ 2240 2241 nextNode: function(node){ 2242 2243 if (node.hasChildNodes()) { 2244 2245 return node.firstChild; 2246 2247 } else { 2248 2249 while (node && !node.nextSibling) { 2250 node = node.parentNode; 2251 } 2252 2253 if (!node) { 2254 return null; 2255 } 2256 2257 return node.nextSibling; 2258 2259 } 2260 2261 }, 2262 2263 /** 2264 * @method 2265 * @param {Node} node The node that needs to be unwrapped. 2266 * @param {Boolean} [removeWrap=true] Delete the empty wrap 2267 */ 2268 2269 unwrap: function(node, removeWrap){ 2270 2271 var parent = node.parentNode; 2272 2273 while (node.childNodes.length > 0) { 2274 parent.insertBefore(node.childNodes[0], node); 2275 } 2276 2277 if (parent !== null && removeWrap !== false) { 2278 parent.removeChild(node); 2279 } 2280 2281 }, 2282 2283 /** 2284 * @method 2285 * @param {Node} node The node that's being wrapped. 2286 * @param {String} wrapNodeName The nodename of the element that's going to wrap the node. 2287 * @param {Object} [attributes] Optional Object containing attributes added to every wrapper. example: 2288 * <pre><code>{ 2289 * 'data-foo': 'bar', 2290 * 'class': 'big-text' 2291 * }</code></pre> 2292 */ 2293 2294 wrap: function(node, wrapNodeName, attributes){ 2295 2296 if(node !== null){ 2297 2298 var parent = node.parentNode, 2299 wrap = document.createElement(wrapNodeName); 2300 2301 if(typeof attributes === "object"){ 2302 for(key in attributes){ 2303 wrap.setAttribute(key, attributes[key]); 2304 } 2305 } 2306 2307 parent.insertBefore(wrap, node); 2308 wrap.appendChild(node); 2309 2310 if(node.isSaveable() === true){ 2311 wrap.setSaveable(true); 2312 } 2313 2314 } 2315 2316 }, 2317 2318 /** 2319 * Recursively loops through all parent elements for a node. 2320 * 2321 * @method 2322 * @param {Node} node The node that's gonna be traversed. 2323 * @param {Function} callback The callback that fires for every node found. The child is the first (and only) parameter of the callback. 2324 * @param {Node} ceiling If this is set the function won't traverse past the given node. 2325 */ 2326 2327 getAllParents: function(node, callback, ceiling) { 2328 2329 // recursively finds all parent nodes and fires a callback for every node found 2330 while (node.parentNode !== null && node.parentNode !== ceiling) { 2331 2332 if (callback(node.parentNode) === false) { 2333 return false; 2334 } 2335 2336 node = node.parentNode; 2337 2338 } 2339 2340 }, 2341 2342 getAllSiblings: function(startNode, reverse) { 2343 2344 if(startNode !== undefined){ 2345 2346 var result = []; 2347 2348 while((startNode = (reverse === true ? startNode.previousSibling : startNode.nextSibling)) !== null){ 2349 result.push(startNode); 2350 } 2351 2352 return result; 2353 } 2354 2355 return []; 2356 }, 2357 2358 /** 2359 * Recursively loops through all children of the given node and fires a callback for every node found. 2360 * 2361 * @method 2362 * @param {Node} node The node that's gonna be traversed. 2363 * @param {Function} callback The callback that fires for every node found. The child is the first (and only) parameter of the callback. 2364 * Return false to cancel the loop and return "continue" to skip the current node. 2365 * @param {Boolean} [reverse] [If true the loop starts at the last child node of every parent it encounters and reverses to the first. False by default.] 2366 */ 2367 2368 getAllDescendants: function(node, callback, reverse) { 2369 2370 // recursively finds all childnodes and fires a callback for every node found 2371 for (var i = (reverse ? node.childNodes.length - 1 : 0); (reverse ? (i >= 0) : (i < node.childNodes.length)); (reverse ? i-- : i++)) { 2372 2373 var child = node.childNodes[i], 2374 result = callback(child); 2375 2376 if(result === false){ 2377 return false;
2378 } 2379 2380 // if "continue" is being returned from the callback we don't go down 2381 // the subtree of the current child. 2382 if(result !== "continue"){ 2383 // recursively go down the tree. 2384 if(this.getAllDescendants(child, callback, reverse) === false){ 2385 return false; 2386 } 2387 } 2388 2389 } 2390 2391 }, 2392 2393 /** 2394 * Returns all nodes that are currently in the first Range of the current Selection. 2395 * 2396 * @method 2397 * @param {ElementNode} rootElement If this node is set the function will select all nodes between the selection and this element. 2398 * @returns {Object} An object that contains the current selection and an array of selected Nodes. 2399 * <pre><code> { 2400 * 'nodes': [], 2401 * 'selection': {} 2402 * }</code></pre> 2403 * 2404 * @memberOf CargoEditor.helpers 2405 */ 2406 2407 getCurrentSelectionTree: function(rootElement) { 2408 2409 var selectedNodes = [], 2410 selection = getSelection(), 2411 range = _core.getActiveRange(), 2412 results = { 2413 nodes: [], 2414 selection: {} 2415 }; 2416 2417 results.selection = selection; 2418 2419 if (range !== false) { 2420 // selected nodes without parents. 2421 selectedNodes = range.getNodes(); 2422 2423 // if a rootElement is set, traverse up and select all parentNodes. 2424 if (typeof rootElement === "object" && selectedNodes.indexOf(rootElement) === -1) { 2425 2426 for (var i = 0; i < selectedNodes.length; i++) { 2427 2428 var node = selectedNodes[i]; 2429 2430 while (node !== null && node.parentNode !== rootElement) { 2431 node = node.parentNode; 2432 2433 if (selectedNodes.indexOf(node) < 0 && node !== null) { 2434 selectedNodes.push(node); 2435 } 2436 2437 } 2438 2439 } 2440 2441 } 2442 2443 } 2444 2445 results.nodes = selectedNodes; 2446 2447 return results; 2448 }, 2449 2450 getEditableNodesInRange: function(){ 2451 2452 2453 2454 }, 2455 2456 /** 2457 * Returns all nodes in the current Range. Also includes the partially selected nodes. 2458 * 2459 * @method 2460 * @param {Range} [range={@link CargoEditor.getActiveRange}] The range object we're getting the nodes from. 2461 * @returns {Array} Array of all nodes. 2462 */ 2463 2464 /*getSelectedNodes: function(range) { 2465 2466 if(range === undefined){ 2467 range = _core.getActiveRange(); 2468 } 2469 2470 if(range === false){ 2471 return []; 2472 } 2473 2474 var node = range.startContainer, 2475 endNode = range.endContainer, 2476 _this = this; 2477 2478 var hasEditableParent; 2479 2480 helpers.getAllParents(endNode, function(parent){ 2481 2482 hasEditableParent = _core.isEditable(parent); 2483 2484 if(hasEditableParent !== undefined){ 2485 return false; 2486 } 2487 2488 }); 2489 2490 if(hasEditableParent === false){ 2491 return []; 2492 } 2493 2494 // a range that is contained within a single node 2495 if (node == endNode) { 2496 return [node]; 2497 } 2498 2499 // Iterate nodes until we hit the end container 2500 var rangeNodes = []; 2501 2502 while (node && node != endNode) { 2503 2504 if(_core.isEditable(node) !== false){ 2505 2506 rangeNodes.push(node); 2507 node = helpers.nextNode(node); 2508 2509 } else { 2510 2511 // don't traverse into the uneditable element. 2512 node = node.nextSibling; 2513 2514 } 2515 2516 } 2517 2518 // Add partially selected nodes at the start of the range 2519 node = range.startContainer; 2520 2521 while (node && node != range.commonAncestorContainer) { 2522 2523 rangeNodes.unshift(node); 2524 node = node.parentNode; 2525 2526 } 2527 2528 return rangeNodes; 2529 2530 },*/ 2531 2532 insertNodeAtCursorPosition: function(event, node){ 2533 2534 if(event === undefined || node === undefined){ 2535 console.warn('Missing parameters'); 2536 return false; 2537 } 2538 2539 window.getSelection().removeAllRanges(); 2540 2541 var newRange = document.createRange(), 2542 start; 2543 2544 if (document.caretPositionFromPoint) { 2545 2546 // Proper way 2547 2548 start = document.caretPositionFromPoint(event.clientX, event.clientY); 2549 newRange.setStart(start.offsetNode, start.offset); 2550 newRange.collapse(); 2551 2552 if(newRange.commonAncestorContainer !== node){ 2553 newRange.insertNode(node); 2554 } 2555 2556 } else if (document.caretRangeFromPoint) { 2557 2558 // Webkit way 2559 2560 start = document.caretRangeFromPoint(event.clientX, event.clientY); 2561 newRange.setStart(start.startContainer, start.startOffset); 2562 newRange.collapse(); 2563 2564 if(newRange.commonAncestorContainer !== node){ 2565 newRange.insertNode(node); 2566 } 2567 2568 } else if (document.body.createTextRange) { 2569 2570 // IE way 2571 2572 newRange = document.body.createTextRange(); 2573 newRange.moveToPoint(event.clientX, event.clientY); 2574 var spanId = "temp_" + ("" + Math.random()).slice(2); 2575 newRange.pasteHTML('<span id="' + spanId + '"> </span>'); 2576 2577 var pastedSpan = document.getElementById(spanId); 2578
2579 if(pastedSpan.parentNode !== node){ 2580 pastedSpan.parentNode.replaceChild(node, span); 2581 } else { 2582 if(pastedSpan.parentNode !== null){ 2583 pastedSpan.parentNode.removeChild(pastedSpan); 2584 } 2585 } 2586 2587 } 2588 2589 window.getSelection().addRange(newRange); 2590 2591 window.getSelection().removeAllRanges(); 2592 2593 }, 2594 2595 getCursorCoordinates: function() { 2596 2597 var sel = document.selection, 2598 range, 2599 rects, 2600 rect, 2601 x = 0, 2602 y = 0, 2603 h = 0; 2604 2605 if (sel) { 2606 2607 if (sel.type != "Control") { 2608 2609 range = sel.createRange(); 2610 range.collapse(true); 2611 2612 x = range.boundingLeft; 2613 y = range.boundingTop; 2614 h = range.boundingHeight; 2615 2616 } 2617 2618 } else if (window.getSelection) { 2619 2620 sel = window.getSelection(); 2621 2622 if (sel.rangeCount) { 2623 2624 range = sel.getRangeAt(0).cloneRange(); 2625 2626 if (range.getClientRects) { 2627 2628 if (!range.collapsed) { 2629 var startRange = range.cloneRange(); 2630 startRange.collapse(true); 2631 var selTop = startRange.getClientRects()[0].top; 2632 startRange.detach(); 2633 var endRange = range.cloneRange(); 2634 endRange.collapse(false); 2635 h = endRange.getClientRects()[0].bottom - selTop; 2636 endRange.detach(); 2637 } 2638 2639 range.collapse(true); 2640 rects = range.getClientRects(); 2641 2642 if (rects.length > 0) { 2643 rect = range.getClientRects()[0]; 2644 } 2645 2646 x = rect.left; 2647 y = rect.top; 2648 2649 } 2650 } 2651 } 2652 2653 return { x: x, y: y, h: h }; 2654 }, 2655 2656 findClosestTextNode: function(referenceNode, reverse) { 2657 2658 var siblings, i, 2659 _self = this, 2660 isValidTextNode = function(node){ 2661 2662 if(node.nodeType === Node.TEXT_NODE && node.nodeValue.search(/\S/g) !== -1){ 2663 return true; 2664 } 2665 2666 return false; 2667 }; 2668 2669 var result; 2670 2671 while(referenceNode !== null && referenceNode !== document.body){ 2672 2673 if(referenceNode.contentEditable === "false"){ 2674 continue; 2675 } 2676 2677 siblings = helpers.getAllSiblings(referenceNode, reverse); 2678 2679 for( i = 0; i < siblings.length; i++){ 2680 2681 if(siblings[i].contentEditable === "false"){ 2682 continue; 2683 } 2684 2685 if(isValidTextNode(siblings[i])){ 2686 result = siblings[i]; 2687 break; 2688 } 2689 2690 if(siblings[i].childNodes.length > 0){ 2691 2692 helpers.getAllDescendants(siblings[i], function(child){ 2693 2694 if(isValidTextNode(child)){ 2695 result = child; 2696 return false; 2697 } 2698 }, reverse); 2699 2700 if(result !== undefined){ 2701 break; 2702 } 2703 2704 } 2705 2706 } 2707 2708 if(result !== undefined){ 2709 break; 2710 } 2711 2712 referenceNode = referenceNode.parentNode; 2713 } 2714 2715 return result; 2716 2717 }, 2718 2719 loadJSON: function(url, callback, async){ 2720 2721 var request = new XMLHttpRequest(); 2722 2723 if(async !== false){ 2724 async = true; 2725 } 2726 2727 request.open('GET', url, async); 2728 2729 request.onload = function() { 2730 if (request.status >= 200 && request.status < 400){ 2731 // Success! 2732 2733 if(typeof callback === "function") { 2734 callback.call(this, JSON.parse(request.responseText)); 2735 } 2736 } else { 2737 // We reached our target server, but it returned an error 2738 2739 } 2740 }; 2741 2742 request.onerror = function() { 2743 // There was a connection error of some sort 2744 }; 2745 2746 request.send(); 2747 }, 2748 2749 /** 2750 * Might wanna do this with a docfrag? 2751 */ 2752 2753 stringToHTMLNodes: function(html) { 2754 var tmp = document.createElement('body'), 2755 result = [], 2756 child; 2757 2758 tmp.innerHTML = html; 2759 2760 while (child = tmp.lastChild) { 2761 result.push(child.cloneNode(true)); 2762 tmp.removeChild(child); 2763 } 2764 2765 return result; 2766 }, 2767 2768 warn: function(arg) { 2769 // TODO: make this failsafe. 2770 console.warn(arg); 2771 2772 }, 2773 2774 log: function(arg) { 2775 // TODO: make this failsafe. 2776 console.warn(arg); 2777 2778 }, 2779 2780 selectNode: function(node) { 2781 var newRange = document.createRange(), 2782 selection = getSelection(); 2783 2784 newRange.selectNode(node); 2785 2786 selection.removeAllRanges(); 2787 selection.addRange(newRange); 2788 }, 2789 2790 logDocumentFragment: function(frag){ 2791 var div = document.createElement('div'); 2792 div.appendChild(frag.cloneNode(true)); 2793 console.log(div); 2794 }, 2795 2796 whiteSpaceHelpers: { 2797 /** 2798 * https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Whitespace_in_the_DOM 2799 * 2800 * Throughout, whitespace is defined as one of the characters 2801 * "\t" TAB \u0009 2802 * "\n" LF \u000A 2803 * "\r" CR \u000D 2804 * " " SPC \u0020 2805 * 2806 * This does not use Javascript's "\s" because that includes non-breaking 2807 * spaces (and also some other characters). 2808 */ 2809 2810 2811 /** 2812 * Determine whether a node's text content is entirely whitespace. 2813 * 2814 * @param nod A node implementing the |CharacterData| interface (i.e., 2815 * a |Text|, |Comment|, or |CDATASection| node 2816 * @return True if all of the text content of |nod| is whitespace, 2817 * otherwise false. 2818 */ 2819 is_all_ws: function( nod ) { 2820 // Use ECMA-262 Edition 3 String and RegExp features 2821 return !(/[^\t\n\r ]/.test(nod.textContent)); 2822 }, 2823 2824 2825 /** 2826 * Determine if a node should be ignored by the iterator functions. 2827 * 2828 * @param nod An object implementing the DOM1 |Node| interface. 2829 * @return true if the node is: 2830 * 1) A |Text| node that is all whitespace 2831 * 2) A |Comment| node 2832 * and otherwise false. 2833 */ 2834 2835 is_ignorable: function( nod ) { 2836 return ( nod.nodeType == 8) || // A comment node 2837 ( (nod.nodeType == 3) && is_all_ws(nod) ); // a text node, all ws 2838 }, 2839 2840 /** 2841 * Version of |previousSibling| that skips nodes that are entirely 2842 * whitespace or comments. (Normally |previousSibling| is a property 2843 * of all DOM nodes that gives the sibling node, the node that is 2844 * a child of the same parent, that occurs immediately before the 2845 * reference node.) 2846 * 2847 * @param sib The reference node. 2848 * @return Either: 2849 * 1) The closest previous sibling to |sib| that is not 2850 * ignorable according to |is_ignorable|, or 2851 * 2) null if no such node exists. 2852 */ 2853 node_before: function( sib ) { 2854 while ((sib = sib.previousSibling)) { 2855 if (!is_ignorable(sib)) return sib; 2856 } 2857 return null; 2858 }, 2859 2860 /** 2861 * Version of |nextSibling| that skips nodes that are entirely 2862 * whitespace or comments. 2863 * 2864 * @param sib The reference node. 2865 * @return Either: 2866 * 1) The closest next sibling to |sib| that is not 2867 * ignorable according to |is_ignorable|, or 2868 * 2) null if no such node exists. 2869 */ 2870 node_after: function( sib ) { 2871 while ((sib = sib.nextSibling)) { 2872 if (!is_ignorable(sib)) return sib; 2873 } 2874 return null; 2875 }, 2876 2877 /** 2878 * Version of |lastChild| that skips nodes that are entirely 2879 * whitespace or comments. (Normally |lastChild| is a property 2880 * of all DOM nodes that gives the last of the nodes contained 2881 * directly in the reference node.) 2882 * 2883 * @param sib The reference node. 2884 * @return Either: 2885 * 1) The last child of |sib| that is not 2886 * ignorable according to |is_ignorable|, or 2887 * 2) null if no such node exists. 2888 */ 2889 last_child: function( par ) { 2890 var res=par.lastChild; 2891 while (res) { 2892 if (!is_ignorable(res)) return res; 2893 res = res.previousSibling; 2894 } 2895 return null; 2896 }, 2897 2898 /** 2899 * Version of |firstChild| that skips nodes that are entirely 2900 * whitespace and comments. 2901 * 2902 * @param sib The reference node. 2903 * @return Either: 2904 * 1) The first child of |sib| that is not 2905 * ignorable according to |is_ignorable|, or 2906 * 2) null if no such node exists. 2907 */ 2908 first_child: function( par ) { 2909 var res=par.firstChild; 2910 while (res) { 2911 if (!is_ignorable(res)) return res; 2912 res = res.nextSibling; 2913 } 2914 return null; 2915 }, 2916 2917 /** 2918 * Version of |data| that doesn't include whitespace at the beginning 2919 * and end and normalizes all whitespace to a single space. (Normally 2920 * |data| is a property of text nodes that gives the text of the node.) 2921 * 2922 * @param txt The text node whose data should be returned 2923 * @return A string giving the contents of the text node with 2924 * whitespace collapsed. 2925 */ 2926 data_of: function( txt ) { 2927 var data = txt.textContent; 2928 // Use ECMA-262 Edition 3 String and RegExp features 2929 data = data.replace(/[\t\n\r ]+/g, " "); 2930 if (data.charAt(0) == " ") 2931 data = data.substring(1, data.length); 2932 if (data.charAt(data.length - 1) == " ") 2933 data = data.substring(0, data.length - 1); 2934 return data; 2935 } 2936 } 2937} 2938 2939/** 2940 * Collection of useful helpers. 2941 * @name helpers 2942 * @namespace 2943 * @public 2944 * @memberOf CargoEditor 2945 * @author Aart Röst 2946 */ 2947 2948this.helpers = helpers; 2949 2950 2951/** 2952 * Rangy, a cross-browser JavaScript range and selection library 2953 * https://github.com/timdown/rangy 2954 * 2955 * Copyright 2015, Tim Down 2956 * Licensed under the MIT license. 2957 * Version: 1.3.0-beta.1
vendor: 1,689 bytes, lines 2958-2961
2958 * Build date: 12 February 2015 2959 */ 2960 2961;!function(e,t){ t.rangy=e() }(function(){function e(e,t){var n=typeof e[t];return n==v||!(n!=R||!e[t])||"unknown"==n}function t(e,t){return!(typeof e[t]!=R||!e[t])}function n(e,t){return typeof e[t]!=C}function r(e){return function(t,n){for(var r=n.length;r--;)if(!e(t,n[r]))return!1;return!0}}function o(e){return e&&w(e,y)&&T(e,S)}function i(e){return t(e,"body")?e.body:e.getElementsByTagName("body")[0]}function a(t){typeof console!=C&&e(console,"log")&&console.log(t)}function s(e,t){x&&t?alert(e):a(e)}function c(e){b.initialized=!0,b.supported=!1,s("Rangy is not supported in this environment. Reason: "+e,b.config.alertOnFail)}function d(e){s("Rangy warning: "+e,b.config.alertOnWarn)}function f(e){return e.message||e.description||String(e)}function u(){if(x&&!b.initialized){var t,n=!1,r=!1;e(document,"createRange")&&(t=document.createRange(),w(t,E)&&T(t,N)&&(n=!0));var s=i(document);if(!s||"body"!=s.nodeName.toLowerCase())return void c("No body element found");if(s&&e(s,"createTextRange")&&(t=s.createTextRange(),o(t)&&(r=!0)),!n&&!r)return void c("Neither Range nor TextRange are available");b.initialized=!0,b.features={implementsDomRange:n,implementsTextRange:r};var d,u;for(var l in D)(d=D[l])instanceof h&&d.init(d,b);for(var g=0,p=B.length;p>g;++g)try{B[g](b)}catch(m){u="Rangy init listener threw an exception. Continuing. Detail: "+f(m),a(u)}}}function l(e){e=e||window,u();for(var t=0,n=H.length;n>t;++t)H[t](e)}function h(e,t,n){this.name=e,this.dependencies=t,this.initialized=!1,this.supported=!1,this.initializer=n}function g(e,t,n){var r=new h(e,t,function(t){if(!t.initialized){t.initialized=!0;try{n(b,t),t.supported=!0}
vendor: 6,550 bytes, line 2961
2961catch(r){var o="Module '"+e+"' failed to load: "+f(r);a(o),r.stack&&a(r.stack)}}});return D[e]=r,r}function p(){}function m(){}var R="object",v="function",C="undefined",N=["startContainer","startOffset","endContainer","endOffset","collapsed","commonAncestorContainer"],E=["setStart","setStartBefore","setStartAfter","setEnd","setEndBefore","setEndAfter","collapse","selectNode","selectNodeContents","compareBoundaryPoints","deleteContents","extractContents","cloneContents","insertNode","surroundContents","cloneRange","toString","detach"],S=["boundingHeight","boundingLeft","boundingTop","boundingWidth","htmlText","text"],y=["collapse","compareEndPoints","duplicate","moveToElementText","parentElement","select","setEndPoint","getBoundingClientRect"],w=r(e),O=r(t),T=r(n),_=[].forEach?function(e,t){e.forEach(t)}:function(e,t){for(var n=0,r=e.length;r>n;++n)t(e[n],n)},D={},x=typeof window!=C&&typeof document!=C,A={isHostMethod:e,isHostObject:t,isHostProperty:n,areHostMethods:w,areHostObjects:O,areHostProperties:T,isTextRange:o,getBody:i,forEach:_},b={version:"1.3.0-beta.1",initialized:!1,isBrowser:x,supported:!0,util:A,features:{},modules:D,config:{alertOnFail:!0,alertOnWarn:!1,preferTextRange:!1,autoInitialize:typeof rangyAutoInitialize==C?!0:rangyAutoInitialize}};b.fail=c,b.warn=d;var P;({}).hasOwnProperty?(A.extend=P=function(e,t,n){var r,o;for(var i in t)t.hasOwnProperty(i)&&(r=e[i],o=t[i],n&&null!==r&&"object"==typeof r&&null!==o&&"object"==typeof o&&P(r,o,!0),e[i]=o);return t.hasOwnProperty("toString")&&(e.toString=t.toString),e},A.createOptions=function(e,t){var n={};return P(n,t),e&&P(n,e),n}):c("hasOwnProperty not supported"),x||c("Rangy can only run in a browser"),function(){var e;if(x){var t=document.createElement("div");t.appendChild(document.createElement("span"));var n=[].slice;try{1==n.call(t.childNodes,0)[0].nodeType&&(e=function(e){return n.call(e,0)})}catch(r){}}e||(e=function(e){for(var t=[],n=0,r=e.length;r>n;++n)t[n]=e[n];return t}),A.toArray=e}();var I;x&&(e(document,"addEventListener")?I=function(e,t,n){e.addEventListener(t,n,!1)}:e(document,"attachEvent")?I=function(e,t,n){e.attachEvent("on"+t,n)}:c("Document does not have required addEventListener or attachEvent method"),A.addListener=I);var B=[];b.init=u,b.addInitListener=function(e){b.initialized?e(b):B.push(e)};var H=[];b.addShimListener=function(e){H.push(e)},x&&(b.shim=b.createMissingNativeApi=l),h.prototype={init:function(){for(var e,t,n=this.dependencies||[],r=0,o=n.length;o>r;++r){if(t=n[r],e=D[t],!(e&&e instanceof h))throw new Error("required module '"+t+"' not found");if(e.init(),!e.supported)throw new Error("required module '"+t+"' not supported")}this.initializer(this)},fail:function(e){throw this.initialized=!0,this.supported=!1,new Error("Module '"+this.name+"' failed to load: "+e)},warn:function(e){b.warn("Module "+this.name+": "+e)},deprecationNotice:function(e,t){b.warn("DEPRECATED: "+e+" in module "+this.name+"is deprecated. Please use "+t+" instead")},createError:function(e){return new Error("Error in Rangy "+this.name+" module: "+e)}},b.createModule=function(e){var t,n;2==arguments.length?(t=arguments[1],n=[]):(t=arguments[2],n=arguments[1]);var r=g(e,n,t);b.initialized&&b.supported&&r.init()},b.createCoreModule=function(e,t,n){g(e,t,n)},b.RangePrototype=p,b.rangePrototype=new p,b.selectionPrototype=new m,b.createCoreModule("DomUtil",[],function(e,t){function n(e){var t;return typeof e.namespaceURI==x||null===(t=e.namespaceURI)||"http://www.w3.org/1999/xhtml"==t}function r(e){var t=e.parentNode;return 1==t.nodeType?t:null}function o(e){for(var t=0;e=e.previousSibling;)++t;return t}function i(e){switch(e.nodeType){case 7:case 10:return 0;case 3:case 8:return e.length;default:return e.childNodes.length}}function a(e,t){var n,r=[];for(n=e;n;n=n.parentNode)r.push(n);for(n=t;n;n=n.parentNode)if(I(r,n))return n;return null}function s(e,t,n){for(var r=n?t:t.parentNode;r;){if(r===e)return!0;r=r.parentNode}return!1}function c(e,t){return s(e,t,!0)}function d(e,t,n){for(var r,o=n?e:e.parentNode;o;){if(r=o.parentNode,r===t)return o;o=r}return null}function f(e){var t=e.nodeType;return 3==t||4==t||8==t}function u(e){if(!e)return!1;var t=e.nodeType;return 3==t||8==t}function l(e,t){var n=t.nextSibling,r=t.parentNode;return n?r.insertBefore(e,n):r.appendChild(e),e}function h(e,t,n){var r=e.cloneNode(!1);if(r.deleteData(0,t),e.deleteData(t,e.length-t),l(r,e),n)for(var i,a=0;i=n[a++];)i.node==e&&i.offset>t?(i.node=r,i.offset-=t):i.node==e.parentNode&&i.offset>o(e)&&++i.offset;return r}function g(e){if(9==e.nodeType)return e;if(typeof e.ownerDocument!=x)return e.ownerDocument;if(typeof e.document!=x)return e.document;if(e.parentNode)return g(e.parentNode);throw t.createError("getDocument: no document found for node")}function p(e){var n=g(e);if(typeof n.defaultView!=x)return n.defaultView;if(typeof n.parentWindow!=x)return n.parentWindow;throw t.createError("Cannot get a window object for node")}function m(e){if(typeof e.contentDocument!=x)return e.contentDocument;if(typeof e.contentWindow!=x)return e.contentWindow.document;throw t.createError("getIframeDocument: No Document object found for iframe element")}function R(e){if(typeof e.contentWindow!=x)return e.contentWindow;if(typeof e.contentDocument!=x)return e.contentDocument.defaultView;throw t.createError("getIframeWindow: No Window object found for iframe element")}function v(e){return e&&A.isHostMethod(e,"setTimeout")&&A.isHostObject(e,"document")}function C(e,t,n){var r;if(e?A.isHostProperty(e,"nodeType")?r=1==e.nodeType&&"iframe"==e.tagName.toLowerCase()?m(e):g(e):v(e)&&(r=e.document):r=document,!r)throw t.createError(n+"(): Parameter must be a Window object or DOM node");return r}function N(e){for(var t;t=e.parentNode;)e=t;return e}function E(e,n,r,i){var s,c,f,u,l;if(e==r)return n===i?0:i>n?-1:1;if(s=d(r,e,!0))return n<=o(s)?-1:1;if(s=d(e,r,!0))return o(s)<i?-1:1;if(c=a(e,r),!c)throw new Error("comparePoints error: nodes have no common ancestor");if(f=e===c?c:d(e,c,!0),u=r===c?c:d(r,c,!0),f===u)throw t.createError("comparePoints got to case 4 and childA and childB are the same!");for(l=c.firstChild;l;){if(l===f)return-1;if(l===u)return 1;l=l.nextSibling}}function S(e){var t;try{return t=e.parentNode,!1}catch(n){return!0}}function y(e){if(!e)return"[No node]";if(B&&S(e))return"[Broken node]";if(f(e))return'"'+e.data+'"';if(1==e.nodeType){var t=e.id?' id="'+e.id+'"':"";return"<"+e.nodeName+t+">[index:"+o(e)+",length:"+e.childNodes.length+"]["+(e.innerHTML||"[innerHTML not supported]").slice(0,25)+"]"}
vendor: 2,367 bytes, line 2961
2961return e.nodeName}function w(e){for(var t,n=g(e).createDocumentFragment();t=e.firstChild;)n.appendChild(t);return n}function O(e){this.root=e,this._next=e}function T(e){return new O(e)}function _(e,t){this.node=e,this.offset=t}function D(e){this.code=this[e],this.codeName=e,this.message="DOMException: "+this.codeName}var x="undefined",A=e.util;A.areHostMethods(document,["createDocumentFragment","createElement","createTextNode"])||t.fail("document missing a Node creation method"),A.isHostMethod(document,"getElementsByTagName")||t.fail("document missing getElementsByTagName method");var b=document.createElement("div");A.areHostMethods(b,["insertBefore","appendChild","cloneNode"]||!A.areHostObjects(b,["previousSibling","nextSibling","childNodes","parentNode"]))||t.fail("Incomplete Element implementation"),A.isHostProperty(b,"innerHTML")||t.fail("Element is missing innerHTML property");var P=document.createTextNode("test");A.areHostMethods(P,["splitText","deleteData","insertData","appendData","cloneNode"]||!A.areHostObjects(b,["previousSibling","nextSibling","childNodes","parentNode"])||!A.areHostProperties(P,["data"]))||t.fail("Incomplete Text Node implementation");var I=function(e,t){for(var n=e.length;n--;)if(e[n]===t)return!0;return!1},B=!1;!function(){var t=document.createElement("b");t.innerHTML="1";var n=t.firstChild;t.innerHTML="<br />",B=S(n),e.features.crashyTextNodes=B}();var H;typeof window.getComputedStyle!=x?H=function(e,t){return p(e).getComputedStyle(e,null)[t]}:typeof document.documentElement.currentStyle!=x?H=function(e,t){return e.currentStyle[t]}:t.fail("No means of obtaining computed style properties found"),O.prototype={_current:null,hasNext:function(){return!!this._next},next:function(){var e,t,n=this._current=this._next;if(this._current)if(e=n.firstChild)this._next=e;else{for(t=null;n!==this.root&&!(t=n.nextSibling);)n=n.parentNode;this._next=t}return this._current},detach:function(){this._current=this._next=this.root=null}},_.prototype={equals:function(e){return!!e&&this.node===e.node&&this.offset==e.offset},inspect:function(){return"[DomPosition("+y(this.node)+":"+this.offset+")]"},toString:function(){return this.inspect()}},D.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_ER
vendor: 22,513 bytes, lines 2961-2962
2961R:24},D.prototype.toString=function(){return this.message},e.dom={arrayContains:I,isHtmlNamespace:n,parentElement:r,getNodeIndex:o,getNodeLength:i,getCommonAncestor:a,isAncestorOf:s,isOrIsAncestorOf:c,getClosestAncestorIn:d,isCharacterDataNode:f,isTextOrCommentNode:u,insertAfter:l,splitDataNode:h,getDocument:g,getWindow:p,getIframeWindow:R,getIframeDocument:m,getBody:A.getBody,isWindow:v,getContentDocument:C,getRootContainer:N,comparePoints:E,isBrokenNode:S,inspectNode:y,getComputedStyleProperty:H,fragmentFromNodeChildren:w,createIterator:T,DomPosition:_},e.DOMException=D}),b.createCoreModule("DomRange",["DomUtil"],function(e){function t(e,t){return 3!=e.nodeType&&(F(e,t.startContainer)||F(e,t.endContainer))}function n(e){return e.document||j(e.startContainer)}function r(e){return new M(e.parentNode,k(e))}function o(e){return new M(e.parentNode,k(e)+1)}function i(e,t,n){var r=11==e.nodeType?e.firstChild:e;return W(t)?n==t.length?B.insertAfter(e,t):t.parentNode.insertBefore(e,0==n?t:U(t,n)):n>=t.childNodes.length?t.appendChild(e):t.insertBefore(e,t.childNodes[n]),r}function a(e,t,r){if(w(e),w(t),n(t)!=n(e))throw new L("WRONG_DOCUMENT_ERR");var o=z(e.startContainer,e.startOffset,t.endContainer,t.endOffset),i=z(e.endContainer,e.endOffset,t.startContainer,t.startOffset);return r?0>=o&&i>=0:0>o&&i>0}function s(e){for(var t,r,o,i=n(e.range).createDocumentFragment();r=e.next();){if(t=e.isPartiallySelectedSubtree(),r=r.cloneNode(!t),t&&(o=e.getSubtreeIterator(),r.appendChild(s(o)),o.detach()),10==r.nodeType)throw new L("HIERARCHY_REQUEST_ERR");i.appendChild(r)}return i}function c(e,t,n){var r,o;n=n||{stop:!1};for(var i,a;i=e.next();)if(e.isPartiallySelectedSubtree()){if(t(i)===!1)return void(n.stop=!0);if(a=e.getSubtreeIterator(),c(a,t,n),a.detach(),n.stop)return}else for(r=B.createIterator(i);o=r.next();)if(t(o)===!1)return void(n.stop=!0)}function d(e){for(var t;e.next();)e.isPartiallySelectedSubtree()?(t=e.getSubtreeIterator(),d(t),t.detach()):e.remove()}function f(e){for(var t,r,o=n(e.range).createDocumentFragment();t=e.next();){if(e.isPartiallySelectedSubtree()?(t=t.cloneNode(!1),r=e.getSubtreeIterator(),t.appendChild(f(r)),r.detach()):e.remove(),10==t.nodeType)throw new L("HIERARCHY_REQUEST_ERR");o.appendChild(t)}return o}function u(e,t,n){var r,o=!(!t||!t.length),i=!!n;o&&(r=new RegExp("^("+t.join("|")+")$"));var a=[];return c(new h(e,!1),function(t){if(!(o&&!r.test(t.nodeType)||i&&!n(t))){var s=e.startContainer;if(t!=s||!W(s)||e.startOffset!=s.length){var c=e.endContainer;t==c&&W(c)&&0==e.endOffset||a.push(t)}}}),a}function l(e){var t="undefined"==typeof e.getName?"Range":e.getName();return"["+t+"("+B.inspectNode(e.startContainer)+":"+e.startOffset+", "+B.inspectNode(e.endContainer)+":"+e.endOffset+")]"}function h(e,t){if(this.range=e,this.clonePartiallySelectedTextNodes=t,!e.collapsed){this.sc=e.startContainer,this.so=e.startOffset,this.ec=e.endContainer,this.eo=e.endOffset;var n=e.commonAncestorContainer;this.sc===this.ec&&W(this.sc)?(this.isSingleCharacterDataNode=!0,this._first=this._last=this._next=this.sc):(this._first=this._next=this.sc!==n||W(this.sc)?V(this.sc,n,!0):this.sc.childNodes[this.so],this._last=this.ec!==n||W(this.ec)?V(this.ec,n,!0):this.ec.childNodes[this.eo-1])}}function g(e){return function(t,n){for(var r,o=n?t:t.parentNode;o;){if(r=o.nodeType,Y(e,r))return o;o=o.parentNode}return null}}function p(e,t){if(nt(e,t))throw new L("INVALID_NODE_TYPE_ERR")}function m(e,t){if(!Y(t,e.nodeType))throw new L("INVALID_NODE_TYPE_ERR")}function R(e,t){if(0>t||t>(W(e)?e.length:e.childNodes.length))throw new L("INDEX_SIZE_ERR")}function v(e,t){if(et(e,!0)!==et(t,!0))throw new L("WRONG_DOCUMENT_ERR")}function C(e){if(tt(e,!0))throw new L("NO_MODIFICATION_ALLOWED_ERR")}function N(e,t){if(!e)throw new L(t)}function E(e){return G&&B.isBrokenNode(e)||!Y(Z,e.nodeType)&&!et(e,!0)}function S(e,t){return t<=(W(e)?e.length:e.childNodes.length)}function y(e){return!!e.startContainer&&!!e.endContainer&&!E(e.startContainer)&&!E(e.endContainer)&&S(e.startContainer,e.startOffset)&&S(e.endContainer,e.endOffset)}function w(e){if(!y(e))throw new Error("Range error: Range is no longer valid after DOM mutation ("+e.inspect()+")")}function O(e,t){w(e);var n=e.startContainer,r=e.startOffset,o=e.endContainer,i=e.endOffset,a=n===o;W(o)&&i>0&&i<o.length&&U(o,i,t),W(n)&&r>0&&r<n.length&&(n=U(n,r,t),a?(i-=r,o=n):o==n.parentNode&&i>=k(n)&&i++,r=0),e.setStartAndEnd(n,r,o,i)}function T(e){w(e);var t=e.commonAncestorContainer.parentNode.cloneNode(!1);return t.appendChild(e.cloneContents()),t.innerHTML}function _(e){e.START_TO_START=ct,e.START_TO_END=dt,e.END_TO_END=ft,e.END_TO_START=ut,e.NODE_BEFORE=lt,e.NODE_AFTER=ht,e.NODE_BEFORE_AND_AFTER=gt,e.NODE_INSIDE=pt}function D(e){_(e),_(e.prototype)}function x(e,t){return function(){w(this);var n,r,i=this.startContainer,a=this.startOffset,s=this.commonAncestorContainer,d=new h(this,!0);i!==s&&(n=V(i,s,!0),r=o(n),i=r.node,a=r.offset),c(d,C),d.reset();var f=e(d);return d.detach(),t(this,i,a,i,a),f}}function A(n,i){function a(e,t){return function(n){m(n,X),m(Q(n),Z);var i=(e?r:o)(n);(t?s:c)(this,i.node,i.offset)}}function s(e,t,n){var r=e.endContainer,o=e.endOffset;(t!==e.startContainer||n!==e.startOffset)&&((Q(t)!=Q(r)||1==z(t,n,r,o))&&(r=t,o=n),i(e,t,n,r,o))}function c(e,t,n){var r=e.startContainer,o=e.startOffset;(t!==e.endContainer||n!==e.endOffset)&&((Q(t)!=Q(r)||-1==z(t,n,r,o))&&(r=t,o=n),i(e,r,o,t,n))}var u=function(){};u.prototype=e.rangePrototype,n.prototype=new u,H.extend(n.prototype,{setStart:function(e,t){p(e,!0),R(e,t),s(this,e,t)},setEnd:function(e,t){p(e,!0),R(e,t),c(this,e,t)},setStartAndEnd:function(){var e=arguments,t=e[0],n=e[1],r=t,o=n;switch(e.length){case 3:o=e[2];break;case 4:r=e[2],o=e[3]}i(this,t,n,r,o)},setBoundary:function(e,t,n){this["set"+(n?"Start":"End")](e,t)},setStartBefore:a(!0,!0),setStartAfter:a(!1,!0),setEndBefore:a(!0,!1),setEndAfter:a(!1,!1),collapse:function(e){w(this),e?i(this,this.startContainer,this.startOffset,this.startContainer,this.startOffset):i(this,this.endContainer,this.endOffset,this.endContainer,this.endOffset)},selectNodeContents:function(e){p(e,!0),i(this,e,0,e,q(e))},selectNode:function(e){p(e,!1),m(e,X);var t=r(e),n=o(e);i(this,t.node,t.offset,n.node,n.offset)},extractContents:x(f,i),deleteContents:x(d,i),canSurroundContents:function(){w(this),C(this.startContainer),C(this.endContainer);var e=new h(this,!0),n=e._first&&t(e._first,this)||e._last&&t(e._last,this);return e.detach(),!n},splitBoundaries:function(){O(this)},splitBoundariesPreservingPositions:function(e){O(this,e)},normalizeBoundaries:function(){w(this);var e=this.startContainer,t=this.startOffset,n=this.endContainer,r=this.endOffset,o=function(e){var t=e.nextSibling;t&&t.nodeType==e.nodeType&&(n=e,r=e.length,e.appendData(t.data),t.parentNode.removeChild(t))},a=function(o){var i=o.previousSibling;if(i&&i.nodeType==o.nodeType){e=o;var a=o.length;if(t=i.length,o.insertData(0,i.data),i.parentNode.removeChild(i),e==n)r+=t,n=e;else if(n==o.parentNode){var s=k(o);r==s?(n=o,r=a):r>s&&r--}}},s=!0;if(W(n))n.length==r&&o(n);else{if(r>0){var c=n.childNodes[r-1];c&&W(c)&&o(c)}s=!this.collapsed}if(s){if(W(e))0==t&&a(e);else if(t<e.childNodes.length){var d=e.childNodes[t];d&&W(d)&&a(d)}}else e=n,t=r;i(this,e,t,n,r)},collapseToPoint:function(e,t){p(e,!0),R(e,t),this.setStartAndEnd(e,t)}}),D(n)}function b(e){e.collapsed=e.startContainer===e.endContainer&&e.startOffset===e.endOffset,e.commonAncestorContainer=e.collapsed?e.startContainer:B.getCommonAncestor(e.startContainer,e.endContainer)}function P(e,t,n,r,o){e.startContainer=t,e.startOffset=n,e.endContainer=r,e.endOffset=o,e.document=B.getDocument(t),b(e)}function I(e){this.startContainer=e,this.startOffset=0,this.endContainer=e,this.endOffset=0,this.document=e,b(this)}var B=e.dom,H=e.util,M=B.DomPosition,L=e.DOMException,W=B.isCharacterDataNode,k=B.getNodeIndex,F=B.isOrIsAncestorOf,j=B.getDocument,z=B.comparePoints,U=B.splitDataNode,V=B.getClosestAncestorIn,q=B.getNodeLength,Y=B.arrayContains,Q=B.getRootContainer,G=e.features.crashyTextNodes;h.prototype={_current:null,_next:null,_first:null,_last:null,isSingleCharacterDataNode:!1,reset:function(){this._current=null,this._next=this._first},hasNext:function(){return!!this._next},next:function(){var e=this._current=this._next;return e&&(this._next=e!==this._last?e.nextSibling:null,W(e)&&this.clonePartiallySelectedTextNodes&&(e===this.ec&&(e=e.cloneNode(!0)).deleteData(this.eo,e.length-this.eo),this._current===this.sc&&(e=e.cloneNode(!0)).deleteData(0,this.so))),e},remove:function(){var e,t,n=this._current;!W(n)||n!==this.sc&&n!==this.ec?n.parentNode&&n.parentNode.removeChild(n):(e=n===this.sc?this.so:0,t=n===this.ec?this.eo:n.length,e!=t&&n.deleteData(e,t-e))},isPartiallySelectedSubtree:function(){var e=this._current;return t(e,this.range)},getSubtreeIterator:function(){var e;if(this.isSingleCharacterDataNode)e=this.range.cloneRange(),e.collapse(!1);else{e=new I(n(this.range));var t=this._current,r=t,o=0,i=t,a=q(t);F(t,this.sc)&&(r=this.sc,o=this.so),F(t,this.ec)&&(i=this.ec,a=this.eo),P(e,r,o,i,a)}return new h(e,this.clonePartiallySelectedTextNodes)},detach:function(){this.range=this._current=this._next=this._first=this._last=this.sc=this.so=this.ec=this.eo=null}};var X=[1,3,4,5,7,8,10],Z=[2,9,11],$=[5,6,10,12],J=[1,3,4,5,7,8,10,11],K=[1,3,4,5,7,8],et=g([9,11]),tt=g($),nt=g([6,10,12]),rt=document.createElement("style"),ot=!1;try{rt.innerHTML="<b>x</b>",ot=3==rt.firstChild.nodeType}catch(it){}e.features.htmlParsingConforms=ot;var at=ot?function(e){var t=this.startContainer,n=j(t);if(!t)throw new L("INVALID_STATE_ERR");var r=null;return 1==t.nodeType?r=t:W(t)&&(r=B.parentElement(t)),r=null===r||"HTML"==r.nodeName&&B.isHtmlNamespace(j(r).documentElement)&&B.isHtmlNamespace(r)?n.createElement("body"):r.cloneNode(!1),r.innerHTML=e,B.fragmentFromNodeChildren(r)}:function(e){var t=n(this),r=t.createElement("body");return r.innerHTML=e,B.fragmentFromNodeChildren(r)},st=["startContainer","startOffset","endContainer","endOffset","collapsed","commonAncestorContainer"],ct=0,dt=1,ft=2,ut=3,lt=0,ht=1,gt=2,pt=3;H.extend(e.rangePrototype,{compareBoundaryPoints:function(e,t){w(this),v(this.startContainer,t.startContainer);var n,r,o,i,a=e==ut||e==ct?"start":"end",s=e==dt||e==ct?"start":"end";return n=this[a+"Container"],r=this[a+"Offset"],o=t[s+"Container"],i=t[s+"Offset"],z(n,r,o,i)},insertNode:function(e){if(w(this),m(e,J),C(this.startContainer),F(e,this.startContainer))throw new L("HIERARCHY_REQUEST_ERR");var t=i(e,this.startContainer,this.startOffset);this.setStartBefore(t)},cloneContents:function(){w(this);var e,t;if(this.collapsed)return n(this).createDocumentFragment();if(this.startContainer===this.endContainer&&W(this.startContainer))return e=this.startContainer.cloneNode(!0),e.data=e.data.slice(this.startOffset,this.endOffset),t=n(this).createDocumentFragment(),t.appendChild(e),t;var r=new h(this,!0);return e=s(r),r.detach(),e},canSurroundContents:function(){w(this),C(this.startContainer),C(this.endContainer);var e=new h(this,!0),n=e._first&&t(e._first,this)||e._last&&t(e._last,this);return e.detach(),!n},surroundContents:function(e){if(m(e,K),!this.canSurroundContents())throw new L("INVALID_STATE_ERR");var t=this.extractContents();if(e.hasChildNodes())for(;e.lastChild;)e.removeChild(e.lastChild);i(e,this.startContainer,this.startOffset),e.appendChild(t),this.selectNode(e)},cloneRange:function(){w(this);for(var e,t=new I(n(this)),r=st.length;r--;)e=st[r],t[e]=this[e];return t},toString:function(){w(this);var e=this.startContainer;if(e===this.endContainer&&W(e))return 3==e.nodeType||4==e.nodeType?e.data.slice(this.startOffset,this.endOffset):"";var t=[],n=new h(this,!0);return c(n,function(e){(3==e.nodeType||4==e.nodeType)&&t.push(e.data)}),n.detach(),t.join("")},compareNode:function(e){w(this);var t=e.parentNode,n=k(e);if(!t)throw new L("NOT_FOUND_ERR");var r=this.comparePoint(t,n),o=this.comparePoint(t,n+1);return 0>r?o>0?gt:lt:o>0?ht:pt},comparePoint:function(e,t){return w(this),N(e,"HIERARCHY_REQUEST_ERR"),v(e,this.startContainer),z(e,t,this.startContainer,this.startOffset)<0?-1:z(e,t,this.endContainer,this.endOffset)>0?1:0},createContextualFragment:at,toHtml:function(){return T(this)},intersectsNode:function(e,t){if(w(this),N(e,"NOT_FOUND_ERR"),j(e)!==n(this))return!1;var r=e.parentNode,o=k(e);N(r,"NOT_FOUND_ERR");var i=z(r,o,this.endContainer,this.endOffset),a=z(r,o+1,this.startContainer,this.startOffset);return t?0>=i&&a>=0:0>i&&a>0},isPointInRange:function(e,t){return w(this),N(e,"HIERARCHY_REQUEST_ERR"),v(e,this.startContainer),z(e,t,this.startContainer,this.startOffset)>=0&&z(e,t,this.endContainer,this.endOffset)<=0},intersectsRange:function(e){return a(this,e,!1)},intersectsOrTouchesRange:function(e){return a(this,e,!0)},intersection:function(e){if(this.intersectsRange(e)){var t=z(this.startContainer,this.startOffset,e.startContainer,e.startOffset),n=z(this.endContainer,this.endOffset,e.endContainer,e.endOffset),r=this.cloneRange();return-1==t&&r.setStart(e.startContainer,e.startOffset),1==n&&r.setEnd(e.endContainer,e.endOffset),r}return null},union:function(e){if(this.intersectsOrTouchesRange(e)){var t=this.cloneRange();return-1==z(e.startContainer,e.startOffset,this.startContainer,this.startOffset)&&t.setStart(e.startContainer,e.startOffset),1==z(e.endContainer,e.endOffset,this.endContainer,this.endOffset)&&t.setEnd(e.endContainer,e.endOffset),t}throw new L("Ranges do not intersect")},containsNode:function(e,t){return t?this.intersectsNode(e,!1):this.compareNode(e)==pt},containsNodeContents:function(e){return this.comparePoint(e,0)>=0&&this.comparePoint(e,q(e))<=0},containsRange:function(e){var t=this.intersection(e);return null!==t&&e.equals(t)},containsNodeText:function(e){var t=this.cloneRange();t.selectNode(e);var n=t.getNodes([3]);if(n.length>0){t.setStart(n[0],0);var r=n.pop();return t.setEnd(r,r.length),this.containsRange(t)}return this.containsNodeContents(e)},getNodes:function(e,t){return w(this),u(this,e,t)},getDocument:function(){return n(this)},collapseBefore:function(e){this.setEndBefore(e),this.collapse(!1)},collapseAfter:function(e){this.setStartAfter(e),this.collapse(!0)},getBookmark:function(t){var r=n(this),o=e.createRange(r);t=t||B.getBody(r),o.selectNodeContents(t);var i=this.intersection(o),a=0,s=0;return i&&(o.setEnd(i.startContainer,i.startOffset),a=o.toString().length,s=a+i.toString().length),{start:a,end:s,containerNode:t}},moveToBookmark:function(e){var t=e.containerNode,n=0;this.setStart(t,0),this.collapse(!0);for(var r,o,i,a,s=[t],c=!1,d=!1;!d&&(r=s.pop());)if(3==r.nodeType)o=n+r.length,!c&&e.start>=n&&e.start<=o&&(this.setStart(r,e.start-n),c=!0),c&&e.end>=n&&e.end<=o&&(this.setEnd(r,e.end-n),d=!0),n=o;else for(a=r.childNodes,i=a.length;i--;)s.push(a[i])},getName:function(){return"DomRange"},equals:function(e){return I.rangesEqual(this,e)},isValid:function(){return y(this)},inspect:function(){return l(this)},detach:function(){}}),A(I,P),H.extend(I,{rangeProperties:st,RangeIterator:h,copyComparisonConstants:D,createPrototypeRange:A,inspect:l,toHtml:T,getRangeDocument:n,rangesEqual:function(e,t){return e.startContainer===t.startContainer&&e.startOffset===t.startOffset&&e.endContainer===t.endContainer&&e.endOffset===t.endOffset}}),e.DomRange=I}),b.createCoreModule("WrappedRange",["DomRange"],function(e,t){var n,r,o=e.dom,i=e.util,a=o.DomPosition,s=e.DomRange,c=o.getBody,d=o.getContentDocument,f=o.isCharacterDataNode;if(e.features.implementsDomRange&&!function(){function r(e){for(var t,n=l.length;n--;)t=l[n],e[t]=e.nativeRange[t];e.collapsed=e.startContainer===e.endContainer&&e.startOffset===e.endOffset}function a(e,t,n,r,o){var i=e.startContainer!==t||e.startOffset!=n,a=e.endContainer!==r||e.endOffset!=o,s=!e.equals(e.nativeRange);(i||a||s)&&(e.setEnd(r,o),e.setStart(t,n))}var f,u,l=s.rangeProperties;n=function(e){if(!e)throw t.createError("WrappedRange: Range must be specified");this.nativeRange=e,r(this)},s.createPrototypeRange(n,a),f=n.prototype,f.selectNode=function(e){this.nativeRange.selectNode(e),r(this)},f.cloneContents=function(){return this.nativeRange.cloneContents()},f.surroundContents=function(e){this.nativeRange.surroundContents(e),r(this)},f.collapse=function(e){this.nativeRange.collapse(e),r(this)},f.cloneRange=function(){return new n(this.nativeRange.cloneRange())},f.refresh=function(){r(this)},f.toString=function(){return this.nativeRange.toString()};var h=document.createTextNode("test");c(document).appendChild(h);var g=document.createRange();g.setStart(h,0),g.setEnd(h,0);try{g.setStart(h,1),f.setStart=function(e,t){this.nativeRange.setStart(e,t),r(this)},f.setEnd=function(e,t){this.nativeRange.setEnd(e,t),r(this)},u=function(e){return function(t){this.nativeRange[e](t),r(this)}}}catch(p){f.setStart=function(e,t){try{this.nativeRange.setStart(e,t)}catch(n){this.nativeRange.setEnd(e,t),this.nativeRange.setStart(e,t)}r(this)},f.setEnd=function(e,t){try{this.nativeRange.setEnd(e,t)}catch(n){this.nativeRange.setStart(e,t),this.nativeRange.setEnd(e,t)}r(this)},u=function(e,t){return function(n){try{this.nativeRange[e](n)}catch(o){this.nativeRange[t](n),this.nativeRange[e](n)}r(this)}}}f.setStartBefore=u("setStartBefore","setEndBefore"),f.setStartAfter=u("setStartAfter","setEndAfter"),f.setEndBefore=u("setEndBefore","setStartBefore"),f.setEndAfter=u("setEndAfter","setStartAfter"),f.selectNodeContents=function(e){this.setStartAndEnd(e,0,o.getNodeLength(e))},g.selectNodeContents(h),g.setEnd(h,3);var m=document.createRange();m.selectNodeContents(h),m.setEnd(h,4),m.setStart(h,2),f.compareBoundaryPoints=-1==g.compareBoundaryPoints(g.START_TO_END,m)&&1==g.compareBoundaryPoints(g.END_TO_START,m)?function(e,t){return t=t.nativeRange||t,e==t.START_TO_END?e=t.END_TO_START:e==t.END_TO_START&&(e=t.START_TO_END),this.nativeRange.compareBoundaryPoints(e,t)}:function(e,t){return this.nativeRange.compareBoundaryPoints(e,t.nativeRange||t)};var R=document.createElement("div");R.innerHTML="123";var v=R.firstChild,C=c(document);C.appendChild(R),g.setStart(v,1),g.setEnd(v,2),g.deleteContents(),"13"==v.data&&(f.deleteContents=function(){this.nativeRange.deleteContents(),r(this)},f.extractContents=function(){var e=this.nativeRange.extractContents();return r(this),e}),C.removeChild(R),C=null,i.isHostMethod(g,"createContextualFragment")&&(f.createContextualFragment=function(e){return this.nativeRange.createContextualFragment(e)}),c(document).removeChild(h),f.getName=function(){return"WrappedRange"},e.WrappedRange=n,e.createNativeRange=function(e){return e=d(e,t,"createNativeRange"),e.createRange()}}(),e.features.implementsTextRange){var u=function(e){var t=e.parentElement(),n=e.duplicate();n.collapse(!0);var r=n.parentElement();n=e.duplicate(),n.collapse(!1);var i=n.parentElement(),a=r==i?r:o.getCommonAncestor(r,i);return a==t?a:o.getCommonAncestor(t,a)},l=function(e){return 0==e.compareEndPoints("StartToEnd",e)},h=function(e,t,n,r,i){var s=e.duplicate();s.collapse(n);var c=s.parentElement();if(o.isOrIsAncestorOf(t,c)||(c=t),!c.canHaveHTML){var d=new a(c.parentNode,o.getNodeIndex(c));return{boundaryPosition:d,nodeInfo:{nodeIndex:d.offset,containerElement:d.node}}}var u=o.getDocument(c).createElement("span");u.parentNode&&u.parentNode.removeChild(u);for(var l,h,g,p,m,R=n?"StartToStart":"StartToEnd",v=i&&i.containerElement==c?i.nodeIndex:0,C=c.childNodes.length,N=C,E=N;;){if(E==C?c.appendChild(u):c.insertBefore(u,c.childNodes[E]),s.moveToElementText(u),l=s.compareEndPoints(R,e),0==l||v==N)break;if(-1==l){if(N==v+1)break;v=E}else N=N==v+1?v:E;E=Math.floor((v+N)/2),c.removeChild(u)}if(m=u.nextSibling,-1==l&&m&&f(m)){s.setEndPoint(n?"EndToStart":"EndToEnd",e);var S;if(/[\r\n]/.test(m.data)){var y=s.duplicate(),w=y.text.replace(/\r\n/g,"\r").length;for(S=y.moveStart("character",w);-1==(l=y.compareEndPoints("StartToEnd",y));)S++,y.moveStart("character",1)}else S=s.text.length;p=new a(m,S)}else h=(r||!n)&&u.previousSibling,g=(r||n)&&u.nextSibling,p=g&&f(g)?new a(g,0):h&&f(h)?new a(h,h.data.length):new a(c,o.getNodeIndex(u));return u.parentNode.removeChild(u),{boundaryPosition:p,nodeInfo:{nodeIndex:E,containerElement:c}}},g=function(e,t){var n,r,i,a,s=e.offset,d=o.getDocument(e.node),u=c(d).createTextRange(),l=f(e.node);return l?(n=e.node,r=n.parentNode):(a=e.node.childNodes,n=s<a.length?a[s]:null,r=e.node),i=d.createElement("span"),i.innerHTML="&#feff;",n?r.insertBefore(i,n):r.appendChild(i),u.moveToElementText(i),u.collapse(!t),r.removeChild(i),l&&u[t?"moveStart":"moveEnd"]("character",s),u};r=function(e){this.textRange=e,this.refresh()},r.prototype=new s(document),r.prototype.refresh=function(){var e,t,n,r=u(this.textRange);l(this.textRange)?t=e=h(this.textRange,r,!0,!0).boundaryPosition:(n=h(this.textRange,r,!0,!1),e=n.boundaryPosition,t=h(this.textRange,r,!1,!1,n.nodeInfo).boundaryPosition),this.setStart(e.node,e.offset),this.setEnd(t.node,t.offset)},r.prototype.getName=function(){return"WrappedTextRange"},s.copyComparisonConstants(r);var p=function(e){if(e.collapsed)return g(new a(e.startContainer,e.startOffset),!0);var t=g(new a(e.startContainer,e.startOffset),!0),n=g(new a(e.endContainer,e.endOffset),!1),r=c(s.getRangeDocument(e)).createTextRange();return r.setEndPoint("StartToStart",t),r.setEndPoint("EndToEnd",n),r};if(r.rangeToTextRange=p,r.prototype.toTextRange=function(){return p(this) 2962},e.WrappedTextRange=r,!e.features.implementsDomRange||e.config.preferTextRange){var m=function(e){return e("return this;")()}(Function);"undefined"==typeof m.Range&&(m.Range=r),e.createNativeRange=function(e){return e=d(e,t,"createNativeRange"),c(e).createTextRange()},e.WrappedRange=r}}e.createRange=function(n){return n=d(n,t,"createRange"),new e.WrappedRange(e.createNativeRange(n))},e.createRangyRange=function(e){return e=d(e,t,"createRangyRange"),new s(e)},e.createIframeRange=function(n){return t.deprecationNotice("createIframeRange()","createRange(iframeEl)"),e.createRange(n)},e.createIframeRangyRange=function(n){return t.deprecationNotice("createIframeRangyRange()","createRangyRange(iframeEl)"),e.createRangyRange(n)},e.addShimListener(function(t){var n=t.document;"undefined"==typeof n.createRange&&(n.createRange=function(){return e.createRange(n)}),n=t=null})}),b.createCoreModule("WrappedSelection",["DomRange","WrappedRange"],function(e,t){function n(e){return"string"==typeof e?/^backward(s)?$/i.test(e):!!e}function r(e,n){if(e){if(D.isWindow(e))return e;if(e instanceof R)return e.win;var r=D.getContentDocument(e,t,n);return D.getWindow(r)}return window}
vendor: 5,301 bytes, line 2962
2962function o(e){return r(e,"getWinSelection").getSelection()}function i(e){return r(e,"getDocSelection").document.selection}function a(e){var t=!1;return e.anchorNode&&(t=1==D.comparePoints(e.anchorNode,e.anchorOffset,e.focusNode,e.focusOffset)),t}function s(e,t,n){var r=n?"end":"start",o=n?"start":"end";e.anchorNode=t[r+"Container"],e.anchorOffset=t[r+"Offset"],e.focusNode=t[o+"Container"],e.focusOffset=t[o+"Offset"]}function c(e){var t=e.nativeSelection;e.anchorNode=t.anchorNode,e.anchorOffset=t.anchorOffset,e.focusNode=t.focusNode,e.focusOffset=t.focusOffset}function d(e){e.anchorNode=e.focusNode=null,e.anchorOffset=e.focusOffset=0,e.rangeCount=0,e.isCollapsed=!0,e._ranges.length=0}function f(t){var n;return t instanceof b?(n=e.createNativeRange(t.getDocument()),n.setEnd(t.endContainer,t.endOffset),n.setStart(t.startContainer,t.startOffset)):t instanceof P?n=t.nativeRange:H.implementsDomRange&&t instanceof D.getWindow(t.startContainer).Range&&(n=t),n}function u(e){if(!e.length||1!=e[0].nodeType)return!1;for(var t=1,n=e.length;n>t;++t)if(!D.isAncestorOf(e[0],e[t]))return!1;return!0}function l(e){var n=e.getNodes();if(!u(n))throw t.createError("getSingleElementFromRange: range "+e.inspect()+" did not consist of a single element");return n[0]}function h(e){return!!e&&"undefined"!=typeof e.text}function g(e,t){var n=new P(t);e._ranges=[n],s(e,n,!1),e.rangeCount=1,e.isCollapsed=n.collapsed}function p(t){if(t._ranges.length=0,"None"==t.docSelection.type)d(t);else{var n=t.docSelection.createRange();if(h(n))g(t,n);else{t.rangeCount=n.length;for(var r,o=L(n.item(0)),i=0;i<t.rangeCount;++i)r=e.createRange(o),r.selectNode(n.item(i)),t._ranges.push(r);t.isCollapsed=1==t.rangeCount&&t._ranges[0].collapsed,s(t,t._ranges[t.rangeCount-1],!1)}}}function m(e,n){for(var r=e.docSelection.createRange(),o=l(n),i=L(r.item(0)),a=W(i).createControlRange(),s=0,c=r.length;c>s;++s)a.add(r.item(s));try{a.add(o)}catch(d){throw t.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)")}a.select(),p(e)}function R(e,t,n){this.nativeSelection=e,this.docSelection=t,this._ranges=[],this.win=n,this.refresh()}function v(e){e.win=e.anchorNode=e.focusNode=e._ranges=null,e.rangeCount=e.anchorOffset=e.focusOffset=0,e.detached=!0}function C(e,t){for(var n,r,o=tt.length;o--;)if(n=tt[o],r=n.selection,"deleteAll"==t)v(r);else if(n.win==e)return"delete"==t?(tt.splice(o,1),!0):r;return"deleteAll"==t&&(tt.length=0),null}function N(e,n){for(var r,o=L(n[0].startContainer),i=W(o).createControlRange(),a=0,s=n.length;s>a;++a){r=l(n[a]);try{i.add(r)}catch(c){throw t.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)")}}i.select(),p(e)}function E(e,t){if(e.win.document!=L(t))throw new I("WRONG_DOCUMENT_ERR")}function S(t){return function(n,r){var o;this.rangeCount?(o=this.getRangeAt(0),o["set"+(t?"Start":"End")](n,r)):(o=e.createRange(this.win.document),o.setStartAndEnd(n,r)),this.setSingleRange(o,this.isBackward())}}function y(e){var t=[],n=new B(e.anchorNode,e.anchorOffset),r=new B(e.focusNode,e.focusOffset),o="function"==typeof e.getName?e.getName():"Selection";if("undefined"!=typeof e.rangeCount)for(var i=0,a=e.rangeCount;a>i;++i)t[i]=b.inspect(e.getRangeAt(i));return"["+o+"(Ranges: "+t.join(", ")+")(anchor: "+n.inspect()+", focus: "+r.inspect()+"]"}e.config.checkSelectionRanges=!0;var w,O,T="boolean",_="number",D=e.dom,x=e.util,A=x.isHostMethod,b=e.DomRange,P=e.WrappedRange,I=e.DOMException,B=D.DomPosition,H=e.features,M="Control",L=D.getDocument,W=D.getBody,k=b.rangesEqual,F=A(window,"getSelection"),j=x.isHostObject(document,"selection");H.implementsWinGetSelection=F,H.implementsDocSelection=j;var z=j&&(!F||e.config.preferTextRange);z?(w=i,e.isSelectionValid=function(e){var t=r(e,"isSelectionValid").document,n=t.selection;return"None"!=n.type||L(n.createRange().parentElement())==t}):F?(w=o,e.isSelectionValid=function(){return!0}):t.fail("Neither document.selection or window.getSelection() detected."),e.getNativeSelection=w;var U=w(),V=e.createNativeRange(document),q=W(document),Y=x.areHostProperties(U,["anchorNode","focusNode","anchorOffset","focusOffset"]);H.selectionHasAnchorAndFocus=Y;var Q=A(U,"extend");H.selectionHasExtend=Q;var G=typeof U.rangeCount==_;H.selectionHasRangeCount=G;var X=!1,Z=!0,$=Q?function(t,n){var r=b.getRangeDocument(n),o=e.createRange(r);o.collapseToPoint(n.endContainer,n.endOffset),t.addRange(f(o)),t.extend(n.startContainer,n.startOffset)}:null;x.areHostMethods(U,["addRange","getRangeAt","removeAllRanges"])&&typeof U.rangeCount==_&&H.implementsDomRange&&!function(){var t=window.getSelection();if(t){for(var n=t.rangeCount,r=n>1,o=[],i=a(t),s=0;n>s;++s)o[s]=t.getRangeAt(s);var c=W(document),d=c.appendChild(document.createElement("div"));d.contentEditable="false";var f=d.appendChild(document.createTextNode("Â Â Â ")),u=document.createRange();if(u.setStart(f,1),u.collapse(!0),t.addRange(u),Z=1==t.rangeCount,t.removeAllRanges(),!r){var l=window.navigator.appVersion.match(/Chrome\/(.*?) /);if(l&&parseInt(l[1])>=36)X=!1;else{var h=u.cloneRange();u.setStart(f,0),h.setEnd(f,3),h.setStart(f,2),t.addRange(u),t.addRange(h),X=2==t.rangeCount}}for(c.removeChild(d),t.removeAllRanges(),s=0;n>s;
vendor: 4,243 bytes, line 2962
2962++s)0==s&&i?$?$(t,o[s]):(e.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend"),t.addRange(o[s])):t.addRange(o[s])}}(),H.selectionSupportsMultipleRanges=X,H.collapsedNonEditableSelectionsSupported=Z;var J,K=!1;q&&A(q,"createControlRange")&&(J=q.createControlRange(),x.areHostProperties(J,["item","add"])&&(K=!0)),H.implementsControlRange=K,O=Y?function(e){return e.anchorNode===e.focusNode&&e.anchorOffset===e.focusOffset}:function(e){return e.rangeCount?e.getRangeAt(e.rangeCount-1).collapsed:!1};var et;A(U,"getRangeAt")?et=function(e,t){try{return e.getRangeAt(t)}catch(n){return null}}:Y&&(et=function(t){var n=L(t.anchorNode),r=e.createRange(n);return r.setStartAndEnd(t.anchorNode,t.anchorOffset,t.focusNode,t.focusOffset),r.collapsed!==this.isCollapsed&&r.setStartAndEnd(t.focusNode,t.focusOffset,t.anchorNode,t.anchorOffset),r}),R.prototype=e.selectionPrototype;var tt=[],nt=function(e){if(e&&e instanceof R)return e.refresh(),e;e=r(e,"getNativeSelection");var t=C(e),n=w(e),o=j?i(e):null;return t?(t.nativeSelection=n,t.docSelection=o,t.refresh()):(t=new R(n,o,e),tt.push({win:e,selection:t})),t};e.getSelection=nt,e.getIframeSelection=function(n){return t.deprecationNotice("getIframeSelection()","getSelection(iframeEl)"),e.getSelection(D.getIframeWindow(n))};var rt=R.prototype;if(!z&&Y&&x.areHostMethods(U,["removeAllRanges","addRange"])){rt.removeAllRanges=function(){this.nativeSelection.removeAllRanges(),d(this)};var ot=function(e,t){$(e.nativeSelection,t),e.refresh()};rt.addRange=G?function(t,r){if(K&&j&&this.docSelection.type==M)m(this,t);else if(n(r)&&Q)ot(this,t);else{var o;X?o=this.rangeCount:(this.removeAllRanges(),o=0);var i=f(t).cloneRange();try{this.nativeSelection.addRange(i)}catch(a){}if(this.rangeCount=this.nativeSelection.rangeCount,this.rangeCount==o+1){if(e.config.checkSelectionRanges){var c=et(this.nativeSelection,this.rangeCount-1);c&&!k(c,t)&&(t=new P(c))}this._ranges[this.rangeCount-1]=t,s(this,t,st(this.nativeSelection)),this.isCollapsed=O(this)}else this.refresh()}}:function(e,t){n(t)&&Q?ot(this,e):(this.nativeSelection.addRange(f(e)),this.refresh())},rt.setRanges=function(e){if(K&&j&&e.length>1)N(this,e);else{this.removeAllRanges();for(var t=0,n=e.length;n>t;++t)this.addRange(e[t])}}}else{if(!(A(U,"empty")&&A(V,"select")&&K&&z))return t.fail("No means of selecting a Range or TextRange was found"),!1;rt.removeAllRanges=function(){try{if(this.docSelection.empty(),"None"!=this.docSelection.type){var e;if(this.anchorNode)e=L(this.anchorNode);else if(this.docSelection.type==M){var t=this.docSelection.createRange();t.length&&(e=L(t.item(0)))}if(e){var n=W(e).createTextRange();n.select(),this.docSelection.empty()}}}catch(r){}d(this)},rt.addRange=function(t){this.docSelection.type==M?m(this,t):(e.WrappedTextRange.rangeToTextRange(t).select(),this._ranges[0]=t,this.rangeCount=1,this.isCollapsed=this._ranges[0].collapsed,s(this,t,!1))},rt.setRanges=function(e){this.removeAllRanges();var t=e.length;t>1?N(this,e):t&&this.addRange(e[0])}}rt.getRangeAt=function(e){if(0>e||e>=this.rangeCount)throw new I("INDEX_SIZE_ERR");return this._ranges[e].cloneRange()};var it;if(z)it=function(t){var n;e.isSelectionValid(t.win)?n=t.docSelection.createRange():(n=W(t.win.document).createTextRange(),n.collapse(!0)),t.docSelection.type==M?p(t):h(n)?g(t,n):d(t)};else if(A(U,"getRangeAt")&&typeof U.rangeCount==_)it=function(t){if(K&&j&&t.docSelection.type==M)p(t);else if(t._ranges.length=t.rangeCount=t.nativeSelection.rangeCount,t.rangeCount){for(var n=0,r=t.rangeCount;r>n;++n)t._ranges[n]=new e.WrappedRange(t.nativeSelection.getRangeAt(n));s(t,t._ranges[t.rangeCount-1],st(t.nativeSelection)),t.isCollapsed=O(t)}else d(t)};else{if(!Y||typeof U.isCollapsed!=T||typeof V.collapsed!=T||!H.implementsDomRange)return t.fail("No means of obtaining a Range or TextRange from the user's selection was found"),!1;it=function(e){var t,n=e.nativeSelection;n.anchorNode?(t=et(n,0),e._ranges=[t],e.rangeCount=1,c(e),e.isCollapsed=O(e)):d(e)}}rt.refresh=function(e){var t=e?this._ranges.slice(0):null,n=this.anchorNode,r=this.anchorOffset;if(it(this),e){var o=t.length;if(o!=this._ranges.length)return!0;
vendor: 4,223 bytes, lines 2962-2975
2962if(this.anchorNode!=n||this.anchorOffset!=r)return!0;for(;o--;)if(!k(t[o],this._ranges[o]))return!0;return!1}};var at=function(e,t){var n=e.getAllRanges();e.removeAllRanges();for(var r=0,o=n.length;o>r;++r)k(t,n[r])||e.addRange(n[r]);e.rangeCount||d(e)};rt.removeRange=K&&j?function(e){if(this.docSelection.type==M){for(var t,n=this.docSelection.createRange(),r=l(e),o=L(n.item(0)),i=W(o).createControlRange(),a=!1,s=0,c=n.length;c>s;++s)t=n.item(s),t!==r||a?i.add(n.item(s)):a=!0;i.select(),p(this)}else at(this,e)}:function(e){at(this,e)};var st;!z&&Y&&H.implementsDomRange?(st=a,rt.isBackward=function(){return st(this)}):st=rt.isBackward=function(){return!1},rt.isBackwards=rt.isBackward,rt.toString=function(){for(var e=[],t=0,n=this.rangeCount;n>t;++t)e[t]=""+this._ranges[t];return e.join("")},rt.collapse=function(t,n){E(this,t);var r=e.createRange(t);r.collapseToPoint(t,n),this.setSingleRange(r),this.isCollapsed=!0},rt.collapseToStart=function(){if(!this.rangeCount)throw new I("INVALID_STATE_ERR");var e=this._ranges[0];this.collapse(e.startContainer,e.startOffset)},rt.collapseToEnd=function(){if(!this.rangeCount)throw new I("INVALID_STATE_ERR");var e=this._ranges[this.rangeCount-1];this.collapse(e.endContainer,e.endOffset)},rt.selectAllChildren=function(t){E(this,t);var n=e.createRange(t);n.selectNodeContents(t),this.setSingleRange(n)},rt.deleteFromDocument=function(){if(K&&j&&this.docSelection.type==M){for(var e,t=this.docSelection.createRange();t.length;)e=t.item(0),t.remove(e),e.parentNode.removeChild(e);this.refresh()}else if(this.rangeCount){var n=this.getAllRanges();if(n.length){this.removeAllRanges();for(var r=0,o=n.length;o>r;++r)n[r].deleteContents();this.addRange(n[o-1])}}},rt.eachRange=function(e,t){for(var n=0,r=this._ranges.length;r>n;++n)if(e(this.getRangeAt(n)))return t},rt.getAllRanges=function(){var e=[];return this.eachRange(function(t){e.push(t)}),e},rt.setSingleRange=function(e,t){this.removeAllRanges(),this.addRange(e,t)},rt.callMethodOnEachRange=function(e,t){var n=[];return this.eachRange(function(r){n.push(r[e].apply(r,t))}),n},rt.setStart=S(!0),rt.setEnd=S(!1),e.rangePrototype.select=function(e){nt(this.getDocument()).setSingleRange(this,e)},rt.changeEachRange=function(e){var t=[],n=this.isBackward();this.eachRange(function(n){e(n),t.push(n)}),this.removeAllRanges(),n&&1==t.length?this.addRange(t[0],"backward"):this.setRanges(t)},rt.containsNode=function(e,t){return this.eachRange(function(n){return n.containsNode(e,t)},!0)||!1},rt.getBookmark=function(e){return{backward:this.isBackward(),rangeBookmarks:this.callMethodOnEachRange("getBookmark",[e])}},rt.moveToBookmark=function(t){for(var n,r,o=[],i=0;n=t.rangeBookmarks[i++];)r=e.createRange(this.win),r.moveToBookmark(n),o.push(r);t.backward?this.setSingleRange(o[0],"backward"):this.setRanges(o)},rt.toHtml=function(){var e=[];return this.eachRange(function(t){e.push(b.toHtml(t))}),e.join("")},H.implementsTextRange&&(rt.getNativeTextRange=function(){var n;if(n=this.docSelection){var r=n.createRange();if(h(r))return r;throw t.createError("getNativeTextRange: selection is a control selection")}if(this.rangeCount>0)return e.WrappedTextRange.rangeToTextRange(this.getRangeAt(0));throw t.createError("getNativeTextRange: selection contains no range")}),rt.getName=function(){return"WrappedSelection"},rt.inspect=function(){return y(this)},rt.detach=function(){C(this.win,"delete"),v(this)},R.detachAll=function(){C(null,"deleteAll")},R.inspect=y,R.isDirectionBackward=n,e.Selection=R,e.selectionPrototype=rt,e.addShimListener(function(e){"undefined"==typeof e.getSelection&&(e.getSelection=function(){return nt(e)}),e=null})});var M=!1,L=function(){M||(M=!0,!b.initialized&&b.config.autoInitialize&&u())};return x&&("complete"==document.readyState?L():(e(document,"addEventListener")&&document.addEventListener("DOMContentLoaded",L,!1),I(window,"load",L))),b},this); 2963 2964/** 2965 * Class Applier module for Rangy. 2966 * Adds, removes and toggles classes on Ranges and Selections 2967 * 2968 * Part of Rangy, a cross-browser JavaScript range and selection library 2969 * https://github.com/timdown/rangy 2970 * 2971 * Depends on Rangy core. 2972 * 2973 * Copyright 2015, Tim Down 2974 * Licensed under the MIT license. 2975 * Version: 1.3.0-beta.1
2976 * Build date: 12 February 2015 2977 */ 2978 2979;!function(e,t){ e(t.rangy) }(function(e){return e.createModule("ClassApplier",["WrappedSelection"],function(e,t){function n(e,t){for(var n in e)if(e.hasOwnProperty(n)&&t(n,e[n])===!1)return!1;return!0}function s(e){return e.replace(/^\s\s*/,"").replace(/\s\s*$/,"")}function r(e){return e&&e.split(/\s+/).sort().join(" ")}function o(e){return r(e.className)}function a(e,t){return o(e)==o(t)}function l(e,t){for(var n=t.split(/\s+/),i=0,r=n.length;r>i;++i)if(!I(e,s(n[i])))return!1;return!0}function u(e,t,n,s,i){var r=e.node,o=e.offset,a=r,l=o;r==s&&o>i&&++l,r!=t||o!=n&&o!=n+1||(a=s,l+=i-n),r==t&&o>n+1&&--l,e.node=a,e.offset=l}function f(e,t,n){e.node==t&&e.offset>n&&--e.offset}function c(e,t,n,s){-1==n&&(n=t.childNodes.length);var i=e.parentNode,r=M.getNodeIndex(e);$(s,function(e){u(e,i,r,t,n)}),t.childNodes.length==n?t.appendChild(e):t.insertBefore(e,t.childNodes[n])}function d(e,t){var n=e.parentNode,s=M.getNodeIndex(e);$(t,function(e){f(e,n,s)}),e.parentNode.removeChild(e)}function p(e,t,n,s,i){for(var r,o=[];r=e.firstChild;)c(r,t,n++,i),o.push(r);return s&&d(e,i),o}function h(e,t){return p(e,e.parentNode,M.getNodeIndex(e),!0,t)}function m(e,t){var n=e.cloneRange();n.selectNodeContents(t);var s=n.intersection(e),i=s?s.toString():"";return""!=i}function g(e){for(var t,n=e.getNodes([3]),s=0;(t=n[s])&&!m(e,t);)++s;for(var i=n.length-1;(t=n[i])&&!m(e,t);)--i;return n.slice(s,i+1)}function N(e,t){if(e.attributes.length!=t.attributes.length)return!1;for(var n,s,i,r=0,o=e.attributes.length;o>r;++r)if(n=e.attributes[r],i=n.name,"class"!=i){if(s=t.attributes.getNamedItem(i),null===n!=(null===s))return!1;if(n.specified!=s.specified)return!1;if(n.specified&&n.nodeValue!==s.nodeValue)return!1}return!0}function v(e,t){for(var n,s=0,i=e.attributes.length;i>s;++s)if(n=e.attributes[s].name,(!t||!z(t,n))&&e.attributes[s].specified&&"class"!=n)return!0;return!1}function y(e){var t;return e&&1==e.nodeType&&((t=e.parentNode)&&9==t.nodeType&&"on"==t.designMode||V(e)&&!V(e.parentNode))}function C(e){return(V(e)||1!=e.nodeType&&V(e.parentNode))&&!y(e)}function E(e){return e&&1==e.nodeType&&!k.test(U(e,"display"))}function T(e){if(0==e.data.length)return!0;if(q.test(e.data))return!1;var t=U(e.parentNode,"whiteSpace");switch(t){case"pre":case"pre-wrap":case"-moz-pre-wrap":return!1;case"pre-line":if(/[\r\n]/.test(e.data))return!1}return E(e.previousSibling)||E(e.nextSibling)}function b(e){var t,n,s=[];for(t=0;n=e[t++];)s.push(new j(n.startContainer,n.startOffset),new j(n.endContainer,n.endOffset));return s}function A(e,t){for(var n,s,i,r=0,o=e.length;o>r;++r)n=e[r],s=t[2*r],i=t[2*r+1],n.setStartAndEnd(s.node,s.offset,i.node,i.offset)}function S(e,t){return M.isCharacterDataNode(e)?0==t?!!e.previousSibling:t==e.length?!!e.nextSibling:!0:t>0&&t<e.childNodes.length}function x(e,n,s,i){var r,o,a=0==s;if(M.isAncestorOf(n,e))return e;if(M.isCharacterDataNode(n)){var l=M.getNodeIndex(n);if(0==s)s=l;else{if(s!=n.length)throw t.createError("splitNodeAt() should not be called with offset in the middle of a data node ("+s+" in "+n.data);s=l+1}n=n.parentNode}if(S(n,s)){r=n.cloneNode(!1),o=n.parentNode,r.id&&r.removeAttribute("id");for(var u,f=0;u=n.childNodes[s];)c(u,r,f++,i);return c(r,o,M.getNodeIndex(n)+1,i),n==e?r:x(e,o,M.getNodeIndex(r),i)}if(e!=n){r=n.parentNode;var d=M.getNodeIndex(n);return a||d++,x(e,r,d,i)}return e}function R(e,t){return e.namespaceURI==t.namespaceURI&&e.tagName.toLowerCase()==t.tagName.toLowerCase()&&a(e,t)&&N(e,t)&&"inline"==U(e,"display")&&"inline"==U(t,"display")}function P(e){var t=e?"nextSibling":"previousSibling";return function(n,s){var i=n.parentNode,r=n[t];if(r){if(r&&3==r.nodeType)return r}else if(s&&(r=i[t],r&&1==r.nodeType&&R(i,r))){var o=r[e?"firstChild":"lastChild"];if(o&&3==o.nodeType)return o}return null}}function w(e){this.isElementMerge=1==e.nodeType,this.textNodes=[];var t=this.isElementMerge?e.lastChild:e;t&&(this.textNodes[0]=t)}function O(e,t,i){var o,a,l,u,f=this;f.cssClass=f.className=e;var c=null,d={};if("object"==typeof t&&null!==t){for("undefined"!=typeof t.elementTagName&&(t.elementTagName=t.elementTagName.toLowerCase()),i=t.tagNames,c=t.elementProperties,d=t.elementAttributes,a=0;u=J[a++];)t.hasOwnProperty(u)&&(f[u]=t[u]);o=t.normalize}else o=t;f.normalize="undefined"==typeof o?!0:o,f.attrExceptions=[];var p=document.createElement(f.elementTagName);f.elementProperties=f.copyPropertiesToElement(c,p,!0),n(d,function(e){f.attrExceptions.push(e)}),f.elementAttributes=d,f.elementSortedClassName=f.elementProperties.hasOwnProperty("className")?r(f.elementProperties.className+" "+e):e,f.applyToAnyTagName=!1;var h=typeof i;if("string"==h)"*"==i?f.applyToAnyTagName=!0:f.tagNames=s(i.toLowerCase()).split(/\s*,\s*/);else if("object"==h&&"number"==typeof i.length)for(f.tagNames=[],a=0,l=i.length;l>a;++a)"*"==i[a]?f.applyToAnyTagName=!0:f.tagNames.push(i[a].toLowerCase());else f.tagNames=[f.elementTagName]}function W(e,t,n){return new O(e,t,n)}var I,L,H,M=e.dom,j=M.DomPosition,z=M.arrayContains,B=M.isHtmlNamespace,$=e.util.forEach,D="span";e.util.isHostObject(document.createElement("div"),"classList")?(I=function(e,t){return e.classList.contains(t)},L=function(e,t){return e.classList.add(t)},H=function(e,t){return e.classList.remove(t)}):(I=function(e,t){return e.className&&new RegExp("(?:^|\\s)"+t+"(?:\\
2979s|$)").test(e.className)},L=function(e,t){e.className?I(e,t)||(e.className+=" "+t):e.className=t},H=function(){function e(e,t,n){return t&&n?" ":""}return function(t,n){t.className&&(t.className=t.className.replace(new RegExp("(^|\\s)"+n+"(\\s|$)"),e))}}());var U=M.getComputedStyleProperty,V=function(){var e=document.createElement("div");return"boolean"==typeof e.isContentEditable?function(e){return e&&1==e.nodeType&&e.isContentEditable}:function(e){return e&&1==e.nodeType&&"false"!=e.contentEditable?"true"==e.contentEditable||V(e.parentNode):!1}}(),k=/^inline(-block|-table)?$/i,q=/[^\r\n\t\f \u200B]/,F=P(!1),G=P(!0);w.prototype={doMerge:function(e){var t=this.textNodes,n=t[0];if(t.length>1){var s,i=M.getNodeIndex(n),r=[],o=0;$(t,function(t,a){s=t.parentNode,a>0&&(s.removeChild(t),s.hasChildNodes()||s.parentNode.removeChild(s),e&&$(e,function(e){e.node==t&&(e.node=n,e.offset+=o),e.node==s&&e.offset>i&&(--e.offset,e.offset==i+1&&len-1>a&&(e.node=n,e.offset=o))})),r[a]=t.data,o+=t.data.length}),n.data=r.join("")}return n.data},getLength:function(){for(var e=this.textNodes.length,t=0;e--;)t+=this.textNodes[e].length;return t},toString:function(){var e=[];return $(this.textNodes,function(t,n){e[n]="'"+t.data+"'"}),"[Merge("+e.join(",")+")]"}};var J=["elementTagName","ignoreWhiteSpace","applyToEditableOnly","useExistingElements","removeEmptyElements","onElementCreate"],K={};O.prototype={elementTagName:D,elementProperties:{},elementAttributes:{},ignoreWhiteSpace:!0,applyToEditableOnly:!1,useExistingElements:!0,removeEmptyElements:!0,onElementCreate:null,copyPropertiesToElement:function(e,t,n){var s,i,o,a,l,u,f={};for(var c in e)if(e.hasOwnProperty(c))if(a=e[c],l=t[c],"className"==c)L(t,a),L(t,this.className),t[c]=r(t[c]),n&&(f[c]=a);else if("style"==c){i=l,n&&(f[c]=o={});for(s in e[c])e[c].hasOwnProperty(s)&&(i[s]=a[s],n&&(o[s]=i[s]));this.attrExceptions.push(c)}else t[c]=a,n&&(f[c]=t[c],u=K.hasOwnProperty(c)?K[c]:c,this.attrExceptions.push(u));return n?f:""},copyAttributesToElement:function(e,t){for(var n in e)e.hasOwnProperty(n)&&!/^class(?:Name)?$/i.test(n)&&t.setAttribute(n,e[n])},appliesToElement:function(e){return z(this.tagNames,e.tagName.toLowerCase())},getEmptyElements:function(e){var t=this;return e.getNodes([1],function(e){return t.appliesToElement(e)&&!e.hasChildNodes()})},hasClass:function(e){return 1==e.nodeType&&(this.applyToAnyTagName||this.appliesToElement(e))&&I(e,this.className)},getSelfOrAncestorWithClass:function(e){for(;e;){if(this.hasClass(e))return e;e=e.parentNode}return null},isModifiable:function(e){return!this.applyToEditableOnly||C(e)},isIgnorableWhiteSpaceNode:function(e){return this.ignoreWhiteSpace&&e&&3==e.nodeType&&T(e)},postApply:function(e,t,n,s){var r,o,a=e[0],l=e[e.length-1],u=[],f=a,c=l,d=0,p=l.length;$(e,function(e){o=F(e,!s),o?(r||(r=new w(o),u.push(r)),r.textNodes.push(e),e===a&&(f=r.textNodes[0],d=f.length),e===l&&(c=r.textNodes[0],p=r.getLength())):r=null});var h=G(l,!s);if(h&&(r||(r=new w(l),u.push(r)),r.textNodes.push(h)),u.length){for(var i=0,len=u.length;len>i;++i)u[i].doMerge(n);t.setStartAndEnd(f,d,c,p)}},createContainer:function(e){var t=e.createElement(this.elementTagName);return this.copyPropertiesToElement(this.elementProperties,t,!1),this.copyAttributesToElement(this.elementAttributes,t),L(t,this.className),this.onElementCreate&&this.onElementCreate(t,this),t},elementHasProperties:function(e,t){var s=this;return n(t,function(t,n){if("className"==t)return l(e,n);if("object"==typeof n){if(!s.elementHasProperties(e[t],n))return!1}else if(e[t]!==n)return!1})},elementHasAttributes:function(e,t){return n(t,function(t,n){return e.getAttribute(t)!==n?!1:void 0})},applyToTextNode:function(e){var t=e.parentNode;if(1==t.childNodes.length&&this.useExistingElements&&B(t)&&this.appliesToElement(t)&&this.elementHasProperties(t,this.elementProperties)&&this.elementHasAttributes(t,this.elementAttributes))L(t,this.className);else{var n=this.createContainer(M.getDocument(e));e.parentNode.insertBefore(n,e),n.appendChild(e)}},isRemovable:function(e){return B(e)&&e.tagName.toLowerCase()==this.elementTagName&&o(e)==this.elementSortedClassName&&this.elementHasProperties(e,this.elementProperties)&&!v(e,this.attrExceptions)&&this.elementHasAttributes(e,this.elementAttributes)&&this.isModifiable(e)},isEmptyContainer:function(e){var t=e.childNodes.length;return 1==e.nodeType&&this.isRemovable(e)&&(0==t||1==t&&this.isEmptyContainer(e.firstChild))},removeEmptyContainers:function(e){var t=this,n=e.getNodes([1],function(e){return t.isEmptyContainer(e)}),s=[e],i=b(s);$(n,function(e){d(e,i)}),A(s,i)},undoToTextNode:function(e,t,n,s){if(!t.containsNode(n)){var i=t.cloneRange();i.selectNode(n),i.isPointInRange(t.endContainer,t.endOffset)&&(x(n,t.endContainer,t.endOffset,s),t.setEndAfter(n)),i.isPointInRange(t.startContainer,t.startOffset)&&(n=x(n,t.startContainer,t.startOffset,s))}this.isRemovable(n)?h(n,s):H(n,this.className)},splitAncestorWithClass:function(e,t,n){var s=this.getSelfOrAncestorWithClass(e);s&&x(s,e,t,n)},undoToAncestor:function(e,t){this.isRemovable(e)?h(e,t):H(e,this.className)},applyToRange:function(e,t){var n=this;t=t||[];var s=b(t||[]);e.splitBoundariesPreservingPositions(s),n.removeEmptyElements&&n.removeEmptyContainers(e);var i=g(e);if(i.length){$(i,function(e){n.isIgnorableWhiteSpaceNode(e)||n.getSelfOrAncestorWithClass(e)||!n.isModifiable(e)||n.applyToTextNode(e,s)});var r=i[i.length-1];e.setStartAndEnd(i[0],0,r,r.length),n.normalize&&n.postApply(i,e,s,!1),A(t,s)}var o=n.getEmptyElements(e);$(o,function(e){L(e,n.className)})},applyToRanges:function(e){for(var t=e.length;t--;)this.applyToRange(e[t],e);return e},applyToSelection:function(t){var n=e.getSelection(t);n.setRanges(this.applyToRanges(n.getAllRanges()))},undoToRange:function(e,t){var n=this;t=t||[];var s=b(t);e.splitBoundariesPreservingPositions(s),n.removeEmptyElements&&n.removeEmptyContainers(e,s);var i,r,o=g(e),a=o[o.length-1];if(o.length){n.splitAncestorWithClass(e.endContainer,e.endOffset,s),n.splitAncestorWithClass(e.startContainer,e.startOffset,s);for(var l=0,u=o.length;u>l;++l)i=o[l],r=n.getSelfOrAncestorWithClass(i),r&&n.isModifiable(i)&&n.undoToAncestor(r,s);e.setStartAndEnd(o[0],0,a,a.length),n.normalize&&n.postApply(o,e,s,!0),A(t,s)}var f=n.getEmptyElements(e);$(f,function(e){H(e,n.className)})},undoToRanges:function(e){for(var t=e.length;t--;)this.undoToRange(e[t],e);return e},undoToSelection:function(t){var n=e.getSelection(t),s=e.getSelection(t).getAllRanges();this.undoToRanges(s),n.setRanges(s)},isAppliedToRange:function(e){if(e.collapsed||""==e.toString())return!!this.getSelfOrAncestorWithClass(e.commonAncestorContainer);var t=e.getNodes([3]);if(t.length)for(var n,s=0;n=t[s++];)if(!this.isIgnorableWhiteSpaceNode(n)&&m(e,n)&&this.isModifiable(n)&&!this.getSelfOrAncestorWithClass(n))return!1;return!0},isAppliedToRanges:function(e){var t=e.length;if(0==t)return!1;for(;t--;)if(!this.isAppliedToRange(e[t]))return!1;return!0},isAppliedToSelection:function(t){var n=e.getSelection(t);return this.isAppliedToRanges(n.getAllRanges())},toggleRange:function(e){this.isAppliedToRange(e)?this.undoToRange(e):this.applyToRange(e)},toggleSelection:function(e){this.isAppliedToSelection(e)?this.undoToSelection(e):this.applyToSelection(e)},getElementsWithClassIntersectingRange:function(e){var t=[],n=this;return e.getNodes([3],function(e){var s=n.getSelfOrAncestorWithClass(e);
2979s&&!z(t,s)&&t.push(s)}),t},detach:function(){}},O.util={hasClass:I,addClass:L,removeClass:H,hasSameClasses:a,hasAllClasses:l,replaceWithOwnChildren:h,elementsHaveSameNonClassAttributes:N,elementHasNonClassAttributes:v,splitNodeAt:x,isEditableElement:V,isEditingHost:y,isEditable:C},e.CssClassApplier=e.ClassApplier=O,e.createCssClassApplier=e.createClassApplier=W}),e},this); 2980 2981 2982 2983 /** 2984 * An Editor instance is created for every individual editor. It handles all input for that element 2985 * <br /><br /> 2986 * <br /><br /> 2987 * This is a private class and can only be instanciated via {@link CargoEditor.addEditor}. Use {@link CargoEditor.getActiveEditor} or {@link CargoEditor.getEditorInstances} to retrieve instances. 2988 * <pre><code> 2989 * $('article').each(function() { 2990 * if (!$(this).is('[contenteditable]')) { 2991 * CargoEditor.addEditor(this); 2992 * } 2993 * }); 2994 * </pre></code> 2995 * 2996 * @constructor 2997 * @private 2998 * @memberOf CargoEditor 2999 * @returns {CargoEditor.Editor|Boolean} Returns an Editor object if init is successful. In case the init fails it returns false. 3000 */ 3001 3002 3003 var Editor = function(element) { 3004 3005 var _this = this, 3006 isActive = false, 3007 isEmpty = false, 3008 element = element, 3009 userInitiatedMutationInProgress = false, 3010 emulatingMutation = false, 3011 mutationSummaryObserver, 3012 historyDelay = 1250, 3013 executionDepth = 0, 3014 dragster; 3015 3016 /* 3017 * Private function to handle mutation summary callbacks. 3018 * To trigger this use the generateMutationSummary method. 3019 */ 3020 3021 function _handleSummaryCallback(summaries){ 3022 3023 if(summaries !== undefined){ 3024 3025 if(summaries.length > 0){ 3026 3027 summaries.forEach(function(summary){ 3028 _core.events.trigger('editor-content-change', _this, summary, userInitiatedMutationInProgress); 3029 }); 3030 3031 } 3032 3033 } 3034 3035 return summaries; 3036 3037 }; 3038 3039 this.keyEvents = {}; 3040 this.historyTimeoutInEffect = false; 3041 this.elementEvents = {}; 3042 this.editorEvents = {}; 3043 this.core = _core; 3044 3045 var initialize = function() { 3046 3047 element.setAttribute('contenteditable', 'true'); 3048 element.setAttribute('spellcheck', 'false'); 3049 3050 mutationSummaryObserver = new MutationSummary({ 3051 callback: function(){ 3052 3053 /** 3054 * This is not supposed to fire as we suspend 3055 * summary deliveries and only manually retrieve changes 3056 * when a mutation starts and finishes. 3057 * 3058 * However there might be a bug or some other unexpected 3059 * cause for this to fire. In that case we still handle 3060 * the callback. 3061 */ 3062 3063 _handleSummaryCallback.apply(this, arguments); 3064 3065 }, 3066 rootNode: _this.getElement(), 3067 oldPreviousSibling: true, 3068 observeOwnChanges: false, 3069 queries: [{ 3070 all: true 3071 }] 3072 }); 3073 3074 /* 3075 * Suspending summary delivery allows us to control when summaries are reported. 3076 * 3077 * By calling the generateMutationSummary function we can report all the changes that 3078 * occured to the editor's DOM since the last time generateMutationSummary was called. 3079 */ 3080 3081 mutationSummaryObserver.suspendDelivery(); 3082 3083 this.addEditorEventListener('mutation-start', function(editor, ignoreHistory) { 3084 3085 // every instance listens to this, but only the targeted instance should respond. 3086 if(editor === _this) { 3087 3088 executionDepth++; 3089 3090 // report any queued mutations that were caught in between our mutations, 3091 // could be DOM manipulations from another script or something. 3092 3093 if(ignoreHistory !== true){ 3094 _this.generateMutationSummary(); 3095 } 3096 3097 userInitiatedMutationInProgress = true; 3098 3099 } 3100 3101 }); 3102 3103 this.addEditorEventListener('mutation-success', function(editor, ignoreHistory) { 3104 3105 // every instance listens to this, but only the targeted instance should respond. 3106 if(editor === _this) { 3107 3108 // report any queued mutations that have not been reported yet 3109 if(ignoreHistory !== true){ 3110 _this.generateMutationSummary(); 3111 } 3112 3113 if(executionDepth <= 1) { 3114 userInitiatedMutationInProgress = false; 3115 } else { 3116 console.info('Nested mutation ended (stack size: ' + executionDepth + '). Not ending userInitiated state.'); 3117 } 3118 3119 } 3120 3121 }); 3122 3123 this.addEditorEventListener('mutation-end', function(editor, ignoreHistory) { 3124 3125 executionDepth--; 3126 3127 }); 3128 3129 this.addEditorEventListener('editor-content-change', function(editor, summary, userGenerated) { 3130 3131 // if nodes are added through a user interaction: mark them as saveable. 3132 if(editor === _this && userGenerated === true) { 3133 3134 summary.added.forEach(function(addedNode){ 3135 3136 addedNode.setSaveable(true); 3137 3138 if(addedNode.childNodes.length > 0){ 3139 3140 helpers.getAllDescendants(addedNode, function(childNode){ 3141 childNode.setSaveable(true); 3142 }); 3143 3144 } 3145 3146 }); 3147 3148 if(summary.added.length > 0 || summary.characterDataChanged.length > 0){ 3149 _core.events.trigger('cursor-activity', editor); 3150 } 3151 3152 } 3153 3154 _this.checkIfEmpty(); 3155 3156 }); 3157 3158 // setup Dragster 3159 dragster = new _core.Dragster( element ); 3160 3161 this.addElementEventListener( "dragster:enter", function (event) { 3162 3163 event.dataTransfer = event.detail.dataTransfer; 3164 _core.events.trigger('editor-dragin', _this, event); 3165 3166 }); 3167 3168 this.addElementEventListener( "dragster:leave", function (event) { 3169 3170 event.dataTransfer = event.detail.dataTransfer; 3171 _core.events.trigger('editor-dragout', _this, event); 3172 3173 }); 3174 3175 /** 3176 * DOM Events 3177 */ 3178 this.addElementEventListener('paste', function(event){ 3179 3180 _core.events.trigger('editor-paste', _this, event); 3181 3182 }); 3183 3184 this.addElementEventListener('dragenter', function(event){ 3185 3186 _core.events.trigger('editor-dragenter', _this, event); 3187 3188 }); 3189 3190 this.addElementEventListener('dragleave', function(event){ 3191 3192 _core.events.trigger('editor-dragleave', _this, event); 3193 3194 }); 3195 3196 this.addElementEventListener('dragstart', function(event){ 3197 3198 _core.events.trigger('editor-dragstart', _this, event); 3199 3200 }); 3201 3202 this.addElementEventListener('dragover', function(event){ 3203 3204 _core.events.trigger('editor-dragover', _this, event); 3205 3206 }); 3207 3208 this.addElementEventListener('dragend', function(event){ 3209 3210 _core.events.trigger('editor-dragend', _this, event); 3211 3212 }); 3213 3214 this.addElementEventListener('drop', function(event){ 3215 3216 console.log('drop', _this, event); 3217 3218 _core.events.trigger('editor-drop', _this, event); 3219 3220 event.preventDefault(); 3221 3222 }); 3223 3224 this.addElementEventListener('mouseenter', function(event){ 3225 3226 _core.events.trigger('editor-mouseenter', _this, event); 3227 3228 }); 3229 3230 this.addElementEventListener('mouseleave', function(event){ 3231 3232 _core.events.trigger('editor-mouseleave', _this, event); 3233 3234 }); 3235 3236 this.addElementEventListener('click', function(event){ 3237 3238 _core.events.trigger('editor-click', _this, event); 3239 3240 }); 3241 3242 this.addElementEventListener('mouseup', function(event){ 3243 3244 /** 3245 * Fires when a the cursor has moved. 3246 * 3247 * @event CargoEditor#cursor-activity 3248 * @property {CargoEditor.Editor} editor The editor instance containing the cursor 3249 */ 3250 3251 _core.events.trigger('cursor-activity', _this); 3252 3253 }); 3254 3255 this.addElementEventListener('focus', function() { 3256 3257 /** 3258 * Fires when an editor recieves focus. 3259 * 3260 * @event CargoEditor#editor-activated 3261 * @property {CargoEditor.Editor} editor The editor instance that's receiving focus 3262 */ 3263 3264 _core.events.trigger('editor-activated', _this); 3265 isActive = true; 3266 3267 }); 3268 3269 this.addElementEventListener('blur', function() { 3270 3271 /** 3272 * Fires when an editor loses focus. 3273 * 3274 * @event CargoEditor#editor-deactivated 3275 * @property {CargoEditor.Editor} editor The editor instance that's losing focus 3276 */ 3277 3278 _core.events.trigger('editor-deactivated', _this); 3279 isActive = false;
3280 3281 }); 3282 3283 // keyPresses are handled by the browser's contentEditable implementation, 3284 // however we need to fire a mutation event for them too. 3285 3286 this.addElementEventListener('keypress', function(event) { 3287 3288 // in case keyPress fires before keyUp occured. This happens 3289 // when a key is pressed and the previous key hasn't been released yet. 3290 if(emulatingMutation === true){ 3291 3292 _core.events.trigger('mutation-end', _this); 3293 _core.events.trigger('mutation-success', _this, _this.historyTimeoutInEffect); 3294 3295 } 3296 3297 emulatingMutation = true; 3298 userInitiatedMutationInProgress = true; 3299 3300 _core.events.trigger('mutation-start', _this, _this.historyTimeoutInEffect); 3301 3302 // IE doesn't support the input event, so we 3303 // do it like this. 3304 setTimeout(function(){ 3305 3306 // this can replace the 'input' event callback 3307 3308 }); 3309 3310 }); 3311 3312 this.addElementEventListener('input', function(event){ 3313 3314 _this.checkIfEmpty(); 3315 3316 if(emulatingMutation === true){ 3317 3318 _core.events.trigger('mutation-end', _this, _this.historyTimeoutInEffect); 3319 _core.events.trigger('mutation-success', _this, _this.historyTimeoutInEffect); 3320 3321 emulatingMutation = false; 3322 3323 } 3324 3325 if(_this.historyTimeoutInEffect === false){ 3326 3327 _this.historyTimeoutInEffect = true; 3328 3329 setTimeout(function(){ 3330 3331 _this.historyTimeoutInEffect = false; 3332 3333 }, historyDelay); 3334 3335 } 3336 }); 3337 3338 this.addElementEventListener('keydown', function(event) { 3339 3340 // if the arrow keys are pressed, we assume the cursor 3341 // is being moved 3342 switch (event.keyCode) { 3343 case 37: 3344 case 38: 3345 case 39: 3346 case 40: 3347 3348 // moving cursor with arrow keys 3349 setTimeout(function(){ 3350 _core.events.trigger('cursor-activity', _this); 3351 }); 3352 break; 3353 3354 case 65: // a 3355 3356 if(event.metaKey) { 3357 3358 // cmd/ctrl + a, select all. 3359 setTimeout(function(){ 3360 _core.events.trigger('cursor-activity', _this); 3361 }, 5); 3362 3363 } 3364 3365 break; 3366 3367 } 3368 3369 3370 }); 3371 3372 /** 3373 * Custom key listeners 3374 */ 3375 3376 // ignore the default contentEditable shortcuts 3377 this.addKeyListener('cmd + b / cmd + i', function(event){ 3378 3379 event.preventDefault(); 3380 3381 }); 3382 3383 this.addKeyListener('backspace', function(event){ 3384 3385 _core.execCommand('delete'); 3386 event.preventDefault(); 3387 3388 }); 3389 3390 this.addKeyListener('delete', function(event){ 3391 3392 _core.execCommand('forwardDelete'); 3393 event.preventDefault(); 3394 3395 }); 3396 3397 this.addKeyListener('tab', function(event){ 3398 3399 event.preventDefault(); 3400 3401 var in_code = false, 3402 selectedNodeList = helpers.getCurrentSelectionTree(event.target.parentNode).nodes, 3403 range = _core.getActiveRange(), 3404 i, nn; 3405 3406 for (i = 0; i < selectedNodeList.length; i++) { 3407 nn = selectedNodeList[i].nodeName.toLowerCase(); 3408 3409 if (nn === "code" || nn === "pre" || nn === "xpm") { 3410 in_code = true; 3411 break; 3412 } 3413 } 3414 3415 if(in_code){ 3416 3417 userInitiatedMutationInProgress = true; 3418 _core.execCommand('inserthtml', false, '\t'); 3419 3420 } else { 3421 3422 if(range !== null){ 3423 3424 if(range.collapsed){ 3425 3426 userInitiatedMutationInProgress = true; 3427 _core.execCommand('inserthtml', false, ' '); 3428 3429 } else { 3430 3431 userInitiatedMutationInProgress = true; 3432 _core.execCommand('indent'); 3433 3434 } 3435 3436 } else { 3437 3438 userInitiatedMutationInProgress = true; 3439 _core.execCommand('indent'); 3440 3441 } 3442 3443 } 3444 3445 }); 3446 3447 this.addKeyListener('shift+tab', function(event){ 3448 3449 event.preventDefault(); 3450 3451 // command ain't workin. 3452 //_core.execCommand('outdent'); 3453 3454 _core.mutationManager.execute(function(){ 3455 3456 var range = _core.getActiveRange(); 3457 3458 if(range !== undefined){ 3459 3460 var parent = range.commonAncestorContainer; 3461 3462 while(parent.parentNode !== null){ 3463 3464 if(parent.nodeName === "BLOCKQUOTE"){ 3465 3466 helpers.unwrap(parent, true); 3467 break; 3468 3469 } 3470 3471 parent = parent.parentNode; 3472 } 3473 3474 } 3475 3476 }); 3477 3478 }); 3479 3480 this.addKeyListener('enter', function(event){ 3481 3482 var in_list = false, 3483 in_code = false, 3484 selectedNodeList = helpers.getCurrentSelectionTree(event.target.parentNode).nodes, 3485 i, nn; 3486 3487 for (i = 0; i < selectedNodeList.length; i++) { 3488 nn = selectedNodeList[i].nodeName.toLowerCase(); 3489 3490 if (nn === "li" || nn === "ul" || nn == "ol") { in_list = true; } 3491 if (nn === "code" || nn === "pre" || nn === "xpm") { in_code = true; } 3492 } 3493 3494 if (event.keyCode === 13 && !in_list) { 3495 3496 if(in_code) { 3497 3498 userInitiatedMutationInProgress = true; 3499 _core.execCommand('inserthtml', false, '\n'); 3500 3501 } else { 3502 3503 userInitiatedMutationInProgress = true; 3504 _core.execCommand('insertlinebreak'); 3505 3506 } 3507 3508 event.preventDefault(); 3509 event.stopPropagation(); 3510 3511 } 3512 3513 }); 3514 3515 /** 3516 * Fires when a new editor has been instanciated 3517 * 3518 * @event CargoEditor#editor-instanciated 3519 * @property {CargoEditor.Editor} editor The instanciated editor 3520 */ 3521 3522 _core.events.trigger('editor-instanciated', _this); 3523 3524 this.checkIfEmpty(); 3525 3526 return this; 3527 3528 }; 3529 3530 /* 3531 * Getters 3532 */ 3533 3534 /** 3535 * Returns the element that's being used as editor. 3536 * @method 3537 * @returns {Element} the DOM object for the element. 3538 */ 3539 3540 this.getElement = function() { 3541 return element; 3542 } 3543 3544 /** 3545 * Returns the element that's being used as editor. 3546 * @method 3547 * @returns {Element} the DOM object for the element. 3548 */ 3549 3550 this.getMutationSummaryObserver = function() { 3551 return mutationSummaryObserver; 3552 } 3553 3554 this.generateMutationSummary = function(){ 3555 return _handleSummaryCallback(mutationSummaryObserver.takeSummaries()); 3556 } 3557 3558 /** 3559 * Returns true if the editor is in focus and false if it's not. 3560 * @method 3561 * @returns {Boolean} 3562 */ 3563 3564 this.isActive = function() { 3565 return isActive; 3566 } 3567 3568 /** 3569 * Returns saveable nodes. 3570 * @method 3571 * @returns {String} 3572 * 3573 */ 3574
3575 this.getSaveableNodes = function() { 3576 3577 var _this = this, 3578 frag = document.createDocumentFragment(), 3579 nodesToUnwrap = [], 3580 nodesToRemove = []; 3581 3582 frag.appendChild(element.persistCloneNode(true)); 3583 3584 // postpone removing and unwrapping to not mess with the structure while 3585 // traversing it. 3586 helpers.getAllDescendants(frag, function(child){ 3587 if(child.isSaveable() === false){ 3588 if(child.childNodes.length > 0) { 3589 nodesToUnwrap.push(child); 3590 } else { 3591 nodesToRemove.push(child); 3592 } 3593 } 3594 }); 3595 3596 for(var i = nodesToUnwrap.length - 1; i >= 0; i--){ 3597 helpers.unwrap(nodesToUnwrap[i], true); 3598 } 3599 3600 for(var i = 0; i < nodesToRemove.length; i++){ 3601 nodesToRemove[i].parentNode.removeChild(nodesToRemove[i]); 3602 } 3603 3604 return frag; 3605 3606 }, 3607 3608 this.markAsClean = function(){ 3609 3610 _core.events.trigger('editor-mark-clean', _this); 3611 3612 }, 3613 3614 this.checkIfEmpty = function(){ 3615 3616 // if an image is present, the editor is not empty. 3617 var containsVisibleElement = false; 3618 3619 helpers.getAllDescendants(element, function(node){ 3620 if(node.nodeName === "IMG"){ 3621 containsVisibleElement = true; 3622 } 3623 }); 3624 3625 if( ! /[^\t\n\r\s]/.test(element.textContent) && ! containsVisibleElement ){ 3626 3627 /** 3628 * @event editor-no-content 3629 * 3630 * Fires when the editor contains no text or just whitespace. 3631 */ 3632 3633 if(isEmpty === false){ 3634 3635 isEmpty = true; 3636 _this.moveCaret(); 3637 _core.events.trigger('editor-no-content', _this); 3638 3639 } 3640 3641 } else { 3642 3643 if(isEmpty === true){ 3644 3645 isEmpty = false; 3646 3647 /** 3648 * @event editor-has-content 3649 * 3650 * Fires when the editor used to contain no text or just whitespace and now has content 3651 */ 3652 3653 _core.events.trigger('editor-has-content', _this); 3654 3655 } 3656 3657 } 3658 3659 }, 3660 3661 this.moveCaret = function(node, index){ 3662 3663 if(node === undefined) { 3664 node = element.firstChild; 3665 } 3666 3667 if(node === null){ 3668 node = element.lastChild; 3669 } 3670 3671 if(isNaN(index)){ 3672 index = 0; 3673 } 3674 3675 var range = _core.rangy.createRange(); 3676 3677 if(node.nodeType === Node.TEXT_NODE){ 3678 3679 range.setStart(node, index); 3680 3681 } else { 3682 3683 if(index > 0) { 3684 range.setStartAfter(node); 3685 } else { 3686 range.setStartBefore(node); 3687 } 3688 3689 } 3690 3691 range.collapse(true); 3692 3693 var sel = _core.rangy.getSelection(); 3694 sel.setSingleRange(range); 3695 3696 _core.events.trigger('cursor-activity', _this); 3697 3698 }, 3699 3700 this.moveCaretToEnd = function(){ 3701 3702 var range = _core.rangy.createRange(); 3703 range.selectNode(element.lastChild); 3704 range.collapse(false); 3705 3706 var sel = _core.rangy.getSelection(); 3707 sel.setSingleRange(range); 3708 3709 _core.events.trigger('cursor-activity', _this); 3710 3711 }, 3712 3713 this.scrollIntoView = function(){ 3714 3715 /*var range; 3716 3717 if((range = _core.getActiveRange()) === undefined){ 3718 return; 3719 } 3720 3721 var parent = range.commonAncestorContainer, 3722 inEditor = false; 3723 3724 while(parent.parentNode !== null){ 3725 3726 if(parent === element){ 3727 inEditor = true; 3728 break; 3729 } 3730 3731 parent = parent.parentNode; 3732 } 3733 3734 if( ! inEditor ) { 3735 return; 3736 } 3737 3738 var cursorPosition = helpers.getCursorCoordinates(), 3739 windowHeight = window.innerHeight; 3740 3741 console.log(cursorPosition, windowHeight);*/ 3742 3743 }, 3744 3745 /** 3746 * Returns the all HTML inside the editor as a string. 3747 * @method 3748 * @returns {String} 3749 */ 3750 3751 this.getHTML = function() { 3752 return element.innerHTML; 3753 } 3754 3755 /** 3756 * Remove self from editor list and all other references, 3757 * unbind all event listeners, 3758 * release mutation observer, 3759 * remove contenteditable and other attributes 3760 */ 3761 3762 this.destroy = function(){ 3763 3764 if( this.destroyed === true ) { 3765 return; 3766 } 3767 3768 _core.events.trigger('editor-before-destroy', _this); 3769 3770 var key; 3771 3772 // Remove key codes 3773 for(key in this.keyEvents){ 3774 this.removeKeyListener(key); 3775 } 3776 3777 // remove DOM event listeners 3778 for(key in this.elementEvents){ 3779 3780 while(this.elementEvents[key] !== undefined && this.elementEvents[key].length > 0){ 3781 this.removeElementEventListener(key, this.elementEvents[key][0]); 3782 } 3783 3784 } 3785 3786 // remove Editor event listeners 3787 for(key in this.editorEvents){ 3788 3789 while(this.editorEvents[key] !== undefined && this.editorEvents[key].length > 0){ 3790 this.removeEditorEventListener(key, this.editorEvents[key][0]); 3791 } 3792 3793 } 3794 3795 // dragster 3796 if(dragster !== null){ 3797 dragster.removeListeners(); 3798 dragster = null; 3799 } 3800 3801 // MutationSummary 3802 if(mutationSummaryObserver !== null){ 3803 mutationSummaryObserver.disconnect(); 3804 mutationSummaryObserver = null; 3805 } 3806 3807 // attributes 3808 element.removeAttribute('contenteditable'); 3809 element.removeAttribute('spellcheck'); 3810 3811 _core.events.trigger('editor-destroyed', _this); 3812 3813 this.destroyed = true; 3814 3815 } 3816 3817 return initialize.call(this); 3818 3819 }; 3820 3821 helpers.extend(Editor.prototype, { 3822 3823 addKeyListener: function(key, callBack){ 3824 3825 if(this.keyEvents.hasOwnProperty(key)){ 3826 this.removeKeyListener(key); 3827 } 3828 3829 var callBack = _core.jwerty.event(key, callBack, this); 3830 3831 this.addElementEventListener('keydown', callBack); 3832 3833 this.keyEvents[key] = callBack; 3834 3835 3836 }, 3837 3838 removeKeyListener: function(key){ 3839 3840 if(this.keyEvents.hasOwnProperty(key)){ 3841 3842 var callBack = this.keyEvents[key]; 3843 3844 this.removeElementEventListener('keydown', callBack); 3845 3846 delete this.keyEvents[key]; 3847 3848 } 3849 3850 }, 3851 3852 /** 3853 * Adds an event listener to the editor's element 3854 */ 3855 3856 addElementEventListener: function(name, callback) { 3857 3858 if( ! this.elementEvents.hasOwnProperty(name) ) { 3859 this.elementEvents[name] = []; 3860 } 3861 3862 if( this.elementEvents[name].indexOf(callback) === -1 ){ 3863 3864 this.elementEvents[name].push(callback); 3865 3866 } 3867 3868 return this.getElement().addEventListener(name, callback, false); 3869 3870 }, 3871 3872 /** 3873 * Removes an event listener from the editor's element 3874 */ 3875 3876 removeElementEventListener: function(name, callback) { 3877 3878 if( this.elementEvents.hasOwnProperty(name) ) { 3879 3880 var callbackIndex = this.elementEvents[name].indexOf(callback); 3881 3882 if( callbackIndex !== -1 ){ 3883 3884 this.elementEvents[name].splice(callbackIndex, 1); 3885 3886 if(this.elementEvents[name].length === 0){ 3887 delete this.elementEvents[name]; 3888 } 3889 3890 } 3891 3892 } 3893 3894 // attempt to remove even if it's not in our list. Just 3895 // in case the event wasn't added through addElementEventListener 3896 this.getElement().removeEventListener(name, callback); 3897 3898 callback = null; 3899 3900 }, 3901 3902 addEditorEventListener: function(name, callback) { 3903 3904 if( ! this.editorEvents.hasOwnProperty(name) ) { 3905 this.editorEvents[name] = []; 3906 } 3907 3908 if( this.editorEvents[name].indexOf(callback) === -1 ){ 3909 3910 this.editorEvents[name].push(callback); 3911 3912 } 3913 3914 this.core.events.on(name, callback); 3915 3916 }, 3917 3918 removeEditorEventListener: function(name, callback) { 3919 3920 if( this.editorEvents.hasOwnProperty(name) ) { 3921 3922 var callbackIndex = this.editorEvents[name].indexOf(callback); 3923 3924 if( callbackIndex !== -1 ){ 3925 3926 this.editorEvents[name].splice(callbackIndex, 1); 3927 3928 if(this.editorEvents[name].length === 0){ 3929 delete this.editorEvents[name]; 3930 } 3931 3932 } 3933 3934 } 3935 3936 this.core.events.off(name, callback); 3937 3938 callback = null; 3939 3940 } 3941 3942 }); 3943 3944(function(){ 3945 3946 /** 3947 * The MutationManager wraps function calls (like execCommand) with some extra functionality. 3948 * 3949 * Listen to mutation events by 3950 * 3951 * @class 3952 * @private 3953 * @memberOf CargoEditor 3954 */ 3955 3956 var MutationManager = function(){}; 3957 3958 helpers.extend(MutationManager.prototype, { 3959 3960 /** 3961 * Executes a mutation 3962 * @method 3963 * @param {Function} mutation The mutation 3964 * @param {Array} args Arguments passed to the subscriptions 3965 * 3966 * @memberOf CargoEditor.MutationManager 3967 */ 3968 3969 execute: function(mutation, ignoreHistory){ 3970 3971 ignoreHistory = !!ignoreHistory; 3972 3973 this.before(ignoreHistory); 3974 3975 if (mutation) { 3976 if(mutation() === false) { 3977 this.error(ignoreHistory); 3978 } else { 3979 this.success(ignoreHistory); 3980 } 3981 } 3982 3983 this.always(ignoreHistory); 3984 3985 }, 3986 3987 /** 3988 * Fires before anything has changed. 3989 * 3990 * @memberOf CargoEditor.MutationManager 3991 */ 3992 3993 before: function(ignoreHistory){ 3994 /** 3995 * Fires before a mutation executes 3996 * 3997 * @event CargoEditor#mutation-start 3998 * @property {CargoEditor.EditorInstance} editor The editor on which the mutation is applied 3999 */ 4000 4001 _core.events.trigger('mutation-start', _core.getActiveEditor(), ignoreHistory); 4002 }, 4003 4004 /** 4005 * Always fires regardless of mutation outcome. 4006 * 4007 * @memberOf CargoEditor.MutationManager 4008 */ 4009 4010 always: function(ignoreHistory){ 4011 4012 /** 4013 * Fires when a mutation is done. Regardless if it failed or succeeded. 4014 * 4015 * @event CargoEditor#mutation-end 4016 * @property {CargoEditor.EditorInstance} editor The editor on which the mutation is applied 4017 */ 4018 4019 _core.events.trigger('mutation-end', _core.getActiveEditor(), ignoreHistory); 4020 }, 4021 4022 /** 4023 * Fires if the mutation threw an exception. 4024 * 4025 * @memberOf CargoEditor.MutationManager 4026 */ 4027 4028 error: function(ignoreHistory){ 4029 console.warn(getLocalization('mutation-fail')); 4030 4031 /** 4032 * Fires when a mutation failed. 4033 * 4034 * @event CargoEditor#mutation-fail 4035 * @property {CargoEditor.EditorInstance} editor The editor on which the mutation is applied 4036 */ 4037 4038 _core.events.trigger('mutation-fail', _core.getActiveEditor(), ignoreHistory); 4039 }, 4040 4041 /** 4042 * Fires if the mutation completed without errors. Fires before always. 4043 * 4044 * @memberOf CargoEditor.MutationManager 4045 */ 4046 4047 success: function(ignoreHistory){ 4048 4049 /** 4050 * Fires when a mutation was a success. 4051 * 4052 * @event CargoEditor#mutation-success 4053 * @property {CargoEditor.EditorInstance} editor The editor on which the mutation is applied 4054 */ 4055 4056 _core.events.trigger('mutation-success', _core.getActiveEditor(), ignoreHistory); 4057 } 4058 }); 4059 4060 this.mutationManager = new MutationManager; 4061 4062}).call(this); 4063 4064// Generated by CoffeeScript 1.6.3 4065(function() { 4066 var Dragster, 4067 __bind = function(fn, me) { 4068 return function() { 4069 return fn.apply(me, arguments); 4070 }; 4071 }; 4072 4073 Dragster = (function() { 4074 function Dragster(el) { 4075 this.el = el; 4076 this.dragleave = __bind(this.dragleave, this); 4077 this.dragenter = __bind(this.dragenter, this); 4078 4079 var _this = this; 4080 4081 if (this.supportsEventConstructors()) { 4082 this.first = false;
4083 this.second = false; 4084 this.el.addEventListener("dragenter", this.dragenter, false); 4085 this.el.addEventListener("dragleave", this.dragleave, false); 4086 this.el.addEventListener("drop", function(){ 4087 _this.first = false; 4088 _this.second = false; 4089 }, false); 4090 } 4091 } 4092 4093 Dragster.prototype.dragenter = function(event) { 4094 4095 if (this.first) { 4096 return this.second = true; 4097 } else { 4098 this.first = true; 4099 4100 return this.el.dispatchEvent(new CustomEvent('dragster:enter', { 4101 bubbles: true, 4102 cancelable: true, 4103 detail: { 4104 dataTransfer: event.dataTransfer, 4105 originalEvent: event 4106 } 4107 })); 4108 } 4109 4110 }; 4111 4112 Dragster.prototype.dragleave = function(event) { 4113 4114 if (this.second) { 4115 this.second = false; 4116 } else if (this.first) { 4117 this.first = false; 4118 } 4119 if (!this.first && !this.second) { 4120 return this.el.dispatchEvent(new CustomEvent('dragster:leave', { 4121 bubbles: true, 4122 cancelable: true, 4123 detail: { 4124 dataTransfer: event.dataTransfer, 4125 originalEvent: event 4126 } 4127 })); 4128 } 4129 4130 }; 4131 4132 Dragster.prototype.removeListeners = function() { 4133 this.el.removeEventListener("dragenter", this.dragenter, false); 4134 return this.el.removeEventListener("dragleave", this.dragleave, false); 4135 }; 4136 4137 Dragster.prototype.supportsEventConstructors = function() { 4138 try { 4139 new CustomEvent("z"); 4140 } catch (_error) { 4141 return false; 4142 } 4143 return true; 4144 }; 4145 4146 Dragster.prototype.reset = function() { 4147 this.first = false; 4148 return this.second = false; 4149 }; 4150 4151 return Dragster; 4152 4153 })(); 4154 4155 this.Dragster = Dragster; 4156 4157}).call(this); 4158 4159(function(){ 4160 4161 /** 4162 * The following code is the work of Aryeh Gregor and released under the CC0 1.0 Universal license. (http://creativecommons.org/publicdomain/zero/1.0/) 4163 * 4164 * https://github.com/ayg/editing 4165 * 4166 * Some modifications by the Aloha-Editor project 4167 * https://github.com/alohaeditor/Aloha-Editor/blob/dev/src/lib/aloha/engine.js 4168 * 4169 */ 4170 4171 4172 "use strict"; 4173 4174 var mapUtil = {}, 4175 enableContentEditableDeletion = true, 4176 ContentRules = {}; 4177 4178 (function(){ 4179 4180 'use strict'; 4181 4182 /** 4183 * White list node names for list elements. 4184 * @type {Array.<string>} 4185 */ 4186 var LIST_WHITELIST_NODE_NAMES = ['li']; 4187 4188 /** 4189 * White list node names for table elements. 4190 * @type {Array.<string>} 4191 */ 4192 var TABLE_WHITELIST_NODE_NAMES = ['caption', 'colgroup', 'col', 'thead', 'tbody', 'tfoot', 'td', 'th']; 4193 4194 /** 4195 * Add default rules for some elements. 4196 * @param {Object.<string, Array.<string>>} 4197 * @return {Object.<string, Array.<string>>} 4198 */ 4199 function addDefaultRules(mapRules) { 4200 var selector; 4201 for (selector in mapRules) { 4202 if (mapRules.hasOwnProperty(selector)) { 4203 if (Arrays.contains(mapRules[selector], 'ol') || Arrays.contains(mapRules[selector], 'ul')) { 4204 mapRules[selector] = Arrays.concat(mapRules[selector], LIST_WHITELIST_NODE_NAMES); 4205 } 4206 if (Arrays.contains(mapRules[selector], 'table')) { 4207 mapRules[selector] = Arrays.concat(mapRules[selector], TABLE_WHITELIST_NODE_NAMES); 4208 } 4209 } 4210 } 4211 return mapRules; 4212 } 4213 4214 /** 4215 * Whitelist rules. 4216 * 4217 * @private 4218 * @type {Object.<string, Array.<string>>} 4219 */ 4220 var whitelist = addDefaultRules({}); 4221 4222 /** 4223 * Blacklist rules. 4224 * 4225 * @private 4226 * @type {Object.<string, Array.<string>>} 4227 */ 4228 var blacklist = {}; 4229 4230 /** 4231 * Translation rules. 4232 * 4233 * @private 4234 * @type {Object<string, string>} 4235 */ 4236 var translations = {}; 4237 4238 /** 4239 * Retrieves a list of all rules in a specified table that are applicable 4240 * the given editable. 4241 * 4242 * @private 4243 * @param {Element} editable 4244 * @param {Object<string, Array.<string>}} table 4245 */ 4246 function getRules(editable, table) { 4247 var $editable = $(editable); 4248 var rules = []; 4249 var selector; 4250 for (selector in table) { 4251 if (table.hasOwnProperty(selector) && $editable.is(selector)) { 4252 rules.push(table[selector]); 4253 } 4254 } 4255 return rules; 4256 } 4257 4258 /** 4259 * Concatenates the given list of lists into a single set. 4260 * 4261 * @private 4262 * @param {Array.<Array<string>>} lists 4263 * @return {Array.<string>} 4264 */ 4265 function setcat(lists) { 4266 var result = []; 4267 $.each(lists, function (index, item) { 4268 result = result.concat(item); 4269 }); 4270 $.unique(result); 4271 return result; 4272 } 4273 4274 /** 4275 * These element's cannot be simply unwrapped because they have dependent 4276 * children. 4277 * 4278 * @private 4279 * @see GROUPED_ELEMENTS 4280 * @type {<string, boolean>} 4281 */ 4282 var GROUP_CONTAINERS = { 4283 FIELDSET : true, 4284 OBJECT : true, 4285 FIGURE : true, 4286 AUDIO : true, 4287 SELECT : true, 4288 COLGROUP : true, 4289 HGROUP : true, 4290 TABLE : true, 4291 TBODY : true, 4292 TR : true, 4293 OL : true, 4294 UL : true, 4295 DL : true, 4296 MENU : true 4297 }; 4298 4299 /** 4300 * These element's cannot be simply unwrapped because their parents only 4301 * allow these as their immediate child nodes. 4302 * 4303 * @private 4304 * @see GROUP_CONTAINERS 4305 * @type {<string, Array.<string>} 4306 */ 4307 var GROUPED_ELEMENTS = { 4308 LI : ['OL', 'UL', 'DL'], 4309 DT : ['DL'], 4310 DD : ['DL'], 4311 TBODY : ['TABLE'], 4312 TR : ['TABLE', 'TBODY'], 4313 TH : ['TABLE', 'TBODY'], 4314 TD : ['TR', 'TH'] 4315 }; 4316 4317 /** 4318 * Checks whether nodes of the specified nodeName are allowed in the given 4319 * editable. 4320 * 4321 * @param {Element} editable 4322 * @param {string} nodeName 4323 * @return {boolean} 4324 */ 4325 function isAllowed(editable, nodeName) { 4326 var white = getRules(editable, whitelist); 4327 // Because if no rules are configured for this editable then permit all 4328 if (white.length > 0) { 4329 // Because textnode are always to be permitted by default. They 4330 // must be explicitly blacklisted if undesired 4331 if (!Arrays.contains(setcat(['#text'].concat(white)), nodeName.toLowerCase())) { 4332 return false;
4333 } 4334 } 4335 var black = getRules(editable, blacklist); 4336 if (black.length > 0) { 4337 return !Arrays.contains(setcat(black), nodeName.toLowerCase()); 4338 } 4339 return true; 4340 } 4341 4342 /** 4343 * Translates nodes from one name to another (eg: i to em) if translation is 4344 * configured for the given editable. 4345 * 4346 * @param {Element} editable 4347 * @param {string} nodeName 4348 * @return {string} Translated nodeName 4349 */ 4350 function translate(editable, nodeName) { 4351 var rules = $.extend.apply({}, getRules(editable, translations)); 4352 return rules[nodeName.toLowerCase()] || nodeName; 4353 }; 4354 4355 function forward(node) { 4356 if (node.firstChild) { 4357 return node.firstChild; 4358 } 4359 var next = node; 4360 while (next && !next.nextSibling) { 4361 next = next.parentNode; 4362 } 4363 return next && next.nextSibling; 4364 }; 4365 4366 function backward(node) { 4367 var prev = node.previousSibling; 4368 while (prev && prev.lastChild) { 4369 prev = prev.lastChild; 4370 } 4371 return prev || node.parentNode; 4372 }; 4373 4374 function removeShallow(node) { 4375 var parent = node.parentNode; 4376 moveNextAll(parent, node.firstChild, node); 4377 parent.removeChild(node); 4378 }; 4379 4380 function moveNextAll(parent, firstChild, refChild) { 4381 while (firstChild) { 4382 var nextChild = firstChild.nextSibling; 4383 parent.insertBefore(firstChild, refChild); 4384 firstChild = nextChild; 4385 } 4386 }; 4387 4388 /** 4389 * Given a string of html content to be inserted in a specified editable 4390 * element, will strip away any elements which are disallowed and translate 4391 * any according to the Content Rules configuration. 4392 * 4393 * @param {string} content 4394 * @param {Element} editable 4395 * @return {string} 4396 */ 4397 function applyRules(content, editable) { 4398 4399 var doc = editable.ownerDocument; 4400 var container = doc.createElement('div'); 4401 4402 container.innerHTML = content; 4403 4404 var node = forward(container); 4405 4406 while (node) { 4407 //skip over node if it is a aloha-block, because we want to keep elements inside 4408 if ($(node).hasClass('aloha-block')) { 4409 node = node.nextSibling || forward(node.lastChild || node); 4410 } else { 4411 var translation = translate(editable, node.nodeName); 4412 if (translation !== node.nodeName) { 4413 var replacement = doc.createElement(translation); 4414 replacement.innerHTML = node.innerHTML; 4415 node.parentNode.replaceChild(replacement, node); 4416 node = replacement; 4417 } 4418 if (isAllowed(editable, node.nodeName)) { 4419 node = forward(node); 4420 } else if (GROUP_CONTAINERS[node.nodeName] || GROUPED_ELEMENTS[node.nodeName]) { 4421 // Because `node` is being entirely removed, we skip over, and 4422 // do not descend its subtree 4423 var prev = backward(node); 4424 node.parentNode.removeChild(node); 4425 node = forward(prev); 4426 } else { 4427 var next = forward(node); 4428 removeShallow(node); 4429 node = next; 4430 } 4431 } 4432 } 4433 return container.innerHTML; 4434 } 4435 4436 this.isAllowed = isAllowed; 4437 this.translate = translate; 4438 this.applyRules = applyRules; 4439 4440 }).call(ContentRules); 4441 4442 console.log(ContentRules); 4443 4444 (function(){ 4445 4446 /** 4447 * Checks whether the given object has no own or inherited properties. 4448 * 4449 * @param {!Object} obj The object to check. 4450 * @return {boolean} True if the object is empty. eg: isEmpty({}) == true 4451 */ 4452 this.isEmpty = function(obj) { 4453 var name; 4454 for (name in obj) { 4455 if (obj.hasOwnProperty(name)) { 4456 return false; 4457 } 4458 } 4459 return true; 4460 } 4461 4462 /** 4463 * Fill the given map with the given keys mapped to the given value. 4464 * 4465 * @param map 4466 * The given map will have one entry added for each given key. 4467 * @param keys 4468 * An array of string keys. Javascript mapUtil can only 4469 * contain string keys, so these must be strings or 4470 * or they will be cast to string. 4471 * @param value 4472 * A single value that each given key will map to. 4473 * @return 4474 * The given map. 4475 */ 4476 this.fillKeys = function(map, keys, value) { 4477 var i = keys.length; 4478 while (i--) { 4479 map[keys[i]] = value; 4480 } 4481 return map; 4482 } 4483 4484 /** 4485 * Fill the given map with entries from the given tuples. 4486 * 4487 * @param map 4488 * The given map will have one entry added for each item in 4489 * the given array. 4490 * @param tuples 4491 * An array of [key, value] tuples. Javascript mapUtil can only 4492 * contain string keys, so the keys must be strings or 4493 * or they will be cast to string. 4494 * @return 4495 * The given map. 4496 */ 4497 this.fillTuples = function(map, tuples) { 4498 var i = tuples.length, 4499 tuple; 4500 while (i--) { 4501 tuple = tuples[i]; 4502 map[tuple[0]] = tuple[1]; 4503 } 4504 return map; 4505 } 4506 4507 /** 4508 * Returns an array of the map's keys. 4509 */ 4510 this.keys = function(map) { 4511 var ks = [], 4512 k; 4513 for (k in map) { 4514 if (map.hasOwnProperty(k)) { 4515 ks.push(k); 4516 } 4517 } 4518 return ks; 4519 } 4520 4521 /** 4522 * For each mapping, call cb(value, key, map). 4523 * 4524 * Emulates ECMAScript edition 5 Array.forEach. 4525 * 4526 * Contrary to "for (key in map)" iterates only over the 4527 * "hasOwnProperty" properties of the map, which is usually what you 4528 * want. 4529 */ 4530 this.forEach = function(map, cb) { 4531 var key; 4532 for (key in map) { 4533 if (map.hasOwnProperty(key)) { 4534 cb(map[key], key, map); 4535 } 4536 } 4537 } 4538 4539 4540 }).call(mapUtil); 4541 4542 var blockLevelElements = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'div', 'pre'], 4543 listElements = ['li', 'ol', 'ul'],
4544 nonEmptyTags = ['br']; 4545 4546 function isBlockLevelElement(node) { 4547 if (!node) { 4548 return false; 4549 } 4550 if (node.nodeType === 1 && jQuery.inArray(node.nodeName.toLowerCase(), blockLevelElements) >= 0) { 4551 return true; 4552 } 4553 return false; 4554 } 4555 4556 function isEmptyBlockLevelElement(node) { 4557 4558 if (!isBlockLevelElement(node)) { 4559 return false; 4560 } 4561 4562 if (!node.childNodes) { 4563 return true; 4564 } 4565 4566 var len = node.childNodes.length, breakFound = false, i, child; 4567 for (i = 0; i < len; i++) { 4568 child = node.childNodes[i]; 4569 if (child.nodeName === 'BR') { 4570 if (breakFound) { 4571 // we found another break, so the element is not empty 4572 return false; 4573 } else { 4574 // first break found, no harm done (so far) 4575 breakFound = true; 4576 } 4577 } else if (!isEmptyNode(child)) { 4578 return false; 4579 } 4580 } 4581 4582 return true; 4583 4584 } 4585 4586 function isEmptyNode(domObject) { 4587 // a non dom object is considered empty 4588 if (!domObject) { 4589 return true; 4590 } 4591 4592 // some tags are considered to be non-empty 4593 if (jQuery.inArray(domObject.nodeName.toLowerCase(), nonEmptyTags) != -1) { 4594 return false; 4595 } 4596 4597 // text nodes are not empty, if they contain non-whitespace characters 4598 if (domObject.nodeType === 3) { 4599 if (domObject.data.search(/\S/) == -1) { 4600 return true; 4601 } else if (domObject.data.length === 1 // Fix FOR IE no width chars 4602 && domObject.data.charCodeAt(0) >= 0x2000) { 4603 return true; 4604 } 4605 4606 return false; 4607 } 4608 4609 // all other nodes are not empty if they contain at least one child which is not empty 4610 var i, childNodes; 4611 for (i = 0, childNodes = domObject.childNodes.length; i < childNodes; ++i) { 4612 if (!isEmptyNode(domObject.childNodes[i])) { 4613 return false; 4614 } 4615 } 4616 4617 // found no contents, so the element is empty 4618 return true; 4619 } 4620 4621 /** 4622 * Gets the index of the given node within its parent element. 4623 * @param {Element} node 4624 * @return {number} 4625 * Index in the parent node or -1 if no node given or node has no 4626 * parent. 4627 */ 4628 4629 function getIndexInParent(node) { 4630 if (!node) { 4631 return -1; 4632 } 4633 4634 var i, 4635 childNodes = node.parentNode.childNodes, 4636 len = childNodes.length; 4637 for (i = 0; i < len; i++) { 4638 if (childNodes[i] === node) { 4639 return i; 4640 } 4641 } 4642 4643 return -1; 4644 } 4645 4646 var blocksTagnameMap = {}, 4647 BLOCKLEVEL_ELEMENTS = [ 4648 'address', 4649 'article', // HTML5 4650 'aside', // HTML5 4651 'audio', // HTML5 4652 'blockquote', 4653 'canvas', // HTML5 4654 'dd', 4655 'div', 4656 'dl', 4657 'fieldset', 4658 'figcaption', 4659 'figure', 4660 'footer', 4661 'form', 4662 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 4663 'header', 4664 'hgroup', 4665 'hr', 4666 'noscript', 4667 'ol', 4668 'output', 4669 'p', 4670 'pre', 4671 'section', // HTML5 4672 'table', 4673 'tfoot', 4674 'ul', 4675 'video' // HTML5 4676 ]; 4677 4678 mapUtil.fillKeys(blocksTagnameMap, BLOCKLEVEL_ELEMENTS, true); 4679 4680 mapUtil.fillKeys( 4681 blocksTagnameMap, 4682 Array.prototype.map.call(BLOCKLEVEL_ELEMENTS, function (str) { 4683 return str.toUpperCase(); 4684 }), 4685 true 4686 ); 4687 4688 function getInternetExplorerVersion() { 4689 var rv = -1; 4690 if (navigator.appName == 'Microsoft Internet Explorer') 4691 { 4692 var ua = navigator.userAgent; 4693 var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); 4694 if (re.exec(ua) != null) 4695 rv = parseFloat( RegExp.$1 ); 4696 } 4697 else if (navigator.appName == 'Netscape') 4698 { 4699 var ua = navigator.userAgent; 4700 var re = new RegExp("Trident/.*rv:([0-9]{1,}[\.0-9]{0,})"); 4701 if (re.exec(ua) != null) 4702 rv = parseFloat( RegExp.$1 ); 4703 } 4704 return rv; 4705 } 4706 4707 var isMSIE = getInternetExplorerVersion() !== -1; 4708 4709 function isBlock(node) { 4710 return blocksTagnameMap[node.nodeName]; 4711 } 4712 4713 function findNodeRight(node, condition) { 4714 while (node && !condition(node)) { 4715 node = node.previousSibling; 4716 } 4717 return node; 4718 } 4719 4720 function findNodeLeft(node, condition) { 4721 while (node && !condition(node)) { 4722 node = node.nextSibling; 4723 } 4724 return node; 4725 } 4726 4727 function getOffsetHeight(node) { 4728 if (!node) { 4729 return 0; 4730 } 4731 var offsetHeight = node.offsetHeight; 4732 if (offsetHeight === 0) { 4733 offsetHeight = node.offsetHeight; 4734 } 4735 return offsetHeight; 4736 } 4737 4738 function isHtmlElement(node, tags) { 4739 if (typeof tags == "string") { 4740 tags = [tags]; 4741 } 4742 if (typeof tags == "object") { 4743 tags = tags.map(function(tag) { return tag.toUpperCase() }); 4744 } 4745 return node 4746 && node.nodeType == Node.ELEMENT_NODE 4747 && isHtmlNamespace(node.namespaceURI) 4748 && (typeof tags == "undefined" || tags.indexOf(node.tagName) != -1); 4749 } 4750 4751 var $_; 4752 4753 var shims = { 4754 // Function bind 4755 bind: function (owner) { 4756 var obj = this.obj || this; 4757 var native_method = Function.prototype.bind; 4758 var args = Array.prototype.slice.call(arguments, 1); 4759 4760 if (native_method) { 4761 return native_method.apply(obj, arguments); 4762 } 4763 return function () { 4764 return obj.apply(owner, arguments.length === 0 ? args : args.concat(Array.prototype.slice.call(arguments))); 4765 }; 4766 }, 4767 4768 // String trim 4769 trim: function () { 4770 var obj = this.obj || this; 4771 var native_method = String.prototype.trim; 4772 4773 if (native_method) { 4774 return native_method.call(obj); 4775 } 4776 return obj.replace(/^\s+/, '').replace(/\s+$/, ''); 4777 }, 4778 4779 // Array methods 4780 // i optional 4781 indexOf: function (find, i) { 4782 var obj = this.obj || this; 4783 var native_method = Array.prototype.indexOf; 4784 4785 if (native_method) { 4786 return native_method.call(obj, find, i); 4787 } 4788 if (i === undefined) { 4789 i = 0; 4790 } 4791 if (i < 0) { 4792 i += obj.length; 4793 } 4794 if (i < 0) { 4795 i = 0; 4796 } 4797 var n; 4798 for (n = obj.length; i < n; i++) { 4799 if (undefined !== obj[i] && obj[i] === find) { 4800 return i; 4801 } 4802 } 4803 return -1; 4804 }, 4805 4806 // that optional 4807 forEach: function (action, that) { 4808 var obj = this.obj || this; 4809 var native_method = Array.prototype.forEach; 4810 4811 if (native_method) { 4812 return native_method.call(obj, action, that); 4813 } 4814 var i, n; 4815 for (i = 0, n = obj.length; i < n; i++) { 4816 if (undefined !== obj[i]) { 4817 action.call(that, obj[i], i, obj); 4818 } 4819 } 4820 }, 4821 4822 // that optional 4823 // chain optional 4824 map: function (mapper, that, chain) { 4825 var obj = this.obj || this; 4826 var native_method = Array.prototype.map; 4827 var returnWrapper = (typeof arguments[arguments.length - 1] == "boolean") ? Array.prototype.pop.call(arguments) : false;
4828 var result = []; 4829 4830 if (native_method) { 4831 result = native_method.call(obj, mapper, that); 4832 } else { 4833 var other = []; 4834 var i, n; 4835 for (i = 0, n = obj.length; i < n; i++) { 4836 if (undefined !== obj[i]) { 4837 other[i] = mapper.call(that, obj[i], i, obj); 4838 } 4839 } 4840 result = other; 4841 } 4842 4843 return returnWrapper ? $_(result) : result; 4844 }, 4845 4846 // that optional 4847 // chain optional 4848 filter: function (filterFunc, that, chain) { 4849 var obj = this.obj || this; 4850 var native_method = Array.prototype.filter; 4851 var returnWrapper = (typeof arguments[arguments.length - 1] == "boolean") ? Array.prototype.pop.call(arguments) : false; 4852 var result = []; 4853 4854 if (native_method) { 4855 result = native_method.call(obj, filterFunc, that); 4856 } else { 4857 var other = [], 4858 v, 4859 i, 4860 n; 4861 for (i = 0, n = obj.length; i < n; i++) { 4862 if (undefined !== obj[i] && filterFunc.call(that, v = obj[i], i, obj)) { 4863 other.push(v); 4864 } 4865 } 4866 result = other; 4867 } 4868 4869 return returnWrapper ? $_(result) : result; 4870 }, 4871 4872 // that optional 4873 every: function (tester, that) { 4874 var obj = this.obj || this; 4875 var native_method = Array.prototype.every; 4876 4877 if (native_method) { 4878 return native_method.call(obj, tester, that); 4879 } 4880 var i, n; 4881 for (i = 0, n = obj.length; i < n; i++) { 4882 if (undefined !== obj[i] && !tester.call(that, obj[i], i, obj)) { 4883 return false; 4884 } 4885 } 4886 return true; 4887 }, 4888 4889 // that optional 4890 some: function (tester, that) { 4891 var obj = this.obj || this; 4892 var native_method = Array.prototype.some; 4893 4894 if (native_method) { 4895 return native_method.call(obj, tester, that); 4896 } 4897 var i, n; 4898 for (i = 0, n = obj.length; i < n; i++) { 4899 if (undefined !== obj[i] && tester.call(that, obj[i], i, obj)) { 4900 return true; 4901 } 4902 } 4903 return false; 4904 }, 4905 4906 // Since IE7 doesn't support 'hasAttribute' method on nodes 4907 // TODO: raise an exception if the object is not an node 4908 hasAttribute: function (attr) { 4909 var obj = this.obj || this; 4910 var native_method = obj.hasAttribute; 4911 4912 if (native_method) { 4913 return obj.hasAttribute(attr); 4914 } 4915 return !!obj.getAttribute(attr); 4916 } 4917 4918 }; 4919 4920 $_ = function (obj) { 4921 var Wrapper = function () {}; 4922 Wrapper.prototype = shims; 4923 4924 var wrapper_instance = new Wrapper(); 4925 wrapper_instance.obj = obj; 4926 return wrapper_instance; 4927 }; 4928 4929 var shim; 4930 for (shim in shims) { 4931 if (shims.hasOwnProperty(shim)) { 4932 $_[shim] = shims[shim]; 4933 } 4934 } 4935 4936 4937 // Node constants 4938 // http://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-1841493061 4939 if (typeof window.Node != 'undefined') { 4940 $_.Node = window.Node; 4941 } else { 4942 $_.Node = { 4943 'ELEMENT_NODE': 1, 4944 'ATTRIBUTE_NODE': 2, 4945 'TEXT_NODE': 3, 4946 'CDATA_SECTION_NODE': 4, 4947 'ENTITY_REFERENCE_NODE': 5, 4948 'ENTITY_NODE': 6, 4949 'PROCESSING_INSTRUCTION_NODE': 7, 4950 'COMMENT_NODE': 8, 4951 'DOCUMENT_NODE': 9, 4952 'DOCUMENT_TYPE_NODE': 10, 4953 'DOCUMENT_FRAGMENT_NODE': 11, 4954 'NOTATION_NODE': 12, 4955 //The two nodes are disconnected. Order between disconnected nodes is always implementation-specific. 4956 'DOCUMENT_POSITION_DISCONNECTED': 0x01, 4957 //The second node precedes the reference node. 4958 'DOCUMENT_POSITION_PRECEDING': 0x02, 4959 //The node follows the reference node. 4960 'DOCUMENT_POSITION_FOLLOWING': 0x04, 4961 //The node contains the reference node. A node which contains is always preceding, too. 4962 'DOCUMENT_POSITION_CONTAINS': 0x08, 4963 //The node is contained by the reference node. A node which is contained is always following, too. 4964 'DOCUMENT_POSITION_CONTAINED_BY': 0x10, 4965 //The determination of preceding versus following is implementation-specific. 4966 'DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC': 0x20 4967 }; 4968 } 4969 4970 //node.ownerDocument gives the document object, which isn't the right info for a disconnect 4971 function getRootParent(node) { 4972 var parent = null; 4973 4974 if (node) { 4975 do { 4976 parent = node; 4977 } while (null != (node = node.parentNode)); 4978 } 4979 4980 return parent; 4981 } 4982 4983 //Compare Position - MIT Licensed, John Resig; http://ejohn.org/blog/comparing-document-position/ 4984 //Already checked for equality and disconnect 4985 function comparePosition(node1, node2) { 4986 return (node1.contains(node2) && 16) + (node2.contains(node1) && 8) + (node1.sourceIndex >= 0 && node2.sourceIndex >
4986= 0 ? (node1.sourceIndex < node2.sourceIndex && 4) + (node1.sourceIndex > node2.sourceIndex && 2) : 1); 4987 } 4988 4989 //get a node with a sourceIndex to use 4990 function getUseNode(node) { 4991 //if the node already has a sourceIndex, use that node 4992 if (null != node.sourceIndex) { 4993 return node; 4994 } 4995 //otherwise, insert a comment (which has a sourceIndex but minimal DOM impact) before the node and use that 4996 return node.parentNode.insertBefore(document.createComment(""), node); 4997 } 4998 4999 // http://www.w3.org/TR/DOM-Level-3-Core/core.html#Node3-compareDocumentPosition 5000 // FIXME: Check if the DOMNode prototype can be set. 5001 $_.compareDocumentPosition = function (node1, node2) { 5002 5003 if (document.documentElement.compareDocumentPosition) { 5004 return node1.compareDocumentPosition(node2); 5005 } 5006 5007 if (!document.documentElement.contains) { 5008 throw 'neither compareDocumentPosition nor contains is supported by this browser.'; 5009 } 5010 5011 if (node1 == node2) { 5012 return 0; 5013 } 5014 5015 //if they don't have the same parent, there's a disconnect 5016 if (getRootParent(node1) != getRootParent(node2)) { 5017 return 1; 5018 } 5019 5020 //use this if both nodes have a sourceIndex (text nodes don't) 5021 if (null != node1.sourceIndex && null != node2.sourceIndex) { 5022 return comparePosition(node1, node2); 5023 } 5024 5025 //document will definitely contain the other node 5026 if (node1 == document) { 5027 return 20; 5028 } 5029 if (node2 == document) { 5030 return 10; 5031 } 5032 5033 //get sourceIndexes to use for both nodes 5034 var useNode1 = getUseNode(node1), 5035 useNode2 = getUseNode(node2); 5036 5037 //call this function again to get the result 5038 var result = comparePosition(useNode1, useNode2); 5039 5040 //clean up if needed 5041 if (node1 != useNode1) { 5042 useNode1.parentNode.removeChild(useNode1); 5043 } 5044 if (node2 != useNode2) { 5045 useNode2.parentNode.removeChild(useNode2); 5046 } 5047 return result; 5048 }; 5049 5050 $_.getComputedStyle = function (node, style) { 5051 if (window.getComputedStyle) { 5052 return window.getComputedStyle(node, style); 5053 } 5054 if (node.currentStyle) { 5055 return node.currentStyle; 5056 } 5057 return null; 5058 }; 5059 5060 /** 5061 * 5062 * @param obj 5063 * @param attr 5064 * @returns {Boolean} true 5065 */ 5066 function hasAttribute(obj, attr) { 5067 var native_method = obj.hasAttribute; 5068 if (native_method) { 5069 return obj.hasAttribute(attr); 5070 } 5071 return (typeof obj.attributes[attr] !== 'undefined'); 5072 } 5073 5074 /** 5075 * Insert the node `node` after `preceding`. 5076 * 5077 * @param {Element} node 5078 * @param {Element} preceding 5079 * @return {Element} 5080 */ 5081 function insertAfter(node, preceding) { 5082 var next = preceding.nextSibling, 5083 parent = preceding.parentNode; 5084 if (next) { 5085 parent.insertBefore(node, next); 5086 } else { 5087 parent.appendChild(node); 5088 } 5089 return node; 5090 } 5091 5092 /** 5093 * Splits text node `node` at the given text index. 5094 * 5095 * Note that we cannot use splitText() because it is bugridden in IE 9. 5096 * 5097 * Borrowed from rangy. 5098 * 5099 * @param {Element} node 5100 * @param {number} index 5101 * @return {Element} 5102 */ 5103 function splitText(node, index) { 5104 var newNode = node.cloneNode(false); 5105 newNode.deleteData(0, index); 5106 node.deleteData(index, node.length - index); 5107 insertAfter(newNode, node); 5108 return newNode; 5109 } 5110 5111 var htmlNamespace = "http://www.w3.org/1999/xhtml"; 5112 5113 var cssStylingFlag = false; 5114 5115 // This is bad :( 5116 var globalRange = null; 5117 5118 // Commands are stored in a dictionary where we call their actions and such 5119 var commands = {}; 5120 5121 /////////////////////////////////////////////////////////////////////////////// 5122 ////////////////////////////// Utility functions ////////////////////////////// 5123 /////////////////////////////////////////////////////////////////////////////// 5124 //@{ 5125 5126 // Opera 11 puts HTML elements in the null namespace, it seems. 5127 function isHtmlNamespace(ns) { 5128 return ns === null || !ns || ns === htmlNamespace; 5129 } 5130 5131 // "An HTML element is an Element whose namespace is the HTML namespace." 5132 // 5133 // I allow an extra argument to more easily check whether something is a 5134 // particular HTML element, like isNamedHtmlElement(node, 'OL'). It accepts arrays 5135 // too, like isHtmlElementInArray(node, ["OL", "UL"]) to check if it's an ol or ul. 5136 // TODO This function was prominent during profiling. Remove it 5137 // and replace with calls to isAnyHtmlElement, isNamedHtmlElement 5138 // and is isMappedHtmlElement. 5139 function isHtmlElement_obsolete(node, tags) { 5140 if (typeof tags == "string") { 5141 tags = [tags]; 5142 } 5143 if (typeof tags == "object") { 5144 tags = $_(tags).map(function (tag) { 5145 return tag.toUpperCase(); 5146 }); 5147 } 5148 return node && node.nodeType == 1 && isHtmlNamespace(node.namespaceURI) && (typeof tags == "undefined" || $_(tags).indexOf(node.tagName) != -1); 5149 } 5150 5151 function isAnyHtmlElement(node) { 5152 return node && node.nodeType == 1 && isHtmlNamespace(node.namespaceURI); 5153 } 5154 5155 // name should be uppercase 5156 function isNamedHtmlElement(node, name) { 5157 return node && node.nodeType == 1 && isHtmlNamespace(node.namespaceURI) 5158 // This function is passed in a mix of upper and lower case names 5159 && name.toUpperCase() === node.nodeName; 5160 } 5161 5162 // TODO remove when isHtmlElementInArray is removed 5163 function arrayContainsInsensitive(array, str) { 5164 var i, len; 5165 str = str.toUpperCase(); 5166 for (i = 0, len = array.length; i < len; i++) { 5167 if (array[i].toUpperCase() === str) { 5168 return true; 5169 } 5170 } 5171 return false;
5172 } 5173 // TODO replace calls to this function with calls to isMappedHtmlElement() 5174 function isHtmlElementInArray(node, array) { 5175 return node && node.nodeType == 1 && isHtmlNamespace(node.namespaceURI) 5176 // This function is passed in a mix of upper and lower case names 5177 && arrayContainsInsensitive(array, node.nodeName); 5178 } 5179 5180 // map must have all-uppercase keys 5181 function isMappedHtmlElement(node, map) { 5182 return node && node.nodeType == 1 && isHtmlNamespace(node.namespaceURI) && map[node.nodeName]; 5183 } 5184 5185 /** 5186 * Method to count the number of styles in the given style 5187 */ 5188 function getStyleLength(node) { 5189 var s; 5190 var styleLength = 0; 5191 5192 if (!node) { 5193 return 0; 5194 } 5195 5196 if (!node.style) { 5197 return 0; 5198 } 5199 5200 // some browsers support .length on styles 5201 if (typeof node.style.length !== 'undefined') { 5202 return node.style.length; 5203 } 5204 5205 /*jslint forin: true*/ //not sure whether node.style.hasOwnProperty is valid 5206 for (s in node.style) { 5207 if (node.style[s] && node.style[s] !== 0 && node.style[s] !== 'false') { 5208 styleLength++; 5209 } 5210 } 5211 /*jslint forin: false*/ 5212 5213 return styleLength; 5214 } 5215 5216 function toArray(obj) { 5217 if (!obj) { 5218 return null; 5219 } 5220 var array = [], 5221 i, 5222 l = obj.length; 5223 // iterate backwards ensuring that length is an UInt32 5224 i = l >>> 0; 5225 while (i--) { 5226 array[i] = obj[i]; 5227 } 5228 return array; 5229 } 5230 5231 function nextNodeDescendants(node) { 5232 while (node && !node.nextSibling) { 5233 node = node.parentNode; 5234 } 5235 if (!node) { 5236 return null; 5237 } 5238 return node.nextSibling; 5239 } 5240 5241 function nextNode(node) { 5242 if (node.hasChildNodes()) { 5243 return node.firstChild; 5244 } 5245 return nextNodeDescendants(node); 5246 } 5247 5248 function previousNode(node) { 5249 if (node.previousSibling) { 5250 node = node.previousSibling; 5251 while (node.hasChildNodes()) { 5252 node = node.lastChild; 5253 } 5254 return node; 5255 } 5256 if (node.parentNode && node.parentNode.nodeType == Node.ELEMENT_NODE) { 5257 return node.parentNode; 5258 } 5259 return null; 5260 } 5261 5262 /** 5263 * Returns true if ancestor is an ancestor of descendant, false otherwise. 5264 */ 5265 function isAncestor(ancestor, descendant) { 5266 return ancestor && descendant && Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY); 5267 } 5268 5269 /** 5270 * Returns true if ancestor is an ancestor of or equal to descendant, false 5271 * otherwise. 5272 */ 5273 function isAncestorContainer(ancestor, descendant) { 5274 return (ancestor || descendant) && (ancestor == descendant || isAncestor(ancestor, descendant)); 5275 } 5276 5277 /** 5278 * Returns true if descendant is a descendant of ancestor, false otherwise. 5279 */ 5280 function isDescendant(descendant, ancestor) { 5281 return ancestor && descendant && Boolean(ancestor.compareDocumentPosition(descendant) & Node.DOCUMENT_POSITION_CONTAINED_BY); 5282 } 5283 5284 /** 5285 * Returns true if node1 is before node2 in tree order, false otherwise. 5286 */ 5287 function isBefore(node1, node2) { 5288 return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION_FOLLOWING); 5289 } 5290 5291 /** 5292 * Returns true if node1 is after node2 in tree order, false otherwise. 5293 */ 5294 function isAfter(node1, node2) { 5295 return Boolean(node1.compareDocumentPosition(node2) & Node.DOCUMENT_POSITION_PRECEDING); 5296 } 5297 5298 function getAncestors(node) { 5299 var ancestors = []; 5300 while (node.parentNode) { 5301 ancestors.unshift(node.parentNode); 5302 node = node.parentNode; 5303 } 5304 return ancestors; 5305 } 5306 5307 function getDescendants(node) { 5308 var descendants = []; 5309 var stop = nextNodeDescendants(node); 5310 while (null != (node = nextNode(node)) && node != stop) { 5311 descendants.push(node); 5312 } 5313 return descendants; 5314 } 5315 5316 function convertProperty(property) { 5317 // Special-case for now 5318 var map = { 5319 "fontFamily": "font-family", 5320 "fontSize": "font-size", 5321 "fontStyle": "font-style", 5322 "fontWeight": "font-weight", 5323 "textDecoration": "text-decoration" 5324 }; 5325 if (typeof map[property] != "undefined") { 5326 return map[property]; 5327 } 5328 5329 return property; 5330 } 5331 5332 // Return the <font size=X> value for the given CSS size, or undefined if there 5333 // is none. 5334 function cssSizeToLegacy(cssVal) { 5335 return { 5336 "xx-small": 1, 5337 "small": 2, 5338 "medium": 3, 5339 "large": 4, 5340 "x-large": 5, 5341 "xx-large": 6, 5342 "xxx-large": 7 5343 }[cssVal]; 5344 } 5345 5346 // Return the CSS size given a legacy size. 5347 function legacySizeToCss(legacyVal) { 5348 return { 5349 1: "xx-small", 5350 2: "small", 5351 3: "medium", 5352 4: "large", 5353 5: "x-large", 5354 6: "xx-large", 5355 7: "xxx-large" 5356 }[legacyVal]; 5357 } 5358 5359 // "the directionality" from HTML. I don't bother caring about non-HTML 5360 // elements. 5361 // 5362 // "The directionality of an element is either 'ltr' or 'rtl', and is 5363 // determined as per the first appropriate set of steps from the following 5364 // list:" 5365 function getDirectionality(element) { 5366 // "If the element's dir attribute is in the ltr state 5367 // The directionality of the element is 'ltr'." 5368 if (element.dir == "ltr") { 5369 return "ltr"; 5370 } 5371 5372 // "If the element's dir attribute is in the rtl state 5373 // The directionality of the element is 'rtl'." 5374 if (element.dir == "rtl") { 5375 return "rtl";
5376 } 5377 5378 // "If the element's dir attribute is in the auto state 5379 // "If the element is a bdi element and the dir attribute is not in a 5380 // defined state (i.e. it is not present or has an invalid value) 5381 // [lots of complicated stuff] 5382 // 5383 // Skip this, since no browser implements it anyway. 5384 5385 // "If the element is a root element and the dir attribute is not in a 5386 // defined state (i.e. it is not present or has an invalid value) 5387 // The directionality of the element is 'ltr'." 5388 if (!isAnyHtmlElement(element.parentNode)) { 5389 return "ltr"; 5390 } 5391 5392 // "If the element has a parent element and the dir attribute is not in a 5393 // defined state (i.e. it is not present or has an invalid value) 5394 // The directionality of the element is the same as the element's 5395 // parent element's directionality." 5396 return getDirectionality(element.parentNode); 5397 } 5398 5399 //@} 5400 5401 /////////////////////////////////////////////////////////////////////////////// 5402 ///////////////////////////// DOM Range functions ///////////////////////////// 5403 /////////////////////////////////////////////////////////////////////////////// 5404 //@{ 5405 5406 // "The length of a Node node is the following, depending on node: 5407 // 5408 // ProcessingInstruction 5409 // DocumentType 5410 // Always 0. 5411 // Text 5412 // Comment 5413 // node's length. 5414 // Any other node 5415 // node's childNodes's length." 5416 function getNodeLength(node) { 5417 switch (node.nodeType) { 5418 case Node.PROCESSING_INSTRUCTION_NODE: 5419 case Node.DOCUMENT_TYPE_NODE: 5420 return 0; 5421 5422 case Node.TEXT_NODE: 5423 case Node.COMMENT_NODE: 5424 return node.length; 5425 5426 default: 5427 return node.childNodes.length; 5428 } 5429 } 5430 5431 /** 5432 * The position of two boundary points relative to one another, as defined by 5433 * DOM Range. 5434 */ 5435 function getPosition(nodeA, offsetA, nodeB, offsetB) { 5436 // "If node A is the same as node B, return equal if offset A equals offset 5437 // B, before if offset A is less than offset B, and after if offset A is 5438 // greater than offset B." 5439 if (nodeA == nodeB) { 5440 if (offsetA == offsetB) { 5441 return "equal"; 5442 } 5443 if (offsetA < offsetB) { 5444 return "before"; 5445 } 5446 if (offsetA > offsetB) { 5447 return "after"; 5448 } 5449 } 5450 5451 var documentPosition = nodeB.compareDocumentPosition(nodeA); 5452 // "If node A is after node B in tree order, compute the position of (node 5453 // B, offset B) relative to (node A, offset A). If it is before, return 5454 // after. If it is after, return before." 5455 if (documentPosition & Node.DOCUMENT_POSITION_FOLLOWING) { 5456 var pos = getPosition(nodeB, offsetB, nodeA, offsetA); 5457 if (pos == "before") { 5458 return "after"; 5459 } 5460 if (pos == "after") { 5461 return "before"; 5462 } 5463 } 5464 5465 // "If node A is an ancestor of node B:" 5466 if (documentPosition & Node.DOCUMENT_POSITION_CONTAINS) { 5467 // "Let child equal node B." 5468 var child = nodeB; 5469 5470 // "While child is not a child of node A, set child to its parent." 5471 while (child.parentNode != nodeA) { 5472 child = child.parentNode; 5473 } 5474 5475 // "If the index of child is less than offset A, return after." 5476 if (getIndexInParent(child) < offsetA) { 5477 return "after"; 5478 } 5479 } 5480 5481 // "Return before." 5482 return "before"; 5483 } 5484 5485 /** 5486 * Returns the furthest ancestor of a Node as defined by DOM Range. 5487 */ 5488 function getFurthestAncestor(node) { 5489 5490 var root = node; 5491 5492 while (root.parentNode != null) { 5493 root = root.parentNode; 5494 } 5495 5496 return root; 5497 5498 } 5499
5500 /** 5501 * "contained" as defined by DOM Range: "A Node node is contained in a range 5502 * range if node's furthest ancestor is the same as range's root, and (node, 0) 5503 * is after range's start, and (node, length of node) is before range's end." 5504 */ 5505 function isContained(node, range) { 5506 var pos1 = getPosition(node, 0, range.startContainer, range.startOffset); 5507 if (pos1 !== "after") { 5508 return false; 5509 } 5510 var pos2 = getPosition(node, getNodeLength(node), range.endContainer, range.endOffset); 5511 if (pos2 !== "before") { 5512 return false; 5513 } 5514 return getFurthestAncestor(node) == getFurthestAncestor(range.startContainer); 5515 } 5516 5517 /** 5518 * Return all nodes contained in range that the provided function returns true 5519 * for, omitting any with an ancestor already being returned. 5520 */ 5521 function getContainedNodes(range, condition) { 5522 if (typeof condition == "undefined") { 5523 condition = function () { 5524 return true; 5525 }; 5526 } 5527 var node = range.startContainer; 5528 if (node.hasChildNodes() && range.startOffset < node.childNodes.length) { 5529 // A child is contained 5530 node = node.childNodes[range.startOffset]; 5531 } else if (range.startOffset == getNodeLength(node)) { 5532 // No descendant can be contained 5533 node = nextNodeDescendants(node); 5534 } else { 5535 // No children; this node at least can't be contained 5536 node = nextNode(node); 5537 } 5538 5539 var stop = range.endContainer; 5540 if (stop.hasChildNodes() && range.endOffset < stop.childNodes.length) { 5541 // The node after the last contained node is a child 5542 stop = stop.childNodes[range.endOffset]; 5543 } else { 5544 // This node and/or some of its children might be contained 5545 stop = nextNodeDescendants(stop); 5546 } 5547 5548 var nodeList = []; 5549 while (isBefore(node, stop)) { 5550 if (isContained(node, range) && condition(node)) { 5551 nodeList.push(node); 5552 node = nextNodeDescendants(node); 5553 continue; 5554 } 5555 node = nextNode(node); 5556 } 5557 return nodeList; 5558 } 5559 5560 /** 5561 * As above, but includes nodes with an ancestor that's already been returned. 5562 */ 5563 function getAllContainedNodes(range, condition) { 5564 if (typeof condition == "undefined") { 5565 condition = function () { 5566 return true; 5567 }; 5568 } 5569 var node = range.startContainer; 5570 if (node.hasChildNodes() && range.startOffset < node.childNodes.length) { 5571 // A child is contained 5572 node = node.childNodes[range.startOffset]; 5573 } else if (range.startOffset == getNodeLength(node)) { 5574 // No descendant can be contained 5575 node = nextNodeDescendants(node); 5576 } else { 5577 // No children; this node at least can't be contained 5578 node = nextNode(node); 5579 } 5580 5581 var stop = range.endContainer; 5582 if (stop.hasChildNodes() && range.endOffset < stop.childNodes.length) { 5583 // The node after the last contained node is a child 5584 stop = stop.childNodes[range.endOffset]; 5585 } else { 5586 // This node and/or some of its children might be contained 5587 stop = nextNodeDescendants(stop); 5588 } 5589 5590 var nodeList = []; 5591 while (isBefore(node, stop)) { 5592 if (isContained(node, range) && condition(node)) { 5593 nodeList.push(node); 5594 } 5595 node = nextNode(node); 5596 } 5597 return nodeList; 5598 } 5599 5600 // Returns either null, or something of the form rgb(x, y, z), or something of 5601 // the form rgb(x, y, z, w) with w != 0. 5602 function normalizeColor(color) { 5603 if (color.toLowerCase() == "currentcolor") { 5604 return null; 5605 } 5606 5607 var outerSpan = document.createElement("span"); 5608 document.body.appendChild(outerSpan); 5609 outerSpan.style.color = "black"; 5610 5611 var innerSpan = document.createElement("span"); 5612 outerSpan.appendChild(innerSpan); 5613 innerSpan.style.color = color; 5614 color = $_.getComputedStyle(innerSpan).color; 5615 5616 if (color == "rgb(0, 0, 0)") { 5617 // Maybe it's really black, maybe it's invalid. 5618 outerSpan.color = "white"; 5619 color = $_.getComputedStyle(innerSpan).color; 5620 if (color != "rgb(0, 0, 0)") { 5621 return null; 5622 } 5623 } 5624 5625 document.body.removeChild(outerSpan); 5626 5627 // I rely on the fact that browsers generally provide consistent syntax for 5628 // getComputedStyle(), although it's not standardized. There are only two 5629 // exceptions I found: 5630 if (/^rgba\([0-9]+, [0-9]+, [0-9]+, 1\)$/.test(color)) { 5631 // IE10PP2 seems to do this sometimes. 5632 return color.replace("rgba", "rgb").replace(", 1)", ")"); 5633 } 5634 if (color == "transparent") { 5635 // IE10PP2, Firefox 7.0a2, and Opera 11.50 all return "transparent" if 5636 // the specified value is "transparent". 5637 return "rgba(0, 0, 0, 0)"; 5638 } 5639 return color; 5640 } 5641 5642 // Returns either null, or something of the form #xxxxxx, or the color itself 5643 // if it's a valid keyword. 5644 function parseSimpleColor(color) { 5645 color = color.toLowerCase(); 5646 if ($_(["aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige", "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown", "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue", "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod", "darkgray", "darkgreen", "darkgrey", "darkkhaki", "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen", "darkslateblue", "darkslategray", "darkslategrey", "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgray", "dimgrey", "dodgerblue", "firebrick", "floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite", "gold", "goldenrod", "gray", "green", "greenyellow", "grey", "honeydew", "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender", "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral", "lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", "lightslategrey", "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin", "navajowhite", "n
5646avy", "oldlace", "olive", "olivedrab", "orange", "orangered", "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred", "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue", "purple", "red", "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue", "slateblue", "slategray", "slategrey", "snow", "springgreen", "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white", "whitesmoke", "yellow", "yellowgreen"]).indexOf(color) != -1) { 5647 return color; 5648 } 5649 5650 color = normalizeColor(color); 5651 var matches = /^rgb\(([0-9]+), ([0-9]+), ([0-9]+)\)$/.exec(color); 5652 if (matches) { 5653 return "#" + parseInt(matches[1], 10).toString(16).replace(/^.$/, "0$&") + parseInt(matches[2], 10).toString(16).replace(/^.$/, "0$&") + parseInt(matches[3], 10).toString(16).replace(/^.$/, "0$&"); 5654 } else if (/^#[abcdef0123456789]+$/i.exec(color)) { 5655 // return hexadecimal color values (as returned by IE 7/8) 5656 return color; 5657 } 5658 return null; 5659 } 5660 5661 //@} 5662 5663 ////////////////////////////////////////////////////////////////////////////// 5664 /////////////////////////// Edit command functions /////////////////////////// 5665 ////////////////////////////////////////////////////////////////////////////// 5666 5667 ///////////////////////////////////////////////// 5668 ///// Methods of the HTMLDocument interface ///// 5669 ///////////////////////////////////////////////// 5670 //@{ 5671 5672 var getStateOverride, 5673 setStateOverride, 5674 resetOverrides, 5675 unsetStateOverride, 5676 getValueOverride, 5677 setValueOverride, 5678 unsetValueOverride; 5679 5680 var executionStackDepth = 0; 5681 5682 // Helper function for fontSize's action plus queryOutputHelper. It's just the 5683 // middle of fontSize's action, ripped out into its own function. 5684 function normalizeFontSize(value) { 5685 // "Strip leading and trailing whitespace from value." 5686 // 5687 // Cheap hack, not following the actual algorithm. 5688 value = $_(value).trim(); 5689 5690 // "If value is a valid floating point number, or would be a valid 5691 // floating point number if a single leading "+" character were 5692 // stripped:" 5693 if (/^[\-+]?[0-9]+(\.[0-9]+)?([eE][\-+]?[0-9]+)?$/.test(value)) { 5694 var mode; 5695 5696 // "If the first character of value is "+", delete the character 5697 // and let mode be "relative-plus"." 5698 if (value[0] == "+") { 5699 value = value.slice(1); 5700 mode = "relative-plus"; 5701 // "Otherwise, if the first character of value is "-", delete the 5702 // character and let mode be "relative-minus"." 5703 } else if (value[0] == "-") { 5704 value = value.slice(1); 5705 mode = "relative-minus"; 5706 // "Otherwise, let mode be "absolute"." 5707 } else { 5708 mode = "absolute"; 5709 } 5710 5711 // "Apply the rules for parsing non-negative integers to value, and 5712 // let number be the result." 5713 // 5714 // Another cheap hack. 5715 var num = parseInt(value, 10); 5716 5717 // "If mode is "relative-plus", add three to number." 5718 if (mode == "relative-plus") { 5719 num += 3; 5720 } 5721 5722 // "If mode is "relative-minus", negate number, then add three to 5723 // it." 5724 if (mode == "relative-minus") { 5725 num = 3 - num; 5726 } 5727 5728 // "If number is less than one, let number equal 1." 5729 if (num < 1) { 5730 num = 1; 5731 } 5732 5733 // "If number is greater than seven, let number equal 7." 5734 if (num > 7) { 5735 num = 7; 5736 } 5737 5738 // "Set value to the string here corresponding to number:" [table 5739 // omitted] 5740 value = { 5741 1: "xx-small", 5742 2: "small", 5743 3: "medium", 5744 4: "large", 5745 5: "x-large", 5746 6: "xx-large", 5747 7: "xxx-large" 5748 }[num]; 5749 } 5750 5751 return value; 5752 } 5753 5754 function getLegacyFontSize(size) { 5755 // For convenience in other places in my code, I handle all sizes, not just 5756 // pixel sizes as the spec says. This means pixel sizes have to be passed 5757 // in suffixed with "px", not as plain numbers. 5758 size = normalizeFontSize(size); 5759 5760 if (jQuery.inArray(size, ["xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"]) == -1 && !/^[0-9]+(\.[0-9]+)?(cm|mm|in|pt|pc|px)$/.test(size)) { 5761 // There is no sensible legacy size for things like "2em". 5762 return null; 5763 } 5764 5765 var font = document.createElement("font"); 5766 document.body.appendChild(font); 5767 if (size == "xxx-large") { 5768 font.size = 7; 5769 } else { 5770 font.style.fontSize = size; 5771 } 5772 var pixelSize = parseInt($_.getComputedStyle(font).fontSize, 10); 5773 document.body.removeChild(font); 5774 5775 // "Let returned size be 1." 5776 var returnedSize = 1; 5777 5778 // "While returned size is less than 7:" 5779 while (returnedSize < 7) { 5780 // "Let lower bound be the resolved value of "font-size" in pixels 5781 // of a font element whose size attribute is set to returned size." 5782 font = document.createElement("font"); 5783 font.size = returnedSize; 5784 document.body.appendChild(font); 5785 var lowerBound = parseInt($_.getComputedStyle(font).fontSize, 10); 5786 5787 // "Let upper bound be the resolved value of "font-size" in pixels 5788 // of a font element whose size attribute is set to one plus 5789 // returned size." 5790 font.size = 1 + returnedSize; 5791 var upperBound = parseInt($_.getComputedStyle(font).fontSize, 10); 5792 document.body.removeChild(font); 5793 5794 // "Let average be the average of upper bound and lower bound." 5795 var average = (upperBound + lowerBound) / 2; 5796 5797 // "If pixel size is less than average, return the one-element 5798 // string consisting of the digit returned size." 5799 if (pixelSize < average) { 5800 return String(returnedSize); 5801 } 5802 5803 // "Add one to returned size." 5804 returnedSize++; 5805 } 5806 5807 // "Return "7"." 5808 return "7"; 5809 } 5810 5811 // Helper function for common behavior. 5812 function editCommandMethod(command, prop, range, callback) { 5813 5814 var ret; 5815 5816 // Set up our global range magic, but only if we're the outermost function 5817 if (executionStackDepth === 0) { 5818 globalRange = range; 5819 } 5820 5821 executionStackDepth++; 5822 5823 try { 5824 5825 ret = callback(); 5826 5827 } catch (e) { 5828 5829 executionStackDepth--; 5830 throw e; 5831 5832 } 5833 5834 executionStackDepth--; 5835 5836 return ret; 5837 } 5838 5839 function myQueryCommandEnabled(command, range) { 5840 // "All of these methods must treat their command argument ASCII 5841 // case-insensitively." 5842 command = command.toLowerCase(); 5843 5844 // "If command is not supported, raise a NOT_SUPPORTED_ERR exception." 5845 return editCommandMethod(command, "action", range, (function (command) { 5846 return function () { 5847 // "Among commands defined in this specification, those listed in 5848 // Miscellaneous commands are always enabled. The other commands defined 5849 // here are enabled if the active range is not null, and disabled 5850 // otherwise." 5851 return jQuery.inArray(command, ["copy", "cut", "paste", "selectall", "stylewithcss", "usecss"]) != -1 || range !== null; 5852 }; 5853 }(command))); 5854 } 5855 5856 function setActiveRange(range) { 5857 5858 if(range === undefined){ 5859 return; 5860 } 5861 5862 var newRange = new _core.rangy.createRange(); 5863 5864 newRange.setStart(range.startContainer, range.startOffset); 5865 newRange.setEnd(range.endContainer, range.endOffset); 5866 5867 var sel = _core.rangy.getSelection(); 5868 5869 sel.removeAllRanges(); 5870 sel.addRange(newRange); 5871 5872 /*console.log(range); 5873 5874 var rangeObject = new window.GENTICS.Utils.RangeObject(); 5875 5876 rangeObject.startContainer = range.startContainer; 5877 rangeObject.startOffset = range.startOffset; 5878 rangeObject.endContainer = range.endContainer; 5879 rangeObject.endOffset = range.endOffset; 5880 5881 rangeObject.select();*/ 5882 } 5883 5884 function myExecCommand(commandArg, showUiArg, valueArg, range) { 5885 // "All of these methods must treat their command argument ASCII 5886 // case-insensitively." 5887 var command = commandArg.toLowerCase(); 5888 var showUi = showUiArg; 5889 var value = valueArg; 5890 5891 // "If only one argument was provided, let show UI be false." 5892 // 5893 // If range was passed, I can't actually detect how many args were passed 5894 // . . . 5895 if (arguments.length == 1 || (arguments.length >= 4 && typeof showUi == "undefined")) { 5896 showUi = false;
5897 } 5898 5899 // "If only one or two arguments were provided, let value be the empty 5900 // string." 5901 if (arguments.length <= 2 || (arguments.length >= 4 && typeof value == "undefined")) { 5902 value = ""; 5903 } 5904 5905 var selection = _core.rangy.getSelection(); 5906 5907 if (!range) { 5908 5909 if (selection.rangeCount === 0) { 5910 return; 5911 } 5912 5913 range = selection.getRangeAt(0); 5914 5915 } 5916 5917 // "If command is not supported, raise a NOT_SUPPORTED_ERR exception." 5918 // 5919 // "If command has no action, raise an INVALID_ACCESS_ERR exception." 5920 return editCommandMethod(command, "action", range, (function (command, showUi, value) { 5921 5922 return function () { 5923 // "If command is not enabled, return false." 5924 if (!myQueryCommandEnabled(command)) { 5925 return false; 5926 } 5927 5928 // "Take the action for command, passing value to the instructions as an 5929 // argument." 5930 commands[command].action(value, range); 5931 5932 // always fix the range after the command is complete 5933 setActiveRange(range); 5934 5935 // "Return true." 5936 return true; 5937 }; 5938 5939 }(command, showUi, value))); 5940 } 5941 5942 function myQueryCommandIndeterm(command, range) { 5943 // "All of these methods must treat their command argument ASCII 5944 // case-insensitively." 5945 command = command.toLowerCase(); 5946 5947 var selection = _core.rangy.getSelection(); 5948 5949 if (!range) { 5950 5951 if (selection.rangeCount === 0) { 5952 return; 5953 } 5954 5955 range = selection.getRangeAt(0); 5956 5957 } 5958 5959 // "If command is not supported, raise a NOT_SUPPORTED_ERR exception." 5960 // 5961 // "If command has no indeterminacy, raise an INVALID_ACCESS_ERR 5962 // exception." 5963 return editCommandMethod(command, "indeterm", range, (function (command) { 5964 return function () { 5965 // "If command is not enabled, return false." 5966 if (!myQueryCommandEnabled(command, range)) { 5967 return false; 5968 } 5969 5970 // "Return true if command is indeterminate, otherwise false." 5971 return commands[command].indeterm(range); 5972 }; 5973 }(command))); 5974 } 5975 5976 function myQueryCommandState(command, range) { 5977 // "All of these methods must treat their command argument ASCII 5978 // case-insensitively." 5979 command = command.toLowerCase(); 5980 5981 var selection = _core.rangy.getSelection(); 5982 5983 if (!range) { 5984 5985 if (selection.rangeCount === 0) { 5986 return; 5987 } 5988 5989 range = selection.getRangeAt(0); 5990 5991 } 5992 5993 // "If command is not supported, raise a NOT_SUPPORTED_ERR exception." 5994 // 5995 // "If command has no state, raise an INVALID_ACCESS_ERR exception." 5996 return editCommandMethod(command, "state", range, (function (command) { 5997 return function () { 5998 // "If command is not enabled, return false." 5999 if (!myQueryCommandEnabled(command, range)) { 6000 return false; 6001 } 6002 6003 // "If the state override for command is set, return it." 6004 if (typeof getStateOverride(command, range) != "undefined") { 6005 return getStateOverride(command, range); 6006 } 6007 6008 // "Return true if command's state is true, otherwise false." 6009 return commands[command].state(range); 6010 }; 6011 }(command))); 6012 } 6013 6014 // "When the queryCommandSupported(command) method on the HTMLDocument 6015 // interface is invoked, the user agent must return true if command is 6016 // supported, and false otherwise." 6017 function myQueryCommandSupported(command) { 6018 // "All of these methods must treat their command argument ASCII 6019 // case-insensitively." 6020 command = command.toLowerCase(); 6021 6022 return commands.hasOwnProperty(command); 6023 } 6024 6025 function myQueryCommandValue(command, range) { 6026 // "All of these methods must treat their command argument ASCII 6027 // case-insensitively." 6028 command = command.toLowerCase(); 6029 6030 var selection = _core.rangy.getSelection(); 6031 6032 if (!range) { 6033 6034 if (selection.rangeCount === 0) { 6035 return; 6036 } 6037 6038 range = selection.getRangeAt(0); 6039 6040 } 6041 6042 return editCommandMethod(command, "value", range, function () { 6043 // "If command is not supported or has no value, return the empty string." 6044 if (!commands.hasOwnProperty(command) || !commands[command].hasOwnProperty("value")) { 6045 return ""; 6046 } 6047 6048 // "If command is "fontSize" and its value override is set, convert the 6049 // value override to an integer number of pixels and return the legacy 6050 // font size for the result." 6051 if (command == "fontsize" && getValueOverride("fontsize", range) !== undefined) { 6052 return getLegacyFontSize(getValueOverride("fontsize", range)); 6053 } 6054 6055 // "If the value override for command is set, return it." 6056 if (typeof getValueOverride(command, range) != "undefined") { 6057 return getValueOverride(command, range); 6058 } 6059 6060 // "Return command's value." 6061 return commands[command].value(range); 6062 }); 6063 } 6064 //@} 6065 6066 ////////////////////////////// 6067 ///// Common definitions ///// 6068 ////////////////////////////// 6069 //@{ 6070 6071 // "A prohibited paragraph child name is "address", "article", "aside", 6072 // "blockquote", "caption", "center", "col", "colgroup", "dd", "details", 6073 // "dir", "div", "dl", "dt", "fieldset", "figcaption", "figure", "footer", 6074 // "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup", "hr", "li", 6075 // "listing", "menu", "nav", "ol", "p", "plaintext", "pre", "section", 6076 // "summary", "table", "tbody", "td", "tfoot", "th", "thead", "tr", "ul", or 6077 // "xmp"." 6078 var prohibitedParagraphChildNamesMap = { 6079 "ADDRESS": true, 6080 "ARTICLE": true,
6081 "ASIDE": true, 6082 "BLOCKQUOTE": true, 6083 "CAPTION": true, 6084 "CENTER": true, 6085 "COL": true, 6086 "COLGROUP": true, 6087 "DD": true, 6088 "DETAILS": true, 6089 "DIR": true, 6090 "DIV": true, 6091 "DL": true, 6092 "DT": true, 6093 "FIELDSET": true, 6094 "FIGCAPTION": true, 6095 "FIGURE": true, 6096 "FOOTER": true, 6097 "FORM": true, 6098 "H1": true, 6099 "H2": true, 6100 "H3": true, 6101 "H4": true, 6102 "H5": true, 6103 "H6": true, 6104 "HEADER": true, 6105 "HGROUP": true, 6106 "HR": true, 6107 "LI": true, 6108 "LISTING": true, 6109 "MENU": true, 6110 "NAV": true, 6111 "OL": true, 6112 "P": true, 6113 "PLAINTEXT": true, 6114 "PRE": true, 6115 "SECTION": true, 6116 "SUMMARY": true, 6117 "TABLE": true, 6118 "TBODY": true, 6119 "TD": true, 6120 "TFOOT": true, 6121 "TH": true, 6122 "THEAD": true, 6123 "TR": true, 6124 "UL": true, 6125 "XMP": true 6126 }; 6127 6128 // "A prohibited paragraph child is an HTML element whose local name is a 6129 // prohibited paragraph child name." 6130 function isProhibitedParagraphChild(node) { 6131 return isMappedHtmlElement(node, prohibitedParagraphChildNamesMap); 6132 } 6133 6134 var nonBlockDisplayValuesMap = { 6135 "inline": true, 6136 "inline-block": true, 6137 "inline-table": true, 6138 "none": true 6139 }; 6140 6141 // "A block node is either an Element whose "display" property does not have 6142 // resolved value "inline" or "inline-block" or "inline-table" or "none", or a 6143 // Document, or a DocumentFragment." 6144 function isBlockNode(node) { 6145 return node && ((node.nodeType == Node.ELEMENT_NODE && !nonBlockDisplayValuesMap[$_.getComputedStyle(node).display]) || node.nodeType == Node.DOCUMENT_NODE || node.nodeType == Node.DOCUMENT_FRAGMENT_NODE); 6146 } 6147 6148 // "An inline node is a node that is not a block node." 6149 function isInlineNode(node) { 6150 return node && !isBlockNode(node); 6151 } 6152 6153 // "An editing host is a node that is either an Element with a contenteditable 6154 // attribute set to the true state, or the Element child of a Document whose 6155 // designMode is enabled." 6156 function isEditingHost(node) { 6157 return node && node.nodeType == Node.ELEMENT_NODE && (node.contentEditable == "true" || (node.parentNode && node.parentNode.nodeType == Node.DOCUMENT_NODE && node.parentNode.designMode == "on")); 6158 } 6159 6160 // "Something is editable if it is a node which is not an editing host, does 6161 // not have a contenteditable attribute set to the false state, and whose 6162 // parent is an editing host or editable." 6163 function isEditable(node) { 6164 // This is slightly a lie, because we're excluding non-HTML elements with 6165 // contentEditable attributes. 6166 return node && !isEditingHost(node) && (node.nodeType != Node.ELEMENT_NODE || node.contentEditable != "false" || jQuery(node).hasClass('aloha-table-wrapper')) && (isEditingHost(node.parentNode) || isEditable(node.parentNode)); 6167 } 6168 6169 // Helper function, not defined in the spec 6170 function hasEditableDescendants(node) { 6171 var i; 6172 for (i = 0; i < node.childNodes.length; i++) { 6173 if (isEditable(node.childNodes[i]) || hasEditableDescendants(node.childNodes[i])) { 6174 return true; 6175 } 6176 } 6177 return false; 6178 } 6179 6180 // "The editing host of node is null if node is neither editable nor an editing 6181 // host; node itself, if node is an editing host; or the nearest ancestor of 6182 // node that is an editing host, if node is editable." 6183 function getEditingHostOf(node) { 6184 if (isEditingHost(node)) { 6185 return node; 6186 } 6187 if (isEditable(node)) { 6188 var ancestor = node.parentNode; 6189 while (!isEditingHost(ancestor)) { 6190 ancestor = ancestor.parentNode; 6191 } 6192 return ancestor; 6193 } 6194 return null; 6195 } 6196 6197 // "Two nodes are in the same editing host if the editing host of the first is 6198 // non-null and the same as the editing host of the second." 6199 function inSameEditingHost(node1, node2) { 6200 return getEditingHostOf(node1) && getEditingHostOf(node1) == getEditingHostOf(node2); 6201 } 6202 6203 // "A collapsed line break is a br that begins a line box which has nothing 6204 // else in it, and therefore has zero height." 6205 function isCollapsedLineBreak(br) { 6206 if (!isNamedHtmlElement(br, 'br')) { 6207 return false; 6208 } 6209 6210 // Add a zwsp after it and see if that changes the height of the nearest 6211 // non-inline parent. Note: this is not actually reliable, because the 6212 // parent might have a fixed height or something. 6213 var ref = br.parentNode; 6214 while ($_.getComputedStyle(ref).display == "inline") { 6215 ref = ref.parentNode; 6216 } 6217 6218 var origStyle = { 6219 height: ref.style.height, 6220 maxHeight: ref.style.maxHeight, 6221 minHeight: ref.style.minHeight 6222 }; 6223 6224 ref.style.height = 'auto'; 6225 ref.style.maxHeight = 'none'; 6226 6227 var space = document.createTextNode('\u200b'); 6228 var origHeight = ref.offsetHeight; 6229 if (origHeight == 0) { 6230 throw 'isCollapsedLineBreak: original height is zero, bug?'; 6231 } 6232 br.parentNode.insertBefore(space, br.nextSibling); 6233 var finalHeight = ref.offsetHeight; 6234 space.parentNode.removeChild(space); 6235 6236 ref.style.height = origStyle.height; 6237 ref.style.maxHeight = origStyle.maxHeight; 6238 6239 // Allow some leeway in case the zwsp didn't create a whole new line, but 6240 // only made an existing line slightly higher. Firefox 6.0a2 shows this 6241 // behavior when the first line is bold. 6242 return origHeight < finalHeight - 5; 6243 } 6244 6245 // "An extraneous line break is a br that has no visual effect, in that 6246 // removing it from the DOM would not change layout, except that a br that is 6247 // the sole child of an li is not extraneous." 6248 function isExtraneousLineBreak(br) { 6249 6250 if (!isNamedHtmlElement(br, 'br')) { 6251 return false;
6252 } 6253 6254 if (isNamedHtmlElement(br.parentNode, "li") && br.parentNode.childNodes.length == 1) { 6255 return false; 6256 } 6257 6258 // Make the line break disappear and see if that changes the block's 6259 // height. Yes, this is an absurd hack. We have to reset height etc. on 6260 // the reference node because otherwise its height won't change if it's not 6261 // auto. 6262 var ref = br.parentNode; 6263 while ($_.getComputedStyle(ref).display == "inline") { 6264 ref = ref.parentNode; 6265 } 6266 6267 var origStyle = { 6268 height: ref.style.height, 6269 maxHeight: ref.style.maxHeight, 6270 minHeight: ref.style.minHeight, 6271 contentEditable: ref.contentEditable 6272 }; 6273 6274 ref.style.height = 'auto'; 6275 ref.style.maxHeight = 'none'; 6276 ref.style.minHeight = '0'; 6277 6278 var origHeight = getOffsetHeight(ref); 6279 if (origHeight == 0) { 6280 throw "isExtraneousLineBreak: original height is zero, bug?"; 6281 } 6282 6283 var origBrDisplay = br.style.display; 6284 br.style.display = 'none'; 6285 var finalHeight = ref.offsetHeight; 6286 6287 // Restore original styles to the touched elements. 6288 ref.style.height = origStyle.height; 6289 ref.style.maxHeight = origStyle.maxHeight; 6290 ref.style.minHeight = origStyle.minHeight; 6291 6292 br.style.display = origBrDisplay; 6293 6294 return origHeight == finalHeight; 6295 } 6296 6297 // "A whitespace node is either a Text node whose data is the empty string; or 6298 // a Text node whose data consists only of one or more tabs (0x0009), line 6299 // feeds (0x000A), carriage returns (0x000D), and/or spaces (0x0020), and whose 6300 // parent is an Element whose resolved value for "white-space" is "normal" or 6301 // "nowrap"; or a Text node whose data consists only of one or more tabs 6302 // (0x0009), carriage returns (0x000D), and/or spaces (0x0020), and whose 6303 // parent is an Element whose resolved value for "white-space" is "pre-line"." 6304 function isWhitespaceNode(node) { 6305 var nodeTypes = Node; 6306 if (node && node.nodeType === nodeTypes.TEXT_NODE) { 6307 var parentNode = node.parentNode; 6308 6309 var nodeData = node.data; 6310 if (jQuery.trim(nodeData).length === 0) { 6311 return true; 6312 } else if (parentNode && /^[\t\n\r ]+$/.test(nodeData)) { 6313 if (parentNode.nodeType === nodeTypes.ELEMENT_NODE) { 6314 if (jQuery.inArray($_.getComputedStyle(parentNode).whiteSpace, ["normal", "nowrap"]) !== -1) { 6315 return true; 6316 } else if ($_.getComputedStyle(parentNode).whiteSpace === "pre-line") { 6317 return true; 6318 } 6319 } else if (parentNode.nodeType === nodeTypes.DOCUMENT_FRAGMENT_NODE) { 6320 return true; 6321 } 6322 } 6323 } 6324 return false; 6325 } 6326 6327 /** 6328 * Collapse sequences of ignorable whitespace (tab (0x0009), line feed (0x000A), carriage return (0x000D), space (0x0020)) to only one space. 6329 * Preserve the given range if necessary. 6330 * @param node text node 6331 * @param range range 6332 */ 6333 function collapseWhitespace(node, range) { 6334 // "If node is neither editable nor an editing host, abort these steps." 6335 if (!isEditable(node) && !isEditingHost(node)) { 6336 return; 6337 } 6338 6339 // if the given node is not a text node, return 6340 if (!node || node.nodeType !== Node.TEXT_NODE) { 6341 return; 6342 } 6343 6344 // if the node is in a pre or pre-wrap node, return 6345 if (jQuery.inArray($_.getComputedStyle(node.parentNode).whiteSpace, ["pre", "pre-wrap"]) != -1) { 6346 return; 6347 } 6348 6349 // if the given node does not contain sequences of at least two consecutive ignorable whitespace characters, return 6350 if (!/[\t\n\r ]{2,}/.test(node.data)) { 6351 return; 6352 } 6353 6354 var newData = ''; 6355 var correctStart = range.startContainer == node; 6356 var correctEnd = range.endContainer == node; 6357 var wsFound = false; 6358 var i; 6359 6360 // iterate through the node data 6361 for (i = 0; i < node.data.length; ++i) { 6362 if (/[\t\n\r ]/.test(node.data.substr(i, 1))) { 6363 // found a whitespace 6364 if (!wsFound) { 6365 // this is the first whitespace in the current sequence 6366 // add a whitespace to the new data sequence 6367 newData += ' '; 6368 // remember that we found a whitespace 6369 wsFound = true; 6370 } else { 6371 // this is not the first whitespace in the sequence, so omit this character 6372 if (correctStart && newData.length < range.startOffset) { 6373 range.startOffset--; 6374 } 6375 if (correctEnd && newData.length < range.endOffset) { 6376 range.endOffset--; 6377 } 6378 } 6379 } else { 6380 newData += node.data.substr(i, 1); 6381 wsFound = false;
6382 } 6383 } 6384 6385 // set the new data 6386 node.data = newData; 6387 } 6388 6389 // "node is a collapsed whitespace node if the following algorithm returns 6390 // true:" 6391 function isCollapsedWhitespaceNode(node) { 6392 // "If node is not a whitespace node, return false." 6393 if (!isWhitespaceNode(node)) { 6394 return false; 6395 } 6396 6397 // "If node's data is the empty string, return true." 6398 if (node.data == "") { 6399 return true; 6400 } 6401 6402 // "Let ancestor be node's parent." 6403 var ancestor = node.parentNode; 6404 6405 // "If ancestor is null, return true." 6406 if (!ancestor) { 6407 return true; 6408 } 6409 6410 // "If the "display" property of some ancestor of node has resolved value 6411 // "none", return true." 6412 if ($_(getAncestors(node)).some(function (ancestor) { return ancestor.nodeType == Node.ELEMENT_NODE && $_.getComputedStyle(ancestor).display == "none"; })) { 6413 return true; 6414 } 6415 6416 // "While ancestor is not a block node and its parent is not null, set 6417 // ancestor to its parent." 6418 while (!isBlockNode(ancestor) && ancestor.parentNode) { 6419 ancestor = ancestor.parentNode; 6420 } 6421 6422 // "Let reference be node." 6423 var reference = node; 6424 6425 // "While reference is a descendant of ancestor:" 6426 while (reference != ancestor) { 6427 // "Let reference be the node before it in tree order." 6428 reference = previousNode(reference); 6429 6430 // "If reference is a block node or a br, return true." 6431 if (isBlockNode(reference) || isNamedHtmlElement(reference, 'br')) { 6432 return true; 6433 } 6434 6435 // "If reference is a Text node that is not a whitespace node, or is an 6436 // img, break from this loop." 6437 if ((reference.nodeType == Node.TEXT_NODE && !isWhitespaceNode(reference)) || isNamedHtmlElement(reference, 'img')) { 6438 break; 6439 } 6440 } 6441 6442 // "Let reference be node." 6443 reference = node; 6444 6445 // "While reference is a descendant of ancestor:" 6446 var stop = nextNodeDescendants(ancestor); 6447 while (reference != stop) { 6448 // "Let reference be the node after it in tree order, or null if there 6449 // is no such node." 6450 reference = nextNode(reference); 6451 6452 // "If reference is a block node or a br, return true." 6453 if (isBlockNode(reference) || isNamedHtmlElement(reference, 'br')) { 6454 return true; 6455 } 6456 6457 // "If reference is a Text node that is not a whitespace node, or is an 6458 // img, break from this loop." 6459 if ((reference && reference.nodeType == Node.TEXT_NODE && !isWhitespaceNode(reference)) || isNamedHtmlElement(reference, 'img')) { 6460 break; 6461 } 6462 } 6463 6464 // "Return false." 6465 return false; 6466 } 6467 6468 // "Something is visible if it is a node that either is a block node, or a Text 6469 // node that is not a collapsed whitespace node, or an img, or a br that is not 6470 // an extraneous line break, or any node with a visible descendant; excluding 6471 // any node with an ancestor container Element whose "display" property has 6472 // resolved value "none"." 6473 function isVisible(node) { 6474 var i; 6475 6476 if (!node) { 6477 return false; 6478 } 6479 6480 if ($_(getAncestors(node).concat(node)) 6481 .filter(function (node) { return node.nodeType == Node.ELEMENT_NODE; }, true) 6482 .some(function (node) { return $_.getComputedStyle(node).display == "none"; })) { 6483 return false; 6484 } 6485 6486 if (isBlockNode(node) || (node.nodeType == Node.TEXT_NODE && !isCollapsedWhitespaceNode(node)) || isNamedHtmlElement(node, 'img') || (isNamedHtmlElement(node, 'br') && !isExtraneousLineBreak(node))) { 6487 return true; 6488 } 6489 6490 for (i = 0; i < node.childNodes.length; i++) { 6491 if (isVisible(node.childNodes[i])) { 6492 return true; 6493 } 6494 } 6495 6496 return false; 6497 } 6498 6499 // "Something is invisible if it is a node that is not visible." 6500 function isInvisible(node) { 6501 return node && !isVisible(node); 6502 } 6503 6504 // "A collapsed block prop is either a collapsed line break that is not an 6505 // extraneous line break, or an Element that is an inline node and whose 6506 // children are all either invisible or collapsed block props and that has at 6507 // least one child that is a collapsed block prop." 6508 function isCollapsedBlockProp(node) { 6509 var i; 6510 6511 if (isCollapsedLineBreak(node) && !isExtraneousLineBreak(node)) { 6512 return true; 6513 } 6514 6515 if (!isInlineNode(node) || node.nodeType != Node.ELEMENT_NODE) { 6516 return false;
6517 } 6518 6519 var hasCollapsedBlockPropChild = false; 6520 for (i = 0; i < node.childNodes.length; i++) { 6521 if (!isInvisible(node.childNodes[i]) && !isCollapsedBlockProp(node.childNodes[i])) { 6522 return false; 6523 } 6524 if (isCollapsedBlockProp(node.childNodes[i])) { 6525 hasCollapsedBlockPropChild = true; 6526 } 6527 } 6528 6529 return hasCollapsedBlockPropChild; 6530 } 6531 6532 /** 6533 * Checks whether the given node is a visible text node. 6534 * 6535 * @param {HTMLElement} node 6536 * @return {Boolean} True if `node` is a visible text node. 6537 */ 6538 function isInvisibleTextNode(node) { 6539 if (node && node.nodeType !== Node.TEXT_NODE) { 6540 return false; 6541 } 6542 var offset = 0; 6543 var data = node.data; 6544 var len = data.length; 6545 while (offset < len && data.charAt(offset) === '\u200b') { 6546 offset++; 6547 } 6548 return offset === len; 6549 } 6550 6551 /** 6552 * Complement of isInvisibleTextNode(). 6553 * 6554 * @param {HTMLElement} node 6555 * @return {Boolean} True if `node` is anything but an invisible text node. 6556 */ 6557 function isNotInvisibleTextNode(node) { 6558 return !isInvisibleTextNode(node); 6559 } 6560 6561 /** 6562 * Checks whether the given node is a otherwise empty block-level element 6563 * containing a propping <br> element. 6564 * 6565 * @param {HTMLElement} node 6566 * @return {Boolean} True if `node` is a propped up block-level element. 6567 */ 6568 function isProppedBlock(node) { 6569 if (!isBlock(node)) { 6570 return false; 6571 } 6572 var child = findNodeRight(node.lastChild, isVisible); 6573 return ( 6574 child 6575 && 'br' === child.nodeName.toLowerCase() 6576 && !findNodeRight(child.previousSibling, isVisible) 6577 ); 6578 } 6579 6580 /** 6581 * Checks whether the given node is a empty element, or an element that 6582 * would otherwise be empty except for a propping <br>, or an element 6583 * containing only invisible text nodes. 6584 * 6585 * @param {HTMLElement} node 6586 * @return {Boolean} True if `node` can be considered empty. 6587 */ 6588 /*function isEmptyNode(node) { 6589 return ( 6590 !node.hasChildNodes() 6591 || isProppedBlock(node) 6592 || !findNodeRight(node.lastChild, isNotInvisibleTextNode) 6593 ); 6594 }*/ 6595 6596 /** 6597 * Check if the given node is a empty element which is the only 6598 * immediate child of a editing host. 6599 * 6600 * @param {HTMLElement} node 6601 * @return {Boolean} True if `node` can be regarded as empty and the 6602 * only immediate child of its parent editing host. 6603 */ 6604 function isEmptyOnlyChildOfEditingHost(node) { 6605 return ( 6606 node 6607 && isEmptyNode(node) 6608 && isEditingHost(node.parentNode) 6609 && !node.previousSibling 6610 && !node.nextSibling 6611 ); 6612 } 6613 6614 /** 6615 * Remove the given node and return the position from where it was 6616 * removed. 6617 * 6618 * @param {HTMLElement} node Element to remove from DOM 6619 * @return {object} Object containing node and offset index. 6620 */ 6621 function removeNode(node) { 6622 var ancestor = node.parentNode; 6623 var offset = getIndexInParent(node); 6624 ancestor.removeChild(node); 6625 return { 6626 node: ancestor, 6627 offset: offset 6628 }; 6629 } 6630 6631 // Please note: This method is deprecated and will be removed. 6632 // Every command should use the value and range parameter. 6633 // 6634 // "The active range is the first range in the Selection given by calling 6635 // getSelection() on the context object, or null if there is no such range." 6636 // 6637 // We cheat and return globalRange if that's defined. We also ensure that the 6638 // active range meets the requirements that selection boundary points are 6639 // supposed to meet, i.e., that the nodes are both Text or Element nodes that 6640 // descend from a Document. 6641 function getActiveRange() { 6642 var ret; 6643 if (globalRange) { 6644 ret = globalRange; 6645 } else if (_core.rangy.getSelection().rangeCount) { 6646 ret = _core.rangy.getSelection().getRangeAt(0); 6647 } else { 6648 return null; 6649 } 6650 if (jQuery.inArray(ret.startContainer.nodeType, [Node.TEXT_NODE, Node.ELEMENT_NODE]) == -1 || jQuery.inArray(ret.endContainer.nodeType, [Node.TEXT_NODE, Node.ELEMENT_NODE]) == -1 || !ret.startContainer.ownerDocument || !ret.endContainer.ownerDocument || !isDescendant(ret.startContainer, ret.startContainer.ownerDocument) || !isDescendant(ret.endContainer, ret.endContainer.ownerDocument)) { 6651 throw "Invalid active range; test bug?"; 6652 } 6653 return ret; 6654 } 6655 6656 // "For some commands, each HTMLDocument must have a boolean state override 6657 // and/or a string value override. These do not change the command's state or 6658 // value, but change the way some algorithms behave, as specified in those 6659 // algorithms' definitions. Initially, both must be unset for every command. 6660 // Whenever the number of ranges in the Selection changes to something 6661 // different, and whenever a boundary point of the range at a given index in 6662 // the Selection changes to something different, the state override and value 6663 // override must be unset for every command." 6664 // 6665 // We implement this crudely by using setters and getters. To verify that the 6666 // selection hasn't changed, we copy the active range and just check the 6667 // endpoints match. This isn't really correct, but it's good enough for us. 6668 // Unset state/value overrides are undefined. We put everything in a function 6669 // so no one can access anything except via the provided functions, since 6670 // otherwise callers might mistakenly use outdated overrides (if the selection 6671 // has changed). 6672 (function () { 6673 var stateOverrides = {}; 6674 var valueOverrides = {}; 6675 var storedRange = null; 6676 6677 resetOverrides = function (range) { 6678 if (!storedRange 6679 || storedRange.startContainer != range.startContainer 6680 || storedRange.endContainer != range.endContainer 6681 || storedRange.startOffset != range.startOffset 6682 || storedRange.endOffset != range.endOffset) { 6683 storedRange = { 6684 startContainer: range.startContainer, 6685 endContainer: range.endContainer, 6686 startOffset: range.startOffset, 6687 endOffset: range.endOffset 6688 }; 6689 if (!mapUtil.isEmpty(stateOverrides) || !mapUtil.isEmpty(valueOverrides)) { 6690 stateOverrides = {}; 6691 valueOverrides = {}; 6692 return true; 6693 } 6694 } 6695 return false;
6696 }; 6697 6698 getStateOverride = function (command, range) { 6699 resetOverrides(range); 6700 return stateOverrides[command]; 6701 }; 6702 6703 setStateOverride = function (command, newState, range) { 6704 resetOverrides(range); 6705 stateOverrides[command] = newState; 6706 }; 6707 6708 unsetStateOverride = function (command, range) { 6709 resetOverrides(range); 6710 delete stateOverrides[command]; 6711 }; 6712 6713 getValueOverride = function (command, range) { 6714 resetOverrides(range); 6715 return valueOverrides[command]; 6716 }; 6717 6718 // "The value override for the backColor command must be the same as the 6719 // value override for the hiliteColor command, such that setting one sets 6720 // the other to the same thing and unsetting one unsets the other." 6721 setValueOverride = function (command, newValue, range) { 6722 6723 resetOverrides(range); 6724 6725 valueOverrides[command] = newValue; 6726 6727 if (command == "backcolor") { 6728 6729 valueOverrides.hilitecolor = newValue; 6730 6731 } else if (command == "hilitecolor") { 6732 6733 valueOverrides.backcolor = newValue; 6734 6735 } 6736 6737 }; 6738 6739 unsetValueOverride = function (command, range) { 6740 6741 resetOverrides(range); 6742 6743 delete valueOverrides[command]; 6744 6745 if (command == "backcolor") { 6746 6747 delete valueOverrides.hilitecolor; 6748 6749 } else if (command == "hilitecolor") { 6750 6751 delete valueOverrides.backcolor; 6752 6753 } 6754 6755 }; 6756 }()); 6757 6758 //@} 6759 6760 ///////////////////////////// 6761 ///// Common algorithms ///// 6762 ///////////////////////////// 6763 6764 ///// Assorted common algorithms ///// 6765 //@{ 6766 6767 function movePreservingRanges(node, newParent, newIndex, range) { 6768 // For convenience, I allow newIndex to be -1 to mean "insert at the end". 6769 if (newIndex == -1) { 6770 newIndex = newParent.childNodes.length; 6771 } 6772 6773 // "When the user agent is to move a Node to a new location, preserving 6774 // ranges, it must remove the Node from its original parent (if any), then 6775 // insert it in the new location. In doing so, however, it must ignore the 6776 // regular range mutation rules, and instead follow these rules:" 6777 6778 // "Let node be the moved Node, old parent and old index be the old parent 6779 // (which may be null) and index, and new parent and new index be the new 6780 // parent and index." 6781 var oldParent = node.parentNode; 6782 var oldIndex = getIndexInParent(node); 6783 var i; 6784 6785 // We only even attempt to preserve the global range object and the ranges 6786 // in the selection, not every range out there (the latter is probably 6787 // impossible). 6788 var ranges = [range]; 6789 for (i = 0; i < _core.rangy.getSelection().rangeCount; i++) { 6790 ranges.push(_core.rangy.getSelection().getRangeAt(i)); 6791 } 6792 var boundaryPoints = []; 6793 $_(ranges).forEach(function (range) { 6794 boundaryPoints.push([range.startContainer, range.startOffset]); 6795 boundaryPoints.push([range.endContainer, range.endOffset]); 6796 }); 6797 6798 $_(boundaryPoints).forEach(function (boundaryPoint) { 6799 // "If a boundary point's node is the same as or a descendant of node, 6800 // leave it unchanged, so it moves to the new location." 6801 // 6802 // No modifications necessary. 6803 6804 // "If a boundary point's node is new parent and its offset is greater 6805 // than new index, add one to its offset." 6806 if (boundaryPoint[0] == newParent && boundaryPoint[1] > newIndex) { 6807 boundaryPoint[1]++; 6808 } 6809 6810 // "If a boundary point's node is old parent and its offset is old index or 6811 // old index + 1, set its node to new parent and add new index â old index 6812 // to its offset." 6813 if (boundaryPoint[0] == oldParent && (boundaryPoint[1] == oldIndex || boundaryPoint[1] == oldIndex + 1)) { 6814 boundaryPoint[0] = newParent; 6815 boundaryPoint[1] += newIndex - oldIndex; 6816 } 6817 6818 // "If a boundary point's node is old parent and its offset is greater than 6819 // old index + 1, subtract one from its offset." 6820 if (boundaryPoint[0] == oldParent && boundaryPoint[1] > oldIndex + 1) { 6821 boundaryPoint[1]--; 6822 } 6823 }); 6824 6825 // Now actually move it and preserve the ranges. 6826 if (newParent.childNodes.length == newIndex) { 6827 newParent.appendChild(node); 6828 } else { 6829 newParent.insertBefore(node, newParent.childNodes[newIndex]); 6830 } 6831 6832 // if we're off actual node boundaries this implies that the move was 6833 // part of a deletion process (backspace). If that's the case we 6834 // attempt to fix this by restoring the range to the first index of 6835 // the node that has been moved 6836 var newRange = null; 6837 if (boundaryPoints[0][1] > boundaryPoints[0][0].childNodes.length && boundaryPoints[1][1] > boundaryPoints[1][0].childNodes.length) {
6838 range.setStart(node, 0); 6839 range.setEnd(node, 0); 6840 } else { 6841 range.setStart(boundaryPoints[0][0], boundaryPoints[0][1]); 6842 range.setEnd(boundaryPoints[1][0], boundaryPoints[1][1]); 6843 6844 _core.rangy.getSelection().removeAllRanges(); 6845 for (i = 1; i < ranges.length; i++) { 6846 newRange = _core.rangy.createRange(); 6847 newRange.setStart(boundaryPoints[2 * i][0], boundaryPoints[2 * i][1]); 6848 newRange.setEnd(boundaryPoints[2 * i + 1][0], boundaryPoints[2 * i + 1][1]); 6849 _core.rangy.getSelection().addRange(newRange); 6850 } 6851 if (newRange) { 6852 range = newRange; 6853 } 6854 } 6855 } 6856 6857 /** 6858 * Copy all non empty attributes from an existing to a new element 6859 * 6860 * @param {dom} element The source DOM element 6861 * @param {dom} newElement The new DOM element which will get the attributes of the source DOM element 6862 * @return void 6863 */ 6864 function copyAttributes(element, newElement) { 6865 6866 // This is an IE7 workaround. We identified three places that were connected 6867 // to the mysterious ie7 crash: 6868 // 1. Add attribute to dom element (Initialization of jquery-ui sortable) 6869 // 2. Access the jquery expando attribute. Just reading the name is 6870 // sufficient to make the browser vulnerable for the crash (Press enter) 6871 // 3. On editable blur the Aloha.editables[0].getContents(); gets invoked. 6872 // This invokation somehow crashes the ie7. We assume that the access of 6873 // shared expando attribute updates internal references which are not 6874 // correclty handled during clone(); 6875 6876 var attrs = element.attributes; 6877 var i; 6878 for (i = 0; i < attrs.length; i++) { 6879 var attr = attrs[i]; 6880 // attr.specified is an IE specific check to exclude attributes that were never really set. 6881 if (typeof attr.specified === "undefined" || attr.specified) { 6882 if (typeof newElement.setAttributeNS === 'function') { 6883 newElement.setAttributeNS(attr.namespaceURI, attr.name, attr.value); 6884 } else { 6885 // fixes https://github.com/alohaeditor/Aloha-Editor/issues/515 6886 newElement.setAttribute(attr.name, attr.value); 6887 } 6888 } 6889 } 6890 } 6891 6892 function setTagName(element, newName, range) { 6893 // "If element is an HTML element with local name equal to new name, return 6894 // element." 6895 if (isNamedHtmlElement(element, newName)) { 6896 return element; 6897 } 6898 6899 // "If element's parent is null, return element." 6900 if (!element.parentNode) { 6901 return element; 6902 } 6903 6904 // "Let replacement element be the result of calling createElement(new 6905 // name) on the ownerDocument of element." 6906 var replacementElement = element.ownerDocument.createElement(newName); 6907 6908 // "Insert replacement element into element's parent immediately before 6909 // element." 6910 element.parentNode.insertBefore(replacementElement, element); 6911 6912 // "Copy all attributes of element to replacement element, in order." 6913 copyAttributes(element, replacementElement); 6914 6915 // "While element has children, append the first child of element as the 6916 // last child of replacement element, preserving ranges." 6917 while (element.childNodes.length) { 6918 movePreservingRanges(element.firstChild, replacementElement, replacementElement.childNodes.length, range); 6919 } 6920 6921 // "Remove element from its parent." 6922 element.parentNode.removeChild(element); 6923 6924 // if the range still uses the old element, we modify it to the new one 6925 if (range.startContainer === element) { 6926 range.startContainer = replacementElement; 6927 } 6928 if (range.endContainer === element) { 6929 range.endContainer = replacementElement; 6930 } 6931 6932 // "Return replacement element." 6933 return replacementElement; 6934 } 6935 6936 function removeExtraneousLineBreaksBefore(node) { 6937 // "Let ref be the previousSibling of node." 6938 var ref = node.previousSibling; 6939 6940 // "If ref is null, abort these steps." 6941 if (!ref) { 6942 return; 6943 } 6944 6945 // "While ref has children, set ref to its lastChild." 6946 while (ref.hasChildNodes()) { 6947 ref = ref.lastChild; 6948 } 6949 6950 // "While ref is invisible but not an extraneous line break, and ref does 6951 // not equal node's parent, set ref to the node before it in tree order." 6952 while (isInvisible(ref) && !isExtraneousLineBreak(ref) && ref != node.parentNode) { 6953 ref = previousNode(ref); 6954 } 6955 6956 // "If ref is an editable extraneous line break, remove it from its 6957 // parent." 6958 if (isEditable(ref) && isExtraneousLineBreak(ref)) { 6959 ref.parentNode.removeChild(ref); 6960 } 6961 } 6962 6963 function removeExtraneousLineBreaksAtTheEndOf(node) { 6964 // "Let ref be node." 6965 var ref = node; 6966 6967 // "While ref has children, set ref to its lastChild." 6968 while (ref.hasChildNodes()) { 6969 ref = ref.lastChild; 6970 } 6971 6972 // "While ref is invisible but not an extraneous line break, and ref does 6973 // not equal node, set ref to the node before it in tree order." 6974 while (isInvisible(ref) && !isExtraneousLineBreak(ref) && ref != node) { 6975 ref = previousNode(ref); 6976 } 6977 6978 // "If ref is an editable extraneous line break, remove it from its 6979 // parent." 6980 if (isEditable(ref) && isExtraneousLineBreak(ref)) { 6981 ref.parentNode.removeChild(ref); 6982 } 6983 } 6984 6985 // "To remove extraneous line breaks from a node, first remove extraneous line 6986 // breaks before it, then remove extraneous line breaks at the end of it." 6987 function removeExtraneousLineBreaksFrom(node) { 6988 removeExtraneousLineBreaksBefore(node); 6989 removeExtraneousLineBreaksAtTheEndOf(node); 6990 } 6991 6992 //@} 6993 ///// Wrapping a list of nodes ///// 6994 //@{ 6995 6996 function wrap(nodeList, siblingCriteria, newParentInstructions, range) { 6997 var i; 6998 6999 // "If not provided, sibling criteria returns false and new parent 7000 // instructions returns null." 7001 if (typeof siblingCriteria == "undefined") { 7002 siblingCriteria = function () { 7003 return false;
7004 }; 7005 } 7006 if (typeof newParentInstructions == "undefined") { 7007 newParentInstructions = function () { 7008 return null; 7009 }; 7010 } 7011 7012 // "If node list is empty, or the first member of node list is not 7013 // editable, return null and abort these steps." 7014 if (!nodeList.length || !isEditable(nodeList[0])) { 7015 return null; 7016 } 7017 7018 // "If node list's last member is an inline node that's not a br, and node 7019 // list's last member's nextSibling is a br, append that br to node list." 7020 if (isInlineNode(nodeList[nodeList.length - 1]) && !isNamedHtmlElement(nodeList[nodeList.length - 1], "br") && isNamedHtmlElement(nodeList[nodeList.length - 1].nextSibling, "br")) { 7021 nodeList.push(nodeList[nodeList.length - 1].nextSibling); 7022 } 7023 7024 // "If the previousSibling of the first member of node list is editable and 7025 // running sibling criteria on it returns true, let new parent be the 7026 // previousSibling of the first member of node list." 7027 var newParent; 7028 if (isEditable(nodeList[0].previousSibling) && siblingCriteria(nodeList[0].previousSibling)) { 7029 newParent = nodeList[0].previousSibling; 7030 7031 // "Otherwise, if the nextSibling of the last member of node list is 7032 // editable and running sibling criteria on it returns true, let new parent 7033 // be the nextSibling of the last member of node list." 7034 } else if (isEditable(nodeList[nodeList.length - 1].nextSibling) && siblingCriteria(nodeList[nodeList.length - 1].nextSibling)) { 7035 newParent = nodeList[nodeList.length - 1].nextSibling; 7036 7037 // "Otherwise, run new parent instructions, and let new parent be the 7038 // result." 7039 } else { 7040 newParent = newParentInstructions(); 7041 } 7042 7043 // "If new parent is null, abort these steps and return null." 7044 if (!newParent) { 7045 return null; 7046 } 7047 7048 // "If new parent's parent is null:" 7049 if (!newParent.parentNode) { 7050 // "Insert new parent into the parent of the first member of node list 7051 // immediately before the first member of node list." 7052 nodeList[0].parentNode.insertBefore(newParent, nodeList[0]); 7053 7054 // "If any range has a boundary point with node equal to the parent of 7055 // new parent and offset equal to the index of new parent, add one to 7056 // that boundary point's offset." 7057 // 7058 // Try to fix range 7059 var startContainer = range.startContainer, 7060 startOffset = range.startOffset, 7061 endContainer = range.endContainer, 7062 endOffset = range.endOffset, 7063 newParentIndex = getIndexInParent(newParent); 7064 7065 if (startOffset >= newParentIndex && startContainer == newParent.parentNode) { 7066 range.setStart(startContainer, startOffset + 1); 7067 } 7068 if (endOffset >= newParentIndex && endContainer == newParent.parentNode) { 7069 range.setEnd(endContainer, endOffset + 1); 7070 } 7071 7072 // Only try to fix the global range. TODO remove globalRange here 7073 if (globalRange && globalRange !== range) { 7074 startContainer = globalRange.startContainer; 7075 startOffset = globalRange.startOffset; 7076 endContainer = globalRange.endContainer; 7077 endOffset = globalRange.endOffset; 7078 if (startContainer == newParent.parentNode && startOffset >= newParentIndex) { 7079 globalRange.setStart(startContainer, startOffset + 1); 7080 } 7081 if (endContainer == newParent.parentNode && endOffset >= newParentIndex) { 7082 globalRange.setEnd(endContainer, endOffset + 1); 7083 } 7084 } 7085 } 7086 7087 // "Let original parent be the parent of the first member of node list." 7088 var originalParent = nodeList[0].parentNode; 7089 7090 // "If new parent is before the first member of node list in tree order:" 7091 if (isBefore(newParent, nodeList[0])) { 7092 // "If new parent is not an inline node, but the last child of new 7093 // parent and the first member of node list are both inline nodes, and 7094 // the last child of new parent is not a br, call createElement("br") 7095 // on the ownerDocument of new parent and append the result as the last 7096 // child of new parent." 7097 if (!isInlineNode(newParent) && isInlineNode(newParent.lastChild) && isInlineNode(nodeList[0]) && !isNamedHtmlElement(newParent.lastChild, "BR")) { 7098 newParent.appendChild(newParent.ownerDocument.createElement("br")); 7099 } 7100 7101 // "For each node in node list, append node as the last child of new 7102 // parent, preserving ranges." 7103 for (i = 0; i < nodeList.length; i++) { 7104 movePreservingRanges(nodeList[i], newParent, -1, range); 7105 } 7106 7107 // "Otherwise:" 7108 } else { 7109 // "If new parent is not an inline node, but the first child of new 7110 // parent and the last member of node list are both inline nodes, and 7111 // the last member of node list is not a br, call createElement("br") 7112 // on the ownerDocument of new parent and insert the result as the 7113 // first child of new parent." 7114 if (!isInlineNode(newParent) && isInlineNode(newParent.firstChild) && isInlineNode(nodeList[nodeList.length - 1]) && !isNamedHtmlElement(nodeList[nodeList.length - 1], "BR")) { 7115 newParent.insertBefore(newParent.ownerDocument.createElement("br"), newParent.firstChild); 7116 } 7117 7118 // "For each node in node list, in reverse order, insert node as the 7119 // first child of new parent, preserving ranges." 7120 for (i = nodeList.length - 1; i >= 0; i--) { 7121 movePreservingRanges(nodeList[i], newParent, 0, range); 7122 } 7123 } 7124 7125 // "If original parent is editable and has no children, remove it from its 7126 // parent." 7127 if (isEditable(originalParent) && !originalParent.hasChildNodes()) { 7128 originalParent.parentNode.removeChild(originalParent); 7129 } 7130 7131 // "If new parent's nextSibling is editable and running sibling criteria on 7132 // it returns true:" 7133 if (isEditable(newParent.nextSibling) && siblingCriteria(newParent.nextSibling)) { 7134 // "If new parent is not an inline node, but new parent's last child 7135 // and new parent's nextSibling's first child are both inline nodes, 7136 // and new parent's last child is not a br, call createElement("br") on 7137 // the ownerDocument of new parent and append the result as the last 7138 // child of new parent." 7139 if (!isInlineNode(newParent) && isInlineNode(newParent.lastChild) && isInlineNode(newParent.nextSibling.firstChild) && !isNamedHtmlElement(newParent.lastChild, "BR")) { 7140 newParent.appendChild(newParent.ownerDocument.createElement("br")); 7141 } 7142 7143 // "While new parent's nextSibling has children, append its first child 7144 // as the last child of new parent, preserving ranges." 7145 while (newParent.nextSibling.hasChildNodes()) { 7146 movePreservingRanges(newParent.nextSibling.firstChild, newParent, -1, range); 7147 } 7148 7149 // "Remove new parent's nextSibling from its parent." 7150 newParent.parentNode.removeChild(newParent.nextSibling); 7151 } 7152 7153 // "Remove extraneous line breaks from new parent." 7154 removeExtraneousLineBreaksFrom(newParent); 7155 7156 // "Return new parent." 7157 return newParent; 7158 } 7159 7160 7161 //@} 7162 ///// Allowed children ///// 7163 //@{ 7164 7165 // "A name of an element with inline contents is "a", "abbr", "b", "bdi", 7166 // "bdo", "cite", "code", "dfn", "em", "h1", "h2", "h3", "h4", "h5", "h6", "i", 7167 // "kbd", "mark", "p", "pre", "q", "rp", "rt", "ruby", "s", "samp", "small", 7168 // "span", "strong", "sub", "sup", "u", "var", "acronym", "listing", "strike", 7169 // "xmp", "big", "blink", "font", "marquee", "nobr", or "tt"." 7170 var namesOfElementsWithInlineContentsMap = { 7171 "A": true, 7172 "ABBR": true, 7173 "B": true, 7174 "BDI": true, 7175 "BDO": true, 7176 "CITE": true, 7177 "CODE": true, 7178 "DFN": true, 7179 "EM": true, 7180 "H1": true, 7181 "H2": true, 7182 "H3": true, 7183 "H4": true, 7184 "H5": true, 7185 "H6": true, 7186 "I": true, 7187 "KBD": true, 7188 "MARK": true, 7189 "P": true, 7190 "PRE": true, 7191 "Q": true, 7192 "RP": true, 7193 "RT": true,
7194 "RUBY": true, 7195 "S": true, 7196 "SAMP": true, 7197 "SMALL": true, 7198 "SPAN": true, 7199 "STRONG": true, 7200 "SUB": true, 7201 "SUP": true, 7202 "U": true, 7203 "VAR": true, 7204 "ACRONYM": true, 7205 "LISTING": true, 7206 "STRIKE": true, 7207 "XMP": true, 7208 "BIG": true, 7209 "BLINK": true, 7210 "FONT": true, 7211 "MARQUEE": true, 7212 "NOBR": true, 7213 "TT": true 7214 }; 7215 7216 7217 var tableRelatedElements = { 7218 "colgroup": true, 7219 "table": true, 7220 "tbody": true, 7221 "tfoot": true, 7222 "thead": true, 7223 "tr": true 7224 }; 7225 7226 var scriptRelatedElements = { 7227 "script": true, 7228 "style": true, 7229 "plaintext": true, 7230 "xmp": true 7231 }; 7232 7233 var prohibitedHeadingNestingMap = jQuery.extend({ 7234 "H1": true, 7235 "H2": true, 7236 "H3": true, 7237 "H4": true, 7238 "H5": true, 7239 "H6": true 7240 }, prohibitedParagraphChildNamesMap); 7241 7242 var prohibitedTableNestingMap = { 7243 "CAPTION": true, 7244 "COL": true, 7245 "COLGROUP": true, 7246 "TBODY": true, 7247 "TD": true, 7248 "TFOOT": true, 7249 "TH": true, 7250 "THEAD": true, 7251 "TR": true 7252 }; 7253 7254 var prohibitedDefNestingMap = { 7255 "DD": true, 7256 "DT": true 7257 }; 7258 7259 var prohibitedNestingCombinationsMap = { 7260 "A": jQuery.extend({ 7261 "A": true 7262 }, prohibitedParagraphChildNamesMap), 7263 "DD": prohibitedDefNestingMap, 7264 "DT": prohibitedDefNestingMap, 7265 "LI": { 7266 "LI": true 7267 }, 7268 "NOBR": jQuery.extend({ 7269 "NOBR": true 7270 }, prohibitedParagraphChildNamesMap), 7271 "H1": prohibitedHeadingNestingMap, 7272 "H2": prohibitedHeadingNestingMap, 7273 "H3": prohibitedHeadingNestingMap, 7274 "H4": prohibitedHeadingNestingMap, 7275 "H5": prohibitedHeadingNestingMap, 7276 "H6": prohibitedHeadingNestingMap, 7277 "TD": prohibitedTableNestingMap, 7278 "TH": prohibitedTableNestingMap, 7279 // this is the same as namesOfElementsWithInlineContentsMap excluding a and h1-h6 elements above 7280 "ABBR": prohibitedParagraphChildNamesMap, 7281 "B": prohibitedParagraphChildNamesMap, 7282 "BDI": prohibitedParagraphChildNamesMap, 7283 "BDO": prohibitedParagraphChildNamesMap, 7284 "CITE": prohibitedParagraphChildNamesMap, 7285 "CODE": prohibitedParagraphChildNamesMap, 7286 "DFN": prohibitedParagraphChildNamesMap, 7287 "EM": prohibitedParagraphChildNamesMap, 7288 "I": prohibitedParagraphChildNamesMap, 7289 "KBD": prohibitedParagraphChildNamesMap, 7290 "MARK": prohibitedParagraphChildNamesMap, 7291 "P": prohibitedParagraphChildNamesMap, 7292 "PRE": prohibitedParagraphChildNamesMap, 7293 "Q": prohibitedParagraphChildNamesMap, 7294 "RP": prohibitedParagraphChildNamesMap, 7295 "RT": prohibitedParagraphChildNamesMap, 7296 "RUBY": prohibitedParagraphChildNamesMap, 7297 "S": prohibitedParagraphChildNamesMap, 7298 "SAMP": prohibitedParagraphChildNamesMap, 7299 "SMALL": prohibitedParagraphChildNamesMap, 7300 "SPAN": prohibitedParagraphChildNamesMap, 7301 "STRONG": prohibitedParagraphChildNamesMap, 7302 "SUB": prohibitedParagraphChildNamesMap, 7303 "SUP": prohibitedParagraphChildNamesMap, 7304 "U": prohibitedParagraphChildNamesMap, 7305 "VAR": prohibitedParagraphChildNamesMap, 7306 "ACRONYM": prohibitedParagraphChildNamesMap, 7307 "LISTING": prohibitedParagraphChildNamesMap, 7308 "STRIKE": prohibitedParagraphChildNamesMap, 7309 "XMP": prohibitedParagraphChildNamesMap, 7310 "BIG": prohibitedParagraphChildNamesMap, 7311 "BLINK": prohibitedParagraphChildNamesMap, 7312 "FONT": prohibitedParagraphChildNamesMap, 7313 "MARQUEE": prohibitedParagraphChildNamesMap, 7314 "TT": prohibitedParagraphChildNamesMap 7315 }; 7316 7317 // "An element with inline contents is an HTML element whose local name is a 7318 // name of an element with inline contents." 7319 function isElementWithInlineContents(node) { 7320 return isMappedHtmlElement(node, namesOfElementsWithInlineContentsMap); 7321 } 7322 7323 function isAllowedChild(child, parent_) { 7324 // "If parent is "colgroup", "table", "tbody", "tfoot", "thead", "tr", or 7325 // an HTML element with local name equal to one of those, and child is a 7326 // Text node whose data does not consist solely of space characters, return 7327 // false." 7328 if ((tableRelatedElements[parent_] || isHtmlElementInArray(parent_, ["colgroup", "table", "tbody", "tfoot", "thead", "tr"])) && typeof child == "object" && child.nodeType == Node.TEXT_NODE && !/^[ \t\n\f\r]*$/.test(child.data)) { 7329 return false; 7330 } 7331 7332 // "If parent is "script", "style", "plaintext", or "xmp", or an HTML 7333 // element with local name equal to one of those, and child is not a Text 7334 // node, return false." 7335 if ((scriptRelatedElements[parent_] || isHtmlElementInArray(parent_, ["script", "style", "plaintext", "xmp"])) && (typeof child != "object" || child.nodeType != Node.TEXT_NODE)) { 7336 return false;
7337 } 7338 7339 // "If child is a Document, DocumentFragment, or DocumentType, return 7340 // false." 7341 if (typeof child == "object" && (child.nodeType == Node.DOCUMENT_NODE || child.nodeType == Node.DOCUMENT_FRAGMENT_NODE || child.nodeType == Node.DOCUMENT_TYPE_NODE)) { 7342 return false; 7343 } 7344 7345 // "If child is an HTML element, set child to the local name of child." 7346 if (isAnyHtmlElement(child)) { 7347 child = child.tagName.toLowerCase(); 7348 } 7349 7350 // "If child is not a string, return true." 7351 if (typeof child != "string") { 7352 return true; 7353 } 7354 7355 // "If parent is an HTML element:" 7356 if (isAnyHtmlElement(parent_)) { 7357 7358 // "If child is "a", and parent or some ancestor of parent is an a, 7359 // return false." 7360 // 7361 // "If child is a prohibited paragraph child name and parent or some 7362 // ancestor of parent is an element with inline contents, return 7363 // false." 7364 // 7365 // "If child is "h1", "h2", "h3", "h4", "h5", or "h6", and parent or 7366 // some ancestor of parent is an HTML element with local name "h1", 7367 // "h2", "h3", "h4", "h5", or "h6", return false." 7368 // 7369 var ancestor = parent_; 7370 7371 while (ancestor) { 7372 if (child == "a" && isNamedHtmlElement(ancestor, 'a')) { 7373 return false; 7374 } 7375 if (prohibitedParagraphChildNamesMap[child.toUpperCase()] && isElementWithInlineContents(ancestor)) { 7376 return false; 7377 } 7378 if (/^h[1-6]$/.test(child) && isAnyHtmlElement(ancestor) && /^H[1-6]$/.test(ancestor.tagName)) { 7379 return false; 7380 } 7381 ancestor = ancestor.parentNode; 7382 } 7383 7384 // "Let parent be the local name of parent." 7385 parent_ = parent_.tagName.toLowerCase(); 7386 } 7387 7388 // "If parent is an Element or DocumentFragment, return true." 7389 if (typeof parent_ == "object" && (parent_.nodeType == Node.ELEMENT_NODE || parent_.nodeType == Node.DOCUMENT_FRAGMENT_NODE)) { 7390 return true; 7391 } 7392 7393 // "If parent is not a string, return false." 7394 if (typeof parent_ != "string") { 7395 return false; 7396 } 7397 7398 // "If parent is on the left-hand side of an entry on the following list, 7399 // then return true if child is listed on the right-hand side of that 7400 // entry, and false otherwise." 7401 switch (parent_) { 7402 case "colgroup": 7403 return child == "col"; 7404 case "table": 7405 return jQuery.inArray(child, ["caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr"]) != -1; 7406 case "tbody": 7407 case "thead": 7408 case "tfoot": 7409 return jQuery.inArray(child, ["td", "th", "tr"]) != -1; 7410 case "tr": 7411 return jQuery.inArray(child, ["td", "th"]) != -1; 7412 case "dl": 7413 return jQuery.inArray(child, ["dt", "dd"]) != -1; 7414 case "dir": 7415 case "ol": 7416 case "ul": 7417 return jQuery.inArray(child, ["dir", "li", "ol", "ul"]) != -1; 7418 case "hgroup": 7419 return (/^h[1-6]$/).test(child); 7420 } 7421 7422 // "If child is "body", "caption", "col", "colgroup", "frame", "frameset", 7423 // "head", "html", "tbody", "td", "tfoot", "th", "thead", or "tr", return 7424 // false." 7425 if (jQuery.inArray(child, ["body", "caption", "col", "colgroup", "frame", "frameset", "head", "html", "tbody", "td", "tfoot", "th", "thead", "tr"]) != -1) { 7426 return false; 7427 } 7428 7429 // "If child is "dd" or "dt" and parent is not "dl", return false." 7430 if (jQuery.inArray(child, ["dd", "dt"]) != -1 && parent_ != "dl") { 7431 return false; 7432 } 7433 7434 // "If child is "li" and parent is not "ol" or "ul", return false." 7435 if (child == "li" && parent_ != "ol" && parent_ != "ul") { 7436 return false; 7437 } 7438 7439 // "If parent is on the left-hand side of an entry on the following list 7440 // and child is listed on the right-hand side of that entry, return false." 7441 var leftSide = prohibitedNestingCombinationsMap[parent_.toUpperCase()]; 7442 if (leftSide) { 7443 var rightSide = leftSide[child.toUpperCase()]; 7444 if (rightSide) { 7445 return false; 7446 } 7447 } 7448 7449 // "Return true." 7450 return true; 7451 } 7452 7453 7454 //@} 7455 7456 ////////////////////////////////////// 7457 ///// Inline formatting commands ///// 7458 ////////////////////////////////////// 7459 7460 ///// Inline formatting command definitions ///// 7461 //@{ 7462 7463 // "A node node is effectively contained in a range range if range is not 7464 // collapsed, and at least one of the following holds:" 7465 function isEffectivelyContained(node, range) { 7466 7467 if (range.collapsed) { 7468 return false;
7469 } 7470 7471 // "node is contained in range." 7472 if (isContained(node, range)) { 7473 return true; 7474 } 7475 7476 // "node is range's start node, it is a Text node, and its length is 7477 // different from range's start offset." 7478 if (node == range.startContainer && node.nodeType == Node.TEXT_NODE && getNodeLength(node) != range.startOffset) { 7479 return true; 7480 } 7481 7482 // "node is range's end node, it is a Text node, and range's end offset is 7483 // not 0." 7484 if (node == range.endContainer && node.nodeType == Node.TEXT_NODE && range.endOffset != 0) { 7485 return true; 7486 } 7487 7488 // "node has at least one child; and all its children are effectively 7489 // contained in range; and either range's start node is not a descendant of 7490 // node or is not a Text node or range's start offset is zero; and either 7491 // range's end node is not a descendant of node or is not a Text node or 7492 // range's end offset is its end node's length." 7493 if (node.hasChildNodes() && $_(node.childNodes).every(function (child) { return isEffectivelyContained(child, range); }) 7494 && (!isDescendant(range.startContainer, node) 7495 || range.startContainer.nodeType != Node.TEXT_NODE 7496 || range.startOffset == 0) 7497 && (!isDescendant(range.endContainer, node) 7498 || range.endContainer.nodeType != Node.TEXT_NODE 7499 || range.endOffset == getNodeLength(range.endContainer))) { 7500 return true; 7501 } 7502 7503 return false; 7504 7505 } 7506 7507 // Like get(All)ContainedNodes(), but for effectively contained nodes. 7508 function getEffectivelyContainedNodes(range, condition) { 7509 7510 if (typeof condition == "undefined") { 7511 condition = function () { 7512 return true; 7513 }; 7514 } 7515 7516 var node = range.startContainer; 7517 7518 while (isEffectivelyContained(node.parentNode, range)) { 7519 node = node.parentNode; 7520 } 7521 7522 var stop = nextNodeDescendants(range.endContainer); 7523 7524 var nodeList = []; 7525 7526 while (isBefore(node, stop)) { 7527 7528 if (isEffectivelyContained(node, range) && condition(node)) { 7529 nodeList.push(node); 7530 node = nextNodeDescendants(node); 7531 continue; 7532 } 7533 7534 node = nextNode(node); 7535 7536 } 7537 7538 return nodeList; 7539 7540 } 7541 7542 function getAllEffectivelyContainedNodes(range, condition) { 7543 7544 if (typeof condition == "undefined") { 7545 condition = function () { 7546 return true; 7547 }; 7548 } 7549 7550 var node = range.startContainer; 7551 7552 while (isEffectivelyContained(node.parentNode, range)) { 7553 node = node.parentNode; 7554 } 7555 7556 var stop = nextNodeDescendants(range.endContainer); 7557 7558 var nodeList = []; 7559 7560 while (isBefore(node, stop)) { 7561 7562 if (isEffectivelyContained(node, range) && condition(node)) { 7563 nodeList.push(node); 7564 } 7565 7566 node = nextNode(node); 7567 } 7568 7569 return nodeList; 7570 } 7571 7572 // "A modifiable element is a b, em, i, s, span, strong, sub, sup, or u element 7573 // with no attributes except possibly style; or a font element with no 7574 // attributes except possibly style, color, face, and/or size; or an a element 7575 // with no attributes except possibly style and/or href." 7576 function isModifiableElement(node) { 7577 if (!isAnyHtmlElement(node)) { 7578 return false; 7579 } 7580 7581 if (jQuery.inArray(node.tagName, ["B", "EM", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"]) != -1) { 7582 if (node.attributes.length == 0) { 7583 return true; 7584 } 7585 7586 if (node.attributes.length == 1 && hasAttribute(node, "style")) { 7587 return true; 7588 } 7589 } 7590 7591 if (node.tagName == "FONT" || node.tagName == "A") { 7592 var numAttrs = node.attributes.length; 7593 7594 if (hasAttribute(node, "style")) { 7595 numAttrs--; 7596 } 7597 7598 if (node.tagName == "FONT") { 7599 if (hasAttribute(node, "color")) { 7600 numAttrs--; 7601 } 7602 7603 if (hasAttribute(node, "face")) { 7604 numAttrs--; 7605 } 7606 7607 if (hasAttribute(node, "size")) { 7608 numAttrs--; 7609 } 7610 } 7611 7612 if (node.tagName == "A" && hasAttribute(node, "href")) { 7613 numAttrs--; 7614 } 7615 7616 if (numAttrs == 0) { 7617 return true; 7618 } 7619 } 7620 7621 return false; 7622 } 7623 7624 function isSimpleModifiableElement(node) { 7625 // "A simple modifiable element is an HTML element for which at least one 7626 // of the following holds:" 7627 if (!isAnyHtmlElement(node)) { 7628 return false; 7629 } 7630 7631 // Only these elements can possibly be a simple modifiable element. 7632 if (jQuery.inArray(node.tagName, ["A", "B", "EM", "FONT", "I", "S", "SPAN", "STRIKE", "STRONG", "SUB", "SUP", "U"]) == -1) { 7633 return false;
7634 } 7635 7636 // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u 7637 // element with no attributes." 7638 if (node.attributes.length == 0) { 7639 return true; 7640 } 7641 7642 // If it's got more than one attribute, everything after this fails. 7643 if (node.attributes.length > 1) { 7644 return false; 7645 } 7646 7647 // "It is an a, b, em, font, i, s, span, strike, strong, sub, sup, or u 7648 // element with exactly one attribute, which is style, which sets no CSS 7649 // properties (including invalid or unrecognized properties)." 7650 // 7651 // Not gonna try for invalid or unrecognized. 7652 if (hasAttribute(node, "style") && getStyleLength(node) == 0) { 7653 return true; 7654 } 7655 7656 // "It is an a element with exactly one attribute, which is href." 7657 if (node.tagName == "A" && hasAttribute(node, "href")) { 7658 return true; 7659 } 7660 7661 // "It is a font element with exactly one attribute, which is either color, 7662 // face, or size." 7663 if (node.tagName == "FONT" && (hasAttribute(node, "color") || hasAttribute(node, "face") || hasAttribute(node, "size"))) { 7664 return true; 7665 } 7666 7667 // "It is a b or strong element with exactly one attribute, which is style, 7668 // and the style attribute sets exactly one CSS property (including invalid 7669 // or unrecognized properties), which is "font-weight"." 7670 if ((node.tagName == "B" || node.tagName == "STRONG") && hasAttribute(node, "style") && getStyleLength(node) == 1 && node.style.fontWeight != "") { 7671 return true; 7672 } 7673 7674 // "It is an i or em element with exactly one attribute, which is style, 7675 // and the style attribute sets exactly one CSS property (including invalid 7676 // or unrecognized properties), which is "font-style"." 7677 if ((node.tagName == "I" || node.tagName == "EM") && hasAttribute(node, "style") && getStyleLength(node) == 1 && node.style.fontStyle != "") { 7678 return true; 7679 } 7680 7681 // "It is an a, font, or span element with exactly one attribute, which is 7682 // style, and the style attribute sets exactly one CSS property (including 7683 // invalid or unrecognized properties), and that property is not 7684 // "text-decoration"." 7685 if ((node.tagName == "A" || node.tagName == "FONT" || node.tagName == "SPAN") && hasAttribute(node, "style") && getStyleLength(node) == 1 && node.style.textDecoration == "") { 7686 return true; 7687 } 7688 7689 // "It is an a, font, s, span, strike, or u element with exactly one 7690 // attribute, which is style, and the style attribute sets exactly one CSS 7691 // property (including invalid or unrecognized properties), which is 7692 // "text-decoration", which is set to "line-through" or "underline" or 7693 // "overline" or "none"." 7694 if (jQuery.inArray(node.tagName, ["A", "FONT", "S", "SPAN", "STRIKE", "U"]) != -1 && hasAttribute(node, "style") && getStyleLength(node) == 1 && (node.style.textDecoration == "line-through" || node.style.textDecoration == "underline" || node.style.textDecoration == "overline" || node.style.textDecoration == "none")) { 7695 return true; 7696 } 7697 7698 return false; 7699 } 7700 7701 // "Two quantities are equivalent values for a command if either both are null, 7702 // or both are strings and they're equal and the command does not define any 7703 // equivalent values, or both are strings and the command defines equivalent 7704 // values and they match the definition." 7705 function areEquivalentValues(command, val1, val2) { 7706 if (val1 === null && val2 === null) { 7707 return true; 7708 } 7709 7710 if (typeof val1 == "string" && typeof val2 == "string" && val1 == val2 && !(commands[command].hasOwnProperty("equivalentValues"))) { 7711 return true; 7712 } 7713 7714 if (typeof val1 == "string" && typeof val2 == "string" && commands[command].hasOwnProperty("equivalentValues") && commands[command].equivalentValues(val1, val2)) { 7715 return true; 7716 } 7717 7718 return false; 7719 } 7720 7721 // "Two quantities are loosely equivalent values for a command if either they 7722 // are equivalent values for the command, or if the command is the fontSize 7723 // command; one of the quantities is one of "xx-small", "small", "medium", 7724 // "large", "x-large", "xx-large", or "xxx-large"; and the other quantity is 7725 // the resolved value of "font-size" on a font element whose size attribute has 7726 // the corresponding value set ("1" through "7" respectively)." 7727 function areLooselyEquivalentValues(command, val1, val2) { 7728 if (areEquivalentValues(command, val1, val2)) { 7729 return true; 7730 } 7731 7732 if (command != "fontsize" || typeof val1 != "string" || typeof val2 != "string") { 7733 return false;
7734 } 7735 7736 // Static variables in JavaScript? 7737 var callee = areLooselyEquivalentValues; 7738 if (callee.sizeMap === undefined) { 7739 callee.sizeMap = {}; 7740 var font = document.createElement("font"); 7741 document.body.appendChild(font); 7742 $_(["xx-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"]).forEach(function (keyword) { 7743 font.size = cssSizeToLegacy(keyword); 7744 callee.sizeMap[keyword] = $_.getComputedStyle(font).fontSize; 7745 }); 7746 document.body.removeChild(font); 7747 } 7748 7749 return val1 === callee.sizeMap[val2] || val2 === callee.sizeMap[val1]; 7750 } 7751 7752 //@} 7753 ///// Assorted inline formatting command algorithms ///// 7754 //@{ 7755 7756 function getEffectiveCommandValue(node, command) { 7757 // "If neither node nor its parent is an Element, return null." 7758 if (node.nodeType != Node.ELEMENT_NODE && (!node.parentNode || node.parentNode.nodeType != Node.ELEMENT_NODE)) { 7759 return null; 7760 } 7761 7762 // "If node is not an Element, return the effective command value of its 7763 // parent for command." 7764 if (node.nodeType != Node.ELEMENT_NODE) { 7765 return getEffectiveCommandValue(node.parentNode, command); 7766 } 7767 7768 // "If command is "createLink" or "unlink":" 7769 if (command == "createlink" || command == "unlink") { 7770 // "While node is not null, and is not an a element that has an href 7771 // attribute, set node to its parent." 7772 while (node && (!isAnyHtmlElement(node) || node.tagName != "A" || !hasAttribute(node, "href"))) { 7773 node = node.parentNode; 7774 } 7775 7776 // "If node is null, return null." 7777 if (!node) { 7778 return null; 7779 } 7780 7781 // "Return the value of node's href attribute." 7782 return node.getAttribute("href"); 7783 } 7784 7785 // "If command is "backColor" or "hiliteColor":" 7786 if (command == "backcolor" || command == "hilitecolor") { 7787 // "While the resolved value of "background-color" on node is any 7788 // fully transparent value, and node's parent is an Element, set 7789 // node to its parent." 7790 // 7791 // Another lame hack to avoid flawed APIs. 7792 while (($_.getComputedStyle(node).backgroundColor == "rgba(0, 0, 0, 0)" || $_.getComputedStyle(node).backgroundColor === "" || $_.getComputedStyle(node).backgroundColor == "transparent") && node.parentNode && node.parentNode.nodeType == Node.ELEMENT_NODE) { 7793 node = node.parentNode; 7794 } 7795 7796 // "If the resolved value of "background-color" on node is a fully 7797 // transparent value, return "rgb(255, 255, 255)"." 7798 if ($_.getComputedStyle(node).backgroundColor == "rgba(0, 0, 0, 0)" || $_.getComputedStyle(node).backgroundColor === "" || $_.getComputedStyle(node).backgroundColor == "transparent") { 7799 return "rgb(255, 255, 255)"; 7800 } 7801 7802 // "Otherwise, return the resolved value of "background-color" for 7803 // node." 7804 return $_.getComputedStyle(node).backgroundColor; 7805 } 7806 7807 // "If command is "subscript" or "superscript":" 7808 if (command == "subscript" || command == "superscript") { 7809 // "Let affected by subscript and affected by superscript be two 7810 // boolean variables, both initially false." 7811 var affectedBySubscript = false; 7812 var affectedBySuperscript = false; 7813 7814 // "While node is an inline node:" 7815 while (isInlineNode(node)) { 7816 var verticalAlign = $_.getComputedStyle(node).verticalAlign; 7817 7818 // "If node is a sub, set affected by subscript to true." 7819 if (isNamedHtmlElement(node, 'sub')) { 7820 affectedBySubscript = true; 7821 // "Otherwise, if node is a sup, set affected by superscript to 7822 // true." 7823 } else if (isNamedHtmlElement(node, 'sup')) { 7824 affectedBySuperscript = true; 7825 } 7826 7827 // "Set node to its parent." 7828 node = node.parentNode; 7829 } 7830 7831 // "If affected by subscript and affected by superscript are both true, 7832 // return the string "mixed"." 7833 if (affectedBySubscript && affectedBySuperscript) { 7834 return "mixed"; 7835 } 7836 7837 // "If affected by subscript is true, return "subscript"." 7838 if (affectedBySubscript) { 7839 return "subscript"; 7840 } 7841 7842 // "If affected by superscript is true, return "superscript"." 7843 if (affectedBySuperscript) { 7844 return "superscript"; 7845 } 7846 7847 // "Return null." 7848 return null; 7849 } 7850 7851 // "If command is "strikethrough", and the "text-decoration" property of 7852 // node or any of its ancestors has resolved value containing 7853 // "line-through", return "line-through". Otherwise, return null." 7854 if (command == "strikethrough") { 7855 do { 7856 if ($_.getComputedStyle(node).textDecoration.indexOf("line-through") != -1) { 7857 return "line-through"; 7858 } 7859 node = node.parentNode; 7860 } while (node && node.nodeType == Node.ELEMENT_NODE); 7861 return null; 7862 } 7863 7864 // "If command is "underline", and the "text-decoration" property of node 7865 // or any of its ancestors has resolved value containing "underline", 7866 // return "underline". Otherwise, return null." 7867 if (command == "underline") { 7868 do { 7869 if ($_.getComputedStyle(node).textDecoration.indexOf("underline") != -1) { 7870 return "underline"; 7871 } 7872 node = node.parentNode; 7873 } while (node && node.nodeType == Node.ELEMENT_NODE); 7874 return null; 7875 } 7876 7877 if (!commands[command].hasOwnProperty("relevantCssProperty")) { 7878 throw "Bug: no relevantCssProperty for " + command + " in getEffectiveCommandValue"; 7879 } 7880 7881 // "Return the resolved value for node of the relevant CSS property for 7882 // command." 7883 return $_.getComputedStyle(node)[commands[command].relevantCssProperty].toString(); 7884 } 7885 7886 function getSpecifiedCommandValue(element, command) { 7887 // "If command is "backColor" or "hiliteColor" and element's display 7888 // property does not have resolved value "inline", return null." 7889 if ((command == "backcolor" || command == "hilitecolor") && $_.getComputedStyle(element).display != "inline") { 7890 return null; 7891 } 7892 7893 // "If command is "createLink" or "unlink":" 7894 if (command == "createlink" || command == "unlink") { 7895 // "If element is an a element and has an href attribute, return the 7896 // value of that attribute." 7897 if (isAnyHtmlElement(element) && element.tagName == "A" && hasAttribute(element, "href")) { 7898 return element.getAttribute("href"); 7899 } 7900 7901 // "Return null." 7902 return null; 7903 } 7904 7905 // "If command is "subscript" or "superscript":" 7906 if (command == "subscript" || command == "superscript") { 7907 // "If element is a sup, return "superscript"." 7908 if (isNamedHtmlElement(element, 'sup')) { 7909 return "superscript"; 7910 } 7911 7912 // "If element is a sub, return "subscript"." 7913 if (isNamedHtmlElement(element, 'sub')) { 7914 return "subscript"; 7915 } 7916 7917 // "Return null." 7918 return null; 7919 } 7920 7921 // "If command is "strikethrough", and element has a style attribute set, 7922 // and that attribute sets "text-decoration":" 7923 if (command == "strikethrough" && element.style.textDecoration != "") { 7924 // "If element's style attribute sets "text-decoration" to a value 7925 // containing "line-through", return "line-through"." 7926 if (element.style.textDecoration.indexOf("line-through") != -1) { 7927 return "line-through"; 7928 } 7929 7930 // "Return null." 7931 return null; 7932 } 7933 7934 // "If command is "strikethrough" and element is a s or strike element, 7935 // return "line-through"." 7936 if (command == "strikethrough" && isHtmlElementInArray(element, ["S", "STRIKE"])) { 7937 return "line-through"; 7938 } 7939 7940 // "If command is "underline", and element has a style attribute set, and 7941 // that attribute sets "text-decoration":" 7942 if (command == "underline" && element.style.textDecoration != "") { 7943 // "If element's style attribute sets "text-decoration" to a value 7944 // containing "underline", return "underline"." 7945 if (element.style.textDecoration.indexOf("underline") != -1) { 7946 return "underline"; 7947 } 7948 7949 // "Return null." 7950 return null; 7951 } 7952 7953 // "If command is "underline" and element is a u element, return 7954 // "underline"." 7955 if (command == "underline" && isNamedHtmlElement(element, 'U')) { 7956 return "underline"; 7957 } 7958 7959 // "Let property be the relevant CSS property for command." 7960 var property = commands[command].relevantCssProperty; 7961 7962 // "If property is null, return null." 7963 if (property === null) { 7964 return null; 7965 } 7966 7967 // "If element has a style attribute set, and that attribute has the 7968 // effect of setting property, return the value that it sets property to." 7969 if (element.style[property] != "") { 7970 return element.style[property]; 7971 } 7972 7973 // "If element is a font element that has an attribute whose effect is 7974 // to create a presentational hint for property, return the value that the 7975 // hint sets property to. (For a size of 7, this will be the non-CSS value 7976 // "xxx-large".)" 7977 if (isHtmlNamespace(element.namespaceURI) && element.tagName == "FONT") { 7978 if (property == "color" && hasAttribute(element, "color")) { 7979 return element.color; 7980 } 7981 if (property == "fontFamily" && hasAttribute(element, "face")) { 7982 return element.face; 7983 } 7984 if (property == "fontSize" && hasAttribute(element, "size")) { 7985 // This is not even close to correct in general. 7986 var size = parseInt(element.size, 10); 7987 if (size < 1) { 7988 size = 1; 7989 } 7990 if (size > 7) { 7991 size = 7; 7992 } 7993 return { 7994 1: "xx-small", 7995 2: "small", 7996 3: "medium", 7997 4: "large", 7998 5: "x-large", 7999 6: "xx-large", 8000 7: "xxx-large" 8001 }[size]; 8002 } 8003 } 8004 8005 // "If element is in the following list, and property is equal to the 8006 // CSS property name listed for it, return the string listed for it." 8007 // 8008 // A list follows, whose meaning is copied here. 8009 if (property == "fontWeight" && (element.tagName == "B" || element.tagName == "STRONG")) { 8010 return "bold"; 8011 } 8012 if (property == "fontStyle" && (element.tagName == "I" || element.tagName == "EM")) { 8013 return "italic"; 8014 } 8015 8016 // "Return null." 8017 return null; 8018 } 8019 8020 function reorderModifiableDescendants(node, command, newValue, range) { 8021 // "Let candidate equal node." 8022 var candidate = node; 8023 8024 // "While candidate is a modifiable element, and candidate has exactly one 8025 // child, and that child is also a modifiable element, and candidate is not 8026 // a simple modifiable element or candidate's specified command value for 8027 // command is not equivalent to new value, set candidate to its child." 8028 while (isModifiableElement(candidate) && candidate.childNodes.length == 1 && isModifiableElement(candidate.firstChild) && (!isSimpleModifiableElement(candidate) || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue))) { 8029 candidate = candidate.firstChild; 8030 } 8031
8032 // "If candidate is node, or is not a simple modifiable element, or its 8033 // specified command value is not equivalent to new value, or its effective 8034 // command value is not loosely equivalent to new value, abort these 8035 // steps." 8036 if (candidate == node || !isSimpleModifiableElement(candidate) || !areEquivalentValues(command, getSpecifiedCommandValue(candidate, command), newValue) || !areLooselyEquivalentValues(command, getEffectiveCommandValue(candidate, command), newValue)) { 8037 return; 8038 } 8039 8040 // "While candidate has children, insert the first child of candidate into 8041 // candidate's parent immediately before candidate, preserving ranges." 8042 while (candidate.hasChildNodes()) { 8043 movePreservingRanges(candidate.firstChild, candidate.parentNode, getIndexInParent(candidate), range); 8044 } 8045 8046 // "Insert candidate into node's parent immediately after node." 8047 node.parentNode.insertBefore(candidate, node.nextSibling); 8048 8049 // "Append the node as the last child of candidate, preserving ranges." 8050 movePreservingRanges(node, candidate, -1, range); 8051 } 8052 8053 var recordValuesCommands = ["subscript", "bold", "fontname", "fontsize", "forecolor", "hilitecolor", "italic", "strikethrough", "underline"]; 8054 8055 function recordValues(nodeList) { 8056 // "Let values be a list of (node, command, specified command value) 8057 // triples, initially empty." 8058 var values = []; 8059 8060 // "For each node in node list, for each command in the list "subscript", 8061 // "bold", "fontName", "fontSize", "foreColor", "hiliteColor", "italic", 8062 // "strikethrough", and "underline" in that order:" 8063 8064 // Ensure we have a plain array to avoid the potential performance 8065 // overhead of a NodeList 8066 var nodes = jQuery.makeArray(nodeList); 8067 var i, j; 8068 var node; 8069 var command; 8070 var ancestor; 8071 var specifiedCommandValue; 8072 for (i = 0; i < nodes.length; i++) { 8073 node = nodes[i]; 8074 for (j = 0; j < recordValuesCommands.length; j++) { 8075 command = recordValuesCommands[j]; 8076 8077 // "Let ancestor equal node." 8078 ancestor = node; 8079 8080 // "If ancestor is not an Element, set it to its parent." 8081 if (ancestor.nodeType != 1) { 8082 ancestor = ancestor.parentNode; 8083 } 8084 8085 // "While ancestor is an Element and its specified command value 8086 // for command is null, set it to its parent." 8087 specifiedCommandValue = null; 8088 while (ancestor && ancestor.nodeType == 1 && (specifiedCommandValue = getSpecifiedCommandValue(ancestor, command)) === null) { 8089 ancestor = ancestor.parentNode; 8090 } 8091 8092 // "If ancestor is an Element, add (node, command, ancestor's 8093 // specified command value for command) to values. Otherwise add 8094 // (node, command, null) to values." 8095 values.push([node, command, specifiedCommandValue]); 8096 } 8097 } 8098 8099 // "Return values." 8100 return values; 8101 } 8102 8103 //@} 8104 ///// Clearing an element's value ///// 8105 //@{ 8106 8107 function clearValue(element, command, range) { 8108 // "If element is not editable, return the empty list." 8109 if (!isEditable(element)) { 8110 return []; 8111 } 8112 8113 // "If element's specified command value for command is null, return the 8114 // empty list." 8115 if (getSpecifiedCommandValue(element, command) === null) { 8116 return []; 8117 } 8118 8119 // "If element is a simple modifiable element:" 8120 if (isSimpleModifiableElement(element)) { 8121 // "Let children be the children of element." 8122 var children = Array.prototype.slice.call(toArray(element.childNodes)); 8123 8124 // "For each child in children, insert child into element's parent 8125 // immediately before element, preserving ranges." 8126 var i; 8127 for (i = 0; i < children.length; i++) { 8128 movePreservingRanges(children[i], element.parentNode, getIndexInParent(element), range); 8129 } 8130 8131 // "Remove element from its parent." 8132 element.parentNode.removeChild(element); 8133 8134 // "Return children." 8135 return children; 8136 } 8137 8138 // "If command is "strikethrough", and element has a style attribute that 8139 // sets "text-decoration" to some value containing "line-through", delete 8140 // "line-through" from the value." 8141 if (command == "strikethrough" && element.style.textDecoration.indexOf("line-through") != -1) { 8142 if (element.style.textDecoration == "line-through") { 8143 element.style.textDecoration = ""; 8144 } else { 8145 element.style.textDecoration = element.style.textDecoration.replace("line-through", ""); 8146 } 8147 if (element.getAttribute("style") == "") { 8148 element.removeAttribute("style"); 8149 } 8150 } 8151 8152 // "If command is "underline", and element has a style attribute that sets 8153 // "text-decoration" to some value containing "underline", delete 8154 // "underline" from the value." 8155 if (command == "underline" && element.style.textDecoration.indexOf("underline") != -1) { 8156 if (element.style.textDecoration == "underline") { 8157 element.style.textDecoration = ""; 8158 } else { 8159 element.style.textDecoration = element.style.textDecoration.replace("underline", ""); 8160 } 8161 if (element.getAttribute("style") == "") { 8162 element.removeAttribute("style"); 8163 } 8164 } 8165 8166 // "If the relevant CSS property for command is not null, unset the CSS 8167 // property property of element." 8168 if (commands[command].relevantCssProperty !== null) { 8169 element.style[commands[command].relevantCssProperty] = ''; 8170 if (element.getAttribute("style") == "") { 8171 element.removeAttribute("style"); 8172 } 8173 } 8174 8175 // "If element is a font element:" 8176 if (isHtmlNamespace(element.namespaceURI) && element.tagName == "FONT") { 8177 // "If command is "foreColor", unset element's color attribute, if set." 8178 if (command == "forecolor") { 8179 element.removeAttribute("color"); 8180 } 8181 8182 // "If command is "fontName", unset element's face attribute, if set." 8183 if (command == "fontname") { 8184 element.removeAttribute("face"); 8185 } 8186 8187 // "If command is "fontSize", unset element's size attribute, if set." 8188 if (command == "fontsize") { 8189 element.removeAttribute("size"); 8190 } 8191 } 8192 8193 // "If element is an a element and command is "createLink" or "unlink", 8194 // unset the href property of element." 8195 if (isNamedHtmlElement(element, 'A') && (command == "createlink" || command == "unlink")) { 8196 element.removeAttribute("href"); 8197 } 8198 8199 // "If element's specified command value for command is null, return the 8200 // empty list." 8201 if (getSpecifiedCommandValue(element, command) === null) { 8202 return []; 8203 } 8204 8205 // "Set the tag name of element to "span", and return the one-node list 8206 // consisting of the result." 8207 return [setTagName(element, "span", range)]; 8208 } 8209 8210 //@} 8211 ///// Forcing the value of a node ///// 8212 //@{ 8213 8214 function forceValue(node, command, newValue, range) { 8215 var children = []; 8216 var i; 8217 var specifiedValue; 8218 8219 // "If node's parent is null, abort this algorithm." 8220 if (!node.parentNode) { 8221 return; 8222 } 8223 8224 // "If new value is null, abort this algorithm." 8225 if (newValue === null) { 8226 return; 8227 } 8228 8229 // "If node is an allowed child of "span":" 8230 if (isAllowedChild(node, "span")) { 8231 // "Reorder modifiable descendants of node's previousSibling." 8232 reorderModifiableDescendants(node.previousSibling, command, newValue, range); 8233 8234 // "Reorder modifiable descendants of node's nextSibling." 8235 reorderModifiableDescendants(node.nextSibling, command, newValue, range); 8236 8237 // "Wrap the one-node list consisting of node, with sibling criteria 8238 // returning true for a simple modifiable element whose specified 8239 // command value is equivalent to new value and whose effective command 8240 // value is loosely equivalent to new value and false otherwise, and 8241 // with new parent instructions returning null." 8242 wrap( 8243 [node], 8244 function (node) { 8245 return isSimpleModifiableElement(node) && areEquivalentValues(command, getSpecifiedCommandValue(node, command), newValue) && areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue); 8246 }, 8247 function () { 8248 return null; 8249 }, 8250 range 8251 ); 8252 } 8253 8254 // "If the effective command value of command is loosely equivalent to new 8255 // value on node, abort this algorithm." 8256 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { 8257 return; 8258 } 8259 8260 // "If node is not an allowed child of "span":" 8261 if (!isAllowedChild(node, "span")) { 8262 // "Let children be all children of node, omitting any that are 8263 // Elements whose specified command value for command is neither null 8264 // nor equivalent to new value." 8265 for (i = 0; i < node.childNodes.length; i++) { 8266 if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) { 8267 specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command); 8268 8269 if (specifiedValue !== null && !areEquivalentValues(command, newValue, specifiedValue)) { 8270 continue; 8271 } 8272 } 8273 children.push(node.childNodes[i]); 8274 } 8275 8276 // "Force the value of each Node in children, with command and new 8277 // value as in this invocation of the algorithm." 8278 for (i = 0; i < children.length; i++) { 8279 forceValue(children[i], command, newValue, range); 8280 } 8281 8282 // "Abort this algorithm." 8283 return; 8284 } 8285 8286 // "If the effective command value of command is loosely equivalent to new 8287 // value on node, abort this algorithm." 8288 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { 8289 return; 8290 } 8291 8292 // "Let new parent be null." 8293 var newParent = null; 8294 8295 // "If the CSS styling flag is false:" 8296 if (!cssStylingFlag) { 8297 // "If command is "bold" and new value is "bold", let new parent be the 8298 // result of calling createElement("b") on the ownerDocument of node." 8299 if (command == "bold" && (newValue == "bold" || newValue == "700")) { 8300 newParent = node.ownerDocument.createElement("b"); 8301 } 8302 8303 // "If command is "italic" and new value is "italic", let new parent be 8304 // the result of calling createElement("i") on the ownerDocument of 8305 // node." 8306 if (command == "italic" && newValue == "italic") { 8307 newParent = node.ownerDocument.createElement("i"); 8308 } 8309 8310 // "If command is "strikethrough" and new value is "line-through", let 8311 // new parent be the result of calling createElement("s") on the 8312 // ownerDocument of node." 8313 if (command == "strikethrough" && newValue == "line-through") { 8314 newParent = node.ownerDocument.createElement("s"); 8315 } 8316 8317 // "If command is "underline" and new value is "underline", let new 8318 // parent be the result of calling createElement("u") on the 8319 // ownerDocument of node." 8320 if (command == "underline" && newValue == "underline") { 8321 newParent = node.ownerDocument.createElement("u"); 8322 } 8323 8324 // "If command is "foreColor", and new value is fully opaque with red, 8325 // green, and blue components in the range 0 to 255:" 8326 if (command == "forecolor" && parseSimpleColor(newValue)) { 8327 // "Let new parent be the result of calling createElement("span") 8328 // on the ownerDocument of node." 8329 // NOTE: modified this process to create span elements with style attributes 8330 // instead of oldschool font tags with color attributes 8331 newParent = node.ownerDocument.createElement("span"); 8332 8333 // "If new value is an extended color keyword, set the color 8334 // attribute of new parent to new value." 8335 // 8336 // "Otherwise, set the color attribute of new parent to the result
8337 // of applying the rules for serializing simple color values to new 8338 // value (interpreted as a simple color)." 8339 jQuery(newParent).css('color', parseSimpleColor(newValue)); 8340 } 8341 8342 // "If command is "fontName", let new parent be the result of calling 8343 // createElement("font") on the ownerDocument of node, then set the 8344 // face attribute of new parent to new value." 8345 if (command == "fontname") { 8346 newParent = node.ownerDocument.createElement("font"); 8347 newParent.face = newValue; 8348 } 8349 } 8350 8351 // "If command is "createLink" or "unlink":" 8352 if (command == "createlink" || command == "unlink") { 8353 // "Let new parent be the result of calling createElement("a") on the 8354 // ownerDocument of node." 8355 newParent = node.ownerDocument.createElement("a"); 8356 8357 // "Set the href attribute of new parent to new value." 8358 newParent.setAttribute("href", newValue); 8359 8360 // "Let ancestor be node's parent." 8361 var ancestor = node.parentNode; 8362 8363 // "While ancestor is not null:" 8364 while (ancestor) { 8365 // "If ancestor is an a, set the tag name of ancestor to "span", 8366 // and let ancestor be the result." 8367 if (isNamedHtmlElement(ancestor, 'A')) { 8368 ancestor = setTagName(ancestor, "span", range); 8369 } 8370 8371 // "Set ancestor to its parent." 8372 ancestor = ancestor.parentNode; 8373 } 8374 } 8375 8376 // "If command is "fontSize"; and new value is one of "xx-small", "small", 8377 // "medium", "large", "x-large", "xx-large", or "xxx-large"; and either the 8378 // CSS styling flag is false, or new value is "xxx-large": let new parent 8379 // be the result of calling createElement("font") on the ownerDocument of 8380 // node, then set the size attribute of new parent to the number from the 8381 // following table based on new value: [table omitted]" 8382 if (command == "fontsize" && jQuery.inArray(newValue, ["xx-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"]) != -1 && (!cssStylingFlag || newValue == "xxx-large")) { 8383 newParent = node.ownerDocument.createElement("font"); 8384 newParent.size = cssSizeToLegacy(newValue); 8385 } 8386 8387 // "If command is "subscript" or "superscript" and new value is 8388 // "subscript", let new parent be the result of calling 8389 // createElement("sub") on the ownerDocument of node." 8390 if ((command == "subscript" || command == "superscript") && newValue == "subscript") { 8391 newParent = node.ownerDocument.createElement("sub"); 8392 } 8393 8394 // "If command is "subscript" or "superscript" and new value is 8395 // "superscript", let new parent be the result of calling 8396 // createElement("sup") on the ownerDocument of node." 8397 if ((command == "subscript" || command == "superscript") && newValue == "superscript") { 8398 newParent = node.ownerDocument.createElement("sup"); 8399 } 8400 8401 // "If new parent is null, let new parent be the result of calling 8402 // createElement("span") on the ownerDocument of node." 8403 if (!newParent) { 8404 newParent = node.ownerDocument.createElement("span"); 8405 } 8406 8407 // "Insert new parent in node's parent before node." 8408 node.parentNode.insertBefore(newParent, node); 8409 8410 // "If the effective command value of command for new parent is not loosely 8411 // equivalent to new value, and the relevant CSS property for command is 8412 // not null, set that CSS property of new parent to new value (if the new 8413 // value would be valid)." 8414 var property = commands[command].relevantCssProperty; 8415 if (property !== null && !areLooselyEquivalentValues(command, getEffectiveCommandValue(newParent, command), newValue)) { 8416 newParent.style[property] = newValue; 8417 } 8418 8419 // "If command is "strikethrough", and new value is "line-through", and the 8420 // effective command value of "strikethrough" for new parent is not 8421 // "line-through", set the "text-decoration" property of new parent to 8422 // "line-through"." 8423 if (command == "strikethrough" && newValue == "line-through" && getEffectiveCommandValue(newParent, "strikethrough") != "line-through") { 8424 newParent.style.textDecoration = "line-through"; 8425 } 8426 8427 // "If command is "underline", and new value is "underline", and the 8428 // effective command value of "underline" for new parent is not 8429 // "underline", set the "text-decoration" property of new parent to 8430 // "underline"." 8431 if (command == "underline" && newValue == "underline" && getEffectiveCommandValue(newParent, "underline") != "underline") { 8432 newParent.style.textDecoration = "underline"; 8433 } 8434 8435 // "Append node to new parent as its last child, preserving ranges." 8436 movePreservingRanges(node, newParent, newParent.childNodes.length, range); 8437 8438 // "If node is an Element and the effective command value of command for 8439 // node is not loosely equivalent to new value:" 8440 if (node.nodeType == Node.ELEMENT_NODE && !areEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { 8441 // "Insert node into the parent of new parent before new parent, 8442 // preserving ranges." 8443 movePreservingRanges(node, newParent.parentNode, getIndexInParent(newParent), range); 8444 8445 // "Remove new parent from its parent." 8446 newParent.parentNode.removeChild(newParent); 8447 8448 // "Let children be all children of node, omitting any that are 8449 // Elements whose specified command value for command is neither null 8450 // nor equivalent to new value." 8451 children = []; 8452 for (i = 0; i < node.childNodes.length; i++) { 8453 if (node.childNodes[i].nodeType == Node.ELEMENT_NODE) { 8454 specifiedValue = getSpecifiedCommandValue(node.childNodes[i], command); 8455 8456 if (specifiedValue !== null && !areEquivalentValues(command, newValue, specifiedValue)) { 8457 continue; 8458 } 8459 } 8460 children.push(node.childNodes[i]); 8461 } 8462 8463 // "Force the value of each Node in children, with command and new 8464 // value as in this invocation of the algorithm." 8465 for (i = 0; i < children.length; i++) { 8466 forceValue(children[i], command, newValue, range); 8467 } 8468 } 8469 } 8470 8471 //@} 8472 ///// Pushing down values ///// 8473 //@{ 8474 8475 function pushDownValues(node, command, newValue, range) { 8476 // "If node's parent is not an Element, abort this algorithm." 8477 if (!node.parentNode || node.parentNode.nodeType != Node.ELEMENT_NODE) { 8478 return; 8479 } 8480 8481 // "If the effective command value of command is loosely equivalent to new 8482 // value on node, abort this algorithm." 8483 if (areLooselyEquivalentValues(command, getEffectiveCommandValue(node, command), newValue)) { 8484 return; 8485 } 8486 8487 // "Let current ancestor be node's parent." 8488 var currentAncestor = node.parentNode; 8489 8490 // "Let ancestor list be a list of Nodes, initially empty." 8491 var ancestorList = []; 8492 8493 // "While current ancestor is an editable Element and the effective command 8494 // value of command is not loosely equivalent to new value on it, append 8495 // current ancestor to ancestor list, then set current ancestor to its 8496 // parent." 8497 while (isEditable(currentAncestor) && currentAncestor.nodeType == Node.ELEMENT_NODE && !areLooselyEquivalentValues(command, getEffectiveCommandValue(currentAncestor, command), newValue)) { 8498 ancestorList.push(currentAncestor); 8499 currentAncestor = currentAncestor.parentNode; 8500 } 8501 8502 // "If ancestor list is empty, abort this algorithm." 8503 if (!ancestorList.length) { 8504 return; 8505 } 8506 8507 // "Let propagated value be the specified command value of command on the 8508 // last member of ancestor list." 8509 var propagatedValue = getSpecifiedCommandValue(ancestorList[ancestorList.length - 1], command); 8510 8511 // "If propagated value is null and is not equal to new value, abort this 8512 // algorithm." 8513 if (propagatedValue === null && propagatedValue != newValue) { 8514 return; 8515 } 8516 8517 // "If the effective command value for the parent of the last member of 8518 // ancestor list is not loosely equivalent to new value, and new value is 8519 // not null, abort this algorithm." 8520 if (newValue !== null && !areLooselyEquivalentValues(command, getEffectiveCommandValue(ancestorList[ancestorList.length - 1].parentNode, command), newValue)) { 8521 return; 8522 } 8523 8524 // "While ancestor list is not empty:" 8525 while (ancestorList.length) { 8526 // "Let current ancestor be the last member of ancestor list." 8527 // "Remove the last member from ancestor list." 8528 currentAncestor = ancestorList.pop(); 8529 8530 // "If the specified command value of current ancestor for command is 8531 // not null, set propagated value to that value." 8532 if (getSpecifiedCommandValue(currentAncestor, command) !== null) { 8533 propagatedValue = getSpecifiedCommandValue(currentAncestor, command); 8534 } 8535 8536 // "Let children be the children of current ancestor." 8537 var children = Array.prototype.slice.call(toArray(currentAncestor.childNodes)); 8538 8539 // "If the specified command value of current ancestor for command is 8540 // not null, clear the value of current ancestor." 8541 if (getSpecifiedCommandValue(currentAncestor, command) !== null) { 8542 clearValue(currentAncestor, command, range); 8543 } 8544 8545 // "For every child in children:" 8546 var i; 8547 for (i = 0; i < children.length; i++) { 8548 var child = children[i]; 8549 8550 // "If child is node, continue with the next child." 8551 if (child == node) { 8552 continue; 8553 } 8554 8555 // "If child is an Element whose specified command value for 8556 // command is neither null nor equivalent to propagated value, 8557 // continue with the next child." 8558 if (child.nodeType == Node.ELEMENT_NODE && getSpecifiedCommandValue(child, c
8558ommand) !== null && !areEquivalentValues(command, propagatedValue, getSpecifiedCommandValue(child, command))) { 8559 continue; 8560 } 8561 8562 // "If child is the last member of ancestor list, continue with the 8563 // next child." 8564 if (child == ancestorList[ancestorList.length - 1]) { 8565 continue; 8566 } 8567 8568 // "Force the value of child, with command as in this algorithm 8569 // and new value equal to propagated value." 8570 forceValue(child, command, propagatedValue, range); 8571 } 8572 } 8573 } 8574 8575 function restoreValues(values, range) { 8576 // "For each (node, command, value) triple in values:" 8577 $_(values).forEach(function (triple) { 8578 var node = triple[0]; 8579 var command = triple[1]; 8580 var value = triple[2]; 8581 8582 // "Let ancestor equal node." 8583 var ancestor = node; 8584 8585 // "If ancestor is not an Element, set it to its parent." 8586 if (!ancestor || ancestor.nodeType != Node.ELEMENT_NODE) { 8587 ancestor = ancestor.parentNode; 8588 } 8589 8590 // "While ancestor is an Element and its specified command value for 8591 // command is null, set it to its parent." 8592 while (ancestor && ancestor.nodeType == Node.ELEMENT_NODE && getSpecifiedCommandValue(ancestor, command) === null) { 8593 ancestor = ancestor.parentNode; 8594 } 8595 8596 // "If value is null and ancestor is an Element, push down values on 8597 // node for command, with new value null." 8598 if (value === null && ancestor && ancestor.nodeType == Node.ELEMENT_NODE) { 8599 pushDownValues(node, command, null, range); 8600 8601 // "Otherwise, if ancestor is an Element and its specified command 8602 // value for command is not equivalent to value, or if ancestor is not 8603 // an Element and value is not null, force the value of command to 8604 // value on node." 8605 } else if ((ancestor && ancestor.nodeType == Node.ELEMENT_NODE && !areEquivalentValues(command, getSpecifiedCommandValue(ancestor, command), value)) || ((!ancestor || ancestor.nodeType != Node.ELEMENT_NODE) && value !== null)) { 8606 forceValue(node, command, value, range); 8607 } 8608 }); 8609 } 8610 8611 //@} 8612 ///// Setting the selection's value ///// 8613 //@{ 8614 8615 function setSelectionValue(command, newValue, range) { 8616 8617 // Use current selected range if no range passed 8618 range = range || getActiveRange(); 8619 8620 // "If there is no editable text node effectively contained in the active 8621 // range:" 8622 8623 if (! $_(getAllEffectivelyContainedNodes(range)).filter(function (node) { 8624 8625 return node.nodeType == Node.TEXT_NODE; 8626 8627 }, true).some(isEditable) 8628 ) { 8629 // "If command has inline command activated values, set the state 8630 // override to true if new value is among them and false if it's not." 8631 if (commands[command].hasOwnProperty("inlineCommandActivatedValues")) { 8632 setStateOverride( 8633 command, 8634 $_(commands[command].inlineCommandActivatedValues).indexOf(newValue) != -1, 8635 range 8636 ); 8637 } 8638 8639 // "If command is "subscript", unset the state override for 8640 // "superscript"." 8641 if (command == "subscript") { 8642 unsetStateOverride("superscript", range); 8643 } 8644 8645 // "If command is "superscript", unset the state override for 8646 // "subscript"." 8647 if (command == "superscript") { 8648 unsetStateOverride("subscript", range); 8649 } 8650 8651 // "If new value is null, unset the value override (if any)." 8652 if (newValue === null) { 8653 unsetValueOverride(command, range); 8654 8655 // "Otherwise, if command has a value specified, set the value override 8656 // to new value." 8657 } else if (commands[command].hasOwnProperty("value")) { 8658 setValueOverride(command, newValue, range); 8659 } 8660 8661 // "Abort these steps." 8662 return; 8663 } 8664 8665 // "If the active range's start node is an editable Text node, and its 8666 // start offset is neither zero nor its start node's length, call 8667 // splitText() on the active range's start node, with argument equal to the 8668 // active range's start offset. Then set the active range's start node to 8669 // the result, and its start offset to zero." 8670 if (isEditable(range.startContainer) && range.startContainer.nodeType == Node.TEXT_NODE && range.startOffset != 0 && range.startOffset != getNodeLength(range.startContainer)) { 8671 // Account for browsers not following range mutation rules 8672 var newNode = splitText(range.startContainer, range.startOffset); 8673 var newActiveRange = _core.rangy.createRange(); 8674 if (range.startContainer == range.endContainer) { 8675 var newEndOffset = range.endOffset - range.startOffset; 8676 newActiveRange.setEnd(newNode, newEndOffset); 8677 range.setEnd(newNode, newEndOffset); 8678 }
8679 newActiveRange.setStart(newNode, 0); 8680 _core.rangy.getSelection().removeAllRanges(); 8681 _core.rangy.getSelection().addRange(newActiveRange); 8682 8683 range.setStart(newNode, 0); 8684 } 8685 8686 // "If the active range's end node is an editable Text node, and its end 8687 // offset is neither zero nor its end node's length, call splitText() on 8688 // the active range's end node, with argument equal to the active range's 8689 // end offset." 8690 if ( 8691 isEditable(range.endContainer) && 8692 range.endContainer.nodeType == Node.TEXT_NODE && range.endOffset != 0 && 8693 range.endOffset != getNodeLength(range.endContainer) 8694 ) { 8695 // IE seems to mutate the range incorrectly here, so we need correction 8696 // here as well. The active range will be temporarily in orphaned 8697 // nodes, so calling getActiveRange() after splitText() but before 8698 // fixing the range will throw an exception. 8699 // TODO: check if this is still neccessary 8700 var activeRange = range; 8701 var newStart = [activeRange.startContainer, activeRange.startOffset]; 8702 var newEnd = [activeRange.endContainer, activeRange.endOffset]; 8703 splitText(activeRange.endContainer, activeRange.endOffset); 8704 activeRange.setStart(newStart[0], newStart[1]); 8705 activeRange.setEnd(newEnd[0], newEnd[1]); 8706 8707 _core.rangy.getSelection().removeAllRanges(); 8708 _core.rangy.getSelection().addRange(activeRange); 8709 } 8710 8711 // "Let element list be all editable Elements effectively contained in the 8712 // active range. 8713 // 8714 // "For each element in element list, clear the value of element." 8715 $_(getAllEffectivelyContainedNodes(getActiveRange(), function (node) { 8716 return isEditable(node) && node.nodeType == Node.ELEMENT_NODE; 8717 })).forEach(function (element) { 8718 clearValue(element, command, range); 8719 }); 8720 8721 // "Let node list be all editable nodes effectively contained in the active 8722 // range. 8723 // 8724 // "For each node in node list:" 8725 $_(getAllEffectivelyContainedNodes(range, isEditable)).forEach(function (node) { 8726 // "Push down values on node." 8727 pushDownValues(node, command, newValue, range); 8728 8729 // "Force the value of node." 8730 forceValue(node, command, newValue, range); 8731 }); 8732 } 8733 8734 /** 8735 * attempt to retrieve a block like a table or an Aloha Block 8736 * which is located one step right of the current caret position. 8737 * If an appropriate element is found it will be returned or 8738 * false otherwise 8739 * 8740 * @param {element} node current node we're in 8741 * @param {number} offset current offset within that node 8742 * 8743 * @return the dom node if found or false if no appropriate 8744 * element was found 8745 */ 8746 function getBlockAtNextPosition(node, offset) { 8747 var i; 8748 8749 // if we're inside a text node we first have to check 8750 // if there is nothing but tabs, newlines or the like 8751 // after our current cursor position 8752 if (node.nodeType === Node.TEXT_NODE && offset < node.length) { 8753 for (i = offset; i < node.length; i++) { 8754 if ((node.data.charAt(i) !== '\t' && node.data.charAt(i) !== '\r' && node.data.charAt(i) !== '\n') || node.data.charCodeAt(i) === 160) { // 8755 // this is a character that has to be deleted first 8756 return false; 8757 } 8758 } 8759 } 8760 8761 // try the most simple approach first: the next sibling 8762 // is a table 8763 if (node.nextSibling && node.nextSibling.className && node.nextSibling.className.indexOf("aloha-table-wrapper") >= 0) { 8764 return node.nextSibling; 8765 } 8766 8767 // since we got only ignorable whitespace here determine if 8768 // our nodes parents next sibling is a table 8769 if (node.parentNode && node.parentNode.nextSibling && node.parentNode.nextSibling.className && node.parentNode.nextSibling.className.indexOf("aloha-table-wrapper") >= 0) { 8770 return node.parentNode.nextSibling; 8771 } 8772 8773 // our parents nextsibling is a pure whitespace node such as 8774 // generated by sourcecode indentation so we'll check for 8775 // the next next sibling 8776 if (node.parentNode && node.parentNode.nextSibling && isWhitespaceNode(node.parentNode.nextSibling) && node.parentNode.nextSibling.nextSibling && node.parentNode.nextSibling.nextSibling.className && node.parentNode.nextSibling.nextSibling.className.indexOf("aloha-table-wrapper") >= 0) { 8777 return node.parentNode.nextSibling.nextSibling; 8778 } 8779 8780 // Note: the search above works for tables, since they cannot be 8781 // nested deeply in paragraphs and other formatting tags. If this code 8782 // is extended to work also for other blocks, the search probably needs to be adapted 8783 } 8784
8785 /** 8786 * Attempt to retrieve a block like a table or an Aloha Block 8787 * which is located right before the current position. 8788 * If an appropriate element is found, it will be returned or 8789 * false otherwise 8790 * 8791 * @param {element} node current node 8792 * @param {offset} offset current offset 8793 * 8794 * @return dom node of found or false if no appropriate 8795 * element was found 8796 */ 8797 function getBlockAtPreviousPosition(node, offset) { 8798 var i; 8799 8800 if (node.nodeType === Node.TEXT_NODE && offset > 0) { 8801 for (i = offset - 1; i >= 0; i--) { 8802 if ((node.data.charAt(i) !== '\t' && node.data.charAt(i) !== '\r' && node.data.charAt(i) !== '\n') || node.data.charCodeAt(i) === 160) { // 8803 // this is a character that has to be deleted first 8804 return false; 8805 } 8806 } 8807 } 8808 8809 // try the previous sibling 8810 if (node.previousSibling && node.previousSibling.className && node.previousSibling.className.indexOf("aloha-table-wrapper") >= 0) { 8811 return node.previousSibling; 8812 } 8813 8814 // try the parent's previous sibling 8815 if (node.parentNode && node.parentNode.previousSibling && node.parentNode.previousSibling.className && node.parentNode.previousSibling.className.indexOf("aloha-table-wrapper") >= 0) { 8816 return node.parentNode.previousSibling; 8817 } 8818 8819 // the parent's previous sibling might be a whitespace node 8820 if (node.parentNode && node.parentNode.previousSibling && isWhitespaceNode(node.parentNode.previousSibling) && node.parentNode.previousSibling.previousSibling && node.parentNode.previousSibling.previousSibling.className && node.parentNode.previousSibling.previousSibling.className.indexOf('aloha-table-wrapper') >= 0) { 8821 return node.parentNode.previousSibling.previousSibling; 8822 } 8823 8824 // Note: the search above works for tables, since they cannot be 8825 // nested deeply in paragraphs and other formatting tags. If this code 8826 // is extended to work also for other blocks, the search probably needs to be adapted 8827 8828 return false; 8829 } 8830 8831 // "A boundary point (node, offset) is a block start point if either node's 8832 // parent is null and offset is zero; or node has a child with index offset â 8833 // 1, and that child is either a visible block node or a visible br." 8834 function isBlockStartPoint(node, offset) { 8835 return (!node.parentNode && offset == 0) || (0 <= offset - 1 && offset - 1 < node.childNodes.length && isVisible(node.childNodes[offset - 1]) && (isBlockNode(node.childNodes[offset - 1]) || isNamedHtmlElement(node.childNodes[offset - 1], "br"))); 8836 } 8837 8838 // "A boundary point (node, offset) is a block end point if either node's 8839 // parent is null and offset is node's length; or node has a child with index 8840 // offset, and that child is a visible block node." 8841 function isBlockEndPoint(node, offset) { 8842 return (!node.parentNode && offset == getNodeLength(node)) || (offset < node.childNodes.length && isVisible(node.childNodes[offset]) && isBlockNode(node.childNodes[offset])); 8843 } 8844 8845 // "A boundary point is a block boundary point if it is either a block start 8846 // point or a block end point." 8847 function isBlockBoundaryPoint(node, offset) { 8848 return isBlockStartPoint(node, offset) || isBlockEndPoint(node, offset); 8849 } 8850 8851 function followsLineBreak(node) { 8852 // "Let offset be zero." 8853 var offset = 0; 8854 8855 // "While (node, offset) is not a block boundary point:" 8856 while (!isBlockBoundaryPoint(node, offset)) { 8857 // "If node has a visible child with index offset minus one, return 8858 // false." 8859 if (0 <= offset - 1 && offset - 1 < node.childNodes.length && isVisible(node.childNodes[offset - 1])) { 8860 return false; 8861 } 8862 8863 // "If offset is zero or node has no children, set offset to node's 8864 // index, then set node to its parent." 8865 if (offset == 0 || !node.hasChildNodes()) { 8866 offset = getIndexInParent(node); 8867 node = node.parentNode; 8868 8869 // "Otherwise, set node to its child with index offset minus one, then 8870 // set offset to node's length." 8871 } else { 8872 node = node.childNodes[offset - 1]; 8873 offset = getNodeLength(node); 8874 } 8875 } 8876 8877 // "Return true." 8878 return true; 8879 } 8880 8881 function precedesLineBreak(node) { 8882 // "Let offset be node's length." 8883 var offset = getNodeLength(node); 8884 8885 // "While (node, offset) is not a block boundary point:" 8886 while (!isBlockBoundaryPoint(node, offset)) { 8887 // "If node has a visible child with index offset, return false." 8888 if (offset < node.childNodes.length && isVisible(node.childNodes[offset])) { 8889 return false;
8890 } 8891 8892 // "If offset is node's length or node has no children, set offset to 8893 // one plus node's index, then set node to its parent." 8894 if (offset == getNodeLength(node) || !node.hasChildNodes()) { 8895 offset = 1 + getIndexInParent(node); 8896 node = node.parentNode; 8897 8898 // "Otherwise, set node to its child with index offset and set offset 8899 // to zero." 8900 } else { 8901 node = node.childNodes[offset]; 8902 offset = 0; 8903 } 8904 } 8905 8906 // "Return true." 8907 return true; 8908 } 8909 8910 //@} 8911 ///// Splitting a node list's parent ///// 8912 //@{ 8913 8914 function splitParent(nodeList, range) { 8915 var i; 8916 8917 // "Let original parent be the parent of the first member of node list." 8918 var originalParent = nodeList[0].parentNode; 8919 8920 // "If original parent is not editable or its parent is null, do nothing 8921 // and abort these steps." 8922 if (!isEditable(originalParent) || !originalParent.parentNode) { 8923 return; 8924 } 8925 8926 // "If the first child of original parent is in node list, remove 8927 // extraneous line breaks before original parent." 8928 if (jQuery.inArray(originalParent.firstChild, nodeList) != -1) { 8929 removeExtraneousLineBreaksBefore(originalParent); 8930 } 8931 8932 var firstChildInNodeList = jQuery.inArray(originalParent.firstChild, nodeList) != -1; 8933 var lastChildInNodeList = jQuery.inArray(originalParent.lastChild, nodeList) != -1; 8934 8935 // "If the first child of original parent is in node list, and original 8936 // parent follows a line break, set follows line break to true. Otherwise, 8937 // set follows line break to false." 8938 var followsLineBreak_ = firstChildInNodeList && followsLineBreak(originalParent); 8939 8940 // "If the last child of original parent is in node list, and original 8941 // parent precedes a line break, set precedes line break to true. 8942 // Otherwise, set precedes line break to false." 8943 var precedesLineBreak_ = lastChildInNodeList && precedesLineBreak(originalParent); 8944 8945 // "If the first child of original parent is not in node list, but its last 8946 // child is:" 8947 if (!firstChildInNodeList && lastChildInNodeList) { 8948 // "For each node in node list, in reverse order, insert node into the 8949 // parent of original parent immediately after original parent, 8950 // preserving ranges." 8951 for (i = nodeList.length - 1; i >= 0; i--) { 8952 movePreservingRanges(nodeList[i], originalParent.parentNode, 1 + getIndexInParent(originalParent), range); 8953 } 8954 8955 // "If precedes line break is true, and the last member of node list 8956 // does not precede a line break, call createElement("br") on the 8957 // context object and insert the result immediately after the last 8958 // member of node list." 8959 if (precedesLineBreak_ && !precedesLineBreak(nodeList[nodeList.length - 1])) { 8960 nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling); 8961 } 8962 8963 // "Remove extraneous line breaks at the end of original parent." 8964 removeExtraneousLineBreaksAtTheEndOf(originalParent); 8965 8966 // "Abort these steps." 8967 return; 8968 } 8969 8970 // "If the first child of original parent is not in node list:" 8971 if (!firstChildInNodeList) { 8972 // "Let cloned parent be the result of calling cloneNode(false) on 8973 // original parent." 8974 var clonedParent = originalParent.cloneNode(false); 8975 8976 // "If original parent has an id attribute, unset it." 8977 originalParent.removeAttribute("id"); 8978 8979 // "Insert cloned parent into the parent of original parent immediately 8980 // before original parent." 8981 originalParent.parentNode.insertBefore(clonedParent, originalParent); 8982 8983 // "While the previousSibling of the first member of node list is not 8984 // null, append the first child of original parent as the last child of 8985 // cloned parent, preserving ranges." 8986 while (nodeList[0].previousSibling) { 8987 movePreservingRanges(originalParent.firstChild, clonedParent, clonedParent.childNodes.length, range); 8988 } 8989 } 8990 8991 // "For each node in node list, insert node into the parent of original 8992 // parent immediately before original parent, preserving ranges." 8993 for (i = 0; i < nodeList.length; i++) { 8994 movePreservingRanges(nodeList[i], originalParent.parentNode, getIndexInParent(originalParent), range); 8995 } 8996 8997 // "If follows line break is true, and the first member of node list does 8998 // not follow a line break, call createElement("br") on the context object 8999 // and insert the result immediately before the first member of node list." 9000 if (followsLineBreak_ && !followsLineBreak(nodeList[0])) { 9001 nodeList[0].parentNode.insertBefore(document.createElement("br"), nodeList[0]); 9002 } 9003 9004 // "If the last member of node list is an inline node other than a br, and 9005 // the first child of original parent is a br, and original parent is not 9006 // an inline node, remove the first child of original parent from original 9007 // parent." 9008 if (isInlineNode(nodeList[nodeList.length - 1]) && !isNamedHtmlElement(nodeList[nodeList.length - 1], "br") && isNamedHtmlElement(originalParent.firstChild, "br") && !isInlineNode(originalParent)) { 9009 originalParent.removeChild(originalParent.firstChild); 9010 } 9011 9012 // "If original parent has no children:" 9013 if (!originalParent.hasChildNodes()) { 9014 // if the current range is collapsed and at the end of the originalParent.parentNode 9015 // the offset will not be available anymore after the next step (remove child) 9016 // that's why we need to fix the range to prevent a bogus offset 9017 if (originalParent.parentNode === range.startContainer && originalParent.parentNode === range.endContainer && range.startContainer === range.endContainer && range.startOffset === range.endOffset && originalParent.parentNode.childNodes.length === range.startOffset) { 9018 range.startOffset = originalParent.parentNode.childNodes.length - 1; 9019 range.endOffset = range.startOffset; 9020 } 9021 9022 // "Remove original parent from its parent." 9023 originalParent.parentNode.removeChild(originalParent); 9024 9025 // "If precedes line break is true, and the last member of node list 9026 // does not precede a line break, call createElement("br") on the 9027 // context object and insert the result immediately after the last 9028 // member of node list." 9029 if (precedesLineBreak_ && !precedesLineBreak(nodeList[nodeList.length - 1])) { 9030 nodeList[nodeList.length - 1].parentNode.insertBefore(document.createElement("br"), nodeList[nodeList.length - 1].nextSibling); 9031 } 9032 9033 // "Otherwise, remove extraneous line breaks before original parent." 9034 } else { 9035 removeExtraneousLineBreaksBefore(originalParent); 9036 } 9037 9038 // "If node list's last member's nextSibling is null, but its parent is not 9039 // null, remove extraneous line breaks at the end of node list's last 9040 // member's parent." 9041 if (!nodeList[nodeList.length - 1].nextSibling && nodeList[nodeList.length - 1].parentNode) { 9042 removeExtraneousLineBreaksAtTheEndOf(nodeList[nodeList.length - 1].parentNode); 9043 } 9044 } 9045 9046 //@} 9047 ///// The backColor command ///// 9048 //@{ 9049 commands.backcolor = { 9050 // Copy-pasted, same as hiliteColor 9051 action: function (value, range) { 9052 // Action is further copy-pasted, same as foreColor 9053 9054 // "If value is not a valid CSS color, prepend "#" to it." 9055 // 9056 // "If value is still not a valid CSS color, or if it is currentColor, 9057 // abort these steps and do nothing." 9058 // 9059 // Cheap hack for testing, no attempt to be comprehensive. 9060 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) { 9061 value = "#" + value; 9062 } 9063 if (!/^(rgba?|hsla?)\(.*\)$/.test(value) && !parseSimpleColor(value) && value.toLowerCase() != "transparent") { 9064 return; 9065 } 9066 9067 // "Set the selection's value to value." 9068 setSelectionValue("backcolor", value, range); 9069 }, 9070 standardInlineValueCommand: true, 9071 relevantCssProperty: "backgroundColor", 9072 equivalentValues: function (val1, val2) { 9073 // "Either both strings are valid CSS colors and have the same red, 9074 // green, blue, and alpha components, or neither string is a valid CSS 9075 // color." 9076 return normalizeColor(val1) === normalizeColor(val2); 9077 } 9078 }; 9079 9080 //@} 9081 ///// The bold command ///// 9082 //@{ 9083 commands.bold = { 9084 action: function (value, range) { 9085 // "If queryCommandState("bold") returns true, set the selection's 9086 // value to "normal". Otherwise set the selection's value to "bold"." 9087 if (myQueryCommandState("bold", range)) { 9088 9089 //setSelectionValue("bold", "normal", range); 9090 9091 setSelectionValue("bold", null, range); 9092 9093 } else { 9094 9095 setSelectionValue("bold", "bold", range); 9096 9097 } 9098 }, 9099 inlineCommandActivatedValues: ["bold", "600", "700", "800", "900"], 9100 relevantCssProperty: "fontWeight", 9101 equivalentValues: function (val1, val2) { 9102 // "Either the two strings are equal, or one is "bold" and the other is 9103 // "700", or one is "normal" and the other is "400"." 9104 return val1 == val2 || (val1 == "bold" && val2 == "700") || (val1 == "700" && val2 == "bold") || (val1 == "normal" && val2 == "400") || (val1 == "400" && val2 == "normal"); 9105 } 9106 }; 9107 9108 //@} 9109 ///// The createLink command ///// 9110 //@{ 9111 commands.createlink = { 9112 action: function (value, range) { 9113 // "If value is the empty string, abort these steps and do nothing." 9114 if (value === "") { 9115 return; 9116 } 9117 9118 // "For each editable a element that has an href attribute and is an 9119 // ancestor of some node effectively contained in the active range, set 9120 // that a element's href attribute to value." 9121 // 9122 // TODO: We don't actually do this in tree order, not that it matters 9123 // unless you're spying with mutation events.
9124 $_(getAllEffectivelyContainedNodes(getActiveRange())).forEach(function (node) { 9125 $_(getAncestors(node)).forEach(function (ancestor) { 9126 if (isEditable(ancestor) && isNamedHtmlElement(ancestor, 'a') && hasAttribute(ancestor, "href")) { 9127 ancestor.setAttribute("href", value); 9128 } 9129 }); 9130 }); 9131 9132 // "Set the selection's value to value." 9133 setSelectionValue("createlink", value, range); 9134 }, 9135 standardInlineValueCommand: true 9136 }; 9137 9138 //@} 9139 ///// The fontName command ///// 9140 //@{ 9141 commands.fontname = { 9142 action: function (value, range) { 9143 // "Set the selection's value to value." 9144 setSelectionValue("fontname", value, range); 9145 }, 9146 standardInlineValueCommand: true, 9147 relevantCssProperty: "fontFamily" 9148 }; 9149 9150 //@} 9151 ///// The fontSize command ///// 9152 //@{ 9153 9154 commands.fontsize = { 9155 action: function (value, range) { 9156 // "If value is the empty string, abort these steps and do nothing." 9157 if (value === "") { 9158 return; 9159 } 9160 9161 value = normalizeFontSize(value); 9162 9163 // "If value is not one of the strings "xx-small", "x-small", "small", 9164 // "medium", "large", "x-large", "xx-large", "xxx-large", and is not a 9165 // valid CSS absolute length, then abort these steps and do nothing." 9166 // 9167 // More cheap hacks to skip valid CSS absolute length checks. 9168 if (jQuery.inArray(value, ["xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"]) == -1 && !/^[0-9]+(\.[0-9]+)?(cm|mm|in|pt|pc)$/.test(value)) { 9169 return; 9170 } 9171 9172 // "Set the selection's value to value." 9173 setSelectionValue("fontsize", value, range); 9174 }, 9175 indeterm: function () { 9176 // "True if among editable Text nodes that are effectively contained in 9177 // the active range, there are two that have distinct effective command 9178 // values. Otherwise false." 9179 return $_(getAllEffectivelyContainedNodes(getActiveRange(), function (node) { 9180 return isEditable(node) && node.nodeType == Node.TEXT_NODE; 9181 })).map(function (node) { 9182 return getEffectiveCommandValue(node, "fontsize"); 9183 }, true).filter(function (value, i, arr) { 9184 return $_(arr.slice(0, i)).indexOf(value) == -1; 9185 }).length >= 2; 9186 }, 9187 value: function (range) { 9188 // "Let pixel size be the effective command value of the first editable 9189 // Text node that is effectively contained in the active range, or if 9190 // there is no such node, the effective command value of the active 9191 // range's start node, in either case interpreted as a number of 9192 // pixels." 9193 var node = getAllEffectivelyContainedNodes(range, function (node) { 9194 return isEditable(node) && node.nodeType == Node.TEXT_NODE; 9195 })[0]; 9196 if (node === undefined) { 9197 node = range.startContainer; 9198 } 9199 var pixelSize = getEffectiveCommandValue(node, "fontsize"); 9200 9201 // "Return the legacy font size for pixel size." 9202 return getLegacyFontSize(pixelSize); 9203 }, 9204 relevantCssProperty: "fontSize" 9205 }; 9206 9207 //@} 9208 ///// The foreColor command ///// 9209 //@{ 9210 commands.forecolor = { 9211 action: function (value, range) { 9212 // Copy-pasted, same as backColor and hiliteColor 9213 9214 // "If value is not a valid CSS color, prepend "#" to it." 9215 // 9216 // "If value is still not a valid CSS color, or if it is currentColor, 9217 // abort these steps and do nothing." 9218 // 9219 // Cheap hack for testing, no attempt to be comprehensive. 9220 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) { 9221 value = "#" + value; 9222 } 9223 if (!/^(rgba?|hsla?)\(.*\)$/.test(value) && !parseSimpleColor(value) && value.toLowerCase() != "transparent") { 9224 return; 9225 } 9226 9227 // "Set the selection's value to value." 9228 setSelectionValue("forecolor", value, range); 9229 }, 9230 standardInlineValueCommand: true, 9231 relevantCssProperty: "color", 9232 equivalentValues: function (val1, val2) { 9233 // "Either both strings are valid CSS colors and have the same red, 9234 // green, blue, and alpha components, or neither string is a valid CSS 9235 // color." 9236 return normalizeColor(val1) === normalizeColor(val2); 9237 } 9238 }; 9239 9240 //@} 9241 ///// The hiliteColor command ///// 9242 //@{ 9243 commands.hilitecolor = { 9244 // Copy-pasted, same as backColor 9245 action: function (value, range) { 9246 // Action is further copy-pasted, same as foreColor 9247 9248 // "If value is not a valid CSS color, prepend "#" to it." 9249 // 9250 // "If value is still not a valid CSS color, or if it is currentColor, 9251 // abort these steps and do nothing." 9252 // 9253 // Cheap hack for testing, no attempt to be comprehensive. 9254 if (/^([0-9a-fA-F]{3}){1,2}$/.test(value)) { 9255 value = "#" + value; 9256 } 9257 if (!/^(rgba?|hsla?)\(.*\)$/.test(value) && !parseSimpleColor(value) && value.toLowerCase() != "transparent") { 9258 return; 9259 } 9260 9261 // "Set the selection's value to value." 9262 setSelectionValue("hilitecolor", value, range); 9263 }, 9264 indeterm: function () { 9265 // "True if among editable Text nodes that are effectively contained in 9266 // the active range, there are two that have distinct effective command 9267 // values. Otherwise false." 9268 return $_(getAllEffectivelyContainedNodes(getActiveRange(), function (node) { 9269 return isEditable(node) && node.nodeType == Node.TEXT_NODE; 9270 })).map(function (node) { 9271 return getEffectiveCommandValue(node, "hilitecolor"); 9272 }, true).filter(function (value, i, arr) {
9273 return $_(arr.slice(0, i)).indexOf(value) == -1; 9274 }).length >= 2; 9275 }, 9276 standardInlineValueCommand: true, 9277 relevantCssProperty: "backgroundColor", 9278 equivalentValues: function (val1, val2) { 9279 // "Either both strings are valid CSS colors and have the same red, 9280 // green, blue, and alpha components, or neither string is a valid CSS 9281 // color." 9282 return normalizeColor(val1) === normalizeColor(val2); 9283 } 9284 }; 9285 9286 //@} 9287 ///// The italic command ///// 9288 //@{ 9289 commands.italic = { 9290 action: function (value, range) { 9291 // "If queryCommandState("italic") returns true, set the selection's 9292 // value to "normal". Otherwise set the selection's value to "italic"." 9293 if (myQueryCommandState("italic", range)) { 9294 setSelectionValue("italic", null, range); 9295 } else { 9296 setSelectionValue("italic", "italic", range); 9297 } 9298 }, 9299 inlineCommandActivatedValues: ["italic", "oblique"], 9300 relevantCssProperty: "fontStyle" 9301 }; 9302 9303 //@} 9304 ///// The removeFormat command ///// 9305 //@{ 9306 commands.removeformat = { 9307 action: function (value, range) { 9308 var newEnd, newStart, newNode; 9309 9310 // "A removeFormat candidate is an editable HTML element with local 9311 // name "abbr", "acronym", "b", "bdi", "bdo", "big", "blink", "cite", 9312 // "code", "dfn", "em", "font", "i", "ins", "kbd", "mark", "nobr", "q", 9313 // "s", "samp", "small", "span", "strike", "strong", "sub", "sup", 9314 // "tt", "u", or "var"." 9315 function isRemoveFormatCandidate(node) { 9316 return isEditable(node) && isHtmlElementInArray(node, ["abbr", "acronym", "b", "bdi", "bdo", "big", "blink", "cite", "code", "dfn", "em", "font", "i", "ins", "kbd", "mark", "nobr", "q", "s", "samp", "small", "span", "strike", "strong", "sub", "sup", "tt", "u", "var"]); 9317 } 9318 9319 // "Let elements to remove be a list of every removeFormat candidate 9320 // effectively contained in the active range." 9321 var elementsToRemove = getAllEffectivelyContainedNodes(getActiveRange(), isRemoveFormatCandidate); 9322 9323 // "For each element in elements to remove:" 9324 $_(elementsToRemove).forEach(function (element) { 9325 // "While element has children, insert the first child of element 9326 // into the parent of element immediately before element, 9327 // preserving ranges." 9328 while (element.hasChildNodes()) { 9329 movePreservingRanges(element.firstChild, element.parentNode, getIndexInParent(element), getActiveRange()); 9330 } 9331 9332 // "Remove element from its parent." 9333 element.parentNode.removeChild(element); 9334 }); 9335 9336 // "If the active range's start node is an editable Text node, and its 9337 // start offset is neither zero nor its start node's length, call 9338 // splitText() on the active range's start node, with argument equal to 9339 // the active range's start offset. Then set the active range's start 9340 // node to the result, and its start offset to zero." 9341 if (isEditable(getActiveRange().startContainer) && getActiveRange().startContainer.nodeType == Node.TEXT_NODE && getActiveRange().startOffset != 0 && getActiveRange().startOffset != getNodeLength(getActiveRange().startContainer)) { 9342 // Account for browsers not following range mutation rules 9343 if (getActiveRange().startContainer == getActiveRange().endContainer) { 9344 newEnd = getActiveRange().endOffset - getActiveRange().startOffset; 9345 newNode = splitText(getActiveRange().startContainer, getActiveRange().startOffset); 9346 getActiveRange().setStart(newNode, 0); 9347 getActiveRange().setEnd(newNode, newEnd); 9348 } else { 9349 getActiveRange().setStart(splitText(getActiveRange().startContainer, getActiveRange().startOffset), 0); 9350 } 9351 } 9352 9353 // "If the active range's end node is an editable Text node, and its 9354 // end offset is neither zero nor its end node's length, call 9355 // splitText() on the active range's end node, with argument equal to 9356 // the active range's end offset." 9357 if (isEditable(getActiveRange().endContainer) && getActiveRange().endContainer.nodeType == Node.TEXT_NODE && getActiveRange().endOffset != 0 && getActiveRange().endOffset != getNodeLength(getActiveRange().endContainer)) { 9358 // IE seems to mutate the range incorrectly here, so we need 9359 // correction here as well. Have to be careful to set the range to 9360 // something not including the text node so that getActiveRange() 9361 // doesn't throw an exception due to a temporarily detached 9362 // endpoint. 9363 newStart = [getActiveRange().startContainer, getActiveRange().startOffset]; 9364 newEnd = [getActiveRange().endContainer, getActiveRange().endOffset]; 9365 getActiveRange().setEnd(document.documentElement, 0); 9366 splitText(newEnd[0], newEnd[1]); 9367 getActiveRange().setStart(newStart[0], newStart[1]); 9368 getActiveRange().setEnd(newEnd[0], newEnd[1]); 9369 } 9370 9371 // "Let node list consist of all editable nodes effectively contained 9372 // in the active range." 9373 // 9374 // "For each node in node list, while node's parent is a removeFormat 9375 // candidate in the same editing host as node, split the parent of the 9376 // one-node list consisting of node." 9377 $_(getAllEffectivelyContainedNodes(getActiveRange(), isEditable)).forEac
9377h(function (node) { 9378 while (isRemoveFormatCandidate(node.parentNode) && inSameEditingHost(node.parentNode, node)) { 9379 splitParent([node], getActiveRange()); 9380 } 9381 }); 9382 9383 // "For each of the entries in the following list, in the given order, 9384 // set the selection's value to null, with command as given." 9385 $_(["subscript", "bold", "fontname", "fontsize", "forecolor", "hilitecolor", "italic", "strikethrough", "underline"]).forEach(function (command) { 9386 setSelectionValue(command, null, range); 9387 }); 9388 } 9389 }; 9390 9391 //@} 9392 ///// The strikethrough command ///// 9393 //@{ 9394 commands.strikethrough = { 9395 action: function (value, range) { 9396 // "If queryCommandState("strikethrough") returns true, set the 9397 // selection's value to null. Otherwise set the selection's value to 9398 // "line-through"." 9399 if (myQueryCommandState("strikethrough", range)) { 9400 setSelectionValue("strikethrough", null, range); 9401 } else { 9402 setSelectionValue("strikethrough", "line-through", range); 9403 } 9404 }, 9405 inlineCommandActivatedValues: ["line-through"] 9406 }; 9407 9408 //@} 9409 ///// The subscript command ///// 9410 //@{ 9411 commands.subscript = { 9412 action: function (value, range) { 9413 // "Call queryCommandState("subscript"), and let state be the result." 9414 var state = myQueryCommandState("subscript", range); 9415 9416 // "Set the selection's value to null." 9417 setSelectionValue("subscript", null, range); 9418 9419 // "If state is false, set the selection's value to "subscript"." 9420 if (!state) { 9421 setSelectionValue("subscript", "subscript", range); 9422 } 9423 }, 9424 indeterm: function () { 9425 // "True if either among editable Text nodes that are effectively 9426 // contained in the active range, there is at least one with effective 9427 // command value "subscript" and at least one with some other effective 9428 // command value; or if there is some editable Text node effectively 9429 // contained in the active range with effective command value "mixed". 9430 // Otherwise false." 9431 var nodes = getAllEffectivelyContainedNodes(getActiveRange(), function (node) { 9432 return isEditable(node) && node.nodeType == Node.TEXT_NODE; 9433 }); 9434 return (($_(nodes).some(function (node) { return getEffectiveCommandValue(node, "subscript") == "subscript"; }) 9435 && $_(nodes).some(function (node) { return getEffectiveCommandValue(node, "subscript") != "subscript"; })) 9436 || $_(nodes).some(function (node) { return getEffectiveCommandValue(node, "subscript") == "mixed"; })); 9437 }, 9438 inlineCommandActivatedValues: ["subscript"] 9439 }; 9440 9441 //@} 9442 ///// The superscript command ///// 9443 //@{ 9444 commands.superscript = { 9445 action: function (value, range) { 9446 // "Call queryCommandState("superscript"), and let state be the 9447 // result." 9448 var state = myQueryCommandState("superscript", range); 9449 9450 // "Set the selection's value to null." 9451 setSelectionValue("superscript", null, range); 9452 9453 // "If state is false, set the selection's value to "superscript"." 9454 if (!state) { 9455 setSelectionValue("superscript", "superscript", range); 9456 } 9457 }, 9458 indeterm: function () { 9459 // "True if either among editable Text nodes that are effectively 9460 // contained in the active range, there is at least one with effective 9461 // command value "superscript" and at least one with some other 9462 // effective command value; or if there is some editable Text node 9463 // effectively contained in the active range with effective command 9464 // value "mixed". Otherwise false." 9465 var nodes = getAllEffectivelyContainedNodes( 9466 getActiveRange(), 9467 function (node) { 9468 return isEditable(node) && node.nodeType == Node.TEXT_NODE; 9469 } 9470 ); 9471 return (($_(nodes).some(function (node) { return getEffectiveCommandValue(node, "superscript") == "superscript"; }) 9472 && $_(nodes).some(function (node) { return getEffectiveCommandValue(node, "superscript") != "superscript"; })) 9473 || $_(nodes).some(function (node) { return getEffectiveCommandValue(node, "superscript") == "mixed"; })); 9474 }, 9475 inlineCommandActivatedValues: ["superscript"] 9476 }; 9477 9478 //@} 9479 ///// The underline command ///// 9480 //@{ 9481 commands.underline = { 9482 action: function (value, range) { 9483 // "If queryCommandState("underline") returns true, set the selection's 9484 // value to null. Otherwise set the selection's value to "underline"." 9485 if (myQueryCommandState("underline", range)) { 9486 setSelectionValue("underline", null, range); 9487 } else { 9488 setSelectionValue("underline", "underline", range); 9489 } 9490 }, 9491 inlineCommandActivatedValues: ["underline"] 9492 }; 9493 9494 //@} 9495 ///// The unlink command ///// 9496 //@{ 9497 commands.unlink = { 9498 action: function () { 9499 // "Let hyperlinks be a list of every a element that has an href 9500 // attribute and is contained in the active range or is an ancestor of 9501 // one of its boundary points." 9502 // 9503 // As usual, take care to ensure it's tree order. The correctness of 9504 // the following is left as an exercise for the reader. 9505 var range = getActiveRange(); 9506 var hyperlinks = []; 9507 var node; 9508 for (node = range.startContainer; node; node = node.parentNode) { 9509 if (isNamedHtmlElement(node, 'A') && hasAttribute(node, "href")) { 9510 hyperlinks.unshift(node); 9511 } 9512 } 9513 for (node = range.startContainer; node != nextNodeDescendants(range.endContainer); node = nextNode(node)) { 9514 if (isNamedHtmlElement(node, 'A') && hasAttribute(node, "href") && (isContained(node, range) || isAncestor(node, range.endContainer) || node == range.endContainer)) { 9515 hyperlinks.push(node); 9516 } 9517 } 9518 9519 // "Clear the value of each member of hyperlinks." 9520 var i; 9521 for (i = 0;
9521 i < hyperlinks.length; i++) { 9522 clearValue(hyperlinks[i], "unlink", range); 9523 } 9524 }, 9525 standardInlineValueCommand: true 9526 }; 9527 9528 //@} 9529 9530 ///////////////////////////////////// 9531 ///// Block formatting commands ///// 9532 ///////////////////////////////////// 9533 9534 ///// Block formatting command definitions ///// 9535 //@{ 9536 9537 // "An indentation element is either a blockquote, or a div that has a style 9538 // attribute that sets "margin" or some subproperty of it." 9539 function isIndentationElement(node) { 9540 // Handling of indentation elements while deleting is somehow broken (pressing backspace 9541 // in blockquotes wraps the blockquote into a div, ...) 9542 // therefore for now, we pretend that indentation elements do not exist at all. 9543 return false; 9544 } 9545 9546 // "A simple indentation element is an indentation element that has no 9547 // attributes other than one or more of 9548 // 9549 // * "a style attribute that sets no properties other than "margin", "border", 9550 // "padding", or subproperties of those; 9551 // * "a class attribute; 9552 // * "a dir attribute." 9553 function isSimpleIndentationElement(node) { 9554 if (!isIndentationElement(node)) { 9555 return false; 9556 } 9557 9558 if (node.tagName != "BLOCKQUOTE" && node.tagName != "DIV") { 9559 return false; 9560 } 9561 9562 var i; 9563 for (i = 0; i < node.attributes.length; i++) { 9564 if (!isHtmlNamespace(node.attributes[i].namespaceURI) || jQuery.inArray(node.attributes[i].name, ["style", "class", "dir"]) == -1) { 9565 return false; 9566 } 9567 } 9568 9569 if (typeof node.style.length !== 'undefined') { 9570 for (i = 0; i < node.style.length; i++) { 9571 // This is approximate, but it works well enough for my purposes. 9572 if (!/^(-[a-z]+-)?(margin|border|padding)/.test(node.style[i])) { 9573 return false; 9574 } 9575 } 9576 } else { 9577 var s; 9578 /*jslint forin: true*/ //not sure whether node.style.hasOwnProperty is valid 9579 for (s in node.style) { 9580 // This is approximate, but it works well enough for my purposes. 9581 if (!/^(-[a-z]+-)?(margin|border|padding)/.test(s) && node.style[s] && node.style[s] !== 0 && node.style[s] !== 'false') { 9582 return false; 9583 } 9584 } 9585 /*jslint forin: false*/ 9586 } 9587 9588 return true; 9589 } 9590 9591 // "A non-list single-line container is an HTML element with local name 9592 // "address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "listing", "p", "pre", 9593 // or "xmp"." 9594 function isNonListSingleLineContainer(node) { 9595 return isHtmlElementInArray(node, ["address", "div", "h1", "h2", "h3", "h4", "h5", "h6", "listing", "p", "pre", "xmp"]); 9596 } 9597 9598 // "A single-line container is either a non-list single-line container, or an 9599 // HTML element with local name "li", "dt", or "dd"." 9600 function isSingleLineContainer(node) { 9601 return isNonListSingleLineContainer(node) || isHtmlElementInArray(node, ["li", "dt", "dd"]); 9602 } 9603 9604 // "The default single-line container name is "p"." 9605 var defaultSingleLineContainerName = "p"; 9606 9607 //@} 9608 ///// Check whether the given element is an end break ///// 9609 //@{ 9610 function isEndBreak(element) { 9611 return (isNamedHtmlElement(element, 'br') && element.parentNode.lastChild === element); 9612 } 9613 9614 //@} 9615 ///// Create an end break ///// 9616 //@{ 9617 function createEndBreak() { 9618 return document.createElement("br"); 9619 } 9620 9621 /** 9622 * Ensure the container is editable 9623 * E.g. when called for an empty paragraph or header, and the browser is not IE, 9624 * we need to append a br (marked with class aloha-end-br) 9625 * For IE7, there is a special behaviour that will append zero-width whitespace 9626 * @param {DOMNode} container 9627 */ 9628 function ensureContainerEditable(container) { 9629 if (!container) { 9630 return; 9631 } 9632 9633 // Because it is useful to be able to completely empty the contents of 9634 // an editing host during editing. So long as the container's 9635 // contenteditable attribute is "true" (as is the case during editing), 9636 // the element will be rendered visibly in all browsers. This fact 9637 // allows us to not have to prop up the container with a <br> in order 9638 // to keep it accessible to the editor. 9639 if (isEditingHost(container)) { 9640 return; 9641 } 9642 9643 if (isNamedHtmlElement(container.lastChild, "br")) { 9644 return; 9645 } 9646 9647 if ($_(container.childNodes).some(isVisible)) { 9648 return; 9649 } 9650 9651 if (!isMSIE) { 9652 // for normal browsers, the end-br will do 9653 container.appendChild(createEndBreak()); 9654 } 9655 9656 } 9657
9658 /** 9659 * Node names that can not be unwrapped. 9660 * 9661 * @const 9662 * @type {string[]} 9663 */ 9664 var NOT_UNWRAPPABLE_NODES = ['TABLE', 'OL', 'UL', 'DL']; 9665 9666 /** 9667 * Checks if `node` can be unwrapped. 9668 * 9669 * @param {Element} node 9670 * @return {boolean} 9671 */ 9672 function isUnwrappable(node) { 9673 return jQuery.inArray(node.nodeName, NOT_UNWRAPPABLE_NODES) === -1; 9674 } 9675 //@} 9676 ///// Assorted block formatting command algorithms ///// 9677 //@{ 9678 9679 function fixDisallowedAncestors(node, range) { 9680 var i; 9681 9682 // "If node is not editable, abort these steps." 9683 if (!isEditable(node)) { 9684 return; 9685 } 9686 9687 // "If node is not an allowed child of any of its ancestors in the same 9688 // editing host, and is not an HTML element with local name equal to the 9689 // default single-line container name:" 9690 if ($_(getAncestors(node)).every(function (ancestor) { return !inSameEditingHost(node, ancestor) || !isAllowedChild(node, ancestor); }) 9691 && !isHtmlElement_obsolete(node, defaultSingleLineContainerName)) { 9692 // "If node is a dd or dt, wrap the one-node list consisting of node, 9693 // with sibling criteria returning true for any dl with no attributes 9694 // and false otherwise, and new parent instructions returning the 9695 // result of calling createElement("dl") on the context object. Then 9696 // abort these steps." 9697 if (isHtmlElementInArray(node, ["dd", "dt"])) { 9698 wrap( 9699 [node], 9700 function (sibling) { 9701 return isNamedHtmlElement(sibling, 'dl') && !sibling.attributes.length; 9702 }, 9703 function () { 9704 return document.createElement("dl"); 9705 }, 9706 range 9707 ); 9708 return; 9709 } 9710 9711 // "If node is not a prohibited paragraph child, abort these steps." 9712 if (!isProhibitedParagraphChild(node)) { 9713 return; 9714 } 9715 9716 // "Set the tag name of node to the default single-line container name, 9717 // and let node be the result." 9718 node = setTagName(node, defaultSingleLineContainerName, range); 9719 9720 ensureContainerEditable(node); 9721 9722 // "Fix disallowed ancestors of node." 9723 fixDisallowedAncestors(node, range); 9724 9725 // "Let descendants be all descendants of node." 9726 var descendants = getDescendants(node); 9727 9728 // "Fix disallowed ancestors of each member of descendants." 9729 for (i = 0;
9729 i < descendants.length; i++) { 9730 fixDisallowedAncestors(descendants[i], range); 9731 } 9732 9733 // "Abort these steps." 9734 return; 9735 } 9736 9737 // "Record the values of the one-node list consisting of node, and let 9738 // values be the result." 9739 var values = recordValues([node]); 9740 var newStartOffset, newEndOffset; 9741 9742 // "While node is not an allowed child of its parent, split the parent of 9743 // the one-node list consisting of node." 9744 while (!isAllowedChild(node, node.parentNode)) { 9745 // If the parent contains only this node and possibly empty text nodes, we rather want to unwrap the node, instead of splitting. 9746 // With splitting, we would get empty nodes, like: 9747 // split: <p><p>foo</p></p> -> <p></p><p>foo</p> (bad) 9748 // unwrap: <p><p>foo</p></p> -> <p>foo</p> (good) 9749 9750 // First remove empty text nodes that are children of the parent and correct the range if necessary 9751 // we do this to have the node being the only child of its parent, so that we can replace the parent with the node 9752 for (i = node.parentNode.childNodes.length - 1; i >= 0; --i) { 9753 if (node.parentNode.childNodes[i].nodeType == 3 && node.parentNode.childNodes[i].data.length == 0) { 9754 // we remove the empty text node 9755 node.parentNode.removeChild(node.parentNode.childNodes[i]); 9756 9757 // if the range points to somewhere behind the removed text node, we reduce the offset 9758 if (range.startContainer == node.parentNode && range.startOffset > i) { 9759 range.startOffset--; 9760 } 9761 if (range.endContainer == node.parentNode && range.endOffset > i) { 9762 range.endOffset--; 9763 } 9764 } 9765 } 9766 9767 // now that the parent has only the node as child (because we 9768 // removed any existing empty text nodes), we can safely unwrap the 9769 // node's contents, and correct the range if necessary 9770 // if the node is unwrappable (table, lists) the node is not unwrapped 9771 // but splitted. 9772 if (node.parentNode.childNodes.length == 1 && isUnwrappable(node)) { 9773 newStartOffset = range.startOffset; 9774 newEndOffset = range.endOffset; 9775 9776 if (range.startContainer === node.parentNode && range.startOffset > getIndexInParent(node)) { 9777 // the node (1 element) will be replaced by its contents (contents().length elements) 9778 newStartOffset = range.startOffset + (jQuery(node).contents().length - 1); 9779 } 9780 if (range.endContainer === node.parentNode && range.endOffset > getIndexInParent(node)) { 9781 // the node (1 element) will be replaced by its contents (contents().length elements) 9782 newEndOffset = range.endOffset + (jQuery(node).contents().length - 1); 9783 } 9784 jQuery(node).contents().unwrap(); 9785 range.startOffset = newStartOffset; 9786 range.endOffset = newEndOffset; 9787 // after unwrapping, we are done 9788 break; 9789 } else { 9790 // store the original parent 9791 var originalParent = node.parentNode; 9792 splitParent([node], range); 9793 // check whether the parent did not change, so the split did not work, e.g. 9794 // because we already reached the editing host itself. 9795 // this situation can occur, e.g. when we insert a paragraph into an contenteditable span 9796 // in such cases, we just unwrap the contents of the paragraph 9797 if (originalParent === node.parentNode) { 9798 // so we unwrap now 9799 newStartOffset = range.startOffset; 9800 newEndOffset = range.endOffset; 9801 9802 if (range.startContainer === node.parentNode && range.startOffset > getIndexInParent(node)) { 9803 // the node (1 element) will be replaced by its contents (contents().length elements) 9804 newStartOffset = range.startOffset + (jQuery(node).contents().length - 1); 9805 } 9806 if (range.endContainer === node.parentNode && range.endOffset > getIndexInParent(node)) { 9807 // the node (1 element) will be replaced by its contents (contents().length elements) 9808 newEndOffset = range.endOffset + (jQuery(node).contents().length - 1); 9809 } 9810 jQuery(node).contents().unwrap(); 9811 range.startOffset = newStartOffset; 9812 range.endOffset = newEndOffset; 9813 // after unwrapping, we are done 9814 break; 9815 } 9816 } 9817 } 9818 9819 // "Restore the values from values." 9820 restoreValues(values, range); 9821 } 9822 9823 /** 9824 * This method "normalizes" sublists of the given item (which is supposed to be a LI): 9825 * If sublists are found in the LI element, they are moved directly into the outer list. 9826 * @param item item 9827 * @param range range, which will be modified if necessary 9828 */ 9829 function normalizeSublists(item, range) { 9830 // "If item is not an li or it is not editable or its parent is not 9831 // editable, abort these steps." 9832 if (!isNamedHtmlElement(item, 'LI') || !isEditable(item) || !isEditable(item.parentNode)) { 9833 return; 9834 } 9835 9836 // "Let new item be null." 9837 var newItem = null; 9838 9839 function isOlUl(node) { 9840 return isHtmlElementInArray(node, ["OL", "UL"]); 9841 } 9842 9843 // "While item has an ol or ul child:" 9844 while ($_(item.childNodes).some(isOlUl)) { 9845 // "Let child be the last child of item." 9846 var child = item.lastChild; 9847 9848 // "If child is an ol or ul, or new item is null and child is a Text 9849 // node whose data consists of zero of more space characters:" 9850 if (isHtmlElementInArray(child, ["OL", "UL"]) || (!newItem && child.nodeType == Node.TEXT_NODE && /^[ \t\n\f\r]*$/.test(child.data))) { 9851 // "Set new item to null." 9852 newItem = null; 9853 9854 // "Insert child into the parent of item immediately following 9855 // item, preserving ranges." 9856 movePreservingRanges(child, item.parentNode, 1 + getIndexInParent(item), range); 9857 9858 // "Otherwise:" 9859 } else { 9860 // "If new item is null, let new item be the result of calling 9861 // createElement("li") on the ownerDocument of item, then insert 9862 // new item into the parent of item immediately after item." 9863 if (!newItem) { 9864 newItem = item.ownerDocument.createElement("li"); 9865 item.parentNode.insertBefore(newItem, item.nextSibling); 9866 } 9867 9868 // "Insert child into new item as its first child, preserving 9869 // ranges." 9870 movePreservingRanges(child, newItem, 0, range); 9871 } 9872 } 9873 } 9874
9875 /** 9876 * This method is the exact opposite of normalizeSublists. 9877 * List nodes directly nested into each other are corrected to be nested in li elements (so that the resulting lists conform the html5 specification) 9878 * @param item list node 9879 * @param range range, which is preserved when modifying the list 9880 */ 9881 function unNormalizeSublists(item, range) { 9882 // "If item is not an ol or ol or it is not editable or its parent is not 9883 // editable, abort these steps." 9884 if (!isHtmlElementInArray(item, ["OL", "UL"]) || !isEditable(item)) { 9885 return; 9886 } 9887 9888 var $list = jQuery(item); 9889 $list.children("ol,ul").each(function (index, sublist) { 9890 if (isNamedHtmlElement(sublist.previousSibling, "LI")) { 9891 // move the sublist into the LI 9892 movePreservingRanges(sublist, sublist.previousSibling, sublist.previousSibling.childNodes.length, range); 9893 } 9894 }); 9895 } 9896 9897 //@} 9898 ///// Block-extending a range ///// 9899 //@{ 9900 9901 function blockExtend(range) { 9902 // "Let start node, start offset, end node, and end offset be the start 9903 // and end nodes and offsets of the range." 9904 var startNode = range.startContainer; 9905 var startOffset = range.startOffset; 9906 var endNode = range.endContainer; 9907 var endOffset = range.endOffset; 9908 9909 // "If some ancestor container of start node is an li, set start offset to 9910 // the index of the last such li in tree order, and set start node to that 9911 // li's parent." 9912 var liAncestors = $_(getAncestors(startNode).concat(startNode)).filter(function (ancestor) { return isNamedHtmlElement(ancestor, 'li'); }).slice(-1); 9913 if (liAncestors.length) { 9914 startOffset = getIndexInParent(liAncestors[0]); 9915 startNode = liAncestors[0].parentNode; 9916 } 9917 9918 // "If (start node, start offset) is not a block start point, repeat the 9919 // following steps:" 9920 if (!isBlockStartPoint(startNode, startOffset)) { 9921 do { 9922 // "If start offset is zero, set it to start node's index, then set 9923 // start node to its parent." 9924 if (startOffset == 0) { 9925 startOffset = getIndexInParent(startNode); 9926 startNode = startNode.parentNode; 9927 9928 // "Otherwise, subtract one from start offset." 9929 } else { 9930 startOffset--; 9931 } 9932 9933 // "If (start node, start offset) is a block boundary point, break from 9934 // this loop." 9935 } while (!isBlockBoundaryPoint(startNode, startOffset)); 9936 } 9937 9938 // "While start offset is zero and start node's parent is not null, set 9939 // start offset to start node's index, then set start node to its parent." 9940 while (startOffset == 0 && startNode.parentNode) { 9941 startOffset = getIndexInParent(startNode); 9942 startNode = startNode.parentNode; 9943 } 9944 9945 // "If some ancestor container of end node is an li, set end offset to one 9946 // plus the index of the last such li in tree order, and set end node to 9947 // that li's parent." 9948 liAncestors = $_(getAncestors(endNode).concat(endNode)).filter(function (ancestor) { return isNamedHtmlElement(ancestor, 'li'); }).slice(-1); 9949 if (liAncestors.length) { 9950 endOffset = 1 + getIndexInParent(liAncestors[0]); 9951 endNode = liAncestors[0].parentNode; 9952 } 9953 9954 // "If (end node, end offset) is not a block end point, repeat the 9955 // following steps:" 9956 if (!isBlockEndPoint(endNode, endOffset)) { 9957 do { 9958 // "If end offset is end node's length, set it to one plus end node's 9959 // index, then set end node to its parent." 9960 if (endOffset == getNodeLength(endNode)) { 9961 endOffset = 1 + getIndexInParent(endNode); 9962 endNode = endNode.parentNode; 9963 9964 // "Otherwise, add one to end offset. 9965 } else { 9966 endOffset++; 9967 } 9968 9969 // "If (end node, end offset) is a block boundary point, break from 9970 // this loop." 9971 } while (!isBlockBoundaryPoint(endNode, endOffset)); 9972 } 9973 9974 // "While end offset is end node's length and end node's parent is not 9975 // null, set end offset to one plus end node's index, then set end node to 9976 // its parent." 9977 while (endOffset == getNodeLength(endNode) && endNode.parentNode) { 9978 endOffset = 1 + getIndexInParent(endNode); 9979 endNode = endNode.parentNode; 9980 } 9981 9982 // "Let new range be a new range whose start and end nodes and offsets 9983 // are start node, start offset, end node, and end offset." 9984 var newRange = _core.rangy.createRange(); 9985 newRange.setStart(startNode, startOffset); 9986 newRange.setEnd(endNode, endOffset); 9987 9988 // "Return new range." 9989 return newRange; 9990 } 9991 9992 function getSelectionListState() { 9993 // "Block-extend the active range, and let new range be the result." 9994 var newRange = blockExtend(getActiveRange()); 9995 9996 // "Let node list be a list of nodes, initially empty." 9997 //
9998 // "For each node contained in new range, append node to node list if the 9999 // last member of node list (if any) is not an ancestor of node; node is 10000 // editable; node is not an indentation element; and node is either an ol 10001 // or ul, or the child of an ol or ul, or an allowed child of "li"." 10002 var nodeList = getContainedNodes(newRange, function (node) { 10003 return isEditable(node) && !isIndentationElement(node) && (isHtmlElementInArray(node, ["ol", "ul"]) || isHtmlElementInArray(node.parentNode, ["ol", "ul"]) || isAllowedChild(node, "li")); 10004 }); 10005 10006 // "If node list is empty, return "none"." 10007 if (!nodeList.length) { 10008 return "none"; 10009 } 10010 10011 // "If every member of node list is either an ol or the child of an ol or 10012 // the child of an li child of an ol, and none is a ul or an ancestor of a 10013 // ul, return "ol"." 10014 if ($_(nodeList).every(function (node) { return (isNamedHtmlElement(node, 'ol') 10015 || isNamedHtmlElement(node.parentNode, "ol") 10016 || (isNamedHtmlElement(node.parentNode, "li") 10017 && isNamedHtmlElement(node.parentNode.parentNode, "ol"))); }) 10018 && !$_(nodeList).some(function (node) { return isNamedHtmlElement(node, 'ul') || (node.querySelector && node.querySelector("ul")); })) { 10019 return "ol"; 10020 } 10021 10022 // "If every member of node list is either a ul or the child of a ul or the 10023 // child of an li child of a ul, and none is an ol or an ancestor of an ol, 10024 // return "ul"." 10025 if ($_(nodeList).every(function (node) { return (isNamedHtmlElement(node, 'ul') 10026 || isNamedHtmlElement(node.parentNode, "ul") 10027 || (isNamedHtmlElement(node.parentNode, "li") 10028 && isNamedHtmlElement(node.parentNode.parentNode, "ul"))); }) 10029 && !$_(nodeList).some(function (node) { return isNamedHtmlElement(node, 'ol') || (node.querySelector && node.querySelector("ol")); })) { 10030 return "ul"; 10031 } 10032 10033 var hasOl = $_(nodeList).some(function (node) { 10034 return (isNamedHtmlElement(node, 'ol') 10035 || isNamedHtmlElement(node.parentNode, "ol") 10036 || (node.querySelector && node.querySelector("ol")) 10037 || (isNamedHtmlElement(node.parentNode, "li") 10038 && isNamedHtmlElement(node.parentNode.parentNode, "ol"))); 10039 }); 10040 var hasUl = $_(nodeList).some(function (node) { 10041 return (isNamedHtmlElement(node, 'ul') 10042 || isNamedHtmlElement(node.parentNode, "ul") 10043 || (node.querySelector && node.querySelector("ul")) 10044 || (isNamedHtmlElement(node.parentNode, "li") 10045 && isNamedHtmlElement(node.parentNode.parentNode, "ul"))); 10046 }); 10047 // "If some member of node list is either an ol or the child or ancestor of 10048 // an ol or the child of an li child of an ol, and some member of node list 10049 // is either a ul or the child or ancestor of a ul or the child of an li 10050 // child of a ul, return "mixed"." 10051 if (hasOl && hasUl) { 10052 return "mixed"; 10053 } 10054 10055 // "If some member of node list is either an ol or the child or ancestor of 10056 // an ol or the child of an li child of an ol, return "mixed ol"." 10057 if (hasOl) { 10058 return "mixed ol"; 10059 } 10060 10061 // "If some member of node list is either a ul or the child or ancestor of 10062 // a ul or the child of an li child of a ul, return "mixed ul"." 10063 if (hasUl) { 10064 return "mixed ul"; 10065 } 10066 10067 // "Return "none"." 10068 return "none"; 10069 } 10070 10071 function getAlignmentValue(node) { 10072 // "While node is neither null nor an Element, or it is an Element but its 10073 // "display" property has resolved value "inline" or "none", set node to 10074 // its parent." 10075 while ((node && node.nodeType != Node.ELEMENT_NODE) || (node.nodeType == Node.ELEMENT_NODE && jQuery.inArray($_.getComputedStyle(node).display, ["inline", "none"]) != -1)) { 10076 node = node.parentNode; 10077 } 10078 10079 // "If node is not an Element, return "left"." 10080 if (!node || node.nodeType != Node.ELEMENT_NODE) { 10081 return "left"; 10082 } 10083 10084 var resolvedValue = $_.getComputedStyle(node).textAlign 10085 // Hack around browser non-standardness 10086 .replace(/^-(moz|webkit)-/, "").replace(/^auto$/, "start"); 10087 10088 // "If node's "text-align" property has resolved value "start", return 10089 // "left" if the directionality of node is "ltr", "right" if it is "rtl"." 10090 if (resolvedValue == "start") { 10091 return getDirectionality(node) == "ltr" ? "left" : "right"; 10092 } 10093 10094 // "If node's "text-align" property has resolved value "end", return 10095 // "right" if the directionality of node is "ltr", "left" if it is "rtl"." 10096 if (resolvedValue == "end") { 10097 return getDirectionality(node) == "ltr" ? "right" : "left"; 10098 } 10099 10100 // "If node's "text-align" property has resolved value "center", "justify", 10101 // "left", or "right", return that value." 10102 if (jQuery.inArray(resolvedValue, ["center", "justify", "left", "right"]) != -1) { 10103 return resolvedValue; 10104 } 10105 10106 // "Return "left"." 10107 return "left"; 10108 } 10109 10110 //@} 10111 ///// Recording and restoring overrides ///// 10112 //@{ 10113 10114 function recordCurrentOverrides(range) { 10115 // "Let overrides be a list of (string, string or boolean) ordered pairs, 10116 // initially empty." 10117 var overrides = []; 10118 10119 // "If there is a value override for "createLink", add ("createLink", value 10120 // override for "createLink") to overrides." 10121 if (getValueOverride("createlink", range) !== undefined) { 10122 overrides.push(["createlink", getValueOverride("createlink", range)]); 10123 } 10124 10125 // "For each command in the list "bold", "italic", "strikethrough", 10126 // "subscript", "superscript", "underline", in order: if there is a state 10127 // override for command, add (command, command's state override) to 10128 // overrides." 10129 $_(["bold", "italic", "strikethrough", "subscript", "superscript", "underline"]).forEac
10129h(function (command) { 10130 if (getStateOverride(command, range) !== undefined) { 10131 overrides.push([command, getStateOverride(command, range)]); 10132 } 10133 }); 10134 10135 // "For each command in the list "fontName", "fontSize", "foreColor", 10136 // "hiliteColor", in order: if there is a value override for command, add 10137 // (command, command's value override) to overrides." 10138 $_(["fontname", "fontsize", "forecolor", "hilitecolor"]).forEach(function (command) { 10139 if (getValueOverride(command, range) !== undefined) { 10140 overrides.push([command, getValueOverride(command, range)]); 10141 } 10142 }); 10143 10144 // "Return overrides." 10145 return overrides; 10146 } 10147 10148 function recordCurrentStatesAndValues(range) { 10149 // "Let overrides be a list of (string, string or boolean) ordered pairs, 10150 // initially empty." 10151 var overrides = []; 10152 10153 // "Let node be the first editable Text node effectively contained in the 10154 // active range, or null if there is none." 10155 var node = $_(getAllEffectivelyContainedNodes(range)).filter(function (node) { 10156 return isEditable(node) && node.nodeType == Node.TEXT_NODE; 10157 })[0]; 10158 10159 // "If node is null, return overrides." 10160 if (!node) { 10161 return overrides; 10162 } 10163 10164 // "Add ("createLink", value for "createLink") to overrides." 10165 overrides.push(["createlink", commands.createlink.value(range)]); 10166 10167 // "For each command in the list "bold", "italic", "strikethrough", 10168 // "subscript", "superscript", "underline", in order: if node's effective 10169 // command value for command is one of its inline command activated values, 10170 // add (command, true) to overrides, and otherwise add (command, false) to 10171 // overrides." 10172 10173 $_(["bold", "italic", "strikethrough", "subscript", "superscript", "underline"]).forEach(function (command) { 10174 if ($_(commands[command].inlineCommandActivatedValues).indexOf(getEffectiveCommandValue(node, command)) != -1) { 10175 overrides.push([command, true]); 10176 } else { 10177 overrides.push([command, false]); 10178 } 10179 }); 10180 10181 // "For each command in the list "fontName", "foreColor", "hiliteColor", in 10182 // order: add (command, command's value) to overrides." 10183 10184 $_(["fontname", "fontsize", "forecolor", "hilitecolor"]).forEach(function (command) { 10185 overrides.push([command, commands[command].value(range)]); 10186 }); 10187 10188 // "Add ("fontSize", node's effective command value for "fontSize") to 10189 // overrides." 10190 overrides.push(["fontsize", getEffectiveCommandValue(node, "fontsize")]); 10191 10192 // "Return overrides." 10193 return overrides; 10194 } 10195 10196 function restoreStatesAndValues(overrides, range) { 10197 var i; 10198 var command; 10199 var override; 10200 // "Let node be the first editable Text node effectively contained in the 10201 // active range, or null if there is none." 10202 var node = $_(getAllEffectivelyContainedNodes(range)).filter(function (node) { 10203 return isEditable(node) && node.nodeType == Node.TEXT_NODE; 10204 })[0]; 10205 10206 function isEditableTextNode(node) { 10207 return isEditable(node) && node.nodeType == Node.TEXT_NODE; 10208 } 10209 10210 // "If node is not null, then for each (command, override) pair in 10211 // overrides, in order:" 10212 if (node) { 10213 10214 for (i = 0; i < overrides.length; i++) { 10215 command = overrides[i][0]; 10216 override = overrides[i][1]; 10217 10218 // "If override is a boolean, and queryCommandState(command) 10219 // returns something different from override, call 10220 // execCommand(command)." 10221 if (typeof override == "boolean" && myQueryCommandState(command, range) != override) { 10222 myExecCommand(command, false, override, range); 10223 10224 // "Otherwise, if override is a string, and command is not 10225 // "fontSize", and queryCommandValue(command) returns something not 10226 // equivalent to override, call execCommand(command, false, 10227 // override)." 10228 } else if (typeof override == "string" && command != "fontsize" && !areEquivalentValues(command, myQueryCommandValue(command, range), override)) { 10229 myExecCommand(command, false, override, range); 10230 10231 // "Otherwise, if override is a string; and command is "fontSize"; 10232 // and either there is a value override for "fontSize" that is not 10233 // equal to override, or there is no value override for "fontSize" 10234 // and node's effective command value for "fontSize" is not loosely 10235 // equivalent to override: call execCommand("fontSize", false, 10236 // override)." 10237 } else if (typeof override == "string" 10238 && command == "fontsize" 10239 && ((getValueOverride("fontsize", range) !== undefined 10240 && getValueOverride("fontsize", range) !== override) 10241 || (getValueOverride("fontsize", range) === undefined 10242 && !areLooselyEquivalentValues(command, getEffectiveCommandValue(node, "fontsize"), override)))) { 10243 myExecCommand("fontsize", false, override, range); 10244 10245 // "Otherwise, continue this loop from the beginning." 10246 } else { 10247 continue; 10248 } 10249 10250 // "Set node to the first editable Text node effectively contained 10251 // in the active range, if there is one." 10252 node = $_(getAllEffectivelyContainedNodes(range)).filter(isEditableTextNode)[0] || node; 10253 } 10254 10255 // "Otherwise, for each (command, override) pair in overrides, in order:" 10256 } else { 10257 for (i = 0; i < overrides.length; i++) { 10258 command = overrides[i][0]; 10259 override = overrides[i][1]; 10260 10261 // "If override is a boolean, set the state override for command to 10262 // override." 10263 if (typeof override == "boolean") { 10264 setStateOverride(command, override, range); 10265 } 10266 10267 // "If override is a string, set the value override for command to 10268 // override." 10269 if (typeof override == "string") { 10270 setValueOverride(command, override, range); 10271 } 10272 } 10273 } 10274 } 10275 10276 //@} 10277 ///// Canonical space sequences ///// 10278 //@{ 10279 10280 function canonicalSpaceSequence(n, nonBreakingStart, nonBreakingEnd) { 10281 // "If n is zero, return the empty string." 10282 if (n == 0) { 10283 return ""; 10284 } 10285
10286 // "If n is one and both non-breaking start and non-breaking end are false, 10287 // return a single space (U+0020)." 10288 if (n == 1 && !nonBreakingStart && !nonBreakingEnd) { 10289 return " "; 10290 } 10291 10292 // "If n is one, return a single non-breaking space (U+00A0)." 10293 if (n == 1) { 10294 return "\xa0"; 10295 } 10296 10297 // "Let buffer be the empty string." 10298 var buffer = ""; 10299 10300 // "If non-breaking start is true, let repeated pair be U+00A0 U+0020. 10301 // Otherwise, let it be U+0020 U+00A0." 10302 var repeatedPair; 10303 if (nonBreakingStart) { 10304 repeatedPair = "\xa0 "; 10305 } else { 10306 repeatedPair = " \xa0"; 10307 } 10308 10309 // "While n is greater than three, append repeated pair to buffer and 10310 // subtract two from n." 10311 while (n > 3) { 10312 buffer += repeatedPair; 10313 n -= 2; 10314 } 10315 10316 // "If n is three, append a three-element string to buffer depending on 10317 // non-breaking start and non-breaking end:" 10318 if (n == 3) { 10319 buffer += !nonBreakingStart && !nonBreakingEnd ? " \xa0 " : nonBreakingStart && !nonBreakingEnd ? "\xa0\xa0 " : !nonBreakingStart && nonBreakingEnd ? " \xa0\xa0" : nonBreakingStart && nonBreakingEnd ? "\xa0 \xa0" : "impossible"; 10320 10321 // "Otherwise, append a two-element string to buffer depending on 10322 // non-breaking start and non-breaking end:" 10323 } else { 10324 buffer += !nonBreakingStart && !nonBreakingEnd ? "\xa0 " : nonBreakingStart && !nonBreakingEnd ? "\xa0 " : !nonBreakingStart && nonBreakingEnd ? " \xa0" : nonBreakingStart && nonBreakingEnd ? "\xa0\xa0" : "impossible"; 10325 } 10326 10327 // "Return buffer." 10328 return buffer; 10329 } 10330 10331 function canonicalizeWhitespace(node, offset) { 10332 // "If node is neither editable nor an editing host, abort these steps." 10333 if (!isEditable(node) && !isEditingHost(node)) { 10334 return; 10335 } 10336 10337 // "Let start node equal node and let start offset equal offset." 10338 var startNode = node; 10339 var startOffset = offset; 10340 10341 // "Repeat the following steps:" 10342 while (true) { 10343 // "If start node has a child in the same editing host with index start 10344 // offset minus one, set start node to that child, then set start 10345 // offset to start node's length." 10346 if (0 <= startOffset - 1 && inSameEditingHost(startNode, startNode.childNodes[startOffset - 1])) { 10347 startNode = startNode.childNodes[startOffset - 1]; 10348 startOffset = getNodeLength(startNode); 10349 10350 // "Otherwise, if start offset is zero and start node does not follow a 10351 // line break and start node's parent is in the same editing host, set 10352 // start offset to start node's index, then set start node to its 10353 // parent." 10354 } else if (startOffset == 0 && !followsLineBreak(startNode) && inSameEditingHost(startNode, startNode.parentNode)) { 10355 startOffset = getIndexInParent(startNode); 10356 startNode = startNode.parentNode; 10357 10358 // "Otherwise, if start node is a Text node and its parent's resolved 10359 // value for "white-space" is neither "pre" nor "pre-wrap" and start 10360 // offset is not zero and the (start offset â 1)st element of start 10361 // node's data is a space (0x0020) or non-breaking space (0x00A0), 10362 // subtract one from start offset." 10363 } else if (startNode.nodeType == Node.TEXT_NODE && jQuery.inArray($_.getComputedStyle(startNode.parentNode).whiteSpace, ["pre", "pre-wrap"]) == -1 && startOffset != 0 && /[ \xa0]/.test(startNode.data[startOffset - 1])) { 10364 startOffset--; 10365 10366 // "Otherwise, break from this loop." 10367 } else { 10368 break; 10369 } 10370 } 10371 10372 // "Let end node equal start node and end offset equal start offset." 10373 var endNode = startNode; 10374 var endOffset = startOffset; 10375 10376 // "Let length equal zero." 10377 var length = 0; 10378 10379 // "Let follows space be false." 10380 var followsSpace = false; 10381 10382 // "Repeat the following steps:" 10383 while (true) { 10384 // "If end node has a child in the same editing host with index end 10385 // offset, set end node to that child, then set end offset to zero." 10386 if (endOffset < endNode.childNodes.length && inSameEditingHost(endNode, endNode.childNodes[endOffset])) { 10387 endNode = endNode.childNodes[endOffset]; 10388 endOffset = 0; 10389 10390 // "Otherwise, if end offset is end node's length and end node does not 10391 // precede a line break and end node's parent is in the same editing 10392 // host, set end offset to one plus end node's index, then set end node 10393 // to its parent." 10394 } else if (endOffset == getNodeLength(endNode) && !precedesLineBreak(endNode) && inSameEditingHost(endNode, endNode.parentNode)) { 10395 endOffset = 1 + getIndexInParent(endNode); 10396 endNode = endNode.parentNode; 10397 10398 // "Otherwise, if end node is a Text node and its parent's resolved 10399 // value for "white-space" is neither "pre" nor "pre-wrap" and end 10400 // offset is not end node's length and the end offsetth element of
10401 // end node's data is a space (0x0020) or non-breaking space (0x00A0):" 10402 } else if (endNode.nodeType == Node.TEXT_NODE && jQuery.inArray($_.getComputedStyle(endNode.parentNode).whiteSpace, ["pre", "pre-wrap"]) == -1 && endOffset != getNodeLength(endNode) && /[ \xa0]/.test(endNode.data[endOffset])) { 10403 // "If follows space is true and the end offsetth element of end 10404 // node's data is a space (0x0020), call deleteData(end offset, 1) 10405 // on end node, then continue this loop from the beginning." 10406 if (followsSpace && " " == endNode.data[endOffset]) { 10407 endNode.deleteData(endOffset, 1); 10408 continue; 10409 } 10410 10411 // "Set follows space to true if the end offsetth element of end 10412 // node's data is a space (0x0020), false otherwise." 10413 followsSpace = " " == endNode.data[endOffset]; 10414 10415 // "Add one to end offset." 10416 endOffset++; 10417 10418 // "Add one to length." 10419 length++; 10420 10421 // "Otherwise, break from this loop." 10422 } else { 10423 break; 10424 } 10425 } 10426 10427 // "Let replacement whitespace be the canonical space sequence of length 10428 // length. non-breaking start is true if start offset is zero and start 10429 // node follows a line break, and false otherwise. non-breaking end is true 10430 // if end offset is end node's length and end node precedes a line break, 10431 // and false otherwise." 10432 var replacementWhitespace = canonicalSpaceSequence(length, startOffset == 0 && followsLineBreak(startNode), endOffset == getNodeLength(endNode) && precedesLineBreak(endNode)); 10433 10434 // "While (start node, start offset) is before (end node, end offset):" 10435 while (getPosition(startNode, startOffset, endNode, endOffset) == "before") { 10436 // "If start node has a child with index start offset, set start node 10437 // to that child, then set start offset to zero." 10438 if (startOffset < startNode.childNodes.length) { 10439 startNode = startNode.childNodes[startOffset]; 10440 startOffset = 0; 10441 10442 // "Otherwise, if start node is not a Text node or if start offset is 10443 // start node's length, set start offset to one plus start node's 10444 // index, then set start node to its parent." 10445 } else if (startNode.nodeType != Node.TEXT_NODE || startOffset == getNodeLength(startNode)) { 10446 startOffset = 1 + getIndexInParent(startNode); 10447 startNode = startNode.parentNode; 10448 10449 // "Otherwise:" 10450 } else { 10451 // "Remove the first element from replacement whitespace, and let 10452 // element be that element." 10453 var element = replacementWhitespace[0]; 10454 replacementWhitespace = replacementWhitespace.slice(1); 10455 10456 // "If element is not the same as the start offsetth element of 10457 // start node's data:" 10458 if (element != startNode.data[startOffset]) { 10459 // "Call insertData(start offset, element) on start node." 10460 startNode.insertData(startOffset, element); 10461 10462 // "Call deleteData(start offset + 1, 1) on start node." 10463 startNode.deleteData(startOffset + 1, 1); 10464 } 10465 10466 // "Add one to start offset." 10467 startOffset++; 10468 } 10469 } 10470 } 10471 10472 /** 10473 * Remove the node if it doesn't have any children or 1 empty text node. 10474 * This function doesn't handle all cases of unrendered nodes. 10475 * In the following cases the node will not be removed: 10476 * * The node contains multiple empty nodes 10477 * * The child text node contains zero width characters 10478 * * The child text node contains unrendered characters 10479 * 10480 * @param {Text} node The node to check 10481 * @param {Element} range Range object to set the position 10482 * @return {boolean} True if node got removed, false otherwise 10483 */ 10484 function removeNodeIfEmptyAndCorrectRange(node, range) { 10485 var 10486 removeNode = false, 10487 parentNode, 10488 nodeChildNodesLength, 10489 offsetInParent, 10490 firstChildNode; 10491 10492 if (!node || !range) { 10493 return false; 10494 } 10495 10496 parentNode = node.parentNode; 10497 10498 if (!parentNode) { 10499 return false; 10500 } 10501 10502 // Check if the node is empty 10503 nodeChildNodesLength = node.childNodes.length; 10504 if (nodeChildNodesLength === 0) { 10505 removeNode = true; 10506 } 10507 10508 // If the inline node only contains an empty text node, 10509 // it is also considered being empty. 10510 if (nodeChildNodesLength === 1) { 10511 firstChildNode = node.childNodes[0]; 10512 10513 if (firstChildNode.nodeType === Node.TEXT_NODE 10514 && firstChildNode.nodeValue.length === 0) { 10515 removeNode = true; 10516 } 10517 } 10518 10519 // Don't continue if node should not be removed or has no parent 10520 if (!removeNode) { 10521 return false;
10522 } 10523 10524 // Before removing, remember the offset of node in its parent 10525 offsetInParent = getIndexInParent(node); 10526 10527 // I'm not gonna comment this, it's clear what it does. 10528 parentNode.removeChild(node); 10529 10530 // Now the cursor has to be set at the position where the 10531 // removed parent was before. 10532 range.setStart(parentNode, offsetInParent); 10533 range.setEnd(parentNode, offsetInParent); 10534 10535 return true; 10536 } 10537 10538 //@} 10539 ///// Deleting the contents of a range ///// 10540 //@{ 10541 10542 function deleteContents(arg1, arg2, arg3, arg4, arg5) { 10543 // We accept several different calling conventions: 10544 // 10545 // 1) A single argument, which is a range. 10546 // 10547 // 2) Two arguments, the first being a range and the second flags. 10548 // 10549 // 3) Four arguments, the start and end of a range. 10550 // 10551 // 4) Five arguments, the start and end of a range plus flags. 10552 // 10553 // The flags argument is a dictionary that can have up to two keys, 10554 // blockMerging and stripWrappers, whose corresponding values are 10555 // interpreted as boolean. E.g., {stripWrappers: false}. 10556 var range; 10557 var flags = {}; 10558 var i; 10559 10560 if (arguments.length < 3) { 10561 range = arg1; 10562 } else { 10563 range = _core.rangy.createRange(); 10564 range.setStart(arg1, arg2); 10565 range.setEnd(arg3, arg4); 10566 } 10567 if (arguments.length == 2) { 10568 flags = arg2; 10569 } 10570 if (arguments.length == 5) { 10571 flags = arg5; 10572 } 10573 10574 var blockMerging = null != flags.blockMerging ? !!flags.blockMerging : true; 10575 var stripWrappers = null != flags.stripWrappers ? !!flags.stripWrappers : true; 10576 10577 // "If range is null, abort these steps and do nothing." 10578 if (!range) { 10579 return; 10580 } 10581 10582 // "Let start node, start offset, end node, and end offset be range's start 10583 // and end nodes and offsets." 10584 var startNode = range.startContainer; 10585 var startOffset = range.startOffset; 10586 var endNode = range.endContainer; 10587 var endOffset = range.endOffset; 10588 var referenceNode; 10589 10590 // "While start node has at least one child:" 10591 while (startNode.hasChildNodes()) { 10592 // "If start offset is start node's length, and start node's parent is 10593 // in the same editing host, and start node is an inline node, set 10594 // start offset to one plus the index of start node, then set start 10595 // node to its parent and continue this loop from the beginning." 10596 if (startOffset == getNodeLength(startNode) && inSameEditingHost(startNode, startNode.parentNode) && isInlineNode(startNode)) { 10597 startOffset = 1 + getIndexInParent(startNode); 10598 startNode = startNode.parentNode; 10599 continue; 10600 } 10601 10602 // "If start offset is start node's length, break from this loop." 10603 if (startOffset == getNodeLength(startNode)) { 10604 break; 10605 } 10606 10607 // "Let reference node be the child of start node with index equal to 10608 // start offset." 10609 referenceNode = startNode.childNodes[startOffset]; 10610 10611 // "If reference node is a block node or an Element with no children, 10612 // or is neither an Element nor a Text node, break from this loop." 10613 if (isBlockNode(referenceNode) || (referenceNode.nodeType == Node.ELEMENT_NODE && !referenceNode.hasChildNodes()) || (referenceNode.nodeType != Node.ELEMENT_NODE && referenceNode.nodeType != Node.TEXT_NODE)) { 10614 break; 10615 } 10616 10617 // "Set start node to reference node and start offset to 0." 10618 startNode = referenceNode; 10619 startOffset = 0; 10620 } 10621 10622 // "While end node has at least one child:" 10623 while (endNode.hasChildNodes()) { 10624 // "If end offset is 0, and end node's parent is in the same editing 10625 // host, and end node is an inline node, set end offset to the index of 10626 // end node, then set end node to its parent and continue this loop 10627 // from the beginning." 10628 if (endOffset == 0 && inSameEditingHost(endNode, endNode.parentNode) && isInlineNode(endNode)) { 10629 endOffset = getIndexInParent(endNode); 10630 endNode = endNode.parentNode; 10631 continue; 10632 } 10633 10634 // "If end offset is 0, break from this loop." 10635 if (endOffset == 0) { 10636 break; 10637 } 10638 10639 // "Let reference node be the child of end node with index equal to end 10640 // offset minus one." 10641 referenceNode = endNode.childNodes[endOffset - 1]; 10642 10643 // "If reference node is a block node or an Element with no children, 10644 // or is neither an Element nor a Text node, break from this loop." 10645 if (isBlockNode(referenceNode) || (referenceNode.nodeType == Node.ELEMENT_NODE && !referenceNode.hasChildNodes()) || (referenceNode.nodeType != Node.ELEMENT_NODE && referenceNode.nodeType != Node.TEXT_NODE)) { 10646 break; 10647 } 10648 10649 // "Set end node to reference node and end offset to the length of 10650 // reference node." 10651 endNode = referenceNode; 10652 endOffset = getNodeLength(referenceNode); 10653 } 10654 10655 // "If (end node, end offset) is not after (start node, start offset), set 10656 // range's end to its start and abort these steps."
10657 if (getPosition(endNode, endOffset, startNode, startOffset) !== "after") { 10658 range.setEnd(range.startContainer, range.startOffset); 10659 return range; 10660 } 10661 10662 // "If start node is a Text node and start offset is 0, set start offset to 10663 // the index of start node, then set start node to its parent." 10664 // Commented out for unknown reason 10665 //if (startNode.nodeType == Node.TEXT_NODE && startOffset == 0 && startNode != endNode) { 10666 // startOffset = getIndexInParent(startNode); 10667 // startNode = startNode.parentNode; 10668 //} 10669 10670 // "If end node is a Text node and end offset is its length, set end offset 10671 // to one plus the index of end node, then set end node to its parent." 10672 if (endNode.nodeType == Node.TEXT_NODE && endOffset == getNodeLength(endNode) && startNode != endNode) { 10673 endOffset = 1 + getIndexInParent(endNode); 10674 endNode = endNode.parentNode; 10675 } 10676 10677 // "Set range's start to (start node, start offset) and its end to (end 10678 // node, end offset)." 10679 range.setStart(startNode, startOffset); 10680 range.setEnd(endNode, endOffset); 10681 10682 // "Let start block be the start node of range." 10683 var startBlock = range.startContainer; 10684 10685 // "While start block's parent is in the same editing host and start block 10686 // is an inline node, set start block to its parent." 10687 while (inSameEditingHost(startBlock, startBlock.parentNode) && isInlineNode(startBlock)) { 10688 startBlock = startBlock.parentNode; 10689 } 10690 10691 // "If start block is neither a block node nor an editing host, or "span" 10692 // is not an allowed child of start block, or start block is a td or th, 10693 // set start block to null." 10694 if ((!isBlockNode(startBlock) && !isEditingHost(startBlock)) || !isAllowedChild("span", startBlock) || isHtmlElementInArray(startBlock, ["td", "th"])) { 10695 startBlock = null; 10696 } 10697 10698 // "Let end block be the end node of range." 10699 var endBlock = range.endContainer; 10700 10701 // "While end block's parent is in the same editing host and end block is 10702 // an inline node, set end block to its parent." 10703 while (inSameEditingHost(endBlock, endBlock.parentNode) && isInlineNode(endBlock)) { 10704 endBlock = endBlock.parentNode; 10705 } 10706 10707 // "If end block is neither a block node nor an editing host, or "span" is 10708 // not an allowed child of end block, or end block is a td or th, set end 10709 // block to null." 10710 if ((!isBlockNode(endBlock) && !isEditingHost(endBlock)) || !isAllowedChild("span", endBlock) || isHtmlElementInArray(endBlock, ["td", "th"])) { 10711 endBlock = null; 10712 } 10713 10714 // "Record current states and values, and let overrides be the result." 10715 var overrides = recordCurrentStatesAndValues(range); 10716 var parent_; 10717 // "If start node and end node are the same, and start node is an editable 10718 // Text node:" 10719 if (startNode == endNode && isEditable(startNode) && startNode.nodeType == Node.TEXT_NODE) { 10720 // "Let parent be the parent of node." 10721 parent_ = startNode.parentNode; 10722 10723 // "Call deleteData(start offset, end offset â start offset) on start 10724 // node." 10725 startNode.deleteData(startOffset, endOffset - startOffset); 10726 10727 // if deleting the text moved two spaces together, we replace the left one by a , which makes the two spaces a visible 10728 // two space sequence 10729 if (startOffset > 0 && startNode.data.substr(startOffset - 1, 1) === ' ' && startOffset < startNode.data.length && startNode.data.substr(startOffset, 1) === ' ') { 10730 startNode.replaceData(startOffset - 1, 1, '\xa0'); 10731 } 10732 10733 // "Canonicalize whitespace at (start node, start offset)." 10734 canonicalizeWhitespace(startNode, startOffset); 10735 10736 // Remove the parent node, if it is an inline node and empty 10737 // If the node is inline and empty, range correction will happen inside 10738 // removeNodeIfEmptyAndCorrectRange, otherwise it'will be done below. 10739 if (!isInlineNode(parent_) || !removeNodeIfEmptyAndCorrectRange(parent_, range)) { 10740 // "Set range's end to its start." 10741 // Ok, also set the range's start to its start, because modifying the text 10742 // might have somehow corrupted the range 10743 range.setStart(range.startContainer, range.startOffset); 10744 range.setEnd(range.startContainer, range.startOffset); 10745 } 10746 10747 // "Restore states and values from overrides." 10748 restoreStatesAndValues(overrides, range); 10749 10750 // "If parent is editable or an editing host, is not an inline node,
10751 // and has no children, call createElement("br") on the context object 10752 // and append the result as the last child of parent." 10753 // only do this, if the offsetHeight is 0 10754 if ((isEditable(parent_) || isEditingHost(parent_)) && !isInlineNode(parent_)) { 10755 ensureContainerEditable(parent_); 10756 } 10757 10758 // "Abort these steps." 10759 return range; 10760 } 10761 10762 // "If start node is an editable Text node, call deleteData() on it, with 10763 // start offset as the first argument and (length of start node â start 10764 // offset) as the second argument." 10765 if (isEditable(startNode) && startNode.nodeType == Node.TEXT_NODE) { 10766 startNode.deleteData(startOffset, getNodeLength(startNode) - startOffset); 10767 } 10768 10769 // "Let node list be a list of nodes, initially empty." 10770 // 10771 // "For each node contained in range, append node to node list if the last 10772 // member of node list (if any) is not an ancestor of node; node is 10773 // editable; and node is not a thead, tbody, tfoot, tr, th, or td." 10774 var nodeList = getContainedNodes( 10775 range, 10776 function (node) { 10777 return (enableContentEditableDeletion ? true : isEditable(node)) && !isHtmlElementInArray(node, ["thead", "tbody", "tfoot", "tr", "th", "td"]); 10778 } 10779 ); 10780 10781 // "For each node in node list:" 10782 for (i = 0; i < nodeList.length; i++) { 10783 var node = nodeList[i]; 10784 10785 // "Let parent be the parent of node." 10786 parent_ = node.parentNode; 10787 10788 // "Remove node from parent." 10789 parent_.removeChild(node); 10790 10791 // "If strip wrappers is true or parent is not an ancestor container of 10792 // start node, while parent is an editable inline node with length 0, 10793 // let grandparent be the parent of parent, then remove parent from 10794 // grandparent, then set parent to grandparent." 10795 if (stripWrappers || (!isAncestor(parent_, startNode) && parent_ != startNode)) { 10796 while (isEditable(parent_) && isInlineNode(parent_) && getNodeLength(parent_) == 0) { 10797 var grandparent = parent_.parentNode; 10798 grandparent.removeChild(parent_); 10799 parent_ = grandparent; 10800 } 10801 } 10802 10803 // "If parent is editable or an editing host, is not an inline node, 10804 // and has no children, call createElement("br") on the context object 10805 // and append the result as the last child of parent." 10806 // only do this, if the offsetHeight is 0 10807 if ((isEditable(parent_) || isEditingHost(parent_)) && !isInlineNode(parent_)) { 10808 ensureContainerEditable(parent_); 10809 } 10810 } 10811 10812 // "If end node is an editable Text node, call deleteData(0, end offset) on 10813 // it." 10814 if (isEditable(endNode) && endNode.nodeType == Node.TEXT_NODE) { 10815 endNode.deleteData(0, endOffset); 10816 } 10817 10818 // Remove parentNode, if it is an empty inline node 10819 if (isInlineNode(startNode.parentNode)) { 10820 removeNodeIfEmptyAndCorrectRange(startNode.parentNode, range); 10821 } 10822 10823 // "Canonicalize whitespace at range's start." 10824 canonicalizeWhitespace(range.startContainer, range.startOffset); 10825 10826 // "Canonicalize whitespace at range's end." 10827 canonicalizeWhitespace(range.endContainer, range.endOffset); 10828 10829 // A reference to the position where a node is removed. 10830 var pos; 10831 10832 // "If block merging is false, or start block or end block is null, or 10833 // start block is not in the same editing host as end block, or start block 10834 // and end block are the same:" 10835 if (!blockMerging || !startBlock || !endBlock || !inSameEditingHost(startBlock, endBlock) || startBlock == endBlock) { 10836 // "Set range's end to its start." 10837 range.setEnd(range.startContainer, range.startOffset); 10838 10839 // Calling delete on the give markup: 10840 // <editable><block><br>[]</block></editable> 10841 // should result in: 10842 // <editable>[]</editable> 10843 var block = startBlock || endBlock; 10844 if (isEmptyOnlyChildOfEditingHost(block)) { 10845 pos = removeNode(block); 10846 range.setStart(pos.node, pos.offset); 10847 range.setEnd(pos.node, pos.offset); 10848 } 10849 10850 // "Restore states and values from overrides." 10851 restoreStatesAndValues(overrides, range); 10852 10853 // "Abort these steps." 10854 return range; 10855 } 10856 10857 // "If start block has one child, which is a collapsed block prop, remove 10858 // its child from it." 10859 if (startBlock.children.length == 1 && isCollapsedBlockProp(startBlock.firstChild)) { 10860 startBlock.removeChild(startBlock.firstChild); 10861 } 10862 10863 // "If end block has one child, which is a collapsed block prop, remove its 10864 // child from it." 10865 if (endBlock.children.length == 1 && isCollapsedBlockProp(endBlock.firstChild)) { 10866 endBlock.removeChild(endBlock.firstChild); 10867 } 10868 10869 var values; 10870 // "If start block is an ancestor of end block:" 10871 if (isAncestor(startBlock, endBlock)) { 10872 // "Let reference node be end block." 10873 referenceNode = endBlock; 10874 10875 // "While reference node is not a child of start block, set reference 10876 // node to its parent." 10877 while (referenceNode.parentNode != startBlock) { 10878 referenceNode = referenceNode.parentNode; 10879 } 10880 10881 // "Set the start and end of range to (start block, index of reference 10882 // node)." 10883 range.setStart(startBlock, getIndexInParent(referenceNode)); 10884 range.setEnd(startBlock, getIndexInParent(referenceNode)); 10885 10886 // "If end block has no children:" 10887 if (!endBlock.hasChildNodes()) {
10888 // "While end block is editable and is the only child of its parent 10889 // and is not a child of start block, let parent equal end block, 10890 // then remove end block from parent, then set end block to 10891 // parent." 10892 while (isEditable(endBlock) && endBlock.parentNode.childNodes.length == 1 && endBlock.parentNode != startBlock) { 10893 parent_ = endBlock; 10894 parent_.removeChild(endBlock); 10895 endBlock = parent_; 10896 } 10897 10898 // "If end block is editable and is not an inline node, and its 10899 // previousSibling and nextSibling are both inline nodes, call 10900 // createElement("br") on the context object and insert it into end 10901 // block's parent immediately after end block." 10902 10903 if (isEditable(endBlock) && !isInlineNode(endBlock) && isInlineNode(endBlock.previousSibling) && isInlineNode(endBlock.nextSibling)) { 10904 endBlock.parentNode.insertBefore(document.createElement("br"), endBlock.nextSibling); 10905 } 10906 10907 // "If end block is editable, remove it from its parent." 10908 if (isEditable(endBlock)) { 10909 endBlock.parentNode.removeChild(endBlock); 10910 } 10911 10912 // "Restore states and values from overrides." 10913 restoreStatesAndValues(overrides, range); 10914 10915 // "Abort these steps." 10916 return range; 10917 } 10918 10919 // "If end block's firstChild is not an inline node, restore states and 10920 // values from overrides, then abort these steps." 10921 if (!isInlineNode(endBlock.firstChild)) { 10922 restoreStatesAndValues(overrides, range); 10923 return range; 10924 } 10925 10926 // "Let children be a list of nodes, initially empty." 10927 var children = []; 10928 10929 // "Append the first child of end block to children." 10930 children.push(endBlock.firstChild); 10931 10932 // "While children's last member is not a br, and children's last 10933 // member's nextSibling is an inline node, append children's last 10934 // member's nextSibling to children." 10935 while (!isNamedHtmlElement(children[children.length - 1], "br") && isInlineNode(children[children.length - 1].nextSibling)) { 10936 children.push(children[children.length - 1].nextSibling); 10937 } 10938 10939 // "Record the values of children, and let values be the result." 10940 values = recordValues(children); 10941 10942 // "While children's first member's parent is not start block, split 10943 // the parent of children." 10944 while (children[0].parentNode != startBlock) { 10945 splitParent(children, range); 10946 } 10947 10948 // "If children's first member's previousSibling is an editable br, 10949 // remove that br from its parent." 10950 if (isEditable(children[0].previousSibling) && isNamedHtmlElement(children[0].previousSibling, "br")) { 10951 children[0].parentNode.removeChild(children[0].previousSibling); 10952 } 10953 10954 // "Otherwise, if start block is a descendant of end block:" 10955 } else if (isDescendant(startBlock, endBlock)) { 10956 // "Set the start and end of range to (start block, length of start 10957 // block)." 10958 range.setStart(startBlock, getNodeLength(startBlock)); 10959 range.setEnd(startBlock, getNodeLength(startBlock)); 10960 10961 // "Let reference node be start block." 10962 referenceNode = startBlock; 10963 10964 // "While reference node is not a child of end block, set reference 10965 // node to its parent." 10966 while (referenceNode.parentNode != endBlock) { 10967 referenceNode = referenceNode.parentNode; 10968 } 10969 10970 // "If reference node's nextSibling is an inline node and start block's 10971 // lastChild is a br, remove start block's lastChild from it." 10972 if (isInlineNode(referenceNode.nextSibling) && isNamedHtmlElement(startBlock.lastChild, "br")) { 10973 startBlock.removeChild(startBlock.lastChild); 10974 } 10975 10976 // "Let nodes to move be a list of nodes, initially empty." 10977 var nodesToMove = []; 10978 10979 // "If reference node's nextSibling is neither null nor a br nor a 10980 // block node, append it to nodes to move." 10981 if (referenceNode.nextSibling && !isNamedHtmlElement(referenceNode.nextSibling, "br") && !isBlockNode(referenceNode.nextSibling)) { 10982 nodesToMove.push(referenceNode.nextSibling); 10983 } 10984 10985 // "While nodes to move is nonempty and its last member's nextSibling 10986 // is neither null nor a br nor a block node, append it to nodes to 10987 // move." 10988 if (nodesToMove.length && nodesToMove[nodesToMove.length - 1].nextSibling && !isNamedHtmlElement(nodesToMove[nodesToMove.length - 1].nextSibling, "br") && !isBlockNode(nodesToMove[nodesToMove.length - 1].nextSibling)) { 10989 nodesToMove.push(nodesToMove[nodesToMove.length - 1].nextSibling); 10990 } 10991 10992 // "Record the values of nodes to move, and let values be the result." 10993 values = recordValues(nodesToMove); 10994 10995 // "For each node in nodes to move, append node as the last child of 10996 // start block, preserving ranges." 10997 $_(nodesToMove).forEach(function (node) { 10998 movePreservingRanges(node, startBlock, -1, range); 10999 }); 11000 11001 // "If the nextSibling of reference node is a br, remove it from its 11002 // parent." 11003 if (isNamedHtmlElement(referenceNode.nextSibling, "br")) { 11004 referenceNode.parentNode.removeChild(referenceNode.nextSibling); 11005 } 11006 11007 // "Otherwise:" 11008 } else { 11009 // "Set the start and end of range to (start block, length of start 11010 // block)." 11011 range.setStart(startBlock, getNodeLength(startBlock)); 11012 range.setEnd(startBlock, getNodeLength(startBlock)); 11013 11014 // "If end block's firstChild is an inline node and start block's
11015 // lastChild is a br, remove start block's lastChild from it." 11016 if (isInlineNode(endBlock.firstChild) && isNamedHtmlElement(startBlock.lastChild, "br")) { 11017 startBlock.removeChild(startBlock.lastChild); 11018 } 11019 11020 // "Record the values of end block's children, and let values be the 11021 // result." 11022 values = recordValues([].slice.call(toArray(endBlock.childNodes))); 11023 11024 // "While end block has children, append the first child of end block 11025 // to start block, preserving ranges." 11026 while (endBlock.hasChildNodes()) { 11027 movePreservingRanges(endBlock.firstChild, startBlock, -1, range); 11028 } 11029 11030 // "While end block has no children, let parent be the parent of end 11031 // block, then remove end block from parent, then set end block to 11032 // parent." 11033 while (!endBlock.hasChildNodes()) { 11034 parent_ = endBlock.parentNode; 11035 parent_.removeChild(endBlock); 11036 endBlock = parent_; 11037 } 11038 } 11039 11040 // "Restore the values from values." 11041 restoreValues(values, range); 11042 11043 // Because otherwise calling deleteContents() with the given selection: 11044 // 11045 // <editable><block>[foo</block><block>bar]</block></editable> 11046 // 11047 // would result in: 11048 // 11049 // <editable><block>[]<br /></block></editable> 11050 // 11051 // instead of: 11052 // 11053 // <editable>[]</editable> 11054 // 11055 // Therefore, the below makes it possible to completely empty contents 11056 // of editing hosts via operations like CTRL+A, DEL. 11057 // 11058 // If startBlock is empty, and startBlock is the immediate and only 11059 // child of its parent editing host, then remove startBlock and collapse 11060 // the selection at the beginning of the editing post. 11061 if (isEmptyOnlyChildOfEditingHost(startBlock)) { 11062 pos = removeNode(startBlock); 11063 range.setStart(pos.node, pos.offset); 11064 range.setEnd(pos.node, pos.offset); 11065 startBlock = pos.node; 11066 } 11067 11068 // "If start block has no children, call createElement("br") on the context 11069 // object and append the result as the last child of start block." 11070 ensureContainerEditable(startBlock); 11071 11072 // "Restore states and values from overrides." 11073 restoreStatesAndValues(overrides, range); 11074 11075 return range; 11076 } 11077 11078 // "To remove a node node while preserving its descendants, split the parent of 11079 // node's children if it has any. If it has no children, instead remove it from 11080 // its parent." 11081 function removePreservingDescendants(node, range) { 11082 if (node.hasChildNodes()) { 11083 splitParent([].slice.call(toArray(node.childNodes)), range); 11084 } else { 11085 node.parentNode.removeChild(node); 11086 } 11087 } 11088 11089 /** 11090 * Determines whether a node must be removed to make the insertparagraph command work in list contexts. 11091 * 11092 * @param {Node} node DOM node to check 11093 * @return {boolean} true if the node should be deleted 11094 */ 11095 function isListWhitespaceToRemove(node) { 11096 // no whitespace node â> no need to remove 11097 if (!isWhitespaceNode(node)) { 11098 return false; 11099 } 11100 11101 // always remove whitespace children from ul and ol 11102 if (node.parentNode.nodeName === 'UL' || node.parentNode.nodeName === 'OL') { 11103 return true; 11104 } 11105 11106 if (node.parentNode.nodeName !== 'LI') { 11107 return false; 11108 } 11109 11110 // only remove whitespace from li if it is between (the opening tag/a block level element) 11111 // and (a block level element/the closing tag) 11112 var index = getIndexInParent(node); 11113 var last = node.parentNode.childNodes.length - 1; 11114 var cleanElements = blockLevelElements.concat(listElements); 11115 11116 return (index === 0 || 11117 cleanElements.indexOf(node.previousSibling.nodeName.toLowerCase()) > -1) && 11118 (index === last || 11119 cleanElements.indexOf(node.nextSibling.nodeName.toLowerCase()) > -1); 11120 } 11121 11122 //@} 11123 ///// Indenting and outdenting ///// 11124 //@{ 11125 11126 function cleanLists(node, range) { 11127 // remove any whitespace nodes around list nodes 11128 if (node) { 11129 jQuery(node).find('ul,ol,li').each(function () { 11130 jQuery(this).contents().each(function () { 11131 if (isListWhitespaceToRemove(this)) { 11132 var index = getIndexInParent(this); 11133 11134 // if the range points to somewhere behind the removed text node, we reduce the offset 11135 if (range.startContainer === this.parentNode && range.startOffset > index) { 11136 range.startOffset--; 11137 } else if (range.startContainer === this) { 11138 // the range starts in the removed text node, let it start right before 11139 range.startContainer = this.parentNode; 11140 range.startOffset = index; 11141 } 11142 // same thing for end of the range 11143 if (range.endContainer === this.parentNode && range.endOffset > index) { 11144 range.endOffset--; 11145 } else if (range.endContainer === this) { 11146 range.endContainer = this.parentNode; 11147 range.endOffset = index; 11148 } 11149 // finally remove the whitespace node 11150 jQuery(this).remove(); 11151 } 11152 }); 11153 }); 11154 } 11155 } 11156 11157 11158 //@} 11159 ///// Indenting and outdenting ///// 11160 //@{ 11161 11162 function indentNodes(nodeList, range) { 11163 // "If node list is empty, do nothing and abort these steps." 11164 if (!nodeList.length) { 11165 return; 11166 } 11167 11168 // "Let first node be the first member of node list." 11169 var firstNode = nodeList[0]; 11170 11171 // "If first node's parent is an ol or ul:" 11172 if (isHtmlElementInArray(firstNode.parentNode, ["OL", "UL"])) { 11173 // "Let tag be the local name of the parent of first node." 11174 var tag = firstNode.parentNode.tagName; 11175 11176 // "Wrap node list, with sibling criteria returning true for an HTML 11177 // element with local name tag and false otherwise, and new parent 11178 // instructions returning the result of calling createElement(tag) on 11179 // the ownerDocument of first node." 11180 wrap( 11181 nodeList, 11182 function (node) { 11183 return isHtmlElement_obsolete(node, tag); 11184 }, 11185 function () { 11186 return firstNode.ownerDocument.createElement(tag); 11187 }, 11188 range 11189 ); 11190 11191 // "Abort these steps." 11192 return; 11193 } 11194 11195 // "Wrap node list, with sibling criteria returning true for a simple 11196 // indentation element and false otherwise, and new parent instructions 11197 // returning the result of calling createElement("blockquote") on the 11198 // ownerDocument of first node. Let new parent be the result." 11199 var newParent = wrap( 11200 nodeList, 11201 function (node) { 11202 return isSimpleIndentationElement(node); 11203 }, 11204 function () { 11205 return firstNode.ownerDocument.createElement("blockquote"); 11206 }, 11207 range 11208 ); 11209 11210 // "Fix disallowed ancestors of new parent." 11211 fixDisallowedAncestors(newParent, range); 11212 } 11213 11214 function outdentNode(node, range) { 11215 // "If node is not editable, abort these steps." 11216 if (!isEditable(node)) { 11217 return; 11218 } 11219 11220 // "If node is a simple indentation element, remove node, preserving its
11221 // descendants. Then abort these steps." 11222 if (isSimpleIndentationElement(node)) { 11223 removePreservingDescendants(node, range); 11224 return; 11225 } 11226 11227 // "If node is an indentation element:" 11228 if (isIndentationElement(node)) { 11229 // "Unset the class and dir attributes of node, if any." 11230 node.removeAttribute("class"); 11231 node.removeAttribute("dir"); 11232 11233 // "Unset the margin, padding, and border CSS properties of node." 11234 node.style.margin = ""; 11235 node.style.padding = ""; 11236 node.style.border = ""; 11237 if (node.getAttribute("style") == "") { 11238 node.removeAttribute("style"); 11239 } 11240 11241 // "Set the tag name of node to "div"." 11242 setTagName(node, "div", range); 11243 11244 // "Abort these steps." 11245 return; 11246 } 11247 11248 // "Let current ancestor be node's parent." 11249 var currentAncestor = node.parentNode; 11250 11251 // "Let ancestor list be a list of nodes, initially empty." 11252 var ancestorList = []; 11253 11254 // "While current ancestor is an editable Element that is neither a simple 11255 // indentation element nor an ol nor a ul, append current ancestor to 11256 // ancestor list and then set current ancestor to its parent." 11257 while (isEditable(currentAncestor) && currentAncestor.nodeType == Node.ELEMENT_NODE && !isSimpleIndentationElement(currentAncestor) && !isHtmlElementInArray(currentAncestor, ["ol", "ul"])) { 11258 ancestorList.push(currentAncestor); 11259 currentAncestor = currentAncestor.parentNode; 11260 } 11261 11262 // "If current ancestor is not an editable simple indentation element:" 11263 if (!isEditable(currentAncestor) || !isSimpleIndentationElement(currentAncestor)) { 11264 // "Let current ancestor be node's parent." 11265 currentAncestor = node.parentNode; 11266 11267 // "Let ancestor list be the empty list." 11268 ancestorList = []; 11269 11270 // "While current ancestor is an editable Element that is neither an 11271 // indentation element nor an ol nor a ul, append current ancestor to 11272 // ancestor list and then set current ancestor to its parent." 11273 while (isEditable(currentAncestor) && currentAncestor.nodeType == Node.ELEMENT_NODE && !isIndentationElement(currentAncestor) && !isHtmlElementInArray(currentAncestor, ["ol", "ul"])) { 11274 ancestorList.push(currentAncestor); 11275 currentAncestor = currentAncestor.parentNode; 11276 } 11277 } 11278 11279 // "If node is an ol or ul and current ancestor is not an editable 11280 // indentation element:" 11281 if (isHtmlElementInArray(node, ["OL", "UL"]) && (!isEditable(currentAncestor) || !isIndentationElement(currentAncestor))) { 11282 // "Unset the reversed, start, and type attributes of node, if any are 11283 // set." 11284 node.removeAttribute("reversed"); 11285 node.removeAttribute("start"); 11286 node.removeAttribute("type"); 11287 11288 // "Let children be the children of node." 11289 var children = [].slice.call(toArray(node.childNodes)); 11290 11291 // "If node has attributes, and its parent is not an ol or ul, set the 11292 // tag name of node to "div"." 11293 if (node.attributes.length && !isHtmlElementInArray(node.parentNode, ["OL", "UL"])) { 11294 setTagName(node, "div", range); 11295 11296 // "Otherwise:" 11297 } else { 11298 // "Record the values of node's children, and let values be the 11299 // result." 11300 var values = recordValues([].slice.call(toArray(node.childNodes))); 11301 11302 // "Remove node, preserving its descendants." 11303 removePreservingDescendants(node, range); 11304 11305 // "Restore the values from values." 11306 restoreValues(values, range); 11307 } 11308 11309 // "Fix disallowed ancestors of each member of children." 11310 var i; 11311 for (i = 0; i < children.length; i++) { 11312 fixDisallowedAncestors(children[i], range); 11313 } 11314 11315 // "Abort these steps." 11316 return; 11317 } 11318 11319 // "If current ancestor is not an editable indentation element, abort these 11320 // steps." 11321 if (!isEditable(currentAncestor) || !isIndentationElement(currentAncestor)) { 11322 return; 11323 } 11324 11325 // "Append current ancestor to ancestor list." 11326 ancestorList.push(currentAncestor); 11327 11328 // "Let original ancestor be current ancestor." 11329 var originalAncestor = currentAncestor; 11330 11331 // "While ancestor list is not empty:" 11332 while (ancestorList.length) { 11333 // "Let current ancestor be the last member of ancestor list." 11334 // 11335 // "Remove the last member of ancestor list." 11336 currentAncestor = ancestorList.pop(); 11337 11338 // "Let target be the child of current ancestor that is equal to either 11339 // node or the last member of ancestor list." 11340 var target = node.parentNode == currentAncestor ? node : ancestorList[ancestorList.length - 1]; 11341 11342 // "If target is an inline node that is not a br, and its nextSibling 11343 // is a br, remove target's nextSibling from its parent." 11344 if (isInlineNode(target) && !isNamedHtmlElement(target, 'BR') && isNamedHtmlElement(target.nextSibling, "BR")) { 11345 target.parentNode.removeChild(target.nextSibling); 11346 } 11347 11348 // "Let preceding siblings be the preceding siblings of target, and let 11349 // following siblings be the following siblings of target." 11350 var precedingSiblings = [].slice.call(toArray(currentAncestor.childNodes), 0, getIndexInParent(target)); 11351 var followingSiblings = [].slice.call(toArray(currentAncestor.childNodes), 1 + getIndexInParent(target)); 11352 11353 // "Indent preceding siblings." 11354 indentNodes(precedingSiblings, range); 11355 11356 // "Indent following siblings." 11357 indentNodes(followingSiblings, range); 11358 } 11359 11360 // "Outdent original ancestor." 11361 outdentNode(originalAncestor, range); 11362 } 11363 11364 11365 //@} 11366 ///// Toggling lists ///// 11367 //@{ 11368 11369 function toggleLists(tagName, range) { 11370 // "Let mode be "disable" if the selection's list state is tag name, and 11371 // "enable" otherwise." 11372 var mode = getSelectionListState() == tagName ? "disable" : "enable"; 11373 11374 tagName = tagName.toUpperCase(); 11375 11376 // "Let other tag name be "ol" if tag name is "ul", and "ul" if tag name is 11377 // "ol"." 11378 var otherTagName = tagName == "OL" ? "UL" : "OL"; 11379 11380 // "Let items be a list of all lis that are ancestor containers of the 11381 // range's start and/or end node." 11382 // 11383 // It's annoying to get this in tree order using functional stuff without 11384 // doing getDescendants(document), which is slow, so I do it imperatively. 11385 var items = []; 11386 (function () { 11387 var ancestorContainer; 11388 for (ancestorContainer = range.endContainer; 11389 ancestorContainer != range.commonAncestorContainer; 11390 ancestorContainer = ancestorContainer.parentNode) { 11391 if (isNamedHtmlElement(ancestorContainer, "li")) { 11392 items.unshift(ancestorContainer); 11393 } 11394 } 11395 for (ancestorContainer = range.startContainer; 11396 ancestorContainer; 11397 ancestorContainer = ancestorContainer.parentNode) { 11398 if (isNamedHtmlElement(ancestorContainer, "li")) { 11399 items.unshift(ancestorContainer); 11400 } 11401 } 11402 }()); 11403 11404 // "For each item in items, normalize sublists of item."
11405 $_(items).forEach(function (thisArg) { 11406 normalizeSublists(thisArg, range); 11407 }); 11408 11409 // "Block-extend the range, and let new range be the result." 11410 var newRange = blockExtend(range); 11411 11412 // "If mode is "enable", then let lists to convert consist of every 11413 // editable HTML element with local name other tag name that is contained 11414 // in new range, and for every list in lists to convert:" 11415 if (mode == "enable") { 11416 $_(getAllContainedNodes(newRange, function (node) { 11417 return isEditable(node) && isHtmlElement_obsolete(node, otherTagName); 11418 })).forEach(function (list) { 11419 // "If list's previousSibling or nextSibling is an editable HTML 11420 // element with local name tag name:" 11421 if ((isEditable(list.previousSibling) && isHtmlElement_obsolete(list.previousSibling, tagName)) || (isEditable(list.nextSibling) && isHtmlElement_obsolete(list.nextSibling, tagName))) { 11422 // "Let children be list's children." 11423 var children = [].slice.call(toArray(list.childNodes)); 11424 11425 // "Record the values of children, and let values be the 11426 // result." 11427 var values = recordValues(children); 11428 11429 // "Split the parent of children." 11430 splitParent(children, range); 11431 11432 // "Wrap children, with sibling criteria returning true for an 11433 // HTML element with local name tag name and false otherwise." 11434 wrap( 11435 children, 11436 function (node) { 11437 return isHtmlElement_obsolete(node, tagName); 11438 }, 11439 function () { 11440 return null; 11441 }, 11442 range 11443 ); 11444 11445 // "Restore the values from values." 11446 restoreValues(values, range); 11447 11448 // "Otherwise, set the tag name of list to tag name." 11449 } else { 11450 setTagName(list, tagName, range); 11451 } 11452 }); 11453 } 11454 11455 // "Let node list be a list of nodes, initially empty." 11456 // 11457 // "For each node node contained in new range, if node is editable; the 11458 // last member of node list (if any) is not an ancestor of node; node 11459 // is not an indentation element; and either node is an ol or ul, or its 11460 // parent is an ol or ul, or it is an allowed child of "li"; then append 11461 // node to node list." 11462 var nodeList = getContainedNodes(newRange, function (node) { 11463 return isEditable(node) && !isIndentationElement(node) && (isHtmlElementInArray(node, ["OL", "UL"]) || isHtmlElementInArray(node.parentNode, ["OL", "UL"]) || isAllowedChild(node, "li")); 11464 }); 11465 11466 // "If mode is "enable", remove from node list any ol or ul whose parent is 11467 // not also an ol or ul." 11468 if (mode == "enable") { 11469 nodeList = $_(nodeList).filter(function (node) { 11470 return !isHtmlElementInArray(node, ["ol", "ul"]) || isHtmlElementInArray(node.parentNode, ["ol", "ul"]); 11471 }); 11472 } 11473 11474 // "If mode is "disable", then while node list is not empty:" 11475 var sublist, values; 11476 11477 function createLi() { 11478 return document.createElement("li"); 11479 } 11480 11481 function isOlUl(node) { 11482 return isHtmlElementInArray(node, ["ol", "ul"]); 11483 } 11484 11485 function makeIsElementPred(tagName) { 11486 return function (node) { 11487 return isHtmlElement_obsolete(node, tagName); 11488 }; 11489 } 11490 11491 function makeCreateElement(tagName) { 11492 return function () { 11493 return document.createElement(tagName); 11494 }; 11495 } 11496 11497 function makeCreateElementSublist(tagName, sublist, range) { 11498 return function () { 11499 // "If sublist's first member's parent is not an editable 11500 // simple indentation element, or sublist's first member's 11501 // parent's previousSibling is not an editable HTML element 11502 // with local name tag name, call createElement(tag name) 11503 // on the context object and return the result." 11504 if (!isEditable(sublist[0].parentNode) || !isSimpleIndentationElement(sublist[0].parentNode) || !isEditable(sublist[0].parentNode.previousSibling) || !isHtmlElement_obsolete(sublist[0].parentNode.previousSibling, tagName)) { 11505 return document.createElement(tagName); 11506 } 11507 11508 // "Let list be sublist's first member's parent's 11509 // previousSibling." 11510 var list = sublist[0].parentNode.previousSibling; 11511 11512 // "Normalize sublists of list's lastChild." 11513 normalizeSublists(list.lastChild, range); 11514 11515 // "If list's lastChild is not an editable HTML element 11516 // with local name tag name, call createElement(tag name) 11517 // on the context object, and append the result as the last 11518 // child of list." 11519 if (!isEditable(list.lastChild) || !isHtmlElement_obsolete(list.lastChild, tagName)) { 11520 list.appendChild(document.createElement(tagName)); 11521 } 11522 11523 // "Return the last child of list." 11524 return list.lastChild; 11525 }; 11526 } 11527 11528 if (mode == "disable") { 11529 while (nodeList.length) { 11530 // "Let sublist be an empty list of nodes." 11531 sublist = []; 11532 11533 // "Remove the first member from node list and append it to 11534 // sublist." 11535 sublist.push(nodeList.shift()); 11536 11537 // "If the first member of sublist is an HTML element with local 11538 // name tag name, outdent it and continue this loop from the 11539 // beginning." 11540 if (isHtmlElement_obsolete(sublist[0], tagName)) {
11541 outdentNode(sublist[0], range); 11542 continue; 11543 } 11544 11545 // "While node list is not empty, and the first member of node list 11546 // is the nextSibling of the last member of sublist and is not an 11547 // HTML element with local name tag name, remove the first member 11548 // from node list and append it to sublist." 11549 while (nodeList.length && nodeList[0] == sublist[sublist.length - 1].nextSibling && !isHtmlElement_obsolete(nodeList[0], tagName)) { 11550 sublist.push(nodeList.shift()); 11551 } 11552 11553 // "Record the values of sublist, and let values be the result." 11554 values = recordValues(sublist); 11555 11556 // "Split the parent of sublist." 11557 splitParent(sublist, range); 11558 11559 // "Fix disallowed ancestors of each member of sublist." 11560 var i; 11561 for (i = 0; i < sublist.length; i++) { 11562 fixDisallowedAncestors(sublist[i], range); 11563 } 11564 11565 // "Restore the values from values." 11566 restoreValues(values, range); 11567 } 11568 11569 // "Otherwise, while node list is not empty:" 11570 } else { 11571 while (nodeList.length) { 11572 // "Let sublist be an empty list of nodes." 11573 sublist = []; 11574 11575 // "While either sublist is empty, or node list is not empty and 11576 // its first member is the nextSibling of sublist's last member:" 11577 while (!sublist.length || (nodeList.length && nodeList[0] == sublist[sublist.length - 1].nextSibling)) { 11578 // "If node list's first member is a p or div, set the tag name 11579 // of node list's first member to "li", and append the result 11580 // to sublist. Remove the first member from node list." 11581 if (isHtmlElementInArray(nodeList[0], ["p", "div"])) { 11582 sublist.push(setTagName(nodeList[0], "li", range)); 11583 nodeList.shift(); 11584 11585 // "Otherwise, if the first member of node list is an li or ol 11586 // or ul, remove it from node list and append it to sublist." 11587 } else if (isHtmlElementInArray(nodeList[0], ["li", "ol", "ul"])) { 11588 sublist.push(nodeList.shift()); 11589 11590 // "Otherwise:" 11591 } else { 11592 // "Let nodes to wrap be a list of nodes, initially empty." 11593 var nodesToWrap = []; 11594 11595 // "While nodes to wrap is empty, or node list is not empty 11596 // and its first member is the nextSibling of nodes to 11597 // wrap's last member and the first member of node list is 11598 // an inline node and the last member of nodes to wrap is 11599 // an inline node other than a br, remove the first member 11600 // from node list and append it to nodes to wrap." 11601 while (!nodesToWrap.length || (nodeList.length && nodeList[0] == nodesToWrap[nodesToWrap.length - 1].nextSibling && isInlineNode(nodeList[0]) && isInlineNode(nodesToWrap[nodesToWrap.length - 1]) && !isNamedHtmlElement(nodesToWrap[nodesToWrap.length - 1], "br"))) { 11602 nodesToWrap.push(nodeList.shift()); 11603 } 11604 11605 // "Wrap nodes to wrap, with new parent instructions 11606 // returning the result of calling createElement("li") on 11607 // the context object. Append the result to sublist." 11608 sublist.push(wrap( 11609 nodesToWrap, 11610 undefined, 11611 createLi, 11612 range 11613 )); 11614 } 11615 } 11616 11617 // "If sublist's first member's parent is an HTML element with 11618 // local name tag name, or if every member of sublist is an ol or 11619 // ul, continue this loop from the beginning." 11620 if (isHtmlElement_obsolete(sublist[0].parentNode, tagName) || $_(sublist).every(isOlUl)) { 11621 continue; 11622 } 11623 11624 // "If sublist's first member's parent is an HTML element with 11625 // local name other tag name:" 11626 if (isHtmlElement_obsolete(sublist[0].parentNode, otherTagName)) { 11627 // "Record the values of sublist, and let values be the 11628 // result." 11629 values = recordValues(sublist); 11630 11631 // "Split the parent of sublist." 11632 splitParent(sublist, range); 11633 11634 // "Wrap sublist, with sibling criteria returning true for an 11635 // HTML element with local name tag name and false otherwise, 11636 // and new parent instructions returning the result of calling 11637 // createElement(tag name) on the context object." 11638 wrap( 11639 sublist, 11640 makeIsElementPred(tagName), 11641 makeCreateElement(tagName), 11642 range 11643 ); 11644 11645 // "Restore the values from values." 11646 restoreValues(values, range); 11647 11648 // "Continue this loop from the beginning." 11649 continue; 11650 } 11651 11652 // "Wrap sublist, with sibling criteria returning true for an HTML 11653 // element with local name tag name and false otherwise, and new 11654 // parent instructions being the following:" 11655 // . . . 11656 // "Fix disallowed ancestors of the previous step's result." 11657 fixDisallowedAncestors(wrap( 11658 sublist, 11659 makeIsElementPred(tagName), 11660 makeCreateElementSublist(tagName, sublist, range), 11661 range 11662 ), range); 11663 } 11664 } 11665 } 11666 11667 11668 //@} 11669 ///// Justifying the selection ///// 11670 //@{ 11671 11672 function justifySelection(alignment, range) { 11673 11674 // "Block-extend the active range, and let new range be the result." 11675 var newRange = blockExtend(range); 11676 11677 // "Let element list be a list of all editable Elements contained in new 11678 // range that either has an attribute in the HTML namespace whose local 11679 // name is "align", or has a style attribute that sets "text-align", or is 11680 // a center." 11681 var elementList = getAllContainedNodes(newRange, function (node) { 11682 return node.nodeType == Node.ELEMENT_NODE && isEditable(node) 11683 // Ignoring namespaces here 11684 && (hasAttribute(node, "align") || node.style.textAlign != "" || isNamedHtmlElement(node, 'center')); 11685 }); 11686 11687 // "For each element in element list:" 11688 var i; 11689 for (i = 0; i < elementList.length; i++) { 11690 var element = elementList[i]; 11691 11692 // "If element has an attribute in the HTML namespace whose local name 11693 // is "align", remove that attribute." 11694 element.removeAttribute("align"); 11695 11696 // "Unset the CSS property "text-align" on element, if it's set by a 11697 // style attribute." 11698 element.style.textAlign = ""; 11699 if (element.getAttribute("style") == "") { 11700 element.removeAttribute("style"); 11701 } 11702 11703 // "If element is a div or span or center with no attributes, remove
11704 // it, preserving its descendants." 11705 if (isHtmlElementInArray(element, ["div", "span", "center"]) && !element.attributes.length) { 11706 removePreservingDescendants(element, range); 11707 } 11708 11709 // "If element is a center with one or more attributes, set the tag 11710 // name of element to "div"." 11711 if (isNamedHtmlElement(element, 'center') && element.attributes.length) { 11712 setTagName(element, "div", range); 11713 } 11714 } 11715 11716 // "Block-extend the active range, and let new range be the result." 11717 newRange = blockExtend(globalRange); 11718 11719 // "Let node list be a list of nodes, initially empty." 11720 var nodeList = []; 11721 11722 // "For each node node contained in new range, append node to node list if 11723 // the last member of node list (if any) is not an ancestor of node; node 11724 // is editable; node is an allowed child of "div"; and node's alignment 11725 // value is not alignment." 11726 nodeList = getContainedNodes(newRange, function (node) { 11727 return isEditable(node) && isAllowedChild(node, "div") && getAlignmentValue(node) != alignment; 11728 }); 11729 11730 function makeIsAlignedDiv(alignment) { 11731 return function (node) { 11732 return isNamedHtmlElement(node, 'div') && $_(node.attributes).every(function (attr) { 11733 return (attr.name == "align" && attr.value.toLowerCase() == alignment) || (attr.name == "style" && getStyleLength(node) == 1 && node.style.textAlign == alignment); 11734 }); 11735 }; 11736 } 11737 11738 function makeCreateAlignedDiv(alignment) { 11739 return function () { 11740 var newParent = document.createElement("div"); 11741 newParent.setAttribute("style", "text-align: " + alignment); 11742 return newParent; 11743 }; 11744 } 11745 11746 // "While node list is not empty:" 11747 while (nodeList.length) { 11748 // "Let sublist be a list of nodes, initially empty." 11749 var sublist = []; 11750 11751 // "Remove the first member of node list and append it to sublist." 11752 sublist.push(nodeList.shift()); 11753 11754 // "While node list is not empty, and the first member of node list is 11755 // the nextSibling of the last member of sublist, remove the first 11756 // member of node list and append it to sublist." 11757 while (nodeList.length && nodeList[0] == sublist[sublist.length - 1].nextSibling) { 11758 sublist.push(nodeList.shift()); 11759 } 11760 11761 // "Wrap sublist. Sibling criteria returns true for any div that has 11762 // one or both of the following two attributes and no other attributes, 11763 // and false otherwise:" 11764 // 11765 // * "An align attribute whose value is an ASCII case-insensitive 11766 // match for alignment. 11767 // * "A style attribute which sets exactly one CSS property 11768 // (including unrecognized or invalid attributes), which is 11769 // "text-align", which is set to alignment. 11770 // 11771 // "New parent instructions are to call createElement("div") on the 11772 // context object, then set its CSS property "text-align" to alignment 11773 // and return the result." 11774 wrap( 11775 sublist, 11776 makeIsAlignedDiv(alignment), 11777 makeCreateAlignedDiv(alignment), 11778 range 11779 ); 11780 } 11781 } 11782 11783 //@} 11784 ///// Move the given collapsed range over adjacent zero-width whitespace characters. 11785 ///// The range is 11786 //@{ 11787 /** 11788 * Move the given collapsed range over adjacent zero-width whitespace characters. 11789 * If the range is not collapsed or is not contained in a text node, it is not modified 11790 * @param range range to modify 11791 * @param forward {Boolean} true to move forward, false to move backward 11792 */ 11793 function moveOverZWSP(range, forward) { 11794 var offset; 11795 if (!range.collapsed) { 11796 return; 11797 } 11798 11799 offset = range.startOffset; 11800 11801 if (forward) { 11802 // check whether the range starts in a text node 11803 if (range.startContainer && range.startContainer.nodeType === Node.TEXT_NODE) { 11804 // move forward (i.e. increase offset) as long as we stay in the text node and have zwsp characters to the right 11805 while (offset < range.startContainer.data.length && range.startContainer.data.charAt(offset) === '\u200b') { 11806 offset++; 11807 } 11808 } 11809 } else { 11810 // check whether the range starts in a text node 11811 if (range.startContainer && range.startContainer.nodeType === Node.TEXT_NODE) { 11812 // move backward (i.e. decrease offset) as long as we stay in the text node and have zwsp characters to the left 11813 while (offset > 0 && range.startContainer.data.charAt(offset - 1) === '\u200b') { 11814 offset--; 11815 } 11816 } 11817 } 11818 11819 // if the offset was changed, set it back to the collapsed range 11820 if (offset !== range.startOffset) { 11821 range.setStart(range.startContainer, offset); 11822 range.setEnd(range.startContainer, offset); 11823 } 11824 } 11825
11826 /** 11827 * implementation of the delete command 11828 * will attempt to delete contents within range if non-collapsed 11829 * or delete the character left of the cursor position if range 11830 * is collapsed. Is used to define the behaviour of the backspace 11831 * button. 11832 * 11833 * @param value is just there for compatibility with the commands api. parameter is ignored. 11834 * @param range the range to execute the delete command for 11835 * @return void 11836 */ 11837 commands["delete"] = { 11838 action: function (value, range) { 11839 var i, 11840 deleteContentsRange; 11841 11842 // "If the active range is not collapsed, delete the contents of the 11843 // active range and abort these steps." 11844 if (!range.collapsed) { 11845 deleteContents(range); 11846 return; 11847 } 11848 11849 // "Canonicalize whitespace at (active range's start node, active 11850 // range's start offset)." 11851 canonicalizeWhitespace(range.startContainer, range.startOffset); 11852 11853 // "Let node and offset be the active range's start node and offset." 11854 var node = range.startContainer; 11855 var offset = range.startOffset; 11856 var isBr = false; 11857 var isHr = false; 11858 11859 // "Repeat the following steps:" 11860 while (true) { 11861 // we need to reset isBr and isHr on every interation of the loop 11862 if (offset > 0) { 11863 isBr = isNamedHtmlElement(node.childNodes[offset - 1], "br") || false; 11864 isHr = isNamedHtmlElement(node.childNodes[offset - 1], "hr") || false; 11865 } 11866 // "If offset is zero and node's previousSibling is an editable 11867 // invisible node, remove node's previousSibling from its parent." 11868 if (offset == 0 && isEditable(node.previousSibling) && isInvisible(node.previousSibling)) { 11869 11870 node.parentNode.removeChild(node.previousSibling); 11871 continue; 11872 11873 } 11874 // "Otherwise, if node has a child with index offset â 1 and that 11875 // child is an editable invisible node, remove that child from 11876 // node, then subtract one from offset." 11877 if (0 <= offset - 1 && offset - 1 < node.childNodes.length && isEditable(node.childNodes[offset - 1]) && (isInvisible(node.childNodes[offset - 1]) || isBr || isHr)) { 11878 11879 node.removeChild(node.childNodes[offset - 1]); 11880 offset--; 11881 11882 if (isBr || isHr) { 11883 11884 range.setStart(node, offset); 11885 range.setEnd(node, offset); 11886 11887 return; 11888 } 11889 11890 continue; 11891 11892 11893 } 11894 // "Otherwise, if offset is zero and node is an inline node, or if 11895 // node is an invisible node, set offset to the index of node, then 11896 // set node to its parent." 11897 if ((offset == 0 && isInlineNode(node)) || isInvisible(node)) { 11898 11899 offset = getIndexInParent(node); 11900 node = node.parentNode; 11901 11902 continue; 11903 11904 } 11905 // "Otherwise, if node has a child with index offset â 1 and that 11906 // child is an editable a, remove that child from node, preserving 11907 // its descendants. Then abort these steps." 11908 if (0 <= offset - 1 && offset - 1 < node.childNodes.length && isEditable(node.childNodes[offset - 1]) && isNamedHtmlElement(node.childNodes[offset - 1], "a")) { 11909 11910 removePreservingDescendants(node.childNodes[offset - 1], range); 11911 11912 return; 11913 11914 } 11915 // "Otherwise, if node has a child with index offset â 1 and that 11916 // child is not a block node or a br or an img, set node to that 11917 // child, then set offset to the length of node." 11918 if (0 <= offset - 1 && offset - 1 < node.childNodes.length && !isBlockNode(node.childNodes[offset - 1]) && !isHtmlElementInArray(node.childNodes[offset - 1], ["br", "img"])) { 11919 11920 node = node.childNodes[offset - 1]; 11921 offset = getNodeLength(node); 11922 11923 continue; 11924 11925 } 11926 // "Otherwise, break from this loop." 11927 // brk is a quick and dirty jslint workaround since I don't want to rewrite this loop 11928 var brk = true; 11929 11930 if (brk) { 11931 break; 11932 } 11933 } 11934 11935 // if the previous node is an aloha-table we want to delete it 11936 var delBlock = getBlockAtPreviousPosition(node, offset); 11937 11938 if (delBlock) { 11939 11940 delBlock.parentNode.removeChild(delBlock); 11941 return; 11942 11943 } 11944 11945 // "If node is a Text node and offset is not zero, call collapse(node, 11946 // offset) on the Selection. Then delete the contents of the range with 11947 // start (node, offset â 1) and end (node, offset) and abort these 11948 // steps." 11949 if (node.nodeType == Node.TEXT_NODE && offset != 0) { 11950 11951 range.setStart(node, offset - 1); 11952 range.setEnd(node, offset - 1); 11953 deleteContentsRange = deleteContents(node, offset - 1, node, offset); 11954 11955 if (deleteContentsRange) { 11956 // The new range position can only be determined inside deleteContents, 11957 // that's why it's necessary to take over the range if one was returned. 11958 range.setStart(deleteContentsRange.startContainer, deleteContentsRange.startOffset); 11959 range.setEnd(deleteContentsRange.endContainer, deleteContentsRange.endOffset); 11960 } 11961 11962 return; 11963 11964 } 11965 11966 // @iebug 11967 // when inserting a special char via the plugin 11968 // there where problems deleting them again with backspace after insertation 11969 // see https://github.com/alohaeditor/Aloha-Editor/issues/517 11970 if (node.nodeType == Node.TEXT_NODE && offset == 0 && isMSIE) { 11971 11972 offset = 1; 11973 range.setStart(node, offset); 11974 range.setEnd(node, offset); 11975 range.startOffset = 0; 11976 deleteContents(range); 11977 11978 return; 11979 11980 } 11981 11982 // "If node is an inline node, abort these steps." 11983 if (isInlineNode(node)) { 11984 11985 return; 11986 11987 } 11988 11989 // "If node has a child with index offset â 1 and that child is a br or 11990 // hr or img, call collapse(node, offset) on the Selection. Then delete 11991 // the contents of the range with start (node, offset â 1) and end 11992 // (node, offset) and abort these steps." 11993 if (0 <= offset - 1 && offset - 1 < node.childNodes.length && isHtmlElementInArray(node.childNodes[offset - 1], ["br", "hr", "img"])) { 11994 11995 range.setStart(node, offset); 11996 range.setEnd(node, offset); 11997 deleteContents(range); 11998 11999 return; 12000 12001 } 12002 12003 // "If node is an li or dt or dd and is the first child of its parent, 12004 // and offset is zero:" 12005 if (isHtmlElementInArray(node, ["li", "dt", "dd"]) && node == node.parentNode.firstChild && offset == 0) { 12006 // "Let items be a list of all lis that are ancestors of node." 12007 // 12008 // Remember, must be in tree order. 12009 var items = []; 12010 var ancestor; 12011 for (ancestor = node.parentNode; ancestor; ancestor = ancestor.parentNode) { 12012 if (isNamedHtmlElement(ancestor, 'li')) { 12013 items.unshift(ancestor); 12014 } 12015 } 12016 12017 // "Normalize sublists of each item in items." 12018 for (i = 0; i < items.length; i++) { 12019 normalizeSublists(items[i], range); 12020 } 12021 12022 // "Record the values of the one-node list consisting of node, and 12023 // let values be the result." 12024 var values = recordValues([node]); 12025 12026 // "Split the parent of the one-node list consisting of node." 12027 splitParent([node], range); 12028 12029 // "Restore the values from values." 12030 restoreValues(values, range); 12031 12032 // "If node is a dd or dt, and it is not an allowed child of any of 12033 // its ancestors in the same editing host, set the tag name of node 12034 // to the default single-line container name and let node be the 12035 // result." 12036 if (isHtmlElementInArray(node, ["dd", "dt"]) && $_(getAncestors(node)).every(function (ancestor) { return !inSameEditingHost(node, ancestor) || !isAllowedChild(node, ancestor); })) { 12037 node = setTagName(node, defaultSingleLineContainerName, range); 12038 } 12039 12040 // "Fix disallowed ancestors of node." 12041 fixDisallowedAncestors(node, range); 12042 12043 // fix the lists to be html5 conformant 12044 for (i = 0; i < items.length; i++) { 12045 unNormalizeSublists(items[i].parentNode, range); 12046 } 12047 12048 // "Abort these steps." 12049 return; 12050 } 12051 12052 // "Let start node equal node and let start offset equal offset." 12053 var startNode = node; 12054 var startOffset = offset; 12055 12056 // "Repeat the following steps:" 12057 while (true) { 12058 // "If start offset is zero, set start offset to the index of start 12059 // node and then set start node to its parent." 12060 if (startOffset == 0) { 12061 startOffset = getIndexInParent(startNode); 12062 startNode = startNode.parentNode; 12063 12064 // "Otherwise, if start node has an editable invisible child with 12065 // index start offset minus one, remove it from start node and 12066 // subtract one from start offset." 12067 } else if (0 <= startOffset - 1 && startOffset - 1 < startNode.childNodes.length && isEditable(startNode.childNodes[startOffset - 1]) && isInvisible(startNode.childNodes[startOffset - 1])
12067) { 12068 startNode.removeChild(startNode.childNodes[startOffset - 1]); 12069 startOffset--; 12070 12071 // "Otherwise, break from this loop." 12072 } else { 12073 break; 12074 } 12075 } 12076 12077 // "If offset is zero, and node has an editable ancestor container in 12078 // the same editing host that's an indentation element:" 12079 if (offset == 0 && $_(getAncestors(node).concat(node)).filter(function (ancestor) { return isEditable(ancestor) && inSameEditingHost(ancestor, node) && isIndentationElement(ancestor); }).length) { 12080 // "Block-extend the range whose start and end are both (node, 0), 12081 // and let new range be the result." 12082 var newRange = _core.rangy.createRange(); 12083 newRange.setStart(node, 0); 12084 newRange.setEnd(node, 0); 12085 newRange = blockExtend(newRange); 12086 12087 // "Let node list be a list of nodes, initially empty." 12088 // 12089 // "For each node current node contained in new range, append 12090 // current node to node list if the last member of node list (if 12091 // any) is not an ancestor of current node, and current node is 12092 // editable but has no editable descendants." 12093 var nodeList = getContainedNodes(newRange, function (currentNode) { 12094 return isEditable(currentNode) && !hasEditableDescendants(currentNode); 12095 }); 12096 12097 // "Outdent each node in node list." 12098 for (i = 0; i < nodeList.length; i++) {
12099 outdentNode(nodeList[i], range); 12100 } 12101 12102 // "Abort these steps." 12103 return; 12104 } 12105 12106 // "If the child of start node with index start offset is a table, 12107 // abort these steps." 12108 if (isNamedHtmlElement(startNode.childNodes[startOffset], "table")) { 12109 return; 12110 } 12111 12112 // "If start node has a child with index start offset â 1, and that 12113 // child is a table:" 12114 if (0 <= startOffset - 1 && startOffset - 1 < startNode.childNodes.length && isNamedHtmlElement(startNode.childNodes[startOffset - 1], "table")) { 12115 // "Call collapse(start node, start offset â 1) on the context 12116 // object's Selection." 12117 range.setStart(startNode, startOffset - 1); 12118 12119 // "Call extend(start node, start offset) on the context object's 12120 // Selection." 12121 range.setEnd(startNode, startOffset); 12122 12123 // "Abort these steps." 12124 return; 12125 } 12126 12127 // "If offset is zero; and either the child of start node with index 12128 // start offset minus one is an hr, or the child is a br whose 12129 // previousSibling is either a br or not an inline node:" 12130 if (offset == 0 12131 && (isNamedHtmlElement(startNode.childNodes[startOffset - 1], "hr") 12132 || (isNamedHtmlElement(startNode.childNodes[startOffset - 1], "br") 12133 && (isNamedHtmlElement(startNode.childNodes[startOffset - 1].previousSibling, "br") 12134 || !isInlineNode(startNode.childNodes[startOffset - 1].previousSibling))))) { 12135 // "Call collapse(node, offset) on the Selection." 12136 range.setStart(node, offset); 12137 range.setEnd(node, offset); 12138 12139 // "Delete the contents of the range with start (start node, start 12140 // offset â 1) and end (start node, start offset)." 12141 deleteContents(startNode, startOffset - 1, startNode, startOffset); 12142 12143 // "Abort these steps." 12144 return; 12145 } 12146 12147 // "If the child of start node with index start offset is an li or dt 12148 // or dd, and that child's firstChild is an inline node, and start 12149 // offset is not zero:" 12150 if (isHtmlElementInArray(startNode.childNodes[startOffset], ["li", "dt", "dd"]) && isInlineNode(startNode.childNodes[startOffset].firstChild) && startOffset != 0) { 12151 // "Let previous item be the child of start node with index start 12152 // offset minus one." 12153 var previousItem = startNode.childNodes[startOffset - 1]; 12154 12155 // "If previous item's lastChild is an inline node other than a br, 12156 // call createElement("br") on the context object and append the 12157 // result as the last child of previous item." 12158 if (isInlineNode(previousItem.lastChild) && !isNamedHtmlElement(previousItem.lastChild, "br")) { 12159 previousItem.appendChild(document.createElement("br")); 12160 } 12161 12162 // "If previous item's lastChild is an inline node, call 12163 // createElement("br") on the context object and append the result 12164 // as the last child of previous item." 12165 if (isInlineNode(previousItem.lastChild)) { 12166 previousItem.appendChild(document.createElement("br")); 12167 } 12168 } 12169 12170 // "If the child of start node with index start offset is an li or dt 12171 // or dd, and its previousSibling is also an li or dt or dd, set start 12172 // node to its child with index start offset â 1, then set start offset 12173 // to start node's length, then set node to start node's nextSibling, 12174 // then set offset to 0." 12175 if (isHtmlElementInArray(startNode.childNodes[startOffset], ["li", "dt", "dd"]) && isHtmlElementInArray(startNode.childNodes[startOffset - 1], ["li", "dt", "dd"])) { 12176 startNode = startNode.childNodes[startOffset - 1]; 12177 startOffset = getNodeLength(startNode); 12178 node = startNode.nextSibling; 12179 offset = 0; 12180 12181 // "Otherwise, while start node has a child with index start offset 12182 // minus one:" 12183 } else { 12184 while (0 <= startOffset - 1 && startOffset - 1 < startNode.childNodes.length) { 12185 // "If start node's child with index start offset minus one is 12186 // editable and invisible, remove it from start node, then 12187 // subtract one from start offset." 12188 if (isEditable(startNode.childNodes[startOffset - 1]) && isInvisible(startNode.childNodes[startOffset - 1])) { 12189 startNode.removeChild(startNode.childNodes[startOffset - 1]); 12190 startOffset--; 12191 12192 // "Otherwise, set start node to its child with index start 12193 // offset minus one, then set start offset to the length of 12194 // start node." 12195 } else { 12196 startNode = startNode.childNodes[startOffset - 1]; 12197 startOffset = getNodeLength(startNode); 12198 } 12199 } 12200 } 12201 12202 // "Delete the contents of the range with start (start node, start 12203 // offset) and end (node, offset)." 12204 var delRange = _core.rangy.createRange(); 12205 delRange.setStart(startNode, startOffset); 12206 delRange.setEnd(node, offset); 12207 deleteContents(delRange); 12208 12209 if (!isAncestorContainer(document.body, range.startContainer)) { 12210 if (delRange.startContainer.hasChildNodes() 12211 || delRange.startContainer.nodeType == Node.TEXT_NODE 12212 || isEditingHost(delRange.startContainer)) { 12213 range.setStart(delRange.startContainer, delRange.startOffset); 12214 range.setEnd(delRange.startContainer, delRange.startOffset); 12215 } else { 12216 range.setStart(delRange.startContainer.parentNode, getIndexInParent(delRange.startContainer)); 12217 range.setEnd(delRange.startContainer.parentNode, getIndexInParent(delRange.startContainer)); 12218 } 12219 } 12220 } 12221 }; 12222 12223 //@} 12224 ///// The formatBlock command ///// 12225 //@{ 12226 // "A formattable block name is "address", "dd", "div", "dt", "h1", "h2", "h3", 12227 // "h4", "h5", "h6", "p", or "pre"." 12228 var formattableBlockNames = ["address", "dd", "div", "dt", "h1", "h2", "h3", "h4", "h5", "h6", "p", "pre", "deleteme"]; 12229 12230 commands.formatblock = { 12231 action: function (value) { 12232 var i; 12233 12234 // "If value begins with a "<" character and ends with a ">" character, 12235 // remove the first and last characters from it." 12236 if (/^<.*>$/.test(value)) { 12237 value = value.slice(1, -1); 12238 } 12239 12240 // "Let value be converted to ASCII lowercase." 12241 value = value.toLowerCase(); 12242 12243 // "If value is not a formattable block name, abort these steps and do 12244 // nothing." 12245 if ($_(formattableBlockNames).indexOf(value) == -1) { 12246 return; 12247 } 12248 12249 // "Block-extend the active range, and let new range be the result." 12250 var newRange = blockExtend(getActiveRange()); 12251 12252 // "Let node list be an empty list of nodes." 12253 // 12254 // "For each node node contained in new range, append node to no
12254de list 12255 // if it is editable, the last member of original node list (if any) is 12256 // not an ancestor of node, node is either a non-list single-line 12257 // container or an allowed child of "p" or a dd or dt, and node is not 12258 // the ancestor of a prohibited paragraph child." 12259 var nodeList = getContainedNodes(newRange, function (node) { 12260 return isEditable(node) && (isNonListSingleLineContainer(node) || isAllowedChild(node, "p") || isHtmlElementInArray(node, ["dd", "dt"])) && !$_(getDescendants(node)).some(isProhibitedParagraphChild); 12261 }); 12262 12263 // "Record the values of node list, and let values be the result." 12264 var values = recordValues(nodeList); 12265 12266 function makeIsEditableElementInSameEditingHostDoesNotContainProhibitedParagraphChildren(node) { 12267 return function (ancestor) { 12268 return (isEditable(ancestor) 12269 && inSameEditingHost(ancestor, node) 12270 && isHtmlElement_obsolete(ancestor, formattableBlockNames) 12271 && !$_(getDescendants(ancestor)).some(isProhibitedParagraphChild)); 12272 }; 12273 } 12274 12275 function makeIsElementWithoutAttributes(value) { 12276 return function (node) { 12277 return isHtmlElement_obsolete(node, value) && !node.attributes.length; 12278 }; 12279 } 12280 12281 function returnFalse() { 12282 return false; 12283 } 12284 12285 function makeCreateElement(value) { 12286 return function () { 12287 return document.createElement(value); 12288 }; 12289 } 12290 12291 // "For each node in node list, while node is the descendant of an 12292 // editable HTML element in the same editing host, whose local name is 12293 // a formattable block name, and which is not the ancestor of a 12294 // prohibited paragraph child, split the parent of the one-node list 12295 // consisting of node." 12296 for (i = 0; i < nodeList.length; i++) { 12297 var node = nodeList[i]; 12298 while ($_(getAncestors(node)).some(makeIsEditableElementInSameEditingHostDoesNotContainProhibitedParagraphChildren(node))) { 12299 splitParent([node], newRange); 12300 } 12301 } 12302 12303 // "Restore the values from values." 12304 restoreValues(values, newRange); 12305 12306 // "While node list is not empty:" 12307 while (nodeList.length) { 12308 var sublist; 12309 12310 // "If the first member of node list is a single-line 12311 // container:" 12312 if (isSingleLineContainer(nodeList[0])) { 12313 // "Let sublist be the children of the first member of node 12314 // list." 12315 sublist = [].slice.call(toArray(nodeList[0].childNodes)); 12316 12317 // "Record the values of sublist, and let values be the 12318 // result." 12319 values = recordValues(sublist); 12320 12321 // "Remove the first member of node list from its parent, 12322 // preserving its descendants." 12323 removePreservingDescendants(nodeList[0], newRange); 12324 12325 // "Restore the values from values." 12326 restoreValues(values, newRange); 12327 12328 // "Remove the first member from node list." 12329 nodeList.shift(); 12330 12331 // "Otherwise:" 12332 } else { 12333 // "Let sublist be an empty list of nodes." 12334 sublist = []; 12335 12336 // "Remove the first member of node list and append it to 12337 // sublist." 12338 sublist.push(nodeList.shift()); 12339 12340 // "While node list is not empty, and the first member of 12341 // node list is the nextSibling of the last member of 12342 // sublist, and the first member of node list is not a 12343 // single-line container, and the last member of sublist is 12344 // not a br, remove the first member of node list and 12345 // append it to sublist." 12346 while (nodeList.length && nodeList[0] == sublist[sublist.length - 1].nextSibling && !isSingleLineContainer(nodeList[0]) && !isNamedHtmlElement(sublist[sublist.length - 1], "BR")) { 12347 sublist.push(nodeList.shift()); 12348 } 12349 } 12350 12351 // "Wrap sublist. If value is "div" or "p", sibling criteria 12352 // returns false; otherwise it returns true for an HTML element 12353 // with local name value and no attributes, and false otherwise. 12354 // New parent instructions return the result of running 12355 // createElement(value) on the context object. Then fix disallowed 12356 // ancestors of the result." 12357 fixDisallowedAncestors(wrap( 12358 sublist, 12359 jQuery.inArray(value, ["div", "p"]) == -1 ? makeIsElementWithoutAttributes(value) : returnFalse, 12360 makeCreateElement(value), 12361 newRange 12362 ), newRange); 12363 } 12364 }, 12365 12366 indeterm: function () { 12367 // "Block-extend the active range, and let new range be the result." 12368 var newRange = blockExtend(getActiveRange()); 12369 12370 // "Let node list be all visible editable nodes that are contained in 12371 // new range and have no children." 12372 var nodeList = getAllContainedNodes(newRange, function (node) { 12373 return isVisible(node) && isEditable(node) && !node.hasChildNodes(); 12374 }); 12375 12376 // "If node list is empty, return false." 12377 if (!nodeList.length) { 12378 return false;
12379 } 12380 12381 // "Let type be null." 12382 var type = null; 12383 12384 // "For each node in node list:" 12385 var i; 12386 for (i = 0; i < nodeList.length; i++) { 12387 var node = nodeList[i]; 12388 12389 // "While node's parent is editable and in the same editing host as 12390 // node, and node is not an HTML element whose local name is a 12391 // formattable block name, set node to its parent." 12392 while (isEditable(node.parentNode) && inSameEditingHost(node, node.parentNode) && !isHtmlElement_obsolete(node, formattableBlockNames)) { 12393 node = node.parentNode; 12394 } 12395 12396 // "Let current type be the empty string." 12397 var currentType = ""; 12398 12399 // "If node is an editable HTML element whose local name is a 12400 // formattable block name, and node is not the ancestor of a 12401 // prohibited paragraph child, set current type to node's local 12402 // name." 12403 if (isEditable(node) && isHtmlElement_obsolete(node, formattableBlockNames) && !$_(getDescendants(node)).some(isProhibitedParagraphChild)) { 12404 currentType = node.tagName; 12405 } 12406 12407 // "If type is null, set type to current type." 12408 if (type === null) { 12409 type = currentType; 12410 12411 // "Otherwise, if type does not equal current type, return true." 12412 } else if (type != currentType) { 12413 return true; 12414 } 12415 } 12416 12417 // "Return false." 12418 return false; 12419 }, 12420 value: function () { 12421 // "Block-extend the active range, and let new range be the result." 12422 var newRange = blockExtend(getActiveRange()); 12423 12424 // "Let node be the first visible editable node that is contained in 12425 // new range and has no children. If there is no such node, return the 12426 // empty string." 12427 var nodes = getAllContainedNodes(newRange, function (node) { 12428 return isVisible(node) && isEditable(node) && !node.hasChildNodes(); 12429 }); 12430 if (!nodes.length) { 12431 return ""; 12432 } 12433 var node = nodes[0]; 12434 12435 // "While node's parent is editable and in the same editing host as 12436 // node, and node is not an HTML element whose local name is a 12437 // formattable block name, set node to its parent." 12438 while (isEditable(node.parentNode) && inSameEditingHost(node, node.parentNode) && !isHtmlElement_obsolete(node, formattableBlockNames)) { 12439 node = node.parentNode; 12440 } 12441 12442 // "If node is an editable HTML element whose local name is a 12443 // formattable block name, and node is not the ancestor of a prohibited 12444 // paragraph child, return node's local name, converted to ASCII 12445 // lowercase." 12446 if (isEditable(node) && isHtmlElement_obsolete(node, formattableBlockNames) && !$_(getDescendants(node)).some(isProhibitedParagraphChild)) { 12447 return node.tagName.toLowerCase(); 12448 } 12449 12450 // "Return the empty string." 12451 return ""; 12452 } 12453 }; 12454 12455 //@} 12456 ///// The forwardDelete command ///// 12457 //@{ 12458 commands.forwarddelete = { 12459 action: function (value, range) { 12460 12461 // "If the active range is not collapsed, delete the contents of the 12462 // active range and abort these steps." 12463 if (!range.collapsed) { 12464 deleteContents(range); 12465 return; 12466 } 12467 12468 // "Canonicalize whitespace at (active range's start node, active 12469 // range's start offset)." 12470 canonicalizeWhitespace(range.startContainer, range.startOffset); 12471 12472 // "Let node and offset be the active range's start node and offset." 12473 var node = range.startContainer; 12474 var offset = range.startOffset; 12475 var isBr = false; 12476 var isHr = false; 12477 12478 // special behaviour: if we are in an empty block-level element (empty, but for the single ending br), we just remove that 12479 // this will fix situations where it was not possible to delete an empty block level element right before a non-editable part 12480 if (isEmptyBlockLevelElement(node)) { 12481 range.startContainer = node.parentElement; 12482 range.startOffset = getIndexInParent(node); 12483 range.endContainer = node.parentElement; 12484 range.endOffset = range.startOffset + 1; 12485 deleteContents(range); 12486 return; 12487 } 12488 12489 // "Repeat the following steps:" 12490 while (true) { 12491 // check whether the next element is a br or hr 12492 // Commented out for unknown reason. 12493 //if (offset < node.childNodes.length) { 12494 // isBr = isHtmlElement_obsolete(node.childNodes[offset], "br") || false; 12495 // isHr = isHtmlElement_obsolete(node.childNodes[offset], "hr") || false; 12496 //} 12497
12498 // "If offset is the length of node and node's nextSibling is an 12499 // editable invisible node, remove node's nextSibling from its 12500 // parent." 12501 if (offset == getNodeLength(node) && isEditable(node.nextSibling) && isInvisible(node.nextSibling)) { 12502 node.parentNode.removeChild(node.nextSibling); 12503 12504 // "Otherwise, if node has a child with index offset and that child 12505 // is an editable invisible node, remove that child from node." 12506 } else if (offset < node.childNodes.length && isEditable(node.childNodes[offset]) && (isInvisible(node.childNodes[offset]) || isBr || isHr)) { 12507 node.removeChild(node.childNodes[offset]); 12508 if (isBr || isHr) { 12509 ensureContainerEditable(node); 12510 range.setStart(node, offset); 12511 range.setEnd(node, offset); 12512 return; 12513 } 12514 12515 // "Otherwise, if node has a child with index offset and that child 12516 // is a collapsed block prop, add one to offset." 12517 } else if (offset < node.childNodes.length && isCollapsedBlockProp(node.childNodes[offset])) { 12518 offset++; 12519 12520 // "Otherwise, if offset is the length of node and node is an 12521 // inline node, or if node is invisible, set offset to one plus the 12522 // index of node, then set node to its parent." 12523 } else if ((offset == getNodeLength(node) && isInlineNode(node)) || isInvisible(node)) { 12524 offset = 1 + getIndexInParent(node); 12525 node = node.parentNode; 12526 12527 // "Otherwise, if node has a child with index offset and that child 12528 // is not a block node or a br or an img, set node to that child, 12529 // then set offset to zero." 12530 } else if (offset < node.childNodes.length && !isBlockNode(node.childNodes[offset]) && !isHtmlElementInArray(node.childNodes[offset], ["br", "img"])) { 12531 node = node.childNodes[offset]; 12532 offset = 0; 12533 12534 // "Otherwise, break from this loop." 12535 } else { 12536 break; 12537 } 12538 } 12539 12540 // collapse whitespace in the node, if it is a text node 12541 canonicalizeWhitespace(range.startContainer, range.startOffset); 12542 12543 // if the next node is an aloha-table we want to delete it 12544 var delBlock = getBlockAtNextPosition(node, offset); 12545 if (delBlock) { 12546 delBlock.parentNode.removeChild(delBlock); 12547 return; 12548 } 12549 12550 var endOffset; 12551 // "If node is a Text node and offset is not node's length:" 12552 if (node.nodeType == Node.TEXT_NODE && offset != getNodeLength(node)) { 12553 // "Call collapse(node, offset) on the Selection." 12554 range.setStart(node, offset); 12555 range.setEnd(node, offset); 12556 12557 // "Let end offset be offset plus one." 12558 endOffset = offset + 1; 12559 12560 // "While end offset is not node's length and the end offsetth 12561 // element of node's data has general category M when interpreted 12562 // as a Unicode code point, add one to end offset." 12563 // 12564 // TODO: Not even going to try handling anything beyond the most 12565 // basic combining marks, since I couldn't find a good list. I 12566 // special-case a few Hebrew diacritics too to test basic coverage 12567 // of non-Latin stuff. 12568 while (endOffset != node.length && /^[\u0300-\u036f\u0591-\u05bd\u05c1\u05c2]$/.test(node.data[endOffset])) { 12569 endOffset++; 12570 } 12571 12572 // "Delete the contents of the range with start (node, offset) and 12573 // end (node, end offset)." 12574 deleteContents(node, offset, node, endOffset); 12575 12576 // "Abort these steps." 12577 return; 12578 } 12579 12580 // "If node has a child with index offset and that child is a br or hr 12581 // or img, call collapse(node, offset) on the Selection. Then delete 12582 // the contents of the range with start (node, offset) and end (node, 12583 // offset + 1) and abort these steps." 12584 if (offset < node.childNodes.length && isHtmlElementInArray(node.childNodes[offset], ["br", "hr", "img"])) { 12585 range.setStart(node, offset); 12586 range.setEnd(node, offset); 12587 deleteContents(node, offset, node, offset + 1); 12588 return; 12589 } 12590 12591 // "If node is an inline node, abort these steps." 12592 if (isInlineNode(node)) { 12593 return; 12594 } 12595 12596 // "Let end node equal node and let end offset equal offset." 12597 var endNode = node; 12598 endOffset = offset; 12599 12600 // "Repeat the following steps:" 12601 while (true) { 12602 // "If end offset is the length of end node, set end offset to one 12603 // plus the index of end node and then set end node to its parent." 12604 if (endOffset == getNodeLength(endNode)) { 12605 endOffset = 1 + getIndexInParent(endNode); 12606 endNode = endNode.parentNode; 12607 12608 // "Otherwise, if end node has a an editable invisible child with 12609 // index end offset, remove it from end node." 12610 } else if (endOffset < endNode.childNodes.length && isEditable(endNode.childNodes[endOffset]) && isInvisible(endNode.childNodes[endOffset])) { 12611 endNode.removeChild(endNode.childNodes[endOffset]); 12612 12613 // "Otherwise, break from this loop." 12614 } else { 12615 break; 12616 } 12617 } 12618 12619 // "If the child of end node with index end offset minus one is a 12620 // table, abort these steps." 12621 if (isNamedHtmlElement(endNode.childNodes[endOffset - 1], "table")) { 12622 return; 12623 } 12624 12625 // "If the child of end node with index end offset is a table:" 12626 if (isNamedHtmlElement(endNode.childNodes[endOffset], "table")) { 12627 // "Call collapse(end node, end offset) on the context object's 12628 // Selection." 12629 range.setStart(endNode, endOffset); 12630 12631 // "Call extend(end node, end offset + 1) on the context object's 12632 // Selection." 12633 range.setEnd(endNode, endOffset + 1); 12634 12635 // "Abort these steps." 12636 return; 12637 } 12638 12639 // "If offset is the length of node, and the child of end node with 12640 // index end offset is an hr or br:" 12641 if (offset == getNodeLength(node) && isHtmlElementInArray(endNode.childNodes[endOffset], ["br", "hr"])) { 12642 // "Call collapse(node, offset) on the Selection." 12643 range.setStart(node, offset); 12644 range.setEnd(node, offset); 12645 12646 // "Delete the contents of the range with end (end node, end 12647 // offset) and end (end node, end offset + 1)." 12648 deleteContents(endNode, endOffset, endNode, endOffset + 1); 12649 12650 // "Abort these steps." 12651 return; 12652 } 12653 12654 // "While end node has a child with index end offset:" 12655 while (endOffset < endNode.childNodes.length) { 12656 // "If end node's child with index end offset is editable and 12657 // invisible, remove it from end node." 12658 if (isEditable(endNode.childNodes[endOffset]) && isInvisible(endNode.childNodes[endOffset])) { 12659 endNode.removeChild(endNode.childNodes[endOffset]); 12660 12661 // "Otherwise, set end node to its child with index end offset and 12662 // set end offset to zero." 12663 } else { 12664 endNode = endNode.childNodes[endOffset]; 12665 endOffset = 0; 12666 } 12667 } 12668 12669 // "Delete the contents of the range with start (node, offset) and end 12670 // (end node, end offset)." 12671 var newRange = deleteContents(node, offset, endNode, endOffset); 12672 range.setStart(newRange.startContainer, newRange.startOffset);
12673 range.setEnd(newRange.endContainer, newRange.endOffset); 12674 } 12675 }; 12676 12677 //@} 12678 ///// The indent command ///// 12679 //@{ 12680 commands.indent = { 12681 action: function () { 12682 // "Let items be a list of all lis that are ancestor containers of the 12683 // active range's start and/or end node." 12684 // 12685 // Has to be in tree order, remember! 12686 var items = []; 12687 var node; 12688 for (node = getActiveRange().endContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) { 12689 if (isNamedHtmlElement(node, "LI")) { 12690 items.unshift(node); 12691 } 12692 } 12693 for (node = getActiveRange().startContainer; node != getActiveRange().commonAncestorContainer; node = node.parentNode) { 12694 if (isNamedHtmlElement(node, "LI")) { 12695 items.unshift(node); 12696 } 12697 } 12698 for (node = getActiveRange().commonAncestorContainer; node; node = node.parentNode) { 12699 if (isNamedHtmlElement(node, "LI")) { 12700 items.unshift(node); 12701 } 12702 } 12703 12704 // "For each item in items, normalize sublists of item." 12705 var i; 12706 for (i = 0; i < items.length; i++) { 12707 normalizeSublists(items[i], getActiveRange()); 12708 } 12709 12710 // "Block-extend the active range, and let new range be the result." 12711 var newRange = blockExtend(getActiveRange()); 12712 12713 // "Let node list be a list of nodes, initially empty." 12714 var nodeList = []; 12715 12716 // "For each node node contained in new range, if node is editable and 12717 // is an allowed child of "div" or "ol" and if the last member of node 12718 // list (if any) is not an ancestor of node, append node to node list." 12719 nodeList = getContainedNodes(newRange, function (node) { 12720 return isEditable(node) && (isAllowedChild(node, "div") || isAllowedChild(node, "ol")); 12721 }); 12722 12723 // "If the first member of node list is an li whose parent is an ol or 12724 // ul, and its previousSibling is an li as well, normalize sublists of 12725 // its previousSibling." 12726 if (nodeList.length && isNamedHtmlElement(nodeList[0], "LI") && isHtmlElementInArray(nodeList[0].parentNode, ["OL", "UL"]) && isNamedHtmlElement(nodeList[0].previousSibling, "LI")) { 12727 normalizeSublists(nodeList[0].previousSibling, newRange); 12728 } 12729 12730 // "While node list is not empty:" 12731 while (nodeList.length) { 12732 // "Let sublist be a list of nodes, initially empty." 12733 var sublist = []; 12734 12735 // "Remove the first member of node list and append it to sublist." 12736 sublist.push(nodeList.shift()); 12737 12738 // "While the first member of node list is the nextSibling of the 12739 // last member of sublist, remove the first member of node list and 12740 // append it to sublist." 12741 while (nodeList.length && nodeList[0] == sublist[sublist.length - 1].nextSibling) { 12742 sublist.push(nodeList.shift()); 12743 } 12744 12745 // "Indent sublist." 12746 indentNodes(sublist, newRange); 12747 } 12748 } 12749 }; 12750 12751 //@} 12752 ///// The insertHorizontalRule command ///// 12753 //@{ 12754 commands.inserthorizontalrule = { 12755 action: function (value, range) { 12756 12757 // "While range's start offset is 0 and its start node's parent is not 12758 // null, set range's start to (parent of start node, index of start 12759 // node)." 12760 while (range.startOffset == 0 && range.startContainer.parentNode) { 12761 range.setStart(range.startContainer.parentNode, getIndexInParent(range.startContainer)); 12762 } 12763 12764 // "While range's end offset is the length of its end node, and its end 12765 // node's parent is not null, set range's end to (parent of end node, 1 12766 // + index of start node)." 12767 while (range.endOffset == getNodeLength(range.endContainer) && range.endContainer.parentNode) { 12768 range.setEnd(range.endContainer.parentNode, 1 + getIndexInParent(range.endContainer)); 12769 } 12770 12771 // "Delete the contents of range, with block merging false." 12772 deleteContents(range, { 12773 blockMerging: false 12774 }); 12775 12776 // "If the active range's start node is neither editable nor an editing 12777 // host, abort these steps." 12778 if (!isEditable(getActiveRange().startContainer) && !isEditingHost(getActiveRange().startContainer)) { 12779 return; 12780 } 12781 12782 // "If the active range's start node is a Text node and its start 12783 // offset is zero, set the active range's start and end to (parent of 12784 // start node, index of start node)." 12785 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE && getActiveRange().startOffset == 0) { 12786 getActiveRange().setStart(getActiveRange().startContainer.parentNode, getIndexInParent(getActiveRange().startContainer)); 12787 getActiveRange().collapse(true); 12788 } 12789 12790 // "If the active range's start node is a Text node and its start 12791 // offset is the length of its start node, set the active range's start 12792 // and end to (parent of start node, 1 + index of start node)." 12793 if (getActiveRange().startContainer.nodeType == Node.TEXT_NODE && getActiveRange().startOffset == getNodeLength(getActiveRange().startContainer)) { 12794 getActiveRange().setStart(getActiveRange().startContainer.parentNode, 1 + getIndexInParent(getActiveRange().startContainer)); 12795 getActiveRange().collapse(true); 12796 } 12797 12798 // "Let hr be the result of calling createElement("hr") on the 12799 // context object." 12800 var hr = document.createElement("hr"); 12801
12802 // "Run insertNode(hr) on the range." 12803 range.insertNode(hr); 12804 12805 // "Fix disallowed ancestors of hr." 12806 fixDisallowedAncestors(hr, range); 12807 12808 // "Run collapse() on the Selection, with first argument equal to the 12809 // parent of hr and the second argument equal to one plus the index of 12810 // hr." 12811 // 12812 // Not everyone actually supports collapse(), so we do it manually 12813 // instead. Also, we need to modify the actual range we're given as 12814 // well, for the sake of autoimplementation.html's range-filling-in. 12815 range.setStart(hr.parentNode, 1 + getIndexInParent(hr)); 12816 range.setEnd(hr.parentNode, 1 + getIndexInParent(hr)); 12817 _core.rangy.getSelection().removeAllRanges(); 12818 _core.rangy.getSelection().addRange(range); 12819 } 12820 }; 12821 12822 /** 12823 * Publish a message for the links that will be inserted at paste. 12824 * @param {DocumentFragment} frag 12825 */ 12826 function publishPastedLinks(frag) { 12827 var children = frag.childNodes; 12828 var links; 12829 var c; 12830 var i; 12831 var len; 12832 12833 for (c = 0; c < children.length; c++) { 12834 links = jQuery(children[c]).find('a'); 12835 for (i = 0, len = links.length; i < len; i++) { 12836 console.log('link pasted'); 12837 } 12838 } 12839 } 12840 12841 //@} 12842 ///// The insertHTML command ///// 12843 //@{ 12844 commands.inserthtml = { 12845 action: function (raw, range) { 12846 12847 var editable = getEditingHostOf(range.commonAncestorContainer); 12848 var value = editable ? ContentRules.applyRules(raw, editable) : raw; 12849 12850 // "Delete the contents of the active range." 12851 deleteContents(range); 12852 12853 // "If the active range's start node is neither editable nor an editing 12854 // host, abort these steps." 12855 if (!isEditable(range.startContainer) && !isEditingHost(range.startContainer)) { 12856 return; 12857 } 12858 12859 // "Let frag be the result of calling createContextualFragment(value) 12860 // on the active range." 12861 var frag = range.createContextualFragment(value); 12862 12863 publishPastedLinks(frag); 12864 12865 // "Let last child be the lastChild of frag." 12866 var lastChild = frag.lastChild; 12867 12868 // "If last child is null, abort these steps." 12869 if (!lastChild) { 12870 return; 12871 } 12872 12873 // "Let descendants be all descendants of frag." 12874 var descendants = getDescendants(frag); 12875 12876 // "If the active range's start node is a block node:" 12877 if (isBlockNode(range.startContainer)) { 12878 // "Let collapsed block props be all editable collapsed block prop 12879 // children of the active range's start node that have index 12880 // greater than or equal to the active range's start offset." 12881 // 12882 // "For each node in collapsed block props, remove node from its 12883 // parent." 12884 $_(range.startContainer.childNodes).filter(function (node, range) { 12885 return isEditable(node) && isCollapsedBlockProp(node) && getIndexInParent(node) >= range.startOffset; 12886 }, true).forEach(function (node) { 12887 node.parentNode.removeChild(node); 12888 }); 12889 } 12890 12891 // "Call insertNode(frag) on the active range." 12892 range.insertNode(frag); 12893 12894 // "If the active range's start node is a block node with no visible 12895 // children, call createElement("br") on the context object and append 12896 // the result as the last child of the active range's start node." 12897 if (isBlockNode(range.startContainer)) { 12898 ensureContainerEditable(range.startContainer); 12899 } 12900 12901 // "Call collapse() on the context object's Selection, with last 12902 // child's parent as the first argument and one plus its index as the 12903 // second." 12904 range.setStart(lastChild.parentNode, 1 + getIndexInParent(lastChild)); 12905 range.setEnd(lastChild.parentNode, 1 + getIndexInParent(lastChild)); 12906 12907 // "Fix disallowed ancestors of each member of descendants." 12908 var i; 12909 for (i = 0;
12909 i < descendants.length; i++) { 12910 fixDisallowedAncestors(descendants[i], range); 12911 } 12912 12913 setActiveRange(range); 12914 } 12915 }; 12916 12917 //@} 12918 ///// The insertImage command ///// 12919 //@{ 12920 commands.insertimage = { 12921 action: function (value) { 12922 // "If value is the empty string, abort these steps and do nothing." 12923 if (value === "") { 12924 return; 12925 } 12926 12927 // "Let range be the active range." 12928 var range = getActiveRange(); 12929 12930 // "Delete the contents of range, with strip wrappers false." 12931 deleteContents(range, { 12932 stripWrappers: false 12933 }); 12934 12935 // "If the active range's start node is neither editable nor an editing 12936 // host, abort these steps." 12937 if (!isEditable(getActiveRange().startContainer) && !isEditingHost(getActiveRange().startContainer)) { 12938 return; 12939 } 12940 12941 // "If range's start node is a block node whose sole child is a br, and 12942 // its start offset is 0, remove its start node's child from it." 12943 if (isBlockNode(range.startContainer) && range.startContainer.childNodes.length == 1 && isNamedHtmlElement(range.startContainer.firstChild, "br") && range.startOffset == 0) { 12944 range.startContainer.removeChild(range.startContainer.firstChild); 12945 } 12946 12947 // "Let img be the result of calling createElement("img") on the 12948 // context object." 12949 var img = document.createElement("img"); 12950 12951 // "Run setAttribute("src", value) on img." 12952 img.setAttribute("src", value); 12953 12954 // "Run insertNode(img) on the range." 12955 range.insertNode(img); 12956 12957 // "Run collapse() on the Selection, with first argument equal to the 12958 // parent of img and the second argument equal to one plus the index of 12959 // img." 12960 // 12961 // Not everyone actually supports collapse(), so we do it manually 12962 // instead. Also, we need to modify the actual range we're given as 12963 // well, for the sake of autoimplementation.html's range-filling-in. 12964 range.setStart(img.parentNode, 1 + getIndexInParent(img)); 12965 range.setEnd(img.parentNode, 1 + getIndexInParent(img)); 12966 _core.rangy.getSelection().removeAllRanges(); 12967 _core.rangy.getSelection().addRange(range); 12968 12969 // IE adds width and height attributes for some reason, so remove those 12970 // to actually do what the spec says. 12971 img.removeAttribute("width"); 12972 img.removeAttribute("height"); 12973 } 12974 }; 12975 12976 //@} 12977 ///// The insertLineBreak command ///// 12978 //@{ 12979 commands.insertlinebreak = { 12980 action: function (value, range) { 12981 // "Delete the contents of the active range, with strip wrappers false." 12982 deleteContents(range, { 12983 stripWrappers: false 12984 }); 12985 12986 // "If the active range's start node is neither editable nor an editing 12987 // host, abort these steps." 12988 if (!isEditable(range.startContainer) && !isEditingHost(range.startContainer)) { 12989 return; 12990 } 12991 12992 // "If the active range's start node is an Element, and "br" is not an 12993 // allowed child of it, abort these steps." 12994 if (range.startContainer.nodeType == Node.ELEMENT_NODE && !isAllowedChild("br", range.startContainer)) { 12995 return; 12996 } 12997 12998 // "If the active range's start node is not an Element, and "br" is not 12999 // an allowed child of the active range's start node's parent, abort 13000 // these steps." 13001 if (range.startContainer.nodeType != Node.ELEMENT_NODE && !isAllowedChild("br", range.startContainer.parentNode)) { 13002 return; 13003 } 13004 13005 // "If the active range's start node is a Text node and its start 13006 // offset is zero, call collapse() on the context object's Selection, 13007 // with first argument equal to the active range's start node's parent 13008 // and second argument equal to the active range's start node's index." 13009 var newNode, newOffset; 13010 if (range.startContainer.nodeType == Node.TEXT_NODE && range.startOffset == 0) { 13011 newNode = range.startContainer.parentNode; 13012 newOffset = getIndexInParent(range.startContainer); 13013 _core.rangy.getSelection().collapse(newNode, newOffset); 13014 range.setStart(newNode, newOffset); 13015 range.setEnd(newNode, newOffset); 13016 } 13017 13018 // "If the active range's start node is a Text node and its start 13019 // offset is the length of its start node, call collapse() on the 13020 // context object's Selection, with first argument equal to the active 13021 // range's start node's parent and second argument equal to one plus 13022 // the active range's start node's index." 13023 if (range.startContainer.nodeType == Node.TEXT_NODE && range.startOffset == getNodeLength(range.startContainer)) { 13024 newNode = range.startContainer.parentNode; 13025 newOffset = 1 + getIndexInParent(range.startContainer); 13026 _core.rangy.getSelection().collapse(newNode, newOffset); 13027 range.setStart(newNode, newOffset); 13028 range.setEnd(newNode, newOffset); 13029 } 13030 13031 // "Let br be the result of calling createElement("br") on the context 13032 // object." 13033 var br = document.createElement("br"); 13034 13035 // "Call insertNode(br) on the active range."
13036 range.insertNode(br); 13037 13038 // "Call collapse() on the context object's Selection, with br's parent 13039 // as the first argument and one plus br's index as the second 13040 // argument." 13041 _core.rangy.getSelection().collapse(br.parentNode, 1 + getIndexInParent(br)); 13042 range.setStart(br.parentNode, 1 + getIndexInParent(br)); 13043 range.setEnd(br.parentNode, 1 + getIndexInParent(br)); 13044 13045 // "If br is a collapsed line break, call createElement("br") on the 13046 // context object and let extra br be the result, then call 13047 // insertNode(extra br) on the active range." 13048 if (isCollapsedLineBreak(br)) { 13049 // TODO 13050 range.insertNode(createEndBreak()); 13051 13052 // Compensate for nonstandard implementations of insertNode 13053 _core.rangy.getSelection().collapse(br.parentNode, 1 + getIndexInParent(br)); 13054 range.setStart(br.parentNode, 1 + getIndexInParent(br)); 13055 range.setEnd(br.parentNode, 1 + getIndexInParent(br)); 13056 } 13057 13058 } 13059 }; 13060 13061 //@} 13062 ///// The insertOrderedList command ///// 13063 //@{ 13064 commands.insertorderedlist = { 13065 // "Toggle lists with tag name "ol"." 13066 action: function (value, range) { 13067 toggleLists("ol", range); 13068 }, 13069 // "True if the selection's list state is "mixed" or "mixed ol", false 13070 // otherwise." 13071 indeterm: function () { 13072 return (/^mixed( ol)?$/).test(getSelectionListState()); 13073 }, 13074 // "True if the selection's list state is "ol", false otherwise." 13075 state: function () { 13076 return getSelectionListState() == "ol"; 13077 } 13078 }; 13079 13080 var listRelatedElements = { 13081 "LI": true, 13082 "DT": true, 13083 "DD": true 13084 }; 13085 13086 //@} 13087 ///// The insertParagraph command ///// 13088 //@{ 13089 commands.insertparagraph = { 13090 action: function (value, range) { 13091 var i; 13092 13093 // "Delete the contents of the active range." 13094 deleteContents(range); 13095 13096 // clean lists in the editing host, this will remove any whitespace nodes around lists 13097 // because the following algorithm is not prepared to deal with them 13098 cleanLists(getEditingHostOf(range.startContainer), range); 13099 13100 // "If the active range's start node is neither editable nor an editing 13101 // host, abort these steps." 13102 if (!isEditable(range.startContainer) && !isEditingHost(range.startContainer)) { 13103 return; 13104 } 13105 13106 var editable = getEditingHostOf(range.commonAncestorContainer); 13107 13108 if (!editable) { 13109 return; 13110 } 13111 13112 // "Let node and offset be the active range's start node and offset." 13113 var node = range.startContainer; 13114 var offset = range.startOffset; 13115 13116 // "If node is a Text node, and offset is neither 0 nor the length of 13117 // node, call splitText(offset) on node." 13118 if (node.nodeType == Node.TEXT_NODE && offset != 0 && offset != getNodeLength(node)) { 13119 splitText(node, offset); 13120 } 13121 13122 // "If node is a Text node and offset is its length, set offset to one 13123 // plus the index of node, then set node to its parent." 13124 if (node.nodeType == Node.TEXT_NODE && offset == getNodeLength(node)) { 13125 offset = 1 + getIndexInParent(node); 13126 node = node.parentNode; 13127 } 13128 13129 // "If node is a Text or Comment node, set offset to the index of node, 13130 // then set node to its parent." 13131 if (node.nodeType == Node.TEXT_NODE || node.nodeType == Node.COMMENT_NODE) { 13132 offset = getIndexInParent(node); 13133 node = node.parentNode; 13134 } 13135 13136 // "Call collapse(node, offset) on the context object's Selection." 13137 _core.rangy.getSelection().collapse(node, offset); 13138 range.setStart(node, offset); 13139 range.setEnd(node, offset); 13140 13141 // "Let container equal node." 13142 var container = node; 13143 13144 // "While container is not a single-line container, and container's 13145 // parent is editable and in the same editing host as node, set 13146 // container to its parent." 13147 while (!isSingleLineContainer(container) && isEditable(container.parentNode) && inSameEditingHost(node, container.parentNode)) { 13148 container = container.parentNode; 13149 } 13150 13151 // "If container is not editable or not in the same editing host as 13152 // node or is not a single-line container:" 13153 if (!isEditable(container) || !inSameEditingHost(container, node) || !isSingleLineContainer(container)) { 13154 editable = getEditingHostOf(container); 13155 13156 if (!editable || !ContentRules.isAllowed(editable, defaultSingleLineContainerName)) { 13157 return; 13158 } 13159 13160 // "Let tag be the default single-line container name." 13161 var tag = defaultSingleLineContainerName; 13162 13163 // "Block-extend the active range, and let new range be the 13164 // result." 13165 var newRange = blockExtend(range); 13166 13167 // "Let node list be a list of nodes, initially empty." 13168 // 13169 // "Append to node list the first node in tree order that is 13170 // contained in new range and is an allowed child of "p", if any." 13171 var nodeList = getContainedNodes(newRange, function (node) { 13172 return isAllowedChild(node, "p"); 13173 }).slice(0, 1); 13174 13175 // "If node list is empty:" 13176 if (!nodeList.length) { 13177 // "If tag is not an allowed child of the active range's start 13178 // node, abort these steps." 13179 if (!isAllowedChild(tag, range.startContainer)) { 13180 return; 13181 } 13182 13183 // "Set container to the result of calling createElement(tag) 13184 // on the context object." 13185 container = document.createElement(tag); 13186 13187 // "Call insertNode(container) on the active range."
13188 range.insertNode(container); 13189 13190 // "Call createElement("br") on the context object, and append 13191 // the result as the last child of container." 13192 // TODO not always 13193 container.appendChild(createEndBreak()); 13194 13195 // "Call collapse(container, 0) on the context object's 13196 // Selection." 13197 // TODO: remove selection from command 13198 _core.rangy.getSelection().collapse(container, 0); 13199 range.setStart(container, 0); 13200 range.setEnd(container, 0); 13201 13202 // "Abort these steps." 13203 return; 13204 } 13205 13206 // "While the nextSibling of the last member of node list is not 13207 // null and is an allowed child of "p", append it to node list." 13208 while (nodeList[nodeList.length - 1].nextSibling && isAllowedChild(nodeList[nodeList.length - 1].nextSibling, "p")) { 13209 nodeList.push(nodeList[nodeList.length - 1].nextSibling); 13210 } 13211 13212 // "Wrap node list, with sibling criteria returning false and new 13213 // parent instructions returning the result of calling 13214 // createElement(tag) on the context object. Set container to the 13215 // result." 13216 container = wrap( 13217 nodeList, 13218 function () { 13219 return false; 13220 }, 13221 function () { 13222 return document.createElement(tag); 13223 }, 13224 range 13225 ); 13226 } 13227 13228 // If no container has been set yet, it is not possible to insert a paragraph at this position; 13229 // the following steps are skipped in order to prevent critical errors from occurring; 13230 if (!container) { 13231 return; 13232 } 13233 13234 // "If container's local name is "address", "listing", or "pre":" 13235 var oldHeight, newHeight; 13236 if (container.tagName == "ADDRESS" || container.tagName == "LISTING" || container.tagName == "PRE") { 13237 13238 if (!ContentRules.isAllowed(editable, 'br')) { 13239 return; 13240 } 13241 13242 // "Let br be the result of calling createElement("br") on the 13243 // context object." 13244 var br = document.createElement("br"); 13245 13246 // remember the old height 13247 oldHeight = container.offsetHeight; 13248 13249 // "Call insertNode(br) on the active range." 13250 range.insertNode(br); 13251 13252 // determine the new height 13253 newHeight = container.offsetHeight; 13254 13255 // "Call collapse(node, offset + 1) on the context object's 13256 // Selection." 13257 _core.rangy.getSelection().collapse(node, offset + 1); 13258 range.setStart(node, offset + 1); 13259 range.setEnd(node, offset + 1); 13260 13261 // "If br is the last descendant of container, let br be the result 13262 // of calling createElement("br") on the context object, then call 13263 // insertNode(br) on the active range." (Fix: only do this, if the container height did not change by inserting a single <br/>) 13264 // 13265 // Work around browser bugs: some browsers select the 13266 // newly-inserted node, not per spec. 13267 if (oldHeight == newHeight && !isDescendant(nextNode(br), container)) { 13268 // TODO check 13269 range.insertNode(createEndBreak()); 13270 _core.rangy.getSelection().collapse(node, offset + 1); 13271 range.setEnd(node, offset + 1); 13272 } 13273 13274 // "Abort these steps." 13275 return; 13276 } 13277 13278 // "If container's local name is "li", "dt", or "dd"; and either it has 13279 // no children or it has a single child and that child is a br:" 13280 if (listRelatedElements[container.tagName] && (!container.hasChildNodes() || (container.childNodes.length == 1 && isNamedHtmlElement(container.firstChild, "br")))) { 13281 // "Split the parent of the one-node list consisting of container." 13282 splitParent([container], range); 13283 13284 // "If container has no children, call createElement("br") on the 13285 // context object and append the result as the last child of 13286 // container." 13287 // only do this, if inserting the br does NOT modify the offset height of the container 13288 // if (!container.hasChildNodes()) { 13289 // var oldHeight = container.offsetHeight, endBr = createEndBreak(); 13290 // container.appendChild(endBr); 13291 // if (container.offsetHeight !== oldHeight) { 13292 // container.removeChild(endBr); 13293 // } 13294 // } 13295 13296 // "If container is a dd or dt, and it is not an allowed child of 13297 // any of its ancestors in the same editing host, set the tag name 13298 // of container to the default single-line container name and let 13299 // container be the result." 13300 if (isHtmlElementInArray(container, ["dd", "dt"]) && $_(getAncestors(container)).every(function (ancestor) { return !inSameEditingHost(container, ancestor) || !isAllowedChild(container, ancestor); })) { 13301 container = setTagName(container, defaultSingleLineContainerName, range); 13302 } 13303 13304 // "Fix disallowed ancestors of container." 13305 fixDisallowedAncestors(container, range); 13306 13307 // fix invalid nested lists 13308 if (isNamedHtmlElement(container, 'li') && isNamedHtmlElement(container.nextSibling, "li") && isHtmlElementInArray(container.nextSibling.firstChild, ["ol", "ul"])) { 13309 // we found a li containing only a br followed by a li containing a list as first element: merge the two li's 13310 var listParent = container.nextSibling, 13311 length = container.nextSibling.childNodes.length; 13312 for (i = 0; i < length; i++) { 13313 // we always move the first child into the container 13314 container.appendChild(listParent.childNodes[0]); 13315 } 13316 listParent.parentNode.removeChild(listParent); 13317 } 13318 13319 // "Abort these steps." 13320 return; 13321 } 13322 13323 // special behaviour when pressing enter in the last empty paragraph, that is nested in a blockquote 13324 if (isNamedHtmlElement(container, "p") 13325 && isNamedHtmlElement(container.parentNode, "blockquote") 13326 && !container.nextSibling 13327 && (!container.hasChildNodes() 13328 || (container.childNodes.length === 1 13329 && isNamedHtmlElement(container.firstChild, "br")))) { 13330 jQuery(container.parentNode).after(container); 13331 return; 13332 } 13333 13334 // "Let new line range be a new range whose start is the same as 13335 // the active range's, and whose end is (container, length of 13336 // container)." 13337 var newLineRange = _core.rangy.createRange(); 13338 newLineRange.setStart(range.startContainer, range.startOffset); 13339 newLineRange.setEnd(container, getNodeLength(container)); 13340 13341 // "While new line range's start offset is zero and its start node is 13342 // not container, set its start to (parent of start node, index of 13343 // start node)." 13344 while (newLineRange.startOffset == 0 && newLineRange.startContainer != container) { 13345 newLineRange.setStart(newLineRange.startContainer.parentNode, getIndexInParent(newLineRange.startContainer)); 13346 } 13347 13348 // "While new line range's start offset is the length of its start node 13349 // and its start node is not container, set its start to (parent of 13350 // start node, 1 + index of start node)." 13351 while (newLineRange.startOffset == getNodeLength(newLineRange.startContainer) && newLineRange.startContainer != container) { 13352 newLineRange.setStart(newLineRange.startContainer.parentNode, 1 + getIndexInParent(newLineRange.startContainer)); 13353 } 13354 13355 // "Let end of line be true if new line range contains either nothing 13356 // or a single br, and false otherwise." 13357 var containedInNewLineRange = getContainedNodes(newLineRange); 13358 var endOfLine = !containedInNewLineRange.length || (containedInNewLineRange.length == 1 && isNamedHtmlElement(containedInNewLineRange[0], "br")); 13359 13360 // "If the local name of container is "h1", "h2", "h3", "h4", "h5", or 13361 // "h6", and end of line is true, let new container name be the default 13362 // single-line container name." 13363 var newContainerName; 13364 if (/^H[1-6]$/.test(container.tagName) && endOfLine) { 13365 newContainerName = defaultSingleLineContainerName; 13366 13367 // "Otherwise, if the local name of container is "dt" and end of line 13368 // is true, let new container name be "dd"." 13369 } else if (container.tagName == "DT" && endOfLine) { 13370 newContainerName = "dd"; 13371 13372 // "Otherwise, if the local name of container is "dd" and end of line 13373 // is true, let new container name be "dt"." 13374 } else if (container.tagName == "DD" && endOfLine) { 13375 newContainerName = "dt"; 13376 13377 // "Otherwise, let new container name be the local name of container." 13378 } else { 13379 newContainerName = container.tagName.toLowerCase(); 13380 } 13381 13382 if (!listRelatedElements[newContainerName.toUpperCase()] 13383 && !ContentRules.isAllowed(editable, newContainerName)) { 13384 return; 13385 } 13386 13387 // "Let new container be the result of calling createElement(new 13388 // container name) on the context object." 13389 var newContainer = document.createElement(newContainerName); 13390 13391 // "Copy all non empty attributes of the container to new container." 13392 copyAttributes(container, newContainer); 13393 13394 // "If new container has an id attribute, unset it." 13395 newContainer.removeAttribute("id"); 13396 13397 // "Insert new container into the parent of container immediately after 13398 // container." 13399 container.parentNode.insertBefore(newContainer, container.nextSibling); 13400 13401 // "Let contained nodes be all nodes contained in new line range." 13402 var containedNodes = getAllContainedNodes(newLineRange); 13403 13404 // "Let frag be the result of calling extractContents() on new line 13405 // range." 13406 var frag = newLineRange.extractContents(); 13407 13408 // "Unset the id attribute (if any) of each Element descendant of frag 13409 // that is not in contained nodes." 13410 var descendants = getDescendants(frag); 13411 for (i = 0;
13411 i < descendants.length; i++) { 13412 if (descendants[i].nodeType == Node.ELEMENT_NODE && $_(containedNodes).indexOf(descendants[i]) == -1) { 13413 descendants[i].removeAttribute("id"); 13414 } 13415 } 13416 13417 var fragChildren = [], 13418 fragChild = frag.firstChild; 13419 if (fragChild) { 13420 do { 13421 if (!isWhitespaceNode(fragChild)) { 13422 fragChildren.push(fragChild); 13423 } 13424 } while (null != (fragChild = fragChild.nextSibling)); 13425 } 13426 13427 // if newContainer is a li and frag contains only a list, we add a br in the li (but only if the height would not change) 13428 if (isNamedHtmlElement(newContainer, 'li') && fragChildren.length && isHtmlElementInArray(fragChildren[0], ["ul", "ol"])) { 13429 oldHeight = newContainer.offsetHeight; 13430 var endBr = createEndBreak(); 13431 newContainer.appendChild(endBr); 13432 newHeight = newContainer.offsetHeight; 13433 if (oldHeight !== newHeight) { 13434 newContainer.removeChild(endBr); 13435 } 13436 } 13437 13438 // "Call appendChild(frag) on new container." 13439 newContainer.appendChild(frag); 13440 13441 // "If container has no visible children, call createElement("br") on 13442 // the context object, and append the result as the last child of 13443 // container." 13444 ensureContainerEditable(container); 13445 13446 // "If new container has no visible children, call createElement("br") 13447 // on the context object, and append the result as the last child of 13448 // new container." 13449 ensureContainerEditable(newContainer); 13450 13451 // "Call collapse(new container, 0) on the context object's Selection." 13452 _core.rangy.getSelection().collapse(newContainer, 0); 13453 range.setStart(newContainer, 0); 13454 range.setEnd(newContainer, 0); 13455 } 13456 }; 13457 13458 //@} 13459 ///// The insertText command ///// 13460 //@{ 13461 commands.inserttext = { 13462 action: function (value, range) { 13463 var i; 13464 13465 // "Delete the contents of the active range, with strip wrappers 13466 // false." 13467 deleteContents(range, { 13468 stripWrappers: false 13469 }); 13470 13471 // "If the active range's start node is neither editable nor an editing 13472 // host, abort these steps." 13473 if (!isEditable(range.startContainer) && !isEditingHost(range.startContainer)) { 13474 return; 13475 } 13476 13477 // "If value's length is greater than one:" 13478 if (value.length > 1) { 13479 // "For each element el in value, take the action for the 13480 // insertText command, with value equal to el." 13481 for (i = 0; i < value.length; i++) { 13482 commands.inserttext.action(value[i], range); 13483 } 13484 13485 // "Abort these steps." 13486 return; 13487 } 13488 13489 // "If value is the empty string, abort these steps." 13490 if (value == "") { 13491 return; 13492 } 13493 13494 // "If value is a newline (U+00A0), take the action for the 13495 // insertParagraph command and abort these steps." 13496 if (value == "\n") { 13497 commands.insertparagraph.action('', range); 13498 return; 13499 } 13500 13501 // "Let node and offset be the active range's start node and offset." 13502 var node = range.startContainer; 13503 var offset = range.startOffset; 13504 13505 // "If node has a child whose index is offset â 1, and that child is a 13506 // Text node, set node to that child, then set offset to node's 13507 // length." 13508 if (0 <= offset - 1 && offset - 1 < node.childNodes.length && node.childNodes[offset - 1].nodeType == Node.TEXT_NODE) { 13509 node = node.childNodes[offset - 1]; 13510 offset = getNodeLength(node); 13511 } 13512 13513 // "If node has a child whose index is offset, and that child is a Text 13514 // node, set node to that child, then set offset to zero." 13515 if (0 <= offset && offset < node.childNodes.length && node.childNodes[offset].nodeType == Node.TEXT_NODE) { 13516 node = node.childNodes[offset]; 13517 offset = 0; 13518 } 13519 13520 // "If value is a space (U+0020), and either node is an Element whose 13521 // resolved value for "white-space" is neither "pre" nor "pre-wrap" or 13522 // node is not an Element but its parent is an Element whose resolved 13523 // value for "white-space" is neither "pre" nor "pre-wrap", set value 13524 // to a non-breaking space (U+00A0)." 13525 var refElement = node.nodeType == Node.ELEMENT_NODE ? node : node.parentNode; 13526 if (value == " " && refElement.nodeType == Node.ELEMENT_NODE && jQuery.inArray($_.getComputedStyle(refElement).whiteSpace, ["pre", "pre-wrap"]) == -1) { 13527 value = "\xa0"; 13528 } 13529 13530 // "Record current overrides, and let overrides be the result." 13531 var overrides = recordCurrentOverrides(range); 13532 13533 // "If node is a Text node:" 13534 if (node.nodeType == Node.TEXT_NODE) { 13535 // "Call insertData(offset, value) on node." 13536 node.insertData(offset, value); 13537 13538 // "Call collapse(node, offset) on the context object's Selection." 13539 _core.rangy.getSelection().collapse(node, offset); 13540 range.setStart(node, offset); 13541 13542 // "Call extend(node, offset + 1) on the context object's 13543 // Selection." 13544 _core.rangy.getSelection().extend(node, offset + 1); 13545 range.setEnd(node, offset + 1); 13546 13547 // "Otherwise:" 13548 } else { 13549 // "If node has only one child, which is a collapsed line break, 13550 // remove its child from it." 13551 // 13552 // FIXME: IE incorrectly returns false here instead of true 13553 // sometimes? 13554 if (node.childNodes.length == 1 && isCollapsedLineBreak(node.firstChild)) { 13555 node.removeChild(node.firstChild); 13556 } 13557 13558 // "Let text be the result of calling createTextNode(value) on the 13559 // context object." 13560 var text = document.createTextNode(value); 13561 13562 // "Call insertNode(text) on the active range."
13563 range.insertNode(text); 13564 13565 // "Call collapse(text, 0) on the context object's Selection." 13566 _core.rangy.getSelection().collapse(text, 0); 13567 range.setStart(text, 0); 13568 13569 // "Call extend(text, 1) on the context object's Selection." 13570 _core.rangy.getSelection().extend(text, 1); 13571 range.setEnd(text, 1); 13572 } 13573 13574 // "Restore states and values from overrides." 13575 restoreStatesAndValues(overrides, range); 13576 13577 // "Canonicalize whitespace at the active range's start." 13578 canonicalizeWhitespace(range.startContainer, range.startOffset); 13579 13580 // "Canonicalize whitespace at the active range's end." 13581 canonicalizeWhitespace(range.endContainer, range.endOffset); 13582 13583 // "Call collapseToEnd() on the context object's Selection." 13584 _core.rangy.getSelection().collapseToEnd(); 13585 range.collapse(false); 13586 } 13587 }; 13588 13589 //@} 13590 ///// The insertUnorderedList command ///// 13591 //@{ 13592 commands.insertunorderedlist = { 13593 // "Toggle lists with tag name "ul"." 13594 action: function (value, range) { 13595 toggleLists("ul", range); 13596 }, 13597 // "True if the selection's list state is "mixed" or "mixed ul", false 13598 // otherwise." 13599 indeterm: function () { 13600 return (/^mixed( ul)?$/).test(getSelectionListState()); 13601 }, 13602 // "True if the selection's list state is "ul", false otherwise." 13603 state: function () { 13604 return getSelectionListState() == "ul"; 13605 } 13606 }; 13607 13608 //@} 13609 ///// The justifyCenter command ///// 13610 //@{ 13611 commands.justifycenter = { 13612 // "Justify the selection with alignment "center"." 13613 action: function (value, range) { 13614 justifySelection("center", range); 13615 }, 13616 indeterm: function () { 13617 // "Block-extend the active range. Return true if among visible 13618 // editable nodes that are contained in the result and have no 13619 // children, at least one has alignment value "center" and at least one 13620 // does not. Otherwise return false." 13621 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) { 13622 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 13623 }); 13624 return $_(nodes).some(function (node) { return getAlignmentValue(node) == "center"; }) 13625 && $_(nodes).some(function (node) { return getAlignmentValue(node) != "center"; }); 13626 }, 13627 state: function () { 13628 // "Block-extend the active range. Return true if there is at least one 13629 // visible editable node that is contained in the result and has no 13630 // children, and all such nodes have alignment value "center". 13631 // Otherwise return false." 13632 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) { 13633 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 13634 }); 13635 return nodes.length && $_(nodes).every(function (node) { 13636 return getAlignmentValue(node) == "center"; 13637 }); 13638 }, 13639 value: function () { 13640 // "Block-extend the active range, and return the alignment value of 13641 // the first visible editable node that is contained in the result and 13642 // has no children. If there is no such node, return "left"." 13643 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) { 13644 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 13645 }); 13646 if (nodes.length) { 13647 return getAlignmentValue(nodes[0]); 13648 } 13649 return "left"; 13650 } 13651 }; 13652 13653 //@} 13654 ///// The justifyFull command ///// 13655 //@{ 13656 commands.justifyfull = { 13657 // "Justify the selection with alignment "justify"." 13658 action: function (value, range) { 13659 justifySelection("justify", range); 13660 }, 13661 indeterm: function () { 13662 // "Block-extend the active range. Return true if among visible 13663 // editable nodes that are contained in the result and have no 13664 // children, at least one has alignment value "justify" and at least 13665 // one does not. Otherwise return false." 13666 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) { 13667 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 13668 }); 13669 return $_(nodes).some(function (node) { return getAlignmentValue(node) == "justify"; }) 13670 && $_(nodes).some(function (node) { return getAlignmentValue(node) != "justify"; }); 13671 }, 13672 state: function () { 13673 // "Block-extend the active range. Return true if there is at least one 13674 // visible editable node that is contained in the result and has no 13675 // children, and all such nodes have alignment value "justify". 13676 // Otherwise return false." 13677 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) { 13678 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 13679 }); 13680 return nodes.length && $_(nodes).every(function (node) { 13681 return getAlignmentValue(node) == "justify"; 13682 }); 13683 }, 13684 value: function () { 13685 // "Block-extend the active range, and return the alignment value of 13686 // the first visible editable node that is contained in the result and 13687 // has no children. If there is no such node, return "left"." 13688 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) { 13689 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 13690 }); 13691 if (nodes.length) { 13692 return getAlignmentValue(nodes[0]); 13693 } 13694 return "left"; 13695 } 13696 }; 13697 13698 //@} 13699 ///// The justifyLeft command ///// 13700 //@{ 13701 commands.justifyleft = { 13702 // "Justify the selection with alignment "left"." 13703 action: function (value, range) { 13704 justifySelection("left", range); 13705 }, 13706 indeterm: function () { 13707 // "Block-extend the active range. Return true if among visible 13708 // editable nodes that are contained in the result and have no 13709 // children, at least one has alignment value "left" and at least one 13710 // does not. Otherwise return false." 13711 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) { 13712 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 13713 }); 13714 return $_(nodes).some(function (node) { return getAlignmentValue(node) == "left"; }) 13715 && $_(nodes).some(function (node) { return getAlignmentValue(node) != "left"; }); 13716 }, 13717 state: function () { 13718 // "Block-extend the active range. Return true if there is at least one 13719 // visible editable node that is contained in the result and has no 13720 // children, and all such nodes have alignment value "left". Otherwise 13721 // return false." 13722 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) { 13723 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 13724 }); 13725 return nodes.length && $_(nodes).every(function (node) { 13726 return getAlignmentValue(node) == "left"; 13727 }); 13728 }, 13729 value: function () { 13730 // "Block-extend the active range, and return the alignment value of 13731 // the first visible editable node that is contained in the result and 13732 // has no children. If there is no such node, return "left"." 13733 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) { 13734 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 13735 }); 13736 if (nodes.length) { 13737 return getAlignmentValue(nodes[0]); 13738 } 13739 return "left"; 13740 } 13741 }; 13742 13743 //@} 13744 ///// The justifyRight command ///// 13745 //@{ 13746 commands.justifyright = { 13747 // "Justify the selection with alignment "right"." 13748 action: function (value, range) { 13749 justifySelection("right", range); 13750 }, 13751 indeterm: function () { 13752 // "Block-extend the active range. Return true if among visible 13753 // editable nodes that are contained in the result and have no 13754 // children, at least one has alignment value "right" and at least one 13755 // does not. Otherwise return false." 13756 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) { 13757 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 13758 }); 13759 return $_(nodes).some(function (node) { return getAlignmentValue(node) == "right"; }) 13760 && $_(nodes).some(function (node) { return getAlignmentValue(node) != "right"; }); 13761 }, 13762 state: function () { 13763 // "Block-extend the active range. Return true if there is at least one 13764 // visible editable node that is contained in the result and has no 13765 // children, and all such nodes have alignment value "right". 13766 // Otherwise return false." 13767 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) { 13768 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 13769 }); 13770 return nodes.length && $_(nodes).every(function (node) { 13771 return getAlignmentValue(node) == "right"; 13772 }); 13773 }, 13774 value: function () { 13775 // "Block-extend the active range, and return the alignment value of 13776 // the first visible editable node that is contained in the result and 13777 // has no children. If there is no such node, return "left"." 13778 var nodes = getAllContainedNodes(blockExtend(getActiveRange()), function (node) { 13779 return isEditable(node) && isVisible(node) && !node.hasChildNodes(); 13780 }); 13781 if (nodes.length) { 13782 return getAlignmentValue(nodes[0]); 13783 } 13784 return "left"; 13785 } 13786 }; 13787 13788 //@} 13789 ///// The outdent command ///// 13790 //@{ 13791 /*commands.outdent = { 13792 action: function () { 13793 // "Let items be a list of all lis that are ancestor containers of the 13794 // range's start and/or end node." 13795 // 13796 // It's annoying to get this in tree order using functional stuff 13797 // without doing getDescendants(document), which is slow, so I do it 13798 // imperatively. 13799 var items = []; 13800 (function () { 13801 var ancestorContainer; 13802 for (ancestorContainer = getActiveRange().endContainer; 13803 ancestorContainer != getActiveRange().commonAncestorContainer; 13804 ancestorContainer = ancestorContainer.parentNode) { 13805 if (isNamedHtmlElement(ancestorContainer, "li")) { 13806 items.unshift(ancestorContainer); 13807 } 13808 } 13809 for (ancestorContainer = getActiveRange().startContainer; 13810 ancestorContainer; 13811 ancestorContainer = ancestorContainer.parentNode) { 13812 if (isNamedHtmlElement(ancestorContainer, "li")) { 13813 items.unshift(ancestorContainer); 13814 } 13815 } 13816 }()); 13817 13818 // "For each item in items, normalize sublists of item."
13819 $_(items).forEach(function (thisArg) { 13820 normalizeSublists(thisArg, getActiveRange()); 13821 }); 13822 13823 // "Block-extend the active range, and let new range be the result." 13824 var newRange = blockExtend(getActiveRange()); 13825 13826 // "Let node list be a list of nodes, initially empty." 13827 // 13828 // "For each node node contained in new range, append node to node list 13829 // if the last member of node list (if any) is not an ancestor of node; 13830 // node is editable; and either node has no editable descendants, or is 13831 // an ol or ul, or is an li whose parent is an ol or ul." 13832 var nodeList = getContainedNodes(newRange, function (node) { 13833 return isEditable(node) && (!$_(getDescendants(node)).some(isEditable) || isHtmlElementInArray(node, ["ol", "ul"]) || (isNamedHtmlElement(node, 'li') && isHtmlElementInArray(node.parentNode, ["ol", "ul"]))); 13834 }); 13835 13836 // "While node list is not empty:" 13837 while (nodeList.length) { 13838 // "While the first member of node list is an ol or ul or is not 13839 // the child of an ol or ul, outde
13839nt it and remove it from node 13840 // list." 13841 while (nodeList.length && (isHtmlElementInArray(nodeList[0], ["OL", "UL"]) || !isHtmlElementInArray(nodeList[0].parentNode, ["OL", "UL"]))) { 13842 outdentNode(nodeList.shift(), newRange); 13843 } 13844 13845 // "If node list is empty, break from these substeps." 13846 if (!nodeList.length) { 13847 break; 13848 } 13849 13850 // "Let sublist be a list of nodes, initially empty." 13851 var sublist = []; 13852 13853 // "Remove the first member of node list and append it to sublist." 13854 sublist.push(nodeList.shift()); 13855 13856 // "While the first member of node list is the nextSibling of the 13857 // last member of sublist, and the first member of node list is not 13858 // an ol or ul, remove the first member of node list and append it 13859 // to sublist." 13860 while (nodeList.length && nodeList[0] == sublist[sublist.length - 1].nextSibling && !isHtmlElementInArray(nodeList[0], ["OL", "UL"])) { 13861 sublist.push(nodeList.shift()); 13862 } 13863 13864 // "Record the values of sublist, and let values be the result." 13865 var values = recordValues(sublist); 13866 13867 // "Split the parent of sublist, with new parent null." 13868 splitParent(sublist, newRange); 13869 13870 // "Fix disallowed ancestors of each member of sublist." 13871 $_(sublist).forEach(fixDisallowedAncestors); 13872 13873 // "Restore the values from values." 13874 restoreValues(values, newRange); 13875 } 13876 } 13877 };*/ 13878 13879 commands.outdent = { 13880 preservesOverrides: true, 13881 action: function() { 13882 // "Let items be a list of all lis that are ancestor containers of the 13883 // range's start and/or end node." 13884 // 13885 // It's annoying to get this in tree order using functional stuff 13886 // without doing getDescendants(document), which is slow, so I do it 13887 // imperatively. 13888 var items = []; 13889 (function(){ 13890 for ( 13891 var ancestorContainer = getActiveRange().endContainer; 13892 ancestorContainer != getActiveRange().commonAncestorContainer; 13893 ancestorContainer = ancestorContainer.parentNode 13894 ) { 13895 if (isHtmlElement(ancestorContainer, "li")) { 13896 items.unshift(ancestorContainer); 13897 } 13898 } 13899 for ( 13900 var ancestorContainer = getActiveRange().startContainer; 13901 ancestorContainer; 13902 ancestorContainer = ancestorContainer.parentNode 13903 ) { 13904 if (isHtmlElement(ancestorContainer, "li")) { 13905 items.unshift(ancestorContainer); 13906 } 13907 } 13908 })(); 13909 13910 // "For each item in items, normalize sublists of item." 13911 items.forEach(normalizeSublists); 13912 13913 // "Block-extend the active range, and let new range be the result." 13914 var newRange = blockExtend(getActiveRange()); 13915 13916 // "Let node list be a list of nodes, initially empty." 13917 // 13918 // "For each node node contained in new range, append node to node list 13919 // if the last member of node list (if any) is not an ancestor of node; 13920 // node is editable; and either node has no editable descendants, or is 13921 // an ol or ul, or is an li whose parent is an ol or ul." 13922 var nodeList = getContainedNodes(newRange, function(node) { 13923 return isEditable(node) 13924 && (!getDescendants(node).some(isEditable) 13925 || isHtmlElement(node, ["ol", "ul"]) 13926 || (isHtmlElement(node, "li") && isHtmlElement(node.parentNode, ["ol", "ul"]))); 13927 }); 13928 13929 // "While node list is not empty:" 13930 while (nodeList.length) { 13931 // "While the first member of node list is an ol or ul or is not 13932 // the child of an ol or ul, outdent it and remove it from node 13933 // list." 13934 while (nodeList.length 13935 && (isHtmlElement(nodeList[0], ["OL", "UL"]) 13936 || !isHtmlElement(nodeList[0].parentNode, ["OL", "UL"]))) {
13937 outdentNode(nodeList.shift()); 13938 } 13939 13940 // "If node list is empty, break from these substeps." 13941 if (!nodeList.length) { 13942 break; 13943 } 13944 13945 // "Let sublist be a list of nodes, initially empty." 13946 var sublist = []; 13947 13948 // "Remove the first member of node list and append it to sublist." 13949 sublist.push(nodeList.shift()); 13950 13951 // "While the first member of node list is the nextSibling of the 13952 // last member of sublist, and the first member of node list is not 13953 // an ol or ul, remove the first member of node list and append it 13954 // to sublist." 13955 while (nodeList.length 13956 && nodeList[0] == sublist[sublist.length - 1].nextSibling 13957 && !isHtmlElement(nodeList[0], ["OL", "UL"])) { 13958 sublist.push(nodeList.shift()); 13959 } 13960 13961 // "Record the values of sublist, and let values be the result." 13962 var values = recordValues(sublist); 13963 13964 // "Split the parent of sublist, with new parent null." 13965 splitParent(sublist); 13966 13967 // "Fix disallowed ancestors of each member of sublist." 13968 sublist.forEach(fixDisallowedAncestors); 13969 13970 // "Restore the values from values." 13971 restoreValues(values); 13972 } 13973 13974 // "Return true." 13975 return true; 13976 } 13977 }; 13978 13979 //@} 13980 13981 ////////////////////////////////// 13982 ///// Miscellaneous commands ///// 13983 ////////////////////////////////// 13984 13985 ///// The selectAll command ///// 13986 //@{ 13987 commands.selectall = { 13988 // Note, this ignores the whole globalRange/getActiveRange() thing and 13989 // works with actual selections. Not suitable for autoimplementation.html. 13990 action: function () { 13991 // "Let target be the body element of the context object." 13992 var target = document.body; 13993 13994 // "If target is null, let target be the context object's 13995 // documentElement." 13996 if (!target) { 13997 target = document.documentElement; 13998 } 13999 14000 // "If target is null, call getSelection() on the context object, and 14001 // call removeAllRanges() on the result." 14002 if (!target) { 14003 _core.rangy.getSelection().removeAllRanges(); 14004 14005 // "Otherwise, call getSelection() on the context object, and call 14006 // selectAllChildren(target) on the result." 14007 } else { 14008 _core.rangy.getSelection().selectAllChildren(target); 14009 } 14010 } 14011 }; 14012 14013 //@} 14014 ///// The styleWithCSS command ///// 14015 //@{ 14016 commands.stylewithcss = { 14017 action: function (value) { 14018 // "If value is an ASCII case-insensitive match for the string 14019 // "false", set the CSS styling flag to false. Otherwise, set the 14020 // CSS styling flag to true." 14021 cssStylingFlag = String(value).toLowerCase() != "false"; 14022 }, 14023 state: function () { 14024 return cssStylingFlag; 14025 } 14026 }; 14027 14028 //@} 14029 ///// The useCSS command ///// 14030 //@{ 14031 commands.usecss = { 14032 action: function (value) { 14033 // "If value is an ASCII case-insensitive match for the string "false", 14034 // set the CSS styling flag to true. Otherwise, set the CSS styling 14035 // flag to false." 14036 cssStylingFlag = String(value).toLowerCase() == "false"; 14037 } 14038 }; 14039 //@} 14040 14041 // Some final setup 14042 //@{ 14043 (function () { 14044 // Opera 11.50 doesn't implement Object.keys, so I have to make an explicit 14045 // temporary, which means I need an extra closure to not leak the temporaries 14046 // into the global namespace. >:( 14047 var commandNames = []; 14048 var command; 14049 for (command in commands) { 14050 if (commands.hasOwnProperty(command)) { 14051 commandNames.push(command); 14052 } 14053 } 14054 $_(commandNames).forEach(function (command) { 14055 // "If a command does not have a relevant CSS property specified, it 14056 // defaults to null." 14057 if (null == commands[command].relevantCssProperty) { 14058 commands[command].relevantCssProperty = null; 14059 } 14060 14061 // "If a command has inline command activated values defined but 14062 // nothing else defines when it is indeterminate, it is indeterminate 14063 // if among editable Text nodes effectively contained in the active 14064 // range, there is at least one whose effective command value is one of 14065 // the given values and at least one whose effective command value is 14066 // not one of the given values." 14067 if (null != commands[command].inlineCommandActivatedValues && null == commands[command].indeterm) { 14068 commands[command].indeterm = function (range) { 14069 var values = $_(getAllEffectivelyContainedNodes(range, function (node) { return isEditable(node) && node.nodeType == Node.TEXT_NODE; })) 14070 .map(function (node) { return getEffectiveCommandValue(node, command); }); 14071 14072 var matchingValues = $_(values).filter(function (value) { 14073 return $_(commands[command].inlineCommandActivatedValues).indexOf(value) != -1; 14074 }); 14075 14076 return matchingValues.length >= 1 && values.length - matchingValues.length >= 1; 14077 }; 14078 } 14079 14080 // "If a command has inline command activated values defined, its state 14081 // is true if either no editable Text node is effectively contained in 14082 // the active range, and the active range's start node's effective 14083 // command value is one of the given values; or if there is at least 14084 // one editable Text node effectively contained in the active range, 14085 // and all of them have an effective command value equal to one of the 14086 // given values." 14087 if (null != commands[command].inlineCommandActivatedValues) { 14088 commands[command].state = function (range) { 14089 var nodes = getAllEffectivelyContainedNodes(range, function (node) { 14090 return isEditable(node) && node.nodeType == Node.TEXT_NODE; 14091 }); 14092 14093 if (nodes.length == 0) { 14094 return $_(commands[command].inlineCommandActivatedValues).indexOf(getEffectiveCommandValue(range.startContainer, command)) != -1; 14095 } 14096 return $_(nodes).every(function (node) { 14097 return $_(commands[command].inlineCommandActivatedValues).indexOf(getEffectiveCommandValue(node, command)) != -1; 14098 }); 14099 }; 14100 } 14101 14102 // "If a command is a standard inline value command, it is 14103 // indeterminate if among editable Text nodes that are effectively 14104 // contained in the active range, there are two that have distinct 14105 // effective command values. Its value is the effective command value 14106 // of the first editable Text node that is effectively contained in the 14107 // active range, or if there is no such node, the effective command 14108 // value of the active range's start node." 14109 if (null != commands[command].standardInlineValueCommand) { 14110 commands[command].indeterm = function () { 14111 var values = $_(getAllEffectivelyContainedNodes(getActiveRange())).filter(function (node) { return isEditable(node) && node.nodeType == Node.TEXT_NODE; }, true) 14112 .map(function (node) { return getEffectiveCommandValue(node, command); }); 14113 var i; 14114 for (i = 1; i < values.length; i++) { 14115 if (values[i] != values[i - 1]) { 14116 return true; 14117 } 14118 } 14119 return false;
14120 }; 14121 14122 commands[command].value = function (range) { 14123 var refNode = getAllEffectivelyContainedNodes(range, function (node) { 14124 return isEditable(node) && node.nodeType == Node.TEXT_NODE; 14125 })[0]; 14126 14127 if (typeof refNode == "undefined") { 14128 refNode = range.startContainer; 14129 } 14130 14131 return getEffectiveCommandValue(refNode, command); 14132 }; 14133 } 14134 }); 14135 }()); 14136 14137 /** 14138 * 14139 * The active range is the range of the selection given by calling 14140 * getSelection() on the context object. (Thus the active range may be null.) 14141 * <br /> 14142 * We cheat and return globalRange if that's defined. We also ensure that the 14143 * active range meets the requirements that selection boundary points are 14144 * supposed to meet, i.e., that the nodes are both Text or Element nodes that 14145 * descend from a Document. 14146 * 14147 * @method getActiveRange 14148 * @memberOf CargoEditor 14149 * @author Aryeh Gregor 14150 * 14151 */ 14152 14153 this.inSameEditingHost = inSameEditingHost; 14154 this.getEditingHostOf = getEditingHostOf; 14155 this.isEditingHost = isEditingHost; 14156 this.isEditable = isEditable; 14157 14158 /** 14159 * 14160 * @field 14161 * @memberOf CargoEditor 14162 */ 14163 14164 this.commands = commands; 14165 14166 /** 14167 * 14168 * Facade for the actual execCommand {@link CargoEditor.editableImplementation.execCommand}. 14169 * This runs the command through the {@link CargoEditor.MutationManager} so editors and plugins can listen to commands 14170 * being executed. 14171 * 14172 * @memberOf CargoEditor 14173 * 14174 */ 14175 14176 this.execCommand = function(){ 14177 14178 var args = arguments, 14179 self = this; 14180 14181 this.mutationManager.execute(function(){ 14182 return myExecCommand.apply(self, args); 14183 }); 14184 14185 }; 14186 14187 this.execCommandSilent = function(){ 14188 14189 var args = arguments, 14190 self = this; 14191 14192 this.mutationManager.execute(function(){ 14193 return myExecCommand.apply(self, args); 14194 }, true); 14195 14196 }; 14197 14198 this.setCommand = function(name, command){ 14199 14200 if(name === undefined || command === undefined) { 14201 return false; 14202 } 14203 14204 name = name.toLowerCase(); 14205 14206 this.commands[name] = command; 14207 14208 }; 14209 14210 this.isCommandOverruledByExternalStyle = function(commandName){ 14211 14212 if( ! commands.hasOwnProperty(commandName)){ 14213 return false; 14214 } 14215 14216 if( ! commands[commandName].hasOwnProperty('relevantCssProperty')) { 14217 return false; 14218 } 14219 14220 var range = _core.getActiveRange(), 14221 node = range.commonAncestorContainer; 14222 14223 if(node.nodeType !== Node.ELEMENT_NODE){ 14224 node = node.parentNode 14225 } 14226 14227 var relevantCssProperty = commands[commandName].relevantCssProperty, 14228 testEl = document.createElement('styletester'), 14229 clone = node.cloneNode(); 14230 14231 document.body.appendChild(testEl); 14232 14233 // make sure no cascaded style will affect the test element's children 14234 testEl.style.setProperty(relevantCssProperty, 'initial', 'important'); 14235 14236 // remove (external) classes from cloned element 14237 clone.className = ''; 14238 14239 testEl.appendChild(clone); 14240 14241 var externallyAffected = getComputedStyle(node)[relevantCssProperty] !== getComputedStyle(clone)[relevantCssProperty]; 14242 14243 testEl.parentNode.removeChild(testEl); 14244 14245 return externallyAffected; 14246 14247 } 14248 14249 /** 14250 * 14251 * @method queryCommandState 14252 * @memberOf CargoEditor 14253 * @author Aryeh Gregor 14254 * 14255 */ 14256 this.queryCommandState = myQueryCommandState; 14257 14258 /** 14259 * 14260 * @method queryCommandEnabled 14261 * @memberOf CargoEditor 14262 * @author Aryeh Gregor 14263 * 14264 */ 14265 this.queryCommandEnabled = myQueryCommandEnabled; 14266 14267 /** 14268 * 14269 * @method queryCommandIndeterm 14270 * @memberOf CargoEditor 14271 * @author Aryeh Gregor 14272 * 14273 */ 14274 this.queryCommandIndeterm = myQueryCommandIndeterm; 14275 14276 /** 14277 * 14278 * @method queryCommandValue 14279 * @memberOf CargoEditor 14280 * @author Aryeh Gregor 14281 * 14282 */ 14283 this.queryCommandValue = myQueryCommandValue; 14284 14285}).call(this); 14286 14287 14288_core.events.on("init", function() { 14289 14290 // Some compatibility checks: 14291 if (!window.getSelection) { 14292 alert('window.getSelection not supported.'); 14293 } 14294 14295 /*document.body.addEventListener('ondragstart', function(e){ 14296 console.log(e); 14297 e.preventDefault(); 14298 }, false); 14299 14300 document.body.addEventListener('drop', function(e){ 14301 console.log(e); 14302 e.preventDefault(); 14303 14304 }, false);*/ 14305}); 14306 14307_core.events.on('cursor-activity', function(editor){ 14308 14309}); 14310 14311_core.events.on("destroy", function() { 14312 console.log('destroy'); 14313}); 14314 14315_core.events.on('editor-activated', function(editor){ 14316 activeEditor = editor; 14317}); 14318 14319_core.events.on('editor-deactivated', function(editor){ 14320}); 14321 14322_core.events.on("click", function() { 14323 14324}); 14325 14326/** 14327 * ---- events triggers ---- 14328 */ 14329 14330/*$(window).unload(function() { 14331 _core.events.trigger("destroy"); 14332});*/ 14333 14334/* 14335 * jwerty - Awesome handling of keyboard events 14336 * 14337 * jwerty is a JS lib which allows you to bind, fire and assert key combination 14338 * strings against elements and events. It normalises the poor std api into 14339 * something easy to use and clear. 14340 * 14341 * This code is licensed under the MIT 14342 * For the full license see: http://keithamus.mit-license.org/ 14343 * For more information see: http://keithamus.github.com/jwerty 14344 * 14345 * @author Keith Cirkel ('keithamus') <[email protected]> 14346 * @license http://keithamus.mit-license.org/ 14347 * @copyright Copyright © 2011, Keith Cirkel 14348 * 14349 */ 14350
14351(function (global, exports) { 14352 14353 // Helper methods & vars: 14354 var $d = global.document, 14355 $ = (global.jQuery || global.Zepto || global.ender || $d), 14356 $$, // Element selector function 14357 $b, // Event binding function 14358 $f, // Event firing function 14359 ke = 'keydown'; 14360 14361 function realTypeOf(v, s) { 14362 return (v === null) ? s === 'null' 14363 : (v === undefined) ? s === 'undefined' 14364 : (v.is && v instanceof $) ? s === 'element' 14365 : Object.prototype.toString.call(v).toLowerCase().indexOf(s) > 7; 14366 } 14367 14368 if ($ === $d) { 14369 $$ = function (selector, context) { 14370 return selector ? $.querySelector(selector, context || $) : $; 14371 }; 14372 $b = function (e, fn) { e.addEventListener(ke, fn, false); }; 14373 $f = function (e, jwertyEv) { 14374 var ret = $d.createEvent('Event'), 14375 i; 14376 14377 ret.initEvent(ke, true, true); 14378 14379 for (i in jwertyEv) ret[i] = jwertyEv[i]; 14380 14381 return (e || $).dispatchEvent(ret); 14382 }; 14383 } else { 14384 $$ = function (selector, context) { return $(selector || $d, context); }; 14385 $b = function (e, fn) { $(e).bind(ke + '.jwerty', fn); }; 14386 $f = function (e, ob) { $(e || $d).trigger($.Event(ke, ob)); }; 14387 } 14388 14389 // Private 14390 var _modProps = { 16: 'shiftKey', 17: 'ctrlKey', 18: 'altKey', 91: 'metaKey' }; 14391 14392 // Generate key mappings for common keys that are not printable. 14393 var _keys = { 14394 14395 // MOD aka toggleable keys 14396 mods: { 14397 // Shift key, â§ 14398 'â§': 16, 14399 shift: 16, 14400 // CTRL key, on Mac: â 14401 'â': 17, 14402 ctrl: 17, 14403 // ALT key, on Mac: ⥠(Alt) 14404 'â¥': 18, 14405 alt: 18, 14406 option: 18, 14407 // META, on Mac: â (CMD), on Windows (Win), on Linux (Super) 14408 'â': 91, 14409 meta: 91, 14410 cmd: 91, 14411 'super': 91, 14412 win: 91 14413 }, 14414 14415 // Normal keys 14416 keys: { 14417 // Backspace key, on Mac: â« (Backspace) 14418 'â«': 8, 14419 backspace: 8, 14420 // Tab Key, on Mac: ⥠(Tab), on Windows â¥â¥ 14421 'â¥': 9, 14422 'â': 9, 14423 tab: 9, 14424 // Return key, â© 14425 'â©': 13, 14426 'return': 13, 14427 enter: 13, 14428 'â ': 13, 14429 // Pause/Break key 14430 'pause': 19, 14431 'pause-break': 19, 14432 // Caps Lock key, ⪠14433 'âª': 20, 14434 caps: 20, 14435 'caps-lock': 20, 14436 // Escape key, on Mac: â, on Windows: Esc 14437 'â': 27, 14438 escape: 27, 14439 esc: 27, 14440 // Space key 14441 space: 32, 14442 // Page-Up key, or pgup, on Mac: â 14443 'â': 33, 14444 pgup: 33, 14445 'page-up': 33, 14446 // Page-Down key, or pgdown, on Mac: â 14447 'â': 34, 14448 pgdown: 34, 14449 'page-down': 34, 14450 // END key, on Mac: â 14451 'â': 35, 14452 end: 35, 14453 // HOME key, on Mac: â 14454 'â': 36, 14455 home: 36, 14456 // Insert key, or ins 14457 ins: 45, 14458 insert: 45, 14459 // Delete key, on Mac: â« (Delete) 14460 del: 46, 14461 'delete': 46, 14462 14463 // Left Arrow Key, or â 14464 'â': 37, 14465 left: 37, 14466 'arrow-left': 37, 14467 // Up Arrow Key, or â 14468 'â': 38, 14469 up: 38, 14470 'arrow-up': 38, 14471 // Right Arrow Key, or â 14472 'â': 39, 14473 right: 39, 14474 'arrow-right': 39, 14475 // Up Arrow Key, or â 14476 'â': 40, 14477 down: 40, 14478 'arrow-down': 40, 14479 14480 // odities, printing characters that come out wrong: 14481 // Num-Multiply, or * 14482 '*': 106, 14483 star: 106, 14484 asterisk: 106, 14485 multiply: 106, 14486 // Num-Plus or + 14487 '+': 107, 14488 'plus': 107, 14489 // Num-Subtract, or - 14490 '-': 109, 14491 subtract: 109, 14492 'num-.': 110, 14493 'num-period': 110, 14494 'num-dot': 110, 14495 'num-full-stop': 110, 14496 'num-delete': 110, 14497 // Semicolon 14498 ';': 186, 14499 semicolon: 186, 14500 // = or equals 14501 '=': 187, 14502 'equals': 187, 14503 // Comma, or , 14504 ',': 188, 14505 comma: 188, 14506 //'-': 189, //??? 14507 // Period, or ., or full-stop 14508 '.': 190, 14509 period: 190, 14510 'full-stop': 190, 14511 // Slash, or /, or forward-slash 14512 '/': 191, 14513 slash: 191, 14514 'forward-slash': 191, 14515 // Tick, or `, or back-quote 14516 '`': 192, 14517 tick: 192, 14518 'back-quote': 192, 14519 // Open bracket, or [ 14520 '[': 219, 14521 'open-bracket': 219, 14522 // Back slash, or \ 14523 '\\': 220, 14524 'back-slash': 220, 14525 // Close backet, or ] 14526 ']': 221, 14527 'close-bracket': 221, 14528 // Apostraphe, or Quote, or ' 14529 '\'': 222, 14530 quote: 222, 14531 apostraphe: 222 14532 } 14533 14534 }; 14535 14536 // To minimise code bloat, add all of the 0-9 and NUMPAD 0-9 keys in a loop 14537 var i = 47, 14538 n = 0; 14539 while (++i < 106) { 14540 _keys.keys[n] = i; 14541 _keys.keys['num-' + n] = i + 48; 14542 ++n; 14543 } 14544 14545 // To minimise code bloat, add all of the F1-F25 keys in a loop 14546 i = 111, 14547 n = 1; 14548 while (++i < 136) { 14549 _keys.keys['f' + n] = i; 14550 ++n; 14551 } 14552 14553 // To minimise code bloat, add all of the letters of the alphabet in a loop 14554 i = 64; 14555 while (++i < 91) { 14556 _keys.keys[String.fromCharCode(i).toLowerCase()] = i; 14557 } 14558 14559 function JwertyCode(jwertyCode) { 14560 var i, 14561 c, 14562 n, 14563 z, 14564 keyCombo, 14565 optionals, 14566 jwertyCodeFragment, 14567 rangeMatches, 14568 rangeI; 14569 14570 // In-case we get called with an instance of ourselves, just return that. 14571 if (jwertyCode instanceof JwertyCode) return jwertyCode; 14572 14573 // If jwertyCode isn't an array, cast it as a string and split into array.
14574 if (!realTypeOf(jwertyCode, 'array')) { 14575 jwertyCode = (String(jwertyCode)).replace(/\s/g, '').toLowerCase() 14576 .match(/(?:\+,|[^,])+/g); 14577 } 14578 14579 // Loop through each key sequence in jwertyCode 14580 for (i = 0, c = jwertyCode.length; i < c; ++i) { 14581 14582 // If the key combo at this part of the sequence isn't an array, 14583 // cast as a string and split into an array. 14584 if (!realTypeOf(jwertyCode[i], 'array')) { 14585 jwertyCode[i] = String(jwertyCode[i]) 14586 .match(/(?:\+\/|[^\/])+/g); 14587 } 14588 14589 // Parse the key optionals in this sequence 14590 optionals = [], 14591 n = jwertyCode[i].length; 14592 while (n--) { 14593 14594 // Begin creating the object for this key combo 14595 jwertyCodeFragment = jwertyCode[i][n]; 14596 14597 keyCombo = { 14598 jwertyCombo: String(jwertyCodeFragment), 14599 shiftKey: false, 14600 ctrlKey: false, 14601 altKey: false, 14602 metaKey: false 14603 }; 14604 14605 // If jwertyCodeFragment isn't an array then cast as a string 14606 // and split it into one. 14607 if (!realTypeOf(jwertyCodeFragment, 'array')) { 14608 jwertyCodeFragment = String(jwertyCodeFragment).toLowerCase() 14609 .match(/(?:(?:[^\+])+|\+\+|^\+$)/g); 14610 } 14611 14612 z = jwertyCodeFragment.length; 14613 while (z--) { 14614 14615 // Normalise matching errors 14616 if (jwertyCodeFragment[z] === '++') jwertyCodeFragment[z] = '+'; 14617 14618 // Inject either keyCode or ctrl/meta/shift/altKey into keyCombo 14619 if (jwertyCodeFragment[z] in _keys.mods) { 14620 keyCombo[_modProps[_keys.mods[jwertyCodeFragment[z]]]] = true; 14621 } else if (jwertyCodeFragment[z] in _keys.keys) { 14622 keyCombo.keyCode = _keys.keys[jwertyCodeFragment[z]]; 14623 } else { 14624 rangeMatches = jwertyCodeFragment[z].match(/^\[([^-]+\-?[^-]*)-([^-]+\-?[^-]*)\]$/); 14625 } 14626 } 14627 if (realTypeOf(keyCombo.keyCode, 'undefined')) { 14628 // If we picked up a range match earlier... 14629 if (rangeMatches && (rangeMatches[1] in _keys.keys) && (rangeMatches[2] in _keys.keys)) { 14630 rangeMatches[2] = _keys.keys[rangeMatches[2]]; 14631 rangeMatches[1] = _keys.keys[rangeMatches[1]]; 14632 14633 // Go from match 1 and capture all key-comobs up to match 2 14634 for (rangeI = rangeMatches[1]; rangeI < rangeMatches[2]; ++rangeI) { 14635 optionals.push({ 14636 altKey: keyCombo.altKey, 14637 shiftKey: keyCombo.shiftKey, 14638 metaKey: keyCombo.metaKey, 14639 ctrlKey: keyCombo.ctrlKey, 14640 keyCode: rangeI, 14641 jwertyCombo: String(jwertyCodeFragment) 14642 }); 14643 14644 } 14645 keyCombo.keyCode = rangeI; 14646 // Inject either keyCode or ctrl/meta/shift/altKey into keyCombo 14647 } else { 14648 keyCombo.keyCode = 0; 14649 } 14650 } 14651 optionals.push(keyCombo); 14652 14653 } 14654 this[i] = optionals; 14655 } 14656 this.length = i; 14657 return this; 14658 } 14659 14660 var jwerty = exports.jwerty = { 14661 /** 14662 * jwerty.event 14663 * 14664 * `jwerty.event` will return a function, which expects the first 14665 * argument to be a key event. When the key event matches `jwertyCode`, 14666 * `callbackFunction` is fired. `jwerty.event` is used by `jwerty.key` 14667 * to bind the function it returns. `jwerty.event` is useful for 14668 * attaching to your own event listeners. It can be used as a decorator 14669 * method to encapsulate functionality that you only want to fire after 14670 * a specific key combo. If `callbackContext` is specified then it will 14671 * be supplied as `callbackFunction`'s context - in other words, the 14672 * keyword `this` will be set to `callbackContext` inside the 14673 * `callbackFunction` function. 14674 * 14675 * @param {Mixed} jwertyCode can be an array, or string of key 14676 * combinations, which includes optinals and or sequences 14677 * @param {Function} callbackFucntion is a function (or boolean) which 14678 * is fired when jwertyCode is matched. Return false to 14679 * preventDefault() 14680 * @param {Object} callbackContext (Optional) The context to call 14681 * `callback` with (i.e this) 14682 * 14683 */ 14684 event: function (jwertyCode, callbackFunction, callbackContext /*? this */) { 14685 14686 // Construct a function out of callbackFunction, if it is a boolean. 14687 if (realTypeOf(callbackFunction, 'boolean')) { 14688 var bool = callbackFunction; 14689 callbackFunction = function () { return bool; }; 14690 } 14691 14692 jwertyCode = new JwertyCode(jwertyCode); 14693 14694 // Initialise in-scope vars. 14695 var i = 0, 14696 c = jwertyCode.length - 1, 14697 returnValue, 14698 jwertyCodeIs; 14699 14700 // This is the event listener function that gets returned... 14701 return function (event) { 14702 14703 // if jwertyCodeIs returns truthy (string)... 14704 if ((jwertyCodeIs = jwerty.is(jwertyCode, event, i))) { 14705 // ... and this isn't the last key in the sequence, 14706 // incriment the key in sequence to check. 14707 if (i < c) { 14708 ++i; 14709 return; 14710 // ... and this is the last in the sequence (or the only 14711 // one in sequence), then fire the callback 14712 } else { 14713 returnValue = callbackFunction.call( 14714 callbackContext || this, event, jwertyCodeIs); 14715 14716 // If the callback returned false, then we should run 14717 // preventDefault(); 14718 if (returnValue === false) event.preventDefault(); 14719 14720 // Reset i for the next sequence to fire. 14721 i = 0; 14722 return; 14723 } 14724 } 14725 14726 // If the event didn't hit this time, we should reset i to 0, 14727 // that is, unless this combo was the first in the sequence, 14728 // in which case we should reset i to 1. 14729 i = jwerty.is(jwertyCode, event) ? 1 : 0; 14730 }; 14731 }, 14732 14733 /** 14734 * jwerty.is 14735 * 14736 * `jwerty.is` will return a boolean value, based on if `event` matches 14737 * `jwertyCode`. `jwerty.is` is called by `jwerty.event` to check 14738 * whether or not to fire the callback. `event` can be a DOM event, or 14739 * a jQuery/Zepto/Ender manufactured event. The properties of
14740 * `jwertyCode` (speficially ctrlKey, altKey, metaKey, shiftKey and 14741 * keyCode) should match `jwertyCode`'s properties - if they do, then 14742 * `jwerty.is` will return `true`. If they don't, `jwerty.is` will 14743 * return `false`. 14744 * 14745 * @param {Mixed} jwertyCode can be an array, or string of key 14746 * combinations, which includes optinals and or sequences 14747 * @param {KeyboardEvent} event is the KeyboardEvent to assert against 14748 * @param {Integer} i (Optional) checks the `i` key in jwertyCode 14749 * sequence 14750 * 14751 */ 14752 is: function (jwertyCode, event, i /*? 0*/) { 14753 jwertyCode = new JwertyCode(jwertyCode); 14754 // Default `i` to 0 14755 i = i || 0; 14756 // We are only interested in `i` of jwertyCode; 14757 jwertyCode = jwertyCode[i]; 14758 // jQuery stores the *real* event in `originalEvent`, which we use 14759 // because it does annoything stuff to `metaKey` 14760 event = event.originalEvent || event; 14761 14762 // We'll look at each optional in this jwertyCode sequence... 14763 var n = jwertyCode.length, 14764 returnValue = false; 14765 14766 // Loop through each fragment of jwertyCode 14767 while (n--) { 14768 returnValue = jwertyCode[n].jwertyCombo; 14769 // For each property in the jwertyCode object, compare to `event` 14770 for (var p in jwertyCode[n]) { 14771 // ...except for jwertyCode.jwertyCombo... 14772 if (p !== 'jwertyCombo' && event[p] != jwertyCode[n][p]) returnValue = false; 14773 } 14774 // If this jwertyCode optional wasn't falsey, then we can return early. 14775 if (returnValue !== false) return returnValue; 14776 } 14777 return returnValue; 14778 }, 14779 14780 /** 14781 * jwerty.key 14782 * 14783 * `jwerty.key` will attach an event listener and fire 14784 * `callbackFunction` when `jwertyCode` matches. The event listener is 14785 * attached to `document`, meaning it will listen for any key events 14786 * on the page (a global shortcut listener). If `callbackContext` is 14787 * specified then it will be supplied as `callbackFunction`'s context 14788 * - in other words, the keyword `this` will be set to 14789 * `callbackContext` inside the `callbackFunction` function. 14790 * 14791 * @param {Mixed} jwertyCode can be an array, or string of key 14792 * combinations, which includes optinals and or sequences 14793 * @param {Function} callbackFunction is a function (or boolean) which 14794 * is fired when jwertyCode is matched. Return false to 14795 * preventDefault() 14796 * @param {Object} callbackContext (Optional) The context to call 14797 * `callback` with (i.e this) 14798 * @param {Mixed} selector can be a string, jQuery/Zepto/Ender object, 14799 * or an HTML*Element on which to bind the eventListener 14800 * @param {Mixed} selectorContext can be a string, jQuery/Zepto/Ender 14801 * object, or an HTML*Element on which to scope the selector 14802 * 14803 */ 14804 key: function (jwertyCode, callbackFunction, callbackContext /*? this */, selector /*? document */, selectorContext /*? body */) { 14805 // Because callbackContext is optional, we should check if the 14806 // `callbackContext` is a string or element, and if it is, then the 14807 // function was called without a context, and `callbackContext` is 14808 // actually `selector` 14809 var realSelector = realTypeOf(callbackContext, 'element') || realTypeOf(callbackContext, 'string') ? callbackContext : selector, 14810 // If `callbackContext` is undefined, or if we skipped it (and 14811 // therefore it is `realSelector`), set context to `global`. 14812 realcallbackContext = realSelector === callbackContext ? global : callbackContext, 14813 // Finally if we did skip `callbackContext`, then shift 14814 // `selectorContext` to the left (take it from `selector`) 14815 realSelectorContext = realSelector === callbackContext ? selector : selectorContext; 14816 14817 // If `realSelector` is already a jQuery/Zepto/Ender/DOM element, 14818 // then just use it neat, otherwise find it in DOM using $$() 14819 $b( 14820 realTypeOf(realSelector, 'element') ? realSelector : $$(realSelector, realSelectorContext), 14821 jwerty.event(jwertyCode, callbackFunction, realcallbackContext) 14822 ); 14823 }, 14824 14825 /** 14826 * jwerty.fire 14827 * 14828 * `jwerty.fire` will construct a keyup event to fire, based on 14829 * `jwertyCode`. The event will be fired against `selector`. 14830 * `selectorContext` is used to search for `selector` within 14831 * `selectorContext`, similar to jQuery's 14832 * `$('selector', 'context')`. 14833 * 14834 * @param {Mixed} jwertyCode can be an array, or string of key 14835 * combinations, which includes optinals and or sequences 14836 * @param {Mixed} selector can be a string, jQuery/Zepto/Ender object, 14837 * or an HTML*Element on which to bind the eventListener 14838 * @param {Mixed}
14838 selectorContext can be a string, jQuery/Zepto/Ender 14839 * object, or an HTML*Element on which to scope the selector 14840 * 14841 */ 14842 fire: function (jwertyCode, selector /*? document */, selectorContext /*? body */, i) { 14843 jwertyCode = new JwertyCode(jwertyCode); 14844 var realI = realTypeOf(selectorContext, 'number') ? selectorContext : i; 14845 14846 // If `realSelector` is already a jQuery/Zepto/Ender/DOM element, 14847 // then just use it neat, otherwise find it in DOM using $$() 14848 $f( 14849 realTypeOf(selector, 'element') ? selector : $$(selector, selectorContext), 14850 jwertyCode[realI || 0][0] 14851 ); 14852 }, 14853 14854 KEYS: _keys 14855 }; 14856 14857}(this, (typeof module !== 'undefined' && module.exports ? module.exports : this))); 14858 14859var lang = 'en', 14860 localization = { 14861 'mutation-fail': { 14862 'en': 'Failed to execute mutation', 14863 'nl': 'Kan mutatie niet uitvoeren' 14864 }, 14865 'missing_param': { 14866 'en': 'One or more parameters are missing', 14867 'nl': 'Een of meerdere parameters missen', 14868 } 14869 }; 14870 14871var getLocalization = function(id){ 14872 14873 if(typeof id !== 'string') { 14874 return ''; 14875 } 14876 14877 id = id.toLowerCase(); 14878 14879 if(localization.hasOwnProperty(id)){ 14880 if(localization[id].hasOwnProperty(lang)){ 14881 return localization[id][lang]; 14882 } 14883 } 14884 14885 return id; 14886 14887}; 14888 14889 14890}.call(window.CargoEditor = window.CargoEditor || {})); 14891 14892 14893if(typeof $ !== 'undefined'){ 14894 $(function($) { 14895 CargoEditor.events.trigger("init"); 14896 14897 if (typeof Cargo !== "undefined") { 14898 for (var key in shortcut.all_shortcuts) { 14899 Cargo.Core.KeyboardShortcut.Remove(key); 14900 } 14901 } 14902 }); 14903}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.