UNPKG

whiplinker

Version:

Connect items visually by drawing whips/links/cables/connectors between them

517 lines (434 loc) 15 kB
'use strict'; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var WhipLinker = function () { function WhipLinker(source, target) { var _this = this; var options = arguments.length <= 2 || arguments[2] === undefined ? {} : arguments[2]; _classCallCheck(this, WhipLinker); // defaults this.options = { prefix: 'wl-', container: document.body }; this.setOptions(options); // styling var style = document.createElement('style'); style.appendChild(document.createTextNode('\n.' + this.options.prefix + 'source {}\n.' + this.options.prefix + 'target {}\n.' + this.options.prefix + 'whiplink {\n\tposition: absolute;\n\twidth: 0;\n\tpointer-events: none;\n\ttransform-origin: left center;\n\n\theight: 3px;\n\tbackground: black;\n\tmargin-top: -1.5px;\n\tborder-radius: 3px;\n}\n.' + this.options.prefix + 'whiplink.' + this.options.prefix + 'missed {\n\tbackground: red;\n\twidth: 0 !important;\n\ttransition: width 200ms;\n}\n.' + this.options.prefix + 'whiplink.' + this.options.prefix + 'hit {\n\tpointer-events: auto;\n}\n.' + this.options.prefix + 'whiplink.' + this.options.prefix + 'selected {\n\tbackground: rgb(59, 153, 252);\n}')); document.head.insertBefore(style, document.head.firstChild); // init this.whiplinkElement = false; this.selectedWhiplinkElements = []; this.sourceElements = []; this.targetElements = []; this.sourceFilters = []; this.targetFilters = []; this.hits = []; // hooks this.hookSourceElements(source); this.hookTargetElements(target); document.addEventListener('mousedown', function (e) { if (_this.sourceElements.indexOf(e.target) >= 0 && _this.filterSourceElement(e.target)) { _this._from(e.target); e.preventDefault(); } }); document.addEventListener('mousemove', function (e) { if (_this.whiplinkElement) { _this._to(e.clientX, e.clientY); e.preventDefault(); } }); document.addEventListener('mouseup', function (e) { if (_this.whiplinkElement) { if (_this.targetElements.indexOf(e.target) >= 0 && _this.filterTargetElement(e.target)) { _this._hit(e.target); } else { _this._miss(); } e.preventDefault(); } }); document.addEventListener('click', function (e) { _this.deselectWhiplinks(); e.preventDefault(); }); document.addEventListener('keyup', function (e) { if (e.keyCode === 46 /*del*/) { _this._reverseForEach(_this.selectedWhiplinkElements, function (whiplinkElement) { _this.deleteHit(_this.findHit({ whiplinkElement: whiplinkElement })); }); e.preventDefault(); } }); } // helpers _createClass(WhipLinker, [{ key: 'setOptions', value: function setOptions() { var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; return Object.assign(this.options, options); } }, { key: '_reverseForEach', value: function _reverseForEach(array, iterator) { for (var i = array.length - 1; i >= 0; i -= 1) { iterator(array[i], i, array); } } // elements }, { key: 'hookSourceElement', value: function hookSourceElement(sourceElement) { sourceElement.classList.add(this.options.prefix + 'source'); this.sourceElements.push(sourceElement); } }, { key: 'hookTargetElement', value: function hookTargetElement(targetElement) { targetElement.classList.add(this.options.prefix + 'target'); this.targetElements.push(targetElement); } }, { key: 'hookSourceElements', value: function hookSourceElements() { var sourceElements = arguments.length <= 0 || arguments[0] === undefined ? [] : arguments[0]; if (typeof sourceElements === 'string') { sourceElements = document.querySelectorAll(sourceElements); } var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = Array.from(sourceElements)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var sourceElement = _step.value; this.hookSourceElement(sourceElement); } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } } }, { key: 'hookTargetElements', value: function hookTargetElements() { var targetElements = arguments.length <= 0 || arguments[0] === undefined ? [] : arguments[0]; if (typeof targetElements === 'string') { targetElements = document.querySelectorAll(targetElements); } var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = Array.from(targetElements)[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var targetElement = _step2.value; this.hookTargetElement(targetElement); } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } } }, { key: 'filterSourceElement', value: function filterSourceElement(sourceElement) { var _this2 = this; return this.sourceFilters.reduce(function (prev, filter) { return prev && filter.call(_this2, { sourceElement: sourceElement, whiplinkElement: _this2.whiplinkElement }); }, true); } }, { key: 'filterTargetElement', value: function filterTargetElement(targetElement) { var _this3 = this; return this.targetFilters.reduce(function (prev, filter) { return prev && filter.call(_this3, { sourceElement: _this3.sourceElement, whiplinkElement: _this3.whiplinkElement, targetElement: targetElement }); }, true); } }, { key: 'addSourceFilter', value: function addSourceFilter(filter) { if (typeof filter === 'function') this.sourceFilters.push(filter); return this; // chainable } }, { key: 'addTargetFilter', value: function addTargetFilter(filter) { if (typeof filter === 'function') this.targetFilters.push(filter); return this; // chainable } }, { key: 'removeSourceFilter', value: function removeSourceFilter(filter) { if (typeof filter === 'function') this.sourceFilters.splice(this.sourceFilters.indexOf(filter), 1); return this; // chainable } }, { key: 'removeTargetFilter', value: function removeTargetFilter(filter) { if (typeof filter === 'function') this.targetFilters.splice(this.targetFilters.indexOf(filter), 1); return this; // chainable } // selection }, { key: '_hookWhiplink', value: function _hookWhiplink(whiplinkElement) { var _this4 = this; whiplinkElement.addEventListener('click', function (e) { if (e.shiftKey) { if (_this4.selectedWhiplinkElements.indexOf(whiplinkElement) >= 0) { _this4.deselectWhiplink(whiplinkElement); } else { _this4.selectWhiplink(whiplinkElement, true); } } else { _this4.selectWhiplink(whiplinkElement); } e.stopPropagation(); }); } }, { key: 'selectWhiplink', value: function selectWhiplink(whiplinkElement, append) { // clear existing selection if not appending if (!append) this.deselectWhiplinks(); var index = this.selectedWhiplinkElements.indexOf(whiplinkElement); if (index < 0) { // add it this.selectedWhiplinkElements.push(whiplinkElement); // mark as selected whiplinkElement.classList.add(this.options.prefix + 'selected'); // fire event var hit = this.findHit({ whiplinkElement: whiplinkElement }); if (hit) { this._emit('select', hit); } } } }, { key: 'deselectWhiplink', value: function deselectWhiplink(whiplinkElement) { var index = this.selectedWhiplinkElements.indexOf(whiplinkElement); if (index >= 0) { // remove it this.selectedWhiplinkElements.splice(index, 1); // unmark as selected whiplinkElement.classList.remove(this.options.prefix + 'selected'); // fire event var hit = this.findHit({ whiplinkElement: whiplinkElement }); if (hit) { this._emit('deselect', hit); } } } }, { key: 'deselectWhiplinks', value: function deselectWhiplinks() { var _this5 = this; var whiplinkElements = arguments.length <= 0 || arguments[0] === undefined ? this.selectedWhiplinkElements : arguments[0]; this._reverseForEach(whiplinkElements, function (whiplinkElement) { _this5.deselectWhiplink(whiplinkElement); }); } }, { key: 'removeWhiplink', value: function removeWhiplink(whiplinkElement) { // make sure it doesn't linger in selected this.deselectWhiplink(whiplinkElement); // remove from DOM this.options.container.removeChild(whiplinkElement); } // storage }, { key: 'addHit', value: function addHit(hit) { this.hits.push(hit); hit.whiplinkElement.classList.add(this.options.prefix + 'hit'); return hit; } }, { key: 'findHit', value: function findHit(q) { // e.g.: q = {whiplinkElement: HTMLElement, ...} var qkeys = Object.keys(q); return this.hits.find(function (hit) { return qkeys.reduce(function (prev, key) { return prev && hit[key] === q[key]; }, true); }); } }, { key: 'deleteHit', value: function deleteHit(hit) { // make sure whiplink doesn't linger in DOM this.removeWhiplink(hit.whiplinkElement); // remove from hits this.hits.splice(this.hits.indexOf(hit), 1); this._emit('delete', hit); } // drawing }, { key: 'snap', value: function snap(el) { var snapTo = arguments.length <= 1 || arguments[1] === undefined ? 'center center' : arguments[1]; var offset = el.getBoundingClientRect(); return { left: offset.left + (/left/.test(snapTo) ? 0 : /right/.test(snapTo) ? offset.width : offset.width / 2), top: offset.top + (/top/.test(snapTo) ? 0 : /bottom/.test(snapTo) ? offset.height : offset.height / 2) }; } }, { key: '__styleWhiplinkFrom', value: function __styleWhiplinkFrom(whiplinkElement, sourceElement) { this._offset = this.snap(sourceElement, this.options.snap); whiplinkElement.style.left = this._offset.left + 'px'; whiplinkElement.style.top = this._offset.top + 'px'; } }, { key: '_from', value: function _from(sourceElement) { var whiplinkElement = document.createElement('div'); whiplinkElement.className = this.options.prefix + 'whiplink'; this.options.container.appendChild(whiplinkElement); this.__styleWhiplinkFrom(whiplinkElement, sourceElement); this.whiplinkElement = whiplinkElement; this.sourceElement = sourceElement; this._emit('from', { sourceElement: sourceElement, whiplinkElement: whiplinkElement }); } }, { key: '__styleWhiplinkTo', value: function __styleWhiplinkTo(whiplinkElement, x, y) { x -= this._offset.left; y -= this._offset.top; var length = Math.sqrt(x * x + y * y), angle = Math.atan(y / x) * // get theta 180 / Math.PI + ( // to degrees x < 0 ? 180 : 0); // quadrants II & III whiplinkElement.style.width = length + 'px'; whiplinkElement.style.transform = 'rotate(' + angle + 'deg)'; } }, { key: '_to', value: function _to(x, y) { if (this.whiplinkElement) { this.__styleWhiplinkTo(this.whiplinkElement, x, y); this._emit('to', { x: x, y: y, sourceElement: this.sourceElement, whiplinkElement: this.whiplinkElement }); } } }, { key: '_hit', value: function _hit(targetElement) { if (this.whiplinkElement) { var offset = this.snap(targetElement, this.options.snap); this._to(offset.left, offset.top); this._hookWhiplink(this.whiplinkElement); var hit = this.addHit({ targetElement: targetElement, sourceElement: this.sourceElement, whiplinkElement: this.whiplinkElement, data: this._data }); this._emit('hit', hit); this._done(); } } }, { key: '_miss', value: function _miss() { var _this6 = this; if (this.whiplinkElement) { this.whiplinkElement.classList.add(this.options.prefix + 'missed'); var whiplinkElement = this.whiplinkElement; setTimeout(function () { _this6.removeWhiplink(whiplinkElement); }, 200); this._emit('miss', { sourceElement: this.sourceElement, whiplinkElement: this.whiplinkElement }); this._done(); } } }, { key: '_done', value: function _done() { this._emit('done', { sourceElement: this.sourceElement, whiplinkElement: this.whiplinkElement }); this._data = undefined; this.sourceElement = null; this.whiplinkElement = false; } }, { key: 'repaint', value: function repaint() { var _this7 = this; this._reverseForEach(this.hits, function (hit, i) { // from _this7.__styleWhiplinkFrom(hit.whiplinkElement, hit.sourceElement); // to var _snap = _this7.snap(hit.targetElement, _this7.options.snap); var x = _snap.left; var y = _snap.top; _this7.__styleWhiplinkTo(hit.whiplinkElement, x, y); }); } // input/output syncing }, { key: 'sync', value: function sync(hit, inputElement, outputElement) { if (!hit || hit.sync) return; // only bind once hit.sync = function () { outputElement.value = inputElement.value; }; inputElement.addEventListener('change', hit.sync); hit.sync(); } }, { key: 'unsync', value: function unsync(hit, inputElement) { if (!hit || !hit.sync) return; // only unbind once inputElement.removeEventListener('change', hit.sync); hit.sync = null; } // event delegation }, { key: 'data', value: function data(_data) { if (_data !== undefined) this._data = _data; return this._data; } }, { key: '_emit', value: function _emit(eventType, detail) { Object.assign(detail, this.data); var ev = new CustomEvent(this.options.prefix + eventType, { bubbles: true, detail: detail }); var _arr = ['sourceElement', 'targetElement']; for (var _i = 0; _i < _arr.length; _i++) { var k = _arr[_i]; if (detail[k] instanceof HTMLElement) { detail[k].dispatchEvent(ev); } } } }]); return WhipLinker; }();