├── .babelrc ├── .eslintrc.js ├── .gitignore ├── README.md ├── example ├── index.html └── test.html ├── lib ├── events.js ├── handlers.js ├── index.js └── isct.js ├── package.json ├── script ├── cdn-uploader.js └── deploy.js ├── server └── test.js ├── test └── index.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"], 3 | "plugins": ["transform-runtime", 4 | "transform-decorators-legacy", 5 | "transform-class-properties"] 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | think: true, 4 | $: true, 5 | }, 6 | // 指定脚本的运行环境 7 | env: { 8 | browser: true, 9 | commonjs: true, 10 | es6: true, 11 | node: true 12 | }, 13 | // 启用airbnb的eslint规则 14 | // 对于我们持不同意见的规则,在下面的rules进行了重新定义 15 | extends: ['airbnb-base'], 16 | plugins: ['vue'], 17 | // 指定解析器 18 | parser: 'babel-eslint', 19 | parserOptions: { 20 | ecmaFeatures: { 21 | jsx: true, 22 | 'ecmaVersion': 7, //使用ES2016 23 | }, 24 | sourceType: 'module', 25 | }, 26 | rules: { 27 | 'no-inner-declarations': 'off', 28 | 'prefer-const': ['error', {'destructuring': 'all'}], 29 | // ✘ : 因为需要异步 one-by-one 执行 sprite 的渲染,故关闭此规则 30 | 'no-await-in-loop': 'off', 31 | // ✘ : 允许匿名函数 32 | 'func-names': 'off', 33 | 'no-restricted-syntax': ['error', 'WithStatement'], 34 | // ✘ : 在确定不需要判断 prototype 的基础上没必要在 for-in 里加 if 35 | 'guard-for-in': 'off', 36 | 'no-unused-vars': 'warn', 37 | // 强制 getter 和 setter 在对象中成对出现 38 | // 默认状态是开启对setWithoutGet的监控,关闭对getWithoutSet的监控 39 | 'accessor-pairs': 'error', 40 | // ✘ : 关闭要求箭头函数体使用大括号的监控 41 | // 严格执行会降低可读性 42 | // 比如:export default context => app.preFetch(context).then(() => app) 43 | 'arrow-body-style': ['off', 'as-needed', { 'requireReturnForObjectLiteral': true }], 44 | // ✘ : 关闭对骆驼拼写法的监控 45 | // 因为我们要和后端接口保持一致 46 | 'camelcase': 'off', 47 | // 因为有些 thinkJS 方法留空,没有 this 48 | // 设置为警告,便于检查 49 | 'class-methods-use-this': 'warn', 50 | // ✘ : 关闭这一规则 51 | // JSON 或 Object 最后可以有逗号也可以没有逗号 52 | 'comma-dangle': 'off', 53 | // ✘ : 关闭这一规则 54 | // 因为 thinkJS 有一些 async 方法,处理一般过程,故不返回任何值 55 | 'consistent-return': 'off', 56 | // ✘ : 关闭对于最后行为空行的监控 57 | 'eol-last': 'off', 58 | // ✘ : 关闭这一规则 59 | // 便于开发环境根据情况来 require 不同的配置 60 | 'global-require': 'off', 61 | // 要求关键字前后都添加空格,但是overrides数组中的关键字后面不添加空格 62 | 'keyword-spacing': ['error', { 63 | 'overrides': { 64 | 'if': { 'after': false }, 65 | 'for': { 'after': false }, 66 | 'while': { 'after': false }, 67 | 'function': { 'after': false }, 68 | }, 69 | }], 70 | // ✘ : 允许覆盖变量名 71 | // 因为我们可能模块内定义一个 decorate,这个 decorate 有可能和变量同名 72 | 'no-shadow': 'off', 73 | // ✘ : 允许使用位操作 74 | // 因为 node 里面底层处理比如文件操作之类用位操作还是方便 75 | // 另外我们一般用 | 0 来取整 76 | 'no-bitwise': 'off', 77 | // 禁止console,但是允许console.warn() 和 console.error() 78 | 'no-console': ['warn', { allow: ['warn', 'error'] }], 79 | // 禁止在条件中使用常量表达式 80 | // 允许在循环中使用常量表达式 81 | 'no-constant-condition': ['warn', { 'checkLoops': false }], 82 | // ✘ : 关闭对于禁用continue的监控 83 | 'no-continue': 'error', 84 | // 禁止使用较短的符号实现类型转换, 对于Boolean类型例外 85 | 'no-implicit-coercion': ['error', { 86 | boolean: false, 87 | number: true, 88 | string: true, 89 | allow: [], 90 | }], 91 | // 禁止在全局范围使用变量和函数声明 92 | 'no-implicit-globals': 'error', 93 | // 禁止 this 关键字在类或类对象之外出现 94 | // 因为在 decorators 里面可能会使用 this,故不强制,仅提示 warn 95 | 'no-invalid-this': 'warn', 96 | // 禁止混合操作符,同组的需要添加圆括号 97 | // 这个其实有点不太实用 98 | 'no-mixed-operators': ['off', { 99 | groups: [ 100 | ['===', '!==', '>', '>=', '<', '<='], 101 | ['&&', '||'], 102 | ['in', 'instanceof'], 103 | ], 104 | allowSamePrecedence: false, 105 | }], 106 | // ✘ : 关闭对于禁止使用new产生副作用的监控 107 | // 由于我们使用Vue,需要创建根实例,所以关闭 108 | 'no-new': 'off', 109 | // ✘ : 关闭对于禁止对参数进行赋值的监控 110 | // 因为我们其实常常需要对参数进行修改操作 111 | 'no-param-reassign': ['off', { 'props': false }], 112 | // ✘ : 关闭对于禁用自增自减的监控 113 | 'no-plusplus': 'off', 114 | // ✘ : 关闭对于禁用下划线的监控 115 | 'no-underscore-dangle': ['off', { allowAfterThis: false }], 116 | // 在声明之前不可以使用变量和类,但是函数除外 117 | 'no-use-before-define': ['error', { functions: false, classes: true, variables: true }], 118 | // ✘ : 关闭对强制行的最大长度的监控 119 | // 传参很多就很方啊,尤其加上默认参数的时候 120 | 'max-len': 'off', 121 | // 禁止一个文件中在忽略空行和注释之后行数超过300行 122 | 'max-lines': ['error', { 123 | max: 300, 124 | skipBlankLines: true, 125 | skipComments: true 126 | }], 127 | // 一个函数的最大参数数量不可以超过5个 128 | 'max-params': ['error', 5], 129 | 'max-statements': ['off', 10], 130 | // ✘ : 关闭这一规则 131 | // 因为 ES6 之前 JS 没有块级作用域,存在变量提升,故采用此规则 132 | // 把变量定义在一起可以减少歧义 133 | 'one-var': 'off', 134 | // 强制在花括号中不使用空格 135 | 'object-curly-spacing': ['error', 'never'], 136 | // 强制将对象的属性放在不同的行上 137 | // 禁止所有的 key 和 value 在同一行 138 | 'object-property-newline': ['error', { 139 | allowMultiplePropertiesPerLine: true, 140 | }], 141 | // 要求把换行符放在操作符前面 142 | 'operator-linebreak': ['error', 'before'], 143 | // 在promise函数的reject()中,要么抛出Error要么为空 144 | 'prefer-promise-reject-errors': ['error', { allowEmptyReject: true }], 145 | // 强制 async 方法使用 await 146 | // 否则可能会导致 babel 把不必要的 async 编译成 generator 147 | 'require-await': 'error', 148 | 'require-yield': 'error', 149 | // 禁用分号 150 | 'semi': ['error', 'never'], 151 | // import/禁止使用dependency外定义的文件 152 | 'import/no-extraneous-dependencies': 0, 153 | 'import/first': 0, 154 | 'import/no-unresolved': 0, 155 | 'import/newline-after-import': 0, 156 | 'import/extensions': 0, 157 | }, 158 | root: true, 159 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage/ 15 | .nyc_output/ 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Dependency directory 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 25 | node_modules/ 26 | 27 | # IDE config 28 | .idea 29 | 30 | # output 31 | output/ 32 | output.tar.gz 33 | 34 | app/ 35 | dist/ 36 | 37 | runtime/ 38 | 39 | .DS_Store 40 | 41 | sftp-config.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # intersection.js 2 | 3 | Control a remote web page with your tablet or your mobile phone. 4 | 5 | ## How to use 6 | 7 | 1. Put the script at the bottom of the page that you want to control. 8 | 9 | ```html 10 | 12 | ``` 13 | 14 | 2. Open the page on your PC and your mobile phone. Operate on your mobile phone. 15 | 16 | ## Websocket server 17 | 18 | Implement a simple broadcast websocket server. 19 | 20 | ```js 21 | 22 | const WebSocket = require('ws'); 23 | 24 | const wss = new WebSocket.Server({ port: 8360 }); 25 | 26 | 27 | wss.on('connection', function connection(ws) { 28 | function broadcast(message) { 29 | // Broadcast to everyone else. 30 | wss.clients.forEach(function each(client) { 31 | if (client !== ws && client.token === ws.token && 32 | client.readyState === WebSocket.OPEN) { 33 | client.send(JSON.stringify(message)); 34 | } 35 | }) 36 | } 37 | 38 | ws.on('message', function incoming(data) { 39 | console.log(data) 40 | const message = JSON.parse(data) 41 | if(message.type === 'connect'){ 42 | ws.token = message.token 43 | } 44 | broadcast(message) 45 | }) 46 | }); 47 | ``` 48 | 49 | ## Demo 50 | 51 | [DEMO](http://code.weizoo.com/fit) 52 | 53 | ## License 54 | 55 | MIT -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 7 | 37 | 38 | 39 | 40 | 41 | 42 |
43 | baidu 44 |
45 |

内容

46 |

内容

47 |

内容

48 |

内容

49 |

内容

50 | 93 |
94 | 95 | 118 | 119 | -------------------------------------------------------------------------------- /example/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 7 | 13 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 39 | 40 | 92 | 93 | -------------------------------------------------------------------------------- /lib/events.js: -------------------------------------------------------------------------------- 1 | const defaultMouseEventOptions = { 2 | bubbles: true, 3 | cancelable: true, 4 | view: window 5 | } 6 | 7 | export function triggerEvent(el, type, options = {}) { 8 | options = Object.assign({}, defaultMouseEventOptions, options) 9 | 10 | const event = new Event(type, options) 11 | el.dispatchEvent(event) 12 | } 13 | 14 | export function triggerMouseEvent(el, type = 'click', options = {}) { 15 | options = Object.assign({}, defaultMouseEventOptions, options) 16 | 17 | const mouseEvent = new MouseEvent(type, options) 18 | el.dispatchEvent(mouseEvent) 19 | } 20 | 21 | export function triggerTouchEvent(el, type = 'touchstart', options) { 22 | options = Object.assign({}, defaultMouseEventOptions, options) 23 | 24 | // console.log(options) 25 | if(options.touches) { 26 | options.touches = options.touches.map((o) => { 27 | return new Touch(o) 28 | }) 29 | } 30 | if(options.targetTouches) { 31 | options.targetTouches = options.targetTouches.map((o) => { 32 | return new Touch(o) 33 | }) 34 | } 35 | if(options.changedTouches) { 36 | options.changedTouches = options.changedTouches.map((o) => { 37 | return new Touch(o) 38 | }) 39 | } 40 | 41 | const touchEvent = new TouchEvent(type, options) 42 | el.dispatchEvent(touchEvent) 43 | } 44 | 45 | export function getMouseEventArgs(evt) { 46 | // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent 47 | let {altKey, button, buttons, clientX, clientY, 48 | ctrlKey, metaKey, movementX, movementY, 49 | offsetX, offsetY, pageX, pageY, region, 50 | relatedTraget, screenX, screenY, shiftKey, x, y} = evt 51 | 52 | if(relatedTraget && relatedTraget.nodeType === 1) { 53 | relatedTraget = relatedTraget.dataset.isctId 54 | } else { 55 | relatedTraget = null 56 | } 57 | 58 | return {altKey, 59 | button, 60 | buttons, 61 | clientX, 62 | clientY, 63 | ctrlKey, 64 | metaKey, 65 | movementX, 66 | movementY, 67 | offsetX, 68 | offsetY, 69 | pageX, 70 | pageY, 71 | region, 72 | relatedTraget, 73 | screenX, 74 | screenY, 75 | shiftKey, 76 | x, 77 | y} 78 | } 79 | 80 | function getTouchTargets(targets) { 81 | targets = Array.from(targets) 82 | 83 | return targets.map((el) => { 84 | // el = Object.assign({}, el) 85 | let {clientX, clientY, force, identifier, pageX, pageY, 86 | radiusX, radiusY, rotationAngle, screenX, screenY, 87 | target} = el 88 | 89 | if(target && target.nodeType === 1) { 90 | target = target.dataset.isctId 91 | } 92 | 93 | return {clientX, 94 | clientY, 95 | force, 96 | identifier, 97 | pageX, 98 | pageY, 99 | radiusX, 100 | radiusY, 101 | rotationAngle, 102 | screenX, 103 | screenY, 104 | target} 105 | }) 106 | } 107 | 108 | export function getTouchEventArgs(evt) { 109 | // https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/TouchEvent 110 | let {touches, targetTouches, changedTouches, 111 | ctrlKey, shiftKey, altKey, metaKey} = evt 112 | 113 | if(touches) { 114 | touches = getTouchTargets(touches) 115 | } 116 | if(targetTouches) { 117 | targetTouches = getTouchTargets(targetTouches) 118 | } 119 | if(changedTouches) { 120 | changedTouches = getTouchTargets(changedTouches) 121 | } 122 | return {touches, 123 | targetTouches, 124 | changedTouches, 125 | ctrlKey, 126 | shiftKey, 127 | altKey, 128 | metaKey} 129 | } 130 | -------------------------------------------------------------------------------- /lib/handlers.js: -------------------------------------------------------------------------------- 1 | import isct from './isct' 2 | 3 | import {triggerEvent, triggerMouseEvent, triggerTouchEvent} from './events' 4 | 5 | function handleEvent(data) { 6 | const {type, target} = data 7 | if(target) { 8 | const el = isct.getIsctEl(target) 9 | if(el.tagName !== 'INPUT' || el.type !== 'checkbox' && el.type !== 'radio'){ 10 | //修正 input 的 change 事件被触发两次的 bug 11 | //因为 click 会触发一次 change,然后该 change 会被同步,于是又触发一次 12 | triggerEvent(el, type) 13 | } 14 | } 15 | } 16 | 17 | function handleMouseEvent(data) { 18 | const {type, target, args} = data 19 | if(target) { 20 | const el = isct.getIsctEl(target) 21 | if(args.relatedTraget) { 22 | args.relatedTraget = isct.getIsctEl(target) 23 | } 24 | triggerMouseEvent(el, type, args) 25 | } 26 | } 27 | 28 | function handleTouchEvent(data) { 29 | const {type, target, args} = data 30 | if(target) { 31 | const el = isct.getIsctEl(target) 32 | if(args.touches) { 33 | args.touches.forEach((o) => { 34 | o.target = isct.getIsctEl(o.target) 35 | }) 36 | } 37 | if(args.targetTouches) { 38 | args.targetTouches.forEach((o) => { 39 | o.target = isct.getIsctEl(o.target) 40 | }) 41 | } 42 | if(args.changedTouches) { 43 | args.changedTouches.forEach((o) => { 44 | o.target = isct.getIsctEl(o.target) 45 | }) 46 | } 47 | triggerTouchEvent(el, type, args) 48 | } 49 | } 50 | 51 | export default { 52 | connect(data, ws) { 53 | const url = data.url 54 | if(url !== location.href || ws.userAction) { 55 | location.href = url 56 | } 57 | }, 58 | disconnect(data) { 59 | // //延迟一下再刷新,这样如果有跳转就先跳转走 60 | // if(ws.readyState === WebSocket.OPEN){ 61 | // ws.close() 62 | // } 63 | // setTimeout(()=> { 64 | // location.reload() 65 | // }, 500) 66 | }, 67 | windowScroll(data, ws) { 68 | const {x, y, w, h} = data 69 | const width = document.documentElement.scrollWidth, 70 | height = document.documentElement.scrollHeight 71 | 72 | ws.isctEvent = true //防止反复推 scroll 消息 73 | window.scrollTo(Math.round(x * width / w), Math.round(y * height / h)) 74 | }, 75 | scroll(data) { 76 | // console.log(data) 77 | const {x, y, w, h, target} = data 78 | if(target) { 79 | const el = isct.getIsctEl(target) 80 | const width = el.scrollWidth, 81 | height = el.scrollHeight 82 | 83 | el.scrollLeft = Math.round(x * width / w) 84 | el.scrollTop = Math.round(y * height / h) 85 | } 86 | }, 87 | input(data, ws){ 88 | const {target, content, timestamp} = data 89 | 90 | if(target) { 91 | const el = isct.getIsctEl(target) 92 | if(!el.dataset.isctTime || el.dataset.isctTime < timestamp){ 93 | if(el.value !== content){ 94 | el.value = content 95 | ws.userAction = true 96 | } 97 | el.dataset.isctTime = timestamp 98 | } 99 | } 100 | }, 101 | 102 | change: handleEvent, 103 | focus: function(data){ 104 | const {target} = data 105 | 106 | if(target) { 107 | const el = isct.getIsctEl(target) 108 | 109 | el.focus() 110 | } 111 | }, 112 | //blur: handleEvent, 113 | blur: function(data){ 114 | const {target} = data 115 | 116 | if(target) { 117 | const el = isct.getIsctEl(target) 118 | 119 | el.blur() 120 | } 121 | }, 122 | 123 | click: handleMouseEvent, 124 | mousedown: handleMouseEvent, 125 | mouseup: handleMouseEvent, 126 | mousemove: handleMouseEvent, 127 | mouseover: handleMouseEvent, 128 | mouseout: handleMouseEvent, 129 | mouseenter: handleMouseEvent, 130 | mouseleave: handleMouseEvent, 131 | 132 | touchstart: handleTouchEvent, 133 | touchend: handleTouchEvent, 134 | touchmove: handleTouchEvent, 135 | } 136 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const scriptEls = document.getElementsByTagName('script') 2 | const theScriptEl = scriptEls[scriptEls.length - 1] 3 | 4 | // 5 | const server = theScriptEl.dataset.isctServer 6 | const token = theScriptEl.dataset.isctToken || 'default' 7 | 8 | import {getMouseEventArgs, getTouchEventArgs} from './events' 9 | 10 | import handlers from './handlers' 11 | 12 | import isct from './isct' 13 | 14 | if(server) { 15 | const els = document.querySelectorAll('*') 16 | 17 | // 给每个元素增加一个 data-isct-id 18 | els.forEach((el) => { 19 | isct.setIsctEl(el) 20 | if(el.tagName === 'A' && el.href) { 21 | // 对 a 标签进行处理 22 | el.addEventListener('click', (evt) => { 23 | location.href = el.href 24 | evt.preventDefault() 25 | }) 26 | } 27 | }) 28 | 29 | // 要检查 DOM 元素变化,给新的元素添加 data-isct-id 30 | const MutationObserver = window.MutationObserver || window.WebKitMutationObserver 31 | 32 | const observer = new MutationObserver((mobjs) => { 33 | if(ws && ws.readyState === WebSocket.OPEN){ 34 | ws.userAction = true 35 | } 36 | mobjs.forEach((m) => { 37 | if(m.addedNodes) { 38 | m.addedNodes.forEach((el) => { 39 | if(el.nodeType === 1) { 40 | isct.setIsctEl(el) 41 | const subNodes = el.querySelectorAll('*') 42 | subNodes.forEach(subEl => { 43 | if(subEl.nodeType === 1){ 44 | isct.setIsctEl(subEl) 45 | } 46 | }) 47 | } 48 | }) 49 | m.removedNodes.forEach((el) => { 50 | const subNodes = el.querySelectorAll('*') 51 | subNodes.forEach(subEl => { 52 | if(subEl.nodeType === 1){ 53 | isct.removeIsctEl(subEl) 54 | } 55 | }) 56 | if(el.nodeType === 1) { 57 | isct.removeIsctEl(el) 58 | } 59 | }) 60 | } 61 | }) 62 | }) 63 | 64 | observer.observe(document, { 65 | attributes: true, 66 | attributeOldValue: true, 67 | childList: true, 68 | characterData: true, 69 | characterDataOldValue: true, 70 | subtree: true, 71 | }) 72 | 73 | const ws = new WebSocket(server) 74 | 75 | function sendMessage(message) { 76 | if(message.type !== 'connect'){ 77 | ws.userAction = true 78 | } 79 | if(ws.readyState === WebSocket.OPEN) { 80 | ws.send(JSON.stringify(message)) 81 | } 82 | } 83 | 84 | ws.onopen = function () { 85 | ws.openTime = Date.now() 86 | 87 | // console.log("Connection open ...") 88 | sendMessage({type: 'connect', token, url: location.href}) 89 | 90 | window.addEventListener('unload', () => { 91 | sendMessage({type: 'disconnect'}) 92 | }) 93 | 94 | window.addEventListener('scroll', evt => { 95 | const x = window.scrollX || window.pageXOffset, 96 | y = window.scrollY || window.pageYOffset 97 | 98 | const h = document.documentElement.scrollHeight, 99 | w = document.documentElement.scrollWidth 100 | 101 | if(evt.isTrusted && !ws.isctEvent) { 102 | sendMessage({type: 'windowScroll', x, y, w, h}) 103 | } 104 | if(ws.isctEvent){ 105 | ws.isctEvent = false 106 | } 107 | }) 108 | 109 | document.documentElement.addEventListener('scroll', (evt) => { 110 | const x = evt.target.scrollLeft, 111 | y = evt.target.scrollTop 112 | 113 | const h = evt.target.scrollHeight, 114 | w = evt.target.scrollWidth 115 | 116 | const target = evt.target.dataset.isctId 117 | 118 | if(evt.isTrusted){ 119 | sendMessage({type: 'scroll', x, y, h, w, target}) 120 | } 121 | }, true) 122 | 123 | function mouseEventListener(evt) { 124 | const target = evt.target.dataset.isctId, 125 | type = evt.type 126 | 127 | const args = getMouseEventArgs(evt) 128 | 129 | if(type === 'click' && evt.target.tagName === 'LABEL') { 130 | // 不发送 label 的 click 事件 131 | // 因为 label 的 click 是会关联触发 input 的事件 132 | // label 的事件在 chrome 下本身疑似 bug 133 | // 接收方收到 label 的 click 事件后执行 click 134 | // 会触发关联的 input 的 click 事件时把该事件当成是用户手工操作的 135 | return; 136 | } 137 | 138 | if(evt.isTrusted) { 139 | sendMessage({type, target, args}) 140 | } 141 | } 142 | 143 | function touchEventListener(evt) { 144 | const target = evt.target.dataset.isctId, 145 | type = evt.type 146 | 147 | const args = getTouchEventArgs(evt) 148 | 149 | if(evt.isTrusted) { 150 | sendMessage({type, target, args}) 151 | } 152 | } 153 | 154 | ['click', 'mousedown', 'mouseup', 'mousemove', 'mouseover', 'mouseout', 155 | 'mouseenter', 'mouseleave'].forEach((type) => { 156 | document.documentElement.addEventListener(type, mouseEventListener, true) 157 | }) 158 | 159 | ;['touchstart', 'touchend', 'touchmove'].forEach((type) => { 160 | document.documentElement.addEventListener(type, touchEventListener, true) 161 | }) 162 | 163 | document.documentElement.addEventListener('input', evt => { 164 | const content = evt.target.value || evt.target.innerHTML, 165 | target = evt.target.dataset.isctId, 166 | type = evt.type 167 | 168 | if(evt.isTrusted) { 169 | sendMessage({type, target, content, timestamp: Date.now()}) 170 | } 171 | }, true) 172 | 173 | document.documentElement.addEventListener('focus', evt => { 174 | const target = evt.target.dataset.isctId, 175 | type = evt.type 176 | 177 | if(evt.isTrusted) { 178 | sendMessage({type, target}) 179 | } 180 | }, true) 181 | 182 | document.documentElement.addEventListener('blur', evt => { 183 | const target = evt.target.dataset.isctId, 184 | type = evt.type 185 | 186 | if(evt.isTrusted) { 187 | sendMessage({type, target}) 188 | } 189 | }, true) 190 | 191 | document.documentElement.addEventListener('change', evt => { 192 | const target = evt.target.dataset.isctId, 193 | type = evt.type 194 | 195 | // 这里可能会有一点问题,会导致 checkbox 的 change 执行两次 196 | // 但是不这么写又会导致 input 的 change 不执行 197 | if(evt.isTrusted) { 198 | sendMessage({type, target}) 199 | } 200 | }) 201 | } 202 | 203 | ws.onmessage = function (evt) { 204 | // console.log(`Received Message: ${evt.data}`) 205 | 206 | const data = JSON.parse(evt.data) 207 | 208 | handlers[data.type](data, ws) 209 | } 210 | 211 | // ws.onclose = function(evt) { 212 | // console.log("Connection closed.") 213 | // } 214 | } else { 215 | // console.log('do nothing') 216 | } 217 | -------------------------------------------------------------------------------- /lib/isct.js: -------------------------------------------------------------------------------- 1 | let isctId = 0 2 | const cacheMap = new Map() 3 | 4 | export default { 5 | setIsctEl(el) { 6 | el.dataset.isctId = isctId++ 7 | cacheMap.set(el.dataset.isctId, el) 8 | }, 9 | getIsctEl(id) { 10 | return cacheMap.get(id) 11 | }, 12 | removeIsctEl(el) { 13 | cacheMap.delete(el.dataset.isctId) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "intersection.js", 3 | "version": "1.0.0", 4 | "description": "Control a remote web page with your tablet or your mobile phone.", 5 | "main": "lib/index.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "scripts": { 10 | "test": "nyc ava --serial", 11 | "lint": "eslint 'lib/**/*.js' --fix", 12 | "start": "webpack-dev-server -d --quiet --port 9090 & http-server example -c-1 -p 9091", 13 | "deploy": "rm -rf dist/* && ./script/deploy.js" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/spritejs/sprite-timeline" 20 | }, 21 | "license": "MIT", 22 | "dependencies": { 23 | "ws": "^3.1.0" 24 | }, 25 | "devDependencies": { 26 | "ava": "^0.21.0", 27 | "babel-core": "^6.24.0", 28 | "babel-eslint": "^7.2.3", 29 | "babel-loader": "^6.4.1", 30 | "babel-plugin-transform-class-properties": "^6.24.1", 31 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 32 | "babel-plugin-transform-runtime": "^6.23.0", 33 | "babel-preset-env": "^1.3.2", 34 | "colors": "^1.1.2", 35 | "coveralls": "^2.13.1", 36 | "eslint": "^3.19.0", 37 | "eslint-config-airbnb-base": "^11.2.0", 38 | "eslint-plugin-import": "^2.2.0", 39 | "eslint-plugin-vue": "^2.1.0", 40 | "http-server": "^0.9.0", 41 | "nyc": "^11.1.0", 42 | "webpack": "^2.3.3", 43 | "webpack-dev-server": "^2.4.2" 44 | }, 45 | "ava": { 46 | "require": [ 47 | "babel-register" 48 | ], 49 | "babel": "inherit" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /script/cdn-uploader.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | upload(file) { 4 | try { 5 | const qcdn = require('@q/qcdn') 6 | return qcdn.upload(file, { 7 | https: true, 8 | keepName: true 9 | }) 10 | } catch (ex) { 11 | return Promise.reject(new Error('no cdn uploader specified!')) 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /script/deploy.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const webpack = require('webpack') 4 | const fs = require('fs') 5 | const path = require('path') 6 | 7 | const webpackConf = require('../webpack.config.js') 8 | 9 | webpack(webpackConf({production: true}), (err, stats) => { 10 | const cdnUploader = require('./cdn-uploader'), 11 | output = stats.compilation.compiler.options.output, 12 | file = path.resolve(output.path, output.filename) 13 | 14 | cdnUploader.upload(file).then((res) => { 15 | const readmeFile = path.resolve(__dirname, '..', 'README.md') 16 | let content = fs.readFileSync(readmeFile, 'utf-8') 17 | content = content.replace(/script src="(.*)"/igm, `script src="${res[file]}"`) 18 | fs.writeFileSync(readmeFile, content) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /server/test.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws'); 2 | 3 | const wss = new WebSocket.Server({ port: 8360 }); 4 | 5 | 6 | wss.on('connection', function connection(ws) { 7 | function broadcast(message) { 8 | // Broadcast to everyone else. 9 | wss.clients.forEach(function each(client) { 10 | if (client !== ws && client.token === ws.token && 11 | client.readyState === WebSocket.OPEN) { 12 | client.send(JSON.stringify(message)); 13 | } 14 | }) 15 | } 16 | 17 | ws.on('message', function incoming(data) { 18 | console.log(data) 19 | const message = JSON.parse(data) 20 | if(message.type === 'connect'){ 21 | ws.token = message.token 22 | } 23 | broadcast(message) 24 | }) 25 | }); 26 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const test = require("ava") 2 | 3 | test('bootstrap', t => { 4 | t.is(1, 1) 5 | }) 6 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (env = {}) { 2 | const webpack = require('webpack'), 3 | path = require('path'), 4 | fs = require('fs'), 5 | packageConf = JSON.parse(fs.readFileSync('package.json', 'utf-8')) 6 | 7 | const version = packageConf.version, 8 | proxyPort = 9091, 9 | plugins = [], 10 | jsLoaders = [] 11 | 12 | if(env.production) { 13 | // compress js in production environment 14 | 15 | // plugins.push( 16 | // new webpack.optimize.UglifyJsPlugin({ 17 | // compress: { 18 | // warnings: false, 19 | // drop_console: false 20 | // } 21 | // }) 22 | // ) 23 | } 24 | 25 | if(fs.existsSync('./.babelrc')) { 26 | // use babel 27 | const babelConf = JSON.parse(fs.readFileSync('.babelrc')) 28 | jsLoaders.push({ 29 | loader: 'babel-loader', 30 | options: babelConf 31 | }) 32 | } 33 | 34 | return { 35 | entry: './lib/index.js', 36 | output: { 37 | filename: env.production ? `intersection-${version}.js` : 'index.js', 38 | path: path.resolve(__dirname, 'dist'), 39 | publicPath: '/js/', 40 | //library: 'intersection', 41 | //libraryTarget: 'umd' 42 | }, 43 | 44 | plugins, 45 | 46 | module: { 47 | rules: [{ 48 | test: /\.js$/, 49 | exclude: /(node_modules\/(?!await-signal)|bower_components)/, 50 | use: jsLoaders 51 | }] 52 | }, 53 | 54 | devServer: { 55 | proxy: { 56 | '*': `http://127.0.0.1:${proxyPort}` 57 | } 58 | }, 59 | //devtool: 'inline-source-map', 60 | } 61 | } 62 | --------------------------------------------------------------------------------