Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions client-tests/svgHelper.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
});
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
47 changes: 31 additions & 16 deletions public/javascripts/SVGHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path> found inside group: ${flowId}`);
return gsap.to({}, {}); // Prevent breaking animation
return;
}
// $('#seq_'+seq).show();

Expand All @@ -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) {
Expand Down Expand Up @@ -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");
Expand All @@ -119,30 +124,38 @@ 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";
});

}
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());
Comment on lines +149 to +150

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Revert active effects before restarting the replay

When Show Animation is clicked again before the previous effects have settled, tl.kill() only cancels animations and callbacks owned by this timeline. The callbacks in animateFlow, activateTask, and endAnimation launch independent gsap.to tweens, including a 10-second fill tween, and killing a partially progressed first-element tween does not restore its original transform. Those effects can therefore continue modifying the diagram during the new replay or leave the first node enlarged; track these tweens in the timeline/a GSAP context and revert or kill them before resetting the state.

Useful? React with 👍 / 👎.

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<flowInfo.length;i++)
{
Expand All @@ -158,7 +171,9 @@ function startAnimation() {
if (i==0)
{
$('#seq_'+item.seq).hide();
tl.to('[data-element-id="${item.id}"]', { scale: 1.2, duration: 0.5, repeat: 1, yoyo: true });
const firstElement = item.id && document.querySelector('[data-element-id="' + CSS.escape(item.id) + '"]');
if (firstElement)
tl.to(firstElement, { scale: 1.2, duration: 0.5, repeat: 1, yoyo: true });
}
else if (item.type==='bpmn:SequenceFlow')
tl.call(() => animateFlow(item.id,item.seq+1), null, "+=0.5"); // Activate task and wait
Expand All @@ -174,7 +189,7 @@ function startAnimation() {
tl.call(() => endAnimation(item.id,item.seq+1,item.action), null, "+=0.5");
}
}
// tl.play();
tl.play();

return;
}
Expand Down Expand Up @@ -539,4 +554,4 @@ function getDescAttribute(desc, attr, pre, post) {
return '';
else
return pre + txt + post;
}
}
1 change: 1 addition & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
Expand Down
3 changes: 2 additions & 1 deletion src/views/InstanceViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ function getHeader() {
<script type='text/javascript' src='/vendor/dhtmlx/codebase/datastore.js'></script>
<script type='text/javascript' src='/javascripts/jsonHelper.js'></script>
<!-- GSAP Library from CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="/vendor/gsap/gsap.min.js"></script>
<script src="/vendor/gsap/MotionPathPlugin.min.js"></script>
<script type='text/javascript' src='/javascripts/SVGHelper.js'></script>

<link rel='stylesheet' href='/vendor/dhtmlx/codebase/dhtmlx.css' type='text/css'>
Expand Down
6 changes: 3 additions & 3 deletions src/views/includes/head.pug
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Comment on lines +13 to 15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Version the changed SVGHelper asset

On deployments that replace an existing release, returning browsers can continue using the old /javascripts/SVGHelper.js: the public static mount in src/app.ts assigns a one-year max-age, while this head still references the helper through the same unversioned URL. Such clients load the new local GSAP files but retain the pre-fix helper, so the animation restoration may not reach them until cache expiry or a hard refresh; fingerprint or version the helper URL when shipping these changes.

Useful? React with 👍 / 👎.

link(rel="stylesheet" type="text/css" href="/stylesheets/bootstrap.min.css")
link(rel="stylesheet" type="text/css" href="/stylesheets/bootstrap-theme.min.css")
Expand Down
2 changes: 0 additions & 2 deletions src/views/layout.pug
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 0 additions & 1 deletion src/views/layoutHome.pug
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 0 additions & 1 deletion src/views/user/layout.pug
Original file line number Diff line number Diff line change
Expand Up @@ -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