@nuasite/cms 0.1.0
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/README.md +237 -0
- package/dist/src/build-processor.d.ts +20 -0
- package/dist/src/build-processor.d.ts.map +1 -0
- package/dist/src/collection-scanner.d.ts +6 -0
- package/dist/src/collection-scanner.d.ts.map +1 -0
- package/dist/src/component-registry.d.ts +63 -0
- package/dist/src/component-registry.d.ts.map +1 -0
- package/dist/src/config.d.ts +24 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/dev-middleware.d.ts +20 -0
- package/dist/src/dev-middleware.d.ts.map +1 -0
- package/dist/src/editor/ai.d.ts +60 -0
- package/dist/src/editor/ai.d.ts.map +1 -0
- package/dist/src/editor/api.d.ts +140 -0
- package/dist/src/editor/api.d.ts.map +1 -0
- package/dist/src/editor/color-utils.d.ts +106 -0
- package/dist/src/editor/color-utils.d.ts.map +1 -0
- package/dist/src/editor/components/ai-chat.d.ts +11 -0
- package/dist/src/editor/components/ai-chat.d.ts.map +1 -0
- package/dist/src/editor/components/ai-tooltip.d.ts +12 -0
- package/dist/src/editor/components/ai-tooltip.d.ts.map +1 -0
- package/dist/src/editor/components/attribute-editor.d.ts +5 -0
- package/dist/src/editor/components/attribute-editor.d.ts.map +1 -0
- package/dist/src/editor/components/block-editor.d.ts +12 -0
- package/dist/src/editor/components/block-editor.d.ts.map +1 -0
- package/dist/src/editor/components/collections-browser.d.ts +2 -0
- package/dist/src/editor/components/collections-browser.d.ts.map +1 -0
- package/dist/src/editor/components/color-toolbar.d.ts +12 -0
- package/dist/src/editor/components/color-toolbar.d.ts.map +1 -0
- package/dist/src/editor/components/confirm-dialog.d.ts +2 -0
- package/dist/src/editor/components/confirm-dialog.d.ts.map +1 -0
- package/dist/src/editor/components/create-page-modal.d.ts +2 -0
- package/dist/src/editor/components/create-page-modal.d.ts.map +1 -0
- package/dist/src/editor/components/editable-highlights.d.ts +9 -0
- package/dist/src/editor/components/editable-highlights.d.ts.map +1 -0
- package/dist/src/editor/components/error-boundary.d.ts +32 -0
- package/dist/src/editor/components/error-boundary.d.ts.map +1 -0
- package/dist/src/editor/components/fields.d.ts +75 -0
- package/dist/src/editor/components/fields.d.ts.map +1 -0
- package/dist/src/editor/components/frontmatter-fields.d.ts +29 -0
- package/dist/src/editor/components/frontmatter-fields.d.ts.map +1 -0
- package/dist/src/editor/components/highlight-overlay.d.ts +64 -0
- package/dist/src/editor/components/highlight-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/image-overlay.d.ts +12 -0
- package/dist/src/editor/components/image-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/markdown-editor-overlay.d.ts +6 -0
- package/dist/src/editor/components/markdown-editor-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/markdown-inline-editor.d.ts +10 -0
- package/dist/src/editor/components/markdown-inline-editor.d.ts.map +1 -0
- package/dist/src/editor/components/media-library.d.ts +2 -0
- package/dist/src/editor/components/media-library.d.ts.map +1 -0
- package/dist/src/editor/components/outline.d.ts +21 -0
- package/dist/src/editor/components/outline.d.ts.map +1 -0
- package/dist/src/editor/components/redirect-countdown.d.ts +2 -0
- package/dist/src/editor/components/redirect-countdown.d.ts.map +1 -0
- package/dist/src/editor/components/seo-editor.d.ts +2 -0
- package/dist/src/editor/components/seo-editor.d.ts.map +1 -0
- package/dist/src/editor/components/text-style-toolbar.d.ts +8 -0
- package/dist/src/editor/components/text-style-toolbar.d.ts.map +1 -0
- package/dist/src/editor/components/toast/toast-container.d.ts +7 -0
- package/dist/src/editor/components/toast/toast-container.d.ts.map +1 -0
- package/dist/src/editor/components/toast/toast.d.ts +7 -0
- package/dist/src/editor/components/toast/toast.d.ts.map +1 -0
- package/dist/src/editor/components/toast/types.d.ts +7 -0
- package/dist/src/editor/components/toast/types.d.ts.map +1 -0
- package/dist/src/editor/components/toolbar.d.ts +21 -0
- package/dist/src/editor/components/toolbar.d.ts.map +1 -0
- package/dist/src/editor/config.d.ts +4 -0
- package/dist/src/editor/config.d.ts.map +1 -0
- package/dist/src/editor/constants.d.ts +101 -0
- package/dist/src/editor/constants.d.ts.map +1 -0
- package/dist/src/editor/context.d.ts +14 -0
- package/dist/src/editor/context.d.ts.map +1 -0
- package/dist/src/editor/dom.d.ts +77 -0
- package/dist/src/editor/dom.d.ts.map +1 -0
- package/dist/src/editor/editor.d.ts +64 -0
- package/dist/src/editor/editor.d.ts.map +1 -0
- package/dist/src/editor/history.d.ts +20 -0
- package/dist/src/editor/history.d.ts.map +1 -0
- package/dist/src/editor/hooks/index.d.ts +14 -0
- package/dist/src/editor/hooks/index.d.ts.map +1 -0
- package/dist/src/editor/hooks/useAIHandlers.d.ts +22 -0
- package/dist/src/editor/hooks/useAIHandlers.d.ts.map +1 -0
- package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts +18 -0
- package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts.map +1 -0
- package/dist/src/editor/hooks/useElementDetection.d.ts +26 -0
- package/dist/src/editor/hooks/useElementDetection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useImageHoverDetection.d.ts +12 -0
- package/dist/src/editor/hooks/useImageHoverDetection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useTextSelection.d.ts +23 -0
- package/dist/src/editor/hooks/useTextSelection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useTooltipState.d.ts +19 -0
- package/dist/src/editor/hooks/useTooltipState.d.ts.map +1 -0
- package/dist/src/editor/hooks/utils.d.ts +32 -0
- package/dist/src/editor/hooks/utils.d.ts.map +1 -0
- package/dist/src/editor/index.d.ts +12 -0
- package/dist/src/editor/index.d.ts.map +1 -0
- package/dist/src/editor/lib/cn.d.ts +3 -0
- package/dist/src/editor/lib/cn.d.ts.map +1 -0
- package/dist/src/editor/manifest.d.ts +19 -0
- package/dist/src/editor/manifest.d.ts.map +1 -0
- package/dist/src/editor/markdown-api.d.ts +36 -0
- package/dist/src/editor/markdown-api.d.ts.map +1 -0
- package/dist/src/editor/signals.d.ts +242 -0
- package/dist/src/editor/signals.d.ts.map +1 -0
- package/dist/src/editor/storage.d.ts +27 -0
- package/dist/src/editor/storage.d.ts.map +1 -0
- package/dist/src/editor/text-styling.d.ts +350 -0
- package/dist/src/editor/text-styling.d.ts.map +1 -0
- package/dist/src/editor/themes.d.ts +38 -0
- package/dist/src/editor/themes.d.ts.map +1 -0
- package/dist/src/editor/types.d.ts +454 -0
- package/dist/src/editor/types.d.ts.map +1 -0
- package/dist/src/error-collector.d.ts +56 -0
- package/dist/src/error-collector.d.ts.map +1 -0
- package/dist/src/handlers/component-ops.d.ts +34 -0
- package/dist/src/handlers/component-ops.d.ts.map +1 -0
- package/dist/src/handlers/markdown-ops.d.ts +41 -0
- package/dist/src/handlers/markdown-ops.d.ts.map +1 -0
- package/dist/src/handlers/request-utils.d.ts +20 -0
- package/dist/src/handlers/request-utils.d.ts.map +1 -0
- package/dist/src/handlers/source-writer.d.ts +51 -0
- package/dist/src/handlers/source-writer.d.ts.map +1 -0
- package/dist/src/html-processor.d.ts +63 -0
- package/dist/src/html-processor.d.ts.map +1 -0
- package/dist/src/index.d.ts +41 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/manifest-writer.d.ts +111 -0
- package/dist/src/manifest-writer.d.ts.map +1 -0
- package/dist/src/media/contember.d.ts +15 -0
- package/dist/src/media/contember.d.ts.map +1 -0
- package/dist/src/media/local.d.ts +9 -0
- package/dist/src/media/local.d.ts.map +1 -0
- package/dist/src/media/s3.d.ts +12 -0
- package/dist/src/media/s3.d.ts.map +1 -0
- package/dist/src/media/types.d.ts +40 -0
- package/dist/src/media/types.d.ts.map +1 -0
- package/dist/src/preview-generator.d.ts +19 -0
- package/dist/src/preview-generator.d.ts.map +1 -0
- package/dist/src/seo-processor.d.ts +23 -0
- package/dist/src/seo-processor.d.ts.map +1 -0
- package/dist/src/source-finder/ast-extractors.d.ts +35 -0
- package/dist/src/source-finder/ast-extractors.d.ts.map +1 -0
- package/dist/src/source-finder/ast-parser.d.ts +16 -0
- package/dist/src/source-finder/ast-parser.d.ts.map +1 -0
- package/dist/src/source-finder/cache.d.ts +18 -0
- package/dist/src/source-finder/cache.d.ts.map +1 -0
- package/dist/src/source-finder/collection-finder.d.ts +29 -0
- package/dist/src/source-finder/collection-finder.d.ts.map +1 -0
- package/dist/src/source-finder/cross-file-tracker.d.ts +39 -0
- package/dist/src/source-finder/cross-file-tracker.d.ts.map +1 -0
- package/dist/src/source-finder/element-finder.d.ts +42 -0
- package/dist/src/source-finder/element-finder.d.ts.map +1 -0
- package/dist/src/source-finder/image-finder.d.ts +24 -0
- package/dist/src/source-finder/image-finder.d.ts.map +1 -0
- package/dist/src/source-finder/index.d.ts +9 -0
- package/dist/src/source-finder/index.d.ts.map +1 -0
- package/dist/src/source-finder/search-index.d.ts +27 -0
- package/dist/src/source-finder/search-index.d.ts.map +1 -0
- package/dist/src/source-finder/snippet-utils.d.ts +90 -0
- package/dist/src/source-finder/snippet-utils.d.ts.map +1 -0
- package/dist/src/source-finder/source-lookup.d.ts +16 -0
- package/dist/src/source-finder/source-lookup.d.ts.map +1 -0
- package/dist/src/source-finder/types.d.ts +167 -0
- package/dist/src/source-finder/types.d.ts.map +1 -0
- package/dist/src/source-finder/variable-extraction.d.ts +37 -0
- package/dist/src/source-finder/variable-extraction.d.ts.map +1 -0
- package/dist/src/tailwind-colors.d.ts +54 -0
- package/dist/src/tailwind-colors.d.ts.map +1 -0
- package/dist/src/tsconfig.tsbuildinfo +1 -0
- package/dist/src/types.d.ts +367 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/utils.d.ts +61 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/vite-plugin.d.ts +14 -0
- package/dist/src/vite-plugin.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +80 -0
- package/src/build-processor.ts +784 -0
- package/src/collection-scanner.ts +304 -0
- package/src/component-registry.ts +393 -0
- package/src/config.ts +74 -0
- package/src/dev-middleware.ts +525 -0
- package/src/dist/src/tsconfig.tsbuildinfo +1 -0
- package/src/editor/ai.ts +185 -0
- package/src/editor/api.ts +513 -0
- package/src/editor/color-utils.ts +556 -0
- package/src/editor/components/ai-chat.tsx +632 -0
- package/src/editor/components/ai-tooltip.tsx +179 -0
- package/src/editor/components/attribute-editor.tsx +596 -0
- package/src/editor/components/block-editor.tsx +546 -0
- package/src/editor/components/collections-browser.tsx +248 -0
- package/src/editor/components/color-toolbar.tsx +314 -0
- package/src/editor/components/confirm-dialog.tsx +69 -0
- package/src/editor/components/create-page-modal.tsx +163 -0
- package/src/editor/components/editable-highlights.tsx +260 -0
- package/src/editor/components/error-boundary.tsx +87 -0
- package/src/editor/components/fields.tsx +387 -0
- package/src/editor/components/frontmatter-fields.tsx +469 -0
- package/src/editor/components/highlight-overlay.ts +229 -0
- package/src/editor/components/image-overlay.tsx +230 -0
- package/src/editor/components/markdown-editor-overlay.tsx +505 -0
- package/src/editor/components/markdown-inline-editor.tsx +780 -0
- package/src/editor/components/media-library.tsx +297 -0
- package/src/editor/components/outline.tsx +402 -0
- package/src/editor/components/redirect-countdown.tsx +45 -0
- package/src/editor/components/seo-editor.tsx +498 -0
- package/src/editor/components/text-style-toolbar.tsx +362 -0
- package/src/editor/components/toast/toast-container.tsx +15 -0
- package/src/editor/components/toast/toast.tsx +49 -0
- package/src/editor/components/toast/types.ts +7 -0
- package/src/editor/components/toolbar.tsx +366 -0
- package/src/editor/config.ts +12 -0
- package/src/editor/constants.ts +106 -0
- package/src/editor/context.tsx +38 -0
- package/src/editor/dom.ts +357 -0
- package/src/editor/editor.ts +1510 -0
- package/src/editor/env.d.ts +4 -0
- package/src/editor/history.ts +355 -0
- package/src/editor/hooks/index.ts +19 -0
- package/src/editor/hooks/useAIHandlers.ts +345 -0
- package/src/editor/hooks/useBlockEditorHandlers.ts +206 -0
- package/src/editor/hooks/useElementDetection.ts +284 -0
- package/src/editor/hooks/useImageHoverDetection.ts +102 -0
- package/src/editor/hooks/useTextSelection.ts +187 -0
- package/src/editor/hooks/useTooltipState.ts +126 -0
- package/src/editor/hooks/utils.ts +101 -0
- package/src/editor/index.tsx +481 -0
- package/src/editor/lib/cn.ts +4 -0
- package/src/editor/manifest.ts +25 -0
- package/src/editor/markdown-api.ts +209 -0
- package/src/editor/signals.ts +1351 -0
- package/src/editor/storage.ts +266 -0
- package/src/editor/styles.css +465 -0
- package/src/editor/text-styling.ts +773 -0
- package/src/editor/themes.ts +210 -0
- package/src/editor/types.ts +591 -0
- package/src/error-collector.ts +106 -0
- package/src/handlers/component-ops.ts +463 -0
- package/src/handlers/markdown-ops.ts +202 -0
- package/src/handlers/request-utils.ts +151 -0
- package/src/handlers/source-writer.ts +649 -0
- package/src/html-processor.ts +1108 -0
- package/src/index.ts +284 -0
- package/src/manifest-writer.ts +371 -0
- package/src/media/contember.ts +84 -0
- package/src/media/local.ts +114 -0
- package/src/media/s3.ts +133 -0
- package/src/media/types.ts +33 -0
- package/src/preview-generator.ts +293 -0
- package/src/seo-processor.ts +567 -0
- package/src/source-finder/ast-extractors.ts +185 -0
- package/src/source-finder/ast-parser.ts +150 -0
- package/src/source-finder/cache.ts +76 -0
- package/src/source-finder/collection-finder.ts +335 -0
- package/src/source-finder/cross-file-tracker.ts +741 -0
- package/src/source-finder/element-finder.ts +387 -0
- package/src/source-finder/image-finder.ts +283 -0
- package/src/source-finder/index.ts +37 -0
- package/src/source-finder/search-index.ts +525 -0
- package/src/source-finder/snippet-utils.ts +668 -0
- package/src/source-finder/source-lookup.ts +200 -0
- package/src/source-finder/types.ts +210 -0
- package/src/source-finder/variable-extraction.ts +406 -0
- package/src/tailwind-colors.ts +874 -0
- package/src/tsconfig.json +25 -0
- package/src/types.ts +406 -0
- package/src/utils.ts +186 -0
- package/src/vite-plugin.ts +42 -0
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text styling utilities for applying Tailwind classes to text selections.
|
|
3
|
+
* Supports inline styling of partial text content within CMS elements.
|
|
4
|
+
* Uses inline styles for immediate visual feedback.
|
|
5
|
+
* CSS values can be overridden by AvailableTextStyles from the manifest.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AvailableTextStyles, TextStyleValue as ManifestTextStyleValue } from './types'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Tailwind text style categories with their class mappings and CSS values.
|
|
12
|
+
* These are fallback defaults when manifest styles are not available.
|
|
13
|
+
*/
|
|
14
|
+
export const TAILWIND_STYLES = {
|
|
15
|
+
weight: {
|
|
16
|
+
normal: { class: 'font-normal', label: 'Normal', css: { fontWeight: '400' } },
|
|
17
|
+
medium: { class: 'font-medium', label: 'Medium', css: { fontWeight: '500' } },
|
|
18
|
+
semibold: { class: 'font-semibold', label: 'Semibold', css: { fontWeight: '600' } },
|
|
19
|
+
bold: { class: 'font-bold', label: 'Bold', css: { fontWeight: '700' } },
|
|
20
|
+
},
|
|
21
|
+
decoration: {
|
|
22
|
+
none: { class: 'no-underline', label: 'None', css: { textDecoration: 'none' } },
|
|
23
|
+
underline: { class: 'underline', label: 'Underline', css: { textDecoration: 'underline' } },
|
|
24
|
+
lineThrough: { class: 'line-through', label: 'Strikethrough', css: { textDecoration: 'line-through' } },
|
|
25
|
+
},
|
|
26
|
+
style: {
|
|
27
|
+
normal: { class: 'not-italic', label: 'Normal', css: { fontStyle: 'normal' } },
|
|
28
|
+
italic: { class: 'italic', label: 'Italic', css: { fontStyle: 'italic' } },
|
|
29
|
+
},
|
|
30
|
+
color: {
|
|
31
|
+
inherit: { class: 'text-inherit', label: 'Inherit', css: { color: 'inherit' } },
|
|
32
|
+
slate: { class: 'text-slate-700', label: 'Slate', css: { color: '#334155' } },
|
|
33
|
+
gray: { class: 'text-gray-700', label: 'Gray', css: { color: '#374151' } },
|
|
34
|
+
red: { class: 'text-red-600', label: 'Red', css: { color: '#dc2626' } },
|
|
35
|
+
orange: { class: 'text-orange-600', label: 'Orange', css: { color: '#ea580c' } },
|
|
36
|
+
amber: { class: 'text-amber-600', label: 'Amber', css: { color: '#d97706' } },
|
|
37
|
+
green: { class: 'text-green-600', label: 'Green', css: { color: '#16a34a' } },
|
|
38
|
+
blue: { class: 'text-blue-600', label: 'Blue', css: { color: '#2563eb' } },
|
|
39
|
+
purple: { class: 'text-purple-600', label: 'Purple', css: { color: '#9333ea' } },
|
|
40
|
+
},
|
|
41
|
+
highlight: {
|
|
42
|
+
none: { class: '', label: 'None', css: { backgroundColor: 'transparent' } },
|
|
43
|
+
yellow: { class: 'bg-yellow-200', label: 'Yellow', css: { backgroundColor: '#fef08a' } },
|
|
44
|
+
green: { class: 'bg-green-200', label: 'Green', css: { backgroundColor: '#bbf7d0' } },
|
|
45
|
+
blue: { class: 'bg-blue-200', label: 'Blue', css: { backgroundColor: '#bfdbfe' } },
|
|
46
|
+
pink: { class: 'bg-pink-200', label: 'Pink', css: { backgroundColor: '#fbcfe8' } },
|
|
47
|
+
},
|
|
48
|
+
size: {
|
|
49
|
+
xs: { class: 'text-xs', label: 'XS', css: { fontSize: '0.75rem', lineHeight: '1rem' } },
|
|
50
|
+
sm: { class: 'text-sm', label: 'SM', css: { fontSize: '0.875rem', lineHeight: '1.25rem' } },
|
|
51
|
+
base: { class: 'text-base', label: 'Base', css: { fontSize: '1rem', lineHeight: '1.5rem' } },
|
|
52
|
+
lg: { class: 'text-lg', label: 'LG', css: { fontSize: '1.125rem', lineHeight: '1.75rem' } },
|
|
53
|
+
xl: { class: 'text-xl', label: 'XL', css: { fontSize: '1.25rem', lineHeight: '1.75rem' } },
|
|
54
|
+
'2xl': { class: 'text-2xl', label: '2XL', css: { fontSize: '1.5rem', lineHeight: '2rem' } },
|
|
55
|
+
},
|
|
56
|
+
} as const
|
|
57
|
+
|
|
58
|
+
export type StyleCategory = keyof typeof TAILWIND_STYLES
|
|
59
|
+
export type StyleValue<C extends StyleCategory> = keyof (typeof TAILWIND_STYLES)[C]
|
|
60
|
+
|
|
61
|
+
export interface TextStyle {
|
|
62
|
+
weight?: StyleValue<'weight'>
|
|
63
|
+
decoration?: StyleValue<'decoration'>
|
|
64
|
+
style?: StyleValue<'style'>
|
|
65
|
+
color?: StyleValue<'color'>
|
|
66
|
+
highlight?: StyleValue<'highlight'>
|
|
67
|
+
size?: StyleValue<'size'>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface TextSelection {
|
|
71
|
+
startOffset: number
|
|
72
|
+
endOffset: number
|
|
73
|
+
text: string
|
|
74
|
+
range: Range
|
|
75
|
+
anchorNode: Node
|
|
76
|
+
focusNode: Node
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Default values for each style category (no visual styling) */
|
|
80
|
+
const DEFAULT_VALUES: Record<StyleCategory, string> = {
|
|
81
|
+
weight: 'normal',
|
|
82
|
+
decoration: 'none',
|
|
83
|
+
style: 'normal',
|
|
84
|
+
color: 'inherit',
|
|
85
|
+
highlight: 'none',
|
|
86
|
+
size: 'base',
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Pre-computed reverse lookup: class name -> { category, key } */
|
|
90
|
+
const CLASS_TO_STYLE_MAP = new Map<string, { category: StyleCategory; key: string }>()
|
|
91
|
+
|
|
92
|
+
// Build the reverse lookup map once at module load
|
|
93
|
+
for (const [category, values] of Object.entries(TAILWIND_STYLES)) {
|
|
94
|
+
for (const [key, config] of Object.entries(values)) {
|
|
95
|
+
if (config.class) {
|
|
96
|
+
CLASS_TO_STYLE_MAP.set(config.class, { category: category as StyleCategory, key })
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Set of all known styling classes for quick lookup */
|
|
102
|
+
const KNOWN_STYLE_CLASSES = new Set(CLASS_TO_STYLE_MAP.keys())
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get the current text selection within a CMS element
|
|
106
|
+
*/
|
|
107
|
+
export function getTextSelection(cmsElement: HTMLElement): TextSelection | null {
|
|
108
|
+
const selection = window.getSelection()
|
|
109
|
+
if (!selection || selection.isCollapsed || selection.rangeCount === 0) {
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const range = selection.getRangeAt(0)
|
|
114
|
+
|
|
115
|
+
// Check if selection is within the CMS element
|
|
116
|
+
if (!cmsElement.contains(range.commonAncestorContainer)) {
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const text = selection.toString()
|
|
121
|
+
if (!text.trim()) {
|
|
122
|
+
return null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const anchorNode = selection.anchorNode
|
|
126
|
+
const focusNode = selection.focusNode
|
|
127
|
+
if (!anchorNode || !focusNode) {
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
startOffset: range.startOffset,
|
|
133
|
+
endOffset: range.endOffset,
|
|
134
|
+
text,
|
|
135
|
+
range,
|
|
136
|
+
anchorNode,
|
|
137
|
+
focusNode,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build the class string from a TextStyle object
|
|
143
|
+
*/
|
|
144
|
+
export function buildStyleClasses(style: TextStyle): string {
|
|
145
|
+
const classes: string[] = []
|
|
146
|
+
|
|
147
|
+
for (const [category, value] of Object.entries(style)) {
|
|
148
|
+
if (value === undefined) continue
|
|
149
|
+
|
|
150
|
+
const defaultValue = DEFAULT_VALUES[category as StyleCategory]
|
|
151
|
+
if (value === defaultValue) continue
|
|
152
|
+
|
|
153
|
+
const styleConfig = TAILWIND_STYLES[category as StyleCategory]
|
|
154
|
+
const config = styleConfig[value as keyof typeof styleConfig] as { class: string } | undefined
|
|
155
|
+
if (config?.class) {
|
|
156
|
+
classes.push(config.class)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return classes.join(' ')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Map style category to manifest property name
|
|
165
|
+
*/
|
|
166
|
+
const CATEGORY_TO_MANIFEST: Record<StyleCategory, keyof AvailableTextStyles | null> = {
|
|
167
|
+
weight: 'fontWeight',
|
|
168
|
+
size: 'fontSize',
|
|
169
|
+
decoration: 'textDecoration',
|
|
170
|
+
style: 'fontStyle',
|
|
171
|
+
color: null, // Colors are handled separately via availableColors
|
|
172
|
+
highlight: null, // Highlights are handled separately via availableColors
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Resolve CSS properties for a class name from the manifest.
|
|
177
|
+
* Falls back to hardcoded TAILWIND_STYLES if not found in manifest.
|
|
178
|
+
*/
|
|
179
|
+
function resolveCssFromManifest(
|
|
180
|
+
className: string,
|
|
181
|
+
availableTextStyles: AvailableTextStyles | undefined,
|
|
182
|
+
): Record<string, string> | undefined {
|
|
183
|
+
if (availableTextStyles) {
|
|
184
|
+
// Check each category in the manifest
|
|
185
|
+
for (const category of ['fontWeight', 'fontSize', 'textDecoration', 'fontStyle'] as const) {
|
|
186
|
+
const styles = availableTextStyles[category]
|
|
187
|
+
if (styles) {
|
|
188
|
+
const found = styles.find(s => s.class === className)
|
|
189
|
+
if (found) {
|
|
190
|
+
return found.css
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return undefined
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Build inline CSS styles from a TextStyle object.
|
|
200
|
+
* Uses manifest styles when available, falls back to hardcoded defaults.
|
|
201
|
+
*/
|
|
202
|
+
export function buildInlineStyles(
|
|
203
|
+
style: TextStyle,
|
|
204
|
+
availableTextStyles?: AvailableTextStyles,
|
|
205
|
+
): Record<string, string> {
|
|
206
|
+
const cssStyles: Record<string, string> = {}
|
|
207
|
+
|
|
208
|
+
for (const [category, value] of Object.entries(style)) {
|
|
209
|
+
if (value === undefined) continue
|
|
210
|
+
|
|
211
|
+
const styleConfig = TAILWIND_STYLES[category as StyleCategory]
|
|
212
|
+
const config = styleConfig[value as keyof typeof styleConfig] as { class?: string; css?: Record<string, string> } | undefined
|
|
213
|
+
|
|
214
|
+
if (config?.class) {
|
|
215
|
+
// Try to resolve from manifest first
|
|
216
|
+
const manifestCss = resolveCssFromManifest(config.class, availableTextStyles)
|
|
217
|
+
if (manifestCss) {
|
|
218
|
+
Object.assign(cssStyles, manifestCss)
|
|
219
|
+
} else if (config.css) {
|
|
220
|
+
// Fall back to hardcoded defaults
|
|
221
|
+
Object.assign(cssStyles, config.css)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return cssStyles
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Stored reference to available text styles for internal functions */
|
|
230
|
+
let _availableTextStyles: AvailableTextStyles | undefined
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Set the available text styles from manifest.
|
|
234
|
+
* Call this when manifest is loaded.
|
|
235
|
+
*/
|
|
236
|
+
export function setAvailableTextStyles(styles: AvailableTextStyles | undefined): void {
|
|
237
|
+
_availableTextStyles = styles
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Apply inline styles to an element from a TextStyle object
|
|
242
|
+
*/
|
|
243
|
+
function applyInlineStyles(element: HTMLElement, style: TextStyle): void {
|
|
244
|
+
const cssStyles = buildInlineStyles(style, _availableTextStyles)
|
|
245
|
+
for (const [property, value] of Object.entries(cssStyles)) {
|
|
246
|
+
element.style[property as any] = value
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Clear inline styles that correspond to text styling
|
|
252
|
+
*/
|
|
253
|
+
function clearTextInlineStyles(element: HTMLElement): void {
|
|
254
|
+
element.style.fontWeight = ''
|
|
255
|
+
element.style.textDecoration = ''
|
|
256
|
+
element.style.fontStyle = ''
|
|
257
|
+
element.style.color = ''
|
|
258
|
+
element.style.backgroundColor = ''
|
|
259
|
+
element.style.fontSize = ''
|
|
260
|
+
element.style.lineHeight = ''
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Parse Tailwind classes from a class string back to TextStyle.
|
|
265
|
+
* Uses O(n) lookup via pre-computed map instead of O(n*m) nested loops.
|
|
266
|
+
*/
|
|
267
|
+
export function parseStyleClasses(classString: string): TextStyle {
|
|
268
|
+
if (!classString) return {}
|
|
269
|
+
|
|
270
|
+
const classes = classString.split(/\s+/).filter(Boolean)
|
|
271
|
+
const style: TextStyle = {}
|
|
272
|
+
|
|
273
|
+
for (const cls of classes) {
|
|
274
|
+
const mapping = CLASS_TO_STYLE_MAP.get(cls)
|
|
275
|
+
if (mapping) {
|
|
276
|
+
// Type-safe assignment using the mapping
|
|
277
|
+
;(style as Record<string, string>)[mapping.category] = mapping.key
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return style
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Separate styling classes from non-styling classes
|
|
286
|
+
*/
|
|
287
|
+
export function separateClasses(classString: string): { styleClasses: string[]; otherClasses: string[] } {
|
|
288
|
+
const classes = classString.split(/\s+/).filter(Boolean)
|
|
289
|
+
const styleClasses: string[] = []
|
|
290
|
+
const otherClasses: string[] = []
|
|
291
|
+
|
|
292
|
+
for (const cls of classes) {
|
|
293
|
+
if (KNOWN_STYLE_CLASSES.has(cls)) {
|
|
294
|
+
styleClasses.push(cls)
|
|
295
|
+
} else {
|
|
296
|
+
otherClasses.push(cls)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { styleClasses, otherClasses }
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Check if an element is a styled span created by the CMS
|
|
305
|
+
*/
|
|
306
|
+
export function isStyledSpan(element: Element | null): element is HTMLElement {
|
|
307
|
+
return element instanceof HTMLElement && element.hasAttribute('data-cms-styled')
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Create a new styled span element with both classes and inline styles
|
|
312
|
+
*/
|
|
313
|
+
function createStyledSpan(style: TextStyle): HTMLSpanElement {
|
|
314
|
+
const span = document.createElement('span')
|
|
315
|
+
span.setAttribute('data-cms-styled', 'true')
|
|
316
|
+
const classString = buildStyleClasses(style)
|
|
317
|
+
if (classString) {
|
|
318
|
+
span.className = classString
|
|
319
|
+
}
|
|
320
|
+
// Apply inline styles for immediate visual feedback
|
|
321
|
+
applyInlineStyles(span, style)
|
|
322
|
+
return span
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Get the styled span element if the selection is entirely within one.
|
|
327
|
+
* Returns null if selection spans multiple elements or is not in a styled span.
|
|
328
|
+
*/
|
|
329
|
+
export function getStyledSpanFromSelection(cmsElement: HTMLElement): HTMLElement | null {
|
|
330
|
+
const selection = window.getSelection()
|
|
331
|
+
if (!selection || selection.rangeCount === 0) {
|
|
332
|
+
return null
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const range = selection.getRangeAt(0)
|
|
336
|
+
const container = range.commonAncestorContainer
|
|
337
|
+
|
|
338
|
+
// Start from the container (or its parent if it's a text node)
|
|
339
|
+
let currentElement: HTMLElement | null = container.nodeType === Node.TEXT_NODE
|
|
340
|
+
? (container.parentElement as HTMLElement | null)
|
|
341
|
+
: (container as HTMLElement)
|
|
342
|
+
|
|
343
|
+
// Walk up the tree to find a styled span
|
|
344
|
+
while (currentElement !== null && currentElement !== cmsElement) {
|
|
345
|
+
if (currentElement.hasAttribute('data-cms-styled')) {
|
|
346
|
+
return currentElement
|
|
347
|
+
}
|
|
348
|
+
currentElement = currentElement.parentElement
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return null
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Remove styling from a styled span element, keeping only the text content.
|
|
356
|
+
* Used to "unstyle" previously styled text.
|
|
357
|
+
*/
|
|
358
|
+
export function removeStyleFromElement(styledSpan: HTMLElement): void {
|
|
359
|
+
const parent = styledSpan.parentNode
|
|
360
|
+
if (!parent) return
|
|
361
|
+
|
|
362
|
+
// Move all children out of the span
|
|
363
|
+
while (styledSpan.firstChild) {
|
|
364
|
+
parent.insertBefore(styledSpan.firstChild, styledSpan)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Remove the now-empty span
|
|
368
|
+
parent.removeChild(styledSpan)
|
|
369
|
+
|
|
370
|
+
// Normalize to merge adjacent text nodes
|
|
371
|
+
parent.normalize()
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Update styles on an existing styled span.
|
|
376
|
+
* Preserves non-styling classes and applies inline styles for immediate feedback.
|
|
377
|
+
*/
|
|
378
|
+
export function updateStyledSpan(span: HTMLElement, newStyle: Partial<TextStyle>): void {
|
|
379
|
+
const existingStyle = parseStyleClasses(span.className)
|
|
380
|
+
const { otherClasses } = separateClasses(span.className)
|
|
381
|
+
|
|
382
|
+
// Merge new style with existing
|
|
383
|
+
const mergedStyle = { ...existingStyle, ...newStyle }
|
|
384
|
+
|
|
385
|
+
// Build new class string
|
|
386
|
+
const newStyleClasses = buildStyleClasses(mergedStyle)
|
|
387
|
+
|
|
388
|
+
if (newStyleClasses || otherClasses.length > 0) {
|
|
389
|
+
span.className = [...otherClasses, newStyleClasses].filter(Boolean).join(' ')
|
|
390
|
+
// Clear existing inline styles and reapply
|
|
391
|
+
clearTextInlineStyles(span)
|
|
392
|
+
applyInlineStyles(span, mergedStyle)
|
|
393
|
+
} else {
|
|
394
|
+
// No styles left, remove the span wrapper
|
|
395
|
+
removeStyleFromElement(span)
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Flatten nested styled spans into a single span with merged styles.
|
|
401
|
+
* Processes the fragment recursively.
|
|
402
|
+
*/
|
|
403
|
+
function flattenStyledSpans(fragment: DocumentFragment, targetSpan: HTMLSpanElement): void {
|
|
404
|
+
const processNode = (node: Node): void => {
|
|
405
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
406
|
+
const htmlElement = node as HTMLElement
|
|
407
|
+
|
|
408
|
+
if (htmlElement.hasAttribute('data-cms-styled')) {
|
|
409
|
+
// Merge classes from nested styled span
|
|
410
|
+
const nestedStyle = parseStyleClasses(htmlElement.className)
|
|
411
|
+
const currentStyle = parseStyleClasses(targetSpan.className)
|
|
412
|
+
// Nested span's styles take precedence
|
|
413
|
+
const mergedStyle = { ...currentStyle, ...nestedStyle }
|
|
414
|
+
targetSpan.className = buildStyleClasses(mergedStyle) || ''
|
|
415
|
+
// Apply inline styles for immediate visual feedback
|
|
416
|
+
clearTextInlineStyles(targetSpan)
|
|
417
|
+
applyInlineStyles(targetSpan, mergedStyle)
|
|
418
|
+
|
|
419
|
+
// Recursively process children of the nested span
|
|
420
|
+
const children = Array.from(htmlElement.childNodes) as ChildNode[]
|
|
421
|
+
for (const child of children) {
|
|
422
|
+
processNode(child)
|
|
423
|
+
}
|
|
424
|
+
} else {
|
|
425
|
+
// Clone the element (without children) and process children recursively
|
|
426
|
+
const clonedElement = htmlElement.cloneNode(false) as HTMLElement
|
|
427
|
+
const children = Array.from(htmlElement.childNodes) as ChildNode[]
|
|
428
|
+
for (const child of children) {
|
|
429
|
+
// For regular elements, we need to keep their structure
|
|
430
|
+
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
431
|
+
const childElement = child as HTMLElement
|
|
432
|
+
if (childElement.hasAttribute('data-cms-styled')) {
|
|
433
|
+
// Flatten nested styled spans within regular elements
|
|
434
|
+
const innerChildren = Array.from(childElement.childNodes) as ChildNode[]
|
|
435
|
+
for (const innerChild of innerChildren) {
|
|
436
|
+
clonedElement.appendChild(innerChild.cloneNode(true))
|
|
437
|
+
}
|
|
438
|
+
// Merge styles
|
|
439
|
+
const nestedStyle = parseStyleClasses(childElement.className)
|
|
440
|
+
const currentStyle = parseStyleClasses(targetSpan.className)
|
|
441
|
+
const mergedStyle = { ...currentStyle, ...nestedStyle }
|
|
442
|
+
targetSpan.className = buildStyleClasses(mergedStyle) || ''
|
|
443
|
+
// Apply inline styles
|
|
444
|
+
clearTextInlineStyles(targetSpan)
|
|
445
|
+
applyInlineStyles(targetSpan, mergedStyle)
|
|
446
|
+
} else {
|
|
447
|
+
clonedElement.appendChild(child.cloneNode(true))
|
|
448
|
+
}
|
|
449
|
+
} else {
|
|
450
|
+
clonedElement.appendChild(child.cloneNode(true))
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
targetSpan.appendChild(clonedElement)
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
// Text nodes and other node types - just append
|
|
457
|
+
targetSpan.appendChild(node.cloneNode(true))
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Process all children of the fragment
|
|
462
|
+
const nodes = Array.from(fragment.childNodes) as ChildNode[]
|
|
463
|
+
for (const node of nodes) {
|
|
464
|
+
processNode(node)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Check if two elements are directly adjacent (no meaningful content between them)
|
|
470
|
+
*/
|
|
471
|
+
function isDirectlyAdjacent(first: Element, second: Element): boolean {
|
|
472
|
+
let node: Node | null = first.nextSibling
|
|
473
|
+
|
|
474
|
+
while (node && node !== second) {
|
|
475
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
476
|
+
// Only whitespace is allowed between
|
|
477
|
+
if (node.textContent && node.textContent.trim() !== '') {
|
|
478
|
+
return false
|
|
479
|
+
}
|
|
480
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
481
|
+
return false
|
|
482
|
+
}
|
|
483
|
+
node = node.nextSibling
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return node === second
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Remove empty styled spans from the element
|
|
491
|
+
*/
|
|
492
|
+
export function cleanupEmptyStyledSpans(cmsElement: HTMLElement): void {
|
|
493
|
+
const styledSpans = Array.from(cmsElement.querySelectorAll('[data-cms-styled]'))
|
|
494
|
+
|
|
495
|
+
for (const span of styledSpans) {
|
|
496
|
+
if (!span.textContent && !span.querySelector('*')) {
|
|
497
|
+
span.remove()
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Merge adjacent styled spans that have the same classes
|
|
504
|
+
*/
|
|
505
|
+
export function mergeAdjacentStyledSpans(cmsElement: HTMLElement): void {
|
|
506
|
+
let styledSpans = Array.from(cmsElement.querySelectorAll('[data-cms-styled]'))
|
|
507
|
+
let merged = true
|
|
508
|
+
|
|
509
|
+
// Keep merging until no more merges are possible
|
|
510
|
+
while (merged) {
|
|
511
|
+
merged = false
|
|
512
|
+
styledSpans = Array.from(cmsElement.querySelectorAll('[data-cms-styled]'))
|
|
513
|
+
|
|
514
|
+
for (let i = 0; i < styledSpans.length - 1; i++) {
|
|
515
|
+
const current = styledSpans[i] as HTMLElement
|
|
516
|
+
const next = styledSpans[i + 1] as HTMLElement
|
|
517
|
+
|
|
518
|
+
if (!current.parentNode || !next.parentNode) continue
|
|
519
|
+
|
|
520
|
+
// Check if they are truly adjacent (no significant content between them)
|
|
521
|
+
const isAdjacent = isDirectlyAdjacent(current, next)
|
|
522
|
+
|
|
523
|
+
if (isAdjacent && current.className === next.className) {
|
|
524
|
+
// Move all children from next to current
|
|
525
|
+
while (next.firstChild) {
|
|
526
|
+
current.appendChild(next.firstChild)
|
|
527
|
+
}
|
|
528
|
+
// Remove the empty next span
|
|
529
|
+
next.remove()
|
|
530
|
+
merged = true
|
|
531
|
+
break // Restart the loop after merging
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Clean up empty styled spans
|
|
537
|
+
cleanupEmptyStyledSpans(cmsElement)
|
|
538
|
+
|
|
539
|
+
cmsElement.normalize()
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Wrap selected text in a span with Tailwind classes.
|
|
544
|
+
* If the selection is already inside a styled span, just update that span's classes.
|
|
545
|
+
* Returns the styled span element or null if failed.
|
|
546
|
+
*/
|
|
547
|
+
export function wrapSelectionWithStyle(cmsElement: HTMLElement, selection: TextSelection, style: TextStyle): HTMLSpanElement | null {
|
|
548
|
+
const classString = buildStyleClasses(style)
|
|
549
|
+
|
|
550
|
+
// Check if we're already inside a styled span
|
|
551
|
+
const existingSpan = getStyledSpanFromSelection(cmsElement)
|
|
552
|
+
if (existingSpan) {
|
|
553
|
+
// Check if we're selecting the entire span content
|
|
554
|
+
const selectionRange = selection.range
|
|
555
|
+
const spanRange = document.createRange()
|
|
556
|
+
spanRange.selectNodeContents(existingSpan)
|
|
557
|
+
|
|
558
|
+
const isFullSelection = selectionRange.compareBoundaryPoints(Range.START_TO_START, spanRange) === 0
|
|
559
|
+
&& selectionRange.compareBoundaryPoints(Range.END_TO_END, spanRange) === 0
|
|
560
|
+
|
|
561
|
+
if (isFullSelection) {
|
|
562
|
+
// Update the entire span's styles
|
|
563
|
+
updateStyledSpan(existingSpan, style)
|
|
564
|
+
return existingSpan
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Partial selection within existing span - need to split
|
|
568
|
+
return splitAndStyleSelection(cmsElement, existingSpan, selection, style)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (!classString) {
|
|
572
|
+
return null
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
try {
|
|
576
|
+
// Create the wrapper span
|
|
577
|
+
const span = createStyledSpan(style)
|
|
578
|
+
|
|
579
|
+
// Extract the selected content and wrap it
|
|
580
|
+
const contents = selection.range.extractContents()
|
|
581
|
+
|
|
582
|
+
// Flatten any nested styled spans from the extracted content
|
|
583
|
+
flattenStyledSpans(contents, span)
|
|
584
|
+
|
|
585
|
+
selection.range.insertNode(span)
|
|
586
|
+
|
|
587
|
+
// Normalize the parent to merge adjacent text nodes
|
|
588
|
+
cmsElement.normalize()
|
|
589
|
+
|
|
590
|
+
// Merge adjacent styled spans with same classes
|
|
591
|
+
mergeAdjacentStyledSpans(cmsElement)
|
|
592
|
+
|
|
593
|
+
return span
|
|
594
|
+
} catch (error) {
|
|
595
|
+
console.error('[CMS] Failed to wrap selection:', error)
|
|
596
|
+
return null
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Handle partial selection within an existing styled span by splitting it
|
|
602
|
+
*/
|
|
603
|
+
function splitAndStyleSelection(
|
|
604
|
+
cmsElement: HTMLElement,
|
|
605
|
+
existingSpan: HTMLElement,
|
|
606
|
+
selection: TextSelection,
|
|
607
|
+
newStyle: TextStyle,
|
|
608
|
+
): HTMLSpanElement | null {
|
|
609
|
+
try {
|
|
610
|
+
const range = selection.range
|
|
611
|
+
const existingStyle = parseStyleClasses(existingSpan.className)
|
|
612
|
+
|
|
613
|
+
// Create a range for the content before the selection
|
|
614
|
+
const beforeRange = document.createRange()
|
|
615
|
+
beforeRange.setStart(existingSpan, 0)
|
|
616
|
+
beforeRange.setEnd(range.startContainer, range.startOffset)
|
|
617
|
+
|
|
618
|
+
// Create a range for the content after the selection
|
|
619
|
+
const afterRange = document.createRange()
|
|
620
|
+
afterRange.setStart(range.endContainer, range.endOffset)
|
|
621
|
+
afterRange.setEndAfter(existingSpan.lastChild || existingSpan)
|
|
622
|
+
|
|
623
|
+
// Extract contents
|
|
624
|
+
const beforeContents = beforeRange.extractContents()
|
|
625
|
+
const selectedContents = range.extractContents()
|
|
626
|
+
const afterContents = afterRange.extractContents()
|
|
627
|
+
|
|
628
|
+
// Get parent for insertion
|
|
629
|
+
const parent = existingSpan.parentNode
|
|
630
|
+
if (!parent) return null
|
|
631
|
+
|
|
632
|
+
// Create spans for each section
|
|
633
|
+
const beforeSpan = beforeContents.textContent?.trim() || beforeContents.querySelector('*')
|
|
634
|
+
? createStyledSpan(existingStyle)
|
|
635
|
+
: null
|
|
636
|
+
|
|
637
|
+
const selectedSpan = createStyledSpan({ ...existingStyle, ...newStyle })
|
|
638
|
+
|
|
639
|
+
const afterSpan = afterContents.textContent?.trim() || afterContents.querySelector('*')
|
|
640
|
+
? createStyledSpan(existingStyle)
|
|
641
|
+
: null
|
|
642
|
+
|
|
643
|
+
// Populate spans
|
|
644
|
+
if (beforeSpan) {
|
|
645
|
+
beforeSpan.appendChild(beforeContents)
|
|
646
|
+
}
|
|
647
|
+
selectedSpan.appendChild(selectedContents)
|
|
648
|
+
if (afterSpan) {
|
|
649
|
+
afterSpan.appendChild(afterContents)
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Insert new spans before the existing one
|
|
653
|
+
if (beforeSpan) parent.insertBefore(beforeSpan, existingSpan)
|
|
654
|
+
parent.insertBefore(selectedSpan, existingSpan)
|
|
655
|
+
if (afterSpan) parent.insertBefore(afterSpan, existingSpan)
|
|
656
|
+
|
|
657
|
+
// Remove the original span
|
|
658
|
+
existingSpan.remove()
|
|
659
|
+
|
|
660
|
+
// Cleanup and merge
|
|
661
|
+
cleanupEmptyStyledSpans(cmsElement)
|
|
662
|
+
mergeAdjacentStyledSpans(cmsElement)
|
|
663
|
+
cmsElement.normalize()
|
|
664
|
+
|
|
665
|
+
return selectedSpan
|
|
666
|
+
} catch (error) {
|
|
667
|
+
console.error('[CMS] Failed to split and style selection:', error)
|
|
668
|
+
return null
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Apply a specific style category to the current selection.
|
|
674
|
+
* This is a convenience function for single-style application.
|
|
675
|
+
*/
|
|
676
|
+
export function applyStyleToSelection<C extends StyleCategory>(
|
|
677
|
+
cmsElement: HTMLElement,
|
|
678
|
+
category: C,
|
|
679
|
+
value: StyleValue<C>,
|
|
680
|
+
): HTMLSpanElement | null {
|
|
681
|
+
const selection = getTextSelection(cmsElement)
|
|
682
|
+
if (!selection) {
|
|
683
|
+
return null
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const style: TextStyle = { [category]: value }
|
|
687
|
+
return wrapSelectionWithStyle(cmsElement, selection, style)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Toggle a style value on the selection or styled element.
|
|
692
|
+
* If the style is already applied, it removes it; otherwise applies it.
|
|
693
|
+
*/
|
|
694
|
+
export function toggleStyle<C extends StyleCategory>(cmsElement: HTMLElement, category: C, value: StyleValue<C>): HTMLSpanElement | null {
|
|
695
|
+
const styledSpan = getStyledSpanFromSelection(cmsElement)
|
|
696
|
+
|
|
697
|
+
if (styledSpan) {
|
|
698
|
+
const currentStyle = parseStyleClasses(styledSpan.className)
|
|
699
|
+
const currentValue = currentStyle[category] as string | undefined
|
|
700
|
+
|
|
701
|
+
// Check if this exact value is already applied
|
|
702
|
+
if (currentValue === value) {
|
|
703
|
+
// Remove this style (set to default)
|
|
704
|
+
const defaultValue = DEFAULT_VALUES[category]
|
|
705
|
+
updateStyledSpan(styledSpan, { [category]: defaultValue } as TextStyle)
|
|
706
|
+
return null
|
|
707
|
+
} else {
|
|
708
|
+
// Update to the new value
|
|
709
|
+
updateStyledSpan(styledSpan, { [category]: value } as TextStyle)
|
|
710
|
+
return styledSpan
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// No existing styled span, create a new one
|
|
715
|
+
return applyStyleToSelection(cmsElement, category, value)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Check if a specific style is currently applied to the selection
|
|
720
|
+
*/
|
|
721
|
+
export function hasStyle<C extends StyleCategory>(cmsElement: HTMLElement, category: C, value: StyleValue<C>): boolean {
|
|
722
|
+
const styledSpan = getStyledSpanFromSelection(cmsElement)
|
|
723
|
+
if (!styledSpan) {
|
|
724
|
+
return false
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const currentStyle = parseStyleClasses(styledSpan.className)
|
|
728
|
+
const currentValue = currentStyle[category] as string | undefined
|
|
729
|
+
return currentValue === value
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Get the current style of the selection
|
|
734
|
+
*/
|
|
735
|
+
export function getCurrentStyle(cmsElement: HTMLElement): TextStyle {
|
|
736
|
+
const styledSpan = getStyledSpanFromSelection(cmsElement)
|
|
737
|
+
if (!styledSpan) {
|
|
738
|
+
return {}
|
|
739
|
+
}
|
|
740
|
+
return parseStyleClasses(styledSpan.className)
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Remove all styling from the current selection
|
|
745
|
+
*/
|
|
746
|
+
export function clearAllStyles(cmsElement: HTMLElement): void {
|
|
747
|
+
const styledSpan = getStyledSpanFromSelection(cmsElement)
|
|
748
|
+
if (styledSpan) {
|
|
749
|
+
removeStyleFromElement(styledSpan)
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Check if a style value is valid for a given category
|
|
755
|
+
*/
|
|
756
|
+
export function isValidStyleValue<C extends StyleCategory>(category: C, value: unknown): value is StyleValue<C> {
|
|
757
|
+
const categoryStyles = TAILWIND_STYLES[category]
|
|
758
|
+
return typeof value === 'string' && value in categoryStyles
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Get the default value for a style category
|
|
763
|
+
*/
|
|
764
|
+
export function getDefaultValue(category: StyleCategory): string {
|
|
765
|
+
return DEFAULT_VALUES[category]
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Get all Tailwind classes used for text styling.
|
|
770
|
+
*/
|
|
771
|
+
export function getAllStyleClasses(): string[] {
|
|
772
|
+
return Array.from(KNOWN_STYLE_CLASSES)
|
|
773
|
+
}
|