UNPKG

doevisualizations

Version:

Data Visualization Library based on RequireJS and D3.js (v4+)

922 lines (871 loc) 28 kB
steal('jquery', 'jquerypp/dom/compare', function ($) { /** * @page jQuery.fn.range * @parent jQuery.Range * @signature `jQuery.fn.range()` * * @body * Returns a new [jQuery.Range] instance for the first selected element. * * $('#content').range() //-> range * * @return {jQuery.Range} A jQuery.Range instance for the selected element */ $.fn.range = function () { return $.Range(this[0]) } var convertType = function (type) { return type.replace(/([a-z])([a-z]+)/gi,function (all, first, next) { return first + next.toLowerCase() }).replace(/_/g, ""); }, // reverses things like START_TO_END into END_TO_START reverse = function (type) { return type.replace(/^([a-z]+)_TO_([a-z]+)/i, function (all, first, last) { return last + "_TO_" + first; }); }, getWindow = function (element) { return element ? element.ownerDocument.defaultView || element.ownerDocument.parentWindow : window }, bisect = function (el, start, end) { //split the start and end ... figure out who is touching ... if (end - start == 1) { return } }, support = {}; /** * * Depending on the object passed, the selected text will be different. * * @param {TextRange|HTMLElement|Point} [range] An object specifiying a * range. Depending on the object, the selected text will be different. $.Range supports the * following types * * - __undefined or null__ - returns a range with nothing selected * - __HTMLElement__ - returns a range with the node's text selected * - __Point__ - returns a range at the point on the screen. The point can be specified like: * * //client coordinates * {clientX: 200, clientY: 300} * * //page coordinates * {pageX: 200, pageY: 300} * {top: 200, left: 300} * * - __TextRange__ a raw text range object. */ $.Range = function (range) { // If it's called w/o new, call it with new! if (this.constructor !== $.Range) { return new $.Range(range); } // If we are passed a jQuery-wrapped element, get the raw element if (range && range.jquery) { range = range[0]; } // If we have an element, or nothing if (!range || range.nodeType) { // create a range this.win = getWindow(range) if (this.win.document.createRange) { this.range = this.win.document.createRange() } else if(this.win && this.win.document.body && this.win.document.body.createTextRange) { this.range = this.win.document.body.createTextRange() } // if we have an element, make the range select it if (range) { this.select(range) } } // if we are given a point else if (range.clientX != null || range.pageX != null || range.left != null) { this.moveToPoint(range); } // if we are given a touch event else if (range.originalEvent && range.originalEvent.touches && range.originalEvent.touches.length) { this.moveToPoint(range.originalEvent.touches[0]) } // if we are a normal event else if (range.originalEvent && range.originalEvent.changedTouches && range.originalEvent.changedTouches.length) { this.moveToPoint(range.originalEvent.changedTouches[0]) } // given a TextRange or something else? else { this.range = range; } }; /** * @add jQuery.Range */ // /** * @static */ $.Range. /** * @function jQuery.Range.static.current current * @signature `jQuery.Range.current([el])` * * @body * `$.Range.current([element])` returns the currently selected range * (using [window.getSelection](https://developer.mozilla.org/en/nsISelection)). * * var range = $.Range.current() * range.start().offset // -> selection start offset * range.end().offset // -> selection end offset * * @param {HTMLElement} [el] an optional element used to get selection for a given window. * @return {jQuery.Range} The range instance. */ current = function (el) { var win = getWindow(el), selection; if (win.getSelection) { // If we can get the selection selection = win.getSelection() return new $.Range(selection.rangeCount ? selection.getRangeAt(0) : win.document.createRange()) } else { // Otherwise use document.selection return new $.Range(win.document.selection.createRange()); } }; $.extend($.Range.prototype, /** @prototype **/ { /** * @function jQuery.Range.prototype.moveToPoint moveToPoint * @signature `range.moveToPoint([point])` * * @body * Moves the range end and start position to a specific point. * A point can be specified like: * * //client coordinates * {clientX: 200, clientY: 300} * * //page coordinates * {pageX: 200, pageY: 300} * {top: 200, left: 300} * * @param {Point} point The point to move the range to * @return {jQuery.Range} */ moveToPoint : function (point) { var clientX = point.clientX, clientY = point.clientY if (!clientX) { var off = scrollOffset(); clientX = (point.pageX || point.left || 0 ) - off.left; clientY = (point.pageY || point.top || 0 ) - off.top; } if (support.moveToPoint) { this.range = $.Range().range this.range.moveToPoint(clientX, clientY); return this; } // it's some text node in this range ... var parent = document.elementFromPoint(clientX, clientY); //typically it will be 'on' text for (var n = 0; n < parent.childNodes.length; n++) { var node = parent.childNodes[n]; if (node.nodeType === 3 || node.nodeType === 4) { var range = $.Range(node), length = range.toString().length; // now lets start moving the end until the boundingRect is within our range for (var i = 1; i < length + 1; i++) { var rect = range.end(i).rect(); if (rect.left <= clientX && rect.left + rect.width >= clientX && rect.top <= clientY && rect.top + rect.height >= clientY) { range.start(i - 1); this.range = range.range; return this; } } } } // if not 'on' text, recursively go through and find out when we shift to next // 'line' var previous; iterate(parent.childNodes, function (textNode) { var range = $.Range(textNode); if (range.rect().top > point.clientY) { return false; } else { previous = range; } }); if (previous) { previous.start(previous.toString().length); this.range = previous.range; } else { this.range = $.Range(parent).range } }, window : function () { return this.win || window; }, /** * @function jQuery.Range.prototype.overlaps overlaps * @signature `range.overlaps([elRange])` * * @body * returns `true` if any portion of these two ranges overlap. * * var foo = document.getElementById('foo'); * * $.Range(foo.childNodes[0]).overlaps(foo.childNodes[1]) //-> false * * @param {jQuery.Range} elRange The range to compare * @return {Boolean} true if part of the ranges overlap, false if otherwise. */ overlaps : function (elRange) { if (elRange.nodeType) { elRange = $.Range(elRange).select(elRange); } //if the start is within the element ... var startToStart = this.compare("START_TO_START", elRange), endToEnd = this.compare("END_TO_END", elRange) // if we wrap elRange if (startToStart <= 0 && endToEnd >= 0) { return true; } // if our start is inside of it if (startToStart >= 0 && this.compare("START_TO_END", elRange) <= 0) { return true; } // if our end is inside of elRange if (this.compare("END_TO_START", elRange) >= 0 && endToEnd <= 0) { return true; } return false; }, /** * @function jQuery.Range.prototype.collapse collapse * @signature `jQuery(el).range.collapse([toStart])` * * @body * Collapses a range to one of its boundary points. * See [range.collapse](https://developer.mozilla.org/en/DOM/range.collapse). * * $('#foo').range().collapse() * * @param {Boolean} [toStart] true if to the start of the range, false if to the * end. Defaults to false. * @return {jQuery.Range} returns the range for chaining. */ collapse : function (toStart) { this.range.collapse(toStart === undefined ? true : toStart); return this; }, /** * @function jQuery.Range.prototype.toString toString * @signature `range.toString()` * * @body * Returns the text of the range. * * currentText = $.Range.current().toString() * * @return {String} The text content of this range */ toString : function () { return typeof this.range.text == "string" ? this.range.text : this.range.toString(); }, /** * @function jQuery.Range.prototype.start start * @signature `range.start()` * * @body * Gets or sets the start of the range. * * If a value is not provided, start returns the range's starting container and offset like: * * $('#foo').range().start() * //-> {container: fooElement, offset: 0 } * * If a set value is provided, it can set the range. The start of the range is set differently * depending on the type of set value: * * - __Object__ - an object with the new starting container and offset like * * $.Range().start({container: $('#foo')[0], offset: 20}) * * - __Number__ - the new offset value. The container is kept the same. * * - __String__ - adjusts the offset by converting the string offset to a number and adding it to the current * offset. For example, the following moves the offset forward four characters: * * $('#foo').range().start("+4") * * Note that `start` can return a text node. To get the containing element use this: * * var startNode = range.start().container; * if( startNode.nodeType === Node.TEXT_NODE || * startNode.nodeType === Node.CDATA_SECTION_NODE ) { * startNode = startNode.parentNode; * } * $(startNode).addClass('highlight'); * * @param {Object|String|Number} [set] a set value if setting the start of the range or nothing if reading it. * @return {jQuery.Range|Object} if setting the start, the range is returned for chaining, otherwise, the * start offset and container are returned. */ start : function (set) { // return start if (set === undefined) { if (this.range.startContainer) { return { container : this.range.startContainer, offset : this.range.startOffset } } else { // Get the start parent element var start = this.clone().collapse().parent(); // used to get the start element offset var startRange = $.Range(start).select(start).collapse(); startRange.move("END_TO_START", this); return { container : start, offset : startRange.toString().length } } } else { if (this.range.setStart) { // supports setStart if (typeof set == 'number') { this.range.setStart(this.range.startContainer, set) } else if (typeof set == 'string') { var res = callMove(this.range.startContainer, this.range.startOffset, parseInt(set, 10)) this.range.setStart(res.node, res.offset); } else { this.range.setStart(set.container, set.offset) } } else { if (typeof set == "string") { this.range.moveStart('character', parseInt(set, 10)) } else { // get the current end container var container = this.start().container, offset if (typeof set == "number") { offset = set } else { container = set.container offset = set.offset } var newPoint = $.Range(container).collapse(); //move it over offset characters newPoint.range.move(offset); this.move("START_TO_START", newPoint); } } return this; } }, /** * @function jQuery.Range.prototype.end end * @signature `range.end([end])` * * @body * Gets or sets the end of the range. * It takes similar options as [jQuery.Range::start start]: * * - __Object__ - an object with the new end container and offset like * * $.Range().end({container: $('#foo')[0], offset: 20}) * * - __Number__ - the new offset value. The container is kept the same. * * - __String__ - adjusts the offset by converting the string offset to a number and adding it to the current * offset. For example, the following moves the offset forward four characters: * * $('#foo').range().end("+4") * * Note that `end` can return a text node. To get the containing element use this: * * var startNode = range.end().container; * if( startNode.nodeType === Node.TEXT_NODE || * startNode.nodeType === Node.CDATA_SECTION_NODE ) { * startNode = startNode.parentNode; * } * $(startNode).addClass('highlight'); * * @param {Object|String|Number} [set] a set value if setting the end of the range or nothing if reading it. */ end : function (set) { // read end if (set === undefined) { if (this.range.startContainer) { return { container : this.range.endContainer, offset : this.range.endOffset } } else { var // Get the end parent element end = this.clone().collapse(false).parent(), // used to get the end elements offset endRange = $.Range(end).select(end).collapse(); endRange.move("END_TO_END", this); return { container : end, offset : endRange.toString().length } } } else { if (this.range.setEnd) { if (typeof set == 'number') { this.range.setEnd(this.range.endContainer, set) } else if (typeof set == 'string') { var res = callMove(this.range.endContainer, this.range.endOffset, parseInt(set, 10)) this.range.setEnd(res.node, res.offset); } else { this.range.setEnd(set.container, set.offset) } } else { if (typeof set == "string") { this.range.moveEnd('character', parseInt(set, 10)); } else { // get the current end container var container = this.end().container, offset if (typeof set == "number") { offset = set } else { container = set.container offset = set.offset } var newPoint = $.Range(container).collapse(); //move it over offset characters newPoint.range.move(offset); this.move("END_TO_START", newPoint); } } return this; } }, /** * @function jQuery.Range.prototype.parent parent * @signature `range.parent()` * * @body * Returns the most common ancestor element of * the endpoints in the range. This will return a text element if the range is * within a text element. In this case, to get the containing element use this: * * var parent = range.parent(); * if( parent.nodeType === Node.TEXT_NODE || * parent.nodeType === Node.CDATA_SECTION_NODE ) { * parent = startNode.parentNode; * } * $(parent).addClass('highlight'); * * @return {HTMLNode} the TextNode or HTMLElement * that fully contains the range */ parent : function () { if (this.range.commonAncestorContainer) { return this.range.commonAncestorContainer; } else { var parentElement = this.range.parentElement(), range = this.range; // IE's parentElement will always give an element, we want text ranges iterate(parentElement.childNodes, function (txtNode) { if ($.Range(txtNode).range.inRange(range)) { // swap out the parentElement parentElement = txtNode; return false; } }); return parentElement; } }, /** * @function jQuery.Range.prototype.rect rect * @signature `range.rect([from])` * * @body * Returns the bounding rectangle of this range. * * @param {String} [from] - where the coordinates should be * positioned from. By default, coordinates are given from the client viewport. * But if 'page' is given, they are provided relative to the page. * * @return {TextRectangle} - The client rects. */ rect : function (from) { var rect = this.range.getBoundingClientRect(); // for some reason in webkit this gets a better value if (!rect.height && !rect.width) { rect = this.range.getClientRects()[0] } if (from === 'page') { // Add the scroll offset var off = scrollOffset(); rect = $.extend({}, rect); rect.top += off.top; rect.left += off.left; } return rect; }, /** * @function jQuery.Range.prototype.rects rects * @signature `range.rects([from])` * * @body * returns the client rects. * * @param {String} [from] how the rects coordinates should be given (viewport or page). Provide 'page' for * rect coordinates from the page. * @return {Array} The client rects */ rects : function (from) { // order rects by size var rects = $.map($.makeArray(this.range.getClientRects()).sort(function (rect1, rect2) { return rect2.width * rect2.height - rect1.width * rect1.height; }), function (rect) { return $.extend({}, rect) }), i = 0, j, len = rects.length; // safari returns overlapping client rects // // - big rects can contain 2 smaller rects // - some rects can contain 0 - width rects // - we don't want these 0 width rects while (i < rects.length) { var cur = rects[i], found = false; j = i + 1; while (j < rects.length) { if (withinRect(cur, rects[j])) { if (!rects[j].width) { rects.splice(j, 1) } else { found = rects[j]; break; } } else { j++; } } if (found) { rects.splice(i, 1) } else { i++; } } // safari will be return overlapping ranges ... if (from == 'page') { var off = scrollOffset(); return $.each(rects, function (ith, item) { item.top += off.top; item.left += off.left; }) } return rects; } }); (function () { //method branching .... var fn = $.Range.prototype, range = $.Range().range; /** * @function jQuery.Range.prototype.compare compare * @signature `range.compare([type], [compareRange])` * * @body * Compares one range to another range. * * ## Example * * // compare the highlight element's start position * // to the start of the current range * $('#highlight') * .range() * .compare('START_TO_START', $.Range.current()) * * * * @param {String} type Specifies the boundary of the * range and the <code>compareRange</code> to compare. * * - `"START_TO_START"` - the start of the range and the start of compareRange * - `"START_TO_END"` - the start of the range and the end of compareRange * - `"END_TO_END"` - the end of the range and the end of compareRange * - `"END_TO_START"` - the end of the range and the start of compareRange * * @param {$.Range} compareRange The other range * to compare against. * @return {Number} a number indicating if the range * boundary is before, * after, or equal to <code>compareRange</code>'s * boundary where: * * - -1 - the range boundary comes before the compareRange boundary * - 0 - the boundaries are equal * - 1 - the range boundary comes after the compareRange boundary */ fn.compare = range.compareBoundaryPoints ? function (type, range) { return this.range.compareBoundaryPoints(this.window().Range[reverse(type)], range.range) } : function (type, range) { return this.range.compareEndPoints(convertType(type), range.range) } /** * @function jQuery.Range.prototype.move move * @signature `range.move([referenceRange])` * * @body * Moves the endpoints of a range relative to another range. * * // Move the current selection's end to the * // end of the #highlight element * $.Range.current().move('END_TO_END', * $('#highlight').range() ) * * * @param {String} type a string indicating the ranges boundary point * to move to which referenceRange boundary point where: * * - `"START_TO_START"` - the start of the range moves to the start of referenceRange * - `"START_TO_END"` - the start of the range move to the end of referenceRange * - `"END_TO_END"` - the end of the range moves to the end of referenceRange * - `"END_TO_START"` - the end of the range moves to the start of referenceRange * * @param {jQuery.Range} referenceRange * @return {jQuery.Range} the original range for chaining */ fn.move = range.setStart ? function (type, range) { var rangesRange = range.range; switch (type) { case "START_TO_END" : this.range.setStart(rangesRange.endContainer, rangesRange.endOffset) break; case "START_TO_START" : this.range.setStart(rangesRange.startContainer, rangesRange.startOffset) break; case "END_TO_END" : this.range.setEnd(rangesRange.endContainer, rangesRange.endOffset) break; case "END_TO_START" : this.range.setEnd(rangesRange.startContainer, rangesRange.startOffset) break; } return this; } : function (type, range) { this.range.setEndPoint(convertType(type), range.range) return this; }; var cloneFunc = range.cloneRange ? "cloneRange" : "duplicate", selectFunc = range.selectNodeContents ? "selectNodeContents" : "moveToElementText"; fn. /** * @function jQuery.Range.prototype.clone clone * @signature `range.clone()` * * @body * Clones the range and returns a new $.Range * object: * * var range = new $.Range(document.getElementById('text')); * var newRange = range.clone(); * range.start('+2'); * range.select(); * * @return {jQuery.Range} returns the range as a $.Range. */ clone = function () { return $.Range(this.range[cloneFunc]()); }; fn. /** * @function jQuery.Range.prototype.select select * @signature `range.select([el])` * * @body * Selects an element with this range. If nothing * is provided, makes the current range appear as if the user has selected it. * * This works with text nodes. For example with: * * <div id="text">This is a text</div> * * $.Range can select `is a` like this: * * var range = new $.Range(document.getElementById('text')); * range.start('+5'); * range.end('-5'); * range.select(); * * @param {HTMLElement} [el] The element in which this range should be selected * @return {jQuery.Range} the range for chaining. */ select = range.selectNodeContents ? function (el) { if (!el) { var selection = this.window().getSelection(); selection.removeAllRanges(); selection.addRange(this.range); } else { this.range.selectNodeContents(el); } return this; } : function (el) { if (!el) { this.range.select() } else if (el.nodeType === 3) { //select this node in the element ... var parent = el.parentNode, start = 0, end; iterate(parent.childNodes, function (txtNode) { if (txtNode === el) { end = start + txtNode.nodeValue.length; return false; } else { start = start + txtNode.nodeValue.length } }) this.range.moveToElementText(parent); this.range.moveEnd('character', end - this.range.text.length) this.range.moveStart('character', start); } else { this.range.moveToElementText(el); } return this; }; })(); // helpers ----------------- // iterates through a list of elements, calls cb on every text node // if cb returns false, exits the iteration var iterate = function (elems, cb) { var elem, start; for (var i = 0; elems[i]; i++) { elem = elems[i]; // Get the text from text nodes and CDATA nodes if (elem.nodeType === 3 || elem.nodeType === 4) { if (cb(elem) === false) { return false; } // Traverse everything else, except comment nodes } else if (elem.nodeType !== 8) { if (iterate(elem.childNodes, cb) === false) { return false; } } } }, isText = function (node) { return node.nodeType === 3 || node.nodeType === 4 }, iteratorMaker = function (toChildren, toNext) { return function (node, mustMoveRight) { // first try down if (node[toChildren] && !mustMoveRight) { return isText(node[toChildren]) ? node[toChildren] : arguments.callee(node[toChildren]) } else if (node[toNext]) { return isText(node[toNext]) ? node[toNext] : arguments.callee(node[toNext]) } else if (node.parentNode) { return arguments.callee(node.parentNode, true) } } }, getNextTextNode = iteratorMaker("firstChild", "nextSibling"), getPrevTextNode = iteratorMaker("lastChild", "previousSibling"), callMove = function (container, offset, howMany) { var mover = howMany < 0 ? getPrevTextNode : getNextTextNode; // find the text element if( !isText(container) ){ // sometimes offset isn't actually an element container = container.childNodes[offset] ? container.childNodes[offset] : // if this happens, use the last child container.lastChild; if( !isText(container) ) { container = mover(container) } return move(container, howMany) } else { if(offset+howMany < 0){ return move(mover(container), offset + howMany) } else { return move(container, offset + howMany) } } }, // Moves howMany characters from the start of // from move = function (from, howMany) { var mover = howMany < 0 ? getPrevTextNode : getNextTextNode; howMany = Math.abs(howMany); while (from && howMany >= from.nodeValue.length) { howMany = howMany - from.nodeValue.length; from = mover(from) } return { node : from, offset : mover === getNextTextNode ? howMany : from.nodeValue.length - howMany } }, supportWhitespace, isWhitespace = function (el) { if (supportWhitespace == null) { supportWhitespace = 'isElementContentWhitespace' in el; } return (supportWhitespace ? el.isElementContentWhitespace : (el.nodeType === 3 && '' == el.data.trim())); }, // if a point is within a rectangle within = function (rect, point) { return rect.left <= point.clientX && rect.left + rect.width >= point.clientX && rect.top <= point.clientY && rect.top + rect.height >= point.clientY }, // if a rectangle is within another rectangle withinRect = function (outer, inner) { return within(outer, { clientX : inner.left, clientY : inner.top }) && //top left within(outer, { clientX : inner.left + inner.width, clientY : inner.top }) && //top right within(outer, { clientX : inner.left, clientY : inner.top + inner.height }) && //bottom left within(outer, { clientX : inner.left + inner.width, clientY : inner.top + inner.height }) //bottom right }, // gets the scroll offset from a window scrollOffset = function (win) { var win = win || window; doc = win.document.documentElement, body = win.document.body; return { left : (doc && doc.scrollLeft || body && body.scrollLeft || 0) + (doc.clientLeft || 0), top : (doc && doc.scrollTop || body && body.scrollTop || 0) + (doc.clientTop || 0) }; }; support.moveToPoint = !!$.Range().range.moveToPoint return $; });