Files
racket-wiki/static/js/cmap/engine/drawing-support.js
T

341 lines
7.3 KiB
JavaScript

/**
* 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 };