Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Ian Sietro
committed
Feb 13, 2021
1 parent
4efcc40
commit 87bf9b9
Showing
6 changed files
with
374 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); |
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |