Path: blob/trunk/javascript/atoms/locators/relative.js
4503 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.Error');21goog.require('bot.ErrorCode');22goog.require('bot.dom');23goog.require('bot.locators');24goog.require('goog.array');25goog.require('goog.dom');26goog.require('goog.math.Rect');27goog.require('goog.utils');282930/**31* @typedef {function(!Element):!boolean}32*/33var Filter;3435/**36* @param {!Element|function():!Element|!Object} selector Mechanism to be used37* to find the element.38* @param {!function(!goog.math.Rect, !goog.math.Rect):boolean} proximity39* @return {!Filter} A function that determines whether the40* selector matches the proximity function.41* @private42*/43bot.locators.relative.proximity_ = function (selector, proximity) {44/**45* Assigning to a temporary variable to keep the closure compiler happy.46* @todo Inline this.47*48* @type {!function(!Element):boolean}49*/50var toReturn = function (compareTo) {51var element = bot.locators.relative.resolve_(selector);5253var rect1 = bot.dom.getClientRect(element);54var rect2 = bot.dom.getClientRect(compareTo);5556return proximity.call(null, rect1, rect2);57};5859return toReturn;60};616263/**64* Relative locator to find elements that are above the expected one. "Above"65* is defined as where the bottom of the element found by `selector` is above66* the top of an element we're comparing to.67*68* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.69* @return {!Filter} A function that determines whether the selector is above the given element.70* @private71*/72bot.locators.relative.above_ = function (selector) {73return bot.locators.relative.proximity_(74selector,75function (expected, toFind) {76return toFind.top + toFind.height <= expected.top;77});78};798081/**82* Relative locator to find elements that are below the expected one. "Below"83* is defined as where the top of the element found by `selector` is below the84* bottom of an element we're comparing to.85*86* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.87* @return {!Filter} A function that determines whether the selector is below the given element.88* @private89*/90bot.locators.relative.below_ = function (selector) {91return bot.locators.relative.proximity_(92selector,93function (expected, toFind) {94return toFind.top >= expected.top + expected.height;95});96};979899/**100* Relative locator to find elements that are to the left of the expected one.101*102* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.103* @return {!Filter} A function that determines whether the selector is left of the given element.104* @private105*/106bot.locators.relative.leftOf_ = function (selector) {107return bot.locators.relative.proximity_(108selector,109function (expected, toFind) {110return toFind.left + toFind.width <= expected.left;111});112};113114115/**116* Relative locator to find elements that are to the left of the expected one.117*118* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.119* @return {!Filter} A function that determines whether the selector is right of the given element.120* @private121*/122bot.locators.relative.rightOf_ = function (selector) {123return bot.locators.relative.proximity_(124selector,125function (expected, toFind) {126return toFind.left >= expected.left + expected.width;127});128};129130131/**132* Relative locator to find elements that are above the expected one. "Above"133* is defined as where the bottom of the element found by `selector` is above134* the top of an element we're comparing to.135*136* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.137* @return {!Filter} A function that determines whether the selector is above the given element.138* @private139*/140bot.locators.relative.straightAbove_ = function (selector) {141return bot.locators.relative.proximity_(142selector,143function (expected, toFind) {144return toFind.left < expected.left + expected.width145&& toFind.left + toFind.width > expected.left146&& toFind.top + toFind.height <= expected.top;147});148};149150151/**152* Relative locator to find elements that are below the expected one. "Below"153* is defined as where the top of the element found by `selector` is below the154* bottom of an element we're comparing to.155*156* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.157* @return {!Filter} A function that determines whether the selector is below the given element.158* @private159*/160bot.locators.relative.straightBelow_ = function (selector) {161return bot.locators.relative.proximity_(162selector,163function (expected, toFind) {164return toFind.left < expected.left + expected.width165&& toFind.left + toFind.width > expected.left166&& toFind.top >= expected.top + expected.height;167});168};169170171/**172* Relative locator to find elements that are to the left of the expected one.173*174* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.175* @return {!Filter} A function that determines whether the selector is left of the given element.176* @private177*/178bot.locators.relative.straightLeftOf_ = function (selector) {179return bot.locators.relative.proximity_(180selector,181function (expected, toFind) {182return toFind.top < expected.top + expected.height183&& toFind.top + toFind.height > expected.top184&& toFind.left + toFind.width <= expected.left;185});186};187188189/**190* Relative locator to find elements that are to the left of the expected one.191*192* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.193* @return {!Filter} A function that determines whether the selector is right of the given element.194* @private195*/196bot.locators.relative.straightRightOf_ = function (selector) {197return bot.locators.relative.proximity_(198selector,199function (expected, toFind) {200return toFind.top < expected.top + expected.height201&& toFind.top + toFind.height > expected.top202&& toFind.left >= expected.left + expected.width;203});204};205206207/**208* Find elements within (by default) 50 pixels of the selected element. An209* element is not near itself.210*211* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.212* @param {number=} opt_distance Optional distance in pixels to count as "near" (defaults to 50 pixels).213* @return {!Filter} A function that determines whether the selector is near the given element.214* @private215*/216bot.locators.relative.near_ = function (selector, opt_distance) {217var distance;218if (opt_distance) {219distance = opt_distance;220} else if (typeof selector['distance'] === 'number') {221distance = /** @type {number} */ (selector['distance']);222// delete selector['distance'];223}224225if (!distance) {226distance = 50;227}228229/**230* @param {!Element} compareTo231* @return {boolean}232*/233var func = function (compareTo) {234var element = bot.locators.relative.resolve_(selector);235236if (element === compareTo) {237return false;238}239240var rect1 = bot.dom.getClientRect(element);241var rect2 = bot.dom.getClientRect(compareTo);242243var rect1_bigger = new goog.math.Rect(244rect1.left-distance,245rect1.top-distance,246rect1.width+distance*2,247rect1.height+distance*2248);249250return rect1_bigger.intersects(rect2);251};252253return func;254};255256257/**258* @param {!Element|function():!Element|!Object} selector Mechanism to be used to find the element.259* @returns {!Element} A single element.260* @private261*/262bot.locators.relative.resolve_ = function (selector) {263if (goog.dom.isElement(selector)) {264return /** @type {!Element} */ (selector);265}266267if (typeof selector === 'function') {268var func = /** @type {function():!Element} */ (selector);269return bot.locators.relative.resolve_(func.call(null));270}271272if (goog.utils.isObject(selector)) {273var element = bot.locators.findElement(selector);274if (!element) {275throw new bot.Error(276bot.ErrorCode.NO_SUCH_ELEMENT,277"No element has been found by " + JSON.stringify(selector));278}279return element;280}281282throw new bot.Error(283bot.ErrorCode.INVALID_ARGUMENT,284"Selector is of wrong type: " + JSON.stringify(selector));285};286287288/**289* @type {!Object<string, function(!Object):!Filter>}290* @private291* @const292*/293bot.locators.relative.STRATEGIES_ = {294'above': bot.locators.relative.above_,295'below': bot.locators.relative.below_,296'left': bot.locators.relative.leftOf_,297'near': bot.locators.relative.near_,298'right': bot.locators.relative.rightOf_,299'straightAbove': bot.locators.relative.straightAbove_,300'straightBelow': bot.locators.relative.straightBelow_,301'straightLeft': bot.locators.relative.straightLeftOf_,302'straightRight': bot.locators.relative.straightRightOf_,303};304305bot.locators.relative.RESOLVERS_ = {306'above': bot.locators.relative.resolve_,307'below': bot.locators.relative.resolve_,308'left': bot.locators.relative.resolve_,309'near': bot.locators.relative.resolve_,310'right': bot.locators.relative.resolve_,311'straightAbove': bot.locators.relative.resolve_,312'straightBelow': bot.locators.relative.resolve_,313'straightLeft': bot.locators.relative.resolve_,314'straightRight': bot.locators.relative.resolve_,315};316317/**318* @param {!IArrayLike<!Element>} allElements319* @param {!IArrayLike<!Filter>}filters320* @return {!Array<!Element>}321* @private322*/323bot.locators.relative.filterElements_ = function (allElements, filters) {324var toReturn = [];325goog.array.forEach(326allElements,327function (element) {328if (!!!element) {329return;330}331332var include = goog.array.every(333filters,334function (filter) {335// Look up the filter function by name336var name = filter["kind"];337var strategy = bot.locators.relative.STRATEGIES_[name];338339if (!!!strategy) {340throw new bot.Error(341bot.ErrorCode.INVALID_ARGUMENT,342"Cannot find filter suitable for " + name);343}344345// Call it with args.346var filterFunc = strategy.apply(null, filter["args"]);347return filterFunc(/** @type {!Element} */(element));348},349null);350351if (include) {352toReturn.push(element);353}354},355null);356357// We want to sort the returned elements by proximity to the last "anchor"358// element in the filters.359var finalFilter = goog.array.last(filters);360var name = finalFilter ? finalFilter["kind"] : "unknown";361var resolver = bot.locators.relative.RESOLVERS_[name];362if (!!!resolver) {363return toReturn;364}365var lastAnchor = resolver.apply(null, finalFilter["args"]);366if (!!!lastAnchor) {367return toReturn;368}369370return bot.locators.relative.sortByProximity_(lastAnchor, toReturn);371};372373374/**375* @param {!Element} anchor376* @param {!Array<!Element>} elements377* @return {!Array<!Element>}378* @private379*/380bot.locators.relative.sortByProximity_ = function (anchor, elements) {381var anchorRect = bot.dom.getClientRect(anchor);382var anchorCenter = {383x: anchorRect.left + (Math.max(1, anchorRect.width) / 2),384y: anchorRect.top + (Math.max(1, anchorRect.height) / 2)385};386387var distance = function (e) {388var rect = bot.dom.getClientRect(e);389var center = {390x: rect.left + (Math.max(1, rect.width) / 2),391y: rect.top + (Math.max(1, rect.height) / 2)392};393394var x = Math.pow(anchorCenter.x - center.x, 2);395var y = Math.pow(anchorCenter.y - center.y, 2);396397return Math.sqrt(x + y);398};399400goog.array.sort(elements, function (left, right) {401return distance(left) - distance(right);402});403404return elements;405};406407408/**409* Find an element by using a relative locator.410*411* @param {!Object} target The search criteria.412* @param {!(Document|Element)} ignored_root The document or element to perform413* the search under, which is ignored.414* @return {Element} The first matching element, or null if no such element415* could be found.416*/417bot.locators.relative.single = function (target, ignored_root) {418var matches = bot.locators.relative.many(target, ignored_root);419if (goog.array.isEmpty(matches)) {420return null;421}422return matches[0];423};424425426/**427* Find many elements by using the value of the ID attribute.428* @param {!Object} target The search criteria.429* @param {!(Document|Element)} root The document or element to perform430* the search under, which is ignored.431* @return {!IArrayLike<Element>} All matching elements, or an empty list.432*/433bot.locators.relative.many = function (target, root) {434if (!target.hasOwnProperty("root") || !target.hasOwnProperty("filters")) {435throw new bot.Error(436bot.ErrorCode.INVALID_ARGUMENT,437"Locator not suitable for relative locators: " + JSON.stringify(target));438}439if (!goog.utils.isArrayLike(target["filters"])) {440throw new bot.Error(441bot.ErrorCode.INVALID_ARGUMENT,442"Targets should be an array: " + JSON.stringify(target));443}444445var elements;446if (bot.dom.isElement(target["root"])) {447elements = [ /** @type {!Element} */ (target["root"])];448} else {449elements = bot.locators.findElements(target["root"], root);450}451452if (goog.array.isEmpty(elements)) {453return [];454}455456var filters = target["filters"];457return bot.locators.relative.filterElements_(elements, filters);458};459460461