From f20131063439167a5c0f99f7f9d65d11653f993e Mon Sep 17 00:00:00 2001 From: ralphhanna Date: Sun, 27 Sep 2026 07:50:04 -0400 Subject: [PATCH] Restore instance animation replay --- client-tests/svgHelper.test.mjs | 49 +++++++++++++++++++++++++++++++++ package-lock.json | 7 +++++ package.json | 1 + public/javascripts/SVGHelper.js | 47 ++++++++++++++++++++----------- src/app.ts | 1 + src/views/InstanceViewer.ts | 3 +- src/views/includes/head.pug | 6 ++-- src/views/layout.pug | 2 -- src/views/layoutHome.pug | 1 - src/views/user/layout.pug | 1 - 10 files changed, 94 insertions(+), 24 deletions(-) diff --git a/client-tests/svgHelper.test.mjs b/client-tests/svgHelper.test.mjs index 5dcd39b..aec6657 100644 --- a/client-tests/svgHelper.test.mjs +++ b/client-tests/svgHelper.test.mjs @@ -53,3 +53,52 @@ test('getItemDescription builds an HTML table with id and type rows', () => { test('getItemDescription returns empty string when desc is null', () => { assert.equal(sb.getItemDescription(null, { id: 'x' }), ''); }); + +test('Show Animation plays the selected element and restarts cleanly', () => { + const timelines = []; + const element = { classList: { remove() {} } }; + const gsap = { + registerPlugin() {}, + timeline(options) { + const timeline = { + options, targets: [], plays: 0, killed: false, + to(target) { this.targets.push(target); return this; }, + call() { return this; }, + play() { this.plays++; }, + kill() { this.killed = true; }, + }; + timelines.push(timeline); + return timeline; + }, + }; + const document = { + addEventListener() {}, + querySelectorAll() { return []; }, + querySelector(selector) { return selector === '[data-element-id="StartEvent_1"]' ? element : null; }, + getElementById(id) { + if (id === 'jsonInfo') return { textContent: JSON.stringify([ + { message: "{'id':'StartEvent_1','seq':1,'action':'Started'}" }, + ]) }; + return null; + }, + }; + const script = loadClientScript('SVGHelper.js', { + gsap, document, CSS: { escape: value => value }, + }); + + script.startAnimation(); + assert.equal(timelines[0].options.paused, true); + assert.deepEqual(timelines[0].targets, [element]); + assert.equal(timelines[0].plays, 1); + + script.startAnimation(); + assert.equal(timelines[0].killed, true); + assert.equal(timelines[1].plays, 1); +}); + +test('Show Animation does not throw if an item is absent from the diagram', () => { + const script = loadClientScript('SVGHelper.js', { + document: { addEventListener() {}, querySelector: () => null }, + }); + assert.doesNotThrow(() => script.endAnimation('missing', 1, 'Ended')); +}); diff --git a/package-lock.json b/package-lock.json index 1d28f04..8002b0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "express-flash": "^0.0.2", "express-rate-limit": "^8.6.0", "express-session": "^1.19.0", + "gsap": "3.12.2", "helmet": "^8.3.0", "lodash": "^4.18.1", "lusca": "^1.7.0", @@ -2549,6 +2550,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gsap": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.2.tgz", + "integrity": "sha512-EkYnpG8qHgYBFAwsgsGEqvT1WUidX0tt/ijepx7z8EUJHElykg91RvW1XbkT59T0gZzzszOpjQv7SE41XuIXyQ==", + "license": "Standard 'no charge' license: https://greensock.com/standard-license. Club GreenSock members get more: https://greensock.com/licensing/. Why GreenSock doesn't employ an MIT license: https://greensock.com/why-license/" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", diff --git a/package.json b/package.json index a01269a..b87c0b7 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "express-flash": "^0.0.2", "express-rate-limit": "^8.6.0", "express-session": "^1.19.0", + "gsap": "3.12.2", "helmet": "^8.3.0", "lodash": "^4.18.1", "lusca": "^1.7.0", diff --git a/public/javascripts/SVGHelper.js b/public/javascripts/SVGHelper.js index 3f205dc..86eeb21 100644 --- a/public/javascripts/SVGHelper.js +++ b/public/javascripts/SVGHelper.js @@ -9,14 +9,14 @@ function animateFlow(flowId,seq) { let group = document.querySelector(`[data-element-id="${flowId}"]`); if (!group) { console.warn(`Flow group not found: ${flowId}`); - return gsap.to({}, {}); // Prevent breaking animation + return; } group.classList.add("Completed"); let path = group.querySelector("path"); if (!path) { console.warn(`No found inside group: ${flowId}`); - return gsap.to({}, {}); // Prevent breaking animation + return; } // $('#seq_'+seq).show(); @@ -28,16 +28,20 @@ function animateFlow(flowId,seq) { movingDot.setAttribute("r", "5"); // Radius of the dot movingDot.setAttribute("fill", "red"); // Color of the dot movingDot.setAttribute("data-moving-dot", flowId); - path.appendChild(movingDot); + group.appendChild(movingDot); // **Animate Path and Move the "Head"** gsap.to(path, { strokeDashoffset: 0, duration: 1, ease: "power2.out" }); - gsap.to(movingDot, { - motionPath: { path: path, align: path, alignOrigin: [0.5, 0.5] }, - duration: 1, - ease: "power2.out", - onComplete: () => movingDot.remove() // Remove dot after animation - }); + if (typeof MotionPathPlugin !== 'undefined') { + gsap.to(movingDot, { + motionPath: { path: path, align: path, alignOrigin: [0.5, 0.5] }, + duration: 1, + ease: "power2.out", + onComplete: () => movingDot.remove() + }); + } else { + movingDot.remove(); + } } function animateFlow2(flowId) { @@ -94,6 +98,7 @@ function activateTask(taskId,seq) { function endAnimation(elementId,seq,action) { let element = document.querySelector(`[data-element-id="${elementId}"]`); + if (!element) return; element.classList.remove("Pending"); if (action==='Ended') element.classList.add("Completed"); @@ -119,11 +124,10 @@ function endAnimation(elementId,seq,action) { // GSAP powers the optional "Show Animation" feature only. Guard its module-load use so a // missing/blocked gsap never throws here — otherwise the whole script aborts and the core // diagram decorations (scanSVG: sequence numbers + status colors) silently disappear. -const gsapLoaded = typeof gsap !== 'undefined'; -let tl = gsapLoaded ? gsap.timeline() : null; // Start paused -if (gsapLoaded && typeof MotionPathPlugin !== 'undefined') gsap.registerPlugin(MotionPathPlugin); +let tl = null; +if (typeof gsap !== 'undefined' && typeof MotionPathPlugin !== 'undefined') gsap.registerPlugin(MotionPathPlugin); function pauseAnimation() { - + if (!tl) return; tl.addPause(() => { console.log("Timeline paused — waiting for user..."); document.getElementById("continueBtn").style.display = "block"; @@ -131,18 +135,27 @@ function pauseAnimation() { } function continueAnimation() { + if (!tl) return; tl.play(); document.getElementById("continueBtn").style.display = "none"; } function startAnimation() { + if (typeof gsap === 'undefined') { + console.error("Animation is unavailable: GSAP did not load."); + return; + } + if (tl) tl.kill(); + document.querySelectorAll('[data-moving-dot]').forEach(dot => dot.remove()); + tl = gsap.timeline({ paused: true }); document.querySelectorAll(`.Pending`).forEach(element => { element.classList.remove("Pending"); }); document.querySelectorAll(`.Completed`).forEach(element => { element.classList.remove("Completed"); }); document.querySelectorAll(`.Cancelled`).forEach(element => { element.classList.remove("Cancelled"); }); let flowInfo = getFlowInfo(); + if (!flowInfo.length) return; for(let i=0;i animateFlow(item.id,item.seq+1), null, "+=0.5"); // Activate task and wait @@ -174,7 +189,7 @@ function startAnimation() { tl.call(() => endAnimation(item.id,item.seq+1,item.action), null, "+=0.5"); } } - // tl.play(); + tl.play(); return; } @@ -539,4 +554,4 @@ function getDescAttribute(desc, attr, pre, post) { return ''; else return pre + txt + post; -} \ No newline at end of file +} diff --git a/src/app.ts b/src/app.ts index df68607..e00dacf 100644 --- a/src/app.ts +++ b/src/app.ts @@ -189,6 +189,7 @@ export class WebApp { var router = express.Router(); var root=path.join(__dirname,'../'); + router.use('/vendor/gsap', express.static(path.join(root, 'node_modules/gsap/dist'), { maxAge: 31557600000 })); router.use('/', express.static(path.join(root, 'public'), { maxAge: 31557600000 })); router.use('/js/lib', express.static(path.join(root, 'node_modules/chart.js/dist'), { maxAge: 31557600000 })); router.use('/js/lib', express.static(path.join(root, 'node_modules/popper.js/dist/umd'), { maxAge: 31557600000 })); diff --git a/src/views/InstanceViewer.ts b/src/views/InstanceViewer.ts index f04b38b..6e0fee9 100644 --- a/src/views/InstanceViewer.ts +++ b/src/views/InstanceViewer.ts @@ -74,7 +74,8 @@ function getHeader() { - + + diff --git a/src/views/includes/head.pug b/src/views/includes/head.pug index 423fc54..c2ce362 100644 --- a/src/views/includes/head.pug +++ b/src/views/includes/head.pug @@ -9,9 +9,9 @@ script(src="/javascripts/jquery.isloading.min.js") script(src="/javascripts/workflow.js") script(src="/javascripts/processEditor.js") script(src="/javascripts/jsonHelper.js") -//- GSAP (instance-viewer "Show Animation"); load before SVGHelper. Decorations work without it. -script(src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js") -script(src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/MotionPathPlugin.min.js") +//- Load animation dependencies once before SVGHelper. +script(src="/vendor/gsap/gsap.min.js") +script(src="/vendor/gsap/MotionPathPlugin.min.js") script(src="/javascripts/SVGHelper.js") link(rel="stylesheet" type="text/css" href="/stylesheets/bootstrap.min.css") link(rel="stylesheet" type="text/css" href="/stylesheets/bootstrap-theme.min.css") diff --git a/src/views/layout.pug b/src/views/layout.pug index d721fb0..795dfee 100644 --- a/src/views/layout.pug +++ b/src/views/layout.pug @@ -2,8 +2,6 @@ extends base block head script(src="/javascripts/descriptions.js") - script(src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js") - script(src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/MotionPathPlugin.min.js") block nav include includes/nav.pug diff --git a/src/views/layoutHome.pug b/src/views/layoutHome.pug index f3a41d8..65a4133 100644 --- a/src/views/layoutHome.pug +++ b/src/views/layoutHome.pug @@ -4,7 +4,6 @@ block title title bpmn-server Workbench block head - script(src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js") block nav include includes/navHome.pug diff --git a/src/views/user/layout.pug b/src/views/user/layout.pug index f59a5fa..9bb07e2 100644 --- a/src/views/user/layout.pug +++ b/src/views/user/layout.pug @@ -2,7 +2,6 @@ extends ../base block head script(src="/javascripts/descriptions.js") - script(src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js") block nav include nav.pug