[JavaScript插件系列] 列表菜单控制器 turn-page-refresh

本文介绍了JavaScript插件Turn-Page-Refresh,主要用于控制列表或菜单内容更新,适用于机顶盒场景。插件支持上下键、上下页键和数字键操作,遵循不处理具体逻辑的原则,通过回调函数处理与列表相关逻辑。文章详细讲解了插件的使用、构建、功能和核心代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Table of Contents

  1. 前言
  2. Turn-Page-Refresh
  3. Build
  4. Usage
    1. create `turn.List` instance
    2. listen key handler
  5. Functions
    1. updateFocus, required
    2. updateTpl, required
    3. clearTpl, required, clear the list content
  6. callbacks
    1. inited
    2. moveUpDown
    3. updateRowsDone
    4. updateFocusDone
    5. inputingNumber
  7. Code
    1. updateRows
    2. updateFocus
    3. udpateList
    4. inputNum
  8. 总结

前言

苦逼的程序猿,尤其苦逼的 IPTV 行业程序猿,作为程序猿一枚,除了写代码,应对各种需要,还要应对现场各种销售运营们,甚至平台,运营商的絮絮叨叨。

今天又是要通宵的一晚(以后估计家常便饭了),也是自己作死生产了个小错误导致升级失败。

老板:嗯(若有所思…),以后升级统统放到凌晨 4 点你们晚上就好好准备升级事宜吧!!!

此处省略若干不雅词汇…..

另外,EPG 的重构总算告一段落了,后续几个省份会陆续上线重构后的新 EPG,还是有些许哈皮期待的,毕竟是经过自己各种加班加点,各种挤时间,自己一行一行敲下来的。

由于兼容性的问题,自己也不得不去自己实现一些简单的通用功能,对自己也算是一种提升吧(虽然比较简单,菜逼或许就这样吧,有点小成就就得意忘形了………)

Turn-Page-Refresh

之所以命名成这个,主要这个插件的功能就是用来控制列表或菜单的内容更新的,而针对列表是一页一页的显示的因此命名成了: turn-page-refresh

它的使用场景主要是在 IPTV 机顶盒上面,因为干运营商这块,厂家太多了真的不敢用网上的一些插件,保不定里面使用的一些新功能导致任意一款盒子跑不起来,这锅可背不起来(陕西就因为这个,27款盒子其中一款有5万用户的华为老款盒子跑步起来),最后商务部协商 KO 这款盒子(可运气不总是这么好…))。

这插件支持的功能主要有如下:

  1. 上下键(38/40) 移动焦点,到头到尾翻页
  2. 上下页键(33/34) 翻页或移动焦点
  3. 数字键定位列表项

实现起来并没有什么难点,主要需要考虑的情况比较多

先说说怎么使用吧

但是该插件遵循一个原则,即其内部不处理任何与 DOM 相关的逻辑,所有与 DOM 相关的处理都在创建实例的时候由提供的回调来完成。

Build

git clone https://github.com/gcclll/turn-page-refresh.git

如果要在线测试可以运行:

npm run dev:iife

另外还有其他一些命令可供使用,主要不同在于打包之后的引入方式:

  1. commonjs: npm run dev:cjs

  2. es6 import: npm run dev:esm

  3. umd: npm run dev:umd

如果用于生产环境:

  • build: npm run build

然后 dist/ 下面会生产如下文件,根据实际情况选择使用:

dist/turn.cjs.js 12.80kb
dist/turn.esm.js 12.78kb
dist/turn.umd.js 13.03kb
dist/turn.iife.js 12.82kb
dist/turn.cjs.min.js 8.06kb (gzipped: 2.26kb)
dist/turn.esm.min.js 0.09kb (gzipped: 0.10kb)
dist/turn.umd.min.js 7.74kb (gzipped: 2.17kb)
dist/turn.iife.min.js 7.60kb (gzipped: 2.11kb)

turn.esm.min.js 压缩有点问题,目前还没解决,可以用 turn.esm.js 先顶着,作为外部文件引入最后项目打包也会压缩代码。

Usage

import or script

<script src="./dist/turn.iife.js"></script>

create turn.List instance

const list = new List({
  statics: {
    nos: document.querySelectorAll('.tvod-list-item-no'),
    cnames: document.querySelectorAll('.tvod-list-item-name'),
    icons: document.querySelectorAll('.tvod-list-item-icon img'),
    items: [].slice.call(document.querySelectorAll('ul li'))
  },
  datas: clist,
  callbacks: {
    moveUpDown: function(opts) {
      console.log([].slice.call(arguments)[0], 'moveUpDown')
    }
  },

  updateFocus: function(opts) {
    // the callback for focus changed by up/down/left/right key
  },

  updateTpl: function(statics, index, data) {
    // update the template
    // template in the every item of the list
  },

  clearTpl: function(statics) {
    // clear the list content
  }
})

实例化之后有些回调函数是必须的

For example:

callbacks.moveUpDown

用来监听列表焦点上下移动的回调

listen key handler

生成实例之后,需要将 turn 内部的按键处理 list.keyHandler 设置响应

window.onkeydown = event => {
  list.keyHandler(event.keyCode || event.which)
}

Functions

主要使用到的回调函数

updateFocus, required

用来更新新旧行的样式和内容

updateFocus: function(opts) {
  if (!opts) { return false }
  const newIcon = opts.statics.icons[opts.newIdx]
  if (opts.newIdx !== opts.oldIdx) {
    const oldIcon = opts.statics.icons[opts.oldIdx]
    if (oldIcon) {
      oldIcon.src = './assets/images/icon_tvod_22x22.png'
      oldIcon.style.width = '22px'
      oldIcon.style.height = '22px'
    }
  }

  if (newIcon) {
    newIcon.src = './assets/images/icon_tvod_26x26.png'
    newIcon.style.width = '26px'
    newIcon.style.height = '26px'
  }
}

updateTpl, required

用来更新列表行内容的模版,即列表中的行中有多少元素,在更新焦点的时候需要更新哪些东西,就在这里面定义

updateTpl: function(statics, index, data) {
  if (!statics || !data) { return false }
  const no = statics.nos && statics.nos[index]
  const name = statics.cnames && statics.cnames[index]
  const icon = statics.icons && statics.icons[index]
  no && (no.innerHTML = fillZero(data.ChannelNumber))
  name && (name.innerHTML = data.ChannelName)
  icon && (icon.style.visibility = 'visible')
}

这个函数会有三个参数

  1. statics

    第一个也是 new 的时候传递进去的 DOM 元素缓存集合, 其中的属性值都是在DOM创建完成之后缓存进去的,避免后续频繁操作DOM的时候去重新获取元素。

  2. index

    当前行的索引值,可以根据这个去获取当前行对应的一些数据。

  3. data

    当前行对应的数据。

turn 里面会根据提供的这个模版更新函数,去更新新旧行的样式或内容

clearTpl, required, clear the list content

清空列表内容,在翻页更新列表内容时调用。

clearTpl: function(statics) {
  if (!statics) {
    return false
  }

  for (let i = 0; i < statics.items.length; i++) {
    const no = statics.nos[i]
    const name = statics.cnames[i]
    const icon = statics.icons[i]
    no.innerHTML = ''
    name.innerHTML = ''
    icon.style.visibility = 'hidden'
  }
}

callbacks

回调函数。

turn 内部通过 execCallbacks(fnName) 去执行的回调函数,也就是说每个回调函数的参数都是一样的。

execCallbacks(fnName) {
  if (!this.datas || !this.datas.length) { return false }

  const callbacks = this.callbacks
  if (!callbacks) { return false }
  const fn = callbacks[fnName] || function() {}

  fn({
    data: this.datas[this.dataIdx],
    currIdx: this.currIdx,
    oldIdx: this.oldIdx,
    dataIdx: this.dataIdx,
    inputNums: this.inputNums
  })
}

All supported callbacks as below:

inited

初始化函数,在 Turn.List 实例化成功之后调用

moveUpDown

The movement of the list by key-up(38) and key-down(40).

The key-up/down(38/40) key event will trigger moveUpDown callback, you can do things like updating row styles, update row content, request the second list datas, and so on.

列表行焦点移动时触发的回调,在这个里面你可以实时的更新当前行的焦点及其样式,甚至可以去请求第二列的数据(如果有的话)

updateRowsDone

This will be triggered by the list’s content updated, the several scenes as follow:

该回调在列表数据刷新的时候出发,刷新列表出发环境有以下几种:

  1. 上键到头翻页(key up to end)。
  2. 下键到头翻页(key down to end)。
  3. 上下页键翻页(key pageup, pagedown)。
  4. 数字键切台(number keys to switch channel)。

updateFocusDone

This will be triggered by the current row focused.

该回调会在列表当前行获得焦点并设置完样式之后触发。

inputingNumber

The callback when inputing number between zero up to nine, and it will log these numbers into this.inputNums = [] defined in Turn.List.

数字输入事件的回调,会实时记录已经输入的数字,在 2 s 之后会触发切台,超出列表范围不做任何操作。

Code

核心代码部分

updateRows

updateRows(items, datas) {
  items = items || this.items

  if (!items || !items.length) {
    // throw new Error('[Turn/List] update rows failed, no items.')
    return false
  }

  if (!datas || !datas.length) {
    // throw new Error('[Turn/List] update rows failed, no datas.')
    return false
  }

  this.clearTpl(this.statics)

  // 遍历所有列表项,更新行内容
  for (let i = 0; i < this.rows; i++) {
    if (this.updateTpl && this.items[i] && datas[i]) {
      this.updateTpl(this.statics, i, datas[i])
    }
  }

  this.execCallbacks('updateRowsDone')
}

updateFocus

updateFocus() {
  if (!this.datas || !this.datas.length) { return false }
  const newEl = this.items[this.currIdx]
  const oldEl = this.items[this.oldIdx]

  if (this.currIdx !== this.oldIdx) {
    cls.remove(oldEl, 'focus')
  }

  if (!this.noInitFocus) {
    cls.add(newEl, 'focus')
  } else {
    this.noInitFocus = false
  }

  this._updateFocus && this._updateFocus({
    newIdx: this.currIdx, oldIdx: this.oldIdx, statics: this.statics
  })

  this.execCallbacks('updateFocusDone')
}

先调用 cls 去更新焦点样式 focus , 后面执行的时外部提供的更新焦点回调(如果有的话)

udpateList

更新列表的核心部分

updateList(direction) {
  if (!this.datas || !this.datas.length) { return false }
  const upCondition = (
    (direction === this.vals.down && this.dataIdx % this.rows === 0) ||
      (direction === this.vals.pdown)
  )
  const downCondition = (
    // direction is up && (not last page || last page)
    (direction === this.vals.up && (
      ((this.dataIdx + 1) % this.rows === 0) ||
        this.dataIdx === this.datas.length - 1)) ||
      (direction === this.vals.pup)
  )

  const mod = this.datas.length % this.rows
  const total = this.datas.length
  // 上下达到翻页条件,或者数字键,更新列表内容
  if (upCondition || downCondition || direction === this.vals.jump) {
    let datas = this.datas.slice(this.dataIdx, this.dataIdx + this.rows)
    if (direction === this.vals.up) {
      // 这里的 rows 需要考虑到从第一页翻页到最后一页的时候,数据索引问题
      // from first page first row turn up to the last page last one
      let rows = (
        (this.dataIdx === total - 1) ? (mod || this.rows) : this.rows
      )

      // if (rows <= 0) { rows = this.rows }

      let start = this.dataIdx - rows + 1, end = this.dataIdx + 1
      start = start < 0 ? 0 : start

      // 取出需要更新的数据
      datas = this.datas.slice(start, end)
    } else if (direction === this.vals.pup) {
      // from first page first row turn up to the last page last one
      let rows = (
        (this.dataIdx === total - mod) ? mod : this.rows
      )

      if (rows <= 0) { rows = this.rows }

      let start = this.dataIdx - this.dataIdx % this.rows
      let end = this.dataIdx + rows

      datas = this.datas.slice(start, end)
    } else if (direction === this.vals.pdown || direction === this.vals.jump) {
      const start = this.dataIdx - this.dataIdx % this.rows
      datas = this.datas.slice(start < 0 ? 0 : start, start + this.rows)
    }

    this.updateRows(this.items, datas)
  }
}

inputNum

数字输入处理

inputNum(num) {
  if (typeof(num) !== 'number' || (num < 0 || num > 9)) {
    return
  }

  // 缓存输入的数字
  this.inputNums.push(num)
  let nums = this.inputNums
  // 频道号数字
  let channelNum = parseInt(nums.join(''), 10)
  this.execCallbacks('inputingNumber')
  clearTimeout(this.inputTimer)
  this.inputTimer = setTimeout(() => {
    clearTimeout(this.jumpTimer)
    this.jump(channelNum)
  }, 2000)

  if (this.inputNums.length >= 3) {
    clearTimeout(this.inputTimer)
    clearTimeout(this.jumpTimer)
    this.jumpTimer = setTimeout(() => {
      this.jump(channelNum)
    }, 800)
    return
  }
}

总结

由于公司项目性质的原因,会有大量使用列表或菜单地方,之前是采用 offsetLeft/Topleft/top 来控制菜单的滚动,但是这个在按键太快的时候有些盒子上表现会有点问题,最后只能通过给按键加上延时来处理,规避这个问题。

采用滚动方式的插件地址: https://github.com/gcclll/scroll-driver.git

加上盒子上对动画的需求不是很强烈,因此会考虑使用这种刷新数据方式来搞。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

若叶岂知秋vip

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值