- каталог lampa-main/ (yumata/lampa) смонтирован bind-mountом в /lampac/wwwroot/lampa-main (override) - в образе 1.72.0 клиента нет, LampaCron из GitHub не докачивает (нет доступа к GitHub) -> / отдавал 500 - README/AGENT: механика обновления клиента через git - compose: 1.72.0, cache/database как bind-mountы, секреты в init.conf
962 lines
26 KiB
JavaScript
962 lines
26 KiB
JavaScript
/**
|
|
* SpatialNavigator simulates four-direction navigation in Javascript level.
|
|
*
|
|
* Navigation is the ability to navigate between focusable elements
|
|
* within a structured document or user interface according to the spatial
|
|
* location. Users are assumed navigating among elements on a 2D plane by
|
|
* arrow keys (up/down/left/right).
|
|
*
|
|
* SpatialNavigator keeps a 'focused' element by itself. When navigating,
|
|
* focus/unfocus events are triggered automatically. Notice the focus is just
|
|
* an internal state rather than actual focus of DOM Element. User should
|
|
* add event listeners of those events, and design required behaviors.
|
|
*
|
|
* @class SpatialNavigator
|
|
* @param {Array.<SpatialNavigatorElement>} [collection=[]]
|
|
* An initial set of traversable elements.
|
|
* @param {Object} [config]
|
|
* An initial set of configurations.
|
|
*/
|
|
/**
|
|
* SpatialNavigatorElement is a navigable element which can be traversed
|
|
* by {@link SpatialNavigator}. Valid types are as follows:
|
|
*
|
|
* 1. a standard HTMLElement.
|
|
* 2. an Object contains at least 4 properties: left, top, width, and height.
|
|
* 3. an Object implementing getBoundingRect() which returns an object of 2.
|
|
*
|
|
* @typedef {Object} SpatialNavigatorElement
|
|
*/
|
|
/**
|
|
* Fired when an element is focused.
|
|
* @event SpatialNavigator#focus
|
|
* @property {SpatialNavigatorElement} elem The element which got focus.
|
|
*/
|
|
/**
|
|
* Fired when an element is unfocused.
|
|
* @event SpatialNavigator#unfocus
|
|
* @property {SpatialNavigatorElement} elem The element which lost focus.
|
|
*/
|
|
var SpatialNavigator = function () {
|
|
this._focus = null;
|
|
this._previous = null;
|
|
this._collection = [];
|
|
|
|
//this.setCollection(collection);
|
|
|
|
/*
|
|
if (config) {
|
|
for (var key in config) {
|
|
this[key] = config[key];
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
|
|
SpatialNavigator.prototype = {
|
|
/**
|
|
* Limit the navigating direction to vertical and horizontal only. Targets
|
|
* in oblique (left-top, right-top, left-bottom, and right-bottom)
|
|
* directions are always ignored.
|
|
* @type {Boolean}
|
|
* @default false
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
straightOnly: true,
|
|
|
|
/**
|
|
* This threshold is used to determine whether an element is considered in
|
|
* straight (vertical or horizontal) directions. Valid number is between 0
|
|
* to 1.0. Setting it to 0.3 means an element is counted in the straight
|
|
* directions if it overlaps the straight area at least 0.3x of width of the
|
|
* area.
|
|
* @type {Number}
|
|
* @default 0.5
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
straightOverlapThreshold: 0.5,
|
|
|
|
/**
|
|
* Ignore elements with "display: none", "visibility: hidden" or
|
|
* "aria-hidden=true".
|
|
* @type {Boolean}
|
|
* @default false
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
ignoreHiddenElement: false,
|
|
|
|
/**
|
|
* The previous focused element has high priority to be chosen as the next
|
|
* candidate.
|
|
* @type {Boolean}
|
|
* @default false
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
rememberSource: false,
|
|
|
|
/**
|
|
* A callback function that accepcts an element as the first argument will
|
|
* be triggered everytime when SpatialNavigator tries to traverse every
|
|
* single candidate. You can ignore arbitrary elements by returning "false"
|
|
* in this function.
|
|
* @type {Function}
|
|
* @default null
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
navigableFilter: null,
|
|
|
|
/**
|
|
* setting this property true prevents spatialNavigator from emitting any
|
|
* events.
|
|
* @type {Boolean}
|
|
*/
|
|
silent: false,
|
|
|
|
/**
|
|
* Rect represents position and dimension of a 2D object.
|
|
* @typedef {Object} Rect
|
|
* @property {Integer} left Left position
|
|
* @property {Integer} top Top position
|
|
* @property {Integer} right Right position
|
|
* @property {Integer} bottom Bottom position
|
|
* @property {Integer} width Width dimension
|
|
* @property {Integer} height Height dimension
|
|
* @property {Rect} [center] Center position
|
|
* @property {Integer} [x] same as left
|
|
* @property {Integer} [y] same as top
|
|
* @access private
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
|
|
|
|
follow: function ( type, listener ) {
|
|
|
|
if ( this._listeners === undefined ) this._listeners = {};
|
|
|
|
var listeners = this._listeners;
|
|
|
|
if ( listeners[ type ] === undefined ) {
|
|
|
|
listeners[ type ] = [];
|
|
|
|
}
|
|
|
|
if ( listeners[ type ].indexOf( listener ) === - 1 ) {
|
|
|
|
listeners[ type ].push( listener );
|
|
|
|
}
|
|
|
|
},
|
|
|
|
hasListener: function ( type, listener ) {
|
|
|
|
if ( this._listeners === undefined ) return false;
|
|
|
|
var listeners = this._listeners;
|
|
|
|
return listeners[ type ] !== undefined && listeners[ type ].indexOf( listener ) !== - 1;
|
|
|
|
},
|
|
|
|
removeListener: function ( type, listener ) {
|
|
|
|
if ( this._listeners === undefined ) return;
|
|
|
|
var listeners = this._listeners;
|
|
var listenerArray = listeners[ type ];
|
|
|
|
if ( listenerArray !== undefined ) {
|
|
|
|
var index = listenerArray.indexOf( listener );
|
|
|
|
if ( index !== - 1 ) {
|
|
|
|
listenerArray.splice( index, 1 );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
send: function ( type,event ) {
|
|
|
|
if ( this._listeners === undefined ) return;
|
|
|
|
var listeners = this._listeners;
|
|
var listenerArray = listeners[ type ];
|
|
|
|
if ( listenerArray !== undefined ) {
|
|
|
|
event.target = this;
|
|
|
|
var array = listenerArray.slice( 0 );
|
|
|
|
for ( var i = 0, l = array.length; i < l; i ++ ) {
|
|
|
|
array[ i ].call( this, event );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
* Get {@link Rect} of a {@link SpatialNavigatorElement}.
|
|
*
|
|
* @param {SpatialNavigatorElement} elem
|
|
*
|
|
* @return {Rect} dimension of elem.
|
|
*
|
|
* @access private
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
_getRect: function snGetRect(elem) {
|
|
var rect = null;
|
|
|
|
if (!this._isNavigable(elem)) {
|
|
return null;
|
|
}
|
|
|
|
if (elem.getBoundingClientRect) {
|
|
var cr = elem.getBoundingClientRect();
|
|
rect = {
|
|
left: cr.left,
|
|
top: cr.top,
|
|
width: cr.width,
|
|
height: cr.height
|
|
};
|
|
} else if (elem.left !== undefined) {
|
|
rect = {
|
|
left: parseInt(elem.left || 0, 10),
|
|
top: parseInt(elem.top || 0, 10),
|
|
width: parseInt(elem.width || 0, 10),
|
|
height: parseInt(elem.height || 0, 10)
|
|
};
|
|
} else {
|
|
return null;
|
|
}
|
|
|
|
rect.element = elem;
|
|
rect.right = rect.left + rect.width;
|
|
rect.bottom = rect.top + rect.height;
|
|
rect.center = {
|
|
x: rect.left + Math.floor(rect.width / 2),
|
|
y: rect.top + Math.floor(rect.height / 2)
|
|
};
|
|
rect.center.left = rect.center.right = rect.center.x;
|
|
rect.center.top = rect.center.bottom = rect.center.y;
|
|
|
|
return rect;
|
|
},
|
|
|
|
/**
|
|
* Get all {@link Rect} objects from the collection.
|
|
*
|
|
* @param {SpatialNavigatorElement} [excludedElem]
|
|
* You can pass excludedElem here to ignore it from calculating.
|
|
* (most likely, the currently focused element is passed).
|
|
*
|
|
* @return {Array.<Rect>} {@link Rect} objects of all traversable elements.
|
|
*
|
|
* @access private
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
_getAllRects: function snGetAllRects(excludedElem) {
|
|
var rects = [];
|
|
|
|
this._collection.forEach(function (elem) {
|
|
if (!excludedElem || excludedElem !== elem) {
|
|
var rect = this._getRect(elem);
|
|
if (rect) {
|
|
rects.push(rect);
|
|
}
|
|
}
|
|
}, this);
|
|
|
|
return rects;
|
|
},
|
|
|
|
/**
|
|
* Check whether a {@link SpatialNavigatorElement} is navigable.
|
|
*
|
|
* @param {SpatialNavigatorElement} elem
|
|
*
|
|
* @return {Boolean} true if it's navigable.
|
|
*
|
|
* @access private
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
_isNavigable: function snIsNavigable(elem) {
|
|
if (this.ignoreHiddenElement && elem instanceof HTMLElement) {
|
|
var computedStyle = window.getComputedStyle(elem);
|
|
if ((elem.offsetWidth <= 0 && elem.offsetHeight <= 0) ||
|
|
computedStyle.getPropertyValue('visibility') == 'hidden' ||
|
|
computedStyle.getPropertyValue('display') == 'none' ||
|
|
elem.hasAttribute('aria-hidden')) {
|
|
return false;
|
|
}
|
|
}
|
|
if (this.navigableFilter && !this.navigableFilter(elem)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Given a set of {@link Rect} array, divide them into 9 groups with
|
|
* respect to the position of targetRect. Rects centered inside targetRect
|
|
* are grouped as 4th group; straight left as 3rd group; straight right as
|
|
* 5th group; ..... and so on. See below for the corresponding group number:
|
|
*
|
|
* <pre>
|
|
* |---+---+---|
|
|
* | 0 | 1 | 2 |
|
|
* |---+---+---|
|
|
* | 3 | 4 | 5 |
|
|
* |---+---+---|
|
|
* | 6 | 7 | 8 |
|
|
* |---+---+---|
|
|
* </pre>
|
|
*
|
|
* @param {Array.<Rect>} rects
|
|
* {@link RectS} to be divided.
|
|
* @param {Rect} targetRect
|
|
* Reference position for groups.
|
|
*
|
|
* @return {Array.Array.<SpatialNavigatorElement>}
|
|
* A 9-elements array of array, where rects are categorized into
|
|
* these 9 arrays by their group number.
|
|
*
|
|
* @access private
|
|
* @memberof SpatialNavigator.prototype
|
|
*
|
|
*/
|
|
_partition: function snDemarcate(rects, targetRect) {
|
|
var groups = [[], [], [], [], [], [], [], [], []];
|
|
|
|
var threshold = this.straightOverlapThreshold;
|
|
if (threshold > 1 || threshold < 0) {
|
|
// Fallback to default value
|
|
threshold = 0.5;
|
|
}
|
|
|
|
rects.forEach(function (rect) {
|
|
var center = rect.center;
|
|
var x, y, groupId;
|
|
|
|
if (center.x < targetRect.left) {
|
|
x = 0;
|
|
} else if (center.x <= targetRect.right) {
|
|
x = 1;
|
|
} else {
|
|
x = 2;
|
|
}
|
|
|
|
if (center.y < targetRect.top) {
|
|
y = 0;
|
|
} else if (center.y <= targetRect.bottom) {
|
|
y = 1;
|
|
} else {
|
|
y = 2;
|
|
}
|
|
|
|
groupId = y * 3 + x;
|
|
groups[groupId].push(rect);
|
|
|
|
// Although a rect is in the oblique directions, we categorize it in
|
|
// the straight area as well if it overlaps the straight directions more
|
|
// than a specified threshold (0.5 by default).
|
|
if ([0, 2, 6, 8].indexOf(groupId) !== -1) {
|
|
if (rect.left <= targetRect.right - targetRect.width * threshold) {
|
|
if (groupId === 2) {
|
|
groups[1].push(rect);
|
|
} else if (groupId === 8) {
|
|
groups[7].push(rect);
|
|
}
|
|
}
|
|
|
|
if (rect.right >= targetRect.left + targetRect.width * threshold) {
|
|
if (groupId === 0) {
|
|
groups[1].push(rect);
|
|
} else if (groupId === 6) {
|
|
groups[7].push(rect);
|
|
}
|
|
}
|
|
|
|
if (rect.top <= targetRect.bottom - targetRect.height * threshold) {
|
|
if (groupId === 6) {
|
|
groups[3].push(rect);
|
|
} else if (groupId === 8) {
|
|
groups[5].push(rect);
|
|
}
|
|
}
|
|
|
|
if (rect.bottom >= targetRect.top + targetRect.height * threshold) {
|
|
if (groupId === 0) {
|
|
groups[3].push(rect);
|
|
} else if (groupId === 2) {
|
|
groups[5].push(rect);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
return groups;
|
|
},
|
|
|
|
/**
|
|
* Bind targetRect to a set of distance functions for ranking. These
|
|
* functions work with another {@link Rect} object passed to get a ranking
|
|
* value relative to targetRect.
|
|
*
|
|
* @param {Rect} targetRect
|
|
*
|
|
* @return {Object.<function>}
|
|
* An object containing a bunch of functions bound with targetRect.
|
|
*
|
|
* @access private
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
_getDistanceFunction: function snGetDistanceFunction(targetRect) {
|
|
return {
|
|
/* Plumb Line: a vertical line through the center of the
|
|
targetRect. */
|
|
nearPlumbLineIsBetter: function (rect) {
|
|
var d;
|
|
if (rect.center.x < targetRect.center.x) {
|
|
d = targetRect.center.x - rect.right;
|
|
} else {
|
|
d = rect.left - targetRect.center.x;
|
|
}
|
|
return d < 0 ? 0 : d;
|
|
},
|
|
|
|
/* Horizon: a horizontal line through the center of the
|
|
"targetRect". */
|
|
nearHorizonIsBetter: function (rect) {
|
|
var d;
|
|
if (rect.center.y < targetRect.center.y) {
|
|
d = targetRect.center.y - rect.bottom;
|
|
} else {
|
|
d = rect.top - targetRect.center.y;
|
|
}
|
|
return d < 0 ? 0 : d;
|
|
},
|
|
|
|
/* Target Left: a coincident line of the left edge of the
|
|
"targetRect". */
|
|
nearTargetLeftIsBetter: function (rect) {
|
|
var d;
|
|
if (rect.center.x < targetRect.center.x) {
|
|
d = targetRect.left - rect.right;
|
|
} else {
|
|
d = rect.left - targetRect.left;
|
|
}
|
|
return d < 0 ? 0 : d;
|
|
},
|
|
|
|
/* Target Top: a coincident line of the top edge of the
|
|
"targetRect". */
|
|
nearTargetTopIsBetter: function (rect) {
|
|
var d;
|
|
if (rect.center.y < targetRect.center.y) {
|
|
d = targetRect.top - rect.bottom;
|
|
} else {
|
|
d = rect.top - targetRect.top;
|
|
}
|
|
return d < 0 ? 0 : d;
|
|
},
|
|
|
|
/* top, bottom, left, and right: Just ranking by absolute coordinate
|
|
without respect to targetRect. Usually they are used as fallback
|
|
rules when ranks above are draw. */
|
|
topIsBetter: function (rect) {
|
|
return rect.top;
|
|
},
|
|
bottomIsBetter: function (rect) {
|
|
return -1 * rect.bottom;
|
|
},
|
|
leftIsBetter: function (rect) {
|
|
return rect.left;
|
|
},
|
|
rightIsBetter: function (rect) {
|
|
return -1 * rect.right;
|
|
}
|
|
};
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
* PrioritySet contains a set of elements with distance functions that
|
|
* should be used to rank them (obtained from {@link
|
|
* SpatialNavigator#_getDistanceFunction}).
|
|
*
|
|
* @typedef PrioritySet
|
|
* @property {Array.<Rects>} group
|
|
* {@link Rects} of elements that need to be prioritized.
|
|
* @property {Array.<function>} distance
|
|
* Distance functions. Primary ranking rule should be put in index
|
|
* 0; secondary in index 1 (fallback rule when primary rule draws
|
|
* ); and so on.
|
|
* @access private
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
/**
|
|
* Pick a {@link Rect} with highest priority.
|
|
*
|
|
* @param {Array.<PrioritySet>} priorities
|
|
* An array of {@link PrioritySet} that need to be prioritized. The
|
|
* set with lowest index containing non-empty {PrioritySet.group}
|
|
* would be prioritized.
|
|
* @param {SpatialNavigatorElement} target
|
|
* The origin of coordinates for traversal.
|
|
* @param {String} direction
|
|
* It should be "left", "right", "up" or "down".
|
|
*
|
|
* @return {Rect} the {@link Rect} of highest priority.
|
|
*
|
|
* @access private
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
_prioritize: function snPrioritize(priorities, target, direction) {
|
|
var destPriority = priorities.find(function (p) {
|
|
return !!p.group.length;
|
|
});
|
|
|
|
if (!destPriority) {
|
|
return null;
|
|
}
|
|
|
|
if (this.rememberSource &&
|
|
this._previous &&
|
|
target == this._previous.destination &&
|
|
direction == this._previous.reverse) {
|
|
|
|
var source = this._previous.source;
|
|
var found = destPriority.group.find(function (dest) {
|
|
return dest.element == source;
|
|
});
|
|
if (found) {
|
|
return found;
|
|
}
|
|
}
|
|
|
|
destPriority.group.sort(function (a, b) {
|
|
return destPriority.distance.reduce(function (answer, distance) {
|
|
return answer || (distance(a) - distance(b));
|
|
}, 0);
|
|
});
|
|
|
|
return destPriority.group[0];
|
|
},
|
|
|
|
/**
|
|
* Replace the set of traversable elements.
|
|
*
|
|
* @param {Array.<SpatialNavigatorElement>} [collection=[]]
|
|
elements to be replaced. The array is deep-copied and never
|
|
be changed directly by SpatialNavigator.
|
|
*
|
|
* @fires SpatialNavigator#unfocus
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
setCollection: function snSetCollection(collection) {
|
|
this.unfocus();
|
|
this._collection = [];
|
|
if (collection) {
|
|
this.multiAdd(collection);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Add an element to traversable set.
|
|
*
|
|
* @param {SpatialNavigatorElement} elem
|
|
* @return {Boolean} true if succeed.
|
|
*
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
add: function snAdd(elem) {
|
|
var index = this._collection.indexOf(elem);
|
|
if (index >= 0) {
|
|
return false;
|
|
}
|
|
this._collection.push(elem);
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Add a bunch of elements to traversable set.
|
|
*
|
|
* @param {Array.<SpatialNavigatorElement>} elements
|
|
* @return {Boolean} true if all elements are added successfully.
|
|
*
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
multiAdd: function snMultiAdd(elements) {
|
|
return Array.from(elements).every(this.add, this);
|
|
},
|
|
|
|
/**
|
|
* Remove an element from traversable set.
|
|
*
|
|
* @param {SpatialNavigatorElement} elem
|
|
* @return {Boolean} true if succeed. false if elem does not exist.
|
|
*
|
|
* @fires SpatialNavigator#unfocus
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
remove: function snRemove(elem) {
|
|
var index = this._collection.indexOf(elem);
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
|
|
if (this._focus === elem) {
|
|
this.unfocus();
|
|
}
|
|
|
|
this._collection.splice(index, 1);
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Remove a bunch of elements to traversable set.
|
|
*
|
|
* @param {Array.<SpatialNavigatorElement>} elements
|
|
* @return {Boolean} true if all elements are removed successfully.
|
|
*
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
multiRemove: function snMultiRemove(elements) {
|
|
return Array.from(elements).every(this.remove, this);
|
|
},
|
|
|
|
focused: function (elem) {
|
|
this._focus = elem;
|
|
},
|
|
|
|
/**
|
|
* Move focus to an existing element.
|
|
*
|
|
* @param {SpatialNavigatorElement} [elem]
|
|
* when omitted, it focuses the last focused element or the first
|
|
* navigable element if no previously-focused element is found.
|
|
*
|
|
* @return {Boolean} true if succeed. false if element doesn't exist.
|
|
*
|
|
* @fires SpatialNavigator#focus
|
|
* @fires SpatialNavigator#unfocus
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
focus: function snFocus(elem) {
|
|
// If no arguments are received, let current focused element be the
|
|
// default.
|
|
if (!elem && this._focus && this._isNavigable(this._focus)) {
|
|
elem = this._focus;
|
|
}
|
|
|
|
if (!this._collection) {
|
|
return false;
|
|
}
|
|
|
|
if (!elem) {
|
|
var navigableElems = this._collection.filter(this._isNavigable, this);
|
|
if (!navigableElems.length) {
|
|
return false;
|
|
}
|
|
elem = navigableElems[0];
|
|
} else if (this._collection.indexOf(elem) < 0 ||
|
|
!this._isNavigable(elem)) {
|
|
return false;
|
|
}
|
|
|
|
this.unfocus();
|
|
this._focus = elem;
|
|
|
|
if (!this.silent) {
|
|
this.send('focus',{elem: this._focus });
|
|
}
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Remove focus if any.
|
|
*
|
|
* It will trigger "unfocus" event.
|
|
*
|
|
* @return {Boolean} succeed or not.
|
|
*
|
|
* @fires SpatialNavigator#unfocus
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
unfocus: function snUnfocus() {
|
|
if (this._focus) {
|
|
var elem = this._focus;
|
|
this._focus = null;
|
|
if (!this.silent) {
|
|
this.send('unfocus',{ elem: elem });
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Get the currently focused element.
|
|
*
|
|
* @return {SpatialNavigatorElement} or null if nothing focused.
|
|
*
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
getFocusedElement: function snGetFocusedElement() {
|
|
return this._focus;
|
|
},
|
|
|
|
/**
|
|
* Given a direction, find the element nearest to the focus element in that
|
|
* direction. This is equivalent to {@link SpatialNavigator#navigate} with
|
|
* focused element passed as target.
|
|
*
|
|
* @param {String} direction
|
|
* It should be "left", "right", "up" or "down".
|
|
*
|
|
* @return {Boolean} true if succeed, false if nothing can be focused.
|
|
*
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
move: function snMove(direction) {
|
|
var reverse = {
|
|
'left': 'right',
|
|
'up': 'down',
|
|
'right': 'left',
|
|
'down': 'up'
|
|
};
|
|
|
|
if (!this._focus) {
|
|
this._previous = null;
|
|
this.focus();
|
|
} else {
|
|
var elem = this.navigate(this._focus, direction);
|
|
if (!elem) {
|
|
return false;
|
|
}
|
|
if (this.rememberSource) {
|
|
this._previous = {
|
|
source: this._focus,
|
|
destination: elem,
|
|
reverse: reverse[direction.toLowerCase()]
|
|
};
|
|
}
|
|
this.unfocus();
|
|
this.focus(elem);
|
|
}
|
|
return true;
|
|
},
|
|
|
|
|
|
canmove: function (direction) {
|
|
if (this._focus) {
|
|
var elem = this.navigate(this._focus, direction);
|
|
|
|
if (elem !== null) return elem;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
|
|
/**
|
|
* Given a direction, find an element nearest to the target element in that
|
|
* direction.
|
|
*
|
|
* @param {SpatialNavigatorElement} target
|
|
* The origin of coordinates for traversal.
|
|
* @param {String} direction
|
|
* It should be "left", "right", "up" or "down".
|
|
*
|
|
* @return {SpatialNavigatorElement}
|
|
* The destination of the element which has the highest priority.
|
|
*
|
|
* @memberof SpatialNavigator.prototype
|
|
*/
|
|
navigate: function snNavigate(target, direction) {
|
|
if (!target || !direction || !this._collection) {
|
|
return null;
|
|
}
|
|
|
|
direction = direction.toLowerCase();
|
|
|
|
var rects = this._getAllRects(target);
|
|
var targetRect = this._getRect(target);
|
|
if (!targetRect || !rects.length) {
|
|
return null;
|
|
}
|
|
|
|
/* Get distance functions for ranking priorities relative to targetRect */
|
|
var distanceFunction = this._getDistanceFunction(targetRect);
|
|
|
|
/* Candidate {@link Rect}s are divided into nine regions based on its
|
|
position with respect to targetRect. */
|
|
var groups = this._partition(rects, targetRect);
|
|
|
|
/* {@link Rect}s in group 4 overlaps with targetRect. We distribute them
|
|
further into 9 regions based on its position with respect to the
|
|
center point of targetRect. */
|
|
var internalGroups = this._partition(groups[4], targetRect.center);
|
|
|
|
/* priorities: This big array carrys candidate elements with related
|
|
* distance functions by appropriate priority we want. Depenging on the
|
|
* direction, 3 kinds of elements are added separately in the following
|
|
* order:
|
|
*
|
|
* - 1st: candidates centered inside targetRect (group 4)
|
|
* (so we pick up corresponding internalGroups).
|
|
* - 2nd: in groups of straight direction (group 1, 3, 5, 7).
|
|
* - 3rd: in groups of oblique direction (group 0, 2, 6, 8).
|
|
*
|
|
* For each kind of element above, ranking is performed by the following
|
|
* rules (distance functions) in order:
|
|
*
|
|
* - 1st: distance between candidate and target.
|
|
* - 2nd: absolute coordinate of candidates.
|
|
* - 3rd: distance of left or top coordinate between candidate and
|
|
* target (for oblique direction only)
|
|
*
|
|
* The switch...case block below is just to construct this array.
|
|
* We just pick the required order into array here, then call
|
|
* {SpatialNavigator#_prioritize} to do the trick.
|
|
*/
|
|
var priorities;
|
|
|
|
switch (direction) {
|
|
case 'left':
|
|
priorities = [
|
|
{
|
|
group: internalGroups[0].concat(internalGroups[3])
|
|
.concat(internalGroups[6]),
|
|
distance: [
|
|
distanceFunction.nearPlumbLineIsBetter,
|
|
distanceFunction.topIsBetter
|
|
]
|
|
},
|
|
{
|
|
group: groups[3],
|
|
distance: [
|
|
distanceFunction.nearPlumbLineIsBetter,
|
|
distanceFunction.topIsBetter
|
|
]
|
|
},
|
|
{
|
|
group: groups[0].concat(groups[6]),
|
|
distance: [
|
|
distanceFunction.nearHorizonIsBetter,
|
|
distanceFunction.rightIsBetter,
|
|
distanceFunction.nearTargetTopIsBetter
|
|
]
|
|
}
|
|
];
|
|
break;
|
|
case 'right':
|
|
priorities = [
|
|
{
|
|
group: internalGroups[2].concat(internalGroups[5])
|
|
.concat(internalGroups[8]),
|
|
distance: [
|
|
distanceFunction.nearPlumbLineIsBetter,
|
|
distanceFunction.topIsBetter
|
|
]
|
|
},
|
|
{
|
|
group: groups[5],
|
|
distance: [
|
|
distanceFunction.nearPlumbLineIsBetter,
|
|
distanceFunction.topIsBetter
|
|
]
|
|
},
|
|
{
|
|
group: groups[2].concat(groups[8]),
|
|
distance: [
|
|
distanceFunction.nearHorizonIsBetter,
|
|
distanceFunction.leftIsBetter,
|
|
distanceFunction.nearTargetTopIsBetter
|
|
]
|
|
}
|
|
];
|
|
break;
|
|
case 'up':
|
|
priorities = [
|
|
{
|
|
group: internalGroups[0].concat(internalGroups[1])
|
|
.concat(internalGroups[2]),
|
|
distance: [
|
|
distanceFunction.nearHorizonIsBetter,
|
|
distanceFunction.leftIsBetter
|
|
]
|
|
},
|
|
{
|
|
group: groups[1],
|
|
distance: [
|
|
distanceFunction.nearHorizonIsBetter,
|
|
distanceFunction.leftIsBetter
|
|
]
|
|
},
|
|
{
|
|
group: groups[0].concat(groups[2]),
|
|
distance: [
|
|
distanceFunction.nearPlumbLineIsBetter,
|
|
distanceFunction.bottomIsBetter,
|
|
distanceFunction.nearTargetLeftIsBetter
|
|
]
|
|
}
|
|
];
|
|
break;
|
|
case 'down':
|
|
priorities = [
|
|
{
|
|
group: internalGroups[6].concat(internalGroups[7])
|
|
.concat(internalGroups[8]),
|
|
distance: [
|
|
distanceFunction.nearHorizonIsBetter,
|
|
distanceFunction.leftIsBetter
|
|
]
|
|
},
|
|
{
|
|
group: groups[7],
|
|
distance: [
|
|
distanceFunction.nearHorizonIsBetter,
|
|
distanceFunction.leftIsBetter
|
|
]
|
|
},
|
|
{
|
|
group: groups[6].concat(groups[8]),
|
|
distance: [
|
|
distanceFunction.nearPlumbLineIsBetter,
|
|
distanceFunction.topIsBetter,
|
|
distanceFunction.nearTargetLeftIsBetter
|
|
]
|
|
}
|
|
];
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
if (this.straightOnly) {
|
|
// Ignore candidates in oblique direction.
|
|
priorities.pop();
|
|
}
|
|
|
|
var dest = this._prioritize(priorities, target, direction);
|
|
if (!dest) {
|
|
return null;
|
|
}
|
|
|
|
return dest.element;
|
|
}
|
|
};
|
|
|
|
var Navigator = new SpatialNavigator(); |