├── .gitignore ├── Dockerfile ├── README.md ├── client ├── v2 │ ├── background.js │ ├── data │ │ ├── icons │ │ │ ├── 128.png │ │ │ ├── 16.png │ │ │ ├── 19.png │ │ │ ├── 256.png │ │ │ ├── 32.png │ │ │ ├── 38.png │ │ │ ├── 48.png │ │ │ ├── 512.png │ │ │ └── 64.png │ │ ├── options │ │ │ ├── index.html │ │ │ └── index.js │ │ └── window │ │ │ ├── assets │ │ │ └── poster.png │ │ │ ├── components │ │ │ └── video-view.js │ │ │ ├── event.js │ │ │ ├── extra.js │ │ │ ├── extra │ │ │ ├── ReadMe │ │ │ ├── Sortable.js │ │ │ └── notify.js │ │ │ ├── index.css │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ └── meeting │ │ │ ├── meeting.js │ │ │ ├── peer.js │ │ │ └── safe.js │ └── manifest.json └── v3 │ ├── data │ ├── icons │ │ ├── 128.png │ │ ├── 16.png │ │ ├── 19.png │ │ ├── 256.png │ │ ├── 32.png │ │ ├── 38.png │ │ ├── 48.png │ │ ├── 512.png │ │ └── 64.png │ ├── options │ │ ├── index.css │ │ ├── index.html │ │ └── index.js │ └── window │ │ ├── assets │ │ └── poster.png │ │ ├── components │ │ └── video-view.js │ │ ├── event.js │ │ ├── extra.js │ │ ├── extra │ │ ├── ReadMe │ │ ├── Sortable.js │ │ └── notify.js │ │ ├── index.css │ │ ├── index.html │ │ ├── index.js │ │ └── meeting │ │ ├── meeting.js │ │ ├── peer.js │ │ └── safe.js │ ├── manifest.json │ └── worker.js ├── package.json └── server ├── .gitignore └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | .openode 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine 2 | 3 | WORKDIR /opt/app 4 | 5 | ENV PORT=80 6 | 7 | # daemon for cron jobs 8 | RUN echo 'crond' > /boot.sh 9 | # RUN echo 'crontab .openode.cron' >> /boot.sh 10 | 11 | # Install app dependencies 12 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 13 | # where available (npm@5+) 14 | 15 | COPY package*.json ./ 16 | 17 | RUN npm install --production 18 | 19 | # Bundle app source 20 | COPY . . 21 | 22 | CMD sh /boot.sh && npm start 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # meeting 2 | a free and secure peer to peer [meeting](https://add0n.com/meeting.html) application 3 | 4 | -> This application uses WebRTC for peer-to-peer streamings 5 | 6 | -> The initial signaling is done through a WebScoket channel (thanks to [replit.com](https://replit.com/@meetingserver)). The data transferred in this channel is encrypted with the given password by the peer. 7 | 8 | -> FAQs page: https://add0n.com/meeting.html 9 | -------------------------------------------------------------------------------- /client/v2/background.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | chrome.browserAction.onClicked.addListener(() => { 4 | chrome.tabs.create({ 5 | url: 'data/window/index.html' 6 | }); 7 | }); 8 | 9 | /* FAQs & Feedback */ 10 | { 11 | const {management, runtime: {onInstalled, setUninstallURL, getManifest}, storage, tabs} = chrome; 12 | if (navigator.webdriver !== true) { 13 | const page = getManifest().homepage_url; 14 | const {name, version} = getManifest(); 15 | onInstalled.addListener(({reason, previousVersion}) => { 16 | management.getSelf(({installType}) => installType === 'normal' && storage.local.get({ 17 | 'faqs': true, 18 | 'last-update': 0 19 | }, prefs => { 20 | if (reason === 'install' || (prefs.faqs && reason === 'update')) { 21 | const doUpdate = (Date.now() - prefs['last-update']) / 1000 / 60 / 60 / 24 > 45; 22 | if (doUpdate && previousVersion !== version) { 23 | tabs.create({ 24 | url: page + '?version=' + version + (previousVersion ? '&p=' + previousVersion : '') + '&type=' + reason, 25 | active: reason === 'install' 26 | }); 27 | storage.local.set({'last-update': Date.now()}); 28 | } 29 | } 30 | })); 31 | }); 32 | setUninstallURL(page + '?rd=feedback&name=' + encodeURIComponent(name) + '&version=' + version); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /client/v2/data/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v2/data/icons/128.png -------------------------------------------------------------------------------- /client/v2/data/icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v2/data/icons/16.png -------------------------------------------------------------------------------- /client/v2/data/icons/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v2/data/icons/19.png -------------------------------------------------------------------------------- /client/v2/data/icons/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v2/data/icons/256.png -------------------------------------------------------------------------------- /client/v2/data/icons/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v2/data/icons/32.png -------------------------------------------------------------------------------- /client/v2/data/icons/38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v2/data/icons/38.png -------------------------------------------------------------------------------- /client/v2/data/icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v2/data/icons/48.png -------------------------------------------------------------------------------- /client/v2/data/icons/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v2/data/icons/512.png -------------------------------------------------------------------------------- /client/v2/data/icons/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v2/data/icons/64.png -------------------------------------------------------------------------------- /client/v2/data/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Options Page :: Meeting 5 | 6 | 18 | 19 |
20 | Signaling Server 21 | 22 | Signaling Token 23 | 24 |
25 |

26 | 27 | - 28 | 29 |

30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /client/v2/data/options/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const toast = document.getElementById('toast'); 4 | 5 | chrome.storage.local.get({ 6 | 'signaling-server': '', 7 | 'signaling-token': '' 8 | }, prefs => { 9 | document.getElementById('signaling-server').value = prefs['signaling-server'] || 'wss://connect.meetingserver.repl.co/[CHANNEL_ID]?apiKey=[API_KEY]'; 10 | document.getElementById('signaling-token').value = prefs['signaling-token']; 11 | }); 12 | 13 | document.getElementById('save').addEventListener('click', () => chrome.storage.local.set({ 14 | 'signaling-server': document.getElementById('signaling-server').value, 15 | 'signaling-token': document.getElementById('signaling-token').value 16 | }, () => { 17 | toast.textContent = 'Options saved'; 18 | window.setTimeout(() => toast.textContent = '', 750); 19 | })); 20 | 21 | // reset 22 | document.getElementById('reset').addEventListener('click', e => { 23 | if (e.detail === 1) { 24 | toast.textContent = 'Double-click to reset!'; 25 | window.setTimeout(() => toast.textContent = '', 750); 26 | } 27 | else { 28 | localStorage.clear(); 29 | chrome.storage.local.clear(() => { 30 | chrome.runtime.reload(); 31 | window.close(); 32 | }); 33 | } 34 | }); 35 | // support 36 | document.getElementById('support').addEventListener('click', () => chrome.tabs.create({ 37 | url: chrome.runtime.getManifest().homepage_url + '?rd=donate' 38 | })); 39 | -------------------------------------------------------------------------------- /client/v2/data/window/assets/poster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v2/data/window/assets/poster.png -------------------------------------------------------------------------------- /client/v2/data/window/components/video-view.js: -------------------------------------------------------------------------------- 1 | class VideoView extends HTMLElement { 2 | constructor() { 3 | super(); 4 | const shadow = this.attachShadow({ 5 | mode: 'open' 6 | }); 7 | shadow.innerHTML = ` 8 | 64 |
65 | 66 | 67 | 68 | 69 |
70 | 71 | 72 | 73 |
74 |
75 | `; 76 | this.onCloseRequested = new Event(); 77 | } 78 | connectedCallback() { 79 | this.shadowRoot.getElementById('close').addEventListener('click', () => { 80 | this.onCloseRequested.emit(); 81 | }); 82 | this.shadowRoot.getElementById('mute').addEventListener('click', () => { 83 | const v = this.querySelector('video'); 84 | v.muted = v.muted === false; 85 | this.dataset.muted = v.muted; 86 | }); 87 | // remove the entire element if video element is removed 88 | this.shadowRoot.querySelector('slot').addEventListener('slotchange', () => { 89 | const v = this.querySelector('video'); 90 | if (!v) { 91 | this.remove(); 92 | } 93 | }); 94 | } 95 | set(video) { 96 | this.appendChild(video); 97 | } 98 | set srcObject(o) { 99 | this.querySelector('video').srcObject = o; 100 | } 101 | play() { 102 | this.querySelector('video').play(); 103 | } 104 | set onloadedmetadata(c) { 105 | this.querySelector('video').onloadedmetadata = c; 106 | } 107 | } 108 | window.customElements.define('video-view', VideoView); 109 | -------------------------------------------------------------------------------- /client/v2/data/window/event.js: -------------------------------------------------------------------------------- 1 | class Event { 2 | constructor() { 3 | this.callbacks = []; 4 | } 5 | addListener(c) { 6 | this.callbacks.push(c); 7 | } 8 | emit(...args) { 9 | for (const c of this.callbacks) { 10 | c(...args); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/v2/data/window/extra.js: -------------------------------------------------------------------------------- 1 | /* global Sortable */ 2 | 'use strict'; 3 | 4 | Sortable.create(document.getElementById('meeting')); 5 | 6 | -------------------------------------------------------------------------------- /client/v2/data/window/extra/ReadMe: -------------------------------------------------------------------------------- 1 | https://github.com/SortableJS/Sortable/releases/tag/1.10.2 2 | -------------------------------------------------------------------------------- /client/v2/data/window/extra/Sortable.js: -------------------------------------------------------------------------------- 1 | /**! 2 | * Sortable 1.10.2 3 | * @author RubaXa 4 | * @author owenm 5 | * @license MIT 6 | */ 7 | (function (global, factory) { 8 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 9 | typeof define === 'function' && define.amd ? define(factory) : 10 | (global = global || self, global.Sortable = factory()); 11 | }(this, function () { 'use strict'; 12 | 13 | function _typeof(obj) { 14 | if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { 15 | _typeof = function (obj) { 16 | return typeof obj; 17 | }; 18 | } else { 19 | _typeof = function (obj) { 20 | return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; 21 | }; 22 | } 23 | 24 | return _typeof(obj); 25 | } 26 | 27 | function _defineProperty(obj, key, value) { 28 | if (key in obj) { 29 | Object.defineProperty(obj, key, { 30 | value: value, 31 | enumerable: true, 32 | configurable: true, 33 | writable: true 34 | }); 35 | } else { 36 | obj[key] = value; 37 | } 38 | 39 | return obj; 40 | } 41 | 42 | function _extends() { 43 | _extends = Object.assign || function (target) { 44 | for (var i = 1; i < arguments.length; i++) { 45 | var source = arguments[i]; 46 | 47 | for (var key in source) { 48 | if (Object.prototype.hasOwnProperty.call(source, key)) { 49 | target[key] = source[key]; 50 | } 51 | } 52 | } 53 | 54 | return target; 55 | }; 56 | 57 | return _extends.apply(this, arguments); 58 | } 59 | 60 | function _objectSpread(target) { 61 | for (var i = 1; i < arguments.length; i++) { 62 | var source = arguments[i] != null ? arguments[i] : {}; 63 | var ownKeys = Object.keys(source); 64 | 65 | if (typeof Object.getOwnPropertySymbols === 'function') { 66 | ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { 67 | return Object.getOwnPropertyDescriptor(source, sym).enumerable; 68 | })); 69 | } 70 | 71 | ownKeys.forEach(function (key) { 72 | _defineProperty(target, key, source[key]); 73 | }); 74 | } 75 | 76 | return target; 77 | } 78 | 79 | function _objectWithoutPropertiesLoose(source, excluded) { 80 | if (source == null) return {}; 81 | var target = {}; 82 | var sourceKeys = Object.keys(source); 83 | var key, i; 84 | 85 | for (i = 0; i < sourceKeys.length; i++) { 86 | key = sourceKeys[i]; 87 | if (excluded.indexOf(key) >= 0) continue; 88 | target[key] = source[key]; 89 | } 90 | 91 | return target; 92 | } 93 | 94 | function _objectWithoutProperties(source, excluded) { 95 | if (source == null) return {}; 96 | 97 | var target = _objectWithoutPropertiesLoose(source, excluded); 98 | 99 | var key, i; 100 | 101 | if (Object.getOwnPropertySymbols) { 102 | var sourceSymbolKeys = Object.getOwnPropertySymbols(source); 103 | 104 | for (i = 0; i < sourceSymbolKeys.length; i++) { 105 | key = sourceSymbolKeys[i]; 106 | if (excluded.indexOf(key) >= 0) continue; 107 | if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; 108 | target[key] = source[key]; 109 | } 110 | } 111 | 112 | return target; 113 | } 114 | 115 | function _toConsumableArray(arr) { 116 | return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); 117 | } 118 | 119 | function _arrayWithoutHoles(arr) { 120 | if (Array.isArray(arr)) { 121 | for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; 122 | 123 | return arr2; 124 | } 125 | } 126 | 127 | function _iterableToArray(iter) { 128 | if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); 129 | } 130 | 131 | function _nonIterableSpread() { 132 | throw new TypeError("Invalid attempt to spread non-iterable instance"); 133 | } 134 | 135 | var version = "1.10.2"; 136 | 137 | function userAgent(pattern) { 138 | if (typeof window !== 'undefined' && window.navigator) { 139 | return !! 140 | /*@__PURE__*/ 141 | navigator.userAgent.match(pattern); 142 | } 143 | } 144 | 145 | var IE11OrLess = userAgent(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i); 146 | var Edge = userAgent(/Edge/i); 147 | var FireFox = userAgent(/firefox/i); 148 | var Safari = userAgent(/safari/i) && !userAgent(/chrome/i) && !userAgent(/android/i); 149 | var IOS = userAgent(/iP(ad|od|hone)/i); 150 | var ChromeForAndroid = userAgent(/chrome/i) && userAgent(/android/i); 151 | 152 | var captureMode = { 153 | capture: false, 154 | passive: false 155 | }; 156 | 157 | function on(el, event, fn) { 158 | el.addEventListener(event, fn, !IE11OrLess && captureMode); 159 | } 160 | 161 | function off(el, event, fn) { 162 | el.removeEventListener(event, fn, !IE11OrLess && captureMode); 163 | } 164 | 165 | function matches( 166 | /**HTMLElement*/ 167 | el, 168 | /**String*/ 169 | selector) { 170 | if (!selector) return; 171 | selector[0] === '>' && (selector = selector.substring(1)); 172 | 173 | if (el) { 174 | try { 175 | if (el.matches) { 176 | return el.matches(selector); 177 | } else if (el.msMatchesSelector) { 178 | return el.msMatchesSelector(selector); 179 | } else if (el.webkitMatchesSelector) { 180 | return el.webkitMatchesSelector(selector); 181 | } 182 | } catch (_) { 183 | return false; 184 | } 185 | } 186 | 187 | return false; 188 | } 189 | 190 | function getParentOrHost(el) { 191 | return el.host && el !== document && el.host.nodeType ? el.host : el.parentNode; 192 | } 193 | 194 | function closest( 195 | /**HTMLElement*/ 196 | el, 197 | /**String*/ 198 | selector, 199 | /**HTMLElement*/ 200 | ctx, includeCTX) { 201 | if (el) { 202 | ctx = ctx || document; 203 | 204 | do { 205 | if (selector != null && (selector[0] === '>' ? el.parentNode === ctx && matches(el, selector) : matches(el, selector)) || includeCTX && el === ctx) { 206 | return el; 207 | } 208 | 209 | if (el === ctx) break; 210 | /* jshint boss:true */ 211 | } while (el = getParentOrHost(el)); 212 | } 213 | 214 | return null; 215 | } 216 | 217 | var R_SPACE = /\s+/g; 218 | 219 | function toggleClass(el, name, state) { 220 | if (el && name) { 221 | if (el.classList) { 222 | el.classList[state ? 'add' : 'remove'](name); 223 | } else { 224 | var className = (' ' + el.className + ' ').replace(R_SPACE, ' ').replace(' ' + name + ' ', ' '); 225 | el.className = (className + (state ? ' ' + name : '')).replace(R_SPACE, ' '); 226 | } 227 | } 228 | } 229 | 230 | function css(el, prop, val) { 231 | var style = el && el.style; 232 | 233 | if (style) { 234 | if (val === void 0) { 235 | if (document.defaultView && document.defaultView.getComputedStyle) { 236 | val = document.defaultView.getComputedStyle(el, ''); 237 | } else if (el.currentStyle) { 238 | val = el.currentStyle; 239 | } 240 | 241 | return prop === void 0 ? val : val[prop]; 242 | } else { 243 | if (!(prop in style) && prop.indexOf('webkit') === -1) { 244 | prop = '-webkit-' + prop; 245 | } 246 | 247 | style[prop] = val + (typeof val === 'string' ? '' : 'px'); 248 | } 249 | } 250 | } 251 | 252 | function matrix(el, selfOnly) { 253 | var appliedTransforms = ''; 254 | 255 | if (typeof el === 'string') { 256 | appliedTransforms = el; 257 | } else { 258 | do { 259 | var transform = css(el, 'transform'); 260 | 261 | if (transform && transform !== 'none') { 262 | appliedTransforms = transform + ' ' + appliedTransforms; 263 | } 264 | /* jshint boss:true */ 265 | 266 | } while (!selfOnly && (el = el.parentNode)); 267 | } 268 | 269 | var matrixFn = window.DOMMatrix || window.WebKitCSSMatrix || window.CSSMatrix || window.MSCSSMatrix; 270 | /*jshint -W056 */ 271 | 272 | return matrixFn && new matrixFn(appliedTransforms); 273 | } 274 | 275 | function find(ctx, tagName, iterator) { 276 | if (ctx) { 277 | var list = ctx.getElementsByTagName(tagName), 278 | i = 0, 279 | n = list.length; 280 | 281 | if (iterator) { 282 | for (; i < n; i++) { 283 | iterator(list[i], i); 284 | } 285 | } 286 | 287 | return list; 288 | } 289 | 290 | return []; 291 | } 292 | 293 | function getWindowScrollingElement() { 294 | var scrollingElement = document.scrollingElement; 295 | 296 | if (scrollingElement) { 297 | return scrollingElement; 298 | } else { 299 | return document.documentElement; 300 | } 301 | } 302 | /** 303 | * Returns the "bounding client rect" of given element 304 | * @param {HTMLElement} el The element whose boundingClientRect is wanted 305 | * @param {[Boolean]} relativeToContainingBlock Whether the rect should be relative to the containing block of (including) the container 306 | * @param {[Boolean]} relativeToNonStaticParent Whether the rect should be relative to the relative parent of (including) the contaienr 307 | * @param {[Boolean]} undoScale Whether the container's scale() should be undone 308 | * @param {[HTMLElement]} container The parent the element will be placed in 309 | * @return {Object} The boundingClientRect of el, with specified adjustments 310 | */ 311 | 312 | 313 | function getRect(el, relativeToContainingBlock, relativeToNonStaticParent, undoScale, container) { 314 | if (!el.getBoundingClientRect && el !== window) return; 315 | var elRect, top, left, bottom, right, height, width; 316 | 317 | if (el !== window && el !== getWindowScrollingElement()) { 318 | elRect = el.getBoundingClientRect(); 319 | top = elRect.top; 320 | left = elRect.left; 321 | bottom = elRect.bottom; 322 | right = elRect.right; 323 | height = elRect.height; 324 | width = elRect.width; 325 | } else { 326 | top = 0; 327 | left = 0; 328 | bottom = window.innerHeight; 329 | right = window.innerWidth; 330 | height = window.innerHeight; 331 | width = window.innerWidth; 332 | } 333 | 334 | if ((relativeToContainingBlock || relativeToNonStaticParent) && el !== window) { 335 | // Adjust for translate() 336 | container = container || el.parentNode; // solves #1123 (see: https://stackoverflow.com/a/37953806/6088312) 337 | // Not needed on <= IE11 338 | 339 | if (!IE11OrLess) { 340 | do { 341 | if (container && container.getBoundingClientRect && (css(container, 'transform') !== 'none' || relativeToNonStaticParent && css(container, 'position') !== 'static')) { 342 | var containerRect = container.getBoundingClientRect(); // Set relative to edges of padding box of container 343 | 344 | top -= containerRect.top + parseInt(css(container, 'border-top-width')); 345 | left -= containerRect.left + parseInt(css(container, 'border-left-width')); 346 | bottom = top + elRect.height; 347 | right = left + elRect.width; 348 | break; 349 | } 350 | /* jshint boss:true */ 351 | 352 | } while (container = container.parentNode); 353 | } 354 | } 355 | 356 | if (undoScale && el !== window) { 357 | // Adjust for scale() 358 | var elMatrix = matrix(container || el), 359 | scaleX = elMatrix && elMatrix.a, 360 | scaleY = elMatrix && elMatrix.d; 361 | 362 | if (elMatrix) { 363 | top /= scaleY; 364 | left /= scaleX; 365 | width /= scaleX; 366 | height /= scaleY; 367 | bottom = top + height; 368 | right = left + width; 369 | } 370 | } 371 | 372 | return { 373 | top: top, 374 | left: left, 375 | bottom: bottom, 376 | right: right, 377 | width: width, 378 | height: height 379 | }; 380 | } 381 | /** 382 | * Checks if a side of an element is scrolled past a side of its parents 383 | * @param {HTMLElement} el The element who's side being scrolled out of view is in question 384 | * @param {String} elSide Side of the element in question ('top', 'left', 'right', 'bottom') 385 | * @param {String} parentSide Side of the parent in question ('top', 'left', 'right', 'bottom') 386 | * @return {HTMLElement} The parent scroll element that the el's side is scrolled past, or null if there is no such element 387 | */ 388 | 389 | 390 | function isScrolledPast(el, elSide, parentSide) { 391 | var parent = getParentAutoScrollElement(el, true), 392 | elSideVal = getRect(el)[elSide]; 393 | /* jshint boss:true */ 394 | 395 | while (parent) { 396 | var parentSideVal = getRect(parent)[parentSide], 397 | visible = void 0; 398 | 399 | if (parentSide === 'top' || parentSide === 'left') { 400 | visible = elSideVal >= parentSideVal; 401 | } else { 402 | visible = elSideVal <= parentSideVal; 403 | } 404 | 405 | if (!visible) return parent; 406 | if (parent === getWindowScrollingElement()) break; 407 | parent = getParentAutoScrollElement(parent, false); 408 | } 409 | 410 | return false; 411 | } 412 | /** 413 | * Gets nth child of el, ignoring hidden children, sortable's elements (does not ignore clone if it's visible) 414 | * and non-draggable elements 415 | * @param {HTMLElement} el The parent element 416 | * @param {Number} childNum The index of the child 417 | * @param {Object} options Parent Sortable's options 418 | * @return {HTMLElement} The child at index childNum, or null if not found 419 | */ 420 | 421 | 422 | function getChild(el, childNum, options) { 423 | var currentChild = 0, 424 | i = 0, 425 | children = el.children; 426 | 427 | while (i < children.length) { 428 | if (children[i].style.display !== 'none' && children[i] !== Sortable.ghost && children[i] !== Sortable.dragged && closest(children[i], options.draggable, el, false)) { 429 | if (currentChild === childNum) { 430 | return children[i]; 431 | } 432 | 433 | currentChild++; 434 | } 435 | 436 | i++; 437 | } 438 | 439 | return null; 440 | } 441 | /** 442 | * Gets the last child in the el, ignoring ghostEl or invisible elements (clones) 443 | * @param {HTMLElement} el Parent element 444 | * @param {selector} selector Any other elements that should be ignored 445 | * @return {HTMLElement} The last child, ignoring ghostEl 446 | */ 447 | 448 | 449 | function lastChild(el, selector) { 450 | var last = el.lastElementChild; 451 | 452 | while (last && (last === Sortable.ghost || css(last, 'display') === 'none' || selector && !matches(last, selector))) { 453 | last = last.previousElementSibling; 454 | } 455 | 456 | return last || null; 457 | } 458 | /** 459 | * Returns the index of an element within its parent for a selected set of 460 | * elements 461 | * @param {HTMLElement} el 462 | * @param {selector} selector 463 | * @return {number} 464 | */ 465 | 466 | 467 | function index(el, selector) { 468 | var index = 0; 469 | 470 | if (!el || !el.parentNode) { 471 | return -1; 472 | } 473 | /* jshint boss:true */ 474 | 475 | 476 | while (el = el.previousElementSibling) { 477 | if (el.nodeName.toUpperCase() !== 'TEMPLATE' && el !== Sortable.clone && (!selector || matches(el, selector))) { 478 | index++; 479 | } 480 | } 481 | 482 | return index; 483 | } 484 | /** 485 | * Returns the scroll offset of the given element, added with all the scroll offsets of parent elements. 486 | * The value is returned in real pixels. 487 | * @param {HTMLElement} el 488 | * @return {Array} Offsets in the format of [left, top] 489 | */ 490 | 491 | 492 | function getRelativeScrollOffset(el) { 493 | var offsetLeft = 0, 494 | offsetTop = 0, 495 | winScroller = getWindowScrollingElement(); 496 | 497 | if (el) { 498 | do { 499 | var elMatrix = matrix(el), 500 | scaleX = elMatrix.a, 501 | scaleY = elMatrix.d; 502 | offsetLeft += el.scrollLeft * scaleX; 503 | offsetTop += el.scrollTop * scaleY; 504 | } while (el !== winScroller && (el = el.parentNode)); 505 | } 506 | 507 | return [offsetLeft, offsetTop]; 508 | } 509 | /** 510 | * Returns the index of the object within the given array 511 | * @param {Array} arr Array that may or may not hold the object 512 | * @param {Object} obj An object that has a key-value pair unique to and identical to a key-value pair in the object you want to find 513 | * @return {Number} The index of the object in the array, or -1 514 | */ 515 | 516 | 517 | function indexOfObject(arr, obj) { 518 | for (var i in arr) { 519 | if (!arr.hasOwnProperty(i)) continue; 520 | 521 | for (var key in obj) { 522 | if (obj.hasOwnProperty(key) && obj[key] === arr[i][key]) return Number(i); 523 | } 524 | } 525 | 526 | return -1; 527 | } 528 | 529 | function getParentAutoScrollElement(el, includeSelf) { 530 | // skip to window 531 | if (!el || !el.getBoundingClientRect) return getWindowScrollingElement(); 532 | var elem = el; 533 | var gotSelf = false; 534 | 535 | do { 536 | // we don't need to get elem css if it isn't even overflowing in the first place (performance) 537 | if (elem.clientWidth < elem.scrollWidth || elem.clientHeight < elem.scrollHeight) { 538 | var elemCSS = css(elem); 539 | 540 | if (elem.clientWidth < elem.scrollWidth && (elemCSS.overflowX == 'auto' || elemCSS.overflowX == 'scroll') || elem.clientHeight < elem.scrollHeight && (elemCSS.overflowY == 'auto' || elemCSS.overflowY == 'scroll')) { 541 | if (!elem.getBoundingClientRect || elem === document.body) return getWindowScrollingElement(); 542 | if (gotSelf || includeSelf) return elem; 543 | gotSelf = true; 544 | } 545 | } 546 | /* jshint boss:true */ 547 | 548 | } while (elem = elem.parentNode); 549 | 550 | return getWindowScrollingElement(); 551 | } 552 | 553 | function extend(dst, src) { 554 | if (dst && src) { 555 | for (var key in src) { 556 | if (src.hasOwnProperty(key)) { 557 | dst[key] = src[key]; 558 | } 559 | } 560 | } 561 | 562 | return dst; 563 | } 564 | 565 | function isRectEqual(rect1, rect2) { 566 | return Math.round(rect1.top) === Math.round(rect2.top) && Math.round(rect1.left) === Math.round(rect2.left) && Math.round(rect1.height) === Math.round(rect2.height) && Math.round(rect1.width) === Math.round(rect2.width); 567 | } 568 | 569 | var _throttleTimeout; 570 | 571 | function throttle(callback, ms) { 572 | return function () { 573 | if (!_throttleTimeout) { 574 | var args = arguments, 575 | _this = this; 576 | 577 | if (args.length === 1) { 578 | callback.call(_this, args[0]); 579 | } else { 580 | callback.apply(_this, args); 581 | } 582 | 583 | _throttleTimeout = setTimeout(function () { 584 | _throttleTimeout = void 0; 585 | }, ms); 586 | } 587 | }; 588 | } 589 | 590 | function cancelThrottle() { 591 | clearTimeout(_throttleTimeout); 592 | _throttleTimeout = void 0; 593 | } 594 | 595 | function scrollBy(el, x, y) { 596 | el.scrollLeft += x; 597 | el.scrollTop += y; 598 | } 599 | 600 | function clone(el) { 601 | var Polymer = window.Polymer; 602 | var $ = window.jQuery || window.Zepto; 603 | 604 | if (Polymer && Polymer.dom) { 605 | return Polymer.dom(el).cloneNode(true); 606 | } else if ($) { 607 | return $(el).clone(true)[0]; 608 | } else { 609 | return el.cloneNode(true); 610 | } 611 | } 612 | 613 | function setRect(el, rect) { 614 | css(el, 'position', 'absolute'); 615 | css(el, 'top', rect.top); 616 | css(el, 'left', rect.left); 617 | css(el, 'width', rect.width); 618 | css(el, 'height', rect.height); 619 | } 620 | 621 | function unsetRect(el) { 622 | css(el, 'position', ''); 623 | css(el, 'top', ''); 624 | css(el, 'left', ''); 625 | css(el, 'width', ''); 626 | css(el, 'height', ''); 627 | } 628 | 629 | var expando = 'Sortable' + new Date().getTime(); 630 | 631 | function AnimationStateManager() { 632 | var animationStates = [], 633 | animationCallbackId; 634 | return { 635 | captureAnimationState: function captureAnimationState() { 636 | animationStates = []; 637 | if (!this.options.animation) return; 638 | var children = [].slice.call(this.el.children); 639 | children.forEach(function (child) { 640 | if (css(child, 'display') === 'none' || child === Sortable.ghost) return; 641 | animationStates.push({ 642 | target: child, 643 | rect: getRect(child) 644 | }); 645 | 646 | var fromRect = _objectSpread({}, animationStates[animationStates.length - 1].rect); // If animating: compensate for current animation 647 | 648 | 649 | if (child.thisAnimationDuration) { 650 | var childMatrix = matrix(child, true); 651 | 652 | if (childMatrix) { 653 | fromRect.top -= childMatrix.f; 654 | fromRect.left -= childMatrix.e; 655 | } 656 | } 657 | 658 | child.fromRect = fromRect; 659 | }); 660 | }, 661 | addAnimationState: function addAnimationState(state) { 662 | animationStates.push(state); 663 | }, 664 | removeAnimationState: function removeAnimationState(target) { 665 | animationStates.splice(indexOfObject(animationStates, { 666 | target: target 667 | }), 1); 668 | }, 669 | animateAll: function animateAll(callback) { 670 | var _this = this; 671 | 672 | if (!this.options.animation) { 673 | clearTimeout(animationCallbackId); 674 | if (typeof callback === 'function') callback(); 675 | return; 676 | } 677 | 678 | var animating = false, 679 | animationTime = 0; 680 | animationStates.forEach(function (state) { 681 | var time = 0, 682 | target = state.target, 683 | fromRect = target.fromRect, 684 | toRect = getRect(target), 685 | prevFromRect = target.prevFromRect, 686 | prevToRect = target.prevToRect, 687 | animatingRect = state.rect, 688 | targetMatrix = matrix(target, true); 689 | 690 | if (targetMatrix) { 691 | // Compensate for current animation 692 | toRect.top -= targetMatrix.f; 693 | toRect.left -= targetMatrix.e; 694 | } 695 | 696 | target.toRect = toRect; 697 | 698 | if (target.thisAnimationDuration) { 699 | // Could also check if animatingRect is between fromRect and toRect 700 | if (isRectEqual(prevFromRect, toRect) && !isRectEqual(fromRect, toRect) && // Make sure animatingRect is on line between toRect & fromRect 701 | (animatingRect.top - toRect.top) / (animatingRect.left - toRect.left) === (fromRect.top - toRect.top) / (fromRect.left - toRect.left)) { 702 | // If returning to same place as started from animation and on same axis 703 | time = calculateRealTime(animatingRect, prevFromRect, prevToRect, _this.options); 704 | } 705 | } // if fromRect != toRect: animate 706 | 707 | 708 | if (!isRectEqual(toRect, fromRect)) { 709 | target.prevFromRect = fromRect; 710 | target.prevToRect = toRect; 711 | 712 | if (!time) { 713 | time = _this.options.animation; 714 | } 715 | 716 | _this.animate(target, animatingRect, toRect, time); 717 | } 718 | 719 | if (time) { 720 | animating = true; 721 | animationTime = Math.max(animationTime, time); 722 | clearTimeout(target.animationResetTimer); 723 | target.animationResetTimer = setTimeout(function () { 724 | target.animationTime = 0; 725 | target.prevFromRect = null; 726 | target.fromRect = null; 727 | target.prevToRect = null; 728 | target.thisAnimationDuration = null; 729 | }, time); 730 | target.thisAnimationDuration = time; 731 | } 732 | }); 733 | clearTimeout(animationCallbackId); 734 | 735 | if (!animating) { 736 | if (typeof callback === 'function') callback(); 737 | } else { 738 | animationCallbackId = setTimeout(function () { 739 | if (typeof callback === 'function') callback(); 740 | }, animationTime); 741 | } 742 | 743 | animationStates = []; 744 | }, 745 | animate: function animate(target, currentRect, toRect, duration) { 746 | if (duration) { 747 | css(target, 'transition', ''); 748 | css(target, 'transform', ''); 749 | var elMatrix = matrix(this.el), 750 | scaleX = elMatrix && elMatrix.a, 751 | scaleY = elMatrix && elMatrix.d, 752 | translateX = (currentRect.left - toRect.left) / (scaleX || 1), 753 | translateY = (currentRect.top - toRect.top) / (scaleY || 1); 754 | target.animatingX = !!translateX; 755 | target.animatingY = !!translateY; 756 | css(target, 'transform', 'translate3d(' + translateX + 'px,' + translateY + 'px,0)'); 757 | repaint(target); // repaint 758 | 759 | css(target, 'transition', 'transform ' + duration + 'ms' + (this.options.easing ? ' ' + this.options.easing : '')); 760 | css(target, 'transform', 'translate3d(0,0,0)'); 761 | typeof target.animated === 'number' && clearTimeout(target.animated); 762 | target.animated = setTimeout(function () { 763 | css(target, 'transition', ''); 764 | css(target, 'transform', ''); 765 | target.animated = false; 766 | target.animatingX = false; 767 | target.animatingY = false; 768 | }, duration); 769 | } 770 | } 771 | }; 772 | } 773 | 774 | function repaint(target) { 775 | return target.offsetWidth; 776 | } 777 | 778 | function calculateRealTime(animatingRect, fromRect, toRect, options) { 779 | return Math.sqrt(Math.pow(fromRect.top - animatingRect.top, 2) + Math.pow(fromRect.left - animatingRect.left, 2)) / Math.sqrt(Math.pow(fromRect.top - toRect.top, 2) + Math.pow(fromRect.left - toRect.left, 2)) * options.animation; 780 | } 781 | 782 | var plugins = []; 783 | var defaults = { 784 | initializeByDefault: true 785 | }; 786 | var PluginManager = { 787 | mount: function mount(plugin) { 788 | // Set default static properties 789 | for (var option in defaults) { 790 | if (defaults.hasOwnProperty(option) && !(option in plugin)) { 791 | plugin[option] = defaults[option]; 792 | } 793 | } 794 | 795 | plugins.push(plugin); 796 | }, 797 | pluginEvent: function pluginEvent(eventName, sortable, evt) { 798 | var _this = this; 799 | 800 | this.eventCanceled = false; 801 | 802 | evt.cancel = function () { 803 | _this.eventCanceled = true; 804 | }; 805 | 806 | var eventNameGlobal = eventName + 'Global'; 807 | plugins.forEach(function (plugin) { 808 | if (!sortable[plugin.pluginName]) return; // Fire global events if it exists in this sortable 809 | 810 | if (sortable[plugin.pluginName][eventNameGlobal]) { 811 | sortable[plugin.pluginName][eventNameGlobal](_objectSpread({ 812 | sortable: sortable 813 | }, evt)); 814 | } // Only fire plugin event if plugin is enabled in this sortable, 815 | // and plugin has event defined 816 | 817 | 818 | if (sortable.options[plugin.pluginName] && sortable[plugin.pluginName][eventName]) { 819 | sortable[plugin.pluginName][eventName](_objectSpread({ 820 | sortable: sortable 821 | }, evt)); 822 | } 823 | }); 824 | }, 825 | initializePlugins: function initializePlugins(sortable, el, defaults, options) { 826 | plugins.forEach(function (plugin) { 827 | var pluginName = plugin.pluginName; 828 | if (!sortable.options[pluginName] && !plugin.initializeByDefault) return; 829 | var initialized = new plugin(sortable, el, sortable.options); 830 | initialized.sortable = sortable; 831 | initialized.options = sortable.options; 832 | sortable[pluginName] = initialized; // Add default options from plugin 833 | 834 | _extends(defaults, initialized.defaults); 835 | }); 836 | 837 | for (var option in sortable.options) { 838 | if (!sortable.options.hasOwnProperty(option)) continue; 839 | var modified = this.modifyOption(sortable, option, sortable.options[option]); 840 | 841 | if (typeof modified !== 'undefined') { 842 | sortable.options[option] = modified; 843 | } 844 | } 845 | }, 846 | getEventProperties: function getEventProperties(name, sortable) { 847 | var eventProperties = {}; 848 | plugins.forEach(function (plugin) { 849 | if (typeof plugin.eventProperties !== 'function') return; 850 | 851 | _extends(eventProperties, plugin.eventProperties.call(sortable[plugin.pluginName], name)); 852 | }); 853 | return eventProperties; 854 | }, 855 | modifyOption: function modifyOption(sortable, name, value) { 856 | var modifiedValue; 857 | plugins.forEach(function (plugin) { 858 | // Plugin must exist on the Sortable 859 | if (!sortable[plugin.pluginName]) return; // If static option listener exists for this option, call in the context of the Sortable's instance of this plugin 860 | 861 | if (plugin.optionListeners && typeof plugin.optionListeners[name] === 'function') { 862 | modifiedValue = plugin.optionListeners[name].call(sortable[plugin.pluginName], value); 863 | } 864 | }); 865 | return modifiedValue; 866 | } 867 | }; 868 | 869 | function dispatchEvent(_ref) { 870 | var sortable = _ref.sortable, 871 | rootEl = _ref.rootEl, 872 | name = _ref.name, 873 | targetEl = _ref.targetEl, 874 | cloneEl = _ref.cloneEl, 875 | toEl = _ref.toEl, 876 | fromEl = _ref.fromEl, 877 | oldIndex = _ref.oldIndex, 878 | newIndex = _ref.newIndex, 879 | oldDraggableIndex = _ref.oldDraggableIndex, 880 | newDraggableIndex = _ref.newDraggableIndex, 881 | originalEvent = _ref.originalEvent, 882 | putSortable = _ref.putSortable, 883 | extraEventProperties = _ref.extraEventProperties; 884 | sortable = sortable || rootEl && rootEl[expando]; 885 | if (!sortable) return; 886 | var evt, 887 | options = sortable.options, 888 | onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1); // Support for new CustomEvent feature 889 | 890 | if (window.CustomEvent && !IE11OrLess && !Edge) { 891 | evt = new CustomEvent(name, { 892 | bubbles: true, 893 | cancelable: true 894 | }); 895 | } else { 896 | evt = document.createEvent('Event'); 897 | evt.initEvent(name, true, true); 898 | } 899 | 900 | evt.to = toEl || rootEl; 901 | evt.from = fromEl || rootEl; 902 | evt.item = targetEl || rootEl; 903 | evt.clone = cloneEl; 904 | evt.oldIndex = oldIndex; 905 | evt.newIndex = newIndex; 906 | evt.oldDraggableIndex = oldDraggableIndex; 907 | evt.newDraggableIndex = newDraggableIndex; 908 | evt.originalEvent = originalEvent; 909 | evt.pullMode = putSortable ? putSortable.lastPutMode : undefined; 910 | 911 | var allEventProperties = _objectSpread({}, extraEventProperties, PluginManager.getEventProperties(name, sortable)); 912 | 913 | for (var option in allEventProperties) { 914 | evt[option] = allEventProperties[option]; 915 | } 916 | 917 | if (rootEl) { 918 | rootEl.dispatchEvent(evt); 919 | } 920 | 921 | if (options[onName]) { 922 | options[onName].call(sortable, evt); 923 | } 924 | } 925 | 926 | var pluginEvent = function pluginEvent(eventName, sortable) { 927 | var _ref = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}, 928 | originalEvent = _ref.evt, 929 | data = _objectWithoutProperties(_ref, ["evt"]); 930 | 931 | PluginManager.pluginEvent.bind(Sortable)(eventName, sortable, _objectSpread({ 932 | dragEl: dragEl, 933 | parentEl: parentEl, 934 | ghostEl: ghostEl, 935 | rootEl: rootEl, 936 | nextEl: nextEl, 937 | lastDownEl: lastDownEl, 938 | cloneEl: cloneEl, 939 | cloneHidden: cloneHidden, 940 | dragStarted: moved, 941 | putSortable: putSortable, 942 | activeSortable: Sortable.active, 943 | originalEvent: originalEvent, 944 | oldIndex: oldIndex, 945 | oldDraggableIndex: oldDraggableIndex, 946 | newIndex: newIndex, 947 | newDraggableIndex: newDraggableIndex, 948 | hideGhostForTarget: _hideGhostForTarget, 949 | unhideGhostForTarget: _unhideGhostForTarget, 950 | cloneNowHidden: function cloneNowHidden() { 951 | cloneHidden = true; 952 | }, 953 | cloneNowShown: function cloneNowShown() { 954 | cloneHidden = false; 955 | }, 956 | dispatchSortableEvent: function dispatchSortableEvent(name) { 957 | _dispatchEvent({ 958 | sortable: sortable, 959 | name: name, 960 | originalEvent: originalEvent 961 | }); 962 | } 963 | }, data)); 964 | }; 965 | 966 | function _dispatchEvent(info) { 967 | dispatchEvent(_objectSpread({ 968 | putSortable: putSortable, 969 | cloneEl: cloneEl, 970 | targetEl: dragEl, 971 | rootEl: rootEl, 972 | oldIndex: oldIndex, 973 | oldDraggableIndex: oldDraggableIndex, 974 | newIndex: newIndex, 975 | newDraggableIndex: newDraggableIndex 976 | }, info)); 977 | } 978 | 979 | var dragEl, 980 | parentEl, 981 | ghostEl, 982 | rootEl, 983 | nextEl, 984 | lastDownEl, 985 | cloneEl, 986 | cloneHidden, 987 | oldIndex, 988 | newIndex, 989 | oldDraggableIndex, 990 | newDraggableIndex, 991 | activeGroup, 992 | putSortable, 993 | awaitingDragStarted = false, 994 | ignoreNextClick = false, 995 | sortables = [], 996 | tapEvt, 997 | touchEvt, 998 | lastDx, 999 | lastDy, 1000 | tapDistanceLeft, 1001 | tapDistanceTop, 1002 | moved, 1003 | lastTarget, 1004 | lastDirection, 1005 | pastFirstInvertThresh = false, 1006 | isCircumstantialInvert = false, 1007 | targetMoveDistance, 1008 | // For positioning ghost absolutely 1009 | ghostRelativeParent, 1010 | ghostRelativeParentInitialScroll = [], 1011 | // (left, top) 1012 | _silent = false, 1013 | savedInputChecked = []; 1014 | /** @const */ 1015 | 1016 | var documentExists = typeof document !== 'undefined', 1017 | PositionGhostAbsolutely = IOS, 1018 | CSSFloatProperty = Edge || IE11OrLess ? 'cssFloat' : 'float', 1019 | // This will not pass for IE9, because IE9 DnD only works on anchors 1020 | supportDraggable = documentExists && !ChromeForAndroid && !IOS && 'draggable' in document.createElement('div'), 1021 | supportCssPointerEvents = function () { 1022 | if (!documentExists) return; // false when <= IE11 1023 | 1024 | if (IE11OrLess) { 1025 | return false; 1026 | } 1027 | 1028 | var el = document.createElement('x'); 1029 | el.style.cssText = 'pointer-events:auto'; 1030 | return el.style.pointerEvents === 'auto'; 1031 | }(), 1032 | _detectDirection = function _detectDirection(el, options) { 1033 | var elCSS = css(el), 1034 | elWidth = parseInt(elCSS.width) - parseInt(elCSS.paddingLeft) - parseInt(elCSS.paddingRight) - parseInt(elCSS.borderLeftWidth) - parseInt(elCSS.borderRightWidth), 1035 | child1 = getChild(el, 0, options), 1036 | child2 = getChild(el, 1, options), 1037 | firstChildCSS = child1 && css(child1), 1038 | secondChildCSS = child2 && css(child2), 1039 | firstChildWidth = firstChildCSS && parseInt(firstChildCSS.marginLeft) + parseInt(firstChildCSS.marginRight) + getRect(child1).width, 1040 | secondChildWidth = secondChildCSS && parseInt(secondChildCSS.marginLeft) + parseInt(secondChildCSS.marginRight) + getRect(child2).width; 1041 | 1042 | if (elCSS.display === 'flex') { 1043 | return elCSS.flexDirection === 'column' || elCSS.flexDirection === 'column-reverse' ? 'vertical' : 'horizontal'; 1044 | } 1045 | 1046 | if (elCSS.display === 'grid') { 1047 | return elCSS.gridTemplateColumns.split(' ').length <= 1 ? 'vertical' : 'horizontal'; 1048 | } 1049 | 1050 | if (child1 && firstChildCSS["float"] && firstChildCSS["float"] !== 'none') { 1051 | var touchingSideChild2 = firstChildCSS["float"] === 'left' ? 'left' : 'right'; 1052 | return child2 && (secondChildCSS.clear === 'both' || secondChildCSS.clear === touchingSideChild2) ? 'vertical' : 'horizontal'; 1053 | } 1054 | 1055 | return child1 && (firstChildCSS.display === 'block' || firstChildCSS.display === 'flex' || firstChildCSS.display === 'table' || firstChildCSS.display === 'grid' || firstChildWidth >= elWidth && elCSS[CSSFloatProperty] === 'none' || child2 && elCSS[CSSFloatProperty] === 'none' && firstChildWidth + secondChildWidth > elWidth) ? 'vertical' : 'horizontal'; 1056 | }, 1057 | _dragElInRowColumn = function _dragElInRowColumn(dragRect, targetRect, vertical) { 1058 | var dragElS1Opp = vertical ? dragRect.left : dragRect.top, 1059 | dragElS2Opp = vertical ? dragRect.right : dragRect.bottom, 1060 | dragElOppLength = vertical ? dragRect.width : dragRect.height, 1061 | targetS1Opp = vertical ? targetRect.left : targetRect.top, 1062 | targetS2Opp = vertical ? targetRect.right : targetRect.bottom, 1063 | targetOppLength = vertical ? targetRect.width : targetRect.height; 1064 | return dragElS1Opp === targetS1Opp || dragElS2Opp === targetS2Opp || dragElS1Opp + dragElOppLength / 2 === targetS1Opp + targetOppLength / 2; 1065 | }, 1066 | 1067 | /** 1068 | * Detects first nearest empty sortable to X and Y position using emptyInsertThreshold. 1069 | * @param {Number} x X position 1070 | * @param {Number} y Y position 1071 | * @return {HTMLElement} Element of the first found nearest Sortable 1072 | */ 1073 | _detectNearestEmptySortable = function _detectNearestEmptySortable(x, y) { 1074 | var ret; 1075 | sortables.some(function (sortable) { 1076 | if (lastChild(sortable)) return; 1077 | var rect = getRect(sortable), 1078 | threshold = sortable[expando].options.emptyInsertThreshold, 1079 | insideHorizontally = x >= rect.left - threshold && x <= rect.right + threshold, 1080 | insideVertically = y >= rect.top - threshold && y <= rect.bottom + threshold; 1081 | 1082 | if (threshold && insideHorizontally && insideVertically) { 1083 | return ret = sortable; 1084 | } 1085 | }); 1086 | return ret; 1087 | }, 1088 | _prepareGroup = function _prepareGroup(options) { 1089 | function toFn(value, pull) { 1090 | return function (to, from, dragEl, evt) { 1091 | var sameGroup = to.options.group.name && from.options.group.name && to.options.group.name === from.options.group.name; 1092 | 1093 | if (value == null && (pull || sameGroup)) { 1094 | // Default pull value 1095 | // Default pull and put value if same group 1096 | return true; 1097 | } else if (value == null || value === false) { 1098 | return false; 1099 | } else if (pull && value === 'clone') { 1100 | return value; 1101 | } else if (typeof value === 'function') { 1102 | return toFn(value(to, from, dragEl, evt), pull)(to, from, dragEl, evt); 1103 | } else { 1104 | var otherGroup = (pull ? to : from).options.group.name; 1105 | return value === true || typeof value === 'string' && value === otherGroup || value.join && value.indexOf(otherGroup) > -1; 1106 | } 1107 | }; 1108 | } 1109 | 1110 | var group = {}; 1111 | var originalGroup = options.group; 1112 | 1113 | if (!originalGroup || _typeof(originalGroup) != 'object') { 1114 | originalGroup = { 1115 | name: originalGroup 1116 | }; 1117 | } 1118 | 1119 | group.name = originalGroup.name; 1120 | group.checkPull = toFn(originalGroup.pull, true); 1121 | group.checkPut = toFn(originalGroup.put); 1122 | group.revertClone = originalGroup.revertClone; 1123 | options.group = group; 1124 | }, 1125 | _hideGhostForTarget = function _hideGhostForTarget() { 1126 | if (!supportCssPointerEvents && ghostEl) { 1127 | css(ghostEl, 'display', 'none'); 1128 | } 1129 | }, 1130 | _unhideGhostForTarget = function _unhideGhostForTarget() { 1131 | if (!supportCssPointerEvents && ghostEl) { 1132 | css(ghostEl, 'display', ''); 1133 | } 1134 | }; // #1184 fix - Prevent click event on fallback if dragged but item not changed position 1135 | 1136 | 1137 | if (documentExists) { 1138 | document.addEventListener('click', function (evt) { 1139 | if (ignoreNextClick) { 1140 | evt.preventDefault(); 1141 | evt.stopPropagation && evt.stopPropagation(); 1142 | evt.stopImmediatePropagation && evt.stopImmediatePropagation(); 1143 | ignoreNextClick = false; 1144 | return false; 1145 | } 1146 | }, true); 1147 | } 1148 | 1149 | var nearestEmptyInsertDetectEvent = function nearestEmptyInsertDetectEvent(evt) { 1150 | if (dragEl) { 1151 | evt = evt.touches ? evt.touches[0] : evt; 1152 | 1153 | var nearest = _detectNearestEmptySortable(evt.clientX, evt.clientY); 1154 | 1155 | if (nearest) { 1156 | // Create imitation event 1157 | var event = {}; 1158 | 1159 | for (var i in evt) { 1160 | if (evt.hasOwnProperty(i)) { 1161 | event[i] = evt[i]; 1162 | } 1163 | } 1164 | 1165 | event.target = event.rootEl = nearest; 1166 | event.preventDefault = void 0; 1167 | event.stopPropagation = void 0; 1168 | 1169 | nearest[expando]._onDragOver(event); 1170 | } 1171 | } 1172 | }; 1173 | 1174 | var _checkOutsideTargetEl = function _checkOutsideTargetEl(evt) { 1175 | if (dragEl) { 1176 | dragEl.parentNode[expando]._isOutsideThisEl(evt.target); 1177 | } 1178 | }; 1179 | /** 1180 | * @class Sortable 1181 | * @param {HTMLElement} el 1182 | * @param {Object} [options] 1183 | */ 1184 | 1185 | 1186 | function Sortable(el, options) { 1187 | if (!(el && el.nodeType && el.nodeType === 1)) { 1188 | throw "Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(el)); 1189 | } 1190 | 1191 | this.el = el; // root element 1192 | 1193 | this.options = options = _extends({}, options); // Export instance 1194 | 1195 | el[expando] = this; 1196 | var defaults = { 1197 | group: null, 1198 | sort: true, 1199 | disabled: false, 1200 | store: null, 1201 | handle: null, 1202 | draggable: /^[uo]l$/i.test(el.nodeName) ? '>li' : '>*', 1203 | swapThreshold: 1, 1204 | // percentage; 0 <= x <= 1 1205 | invertSwap: false, 1206 | // invert always 1207 | invertedSwapThreshold: null, 1208 | // will be set to same as swapThreshold if default 1209 | removeCloneOnHide: true, 1210 | direction: function direction() { 1211 | return _detectDirection(el, this.options); 1212 | }, 1213 | ghostClass: 'sortable-ghost', 1214 | chosenClass: 'sortable-chosen', 1215 | dragClass: 'sortable-drag', 1216 | ignore: 'a, img', 1217 | filter: null, 1218 | preventOnFilter: true, 1219 | animation: 0, 1220 | easing: null, 1221 | setData: function setData(dataTransfer, dragEl) { 1222 | dataTransfer.setData('Text', dragEl.textContent); 1223 | }, 1224 | dropBubble: false, 1225 | dragoverBubble: false, 1226 | dataIdAttr: 'data-id', 1227 | delay: 0, 1228 | delayOnTouchOnly: false, 1229 | touchStartThreshold: (Number.parseInt ? Number : window).parseInt(window.devicePixelRatio, 10) || 1, 1230 | forceFallback: false, 1231 | fallbackClass: 'sortable-fallback', 1232 | fallbackOnBody: false, 1233 | fallbackTolerance: 0, 1234 | fallbackOffset: { 1235 | x: 0, 1236 | y: 0 1237 | }, 1238 | supportPointer: Sortable.supportPointer !== false && 'PointerEvent' in window, 1239 | emptyInsertThreshold: 5 1240 | }; 1241 | PluginManager.initializePlugins(this, el, defaults); // Set default options 1242 | 1243 | for (var name in defaults) { 1244 | !(name in options) && (options[name] = defaults[name]); 1245 | } 1246 | 1247 | _prepareGroup(options); // Bind all private methods 1248 | 1249 | 1250 | for (var fn in this) { 1251 | if (fn.charAt(0) === '_' && typeof this[fn] === 'function') { 1252 | this[fn] = this[fn].bind(this); 1253 | } 1254 | } // Setup drag mode 1255 | 1256 | 1257 | this.nativeDraggable = options.forceFallback ? false : supportDraggable; 1258 | 1259 | if (this.nativeDraggable) { 1260 | // Touch start threshold cannot be greater than the native dragstart threshold 1261 | this.options.touchStartThreshold = 1; 1262 | } // Bind events 1263 | 1264 | 1265 | if (options.supportPointer) { 1266 | on(el, 'pointerdown', this._onTapStart); 1267 | } else { 1268 | on(el, 'mousedown', this._onTapStart); 1269 | on(el, 'touchstart', this._onTapStart); 1270 | } 1271 | 1272 | if (this.nativeDraggable) { 1273 | on(el, 'dragover', this); 1274 | on(el, 'dragenter', this); 1275 | } 1276 | 1277 | sortables.push(this.el); // Restore sorting 1278 | 1279 | options.store && options.store.get && this.sort(options.store.get(this) || []); // Add animation state manager 1280 | 1281 | _extends(this, AnimationStateManager()); 1282 | } 1283 | 1284 | Sortable.prototype = 1285 | /** @lends Sortable.prototype */ 1286 | { 1287 | constructor: Sortable, 1288 | _isOutsideThisEl: function _isOutsideThisEl(target) { 1289 | if (!this.el.contains(target) && target !== this.el) { 1290 | lastTarget = null; 1291 | } 1292 | }, 1293 | _getDirection: function _getDirection(evt, target) { 1294 | return typeof this.options.direction === 'function' ? this.options.direction.call(this, evt, target, dragEl) : this.options.direction; 1295 | }, 1296 | _onTapStart: function _onTapStart( 1297 | /** Event|TouchEvent */ 1298 | evt) { 1299 | if (!evt.cancelable) return; 1300 | 1301 | var _this = this, 1302 | el = this.el, 1303 | options = this.options, 1304 | preventOnFilter = options.preventOnFilter, 1305 | type = evt.type, 1306 | touch = evt.touches && evt.touches[0] || evt.pointerType && evt.pointerType === 'touch' && evt, 1307 | target = (touch || evt).target, 1308 | originalTarget = evt.target.shadowRoot && (evt.path && evt.path[0] || evt.composedPath && evt.composedPath()[0]) || target, 1309 | filter = options.filter; 1310 | 1311 | _saveInputCheckedState(el); // Don't trigger start event when an element is been dragged, otherwise the evt.oldindex always wrong when set option.group. 1312 | 1313 | 1314 | if (dragEl) { 1315 | return; 1316 | } 1317 | 1318 | if (/mousedown|pointerdown/.test(type) && evt.button !== 0 || options.disabled) { 1319 | return; // only left button and enabled 1320 | } // cancel dnd if original target is content editable 1321 | 1322 | 1323 | if (originalTarget.isContentEditable) { 1324 | return; 1325 | } 1326 | 1327 | target = closest(target, options.draggable, el, false); 1328 | 1329 | if (target && target.animated) { 1330 | return; 1331 | } 1332 | 1333 | if (lastDownEl === target) { 1334 | // Ignoring duplicate `down` 1335 | return; 1336 | } // Get the index of the dragged element within its parent 1337 | 1338 | 1339 | oldIndex = index(target); 1340 | oldDraggableIndex = index(target, options.draggable); // Check filter 1341 | 1342 | if (typeof filter === 'function') { 1343 | if (filter.call(this, evt, target, this)) { 1344 | _dispatchEvent({ 1345 | sortable: _this, 1346 | rootEl: originalTarget, 1347 | name: 'filter', 1348 | targetEl: target, 1349 | toEl: el, 1350 | fromEl: el 1351 | }); 1352 | 1353 | pluginEvent('filter', _this, { 1354 | evt: evt 1355 | }); 1356 | preventOnFilter && evt.cancelable && evt.preventDefault(); 1357 | return; // cancel dnd 1358 | } 1359 | } else if (filter) { 1360 | filter = filter.split(',').some(function (criteria) { 1361 | criteria = closest(originalTarget, criteria.trim(), el, false); 1362 | 1363 | if (criteria) { 1364 | _dispatchEvent({ 1365 | sortable: _this, 1366 | rootEl: criteria, 1367 | name: 'filter', 1368 | targetEl: target, 1369 | fromEl: el, 1370 | toEl: el 1371 | }); 1372 | 1373 | pluginEvent('filter', _this, { 1374 | evt: evt 1375 | }); 1376 | return true; 1377 | } 1378 | }); 1379 | 1380 | if (filter) { 1381 | preventOnFilter && evt.cancelable && evt.preventDefault(); 1382 | return; // cancel dnd 1383 | } 1384 | } 1385 | 1386 | if (options.handle && !closest(originalTarget, options.handle, el, false)) { 1387 | return; 1388 | } // Prepare `dragstart` 1389 | 1390 | 1391 | this._prepareDragStart(evt, touch, target); 1392 | }, 1393 | _prepareDragStart: function _prepareDragStart( 1394 | /** Event */ 1395 | evt, 1396 | /** Touch */ 1397 | touch, 1398 | /** HTMLElement */ 1399 | target) { 1400 | var _this = this, 1401 | el = _this.el, 1402 | options = _this.options, 1403 | ownerDocument = el.ownerDocument, 1404 | dragStartFn; 1405 | 1406 | if (target && !dragEl && target.parentNode === el) { 1407 | var dragRect = getRect(target); 1408 | rootEl = el; 1409 | dragEl = target; 1410 | parentEl = dragEl.parentNode; 1411 | nextEl = dragEl.nextSibling; 1412 | lastDownEl = target; 1413 | activeGroup = options.group; 1414 | Sortable.dragged = dragEl; 1415 | tapEvt = { 1416 | target: dragEl, 1417 | clientX: (touch || evt).clientX, 1418 | clientY: (touch || evt).clientY 1419 | }; 1420 | tapDistanceLeft = tapEvt.clientX - dragRect.left; 1421 | tapDistanceTop = tapEvt.clientY - dragRect.top; 1422 | this._lastX = (touch || evt).clientX; 1423 | this._lastY = (touch || evt).clientY; 1424 | dragEl.style['will-change'] = 'all'; 1425 | 1426 | dragStartFn = function dragStartFn() { 1427 | pluginEvent('delayEnded', _this, { 1428 | evt: evt 1429 | }); 1430 | 1431 | if (Sortable.eventCanceled) { 1432 | _this._onDrop(); 1433 | 1434 | return; 1435 | } // Delayed drag has been triggered 1436 | // we can re-enable the events: touchmove/mousemove 1437 | 1438 | 1439 | _this._disableDelayedDragEvents(); 1440 | 1441 | if (!FireFox && _this.nativeDraggable) { 1442 | dragEl.draggable = true; 1443 | } // Bind the events: dragstart/dragend 1444 | 1445 | 1446 | _this._triggerDragStart(evt, touch); // Drag start event 1447 | 1448 | 1449 | _dispatchEvent({ 1450 | sortable: _this, 1451 | name: 'choose', 1452 | originalEvent: evt 1453 | }); // Chosen item 1454 | 1455 | 1456 | toggleClass(dragEl, options.chosenClass, true); 1457 | }; // Disable "draggable" 1458 | 1459 | 1460 | options.ignore.split(',').forEach(function (criteria) { 1461 | find(dragEl, criteria.trim(), _disableDraggable); 1462 | }); 1463 | on(ownerDocument, 'dragover', nearestEmptyInsertDetectEvent); 1464 | on(ownerDocument, 'mousemove', nearestEmptyInsertDetectEvent); 1465 | on(ownerDocument, 'touchmove', nearestEmptyInsertDetectEvent); 1466 | on(ownerDocument, 'mouseup', _this._onDrop); 1467 | on(ownerDocument, 'touchend', _this._onDrop); 1468 | on(ownerDocument, 'touchcancel', _this._onDrop); // Make dragEl draggable (must be before delay for FireFox) 1469 | 1470 | if (FireFox && this.nativeDraggable) { 1471 | this.options.touchStartThreshold = 4; 1472 | dragEl.draggable = true; 1473 | } 1474 | 1475 | pluginEvent('delayStart', this, { 1476 | evt: evt 1477 | }); // Delay is impossible for native DnD in Edge or IE 1478 | 1479 | if (options.delay && (!options.delayOnTouchOnly || touch) && (!this.nativeDraggable || !(Edge || IE11OrLess))) { 1480 | if (Sortable.eventCanceled) { 1481 | this._onDrop(); 1482 | 1483 | return; 1484 | } // If the user moves the pointer or let go the click or touch 1485 | // before the delay has been reached: 1486 | // disable the delayed drag 1487 | 1488 | 1489 | on(ownerDocument, 'mouseup', _this._disableDelayedDrag); 1490 | on(ownerDocument, 'touchend', _this._disableDelayedDrag); 1491 | on(ownerDocument, 'touchcancel', _this._disableDelayedDrag); 1492 | on(ownerDocument, 'mousemove', _this._delayedDragTouchMoveHandler); 1493 | on(ownerDocument, 'touchmove', _this._delayedDragTouchMoveHandler); 1494 | options.supportPointer && on(ownerDocument, 'pointermove', _this._delayedDragTouchMoveHandler); 1495 | _this._dragStartTimer = setTimeout(dragStartFn, options.delay); 1496 | } else { 1497 | dragStartFn(); 1498 | } 1499 | } 1500 | }, 1501 | _delayedDragTouchMoveHandler: function _delayedDragTouchMoveHandler( 1502 | /** TouchEvent|PointerEvent **/ 1503 | e) { 1504 | var touch = e.touches ? e.touches[0] : e; 1505 | 1506 | if (Math.max(Math.abs(touch.clientX - this._lastX), Math.abs(touch.clientY - this._lastY)) >= Math.floor(this.options.touchStartThreshold / (this.nativeDraggable && window.devicePixelRatio || 1))) { 1507 | this._disableDelayedDrag(); 1508 | } 1509 | }, 1510 | _disableDelayedDrag: function _disableDelayedDrag() { 1511 | dragEl && _disableDraggable(dragEl); 1512 | clearTimeout(this._dragStartTimer); 1513 | 1514 | this._disableDelayedDragEvents(); 1515 | }, 1516 | _disableDelayedDragEvents: function _disableDelayedDragEvents() { 1517 | var ownerDocument = this.el.ownerDocument; 1518 | off(ownerDocument, 'mouseup', this._disableDelayedDrag); 1519 | off(ownerDocument, 'touchend', this._disableDelayedDrag); 1520 | off(ownerDocument, 'touchcancel', this._disableDelayedDrag); 1521 | off(ownerDocument, 'mousemove', this._delayedDragTouchMoveHandler); 1522 | off(ownerDocument, 'touchmove', this._delayedDragTouchMoveHandler); 1523 | off(ownerDocument, 'pointermove', this._delayedDragTouchMoveHandler); 1524 | }, 1525 | _triggerDragStart: function _triggerDragStart( 1526 | /** Event */ 1527 | evt, 1528 | /** Touch */ 1529 | touch) { 1530 | touch = touch || evt.pointerType == 'touch' && evt; 1531 | 1532 | if (!this.nativeDraggable || touch) { 1533 | if (this.options.supportPointer) { 1534 | on(document, 'pointermove', this._onTouchMove); 1535 | } else if (touch) { 1536 | on(document, 'touchmove', this._onTouchMove); 1537 | } else { 1538 | on(document, 'mousemove', this._onTouchMove); 1539 | } 1540 | } else { 1541 | on(dragEl, 'dragend', this); 1542 | on(rootEl, 'dragstart', this._onDragStart); 1543 | } 1544 | 1545 | try { 1546 | if (document.selection) { 1547 | // Timeout neccessary for IE9 1548 | _nextTick(function () { 1549 | document.selection.empty(); 1550 | }); 1551 | } else { 1552 | window.getSelection().removeAllRanges(); 1553 | } 1554 | } catch (err) {} 1555 | }, 1556 | _dragStarted: function _dragStarted(fallback, evt) { 1557 | 1558 | awaitingDragStarted = false; 1559 | 1560 | if (rootEl && dragEl) { 1561 | pluginEvent('dragStarted', this, { 1562 | evt: evt 1563 | }); 1564 | 1565 | if (this.nativeDraggable) { 1566 | on(document, 'dragover', _checkOutsideTargetEl); 1567 | } 1568 | 1569 | var options = this.options; // Apply effect 1570 | 1571 | !fallback && toggleClass(dragEl, options.dragClass, false); 1572 | toggleClass(dragEl, options.ghostClass, true); 1573 | Sortable.active = this; 1574 | fallback && this._appendGhost(); // Drag start event 1575 | 1576 | _dispatchEvent({ 1577 | sortable: this, 1578 | name: 'start', 1579 | originalEvent: evt 1580 | }); 1581 | } else { 1582 | this._nulling(); 1583 | } 1584 | }, 1585 | _emulateDragOver: function _emulateDragOver() { 1586 | if (touchEvt) { 1587 | this._lastX = touchEvt.clientX; 1588 | this._lastY = touchEvt.clientY; 1589 | 1590 | _hideGhostForTarget(); 1591 | 1592 | var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY); 1593 | var parent = target; 1594 | 1595 | while (target && target.shadowRoot) { 1596 | target = target.shadowRoot.elementFromPoint(touchEvt.clientX, touchEvt.clientY); 1597 | if (target === parent) break; 1598 | parent = target; 1599 | } 1600 | 1601 | dragEl.parentNode[expando]._isOutsideThisEl(target); 1602 | 1603 | if (parent) { 1604 | do { 1605 | if (parent[expando]) { 1606 | var inserted = void 0; 1607 | inserted = parent[expando]._onDragOver({ 1608 | clientX: touchEvt.clientX, 1609 | clientY: touchEvt.clientY, 1610 | target: target, 1611 | rootEl: parent 1612 | }); 1613 | 1614 | if (inserted && !this.options.dragoverBubble) { 1615 | break; 1616 | } 1617 | } 1618 | 1619 | target = parent; // store last element 1620 | } 1621 | /* jshint boss:true */ 1622 | while (parent = parent.parentNode); 1623 | } 1624 | 1625 | _unhideGhostForTarget(); 1626 | } 1627 | }, 1628 | _onTouchMove: function _onTouchMove( 1629 | /**TouchEvent*/ 1630 | evt) { 1631 | if (tapEvt) { 1632 | var options = this.options, 1633 | fallbackTolerance = options.fallbackTolerance, 1634 | fallbackOffset = options.fallbackOffset, 1635 | touch = evt.touches ? evt.touches[0] : evt, 1636 | ghostMatrix = ghostEl && matrix(ghostEl, true), 1637 | scaleX = ghostEl && ghostMatrix && ghostMatrix.a, 1638 | scaleY = ghostEl && ghostMatrix && ghostMatrix.d, 1639 | relativeScrollOffset = PositionGhostAbsolutely && ghostRelativeParent && getRelativeScrollOffset(ghostRelativeParent), 1640 | dx = (touch.clientX - tapEvt.clientX + fallbackOffset.x) / (scaleX || 1) + (relativeScrollOffset ? relativeScrollOffset[0] - ghostRelativeParentInitialScroll[0] : 0) / (scaleX || 1), 1641 | dy = (touch.clientY - tapEvt.clientY + fallbackOffset.y) / (scaleY || 1) + (relativeScrollOffset ? relativeScrollOffset[1] - ghostRelativeParentInitialScroll[1] : 0) / (scaleY || 1); // only set the status to dragging, when we are actually dragging 1642 | 1643 | if (!Sortable.active && !awaitingDragStarted) { 1644 | if (fallbackTolerance && Math.max(Math.abs(touch.clientX - this._lastX), Math.abs(touch.clientY - this._lastY)) < fallbackTolerance) { 1645 | return; 1646 | } 1647 | 1648 | this._onDragStart(evt, true); 1649 | } 1650 | 1651 | if (ghostEl) { 1652 | if (ghostMatrix) { 1653 | ghostMatrix.e += dx - (lastDx || 0); 1654 | ghostMatrix.f += dy - (lastDy || 0); 1655 | } else { 1656 | ghostMatrix = { 1657 | a: 1, 1658 | b: 0, 1659 | c: 0, 1660 | d: 1, 1661 | e: dx, 1662 | f: dy 1663 | }; 1664 | } 1665 | 1666 | var cssMatrix = "matrix(".concat(ghostMatrix.a, ",").concat(ghostMatrix.b, ",").concat(ghostMatrix.c, ",").concat(ghostMatrix.d, ",").concat(ghostMatrix.e, ",").concat(ghostMatrix.f, ")"); 1667 | css(ghostEl, 'webkitTransform', cssMatrix); 1668 | css(ghostEl, 'mozTransform', cssMatrix); 1669 | css(ghostEl, 'msTransform', cssMatrix); 1670 | css(ghostEl, 'transform', cssMatrix); 1671 | lastDx = dx; 1672 | lastDy = dy; 1673 | touchEvt = touch; 1674 | } 1675 | 1676 | evt.cancelable && evt.preventDefault(); 1677 | } 1678 | }, 1679 | _appendGhost: function _appendGhost() { 1680 | // Bug if using scale(): https://stackoverflow.com/questions/2637058 1681 | // Not being adjusted for 1682 | if (!ghostEl) { 1683 | var container = this.options.fallbackOnBody ? document.body : rootEl, 1684 | rect = getRect(dragEl, true, PositionGhostAbsolutely, true, container), 1685 | options = this.options; // Position absolutely 1686 | 1687 | if (PositionGhostAbsolutely) { 1688 | // Get relatively positioned parent 1689 | ghostRelativeParent = container; 1690 | 1691 | while (css(ghostRelativeParent, 'position') === 'static' && css(ghostRelativeParent, 'transform') === 'none' && ghostRelativeParent !== document) { 1692 | ghostRelativeParent = ghostRelativeParent.parentNode; 1693 | } 1694 | 1695 | if (ghostRelativeParent !== document.body && ghostRelativeParent !== document.documentElement) { 1696 | if (ghostRelativeParent === document) ghostRelativeParent = getWindowScrollingElement(); 1697 | rect.top += ghostRelativeParent.scrollTop; 1698 | rect.left += ghostRelativeParent.scrollLeft; 1699 | } else { 1700 | ghostRelativeParent = getWindowScrollingElement(); 1701 | } 1702 | 1703 | ghostRelativeParentInitialScroll = getRelativeScrollOffset(ghostRelativeParent); 1704 | } 1705 | 1706 | ghostEl = dragEl.cloneNode(true); 1707 | toggleClass(ghostEl, options.ghostClass, false); 1708 | toggleClass(ghostEl, options.fallbackClass, true); 1709 | toggleClass(ghostEl, options.dragClass, true); 1710 | css(ghostEl, 'transition', ''); 1711 | css(ghostEl, 'transform', ''); 1712 | css(ghostEl, 'box-sizing', 'border-box'); 1713 | css(ghostEl, 'margin', 0); 1714 | css(ghostEl, 'top', rect.top); 1715 | css(ghostEl, 'left', rect.left); 1716 | css(ghostEl, 'width', rect.width); 1717 | css(ghostEl, 'height', rect.height); 1718 | css(ghostEl, 'opacity', '0.8'); 1719 | css(ghostEl, 'position', PositionGhostAbsolutely ? 'absolute' : 'fixed'); 1720 | css(ghostEl, 'zIndex', '100000'); 1721 | css(ghostEl, 'pointerEvents', 'none'); 1722 | Sortable.ghost = ghostEl; 1723 | container.appendChild(ghostEl); // Set transform-origin 1724 | 1725 | css(ghostEl, 'transform-origin', tapDistanceLeft / parseInt(ghostEl.style.width) * 100 + '% ' + tapDistanceTop / parseInt(ghostEl.style.height) * 100 + '%'); 1726 | } 1727 | }, 1728 | _onDragStart: function _onDragStart( 1729 | /**Event*/ 1730 | evt, 1731 | /**boolean*/ 1732 | fallback) { 1733 | var _this = this; 1734 | 1735 | var dataTransfer = evt.dataTransfer; 1736 | var options = _this.options; 1737 | pluginEvent('dragStart', this, { 1738 | evt: evt 1739 | }); 1740 | 1741 | if (Sortable.eventCanceled) { 1742 | this._onDrop(); 1743 | 1744 | return; 1745 | } 1746 | 1747 | pluginEvent('setupClone', this); 1748 | 1749 | if (!Sortable.eventCanceled) { 1750 | cloneEl = clone(dragEl); 1751 | cloneEl.draggable = false; 1752 | cloneEl.style['will-change'] = ''; 1753 | 1754 | this._hideClone(); 1755 | 1756 | toggleClass(cloneEl, this.options.chosenClass, false); 1757 | Sortable.clone = cloneEl; 1758 | } // #1143: IFrame support workaround 1759 | 1760 | 1761 | _this.cloneId = _nextTick(function () { 1762 | pluginEvent('clone', _this); 1763 | if (Sortable.eventCanceled) return; 1764 | 1765 | if (!_this.options.removeCloneOnHide) { 1766 | rootEl.insertBefore(cloneEl, dragEl); 1767 | } 1768 | 1769 | _this._hideClone(); 1770 | 1771 | _dispatchEvent({ 1772 | sortable: _this, 1773 | name: 'clone' 1774 | }); 1775 | }); 1776 | !fallback && toggleClass(dragEl, options.dragClass, true); // Set proper drop events 1777 | 1778 | if (fallback) { 1779 | ignoreNextClick = true; 1780 | _this._loopId = setInterval(_this._emulateDragOver, 50); 1781 | } else { 1782 | // Undo what was set in _prepareDragStart before drag started 1783 | off(document, 'mouseup', _this._onDrop); 1784 | off(document, 'touchend', _this._onDrop); 1785 | off(document, 'touchcancel', _this._onDrop); 1786 | 1787 | if (dataTransfer) { 1788 | dataTransfer.effectAllowed = 'move'; 1789 | options.setData && options.setData.call(_this, dataTransfer, dragEl); 1790 | } 1791 | 1792 | on(document, 'drop', _this); // #1276 fix: 1793 | 1794 | css(dragEl, 'transform', 'translateZ(0)'); 1795 | } 1796 | 1797 | awaitingDragStarted = true; 1798 | _this._dragStartId = _nextTick(_this._dragStarted.bind(_this, fallback, evt)); 1799 | on(document, 'selectstart', _this); 1800 | moved = true; 1801 | 1802 | if (Safari) { 1803 | css(document.body, 'user-select', 'none'); 1804 | } 1805 | }, 1806 | // Returns true - if no further action is needed (either inserted or another condition) 1807 | _onDragOver: function _onDragOver( 1808 | /**Event*/ 1809 | evt) { 1810 | var el = this.el, 1811 | target = evt.target, 1812 | dragRect, 1813 | targetRect, 1814 | revert, 1815 | options = this.options, 1816 | group = options.group, 1817 | activeSortable = Sortable.active, 1818 | isOwner = activeGroup === group, 1819 | canSort = options.sort, 1820 | fromSortable = putSortable || activeSortable, 1821 | vertical, 1822 | _this = this, 1823 | completedFired = false; 1824 | 1825 | if (_silent) return; 1826 | 1827 | function dragOverEvent(name, extra) { 1828 | pluginEvent(name, _this, _objectSpread({ 1829 | evt: evt, 1830 | isOwner: isOwner, 1831 | axis: vertical ? 'vertical' : 'horizontal', 1832 | revert: revert, 1833 | dragRect: dragRect, 1834 | targetRect: targetRect, 1835 | canSort: canSort, 1836 | fromSortable: fromSortable, 1837 | target: target, 1838 | completed: completed, 1839 | onMove: function onMove(target, after) { 1840 | return _onMove(rootEl, el, dragEl, dragRect, target, getRect(target), evt, after); 1841 | }, 1842 | changed: changed 1843 | }, extra)); 1844 | } // Capture animation state 1845 | 1846 | 1847 | function capture() { 1848 | dragOverEvent('dragOverAnimationCapture'); 1849 | 1850 | _this.captureAnimationState(); 1851 | 1852 | if (_this !== fromSortable) { 1853 | fromSortable.captureAnimationState(); 1854 | } 1855 | } // Return invocation when dragEl is inserted (or completed) 1856 | 1857 | 1858 | function completed(insertion) { 1859 | dragOverEvent('dragOverCompleted', { 1860 | insertion: insertion 1861 | }); 1862 | 1863 | if (insertion) { 1864 | // Clones must be hidden before folding animation to capture dragRectAbsolute properly 1865 | if (isOwner) { 1866 | activeSortable._hideClone(); 1867 | } else { 1868 | activeSortable._showClone(_this); 1869 | } 1870 | 1871 | if (_this !== fromSortable) { 1872 | // Set ghost class to new sortable's ghost class 1873 | toggleClass(dragEl, putSortable ? putSortable.options.ghostClass : activeSortable.options.ghostClass, false); 1874 | toggleClass(dragEl, options.ghostClass, true); 1875 | } 1876 | 1877 | if (putSortable !== _this && _this !== Sortable.active) { 1878 | putSortable = _this; 1879 | } else if (_this === Sortable.active && putSortable) { 1880 | putSortable = null; 1881 | } // Animation 1882 | 1883 | 1884 | if (fromSortable === _this) { 1885 | _this._ignoreWhileAnimating = target; 1886 | } 1887 | 1888 | _this.animateAll(function () { 1889 | dragOverEvent('dragOverAnimationComplete'); 1890 | _this._ignoreWhileAnimating = null; 1891 | }); 1892 | 1893 | if (_this !== fromSortable) { 1894 | fromSortable.animateAll(); 1895 | fromSortable._ignoreWhileAnimating = null; 1896 | } 1897 | } // Null lastTarget if it is not inside a previously swapped element 1898 | 1899 | 1900 | if (target === dragEl && !dragEl.animated || target === el && !target.animated) { 1901 | lastTarget = null; 1902 | } // no bubbling and not fallback 1903 | 1904 | 1905 | if (!options.dragoverBubble && !evt.rootEl && target !== document) { 1906 | dragEl.parentNode[expando]._isOutsideThisEl(evt.target); // Do not detect for empty insert if already inserted 1907 | 1908 | 1909 | !insertion && nearestEmptyInsertDetectEvent(evt); 1910 | } 1911 | 1912 | !options.dragoverBubble && evt.stopPropagation && evt.stopPropagation(); 1913 | return completedFired = true; 1914 | } // Call when dragEl has been inserted 1915 | 1916 | 1917 | function changed() { 1918 | newIndex = index(dragEl); 1919 | newDraggableIndex = index(dragEl, options.draggable); 1920 | 1921 | _dispatchEvent({ 1922 | sortable: _this, 1923 | name: 'change', 1924 | toEl: el, 1925 | newIndex: newIndex, 1926 | newDraggableIndex: newDraggableIndex, 1927 | originalEvent: evt 1928 | }); 1929 | } 1930 | 1931 | if (evt.preventDefault !== void 0) { 1932 | evt.cancelable && evt.preventDefault(); 1933 | } 1934 | 1935 | target = closest(target, options.draggable, el, true); 1936 | dragOverEvent('dragOver'); 1937 | if (Sortable.eventCanceled) return completedFired; 1938 | 1939 | if (dragEl.contains(evt.target) || target.animated && target.animatingX && target.animatingY || _this._ignoreWhileAnimating === target) { 1940 | return completed(false); 1941 | } 1942 | 1943 | ignoreNextClick = false; 1944 | 1945 | if (activeSortable && !options.disabled && (isOwner ? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list 1946 | : putSortable === this || (this.lastPutMode = activeGroup.checkPull(this, activeSortable, dragEl, evt)) && group.checkPut(this, activeSortable, dragEl, evt))) { 1947 | vertical = this._getDirection(evt, target) === 'vertical'; 1948 | dragRect = getRect(dragEl); 1949 | dragOverEvent('dragOverValid'); 1950 | if (Sortable.eventCanceled) return completedFired; 1951 | 1952 | if (revert) { 1953 | parentEl = rootEl; // actualization 1954 | 1955 | capture(); 1956 | 1957 | this._hideClone(); 1958 | 1959 | dragOverEvent('revert'); 1960 | 1961 | if (!Sortable.eventCanceled) { 1962 | if (nextEl) { 1963 | rootEl.insertBefore(dragEl, nextEl); 1964 | } else { 1965 | rootEl.appendChild(dragEl); 1966 | } 1967 | } 1968 | 1969 | return completed(true); 1970 | } 1971 | 1972 | var elLastChild = lastChild(el, options.draggable); 1973 | 1974 | if (!elLastChild || _ghostIsLast(evt, vertical, this) && !elLastChild.animated) { 1975 | // If already at end of list: Do not insert 1976 | if (elLastChild === dragEl) { 1977 | return completed(false); 1978 | } // assign target only if condition is true 1979 | 1980 | 1981 | if (elLastChild && el === evt.target) { 1982 | target = elLastChild; 1983 | } 1984 | 1985 | if (target) { 1986 | targetRect = getRect(target); 1987 | } 1988 | 1989 | if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) { 1990 | capture(); 1991 | el.appendChild(dragEl); 1992 | parentEl = el; // actualization 1993 | 1994 | changed(); 1995 | return completed(true); 1996 | } 1997 | } else if (target.parentNode === el) { 1998 | targetRect = getRect(target); 1999 | var direction = 0, 2000 | targetBeforeFirstSwap, 2001 | differentLevel = dragEl.parentNode !== el, 2002 | differentRowCol = !_dragElInRowColumn(dragEl.animated && dragEl.toRect || dragRect, target.animated && target.toRect || targetRect, vertical), 2003 | side1 = vertical ? 'top' : 'left', 2004 | scrolledPastTop = isScrolledPast(target, 'top', 'top') || isScrolledPast(dragEl, 'top', 'top'), 2005 | scrollBefore = scrolledPastTop ? scrolledPastTop.scrollTop : void 0; 2006 | 2007 | if (lastTarget !== target) { 2008 | targetBeforeFirstSwap = targetRect[side1]; 2009 | pastFirstInvertThresh = false; 2010 | isCircumstantialInvert = !differentRowCol && options.invertSwap || differentLevel; 2011 | } 2012 | 2013 | direction = _getSwapDirection(evt, target, targetRect, vertical, differentRowCol ? 1 : options.swapThreshold, options.invertedSwapThreshold == null ? options.swapThreshold : options.invertedSwapThreshold, isCircumstantialInvert, lastTarget === target); 2014 | var sibling; 2015 | 2016 | if (direction !== 0) { 2017 | // Check if target is beside dragEl in respective direction (ignoring hidden elements) 2018 | var dragIndex = index(dragEl); 2019 | 2020 | do { 2021 | dragIndex -= direction; 2022 | sibling = parentEl.children[dragIndex]; 2023 | } while (sibling && (css(sibling, 'display') === 'none' || sibling === ghostEl)); 2024 | } // If dragEl is already beside target: Do not insert 2025 | 2026 | 2027 | if (direction === 0 || sibling === target) { 2028 | return completed(false); 2029 | } 2030 | 2031 | lastTarget = target; 2032 | lastDirection = direction; 2033 | var nextSibling = target.nextElementSibling, 2034 | after = false; 2035 | after = direction === 1; 2036 | 2037 | var moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, after); 2038 | 2039 | if (moveVector !== false) { 2040 | if (moveVector === 1 || moveVector === -1) { 2041 | after = moveVector === 1; 2042 | } 2043 | 2044 | _silent = true; 2045 | setTimeout(_unsilent, 30); 2046 | capture(); 2047 | 2048 | if (after && !nextSibling) { 2049 | el.appendChild(dragEl); 2050 | } else { 2051 | target.parentNode.insertBefore(dragEl, after ? nextSibling : target); 2052 | } // Undo chrome's scroll adjustment (has no effect on other browsers) 2053 | 2054 | 2055 | if (scrolledPastTop) { 2056 | scrollBy(scrolledPastTop, 0, scrollBefore - scrolledPastTop.scrollTop); 2057 | } 2058 | 2059 | parentEl = dragEl.parentNode; // actualization 2060 | // must be done before animation 2061 | 2062 | if (targetBeforeFirstSwap !== undefined && !isCircumstantialInvert) { 2063 | targetMoveDistance = Math.abs(targetBeforeFirstSwap - getRect(target)[side1]); 2064 | } 2065 | 2066 | changed(); 2067 | return completed(true); 2068 | } 2069 | } 2070 | 2071 | if (el.contains(dragEl)) { 2072 | return completed(false); 2073 | } 2074 | } 2075 | 2076 | return false; 2077 | }, 2078 | _ignoreWhileAnimating: null, 2079 | _offMoveEvents: function _offMoveEvents() { 2080 | off(document, 'mousemove', this._onTouchMove); 2081 | off(document, 'touchmove', this._onTouchMove); 2082 | off(document, 'pointermove', this._onTouchMove); 2083 | off(document, 'dragover', nearestEmptyInsertDetectEvent); 2084 | off(document, 'mousemove', nearestEmptyInsertDetectEvent); 2085 | off(document, 'touchmove', nearestEmptyInsertDetectEvent); 2086 | }, 2087 | _offUpEvents: function _offUpEvents() { 2088 | var ownerDocument = this.el.ownerDocument; 2089 | off(ownerDocument, 'mouseup', this._onDrop); 2090 | off(ownerDocument, 'touchend', this._onDrop); 2091 | off(ownerDocument, 'pointerup', this._onDrop); 2092 | off(ownerDocument, 'touchcancel', this._onDrop); 2093 | off(document, 'selectstart', this); 2094 | }, 2095 | _onDrop: function _onDrop( 2096 | /**Event*/ 2097 | evt) { 2098 | var el = this.el, 2099 | options = this.options; // Get the index of the dragged element within its parent 2100 | 2101 | newIndex = index(dragEl); 2102 | newDraggableIndex = index(dragEl, options.draggable); 2103 | pluginEvent('drop', this, { 2104 | evt: evt 2105 | }); 2106 | parentEl = dragEl && dragEl.parentNode; // Get again after plugin event 2107 | 2108 | newIndex = index(dragEl); 2109 | newDraggableIndex = index(dragEl, options.draggable); 2110 | 2111 | if (Sortable.eventCanceled) { 2112 | this._nulling(); 2113 | 2114 | return; 2115 | } 2116 | 2117 | awaitingDragStarted = false; 2118 | isCircumstantialInvert = false; 2119 | pastFirstInvertThresh = false; 2120 | clearInterval(this._loopId); 2121 | clearTimeout(this._dragStartTimer); 2122 | 2123 | _cancelNextTick(this.cloneId); 2124 | 2125 | _cancelNextTick(this._dragStartId); // Unbind events 2126 | 2127 | 2128 | if (this.nativeDraggable) { 2129 | off(document, 'drop', this); 2130 | off(el, 'dragstart', this._onDragStart); 2131 | } 2132 | 2133 | this._offMoveEvents(); 2134 | 2135 | this._offUpEvents(); 2136 | 2137 | if (Safari) { 2138 | css(document.body, 'user-select', ''); 2139 | } 2140 | 2141 | css(dragEl, 'transform', ''); 2142 | 2143 | if (evt) { 2144 | if (moved) { 2145 | evt.cancelable && evt.preventDefault(); 2146 | !options.dropBubble && evt.stopPropagation(); 2147 | } 2148 | 2149 | ghostEl && ghostEl.parentNode && ghostEl.parentNode.removeChild(ghostEl); 2150 | 2151 | if (rootEl === parentEl || putSortable && putSortable.lastPutMode !== 'clone') { 2152 | // Remove clone(s) 2153 | cloneEl && cloneEl.parentNode && cloneEl.parentNode.removeChild(cloneEl); 2154 | } 2155 | 2156 | if (dragEl) { 2157 | if (this.nativeDraggable) { 2158 | off(dragEl, 'dragend', this); 2159 | } 2160 | 2161 | _disableDraggable(dragEl); 2162 | 2163 | dragEl.style['will-change'] = ''; // Remove classes 2164 | // ghostClass is added in dragStarted 2165 | 2166 | if (moved && !awaitingDragStarted) { 2167 | toggleClass(dragEl, putSortable ? putSortable.options.ghostClass : this.options.ghostClass, false); 2168 | } 2169 | 2170 | toggleClass(dragEl, this.options.chosenClass, false); // Drag stop event 2171 | 2172 | _dispatchEvent({ 2173 | sortable: this, 2174 | name: 'unchoose', 2175 | toEl: parentEl, 2176 | newIndex: null, 2177 | newDraggableIndex: null, 2178 | originalEvent: evt 2179 | }); 2180 | 2181 | if (rootEl !== parentEl) { 2182 | if (newIndex >= 0) { 2183 | // Add event 2184 | _dispatchEvent({ 2185 | rootEl: parentEl, 2186 | name: 'add', 2187 | toEl: parentEl, 2188 | fromEl: rootEl, 2189 | originalEvent: evt 2190 | }); // Remove event 2191 | 2192 | 2193 | _dispatchEvent({ 2194 | sortable: this, 2195 | name: 'remove', 2196 | toEl: parentEl, 2197 | originalEvent: evt 2198 | }); // drag from one list and drop into another 2199 | 2200 | 2201 | _dispatchEvent({ 2202 | rootEl: parentEl, 2203 | name: 'sort', 2204 | toEl: parentEl, 2205 | fromEl: rootEl, 2206 | originalEvent: evt 2207 | }); 2208 | 2209 | _dispatchEvent({ 2210 | sortable: this, 2211 | name: 'sort', 2212 | toEl: parentEl, 2213 | originalEvent: evt 2214 | }); 2215 | } 2216 | 2217 | putSortable && putSortable.save(); 2218 | } else { 2219 | if (newIndex !== oldIndex) { 2220 | if (newIndex >= 0) { 2221 | // drag & drop within the same list 2222 | _dispatchEvent({ 2223 | sortable: this, 2224 | name: 'update', 2225 | toEl: parentEl, 2226 | originalEvent: evt 2227 | }); 2228 | 2229 | _dispatchEvent({ 2230 | sortable: this, 2231 | name: 'sort', 2232 | toEl: parentEl, 2233 | originalEvent: evt 2234 | }); 2235 | } 2236 | } 2237 | } 2238 | 2239 | if (Sortable.active) { 2240 | /* jshint eqnull:true */ 2241 | if (newIndex == null || newIndex === -1) { 2242 | newIndex = oldIndex; 2243 | newDraggableIndex = oldDraggableIndex; 2244 | } 2245 | 2246 | _dispatchEvent({ 2247 | sortable: this, 2248 | name: 'end', 2249 | toEl: parentEl, 2250 | originalEvent: evt 2251 | }); // Save sorting 2252 | 2253 | 2254 | this.save(); 2255 | } 2256 | } 2257 | } 2258 | 2259 | this._nulling(); 2260 | }, 2261 | _nulling: function _nulling() { 2262 | pluginEvent('nulling', this); 2263 | rootEl = dragEl = parentEl = ghostEl = nextEl = cloneEl = lastDownEl = cloneHidden = tapEvt = touchEvt = moved = newIndex = newDraggableIndex = oldIndex = oldDraggableIndex = lastTarget = lastDirection = putSortable = activeGroup = Sortable.dragged = Sortable.ghost = Sortable.clone = Sortable.active = null; 2264 | savedInputChecked.forEach(function (el) { 2265 | el.checked = true; 2266 | }); 2267 | savedInputChecked.length = lastDx = lastDy = 0; 2268 | }, 2269 | handleEvent: function handleEvent( 2270 | /**Event*/ 2271 | evt) { 2272 | switch (evt.type) { 2273 | case 'drop': 2274 | case 'dragend': 2275 | this._onDrop(evt); 2276 | 2277 | break; 2278 | 2279 | case 'dragenter': 2280 | case 'dragover': 2281 | if (dragEl) { 2282 | this._onDragOver(evt); 2283 | 2284 | _globalDragOver(evt); 2285 | } 2286 | 2287 | break; 2288 | 2289 | case 'selectstart': 2290 | evt.preventDefault(); 2291 | break; 2292 | } 2293 | }, 2294 | 2295 | /** 2296 | * Serializes the item into an array of string. 2297 | * @returns {String[]} 2298 | */ 2299 | toArray: function toArray() { 2300 | var order = [], 2301 | el, 2302 | children = this.el.children, 2303 | i = 0, 2304 | n = children.length, 2305 | options = this.options; 2306 | 2307 | for (; i < n; i++) { 2308 | el = children[i]; 2309 | 2310 | if (closest(el, options.draggable, this.el, false)) { 2311 | order.push(el.getAttribute(options.dataIdAttr) || _generateId(el)); 2312 | } 2313 | } 2314 | 2315 | return order; 2316 | }, 2317 | 2318 | /** 2319 | * Sorts the elements according to the array. 2320 | * @param {String[]} order order of the items 2321 | */ 2322 | sort: function sort(order) { 2323 | var items = {}, 2324 | rootEl = this.el; 2325 | this.toArray().forEach(function (id, i) { 2326 | var el = rootEl.children[i]; 2327 | 2328 | if (closest(el, this.options.draggable, rootEl, false)) { 2329 | items[id] = el; 2330 | } 2331 | }, this); 2332 | order.forEach(function (id) { 2333 | if (items[id]) { 2334 | rootEl.removeChild(items[id]); 2335 | rootEl.appendChild(items[id]); 2336 | } 2337 | }); 2338 | }, 2339 | 2340 | /** 2341 | * Save the current sorting 2342 | */ 2343 | save: function save() { 2344 | var store = this.options.store; 2345 | store && store.set && store.set(this); 2346 | }, 2347 | 2348 | /** 2349 | * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. 2350 | * @param {HTMLElement} el 2351 | * @param {String} [selector] default: `options.draggable` 2352 | * @returns {HTMLElement|null} 2353 | */ 2354 | closest: function closest$1(el, selector) { 2355 | return closest(el, selector || this.options.draggable, this.el, false); 2356 | }, 2357 | 2358 | /** 2359 | * Set/get option 2360 | * @param {string} name 2361 | * @param {*} [value] 2362 | * @returns {*} 2363 | */ 2364 | option: function option(name, value) { 2365 | var options = this.options; 2366 | 2367 | if (value === void 0) { 2368 | return options[name]; 2369 | } else { 2370 | var modifiedValue = PluginManager.modifyOption(this, name, value); 2371 | 2372 | if (typeof modifiedValue !== 'undefined') { 2373 | options[name] = modifiedValue; 2374 | } else { 2375 | options[name] = value; 2376 | } 2377 | 2378 | if (name === 'group') { 2379 | _prepareGroup(options); 2380 | } 2381 | } 2382 | }, 2383 | 2384 | /** 2385 | * Destroy 2386 | */ 2387 | destroy: function destroy() { 2388 | pluginEvent('destroy', this); 2389 | var el = this.el; 2390 | el[expando] = null; 2391 | off(el, 'mousedown', this._onTapStart); 2392 | off(el, 'touchstart', this._onTapStart); 2393 | off(el, 'pointerdown', this._onTapStart); 2394 | 2395 | if (this.nativeDraggable) { 2396 | off(el, 'dragover', this); 2397 | off(el, 'dragenter', this); 2398 | } // Remove draggable attributes 2399 | 2400 | 2401 | Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) { 2402 | el.removeAttribute('draggable'); 2403 | }); 2404 | 2405 | this._onDrop(); 2406 | 2407 | this._disableDelayedDragEvents(); 2408 | 2409 | sortables.splice(sortables.indexOf(this.el), 1); 2410 | this.el = el = null; 2411 | }, 2412 | _hideClone: function _hideClone() { 2413 | if (!cloneHidden) { 2414 | pluginEvent('hideClone', this); 2415 | if (Sortable.eventCanceled) return; 2416 | css(cloneEl, 'display', 'none'); 2417 | 2418 | if (this.options.removeCloneOnHide && cloneEl.parentNode) { 2419 | cloneEl.parentNode.removeChild(cloneEl); 2420 | } 2421 | 2422 | cloneHidden = true; 2423 | } 2424 | }, 2425 | _showClone: function _showClone(putSortable) { 2426 | if (putSortable.lastPutMode !== 'clone') { 2427 | this._hideClone(); 2428 | 2429 | return; 2430 | } 2431 | 2432 | if (cloneHidden) { 2433 | pluginEvent('showClone', this); 2434 | if (Sortable.eventCanceled) return; // show clone at dragEl or original position 2435 | 2436 | if (rootEl.contains(dragEl) && !this.options.group.revertClone) { 2437 | rootEl.insertBefore(cloneEl, dragEl); 2438 | } else if (nextEl) { 2439 | rootEl.insertBefore(cloneEl, nextEl); 2440 | } else { 2441 | rootEl.appendChild(cloneEl); 2442 | } 2443 | 2444 | if (this.options.group.revertClone) { 2445 | this.animate(dragEl, cloneEl); 2446 | } 2447 | 2448 | css(cloneEl, 'display', ''); 2449 | cloneHidden = false; 2450 | } 2451 | } 2452 | }; 2453 | 2454 | function _globalDragOver( 2455 | /**Event*/ 2456 | evt) { 2457 | if (evt.dataTransfer) { 2458 | evt.dataTransfer.dropEffect = 'move'; 2459 | } 2460 | 2461 | evt.cancelable && evt.preventDefault(); 2462 | } 2463 | 2464 | function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect, originalEvent, willInsertAfter) { 2465 | var evt, 2466 | sortable = fromEl[expando], 2467 | onMoveFn = sortable.options.onMove, 2468 | retVal; // Support for new CustomEvent feature 2469 | 2470 | if (window.CustomEvent && !IE11OrLess && !Edge) { 2471 | evt = new CustomEvent('move', { 2472 | bubbles: true, 2473 | cancelable: true 2474 | }); 2475 | } else { 2476 | evt = document.createEvent('Event'); 2477 | evt.initEvent('move', true, true); 2478 | } 2479 | 2480 | evt.to = toEl; 2481 | evt.from = fromEl; 2482 | evt.dragged = dragEl; 2483 | evt.draggedRect = dragRect; 2484 | evt.related = targetEl || toEl; 2485 | evt.relatedRect = targetRect || getRect(toEl); 2486 | evt.willInsertAfter = willInsertAfter; 2487 | evt.originalEvent = originalEvent; 2488 | fromEl.dispatchEvent(evt); 2489 | 2490 | if (onMoveFn) { 2491 | retVal = onMoveFn.call(sortable, evt, originalEvent); 2492 | } 2493 | 2494 | return retVal; 2495 | } 2496 | 2497 | function _disableDraggable(el) { 2498 | el.draggable = false; 2499 | } 2500 | 2501 | function _unsilent() { 2502 | _silent = false; 2503 | } 2504 | 2505 | function _ghostIsLast(evt, vertical, sortable) { 2506 | var rect = getRect(lastChild(sortable.el, sortable.options.draggable)); 2507 | var spacer = 10; 2508 | return vertical ? evt.clientX > rect.right + spacer || evt.clientX <= rect.right && evt.clientY > rect.bottom && evt.clientX >= rect.left : evt.clientX > rect.right && evt.clientY > rect.top || evt.clientX <= rect.right && evt.clientY > rect.bottom + spacer; 2509 | } 2510 | 2511 | function _getSwapDirection(evt, target, targetRect, vertical, swapThreshold, invertedSwapThreshold, invertSwap, isLastTarget) { 2512 | var mouseOnAxis = vertical ? evt.clientY : evt.clientX, 2513 | targetLength = vertical ? targetRect.height : targetRect.width, 2514 | targetS1 = vertical ? targetRect.top : targetRect.left, 2515 | targetS2 = vertical ? targetRect.bottom : targetRect.right, 2516 | invert = false; 2517 | 2518 | if (!invertSwap) { 2519 | // Never invert or create dragEl shadow when target movemenet causes mouse to move past the end of regular swapThreshold 2520 | if (isLastTarget && targetMoveDistance < targetLength * swapThreshold) { 2521 | // multiplied only by swapThreshold because mouse will already be inside target by (1 - threshold) * targetLength / 2 2522 | // check if past first invert threshold on side opposite of lastDirection 2523 | if (!pastFirstInvertThresh && (lastDirection === 1 ? mouseOnAxis > targetS1 + targetLength * invertedSwapThreshold / 2 : mouseOnAxis < targetS2 - targetLength * invertedSwapThreshold / 2)) { 2524 | // past first invert threshold, do not restrict inverted threshold to dragEl shadow 2525 | pastFirstInvertThresh = true; 2526 | } 2527 | 2528 | if (!pastFirstInvertThresh) { 2529 | // dragEl shadow (target move distance shadow) 2530 | if (lastDirection === 1 ? mouseOnAxis < targetS1 + targetMoveDistance // over dragEl shadow 2531 | : mouseOnAxis > targetS2 - targetMoveDistance) { 2532 | return -lastDirection; 2533 | } 2534 | } else { 2535 | invert = true; 2536 | } 2537 | } else { 2538 | // Regular 2539 | if (mouseOnAxis > targetS1 + targetLength * (1 - swapThreshold) / 2 && mouseOnAxis < targetS2 - targetLength * (1 - swapThreshold) / 2) { 2540 | return _getInsertDirection(target); 2541 | } 2542 | } 2543 | } 2544 | 2545 | invert = invert || invertSwap; 2546 | 2547 | if (invert) { 2548 | // Invert of regular 2549 | if (mouseOnAxis < targetS1 + targetLength * invertedSwapThreshold / 2 || mouseOnAxis > targetS2 - targetLength * invertedSwapThreshold / 2) { 2550 | return mouseOnAxis > targetS1 + targetLength / 2 ? 1 : -1; 2551 | } 2552 | } 2553 | 2554 | return 0; 2555 | } 2556 | /** 2557 | * Gets the direction dragEl must be swapped relative to target in order to make it 2558 | * seem that dragEl has been "inserted" into that element's position 2559 | * @param {HTMLElement} target The target whose position dragEl is being inserted at 2560 | * @return {Number} Direction dragEl must be swapped 2561 | */ 2562 | 2563 | 2564 | function _getInsertDirection(target) { 2565 | if (index(dragEl) < index(target)) { 2566 | return 1; 2567 | } else { 2568 | return -1; 2569 | } 2570 | } 2571 | /** 2572 | * Generate id 2573 | * @param {HTMLElement} el 2574 | * @returns {String} 2575 | * @private 2576 | */ 2577 | 2578 | 2579 | function _generateId(el) { 2580 | var str = el.tagName + el.className + el.src + el.href + el.textContent, 2581 | i = str.length, 2582 | sum = 0; 2583 | 2584 | while (i--) { 2585 | sum += str.charCodeAt(i); 2586 | } 2587 | 2588 | return sum.toString(36); 2589 | } 2590 | 2591 | function _saveInputCheckedState(root) { 2592 | savedInputChecked.length = 0; 2593 | var inputs = root.getElementsByTagName('input'); 2594 | var idx = inputs.length; 2595 | 2596 | while (idx--) { 2597 | var el = inputs[idx]; 2598 | el.checked && savedInputChecked.push(el); 2599 | } 2600 | } 2601 | 2602 | function _nextTick(fn) { 2603 | return setTimeout(fn, 0); 2604 | } 2605 | 2606 | function _cancelNextTick(id) { 2607 | return clearTimeout(id); 2608 | } // Fixed #973: 2609 | 2610 | 2611 | if (documentExists) { 2612 | on(document, 'touchmove', function (evt) { 2613 | if ((Sortable.active || awaitingDragStarted) && evt.cancelable) { 2614 | evt.preventDefault(); 2615 | } 2616 | }); 2617 | } // Export utils 2618 | 2619 | 2620 | Sortable.utils = { 2621 | on: on, 2622 | off: off, 2623 | css: css, 2624 | find: find, 2625 | is: function is(el, selector) { 2626 | return !!closest(el, selector, el, false); 2627 | }, 2628 | extend: extend, 2629 | throttle: throttle, 2630 | closest: closest, 2631 | toggleClass: toggleClass, 2632 | clone: clone, 2633 | index: index, 2634 | nextTick: _nextTick, 2635 | cancelNextTick: _cancelNextTick, 2636 | detectDirection: _detectDirection, 2637 | getChild: getChild 2638 | }; 2639 | /** 2640 | * Get the Sortable instance of an element 2641 | * @param {HTMLElement} element The element 2642 | * @return {Sortable|undefined} The instance of Sortable 2643 | */ 2644 | 2645 | Sortable.get = function (element) { 2646 | return element[expando]; 2647 | }; 2648 | /** 2649 | * Mount a plugin to Sortable 2650 | * @param {...SortablePlugin|SortablePlugin[]} plugins Plugins being mounted 2651 | */ 2652 | 2653 | 2654 | Sortable.mount = function () { 2655 | for (var _len = arguments.length, plugins = new Array(_len), _key = 0; _key < _len; _key++) { 2656 | plugins[_key] = arguments[_key]; 2657 | } 2658 | 2659 | if (plugins[0].constructor === Array) plugins = plugins[0]; 2660 | plugins.forEach(function (plugin) { 2661 | if (!plugin.prototype || !plugin.prototype.constructor) { 2662 | throw "Sortable: Mounted plugin must be a constructor function, not ".concat({}.toString.call(plugin)); 2663 | } 2664 | 2665 | if (plugin.utils) Sortable.utils = _objectSpread({}, Sortable.utils, plugin.utils); 2666 | PluginManager.mount(plugin); 2667 | }); 2668 | }; 2669 | /** 2670 | * Create sortable instance 2671 | * @param {HTMLElement} el 2672 | * @param {Object} [options] 2673 | */ 2674 | 2675 | 2676 | Sortable.create = function (el, options) { 2677 | return new Sortable(el, options); 2678 | }; // Export 2679 | 2680 | 2681 | Sortable.version = version; 2682 | 2683 | var autoScrolls = [], 2684 | scrollEl, 2685 | scrollRootEl, 2686 | scrolling = false, 2687 | lastAutoScrollX, 2688 | lastAutoScrollY, 2689 | touchEvt$1, 2690 | pointerElemChangedInterval; 2691 | 2692 | function AutoScrollPlugin() { 2693 | function AutoScroll() { 2694 | this.defaults = { 2695 | scroll: true, 2696 | scrollSensitivity: 30, 2697 | scrollSpeed: 10, 2698 | bubbleScroll: true 2699 | }; // Bind all private methods 2700 | 2701 | for (var fn in this) { 2702 | if (fn.charAt(0) === '_' && typeof this[fn] === 'function') { 2703 | this[fn] = this[fn].bind(this); 2704 | } 2705 | } 2706 | } 2707 | 2708 | AutoScroll.prototype = { 2709 | dragStarted: function dragStarted(_ref) { 2710 | var originalEvent = _ref.originalEvent; 2711 | 2712 | if (this.sortable.nativeDraggable) { 2713 | on(document, 'dragover', this._handleAutoScroll); 2714 | } else { 2715 | if (this.options.supportPointer) { 2716 | on(document, 'pointermove', this._handleFallbackAutoScroll); 2717 | } else if (originalEvent.touches) { 2718 | on(document, 'touchmove', this._handleFallbackAutoScroll); 2719 | } else { 2720 | on(document, 'mousemove', this._handleFallbackAutoScroll); 2721 | } 2722 | } 2723 | }, 2724 | dragOverCompleted: function dragOverCompleted(_ref2) { 2725 | var originalEvent = _ref2.originalEvent; 2726 | 2727 | // For when bubbling is canceled and using fallback (fallback 'touchmove' always reached) 2728 | if (!this.options.dragOverBubble && !originalEvent.rootEl) { 2729 | this._handleAutoScroll(originalEvent); 2730 | } 2731 | }, 2732 | drop: function drop() { 2733 | if (this.sortable.nativeDraggable) { 2734 | off(document, 'dragover', this._handleAutoScroll); 2735 | } else { 2736 | off(document, 'pointermove', this._handleFallbackAutoScroll); 2737 | off(document, 'touchmove', this._handleFallbackAutoScroll); 2738 | off(document, 'mousemove', this._handleFallbackAutoScroll); 2739 | } 2740 | 2741 | clearPointerElemChangedInterval(); 2742 | clearAutoScrolls(); 2743 | cancelThrottle(); 2744 | }, 2745 | nulling: function nulling() { 2746 | touchEvt$1 = scrollRootEl = scrollEl = scrolling = pointerElemChangedInterval = lastAutoScrollX = lastAutoScrollY = null; 2747 | autoScrolls.length = 0; 2748 | }, 2749 | _handleFallbackAutoScroll: function _handleFallbackAutoScroll(evt) { 2750 | this._handleAutoScroll(evt, true); 2751 | }, 2752 | _handleAutoScroll: function _handleAutoScroll(evt, fallback) { 2753 | var _this = this; 2754 | 2755 | var x = (evt.touches ? evt.touches[0] : evt).clientX, 2756 | y = (evt.touches ? evt.touches[0] : evt).clientY, 2757 | elem = document.elementFromPoint(x, y); 2758 | touchEvt$1 = evt; // IE does not seem to have native autoscroll, 2759 | // Edge's autoscroll seems too conditional, 2760 | // MACOS Safari does not have autoscroll, 2761 | // Firefox and Chrome are good 2762 | 2763 | if (fallback || Edge || IE11OrLess || Safari) { 2764 | autoScroll(evt, this.options, elem, fallback); // Listener for pointer element change 2765 | 2766 | var ogElemScroller = getParentAutoScrollElement(elem, true); 2767 | 2768 | if (scrolling && (!pointerElemChangedInterval || x !== lastAutoScrollX || y !== lastAutoScrollY)) { 2769 | pointerElemChangedInterval && clearPointerElemChangedInterval(); // Detect for pointer elem change, emulating native DnD behaviour 2770 | 2771 | pointerElemChangedInterval = setInterval(function () { 2772 | var newElem = getParentAutoScrollElement(document.elementFromPoint(x, y), true); 2773 | 2774 | if (newElem !== ogElemScroller) { 2775 | ogElemScroller = newElem; 2776 | clearAutoScrolls(); 2777 | } 2778 | 2779 | autoScroll(evt, _this.options, newElem, fallback); 2780 | }, 10); 2781 | lastAutoScrollX = x; 2782 | lastAutoScrollY = y; 2783 | } 2784 | } else { 2785 | // if DnD is enabled (and browser has good autoscrolling), first autoscroll will already scroll, so get parent autoscroll of first autoscroll 2786 | if (!this.options.bubbleScroll || getParentAutoScrollElement(elem, true) === getWindowScrollingElement()) { 2787 | clearAutoScrolls(); 2788 | return; 2789 | } 2790 | 2791 | autoScroll(evt, this.options, getParentAutoScrollElement(elem, false), false); 2792 | } 2793 | } 2794 | }; 2795 | return _extends(AutoScroll, { 2796 | pluginName: 'scroll', 2797 | initializeByDefault: true 2798 | }); 2799 | } 2800 | 2801 | function clearAutoScrolls() { 2802 | autoScrolls.forEach(function (autoScroll) { 2803 | clearInterval(autoScroll.pid); 2804 | }); 2805 | autoScrolls = []; 2806 | } 2807 | 2808 | function clearPointerElemChangedInterval() { 2809 | clearInterval(pointerElemChangedInterval); 2810 | } 2811 | 2812 | var autoScroll = throttle(function (evt, options, rootEl, isFallback) { 2813 | // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521 2814 | if (!options.scroll) return; 2815 | var x = (evt.touches ? evt.touches[0] : evt).clientX, 2816 | y = (evt.touches ? evt.touches[0] : evt).clientY, 2817 | sens = options.scrollSensitivity, 2818 | speed = options.scrollSpeed, 2819 | winScroller = getWindowScrollingElement(); 2820 | var scrollThisInstance = false, 2821 | scrollCustomFn; // New scroll root, set scrollEl 2822 | 2823 | if (scrollRootEl !== rootEl) { 2824 | scrollRootEl = rootEl; 2825 | clearAutoScrolls(); 2826 | scrollEl = options.scroll; 2827 | scrollCustomFn = options.scrollFn; 2828 | 2829 | if (scrollEl === true) { 2830 | scrollEl = getParentAutoScrollElement(rootEl, true); 2831 | } 2832 | } 2833 | 2834 | var layersOut = 0; 2835 | var currentParent = scrollEl; 2836 | 2837 | do { 2838 | var el = currentParent, 2839 | rect = getRect(el), 2840 | top = rect.top, 2841 | bottom = rect.bottom, 2842 | left = rect.left, 2843 | right = rect.right, 2844 | width = rect.width, 2845 | height = rect.height, 2846 | canScrollX = void 0, 2847 | canScrollY = void 0, 2848 | scrollWidth = el.scrollWidth, 2849 | scrollHeight = el.scrollHeight, 2850 | elCSS = css(el), 2851 | scrollPosX = el.scrollLeft, 2852 | scrollPosY = el.scrollTop; 2853 | 2854 | if (el === winScroller) { 2855 | canScrollX = width < scrollWidth && (elCSS.overflowX === 'auto' || elCSS.overflowX === 'scroll' || elCSS.overflowX === 'visible'); 2856 | canScrollY = height < scrollHeight && (elCSS.overflowY === 'auto' || elCSS.overflowY === 'scroll' || elCSS.overflowY === 'visible'); 2857 | } else { 2858 | canScrollX = width < scrollWidth && (elCSS.overflowX === 'auto' || elCSS.overflowX === 'scroll'); 2859 | canScrollY = height < scrollHeight && (elCSS.overflowY === 'auto' || elCSS.overflowY === 'scroll'); 2860 | } 2861 | 2862 | var vx = canScrollX && (Math.abs(right - x) <= sens && scrollPosX + width < scrollWidth) - (Math.abs(left - x) <= sens && !!scrollPosX); 2863 | var vy = canScrollY && (Math.abs(bottom - y) <= sens && scrollPosY + height < scrollHeight) - (Math.abs(top - y) <= sens && !!scrollPosY); 2864 | 2865 | if (!autoScrolls[layersOut]) { 2866 | for (var i = 0; i <= layersOut; i++) { 2867 | if (!autoScrolls[i]) { 2868 | autoScrolls[i] = {}; 2869 | } 2870 | } 2871 | } 2872 | 2873 | if (autoScrolls[layersOut].vx != vx || autoScrolls[layersOut].vy != vy || autoScrolls[layersOut].el !== el) { 2874 | autoScrolls[layersOut].el = el; 2875 | autoScrolls[layersOut].vx = vx; 2876 | autoScrolls[layersOut].vy = vy; 2877 | clearInterval(autoScrolls[layersOut].pid); 2878 | 2879 | if (vx != 0 || vy != 0) { 2880 | scrollThisInstance = true; 2881 | /* jshint loopfunc:true */ 2882 | 2883 | autoScrolls[layersOut].pid = setInterval(function () { 2884 | // emulate drag over during autoscroll (fallback), emulating native DnD behaviour 2885 | if (isFallback && this.layer === 0) { 2886 | Sortable.active._onTouchMove(touchEvt$1); // To move ghost if it is positioned absolutely 2887 | 2888 | } 2889 | 2890 | var scrollOffsetY = autoScrolls[this.layer].vy ? autoScrolls[this.layer].vy * speed : 0; 2891 | var scrollOffsetX = autoScrolls[this.layer].vx ? autoScrolls[this.layer].vx * speed : 0; 2892 | 2893 | if (typeof scrollCustomFn === 'function') { 2894 | if (scrollCustomFn.call(Sortable.dragged.parentNode[expando], scrollOffsetX, scrollOffsetY, evt, touchEvt$1, autoScrolls[this.layer].el) !== 'continue') { 2895 | return; 2896 | } 2897 | } 2898 | 2899 | scrollBy(autoScrolls[this.layer].el, scrollOffsetX, scrollOffsetY); 2900 | }.bind({ 2901 | layer: layersOut 2902 | }), 24); 2903 | } 2904 | } 2905 | 2906 | layersOut++; 2907 | } while (options.bubbleScroll && currentParent !== winScroller && (currentParent = getParentAutoScrollElement(currentParent, false))); 2908 | 2909 | scrolling = scrollThisInstance; // in case another function catches scrolling as false in between when it is not 2910 | }, 30); 2911 | 2912 | var drop = function drop(_ref) { 2913 | var originalEvent = _ref.originalEvent, 2914 | putSortable = _ref.putSortable, 2915 | dragEl = _ref.dragEl, 2916 | activeSortable = _ref.activeSortable, 2917 | dispatchSortableEvent = _ref.dispatchSortableEvent, 2918 | hideGhostForTarget = _ref.hideGhostForTarget, 2919 | unhideGhostForTarget = _ref.unhideGhostForTarget; 2920 | if (!originalEvent) return; 2921 | var toSortable = putSortable || activeSortable; 2922 | hideGhostForTarget(); 2923 | var touch = originalEvent.changedTouches && originalEvent.changedTouches.length ? originalEvent.changedTouches[0] : originalEvent; 2924 | var target = document.elementFromPoint(touch.clientX, touch.clientY); 2925 | unhideGhostForTarget(); 2926 | 2927 | if (toSortable && !toSortable.el.contains(target)) { 2928 | dispatchSortableEvent('spill'); 2929 | this.onSpill({ 2930 | dragEl: dragEl, 2931 | putSortable: putSortable 2932 | }); 2933 | } 2934 | }; 2935 | 2936 | function Revert() {} 2937 | 2938 | Revert.prototype = { 2939 | startIndex: null, 2940 | dragStart: function dragStart(_ref2) { 2941 | var oldDraggableIndex = _ref2.oldDraggableIndex; 2942 | this.startIndex = oldDraggableIndex; 2943 | }, 2944 | onSpill: function onSpill(_ref3) { 2945 | var dragEl = _ref3.dragEl, 2946 | putSortable = _ref3.putSortable; 2947 | this.sortable.captureAnimationState(); 2948 | 2949 | if (putSortable) { 2950 | putSortable.captureAnimationState(); 2951 | } 2952 | 2953 | var nextSibling = getChild(this.sortable.el, this.startIndex, this.options); 2954 | 2955 | if (nextSibling) { 2956 | this.sortable.el.insertBefore(dragEl, nextSibling); 2957 | } else { 2958 | this.sortable.el.appendChild(dragEl); 2959 | } 2960 | 2961 | this.sortable.animateAll(); 2962 | 2963 | if (putSortable) { 2964 | putSortable.animateAll(); 2965 | } 2966 | }, 2967 | drop: drop 2968 | }; 2969 | 2970 | _extends(Revert, { 2971 | pluginName: 'revertOnSpill' 2972 | }); 2973 | 2974 | function Remove() {} 2975 | 2976 | Remove.prototype = { 2977 | onSpill: function onSpill(_ref4) { 2978 | var dragEl = _ref4.dragEl, 2979 | putSortable = _ref4.putSortable; 2980 | var parentSortable = putSortable || this.sortable; 2981 | parentSortable.captureAnimationState(); 2982 | dragEl.parentNode && dragEl.parentNode.removeChild(dragEl); 2983 | parentSortable.animateAll(); 2984 | }, 2985 | drop: drop 2986 | }; 2987 | 2988 | _extends(Remove, { 2989 | pluginName: 'removeOnSpill' 2990 | }); 2991 | 2992 | var lastSwapEl; 2993 | 2994 | function SwapPlugin() { 2995 | function Swap() { 2996 | this.defaults = { 2997 | swapClass: 'sortable-swap-highlight' 2998 | }; 2999 | } 3000 | 3001 | Swap.prototype = { 3002 | dragStart: function dragStart(_ref) { 3003 | var dragEl = _ref.dragEl; 3004 | lastSwapEl = dragEl; 3005 | }, 3006 | dragOverValid: function dragOverValid(_ref2) { 3007 | var completed = _ref2.completed, 3008 | target = _ref2.target, 3009 | onMove = _ref2.onMove, 3010 | activeSortable = _ref2.activeSortable, 3011 | changed = _ref2.changed, 3012 | cancel = _ref2.cancel; 3013 | if (!activeSortable.options.swap) return; 3014 | var el = this.sortable.el, 3015 | options = this.options; 3016 | 3017 | if (target && target !== el) { 3018 | var prevSwapEl = lastSwapEl; 3019 | 3020 | if (onMove(target) !== false) { 3021 | toggleClass(target, options.swapClass, true); 3022 | lastSwapEl = target; 3023 | } else { 3024 | lastSwapEl = null; 3025 | } 3026 | 3027 | if (prevSwapEl && prevSwapEl !== lastSwapEl) { 3028 | toggleClass(prevSwapEl, options.swapClass, false); 3029 | } 3030 | } 3031 | 3032 | changed(); 3033 | completed(true); 3034 | cancel(); 3035 | }, 3036 | drop: function drop(_ref3) { 3037 | var activeSortable = _ref3.activeSortable, 3038 | putSortable = _ref3.putSortable, 3039 | dragEl = _ref3.dragEl; 3040 | var toSortable = putSortable || this.sortable; 3041 | var options = this.options; 3042 | lastSwapEl && toggleClass(lastSwapEl, options.swapClass, false); 3043 | 3044 | if (lastSwapEl && (options.swap || putSortable && putSortable.options.swap)) { 3045 | if (dragEl !== lastSwapEl) { 3046 | toSortable.captureAnimationState(); 3047 | if (toSortable !== activeSortable) activeSortable.captureAnimationState(); 3048 | swapNodes(dragEl, lastSwapEl); 3049 | toSortable.animateAll(); 3050 | if (toSortable !== activeSortable) activeSortable.animateAll(); 3051 | } 3052 | } 3053 | }, 3054 | nulling: function nulling() { 3055 | lastSwapEl = null; 3056 | } 3057 | }; 3058 | return _extends(Swap, { 3059 | pluginName: 'swap', 3060 | eventProperties: function eventProperties() { 3061 | return { 3062 | swapItem: lastSwapEl 3063 | }; 3064 | } 3065 | }); 3066 | } 3067 | 3068 | function swapNodes(n1, n2) { 3069 | var p1 = n1.parentNode, 3070 | p2 = n2.parentNode, 3071 | i1, 3072 | i2; 3073 | if (!p1 || !p2 || p1.isEqualNode(n2) || p2.isEqualNode(n1)) return; 3074 | i1 = index(n1); 3075 | i2 = index(n2); 3076 | 3077 | if (p1.isEqualNode(p2) && i1 < i2) { 3078 | i2++; 3079 | } 3080 | 3081 | p1.insertBefore(n2, p1.children[i1]); 3082 | p2.insertBefore(n1, p2.children[i2]); 3083 | } 3084 | 3085 | var multiDragElements = [], 3086 | multiDragClones = [], 3087 | lastMultiDragSelect, 3088 | // for selection with modifier key down (SHIFT) 3089 | multiDragSortable, 3090 | initialFolding = false, 3091 | // Initial multi-drag fold when drag started 3092 | folding = false, 3093 | // Folding any other time 3094 | dragStarted = false, 3095 | dragEl$1, 3096 | clonesFromRect, 3097 | clonesHidden; 3098 | 3099 | function MultiDragPlugin() { 3100 | function MultiDrag(sortable) { 3101 | // Bind all private methods 3102 | for (var fn in this) { 3103 | if (fn.charAt(0) === '_' && typeof this[fn] === 'function') { 3104 | this[fn] = this[fn].bind(this); 3105 | } 3106 | } 3107 | 3108 | if (sortable.options.supportPointer) { 3109 | on(document, 'pointerup', this._deselectMultiDrag); 3110 | } else { 3111 | on(document, 'mouseup', this._deselectMultiDrag); 3112 | on(document, 'touchend', this._deselectMultiDrag); 3113 | } 3114 | 3115 | on(document, 'keydown', this._checkKeyDown); 3116 | on(document, 'keyup', this._checkKeyUp); 3117 | this.defaults = { 3118 | selectedClass: 'sortable-selected', 3119 | multiDragKey: null, 3120 | setData: function setData(dataTransfer, dragEl) { 3121 | var data = ''; 3122 | 3123 | if (multiDragElements.length && multiDragSortable === sortable) { 3124 | multiDragElements.forEach(function (multiDragElement, i) { 3125 | data += (!i ? '' : ', ') + multiDragElement.textContent; 3126 | }); 3127 | } else { 3128 | data = dragEl.textContent; 3129 | } 3130 | 3131 | dataTransfer.setData('Text', data); 3132 | } 3133 | }; 3134 | } 3135 | 3136 | MultiDrag.prototype = { 3137 | multiDragKeyDown: false, 3138 | isMultiDrag: false, 3139 | delayStartGlobal: function delayStartGlobal(_ref) { 3140 | var dragged = _ref.dragEl; 3141 | dragEl$1 = dragged; 3142 | }, 3143 | delayEnded: function delayEnded() { 3144 | this.isMultiDrag = ~multiDragElements.indexOf(dragEl$1); 3145 | }, 3146 | setupClone: function setupClone(_ref2) { 3147 | var sortable = _ref2.sortable, 3148 | cancel = _ref2.cancel; 3149 | if (!this.isMultiDrag) return; 3150 | 3151 | for (var i = 0; i < multiDragElements.length; i++) { 3152 | multiDragClones.push(clone(multiDragElements[i])); 3153 | multiDragClones[i].sortableIndex = multiDragElements[i].sortableIndex; 3154 | multiDragClones[i].draggable = false; 3155 | multiDragClones[i].style['will-change'] = ''; 3156 | toggleClass(multiDragClones[i], this.options.selectedClass, false); 3157 | multiDragElements[i] === dragEl$1 && toggleClass(multiDragClones[i], this.options.chosenClass, false); 3158 | } 3159 | 3160 | sortable._hideClone(); 3161 | 3162 | cancel(); 3163 | }, 3164 | clone: function clone(_ref3) { 3165 | var sortable = _ref3.sortable, 3166 | rootEl = _ref3.rootEl, 3167 | dispatchSortableEvent = _ref3.dispatchSortableEvent, 3168 | cancel = _ref3.cancel; 3169 | if (!this.isMultiDrag) return; 3170 | 3171 | if (!this.options.removeCloneOnHide) { 3172 | if (multiDragElements.length && multiDragSortable === sortable) { 3173 | insertMultiDragClones(true, rootEl); 3174 | dispatchSortableEvent('clone'); 3175 | cancel(); 3176 | } 3177 | } 3178 | }, 3179 | showClone: function showClone(_ref4) { 3180 | var cloneNowShown = _ref4.cloneNowShown, 3181 | rootEl = _ref4.rootEl, 3182 | cancel = _ref4.cancel; 3183 | if (!this.isMultiDrag) return; 3184 | insertMultiDragClones(false, rootEl); 3185 | multiDragClones.forEach(function (clone) { 3186 | css(clone, 'display', ''); 3187 | }); 3188 | cloneNowShown(); 3189 | clonesHidden = false; 3190 | cancel(); 3191 | }, 3192 | hideClone: function hideClone(_ref5) { 3193 | var _this = this; 3194 | 3195 | var sortable = _ref5.sortable, 3196 | cloneNowHidden = _ref5.cloneNowHidden, 3197 | cancel = _ref5.cancel; 3198 | if (!this.isMultiDrag) return; 3199 | multiDragClones.forEach(function (clone) { 3200 | css(clone, 'display', 'none'); 3201 | 3202 | if (_this.options.removeCloneOnHide && clone.parentNode) { 3203 | clone.parentNode.removeChild(clone); 3204 | } 3205 | }); 3206 | cloneNowHidden(); 3207 | clonesHidden = true; 3208 | cancel(); 3209 | }, 3210 | dragStartGlobal: function dragStartGlobal(_ref6) { 3211 | var sortable = _ref6.sortable; 3212 | 3213 | if (!this.isMultiDrag && multiDragSortable) { 3214 | multiDragSortable.multiDrag._deselectMultiDrag(); 3215 | } 3216 | 3217 | multiDragElements.forEach(function (multiDragElement) { 3218 | multiDragElement.sortableIndex = index(multiDragElement); 3219 | }); // Sort multi-drag elements 3220 | 3221 | multiDragElements = multiDragElements.sort(function (a, b) { 3222 | return a.sortableIndex - b.sortableIndex; 3223 | }); 3224 | dragStarted = true; 3225 | }, 3226 | dragStarted: function dragStarted(_ref7) { 3227 | var _this2 = this; 3228 | 3229 | var sortable = _ref7.sortable; 3230 | if (!this.isMultiDrag) return; 3231 | 3232 | if (this.options.sort) { 3233 | // Capture rects, 3234 | // hide multi drag elements (by positioning them absolute), 3235 | // set multi drag elements rects to dragRect, 3236 | // show multi drag elements, 3237 | // animate to rects, 3238 | // unset rects & remove from DOM 3239 | sortable.captureAnimationState(); 3240 | 3241 | if (this.options.animation) { 3242 | multiDragElements.forEach(function (multiDragElement) { 3243 | if (multiDragElement === dragEl$1) return; 3244 | css(multiDragElement, 'position', 'absolute'); 3245 | }); 3246 | var dragRect = getRect(dragEl$1, false, true, true); 3247 | multiDragElements.forEach(function (multiDragElement) { 3248 | if (multiDragElement === dragEl$1) return; 3249 | setRect(multiDragElement, dragRect); 3250 | }); 3251 | folding = true; 3252 | initialFolding = true; 3253 | } 3254 | } 3255 | 3256 | sortable.animateAll(function () { 3257 | folding = false; 3258 | initialFolding = false; 3259 | 3260 | if (_this2.options.animation) { 3261 | multiDragElements.forEach(function (multiDragElement) { 3262 | unsetRect(multiDragElement); 3263 | }); 3264 | } // Remove all auxiliary multidrag items from el, if sorting enabled 3265 | 3266 | 3267 | if (_this2.options.sort) { 3268 | removeMultiDragElements(); 3269 | } 3270 | }); 3271 | }, 3272 | dragOver: function dragOver(_ref8) { 3273 | var target = _ref8.target, 3274 | completed = _ref8.completed, 3275 | cancel = _ref8.cancel; 3276 | 3277 | if (folding && ~multiDragElements.indexOf(target)) { 3278 | completed(false); 3279 | cancel(); 3280 | } 3281 | }, 3282 | revert: function revert(_ref9) { 3283 | var fromSortable = _ref9.fromSortable, 3284 | rootEl = _ref9.rootEl, 3285 | sortable = _ref9.sortable, 3286 | dragRect = _ref9.dragRect; 3287 | 3288 | if (multiDragElements.length > 1) { 3289 | // Setup unfold animation 3290 | multiDragElements.forEach(function (multiDragElement) { 3291 | sortable.addAnimationState({ 3292 | target: multiDragElement, 3293 | rect: folding ? getRect(multiDragElement) : dragRect 3294 | }); 3295 | unsetRect(multiDragElement); 3296 | multiDragElement.fromRect = dragRect; 3297 | fromSortable.removeAnimationState(multiDragElement); 3298 | }); 3299 | folding = false; 3300 | insertMultiDragElements(!this.options.removeCloneOnHide, rootEl); 3301 | } 3302 | }, 3303 | dragOverCompleted: function dragOverCompleted(_ref10) { 3304 | var sortable = _ref10.sortable, 3305 | isOwner = _ref10.isOwner, 3306 | insertion = _ref10.insertion, 3307 | activeSortable = _ref10.activeSortable, 3308 | parentEl = _ref10.parentEl, 3309 | putSortable = _ref10.putSortable; 3310 | var options = this.options; 3311 | 3312 | if (insertion) { 3313 | // Clones must be hidden before folding animation to capture dragRectAbsolute properly 3314 | if (isOwner) { 3315 | activeSortable._hideClone(); 3316 | } 3317 | 3318 | initialFolding = false; // If leaving sort:false root, or already folding - Fold to new location 3319 | 3320 | if (options.animation && multiDragElements.length > 1 && (folding || !isOwner && !activeSortable.options.sort && !putSortable)) { 3321 | // Fold: Set all multi drag elements's rects to dragEl's rect when multi-drag elements are invisible 3322 | var dragRectAbsolute = getRect(dragEl$1, false, true, true); 3323 | multiDragElements.forEach(function (multiDragElement) { 3324 | if (multiDragElement === dragEl$1) return; 3325 | setRect(multiDragElement, dragRectAbsolute); // Move element(s) to end of parentEl so that it does not interfere with multi-drag clones insertion if they are inserted 3326 | // while folding, and so that we can capture them again because old sortable will no longer be fromSortable 3327 | 3328 | parentEl.appendChild(multiDragElement); 3329 | }); 3330 | folding = true; 3331 | } // Clones must be shown (and check to remove multi drags) after folding when interfering multiDragElements are moved out 3332 | 3333 | 3334 | if (!isOwner) { 3335 | // Only remove if not folding (folding will remove them anyways) 3336 | if (!folding) { 3337 | removeMultiDragElements(); 3338 | } 3339 | 3340 | if (multiDragElements.length > 1) { 3341 | var clonesHiddenBefore = clonesHidden; 3342 | 3343 | activeSortable._showClone(sortable); // Unfold animation for clones if showing from hidden 3344 | 3345 | 3346 | if (activeSortable.options.animation && !clonesHidden && clonesHiddenBefore) { 3347 | multiDragClones.forEach(function (clone) { 3348 | activeSortable.addAnimationState({ 3349 | target: clone, 3350 | rect: clonesFromRect 3351 | }); 3352 | clone.fromRect = clonesFromRect; 3353 | clone.thisAnimationDuration = null; 3354 | }); 3355 | } 3356 | } else { 3357 | activeSortable._showClone(sortable); 3358 | } 3359 | } 3360 | } 3361 | }, 3362 | dragOverAnimationCapture: function dragOverAnimationCapture(_ref11) { 3363 | var dragRect = _ref11.dragRect, 3364 | isOwner = _ref11.isOwner, 3365 | activeSortable = _ref11.activeSortable; 3366 | multiDragElements.forEach(function (multiDragElement) { 3367 | multiDragElement.thisAnimationDuration = null; 3368 | }); 3369 | 3370 | if (activeSortable.options.animation && !isOwner && activeSortable.multiDrag.isMultiDrag) { 3371 | clonesFromRect = _extends({}, dragRect); 3372 | var dragMatrix = matrix(dragEl$1, true); 3373 | clonesFromRect.top -= dragMatrix.f; 3374 | clonesFromRect.left -= dragMatrix.e; 3375 | } 3376 | }, 3377 | dragOverAnimationComplete: function dragOverAnimationComplete() { 3378 | if (folding) { 3379 | folding = false; 3380 | removeMultiDragElements(); 3381 | } 3382 | }, 3383 | drop: function drop(_ref12) { 3384 | var evt = _ref12.originalEvent, 3385 | rootEl = _ref12.rootEl, 3386 | parentEl = _ref12.parentEl, 3387 | sortable = _ref12.sortable, 3388 | dispatchSortableEvent = _ref12.dispatchSortableEvent, 3389 | oldIndex = _ref12.oldIndex, 3390 | putSortable = _ref12.putSortable; 3391 | var toSortable = putSortable || this.sortable; 3392 | if (!evt) return; 3393 | var options = this.options, 3394 | children = parentEl.children; // Multi-drag selection 3395 | 3396 | if (!dragStarted) { 3397 | if (options.multiDragKey && !this.multiDragKeyDown) { 3398 | this._deselectMultiDrag(); 3399 | } 3400 | 3401 | toggleClass(dragEl$1, options.selectedClass, !~multiDragElements.indexOf(dragEl$1)); 3402 | 3403 | if (!~multiDragElements.indexOf(dragEl$1)) { 3404 | multiDragElements.push(dragEl$1); 3405 | dispatchEvent({ 3406 | sortable: sortable, 3407 | rootEl: rootEl, 3408 | name: 'select', 3409 | targetEl: dragEl$1, 3410 | originalEvt: evt 3411 | }); // Modifier activated, select from last to dragEl 3412 | 3413 | if (evt.shiftKey && lastMultiDragSelect && sortable.el.contains(lastMultiDragSelect)) { 3414 | var lastIndex = index(lastMultiDragSelect), 3415 | currentIndex = index(dragEl$1); 3416 | 3417 | if (~lastIndex && ~currentIndex && lastIndex !== currentIndex) { 3418 | // Must include lastMultiDragSelect (select it), in case modified selection from no selection 3419 | // (but previous selection existed) 3420 | var n, i; 3421 | 3422 | if (currentIndex > lastIndex) { 3423 | i = lastIndex; 3424 | n = currentIndex; 3425 | } else { 3426 | i = currentIndex; 3427 | n = lastIndex + 1; 3428 | } 3429 | 3430 | for (; i < n; i++) { 3431 | if (~multiDragElements.indexOf(children[i])) continue; 3432 | toggleClass(children[i], options.selectedClass, true); 3433 | multiDragElements.push(children[i]); 3434 | dispatchEvent({ 3435 | sortable: sortable, 3436 | rootEl: rootEl, 3437 | name: 'select', 3438 | targetEl: children[i], 3439 | originalEvt: evt 3440 | }); 3441 | } 3442 | } 3443 | } else { 3444 | lastMultiDragSelect = dragEl$1; 3445 | } 3446 | 3447 | multiDragSortable = toSortable; 3448 | } else { 3449 | multiDragElements.splice(multiDragElements.indexOf(dragEl$1), 1); 3450 | lastMultiDragSelect = null; 3451 | dispatchEvent({ 3452 | sortable: sortable, 3453 | rootEl: rootEl, 3454 | name: 'deselect', 3455 | targetEl: dragEl$1, 3456 | originalEvt: evt 3457 | }); 3458 | } 3459 | } // Multi-drag drop 3460 | 3461 | 3462 | if (dragStarted && this.isMultiDrag) { 3463 | // Do not "unfold" after around dragEl if reverted 3464 | if ((parentEl[expando].options.sort || parentEl !== rootEl) && multiDragElements.length > 1) { 3465 | var dragRect = getRect(dragEl$1), 3466 | multiDragIndex = index(dragEl$1, ':not(.' + this.options.selectedClass + ')'); 3467 | if (!initialFolding && options.animation) dragEl$1.thisAnimationDuration = null; 3468 | toSortable.captureAnimationState(); 3469 | 3470 | if (!initialFolding) { 3471 | if (options.animation) { 3472 | dragEl$1.fromRect = dragRect; 3473 | multiDragElements.forEach(function (multiDragElement) { 3474 | multiDragElement.thisAnimationDuration = null; 3475 | 3476 | if (multiDragElement !== dragEl$1) { 3477 | var rect = folding ? getRect(multiDragElement) : dragRect; 3478 | multiDragElement.fromRect = rect; // Prepare unfold animation 3479 | 3480 | toSortable.addAnimationState({ 3481 | target: multiDragElement, 3482 | rect: rect 3483 | }); 3484 | } 3485 | }); 3486 | } // Multi drag elements are not necessarily removed from the DOM on drop, so to reinsert 3487 | // properly they must all be removed 3488 | 3489 | 3490 | removeMultiDragElements(); 3491 | multiDragElements.forEach(function (multiDragElement) { 3492 | if (children[multiDragIndex]) { 3493 | parentEl.insertBefore(multiDragElement, children[multiDragIndex]); 3494 | } else { 3495 | parentEl.appendChild(multiDragElement); 3496 | } 3497 | 3498 | multiDragIndex++; 3499 | }); // If initial folding is done, the elements may have changed position because they are now 3500 | // unfolding around dragEl, even though dragEl may not have his index changed, so update event 3501 | // must be fired here as Sortable will not. 3502 | 3503 | if (oldIndex === index(dragEl$1)) { 3504 | var update = false; 3505 | multiDragElements.forEach(function (multiDragElement) { 3506 | if (multiDragElement.sortableIndex !== index(multiDragElement)) { 3507 | update = true; 3508 | return; 3509 | } 3510 | }); 3511 | 3512 | if (update) { 3513 | dispatchSortableEvent('update'); 3514 | } 3515 | } 3516 | } // Must be done after capturing individual rects (scroll bar) 3517 | 3518 | 3519 | multiDragElements.forEach(function (multiDragElement) { 3520 | unsetRect(multiDragElement); 3521 | }); 3522 | toSortable.animateAll(); 3523 | } 3524 | 3525 | multiDragSortable = toSortable; 3526 | } // Remove clones if necessary 3527 | 3528 | 3529 | if (rootEl === parentEl || putSortable && putSortable.lastPutMode !== 'clone') { 3530 | multiDragClones.forEach(function (clone) { 3531 | clone.parentNode && clone.parentNode.removeChild(clone); 3532 | }); 3533 | } 3534 | }, 3535 | nullingGlobal: function nullingGlobal() { 3536 | this.isMultiDrag = dragStarted = false; 3537 | multiDragClones.length = 0; 3538 | }, 3539 | destroyGlobal: function destroyGlobal() { 3540 | this._deselectMultiDrag(); 3541 | 3542 | off(document, 'pointerup', this._deselectMultiDrag); 3543 | off(document, 'mouseup', this._deselectMultiDrag); 3544 | off(document, 'touchend', this._deselectMultiDrag); 3545 | off(document, 'keydown', this._checkKeyDown); 3546 | off(document, 'keyup', this._checkKeyUp); 3547 | }, 3548 | _deselectMultiDrag: function _deselectMultiDrag(evt) { 3549 | if (typeof dragStarted !== "undefined" && dragStarted) return; // Only deselect if selection is in this sortable 3550 | 3551 | if (multiDragSortable !== this.sortable) return; // Only deselect if target is not item in this sortable 3552 | 3553 | if (evt && closest(evt.target, this.options.draggable, this.sortable.el, false)) return; // Only deselect if left click 3554 | 3555 | if (evt && evt.button !== 0) return; 3556 | 3557 | while (multiDragElements.length) { 3558 | var el = multiDragElements[0]; 3559 | toggleClass(el, this.options.selectedClass, false); 3560 | multiDragElements.shift(); 3561 | dispatchEvent({ 3562 | sortable: this.sortable, 3563 | rootEl: this.sortable.el, 3564 | name: 'deselect', 3565 | targetEl: el, 3566 | originalEvt: evt 3567 | }); 3568 | } 3569 | }, 3570 | _checkKeyDown: function _checkKeyDown(evt) { 3571 | if (evt.key === this.options.multiDragKey) { 3572 | this.multiDragKeyDown = true; 3573 | } 3574 | }, 3575 | _checkKeyUp: function _checkKeyUp(evt) { 3576 | if (evt.key === this.options.multiDragKey) { 3577 | this.multiDragKeyDown = false; 3578 | } 3579 | } 3580 | }; 3581 | return _extends(MultiDrag, { 3582 | // Static methods & properties 3583 | pluginName: 'multiDrag', 3584 | utils: { 3585 | /** 3586 | * Selects the provided multi-drag item 3587 | * @param {HTMLElement} el The element to be selected 3588 | */ 3589 | select: function select(el) { 3590 | var sortable = el.parentNode[expando]; 3591 | if (!sortable || !sortable.options.multiDrag || ~multiDragElements.indexOf(el)) return; 3592 | 3593 | if (multiDragSortable && multiDragSortable !== sortable) { 3594 | multiDragSortable.multiDrag._deselectMultiDrag(); 3595 | 3596 | multiDragSortable = sortable; 3597 | } 3598 | 3599 | toggleClass(el, sortable.options.selectedClass, true); 3600 | multiDragElements.push(el); 3601 | }, 3602 | 3603 | /** 3604 | * Deselects the provided multi-drag item 3605 | * @param {HTMLElement} el The element to be deselected 3606 | */ 3607 | deselect: function deselect(el) { 3608 | var sortable = el.parentNode[expando], 3609 | index = multiDragElements.indexOf(el); 3610 | if (!sortable || !sortable.options.multiDrag || !~index) return; 3611 | toggleClass(el, sortable.options.selectedClass, false); 3612 | multiDragElements.splice(index, 1); 3613 | } 3614 | }, 3615 | eventProperties: function eventProperties() { 3616 | var _this3 = this; 3617 | 3618 | var oldIndicies = [], 3619 | newIndicies = []; 3620 | multiDragElements.forEach(function (multiDragElement) { 3621 | oldIndicies.push({ 3622 | multiDragElement: multiDragElement, 3623 | index: multiDragElement.sortableIndex 3624 | }); // multiDragElements will already be sorted if folding 3625 | 3626 | var newIndex; 3627 | 3628 | if (folding && multiDragElement !== dragEl$1) { 3629 | newIndex = -1; 3630 | } else if (folding) { 3631 | newIndex = index(multiDragElement, ':not(.' + _this3.options.selectedClass + ')'); 3632 | } else { 3633 | newIndex = index(multiDragElement); 3634 | } 3635 | 3636 | newIndicies.push({ 3637 | multiDragElement: multiDragElement, 3638 | index: newIndex 3639 | }); 3640 | }); 3641 | return { 3642 | items: _toConsumableArray(multiDragElements), 3643 | clones: [].concat(multiDragClones), 3644 | oldIndicies: oldIndicies, 3645 | newIndicies: newIndicies 3646 | }; 3647 | }, 3648 | optionListeners: { 3649 | multiDragKey: function multiDragKey(key) { 3650 | key = key.toLowerCase(); 3651 | 3652 | if (key === 'ctrl') { 3653 | key = 'Control'; 3654 | } else if (key.length > 1) { 3655 | key = key.charAt(0).toUpperCase() + key.substr(1); 3656 | } 3657 | 3658 | return key; 3659 | } 3660 | } 3661 | }); 3662 | } 3663 | 3664 | function insertMultiDragElements(clonesInserted, rootEl) { 3665 | multiDragElements.forEach(function (multiDragElement, i) { 3666 | var target = rootEl.children[multiDragElement.sortableIndex + (clonesInserted ? Number(i) : 0)]; 3667 | 3668 | if (target) { 3669 | rootEl.insertBefore(multiDragElement, target); 3670 | } else { 3671 | rootEl.appendChild(multiDragElement); 3672 | } 3673 | }); 3674 | } 3675 | /** 3676 | * Insert multi-drag clones 3677 | * @param {[Boolean]} elementsInserted Whether the multi-drag elements are inserted 3678 | * @param {HTMLElement} rootEl 3679 | */ 3680 | 3681 | 3682 | function insertMultiDragClones(elementsInserted, rootEl) { 3683 | multiDragClones.forEach(function (clone, i) { 3684 | var target = rootEl.children[clone.sortableIndex + (elementsInserted ? Number(i) : 0)]; 3685 | 3686 | if (target) { 3687 | rootEl.insertBefore(clone, target); 3688 | } else { 3689 | rootEl.appendChild(clone); 3690 | } 3691 | }); 3692 | } 3693 | 3694 | function removeMultiDragElements() { 3695 | multiDragElements.forEach(function (multiDragElement) { 3696 | if (multiDragElement === dragEl$1) return; 3697 | multiDragElement.parentNode && multiDragElement.parentNode.removeChild(multiDragElement); 3698 | }); 3699 | } 3700 | 3701 | Sortable.mount(new AutoScrollPlugin()); 3702 | Sortable.mount(Remove, Revert); 3703 | 3704 | Sortable.mount(new SwapPlugin()); 3705 | Sortable.mount(new MultiDragPlugin()); 3706 | 3707 | return Sortable; 3708 | 3709 | })); 3710 | -------------------------------------------------------------------------------- /client/v2/data/window/extra/notify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Notify = function() { 4 | const fragment = this.fragment = document.createRange().createContextualFragment(` 5 | 85 |
86 |
87 | Error 88 | This is a Text 89 | 90 |
91 |
92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | `); 103 | document.head.appendChild(fragment.querySelector('style')); 104 | const root = this.root = document.createElement('div'); 105 | root.classList.add('notify'); 106 | document.body.appendChild(root); 107 | }; 108 | Notify.prototype.display = function(message, type = 'info', delay = 500000) { 109 | const div = this.fragment.querySelector('div').cloneNode(true); 110 | div.querySelector('b').textContent = type; 111 | div.querySelector('span').textContent = message; 112 | div.querySelector('input').onclick = () => { 113 | div.remove(); 114 | }; 115 | div.classList.add(type); 116 | const svg = this.fragment.querySelector(`svg[data-id="${type}"]`).cloneNode(true); 117 | div.insertBefore(svg, div.firstChild); 118 | this.root.appendChild(div); 119 | 120 | window.setTimeout(() => div.remove(), delay); 121 | }; 122 | -------------------------------------------------------------------------------- /client/v2/data/window/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --bg-color: #efefef; 3 | --bg-active-color: #e5fbff; 4 | } 5 | 6 | body { 7 | font-size: 13px; 8 | font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; 9 | margin: 0; 10 | height: 100vh; 11 | overflow: hidden; 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | } 16 | 17 | h1 { 18 | font-weight: normal; 19 | margin-bottom: 40px; 20 | } 21 | input { 22 | background-color: var(--bg-color); 23 | border: none; 24 | outline: none; 25 | } 26 | input:focus { 27 | background-color: var(--bg-active-color); 28 | } 29 | input[type=number], 30 | input[type=password] { 31 | text-indent: 5px; 32 | } 33 | 34 | #join { 35 | display: flex; 36 | flex-direction: column; 37 | align-items: center; 38 | } 39 | #join form { 40 | display: grid; 41 | grid-template-rows: 32px 32px 50px; 42 | grid-gap: 5px; 43 | width: 100%; 44 | } 45 | 46 | body[data-mode="joined"] #join { 47 | display: none; 48 | } 49 | body[data-mode="login"] #meeting { 50 | display: none; 51 | } 52 | 53 | #meeting { 54 | width: 100vw; 55 | height: 100vh; 56 | display: grid; 57 | grid-template-columns: repeat(4, 1fr); 58 | } 59 | body[data-count="1"] #meeting { 60 | grid-template-columns: repeat(1, 1fr); 61 | grid-template-rows: repeat(1, 1fr); 62 | } 63 | body[data-count="2"] #meeting { 64 | grid-template-columns: repeat(2, 1fr); 65 | grid-template-rows: repeat(1, 1fr); 66 | } 67 | body[data-count="3"] #meeting, 68 | body[data-count="4"] #meeting { 69 | grid-template-columns: repeat(2, 1fr); 70 | grid-template-rows: repeat(2, 1fr); 71 | } 72 | @media screen and (max-width: 600px) { 73 | body[data-count="2"] #meeting { 74 | grid-template-columns: repeat(1, 1fr); 75 | grid-template-rows: repeat(2, 1fr); 76 | } 77 | body[data-count="3"] video-view:first-child { 78 | grid-column-end: 3; 79 | grid-column-start: 1; 80 | } 81 | } 82 | body[data-count="5"] #meeting, 83 | body[data-count="6"] #meeting { 84 | grid-template-columns: repeat(3, 1fr); 85 | grid-template-rows: repeat(2, 1fr); 86 | } 87 | @media screen and (max-width: 600px) { 88 | body[data-count="5"] #meeting, 89 | body[data-count="6"] #meeting { 90 | grid-template-columns: repeat(2, 1fr); 91 | grid-template-rows: repeat(3, 1fr); 92 | } 93 | } 94 | 95 | body[data-mode="login"] #socket { 96 | display: none; 97 | } 98 | #socket { 99 | display: flex; 100 | align-items: center; 101 | position: absolute; 102 | bottom: 10px; 103 | left: 10px; 104 | z-index: 2; 105 | padding: 5px 10px; 106 | border: none; 107 | color: #808080; 108 | text-shadow: 0 0 10px #000; 109 | } 110 | -------------------------------------------------------------------------------- /client/v2/data/window/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Meeting 6 | 7 | 8 | 9 |
10 |

Welcome to Meeting!

11 | 12 |
13 | 14 | 15 | 16 |
17 |
18 | 19 |
20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /client/v2/data/window/index.js: -------------------------------------------------------------------------------- 1 | /* global Meeting, configuration, Notify */ 2 | 3 | 'use strict'; 4 | 5 | const args = new URLSearchParams(location.search); 6 | 7 | let meeting; 8 | // auto-fill 9 | if (args.has('channel-id')) { 10 | document.querySelector('#join [name=channel-id]').value = args.get('channel-id'); 11 | document.querySelector('#join [type=password]').focus(); 12 | } 13 | 14 | chrome.storage.local.get({ 15 | 'signaling-server': '', 16 | 'signaling-token': '' 17 | }, prefs => { 18 | meeting = new Meeting(prefs['signaling-server'], prefs['signaling-token']); 19 | 20 | document.getElementById('join').addEventListener('submit', async e => { 21 | e.preventDefault(); 22 | const join = e.target.querySelector('input[type=submit]'); 23 | join.value = 'connecting...'; 24 | document.title = 'Connecting to Server...'; 25 | join.disabled = true; 26 | const cid = e.target.querySelector('input[type=number]').value; 27 | const password = e.target.querySelector('input[type=password]').value; 28 | await meeting.password(password); 29 | 30 | meeting.join(cid, { // extra info 31 | nickname: Math.random() 32 | }).then(() => { 33 | document.title = 'Joined on channel #' + cid; 34 | history.pushState({}, '', '?channel-id=' + cid); 35 | return navigator.mediaDevices.getUserMedia(configuration.media).then(stream => { 36 | document.body.dataset.mode = 'joined'; 37 | const me = document.getElementById('me'); 38 | me.onloadedmetadata = () => me.play(); 39 | me.srcObject = stream; 40 | }).catch(e => { 41 | throw Error('Cannot access to the user media: ' + e.message); 42 | }); 43 | }).catch(e => { 44 | document.title = 'Meeting'; 45 | join.value = 'Join'; 46 | join.disabled = false; 47 | alert(e.message); 48 | }); 49 | }); 50 | 51 | // allow or block joining 52 | document.getElementById('socket').addEventListener('change', e => { 53 | meeting.socket[e.target.checked ? 'restart' : 'close'](); 54 | if (e.target.checked) { 55 | meeting.send({ 56 | method: 'socket-restart' 57 | }); 58 | } 59 | }); 60 | meeting.onConnectionStateChanged.addListener((type, state) => { 61 | if (type === 'socket') { 62 | const e = document.querySelector('#socket input'); 63 | if (state === 'closed') { 64 | e.checked = false; 65 | } 66 | else { 67 | e.checked = true; 68 | } 69 | } 70 | }); 71 | meeting.onCountChanged.addListener(count => { 72 | document.body.dataset.count = count; 73 | }); 74 | meeting.onVideoRequest.addListener((video, peer) => { 75 | const view = document.createElement('video-view'); 76 | view.setAttribute('controls', 'mute close'); 77 | view.set(video); 78 | video.poster = 'assets/poster.png'; 79 | document.getElementById('meeting').appendChild(view); 80 | view.onCloseRequested.addListener(() => { 81 | peer.close(); 82 | }); 83 | }); 84 | 85 | const notify = new Notify(); 86 | meeting.onMessage.addListener((guid, o) => { 87 | if ('message' in o) { 88 | notify.display(o.message, undefined, 5000); 89 | } 90 | }); 91 | 92 | // 93 | document.getElementById('me').onCloseRequested.addListener(() => { 94 | location.reload(); 95 | }); 96 | }); 97 | 98 | -------------------------------------------------------------------------------- /client/v2/data/window/meeting/meeting.js: -------------------------------------------------------------------------------- 1 | /* global Peer, Socket, safe */ 2 | 'use strict'; 3 | 4 | const configuration = { 5 | server: { 6 | iceServers: [{ 7 | 'urls': [ 8 | 'stun:stun.services.mozilla.com', 9 | 'stun:stun.l.google.com:19302', 10 | 'stun:stun.mit.de:3478', 11 | 'stun:stun.gmx.net:3478' 12 | ] 13 | }] 14 | }, 15 | media: { 16 | audio: true, 17 | video: true 18 | }, 19 | timeout: { 20 | peer: 10000, 21 | resize: 1000 22 | } 23 | }; 24 | 25 | const COMMON = Symbol('common'); 26 | const INCLUDE = Symbol('include'); 27 | 28 | class Meeting { 29 | constructor(orign, key) { 30 | const socket = this.socket = new class extends Socket { 31 | encrypt(msg) { 32 | return safe.encrypt(msg); 33 | } 34 | decrypt(msg) { 35 | return safe.decrypt(msg); 36 | } 37 | }(orign, key); 38 | socket.onMessage.addListener(msg => { 39 | if (msg.method === 'whoami') { 40 | this.client(msg.sender).then(peer => { 41 | peer.extra = msg.extra || {}; 42 | this[INCLUDE](msg.sender, peer); 43 | socket.send({ 44 | method: 'whoami-reply', 45 | extra: this.exta 46 | }, msg.sender); 47 | }); 48 | } 49 | else if (msg.method === 'whoami-reply') { 50 | this.server(msg.sender).then(peer => { 51 | peer.extra = msg.extra || {}; 52 | this[INCLUDE](msg.sender, peer); 53 | peer.offer(); 54 | }); 55 | } 56 | }); 57 | 58 | this.videos = {}; 59 | this.onVideoRequest = new Event(); 60 | 61 | this.peers = {}; 62 | this.onMessage = new Event(); 63 | 64 | this.onConnectionStateChanged = new Event(); 65 | socket.onConnectionStateChanged.addListener((type, state) => { 66 | this.onConnectionStateChanged.emit(type, state); 67 | }); 68 | 69 | // count 70 | this.onCountChanged = new Event(); 71 | } 72 | get count() { 73 | return this._count || 1; 74 | } 75 | set count(v) { 76 | this._count = v; 77 | this.onCountChanged.emit(v); 78 | } 79 | [COMMON](stream, recipient) { 80 | const p = new Peer(this.socket, configuration.server, recipient); 81 | 82 | let video; 83 | // add track 84 | p.onTrack.addListener(stream => { 85 | if (!video) { 86 | const tid = setTimeout(() => { 87 | console.warn('no signal is received from the peer', 'closing peer connection'); 88 | this.onMessage.emit(recipient, { 89 | message: 'No signal is received' 90 | }); 91 | p.close(); 92 | }, configuration.timeout.peer); 93 | video = document.createElement('video'); 94 | video.onloadedmetadata = () => { 95 | video.play(); 96 | clearTimeout(tid); 97 | }; 98 | this.onVideoRequest.emit(video, p); 99 | this.videos[recipient] = video; 100 | this.count += 1; 101 | 102 | // monitor video element size changes 103 | const send = e => { 104 | const width = Math.min(600, e.contentRect.width); 105 | const height = width * (e.contentRect.height / e.contentRect.width); 106 | p.send({ 107 | method: 'video-constrain', 108 | value: { 109 | width: { 110 | max: width 111 | }, 112 | height: { 113 | max: height 114 | }, 115 | frameRate: { 116 | max: 30 117 | } 118 | } 119 | }); 120 | }; 121 | const resizeObserver = new ResizeObserver(([e]) => { 122 | clearTimeout(send.id); 123 | send.id = setTimeout(send, configuration.timeout.resize, e); 124 | }); 125 | resizeObserver.observe(video); 126 | } 127 | 128 | // don't set srcObject again if it is already set. 129 | if (video.srcObject) { 130 | return; 131 | } 132 | video.srcObject = stream; 133 | }); 134 | stream.getTracks().forEach(track => p.pc.addTrack(track, stream)); 135 | 136 | p.onMessage.addListener(request => { 137 | this.onMessage.emit(recipient, request); 138 | 139 | if (request.method === 'video-constrain') { 140 | const [vst] = stream.getVideoTracks(); 141 | vst.applyConstraints(request.value); 142 | } 143 | else if (request.method === 'socket-restart') { 144 | this.socket.restart(); 145 | } 146 | }); 147 | 148 | return p; 149 | } 150 | 151 | async server(recipient) { 152 | const stream = await navigator.mediaDevices.getUserMedia(configuration.media); 153 | const p = this[COMMON](stream, recipient); 154 | p.channel(); 155 | 156 | return p; 157 | } 158 | async client(recipient) { 159 | const stream = await navigator.mediaDevices.getUserMedia(configuration.media); 160 | return this[COMMON](stream, recipient); 161 | } 162 | password(v) { 163 | return safe.password(v); 164 | } 165 | 166 | /* include new peer */ 167 | [INCLUDE](guid, p) { 168 | this.peers[guid] = p; 169 | p.onConnectionStateChanged.addListener((type, state) => { 170 | if ( 171 | (type === 'channel' && state === 'closed') || 172 | (type === 'peer' && (state === 'disconnected' || state === 'failed')) 173 | ) { 174 | const v = this.videos[guid]; 175 | if (v) { 176 | v.remove(); 177 | delete this.videos[guid]; 178 | this.count -= 1; 179 | } 180 | delete this.peers[guid]; 181 | } 182 | this.onConnectionStateChanged.emit(type, state); 183 | }); 184 | } 185 | 186 | join(cid, extra = {}) { 187 | this.exta = extra; 188 | return this.socket.create(cid).then(() => this.socket.send({ 189 | method: 'whoami', 190 | extra 191 | })); 192 | } 193 | 194 | send(o) { 195 | for (const peer of Object.values(this.peers)) { 196 | peer.send(o); 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /client/v2/data/window/meeting/peer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const KEY = 'SFpKNlNlb1RkMDNIc1ZNNHZ0d1pmNnF1VHY2NHl1TVlmVEVOQmdvcDR3TG9NWmh3QUNvWGxwTXBhRHJK'; 4 | // const ORIGIN = 'wss://connect.websocket.in/v3/[CHANNEL_ID]?apiKey=[API_KEY]'; 5 | // const ORIGIN = 'wss://meeting-server.herokuapp.com/[CHANNEL_ID]?apiKey=[API_KEY]'; 6 | // const ORIGIN = 'ws://127.0.0.1:8000/[CHANNEL_ID]?apiKey=[API_KEY]'; 7 | // const ORIGIN = 'wss://meetingserver.eu.openode.io/[CHANNEL_ID]?apiKey=[API_KEY]'; 8 | const ORIGIN = 'wss://connect.meetingserver.repl.co/[CHANNEL_ID]?apiKey=[API_KEY]'; 9 | const GUID = ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => 10 | (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) 11 | ); 12 | 13 | class Socket { 14 | constructor(origin, key) { 15 | this.key = key || atob(KEY); 16 | this.server = (origin || ORIGIN).replace('[API_KEY]', this.key); 17 | this.onConnectionStateChanged = new Event(); 18 | this.onMessage = new Event(); 19 | } 20 | create(cid = 1) { 21 | if (cid < 1 || cid > 10000) { 22 | throw Error('range limit'); 23 | } 24 | this.cid = cid; 25 | 26 | return new Promise((resolve, reject) => { 27 | const socket = this.socket = new WebSocket(this.server.replace('[CHANNEL_ID]', cid)); 28 | socket.onopen = () => { 29 | setTimeout(() => { 30 | if (socket.readyState === 1) { 31 | resolve(); 32 | this.onConnectionStateChanged.emit('socket', 'ready'); 33 | } 34 | else { 35 | reject(Error('connection closed')); 36 | this.onConnectionStateChanged.emit('socket', 'closed'); 37 | } 38 | }, 500); 39 | }; 40 | socket.onclose = () => { 41 | reject(Error('connection closed')); 42 | this.onConnectionStateChanged.emit('socket', 'closed'); 43 | }; 44 | socket.onmessage = e => { 45 | this.decrypt(e.data).then(msg => { 46 | msg = JSON.parse(msg); 47 | if ('recipient' in msg) { 48 | if (msg.recipient === GUID) { 49 | this.onMessage.emit(msg); 50 | } 51 | } 52 | else { 53 | this.onMessage.emit(msg); 54 | } 55 | }).catch(() => { 56 | try { 57 | const j = JSON.parse(e.data); 58 | if (j.error) { 59 | alert(j.error); 60 | } 61 | else { 62 | throw Error('message is not encrypted'); 63 | } 64 | } 65 | catch (e) { 66 | console.warn('cannot decrypt the encrypted message'); 67 | } 68 | }); 69 | }; 70 | }); 71 | } 72 | encrypt(msg) { 73 | return Promise.resolve(msg); 74 | } 75 | decrypt(msg) { 76 | return Promise.resolve(msg); 77 | } 78 | send(msg, recipient) { 79 | const o = { 80 | ...msg, 81 | sender: GUID 82 | }; 83 | if (recipient) { 84 | o.recipient = recipient; 85 | } 86 | this.encrypt(JSON.stringify(o)).then(e => this.socket.send(e)); 87 | } 88 | close() { 89 | try { 90 | this.socket.close(); 91 | } 92 | catch (e) {} 93 | } 94 | restart() { 95 | this.close(); 96 | this.create(this.cid); 97 | } 98 | } 99 | 100 | class Peer { 101 | constructor(socket, configuration, recipient) { 102 | const pc = this.pc = new RTCPeerConnection(configuration); 103 | 104 | this.recipient = recipient; 105 | 106 | this.onConnectionStateChanged = new Event(); 107 | pc.onconnectionstatechange = () => this.onConnectionStateChanged.emit('peer', pc.connectionState); 108 | 109 | let cid; // send multiple candidates in one request 110 | const candidates = []; 111 | pc.onicecandidate = ({candidate}) => { 112 | if (candidate) { 113 | this.onConnectionStateChanged.emit('candidate', 'generated'); 114 | clearTimeout(cid); 115 | cid = setTimeout(() => { 116 | socket.send({candidates}, recipient); 117 | }, 500); 118 | candidates.push(candidate); 119 | } 120 | }; 121 | this.socket = socket; 122 | socket.onMessage.addListener(msg => this.parse(msg)); 123 | 124 | // track 125 | this.onTrack = new Event(); 126 | pc.ontrack = e => this.onTrack.emit(e.streams[0]); 127 | 128 | // channel 129 | this.onMessage = new Event(); 130 | this.channels = {}; 131 | pc.ondatachannel = e => { 132 | const ch = e.channel; 133 | this.channel(ch.label, ch); 134 | }; 135 | } 136 | // process messages from the other end 137 | async parse(msg) { 138 | // all new client peers receive messages from any recipient. Only accept from recipient 139 | if (msg.sender !== this.recipient) { 140 | // console.log('ignored', msg, msg.sender, this.recipient); 141 | return; 142 | } 143 | const {desc, candidates} = msg; 144 | const {socket, pc} = this; 145 | if (candidates) { 146 | for (const candidate of candidates) { 147 | pc.addIceCandidate(candidate); 148 | } 149 | } 150 | else if (desc && desc.type === 'answer') { // server-side 151 | this.onConnectionStateChanged.emit('desc', 'received'); 152 | pc.setRemoteDescription(desc); 153 | } 154 | else if (desc && desc.type === 'offer') { // client-side 155 | this.onConnectionStateChanged.emit('desc', 'received'); 156 | await pc.setRemoteDescription(desc); 157 | const answer = await pc.createAnswer(); 158 | 159 | await pc.setLocalDescription(answer); 160 | socket.send({desc: answer}, this.recipient); 161 | } 162 | } 163 | send(msg, name = 'json') { 164 | try { 165 | this.channels[name].send(JSON.stringify(msg)); 166 | } 167 | catch (e) {} 168 | } 169 | // create offer (server-side) 170 | async offer() { 171 | const {socket, pc} = this; 172 | const offer = await pc.createOffer(); 173 | await pc.setLocalDescription(offer); 174 | socket.send({desc: pc.localDescription}, this.recipient); 175 | } 176 | // channel 177 | channel(name = 'json', ch) { 178 | ch = ch || this.pc.createDataChannel(name); 179 | ch.onmessage = e => this.onMessage.emit(JSON.parse(e.data)); 180 | ch.onopen = () => this.onConnectionStateChanged.emit('channel', 'ready'); 181 | ch.onclose = () => this.onConnectionStateChanged.emit('channel', 'closed'); 182 | this.channels[ch.label] = ch; 183 | } 184 | // close 185 | close() { 186 | const {pc} = this; 187 | pc.close(); 188 | this.onConnectionStateChanged.emit('channel', 'closed'); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /client/v2/data/window/meeting/safe.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const safe = {}; 4 | window.safe = safe; 5 | 6 | { 7 | const iv = crypto.getRandomValues(new Uint8Array(16)); 8 | 9 | const toBuffer = str => { 10 | const bytes = new Uint8Array(str.length); 11 | [...str].forEach((c, i) => bytes[i] = c.charCodeAt(0)); 12 | return bytes; 13 | }; 14 | const toString = buffer => [...buffer].map(b => String.fromCharCode(b)).join(''); 15 | 16 | let key = ''; 17 | safe.password = async password => { 18 | key = await crypto.subtle.digest({ 19 | name: 'SHA-256' 20 | }, toBuffer(password)).then(result => crypto.subtle.importKey('raw', result, { 21 | name: 'AES-CBC' 22 | }, false, ['encrypt', 'decrypt'])); 23 | }; 24 | safe.encrypt = async data => { 25 | const result = await crypto.subtle.encrypt({ 26 | name: 'AES-CBC', 27 | iv 28 | }, key, toBuffer(data)); 29 | return new Promise(resolve => { 30 | const reader = new FileReader(); 31 | reader.onload = () => resolve(reader.result); 32 | reader.readAsDataURL(new Blob([iv, result], {type: 'application/octet-binary'})); 33 | }); 34 | }; 35 | safe.decrypt = async data => { 36 | const result = await crypto.subtle.decrypt({ 37 | name: 'AES-CBC', 38 | iv 39 | }, key, toBuffer(atob(data.split(',')[1]))); 40 | return toString((new Uint8Array(result)).subarray(16)); 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /client/v2/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "version": "0.1.1", 4 | "name": "Meeting", 5 | "description": "a secure peer-to-peer video and audio conference using WebRTC that works with or without INTERNET access", 6 | "permissions": [ 7 | "storage" 8 | ], 9 | "icons": { 10 | "16": "data/icons/16.png", 11 | "19": "data/icons/19.png", 12 | "32": "data/icons/32.png", 13 | "38": "data/icons/38.png", 14 | "48": "data/icons/48.png", 15 | "64": "data/icons/64.png", 16 | "128": "data/icons/128.png", 17 | "256": "data/icons/256.png", 18 | "512": "data/icons/512.png" 19 | }, 20 | "background": { 21 | "persistent": false, 22 | "scripts": [ 23 | "background.js" 24 | ] 25 | }, 26 | "browser_action": {}, 27 | "options_ui": { 28 | "page": "data/options/index.html", 29 | "chrome_style": true 30 | }, 31 | "homepage_url": "https://add0n.com/meeting.html" 32 | } 33 | -------------------------------------------------------------------------------- /client/v3/data/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v3/data/icons/128.png -------------------------------------------------------------------------------- /client/v3/data/icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v3/data/icons/16.png -------------------------------------------------------------------------------- /client/v3/data/icons/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v3/data/icons/19.png -------------------------------------------------------------------------------- /client/v3/data/icons/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v3/data/icons/256.png -------------------------------------------------------------------------------- /client/v3/data/icons/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v3/data/icons/32.png -------------------------------------------------------------------------------- /client/v3/data/icons/38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v3/data/icons/38.png -------------------------------------------------------------------------------- /client/v3/data/icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v3/data/icons/48.png -------------------------------------------------------------------------------- /client/v3/data/icons/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v3/data/icons/512.png -------------------------------------------------------------------------------- /client/v3/data/icons/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v3/data/icons/64.png -------------------------------------------------------------------------------- /client/v3/data/options/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 14px; 3 | font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif; 4 | background-color: #fff; 5 | color: #4d5156; 6 | width: min(100% - 2rem, 70rem); 7 | margin-inline: auto; 8 | } 9 | select, 10 | button, 11 | input[type=submit], 12 | input[type=button] { 13 | height: 24px; 14 | color: #444; 15 | background-image: linear-gradient(rgb(237, 237, 237), rgb(237, 237, 237) 38%, rgb(222, 222, 222)); 16 | box-shadow: rgba(0, 0, 0, 0.08) 0 1px 0, rgba(255, 255, 255, 0.75) 0 1px 2px inset; 17 | text-shadow: rgb(240, 240, 240) 0 1px 0; 18 | } 19 | select, 20 | button, 21 | textarea, 22 | input { 23 | border: solid 1px rgba(0, 0, 0, 0.25); 24 | } 25 | input[type=button]:disabled { 26 | opacity: 0.5; 27 | } 28 | input[type=text] { 29 | width: 100%; 30 | box-sizing: border-box; 31 | } 32 | textarea { 33 | width: 100%; 34 | box-sizing: border-box; 35 | display: block; 36 | } 37 | textarea, 38 | input[type=text], 39 | input[type=number] { 40 | padding: 5px; 41 | outline: none; 42 | } 43 | textarea:focus, 44 | input[type=text]:focus, 45 | input[type=number]:focus { 46 | background-color: #e5f8ff; 47 | } 48 | a, 49 | a:visited { 50 | color: #07c; 51 | } 52 | #signaling { 53 | display: grid; 54 | grid-template-columns: min-content 1fr; 55 | grid-gap: 10px; 56 | white-space: nowrap; 57 | align-items: center; 58 | } 59 | -------------------------------------------------------------------------------- /client/v3/data/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Options Page :: Meeting 5 | 6 | 7 | 8 |
9 | Signaling Server 10 | 11 | Signaling Token 12 | 13 |
14 |

15 | 16 |

17 |

18 | 19 | 20 | 21 |

22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /client/v3/data/options/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const toast = document.getElementById('toast'); 4 | 5 | chrome.storage.local.get({ 6 | 'signaling-server': '', 7 | 'signaling-token': '' 8 | }, prefs => { 9 | document.getElementById('signaling-server').value = prefs['signaling-server'] || 'wss://connect.meetingserver.repl.co/[CHANNEL_ID]?apiKey=[API_KEY]'; 10 | document.getElementById('signaling-token').value = prefs['signaling-token']; 11 | }); 12 | 13 | document.getElementById('save').addEventListener('click', () => chrome.storage.local.set({ 14 | 'signaling-server': document.getElementById('signaling-server').value, 15 | 'signaling-token': document.getElementById('signaling-token').value 16 | }, () => { 17 | toast.textContent = 'Options saved'; 18 | window.setTimeout(() => toast.textContent = '', 750); 19 | })); 20 | 21 | // reset 22 | document.getElementById('reset').addEventListener('click', e => { 23 | if (e.detail === 1) { 24 | toast.textContent = 'Double-click to reset!'; 25 | window.setTimeout(() => toast.textContent = '', 750); 26 | } 27 | else { 28 | localStorage.clear(); 29 | chrome.storage.local.clear(() => { 30 | chrome.runtime.reload(); 31 | window.close(); 32 | }); 33 | } 34 | }); 35 | // support 36 | document.getElementById('support').addEventListener('click', () => chrome.tabs.create({ 37 | url: chrome.runtime.getManifest().homepage_url + '?rd=donate' 38 | })); 39 | -------------------------------------------------------------------------------- /client/v3/data/window/assets/poster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandler-stimson/meeting/5c030edaaa04ff8b0d0c0bacbd59edcf5d5017c9/client/v3/data/window/assets/poster.png -------------------------------------------------------------------------------- /client/v3/data/window/components/video-view.js: -------------------------------------------------------------------------------- 1 | class VideoView extends HTMLElement { 2 | constructor() { 3 | super(); 4 | const shadow = this.attachShadow({ 5 | mode: 'open' 6 | }); 7 | shadow.innerHTML = ` 8 | 64 |
65 | 66 | 67 | 68 | 69 |
70 | 71 | 72 | 73 |
74 |
75 | `; 76 | this.onCloseRequested = new Event(); 77 | } 78 | connectedCallback() { 79 | this.shadowRoot.getElementById('close').addEventListener('click', () => { 80 | this.onCloseRequested.emit(); 81 | }); 82 | this.shadowRoot.getElementById('mute').addEventListener('click', () => { 83 | const v = this.querySelector('video'); 84 | v.muted = v.muted === false; 85 | this.dataset.muted = v.muted; 86 | }); 87 | // remove the entire element if video element is removed 88 | this.shadowRoot.querySelector('slot').addEventListener('slotchange', () => { 89 | const v = this.querySelector('video'); 90 | if (!v) { 91 | this.remove(); 92 | } 93 | }); 94 | } 95 | set(video) { 96 | this.appendChild(video); 97 | } 98 | set srcObject(o) { 99 | this.querySelector('video').srcObject = o; 100 | } 101 | play() { 102 | this.querySelector('video').play(); 103 | } 104 | set onloadedmetadata(c) { 105 | this.querySelector('video').onloadedmetadata = c; 106 | } 107 | } 108 | window.customElements.define('video-view', VideoView); 109 | -------------------------------------------------------------------------------- /client/v3/data/window/event.js: -------------------------------------------------------------------------------- 1 | class Event { 2 | constructor() { 3 | this.callbacks = []; 4 | } 5 | addListener(c) { 6 | this.callbacks.push(c); 7 | } 8 | emit(...args) { 9 | for (const c of this.callbacks) { 10 | c(...args); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/v3/data/window/extra.js: -------------------------------------------------------------------------------- 1 | /* global Sortable */ 2 | 'use strict'; 3 | 4 | Sortable.create(document.getElementById('meeting')); 5 | 6 | -------------------------------------------------------------------------------- /client/v3/data/window/extra/ReadMe: -------------------------------------------------------------------------------- 1 | https://github.com/SortableJS/Sortable/releases/tag/1.10.2 2 | -------------------------------------------------------------------------------- /client/v3/data/window/extra/notify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Notify = function() { 4 | const fragment = this.fragment = document.createRange().createContextualFragment(` 5 | 85 |
86 |
87 | Error 88 | This is a Text 89 | 90 |
91 |
92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | `); 103 | document.head.appendChild(fragment.querySelector('style')); 104 | const root = this.root = document.createElement('div'); 105 | root.classList.add('notify'); 106 | document.body.appendChild(root); 107 | }; 108 | Notify.prototype.display = function(message, type = 'info', delay = 500000) { 109 | const div = this.fragment.querySelector('div').cloneNode(true); 110 | div.querySelector('b').textContent = type; 111 | div.querySelector('span').textContent = message; 112 | div.querySelector('input').onclick = () => { 113 | div.remove(); 114 | }; 115 | div.classList.add(type); 116 | const svg = this.fragment.querySelector(`svg[data-id="${type}"]`).cloneNode(true); 117 | div.insertBefore(svg, div.firstChild); 118 | this.root.appendChild(div); 119 | 120 | window.setTimeout(() => div.remove(), delay); 121 | }; 122 | -------------------------------------------------------------------------------- /client/v3/data/window/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --bg-color: #efefef; 3 | --bg-active-color: #e5fbff; 4 | } 5 | 6 | body { 7 | font-size: 13px; 8 | font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; 9 | margin: 0; 10 | height: 100vh; 11 | overflow: hidden; 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | } 16 | 17 | h1 { 18 | font-weight: normal; 19 | margin-bottom: 40px; 20 | } 21 | input { 22 | background-color: var(--bg-color); 23 | border: none; 24 | outline: none; 25 | } 26 | input:focus { 27 | background-color: var(--bg-active-color); 28 | } 29 | input[type=number], 30 | input[type=password] { 31 | text-indent: 5px; 32 | } 33 | 34 | #join { 35 | display: flex; 36 | flex-direction: column; 37 | align-items: center; 38 | } 39 | #join form { 40 | display: grid; 41 | grid-template-rows: 32px 32px 50px; 42 | grid-gap: 5px; 43 | width: 100%; 44 | } 45 | 46 | body[data-mode="joined"] #join { 47 | display: none; 48 | } 49 | body[data-mode="login"] #meeting { 50 | display: none; 51 | } 52 | 53 | #meeting { 54 | width: 100vw; 55 | height: 100vh; 56 | display: grid; 57 | grid-template-columns: repeat(4, 1fr); 58 | } 59 | body[data-count="1"] #meeting { 60 | grid-template-columns: repeat(1, 1fr); 61 | grid-template-rows: repeat(1, 1fr); 62 | } 63 | body[data-count="2"] #meeting { 64 | grid-template-columns: repeat(2, 1fr); 65 | grid-template-rows: repeat(1, 1fr); 66 | } 67 | body[data-count="3"] #meeting, 68 | body[data-count="4"] #meeting { 69 | grid-template-columns: repeat(2, 1fr); 70 | grid-template-rows: repeat(2, 1fr); 71 | } 72 | @media screen and (max-width: 600px) { 73 | body[data-count="2"] #meeting { 74 | grid-template-columns: repeat(1, 1fr); 75 | grid-template-rows: repeat(2, 1fr); 76 | } 77 | body[data-count="3"] video-view:first-child { 78 | grid-column-end: 3; 79 | grid-column-start: 1; 80 | } 81 | } 82 | body[data-count="5"] #meeting, 83 | body[data-count="6"] #meeting { 84 | grid-template-columns: repeat(3, 1fr); 85 | grid-template-rows: repeat(2, 1fr); 86 | } 87 | @media screen and (max-width: 600px) { 88 | body[data-count="5"] #meeting, 89 | body[data-count="6"] #meeting { 90 | grid-template-columns: repeat(2, 1fr); 91 | grid-template-rows: repeat(3, 1fr); 92 | } 93 | } 94 | 95 | body[data-mode="login"] #socket { 96 | display: none; 97 | } 98 | #socket { 99 | display: flex; 100 | align-items: center; 101 | position: absolute; 102 | bottom: 10px; 103 | left: 10px; 104 | z-index: 2; 105 | padding: 5px 10px; 106 | border: none; 107 | color: #808080; 108 | text-shadow: 0 0 10px #000; 109 | } 110 | -------------------------------------------------------------------------------- /client/v3/data/window/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Meeting 6 | 7 | 8 | 9 |
10 |

Welcome to Meeting!

11 | 12 |
13 | 14 | 15 | 16 |
17 |
18 | 19 |
20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /client/v3/data/window/index.js: -------------------------------------------------------------------------------- 1 | /* global Meeting, configuration, Notify */ 2 | 3 | 'use strict'; 4 | 5 | const args = new URLSearchParams(location.search); 6 | 7 | let meeting; 8 | // auto-fill 9 | if (args.has('channel-id')) { 10 | document.querySelector('#join [name=channel-id]').value = args.get('channel-id'); 11 | document.querySelector('#join [type=password]').focus(); 12 | } 13 | 14 | chrome.storage.local.get({ 15 | 'signaling-server': '', 16 | 'signaling-token': '', 17 | 'media': configuration.media 18 | }, prefs => { 19 | Object.assign(configuration.media, prefs.media); 20 | 21 | console.log(configuration.media); 22 | 23 | meeting = new Meeting(prefs['signaling-server'], prefs['signaling-token']); 24 | 25 | document.getElementById('join').addEventListener('submit', async e => { 26 | e.preventDefault(); 27 | const join = e.target.querySelector('input[type=submit]'); 28 | join.value = 'connecting...'; 29 | document.title = 'Connecting to Server...'; 30 | join.disabled = true; 31 | const cid = e.target.querySelector('input[type=number]').value; 32 | const password = e.target.querySelector('input[type=password]').value; 33 | await meeting.password(password); 34 | 35 | meeting.join(cid, { // extra info 36 | nickname: Math.random() 37 | }).then(() => { 38 | document.title = 'Joined on channel #' + cid; 39 | history.pushState({}, '', '?channel-id=' + cid); 40 | return navigator.mediaDevices.getUserMedia(configuration.media).then(stream => { 41 | console.log(stream); 42 | document.body.dataset.mode = 'joined'; 43 | const me = document.getElementById('me'); 44 | me.onloadedmetadata = () => me.play(); 45 | me.srcObject = stream; 46 | }).catch(e => { 47 | throw Error('Cannot access to the user media: ' + e.message); 48 | }); 49 | }).catch(e => { 50 | document.title = 'Meeting'; 51 | join.value = 'Join'; 52 | join.disabled = false; 53 | alert(e.message); 54 | }); 55 | }); 56 | 57 | // allow or block joining 58 | document.getElementById('socket').addEventListener('change', e => { 59 | meeting.socket[e.target.checked ? 'restart' : 'close'](); 60 | if (e.target.checked) { 61 | meeting.send({ 62 | method: 'socket-restart' 63 | }); 64 | } 65 | }); 66 | meeting.onConnectionStateChanged.addListener((type, state) => { 67 | if (type === 'socket') { 68 | const e = document.querySelector('#socket input'); 69 | if (state === 'closed') { 70 | e.checked = false; 71 | } 72 | else { 73 | e.checked = true; 74 | } 75 | } 76 | }); 77 | meeting.onCountChanged.addListener(count => { 78 | document.body.dataset.count = count; 79 | }); 80 | meeting.onVideoRequest.addListener((video, peer) => { 81 | const view = document.createElement('video-view'); 82 | view.setAttribute('controls', 'mute close'); 83 | view.set(video); 84 | video.poster = 'assets/poster.png'; 85 | document.getElementById('meeting').appendChild(view); 86 | view.onCloseRequested.addListener(() => { 87 | peer.close(); 88 | }); 89 | }); 90 | 91 | const notify = new Notify(); 92 | meeting.onMessage.addListener((guid, o) => { 93 | if ('message' in o) { 94 | notify.display(o.message, undefined, 5000); 95 | } 96 | }); 97 | 98 | // 99 | document.getElementById('me').onCloseRequested.addListener(() => { 100 | location.reload(); 101 | }); 102 | }); 103 | 104 | -------------------------------------------------------------------------------- /client/v3/data/window/meeting/meeting.js: -------------------------------------------------------------------------------- 1 | /* global Peer, Socket, safe */ 2 | 'use strict'; 3 | 4 | const configuration = { 5 | server: { 6 | iceServers: [{ 7 | 'urls': [ 8 | 'stun:stun.services.mozilla.com', 9 | 'stun:stun.l.google.com:19302', 10 | 'stun:stun.mit.de:3478', 11 | 'stun:stun.gmx.net:3478' 12 | ] 13 | }] 14 | }, 15 | media: { 16 | audio: true, 17 | video: true 18 | }, 19 | timeout: { 20 | peer: 10000, 21 | resize: 1000 22 | } 23 | }; 24 | 25 | const COMMON = Symbol('common'); 26 | const INCLUDE = Symbol('include'); 27 | 28 | class Meeting { 29 | constructor(orign, key) { 30 | const socket = this.socket = new class extends Socket { 31 | encrypt(msg) { 32 | return safe.encrypt(msg); 33 | } 34 | decrypt(msg) { 35 | return safe.decrypt(msg); 36 | } 37 | }(orign, key); 38 | socket.onMessage.addListener(msg => { 39 | if (msg.method === 'whoami') { 40 | this.client(msg.sender).then(peer => { 41 | peer.extra = msg.extra || {}; 42 | this[INCLUDE](msg.sender, peer); 43 | socket.send({ 44 | method: 'whoami-reply', 45 | extra: this.exta 46 | }, msg.sender); 47 | }); 48 | } 49 | else if (msg.method === 'whoami-reply') { 50 | this.server(msg.sender).then(peer => { 51 | peer.extra = msg.extra || {}; 52 | this[INCLUDE](msg.sender, peer); 53 | peer.offer(); 54 | }); 55 | } 56 | }); 57 | 58 | this.videos = {}; 59 | this.onVideoRequest = new Event(); 60 | 61 | this.peers = {}; 62 | this.onMessage = new Event(); 63 | 64 | this.onConnectionStateChanged = new Event(); 65 | socket.onConnectionStateChanged.addListener((type, state) => { 66 | this.onConnectionStateChanged.emit(type, state); 67 | }); 68 | 69 | // count 70 | this.onCountChanged = new Event(); 71 | } 72 | get count() { 73 | return this._count || 1; 74 | } 75 | set count(v) { 76 | this._count = v; 77 | this.onCountChanged.emit(v); 78 | } 79 | [COMMON](stream, recipient) { 80 | const p = new Peer(this.socket, configuration.server, recipient); 81 | 82 | let video; 83 | // add track 84 | p.onTrack.addListener(stream => { 85 | if (!video) { 86 | const tid = setTimeout(() => { 87 | console.warn('no signal is received from the peer', 'closing peer connection'); 88 | this.onMessage.emit(recipient, { 89 | message: 'No signal is received' 90 | }); 91 | p.close(); 92 | }, configuration.timeout.peer); 93 | video = document.createElement('video'); 94 | video.onloadedmetadata = () => { 95 | video.play(); 96 | clearTimeout(tid); 97 | }; 98 | this.onVideoRequest.emit(video, p); 99 | this.videos[recipient] = video; 100 | this.count += 1; 101 | 102 | // monitor video element size changes 103 | const send = e => { 104 | const width = Math.min(600, e.contentRect.width); 105 | const height = width * (e.contentRect.height / e.contentRect.width); 106 | p.send({ 107 | method: 'video-constrain', 108 | value: { 109 | width: { 110 | max: width 111 | }, 112 | height: { 113 | max: height 114 | }, 115 | frameRate: { 116 | max: 30 117 | } 118 | } 119 | }); 120 | }; 121 | const resizeObserver = new ResizeObserver(([e]) => { 122 | clearTimeout(send.id); 123 | send.id = setTimeout(send, configuration.timeout.resize, e); 124 | }); 125 | resizeObserver.observe(video); 126 | } 127 | 128 | // don't set srcObject again if it is already set. 129 | if (video.srcObject) { 130 | return; 131 | } 132 | video.srcObject = stream; 133 | }); 134 | stream.getTracks().forEach(track => p.pc.addTrack(track, stream)); 135 | 136 | p.onMessage.addListener(request => { 137 | this.onMessage.emit(recipient, request); 138 | 139 | if (request.method === 'video-constrain') { 140 | const [vst] = stream.getVideoTracks(); 141 | if (vst) { 142 | vst.applyConstraints(request.value); 143 | } 144 | } 145 | else if (request.method === 'socket-restart') { 146 | this.socket.restart(); 147 | } 148 | }); 149 | 150 | return p; 151 | } 152 | 153 | async server(recipient) { 154 | const stream = await navigator.mediaDevices.getUserMedia(configuration.media); 155 | const p = this[COMMON](stream, recipient); 156 | p.channel(); 157 | 158 | return p; 159 | } 160 | async client(recipient) { 161 | const stream = await navigator.mediaDevices.getUserMedia(configuration.media); 162 | return this[COMMON](stream, recipient); 163 | } 164 | password(v) { 165 | return safe.password(v); 166 | } 167 | 168 | /* include new peer */ 169 | [INCLUDE](guid, p) { 170 | this.peers[guid] = p; 171 | p.onConnectionStateChanged.addListener((type, state) => { 172 | if ( 173 | (type === 'channel' && state === 'closed') || 174 | (type === 'peer' && (state === 'disconnected' || state === 'failed')) 175 | ) { 176 | const v = this.videos[guid]; 177 | if (v) { 178 | v.remove(); 179 | delete this.videos[guid]; 180 | this.count -= 1; 181 | } 182 | delete this.peers[guid]; 183 | } 184 | this.onConnectionStateChanged.emit(type, state); 185 | }); 186 | } 187 | 188 | join(cid, extra = {}) { 189 | this.exta = extra; 190 | return this.socket.create(cid).then(() => this.socket.send({ 191 | method: 'whoami', 192 | extra 193 | })); 194 | } 195 | 196 | send(o) { 197 | for (const peer of Object.values(this.peers)) { 198 | peer.send(o); 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /client/v3/data/window/meeting/peer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const KEY = 'SFpKNlNlb1RkMDNIc1ZNNHZ0d1pmNnF1VHY2NHl1TVlmVEVOQmdvcDR3TG9NWmh3QUNvWGxwTXBhRHJK'; 4 | // const ORIGIN = 'wss://connect.websocket.in/v3/[CHANNEL_ID]?apiKey=[API_KEY]'; 5 | // const ORIGIN = 'wss://meeting-server.herokuapp.com/[CHANNEL_ID]?apiKey=[API_KEY]'; 6 | // const ORIGIN = 'ws://127.0.0.1:8000/[CHANNEL_ID]?apiKey=[API_KEY]'; 7 | // const ORIGIN = 'wss://meetingserver.eu.openode.io/[CHANNEL_ID]?apiKey=[API_KEY]'; 8 | const ORIGIN = 'wss://connect.meetingserver.repl.co/[CHANNEL_ID]?apiKey=[API_KEY]'; 9 | const GUID = ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => 10 | (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) 11 | ); 12 | 13 | class Socket { 14 | constructor(origin, key) { 15 | this.key = key || atob(KEY); 16 | this.server = (origin || ORIGIN).replace('[API_KEY]', this.key); 17 | this.onConnectionStateChanged = new Event(); 18 | this.onMessage = new Event(); 19 | } 20 | create(cid = 1) { 21 | if (cid < 1 || cid > 10000) { 22 | throw Error('range limit'); 23 | } 24 | this.cid = cid; 25 | 26 | return new Promise((resolve, reject) => { 27 | const socket = this.socket = new WebSocket(this.server.replace('[CHANNEL_ID]', cid)); 28 | socket.onopen = () => { 29 | setTimeout(() => { 30 | if (socket.readyState === 1) { 31 | resolve(); 32 | this.onConnectionStateChanged.emit('socket', 'ready'); 33 | } 34 | else { 35 | reject(Error('connection closed')); 36 | this.onConnectionStateChanged.emit('socket', 'closed'); 37 | } 38 | }, 500); 39 | }; 40 | socket.onclose = () => { 41 | reject(Error('connection closed')); 42 | this.onConnectionStateChanged.emit('socket', 'closed'); 43 | }; 44 | socket.onmessage = e => { 45 | this.decrypt(e.data).then(msg => { 46 | msg = JSON.parse(msg); 47 | if ('recipient' in msg) { 48 | if (msg.recipient === GUID) { 49 | this.onMessage.emit(msg); 50 | } 51 | } 52 | else { 53 | this.onMessage.emit(msg); 54 | } 55 | }).catch(() => { 56 | try { 57 | const j = JSON.parse(e.data); 58 | if (j.error) { 59 | alert(j.error); 60 | } 61 | else { 62 | throw Error('message is not encrypted'); 63 | } 64 | } 65 | catch (e) { 66 | console.warn('cannot decrypt the encrypted message'); 67 | } 68 | }); 69 | }; 70 | }); 71 | } 72 | encrypt(msg) { 73 | return Promise.resolve(msg); 74 | } 75 | decrypt(msg) { 76 | return Promise.resolve(msg); 77 | } 78 | send(msg, recipient) { 79 | const o = { 80 | ...msg, 81 | sender: GUID 82 | }; 83 | if (recipient) { 84 | o.recipient = recipient; 85 | } 86 | this.encrypt(JSON.stringify(o)).then(e => this.socket.send(e)); 87 | } 88 | close() { 89 | try { 90 | this.socket.close(); 91 | } 92 | catch (e) {} 93 | } 94 | restart() { 95 | this.close(); 96 | this.create(this.cid); 97 | } 98 | } 99 | 100 | class Peer { 101 | constructor(socket, configuration, recipient) { 102 | const pc = this.pc = new RTCPeerConnection(configuration); 103 | 104 | this.recipient = recipient; 105 | 106 | this.onConnectionStateChanged = new Event(); 107 | pc.onconnectionstatechange = () => this.onConnectionStateChanged.emit('peer', pc.connectionState); 108 | 109 | let cid; // send multiple candidates in one request 110 | const candidates = []; 111 | pc.onicecandidate = ({candidate}) => { 112 | if (candidate) { 113 | this.onConnectionStateChanged.emit('candidate', 'generated'); 114 | clearTimeout(cid); 115 | cid = setTimeout(() => { 116 | socket.send({candidates}, recipient); 117 | }, 500); 118 | candidates.push(candidate); 119 | } 120 | }; 121 | this.socket = socket; 122 | socket.onMessage.addListener(msg => this.parse(msg)); 123 | 124 | // track 125 | this.onTrack = new Event(); 126 | pc.ontrack = e => this.onTrack.emit(e.streams[0]); 127 | 128 | // channel 129 | this.onMessage = new Event(); 130 | this.channels = {}; 131 | pc.ondatachannel = e => { 132 | const ch = e.channel; 133 | this.channel(ch.label, ch); 134 | }; 135 | } 136 | // process messages from the other end 137 | async parse(msg) { 138 | // all new client peers receive messages from any recipient. Only accept from recipient 139 | if (msg.sender !== this.recipient) { 140 | // console.log('ignored', msg, msg.sender, this.recipient); 141 | return; 142 | } 143 | const {desc, candidates} = msg; 144 | const {socket, pc} = this; 145 | if (candidates) { 146 | for (const candidate of candidates) { 147 | pc.addIceCandidate(candidate); 148 | } 149 | } 150 | else if (desc && desc.type === 'answer') { // server-side 151 | this.onConnectionStateChanged.emit('desc', 'received'); 152 | pc.setRemoteDescription(desc); 153 | } 154 | else if (desc && desc.type === 'offer') { // client-side 155 | this.onConnectionStateChanged.emit('desc', 'received'); 156 | await pc.setRemoteDescription(desc); 157 | const answer = await pc.createAnswer(); 158 | 159 | await pc.setLocalDescription(answer); 160 | socket.send({desc: answer}, this.recipient); 161 | } 162 | } 163 | send(msg, name = 'json') { 164 | try { 165 | this.channels[name].send(JSON.stringify(msg)); 166 | } 167 | catch (e) {} 168 | } 169 | // create offer (server-side) 170 | async offer() { 171 | const {socket, pc} = this; 172 | const offer = await pc.createOffer(); 173 | await pc.setLocalDescription(offer); 174 | socket.send({desc: pc.localDescription}, this.recipient); 175 | } 176 | // channel 177 | channel(name = 'json', ch) { 178 | ch = ch || this.pc.createDataChannel(name); 179 | ch.onmessage = e => this.onMessage.emit(JSON.parse(e.data)); 180 | ch.onopen = () => this.onConnectionStateChanged.emit('channel', 'ready'); 181 | ch.onclose = () => this.onConnectionStateChanged.emit('channel', 'closed'); 182 | this.channels[ch.label] = ch; 183 | } 184 | // close 185 | close() { 186 | const {pc} = this; 187 | pc.close(); 188 | this.onConnectionStateChanged.emit('channel', 'closed'); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /client/v3/data/window/meeting/safe.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const safe = {}; 4 | window.safe = safe; 5 | 6 | { 7 | const iv = crypto.getRandomValues(new Uint8Array(16)); 8 | 9 | const toBuffer = str => { 10 | const bytes = new Uint8Array(str.length); 11 | [...str].forEach((c, i) => bytes[i] = c.charCodeAt(0)); 12 | return bytes; 13 | }; 14 | const toString = buffer => [...buffer].map(b => String.fromCharCode(b)).join(''); 15 | 16 | let key = ''; 17 | safe.password = async password => { 18 | key = await crypto.subtle.digest({ 19 | name: 'SHA-256' 20 | }, toBuffer(password)).then(result => crypto.subtle.importKey('raw', result, { 21 | name: 'AES-CBC' 22 | }, false, ['encrypt', 'decrypt'])); 23 | }; 24 | safe.encrypt = async data => { 25 | const result = await crypto.subtle.encrypt({ 26 | name: 'AES-CBC', 27 | iv 28 | }, key, toBuffer(data)); 29 | return new Promise(resolve => { 30 | const reader = new FileReader(); 31 | reader.onload = () => resolve(reader.result); 32 | reader.readAsDataURL(new Blob([iv, result], {type: 'application/octet-binary'})); 33 | }); 34 | }; 35 | safe.decrypt = async data => { 36 | const result = await crypto.subtle.decrypt({ 37 | name: 'AES-CBC', 38 | iv 39 | }, key, toBuffer(atob(data.split(',')[1]))); 40 | return toString((new Uint8Array(result)).subarray(16)); 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /client/v3/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "version": "0.2.0", 4 | "name": "P2P Video Meeting", 5 | "description": "a secure peer-to-peer video and audio conference using WebRTC that works with or without INTERNET access", 6 | "permissions": [ 7 | "storage", 8 | "contextMenus" 9 | ], 10 | "icons": { 11 | "16": "data/icons/16.png", 12 | "32": "data/icons/32.png", 13 | "48": "data/icons/48.png", 14 | "64": "data/icons/64.png", 15 | "128": "data/icons/128.png", 16 | "256": "data/icons/256.png", 17 | "512": "data/icons/512.png" 18 | }, 19 | "background": { 20 | "service_worker": "worker.js" 21 | }, 22 | "action": {}, 23 | "options_ui": { 24 | "page": "data/options/index.html" 25 | }, 26 | "homepage_url": "https://add0n.com/meeting.html" 27 | } 28 | -------------------------------------------------------------------------------- /client/v3/worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | { 4 | const once = () => chrome.storage.local.get({ 5 | media: { 6 | audio: true, 7 | video: true 8 | } 9 | }, prefs => { 10 | chrome.contextMenus.create({ 11 | id: 'media', 12 | title: 'Media', 13 | contexts: ['action'] 14 | }); 15 | chrome.contextMenus.create({ 16 | id: 'media.video.audio', 17 | title: 'Use Camera and Microphone', 18 | parentId: 'media', 19 | contexts: ['action'], 20 | type: 'radio', 21 | checked: prefs.media.video && prefs.media.audio 22 | }); 23 | chrome.contextMenus.create({ 24 | id: 'media.video', 25 | title: 'Use Camera Only', 26 | parentId: 'media', 27 | contexts: ['action'], 28 | type: 'radio', 29 | checked: prefs.media.video && prefs.media.audio === false 30 | }); 31 | chrome.contextMenus.create({ 32 | id: 'media.audio', 33 | title: 'Use Microphone Only', 34 | parentId: 'media', 35 | contexts: ['action'], 36 | type: 'radio', 37 | checked: prefs.media.audio && prefs.media.video === false 38 | }); 39 | }); 40 | 41 | chrome.runtime.onStartup.addListener(once); 42 | chrome.runtime.onInstalled.addListener(once); 43 | } 44 | chrome.contextMenus.onClicked.addListener(info => chrome.storage.local.set({ 45 | media: { 46 | video: info.menuItemId.includes('video'), 47 | audio: info.menuItemId.includes('audio') 48 | } 49 | })); 50 | 51 | chrome.action.onClicked.addListener(() => { 52 | chrome.tabs.create({ 53 | url: 'data/window/index.html' 54 | }); 55 | }); 56 | 57 | /* FAQs & Feedback */ 58 | { 59 | const {management, runtime: {onInstalled, setUninstallURL, getManifest}, storage, tabs} = chrome; 60 | if (navigator.webdriver !== true) { 61 | const page = getManifest().homepage_url; 62 | const {name, version} = getManifest(); 63 | onInstalled.addListener(({reason, previousVersion}) => { 64 | management.getSelf(({installType}) => installType === 'normal' && storage.local.get({ 65 | 'faqs': true, 66 | 'last-update': 0 67 | }, prefs => { 68 | if (reason === 'install' || (prefs.faqs && reason === 'update')) { 69 | const doUpdate = (Date.now() - prefs['last-update']) / 1000 / 60 / 60 / 24 > 45; 70 | if (doUpdate && previousVersion !== version) { 71 | tabs.create({ 72 | url: page + '?version=' + version + (previousVersion ? '&p=' + previousVersion : '') + '&type=' + reason, 73 | active: reason === 'install' 74 | }); 75 | storage.local.set({'last-update': Date.now()}); 76 | } 77 | } 78 | })); 79 | }); 80 | setUninstallURL(page + '?rd=feedback&name=' + encodeURIComponent(name) + '&version=' + version); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meeting.js", 3 | "version": "0.1.0", 4 | "description": "", 5 | "dependencies": { 6 | "ws": "^7.3.1" 7 | }, 8 | "devDependencies": {}, 9 | "scripts": { 10 | "start": "node server/server.js", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "license": "MPL2.0" 14 | } 15 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | .openode 2 | node_modules 3 | Dockerfile 4 | package.json 5 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const KEY = Buffer.from('SFpKNlNlb1RkMDNIc1ZNNHZ0d1pmNnF1VHY2NHl1TVlmVEVOQmdvcDR3TG9NWmh3QUNvWGxwTXBhRHJK', 'base64') 2 | .toString(); 3 | 4 | const WebSocket = require('ws'); 5 | const http = require('http'); 6 | 7 | const wss = new WebSocket.Server({ 8 | port: 80 // process.env.PORT || 8000 9 | }); 10 | 11 | http.createServer((req, res) => { 12 | res.write('Server is up'); 13 | res.end(); 14 | }).listen(8080); 15 | 16 | wss.on('connection', (ws, req) => { 17 | const {url} = req; 18 | const id = Number(url.split('/').pop().split('?').shift()); 19 | const apiKey = url.split('apiKey=').pop().split('&').shift(); 20 | 21 | if (!apiKey || apiKey !== KEY) { 22 | ws.send(JSON.stringify({ 23 | error: apiKey ? 'wrong API key' : 'apiKey is mandatory parameter' 24 | })); 25 | return ws.close(); 26 | } 27 | if (isNaN(id)) { 28 | ws.send(JSON.stringify({ 29 | error: 'unknown id' 30 | })); 31 | return ws.close(); 32 | } 33 | if (id < 1 || id > 1000) { 34 | ws.send(JSON.stringify({ 35 | error: 'out of range' 36 | })); 37 | return ws.close(); 38 | } 39 | 40 | ws.apiKey = apiKey; 41 | ws.id = id; 42 | 43 | ws.on('message', msg => { 44 | for (const client of wss.clients) { 45 | if (client !== ws && client.id === id && client.apiKey === apiKey && client.readyState === WebSocket.OPEN) { 46 | client.send(msg); 47 | } 48 | } 49 | }); 50 | }); 51 | --------------------------------------------------------------------------------