jointjs
Version:
JavaScript diagramming library
254 lines (228 loc) • 8.44 kB
JavaScript
var DATA = {
scale: 1,
tasks: {
taskA: {
title: 'Create Story',
assignment: 'Bob',
status: 'done',
},
taskB: {
title: 'Promote',
assignment: 'Mary',
status: 'pending',
},
taskC: {
title: 'Measure',
assignment: 'John',
status: 'at-risk',
},
},
};
// Define a Vue Component for Toolbar
var ToolbarComponent = {
emits: ['zoom-out', 'zoom-in', 'reset'],
template: `
<div class="toolbar">
<button ="$emit('zoom-out')">Zoom Out</button>
<button ="$emit('zoom-in')">Zoom In</button>
<button ="$emit('reset')">Reset</button>
</div>
`,
};
// Define a Vue Component for Task
var TaskComponent = {
props: ['id', 'task', 'position', 'scale'],
emits: ['input'],
template: `
<div
ref="taskElement"
class="task"
:data-status="task.status"
:style="taskElementStyle"
>
<header><h1 v-text="task.title"/><i/></header>
<input
placeholder="Enter an assignment …"
:value="task.assignment"
="$emit('input', id, 'assignment', $event.target.value)"
/>
<select :value="task.status" ="$emit('input', id, 'status', $event.target.value)">
<option disabled value="" text="Select status …"/>
<option value="done" text="Done"/>
<option value="pending" text="Pending"/>
<option value="at-risk" text="At Risk"/>
</select>
</div>
`,
setup(props) {
var graph = Vue.inject('graph');
var paperContext = Vue.inject('paperContext');
var taskElement = Vue.ref(null);
var taskElementPosition = Vue.shallowRef({ x: 0, y: 0 });
var taskElementStyle = Vue.computed(function () {
return {
top: (taskElementPosition.value.y || 0) + 'px',
left: (taskElementPosition.value.x || 0) + 'px',
transform: 'scale(' + props.scale + ')',
transformOrigin: '0 0',
}
});
// Update task element position to match the graph Element View
function updateTaskElementPosition() {
if (paperContext.paper) {
var graphElementView = paperContext.paper.findViewByModel(graph.getCell(props.id));
var viewBBox = graphElementView.getBBox({ useModelGeometry: true });
taskElementPosition.value = { x: viewBBox.x, y: viewBBox.y };
}
}
Vue.onMounted(function () {
// Resize the graph Element to match the task element ...
graph.getCell(props.id).resize(taskElement.value.offsetWidth, taskElement.value.offsetHeight);
// ... and update task element position afterwards
Vue.nextTick(updateTaskElementPosition);
});
// React to changes of position/scale
Vue.watch(
function () {
return {
position: props.position,
scale: props.scale,
};
},
updateTaskElementPosition,
);
return {
id: props.id,
task: props.task,
taskElement: taskElement,
taskElementStyle: taskElementStyle,
};
},
};
// Define a Vue Component for JointJS Paper
var JointPaperComponent = {
props: ['tasks', 'scale'],
emits: ['task-change'],
components: { 'my-task': TaskComponent },
template: `
<my-task
v-for="element in htmlElements"
:key="element.id"
:id="element.id"
:position="element.position"
:scale="scale"
:task="tasks[element.id]"
="handleTaskInput"
/>
<div ref="paperElement"/>
`,
setup(props, vmContext) {
var scale = Vue.toRef(props, 'scale');
var graph = Vue.inject('graph');
var paperElement = Vue.ref(null);
var paperContext = {};
Vue.provide('paperContext', paperContext);
// Create JointJS Paper (after the paper element is available)
Vue.onMounted(function () {
paperContext.paper = new joint.dia.Paper({
el: paperElement.value,
model: graph,
width: 850,
height: 600,
background: {
color: '#F8F9FB',
},
});
});
/*
* Create a custom observable for (current) graph elements.
* Warning: Observing the graph directly may trigger
* too many updates and cause performance issues.
*/
var htmlElements = Vue.shallowRef(
graph.getElements().map(function (cell) {
return { id: cell.get('id'), position: cell.get('position')};
})
);
// Track positions of graph elements
graph.on('change:position', function (cell) {
for (var i = 0; i < htmlElements.value.length; i += 1) {
if (htmlElements.value[i].id === cell.get('id')) {
htmlElements.value[i].position = cell.get('position');
Vue.triggerRef(htmlElements);
break;
}
}
});
// React to changes of scale
Vue.watch(
function () { return scale.value; },
function (value) {
var size = paperContext.paper.getComputedSize();
paperContext.paper.translate(0, 0);
paperContext.paper.scale(value, value, size.width / 2, size.height / 2);
}
);
function handleTaskInput(taskId, key, value) {
vmContext.emit('task-change', taskId, key, value);
}
return {
tasks: props.tasks,
paperElement: paperElement,
htmlElements: htmlElements,
scale: scale,
handleTaskInput: handleTaskInput,
};
},
};
// Create a Vue application
var app = Vue.createApp({
components: { 'my-toolbar': ToolbarComponent, 'my-joint-paper': JointPaperComponent },
template: `
<my-toolbar -out="zoomOut" -in="zoomIn" ="reset"/>
<my-joint-paper :scale="scale" :tasks="tasks" -change="handleTaskChange"/>
`,
setup() {
var tasks = Vue.reactive(JSON.parse(JSON.stringify(DATA.tasks)));
var scale = Vue.ref(DATA.scale);
var graph = new joint.dia.Graph();
Vue.provide('graph', graph);
function handleTaskChange(taskId, key, value) {
tasks[taskId][key] = value;
}
function zoomOut() {
scale.value = Math.max(0.2, scale.value - 0.2);
}
function zoomIn() {
scale.value = Math.min(3, scale.value + 0.2);
}
function reset() {
scale.value = DATA.scale;
graph.getCell('taskA').position(17, 100);
graph.getCell('taskB').position(297, 100);
graph.getCell('taskC').position(576, 100);
Object.entries(DATA.tasks).forEach(function ([taskId, task]) {
Object.entries(task).forEach(function ([key, value]) {
handleTaskChange(taskId, key, value);
});
});
}
// Create JointJS elements and links for the tasks
var rect1 = (new joint.shapes.standard.Rectangle()).position(17, 100).attr({
body: { fill: 'transparent', strokeWidth: 0 }
}).prop('id', 'taskA');
var rect2 = rect1.clone().position(297, 100).prop('id', 'taskB');
var rect3 = rect1.clone().position(576, 100).prop('id', 'taskC');
var link1 = (new joint.shapes.standard.Link()).source(rect1).target(rect2);
var link2 = (new joint.shapes.standard.Link()).source(rect2).target(rect3);
graph.resetCells([rect1, rect2, rect3, link1, link2]);
return {
tasks: tasks,
zoomOut: zoomOut,
zoomIn: zoomIn,
reset: reset,
scale: scale,
handleTaskChange: handleTaskChange,
};
},
}).mount('#app');