@nuasite/cms 0.7.3 → 0.8.2
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 +2 -6
- package/dist/editor.js +8983 -8467
- package/package.json +1 -1
- package/src/color-patterns.ts +68 -1
- package/src/editor/color-utils.ts +8 -8
- package/src/editor/components/bg-image-overlay.tsx +483 -0
- package/src/editor/components/editable-highlights.tsx +14 -0
- package/src/editor/components/frontmatter-fields.tsx +7 -3
- package/src/editor/components/outline.tsx +3 -2
- package/src/editor/components/text-style-toolbar.tsx +4 -1
- package/src/editor/constants.ts +3 -0
- package/src/editor/editor.ts +173 -7
- package/src/editor/history.ts +120 -34
- package/src/editor/hooks/index.ts +3 -0
- package/src/editor/hooks/useBgImageHoverDetection.ts +101 -0
- package/src/editor/index.tsx +12 -0
- package/src/editor/signals.ts +48 -14
- package/src/editor/storage.ts +50 -0
- package/src/editor/types.ts +79 -8
- package/src/handlers/source-writer.ts +61 -69
- package/src/html-processor.ts +75 -5
- package/src/source-finder/snippet-utils.ts +6 -2
- package/src/tailwind-colors.ts +1 -0
- package/src/types.ts +16 -0
package/src/editor/constants.ts
CHANGED
|
@@ -87,6 +87,7 @@ export const STORAGE_KEYS = {
|
|
|
87
87
|
PENDING_IMAGE_EDITS: 'cms-pending-image-edits',
|
|
88
88
|
PENDING_COLOR_EDITS: 'cms-pending-color-edits',
|
|
89
89
|
PENDING_ATTRIBUTE_EDITS: 'cms-pending-attribute-edits',
|
|
90
|
+
PENDING_BG_IMAGE_EDITS: 'cms-pending-bg-image-edits',
|
|
90
91
|
SETTINGS: 'cms-settings',
|
|
91
92
|
PENDING_ENTRY_NAVIGATION: 'cms-pending-entry-navigation',
|
|
92
93
|
IS_EDITING: 'cms-is-editing',
|
|
@@ -104,4 +105,6 @@ export const CSS = {
|
|
|
104
105
|
COMPONENT_ID_ATTRIBUTE: 'data-cms-component-id',
|
|
105
106
|
/** Custom element tag for highlight overlay */
|
|
106
107
|
HIGHLIGHT_ELEMENT: 'cms-highlight-overlay',
|
|
108
|
+
/** Data attribute for background image elements */
|
|
109
|
+
BG_IMAGE_ATTRIBUTE: 'data-cms-bg-img',
|
|
107
110
|
} as const
|
package/src/editor/editor.ts
CHANGED
|
@@ -20,11 +20,13 @@ import * as signals from './signals'
|
|
|
20
20
|
import {
|
|
21
21
|
clearAllEditsFromStorage,
|
|
22
22
|
loadAttributeEditsFromStorage,
|
|
23
|
+
loadBgImageEditsFromStorage,
|
|
23
24
|
loadColorEditsFromStorage,
|
|
24
25
|
loadEditsFromStorage,
|
|
25
26
|
loadImageEditsFromStorage,
|
|
26
27
|
loadPendingEntryNavigation,
|
|
27
28
|
saveAttributeEditsToStorage,
|
|
29
|
+
saveBgImageEditsToStorage,
|
|
28
30
|
saveColorEditsToStorage,
|
|
29
31
|
saveEditingState,
|
|
30
32
|
saveEditsToStorage,
|
|
@@ -37,6 +39,8 @@ import type { AttributeChangePayload, ChangePayload, CmsConfig, DeploymentStatus
|
|
|
37
39
|
const MARKDOWN_ATTRIBUTE = 'data-cms-markdown'
|
|
38
40
|
// CSS attribute for image elements
|
|
39
41
|
const IMAGE_ATTRIBUTE = 'data-cms-img'
|
|
42
|
+
// CSS attribute for background image elements
|
|
43
|
+
const BG_IMAGE_ATTRIBUTE = 'data-cms-bg-img'
|
|
40
44
|
|
|
41
45
|
/**
|
|
42
46
|
* Inline HTML elements that indicate styled/formatted content.
|
|
@@ -112,6 +116,7 @@ export async function startEditMode(
|
|
|
112
116
|
const savedImageEdits = loadImageEditsFromStorage()
|
|
113
117
|
const savedColorEdits = loadColorEditsFromStorage()
|
|
114
118
|
const savedAttributeEdits = loadAttributeEditsFromStorage()
|
|
119
|
+
const savedBgImageEdits = loadBgImageEditsFromStorage()
|
|
115
120
|
const currentManifest = signals.manifest.value
|
|
116
121
|
|
|
117
122
|
getAllCmsElements().forEach(el => {
|
|
@@ -151,6 +156,15 @@ export async function startEditMode(
|
|
|
151
156
|
return
|
|
152
157
|
}
|
|
153
158
|
|
|
159
|
+
// Check if this is a background image element
|
|
160
|
+
// Background image elements are edited via the bg image overlay panel
|
|
161
|
+
if (el.hasAttribute(BG_IMAGE_ATTRIBUTE)) {
|
|
162
|
+
logDebug(config.debug, 'Background image element detected:', cmsId)
|
|
163
|
+
makeElementNonEditable(el)
|
|
164
|
+
setupBgImageTracking(config, el, cmsId, savedBgImageEdits[cmsId])
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
154
168
|
makeElementEditable(el)
|
|
155
169
|
|
|
156
170
|
// Suppress browser native contentEditable undo/redo (we handle it ourselves)
|
|
@@ -502,11 +516,42 @@ export function discardAllChanges(onStateChange?: () => void): void {
|
|
|
502
516
|
applyAttributesToElement(element, originalAttributes)
|
|
503
517
|
})
|
|
504
518
|
|
|
519
|
+
// Restore original background image classes
|
|
520
|
+
signals.pendingBgImageChanges.value.forEach((change) => {
|
|
521
|
+
const {
|
|
522
|
+
element,
|
|
523
|
+
originalBgImageClass,
|
|
524
|
+
newBgImageClass,
|
|
525
|
+
originalBgSize,
|
|
526
|
+
newBgSize,
|
|
527
|
+
originalBgPosition,
|
|
528
|
+
newBgPosition,
|
|
529
|
+
originalBgRepeat,
|
|
530
|
+
newBgRepeat,
|
|
531
|
+
} = change
|
|
532
|
+
const classes = element.className.split(/\s+/).filter(Boolean)
|
|
533
|
+
const newClassValues = new Set([newBgImageClass, newBgSize, newBgPosition, newBgRepeat].filter(Boolean))
|
|
534
|
+
const originalClassValues = [originalBgImageClass, originalBgSize, originalBgPosition, originalBgRepeat].filter(Boolean)
|
|
535
|
+
|
|
536
|
+
const filtered = classes.filter(c => !newClassValues.has(c))
|
|
537
|
+
for (const c of originalClassValues) {
|
|
538
|
+
if (!filtered.includes(c)) {
|
|
539
|
+
filtered.push(c)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
element.className = filtered.join(' ')
|
|
543
|
+
|
|
544
|
+
// Clear inline bg style overrides
|
|
545
|
+
element.style.backgroundImage = ''
|
|
546
|
+
element.style.backgroundSize = ''
|
|
547
|
+
element.style.backgroundPosition = ''
|
|
548
|
+
element.style.backgroundRepeat = ''
|
|
549
|
+
})
|
|
550
|
+
|
|
505
551
|
cleanupHighlightSystem()
|
|
506
|
-
signals.
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
signals.clearPendingAttributeChanges()
|
|
552
|
+
for (const entry of Object.values(signals.changeRegistry)) {
|
|
553
|
+
entry.mapSignal.value = new Map()
|
|
554
|
+
}
|
|
510
555
|
clearAllEditsFromStorage()
|
|
511
556
|
clearHistory()
|
|
512
557
|
stopEditMode(onStateChange)
|
|
@@ -550,12 +595,13 @@ export async function saveAllChanges(
|
|
|
550
595
|
const dirtyChanges = signals.dirtyChanges.value
|
|
551
596
|
const dirtyImageChanges = signals.dirtyImageChanges.value
|
|
552
597
|
const dirtyColorChanges = signals.dirtyColorChanges.value
|
|
598
|
+
const dirtyBgImageChanges = signals.dirtyBgImageChanges.value
|
|
553
599
|
const dirtyAttributeChanges = signals.dirtyAttributeChanges.value
|
|
554
600
|
const dirtySeoChanges = signals.dirtySeoChanges.value
|
|
555
601
|
|
|
556
602
|
if (
|
|
557
|
-
dirtyChanges.length === 0 && dirtyImageChanges.length === 0 && dirtyColorChanges.length === 0 &&
|
|
558
|
-
&& dirtySeoChanges.length === 0
|
|
603
|
+
dirtyChanges.length === 0 && dirtyImageChanges.length === 0 && dirtyColorChanges.length === 0 && dirtyBgImageChanges.length === 0
|
|
604
|
+
&& dirtyAttributeChanges.length === 0 && dirtySeoChanges.length === 0
|
|
559
605
|
) {
|
|
560
606
|
return { success: true, updated: 0 }
|
|
561
607
|
}
|
|
@@ -653,7 +699,7 @@ export async function saveAllChanges(
|
|
|
653
699
|
sourcePath: bestSourcePath ?? entry?.sourcePath ?? '',
|
|
654
700
|
sourceLine: bestSourceLine ?? entry?.sourceLine ?? 0,
|
|
655
701
|
sourceSnippet: bestSourceSnippet ?? entry?.sourceSnippet ?? '',
|
|
656
|
-
|
|
702
|
+
styleChange: {
|
|
657
703
|
oldClass: origAttr?.value || '',
|
|
658
704
|
newClass: newAttr.value,
|
|
659
705
|
type: classType,
|
|
@@ -666,6 +712,44 @@ export async function saveAllChanges(
|
|
|
666
712
|
}
|
|
667
713
|
})
|
|
668
714
|
|
|
715
|
+
// Add background image changes to the payload
|
|
716
|
+
dirtyBgImageChanges.forEach(([cmsId, change]) => {
|
|
717
|
+
const entry = manifest.entries[cmsId]
|
|
718
|
+
const bgChanges: Array<{ oldClass: string; newClass: string; type: 'bgImage' | 'bgSize' | 'bgPosition' | 'bgRepeat' }> = []
|
|
719
|
+
|
|
720
|
+
if (change.newBgImageClass !== change.originalBgImageClass) {
|
|
721
|
+
bgChanges.push({ oldClass: change.originalBgImageClass, newClass: change.newBgImageClass, type: 'bgImage' })
|
|
722
|
+
}
|
|
723
|
+
if (change.newBgSize !== change.originalBgSize) {
|
|
724
|
+
bgChanges.push({ oldClass: change.originalBgSize, newClass: change.newBgSize, type: 'bgSize' })
|
|
725
|
+
}
|
|
726
|
+
if (change.newBgPosition !== change.originalBgPosition) {
|
|
727
|
+
bgChanges.push({ oldClass: change.originalBgPosition, newClass: change.newBgPosition, type: 'bgPosition' })
|
|
728
|
+
}
|
|
729
|
+
if (change.newBgRepeat !== change.originalBgRepeat) {
|
|
730
|
+
bgChanges.push({ oldClass: change.originalBgRepeat, newClass: change.newBgRepeat, type: 'bgRepeat' })
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
for (const bgChange of bgChanges) {
|
|
734
|
+
changes.push({
|
|
735
|
+
cmsId,
|
|
736
|
+
newValue: '',
|
|
737
|
+
originalValue: '',
|
|
738
|
+
sourcePath: entry?.sourcePath ?? '',
|
|
739
|
+
sourceLine: entry?.sourceLine ?? 0,
|
|
740
|
+
sourceSnippet: entry?.sourceSnippet ?? '',
|
|
741
|
+
styleChange: {
|
|
742
|
+
oldClass: bgChange.oldClass,
|
|
743
|
+
newClass: bgChange.newClass,
|
|
744
|
+
type: bgChange.type,
|
|
745
|
+
sourcePath: entry?.sourcePath,
|
|
746
|
+
sourceLine: entry?.sourceLine,
|
|
747
|
+
sourceSnippet: entry?.sourceSnippet,
|
|
748
|
+
},
|
|
749
|
+
})
|
|
750
|
+
}
|
|
751
|
+
})
|
|
752
|
+
|
|
669
753
|
// Add attribute changes to the payload
|
|
670
754
|
dirtyAttributeChanges.forEach(([cmsId, change]) => {
|
|
671
755
|
const { originalAttributes, newAttributes } = change
|
|
@@ -740,6 +824,18 @@ export async function saveAllChanges(
|
|
|
740
824
|
}))
|
|
741
825
|
})
|
|
742
826
|
|
|
827
|
+
// Update all dirty bg image changes to mark as saved
|
|
828
|
+
dirtyBgImageChanges.forEach(([cmsId, change]) => {
|
|
829
|
+
signals.updatePendingBgImageChange(cmsId, (c) => ({
|
|
830
|
+
...c,
|
|
831
|
+
originalBgImageClass: c.newBgImageClass,
|
|
832
|
+
originalBgSize: c.newBgSize,
|
|
833
|
+
originalBgPosition: c.newBgPosition,
|
|
834
|
+
originalBgRepeat: c.newBgRepeat,
|
|
835
|
+
isDirty: false,
|
|
836
|
+
}))
|
|
837
|
+
})
|
|
838
|
+
|
|
743
839
|
// Update all dirty attribute changes to mark as saved
|
|
744
840
|
dirtyAttributeChanges.forEach(([cmsId, change]) => {
|
|
745
841
|
signals.updatePendingAttributeChange(cmsId, (c) => ({
|
|
@@ -786,6 +882,7 @@ export async function saveAllChanges(
|
|
|
786
882
|
saveEditsToStorage(signals.pendingChanges.value)
|
|
787
883
|
saveImageEditsToStorage(signals.pendingImageChanges.value)
|
|
788
884
|
saveColorEditsToStorage(signals.pendingColorChanges.value)
|
|
885
|
+
saveBgImageEditsToStorage(signals.pendingBgImageChanges.value)
|
|
789
886
|
saveAttributeEditsToStorage(signals.pendingAttributeChanges.value)
|
|
790
887
|
throw err
|
|
791
888
|
} finally {
|
|
@@ -1026,6 +1123,75 @@ function setupColorTracking(
|
|
|
1026
1123
|
}
|
|
1027
1124
|
}
|
|
1028
1125
|
|
|
1126
|
+
/**
|
|
1127
|
+
* Initialize background image change tracking for elements with bg-[url()] classes.
|
|
1128
|
+
* Background image editing is triggered via the bg image overlay panel.
|
|
1129
|
+
*/
|
|
1130
|
+
function setupBgImageTracking(
|
|
1131
|
+
config: CmsConfig,
|
|
1132
|
+
el: HTMLElement,
|
|
1133
|
+
cmsId: string,
|
|
1134
|
+
savedEdit: import('./types').SavedBackgroundImageEdit | undefined,
|
|
1135
|
+
): void {
|
|
1136
|
+
const manifest = signals.manifest.value
|
|
1137
|
+
const entry = manifest.entries[cmsId]
|
|
1138
|
+
|
|
1139
|
+
if (!entry?.backgroundImage) {
|
|
1140
|
+
return
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
logDebug(config.debug, 'Setting up bg image tracking for:', cmsId, entry.backgroundImage)
|
|
1144
|
+
|
|
1145
|
+
if (!signals.pendingBgImageChanges.value.has(cmsId)) {
|
|
1146
|
+
if (savedEdit) {
|
|
1147
|
+
// Restore saved bg image classes on the element
|
|
1148
|
+
const classes = el.className.split(/\s+/).filter(Boolean)
|
|
1149
|
+
const originals = [savedEdit.originalBgImageClass, savedEdit.originalBgSize, savedEdit.originalBgPosition, savedEdit.originalBgRepeat].filter(
|
|
1150
|
+
Boolean,
|
|
1151
|
+
)
|
|
1152
|
+
const news = [savedEdit.newBgImageClass, savedEdit.newBgSize, savedEdit.newBgPosition, savedEdit.newBgRepeat].filter(Boolean)
|
|
1153
|
+
|
|
1154
|
+
const filtered = classes.filter(c => !originals.includes(c))
|
|
1155
|
+
for (const c of news) {
|
|
1156
|
+
if (!filtered.includes(c)) {
|
|
1157
|
+
filtered.push(c)
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
el.className = filtered.join(' ')
|
|
1161
|
+
|
|
1162
|
+
signals.setPendingBgImageChange(cmsId, {
|
|
1163
|
+
element: el,
|
|
1164
|
+
cmsId,
|
|
1165
|
+
originalBgImageClass: savedEdit.originalBgImageClass,
|
|
1166
|
+
newBgImageClass: savedEdit.newBgImageClass,
|
|
1167
|
+
originalBgSize: savedEdit.originalBgSize,
|
|
1168
|
+
newBgSize: savedEdit.newBgSize,
|
|
1169
|
+
originalBgPosition: savedEdit.originalBgPosition,
|
|
1170
|
+
newBgPosition: savedEdit.newBgPosition,
|
|
1171
|
+
originalBgRepeat: savedEdit.originalBgRepeat,
|
|
1172
|
+
newBgRepeat: savedEdit.newBgRepeat,
|
|
1173
|
+
isDirty: true,
|
|
1174
|
+
})
|
|
1175
|
+
logDebug(config.debug, 'Restored saved bg image edit:', cmsId, savedEdit)
|
|
1176
|
+
} else {
|
|
1177
|
+
const bg = entry.backgroundImage
|
|
1178
|
+
signals.setPendingBgImageChange(cmsId, {
|
|
1179
|
+
element: el,
|
|
1180
|
+
cmsId,
|
|
1181
|
+
originalBgImageClass: bg.bgImageClass,
|
|
1182
|
+
newBgImageClass: bg.bgImageClass,
|
|
1183
|
+
originalBgSize: bg.bgSize ?? '',
|
|
1184
|
+
newBgSize: bg.bgSize ?? '',
|
|
1185
|
+
originalBgPosition: bg.bgPosition ?? '',
|
|
1186
|
+
newBgPosition: bg.bgPosition ?? '',
|
|
1187
|
+
originalBgRepeat: bg.bgRepeat ?? '',
|
|
1188
|
+
newBgRepeat: bg.bgRepeat ?? '',
|
|
1189
|
+
isDirty: false,
|
|
1190
|
+
})
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1029
1195
|
/**
|
|
1030
1196
|
* Handle color change from the color toolbar.
|
|
1031
1197
|
* Called when user selects a new color.
|
package/src/editor/history.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { computed, signal } from '@preact/signals'
|
|
2
2
|
import * as signals from './signals'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
saveAttributeEditsToStorage,
|
|
5
|
+
saveBgImageEditsToStorage,
|
|
6
|
+
saveColorEditsToStorage,
|
|
7
|
+
saveEditsToStorage,
|
|
8
|
+
saveImageEditsToStorage,
|
|
9
|
+
} from './storage'
|
|
4
10
|
import type { Attribute, UndoAction, UndoTextAction } from './types'
|
|
5
11
|
|
|
6
12
|
// ============================================================================
|
|
@@ -159,52 +165,104 @@ function scrollToElement(element: HTMLElement): void {
|
|
|
159
165
|
}
|
|
160
166
|
}
|
|
161
167
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
168
|
+
/** Registry of undo/redo handlers keyed by action type.
|
|
169
|
+
* Adding a new UndoAction variant forces a TypeScript error until its handlers are added here. */
|
|
170
|
+
type UndoHandlers = {
|
|
171
|
+
[K in UndoAction['type']]: {
|
|
172
|
+
applyReverse: (action: Extract<UndoAction, { type: K }>) => void
|
|
173
|
+
applyForward: (action: Extract<UndoAction, { type: K }>) => void
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const undoRegistry: UndoHandlers = {
|
|
178
|
+
text: {
|
|
179
|
+
applyReverse: (action) => {
|
|
165
180
|
scrollToElement(action.element)
|
|
166
181
|
applyTextState(action.cmsId, action.element, action.previousHTML, action.previousText, action.wasDirty)
|
|
167
|
-
|
|
168
|
-
|
|
182
|
+
},
|
|
183
|
+
applyForward: (action) => {
|
|
184
|
+
scrollToElement(action.element)
|
|
185
|
+
applyTextState(action.cmsId, action.element, action.currentHTML, action.currentText, true)
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
image: {
|
|
189
|
+
applyReverse: (action) => {
|
|
169
190
|
scrollToElement(action.element)
|
|
170
191
|
applyImageState(action.cmsId, action.element, action.previousSrc, action.previousAlt, action.wasDirty)
|
|
171
|
-
|
|
172
|
-
|
|
192
|
+
},
|
|
193
|
+
applyForward: (action) => {
|
|
194
|
+
scrollToElement(action.element)
|
|
195
|
+
applyImageState(action.cmsId, action.element, action.currentSrc, action.currentAlt, true)
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
color: {
|
|
199
|
+
applyReverse: (action) => {
|
|
173
200
|
scrollToElement(action.element)
|
|
174
201
|
applyColorState(action.cmsId, action.element, action.previousClassName, action.previousStyleCssText, action.previousClasses, action.wasDirty)
|
|
175
|
-
|
|
176
|
-
|
|
202
|
+
},
|
|
203
|
+
applyForward: (action) => {
|
|
177
204
|
scrollToElement(action.element)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function applyForward(action: UndoAction): void {
|
|
187
|
-
switch (action.type) {
|
|
188
|
-
case 'text':
|
|
205
|
+
applyColorState(action.cmsId, action.element, action.currentClassName, action.currentStyleCssText, action.currentClasses, true)
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
bgImage: {
|
|
209
|
+
applyReverse: (action) => {
|
|
189
210
|
scrollToElement(action.element)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
211
|
+
applyBgImageState(
|
|
212
|
+
action.cmsId,
|
|
213
|
+
action.element,
|
|
214
|
+
action.previousClassName,
|
|
215
|
+
action.previousStyleCssText,
|
|
216
|
+
action.previousBgImageClass,
|
|
217
|
+
action.previousBgSize,
|
|
218
|
+
action.previousBgPosition,
|
|
219
|
+
action.previousBgRepeat,
|
|
220
|
+
action.wasDirty,
|
|
221
|
+
)
|
|
222
|
+
},
|
|
223
|
+
applyForward: (action) => {
|
|
193
224
|
scrollToElement(action.element)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
225
|
+
applyBgImageState(
|
|
226
|
+
action.cmsId,
|
|
227
|
+
action.element,
|
|
228
|
+
action.currentClassName,
|
|
229
|
+
action.currentStyleCssText,
|
|
230
|
+
action.currentBgImageClass,
|
|
231
|
+
action.currentBgSize,
|
|
232
|
+
action.currentBgPosition,
|
|
233
|
+
action.currentBgRepeat,
|
|
234
|
+
true,
|
|
235
|
+
)
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
attribute: {
|
|
239
|
+
applyReverse: (action) => {
|
|
197
240
|
scrollToElement(action.element)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
241
|
+
applyAttributeState(action.cmsId, action.element, action.previousAttributes, action.wasDirty)
|
|
242
|
+
},
|
|
243
|
+
applyForward: (action) => {
|
|
201
244
|
scrollToElement(action.element)
|
|
202
245
|
applyAttributeState(action.cmsId, action.element, action.currentAttributes, true)
|
|
203
|
-
|
|
204
|
-
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
seo: {
|
|
249
|
+
applyReverse: (action) => {
|
|
250
|
+
applySeoState(action.cmsId, action.previousValue, action.originalValue, action.wasDirty)
|
|
251
|
+
},
|
|
252
|
+
applyForward: (action) => {
|
|
205
253
|
applySeoState(action.cmsId, action.currentValue, action.originalValue, true)
|
|
206
|
-
|
|
207
|
-
}
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function applyReverse(action: UndoAction): void {
|
|
259
|
+
const handlers = undoRegistry[action.type] as UndoHandlers[typeof action.type]
|
|
260
|
+
handlers.applyReverse(action as any)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function applyForward(action: UndoAction): void {
|
|
264
|
+
const handlers = undoRegistry[action.type] as UndoHandlers[typeof action.type]
|
|
265
|
+
handlers.applyForward(action as any)
|
|
208
266
|
}
|
|
209
267
|
|
|
210
268
|
// ============================================================================
|
|
@@ -323,6 +381,34 @@ function applySeoState(
|
|
|
323
381
|
})
|
|
324
382
|
}
|
|
325
383
|
|
|
384
|
+
function applyBgImageState(
|
|
385
|
+
cmsId: string,
|
|
386
|
+
element: HTMLElement,
|
|
387
|
+
className: string,
|
|
388
|
+
styleCssText: string,
|
|
389
|
+
bgImageClass: string,
|
|
390
|
+
bgSize: string,
|
|
391
|
+
bgPosition: string,
|
|
392
|
+
bgRepeat: string,
|
|
393
|
+
isDirty: boolean,
|
|
394
|
+
): void {
|
|
395
|
+
if (element.isConnected) {
|
|
396
|
+
element.className = className
|
|
397
|
+
element.style.cssText = styleCssText
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
signals.updatePendingBgImageChange(cmsId, (c) => ({
|
|
401
|
+
...c,
|
|
402
|
+
newBgImageClass: bgImageClass,
|
|
403
|
+
newBgSize: bgSize,
|
|
404
|
+
newBgPosition: bgPosition,
|
|
405
|
+
newBgRepeat: bgRepeat,
|
|
406
|
+
isDirty,
|
|
407
|
+
}))
|
|
408
|
+
|
|
409
|
+
saveBgImageEditsToStorage(signals.pendingBgImageChanges.value)
|
|
410
|
+
}
|
|
411
|
+
|
|
326
412
|
// ============================================================================
|
|
327
413
|
// Clear History
|
|
328
414
|
// ============================================================================
|
|
@@ -17,3 +17,6 @@ export type { TextSelectionState } from './useTextSelection'
|
|
|
17
17
|
|
|
18
18
|
export { useImageHoverDetection } from './useImageHoverDetection'
|
|
19
19
|
export type { ImageHoverState } from './useImageHoverDetection'
|
|
20
|
+
|
|
21
|
+
export { useBgImageHoverDetection } from './useBgImageHoverDetection'
|
|
22
|
+
export type { BgImageHoverState } from './useBgImageHoverDetection'
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
|
2
|
+
import { CSS, TIMING } from '../constants'
|
|
3
|
+
import * as signals from '../signals'
|
|
4
|
+
import { isEventOnCmsUI, usePositionTracking } from './utils'
|
|
5
|
+
|
|
6
|
+
export interface BgImageHoverState {
|
|
7
|
+
visible: boolean
|
|
8
|
+
rect: DOMRect | null
|
|
9
|
+
element: HTMLElement | null
|
|
10
|
+
cmsId: string | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const INITIAL_STATE: BgImageHoverState = {
|
|
14
|
+
visible: false,
|
|
15
|
+
rect: null,
|
|
16
|
+
element: null,
|
|
17
|
+
cmsId: null,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hook for detecting and tracking hovered CMS background image elements.
|
|
22
|
+
* Shows a visual overlay when hovering over elements marked with data-cms-bg-img.
|
|
23
|
+
*/
|
|
24
|
+
export function useBgImageHoverDetection(): BgImageHoverState {
|
|
25
|
+
const [bgImageHoverState, setBgImageHoverState] = useState<BgImageHoverState>(INITIAL_STATE)
|
|
26
|
+
|
|
27
|
+
// Throttle ref for element detection
|
|
28
|
+
const lastDetectionTime = useRef<number>(0)
|
|
29
|
+
|
|
30
|
+
// Handle position updates on scroll/resize
|
|
31
|
+
const handlePositionChange = useCallback((rect: DOMRect | null) => {
|
|
32
|
+
if (rect) {
|
|
33
|
+
setBgImageHoverState(prev => ({ ...prev, rect }))
|
|
34
|
+
} else {
|
|
35
|
+
setBgImageHoverState(INITIAL_STATE)
|
|
36
|
+
}
|
|
37
|
+
}, [])
|
|
38
|
+
|
|
39
|
+
// Track element position on scroll/resize
|
|
40
|
+
usePositionTracking(
|
|
41
|
+
bgImageHoverState.element,
|
|
42
|
+
handlePositionChange,
|
|
43
|
+
bgImageHoverState.visible,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
// Setup hover detection for background image elements
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const handleMouseMove = (ev: MouseEvent) => {
|
|
49
|
+
const isEditing = signals.isEditing.value
|
|
50
|
+
|
|
51
|
+
if (!isEditing) {
|
|
52
|
+
setBgImageHoverState(prev => prev.visible ? INITIAL_STATE : prev)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if hovering over CMS UI - keep current state
|
|
57
|
+
if (isEventOnCmsUI(ev)) {
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Throttle detection for performance
|
|
62
|
+
const now = Date.now()
|
|
63
|
+
if (now - lastDetectionTime.current < TIMING.ELEMENT_DETECTION_THROTTLE_MS) {
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
lastDetectionTime.current = now
|
|
67
|
+
|
|
68
|
+
// Check if hovering over an element with data-cms-bg-img attribute
|
|
69
|
+
const elements = document.elementsFromPoint(ev.clientX, ev.clientY)
|
|
70
|
+
|
|
71
|
+
for (const el of elements) {
|
|
72
|
+
// If there's a contentEditable element above, don't show overlay
|
|
73
|
+
if (el instanceof HTMLElement && el.contentEditable === 'true') {
|
|
74
|
+
setBgImageHoverState(INITIAL_STATE)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (el instanceof HTMLElement && el.hasAttribute(CSS.BG_IMAGE_ATTRIBUTE)) {
|
|
79
|
+
const cmsId = el.getAttribute(CSS.ID_ATTRIBUTE)
|
|
80
|
+
const rect = el.getBoundingClientRect()
|
|
81
|
+
|
|
82
|
+
setBgImageHoverState({
|
|
83
|
+
visible: true,
|
|
84
|
+
rect,
|
|
85
|
+
element: el,
|
|
86
|
+
cmsId,
|
|
87
|
+
})
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// No bg image element found, hide overlay
|
|
93
|
+
setBgImageHoverState(INITIAL_STATE)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
document.addEventListener('mousemove', handleMouseMove, true)
|
|
97
|
+
return () => document.removeEventListener('mousemove', handleMouseMove, true)
|
|
98
|
+
}, [])
|
|
99
|
+
|
|
100
|
+
return bgImageHoverState
|
|
101
|
+
}
|
package/src/editor/index.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { fetchManifest } from './api'
|
|
|
4
4
|
import { AIChat } from './components/ai-chat'
|
|
5
5
|
import { AITooltip } from './components/ai-tooltip'
|
|
6
6
|
import { AttributeEditor } from './components/attribute-editor'
|
|
7
|
+
import { BgImageOverlay } from './components/bg-image-overlay'
|
|
7
8
|
import { BlockEditor } from './components/block-editor'
|
|
8
9
|
import { CollectionsBrowser } from './components/collections-browser'
|
|
9
10
|
import { ColorToolbar } from './components/color-toolbar'
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
import { performRedo, performUndo } from './history'
|
|
35
36
|
import {
|
|
36
37
|
useAIHandlers,
|
|
38
|
+
useBgImageHoverDetection,
|
|
37
39
|
useBlockEditorHandlers,
|
|
38
40
|
useComponentClickHandler,
|
|
39
41
|
useElementDetection,
|
|
@@ -79,6 +81,7 @@ const CmsUI = () => {
|
|
|
79
81
|
const config = signals.config.value
|
|
80
82
|
const outlineState = useElementDetection()
|
|
81
83
|
const imageHoverState = useImageHoverDetection()
|
|
84
|
+
const bgImageHoverState = useBgImageHoverDetection()
|
|
82
85
|
const textSelectionState = useTextSelection()
|
|
83
86
|
const { tooltipState, showTooltipForElement, hideTooltip } = useTooltipState()
|
|
84
87
|
const updateUI = useCallback(() => {
|
|
@@ -365,6 +368,15 @@ const CmsUI = () => {
|
|
|
365
368
|
/>
|
|
366
369
|
</ErrorBoundary>
|
|
367
370
|
|
|
371
|
+
<ErrorBoundary componentName="BgImageOverlay">
|
|
372
|
+
<BgImageOverlay
|
|
373
|
+
visible={bgImageHoverState.visible && isEditing}
|
|
374
|
+
rect={bgImageHoverState.rect}
|
|
375
|
+
element={bgImageHoverState.element}
|
|
376
|
+
cmsId={bgImageHoverState.cmsId}
|
|
377
|
+
/>
|
|
378
|
+
</ErrorBoundary>
|
|
379
|
+
|
|
368
380
|
<ErrorBoundary componentName="Toolbar">
|
|
369
381
|
<Toolbar
|
|
370
382
|
callbacks={{
|