├── .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 |
--------------------------------------------------------------------------------