├── .gitignore ├── src ├── client │ ├── tool.png │ ├── fillin-map-v5.png │ ├── index.html │ ├── style.css │ ├── edit.html │ └── DragDropTouch.js └── index.ts ├── README.md ├── tsconfig.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | users.js 4 | -------------------------------------------------------------------------------- /src/client/tool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/fillinboard/master/src/client/tool.png -------------------------------------------------------------------------------- /src/client/fillin-map-v5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stats/fillinboard/master/src/client/fillin-map-v5.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fillinboard 2 | A whiteboard for managing RFD fillins 3 | 4 | 5 | Create a user.js file in the root folder it must contain users for authentication. 6 | 7 | ``` 8 | "use strict"; 9 | /** 10 | * EDIT ME IN PRODUCTION 11 | **/ 12 | exports.__esModule = true; 13 | exports.BASIC_USERS = { 14 | 'user': 'password' 15 | }; 16 | exports.ADMIN_USERS = { 17 | 'admin': 'password' 18 | }; 19 | ``` 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "lib", 4 | "module": "commonjs", 5 | "lib": ["es6"], 6 | "target": "es2016", 7 | "declaration": true, 8 | "noImplicitAny": false, 9 | "experimentalDecorators": true, 10 | "sourceMap": false, 11 | "esModuleInterop": true, 12 | "forceConsistentCasingInFileNames": true 13 | }, 14 | "include": [ 15 | "**/*.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fillinboard", 3 | "version": "1.0.0", 4 | "description": "An application for managing RFD FillIns with a whiteboard like application.", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "start": "nodemon --ignore \"client\" --exec node -r ts-node/register src/index.ts", 8 | "start:prod": "pm2 start dist/index.js", 9 | "build": "tsc src/index.ts --outDir dist && cd src && copyfiles client/** ../dist" 10 | }, 11 | "author": "Daniel Curran", 12 | "license": "MIT", 13 | "dependencies": { 14 | "@types/express": "^4.17.7", 15 | "@types/socket.io": "^2.1.8", 16 | "cors": "^2.8.5", 17 | "express": "^4.17.1", 18 | "express-basic-auth": "^1.2.0", 19 | "moment": "^2.26.0", 20 | "moment-timezone": "^0.5.31", 21 | "socket.io": "^2.3.0", 22 | "typescript": "^3.9.6" 23 | }, 24 | "devDependencies": { 25 | "copyfiles": "^2.3.0", 26 | "nodemon": "^2.0.4", 27 | "ts-node": "^8.10.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | FillIn Client 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |

First Alarm

13 |

Default title not changed

14 |
15 |
16 |

Second Alarm

17 |

Default title not changed

18 |
19 |
20 |

Out of Service

21 |

Default title not changed

22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 | 30 |
31 |
32 |
33 | 34 | 35 | 36 | 37 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/client/style.css: -------------------------------------------------------------------------------- 1 | *.unselectable { 2 | -moz-user-select: -moz-none; 3 | -khtml-user-select: none; 4 | -webkit-user-select: none; 5 | 6 | /* 7 | Introduced in IE 10. 8 | See http://ie.microsoft.com/testdrive/HTML5/msUserSelect/ 9 | */ 10 | -ms-user-select: none; 11 | user-select: none; 12 | } 13 | 14 | #waiting-preloader { 15 | position: absolute; 16 | top: 50%; 17 | left: 50%; 18 | } 19 | 20 | .unit { 21 | position: absolute; 22 | top: 50px; 23 | left: 50px; 24 | cursor: grab; 25 | border-radius: 0; 26 | font-size: 12px; 27 | height: 22px !important; 28 | min-width: 50px !important; 29 | padding: 0 2px !important; 30 | width: auto !important; 31 | line-height: 17px !important; 32 | border: 2px solid white; 33 | } 34 | 35 | .unit:active { 36 | cursor: grabbing; 37 | } 38 | 39 | .modal h4 { 40 | padding-bottom: 8px; 41 | margin-left: -2px; 42 | } 43 | 44 | .on-call { 45 | transform: rotate(90deg); 46 | } 47 | 48 | .has-tool { 49 | background: url('/tool.png') no-repeat; 50 | background-position: right 2px center; 51 | padding-right: 10px !important; 52 | } 53 | 54 | #titles { 55 | position: absolute; 56 | top: 5px; 57 | left: 613px; 58 | } 59 | 60 | #titles h1 { 61 | font-size: 28px; 62 | font-weight: bold; 63 | margin: 0; 64 | padding: 0; 65 | } 66 | 67 | #titles h2 { 68 | font-size: 0.8em; 69 | font-weight: normal; 70 | color: #777; 71 | margin: 0; 72 | padding: 0; 73 | } 74 | 75 | #title1 { 76 | position: absolute; 77 | top: 300px; 78 | } 79 | 80 | #title2 { 81 | position: absolute; 82 | top: 550px; 83 | } 84 | 85 | .chat-container { 86 | border: 1px solid black; 87 | width: 590px; 88 | height: 400px; 89 | padding: 10px; 90 | margin: 10px; 91 | } 92 | 93 | .chat-container ul { 94 | height: 270px; 95 | overflow-y: scroll; 96 | } 97 | 98 | .chat-container h1 { 99 | font-weight: bold; 100 | font-size: 1.1em; 101 | margin: 0; 102 | padding: 0; 103 | border-bottom: 1px solid black; 104 | } 105 | 106 | .chat-container .name { 107 | font-weight: bold; 108 | } 109 | .chat-container .date { 110 | color: #888; 111 | font-size: 0.8em; 112 | } 113 | 114 | #fill-in-image { 115 | background: url('/fillin-map.png') no-repeat; 116 | width: 964px; 117 | height: 700px; 118 | margin-left: 12px; 119 | margin-top: 0px; 120 | } 121 | 122 | .modal { 123 | overflow-y: visible 124 | } 125 | 126 | #toast-container { 127 | top: 10px !important; 128 | right: 10px !important; 129 | bottom: auto !important; 130 | left: auto !important; 131 | } 132 | 133 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import express = require('express'); 2 | import { Application, Router } from 'express'; 3 | 4 | import { BASIC_USERS, ADMIN_USERS } from '../users'; 5 | 6 | import socketIo = require('socket.io'); 7 | 8 | import * as https from 'https'; 9 | import * as fs from 'fs'; 10 | 11 | const PORT = process.env.PORT || 443; 12 | const SSL_KEY = process.env.SSL_KEY || 'key.pem'; 13 | const SSL_CERT = process.env.SSL_CERT || 'cert.pem'; 14 | const SSL_CHAIN = process.env.SSL_CHAIN || 'chain.pem'; 15 | 16 | var moment = require('moment-timezone'); 17 | 18 | let app: Application = express(); 19 | 20 | const basicAuth = require('express-basic-auth'); 21 | 22 | function defaultTitle() { 23 | return [ 24 | { user: 'system', text: 'On Current Alarm', date: moment().tz("America/New_York").format('MM/DD/YY HH:mm:ss') }, 25 | { user: 'system', text: 'On Second Alarm', date: moment().tz("America/New_York").format('MM/DD/YY HH:mm:ss') }, 26 | { user: 'system', text: 'Out of Service', date: moment().tz("America/New_York").format('MM/DD/YY HH:mm:ss') } 27 | ]; 28 | } 29 | 30 | var clients = {}; 31 | var units; 32 | var titles = defaultTitle(); 33 | var messages = []; 34 | 35 | resetUnits(); 36 | 37 | app.get('/', (req, res) => { 38 | res.send('

Fillin Board

'); 39 | }); 40 | 41 | app.get('/tool.png', (req, res) => { 42 | res.sendFile(__dirname + '/client/tool.png'); 43 | }); 44 | 45 | app.get('/style.css', (req, res) => { 46 | res.sendFile(__dirname + '/client/style.css'); 47 | }); 48 | 49 | app.get('/fillin-map.png', (req, res) => { 50 | res.sendFile(__dirname + '/client/fillin-map-v5.png'); 51 | }); 52 | 53 | app.get('/DragDropTouch.js', (req, res) => { 54 | res.sendFile(__dirname + '/client/DragDropTouch.js'); 55 | }); 56 | 57 | /** 58 | * Basic Router with User Security 59 | **/ 60 | const router = Router(); 61 | 62 | router.get('/', (req, res) => { 63 | res.sendFile(__dirname + '/client/index.html'); 64 | }); 65 | 66 | app.use('/view', basicAuth({ 67 | challenge: true, 68 | users: BASIC_USERS 69 | }), router); 70 | 71 | /** 72 | * Admin Router with Admin Security 73 | **/ 74 | const admin = Router(); 75 | admin.get('/', (req, res) => { 76 | res.sendFile(__dirname + '/client/edit.html'); 77 | }); 78 | app.use('/admin', basicAuth({ 79 | challenge: true, 80 | users: ADMIN_USERS 81 | }), admin); 82 | 83 | let options = { 84 | key: fs.readFileSync(SSL_KEY), 85 | cert: fs.readFileSync(SSL_CERT), 86 | ca: fs.readFileSync(SSL_CHAIN) 87 | } 88 | 89 | let http = https.createServer(options, app); 90 | 91 | let io = socketIo(http); 92 | 93 | io.on('connection', (socket) => { 94 | console.log('a user connected'); 95 | 96 | clients[socket.id] = {}; 97 | 98 | socket.emit('units', units); 99 | socket.emit('set-titles', titles); 100 | 101 | socket.on('chat', (msg) => { 102 | let message = { 103 | user: clients[socket.id].name, 104 | text: msg, 105 | date: moment().tz("America/New_York").format('MM/DD/YY HH:mm:ss') 106 | } 107 | messages.push(message); 108 | if(messages.length > 50) { 109 | messages.shift(); 110 | } 111 | io.sockets.emit('chat', message); 112 | }); 113 | 114 | socket.on('submit-name', (msg) => { 115 | console.log('Client Submitted Name: ' + msg); 116 | clients[socket.id].name = msg; 117 | socket.emit('signed-in', clients[socket.id].name); 118 | socket.emit('units', units); 119 | socket.emit('set-titles', titles); 120 | socket.emit('recent-chat', messages); 121 | io.sockets.emit('users', clientsToStringArray(clients)); 122 | socket.broadcast.emit('joined', clients[socket.id].name); 123 | }); 124 | 125 | socket.on('set-titles', (msg) => { 126 | titles = []; 127 | for(let t of msg) { 128 | titles.push( 129 | { 130 | user: clients[socket.id].name, 131 | text: t, 132 | date: moment().tz("America/New_York").format('MM/DD/YY HH:mm:ss') 133 | } 134 | ) 135 | } 136 | io.sockets.emit('set-titles', titles); 137 | }) 138 | 139 | socket.on('change-unit', (msg) => { 140 | console.log('Client changed a unit', msg); 141 | let unit = units.find(unit => unit.id === msg.id); 142 | if(unit) { 143 | unit.left = msg.left; 144 | unit.top = msg.top; 145 | } 146 | io.sockets.emit('units', units); 147 | //console.log(JSON.stringify(units)); 148 | }); 149 | 150 | socket.on('add-unit', (msg) => { 151 | console.log('Add unit', msg); 152 | let unit = units.find(u => u.id === msg.id); 153 | if(unit != null) return; 154 | if(msg == null || msg.id == null || msg.id == '') { 155 | socket.emit('add-error', 'Cannot add a blank unit'); 156 | return; 157 | } 158 | units.push(msg); 159 | io.sockets.emit('units', units); 160 | }); 161 | 162 | socket.on('remove-unit', (msg) => { 163 | console.log('Remove unit', msg, msg.id); 164 | units = units.filter(unit => unit.id != msg.id); 165 | io.sockets.emit('units', units); 166 | }); 167 | 168 | socket.on('reset-units', (msg) => { 169 | console.log("Reset Units called."); 170 | resetUnits(); 171 | titles = defaultTitle(); 172 | io.sockets.emit('message', clients[socket.id].name + " has reset all units."); 173 | io.sockets.emit('units', units); 174 | io.sockets.emit('set-titles', titles); 175 | }); 176 | 177 | socket.on('toggle-on-call', (msg) => { 178 | let unit = units.find(unit => unit.id === msg.id); 179 | if(!unit) return; 180 | if(unit.onCall == null || unit.onCall == false) { 181 | unit.onCall = true; 182 | } else { 183 | unit.onCall = false; 184 | } 185 | io.sockets.emit('units', units); 186 | }); 187 | 188 | socket.on('disconnect', () => { 189 | delete clients[socket.id]; 190 | io.sockets.emit('users', clientsToStringArray(clients)); 191 | console.log('user disconnected'); 192 | }); 193 | }) 194 | 195 | http.listen(PORT, () => { 196 | console.log('listening on *:443'); 197 | }) 198 | 199 | function clientsToStringArray(clients) { 200 | let a = []; 201 | for(let client in clients) { 202 | let c = clients[client]; 203 | if(c.name != null) { 204 | a.push(c.name); 205 | } 206 | } 207 | return a; 208 | } 209 | 210 | function resetUnits() { 211 | units = [ 212 | {"id":"E1","top":475,"left":329,"color":"black","hasTool":true}, 213 | {"id":"E2","top":318,"left":274,"color":"black"}, 214 | {"id":"E3","top":361,"left":125,"color":"black"}, 215 | {"id":"E5","top":400,"left":180,"color":"black"}, 216 | {"id":"E7","top":536,"left":185,"color":"black"}, 217 | {"id":"E8","top":597,"left":361,"color":"black"}, 218 | {"id":"E9","top":366,"left":406,"color":"black"}, 219 | {"id":"E10","top":272,"left":180,"color":"black","hasTool":true}, 220 | {"id":"E12","top":424,"left":464,"color":"black"}, 221 | {"id":"E13","top":443,"left":217,"color":"black"}, 222 | {"id":"E16","top":340,"left":323,"color":"black"}, 223 | {"id":"E17","top":402,"left":315,"color":"black"}, 224 | {"id":"E19","top":95,"left":254,"color":"black"}, 225 | {"id":"T2","top":299,"left":179,"color":"red"}, 226 | {"id":"T3","top":562,"left":258,"color":"red"}, 227 | {"id":"T4","top":479,"left":427,"color":"red"}, 228 | {"id":"T5","top":464,"left":132,"color":"red"}, 229 | {"id":"T6","top":369,"left":322,"color":"red"}, 230 | {"id":"T10","top":470,"left":216,"color":"red"}, 231 | {"id":"R11","top":429,"left":313,"color":"blue","hasTool":true} 232 | ]; 233 | } 234 | -------------------------------------------------------------------------------- /src/client/edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | FillIn Client - Administration 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 25 |
26 | 27 | 28 |
29 |
30 |

First Alarm

31 |

Default title not changed

32 |
33 |
34 |

Second Alarm

35 |

Default title not changed

36 |
37 |
38 |

Out of Service

39 |

Default title not changed

40 |
41 |
42 | 43 |
44 |
45 | 46 |
47 | 48 |
49 |
50 | 51 |
52 |

Fillin Board Chat

53 | 54 |
55 |
56 |
57 | 58 |
59 |
60 | 61 |
62 |
63 |
64 |
65 |
66 | 67 |
68 | 69 | mode_edit 70 | 71 | 78 |
79 | 80 | 116 | 117 | 134 | 135 | 147 | 148 | 159 | 160 | 183 | 184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 | 196 | 197 | 198 | 199 | 200 | 478 | 479 | 480 | -------------------------------------------------------------------------------- /src/client/DragDropTouch.js: -------------------------------------------------------------------------------- 1 | var DragDropTouch; 2 | (function (DragDropTouch_1) { 3 | 'use strict'; 4 | /** 5 | * Object used to hold the data that is being dragged during drag and drop operations. 6 | * 7 | * It may hold one or more data items of different types. For more information about 8 | * drag and drop operations and data transfer objects, see 9 | * HTML Drag and Drop API. 10 | * 11 | * This object is created automatically by the @see:DragDropTouch singleton and is 12 | * accessible through the @see:dataTransfer property of all drag events. 13 | */ 14 | var DataTransfer = (function () { 15 | function DataTransfer() { 16 | this._dropEffect = 'move'; 17 | this._effectAllowed = 'all'; 18 | this._data = {}; 19 | } 20 | Object.defineProperty(DataTransfer.prototype, "dropEffect", { 21 | /** 22 | * Gets or sets the type of drag-and-drop operation currently selected. 23 | * The value must be 'none', 'copy', 'link', or 'move'. 24 | */ 25 | get: function () { 26 | return this._dropEffect; 27 | }, 28 | set: function (value) { 29 | this._dropEffect = value; 30 | }, 31 | enumerable: true, 32 | configurable: true 33 | }); 34 | Object.defineProperty(DataTransfer.prototype, "effectAllowed", { 35 | /** 36 | * Gets or sets the types of operations that are possible. 37 | * Must be one of 'none', 'copy', 'copyLink', 'copyMove', 'link', 38 | * 'linkMove', 'move', 'all' or 'uninitialized'. 39 | */ 40 | get: function () { 41 | return this._effectAllowed; 42 | }, 43 | set: function (value) { 44 | this._effectAllowed = value; 45 | }, 46 | enumerable: true, 47 | configurable: true 48 | }); 49 | Object.defineProperty(DataTransfer.prototype, "types", { 50 | /** 51 | * Gets an array of strings giving the formats that were set in the @see:dragstart event. 52 | */ 53 | get: function () { 54 | return Object.keys(this._data); 55 | }, 56 | enumerable: true, 57 | configurable: true 58 | }); 59 | /** 60 | * Removes the data associated with a given type. 61 | * 62 | * The type argument is optional. If the type is empty or not specified, the data 63 | * associated with all types is removed. If data for the specified type does not exist, 64 | * or the data transfer contains no data, this method will have no effect. 65 | * 66 | * @param type Type of data to remove. 67 | */ 68 | DataTransfer.prototype.clearData = function (type) { 69 | if (type != null) { 70 | delete this._data[type]; 71 | } 72 | else { 73 | this._data = null; 74 | } 75 | }; 76 | /** 77 | * Retrieves the data for a given type, or an empty string if data for that type does 78 | * not exist or the data transfer contains no data. 79 | * 80 | * @param type Type of data to retrieve. 81 | */ 82 | DataTransfer.prototype.getData = function (type) { 83 | return this._data[type] || ''; 84 | }; 85 | /** 86 | * Set the data for a given type. 87 | * 88 | * For a list of recommended drag types, please see 89 | * https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Recommended_Drag_Types. 90 | * 91 | * @param type Type of data to add. 92 | * @param value Data to add. 93 | */ 94 | DataTransfer.prototype.setData = function (type, value) { 95 | this._data[type] = value; 96 | }; 97 | /** 98 | * Set the image to be used for dragging if a custom one is desired. 99 | * 100 | * @param img An image element to use as the drag feedback image. 101 | * @param offsetX The horizontal offset within the image. 102 | * @param offsetY The vertical offset within the image. 103 | */ 104 | DataTransfer.prototype.setDragImage = function (img, offsetX, offsetY) { 105 | var ddt = DragDropTouch._instance; 106 | ddt._imgCustom = img; 107 | ddt._imgOffset = { x: offsetX, y: offsetY }; 108 | }; 109 | return DataTransfer; 110 | }()); 111 | DragDropTouch_1.DataTransfer = DataTransfer; 112 | /** 113 | * Defines a class that adds support for touch-based HTML5 drag/drop operations. 114 | * 115 | * The @see:DragDropTouch class listens to touch events and raises the 116 | * appropriate HTML5 drag/drop events as if the events had been caused 117 | * by mouse actions. 118 | * 119 | * The purpose of this class is to enable using existing, standard HTML5 120 | * drag/drop code on mobile devices running IOS or Android. 121 | * 122 | * To use, include the DragDropTouch.js file on the page. The class will 123 | * automatically start monitoring touch events and will raise the HTML5 124 | * drag drop events (dragstart, dragenter, dragleave, drop, dragend) which 125 | * should be handled by the application. 126 | * 127 | * For details and examples on HTML drag and drop, see 128 | * https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Drag_operations. 129 | */ 130 | var DragDropTouch = (function () { 131 | /** 132 | * Initializes the single instance of the @see:DragDropTouch class. 133 | */ 134 | function DragDropTouch() { 135 | this._lastClick = 0; 136 | // enforce singleton pattern 137 | if (DragDropTouch._instance) { 138 | throw 'DragDropTouch instance already created.'; 139 | } 140 | // detect passive event support 141 | // https://github.com/Modernizr/Modernizr/issues/1894 142 | var supportsPassive = false; 143 | document.addEventListener('test', function () { }, { 144 | get passive() { 145 | supportsPassive = true; 146 | return true; 147 | } 148 | }); 149 | // listen to touch events 150 | if ('ontouchstart' in document) { 151 | var d = document, ts = this._touchstart.bind(this), tm = this._touchmove.bind(this), te = this._touchend.bind(this), opt = supportsPassive ? { passive: false, capture: false } : false; 152 | d.addEventListener('touchstart', ts, opt); 153 | d.addEventListener('touchmove', tm, opt); 154 | d.addEventListener('touchend', te); 155 | d.addEventListener('touchcancel', te); 156 | } 157 | } 158 | /** 159 | * Gets a reference to the @see:DragDropTouch singleton. 160 | */ 161 | DragDropTouch.getInstance = function () { 162 | return DragDropTouch._instance; 163 | }; 164 | // ** event handlers 165 | DragDropTouch.prototype._touchstart = function (e) { 166 | var _this = this; 167 | if (this._shouldHandle(e)) { 168 | // raise double-click and prevent zooming 169 | if (Date.now() - this._lastClick < DragDropTouch._DBLCLICK) { 170 | if (this._dispatchEvent(e, 'dblclick', e.target)) { 171 | e.preventDefault(); 172 | this._reset(); 173 | return; 174 | } 175 | } 176 | // clear all variables 177 | this._reset(); 178 | // get nearest draggable element 179 | var src = this._closestDraggable(e.target); 180 | if (src) { 181 | // give caller a chance to handle the hover/move events 182 | if (!this._dispatchEvent(e, 'mousemove', e.target) && 183 | !this._dispatchEvent(e, 'mousedown', e.target)) { 184 | // get ready to start dragging 185 | this._dragSource = src; 186 | this._ptDown = this._getPoint(e); 187 | this._lastTouch = e; 188 | e.preventDefault(); 189 | // show context menu if the user hasn't started dragging after a while 190 | setTimeout(function () { 191 | if (_this._dragSource == src && _this._img == null) { 192 | if (_this._dispatchEvent(e, 'contextmenu', src)) { 193 | _this._reset(); 194 | } 195 | } 196 | }, DragDropTouch._CTXMENU); 197 | if (DragDropTouch._ISPRESSHOLDMODE) { 198 | this._pressHoldInterval = setTimeout(function () { 199 | _this._isDragEnabled = true; 200 | _this._touchmove(e); 201 | }, DragDropTouch._PRESSHOLDAWAIT); 202 | } 203 | } 204 | } 205 | } 206 | }; 207 | DragDropTouch.prototype._touchmove = function (e) { 208 | if (this._shouldCancelPressHoldMove(e)) { 209 | this._reset(); 210 | return; 211 | } 212 | if (this._shouldHandleMove(e) || this._shouldHandlePressHoldMove(e)) { 213 | // see if target wants to handle move 214 | var target = this._getTarget(e); 215 | if (this._dispatchEvent(e, 'mousemove', target)) { 216 | this._lastTouch = e; 217 | e.preventDefault(); 218 | return; 219 | } 220 | // start dragging 221 | if (this._dragSource && !this._img && this._shouldStartDragging(e)) { 222 | this._dispatchEvent(e, 'dragstart', this._dragSource); 223 | this._createImage(e); 224 | this._dispatchEvent(e, 'dragenter', target); 225 | } 226 | // continue dragging 227 | if (this._img) { 228 | this._lastTouch = e; 229 | e.preventDefault(); // prevent scrolling 230 | if (target != this._lastTarget) { 231 | this._dispatchEvent(this._lastTouch, 'dragleave', this._lastTarget); 232 | this._dispatchEvent(e, 'dragenter', target); 233 | this._lastTarget = target; 234 | } 235 | this._moveImage(e); 236 | this._isDropZone = this._dispatchEvent(e, 'dragover', target); 237 | } 238 | } 239 | }; 240 | DragDropTouch.prototype._touchend = function (e) { 241 | if (this._shouldHandle(e)) { 242 | // see if target wants to handle up 243 | if (this._dispatchEvent(this._lastTouch, 'mouseup', e.target)) { 244 | e.preventDefault(); 245 | return; 246 | } 247 | // user clicked the element but didn't drag, so clear the source and simulate a click 248 | if (!this._img) { 249 | this._dragSource = null; 250 | this._dispatchEvent(this._lastTouch, 'click', e.target); 251 | this._lastClick = Date.now(); 252 | } 253 | // finish dragging 254 | this._destroyImage(); 255 | if (this._dragSource) { 256 | if (e.type.indexOf('cancel') < 0 && this._isDropZone) { 257 | this._dispatchEvent(this._lastTouch, 'drop', this._lastTarget); 258 | } 259 | this._dispatchEvent(this._lastTouch, 'dragend', this._dragSource); 260 | this._reset(); 261 | } 262 | } 263 | }; 264 | // ** utilities 265 | // ignore events that have been handled or that involve more than one touch 266 | DragDropTouch.prototype._shouldHandle = function (e) { 267 | return e && 268 | !e.defaultPrevented && 269 | e.touches && e.touches.length < 2; 270 | }; 271 | 272 | // use regular condition outside of press & hold mode 273 | DragDropTouch.prototype._shouldHandleMove = function (e) { 274 | return !DragDropTouch._ISPRESSHOLDMODE && this._shouldHandle(e); 275 | }; 276 | 277 | // allow to handle moves that involve many touches for press & hold 278 | DragDropTouch.prototype._shouldHandlePressHoldMove = function (e) { 279 | return DragDropTouch._ISPRESSHOLDMODE && 280 | this._isDragEnabled && e && e.touches && e.touches.length; 281 | }; 282 | 283 | // reset data if user drags without pressing & holding 284 | DragDropTouch.prototype._shouldCancelPressHoldMove = function (e) { 285 | return DragDropTouch._ISPRESSHOLDMODE && !this._isDragEnabled && 286 | this._getDelta(e) > DragDropTouch._PRESSHOLDMARGIN; 287 | }; 288 | 289 | // start dragging when specified delta is detected 290 | DragDropTouch.prototype._shouldStartDragging = function (e) { 291 | var delta = this._getDelta(e); 292 | return delta > DragDropTouch._THRESHOLD || 293 | (DragDropTouch._ISPRESSHOLDMODE && delta >= DragDropTouch._PRESSHOLDTHRESHOLD); 294 | } 295 | 296 | // clear all members 297 | DragDropTouch.prototype._reset = function () { 298 | this._destroyImage(); 299 | this._dragSource = null; 300 | this._lastTouch = null; 301 | this._lastTarget = null; 302 | this._ptDown = null; 303 | this._isDragEnabled = false; 304 | this._isDropZone = false; 305 | this._dataTransfer = new DataTransfer(); 306 | clearInterval(this._pressHoldInterval); 307 | }; 308 | // get point for a touch event 309 | DragDropTouch.prototype._getPoint = function (e, page) { 310 | if (e && e.touches) { 311 | e = e.touches[0]; 312 | } 313 | return { x: page ? e.pageX : e.clientX, y: page ? e.pageY : e.clientY }; 314 | }; 315 | // get distance between the current touch event and the first one 316 | DragDropTouch.prototype._getDelta = function (e) { 317 | if (DragDropTouch._ISPRESSHOLDMODE && !this._ptDown) { return 0; } 318 | var p = this._getPoint(e); 319 | return Math.abs(p.x - this._ptDown.x) + Math.abs(p.y - this._ptDown.y); 320 | }; 321 | // get the element at a given touch event 322 | DragDropTouch.prototype._getTarget = function (e) { 323 | var pt = this._getPoint(e), el = document.elementFromPoint(pt.x, pt.y); 324 | while (el && getComputedStyle(el).pointerEvents == 'none') { 325 | el = el.parentElement; 326 | } 327 | return el; 328 | }; 329 | // create drag image from source element 330 | DragDropTouch.prototype._createImage = function (e) { 331 | // just in case... 332 | if (this._img) { 333 | this._destroyImage(); 334 | } 335 | // create drag image from custom element or drag source 336 | var src = this._imgCustom || this._dragSource; 337 | this._img = src.cloneNode(true); 338 | this._copyStyle(src, this._img); 339 | this._img.style.top = this._img.style.left = '-9999px'; 340 | // if creating from drag source, apply offset and opacity 341 | if (!this._imgCustom) { 342 | var rc = src.getBoundingClientRect(), pt = this._getPoint(e); 343 | this._imgOffset = { x: pt.x - rc.left, y: pt.y - rc.top }; 344 | this._img.style.opacity = DragDropTouch._OPACITY.toString(); 345 | } 346 | // add image to document 347 | this._moveImage(e); 348 | document.body.appendChild(this._img); 349 | }; 350 | // dispose of drag image element 351 | DragDropTouch.prototype._destroyImage = function () { 352 | if (this._img && this._img.parentElement) { 353 | this._img.parentElement.removeChild(this._img); 354 | } 355 | this._img = null; 356 | this._imgCustom = null; 357 | }; 358 | // move the drag image element 359 | DragDropTouch.prototype._moveImage = function (e) { 360 | var _this = this; 361 | requestAnimationFrame(function () { 362 | if (_this._img) { 363 | var pt = _this._getPoint(e, true), s = _this._img.style; 364 | s.position = 'absolute'; 365 | s.pointerEvents = 'none'; 366 | s.zIndex = '999999'; 367 | s.left = Math.round(pt.x - _this._imgOffset.x) + 'px'; 368 | s.top = Math.round(pt.y - _this._imgOffset.y) + 'px'; 369 | } 370 | }); 371 | }; 372 | // copy properties from an object to another 373 | DragDropTouch.prototype._copyProps = function (dst, src, props) { 374 | for (var i = 0; i < props.length; i++) { 375 | var p = props[i]; 376 | dst[p] = src[p]; 377 | } 378 | }; 379 | DragDropTouch.prototype._copyStyle = function (src, dst) { 380 | // remove potentially troublesome attributes 381 | DragDropTouch._rmvAtts.forEach(function (att) { 382 | dst.removeAttribute(att); 383 | }); 384 | // copy canvas content 385 | if (src instanceof HTMLCanvasElement) { 386 | var cSrc = src, cDst = dst; 387 | cDst.width = cSrc.width; 388 | cDst.height = cSrc.height; 389 | cDst.getContext('2d').drawImage(cSrc, 0, 0); 390 | } 391 | // copy style (without transitions) 392 | var cs = getComputedStyle(src); 393 | for (var i = 0; i < cs.length; i++) { 394 | var key = cs[i]; 395 | if (key.indexOf('transition') < 0) { 396 | dst.style[key] = cs[key]; 397 | } 398 | } 399 | dst.style.pointerEvents = 'none'; 400 | // and repeat for all children 401 | for (var i = 0; i < src.children.length; i++) { 402 | this._copyStyle(src.children[i], dst.children[i]); 403 | } 404 | }; 405 | DragDropTouch.prototype._dispatchEvent = function (e, type, target) { 406 | if (e && target) { 407 | var evt = document.createEvent('Event'), t = e.touches ? e.touches[0] : e; 408 | evt.initEvent(type, true, true); 409 | evt.button = 0; 410 | evt.which = evt.buttons = 1; 411 | this._copyProps(evt, e, DragDropTouch._kbdProps); 412 | this._copyProps(evt, t, DragDropTouch._ptProps); 413 | evt.dataTransfer = this._dataTransfer; 414 | target.dispatchEvent(evt); 415 | return evt.defaultPrevented; 416 | } 417 | return false; 418 | }; 419 | // gets an element's closest draggable ancestor 420 | DragDropTouch.prototype._closestDraggable = function (e) { 421 | for (; e; e = e.parentElement) { 422 | if (e.hasAttribute('draggable') && e.draggable) { 423 | return e; 424 | } 425 | } 426 | return null; 427 | }; 428 | return DragDropTouch; 429 | }()); 430 | /*private*/ DragDropTouch._instance = new DragDropTouch(); // singleton 431 | // constants 432 | DragDropTouch._THRESHOLD = 5; // pixels to move before drag starts 433 | DragDropTouch._OPACITY = 0.5; // drag image opacity 434 | DragDropTouch._DBLCLICK = 500; // max ms between clicks in a double click 435 | DragDropTouch._CTXMENU = 900; // ms to hold before raising 'contextmenu' event 436 | DragDropTouch._ISPRESSHOLDMODE = false; // decides of press & hold mode presence 437 | DragDropTouch._PRESSHOLDAWAIT = 400; // ms to wait before press & hold is detected 438 | DragDropTouch._PRESSHOLDMARGIN = 25; // pixels that finger might shiver while pressing 439 | DragDropTouch._PRESSHOLDTHRESHOLD = 0; // pixels to move before drag starts 440 | // copy styles/attributes from drag source to drag image element 441 | DragDropTouch._rmvAtts = 'id,class,style,draggable'.split(','); 442 | // synthesize and dispatch an event 443 | // returns true if the event has been handled (e.preventDefault == true) 444 | DragDropTouch._kbdProps = 'altKey,ctrlKey,metaKey,shiftKey'.split(','); 445 | DragDropTouch._ptProps = 'pageX,pageY,clientX,clientY,screenX,screenY'.split(','); 446 | DragDropTouch_1.DragDropTouch = DragDropTouch; 447 | })(DragDropTouch || (DragDropTouch = {})); 448 | --------------------------------------------------------------------------------