'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:
*
*
*
* Hello, World!
*
*
*
*
*
* 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