Workbench

Source

index.js
/* global document */

import Monologue from 'monologue.js';
import style from 'PVWStyle/ComponentNative/Workbench.mcss';
import Layouts from 'paraviewweb/src/React/Renderers/MultiLayoutRenderer/Layouts';

const CHANGE_TOPIC = 'Workbench.change';
const VISIBILITY_TOPIC = 'Workbench.visibility';
const noOpRenderer = { resize() {}, render() {} };
const NUMBER_OF_VIEWPORTS = 4;
const LAYOUT_TO_COUNT = {
'2x2': 4,
'1x1': 1,
'1x2': 2,
'2x1': 2,
'3xT': 3,
'3xL': 3,
'3xR': 3,
'3xB': 3,
};

function applyLayout(viewport, layout) {
const { el, renderer } = viewport;
let styleElements = [];

if (layout) {
styleElements = layout.slice();
} else {
styleElements = [0, 0, 0, 0];
}

el.style.top = `${styleElements[1]}px`;
el.style.left = `${styleElements[0]}px`;
el.style.width = `${styleElements[2]}px`;
el.style.height = `${styleElements[3]}px`;

(renderer || noOpRenderer).resize();
}

export default class ComponentWorkbench {
constructor(
el,
{
useMouse: useMouse = true,
spacing: spacing = 10,
center: center = [0.5, 0.5],
} = {}
) {
this.el = null;
this.useMouse = useMouse;
this.dragging = false;
this.dragOffset = { x: 0, y: 0 };
this.wbArrange = {
center,
spacing,
};
this.layoutList = [];
this.boundingRect = {};
this.viewportList = [];
this.activeLayout = Object.keys(Layouts)[0];
this.layoutFn = Layouts[this.activeLayout];
this.mouseHandlers = {
mousedown: (event) => {
if (
this.getClickedViewport(
event.clientX - this.boundingRect.left,
event.clientY - this.boundingRect.top
) === -1 &&
event.target === this.el
) {
this.dragging = true;
// offset from current center to drag start.
this.dragOffset.x =
this.boundingRect.width * this.wbArrange.center[0] -
(event.clientX - this.boundingRect.left);
this.dragOffset.y =
this.boundingRect.height * this.wbArrange.center[1] -
(event.clientY - this.boundingRect.top);
event.stopPropagation();
event.preventDefault();
}
},
mouseup: (event) => {
this.dragging = false;
},
mousemove: (event) => {
if (this.dragging) {
event.stopPropagation();
event.preventDefault();
const centerSize = this.wbArrange.spacing;
if (Math.abs(this.dragOffset.x) > centerSize) {
// only drag boundary vertically
this.wbArrange.center[1] =
(event.clientY - this.boundingRect.top + this.dragOffset.y) /
this.boundingRect.height;
} else if (Math.abs(this.dragOffset.y) > centerSize) {
// only drag boundary horizontally
this.wbArrange.center[0] =
(event.clientX - this.boundingRect.left + this.dragOffset.x) /
this.boundingRect.width;
} else {
this.wbArrange.center = [
(event.clientX - this.boundingRect.left + this.dragOffset.x) /
this.boundingRect.width,
(event.clientY - this.boundingRect.top + this.dragOffset.y) /
this.boundingRect.height,
];
}
this.render();
}
},
};

this.initializeViewports();
this.setContainer(el);
this.computeContainerGeometry();
}

initializeViewports() {
for (let i = 0; i < NUMBER_OF_VIEWPORTS; ++i) {
const newElt = document.createElement('div');
newElt.setAttribute('class', style.viewport);

this.viewportList.push({
el: newElt,
renderer: null,
});
}
}

setComponents(componentDict) {
this.componentMap = componentDict;

Object.keys(componentDict).forEach((k) => {
if (componentDict[k].viewport !== -1) {
// set the viewport as well
this.setViewport(
componentDict[k].viewport,
componentDict[k].component,
false
);
}
});

this.componentMap.None = {
component: null,
viewport: -1,
};

this.triggerChange();
}

getClickedViewport(x, y) {
let index = -1;
for (let i = 0; i < this.layoutList.length; ++i) {
const layout = this.layoutList[i];
if (
x >= layout[0] &&
x <= layout[0] + layout[2] &&
y >= layout[1] &&
y <= layout[1] + layout[3]
) {
index = i;
}
}
return index;
}

addMouseListeners() {
// Set up mouse handling so we can resize the individual viewports
this.el.addEventListener('mousedown', this.mouseHandlers.mousedown);
this.el.addEventListener('mouseup', this.mouseHandlers.mouseup);
this.el.addEventListener('mousemove', this.mouseHandlers.mousemove);
}

removeMouseListeners() {
this.el.removeEventListener('mousedown', this.mouseHandlers.mousedown);
this.el.removeEventListener('mouseup', this.mouseHandlers.mouseup);
this.el.removeEventListener('mousemove', this.mouseHandlers.mousemove);
}

render() {
const pixelCenter = [
this.wbArrange.center[0] * this.boundingRect.width,
this.wbArrange.center[1] * this.boundingRect.height,
];
this.layoutList = this.layoutFn(
pixelCenter,
this.wbArrange.spacing,
this.wbArrange.width,
this.wbArrange.height
);

// Now apply new styles, rendering the new workbench layout and each component
for (let i = 0; i < NUMBER_OF_VIEWPORTS; ++i) {
applyLayout(this.viewportList[i], this.layoutList[i]);
}
}

computeContainerGeometry() {
if (this.el) {
this.boundingRect = this.el.getBoundingClientRect();
this.wbArrange.width = this.boundingRect.width;
this.wbArrange.height = this.boundingRect.height;
}
}

resize() {
if (this.el) {
this.computeContainerGeometry();
this.render();
}
}

/* eslint-disable class-methods-use-this */
checkIndex(idx) {
if (idx < 0 || idx >= NUMBER_OF_VIEWPORTS) {
throw new Error('The only available indices are in the range [0, 3]');
}
}

setViewport(index, instance, shouldTriggerChange = true) {
let count = NUMBER_OF_VIEWPORTS;
this.checkIndex(index);

// Find out if this instance is in another viewport
while (count) {
count -= 1;
if (this.viewportList[count].renderer === instance) {
this.viewportList[count].renderer = null;
}
}

// Find out if this viewport already has something else in it
if (this.viewportList[index].renderer !== null) {
this.triggerVisibilityChange(
this.viewportList[index].renderer,
-1,
this.activeLayout
);
this.viewportList[index].renderer.setContainer(null);
this.viewportList[index].renderer = null;
}

this.triggerVisibilityChange(instance, index, this.activeLayout);
this.viewportList[index].renderer = instance;
this.viewportList[index].el.setAttribute('class', style.viewport);
if (instance !== null) {
instance.setContainer(this.viewportList[index].el);
instance.resize();
Object.keys(this.componentMap).forEach((name) => {
if (
this.componentMap[name].component === instance &&
this.componentMap[name].scroll
) {
this.viewportList[index].el.setAttribute(
'class',
style.scrollableViewport
);
}
});
}

if (shouldTriggerChange) {
this.triggerChange();
}
}

setContainer(el) {
if (this.el) {
this.viewportList.forEach((viewport) => {
this.el.removeChild(viewport.el);
});
this.removeMouseListeners();
}

this.el = el;
if (this.el) {
this.viewportList.forEach((viewport) => {
this.el.appendChild(viewport.el);
});
if (this.useMouse) {
this.addMouseListeners();
}
this.resize();
}
}

getViewport(index) {
this.checkIndex(index);
return this.viewportList[index].renderer;
}

/* eslint-disable class-methods-use-this */
getLayoutLabels() {
return Object.keys(Layouts);
}

/*
* Parameter 'layout' should be one of the layout keys:
*
* "2x2", "1x1", "1x2", "2x1", "3xT", "3xL", "3xR", "3xB"
*/
setLayout(layout) {
if (Layouts[layout]) {
if (this.activeLayout !== layout) {
if (LAYOUT_TO_COUNT[this.activeLayout] !== LAYOUT_TO_COUNT[layout]) {
const counts = [
LAYOUT_TO_COUNT[this.activeLayout],
LAYOUT_TO_COUNT[layout],
].sort();
for (let i = counts[0]; i < counts[1]; ++i) {
this.triggerVisibilityChange(
this.viewportList[i].renderer,
i,
layout
);
}
}
}
this.activeLayout = layout;
this.layoutFn = Layouts[layout];
this.resize();
//
this.triggerChange();
}
}

getViewportMapping() {
const viewportMapping = this.viewportList.map(
(viewport) => viewport.renderer
);
Object.keys(this.componentMap).forEach((name) => {
this.componentMap[name].viewport = viewportMapping.indexOf(
this.componentMap[name].component
);
});
return this.componentMap;
}

getLayout() {
return this.activeLayout;
}

getLayoutCount() {
return LAYOUT_TO_COUNT[this.activeLayout];
}

triggerChange() {
const viewports = this.getViewportMapping();
const layout = this.getLayout();
const center = this.getCenter();
const count = LAYOUT_TO_COUNT[layout];
this.emit(CHANGE_TOPIC, { layout, viewports, center, count });
}

onChange(callback) {
return this.on(CHANGE_TOPIC, callback);
}

// visibility changes are issued _before_ component.setContainer() is called to render the viewport's contents
// if index is -1, viewport will not be in the DOM
// if index is >= count, viewport is in the DOM but not visible in the current layout
triggerVisibilityChange(component, index, layout) {
const count = LAYOUT_TO_COUNT[layout];
this.emit(VISIBILITY_TOPIC, { component, index, count });
}

// register interest in visibility events
onVisibilityChange(callback) {
return this.on(VISIBILITY_TOPIC, callback);
}

setCenter(x, y) {
this.wbArrange.center = [x, y];
//
this.triggerChange();
}

getCenter() {
return this.wbArrange.center;
}

destroy() {
this.off();
this.setContainer(null);
this.viewportList.forEach((viewport) => {
viewport.el = null;
if (viewport.renderer && viewport.renderer.destroy) {
viewport.renderer.destroy();
viewport.renderer = null;
}
});
}
}

// Add Observer pattern using Monologue.js
Monologue.mixInto(ComponentWorkbench);