├── .editorconfig ├── .eslintignore ├── .gitignore ├── .npmignore ├── LICENSE ├── README-cn.md ├── README.md ├── config ├── rollup.config.js └── uglify.config.js ├── dist ├── .gitkeep ├── tvvm.common.js ├── tvvm.common.min.js ├── tvvm.esm.js ├── tvvm.js └── tvvm.min.js ├── docs ├── .gitkeep ├── favicon.ico ├── fonts │ ├── OpenSans-Regular.ttf │ └── Raleway-Regular.ttf ├── imgs │ ├── index.bmp │ └── logo.png ├── index.html ├── pages │ └── doc.html ├── scripts │ ├── bscroll.min.js │ ├── doc.js │ ├── highlight.pack.js │ └── home.js └── style │ ├── common.css │ ├── default.css │ ├── doc.css │ ├── github.css │ ├── home.css │ ├── iconfont.css │ ├── mono-blue.css │ └── tomorrow.css ├── index.js ├── package-lock.json ├── package.json ├── scripts ├── build.js └── watch.js ├── src ├── .babelrc ├── compileUtil.js ├── compiler.js ├── compiler_backup.js ├── dep.js ├── focuser.js ├── lifycycle.js ├── main.js ├── observer.js └── watcher.js └── test ├── .gitkeep ├── index.html ├── run.test.js └── unit └── compiler.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | .vscode/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .cache 3 | scripts 4 | test 5 | build 6 | package.json 7 | rollup.config.js 8 | yarn.lock 9 | src 10 | website 11 | config 12 | 13 | LICENSE 14 | coverage 15 | .DS_Store 16 | .eslintrc 17 | .eslintignore 18 | .babelrc 19 | .editorconfig 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Float 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-cn.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zexiplus/TVVM/9852ac9edf2c0a2a688e61c0a13b44b64de8aa52/README-cn.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TVVM 2 | 3 | ![build](https://img.shields.io/badge/build-passing-green.svg) ![no-dependency](https://img.shields.io/badge/no-dependency-yellow.svg) 4 | 5 | 轻量级 TV 端 WEB 应用开发框架 6 | 7 | TVVM 是一个专门为 TV WEB APP 开发的 MVVM 模式框架, 它帮助开发者快速开发应用而无需关心焦点控制,键盘绑定,数据绑定等通用逻辑。它没有依赖,体型小巧(20 kb, 官方文档请参考 [offcial web](https://zexiplus.github.io/TVVM) 8 | 9 | 10 | 11 | ## 使用 12 | 13 | **通过 npm 下载** 14 | 15 | 你的系统上需要安装有 nodejs 16 | 17 | ```shell 18 | $ npm install -S tvvm 19 | ``` 20 | 21 | ```js 22 | import * as TVVM from 'tvvm' 23 | 24 | var tv = new TVVM({ 25 | ...options 26 | }) 27 | ``` 28 | 29 | **手动下载** 30 | 31 | 你也可以通过手动下载的方式, 然后通过` 35 | 40 | ``` 41 | 42 | 43 | 44 | ## 特色 45 | 46 | ### 焦点自动控制 47 | 48 | TV 应用的特点是使用遥控器控制焦点移动, 而并非鼠标点击/手势触摸事件来触发控件,从而进行下一步操作。 49 | 50 | 这需要我们通过编程的方式控制焦点的移动和跳转。TVVM 致力于减少焦点移动的代码逻辑, 通过配置 t-index 索引, 而非编程的方式解决焦点的移动。 51 | 52 | TVVM把应用分成横轴空间和纵轴空间, 例如 声明 `t-index="0-1"` 代表应用是第1行 (row=0) 第2列 (col=1)为当前焦点 , 当你点击遥控器的向下移动按钮时 row 增加,col 不变, 此时焦点从 `t-index="0-1"`移动至 `t-index="1-1"`的元素上 。这些在TVVM内部已经做好了, 开发者只需要在焦点块元素上声明 t-index 索引即可。 53 | 54 | 除此之外, TVVM 还提供了丰富的选项,例如默认焦点,焦点元素样式,边界穿越,按键值配置等。 55 | 56 | **focus选项** 57 | 58 | ```js 59 | new TVVM({ 60 | ..., 61 | focus: { 62 | defaultFocusIndex: '0-1', // 默认焦点 63 | circle: { 64 | horizontal: true, // 当焦点移动到边界时在水平方向可穿越 65 | vertical: true // 当焦点移动到边界时在垂直方向可穿越 66 | }, 67 | ...options 68 | } 69 | }) 70 | ``` 71 | 72 | **html模板** 73 | 74 | ```html 75 | 81 |
82 |
83 |
84 |
0-1
85 |
0-2
86 |
0-3
87 |
88 |
89 |
90 |
91 | 1-0, 2-0 92 |
93 |
94 |
95 | 1-1, 1-2, 1-3 96 |
97 |
98 |
2-1
99 |
2-2
100 |
2-3
101 |
102 |
103 |
104 |
105 | ``` 106 | 107 | **显示结果** 108 | 109 | ![t-index-demo](./website/imgs/index.bmp) 110 | 111 | ### 按键去抖优化 112 | 113 | 某些物理遥控器在按下按钮时, 有可能高速触发按键事件, 这对应用会产生不良后果, TVVM 在绑定事件的同时在内部会优化操作逻辑, 利用函数去抖控制按键触发频率, 防止因为物理设备差异导致应用逻辑混乱。 114 | 115 | 116 | 117 | ### 数据驱动 118 | 119 | TVVM 与大多数 mvvm 思想的前端框架一样, 采用数据驱动的开发模式, 简单来讲,数据驱动使开发者只用关系数据的修改, 而无需手动将数据同步至视图。以下是 t-value 指令来实现双向数据绑定的例子, span 标签内的内容会随着 input 输入框的值的改变而改变 120 | 121 | 122 | 123 | ```js 124 | data: function () { 125 | return { 126 | demoInputValue: 'demo' 127 | } 128 | } 129 | ``` 130 | 131 | ```html 132 | 133 | {{data.demoInputValue}} 134 | ``` 135 | 136 | 137 | 138 | ## API 139 | 140 | ### new TVVM(options) 141 | 142 | new TVVM 返回一个tv实例, 作为该页面的全局单例入口 143 | 144 | ```js 145 | var tv = new TVVM({ 146 | el: '#tv', 147 | data () { 148 | return { 149 | dialogVisible: true 150 | } 151 | }, 152 | focus: { 153 | 154 | }, 155 | methods: { 156 | testFn: function (a, b) { 157 | 158 | } 159 | } 160 | }) 161 | ``` 162 | 163 | 164 | 165 | ### 选项 166 | 167 | new TVVM接收一个选项对象作为唯一参数 168 | 169 | #### el 170 | 171 | new TVVM() 实例挂载的dom元素, 可以是一个元素查找符或者dom节点对象 172 | 173 | ```js 174 | new TVVM({ 175 | el: '#tv', 176 | }) 177 | ``` 178 | 179 | #### data 180 | 181 | tm单例的数据对象, 可以是一个函数或者对象, 当该参数为函数时, 取值为该函数的返回值 182 | 183 | ```js 184 | new TVVM({ 185 | data: function () { 186 | return { 187 | title: 'tvvm demo page', 188 | index: '0' 189 | } 190 | } 191 | }) 192 | ``` 193 | 194 | #### methods 195 | 196 | 该参数是一个对象, 存放所有的方法函数 197 | 198 | ```js 199 | methods: { 200 | methods1: function () { 201 | console.log('methods1') 202 | } 203 | } 204 | ``` 205 | 206 | #### focus 207 | 208 | focus选项用于设置焦点移动, 键值绑定, 默认焦点等逻辑 209 | 210 | ```js 211 | new TVVM({ 212 | focus: { 213 | defaultFocusIndex: '1-0', 214 | activeClass: 'high-light', 215 | keysMap: { 216 | 'up': { 217 | codes: [38, 103], 218 | handler: function (event, node, index, prevNode) { 219 | 220 | } 221 | }, 222 | 'g': { 223 | codes: [71], 224 | handler: function (event, node, index, prevNode) { 225 | console.log('you press g') 226 | } 227 | }, 228 | 229 | }, 230 | keysMapMergeCoverage: false, 231 | circle: { 232 | horizontal: true, 233 | vertical: true, 234 | }, 235 | } 236 | }) 237 | ``` 238 | 239 | - **defaultFocusIndex (可选)** 240 | 241 | 进入应用默认聚焦的元素的索引, 该参数为空时, 默认聚焦到页面首个焦点元素上 242 | 243 | ```js 244 | focus: { 245 | defaultIndex: '0-1' 246 | } 247 | ``` 248 | 249 | - **activeClass** 250 | 251 | 焦点元素的样式名 252 | 253 | ```js 254 | focus: { 255 | activeClass: 'high-light' 256 | } 257 | ``` 258 | 259 | - **circle (可选)** 260 | 261 | - **horizontal (可选)** 焦点元素在水平方向是否可以循环移动, 默认false 262 | - **vertival (可选)** 焦点元素在水平方向上是否可以循环移动, 默认false 263 | 264 | ```js 265 | focus: { 266 | circle: { 267 | horizontal: true, 268 | vertical: true 269 | } 270 | } 271 | ``` 272 | 273 | - **keysMap (可选)** 274 | 275 | 遥控器键盘键值码映射表, 该参数为空时使用默认键值码映射表 276 | 277 | \- 'alias' 对应键值的别名 278 | 279 | \- codes 对应键值数组 280 | 281 | - handler 对应按键值绑定的事件处理函数 参数分别是event(事件), node(当前焦点dom节点索引), index (当前焦点dom节点的t-index值), prevNode(上一个焦点dom节点索引) 282 | 283 | - up 方向上键 284 | - down 方向下键 285 | - left 方向左键 286 | - right 方向右键 287 | - enter 确定键 288 | - space 空格键 289 | - home 主页键 290 | - menu 菜单键 291 | - return 返回键 292 | - addVolume 增加音量键 293 | - subVolume 减少音量键 294 | 295 | ```js 296 | focus: { 297 | keysMap: { 298 | 'up': { 299 | codes: [38, 104], 300 | handler: function (event, node, index, prevNode) { 301 | 302 | } 303 | }, 304 | 'down': { 305 | codes: [40, 98], 306 | handler 307 | }, 308 | 'left': { 309 | codes: [37, 100], 310 | handler 311 | }, 312 | 'right': { 313 | codes: [39, 102], 314 | handler 315 | }, 316 | 'enter': { 317 | codes: [13, 32], 318 | handler 319 | } 320 | }, 321 | } 322 | ``` 323 | 324 | - **keysMapMergeCoverage (可选)** 325 | 326 | 键值映射表合并策略 true 为覆盖, false 为合并 327 | 328 | ```js 329 | focus: { 330 | keysMapMergeCoverage: false, 331 | } 332 | ``` 333 | 334 | #### hooks 335 | 336 | 生命周期钩子函数集合 337 | 338 | - beforeCreate 339 | 340 | 在实例化之前调用, 此时不可访问 data 341 | 342 | - created 343 | 344 | 在实例化后调用,此时 data 已经设置响应式, 并可访问 345 | 346 | - beforeMount 347 | 348 | 在实例被挂在到真实 dom 前调用 349 | 350 | - mounted 351 | 352 | 在实例被挂在到 dom 上时调用 353 | 354 | - beforeUpdate 355 | 356 | 响应式 data 变动从而导致视图更新前调用 357 | 358 | - updated 359 | 360 | 响应式 data 变动从而导致视图更新后调用 361 | 362 | - beforeDestory 363 | 364 | 在实例被销毁前调用 365 | 366 | ```js 367 | hooks: { 368 | beforeCreate: function () { 369 | // this 指向tv实例 370 | }, 371 | mounted: function () { 372 | 373 | }, 374 | ... 375 | } 376 | ``` 377 | 378 | 379 | 380 | ### 指令 381 | 382 | TVVM 内置指令系统, 包含了一些常用的指令, 用于处理简单的模版逻辑.TVVM 内置指令通常以 `t-`开头作为标识 383 | 384 | ```html 385 | 386 | 387 | 388 |
389 |
{{data.dialog.title}}
390 | 393 |
394 | 395 |
396 | 397 |
398 | 399 | 400 | ``` 401 | 402 | #### t-index 403 | 404 | 用于指定焦点区块元素的二维空间索引,以便用户点击遥控器方向按键时移动焦点,`t-index="x-y"`, 例如`t-index="0-0"`代表第一排第一列的元素 405 | 406 | ```html 407 |
408 |
0-0
409 |
0-1
410 |
0-2
411 | 412 |
1-0, 2-0
413 |
414 | 415 |
1-1, 1-2, 1-3
416 |
417 |
2-1
418 |
2-2
419 |
2-3
420 |
421 |
422 |
423 | ``` 424 | 425 | ![效果如图](./website/imgs/index.bmp) 426 | 427 | #### t-if 428 | 429 | 用于显示/隐藏 dom 节点, 该指令接收一个在 data 参数上存在的布尔值 430 | 431 | ```html 432 |
433 | ``` 434 | 435 | #### t-for 436 | 437 | 用于循环指定的 dom 节点, 该指令接收一个表达式,如下 item 代表数组的每一项, `data.array`是 data 上定义的一个数组 438 | 439 | ```js 440 | data: function () { 441 | return { 442 | array: [ 443 | {label: 'first', value: 0}, 444 | {label: 'second', value: 1}, 445 | {label: 'third', value: 2} 446 | ] 447 | } 448 | } 449 | ``` 450 | 451 | ```html 452 | 455 | 456 | 461 | ``` 462 | 463 | #### {{value}} 464 | 465 | 数据插值, 双花括号内接收一个 data 的属性.用于页面内数据插值 466 | 467 | ```js 468 | data: function () { 469 | return { name: 'float' } 470 | } 471 | ``` 472 | 473 | ```html 474 | {{data.name}} 475 | 476 | float 477 | ``` 478 | 479 | #### t-class 480 | 481 | 样式表绑定, t-class 接收一个 data 上已经定义的样式名数组或对象 482 | 483 | ```js 484 | data: function () { 485 | return { 486 | classList1: ['container', 'container-row'], 487 | classList2: { 488 | 'container': true, 489 | 'container-row': false, 490 | 'container-col': true 491 | } 492 | } 493 | } 494 | ``` 495 | 496 | ```html 497 |
498 | 499 |
500 | ``` 501 | 502 | #### t-bind 503 | 504 | 属性绑定,`t-bind:name="data.name"` 简写形式为 `:name="data.name"` 505 | 506 | ```js 507 | data: function () { 508 | return { 509 | id: 'billboard', 510 | height: '365', 511 | classname: 'spin' 512 | } 513 | } 514 | ``` 515 | 516 | ```html 517 |
518 | 519 |
520 | ``` 521 | 522 | #### t-value 523 | 524 | 用于 input 输入数据与 data 上数据的绑定,input 的 value 会实时同步至t-model 绑定的数据 525 | 526 | ```js 527 | data: function () { 528 | return { 529 | demoInputValue: 'demo' 530 | } 531 | } 532 | ``` 533 | 534 | ```html 535 | 536 | {{data.demoInputValue}} 537 | ``` 538 | 539 | 540 | 541 | #### event bind 542 | 543 | 事件绑定 544 | 545 | ```js 546 | methods: { 547 | clickHandler: function () { 548 | // do something 549 | }, 550 | clickHandler2: function (param1, param2) { 551 | 552 | } 553 | } 554 | ``` 555 | 556 | ```html 557 |
558 |
559 | ``` 560 | 561 | 562 | 563 | 564 | 565 | 566 | ## 协议 567 | 568 | TVVM 采用MIT协议,详情查看[LICENSE](./LICENSE) 569 | 570 | 571 | 572 | ## 链接 573 | 574 | * [官方文档](https://github.com/zexiplus/TVVM) 575 | 576 | -------------------------------------------------------------------------------- /config/rollup.config.js: -------------------------------------------------------------------------------- 1 | const banner = require('bannerjs') // output Author, copyright ... 2 | const babel = require('rollup-plugin-babel') // transform ES6/7 to ES5 3 | const nodeResolve = require('rollup-plugin-node-resolve') 4 | const commonjs = require('rollup-plugin-commonjs') // handle commonjs module 5 | 6 | const buildConfig = { 7 | rollupInputOptions: { 8 | input: 'src/main.js', 9 | plugins: [ 10 | nodeResolve(), 11 | commonjs(), 12 | babel({ 13 | exclude: 'node_modules/**' 14 | }) 15 | ] 16 | }, 17 | rollupOutputOptions: [ 18 | { 19 | file: 'dist/tvvm.js', 20 | minFile: 'dist/tvvm.min.js', 21 | format: 'umd', 22 | name: 'TVVM', // global value name 23 | banner: banner.multibanner() 24 | }, 25 | { 26 | file: 'dist/tvvm.common.js', 27 | minFile: 'dist/tvvm.common.min.js', 28 | format: 'cjs', 29 | name: 'TVVM', 30 | banner: banner.multibanner() 31 | }, 32 | { 33 | file: 'dist/tvvm.esm.js', 34 | format: 'es', 35 | name: 'TVVM', 36 | banner: banner.multibanner() 37 | } 38 | ] 39 | } 40 | 41 | const watchConfig = { 42 | ...buildConfig.rollupInputOptions, 43 | output: buildConfig.rollupOutputOptions, 44 | } 45 | 46 | module.exports = { buildConfig, watchConfig } -------------------------------------------------------------------------------- /config/uglify.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | compress: { 3 | pure_getters: true, 4 | unsafe: true, 5 | unsafe_comps: true, 6 | warnings: false, 7 | }, 8 | output: { 9 | ascii_only: true, 10 | } 11 | } -------------------------------------------------------------------------------- /dist/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zexiplus/TVVM/9852ac9edf2c0a2a688e61c0a13b44b64de8aa52/dist/.gitkeep -------------------------------------------------------------------------------- /dist/tvvm.common.min.js: -------------------------------------------------------------------------------- 1 | /*! tvvm v1.0.2 | MIT (c) 2018 float | https://github.com/zexiplus/TVM#readme */ 2 | "use strict";var _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},classCallCheck=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},createClass=function(){function i(e,t){for(var n=0;n=++e}},{key:"isLeftBoundary",value:function(){var e=this.focusState.currentRowIndex,t=this.focusState.currentColIndex;if(t===this.indexMap[e][0])return!0;for(var n=[e,--t].join("-");this.focusElementMap[n]&&this.focusElementMap[this.focusState.currentIndexString]===this.focusElementMap[n];)n=[e,--t].join("-");return this.indexMap[e][0]>=++t}},{key:"isRightBoundary",value:function(){var e=this.focusState.currentRowIndex,t=this.focusState.currentColIndex;if(t===this.indexMap[e][this.indexMap[e].length-1])return!0;for(var n=[e,++t].join("-");this.focusElementMap[n]&&this.focusElementMap[this.focusState.currentIndexString]===this.focusElementMap[n];)n=[e,++t].join("-");return--t>=this.indexMap[e][this.indexMap[e].length-1]}},{key:"isBottomBoundary",value:function(){var e=this.focusState.currentRowIndex,t=this.focusState.currentColIndex;if(e===this.indexMap.length-1)return!0;for(var n=[++e,t].join("-");this.focusElementMap[n]&&this.focusElementMap[this.focusState.currentIndexString]===this.focusElementMap[n];)n=[++e,t].join("-");return--e>=this.indexMap.length-1}},{key:"moveUp",value:function(e,t,n){if(this.keysMap.up.handler&&this.keysMap.up.handler(e,t,n),this.isTopBoundary()){if(this.focusOptions.circle.vertical)this.removeFocus(n),this.setFocus([this.indexMap.length-1,this.focusState.currentColIndex].join("-"))}else{this.removeFocus(n);for(var i=this.focusState.currentRowIndex-1,s=this.focusState.currentColIndex,o=[i,s].join("-");this.focusElementMap[this.focusState.currentIndexString]===this.focusElementMap[o];)o=[--i,s].join("-");this.setFocus(o=[i,s].join("-"))}}},{key:"moveDown",value:function(e,t,n){if(this.keysMap.down.handler&&this.keysMap.down.handler(e,t,n),this.isBottomBoundary()){if(this.focusOptions.circle.vertical){this.removeFocus(n);this.setFocus([0,this.focusState.currentColIndex].join("-"))}}else{this.removeFocus(n);for(var i=this.focusState.currentRowIndex+1,s=this.focusState.currentColIndex,o=[i,s].join("-");this.focusElementMap[this.focusState.currentIndexString]===this.focusElementMap[o];)o=[++i,s].join("-");this.setFocus(o=[i,s].join("-"))}}},{key:"moveLeft",value:function(e,t,n){if(this.keysMap.left.handler&&this.keysMap.left.handler(e,t,n),this.isLeftBoundary()){if(this.focusOptions.circle.horizontal){this.removeFocus(n);var i=this.focusState.currentRowIndex;this.setFocus([i,this.indexMap[i][this.indexMap[i].length-1]].join("-"))}}else{this.removeFocus(n);for(var s=this.focusState.currentRowIndex,o=this.focusState.currentColIndex-1,a=[s,o].join("-");this.focusElementMap[this.focusState.currentIndexString]===this.focusElementMap[a];)a=[s,--o].join("-");this.setFocus(a=[s,o].join("-"))}}},{key:"moveRight",value:function(e,t,n){if(this.keysMap.right.handler&&this.keysMap.right.handler(e,t,n),this.isRightBoundary()){if(this.focusOptions.circle.horizontal){this.removeFocus(n);var i=this.focusState.currentRowIndex;this.setFocus([i,this.indexMap[i][0]].join("-"))}}else{this.removeFocus(n);for(var s=this.focusState.currentRowIndex,o=this.focusState.currentColIndex+1,a=[s,o].join("-");this.focusElementMap[this.focusState.currentIndexString]===this.focusElementMap[a];)a=[s,++o].join("-");this.setFocus(a=[s,o].join("-"))}}},{key:"move",value:function(e,t){Object.assign({},this.keysMap,{up:{handler:this.moveUp},down:{handler:this.moveDown},left:{handler:this.moveLeft},right:{handler:this.moveRight}})[e].handler.call(this,t,this.focusState.currentFocusElement,this.focusState.currentIndexString)}}]),n}(),Lifecycle=function(){function n(e,t){classCallCheck(this,n),this.hooks={},this.init(e,t),t.lifecycle=this,t.callHook=this.callHook.bind(this)}return createClass(n,[{key:"init",value:function(e,n){var i={beforeCreate:e.beforeCreate,created:e.created,beforeMount:e.beforeMount,mounted:e.mounted,beforeUpdate:e.beforeUpdate,updated:e.updated,beforeDestory:e.beforeDestory,destoried:e.destoried};Object.keys(i).forEach(function(e,t){void 0===i[e]&&(i[e]=emptyFn),i[e]=i[e]instanceof Function?i[e].bind(n):(console.warn("lifecycle hooks must be a function"),emptyFn)}),this.hooks=i}},{key:"callHook",value:function(e){this.hooks[e]()}}]),n}();function emptyFn(){}var TVVM=function(){function t(e){classCallCheck(this,t),new Focuser(this,e),new Lifecycle(e.hooks||{},this),this.callHook("beforeCreate"),this.$data="function"==typeof e.data?e.data():e.data,this.methods=e.methods,this.proxy(this.$data,this),this.proxy(e.methods,this),e.el&&(new Observer(this.$data,this),this.callHook("created"),this.callHook("beforeMount"),new Compiler(e.el,this),this.focuser.generateIndexMap(),this.callHook("mounted"))}return createClass(t,[{key:"proxy",value:function(n,e){Object.keys(n).forEach(function(t){Object.defineProperty(e,t,{enumerable:!0,configurable:!0,get:function(){return n[t]},set:function(e){n[t]=e}})})}}]),t}();module.exports=TVVM; -------------------------------------------------------------------------------- /dist/tvvm.esm.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * tvvm v1.0.2 3 | * A simple micro-library for agile building TV web app with no dependency 4 | * 5 | * Copyright (c) 2018 float 6 | * https://github.com/zexiplus/TVM#readme 7 | * 8 | * Licensed under the MIT license. 9 | */ 10 | 11 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { 12 | return typeof obj; 13 | } : function (obj) { 14 | return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; 15 | }; 16 | 17 | var classCallCheck = function (instance, Constructor) { 18 | if (!(instance instanceof Constructor)) { 19 | throw new TypeError("Cannot call a class as a function"); 20 | } 21 | }; 22 | 23 | var createClass = function () { 24 | function defineProperties(target, props) { 25 | for (var i = 0; i < props.length; i++) { 26 | var descriptor = props[i]; 27 | descriptor.enumerable = descriptor.enumerable || false; 28 | descriptor.configurable = true; 29 | if ("value" in descriptor) descriptor.writable = true; 30 | Object.defineProperty(target, descriptor.key, descriptor); 31 | } 32 | } 33 | 34 | return function (Constructor, protoProps, staticProps) { 35 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 36 | if (staticProps) defineProperties(Constructor, staticProps); 37 | return Constructor; 38 | }; 39 | }(); 40 | 41 | var toConsumableArray = function (arr) { 42 | if (Array.isArray(arr)) { 43 | for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; 44 | 45 | return arr2; 46 | } else { 47 | return Array.from(arr); 48 | } 49 | }; 50 | 51 | var Dep = function () { 52 | function Dep() { 53 | classCallCheck(this, Dep); 54 | 55 | this.subs = []; 56 | } 57 | 58 | createClass(Dep, [{ 59 | key: "addSubs", 60 | value: function addSubs(watcher) { 61 | this.subs.push(watcher); // add subscribers 62 | } 63 | }, { 64 | key: "notify", 65 | value: function notify() { 66 | this.subs.forEach(function (watcher) { 67 | return watcher.update(); 68 | }); 69 | } 70 | }]); 71 | return Dep; 72 | }(); 73 | 74 | var Observer = function () { 75 | function Observer(data, vm) { 76 | classCallCheck(this, Observer); 77 | 78 | this.observer(data); 79 | this.vm = vm; 80 | } 81 | 82 | createClass(Observer, [{ 83 | key: "observer", 84 | value: function observer(data) { 85 | var _this2 = this; 86 | 87 | // 递归的终止条件: 当观察数据不存在或不再是对象是停止 88 | if (!data || (typeof data === "undefined" ? "undefined" : _typeof(data)) !== "object") { 89 | return; 90 | } 91 | Object.keys(data).forEach(function (key) { 92 | // 递归调用自身, 遍历对象上的所有属性都为响应式的 93 | _this2.observer(data[key]); 94 | _this2.setReactive(data, key); 95 | }); 96 | } 97 | // 响应式 对数据的修改会触发相应的功能 98 | 99 | }, { 100 | key: "setReactive", 101 | value: function setReactive(obj, key) { 102 | var value = obj[key]; 103 | var _this = this; 104 | var dep = new Dep(); 105 | Object.defineProperty(obj, key, { 106 | enumerable: true, 107 | configurable: true, 108 | get: function get$$1() { 109 | // 依赖收集 进行订阅, 在编译阶段, compiler会给template中的每个指令增加一个watcher, Dep.target 为一个watcher 110 | Dep.target && dep.addSubs(Dep.target); 111 | 112 | return value; 113 | }, 114 | set: function set$$1(newValue) { 115 | if (newValue !== obj[key]) { 116 | // 对新值继续劫持 117 | _this.observer(newValue); 118 | // 用新值替换旧值 119 | _this.vm.callHook("beforeUpdate"); 120 | value = newValue; 121 | // 发布通知 122 | dep.notify(); 123 | _this.vm.callHook("updated"); 124 | // update 125 | } 126 | } 127 | }); 128 | } 129 | }]); 130 | return Observer; 131 | }(); 132 | 133 | var Watcher = function () { 134 | /** 135 | * 136 | * @param {*} vm 137 | * @param {*} watchTarget | watch target in data e.g input.value 138 | * @param {*} expr | expresion in {{data.input.value + 1}} e.g 'data.input.value + 1' 139 | * @param {*} bindAttrName | t-bind node attribute name e.g :id="data.tid" --> id 140 | * @param {*} cb | update callback 141 | */ 142 | function Watcher(vm, watchTarget, expr, bindAttrName, cb) { 143 | classCallCheck(this, Watcher); 144 | 145 | this.vm = vm; 146 | this.watchTarget = watchTarget; 147 | this.expr = expr; 148 | this.bindAttrName = bindAttrName; 149 | this.cb = cb; 150 | this.value = this.getValAndSetTarget(); // save value when first compiled 151 | } 152 | 153 | createClass(Watcher, [{ 154 | key: "getValAndSetTarget", 155 | value: function getValAndSetTarget() { 156 | Dep.target = this; 157 | var value = this.getValue(this.watchTarget, this.vm.$data); 158 | Dep.target = null; 159 | return value; 160 | } 161 | }, { 162 | key: "getValue", 163 | value: function getValue(tag, base) { 164 | var arr = tag.split("."); 165 | return arr.reduce(function (prev, next) { 166 | return prev[next]; 167 | }, base); 168 | } 169 | }, { 170 | key: "update", 171 | value: function update() { 172 | var oldVal = this.value; 173 | var newVal = this.getValue(this.watchTarget, this.vm.$data); 174 | this.cb && this.cb(newVal, this.bindAttrName, this.expr); 175 | } 176 | }]); 177 | return Watcher; 178 | }(); 179 | 180 | // 编译功能函数 181 | 182 | var compileUtil = { 183 | updateText: function updateText(text, node, vm, expr) { 184 | // console.log('compileUtil.updateText text is', text) 185 | node && (node.textContent = text); 186 | }, 187 | 188 | // 在绑定有t-model节点的input上绑定事件, expr为t-model的表达式例如 'message.name' 189 | "t-value": function tValue(value, node, vm, expr) { 190 | var _this = this; 191 | 192 | node && (node.value = value); 193 | node.addEventListener("input", function (e) { 194 | _this.setVal(vm.$data, expr, e.target.value); 195 | }); 196 | }, 197 | 198 | "t-bind": function tBind(value, node, vm, expr, attrname) { 199 | node && node.setAttribute(attrname, value); 200 | }, 201 | "t-if": function tIf(value, node, vm, expr) { 202 | // const originalDisplay = node.style.display || 'block' 203 | node && (node.style.display = value ? "block" : "none"); 204 | }, 205 | "t-show": function tShow(value, node, vm, expr) { 206 | var originalVisible = window.getComputedStyle(node); 207 | node && (node.style.visibility = value ? originalVisible : "hidden"); 208 | }, 209 | "t-class": function tClass(value, node, vm, expr) { 210 | console.log('trigger t class'); 211 | if (Array.isArray(value)) { 212 | value.forEach(function (item) { 213 | node.classList.add(item); 214 | }); 215 | } else if ({}.toString.call(value) === "[object Object]") { 216 | // node.classList = []; 217 | Object.keys(value).forEach(function (classname) { 218 | if (value[classname]) { 219 | node.classList.add(classname); 220 | } else { 221 | node.classList.remove(classname); 222 | } 223 | }); 224 | } else { 225 | console.warn("t-class must receive an array or object"); 226 | } 227 | }, 228 | "t-for": function tFor(value, node, vm, expr, attrname, originalExpr) { 229 | // 截取 in 后的数组表达式 230 | var startIndex = originalExpr.indexOf("in") + 3; 231 | var arrNamePrefix = originalExpr.slice(startIndex); 232 | var arrName = arrNamePrefix.split('.').slice(1).join('.'); 233 | var itemName = originalExpr.slice(0, startIndex - 4); 234 | var arr = this.getVal(vm.$data, arrName); 235 | var reg = /\{\{([^}]+)\}\}/g; 236 | if (!Array.isArray(arr)) { 237 | return console.warn("t-for value must be an array"); 238 | } 239 | var parentElement = node.parentElement; 240 | parentElement.removeChild(node); 241 | var baseNode = node.cloneNode(true); 242 | baseNode.setAttribute("t-scope", arrNamePrefix); 243 | baseNode.setAttribute("t-itemname", itemName); 244 | baseNode.removeAttribute("t-for"); 245 | baseNode.setAttribute("t-index", 0); 246 | baseNode.setAttribute("is-t-for", "true"); 247 | arr.forEach(function (item, index) { 248 | var cloneNode = baseNode.cloneNode(true); 249 | cloneNode.setAttribute("t-index", index); 250 | if (cloneNode.textContent) { 251 | var match = cloneNode.textContent.match(/\{\{([^}]+)\}\}/)[1]; 252 | var execFn = new Function("item", "return " + match); 253 | var result = execFn(item); 254 | cloneNode.textContent = cloneNode.textContent.replace(reg, result); 255 | } 256 | parentElement.appendChild(cloneNode); 257 | }); 258 | }, 259 | 260 | // 解析vm.data上的t-model绑定的值 261 | setVal: function setVal(obj, expr, value) { 262 | var arr = expr.split("."); 263 | arr.reduce(function (prev, next) { 264 | if (arr.indexOf(next) == arr.length - 1) { 265 | prev[next] = value; 266 | } else { 267 | return prev[next]; 268 | } 269 | }, obj); 270 | }, 271 | 272 | // 解析vm.$data 上的 例如 'member.id'属性 273 | getVal: function getVal(obj, expr) { 274 | var arr = expr.split("."); 275 | return arr.reduce(function (prev, next) { 276 | return prev[next]; 277 | }, obj); 278 | } 279 | }; 280 | 281 | var privateDirectives = ["is-t-for", "t-index", "t-scope", "t-itemname"]; 282 | 283 | var Compiler = function () { 284 | function Compiler(el, vm) { 285 | classCallCheck(this, Compiler); 286 | 287 | // 把dom节点挂载在Complier实例上 288 | this.el = this.getDOM(el); 289 | // 把mvvm实例挂载在complier实例上 290 | this.vm = vm; 291 | // debugger 292 | if (this.el) { 293 | // 如果存在再编译成文档片段 294 | // 编译解析出相应的指令 如 t-text, t-model, {{}} 295 | // 保存原有dom节点到fragment文档片段, 并做替换 296 | 297 | // 转化为文档片段并存到内存中去 298 | var fragment = this.toFragment(this.el); 299 | 300 | // 编译节点 301 | this.compile(fragment); 302 | 303 | // 把编译后的文档片段重新添加到document中 304 | this.el.appendChild(fragment); 305 | } else { 306 | // 没有找到el根节点给出警告 307 | console.error("can not find element named " + el); 308 | } 309 | this.vm.$el = this.el; 310 | } 311 | 312 | // 编译节点,如果子节点是node节点, 递归调用自身和compileNode, 如果不是 则调用 compileText 313 | 314 | 315 | createClass(Compiler, [{ 316 | key: "compile", 317 | value: function compile(parentNode) { 318 | var _this = this; 319 | 320 | var childNodes = parentNode.childNodes; 321 | childNodes.forEach(function (node, index) { 322 | // 不编译code代码节点 323 | if (node.tagName === "CODE") return; 324 | if (_this.isElement(node)) { 325 | _this.compile(node); 326 | _this.compileNode(node); 327 | } else if (_this.isText(node)) { 328 | _this.compileText(node); 329 | } 330 | }); 331 | } 332 | 333 | // 编译文本节点, 待优化 334 | 335 | }, { 336 | key: "compileText", 337 | value: function compileText(node) { 338 | var _this2 = this; 339 | 340 | // 测试文本节点含有 {{val}} 的 regexp 341 | var reg = /\{\{([^}]+)\}\}/; 342 | // 拿到文本节点的文本值 343 | var text = node.textContent; 344 | if (reg.test(text)) { 345 | // 去掉{{}} 保留 value 346 | if (node.parentElement.getAttribute("t-for") || node.parentElement.getAttribute("is-t-for")) {} else { 347 | // 捕获{{expr}} 双花括号中的表达式 348 | var expr = text.match(reg)[1]; 349 | // 捕获data的属性表达式 350 | var dataAttrReg = /data(\.[a-zA-Z_]+[a-zA-Z_\d]*)+(\(\))*/g; 351 | var watcherList = expr.match(dataAttrReg); 352 | var methodReg = /\.([a-zA-Z_]+[a-zA-Z_\d])+(\(\))/; 353 | 354 | // 例如取出{{message}} 中的 message, 交给compileUtil.updateText 方法去查找vm.data的值并替换到节点 355 | // let textValue = this.getValue(attrName, this.vm.$data); 356 | var execFn = new Function("data", "return " + expr); 357 | var data = this.vm.$data; 358 | var value = execFn(data); 359 | compileUtil.updateText(value, node, this.vm); 360 | 361 | // 给每个attribute上设置watcher 362 | watcherList = watcherList.map(function (item) { 363 | var attr = item.replace(methodReg, ""); 364 | attr = attr.split(".").slice(1).join("."); 365 | new Watcher(_this2.vm, attr, expr, null, function (value) { 366 | var expr = this.expr; 367 | var execFn = new Function("data", "return " + expr); 368 | var data = this.vm.$data; 369 | var val = execFn(data); 370 | compileUtil.updateText(val, node, this.vm); 371 | }); 372 | return attr; 373 | }); 374 | } 375 | } 376 | } 377 | 378 | // 传入表达式, 获得属性值 379 | 380 | }, { 381 | key: "getValue", 382 | value: function getValue(expr, base) { 383 | // 传入 expr 形如 'group.member.name', 找到$base上对应的属性值并返回 384 | var arr = expr && expr.split("."); 385 | var ret = arr.reduce(function (prev, next) { 386 | return prev[next]; 387 | }, base); 388 | return ret; 389 | } 390 | 391 | // 编译node节点 分析t指令, 待优化 392 | 393 | }, { 394 | key: "compileNode", 395 | value: function compileNode(node) { 396 | var _this3 = this; 397 | 398 | var attrs = node.getAttributeNames(); 399 | // 把t-指令(不包括t-index)属性存到一个数组中 400 | var directiveAttrs = attrs.filter(function (attrname) { 401 | return _this3.isDirective(attrname) && !_this3.isTFocus(attrname); 402 | }); 403 | 404 | directiveAttrs.forEach(function (item) { 405 | if (item === 't-itemname' || item === 'is-t-for') return; 406 | var originalExpr = node.getAttribute(item); // 属性值 407 | var expr = originalExpr.split(".").slice(1).join("."); 408 | 409 | // t-bind logic 410 | var bindAttrName = null; 411 | if (_this3.isTBind(item)) { 412 | var startIndex = item.indexOf(':') + 1; 413 | bindAttrName = item.slice(startIndex); 414 | item = 't-bind'; 415 | } 416 | new Watcher(_this3.vm, expr, originalExpr, bindAttrName, function (value, bindAttrName, originalExpr) { 417 | compileUtil[item](value, node, _this3.vm, expr, bindAttrName, originalExpr); 418 | }); 419 | // debugger 420 | var value = _this3.getValue(expr, _this3.vm.$data); 421 | if (compileUtil[item]) { 422 | compileUtil[item](value, node, _this3.vm, expr, bindAttrName, originalExpr); 423 | } else if (!_this3.isPrivateDirective(item) && !_this3.isEventBinding(item)) { 424 | console.warn("can't find directive " + item); 425 | } 426 | }); 427 | 428 | // 焦点记录逻辑 429 | if (attrs.includes("t-index")) { 430 | var focusIndex = node.getAttribute("t-index"); 431 | node.setAttribute("tabindex", this.vm.focuser.tid); 432 | this.vm.focuser.addFocusMap(focusIndex, node); 433 | } 434 | 435 | // @event 事件绑定逻辑 436 | var eventBindAttrs = attrs.filter(this.isEventBinding); 437 | eventBindAttrs.forEach(function (item) { 438 | var expr = node.getAttribute(item); 439 | var eventName = item.slice(1); 440 | var reg = /\(([^)]+)\)/; 441 | var hasParams = reg.test(expr); 442 | var fnName = expr.replace(reg, ""); 443 | var fn = _this3.getValue(fnName, _this3.vm); 444 | 445 | if (node.getAttribute("is-t-for")) { 446 | // 是 t-for 循环生成的列表, 则事件绑定在父元素上 447 | var parentElement = node.parentElement; 448 | parentElement.addEventListener(eventName, function (event) { 449 | if (event.target.getAttribute("is-t-for")) { 450 | if (hasParams) { 451 | var params = expr.match(reg)[1].split(",").map(function (item) { 452 | return _this3.getValue(item.trim(), _this3.vm.$data); 453 | }); 454 | // 取到 事件回调函数 的参数值 455 | 456 | var scope = event.target.getAttribute("t-scope"); 457 | var arrName = scope.split('.').slice(1).join('.'); 458 | var param = _this3.getValue(arrName, _this3.vm.$data)[event.target.getAttribute("t-index")]; 459 | fn.call(_this3.vm, param); 460 | } else { 461 | fn.call(_this3.vm); 462 | } 463 | } 464 | }); 465 | } else { 466 | // debugger 467 | // 非 t-for循环生成的元素 468 | if (hasParams) { 469 | // fn含有参数 470 | var params = expr.match(reg)[1].split(",").map(function (item) { 471 | return _this3.getValue(item.trim(), _this3.vm.$data); 472 | }); 473 | node.addEventListener(eventName, fn.bind.apply(fn, [_this3.vm].concat(toConsumableArray(params)))); 474 | } else { 475 | // fn不含参数 476 | node.addEventListener(eventName, fn.bind(_this3.vm)); 477 | } 478 | } 479 | }); 480 | } 481 | }, { 482 | key: "isPrivateDirective", 483 | value: function isPrivateDirective(text) { 484 | return privateDirectives.includes(text); 485 | } 486 | 487 | // 判断是否是事件绑定 488 | 489 | }, { 490 | key: "isEventBinding", 491 | value: function isEventBinding(text) { 492 | var reg = /^@/; 493 | return reg.test(text); 494 | } 495 | 496 | // 判断节点属性是否是t指令 497 | 498 | }, { 499 | key: "isDirective", 500 | value: function isDirective(attrname) { 501 | return attrname.includes("t-") || attrname.indexOf(":") === 0; 502 | } 503 | 504 | // 判断是否是t-index 505 | 506 | }, { 507 | key: "isTFocus", 508 | value: function isTFocus(attrname) { 509 | return attrname === "t-index"; 510 | } 511 | }, { 512 | key: "isTFor", 513 | value: function isTFor(attrname) { 514 | return attrname === "t-for"; 515 | } 516 | }, { 517 | key: "isTBind", 518 | value: function isTBind(attrname) { 519 | return (/(^t-bind:|^:)/.test(attrname) 520 | ); 521 | } 522 | 523 | // 根据传入的值, 如果是dom节点直接返回, 如果是选择器, 则返回相应的dom节点 524 | 525 | }, { 526 | key: "getDOM", 527 | value: function getDOM(el) { 528 | if (this.isElement(el)) { 529 | return el; 530 | } else { 531 | return document.querySelector(el) || null; 532 | } 533 | } 534 | 535 | // 判断dom类型, 1 为元素, 2 是属性, 3是文本, 9是文档, 11是文档片段 536 | 537 | }, { 538 | key: "isElement", 539 | value: function isElement(el) { 540 | return el.nodeType === 1; 541 | } 542 | }, { 543 | key: "isText", 544 | value: function isText(el) { 545 | return el.nodeType === 3; 546 | } 547 | 548 | // 把el dom节点转换为fragment保存在内存中并返回 549 | 550 | }, { 551 | key: "toFragment", 552 | value: function toFragment(el) { 553 | var fragment = document.createDocumentFragment(); 554 | var firstChild = void 0; 555 | // 循环把el的首个子元素推入fragment中 556 | while (firstChild = el.firstChild) { 557 | // 把原始 el dom节点的所有子元素增加到文档片段中并移除原 el dom节点的所有子元素 558 | fragment.appendChild(firstChild); 559 | } 560 | return fragment; 561 | } 562 | }]); 563 | return Compiler; 564 | }(); 565 | 566 | var blankFn = function blankFn(event, node, index, prevNode) {}; 567 | var defaultFocusOptions = { 568 | circle: { 569 | horizontal: false, 570 | vertical: false 571 | }, 572 | keysMap: { 573 | up: { 574 | codes: [38, 104], 575 | handler: blankFn 576 | }, 577 | down: { 578 | codes: [40, 98], 579 | handler: blankFn 580 | }, 581 | left: { 582 | codes: [37, 100], 583 | handler: blankFn 584 | }, 585 | right: { 586 | codes: [39, 102], 587 | handler: blankFn 588 | }, 589 | enter: { 590 | codes: [13, 32], 591 | handler: blankFn 592 | }, 593 | space: { 594 | codes: [32], 595 | handler: blankFn 596 | }, 597 | home: { 598 | codes: [36], 599 | handler: blankFn 600 | }, 601 | menu: { 602 | codes: [18], 603 | handler: blankFn 604 | }, 605 | return: { 606 | codes: [27], 607 | handler: blankFn 608 | }, 609 | addVolume: { 610 | codes: [175], 611 | handler: blankFn 612 | }, 613 | subVolume: { 614 | codes: [174], 615 | handler: blankFn 616 | }, 617 | shutdown: { 618 | codes: [71], 619 | handler: blankFn 620 | } 621 | }, 622 | keysMapMergeCoverage: false 623 | }; 624 | 625 | var Focuser = function () { 626 | function Focuser(vm, options) { 627 | classCallCheck(this, Focuser); 628 | 629 | this.tid = 0; 630 | this.init(vm, options); 631 | this.bindKeyEvent(); 632 | } 633 | 634 | createClass(Focuser, [{ 635 | key: "init", 636 | value: function init(vm, options) { 637 | var _this = this; 638 | 639 | // 存放indexString索引的node节点 640 | this.focusElementMap = {}; 641 | // 索引转化后的数组,例如[[0,1,2], [0,2]] 用于边界判断 642 | this.indexMap = []; 643 | // 存放原始focus相关参数 644 | this.focusOptions = Object.assign({}, defaultFocusOptions, options.focus); 645 | var currentRowIndex = void 0, 646 | currentColIndex = void 0; 647 | if (this.focusOptions.defaultFocusIndex) { 648 | var IndexArr = options.focus && options.focus.defaultFocusIndex.split("-"); 649 | currentRowIndex = Number(IndexArr[0]); 650 | currentColIndex = Number(IndexArr[1]); 651 | } 652 | // 存放当前状态信息 653 | this.focusState = { 654 | currentIndexString: options.focus && options.focus.defaultFocusIndex || "", 655 | currentRowIndex: currentRowIndex, 656 | currentColIndex: currentColIndex 657 | }; 658 | 659 | this.keysMap = this.focusOptions.keysMap; 660 | // 合并键盘绑定键值码 661 | if (options.focus && options.focus.keysMap) { 662 | if (this.focusOptions.keysMapMergeCoverage) { 663 | // options.focus.keysMap 覆盖默认keysMap 664 | this.keysMap = Object.assign({}, this.keysMap, options.focus.keysMap); 665 | } else { 666 | // options.focus.keysMap 合并 默认keysmap 667 | Object.keys(this.keysMap).forEach(function (key) { 668 | // debugger 669 | if (defaultFocusOptions.keysMap[key]) { 670 | _this.keysMap[key].codes = options.focus.keysMap[key].codes ? [].concat(toConsumableArray(new Set(defaultFocusOptions.keysMap[key].codes.concat(options.focus.keysMap[key].codes)))) : _this.keysMap[key].codes; 671 | } else { 672 | _this.keysMap[key].codes = options.focus.keysMap[key].codes; 673 | } 674 | }); 675 | } 676 | } 677 | 678 | vm.focuser = this; 679 | this.vm = vm; 680 | } 681 | 682 | // 传入键值码并执行相应的操作 683 | 684 | }, { 685 | key: "execCommand", 686 | value: function execCommand(event) { 687 | var _this2 = this; 688 | 689 | Object.keys(this.keysMap).forEach(function (key) { 690 | if (_this2.keysMap[key].codes.includes(event.keyCode)) { 691 | _this2.move(key, event); 692 | } 693 | }); 694 | } 695 | 696 | // 绑定键盘事件 697 | 698 | }, { 699 | key: "bindKeyEvent", 700 | value: function bindKeyEvent() { 701 | window.addEventListener("keydown", this.keyDownHandler.bind(this)); 702 | } 703 | }, { 704 | key: "keyDownHandler", 705 | value: function keyDownHandler(event) { 706 | this.execCommand(event); 707 | } 708 | 709 | // 把有t-focus指令的node节点储存起来 710 | 711 | }, { 712 | key: "addFocusMap", 713 | value: function addFocusMap(key, node) { 714 | var _this3 = this; 715 | 716 | this.tid++; 717 | var keys = key.split(/,\s*/); 718 | keys.forEach(function (item) { 719 | if (item in _this3.focusElementMap) { 720 | return console.warn("t-focus should be unique in one TVVM page but t-focus=" + item + " has already exist"); 721 | } 722 | _this3.focusElementMap[item] = node; 723 | }); 724 | } 725 | // 设置焦点dom 726 | 727 | }, { 728 | key: "setFocus", 729 | value: function setFocus(index) { 730 | if (index in this.focusElementMap) { 731 | var arr = index.split("-"); 732 | var currentRowIndex = Number(arr[0]); 733 | var currentColIndex = Number(arr[1]); 734 | var el = this.focusElementMap[index]; 735 | if (el.getAttribute("real-focus") === "true") { 736 | el.focus(); 737 | } else { 738 | var activeClass = this.focusOptions.activeClass; 739 | el.classList.add(activeClass); 740 | } 741 | this.focusState.currentIndexString = index; 742 | this.focusState.currentFocusElement = this.focusElementMap[index]; 743 | this.focusState.currentRowIndex = currentRowIndex; 744 | this.focusState.currentColIndex = currentColIndex; 745 | } else { 746 | // console.warn(`can't find t-focus ${index} node`) 747 | } 748 | } 749 | }, { 750 | key: "removeFocus", 751 | value: function removeFocus(index) { 752 | if (index in this.focusElementMap) { 753 | var el = this.focusElementMap[index]; 754 | if (el.getAttribute("real-focus") === "true") { 755 | el.blur(); 756 | } else { 757 | var activeClass = this.focusOptions.activeClass; 758 | el.classList.remove(activeClass); 759 | } 760 | } 761 | } 762 | }, { 763 | key: "generateIndexMap", 764 | value: function generateIndexMap() { 765 | var _this4 = this; 766 | 767 | // 0-0, 0-1, 768 | Object.keys(this.focusElementMap).forEach(function (key) { 769 | var keyArr = key.split("-"); 770 | var rowIndex = keyArr[0]; 771 | var colIndex = keyArr[1]; 772 | if (_this4.indexMap[rowIndex] === undefined) { 773 | _this4.indexMap[rowIndex] = [colIndex]; 774 | } else { 775 | _this4.indexMap[rowIndex].push(colIndex); 776 | } 777 | }); 778 | this.indexMap = this.indexMap.map(function (item) { 779 | return item.sort(function (a, b) { 780 | return a - b; 781 | }); 782 | }); 783 | 784 | if (this.focusOptions.defaultFocusIndex !== undefined) { 785 | this.setFocus(this.focusOptions.defaultFocusIndex); 786 | } else { 787 | if (this.indexMap.length !== 0) { 788 | this.setFocus([0, this.indexMap[0][0]].join("-")); 789 | } else { 790 | window.removeEventListener("keydown", this.keyDownHandler); 791 | } 792 | } 793 | } 794 | }, { 795 | key: "isBoundary", 796 | value: function isBoundary() {} 797 | 798 | // 焦点处于顶部边界判断 799 | 800 | }, { 801 | key: "isTopBoundary", 802 | value: function isTopBoundary() { 803 | var rowIndex = this.focusState.currentRowIndex; 804 | var colIndex = this.focusState.currentColIndex; 805 | if (rowIndex === 0) { 806 | return true; 807 | } 808 | rowIndex--; 809 | var indexString = [rowIndex, colIndex].join("-"); 810 | while (this.focusElementMap[indexString] && this.focusElementMap[this.focusState.currentIndexString] === this.focusElementMap[indexString]) { 811 | rowIndex--; 812 | indexString = [rowIndex, colIndex].join("-"); 813 | } 814 | rowIndex++; 815 | if (rowIndex <= 0) { 816 | return true; 817 | } else { 818 | return false; 819 | } 820 | } 821 | }, { 822 | key: "isLeftBoundary", 823 | value: function isLeftBoundary() { 824 | var rowIndex = this.focusState.currentRowIndex; 825 | var colIndex = this.focusState.currentColIndex; 826 | if (colIndex === this.indexMap[rowIndex][0]) { 827 | return true; 828 | } 829 | colIndex--; 830 | var indexString = [rowIndex, colIndex].join("-"); 831 | while (this.focusElementMap[indexString] && this.focusElementMap[this.focusState.currentIndexString] === this.focusElementMap[indexString]) { 832 | colIndex--; 833 | indexString = [rowIndex, colIndex].join("-"); 834 | } 835 | colIndex++; 836 | if (colIndex > this.indexMap[rowIndex][0]) { 837 | return false; 838 | } else { 839 | return true; 840 | } 841 | } 842 | }, { 843 | key: "isRightBoundary", 844 | value: function isRightBoundary() { 845 | var rowIndex = this.focusState.currentRowIndex; 846 | var colIndex = this.focusState.currentColIndex; 847 | if (colIndex === this.indexMap[rowIndex][this.indexMap[rowIndex].length - 1]) { 848 | return true; 849 | } 850 | colIndex++; 851 | var indexString = [rowIndex, colIndex].join("-"); 852 | while (this.focusElementMap[indexString] && this.focusElementMap[this.focusState.currentIndexString] === this.focusElementMap[indexString]) { 853 | colIndex++; 854 | indexString = [rowIndex, colIndex].join("-"); 855 | } 856 | colIndex--; 857 | if (colIndex < this.indexMap[rowIndex][this.indexMap[rowIndex].length - 1]) { 858 | return false; 859 | } else { 860 | return true; 861 | } 862 | } 863 | }, { 864 | key: "isBottomBoundary", 865 | value: function isBottomBoundary() { 866 | var rowIndex = this.focusState.currentRowIndex; 867 | var colIndex = this.focusState.currentColIndex; 868 | if (rowIndex === this.indexMap.length - 1) { 869 | return true; 870 | } 871 | rowIndex++; 872 | var indexString = [rowIndex, colIndex].join("-"); 873 | while (this.focusElementMap[indexString] && this.focusElementMap[this.focusState.currentIndexString] === this.focusElementMap[indexString]) { 874 | rowIndex++; 875 | indexString = [rowIndex, colIndex].join("-"); 876 | } 877 | rowIndex--; 878 | if (rowIndex >= this.indexMap.length - 1) { 879 | return true; 880 | } else { 881 | return false; 882 | } 883 | } 884 | }, { 885 | key: "moveUp", 886 | value: function moveUp(event, node, index) { 887 | this.keysMap["up"].handler && this.keysMap["up"].handler(event, node, index); 888 | if (this.isTopBoundary()) { 889 | if (this.focusOptions.circle.vertical) { 890 | this.removeFocus(index); 891 | var rowIndex = this.indexMap.length - 1; 892 | var colIndex = this.focusState.currentColIndex; 893 | var indexString = [rowIndex, colIndex].join("-"); 894 | this.setFocus(indexString); 895 | } 896 | } else { 897 | this.removeFocus(index); 898 | var _rowIndex = this.focusState.currentRowIndex - 1; 899 | var _colIndex = this.focusState.currentColIndex; 900 | var _indexString = [_rowIndex, _colIndex].join("-"); 901 | while (this.focusElementMap[this.focusState.currentIndexString] === this.focusElementMap[_indexString]) { 902 | _rowIndex--; 903 | _indexString = [_rowIndex, _colIndex].join("-"); 904 | } 905 | _indexString = [_rowIndex, _colIndex].join("-"); 906 | this.setFocus(_indexString); 907 | } 908 | } 909 | }, { 910 | key: "moveDown", 911 | value: function moveDown(event, node, index) { 912 | this.keysMap["down"].handler && this.keysMap["down"].handler(event, node, index); 913 | if (this.isBottomBoundary()) { 914 | if (this.focusOptions.circle.vertical) { 915 | this.removeFocus(index); 916 | var rowIndex = 0; 917 | var colIndex = this.focusState.currentColIndex; 918 | var indexString = [rowIndex, colIndex].join("-"); 919 | this.setFocus(indexString); 920 | } 921 | } else { 922 | this.removeFocus(index); 923 | var _rowIndex2 = this.focusState.currentRowIndex + 1; 924 | var _colIndex2 = this.focusState.currentColIndex; 925 | var _indexString2 = [_rowIndex2, _colIndex2].join("-"); 926 | while (this.focusElementMap[this.focusState.currentIndexString] === this.focusElementMap[_indexString2]) { 927 | _rowIndex2++; 928 | _indexString2 = [_rowIndex2, _colIndex2].join("-"); 929 | } 930 | _indexString2 = [_rowIndex2, _colIndex2].join("-"); 931 | this.setFocus(_indexString2); 932 | } 933 | } 934 | }, { 935 | key: "moveLeft", 936 | value: function moveLeft(event, node, index) { 937 | this.keysMap["left"].handler && this.keysMap["left"].handler(event, node, index); 938 | if (this.isLeftBoundary()) { 939 | if (this.focusOptions.circle.horizontal) { 940 | this.removeFocus(index); 941 | var rowIndex = this.focusState.currentRowIndex; 942 | var colIndex = this.indexMap[rowIndex][this.indexMap[rowIndex].length - 1]; 943 | var indexString = [rowIndex, colIndex].join("-"); 944 | this.setFocus(indexString); 945 | } 946 | } else { 947 | this.removeFocus(index); 948 | var _rowIndex3 = this.focusState.currentRowIndex; 949 | var _colIndex3 = this.focusState.currentColIndex - 1; 950 | var _indexString3 = [_rowIndex3, _colIndex3].join("-"); 951 | // 如果nextindex和previndex引用的是同一个element,则自减 952 | while (this.focusElementMap[this.focusState.currentIndexString] === this.focusElementMap[_indexString3]) { 953 | _colIndex3--; 954 | _indexString3 = [_rowIndex3, _colIndex3].join("-"); 955 | } 956 | _indexString3 = [_rowIndex3, _colIndex3].join("-"); 957 | this.setFocus(_indexString3); 958 | } 959 | } 960 | }, { 961 | key: "moveRight", 962 | value: function moveRight(event, node, index) { 963 | this.keysMap["right"].handler && this.keysMap["right"].handler(event, node, index); 964 | if (this.isRightBoundary()) { 965 | if (this.focusOptions.circle.horizontal) { 966 | this.removeFocus(index); 967 | var rowIndex = this.focusState.currentRowIndex; 968 | var colIndex = this.indexMap[rowIndex][0]; 969 | var indexString = [rowIndex, colIndex].join("-"); 970 | this.setFocus(indexString); 971 | } 972 | } else { 973 | this.removeFocus(index); 974 | var _rowIndex4 = this.focusState.currentRowIndex; 975 | var _colIndex4 = this.focusState.currentColIndex + 1; 976 | var _indexString4 = [_rowIndex4, _colIndex4].join("-"); 977 | while (this.focusElementMap[this.focusState.currentIndexString] === this.focusElementMap[_indexString4]) { 978 | _colIndex4++; 979 | _indexString4 = [_rowIndex4, _colIndex4].join("-"); 980 | } 981 | _indexString4 = [_rowIndex4, _colIndex4].join("-"); 982 | this.setFocus(_indexString4); 983 | } 984 | } 985 | 986 | // 键盘上下左右触发函数 参数 按键方向, 原焦点索引字符串,焦点可循环标志位 987 | 988 | }, { 989 | key: "move", 990 | value: function move(direction, event) { 991 | var directionMap = { 992 | up: { 993 | handler: this.moveUp 994 | }, 995 | down: { 996 | handler: this.moveDown 997 | }, 998 | left: { 999 | handler: this.moveLeft 1000 | }, 1001 | right: { 1002 | handler: this.moveRight 1003 | } 1004 | }; 1005 | var runner = Object.assign({}, this.keysMap, directionMap); 1006 | runner[direction].handler.call(this, event, this.focusState.currentFocusElement, this.focusState.currentIndexString); 1007 | } 1008 | }]); 1009 | return Focuser; 1010 | }(); 1011 | 1012 | var Lifecycle = function () { 1013 | function Lifecycle(options, vm) { 1014 | classCallCheck(this, Lifecycle); 1015 | 1016 | this.hooks = {}; 1017 | this.init(options, vm); 1018 | vm.lifecycle = this; 1019 | vm.callHook = this.callHook.bind(this); 1020 | } 1021 | 1022 | createClass(Lifecycle, [{ 1023 | key: "init", 1024 | value: function init(options, vm) { 1025 | var beforeCreate = options.beforeCreate, 1026 | created = options.created, 1027 | beforeMount = options.beforeMount, 1028 | mounted = options.mounted, 1029 | beforeUpdate = options.beforeUpdate, 1030 | updated = options.updated, 1031 | beforeDestory = options.beforeDestory, 1032 | destoried = options.destoried; 1033 | 1034 | var hooks = { 1035 | beforeCreate: beforeCreate, 1036 | created: created, 1037 | beforeMount: beforeMount, 1038 | mounted: mounted, 1039 | beforeUpdate: beforeUpdate, 1040 | updated: updated, 1041 | beforeDestory: beforeDestory, 1042 | destoried: destoried 1043 | }; 1044 | Object.keys(hooks).forEach(function (key, index) { 1045 | if (hooks[key] === undefined) { 1046 | hooks[key] = emptyFn; 1047 | } 1048 | if (hooks[key] instanceof Function) { 1049 | hooks[key] = hooks[key].bind(vm); 1050 | } else { 1051 | console.warn("lifecycle hooks must be a function"); 1052 | hooks[key] = emptyFn; 1053 | } 1054 | }); 1055 | this.hooks = hooks; 1056 | } 1057 | }, { 1058 | key: "callHook", 1059 | value: function callHook(fnName) { 1060 | // fnName in this.hooks && this.hooks[fnName]() 1061 | this.hooks[fnName](); 1062 | } 1063 | }]); 1064 | return Lifecycle; 1065 | }(); 1066 | 1067 | function emptyFn() { 1068 | return; 1069 | } 1070 | 1071 | var TVVM = function () { 1072 | function TVVM(options) { 1073 | classCallCheck(this, TVVM); 1074 | 1075 | // 初始化参数, 把el, data等进行赋值与绑定 1076 | // data如果是函数就取返回值, 如果不是则直接赋值 1077 | // 初始化焦点管理对象 1078 | new Focuser(this, options); 1079 | // 初始化生命周期对象 1080 | new Lifecycle(options.hooks || {}, this); 1081 | // beforeCreate 1082 | this.callHook("beforeCreate"); 1083 | 1084 | this.$data = typeof options.data === "function" ? options.data() : options.data; 1085 | this.methods = options.methods; 1086 | // 数据代理, 把data对象属性代理到vm实例上 1087 | this.proxy(this.$data, this); 1088 | this.proxy(options.methods, this); 1089 | 1090 | // 把$el真实的dom节点编译成vdom, 并解析相关指令 1091 | if (options.el) { 1092 | // 数据劫持, 1093 | new Observer(this.$data, this); 1094 | // created 1095 | this.callHook("created"); 1096 | // beforeMounte 1097 | this.callHook("beforeMount"); 1098 | new Compiler(options.el, this); 1099 | this.focuser.generateIndexMap(); 1100 | // mounted 此时可以访问 this.$el 1101 | this.callHook("mounted"); 1102 | } 1103 | } 1104 | // 数据代理, 访问/设置 this.a 相当于访问设置 this.data.a 1105 | 1106 | 1107 | createClass(TVVM, [{ 1108 | key: "proxy", 1109 | value: function proxy(data, proxyTarget) { 1110 | Object.keys(data).forEach(function (key) { 1111 | Object.defineProperty(proxyTarget, key, { 1112 | enumerable: true, 1113 | configurable: true, 1114 | get: function get$$1() { 1115 | return data[key]; 1116 | }, 1117 | set: function set$$1(newValue) { 1118 | // if (proxyTarget[key] !== undefined) { 1119 | // console.warn(`key ${key} has already in Target`); 1120 | // } 1121 | data[key] = newValue; 1122 | } 1123 | }); 1124 | }); 1125 | } 1126 | }]); 1127 | return TVVM; 1128 | }(); 1129 | 1130 | export default TVVM; 1131 | -------------------------------------------------------------------------------- /dist/tvvm.min.js: -------------------------------------------------------------------------------- 1 | /*! tvvm v1.0.2 | MIT (c) 2018 float | https://github.com/zexiplus/TVM#readme */ 2 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.TVVM=e()}(this,function(){"use strict";var t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},r=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},s=function(){function i(t,e){for(var n=0;n=++t}},{key:"isLeftBoundary",value:function(){var t=this.focusState.currentRowIndex,e=this.focusState.currentColIndex;if(e===this.indexMap[t][0])return!0;for(var n=[t,--e].join("-");this.focusElementMap[n]&&this.focusElementMap[this.focusState.currentIndexString]===this.focusElementMap[n];)n=[t,--e].join("-");return this.indexMap[t][0]>=++e}},{key:"isRightBoundary",value:function(){var t=this.focusState.currentRowIndex,e=this.focusState.currentColIndex;if(e===this.indexMap[t][this.indexMap[t].length-1])return!0;for(var n=[t,++e].join("-");this.focusElementMap[n]&&this.focusElementMap[this.focusState.currentIndexString]===this.focusElementMap[n];)n=[t,++e].join("-");return--e>=this.indexMap[t][this.indexMap[t].length-1]}},{key:"isBottomBoundary",value:function(){var t=this.focusState.currentRowIndex,e=this.focusState.currentColIndex;if(t===this.indexMap.length-1)return!0;for(var n=[++t,e].join("-");this.focusElementMap[n]&&this.focusElementMap[this.focusState.currentIndexString]===this.focusElementMap[n];)n=[++t,e].join("-");return--t>=this.indexMap.length-1}},{key:"moveUp",value:function(t,e,n){if(this.keysMap.up.handler&&this.keysMap.up.handler(t,e,n),this.isTopBoundary()){if(this.focusOptions.circle.vertical)this.removeFocus(n),this.setFocus([this.indexMap.length-1,this.focusState.currentColIndex].join("-"))}else{this.removeFocus(n);for(var i=this.focusState.currentRowIndex-1,s=this.focusState.currentColIndex,o=[i,s].join("-");this.focusElementMap[this.focusState.currentIndexString]===this.focusElementMap[o];)o=[--i,s].join("-");this.setFocus(o=[i,s].join("-"))}}},{key:"moveDown",value:function(t,e,n){if(this.keysMap.down.handler&&this.keysMap.down.handler(t,e,n),this.isBottomBoundary()){if(this.focusOptions.circle.vertical){this.removeFocus(n);this.setFocus([0,this.focusState.currentColIndex].join("-"))}}else{this.removeFocus(n);for(var i=this.focusState.currentRowIndex+1,s=this.focusState.currentColIndex,o=[i,s].join("-");this.focusElementMap[this.focusState.currentIndexString]===this.focusElementMap[o];)o=[++i,s].join("-");this.setFocus(o=[i,s].join("-"))}}},{key:"moveLeft",value:function(t,e,n){if(this.keysMap.left.handler&&this.keysMap.left.handler(t,e,n),this.isLeftBoundary()){if(this.focusOptions.circle.horizontal){this.removeFocus(n);var i=this.focusState.currentRowIndex;this.setFocus([i,this.indexMap[i][this.indexMap[i].length-1]].join("-"))}}else{this.removeFocus(n);for(var s=this.focusState.currentRowIndex,o=this.focusState.currentColIndex-1,r=[s,o].join("-");this.focusElementMap[this.focusState.currentIndexString]===this.focusElementMap[r];)r=[s,--o].join("-");this.setFocus(r=[s,o].join("-"))}}},{key:"moveRight",value:function(t,e,n){if(this.keysMap.right.handler&&this.keysMap.right.handler(t,e,n),this.isRightBoundary()){if(this.focusOptions.circle.horizontal){this.removeFocus(n);var i=this.focusState.currentRowIndex;this.setFocus([i,this.indexMap[i][0]].join("-"))}}else{this.removeFocus(n);for(var s=this.focusState.currentRowIndex,o=this.focusState.currentColIndex+1,r=[s,o].join("-");this.focusElementMap[this.focusState.currentIndexString]===this.focusElementMap[r];)r=[s,++o].join("-");this.setFocus(r=[s,o].join("-"))}}},{key:"move",value:function(t,e){Object.assign({},this.keysMap,{up:{handler:this.moveUp},down:{handler:this.moveDown},left:{handler:this.moveLeft},right:{handler:this.moveRight}})[t].handler.call(this,e,this.focusState.currentFocusElement,this.focusState.currentIndexString)}}]),n}(),d=function(){function n(t,e){r(this,n),this.hooks={},this.init(t,e),e.lifecycle=this,e.callHook=this.callHook.bind(this)}return s(n,[{key:"init",value:function(t,n){var i={beforeCreate:t.beforeCreate,created:t.created,beforeMount:t.beforeMount,mounted:t.mounted,beforeUpdate:t.beforeUpdate,updated:t.updated,beforeDestory:t.beforeDestory,destoried:t.destoried};Object.keys(i).forEach(function(t,e){void 0===i[t]&&(i[t]=v),i[t]=i[t]instanceof Function?i[t].bind(n):(console.warn("lifecycle hooks must be a function"),v)}),this.hooks=i}},{key:"callHook",value:function(t){this.hooks[t]()}}]),n}();function v(){}return function(){function e(t){r(this,e),new c(this,t),new d(t.hooks||{},this),this.callHook("beforeCreate"),this.$data="function"==typeof t.data?t.data():t.data,this.methods=t.methods,this.proxy(this.$data,this),this.proxy(t.methods,this),t.el&&(new n(this.$data,this),this.callHook("created"),this.callHook("beforeMount"),new i(t.el,this),this.focuser.generateIndexMap(),this.callHook("mounted"))}return s(e,[{key:"proxy",value:function(n,t){Object.keys(n).forEach(function(e){Object.defineProperty(t,e,{enumerable:!0,configurable:!0,get:function(){return n[e]},set:function(t){n[e]=t}})})}}]),e}()}); -------------------------------------------------------------------------------- /docs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zexiplus/TVVM/9852ac9edf2c0a2a688e61c0a13b44b64de8aa52/docs/.gitkeep -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zexiplus/TVVM/9852ac9edf2c0a2a688e61c0a13b44b64de8aa52/docs/favicon.ico -------------------------------------------------------------------------------- /docs/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zexiplus/TVVM/9852ac9edf2c0a2a688e61c0a13b44b64de8aa52/docs/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /docs/fonts/Raleway-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zexiplus/TVVM/9852ac9edf2c0a2a688e61c0a13b44b64de8aa52/docs/fonts/Raleway-Regular.ttf -------------------------------------------------------------------------------- /docs/imgs/index.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zexiplus/TVVM/9852ac9edf2c0a2a688e61c0a13b44b64de8aa52/docs/imgs/index.bmp -------------------------------------------------------------------------------- /docs/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zexiplus/TVVM/9852ac9edf2c0a2a688e61c0a13b44b64de8aa52/docs/imgs/logo.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | TVVM 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 |
26 | 34 |
35 |
36 |
37 |
38 | 39 |
40 |
41 | 42 |
43 |
44 | 45 |
46 |
47 | 48 |
49 |
50 |
51 |
52 |
57 | 1-0, 2-0 58 |
59 |
60 |
61 | 1-1, 1-2, 1-3 62 |
63 |
64 |
2-1
65 |
2-2
66 |
2-3
67 |
68 |
69 |
70 |
71 |
72 | 73 |
74 |
75 |
76 |

TVVM

77 |
78 | 轻量级响应式TV WEB应用开发框架 79 |
80 |
download
81 |
82 |
83 | 84 |
85 |
86 | 焦点控制 87 |
88 | 89 | TV应用的特点是使用遥控器控制焦点移动, 从而进行下一步操作。 90 | TVVM致力于减少焦点移动的代码逻辑, 通过配置t-index索引, 而非编程的方式解决焦点的移动。 91 | 92 |
93 |
94 |
95 | 按键优化 96 | 97 | 某些物理遥控器在按下按钮时, 有可能高速重复触发按键事件, 这对应用会产生不良后果, 98 | TVVM在绑定事件的同时在内部会优化操作逻辑, 利用函数去抖控制按键触发频率, 防止因为物理设备差异导致应用逻辑混乱。 99 | 100 |
101 |
102 | 数据驱动 103 | 104 | TVVM与大多数mvvm思想的前端框架一样, 采用数据驱动的开发模式, 简单来讲,数据驱动使开发者只用关系数据的修改, 而无需手动将数据同步至视图。 105 | 106 |
107 |
108 |
109 |
110 | 111 |
112 |
113 |
118 | 119 |
120 |
121 |
126 | 131 | 136 | 141 | 146 |
147 |
148 |
153 | 154 |
155 |
160 | 161 |
162 | 169 |
170 | 171 |
172 |
173 |
174 |
175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /docs/pages/doc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | TVVM 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 37 |
38 |
39 | 82 |
83 | 84 |
85 |
86 |

开始

87 | 88 |

介绍

89 | 90 | TVVM是一个专门为TV WEB APP开发的MVVM模式框架, 它帮助开发者快速开发应用而无需关心焦点控制, 91 | 键盘绑定, 数据绑定等通用逻辑。 92 | 它没有依赖, 体型小巧(20kb) 93 | 94 | 95 |

下载

96 |

通过npm下载

97 | 你的系统上需要安装有nodejs 98 |
 99 |           $ npm install -S tvvm
100 |         
101 | 102 |

手动下载

103 | 104 | 你也可以通过手动下载的方式, 然后通过<script>标签进行引用, 105 | 下载链接 106 | 107 | 108 |

使用

109 |
110 |           
111 |   // es module
112 |   import * as TVVM from 'tvvm'
113 | 
114 |   var tv = new TVVM({
115 |   ...options
116 |   })
117 |           
118 |           
119 |   <!-- html -->
120 |   <script src="https://unpkg.com/tvvm/dist/tvvm.min.js"></script>
121 |   <script>
122 |       var tv = new TVVM({
123 |         ...options
124 |       })
125 |   </script>
126 |           
127 |         
128 | 129 |

特色

130 |

焦点自动控制

131 | 132 | 133 | TV应用的特点是使用遥控器控制焦点移动, 而并非鼠标点击/手势触摸事件来触发控件, 从而进行下一步操作。 134 | 这需要我们通过编程的方式控制焦点的移动和跳转。TVVM致力于减少焦点移动的代码逻辑, 通过配置t-index索引, 而非编程的方式解决焦点的移动。 135 |
136 | 137 | TVVM把应用分成横轴空间和纵轴空间, 例如 声明 t-index="0-1" 代表应用是第1行 (row=0) 第2列 (col=1)为当前焦点 , 当你点击遥控器的向下移动按钮时 row 增加,col 不变, 此时焦点从 t-index="0-1"移动至 t-index="1-1"的元素上 。 138 | 这些在TVVM内部已经做好了, 开发者只需要在焦点块元素上声明 t-index索引即可。 139 | 140 | 141 | 除此之外, TVVM还提供了丰富的选项, 例如默认焦点, 焦点元素样式, 边界穿越, 按键值配置等。 142 | 143 |
144 |

focus选项

145 |
146 |           
147 |   new TVVM({
148 |       ...,
149 |       focus: {
150 |         defaultFocusIndex: '0-1', // 默认焦点
151 |         circle: {
152 |           horizontal: true, // 当焦点移动到边界时在水平方向可穿越
153 |           vertical: true // 当焦点移动到边界时在垂直方向可穿越
154 |       },
155 |           ...options
156 |     }
157 |   })
158 |           
159 |           
160 |   <style>
161 |     *:focus {
162 |         outline: none;
163 |         border: 2px solid #fff;
164 |     }
165 |   </style>
166 |   <div class="tv">
167 |       <div>
168 |           <div>
169 |               <div t-index="0-1">0-1</div>
170 |               <div t-index="0-2">0-2</div>
171 |               <div t-index="0-3">0-3</div>
172 |           </div>
173 |       </div>
174 |       <div>
175 |           <div t-index="1-0, 2-0" real-focus="true">
176 |               <span>1-0,</span> <span>2-0</span>
177 |           </div>
178 |           <div>
179 |               <div t-index="1-1, 1-2, 1-3">
180 |                   <span>1-1,</span> <span>1-2,</span> <span>1-3</span>
181 |               </div>
182 |               <div>
183 |                   <div t-index="2-1">2-1</div>
184 |                   <div t-index="2-2">2-2</div>
185 |                   <div t-index="2-3">2-3</div>
186 |               </div>
187 |           </div>
188 |       </div>
189 |   </div>
190 |           
191 |         
192 |

显示结果

193 | t-index 194 | 195 |

按键去抖优化

196 | 197 | 某些物理遥控器在按下按钮时, 有可能高速触发按键事件, 这对应用会产生不良后果, 198 | TVVM在绑定事件的同时在内部会优化操作逻辑, 利用函数去抖控制按键触发频率, 防止因为物理设备差异导致应用逻辑混乱。 199 | 200 | 201 |

数据驱动

202 | 203 | TVVM与大多数mvvm思想的前端框架一样, 采用数据驱动的开发模式, 简单来讲,数据驱动使开发者只用关系数据的修改, 而无需手动将数据同步至视图。 204 | 以下是 t-value 指令来实现双向数据绑定的例子, span标签内的内容会随着input输入框的值的改变而改变 205 | 206 |
207 |           
208 |     data: function () {
209 |         return {
210 |             demoInputValue: 'demo'
211 |         }
212 |     }
213 |           
214 |           
215 |   <input t-value="data.demoInputValue" />
216 |   <span>{{data.demoInputValue}}</span>
217 |           
218 |         
219 |
220 |

demo

221 | 222 | {{data.demoInputValue}} 223 |
224 | 225 |

API

226 |

实例化

227 | new TVVM 返回一个tv实例, 作为该页面的全局单例入口 228 |
229 |             
230 |   var tv = new TVVM({
231 |     el: '#tv',
232 |     data () {
233 |         return {
234 | 
235 |         }
236 |     },
237 |     focus: {
238 | 
239 |     },
240 |     methods: {
241 |         testFn: function (a, b) {
242 | 
243 |         }
244 |     }
245 |   })
246 |             
247 |           
248 | 249 |

指令

250 | 251 | TVVM内置指令系统, 包含了一些常用的指令, 用于处理简单的模版逻辑.TVVM内置指令通常以 t- 开头作为标识 252 | 253 |
254 |             
255 |   <html>
256 |     <head></head>
257 |     <body>
258 |         <div id="tv">
259 |             <div t-if="data.dialogVisible" class="dialog">{{data.dialog.title}}</div>
260 |             <nav>
261 |                 <a t-for="item in data.linkList">{{item.label}}</a>
262 |             </nav>
263 |             <form>
264 |               <input t-value="data.dialog.title" />
265 |             </form>
266 |             <div>
267 |             <div " t-index="1-1, 1-2, 1-3"></div>
268 |             <div>
269 |               <div t-index="2-1"></div>
270 |               <div t-index="2-2"></div>
271 |               <div t-index="2-3"></div>
272 |             </div>
273 |           </div>
274 |         </div>
275 |     </body>
276 |   </html>
277 |             
278 |           
279 |
280 |

t-index

281 |
282 | 用于指定焦点区块元素的二维空间位置索引,以便用户点击遥控器方向按键时移动焦点,t-index="x-y", 例如t-index="0-0"代表第一排第一列的元素 283 |
284 |
285 |               
286 |  <div class="tv">
287 |     <div t-index="0-0">0-0</div> <!-- 第1排第1个元素 -->
288 |     <div t-index="0-1">0-1</div>
289 |     <div t-index="0-2">0-2</div>
290 |     <!-- 第2排第1个元素, 纵向占据2个空间 -->
291 |     <div t-index="1-0, 2-0">1-0, 2-0</div> 
292 |     <div>
293 |         <!-- 第2排第2个元素, 横向占据3个空间 -->
294 |         <div t-index="1-1, 1-2, 1-3">1-1, 1-2, 1-3</div> 
295 |         <div>
296 |             <div t-index="2-1">2-1</div> <!-- 第3排第2个元素 -->
297 |             <div t-index="2-2">2-2</div> <!-- 第3排第3个元素 -->
298 |             <div t-index="2-3">2-3</div> <!-- 第3排第4个元素 -->
299 |         </div>
300 |     </div>
301 | </div>
302 |               
303 |             
304 | 305 |

t-if

306 | 307 | 用于显示/隐藏dom节点, 该指令接收一个在data参数上存在的布尔值 308 | 309 |
310 |               
311 | <div t-if="data.dialogVisible"></div>
312 |               
313 |             
314 |

t-for

315 | 316 | 用于循环指定的dom节点, 该指令接收一个表达式,如下item代表数组的每一项, array是data上定义的一个数组 317 | 318 |
319 |               
320 | <ul>
321 |   <li t-for="item in data.array">{{item.label}}</li>
322 | </ul>
323 |               
324 |             
325 |

data bind

326 | 327 | 数据插值, 双花括号({{}})内接收一个data的属性.用于页面内数据插值 328 | 329 |
330 |               
331 | data: { name: 'float' }
332 |               
333 |               
334 | <span>{{data.name}}</span>
335 | <!-- 渲染为 -->
336 | <span>float</span>
337 |               
338 |             
339 | 340 |

t-class

341 | 342 | 样式表绑定, t-class接收一个data上已经定义的样式名数组或对象 343 | 344 |
345 |               
346 |   data: function () {
347 |       return {
348 |           classList1: ['container', 'container-row'],
349 |           classList2: {
350 |               'container': true,
351 |               'container-row': false,
352 |               'container-col': true
353 |           }
354 |       }
355 |   }
356 |               
357 |               
358 |   <div t-class="data.classList2"></div>
359 |   <!-- 渲染为 -->
360 |   <div class="container container-col"></div>
361 |               
362 |             
363 |

t-bind

364 | 365 | 属性绑定,t-bind:name="data.name" 简写形式为 :name="data.name" 366 | 367 |
368 |               
369 |   data: function () {
370 |       return {
371 |           id: 'billboard',
372 |           height: '365',
373 |           classname: 'spin'
374 |       }
375 |   }
376 |               
377 |               
378 |     <div :id="data.id" :height="data.height" :class="data.classname"></div>
379 |     <!-- 渲染为 -->
380 |     <div id="billboard" height="365" class="spin"></div>
381 |               
382 |             
383 | 384 |

t-value

385 | 386 | 用于input输入数据与data上数据的绑定, input的value会实时同步至t-model绑定的数据 387 | 388 |
389 |               
390 |     data: function () {
391 |         return {
392 |             demoInputValue: 'demo'
393 |         }
394 |     }
395 |               
396 |               
397 |     <input t-value="data.demoInputValue" />
398 |     <span>{{data.demoInputValue}}</span>
399 |               
400 |             
401 |
402 |

demo

403 | 404 | {{data.demoInputValue}} 405 |
406 | 407 |

event bind

408 | 409 | 事件绑定 410 | 411 |
412 |               
413 |     methods: {
414 |       clickHandler: function () {
415 |           // do something
416 |       },
417 |       clickHandler2: function (param1, param2) {
418 |               
419 |       }
420 |     }
421 |               
422 |               
423 |     <div @click="methods.clickHandler"></div>
424 |     <div @click="methods.clickHandler2(data.inputValue)"></div>
425 |               
426 |             
427 |
428 | 429 |

选项

430 | 431 | new TVVM接收一个选项对象作为唯一参数 432 | 433 |
434 |             
435 | var tv = new TVVM({
436 |   el,
437 |   data,
438 |   focus,
439 |   methods,
440 |   lifeHooks,
441 | })
442 |             
443 |           
444 |
445 |

el

446 | 447 | new TVVM() 实例挂载的dom元素, 可以是一个元素查找符或者dom节点对象 448 | 449 |
450 |               
451 | new TVVM({
452 |     el: '#tv',
453 | })
454 |               
455 |             
456 | 457 |

focus

458 | 459 | focus选项用于设置焦点移动, 键值绑定, 默认焦点等逻辑 460 | 461 |
462 |               
463 | new TVVM({
464 |   focus: {
465 |     defaultFocusIndex: '1-0',
466 |     keysMap: {
467 |       'up': {
468 |         codes: [38, 103],
469 |         handler: function (event, node, index, prevNode) {
470 | 
471 |         }
472 |       },
473 |       'g': {
474 |         codes: [71],
475 |         handler: function (event, node, index, prevNode) {
476 |           console.log('you press g')
477 |         }
478 |       },
479 |     },
480 |     keysMapMergeCoverage: false,
481 |     circle: {
482 |       horizontal: true,
483 |       vertical: true,
484 |     }
485 |   }
486 | })
487 |               
488 |             
489 | 490 |
491 |

defaultFocusIndex (可选)

492 | 493 | 进入应用默认聚焦的元素的索引, 该参数为空时, 默认聚焦到页面首个焦点元素上 494 | 495 |
496 |                 
497 |     focus: {
498 |         defaultIndex: '0-1'
499 |     }
500 |                 
501 |               
502 |

activeClass (可选)

503 | 焦点元素的样式名 504 |
505 |                 
506 |     focus: {
507 |       activeClass: 'high-light'
508 |     }
509 |                 
510 |               
511 | 512 |

circle (可选)

513 | 514 | 定义焦点在水平/垂直方向上是否可循环移动, 默认false.
515 | - horizontal (可选) 焦点元素在水平方向是否可以循环移动, 默认false
516 | - vertival (可选) 焦点元素在水平方向上是否可以循环移动, 默认false 517 | 518 |
519 |
520 |                 
521 |     focus: {
522 |         circle: {
523 |             horizontal: true,
524 |             vertical: true
525 |         }
526 |     }
527 |                 
528 |               
529 | 530 |

keysMap

531 | 532 | 遥控器键盘键值码映射表, 该参数为空时使用默认键值码映射表
533 | - 'alias' 对应键值的别名
534 | - codes 对应键值数组
535 | - handler 对应按键值绑定的事件处理函数 参数分别是event(事件), node(当前焦点dom节点索引), index (当前焦点dom节点的t-index值), prevNode(上一个焦点dom节点索引) 536 |
537 |
538 |                 
539 |     focus: {
540 |       keysMap: {
541 |       'up': {
542 |         codes: [38, 104],
543 |         handler: function (event, node, index, prevNode)
544 |       },
545 |       'down': {
546 |         codes: [40, 98],
547 |         handler
548 |       },
549 |       'left': {
550 |         codes: [37, 100],
551 |         handler
552 |       },
553 |       'right': {
554 |         codes: [39, 102],
555 |         handler
556 |       },
557 |       'enter': {
558 |         codes: [13, 32],
559 |         handler
560 |       }
561 |     }
562 |   
563 |                 
564 |               
565 |

keysMapMergeCoverage (可选)

566 | 567 | 键值映射表合并策略true为覆盖, false为合并 568 | 569 |
570 |                 
571 |   focus: {
572 |     keysMapMergeCoverage: false,
573 |   }
574 |                 
575 |               
576 |
577 |

data

578 | 579 | tv单例的数据对象, 可以是一个函数或者对象, 当该参数为函数时, 取值为该函数的返回值 580 | 581 |
582 |               
583 |   new TVVM({
584 |     data: function () {
585 |         return {
586 |             title: 'tvvm demo page',
587 |             index: '0'
588 |         }
589 |     }
590 |   })
591 |               
592 |             
593 |

methods

594 | 595 | 该参数是一个对象, 存放所有的方法函数 596 | 597 |
598 |               
599 |   methods: {
600 |     methods1: function () {
601 |         console.log('methods1')
602 |     }
603 |   }
604 |               
605 |             
606 |

hooks

607 | 608 | 生命周期钩子函数集合 609 | 610 | 611 | - beforeCreate 612 | 在实例化之前调用, 此时不可访问data
613 | - created 614 | 在实例化后调用,此时data已经设置响应式, 并可访问
615 | - beforeMount 616 | 在实例被挂在到真实dom前调用
617 | - mounted 618 | 在实例被挂在到dom上时调用
619 | - beforeUpdate 620 | 响应式data变动从而导致视图更新前调用
621 | - updated 622 | 响应式data变动从而导致视图更新后调用
623 | - beforeDestory 624 | 在实例被销毁前调用
625 |
626 |
627 |               
628 |     hooks: {
629 |       beforeCreate: function () {
630 |           // this 指向tv实例
631 |       },
632 |       mounted: function () {
633 |                 
634 |       },
635 |       ...
636 |     }
637 |               
638 |             
639 |
640 |
641 |
642 | 643 | 644 |
645 |
646 |
647 | 648 |
649 |
650 | 651 | 652 | -------------------------------------------------------------------------------- /docs/scripts/doc.js: -------------------------------------------------------------------------------- 1 | var tv = new TVVM({ 2 | el: '#doc', 3 | hooks: { 4 | mounted: function () { 5 | var scroll = new BScroll('.content-wrapper', { 6 | // startY: 600, 7 | scrollY: true, 8 | scrollX: false, 9 | click: true, 10 | type: true, 11 | bounce: { 12 | top: true, 13 | bottom: true, 14 | left: true, 15 | right: true 16 | } 17 | }) 18 | }, 19 | updated: function () { 20 | 21 | } 22 | }, 23 | data: function () { 24 | return { 25 | demoInputValue: 'demo', 26 | menuShow: false, 27 | navClassList: { 28 | 'menu-hidden': true, 29 | 'menu-show': false 30 | }, 31 | cloakClassList: { 32 | 'cloak-hidden': true, 33 | 'cloak-show': false 34 | }, 35 | buttonClassList: { 36 | 'icon-back': false, 37 | 'icon-menu': true 38 | } 39 | } 40 | }, 41 | methods: { 42 | toggleMenu: function (event) { 43 | event.preventDefault() 44 | var button = document.querySelector('#menu-button') 45 | if (this.menuShow) { 46 | this.menuShow = false 47 | this.navClassList = { 48 | 'menu-hidden': true, 49 | 'menu-show': false 50 | } 51 | this.cloakClassList = { 52 | 'cloak-hidden': true, 53 | 'cloak-show': false 54 | } 55 | this.buttonClassList = { 56 | 'icon-back': false, 57 | 'icon-menu': true 58 | } 59 | } else { 60 | this.menuShow = true 61 | this.navClassList = { 62 | 'menu-hidden': false, 63 | 'menu-show': true 64 | } 65 | this.cloakClassList = { 66 | 'cloak-hidden': false, 67 | 'cloak-show': true 68 | } 69 | this.buttonClassList = { 70 | 'icon-back': true, 71 | 'icon-menu': false 72 | } 73 | } 74 | } 75 | } 76 | }) 77 | 78 | -------------------------------------------------------------------------------- /docs/scripts/highlight.pack.js: -------------------------------------------------------------------------------- 1 | /*! highlight.js v9.13.1 | BSD3 License | git.io/hljslicense */ 2 | !function(e){var n="object"==typeof window&&window||"object"==typeof self&&self;"undefined"!=typeof exports?e(exports):n&&(n.hljs=e({}),"function"==typeof define&&define.amd&&define([],function(){return n.hljs}))}(function(e){function n(e){return e.replace(/&/g,"&").replace(//g,">")}function t(e){return e.nodeName.toLowerCase()}function r(e,n){var t=e&&e.exec(n);return t&&0===t.index}function a(e){return k.test(e)}function i(e){var n,t,r,i,o=e.className+" ";if(o+=e.parentNode?e.parentNode.className:"",t=M.exec(o))return w(t[1])?t[1]:"no-highlight";for(o=o.split(/\s+/),n=0,r=o.length;r>n;n++)if(i=o[n],a(i)||w(i))return i}function o(e){var n,t={},r=Array.prototype.slice.call(arguments,1);for(n in e)t[n]=e[n];return r.forEach(function(e){for(n in e)t[n]=e[n]}),t}function c(e){var n=[];return function r(e,a){for(var i=e.firstChild;i;i=i.nextSibling)3===i.nodeType?a+=i.nodeValue.length:1===i.nodeType&&(n.push({event:"start",offset:a,node:i}),a=r(i,a),t(i).match(/br|hr|img|input/)||n.push({event:"stop",offset:a,node:i}));return a}(e,0),n}function u(e,r,a){function i(){return e.length&&r.length?e[0].offset!==r[0].offset?e[0].offset"}function c(e){l+=""}function u(e){("start"===e.event?o:c)(e.node)}for(var s=0,l="",f=[];e.length||r.length;){var g=i();if(l+=n(a.substring(s,g[0].offset)),s=g[0].offset,g===e){f.reverse().forEach(c);do u(g.splice(0,1)[0]),g=i();while(g===e&&g.length&&g[0].offset===s);f.reverse().forEach(o)}else"start"===g[0].event?f.push(g[0].node):f.pop(),u(g.splice(0,1)[0])}return l+n(a.substr(s))}function s(e){return e.v&&!e.cached_variants&&(e.cached_variants=e.v.map(function(n){return o(e,{v:null},n)})),e.cached_variants||e.eW&&[o(e)]||[e]}function l(e){function n(e){return e&&e.source||e}function t(t,r){return new RegExp(n(t),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,i){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var o={},c=function(n,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");o[t[0]]=[n,t[1]?Number(t[1]):1]})};"string"==typeof a.k?c("keyword",a.k):B(a.k).forEach(function(e){c(e,a.k[e])}),a.k=o}a.lR=t(a.l||/\w+/,!0),i&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.endSameAsBegin&&(a.e=a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=n(a.e)||"",a.eW&&i.tE&&(a.tE+=(a.e?"|":"")+i.tE)),a.i&&(a.iR=t(a.i)),null==a.r&&(a.r=1),a.c||(a.c=[]),a.c=Array.prototype.concat.apply([],a.c.map(function(e){return s("self"===e?a:e)})),a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,i);var u=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(n).filter(Boolean);a.t=u.length?t(u.join("|"),!0):{exec:function(){return null}}}}r(e)}function f(e,t,a,i){function o(e){return new RegExp(e.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&"),"m")}function c(e,n){var t,a;for(t=0,a=n.c.length;a>t;t++)if(r(n.c[t].bR,e))return n.c[t].endSameAsBegin&&(n.c[t].eR=o(n.c[t].bR.exec(e)[0])),n.c[t]}function u(e,n){if(r(e.eR,n)){for(;e.endsParent&&e.parent;)e=e.parent;return e}return e.eW?u(e.parent,n):void 0}function s(e,n){return!a&&r(n.iR,e)}function p(e,n){var t=R.cI?n[0].toLowerCase():n[0];return e.k.hasOwnProperty(t)&&e.k[t]}function d(e,n,t,r){var a=r?"":j.classPrefix,i='',i+n+o}function h(){var e,t,r,a;if(!E.k)return n(k);for(a="",t=0,E.lR.lastIndex=0,r=E.lR.exec(k);r;)a+=n(k.substring(t,r.index)),e=p(E,r),e?(M+=e[1],a+=d(e[0],n(r[0]))):a+=n(r[0]),t=E.lR.lastIndex,r=E.lR.exec(k);return a+n(k.substr(t))}function b(){var e="string"==typeof E.sL;if(e&&!L[E.sL])return n(k);var t=e?f(E.sL,k,!0,B[E.sL]):g(k,E.sL.length?E.sL:void 0);return E.r>0&&(M+=t.r),e&&(B[E.sL]=t.top),d(t.language,t.value,!1,!0)}function v(){y+=null!=E.sL?b():h(),k=""}function m(e){y+=e.cN?d(e.cN,"",!0):"",E=Object.create(e,{parent:{value:E}})}function N(e,n){if(k+=e,null==n)return v(),0;var t=c(n,E);if(t)return t.skip?k+=n:(t.eB&&(k+=n),v(),t.rB||t.eB||(k=n)),m(t,n),t.rB?0:n.length;var r=u(E,n);if(r){var a=E;a.skip?k+=n:(a.rE||a.eE||(k+=n),v(),a.eE&&(k=n));do E.cN&&(y+=I),E.skip||E.sL||(M+=E.r),E=E.parent;while(E!==r.parent);return r.starts&&(r.endSameAsBegin&&(r.starts.eR=r.eR),m(r.starts,"")),a.rE?0:n.length}if(s(n,E))throw new Error('Illegal lexeme "'+n+'" for mode "'+(E.cN||"")+'"');return k+=n,n.length||1}var R=w(e);if(!R)throw new Error('Unknown language: "'+e+'"');l(R);var x,E=i||R,B={},y="";for(x=E;x!==R;x=x.parent)x.cN&&(y=d(x.cN,"",!0)+y);var k="",M=0;try{for(var C,A,S=0;;){if(E.t.lastIndex=S,C=E.t.exec(t),!C)break;A=N(t.substring(S,C.index),C[0]),S=C.index+A}for(N(t.substr(S)),x=E;x.parent;x=x.parent)x.cN&&(y+=I);return{r:M,value:y,language:e,top:E}}catch(O){if(O.message&&-1!==O.message.indexOf("Illegal"))return{r:0,value:n(t)};throw O}}function g(e,t){t=t||j.languages||B(L);var r={r:0,value:n(e)},a=r;return t.filter(w).filter(x).forEach(function(n){var t=f(n,e,!1);t.language=n,t.r>a.r&&(a=t),t.r>r.r&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function p(e){return j.tabReplace||j.useBR?e.replace(C,function(e,n){return j.useBR&&"\n"===e?"
":j.tabReplace?n.replace(/\t/g,j.tabReplace):""}):e}function d(e,n,t){var r=n?y[n]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(r)&&a.push(r),a.join(" ").trim()}function h(e){var n,t,r,o,s,l=i(e);a(l)||(j.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):n=e,s=n.textContent,r=l?f(l,s,!0):g(s),t=c(n),t.length&&(o=document.createElementNS("http://www.w3.org/1999/xhtml","div"),o.innerHTML=r.value,r.value=u(t,c(o),s)),r.value=p(r.value),e.innerHTML=r.value,e.className=d(e.className,l,r.language),e.result={language:r.language,re:r.r},r.second_best&&(e.second_best={language:r.second_best.language,re:r.second_best.r}))}function b(e){j=o(j,e)}function v(){if(!v.called){v.called=!0;var e=document.querySelectorAll("pre code");E.forEach.call(e,h)}}function m(){addEventListener("DOMContentLoaded",v,!1),addEventListener("load",v,!1)}function N(n,t){var r=L[n]=t(e);r.aliases&&r.aliases.forEach(function(e){y[e]=n})}function R(){return B(L)}function w(e){return e=(e||"").toLowerCase(),L[e]||L[y[e]]}function x(e){var n=w(e);return n&&!n.disableAutodetect}var E=[],B=Object.keys,L={},y={},k=/^(no-?highlight|plain|text)$/i,M=/\blang(?:uage)?-([\w-]+)\b/i,C=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,I="
",j={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};return e.highlight=f,e.highlightAuto=g,e.fixMarkup=p,e.highlightBlock=h,e.configure=b,e.initHighlighting=v,e.initHighlightingOnLoad=m,e.registerLanguage=N,e.listLanguages=R,e.getLanguage=w,e.autoDetection=x,e.inherit=o,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},e.C=function(n,t,r){var a=e.inherit({cN:"comment",b:n,e:t,c:[]},r||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e.METHOD_GUARD={b:"\\.\\s*"+e.UIR,r:0},e});hljs.registerLanguage("perl",function(e){var t="getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when",r={cN:"subst",b:"[$@]\\{",e:"\\}",k:t},s={b:"->{",e:"}"},n={v:[{b:/\$\d/},{b:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{b:/[\$%@][^\s\w{]/,r:0}]},i=[e.BE,r,n],o=[n,e.HCM,e.C("^\\=\\w","\\=cut",{eW:!0}),s,{cN:"string",c:i,v:[{b:"q[qwxr]?\\s*\\(",e:"\\)",r:5},{b:"q[qwxr]?\\s*\\[",e:"\\]",r:5},{b:"q[qwxr]?\\s*\\{",e:"\\}",r:5},{b:"q[qwxr]?\\s*\\|",e:"\\|",r:5},{b:"q[qwxr]?\\s*\\<",e:"\\>",r:5},{b:"qw\\s+q",e:"q",r:5},{b:"'",e:"'",c:[e.BE]},{b:'"',e:'"'},{b:"`",e:"`",c:[e.BE]},{b:"{\\w+}",c:[],r:0},{b:"-?\\w+\\s*\\=\\>",c:[],r:0}]},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"(\\/\\/|"+e.RSR+"|\\b(split|return|print|reverse|grep)\\b)\\s*",k:"split return print reverse grep",r:0,c:[e.HCM,{cN:"regexp",b:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",r:10},{cN:"regexp",b:"(m|qr)?/",e:"/[a-z]*",c:[e.BE],r:0}]},{cN:"function",bK:"sub",e:"(\\s*\\(.*?\\))?[;{]",eE:!0,r:5,c:[e.TM]},{b:"-\\w\\b",r:0},{b:"^__DATA__$",e:"^__END__$",sL:"mojolicious",c:[{b:"^@@.*",e:"$",cN:"comment"}]}];return r.c=o,s.c=o,{aliases:["pl","pm"],l:/[\w\.]+/,k:t,c:o}});hljs.registerLanguage("bash",function(e){var t={cN:"variable",v:[{b:/\$[\w\d#@][\w\d_]*/},{b:/\$\{(.*?)}/}]},s={cN:"string",b:/"/,e:/"/,c:[e.BE,t,{cN:"variable",b:/\$\(/,e:/\)/,c:[e.BE]}]},a={cN:"string",b:/'/,e:/'/};return{aliases:["sh","zsh"],l:/\b-?[a-z\._]+\b/,k:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp",_:"-ne -eq -lt -gt -f -d -e -s -l -a"},c:[{cN:"meta",b:/^#![^\n]+sh\s*$/,r:10},{cN:"function",b:/\w[\w\d_]*\s*\(\s*\)\s*\{/,rB:!0,c:[e.inherit(e.TM,{b:/\w[\w\d_]*/})],r:0},e.HCM,s,a,t]}});hljs.registerLanguage("shell",function(s){return{aliases:["console"],c:[{cN:"meta",b:"^\\s{0,3}[\\w\\d\\[\\]()@-]*[>%$#]",starts:{e:"$",sL:"bash"}}]}});hljs.registerLanguage("xml",function(s){var e="[A-Za-z0-9\\._:-]+",t={eW:!0,i:/`]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist"],cI:!0,c:[{cN:"meta",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},s.C("",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"meta",b:/<\?xml/,e:/\?>/,r:10},{b:/<\?(php)?/,e:/\?>/,sL:"php",c:[{b:"/\\*",e:"\\*/",skip:!0},{b:'b"',e:'"',skip:!0},{b:"b'",e:"'",skip:!0},s.inherit(s.ASM,{i:null,cN:null,c:null,skip:!0}),s.inherit(s.QSM,{i:null,cN:null,c:null,skip:!0})]},{cN:"tag",b:"|$)",e:">",k:{name:"style"},c:[t],starts:{e:"",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"|$)",e:">",k:{name:"script"},c:[t],starts:{e:"",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},{cN:"tag",b:"",c:[{cN:"name",b:/[^\/><\s]+/,r:0},t]}]}});hljs.registerLanguage("json",function(e){var i={literal:"true false null"},n=[e.QSM,e.CNM],r={e:",",eW:!0,eE:!0,c:n,k:i},t={b:"{",e:"}",c:[{cN:"attr",b:/"/,e:/"/,c:[e.BE],i:"\\n"},e.inherit(r,{b:/:/})],i:"\\S"},c={b:"\\[",e:"\\]",c:[e.inherit(r)],i:"\\S"};return n.splice(n.length,0,t,c),{c:n,k:i,i:"\\S"}});hljs.registerLanguage("css",function(e){var c="[a-zA-Z-][a-zA-Z0-9_-]*",t={b:/[A-Z\_\.\-]+\s*:/,rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:/\S/,e:":",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\w-]+\(/,rB:!0,c:[{cN:"built_in",b:/[\w-]+/},{b:/\(/,e:/\)/,c:[e.ASM,e.QSM]}]},e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"number",b:"#[0-9A-Fa-f]+"},{cN:"meta",b:"!important"}]}}]};return{cI:!0,i:/[=\/|'\$]/,c:[e.CBCM,{cN:"selector-id",b:/#[A-Za-z0-9_-]+/},{cN:"selector-class",b:/\.[A-Za-z0-9_-]+/},{cN:"selector-attr",b:/\[/,e:/\]/,i:"$"},{cN:"selector-pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{b:"@",e:"[{;]",i:/:/,c:[{cN:"keyword",b:/\w+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[e.ASM,e.QSM,e.CSSNM]}]},{cN:"selector-tag",b:c,r:0},{b:"{",e:"}",i:/\S/,c:[e.CBCM,t]}]}});hljs.registerLanguage("javascript",function(e){var r="[A-Za-z$_][0-9A-Za-z$_]*",t={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},a={cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},n={cN:"subst",b:"\\$\\{",e:"\\}",k:t,c:[]},c={cN:"string",b:"`",e:"`",c:[e.BE,n]};n.c=[e.ASM,e.QSM,c,a,e.RM];var s=n.c.concat([e.CBCM,e.CLCM]);return{aliases:["js","jsx"],k:t,c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},{cN:"meta",b:/^#!/,e:/$/},e.ASM,e.QSM,c,e.CLCM,e.CBCM,a,{b:/[{,]\s*/,r:0,c:[{b:r+"\\s*:",rB:!0,r:0,c:[{cN:"attr",b:r,r:0}]}]},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{cN:"function",b:"(\\(.*?\\)|"+r+")\\s*=>",rB:!0,e:"\\s*=>",c:[{cN:"params",v:[{b:r},{b:/\(\s*\)/},{b:/\(/,e:/\)/,eB:!0,eE:!0,k:t,c:s}]}]},{b://,sL:"xml",c:[{b:/<\w+\s*\/>/,skip:!0},{b:/<\w+/,e:/(\/\w+|\w+\/)>/,skip:!0,c:[{b:/<\w+\s*\/>/,skip:!0},"self"]}]}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:r}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:s}],i:/\[|%/},{b:/\$[(.]/},e.METHOD_GUARD,{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor",e:/\{/,eE:!0}],i:/#(?!!)/}});hljs.registerLanguage("markdown",function(e){return{aliases:["md","mkdown","mkd"],c:[{cN:"section",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"quote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"^```w*s*$",e:"^```s*$"},{b:"`.+?`"},{b:"^( {4}| )",e:"$",r:0}]},{b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"string",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"symbol",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:/^\[[^\n]+\]:/,rB:!0,c:[{cN:"symbol",b:/\[/,e:/\]/,eB:!0,eE:!0},{cN:"link",b:/:\s*/,e:/$/,eB:!0}]}]}}); -------------------------------------------------------------------------------- /docs/scripts/home.js: -------------------------------------------------------------------------------- 1 | new TVVM({ 2 | el: '#tv', 3 | data: function () { 4 | return { 5 | userinfo: { 6 | name: 'Float' 7 | }, 8 | remoteControl: { 9 | opacity: 1, 10 | bottom: 0, 11 | style: '' 12 | }, 13 | downloadWrapper: { 14 | opacity: 0, 15 | style: '' 16 | }, 17 | tv: { 18 | height: 0 19 | }, 20 | nav: { 21 | classList: { 22 | 'nav-dark': false, 23 | 'nav-light': true 24 | } 25 | } 26 | } 27 | }, 28 | hooks: { 29 | mounted: function () { 30 | var tvWrapper = document.querySelector('.tv-wrapper') 31 | this.tv.height = tvWrapper.getBoundingClientRect().height 32 | } 33 | }, 34 | methods: { 35 | gotoDownload: function () { 36 | window.location.assign('https://unpkg.com/tvvm@1.0.2/dist/tvvm.min.js') 37 | }, 38 | handleScroll: function (event) { 39 | var percent = (event.target.scrollTop / this.tv.height).toFixed(2) > 1 ? 1 : (event.target.scrollTop / this.tv.height).toFixed(2) 40 | var remoteHeight = document.querySelector('.remote-control').getBoundingClientRect().height 41 | this.remoteControl.bottom = - (remoteHeight * percent).toFixed(2) + 'px' 42 | this.remoteControl.opacity = (1 - percent).toFixed(2) 43 | this.remoteControl.style = `opacity: ${this.remoteControl.opacity}; bottom: ${this.remoteControl.bottom}` 44 | 45 | this.downloadWrapper.opacity = percent 46 | this.downloadWrapper.style = `opacity: ${this.downloadWrapper.opacity}` 47 | }, 48 | createPressEvent: function (keyCode) { 49 | var customEvent = new Event('keydown', {bubbles: true, cancelable: true}) 50 | customEvent.keyCode = keyCode 51 | window.dispatchEvent(customEvent) 52 | }, 53 | pressPower: function (e) { 54 | e.preventDefault() 55 | this.createPressEvent(71) 56 | }, 57 | pressEnter: function (e) { 58 | e.preventDefault() 59 | this.createPressEvent(13) 60 | }, 61 | pressLeft: function (e) { 62 | e.preventDefault() 63 | this.createPressEvent(37) 64 | }, 65 | pressUp: function(e) { 66 | e.preventDefault() 67 | this.createPressEvent(38) 68 | }, 69 | pressRight: function(e) { 70 | e.preventDefault() 71 | this.createPressEvent(39) 72 | }, 73 | pressDown: function(e) { 74 | e.preventDefault() 75 | this.createPressEvent(40) 76 | }, 77 | pressHome: function(e) { 78 | e.preventDefault() 79 | this.createPressEvent(36) 80 | }, 81 | pressBack: function(e) { 82 | e.preventDefault() 83 | this.createPressEvent(27) 84 | }, 85 | pressMenu: function(e) { 86 | e.preventDefault() 87 | this.createPressEvent(18) 88 | }, 89 | }, 90 | focus: { 91 | defaultFocusIndex: '1-0', 92 | activeClass: 'high-light', 93 | keysMap: { 94 | 'up': { 95 | codes: [38], 96 | handler: function (event, node, index, prevNode) { 97 | 98 | } 99 | }, 100 | 'shutdown': { 101 | codes: [71], 102 | handler: function () { 103 | console.log('shutdonw') 104 | } 105 | }, 106 | 'down': { 107 | codes: [40] 108 | }, 109 | 'left': { 110 | codes: [37], 111 | handler: function (event, node, index, prevNode) { 112 | 113 | } 114 | }, 115 | 'right': { 116 | codes: [39], 117 | handler: function (event, node, index, prevNode) { 118 | 119 | } 120 | }, 121 | 'enter': { 122 | codes: [13], 123 | handler: function (event, node, index, prevNode) { 124 | console.log('enter') 125 | } 126 | }, 127 | 'space': { 128 | codes: [32], 129 | handler: function (event, node, index, prevNode) { 130 | 131 | } 132 | }, 133 | 'return': { 134 | codes: [27], 135 | handler: function (event, node, index, prevNode) { 136 | console.log('back') 137 | } 138 | }, 139 | 'home': { 140 | codes: [36], 141 | handler: function () { 142 | console.log('home') 143 | } 144 | }, 145 | 'menu': { 146 | codes: [18], 147 | handler: function (event, node, index, prevNode) { 148 | console.log('menu') 149 | } 150 | }, 151 | }, 152 | keysMapMergeCoverage: false, 153 | circle: { 154 | horizontal: true, 155 | vertical: true, 156 | }, 157 | } 158 | }) -------------------------------------------------------------------------------- /docs/style/common.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'raleway'; 3 | src: local('raleway'), url('../fonts/Raleway-Regular.ttf') format('truetype'); 4 | } 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | border: 0; 9 | font-family: 'raleway'; 10 | } 11 | 12 | html { 13 | font-size: 12px; 14 | } 15 | 16 | @media only screen and (min-width: 600px) { 17 | html { 18 | font-size: 16px !important; 19 | } 20 | } 21 | 22 | a { 23 | text-decoration: none; 24 | color: #666; 25 | font-weight: 50; 26 | cursor: pointer; 27 | } 28 | a:active { 29 | font-weight: bold; 30 | } 31 | 32 | .navbar { 33 | position: fixed; 34 | top: 0; 35 | height: 60px; 36 | width: 100%; 37 | box-sizing: border-box; 38 | border-bottom: 1px solid #ebebeb; 39 | background-color: rgba(255,255,255,0.9); 40 | display: flex; 41 | justify-content: space-between; 42 | overflow: hidden; 43 | font-size: 18px; 44 | z-index: 1000; 45 | } 46 | .logo { 47 | background-image: url(../imgs/logo.png); 48 | background-size: 40px 35px; 49 | background-repeat: no-repeat; 50 | background-position: 10% 50%; 51 | cursor: pointer; 52 | width: 100px; 53 | height: 60px; 54 | line-height: 60px; 55 | text-align: right; 56 | font-weight: 100; 57 | margin-left: 1rem; 58 | } 59 | 60 | .link-list { 61 | margin-right: 20px; 62 | margin-left: 20px; 63 | line-height: 60px; 64 | } 65 | .container { 66 | position: relative; 67 | top: 60px; 68 | height: calc(100vh - 60px); 69 | overflow: auto; 70 | background-image: linear-gradient(120deg, #fdfbfb 0%, #ebedee 100%); 71 | } -------------------------------------------------------------------------------- /docs/style/default.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Original highlight.js style (c) Ivan Sagalaev 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #F0F0F0; 12 | } 13 | 14 | 15 | /* Base color: saturation 0; */ 16 | 17 | .hljs, 18 | .hljs-subst { 19 | color: #444; 20 | } 21 | 22 | .hljs-comment { 23 | color: #888888; 24 | } 25 | 26 | .hljs-keyword, 27 | .hljs-attribute, 28 | .hljs-selector-tag, 29 | .hljs-meta-keyword, 30 | .hljs-doctag, 31 | .hljs-name { 32 | font-weight: bold; 33 | } 34 | 35 | 36 | /* User color: hue: 0 */ 37 | 38 | .hljs-type, 39 | .hljs-string, 40 | .hljs-number, 41 | .hljs-selector-id, 42 | .hljs-selector-class, 43 | .hljs-quote, 44 | .hljs-template-tag, 45 | .hljs-deletion { 46 | color: #880000; 47 | } 48 | 49 | .hljs-title, 50 | .hljs-section { 51 | color: #880000; 52 | font-weight: bold; 53 | } 54 | 55 | .hljs-regexp, 56 | .hljs-symbol, 57 | .hljs-variable, 58 | .hljs-template-variable, 59 | .hljs-link, 60 | .hljs-selector-attr, 61 | .hljs-selector-pseudo { 62 | color: #BC6060; 63 | } 64 | 65 | 66 | /* Language color: hue: 90; */ 67 | 68 | .hljs-literal { 69 | color: #78A960; 70 | } 71 | 72 | .hljs-built_in, 73 | .hljs-bullet, 74 | .hljs-code, 75 | .hljs-addition { 76 | color: #397300; 77 | } 78 | 79 | 80 | /* Meta color: hue: 200 */ 81 | 82 | .hljs-meta { 83 | color: #1f7199; 84 | } 85 | 86 | .hljs-meta-string { 87 | color: #4d99bf; 88 | } 89 | 90 | 91 | /* Misc effects */ 92 | 93 | .hljs-emphasis { 94 | font-style: italic; 95 | } 96 | 97 | .hljs-strong { 98 | font-weight: bold; 99 | } 100 | -------------------------------------------------------------------------------- /docs/style/doc.css: -------------------------------------------------------------------------------- 1 | @media only screen and (max-width: 600px) { 2 | .left-nav { 3 | position: absolute; 4 | width: 0 !important; 5 | } 6 | .logo { 7 | display: none; 8 | } 9 | .menu-button { 10 | display: block; 11 | width: 100px !important; 12 | line-height: 60px; 13 | padding-left: 20px; 14 | cursor: pointer; 15 | } 16 | .menu-hidden { 17 | width: 0 !important; 18 | } 19 | .menu-show { 20 | width: 200px !important; 21 | } 22 | 23 | .cloak { 24 | position: fixed; 25 | left: 0; 26 | top: 61px; 27 | right: 0; 28 | bottom: 0; 29 | background-color: rgba(0, 0, 0, 0.3); 30 | } 31 | .cloak-show { 32 | display: block; 33 | } 34 | .cloak-hidden { 35 | display: none; 36 | } 37 | } 38 | 39 | h1, h2, h3, h4 { 40 | color: #0048ab; 41 | } 42 | 43 | span { 44 | line-height: 1.5rem; 45 | } 46 | 47 | .container { 48 | display: flex; 49 | flex-direction: row; 50 | position: relative; 51 | } 52 | 53 | .menu-button { 54 | width: 0; 55 | overflow: hidden; 56 | } 57 | 58 | .left-nav { 59 | width: 200px; 60 | height: calc(100vh - 61px); 61 | font-size: 14px; 62 | display: flex; 63 | z-index: 1000; 64 | background-color: #fdfbfb; 65 | border-right: 1px solid #ebebeb; 66 | flex-direction: column; 67 | overflow-y: auto; 68 | overflow-x: hidden; 69 | transition: all .1s ease-in-out; 70 | } 71 | 72 | .nav-list-wrapper { 73 | width: 200px; 74 | } 75 | 76 | .left-nav a { 77 | padding: 5px 10px; 78 | text-indent: 1rem; 79 | display: block; 80 | } 81 | .left-nav a:hover { 82 | font-weight: bold; 83 | } 84 | 85 | 86 | .link-title { 87 | 88 | } 89 | .link-title a { 90 | font-size: 16px; 91 | color: #0048ab !important; 92 | } 93 | .link-group { 94 | 95 | } 96 | .link-group a { 97 | text-indent: 2rem; 98 | } 99 | .link-detail a { 100 | text-indent: 4rem; 101 | } 102 | 103 | .content-wrapper { 104 | padding: 0 1rem; 105 | box-sizing: border-box; 106 | width: 100%; 107 | height: calc(100vh - 61px); 108 | overflow: auto; 109 | position: relative; 110 | } 111 | 112 | @media only screen and (min-width: 960px) and (max-width: 1600px) { 113 | .content { 114 | width: 700px !important; 115 | margin: 0 auto !important; 116 | } 117 | } 118 | 119 | @media only screen and (min-width: 1600px) { 120 | .content { 121 | width: 1000px !important; 122 | margin: 0 auto !important; 123 | } 124 | } 125 | 126 | .content { 127 | width: 100%; 128 | } 129 | 130 | .content h1 { 131 | margin: 20px 0; 132 | } 133 | .content h2 { 134 | margin: 15px 0; 135 | } 136 | .content h3 { 137 | margin: 10px 0; 138 | } 139 | 140 | .block-content { 141 | margin-bottom: 2rem; 142 | } 143 | .desc-text { 144 | margin-top: 1rem; 145 | } 146 | 147 | .block-indent-1 { 148 | padding-left: 2rem; 149 | } 150 | .options-dot { 151 | margin-bottom: 1rem; 152 | } 153 | .options-dot:before { 154 | content: '● '; 155 | } 156 | .demo-wrapper { 157 | margin-left: 2rem; 158 | } 159 | 160 | pre { 161 | overflow: hidden; 162 | padding: 0 !important; 163 | border-radius: 5px; 164 | width: 100% !important; 165 | } 166 | 167 | code { 168 | border-left: 2px solid #0048ab; 169 | border-radius: 0 5px 5px 0; 170 | } 171 | -------------------------------------------------------------------------------- /docs/style/github.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | github.com style (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | color: #333; 12 | background: #f8f8f8; 13 | } 14 | 15 | .hljs-comment, 16 | .hljs-quote { 17 | color: #998; 18 | font-style: italic; 19 | } 20 | 21 | .hljs-keyword, 22 | .hljs-selector-tag, 23 | .hljs-subst { 24 | color: #333; 25 | font-weight: bold; 26 | } 27 | 28 | .hljs-number, 29 | .hljs-literal, 30 | .hljs-variable, 31 | .hljs-template-variable, 32 | .hljs-tag .hljs-attr { 33 | color: #008080; 34 | } 35 | 36 | .hljs-string, 37 | .hljs-doctag { 38 | color: #d14; 39 | } 40 | 41 | .hljs-title, 42 | .hljs-section, 43 | .hljs-selector-id { 44 | color: #900; 45 | font-weight: bold; 46 | } 47 | 48 | .hljs-subst { 49 | font-weight: normal; 50 | } 51 | 52 | .hljs-type, 53 | .hljs-class .hljs-title { 54 | color: #458; 55 | font-weight: bold; 56 | } 57 | 58 | .hljs-tag, 59 | .hljs-name, 60 | .hljs-attribute { 61 | color: #000080; 62 | font-weight: normal; 63 | } 64 | 65 | .hljs-regexp, 66 | .hljs-link { 67 | color: #009926; 68 | } 69 | 70 | .hljs-symbol, 71 | .hljs-bullet { 72 | color: #990073; 73 | } 74 | 75 | .hljs-built_in, 76 | .hljs-builtin-name { 77 | color: #0086b3; 78 | } 79 | 80 | .hljs-meta { 81 | color: #999; 82 | font-weight: bold; 83 | } 84 | 85 | .hljs-deletion { 86 | background: #fdd; 87 | } 88 | 89 | .hljs-addition { 90 | background: #dfd; 91 | } 92 | 93 | .hljs-emphasis { 94 | font-style: italic; 95 | } 96 | 97 | .hljs-strong { 98 | font-weight: bold; 99 | } 100 | -------------------------------------------------------------------------------- /docs/style/home.css: -------------------------------------------------------------------------------- 1 | @media only screen and (min-width: 600px) { 2 | .tv { 3 | width: 600px !important; 4 | height: 400px !important; 5 | } 6 | .tv-wrapper { 7 | height: 500px !important; 8 | } 9 | } 10 | 11 | .high-light { 12 | outline: none; 13 | border: 3px solid #e2ebf0; 14 | box-sizing: border-box; 15 | box-shadow: 1px 1px 5px rgba(25,26,27,20%); 16 | } 17 | 18 | /* tv style */ 19 | .tv-wrapper { 20 | height: auto; 21 | padding: 20px 0; 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | background-image: linear-gradient(120deg, #fdfbfb 0%, #ebedee 100%); 26 | } 27 | .tv { 28 | box-sizing: border-box; 29 | position: relative; 30 | width: 350px; 31 | height: 230px; 32 | display: flex; 33 | flex-direction: column; 34 | border: 0.3rem solid #777; 35 | border-bottom: 0.8rem solid #777; 36 | border-radius: 5px; 37 | color: #fff; 38 | background-image: linear-gradient(120deg, #a6c0fe 0%, #f68084 100%); 39 | } 40 | .tv:before { 41 | content: ''; 42 | position: absolute; 43 | width: 3px; 44 | border-radius: 3px; 45 | border-top: 40px solid #777; 46 | border-bottom: 0; 47 | border-left: 3px solid transparent; 48 | border-right: 3px solid transparent; 49 | height: 0; 50 | bottom: -35px; 51 | left: 10rem; 52 | transform: rotate(60deg) 53 | } 54 | .tv:after { 55 | content: ''; 56 | position: absolute; 57 | width: 3px; 58 | border-radius: 3px; 59 | border-top: 40px solid #777; 60 | border-bottom: 0; 61 | border-left: 3px solid transparent; 62 | border-right: 3px solid transparent; 63 | height: 0; 64 | bottom: -35px; 65 | right: 10rem; 66 | transform: rotate(-60deg) 67 | } 68 | .tv *:focus { 69 | outline: none; 70 | border: 3px solid #e2ebf0; 71 | box-sizing: border-box; 72 | box-shadow: 1px 1px 5px rgba(25,26,27,20%); 73 | } 74 | .header-block { 75 | display: flex; 76 | flex-direction: row; 77 | justify-content: space-between; 78 | padding: 1rem; 79 | } 80 | .status-right { 81 | color: #666; 82 | width: 8rem; 83 | display: flex; 84 | flex-direction: row; 85 | justify-content: space-around; 86 | } 87 | .circle { 88 | width: 2rem; 89 | height: 2rem; 90 | line-height: 2rem; 91 | text-align: center; 92 | background-color: #fff; 93 | border-radius: 50%; 94 | box-shadow: 2px 2px 8px rgba(25,26,27,20%); 95 | } 96 | .circle.high-light { 97 | background: #777; 98 | color: #fff; 99 | border: 0; 100 | } 101 | .circle:focus { 102 | background: #777; 103 | color: #fff; 104 | border: 0; 105 | } 106 | 107 | .block-container { 108 | display: flex; 109 | flex-direction: row; 110 | justify-content: space-between; 111 | position: relative; 112 | padding: 1rem; 113 | height: 100%; 114 | } 115 | .block { 116 | border-radius: 0.5rem; 117 | display: flex; 118 | justify-content: space-around; 119 | align-items: center; 120 | } 121 | .block-col { 122 | flex-direction: column; 123 | } 124 | .block-row { 125 | flex-direction: row; 126 | } 127 | .block1 { 128 | width: 32%; 129 | height: 100%; 130 | background-color: #f68084; 131 | } 132 | .block2 { 133 | width: 65%; 134 | height: 100%; 135 | display: flex; 136 | flex-direction: column; 137 | justify-content: space-between; 138 | } 139 | .block2-1 { 140 | height: 65%; 141 | background: #a6c0fe; 142 | } 143 | .block2-2 { 144 | height: 32%; 145 | display: flex; 146 | flex-direction: row; 147 | justify-content: space-between; 148 | } 149 | .block2-2-1 { 150 | width: 32%; 151 | height: 100%; 152 | background: #8fd3f4; 153 | } 154 | .block2-2-2 { 155 | width: 32%; 156 | height: 100%; 157 | background: #96e6a1; 158 | } 159 | .block2-2-3 { 160 | width: 32%; 161 | height: 100%; 162 | background: #30cfd0; 163 | } 164 | 165 | /* remote control style */ 166 | .control-wrapper { 167 | position: fixed; 168 | bottom: 0; 169 | left: calc(50vw - 75px); 170 | width: auto; 171 | } 172 | .remote-control { 173 | width: 150px; 174 | height: 240px; 175 | background: linear-gradient(to bottom, rgba(255, 255, 255, 0.15) 0%, rgba(0, 0, 0, 0.35) 100%), 176 | radial-gradient(at top center, rgba(255, 255, 255, 0.15) 0%, rgba(0, 0, 0, 0.60) 120%) #989898; 177 | background-blend-mode: multiply, 178 | multiply; 179 | border-radius: 20px 20px 0 0; 180 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); 181 | position: relative; 182 | display: flex; 183 | align-items: center; 184 | justify-content: space-around; 185 | flex-direction: column; 186 | } 187 | 188 | .power { 189 | margin-top: 10px; 190 | width: 40px; 191 | height: 40px; 192 | border-radius: 20px; 193 | color: rgb(160, 41, 41); 194 | text-align: center; 195 | line-height: 40px; 196 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); 197 | background-image: radial-gradient(at center, rgba(70, 70, 70, 0.2) 0%, rgba(50, 50, 50, 0.3) 100%); 198 | } 199 | .compass { 200 | width: 120px; 201 | height: 120px; 202 | position: relative; 203 | border-radius: 60px; 204 | background: radial-gradient(at center, rgba(0, 0, 0, 0.5) 0%, rgba(155, 155, 155, 0.1) 50%, rgba(0, 0, 0, 0.5) 100%); 205 | background-blend-mode: screen; 206 | display: flex; 207 | justify-content: center; 208 | align-items: center; 209 | } 210 | .arrow { 211 | color: rgba(188, 188, 188, 0.5); 212 | position: absolute; 213 | } 214 | .arrow-left { 215 | left: 5px; 216 | } 217 | .arrow-right { 218 | right: 5px; 219 | } 220 | .arrow-up { 221 | top: 5px; 222 | } 223 | .arrow-down { 224 | bottom: 5px; 225 | } 226 | .enter { 227 | width: 60px; 228 | height: 60px; 229 | border-radius: 30px; 230 | background-image:radial-gradient(at center, rgba(155, 155, 155, 0.5) 0%, rgba(0, 0, 0, 0.5) 100%) 231 | 232 | } 233 | .three-bar { 234 | width: 90%; 235 | display: flex; 236 | color: rgba(188, 188, 188, 0.5); 237 | flex-direction: row; 238 | justify-content: space-around; 239 | } 240 | .volume { 241 | height: 5px; 242 | width: 100px; 243 | border-radius: 15px; 244 | } 245 | .dot { 246 | width: 35px; 247 | height: 35px; 248 | border-radius: 15px; 249 | text-align: center; 250 | line-height: 35px; 251 | background-image: radial-gradient(at center, rgba(70, 70, 70, 0.8) 0%, rgba(50, 50, 50, 0.6) 100%); 252 | } 253 | 254 | /* content style */ 255 | .content-wrapper { 256 | background-color: #fff; 257 | /* background-color: #00A388; */ 258 | } 259 | .download-wrapper { 260 | padding: 5vh 0; 261 | opacity: 0; 262 | display: flex; 263 | flex-direction: column; 264 | align-items: center; 265 | justify-content: space-around; 266 | height: auto; 267 | } 268 | .download-wrapper * { 269 | text-align: center; 270 | } 271 | .symbol { 272 | height: 100px; 273 | margin-top: 2rem; 274 | display: flex; 275 | font-size: 1.5rem; 276 | align-items: center; 277 | justify-content: center; 278 | } 279 | .download-wrapper span { 280 | font-size: 1.5rem; 281 | margin: 50px 0 50px; 282 | } 283 | .download-button { 284 | width: 200px; 285 | height:80px; 286 | margin-bottom: 4rem; 287 | line-height: 80px; 288 | font-size: 1.5rem; 289 | cursor: pointer; 290 | border: 1px solid #000; 291 | } 292 | .download-button:hover { 293 | color: rgba(0, 0, 0, 0.5); 294 | border-color: rgba(0, 0, 0, 0.5); 295 | } 296 | 297 | @media only screen and (min-width: 600px) { 298 | .content-1 { 299 | height: 40vh; 300 | display: flex; 301 | flex-direction: row; 302 | justify-content: space-around; 303 | } 304 | .feature-block { 305 | display: flex; 306 | width: 30% !important; 307 | text-align: center !important; 308 | } 309 | .feature-desc { 310 | height: 150px !important; 311 | } 312 | } 313 | .content-1 { 314 | padding-top: 1rem; 315 | background-image: linear-gradient(120deg, #fdfbfb 0%, #ebedee 100%); 316 | } 317 | .feature-block { 318 | width: 80%; 319 | margin: 2rem auto; 320 | text-align: left; 321 | display: flex; 322 | flex-direction: column; 323 | justify-content: space-around; 324 | } 325 | 326 | .feature-desc { 327 | text-align: left; 328 | display: block; 329 | height: auto; 330 | } 331 | .feature-block a { 332 | font-size: 1.5rem; 333 | margin-bottom: 1rem; 334 | color: #0048ab; 335 | } 336 | 337 | /* nav style */ 338 | .nav-dark { 339 | background-color: #00A388; 340 | border: 0; 341 | color: #e8e8e8; 342 | } 343 | .nav-dark a { 344 | color: #e8e8e8 !important; 345 | } 346 | 347 | .nav-light { 348 | background-color: rgba(255,255,255,0.9); 349 | color: #666; 350 | } 351 | 352 | -------------------------------------------------------------------------------- /docs/style/iconfont.css: -------------------------------------------------------------------------------- 1 | 2 | @font-face {font-family: "iconfont"; 3 | src: url('iconfont.eot?t=1544200381124'); /* IE9*/ 4 | src: url('iconfont.eot?t=1544200381124#iefix') format('embedded-opentype'), /* IE6-IE8 */ 5 | url('data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAABmsAAsAAAAAJywAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADMAAABCsP6z7U9TLzIAAAE8AAAARAAAAFY8hEklY21hcAAAAYAAAAD7AAADAGq9QMBnbHlmAAACfAAAFIcAAB5kWCtmqmhlYWQAABcEAAAALwAAADYTgcd3aGhlYQAAFzQAAAAgAAAAJAfgA5ZobXR4AAAXVAAAAA8AAABYWAAAAGxvY2EAABdkAAAALgAAAC5ojl/MbWF4cAAAF5QAAAAfAAAAIAEyAUVuYW1lAAAXtAAAAUUAAAJtPlT+fXBvc3QAABj8AAAArgAAAPvAzVETeJxjYGRgYOBikGPQYWB0cfMJYeBgYGGAAJAMY05meiJQDMoDyrGAaQ4gZoOIAgCKIwNPAHicY2BkYWCcwMDKwMHUyXSGgYGhH0IzvmYwYuRgYGBiYGVmwAoC0lxTGBye8T03Zm7438AQw9zA0AAUZgTJAQDiKAwleJzdkrtuwkAQRY/BIS+HPKmJTBQBAkFiGhpq/oJ/oeG/8jF3u1TpyV0moqGgz6yO7R2NvGOfAS6AtpmYEoofCnJ8O1sc8m1uDvmSL+9rXv3UUlcDDTXWVDMttNRKa2201S710yg1+z24pj7WNCc181xzJgqfV/N2XO8nC7quarnL0t/S4ZIrrt3xLRV3PHLPA08880LPr+ucPfH/R5UvRfW362VPAfleB9m9BoH/LhoGeVY0DvK8aBrkOdIssAXUBPaBFoHNoGVgR2gV5PnSOrA3tAlyp9oGdol2ga2S+oH9kkaBTZPmgZ2TPgLbJ30GngNSE9D7BR/CX9gAeJyNWXuMJEd576+qq6rf3TPTr3nt7rx3d2Znd+e5r9u9vT2vz773ne9OmPPhx9khih8EbBwwJAZbIAuUEMDBARMpECkRCcTIiSAyUR7gRCEBHAQSCYQIHHKHFJKYP4KMfON81TN7nG0FZWa2uru66vu++up7/L5ahSjKy5+lN9Pjyi8qb1cUVm7UG/V90MO2UcGm39sknSkS+IK74EAkb5pQrjfasAlROOxuQqPfGwyTb7/XJmUHB4fRFAi8TMMUbEFniEOncHajzkUFL7RcaQNO2MTXggsH4MVW0Vd9yv0OrJcr3VsfeuyhW7uVyj7YniGNuM7soq0J1RZBls3FwMzCzmBqZuHsvW+79+zC+nBnijJOoTzrGjlW84VgtmbmOeSz9AeECJ1YddW1qVo2wTjuWlpYvYOE6U5t6eDuiYPICPkdPHHo4BK0d+hyO7BAJbarEQaQczskF/Rb8/u2Du275+zCwtl79u0+1Bzeqt1heZxSD0AFyonjckIJzgNyhljaziqnuiM6G0JTFAVQx39FP0ffoNwtNbwIUnfT0G/UUWVSN6wPqPM26qeMykDFCQhqA9Re6EIwHIRR6ADuRdkFLvdmCxqM11HtQ5Cq34SO1HcRIIwC1G5N+CIKi4A7RR6l5ILgzBa7qwKICSkNVkbPcLDNeBCbNvDRM9DRPdwbPly1m2mVWg4TITmluWxgjf4BX3HY9ZzU3MJ8yvFgF3Xi6aO/0WHIBdsHM9R2OInzPaHbxlO6oX5sG4pM+1ZqNlWrYfMtjY2e3/9R1dC/rPNO3ucsyx2NcAuK1NQ3NLb8Bx9TDeNOjUPhubAT5vL5HF6eG/0b1+40kNyHNaPLtA2dU0XB38t/TL9LV5TXQwf6ihJV0BD79bH1oSUOG1KH2AxRN/UeamZYxwYtMPQTywt4FEozRkWiUlHX0lSl8qJQmmFii1HQlcPRE9pQYW3YB2MGWyBV3RgOetdwGQyX5Q0yWsYBk9tBdzhINkTSlpz5tLzFHeFizCYMIilA0j12FS4a5XpvsA79gXS9+qLc8D162OxDlpuA3T3ZTRt1UQnFhFzA/SgI0T+vXQ2XDssnKyoNEjachtHxmmYAmPm41lyBmUW09am0Y4QW2KrKGjohZGrO0CxdM7M2EIpeV1LRxlXCVW6kNDtTY4Rw52DpQitlk7RmmuQJxvLrbOQjYe5mqq3twM/NRwBA2Gzg6L4JJgcGTK2rOsdeCmDYpg4z5Yfu8g2KtLGHCEYM37DDWYZOROL5XCbcblV9h4OpPY5stHeDarnF2mrs54+aFIVox5bm6MISBCgQDXdz/uj69JRAetSwaZ6qSMpiuqPZUVsQQs1juSBerRVdSwX4J3Z9RVCVX0AvN7WqtVadCmKDWcyrLGY8u7zKIZsp7M/PDrhpOmUKhmcCcZnQDF2Q+RmS25/ThWoZbCFvOsFaK+swM81Sg6jp+jh2QF0dF6oFfK5QjgoWpcLyUJOpzNL+0LWnexRsj+nFGwq1JaLpQFRLY46MIMReZxBXDD27VItRbG4JDWhv2nbD/UuZFNLwLEGpXYhL+TkeJPvpmgxmgmwmMlUq/BmWy60ejh0znFWpZjdLrQi4nkuDF4QMCGpEdQ1SiWl2J0e0EOeyRmg68eHVXI7N+KgWI87k0D5VMH3mIPsyQOJ/n6bfwzj2HeVHyosYy6QjLMpk0QYZ1VzC8a5fR1cZ22+9IpuJ8zSkL0aDaZi40tj+0VVkgHMheZJGKs02ChPLbaPvyiiZEJj4BFJHtoJPvGRi5XwR3Xo8LUlUyCVxBCSKz3h3lSEms0hmpcSpp8e+25kCyS6QZMYEG+U2Gac+3yEUSSMJHNnG3ZlfJAYqBszItHFLtaJhXbdCRRtUUyWqiHOhpabTlJmHraCn8YWTqqltTFdUMOitJy3CKqVN3VRPLnBtLW2eMRlNpVT7tJEaCAqO6mCe4kONRSa1b9I5O2OrVsS11ULf2W+YjZYFfPk6TwWrVbf0Dadf6Go8Z6v2YcZ1UZxKu+gZaEi8obXms8aUb6C5Gk5Ro+51y4xYy03TXKdqZwGtxhSNRto01mtz+KJTbZ/arlS2T50/LS+n2yfvJuTuk6dkewpszLn7McUywbjKUloW13WCmVoXrKyuYeIGsE/eSlEv9cq6Ye6ugCoGnnHSUlW5ujNmalWosLJrGtszE01g8qmWNzSTxJxqjjA2DKvRRj6d61yVWM0GDp1viVWNozrtmwzOTtmqGTCtLxbm1kxzXoYuXBOAxguO4LEfi6bbK4RmGiVkKlXdWOsxph+1qZMTWo+oPjFdA12YmFzrBdYxm1ovlA/IBZfL26fPnzpQht/Dxe4tPMnbH8K8/Ubl3QkyaoOoJMgHU6+8lyaTBHUHmmiT/RTG5soyvsbEjDYNSdIp8yA1wUwyvEurS2Ha6CJYaqD1CuB+mOTvIBVG3c6wM9hLODKWJxiqO4ne5BJnhX0sIOiwOoQFBDimAJ1ZAflukKeMhQXcqEcyNQPea2UDwnXDzmK4N7QaJn/AHpeO3sEEGf0qFXpCxPguohyM7nbQ4gsoMkKakN1Q5WiD99CULk3ad+/zM5j2bcQCb3Z97IG8Dx/EAASFMO3Co2lmjN7mNvKUi9FbfCdjegZQVcCv+3lKLEbhUZWS0TupKZjO0s7oHh0Rk3Yvn/ZyBFg6qN0nswKYgepyHeGSIjHpV+gn6Y6yo9yUICZSvxZf+lOk+0pMySvlJCknKX/QSTxfIlUh0y+8Gm3SDwj+osCYt5wrLJ9/4JEHzi8tnX/gAObUI3UMxJZW3qUw37pxZ//FAn4u7t/50M72xXxhKt/6MTevOJirDBAzquOotCDA6PIXNe7whY3N6w6MaSHJZR0NDaQuiAtGIX9xG6lcJXjjkd319o+5axIb+W2vCBVdoLOqJeuXcfZJ+hV6TvlX5fvKDya291osUnkVFtna08AeFpG4fD0JhEF3jAwwsnWj12CR8quxCCrsVVikDdeAEcRci4kkEn01xi+JjLJXAUkoAYlYxt/SqyHWHiopwlVU0oka5YD8BWPxujNC5AHcC+rtg1GYX4jRMoA3Q8cMEhgBnKmzqiEQMKjERBRBSrV33Q2BhBGAJommxYkZGE40zxMcsVAIot123fc4MbTPShzxO2juqanGvlxQ6DWQCuXLWVvHyIBWa6GjIBqhwoMwbL7u4AwCIlU1bVqkaC+IJlRiuJoTL0s4QRvdQpjdmJ1K2Sr8Oztck25zCuDbcAd1TZn7I9YqVrJTFuZkO62StN/biT2nNMBA7zEwpo9MNzpEMwhhFloQikKcTZ6rWlq+28hyirgdfWVQcrx4p+eniZq2MTXbU9lKsclDmfcRZ3BSivJ+1lRV4RQtli9sHMu5ZjzPKKGqs5DONzOY+4tpkjIzKAMu3uGeQWp5kj+UpxpGV/TC+dh0c8c2CnlmFR2hqkbWz8dlxP5mqLqY/jHMQuKXX6bfoHcpv6y8WVplPUnjpc5gmBm0iQxxaJjSVPt1kYDsZKMbFZmUZXgLpwkGyQGGuxRaWUXm2EYqQQXdcAzipYFFU9SFYRITBWbeSqmM03FPdIPw4jEDfhfrvAM8lS1nPmEYbKdatGKIU26awuLOYHtwA39E03QMkhoXBcOVaW6RRIUsQJSpqo+SA9315c0WKRQqdTpVZPnlh/NLaS/SdtzBYHQhWHPZXN92WEaHuR6I1rDopfU7b9jA4JhiLKfBHdv9QqQRVJo1M7q3ZGNohBvhfhlmR++/BTdT9aoz8MHZGGstdIX80sZ5MW8YeTLVPzM8gmA8vTsc/WRlC6sbEisCdfo/9G8pUTQlpeSUitJS+sq9WDRKF5SFSKMsM8SgV4/+Hz0gI8VwvC2oyxR6ZgmdLjWFgEgObEo37qfwVQk9MoWvSmOvBKPUbB5oNkupMCyH4X/h03arNTN++uFMq7V99d3IYFhtq7lDcZXCU/kqQDV/5Yi8zmTXpIP1jHofVpvkT5qrgNfRphf58+i7Z0HS3G6+RVIph/f/3MdRiyLwnLXROpE+cSd8XshWxkhcXW0Qd8zgygvyqtKUp8vYiQH0q+RflNcrF5V7FKVWkuFpb8VJMHJgHDHbY9tC5UgbloceEsRKdUiNJOBS8PIkqSRnGeNjjuRWYKn92j2iZ0bhN+o9gLUmeRqloojmME24PBH+aAEXMZ0lz+Rrwn4fLFeLw2yuVyytTbGZzf2YDVilG8bLeZZdyDSa6yd62/R9aIeweuXzkx3woqgUhhjr1hrEaa4Sstr8PlnJY/JgvLTNv1eoVgtXfpyt0mrhFhKlP0+bQDeGa5tCzGTVYrzNTJeX5zUDk22QU/XbHtHJCv9crkovoLBfeOUOKBxt80/p39Mzyq5yQfkV5TeVjyqfnGSj1+ZaibrbY1zdH6el5DADa1M5ottJnHsCtMUyKk9qFgd3kho5OXcqy0AxPiApyzyBI7ZgEzpRonEuA07yjHFEVriNybBIQqtkGxyYuMFLbzh7+uFKtVp5+PTZP5e3tVzc9phjAnPmo6g889ajxz9z4siDM+XyzINHTpzr3dXmJ7rovIZo30g7947eKEx26Jbbvv7Mjcxw2c7Z1z1787kdRo4JRhcffOhNC1gCI3LvvvOxg/s7mBrM0eMyPJHs1u5WVtamwtYynWHXRxwVFgrNQiEQmmZr2o3VKor0BRSpWk1uLxy74foNxFfIJTXcPnjo3PGjby3hRwqY3GZjVuY65hNWIvk8qZoOv74xe/QG7sjgV6tXd9iDjDQXm7OE4zu6sFTItQlzrDdjFINsNpclTNhENTJ+4Kv0fcX5Iv62AWt83ZLHU0oa9/lp+mV6UnGVvNJUFpUtRF6nlXPKLyhvUd6j/JbyCeXzyt8p38aq76cYmUoJpg33Igzl6EaN1KBb6kQS7yK2CNcB97svQXK/m5n0RfKEZtIH3bHDYc7gIkDH6gYVhpFK0u3KhJLQFaU60h2WsEock5CTEJds4sxKA9ljNqnv+XAmESCVCINQSVKWPIOKvCbyYF7q1X8mX68+HAswpjzsSxuVuOTqDClh7Wec0a4+O/pMOo7TcCqNwCS2gI0+g/GfwSkG97/VdF0TGwA3uP1245WP8O5XPo/+4/+m9EQ6NhA3sq/InPI1OSJOy9HjcXLEZPQjCDF25KDl35Zx9w8REYz+aAHnGtoWdn/JFA/gBUndLky8+3059M+wGf1j0m3QE0j40+kIU2MaTqfj0SOMwWnGbnKtByw3ad5z22hxT2x5ec/tcN+1b/8TxrKNVzJ6dG8NwObi9Bclwy+ibXvG0QmLCcN3MDb6NGOPjcW7PhF5dKUrx+8K03TN38BWfFKu6gPmYVzmX2OfKb6pGRcNbUMOw2YPK7+MyA8UU4kwe6J1yqAsg73PaUlGBjQLWQUkbSXpod986Ww6m03TT8n2mvuXDl7TBWcgrkqlY3Mmrmaz1fjKp8bXpEZB5veQrysFpagoGbQuGYJcSBiL0jimgTxAHy5/HOXrmHnfeIkcdDIZ58oXXjL8vDn6qmoY8AQmuAXDz5k/gUwhAz8xc74BC9SwUrIYwL+Xr9DHcX0fxBJOpvE6NEAet6wg1+TkY3LU5ycIXgbKKDlP4dFeTziBVlEnCsdfLJ+WO+MyAmvW4eQEda+06CUHLYvJgTS+7WM8n/TUJ0eOjSRcy69YLo8hPVINQsHlIcmg0WuMY/vkTGfCX9YgvhRsLGJy8SdAUWaKyJfPfJx6G9I1J+zkPyJ6Y2jZmNQbSfrBLunXg3ZCoCIrb8lnnMMxIlSmcDF1HlX8cO8q3b7Mv6MyzVWZaQU60XWCINg0XJdLAI5adrDZPM6jyDRU1YksTVi2qupWKqMySo9vITzFPSMaV5nuMFVSoZomobSpex6TVLAaQhoneBRa19IwzDCeUJAfFTKGh3U9Vh79A8LR9SBjz2SCkKm6y3d68iyQUdd2Y8YNBwsb6upG5Boex+oDp+xwF4Gub7uq64ahFEYc6CclUDKJM5wkBPF0I4Zs60iWqymfp4fTg0OwsmCYasrYOgpw8bEcZOM4e3NWfnpZJ3TwR6qCawJBrrADPndqjge2pqqaZwlhpgzLzqZRhJUcYMWkUaozIFSnVONYk+SHNJNzLMZSocm5jtBdFXYoZk/P/lwixNgjIkxCcleJWPaakUsHec5x4e0QdNxrpoLKXKoKLWpjMYMuY+cxImHBo7PybkkgF0T+e7PYYjLLVVW6N4mlDDPr2wUnZchJvLRb5jjJHv1otqL7FtVyBTg0aB80SSowifuG/YfvhA3HdQuue9B1nKLj3GY6TuC6MgyYSSx4ivyzEiB631GOKGeUs8otiJnuVO6SGBQRzCaRp6EJHgrl2UV3kJg8+kZn/A8c9MlNCc4n3d3Xdo+NV+yBHZhYdeNVV/IoatGhprG+79eef53wMtqp5x5YWTn3sVU0Bc3SOu/9yHuXMfm7RKx99NzS6SlBXAQC0dahrQhfe0RMnV6anp/fnJ+fNmwnY9twf15+2km7nrTk4wBmJkUsuO285rvaTTfDk3D9Ad23QVvb2lrTbF87cP3XYLam+baRn57OG9hTm30c5rbm8HernXGQNFx2PW/adZvjy4yHf2OdylrzYfqX9GHFUrqowTHUmGT6ZQTmmNuXMYCgttDt5ZlH4veIK0WoRANl2Ab4bz+P1jgrmy9Fs5B2Lr6dqM89+eRzKnn7xYznlDPv+jhhwJ99FrElruhN8mDrKT+f90cncOr7tULWi1nr6ftxBs67/+mWqNQ9Pnj+w8+OfsrkxA8/r4z/v4f7nyK+klGUIU9CVWdaHv8hlK1jdBsZnnbZbacuCdfXLlF6WXsBPF9c9rxLUnnYc0m7SidNAkmnJuf2Ejpbk9TC4aUwdUnzkIZKLsuZl50XXHkjLqnqZeEGyGWPDnk/eSKhM5bnKrGEzrsMnIJcpTxSCv1uKQ/2/FDzUdRUC7v26DxGPqL4e/JcFcVP1kfuM7RLnpfIcokQlO2XxCWvjR1yeUCxx/f+F8T+2NcAeJxjYGRgYABil0fv38bz23xl4GZhAIEbBgf3Iuj/DSyMzA1ALgcDE0gUAGFYC9AAeJxjYGRgYG7438AQw8LAwPD/PwsjA1AEBYgBAHIEBH54nGNhYGBgoQEGABAoAFkAAAAAAACyAVYDFgSOBUoFygcWB8AIaAkOCgILYAucC9ANog5MDqIOxg7qDw4PMgAAeJxjYGRgYBBjtGQQYgABJiDmAkIGhv9gPgMAEeUBeQB4nGWPTU7DMBCFX/oHpBKqqGCH5AViASj9EatuWFRq911036ZOmyqJI8et1ANwHo7ACTgC3IA78EgnmzaWx9+8eWNPANzgBx6O3y33kT1cMjtyDRe4F65TfxBukF+Em2jjVbhF/U3YxzOmwm10YXmD17hi9oR3YQ8dfAjXcI1P4Tr1L+EG+Vu4iTv8CrfQ8erCPuZeV7iNRy/2x1YvnF6p5UHFockikzm/gple75KFrdLqnGtbxCZTg6BfSVOdaVvdU+zXQ+ciFVmTqgmrOkmMyq3Z6tAFG+fyUa8XiR6EJuVYY/62xgKOcQWFJQ6MMUIYZIjK6Og7VWb0r7FDwl57Vj3N53RbFNT/c4UBAvTPXFO6stJ5Ok+BPV8bUnV0K27LnpQ0kV7NSRKyQl7WtlRC6gE2ZVeOEXpc0Yk/KGdI/wAJWm7IAAAAeJxtjVkOwjAQQ2NoUyhl387RIwUIzRSRKU1HXU4PCPjDP36SZVuN1Eep+q8DRhgjQgyNBBNMkWKGDHMssMQKa2ywxQ57HHBU2cX6ojLcUHCUXi2VdHbGF/FD/CA6ODs4ehlLb3VgCcK6Z184GQ/Gzy5CTkxHviPdSEXGRy1dKbF1a+luorv1Ep3M+ZZ8lvL4Hedxxa2ts/B+Kl+lhiXtyPywZ/nhIPxFpZ57o0DDAAA=') format('woff'), 6 | url('iconfont.ttf?t=1544200381124') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/ 7 | url('iconfont.svg?t=1544200381124#iconfont') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | .iconfont { 11 | font-family:"iconfont" !important; 12 | font-size:16px; 13 | font-style:normal; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | .icon-dengpaotishi:before { content: "\e622"; } 19 | 20 | .icon-feijichang:before { content: "\e623"; } 21 | 22 | .icon-qunzu:before { content: "\e628"; } 23 | 24 | .icon-shezhi:before { content: "\e62a"; } 25 | 26 | .icon-shouye:before { content: "\e62d"; } 27 | 28 | .icon-sousuo:before { content: "\e62f"; } 29 | 30 | .icon-yonghu:before { content: "\e633"; } 31 | 32 | .icon-zan:before { content: "\e634"; } 33 | 34 | .icon-duihuaxinxi:before { content: "\e639"; } 35 | 36 | .icon-tupian:before { content: "\e63e"; } 37 | 38 | .icon-wifi:before { content: "\e648"; } 39 | 40 | .icon-erweima:before { content: "\e65f"; } 41 | 42 | .icon-menu:before { content: "\e682"; } 43 | 44 | .icon-back:before { content: "\e60e"; } 45 | 46 | .icon-shezhi1:before { content: "\e71f"; } 47 | 48 | .icon-wifi1:before { content: "\e729"; } 49 | 50 | .icon-power:before { content: "\e687"; } 51 | 52 | .icon-shangjiantou:before { content: "\e730"; } 53 | 54 | .icon-xiajiantou:before { content: "\e731"; } 55 | 56 | .icon-youjiantou:before { content: "\e732"; } 57 | 58 | .icon-zuojiantou:before { content: "\e733"; } 59 | 60 | -------------------------------------------------------------------------------- /docs/style/mono-blue.css: -------------------------------------------------------------------------------- 1 | /* 2 | Five-color theme from a single blue hue. 3 | */ 4 | .hljs { 5 | display: block; 6 | overflow-x: auto; 7 | padding: 0.5em; 8 | background: #eaeef3; 9 | } 10 | 11 | .hljs { 12 | color: #00193a; 13 | } 14 | 15 | .hljs-keyword, 16 | .hljs-selector-tag, 17 | .hljs-title, 18 | .hljs-section, 19 | .hljs-doctag, 20 | .hljs-name, 21 | .hljs-strong { 22 | font-weight: bold; 23 | } 24 | 25 | .hljs-comment { 26 | color: #738191; 27 | } 28 | 29 | .hljs-string, 30 | .hljs-title, 31 | .hljs-section, 32 | .hljs-built_in, 33 | .hljs-literal, 34 | .hljs-type, 35 | .hljs-addition, 36 | .hljs-tag, 37 | .hljs-quote, 38 | .hljs-name, 39 | .hljs-selector-id, 40 | .hljs-selector-class { 41 | color: #0048ab; 42 | } 43 | 44 | .hljs-meta, 45 | .hljs-subst, 46 | .hljs-symbol, 47 | .hljs-regexp, 48 | .hljs-attribute, 49 | .hljs-deletion, 50 | .hljs-variable, 51 | .hljs-template-variable, 52 | .hljs-link, 53 | .hljs-bullet { 54 | color: #4c81c9; 55 | } 56 | 57 | .hljs-emphasis { 58 | font-style: italic; 59 | } 60 | -------------------------------------------------------------------------------- /docs/style/tomorrow.css: -------------------------------------------------------------------------------- 1 | /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ 2 | 3 | /* Tomorrow Comment */ 4 | .hljs-comment, 5 | .hljs-quote { 6 | color: #8e908c; 7 | } 8 | 9 | /* Tomorrow Red */ 10 | .hljs-variable, 11 | .hljs-template-variable, 12 | .hljs-tag, 13 | .hljs-name, 14 | .hljs-selector-id, 15 | .hljs-selector-class, 16 | .hljs-regexp, 17 | .hljs-deletion { 18 | color: #c82829; 19 | } 20 | 21 | /* Tomorrow Orange */ 22 | .hljs-number, 23 | .hljs-built_in, 24 | .hljs-builtin-name, 25 | .hljs-literal, 26 | .hljs-type, 27 | .hljs-params, 28 | .hljs-meta, 29 | .hljs-link { 30 | color: #f5871f; 31 | } 32 | 33 | /* Tomorrow Yellow */ 34 | .hljs-attribute { 35 | color: #eab700; 36 | } 37 | 38 | /* Tomorrow Green */ 39 | .hljs-string, 40 | .hljs-symbol, 41 | .hljs-bullet, 42 | .hljs-addition { 43 | color: #718c00; 44 | } 45 | 46 | /* Tomorrow Blue */ 47 | .hljs-title, 48 | .hljs-section { 49 | color: #4271ae; 50 | } 51 | 52 | /* Tomorrow Purple */ 53 | .hljs-keyword, 54 | .hljs-selector-tag { 55 | color: #8959a8; 56 | } 57 | 58 | .hljs { 59 | display: block; 60 | overflow-x: auto; 61 | background: white; 62 | color: #4d4d4c; 63 | padding: 0.5em; 64 | } 65 | 66 | .hljs-emphasis { 67 | font-style: italic; 68 | } 69 | 70 | .hljs-strong { 71 | font-weight: bold; 72 | } 73 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./dist/tvvm.common.min.js') 3 | } else { 4 | module.exports = require('./dist/tvvm.common.js') 5 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tvvm.js", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ansi-styles": { 8 | "version": "3.2.1", 9 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 10 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 11 | "dev": true, 12 | "requires": { 13 | "color-convert": "^1.9.0" 14 | } 15 | }, 16 | "chalk": { 17 | "version": "2.4.1", 18 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", 19 | "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", 20 | "dev": true, 21 | "requires": { 22 | "ansi-styles": "^3.2.1", 23 | "escape-string-regexp": "^1.0.5", 24 | "supports-color": "^5.3.0" 25 | } 26 | }, 27 | "color-convert": { 28 | "version": "1.9.3", 29 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 30 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 31 | "dev": true, 32 | "requires": { 33 | "color-name": "1.1.3" 34 | } 35 | }, 36 | "color-name": { 37 | "version": "1.1.3", 38 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 39 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 40 | "dev": true 41 | }, 42 | "escape-string-regexp": { 43 | "version": "1.0.5", 44 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 45 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 46 | "dev": true 47 | }, 48 | "has-flag": { 49 | "version": "3.0.0", 50 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 51 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 52 | "dev": true 53 | }, 54 | "supports-color": { 55 | "version": "5.5.0", 56 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 57 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 58 | "dev": true, 59 | "requires": { 60 | "has-flag": "^3.0.0" 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tvvm", 3 | "version": "1.0.3", 4 | "description": "A simple micro-library for agile building TV web app with no dependency", 5 | "main": "index.js", 6 | "module": "dist/tvvm.esm.js", 7 | "scripts": { 8 | "test": "jest --env=node --coverage", 9 | "test:watch": "jest --watch", 10 | "build": "node scripts/build.js", 11 | "watch": "node scripts/watch.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/zexiplus/TVM.git" 16 | }, 17 | "keywords": [ 18 | "TV", 19 | "tv", 20 | "tvvm", 21 | "tvvm.js", 22 | "television", 23 | "TV web app", 24 | "" 25 | ], 26 | "author": "float ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/zexiplus/TVM/issues" 30 | }, 31 | "homepage": "https://github.com/zexiplus/TVM#readme", 32 | "dependencies": {}, 33 | "devDependencies": { 34 | "autoprefixer": "^8.6.1", 35 | "babel-core": "^6.26.3", 36 | "babel-eslint": "^8.2.3", 37 | "babel-plugin-external-helpers": "^6.22.0", 38 | "babel-preset-env": "^1.7.0", 39 | "babel-preset-react": "^6.24.1", 40 | "bannerjs": "^1.0.5", 41 | "chalk": "^2.4.1", 42 | "classnames": "^2.2.5", 43 | "colors-cli": "^1.0.13", 44 | "eslint": "^4.19.1", 45 | "eslint-config-airbnb": "^16.1.0", 46 | "eslint-plugin-import": "^2.12.0", 47 | "eslint-plugin-jsx-a11y": "^6.0.3", 48 | "eslint-plugin-react": "^7.9.1", 49 | "gh-pages": "^1.2.0", 50 | "highlight.js": "^9.12.0", 51 | "husky": "^1.0.0-rc.8", 52 | "jest": "^22.4.4", 53 | "less": "^3.0.4", 54 | "parcel-bundler": "^1.10.3", 55 | "parcel-plugin-markdown-string": "^1.3.1", 56 | "postcss-modules": "^1.1.0", 57 | "puppeteer": "^1.5.0", 58 | "react": "^16.4.0", 59 | "react-dom": "^16.4.0", 60 | "react-markdown": "^3.3.2", 61 | "rollup": "^0.57.1", 62 | "rollup-plugin-babel": "^3.0.4", 63 | "rollup-plugin-commonjs": "^9.1.3", 64 | "rollup-plugin-node-resolve": "^3.3.0", 65 | "uglify-js": "^3.4.0", 66 | "zlib": "^1.0.5" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const rollup = require('rollup') 4 | const uglify = require('uglify-js') // minify files output like .min.js e.g 5 | const banner = require('bannerjs') 6 | const chalk = require('chalk') 7 | const log = console.log 8 | 9 | // import rollup config 10 | const { rollupInputOptions, rollupOutputOptions } = require('../config/rollup.config').buildConfig 11 | // import uglify-js config 12 | const uglifyOption = require('../config/uglify.config') 13 | 14 | async function build(rollupInputOptions, rollupOutputOptions, uglifyOpt) { 15 | const bundle = await rollup.rollup(rollupInputOptions) 16 | 17 | if (Array.isArray(rollupOutputOptions)) { 18 | rollupOutputOptions.forEach(async (option) => { 19 | let { code } = await bundle.generate(option) 20 | let minCode = `${banner.onebanner()}\n${uglify.minify(code, uglifyOpt).code}` 21 | write(option.file, code) 22 | .then(() => { 23 | if (option.minFile) { 24 | write(option.minFile, minCode) 25 | } 26 | }) 27 | }) 28 | } else { 29 | let { code } = await bundle.generate(rollupOutputOptions) 30 | let minCode = `${banner.onebanner()}\n${uglify.minify(code, uglifyOpt).code}` 31 | write(rollupOutputOptions.file, code) 32 | .then(() => { 33 | if (rollupOutputOptions.minFile) { 34 | write(rollupOutputOptions.minFile, minCode) 35 | } 36 | }) 37 | } 38 | } 39 | 40 | build(rollupInputOptions, rollupOutputOptions, uglifyOption) 41 | 42 | // write code to the disk and log each file size 43 | function write(dest, code) { 44 | return new Promise((resolve, reject) => { 45 | if (!fs.existsSync(path.dirname(dest))) { 46 | fs.mkdirSync(path.dirname(dest)) 47 | } 48 | fs.writeFile(dest, code, err => { 49 | if (err) { 50 | return reject(err) 51 | } else { 52 | log(chalk.yellow(`${path.relative(process.cwd(), dest)}`) + chalk.green(` ${getSize(code)}`)) 53 | resolve() 54 | } 55 | }) 56 | }) 57 | } 58 | 59 | function getSize(code) { 60 | return `${(code.length / 1024).toFixed(2)}kb` 61 | } 62 | 63 | -------------------------------------------------------------------------------- /scripts/watch.js: -------------------------------------------------------------------------------- 1 | const rollup = require('rollup') 2 | const chalk = require('chalk') 3 | const path = require('path') 4 | const log = console.log 5 | const { watchConfig } = require('../config/rollup.config') 6 | 7 | const watcher = rollup.watch(watchConfig) 8 | watcher.on('event', (event) => { 9 | // event.code can be one of: 10 | // START — the watcher is (re)starting 11 | // BUNDLE_START — building an individual bundle 12 | // BUNDLE_END — finished building a bundle 13 | // END — finished building all bundles 14 | // ERROR — encountered an error while bundling 15 | // FATAL — encountered an unrecoverable error 16 | if (event.code === 'BUNDLE_END') { 17 | event.output.forEach(item => { 18 | log(chalk.yellow(`bundle ${event.input} to ${item.replace(process.cwd() + path.sep, '')}`)) 19 | }) 20 | log(chalk.yellow(`duration ${event.duration}ms\n`)) 21 | } else if (event.code === 'END') { 22 | log(chalk.green('waiting for changes...')) 23 | } 24 | 25 | }) 26 | 27 | // stop watching 28 | // watcher.close(); -------------------------------------------------------------------------------- /src/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false 5 | }] 6 | ], 7 | "plugins": ["external-helpers"], 8 | "env": { 9 | "test": { 10 | "plugins": ["transform-es2015-modules-commonjs"] 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/compileUtil.js: -------------------------------------------------------------------------------- 1 | // 编译功能函数 2 | 3 | const compileUtil = { 4 | updateText(text, node, vm, expr) { 5 | // console.log('compileUtil.updateText text is', text) 6 | node && (node.textContent = text); 7 | }, 8 | // 在绑定有t-model节点的input上绑定事件, expr为t-model的表达式例如 'message.name' 9 | "t-value": function(value, node, vm, expr) { 10 | node && (node.value = value); 11 | node.addEventListener("input", e => { 12 | this.setVal(vm.$data, expr, e.target.value); 13 | }); 14 | }, 15 | 16 | "t-bind": function (value, node, vm, expr, attrname) { 17 | node && node.setAttribute(attrname, value) 18 | }, 19 | "t-if": function(value, node, vm, expr) { 20 | // const originalDisplay = node.style.display || 'block' 21 | node && (node.style.display = value ? "block" : "none"); 22 | }, 23 | "t-show": function(value, node, vm, expr) { 24 | const originalVisible = window.getComputedStyle(node); 25 | node && (node.style.visibility = value ? originalVisible : "hidden"); 26 | }, 27 | "t-class": function(value, node, vm, expr) { 28 | console.log('trigger t class') 29 | if (Array.isArray(value)) { 30 | value.forEach(item => { 31 | node.classList.add(item); 32 | }); 33 | } else if ({}.toString.call(value) === "[object Object]") { 34 | // node.classList = []; 35 | Object.keys(value).forEach(classname => { 36 | if (value[classname]) { 37 | node.classList.add(classname); 38 | } else { 39 | node.classList.remove(classname); 40 | } 41 | }); 42 | } else { 43 | console.warn("t-class must receive an array or object"); 44 | } 45 | }, 46 | "t-for": function(value, node, vm, expr, attrname, originalExpr) { 47 | // 截取 in 后的数组表达式 48 | const startIndex = originalExpr.indexOf("in") + 3; 49 | const arrNamePrefix = originalExpr.slice(startIndex) 50 | const arrName = arrNamePrefix.split('.').slice(1).join('.'); 51 | const itemName = originalExpr.slice(0, startIndex - 4); 52 | const arr = this.getVal(vm.$data, arrName); 53 | const reg = /\{\{([^}]+)\}\}/g; 54 | if (!Array.isArray(arr)) { 55 | return console.warn("t-for value must be an array"); 56 | } 57 | const parentElement = node.parentElement; 58 | parentElement.removeChild(node); 59 | const baseNode = node.cloneNode(true); 60 | baseNode.setAttribute("t-scope", arrNamePrefix); 61 | baseNode.setAttribute("t-itemname", itemName); 62 | baseNode.removeAttribute("t-for"); 63 | baseNode.setAttribute("t-index", 0); 64 | baseNode.setAttribute("is-t-for", "true"); 65 | arr.forEach((item, index) => { 66 | let cloneNode = baseNode.cloneNode(true); 67 | cloneNode.setAttribute("t-index", index); 68 | if (cloneNode.textContent) { 69 | let match = cloneNode.textContent.match(/\{\{([^}]+)\}\}/)[1]; 70 | let execFn = new Function("item", `return ${match}`); 71 | let result = execFn(item); 72 | cloneNode.textContent = cloneNode.textContent.replace(reg, result); 73 | } 74 | parentElement.appendChild(cloneNode); 75 | }); 76 | }, 77 | 78 | // 解析vm.data上的t-model绑定的值 79 | setVal(obj, expr, value) { 80 | let arr = expr.split("."); 81 | arr.reduce((prev, next) => { 82 | if (arr.indexOf(next) == arr.length - 1) { 83 | prev[next] = value; 84 | } else { 85 | return prev[next]; 86 | } 87 | }, obj); 88 | }, 89 | // 解析vm.$data 上的 例如 'member.id'属性 90 | getVal(obj, expr) { 91 | let arr = expr.split("."); 92 | return arr.reduce((prev, next) => { 93 | return prev[next]; 94 | }, obj); 95 | } 96 | }; 97 | 98 | export default compileUtil; 99 | -------------------------------------------------------------------------------- /src/compiler.js: -------------------------------------------------------------------------------- 1 | import Watcher from "./watcher"; 2 | import compileUtil from "./compileUtil"; 3 | 4 | const privateDirectives = ["is-t-for", "t-index", "t-scope", "t-itemname"]; 5 | const REG_STRING_MAP = { 6 | T_BIND: "(^t-bind:|^:)", 7 | EVENT_BIND: "^@", 8 | DATA_BIND: "{{([^}]+)}}", // 数据绑定 e.g {{expression}} 9 | JAVASCRIPT_VARIABLE: "^[a-zA-Z_]+[a-zA-Z_d]*", // javascript合法变量名 e.g name , age... 10 | JAVASCRIPT_EXPRESSION: "(^[a-zA-Z_]+[a-zA-Z_d]*)(.[a-zA-Z_]+[a-zA-Zd]*)*" // javascript属性值表达式 e.g person.info.age 11 | }; 12 | 13 | class Compiler { 14 | constructor(el, vm) { 15 | // 把dom节点挂载在Complier实例上 16 | this.el = this.getDOM(el); 17 | // 把mvvm实例挂载在complier实例上 18 | this.vm = vm; 19 | // debugger 20 | if (this.el) { 21 | // 如果存在再编译成文档片段 22 | // 编译解析出相应的指令 如 t-text, t-model, {{}} 23 | // 保存原有dom节点到fragment文档片段, 并做替换 24 | 25 | // 转化为文档片段并存到内存中去 26 | let fragment = this.toFragment(this.el); 27 | 28 | // 编译节点 29 | this.compile(fragment); 30 | 31 | // 把编译后的文档片段重新添加到document中 32 | this.el.appendChild(fragment); 33 | } else { 34 | // 没有找到el根节点给出警告 35 | console.error(`can not find element named ${el}`); 36 | } 37 | this.vm.$el = this.el; 38 | } 39 | 40 | // 编译节点,如果子节点是node节点, 递归调用自身和compileNode, 如果不是 则调用 compileText 41 | compile(parentNode) { 42 | let childNodes = parentNode.childNodes; 43 | childNodes.forEach((node, index) => { 44 | // 不编译code代码节点 45 | if (node.tagName === "CODE") return; 46 | if (this.isElement(node)) { 47 | this.compile(node); 48 | this.compileNode(node); 49 | } else if (this.isText(node)) { 50 | this.compileText(node); 51 | } 52 | }); 53 | } 54 | 55 | // 编译文本节点, 待优化 56 | compileText(node) { 57 | // 测试文本节点含有 {{val}} 的 regexp 58 | let reg = /\{\{([^}]+)\}\}/; 59 | // 拿到文本节点的文本值 60 | let text = node.textContent; 61 | if (reg.test(text)) { 62 | // 去掉{{}} 保留 value 63 | if ( 64 | node.parentElement.getAttribute("t-for") || 65 | node.parentElement.getAttribute("is-t-for") 66 | ) { 67 | 68 | } else { 69 | // 捕获{{expr}} 双花括号中的表达式 70 | let expr = text.match(reg)[1]; 71 | // 捕获data的属性表达式 72 | let dataAttrReg = /data(\.[a-zA-Z_]+[a-zA-Z_\d]*)+(\(\))*/g; 73 | let watcherList = expr.match(dataAttrReg); 74 | let methodReg = /\.([a-zA-Z_]+[a-zA-Z_\d])+(\(\))/; 75 | 76 | // 例如取出{{message}} 中的 message, 交给compileUtil.updateText 方法去查找vm.data的值并替换到节点 77 | // let textValue = this.getValue(attrName, this.vm.$data); 78 | let execFn = new Function("data", `return ${expr}`); 79 | let data = this.vm.$data; 80 | let value = execFn(data); 81 | compileUtil.updateText(value, node, this.vm); 82 | 83 | // 给每个attribute上设置watcher 84 | watcherList = watcherList.map(item => { 85 | let attr = item.replace(methodReg, ""); 86 | attr = attr 87 | .split(".") 88 | .slice(1) 89 | .join("."); 90 | new Watcher(this.vm, attr, expr, null, function(value) { 91 | let expr = this.expr; 92 | let execFn = new Function("data", `return ${expr}`); 93 | let data = this.vm.$data; 94 | let val = execFn(data); 95 | compileUtil.updateText(val, node, this.vm); 96 | }); 97 | return attr; 98 | }); 99 | } 100 | } 101 | } 102 | 103 | // 传入表达式, 获得属性值 104 | getValue(expr, base) { 105 | // 传入 expr 形如 'group.member.name', 找到$base上对应的属性值并返回 106 | let arr = expr && expr.split("."); 107 | let ret = arr.reduce((prev, next) => { 108 | return prev[next]; 109 | }, base); 110 | return ret; 111 | } 112 | 113 | // 编译node节点 分析t指令, 待优化 114 | compileNode(node) { 115 | let attrs = node.getAttributeNames(); 116 | // 把t-指令(不包括t-index)属性存到一个数组中 117 | let directiveAttrs = attrs.filter(attrname => { 118 | return this.isDirective(attrname) && !this.isTFocus(attrname); 119 | }); 120 | 121 | directiveAttrs.forEach(item => { 122 | if (item === 't-itemname' || item === 'is-t-for') return 123 | let originalExpr = node.getAttribute(item); // 属性值 124 | let expr = originalExpr.split(".").slice(1).join("."); 125 | 126 | // t-bind logic 127 | let bindAttrName = null 128 | if (this.isTBind(item)) { 129 | let startIndex = item.indexOf(':') + 1; 130 | bindAttrName = item.slice(startIndex) 131 | item = 't-bind' 132 | } 133 | new Watcher(this.vm, expr, originalExpr, bindAttrName, (value, bindAttrName, originalExpr) => { 134 | compileUtil[item](value, node, this.vm, expr, bindAttrName, originalExpr); 135 | }); 136 | // debugger 137 | var value = this.getValue(expr, this.vm.$data); 138 | if (compileUtil[item]) { 139 | compileUtil[item](value, node, this.vm, expr, bindAttrName, originalExpr); 140 | } else if (!this.isPrivateDirective(item) && !this.isEventBinding(item)) { 141 | console.warn(`can't find directive ${item}`); 142 | } 143 | }); 144 | 145 | // 焦点记录逻辑 146 | if (attrs.includes("t-index")) { 147 | let focusIndex = node.getAttribute("t-index"); 148 | node.setAttribute("tabindex", this.vm.focuser.tid); 149 | this.vm.focuser.addFocusMap(focusIndex, node); 150 | } 151 | 152 | // @event 事件绑定逻辑 153 | let eventBindAttrs = attrs.filter(this.isEventBinding); 154 | eventBindAttrs.forEach(item => { 155 | let expr = node.getAttribute(item); 156 | let eventName = item.slice(1); 157 | let reg = /\(([^)]+)\)/; 158 | let hasParams = reg.test(expr); 159 | let fnName = expr.replace(reg, ""); 160 | let fn = this.getValue(fnName, this.vm); 161 | 162 | if (node.getAttribute("is-t-for")) { 163 | // 是 t-for 循环生成的列表, 则事件绑定在父元素上 164 | let parentElement = node.parentElement; 165 | parentElement.addEventListener(eventName, event => { 166 | if (event.target.getAttribute("is-t-for")) { 167 | if (hasParams) { 168 | let params = expr 169 | .match(reg)[1] 170 | .split(",") 171 | .map(item => { 172 | return this.getValue(item.trim(), this.vm.$data); 173 | }); 174 | // 取到 事件回调函数 的参数值 175 | 176 | let scope = event.target.getAttribute("t-scope") 177 | let arrName = scope.split('.').slice(1).join('.') 178 | let param = this.getValue( 179 | arrName, 180 | this.vm.$data 181 | )[event.target.getAttribute("t-index")]; 182 | fn.call(this.vm, param); 183 | } else { 184 | fn.call(this.vm); 185 | } 186 | } 187 | }); 188 | } else { 189 | // debugger 190 | // 非 t-for循环生成的元素 191 | if (hasParams) { 192 | // fn含有参数 193 | let params = expr 194 | .match(reg)[1] 195 | .split(",") 196 | .map(item => { 197 | return this.getValue(item.trim(), this.vm.$data); 198 | }); 199 | node.addEventListener(eventName, fn.bind(this.vm, ...params)); 200 | } else { 201 | // fn不含参数 202 | node.addEventListener(eventName, fn.bind(this.vm)); 203 | } 204 | } 205 | }); 206 | } 207 | 208 | isPrivateDirective(text) { 209 | return privateDirectives.includes(text); 210 | } 211 | 212 | // 判断是否是事件绑定 213 | isEventBinding(text) { 214 | const reg = /^@/; 215 | return reg.test(text); 216 | } 217 | 218 | // 判断节点属性是否是t指令 219 | isDirective(attrname) { 220 | return attrname.includes("t-") || attrname.indexOf(":") === 0; 221 | } 222 | 223 | // 判断是否是t-index 224 | isTFocus(attrname) { 225 | return attrname === "t-index"; 226 | } 227 | 228 | isTFor(attrname) { 229 | return attrname === "t-for"; 230 | } 231 | 232 | isTBind(attrname) { 233 | return /(^t-bind:|^:)/.test(attrname); 234 | } 235 | 236 | // 根据传入的值, 如果是dom节点直接返回, 如果是选择器, 则返回相应的dom节点 237 | getDOM(el) { 238 | if (this.isElement(el)) { 239 | return el; 240 | } else { 241 | return document.querySelector(el) || null; 242 | } 243 | } 244 | 245 | // 判断dom类型, 1 为元素, 2 是属性, 3是文本, 9是文档, 11是文档片段 246 | isElement(el) { 247 | return el.nodeType === 1; 248 | } 249 | 250 | isText(el) { 251 | return el.nodeType === 3; 252 | } 253 | 254 | // 把el dom节点转换为fragment保存在内存中并返回 255 | toFragment(el) { 256 | let fragment = document.createDocumentFragment(); 257 | let firstChild; 258 | // 循环把el的首个子元素推入fragment中 259 | while ((firstChild = el.firstChild)) { 260 | // 把原始 el dom节点的所有子元素增加到文档片段中并移除原 el dom节点的所有子元素 261 | fragment.appendChild(firstChild); 262 | } 263 | return fragment; 264 | } 265 | } 266 | 267 | export default Compiler; 268 | -------------------------------------------------------------------------------- /src/compiler_backup.js: -------------------------------------------------------------------------------- 1 | import Watcher from "./watcher"; 2 | import compileUtil from "./compileUtil"; 3 | 4 | const privateDirectives = ['is-t-for', 't-index', 't-scope', 't-itemname']; 5 | const REG_STRING_MAP = { 6 | 'T_BIND': '(^t-bind:|^:)', 7 | 'EVENT_BIND': '^@', 8 | 'DATA_BIND': '\{\{([^}]+)\}\}', // 数据绑定 e.g {{expression}} 9 | 'JAVASCRIPT_VARIABLE': '^[a-zA-Z_]+[a-zA-Z_\d]*', // javascript合法变量名 e.g name , age... 10 | 'JAVASCRIPT_EXPRESSION': '(^[a-zA-Z_]+[a-zA-Z_\d]*)(\.[a-zA-Z_]+[a-zA-Z\d]*)*', // javascript属性值表达式 e.g person.info.age 11 | } 12 | 13 | class Compiler { 14 | constructor(el, vm) { 15 | // 把dom节点挂载在Complier实例上 16 | this.el = this.getDOM(el); 17 | // 把mvvm实例挂载在complier实例上 18 | this.vm = vm; 19 | // debugger 20 | if (this.el) { 21 | // 如果存在再编译成文档片段 22 | // 编译解析出相应的指令 如 t-text, t-model, {{}} 23 | // 保存原有dom节点到fragment文档片段, 并做替换 24 | 25 | // 转化为文档片段并存到内存中去 26 | let fragment = this.toFragment(this.el); 27 | 28 | // 编译节点 29 | this.compile(fragment); 30 | 31 | // 把编译后的文档片段重新添加到document中 32 | this.el.appendChild(fragment); 33 | } else { 34 | // 没有找到el根节点给出警告 35 | console.error(`can not find element named ${el}`); 36 | } 37 | this.vm.$el = this.el 38 | } 39 | 40 | // 编译节点,如果子节点是node节点, 递归调用自身和compileNode, 如果不是 则调用 compileText 41 | compile(parentNode) { 42 | let childNodes = parentNode.childNodes; 43 | childNodes.forEach((node, index) => { 44 | // 不编译code代码节点 45 | if (node.tagName === 'CODE') return 46 | if (this.isElement(node)) { 47 | this.compile(node); 48 | this.compileNode(node); 49 | } else if (this.isText(node)) { 50 | this.compileText(node); 51 | } 52 | }); 53 | } 54 | 55 | // 编译文本节点, 待优化 56 | compileText(node) { 57 | // 测试文本节点含有 {{val}} 的 regexp 58 | let reg = /\{\{([^}]+)\}\}/g; 59 | // 拿到文本节点的文本值 60 | let text = node.textContent; 61 | if (reg.test(text)) { 62 | // 去掉{{}} 保留 value 63 | if (node.parentElement.getAttribute("t-for") || node.parentElement.getAttribute("is-t-for")) { 64 | 65 | } else { 66 | // 非t-for循环的替换逻辑 67 | let attrName = text.replace(reg, (...args) => { 68 | // 对每个{{}}之类的表达式增加增加一个watcher,参数为vm实例, expr表达式, 更新回调函数 69 | let expr = args[1] 70 | new Watcher(this.vm, args[1], value => { 71 | compileUtil.updateText(value, node, this.vm); 72 | }); 73 | return args[1]; 74 | }); 75 | // 例如取出{{message}} 中的 message, 交给compileUtil.updateText 方法去查找vm.data的值并替换到节点 76 | let textValue = this.getData(attrName, this.vm.$data); 77 | compileUtil.updateText(textValue, node, this.vm); 78 | } 79 | } 80 | } 81 | 82 | // 传入表达式, 获得属性值 83 | getData(expr, data) { 84 | // 传入 expr 形如 'group.member.name', 找到$data上对应的属性值并返回 85 | let arr = expr && expr.split("."); 86 | let ret = arr.reduce((prev, next) => { 87 | return prev[next]; 88 | }, data); 89 | return ret; 90 | } 91 | 92 | // 编译node节点 分析t指令, 待优化 93 | compileNode(node) { 94 | let attrs = node.getAttributeNames(); 95 | // 把t-指令(不包括t-index)属性存到一个数组中 96 | let directiveAttrs = attrs.filter((attrname) => { 97 | return this.isDirective(attrname) && !this.isTFocus(attrname) 98 | }); 99 | 100 | directiveAttrs.forEach(item => { 101 | let expr = node.getAttribute(item); // 属性值 102 | let value = this.getData(expr, this.vm.$data); 103 | if (compileUtil[item]) { 104 | compileUtil[item](value, node, this.vm, expr); 105 | } else if (!this.isPrivateDirective(item) && !this.isEventBinding(item)) { 106 | console.warn(`can't find directive ${item}`); 107 | } 108 | }); 109 | 110 | // 焦点记录逻辑 111 | if (attrs.includes('t-index')) { 112 | let focusIndex = node.getAttribute('t-index') 113 | node.setAttribute('tabindex', this.vm.focuser.tid) 114 | this.vm.focuser.addFocusMap(focusIndex, node) 115 | } 116 | 117 | // @event 事件绑定逻辑 118 | let eventBindAttrs = attrs.filter(this.isEventBinding); 119 | eventBindAttrs.forEach(item => { 120 | let expr = node.getAttribute(item) 121 | let eventName = item.slice(1) 122 | let reg = /\(([^)]+)\)/ 123 | let hasParams = reg.test(expr) 124 | let fnName = expr.replace(reg, '') 125 | let fn = this.getData(fnName, this.vm.methods) 126 | 127 | if (node.getAttribute('is-t-for')) { // 是 t-for 循环生成的列表, 则事件绑定在父元素上 128 | let parentElement = node.parentElement 129 | parentElement.addEventListener(eventName, (event) => { 130 | if (event.target.getAttribute('is-t-for')) { 131 | if (hasParams) { 132 | let params = expr.match(reg)[1].split(',').map(item => { 133 | return this.getData(item.trim(), this.vm.$data) 134 | }) 135 | // 取到 事件回调函数 的参数值 136 | let param = this.getData(event.target.getAttribute('t-scope'), this.vm.$data)[event.target.getAttribute('t-index')] 137 | fn.call(this.vm, param) 138 | } else { 139 | fn.call(this.vm) 140 | } 141 | } 142 | }) 143 | } else { // 非 t-for循环生成的元素 144 | if (hasParams) { // fn含有参数 145 | let params = expr.match(reg)[1].split(',').map(item => { 146 | return this.getData(item.trim(), this.vm.$data) 147 | }) 148 | node.addEventListener(eventName, fn.bind(this.vm, ...params)) 149 | } else { // fn不含参数 150 | node.addEventListener(eventName, fn.bind(this.vm)) 151 | } 152 | } 153 | }); 154 | } 155 | 156 | isPrivateDirective(text) { 157 | return privateDirectives.includes(text); 158 | } 159 | 160 | // 判断是否是事件绑定 161 | isEventBinding(text) { 162 | const reg = /^@/; 163 | return reg.test(text); 164 | } 165 | 166 | // 判断节点属性是否是t指令 167 | isDirective(attrname) { 168 | return attrname.includes("t-") || attrname.indexOf(':') === 0; 169 | } 170 | 171 | // 判断是否是t-index 172 | isTFocus(attrname) { 173 | return attrname === 't-index' 174 | } 175 | 176 | isTBind(attrname) { 177 | return /(^t-bind:|^:)/.test(attrname) 178 | } 179 | 180 | // 根据传入的值, 如果是dom节点直接返回, 如果是选择器, 则返回相应的dom节点 181 | getDOM(el) { 182 | if (this.isElement(el)) { 183 | return el; 184 | } else { 185 | return document.querySelector(el) || null; 186 | } 187 | } 188 | 189 | // 判断dom类型, 1 为元素, 2 是属性, 3是文本, 9是文档, 11是文档片段 190 | isElement(el) { 191 | return el.nodeType === 1; 192 | } 193 | 194 | isText(el) { 195 | return el.nodeType === 3; 196 | } 197 | 198 | // 把el dom节点转换为fragment保存在内存中并返回 199 | toFragment(el) { 200 | let fragment = document.createDocumentFragment(); 201 | let firstChild; 202 | // 循环把el的首个子元素推入fragment中 203 | while ((firstChild = el.firstChild)) { 204 | // 把原始 el dom节点的所有子元素增加到文档片段中并移除原 el dom节点的所有子元素 205 | fragment.appendChild(firstChild); 206 | } 207 | return fragment; 208 | } 209 | } 210 | 211 | export default Compiler; 212 | -------------------------------------------------------------------------------- /src/dep.js: -------------------------------------------------------------------------------- 1 | class Dep { 2 | constructor() { 3 | this.subs = []; 4 | } 5 | addSubs(watcher) { 6 | this.subs.push(watcher); // add subscribers 7 | } 8 | notify() { 9 | this.subs.forEach(watcher => watcher.update()); 10 | } 11 | } 12 | 13 | export default Dep; 14 | -------------------------------------------------------------------------------- /src/focuser.js: -------------------------------------------------------------------------------- 1 | const blankFn = function(event, node, index, prevNode) {}; 2 | const defaultFocusOptions = { 3 | circle: { 4 | horizontal: false, 5 | vertical: false 6 | }, 7 | keysMap: { 8 | up: { 9 | codes: [38, 104], 10 | handler: blankFn 11 | }, 12 | down: { 13 | codes: [40, 98], 14 | handler: blankFn 15 | }, 16 | left: { 17 | codes: [37, 100], 18 | handler: blankFn 19 | }, 20 | right: { 21 | codes: [39, 102], 22 | handler: blankFn 23 | }, 24 | enter: { 25 | codes: [13, 32], 26 | handler: blankFn 27 | }, 28 | space: { 29 | codes: [32], 30 | handler: blankFn 31 | }, 32 | home: { 33 | codes: [36], 34 | handler: blankFn 35 | }, 36 | menu: { 37 | codes: [18], 38 | handler: blankFn 39 | }, 40 | return: { 41 | codes: [27], 42 | handler: blankFn 43 | }, 44 | addVolume: { 45 | codes: [175], 46 | handler: blankFn 47 | }, 48 | subVolume: { 49 | codes: [174], 50 | handler: blankFn 51 | }, 52 | shutdown: { 53 | codes: [71], 54 | handler: blankFn 55 | } 56 | }, 57 | keysMapMergeCoverage: false 58 | }; 59 | 60 | class Focuser { 61 | constructor(vm, options) { 62 | this.tid = 0; 63 | this.init(vm, options); 64 | this.bindKeyEvent(); 65 | } 66 | 67 | init(vm, options) { 68 | // 存放indexString索引的node节点 69 | this.focusElementMap = {}; 70 | // 索引转化后的数组,例如[[0,1,2], [0,2]] 用于边界判断 71 | this.indexMap = []; 72 | // 存放原始focus相关参数 73 | this.focusOptions = Object.assign({}, defaultFocusOptions, options.focus); 74 | let currentRowIndex, currentColIndex; 75 | if (this.focusOptions.defaultFocusIndex) { 76 | let IndexArr = 77 | options.focus && options.focus.defaultFocusIndex.split("-"); 78 | currentRowIndex = Number(IndexArr[0]); 79 | currentColIndex = Number(IndexArr[1]); 80 | } 81 | // 存放当前状态信息 82 | this.focusState = { 83 | currentIndexString: 84 | (options.focus && options.focus.defaultFocusIndex) || "", 85 | currentRowIndex, 86 | currentColIndex 87 | }; 88 | 89 | this.keysMap = this.focusOptions.keysMap; 90 | // 合并键盘绑定键值码 91 | if (options.focus && options.focus.keysMap) { 92 | if (this.focusOptions.keysMapMergeCoverage) { 93 | // options.focus.keysMap 覆盖默认keysMap 94 | this.keysMap = Object.assign({}, this.keysMap, options.focus.keysMap); 95 | } else { 96 | // options.focus.keysMap 合并 默认keysmap 97 | Object.keys(this.keysMap).forEach(key => { 98 | // debugger 99 | if (defaultFocusOptions.keysMap[key]) { 100 | this.keysMap[key].codes = options.focus.keysMap[key].codes 101 | ? [ 102 | ...new Set( 103 | defaultFocusOptions.keysMap[key].codes.concat( 104 | options.focus.keysMap[key].codes 105 | ) 106 | ) 107 | ] 108 | : this.keysMap[key].codes; 109 | } else { 110 | this.keysMap[key].codes = options.focus.keysMap[key].codes; 111 | } 112 | }); 113 | } 114 | } 115 | 116 | vm.focuser = this; 117 | this.vm = vm; 118 | } 119 | 120 | // 传入键值码并执行相应的操作 121 | execCommand(event) { 122 | Object.keys(this.keysMap).forEach(key => { 123 | if (this.keysMap[key].codes.includes(event.keyCode)) { 124 | this.move(key, event); 125 | } 126 | }); 127 | } 128 | 129 | // 绑定键盘事件 130 | bindKeyEvent() { 131 | window.addEventListener("keydown", this.keyDownHandler.bind(this)); 132 | } 133 | 134 | keyDownHandler(event) { 135 | this.execCommand(event); 136 | } 137 | 138 | // 把有t-focus指令的node节点储存起来 139 | addFocusMap(key, node) { 140 | this.tid++; 141 | let keys = key.split(/,\s*/); 142 | keys.forEach(item => { 143 | if (item in this.focusElementMap) { 144 | return console.warn( 145 | `t-focus should be unique in one TVVM page but t-focus=${item} has already exist` 146 | ); 147 | } 148 | this.focusElementMap[item] = node; 149 | }); 150 | } 151 | // 设置焦点dom 152 | setFocus(index) { 153 | if (index in this.focusElementMap) { 154 | let arr = index.split("-"); 155 | let currentRowIndex = Number(arr[0]); 156 | let currentColIndex = Number(arr[1]); 157 | let el = this.focusElementMap[index]; 158 | if (el.getAttribute("real-focus") === "true") { 159 | el.focus(); 160 | } else { 161 | let activeClass = this.focusOptions.activeClass; 162 | el.classList.add(activeClass); 163 | } 164 | this.focusState.currentIndexString = index; 165 | this.focusState.currentFocusElement = this.focusElementMap[index]; 166 | this.focusState.currentRowIndex = currentRowIndex; 167 | this.focusState.currentColIndex = currentColIndex; 168 | } else { 169 | // console.warn(`can't find t-focus ${index} node`) 170 | } 171 | } 172 | 173 | removeFocus(index) { 174 | if (index in this.focusElementMap) { 175 | let el = this.focusElementMap[index]; 176 | if (el.getAttribute("real-focus") === "true") { 177 | el.blur(); 178 | } else { 179 | let activeClass = this.focusOptions.activeClass; 180 | el.classList.remove(activeClass); 181 | } 182 | } 183 | } 184 | 185 | generateIndexMap() { 186 | // 0-0, 0-1, 187 | Object.keys(this.focusElementMap).forEach(key => { 188 | let keyArr = key.split("-"); 189 | let rowIndex = keyArr[0]; 190 | let colIndex = keyArr[1]; 191 | if (this.indexMap[rowIndex] === undefined) { 192 | this.indexMap[rowIndex] = [colIndex]; 193 | } else { 194 | this.indexMap[rowIndex].push(colIndex); 195 | } 196 | }); 197 | this.indexMap = this.indexMap.map(item => item.sort((a, b) => a - b)); 198 | 199 | if (this.focusOptions.defaultFocusIndex !== undefined) { 200 | this.setFocus(this.focusOptions.defaultFocusIndex); 201 | } else { 202 | if (this.indexMap.length !== 0) { 203 | this.setFocus([0, this.indexMap[0][0]].join("-")); 204 | } else { 205 | window.removeEventListener("keydown", this.keyDownHandler); 206 | } 207 | } 208 | } 209 | 210 | isBoundary() {} 211 | 212 | // 焦点处于顶部边界判断 213 | isTopBoundary() { 214 | let rowIndex = this.focusState.currentRowIndex; 215 | let colIndex = this.focusState.currentColIndex; 216 | if (rowIndex === 0) { 217 | return true; 218 | } 219 | rowIndex--; 220 | let indexString = [rowIndex, colIndex].join("-"); 221 | while ( 222 | this.focusElementMap[indexString] && 223 | this.focusElementMap[this.focusState.currentIndexString] === 224 | this.focusElementMap[indexString] 225 | ) { 226 | rowIndex--; 227 | indexString = [rowIndex, colIndex].join("-"); 228 | } 229 | rowIndex++; 230 | if (rowIndex <= 0) { 231 | return true; 232 | } else { 233 | return false; 234 | } 235 | } 236 | 237 | isLeftBoundary() { 238 | let rowIndex = this.focusState.currentRowIndex; 239 | let colIndex = this.focusState.currentColIndex; 240 | if (colIndex === this.indexMap[rowIndex][0]) { 241 | return true; 242 | } 243 | colIndex--; 244 | let indexString = [rowIndex, colIndex].join("-"); 245 | while ( 246 | this.focusElementMap[indexString] && 247 | this.focusElementMap[this.focusState.currentIndexString] === 248 | this.focusElementMap[indexString] 249 | ) { 250 | colIndex--; 251 | indexString = [rowIndex, colIndex].join("-"); 252 | } 253 | colIndex++; 254 | if (colIndex > this.indexMap[rowIndex][0]) { 255 | return false; 256 | } else { 257 | return true; 258 | } 259 | } 260 | 261 | isRightBoundary() { 262 | let rowIndex = this.focusState.currentRowIndex; 263 | let colIndex = this.focusState.currentColIndex; 264 | if ( 265 | colIndex === this.indexMap[rowIndex][this.indexMap[rowIndex].length - 1] 266 | ) { 267 | return true; 268 | } 269 | colIndex++; 270 | let indexString = [rowIndex, colIndex].join("-"); 271 | while ( 272 | this.focusElementMap[indexString] && 273 | this.focusElementMap[this.focusState.currentIndexString] === 274 | this.focusElementMap[indexString] 275 | ) { 276 | colIndex++; 277 | indexString = [rowIndex, colIndex].join("-"); 278 | } 279 | colIndex--; 280 | if ( 281 | colIndex < this.indexMap[rowIndex][this.indexMap[rowIndex].length - 1] 282 | ) { 283 | return false; 284 | } else { 285 | return true; 286 | } 287 | } 288 | 289 | isBottomBoundary() { 290 | let rowIndex = this.focusState.currentRowIndex; 291 | let colIndex = this.focusState.currentColIndex; 292 | if (rowIndex === this.indexMap.length - 1) { 293 | return true; 294 | } 295 | rowIndex++; 296 | let indexString = [rowIndex, colIndex].join("-"); 297 | while ( 298 | this.focusElementMap[indexString] && 299 | this.focusElementMap[this.focusState.currentIndexString] === 300 | this.focusElementMap[indexString] 301 | ) { 302 | rowIndex++; 303 | indexString = [rowIndex, colIndex].join("-"); 304 | } 305 | rowIndex--; 306 | if (rowIndex >= this.indexMap.length - 1) { 307 | return true; 308 | } else { 309 | return false; 310 | } 311 | } 312 | 313 | moveUp(event, node, index) { 314 | this.keysMap["up"].handler && 315 | this.keysMap["up"].handler(event, node, index); 316 | if (this.isTopBoundary()) { 317 | if (this.focusOptions.circle.vertical) { 318 | this.removeFocus(index); 319 | let rowIndex = this.indexMap.length - 1; 320 | let colIndex = this.focusState.currentColIndex; 321 | let indexString = [rowIndex, colIndex].join("-"); 322 | this.setFocus(indexString); 323 | } 324 | } else { 325 | this.removeFocus(index); 326 | let rowIndex = this.focusState.currentRowIndex - 1; 327 | let colIndex = this.focusState.currentColIndex; 328 | let indexString = [rowIndex, colIndex].join("-"); 329 | while ( 330 | this.focusElementMap[this.focusState.currentIndexString] === 331 | this.focusElementMap[indexString] 332 | ) { 333 | rowIndex--; 334 | indexString = [rowIndex, colIndex].join("-"); 335 | } 336 | indexString = [rowIndex, colIndex].join("-"); 337 | this.setFocus(indexString); 338 | } 339 | } 340 | 341 | moveDown(event, node, index) { 342 | this.keysMap["down"].handler && 343 | this.keysMap["down"].handler(event, node, index); 344 | if (this.isBottomBoundary()) { 345 | if (this.focusOptions.circle.vertical) { 346 | this.removeFocus(index); 347 | let rowIndex = 0; 348 | let colIndex = this.focusState.currentColIndex; 349 | let indexString = [rowIndex, colIndex].join("-"); 350 | this.setFocus(indexString); 351 | } 352 | } else { 353 | this.removeFocus(index); 354 | let rowIndex = this.focusState.currentRowIndex + 1; 355 | let colIndex = this.focusState.currentColIndex; 356 | let indexString = [rowIndex, colIndex].join("-"); 357 | while ( 358 | this.focusElementMap[this.focusState.currentIndexString] === 359 | this.focusElementMap[indexString] 360 | ) { 361 | rowIndex++; 362 | indexString = [rowIndex, colIndex].join("-"); 363 | } 364 | indexString = [rowIndex, colIndex].join("-"); 365 | this.setFocus(indexString); 366 | } 367 | } 368 | 369 | moveLeft(event, node, index) { 370 | this.keysMap["left"].handler && 371 | this.keysMap["left"].handler(event, node, index); 372 | if (this.isLeftBoundary()) { 373 | if (this.focusOptions.circle.horizontal) { 374 | this.removeFocus(index); 375 | let rowIndex = this.focusState.currentRowIndex; 376 | let colIndex = this.indexMap[rowIndex][ 377 | this.indexMap[rowIndex].length - 1 378 | ]; 379 | let indexString = [rowIndex, colIndex].join("-"); 380 | this.setFocus(indexString); 381 | } 382 | } else { 383 | this.removeFocus(index); 384 | let rowIndex = this.focusState.currentRowIndex; 385 | let colIndex = this.focusState.currentColIndex - 1; 386 | let indexString = [rowIndex, colIndex].join("-"); 387 | // 如果nextindex和previndex引用的是同一个element,则自减 388 | while ( 389 | this.focusElementMap[this.focusState.currentIndexString] === 390 | this.focusElementMap[indexString] 391 | ) { 392 | colIndex--; 393 | indexString = [rowIndex, colIndex].join("-"); 394 | } 395 | indexString = [rowIndex, colIndex].join("-"); 396 | this.setFocus(indexString); 397 | } 398 | } 399 | 400 | moveRight(event, node, index) { 401 | this.keysMap["right"].handler && 402 | this.keysMap["right"].handler(event, node, index); 403 | if (this.isRightBoundary()) { 404 | if (this.focusOptions.circle.horizontal) { 405 | this.removeFocus(index); 406 | let rowIndex = this.focusState.currentRowIndex; 407 | let colIndex = this.indexMap[rowIndex][0]; 408 | let indexString = [rowIndex, colIndex].join("-"); 409 | this.setFocus(indexString); 410 | } 411 | } else { 412 | this.removeFocus(index); 413 | let rowIndex = this.focusState.currentRowIndex; 414 | let colIndex = this.focusState.currentColIndex + 1; 415 | let indexString = [rowIndex, colIndex].join("-"); 416 | while ( 417 | this.focusElementMap[this.focusState.currentIndexString] === 418 | this.focusElementMap[indexString] 419 | ) { 420 | colIndex++; 421 | indexString = [rowIndex, colIndex].join("-"); 422 | } 423 | indexString = [rowIndex, colIndex].join("-"); 424 | this.setFocus(indexString); 425 | } 426 | } 427 | 428 | // 键盘上下左右触发函数 参数 按键方向, 原焦点索引字符串,焦点可循环标志位 429 | move(direction, event) { 430 | const directionMap = { 431 | up: { 432 | handler: this.moveUp 433 | }, 434 | down: { 435 | handler: this.moveDown 436 | }, 437 | left: { 438 | handler: this.moveLeft 439 | }, 440 | right: { 441 | handler: this.moveRight 442 | } 443 | }; 444 | const runner = Object.assign({}, this.keysMap, directionMap); 445 | runner[direction].handler.call( 446 | this, 447 | event, 448 | this.focusState.currentFocusElement, 449 | this.focusState.currentIndexString 450 | ); 451 | } 452 | } 453 | 454 | export default Focuser; 455 | -------------------------------------------------------------------------------- /src/lifycycle.js: -------------------------------------------------------------------------------- 1 | class Lifecycle { 2 | constructor(options, vm) { 3 | this.hooks = {}; 4 | this.init(options, vm); 5 | vm.lifecycle = this; 6 | vm.callHook = this.callHook.bind(this); 7 | } 8 | 9 | init(options, vm) { 10 | const { 11 | beforeCreate, 12 | created, 13 | beforeMount, 14 | mounted, 15 | beforeUpdate, 16 | updated, 17 | beforeDestory, 18 | destoried 19 | } = options; 20 | const hooks = { 21 | beforeCreate, 22 | created, 23 | beforeMount, 24 | mounted, 25 | beforeUpdate, 26 | updated, 27 | beforeDestory, 28 | destoried 29 | }; 30 | Object.keys(hooks).forEach((key, index) => { 31 | if (hooks[key] === undefined) { 32 | hooks[key] = emptyFn; 33 | } 34 | if (hooks[key] instanceof Function) { 35 | hooks[key] = hooks[key].bind(vm); 36 | } else { 37 | console.warn("lifecycle hooks must be a function"); 38 | hooks[key] = emptyFn; 39 | } 40 | }); 41 | this.hooks = hooks; 42 | } 43 | 44 | callHook(fnName) { 45 | // fnName in this.hooks && this.hooks[fnName]() 46 | this.hooks[fnName](); 47 | } 48 | } 49 | 50 | function emptyFn() { 51 | return; 52 | } 53 | 54 | export default Lifecycle; 55 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Observer from "./observer"; 2 | import Compiler from "./compiler"; 3 | import Focuser from "./focuser"; 4 | import Lifecycle from "./lifycycle"; 5 | 6 | class TVVM { 7 | constructor(options) { 8 | // 初始化参数, 把el, data等进行赋值与绑定 9 | // data如果是函数就取返回值, 如果不是则直接赋值 10 | // 初始化焦点管理对象 11 | new Focuser(this, options); 12 | // 初始化生命周期对象 13 | new Lifecycle(options.hooks || {}, this); 14 | // beforeCreate 15 | this.callHook("beforeCreate"); 16 | 17 | this.$data = 18 | typeof options.data === "function" ? options.data() : options.data; 19 | this.methods = options.methods; 20 | // 数据代理, 把data对象属性代理到vm实例上 21 | this.proxy(this.$data, this); 22 | this.proxy(options.methods, this); 23 | 24 | // 把$el真实的dom节点编译成vdom, 并解析相关指令 25 | if (options.el) { 26 | // 数据劫持, 27 | new Observer(this.$data, this); 28 | // created 29 | this.callHook("created"); 30 | // beforeMounte 31 | this.callHook("beforeMount"); 32 | new Compiler(options.el, this); 33 | this.focuser.generateIndexMap(); 34 | // mounted 此时可以访问 this.$el 35 | this.callHook("mounted"); 36 | } 37 | } 38 | // 数据代理, 访问/设置 this.a 相当于访问设置 this.data.a 39 | proxy(data, proxyTarget) { 40 | Object.keys(data).forEach(key => { 41 | Object.defineProperty(proxyTarget, key, { 42 | enumerable: true, 43 | configurable: true, 44 | get() { 45 | return data[key]; 46 | }, 47 | set(newValue) { 48 | // if (proxyTarget[key] !== undefined) { 49 | // console.warn(`key ${key} has already in Target`); 50 | // } 51 | data[key] = newValue; 52 | } 53 | }); 54 | }); 55 | } 56 | } 57 | 58 | export default TVVM; 59 | -------------------------------------------------------------------------------- /src/observer.js: -------------------------------------------------------------------------------- 1 | import Dep from "./dep"; 2 | 3 | class Observer { 4 | constructor(data, vm) { 5 | this.observer(data); 6 | this.vm = vm; 7 | } 8 | 9 | observer(data) { 10 | // 递归的终止条件: 当观察数据不存在或不再是对象是停止 11 | if (!data || typeof data !== "object") { 12 | return; 13 | } 14 | Object.keys(data).forEach(key => { 15 | // 递归调用自身, 遍历对象上的所有属性都为响应式的 16 | this.observer(data[key]); 17 | this.setReactive(data, key); 18 | }); 19 | } 20 | // 响应式 对数据的修改会触发相应的功能 21 | setReactive(obj, key) { 22 | let value = obj[key]; 23 | let _this = this; 24 | let dep = new Dep(); 25 | Object.defineProperty(obj, key, { 26 | enumerable: true, 27 | configurable: true, 28 | get() { 29 | // 依赖收集 进行订阅, 在编译阶段, compiler会给template中的每个指令增加一个watcher, Dep.target 为一个watcher 30 | Dep.target && dep.addSubs(Dep.target); 31 | 32 | return value; 33 | }, 34 | set(newValue) { 35 | if (newValue !== obj[key]) { 36 | // 对新值继续劫持 37 | _this.observer(newValue); 38 | // 用新值替换旧值 39 | _this.vm.callHook("beforeUpdate"); 40 | value = newValue; 41 | // 发布通知 42 | dep.notify(); 43 | _this.vm.callHook("updated"); 44 | // update 45 | } 46 | } 47 | }); 48 | } 49 | } 50 | 51 | export default Observer; 52 | -------------------------------------------------------------------------------- /src/watcher.js: -------------------------------------------------------------------------------- 1 | import Dep from "./dep"; 2 | 3 | class Watcher { 4 | /** 5 | * 6 | * @param {*} vm 7 | * @param {*} watchTarget | watch target in data e.g input.value 8 | * @param {*} expr | expresion in {{data.input.value + 1}} e.g 'data.input.value + 1' 9 | * @param {*} bindAttrName | t-bind node attribute name e.g :id="data.tid" --> id 10 | * @param {*} cb | update callback 11 | */ 12 | constructor(vm, watchTarget, expr, bindAttrName, cb) { 13 | this.vm = vm; 14 | this.watchTarget = watchTarget; 15 | this.expr = expr; 16 | this.bindAttrName = bindAttrName; 17 | this.cb = cb; 18 | this.value = this.getValAndSetTarget(); // save value when first compiled 19 | } 20 | getValAndSetTarget() { 21 | Dep.target = this; 22 | let value = this.getValue(this.watchTarget, this.vm.$data); 23 | Dep.target = null; 24 | return value; 25 | } 26 | getValue(tag, base) { 27 | let arr = tag.split("."); 28 | return arr.reduce((prev, next) => { 29 | return prev[next]; 30 | }, base); 31 | } 32 | update() { 33 | let oldVal = this.value; 34 | let newVal = this.getValue(this.watchTarget, this.vm.$data); 35 | this.cb && this.cb(newVal, this.bindAttrName, this.expr); 36 | } 37 | } 38 | 39 | export default Watcher; 40 | -------------------------------------------------------------------------------- /test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zexiplus/TVVM/9852ac9edf2c0a2a688e61c0a13b44b64de8aa52/test/.gitkeep -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | TVVM test page 8 | 9 | 10 | 54 | 55 | 56 |
57 |
58 | 59 | 60 |
61 | t-if test 62 | 63 | 64 |
t-if="data.display"
65 |
66 | 67 | 68 |
69 | t-bind test 70 |
:id="data.id" t-bind:name="data.name"
71 |
72 | 73 | 74 |
75 | t-class test 76 | 77 | 78 |
t-class="data.classList"
79 |
t-class="data.classList2"
80 |
81 | 82 | 83 |
84 | t-index test 85 |
86 |
t-index="0-0, 1-0"
87 |
t-index="0-1, 1-1"
88 |
89 |
90 | t-index="0-2" 91 |
92 |
93 | t-index="1-2" 94 |
95 |
96 |
97 |
98 | 99 | 100 |
101 | t-for test 102 |
    103 |
  • {{item + 1}}
  • 104 |
105 |
106 | 107 | 108 |
109 | t-value test 110 | 111 | {{data.input.value}} 112 | 113 | {{data.name}} 114 |
115 | 116 | 117 |
118 | {{}} test
119 | {{data.input.value.toString()}}
120 | {{data.input.value + data.name}}
121 | {{Number(data.input.value)}}
122 | {{data.input.value}}
123 | {{data.input.value + 1}}
124 | {{data.input.value + '1'}}
125 | {{data.input.value * 2}}
126 |
127 |
128 | 129 |
130 | 198 | 199 | -------------------------------------------------------------------------------- /test/run.test.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer') 2 | const path = require('path') 3 | // const tvvm = require('../dist/tvvm.js') 4 | 5 | let browser 6 | let page 7 | 8 | // jest api 9 | // describe('title', fn) 10 | // test('name', fn, timeout) 11 | 12 | // assert api 13 | // expect().toEqual() 14 | // expect().toBeFalsy() 15 | // expect().toBeTruthy() 16 | 17 | // browser api 18 | // page.goto(url) 19 | 20 | // jest global hooks 21 | beforeAll(async () => { 22 | browser = await puppeteer.launch() 23 | page = await browser.newPage({ args: ['--no-sandbox'] }) 24 | }) 25 | 26 | describe('\n TVVM test case \n', () => { 27 | test('page loader', async () => { 28 | await page.goto(`file://${path.resolve('./test/index.html')}`, { waitUntil: 'load'}) 29 | }, 10000) 30 | 31 | test('Test html load', async () => { 32 | const title = await page.title() 33 | expect(title).toBe('TVVM') 34 | }) 35 | 36 | }) 37 | -------------------------------------------------------------------------------- /test/unit/compiler.test.js: -------------------------------------------------------------------------------- 1 | const Compiler = require('../../src/compiler') 2 | 3 | describe('Compiler unit test', () => { 4 | test('Compiler is a function', () => { 5 | expect(Compiler instanceof Function).toBeTruthy() 6 | }) 7 | }) --------------------------------------------------------------------------------