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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+