From 9c6af643b95a83fbe5d34055a6b03a6a286a2468 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Wed, 23 Feb 2011 13:39:16 +0100 Subject: [PATCH] #315: select motif range, ie fix --- wolnelektury/settings.py | 5 + wolnelektury/static/js/ierange-m2.js | 502 ++++++++++++++++++ .../templates/catalogue/book_text.html | 4 + 3 files changed, 511 insertions(+) create mode 100644 wolnelektury/static/js/ierange-m2.js diff --git a/wolnelektury/settings.py b/wolnelektury/settings.py index e85f1893f..49073a18f 100644 --- a/wolnelektury/settings.py +++ b/wolnelektury/settings.py @@ -178,7 +178,12 @@ COMPRESS_JS = { 'book': { 'source_filenames': ('js/jquery.eventdelegation.js', 'js/jquery.scrollto.js', 'js/jquery.highlightfade.js', 'js/book.js',), 'output_filename': 'js/book?.min.js', + }, + 'book_ie': { + 'source_filenames': ('js/ierange-m2.js',), + 'output_filename': 'js/book_ie?.min.js', } + } COMPRESS_VERSION = True diff --git a/wolnelektury/static/js/ierange-m2.js b/wolnelektury/static/js/ierange-m2.js new file mode 100644 index 000000000..e87738b0c --- /dev/null +++ b/wolnelektury/static/js/ierange-m2.js @@ -0,0 +1,502 @@ +/* + DOM Ranges for Internet Explorer (m2) + + Copyright (c) 2009 Tim Cameron Ryan + Released under the MIT/X License + */ + +/* + Range reference: + http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html + http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsRange.cpp + https://developer.mozilla.org/En/DOM:Range + Selection reference: + http://trac.webkit.org/browser/trunk/WebCore/page/DOMSelection.cpp + TextRange reference: + http://msdn.microsoft.com/en-us/library/ms535872.aspx + Other links: + http://jorgenhorstink.nl/test/javascript/range/range.js + http://jorgenhorstink.nl/2006/07/05/dom-range-implementation-in-ecmascript-completed/ + http://dylanschiemann.com/articles/dom2Range/dom2RangeExamples.html +*/ + +//[TODO] better exception support + +(function () { // sandbox + +/* + DOM functions + */ + +var DOMUtils = { + findChildPosition: function (node) { + for (var i = 0; node = node.previousSibling; i++) + continue; + return i; + }, + isDataNode: function (node) { + return node && node.nodeValue !== null && node.data !== null; + }, + isAncestorOf: function (parent, node) { + return !DOMUtils.isDataNode(parent) && + (parent.contains(DOMUtils.isDataNode(node) ? node.parentNode : node) || + node.parentNode == parent); + }, + isAncestorOrSelf: function (root, node) { + return DOMUtils.isAncestorOf(root, node) || root == node; + }, + findClosestAncestor: function (root, node) { + if (DOMUtils.isAncestorOf(root, node)) + while (node && node.parentNode != root) + node = node.parentNode; + return node; + }, + getNodeLength: function (node) { + return DOMUtils.isDataNode(node) ? node.length : node.childNodes.length; + }, + splitDataNode: function (node, offset) { + if (!DOMUtils.isDataNode(node)) + return false; + var newNode = node.cloneNode(false); + node.deleteData(offset, node.length); + newNode.deleteData(0, offset); + node.parentNode.insertBefore(newNode, node.nextSibling); + } +}; + +/* + Text Range utilities + functions to simplify text range manipulation in ie + */ + +var TextRangeUtils = { + convertToDOMRange: function (textRange, document) { + function adoptBoundary(domRange, textRange, bStart) { + // iterate backwards through parent element to find anchor location + var cursorNode = document.createElement('a'), cursor = textRange.duplicate(); + cursor.collapse(bStart); + var parent = cursor.parentElement(); + do { + parent.insertBefore(cursorNode, cursorNode.previousSibling); + cursor.moveToElementText(cursorNode); + } while (cursor.compareEndPoints(bStart ? 'StartToStart' : 'StartToEnd', textRange) > 0 && cursorNode.previousSibling); + + // when we exceed or meet the cursor, we've found the node + if (cursor.compareEndPoints(bStart ? 'StartToStart' : 'StartToEnd', textRange) == -1 && cursorNode.nextSibling) { + // data node + cursor.setEndPoint(bStart ? 'EndToStart' : 'EndToEnd', textRange); + domRange[bStart ? 'setStart' : 'setEnd'](cursorNode.nextSibling, cursor.text.length); + } else { + // element + domRange[bStart ? 'setStartBefore' : 'setEndBefore'](cursorNode); + } + cursorNode.parentNode.removeChild(cursorNode); + } + + // return a DOM range + var domRange = new DOMRange(document); + adoptBoundary(domRange, textRange, true); + adoptBoundary(domRange, textRange, false); + return domRange; + }, + + convertFromDOMRange: function (domRange) { + function adoptEndPoint(textRange, domRange, bStart) { + // find anchor node and offset + var container = domRange[bStart ? 'startContainer' : 'endContainer']; + var offset = domRange[bStart ? 'startOffset' : 'endOffset'], textOffset = 0; + var anchorNode = DOMUtils.isDataNode(container) ? container : container.childNodes[offset]; + var anchorParent = DOMUtils.isDataNode(container) ? container.parentNode : container; + // visible data nodes need a text offset + if (container.nodeType == 3 || container.nodeType == 4) + textOffset = offset; + + // create a cursor element node to position range (since we can't select text nodes) + var cursorNode = domRange._document.createElement('a'); + anchorParent.insertBefore(cursorNode, anchorNode); + var cursor = domRange._document.body.createTextRange(); + cursor.moveToElementText(cursorNode); + cursorNode.parentNode.removeChild(cursorNode); + // move range + textRange.setEndPoint(bStart ? 'StartToStart' : 'EndToStart', cursor); + textRange[bStart ? 'moveStart' : 'moveEnd']('character', textOffset); + } + + // return an IE text range + var textRange = domRange._document.body.createTextRange(); + adoptEndPoint(textRange, domRange, true); + adoptEndPoint(textRange, domRange, false); + return textRange; + } +}; + +/* + DOM Range + */ + +function DOMRange(document) { + // save document parameter + this._document = document; + + // initialize range +//[TODO] this should be located at document[0], document[0] + this.startContainer = this.endContainer = document.body; + this.endOffset = DOMUtils.getNodeLength(document.body); +} +DOMRange.START_TO_START = 0; +DOMRange.START_TO_END = 1; +DOMRange.END_TO_END = 2; +DOMRange.END_TO_START = 3; + +DOMRange.prototype = { + // public properties + startContainer: null, + startOffset: 0, + endContainer: null, + endOffset: 0, + commonAncestorContainer: null, + collapsed: false, + // private properties + _document: null, + + // private methods + _refreshProperties: function () { + // collapsed attribute + this.collapsed = (this.startContainer == this.endContainer && this.startOffset == this.endOffset); + // find common ancestor + var node = this.startContainer; + while (node && node != this.endContainer && !DOMUtils.isAncestorOf(node, this.endContainer)) + node = node.parentNode; + this.commonAncestorContainer = node; + }, + + // range methods +//[TODO] collapse if start is after end, end is before start + setStart: function(container, offset) { + this.startContainer = container; + this.startOffset = offset; + this._refreshProperties(); + }, + setEnd: function(container, offset) { + this.endContainer = container; + this.endOffset = offset; + this._refreshProperties(); + }, + setStartBefore: function (refNode) { + // set start to beore this node + this.setStart(refNode.parentNode, DOMUtils.findChildPosition(refNode)); + }, + setStartAfter: function (refNode) { + // select next sibling + this.setStart(refNode.parentNode, DOMUtils.findChildPosition(refNode) + 1); + }, + setEndBefore: function (refNode) { + // set end to beore this node + this.setEnd(refNode.parentNode, DOMUtils.findChildPosition(refNode)); + }, + setEndAfter: function (refNode) { + // select next sibling + this.setEnd(refNode.parentNode, DOMUtils.findChildPosition(refNode) + 1); + }, + selectNode: function (refNode) { + this.setStartBefore(refNode); + this.setEndAfter(refNode); + }, + selectNodeContents: function (refNode) { + this.setStart(refNode, 0); + this.setEnd(refNode, DOMUtils.getNodeLength(refNode)); + }, + collapse: function (toStart) { + if (toStart) + this.setEnd(this.startContainer, this.startOffset); + else + this.setStart(this.endContainer, this.endOffset); + }, + + // editing methods + cloneContents: function () { + // clone subtree + return (function cloneSubtree(iterator) { + for (var node, frag = document.createDocumentFragment(); node = iterator.next(); ) { + node = node.cloneNode(!iterator.hasPartialSubtree()); + if (iterator.hasPartialSubtree()) + node.appendChild(cloneSubtree(iterator.getSubtreeIterator())); + frag.appendChild(node); + } + return frag; + })(new RangeIterator(this)); + }, + extractContents: function () { + // cache range and move anchor points + var range = this.cloneRange(); + if (this.startContainer != this.commonAncestorContainer) + this.setStartAfter(DOMUtils.findClosestAncestor(this.commonAncestorContainer, this.startContainer)); + this.collapse(true); + // extract range + return (function extractSubtree(iterator) { + for (var node, frag = document.createDocumentFragment(); node = iterator.next(); ) { + iterator.hasPartialSubtree() ? node = node.cloneNode(false) : iterator.remove(); + if (iterator.hasPartialSubtree()) + node.appendChild(extractSubtree(iterator.getSubtreeIterator())); + frag.appendChild(node); + } + return frag; + })(new RangeIterator(range)); + }, + deleteContents: function () { + // cache range and move anchor points + var range = this.cloneRange(); + if (this.startContainer != this.commonAncestorContainer) + this.setStartAfter(DOMUtils.findClosestAncestor(this.commonAncestorContainer, this.startContainer)); + this.collapse(true); + // delete range + (function deleteSubtree(iterator) { + while (iterator.next()) + iterator.hasPartialSubtree() ? deleteSubtree(iterator.getSubtreeIterator()) : iterator.remove(); + })(new RangeIterator(range)); + }, + insertNode: function (newNode) { + // set original anchor and insert node + if (DOMUtils.isDataNode(this.startContainer)) { + DOMUtils.splitDataNode(this.startContainer, this.startOffset); + this.startContainer.parentNode.insertBefore(newNode, this.startContainer.nextSibling); + } else { + this.startContainer.insertBefore(newNode, this.startContainer.childNodes[this.startOffset]); + } + // resync start anchor + this.setStart(this.startContainer, this.startOffset); + }, + surroundContents: function (newNode) { + // extract and surround contents + var content = this.extractContents(); + this.insertNode(newNode); + newNode.appendChild(content); + this.selectNode(newNode); + }, + + // other methods + compareBoundaryPoints: function (how, sourceRange) { + // get anchors + var containerA, offsetA, containerB, offsetB; + switch (how) { + case DOMRange.START_TO_START: + case DOMRange.START_TO_END: + containerA = this.startContainer; + offsetA = this.startOffset; + break; + case DOMRange.END_TO_END: + case DOMRange.END_TO_START: + containerA = this.endContainer; + offsetA = this.endOffset; + break; + } + switch (how) { + case DOMRange.START_TO_START: + case DOMRange.END_TO_START: + containerB = sourceRange.startContainer; + offsetB = sourceRange.startOffset; + break; + case DOMRange.START_TO_END: + case DOMRange.END_TO_END: + containerB = sourceRange.endContainer; + offsetB = sourceRange.endOffset; + break; + } + + // compare + return containerA.sourceIndex < containerB.sourceIndex ? -1 : + containerA.sourceIndex == containerB.sourceIndex ? + offsetA < offsetB ? -1 : offsetA == offsetB ? 0 : 1 + : 1; + }, + cloneRange: function () { + // return cloned range + var range = new DOMRange(this._document); + range.setStart(this.startContainer, this.startOffset); + range.setEnd(this.endContainer, this.endOffset); + return range; + }, + detach: function () { +//[TODO] Releases Range from use to improve performance. + }, + toString: function () { + return TextRangeUtils.convertFromDOMRange(this).text; + }, + createContextualFragment: function (tagString) { + // parse the tag string in a context node + var content = (DOMUtils.isDataNode(this.startContainer) ? this.startContainer.parentNode : this.startContainer).cloneNode(false); + content.innerHTML = tagString; + // return a document fragment from the created node + for (var fragment = this._document.createDocumentFragment(); content.firstChild; ) + fragment.appendChild(content.firstChild); + return fragment; + } +}; + +/* + Range iterator + */ + +function RangeIterator(range) { + this.range = range; + if (range.collapsed) + return; + +//[TODO] ensure this works + // get anchors + var root = range.commonAncestorContainer; + this._next = range.startContainer == root && !DOMUtils.isDataNode(range.startContainer) ? + range.startContainer.childNodes[range.startOffset] : + DOMUtils.findClosestAncestor(root, range.startContainer); + this._end = range.endContainer == root && !DOMUtils.isDataNode(range.endContainer) ? + range.endContainer.childNodes[range.endOffset] : + DOMUtils.findClosestAncestor(root, range.endContainer).nextSibling; +} + +RangeIterator.prototype = { + // public properties + range: null, + // private properties + _current: null, + _next: null, + _end: null, + + // public methods + hasNext: function () { + return !!this._next; + }, + next: function () { + // move to next node + var current = this._current = this._next; + this._next = this._current && this._current.nextSibling != this._end ? + this._current.nextSibling : null; + + // check for partial text nodes + if (DOMUtils.isDataNode(this._current)) { + if (this.range.endContainer == this._current) + (current = current.cloneNode(true)).deleteData(this.range.endOffset, current.length - this.range.endOffset); + if (this.range.startContainer == this._current) + (current = current.cloneNode(true)).deleteData(0, this.range.startOffset); + } + return current; + }, + remove: function () { + // check for partial text nodes + if (DOMUtils.isDataNode(this._current) && + (this.range.startContainer == this._current || this.range.endContainer == this._current)) { + var start = this.range.startContainer == this._current ? this.range.startOffset : 0; + var end = this.range.endContainer == this._current ? this.range.endOffset : this._current.length; + this._current.deleteData(start, end - start); + } else + this._current.parentNode.removeChild(this._current); + }, + hasPartialSubtree: function () { + // check if this node be partially selected + return !DOMUtils.isDataNode(this._current) && + (DOMUtils.isAncestorOrSelf(this._current, this.range.startContainer) || + DOMUtils.isAncestorOrSelf(this._current, this.range.endContainer)); + }, + getSubtreeIterator: function () { + // create a new range + var subRange = new DOMRange(this.range._document); + subRange.selectNodeContents(this._current); + // handle anchor points + if (DOMUtils.isAncestorOrSelf(this._current, this.range.startContainer)) + subRange.setStart(this.range.startContainer, this.range.startOffset); + if (DOMUtils.isAncestorOrSelf(this._current, this.range.endContainer)) + subRange.setEnd(this.range.endContainer, this.range.endOffset); + // return iterator + return new RangeIterator(subRange); + } +}; + +/* + DOM Selection + */ + +//[NOTE] This is a very shallow implementation of the Selection object, based on Webkit's +// implementation and without redundant features. Complete selection manipulation is still +// possible with just removeAllRanges/addRange/getRangeAt. + +function DOMSelection(document) { + // save document parameter + this._document = document; + + // add DOM selection handler + var selection = this; + document.attachEvent('onselectionchange', function () { selection._selectionChangeHandler(); }); +} + +DOMSelection.prototype = { + // public properties + rangeCount: 0, + // private properties + _document: null, + + // private methods + _selectionChangeHandler: function () { + // check if there exists a range + this.rangeCount = this._selectionExists(this._document.selection.createRange()) ? 1 : 0; + }, + _selectionExists: function (textRange) { + // checks if a created text range exists or is an editable cursor + return textRange.compareEndPoints('StartToEnd', textRange) != 0 || + textRange.parentElement().isContentEditable; + }, + + // public methods + addRange: function (range) { + // add range or combine with existing range + var selection = this._document.selection.createRange(), textRange = TextRangeUtils.convertFromDOMRange(range); + if (!this._selectionExists(selection)) + { + // select range + textRange.select(); + } + else + { + // only modify range if it intersects with current range + if (textRange.compareEndPoints('StartToStart', selection) == -1) + if (textRange.compareEndPoints('StartToEnd', selection) > -1 && + textRange.compareEndPoints('EndToEnd', selection) == -1) + selection.setEndPoint('StartToStart', textRange); + else + if (textRange.compareEndPoints('EndToStart', selection) < 1 && + textRange.compareEndPoints('EndToEnd', selection) > -1) + selection.setEndPoint('EndToEnd', textRange); + selection.select(); + } + }, + removeAllRanges: function () { + // remove all ranges + this._document.selection.empty(); + }, + getRangeAt: function (index) { + // return any existing selection, or a cursor position in content editable mode + var textRange = this._document.selection.createRange(); + if (this._selectionExists(textRange)) + return TextRangeUtils.convertToDOMRange(textRange, this._document); + return null; + }, + toString: function () { + // get selection text + return this._document.selection.createRange().text; + } +}; + +/* + scripting hooks + */ + +document.createRange = function () { + return new DOMRange(document); +}; + +var selection = new DOMSelection(document); +window.getSelection = function () { + return selection; +}; + +//[TODO] expose DOMRange/DOMSelection to window.? + +})(); diff --git a/wolnelektury/templates/catalogue/book_text.html b/wolnelektury/templates/catalogue/book_text.html index a6dc6a7d8..cb0081809 100644 --- a/wolnelektury/templates/catalogue/book_text.html +++ b/wolnelektury/templates/catalogue/book_text.html @@ -10,6 +10,10 @@ {% compressed_css "book" %} {% compressed_js "jquery" %} {% compressed_js "book" %} + +