UNPKG

db-lgtv-focus-engine

Version:

the Best TV focus engine

291 lines (275 loc) 10.9 kB
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