Motyw
Content builder
Content builder umożliwia redaktorowi układanie treści strony z bloków. Struktura jest trójpoziomowa i w całości mieści się wewnątrz formularza vee-validate — wartości nie są przechowywane w osobnym store.
Te same typy treści w stałym formularzu
Gdy struktura treści ma być narzucona przez dewelopera, a nie układana przez redaktora, zamiast buildera używa się Custom fields. Oba mechanizmy korzystają z tych samych typów treści i tych samych modeli po stronie backendu.
Hierarchia
section_groups[] grupa sekcji
└── sections[] sekcja
└── items[] blok (element sekcji)| Poziom | Komponent pola | Definicja formularza |
|---|---|---|
| korzeń | form/VContentBuilder.vue | — |
| grupa | ContentBuilderSectionGroupField.vue | forms/section-group.form.js |
| sekcja | ContentBuilderSectionsField.vue, ContentBuilderSectionField.vue | forms/section.form.js |
| blok | ContentBuilderSectionItemField.vue | forms/section-item.form.js |
Każdy poziom stanowi useFieldArray opakowany w vuedraggable z uchwytem .drag-handle.
Zastosowanie w formularzu
vue
<!-- src/modules/blog/components/BlogArticleForm.vue -->
<base-form-section class="q-mb-lg">
<v-content-builder />
</base-form-section>js
// src/modules/blog/forms/article.form.js
initialValues() {
return {
// ...
section_groups: [],
};
},
validationSchema() {
return object({
// ...
section_groups: array().of(getContentBuilderValidationSchema()),
});
}Propsy komponentu VContentBuilder
| Prop | Typ | Domyślnie | Opis |
|---|---|---|---|
name | String | "section_groups" | Ścieżka pola w formularzu |
mode | String | null | Wartość "shared-section" ukrywa typ bloku odpowiadający sekcji współdzielonej |
Wartość name musi odpowiadać wartościom początkowym i schematowi
Zmieniając prop name, zmień jednocześnie klucz w initialValues() oraz w validationSchema().
Transformacja danych z API
Zapytanie o szczegóły musi przepuścić dane przez transformContentBuilderData:
js
// src/modules/blog/api/queries/article.queries.js
queryFn: async () => {
const { data } = await getArticle(toValue(id));
return {
...data.model,
section_groups: transformContentBuilderData(data.model.section_groups),
};
},Bez tej transformacji builder nie wczyta treści
Funkcja transformContentBuilderData normalizuje strukturę otrzymaną z backendu, w tym kolekcje mediów, do kształtu oczekiwanego przez pola formularza. Uwzględnij ją w każdym modelu korzystającym z content buildera — jej brak objawia się pustym builderem podczas edycji, bez żadnego błędu.
Rejestr typów bloków
Typy bloków zdefiniowane są w dwóch plikach:
js
// src/modules/contentBuilder/constants/section-types.js
export const HEADER = "header";
export const TEXT = "text";
export const TEXT_IMAGE = "text-image";
export const YOUTUBE = "youtube";
export const IMAGE = "image";
export const FILE = "file";
export const SHARED_SECTION = "shared-section";
export const CUSTOM_ITEM = "custom-item";
export const OTHER = "other";js
// src/modules/contentBuilder/configs/section-type-config.js
export const sectionTypeConfig = {
[HEADER]: {
component: () =>
import("modules/contentBuilder/components/sectionItem/ContentBuilderSectionItemHeader.vue"),
getFields: () => ({
title: "",
tag: "h1",
}),
validation: {
title: string().required().label("labels.title"),
tag: string().required().label("labels.type"),
},
itemOptions: {
layout: null,
hasGaps: false,
hasImgClasses: false,
},
customComponents: [],
allowCopy: true,
allowDelete: true,
},
// ...
};Pola konfiguracji typu
| Pole | Typ | Opis |
|---|---|---|
component | funkcja | Fabryka dynamicznego importu komponentu bloku |
getFields | funkcja | Zwraca wartości początkowe pól bloku |
validation | obiekt | Kształt yup przekazywany do object() — walidacja bloku |
itemOptions.layout | string | null | Wskazuje zestaw opcji układu do wyświetlenia |
itemOptions.hasGaps | boolean | Włącza ustawienia odstępów |
itemOptions.hasImgClasses | boolean | Włącza ustawienia klas obrazu |
customComponents | tablica | Dodatkowe komponenty opcji |
allowCopy | boolean | Zezwala na kopiowanie bloku |
allowDelete | boolean | Zezwala na usunięcie bloku |
Dodawanie nowego typu bloku
Krok 1: dodaj stałą typu
js
// src/modules/contentBuilder/constants/section-types.js
export const QUOTE = "quote";Krok 2: utwórz komponent bloku
vue
<!-- src/modules/contentBuilder/components/sectionItem/ContentBuilderSectionItemQuote.vue -->
<script setup>
const props = defineProps({
name: { type: String, required: true },
});
</script>
<template>
<div class="row q-col-gutter-md">
<v-input class="col-12" :name="`${name}.quote`" :label="$t('labels.quote')" />
<v-input class="col-12" :name="`${name}.author`" :label="$t('labels.author')" />
</div>
</template>Prop name zawiera pełną ścieżkę pola
Komponent bloku otrzymuje ścieżkę w rodzaju section_groups[0].sections[1].items[0]. Nazwy pól buduj więc zawsze przez interpolację — `${name}.quote`. Nazwa zapisana na sztywno nie podepnie się pod formularz i przestanie działać przy przestawianiu bloków.
Krok 3: dopisz wpis w rejestrze
js
// src/modules/contentBuilder/configs/section-type-config.js
[QUOTE]: {
component: () =>
import("modules/contentBuilder/components/sectionItem/ContentBuilderSectionItemQuote.vue"),
getFields: () => ({
quote: "",
author: "",
}),
validation: {
quote: string().required().label("labels.quote"),
author: string().nullable().label("labels.author"),
},
itemOptions: {
layout: null,
hasGaps: false,
hasImgClasses: false,
},
customComponents: [],
allowCopy: true,
allowDelete: true,
},Krok 4: uzupełnij tłumaczenia
Dodaj etykiety w src/modules/contentBuilder/trans/{en,pl} — opisują to Tłumaczenia.
Krok 5: zgłoś typ po stronie backendu
Lista typów pochodzi z API
Dostępne typy pobierane są zapytaniem GET content-builder/section-types do store'a content-builder. Sam wpis w sectionTypeConfig nie wystarczy — dopóki backend nie zwróci nowego typu, nie pojawi się on w selektorze dodawania bloku.
Renderowanie bloku w czasie działania
js
// src/modules/contentBuilder/components/sectionItem/ContentBuilderSectionItemField.vue
const { componentIs, componentAttrs } = useContentBuilderSectionItemComponent(
sectionItem,
() => props.type,
);vue
<component :is="componentIs" :name="name" v-bind="componentAttrs" />Composable odczytuje sectionTypeConfig[type].component i opakowuje go w defineAsyncComponent. Dla typu custom-item dokłada dodatkowo prop options, pochodzący z pola option_settings bloku.
Dodawanie bloków przez użytkownika
Komponent ContentBuilderSectionSelect.vue to niewielki formularz vee-validate z polami section_type oraz — dla typów oznaczonych jako selectable — sectionable. Emituje zdarzenia add i copy z ładunkiem { sectionType, sectionable }.
Dane do wyboru pochodzą ze store'a content-builder:
| Akcja | Endpoint |
|---|---|
fetchSectionTypes() | GET content-builder/section-types |
fetchSectionables({ type }) | GET content-builder/sectionables?type=... |
Getter sectionTypesBasedOnMode filtruje listę — w trybie "shared-section" usuwa typ shared-section, uniemożliwiając zagnieżdżenie sekcji współdzielonej w niej samej.
Wartości początkowe nowego bloku tworzy sectionItemForm:
js
// src/modules/contentBuilder/forms/section-item.form.js
sectionItemForm.initialValues(type, currentValues?) // zwykły blok
sectionItemForm.selectableInitialValues(type, sectionable) // blok wskazujący na inny byt
sectionItemForm.copyValues(item, isSelectable) // kopia bez identyfikatoraOpcje bloków, sekcji i grup
js
import { getOptions } from "modules/contentBuilder/helpers/forms/options";
getOptions("group"); // klasy, kontener, marginesy, ustawienia flex/grid
getOptions("section"); // jak wyżej
getOptions("item"); // layout, gap_x, gap_y, img_classesKluczami wspólnymi są classes, custom_component, in_container, custom_padding oraz custom_margin.
Za renderowanie opcji odpowiadają komponenty options/ContentBuilderSectionOptionsField.vue, options/ContentBuilderSectionItemOptionsField.vue oraz ContentBuilderOptionSettingsField.vue. Dostępne układy zdefiniowane są w pliku constants/section-item-layouts.js.
Zakres opcji widocznych dla redaktora wynika z pola itemOptions w rejestrze typu.
Sekcje współdzielone
Sekcja współdzielona to zestaw grup sekcji zapisany jednokrotnie i wstawiany do wielu stron.
| Element | Plik |
|---|---|
| Formularz | ContentBuilderSharedSectionForm.vue |
| Lista | ContentBuilderSharedSectionsTable.vue |
| Strony | pages/sharedSections/ |
Podczas edycji samej sekcji współdzielonej builder uruchamiany jest w trybie ograniczonym:
vue
<!-- ContentBuilderSharedSectionForm.vue -->
<v-content-builder mode="shared-section" />W formularzu docelowej strony sekcja jest wstawiana jako blok typu shared-section. Karta pozwala przejść do jej edycji bez opuszczania formularza strony; zapis aktualizuje sekcję współdzieloną. Custom fields zależne od typu są rejestrowane osobno w src/modules/contentBuilder/constants/shared-section-custom-fields.js. Nie są kopiowane do custom fields strony. Szczegóły: Pola sekcji współdzielonych.