init
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
@charset "UTF-8";
|
||||
:root {
|
||||
--wf-bg: var(--bg-primary);
|
||||
--wf-border: var(--border-color);
|
||||
--wf-border-hover: var(--border-color-translucent);
|
||||
--wf-dot-color: var(--body-color);
|
||||
--stage-bg: rgb(var(--primary-rgb));
|
||||
--stage-border: var(--border-color);
|
||||
--stage-text: var(--white);
|
||||
--stage-virtual-bg: purple;
|
||||
--transition-hover: var(--border-color-hover);
|
||||
--transition-label-bg: #2071c6;
|
||||
--transition-stroke: var(--transition-label-bg);
|
||||
--transition-label-color: var(--white);
|
||||
--transition-highlight: #ff6868;
|
||||
--zoom-btn-bg: transparent;
|
||||
--zoom-btn-hover: var(--border-color);
|
||||
--zoom-btn-color: var(--text-primary);
|
||||
--vf-custom-controls-bgcolor: var(--secondary);
|
||||
--stage-desc: #ffffffc0 !important;
|
||||
}
|
||||
|
||||
@keyframes dashmove {
|
||||
from {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
stroke-dashoffset: -36px;
|
||||
}
|
||||
}
|
||||
|
||||
#workflow-graph {
|
||||
cursor: grab;
|
||||
background-image: radial-gradient(circle at 1px 1px, var(--wf-dot-color) 1px, transparent 1px);
|
||||
border: 1px solid var(--stage-border);
|
||||
background-size: 38px 38px;
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
height: 80vh;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#workflow-graph:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
#workflow-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#graph {
|
||||
transform-origin: 0 0;
|
||||
width: fit-content;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
transition: transform .15s ease-out;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#graph.dragging {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
#stages {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#connections {
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.stage {
|
||||
z-index: 10;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
background: var(--stage-bg);
|
||||
border: 1px solid var(--stage-border);
|
||||
border-radius: 6px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 200px;
|
||||
min-height: 80px;
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
box-shadow: 0 4px 10px #0009;
|
||||
}
|
||||
|
||||
.stage:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.stage.dragging {
|
||||
z-index: 1000;
|
||||
transform: rotate(1deg) scale(1.02);
|
||||
}
|
||||
|
||||
.stage.virtual {
|
||||
background: var(--stage-virtual-bg);
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.stage-title {
|
||||
color: var(--stage-text);
|
||||
word-wrap: break-word;
|
||||
margin: 0 0 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.stage-description {
|
||||
color: var(--stage-desc);
|
||||
word-wrap: break-word;
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.stage-badge {
|
||||
color: var(--stage-text);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
background: var(--stage-border);
|
||||
border-radius: 3px;
|
||||
align-self: flex-start;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.transition-path {
|
||||
stroke: var(--transition-stroke);
|
||||
stroke-width: 3px;
|
||||
fill: none;
|
||||
opacity: .8;
|
||||
transition: stroke .2s, stroke-width .2s;
|
||||
}
|
||||
|
||||
.transition-path:hover {
|
||||
stroke-width: 3px;
|
||||
opacity: 1;
|
||||
stroke: var(--transition-hover);
|
||||
animation-duration: .5s;
|
||||
}
|
||||
|
||||
.arrow-marker {
|
||||
fill: var(--transition-stroke);
|
||||
transition: fill .2s;
|
||||
}
|
||||
|
||||
.transition-path:hover + .arrow-marker {
|
||||
fill: var(--transition-hover);
|
||||
}
|
||||
|
||||
.transition-path.highlighted {
|
||||
stroke: var(--transition-highlight);
|
||||
stroke-width: 5px;
|
||||
stroke-dasharray: 12 6;
|
||||
animation: .5s linear infinite dashmove;
|
||||
}
|
||||
|
||||
.transition-label-content {
|
||||
z-index: 20;
|
||||
min-width: 80px;
|
||||
max-width: 300px;
|
||||
color: var(--transition-label-color);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
border: 2px solid var(--transition-stroke);
|
||||
border-radius: 6px;
|
||||
padding: 2px 12px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
transition: background .2s, border .2s, box-shadow .2s;
|
||||
position: absolute;
|
||||
box-shadow: 0 2px 8px #00000026;
|
||||
background: var(--transition-label-bg) !important;
|
||||
}
|
||||
|
||||
.transition-label-content.highlighted {
|
||||
border-color: var(--transition-highlight);
|
||||
font-weight: 700;
|
||||
transition: transform .2s;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.zoom-controls {
|
||||
z-index: 1000;
|
||||
background: var(--vf-custom-controls-bgcolor);
|
||||
border: 1px solid var(--wf-border);
|
||||
backdrop-filter: blur(4px);
|
||||
border-radius: 6px;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.zoom-btn:hover {
|
||||
background: var(--zoom-btn-hover);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.zoom-btn:active {
|
||||
transition: transform .5s;
|
||||
transform: scale(.95);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@charset "UTF-8";:root{--wf-bg:var(--bg-primary);--wf-border:var(--border-color);--wf-border-hover:var(--border-color-translucent);--wf-dot-color:var(--body-color);--stage-bg:rgb(var(--primary-rgb));--stage-border:var(--border-color);--stage-text:var(--white);--stage-virtual-bg:purple;--transition-hover:var(--border-color-hover);--transition-label-bg:#2071c6;--transition-stroke:var(--transition-label-bg);--transition-label-color:var(--white);--transition-highlight:#ff6868;--zoom-btn-bg:transparent;--zoom-btn-hover:var(--border-color);--zoom-btn-color:var(--text-primary);--vf-custom-controls-bgcolor:var(--secondary);--stage-desc:#ffffffc0!important}@keyframes dashmove{0%{stroke-dashoffset:0}to{stroke-dashoffset:-36px}}#workflow-graph{cursor:grab;background-image:radial-gradient(circle at 1px 1px, var(--wf-dot-color) 1px, transparent 1px);border:1px solid var(--stage-border);background-size:38px 38px;border-radius:8px;width:100%;height:80vh;position:relative;overflow:hidden}#workflow-graph:active{cursor:grabbing}#workflow-container{width:100%;height:100%;position:relative;overflow:hidden}#graph{transform-origin:0 0;width:fit-content;min-width:100%;min-height:100%;transition:transform .15s ease-out;position:relative}#graph.dragging{transition:none}#stages{width:100%;height:100%;position:relative}#connections{z-index:1;pointer-events:none;width:100%;height:100%;position:absolute;top:0;left:0;overflow:visible}.stage{z-index:10;cursor:move;user-select:none;background:var(--stage-bg);border:1px solid var(--stage-border);border-radius:6px;flex-direction:column;justify-content:center;width:200px;min-height:80px;padding:12px 16px;display:flex;position:absolute;box-shadow:0 4px 10px #0009}.stage:hover{transform:translateY(-1px)}.stage.dragging{z-index:1000;transform:rotate(1deg)scale(1.02)}.stage.virtual{background:var(--stage-virtual-bg);border-width:1px}.stage-title{color:var(--stage-text);word-wrap:break-word;margin:0 0 6px;font-size:14px;font-weight:600;line-height:1.3}.stage-description{color:var(--stage-desc);word-wrap:break-word;margin:0 0 8px;font-size:12px;line-height:1.4}.stage-badge{color:var(--stage-text);text-transform:uppercase;letter-spacing:.5px;background:var(--stage-border);border-radius:3px;align-self:flex-start;padding:2px 6px;font-size:10px;font-weight:500;display:inline-block}.transition-path{stroke:var(--transition-stroke);stroke-width:3px;fill:none;opacity:.8;transition:stroke .2s,stroke-width .2s}.transition-path:hover{stroke-width:3px;opacity:1;stroke:var(--transition-hover);animation-duration:.5s}.arrow-marker{fill:var(--transition-stroke);transition:fill .2s}.transition-path:hover+.arrow-marker{fill:var(--transition-hover)}.transition-path.highlighted{stroke:var(--transition-highlight);stroke-width:5px;stroke-dasharray:12 6;animation:.5s linear infinite dashmove}.transition-label-content{z-index:20;min-width:80px;max-width:300px;color:var(--transition-label-color);text-align:center;white-space:nowrap;pointer-events:auto;cursor:pointer;user-select:none;border:2px solid var(--transition-stroke);border-radius:6px;padding:2px 12px;font-size:1rem;font-weight:600;line-height:1.2;transition:background .2s,border .2s,box-shadow .2s;position:absolute;box-shadow:0 2px 8px #00000026;background:var(--transition-label-bg)!important}.transition-label-content.highlighted{border-color:var(--transition-highlight);font-weight:700;transition:transform .2s;transform:scale(1.1)}.zoom-controls{z-index:1000;background:var(--vf-custom-controls-bgcolor);border:1px solid var(--wf-border);backdrop-filter:blur(4px);border-radius:6px;flex-direction:column;gap:2px;padding:4px;display:flex;position:absolute;bottom:20px;right:20px}.zoom-btn:hover{background:var(--zoom-btn-hover);transform:scale(1.05)}.zoom-btn:active{transition:transform .5s;transform:scale(.95)}
|
||||
Binary file not shown.
@@ -0,0 +1,199 @@
|
||||
@charset "UTF-8";
|
||||
:root {
|
||||
--vf-black: #000;
|
||||
--vf-custom-controls-bgcolor: var(--secondary);
|
||||
--vf-edge-color: #2071c6;
|
||||
--vf-white: #fff;
|
||||
--vf-opacity-less: #0000001a;
|
||||
--vf-opacity-more: #0009;
|
||||
}
|
||||
|
||||
.vue-flow {
|
||||
z-index: 0;
|
||||
direction: ltr;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[dir="rtl"] .vue-flow__node, [dir="rtl"] .edge-label, [dir="rtl"] .vue-flow__panel {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.vue-flow__container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.vue-flow__pane.draggable {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.vue-flow__pane.selection {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vue-flow__pane.dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.vue-flow__transformationpane {
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.vue-flow__viewport {
|
||||
z-index: 4;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.vue-flow__nodesselection-rect:focus, .vue-flow__nodesselection-rect:focus-visible, .vue-flow__edge.selected, .vue-flow__edge:focus, .vue-flow__edge:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.vue-flow .vue-flow__edges {
|
||||
pointer-events: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.vue-flow__edge-path, .vue-flow__connection-path {
|
||||
stroke: var(--success);
|
||||
stroke-width: 3px;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.vue-flow__edge.animated path {
|
||||
stroke-dasharray: 5;
|
||||
animation: .5s linear infinite dashdraw;
|
||||
}
|
||||
|
||||
.vue-flow__edge.animated path.vue-flow__edge-interaction {
|
||||
stroke-dasharray: none;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.vue-flow__edge.inactive, .vue-flow__connection {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vue-flow__connection .animated {
|
||||
stroke-dasharray: 5;
|
||||
animation: .5s linear infinite dashdraw;
|
||||
}
|
||||
|
||||
.vue-flow__connectionline {
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.vue-flow__nodes {
|
||||
pointer-events: none;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.vue-flow__node {
|
||||
box-sizing: border-box;
|
||||
pointer-events: all;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
transform-origin: 0 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.vue-flow__handle.connectable {
|
||||
pointer-events: all;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.vue-flow__panel {
|
||||
z-index: 5;
|
||||
margin: 15px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.vue-flow__panel.top {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.vue-flow__panel.bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.vue-flow__panel.left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.vue-flow__panel.right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.vue-flow__panel.center {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
@keyframes dashdraw {
|
||||
from {
|
||||
stroke-dashoffset: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.min-vh-80 {
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
.end-20-px {
|
||||
right: 20px !important;
|
||||
}
|
||||
|
||||
.top-25-px {
|
||||
top: 25px !important;
|
||||
}
|
||||
|
||||
.z-10 {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.z-20 {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.custom-edge {
|
||||
background: var(--vf-edge-color) !important;
|
||||
}
|
||||
|
||||
.vue-flow__minimap {
|
||||
box-shadow: 0 10px 15px -3px var(--vf-opacity-less);
|
||||
}
|
||||
|
||||
.custom-controls {
|
||||
background: var(--vf-custom-controls-bgcolor);
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.vue-flow__minimap.pannable {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.stage-node .edge-handler {
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
}
|
||||
|
||||
.stage-node:hover .vue-flow__handle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.vue-flow__node-stage {
|
||||
max-width: 250px !important;
|
||||
}
|
||||
|
||||
.workflow-browser-actions-list {
|
||||
background-color: var(--vf-black);
|
||||
box-shadow: 0 4px 10px var(--vf-opacity-more);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@charset "UTF-8";:root{--vf-black:#000;--vf-custom-controls-bgcolor:var(--secondary);--vf-edge-color:#2071c6;--vf-white:#fff;--vf-opacity-less:#0000001a;--vf-opacity-more:#0009}.vue-flow{z-index:0;direction:ltr;width:100%;height:100%;position:relative;overflow:hidden}[dir=rtl] .vue-flow__node,[dir=rtl] .edge-label,[dir=rtl] .vue-flow__panel{direction:rtl}.vue-flow__container{width:100%;height:100%;position:absolute;top:0;left:0}.vue-flow__pane.draggable{cursor:grab}.vue-flow__pane.selection{cursor:pointer}.vue-flow__pane.dragging{cursor:grabbing}.vue-flow__transformationpane{z-index:2;pointer-events:none;transform-origin:0 0}.vue-flow__viewport{z-index:4;overflow:clip}.vue-flow__nodesselection-rect:focus,.vue-flow__nodesselection-rect:focus-visible,.vue-flow__edge.selected,.vue-flow__edge:focus,.vue-flow__edge:focus-visible{outline:none}.vue-flow .vue-flow__edges{pointer-events:none;overflow:visible}.vue-flow__edge-path,.vue-flow__connection-path{stroke:var(--success);stroke-width:3px;fill:none}.vue-flow__edge.animated path{stroke-dasharray:5;animation:.5s linear infinite dashdraw}.vue-flow__edge.animated path.vue-flow__edge-interaction{stroke-dasharray:none;animation:none}.vue-flow__edge.inactive,.vue-flow__connection{pointer-events:none}.vue-flow__connection .animated{stroke-dasharray:5;animation:.5s linear infinite dashdraw}.vue-flow__connectionline{z-index:1001}.vue-flow__nodes{pointer-events:none;transform-origin:0 0}.vue-flow__node{box-sizing:border-box;pointer-events:all;cursor:default;user-select:none;transform-origin:0 0;position:absolute}.vue-flow__handle.connectable{pointer-events:all;cursor:crosshair}.vue-flow__panel{z-index:5;margin:15px;position:absolute}.vue-flow__panel.top{top:0}.vue-flow__panel.bottom{bottom:0}.vue-flow__panel.left{left:0}.vue-flow__panel.right{right:0}.vue-flow__panel.center{left:50%;transform:translate(-50%)}@keyframes dashdraw{0%{stroke-dashoffset:10px}}.min-vh-80{height:80vh}.end-20-px{right:20px!important}.top-25-px{top:25px!important}.z-10{z-index:10}.z-20{z-index:20}.custom-edge{background:var(--vf-edge-color)!important}.vue-flow__minimap{box-shadow:0 10px 15px -3px var(--vf-opacity-less)}.custom-controls{background:var(--vf-custom-controls-bgcolor);bottom:10px;right:10px}.vue-flow__minimap.pannable{cursor:grab}.stage-node .edge-handler{width:12px!important;height:12px!important}.stage-node:hover .vue-flow__handle{opacity:1}.vue-flow__node-stage{max-width:250px!important}.workflow-browser-actions-list{background-color:var(--vf-black);box-shadow:0 4px 10px var(--vf-opacity-more)}
|
||||
Binary file not shown.
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
|
||||
"name": "com_workflow",
|
||||
"version": "4.0.0",
|
||||
"description": "Joomla CMS",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"assets": [
|
||||
{
|
||||
"name": "com_workflow.admin-items-workflow-buttons",
|
||||
"type": "script",
|
||||
"uri": "com_workflow/admin-items-workflow-buttons.min.js",
|
||||
"dependencies": [
|
||||
"core"
|
||||
],
|
||||
"attributes": {
|
||||
"type": "module"
|
||||
},
|
||||
"version": "34dbe2"
|
||||
},
|
||||
{
|
||||
"name": "com_workflow.workflowgraph",
|
||||
"type": "style",
|
||||
"uri": "com_workflow/workflow-graph.min.css",
|
||||
"version": "6f090d"
|
||||
},
|
||||
{
|
||||
"name": "com_workflow.workflowgraphclient",
|
||||
"type": "style",
|
||||
"uri": "com_workflow/workflow-graph-client.min.css",
|
||||
"version": "47a3eb"
|
||||
},
|
||||
{
|
||||
"name": "com_workflow.workflowgraphclient",
|
||||
"type": "script",
|
||||
"uri": "com_workflow/workflow-graph-client.min.js",
|
||||
"dependencies": [
|
||||
"core"
|
||||
],
|
||||
"attributes": {
|
||||
"type": "module"
|
||||
},
|
||||
"version": "41dd8e"
|
||||
},
|
||||
{
|
||||
"name": "com_workflow.workflowgraph",
|
||||
"type": "script",
|
||||
"uri": "com_workflow/workflow-graph.min.js",
|
||||
"dependencies": [
|
||||
"core"
|
||||
],
|
||||
"attributes": {
|
||||
"type": "module"
|
||||
},
|
||||
"version": "08f7bd"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
Joomla = window.Joomla || {};
|
||||
|
||||
/**
|
||||
* Method that switches a given class to the following elements of the element provided
|
||||
*
|
||||
* @param {HTMLElement} element The reference element
|
||||
* @param {string} className The class name to be toggled
|
||||
*/
|
||||
Joomla.toggleAllNextElements = (element, className) => {
|
||||
const getNextSiblings = el => {
|
||||
const siblings = [];
|
||||
do {
|
||||
siblings.push(el);
|
||||
} while ((el = el.nextElementSibling) !== null);
|
||||
return siblings;
|
||||
};
|
||||
const followingElements = getNextSiblings(element);
|
||||
if (followingElements.length) {
|
||||
followingElements.forEach(elem => {
|
||||
if (elem.classList.contains(className)) {
|
||||
elem.classList.remove(className);
|
||||
} else {
|
||||
elem.classList.add(className);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
(() => {
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const dropDownBtn = document.getElementById('toolbar-status-group');
|
||||
if (!dropDownBtn) {
|
||||
return;
|
||||
}
|
||||
const headline = dropDownBtn.querySelector('.button-transition-headline');
|
||||
const separator = dropDownBtn.querySelector('.button-transition-separator');
|
||||
const itemList = document.querySelector('table.itemList');
|
||||
let itemListRows = [];
|
||||
let transitionIds = [];
|
||||
if (itemList) {
|
||||
itemListRows = itemList.querySelectorAll('tbody tr');
|
||||
}
|
||||
function enableTransitions() {
|
||||
if (transitionIds.length) {
|
||||
let availableTrans = transitionIds.shift();
|
||||
while (transitionIds.length) {
|
||||
const compareTrans = transitionIds.shift();
|
||||
availableTrans = availableTrans.filter(id => compareTrans.indexOf(id) !== -1);
|
||||
}
|
||||
if (availableTrans.length) {
|
||||
if (headline) {
|
||||
headline.classList.remove('d-none');
|
||||
}
|
||||
if (separator) {
|
||||
separator.classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
availableTrans.forEach(trans => {
|
||||
const elem = dropDownBtn.querySelector(`.transition-${trans}`);
|
||||
if (elem) {
|
||||
elem.parentNode.classList.remove('d-none');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// check for common attributes for which the conditions for a transition are possible or not
|
||||
// and save this information in a boolean variable.
|
||||
function collectTransitions(row) {
|
||||
transitionIds.push(row.getAttribute('data-transitions').split(','));
|
||||
}
|
||||
|
||||
// listen to click event to get selected rows
|
||||
if (itemList) {
|
||||
itemList.addEventListener('click', () => {
|
||||
dropDownBtn.querySelectorAll('.button-transition').forEach(trans => {
|
||||
trans.parentNode.classList.add('d-none');
|
||||
});
|
||||
if (headline) {
|
||||
headline.classList.add('d-none');
|
||||
}
|
||||
if (separator) {
|
||||
separator.classList.add('d-none');
|
||||
}
|
||||
transitionIds = [];
|
||||
itemListRows.forEach(el => {
|
||||
const checkedBox = el.querySelector('input[type=checkbox]');
|
||||
if (checkedBox.checked) {
|
||||
const parentTr = checkedBox.closest('tr');
|
||||
collectTransitions(parentTr);
|
||||
}
|
||||
});
|
||||
enableTransitions();
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org>
|
||||
* @license GNU General Public License version 2 or later; see LICENSE.txt
|
||||
*/Joomla=window.Joomla||{},Joomla.toggleAllNextElements=(s,o)=>{const i=(t=>{const n=[];do n.push(t);while((t=t.nextElementSibling)!==null);return n})(s);i.length&&i.forEach(t=>{t.classList.contains(o)?t.classList.remove(o):t.classList.add(o)})},document.addEventListener("DOMContentLoaded",()=>{const s=document.getElementById("toolbar-status-group");if(!s)return;const o=s.querySelector(".button-transition-headline"),a=s.querySelector(".button-transition-separator"),i=document.querySelector("table.itemList");let t=[],n=[];i&&(t=i.querySelectorAll("tbody tr"));function c(){if(n.length){let e=n.shift();for(;n.length;){const l=n.shift();e=e.filter(r=>l.indexOf(r)!==-1)}e.length&&(o&&o.classList.remove("d-none"),a&&a.classList.remove("d-none")),e.forEach(l=>{const r=s.querySelector(`.transition-${l}`);r&&r.parentNode.classList.remove("d-none")})}}function d(e){n.push(e.getAttribute("data-transitions").split(","))}i&&i.addEventListener("click",()=>{s.querySelectorAll(".button-transition").forEach(e=>{e.parentNode.classList.add("d-none")}),o&&o.classList.add("d-none"),a&&a.classList.add("d-none"),n=[],t.forEach(e=>{const l=e.querySelector("input[type=checkbox]");if(l.checked){const r=l.closest("tr");d(r)}}),c()})});
|
||||
Binary file not shown.
@@ -0,0 +1,608 @@
|
||||
function _extends() {
|
||||
return _extends = Object.assign ? Object.assign.bind() : function (n) {
|
||||
for (var e = 1; e < arguments.length; e++) {
|
||||
var t = arguments[e];
|
||||
for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]);
|
||||
}
|
||||
return n;
|
||||
}, _extends.apply(null, arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @copyright (C) 2026 Open Source Matters
|
||||
* @license GNU GPL v2 or later; see LICENSE.txt
|
||||
*/
|
||||
|
||||
Joomla = window.Joomla || {};
|
||||
(() => {
|
||||
// --- Constants ---
|
||||
const STAGE_WIDTH = 200;
|
||||
const STAGE_HEIGHT = 100;
|
||||
const MIN_ZOOM = 0.5;
|
||||
const MAX_ZOOM = 2;
|
||||
const ZOOM_SENSITIVITY = 0.1;
|
||||
const CORNER_RADIUS = 10;
|
||||
|
||||
// This central state object holds all data needed for rendering.
|
||||
const state = {
|
||||
workflow: null,
|
||||
stages: [],
|
||||
transitions: [],
|
||||
scale: 1,
|
||||
panX: 0,
|
||||
panY: 0,
|
||||
isDraggingStage: false,
|
||||
highlightedEdge: null
|
||||
};
|
||||
|
||||
// --- Translation ---
|
||||
const translate = string => {
|
||||
return Joomla.Text._(string);
|
||||
};
|
||||
const sprintf = (string, ...args) => {
|
||||
const base = Joomla.Text._(string, string);
|
||||
let i = 0;
|
||||
return base.replace(/%((%)|s|d)/g, m => {
|
||||
let val = args[i];
|
||||
if (m === '%d') {
|
||||
val = parseFloat(val);
|
||||
if (Number.isNaN(val)) val = 0;
|
||||
}
|
||||
i += 1;
|
||||
return val;
|
||||
});
|
||||
};
|
||||
|
||||
// Get full url for marker to avoid issues with base tags
|
||||
const getMarkerUrl = id => {
|
||||
const location = window.location.href.split('#')[0];
|
||||
return `url(${location}#${id})`;
|
||||
};
|
||||
function showMessageInModal(message, type) {
|
||||
const messages = {};
|
||||
messages[type] = [Joomla.Text._(message)];
|
||||
Joomla.renderMessages(messages);
|
||||
{
|
||||
const dialog = document.querySelector('joomla-dialog');
|
||||
if (dialog) {
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
async function makeRequest(url) {
|
||||
try {
|
||||
const paths = Joomla.getOptions('system.paths');
|
||||
const baseUri = `${paths ? `${paths.baseFull}index.php` : window.location.pathname}`;
|
||||
const uri = `${baseUri}?option=com_workflow&extension=com_content&layout=modal&view=graph${url}`;
|
||||
const response = await fetch(uri, {
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
if (!response.ok) {
|
||||
let message = 'COM_WORKFLOW_GRAPH_ERROR_UNKNOWN';
|
||||
if (response.status === 401) message = 'COM_WORKFLOW_GRAPH_ERROR_NOT_AUTHENTICATED';else if (response.status >= 403) message = 'COM_WORKFLOW_GRAPH_ERROR_NO_PERMISSION';else if (response.status != 200) message = sprintf('COM_WORKFLOW_GRAPH_ERROR_REQUEST_FAILED', response.status);
|
||||
throw new Error(message);
|
||||
}
|
||||
const responseData = await response.json();
|
||||
if (responseData.success === false) {
|
||||
throw new Error(responseData.message || 'COM_WORKFLOW_GRAPH_ERROR_API_RETURNED_ERROR');
|
||||
}
|
||||
return responseData;
|
||||
} catch (err) {
|
||||
showMessageInModal(err.message, "error");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Initial Layout Creation ---
|
||||
function calculateAutoLayout(stages) {
|
||||
const withNoPosition = stages.filter(stage => !stage.position || isNaN(stage.position.x) || isNaN(stage.position.y));
|
||||
if (withNoPosition.length === 0) return stages;
|
||||
const fromAnyStage = stages.find(s => s.id === 'from_any');
|
||||
const transitionStages = stages.filter(s => s.id !== 'from_any');
|
||||
const gapX = 400;
|
||||
const gapY = 300;
|
||||
const paddingX = 100;
|
||||
const paddingY = 100;
|
||||
const columns = Math.min(4, Math.ceil(Math.sqrt(transitionStages.length) + 1));
|
||||
transitionStages.forEach((stage, index) => {
|
||||
if (withNoPosition.some(s => s.id === stage.id)) {
|
||||
const col = index % columns;
|
||||
const row = Math.floor(index / columns);
|
||||
stage.position = {
|
||||
x: col * gapX + paddingX,
|
||||
y: row * gapY + paddingY
|
||||
};
|
||||
}
|
||||
});
|
||||
if (fromAnyStage && withNoPosition.some(s => s.id === fromAnyStage.id)) {
|
||||
if (!fromAnyStage.position || isNaN(fromAnyStage.position.x) || isNaN(fromAnyStage.position.y)) {
|
||||
fromAnyStage.position = {
|
||||
x: 600,
|
||||
y: -200
|
||||
};
|
||||
}
|
||||
}
|
||||
return stages;
|
||||
}
|
||||
function buildPathFromPoints(points) {
|
||||
let path = `M ${points[0].x} ${points[0].y - 50}`;
|
||||
for (let i = 1; i < points.length - 1; i++) {
|
||||
const prev = points[i - 1];
|
||||
const curr = points[i];
|
||||
const next = points[i + 1];
|
||||
const dx1 = prev.x - curr.x;
|
||||
const dy1 = prev.y - curr.y;
|
||||
const len1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
|
||||
const dx2 = next.x - curr.x;
|
||||
const dy2 = next.y - curr.y;
|
||||
const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
|
||||
if (len1 < 1 || len2 < 1) {
|
||||
path += ` L ${curr.x} ${curr.y}`;
|
||||
continue;
|
||||
}
|
||||
const r = Math.min(CORNER_RADIUS, len1 / 2, len2 / 2);
|
||||
const startX = curr.x + dx1 / len1 * r;
|
||||
const startY = curr.y + dy1 / len1 * r;
|
||||
const endX = curr.x + dx2 / len2 * r;
|
||||
const endY = curr.y + dy2 / len2 * r;
|
||||
path += ` L ${startX} ${startY}`;
|
||||
path += ` Q ${curr.x} ${curr.y} ${endX} ${endY}`;
|
||||
}
|
||||
path += ` L ${points[points.length - 1].x} ${points[points.length - 1].y}`;
|
||||
return path;
|
||||
}
|
||||
function getVerticalStepPath(sourceX, sourceY, targetX, targetY, midY) {
|
||||
const points = [{
|
||||
x: sourceX,
|
||||
y: sourceY
|
||||
}, {
|
||||
x: sourceX,
|
||||
y: midY
|
||||
}, {
|
||||
x: targetX,
|
||||
y: midY
|
||||
}, {
|
||||
x: targetX,
|
||||
y: targetY
|
||||
}];
|
||||
return [buildPathFromPoints(points), (sourceX + targetX) / 2, midY];
|
||||
}
|
||||
function getHorizontalStepPath(sourceX, sourceY, targetX, targetY, midX) {
|
||||
const startStubY = sourceY + 30;
|
||||
const endStubY = targetY - 30;
|
||||
const points = [{
|
||||
x: sourceX,
|
||||
y: sourceY
|
||||
}, {
|
||||
x: sourceX,
|
||||
y: startStubY
|
||||
}, {
|
||||
x: midX,
|
||||
y: startStubY
|
||||
}, {
|
||||
x: midX,
|
||||
y: endStubY
|
||||
}, {
|
||||
x: targetX,
|
||||
y: endStubY
|
||||
}, {
|
||||
x: targetX,
|
||||
y: targetY
|
||||
}];
|
||||
return [buildPathFromPoints(points), midX, (startStubY + endStubY) / 2];
|
||||
}
|
||||
function generateEdges(transitions, stages) {
|
||||
const stageMap = new Map(stages.map(s => [s.id, s]));
|
||||
const edgeGroups = {};
|
||||
|
||||
// Undirected Grouping to prevent overlaps on bi-directional edges
|
||||
transitions.forEach(tr => {
|
||||
const fromId = tr.from_stage_id === -1 ? 'from_any' : tr.from_stage_id;
|
||||
const toId = tr.to_stage_id;
|
||||
const s1 = String(fromId);
|
||||
const s2 = String(toId);
|
||||
const key = s1 < s2 ? `${s1}|${s2}` : `${s2}|${s1}`;
|
||||
if (!edgeGroups[key]) edgeGroups[key] = [];
|
||||
edgeGroups[key].push(tr);
|
||||
});
|
||||
Object.values(edgeGroups).forEach(group => group.sort((a, b) => a.id - b.id));
|
||||
return transitions.flatMap(tr => {
|
||||
const fromId = tr.from_stage_id === -1 ? 'from_any' : tr.from_stage_id;
|
||||
const toId = tr.to_stage_id;
|
||||
const fromStage = stageMap.get(fromId);
|
||||
const toStage = stageMap.get(toId);
|
||||
if (!(fromStage != null && fromStage.position) || !(toStage != null && toStage.position)) return [];
|
||||
const sourceX = fromStage.position.x + STAGE_WIDTH / 2;
|
||||
const sourceY = fromStage.position.y + STAGE_HEIGHT;
|
||||
const targetX = toStage.position.x + STAGE_WIDTH / 2;
|
||||
const targetY = toStage.position.y;
|
||||
const s1 = String(fromId);
|
||||
const s2 = String(toId);
|
||||
const groupKey = s1 < s2 ? `${s1}|${s2}` : `${s2}|${s1}`;
|
||||
const group = edgeGroups[groupKey] || [tr];
|
||||
const transitionIndex = group.findIndex(t => t.id === tr.id);
|
||||
let offsetIndex = transitionIndex - (group.length - 1) / 2;
|
||||
const bundleSpacing = 40;
|
||||
let pathData, labelX, labelY;
|
||||
|
||||
// Obstruction Check
|
||||
let isVerticalObstructed = false;
|
||||
const distX = Math.abs(sourceX - targetX);
|
||||
const isVerticallyAligned = distX < STAGE_WIDTH;
|
||||
if (isVerticallyAligned && targetY > sourceY) {
|
||||
isVerticalObstructed = stages.some(stage => {
|
||||
if (stage.id === fromId || stage.id === toId) return false;
|
||||
const sTop = stage.position.y;
|
||||
const sBottom = stage.position.y + STAGE_HEIGHT;
|
||||
const isBetweenY = sTop > sourceY && sBottom < targetY;
|
||||
const sLeft = stage.position.x;
|
||||
const sRight = stage.position.x + STAGE_WIDTH;
|
||||
const pathX = sourceX;
|
||||
const isBlockingX = pathX > sLeft - 20 && pathX < sRight + 20;
|
||||
return isBetweenY && isBlockingX;
|
||||
});
|
||||
}
|
||||
const isStacked = targetY > sourceY + 50 && !isVerticalObstructed && distX > 40;
|
||||
if (isStacked) {
|
||||
let midY = (sourceY + targetY) / 2;
|
||||
midY += offsetIndex * bundleSpacing;
|
||||
[pathData, labelX, labelY] = getVerticalStepPath(sourceX, sourceY, targetX, targetY, midY);
|
||||
labelX += offsetIndex * 60; // Stagger X
|
||||
} else {
|
||||
let midX = (sourceX + targetX) / 2;
|
||||
if (distX < STAGE_WIDTH || isVerticalObstructed) {
|
||||
midX = Math.max(sourceX, targetX) + STAGE_WIDTH / 2 + 60;
|
||||
}
|
||||
midX += offsetIndex * bundleSpacing;
|
||||
[pathData, labelX, labelY] = getHorizontalStepPath(sourceX, sourceY, targetX, targetY, midX);
|
||||
labelY += offsetIndex * 35; // Stagger Y
|
||||
}
|
||||
return {
|
||||
id: `transition-${tr.id}`,
|
||||
pathData,
|
||||
label: tr.title,
|
||||
labelPosition: {
|
||||
x: labelX,
|
||||
y: labelY
|
||||
},
|
||||
fromId,
|
||||
toId
|
||||
};
|
||||
}).filter(Boolean);
|
||||
}
|
||||
function renderGraph(modal) {
|
||||
const graph = modal.querySelector('#graph');
|
||||
const stageContainer = modal.querySelector('#stages');
|
||||
const svg = modal.querySelector('#connections');
|
||||
if (!graph || !stageContainer || !svg) return;
|
||||
|
||||
// Render Stages
|
||||
stageContainer.querySelectorAll('[id^="stage-"]').forEach(el => el.remove());
|
||||
state.stages.forEach(stage => {
|
||||
let stageEl = document.createElement('div');
|
||||
stageEl.id = `stage-${stage.id}`;
|
||||
stageEl.addEventListener('mousedown', e => {
|
||||
if (e.button === 0) handleNodeDrag(e, stage);
|
||||
});
|
||||
const isVirtual = stage.id === 'from_any';
|
||||
stageEl.className = `stage ${stage.default ? 'default' : ''} ${isVirtual ? 'virtual' : ''}`;
|
||||
stageEl.style.left = `${stage.position.x}px`;
|
||||
stageEl.style.top = `${stage.position.y}px`;
|
||||
let newHTML = isVirtual ? `<div class="stage-title text-truncate">${stage.title}</div>
|
||||
<div class="d-flex justify-content-between align-items-center mt-2"><div class="badge bg-info rounded-pill p-1"></div></div>` : `<div class="stage-title text-truncate" title="${stage.title}">${stage.title}</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
${stage.description ? `<div class="stage-description text-truncate small">${stage.description}</div>` : ''}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mt-2">
|
||||
${typeof stage.published !== 'undefined' ? `<div class="badge ${stage.published == '1' ? 'bg-success' : 'bg-danger'} rounded-pill p-1">${stage.published == '1' ? translate('COM_WORKFLOW_GRAPH_ENABLED') : translate('COM_WORKFLOW_GRAPH_DISABLED')}</div>` : ''}
|
||||
${stage.default ? `<div class="badge bg-warning rounded-pill p-1">${translate('COM_WORKFLOW_GRAPH_DEFAULT')}</div>` : ''}
|
||||
</div>`;
|
||||
stageEl.innerHTML = newHTML;
|
||||
stageContainer.appendChild(stageEl);
|
||||
});
|
||||
|
||||
// --- Setting up path SVG layers ---
|
||||
let pathsLayer = svg.querySelector('g.layers-paths');
|
||||
let labelsLayer = svg.querySelector('g.layers-labels');
|
||||
if (!pathsLayer) {
|
||||
pathsLayer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
pathsLayer.classList.add('layers-paths');
|
||||
svg.appendChild(pathsLayer);
|
||||
}
|
||||
if (!labelsLayer) {
|
||||
labelsLayer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||
labelsLayer.classList.add('layers-labels');
|
||||
svg.appendChild(labelsLayer);
|
||||
} else {
|
||||
svg.appendChild(labelsLayer); // Ensure it is last (top)
|
||||
}
|
||||
const edges = generateEdges(state.transitions, state.stages);
|
||||
|
||||
// Cleanup orphans
|
||||
pathsLayer.querySelectorAll('path[data-edge-id]').forEach(el => {
|
||||
if (!edges.find(e => e.id === el.dataset.edgeId)) el.remove();
|
||||
});
|
||||
labelsLayer.querySelectorAll('foreignObject[data-edge-id]').forEach(el => {
|
||||
if (!edges.find(e => e.id === el.dataset.edgeId)) el.remove();
|
||||
});
|
||||
edges.forEach(edge => {
|
||||
let path = pathsLayer.querySelector(`path[data-edge-id="${edge.id}"]`);
|
||||
if (!path) {
|
||||
path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
path.dataset.edgeId = edge.id;
|
||||
path.setAttribute('class', 'transition-path');
|
||||
pathsLayer.appendChild(path);
|
||||
}
|
||||
path.setAttribute('d', edge.pathData);
|
||||
path.classList.toggle('highlighted', state.highlightedEdge === edge.id);
|
||||
path.setAttribute('marker-end', getMarkerUrl('arrowhead'));
|
||||
let foreignObject = labelsLayer.querySelector(`foreignObject[data-edge-id="${edge.id}"]`);
|
||||
let labelDiv;
|
||||
if (!foreignObject) {
|
||||
foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
|
||||
foreignObject.dataset.edgeId = edge.id;
|
||||
foreignObject.style.overflow = 'visible';
|
||||
labelDiv = document.createElement('div');
|
||||
labelDiv.className = 'transition-label-content';
|
||||
labelDiv.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
state.highlightedEdge = state.highlightedEdge === edge.id ? null : edge.id;
|
||||
renderGraph(modal);
|
||||
});
|
||||
foreignObject.appendChild(labelDiv);
|
||||
labelsLayer.appendChild(foreignObject);
|
||||
} else {
|
||||
labelDiv = foreignObject.querySelector('div');
|
||||
}
|
||||
labelDiv.textContent = edge.label;
|
||||
labelDiv.classList.toggle('highlighted', state.highlightedEdge === edge.id);
|
||||
graph.style.transform = `translate(${state.panX}px, ${state.panY}px) scale(${state.scale})`;
|
||||
requestAnimationFrame(() => {
|
||||
labelDiv.style.width = 'max-content';
|
||||
const rect = labelDiv.getBoundingClientRect();
|
||||
if (rect.width === 0 && rect.height === 0) return;
|
||||
const measuredWidth = rect.width / state.scale;
|
||||
const measuredHeight = rect.height / state.scale || 24;
|
||||
foreignObject.setAttribute('width', measuredWidth + 4);
|
||||
foreignObject.setAttribute('height', measuredHeight + 4);
|
||||
foreignObject.setAttribute('x', edge.labelPosition.x - measuredWidth / 2);
|
||||
foreignObject.setAttribute('y', edge.labelPosition.y - measuredHeight / 2);
|
||||
});
|
||||
});
|
||||
|
||||
// Grid Background
|
||||
const workflowGraph = modal.querySelector('#workflow-graph');
|
||||
if (workflowGraph) {
|
||||
const dotSize = Math.max(0.3, Math.min(1, state.scale));
|
||||
const spacing = 20 * state.scale;
|
||||
workflowGraph.style.backgroundImage = `
|
||||
radial-gradient(circle,
|
||||
color-mix(in srgb, var(--body-color) 80%, transparent) 0px,
|
||||
color-mix(in srgb, var(--body-color) 60%, transparent) ${dotSize}px,
|
||||
transparent ${dotSize * 2}px
|
||||
)
|
||||
`;
|
||||
workflowGraph.style.backgroundSize = `${spacing}px ${spacing}px`;
|
||||
workflowGraph.style.backgroundPosition = `${state.panX}px ${state.panY}px`;
|
||||
}
|
||||
}
|
||||
function handleNodeDrag(startEvent, draggedStage) {
|
||||
if (draggedStage.id === 'from_any') return;
|
||||
const stageElement = document.getElementById(`stage-${draggedStage.id}`);
|
||||
state.isDraggingStage = true;
|
||||
const dragStart = {
|
||||
x: startEvent.clientX,
|
||||
y: startEvent.clientY,
|
||||
stageX: draggedStage.position.x,
|
||||
stageY: draggedStage.position.y
|
||||
};
|
||||
stageElement.classList.add('dragging');
|
||||
const onMouseMove = moveEvent => {
|
||||
const newX = dragStart.stageX + (moveEvent.clientX - dragStart.x) / state.scale;
|
||||
const newY = dragStart.stageY + (moveEvent.clientY - dragStart.y) / state.scale;
|
||||
const stageToUpdate = state.stages.find(s => s.id === draggedStage.id);
|
||||
if (stageToUpdate) {
|
||||
stageToUpdate.position.x = newX;
|
||||
stageToUpdate.position.y = newY;
|
||||
}
|
||||
renderGraph(document.querySelector('#workflow-graph'));
|
||||
};
|
||||
const onMouseUp = () => {
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
stageElement.classList.remove('dragging');
|
||||
state.isDraggingStage = false;
|
||||
};
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
}
|
||||
async function init(modal) {
|
||||
const container = modal.querySelector('#workflow-graph');
|
||||
const graph = modal.querySelector('#graph');
|
||||
if (!container || container.dataset.initialized) return;
|
||||
container.dataset.initialized = 'true';
|
||||
const workflowContainer = container.querySelector('#workflow-container');
|
||||
const workflowId = parseInt(workflowContainer.dataset.workflowId, 10);
|
||||
if (!workflowId) return showMessageInModal('COM_WORKFLOW_GRAPH_ERROR_INVALID_ID', 'error');
|
||||
|
||||
// Standard Arrowhead definition (points right, orient=auto handles the rest)
|
||||
modal.querySelector('#connections').innerHTML = `<defs>
|
||||
<marker id="arrowhead" viewBox="0 0 10 10" refX="10" refY="5"
|
||||
markerWidth="6" markerHeight="6" orient="auto">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2071c6" />
|
||||
</marker>
|
||||
</defs>`;
|
||||
try {
|
||||
const workflowData = await makeRequest(`&task=graph.getWorkflow&workflow_id=${workflowId}&format=json`);
|
||||
if (!workflowData) return;
|
||||
const stagesData = await makeRequest(`&task=graph.getStages&workflow_id=${workflowId}&format=json`);
|
||||
if (!stagesData) return;
|
||||
const transitionsData = await makeRequest(`&task=graph.getTransitions&workflow_id=${workflowId}&format=json`);
|
||||
if (!transitionsData) return;
|
||||
state.workflow = (workflowData == null ? void 0 : workflowData.data) || {};
|
||||
let stages = (stagesData == null ? void 0 : stagesData.data) || [];
|
||||
state.transitions = (transitionsData == null ? void 0 : transitionsData.data) || [];
|
||||
if (!stages.length) return showMessageInModal('COM_WORKFLOW_GRAPH_ERROR_STAGES_NOT_FOUND', 'error');
|
||||
if (state.transitions.some(tr => tr.from_stage_id === -1) && !stages.some(s => s.id === 'from_any')) {
|
||||
stages.unshift({
|
||||
id: 'from_any',
|
||||
title: translate('COM_WORKFLOW_GRAPH_FROM_ANY'),
|
||||
position: null
|
||||
});
|
||||
}
|
||||
state.stages = stages.map(s => _extends({}, s, {
|
||||
position: s.position || {
|
||||
x: NaN,
|
||||
y: NaN
|
||||
}
|
||||
}));
|
||||
state.stages = calculateAutoLayout(state.stages);
|
||||
|
||||
// Update UI Counts
|
||||
modal.querySelector('.joomla-dialog-header h3').textContent = state.workflow.title || translate('COM_WORKFLOW_GRAPH_WORKFLOW');
|
||||
const statusBadge = modal.querySelector('#workflow-status-badge');
|
||||
if (statusBadge) {
|
||||
statusBadge.textContent = state.workflow.published == '1' ? translate('COM_WORKFLOW_GRAPH_ENABLED') : translate('COM_WORKFLOW_GRAPH_DISABLED');
|
||||
statusBadge.classList.add(state.workflow.published == '1' ? 'bg-success' : 'bg-warning');
|
||||
}
|
||||
const realStagesCount = state.stages.filter(s => s.id !== 'from_any').length;
|
||||
const stageCount = modal.querySelector('#workflow-stage-count');
|
||||
if (stageCount) stageCount.textContent = `${realStagesCount} ${realStagesCount === 1 ? translate('COM_WORKFLOW_GRAPH_STAGE') : translate('COM_WORKFLOW_GRAPH_STAGES')}`;
|
||||
const transitionCount = modal.querySelector('#workflow-transition-count');
|
||||
if (transitionCount) transitionCount.textContent = `${state.transitions.length} ${state.transitions.length === 1 ? translate('COM_WORKFLOW_GRAPH_TRANSITION') : translate('COM_WORKFLOW_GRAPH_TRANSITIONS')}`;
|
||||
renderGraph(modal);
|
||||
setTimeout(() => fitToScreen(modal), 150);
|
||||
} catch (error) {
|
||||
showMessageInModal(error.message, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Zoom & Pan Logic
|
||||
let isPanning = false,
|
||||
panStart = {};
|
||||
container.addEventListener("mousedown", e => {
|
||||
if (e.target.closest('.stage') || e.target.closest('.zoom-controls') || e.button !== 0) return;
|
||||
isPanning = true;
|
||||
panStart = {
|
||||
x: e.clientX - state.panX,
|
||||
y: e.clientY - state.panY
|
||||
};
|
||||
if (graph) graph.classList.add('dragging');
|
||||
});
|
||||
document.addEventListener("mousemove", e => {
|
||||
if (!isPanning) return;
|
||||
state.panX = e.clientX - panStart.x;
|
||||
state.panY = e.clientY - panStart.y;
|
||||
renderGraph(modal);
|
||||
});
|
||||
const stopPanning = () => {
|
||||
isPanning = false;
|
||||
if (graph) graph.classList.remove('dragging');
|
||||
};
|
||||
document.addEventListener("mouseup", stopPanning);
|
||||
container.addEventListener("mouseleave", stopPanning);
|
||||
container.addEventListener("wheel", e => {
|
||||
e.preventDefault();
|
||||
const rect = container.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
const oldScale = state.scale;
|
||||
const zoomDirection = e.deltaY < 0 ? 1 : -1;
|
||||
state.scale = Math.max(MIN_ZOOM, Math.min(state.scale * (1 + zoomDirection * ZOOM_SENSITIVITY), MAX_ZOOM));
|
||||
const factor = state.scale / oldScale;
|
||||
state.panX = mouseX - (mouseX - state.panX) * factor;
|
||||
state.panY = mouseY - (mouseY - state.panY) * factor;
|
||||
renderGraph(modal);
|
||||
});
|
||||
const zoomControls = container.querySelector('.zoom-controls');
|
||||
zoomControls.querySelector('.zoom-in').addEventListener('click', () => applyZoom(1.2, modal));
|
||||
zoomControls.querySelector('.zoom-out').addEventListener('click', () => applyZoom(1 / 1.2, modal));
|
||||
zoomControls.querySelector('.fit-screen').addEventListener('click', () => fitToScreen(modal));
|
||||
function applyZoom(factor, modalContext) {
|
||||
const rect = modalContext.querySelector('#workflow-graph').getBoundingClientRect();
|
||||
const centerX = rect.width / 2;
|
||||
const centerY = rect.height / 2;
|
||||
const oldScale = state.scale;
|
||||
state.scale = Math.max(MIN_ZOOM, Math.min(state.scale * factor, MAX_ZOOM));
|
||||
const scaleRatio = state.scale / oldScale;
|
||||
state.panX = centerX - (centerX - state.panX) * scaleRatio;
|
||||
state.panY = centerY - (centerY - state.panY) * scaleRatio;
|
||||
renderGraph(modalContext);
|
||||
}
|
||||
function fitToScreen(modalContext) {
|
||||
if (!state.stages.length) return;
|
||||
let minX = Infinity,
|
||||
minY = Infinity,
|
||||
maxX = -Infinity,
|
||||
maxY = -Infinity;
|
||||
state.stages.forEach(s => {
|
||||
if (s.position) {
|
||||
minX = Math.min(minX, s.position.x);
|
||||
minY = Math.min(minY, s.position.y);
|
||||
maxX = Math.max(maxX, s.position.x + STAGE_WIDTH);
|
||||
maxY = Math.max(maxY, s.position.y + STAGE_HEIGHT);
|
||||
}
|
||||
});
|
||||
if (minX === Infinity) return;
|
||||
const containerRect = modalContext.querySelector('#workflow-graph').getBoundingClientRect();
|
||||
const padding = 50;
|
||||
const w = maxX - minX;
|
||||
const h = maxY - minY;
|
||||
state.scale = Math.max(MIN_ZOOM, Math.min((containerRect.width - padding) / w, (containerRect.height - padding) / h, MAX_ZOOM));
|
||||
state.panX = (containerRect.width - w * state.scale) / 2 - minX * state.scale;
|
||||
state.panY = (containerRect.height - h * state.scale) / 2 - minY * state.scale;
|
||||
renderGraph(modalContext);
|
||||
}
|
||||
|
||||
// --- Keyboard Shortcuts ---
|
||||
document.addEventListener('keydown', e => {
|
||||
if (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) return;
|
||||
const PAN_STEP = 30; // Pixels to move
|
||||
const ZOOM_STEP = 1.1; // Multiplier
|
||||
|
||||
switch (e.code) {
|
||||
/* ---------- Zoom ---------- */
|
||||
case 'Equal':
|
||||
// + / =
|
||||
applyZoom(ZOOM_STEP, modal);
|
||||
break;
|
||||
case 'Minus':
|
||||
// - / _
|
||||
applyZoom(1 / ZOOM_STEP, modal);
|
||||
break;
|
||||
|
||||
/* ---------- Panning ---------- */
|
||||
case 'ArrowLeft':
|
||||
case 'KeyA':
|
||||
state.panX += PAN_STEP;
|
||||
renderGraph(modal);
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
case 'KeyD':
|
||||
state.panX -= PAN_STEP;
|
||||
renderGraph(modal);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
case 'KeyW':
|
||||
state.panY += PAN_STEP;
|
||||
renderGraph(modal);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
case 'KeyS':
|
||||
state.panY -= PAN_STEP;
|
||||
renderGraph(modal);
|
||||
break;
|
||||
|
||||
/* ---------- Reset / Fit ---------- */
|
||||
case 'Digit0':
|
||||
case 'KeyF':
|
||||
fitToScreen(modal);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
document.addEventListener('joomla-dialog:open', event => {
|
||||
const dialog = event.target;
|
||||
if (dialog.querySelector('#workflow-container')) init(dialog);
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,23 @@
|
||||
function _extends(){return _extends=Object.assign?Object.assign.bind():function($){for(var M=1;M<arguments.length;M++){var A=arguments[M];for(var b in A)({}).hasOwnProperty.call(A,b)&&($[b]=A[b])}return $},_extends.apply(null,arguments)}/**
|
||||
* @copyright (C) 2026 Open Source Matters
|
||||
* @license GNU GPL v2 or later; see LICENSE.txt
|
||||
*/Joomla=window.Joomla||{},(()=>{const e={workflow:null,stages:[],transitions:[],scale:1,panX:0,panY:0,isDraggingStage:!1,highlightedEdge:null},E=t=>Joomla.Text._(t),H=(t,...a)=>{const l=Joomla.Text._(t,t);let d=0;return l.replace(/%((%)|s|d)/g,n=>{let r=a[d];return n==="%d"&&(r=parseFloat(r),Number.isNaN(r)&&(r=0)),d+=1,r})},G=t=>`url(${window.location.href.split("#")[0]}#${t})`;function v(t,a){const l={};l[a]=[Joomla.Text._(t)],Joomla.renderMessages(l);{const d=document.querySelector("joomla-dialog");d&&d.close()}}async function T(t){try{const a=Joomla.getOptions("system.paths"),d=`${`${a?`${a.baseFull}index.php`:window.location.pathname}`}?option=com_workflow&extension=com_content&layout=modal&view=graph${t}`,n=await fetch(d,{credentials:"same-origin"});if(!n.ok){let u="COM_WORKFLOW_GRAPH_ERROR_UNKNOWN";throw n.status===401?u="COM_WORKFLOW_GRAPH_ERROR_NOT_AUTHENTICATED":n.status>=403?u="COM_WORKFLOW_GRAPH_ERROR_NO_PERMISSION":n.status!=200&&(u=H("COM_WORKFLOW_GRAPH_ERROR_REQUEST_FAILED",n.status)),new Error(u)}const r=await n.json();if(r.success===!1)throw new Error(r.message||"COM_WORKFLOW_GRAPH_ERROR_API_RETURNED_ERROR");return r}catch(a){return v(a.message,"error"),!1}}function D(t){const a=t.filter(i=>!i.position||isNaN(i.position.x)||isNaN(i.position.y));if(a.length===0)return t;const l=t.find(i=>i.id==="from_any"),d=t.filter(i=>i.id!=="from_any"),n=400,r=300,u=100,p=100,o=Math.min(4,Math.ceil(Math.sqrt(d.length)+1));return d.forEach((i,g)=>{if(a.some(s=>s.id===i.id)){const s=g%o,c=Math.floor(g/o);i.position={x:s*n+u,y:c*r+p}}}),l&&a.some(i=>i.id===l.id)&&(!l.position||isNaN(l.position.x)||isNaN(l.position.y))&&(l.position={x:600,y:-200}),t}function W(t){let a=`M ${t[0].x} ${t[0].y-50}`;for(let l=1;l<t.length-1;l++){const d=t[l-1],n=t[l],r=t[l+1],u=d.x-n.x,p=d.y-n.y,o=Math.sqrt(u*u+p*p),i=r.x-n.x,g=r.y-n.y,s=Math.sqrt(i*i+g*g);if(o<1||s<1){a+=` L ${n.x} ${n.y}`;continue}const c=Math.min(10,o/2,s/2),f=n.x+u/o*c,h=n.y+p/o*c,_=n.x+i/s*c,y=n.y+g/s*c;a+=` L ${f} ${h}`,a+=` Q ${n.x} ${n.y} ${_} ${y}`}return a+=` L ${t[t.length-1].x} ${t[t.length-1].y}`,a}function P(t,a,l,d,n){return[W([{x:t,y:a},{x:t,y:n},{x:l,y:n},{x:l,y:d}]),(t+l)/2,n]}function Y(t,a,l,d,n){const r=a+30,u=d-30;return[W([{x:t,y:a},{x:t,y:r},{x:n,y:r},{x:n,y:u},{x:l,y:u},{x:l,y:d}]),n,(r+u)/2]}function q(t,a){const l=new Map(a.map(n=>[n.id,n])),d={};return t.forEach(n=>{const r=n.from_stage_id===-1?"from_any":n.from_stage_id,u=n.to_stage_id,p=String(r),o=String(u),i=p<o?`${p}|${o}`:`${o}|${p}`;d[i]||(d[i]=[]),d[i].push(n)}),Object.values(d).forEach(n=>n.sort((r,u)=>r.id-u.id)),t.flatMap(n=>{const r=n.from_stage_id===-1?"from_any":n.from_stage_id,u=n.to_stage_id,p=l.get(r),o=l.get(u);if(!(p!=null&&p.position)||!(o!=null&&o.position))return[];const i=p.position.x+200/2,g=p.position.y+100,s=o.position.x+200/2,c=o.position.y,f=String(r),h=String(u),_=f<h?`${f}|${h}`:`${h}|${f}`,y=d[_]||[n];let x=y.findIndex(m=>m.id===n.id)-(y.length-1)/2;const w=40;let R,L,I,k=!1;const N=Math.abs(i-s);if(N<200&&c>g&&(k=a.some(m=>{if(m.id===r||m.id===u)return!1;const K=m.position.y,j=m.position.y+100,B=K>g&&j<c,U=m.position.x,z=m.position.x+200,C=i,Z=C>U-20&&C<z+20;return B&&Z})),c>g+50&&!k&&N>40){let m=(g+c)/2;m+=x*w,[R,L,I]=P(i,g,s,c,m),L+=x*60}else{let m=(i+s)/2;(N<200||k)&&(m=Math.max(i,s)+200/2+60),m+=x*w,[R,L,I]=Y(i,g,s,c,m),I+=x*35}return{id:`transition-${n.id}`,pathData:R,label:n.title,labelPosition:{x:L,y:I},fromId:r,toId:u}}).filter(Boolean)}function S(t){const a=t.querySelector("#graph"),l=t.querySelector("#stages"),d=t.querySelector("#connections");if(!a||!l||!d)return;l.querySelectorAll('[id^="stage-"]').forEach(o=>o.remove()),e.stages.forEach(o=>{let i=document.createElement("div");i.id=`stage-${o.id}`,i.addEventListener("mousedown",c=>{c.button===0&&X(c,o)});const g=o.id==="from_any";i.className=`stage ${o.default?"default":""} ${g?"virtual":""}`,i.style.left=`${o.position.x}px`,i.style.top=`${o.position.y}px`;let s=g?`<div class="stage-title text-truncate">${o.title}</div>
|
||||
<div class="d-flex justify-content-between align-items-center mt-2"><div class="badge bg-info rounded-pill p-1"></div></div>`:`<div class="stage-title text-truncate" title="${o.title}">${o.title}</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
${o.description?`<div class="stage-description text-truncate small">${o.description}</div>`:""}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mt-2">
|
||||
${typeof o.published<"u"?`<div class="badge ${o.published=="1"?"bg-success":"bg-danger"} rounded-pill p-1">${o.published=="1"?E("COM_WORKFLOW_GRAPH_ENABLED"):E("COM_WORKFLOW_GRAPH_DISABLED")}</div>`:""}
|
||||
${o.default?`<div class="badge bg-warning rounded-pill p-1">${E("COM_WORKFLOW_GRAPH_DEFAULT")}</div>`:""}
|
||||
</div>`;i.innerHTML=s,l.appendChild(i)});let n=d.querySelector("g.layers-paths"),r=d.querySelector("g.layers-labels");n||(n=document.createElementNS("http://www.w3.org/2000/svg","g"),n.classList.add("layers-paths"),d.appendChild(n)),r||(r=document.createElementNS("http://www.w3.org/2000/svg","g"),r.classList.add("layers-labels")),d.appendChild(r);const u=q(e.transitions,e.stages);n.querySelectorAll("path[data-edge-id]").forEach(o=>{u.find(i=>i.id===o.dataset.edgeId)||o.remove()}),r.querySelectorAll("foreignObject[data-edge-id]").forEach(o=>{u.find(i=>i.id===o.dataset.edgeId)||o.remove()}),u.forEach(o=>{let i=n.querySelector(`path[data-edge-id="${o.id}"]`);i||(i=document.createElementNS("http://www.w3.org/2000/svg","path"),i.dataset.edgeId=o.id,i.setAttribute("class","transition-path"),n.appendChild(i)),i.setAttribute("d",o.pathData),i.classList.toggle("highlighted",e.highlightedEdge===o.id),i.setAttribute("marker-end",G("arrowhead"));let g=r.querySelector(`foreignObject[data-edge-id="${o.id}"]`),s;g?s=g.querySelector("div"):(g=document.createElementNS("http://www.w3.org/2000/svg","foreignObject"),g.dataset.edgeId=o.id,g.style.overflow="visible",s=document.createElement("div"),s.className="transition-label-content",s.addEventListener("click",c=>{c.stopPropagation(),e.highlightedEdge=e.highlightedEdge===o.id?null:o.id,S(t)}),g.appendChild(s),r.appendChild(g)),s.textContent=o.label,s.classList.toggle("highlighted",e.highlightedEdge===o.id),a.style.transform=`translate(${e.panX}px, ${e.panY}px) scale(${e.scale})`,requestAnimationFrame(()=>{s.style.width="max-content";const c=s.getBoundingClientRect();if(c.width===0&&c.height===0)return;const f=c.width/e.scale,h=c.height/e.scale||24;g.setAttribute("width",f+4),g.setAttribute("height",h+4),g.setAttribute("x",o.labelPosition.x-f/2),g.setAttribute("y",o.labelPosition.y-h/2)})});const p=t.querySelector("#workflow-graph");if(p){const o=Math.max(.3,Math.min(1,e.scale)),i=20*e.scale;p.style.backgroundImage=`
|
||||
radial-gradient(circle,
|
||||
color-mix(in srgb, var(--body-color) 80%, transparent) 0px,
|
||||
color-mix(in srgb, var(--body-color) 60%, transparent) ${o}px,
|
||||
transparent ${o*2}px
|
||||
)
|
||||
`,p.style.backgroundSize=`${i}px ${i}px`,p.style.backgroundPosition=`${e.panX}px ${e.panY}px`}}function X(t,a){if(a.id==="from_any")return;const l=document.getElementById(`stage-${a.id}`);e.isDraggingStage=!0;const d={x:t.clientX,y:t.clientY,stageX:a.position.x,stageY:a.position.y};l.classList.add("dragging");const n=u=>{const p=d.stageX+(u.clientX-d.x)/e.scale,o=d.stageY+(u.clientY-d.y)/e.scale,i=e.stages.find(g=>g.id===a.id);i&&(i.position.x=p,i.position.y=o),S(document.querySelector("#workflow-graph"))},r=()=>{document.removeEventListener("mousemove",n),document.removeEventListener("mouseup",r),l.classList.remove("dragging"),e.isDraggingStage=!1};document.addEventListener("mousemove",n),document.addEventListener("mouseup",r)}async function F(t){const a=t.querySelector("#workflow-graph"),l=t.querySelector("#graph");if(!a||a.dataset.initialized)return;a.dataset.initialized="true";const d=a.querySelector("#workflow-container"),n=parseInt(d.dataset.workflowId,10);if(!n)return v("COM_WORKFLOW_GRAPH_ERROR_INVALID_ID","error");t.querySelector("#connections").innerHTML=`<defs>
|
||||
<marker id="arrowhead" viewBox="0 0 10 10" refX="10" refY="5"
|
||||
markerWidth="6" markerHeight="6" orient="auto">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#2071c6" />
|
||||
</marker>
|
||||
</defs>`;try{const s=await T(`&task=graph.getWorkflow&workflow_id=${n}&format=json`);if(!s)return;const c=await T(`&task=graph.getStages&workflow_id=${n}&format=json`);if(!c)return;const f=await T(`&task=graph.getTransitions&workflow_id=${n}&format=json`);if(!f)return;e.workflow=s?.data||{};let h=c?.data||[];if(e.transitions=f?.data||[],!h.length)return v("COM_WORKFLOW_GRAPH_ERROR_STAGES_NOT_FOUND","error");e.transitions.some(w=>w.from_stage_id===-1)&&!h.some(w=>w.id==="from_any")&&h.unshift({id:"from_any",title:E("COM_WORKFLOW_GRAPH_FROM_ANY"),position:null}),e.stages=h.map(w=>_extends({},w,{position:w.position||{x:NaN,y:NaN}})),e.stages=D(e.stages),t.querySelector(".joomla-dialog-header h3").textContent=e.workflow.title||E("COM_WORKFLOW_GRAPH_WORKFLOW");const _=t.querySelector("#workflow-status-badge");_&&(_.textContent=e.workflow.published=="1"?E("COM_WORKFLOW_GRAPH_ENABLED"):E("COM_WORKFLOW_GRAPH_DISABLED"),_.classList.add(e.workflow.published=="1"?"bg-success":"bg-warning"));const y=e.stages.filter(w=>w.id!=="from_any").length,O=t.querySelector("#workflow-stage-count");O&&(O.textContent=`${y} ${E(y===1?"COM_WORKFLOW_GRAPH_STAGE":"COM_WORKFLOW_GRAPH_STAGES")}`);const x=t.querySelector("#workflow-transition-count");x&&(x.textContent=`${e.transitions.length} ${e.transitions.length===1?E("COM_WORKFLOW_GRAPH_TRANSITION"):E("COM_WORKFLOW_GRAPH_TRANSITIONS")}`),S(t),setTimeout(()=>g(t),150)}catch(s){v(s.message,"error");return}let r=!1,u={};a.addEventListener("mousedown",s=>{s.target.closest(".stage")||s.target.closest(".zoom-controls")||s.button!==0||(r=!0,u={x:s.clientX-e.panX,y:s.clientY-e.panY},l&&l.classList.add("dragging"))}),document.addEventListener("mousemove",s=>{r&&(e.panX=s.clientX-u.x,e.panY=s.clientY-u.y,S(t))});const p=()=>{r=!1,l&&l.classList.remove("dragging")};document.addEventListener("mouseup",p),a.addEventListener("mouseleave",p),a.addEventListener("wheel",s=>{s.preventDefault();const c=a.getBoundingClientRect(),f=s.clientX-c.left,h=s.clientY-c.top,_=e.scale,y=s.deltaY<0?1:-1;e.scale=Math.max(.5,Math.min(e.scale*(1+y*.1),2));const O=e.scale/_;e.panX=f-(f-e.panX)*O,e.panY=h-(h-e.panY)*O,S(t)});const o=a.querySelector(".zoom-controls");o.querySelector(".zoom-in").addEventListener("click",()=>i(1.2,t)),o.querySelector(".zoom-out").addEventListener("click",()=>i(1/1.2,t)),o.querySelector(".fit-screen").addEventListener("click",()=>g(t));function i(s,c){const f=c.querySelector("#workflow-graph").getBoundingClientRect(),h=f.width/2,_=f.height/2,y=e.scale;e.scale=Math.max(.5,Math.min(e.scale*s,2));const O=e.scale/y;e.panX=h-(h-e.panX)*O,e.panY=_-(_-e.panY)*O,S(c)}function g(s){if(!e.stages.length)return;let c=1/0,f=1/0,h=-1/0,_=-1/0;if(e.stages.forEach(R=>{R.position&&(c=Math.min(c,R.position.x),f=Math.min(f,R.position.y),h=Math.max(h,R.position.x+200),_=Math.max(_,R.position.y+100))}),c===1/0)return;const y=s.querySelector("#workflow-graph").getBoundingClientRect(),O=50,x=h-c,w=_-f;e.scale=Math.max(.5,Math.min((y.width-O)/x,(y.height-O)/w,2)),e.panX=(y.width-x*e.scale)/2-c*e.scale,e.panY=(y.height-w*e.scale)/2-f*e.scale,S(s)}document.addEventListener("keydown",s=>{if(["INPUT","TEXTAREA"].includes(document.activeElement.tagName))return;const c=30,f=1.1;switch(s.code){case"Equal":i(f,t);break;case"Minus":i(1/f,t);break;case"ArrowLeft":case"KeyA":e.panX+=c,S(t);break;case"ArrowRight":case"KeyD":e.panX-=c,S(t);break;case"ArrowUp":case"KeyW":e.panY+=c,S(t);break;case"ArrowDown":case"KeyS":e.panY-=c,S(t);break;case"Digit0":case"KeyF":g(t);break}})}document.addEventListener("joomla-dialog:open",t=>{const a=t.target;a.querySelector("#workflow-container")&&F(a)})})();
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1,242 @@
|
||||
/* ================================
|
||||
Workflow Graph Client Styles
|
||||
================================ */
|
||||
/* Color Variables */
|
||||
:root {
|
||||
--wf-bg: var(--bg-primary);
|
||||
--wf-bg: var(--bg-primary);
|
||||
--wf-border: var(--border-color);
|
||||
--wf-border-hover: var(--border-color-translucent);
|
||||
--wf-dot-color: var(--body-color);
|
||||
--stage-bg: rgb(var(--primary-rgb));
|
||||
--stage-border: var(--border-color);
|
||||
--stage-text: var(--white);
|
||||
--stage-desc: #ffffffc0 !important;
|
||||
--stage-virtual-bg: #800080;
|
||||
--transition-hover: var(--border-color-hover);
|
||||
--transition-label-bg: #2071c6;
|
||||
--transition-stroke: var(--transition-label-bg);
|
||||
--transition-label-color: var(--white);
|
||||
--transition-highlight: #ff6868;
|
||||
--zoom-btn-bg: transparent;
|
||||
--zoom-btn-hover: var(--border-color);
|
||||
--zoom-btn-color: var(--text-primary);
|
||||
--vf-custom-controls-bgcolor: var(--secondary);
|
||||
}
|
||||
|
||||
@keyframes dashmove {
|
||||
from {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
to {
|
||||
stroke-dashoffset: -36;
|
||||
}
|
||||
}
|
||||
|
||||
#workflow-graph {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 80vh;
|
||||
overflow: hidden;
|
||||
cursor: grab;
|
||||
background-image: radial-gradient(circle at 1px 1px, var(--wf-dot-color) 1px, transparent 1px);
|
||||
background-size: 38px 38px;
|
||||
border: 1px solid var(--stage-border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#workflow-graph:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
#workflow-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#graph {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
min-width: 100%;
|
||||
min-height: 100%;
|
||||
transition: transform .15s ease-out;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
#graph.dragging {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
#stages {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#connections {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
Stage Styling
|
||||
================================ */
|
||||
.stage {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 200px;
|
||||
min-height: 80px;
|
||||
padding: 12px 16px;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
background: var(--stage-bg);
|
||||
border: 1px solid var(--stage-border);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, .6);
|
||||
}
|
||||
|
||||
.stage:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.stage.dragging {
|
||||
z-index: 1000;
|
||||
transform: rotate(1deg) scale(1.02);
|
||||
}
|
||||
|
||||
.stage.virtual {
|
||||
background: var(--stage-virtual-bg);
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.stage-title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
color: var(--stage-text);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.stage-description {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--stage-desc);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.stage-badge {
|
||||
display: inline-block;
|
||||
align-self: flex-start;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: var(--stage-text);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
background: var(--stage-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* ================================
|
||||
Transition Styling
|
||||
================================ */
|
||||
.transition-path {
|
||||
stroke: var(--transition-stroke);
|
||||
stroke-width: 3px;
|
||||
fill: none;
|
||||
opacity: .8;
|
||||
transition: stroke .2s, stroke-width .2s;
|
||||
}
|
||||
|
||||
.transition-path:hover {
|
||||
stroke-width: 3px;
|
||||
opacity: 1;
|
||||
stroke: var(--transition-hover);
|
||||
animation-duration: .5s;
|
||||
}
|
||||
|
||||
.arrow-marker {
|
||||
fill: var(--transition-stroke);
|
||||
transition: fill .2s ease;
|
||||
}
|
||||
|
||||
.transition-path:hover+.arrow-marker {
|
||||
fill: var(--transition-hover);
|
||||
}
|
||||
|
||||
.transition-path.highlighted {
|
||||
stroke: var(--transition-highlight);
|
||||
stroke-width: 5px;
|
||||
stroke-dasharray: 12 6;
|
||||
animation: dashmove .5s linear infinite;
|
||||
}
|
||||
|
||||
.transition-label-content {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
min-width: 80px;
|
||||
max-width: 300px;
|
||||
padding: 2px 12px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
color: var(--transition-label-color);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: var(--transition-label-bg) !important;
|
||||
border: 2px solid var(--transition-stroke);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
|
||||
transition: background .2s, border .2s, box-shadow .2s;
|
||||
}
|
||||
|
||||
.transition-label-content.highlighted {
|
||||
font-weight: 700;
|
||||
border-color: var(--transition-highlight);
|
||||
transition: transform .2s;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* ================================
|
||||
Zoom Controls
|
||||
================================ */
|
||||
.zoom-controls {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 4px;
|
||||
background: var(--vf-custom-controls-bgcolor);
|
||||
border: 1px solid var(--wf-border);
|
||||
border-radius: 6px;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.zoom-btn:hover {
|
||||
background: var(--zoom-btn-hover);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.zoom-btn:active {
|
||||
transition: transform .5s ease;
|
||||
transform: scale(.95);
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
/* ================================
|
||||
Workflow Graph Styles
|
||||
================================ */
|
||||
|
||||
/* Color Variables */
|
||||
:root {
|
||||
--vf-black: #000;
|
||||
--vf-custom-controls-bgcolor: var(--secondary);
|
||||
--vf-edge-color: #2071c6;
|
||||
--vf-white: #fff;
|
||||
--vf-opacity-less: rgba(0, 0, 0, .1);
|
||||
--vf-opacity-more: rgba(0, 0, 0, .6);
|
||||
}
|
||||
|
||||
/* ================================
|
||||
Vue Flow Core Components
|
||||
================================ */
|
||||
.vue-flow {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
[dir="rtl"] .vue-flow__node, [dir="rtl"] .edge-label, [dir="rtl"] .vue-flow__panel {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.vue-flow__container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.vue-flow__pane.draggable {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.vue-flow__pane.selection {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vue-flow__pane.dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.vue-flow__transformationpane {
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.vue-flow__viewport {
|
||||
z-index: 4;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.vue-flow__nodesselection-rect:focus,
|
||||
.vue-flow__nodesselection-rect:focus-visible,
|
||||
.vue-flow__edge.selected,
|
||||
.vue-flow__edge:focus,
|
||||
.vue-flow__edge:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.vue-flow .vue-flow__edges {
|
||||
overflow: visible;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vue-flow__edge-path,
|
||||
.vue-flow__connection-path {
|
||||
stroke: var(--success);
|
||||
stroke-width: 3;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.vue-flow__edge.animated path {
|
||||
stroke-dasharray: 5;
|
||||
animation: dashdraw .5s linear infinite;
|
||||
}
|
||||
|
||||
.vue-flow__edge.animated path.vue-flow__edge-interaction {
|
||||
stroke-dasharray: none;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.vue-flow__edge.inactive {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vue-flow__connection {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.vue-flow__connection .animated {
|
||||
stroke-dasharray: 5;
|
||||
animation: dashdraw .5s linear infinite;
|
||||
}
|
||||
|
||||
.vue-flow__connectionline {
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.vue-flow__nodes {
|
||||
pointer-events: none;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
.vue-flow__node {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
pointer-events: all;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
transform-origin: 0 0;
|
||||
}
|
||||
|
||||
|
||||
.vue-flow__handle.connectable {
|
||||
pointer-events: all;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.vue-flow__panel {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
.vue-flow__panel.top {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.vue-flow__panel.bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.vue-flow__panel.left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.vue-flow__panel.right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.vue-flow__panel.center {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
@keyframes dashdraw {
|
||||
from {
|
||||
stroke-dashoffset: 10;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================
|
||||
Vue Flow Custom Components
|
||||
================================ */
|
||||
.min-vh-80 {
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
.end-20-px {
|
||||
right: 20px !important;
|
||||
}
|
||||
|
||||
.top-25-px {
|
||||
top: 25px !important;
|
||||
}
|
||||
|
||||
.z-10 {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.z-20 {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.custom-edge {
|
||||
background: var(--vf-edge-color) !important;
|
||||
}
|
||||
|
||||
.vue-flow__minimap {
|
||||
box-shadow: 0 10px 15px -3px var(--vf-opacity-less);
|
||||
}
|
||||
|
||||
.custom-controls {
|
||||
right: 10px;
|
||||
bottom: 10px;
|
||||
background: var(--vf-custom-controls-bgcolor);
|
||||
}
|
||||
|
||||
.vue-flow__minimap.pannable {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.stage-node .edge-handler {
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
}
|
||||
|
||||
.stage-node:hover .vue-flow__handle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.vue-flow__node-stage {
|
||||
max-width: 250px !important;
|
||||
}
|
||||
|
||||
.workflow-browser-actions-list {
|
||||
background-color: var(--vf-black);
|
||||
box-shadow: 0 4px 10px var(--vf-opacity-more);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user