330 lines
9.3 KiB
TypeScript
330 lines
9.3 KiB
TypeScript
namespace pxsim.visuals {
|
|
export abstract class View {
|
|
protected element: SVGGElement;
|
|
protected rendered = false;
|
|
protected visible = false;
|
|
protected selected: boolean;
|
|
protected width: number = 0;
|
|
protected height: number = 0;
|
|
protected left: number = 0;
|
|
protected top: number = 0;
|
|
protected scaleFactor: number = 1;
|
|
|
|
private changed: boolean;
|
|
private hover: boolean = false;
|
|
|
|
protected theme: IBoardTheme;
|
|
|
|
protected abstract buildDom(): SVGElement;
|
|
public abstract getInnerWidth(): number;
|
|
public abstract getInnerHeight(): number;
|
|
|
|
public inject(parent: SVGElement, theme: IBoardTheme, width?: number, visible = true) {
|
|
this.width = width;
|
|
this.theme = theme;
|
|
parent.appendChild(this.getView());
|
|
|
|
if (visible) {
|
|
this.visible = true;
|
|
this.onComponentInjected();
|
|
}
|
|
}
|
|
|
|
public getWidth() {
|
|
return this.scaleFactor == undefined ? this.getInnerWidth() : this.getInnerWidth() * this.scaleFactor;
|
|
}
|
|
|
|
public getHeight() {
|
|
return this.scaleFactor == undefined ? this.getInnerHeight() : this.getInnerHeight() * this.scaleFactor;
|
|
}
|
|
|
|
public onComponentInjected() {
|
|
// To be overridden by sub class
|
|
}
|
|
|
|
public onComponentVisible() {
|
|
// To be overridden by sub class
|
|
}
|
|
|
|
public onComponentHidden() {
|
|
// To be overridden by sub class
|
|
}
|
|
|
|
public translate(x: number, y: number, applyImmediately = true) {
|
|
this.left = x;
|
|
this.top = y;
|
|
|
|
if (applyImmediately) {
|
|
this.updateTransform();
|
|
}
|
|
}
|
|
|
|
public scale(scaleFactor: number, applyImmediately = true) {
|
|
this.scaleFactor = scaleFactor;
|
|
|
|
if (applyImmediately) {
|
|
this.updateTransform();
|
|
}
|
|
}
|
|
|
|
public updateState() {
|
|
}
|
|
|
|
public updateTheme(theme: IBoardTheme) {
|
|
this.theme = theme;
|
|
this.updateThemeCore();
|
|
}
|
|
|
|
public updateThemeCore() {
|
|
}
|
|
|
|
public setVisible(visible: boolean) {
|
|
if (this.rendered) {
|
|
this.getView().style.display = visible ? 'block' : 'none';
|
|
}
|
|
}
|
|
|
|
public hasClick() {
|
|
return true;
|
|
}
|
|
|
|
private onClickHandler: (ev: any) => void;
|
|
public registerClick(handler: (ev: any) => void, zoom?: boolean) {
|
|
this.onClickHandler = handler;
|
|
if (zoom) {
|
|
this.getView().addEventListener(pointerEvents.up, (ev: any) => {
|
|
if (!this.getSelected()) {
|
|
this.onClickHandler(ev);
|
|
this.setHover(false);
|
|
}
|
|
});
|
|
this.getView().addEventListener(pointerEvents.move, () => {
|
|
if (!this.getSelected()) {
|
|
this.setHover(true);
|
|
}
|
|
});
|
|
this.getView().addEventListener(pointerEvents.leave, () => {
|
|
this.setHover(false);
|
|
});
|
|
} else {
|
|
this.getView().addEventListener(pointerEvents.up, this.onClickHandler);
|
|
}
|
|
}
|
|
|
|
public dispose() {
|
|
if (this.onClickHandler) this.getView().removeEventListener(pointerEvents.up, this.onClickHandler)
|
|
View.dispose(this);
|
|
}
|
|
|
|
protected getView() {
|
|
if (!this.rendered) {
|
|
this.element = svg.elt("g") as SVGGElement;
|
|
View.track(this);
|
|
|
|
const content = this.buildDom();
|
|
if (content) {
|
|
this.element.appendChild(content);
|
|
}
|
|
this.updateTransform();
|
|
this.rendered = true;
|
|
}
|
|
return this.element;
|
|
}
|
|
|
|
public resize(width: number, height: number, strict?: boolean) {
|
|
this.width = width;
|
|
this.height = height;
|
|
}
|
|
|
|
public getActualHeight() {
|
|
return this.height;
|
|
}
|
|
|
|
public getActualWidth() {
|
|
return this.width;
|
|
}
|
|
|
|
private updateTransform() {
|
|
if (this.rendered) {
|
|
let left = this.left;
|
|
let top = this.top;
|
|
let scaleFactor = this.scaleFactor;
|
|
if (this.hover) {
|
|
const hoverScaleFactor = scaleFactor + 0.05;
|
|
// Scale around center of module
|
|
const centerX = this.getWidth() / 2;
|
|
const centerY = this.getHeight() / 2;
|
|
left = left - centerX * (hoverScaleFactor - 1);
|
|
top = top - centerY * (hoverScaleFactor - 1);
|
|
scaleFactor = hoverScaleFactor;
|
|
}
|
|
|
|
let transform = `translate(${left} ${top})`;
|
|
|
|
if (scaleFactor !== 1) {
|
|
transform += ` scale(${scaleFactor})`;
|
|
}
|
|
|
|
this.element.setAttribute("transform", transform);
|
|
}
|
|
}
|
|
|
|
private static currentId = 0;
|
|
private static allViews: Map<View> = {};
|
|
|
|
protected static getInstance(element: Element) {
|
|
if (element.hasAttribute("ref-id")) {
|
|
return View.allViews[element.getAttribute("ref-id")];
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
private static track(view: View) {
|
|
const myId = "id-" + (View.currentId++);
|
|
view.element.setAttribute("ref-id", myId);
|
|
View.allViews[myId] = view;
|
|
}
|
|
|
|
private static dispose(view: View) {
|
|
if (view.element) {
|
|
const id = view.element.getAttribute("ref-id");
|
|
// TODO: Remove from DOM
|
|
view.element.parentNode.removeChild(view.element);
|
|
delete View.allViews[id];
|
|
}
|
|
}
|
|
|
|
///////// HOVERED STATE /////////////
|
|
|
|
public getHover() {
|
|
return this.hover;
|
|
}
|
|
|
|
public setHover(hover: boolean) {
|
|
if (this.hover != hover) {
|
|
this.hover = hover;
|
|
this.updateTransform();
|
|
}
|
|
}
|
|
|
|
///////// SELECTED STATE /////////////
|
|
|
|
public getSelected() {
|
|
return this.selected;
|
|
}
|
|
|
|
public setSelected(selected: boolean) {
|
|
if (this.selected != selected) {
|
|
this.selected = selected;
|
|
this.setChangedState();
|
|
}
|
|
}
|
|
|
|
protected setChangedState() {
|
|
this.changed = true;
|
|
}
|
|
|
|
public didChange() {
|
|
const res = this.changed;
|
|
this.changed = false;
|
|
return res;
|
|
}
|
|
|
|
public hasBackground() {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export abstract class SimView<T extends BaseNode> extends View implements LayoutElement {
|
|
constructor(protected state: T) {
|
|
super();
|
|
}
|
|
|
|
public getId() {
|
|
return this.state.id;
|
|
}
|
|
|
|
public getPort() {
|
|
return this.state.port;
|
|
}
|
|
|
|
public getPaddingRatio() {
|
|
return 0;
|
|
}
|
|
|
|
public getWiringRatio() {
|
|
return 0.5;
|
|
}
|
|
|
|
public setSelected(selected: boolean) { }
|
|
|
|
protected getView() {
|
|
return super.getView();
|
|
}
|
|
|
|
public kill() {
|
|
}
|
|
}
|
|
|
|
export class ViewContainer extends View {
|
|
public getInnerWidth() {
|
|
return 0;
|
|
}
|
|
|
|
public getInnerHeight() {
|
|
return 0;
|
|
}
|
|
|
|
public addView(view: View) {
|
|
view.inject(this.element, this.theme);
|
|
}
|
|
|
|
public clear() {
|
|
const markForRemoval: Element[] = [];
|
|
forEachElement(this.element.childNodes, e => {
|
|
markForRemoval.push(e);
|
|
});
|
|
markForRemoval.forEach(e => {
|
|
this.element.removeChild(e);
|
|
})
|
|
}
|
|
|
|
public onComponentInjected() {
|
|
const observer = new MutationObserver(records => {
|
|
records.forEach(r => {
|
|
forEachElement(r.addedNodes, node => {
|
|
const instance = View.getInstance(node);
|
|
if (instance) {
|
|
instance.onComponentVisible();
|
|
}
|
|
});
|
|
forEachElement(r.removedNodes, node => {
|
|
const instance = View.getInstance(node);
|
|
if (instance) {
|
|
instance.onComponentHidden();
|
|
}
|
|
});
|
|
})
|
|
});
|
|
|
|
observer.observe(this.element, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
}
|
|
|
|
protected buildDom(): SVGElement {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function forEachElement(nodes: NodeList, cb: (e: Element) => void) {
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
const node = nodes[i];
|
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
cb(node as Element);
|
|
}
|
|
}
|
|
}
|
|
} |