ngx-vflow 1.3.1 → 1.4.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/esm2022/lib/vflow/components/node/node.component.mjs +4 -2
- package/esm2022/lib/vflow/components/vflow/vflow.component.mjs +10 -2
- package/esm2022/lib/vflow/public-components/resizable/resizable.component.mjs +48 -68
- package/esm2022/lib/vflow/services/flow-settings.service.mjs +2 -1
- package/esm2022/lib/vflow/services/node-rendering.service.mjs +7 -6
- package/fesm2022/ngx-vflow.mjs +66 -74
- package/fesm2022/ngx-vflow.mjs.map +1 -1
- package/lib/vflow/components/vflow/vflow.component.d.ts +5 -1
- package/lib/vflow/public-components/resizable/resizable.component.d.ts +1 -1
- package/lib/vflow/services/flow-settings.service.d.ts +1 -0
- package/package.json +1 -1
package/fesm2022/ngx-vflow.mjs
CHANGED
|
@@ -150,6 +150,7 @@ function clamp(value, min = 0, max = 1) {
|
|
|
150
150
|
class FlowSettingsService {
|
|
151
151
|
constructor() {
|
|
152
152
|
this.entitiesSelectable = signal(true);
|
|
153
|
+
this.elevateNodesOnSelect = signal(true);
|
|
153
154
|
/**
|
|
154
155
|
* @see {VflowComponent.view}
|
|
155
156
|
*/
|
|
@@ -1613,14 +1614,15 @@ class NodeRenderingService {
|
|
|
1613
1614
|
});
|
|
1614
1615
|
}
|
|
1615
1616
|
pullNode(node) {
|
|
1616
|
-
|
|
1617
|
+
const isAlreadyOnTop = node.renderOrder() !== 0 && this.maxOrder() === node.renderOrder();
|
|
1618
|
+
// TODO: does not work for group nodes
|
|
1619
|
+
if (isAlreadyOnTop) {
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1617
1622
|
// pull node
|
|
1618
1623
|
node.renderOrder.set(this.maxOrder() + 1);
|
|
1619
1624
|
// pull children
|
|
1620
|
-
this.
|
|
1621
|
-
.nodes()
|
|
1622
|
-
.filter((n) => n.parent() === node)
|
|
1623
|
-
.forEach((n) => this.pullNode(n));
|
|
1625
|
+
node.children().forEach((n) => this.pullNode(n));
|
|
1624
1626
|
}
|
|
1625
1627
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NodeRenderingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
1626
1628
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NodeRenderingService }); }
|
|
@@ -2241,10 +2243,9 @@ class ResizableComponent {
|
|
|
2241
2243
|
resize(event) {
|
|
2242
2244
|
if (!this.resizeSide)
|
|
2243
2245
|
return;
|
|
2244
|
-
if (this.isResizeConstrained(event))
|
|
2245
|
-
return;
|
|
2246
2246
|
const offset = calcOffset(event.movementX, event.movementY, this.zoom());
|
|
2247
|
-
const
|
|
2247
|
+
const resized = applyResize(this.resizeSide, this.model, offset, this.getDistanceToEdge(event));
|
|
2248
|
+
const { x, y, width, height } = constrainRect(resized, this.model, this.resizeSide, this.minWidth, this.minHeight);
|
|
2248
2249
|
this.model.setPoint({ x, y });
|
|
2249
2250
|
this.model.width.set(width);
|
|
2250
2251
|
this.model.height.set(height);
|
|
@@ -2253,41 +2254,15 @@ class ResizableComponent {
|
|
|
2253
2254
|
this.resizeSide = null;
|
|
2254
2255
|
this.model.resizing.set(false);
|
|
2255
2256
|
}
|
|
2256
|
-
|
|
2257
|
-
const flowPoint = this.spacePointContext.documentPointToFlowPoint({ x, y });
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
}
|
|
2266
|
-
if (this.resizeSide?.includes('left')) {
|
|
2267
|
-
if (movementX < 0 && flowPoint.x > this.model.point().x) {
|
|
2268
|
-
return true;
|
|
2269
|
-
}
|
|
2270
|
-
if (movementX > 0 && flowPoint.x < this.model.point().x) {
|
|
2271
|
-
return true;
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
if (this.resizeSide?.includes('bottom')) {
|
|
2275
|
-
if (movementY > 0 && flowPoint.y < this.model.point().y + this.model.size().height) {
|
|
2276
|
-
return true;
|
|
2277
|
-
}
|
|
2278
|
-
if (movementY < 0 && flowPoint.y > this.model.point().y + this.model.size().height) {
|
|
2279
|
-
return true;
|
|
2280
|
-
}
|
|
2281
|
-
}
|
|
2282
|
-
if (this.resizeSide?.includes('top')) {
|
|
2283
|
-
if (movementY < 0 && flowPoint.y > this.model.point().y) {
|
|
2284
|
-
return true;
|
|
2285
|
-
}
|
|
2286
|
-
if (movementY > 0 && flowPoint.y < this.model.point().y) {
|
|
2287
|
-
return true;
|
|
2288
|
-
}
|
|
2289
|
-
}
|
|
2290
|
-
return false;
|
|
2257
|
+
getDistanceToEdge(event) {
|
|
2258
|
+
const flowPoint = this.spacePointContext.documentPointToFlowPoint({ x: event.x, y: event.y });
|
|
2259
|
+
const { x, y } = this.model.globalPoint();
|
|
2260
|
+
return {
|
|
2261
|
+
left: flowPoint.x - x,
|
|
2262
|
+
right: flowPoint.x - (x + this.model.width()),
|
|
2263
|
+
top: flowPoint.y - y,
|
|
2264
|
+
bottom: flowPoint.y - (y + this.model.height()),
|
|
2265
|
+
};
|
|
2291
2266
|
}
|
|
2292
2267
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ResizableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2293
2268
|
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "17.3.12", type: ResizableComponent, isStandalone: true, selector: "[resizable]", inputs: { resizable: { classPropertyName: "resizable", publicName: "resizable", isSignal: true, isRequired: false, transformFunction: null }, resizerColor: { classPropertyName: "resizerColor", publicName: "resizerColor", isSignal: true, isRequired: false, transformFunction: null }, gap: { classPropertyName: "gap", publicName: "gap", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "resizer", first: true, predicate: ["resizer"], descendants: true, isSignal: true }], ngImport: i0, template: "<ng-template #resizer>\n <svg:g>\n <!-- top line -->\n <svg:line\n class=\"top\"\n stroke-width=\"2\"\n [attr.x1]=\"lineGap\"\n [attr.y1]=\"-gap()\"\n [attr.x2]=\"model.size().width - lineGap\"\n [attr.y2]=\"-gap()\"\n [attr.stroke]=\"resizerColor()\"\n (pointerStart)=\"startResize('top', $event)\" />\n <!-- Left line -->\n <svg:line\n class=\"left\"\n stroke-width=\"2\"\n [attr.x1]=\"-gap()\"\n [attr.y1]=\"lineGap\"\n [attr.x2]=\"-gap()\"\n [attr.y2]=\"model.size().height - lineGap\"\n [attr.stroke]=\"resizerColor()\"\n (pointerStart)=\"startResize('left', $event)\" />\n <!-- Bottom line -->\n <svg:line\n class=\"bottom\"\n stroke-width=\"2\"\n [attr.x1]=\"lineGap\"\n [attr.y1]=\"model.size().height + gap()\"\n [attr.x2]=\"model.size().width - lineGap\"\n [attr.y2]=\"model.size().height + gap()\"\n [attr.stroke]=\"resizerColor()\"\n (pointerStart)=\"startResize('bottom', $event)\" />\n <!-- Right line -->\n <svg:line\n class=\"right\"\n stroke-width=\"2\"\n [attr.x1]=\"model.size().width + gap()\"\n [attr.y1]=\"lineGap\"\n [attr.x2]=\"model.size().width + gap()\"\n [attr.y2]=\"model.size().height - lineGap\"\n [attr.stroke]=\"resizerColor()\"\n (pointerStart)=\"startResize('right', $event)\" />\n\n <!-- Top Left -->\n <svg:rect\n class=\"top-left\"\n [attr.x]=\"-(handleSize / 2) - gap()\"\n [attr.y]=\"-(handleSize / 2) - gap()\"\n [attr.width]=\"handleSize\"\n [attr.height]=\"handleSize\"\n [attr.fill]=\"resizerColor()\"\n (pointerStart)=\"startResize('top-left', $event)\" />\n\n <!-- Top right -->\n <svg:rect\n class=\"top-right\"\n [attr.x]=\"model.size().width - handleSize / 2 + gap()\"\n [attr.y]=\"-(handleSize / 2) - gap()\"\n [attr.width]=\"handleSize\"\n [attr.height]=\"handleSize\"\n [attr.fill]=\"resizerColor()\"\n (pointerStart)=\"startResize('top-right', $event)\" />\n\n <!-- Bottom left -->\n <svg:rect\n class=\"bottom-left\"\n [attr.x]=\"-(handleSize / 2) - gap()\"\n [attr.y]=\"model.size().height - handleSize / 2 + gap()\"\n [attr.width]=\"handleSize\"\n [attr.height]=\"handleSize\"\n [attr.fill]=\"resizerColor()\"\n (pointerStart)=\"startResize('bottom-left', $event)\" />\n\n <!-- Bottom right -->\n <svg:rect\n class=\"bottom-right\"\n [attr.x]=\"model.size().width - handleSize / 2 + gap()\"\n [attr.y]=\"model.size().height - handleSize / 2 + gap()\"\n [attr.width]=\"handleSize\"\n [attr.height]=\"handleSize\"\n [attr.fill]=\"resizerColor()\"\n (pointerStart)=\"startResize('bottom-right', $event)\" />\n </svg:g>\n</ng-template>\n\n<ng-content />\n", styles: [".top{cursor:n-resize}.left{cursor:w-resize}.right{cursor:e-resize}.bottom{cursor:s-resize}.top-left{cursor:nw-resize}.top-right{cursor:ne-resize}.bottom-left{cursor:sw-resize}.bottom-right{cursor:se-resize}\n"], dependencies: [{ kind: "directive", type: PointerDirective, selector: "[pointerStart], [pointerEnd], [pointerOver], [pointerOut]", outputs: ["pointerOver", "pointerOut", "pointerStart", "pointerEnd"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
@@ -2305,43 +2280,44 @@ function calcOffset(movementX, movementY, zoom) {
|
|
|
2305
2280
|
offsetY: round(movementY / zoom),
|
|
2306
2281
|
};
|
|
2307
2282
|
}
|
|
2308
|
-
function applyResize(side, model, offset) {
|
|
2283
|
+
function applyResize(side, model, offset, distanceToEdge) {
|
|
2309
2284
|
const { offsetX, offsetY } = offset;
|
|
2310
2285
|
const { x, y } = model.point();
|
|
2311
|
-
const
|
|
2286
|
+
const width = model.width();
|
|
2287
|
+
const height = model.height();
|
|
2312
2288
|
// Handle each case of resizing (top, bottom, left, right, corners)
|
|
2313
2289
|
switch (side) {
|
|
2314
2290
|
case 'left':
|
|
2315
|
-
return { x: x + offsetX, y, width: width - offsetX, height };
|
|
2291
|
+
return { x: x + offsetX + distanceToEdge.left, y, width: width - offsetX - distanceToEdge.left, height };
|
|
2316
2292
|
case 'right':
|
|
2317
|
-
return { x, y, width: width + offsetX, height };
|
|
2293
|
+
return { x, y, width: width + offsetX + distanceToEdge.right, height };
|
|
2318
2294
|
case 'top':
|
|
2319
|
-
return { x, y: y + offsetY, width, height: height - offsetY };
|
|
2295
|
+
return { x, y: y + offsetY + distanceToEdge.top, width, height: height - offsetY - distanceToEdge.top };
|
|
2320
2296
|
case 'bottom':
|
|
2321
|
-
return { x, y, width, height: height + offsetY };
|
|
2297
|
+
return { x, y, width, height: height + offsetY + distanceToEdge.bottom };
|
|
2322
2298
|
case 'top-left':
|
|
2323
2299
|
return {
|
|
2324
|
-
x: x + offsetX,
|
|
2325
|
-
y: y + offsetY,
|
|
2326
|
-
width: width - offsetX,
|
|
2327
|
-
height: height - offsetY,
|
|
2300
|
+
x: x + offsetX + distanceToEdge.left,
|
|
2301
|
+
y: y + offsetY + distanceToEdge.top,
|
|
2302
|
+
width: width - offsetX - distanceToEdge.left,
|
|
2303
|
+
height: height - offsetY - distanceToEdge.top,
|
|
2328
2304
|
};
|
|
2329
2305
|
case 'top-right':
|
|
2330
2306
|
return {
|
|
2331
2307
|
x,
|
|
2332
|
-
y: y + offsetY,
|
|
2333
|
-
width: width + offsetX,
|
|
2334
|
-
height: height - offsetY,
|
|
2308
|
+
y: y + offsetY + distanceToEdge.top,
|
|
2309
|
+
width: width + offsetX + distanceToEdge.right,
|
|
2310
|
+
height: height - offsetY - distanceToEdge.top,
|
|
2335
2311
|
};
|
|
2336
2312
|
case 'bottom-left':
|
|
2337
2313
|
return {
|
|
2338
|
-
x: x + offsetX,
|
|
2314
|
+
x: x + offsetX + distanceToEdge.left,
|
|
2339
2315
|
y,
|
|
2340
|
-
width: width - offsetX,
|
|
2341
|
-
height: height + offsetY,
|
|
2316
|
+
width: width - offsetX - distanceToEdge.left,
|
|
2317
|
+
height: height + offsetY + distanceToEdge.bottom,
|
|
2342
2318
|
};
|
|
2343
2319
|
case 'bottom-right':
|
|
2344
|
-
return { x, y, width: width + offsetX, height: height + offsetY };
|
|
2320
|
+
return { x, y, width: width + offsetX + distanceToEdge.right, height: height + offsetY + distanceToEdge.bottom };
|
|
2345
2321
|
}
|
|
2346
2322
|
}
|
|
2347
2323
|
function constrainRect(rect, model, side, minWidth, minHeight) {
|
|
@@ -2353,27 +2329,33 @@ function constrainRect(rect, model, side, minWidth, minHeight) {
|
|
|
2353
2329
|
width = Math.max(minWidth, width);
|
|
2354
2330
|
height = Math.max(minHeight, height);
|
|
2355
2331
|
// Apply left/top constraints based on minimum size
|
|
2356
|
-
x = Math.min(x, model.point().x + model.
|
|
2357
|
-
y = Math.min(y, model.point().y + model.
|
|
2332
|
+
x = Math.min(x, model.point().x + model.width() - minWidth);
|
|
2333
|
+
y = Math.min(y, model.point().y + model.height() - minHeight);
|
|
2358
2334
|
const parent = model.parent();
|
|
2359
2335
|
// 3. Apply maximum size constraints based on parent size (if exists)
|
|
2360
2336
|
if (parent) {
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2337
|
+
const parentWidth = parent.width();
|
|
2338
|
+
const parentHeight = parent.height();
|
|
2339
|
+
const modelX = model.point().x;
|
|
2340
|
+
const modelY = model.point().y;
|
|
2341
|
+
x = Math.max(x, 0);
|
|
2342
|
+
y = Math.max(y, 0);
|
|
2343
|
+
// Stop resizing when hitting left or top boundary
|
|
2344
|
+
if (side.includes('left') && x === 0) {
|
|
2345
|
+
width = Math.min(width, modelX + model.width());
|
|
2365
2346
|
}
|
|
2366
|
-
if (y === 0) {
|
|
2367
|
-
height =
|
|
2347
|
+
if (side.includes('top') && y === 0) {
|
|
2348
|
+
height = Math.min(height, modelY + model.height());
|
|
2368
2349
|
}
|
|
2369
|
-
|
|
2370
|
-
|
|
2350
|
+
// Allow right/bottom resizing without being blocked
|
|
2351
|
+
width = Math.min(width, parentWidth - x);
|
|
2352
|
+
height = Math.min(height, parentHeight - y);
|
|
2371
2353
|
}
|
|
2372
2354
|
const bounds = getNodesBounds(model.children());
|
|
2373
2355
|
// 4. Apply child node constraints (if children exist)
|
|
2374
2356
|
if (bounds) {
|
|
2375
2357
|
if (side.includes('left')) {
|
|
2376
|
-
x = Math.min(x, model.point().x + model.
|
|
2358
|
+
x = Math.min(x, model.point().x + model.width() - (bounds.x + bounds.width));
|
|
2377
2359
|
width = Math.max(width, bounds.x + bounds.width);
|
|
2378
2360
|
}
|
|
2379
2361
|
if (side.includes('right')) {
|
|
@@ -2383,7 +2365,7 @@ function constrainRect(rect, model, side, minWidth, minHeight) {
|
|
|
2383
2365
|
height = Math.max(height, bounds.y + bounds.height);
|
|
2384
2366
|
}
|
|
2385
2367
|
if (side.includes('top')) {
|
|
2386
|
-
y = Math.min(y, model.point().y + model.
|
|
2368
|
+
y = Math.min(y, model.point().y + model.height() - (bounds.y + bounds.height));
|
|
2387
2369
|
height = Math.max(height, bounds.y + bounds.height);
|
|
2388
2370
|
}
|
|
2389
2371
|
}
|
|
@@ -2658,7 +2640,9 @@ class NodeComponent {
|
|
|
2658
2640
|
this.connectionController?.endConnection();
|
|
2659
2641
|
}
|
|
2660
2642
|
pullNode() {
|
|
2661
|
-
this.
|
|
2643
|
+
if (this.flowSettingsService.elevateNodesOnSelect()) {
|
|
2644
|
+
this.nodeRenderingService.pullNode(this.model());
|
|
2645
|
+
}
|
|
2662
2646
|
}
|
|
2663
2647
|
selectNode() {
|
|
2664
2648
|
if (this.flowSettingsService.entitiesSelectable()) {
|
|
@@ -3172,6 +3156,12 @@ class VflowComponent {
|
|
|
3172
3156
|
set snapGrid(value) {
|
|
3173
3157
|
this.flowSettingsService.snapGrid.set(value);
|
|
3174
3158
|
}
|
|
3159
|
+
/**
|
|
3160
|
+
* Raizing z-index for selected node
|
|
3161
|
+
*/
|
|
3162
|
+
set elevateNodesOnSelect(value) {
|
|
3163
|
+
this.flowSettingsService.elevateNodesOnSelect.set(value);
|
|
3164
|
+
}
|
|
3175
3165
|
// #endregion
|
|
3176
3166
|
// #region MAIN_INPUTS
|
|
3177
3167
|
/**
|
|
@@ -3273,7 +3263,7 @@ class VflowComponent {
|
|
|
3273
3263
|
});
|
|
3274
3264
|
}
|
|
3275
3265
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: VflowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3276
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: VflowComponent, isStandalone: true, selector: "vflow", inputs: { view: { classPropertyName: "view", publicName: "view", isSignal: false, isRequired: false, transformFunction: null }, minZoom: { classPropertyName: "minZoom", publicName: "minZoom", isSignal: false, isRequired: false, transformFunction: null }, maxZoom: { classPropertyName: "maxZoom", publicName: "maxZoom", isSignal: false, isRequired: false, transformFunction: null }, background: { classPropertyName: "background", publicName: "background", isSignal: false, isRequired: false, transformFunction: null }, optimization: { classPropertyName: "optimization", publicName: "optimization", isSignal: true, isRequired: false, transformFunction: null }, entitiesSelectable: { classPropertyName: "entitiesSelectable", publicName: "entitiesSelectable", isSignal: false, isRequired: false, transformFunction: null }, keyboardShortcuts: { classPropertyName: "keyboardShortcuts", publicName: "keyboardShortcuts", isSignal: false, isRequired: false, transformFunction: null }, connection: { classPropertyName: "connection", publicName: "connection", isSignal: false, isRequired: false, transformFunction: (settings) => new ConnectionModel(settings) }, snapGrid: { classPropertyName: "snapGrid", publicName: "snapGrid", isSignal: false, isRequired: false, transformFunction: null }, nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: false, isRequired: true, transformFunction: null }, edges: { classPropertyName: "edges", publicName: "edges", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { onComponentNodeEvent: "onComponentNodeEvent" }, providers: [
|
|
3266
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: VflowComponent, isStandalone: true, selector: "vflow", inputs: { view: { classPropertyName: "view", publicName: "view", isSignal: false, isRequired: false, transformFunction: null }, minZoom: { classPropertyName: "minZoom", publicName: "minZoom", isSignal: false, isRequired: false, transformFunction: null }, maxZoom: { classPropertyName: "maxZoom", publicName: "maxZoom", isSignal: false, isRequired: false, transformFunction: null }, background: { classPropertyName: "background", publicName: "background", isSignal: false, isRequired: false, transformFunction: null }, optimization: { classPropertyName: "optimization", publicName: "optimization", isSignal: true, isRequired: false, transformFunction: null }, entitiesSelectable: { classPropertyName: "entitiesSelectable", publicName: "entitiesSelectable", isSignal: false, isRequired: false, transformFunction: null }, keyboardShortcuts: { classPropertyName: "keyboardShortcuts", publicName: "keyboardShortcuts", isSignal: false, isRequired: false, transformFunction: null }, connection: { classPropertyName: "connection", publicName: "connection", isSignal: false, isRequired: false, transformFunction: (settings) => new ConnectionModel(settings) }, snapGrid: { classPropertyName: "snapGrid", publicName: "snapGrid", isSignal: false, isRequired: false, transformFunction: null }, elevateNodesOnSelect: { classPropertyName: "elevateNodesOnSelect", publicName: "elevateNodesOnSelect", isSignal: false, isRequired: false, transformFunction: null }, nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: false, isRequired: true, transformFunction: null }, edges: { classPropertyName: "edges", publicName: "edges", isSignal: false, isRequired: false, transformFunction: null } }, outputs: { onComponentNodeEvent: "onComponentNodeEvent" }, providers: [
|
|
3277
3267
|
DraggableService,
|
|
3278
3268
|
ViewportService,
|
|
3279
3269
|
FlowStatusService,
|
|
@@ -3336,6 +3326,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImpo
|
|
|
3336
3326
|
}]
|
|
3337
3327
|
}], snapGrid: [{
|
|
3338
3328
|
type: Input
|
|
3329
|
+
}], elevateNodesOnSelect: [{
|
|
3330
|
+
type: Input
|
|
3339
3331
|
}], nodes: [{
|
|
3340
3332
|
type: Input,
|
|
3341
3333
|
args: [{ required: true }]
|