123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- 'use strict';
- /**
- * Licensed Materials - Property of IBM
- * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2015, 2017
- * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
- */
- define(['../../../../lib/@waca/core-client/js/core-client/ui/core/Class', 'jquery', 'underscore'], function (Class, $, _) {
- //Constants representing whether a cursor is on the leading boundary of a node, inside it, or on the trailing boundary.
- var POSITIONS = { BEFORE: 0, INSIDE: 1, AFTER: 2 };
- //DOM NODE types used in this file.
- var NODE_TYPES = { ELEMENT: 1, TEXT: 3 };
- /*
- * A cursor position visually is where the text cursor (i.e. carot) appears.
- * This is represented in the document selection/range model in a somewhat ambiguous way: a (node, offset) pair. The ambiguity in this approach is that
- * when the cursor is on the boundary of nodes, which node is used in that (node, offset) pair? The answer changes based on which browser and platform you
- * are using. The NormalizedPosition class attempts to uniquely represent a cursor position by normalizing it.
- */
- var NormalizedPosition = Class.extend({
- /*
- * Create a NormalizedPosition.
- * @param {node} container - the DOM node of the position (usually corresponds to a range.startContainer or range.endContainer)
- * @param {number} offset - the offset into the DOM node (usually corresponds to a range.startOffset or range.endOffset). When offset is against a
- * text node, this number represents before which character in the text node the cursor appears; in an element node, it represents before which
- * child node the cursor appears
- */
- init: function init(container, offset) {
- this.container = container;
- this.offset = offset;
- this._normalize();
- },
- /*
- * Normalizes the (container, offset) pair set in the constructor.
- * In general: attempts to use the last leaf node which the position represents.
- * For example, consider this DOM snippet:
- *
- * <div>
- * <p>
- * Hello, World!
- * </p>
- * <p>
- * </p>
- * </div>
- *
- * This snippet will result in the following DOM structure:
- *
- * 1 Element node: div
- * 2 Text node: \n\t
- * 3 Element node: p
- * 4 Text node: \n\t
- * 5 Text node: Hello, World!
- * 6 Text node \n
- * 7 Text node: \n\t
- * 8 Element node: p
- * 9 Text node: \n
- * 10 Text node: \n
- *
- * If the text "Hello, World!" is selected, the start node could be any of: 1, 2, 3, 4 or 5 (offset 0) and the end node could be any of: 5 (offset 13),
- * 6, 7, 8 (offset 0). The "normalized" position will choose the last leaf which refers to a given position, so the normalized start is node 5, offset 0
- * and the normalized end is node 8, offset 0.
- *
- * Note that nodes 2, 4, 6, 7, 9 and 10 don't render anything. They are considered "trivial".
- */
- _normalize: function _normalize() {
- var container = null;
- //Populate the this.position value
- this._setCursorPosition();
- if (this.position === POSITIONS.BEFORE) {
- //Find most precise container (i.e. a leaf node)
- this.container = this._findDeepestDescendant(this.container);
- //Find closest non-trivial container (i.e. has content user can see)
- //(This may already be this.container)
- container = this._ensureNodeIsNontrivial(this.container);
- //Confirm a non-trivial container was found
- if (!container) {
- //At end of document, so need to revert to AFTER
- container = this._findNontrivialNode(this.container, true);
- this.position = POSITIONS.AFTER;
- }
- this.container = container;
- } else if (this.position === POSITIONS.INSIDE) {
- if (this.container.nodeType === NODE_TYPES.ELEMENT) {
- //Container is an element node, move selection to child node indicated by offset
- this.container = this._findDeepestDescendant(this.container.childNodes[this.offset]);
- this.position = POSITIONS.BEFORE;
- } //else: container is a text node and is already considered 'normal'
- } else {
- /*i.e. this.position === POSITIONS.AFTER*/
- //Find most precise container (i.e. a leaf node)
- this.container = this._findDeepestDescendant(this.container, true);
- //Find closest non-trivial container (i.e. has content user can see)
- //(This should be the next container, since we're trying to switch AFTER to BEFORE)
- container = this._findNontrivialNode(this.container);
- //Confirm a subsequent container was found and switch position
- if (container) {
- this.position = POSITIONS.BEFORE;
- } else {
- //At end of document, so need to keep AFTER
- container = this._ensureNodeIsNontrivial(this.container, true);
- }
- this.container = container;
- }
- //Update the this.offset value
- this._updateOffset();
- },
- /*
- * Initializes this.position from this.offset and this.container
- */
- _setCursorPosition: function _setCursorPosition() {
- if (this.container.nodeType === NODE_TYPES.TEXT) {
- if (this.offset === 0) {
- this.position = POSITIONS.BEFORE;
- } else if (this.offset === this.container.data.length) {
- this.position = POSITIONS.AFTER;
- } else {
- this.position = POSITIONS.INSIDE;
- }
- } else {
- //i.e. this.container.nodeType === NODE_TYPES.ELEMENT
- if (this.offset === 0) {
- this.position = POSITIONS.BEFORE;
- } else if (this.container.childNodes.length && this.offset === this.container.childNodes.length) {
- this.position = POSITIONS.AFTER;
- } else if (!this.container.childNodes.length && this.offset === 1) {
- this.position = POSITIONS.AFTER;
- } else {
- this.position = POSITIONS.INSIDE;
- }
- }
- },
- /*
- * Updates this.offset from this.position and this.container
- */
- _updateOffset: function _updateOffset() {
- if (this.position === POSITIONS.BEFORE) {
- this.offset = 0;
- } else if (this.position === POSITIONS.AFTER) {
- this.offset = this.container.nodeType === NODE_TYPES.TEXT ? this.container.data.length : this.container.childNodes.length;
- } //else: this.position === POSITIONS.INSIDE, don't need to update offset
- },
- /*
- * @param {node} n
- * @param {boolean} [isLast=false] - indicates whether to find the first-child ancestor of 'n' (false) or the last-child ancestor of 'n' (true)
- * @return {node} which is a leaf and either the very first descendant of 'n' ('isLast'=false) or the very last descendant of 'n' ('isLast'=true)
- */
- _findDeepestDescendant: function _findDeepestDescendant(n, isLast) {
- while (n.firstChild) {
- n = isLast ? n.lastChild : n.firstChild;
- }
- return n;
- },
- /*
- * @param {node} n
- * @return {boolean} true iff 'n' is a blank text node, i.e. the browser will collapse it into the visible text so it doesn't participate in cursor
- * positioning
- */
- _isTrivialNode: function _isTrivialNode(n) {
- return n && n.nodeType === NODE_TYPES.TEXT /*text node*/ && n.data.trim() === '' /*no text*/;
- },
- /*
- * This utility function's purpose is to gather all the nodes which will be collapsed into a single cursor-point by the browser.
- * @param {node} n - the node to start at; will be included in the returned list
- * @param {boolean} [isPrev=false] - a flag to control the direction of the search: forward (false) or backward (true)
- * @return {object[]} a list of objects, with members 'container' and 'offset', which all represent equivalent points when selecting. Note that these
- * are only nodes in a specific direction (forward or backward) so there may be more equivalent ones in the other directions if 'n' is trivial.
- */
- _collectEquivalentNodes: function _collectEquivalentNodes(n, isPrev) {
- var nodeList = [];
- function list(container, beginningOrEnd) {
- nodeList.push({
- container: container,
- offset: beginningOrEnd ? container.nodeType === NODE_TYPES.TEXT ? container.data.length : container.childNodes.length : 0
- });
- }
- //Include parameter n as first entry in list
- list(n, !isPrev);
- var m = null;
- while (!m && n) {
- //Step to next sibling
- m = isPrev ? n.previousSibling : n.nextSibling;
- if (m) {
- //There is a sibling:
- //If there are children, drill down to a leaf
- if (m.childNodes.length) {
- m = this._findDeepestDescendant(m, isPrev);
- }
- //If this node is trivial, continue looping
- if (this._isTrivialNode(m)) {
- n = m;
- m = null;
- //Include this as one of the trivial leaf nodes in the list
- list(n, false);
- } //else: will exit loop
- } else {
- //There is no sibling: walk the parent chain (so as to then look at their siblings)
- n = n.parentNode;
- }
- }
- //Include non-trivial element which ends the list; or null if end of document reached
- list(n ? m : null, isPrev);
- return nodeList;
- },
- /*
- * @param {node} n
- * @param {boolean} [isPrev=false] - indicate whether to look forward (false) or backward (true) in the document
- * @return {node} the next (or, if 'isPrev'=true, previous) non-trivial node after (or before) 'n'
- */
- _findNontrivialNode: function _findNontrivialNode(n, isPrev) {
- return this._collectEquivalentNodes(n, isPrev).pop().container;
- },
- /*
- * @param {node} n
- * @param {boolean} [isPrev=false]
- * @return {node} 'n' if it is non-trivial, otherwise same behaviour as '_findNontrivialNode'
- */
- _ensureNodeIsNontrivial: function _ensureNodeIsNontrivial(n, isPrev) {
- return this._isTrivialNode(n) ? this._findNontrivialNode(n, isPrev) : n;
- },
- /**
- * @param {NormalizedPosition} position
- * @return {boolean} true iff 'position' refers to the same position as this
- */
- equals: function equals(position) {
- return this.container === position.container && this.offset === position.offset;
- },
- /**
- * Finds an object representing a valid (not necessarily normalized) range position reference which is equivalent to this, which satisfies the provided
- * parameters.
- * @param {boolean} favourEarlier - whether to return a position reference which is earlier in the document (true) or later (false)
- * @param {string} [selector] - a jQuery selector, describing the desired node
- * @return {object} an object with 'container' and 'offset' members, or null if no position was found matching the given selector
- */
- chooseDocumentPosition: function chooseDocumentPosition(favourEarlier, selector) {
- selector = selector || '*';
- var nodes = [];
- if (this.position === POSITIONS.INSIDE) {
- //When the normalized position is inside, there is only ever one node with the equivalent position
- nodes.push(this);
- } else if (this.position === POSITIONS.BEFORE) {
- //Get the nodes starting with the normalized one, then work backward
- nodes = this._collectEquivalentNodes(this.container, true);
- if (favourEarlier) {
- nodes.reverse();
- }
- } else {
- //(This case only occurs when there is no non-trivial node after this one in the document)
- //Get the nodes starting with the normalized one, then work forward
- nodes = this._collectEquivalentNodes(this.container, false);
- nodes.splice(nodes.length - 1, 1); //The last is expected to be null; remove it
- if (!favourEarlier) {
- nodes.reverse();
- }
- }
- //Find the first node matching the provided selector
- return _.find(nodes, function (n) {
- var node = n.container;
- if (node.nodeType === NODE_TYPES.TEXT) {
- node = node.parentNode; //Text nodes never match jQuery expressions, so get the parent
- }
- return $(node).is(selector);
- });
- }
- });
- return NormalizedPosition;
- });
- //# sourceMappingURL=NormalizedPosition.js.map
|