/** * Shared DOM and state support for the low-level diagram renderer. * * Derived from ionstage/cmap 0.1.3, (c) 2015 iOnStage, MIT License. */ const CONTENT_TYPE_TEXT = 'text'; const CONTENT_TYPE_HTML = 'html'; class ItemList { constructor() { this.data = []; } add(item) { if (!this.contains(item)) this.data.push(item); } remove(item) { for (let index = this.data.length - 1; index >= 0; index -= 1) { if (this.equal(this.data[index], item)) { this.data.splice(index, 1); break; } } } contains(item) { return this.data.some((candidate) => this.equal(candidate, item)); } equal(first, second) { return first === second; } toArray() { return this.data.slice(); } } const helper = { CONTENT_TYPE_HTML, CONTENT_TYPE_TEXT, List: ItemList, toNumber(value, defaultValue) { return !isNaN(value) ? Number(value) : defaultValue; }, toString(value, defaultValue) { return value !== undefined ? String(value) : defaultValue; }, toBoolean(value, defaultValue) { return value !== undefined ? Boolean(value) : defaultValue; }, toContentType(value, defaultValue) { if (value === CONTENT_TYPE_TEXT || value === CONTENT_TYPE_HTML) return value; return defaultValue; }, eachInstance(values, constructor, callback) { values.filter((value) => value instanceof constructor).forEach(callback); }, firstInstance(values, constructor) { return values.find((value) => value instanceof constructor); }, diffObj(newObject, oldObject) { const difference = {}; for (const key in newObject) { if (!oldObject || newObject[key] !== oldObject[key]) { difference[key] = newObject[key]; } } return difference; }, identity(value) { return value; } }; var dom = {}; dom.disabled = function() { return (typeof document === 'undefined'); }; dom.el = function(selector) { if (selector.charAt(0) === '<') { selector = selector.match(/<(.+)>/)[1]; return document.createElement(selector); } }; dom.body = function() { return document.body; }; dom.attr = function(el, props) { for (var key in props) { el.setAttribute(key, props[key]); } }; dom.css = function(el, props) { var style = el.style; for (var key in props) { style[key] = props[key]; } }; dom.rect = function(el) { return el.getBoundingClientRect(); }; dom.clientWidth = function(el) { return el.clientWidth; }; dom.clientHeight = function(el) { return el.clientHeight; }; dom.scrollLeft = function(el) { return el.scrollLeft; }; dom.scrollTop = function(el) { return el.scrollTop; }; dom.scrollWidth = function(el) { return el.scrollWidth; }; dom.scrollHeight = function(el) { return el.scrollHeight; }; dom.text = function(el, s) { el.textContent = s; }; dom.html = function(el, s) { el.innerHTML = s; }; dom.append = function(parent, el) { parent.appendChild(el); }; dom.remove = function(el) { el.parentNode.removeChild(el); }; dom.child = function(el, index) { return el.childNodes[index]; }; dom.animate = function(callback) { return window.requestAnimationFrame(callback); }; dom.supportsTouch = function() { return ('ontouchstart' in window || (typeof DocumentTouch !== 'undefined' && document instanceof DocumentTouch)); }; dom.on = function(el, type, listener) { el.addEventListener(type, listener); }; dom.off = function(el, type, listener) { el.removeEventListener(type, listener); }; dom.pagePoint = function(event, offset) { if (dom.supportsTouch()) event = event.changedTouches[0]; return { x: event.pageX - (offset ? offset.x : 0), y: event.pageY - (offset ? offset.y : 0) }; }; dom.clientPoint = function(event, offset) { if (dom.supportsTouch()) event = event.changedTouches[0]; return { x: event.clientX - (offset ? offset.x : 0), y: event.clientY - (offset ? offset.y : 0) }; }; dom.cancel = function(event) { event.preventDefault(); }; class Draggable { constructor(element, onStart, onMove, onEnd) { this.element = element; this.onStart = onStart; this.onMove = onMove; this.onEnd = onEnd; this.start = this.start.bind(this); this.move = this.move.bind(this); this.end = this.end.bind(this); this.locked = false; this.startingPoint = null; this.startEvent = dom.supportsTouch() ? 'touchstart' : 'mousedown'; this.moveEvent = dom.supportsTouch() ? 'touchmove' : 'mousemove'; this.endEvent = dom.supportsTouch() ? 'touchend' : 'mouseup'; dom.on(this.element, this.startEvent, this.start); } start(event) { if (this.locked) return; this.locked = true; this.startingPoint = dom.pagePoint(event); const rectangle = dom.rect(this.element); const point = dom.clientPoint(event, { x: rectangle.left - dom.scrollLeft(this.element), y: rectangle.top - dom.scrollTop(this.element) }); if (typeof this.onStart === 'function') this.onStart(point.x, point.y, event); dom.on(document, this.moveEvent, this.move); dom.on(document, this.endEvent, this.end); } move(event) { const distance = dom.pagePoint(event, this.startingPoint); if (typeof this.onMove === 'function') this.onMove(distance.x, distance.y, event); } end(event) { dom.off(document, this.moveEvent, this.move); dom.off(document, this.endEvent, this.end); const distance = dom.pagePoint(event, this.startingPoint); if (typeof this.onEnd === 'function') this.onEnd(distance.x, distance.y, event); this.locked = false; } } dom.draggable = function(element, onStart, onMove, onEnd) { if (dom.disabled()) return null; return new Draggable(element, onStart, onMove, onEnd); }; const dirtyComponents = []; let renderRequestId = null; class Component { constructor() { this.disposed = false; } dispose() { this.disposed = true; } prop(initialValue, defaultValue, converter) { const convert = typeof converter === 'function' ? converter : helper.identity; let cache = convert(initialValue, defaultValue); return (value) => { if (typeof value === 'undefined') return cache; if (value === cache) return; cache = convert(value, cache); this.markDirty(); }; } relations() { return []; } redraw() {} notifyRendered() { if (typeof this.renderedHandler === 'function' && this.element()) this.renderedHandler(this.element()); } markDirty() { if (dom.disabled() || this.disposed) return; if (!dirtyComponents.includes(this)) dirtyComponents.push(this); if (renderRequestId !== null) return; renderRequestId = dom.animate(redrawDirtyComponents); } } function updateDirtyRelations(index) { const initialLength = dirtyComponents.length; for (let position = index; position < initialLength; position += 1) { const component = dirtyComponents[position]; if (component.disposed) continue; component.relations().forEach((relation) => { if (!relation.disposed) relation.update(component); }); } if (dirtyComponents.length > initialLength) updateDirtyRelations(initialLength); } function redrawDirtyComponents() { updateDirtyRelations(0); dirtyComponents.forEach((component) => { if (!component.disposed) component.redraw(); }); dirtyComponents.length = 0; renderRequestId = null; } export { Component, dom, helper };