├── .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 |
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 |
64 |
65 |
66 |
67 |
79 |
80 |
116 |
117 |
134 |
135 |
136 |
137 |
Are you sure?
138 |
All changes will be reset to a base RFD deployment model.
139 |
140 |
146 |
147 |
148 |
149 |
150 |
Current Users
151 |
152 |
154 |
155 |
158 |
159 |
160 |
183 |
184 |
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 |
--------------------------------------------------------------------------------