Files
racket-wiki/static/js/cmap/engine/diagram-component.js
T

117 lines
3.5 KiB
JavaScript

/**
* Public handle for one component rendered by a DiagramEngine.
*
* A handle exposes only the operations used by the CMap editor. The drawing
* object remains private to the engine so application code cannot depend on
* the representation inherited from the original renderer.
*/
export class DiagramComponent {
constructor(engine, component, attributeNames) {
this.engine = engine;
this.component = component;
this.attributeNames = attributeNames;
this.baseVisible = true;
}
/** Read or update the supported rendering attributes. */
attr(name, value) {
if (name === undefined) {
const attributes = {};
for (const attributeName of this.attributeNames) {
attributes[attributeName] = this.component[attributeName]();
}
return attributes;
}
if (isPlainObject(name)) {
for (const [attributeName, attributeValue] of Object.entries(name)) {
this.attr(attributeName, attributeValue);
}
return this;
}
if (!this.attributeNames.includes(name)) return this;
if (value === undefined) return this.component[name]();
this.component[name](value);
return this;
}
/** Remove this component from its diagram. */
remove() {
this.engine.removeComponent(this);
}
/** Move this component to the front of its own rendering band. */
toFront() {
this.engine.drawingSurface.toFront(this.component);
return this;
}
/** Return the concrete element currently rendering this component. */
element() {
return this.component.element();
}
/** Immediately render the current component state. */
redraw() {
this.component.redraw();
return this;
}
/** Read or change whether the component participates in the presentation. */
visible(value) {
if (value === undefined) return this.component.visible !== false;
this.baseVisible = Boolean(value);
this.engine.applyFilter(this);
return this.component.visible;
}
/** Register a callback invoked after this component has been rendered. */
onRendered(handler) {
validateOptionalHandler(handler, "render");
this.component.renderedHandler = handler ?
(element) => handler(this, element) : null;
if (handler && this.component.element()) handler(this, this.component.element());
return this;
}
/** Read or change whether the component can be dragged. */
draggable(enabled) {
if (enabled === undefined) {
return this.engine.drawingSurface.dragEnabled(this.component);
}
if (enabled) this.engine.drawingSurface.enableDrag(this.component);
else this.engine.drawingSurface.disableDrag(this.component);
return this;
}
}
/** Return a new object containing only supported input attributes. */
export function pickAttributes(attributes, names) {
if (attributes === undefined) return {};
if (!isPlainObject(attributes)) throw new TypeError("Invalid component attributes");
const selected = {};
for (const name of names) {
if (name in attributes) selected[name] = attributes[name];
}
return selected;
}
/** Validate an optional event handler at the public engine boundary. */
export function validateOptionalHandler(handler, meaning) {
if (handler !== null && handler !== undefined && typeof handler !== "function") {
throw new TypeError(`Invalid ${meaning} handler`);
}
}
/** Determine whether a value is a plain attributes object. */
function isPlainObject(value) {
return typeof value === "object" && value !== null &&
Object.prototype.toString.call(value) === "[object Object]";
}