[ Index ] |
PHP Cross Reference of moodle-2.8 |
[Summary view] [Print] [Text view]
1 /** 2 * @license CSS Class Applier module for Rangy. 3 * Adds, removes and toggles CSS classes on Ranges and Selections 4 * 5 * Part of Rangy, a cross-browser JavaScript range and selection library 6 * http://code.google.com/p/rangy/ 7 * 8 * Depends on Rangy core. 9 * 10 * Copyright 2012, Tim Down 11 * Licensed under the MIT license. 12 * Version: 1.2.3 13 * Build date: 26 February 2012 14 */ 15 rangy.createModule("CssClassApplier", function(api, module) { 16 api.requireModules( ["WrappedSelection", "WrappedRange"] ); 17 18 var dom = api.dom; 19 20 21 22 var defaultTagName = "span"; 23 24 function trim(str) { 25 return str.replace(/^\s\s*/, "").replace(/\s\s*$/, ""); 26 } 27 28 function hasClass(el, cssClass) { 29 return el.className && new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)").test(el.className); 30 } 31 32 function addClass(el, cssClass) { 33 if (el.className) { 34 if (!hasClass(el, cssClass)) { 35 el.className += " " + cssClass; 36 } 37 } else { 38 el.className = cssClass; 39 } 40 } 41 42 var removeClass = (function() { 43 function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) { 44 return (whiteSpaceBefore && whiteSpaceAfter) ? " " : ""; 45 } 46 47 return function(el, cssClass) { 48 if (el.className) { 49 el.className = el.className.replace(new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)"), replacer); 50 } 51 }; 52 })(); 53 54 function sortClassName(className) { 55 return className.split(/\s+/).sort().join(" "); 56 } 57 58 function getSortedClassName(el) { 59 return sortClassName(el.className); 60 } 61 62 function haveSameClasses(el1, el2) { 63 return getSortedClassName(el1) == getSortedClassName(el2); 64 } 65 66 function replaceWithOwnChildren(el) { 67 68 var parent = el.parentNode; 69 while (el.hasChildNodes()) { 70 parent.insertBefore(el.firstChild, el); 71 } 72 parent.removeChild(el); 73 } 74 75 function rangeSelectsAnyText(range, textNode) { 76 var textRange = range.cloneRange(); 77 textRange.selectNodeContents(textNode); 78 79 var intersectionRange = textRange.intersection(range); 80 var text = intersectionRange ? intersectionRange.toString() : ""; 81 textRange.detach(); 82 83 return text != ""; 84 } 85 86 function getEffectiveTextNodes(range) { 87 return range.getNodes([3], function(textNode) { 88 return rangeSelectsAnyText(range, textNode); 89 }); 90 } 91 92 function elementsHaveSameNonClassAttributes(el1, el2) { 93 if (el1.attributes.length != el2.attributes.length) return false; 94 for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) { 95 attr1 = el1.attributes[i]; 96 name = attr1.name; 97 if (name != "class") { 98 attr2 = el2.attributes.getNamedItem(name); 99 if (attr1.specified != attr2.specified) return false; 100 if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false; 101 } 102 } 103 return true; 104 } 105 106 function elementHasNonClassAttributes(el, exceptions) { 107 for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) { 108 attrName = el.attributes[i].name; 109 if ( !(exceptions && dom.arrayContains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") { 110 return true; 111 } 112 } 113 return false; 114 } 115 116 function elementHasProps(el, props) { 117 for (var p in props) { 118 if (props.hasOwnProperty(p) && el[p] !== props[p]) { 119 return false; 120 } 121 } 122 return true; 123 } 124 125 var getComputedStyleProperty; 126 127 if (typeof window.getComputedStyle != "undefined") { 128 getComputedStyleProperty = function(el, propName) { 129 return dom.getWindow(el).getComputedStyle(el, null)[propName]; 130 }; 131 } else if (typeof document.documentElement.currentStyle != "undefined") { 132 getComputedStyleProperty = function(el, propName) { 133 return el.currentStyle[propName]; 134 }; 135 } else { 136 module.fail("No means of obtaining computed style properties found"); 137 } 138 139 var isEditableElement; 140 141 (function() { 142 var testEl = document.createElement("div"); 143 if (typeof testEl.isContentEditable == "boolean") { 144 isEditableElement = function(node) { 145 return node && node.nodeType == 1 && node.isContentEditable; 146 }; 147 } else { 148 isEditableElement = function(node) { 149 if (!node || node.nodeType != 1 || node.contentEditable == "false") { 150 return false; 151 } 152 return node.contentEditable == "true" || isEditableElement(node.parentNode); 153 }; 154 } 155 })(); 156 157 function isEditingHost(node) { 158 var parent; 159 return node && node.nodeType == 1 160 && (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on") 161 || (isEditableElement(node) && !isEditableElement(node.parentNode))); 162 } 163 164 function isEditable(node) { 165 return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node); 166 } 167 168 var inlineDisplayRegex = /^inline(-block|-table)?$/i; 169 170 function isNonInlineElement(node) { 171 return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display")); 172 } 173 174 // White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html) 175 var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/; 176 177 function isUnrenderedWhiteSpaceNode(node) { 178 if (node.data.length == 0) { 179 return true; 180 } 181 if (htmlNonWhiteSpaceRegex.test(node.data)) { 182 return false; 183 } 184 var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace"); 185 switch (cssWhiteSpace) { 186 case "pre": 187 case "pre-wrap": 188 case "-moz-pre-wrap": 189 return false; 190 case "pre-line": 191 if (/[\r\n]/.test(node.data)) { 192 return false; 193 } 194 } 195 196 // We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a 197 // non-inline element, it will not be rendered. This seems to be a good enough definition. 198 return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling); 199 } 200 201 function isSplitPoint(node, offset) { 202 if (dom.isCharacterDataNode(node)) { 203 if (offset == 0) { 204 return !!node.previousSibling; 205 } else if (offset == node.length) { 206 return !!node.nextSibling; 207 } else { 208 return true; 209 } 210 } 211 212 return offset > 0 && offset < node.childNodes.length; 213 } 214 215 function splitNodeAt(node, descendantNode, descendantOffset, rangesToPreserve) { 216 var newNode; 217 var splitAtStart = (descendantOffset == 0); 218 219 if (dom.isAncestorOf(descendantNode, node)) { 220 221 return node; 222 } 223 224 if (dom.isCharacterDataNode(descendantNode)) { 225 if (descendantOffset == 0) { 226 descendantOffset = dom.getNodeIndex(descendantNode); 227 descendantNode = descendantNode.parentNode; 228 } else if (descendantOffset == descendantNode.length) { 229 descendantOffset = dom.getNodeIndex(descendantNode) + 1; 230 descendantNode = descendantNode.parentNode; 231 } else { 232 throw module.createError("splitNodeAt should not be called with offset in the middle of a data node (" 233 + descendantOffset + " in " + descendantNode.data); 234 } 235 } 236 237 if (isSplitPoint(descendantNode, descendantOffset)) { 238 if (!newNode) { 239 newNode = descendantNode.cloneNode(false); 240 if (newNode.id) { 241 newNode.removeAttribute("id"); 242 } 243 var child; 244 while ((child = descendantNode.childNodes[descendantOffset])) { 245 newNode.appendChild(child); 246 } 247 dom.insertAfter(newNode, descendantNode); 248 } 249 return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, dom.getNodeIndex(newNode), rangesToPreserve); 250 } else if (node != descendantNode) { 251 newNode = descendantNode.parentNode; 252 253 // Work out a new split point in the parent node 254 var newNodeIndex = dom.getNodeIndex(descendantNode); 255 256 if (!splitAtStart) { 257 newNodeIndex++; 258 } 259 return splitNodeAt(node, newNode, newNodeIndex, rangesToPreserve); 260 } 261 return node; 262 } 263 264 function areElementsMergeable(el1, el2) { 265 return el1.tagName == el2.tagName && haveSameClasses(el1, el2) && elementsHaveSameNonClassAttributes(el1, el2); 266 } 267 268 function createAdjacentMergeableTextNodeGetter(forward) { 269 var propName = forward ? "nextSibling" : "previousSibling"; 270 271 return function(textNode, checkParentElement) { 272 var el = textNode.parentNode; 273 var adjacentNode = textNode[propName]; 274 if (adjacentNode) { 275 // Can merge if the node's previous/next sibling is a text node 276 if (adjacentNode && adjacentNode.nodeType == 3) { 277 return adjacentNode; 278 } 279 } else if (checkParentElement) { 280 // Compare text node parent element with its sibling 281 adjacentNode = el[propName]; 282 283 if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) { 284 return adjacentNode[forward ? "firstChild" : "lastChild"]; 285 } 286 } 287 return null; 288 } 289 } 290 291 var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false), 292 getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true); 293 294 295 function Merge(firstNode) { 296 this.isElementMerge = (firstNode.nodeType == 1); 297 this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode; 298 this.textNodes = [this.firstTextNode]; 299 } 300 301 Merge.prototype = { 302 doMerge: function() { 303 var textBits = [], textNode, parent, text; 304 for (var i = 0, len = this.textNodes.length; i < len; ++i) { 305 textNode = this.textNodes[i]; 306 parent = textNode.parentNode; 307 textBits[i] = textNode.data; 308 if (i) { 309 parent.removeChild(textNode); 310 if (!parent.hasChildNodes()) { 311 parent.parentNode.removeChild(parent); 312 } 313 } 314 } 315 this.firstTextNode.data = text = textBits.join(""); 316 return text; 317 }, 318 319 getLength: function() { 320 var i = this.textNodes.length, len = 0; 321 while (i--) { 322 len += this.textNodes[i].length; 323 } 324 return len; 325 }, 326 327 toString: function() { 328 var textBits = []; 329 for (var i = 0, len = this.textNodes.length; i < len; ++i) { 330 textBits[i] = "'" + this.textNodes[i].data + "'"; 331 } 332 return "[Merge(" + textBits.join(",") + ")]"; 333 } 334 }; 335 336 var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly"]; 337 338 // Allow "class" as a property name in object properties 339 var mappedPropertyNames = {"class" : "className"}; 340 341 function CssClassApplier(cssClass, options, tagNames) { 342 this.cssClass = cssClass; 343 var normalize, i, len, propName; 344 345 var elementPropertiesFromOptions = null; 346 347 // Initialize from options object 348 if (typeof options == "object" && options !== null) { 349 tagNames = options.tagNames; 350 elementPropertiesFromOptions = options.elementProperties; 351 352 for (i = 0; propName = optionProperties[i++]; ) { 353 if (options.hasOwnProperty(propName)) { 354 this[propName] = options[propName]; 355 } 356 } 357 normalize = options.normalize; 358 } else { 359 normalize = options; 360 } 361 362 // Backwards compatibility: the second parameter can also be a Boolean indicating whether normalization 363 this.normalize = (typeof normalize == "undefined") ? true : normalize; 364 365 // Initialize element properties and attribute exceptions 366 this.attrExceptions = []; 367 var el = document.createElement(this.elementTagName); 368 this.elementProperties = {}; 369 for (var p in elementPropertiesFromOptions) { 370 if (elementPropertiesFromOptions.hasOwnProperty(p)) { 371 // Map "class" to "className" 372 if (mappedPropertyNames.hasOwnProperty(p)) { 373 p = mappedPropertyNames[p]; 374 } 375 el[p] = elementPropertiesFromOptions[p]; 376 377 // Copy the property back from the dummy element so that later comparisons to check whether elements 378 // may be removed are checking against the right value. For example, the href property of an element 379 // returns a fully qualified URL even if it was previously assigned a relative URL. 380 this.elementProperties[p] = el[p]; 381 this.attrExceptions.push(p); 382 } 383 } 384 385 this.elementSortedClassName = this.elementProperties.hasOwnProperty("className") ? 386 sortClassName(this.elementProperties.className + " " + cssClass) : cssClass; 387 388 // Initialize tag names 389 this.applyToAnyTagName = false; 390 var type = typeof tagNames; 391 if (type == "string") { 392 if (tagNames == "*") { 393 this.applyToAnyTagName = true; 394 } else { 395 this.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/); 396 } 397 } else if (type == "object" && typeof tagNames.length == "number") { 398 this.tagNames = []; 399 for (i = 0, len = tagNames.length; i < len; ++i) { 400 if (tagNames[i] == "*") { 401 this.applyToAnyTagName = true; 402 } else { 403 this.tagNames.push(tagNames[i].toLowerCase()); 404 } 405 } 406 } else { 407 this.tagNames = [this.elementTagName]; 408 } 409 } 410 411 CssClassApplier.prototype = { 412 elementTagName: defaultTagName, 413 elementProperties: {}, 414 ignoreWhiteSpace: true, 415 applyToEditableOnly: false, 416 417 hasClass: function(node) { 418 return node.nodeType == 1 && dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && hasClass(node, this.cssClass); 419 }, 420 421 getSelfOrAncestorWithClass: function(node) { 422 while (node) { 423 if (this.hasClass(node, this.cssClass)) { 424 return node; 425 } 426 node = node.parentNode; 427 } 428 return null; 429 }, 430 431 isModifiable: function(node) { 432 return !this.applyToEditableOnly || isEditable(node); 433 }, 434 435 // White space adjacent to an unwrappable node can be ignored for wrapping 436 isIgnorableWhiteSpaceNode: function(node) { 437 return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node); 438 }, 439 440 // Normalizes nodes after applying a CSS class to a Range. 441 postApply: function(textNodes, range, isUndo) { 442 443 var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1]; 444 445 var merges = [], currentMerge; 446 447 var rangeStartNode = firstNode, rangeEndNode = lastNode; 448 var rangeStartOffset = 0, rangeEndOffset = lastNode.length; 449 450 var textNode, precedingTextNode; 451 452 for (var i = 0, len = textNodes.length; i < len; ++i) { 453 textNode = textNodes[i]; 454 precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo); 455 456 if (precedingTextNode) { 457 if (!currentMerge) { 458 currentMerge = new Merge(precedingTextNode); 459 merges.push(currentMerge); 460 } 461 currentMerge.textNodes.push(textNode); 462 if (textNode === firstNode) { 463 rangeStartNode = currentMerge.firstTextNode; 464 rangeStartOffset = rangeStartNode.length; 465 } 466 if (textNode === lastNode) { 467 rangeEndNode = currentMerge.firstTextNode; 468 rangeEndOffset = currentMerge.getLength(); 469 } 470 } else { 471 currentMerge = null; 472 } 473 } 474 475 // Test whether the first node after the range needs merging 476 var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo); 477 478 if (nextTextNode) { 479 if (!currentMerge) { 480 currentMerge = new Merge(lastNode); 481 merges.push(currentMerge); 482 } 483 currentMerge.textNodes.push(nextTextNode); 484 } 485 486 // Do the merges 487 if (merges.length) { 488 489 for (i = 0, len = merges.length; i < len; ++i) { 490 merges[i].doMerge(); 491 } 492 493 494 // Set the range boundaries 495 range.setStart(rangeStartNode, rangeStartOffset); 496 range.setEnd(rangeEndNode, rangeEndOffset); 497 } 498 499 }, 500 501 createContainer: function(doc) { 502 var el = doc.createElement(this.elementTagName); 503 api.util.extend(el, this.elementProperties); 504 addClass(el, this.cssClass); 505 return el; 506 }, 507 508 applyToTextNode: function(textNode) { 509 510 511 var parent = textNode.parentNode; 512 if (parent.childNodes.length == 1 && dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) { 513 addClass(parent, this.cssClass); 514 } else { 515 var el = this.createContainer(dom.getDocument(textNode)); 516 textNode.parentNode.insertBefore(el, textNode); 517 el.appendChild(textNode); 518 } 519 520 }, 521 522 isRemovable: function(el) { 523 return el.tagName.toLowerCase() == this.elementTagName 524 && getSortedClassName(el) == this.elementSortedClassName 525 && elementHasProps(el, this.elementProperties) 526 && !elementHasNonClassAttributes(el, this.attrExceptions) 527 && this.isModifiable(el); 528 }, 529 530 undoToTextNode: function(textNode, range, ancestorWithClass) { 531 532 if (!range.containsNode(ancestorWithClass)) { 533 // Split out the portion of the ancestor from which we can remove the CSS class 534 //var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass); 535 var ancestorRange = range.cloneRange(); 536 ancestorRange.selectNode(ancestorWithClass); 537 538 if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)/* && isSplitPoint(range.endContainer, range.endOffset)*/) { 539 splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, [range]); 540 range.setEndAfter(ancestorWithClass); 541 } 542 if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)/* && isSplitPoint(range.startContainer, range.startOffset)*/) { 543 ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, [range]); 544 } 545 } 546 547 if (this.isRemovable(ancestorWithClass)) { 548 replaceWithOwnChildren(ancestorWithClass); 549 } else { 550 removeClass(ancestorWithClass, this.cssClass); 551 } 552 }, 553 554 applyToRange: function(range) { 555 range.splitBoundaries(); 556 var textNodes = getEffectiveTextNodes(range); 557 558 if (textNodes.length) { 559 var textNode; 560 561 for (var i = 0, len = textNodes.length; i < len; ++i) { 562 textNode = textNodes[i]; 563 564 if (!this.isIgnorableWhiteSpaceNode(textNode) && !this.getSelfOrAncestorWithClass(textNode) 565 && this.isModifiable(textNode)) { 566 this.applyToTextNode(textNode); 567 } 568 } 569 range.setStart(textNodes[0], 0); 570 textNode = textNodes[textNodes.length - 1]; 571 range.setEnd(textNode, textNode.length); 572 if (this.normalize) { 573 this.postApply(textNodes, range, false); 574 } 575 } 576 }, 577 578 applyToSelection: function(win) { 579 580 win = win || window; 581 var sel = api.getSelection(win); 582 583 var range, ranges = sel.getAllRanges(); 584 sel.removeAllRanges(); 585 var i = ranges.length; 586 while (i--) { 587 range = ranges[i]; 588 this.applyToRange(range); 589 sel.addRange(range); 590 } 591 592 }, 593 594 undoToRange: function(range) { 595 596 range.splitBoundaries(); 597 var textNodes = getEffectiveTextNodes(range); 598 var textNode, ancestorWithClass; 599 var lastTextNode = textNodes[textNodes.length - 1]; 600 601 if (textNodes.length) { 602 for (var i = 0, len = textNodes.length; i < len; ++i) { 603 textNode = textNodes[i]; 604 ancestorWithClass = this.getSelfOrAncestorWithClass(textNode); 605 if (ancestorWithClass && this.isModifiable(textNode)) { 606 this.undoToTextNode(textNode, range, ancestorWithClass); 607 } 608 609 // Ensure the range is still valid 610 range.setStart(textNodes[0], 0); 611 range.setEnd(lastTextNode, lastTextNode.length); 612 } 613 614 615 616 if (this.normalize) { 617 this.postApply(textNodes, range, true); 618 } 619 } 620 }, 621 622 undoToSelection: function(win) { 623 win = win || window; 624 var sel = api.getSelection(win); 625 var ranges = sel.getAllRanges(), range; 626 sel.removeAllRanges(); 627 for (var i = 0, len = ranges.length; i < len; ++i) { 628 range = ranges[i]; 629 this.undoToRange(range); 630 sel.addRange(range); 631 } 632 }, 633 634 getTextSelectedByRange: function(textNode, range) { 635 var textRange = range.cloneRange(); 636 textRange.selectNodeContents(textNode); 637 638 var intersectionRange = textRange.intersection(range); 639 var text = intersectionRange ? intersectionRange.toString() : ""; 640 textRange.detach(); 641 642 return text; 643 }, 644 645 isAppliedToRange: function(range) { 646 if (range.collapsed) { 647 return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer); 648 } else { 649 var textNodes = range.getNodes( [3] ); 650 for (var i = 0, textNode; textNode = textNodes[i++]; ) { 651 if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode) 652 && this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) { 653 return false; 654 } 655 } 656 return true; 657 } 658 }, 659 660 isAppliedToSelection: function(win) { 661 win = win || window; 662 var sel = api.getSelection(win); 663 var ranges = sel.getAllRanges(); 664 var i = ranges.length; 665 while (i--) { 666 if (!this.isAppliedToRange(ranges[i])) { 667 return false; 668 } 669 } 670 671 return true; 672 }, 673 674 toggleRange: function(range) { 675 if (this.isAppliedToRange(range)) { 676 this.undoToRange(range); 677 } else { 678 this.applyToRange(range); 679 } 680 }, 681 682 toggleSelection: function(win) { 683 if (this.isAppliedToSelection(win)) { 684 this.undoToSelection(win); 685 } else { 686 this.applyToSelection(win); 687 } 688 }, 689 690 detach: function() {} 691 }; 692 693 function createCssClassApplier(cssClass, options, tagNames) { 694 return new CssClassApplier(cssClass, options, tagNames); 695 } 696 697 CssClassApplier.util = { 698 hasClass: hasClass, 699 addClass: addClass, 700 removeClass: removeClass, 701 hasSameClasses: haveSameClasses, 702 replaceWithOwnChildren: replaceWithOwnChildren, 703 elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes, 704 elementHasNonClassAttributes: elementHasNonClassAttributes, 705 splitNodeAt: splitNodeAt, 706 isEditableElement: isEditableElement, 707 isEditingHost: isEditingHost, 708 isEditable: isEditable 709 }; 710 711 api.CssClassApplier = CssClassApplier; 712 api.createCssClassApplier = createCssClassApplier; 713 });
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 20:29:05 2014 | Cross-referenced by PHPXref 0.7.1 |