db-lgtv-focus-engine
Version:
the Best TV focus engine
291 lines (275 loc) • 10.9 kB
JavaScript
import Point from './Point'
class Leaf {
constructor(engine, el) {
// 构建Leaf
this.engine = engine
this.id = el.getAttribute('db-id') || window.getIdByDom(el)
this.rect = el.getBoundingClientRect()
this.el = el
this.parent = null
// freeze 逻辑顺序 => trunk 未冻结 leaf 冻结状态优先
// freeze 逻辑顺序 => trunk 冻结 leaf 冻结状态无效
if (el.hasAttribute('db-freeze')) this.freeze()
if (el.hasAttribute('db-block')) {
this.block = el.getAttribute('db-block')
}
if (el.hasAttribute('db-inline')) {
this.inline = el.getAttribute('db-inline')
}
// 等外层leaf_pool初始化逻辑跑完
// 点击事件
if (engine.options.MOUSE_OK) {
el.onclick = () => {
if (this.engine.focused_map[window.location.href] === this.id) {
this.ok()
} else this.focus()
}
}
// 鼠标停留事件
if (engine.options.MOUSE_FOCUS) {
el.onmouseenter = () => {
window._onmouseenter_t && clearTimeout(window._onmouseenter_t)
window._onmouseenter_t = setTimeout(() => {
if (!window.is_scrolling) this.focus({
noquick: true
})
}, 300)
}
}
}
findAllNeighbor() {
this.findNeighbor('top')
this.findNeighbor('bottom')
this.findNeighbor('left')
this.findNeighbor('right')
}
clearAllNeighbor() {
this.left = null
this.right = null
this.top = null
this.bottom = null
}
isLeafVisibleFor(leaf = this) {
// display不可见
if (!this.el) return false
if (this.el.style.display === 'none') return false
if (this.el.offsetParent === null) return false
if (this.parent && this.parent instanceof this.engine.Trunk) {
if (this.parent === leaf.parent) return true
// 超出左侧区域
if ((this.rect.left - this.parent.rect.left) > (this.parent.el.offsetWidth - this.el.offsetWidth / 2)) return false
// 超出右侧区域
if ((this.rect.left - this.parent.rect.left) < (-this.el.offsetWidth / 2)) return false
// 超出上侧区域
if ((this.rect.top - this.parent.rect.top) > (this.parent.el.offsetHeight - this.el.offsetHeight / 2)) return false
// 超出下侧区域
if ((this.rect.top - this.parent.rect.top) < (-this.el.offsetHeight / 2)) return false
}
return true
}
findNeighbor(direction, parent = this.parent) {
this[direction] = null
// 自定义邻居虚拟dom
function diyNeighbor(direction) {
if (!this.el) return
let db_id = this.el.getAttribute(`db-${direction}`)
if (db_id == 'null') {
return true
}
if (!db_id) return
let _n = this.engine.leaf_pool.find(_i => _i.id === db_id)
if (!_n) return
this[direction] = _n
return _n
}
if (diyNeighbor.apply(this, arguments)) return
let pool = (parent ? parent.child : null) || this.engine.leaf_pool
// 过滤不可视的leaf
pool = pool.filter(_i => _i.isLeafVisibleFor(this))
// 智能查找邻居虚拟dom
let _x = {
top: this.rect.left + this.rect.width / 2,
bottom: this.rect.left + this.rect.width / 2,
left: this.rect.left,
right: this.rect.left + this.rect.width
} [direction]
let _y = {
top: this.rect.top,
bottom: this.rect.top + this.rect.height,
left: this.rect.top + this.rect.height / 2,
right: this.rect.top + this.rect.height / 2
} [direction]
this.point = new Point(_x, _y)
let min_distance = null
pool.filter(_i => !_i.freezed).forEach(__i => {
let __x = {
top: __i.rect.left + __i.rect.width / 2,
bottom: __i.rect.left + __i.rect.width / 2,
left: (__i.rect.left + __i.rect.width),
right: __i.rect.left
} [direction]
let __y = {
top: (__i.rect.top + __i.rect.height),
bottom: __i.rect.top,
left: __i.rect.top + __i.rect.height / 2,
right: __i.rect.top + __i.rect.height / 2
} [direction]
let __point = new Point(__x, __y)
let __distance = __point.getWeightDistance(this.point, direction)
if (direction === 'top' && __y > (_y + 2)) return
if (direction === 'bottom' && __y < (_y - 2)) return
if (direction === 'left' && __x > (_x + 2)) return
if (direction === 'right' && __x < (_x - 2)) return
if (min_distance === null || __distance < min_distance) {
min_distance = __distance
this[direction] = __i
}
})
if (parent && !this[direction]) {
// 父组件允许穿透
if (parent[`${direction}_exit`]) {
this.findNeighbor(direction, parent.parent || 0)
}
if (parent instanceof this.engine.LongScroll) return
let event_name = 'touch' + direction.charAt(0).toUpperCase() + direction.slice(1);
parent.el.dispatchEvent(new Event(event_name))
}
}
blur() {
this.el.removeAttribute('db-focus')
this.el.dispatchEvent(new Event("blur"))
this.engine.focused_map[window.location.href] = undefined
}
focus({
behavior,
noquick
} = {}) {
/** 鉴权 => Leaf是否失效(由于dom在mvvm框架中可能被复用,绑定事件未被清除,仍然可能触发闭包中的focus导致异常焦点) */
if (!this.engine.leaf_pool.includes(this)) return
/** --------------------------------------------- */
let is_same = (this.engine.focused_map[window.location.href] === this.id)
if (!is_same) {
// 抛出焦点框架聚焦事件
if (this.engine.onFocus) this.engine.onFocus(this)
// focused_map记录
let old_focused = this.engine.findFocusedLeaf()
// 分发父元素事件
let _o = this
while (_o.parent) {
_o = _o.parent
_o.focus()
let _e = new Event("change")
_e.leaf = this
_o.el.dispatchEvent(_e)
}
// 撤销旧聚焦
if (old_focused) {
old_focused.blur()
let _o = old_focused
while (_o.parent) {
_o = _o.parent
if (!_o.el.contains(this.el)) {
_o.blur()
}
}
}
// 清除异常焦点残留
let focus_pool = document.querySelectorAll('[db-focus]')
Array.from(focus_pool).forEach(_i => {
_i.removeAttribute('db-focus')
})
// 操作轨迹记录
let history = this.engine.history
history.push({
url: window.location.href,
cmd: 'focus',
leaf_id: this.id
})
// 事件分发
setTimeout(() => {
this.el.dispatchEvent(new Event("focus"))
})
this.engine.focused_map[window.location.href] = this.id
if (this.block !== 'none' && this.inline !== 'none') {
this.el.scrollIntoView({
behavior: behavior || this.engine.options.SCROLL_BEHAVIOR,
block: this.block || "center",
inline: this.inline || "center",
speed: ((!noquick) && this.engine.is_smooth_scrolling) ? this.engine.options.KEY_SPEED : undefined,
msg: '常规移动'
})
}
// 滚动完成后 => 重新定位 => 避免落焦误差
this.engine.$nextScroll(() => {
this.engine.leaf_pool.forEach(leaf => leaf.rect = leaf.el.getBoundingClientRect())
this.engine.trunk_pool.forEach(trunk => trunk.rect = trunk.el.getBoundingClientRect())
this.findAllNeighbor()
})
} else {
// 保持父元素属性
let _o = this
while (_o.parent) {
_o = _o.parent
_o.el.setAttribute('db-child-focus', '')
}
}
// 缓存
if (this.parent && this.parent.cacheable === 'focus') this.cache()
// 状态属性
this.el.setAttribute('db-focus', '')
}
cache() {
if (!this.parent || !this.parent.cacheable) {
return
}
if (this.parent.cache) {
this.parent.cache.uncache()
}
if (!this.engine.cached_map[window.location.href]) {
this.engine.cached_map[window.location.href] = {}
}
this.engine.cached_map[window.location.href][this.id] = true
this.parent.cache = this
this.el.setAttribute('db-cache', '')
}
uncache() {
if (!this.parent || !this.parent.cacheable) {
return
}
if (!this.engine.cached_map[window.location.href]) {
this.engine.cached_map[window.location.href] = {}
}
delete(this.engine.cached_map[window.location.href][this.id])
this.el.removeAttribute('db-cache')
}
freeze({
render = true
} = {}) {
this.freezed = true
if (!this.engine.freezed_map[window.location.href]) this.engine.freezed_map[window.location.href] = {}
if (this.engine.freezed_map[window.location.href][this.id]) return
this.engine.freezed_map[window.location.href][this.id] = true
if (render) this.engine.render()
}
unfreeze({
render = true
} = {}) {
this.freezed = false
if (!this.engine.freezed_map[window.location.href]) this.engine.freezed_map[window.location.href] = {}
if (!this.engine.freezed_map[window.location.href][this.id]) return
delete(this.engine.freezed_map[window.location.href][this.id])
if (render) this.engine.render()
}
ok() {
if (this.parent && this.parent.cacheable === 'click') this.cache()
this.el.dispatchEvent(new Event("ok"))
let _o = this
while (_o.parent) {
_o = _o.parent
let _e = new Event("ok")
_e.leaf = this
_o.el.dispatchEvent(_e)
}
}
}
export default Leaf