diff --git a/documentation/build/include/plugins.html b/documentation/build/include/plugins.html index c0a04e959b0741907cdb69a72913b4703a1153a6..f6d4886f86444735f5ccb66725b44ede0daa43cc 100644 --- a/documentation/build/include/plugins.html +++ b/documentation/build/include/plugins.html @@ -27,7 +27,7 @@
element, for example), we need a slightly complicated approach to get the boundary's offset in + IE. The facts: + + - Each line break is represented as \r in the text node's data/nodeValue properties + - Each line break is represented as \r\n in the TextRange's 'text' property + - The 'text' property of the TextRange does not contain trailing line breaks + + To get round the problem presented by the final fact above, we can use the fact that TextRange's + moveStart() and moveEnd() methods return the actual number of characters moved, which is not + necessarily the same as the number of characters it was instructed to move. The simplest approach is + to use this to store the characters moved when moving both the start and end of the range to the + start of the document body and subtracting the start offset from the end offset (the + "move-negative-gazillion" method). However, this is extremely slow when the document is large and + the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to + the end of the document) has the same problem. + + Another approach that works is to use moveStart() to move the start boundary of the range up to the + end boundary one character at a time and incrementing a counter with the value returned by the + moveStart() call. However, the check for whether the start boundary has reached the end boundary is + expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected + by the location of the range within the document). + + The approach used below is a hybrid of the two methods above. It uses the fact that a string + containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot + be longer than the text of the TextRange, so the start of the range is moved that length initially + and then a character at a time to make up for any trailing line breaks not contained in the 'text' + property. This has good performance in most situations compared to the previous two methods. + */ + var tempRange = workingRange.duplicate(); + var rangeLength = tempRange.text.replace(/\r\n/g, "\r").length; + + offset = tempRange.moveStart("character", rangeLength); + while ( (comparison = tempRange.compareEndPoints("StartToEnd", tempRange)) == -1) { + offset++; + tempRange.moveStart("character", 1); + } + } else { + offset = workingRange.text.length; + } + boundaryPosition = new DomPosition(boundaryNode, offset); + } else { + + // If the boundary immediately follows a character data node and this is the end boundary, we should favour + // a position within that, and likewise for a start boundary preceding a character data node + previousNode = (isCollapsed || !isStart) && workingNode.previousSibling; + nextNode = (isCollapsed || isStart) && workingNode.nextSibling; + if (nextNode && isCharacterDataNode(nextNode)) { + boundaryPosition = new DomPosition(nextNode, 0); + } else if (previousNode && isCharacterDataNode(previousNode)) { + boundaryPosition = new DomPosition(previousNode, previousNode.data.length); + } else { + boundaryPosition = new DomPosition(containerElement, dom.getNodeIndex(workingNode)); + } + } + + // Clean up + workingNode.parentNode.removeChild(workingNode); + + return { + boundaryPosition: boundaryPosition, + nodeInfo: { + nodeIndex: nodeIndex, + containerElement: containerElement + } + }; + }; + + // Returns a TextRange representing the boundary of a TextRange expressed as a node and an offset within that + // node. This function started out as an optimized version of code found in Tim Cameron Ryan's IERange + // (http://code.google.com/p/ierange/) + var createBoundaryTextRange = function(boundaryPosition, isStart) { + var boundaryNode, boundaryParent, boundaryOffset = boundaryPosition.offset; + var doc = dom.getDocument(boundaryPosition.node); + var workingNode, childNodes, workingRange = getBody(doc).createTextRange(); + var nodeIsDataNode = isCharacterDataNode(boundaryPosition.node); + + if (nodeIsDataNode) { + boundaryNode = boundaryPosition.node; + boundaryParent = boundaryNode.parentNode; + } else { + childNodes = boundaryPosition.node.childNodes; + boundaryNode = (boundaryOffset < childNodes.length) ? childNodes[boundaryOffset] : null; + boundaryParent = boundaryPosition.node; + } + + // Position the range immediately before the node containing the boundary + workingNode = doc.createElement("span"); + + // Making the working element non-empty element persuades IE to consider the TextRange boundary to be within + // the element rather than immediately before or after it + workingNode.innerHTML = "feff;"; + + // insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report + // for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12 + if (boundaryNode) { + boundaryParent.insertBefore(workingNode, boundaryNode); + } else { + boundaryParent.appendChild(workingNode); + } + + workingRange.moveToElementText(workingNode); + workingRange.collapse(!isStart); + + // Clean up + boundaryParent.removeChild(workingNode); + + // Move the working range to the text offset, if required + if (nodeIsDataNode) { + workingRange[isStart ? "moveStart" : "moveEnd"]("character", boundaryOffset); + } + + return workingRange; + }; + + /*------------------------------------------------------------------------------------------------------------*/ + + // This is a wrapper around a TextRange, providing full DOM Range functionality using rangy's DomRange as a + // prototype + + WrappedTextRange = function(textRange) { + this.textRange = textRange; + this.refresh(); + }; + + WrappedTextRange.prototype = new DomRange(document); + + WrappedTextRange.prototype.refresh = function() { + var start, end, startBoundary; + + // TextRange's parentElement() method cannot be trusted. getTextRangeContainerElement() works around that. + var rangeContainerElement = getTextRangeContainerElement(this.textRange); + + if (textRangeIsCollapsed(this.textRange)) { + end = start = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, + true).boundaryPosition; + } else { + startBoundary = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, true, false); + start = startBoundary.boundaryPosition; + + // An optimization used here is that if the start and end boundaries have the same parent element, the + // search scope for the end boundary can be limited to exclude the portion of the element that precedes + // the start boundary + end = getTextRangeBoundaryPosition(this.textRange, rangeContainerElement, false, false, + startBoundary.nodeInfo).boundaryPosition; + } + + this.setStart(start.node, start.offset); + this.setEnd(end.node, end.offset); + }; + + WrappedTextRange.prototype.getName = function() { + return "WrappedTextRange"; + }; + + DomRange.copyComparisonConstants(WrappedTextRange); + + var rangeToTextRange = function(range) { + if (range.collapsed) { + return createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); + } else { + var startRange = createBoundaryTextRange(new DomPosition(range.startContainer, range.startOffset), true); + var endRange = createBoundaryTextRange(new DomPosition(range.endContainer, range.endOffset), false); + var textRange = getBody( DomRange.getRangeDocument(range) ).createTextRange(); + textRange.setEndPoint("StartToStart", startRange); + textRange.setEndPoint("EndToEnd", endRange); + return textRange; + } + }; + + WrappedTextRange.rangeToTextRange = rangeToTextRange; + + WrappedTextRange.prototype.toTextRange = function() { + return rangeToTextRange(this); + }; + + api.WrappedTextRange = WrappedTextRange; + + // IE 9 and above have both implementations and Rangy makes both available. The next few lines sets which + // implementation to use by default. + if (!api.features.implementsDomRange || api.config.preferTextRange) { + // Add WrappedTextRange as the Range property of the global object to allow expression like Range.END_TO_END to work + var globalObj = (function() { return this; })(); + if (typeof globalObj.Range == "undefined") { + globalObj.Range = WrappedTextRange; + } + + api.createNativeRange = function(doc) { + doc = getContentDocument(doc, module, "createNativeRange"); + return getBody(doc).createTextRange(); + }; + + api.WrappedRange = WrappedTextRange; + } + } + + api.createRange = function(doc) { + doc = getContentDocument(doc, module, "createRange"); + return new api.WrappedRange(api.createNativeRange(doc)); + }; + + api.createRangyRange = function(doc) { + doc = getContentDocument(doc, module, "createRangyRange"); + return new DomRange(doc); + }; + + api.createIframeRange = function(iframeEl) { + module.deprecationNotice("createIframeRange()", "createRange(iframeEl)"); + return api.createRange(iframeEl); + }; + + api.createIframeRangyRange = function(iframeEl) { + module.deprecationNotice("createIframeRangyRange()", "createRangyRange(iframeEl)"); + return api.createRangyRange(iframeEl); + }; + + api.addShimListener(function(win) { + var doc = win.document; + if (typeof doc.createRange == "undefined") { + doc.createRange = function() { + return api.createRange(doc); + }; + } + doc = win = null; + }); + }); + + /*----------------------------------------------------------------------------------------------------------------*/ + + // This module creates a selection object wrapper that conforms as closely as possible to the Selection specification + // in the HTML Editing spec (http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections) + api.createCoreModule("WrappedSelection", ["DomRange", "WrappedRange"], function(api, module) { + api.config.checkSelectionRanges = true; + + var BOOLEAN = "boolean"; + var NUMBER = "number"; + var dom = api.dom; + var util = api.util; + var isHostMethod = util.isHostMethod; + var DomRange = api.DomRange; + var WrappedRange = api.WrappedRange; + var DOMException = api.DOMException; + var DomPosition = dom.DomPosition; + var getNativeSelection; + var selectionIsCollapsed; + var features = api.features; + var CONTROL = "Control"; + var getDocument = dom.getDocument; + var getBody = dom.getBody; + var rangesEqual = DomRange.rangesEqual; + + + // Utility function to support direction parameters in the API that may be a string ("backward" or "forward") or a + // Boolean (true for backwards). + function isDirectionBackward(dir) { + return (typeof dir == "string") ? /^backward(s)?$/i.test(dir) : !!dir; + } + + function getWindow(win, methodName) { + if (!win) { + return window; + } else if (dom.isWindow(win)) { + return win; + } else if (win instanceof WrappedSelection) { + return win.win; + } else { + var doc = dom.getContentDocument(win, module, methodName); + return dom.getWindow(doc); + } + } + + function getWinSelection(winParam) { + return getWindow(winParam, "getWinSelection").getSelection(); + } + + function getDocSelection(winParam) { + return getWindow(winParam, "getDocSelection").document.selection; + } + + function winSelectionIsBackward(sel) { + var backward = false; + if (sel.anchorNode) { + backward = (dom.comparePoints(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset) == 1); + } + return backward; + } + + // Test for the Range/TextRange and Selection features required + // Test for ability to retrieve selection + var implementsWinGetSelection = isHostMethod(window, "getSelection"), + implementsDocSelection = util.isHostObject(document, "selection"); + + features.implementsWinGetSelection = implementsWinGetSelection; + features.implementsDocSelection = implementsDocSelection; + + var useDocumentSelection = implementsDocSelection && (!implementsWinGetSelection || api.config.preferTextRange); + + if (useDocumentSelection) { + getNativeSelection = getDocSelection; + api.isSelectionValid = function(winParam) { + var doc = getWindow(winParam, "isSelectionValid").document, nativeSel = doc.selection; + + // Check whether the selection TextRange is actually contained within the correct document + return (nativeSel.type != "None" || getDocument(nativeSel.createRange().parentElement()) == doc); + }; + } else if (implementsWinGetSelection) { + getNativeSelection = getWinSelection; + api.isSelectionValid = function() { + return true; + }; + } else { + module.fail("Neither document.selection or window.getSelection() detected."); + } + + api.getNativeSelection = getNativeSelection; + + var testSelection = getNativeSelection(); + var testRange = api.createNativeRange(document); + var body = getBody(document); + + // Obtaining a range from a selection + var selectionHasAnchorAndFocus = util.areHostProperties(testSelection, + ["anchorNode", "focusNode", "anchorOffset", "focusOffset"]); + + features.selectionHasAnchorAndFocus = selectionHasAnchorAndFocus; + + // Test for existence of native selection extend() method + var selectionHasExtend = isHostMethod(testSelection, "extend"); + features.selectionHasExtend = selectionHasExtend; + + // Test if rangeCount exists + var selectionHasRangeCount = (typeof testSelection.rangeCount == NUMBER); + features.selectionHasRangeCount = selectionHasRangeCount; + + var selectionSupportsMultipleRanges = false; + var collapsedNonEditableSelectionsSupported = true; + + var addRangeBackwardToNative = selectionHasExtend ? + function(nativeSelection, range) { + var doc = DomRange.getRangeDocument(range); + var endRange = api.createRange(doc); + endRange.collapseToPoint(range.endContainer, range.endOffset); + nativeSelection.addRange(getNativeRange(endRange)); + nativeSelection.extend(range.startContainer, range.startOffset); + } : null; + + if (util.areHostMethods(testSelection, ["addRange", "getRangeAt", "removeAllRanges"]) && + typeof testSelection.rangeCount == NUMBER && features.implementsDomRange) { + + (function() { + // Previously an iframe was used but this caused problems in some circumstances in IE, so tests are + // performed on the current document's selection. See issue 109. + + // Note also that if a selection previously existed, it is wiped by these tests. This should usually be fine + // because initialization usually happens when the document loads, but could be a problem for a script that + // loads and initializes Rangy later. If anyone complains, code could be added to save and restore the + // selection. + var sel = window.getSelection(); + if (sel) { + // Store the current selection + var originalSelectionRangeCount = sel.rangeCount; + var selectionHasMultipleRanges = (originalSelectionRangeCount > 1); + var originalSelectionRanges = []; + var originalSelectionBackward = winSelectionIsBackward(sel); + for (var i = 0; i < originalSelectionRangeCount; ++i) { + originalSelectionRanges[i] = sel.getRangeAt(i); + } + + // Create some test elements + var body = getBody(document); + var testEl = body.appendChild( document.createElement("div") ); + testEl.contentEditable = "false"; + var textNode = testEl.appendChild( document.createTextNode("\u00a0\u00a0\u00a0") ); + + // Test whether the native selection will allow a collapsed selection within a non-editable element + var r1 = document.createRange(); + + r1.setStart(textNode, 1); + r1.collapse(true); + sel.addRange(r1); + collapsedNonEditableSelectionsSupported = (sel.rangeCount == 1); + sel.removeAllRanges(); + + // Test whether the native selection is capable of supporting multiple ranges. + if (!selectionHasMultipleRanges) { + // Doing the original feature test here in Chrome 36 (and presumably later versions) prints a + // console error of "Discontiguous selection is not supported." that cannot be suppressed. There's + // nothing we can do about this while retaining the feature test so we have to resort to a browser + // sniff. I'm not happy about it. See + // https://code.google.com/p/chromium/issues/detail?id=399791 + var chromeMatch = window.navigator.appVersion.match(/Chrome\/(.*?) /); + if (chromeMatch && parseInt(chromeMatch[1]) >= 36) { + selectionSupportsMultipleRanges = false; + } else { + var r2 = r1.cloneRange(); + r1.setStart(textNode, 0); + r2.setEnd(textNode, 3); + r2.setStart(textNode, 2); + sel.addRange(r1); + sel.addRange(r2); + selectionSupportsMultipleRanges = (sel.rangeCount == 2); + } + } + + // Clean up + body.removeChild(testEl); + sel.removeAllRanges(); + + for (i = 0; i < originalSelectionRangeCount; ++i) { + if (i == 0 && originalSelectionBackward) { + if (addRangeBackwardToNative) { + addRangeBackwardToNative(sel, originalSelectionRanges[i]); + } else { + api.warn("Rangy initialization: original selection was backwards but selection has been restored forwards because the browser does not support Selection.extend"); + sel.addRange(originalSelectionRanges[i]); + } + } else { + sel.addRange(originalSelectionRanges[i]); + } + } + } + })(); + } + + features.selectionSupportsMultipleRanges = selectionSupportsMultipleRanges; + features.collapsedNonEditableSelectionsSupported = collapsedNonEditableSelectionsSupported; + + // ControlRanges + var implementsControlRange = false, testControlRange; + + if (body && isHostMethod(body, "createControlRange")) { + testControlRange = body.createControlRange(); + if (util.areHostProperties(testControlRange, ["item", "add"])) { + implementsControlRange = true; + } + } + features.implementsControlRange = implementsControlRange; + + // Selection collapsedness + if (selectionHasAnchorAndFocus) { + selectionIsCollapsed = function(sel) { + return sel.anchorNode === sel.focusNode && sel.anchorOffset === sel.focusOffset; + }; + } else { + selectionIsCollapsed = function(sel) { + return sel.rangeCount ? sel.getRangeAt(sel.rangeCount - 1).collapsed : false; + }; + } + + function updateAnchorAndFocusFromRange(sel, range, backward) { + var anchorPrefix = backward ? "end" : "start", focusPrefix = backward ? "start" : "end"; + sel.anchorNode = range[anchorPrefix + "Container"]; + sel.anchorOffset = range[anchorPrefix + "Offset"]; + sel.focusNode = range[focusPrefix + "Container"]; + sel.focusOffset = range[focusPrefix + "Offset"]; + } + + function updateAnchorAndFocusFromNativeSelection(sel) { + var nativeSel = sel.nativeSelection; + sel.anchorNode = nativeSel.anchorNode; + sel.anchorOffset = nativeSel.anchorOffset; + sel.focusNode = nativeSel.focusNode; + sel.focusOffset = nativeSel.focusOffset; + } + + function updateEmptySelection(sel) { + sel.anchorNode = sel.focusNode = null; + sel.anchorOffset = sel.focusOffset = 0; + sel.rangeCount = 0; + sel.isCollapsed = true; + sel._ranges.length = 0; + } + + function getNativeRange(range) { + var nativeRange; + if (range instanceof DomRange) { + nativeRange = api.createNativeRange(range.getDocument()); + nativeRange.setEnd(range.endContainer, range.endOffset); + nativeRange.setStart(range.startContainer, range.startOffset); + } else if (range instanceof WrappedRange) { + nativeRange = range.nativeRange; + } else if (features.implementsDomRange && (range instanceof dom.getWindow(range.startContainer).Range)) { + nativeRange = range; + } + return nativeRange; + } + + function rangeContainsSingleElement(rangeNodes) { + if (!rangeNodes.length || rangeNodes[0].nodeType != 1) { + return false; + } + for (var i = 1, len = rangeNodes.length; i < len; ++i) { + if (!dom.isAncestorOf(rangeNodes[0], rangeNodes[i])) { + return false; + } + } + return true; + } + + function getSingleElementFromRange(range) { + var nodes = range.getNodes(); + if (!rangeContainsSingleElement(nodes)) { + throw module.createError("getSingleElementFromRange: range " + range.inspect() + " did not consist of a single element"); + } + return nodes[0]; + } + + // Simple, quick test which only needs to distinguish between a TextRange and a ControlRange + function isTextRange(range) { + return !!range && typeof range.text != "undefined"; + } + + function updateFromTextRange(sel, range) { + // Create a Range from the selected TextRange + var wrappedRange = new WrappedRange(range); + sel._ranges = [wrappedRange]; + + updateAnchorAndFocusFromRange(sel, wrappedRange, false); + sel.rangeCount = 1; + sel.isCollapsed = wrappedRange.collapsed; + } + + function updateControlSelection(sel) { + // Update the wrapped selection based on what's now in the native selection + sel._ranges.length = 0; + if (sel.docSelection.type == "None") { + updateEmptySelection(sel); + } else { + var controlRange = sel.docSelection.createRange(); + if (isTextRange(controlRange)) { + // This case (where the selection type is "Control" and calling createRange() on the selection returns + // a TextRange) can happen in IE 9. It happens, for example, when all elements in the selected + // ControlRange have been removed from the ControlRange and removed from the document. + updateFromTextRange(sel, controlRange); + } else { + sel.rangeCount = controlRange.length; + var range, doc = getDocument(controlRange.item(0)); + for (var i = 0; i < sel.rangeCount; ++i) { + range = api.createRange(doc); + range.selectNode(controlRange.item(i)); + sel._ranges.push(range); + } + sel.isCollapsed = sel.rangeCount == 1 && sel._ranges[0].collapsed; + updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], false); + } + } + } + + function addRangeToControlSelection(sel, range) { + var controlRange = sel.docSelection.createRange(); + var rangeElement = getSingleElementFromRange(range); + + // Create a new ControlRange containing all the elements in the selected ControlRange plus the element + // contained by the supplied range + var doc = getDocument(controlRange.item(0)); + var newControlRange = getBody(doc).createControlRange(); + for (var i = 0, len = controlRange.length; i < len; ++i) { + newControlRange.add(controlRange.item(i)); + } + try { + newControlRange.add(rangeElement); + } catch (ex) { + throw module.createError("addRange(): Element within the specified Range could not be added to control selection (does it have layout?)"); + } + newControlRange.select(); + + // Update the wrapped selection based on what's now in the native selection + updateControlSelection(sel); + } + + var getSelectionRangeAt; + + if (isHostMethod(testSelection, "getRangeAt")) { + // try/catch is present because getRangeAt() must have thrown an error in some browser and some situation. + // Unfortunately, I didn't write a comment about the specifics and am now scared to take it out. Let that be a + // lesson to us all, especially me. + getSelectionRangeAt = function(sel, index) { + try { + return sel.getRangeAt(index); + } catch (ex) { + return null; + } + }; + } else if (selectionHasAnchorAndFocus) { + getSelectionRangeAt = function(sel) { + var doc = getDocument(sel.anchorNode); + var range = api.createRange(doc); + range.setStartAndEnd(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset); + + // Handle the case when the selection was selected backwards (from the end to the start in the + // document) + if (range.collapsed !== this.isCollapsed) { + range.setStartAndEnd(sel.focusNode, sel.focusOffset, sel.anchorNode, sel.anchorOffset); + } + + return range; + }; + } + + function WrappedSelection(selection, docSelection, win) { + this.nativeSelection = selection; + this.docSelection = docSelection; + this._ranges = []; + this.win = win; + this.refresh(); + } + + WrappedSelection.prototype = api.selectionPrototype; + + function deleteProperties(sel) { + sel.win = sel.anchorNode = sel.focusNode = sel._ranges = null; + sel.rangeCount = sel.anchorOffset = sel.focusOffset = 0; + sel.detached = true; + } + + var cachedRangySelections = []; + + function actOnCachedSelection(win, action) { + var i = cachedRangySelections.length, cached, sel; + while (i--) { + cached = cachedRangySelections[i]; + sel = cached.selection; + if (action == "deleteAll") { + deleteProperties(sel); + } else if (cached.win == win) { + if (action == "delete") { + cachedRangySelections.splice(i, 1); + return true; + } else { + return sel; + } + } + } + if (action == "deleteAll") { + cachedRangySelections.length = 0; + } + return null; + } + + var getSelection = function(win) { + // Check if the parameter is a Rangy Selection object + if (win && win instanceof WrappedSelection) { + win.refresh(); + return win; + } + + win = getWindow(win, "getNativeSelection"); + + var sel = actOnCachedSelection(win); + var nativeSel = getNativeSelection(win), docSel = implementsDocSelection ? getDocSelection(win) : null; + if (sel) { + sel.nativeSelection = nativeSel; + sel.docSelection = docSel; + sel.refresh(); + } else { + sel = new WrappedSelection(nativeSel, docSel, win); + cachedRangySelections.push( { win: win, selection: sel } ); + } + return sel; + }; + + api.getSelection = getSelection; + + api.getIframeSelection = function(iframeEl) { + module.deprecationNotice("getIframeSelection()", "getSelection(iframeEl)"); + return api.getSelection(dom.getIframeWindow(iframeEl)); + }; + + var selProto = WrappedSelection.prototype; + + function createControlSelection(sel, ranges) { + // Ensure that the selection becomes of type "Control" + var doc = getDocument(ranges[0].startContainer); + var controlRange = getBody(doc).createControlRange(); + for (var i = 0, el, len = ranges.length; i < len; ++i) { + el = getSingleElementFromRange(ranges[i]); + try { + controlRange.add(el); + } catch (ex) { + throw module.createError("setRanges(): Element within one of the specified Ranges could not be added to control selection (does it have layout?)"); + } + } + controlRange.select(); + + // Update the wrapped selection based on what's now in the native selection + updateControlSelection(sel); + } + + // Selecting a range + if (!useDocumentSelection && selectionHasAnchorAndFocus && util.areHostMethods(testSelection, ["removeAllRanges", "addRange"])) { + selProto.removeAllRanges = function() { + this.nativeSelection.removeAllRanges(); + updateEmptySelection(this); + }; + + var addRangeBackward = function(sel, range) { + addRangeBackwardToNative(sel.nativeSelection, range); + sel.refresh(); + }; + + if (selectionHasRangeCount) { + selProto.addRange = function(range, direction) { + if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) { + addRangeToControlSelection(this, range); + } else { + if (isDirectionBackward(direction) && selectionHasExtend) { + addRangeBackward(this, range); + } else { + var previousRangeCount; + if (selectionSupportsMultipleRanges) { + previousRangeCount = this.rangeCount; + } else { + this.removeAllRanges(); + previousRangeCount = 0; + } + // Clone the native range so that changing the selected range does not affect the selection. + // This is contrary to the spec but is the only way to achieve consistency between browsers. See + // issue 80. + this.nativeSelection.addRange(getNativeRange(range).cloneRange()); + + // Check whether adding the range was successful + this.rangeCount = this.nativeSelection.rangeCount; + + if (this.rangeCount == previousRangeCount + 1) { + // The range was added successfully + + // Check whether the range that we added to the selection is reflected in the last range extracted from + // the selection + if (api.config.checkSelectionRanges) { + var nativeRange = getSelectionRangeAt(this.nativeSelection, this.rangeCount - 1); + if (nativeRange && !rangesEqual(nativeRange, range)) { + // Happens in WebKit with, for example, a selection placed at the start of a text node + range = new WrappedRange(nativeRange); + } + } + this._ranges[this.rangeCount - 1] = range; + updateAnchorAndFocusFromRange(this, range, selectionIsBackward(this.nativeSelection)); + this.isCollapsed = selectionIsCollapsed(this); + } else { + // The range was not added successfully. The simplest thing is to refresh + this.refresh(); + } + } + } + }; + } else { + selProto.addRange = function(range, direction) { + if (isDirectionBackward(direction) && selectionHasExtend) { + addRangeBackward(this, range); + } else { + this.nativeSelection.addRange(getNativeRange(range)); + this.refresh(); + } + }; + } + + selProto.setRanges = function(ranges) { + if (implementsControlRange && implementsDocSelection && ranges.length > 1) { + createControlSelection(this, ranges); + } else { + this.removeAllRanges(); + for (var i = 0, len = ranges.length; i < len; ++i) { + this.addRange(ranges[i]); + } + } + }; + } else if (isHostMethod(testSelection, "empty") && isHostMethod(testRange, "select") && + implementsControlRange && useDocumentSelection) { + + selProto.removeAllRanges = function() { + // Added try/catch as fix for issue #21 + try { + this.docSelection.empty(); + + // Check for empty() not working (issue #24) + if (this.docSelection.type != "None") { + // Work around failure to empty a control selection by instead selecting a TextRange and then + // calling empty() + var doc; + if (this.anchorNode) { + doc = getDocument(this.anchorNode); + } else if (this.docSelection.type == CONTROL) { + var controlRange = this.docSelection.createRange(); + if (controlRange.length) { + doc = getDocument( controlRange.item(0) ); + } + } + if (doc) { + var textRange = getBody(doc).createTextRange(); + textRange.select(); + this.docSelection.empty(); + } + } + } catch(ex) {} + updateEmptySelection(this); + }; + + selProto.addRange = function(range) { + if (this.docSelection.type == CONTROL) { + addRangeToControlSelection(this, range); + } else { + api.WrappedTextRange.rangeToTextRange(range).select(); + this._ranges[0] = range; + this.rangeCount = 1; + this.isCollapsed = this._ranges[0].collapsed; + updateAnchorAndFocusFromRange(this, range, false); + } + }; + + selProto.setRanges = function(ranges) { + this.removeAllRanges(); + var rangeCount = ranges.length; + if (rangeCount > 1) { + createControlSelection(this, ranges); + } else if (rangeCount) { + this.addRange(ranges[0]); + } + }; + } else { + module.fail("No means of selecting a Range or TextRange was found"); + return false; + } + + selProto.getRangeAt = function(index) { + if (index < 0 || index >= this.rangeCount) { + throw new DOMException("INDEX_SIZE_ERR"); + } else { + // Clone the range to preserve selection-range independence. See issue 80. + return this._ranges[index].cloneRange(); + } + }; + + var refreshSelection; + + if (useDocumentSelection) { + refreshSelection = function(sel) { + var range; + if (api.isSelectionValid(sel.win)) { + range = sel.docSelection.createRange(); + } else { + range = getBody(sel.win.document).createTextRange(); + range.collapse(true); + } + + if (sel.docSelection.type == CONTROL) { + updateControlSelection(sel); + } else if (isTextRange(range)) { + updateFromTextRange(sel, range); + } else { + updateEmptySelection(sel); + } + }; + } else if (isHostMethod(testSelection, "getRangeAt") && typeof testSelection.rangeCount == NUMBER) { + refreshSelection = function(sel) { + if (implementsControlRange && implementsDocSelection && sel.docSelection.type == CONTROL) { + updateControlSelection(sel); + } else { + sel._ranges.length = sel.rangeCount = sel.nativeSelection.rangeCount; + if (sel.rangeCount) { + for (var i = 0, len = sel.rangeCount; i < len; ++i) { + sel._ranges[i] = new api.WrappedRange(sel.nativeSelection.getRangeAt(i)); + } + updateAnchorAndFocusFromRange(sel, sel._ranges[sel.rangeCount - 1], selectionIsBackward(sel.nativeSelection)); + sel.isCollapsed = selectionIsCollapsed(sel); + } else { + updateEmptySelection(sel); + } + } + }; + } else if (selectionHasAnchorAndFocus && typeof testSelection.isCollapsed == BOOLEAN && typeof testRange.collapsed == BOOLEAN && features.implementsDomRange) { + refreshSelection = function(sel) { + var range, nativeSel = sel.nativeSelection; + if (nativeSel.anchorNode) { + range = getSelectionRangeAt(nativeSel, 0); + sel._ranges = [range]; + sel.rangeCount = 1; + updateAnchorAndFocusFromNativeSelection(sel); + sel.isCollapsed = selectionIsCollapsed(sel); + } else { + updateEmptySelection(sel); + } + }; + } else { + module.fail("No means of obtaining a Range or TextRange from the user's selection was found"); + return false; + } + + selProto.refresh = function(checkForChanges) { + var oldRanges = checkForChanges ? this._ranges.slice(0) : null; + var oldAnchorNode = this.anchorNode, oldAnchorOffset = this.anchorOffset; + + refreshSelection(this); + if (checkForChanges) { + // Check the range count first + var i = oldRanges.length; + if (i != this._ranges.length) { + return true; + } + + // Now check the direction. Checking the anchor position is the same is enough since we're checking all the + // ranges after this + if (this.anchorNode != oldAnchorNode || this.anchorOffset != oldAnchorOffset) { + return true; + } + + // Finally, compare each range in turn + while (i--) { + if (!rangesEqual(oldRanges[i], this._ranges[i])) { + return true; + } + } + return false; + } + }; + + // Removal of a single range + var removeRangeManually = function(sel, range) { + var ranges = sel.getAllRanges(); + sel.removeAllRanges(); + for (var i = 0, len = ranges.length; i < len; ++i) { + if (!rangesEqual(range, ranges[i])) { + sel.addRange(ranges[i]); + } + } + if (!sel.rangeCount) { + updateEmptySelection(sel); + } + }; + + if (implementsControlRange && implementsDocSelection) { + selProto.removeRange = function(range) { + if (this.docSelection.type == CONTROL) { + var controlRange = this.docSelection.createRange(); + var rangeElement = getSingleElementFromRange(range); + + // Create a new ControlRange containing all the elements in the selected ControlRange minus the + // element contained by the supplied range + var doc = getDocument(controlRange.item(0)); + var newControlRange = getBody(doc).createControlRange(); + var el, removed = false; + for (var i = 0, len = controlRange.length; i < len; ++i) { + el = controlRange.item(i); + if (el !== rangeElement || removed) { + newControlRange.add(controlRange.item(i)); + } else { + removed = true; + } + } + newControlRange.select(); + + // Update the wrapped selection based on what's now in the native selection + updateControlSelection(this); + } else { + removeRangeManually(this, range); + } + }; + } else { + selProto.removeRange = function(range) { + removeRangeManually(this, range); + }; + } + + // Detecting if a selection is backward + var selectionIsBackward; + if (!useDocumentSelection && selectionHasAnchorAndFocus && features.implementsDomRange) { + selectionIsBackward = winSelectionIsBackward; + + selProto.isBackward = function() { + return selectionIsBackward(this); + }; + } else { + selectionIsBackward = selProto.isBackward = function() { + return false; + }; + } + + // Create an alias for backwards compatibility. From 1.3, everything is "backward" rather than "backwards" + selProto.isBackwards = selProto.isBackward; + + // Selection stringifier + // This is conformant to the old HTML5 selections draft spec but differs from WebKit and Mozilla's implementation. + // The current spec does not yet define this method. + selProto.toString = function() { + var rangeTexts = []; + for (var i = 0, len = this.rangeCount; i < len; ++i) { + rangeTexts[i] = "" + this._ranges[i]; + } + return rangeTexts.join(""); + }; + + function assertNodeInSameDocument(sel, node) { + if (sel.win.document != getDocument(node)) { + throw new DOMException("WRONG_DOCUMENT_ERR"); + } + } + + // No current browser conforms fully to the spec for this method, so Rangy's own method is always used + selProto.collapse = function(node, offset) { + assertNodeInSameDocument(this, node); + var range = api.createRange(node); + range.collapseToPoint(node, offset); + this.setSingleRange(range); + this.isCollapsed = true; + }; + + selProto.collapseToStart = function() { + if (this.rangeCount) { + var range = this._ranges[0]; + this.collapse(range.startContainer, range.startOffset); + } else { + throw new DOMException("INVALID_STATE_ERR"); + } + }; + + selProto.collapseToEnd = function() { + if (this.rangeCount) { + var range = this._ranges[this.rangeCount - 1]; + this.collapse(range.endContainer, range.endOffset); + } else { + throw new DOMException("INVALID_STATE_ERR"); + } + }; + + // The spec is very specific on how selectAllChildren should be implemented so the native implementation is + // never used by Rangy. + selProto.selectAllChildren = function(node) { + assertNodeInSameDocument(this, node); + var range = api.createRange(node); + range.selectNodeContents(node); + this.setSingleRange(range); + }; + + selProto.deleteFromDocument = function() { + // Sepcial behaviour required for IE's control selections + if (implementsControlRange && implementsDocSelection && this.docSelection.type == CONTROL) { + var controlRange = this.docSelection.createRange(); + var element; + while (controlRange.length) { + element = controlRange.item(0); + controlRange.remove(element); + element.parentNode.removeChild(element); + } + this.refresh(); + } else if (this.rangeCount) { + var ranges = this.getAllRanges(); + if (ranges.length) { + this.removeAllRanges(); + for (var i = 0, len = ranges.length; i < len; ++i) { + ranges[i].deleteContents(); + } + // The spec says nothing about what the selection should contain after calling deleteContents on each + // range. Firefox moves the selection to where the final selected range was, so we emulate that + this.addRange(ranges[len - 1]); + } + } + }; + + // The following are non-standard extensions + selProto.eachRange = function(func, returnValue) { + for (var i = 0, len = this._ranges.length; i < len; ++i) { + if ( func( this.getRangeAt(i) ) ) { + return returnValue; + } + } + }; + + selProto.getAllRanges = function() { + var ranges = []; + this.eachRange(function(range) { + ranges.push(range); + }); + return ranges; + }; + + selProto.setSingleRange = function(range, direction) { + this.removeAllRanges(); + this.addRange(range, direction); + }; + + selProto.callMethodOnEachRange = function(methodName, params) { + var results = []; + this.eachRange( function(range) { + results.push( range[methodName].apply(range, params) ); + } ); + return results; + }; + + function createStartOrEndSetter(isStart) { + return function(node, offset) { + var range; + if (this.rangeCount) { + range = this.getRangeAt(0); + range["set" + (isStart ? "Start" : "End")](node, offset); + } else { + range = api.createRange(this.win.document); + range.setStartAndEnd(node, offset); + } + this.setSingleRange(range, this.isBackward()); + }; + } + + selProto.setStart = createStartOrEndSetter(true); + selProto.setEnd = createStartOrEndSetter(false); + + // Add select() method to Range prototype. Any existing selection will be removed. + api.rangePrototype.select = function(direction) { + getSelection( this.getDocument() ).setSingleRange(this, direction); + }; + + selProto.changeEachRange = function(func) { + var ranges = []; + var backward = this.isBackward(); + + this.eachRange(function(range) { + func(range); + ranges.push(range); + }); + + this.removeAllRanges(); + if (backward && ranges.length == 1) { + this.addRange(ranges[0], "backward"); + } else { + this.setRanges(ranges); + } + }; + + selProto.containsNode = function(node, allowPartial) { + return this.eachRange( function(range) { + return range.containsNode(node, allowPartial); + }, true ) || false; + }; + + selProto.getBookmark = function(containerNode) { + return { + backward: this.isBackward(), + rangeBookmarks: this.callMethodOnEachRange("getBookmark", [containerNode]) + }; + }; + + selProto.moveToBookmark = function(bookmark) { + var selRanges = []; + for (var i = 0, rangeBookmark, range; rangeBookmark = bookmark.rangeBookmarks[i++]; ) { + range = api.createRange(this.win); + range.moveToBookmark(rangeBookmark); + selRanges.push(range); + } + if (bookmark.backward) { + this.setSingleRange(selRanges[0], "backward"); + } else { + this.setRanges(selRanges); + } + }; + + selProto.toHtml = function() { + var rangeHtmls = []; + this.eachRange(function(range) { + rangeHtmls.push( DomRange.toHtml(range) ); + }); + return rangeHtmls.join(""); + }; + + if (features.implementsTextRange) { + selProto.getNativeTextRange = function() { + var sel, textRange; + if ( (sel = this.docSelection) ) { + var range = sel.createRange(); + if (isTextRange(range)) { + return range; + } else { + throw module.createError("getNativeTextRange: selection is a control selection"); + } + } else if (this.rangeCount > 0) { + return api.WrappedTextRange.rangeToTextRange( this.getRangeAt(0) ); + } else { + throw module.createError("getNativeTextRange: selection contains no range"); + } + }; + } + + function inspect(sel) { + var rangeInspects = []; + var anchor = new DomPosition(sel.anchorNode, sel.anchorOffset); + var focus = new DomPosition(sel.focusNode, sel.focusOffset); + var name = (typeof sel.getName == "function") ? sel.getName() : "Selection"; + + if (typeof sel.rangeCount != "undefined") { + for (var i = 0, len = sel.rangeCount; i < len; ++i) { + rangeInspects[i] = DomRange.inspect(sel.getRangeAt(i)); + } + } + return "[" + name + "(Ranges: " + rangeInspects.join(", ") + + ")(anchor: " + anchor.inspect() + ", focus: " + focus.inspect() + "]"; + } + + selProto.getName = function() { + return "WrappedSelection"; + }; + + selProto.inspect = function() { + return inspect(this); + }; + + selProto.detach = function() { + actOnCachedSelection(this.win, "delete"); + deleteProperties(this); + }; + + WrappedSelection.detachAll = function() { + actOnCachedSelection(null, "deleteAll"); + }; + + WrappedSelection.inspect = inspect; + WrappedSelection.isDirectionBackward = isDirectionBackward; + + api.Selection = WrappedSelection; + + api.selectionPrototype = selProto; + + api.addShimListener(function(win) { + if (typeof win.getSelection == "undefined") { + win.getSelection = function() { + return getSelection(win); + }; + } + win = null; + }); + }); + + + /*----------------------------------------------------------------------------------------------------------------*/ + + return api; +}, this);;/** + * 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 2014, Tim Down + * Licensed under the MIT license. + * Version: 1.3alpha.20140804 + * Build date: 4 August 2014 + */ +(function(factory, global) { + if (typeof define == "function" && define.amd) { + // AMD. Register as an anonymous module with a dependency on Rangy. + define(["rangy"], factory); + /* + } else if (typeof exports == "object") { + // Node/CommonJS style for Browserify + module.exports = factory; + */ + } else { + // No AMD or CommonJS support so we use the rangy global variable + factory(global.rangy); + } +})(function(rangy) { + rangy.createModule("SaveRestore", ["WrappedRange"], function(api, module) { + 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); + 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 saveRange(range, backward) { + var startEl, endEl, doc = api.DomRange.getRangeDocument(range), text = range.toString(); + + if (range.collapsed) { + endEl = insertRangeBoundaryMarker(range, false); + return { + document: doc, + markerId: endEl.id, + collapsed: true + }; + } else { + endEl = insertRangeBoundaryMarker(range, false); + startEl = insertRangeBoundaryMarker(range, true); + + return { + document: doc, + startMarkerId: startEl.id, + endMarkerId: endEl.id, + collapsed: false, + backward: backward, + toString: function() { + return "original text: '" + text + "', new text: '" + range.toString() + "'"; + } + }; + } + } + + function restoreRange(rangeInfo, normalize) { + var doc = rangeInfo.document; + if (typeof normalize == "undefined") { + normalize = true; + } + var range = api.createRange(doc); + if (rangeInfo.collapsed) { + var markerEl = gEBI(rangeInfo.markerId, 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(doc, range, rangeInfo.startMarkerId, true); + setRangeBoundary(doc, range, rangeInfo.endMarkerId, false); + } + + if (normalize) { + range.normalizeBoundaries(); + } + + return range; + } + + function saveRanges(ranges, backward) { + var rangeInfos = [], range, doc; + + // Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched + ranges = ranges.slice(0); + ranges.sort(compareRanges); + + for (var i = 0, len = ranges.length; i < len; ++i) { + rangeInfos[i] = saveRange(ranges[i], backward); + } + + // 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]; + doc = api.DomRange.getRangeDocument(range); + if (range.collapsed) { + range.collapseAfter(gEBI(rangeInfos[i].markerId, doc)); + } else { + range.setEndBefore(gEBI(rangeInfos[i].endMarkerId, doc)); + range.setStartAfter(gEBI(rangeInfos[i].startMarkerId, doc)); + } + } + + return rangeInfos; + } + + function saveSelection(win) { + 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 null; + } + var sel = api.getSelection(win); + var ranges = sel.getAllRanges(); + var backward = (ranges.length == 1 && sel.isBackward()); + + var rangeInfos = saveRanges(ranges, backward); + + // Ensure current selection is unaffected + if (backward) { + sel.setSingleRange(ranges[0], "backward"); + } else { + sel.setRanges(ranges); + } + + return { + win: win, + rangeInfos: rangeInfos, + restored: false + }; + } + + function restoreRanges(rangeInfos) { + 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. + var rangeCount = rangeInfos.length; + + for (var i = rangeCount - 1; i >= 0; i--) { + ranges[i] = restoreRange(rangeInfos[i], true); + } + + return ranges; + } + + function restoreSelection(savedSelection, preserveDirection) { + if (!savedSelection.restored) { + var rangeInfos = savedSelection.rangeInfos; + var sel = api.getSelection(savedSelection.win); + var ranges = restoreRanges(rangeInfos), rangeCount = rangeInfos.length; + + if (rangeCount == 1 && preserveDirection && api.features.selectionHasExtend && rangeInfos[0].backward) { + 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.util.extend(api, { + saveRange: saveRange, + restoreRange: restoreRange, + saveRanges: saveRanges, + restoreRanges: restoreRanges, + saveSelection: saveSelection, + restoreSelection: restoreSelection, + removeMarkerElement: removeMarkerElement, + removeMarkers: removeMarkers + }); + }); + +}, this);;/* + Base.js, version 1.1a + Copyright 2006-2010, Dean Edwards + License: http://www.opensource.org/licenses/mit-license.php +*/ + +var Base = function() { + // dummy +}; + +Base.extend = function(_instance, _static) { // subclass + var extend = Base.prototype.extend; + + // build the prototype + Base._prototyping = true; + var proto = new this; + extend.call(proto, _instance); + proto.base = function() { + // call this method from any other method to invoke that method's ancestor + }; + delete Base._prototyping; + + // create the wrapper for the constructor function + //var constructor = proto.constructor.valueOf(); //-dean + var constructor = proto.constructor; + var klass = proto.constructor = function() { + if (!Base._prototyping) { + if (this._constructing || this.constructor == klass) { // instantiation + this._constructing = true; + constructor.apply(this, arguments); + delete this._constructing; + } else if (arguments[0] != null) { // casting + return (arguments[0].extend || extend).call(arguments[0], proto); + } + } + }; + + // build the class interface + klass.ancestor = this; + klass.extend = this.extend; + klass.forEach = this.forEach; + klass.implement = this.implement; + klass.prototype = proto; + klass.toString = this.toString; + klass.valueOf = function(type) { + //return (type == "object") ? klass : constructor; //-dean + return (type == "object") ? klass : constructor.valueOf(); + }; + extend.call(klass, _static); + // class initialisation + if (typeof klass.init == "function") klass.init(); + return klass; +}; + +Base.prototype = { + extend: function(source, value) { + if (arguments.length > 1) { // extending with a name/value pair + var ancestor = this[source]; + if (ancestor && (typeof value == "function") && // overriding a method? + // the valueOf() comparison is to avoid circular references + (!ancestor.valueOf || ancestor.valueOf() != value.valueOf()) && + /\bbase\b/.test(value)) { + // get the underlying method + var method = value.valueOf(); + // override + value = function() { + var previous = this.base || Base.prototype.base; + this.base = ancestor; + var returnValue = method.apply(this, arguments); + this.base = previous; + return returnValue; + }; + // point to the underlying method + value.valueOf = function(type) { + return (type == "object") ? value : method; + }; + value.toString = Base.toString; + } + this[source] = value; + } else if (source) { // extending with an object literal + var extend = Base.prototype.extend; + // if this object has a customised extend method then use it + if (!Base._prototyping && typeof this != "function") { + extend = this.extend || extend; + } + var proto = {toSource: null}; + // do the "toString" and other methods manually + var hidden = ["constructor", "toString", "valueOf"]; + // if we are prototyping then include the constructor + var i = Base._prototyping ? 0 : 1; + while (key = hidden[i++]) { + if (source[key] != proto[key]) { + extend.call(this, key, source[key]); + + } + } + // copy each of the source object's properties to this object + for (var key in source) { + if (!proto[key]) extend.call(this, key, source[key]); + } + } + return this; + } +}; + +// initialise +Base = Base.extend({ + constructor: function() { + this.extend(arguments[0]); + } +}, { + ancestor: Object, + version: "1.1", + + forEach: function(object, block, context) { + for (var key in object) { + if (this.prototype[key] === undefined) { + block.call(context, object[key], key, object); + } + } + }, + + implement: function() { + for (var i = 0; i < arguments.length; i++) { + if (typeof arguments[i] == "function") { + // if it's a function, call it + arguments[i](this.prototype); + } else { + // add the interface using the extend method + this.prototype.extend(arguments[i]); + } + } + return this; + }, + + toString: function() { + return String(this.valueOf()); + } +});;/** + * Detect browser support for specific features + */ +wysihtml5.browser = (function() { + var userAgent = navigator.userAgent, + testElement = document.createElement("div"), + // Browser sniffing is unfortunately needed since some behaviors are impossible to feature detect + isGecko = userAgent.indexOf("Gecko") !== -1 && userAgent.indexOf("KHTML") === -1, + isWebKit = userAgent.indexOf("AppleWebKit/") !== -1, + isChrome = userAgent.indexOf("Chrome/") !== -1, + isOpera = userAgent.indexOf("Opera/") !== -1; + + function iosVersion(userAgent) { + return +((/ipad|iphone|ipod/.test(userAgent) && userAgent.match(/ os (\d+).+? like mac os x/)) || [undefined, 0])[1]; + } + + function androidVersion(userAgent) { + return +(userAgent.match(/android (\d+)/) || [undefined, 0])[1]; + } + + function isIE(version, equation) { + var rv = -1, + re; + + if (navigator.appName == 'Microsoft Internet Explorer') { + re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); + } else if (navigator.appName == 'Netscape') { + re = new RegExp("Trident/.*rv:([0-9]{1,}[\.0-9]{0,})"); + } + + if (re && re.exec(navigator.userAgent) != null) { + rv = parseFloat(RegExp.$1); + } + + if (rv === -1) { return false; } + if (!version) { return true; } + if (!equation) { return version === rv; } + if (equation === "<") { return version < rv; } + if (equation === ">") { return version > rv; } + if (equation === "<=") { return version <= rv; } + if (equation === ">=") { return version >= rv; } + } + + return { + // Static variable needed, publicly accessible, to be able override it in unit tests + USER_AGENT: userAgent, + + /** + * Exclude browsers that are not capable of displaying and handling + * contentEditable as desired: + * - iPhone, iPad (tested iOS 4.2.2) and Android (tested 2.2) refuse to make contentEditables focusable + * - IE < 8 create invalid markup and crash randomly from time to time + * + * @return {Boolean} + */ + supported: function() { + var userAgent = this.USER_AGENT.toLowerCase(), + // Essential for making html elements editable + hasContentEditableSupport = "contentEditable" in testElement, + // Following methods are needed in order to interact with the contentEditable area + hasEditingApiSupport = document.execCommand && document.queryCommandSupported && document.queryCommandState, + // document selector apis are only supported by IE 8+, Safari 4+, Chrome and Firefox 3.5+ + hasQuerySelectorSupport = document.querySelector && document.querySelectorAll, + // contentEditable is unusable in mobile browsers (tested iOS 4.2.2, Android 2.2, Opera Mobile, WebOS 3.05) + isIncompatibleMobileBrowser = (this.isIos() && iosVersion(userAgent) < 5) || (this.isAndroid() && androidVersion(userAgent) < 4) || userAgent.indexOf("opera mobi") !== -1 || userAgent.indexOf("hpwos/") !== -1; + return hasContentEditableSupport + && hasEditingApiSupport + && hasQuerySelectorSupport + && !isIncompatibleMobileBrowser; + }, + + isTouchDevice: function() { + return this.supportsEvent("touchmove"); + }, + + isIos: function() { + return (/ipad|iphone|ipod/i).test(this.USER_AGENT); + }, + + isAndroid: function() { + return this.USER_AGENT.indexOf("Android") !== -1; + }, + + /** + * Whether the browser supports sandboxed iframes + * Currently only IE 6+ offers such feature