@nascentdigital/funnel-editor 4.4.21 → 4.5.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.
Files changed (38) hide show
  1. package/dist/core/components/interaction/Animate.d.ts +2 -2
  2. package/dist/core/components/interaction/Animate.js +12 -10
  3. package/dist/core/components/interaction/QuickPicker.d.ts +27 -0
  4. package/dist/core/components/interaction/QuickPicker.js +108 -0
  5. package/dist/core/components/interaction/Tabs.d.ts +16 -0
  6. package/dist/core/components/interaction/Tabs.js +24 -0
  7. package/dist/core/components/interaction/index.d.ts +2 -0
  8. package/dist/core/components/interaction/index.js +2 -0
  9. package/dist/core/components/visuals/Icon.d.ts +1 -1
  10. package/dist/core/components/visuals/Icon.js +3 -0
  11. package/dist/core/components/visuals/SkeletonLayout.js +226 -33
  12. package/dist/core/domain/BlockConfiguration.d.ts +9 -1
  13. package/dist/core/domain/BlockConfiguration.js +50 -20
  14. package/dist/core/domain/Section.js +1 -1
  15. package/dist/page/components/BlockEditor.js +3 -3
  16. package/dist/page/components/{BlockConfig.d.ts → config/BlockConfig.d.ts} +1 -1
  17. package/dist/page/components/config/BlockConfig.js +43 -0
  18. package/dist/page/components/config/BlockConfigEditor.d.ts +13 -0
  19. package/dist/page/components/config/BlockConfigEditor.js +110 -0
  20. package/dist/page/components/config/BlockPreview.d.ts +10 -0
  21. package/dist/page/components/config/BlockPreview.js +456 -0
  22. package/dist/page/components/config/BlockView.d.ts +4 -0
  23. package/dist/page/components/config/BlockView.js +9 -0
  24. package/dist/page/components/config/LegacyBlockConfigEditor.d.ts +13 -0
  25. package/dist/page/components/{BlockConfig.js → config/LegacyBlockConfigEditor.js} +4 -29
  26. package/dist/page/components/config/ListLayoutPicker.d.ts +6 -0
  27. package/dist/page/components/config/ListLayoutPicker.js +13 -0
  28. package/dist/page/components/config/ListPanelEditor.d.ts +6 -0
  29. package/dist/page/components/config/ListPanelEditor.js +13 -0
  30. package/dist/page/components/config/MessagePanelEditor.d.ts +6 -0
  31. package/dist/page/components/config/MessagePanelEditor.js +13 -0
  32. package/dist/page/components/config/VisualPanelEditor.d.ts +6 -0
  33. package/dist/page/components/config/VisualPanelEditor.js +13 -0
  34. package/dist/page/components/config/index.d.ts +1 -0
  35. package/dist/page/components/config/index.js +17 -0
  36. package/dist/page/hooks/PageController.js +1 -1
  37. package/dist/styles/index.css +108 -3
  38. package/package.json +2 -2
@@ -1,4 +1,5 @@
1
- import { BlockLayout, EventBus, FieldModel } from '@nascentdigital/funnel-core';
1
+ import { BlockLayout, EventBus, Field, FieldModel, Schema } from '@nascentdigital/funnel-core';
2
+ import { PanelLayout } from '@nascentdigital/funnel-ui';
2
3
  import { Block } from './Block';
3
4
  import { IAssetStore } from './IAssetStore';
4
5
  import { SectionBlock } from './SectionBlock';
@@ -15,6 +16,13 @@ export declare class BlockConfiguration {
15
16
  constructor({ block: blockOrSectionBlock, assetStore }: BlockConfiguration.ConstructorArgs);
16
17
  get editableFields(): ReadonlyArray<FieldModel.Config>;
17
18
  set editableFields(fields: ReadonlyArray<FieldModel.Config>);
19
+ get blocksFields(): FieldModel.Config[];
20
+ get blockFieldData(): Schema.Block.Data.ForFields<Schema.Fields>;
21
+ get desktopFields(): FieldModel.Config[];
22
+ get mobileFields(): never[];
23
+ get nameField(): FieldModel.Config | undefined;
24
+ get themeField(): FieldModel.Config | undefined;
25
+ get layoutField(): Field<Schema.EnumField<Schema.EnumField.Options<PanelLayout>>> | undefined;
18
26
  get layout(): BlockLayout | undefined;
19
27
  get events$(): import("rxjs").Observable<EventBus.Event<string, string>>;
20
28
  get valid(): boolean;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BlockConfiguration = void 0;
4
4
  const funnel_core_1 = require("@nascentdigital/funnel-core");
5
+ const funnel_ui_1 = require("@nascentdigital/funnel-ui");
5
6
  const rxjs_1 = require("rxjs");
6
7
  const SectionBlock_1 = require("./SectionBlock");
7
8
  // model
@@ -46,6 +47,53 @@ class BlockConfiguration {
46
47
  this._editableFields = fields;
47
48
  this.raiseEvent('changed');
48
49
  }
50
+ get blocksFields() {
51
+ return [
52
+ this.nameField,
53
+ this.themeField
54
+ ].filter(funnel_core_1.Value.isDefined);
55
+ }
56
+ get blockFieldData() {
57
+ const { assetStore, block } = this;
58
+ return block.fields.reduce((data, field) => {
59
+ // link assets
60
+ const fieldValue = field.value;
61
+ if ((fieldValue === null || fieldValue === void 0 ? void 0 : fieldValue.type) === 'asset') {
62
+ const assetData = funnel_core_1.Schema.AssetField.Value.fromData(fieldValue);
63
+ const asset = (assetData === null || assetData === void 0 ? void 0 : assetData.id)
64
+ ? assetStore.getAsset(assetData === null || assetData === void 0 ? void 0 : assetData.id)
65
+ : undefined;
66
+ data[field.id] = assetData && asset
67
+ ? funnel_core_1.Schema.AssetField.LinkedValue.toData(funnel_core_1.Schema.AssetField.Value.toLinked(assetData, asset))
68
+ : fieldValue;
69
+ }
70
+ else {
71
+ data[field.id] = field.value;
72
+ }
73
+ // keep reducing
74
+ return data;
75
+ }, {});
76
+ }
77
+ get desktopFields() {
78
+ return [
79
+ this._editableFields.find(field => field.id === 'contentAlign')
80
+ ].filter(funnel_core_1.Value.isDefined);
81
+ }
82
+ get mobileFields() {
83
+ return [].filter(funnel_core_1.Value.isDefined);
84
+ }
85
+ get nameField() {
86
+ const { block, sectionBlock } = this;
87
+ return sectionBlock
88
+ ? this._editableFields.find(field => field.id === block.nameField.id)
89
+ : undefined;
90
+ }
91
+ get themeField() {
92
+ return this._editableFields.find(field => field.id === funnel_ui_1.BlockConfig.Presets.ThemeOnly.key);
93
+ }
94
+ get layoutField() {
95
+ return this._editableFields.find(field => field.id === 'layout');
96
+ }
49
97
  get layout() {
50
98
  return this._layout;
51
99
  }
@@ -149,30 +197,12 @@ class BlockConfiguration {
149
197
  }
150
198
  sync() {
151
199
  // recompute the editable fields
152
- const { assetStore, block, fields, editableFields } = this;
200
+ const { block, blockFieldData: fieldData, fields, editableFields } = this;
153
201
  const configData = editableFields.reduce((data, field) => {
154
202
  data[field.id] = field.value;
155
203
  // keep reducing
156
204
  return data;
157
205
  }, {});
158
- const fieldData = block.fields.reduce((data, field) => {
159
- // link assets
160
- const fieldValue = field.value;
161
- if ((fieldValue === null || fieldValue === void 0 ? void 0 : fieldValue.type) === 'asset') {
162
- const assetData = funnel_core_1.Schema.AssetField.Value.fromData(fieldValue);
163
- const asset = (assetData === null || assetData === void 0 ? void 0 : assetData.id)
164
- ? assetStore.getAsset(assetData === null || assetData === void 0 ? void 0 : assetData.id)
165
- : undefined;
166
- data[field.id] = assetData && asset
167
- ? funnel_core_1.Schema.AssetField.LinkedValue.toData(funnel_core_1.Schema.AssetField.Value.toLinked(assetData, asset))
168
- : fieldValue;
169
- }
170
- else {
171
- data[field.id] = field.value;
172
- }
173
- // keep reducing
174
- return data;
175
- }, {});
176
206
  // get customized schema (if there are effects)
177
207
  const blockSchema = block.schema;
178
208
  const { getConfig, getLayout } = blockSchema;
@@ -204,7 +234,7 @@ class BlockConfiguration {
204
234
  });
205
235
  // resolve layouts
206
236
  this._layout = getLayout
207
- ? getLayout({ config: configData, fields: fieldData })
237
+ ? getLayout({ config: updatedConfig, fields: fieldData })
208
238
  : undefined;
209
239
  // assign to editable fields (raised event)
210
240
  this.editableFields = configFields;
@@ -88,7 +88,7 @@ class Section extends ItemBase_1.ItemBase {
88
88
  this._blocks.push(block);
89
89
  this._modifiedOn = Date.now();
90
90
  // bind block
91
- block.bind(this);
91
+ block.bind({ parent: this, area: this.area });
92
92
  // raise change
93
93
  this.raiseEvent('changed');
94
94
  // return block
@@ -7,7 +7,7 @@ exports.BlockEditor = BlockEditor;
7
7
  const jsx_runtime_1 = require("react/jsx-runtime");
8
8
  const classnames_1 = __importDefault(require("classnames"));
9
9
  const core_1 = require("../../core");
10
- const BlockConfig_1 = require("./BlockConfig");
10
+ const config_1 = require("./config");
11
11
  // component
12
12
  function BlockEditor({ className, block, onSelectItem }) {
13
13
  // bind props + hooks
@@ -39,7 +39,7 @@ function BlockEditor({ className, block, onSelectItem }) {
39
39
  // delete the block
40
40
  block.delete();
41
41
  // and select the nearest neighbour (if any)
42
- if (block.parent instanceof core_1.Section && nearestBlock) {
42
+ if (nearestBlock) {
43
43
  onSelectItem(nearestBlock);
44
44
  }
45
45
  };
@@ -54,7 +54,7 @@ function BlockEditor({ className, block, onSelectItem }) {
54
54
  }
55
55
  };
56
56
  // render editor
57
- return ((0, jsx_runtime_1.jsx)(core_1.FieldsPanel, { className: (0, classnames_1.default)(className, 'px-l6 py-l10'), title: block.name, actions: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(BlockConfig_1.BlockConfig, { className: "shrink-0", block: block }), block.canDelete &&
57
+ return ((0, jsx_runtime_1.jsx)(core_1.FieldsPanel, { className: (0, classnames_1.default)(className, 'px-l6 py-l10'), title: block.name, actions: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(config_1.BlockConfig, { className: "shrink-0", block: block }), block.canDelete &&
58
58
  (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(core_1.IconButton, { className: "DeleteAction text-content-primary hover:text-destructive shrink-0", variant: "util", icon: "EllipsisVertical", onClick: e => menu.open(e.currentTarget) }), (0, jsx_runtime_1.jsx)(core_1.Menu, { controller: menu, onItemClick: onMenuClick })] })] }), fields: block.fields }));
59
59
  }
60
60
  const blockMenu = [
@@ -1,4 +1,4 @@
1
- import { Block, SectionBlock } from '../../core';
1
+ import { Block, SectionBlock } from '../../../core';
2
2
  export declare function BlockConfig({ className, block: blockOrSectionBlock }: BlockConfig.Props): import("react/jsx-runtime").JSX.Element | undefined;
3
3
  export declare namespace BlockConfig {
4
4
  type Props = {
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BlockConfig = BlockConfig;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const core_1 = require("../../../core");
7
+ const BlockConfigEditor_1 = require("./BlockConfigEditor");
8
+ const LegacyBlockConfigEditor_1 = require("./LegacyBlockConfigEditor");
9
+ // component
10
+ function BlockConfig({ className, block: blockOrSectionBlock }) {
11
+ // bind ref
12
+ const editorRef = (0, react_1.useRef)(null);
13
+ const legacyEditorRef = (0, react_1.useRef)(null);
14
+ // skip if there's no config
15
+ const block = blockOrSectionBlock instanceof core_1.SectionBlock
16
+ ? blockOrSectionBlock.block
17
+ : blockOrSectionBlock;
18
+ const { config } = block;
19
+ if (config.length === 0) {
20
+ return undefined;
21
+ }
22
+ // helpers
23
+ const onEdit = async (event) => {
24
+ var _a, _b;
25
+ // use new configurator (if enabled)
26
+ if (process.env.NEXT_PUBLIC_EDITOR_BLOCK_CONFIG === 'experimental') {
27
+ await ((_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.edit({
28
+ block: blockOrSectionBlock,
29
+ anchor: event.currentTarget
30
+ }));
31
+ }
32
+ // or use legacy version
33
+ else {
34
+ await ((_b = legacyEditorRef.current) === null || _b === void 0 ? void 0 : _b.edit({
35
+ block: blockOrSectionBlock,
36
+ anchor: event.currentTarget
37
+ }));
38
+ }
39
+ };
40
+ // render
41
+ const valid = config.every(field => field.valid);
42
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(core_1.StatusChip, { className: className, variant: valid ? 'success' : 'error', onClick: onEdit, children: "Configure" }), (0, jsx_runtime_1.jsx)(BlockConfigEditor_1.BlockConfigEditor, { ref: editorRef }), (0, jsx_runtime_1.jsx)(LegacyBlockConfigEditor_1.LegacyBlockConfigEditor, { ref: legacyEditorRef })] }));
43
+ }
@@ -0,0 +1,13 @@
1
+ import { Rect } from '@nascentdigital/funnel-core';
2
+ import { Block, SectionBlock } from '../../../core';
3
+ export declare const BlockConfigEditor: import("react").ForwardRefExoticComponent<import("react").RefAttributes<BlockConfigEditor.Ref>>;
4
+ export declare namespace BlockConfigEditor {
5
+ type Props = {};
6
+ interface Ref {
7
+ edit(options: Options): Promise<void>;
8
+ }
9
+ type Options = {
10
+ block: Block | SectionBlock;
11
+ anchor: HTMLElement | Rect;
12
+ };
13
+ }
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BlockConfigEditor = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const funnel_core_1 = require("@nascentdigital/funnel-core");
6
+ const client_1 = require("@nascentdigital/funnel-ui/client");
7
+ const react_1 = require("motion/react");
8
+ const react_2 = require("react");
9
+ const rxjs_1 = require("rxjs");
10
+ const core_1 = require("../../../core");
11
+ const hooks_1 = require("../../hooks");
12
+ const BlockPreview_1 = require("./BlockPreview");
13
+ // editor
14
+ exports.BlockConfigEditor = (0, react_2.forwardRef)(function BlockConfigEditor(_, ref) {
15
+ // bind state + hooks
16
+ const [deferral, setDeferral] = (0, react_2.useState)(undefined);
17
+ const [configuration, setConfiguration] = (0, react_2.useState)();
18
+ const [anchor, setAnchor] = (0, react_2.useState)();
19
+ const [blockView, setBlockView] = (0, react_2.useState)('desktop');
20
+ const pageController = (0, hooks_1.usePageController)();
21
+ const touch = (0, client_1.useTouch)();
22
+ const panelLayoutPickerRef = (0, react_2.useRef)(null);
23
+ const { modified = false, valid = false, blocksFields = [], desktopFields = [], mobileFields = [] } = configuration !== null && configuration !== void 0 ? configuration : {};
24
+ const settingsFields = blockView === 'desktop' ? desktopFields : mobileFields;
25
+ // sync w/ configuration
26
+ (0, core_1.useBlockConfiguration)(configuration);
27
+ // bind ref
28
+ (0, react_2.useImperativeHandle)(ref, () => ({
29
+ edit: async ({ block, anchor }) => {
30
+ // reset state
31
+ setConfiguration(block.getConfiguration());
32
+ setAnchor(anchor);
33
+ setBlockView('desktop');
34
+ // create deferral
35
+ const deferral = new funnel_core_1.Deferral();
36
+ setDeferral(deferral);
37
+ // wait until deferral is resolved
38
+ return deferral.promise;
39
+ }
40
+ }), []);
41
+ // helpers
42
+ const onSave = async () => {
43
+ // save config
44
+ configuration === null || configuration === void 0 ? void 0 : configuration.save();
45
+ // wait for page to save
46
+ await (0, rxjs_1.firstValueFrom)(pageController.saved$);
47
+ // complete (we're done)
48
+ deferral === null || deferral === void 0 ? void 0 : deferral.resolve();
49
+ // refresh
50
+ touch();
51
+ };
52
+ const onCancel = () => {
53
+ // skip if missing or already completed
54
+ if (!deferral || deferral.isComplete) {
55
+ return;
56
+ }
57
+ // cancel
58
+ deferral.resolve(undefined);
59
+ // touch
60
+ touch();
61
+ };
62
+ const onChangePanelLayout = async (e) => {
63
+ var _a;
64
+ // skip if there's no layout field (shouldn't happen)
65
+ const { layoutField } = configuration !== null && configuration !== void 0 ? configuration : {};
66
+ if (!layoutField) {
67
+ return;
68
+ }
69
+ // pick new layout
70
+ const { schema: layoutSchema, value: layoutData } = layoutField;
71
+ const selectedLayout = await ((_a = panelLayoutPickerRef.current) === null || _a === void 0 ? void 0 : _a.open({
72
+ anchor: e.currentTarget,
73
+ anchorOffset: 4,
74
+ placement: 'top',
75
+ items: Object.entries(layoutSchema.options).map(([layout, option]) => ({
76
+ id: layout,
77
+ label: option.name
78
+ })),
79
+ selectedItem: layoutData === null || layoutData === void 0 ? void 0 : layoutData.value
80
+ }));
81
+ // update if something was picked
82
+ if (selectedLayout) {
83
+ layoutField.value = funnel_core_1.Schema.EnumField.Value.toData(selectedLayout);
84
+ }
85
+ };
86
+ // skip if there's no config
87
+ if (!configuration) {
88
+ return null;
89
+ }
90
+ // render
91
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(core_1.MiniEditor, { className: "w-[550px] max-w-full", title: "Block Configuration", open: !!deferral && !deferral.isComplete, anchor: anchor, anchorOffset: 4, onSave: onSave, onClose: onCancel, disableSave: !modified || !valid, customActions: (0, jsx_runtime_1.jsx)(core_1.Button, { variant: "secondary", size: "sm", disabled: configuration.isConfigInDefault, onClick: () => configuration.reset(), children: "Reset to Default" }), wide: true, children: [(0, jsx_runtime_1.jsxs)("section", { className: "flex items-center gap-l4", children: [(0, jsx_runtime_1.jsx)("span", { className: "label-sm text-content-primary flex-1", children: "Layout" }), (0, jsx_runtime_1.jsx)(core_1.Button, { variant: "secondary", size: "sm", iconLeft: "ArrowLeftRight", children: "Rearrange" }), (0, jsx_runtime_1.jsx)(core_1.Button, { variant: "secondary", size: "sm", iconLeft: "LayoutTemplate", onClick: onChangePanelLayout, children: "Change" })] }), blocksFields.length > 0 &&
92
+ (0, jsx_runtime_1.jsx)("section", { className: "grid grid-cols-2 auto-rows-auto items-start gap-l6", children: (0, jsx_runtime_1.jsx)(core_1.FieldErrorController, { children: blocksFields.map(field => (0, jsx_runtime_1.jsx)(core_1.FieldEditor, { className: "col-span-2", field: field, variant: "wide" }, field.id)) }) }), (0, jsx_runtime_1.jsx)(core_1.Tabs, { items: blockViews, selectedItem: blockView, onSelect: setBlockView }), (0, jsx_runtime_1.jsx)("section", { className: "stacked p-l6 bg-surface1 rounded-md", children: (0, jsx_runtime_1.jsx)(react_1.AnimatePresence, { children: (0, jsx_runtime_1.jsx)(core_1.Animate, { effect: "fade", present: true, children: (0, jsx_runtime_1.jsx)(BlockPreview_1.BlockPreview, { variant: blockView, configuration: configuration }) }, blockView) }) }), settingsFields.length > 0 &&
93
+ (0, jsx_runtime_1.jsx)("section", { className: "grid grid-cols-2 auto-rows-auto items-start gap-l6", children: (0, jsx_runtime_1.jsx)(core_1.FieldErrorController, { children: settingsFields.map(field => (0, jsx_runtime_1.jsx)(core_1.FieldEditor, { className: "col-span-2", field: field, variant: "wide" }, field.id)) }) }), (0, jsx_runtime_1.jsx)("hr", { className: "divider border-border-l1" })] }), (0, jsx_runtime_1.jsx)(core_1.QuickPicker, { ref: panelLayoutPickerRef, variant: "card", title: "Pick Block Layout", description: "Select an available layout for this block.", renderItem: (item) => renderPanelLayoutItem(item) })] }));
94
+ });
95
+ function renderPanelLayoutItem(item) {
96
+ switch (item.id) {
97
+ case 'stack.h':
98
+ return ((0, jsx_runtime_1.jsx)("div", { children: "Stack H" }));
99
+ case 'stack.v':
100
+ return ((0, jsx_runtime_1.jsx)("div", { children: "Stack V" }));
101
+ case 'nested':
102
+ return ((0, jsx_runtime_1.jsx)("div", { children: "Nested" }));
103
+ default:
104
+ return ((0, jsx_runtime_1.jsx)("div", { children: item.id }));
105
+ }
106
+ }
107
+ const blockViews = [
108
+ { id: 'desktop', label: 'Desktop' },
109
+ { id: 'mobile', label: 'Mobile' },
110
+ ];
@@ -0,0 +1,10 @@
1
+ import { BlockConfiguration } from '../../../core';
2
+ export declare function BlockPreview({ className, variant, configuration }: BlockPreview.Props): import("react/jsx-runtime").JSX.Element;
3
+ export declare namespace BlockPreview {
4
+ type Variant = 'desktop' | 'mobile';
5
+ type Props = {
6
+ className?: string;
7
+ variant: Variant;
8
+ configuration: BlockConfiguration;
9
+ };
10
+ }