@anthologen/cytoscape-node-html-label
Version:
380 lines (321 loc) • 10.1 kB
text/typescript
type IHAlign = "left" | "center" | "right";
type IVAlign = "top" | "center" | "bottom";
declare var module: any;
declare var define: any;
declare var cytoscape: any;
interface CytoscapeNodeHtmlParams {
query?: string;
halign?: IHAlign;
valign?: IVAlign;
halignBox?: IHAlign;
valignBox?: IVAlign;
cssClass?: string;
tpl?: (d: any) => string;
}
interface CytoscapeContainerParams {
enablePointerEvents?: boolean;
}
(function () {
"use strict";
const $$find = function <T>(arr: T[], predicate: (a: T) => boolean) {
if (typeof predicate !== "function") {
throw new TypeError("predicate must be a function");
}
const length = arr.length >>> 0;
// eslint-disable-next-line prefer-rest-params
const thisArg = arguments[1];
let value;
for (let i = 0; i < length; i++) {
value = arr[i];
if (predicate.call(thisArg, value, i, arr)) {
return value;
}
}
return undefined;
};
interface ICyEventObject {
cy: any;
type: string;
target: any;
}
interface ICytoscapeNodeHtmlPosition {
x: number;
y: number;
w: number;
h: number;
}
interface ILabelElement {
data?: any;
position?: ICytoscapeNodeHtmlPosition;
node: HTMLElement;
}
interface HashTableElements {
[key: string]: LabelElement;
}
class LabelElement {
public tpl: (d: any) => string;
private _position: number[];
private _node: HTMLElement;
private _align: [number, number, number, number];
constructor({
node,
position = null,
data = null
}: ILabelElement,
params: CytoscapeNodeHtmlParams) {
this.updateParams(params);
this._node = node;
this.initStyles(params.cssClass);
if (data) {
this.updateData(data);
}
if (position) {
this.updatePosition(position);
}
}
updateParams({
tpl = () => "",
// eslint-disable-next-line @typescript-eslint/no-unused-vars
cssClass = null,
halign = "center",
valign = "center",
halignBox = "center",
valignBox = "center"
}: CytoscapeNodeHtmlParams) {
const _align = {
"top": -.5,
"left": -.5,
"center": 0,
"right": .5,
"bottom": .5
};
this._align = [
_align[halign],
_align[valign],
100 * (_align[halignBox] - 0.5),
100 * (_align[valignBox] - 0.5)
];
this.tpl = tpl;
}
updateData(data: any) {
while (this._node.firstChild) {
this._node.removeChild(this._node.firstChild);
}
const children = new DOMParser()
.parseFromString(this.tpl(data), "text/html")
.body.children;
for (let i = 0; i < children.length; ++i) {
const el = children[i];
this._node.appendChild(el);
}
}
getNode(): HTMLElement {
return this._node;
}
updatePosition(pos: ICytoscapeNodeHtmlPosition) {
this._renderPosition(pos);
}
private initStyles(cssClass: string) {
const stl = this._node.style;
stl.position = "absolute";
if (cssClass && cssClass.length) {
this._node.classList.add(cssClass);
}
}
private _renderPosition(position: ICytoscapeNodeHtmlPosition) {
const prev = this._position;
const x = position.x + this._align[0] * position.w;
const y = position.y + this._align[1] * position.h;
if (!prev || prev[0] !== x || prev[1] !== y) {
this._position = [x, y];
const valRel = `translate(${this._align[2]}%,${this._align[3]}%) `;
const valAbs = `translate(${x.toFixed(2)}px,${y.toFixed(2)}px) `;
const val = valRel + valAbs;
const stl = <any>this._node.style;
stl.webkitTransform = val;
stl.msTransform = val;
stl.transform = val;
}
}
}
/**
* LabelContainer
* Html manipulate, find and upgrade nodes
* it don't know about cy.
*/
class LabelContainer {
private _elements: HashTableElements;
private _node: HTMLElement;
constructor(node: HTMLElement) {
this._node = node;
this._elements = <HashTableElements>{};
}
addOrUpdateElem(id: string, param: CytoscapeNodeHtmlParams, payload: { data?: any, position?: ICytoscapeNodeHtmlPosition } = {}) {
const cur = this._elements[id];
if (cur) {
cur.updateParams(param);
cur.updateData(payload.data);
cur.updatePosition(payload.position);
} else {
const nodeElem = document.createElement("div");
this._node.appendChild(nodeElem);
this._elements[id] = new LabelElement({
node: nodeElem,
data: payload.data,
position: payload.position
}, param);
}
}
removeElemById(id: string) {
if (this._elements[id]) {
this._node.removeChild(this._elements[id].getNode());
delete this._elements[id];
}
}
updateElemPosition(id: string, position?: ICytoscapeNodeHtmlPosition) {
const ele = this._elements[id];
if (ele) {
ele.updatePosition(position);
}
}
updatePanZoom({pan, zoom}: { pan: { x: number, y: number }, zoom: number }) {
const val = `translate(${pan.x}px,${pan.y}px) scale(${zoom})`;
const stl = <any>this._node.style;
const origin = "top left";
stl.webkitTransform = val;
stl.msTransform = val;
stl.transform = val;
stl.webkitTransformOrigin = origin;
stl.msTransformOrigin = origin;
stl.transformOrigin = origin;
}
}
function cyNodeHtmlLabel(_cy: any, params: CytoscapeNodeHtmlParams[], options?: CytoscapeContainerParams) {
const _params = (!params || typeof params !== "object") ? [] : params;
const _lc = createLabelContainer();
_cy.one("render", (e: any) => {
createNodesCyHandler(e);
wrapCyHandler(e);
});
_cy.on("add", addCyHandler);
_cy.on("layoutstop", layoutstopHandler);
_cy.on("remove", removeCyHandler);
_cy.on("data", updateDataOrStyleCyHandler);
_cy.on("style", updateDataOrStyleCyHandler);
_cy.on("pan zoom", wrapCyHandler);
_cy.on("position bounds", moveCyHandler); // "bounds" - not documented event
return _cy;
function createLabelContainer(): LabelContainer {
const _cyContainer = _cy.container();
const _titlesContainer = document.createElement("div");
const _cyCanvas = _cyContainer.querySelector("canvas");
const cur = _cyContainer.querySelector("[class^='cy-node-html']");
if (cur) {
_cyCanvas.parentNode.removeChild(cur);
}
const stl = _titlesContainer.style;
stl.position = 'absolute';
stl['z-index'] = 10;
stl.width = '500px';
stl.margin = '0px';
stl.padding = '0px';
stl.border = '0px';
stl.outline = '0px';
stl.outline = '0px';
if (options && options.enablePointerEvents !== true) {
stl['pointer-events'] = 'none';
}
_cyCanvas.parentNode.appendChild(_titlesContainer);
return new LabelContainer(_titlesContainer);
}
function createNodesCyHandler({cy}: ICyEventObject) {
_params.forEach(x => {
cy.elements(x.query).forEach((d: any) => {
if (d.isNode()) {
_lc.addOrUpdateElem(d.id(), x, {
position: getNodePosition(d),
data: d.data()
});
}
});
});
}
function addCyHandler(ev: ICyEventObject) {
const target = ev.target;
const param = $$find(_params.slice().reverse(), x => target.is(x.query));
if (param) {
_lc.addOrUpdateElem(target.id(), param, {
position: getNodePosition(target),
data: target.data()
});
}
}
function layoutstopHandler({cy}: ICyEventObject) {
_params.forEach(x => {
cy.elements(x.query).forEach((d: any) => {
if (d.isNode()) {
_lc.updateElemPosition(d.id(), getNodePosition(d));
}
});
});
}
function removeCyHandler(ev: ICyEventObject) {
_lc.removeElemById(ev.target.id());
}
function moveCyHandler(ev: ICyEventObject) {
// console.log('moveCyHandler');
_lc.updateElemPosition(ev.target.id(), getNodePosition(ev.target));
}
function updateDataOrStyleCyHandler(ev: ICyEventObject) {
setTimeout(() => {
const target = ev.target;
const param = $$find(_params.slice().reverse(), x => target.is(x.query));
if (param && !target.removed()) {
_lc.addOrUpdateElem(target.id(), param, {
position: getNodePosition(target),
data: target.data()
});
} else {
_lc.removeElemById(target.id());
}
}, 0);
}
function wrapCyHandler({cy}: ICyEventObject) {
_lc.updatePanZoom({
pan: cy.pan(),
zoom: cy.zoom()
});
}
function getNodePosition(node: any): ICytoscapeNodeHtmlPosition {
return {
w: node.width(),
h: node.height(),
x: node.position("x"),
y: node.position("y")
};
}
}
// registers the extension on a cytoscape lib ref
const register = function (cy: any) {
if (!cy) {
return;
} // can't register if cytoscape unspecified
cy("core", "nodeHtmlLabel", function (optArr: any, options?: any) {
return cyNodeHtmlLabel(this, optArr, options);
});
};
if (typeof module !== "undefined" && module.exports) { // expose as a commonjs module
module.exports = function (cy: any) {
register(cy);
};
} else {
if (typeof define !== "undefined" && define.amd) { // expose as an amd/requirejs module
define("cytoscape-nodeHtmlLabel", function () {
return register;
});
}
}
if (typeof cytoscape !== "undefined") { // expose to global cytoscape (i.e. window.cytoscape)
register(cytoscape);
}
}());