@webiny/website-builder-vue 6.4.0-beta.4

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 (45) hide show
  1. package/index.js +4 -0
  2. package/package.json +46 -0
  3. package/src/components/ConnectToEditor.ts +46 -0
  4. package/src/components/DocumentFragment.ts +43 -0
  5. package/src/components/DocumentRenderer.ts +102 -0
  6. package/src/components/DocumentStoreProvider.ts +53 -0
  7. package/src/components/EditingElementRenderer/EditingElementRenderer.presenter.ts +59 -0
  8. package/src/components/EditingElementRenderer/EditingElementRenderer.ts +70 -0
  9. package/src/components/EditingElementRenderer/index.ts +2 -0
  10. package/src/components/ElementIndexProvider.ts +24 -0
  11. package/src/components/ElementRenderer.ts +42 -0
  12. package/src/components/ElementSlot.ts +34 -0
  13. package/src/components/ElementSlotDepthProvider.ts +24 -0
  14. package/src/components/FragmentsProvider.ts +87 -0
  15. package/src/components/LiveElementRenderer.ts +102 -0
  16. package/src/components/LiveElementSlot.ts +46 -0
  17. package/src/components/PreviewElementSlot.ts +43 -0
  18. package/src/components/index.ts +17 -0
  19. package/src/composables/useBindingsForElement.ts +40 -0
  20. package/src/composables/useDocumentState.ts +13 -0
  21. package/src/composables/useObservable.ts +30 -0
  22. package/src/composables/useSelectFromState.ts +18 -0
  23. package/src/composables/useViewport.ts +27 -0
  24. package/src/createComponent.ts +55 -0
  25. package/src/editorComponents/Box.manifest.ts +19 -0
  26. package/src/editorComponents/Box.ts +8 -0
  27. package/src/editorComponents/Fragment.manifest.ts +19 -0
  28. package/src/editorComponents/Fragment.ts +57 -0
  29. package/src/editorComponents/Grid.manifest.ts +166 -0
  30. package/src/editorComponents/Grid.ts +72 -0
  31. package/src/editorComponents/GridColumn.manifest.ts +23 -0
  32. package/src/editorComponents/GridColumn.ts +6 -0
  33. package/src/editorComponents/Image.manifest.ts +36 -0
  34. package/src/editorComponents/Image.ts +144 -0
  35. package/src/editorComponents/Lexical.manifest.ts +24 -0
  36. package/src/editorComponents/Lexical.ts +23 -0
  37. package/src/editorComponents/Root.manifest.ts +10 -0
  38. package/src/editorComponents/Root.ts +8 -0
  39. package/src/editorComponents/index.ts +12 -0
  40. package/src/index.ts +52 -0
  41. package/src/lexical.css +379 -0
  42. package/src/types.ts +46 -0
  43. package/tsconfig.build.json +16 -0
  44. package/tsconfig.json +16 -0
  45. package/webiny.config.js +8 -0
@@ -0,0 +1,102 @@
1
+ import { defineComponent, ref, computed, watch, onMounted, h, Fragment, type PropType } from "vue";
2
+ import { contentSdk, type DocumentElement, type ComponentInput } from "@webiny/website-builder-sdk";
3
+ import { ElementSlot } from "./ElementSlot.js";
4
+ import { useViewport } from "~/composables/useViewport.js";
5
+ import { useBindingsForElement } from "~/composables/useBindingsForElement.js";
6
+ import { useDocumentState } from "~/composables/useDocumentState.js";
7
+
8
+ /**
9
+ * Resolves a single document element into its rendered Vue subtree.
10
+ *
11
+ * Steps:
12
+ * 1. Determines the current breakpoint (starts at "desktop" for SSR safety).
13
+ * 2. Fetches merged bindings for the element at that breakpoint.
14
+ * 3. Calls contentSdk.resolveElement() to get the list of component instances.
15
+ * 4. Renders each instance, optionally wrapping it in a style div.
16
+ */
17
+ export const LiveElementRenderer = defineComponent({
18
+ name: "WebinyLiveElementRenderer",
19
+
20
+ props: {
21
+ element: { type: Object as PropType<DocumentElement>, required: true }
22
+ },
23
+
24
+ setup(props) {
25
+ const viewport = useViewport();
26
+
27
+ // Start with "desktop" on both server and initial client render (SSR-safe).
28
+ const breakpoint = ref<string>("desktop");
29
+
30
+ onMounted(() => {
31
+ if (viewport.value?.breakpoint) {
32
+ breakpoint.value = viewport.value.breakpoint;
33
+ }
34
+ });
35
+
36
+ watch(
37
+ () => viewport.value?.breakpoint,
38
+ newBp => {
39
+ if (newBp && newBp !== breakpoint.value) {
40
+ breakpoint.value = newBp;
41
+ }
42
+ }
43
+ );
44
+
45
+ const elementId = computed(() => props.element?.id ?? "");
46
+ const elementBindings = useBindingsForElement(elementId.value, breakpoint);
47
+ const state = useDocumentState();
48
+
49
+ return () => {
50
+ const { element } = props;
51
+ if (!element?.component) {
52
+ return null;
53
+ }
54
+
55
+ const onResolved = (value: unknown, input: ComponentInput) => {
56
+ if (input.type === "slot") {
57
+ const elements = (input as { list?: boolean }).list
58
+ ? (value as string[])
59
+ : [value as string];
60
+ return h(ElementSlot, {
61
+ parentId: element.id,
62
+ slot: input.name,
63
+ elements
64
+ });
65
+ }
66
+ return value;
67
+ };
68
+
69
+ const instances = contentSdk.resolveElement({
70
+ element,
71
+ state: state.value,
72
+ elementBindings: elementBindings.value,
73
+ onResolved
74
+ });
75
+
76
+ if (!instances) {
77
+ return null;
78
+ }
79
+
80
+ const vnodes = instances.map((resolved, index) => {
81
+ const { component: Component, inputs, styles, manifest } = resolved;
82
+ const autoApplyStyles = manifest.autoApplyStyles !== false;
83
+
84
+ const userVNode = h(Component, {
85
+ key: `${element.id}-${index}`,
86
+ inputs,
87
+ styles,
88
+ element,
89
+ breakpoint: breakpoint.value
90
+ });
91
+
92
+ if (!autoApplyStyles) {
93
+ return userVNode;
94
+ }
95
+
96
+ return h("div", { key: `wrapper-${index}`, style: styles }, [userVNode]);
97
+ });
98
+
99
+ return h(Fragment, null, vnodes);
100
+ };
101
+ }
102
+ });
@@ -0,0 +1,46 @@
1
+ import { defineComponent, h, type PropType } from "vue";
2
+ import { ElementSlotDepthProvider, useElementSlotDepth } from "./ElementSlotDepthProvider.js";
3
+ import { ElementIndexProvider } from "./ElementIndexProvider.js";
4
+ import { setLiveElementSlot } from "./PreviewElementSlot.js";
5
+
6
+ // Forward-declared to resolve the circular ElementRenderer ↔ LiveElementSlot cycle.
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ let ElementRenderer: any;
9
+ export const setElementRenderer = (c: unknown) => {
10
+ ElementRenderer = c;
11
+ };
12
+
13
+ /**
14
+ * Renders a flat list of element IDs as sibling ElementRenderer instances,
15
+ * each wrapped in an index-provider and nested inside an incremented depth.
16
+ */
17
+ export const LiveElementSlot = defineComponent({
18
+ name: "WebinyLiveElementSlot",
19
+
20
+ props: {
21
+ elements: { type: Array as PropType<string[]>, default: () => [] }
22
+ },
23
+
24
+ setup(props) {
25
+ const depth = useElementSlotDepth();
26
+
27
+ return () =>
28
+ h(
29
+ ElementSlotDepthProvider,
30
+ { depth: depth + 1 },
31
+ {
32
+ default: () =>
33
+ props.elements.map((id, index) =>
34
+ h(
35
+ ElementIndexProvider,
36
+ { key: id, index },
37
+ { default: () => h(ElementRenderer, { id }) }
38
+ )
39
+ )
40
+ }
41
+ );
42
+ }
43
+ });
44
+
45
+ // Break the circular dependency: PreviewElementSlot needs LiveElementSlot.
46
+ setLiveElementSlot(LiveElementSlot);
@@ -0,0 +1,43 @@
1
+ import { defineComponent, h, type PropType } from "vue";
2
+ import { useElementSlotDepth } from "./ElementSlotDepthProvider.js";
3
+
4
+ // Forward-declare to avoid circular dep at module init time.
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ let LiveElementSlot: any;
7
+ export const setLiveElementSlot = (c: unknown) => {
8
+ LiveElementSlot = c;
9
+ };
10
+
11
+ /**
12
+ * In editing mode, renders an empty placeholder div when the slot has no
13
+ * children (so the editor can show a drop zone). Otherwise delegates to
14
+ * LiveElementSlot.
15
+ */
16
+ export const PreviewElementSlot = defineComponent({
17
+ name: "WebinyPreviewElementSlot",
18
+
19
+ props: {
20
+ parentId: { type: String, required: true },
21
+ slot: { type: String, required: true },
22
+ elements: { type: Array as PropType<string[]>, default: () => [] }
23
+ },
24
+
25
+ setup(props) {
26
+ const depth = useElementSlotDepth();
27
+
28
+ return () => {
29
+ if (!props.elements.length) {
30
+ return h("div", {
31
+ style: { height: "100px", width: "100% !important" },
32
+ "data-role": "element-slot",
33
+ "data-parent-id": props.parentId,
34
+ "data-parent-slot": props.slot,
35
+ "data-depth": depth,
36
+ "data-empty": true
37
+ });
38
+ }
39
+
40
+ return h(LiveElementSlot, { elements: props.elements });
41
+ };
42
+ }
43
+ });
@@ -0,0 +1,17 @@
1
+ export { DocumentRenderer } from "./DocumentRenderer.js";
2
+ export { DocumentFragment, type DocumentFragmentProps } from "./DocumentFragment.js";
3
+ export { ElementSlot } from "./ElementSlot.js";
4
+ export {
5
+ DocumentStoreProvider,
6
+ useDocumentStore,
7
+ DOCUMENT_STORE_KEY
8
+ } from "./DocumentStoreProvider.js";
9
+ export {
10
+ FragmentsProvider,
11
+ useDocumentFragments,
12
+ type DocumentFragmentConfig,
13
+ type DocumentFragments
14
+ } from "./FragmentsProvider.js";
15
+ export { ElementSlotDepthProvider, useElementSlotDepth } from "./ElementSlotDepthProvider.js";
16
+ export { ElementIndexProvider, useElementIndex } from "./ElementIndexProvider.js";
17
+ export { EditingElementRenderer } from "./EditingElementRenderer/index.js";
@@ -0,0 +1,40 @@
1
+ import { computed, type Ref } from "vue";
2
+ import { toJS } from "mobx";
3
+ import { BindingsProcessor } from "@webiny/website-builder-sdk";
4
+ import { useViewport } from "./useViewport.js";
5
+ import { useObservable } from "./useObservable.js";
6
+ import { useDocumentStore } from "~/components/DocumentStoreProvider.js";
7
+
8
+ /**
9
+ * Returns a Vue computed ref containing the merged bindings (inputs + styles)
10
+ * for the given element ID at the current breakpoint.
11
+ *
12
+ * Re-evaluates automatically when:
13
+ * - the MobX document store changes (via useObservable)
14
+ * - the breakpoint ref changes
15
+ * - the viewport breakpoints change
16
+ */
17
+ export const useBindingsForElement = (elementId: string, breakpoint: Ref<string>) => {
18
+ const documentStore = useDocumentStore();
19
+ const viewport = useViewport();
20
+
21
+ // Observe this element's bindings directly inside the MobX autorun so that
22
+ // deep mutations from applyPatch (e.g. adding a child element) trigger a
23
+ // re-render without needing the top-level document reference to change.
24
+ // toJS() inside the autorun causes MobX to track all sub-properties.
25
+ const elementBindings = useObservable(() => {
26
+ const doc = documentStore.getDocument();
27
+ if (!doc) {
28
+ return undefined;
29
+ }
30
+ return toJS(doc.bindings[elementId]);
31
+ });
32
+
33
+ return computed(() => {
34
+ const bindings = elementBindings.value ?? {};
35
+ const breakpoints =
36
+ viewport.value?.breakpoints?.map((bp: { name: string }) => bp.name) ?? [];
37
+ const processor = new BindingsProcessor(breakpoints);
38
+ return processor.getBindings(bindings, breakpoint.value);
39
+ });
40
+ };
@@ -0,0 +1,13 @@
1
+ import { computed } from "vue";
2
+ import { useObservable } from "./useObservable.js";
3
+ import { useDocumentStore } from "~/components/DocumentStoreProvider.js";
4
+
5
+ /**
6
+ * Returns a Vue computed ref containing the document's `state` object.
7
+ * Re-evaluates whenever the MobX document store changes.
8
+ */
9
+ export const useDocumentState = () => {
10
+ const documentStore = useDocumentStore();
11
+ const document = useObservable(() => documentStore.getDocument());
12
+ return computed(() => document.value?.state ?? {});
13
+ };
@@ -0,0 +1,30 @@
1
+ import { shallowRef, onUnmounted, triggerRef, getCurrentInstance, type Ref } from "vue";
2
+ import { autorun } from "mobx";
3
+
4
+ /**
5
+ * Bridges a MobX observable into a Vue shallow ref.
6
+ *
7
+ * The supplied `fn` is run inside a MobX `autorun`. Every time a MobX
8
+ * observable accessed by `fn` changes, the ref is updated and Vue is told
9
+ * to treat it as "dirty" so that all computed values and templates that
10
+ * depend on it re-evaluate.
11
+ *
12
+ * Must be called inside a component setup() (or equivalent) so that
13
+ * `onUnmounted` can clean up the MobX reaction automatically.
14
+ */
15
+ export function useObservable<T>(fn: () => T): Ref<T> {
16
+ const value = shallowRef<T>(fn());
17
+
18
+ const disposer = autorun(() => {
19
+ value.value = fn();
20
+ // shallowRef won't trigger on the same object reference; triggerRef
21
+ // forces Vue to re-evaluate all dependents regardless.
22
+ triggerRef(value);
23
+ });
24
+
25
+ if (getCurrentInstance()) {
26
+ onUnmounted(() => disposer());
27
+ }
28
+
29
+ return value as Ref<T>;
30
+ }
@@ -0,0 +1,18 @@
1
+ import { computed, type Ref } from "vue";
2
+ import { useObservable } from "./useObservable.js";
3
+
4
+ /**
5
+ * Selects a slice of MobX state and exposes it as a Vue computed ref.
6
+ *
7
+ * - `getState` is tracked by MobX: any observable accessed inside it causes
8
+ * re-evaluation when it changes.
9
+ * - `selector` is then applied to the latest state value in a Vue computed,
10
+ * so it also re-runs when any Vue reactive value accessed inside it changes.
11
+ */
12
+ export function useSelectFromState<TState, T>(
13
+ getState: () => TState,
14
+ selector: (state: TState) => T
15
+ ): Ref<T> {
16
+ const stateRef = useObservable(getState);
17
+ return computed(() => selector(stateRef.value)) as unknown as Ref<T>;
18
+ }
@@ -0,0 +1,27 @@
1
+ import { shallowRef, triggerRef, onMounted, onUnmounted } from "vue";
2
+ import { viewportManager } from "@webiny/website-builder-sdk";
3
+
4
+ /**
5
+ * Returns a reactive ref that contains the current viewport info.
6
+ *
7
+ * Starts with the current value (or a sensible default on the server) and
8
+ * updates whenever the viewport manager fires a change-end event.
9
+ */
10
+ export const useViewport = () => {
11
+ const viewport = shallowRef(viewportManager.getViewport());
12
+ let unsubscribe: (() => void) | undefined;
13
+
14
+ onMounted(() => {
15
+ // Sync the latest value after mount (in case a resize already happened).
16
+ viewport.value = viewportManager.getViewport();
17
+
18
+ unsubscribe = viewportManager.onViewportChangeEnd(newViewport => {
19
+ viewport.value = newViewport;
20
+ triggerRef(viewport);
21
+ });
22
+ });
23
+
24
+ onUnmounted(() => unsubscribe?.());
25
+
26
+ return viewport;
27
+ };
@@ -0,0 +1,55 @@
1
+ import type {
2
+ Component,
3
+ ComponentInput,
4
+ ComponentManifest,
5
+ ComponentManifestInput,
6
+ InputFactory
7
+ } from "@webiny/website-builder-sdk";
8
+ import { createSlotInput } from "@webiny/website-builder-sdk";
9
+ import type { ExtractInputs } from "~/types.js";
10
+
11
+ /**
12
+ * Wraps a Vue component with a Webiny Website Builder manifest so it can be
13
+ * registered with the SDK and rendered inside a DocumentRenderer.
14
+ *
15
+ * Usage:
16
+ * ```ts
17
+ * import { createComponent, createTextInput } from "@webiny/website-builder-vue";
18
+ *
19
+ * const MyBanner = (props: ComponentProps<{ headline: string }>) =>
20
+ * h("div", props.inputs.headline);
21
+ *
22
+ * export const Banner = createComponent(MyBanner, {
23
+ * name: "My/Banner",
24
+ * label: "Banner",
25
+ * inputs: { headline: createTextInput({ label: "Headline" }) }
26
+ * });
27
+ * ```
28
+ */
29
+ export function createComponent<
30
+ TComponent extends (props: any) => any,
31
+ TInputs extends ExtractInputs<Parameters<TComponent>[0]>
32
+ >(component: TComponent, manifest: ComponentManifestInput<TInputs>): Component {
33
+ const inputs: ComponentInput[] = [];
34
+
35
+ if (Array.isArray(manifest.inputs)) {
36
+ inputs.push(...manifest.inputs);
37
+ } else {
38
+ const inputsObject: Record<string, InputFactory<any>> = manifest.inputs ?? {};
39
+ Object.keys(inputsObject).forEach((name: string) => {
40
+ inputs.push({ ...inputsObject[name], name });
41
+ });
42
+ }
43
+
44
+ if (manifest.acceptsChildren) {
45
+ const hasChildren = inputs.some(input => input.name === "children");
46
+ if (!hasChildren) {
47
+ inputs.push(createSlotInput({ name: "children" }));
48
+ }
49
+ }
50
+
51
+ return {
52
+ component,
53
+ manifest: { ...manifest, tags: manifest.tags ?? [], inputs } as ComponentManifest
54
+ };
55
+ }
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { createComponent } from "~/createComponent.js";
3
+ import { BoxComponent } from "./Box.js";
4
+
5
+ export const Box = createComponent(BoxComponent, {
6
+ name: "Webiny/Box",
7
+ label: "Box",
8
+ group: "basic",
9
+ image: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M120-120v-720h720v720H120Zm80-80h560v-560H200v560Zm0 0v-560 560Z"/></svg>`,
10
+ acceptsChildren: true,
11
+ defaults: {
12
+ styles: {
13
+ paddingTop: "5px",
14
+ paddingRight: "5px",
15
+ paddingBottom: "5px",
16
+ paddingLeft: "5px"
17
+ }
18
+ }
19
+ });
@@ -0,0 +1,8 @@
1
+ import type { ComponentPropsWithChildren } from "~/types.js";
2
+
3
+ /**
4
+ * A generic container that renders only its children slot.
5
+ */
6
+ export const BoxComponent = (props: ComponentPropsWithChildren) => {
7
+ return props.inputs?.children ?? null;
8
+ };
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { createTextInput } from "@webiny/website-builder-sdk";
3
+ import { createComponent } from "~/createComponent.js";
4
+ import { FragmentComponent } from "./Fragment.js";
5
+
6
+ export const Fragment = createComponent(FragmentComponent, {
7
+ name: "Webiny/Fragment",
8
+ label: "Fragment",
9
+ group: "basic",
10
+ image: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M120-120v-720h720v720H120Zm80-80h560v-560H200v560Zm0 0v-560 560Z"/></svg>`,
11
+ inputs: [
12
+ createTextInput({
13
+ name: "name",
14
+ label: "Fragment",
15
+ description: "Select fragment to display.",
16
+ renderer: "Webiny/FragmentSelector"
17
+ })
18
+ ]
19
+ });
@@ -0,0 +1,57 @@
1
+ import { h } from "vue";
2
+ import { contentSdk } from "@webiny/website-builder-sdk";
3
+ import { useDocumentFragments } from "~/components/FragmentsProvider.js";
4
+ import type { ComponentProps } from "~/types.js";
5
+ import type { DocumentFragments } from "~/components/FragmentsProvider.js";
6
+ import type { DocumentFragmentConfig } from "~/components/FragmentsProvider.js";
7
+
8
+ type FragmentProps = ComponentProps<{ name: string }>;
9
+
10
+ const findFixed = (fragments: DocumentFragments, name: string) =>
11
+ fragments.find(f => f.type === "fixed" && f.name === name) as
12
+ | Extract<DocumentFragmentConfig, { type: "fixed" }>
13
+ | undefined;
14
+
15
+ const FragmentPlaceholder = (props: { name: string }) => {
16
+ const label = props.name ? h("strong", null, [` ${props.name} `]) : " ";
17
+
18
+ return h(
19
+ "div",
20
+ {
21
+ style: {
22
+ display: "flex",
23
+ height: "100px",
24
+ backgroundColor: "#f4f4f4",
25
+ justifyContent: "center",
26
+ alignItems: "center",
27
+ fill: "#ffffff"
28
+ }
29
+ },
30
+ ["This is a placeholder for", label, "content coming from your frontend app."]
31
+ );
32
+ };
33
+
34
+ /**
35
+ * Looks up a named fixed fragment provided by the consumer's DocumentRenderer
36
+ * and renders it in place. Shows a placeholder in editing mode when no
37
+ * matching fragment is found.
38
+ */
39
+ export const FragmentComponent = (props: FragmentProps) => {
40
+ const isEditing = contentSdk.isEditing();
41
+ const fragments = useDocumentFragments();
42
+ const fragment = findFixed(fragments, props.inputs?.name);
43
+
44
+ if (!fragment && isEditing) {
45
+ return h(FragmentPlaceholder, { name: props.inputs?.name });
46
+ }
47
+
48
+ if (fragment) {
49
+ // `element` is a Vue slot function (() => VNode[]) or a VNode.
50
+ if (typeof fragment.element === "function") {
51
+ return h("div", null, fragment.element());
52
+ }
53
+ return fragment.element;
54
+ }
55
+
56
+ return null;
57
+ };
@@ -0,0 +1,166 @@
1
+ "use client";
2
+ import {
3
+ createBooleanInput,
4
+ createElement,
5
+ createNumberInput,
6
+ createObjectInput,
7
+ createSelectInput,
8
+ createSlotInput,
9
+ createTextInput
10
+ } from "@webiny/website-builder-sdk";
11
+ import { createComponent } from "~/createComponent.js";
12
+ import { GridComponent } from "./Grid.js";
13
+
14
+ export const Grid = createComponent(GridComponent, {
15
+ name: "Webiny/Grid",
16
+ label: "Grid",
17
+ image: `<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M120-520v-320h320v320H120Zm0 400v-320h320v320H120Zm400-400v-320h320v320H520Zm0 400v-320h320v320H520ZM200-600h160v-160H200v160Zm400 0h160v-160H600v160Zm0 400h160v-160H600v160Zm-400 0h160v-160H200v160Zm400-400Zm0 240Zm-240 0Zm0-240Z"/></svg>`,
18
+ group: "basic",
19
+ autoApplyStyles: false,
20
+ inputs: [
21
+ createTextInput({
22
+ name: "gridLayout",
23
+ label: "Grid Layout",
24
+ renderer: "Webiny/GridLayout",
25
+ onChange: ({ inputs, createElement: ce }) => {
26
+ const rowColumnCount = inputs.gridLayout.split("-").length;
27
+ const columns = inputs.columns.length;
28
+ const remainder = columns % rowColumnCount;
29
+
30
+ if (remainder !== 0) {
31
+ const fullColumnCount = rowColumnCount * inputs.rowCount;
32
+ const toCreate =
33
+ columns > fullColumnCount ? remainder : rowColumnCount - remainder;
34
+
35
+ Array.from({ length: toCreate }).forEach(() => {
36
+ inputs.columns.push({
37
+ children: ce({ component: "Webiny/GridColumn" })
38
+ });
39
+ });
40
+ }
41
+
42
+ inputs.rowCount = inputs.columns.length / rowColumnCount;
43
+ }
44
+ }),
45
+ createNumberInput({
46
+ name: "rowCount",
47
+ label: "Row Count",
48
+ defaultValue: 1,
49
+ minValue: 1,
50
+ onChange: ({ inputs, createElement: ce }) => {
51
+ const columnCount = inputs.gridLayout.split("-").length;
52
+ const rowCount = Math.max(1, inputs.rowCount);
53
+ const columns = inputs.columns;
54
+ const rows: unknown[][] = [];
55
+
56
+ for (let i = 0; i < columns.length; i += columnCount) {
57
+ rows.push(columns.slice(i, i + columnCount));
58
+ }
59
+
60
+ if (rows.length > rowCount) {
61
+ inputs.columns = columns.slice(0, columnCount * rowCount);
62
+ return;
63
+ }
64
+
65
+ const createRows = Math.max(0, rowCount - rows.length);
66
+ if (createRows <= 0) {
67
+ return;
68
+ }
69
+
70
+ const newRows = Array.from({ length: createRows * columnCount }).map(() => ({
71
+ children: ce({ component: "Webiny/GridColumn" })
72
+ }));
73
+
74
+ inputs.columns.push(...newRows);
75
+ }
76
+ }),
77
+ createNumberInput({
78
+ name: "rowGap",
79
+ label: "Row Gap",
80
+ defaultValue: 0,
81
+ responsive: true,
82
+ onChange: ({ inputs, styles }) => {
83
+ const v = parseInt(inputs.rowGap);
84
+ if (isNaN(v)) {
85
+ delete styles.rowGap;
86
+ } else {
87
+ styles.rowGap = `${inputs.rowGap}px`;
88
+ }
89
+ }
90
+ }),
91
+ createNumberInput({
92
+ name: "columnGap",
93
+ label: "Column Gap",
94
+ defaultValue: 0,
95
+ responsive: true,
96
+ onChange: ({ inputs, styles }) => {
97
+ const v = parseInt(inputs.columnGap);
98
+ if (isNaN(v)) {
99
+ delete styles.columnGap;
100
+ } else {
101
+ styles.columnGap = `${inputs.columnGap}px`;
102
+ }
103
+ }
104
+ }),
105
+ createSelectInput({
106
+ name: "stackAtBreakpoint",
107
+ label: "Stack at breakpoint",
108
+ options: [
109
+ { label: "Tablet", value: "tablet" },
110
+ { label: "Mobile", value: "mobile" }
111
+ ]
112
+ }),
113
+ createBooleanInput({
114
+ name: "reverseWhenStacked",
115
+ label: "Reverse columns when stacked"
116
+ }),
117
+ createObjectInput({
118
+ name: "columns",
119
+ list: true,
120
+ hideFromUi: true,
121
+ fields: [
122
+ createSlotInput({
123
+ name: "children",
124
+ list: false,
125
+ components: ["Webiny/GridColumn"]
126
+ })
127
+ ]
128
+ })
129
+ ],
130
+ defaults: {
131
+ inputs: {
132
+ gridLayout: "6-6",
133
+ columns: [
134
+ {
135
+ children: createElement({
136
+ component: "Webiny/GridColumn",
137
+ inputs: { children: [createElement({ component: "Webiny/Lexical" })] }
138
+ })
139
+ },
140
+ {
141
+ children: createElement({
142
+ component: "Webiny/GridColumn",
143
+ inputs: { children: [createElement({ component: "Webiny/Lexical" })] }
144
+ })
145
+ }
146
+ ]
147
+ },
148
+ styles: {
149
+ boxSizing: "border-box",
150
+ display: "flex",
151
+ flexDirection: "row",
152
+ flexWrap: "wrap",
153
+ justifyContent: "flex-start",
154
+ alignItems: "stretch",
155
+ width: "100%",
156
+ marginTop: "0px",
157
+ marginBottom: "0px",
158
+ marginLeft: "0px",
159
+ marginRight: "0px",
160
+ paddingTop: "5px",
161
+ paddingRight: "5px",
162
+ paddingBottom: "5px",
163
+ paddingLeft: "5px"
164
+ }
165
+ }
166
+ });