@chancestv/tv-focus 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -64,278 +64,172 @@ function getRect(elem) {
64
64
  rect.center.top = rect.center.bottom = rect.center.y;
65
65
  return rect;
66
66
  }
67
- function partition(rects, targetRect, straightOverlapThreshold) {
68
- var groups = [[], [], [], [], [], [], [], [], []];
69
- for (var i = 0; i < rects.length; i++) {
70
- var rect = rects[i];
71
- var center = rect.center;
72
- var x, y, groupId;
73
- if (center.x < targetRect.left) {
74
- x = 0;
75
- } else if (center.x <= targetRect.right) {
76
- x = 1;
77
- } else {
78
- x = 2;
79
- }
80
- if (center.y < targetRect.top) {
81
- y = 0;
82
- } else if (center.y <= targetRect.bottom) {
83
- y = 1;
84
- } else {
85
- y = 2;
86
- }
87
- groupId = y * 3 + x;
88
- groups[groupId].push(rect);
89
- if ([0, 2, 6, 8].indexOf(groupId) !== -1) {
90
- var threshold = straightOverlapThreshold;
91
- if (rect.left <= targetRect.right - targetRect.width * threshold) {
92
- if (groupId === 2) {
93
- groups[1].push(rect);
94
- } else if (groupId === 8) {
95
- groups[7].push(rect);
96
- }
97
- }
98
- if (rect.right >= targetRect.left + targetRect.width * threshold) {
99
- if (groupId === 0) {
100
- groups[1].push(rect);
101
- } else if (groupId === 6) {
102
- groups[7].push(rect);
103
- }
104
- }
105
- if (rect.top <= targetRect.bottom - targetRect.height * threshold) {
106
- if (groupId === 6) {
107
- groups[3].push(rect);
108
- } else if (groupId === 8) {
109
- groups[5].push(rect);
110
- }
111
- }
112
- if (rect.bottom >= targetRect.top + targetRect.height * threshold) {
113
- if (groupId === 0) {
114
- groups[3].push(rect);
115
- } else if (groupId === 2) {
116
- groups[5].push(rect);
117
- }
118
- }
119
- }
67
+ function snMajorAxisDistanceRaw(direction, source, dest) {
68
+ switch (direction) {
69
+ case "left":
70
+ return source.left - dest.right;
71
+ case "right":
72
+ return dest.left - source.right;
73
+ case "up":
74
+ return source.top - dest.bottom;
75
+ case "down":
76
+ return dest.top - source.bottom;
77
+ default:
78
+ return 0;
120
79
  }
121
- return groups;
122
80
  }
123
- function generateDistanceFunction(targetRect) {
124
- return {
125
- nearPlumbLineIsBetter: function(rect) {
126
- var d;
127
- if (rect.center.x < targetRect.center.x) {
128
- d = targetRect.center.x - rect.right;
129
- } else {
130
- d = rect.left - targetRect.center.x;
131
- }
132
- return d < 0 ? 0 : d;
133
- },
134
- nearHorizonIsBetter: function(rect) {
135
- var d;
136
- if (rect.center.y < targetRect.center.y) {
137
- d = targetRect.center.y - rect.bottom;
138
- } else {
139
- d = rect.top - targetRect.center.y;
140
- }
141
- return d < 0 ? 0 : d;
142
- },
143
- nearTargetLeftIsBetter: function(rect) {
144
- var d;
145
- if (rect.center.x < targetRect.center.x) {
146
- d = targetRect.left - rect.right;
147
- } else {
148
- d = rect.left - targetRect.left;
149
- }
150
- return d < 0 ? 0 : d;
151
- },
152
- nearTargetTopIsBetter: function(rect) {
153
- var d;
154
- if (rect.center.y < targetRect.center.y) {
155
- d = targetRect.top - rect.bottom;
156
- } else {
157
- d = rect.top - targetRect.top;
158
- }
159
- return d < 0 ? 0 : d;
160
- },
161
- topIsBetter: function(rect) {
162
- return rect.top;
163
- },
164
- bottomIsBetter: function(rect) {
165
- return -1 * rect.bottom;
166
- },
167
- leftIsBetter: function(rect) {
168
- return rect.left;
169
- },
170
- rightIsBetter: function(rect) {
171
- return -1 * rect.right;
172
- }
173
- };
81
+ function snMajorAxisDistance(direction, source, dest) {
82
+ return Math.max(0, snMajorAxisDistanceRaw(direction, source, dest));
174
83
  }
175
- function prioritize(priorities) {
176
- var destPriority = null;
177
- for (var i = 0; i < priorities.length; i++) {
178
- if (priorities[i].group.length) {
179
- destPriority = priorities[i];
180
- break;
181
- }
84
+ function snMajorAxisDistanceToFarEdgeRaw(direction, source, dest) {
85
+ switch (direction) {
86
+ case "left":
87
+ return source.left - dest.left;
88
+ case "right":
89
+ return dest.right - source.right;
90
+ case "up":
91
+ return source.top - dest.top;
92
+ case "down":
93
+ return dest.bottom - source.bottom;
94
+ default:
95
+ return 0;
182
96
  }
183
- if (!destPriority) {
184
- return null;
97
+ }
98
+ function snMajorAxisDistanceToFarEdge(direction, source, dest) {
99
+ return Math.max(1, snMajorAxisDistanceToFarEdgeRaw(direction, source, dest));
100
+ }
101
+ function snMinorAxisDistance(direction, source, dest) {
102
+ switch (direction) {
103
+ case "left":
104
+ case "right":
105
+ return Math.abs(source.center.y - dest.center.y);
106
+ case "up":
107
+ case "down":
108
+ return Math.abs(source.center.x - dest.center.x);
109
+ default:
110
+ return 0;
185
111
  }
186
- var destDistance = destPriority.distance;
187
- destPriority.group.sort(function(a, b) {
188
- for (var i2 = 0; i2 < destDistance.length; i2++) {
189
- var distance = destDistance[i2];
190
- var delta = distance(a) - distance(b);
191
- if (delta) {
192
- return delta;
193
- }
194
- }
195
- return 0;
196
- });
197
- return destPriority.group;
112
+ }
113
+ function snGetWeightedDistanceFor(major, minor) {
114
+ return 13 * major * major + minor * minor;
115
+ }
116
+ function snIsToDirectionOf(direction, source, dest) {
117
+ switch (direction) {
118
+ case "left":
119
+ return source.left >= dest.right;
120
+ case "right":
121
+ return source.right <= dest.left;
122
+ case "up":
123
+ return source.top >= dest.bottom;
124
+ case "down":
125
+ return source.bottom <= dest.top;
126
+ default:
127
+ return false;
128
+ }
129
+ }
130
+ function snIsCandidate(source, dest, direction) {
131
+ switch (direction) {
132
+ case "left":
133
+ return (source.right > dest.right || source.left >= dest.right) && source.left > dest.left;
134
+ case "right":
135
+ return (source.left < dest.left || source.right <= dest.left) && source.right < dest.right;
136
+ case "up":
137
+ return (source.bottom > dest.bottom || source.top >= dest.bottom) && source.top > dest.top;
138
+ case "down":
139
+ return (source.top < dest.top || source.bottom <= dest.top) && source.bottom < dest.bottom;
140
+ default:
141
+ return false;
142
+ }
143
+ }
144
+ function snBeamsOverlap(direction, rect1, rect2) {
145
+ switch (direction) {
146
+ case "left":
147
+ case "right":
148
+ return rect2.bottom >= rect1.top && rect2.top <= rect1.bottom;
149
+ case "up":
150
+ case "down":
151
+ return rect2.right >= rect1.left && rect2.left <= rect1.right;
152
+ default:
153
+ return false;
154
+ }
155
+ }
156
+ function snBeamBeats(direction, source, rect1, rect2) {
157
+ var r1In = snBeamsOverlap(direction, source, rect1);
158
+ var r2In = snBeamsOverlap(direction, source, rect2);
159
+ if (r2In || !r1In) {
160
+ return false;
161
+ }
162
+ if (!snIsToDirectionOf(direction, source, rect2)) {
163
+ return true;
164
+ }
165
+ if (direction === "left" || direction === "right") {
166
+ return true;
167
+ }
168
+ return snMajorAxisDistance(direction, source, rect1) < snMajorAxisDistanceToFarEdge(direction, source, rect2);
169
+ }
170
+ function snIsBetterCandidate(direction, source, rect1, rect2) {
171
+ if (!snIsCandidate(source, rect1, direction)) {
172
+ return false;
173
+ }
174
+ if (!snIsCandidate(source, rect2, direction)) {
175
+ return true;
176
+ }
177
+ if (snBeamBeats(direction, source, rect1, rect2)) {
178
+ return true;
179
+ }
180
+ if (snBeamBeats(direction, source, rect2, rect1)) {
181
+ return false;
182
+ }
183
+ return snGetWeightedDistanceFor(
184
+ snMajorAxisDistance(direction, source, rect1),
185
+ snMinorAxisDistance(direction, source, rect1)
186
+ ) < snGetWeightedDistanceFor(
187
+ snMajorAxisDistance(direction, source, rect2),
188
+ snMinorAxisDistance(direction, source, rect2)
189
+ );
198
190
  }
199
191
  function navigate(target, direction, candidates, config, preferNearest) {
200
192
  if (!target || !direction || !candidates || !candidates.length) {
201
193
  return null;
202
194
  }
195
+ var targetRect = getRect(target);
196
+ if (!targetRect) {
197
+ return null;
198
+ }
203
199
  var rects = [];
204
200
  for (var i = 0; i < candidates.length; i++) {
205
- var rect = getRect(candidates[i]);
206
- if (rect) {
207
- rects.push(rect);
201
+ var r = getRect(candidates[i]);
202
+ if (!r) {
203
+ continue;
204
+ }
205
+ if (!snIsCandidate(targetRect, r, direction)) {
206
+ continue;
207
+ }
208
+ if (config.straightOnly && !snBeamsOverlap(direction, targetRect, r)) {
209
+ continue;
208
210
  }
211
+ rects.push(r);
209
212
  }
210
213
  if (!rects.length) {
211
214
  return null;
212
215
  }
213
- var targetRect = getRect(target);
214
- if (!targetRect) {
215
- return null;
216
- }
217
- var distanceFunction = generateDistanceFunction(targetRect);
218
- var groups = partition(
219
- rects,
220
- targetRect,
221
- config.straightOverlapThreshold
222
- );
223
- var internalGroups = partition(
224
- groups[4],
225
- targetRect.center,
226
- config.straightOverlapThreshold
227
- );
228
- var priorities;
229
- var df = distanceFunction;
230
- var internalGroup, straightGroup, diagonalGroups;
231
- var internalDist, straightDist, diagonalDist, mergedDist;
232
- switch (direction) {
233
- case "left":
234
- internalGroup = internalGroups[0].concat(internalGroups[3]).concat(internalGroups[6]);
235
- straightGroup = groups[3];
236
- diagonalGroups = groups[0].concat(groups[6]);
237
- internalDist = [df.nearPlumbLineIsBetter, df.topIsBetter];
238
- straightDist = [df.nearPlumbLineIsBetter, df.topIsBetter];
239
- diagonalDist = [
240
- df.nearHorizonIsBetter,
241
- df.rightIsBetter,
242
- df.nearTargetTopIsBetter
243
- ];
244
- mergedDist = [
245
- df.nearPlumbLineIsBetter,
246
- df.nearHorizonIsBetter,
247
- df.topIsBetter
248
- ];
249
- break;
250
- case "right":
251
- internalGroup = internalGroups[2].concat(internalGroups[5]).concat(internalGroups[8]);
252
- straightGroup = groups[5];
253
- diagonalGroups = groups[2].concat(groups[8]);
254
- internalDist = [df.nearPlumbLineIsBetter, df.topIsBetter];
255
- straightDist = [df.nearPlumbLineIsBetter, df.topIsBetter];
256
- diagonalDist = [
257
- df.nearHorizonIsBetter,
258
- df.leftIsBetter,
259
- df.nearTargetTopIsBetter
260
- ];
261
- mergedDist = [
262
- df.nearPlumbLineIsBetter,
263
- df.nearHorizonIsBetter,
264
- df.topIsBetter
265
- ];
266
- break;
267
- case "up":
268
- internalGroup = internalGroups[0].concat(internalGroups[1]).concat(internalGroups[2]);
269
- straightGroup = groups[1];
270
- diagonalGroups = groups[0].concat(groups[2]);
271
- internalDist = [df.nearHorizonIsBetter, df.leftIsBetter];
272
- straightDist = [df.nearHorizonIsBetter, df.leftIsBetter];
273
- diagonalDist = [
274
- df.nearPlumbLineIsBetter,
275
- df.bottomIsBetter,
276
- df.nearTargetLeftIsBetter
277
- ];
278
- mergedDist = [
279
- df.nearHorizonIsBetter,
280
- df.nearPlumbLineIsBetter,
281
- df.leftIsBetter
282
- ];
283
- break;
284
- case "down":
285
- internalGroup = internalGroups[6].concat(internalGroups[7]).concat(internalGroups[8]);
286
- straightGroup = groups[7];
287
- diagonalGroups = groups[6].concat(groups[8]);
288
- internalDist = [df.nearHorizonIsBetter, df.leftIsBetter];
289
- straightDist = [df.nearHorizonIsBetter, df.leftIsBetter];
290
- diagonalDist = [
291
- df.nearPlumbLineIsBetter,
292
- df.topIsBetter,
293
- df.nearTargetLeftIsBetter
294
- ];
295
- mergedDist = [
296
- df.nearHorizonIsBetter,
297
- df.nearPlumbLineIsBetter,
298
- df.leftIsBetter
299
- ];
300
- break;
301
- default:
302
- return null;
303
- }
304
- if (preferNearest) {
305
- priorities = [
306
- { group: internalGroup, distance: internalDist },
307
- {
308
- group: config.straightOnly ? straightGroup : straightGroup.concat(diagonalGroups),
309
- distance: mergedDist
310
- }
311
- ];
312
- } else {
313
- priorities = [
314
- { group: internalGroup, distance: internalDist },
315
- { group: straightGroup, distance: straightDist },
316
- { group: diagonalGroups, distance: diagonalDist }
317
- ];
318
- if (config.straightOnly) {
319
- priorities.pop();
216
+ var best = rects[0];
217
+ for (var k = 1; k < rects.length; k++) {
218
+ if (snIsBetterCandidate(direction, targetRect, rects[k], best)) {
219
+ best = rects[k];
320
220
  }
321
221
  }
322
- var destGroup = prioritize(priorities);
323
- if (!destGroup) {
324
- return null;
325
- }
326
- var dest = null;
327
222
  if (config.rememberSource && config.previous && config.previous.destination === target && config.previous.reverse === direction) {
328
- for (var j = 0; j < destGroup.length; j++) {
329
- if (destGroup[j].element === config.previous.target) {
330
- dest = destGroup[j].element;
223
+ for (var j = 0; j < rects.length; j++) {
224
+ if (rects[j].element === config.previous.target) {
225
+ if (!snIsBetterCandidate(direction, targetRect, best, rects[j])) {
226
+ return rects[j].element;
227
+ }
331
228
  break;
332
229
  }
333
230
  }
334
231
  }
335
- if (!dest) {
336
- dest = destGroup[0].element;
337
- }
338
- return dest;
232
+ return best.element;
339
233
  }
340
234
  function generateId() {
341
235
  var id;
@@ -615,7 +509,7 @@ function getScrollScope(elem) {
615
509
  }
616
510
  function navigateWithinScrollScope(target, direction, candidates, config, preferNearest) {
617
511
  if (!candidates || candidates.length < 2) {
618
- return navigate(target, direction, candidates, config, preferNearest);
512
+ return navigate(target, direction, candidates, config);
619
513
  }
620
514
  var targetScope = getScrollScope(target);
621
515
  var inScope = [];
@@ -628,9 +522,60 @@ function navigateWithinScrollScope(target, direction, candidates, config, prefer
628
522
  }
629
523
  }
630
524
  if (inScope.length && outScope.length) {
631
- return navigate(target, direction, inScope, config, preferNearest) || navigate(target, direction, outScope, config, preferNearest);
525
+ return navigate(target, direction, inScope, config) || navigate(target, direction, outScope, config);
526
+ }
527
+ return navigate(target, direction, candidates, config);
528
+ }
529
+ function filterCandidatesByDirection(source, direction, sectionNavMap, excludeSectionId) {
530
+ var srcRect = getRect(source);
531
+ var kept = [];
532
+ for (var id in sectionNavMap) {
533
+ if (id === excludeSectionId) {
534
+ continue;
535
+ }
536
+ var elems = sectionNavMap[id];
537
+ if (!elems || !elems.length) {
538
+ continue;
539
+ }
540
+ var left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity;
541
+ var rects = [];
542
+ for (var i = 0; i < elems.length; i++) {
543
+ var r = getRect(elems[i]);
544
+ if (!r) {
545
+ continue;
546
+ }
547
+ rects.push(r);
548
+ if (r.left < left) {
549
+ left = r.left;
550
+ }
551
+ if (r.top < top) {
552
+ top = r.top;
553
+ }
554
+ if (r.right > right) {
555
+ right = r.right;
556
+ }
557
+ if (r.bottom > bottom) {
558
+ bottom = r.bottom;
559
+ }
560
+ }
561
+ if (!rects.length) {
562
+ continue;
563
+ }
564
+ var unionRect = {
565
+ left,
566
+ top,
567
+ right,
568
+ bottom,
569
+ width: right - left,
570
+ height: bottom - top
571
+ };
572
+ if (snIsCandidate(srcRect, unionRect, direction)) {
573
+ for (var j = 0; j < rects.length; j++) {
574
+ kept.push(rects[j].element);
575
+ }
576
+ }
632
577
  }
633
- return navigate(target, direction, candidates, config, preferNearest);
578
+ return kept;
634
579
  }
635
580
  function focusNext(direction, currentFocusedElement, currentSectionId) {
636
581
  var extSelector = currentFocusedElement.getAttribute("data-sn-" + direction);
@@ -658,12 +603,23 @@ function focusNext(direction, currentFocusedElement, currentSectionId) {
658
603
  config
659
604
  );
660
605
  if (!next && config.restrict == "self-first") {
606
+ var prunedCandidates = filterCandidatesByDirection(
607
+ currentFocusedElement,
608
+ direction,
609
+ sectionNavigableElements,
610
+ currentSectionId
611
+ );
612
+ if (!prunedCandidates.length) {
613
+ prunedCandidates = exclude(
614
+ allNavigableElements,
615
+ currentSectionNavigableElements
616
+ );
617
+ }
661
618
  next = navigateWithinScrollScope(
662
619
  currentFocusedElement,
663
620
  direction,
664
- exclude(allNavigableElements, currentSectionNavigableElements),
665
- config,
666
- true
621
+ prunedCandidates,
622
+ config
667
623
  );
668
624
  }
669
625
  } else {
@@ -671,8 +627,7 @@ function focusNext(direction, currentFocusedElement, currentSectionId) {
671
627
  currentFocusedElement,
672
628
  direction,
673
629
  exclude(allNavigableElements, currentFocusedElement),
674
- config,
675
- false
630
+ config
676
631
  );
677
632
  }
678
633
  if (next) {