Path: blob/trunk/javascript/atoms/locators/relative.js
2884 views
// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617goog.provide('bot.locators.relative');1819goog.require('bot');20goog.require('bot.dom');21goog.require('bot.locators');22goog.require('goog.array');23goog.require('goog.dom');24goog.require('goog.math.Rect');252627/**28* @typedef {function(!Element):!boolean}29*/30var Filter;3132/**33* @param {!Element|function():!Element|!Object} selector Mechanism to be used34* to find the element.35* @param {!function(!goog.math.Rect, !goog.math.Rect):boolean} proximity36* @return {!Filter} A function that determines whether the37* selector matches the proximity function.38* @private39*/40bot.locators.relative.proximity_ = function (selector, proximity) {41/**42* Assigning to a temporary variable to keep the closure compiler happy.43* @todo Inline this.44*45* @type {!function(!Element):boolean}46*/47var toReturn = function (compareTo) {48var element = bot.locators.relative.resolve_(selector);4950var rect1 = bot.dom.getClientRect(element);51var rect2 = bot.dom.getClientRect(compareTo);5253return proximity.call(null, rect1, rect2);54};5556return toReturn;57};585960/**61* Relative locator to find elements that are above the expected one. "Above"62* is defined as where the bottom of the element found by `selector` is above63* the top of an element we're comparing to.64*65* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.66* @return {!Filter} A function that determines whether the selector is above the given element.67* @private68*/69bot.locators.relative.above_ = function (selector) {70return bot.locators.relative.proximity_(71selector,72function (expected, toFind) {73return toFind.top + toFind.height <= expected.top;74});75};767778/**79* Relative locator to find elements that are below the expected one. "Below"80* is defined as where the top of the element found by `selector` is below the81* bottom of an element we're comparing to.82*83* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.84* @return {!Filter} A function that determines whether the selector is below the given element.85* @private86*/87bot.locators.relative.below_ = function (selector) {88return bot.locators.relative.proximity_(89selector,90function (expected, toFind) {91return toFind.top >= expected.top + expected.height;92});93};949596/**97* Relative locator to find elements that are to the left of the expected one.98*99* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.100* @return {!Filter} A function that determines whether the selector is left of the given element.101* @private102*/103bot.locators.relative.leftOf_ = function (selector) {104return bot.locators.relative.proximity_(105selector,106function (expected, toFind) {107return toFind.left + toFind.width <= expected.left;108});109};110111112/**113* Relative locator to find elements that are to the left of the expected one.114*115* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.116* @return {!Filter} A function that determines whether the selector is right of the given element.117* @private118*/119bot.locators.relative.rightOf_ = function (selector) {120return bot.locators.relative.proximity_(121selector,122function (expected, toFind) {123return toFind.left >= expected.left + expected.width;124});125};126127128/**129* Relative locator to find elements that are above the expected one. "Above"130* is defined as where the bottom of the element found by `selector` is above131* the top of an element we're comparing to.132*133* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.134* @return {!Filter} A function that determines whether the selector is above the given element.135* @private136*/137bot.locators.relative.straightAbove_ = function (selector) {138return bot.locators.relative.proximity_(139selector,140function (expected, toFind) {141return toFind.left < expected.left + expected.width142&& toFind.left + toFind.width > expected.left143&& toFind.top + toFind.height <= expected.top;144});145};146147148/**149* Relative locator to find elements that are below the expected one. "Below"150* is defined as where the top of the element found by `selector` is below the151* bottom of an element we're comparing to.152*153* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.154* @return {!Filter} A function that determines whether the selector is below the given element.155* @private156*/157bot.locators.relative.straightBelow_ = function (selector) {158return bot.locators.relative.proximity_(159selector,160function (expected, toFind) {161return toFind.left < expected.left + expected.width162&& toFind.left + toFind.width > expected.left163&& toFind.top >= expected.top + expected.height;164});165};166167168/**169* Relative locator to find elements that are to the left of the expected one.170*171* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.172* @return {!Filter} A function that determines whether the selector is left of the given element.173* @private174*/175bot.locators.relative.straightLeftOf_ = function (selector) {176return bot.locators.relative.proximity_(177selector,178function (expected, toFind) {179return toFind.top < expected.top + expected.height180&& toFind.top + toFind.height > expected.top181&& toFind.left + toFind.width <= expected.left;182});183};184185186/**187* Relative locator to find elements that are to the left of the expected one.188*189* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.190* @return {!Filter} A function that determines whether the selector is right of the given element.191* @private192*/193bot.locators.relative.straightRightOf_ = function (selector) {194return bot.locators.relative.proximity_(195selector,196function (expected, toFind) {197return toFind.top < expected.top + expected.height198&& toFind.top + toFind.height > expected.top199&& toFind.left >= expected.left + expected.width;200});201};202203204/**205* Find elements within (by default) 50 pixels of the selected element. An206* element is not near itself.207*208* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.209* @param {number=} opt_distance Optional distance in pixels to count as "near" (defaults to 50 pixels).210* @return {!Filter} A function that determines whether the selector is near the given element.211* @private212*/213bot.locators.relative.near_ = function (selector, opt_distance) {214var distance;215if (opt_distance) {216distance = opt_distance;217} else if (goog.isNumber(selector['distance'])) {218distance = /** @type {number} */ (selector['distance']);219// delete selector['distance'];220}221222if (!distance) {223distance = 50;224}225226/**227* @param {!Element} compareTo228* @return {boolean}229*/230var func = function (compareTo) {231var element = bot.locators.relative.resolve_(selector);232233if (element === compareTo) {234return false;235}236237var rect1 = bot.dom.getClientRect(element);238var rect2 = bot.dom.getClientRect(compareTo);239240var rect1_bigger = new goog.math.Rect(241rect1.left-distance,242rect1.top-distance,243rect1.width+distance*2,244rect1.height+distance*2245);246247return rect1_bigger.intersects(rect2);248};249250return func;251};252253254/**255* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.256* @returns {!Element} A single element.257* @private258*/259bot.locators.relative.resolve_ = function (selector) {260if (goog.dom.isElement(selector)) {261return /** @type {!Element} */ (selector);262}263264if (goog.isFunction(selector)) {265var func = /** @type {function():!Element} */ (selector);266return bot.locators.relative.resolve_(func.call(null));267}268269if (goog.isObject(selector)) {270var element = bot.locators.findElement(selector);271if (!element) {272throw new bot.Error(273bot.ErrorCode.NO_SUCH_ELEMENT,274"No element has been found by " + JSON.stringify(selector));275}276return element;277}278279throw new bot.Error(280bot.ErrorCode.INVALID_ARGUMENT,281"Selector is of wrong type: " + JSON.stringify(selector));282};283284285/**286* @type {!Object<string, function(!Object):!Filter>}287* @private288* @const289*/290bot.locators.relative.STRATEGIES_ = {291'above': bot.locators.relative.above_,292'below': bot.locators.relative.below_,293'left': bot.locators.relative.leftOf_,294'near': bot.locators.relative.near_,295'right': bot.locators.relative.rightOf_,296'straightAbove': bot.locators.relative.straightAbove_,297'straightBelow': bot.locators.relative.straightBelow_,298'straightLeft': bot.locators.relative.straightLeftOf_,299'straightRight': bot.locators.relative.straightRightOf_,300};301302bot.locators.relative.RESOLVERS_ = {303'above': bot.locators.relative.resolve_,304'below': bot.locators.relative.resolve_,305'left': bot.locators.relative.resolve_,306'near': bot.locators.relative.resolve_,307'right': bot.locators.relative.resolve_,308'straightAbove': bot.locators.relative.resolve_,309'straightBelow': bot.locators.relative.resolve_,310'straightLeft': bot.locators.relative.resolve_,311'straightRight': bot.locators.relative.resolve_,312};313314/**315* @param {!IArrayLike<!Element>} allElements316* @param {!IArrayLike<!Filter>}filters317* @return {!Array<!Element>}318* @private319*/320bot.locators.relative.filterElements_ = function (allElements, filters) {321var toReturn = [];322goog.array.forEach(323allElements,324function (element) {325if (!!!element) {326return;327}328329var include = goog.array.every(330filters,331function (filter) {332// Look up the filter function by name333var name = filter["kind"];334var strategy = bot.locators.relative.STRATEGIES_[name];335336if (!!!strategy) {337throw new bot.Error(338bot.ErrorCode.INVALID_ARGUMENT,339"Cannot find filter suitable for " + name);340}341342// Call it with args.343var filterFunc = strategy.apply(null, filter["args"]);344return filterFunc(/** @type {!Element} */(element));345},346null);347348if (include) {349toReturn.push(element);350}351},352null);353354// We want to sort the returned elements by proximity to the last "anchor"355// element in the filters.356var finalFilter = goog.array.last(filters);357var name = finalFilter ? finalFilter["kind"] : "unknown";358var resolver = bot.locators.relative.RESOLVERS_[name];359if (!!!resolver) {360return toReturn;361}362var lastAnchor = resolver.apply(null, finalFilter["args"]);363if (!!!lastAnchor) {364return toReturn;365}366367return bot.locators.relative.sortByProximity_(lastAnchor, toReturn);368};369370371/**372* @param {!Element} anchor373* @param {!Array<!Element>} elements374* @return {!Array<!Element>}375* @private376*/377bot.locators.relative.sortByProximity_ = function (anchor, elements) {378var anchorRect = bot.dom.getClientRect(anchor);379var anchorCenter = {380x: anchorRect.left + (Math.max(1, anchorRect.width) / 2),381y: anchorRect.top + (Math.max(1, anchorRect.height) / 2)382};383384var distance = function (e) {385var rect = bot.dom.getClientRect(e);386var center = {387x: rect.left + (Math.max(1, rect.width) / 2),388y: rect.top + (Math.max(1, rect.height) / 2)389};390391var x = Math.pow(anchorCenter.x - center.x, 2);392var y = Math.pow(anchorCenter.y - center.y, 2);393394return Math.sqrt(x + y);395};396397goog.array.sort(elements, function (left, right) {398return distance(left) - distance(right);399});400401return elements;402};403404405/**406* Find an element by using a relative locator.407*408* @param {!Object} target The search criteria.409* @param {!(Document|Element)} ignored_root The document or element to perform410* the search under, which is ignored.411* @return {Element} The first matching element, or null if no such element412* could be found.413*/414bot.locators.relative.single = function (target, ignored_root) {415var matches = bot.locators.relative.many(target, ignored_root);416if (goog.array.isEmpty(matches)) {417return null;418}419return matches[0];420};421422423/**424* Find many elements by using the value of the ID attribute.425* @param {!Object} target The search criteria.426* @param {!(Document|Element)} root The document or element to perform427* the search under, which is ignored.428* @return {!IArrayLike<Element>} All matching elements, or an empty list.429*/430bot.locators.relative.many = function (target, root) {431if (!target.hasOwnProperty("root") || !target.hasOwnProperty("filters")) {432throw new bot.Error(433bot.ErrorCode.INVALID_ARGUMENT,434"Locator not suitable for relative locators: " + JSON.stringify(target));435}436if (!goog.isArrayLike(target["filters"])) {437throw new bot.Error(438bot.ErrorCode.INVALID_ARGUMENT,439"Targets should be an array: " + JSON.stringify(target));440}441442var elements;443if (bot.dom.isElement(target["root"])) {444elements = [ /** @type {!Element} */ (target["root"])];445} else {446elements = bot.locators.findElements(target["root"], root);447}448449if (goog.array.isEmpty(elements)) {450return [];451}452453var filters = target["filters"];454return bot.locators.relative.filterElements_(elements, filters);455};456457458