@x33025/sveltely 0.1.10 → 0.1.12
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/dist/components/Library/ArticleEditor/ArticleBlockCode.svelte +21 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockCode.svelte.d.ts +8 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockDragControl.svelte +144 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockDragControl.svelte.d.ts +14 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockFAQ.svelte +47 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockFAQ.svelte.d.ts +8 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockFallback.svelte +79 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockFallback.svelte.d.ts +15 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockHeading.svelte +73 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockHeading.svelte.d.ts +14 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockImage.svelte +48 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockImage.svelte.d.ts +9 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockInsertControl.svelte +120 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockInsertControl.svelte.d.ts +9 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockList.svelte +114 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockList.svelte.d.ts +15 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockParagraph.svelte +79 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockParagraph.svelte.d.ts +15 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockShell.svelte +127 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockShell.svelte.d.ts +22 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockTable.svelte +274 -0
- package/dist/components/Library/ArticleEditor/ArticleBlockTable.svelte.d.ts +13 -0
- package/dist/components/Library/ArticleEditor/ArticleEditor.svelte +192 -0
- package/dist/components/Library/ArticleEditor/ArticleEditor.svelte.d.ts +39 -0
- package/dist/components/Library/ArticleEditor/ArticleEditorBody.svelte +328 -0
- package/dist/components/Library/ArticleEditor/ArticleEditorBody.svelte.d.ts +31 -0
- package/dist/components/Library/ArticleEditor/ArticleEditorHeader.svelte +57 -0
- package/dist/components/Library/ArticleEditor/ArticleEditorHeader.svelte.d.ts +11 -0
- package/dist/components/Library/ArticleEditor/ArticleImagePreview.svelte +71 -0
- package/dist/components/Library/ArticleEditor/ArticleImagePreview.svelte.d.ts +8 -0
- package/dist/components/Library/ArticleEditor/articleEditor.svelte.js +532 -0
- package/dist/components/Library/ArticleEditor/index.d.ts +18 -0
- package/dist/components/Library/ArticleEditor/index.js +16 -0
- package/dist/components/Library/ArticleEditor/types.d.ts +37 -0
- package/dist/components/Library/ArticleEditor/types.js +1 -0
- package/dist/components/Library/Floating/Floating.svelte +2 -1
- package/dist/components/Library/Grid/index.d.ts +1 -0
- package/dist/components/Library/Grid/index.js +1 -0
- package/dist/components/Library/HStack/HStack.svelte +12 -7
- package/dist/components/Library/Sheet/Sheet.svelte +1 -0
- package/dist/components/Library/TextEditor/TextEditor.svelte +94 -0
- package/dist/components/Library/TextEditor/TextEditor.svelte.d.ts +16 -0
- package/dist/components/Library/TextEditor/index.d.ts +1 -0
- package/dist/components/Library/TextEditor/index.js +1 -0
- package/dist/components/Library/TokenSearchField/TokenSearchField.svelte +2 -1
- package/dist/components/Library/VStack/VStack.svelte +12 -7
- package/dist/components/Local/ComponentGrid.svelte +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/style/index.css +1 -0
- package/dist/style.css +233 -0
- package/package.json +1 -1
- package/dist/components/Library/GridItem/index.d.ts +0 -1
- package/dist/components/Library/GridItem/index.js +0 -1
- /package/dist/components/Library/{GridItem → Grid}/GridItem.svelte +0 -0
- /package/dist/components/Library/{GridItem → Grid}/GridItem.svelte.d.ts +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import VStack from '../VStack';
|
|
3
|
+
import TextEditor from '../TextEditor';
|
|
4
|
+
import ArticleBlockFAQ from './ArticleBlockFAQ.svelte';
|
|
5
|
+
import ArticleEditorBody from './ArticleEditorBody.svelte';
|
|
6
|
+
import ArticleEditorHeader from './ArticleEditorHeader.svelte';
|
|
7
|
+
import type { BlockInsertKind, BlockTextFormat } from './articleEditor.svelte.js';
|
|
8
|
+
import type { ArticleEditorArticle, BlockDraft } from './types.js';
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
article,
|
|
12
|
+
draftTitle,
|
|
13
|
+
draftImageAltText,
|
|
14
|
+
blocks,
|
|
15
|
+
selectedBlockID,
|
|
16
|
+
onTitleChange,
|
|
17
|
+
onImageAltTextChange,
|
|
18
|
+
onSelectBlock,
|
|
19
|
+
onUpdateBlock,
|
|
20
|
+
onChangeBlockTextFormat,
|
|
21
|
+
onInsertBlockAfter,
|
|
22
|
+
onUpdateListItem,
|
|
23
|
+
onInsertListItemAfter,
|
|
24
|
+
onMergeListItemWithPrevious,
|
|
25
|
+
onRemoveListItem,
|
|
26
|
+
onCreateParagraphAfter,
|
|
27
|
+
onMergeBlockWithPrevious,
|
|
28
|
+
onRemoveBlock,
|
|
29
|
+
onConvertToList,
|
|
30
|
+
onUpdateFAQItem,
|
|
31
|
+
onUpdateTableCell,
|
|
32
|
+
onUpdateTableHeader,
|
|
33
|
+
onAddTableColumn,
|
|
34
|
+
onAddTableRow,
|
|
35
|
+
onRemoveTableColumn,
|
|
36
|
+
onRemoveTableRow,
|
|
37
|
+
onReorderBlock,
|
|
38
|
+
focusTarget,
|
|
39
|
+
focusPosition,
|
|
40
|
+
onFocusHandled,
|
|
41
|
+
onUndo,
|
|
42
|
+
onRedo
|
|
43
|
+
} = $props<{
|
|
44
|
+
article: ArticleEditorArticle;
|
|
45
|
+
draftTitle: string;
|
|
46
|
+
draftImageAltText: string;
|
|
47
|
+
blocks: BlockDraft[];
|
|
48
|
+
selectedBlockID: string | null;
|
|
49
|
+
onTitleChange: (value: string) => void;
|
|
50
|
+
onImageAltTextChange: (value: string) => void;
|
|
51
|
+
onSelectBlock: (id: string) => void;
|
|
52
|
+
onUpdateBlock: (id: string, patch: Partial<BlockDraft>) => void;
|
|
53
|
+
onChangeBlockTextFormat: (id: string, format: BlockTextFormat) => void;
|
|
54
|
+
onInsertBlockAfter: (id: string, kind: BlockInsertKind) => void;
|
|
55
|
+
onUpdateListItem: (blockID: string, index: number, value: string) => void;
|
|
56
|
+
onInsertListItemAfter: (
|
|
57
|
+
blockID: string,
|
|
58
|
+
index: number,
|
|
59
|
+
text?: string,
|
|
60
|
+
currentText?: string | null
|
|
61
|
+
) => void;
|
|
62
|
+
onMergeListItemWithPrevious: (blockID: string, index: number) => void;
|
|
63
|
+
onRemoveListItem: (blockID: string, index: number) => void;
|
|
64
|
+
onCreateParagraphAfter: (id: string, text?: string, currentText?: string | null) => void;
|
|
65
|
+
onMergeBlockWithPrevious: (id: string) => void;
|
|
66
|
+
onRemoveBlock: (id: string) => void;
|
|
67
|
+
onConvertToList: (id: string, kind: 'bullet_list' | 'numbered_list') => void;
|
|
68
|
+
onUpdateFAQItem: (
|
|
69
|
+
blockID: string,
|
|
70
|
+
index: number,
|
|
71
|
+
field: 'question' | 'answer',
|
|
72
|
+
value: string
|
|
73
|
+
) => void;
|
|
74
|
+
onUpdateTableCell: (
|
|
75
|
+
blockID: string,
|
|
76
|
+
rowIndex: number,
|
|
77
|
+
cellIndex: number,
|
|
78
|
+
value: string
|
|
79
|
+
) => void;
|
|
80
|
+
onUpdateTableHeader: (blockID: string, cellIndex: number, value: string) => void;
|
|
81
|
+
onAddTableColumn: (blockID: string) => void;
|
|
82
|
+
onAddTableRow: (blockID: string) => void;
|
|
83
|
+
onRemoveTableColumn: (blockID: string, columnIndex: number) => void;
|
|
84
|
+
onRemoveTableRow: (blockID: string, rowIndex: number) => void;
|
|
85
|
+
onReorderBlock: (draggedID: string, targetID: string, placement: 'before' | 'after') => void;
|
|
86
|
+
focusTarget: string | null;
|
|
87
|
+
focusPosition: number | null;
|
|
88
|
+
onFocusHandled: () => void;
|
|
89
|
+
onUndo: () => void;
|
|
90
|
+
onRedo: () => void;
|
|
91
|
+
}>();
|
|
92
|
+
|
|
93
|
+
const faqBlocks = $derived(blocks.filter((block: BlockDraft) => block.type === 'faq'));
|
|
94
|
+
|
|
95
|
+
const handleGlobalKeyDown = (event: KeyboardEvent) => {
|
|
96
|
+
const isUndoKey = event.key.toLowerCase() === 'z';
|
|
97
|
+
const isRedoKey = event.key.toLowerCase() === 'y';
|
|
98
|
+
const hasShortcutModifier = event.metaKey || event.ctrlKey;
|
|
99
|
+
|
|
100
|
+
if (!hasShortcutModifier || event.altKey) return;
|
|
101
|
+
|
|
102
|
+
if (isUndoKey && !event.shiftKey) {
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
onUndo();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if ((isUndoKey && event.shiftKey) || isRedoKey) {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
onRedo();
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<svelte:window onkeydown={handleGlobalKeyDown} />
|
|
116
|
+
|
|
117
|
+
<VStack
|
|
118
|
+
width="100%"
|
|
119
|
+
maxWidth="860px"
|
|
120
|
+
gap="2.5rem"
|
|
121
|
+
paddingX="3.5rem"
|
|
122
|
+
paddingY="2.5rem"
|
|
123
|
+
background="var(--sveltely-background-color)"
|
|
124
|
+
>
|
|
125
|
+
<ArticleEditorHeader
|
|
126
|
+
{article}
|
|
127
|
+
{draftTitle}
|
|
128
|
+
{draftImageAltText}
|
|
129
|
+
{onTitleChange}
|
|
130
|
+
{onImageAltTextChange}
|
|
131
|
+
/>
|
|
132
|
+
|
|
133
|
+
<ArticleEditorBody
|
|
134
|
+
{draftTitle}
|
|
135
|
+
{blocks}
|
|
136
|
+
{onSelectBlock}
|
|
137
|
+
{onUpdateBlock}
|
|
138
|
+
{onChangeBlockTextFormat}
|
|
139
|
+
{onInsertBlockAfter}
|
|
140
|
+
{onUpdateListItem}
|
|
141
|
+
{onInsertListItemAfter}
|
|
142
|
+
{onMergeListItemWithPrevious}
|
|
143
|
+
{onRemoveListItem}
|
|
144
|
+
{onCreateParagraphAfter}
|
|
145
|
+
{onMergeBlockWithPrevious}
|
|
146
|
+
{onRemoveBlock}
|
|
147
|
+
{onConvertToList}
|
|
148
|
+
{onUpdateTableCell}
|
|
149
|
+
{onUpdateTableHeader}
|
|
150
|
+
{onAddTableColumn}
|
|
151
|
+
{onAddTableRow}
|
|
152
|
+
{onRemoveTableColumn}
|
|
153
|
+
{onRemoveTableRow}
|
|
154
|
+
{onReorderBlock}
|
|
155
|
+
{focusTarget}
|
|
156
|
+
{focusPosition}
|
|
157
|
+
{onFocusHandled}
|
|
158
|
+
/>
|
|
159
|
+
|
|
160
|
+
{#if faqBlocks.length > 0}
|
|
161
|
+
<section class="article-editor-faq-section">
|
|
162
|
+
<VStack gap="1.25rem">
|
|
163
|
+
{#each faqBlocks as block (block.id)}
|
|
164
|
+
<TextEditor
|
|
165
|
+
autosize
|
|
166
|
+
value={block.text ?? 'Frequently asked questions'}
|
|
167
|
+
rows={1}
|
|
168
|
+
onInput={(event) =>
|
|
169
|
+
onUpdateBlock(block.id, {
|
|
170
|
+
text: (event.currentTarget as HTMLTextAreaElement).value
|
|
171
|
+
})}
|
|
172
|
+
className="text-2xl leading-tight font-semibold tracking-[-0.025em] text-zinc-950 placeholder:text-zinc-300"
|
|
173
|
+
placeholder="FAQ title"
|
|
174
|
+
/>
|
|
175
|
+
<ArticleBlockFAQ {block} onUpdateItem={onUpdateFAQItem} />
|
|
176
|
+
{/each}
|
|
177
|
+
</VStack>
|
|
178
|
+
</section>
|
|
179
|
+
{/if}
|
|
180
|
+
</VStack>
|
|
181
|
+
|
|
182
|
+
<style>
|
|
183
|
+
.article-editor-faq-section {
|
|
184
|
+
display: flex;
|
|
185
|
+
min-width: 0;
|
|
186
|
+
min-height: 0;
|
|
187
|
+
flex-direction: column;
|
|
188
|
+
gap: 1.25rem;
|
|
189
|
+
border-top: 1px solid color-mix(in oklab, var(--sveltely-border-color) 70%, transparent);
|
|
190
|
+
padding-block: 2.5rem;
|
|
191
|
+
}
|
|
192
|
+
</style>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { BlockInsertKind, BlockTextFormat } from './articleEditor.svelte.js';
|
|
2
|
+
import type { ArticleEditorArticle, BlockDraft } from './types.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
article: ArticleEditorArticle;
|
|
5
|
+
draftTitle: string;
|
|
6
|
+
draftImageAltText: string;
|
|
7
|
+
blocks: BlockDraft[];
|
|
8
|
+
selectedBlockID: string | null;
|
|
9
|
+
onTitleChange: (value: string) => void;
|
|
10
|
+
onImageAltTextChange: (value: string) => void;
|
|
11
|
+
onSelectBlock: (id: string) => void;
|
|
12
|
+
onUpdateBlock: (id: string, patch: Partial<BlockDraft>) => void;
|
|
13
|
+
onChangeBlockTextFormat: (id: string, format: BlockTextFormat) => void;
|
|
14
|
+
onInsertBlockAfter: (id: string, kind: BlockInsertKind) => void;
|
|
15
|
+
onUpdateListItem: (blockID: string, index: number, value: string) => void;
|
|
16
|
+
onInsertListItemAfter: (blockID: string, index: number, text?: string, currentText?: string | null) => void;
|
|
17
|
+
onMergeListItemWithPrevious: (blockID: string, index: number) => void;
|
|
18
|
+
onRemoveListItem: (blockID: string, index: number) => void;
|
|
19
|
+
onCreateParagraphAfter: (id: string, text?: string, currentText?: string | null) => void;
|
|
20
|
+
onMergeBlockWithPrevious: (id: string) => void;
|
|
21
|
+
onRemoveBlock: (id: string) => void;
|
|
22
|
+
onConvertToList: (id: string, kind: 'bullet_list' | 'numbered_list') => void;
|
|
23
|
+
onUpdateFAQItem: (blockID: string, index: number, field: 'question' | 'answer', value: string) => void;
|
|
24
|
+
onUpdateTableCell: (blockID: string, rowIndex: number, cellIndex: number, value: string) => void;
|
|
25
|
+
onUpdateTableHeader: (blockID: string, cellIndex: number, value: string) => void;
|
|
26
|
+
onAddTableColumn: (blockID: string) => void;
|
|
27
|
+
onAddTableRow: (blockID: string) => void;
|
|
28
|
+
onRemoveTableColumn: (blockID: string, columnIndex: number) => void;
|
|
29
|
+
onRemoveTableRow: (blockID: string, rowIndex: number) => void;
|
|
30
|
+
onReorderBlock: (draggedID: string, targetID: string, placement: 'before' | 'after') => void;
|
|
31
|
+
focusTarget: string | null;
|
|
32
|
+
focusPosition: number | null;
|
|
33
|
+
onFocusHandled: () => void;
|
|
34
|
+
onUndo: () => void;
|
|
35
|
+
onRedo: () => void;
|
|
36
|
+
};
|
|
37
|
+
declare const ArticleEditor: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
38
|
+
type ArticleEditor = ReturnType<typeof ArticleEditor>;
|
|
39
|
+
export default ArticleEditor;
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import VStack from '../VStack';
|
|
3
|
+
import type { BlockInsertKind, BlockTextFormat } from './articleEditor.svelte.js';
|
|
4
|
+
import type { BlockDraft } from './types.js';
|
|
5
|
+
import ArticleBlockCode from './ArticleBlockCode.svelte';
|
|
6
|
+
import ArticleBlockFallback from './ArticleBlockFallback.svelte';
|
|
7
|
+
import ArticleBlockHeading from './ArticleBlockHeading.svelte';
|
|
8
|
+
import ArticleBlockImage from './ArticleBlockImage.svelte';
|
|
9
|
+
import ArticleBlockList from './ArticleBlockList.svelte';
|
|
10
|
+
import ArticleBlockParagraph from './ArticleBlockParagraph.svelte';
|
|
11
|
+
import ArticleBlockShell from './ArticleBlockShell.svelte';
|
|
12
|
+
import ArticleBlockTable from './ArticleBlockTable.svelte';
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
draftTitle,
|
|
16
|
+
blocks,
|
|
17
|
+
onSelectBlock,
|
|
18
|
+
onUpdateBlock,
|
|
19
|
+
onChangeBlockTextFormat,
|
|
20
|
+
onInsertBlockAfter,
|
|
21
|
+
onUpdateListItem,
|
|
22
|
+
onInsertListItemAfter,
|
|
23
|
+
onMergeListItemWithPrevious,
|
|
24
|
+
onRemoveListItem,
|
|
25
|
+
onCreateParagraphAfter,
|
|
26
|
+
onMergeBlockWithPrevious,
|
|
27
|
+
onRemoveBlock,
|
|
28
|
+
onConvertToList,
|
|
29
|
+
onUpdateTableCell,
|
|
30
|
+
onUpdateTableHeader,
|
|
31
|
+
onAddTableColumn,
|
|
32
|
+
onAddTableRow,
|
|
33
|
+
onRemoveTableColumn,
|
|
34
|
+
onRemoveTableRow,
|
|
35
|
+
onReorderBlock,
|
|
36
|
+
focusTarget,
|
|
37
|
+
focusPosition,
|
|
38
|
+
onFocusHandled
|
|
39
|
+
} = $props<{
|
|
40
|
+
draftTitle: string;
|
|
41
|
+
blocks: BlockDraft[];
|
|
42
|
+
onSelectBlock: (id: string) => void;
|
|
43
|
+
onUpdateBlock: (id: string, patch: Partial<BlockDraft>) => void;
|
|
44
|
+
onChangeBlockTextFormat: (id: string, format: BlockTextFormat) => void;
|
|
45
|
+
onInsertBlockAfter: (id: string, kind: BlockInsertKind) => void;
|
|
46
|
+
onUpdateListItem: (blockID: string, index: number, value: string) => void;
|
|
47
|
+
onInsertListItemAfter: (
|
|
48
|
+
blockID: string,
|
|
49
|
+
index: number,
|
|
50
|
+
text?: string,
|
|
51
|
+
currentText?: string | null
|
|
52
|
+
) => void;
|
|
53
|
+
onMergeListItemWithPrevious: (blockID: string, index: number) => void;
|
|
54
|
+
onRemoveListItem: (blockID: string, index: number) => void;
|
|
55
|
+
onCreateParagraphAfter: (id: string, text?: string, currentText?: string | null) => void;
|
|
56
|
+
onMergeBlockWithPrevious: (id: string) => void;
|
|
57
|
+
onRemoveBlock: (id: string) => void;
|
|
58
|
+
onConvertToList: (id: string, kind: 'bullet_list' | 'numbered_list') => void;
|
|
59
|
+
onUpdateTableCell: (
|
|
60
|
+
blockID: string,
|
|
61
|
+
rowIndex: number,
|
|
62
|
+
cellIndex: number,
|
|
63
|
+
value: string
|
|
64
|
+
) => void;
|
|
65
|
+
onUpdateTableHeader: (blockID: string, cellIndex: number, value: string) => void;
|
|
66
|
+
onAddTableColumn: (blockID: string) => void;
|
|
67
|
+
onAddTableRow: (blockID: string) => void;
|
|
68
|
+
onRemoveTableColumn: (blockID: string, columnIndex: number) => void;
|
|
69
|
+
onRemoveTableRow: (blockID: string, rowIndex: number) => void;
|
|
70
|
+
onReorderBlock: (draggedID: string, targetID: string, placement: 'before' | 'after') => void;
|
|
71
|
+
focusTarget: string | null;
|
|
72
|
+
focusPosition: number | null;
|
|
73
|
+
onFocusHandled: () => void;
|
|
74
|
+
}>();
|
|
75
|
+
|
|
76
|
+
let draggedBlockID = $state<string | null>(null);
|
|
77
|
+
let dropTarget = $state<{ id: string; placement: 'before' | 'after' } | null>(null);
|
|
78
|
+
let autoScrollFrame: number | null = null;
|
|
79
|
+
let autoScrollVelocity = 0;
|
|
80
|
+
let autoScrollTarget: HTMLElement | Window | null = null;
|
|
81
|
+
|
|
82
|
+
const articleBlocks = $derived(blocks.filter((block: BlockDraft) => block.type !== 'faq'));
|
|
83
|
+
|
|
84
|
+
const blockTextFormat = (block: BlockDraft): BlockTextFormat | null => {
|
|
85
|
+
if (block.type === 'heading') {
|
|
86
|
+
const level = Math.min(3, Math.max(1, Number(block.level ?? 2)));
|
|
87
|
+
if (level === 1) return null;
|
|
88
|
+
return `heading-${level}` as BlockTextFormat;
|
|
89
|
+
}
|
|
90
|
+
if (block.type === 'paragraph' || block.type === 'code') return 'paragraph';
|
|
91
|
+
if (block.type === 'numbered_list') return 'numbered_list';
|
|
92
|
+
if (block.type === 'bullet_list' || block.type === 'list') return 'bullet_list';
|
|
93
|
+
return null;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const canChangeBlockTextFormat = (block: BlockDraft) => blockTextFormat(block) !== null;
|
|
97
|
+
|
|
98
|
+
const updateDropTarget = (event: DragEvent, id: string) => {
|
|
99
|
+
if (!draggedBlockID || draggedBlockID === id) {
|
|
100
|
+
dropTarget = null;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
105
|
+
dropTarget = {
|
|
106
|
+
id,
|
|
107
|
+
placement: event.clientY < rect.top + rect.height / 2 ? 'before' : 'after'
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const stopAutoScroll = () => {
|
|
112
|
+
autoScrollVelocity = 0;
|
|
113
|
+
if (autoScrollFrame === null) return;
|
|
114
|
+
cancelAnimationFrame(autoScrollFrame);
|
|
115
|
+
autoScrollFrame = null;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const runAutoScroll = () => {
|
|
119
|
+
if (!autoScrollVelocity) {
|
|
120
|
+
autoScrollFrame = null;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const target = autoScrollTarget ?? window;
|
|
125
|
+
if (target instanceof Window) {
|
|
126
|
+
target.scrollBy(0, autoScrollVelocity);
|
|
127
|
+
} else {
|
|
128
|
+
target.scrollTop += autoScrollVelocity;
|
|
129
|
+
}
|
|
130
|
+
autoScrollFrame = requestAnimationFrame(runAutoScroll);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const scrollTargetFor = (event: DragEvent): HTMLElement | Window =>
|
|
134
|
+
((event.target as HTMLElement | null)?.closest?.(
|
|
135
|
+
'.scroll-view, .navigation-stack-content'
|
|
136
|
+
) as HTMLElement | null) ?? window;
|
|
137
|
+
|
|
138
|
+
const updateAutoScroll = (event: DragEvent) => {
|
|
139
|
+
if (!draggedBlockID) {
|
|
140
|
+
stopAutoScroll();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
autoScrollTarget = scrollTargetFor(event);
|
|
145
|
+
const scrollRect =
|
|
146
|
+
autoScrollTarget instanceof Window
|
|
147
|
+
? { top: 0, bottom: window.innerHeight, height: window.innerHeight }
|
|
148
|
+
: autoScrollTarget.getBoundingClientRect();
|
|
149
|
+
const edgeSize = Math.min(160, scrollRect.height / 3);
|
|
150
|
+
const maxSpeed = 22;
|
|
151
|
+
const topDistance = event.clientY - scrollRect.top;
|
|
152
|
+
const bottomDistance = scrollRect.bottom - event.clientY;
|
|
153
|
+
|
|
154
|
+
if (topDistance < edgeSize) {
|
|
155
|
+
autoScrollVelocity = -Math.ceil(((edgeSize - topDistance) / edgeSize) * maxSpeed);
|
|
156
|
+
} else if (bottomDistance < edgeSize) {
|
|
157
|
+
autoScrollVelocity = Math.ceil(((edgeSize - bottomDistance) / edgeSize) * maxSpeed);
|
|
158
|
+
} else {
|
|
159
|
+
stopAutoScroll();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (autoScrollFrame === null) {
|
|
164
|
+
autoScrollFrame = requestAnimationFrame(runAutoScroll);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const dropPlacementFor = (id: string) => {
|
|
169
|
+
const currentDropTarget = dropTarget;
|
|
170
|
+
return currentDropTarget?.id === id ? currentDropTarget.placement : null;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const handleBlockDragOver = (event: DragEvent, id: string) => {
|
|
174
|
+
if (!draggedBlockID) return;
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
event.dataTransfer!.dropEffect = 'move';
|
|
177
|
+
updateAutoScroll(event);
|
|
178
|
+
updateDropTarget(event, id);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const handleBlockDragLeave = (event: DragEvent) => {
|
|
182
|
+
if (!(event.currentTarget as HTMLElement).contains(event.relatedTarget as Node | null)) {
|
|
183
|
+
dropTarget = null;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const handleBlockDrop = (event: DragEvent, id: string) => {
|
|
188
|
+
event.preventDefault();
|
|
189
|
+
const draggedID = event.dataTransfer?.getData('text/plain') || draggedBlockID;
|
|
190
|
+
const currentDropTarget = dropTarget;
|
|
191
|
+
if (draggedID && currentDropTarget && currentDropTarget.id === id) {
|
|
192
|
+
onReorderBlock(draggedID, id, currentDropTarget.placement);
|
|
193
|
+
}
|
|
194
|
+
draggedBlockID = null;
|
|
195
|
+
dropTarget = null;
|
|
196
|
+
stopAutoScroll();
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const handleBlockDragStart = (event: DragEvent, id: string) => {
|
|
200
|
+
draggedBlockID = id;
|
|
201
|
+
dropTarget = null;
|
|
202
|
+
event.dataTransfer!.effectAllowed = 'move';
|
|
203
|
+
event.dataTransfer!.setData('text/plain', id);
|
|
204
|
+
const row = (event.currentTarget as HTMLElement).closest<HTMLElement>('[data-block-row]');
|
|
205
|
+
if (row) {
|
|
206
|
+
const rect = row.getBoundingClientRect();
|
|
207
|
+
event.dataTransfer!.setDragImage(row, event.clientX - rect.left, event.clientY - rect.top);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const handleBlockDragEnd = () => {
|
|
212
|
+
draggedBlockID = null;
|
|
213
|
+
dropTarget = null;
|
|
214
|
+
stopAutoScroll();
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
$effect(() => {
|
|
218
|
+
const handleWindowDragOver = (event: DragEvent) => {
|
|
219
|
+
if (!draggedBlockID) return;
|
|
220
|
+
updateAutoScroll(event);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const handleWindowDrop = () => {
|
|
224
|
+
draggedBlockID = null;
|
|
225
|
+
dropTarget = null;
|
|
226
|
+
stopAutoScroll();
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
window.addEventListener('dragover', handleWindowDragOver);
|
|
230
|
+
window.addEventListener('drop', handleWindowDrop);
|
|
231
|
+
|
|
232
|
+
return () => {
|
|
233
|
+
window.removeEventListener('dragover', handleWindowDragOver);
|
|
234
|
+
window.removeEventListener('drop', handleWindowDrop);
|
|
235
|
+
stopAutoScroll();
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
</script>
|
|
239
|
+
|
|
240
|
+
<VStack gap="0.5rem" paddingX="0" paddingY="0">
|
|
241
|
+
{#each articleBlocks as block (block.id)}
|
|
242
|
+
{@const dropPlacement = dropPlacementFor(block.id)}
|
|
243
|
+
{@const textFormat = blockTextFormat(block)}
|
|
244
|
+
{@const canChangeTextFormat = canChangeBlockTextFormat(block)}
|
|
245
|
+
<ArticleBlockShell
|
|
246
|
+
{block}
|
|
247
|
+
{draggedBlockID}
|
|
248
|
+
{dropPlacement}
|
|
249
|
+
{textFormat}
|
|
250
|
+
{canChangeTextFormat}
|
|
251
|
+
onSelect={onSelectBlock}
|
|
252
|
+
onChangeTextFormat={onChangeBlockTextFormat}
|
|
253
|
+
{onInsertBlockAfter}
|
|
254
|
+
onDragOver={handleBlockDragOver}
|
|
255
|
+
onDragLeave={handleBlockDragLeave}
|
|
256
|
+
onDrop={handleBlockDrop}
|
|
257
|
+
onDragStart={handleBlockDragStart}
|
|
258
|
+
onDragEnd={handleBlockDragEnd}
|
|
259
|
+
>
|
|
260
|
+
{#snippet children()}
|
|
261
|
+
{#if block.type === 'heading'}
|
|
262
|
+
<ArticleBlockHeading
|
|
263
|
+
{block}
|
|
264
|
+
onUpdate={onUpdateBlock}
|
|
265
|
+
{onCreateParagraphAfter}
|
|
266
|
+
{onMergeBlockWithPrevious}
|
|
267
|
+
{onRemoveBlock}
|
|
268
|
+
shouldFocus={focusTarget === block.id}
|
|
269
|
+
focusPosition={focusTarget === block.id ? focusPosition : null}
|
|
270
|
+
onFocused={onFocusHandled}
|
|
271
|
+
/>
|
|
272
|
+
{:else if block.type === 'paragraph'}
|
|
273
|
+
<ArticleBlockParagraph
|
|
274
|
+
{block}
|
|
275
|
+
onUpdate={onUpdateBlock}
|
|
276
|
+
{onCreateParagraphAfter}
|
|
277
|
+
{onMergeBlockWithPrevious}
|
|
278
|
+
{onRemoveBlock}
|
|
279
|
+
{onConvertToList}
|
|
280
|
+
shouldFocus={focusTarget === block.id}
|
|
281
|
+
focusPosition={focusTarget === block.id ? focusPosition : null}
|
|
282
|
+
onFocused={onFocusHandled}
|
|
283
|
+
/>
|
|
284
|
+
{:else if block.type === 'bullet_list' || block.type === 'list' || block.type === 'numbered_list'}
|
|
285
|
+
<ArticleBlockList
|
|
286
|
+
{block}
|
|
287
|
+
onUpdateItem={onUpdateListItem}
|
|
288
|
+
onInsertItemAfter={onInsertListItemAfter}
|
|
289
|
+
{onMergeListItemWithPrevious}
|
|
290
|
+
{onCreateParagraphAfter}
|
|
291
|
+
{onRemoveListItem}
|
|
292
|
+
focusItemIndex={focusTarget?.startsWith(`${block.id}:`)
|
|
293
|
+
? Number(focusTarget.split(':')[1])
|
|
294
|
+
: null}
|
|
295
|
+
focusPosition={focusTarget?.startsWith(`${block.id}:`) ? focusPosition : null}
|
|
296
|
+
onFocused={onFocusHandled}
|
|
297
|
+
/>
|
|
298
|
+
{:else if block.type === 'image'}
|
|
299
|
+
<ArticleBlockImage {block} titleFallback={draftTitle} onUpdate={onUpdateBlock} />
|
|
300
|
+
{:else if block.type === 'table'}
|
|
301
|
+
<ArticleBlockTable
|
|
302
|
+
{block}
|
|
303
|
+
onUpdateHeader={onUpdateTableHeader}
|
|
304
|
+
onUpdateCell={onUpdateTableCell}
|
|
305
|
+
onAddColumn={onAddTableColumn}
|
|
306
|
+
onAddRow={onAddTableRow}
|
|
307
|
+
onRemoveColumn={onRemoveTableColumn}
|
|
308
|
+
onRemoveRow={onRemoveTableRow}
|
|
309
|
+
/>
|
|
310
|
+
{:else if block.type === 'code'}
|
|
311
|
+
<ArticleBlockCode {block} onUpdate={onUpdateBlock} />
|
|
312
|
+
{:else}
|
|
313
|
+
<ArticleBlockFallback
|
|
314
|
+
{block}
|
|
315
|
+
onUpdate={onUpdateBlock}
|
|
316
|
+
{onCreateParagraphAfter}
|
|
317
|
+
{onMergeBlockWithPrevious}
|
|
318
|
+
{onRemoveBlock}
|
|
319
|
+
{onConvertToList}
|
|
320
|
+
shouldFocus={focusTarget === block.id}
|
|
321
|
+
focusPosition={focusTarget === block.id ? focusPosition : null}
|
|
322
|
+
onFocused={onFocusHandled}
|
|
323
|
+
/>
|
|
324
|
+
{/if}
|
|
325
|
+
{/snippet}
|
|
326
|
+
</ArticleBlockShell>
|
|
327
|
+
{/each}
|
|
328
|
+
</VStack>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { BlockInsertKind, BlockTextFormat } from './articleEditor.svelte.js';
|
|
2
|
+
import type { BlockDraft } from './types.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
draftTitle: string;
|
|
5
|
+
blocks: BlockDraft[];
|
|
6
|
+
onSelectBlock: (id: string) => void;
|
|
7
|
+
onUpdateBlock: (id: string, patch: Partial<BlockDraft>) => void;
|
|
8
|
+
onChangeBlockTextFormat: (id: string, format: BlockTextFormat) => void;
|
|
9
|
+
onInsertBlockAfter: (id: string, kind: BlockInsertKind) => void;
|
|
10
|
+
onUpdateListItem: (blockID: string, index: number, value: string) => void;
|
|
11
|
+
onInsertListItemAfter: (blockID: string, index: number, text?: string, currentText?: string | null) => void;
|
|
12
|
+
onMergeListItemWithPrevious: (blockID: string, index: number) => void;
|
|
13
|
+
onRemoveListItem: (blockID: string, index: number) => void;
|
|
14
|
+
onCreateParagraphAfter: (id: string, text?: string, currentText?: string | null) => void;
|
|
15
|
+
onMergeBlockWithPrevious: (id: string) => void;
|
|
16
|
+
onRemoveBlock: (id: string) => void;
|
|
17
|
+
onConvertToList: (id: string, kind: 'bullet_list' | 'numbered_list') => void;
|
|
18
|
+
onUpdateTableCell: (blockID: string, rowIndex: number, cellIndex: number, value: string) => void;
|
|
19
|
+
onUpdateTableHeader: (blockID: string, cellIndex: number, value: string) => void;
|
|
20
|
+
onAddTableColumn: (blockID: string) => void;
|
|
21
|
+
onAddTableRow: (blockID: string) => void;
|
|
22
|
+
onRemoveTableColumn: (blockID: string, columnIndex: number) => void;
|
|
23
|
+
onRemoveTableRow: (blockID: string, rowIndex: number) => void;
|
|
24
|
+
onReorderBlock: (draggedID: string, targetID: string, placement: 'before' | 'after') => void;
|
|
25
|
+
focusTarget: string | null;
|
|
26
|
+
focusPosition: number | null;
|
|
27
|
+
onFocusHandled: () => void;
|
|
28
|
+
};
|
|
29
|
+
declare const ArticleEditorBody: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
30
|
+
type ArticleEditorBody = ReturnType<typeof ArticleEditorBody>;
|
|
31
|
+
export default ArticleEditorBody;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import VStack from '../VStack';
|
|
3
|
+
import ArticleImagePreview from './ArticleImagePreview.svelte';
|
|
4
|
+
import TextEditor from '../TextEditor';
|
|
5
|
+
import type { ArticleEditorArticle } from './types.js';
|
|
6
|
+
|
|
7
|
+
let { article, draftTitle, draftImageAltText, onTitleChange, onImageAltTextChange } = $props<{
|
|
8
|
+
article: ArticleEditorArticle;
|
|
9
|
+
draftTitle: string;
|
|
10
|
+
draftImageAltText: string;
|
|
11
|
+
onTitleChange: (value: string) => void;
|
|
12
|
+
onImageAltTextChange: (value: string) => void;
|
|
13
|
+
}>();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<VStack gap="2rem" paddingX="0" paddingY="2rem">
|
|
17
|
+
<TextEditor
|
|
18
|
+
autosize
|
|
19
|
+
value={draftTitle}
|
|
20
|
+
rows={1}
|
|
21
|
+
spellcheck={true}
|
|
22
|
+
onInput={(event) => onTitleChange((event.currentTarget as HTMLTextAreaElement).value)}
|
|
23
|
+
className="text-4xl leading-tight font-semibold tracking-[-0.03em] text-zinc-950 placeholder:text-zinc-300"
|
|
24
|
+
placeholder="Untitled article"
|
|
25
|
+
/>
|
|
26
|
+
|
|
27
|
+
{#if article.imageURL}
|
|
28
|
+
<ArticleImagePreview
|
|
29
|
+
src={article.imageURL}
|
|
30
|
+
alt={draftImageAltText || article.title}
|
|
31
|
+
aspectRatio="4 / 3"
|
|
32
|
+
/>
|
|
33
|
+
<TextEditor
|
|
34
|
+
autosize
|
|
35
|
+
value={draftImageAltText}
|
|
36
|
+
rows={1}
|
|
37
|
+
onInput={(event) => onImageAltTextChange((event.currentTarget as HTMLTextAreaElement).value)}
|
|
38
|
+
className="article-editor-hero-description"
|
|
39
|
+
placeholder="Image description"
|
|
40
|
+
/>
|
|
41
|
+
{/if}
|
|
42
|
+
</VStack>
|
|
43
|
+
|
|
44
|
+
<style>
|
|
45
|
+
:global(.article-editor-hero-description) {
|
|
46
|
+
border: 0;
|
|
47
|
+
background: transparent;
|
|
48
|
+
padding: 0;
|
|
49
|
+
color: #3f3f46;
|
|
50
|
+
font-size: 0.875rem;
|
|
51
|
+
line-height: 1.5rem;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
:global(.article-editor-hero-description::placeholder) {
|
|
55
|
+
color: #d4d4d8;
|
|
56
|
+
}
|
|
57
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ArticleEditorArticle } from './types.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
article: ArticleEditorArticle;
|
|
4
|
+
draftTitle: string;
|
|
5
|
+
draftImageAltText: string;
|
|
6
|
+
onTitleChange: (value: string) => void;
|
|
7
|
+
onImageAltTextChange: (value: string) => void;
|
|
8
|
+
};
|
|
9
|
+
declare const ArticleEditorHeader: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
|
+
type ArticleEditorHeader = ReturnType<typeof ArticleEditorHeader>;
|
|
11
|
+
export default ArticleEditorHeader;
|