Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Version 1.2.2-1

  • Loading branch information...
commit 8c6ab6f907dd444c4c34bb9acd34cf76232ecee7 1 parent 73ee710
@kapouer authored
View
5 README
@@ -0,0 +1,5 @@
+This is the npm package for "rangy", a javascript browser library.
+
+Rangy scripts are provided uncompressed and unmodified.
+
+http://code.google.com/p/rangy/
View
3,211 lib/rangy-core.js
3,211 additions, 0 deletions not shown
View
713 lib/rangy-cssclassapplier.js
@@ -0,0 +1,713 @@
+/**
+ * @license CSS Class Applier module for Rangy.
+ * Adds, removes and toggles CSS classes on Ranges and Selections
+ *
+ * Part of Rangy, a cross-browser JavaScript range and selection library
+ * http://code.google.com/p/rangy/
+ *
+ * Depends on Rangy core.
+ *
+ * Copyright 2011, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.2.2
+ * Build date: 13 November 2011
+ */
+rangy.createModule("CssClassApplier", function(api, module) {
+ api.requireModules( ["WrappedSelection", "WrappedRange"] );
+
+ var dom = api.dom;
+
+
+
+ var defaultTagName = "span";
+
+ function trim(str) {
+ return str.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
+ }
+
+ function hasClass(el, cssClass) {
+ return el.className && new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)").test(el.className);
+ }
+
+ function addClass(el, cssClass) {
+ if (el.className) {
+ if (!hasClass(el, cssClass)) {
+ el.className += " " + cssClass;
+ }
+ } else {
+ el.className = cssClass;
+ }
+ }
+
+ var removeClass = (function() {
+ function replacer(matched, whiteSpaceBefore, whiteSpaceAfter) {
+ return (whiteSpaceBefore && whiteSpaceAfter) ? " " : "";
+ }
+
+ return function(el, cssClass) {
+ if (el.className) {
+ el.className = el.className.replace(new RegExp("(?:^|\\s)" + cssClass + "(?:\\s|$)"), replacer);
+ }
+ };
+ })();
+
+ function sortClassName(className) {
+ return className.split(/\s+/).sort().join(" ");
+ }
+
+ function getSortedClassName(el) {
+ return sortClassName(el.className);
+ }
+
+ function haveSameClasses(el1, el2) {
+ return getSortedClassName(el1) == getSortedClassName(el2);
+ }
+
+ function replaceWithOwnChildren(el) {
+
+ var parent = el.parentNode;
+ while (el.hasChildNodes()) {
+ parent.insertBefore(el.firstChild, el);
+ }
+ parent.removeChild(el);
+ }
+
+ function rangeSelectsAnyText(range, textNode) {
+ var textRange = range.cloneRange();
+ textRange.selectNodeContents(textNode);
+
+ var intersectionRange = textRange.intersection(range);
+ var text = intersectionRange ? intersectionRange.toString() : "";
+ textRange.detach();
+
+ return text != "";
+ }
+
+ function getEffectiveTextNodes(range) {
+ return range.getNodes([3], function(textNode) {
+ return rangeSelectsAnyText(range, textNode);
+ });
+ }
+
+ function elementsHaveSameNonClassAttributes(el1, el2) {
+ if (el1.attributes.length != el2.attributes.length) return false;
+ for (var i = 0, len = el1.attributes.length, attr1, attr2, name; i < len; ++i) {
+ attr1 = el1.attributes[i];
+ name = attr1.name;
+ if (name != "class") {
+ attr2 = el2.attributes.getNamedItem(name);
+ if (attr1.specified != attr2.specified) return false;
+ if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) return false;
+ }
+ }
+ return true;
+ }
+
+ function elementHasNonClassAttributes(el, exceptions) {
+ for (var i = 0, len = el.attributes.length, attrName; i < len; ++i) {
+ attrName = el.attributes[i].name;
+ if ( !(exceptions && dom.arrayContains(exceptions, attrName)) && el.attributes[i].specified && attrName != "class") {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function elementHasProps(el, props) {
+ for (var p in props) {
+ if (props.hasOwnProperty(p) && el[p] !== props[p]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ var getComputedStyleProperty;
+
+ if (typeof window.getComputedStyle != "undefined") {
+ getComputedStyleProperty = function(el, propName) {
+ return dom.getWindow(el).getComputedStyle(el, null)[propName];
+ };
+ } else if (typeof document.documentElement.currentStyle != "undefined") {
+ getComputedStyleProperty = function(el, propName) {
+ return el.currentStyle[propName];
+ };
+ } else {
+ module.fail("No means of obtaining computed style properties found");
+ }
+
+ var isEditableElement;
+
+ (function() {
+ var testEl = document.createElement("div");
+ if (typeof testEl.isContentEditable == "boolean") {
+ isEditableElement = function(node) {
+ return node && node.nodeType == 1 && node.isContentEditable;
+ };
+ } else {
+ isEditableElement = function(node) {
+ if (!node || node.nodeType != 1 || node.contentEditable == "false") {
+ return false;
+ }
+ return node.contentEditable == "true" || isEditableElement(node.parentNode);
+ };
+ }
+ })();
+
+ function isEditingHost(node) {
+ var parent;
+ return node && node.nodeType == 1
+ && (( (parent = node.parentNode) && parent.nodeType == 9 && parent.designMode == "on")
+ || (isEditableElement(node) && !isEditableElement(node.parentNode)));
+ }
+
+ function isEditable(node) {
+ return (isEditableElement(node) || (node.nodeType != 1 && isEditableElement(node.parentNode))) && !isEditingHost(node);
+ }
+
+ var inlineDisplayRegex = /^inline(-block|-table)?$/i;
+
+ function isNonInlineElement(node) {
+ return node && node.nodeType == 1 && !inlineDisplayRegex.test(getComputedStyleProperty(node, "display"));
+ }
+
+ // White space characters as defined by HTML 4 (http://www.w3.org/TR/html401/struct/text.html)
+ var htmlNonWhiteSpaceRegex = /[^\r\n\t\f \u200B]/;
+
+ function isUnrenderedWhiteSpaceNode(node) {
+ if (node.data.length == 0) {
+ return true;
+ }
+ if (htmlNonWhiteSpaceRegex.test(node.data)) {
+ return false;
+ }
+ var cssWhiteSpace = getComputedStyleProperty(node.parentNode, "whiteSpace");
+ switch (cssWhiteSpace) {
+ case "pre":
+ case "pre-wrap":
+ case "-moz-pre-wrap":
+ return false;
+ case "pre-line":
+ if (/[\r\n]/.test(node.data)) {
+ return false;
+ }
+ }
+
+ // We now have a whitespace-only text node that may be rendered depending on its context. If it is adjacent to a
+ // non-inline element, it will not be rendered. This seems to be a good enough definition.
+ return isNonInlineElement(node.previousSibling) || isNonInlineElement(node.nextSibling);
+ }
+
+ function isSplitPoint(node, offset) {
+ if (dom.isCharacterDataNode(node)) {
+ if (offset == 0) {
+ return !!node.previousSibling;
+ } else if (offset == node.length) {
+ return !!node.nextSibling;
+ } else {
+ return true;
+ }
+ }
+
+ return offset > 0 && offset < node.childNodes.length;
+ }
+
+ function splitNodeAt(node, descendantNode, descendantOffset, rangesToPreserve) {
+ var newNode;
+ var splitAtStart = (descendantOffset == 0);
+
+ if (dom.isAncestorOf(descendantNode, node)) {
+
+ return node;
+ }
+
+ if (dom.isCharacterDataNode(descendantNode)) {
+ if (descendantOffset == 0) {
+ descendantOffset = dom.getNodeIndex(descendantNode);
+ descendantNode = descendantNode.parentNode;
+ } else if (descendantOffset == descendantNode.length) {
+ descendantOffset = dom.getNodeIndex(descendantNode) + 1;
+ descendantNode = descendantNode.parentNode;
+ } else {
+ throw module.createError("splitNodeAt should not be called with offset in the middle of a data node ("
+ + descendantOffset + " in " + descendantNode.data);
+ }
+ }
+
+ if (isSplitPoint(descendantNode, descendantOffset)) {
+ if (!newNode) {
+ newNode = descendantNode.cloneNode(false);
+ if (newNode.id) {
+ newNode.removeAttribute("id");
+ }
+ var child;
+ while ((child = descendantNode.childNodes[descendantOffset])) {
+ newNode.appendChild(child);
+ }
+ dom.insertAfter(newNode, descendantNode);
+ }
+ return (descendantNode == node) ? newNode : splitNodeAt(node, newNode.parentNode, dom.getNodeIndex(newNode), rangesToPreserve);
+ } else if (node != descendantNode) {
+ newNode = descendantNode.parentNode;
+
+ // Work out a new split point in the parent node
+ var newNodeIndex = dom.getNodeIndex(descendantNode);
+
+ if (!splitAtStart) {
+ newNodeIndex++;
+ }
+ return splitNodeAt(node, newNode, newNodeIndex, rangesToPreserve);
+ }
+ return node;
+ }
+
+ function areElementsMergeable(el1, el2) {
+ return el1.tagName == el2.tagName && haveSameClasses(el1, el2) && elementsHaveSameNonClassAttributes(el1, el2);
+ }
+
+ function createAdjacentMergeableTextNodeGetter(forward) {
+ var propName = forward ? "nextSibling" : "previousSibling";
+
+ return function(textNode, checkParentElement) {
+ var el = textNode.parentNode;
+ var adjacentNode = textNode[propName];
+ if (adjacentNode) {
+ // Can merge if the node's previous/next sibling is a text node
+ if (adjacentNode && adjacentNode.nodeType == 3) {
+ return adjacentNode;
+ }
+ } else if (checkParentElement) {
+ // Compare text node parent element with its sibling
+ adjacentNode = el[propName];
+
+ if (adjacentNode && adjacentNode.nodeType == 1 && areElementsMergeable(el, adjacentNode)) {
+ return adjacentNode[forward ? "firstChild" : "lastChild"];
+ }
+ }
+ return null;
+ }
+ }
+
+ var getPreviousMergeableTextNode = createAdjacentMergeableTextNodeGetter(false),
+ getNextMergeableTextNode = createAdjacentMergeableTextNodeGetter(true);
+
+
+ function Merge(firstNode) {
+ this.isElementMerge = (firstNode.nodeType == 1);
+ this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode;
+ this.textNodes = [this.firstTextNode];
+ }
+
+ Merge.prototype = {
+ doMerge: function() {
+ var textBits = [], textNode, parent, text;
+ for (var i = 0, len = this.textNodes.length; i < len; ++i) {
+ textNode = this.textNodes[i];
+ parent = textNode.parentNode;
+ textBits[i] = textNode.data;
+ if (i) {
+ parent.removeChild(textNode);
+ if (!parent.hasChildNodes()) {
+ parent.parentNode.removeChild(parent);
+ }
+ }
+ }
+ this.firstTextNode.data = text = textBits.join("");
+ return text;
+ },
+
+ getLength: function() {
+ var i = this.textNodes.length, len = 0;
+ while (i--) {
+ len += this.textNodes[i].length;
+ }
+ return len;
+ },
+
+ toString: function() {
+ var textBits = [];
+ for (var i = 0, len = this.textNodes.length; i < len; ++i) {
+ textBits[i] = "'" + this.textNodes[i].data + "'";
+ }
+ return "[Merge(" + textBits.join(",") + ")]";
+ }
+ };
+
+ var optionProperties = ["elementTagName", "ignoreWhiteSpace", "applyToEditableOnly"];
+
+ // Allow "class" as a property name in object properties
+ var mappedPropertyNames = {"class" : "className"};
+
+ function CssClassApplier(cssClass, options, tagNames) {
+ this.cssClass = cssClass;
+ var normalize, i, len, propName;
+
+ var elementPropertiesFromOptions = null;
+
+ // Initialize from options object
+ if (typeof options == "object" && options !== null) {
+ tagNames = options.tagNames;
+ elementPropertiesFromOptions = options.elementProperties;
+
+ for (i = 0; propName = optionProperties[i++]; ) {
+ if (options.hasOwnProperty(propName)) {
+ this[propName] = options[propName];
+ }
+ }
+ normalize = options.normalize;
+ } else {
+ normalize = options;
+ }
+
+ // Backwards compatibility: the second parameter can also be a Boolean indicating whether normalization
+ this.normalize = (typeof normalize == "undefined") ? true : normalize;
+
+ // Initialize element properties and attribute exceptions
+ this.attrExceptions = [];
+ var el = document.createElement(this.elementTagName);
+ this.elementProperties = {};
+ for (var p in elementPropertiesFromOptions) {
+ if (elementPropertiesFromOptions.hasOwnProperty(p)) {
+ // Map "class" to "className"
+ if (mappedPropertyNames.hasOwnProperty(p)) {
+ p = mappedPropertyNames[p];
+ }
+ el[p] = elementPropertiesFromOptions[p];
+
+ // Copy the property back from the dummy element so that later comparisons to check whether elements
+ // may be removed are checking against the right value. For example, the href property of an element
+ // returns a fully qualified URL even if it was previously assigned a relative URL.
+ this.elementProperties[p] = el[p];
+ this.attrExceptions.push(p);
+ }
+ }
+
+ this.elementSortedClassName = this.elementProperties.hasOwnProperty("className") ?
+ sortClassName(this.elementProperties.className + " " + cssClass) : cssClass;
+
+ // Initialize tag names
+ this.applyToAnyTagName = false;
+ var type = typeof tagNames;
+ if (type == "string") {
+ if (tagNames == "*") {
+ this.applyToAnyTagName = true;
+ } else {
+ this.tagNames = trim(tagNames.toLowerCase()).split(/\s*,\s*/);
+ }
+ } else if (type == "object" && typeof tagNames.length == "number") {
+ this.tagNames = [];
+ for (i = 0, len = tagNames.length; i < len; ++i) {
+ if (tagNames[i] == "*") {
+ this.applyToAnyTagName = true;
+ } else {
+ this.tagNames.push(tagNames[i].toLowerCase());
+ }
+ }
+ } else {
+ this.tagNames = [this.elementTagName];
+ }
+ }
+
+ CssClassApplier.prototype = {
+ elementTagName: defaultTagName,
+ elementProperties: {},
+ ignoreWhiteSpace: true,
+ applyToEditableOnly: false,
+
+ hasClass: function(node) {
+ return node.nodeType == 1 && dom.arrayContains(this.tagNames, node.tagName.toLowerCase()) && hasClass(node, this.cssClass);
+ },
+
+ getSelfOrAncestorWithClass: function(node) {
+ while (node) {
+ if (this.hasClass(node, this.cssClass)) {
+ return node;
+ }
+ node = node.parentNode;
+ }
+ return null;
+ },
+
+ isModifiable: function(node) {
+ return !this.applyToEditableOnly || isEditable(node);
+ },
+
+ // White space adjacent to an unwrappable node can be ignored for wrapping
+ isIgnorableWhiteSpaceNode: function(node) {
+ return this.ignoreWhiteSpace && node && node.nodeType == 3 && isUnrenderedWhiteSpaceNode(node);
+ },
+
+ // Normalizes nodes after applying a CSS class to a Range.
+ postApply: function(textNodes, range, isUndo) {
+
+ var firstNode = textNodes[0], lastNode = textNodes[textNodes.length - 1];
+
+ var merges = [], currentMerge;
+
+ var rangeStartNode = firstNode, rangeEndNode = lastNode;
+ var rangeStartOffset = 0, rangeEndOffset = lastNode.length;
+
+ var textNode, precedingTextNode;
+
+ for (var i = 0, len = textNodes.length; i < len; ++i) {
+ textNode = textNodes[i];
+ precedingTextNode = getPreviousMergeableTextNode(textNode, !isUndo);
+
+ if (precedingTextNode) {
+ if (!currentMerge) {
+ currentMerge = new Merge(precedingTextNode);
+ merges.push(currentMerge);
+ }
+ currentMerge.textNodes.push(textNode);
+ if (textNode === firstNode) {
+ rangeStartNode = currentMerge.firstTextNode;
+ rangeStartOffset = rangeStartNode.length;
+ }
+ if (textNode === lastNode) {
+ rangeEndNode = currentMerge.firstTextNode;
+ rangeEndOffset = currentMerge.getLength();
+ }
+ } else {
+ currentMerge = null;
+ }
+ }
+
+ // Test whether the first node after the range needs merging
+ var nextTextNode = getNextMergeableTextNode(lastNode, !isUndo);
+
+ if (nextTextNode) {
+ if (!currentMerge) {
+ currentMerge = new Merge(lastNode);
+ merges.push(currentMerge);
+ }
+ currentMerge.textNodes.push(nextTextNode);
+ }
+
+ // Do the merges
+ if (merges.length) {
+
+ for (i = 0, len = merges.length; i < len; ++i) {
+ merges[i].doMerge();
+ }
+
+
+ // Set the range boundaries
+ range.setStart(rangeStartNode, rangeStartOffset);
+ range.setEnd(rangeEndNode, rangeEndOffset);
+ }
+
+ },
+
+ createContainer: function(doc) {
+ var el = doc.createElement(this.elementTagName);
+ api.util.extend(el, this.elementProperties);
+ addClass(el, this.cssClass);
+ return el;
+ },
+
+ applyToTextNode: function(textNode) {
+
+
+ var parent = textNode.parentNode;
+ if (parent.childNodes.length == 1 && dom.arrayContains(this.tagNames, parent.tagName.toLowerCase())) {
+ addClass(parent, this.cssClass);
+ } else {
+ var el = this.createContainer(dom.getDocument(textNode));
+ textNode.parentNode.insertBefore(el, textNode);
+ el.appendChild(textNode);
+ }
+
+ },
+
+ isRemovable: function(el) {
+ return el.tagName.toLowerCase() == this.elementTagName
+ && getSortedClassName(el) == this.elementSortedClassName
+ && elementHasProps(el, this.elementProperties)
+ && !elementHasNonClassAttributes(el, this.attrExceptions)
+ && this.isModifiable(el);
+ },
+
+ undoToTextNode: function(textNode, range, ancestorWithClass) {
+
+ if (!range.containsNode(ancestorWithClass)) {
+ // Split out the portion of the ancestor from which we can remove the CSS class
+ //var parent = ancestorWithClass.parentNode, index = dom.getNodeIndex(ancestorWithClass);
+ var ancestorRange = range.cloneRange();
+ ancestorRange.selectNode(ancestorWithClass);
+
+ if (ancestorRange.isPointInRange(range.endContainer, range.endOffset)/* && isSplitPoint(range.endContainer, range.endOffset)*/) {
+ splitNodeAt(ancestorWithClass, range.endContainer, range.endOffset, [range]);
+ range.setEndAfter(ancestorWithClass);
+ }
+ if (ancestorRange.isPointInRange(range.startContainer, range.startOffset)/* && isSplitPoint(range.startContainer, range.startOffset)*/) {
+ ancestorWithClass = splitNodeAt(ancestorWithClass, range.startContainer, range.startOffset, [range]);
+ }
+ }
+
+ if (this.isRemovable(ancestorWithClass)) {
+ replaceWithOwnChildren(ancestorWithClass);
+ } else {
+ removeClass(ancestorWithClass, this.cssClass);
+ }
+ },
+
+ applyToRange: function(range) {
+ range.splitBoundaries();
+ var textNodes = getEffectiveTextNodes(range);
+
+ if (textNodes.length) {
+ var textNode;
+
+ for (var i = 0, len = textNodes.length; i < len; ++i) {
+ textNode = textNodes[i];
+
+ if (!this.isIgnorableWhiteSpaceNode(textNode) && !this.getSelfOrAncestorWithClass(textNode)
+ && this.isModifiable(textNode)) {
+ this.applyToTextNode(textNode);
+ }
+ }
+ range.setStart(textNodes[0], 0);
+ textNode = textNodes[textNodes.length - 1];
+ range.setEnd(textNode, textNode.length);
+ if (this.normalize) {
+ this.postApply(textNodes, range, false);
+ }
+ }
+ },
+
+ applyToSelection: function(win) {
+
+ win = win || window;
+ var sel = api.getSelection(win);
+
+ var range, ranges = sel.getAllRanges();
+ sel.removeAllRanges();
+ var i = ranges.length;
+ while (i--) {
+ range = ranges[i];
+ this.applyToRange(range);
+ sel.addRange(range);
+ }
+
+ },
+
+ undoToRange: function(range) {
+
+ range.splitBoundaries();
+ var textNodes = getEffectiveTextNodes(range);
+ var textNode, ancestorWithClass;
+ var lastTextNode = textNodes[textNodes.length - 1];
+
+ if (textNodes.length) {
+ for (var i = 0, len = textNodes.length; i < len; ++i) {
+ textNode = textNodes[i];
+ ancestorWithClass = this.getSelfOrAncestorWithClass(textNode);
+ if (ancestorWithClass && this.isModifiable(textNode)) {
+ this.undoToTextNode(textNode, range, ancestorWithClass);
+ }
+
+ // Ensure the range is still valid
+ range.setStart(textNodes[0], 0);
+ range.setEnd(lastTextNode, lastTextNode.length);
+ }
+
+
+
+ if (this.normalize) {
+ this.postApply(textNodes, range, true);
+ }
+ }
+ },
+
+ undoToSelection: function(win) {
+ win = win || window;
+ var sel = api.getSelection(win);
+ var ranges = sel.getAllRanges(), range;
+ sel.removeAllRanges();
+ for (var i = 0, len = ranges.length; i < len; ++i) {
+ range = ranges[i];
+ this.undoToRange(range);
+ sel.addRange(range);
+ }
+ },
+
+ getTextSelectedByRange: function(textNode, range) {
+ var textRange = range.cloneRange();
+ textRange.selectNodeContents(textNode);
+
+ var intersectionRange = textRange.intersection(range);
+ var text = intersectionRange ? intersectionRange.toString() : "";
+ textRange.detach();
+
+ return text;
+ },
+
+ isAppliedToRange: function(range) {
+ if (range.collapsed) {
+ return !!this.getSelfOrAncestorWithClass(range.commonAncestorContainer);
+ } else {
+ var textNodes = range.getNodes( [3] );
+ for (var i = 0, textNode; textNode = textNodes[i++]; ) {
+ if (!this.isIgnorableWhiteSpaceNode(textNode) && rangeSelectsAnyText(range, textNode)
+ && this.isModifiable(textNode) && !this.getSelfOrAncestorWithClass(textNode)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ },
+
+ isAppliedToSelection: function(win) {
+ win = win || window;
+ var sel = api.getSelection(win);
+ var ranges = sel.getAllRanges();
+ var i = ranges.length;
+ while (i--) {
+ if (!this.isAppliedToRange(ranges[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ toggleRange: function(range) {
+ if (this.isAppliedToRange(range)) {
+ this.undoToRange(range);
+ } else {
+ this.applyToRange(range);
+ }
+ },
+
+ toggleSelection: function(win) {
+ if (this.isAppliedToSelection(win)) {
+ this.undoToSelection(win);
+ } else {
+ this.applyToSelection(win);
+ }
+ },
+
+ detach: function() {}
+ };
+
+ function createCssClassApplier(cssClass, options, tagNames) {
+ return new CssClassApplier(cssClass, options, tagNames);
+ }
+
+ CssClassApplier.util = {
+ hasClass: hasClass,
+ addClass: addClass,
+ removeClass: removeClass,
+ hasSameClasses: haveSameClasses,
+ replaceWithOwnChildren: replaceWithOwnChildren,
+ elementsHaveSameNonClassAttributes: elementsHaveSameNonClassAttributes,
+ elementHasNonClassAttributes: elementHasNonClassAttributes,
+ splitNodeAt: splitNodeAt,
+ isEditableElement: isEditableElement,
+ isEditingHost: isEditingHost,
+ isEditable: isEditable
+ };
+
+ api.CssClassApplier = CssClassApplier;
+ api.createCssClassApplier = createCssClassApplier;
+});
View
195 lib/rangy-selectionsaverestore.js
@@ -0,0 +1,195 @@
+/**
+ * @license Selection save and restore module for Rangy.
+ * Saves and restores user selections using marker invisible elements in the DOM.
+ *
+ * Part of Rangy, a cross-browser JavaScript range and selection library
+ * http://code.google.com/p/rangy/
+ *
+ * Depends on Rangy core.
+ *
+ * Copyright 2011, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.2.2
+ * Build date: 13 November 2011
+ */
+rangy.createModule("SaveRestore", function(api, module) {
+ api.requireModules( ["DomUtil", "DomRange", "WrappedRange"] );
+
+ var dom = api.dom;
+
+ var markerTextChar = "\ufeff";
+
+ function gEBI(id, doc) {
+ return (doc || document).getElementById(id);
+ }
+
+ function insertRangeBoundaryMarker(range, atStart) {
+ var markerId = "selectionBoundary_" + (+new Date()) + "_" + ("" + Math.random()).slice(2);
+ var markerEl;
+ var doc = dom.getDocument(range.startContainer);
+
+ // Clone the Range and collapse to the appropriate boundary point
+ var boundaryRange = range.cloneRange();
+ boundaryRange.collapse(atStart);
+
+ // Create the marker element containing a single invisible character using DOM methods and insert it
+ markerEl = doc.createElement("span");
+ markerEl.id = markerId;
+ markerEl.style.lineHeight = "0";
+ markerEl.style.display = "none";
+ markerEl.className = "rangySelectionBoundary";
+ markerEl.appendChild(doc.createTextNode(markerTextChar));
+
+ boundaryRange.insertNode(markerEl);
+ boundaryRange.detach();
+ return markerEl;
+ }
+
+ function setRangeBoundary(doc, range, markerId, atStart) {
+ var markerEl = gEBI(markerId, doc);
+ if (markerEl) {
+ range[atStart ? "setStartBefore" : "setEndBefore"](markerEl);
+ markerEl.parentNode.removeChild(markerEl);
+ } else {
+ module.warn("Marker element has been removed. Cannot restore selection.");
+ }
+ }
+
+ function compareRanges(r1, r2) {
+ return r2.compareBoundaryPoints(r1.START_TO_START, r1);
+ }
+
+ function saveSelection(win) {
+ win = win || window;
+ var doc = win.document;
+ if (!api.isSelectionValid(win)) {
+ module.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus.");
+ return;
+ }
+ var sel = api.getSelection(win);
+ var ranges = sel.getAllRanges();
+ var rangeInfos = [], startEl, endEl, range;
+
+ // Order the ranges by position within the DOM, latest first
+ ranges.sort(compareRanges);
+
+ for (var i = 0, len = ranges.length; i < len; ++i) {
+ range = ranges[i];
+ if (range.collapsed) {
+ endEl = insertRangeBoundaryMarker(range, false);
+ rangeInfos.push({
+ markerId: endEl.id,
+ collapsed: true
+ });
+ } else {
+ endEl = insertRangeBoundaryMarker(range, false);
+ startEl = insertRangeBoundaryMarker(range, true);
+
+ rangeInfos[i] = {
+ startMarkerId: startEl.id,
+ endMarkerId: endEl.id,
+ collapsed: false,
+ backwards: ranges.length == 1 && sel.isBackwards()
+ };
+ }
+ }
+
+ // Now that all the markers are in place and DOM manipulation over, adjust each range's boundaries to lie
+ // between its markers
+ for (i = len - 1; i >= 0; --i) {
+ range = ranges[i];
+ if (range.collapsed) {
+ range.collapseBefore(gEBI(rangeInfos[i].markerId, doc));
+ } else {
+ range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc));
+ range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc));
+ }
+ }
+
+ // Ensure current selection is unaffected
+ sel.setRanges(ranges);
+ return {
+ win: win,
+ doc: doc,
+ rangeInfos: rangeInfos,
+ restored: false
+ };
+ }
+
+ function restoreSelection(savedSelection, preserveDirection) {
+ if (!savedSelection.restored) {
+ var rangeInfos = savedSelection.rangeInfos;
+ var sel = api.getSelection(savedSelection.win);
+ var ranges = [];
+
+ // Ranges are in reverse order of appearance in the DOM. We want to restore earliest first to avoid
+ // normalization affecting previously restored ranges.
+ for (var len = rangeInfos.length, i = len - 1, rangeInfo, range; i >= 0; --i) {
+ rangeInfo = rangeInfos[i];
+ range = api.createRange(savedSelection.doc);
+ if (rangeInfo.collapsed) {
+ var markerEl = gEBI(rangeInfo.markerId, savedSelection.doc);
+ if (markerEl) {
+ markerEl.style.display = "inline";
+ var previousNode = markerEl.previousSibling;
+
+ // Workaround for issue 17
+ if (previousNode && previousNode.nodeType == 3) {
+ markerEl.parentNode.removeChild(markerEl);
+ range.collapseToPoint(previousNode, previousNode.length);
+ } else {
+ range.collapseBefore(markerEl);
+ markerEl.parentNode.removeChild(markerEl);
+ }
+ } else {
+ module.warn("Marker element has been removed. Cannot restore selection.");
+ }
+ } else {
+ setRangeBoundary(savedSelection.doc, range, rangeInfo.startMarkerId, true);
+ setRangeBoundary(savedSelection.doc, range, rangeInfo.endMarkerId, false);
+ }
+
+ // Normalizing range boundaries is only viable if the selection contains only one range. For example,
+ // if the selection contained two ranges that were both contained within the same single text node,
+ // both would alter the same text node when restoring and break the other range.
+ if (len == 1) {
+ range.normalizeBoundaries();
+ }
+ ranges[i] = range;
+ }
+ if (len == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backwards) {
+ sel.removeAllRanges();
+ sel.addRange(ranges[0], true);
+ } else {
+ sel.setRanges(ranges);
+ }
+
+ savedSelection.restored = true;
+ }
+ }
+
+ function removeMarkerElement(doc, markerId) {
+ var markerEl = gEBI(markerId, doc);
+ if (markerEl) {
+ markerEl.parentNode.removeChild(markerEl);
+ }
+ }
+
+ function removeMarkers(savedSelection) {
+ var rangeInfos = savedSelection.rangeInfos;
+ for (var i = 0, len = rangeInfos.length, rangeInfo; i < len; ++i) {
+ rangeInfo = rangeInfos[i];
+ if (rangeInfo.collapsed) {
+ removeMarkerElement(savedSelection.doc, rangeInfo.markerId);
+ } else {
+ removeMarkerElement(savedSelection.doc, rangeInfo.startMarkerId);
+ removeMarkerElement(savedSelection.doc, rangeInfo.endMarkerId);
+ }
+ }
+ }
+
+ api.saveSelection = saveSelection;
+ api.restoreSelection = restoreSelection;
+ api.removeMarkerElement = removeMarkerElement;
+ api.removeMarkers = removeMarkers;
+});
View
300 lib/rangy-serializer.js
@@ -0,0 +1,300 @@
+/**
+ * @license Serializer module for Rangy.
+ * Serializes Ranges and Selections. An example use would be to store a user's selection on a particular page in a
+ * cookie or local storage and restore it on the user's next visit to the same page.
+ *
+ * Part of Rangy, a cross-browser JavaScript range and selection library
+ * http://code.google.com/p/rangy/
+ *
+ * Depends on Rangy core.
+ *
+ * Copyright 2011, Tim Down
+ * Licensed under the MIT license.
+ * Version: 1.2.2
+ * Build date: 13 November 2011
+ */
+rangy.createModule("Serializer", function(api, module) {
+ api.requireModules( ["WrappedSelection", "WrappedRange"] );
+ var UNDEF = "undefined";
+
+ // encodeURIComponent and decodeURIComponent are required for cookie handling
+ if (typeof encodeURIComponent == UNDEF || typeof decodeURIComponent == UNDEF) {
+ module.fail("Global object is missing encodeURIComponent and/or decodeURIComponent method");
+ }
+
+ // Checksum for checking whether range can be serialized
+ var crc32 = (function() {
+ function utf8encode(str) {
+ var utf8CharCodes = [];
+
+ for (var i = 0, len = str.length, c; i < len; ++i) {
+ c = str.charCodeAt(i);
+ if (c < 128) {
+ utf8CharCodes.push(c);
+ } else if (c < 2048) {
+ utf8CharCodes.push((c >> 6) | 192, (c & 63) | 128);
+ } else {
+ utf8CharCodes.push((c >> 12) | 224, ((c >> 6) & 63) | 128, (c & 63) | 128);
+ }
+ }
+ return utf8CharCodes;
+ }
+
+ var cachedCrcTable = null;
+
+ function buildCRCTable() {
+ var table = [];
+ for (var i = 0, j, crc; i < 256; ++i) {
+ crc = i;
+ j = 8;
+ while (j--) {
+ if ((crc & 1) == 1) {
+ crc = (crc >>> 1) ^ 0xEDB88320;
+ } else {
+ crc >>>= 1;
+ }
+ }
+ table[i] = crc >>> 0;
+ }
+ return table;
+ }
+
+ function getCrcTable() {
+ if (!cachedCrcTable) {
+ cachedCrcTable = buildCRCTable();
+ }
+ return cachedCrcTable;
+ }
+
+ return function(str) {
+ var utf8CharCodes = utf8encode(str), crc = -1, crcTable = getCrcTable();
+ for (var i = 0, len = utf8CharCodes.length, y; i < len; ++i) {
+ y = (crc ^ utf8CharCodes[i]) & 0xFF;
+ crc = (crc >>> 8) ^ crcTable[y];
+ }
+ return (crc ^ -1) >>> 0;
+ };
+ })();
+
+ var dom = api.dom;
+
+ function escapeTextForHtml(str) {
+ return str.replace(/</g, "&lt;").replace(/>/g, "&gt;");
+ }
+
+ function nodeToInfoString(node, infoParts) {
+ infoParts = infoParts || [];
+ var nodeType = node.nodeType, children = node.childNodes, childCount = children.length;
+ var nodeInfo = [nodeType, node.nodeName, childCount].join(":");
+ var start = "", end = "";
+ switch (nodeType) {
+ case 3: // Text node
+ start = escapeTextForHtml(node.nodeValue);
+ break;
+ case 8: // Comment
+ start = "<!--" + escapeTextForHtml(node.nodeValue) + "-->";
+ break;
+ default:
+ start = "<" + nodeInfo + ">";
+ end = "</>";
+ break;
+ }
+ if (start) {
+ infoParts.push(start);
+ }
+ for (var i = 0; i < childCount; ++i) {
+ nodeToInfoString(children[i], infoParts);
+ }
+ if (end) {
+ infoParts.push(end);
+ }
+ return infoParts;
+ }
+
+ // Creates a string representation of the specified element's contents that is similar to innerHTML but omits all
+ // attributes and comments and includes child node counts. This is done instead of using innerHTML to work around
+ // IE <= 8's policy of including element properties in attributes, which ruins things by changing an element's
+ // innerHTML whenever the user changes an input within the element.
+ function getElementChecksum(el) {
+ var info = nodeToInfoString(el).join("");
+ return crc32(info).toString(16);
+ }
+
+ function serializePosition(node, offset, rootNode) {
+ var pathBits = [], n = node;
+ rootNode = rootNode || dom.getDocument(node).documentElement;
+ while (n && n != rootNode) {
+ pathBits.push(dom.getNodeIndex(n, true));
+ n = n.parentNode;
+ }
+ return pathBits.join("/") + ":" + offset;
+ }
+
+ function deserializePosition(serialized, rootNode, doc) {
+ if (rootNode) {
+ doc = doc || dom.getDocument(rootNode);
+ } else {
+ doc = doc || document;
+ rootNode = doc.documentElement;
+ }
+ var bits = serialized.split(":");
+ var node = rootNode;
+ var nodeIndices = bits[0] ? bits[0].split("/") : [], i = nodeIndices.length, nodeIndex;
+
+ while (i--) {
+ nodeIndex = parseInt(nodeIndices[i], 10);
+ if (nodeIndex < node.childNodes.length) {
+ node = node.childNodes[parseInt(nodeIndices[i], 10)];
+ } else {
+ throw module.createError("deserializePosition failed: node " + dom.inspectNode(node) +
+ " has no child with index " + nodeIndex + ", " + i);
+ }
+ }
+
+ return new dom.DomPosition(node, parseInt(bits[1], 10));
+ }
+
+ function serializeRange(range, omitChecksum, rootNode) {
+ rootNode = rootNode || api.DomRange.getRangeDocument(range).documentElement;
+ if (!dom.isAncestorOf(rootNode, range.commonAncestorContainer, true)) {
+ throw new Error("serializeRange: range is not wholly contained within specified root node");
+ }
+ var serialized = serializePosition(range.startContainer, range.startOffset, rootNode) + "," +
+ serializePosition(range.endContainer, range.endOffset, rootNode);
+ if (!omitChecksum) {
+ serialized += "{" + getElementChecksum(rootNode) + "}";
+ }
+ return serialized;
+ }
+
+ function deserializeRange(serialized, rootNode, doc) {
+ if (rootNode) {
+ doc = doc || dom.getDocument(rootNode);
+ } else {
+ doc = doc || document;
+ rootNode = doc.documentElement;
+ }
+ var result = /^([^,]+),([^,\{]+)({([^}]+)})?$/.exec(serialized);
+ var checksum = result[4], rootNodeChecksum = getElementChecksum(rootNode);
+ if (checksum && checksum !== getElementChecksum(rootNode)) {
+ throw new Error("deserializeRange: checksums of serialized range root node (" + checksum +
+ ") and target root node (" + rootNodeChecksum + ") do not match");
+ }
+ var start = deserializePosition(result[1], rootNode, doc), end = deserializePosition(result[2], rootNode, doc);
+ var range = api.createRange(doc);
+ range.setStart(start.node, start.offset);
+ range.setEnd(end.node, end.offset);
+ return range;
+ }
+
+ function canDeserializeRange(serialized, rootNode, doc) {
+ if (rootNode) {
+ doc = doc || dom.getDocument(rootNode);
+ } else {
+ doc = doc || document;
+ rootNode = doc.documentElement;
+ }
+ var result = /^([^,]+),([^,]+)({([^}]+)})?$/.exec(serialized);
+ var checksum = result[3];
+ return !checksum || checksum === getElementChecksum(rootNode);
+ }
+
+ function serializeSelection(selection, omitChecksum, rootNode) {
+ selection = selection || api.getSelection();
+ var ranges = selection.getAllRanges(), serializedRanges = [];
+ for (var i = 0, len = ranges.length; i < len; ++i) {
+ serializedRanges[i] = serializeRange(ranges[i], omitChecksum, rootNode);
+ }
+ return serializedRanges.join("|");
+ }
+
+ function deserializeSelection(serialized, rootNode, win) {
+ if (rootNode) {
+ win = win || dom.getWindow(rootNode);
+ } else {
+ win = win || window;
+ rootNode = win.document.documentElement;
+ }
+ var serializedRanges = serialized.split("|");
+ var sel = api.getSelection(win);
+ var ranges = [];
+
+ for (var i = 0, len = serializedRanges.length; i < len; ++i) {
+ ranges[i] = deserializeRange(serializedRanges[i], rootNode, win.document);
+ }
+ sel.setRanges(ranges);
+
+ return sel;
+ }
+
+ function canDeserializeSelection(serialized, rootNode, win) {
+ var doc;
+ if (rootNode) {
+ doc = win ? win.document : dom.getDocument(rootNode);
+ } else {
+ win = win || window;
+ rootNode = win.document.documentElement;
+ }
+ var serializedRanges = serialized.split("|");
+
+ for (var i = 0, len = serializedRanges.length; i < len; ++i) {
+ if (!canDeserializeRange(serializedRanges[i], rootNode, doc)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+
+ var cookieName = "rangySerializedSelection";
+
+ function getSerializedSelectionFromCookie(cookie) {
+ var parts = cookie.split(/[;,]/);
+ for (var i = 0, len = parts.length, nameVal, val; i < len; ++i) {
+ nameVal = parts[i].split("=");
+ if (nameVal[0].replace(/^\s+/, "") == cookieName) {
+ val = nameVal[1];
+ if (val) {
+ return decodeURIComponent(val.replace(/\s+$/, ""));
+ }
+ }
+ }
+ return null;
+ }
+
+ function restoreSelectionFromCookie(win) {
+ win = win || window;
+ var serialized = getSerializedSelectionFromCookie(win.document.cookie);
+ if (serialized) {
+ deserializeSelection(serialized, win.doc)
+ }
+ }
+
+ function saveSelectionCookie(win, props) {
+ win = win || window;
+ props = (typeof props == "object") ? props : {};
+ var expires = props.expires ? ";expires=" + props.expires.toUTCString() : "";
+ var path = props.path ? ";path=" + props.path : "";
+ var domain = props.domain ? ";domain=" + props.domain : "";
+ var secure = props.secure ? ";secure" : "";
+ var serialized = serializeSelection(api.getSelection(win));
+ win.document.cookie = encodeURIComponent(cookieName) + "=" + encodeURIComponent(serialized) + expires + path + domain + secure;
+ }
+
+ api.serializePosition = serializePosition;
+ api.deserializePosition = deserializePosition;
+
+ api.serializeRange = serializeRange;
+ api.deserializeRange = deserializeRange;
+ api.canDeserializeRange = canDeserializeRange;
+
+ api.serializeSelection = serializeSelection;
+ api.deserializeSelection = deserializeSelection;
+ api.canDeserializeSelection = canDeserializeSelection;
+
+ api.restoreSelectionFromCookie = restoreSelectionFromCookie;
+ api.saveSelectionCookie = saveSelectionCookie;
+
+ api.getElementChecksum = getElementChecksum;
+});
View
23 package.json
@@ -0,0 +1,23 @@
+{
+ "author": "Tim Down",
+ "name": "rangy-browser",
+ "description": "A cross-browser DOM range and selection library",
+ "keywords": ["range", "selection", "caret", "DOM"],
+ "version": "1.2.2-1",
+ "homepage": "http://code.google.com/p/rangy/",
+ "licenses": [
+ {
+ "type": "MIT",
+ "url": "http://www.opensource.org/licenses/mit-license.php"
+ }
+ ]
+ "repository": {
+ "type":"svn",
+ "url": "http://rangy.googlecode.com/svn/trunk/"
+ },
+ "directories": {
+ "lib": "./lib",
+ },
+ "dependencies": {},
+ "devDependencies": {}
+}

0 comments on commit 8c6ab6f

Please sign in to comment.
Something went wrong with that request. Please try again.