@bpmn-io/properties-panel 3.10.0 → 3.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/properties-panel.css +4 -0
- package/dist/index.esm.js +4259 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.mjs +810 -390
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -5
|
@@ -0,0 +1,4259 @@
|
|
|
1
|
+
import { useContext, useState, useRef, useEffect, useMemo, useCallback, useLayoutEffect } from '../preact/hooks';
|
|
2
|
+
import { isFunction, isString, isArray, get, assign, set, sortBy, find, isNumber, debounce } from 'min-dash';
|
|
3
|
+
import { createPortal, forwardRef } from '../preact/compat';
|
|
4
|
+
import { jsx, jsxs, Fragment } from '../preact/jsx-runtime';
|
|
5
|
+
import { createContext, createElement } from '../preact';
|
|
6
|
+
import classnames from 'classnames';
|
|
7
|
+
import { query, domify } from 'min-dom';
|
|
8
|
+
import { FeelersEditor } from 'feelers';
|
|
9
|
+
import FeelEditor from '@bpmn-io/feel-editor';
|
|
10
|
+
import { lineNumbers } from '@codemirror/view';
|
|
11
|
+
import * as focusTrap from 'focus-trap';
|
|
12
|
+
|
|
13
|
+
var ArrowIcon = function ArrowIcon(props) {
|
|
14
|
+
return jsx("svg", {
|
|
15
|
+
...props,
|
|
16
|
+
children: jsx("path", {
|
|
17
|
+
fillRule: "evenodd",
|
|
18
|
+
d: "m11.657 8-4.95 4.95a1 1 0 0 1-1.414-1.414L8.828 8 5.293 4.464A1 1 0 1 1 6.707 3.05L11.657 8Z"
|
|
19
|
+
})
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
ArrowIcon.defaultProps = {
|
|
23
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
24
|
+
width: "16",
|
|
25
|
+
height: "16"
|
|
26
|
+
};
|
|
27
|
+
var CreateIcon = function CreateIcon(props) {
|
|
28
|
+
return jsx("svg", {
|
|
29
|
+
...props,
|
|
30
|
+
children: jsx("path", {
|
|
31
|
+
fillRule: "evenodd",
|
|
32
|
+
d: "M9 13V9h4a1 1 0 0 0 0-2H9V3a1 1 0 1 0-2 0v4H3a1 1 0 1 0 0 2h4v4a1 1 0 0 0 2 0Z"
|
|
33
|
+
})
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
CreateIcon.defaultProps = {
|
|
37
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
38
|
+
width: "16",
|
|
39
|
+
height: "16"
|
|
40
|
+
};
|
|
41
|
+
var DeleteIcon = function DeleteIcon(props) {
|
|
42
|
+
return jsx("svg", {
|
|
43
|
+
...props,
|
|
44
|
+
children: jsx("path", {
|
|
45
|
+
fillRule: "evenodd",
|
|
46
|
+
d: "M12 6v7c0 1.1-.4 1.55-1.5 1.55h-5C4.4 14.55 4 14.1 4 13V6h8Zm-1.5 1.5h-5v4.3c0 .66.5 1.2 1.111 1.2H9.39c.611 0 1.111-.54 1.111-1.2V7.5ZM13 3h-2l-1-1H6L5 3H3v1.5h10V3Z"
|
|
47
|
+
})
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
DeleteIcon.defaultProps = {
|
|
51
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
52
|
+
width: "16",
|
|
53
|
+
height: "16"
|
|
54
|
+
};
|
|
55
|
+
var DragIcon = function DragIcon(props) {
|
|
56
|
+
return jsxs("svg", {
|
|
57
|
+
...props,
|
|
58
|
+
children: [jsx("path", {
|
|
59
|
+
fill: "#fff",
|
|
60
|
+
style: {
|
|
61
|
+
mixBlendMode: "multiply"
|
|
62
|
+
},
|
|
63
|
+
d: "M0 0h16v16H0z"
|
|
64
|
+
}), jsx("path", {
|
|
65
|
+
fill: "#fff",
|
|
66
|
+
style: {
|
|
67
|
+
mixBlendMode: "multiply"
|
|
68
|
+
},
|
|
69
|
+
d: "M0 0h16v16H0z"
|
|
70
|
+
}), jsx("path", {
|
|
71
|
+
d: "M7 3H5v2h2V3zm4 0H9v2h2V3zM7 7H5v2h2V7zm4 0H9v2h2V7zm-4 4H5v2h2v-2zm4 0H9v2h2v-2z",
|
|
72
|
+
fill: "#161616"
|
|
73
|
+
})]
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
DragIcon.defaultProps = {
|
|
77
|
+
width: "16",
|
|
78
|
+
height: "16",
|
|
79
|
+
fill: "none",
|
|
80
|
+
xmlns: "http://www.w3.org/2000/svg"
|
|
81
|
+
};
|
|
82
|
+
var ExternalLinkIcon = function ExternalLinkIcon(props) {
|
|
83
|
+
return jsx("svg", {
|
|
84
|
+
...props,
|
|
85
|
+
children: jsx("path", {
|
|
86
|
+
fillRule: "evenodd",
|
|
87
|
+
clipRule: "evenodd",
|
|
88
|
+
d: "M12.637 12.637v-4.72h1.362v4.721c0 .36-.137.676-.411.95-.275.275-.591.412-.95.412H3.362c-.38 0-.703-.132-.967-.396A1.315 1.315 0 0 1 2 12.638V3.362c0-.38.132-.703.396-.967S2.982 2 3.363 2h4.553v1.363H3.363v9.274h9.274ZM14 2H9.28l-.001 1.362h2.408L5.065 9.984l.95.95 6.622-6.622v2.409H14V2Z",
|
|
89
|
+
fill: "currentcolor"
|
|
90
|
+
})
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
ExternalLinkIcon.defaultProps = {
|
|
94
|
+
width: "16",
|
|
95
|
+
height: "16",
|
|
96
|
+
fill: "none",
|
|
97
|
+
xmlns: "http://www.w3.org/2000/svg"
|
|
98
|
+
};
|
|
99
|
+
var FeelIcon$1 = function FeelIcon(props) {
|
|
100
|
+
return jsx("svg", {
|
|
101
|
+
...props,
|
|
102
|
+
children: jsx("path", {
|
|
103
|
+
d: "M3.617 11.99c-.137.684-.392 1.19-.765 1.518-.362.328-.882.492-1.558.492H0l.309-1.579h1.264l1.515-7.64h-.912l.309-1.579h.911l.236-1.191c.137-.685.387-1.192.75-1.52C4.753.164 5.277 0 5.953 0h1.294L6.94 1.579H5.675l-.323 1.623h1.264l-.309 1.579H5.043l-1.426 7.208ZM5.605 11.021l3.029-4.155L7.28 3.202h2.073l.706 2.547h.176l1.691-2.547H14l-3.014 4.051 1.338 3.768H10.25l-.706-2.606H9.37L7.678 11.02H5.605Z",
|
|
104
|
+
fill: "currentcolor"
|
|
105
|
+
})
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
FeelIcon$1.defaultProps = {
|
|
109
|
+
width: "14",
|
|
110
|
+
height: "14",
|
|
111
|
+
fill: "none",
|
|
112
|
+
xmlns: "http://www.w3.org/2000/svg"
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function Header(props) {
|
|
116
|
+
const {
|
|
117
|
+
element,
|
|
118
|
+
headerProvider
|
|
119
|
+
} = props;
|
|
120
|
+
const {
|
|
121
|
+
getElementIcon,
|
|
122
|
+
getDocumentationRef,
|
|
123
|
+
getElementLabel,
|
|
124
|
+
getTypeLabel
|
|
125
|
+
} = headerProvider;
|
|
126
|
+
const label = getElementLabel(element);
|
|
127
|
+
const type = getTypeLabel(element);
|
|
128
|
+
const documentationRef = getDocumentationRef && getDocumentationRef(element);
|
|
129
|
+
const ElementIcon = getElementIcon(element);
|
|
130
|
+
return jsxs("div", {
|
|
131
|
+
class: "bio-properties-panel-header",
|
|
132
|
+
children: [jsx("div", {
|
|
133
|
+
class: "bio-properties-panel-header-icon",
|
|
134
|
+
children: ElementIcon && jsx(ElementIcon, {
|
|
135
|
+
width: "32",
|
|
136
|
+
height: "32",
|
|
137
|
+
viewBox: "0 0 32 32"
|
|
138
|
+
})
|
|
139
|
+
}), jsxs("div", {
|
|
140
|
+
class: "bio-properties-panel-header-labels",
|
|
141
|
+
children: [jsx("div", {
|
|
142
|
+
title: type,
|
|
143
|
+
class: "bio-properties-panel-header-type",
|
|
144
|
+
children: type
|
|
145
|
+
}), label ? jsx("div", {
|
|
146
|
+
title: label,
|
|
147
|
+
class: "bio-properties-panel-header-label",
|
|
148
|
+
children: label
|
|
149
|
+
}) : null]
|
|
150
|
+
}), jsx("div", {
|
|
151
|
+
class: "bio-properties-panel-header-actions",
|
|
152
|
+
children: documentationRef ? jsx("a", {
|
|
153
|
+
rel: "noopener",
|
|
154
|
+
class: "bio-properties-panel-header-link",
|
|
155
|
+
href: documentationRef,
|
|
156
|
+
title: "Open documentation",
|
|
157
|
+
target: "_blank",
|
|
158
|
+
children: jsx(ExternalLinkIcon, {})
|
|
159
|
+
}) : null
|
|
160
|
+
})]
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const DescriptionContext = createContext({
|
|
165
|
+
description: {},
|
|
166
|
+
getDescriptionForId: () => {}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const ErrorsContext = createContext({
|
|
170
|
+
errors: {}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @typedef {Function} <propertiesPanel.showEntry> callback
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
*
|
|
178
|
+
* useEvent('propertiesPanel.showEntry', ({ focus = false, ...rest }) => {
|
|
179
|
+
* // ...
|
|
180
|
+
* });
|
|
181
|
+
*
|
|
182
|
+
* @param {Object} context
|
|
183
|
+
* @param {boolean} [context.focus]
|
|
184
|
+
*
|
|
185
|
+
* @returns void
|
|
186
|
+
*/
|
|
187
|
+
const EventContext = createContext({
|
|
188
|
+
eventBus: null
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const LayoutContext = createContext({
|
|
192
|
+
layout: {},
|
|
193
|
+
setLayout: () => {},
|
|
194
|
+
getLayoutForKey: () => {},
|
|
195
|
+
setLayoutForKey: () => {}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const TooltipContext = createContext({
|
|
199
|
+
tooltip: {},
|
|
200
|
+
getTooltipForId: () => {}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Accesses the global TooltipContext and returns a tooltip for a given id and element.
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```jsx
|
|
208
|
+
* function TextField(props) {
|
|
209
|
+
* const tooltip = useTooltipContext('input1', element);
|
|
210
|
+
* }
|
|
211
|
+
* ```
|
|
212
|
+
*
|
|
213
|
+
* @param {string} id
|
|
214
|
+
* @param {object} element
|
|
215
|
+
*
|
|
216
|
+
* @returns {string}
|
|
217
|
+
*/
|
|
218
|
+
function useTooltipContext(id, element) {
|
|
219
|
+
const {
|
|
220
|
+
getTooltipForId
|
|
221
|
+
} = useContext(TooltipContext);
|
|
222
|
+
return getTooltipForId(id, element);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function TooltipWrapper(props) {
|
|
226
|
+
const {
|
|
227
|
+
forId,
|
|
228
|
+
element
|
|
229
|
+
} = props;
|
|
230
|
+
const contextDescription = useTooltipContext(forId, element);
|
|
231
|
+
const value = props.value || contextDescription;
|
|
232
|
+
if (!value) {
|
|
233
|
+
return props.children;
|
|
234
|
+
}
|
|
235
|
+
return jsx(Tooltip, {
|
|
236
|
+
...props,
|
|
237
|
+
value: value,
|
|
238
|
+
forId: prefixId$9(forId)
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function Tooltip(props) {
|
|
242
|
+
const {
|
|
243
|
+
forId,
|
|
244
|
+
value,
|
|
245
|
+
parent
|
|
246
|
+
} = props;
|
|
247
|
+
const [visible, setShow] = useState(false);
|
|
248
|
+
const [focusedViaKeyboard, setFocusedViaKeyboard] = useState(false);
|
|
249
|
+
let timeout = null;
|
|
250
|
+
const wrapperRef = useRef(null);
|
|
251
|
+
const tooltipRef = useRef(null);
|
|
252
|
+
const showTooltip = async event => {
|
|
253
|
+
const show = () => setShow(true);
|
|
254
|
+
if (!visible && !timeout) {
|
|
255
|
+
if (event instanceof MouseEvent) {
|
|
256
|
+
timeout = setTimeout(show, 200);
|
|
257
|
+
} else {
|
|
258
|
+
show();
|
|
259
|
+
setFocusedViaKeyboard(true);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
const hideTooltip = () => {
|
|
264
|
+
setShow(false);
|
|
265
|
+
setFocusedViaKeyboard(false);
|
|
266
|
+
};
|
|
267
|
+
const hideTooltipViaEscape = e => {
|
|
268
|
+
e.code === 'Escape' && hideTooltip();
|
|
269
|
+
};
|
|
270
|
+
const isTooltipHovered = ({
|
|
271
|
+
x,
|
|
272
|
+
y
|
|
273
|
+
}) => {
|
|
274
|
+
const tooltip = tooltipRef.current;
|
|
275
|
+
const wrapper = wrapperRef.current;
|
|
276
|
+
return tooltip && (inBounds(x, y, wrapper.getBoundingClientRect()) || inBounds(x, y, tooltip.getBoundingClientRect()));
|
|
277
|
+
};
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
const {
|
|
280
|
+
current
|
|
281
|
+
} = wrapperRef;
|
|
282
|
+
if (!current) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const hideHoveredTooltip = e => {
|
|
286
|
+
const isFocused = document.activeElement === wrapperRef.current || document.activeElement.closest('.bio-properties-panel-tooltip');
|
|
287
|
+
if (visible && !isTooltipHovered({
|
|
288
|
+
x: e.x,
|
|
289
|
+
y: e.y
|
|
290
|
+
}) && !(isFocused && focusedViaKeyboard)) {
|
|
291
|
+
hideTooltip();
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
const hideFocusedTooltip = e => {
|
|
295
|
+
const {
|
|
296
|
+
relatedTarget
|
|
297
|
+
} = e;
|
|
298
|
+
const isTooltipChild = el => !!el.closest('.bio-properties-panel-tooltip');
|
|
299
|
+
if (visible && !isHovered(wrapperRef.current) && relatedTarget && !isTooltipChild(relatedTarget)) {
|
|
300
|
+
hideTooltip();
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
document.addEventListener('wheel', hideHoveredTooltip);
|
|
304
|
+
document.addEventListener('focusout', hideFocusedTooltip);
|
|
305
|
+
document.addEventListener('mousemove', hideHoveredTooltip);
|
|
306
|
+
return () => {
|
|
307
|
+
document.removeEventListener('wheel', hideHoveredTooltip);
|
|
308
|
+
document.removeEventListener('mousemove', hideHoveredTooltip);
|
|
309
|
+
document.removeEventListener('focusout', hideFocusedTooltip);
|
|
310
|
+
};
|
|
311
|
+
}, [wrapperRef.current, visible, focusedViaKeyboard]);
|
|
312
|
+
const renderTooltip = () => {
|
|
313
|
+
return jsxs("div", {
|
|
314
|
+
class: "bio-properties-panel-tooltip",
|
|
315
|
+
role: "tooltip",
|
|
316
|
+
id: "bio-properties-panel-tooltip",
|
|
317
|
+
"aria-labelledby": forId,
|
|
318
|
+
style: getTooltipPosition(wrapperRef.current),
|
|
319
|
+
ref: tooltipRef,
|
|
320
|
+
onClick: e => e.stopPropagation(),
|
|
321
|
+
children: [jsx("div", {
|
|
322
|
+
class: "bio-properties-panel-tooltip-content",
|
|
323
|
+
children: value
|
|
324
|
+
}), jsx("div", {
|
|
325
|
+
class: "bio-properties-panel-tooltip-arrow"
|
|
326
|
+
})]
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
return jsxs("div", {
|
|
330
|
+
class: "bio-properties-panel-tooltip-wrapper",
|
|
331
|
+
tabIndex: "0",
|
|
332
|
+
ref: wrapperRef,
|
|
333
|
+
onMouseEnter: showTooltip,
|
|
334
|
+
onMouseLeave: () => {
|
|
335
|
+
clearTimeout(timeout);
|
|
336
|
+
timeout = null;
|
|
337
|
+
},
|
|
338
|
+
onFocus: showTooltip,
|
|
339
|
+
onKeyDown: hideTooltipViaEscape,
|
|
340
|
+
children: [props.children, visible ? parent ? createPortal(renderTooltip(), parent.current) : renderTooltip() : null]
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// helper
|
|
345
|
+
function inBounds(x, y, bounds) {
|
|
346
|
+
const {
|
|
347
|
+
top,
|
|
348
|
+
right,
|
|
349
|
+
bottom,
|
|
350
|
+
left
|
|
351
|
+
} = bounds;
|
|
352
|
+
return x >= left && x <= right && y >= top && y <= bottom;
|
|
353
|
+
}
|
|
354
|
+
function getTooltipPosition(refElement) {
|
|
355
|
+
const refPosition = refElement.getBoundingClientRect();
|
|
356
|
+
const right = `calc(100% - ${refPosition.x}px)`;
|
|
357
|
+
const top = `${refPosition.top - 10}px`;
|
|
358
|
+
return `right: ${right}; top: ${top};`;
|
|
359
|
+
}
|
|
360
|
+
function isHovered(element) {
|
|
361
|
+
return element.matches(':hover');
|
|
362
|
+
}
|
|
363
|
+
function prefixId$9(id) {
|
|
364
|
+
return `bio-properties-panel-${id}`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Accesses the global DescriptionContext and returns a description for a given id and element.
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* ```jsx
|
|
372
|
+
* function TextField(props) {
|
|
373
|
+
* const description = useDescriptionContext('input1', element);
|
|
374
|
+
* }
|
|
375
|
+
* ```
|
|
376
|
+
*
|
|
377
|
+
* @param {string} id
|
|
378
|
+
* @param {object} element
|
|
379
|
+
*
|
|
380
|
+
* @returns {string}
|
|
381
|
+
*/
|
|
382
|
+
function useDescriptionContext(id, element) {
|
|
383
|
+
const {
|
|
384
|
+
getDescriptionForId
|
|
385
|
+
} = useContext(DescriptionContext);
|
|
386
|
+
return getDescriptionForId(id, element);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function useError(id) {
|
|
390
|
+
const {
|
|
391
|
+
errors
|
|
392
|
+
} = useContext(ErrorsContext);
|
|
393
|
+
return errors[id];
|
|
394
|
+
}
|
|
395
|
+
function useErrors() {
|
|
396
|
+
const {
|
|
397
|
+
errors
|
|
398
|
+
} = useContext(ErrorsContext);
|
|
399
|
+
return errors;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Subscribe to an event immediately. Update subscription after inputs changed.
|
|
404
|
+
*
|
|
405
|
+
* @param {string} event
|
|
406
|
+
* @param {Function} callback
|
|
407
|
+
*/
|
|
408
|
+
function useEvent(event, callback, eventBus) {
|
|
409
|
+
const eventContext = useContext(EventContext);
|
|
410
|
+
if (!eventBus) {
|
|
411
|
+
({
|
|
412
|
+
eventBus
|
|
413
|
+
} = eventContext);
|
|
414
|
+
}
|
|
415
|
+
const didMount = useRef(false);
|
|
416
|
+
|
|
417
|
+
// (1) subscribe immediately
|
|
418
|
+
if (eventBus && !didMount.current) {
|
|
419
|
+
eventBus.on(event, callback);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// (2) update subscription after inputs changed
|
|
423
|
+
useEffect(() => {
|
|
424
|
+
if (eventBus && didMount.current) {
|
|
425
|
+
eventBus.on(event, callback);
|
|
426
|
+
}
|
|
427
|
+
didMount.current = true;
|
|
428
|
+
return () => {
|
|
429
|
+
if (eventBus) {
|
|
430
|
+
eventBus.off(event, callback);
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
}, [callback, event, eventBus]);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const KEY_LENGTH = 6;
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Create a persistent key factory for plain objects without id.
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* ```jsx
|
|
443
|
+
* function List({ objects }) {
|
|
444
|
+
* const getKey = useKeyFactory();
|
|
445
|
+
* return (<ol>{
|
|
446
|
+
* objects.map(obj => {
|
|
447
|
+
* const key = getKey(obj);
|
|
448
|
+
* return <li key={key}>obj.name</li>
|
|
449
|
+
* })
|
|
450
|
+
* }</ol>);
|
|
451
|
+
* }
|
|
452
|
+
* ```
|
|
453
|
+
*
|
|
454
|
+
* @param {any[]} dependencies
|
|
455
|
+
* @returns {(element: object) => string}
|
|
456
|
+
*/
|
|
457
|
+
function useKeyFactory(dependencies = []) {
|
|
458
|
+
const map = useMemo(() => new Map(), dependencies);
|
|
459
|
+
const getKey = el => {
|
|
460
|
+
let key = map.get(el);
|
|
461
|
+
if (!key) {
|
|
462
|
+
key = Math.random().toString().slice(-KEY_LENGTH);
|
|
463
|
+
map.set(el, key);
|
|
464
|
+
}
|
|
465
|
+
return key;
|
|
466
|
+
};
|
|
467
|
+
return getKey;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Creates a state that persists in the global LayoutContext.
|
|
472
|
+
*
|
|
473
|
+
* @example
|
|
474
|
+
* ```jsx
|
|
475
|
+
* function Group(props) {
|
|
476
|
+
* const [ open, setOpen ] = useLayoutState([ 'groups', 'foo', 'open' ], false);
|
|
477
|
+
* }
|
|
478
|
+
* ```
|
|
479
|
+
*
|
|
480
|
+
* @param {(string|number)[]} path
|
|
481
|
+
* @param {any} [defaultValue]
|
|
482
|
+
*
|
|
483
|
+
* @returns {[ any, Function ]}
|
|
484
|
+
*/
|
|
485
|
+
function useLayoutState(path, defaultValue) {
|
|
486
|
+
const {
|
|
487
|
+
getLayoutForKey,
|
|
488
|
+
setLayoutForKey
|
|
489
|
+
} = useContext(LayoutContext);
|
|
490
|
+
const layoutForKey = getLayoutForKey(path, defaultValue);
|
|
491
|
+
const setState = useCallback(newValue => {
|
|
492
|
+
setLayoutForKey(path, newValue);
|
|
493
|
+
}, [setLayoutForKey]);
|
|
494
|
+
return [layoutForKey, setState];
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* @pinussilvestrus: we need to introduce our own hook to persist the previous
|
|
499
|
+
* state on updates.
|
|
500
|
+
*
|
|
501
|
+
* cf. https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
|
|
502
|
+
*/
|
|
503
|
+
|
|
504
|
+
function usePrevious(value) {
|
|
505
|
+
const ref = useRef();
|
|
506
|
+
useEffect(() => {
|
|
507
|
+
ref.current = value;
|
|
508
|
+
});
|
|
509
|
+
return ref.current;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Subscribe to `propertiesPanel.showEntry`.
|
|
514
|
+
*
|
|
515
|
+
* @param {string} id
|
|
516
|
+
*
|
|
517
|
+
* @returns {import('preact').Ref}
|
|
518
|
+
*/
|
|
519
|
+
function useShowEntryEvent(id) {
|
|
520
|
+
const {
|
|
521
|
+
onShow
|
|
522
|
+
} = useContext(LayoutContext);
|
|
523
|
+
const ref = useRef();
|
|
524
|
+
const focus = useRef(false);
|
|
525
|
+
const onShowEntry = useCallback(event => {
|
|
526
|
+
if (event.id === id) {
|
|
527
|
+
onShow();
|
|
528
|
+
if (!focus.current) {
|
|
529
|
+
focus.current = true;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}, [id]);
|
|
533
|
+
useEffect(() => {
|
|
534
|
+
if (focus.current && ref.current) {
|
|
535
|
+
if (isFunction(ref.current.focus)) {
|
|
536
|
+
ref.current.focus();
|
|
537
|
+
}
|
|
538
|
+
if (isFunction(ref.current.select)) {
|
|
539
|
+
ref.current.select();
|
|
540
|
+
}
|
|
541
|
+
focus.current = false;
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
useEvent('propertiesPanel.showEntry', onShowEntry);
|
|
545
|
+
return ref;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* @callback setSticky
|
|
550
|
+
* @param {boolean} value
|
|
551
|
+
*/
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Use IntersectionObserver to identify when DOM element is in sticky mode.
|
|
555
|
+
* If sticky is observered setSticky(true) will be called.
|
|
556
|
+
* If sticky mode is left, setSticky(false) will be called.
|
|
557
|
+
*
|
|
558
|
+
*
|
|
559
|
+
* @param {Object} ref
|
|
560
|
+
* @param {string} scrollContainerSelector
|
|
561
|
+
* @param {setSticky} setSticky
|
|
562
|
+
*/
|
|
563
|
+
function useStickyIntersectionObserver(ref, scrollContainerSelector, setSticky) {
|
|
564
|
+
const [scrollContainer, setScrollContainer] = useState(query(scrollContainerSelector));
|
|
565
|
+
const updateScrollContainer = useCallback(() => {
|
|
566
|
+
const newScrollContainer = query(scrollContainerSelector);
|
|
567
|
+
if (newScrollContainer !== scrollContainer) {
|
|
568
|
+
setScrollContainer(newScrollContainer);
|
|
569
|
+
}
|
|
570
|
+
}, [scrollContainerSelector, scrollContainer]);
|
|
571
|
+
useEffect(() => {
|
|
572
|
+
updateScrollContainer();
|
|
573
|
+
}, [updateScrollContainer]);
|
|
574
|
+
useEvent('propertiesPanel.attach', updateScrollContainer);
|
|
575
|
+
useEvent('propertiesPanel.detach', updateScrollContainer);
|
|
576
|
+
useEffect(() => {
|
|
577
|
+
const Observer = IntersectionObserver;
|
|
578
|
+
|
|
579
|
+
// return early if IntersectionObserver is not available
|
|
580
|
+
if (!Observer) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// TODO(@barmac): test this
|
|
585
|
+
if (!ref.current || !scrollContainer) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const observer = new Observer(entries => {
|
|
589
|
+
// scroll container is unmounted, do not update sticky state
|
|
590
|
+
if (scrollContainer.scrollHeight === 0) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
entries.forEach(entry => {
|
|
594
|
+
if (entry.intersectionRatio < 1) {
|
|
595
|
+
setSticky(true);
|
|
596
|
+
} else if (entry.intersectionRatio === 1) {
|
|
597
|
+
setSticky(false);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
}, {
|
|
601
|
+
root: scrollContainer,
|
|
602
|
+
rootMargin: '0px 0px 999999% 0px',
|
|
603
|
+
// Use bottom margin to avoid stickyness when scrolling out to bottom
|
|
604
|
+
threshold: [1]
|
|
605
|
+
});
|
|
606
|
+
observer.observe(ref.current);
|
|
607
|
+
|
|
608
|
+
// Unobserve if unmounted
|
|
609
|
+
return () => {
|
|
610
|
+
observer.unobserve(ref.current);
|
|
611
|
+
};
|
|
612
|
+
}, [ref.current, scrollContainer, setSticky]);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Creates a static function reference with changing body.
|
|
617
|
+
* This is necessary when external libraries require a callback function
|
|
618
|
+
* that has references to state variables.
|
|
619
|
+
*
|
|
620
|
+
* Usage:
|
|
621
|
+
* const callback = useStaticCallback((val) => {val === currentState});
|
|
622
|
+
*
|
|
623
|
+
* The `callback` reference is static and can be safely used in external
|
|
624
|
+
* libraries or as a prop that does not cause rerendering of children.
|
|
625
|
+
*
|
|
626
|
+
* @param {Function} callback function with changing reference
|
|
627
|
+
* @returns {Function} static function reference
|
|
628
|
+
*/
|
|
629
|
+
function useStaticCallback(callback) {
|
|
630
|
+
const callbackRef = useRef(callback);
|
|
631
|
+
callbackRef.current = callback;
|
|
632
|
+
return useCallback((...args) => callbackRef.current(...args), []);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function Group(props) {
|
|
636
|
+
const {
|
|
637
|
+
element,
|
|
638
|
+
entries = [],
|
|
639
|
+
id,
|
|
640
|
+
label,
|
|
641
|
+
shouldOpen = false
|
|
642
|
+
} = props;
|
|
643
|
+
const groupRef = useRef(null);
|
|
644
|
+
const [open, setOpen] = useLayoutState(['groups', id, 'open'], shouldOpen);
|
|
645
|
+
const onShow = useCallback(() => setOpen(true), [setOpen]);
|
|
646
|
+
const toggleOpen = () => setOpen(!open);
|
|
647
|
+
const [edited, setEdited] = useState(false);
|
|
648
|
+
const [sticky, setSticky] = useState(false);
|
|
649
|
+
|
|
650
|
+
// set edited state depending on all entries
|
|
651
|
+
useEffect(() => {
|
|
652
|
+
// TODO(@barmac): replace with CSS when `:has()` is supported in all major browsers, or rewrite as in https://github.com/camunda/camunda-modeler/issues/3815#issuecomment-1733038161
|
|
653
|
+
const scheduled = requestAnimationFrame(() => {
|
|
654
|
+
const hasOneEditedEntry = entries.find(entry => {
|
|
655
|
+
const {
|
|
656
|
+
id,
|
|
657
|
+
isEdited
|
|
658
|
+
} = entry;
|
|
659
|
+
const entryNode = query(`[data-entry-id="${id}"]`);
|
|
660
|
+
if (!isFunction(isEdited) || !entryNode) {
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
const inputNode = query('.bio-properties-panel-input', entryNode);
|
|
664
|
+
return isEdited(inputNode);
|
|
665
|
+
});
|
|
666
|
+
setEdited(hasOneEditedEntry);
|
|
667
|
+
});
|
|
668
|
+
return () => cancelAnimationFrame(scheduled);
|
|
669
|
+
}, [entries, setEdited]);
|
|
670
|
+
|
|
671
|
+
// set error state depending on all entries
|
|
672
|
+
const allErrors = useErrors();
|
|
673
|
+
const hasErrors = entries.some(entry => allErrors[entry.id]);
|
|
674
|
+
|
|
675
|
+
// set css class when group is sticky to top
|
|
676
|
+
useStickyIntersectionObserver(groupRef, 'div.bio-properties-panel-scroll-container', setSticky);
|
|
677
|
+
const propertiesPanelContext = {
|
|
678
|
+
...useContext(LayoutContext),
|
|
679
|
+
onShow
|
|
680
|
+
};
|
|
681
|
+
return jsxs("div", {
|
|
682
|
+
class: "bio-properties-panel-group",
|
|
683
|
+
"data-group-id": 'group-' + id,
|
|
684
|
+
ref: groupRef,
|
|
685
|
+
children: [jsxs("div", {
|
|
686
|
+
class: classnames('bio-properties-panel-group-header', edited ? '' : 'empty', open ? 'open' : '', sticky && open ? 'sticky' : ''),
|
|
687
|
+
onClick: toggleOpen,
|
|
688
|
+
children: [jsx("div", {
|
|
689
|
+
title: props.tooltip ? null : label,
|
|
690
|
+
"data-title": label,
|
|
691
|
+
class: "bio-properties-panel-group-header-title",
|
|
692
|
+
children: jsx(TooltipWrapper, {
|
|
693
|
+
value: props.tooltip,
|
|
694
|
+
forId: 'group-' + id,
|
|
695
|
+
element: element,
|
|
696
|
+
parent: groupRef,
|
|
697
|
+
children: label
|
|
698
|
+
})
|
|
699
|
+
}), jsxs("div", {
|
|
700
|
+
class: "bio-properties-panel-group-header-buttons",
|
|
701
|
+
children: [jsx(DataMarker, {
|
|
702
|
+
edited: edited,
|
|
703
|
+
hasErrors: hasErrors
|
|
704
|
+
}), jsx("button", {
|
|
705
|
+
title: "Toggle section",
|
|
706
|
+
class: "bio-properties-panel-group-header-button bio-properties-panel-arrow",
|
|
707
|
+
children: jsx(ArrowIcon, {
|
|
708
|
+
class: open ? 'bio-properties-panel-arrow-down' : 'bio-properties-panel-arrow-right'
|
|
709
|
+
})
|
|
710
|
+
})]
|
|
711
|
+
})]
|
|
712
|
+
}), jsx("div", {
|
|
713
|
+
class: classnames('bio-properties-panel-group-entries', open ? 'open' : ''),
|
|
714
|
+
children: jsx(LayoutContext.Provider, {
|
|
715
|
+
value: propertiesPanelContext,
|
|
716
|
+
children: entries.map(entry => {
|
|
717
|
+
const {
|
|
718
|
+
component: Component,
|
|
719
|
+
id
|
|
720
|
+
} = entry;
|
|
721
|
+
return createElement(Component, {
|
|
722
|
+
...entry,
|
|
723
|
+
element: element,
|
|
724
|
+
key: id
|
|
725
|
+
});
|
|
726
|
+
})
|
|
727
|
+
})
|
|
728
|
+
})]
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
function DataMarker(props) {
|
|
732
|
+
const {
|
|
733
|
+
edited,
|
|
734
|
+
hasErrors
|
|
735
|
+
} = props;
|
|
736
|
+
if (hasErrors) {
|
|
737
|
+
return jsx("div", {
|
|
738
|
+
title: "Section contains an error",
|
|
739
|
+
class: "bio-properties-panel-dot bio-properties-panel-dot--error"
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
if (edited) {
|
|
743
|
+
return jsx("div", {
|
|
744
|
+
title: "Section contains data",
|
|
745
|
+
class: "bio-properties-panel-dot"
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* @typedef { {
|
|
753
|
+
* text: (element: object) => string,
|
|
754
|
+
* icon?: (element: Object) => import('preact').Component
|
|
755
|
+
* } } PlaceholderDefinition
|
|
756
|
+
*
|
|
757
|
+
* @param { PlaceholderDefinition } props
|
|
758
|
+
*/
|
|
759
|
+
function Placeholder(props) {
|
|
760
|
+
const {
|
|
761
|
+
text,
|
|
762
|
+
icon: Icon
|
|
763
|
+
} = props;
|
|
764
|
+
return jsx("div", {
|
|
765
|
+
class: "bio-properties-panel open",
|
|
766
|
+
children: jsxs("section", {
|
|
767
|
+
class: "bio-properties-panel-placeholder",
|
|
768
|
+
children: [Icon && jsx(Icon, {
|
|
769
|
+
class: "bio-properties-panel-placeholder-icon"
|
|
770
|
+
}), jsx("p", {
|
|
771
|
+
class: "bio-properties-panel-placeholder-text",
|
|
772
|
+
children: text
|
|
773
|
+
})]
|
|
774
|
+
})
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function Description(props) {
|
|
779
|
+
const {
|
|
780
|
+
element,
|
|
781
|
+
forId,
|
|
782
|
+
value
|
|
783
|
+
} = props;
|
|
784
|
+
const contextDescription = useDescriptionContext(forId, element);
|
|
785
|
+
const description = value || contextDescription;
|
|
786
|
+
if (description) {
|
|
787
|
+
return jsx("div", {
|
|
788
|
+
class: "bio-properties-panel-description",
|
|
789
|
+
children: description
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const noop$6 = () => {};
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Buffer `.focus()` calls while the editor is not initialized.
|
|
798
|
+
* Set Focus inside when the editor is ready.
|
|
799
|
+
*/
|
|
800
|
+
const useBufferedFocus$1 = function (editor, ref) {
|
|
801
|
+
const [buffer, setBuffer] = useState(undefined);
|
|
802
|
+
ref.current = useMemo(() => ({
|
|
803
|
+
focus: offset => {
|
|
804
|
+
if (editor) {
|
|
805
|
+
editor.focus(offset);
|
|
806
|
+
} else {
|
|
807
|
+
if (typeof offset === 'undefined') {
|
|
808
|
+
offset = Infinity;
|
|
809
|
+
}
|
|
810
|
+
setBuffer(offset);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}), [editor]);
|
|
814
|
+
useEffect(() => {
|
|
815
|
+
if (typeof buffer !== 'undefined' && editor) {
|
|
816
|
+
editor.focus(buffer);
|
|
817
|
+
setBuffer(false);
|
|
818
|
+
}
|
|
819
|
+
}, [editor, buffer]);
|
|
820
|
+
};
|
|
821
|
+
const CodeEditor$1 = forwardRef((props, ref) => {
|
|
822
|
+
const {
|
|
823
|
+
onInput,
|
|
824
|
+
disabled,
|
|
825
|
+
tooltipContainer,
|
|
826
|
+
enableGutters,
|
|
827
|
+
value,
|
|
828
|
+
onLint = noop$6,
|
|
829
|
+
onPopupOpen = noop$6,
|
|
830
|
+
popupOpen,
|
|
831
|
+
contentAttributes = {},
|
|
832
|
+
hostLanguage = null,
|
|
833
|
+
singleLine = false
|
|
834
|
+
} = props;
|
|
835
|
+
const inputRef = useRef();
|
|
836
|
+
const [editor, setEditor] = useState();
|
|
837
|
+
const [localValue, setLocalValue] = useState(value || '');
|
|
838
|
+
useBufferedFocus$1(editor, ref);
|
|
839
|
+
const handleInput = useStaticCallback(newValue => {
|
|
840
|
+
onInput(newValue);
|
|
841
|
+
setLocalValue(newValue);
|
|
842
|
+
});
|
|
843
|
+
useEffect(() => {
|
|
844
|
+
let editor;
|
|
845
|
+
editor = new FeelersEditor({
|
|
846
|
+
container: inputRef.current,
|
|
847
|
+
onChange: handleInput,
|
|
848
|
+
value: localValue,
|
|
849
|
+
onLint,
|
|
850
|
+
contentAttributes,
|
|
851
|
+
tooltipContainer,
|
|
852
|
+
enableGutters,
|
|
853
|
+
hostLanguage,
|
|
854
|
+
singleLine
|
|
855
|
+
});
|
|
856
|
+
setEditor(editor);
|
|
857
|
+
return () => {
|
|
858
|
+
onLint([]);
|
|
859
|
+
inputRef.current.innerHTML = '';
|
|
860
|
+
setEditor(null);
|
|
861
|
+
};
|
|
862
|
+
}, []);
|
|
863
|
+
useEffect(() => {
|
|
864
|
+
if (!editor) {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
if (value === localValue) {
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
editor.setValue(value);
|
|
871
|
+
setLocalValue(value);
|
|
872
|
+
}, [value]);
|
|
873
|
+
const handleClick = () => {
|
|
874
|
+
ref.current.focus();
|
|
875
|
+
};
|
|
876
|
+
return jsxs("div", {
|
|
877
|
+
class: classnames('bio-properties-panel-feelers-editor-container', popupOpen ? 'popupOpen' : null),
|
|
878
|
+
children: [jsx("div", {
|
|
879
|
+
class: "bio-properties-panel-feelers-editor__open-popup-placeholder",
|
|
880
|
+
children: "Opened in editor"
|
|
881
|
+
}), jsx("div", {
|
|
882
|
+
name: props.name,
|
|
883
|
+
class: classnames('bio-properties-panel-feelers-editor bio-properties-panel-input', localValue ? 'edited' : null, disabled ? 'disabled' : null),
|
|
884
|
+
ref: inputRef,
|
|
885
|
+
onClick: handleClick
|
|
886
|
+
}), jsx("button", {
|
|
887
|
+
title: "Open pop-up editor",
|
|
888
|
+
class: "bio-properties-panel-open-feel-popup",
|
|
889
|
+
onClick: () => onPopupOpen('feelers'),
|
|
890
|
+
children: jsx(ExternalLinkIcon, {})
|
|
891
|
+
})]
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
const noop$5 = () => {};
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Buffer `.focus()` calls while the editor is not initialized.
|
|
899
|
+
* Set Focus inside when the editor is ready.
|
|
900
|
+
*/
|
|
901
|
+
const useBufferedFocus = function (editor, ref) {
|
|
902
|
+
const [buffer, setBuffer] = useState(undefined);
|
|
903
|
+
ref.current = useMemo(() => ({
|
|
904
|
+
focus: offset => {
|
|
905
|
+
if (editor) {
|
|
906
|
+
editor.focus(offset);
|
|
907
|
+
} else {
|
|
908
|
+
if (typeof offset === 'undefined') {
|
|
909
|
+
offset = Infinity;
|
|
910
|
+
}
|
|
911
|
+
setBuffer(offset);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}), [editor]);
|
|
915
|
+
useEffect(() => {
|
|
916
|
+
if (typeof buffer !== 'undefined' && editor) {
|
|
917
|
+
editor.focus(buffer);
|
|
918
|
+
setBuffer(false);
|
|
919
|
+
}
|
|
920
|
+
}, [editor, buffer]);
|
|
921
|
+
};
|
|
922
|
+
const CodeEditor = forwardRef((props, ref) => {
|
|
923
|
+
const {
|
|
924
|
+
enableGutters,
|
|
925
|
+
value,
|
|
926
|
+
onInput,
|
|
927
|
+
onFeelToggle = noop$5,
|
|
928
|
+
onLint = noop$5,
|
|
929
|
+
onPopupOpen = noop$5,
|
|
930
|
+
popupOpen,
|
|
931
|
+
disabled,
|
|
932
|
+
tooltipContainer,
|
|
933
|
+
variables
|
|
934
|
+
} = props;
|
|
935
|
+
const inputRef = useRef();
|
|
936
|
+
const [editor, setEditor] = useState();
|
|
937
|
+
const [localValue, setLocalValue] = useState(value || '');
|
|
938
|
+
useBufferedFocus(editor, ref);
|
|
939
|
+
const handleInput = useStaticCallback(newValue => {
|
|
940
|
+
onInput(newValue);
|
|
941
|
+
setLocalValue(newValue);
|
|
942
|
+
});
|
|
943
|
+
useEffect(() => {
|
|
944
|
+
let editor;
|
|
945
|
+
|
|
946
|
+
/* Trigger FEEL toggle when
|
|
947
|
+
*
|
|
948
|
+
* - `backspace` is pressed
|
|
949
|
+
* - AND the cursor is at the beginning of the input
|
|
950
|
+
*/
|
|
951
|
+
const onKeyDown = e => {
|
|
952
|
+
if (e.key !== 'Backspace' || !editor) {
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
const selection = editor.getSelection();
|
|
956
|
+
const range = selection.ranges[selection.mainIndex];
|
|
957
|
+
if (range.from === 0 && range.to === 0) {
|
|
958
|
+
onFeelToggle();
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
editor = new FeelEditor({
|
|
962
|
+
container: inputRef.current,
|
|
963
|
+
onChange: handleInput,
|
|
964
|
+
onKeyDown: onKeyDown,
|
|
965
|
+
onLint: onLint,
|
|
966
|
+
tooltipContainer: tooltipContainer,
|
|
967
|
+
value: localValue,
|
|
968
|
+
variables: variables,
|
|
969
|
+
extensions: [...(enableGutters ? [lineNumbers()] : [])]
|
|
970
|
+
});
|
|
971
|
+
setEditor(editor);
|
|
972
|
+
return () => {
|
|
973
|
+
onLint([]);
|
|
974
|
+
inputRef.current.innerHTML = '';
|
|
975
|
+
setEditor(null);
|
|
976
|
+
};
|
|
977
|
+
}, []);
|
|
978
|
+
useEffect(() => {
|
|
979
|
+
if (!editor) {
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
if (value === localValue) {
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
editor.setValue(value);
|
|
986
|
+
setLocalValue(value);
|
|
987
|
+
}, [value]);
|
|
988
|
+
useEffect(() => {
|
|
989
|
+
if (!editor) {
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
editor.setVariables(variables);
|
|
993
|
+
}, [variables]);
|
|
994
|
+
const handleClick = () => {
|
|
995
|
+
ref.current.focus();
|
|
996
|
+
};
|
|
997
|
+
return jsxs("div", {
|
|
998
|
+
class: classnames('bio-properties-panel-feel-editor-container', disabled ? 'disabled' : null, popupOpen ? 'popupOpen' : null),
|
|
999
|
+
children: [jsx("div", {
|
|
1000
|
+
class: "bio-properties-panel-feel-editor__open-popup-placeholder",
|
|
1001
|
+
children: "Opened in editor"
|
|
1002
|
+
}), jsx("div", {
|
|
1003
|
+
name: props.name,
|
|
1004
|
+
class: classnames('bio-properties-panel-input', localValue ? 'edited' : null),
|
|
1005
|
+
ref: inputRef,
|
|
1006
|
+
onClick: handleClick
|
|
1007
|
+
}), jsx("button", {
|
|
1008
|
+
title: "Open pop-up editor",
|
|
1009
|
+
class: "bio-properties-panel-open-feel-popup",
|
|
1010
|
+
onClick: () => onPopupOpen(),
|
|
1011
|
+
children: jsx(ExternalLinkIcon, {})
|
|
1012
|
+
})]
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
function FeelIndicator(props) {
|
|
1017
|
+
const {
|
|
1018
|
+
active
|
|
1019
|
+
} = props;
|
|
1020
|
+
if (!active) {
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
return jsx("span", {
|
|
1024
|
+
class: "bio-properties-panel-feel-indicator",
|
|
1025
|
+
children: "="
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const noop$4 = () => {};
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* @param {Object} props
|
|
1033
|
+
* @param {Object} props.label
|
|
1034
|
+
* @param {String} props.feel
|
|
1035
|
+
*/
|
|
1036
|
+
function FeelIcon(props) {
|
|
1037
|
+
const {
|
|
1038
|
+
feel = false,
|
|
1039
|
+
active,
|
|
1040
|
+
disabled = false,
|
|
1041
|
+
onClick = noop$4
|
|
1042
|
+
} = props;
|
|
1043
|
+
const feelRequiredLabel = 'FEEL expression is mandatory';
|
|
1044
|
+
const feelOptionalLabel = `Click to ${active ? 'remove' : 'set a'} dynamic value with FEEL expression`;
|
|
1045
|
+
const handleClick = e => {
|
|
1046
|
+
onClick(e);
|
|
1047
|
+
|
|
1048
|
+
// when pointer event was created from keyboard, keep focus on button
|
|
1049
|
+
if (!e.pointerType) {
|
|
1050
|
+
e.stopPropagation();
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
return jsx("button", {
|
|
1054
|
+
class: classnames('bio-properties-panel-feel-icon', active ? 'active' : null, feel === 'required' ? 'required' : 'optional'),
|
|
1055
|
+
onClick: handleClick,
|
|
1056
|
+
disabled: feel === 'required' || disabled,
|
|
1057
|
+
title: feel === 'required' ? feelRequiredLabel : feelOptionalLabel,
|
|
1058
|
+
children: jsx(FeelIcon$1, {})
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const FeelPopupContext = createContext({
|
|
1063
|
+
open: () => {},
|
|
1064
|
+
close: () => {},
|
|
1065
|
+
source: null
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Add a dragger that calls back the passed function with
|
|
1070
|
+
* { event, delta } on drag.
|
|
1071
|
+
*
|
|
1072
|
+
* @example
|
|
1073
|
+
*
|
|
1074
|
+
* function dragMove(event, delta) {
|
|
1075
|
+
* // we are dragging (!!)
|
|
1076
|
+
* }
|
|
1077
|
+
*
|
|
1078
|
+
* domElement.addEventListener('dragstart', dragger(dragMove));
|
|
1079
|
+
*
|
|
1080
|
+
* @param {Function} fn
|
|
1081
|
+
* @param {Element} [dragPreview]
|
|
1082
|
+
*
|
|
1083
|
+
* @return {Function} drag start callback function
|
|
1084
|
+
*/
|
|
1085
|
+
function createDragger(fn, dragPreview) {
|
|
1086
|
+
let self;
|
|
1087
|
+
let startX, startY;
|
|
1088
|
+
|
|
1089
|
+
/** drag start */
|
|
1090
|
+
function onDragStart(event) {
|
|
1091
|
+
self = this;
|
|
1092
|
+
startX = event.clientX;
|
|
1093
|
+
startY = event.clientY;
|
|
1094
|
+
|
|
1095
|
+
// (1) prevent preview image
|
|
1096
|
+
if (event.dataTransfer) {
|
|
1097
|
+
event.dataTransfer.setDragImage(dragPreview || emptyCanvas(), 0, 0);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// (2) setup drag listeners
|
|
1101
|
+
|
|
1102
|
+
// attach drag + cleanup event
|
|
1103
|
+
// we need to do this to make sure we track cursor
|
|
1104
|
+
// movements before we reach other drag event handlers,
|
|
1105
|
+
// e.g. in child containers.
|
|
1106
|
+
document.addEventListener('dragover', onDrag, true);
|
|
1107
|
+
document.addEventListener('dragenter', preventDefault, true);
|
|
1108
|
+
document.addEventListener('dragend', onEnd);
|
|
1109
|
+
document.addEventListener('drop', preventDefault);
|
|
1110
|
+
}
|
|
1111
|
+
function onDrag(event) {
|
|
1112
|
+
const delta = {
|
|
1113
|
+
x: event.clientX - startX,
|
|
1114
|
+
y: event.clientY - startY
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
// call provided fn with event, delta
|
|
1118
|
+
return fn.call(self, event, delta);
|
|
1119
|
+
}
|
|
1120
|
+
function onEnd() {
|
|
1121
|
+
document.removeEventListener('dragover', onDrag, true);
|
|
1122
|
+
document.removeEventListener('dragenter', preventDefault, true);
|
|
1123
|
+
document.removeEventListener('dragend', onEnd);
|
|
1124
|
+
document.removeEventListener('drop', preventDefault);
|
|
1125
|
+
}
|
|
1126
|
+
return onDragStart;
|
|
1127
|
+
}
|
|
1128
|
+
function preventDefault(event) {
|
|
1129
|
+
event.preventDefault();
|
|
1130
|
+
event.stopPropagation();
|
|
1131
|
+
}
|
|
1132
|
+
function emptyCanvas() {
|
|
1133
|
+
return domify('<canvas width="0" height="0" />');
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const noop$3 = () => {};
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* A generic popup component.
|
|
1140
|
+
*
|
|
1141
|
+
* @param {Object} props
|
|
1142
|
+
* @param {HTMLElement} [props.container]
|
|
1143
|
+
* @param {string} [props.className]
|
|
1144
|
+
* @param {boolean} [props.delayInitialFocus]
|
|
1145
|
+
* @param {{x: number, y: number}} [props.position]
|
|
1146
|
+
* @param {number} [props.width]
|
|
1147
|
+
* @param {number} [props.height]
|
|
1148
|
+
* @param {Function} props.onClose
|
|
1149
|
+
* @param {Function} [props.onPostActivate]
|
|
1150
|
+
* @param {Function} [props.onPostDeactivate]
|
|
1151
|
+
* @param {boolean} [props.returnFocus]
|
|
1152
|
+
* @param {boolean} [props.closeOnEscape]
|
|
1153
|
+
* @param {string} props.title
|
|
1154
|
+
* @param {Ref} [ref]
|
|
1155
|
+
*/
|
|
1156
|
+
function PopupComponent(props, globalRef) {
|
|
1157
|
+
const {
|
|
1158
|
+
container,
|
|
1159
|
+
className,
|
|
1160
|
+
delayInitialFocus,
|
|
1161
|
+
position,
|
|
1162
|
+
width,
|
|
1163
|
+
height,
|
|
1164
|
+
onClose,
|
|
1165
|
+
onPostActivate = noop$3,
|
|
1166
|
+
onPostDeactivate = noop$3,
|
|
1167
|
+
returnFocus = true,
|
|
1168
|
+
closeOnEscape = true,
|
|
1169
|
+
title
|
|
1170
|
+
} = props;
|
|
1171
|
+
const focusTrapRef = useRef(null);
|
|
1172
|
+
const localRef = useRef(null);
|
|
1173
|
+
const popupRef = globalRef || localRef;
|
|
1174
|
+
const containerNode = useMemo(() => getContainerNode(container), [container]);
|
|
1175
|
+
const handleKeydown = event => {
|
|
1176
|
+
// do not allow keyboard events to bubble
|
|
1177
|
+
event.stopPropagation();
|
|
1178
|
+
if (closeOnEscape && event.key === 'Escape') {
|
|
1179
|
+
onClose();
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
// re-activate focus trap on focus
|
|
1184
|
+
const handleFocus = () => {
|
|
1185
|
+
if (focusTrapRef.current) {
|
|
1186
|
+
focusTrapRef.current.activate();
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
let style = {};
|
|
1190
|
+
if (position) {
|
|
1191
|
+
style = {
|
|
1192
|
+
...style,
|
|
1193
|
+
top: position.top + 'px',
|
|
1194
|
+
left: position.left + 'px'
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
if (width) {
|
|
1198
|
+
style.width = width + 'px';
|
|
1199
|
+
}
|
|
1200
|
+
if (height) {
|
|
1201
|
+
style.height = height + 'px';
|
|
1202
|
+
}
|
|
1203
|
+
useEffect(() => {
|
|
1204
|
+
if (popupRef.current) {
|
|
1205
|
+
popupRef.current.addEventListener('focusin', handleFocus);
|
|
1206
|
+
}
|
|
1207
|
+
return () => {
|
|
1208
|
+
popupRef.current.removeEventListener('focusin', handleFocus);
|
|
1209
|
+
};
|
|
1210
|
+
}, [popupRef]);
|
|
1211
|
+
useEffect(() => {
|
|
1212
|
+
if (popupRef.current) {
|
|
1213
|
+
focusTrapRef.current = focusTrap.createFocusTrap(popupRef.current, {
|
|
1214
|
+
clickOutsideDeactivates: true,
|
|
1215
|
+
delayInitialFocus,
|
|
1216
|
+
fallbackFocus: popupRef.current,
|
|
1217
|
+
onPostActivate,
|
|
1218
|
+
onPostDeactivate,
|
|
1219
|
+
returnFocusOnDeactivate: returnFocus
|
|
1220
|
+
});
|
|
1221
|
+
focusTrapRef.current.activate();
|
|
1222
|
+
}
|
|
1223
|
+
return () => focusTrapRef.current && focusTrapRef.current.deactivate();
|
|
1224
|
+
}, [popupRef]);
|
|
1225
|
+
return createPortal(jsx("div", {
|
|
1226
|
+
"aria-label": title,
|
|
1227
|
+
tabIndex: -1,
|
|
1228
|
+
ref: popupRef,
|
|
1229
|
+
onKeyDown: handleKeydown,
|
|
1230
|
+
role: "dialog",
|
|
1231
|
+
class: classnames('bio-properties-panel-popup', className),
|
|
1232
|
+
style: style,
|
|
1233
|
+
children: props.children
|
|
1234
|
+
}), containerNode || document.body);
|
|
1235
|
+
}
|
|
1236
|
+
const Popup = forwardRef(PopupComponent);
|
|
1237
|
+
Popup.Title = Title;
|
|
1238
|
+
Popup.Body = Body;
|
|
1239
|
+
Popup.Footer = Footer;
|
|
1240
|
+
function Title(props) {
|
|
1241
|
+
const {
|
|
1242
|
+
children,
|
|
1243
|
+
className,
|
|
1244
|
+
draggable,
|
|
1245
|
+
emit = () => {},
|
|
1246
|
+
title,
|
|
1247
|
+
...rest
|
|
1248
|
+
} = props;
|
|
1249
|
+
|
|
1250
|
+
// we can't use state as we need to
|
|
1251
|
+
// manipulate this inside dragging events
|
|
1252
|
+
const context = useRef({
|
|
1253
|
+
startPosition: null,
|
|
1254
|
+
newPosition: null
|
|
1255
|
+
});
|
|
1256
|
+
const dragPreviewRef = useRef();
|
|
1257
|
+
const titleRef = useRef();
|
|
1258
|
+
const onMove = (event, delta) => {
|
|
1259
|
+
cancel(event);
|
|
1260
|
+
const {
|
|
1261
|
+
x: dx,
|
|
1262
|
+
y: dy
|
|
1263
|
+
} = delta;
|
|
1264
|
+
const newPosition = {
|
|
1265
|
+
x: context.current.startPosition.x + dx,
|
|
1266
|
+
y: context.current.startPosition.y + dy
|
|
1267
|
+
};
|
|
1268
|
+
const popupParent = getPopupParent(titleRef.current);
|
|
1269
|
+
popupParent.style.top = newPosition.y + 'px';
|
|
1270
|
+
popupParent.style.left = newPosition.x + 'px';
|
|
1271
|
+
|
|
1272
|
+
// notify interested parties
|
|
1273
|
+
emit('dragover', {
|
|
1274
|
+
newPosition,
|
|
1275
|
+
delta
|
|
1276
|
+
});
|
|
1277
|
+
};
|
|
1278
|
+
const onMoveStart = event => {
|
|
1279
|
+
// initialize drag handler
|
|
1280
|
+
const onDragStart = createDragger(onMove, dragPreviewRef.current);
|
|
1281
|
+
onDragStart(event);
|
|
1282
|
+
event.stopPropagation();
|
|
1283
|
+
const popupParent = getPopupParent(titleRef.current);
|
|
1284
|
+
const bounds = popupParent.getBoundingClientRect();
|
|
1285
|
+
context.current.startPosition = {
|
|
1286
|
+
x: bounds.left,
|
|
1287
|
+
y: bounds.top
|
|
1288
|
+
};
|
|
1289
|
+
|
|
1290
|
+
// notify interested parties
|
|
1291
|
+
emit('dragstart');
|
|
1292
|
+
};
|
|
1293
|
+
const onMoveEnd = () => {
|
|
1294
|
+
context.current.newPosition = null;
|
|
1295
|
+
|
|
1296
|
+
// notify interested parties
|
|
1297
|
+
emit('dragend');
|
|
1298
|
+
};
|
|
1299
|
+
return jsxs("div", {
|
|
1300
|
+
class: classnames('bio-properties-panel-popup__header', draggable && 'draggable', className),
|
|
1301
|
+
ref: titleRef,
|
|
1302
|
+
draggable: draggable,
|
|
1303
|
+
onDragStart: onMoveStart,
|
|
1304
|
+
onDragEnd: onMoveEnd,
|
|
1305
|
+
...rest,
|
|
1306
|
+
children: [draggable && jsxs(Fragment, {
|
|
1307
|
+
children: [jsx("div", {
|
|
1308
|
+
ref: dragPreviewRef,
|
|
1309
|
+
class: "bio-properties-panel-popup__drag-preview"
|
|
1310
|
+
}), jsx("div", {
|
|
1311
|
+
class: "bio-properties-panel-popup__drag-handle",
|
|
1312
|
+
children: jsx(DragIcon, {})
|
|
1313
|
+
})]
|
|
1314
|
+
}), jsx("div", {
|
|
1315
|
+
class: "bio-properties-panel-popup__title",
|
|
1316
|
+
children: title
|
|
1317
|
+
}), children]
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
function Body(props) {
|
|
1321
|
+
const {
|
|
1322
|
+
children,
|
|
1323
|
+
className,
|
|
1324
|
+
...rest
|
|
1325
|
+
} = props;
|
|
1326
|
+
return jsx("div", {
|
|
1327
|
+
class: classnames('bio-properties-panel-popup__body', className),
|
|
1328
|
+
...rest,
|
|
1329
|
+
children: children
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
function Footer(props) {
|
|
1333
|
+
const {
|
|
1334
|
+
children,
|
|
1335
|
+
className,
|
|
1336
|
+
...rest
|
|
1337
|
+
} = props;
|
|
1338
|
+
return jsx("div", {
|
|
1339
|
+
class: classnames('bio-properties-panel-popup__footer', className),
|
|
1340
|
+
...rest,
|
|
1341
|
+
children: props.children
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// helpers //////////////////////
|
|
1346
|
+
|
|
1347
|
+
function getPopupParent(node) {
|
|
1348
|
+
return node.closest('.bio-properties-panel-popup');
|
|
1349
|
+
}
|
|
1350
|
+
function cancel(event) {
|
|
1351
|
+
event.preventDefault();
|
|
1352
|
+
event.stopPropagation();
|
|
1353
|
+
}
|
|
1354
|
+
function getContainerNode(node) {
|
|
1355
|
+
if (typeof node === 'string') {
|
|
1356
|
+
return query(node);
|
|
1357
|
+
}
|
|
1358
|
+
return node;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const FEEL_POPUP_WIDTH = 700;
|
|
1362
|
+
const FEEL_POPUP_HEIGHT = 250;
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* FEEL popup component, built as a singleton. Emits lifecycle events as follows:
|
|
1366
|
+
* - `feelPopup.open` - fired before the popup is mounted
|
|
1367
|
+
* - `feelPopup.opened` - fired after the popup is mounted. Event context contains the DOM node of the popup
|
|
1368
|
+
* - `feelPopup.close` - fired before the popup is unmounted. Event context contains the DOM node of the popup
|
|
1369
|
+
* - `feelPopup.closed` - fired after the popup is unmounted
|
|
1370
|
+
*/
|
|
1371
|
+
function FEELPopupRoot(props) {
|
|
1372
|
+
const {
|
|
1373
|
+
element,
|
|
1374
|
+
eventBus = {
|
|
1375
|
+
fire() {},
|
|
1376
|
+
on() {},
|
|
1377
|
+
off() {}
|
|
1378
|
+
},
|
|
1379
|
+
popupContainer
|
|
1380
|
+
} = props;
|
|
1381
|
+
const prevElement = usePrevious(element);
|
|
1382
|
+
const [popupConfig, setPopupConfig] = useState({});
|
|
1383
|
+
const [open, setOpen] = useState(false);
|
|
1384
|
+
const [source, setSource] = useState(null);
|
|
1385
|
+
const [sourceElement, setSourceElement] = useState(null);
|
|
1386
|
+
const emit = (type, context) => {
|
|
1387
|
+
eventBus.fire('feelPopup.' + type, context);
|
|
1388
|
+
};
|
|
1389
|
+
const isOpen = useCallback(() => {
|
|
1390
|
+
return !!open;
|
|
1391
|
+
}, [open]);
|
|
1392
|
+
useUpdateEffect(() => {
|
|
1393
|
+
if (!open) {
|
|
1394
|
+
emit('closed');
|
|
1395
|
+
}
|
|
1396
|
+
}, [open]);
|
|
1397
|
+
const handleOpen = (entryId, config, _sourceElement) => {
|
|
1398
|
+
setSource(entryId);
|
|
1399
|
+
setPopupConfig(config);
|
|
1400
|
+
setOpen(true);
|
|
1401
|
+
setSourceElement(_sourceElement);
|
|
1402
|
+
emit('open');
|
|
1403
|
+
};
|
|
1404
|
+
const handleClose = () => {
|
|
1405
|
+
setOpen(false);
|
|
1406
|
+
setSource(null);
|
|
1407
|
+
};
|
|
1408
|
+
const feelPopupContext = {
|
|
1409
|
+
open: handleOpen,
|
|
1410
|
+
close: handleClose,
|
|
1411
|
+
source
|
|
1412
|
+
};
|
|
1413
|
+
|
|
1414
|
+
// close popup on element change, cf. https://github.com/bpmn-io/properties-panel/issues/270
|
|
1415
|
+
useEffect(() => {
|
|
1416
|
+
if (element && prevElement && element !== prevElement) {
|
|
1417
|
+
handleClose();
|
|
1418
|
+
}
|
|
1419
|
+
}, [element]);
|
|
1420
|
+
|
|
1421
|
+
// allow close and open via events
|
|
1422
|
+
useEffect(() => {
|
|
1423
|
+
const handlePopupOpen = context => {
|
|
1424
|
+
const {
|
|
1425
|
+
entryId,
|
|
1426
|
+
popupConfig,
|
|
1427
|
+
sourceElement
|
|
1428
|
+
} = context;
|
|
1429
|
+
handleOpen(entryId, popupConfig, sourceElement);
|
|
1430
|
+
};
|
|
1431
|
+
const handleIsOpen = () => {
|
|
1432
|
+
return isOpen();
|
|
1433
|
+
};
|
|
1434
|
+
eventBus.on('feelPopup._close', handleClose);
|
|
1435
|
+
eventBus.on('feelPopup._open', handlePopupOpen);
|
|
1436
|
+
eventBus.on('feelPopup._isOpen', handleIsOpen);
|
|
1437
|
+
return () => {
|
|
1438
|
+
eventBus.off('feelPopup._close', handleClose);
|
|
1439
|
+
eventBus.off('feelPopup._open', handleOpen);
|
|
1440
|
+
eventBus.off('feelPopup._isOpen', handleIsOpen);
|
|
1441
|
+
};
|
|
1442
|
+
}, [eventBus, isOpen]);
|
|
1443
|
+
return jsxs(FeelPopupContext.Provider, {
|
|
1444
|
+
value: feelPopupContext,
|
|
1445
|
+
children: [open && jsx(FeelPopupComponent, {
|
|
1446
|
+
onClose: handleClose,
|
|
1447
|
+
container: popupContainer,
|
|
1448
|
+
sourceElement: sourceElement,
|
|
1449
|
+
emit: emit,
|
|
1450
|
+
...popupConfig
|
|
1451
|
+
}), props.children]
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
function FeelPopupComponent(props) {
|
|
1455
|
+
const {
|
|
1456
|
+
container,
|
|
1457
|
+
id,
|
|
1458
|
+
hostLanguage,
|
|
1459
|
+
onInput,
|
|
1460
|
+
onClose,
|
|
1461
|
+
position,
|
|
1462
|
+
singleLine,
|
|
1463
|
+
sourceElement,
|
|
1464
|
+
title,
|
|
1465
|
+
tooltipContainer,
|
|
1466
|
+
type,
|
|
1467
|
+
value,
|
|
1468
|
+
variables,
|
|
1469
|
+
emit
|
|
1470
|
+
} = props;
|
|
1471
|
+
const editorRef = useRef();
|
|
1472
|
+
const popupRef = useRef();
|
|
1473
|
+
const isAutoCompletionOpen = useRef(false);
|
|
1474
|
+
const handleSetReturnFocus = () => {
|
|
1475
|
+
sourceElement && sourceElement.focus();
|
|
1476
|
+
};
|
|
1477
|
+
const onKeyDownCapture = event => {
|
|
1478
|
+
// we use capture here to make sure we handle the event before the editor does
|
|
1479
|
+
if (event.key === 'Escape') {
|
|
1480
|
+
isAutoCompletionOpen.current = autoCompletionOpen(event.target);
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
const onKeyDown = event => {
|
|
1484
|
+
if (event.key === 'Escape') {
|
|
1485
|
+
// close popup only if auto completion is not open
|
|
1486
|
+
// we need to do check this because the editor is not
|
|
1487
|
+
// stop propagating the keydown event
|
|
1488
|
+
// cf. https://discuss.codemirror.net/t/how-can-i-replace-the-default-autocompletion-keymap-v6/3322/5
|
|
1489
|
+
if (!isAutoCompletionOpen.current) {
|
|
1490
|
+
onClose();
|
|
1491
|
+
isAutoCompletionOpen.current = false;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
};
|
|
1495
|
+
useEffect(() => {
|
|
1496
|
+
emit('opened', {
|
|
1497
|
+
domNode: popupRef.current
|
|
1498
|
+
});
|
|
1499
|
+
return () => emit('close', {
|
|
1500
|
+
domNode: popupRef.current
|
|
1501
|
+
});
|
|
1502
|
+
}, []);
|
|
1503
|
+
return jsxs(Popup, {
|
|
1504
|
+
container: container,
|
|
1505
|
+
className: "bio-properties-panel-feel-popup",
|
|
1506
|
+
emit: emit,
|
|
1507
|
+
position: position,
|
|
1508
|
+
title: title,
|
|
1509
|
+
onClose: onClose
|
|
1510
|
+
|
|
1511
|
+
// handle focus manually on deactivate
|
|
1512
|
+
,
|
|
1513
|
+
returnFocus: false,
|
|
1514
|
+
closeOnEscape: false,
|
|
1515
|
+
delayInitialFocus: false,
|
|
1516
|
+
onPostDeactivate: handleSetReturnFocus,
|
|
1517
|
+
height: FEEL_POPUP_HEIGHT,
|
|
1518
|
+
width: FEEL_POPUP_WIDTH,
|
|
1519
|
+
ref: popupRef,
|
|
1520
|
+
children: [jsx(Popup.Title, {
|
|
1521
|
+
title: title,
|
|
1522
|
+
emit: emit,
|
|
1523
|
+
draggable: true
|
|
1524
|
+
}), jsx(Popup.Body, {
|
|
1525
|
+
children: jsxs("div", {
|
|
1526
|
+
onKeyDownCapture: onKeyDownCapture,
|
|
1527
|
+
onKeyDown: onKeyDown,
|
|
1528
|
+
class: "bio-properties-panel-feel-popup__body",
|
|
1529
|
+
children: [type === 'feel' && jsx(CodeEditor, {
|
|
1530
|
+
enableGutters: true,
|
|
1531
|
+
id: prefixId$8(id),
|
|
1532
|
+
name: id,
|
|
1533
|
+
onInput: onInput,
|
|
1534
|
+
value: value,
|
|
1535
|
+
variables: variables,
|
|
1536
|
+
ref: editorRef,
|
|
1537
|
+
tooltipContainer: tooltipContainer
|
|
1538
|
+
}), type === 'feelers' && jsx(CodeEditor$1, {
|
|
1539
|
+
id: prefixId$8(id),
|
|
1540
|
+
contentAttributes: {
|
|
1541
|
+
'aria-label': title
|
|
1542
|
+
},
|
|
1543
|
+
enableGutters: true,
|
|
1544
|
+
hostLanguage: hostLanguage,
|
|
1545
|
+
name: id,
|
|
1546
|
+
onInput: onInput,
|
|
1547
|
+
value: value,
|
|
1548
|
+
ref: editorRef,
|
|
1549
|
+
singleLine: singleLine,
|
|
1550
|
+
tooltipContainer: tooltipContainer
|
|
1551
|
+
})]
|
|
1552
|
+
})
|
|
1553
|
+
}), jsx(Popup.Footer, {
|
|
1554
|
+
children: jsx("button", {
|
|
1555
|
+
onClick: onClose,
|
|
1556
|
+
title: "Close pop-up editor",
|
|
1557
|
+
class: "bio-properties-panel-feel-popup__close-btn",
|
|
1558
|
+
children: "Close"
|
|
1559
|
+
})
|
|
1560
|
+
})]
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// helpers /////////////////
|
|
1565
|
+
|
|
1566
|
+
function prefixId$8(id) {
|
|
1567
|
+
return `bio-properties-panel-${id}`;
|
|
1568
|
+
}
|
|
1569
|
+
function autoCompletionOpen(element) {
|
|
1570
|
+
return element.closest('.cm-editor').querySelector('.cm-tooltip-autocomplete');
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
/**
|
|
1574
|
+
* This hook behaves like useEffect, but does not trigger on the first render.
|
|
1575
|
+
*
|
|
1576
|
+
* @param {Function} effect
|
|
1577
|
+
* @param {Array} deps
|
|
1578
|
+
*/
|
|
1579
|
+
function useUpdateEffect(effect, deps) {
|
|
1580
|
+
const isMounted = useRef(false);
|
|
1581
|
+
useEffect(() => {
|
|
1582
|
+
if (isMounted.current) {
|
|
1583
|
+
return effect();
|
|
1584
|
+
} else {
|
|
1585
|
+
isMounted.current = true;
|
|
1586
|
+
}
|
|
1587
|
+
}, deps);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function ToggleSwitch(props) {
|
|
1591
|
+
const {
|
|
1592
|
+
id,
|
|
1593
|
+
label,
|
|
1594
|
+
onInput,
|
|
1595
|
+
value,
|
|
1596
|
+
switcherLabel,
|
|
1597
|
+
inline,
|
|
1598
|
+
onFocus,
|
|
1599
|
+
onBlur,
|
|
1600
|
+
inputRef,
|
|
1601
|
+
tooltip
|
|
1602
|
+
} = props;
|
|
1603
|
+
const [localValue, setLocalValue] = useState(value);
|
|
1604
|
+
const handleInputCallback = async () => {
|
|
1605
|
+
onInput(!value);
|
|
1606
|
+
};
|
|
1607
|
+
const handleInput = e => {
|
|
1608
|
+
handleInputCallback();
|
|
1609
|
+
setLocalValue(e.target.value);
|
|
1610
|
+
};
|
|
1611
|
+
useEffect(() => {
|
|
1612
|
+
if (value === localValue) {
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
setLocalValue(value);
|
|
1616
|
+
}, [value]);
|
|
1617
|
+
return jsxs("div", {
|
|
1618
|
+
class: classnames('bio-properties-panel-toggle-switch', {
|
|
1619
|
+
inline
|
|
1620
|
+
}),
|
|
1621
|
+
children: [jsx("label", {
|
|
1622
|
+
class: "bio-properties-panel-label",
|
|
1623
|
+
for: prefixId$7(id),
|
|
1624
|
+
children: jsx(TooltipWrapper, {
|
|
1625
|
+
value: tooltip,
|
|
1626
|
+
forId: id,
|
|
1627
|
+
element: props.element,
|
|
1628
|
+
children: label
|
|
1629
|
+
})
|
|
1630
|
+
}), jsxs("div", {
|
|
1631
|
+
class: "bio-properties-panel-field-wrapper",
|
|
1632
|
+
children: [jsxs("label", {
|
|
1633
|
+
class: "bio-properties-panel-toggle-switch__switcher",
|
|
1634
|
+
children: [jsx("input", {
|
|
1635
|
+
ref: inputRef,
|
|
1636
|
+
id: prefixId$7(id),
|
|
1637
|
+
class: "bio-properties-panel-input",
|
|
1638
|
+
type: "checkbox",
|
|
1639
|
+
onFocus: onFocus,
|
|
1640
|
+
onBlur: onBlur,
|
|
1641
|
+
name: id,
|
|
1642
|
+
onInput: handleInput,
|
|
1643
|
+
checked: !!localValue
|
|
1644
|
+
}), jsx("span", {
|
|
1645
|
+
class: "bio-properties-panel-toggle-switch__slider"
|
|
1646
|
+
})]
|
|
1647
|
+
}), switcherLabel && jsx("p", {
|
|
1648
|
+
class: "bio-properties-panel-toggle-switch__label",
|
|
1649
|
+
children: switcherLabel
|
|
1650
|
+
})]
|
|
1651
|
+
})]
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/**
|
|
1656
|
+
* @param {Object} props
|
|
1657
|
+
* @param {Object} props.element
|
|
1658
|
+
* @param {String} props.id
|
|
1659
|
+
* @param {String} props.description
|
|
1660
|
+
* @param {String} props.label
|
|
1661
|
+
* @param {String} props.switcherLabel
|
|
1662
|
+
* @param {Boolean} props.inline
|
|
1663
|
+
* @param {Function} props.getValue
|
|
1664
|
+
* @param {Function} props.setValue
|
|
1665
|
+
* @param {Function} props.onFocus
|
|
1666
|
+
* @param {Function} props.onBlur
|
|
1667
|
+
* @param {string|import('preact').Component} props.tooltip
|
|
1668
|
+
*/
|
|
1669
|
+
function ToggleSwitchEntry(props) {
|
|
1670
|
+
const {
|
|
1671
|
+
element,
|
|
1672
|
+
id,
|
|
1673
|
+
description,
|
|
1674
|
+
label,
|
|
1675
|
+
switcherLabel,
|
|
1676
|
+
inline,
|
|
1677
|
+
getValue,
|
|
1678
|
+
setValue,
|
|
1679
|
+
onFocus,
|
|
1680
|
+
onBlur,
|
|
1681
|
+
tooltip
|
|
1682
|
+
} = props;
|
|
1683
|
+
const value = getValue(element);
|
|
1684
|
+
return jsxs("div", {
|
|
1685
|
+
class: "bio-properties-panel-entry bio-properties-panel-toggle-switch-entry",
|
|
1686
|
+
"data-entry-id": id,
|
|
1687
|
+
children: [jsx(ToggleSwitch, {
|
|
1688
|
+
id: id,
|
|
1689
|
+
label: label,
|
|
1690
|
+
value: value,
|
|
1691
|
+
onInput: setValue,
|
|
1692
|
+
onFocus: onFocus,
|
|
1693
|
+
onBlur: onBlur,
|
|
1694
|
+
switcherLabel: switcherLabel,
|
|
1695
|
+
inline: inline,
|
|
1696
|
+
tooltip: tooltip,
|
|
1697
|
+
element: element
|
|
1698
|
+
}), jsx(Description, {
|
|
1699
|
+
forId: id,
|
|
1700
|
+
element: element,
|
|
1701
|
+
value: description
|
|
1702
|
+
})]
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
function isEdited$8(node) {
|
|
1706
|
+
return node && !!node.checked;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// helpers /////////////////
|
|
1710
|
+
|
|
1711
|
+
function prefixId$7(id) {
|
|
1712
|
+
return `bio-properties-panel-${id}`;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
function NumberField(props) {
|
|
1716
|
+
const {
|
|
1717
|
+
debounce,
|
|
1718
|
+
disabled,
|
|
1719
|
+
displayLabel = true,
|
|
1720
|
+
id,
|
|
1721
|
+
inputRef,
|
|
1722
|
+
label,
|
|
1723
|
+
max,
|
|
1724
|
+
min,
|
|
1725
|
+
onInput,
|
|
1726
|
+
step,
|
|
1727
|
+
value = '',
|
|
1728
|
+
onFocus,
|
|
1729
|
+
onBlur
|
|
1730
|
+
} = props;
|
|
1731
|
+
const [localValue, setLocalValue] = useState(value);
|
|
1732
|
+
const handleInputCallback = useMemo(() => {
|
|
1733
|
+
return debounce(event => {
|
|
1734
|
+
const {
|
|
1735
|
+
validity,
|
|
1736
|
+
value
|
|
1737
|
+
} = event.target;
|
|
1738
|
+
if (validity.valid) {
|
|
1739
|
+
onInput(value ? parseFloat(value) : undefined);
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
}, [onInput, debounce]);
|
|
1743
|
+
const handleInput = e => {
|
|
1744
|
+
handleInputCallback(e);
|
|
1745
|
+
setLocalValue(e.target.value);
|
|
1746
|
+
};
|
|
1747
|
+
useEffect(() => {
|
|
1748
|
+
if (value === localValue) {
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
setLocalValue(value);
|
|
1752
|
+
}, [value]);
|
|
1753
|
+
return jsxs("div", {
|
|
1754
|
+
class: "bio-properties-panel-numberfield",
|
|
1755
|
+
children: [displayLabel && jsx("label", {
|
|
1756
|
+
for: prefixId$6(id),
|
|
1757
|
+
class: "bio-properties-panel-label",
|
|
1758
|
+
children: label
|
|
1759
|
+
}), jsx("input", {
|
|
1760
|
+
id: prefixId$6(id),
|
|
1761
|
+
ref: inputRef,
|
|
1762
|
+
type: "number",
|
|
1763
|
+
name: id,
|
|
1764
|
+
spellCheck: "false",
|
|
1765
|
+
autoComplete: "off",
|
|
1766
|
+
disabled: disabled,
|
|
1767
|
+
class: "bio-properties-panel-input",
|
|
1768
|
+
max: max,
|
|
1769
|
+
min: min,
|
|
1770
|
+
onInput: handleInput,
|
|
1771
|
+
onFocus: onFocus,
|
|
1772
|
+
onBlur: onBlur,
|
|
1773
|
+
step: step,
|
|
1774
|
+
value: localValue
|
|
1775
|
+
})]
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
/**
|
|
1780
|
+
* @param {Object} props
|
|
1781
|
+
* @param {Boolean} props.debounce
|
|
1782
|
+
* @param {String} props.description
|
|
1783
|
+
* @param {Boolean} props.disabled
|
|
1784
|
+
* @param {Object} props.element
|
|
1785
|
+
* @param {Function} props.getValue
|
|
1786
|
+
* @param {String} props.id
|
|
1787
|
+
* @param {String} props.label
|
|
1788
|
+
* @param {String} props.max
|
|
1789
|
+
* @param {String} props.min
|
|
1790
|
+
* @param {Function} props.setValue
|
|
1791
|
+
* @param {Function} props.onFocus
|
|
1792
|
+
* @param {Function} props.onBlur
|
|
1793
|
+
* @param {String} props.step
|
|
1794
|
+
* @param {Function} props.validate
|
|
1795
|
+
*/
|
|
1796
|
+
function NumberFieldEntry(props) {
|
|
1797
|
+
const {
|
|
1798
|
+
debounce,
|
|
1799
|
+
description,
|
|
1800
|
+
disabled,
|
|
1801
|
+
element,
|
|
1802
|
+
getValue,
|
|
1803
|
+
id,
|
|
1804
|
+
label,
|
|
1805
|
+
max,
|
|
1806
|
+
min,
|
|
1807
|
+
setValue,
|
|
1808
|
+
step,
|
|
1809
|
+
onFocus,
|
|
1810
|
+
onBlur,
|
|
1811
|
+
validate
|
|
1812
|
+
} = props;
|
|
1813
|
+
const globalError = useError(id);
|
|
1814
|
+
const [localError, setLocalError] = useState(null);
|
|
1815
|
+
let value = getValue(element);
|
|
1816
|
+
useEffect(() => {
|
|
1817
|
+
if (isFunction(validate)) {
|
|
1818
|
+
const newValidationError = validate(value) || null;
|
|
1819
|
+
setLocalError(newValidationError);
|
|
1820
|
+
}
|
|
1821
|
+
}, [value]);
|
|
1822
|
+
const onInput = newValue => {
|
|
1823
|
+
let newValidationError = null;
|
|
1824
|
+
if (isFunction(validate)) {
|
|
1825
|
+
newValidationError = validate(newValue) || null;
|
|
1826
|
+
}
|
|
1827
|
+
setValue(newValue, newValidationError);
|
|
1828
|
+
setLocalError(newValidationError);
|
|
1829
|
+
};
|
|
1830
|
+
const error = globalError || localError;
|
|
1831
|
+
return jsxs("div", {
|
|
1832
|
+
class: classnames('bio-properties-panel-entry', error ? 'has-error' : ''),
|
|
1833
|
+
"data-entry-id": id,
|
|
1834
|
+
children: [jsx(NumberField, {
|
|
1835
|
+
debounce: debounce,
|
|
1836
|
+
disabled: disabled,
|
|
1837
|
+
id: id,
|
|
1838
|
+
label: label,
|
|
1839
|
+
onFocus: onFocus,
|
|
1840
|
+
onBlur: onBlur,
|
|
1841
|
+
onInput: onInput,
|
|
1842
|
+
max: max,
|
|
1843
|
+
min: min,
|
|
1844
|
+
step: step,
|
|
1845
|
+
value: value
|
|
1846
|
+
}, element), error && jsx("div", {
|
|
1847
|
+
class: "bio-properties-panel-error",
|
|
1848
|
+
children: error
|
|
1849
|
+
}), jsx(Description, {
|
|
1850
|
+
forId: id,
|
|
1851
|
+
element: element,
|
|
1852
|
+
value: description
|
|
1853
|
+
})]
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
function isEdited$7(node) {
|
|
1857
|
+
return node && !!node.value;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
// helpers /////////////////
|
|
1861
|
+
|
|
1862
|
+
function prefixId$6(id) {
|
|
1863
|
+
return `bio-properties-panel-${id}`;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
const noop$2 = () => {};
|
|
1867
|
+
function FeelTextfield(props) {
|
|
1868
|
+
const {
|
|
1869
|
+
debounce,
|
|
1870
|
+
id,
|
|
1871
|
+
element,
|
|
1872
|
+
label,
|
|
1873
|
+
hostLanguage,
|
|
1874
|
+
onInput,
|
|
1875
|
+
onError,
|
|
1876
|
+
feel,
|
|
1877
|
+
value = '',
|
|
1878
|
+
disabled = false,
|
|
1879
|
+
variables,
|
|
1880
|
+
singleLine,
|
|
1881
|
+
tooltipContainer,
|
|
1882
|
+
OptionalComponent = OptionalFeelInput,
|
|
1883
|
+
tooltip
|
|
1884
|
+
} = props;
|
|
1885
|
+
const [localValue, _setLocalValue] = useState(value);
|
|
1886
|
+
const editorRef = useShowEntryEvent(id);
|
|
1887
|
+
const containerRef = useRef();
|
|
1888
|
+
const feelActive = isString(localValue) && localValue.startsWith('=') || feel === 'required';
|
|
1889
|
+
const feelOnlyValue = isString(localValue) && localValue.startsWith('=') ? localValue.substring(1) : localValue;
|
|
1890
|
+
const [focus, _setFocus] = useState(undefined);
|
|
1891
|
+
const {
|
|
1892
|
+
open: openPopup,
|
|
1893
|
+
source: popupSource
|
|
1894
|
+
} = useContext(FeelPopupContext);
|
|
1895
|
+
const popuOpen = popupSource === id;
|
|
1896
|
+
const setFocus = (offset = 0) => {
|
|
1897
|
+
const hasFocus = containerRef.current.contains(document.activeElement);
|
|
1898
|
+
|
|
1899
|
+
// Keep caret position if it is already focused, otherwise focus at the end
|
|
1900
|
+
const position = hasFocus ? document.activeElement.selectionStart : Infinity;
|
|
1901
|
+
_setFocus(position + offset);
|
|
1902
|
+
};
|
|
1903
|
+
const handleInputCallback = useMemo(() => {
|
|
1904
|
+
return debounce(newValue => {
|
|
1905
|
+
onInput(newValue);
|
|
1906
|
+
});
|
|
1907
|
+
}, [onInput, debounce]);
|
|
1908
|
+
const setLocalValue = newValue => {
|
|
1909
|
+
_setLocalValue(newValue);
|
|
1910
|
+
if (typeof newValue === 'undefined' || newValue === '' || newValue === '=') {
|
|
1911
|
+
handleInputCallback(undefined);
|
|
1912
|
+
} else {
|
|
1913
|
+
handleInputCallback(newValue);
|
|
1914
|
+
}
|
|
1915
|
+
};
|
|
1916
|
+
const handleFeelToggle = useStaticCallback(() => {
|
|
1917
|
+
if (feel === 'required') {
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
if (!feelActive) {
|
|
1921
|
+
setLocalValue('=' + localValue);
|
|
1922
|
+
} else {
|
|
1923
|
+
setLocalValue(feelOnlyValue);
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
const handleLocalInput = newValue => {
|
|
1927
|
+
if (feelActive) {
|
|
1928
|
+
newValue = '=' + newValue;
|
|
1929
|
+
}
|
|
1930
|
+
if (newValue === localValue) {
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
setLocalValue(newValue);
|
|
1934
|
+
if (!feelActive && isString(newValue) && newValue.startsWith('=')) {
|
|
1935
|
+
// focus is behind `=` sign that will be removed
|
|
1936
|
+
setFocus(-1);
|
|
1937
|
+
}
|
|
1938
|
+
};
|
|
1939
|
+
const handleLint = useStaticCallback(lint => {
|
|
1940
|
+
if (!(lint && lint.length)) {
|
|
1941
|
+
onError(undefined);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
const error = lint[0];
|
|
1945
|
+
const message = `${error.source}: ${error.message}`;
|
|
1946
|
+
onError(message);
|
|
1947
|
+
});
|
|
1948
|
+
const handlePopupOpen = (type = 'feel') => {
|
|
1949
|
+
const popupOptions = {
|
|
1950
|
+
id,
|
|
1951
|
+
hostLanguage,
|
|
1952
|
+
onInput: handleLocalInput,
|
|
1953
|
+
position: calculatePopupPosition(containerRef.current),
|
|
1954
|
+
singleLine,
|
|
1955
|
+
title: getPopupTitle(element, label),
|
|
1956
|
+
tooltipContainer,
|
|
1957
|
+
type,
|
|
1958
|
+
value: feelOnlyValue,
|
|
1959
|
+
variables
|
|
1960
|
+
};
|
|
1961
|
+
openPopup(id, popupOptions, editorRef.current);
|
|
1962
|
+
};
|
|
1963
|
+
useEffect(() => {
|
|
1964
|
+
if (typeof focus !== 'undefined') {
|
|
1965
|
+
editorRef.current.focus(focus);
|
|
1966
|
+
_setFocus(undefined);
|
|
1967
|
+
}
|
|
1968
|
+
}, [focus]);
|
|
1969
|
+
useEffect(() => {
|
|
1970
|
+
if (value === localValue) {
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// External value change removed content => keep FEEL configuration
|
|
1975
|
+
if (!value) {
|
|
1976
|
+
setLocalValue(feelActive ? '=' : '');
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
setLocalValue(value);
|
|
1980
|
+
}, [value]);
|
|
1981
|
+
|
|
1982
|
+
// copy-paste integration
|
|
1983
|
+
useEffect(() => {
|
|
1984
|
+
const copyHandler = event => {
|
|
1985
|
+
if (!feelActive) {
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
event.clipboardData.setData('application/FEEL', event.clipboardData.getData('text'));
|
|
1989
|
+
};
|
|
1990
|
+
const pasteHandler = event => {
|
|
1991
|
+
if (feelActive || popuOpen) {
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
const data = event.clipboardData.getData('application/FEEL');
|
|
1995
|
+
if (data) {
|
|
1996
|
+
setTimeout(() => {
|
|
1997
|
+
handleFeelToggle();
|
|
1998
|
+
setFocus();
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
};
|
|
2002
|
+
containerRef.current.addEventListener('copy', copyHandler);
|
|
2003
|
+
containerRef.current.addEventListener('cut', copyHandler);
|
|
2004
|
+
containerRef.current.addEventListener('paste', pasteHandler);
|
|
2005
|
+
return () => {
|
|
2006
|
+
containerRef.current.removeEventListener('copy', copyHandler);
|
|
2007
|
+
containerRef.current.removeEventListener('cut', copyHandler);
|
|
2008
|
+
containerRef.current.removeEventListener('paste', pasteHandler);
|
|
2009
|
+
};
|
|
2010
|
+
}, [containerRef, feelActive, handleFeelToggle, setFocus]);
|
|
2011
|
+
return jsxs("div", {
|
|
2012
|
+
class: classnames('bio-properties-panel-feel-entry', {
|
|
2013
|
+
'feel-active': feelActive
|
|
2014
|
+
}),
|
|
2015
|
+
children: [jsxs("label", {
|
|
2016
|
+
for: prefixId$5(id),
|
|
2017
|
+
class: "bio-properties-panel-label",
|
|
2018
|
+
onClick: () => setFocus(),
|
|
2019
|
+
children: [jsx(TooltipWrapper, {
|
|
2020
|
+
value: tooltip,
|
|
2021
|
+
forId: id,
|
|
2022
|
+
element: props.element,
|
|
2023
|
+
children: label
|
|
2024
|
+
}), jsx(FeelIcon, {
|
|
2025
|
+
label: label,
|
|
2026
|
+
feel: feel,
|
|
2027
|
+
onClick: handleFeelToggle,
|
|
2028
|
+
active: feelActive
|
|
2029
|
+
})]
|
|
2030
|
+
}), jsxs("div", {
|
|
2031
|
+
class: "bio-properties-panel-feel-container",
|
|
2032
|
+
ref: containerRef,
|
|
2033
|
+
children: [jsx(FeelIndicator, {
|
|
2034
|
+
active: feelActive,
|
|
2035
|
+
disabled: feel !== 'optional' || disabled,
|
|
2036
|
+
onClick: handleFeelToggle
|
|
2037
|
+
}), feelActive ? jsx(CodeEditor, {
|
|
2038
|
+
id: prefixId$5(id),
|
|
2039
|
+
name: id,
|
|
2040
|
+
onInput: handleLocalInput,
|
|
2041
|
+
disabled: disabled,
|
|
2042
|
+
popupOpen: popuOpen,
|
|
2043
|
+
onFeelToggle: () => {
|
|
2044
|
+
handleFeelToggle();
|
|
2045
|
+
setFocus(true);
|
|
2046
|
+
},
|
|
2047
|
+
onLint: handleLint,
|
|
2048
|
+
onPopupOpen: handlePopupOpen,
|
|
2049
|
+
value: feelOnlyValue,
|
|
2050
|
+
variables: variables,
|
|
2051
|
+
ref: editorRef,
|
|
2052
|
+
tooltipContainer: tooltipContainer
|
|
2053
|
+
}) : jsx(OptionalComponent, {
|
|
2054
|
+
...props,
|
|
2055
|
+
popupOpen: popuOpen,
|
|
2056
|
+
onInput: handleLocalInput,
|
|
2057
|
+
contentAttributes: {
|
|
2058
|
+
'id': prefixId$5(id),
|
|
2059
|
+
'aria-label': label
|
|
2060
|
+
},
|
|
2061
|
+
value: localValue,
|
|
2062
|
+
ref: editorRef,
|
|
2063
|
+
onPopupOpen: handlePopupOpen,
|
|
2064
|
+
containerRef: containerRef
|
|
2065
|
+
})]
|
|
2066
|
+
})]
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
const OptionalFeelInput = forwardRef((props, ref) => {
|
|
2070
|
+
const {
|
|
2071
|
+
id,
|
|
2072
|
+
disabled,
|
|
2073
|
+
onInput,
|
|
2074
|
+
value,
|
|
2075
|
+
onFocus,
|
|
2076
|
+
onBlur
|
|
2077
|
+
} = props;
|
|
2078
|
+
const inputRef = useRef();
|
|
2079
|
+
|
|
2080
|
+
// To be consistent with the FEEL editor, set focus at start of input
|
|
2081
|
+
// this ensures clean editing experience when switching with the keyboard
|
|
2082
|
+
ref.current = {
|
|
2083
|
+
focus: position => {
|
|
2084
|
+
const input = inputRef.current;
|
|
2085
|
+
if (!input) {
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
input.focus();
|
|
2089
|
+
if (typeof position === 'number') {
|
|
2090
|
+
if (position > value.length) {
|
|
2091
|
+
position = value.length;
|
|
2092
|
+
}
|
|
2093
|
+
input.setSelectionRange(position, position);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
return jsx("input", {
|
|
2098
|
+
id: prefixId$5(id),
|
|
2099
|
+
type: "text",
|
|
2100
|
+
ref: inputRef,
|
|
2101
|
+
name: id,
|
|
2102
|
+
spellCheck: "false",
|
|
2103
|
+
autoComplete: "off",
|
|
2104
|
+
disabled: disabled,
|
|
2105
|
+
class: "bio-properties-panel-input",
|
|
2106
|
+
onInput: e => onInput(e.target.value),
|
|
2107
|
+
onFocus: onFocus,
|
|
2108
|
+
onBlur: onBlur,
|
|
2109
|
+
value: value || ''
|
|
2110
|
+
});
|
|
2111
|
+
});
|
|
2112
|
+
const OptionalFeelNumberField = forwardRef((props, ref) => {
|
|
2113
|
+
const {
|
|
2114
|
+
id,
|
|
2115
|
+
debounce,
|
|
2116
|
+
disabled,
|
|
2117
|
+
onInput,
|
|
2118
|
+
value,
|
|
2119
|
+
min,
|
|
2120
|
+
max,
|
|
2121
|
+
step,
|
|
2122
|
+
onFocus,
|
|
2123
|
+
onBlur
|
|
2124
|
+
} = props;
|
|
2125
|
+
const inputRef = useRef();
|
|
2126
|
+
|
|
2127
|
+
// To be consistent with the FEEL editor, set focus at start of input
|
|
2128
|
+
// this ensures clean editing experience when switching with the keyboard
|
|
2129
|
+
ref.current = {
|
|
2130
|
+
focus: position => {
|
|
2131
|
+
const input = inputRef.current;
|
|
2132
|
+
if (!input) {
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
input.focus();
|
|
2136
|
+
if (typeof position === 'number' && position !== Infinity) {
|
|
2137
|
+
if (position > value.length) {
|
|
2138
|
+
position = value.length;
|
|
2139
|
+
}
|
|
2140
|
+
input.setSelectionRange(position, position);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
return jsx(NumberField, {
|
|
2145
|
+
id: id,
|
|
2146
|
+
debounce: debounce,
|
|
2147
|
+
disabled: disabled,
|
|
2148
|
+
displayLabel: false,
|
|
2149
|
+
inputRef: inputRef,
|
|
2150
|
+
max: max,
|
|
2151
|
+
min: min,
|
|
2152
|
+
onInput: onInput,
|
|
2153
|
+
step: step,
|
|
2154
|
+
value: value,
|
|
2155
|
+
onFocus: onFocus,
|
|
2156
|
+
onBlur: onBlur
|
|
2157
|
+
});
|
|
2158
|
+
});
|
|
2159
|
+
const OptionalFeelTextArea = forwardRef((props, ref) => {
|
|
2160
|
+
const {
|
|
2161
|
+
id,
|
|
2162
|
+
disabled,
|
|
2163
|
+
onInput,
|
|
2164
|
+
value,
|
|
2165
|
+
onFocus,
|
|
2166
|
+
onBlur
|
|
2167
|
+
} = props;
|
|
2168
|
+
const inputRef = useRef();
|
|
2169
|
+
|
|
2170
|
+
// To be consistent with the FEEL editor, set focus at start of input
|
|
2171
|
+
// this ensures clean editing experience when switching with the keyboard
|
|
2172
|
+
ref.current = {
|
|
2173
|
+
focus: () => {
|
|
2174
|
+
const input = inputRef.current;
|
|
2175
|
+
if (!input) {
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
input.focus();
|
|
2179
|
+
input.setSelectionRange(0, 0);
|
|
2180
|
+
}
|
|
2181
|
+
};
|
|
2182
|
+
return jsx("textarea", {
|
|
2183
|
+
id: prefixId$5(id),
|
|
2184
|
+
type: "text",
|
|
2185
|
+
ref: inputRef,
|
|
2186
|
+
name: id,
|
|
2187
|
+
spellCheck: "false",
|
|
2188
|
+
autoComplete: "off",
|
|
2189
|
+
disabled: disabled,
|
|
2190
|
+
class: "bio-properties-panel-input",
|
|
2191
|
+
onInput: e => onInput(e.target.value),
|
|
2192
|
+
onFocus: onFocus,
|
|
2193
|
+
onBlur: onBlur,
|
|
2194
|
+
value: value || '',
|
|
2195
|
+
"data-gramm": "false"
|
|
2196
|
+
});
|
|
2197
|
+
});
|
|
2198
|
+
const OptionalFeelToggleSwitch = forwardRef((props, ref) => {
|
|
2199
|
+
const {
|
|
2200
|
+
id,
|
|
2201
|
+
onInput,
|
|
2202
|
+
value,
|
|
2203
|
+
onFocus,
|
|
2204
|
+
onBlur,
|
|
2205
|
+
switcherLabel
|
|
2206
|
+
} = props;
|
|
2207
|
+
const inputRef = useRef();
|
|
2208
|
+
|
|
2209
|
+
// To be consistent with the FEEL editor, set focus at start of input
|
|
2210
|
+
// this ensures clean editing experience when switching with the keyboard
|
|
2211
|
+
ref.current = {
|
|
2212
|
+
focus: () => {
|
|
2213
|
+
const input = inputRef.current;
|
|
2214
|
+
if (!input) {
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
input.focus();
|
|
2218
|
+
}
|
|
2219
|
+
};
|
|
2220
|
+
return jsx(ToggleSwitch, {
|
|
2221
|
+
id: id,
|
|
2222
|
+
value: value,
|
|
2223
|
+
inputRef: inputRef,
|
|
2224
|
+
onInput: onInput,
|
|
2225
|
+
onFocus: onFocus,
|
|
2226
|
+
onBlur: onBlur,
|
|
2227
|
+
switcherLabel: switcherLabel
|
|
2228
|
+
});
|
|
2229
|
+
});
|
|
2230
|
+
const OptionalFeelCheckbox = forwardRef((props, ref) => {
|
|
2231
|
+
const {
|
|
2232
|
+
id,
|
|
2233
|
+
disabled,
|
|
2234
|
+
onInput,
|
|
2235
|
+
value,
|
|
2236
|
+
onFocus,
|
|
2237
|
+
onBlur
|
|
2238
|
+
} = props;
|
|
2239
|
+
const inputRef = useRef();
|
|
2240
|
+
const handleChange = ({
|
|
2241
|
+
target
|
|
2242
|
+
}) => {
|
|
2243
|
+
onInput(target.checked);
|
|
2244
|
+
};
|
|
2245
|
+
|
|
2246
|
+
// To be consistent with the FEEL editor, set focus at start of input
|
|
2247
|
+
// this ensures clean editing experience when switching with the keyboard
|
|
2248
|
+
ref.current = {
|
|
2249
|
+
focus: () => {
|
|
2250
|
+
const input = inputRef.current;
|
|
2251
|
+
if (!input) {
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
input.focus();
|
|
2255
|
+
}
|
|
2256
|
+
};
|
|
2257
|
+
return jsx("input", {
|
|
2258
|
+
ref: inputRef,
|
|
2259
|
+
id: prefixId$5(id),
|
|
2260
|
+
name: id,
|
|
2261
|
+
onFocus: onFocus,
|
|
2262
|
+
onBlur: onBlur,
|
|
2263
|
+
type: "checkbox",
|
|
2264
|
+
class: "bio-properties-panel-input",
|
|
2265
|
+
onChange: handleChange,
|
|
2266
|
+
checked: value,
|
|
2267
|
+
disabled: disabled
|
|
2268
|
+
});
|
|
2269
|
+
});
|
|
2270
|
+
|
|
2271
|
+
/**
|
|
2272
|
+
* @param {Object} props
|
|
2273
|
+
* @param {Object} props.element
|
|
2274
|
+
* @param {String} props.id
|
|
2275
|
+
* @param {String} props.description
|
|
2276
|
+
* @param {Boolean} props.debounce
|
|
2277
|
+
* @param {Boolean} props.disabled
|
|
2278
|
+
* @param {Boolean} props.feel
|
|
2279
|
+
* @param {String} props.label
|
|
2280
|
+
* @param {Function} props.getValue
|
|
2281
|
+
* @param {Function} props.setValue
|
|
2282
|
+
* @param {Function} props.tooltipContainer
|
|
2283
|
+
* @param {Function} props.validate
|
|
2284
|
+
* @param {Function} props.show
|
|
2285
|
+
* @param {Function} props.example
|
|
2286
|
+
* @param {Function} props.variables
|
|
2287
|
+
* @param {Function} props.onFocus
|
|
2288
|
+
* @param {Function} props.onBlur
|
|
2289
|
+
* @param {string|import('preact').Component} props.tooltip
|
|
2290
|
+
*/
|
|
2291
|
+
function FeelEntry(props) {
|
|
2292
|
+
const {
|
|
2293
|
+
element,
|
|
2294
|
+
id,
|
|
2295
|
+
description,
|
|
2296
|
+
debounce,
|
|
2297
|
+
disabled,
|
|
2298
|
+
feel,
|
|
2299
|
+
label,
|
|
2300
|
+
getValue,
|
|
2301
|
+
setValue,
|
|
2302
|
+
tooltipContainer,
|
|
2303
|
+
hostLanguage,
|
|
2304
|
+
singleLine,
|
|
2305
|
+
validate,
|
|
2306
|
+
show = noop$2,
|
|
2307
|
+
example,
|
|
2308
|
+
variables,
|
|
2309
|
+
onFocus,
|
|
2310
|
+
onBlur,
|
|
2311
|
+
tooltip
|
|
2312
|
+
} = props;
|
|
2313
|
+
const [validationError, setValidationError] = useState(null);
|
|
2314
|
+
const [localError, setLocalError] = useState(null);
|
|
2315
|
+
let value = getValue(element);
|
|
2316
|
+
useEffect(() => {
|
|
2317
|
+
if (isFunction(validate)) {
|
|
2318
|
+
const newValidationError = validate(value) || null;
|
|
2319
|
+
setValidationError(newValidationError);
|
|
2320
|
+
}
|
|
2321
|
+
}, [value]);
|
|
2322
|
+
const onInput = useStaticCallback(newValue => {
|
|
2323
|
+
let newValidationError = null;
|
|
2324
|
+
if (isFunction(validate)) {
|
|
2325
|
+
newValidationError = validate(newValue) || null;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
// don't create multiple commandStack entries for the same value
|
|
2329
|
+
if (newValue !== value) {
|
|
2330
|
+
setValue(newValue, newValidationError);
|
|
2331
|
+
}
|
|
2332
|
+
setValidationError(newValidationError);
|
|
2333
|
+
});
|
|
2334
|
+
const onError = useCallback(err => {
|
|
2335
|
+
setLocalError(err);
|
|
2336
|
+
}, []);
|
|
2337
|
+
const temporaryError = useError(id);
|
|
2338
|
+
const error = temporaryError || localError || validationError;
|
|
2339
|
+
return jsxs("div", {
|
|
2340
|
+
class: classnames(props.class, 'bio-properties-panel-entry', error ? 'has-error' : ''),
|
|
2341
|
+
"data-entry-id": id,
|
|
2342
|
+
children: [createElement(FeelTextfield, {
|
|
2343
|
+
...props,
|
|
2344
|
+
debounce: debounce,
|
|
2345
|
+
disabled: disabled,
|
|
2346
|
+
feel: feel,
|
|
2347
|
+
id: id,
|
|
2348
|
+
key: element,
|
|
2349
|
+
label: label,
|
|
2350
|
+
onInput: onInput,
|
|
2351
|
+
onError: onError,
|
|
2352
|
+
onFocus: onFocus,
|
|
2353
|
+
onBlur: onBlur,
|
|
2354
|
+
example: example,
|
|
2355
|
+
hostLanguage: hostLanguage,
|
|
2356
|
+
singleLine: singleLine,
|
|
2357
|
+
show: show,
|
|
2358
|
+
value: value,
|
|
2359
|
+
variables: variables,
|
|
2360
|
+
tooltipContainer: tooltipContainer,
|
|
2361
|
+
OptionalComponent: props.OptionalComponent,
|
|
2362
|
+
tooltip: tooltip
|
|
2363
|
+
}), error && jsx("div", {
|
|
2364
|
+
class: "bio-properties-panel-error",
|
|
2365
|
+
children: error
|
|
2366
|
+
}), jsx(Description, {
|
|
2367
|
+
forId: id,
|
|
2368
|
+
element: element,
|
|
2369
|
+
value: description
|
|
2370
|
+
})]
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
/**
|
|
2375
|
+
* @param {Object} props
|
|
2376
|
+
* @param {Object} props.element
|
|
2377
|
+
* @param {String} props.id
|
|
2378
|
+
* @param {String} props.description
|
|
2379
|
+
* @param {Boolean} props.debounce
|
|
2380
|
+
* @param {Boolean} props.disabled
|
|
2381
|
+
* @param {String} props.max
|
|
2382
|
+
* @param {String} props.min
|
|
2383
|
+
* @param {String} props.step
|
|
2384
|
+
* @param {Boolean} props.feel
|
|
2385
|
+
* @param {String} props.label
|
|
2386
|
+
* @param {Function} props.getValue
|
|
2387
|
+
* @param {Function} props.setValue
|
|
2388
|
+
* @param {Function} props.tooltipContainer
|
|
2389
|
+
* @param {Function} props.validate
|
|
2390
|
+
* @param {Function} props.show
|
|
2391
|
+
* @param {Function} props.example
|
|
2392
|
+
* @param {Function} props.variables
|
|
2393
|
+
* @param {Function} props.onFocus
|
|
2394
|
+
* @param {Function} props.onBlur
|
|
2395
|
+
*/
|
|
2396
|
+
function FeelNumberEntry(props) {
|
|
2397
|
+
return jsx(FeelEntry, {
|
|
2398
|
+
class: "bio-properties-panel-feel-number",
|
|
2399
|
+
OptionalComponent: OptionalFeelNumberField,
|
|
2400
|
+
...props
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
/**
|
|
2405
|
+
* @param {Object} props
|
|
2406
|
+
* @param {Object} props.element
|
|
2407
|
+
* @param {String} props.id
|
|
2408
|
+
* @param {String} props.description
|
|
2409
|
+
* @param {Boolean} props.debounce
|
|
2410
|
+
* @param {Boolean} props.disabled
|
|
2411
|
+
* @param {Boolean} props.feel
|
|
2412
|
+
* @param {String} props.label
|
|
2413
|
+
* @param {Function} props.getValue
|
|
2414
|
+
* @param {Function} props.setValue
|
|
2415
|
+
* @param {Function} props.tooltipContainer
|
|
2416
|
+
* @param {Function} props.validate
|
|
2417
|
+
* @param {Function} props.show
|
|
2418
|
+
* @param {Function} props.example
|
|
2419
|
+
* @param {Function} props.variables
|
|
2420
|
+
* @param {Function} props.onFocus
|
|
2421
|
+
* @param {Function} props.onBlur
|
|
2422
|
+
*/
|
|
2423
|
+
function FeelTextAreaEntry(props) {
|
|
2424
|
+
return jsx(FeelEntry, {
|
|
2425
|
+
class: "bio-properties-panel-feel-textarea",
|
|
2426
|
+
OptionalComponent: OptionalFeelTextArea,
|
|
2427
|
+
...props
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
/**
|
|
2432
|
+
* @param {Object} props
|
|
2433
|
+
* @param {Object} props.element
|
|
2434
|
+
* @param {String} props.id
|
|
2435
|
+
* @param {String} props.description
|
|
2436
|
+
* @param {Boolean} props.debounce
|
|
2437
|
+
* @param {Boolean} props.disabled
|
|
2438
|
+
* @param {Boolean} props.feel
|
|
2439
|
+
* @param {String} props.label
|
|
2440
|
+
* @param {Function} props.getValue
|
|
2441
|
+
* @param {Function} props.setValue
|
|
2442
|
+
* @param {Function} props.tooltipContainer
|
|
2443
|
+
* @param {Function} props.validate
|
|
2444
|
+
* @param {Function} props.show
|
|
2445
|
+
* @param {Function} props.example
|
|
2446
|
+
* @param {Function} props.variables
|
|
2447
|
+
* @param {Function} props.onFocus
|
|
2448
|
+
* @param {Function} props.onBlur
|
|
2449
|
+
*/
|
|
2450
|
+
function FeelToggleSwitchEntry(props) {
|
|
2451
|
+
return jsx(FeelEntry, {
|
|
2452
|
+
class: "bio-properties-panel-feel-toggle-switch",
|
|
2453
|
+
OptionalComponent: OptionalFeelToggleSwitch,
|
|
2454
|
+
...props
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
/**
|
|
2459
|
+
* @param {Object} props
|
|
2460
|
+
* @param {Object} props.element
|
|
2461
|
+
* @param {String} props.id
|
|
2462
|
+
* @param {String} props.description
|
|
2463
|
+
* @param {Boolean} props.debounce
|
|
2464
|
+
* @param {Boolean} props.disabled
|
|
2465
|
+
* @param {Boolean} props.feel
|
|
2466
|
+
* @param {String} props.label
|
|
2467
|
+
* @param {Function} props.getValue
|
|
2468
|
+
* @param {Function} props.setValue
|
|
2469
|
+
* @param {Function} props.tooltipContainer
|
|
2470
|
+
* @param {Function} props.validate
|
|
2471
|
+
* @param {Function} props.show
|
|
2472
|
+
* @param {Function} props.example
|
|
2473
|
+
* @param {Function} props.variables
|
|
2474
|
+
* @param {Function} props.onFocus
|
|
2475
|
+
* @param {Function} props.onBlur
|
|
2476
|
+
*/
|
|
2477
|
+
function FeelCheckboxEntry(props) {
|
|
2478
|
+
return jsx(FeelEntry, {
|
|
2479
|
+
class: "bio-properties-panel-feel-checkbox",
|
|
2480
|
+
OptionalComponent: OptionalFeelCheckbox,
|
|
2481
|
+
...props
|
|
2482
|
+
});
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
/**
|
|
2486
|
+
* @param {Object} props
|
|
2487
|
+
* @param {Object} props.element
|
|
2488
|
+
* @param {String} props.id
|
|
2489
|
+
* @param {String} props.description
|
|
2490
|
+
* @param {String} props.hostLanguage
|
|
2491
|
+
* @param {Boolean} props.singleLine
|
|
2492
|
+
* @param {Boolean} props.debounce
|
|
2493
|
+
* @param {Boolean} props.disabled
|
|
2494
|
+
* @param {Boolean} props.feel
|
|
2495
|
+
* @param {String} props.label
|
|
2496
|
+
* @param {Function} props.getValue
|
|
2497
|
+
* @param {Function} props.setValue
|
|
2498
|
+
* @param {Function} props.tooltipContainer
|
|
2499
|
+
* @param {Function} props.validate
|
|
2500
|
+
* @param {Function} props.show
|
|
2501
|
+
* @param {Function} props.example
|
|
2502
|
+
* @param {Function} props.variables
|
|
2503
|
+
* @param {Function} props.onFocus
|
|
2504
|
+
* @param {Function} props.onBlur
|
|
2505
|
+
*/
|
|
2506
|
+
function FeelTemplatingEntry(props) {
|
|
2507
|
+
return jsx(FeelEntry, {
|
|
2508
|
+
class: "bio-properties-panel-feel-templating",
|
|
2509
|
+
OptionalComponent: CodeEditor$1,
|
|
2510
|
+
...props
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
function isEdited$6(node) {
|
|
2514
|
+
if (!node) {
|
|
2515
|
+
return false;
|
|
2516
|
+
}
|
|
2517
|
+
if (node.type === 'checkbox') {
|
|
2518
|
+
return !!node.checked || node.classList.contains('edited');
|
|
2519
|
+
}
|
|
2520
|
+
return !!node.value || node.classList.contains('edited');
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
// helpers /////////////////
|
|
2524
|
+
|
|
2525
|
+
function prefixId$5(id) {
|
|
2526
|
+
return `bio-properties-panel-${id}`;
|
|
2527
|
+
}
|
|
2528
|
+
function calculatePopupPosition(element) {
|
|
2529
|
+
const {
|
|
2530
|
+
top,
|
|
2531
|
+
left
|
|
2532
|
+
} = element.getBoundingClientRect();
|
|
2533
|
+
return {
|
|
2534
|
+
left: left - FEEL_POPUP_WIDTH - 20,
|
|
2535
|
+
top: top
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// todo(pinussilvestrus): make this configurable in the future
|
|
2540
|
+
function getPopupTitle(element, label) {
|
|
2541
|
+
let popupTitle = '';
|
|
2542
|
+
if (element && element.type) {
|
|
2543
|
+
popupTitle = `${element.type} / `;
|
|
2544
|
+
}
|
|
2545
|
+
return `${popupTitle}${label}`;
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
const DEFAULT_LAYOUT = {};
|
|
2549
|
+
const DEFAULT_DESCRIPTION = {};
|
|
2550
|
+
const DEFAULT_TOOLTIP = {};
|
|
2551
|
+
|
|
2552
|
+
/**
|
|
2553
|
+
* @typedef { {
|
|
2554
|
+
* component: import('preact').Component,
|
|
2555
|
+
* id: String,
|
|
2556
|
+
* isEdited?: Function
|
|
2557
|
+
* } } EntryDefinition
|
|
2558
|
+
*
|
|
2559
|
+
* @typedef { {
|
|
2560
|
+
* autoFocusEntry: String,
|
|
2561
|
+
* autoOpen?: Boolean,
|
|
2562
|
+
* entries: Array<EntryDefinition>,
|
|
2563
|
+
* id: String,
|
|
2564
|
+
* label: String,
|
|
2565
|
+
* remove: (event: MouseEvent) => void
|
|
2566
|
+
* } } ListItemDefinition
|
|
2567
|
+
*
|
|
2568
|
+
* @typedef { {
|
|
2569
|
+
* add: (event: MouseEvent) => void,
|
|
2570
|
+
* component: import('preact').Component,
|
|
2571
|
+
* element: Object,
|
|
2572
|
+
* id: String,
|
|
2573
|
+
* items: Array<ListItemDefinition>,
|
|
2574
|
+
* label: String,
|
|
2575
|
+
* shouldSort?: Boolean,
|
|
2576
|
+
* shouldOpen?: Boolean
|
|
2577
|
+
* } } ListGroupDefinition
|
|
2578
|
+
*
|
|
2579
|
+
* @typedef { {
|
|
2580
|
+
* component?: import('preact').Component,
|
|
2581
|
+
* entries: Array<EntryDefinition>,
|
|
2582
|
+
* id: String,
|
|
2583
|
+
* label: String,
|
|
2584
|
+
* shouldOpen?: Boolean
|
|
2585
|
+
* } } GroupDefinition
|
|
2586
|
+
*
|
|
2587
|
+
* @typedef { {
|
|
2588
|
+
* [id: String]: GetDescriptionFunction
|
|
2589
|
+
* } } DescriptionConfig
|
|
2590
|
+
*
|
|
2591
|
+
* @typedef { {
|
|
2592
|
+
* [id: String]: GetTooltipFunction
|
|
2593
|
+
* } } TooltipConfig
|
|
2594
|
+
*
|
|
2595
|
+
* @callback { {
|
|
2596
|
+
* @param {string} id
|
|
2597
|
+
* @param {Object} element
|
|
2598
|
+
* @returns {string}
|
|
2599
|
+
* } } GetDescriptionFunction
|
|
2600
|
+
*
|
|
2601
|
+
* @callback { {
|
|
2602
|
+
* @param {string} id
|
|
2603
|
+
* @param {Object} element
|
|
2604
|
+
* @returns {string}
|
|
2605
|
+
* } } GetTooltipFunction
|
|
2606
|
+
*
|
|
2607
|
+
* @typedef { {
|
|
2608
|
+
* getEmpty: (element: object) => import('./components/Placeholder').PlaceholderDefinition,
|
|
2609
|
+
* getMultiple: (element: Object) => import('./components/Placeholder').PlaceholderDefinition
|
|
2610
|
+
* } } PlaceholderProvider
|
|
2611
|
+
*
|
|
2612
|
+
*/
|
|
2613
|
+
|
|
2614
|
+
/**
|
|
2615
|
+
* A basic properties panel component. Describes *how* content will be rendered, accepts
|
|
2616
|
+
* data from implementor to describe *what* will be rendered.
|
|
2617
|
+
*
|
|
2618
|
+
* @param {Object} props
|
|
2619
|
+
* @param {Object|Array} props.element
|
|
2620
|
+
* @param {import('./components/Header').HeaderProvider} props.headerProvider
|
|
2621
|
+
* @param {PlaceholderProvider} [props.placeholderProvider]
|
|
2622
|
+
* @param {Array<GroupDefinition|ListGroupDefinition>} props.groups
|
|
2623
|
+
* @param {Object} [props.layoutConfig]
|
|
2624
|
+
* @param {Function} [props.layoutChanged]
|
|
2625
|
+
* @param {DescriptionConfig} [props.descriptionConfig]
|
|
2626
|
+
* @param {Function} [props.descriptionLoaded]
|
|
2627
|
+
* @param {TooltipConfig} [props.tooltipConfig]
|
|
2628
|
+
* @param {Function} [props.tooltipLoaded]
|
|
2629
|
+
* @param {HTMLElement} [props.feelPopupContainer]
|
|
2630
|
+
* @param {Object} [props.eventBus]
|
|
2631
|
+
*/
|
|
2632
|
+
function PropertiesPanel(props) {
|
|
2633
|
+
const {
|
|
2634
|
+
element,
|
|
2635
|
+
headerProvider,
|
|
2636
|
+
placeholderProvider,
|
|
2637
|
+
groups,
|
|
2638
|
+
layoutConfig,
|
|
2639
|
+
layoutChanged,
|
|
2640
|
+
descriptionConfig,
|
|
2641
|
+
descriptionLoaded,
|
|
2642
|
+
tooltipConfig,
|
|
2643
|
+
tooltipLoaded,
|
|
2644
|
+
feelPopupContainer,
|
|
2645
|
+
eventBus
|
|
2646
|
+
} = props;
|
|
2647
|
+
|
|
2648
|
+
// set-up layout context
|
|
2649
|
+
const [layout, setLayout] = useState(createLayout(layoutConfig));
|
|
2650
|
+
|
|
2651
|
+
// react to external changes in the layout config
|
|
2652
|
+
useUpdateLayoutEffect(() => {
|
|
2653
|
+
const newLayout = createLayout(layoutConfig);
|
|
2654
|
+
setLayout(newLayout);
|
|
2655
|
+
}, [layoutConfig]);
|
|
2656
|
+
useEffect(() => {
|
|
2657
|
+
if (typeof layoutChanged === 'function') {
|
|
2658
|
+
layoutChanged(layout);
|
|
2659
|
+
}
|
|
2660
|
+
}, [layout, layoutChanged]);
|
|
2661
|
+
const getLayoutForKey = (key, defaultValue) => {
|
|
2662
|
+
return get(layout, key, defaultValue);
|
|
2663
|
+
};
|
|
2664
|
+
const setLayoutForKey = (key, config) => {
|
|
2665
|
+
const newLayout = assign({}, layout);
|
|
2666
|
+
set(newLayout, key, config);
|
|
2667
|
+
setLayout(newLayout);
|
|
2668
|
+
};
|
|
2669
|
+
const layoutContext = {
|
|
2670
|
+
layout,
|
|
2671
|
+
setLayout,
|
|
2672
|
+
getLayoutForKey,
|
|
2673
|
+
setLayoutForKey
|
|
2674
|
+
};
|
|
2675
|
+
|
|
2676
|
+
// set-up description context
|
|
2677
|
+
const description = useMemo(() => createDescriptionContext(descriptionConfig), [descriptionConfig]);
|
|
2678
|
+
useEffect(() => {
|
|
2679
|
+
if (typeof descriptionLoaded === 'function') {
|
|
2680
|
+
descriptionLoaded(description);
|
|
2681
|
+
}
|
|
2682
|
+
}, [description, descriptionLoaded]);
|
|
2683
|
+
const getDescriptionForId = (id, element) => {
|
|
2684
|
+
return description[id] && description[id](element);
|
|
2685
|
+
};
|
|
2686
|
+
const descriptionContext = {
|
|
2687
|
+
description,
|
|
2688
|
+
getDescriptionForId
|
|
2689
|
+
};
|
|
2690
|
+
|
|
2691
|
+
// set-up tooltip context
|
|
2692
|
+
const tooltip = useMemo(() => createTooltipContext(tooltipConfig), [tooltipConfig]);
|
|
2693
|
+
useEffect(() => {
|
|
2694
|
+
if (typeof tooltipLoaded === 'function') {
|
|
2695
|
+
tooltipLoaded(tooltip);
|
|
2696
|
+
}
|
|
2697
|
+
}, [tooltip, tooltipLoaded]);
|
|
2698
|
+
const getTooltipForId = (id, element) => {
|
|
2699
|
+
return tooltip[id] && tooltip[id](element);
|
|
2700
|
+
};
|
|
2701
|
+
const tooltipContext = {
|
|
2702
|
+
tooltip,
|
|
2703
|
+
getTooltipForId
|
|
2704
|
+
};
|
|
2705
|
+
const [errors, setErrors] = useState({});
|
|
2706
|
+
const onSetErrors = ({
|
|
2707
|
+
errors
|
|
2708
|
+
}) => setErrors(errors);
|
|
2709
|
+
useEvent('propertiesPanel.setErrors', onSetErrors, eventBus);
|
|
2710
|
+
const errorsContext = {
|
|
2711
|
+
errors
|
|
2712
|
+
};
|
|
2713
|
+
const eventContext = {
|
|
2714
|
+
eventBus
|
|
2715
|
+
};
|
|
2716
|
+
const propertiesPanelContext = {
|
|
2717
|
+
element
|
|
2718
|
+
};
|
|
2719
|
+
|
|
2720
|
+
// empty state
|
|
2721
|
+
if (placeholderProvider && !element) {
|
|
2722
|
+
return jsx(Placeholder, {
|
|
2723
|
+
...placeholderProvider.getEmpty()
|
|
2724
|
+
});
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
// multiple state
|
|
2728
|
+
if (placeholderProvider && isArray(element)) {
|
|
2729
|
+
return jsx(Placeholder, {
|
|
2730
|
+
...placeholderProvider.getMultiple()
|
|
2731
|
+
});
|
|
2732
|
+
}
|
|
2733
|
+
return jsx(LayoutContext.Provider, {
|
|
2734
|
+
value: propertiesPanelContext,
|
|
2735
|
+
children: jsx(ErrorsContext.Provider, {
|
|
2736
|
+
value: errorsContext,
|
|
2737
|
+
children: jsx(DescriptionContext.Provider, {
|
|
2738
|
+
value: descriptionContext,
|
|
2739
|
+
children: jsx(TooltipContext.Provider, {
|
|
2740
|
+
value: tooltipContext,
|
|
2741
|
+
children: jsx(LayoutContext.Provider, {
|
|
2742
|
+
value: layoutContext,
|
|
2743
|
+
children: jsx(EventContext.Provider, {
|
|
2744
|
+
value: eventContext,
|
|
2745
|
+
children: jsx(FEELPopupRoot, {
|
|
2746
|
+
element: element,
|
|
2747
|
+
eventBus: eventBus,
|
|
2748
|
+
popupContainer: feelPopupContainer,
|
|
2749
|
+
children: jsxs("div", {
|
|
2750
|
+
class: "bio-properties-panel",
|
|
2751
|
+
children: [jsx(Header, {
|
|
2752
|
+
element: element,
|
|
2753
|
+
headerProvider: headerProvider
|
|
2754
|
+
}), jsx("div", {
|
|
2755
|
+
class: "bio-properties-panel-scroll-container",
|
|
2756
|
+
children: groups.map(group => {
|
|
2757
|
+
const {
|
|
2758
|
+
component: Component = Group,
|
|
2759
|
+
id
|
|
2760
|
+
} = group;
|
|
2761
|
+
return createElement(Component, {
|
|
2762
|
+
...group,
|
|
2763
|
+
key: id,
|
|
2764
|
+
element: element
|
|
2765
|
+
});
|
|
2766
|
+
})
|
|
2767
|
+
})]
|
|
2768
|
+
})
|
|
2769
|
+
})
|
|
2770
|
+
})
|
|
2771
|
+
})
|
|
2772
|
+
})
|
|
2773
|
+
})
|
|
2774
|
+
})
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
// helpers //////////////////
|
|
2779
|
+
|
|
2780
|
+
function createLayout(overrides = {}, defaults = DEFAULT_LAYOUT) {
|
|
2781
|
+
return {
|
|
2782
|
+
...defaults,
|
|
2783
|
+
...overrides
|
|
2784
|
+
};
|
|
2785
|
+
}
|
|
2786
|
+
function createDescriptionContext(overrides = {}) {
|
|
2787
|
+
return {
|
|
2788
|
+
...DEFAULT_DESCRIPTION,
|
|
2789
|
+
...overrides
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
function createTooltipContext(overrides = {}) {
|
|
2793
|
+
return {
|
|
2794
|
+
...DEFAULT_TOOLTIP,
|
|
2795
|
+
...overrides
|
|
2796
|
+
};
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
// hooks //////////////////
|
|
2800
|
+
|
|
2801
|
+
/**
|
|
2802
|
+
* This hook behaves like useLayoutEffect, but does not trigger on the first render.
|
|
2803
|
+
*
|
|
2804
|
+
* @param {Function} effect
|
|
2805
|
+
* @param {Array} deps
|
|
2806
|
+
*/
|
|
2807
|
+
function useUpdateLayoutEffect(effect, deps) {
|
|
2808
|
+
const isMounted = useRef(false);
|
|
2809
|
+
useLayoutEffect(() => {
|
|
2810
|
+
if (isMounted.current) {
|
|
2811
|
+
return effect();
|
|
2812
|
+
} else {
|
|
2813
|
+
isMounted.current = true;
|
|
2814
|
+
}
|
|
2815
|
+
}, deps);
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
function DropdownButton(props) {
|
|
2819
|
+
const {
|
|
2820
|
+
class: className,
|
|
2821
|
+
children,
|
|
2822
|
+
menuItems = []
|
|
2823
|
+
} = props;
|
|
2824
|
+
const dropdownRef = useRef(null);
|
|
2825
|
+
const menuRef = useRef(null);
|
|
2826
|
+
const [open, setOpen] = useState(false);
|
|
2827
|
+
const close = () => setOpen(false);
|
|
2828
|
+
function onDropdownToggle(event) {
|
|
2829
|
+
if (menuRef.current && menuRef.current.contains(event.target)) {
|
|
2830
|
+
return;
|
|
2831
|
+
}
|
|
2832
|
+
event.stopPropagation();
|
|
2833
|
+
setOpen(open => !open);
|
|
2834
|
+
}
|
|
2835
|
+
function onActionClick(event, action) {
|
|
2836
|
+
event.stopPropagation();
|
|
2837
|
+
close();
|
|
2838
|
+
action();
|
|
2839
|
+
}
|
|
2840
|
+
useGlobalClick([dropdownRef.current], () => close());
|
|
2841
|
+
return jsxs("div", {
|
|
2842
|
+
class: classnames('bio-properties-panel-dropdown-button', {
|
|
2843
|
+
open
|
|
2844
|
+
}, className),
|
|
2845
|
+
onClick: onDropdownToggle,
|
|
2846
|
+
ref: dropdownRef,
|
|
2847
|
+
children: [children, jsx("div", {
|
|
2848
|
+
class: "bio-properties-panel-dropdown-button__menu",
|
|
2849
|
+
ref: menuRef,
|
|
2850
|
+
children: menuItems.map((item, index) => jsx(MenuItem, {
|
|
2851
|
+
onClick: onActionClick,
|
|
2852
|
+
item: item
|
|
2853
|
+
}, index))
|
|
2854
|
+
})]
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
function MenuItem({
|
|
2858
|
+
item,
|
|
2859
|
+
onClick
|
|
2860
|
+
}) {
|
|
2861
|
+
if (item.separator) {
|
|
2862
|
+
return jsx("div", {
|
|
2863
|
+
class: "bio-properties-panel-dropdown-button__menu-item bio-properties-panel-dropdown-button__menu-item--separator"
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2866
|
+
if (item.action) {
|
|
2867
|
+
return jsx("button", {
|
|
2868
|
+
class: "bio-properties-panel-dropdown-button__menu-item bio-properties-panel-dropdown-button__menu-item--actionable",
|
|
2869
|
+
onClick: event => onClick(event, item.action),
|
|
2870
|
+
children: item.entry
|
|
2871
|
+
});
|
|
2872
|
+
}
|
|
2873
|
+
return jsx("div", {
|
|
2874
|
+
class: "bio-properties-panel-dropdown-button__menu-item",
|
|
2875
|
+
children: item.entry
|
|
2876
|
+
});
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
/**
|
|
2880
|
+
*
|
|
2881
|
+
* @param {Array<null | Element>} ignoredElements
|
|
2882
|
+
* @param {Function} callback
|
|
2883
|
+
*/
|
|
2884
|
+
function useGlobalClick(ignoredElements, callback) {
|
|
2885
|
+
useEffect(() => {
|
|
2886
|
+
/**
|
|
2887
|
+
* @param {MouseEvent} event
|
|
2888
|
+
*/
|
|
2889
|
+
function listener(event) {
|
|
2890
|
+
if (ignoredElements.some(element => element && element.contains(event.target))) {
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
callback();
|
|
2894
|
+
}
|
|
2895
|
+
document.addEventListener('click', listener, {
|
|
2896
|
+
capture: true
|
|
2897
|
+
});
|
|
2898
|
+
return () => document.removeEventListener('click', listener, {
|
|
2899
|
+
capture: true
|
|
2900
|
+
});
|
|
2901
|
+
}, [...ignoredElements, callback]);
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
function HeaderButton(props) {
|
|
2905
|
+
const {
|
|
2906
|
+
children = null,
|
|
2907
|
+
class: classname,
|
|
2908
|
+
onClick = () => {},
|
|
2909
|
+
...otherProps
|
|
2910
|
+
} = props;
|
|
2911
|
+
return jsx("button", {
|
|
2912
|
+
...otherProps,
|
|
2913
|
+
onClick: onClick,
|
|
2914
|
+
class: classnames('bio-properties-panel-group-header-button', classname),
|
|
2915
|
+
children: children
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
function CollapsibleEntry(props) {
|
|
2920
|
+
const {
|
|
2921
|
+
element,
|
|
2922
|
+
entries = [],
|
|
2923
|
+
id,
|
|
2924
|
+
label,
|
|
2925
|
+
open: shouldOpen,
|
|
2926
|
+
remove
|
|
2927
|
+
} = props;
|
|
2928
|
+
const [open, setOpen] = useState(shouldOpen);
|
|
2929
|
+
const toggleOpen = () => setOpen(!open);
|
|
2930
|
+
const {
|
|
2931
|
+
onShow
|
|
2932
|
+
} = useContext(LayoutContext);
|
|
2933
|
+
const propertiesPanelContext = {
|
|
2934
|
+
...useContext(LayoutContext),
|
|
2935
|
+
onShow: useCallback(() => {
|
|
2936
|
+
setOpen(true);
|
|
2937
|
+
if (isFunction(onShow)) {
|
|
2938
|
+
onShow();
|
|
2939
|
+
}
|
|
2940
|
+
}, [onShow, setOpen])
|
|
2941
|
+
};
|
|
2942
|
+
|
|
2943
|
+
// todo(pinussilvestrus): translate once we have a translate mechanism for the core
|
|
2944
|
+
const placeholderLabel = '<empty>';
|
|
2945
|
+
return jsxs("div", {
|
|
2946
|
+
"data-entry-id": id,
|
|
2947
|
+
class: classnames('bio-properties-panel-collapsible-entry', open ? 'open' : ''),
|
|
2948
|
+
children: [jsxs("div", {
|
|
2949
|
+
class: "bio-properties-panel-collapsible-entry-header",
|
|
2950
|
+
onClick: toggleOpen,
|
|
2951
|
+
children: [jsx("div", {
|
|
2952
|
+
title: label || placeholderLabel,
|
|
2953
|
+
class: classnames('bio-properties-panel-collapsible-entry-header-title', !label && 'empty'),
|
|
2954
|
+
children: label || placeholderLabel
|
|
2955
|
+
}), jsx("button", {
|
|
2956
|
+
title: "Toggle list item",
|
|
2957
|
+
class: "bio-properties-panel-arrow bio-properties-panel-collapsible-entry-arrow",
|
|
2958
|
+
children: jsx(ArrowIcon, {
|
|
2959
|
+
class: open ? 'bio-properties-panel-arrow-down' : 'bio-properties-panel-arrow-right'
|
|
2960
|
+
})
|
|
2961
|
+
}), remove ? jsx("button", {
|
|
2962
|
+
title: "Delete item",
|
|
2963
|
+
class: "bio-properties-panel-remove-entry",
|
|
2964
|
+
onClick: remove,
|
|
2965
|
+
children: jsx(DeleteIcon, {})
|
|
2966
|
+
}) : null]
|
|
2967
|
+
}), jsx("div", {
|
|
2968
|
+
class: classnames('bio-properties-panel-collapsible-entry-entries', open ? 'open' : ''),
|
|
2969
|
+
children: jsx(LayoutContext.Provider, {
|
|
2970
|
+
value: propertiesPanelContext,
|
|
2971
|
+
children: entries.map(entry => {
|
|
2972
|
+
const {
|
|
2973
|
+
component: Component,
|
|
2974
|
+
id
|
|
2975
|
+
} = entry;
|
|
2976
|
+
return createElement(Component, {
|
|
2977
|
+
...entry,
|
|
2978
|
+
element: element,
|
|
2979
|
+
key: id
|
|
2980
|
+
});
|
|
2981
|
+
})
|
|
2982
|
+
})
|
|
2983
|
+
})]
|
|
2984
|
+
});
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
function ListItem(props) {
|
|
2988
|
+
const {
|
|
2989
|
+
autoFocusEntry,
|
|
2990
|
+
autoOpen
|
|
2991
|
+
} = props;
|
|
2992
|
+
|
|
2993
|
+
// focus specified entry on auto open
|
|
2994
|
+
useEffect(() => {
|
|
2995
|
+
if (autoOpen && autoFocusEntry) {
|
|
2996
|
+
const entry = query(`[data-entry-id="${autoFocusEntry}"]`);
|
|
2997
|
+
const focusableInput = query('.bio-properties-panel-input', entry);
|
|
2998
|
+
if (focusableInput) {
|
|
2999
|
+
if (isFunction(focusableInput.select)) {
|
|
3000
|
+
focusableInput.select();
|
|
3001
|
+
} else if (isFunction(focusableInput.focus)) {
|
|
3002
|
+
focusableInput.focus();
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
}, [autoOpen, autoFocusEntry]);
|
|
3007
|
+
return jsx("div", {
|
|
3008
|
+
class: "bio-properties-panel-list-item",
|
|
3009
|
+
children: jsx(CollapsibleEntry, {
|
|
3010
|
+
...props,
|
|
3011
|
+
open: autoOpen
|
|
3012
|
+
})
|
|
3013
|
+
});
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
const noop$1 = () => {};
|
|
3017
|
+
|
|
3018
|
+
/**
|
|
3019
|
+
* @param {import('../PropertiesPanel').ListGroupDefinition} props
|
|
3020
|
+
*/
|
|
3021
|
+
function ListGroup(props) {
|
|
3022
|
+
const {
|
|
3023
|
+
add,
|
|
3024
|
+
element,
|
|
3025
|
+
id,
|
|
3026
|
+
items,
|
|
3027
|
+
label,
|
|
3028
|
+
shouldOpen = true,
|
|
3029
|
+
shouldSort = true
|
|
3030
|
+
} = props;
|
|
3031
|
+
const groupRef = useRef(null);
|
|
3032
|
+
const [open, setOpen] = useLayoutState(['groups', id, 'open'], false);
|
|
3033
|
+
const [sticky, setSticky] = useState(false);
|
|
3034
|
+
const onShow = useCallback(() => setOpen(true), [setOpen]);
|
|
3035
|
+
const [ordering, setOrdering] = useState([]);
|
|
3036
|
+
const [newItemAdded, setNewItemAdded] = useState(false);
|
|
3037
|
+
|
|
3038
|
+
// Flag to mark that add button was clicked in the last render cycle
|
|
3039
|
+
const [addTriggered, setAddTriggered] = useState(false);
|
|
3040
|
+
const prevItems = usePrevious(items);
|
|
3041
|
+
const prevElement = usePrevious(element);
|
|
3042
|
+
const elementChanged = element !== prevElement;
|
|
3043
|
+
const shouldHandleEffects = !elementChanged && (shouldSort || shouldOpen);
|
|
3044
|
+
|
|
3045
|
+
// reset initial ordering when element changes (before first render)
|
|
3046
|
+
if (elementChanged) {
|
|
3047
|
+
setOrdering(createOrdering(shouldSort ? sortItems(items) : items));
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
// keep ordering in sync to items - and open changes
|
|
3051
|
+
|
|
3052
|
+
// (0) set initial ordering from given items
|
|
3053
|
+
useEffect(() => {
|
|
3054
|
+
if (!prevItems || !shouldSort) {
|
|
3055
|
+
setOrdering(createOrdering(items));
|
|
3056
|
+
}
|
|
3057
|
+
}, [items, element]);
|
|
3058
|
+
|
|
3059
|
+
// (1) items were added
|
|
3060
|
+
useEffect(() => {
|
|
3061
|
+
// reset addTriggered flag
|
|
3062
|
+
setAddTriggered(false);
|
|
3063
|
+
if (shouldHandleEffects && prevItems && items.length > prevItems.length) {
|
|
3064
|
+
let add = [];
|
|
3065
|
+
items.forEach(item => {
|
|
3066
|
+
if (!ordering.includes(item.id)) {
|
|
3067
|
+
add.push(item.id);
|
|
3068
|
+
}
|
|
3069
|
+
});
|
|
3070
|
+
let newOrdering = ordering;
|
|
3071
|
+
|
|
3072
|
+
// open if not open, configured and triggered by add button
|
|
3073
|
+
//
|
|
3074
|
+
// TODO(marstamm): remove once we refactor layout handling for listGroups.
|
|
3075
|
+
// Ideally, opening should be handled as part of the `add` callback and
|
|
3076
|
+
// not be a concern for the ListGroup component.
|
|
3077
|
+
if (addTriggered && !open && shouldOpen) {
|
|
3078
|
+
toggleOpen();
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
// filter when not open and configured
|
|
3082
|
+
if (!open && shouldSort) {
|
|
3083
|
+
newOrdering = createOrdering(sortItems(items));
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
// add new items on top or bottom depending on sorting behavior
|
|
3087
|
+
newOrdering = newOrdering.filter(item => !add.includes(item));
|
|
3088
|
+
if (shouldSort) {
|
|
3089
|
+
newOrdering.unshift(...add);
|
|
3090
|
+
} else {
|
|
3091
|
+
newOrdering.push(...add);
|
|
3092
|
+
}
|
|
3093
|
+
setOrdering(newOrdering);
|
|
3094
|
+
setNewItemAdded(addTriggered);
|
|
3095
|
+
} else {
|
|
3096
|
+
setNewItemAdded(false);
|
|
3097
|
+
}
|
|
3098
|
+
}, [items, open, shouldHandleEffects, addTriggered]);
|
|
3099
|
+
|
|
3100
|
+
// (2) sort items on open if shouldSort is set
|
|
3101
|
+
useEffect(() => {
|
|
3102
|
+
if (shouldSort && open && !newItemAdded) {
|
|
3103
|
+
setOrdering(createOrdering(sortItems(items)));
|
|
3104
|
+
}
|
|
3105
|
+
}, [open, shouldSort]);
|
|
3106
|
+
|
|
3107
|
+
// (3) items were deleted
|
|
3108
|
+
useEffect(() => {
|
|
3109
|
+
if (shouldHandleEffects && prevItems && items.length < prevItems.length) {
|
|
3110
|
+
let keep = [];
|
|
3111
|
+
ordering.forEach(o => {
|
|
3112
|
+
if (getItem(items, o)) {
|
|
3113
|
+
keep.push(o);
|
|
3114
|
+
}
|
|
3115
|
+
});
|
|
3116
|
+
setOrdering(keep);
|
|
3117
|
+
}
|
|
3118
|
+
}, [items, shouldHandleEffects]);
|
|
3119
|
+
|
|
3120
|
+
// set css class when group is sticky to top
|
|
3121
|
+
useStickyIntersectionObserver(groupRef, 'div.bio-properties-panel-scroll-container', setSticky);
|
|
3122
|
+
const toggleOpen = () => setOpen(!open);
|
|
3123
|
+
const hasItems = !!items.length;
|
|
3124
|
+
const propertiesPanelContext = {
|
|
3125
|
+
...useContext(LayoutContext),
|
|
3126
|
+
onShow
|
|
3127
|
+
};
|
|
3128
|
+
const handleAddClick = e => {
|
|
3129
|
+
setAddTriggered(true);
|
|
3130
|
+
add(e);
|
|
3131
|
+
};
|
|
3132
|
+
const allErrors = useErrors();
|
|
3133
|
+
const hasError = items.some(item => {
|
|
3134
|
+
if (allErrors[item.id]) {
|
|
3135
|
+
return true;
|
|
3136
|
+
}
|
|
3137
|
+
if (!item.entries) {
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
// also check if the error is nested, e.g. for name-value entries
|
|
3142
|
+
return item.entries.some(entry => allErrors[entry.id]);
|
|
3143
|
+
});
|
|
3144
|
+
return jsxs("div", {
|
|
3145
|
+
class: "bio-properties-panel-group",
|
|
3146
|
+
"data-group-id": 'group-' + id,
|
|
3147
|
+
ref: groupRef,
|
|
3148
|
+
children: [jsxs("div", {
|
|
3149
|
+
class: classnames('bio-properties-panel-group-header', hasItems ? '' : 'empty', hasItems && open ? 'open' : '', sticky && open ? 'sticky' : ''),
|
|
3150
|
+
onClick: hasItems ? toggleOpen : noop$1,
|
|
3151
|
+
children: [jsx("div", {
|
|
3152
|
+
title: props.tooltip ? null : label,
|
|
3153
|
+
"data-title": label,
|
|
3154
|
+
class: "bio-properties-panel-group-header-title",
|
|
3155
|
+
children: jsx(TooltipWrapper, {
|
|
3156
|
+
value: props.tooltip,
|
|
3157
|
+
forId: 'group-' + id,
|
|
3158
|
+
element: element,
|
|
3159
|
+
parent: groupRef,
|
|
3160
|
+
children: label
|
|
3161
|
+
})
|
|
3162
|
+
}), jsxs("div", {
|
|
3163
|
+
class: "bio-properties-panel-group-header-buttons",
|
|
3164
|
+
children: [add ? jsxs("button", {
|
|
3165
|
+
title: "Create new list item",
|
|
3166
|
+
class: "bio-properties-panel-group-header-button bio-properties-panel-add-entry",
|
|
3167
|
+
onClick: handleAddClick,
|
|
3168
|
+
children: [jsx(CreateIcon, {}), !hasItems ? jsx("span", {
|
|
3169
|
+
class: "bio-properties-panel-add-entry-label",
|
|
3170
|
+
children: "Create"
|
|
3171
|
+
}) : null]
|
|
3172
|
+
}) : null, hasItems ? jsx("div", {
|
|
3173
|
+
title: `List contains ${items.length} item${items.length != 1 ? 's' : ''}`,
|
|
3174
|
+
class: classnames('bio-properties-panel-list-badge', hasError ? 'bio-properties-panel-list-badge--error' : ''),
|
|
3175
|
+
children: items.length
|
|
3176
|
+
}) : null, hasItems ? jsx("button", {
|
|
3177
|
+
title: "Toggle section",
|
|
3178
|
+
class: "bio-properties-panel-group-header-button bio-properties-panel-arrow",
|
|
3179
|
+
children: jsx(ArrowIcon, {
|
|
3180
|
+
class: open ? 'bio-properties-panel-arrow-down' : 'bio-properties-panel-arrow-right'
|
|
3181
|
+
})
|
|
3182
|
+
}) : null]
|
|
3183
|
+
})]
|
|
3184
|
+
}), jsx("div", {
|
|
3185
|
+
class: classnames('bio-properties-panel-list', open && hasItems ? 'open' : ''),
|
|
3186
|
+
children: jsx(LayoutContext.Provider, {
|
|
3187
|
+
value: propertiesPanelContext,
|
|
3188
|
+
children: ordering.map((o, index) => {
|
|
3189
|
+
const item = getItem(items, o);
|
|
3190
|
+
if (!item) {
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3193
|
+
const {
|
|
3194
|
+
id
|
|
3195
|
+
} = item;
|
|
3196
|
+
|
|
3197
|
+
// if item was added, open it
|
|
3198
|
+
// Existing items will not be affected as autoOpen is only applied on first render
|
|
3199
|
+
const autoOpen = newItemAdded;
|
|
3200
|
+
return createElement(ListItem, {
|
|
3201
|
+
...item,
|
|
3202
|
+
autoOpen: autoOpen,
|
|
3203
|
+
element: element,
|
|
3204
|
+
index: index,
|
|
3205
|
+
key: id
|
|
3206
|
+
});
|
|
3207
|
+
})
|
|
3208
|
+
})
|
|
3209
|
+
})]
|
|
3210
|
+
});
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
// helpers ////////////////////
|
|
3214
|
+
|
|
3215
|
+
/**
|
|
3216
|
+
* Sorts given items alphanumeric by label
|
|
3217
|
+
*/
|
|
3218
|
+
function sortItems(items) {
|
|
3219
|
+
return sortBy(items, i => i.label.toLowerCase());
|
|
3220
|
+
}
|
|
3221
|
+
function getItem(items, id) {
|
|
3222
|
+
return find(items, i => i.id === id);
|
|
3223
|
+
}
|
|
3224
|
+
function createOrdering(items) {
|
|
3225
|
+
return items.map(i => i.id);
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
function Checkbox(props) {
|
|
3229
|
+
const {
|
|
3230
|
+
id,
|
|
3231
|
+
label,
|
|
3232
|
+
onChange,
|
|
3233
|
+
disabled,
|
|
3234
|
+
value = false,
|
|
3235
|
+
onFocus,
|
|
3236
|
+
onBlur,
|
|
3237
|
+
tooltip
|
|
3238
|
+
} = props;
|
|
3239
|
+
const [localValue, setLocalValue] = useState(value);
|
|
3240
|
+
const handleChangeCallback = ({
|
|
3241
|
+
target
|
|
3242
|
+
}) => {
|
|
3243
|
+
onChange(target.checked);
|
|
3244
|
+
};
|
|
3245
|
+
const handleChange = e => {
|
|
3246
|
+
handleChangeCallback(e);
|
|
3247
|
+
setLocalValue(e.target.value);
|
|
3248
|
+
};
|
|
3249
|
+
useEffect(() => {
|
|
3250
|
+
if (value === localValue) {
|
|
3251
|
+
return;
|
|
3252
|
+
}
|
|
3253
|
+
setLocalValue(value);
|
|
3254
|
+
}, [value]);
|
|
3255
|
+
const ref = useShowEntryEvent(id);
|
|
3256
|
+
return jsxs("div", {
|
|
3257
|
+
class: "bio-properties-panel-checkbox",
|
|
3258
|
+
children: [jsx("input", {
|
|
3259
|
+
ref: ref,
|
|
3260
|
+
id: prefixId$4(id),
|
|
3261
|
+
name: id,
|
|
3262
|
+
onFocus: onFocus,
|
|
3263
|
+
onBlur: onBlur,
|
|
3264
|
+
type: "checkbox",
|
|
3265
|
+
class: "bio-properties-panel-input",
|
|
3266
|
+
onChange: handleChange,
|
|
3267
|
+
checked: localValue,
|
|
3268
|
+
disabled: disabled
|
|
3269
|
+
}), jsx("label", {
|
|
3270
|
+
for: prefixId$4(id),
|
|
3271
|
+
class: "bio-properties-panel-label",
|
|
3272
|
+
children: jsx(TooltipWrapper, {
|
|
3273
|
+
value: tooltip,
|
|
3274
|
+
forId: id,
|
|
3275
|
+
element: props.element,
|
|
3276
|
+
children: label
|
|
3277
|
+
})
|
|
3278
|
+
})]
|
|
3279
|
+
});
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
/**
|
|
3283
|
+
* @param {Object} props
|
|
3284
|
+
* @param {Object} props.element
|
|
3285
|
+
* @param {String} props.id
|
|
3286
|
+
* @param {String} props.description
|
|
3287
|
+
* @param {String} props.label
|
|
3288
|
+
* @param {Function} props.getValue
|
|
3289
|
+
* @param {Function} props.setValue
|
|
3290
|
+
* @param {Function} props.onFocus
|
|
3291
|
+
* @param {Function} props.onBlur
|
|
3292
|
+
* @param {string|import('preact').Component} props.tooltip
|
|
3293
|
+
* @param {boolean} [props.disabled]
|
|
3294
|
+
*/
|
|
3295
|
+
function CheckboxEntry(props) {
|
|
3296
|
+
const {
|
|
3297
|
+
element,
|
|
3298
|
+
id,
|
|
3299
|
+
description,
|
|
3300
|
+
label,
|
|
3301
|
+
getValue,
|
|
3302
|
+
setValue,
|
|
3303
|
+
disabled,
|
|
3304
|
+
onFocus,
|
|
3305
|
+
onBlur,
|
|
3306
|
+
tooltip
|
|
3307
|
+
} = props;
|
|
3308
|
+
const value = getValue(element);
|
|
3309
|
+
const error = useError(id);
|
|
3310
|
+
return jsxs("div", {
|
|
3311
|
+
class: "bio-properties-panel-entry bio-properties-panel-checkbox-entry",
|
|
3312
|
+
"data-entry-id": id,
|
|
3313
|
+
children: [jsx(Checkbox, {
|
|
3314
|
+
disabled: disabled,
|
|
3315
|
+
id: id,
|
|
3316
|
+
label: label,
|
|
3317
|
+
onChange: setValue,
|
|
3318
|
+
onFocus: onFocus,
|
|
3319
|
+
onBlur: onBlur,
|
|
3320
|
+
value: value,
|
|
3321
|
+
tooltip: tooltip,
|
|
3322
|
+
element: element
|
|
3323
|
+
}, element), error && jsx("div", {
|
|
3324
|
+
class: "bio-properties-panel-error",
|
|
3325
|
+
children: error
|
|
3326
|
+
}), jsx(Description, {
|
|
3327
|
+
forId: id,
|
|
3328
|
+
element: element,
|
|
3329
|
+
value: description
|
|
3330
|
+
})]
|
|
3331
|
+
});
|
|
3332
|
+
}
|
|
3333
|
+
function isEdited$5(node) {
|
|
3334
|
+
return node && !!node.checked;
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
// helpers /////////////////
|
|
3338
|
+
|
|
3339
|
+
function prefixId$4(id) {
|
|
3340
|
+
return `bio-properties-panel-${id}`;
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
const noop = () => {};
|
|
3344
|
+
|
|
3345
|
+
/**
|
|
3346
|
+
* @param {Object} props
|
|
3347
|
+
* @param {Object} props.element
|
|
3348
|
+
* @param {String} props.id
|
|
3349
|
+
* @param {String} props.description
|
|
3350
|
+
* @param {Boolean} props.debounce
|
|
3351
|
+
* @param {Boolean} props.disabled
|
|
3352
|
+
* @param {String} props.label
|
|
3353
|
+
* @param {Function} props.getValue
|
|
3354
|
+
* @param {Function} props.setValue
|
|
3355
|
+
* @param {Function} props.tooltipContainer
|
|
3356
|
+
* @param {Function} props.validate
|
|
3357
|
+
* @param {Function} props.show
|
|
3358
|
+
*/
|
|
3359
|
+
function TemplatingEntry(props) {
|
|
3360
|
+
const {
|
|
3361
|
+
element,
|
|
3362
|
+
id,
|
|
3363
|
+
description,
|
|
3364
|
+
debounce,
|
|
3365
|
+
disabled,
|
|
3366
|
+
label,
|
|
3367
|
+
getValue,
|
|
3368
|
+
setValue,
|
|
3369
|
+
tooltipContainer,
|
|
3370
|
+
validate,
|
|
3371
|
+
show = noop
|
|
3372
|
+
} = props;
|
|
3373
|
+
const [validationError, setValidationError] = useState(null);
|
|
3374
|
+
const [localError, setLocalError] = useState(null);
|
|
3375
|
+
let value = getValue(element);
|
|
3376
|
+
useEffect(() => {
|
|
3377
|
+
if (isFunction(validate)) {
|
|
3378
|
+
const newValidationError = validate(value) || null;
|
|
3379
|
+
setValidationError(newValidationError);
|
|
3380
|
+
}
|
|
3381
|
+
}, [value]);
|
|
3382
|
+
const onInput = useStaticCallback(newValue => {
|
|
3383
|
+
let newValidationError = null;
|
|
3384
|
+
if (isFunction(validate)) {
|
|
3385
|
+
newValidationError = validate(newValue) || null;
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
// don't create multiple commandStack entries for the same value
|
|
3389
|
+
if (newValue !== value) {
|
|
3390
|
+
setValue(newValue, newValidationError);
|
|
3391
|
+
}
|
|
3392
|
+
setValidationError(newValidationError);
|
|
3393
|
+
});
|
|
3394
|
+
const onError = useCallback(err => {
|
|
3395
|
+
setLocalError(err);
|
|
3396
|
+
}, []);
|
|
3397
|
+
const temporaryError = useError(id);
|
|
3398
|
+
const error = localError || temporaryError || validationError;
|
|
3399
|
+
return jsxs("div", {
|
|
3400
|
+
class: classnames('bio-properties-panel-entry', error ? 'has-error' : ''),
|
|
3401
|
+
"data-entry-id": id,
|
|
3402
|
+
children: [jsx(Templating, {
|
|
3403
|
+
debounce: debounce,
|
|
3404
|
+
disabled: disabled,
|
|
3405
|
+
id: id,
|
|
3406
|
+
label: label,
|
|
3407
|
+
onInput: onInput,
|
|
3408
|
+
onError: onError,
|
|
3409
|
+
show: show,
|
|
3410
|
+
value: value,
|
|
3411
|
+
tooltipContainer: tooltipContainer
|
|
3412
|
+
}, element), error && jsx("div", {
|
|
3413
|
+
class: "bio-properties-panel-error",
|
|
3414
|
+
children: error
|
|
3415
|
+
}), jsx(Description, {
|
|
3416
|
+
forId: id,
|
|
3417
|
+
element: element,
|
|
3418
|
+
value: description
|
|
3419
|
+
})]
|
|
3420
|
+
});
|
|
3421
|
+
}
|
|
3422
|
+
function Templating(props) {
|
|
3423
|
+
const {
|
|
3424
|
+
debounce,
|
|
3425
|
+
id,
|
|
3426
|
+
label,
|
|
3427
|
+
onInput,
|
|
3428
|
+
onError,
|
|
3429
|
+
value = '',
|
|
3430
|
+
disabled = false,
|
|
3431
|
+
tooltipContainer
|
|
3432
|
+
} = props;
|
|
3433
|
+
const [localValue, setLocalValue] = useState(value);
|
|
3434
|
+
const editorRef = useShowEntryEvent(id);
|
|
3435
|
+
const containerRef = useRef();
|
|
3436
|
+
const [focus, _setFocus] = useState(undefined);
|
|
3437
|
+
const setFocus = (offset = 0) => {
|
|
3438
|
+
const hasFocus = containerRef.current.contains(document.activeElement);
|
|
3439
|
+
|
|
3440
|
+
// Keep caret position if it is already focused, otherwise focus at the end
|
|
3441
|
+
const position = hasFocus ? document.activeElement.selectionStart : Infinity;
|
|
3442
|
+
_setFocus(position + offset);
|
|
3443
|
+
};
|
|
3444
|
+
const handleInputCallback = useMemo(() => {
|
|
3445
|
+
return debounce(newValue => onInput(newValue.length ? newValue : undefined));
|
|
3446
|
+
}, [onInput, debounce]);
|
|
3447
|
+
const handleInput = newValue => {
|
|
3448
|
+
handleInputCallback(newValue);
|
|
3449
|
+
setLocalValue(newValue);
|
|
3450
|
+
};
|
|
3451
|
+
const handleLint = useStaticCallback(lint => {
|
|
3452
|
+
const errors = lint && lint.length && lint.filter(e => e.severity === 'error') || [];
|
|
3453
|
+
if (!errors.length) {
|
|
3454
|
+
onError(undefined);
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
3457
|
+
const error = lint[0];
|
|
3458
|
+
const message = `${error.source}: ${error.message}`;
|
|
3459
|
+
onError(message);
|
|
3460
|
+
});
|
|
3461
|
+
useEffect(() => {
|
|
3462
|
+
if (typeof focus !== 'undefined') {
|
|
3463
|
+
editorRef.current.focus(focus);
|
|
3464
|
+
_setFocus(undefined);
|
|
3465
|
+
}
|
|
3466
|
+
}, [focus]);
|
|
3467
|
+
useEffect(() => {
|
|
3468
|
+
if (value === localValue) {
|
|
3469
|
+
return;
|
|
3470
|
+
}
|
|
3471
|
+
setLocalValue(value ? value : '');
|
|
3472
|
+
}, [value]);
|
|
3473
|
+
return jsxs("div", {
|
|
3474
|
+
class: "bio-properties-panel-feelers",
|
|
3475
|
+
children: [jsx("label", {
|
|
3476
|
+
id: prefixIdLabel(id),
|
|
3477
|
+
class: "bio-properties-panel-label",
|
|
3478
|
+
onClick: () => setFocus(),
|
|
3479
|
+
children: label
|
|
3480
|
+
}), jsx("div", {
|
|
3481
|
+
class: "bio-properties-panel-feelers-input",
|
|
3482
|
+
ref: containerRef,
|
|
3483
|
+
children: jsx(CodeEditor$1, {
|
|
3484
|
+
name: id,
|
|
3485
|
+
onInput: handleInput,
|
|
3486
|
+
contentAttributes: {
|
|
3487
|
+
'aria-labelledby': prefixIdLabel(id)
|
|
3488
|
+
},
|
|
3489
|
+
disabled: disabled,
|
|
3490
|
+
onLint: handleLint,
|
|
3491
|
+
value: localValue,
|
|
3492
|
+
ref: editorRef,
|
|
3493
|
+
tooltipContainer: tooltipContainer
|
|
3494
|
+
})
|
|
3495
|
+
})]
|
|
3496
|
+
});
|
|
3497
|
+
}
|
|
3498
|
+
function isEdited$4(node) {
|
|
3499
|
+
return node && (!!node.value || node.classList.contains('edited'));
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
// helpers /////////////////
|
|
3503
|
+
|
|
3504
|
+
function prefixIdLabel(id) {
|
|
3505
|
+
return `bio-properties-panel-feelers-${id}-label`;
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
function List(props) {
|
|
3509
|
+
const {
|
|
3510
|
+
id,
|
|
3511
|
+
element,
|
|
3512
|
+
items = [],
|
|
3513
|
+
component,
|
|
3514
|
+
label = '<empty>',
|
|
3515
|
+
open: shouldOpen,
|
|
3516
|
+
onAdd,
|
|
3517
|
+
onRemove,
|
|
3518
|
+
autoFocusEntry,
|
|
3519
|
+
compareFn,
|
|
3520
|
+
...restProps
|
|
3521
|
+
} = props;
|
|
3522
|
+
const [open, setOpen] = useState(!!shouldOpen);
|
|
3523
|
+
const hasItems = !!items.length;
|
|
3524
|
+
const toggleOpen = () => hasItems && setOpen(!open);
|
|
3525
|
+
const opening = !usePrevious(open) && open;
|
|
3526
|
+
const elementChanged = usePrevious(element) !== element;
|
|
3527
|
+
const shouldReset = opening || elementChanged;
|
|
3528
|
+
const sortedItems = useSortedItems(items, compareFn, shouldReset);
|
|
3529
|
+
const newItems = useNewItems(items, elementChanged);
|
|
3530
|
+
useEffect(() => {
|
|
3531
|
+
if (open && !hasItems) {
|
|
3532
|
+
setOpen(false);
|
|
3533
|
+
}
|
|
3534
|
+
}, [open, hasItems]);
|
|
3535
|
+
|
|
3536
|
+
/**
|
|
3537
|
+
* @param {MouseEvent} event
|
|
3538
|
+
*/
|
|
3539
|
+
function addItem(event) {
|
|
3540
|
+
event.stopPropagation();
|
|
3541
|
+
onAdd();
|
|
3542
|
+
if (!open) {
|
|
3543
|
+
setOpen(true);
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
return jsxs("div", {
|
|
3547
|
+
"data-entry-id": id,
|
|
3548
|
+
class: classnames('bio-properties-panel-entry', 'bio-properties-panel-list-entry', hasItems ? '' : 'empty', open ? 'open' : ''),
|
|
3549
|
+
children: [jsxs("div", {
|
|
3550
|
+
class: "bio-properties-panel-list-entry-header",
|
|
3551
|
+
onClick: toggleOpen,
|
|
3552
|
+
children: [jsx("div", {
|
|
3553
|
+
title: label,
|
|
3554
|
+
class: classnames('bio-properties-panel-list-entry-header-title', open && 'open'),
|
|
3555
|
+
children: label
|
|
3556
|
+
}), jsxs("div", {
|
|
3557
|
+
class: "bio-properties-panel-list-entry-header-buttons",
|
|
3558
|
+
children: [jsxs("button", {
|
|
3559
|
+
title: "Create new list item",
|
|
3560
|
+
onClick: addItem,
|
|
3561
|
+
class: "bio-properties-panel-add-entry",
|
|
3562
|
+
children: [jsx(CreateIcon, {}), !hasItems ? jsx("span", {
|
|
3563
|
+
class: "bio-properties-panel-add-entry-label",
|
|
3564
|
+
children: "Create"
|
|
3565
|
+
}) : null]
|
|
3566
|
+
}), hasItems && jsx("div", {
|
|
3567
|
+
title: `List contains ${items.length} item${items.length != 1 ? 's' : ''}`,
|
|
3568
|
+
class: "bio-properties-panel-list-badge",
|
|
3569
|
+
children: items.length
|
|
3570
|
+
}), hasItems && jsx("button", {
|
|
3571
|
+
title: "Toggle list item",
|
|
3572
|
+
class: "bio-properties-panel-arrow",
|
|
3573
|
+
children: jsx(ArrowIcon, {
|
|
3574
|
+
class: open ? 'bio-properties-panel-arrow-down' : 'bio-properties-panel-arrow-right'
|
|
3575
|
+
})
|
|
3576
|
+
})]
|
|
3577
|
+
})]
|
|
3578
|
+
}), hasItems && jsx(ItemsList, {
|
|
3579
|
+
...restProps,
|
|
3580
|
+
autoFocusEntry: autoFocusEntry,
|
|
3581
|
+
component: component,
|
|
3582
|
+
element: element,
|
|
3583
|
+
id: id,
|
|
3584
|
+
items: sortedItems,
|
|
3585
|
+
newItems: newItems,
|
|
3586
|
+
onRemove: onRemove,
|
|
3587
|
+
open: open
|
|
3588
|
+
})]
|
|
3589
|
+
});
|
|
3590
|
+
}
|
|
3591
|
+
function ItemsList(props) {
|
|
3592
|
+
const {
|
|
3593
|
+
autoFocusEntry,
|
|
3594
|
+
component: Component,
|
|
3595
|
+
element,
|
|
3596
|
+
id,
|
|
3597
|
+
items,
|
|
3598
|
+
newItems,
|
|
3599
|
+
onRemove,
|
|
3600
|
+
open,
|
|
3601
|
+
...restProps
|
|
3602
|
+
} = props;
|
|
3603
|
+
const getKey = useKeyFactory();
|
|
3604
|
+
const newItem = newItems[0];
|
|
3605
|
+
useEffect(() => {
|
|
3606
|
+
if (newItem && autoFocusEntry) {
|
|
3607
|
+
// (0) select the parent entry (containing all list items)
|
|
3608
|
+
const entry = query(`[data-entry-id="${id}"]`);
|
|
3609
|
+
|
|
3610
|
+
// (1) select the first input or a custom element to be focussed
|
|
3611
|
+
const selector = typeof autoFocusEntry === 'boolean' ? '.bio-properties-panel-input' : autoFocusEntry;
|
|
3612
|
+
const focusableInput = query(selector, entry);
|
|
3613
|
+
|
|
3614
|
+
// (2) set focus
|
|
3615
|
+
if (focusableInput) {
|
|
3616
|
+
if (isFunction(focusableInput.select)) {
|
|
3617
|
+
focusableInput.select();
|
|
3618
|
+
} else if (isFunction(focusableInput.focus)) {
|
|
3619
|
+
focusableInput.focus();
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
}, [newItem, autoFocusEntry, id]);
|
|
3624
|
+
return jsx("ol", {
|
|
3625
|
+
class: classnames('bio-properties-panel-list-entry-items', open ? 'open' : ''),
|
|
3626
|
+
children: items.map((item, index) => {
|
|
3627
|
+
const key = getKey(item);
|
|
3628
|
+
return jsxs("li", {
|
|
3629
|
+
class: "bio-properties-panel-list-entry-item",
|
|
3630
|
+
children: [jsx(Component, {
|
|
3631
|
+
...restProps,
|
|
3632
|
+
element: element,
|
|
3633
|
+
id: id,
|
|
3634
|
+
index: index,
|
|
3635
|
+
item: item,
|
|
3636
|
+
open: item === newItem
|
|
3637
|
+
}), onRemove && jsx("button", {
|
|
3638
|
+
type: "button",
|
|
3639
|
+
title: "Delete item",
|
|
3640
|
+
class: "bio-properties-panel-remove-entry bio-properties-panel-remove-list-entry",
|
|
3641
|
+
onClick: () => onRemove && onRemove(item),
|
|
3642
|
+
children: jsx(DeleteIcon, {})
|
|
3643
|
+
})]
|
|
3644
|
+
}, key);
|
|
3645
|
+
})
|
|
3646
|
+
});
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
/**
|
|
3650
|
+
* Place new items in the beginning of the list and sort the rest with provided function.
|
|
3651
|
+
*
|
|
3652
|
+
* @template Item
|
|
3653
|
+
* @param {Item[]} currentItems
|
|
3654
|
+
* @param {(a: Item, b: Item) => 0 | 1 | -1} [compareFn] function used to sort items
|
|
3655
|
+
* @param {boolean} [shouldReset=false] set to `true` to reset state of the hook
|
|
3656
|
+
* @returns {Item[]}
|
|
3657
|
+
*/
|
|
3658
|
+
function useSortedItems(currentItems, compareFn, shouldReset = false) {
|
|
3659
|
+
const itemsRef = useRef(currentItems.slice());
|
|
3660
|
+
|
|
3661
|
+
// (1) Reset and optionally sort.
|
|
3662
|
+
if (shouldReset) {
|
|
3663
|
+
itemsRef.current = currentItems.slice();
|
|
3664
|
+
if (compareFn) {
|
|
3665
|
+
itemsRef.current.sort(compareFn);
|
|
3666
|
+
}
|
|
3667
|
+
} else {
|
|
3668
|
+
const items = itemsRef.current;
|
|
3669
|
+
|
|
3670
|
+
// (2) Add new item to the list.
|
|
3671
|
+
for (const item of currentItems) {
|
|
3672
|
+
if (!items.includes(item)) {
|
|
3673
|
+
// Unshift or push depending on whether we have a compareFn
|
|
3674
|
+
compareFn ? items.unshift(item) : items.push(item);
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
// (3) Filter out removed items.
|
|
3679
|
+
itemsRef.current = items.filter(item => currentItems.includes(item));
|
|
3680
|
+
}
|
|
3681
|
+
return itemsRef.current;
|
|
3682
|
+
}
|
|
3683
|
+
function useNewItems(items = [], shouldReset) {
|
|
3684
|
+
const previousItems = usePrevious(items.slice()) || [];
|
|
3685
|
+
if (shouldReset) {
|
|
3686
|
+
return [];
|
|
3687
|
+
}
|
|
3688
|
+
return previousItems ? items.filter(item => !previousItems.includes(item)) : [];
|
|
3689
|
+
}
|
|
3690
|
+
|
|
3691
|
+
function Select(props) {
|
|
3692
|
+
const {
|
|
3693
|
+
id,
|
|
3694
|
+
label,
|
|
3695
|
+
onChange,
|
|
3696
|
+
options = [],
|
|
3697
|
+
value = '',
|
|
3698
|
+
disabled,
|
|
3699
|
+
onFocus,
|
|
3700
|
+
onBlur,
|
|
3701
|
+
tooltip
|
|
3702
|
+
} = props;
|
|
3703
|
+
const ref = useShowEntryEvent(id);
|
|
3704
|
+
const [localValue, setLocalValue] = useState(value);
|
|
3705
|
+
const handleChangeCallback = ({
|
|
3706
|
+
target
|
|
3707
|
+
}) => {
|
|
3708
|
+
onChange(target.value);
|
|
3709
|
+
};
|
|
3710
|
+
const handleChange = e => {
|
|
3711
|
+
handleChangeCallback(e);
|
|
3712
|
+
setLocalValue(e.target.value);
|
|
3713
|
+
};
|
|
3714
|
+
useEffect(() => {
|
|
3715
|
+
if (value === localValue) {
|
|
3716
|
+
return;
|
|
3717
|
+
}
|
|
3718
|
+
setLocalValue(value);
|
|
3719
|
+
}, [value]);
|
|
3720
|
+
return jsxs("div", {
|
|
3721
|
+
class: "bio-properties-panel-select",
|
|
3722
|
+
children: [jsx("label", {
|
|
3723
|
+
for: prefixId$3(id),
|
|
3724
|
+
class: "bio-properties-panel-label",
|
|
3725
|
+
children: jsx(TooltipWrapper, {
|
|
3726
|
+
value: tooltip,
|
|
3727
|
+
forId: id,
|
|
3728
|
+
element: props.element,
|
|
3729
|
+
children: label
|
|
3730
|
+
})
|
|
3731
|
+
}), jsx("select", {
|
|
3732
|
+
ref: ref,
|
|
3733
|
+
id: prefixId$3(id),
|
|
3734
|
+
name: id,
|
|
3735
|
+
class: "bio-properties-panel-input",
|
|
3736
|
+
onInput: handleChange,
|
|
3737
|
+
onFocus: onFocus,
|
|
3738
|
+
onBlur: onBlur,
|
|
3739
|
+
value: localValue,
|
|
3740
|
+
disabled: disabled,
|
|
3741
|
+
children: options.map((option, idx) => {
|
|
3742
|
+
if (option.children) {
|
|
3743
|
+
return jsx("optgroup", {
|
|
3744
|
+
label: option.label,
|
|
3745
|
+
children: option.children.map((child, idx) => jsx("option", {
|
|
3746
|
+
value: child.value,
|
|
3747
|
+
disabled: child.disabled,
|
|
3748
|
+
children: child.label
|
|
3749
|
+
}, idx))
|
|
3750
|
+
}, idx);
|
|
3751
|
+
}
|
|
3752
|
+
return jsx("option", {
|
|
3753
|
+
value: option.value,
|
|
3754
|
+
disabled: option.disabled,
|
|
3755
|
+
children: option.label
|
|
3756
|
+
}, idx);
|
|
3757
|
+
})
|
|
3758
|
+
})]
|
|
3759
|
+
});
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
/**
|
|
3763
|
+
* @param {object} props
|
|
3764
|
+
* @param {object} props.element
|
|
3765
|
+
* @param {string} props.id
|
|
3766
|
+
* @param {string} [props.description]
|
|
3767
|
+
* @param {string} props.label
|
|
3768
|
+
* @param {Function} props.getValue
|
|
3769
|
+
* @param {Function} props.setValue
|
|
3770
|
+
* @param {Function} props.onFocus
|
|
3771
|
+
* @param {Function} props.onBlur
|
|
3772
|
+
* @param {Function} props.getOptions
|
|
3773
|
+
* @param {boolean} [props.disabled]
|
|
3774
|
+
* @param {Function} [props.validate]
|
|
3775
|
+
* @param {string|import('preact').Component} props.tooltip
|
|
3776
|
+
*/
|
|
3777
|
+
function SelectEntry(props) {
|
|
3778
|
+
const {
|
|
3779
|
+
element,
|
|
3780
|
+
id,
|
|
3781
|
+
description,
|
|
3782
|
+
label,
|
|
3783
|
+
getValue,
|
|
3784
|
+
setValue,
|
|
3785
|
+
getOptions,
|
|
3786
|
+
disabled,
|
|
3787
|
+
onFocus,
|
|
3788
|
+
onBlur,
|
|
3789
|
+
validate,
|
|
3790
|
+
tooltip
|
|
3791
|
+
} = props;
|
|
3792
|
+
const options = getOptions(element);
|
|
3793
|
+
const globalError = useError(id);
|
|
3794
|
+
const [localError, setLocalError] = useState(null);
|
|
3795
|
+
let value = getValue(element);
|
|
3796
|
+
useEffect(() => {
|
|
3797
|
+
if (isFunction(validate)) {
|
|
3798
|
+
const newValidationError = validate(value) || null;
|
|
3799
|
+
setLocalError(newValidationError);
|
|
3800
|
+
}
|
|
3801
|
+
}, [value]);
|
|
3802
|
+
const onChange = newValue => {
|
|
3803
|
+
let newValidationError = null;
|
|
3804
|
+
if (isFunction(validate)) {
|
|
3805
|
+
newValidationError = validate(newValue) || null;
|
|
3806
|
+
}
|
|
3807
|
+
setValue(newValue, newValidationError);
|
|
3808
|
+
setLocalError(newValidationError);
|
|
3809
|
+
};
|
|
3810
|
+
const error = globalError || localError;
|
|
3811
|
+
return jsxs("div", {
|
|
3812
|
+
class: classnames('bio-properties-panel-entry', error ? 'has-error' : ''),
|
|
3813
|
+
"data-entry-id": id,
|
|
3814
|
+
children: [jsx(Select, {
|
|
3815
|
+
id: id,
|
|
3816
|
+
label: label,
|
|
3817
|
+
value: value,
|
|
3818
|
+
onChange: onChange,
|
|
3819
|
+
onFocus: onFocus,
|
|
3820
|
+
onBlur: onBlur,
|
|
3821
|
+
options: options,
|
|
3822
|
+
disabled: disabled,
|
|
3823
|
+
tooltip: tooltip,
|
|
3824
|
+
element: element
|
|
3825
|
+
}, element), error && jsx("div", {
|
|
3826
|
+
class: "bio-properties-panel-error",
|
|
3827
|
+
children: error
|
|
3828
|
+
}), jsx(Description, {
|
|
3829
|
+
forId: id,
|
|
3830
|
+
element: element,
|
|
3831
|
+
value: description
|
|
3832
|
+
})]
|
|
3833
|
+
});
|
|
3834
|
+
}
|
|
3835
|
+
function isEdited$3(node) {
|
|
3836
|
+
return node && !!node.value;
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3839
|
+
// helpers /////////////////
|
|
3840
|
+
|
|
3841
|
+
function prefixId$3(id) {
|
|
3842
|
+
return `bio-properties-panel-${id}`;
|
|
3843
|
+
}
|
|
3844
|
+
|
|
3845
|
+
function Simple(props) {
|
|
3846
|
+
const {
|
|
3847
|
+
debounce,
|
|
3848
|
+
disabled,
|
|
3849
|
+
element,
|
|
3850
|
+
getValue,
|
|
3851
|
+
id,
|
|
3852
|
+
onBlur,
|
|
3853
|
+
onFocus,
|
|
3854
|
+
setValue
|
|
3855
|
+
} = props;
|
|
3856
|
+
const value = getValue(element);
|
|
3857
|
+
const [localValue, setLocalValue] = useState(value);
|
|
3858
|
+
const handleInputCallback = useMemo(() => {
|
|
3859
|
+
return debounce(({
|
|
3860
|
+
target
|
|
3861
|
+
}) => setValue(target.value.length ? target.value : undefined));
|
|
3862
|
+
}, [setValue, debounce]);
|
|
3863
|
+
const handleInput = e => {
|
|
3864
|
+
handleInputCallback(e);
|
|
3865
|
+
setLocalValue(e.target.value);
|
|
3866
|
+
};
|
|
3867
|
+
useEffect(() => {
|
|
3868
|
+
if (value === localValue) {
|
|
3869
|
+
return;
|
|
3870
|
+
}
|
|
3871
|
+
setLocalValue(value);
|
|
3872
|
+
}, [value]);
|
|
3873
|
+
return jsx("div", {
|
|
3874
|
+
class: "bio-properties-panel-simple",
|
|
3875
|
+
children: jsx("input", {
|
|
3876
|
+
id: prefixId$2(id),
|
|
3877
|
+
type: "text",
|
|
3878
|
+
name: id,
|
|
3879
|
+
spellCheck: "false",
|
|
3880
|
+
autoComplete: "off",
|
|
3881
|
+
disabled: disabled,
|
|
3882
|
+
class: "bio-properties-panel-input",
|
|
3883
|
+
onInput: handleInput,
|
|
3884
|
+
"aria-label": localValue || '<empty>',
|
|
3885
|
+
onFocus: onFocus,
|
|
3886
|
+
onBlur: onBlur,
|
|
3887
|
+
value: localValue
|
|
3888
|
+
}, element)
|
|
3889
|
+
});
|
|
3890
|
+
}
|
|
3891
|
+
function isEdited$2(node) {
|
|
3892
|
+
return node && !!node.value;
|
|
3893
|
+
}
|
|
3894
|
+
|
|
3895
|
+
// helpers /////////////////
|
|
3896
|
+
|
|
3897
|
+
function prefixId$2(id) {
|
|
3898
|
+
return `bio-properties-panel-${id}`;
|
|
3899
|
+
}
|
|
3900
|
+
|
|
3901
|
+
function resizeToContents(element) {
|
|
3902
|
+
element.style.height = 'auto';
|
|
3903
|
+
|
|
3904
|
+
// a 2px pixel offset is required to prevent scrollbar from
|
|
3905
|
+
// appearing on OS with a full length scroll bar (Windows/Linux)
|
|
3906
|
+
element.style.height = `${element.scrollHeight + 2}px`;
|
|
3907
|
+
}
|
|
3908
|
+
function TextArea(props) {
|
|
3909
|
+
const {
|
|
3910
|
+
id,
|
|
3911
|
+
label,
|
|
3912
|
+
debounce,
|
|
3913
|
+
onInput,
|
|
3914
|
+
value = '',
|
|
3915
|
+
disabled,
|
|
3916
|
+
monospace,
|
|
3917
|
+
onFocus,
|
|
3918
|
+
onBlur,
|
|
3919
|
+
autoResize,
|
|
3920
|
+
rows = autoResize ? 1 : 2,
|
|
3921
|
+
tooltip
|
|
3922
|
+
} = props;
|
|
3923
|
+
const [localValue, setLocalValue] = useState(value);
|
|
3924
|
+
const ref = useShowEntryEvent(id);
|
|
3925
|
+
const handleInputCallback = useMemo(() => {
|
|
3926
|
+
return debounce(({
|
|
3927
|
+
target
|
|
3928
|
+
}) => onInput(target.value.length ? target.value : undefined));
|
|
3929
|
+
}, [onInput, debounce]);
|
|
3930
|
+
const handleInput = e => {
|
|
3931
|
+
handleInputCallback(e);
|
|
3932
|
+
autoResize && resizeToContents(e.target);
|
|
3933
|
+
setLocalValue(e.target.value);
|
|
3934
|
+
};
|
|
3935
|
+
useLayoutEffect(() => {
|
|
3936
|
+
autoResize && resizeToContents(ref.current);
|
|
3937
|
+
}, []);
|
|
3938
|
+
useEffect(() => {
|
|
3939
|
+
if (value === localValue) {
|
|
3940
|
+
return;
|
|
3941
|
+
}
|
|
3942
|
+
setLocalValue(value);
|
|
3943
|
+
}, [value]);
|
|
3944
|
+
return jsxs("div", {
|
|
3945
|
+
class: "bio-properties-panel-textarea",
|
|
3946
|
+
children: [jsx("label", {
|
|
3947
|
+
for: prefixId$1(id),
|
|
3948
|
+
class: "bio-properties-panel-label",
|
|
3949
|
+
children: jsx(TooltipWrapper, {
|
|
3950
|
+
value: tooltip,
|
|
3951
|
+
forId: id,
|
|
3952
|
+
element: props.element,
|
|
3953
|
+
children: label
|
|
3954
|
+
})
|
|
3955
|
+
}), jsx("textarea", {
|
|
3956
|
+
ref: ref,
|
|
3957
|
+
id: prefixId$1(id),
|
|
3958
|
+
name: id,
|
|
3959
|
+
spellCheck: "false",
|
|
3960
|
+
class: classnames('bio-properties-panel-input', monospace ? 'bio-properties-panel-input-monospace' : '', autoResize ? 'auto-resize' : ''),
|
|
3961
|
+
onInput: handleInput,
|
|
3962
|
+
onFocus: onFocus,
|
|
3963
|
+
onBlur: onBlur,
|
|
3964
|
+
rows: rows,
|
|
3965
|
+
value: localValue,
|
|
3966
|
+
disabled: disabled,
|
|
3967
|
+
"data-gramm": "false"
|
|
3968
|
+
})]
|
|
3969
|
+
});
|
|
3970
|
+
}
|
|
3971
|
+
|
|
3972
|
+
/**
|
|
3973
|
+
* @param {object} props
|
|
3974
|
+
* @param {object} props.element
|
|
3975
|
+
* @param {string} props.id
|
|
3976
|
+
* @param {string} props.description
|
|
3977
|
+
* @param {boolean} props.debounce
|
|
3978
|
+
* @param {string} props.label
|
|
3979
|
+
* @param {Function} props.getValue
|
|
3980
|
+
* @param {Function} props.setValue
|
|
3981
|
+
* @param {Function} props.onFocus
|
|
3982
|
+
* @param {Function} props.onBlur
|
|
3983
|
+
* @param {number} props.rows
|
|
3984
|
+
* @param {boolean} props.monospace
|
|
3985
|
+
* @param {Function} [props.validate]
|
|
3986
|
+
* @param {boolean} [props.disabled]
|
|
3987
|
+
*/
|
|
3988
|
+
function TextAreaEntry(props) {
|
|
3989
|
+
const {
|
|
3990
|
+
element,
|
|
3991
|
+
id,
|
|
3992
|
+
description,
|
|
3993
|
+
debounce,
|
|
3994
|
+
label,
|
|
3995
|
+
getValue,
|
|
3996
|
+
setValue,
|
|
3997
|
+
rows,
|
|
3998
|
+
monospace,
|
|
3999
|
+
disabled,
|
|
4000
|
+
validate,
|
|
4001
|
+
onFocus,
|
|
4002
|
+
onBlur,
|
|
4003
|
+
autoResize,
|
|
4004
|
+
tooltip
|
|
4005
|
+
} = props;
|
|
4006
|
+
const globalError = useError(id);
|
|
4007
|
+
const [localError, setLocalError] = useState(null);
|
|
4008
|
+
let value = getValue(element);
|
|
4009
|
+
useEffect(() => {
|
|
4010
|
+
if (isFunction(validate)) {
|
|
4011
|
+
const newValidationError = validate(value) || null;
|
|
4012
|
+
setLocalError(newValidationError);
|
|
4013
|
+
}
|
|
4014
|
+
}, [value]);
|
|
4015
|
+
const onInput = newValue => {
|
|
4016
|
+
let newValidationError = null;
|
|
4017
|
+
if (isFunction(validate)) {
|
|
4018
|
+
newValidationError = validate(newValue) || null;
|
|
4019
|
+
}
|
|
4020
|
+
setValue(newValue, newValidationError);
|
|
4021
|
+
setLocalError(newValidationError);
|
|
4022
|
+
};
|
|
4023
|
+
const error = globalError || localError;
|
|
4024
|
+
return jsxs("div", {
|
|
4025
|
+
class: classnames('bio-properties-panel-entry', error ? 'has-error' : ''),
|
|
4026
|
+
"data-entry-id": id,
|
|
4027
|
+
children: [jsx(TextArea, {
|
|
4028
|
+
id: id,
|
|
4029
|
+
label: label,
|
|
4030
|
+
value: value,
|
|
4031
|
+
onInput: onInput,
|
|
4032
|
+
onFocus: onFocus,
|
|
4033
|
+
onBlur: onBlur,
|
|
4034
|
+
rows: rows,
|
|
4035
|
+
debounce: debounce,
|
|
4036
|
+
monospace: monospace,
|
|
4037
|
+
disabled: disabled,
|
|
4038
|
+
autoResize: autoResize,
|
|
4039
|
+
tooltip: tooltip,
|
|
4040
|
+
element: element
|
|
4041
|
+
}, element), error && jsx("div", {
|
|
4042
|
+
class: "bio-properties-panel-error",
|
|
4043
|
+
children: error
|
|
4044
|
+
}), jsx(Description, {
|
|
4045
|
+
forId: id,
|
|
4046
|
+
element: element,
|
|
4047
|
+
value: description
|
|
4048
|
+
})]
|
|
4049
|
+
});
|
|
4050
|
+
}
|
|
4051
|
+
function isEdited$1(node) {
|
|
4052
|
+
return node && !!node.value;
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
// helpers /////////////////
|
|
4056
|
+
|
|
4057
|
+
function prefixId$1(id) {
|
|
4058
|
+
return `bio-properties-panel-${id}`;
|
|
4059
|
+
}
|
|
4060
|
+
|
|
4061
|
+
function Textfield(props) {
|
|
4062
|
+
const {
|
|
4063
|
+
debounce,
|
|
4064
|
+
disabled = false,
|
|
4065
|
+
id,
|
|
4066
|
+
label,
|
|
4067
|
+
onInput,
|
|
4068
|
+
onFocus,
|
|
4069
|
+
onBlur,
|
|
4070
|
+
value = '',
|
|
4071
|
+
tooltip
|
|
4072
|
+
} = props;
|
|
4073
|
+
const [localValue, setLocalValue] = useState(value || '');
|
|
4074
|
+
const ref = useShowEntryEvent(id);
|
|
4075
|
+
const handleInputCallback = useMemo(() => {
|
|
4076
|
+
return debounce(({
|
|
4077
|
+
target
|
|
4078
|
+
}) => onInput(target.value.length ? target.value : undefined));
|
|
4079
|
+
}, [onInput, debounce]);
|
|
4080
|
+
const handleInput = e => {
|
|
4081
|
+
handleInputCallback(e);
|
|
4082
|
+
setLocalValue(e.target.value);
|
|
4083
|
+
};
|
|
4084
|
+
useEffect(() => {
|
|
4085
|
+
if (value === localValue) {
|
|
4086
|
+
return;
|
|
4087
|
+
}
|
|
4088
|
+
setLocalValue(value);
|
|
4089
|
+
}, [value]);
|
|
4090
|
+
return jsxs("div", {
|
|
4091
|
+
class: "bio-properties-panel-textfield",
|
|
4092
|
+
children: [jsx("label", {
|
|
4093
|
+
for: prefixId(id),
|
|
4094
|
+
class: "bio-properties-panel-label",
|
|
4095
|
+
children: jsx(TooltipWrapper, {
|
|
4096
|
+
value: tooltip,
|
|
4097
|
+
forId: id,
|
|
4098
|
+
element: props.element,
|
|
4099
|
+
children: label
|
|
4100
|
+
})
|
|
4101
|
+
}), jsx("input", {
|
|
4102
|
+
ref: ref,
|
|
4103
|
+
id: prefixId(id),
|
|
4104
|
+
type: "text",
|
|
4105
|
+
name: id,
|
|
4106
|
+
spellCheck: "false",
|
|
4107
|
+
autoComplete: "off",
|
|
4108
|
+
disabled: disabled,
|
|
4109
|
+
class: "bio-properties-panel-input",
|
|
4110
|
+
onInput: handleInput,
|
|
4111
|
+
onFocus: onFocus,
|
|
4112
|
+
onBlur: onBlur,
|
|
4113
|
+
value: localValue
|
|
4114
|
+
})]
|
|
4115
|
+
});
|
|
4116
|
+
}
|
|
4117
|
+
|
|
4118
|
+
/**
|
|
4119
|
+
* @param {Object} props
|
|
4120
|
+
* @param {Object} props.element
|
|
4121
|
+
* @param {String} props.id
|
|
4122
|
+
* @param {String} props.description
|
|
4123
|
+
* @param {Boolean} props.debounce
|
|
4124
|
+
* @param {Boolean} props.disabled
|
|
4125
|
+
* @param {String} props.label
|
|
4126
|
+
* @param {Function} props.getValue
|
|
4127
|
+
* @param {Function} props.setValue
|
|
4128
|
+
* @param {Function} props.onFocus
|
|
4129
|
+
* @param {Function} props.onBlur
|
|
4130
|
+
* @param {string|import('preact').Component} props.tooltip
|
|
4131
|
+
* @param {Function} props.validate
|
|
4132
|
+
*/
|
|
4133
|
+
function TextfieldEntry(props) {
|
|
4134
|
+
const {
|
|
4135
|
+
element,
|
|
4136
|
+
id,
|
|
4137
|
+
description,
|
|
4138
|
+
debounce,
|
|
4139
|
+
disabled,
|
|
4140
|
+
label,
|
|
4141
|
+
getValue,
|
|
4142
|
+
setValue,
|
|
4143
|
+
validate,
|
|
4144
|
+
onFocus,
|
|
4145
|
+
onBlur,
|
|
4146
|
+
tooltip
|
|
4147
|
+
} = props;
|
|
4148
|
+
const globalError = useError(id);
|
|
4149
|
+
const [localError, setLocalError] = useState(null);
|
|
4150
|
+
let value = getValue(element);
|
|
4151
|
+
useEffect(() => {
|
|
4152
|
+
if (isFunction(validate)) {
|
|
4153
|
+
const newValidationError = validate(value) || null;
|
|
4154
|
+
setLocalError(newValidationError);
|
|
4155
|
+
}
|
|
4156
|
+
}, [value]);
|
|
4157
|
+
const onInput = newValue => {
|
|
4158
|
+
let newValidationError = null;
|
|
4159
|
+
if (isFunction(validate)) {
|
|
4160
|
+
newValidationError = validate(newValue) || null;
|
|
4161
|
+
}
|
|
4162
|
+
setValue(newValue, newValidationError);
|
|
4163
|
+
setLocalError(newValidationError);
|
|
4164
|
+
};
|
|
4165
|
+
const error = globalError || localError;
|
|
4166
|
+
return jsxs("div", {
|
|
4167
|
+
class: classnames('bio-properties-panel-entry', error ? 'has-error' : ''),
|
|
4168
|
+
"data-entry-id": id,
|
|
4169
|
+
children: [jsx(Textfield, {
|
|
4170
|
+
debounce: debounce,
|
|
4171
|
+
disabled: disabled,
|
|
4172
|
+
id: id,
|
|
4173
|
+
label: label,
|
|
4174
|
+
onInput: onInput,
|
|
4175
|
+
onFocus: onFocus,
|
|
4176
|
+
onBlur: onBlur,
|
|
4177
|
+
value: value,
|
|
4178
|
+
tooltip: tooltip,
|
|
4179
|
+
element: element
|
|
4180
|
+
}, element), error && jsx("div", {
|
|
4181
|
+
class: "bio-properties-panel-error",
|
|
4182
|
+
children: error
|
|
4183
|
+
}), jsx(Description, {
|
|
4184
|
+
forId: id,
|
|
4185
|
+
element: element,
|
|
4186
|
+
value: description
|
|
4187
|
+
})]
|
|
4188
|
+
});
|
|
4189
|
+
}
|
|
4190
|
+
function isEdited(node) {
|
|
4191
|
+
return node && !!node.value;
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
// helpers /////////////////
|
|
4195
|
+
|
|
4196
|
+
function prefixId(id) {
|
|
4197
|
+
return `bio-properties-panel-${id}`;
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4200
|
+
const DEFAULT_DEBOUNCE_TIME = 300;
|
|
4201
|
+
function debounceInput(debounceDelay) {
|
|
4202
|
+
return function _debounceInput(fn) {
|
|
4203
|
+
if (debounceDelay !== false) {
|
|
4204
|
+
var debounceTime = isNumber(debounceDelay) ? debounceDelay : DEFAULT_DEBOUNCE_TIME;
|
|
4205
|
+
return debounce(fn, debounceTime);
|
|
4206
|
+
} else {
|
|
4207
|
+
return fn;
|
|
4208
|
+
}
|
|
4209
|
+
};
|
|
4210
|
+
}
|
|
4211
|
+
debounceInput.$inject = ['config.debounceInput'];
|
|
4212
|
+
|
|
4213
|
+
var index$1 = {
|
|
4214
|
+
debounceInput: ['factory', debounceInput]
|
|
4215
|
+
};
|
|
4216
|
+
|
|
4217
|
+
class FeelPopupModule {
|
|
4218
|
+
constructor(eventBus) {
|
|
4219
|
+
this._eventBus = eventBus;
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4222
|
+
/**
|
|
4223
|
+
* Check if the FEEL popup is open.
|
|
4224
|
+
* @return {Boolean}
|
|
4225
|
+
*/
|
|
4226
|
+
isOpen() {
|
|
4227
|
+
return this._eventBus.fire('feelPopup._isOpen');
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
/**
|
|
4231
|
+
* Open the FEEL popup.
|
|
4232
|
+
*
|
|
4233
|
+
* @param {String} entryId
|
|
4234
|
+
* @param {Object} popupConfig
|
|
4235
|
+
* @param {HTMLElement} sourceElement
|
|
4236
|
+
*/
|
|
4237
|
+
open(entryId, popupConfig, sourceElement) {
|
|
4238
|
+
return this._eventBus.fire('feelPopup._open', {
|
|
4239
|
+
entryId,
|
|
4240
|
+
popupConfig,
|
|
4241
|
+
sourceElement
|
|
4242
|
+
});
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4245
|
+
/**
|
|
4246
|
+
* Close the FEEL popup.
|
|
4247
|
+
*/
|
|
4248
|
+
close() {
|
|
4249
|
+
return this._eventBus.fire('feelPopup._close');
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
FeelPopupModule.$inject = ['eventBus'];
|
|
4253
|
+
|
|
4254
|
+
var index = {
|
|
4255
|
+
feelPopup: ['type', FeelPopupModule]
|
|
4256
|
+
};
|
|
4257
|
+
|
|
4258
|
+
export { ArrowIcon, CheckboxEntry, CollapsibleEntry, CreateIcon, index$1 as DebounceInputModule, DeleteIcon, DescriptionContext, Description as DescriptionEntry, DragIcon, DropdownButton, ErrorsContext, EventContext, ExternalLinkIcon, FeelCheckboxEntry, FeelEntry, FeelIcon$1 as FeelIcon, FeelNumberEntry, index as FeelPopupModule, FeelTemplatingEntry, FeelTextAreaEntry, FeelToggleSwitchEntry, Group, Header, HeaderButton, LayoutContext, List as ListEntry, ListGroup, ListItem, NumberFieldEntry, Placeholder, Popup, PropertiesPanel, LayoutContext as PropertiesPanelContext, SelectEntry, Simple as SimpleEntry, TemplatingEntry, TextAreaEntry, TextfieldEntry as TextFieldEntry, ToggleSwitchEntry, TooltipContext, isEdited$5 as isCheckboxEntryEdited, isEdited$6 as isFeelEntryEdited, isEdited$7 as isNumberFieldEntryEdited, isEdited$3 as isSelectEntryEdited, isEdited$2 as isSimpleEntryEdited, isEdited$4 as isTemplatingEntryEdited, isEdited$1 as isTextAreaEntryEdited, isEdited as isTextFieldEntryEdited, isEdited$8 as isToggleSwitchEntryEdited, useDescriptionContext, useError, useErrors, useEvent, useKeyFactory, useLayoutState, usePrevious, useShowEntryEvent, useStaticCallback, useStickyIntersectionObserver, useTooltipContext };
|
|
4259
|
+
//# sourceMappingURL=index.esm.js.map
|