Skip to content

Commit

Permalink
add VueJS demo
Browse files Browse the repository at this point in the history
  • Loading branch information
Ian Sietro committed Feb 13, 2021
1 parent 4efcc40 commit 87bf9b9
Show file tree
Hide file tree
Showing 6 changed files with 374 additions and 0 deletions.
93 changes: 93 additions & 0 deletions demo/vuejs/demo.css
@@ -0,0 +1,93 @@
#app {
font-family: sans-serif;
position: relative;
width: 850px;
height: 600px;
overflow: hidden;
}

.toolbar {
z-index: 2;
position: absolute;
top: 16px;
left: 50%;
transform: translateX(-50%);
}

.toolbar button {
padding: 8px 16px;
margin: 0 4px;
font-size: 12px;
border-radius: 4px;
border: solid 1px #D3D4D6;
color: #1c1c1c;
background: #F8F9FB;
outline: none;
cursor: pointer;
}

.toolbar button:hover { border-color: #636464; }

.task {
box-sizing: border-box;
pointer-events: none;
position: absolute;
z-index: 1;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
border-radius: 4px;
background: white;
}

.task::before {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
height: 8px;
border-radius: 4px 4px 0 0;
}

.task[data-status=done]::before { background: #41B883; }
.task[data-status=pending]::before { background: #36485E; }
.task[data-status=at-risk]::before { background: #FF785A; }

.task header {
display: flex;
justify-content: space-between;
margin: 32px 24px 24px;
}

.task h1 {
margin: 0;
font-weight: bold;
font-size: 16px;
color: #1c1c1c;
}

.task i {
width: 16px;
height: 16px;
background-position: center;
background-repeat: no-repeat;
background-size: 16px;
}

.task[data-status=done] i { background-image: url('./images/icon-done.svg'); }
.task[data-status=pending] i { background-image: url('./images/icon-pending.svg'); }
.task[data-status=at-risk] i { background-image: url('./images/icon-warning.svg'); }

.task input, .task select {
pointer-events: auto;
display: block;
width: 200px;
margin: 24px;
padding: 10px;
border: solid 1px #DADADA;
border-radius: 4px;
color: #1c1c1c;
}

.task select {
padding: 4px 0;
}
253 changes: 253 additions & 0 deletions demo/vuejs/demo.js
@@ -0,0 +1,253 @@
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 @click="$emit('zoom-out')">Zoom Out</button>
<button @click="$emit('zoom-in')">Zoom In</button>
<button @click="$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"
@input="$emit('input', id, 'assignment', $event.target.value)"
/>
<select :value="task.status" @input="$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]"
@input="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 @zoom-out="zoomOut" @zoom-in="zoomIn" @reset="reset"/>
<my-joint-paper :scale="scale" :tasks="tasks" @task-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');
1 change: 1 addition & 0 deletions demo/vuejs/images/icon-done.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions demo/vuejs/images/icon-pending.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions demo/vuejs/images/icon-warning.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions demo/vuejs/index.html
@@ -0,0 +1,25 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VueJS Demo</title>

<link rel="stylesheet" type="text/css" href="../../build/joint.css" />
<link rel="stylesheet" type="text/css" href="./demo.css" />
</head>
<body>
<!-- JointJS dependencies -->
<script src="../../node_modules/jquery/dist/jquery.js"></script>
<script src="../../node_modules/lodash/lodash.js"></script>
<script src="../../node_modules/backbone/backbone.js"></script>
<script src="../../build/joint.js"></script>

<!-- VueJS (global) -->
<script src="https://unpkg.com/vue@3.0.0"></script>

<!-- Demo -->
<div id="app"></div>
<script src="./demo.js"></script>
</body>
</html>

0 comments on commit 87bf9b9

Please sign in to comment.