lampa-main в git: клиент переживает пересоздание контейнера
- каталог 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
This commit is contained in:
@@ -0,0 +1,962 @@
|
||||
/**
|
||||
* 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();
|
||||
Reference in New Issue
Block a user