includio-cms 0.5.2 → 0.5.3
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/CHANGELOG.md +19 -0
- package/ROADMAP.md +13 -0
- package/dist/admin/client/entry/entry-form.svelte +1 -0
- package/dist/admin/client/entry/entry.svelte +130 -123
- package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +92 -9
- package/dist/admin/components/fields/blocks-field.svelte +142 -112
- package/dist/admin/components/fields/blocks-field.svelte.d.ts +10 -30
- package/dist/admin/components/fields/boolean-field.svelte +28 -38
- package/dist/admin/components/fields/boolean-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/checkboxes-field.svelte +12 -24
- package/dist/admin/components/fields/checkboxes-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/content-field.svelte +4 -17
- package/dist/admin/components/fields/content-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/date-field.svelte +8 -21
- package/dist/admin/components/fields/date-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/datetime-field.svelte +8 -21
- package/dist/admin/components/fields/datetime-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/field-renderer.svelte +32 -19
- package/dist/admin/components/fields/field-renderer.svelte.d.ts +1 -1
- package/dist/admin/components/fields/field-value-bridge.svelte +21 -0
- package/dist/admin/components/fields/field-value-bridge.svelte.d.ts +31 -0
- package/dist/admin/components/fields/fields-form.svelte +13 -10
- package/dist/admin/components/fields/file-field.svelte +12 -27
- package/dist/admin/components/fields/file-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/image-field.svelte +13 -28
- package/dist/admin/components/fields/image-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/media-field.svelte +15 -30
- package/dist/admin/components/fields/media-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/number-field.svelte +6 -20
- package/dist/admin/components/fields/number-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/object-field.svelte +26 -29
- package/dist/admin/components/fields/object-field.svelte.d.ts +11 -31
- package/dist/admin/components/fields/radio-field.svelte +8 -20
- package/dist/admin/components/fields/radio-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/relation-field.svelte +15 -30
- package/dist/admin/components/fields/relation-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/richtext-field.svelte +4 -17
- package/dist/admin/components/fields/richtext-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/select-field.svelte +14 -28
- package/dist/admin/components/fields/select-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/seo-field.svelte +5 -12
- package/dist/admin/components/fields/seo-field.svelte.d.ts +8 -28
- package/dist/admin/components/fields/simple-array-field.svelte +29 -42
- package/dist/admin/components/fields/simple-array-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/slug-field.svelte +6 -11
- package/dist/admin/components/fields/slug-field.svelte.d.ts +6 -26
- package/dist/admin/components/fields/text-field-wrapper.svelte +22 -40
- package/dist/admin/components/fields/text-field.svelte +7 -19
- package/dist/admin/components/fields/text-field.svelte.d.ts +5 -27
- package/dist/admin/components/fields/url-field-wrapper.svelte +8 -3
- package/dist/admin/components/fields/url-field.svelte +294 -128
- package/dist/admin/components/fields/url-field.svelte.d.ts +5 -27
- package/dist/admin/components/layout/layout-renderer.svelte +8 -6
- package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +221 -31
- package/dist/admin/components/tiptap/content-editor.svelte +13 -2
- package/dist/admin/components/tiptap/inline-block-node.d.ts +1 -0
- package/dist/admin/components/tiptap/inline-block-node.js +18 -1
- package/dist/admin/components/tiptap/slash-command.js +2 -3
- package/dist/admin/components/tiptap/standalone-form.d.ts +7 -0
- package/dist/admin/components/tiptap/standalone-form.js +31 -0
- package/dist/admin/components/tiptap/tiptap-editor.svelte +7 -0
- package/dist/admin/remote/entry.remote.js +16 -0
- package/dist/admin/styles/admin.css +10 -0
- package/dist/admin/utils/fieldCondition.d.ts +6 -0
- package/dist/admin/utils/fieldCondition.js +20 -0
- package/dist/components/ui/switch/index.d.ts +2 -0
- package/dist/components/ui/switch/index.js +4 -0
- package/dist/components/ui/switch/switch.svelte +26 -0
- package/dist/components/ui/switch/switch.svelte.d.ts +4 -0
- package/dist/core/fields/fieldSchemaToTs.js +15 -3
- package/dist/core/fields/formFieldSchemaToTs.js +22 -6
- package/dist/core/fields/urlUtils.d.ts +14 -0
- package/dist/core/fields/urlUtils.js +21 -0
- package/dist/core/server/fields/populateEntry.js +43 -0
- package/dist/core/server/fields/resolveImageFields.js +33 -1
- package/dist/core/server/fields/resolveRelationFields.js +46 -0
- package/dist/core/server/fields/resolveRichtextLinks.js +15 -1
- package/dist/core/server/fields/resolveUrlFields.js +65 -0
- package/dist/core/server/generator/formFieldSchemaToString.js +40 -9
- package/dist/core/server/generator/formFields.js +2 -0
- package/dist/core/server/generator/generator.js +25 -1
- package/dist/schemas/field/url.d.ts +2 -0
- package/dist/schemas/field/url.js +4 -2
- package/dist/types/fields.d.ts +9 -0
- package/dist/types/formFields.d.ts +15 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/updates/0.5.3/index.d.ts +2 -0
- package/dist/updates/0.5.3/index.js +19 -0
- package/dist/updates/index.js +2 -1
- package/package.json +2 -1
- package/dist/admin/components/fields/standalone-field-renderer.svelte +0 -148
- package/dist/admin/components/fields/standalone-field-renderer.svelte.d.ts +0 -9
|
@@ -1,25 +1,144 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { NodeViewWrapper } from 'svelte-tiptap';
|
|
3
3
|
import type { NodeViewProps } from '@tiptap/core';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import FieldRenderer from '../fields/field-renderer.svelte';
|
|
5
|
+
import { createStandaloneForm } from './standalone-form.js';
|
|
6
|
+
import type { ObjectField, BlocksField, Field, FieldType } from '../../../types/fields.js';
|
|
7
|
+
import { getContentLanguage } from '../../state/content-language.svelte.js';
|
|
6
8
|
import GripVertical from '@tabler/icons-svelte/icons/grip-vertical';
|
|
7
9
|
import ArrowUp from '@tabler/icons-svelte/icons/arrow-up';
|
|
8
10
|
import ArrowDown from '@tabler/icons-svelte/icons/arrow-down';
|
|
9
11
|
import Trash from '@tabler/icons-svelte/icons/trash';
|
|
12
|
+
import ChevronDown from '@tabler/icons-svelte/icons/chevron-down';
|
|
13
|
+
import { slide } from 'svelte/transition';
|
|
14
|
+
import { onMount, onDestroy } from 'svelte';
|
|
15
|
+
import type { FormPathLeaves } from 'sveltekit-superforms';
|
|
16
|
+
import { evaluateCondition } from '../../utils/fieldCondition.js';
|
|
10
17
|
|
|
11
18
|
let { node, updateAttributes, editor, getPos, deleteNode, selected }: NodeViewProps = $props();
|
|
12
19
|
|
|
20
|
+
let collapsed = $state(false);
|
|
21
|
+
|
|
22
|
+
const contentLanguage = getContentLanguage();
|
|
23
|
+
|
|
24
|
+
const SKIP_TYPES: Set<FieldType> = new Set(['slug', 'seo']);
|
|
25
|
+
|
|
26
|
+
function normalizeBlockData(
|
|
27
|
+
data: Record<string, unknown>,
|
|
28
|
+
fields: Field[],
|
|
29
|
+
langs: string[]
|
|
30
|
+
): Record<string, unknown> {
|
|
31
|
+
const result = { ...data };
|
|
32
|
+
for (const f of fields) {
|
|
33
|
+
const key = f.slug;
|
|
34
|
+
if (['text', 'richtext', 'content'].includes(f.type)) {
|
|
35
|
+
if (typeof result[key] !== 'object' || result[key] == null) {
|
|
36
|
+
const val = result[key] ?? (f.type === 'content' ? null : '');
|
|
37
|
+
result[key] = Object.fromEntries(langs.map((l) => [l, val]));
|
|
38
|
+
}
|
|
39
|
+
} else if (f.type === 'url') {
|
|
40
|
+
const v = result[key] as Record<string, unknown> | undefined;
|
|
41
|
+
if (!v || typeof v !== 'object') {
|
|
42
|
+
result[key] = { id: '', url: {} };
|
|
43
|
+
} else if (!('url' in v) || v.url == null) {
|
|
44
|
+
result[key] = { ...v, url: {} };
|
|
45
|
+
} else if (typeof v.url === 'string') {
|
|
46
|
+
const urlStr = v.url as string;
|
|
47
|
+
const textStr = typeof v.text === 'string' ? v.text : '';
|
|
48
|
+
result[key] = {
|
|
49
|
+
...v,
|
|
50
|
+
url: Object.fromEntries(langs.map((l) => [l, urlStr])),
|
|
51
|
+
text: Object.fromEntries(langs.map((l) => [l, textStr]))
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const urlField = f as import('../../../types/fields.js').UrlField;
|
|
55
|
+
const d = result[key] as Record<string, unknown>;
|
|
56
|
+
if (urlField.text && !d.text) d.text = {};
|
|
57
|
+
if (urlField.newTab && d.newTab === undefined) d.newTab = false;
|
|
58
|
+
if (urlField.rel && d.rel === undefined) d.rel = '';
|
|
59
|
+
} else if (f.type === 'blocks') {
|
|
60
|
+
const bf = f as BlocksField;
|
|
61
|
+
if (Array.isArray(result[key])) {
|
|
62
|
+
result[key] = (result[key] as any[]).map((item) => ({
|
|
63
|
+
...item,
|
|
64
|
+
data: normalizeBlockData(
|
|
65
|
+
item.data ?? {},
|
|
66
|
+
bf.of.find((d: ObjectField) => d.slug === item.slug)?.fields ?? [],
|
|
67
|
+
langs
|
|
68
|
+
)
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
} else if (f.type === 'object') {
|
|
72
|
+
const of_ = f as ObjectField;
|
|
73
|
+
if (result[key] && typeof result[key] === 'object' && 'data' in (result[key] as object)) {
|
|
74
|
+
const obj = result[key] as Record<string, unknown>;
|
|
75
|
+
result[key] = { ...obj, data: normalizeBlockData(obj.data as Record<string, unknown>, of_.fields, langs) };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function denormalizeBlockData(
|
|
83
|
+
data: Record<string, unknown>,
|
|
84
|
+
fields: Field[],
|
|
85
|
+
currentLang: string
|
|
86
|
+
): Record<string, unknown> {
|
|
87
|
+
const result = { ...data };
|
|
88
|
+
for (const f of fields) {
|
|
89
|
+
const key = f.slug;
|
|
90
|
+
const val = result[key];
|
|
91
|
+
if (['text', 'richtext'].includes(f.type)) {
|
|
92
|
+
if (val && typeof val === 'object' && !Array.isArray(val)) {
|
|
93
|
+
result[key] = (val as Record<string, unknown>)[currentLang] ?? '';
|
|
94
|
+
}
|
|
95
|
+
} else if (f.type === 'url' && val && typeof val === 'object') {
|
|
96
|
+
const v = val as Record<string, unknown>;
|
|
97
|
+
const flat: Record<string, unknown> = {};
|
|
98
|
+
if (v.id !== undefined) flat.id = v.id;
|
|
99
|
+
flat.url =
|
|
100
|
+
v.url && typeof v.url === 'object'
|
|
101
|
+
? (v.url as Record<string, string>)[currentLang] ?? ''
|
|
102
|
+
: v.url ?? '';
|
|
103
|
+
if ('text' in v) {
|
|
104
|
+
flat.text =
|
|
105
|
+
v.text && typeof v.text === 'object'
|
|
106
|
+
? (v.text as Record<string, string>)[currentLang] ?? ''
|
|
107
|
+
: v.text ?? '';
|
|
108
|
+
}
|
|
109
|
+
if (v.newTab !== undefined) flat.newTab = v.newTab;
|
|
110
|
+
if (v.rel !== undefined) flat.rel = v.rel;
|
|
111
|
+
result[key] = flat;
|
|
112
|
+
} else if (f.type === 'blocks') {
|
|
113
|
+
const bf = f as BlocksField;
|
|
114
|
+
if (Array.isArray(val)) {
|
|
115
|
+
result[key] = (val as any[]).map((item) => ({
|
|
116
|
+
...item,
|
|
117
|
+
data: denormalizeBlockData(
|
|
118
|
+
item.data ?? {},
|
|
119
|
+
bf.of.find((d: ObjectField) => d.slug === item.slug)?.fields ?? [],
|
|
120
|
+
currentLang
|
|
121
|
+
)
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
} else if (f.type === 'object') {
|
|
125
|
+
const of_ = f as ObjectField;
|
|
126
|
+
if (val && typeof val === 'object' && 'data' in (val as object)) {
|
|
127
|
+
const obj = val as Record<string, unknown>;
|
|
128
|
+
result[key] = { ...obj, data: denormalizeBlockData(obj.data as Record<string, unknown>, of_.fields, currentLang) };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
13
135
|
const inlineBlocks: ObjectField[] = $derived(
|
|
14
136
|
(editor.extensionManager.extensions.find((e) => e.name === 'inlineBlock')?.options as { inlineBlocks: ObjectField[] })?.inlineBlocks ?? []
|
|
15
137
|
);
|
|
16
138
|
|
|
17
139
|
const blockDef = $derived(inlineBlocks.find((b) => b.slug === node.attrs.blockType));
|
|
18
140
|
const blockLabel = $derived(blockDef?.label ? (Object.values(blockDef.label)[0] ?? blockDef.slug) : node.attrs.blockType);
|
|
19
|
-
|
|
20
|
-
let blockData = $state<Record<string, unknown>>(parseBlockData(node.attrs.blockData));
|
|
21
|
-
// Track last JSON we wrote to PM to avoid re-parsing our own writes
|
|
22
|
-
let lastWrittenJson = '';
|
|
141
|
+
const supportedFields = $derived(blockDef?.fields.filter((f) => !SKIP_TYPES.has(f.type)) ?? []);
|
|
23
142
|
|
|
24
143
|
function parseBlockData(raw: unknown): Record<string, unknown> {
|
|
25
144
|
if (typeof raw === 'string') {
|
|
@@ -29,25 +148,41 @@
|
|
|
29
148
|
return {};
|
|
30
149
|
}
|
|
31
150
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (rawJson !== lastWrittenJson) {
|
|
37
|
-
blockData = parseBlockData(raw);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
151
|
+
const langs = contentLanguage.all;
|
|
152
|
+
const allFields = blockDef?.fields ?? [];
|
|
153
|
+
const standaloneForm = createStandaloneForm(normalizeBlockData(parseBlockData(node.attrs.blockData), allFields, langs));
|
|
154
|
+
const formStore = standaloneForm.form;
|
|
40
155
|
|
|
156
|
+
// Track last JSON we wrote to PM to avoid re-parsing our own writes
|
|
157
|
+
let lastWrittenJson = '';
|
|
41
158
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
42
159
|
|
|
43
|
-
|
|
44
|
-
|
|
160
|
+
// Form store → ProseMirror (user edits)
|
|
161
|
+
const unsubscribe = formStore.subscribe((data) => {
|
|
45
162
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
46
163
|
debounceTimer = setTimeout(() => {
|
|
47
|
-
|
|
48
|
-
|
|
164
|
+
const denormalized = denormalizeBlockData(data, allFields, contentLanguage.current);
|
|
165
|
+
const json = JSON.stringify(denormalized);
|
|
166
|
+
if (json !== lastWrittenJson) {
|
|
167
|
+
lastWrittenJson = json;
|
|
168
|
+
updateAttributes({ blockData: json });
|
|
169
|
+
}
|
|
49
170
|
}, 300);
|
|
50
|
-
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
onDestroy(() => {
|
|
174
|
+
unsubscribe();
|
|
175
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ProseMirror → Form store (undo/redo, external changes)
|
|
179
|
+
$effect(() => {
|
|
180
|
+
const raw = node.attrs.blockData;
|
|
181
|
+
const rawJson = typeof raw === 'string' ? raw : JSON.stringify(raw);
|
|
182
|
+
if (rawJson !== lastWrittenJson) {
|
|
183
|
+
formStore.set(normalizeBlockData(parseBlockData(raw), allFields, langs));
|
|
184
|
+
}
|
|
185
|
+
});
|
|
51
186
|
|
|
52
187
|
function moveUp() {
|
|
53
188
|
const pos = getPos();
|
|
@@ -116,7 +251,18 @@
|
|
|
116
251
|
<button type="button" class="grip-handle" aria-label="Przeciągnij" data-drag-handle>
|
|
117
252
|
<GripVertical size={16} />
|
|
118
253
|
</button>
|
|
119
|
-
<
|
|
254
|
+
<button
|
|
255
|
+
type="button"
|
|
256
|
+
class="inline-block-collapse-toggle"
|
|
257
|
+
onclick={() => (collapsed = !collapsed)}
|
|
258
|
+
aria-expanded={!collapsed}
|
|
259
|
+
aria-label={collapsed ? 'Rozwiń blok' : 'Zwiń blok'}
|
|
260
|
+
>
|
|
261
|
+
<span class="inline-block-label">{blockLabel}</span>
|
|
262
|
+
<span class="collapse-chevron" class:collapsed>
|
|
263
|
+
<ChevronDown size={14} />
|
|
264
|
+
</span>
|
|
265
|
+
</button>
|
|
120
266
|
</div>
|
|
121
267
|
<div class="inline-block-header-actions">
|
|
122
268
|
<button type="button" class="action-btn" onclick={moveUp} aria-label="Przenieś wyżej" title="Przenieś wyżej">
|
|
@@ -132,17 +278,25 @@
|
|
|
132
278
|
</div>
|
|
133
279
|
|
|
134
280
|
<!-- Body: field renderer -->
|
|
135
|
-
{#if
|
|
136
|
-
<div
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
281
|
+
{#if !collapsed}
|
|
282
|
+
<div transition:slide={{ duration: 150 }}>
|
|
283
|
+
{#if blockDef}
|
|
284
|
+
<div class="inline-block-body space-y-3">
|
|
285
|
+
{#each supportedFields as f}
|
|
286
|
+
{#if evaluateCondition(f.showWhen, (slug) => $formStore[slug])}
|
|
287
|
+
<FieldRenderer
|
|
288
|
+
field={f}
|
|
289
|
+
form={standaloneForm}
|
|
290
|
+
path={f.slug as FormPathLeaves<Record<string, unknown>>}
|
|
291
|
+
/>
|
|
292
|
+
{/if}
|
|
293
|
+
{/each}
|
|
294
|
+
</div>
|
|
295
|
+
{:else}
|
|
296
|
+
<div class="inline-block-body inline-block-unknown">
|
|
297
|
+
Nieznany typ bloku: {node.attrs.blockType}
|
|
298
|
+
</div>
|
|
299
|
+
{/if}
|
|
146
300
|
</div>
|
|
147
301
|
{/if}
|
|
148
302
|
</div>
|
|
@@ -175,18 +329,54 @@
|
|
|
175
329
|
min-height: 36px;
|
|
176
330
|
}
|
|
177
331
|
|
|
332
|
+
.inline-block-wrapper:has(.collapse-chevron.collapsed) .inline-block-header {
|
|
333
|
+
border-bottom-color: transparent;
|
|
334
|
+
}
|
|
335
|
+
|
|
178
336
|
.inline-block-header-left {
|
|
179
337
|
display: flex;
|
|
180
338
|
align-items: center;
|
|
181
339
|
gap: 0.25rem;
|
|
182
340
|
}
|
|
183
341
|
|
|
342
|
+
.inline-block-collapse-toggle {
|
|
343
|
+
display: flex;
|
|
344
|
+
align-items: center;
|
|
345
|
+
gap: 0.25rem;
|
|
346
|
+
border: none;
|
|
347
|
+
background: transparent;
|
|
348
|
+
cursor: pointer;
|
|
349
|
+
padding: 2px 4px;
|
|
350
|
+
border-radius: 4px;
|
|
351
|
+
color: inherit;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.inline-block-collapse-toggle:hover {
|
|
355
|
+
background: var(--accent);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.inline-block-collapse-toggle:focus-visible {
|
|
359
|
+
outline: 2px solid var(--ring);
|
|
360
|
+
outline-offset: 1px;
|
|
361
|
+
}
|
|
362
|
+
|
|
184
363
|
.inline-block-label {
|
|
185
364
|
font-size: 0.8125rem;
|
|
186
365
|
font-weight: 600;
|
|
187
366
|
color: var(--foreground);
|
|
188
367
|
}
|
|
189
368
|
|
|
369
|
+
.collapse-chevron {
|
|
370
|
+
display: flex;
|
|
371
|
+
align-items: center;
|
|
372
|
+
color: var(--muted-foreground);
|
|
373
|
+
transition: transform 0.15s ease;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.collapse-chevron.collapsed {
|
|
377
|
+
transform: rotate(-90deg);
|
|
378
|
+
}
|
|
379
|
+
|
|
190
380
|
.inline-block-header-actions {
|
|
191
381
|
display: flex;
|
|
192
382
|
align-items: center;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { onMount } from 'svelte';
|
|
2
|
+
import { onMount, getAllContexts } from 'svelte';
|
|
3
3
|
import { Editor, type Editor as EditorType } from '@tiptap/core';
|
|
4
4
|
import BubbleMenu from '@tiptap/extension-bubble-menu';
|
|
5
5
|
import { extensions } from './extensions.js';
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { InlineBlockNode } from './inline-block-node.js';
|
|
18
18
|
import { SlashCommand } from './slash-command.js';
|
|
19
19
|
import { HeadingA11yPlugin } from './heading-a11y-plugin.js';
|
|
20
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
20
21
|
|
|
21
22
|
// Icons (bubble menu only)
|
|
22
23
|
import Bold from '@tabler/icons-svelte/icons/bold';
|
|
@@ -43,6 +44,10 @@
|
|
|
43
44
|
|
|
44
45
|
let { value = $bindable(undefined), inlineBlocks = [] }: Props = $props();
|
|
45
46
|
|
|
47
|
+
// Capture parent contexts at init time (before onMount) so SvelteNodeViewRenderer
|
|
48
|
+
// can propagate them to inline block node views (needed for getRemotes() etc.)
|
|
49
|
+
const parentContexts = getAllContexts();
|
|
50
|
+
|
|
46
51
|
const hasInlineBlocks = $derived(inlineBlocks.length > 0);
|
|
47
52
|
|
|
48
53
|
let element = $state<HTMLDivElement | null>(null);
|
|
@@ -64,7 +69,7 @@
|
|
|
64
69
|
|
|
65
70
|
const extraExtensions = hasInlineBlocks
|
|
66
71
|
? [
|
|
67
|
-
InlineBlockNode.configure({ inlineBlocks }),
|
|
72
|
+
InlineBlockNode.configure({ inlineBlocks, context: parentContexts }),
|
|
68
73
|
SlashCommand.configure({ inlineBlocks })
|
|
69
74
|
]
|
|
70
75
|
: [];
|
|
@@ -77,6 +82,10 @@
|
|
|
77
82
|
HeadingA11yPlugin,
|
|
78
83
|
BubbleMenu.configure({
|
|
79
84
|
element: bubbleMenu!
|
|
85
|
+
}),
|
|
86
|
+
Placeholder.configure({
|
|
87
|
+
placeholder: 'Wpisz treść lub "/" by wstawić element',
|
|
88
|
+
showOnlyCurrent: true
|
|
80
89
|
})
|
|
81
90
|
],
|
|
82
91
|
content: initialContent,
|
|
@@ -271,6 +280,8 @@
|
|
|
271
280
|
|
|
272
281
|
<style>
|
|
273
282
|
.bubble-menu {
|
|
283
|
+
height: 0;
|
|
284
|
+
overflow: visible;
|
|
274
285
|
visibility: hidden;
|
|
275
286
|
opacity: 0;
|
|
276
287
|
transition:
|
|
@@ -3,6 +3,7 @@ import type { ObjectField } from '../../../types/fields.js';
|
|
|
3
3
|
export interface InlineBlockOptions {
|
|
4
4
|
HTMLAttributes: Record<string, string>;
|
|
5
5
|
inlineBlocks: ObjectField[];
|
|
6
|
+
context?: Map<unknown, unknown>;
|
|
6
7
|
}
|
|
7
8
|
declare module '@tiptap/core' {
|
|
8
9
|
interface Commands<ReturnType> {
|
|
@@ -17,6 +17,9 @@ function getFieldDefault(field) {
|
|
|
17
17
|
return field.defaultValue;
|
|
18
18
|
switch (field.type) {
|
|
19
19
|
case 'text':
|
|
20
|
+
case 'richtext':
|
|
21
|
+
case 'date':
|
|
22
|
+
case 'datetime':
|
|
20
23
|
return '';
|
|
21
24
|
case 'number':
|
|
22
25
|
return 0;
|
|
@@ -25,8 +28,20 @@ function getFieldDefault(field) {
|
|
|
25
28
|
case 'select':
|
|
26
29
|
case 'radio':
|
|
27
30
|
return field.options[0]?.value ?? '';
|
|
31
|
+
case 'checkboxes':
|
|
32
|
+
return [];
|
|
28
33
|
case 'url':
|
|
29
34
|
return { url: '' };
|
|
35
|
+
case 'image':
|
|
36
|
+
case 'file':
|
|
37
|
+
case 'media':
|
|
38
|
+
case 'relation':
|
|
39
|
+
return field.multiple ? [] : '';
|
|
40
|
+
case 'object':
|
|
41
|
+
return { slug: field.slug, data: {} };
|
|
42
|
+
case 'blocks':
|
|
43
|
+
case 'array':
|
|
44
|
+
return [];
|
|
30
45
|
default:
|
|
31
46
|
return '';
|
|
32
47
|
}
|
|
@@ -93,6 +108,8 @@ export const InlineBlockNode = Node.create({
|
|
|
93
108
|
};
|
|
94
109
|
},
|
|
95
110
|
addNodeView() {
|
|
96
|
-
return SvelteNodeViewRenderer(InlineBlockNodeView
|
|
111
|
+
return SvelteNodeViewRenderer(InlineBlockNodeView, {
|
|
112
|
+
context: this.options.context
|
|
113
|
+
});
|
|
97
114
|
}
|
|
98
115
|
});
|
|
@@ -104,8 +104,7 @@ export const SlashCommand = Extension.create({
|
|
|
104
104
|
if (!query)
|
|
105
105
|
return allItems;
|
|
106
106
|
const q = query.toLowerCase();
|
|
107
|
-
return allItems.filter((item) => item.label.toLowerCase().includes(q) ||
|
|
108
|
-
item.group.toLowerCase().includes(q));
|
|
107
|
+
return allItems.filter((item) => item.label.toLowerCase().includes(q) || item.group.toLowerCase().includes(q));
|
|
109
108
|
},
|
|
110
109
|
render: () => {
|
|
111
110
|
let popup;
|
|
@@ -167,7 +166,7 @@ export const SlashCommand = Extension.create({
|
|
|
167
166
|
return false;
|
|
168
167
|
},
|
|
169
168
|
onExit: () => {
|
|
170
|
-
popup?.destroy();
|
|
169
|
+
// popup?.destroy();
|
|
171
170
|
if (component)
|
|
172
171
|
unmount(component);
|
|
173
172
|
container = undefined;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SuperForm } from 'sveltekit-superforms';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a minimal SuperForm-compatible object for use outside SvelteKit form actions.
|
|
4
|
+
* Only implements the 4 stores that formFieldProxy and formsnap actually use:
|
|
5
|
+
* form, errors, constraints, tainted.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createStandaloneForm(data: Record<string, unknown>): SuperForm<Record<string, unknown>>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { writable, readable } from 'svelte/store';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a minimal SuperForm-compatible object for use outside SvelteKit form actions.
|
|
4
|
+
* Only implements the 4 stores that formFieldProxy and formsnap actually use:
|
|
5
|
+
* form, errors, constraints, tainted.
|
|
6
|
+
*/
|
|
7
|
+
export function createStandaloneForm(data) {
|
|
8
|
+
return {
|
|
9
|
+
form: writable(data),
|
|
10
|
+
errors: writable({}),
|
|
11
|
+
constraints: writable({}),
|
|
12
|
+
tainted: writable(undefined),
|
|
13
|
+
// Stubs required by SuperForm type — not used by formFieldProxy/formsnap
|
|
14
|
+
formId: writable('standalone'),
|
|
15
|
+
message: writable(undefined),
|
|
16
|
+
submitting: readable(false),
|
|
17
|
+
delayed: readable(false),
|
|
18
|
+
timeout: readable(false),
|
|
19
|
+
posted: readable(false),
|
|
20
|
+
allErrors: readable([]),
|
|
21
|
+
options: {},
|
|
22
|
+
enhance: (() => ({ destroy() { } })),
|
|
23
|
+
validate: (async () => undefined),
|
|
24
|
+
validateForm: (async () => ({ valid: true })),
|
|
25
|
+
isTainted: (() => false),
|
|
26
|
+
reset: (() => { }),
|
|
27
|
+
submit: (() => { }),
|
|
28
|
+
capture: (() => ({})),
|
|
29
|
+
restore: (() => { })
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Editor, type Editor as EditorType } from '@tiptap/core';
|
|
4
4
|
import BubbleMenu from '@tiptap/extension-bubble-menu';
|
|
5
5
|
import { extensions } from './extensions.js';
|
|
6
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
6
7
|
import ToolbarButton from './toolbar-button.svelte';
|
|
7
8
|
import EditorToolbar from './editor-toolbar.svelte';
|
|
8
9
|
import LinkDialog from './link-dialog.svelte';
|
|
@@ -55,6 +56,10 @@
|
|
|
55
56
|
...extensions,
|
|
56
57
|
BubbleMenu.configure({
|
|
57
58
|
element: bubbleMenu!
|
|
59
|
+
}),
|
|
60
|
+
Placeholder.configure({
|
|
61
|
+
placeholder: 'Wpisz treść...',
|
|
62
|
+
showOnlyCurrent: true
|
|
58
63
|
})
|
|
59
64
|
],
|
|
60
65
|
content: value || '',
|
|
@@ -246,6 +251,8 @@
|
|
|
246
251
|
|
|
247
252
|
<style>
|
|
248
253
|
.bubble-menu {
|
|
254
|
+
height: 0;
|
|
255
|
+
overflow: visible;
|
|
249
256
|
visibility: hidden;
|
|
250
257
|
opacity: 0;
|
|
251
258
|
transition:
|
|
@@ -199,6 +199,14 @@ export const getRecentEntries = query(z.number().default(6), async (limit) => {
|
|
|
199
199
|
label = String(Object.values(titleData)[0] || '');
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
|
+
if (config && config.type === 'single' && config.label) {
|
|
203
|
+
if (typeof config.label === 'object') {
|
|
204
|
+
label = String(Object.values(config.label)[0] || '');
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
label = config.label;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
202
210
|
let collectionLabel = { pl: entry.slug, en: entry.slug };
|
|
203
211
|
if (config) {
|
|
204
212
|
if (config.type === 'collection' && config.labels) {
|
|
@@ -237,6 +245,14 @@ export const getRecentActivity = query(z.number().default(10), async (limit) =>
|
|
|
237
245
|
label = String(Object.values(titleData)[0] || '');
|
|
238
246
|
}
|
|
239
247
|
}
|
|
248
|
+
if (config && config.type === 'single' && config.label) {
|
|
249
|
+
if (typeof config.label === 'object') {
|
|
250
|
+
label = String(Object.values(config.label)[0] || '');
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
label = config.label;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
240
256
|
activity.push({
|
|
241
257
|
type: 'entry_edit',
|
|
242
258
|
entryId: entry.id,
|
|
@@ -308,6 +308,16 @@ div[data-collapsible]:not([data-collapsible='icon'])
|
|
|
308
308
|
flex-shrink: 0;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
/* Placeholder — Notion-style */
|
|
312
|
+
.ProseMirror .is-empty.is-editor-empty::before,
|
|
313
|
+
.ProseMirror .has-focus.is-empty::before {
|
|
314
|
+
color: var(--text-light);
|
|
315
|
+
content: attr(data-placeholder);
|
|
316
|
+
float: left;
|
|
317
|
+
height: 0;
|
|
318
|
+
pointer-events: none;
|
|
319
|
+
}
|
|
320
|
+
|
|
311
321
|
.tiptap-highlight {
|
|
312
322
|
background-color: rgb(250 204 21 / 0.4);
|
|
313
323
|
padding: 0.125em 0.25em;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FieldCondition } from '../../types/fields.js';
|
|
2
|
+
/**
|
|
3
|
+
* Evaluate whether a field should be visible based on its showWhen condition.
|
|
4
|
+
* Returns true (visible) when no condition is set.
|
|
5
|
+
*/
|
|
6
|
+
export declare function evaluateCondition(condition: FieldCondition | undefined, getValue: (slug: string) => unknown): boolean;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evaluate whether a field should be visible based on its showWhen condition.
|
|
3
|
+
* Returns true (visible) when no condition is set.
|
|
4
|
+
*/
|
|
5
|
+
export function evaluateCondition(condition, getValue) {
|
|
6
|
+
if (!condition)
|
|
7
|
+
return true;
|
|
8
|
+
const value = getValue(condition.field);
|
|
9
|
+
if (condition.equals !== undefined) {
|
|
10
|
+
const allowed = Array.isArray(condition.equals) ? condition.equals : [condition.equals];
|
|
11
|
+
return allowed.includes(value);
|
|
12
|
+
}
|
|
13
|
+
if (condition.notEquals !== undefined) {
|
|
14
|
+
const disallowed = Array.isArray(condition.notEquals)
|
|
15
|
+
? condition.notEquals
|
|
16
|
+
: [condition.notEquals];
|
|
17
|
+
return !disallowed.includes(value);
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Switch as SwitchPrimitive } from "bits-ui";
|
|
3
|
+
import { cn, type WithoutChildrenOrChild } from "../../../utils.js";
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
ref = $bindable(null),
|
|
7
|
+
checked = $bindable(false),
|
|
8
|
+
class: className,
|
|
9
|
+
...restProps
|
|
10
|
+
}: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<SwitchPrimitive.Root
|
|
14
|
+
bind:ref
|
|
15
|
+
bind:checked
|
|
16
|
+
data-slot="switch"
|
|
17
|
+
class={cn(
|
|
18
|
+
"peer inline-flex h-[22px] w-[38px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-border",
|
|
19
|
+
className
|
|
20
|
+
)}
|
|
21
|
+
{...restProps}
|
|
22
|
+
>
|
|
23
|
+
<SwitchPrimitive.Thumb
|
|
24
|
+
class="pointer-events-none block size-[18px] rounded-full bg-white shadow-sm ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
|
25
|
+
/>
|
|
26
|
+
</SwitchPrimitive.Root>
|
|
@@ -139,7 +139,8 @@ export function generateZodSchemaFromField(field, languages, options = {
|
|
|
139
139
|
}
|
|
140
140
|
case 'object': {
|
|
141
141
|
const data = generateZodSchemaFromFields(field.fields, languages, {
|
|
142
|
-
parentRequired: field.required
|
|
142
|
+
parentRequired: field.required,
|
|
143
|
+
localized: options.localized
|
|
143
144
|
});
|
|
144
145
|
const finalData = !field.required ? data.partial().optional() : data;
|
|
145
146
|
return z.object({
|
|
@@ -148,7 +149,10 @@ export function generateZodSchemaFromField(field, languages, options = {
|
|
|
148
149
|
});
|
|
149
150
|
}
|
|
150
151
|
case 'blocks': {
|
|
151
|
-
const schemas = field.of.map((f) => generateZodSchemaFromField(f, languages, {
|
|
152
|
+
const schemas = field.of.map((f) => generateZodSchemaFromField(f, languages, {
|
|
153
|
+
parentRequired: field.required,
|
|
154
|
+
localized: options.localized
|
|
155
|
+
}));
|
|
152
156
|
const itemSchema = schemas.length > 1
|
|
153
157
|
? z.discriminatedUnion('slug', schemas)
|
|
154
158
|
: schemas[0];
|
|
@@ -283,6 +287,14 @@ export function generateZodSchemaFromField(field, languages, options = {
|
|
|
283
287
|
}
|
|
284
288
|
export function generateZodSchemaFromFields(fields, languages, options = {}) {
|
|
285
289
|
return z.object({
|
|
286
|
-
...Object.fromEntries(fields.map((f) =>
|
|
290
|
+
...Object.fromEntries(fields.map((f) => {
|
|
291
|
+
let schema = generateZodSchemaFromField(f, languages, options);
|
|
292
|
+
// Fields with showWhen are conditionally visible — make them optional
|
|
293
|
+
// so hidden fields don't block validation
|
|
294
|
+
if (f.showWhen) {
|
|
295
|
+
schema = schema.optional();
|
|
296
|
+
}
|
|
297
|
+
return [f.slug, schema];
|
|
298
|
+
}))
|
|
287
299
|
});
|
|
288
300
|
}
|