@wikex/admin-kit 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/admin/before-login.d.ts +5 -0
- package/dist/admin/before-login.js +26 -0
- package/dist/admin/brand.d.ts +19 -0
- package/dist/admin/brand.js +15 -0
- package/dist/admin/header.d.ts +5 -0
- package/dist/admin/header.js +87 -0
- package/dist/admin/logout-button.d.ts +8 -0
- package/dist/admin/logout-button.js +7 -0
- package/dist/admin/nav.d.ts +19 -0
- package/dist/admin/nav.js +285 -0
- package/dist/admin/theme-toggle.d.ts +5 -0
- package/dist/admin/theme-toggle.js +7 -0
- package/dist/admin/view-site.d.ts +5 -0
- package/dist/admin/view-site.js +6 -0
- package/dist/chunk-7FEACJUN.js +151 -0
- package/dist/chunk-BUD6BMFV.js +117 -0
- package/dist/chunk-GGSOO4FQ.js +27 -0
- package/dist/chunk-IL2CUCDG.js +2062 -0
- package/dist/chunk-JJLHSZ3T.js +40 -0
- package/dist/chunk-LC2CP5SR.js +13 -0
- package/dist/chunk-LP22Y3NO.js +61 -0
- package/dist/chunk-MDMKKWPU.js +12 -0
- package/dist/chunk-W4ZKDOWJ.js +37 -0
- package/dist/chunk-XGKGKCQ6.js +50 -0
- package/dist/chunk-YMBJCWN3.js +73 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +44 -0
- package/dist/live-preview.d.ts +50 -0
- package/dist/live-preview.js +19 -0
- package/dist/project.d.ts +34 -0
- package/dist/project.js +8 -0
- package/dist/rich-text-client.d.ts +5 -0
- package/dist/rich-text-client.js +165 -0
- package/dist/rich-text.d.ts +5 -0
- package/dist/rich-text.js +6 -0
- package/dist/runtimeConfig-BwjQepcW.d.ts +30 -0
- package/dist/starter-client.d.ts +15 -0
- package/dist/starter-client.js +25 -0
- package/dist/starter.d.ts +40 -0
- package/dist/starter.js +62 -0
- package/dist/text-style-runtime.d.ts +5 -0
- package/dist/text-style-runtime.js +8 -0
- package/dist/text-styles.d.ts +86 -0
- package/dist/text-styles.js +22 -0
- package/package.json +23 -19
- package/src/TextStyleRuntime.tsx +0 -90
- package/src/admin/BeforeLogin.tsx +0 -19
- package/src/admin/Brand.tsx +0 -74
- package/src/admin/Header.tsx +0 -87
- package/src/admin/LogoutButton.tsx +0 -41
- package/src/admin/Nav.tsx +0 -35
- package/src/admin/NavClient.tsx +0 -288
- package/src/admin/ThemeToggle.tsx +0 -31
- package/src/admin/ViewSite.tsx +0 -11
- package/src/admin/useAdminBrand.ts +0 -81
- package/src/adminExperience.ts +0 -117
- package/src/index.ts +0 -11
- package/src/live-preview/GlobalInlineInspector.tsx +0 -334
- package/src/live-preview/LivePreviewEditor.tsx +0 -2088
- package/src/live-preview/TextStyleInspector.tsx +0 -289
- package/src/live-preview/index.ts +0 -23
- package/src/live-preview/runtimeConfig.ts +0 -52
- package/src/plugin.ts +0 -70
- package/src/project.ts +0 -82
- package/src/rich-text/client.tsx +0 -202
- package/src/rich-text/index.ts +0 -10
- package/src/starter/client.tsx +0 -22
- package/src/starter.ts +0 -95
- package/src/textStyles.ts +0 -161
package/src/rich-text/client.tsx
DELETED
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
|
4
|
-
import { $forEachSelectedTextNode } from '@lexical/selection'
|
|
5
|
-
import type { ToolbarGroupItem } from '@payloadcms/richtext-lexical'
|
|
6
|
-
import { createClientFeature } from '@payloadcms/richtext-lexical/client'
|
|
7
|
-
import {
|
|
8
|
-
$getNodeByKey,
|
|
9
|
-
$getSelection,
|
|
10
|
-
$getState,
|
|
11
|
-
$isRangeSelection,
|
|
12
|
-
$isTextNode,
|
|
13
|
-
$setSelection,
|
|
14
|
-
$setState,
|
|
15
|
-
createState,
|
|
16
|
-
type LexicalEditor,
|
|
17
|
-
type RangeSelection,
|
|
18
|
-
TextNode,
|
|
19
|
-
} from 'lexical'
|
|
20
|
-
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
getTextStyleCSS,
|
|
24
|
-
normalizeTextStyleValue,
|
|
25
|
-
TEXT_FONT_OPTIONS,
|
|
26
|
-
TEXT_LINE_HEIGHT_OPTIONS,
|
|
27
|
-
type TextFontFamily,
|
|
28
|
-
type TextLineHeight,
|
|
29
|
-
} from '../textStyles'
|
|
30
|
-
|
|
31
|
-
const fontFamilyState = createState('fontFamily', {
|
|
32
|
-
parse: (value) => normalizeTextStyleValue({ fontFamily: value }).fontFamily,
|
|
33
|
-
})
|
|
34
|
-
const lineHeightState = createState('lineHeight', {
|
|
35
|
-
parse: (value) => normalizeTextStyleValue({ lineHeight: value }).lineHeight,
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
type TypographyValue = {
|
|
39
|
-
fontFamily: TextFontFamily
|
|
40
|
-
lineHeight: TextLineHeight
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const DEFAULT_TYPOGRAPHY: TypographyValue = {
|
|
44
|
-
fontFamily: 'default',
|
|
45
|
-
lineHeight: 'default',
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const readSelectedTypography = (editor: LexicalEditor): TypographyValue =>
|
|
49
|
-
editor.getEditorState().read(() => {
|
|
50
|
-
const selection = $getSelection()
|
|
51
|
-
if (!$isRangeSelection(selection)) return DEFAULT_TYPOGRAPHY
|
|
52
|
-
|
|
53
|
-
const textNode = selection.getNodes().find($isTextNode)
|
|
54
|
-
if (!textNode) return DEFAULT_TYPOGRAPHY
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
fontFamily: $getState(textNode, fontFamilyState) ?? 'default',
|
|
58
|
-
lineHeight: $getState(textNode, lineHeightState) ?? 'default',
|
|
59
|
-
}
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
const updateSelectedTypography = (
|
|
63
|
-
editor: LexicalEditor,
|
|
64
|
-
property: keyof TypographyValue,
|
|
65
|
-
value: TextFontFamily | TextLineHeight,
|
|
66
|
-
savedSelection: null | RangeSelection,
|
|
67
|
-
) => {
|
|
68
|
-
editor.update(() => {
|
|
69
|
-
if (!$isRangeSelection($getSelection()) && savedSelection) {
|
|
70
|
-
$setSelection(savedSelection.clone())
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
$forEachSelectedTextNode((textNode) => {
|
|
74
|
-
if (property === 'fontFamily') {
|
|
75
|
-
const fontFamily = value as TextFontFamily
|
|
76
|
-
$setState(textNode, fontFamilyState, fontFamily === 'default' ? undefined : fontFamily)
|
|
77
|
-
} else {
|
|
78
|
-
const lineHeight = value as TextLineHeight
|
|
79
|
-
$setState(textNode, lineHeightState, lineHeight === 'default' ? undefined : lineHeight)
|
|
80
|
-
}
|
|
81
|
-
})
|
|
82
|
-
})
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const TypographyToolbarItem = ({ editor }: { editor: LexicalEditor }) => {
|
|
86
|
-
const [value, setValue] = useState<TypographyValue>(DEFAULT_TYPOGRAPHY)
|
|
87
|
-
const savedSelection = useRef<null | RangeSelection>(null)
|
|
88
|
-
|
|
89
|
-
const captureSelection = useCallback(() => {
|
|
90
|
-
editor.getEditorState().read(() => {
|
|
91
|
-
const selection = $getSelection()
|
|
92
|
-
if ($isRangeSelection(selection)) savedSelection.current = selection.clone()
|
|
93
|
-
})
|
|
94
|
-
}, [editor])
|
|
95
|
-
|
|
96
|
-
useEffect(() => {
|
|
97
|
-
const syncValue = () => {
|
|
98
|
-
captureSelection()
|
|
99
|
-
setValue(readSelectedTypography(editor))
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
syncValue()
|
|
103
|
-
return editor.registerUpdateListener(syncValue)
|
|
104
|
-
}, [captureSelection, editor])
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<div
|
|
108
|
-
aria-label="Kiểu chữ"
|
|
109
|
-
className="wikex-typography-selects"
|
|
110
|
-
onMouseDown={(event) => {
|
|
111
|
-
event.stopPropagation()
|
|
112
|
-
captureSelection()
|
|
113
|
-
}}
|
|
114
|
-
>
|
|
115
|
-
<select
|
|
116
|
-
aria-label="Phông chữ"
|
|
117
|
-
className="wikex-minimal-select wikex-minimal-select--font"
|
|
118
|
-
onChange={(event) => {
|
|
119
|
-
const fontFamily = event.target.value as TextFontFamily
|
|
120
|
-
setValue((current) => ({ ...current, fontFamily }))
|
|
121
|
-
updateSelectedTypography(editor, 'fontFamily', fontFamily, savedSelection.current)
|
|
122
|
-
}}
|
|
123
|
-
title="Phông chữ"
|
|
124
|
-
value={value.fontFamily}
|
|
125
|
-
>
|
|
126
|
-
{TEXT_FONT_OPTIONS.map((option) => (
|
|
127
|
-
<option key={option.value} value={option.value}>
|
|
128
|
-
{option.label}
|
|
129
|
-
</option>
|
|
130
|
-
))}
|
|
131
|
-
</select>
|
|
132
|
-
<select
|
|
133
|
-
aria-label="Chiều cao dòng"
|
|
134
|
-
className="wikex-minimal-select wikex-minimal-select--line-height"
|
|
135
|
-
onChange={(event) => {
|
|
136
|
-
const lineHeight = event.target.value as TextLineHeight
|
|
137
|
-
setValue((current) => ({ ...current, lineHeight }))
|
|
138
|
-
updateSelectedTypography(editor, 'lineHeight', lineHeight, savedSelection.current)
|
|
139
|
-
}}
|
|
140
|
-
title="Chiều cao dòng"
|
|
141
|
-
value={value.lineHeight}
|
|
142
|
-
>
|
|
143
|
-
{TEXT_LINE_HEIGHT_OPTIONS.map((option) => (
|
|
144
|
-
<option key={option.value} value={option.value}>
|
|
145
|
-
{option.label}
|
|
146
|
-
</option>
|
|
147
|
-
))}
|
|
148
|
-
</select>
|
|
149
|
-
</div>
|
|
150
|
-
)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const TypographyStatePlugin = () => {
|
|
154
|
-
const [editor] = useLexicalComposerContext()
|
|
155
|
-
|
|
156
|
-
useEffect(
|
|
157
|
-
() =>
|
|
158
|
-
editor.registerMutationListener(TextNode, (mutatedNodes) => {
|
|
159
|
-
editor.getEditorState().read(() => {
|
|
160
|
-
for (const [nodeKey, mutation] of mutatedNodes) {
|
|
161
|
-
if (mutation === 'destroyed') continue
|
|
162
|
-
|
|
163
|
-
const node = $getNodeByKey(nodeKey)
|
|
164
|
-
const element = editor.getElementByKey(nodeKey)
|
|
165
|
-
if (!$isTextNode(node) || !element) continue
|
|
166
|
-
|
|
167
|
-
const style = getTextStyleCSS({
|
|
168
|
-
fontFamily: $getState(node, fontFamilyState),
|
|
169
|
-
lineHeight: $getState(node, lineHeightState),
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
if (style.fontFamily) element.style.fontFamily = style.fontFamily
|
|
173
|
-
else element.style.removeProperty('font-family')
|
|
174
|
-
|
|
175
|
-
if (style.lineHeight) element.style.lineHeight = style.lineHeight
|
|
176
|
-
else element.style.removeProperty('line-height')
|
|
177
|
-
}
|
|
178
|
-
})
|
|
179
|
-
}),
|
|
180
|
-
[editor],
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
return null
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const toolbarItem: ToolbarGroupItem = {
|
|
187
|
-
Component: TypographyToolbarItem,
|
|
188
|
-
key: 'wikex-typography-controls',
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const typographyToolbarGroup = {
|
|
192
|
-
items: [toolbarItem],
|
|
193
|
-
key: 'wikexTypography',
|
|
194
|
-
order: 34,
|
|
195
|
-
type: 'buttons' as const,
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export const RichTextTypographyFeatureClient = createClientFeature({
|
|
199
|
-
plugins: [{ Component: TypographyStatePlugin, position: 'normal' }],
|
|
200
|
-
toolbarFixed: { groups: [typographyToolbarGroup] },
|
|
201
|
-
toolbarInline: { groups: [typographyToolbarGroup] },
|
|
202
|
-
})
|
package/src/rich-text/index.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { createServerFeature } from '@payloadcms/richtext-lexical'
|
|
2
|
-
|
|
3
|
-
const TypographyFeature = createServerFeature({
|
|
4
|
-
feature: {
|
|
5
|
-
ClientFeature: '@wikex/admin-kit/rich-text/client#RichTextTypographyFeatureClient',
|
|
6
|
-
},
|
|
7
|
-
key: 'wikexTypography',
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
export const RichTextTypographyFeature = () => TypographyFeature()
|
package/src/starter/client.tsx
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { configureAdminBrand } from '../admin/Brand'
|
|
4
|
-
import LivePreviewEditor, { configureLivePreview } from '../live-preview'
|
|
5
|
-
import type { WikexStarter } from '../starter'
|
|
6
|
-
|
|
7
|
-
type LivePreviewConfig = WikexStarter['livePreview'] & {
|
|
8
|
-
getVisualEditorRoute: WikexStarter['getVisualEditorRoute']
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** Configure and return the shared client-side Live Preview provider for a Wikex starter. */
|
|
12
|
-
export const createWikexLivePreviewAdapter = ({
|
|
13
|
-
brand,
|
|
14
|
-
getVisualEditorRoute,
|
|
15
|
-
pageBlocks,
|
|
16
|
-
postBlocks,
|
|
17
|
-
}: LivePreviewConfig) => {
|
|
18
|
-
configureAdminBrand({ logo: { alt: brand.name, ...brand.logo }, siteName: brand.name })
|
|
19
|
-
configureLivePreview({ getVisualEditorRoute, pageBlocks, postBlocks })
|
|
20
|
-
|
|
21
|
-
return LivePreviewEditor
|
|
22
|
-
}
|
package/src/starter.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { createContentModule, type ContentModuleOptions } from '@wikex/content-kit'
|
|
2
|
-
import { adminExperiencePlugin } from './adminExperience'
|
|
3
|
-
import type { BlockPickerOption, VisualEditorRoute } from './live-preview/runtimeConfig'
|
|
4
|
-
import { createVisualEditorRouteResolver, defineWikexProject } from './project'
|
|
5
|
-
import { wikexAdminPlugin } from './plugin'
|
|
6
|
-
|
|
7
|
-
export type WikexStarterBrand = {
|
|
8
|
-
description: string
|
|
9
|
-
logo: {
|
|
10
|
-
height: number
|
|
11
|
-
url: string
|
|
12
|
-
width: number
|
|
13
|
-
}
|
|
14
|
-
name: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export type WikexStarterOptions = {
|
|
18
|
-
brand: WikexStarterBrand
|
|
19
|
-
content: ContentModuleOptions
|
|
20
|
-
globals?: Readonly<Record<string, 'content' | 'footer' | 'header'>>
|
|
21
|
-
id: string
|
|
22
|
-
modules?: readonly string[]
|
|
23
|
-
provider: string
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export type WikexStarter = {
|
|
27
|
-
collections: ReturnType<typeof createContentModule>['collections']
|
|
28
|
-
getVisualEditorRoute: (pathname: string) => VisualEditorRoute | null
|
|
29
|
-
livePreview: {
|
|
30
|
-
brand: WikexStarterBrand
|
|
31
|
-
pageBlocks: readonly BlockPickerOption[]
|
|
32
|
-
postBlocks: readonly BlockPickerOption[]
|
|
33
|
-
}
|
|
34
|
-
plugins: ReturnType<typeof createContentModule>['plugins']
|
|
35
|
-
project: ReturnType<typeof defineWikexProject>
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const getLabel = (block: { labels?: unknown; slug: string }) => {
|
|
39
|
-
if (typeof block.labels === 'object' && block.labels && 'singular' in block.labels) {
|
|
40
|
-
const singular = block.labels.singular
|
|
41
|
-
if (typeof singular === 'string') return singular
|
|
42
|
-
}
|
|
43
|
-
return block.slug
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const isStarterBlock = (block: unknown): block is { labels?: unknown; slug: string } =>
|
|
47
|
-
typeof block === 'object' && block !== null && 'slug' in block && typeof block.slug === 'string'
|
|
48
|
-
|
|
49
|
-
const toBlockPickerOptions = (blocks: unknown): readonly BlockPickerOption[] => {
|
|
50
|
-
if (!Array.isArray(blocks)) return []
|
|
51
|
-
return blocks.filter(isStarterBlock).map((block) => {
|
|
52
|
-
const label = getLabel(block)
|
|
53
|
-
return { description: label, label, payloadLabel: label, slug: block.slug }
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* One factory for the conventional Wikex Payload setup. The consuming project still owns
|
|
59
|
-
* its visual block components and frontend renderer, but no longer has to wire plugins,
|
|
60
|
-
* content schema, branding and preview routing independently.
|
|
61
|
-
*/
|
|
62
|
-
export const createWikexStarter = (options: WikexStarterOptions): WikexStarter => {
|
|
63
|
-
const seo = options.content.seo === undefined ? { siteName: options.brand.name } : options.content.seo
|
|
64
|
-
const content = createContentModule({ ...options.content, seo })
|
|
65
|
-
const collections = [content.pageSlug, content.postSlug].filter(
|
|
66
|
-
(slug): slug is string => Boolean(slug),
|
|
67
|
-
)
|
|
68
|
-
const getVisualEditorRoute = createVisualEditorRouteResolver({
|
|
69
|
-
collections,
|
|
70
|
-
globals: options.globals,
|
|
71
|
-
})
|
|
72
|
-
const project = defineWikexProject({
|
|
73
|
-
brand: options.brand,
|
|
74
|
-
id: options.id,
|
|
75
|
-
modules: options.modules ?? ['website', 'blog'],
|
|
76
|
-
})
|
|
77
|
-
const pageBlocks = toBlockPickerOptions(
|
|
78
|
-
options.content.page === false ? undefined : options.content.page?.blocks,
|
|
79
|
-
)
|
|
80
|
-
const postBlocks = toBlockPickerOptions(
|
|
81
|
-
options.content.post === false ? undefined : options.content.post?.blocks,
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
collections: content.collections,
|
|
86
|
-
getVisualEditorRoute,
|
|
87
|
-
livePreview: { brand: options.brand, pageBlocks, postBlocks },
|
|
88
|
-
plugins: [
|
|
89
|
-
adminExperiencePlugin,
|
|
90
|
-
...content.plugins,
|
|
91
|
-
wikexAdminPlugin({ components: { provider: options.provider } }),
|
|
92
|
-
],
|
|
93
|
-
project,
|
|
94
|
-
}
|
|
95
|
-
}
|
package/src/textStyles.ts
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
export const TEXT_FONT_OPTIONS = [
|
|
2
|
-
{ css: '', label: 'Mặc định', value: 'default' },
|
|
3
|
-
{ css: 'var(--font-manrope), sans-serif', label: 'Manrope', value: 'manrope' },
|
|
4
|
-
{ css: 'var(--font-inter), sans-serif', label: 'Inter', value: 'inter' },
|
|
5
|
-
{ css: 'var(--font-roboto), sans-serif', label: 'Roboto', value: 'roboto' },
|
|
6
|
-
{ css: 'var(--font-montserrat), sans-serif', label: 'Montserrat', value: 'montserrat' },
|
|
7
|
-
{ css: 'var(--font-lora), serif', label: 'Lora', value: 'lora' },
|
|
8
|
-
{
|
|
9
|
-
css: 'var(--font-playfair-display), serif',
|
|
10
|
-
label: 'Playfair Display',
|
|
11
|
-
value: 'playfair-display',
|
|
12
|
-
},
|
|
13
|
-
] as const
|
|
14
|
-
|
|
15
|
-
export const TEXT_LINE_HEIGHT_OPTIONS = [
|
|
16
|
-
{ css: '', label: 'Mặc định', value: 'default' },
|
|
17
|
-
{ css: '0.9', label: 'Rất chặt · 0.9', value: '0.9' },
|
|
18
|
-
{ css: '1', label: 'Chặt · 1.0', value: '1' },
|
|
19
|
-
{ css: '1.15', label: 'Gọn · 1.15', value: '1.15' },
|
|
20
|
-
{ css: '1.35', label: 'Tiêu chuẩn · 1.35', value: '1.35' },
|
|
21
|
-
{ css: '1.5', label: 'Thoáng · 1.5', value: '1.5' },
|
|
22
|
-
{ css: '1.7', label: 'Rộng · 1.7', value: '1.7' },
|
|
23
|
-
] as const
|
|
24
|
-
|
|
25
|
-
export type TextFontFamily = (typeof TEXT_FONT_OPTIONS)[number]['value']
|
|
26
|
-
export type TextLineHeight = (typeof TEXT_LINE_HEIGHT_OPTIONS)[number]['value']
|
|
27
|
-
|
|
28
|
-
export type TextStyleValue = {
|
|
29
|
-
fontFamily?: TextFontFamily
|
|
30
|
-
lineHeight?: TextLineHeight
|
|
31
|
-
/** Percentage of the containing block, written by the canvas resize handles. */
|
|
32
|
-
width?: number
|
|
33
|
-
/** Percentage offset from the containing block's left content edge. */
|
|
34
|
-
widthOffset?: number
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export type TextStyleMap = Record<string, TextStyleValue>
|
|
38
|
-
|
|
39
|
-
export const EDITABLE_INTERACTIVE_SELECTOR = [
|
|
40
|
-
'a',
|
|
41
|
-
'button',
|
|
42
|
-
'input:not([type="hidden"])',
|
|
43
|
-
'select',
|
|
44
|
-
'textarea',
|
|
45
|
-
'[role="button"]',
|
|
46
|
-
'[role="link"]',
|
|
47
|
-
'[tabindex]:not([tabindex="-1"])',
|
|
48
|
-
].join(',')
|
|
49
|
-
|
|
50
|
-
export const EDITABLE_PREVIEW_SELECTOR = [
|
|
51
|
-
'h1',
|
|
52
|
-
'h2',
|
|
53
|
-
'h3',
|
|
54
|
-
'h4',
|
|
55
|
-
'h5',
|
|
56
|
-
'h6',
|
|
57
|
-
'p',
|
|
58
|
-
'li',
|
|
59
|
-
'span',
|
|
60
|
-
'strong',
|
|
61
|
-
EDITABLE_INTERACTIVE_SELECTOR,
|
|
62
|
-
].join(',')
|
|
63
|
-
|
|
64
|
-
export const isSyntheticElementStyleKey = (value: string) => value.startsWith('@element:')
|
|
65
|
-
|
|
66
|
-
export const getEditableElementStyleKey = (element: HTMLElement) => {
|
|
67
|
-
const scopedRoot = element.closest<HTMLElement>('[data-wikex-scope]')
|
|
68
|
-
const root = scopedRoot ?? element.ownerDocument.body
|
|
69
|
-
const scope = scopedRoot?.dataset.wikexScope || 'body'
|
|
70
|
-
const segments: string[] = []
|
|
71
|
-
let current: HTMLElement | null = element
|
|
72
|
-
|
|
73
|
-
while (current && current !== root) {
|
|
74
|
-
const parentElement: HTMLElement | null = current.parentElement
|
|
75
|
-
if (!parentElement) break
|
|
76
|
-
|
|
77
|
-
const tagName = current.tagName.toLowerCase()
|
|
78
|
-
const sameTagSiblings = Array.from(parentElement.children).filter(
|
|
79
|
-
(sibling) => sibling.tagName === current?.tagName,
|
|
80
|
-
)
|
|
81
|
-
const index = sameTagSiblings.indexOf(current) + 1
|
|
82
|
-
segments.push(`${tagName}:nth-of-type(${Math.max(index, 1)})`)
|
|
83
|
-
current = parentElement
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return `@element:${scope}:${segments.reverse().join('>')}`
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const fontValues = new Set<string>(TEXT_FONT_OPTIONS.map((option) => option.value))
|
|
90
|
-
const lineHeightValues = new Set<string>(TEXT_LINE_HEIGHT_OPTIONS.map((option) => option.value))
|
|
91
|
-
|
|
92
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
93
|
-
Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
|
94
|
-
|
|
95
|
-
export const normalizeTextStyleValue = (value: unknown): TextStyleValue => {
|
|
96
|
-
if (!isRecord(value)) return {}
|
|
97
|
-
|
|
98
|
-
const fontFamily =
|
|
99
|
-
typeof value.fontFamily === 'string' && fontValues.has(value.fontFamily)
|
|
100
|
-
? (value.fontFamily as TextFontFamily)
|
|
101
|
-
: undefined
|
|
102
|
-
const lineHeight =
|
|
103
|
-
typeof value.lineHeight === 'string' && lineHeightValues.has(value.lineHeight)
|
|
104
|
-
? (value.lineHeight as TextLineHeight)
|
|
105
|
-
: undefined
|
|
106
|
-
const legacyWidth =
|
|
107
|
-
typeof value.width === 'string' && /^\d+(?:\.\d+)?%$/.test(value.width)
|
|
108
|
-
? Number.parseFloat(value.width)
|
|
109
|
-
: undefined
|
|
110
|
-
const widthValue = typeof value.width === 'number' ? value.width : legacyWidth
|
|
111
|
-
const width =
|
|
112
|
-
typeof widthValue === 'number' && Number.isFinite(widthValue)
|
|
113
|
-
? Math.min(100, Math.max(5, widthValue))
|
|
114
|
-
: undefined
|
|
115
|
-
const widthOffset =
|
|
116
|
-
typeof value.widthOffset === 'number' && Number.isFinite(value.widthOffset)
|
|
117
|
-
? Math.min(width ? 100 - width : 95, Math.max(0, value.widthOffset))
|
|
118
|
-
: undefined
|
|
119
|
-
|
|
120
|
-
return { fontFamily, lineHeight, width, widthOffset }
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export const normalizeTextStyleMap = (value: unknown): TextStyleMap => {
|
|
124
|
-
if (!isRecord(value)) return {}
|
|
125
|
-
|
|
126
|
-
return Object.fromEntries(
|
|
127
|
-
Object.entries(value).map(([fieldPath, fieldStyle]) => [
|
|
128
|
-
fieldPath,
|
|
129
|
-
normalizeTextStyleValue(fieldStyle),
|
|
130
|
-
]),
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export const getTextStyleCSS = (
|
|
135
|
-
value: TextStyleValue,
|
|
136
|
-
): {
|
|
137
|
-
boxSizing?: 'border-box'
|
|
138
|
-
fontFamily?: string
|
|
139
|
-
lineHeight?: string
|
|
140
|
-
marginLeft?: string
|
|
141
|
-
marginRight?: string
|
|
142
|
-
maxWidth?: string
|
|
143
|
-
width?: string
|
|
144
|
-
} => {
|
|
145
|
-
const fontFamily = TEXT_FONT_OPTIONS.find((option) => option.value === value.fontFamily)?.css
|
|
146
|
-
const lineHeight = TEXT_LINE_HEIGHT_OPTIONS.find(
|
|
147
|
-
(option) => option.value === value.lineHeight,
|
|
148
|
-
)?.css
|
|
149
|
-
const width = typeof value.width === 'number' ? `${value.width}%` : undefined
|
|
150
|
-
const widthOffset = typeof value.widthOffset === 'number' ? `${value.widthOffset}%` : undefined
|
|
151
|
-
|
|
152
|
-
return {
|
|
153
|
-
boxSizing: width ? 'border-box' : undefined,
|
|
154
|
-
fontFamily: fontFamily || undefined,
|
|
155
|
-
lineHeight: lineHeight || undefined,
|
|
156
|
-
marginLeft: width ? widthOffset || '0%' : undefined,
|
|
157
|
-
marginRight: width ? 'auto' : undefined,
|
|
158
|
-
maxWidth: width ? 'none' : undefined,
|
|
159
|
-
width,
|
|
160
|
-
}
|
|
161
|
-
}
|