'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