From 87bf9b97598232268d31ed9114087567b6e55e1e Mon Sep 17 00:00:00 2001 From: Ian Sietro <31187902+IanSietro@users.noreply.github.com> Date: Sun, 14 Feb 2021 02:05:56 +0700 Subject: [PATCH] add VueJS demo --- demo/vuejs/demo.css | 93 +++++++++++ demo/vuejs/demo.js | 253 +++++++++++++++++++++++++++++ demo/vuejs/images/icon-done.svg | 1 + demo/vuejs/images/icon-pending.svg | 1 + demo/vuejs/images/icon-warning.svg | 1 + demo/vuejs/index.html | 25 +++ 6 files changed, 374 insertions(+) create mode 100644 demo/vuejs/demo.css create mode 100644 demo/vuejs/demo.js create mode 100644 demo/vuejs/images/icon-done.svg create mode 100644 demo/vuejs/images/icon-pending.svg create mode 100644 demo/vuejs/images/icon-warning.svg create mode 100644 demo/vuejs/index.html diff --git a/demo/vuejs/demo.css b/demo/vuejs/demo.css new file mode 100644 index 0000000000..8e048ca43c --- /dev/null +++ b/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; +} diff --git a/demo/vuejs/demo.js b/demo/vuejs/demo.js new file mode 100644 index 0000000000..383eec664a --- /dev/null +++ b/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: ` +
+ + + +
+ `, +}; + +// Define a Vue Component for Task +var TaskComponent = { + props: ['id', 'task', 'position', 'scale'], + emits: ['input'], + template: ` +
+

+ + +
+ `, + 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: ` + +
+ `, + 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: ` + + + `, + 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'); diff --git a/demo/vuejs/images/icon-done.svg b/demo/vuejs/images/icon-done.svg new file mode 100644 index 0000000000..ad2cdf9e08 --- /dev/null +++ b/demo/vuejs/images/icon-done.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demo/vuejs/images/icon-pending.svg b/demo/vuejs/images/icon-pending.svg new file mode 100644 index 0000000000..91e0fccf3e --- /dev/null +++ b/demo/vuejs/images/icon-pending.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demo/vuejs/images/icon-warning.svg b/demo/vuejs/images/icon-warning.svg new file mode 100644 index 0000000000..3c0b546a89 --- /dev/null +++ b/demo/vuejs/images/icon-warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demo/vuejs/index.html b/demo/vuejs/index.html new file mode 100644 index 0000000000..6526c38adf --- /dev/null +++ b/demo/vuejs/index.html @@ -0,0 +1,25 @@ + + + + + + VueJS Demo + + + + + + + + + + + + + + + +
+ + +