@nuasite/cms 0.8.0 → 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 +1 -1
- package/dist/editor.js +4760 -4685
- package/package.json +1 -1
- package/src/editor/components/bg-image-overlay.tsx +27 -0
- package/src/editor/editor.ts +5 -7
- package/src/editor/history.ts +120 -34
- package/src/editor/signals.ts +31 -16
- package/src/editor/types.ts +27 -7
- package/src/handlers/source-writer.ts +31 -55
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
|
2
2
|
import { Z_INDEX } from '../constants'
|
|
3
|
+
import { isApplyingUndoRedo, recordChange } from '../history'
|
|
3
4
|
import { cn } from '../lib/cn'
|
|
4
5
|
import * as signals from '../signals'
|
|
5
6
|
import { saveBgImageEditsToStorage } from '../storage'
|
|
@@ -410,6 +411,10 @@ function applyBgImageUpdate(
|
|
|
410
411
|
const change = signals.getPendingBgImageChange(cmsId)
|
|
411
412
|
if (!change) return
|
|
412
413
|
|
|
414
|
+
// Capture pre-mutation state for undo
|
|
415
|
+
const previousClassName = element.className
|
|
416
|
+
const previousStyleCssText = element.style.cssText
|
|
417
|
+
|
|
413
418
|
const newChange = { ...change }
|
|
414
419
|
|
|
415
420
|
// Apply bg image class change
|
|
@@ -451,6 +456,28 @@ function applyBgImageUpdate(
|
|
|
451
456
|
|| newChange.newBgPosition !== newChange.originalBgPosition
|
|
452
457
|
|| newChange.newBgRepeat !== newChange.originalBgRepeat
|
|
453
458
|
|
|
459
|
+
// Record undo action after DOM is mutated
|
|
460
|
+
if (!isApplyingUndoRedo) {
|
|
461
|
+
recordChange({
|
|
462
|
+
type: 'bgImage',
|
|
463
|
+
cmsId,
|
|
464
|
+
element,
|
|
465
|
+
previousClassName,
|
|
466
|
+
currentClassName: element.className,
|
|
467
|
+
previousStyleCssText,
|
|
468
|
+
currentStyleCssText: element.style.cssText,
|
|
469
|
+
previousBgImageClass: change.newBgImageClass,
|
|
470
|
+
currentBgImageClass: newChange.newBgImageClass,
|
|
471
|
+
previousBgSize: change.newBgSize,
|
|
472
|
+
currentBgSize: newChange.newBgSize,
|
|
473
|
+
previousBgPosition: change.newBgPosition,
|
|
474
|
+
currentBgPosition: newChange.newBgPosition,
|
|
475
|
+
previousBgRepeat: change.newBgRepeat,
|
|
476
|
+
currentBgRepeat: newChange.newBgRepeat,
|
|
477
|
+
wasDirty: change.isDirty,
|
|
478
|
+
})
|
|
479
|
+
}
|
|
480
|
+
|
|
454
481
|
signals.setPendingBgImageChange(cmsId, newChange)
|
|
455
482
|
saveBgImageEditsToStorage(signals.pendingBgImageChanges.value)
|
|
456
483
|
}
|
package/src/editor/editor.ts
CHANGED
|
@@ -549,11 +549,9 @@ export function discardAllChanges(onStateChange?: () => void): void {
|
|
|
549
549
|
})
|
|
550
550
|
|
|
551
551
|
cleanupHighlightSystem()
|
|
552
|
-
signals.
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
signals.clearPendingBgImageChanges()
|
|
556
|
-
signals.clearPendingAttributeChanges()
|
|
552
|
+
for (const entry of Object.values(signals.changeRegistry)) {
|
|
553
|
+
entry.mapSignal.value = new Map()
|
|
554
|
+
}
|
|
557
555
|
clearAllEditsFromStorage()
|
|
558
556
|
clearHistory()
|
|
559
557
|
stopEditMode(onStateChange)
|
|
@@ -701,7 +699,7 @@ export async function saveAllChanges(
|
|
|
701
699
|
sourcePath: bestSourcePath ?? entry?.sourcePath ?? '',
|
|
702
700
|
sourceLine: bestSourceLine ?? entry?.sourceLine ?? 0,
|
|
703
701
|
sourceSnippet: bestSourceSnippet ?? entry?.sourceSnippet ?? '',
|
|
704
|
-
|
|
702
|
+
styleChange: {
|
|
705
703
|
oldClass: origAttr?.value || '',
|
|
706
704
|
newClass: newAttr.value,
|
|
707
705
|
type: classType,
|
|
@@ -740,7 +738,7 @@ export async function saveAllChanges(
|
|
|
740
738
|
sourcePath: entry?.sourcePath ?? '',
|
|
741
739
|
sourceLine: entry?.sourceLine ?? 0,
|
|
742
740
|
sourceSnippet: entry?.sourceSnippet ?? '',
|
|
743
|
-
|
|
741
|
+
styleChange: {
|
|
744
742
|
oldClass: bgChange.oldClass,
|
|
745
743
|
newClass: bgChange.newClass,
|
|
746
744
|
type: bgChange.type,
|
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
|
// ============================================================================
|
package/src/editor/signals.ts
CHANGED
|
@@ -496,6 +496,16 @@ let toastIdCounter = 0
|
|
|
496
496
|
// Computed Values - Dirty Tracking
|
|
497
497
|
// ============================================================================
|
|
498
498
|
|
|
499
|
+
/** All change categories that participate in dirty tracking.
|
|
500
|
+
* Adding a new category forces a TypeScript error until it's added to changeRegistry. */
|
|
501
|
+
type ChangeCategory = 'text' | 'image' | 'color' | 'bgImage' | 'seo' | 'attribute'
|
|
502
|
+
|
|
503
|
+
interface ChangeRegistryEntry {
|
|
504
|
+
mapSignal: Signal<Map<string, any>>
|
|
505
|
+
dirtyCount: ReturnType<typeof computed<number>>
|
|
506
|
+
hasDirty: ReturnType<typeof computed<boolean>>
|
|
507
|
+
}
|
|
508
|
+
|
|
499
509
|
// Use factory for dirty tracking to reduce duplication
|
|
500
510
|
const _pendingChangesDirty = createDirtyTracking(pendingChanges)
|
|
501
511
|
const _pendingImageChangesDirty = createDirtyTracking(pendingImageChanges)
|
|
@@ -504,6 +514,19 @@ const _pendingBgImageChangesDirty = createDirtyTracking(pendingBgImageChanges)
|
|
|
504
514
|
const _pendingSeoChangesDirty = createDirtyTracking(pendingSeoChanges)
|
|
505
515
|
const _pendingAttributeChangesDirty = createDirtyTracking(pendingAttributeChanges)
|
|
506
516
|
|
|
517
|
+
export const changeRegistry: Record<ChangeCategory, ChangeRegistryEntry> = {
|
|
518
|
+
text: { mapSignal: pendingChanges, dirtyCount: _pendingChangesDirty.dirtyCount, hasDirty: _pendingChangesDirty.hasDirty },
|
|
519
|
+
image: { mapSignal: pendingImageChanges, dirtyCount: _pendingImageChangesDirty.dirtyCount, hasDirty: _pendingImageChangesDirty.hasDirty },
|
|
520
|
+
color: { mapSignal: pendingColorChanges, dirtyCount: _pendingColorChangesDirty.dirtyCount, hasDirty: _pendingColorChangesDirty.hasDirty },
|
|
521
|
+
bgImage: { mapSignal: pendingBgImageChanges, dirtyCount: _pendingBgImageChangesDirty.dirtyCount, hasDirty: _pendingBgImageChangesDirty.hasDirty },
|
|
522
|
+
seo: { mapSignal: pendingSeoChanges, dirtyCount: _pendingSeoChangesDirty.dirtyCount, hasDirty: _pendingSeoChangesDirty.hasDirty },
|
|
523
|
+
attribute: {
|
|
524
|
+
mapSignal: pendingAttributeChanges,
|
|
525
|
+
dirtyCount: _pendingAttributeChangesDirty.dirtyCount,
|
|
526
|
+
hasDirty: _pendingAttributeChangesDirty.hasDirty,
|
|
527
|
+
},
|
|
528
|
+
}
|
|
529
|
+
|
|
507
530
|
export const dirtyChangesCount = _pendingChangesDirty.dirtyCount
|
|
508
531
|
export const dirtyChanges = _pendingChangesDirty.dirtyItems
|
|
509
532
|
export const hasDirtyChanges = _pendingChangesDirty.hasDirty
|
|
@@ -528,17 +551,11 @@ export const dirtyAttributeChangesCount = _pendingAttributeChangesDirty.dirtyCou
|
|
|
528
551
|
export const dirtyAttributeChanges = _pendingAttributeChangesDirty.dirtyItems
|
|
529
552
|
export const hasDirtyAttributeChanges = _pendingAttributeChangesDirty.hasDirty
|
|
530
553
|
|
|
531
|
-
|
|
532
|
-
() =>
|
|
533
|
-
dirtyChangesCount.value + dirtyImageChangesCount.value + dirtyColorChangesCount.value + dirtyBgImageChangesCount.value + dirtySeoChangesCount.value
|
|
534
|
-
+ dirtyAttributeChangesCount.value,
|
|
535
|
-
)
|
|
554
|
+
const _registryEntries = Object.values(changeRegistry)
|
|
536
555
|
|
|
537
|
-
export const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|| hasDirtyAttributeChanges.value,
|
|
541
|
-
)
|
|
556
|
+
export const totalDirtyCount = computed(() => _registryEntries.reduce((sum, entry) => sum + entry.dirtyCount.value, 0))
|
|
557
|
+
|
|
558
|
+
export const hasAnyDirtyChanges = computed(() => _registryEntries.some((entry) => entry.hasDirty.value))
|
|
542
559
|
|
|
543
560
|
// Navigation index for cycling through dirty elements
|
|
544
561
|
export const changeNavigationIndex = signal<number>(0)
|
|
@@ -1345,14 +1362,12 @@ export function resetAllState(): void {
|
|
|
1345
1362
|
showingOriginal.value = false
|
|
1346
1363
|
currentEditingId.value = null
|
|
1347
1364
|
currentComponentId.value = null
|
|
1348
|
-
|
|
1365
|
+
for (const entry of Object.values(changeRegistry)) {
|
|
1366
|
+
entry.mapSignal.value = new Map()
|
|
1367
|
+
}
|
|
1368
|
+
// Non-dirty-tracked maps still cleared individually
|
|
1349
1369
|
pendingComponentChanges.value = new Map()
|
|
1350
1370
|
pendingInserts.value = new Map()
|
|
1351
|
-
pendingImageChanges.value = new Map()
|
|
1352
|
-
pendingColorChanges.value = new Map()
|
|
1353
|
-
pendingBgImageChanges.value = new Map()
|
|
1354
|
-
pendingSeoChanges.value = new Map()
|
|
1355
|
-
pendingAttributeChanges.value = new Map()
|
|
1356
1371
|
manifest.value = { entries: {}, components: {}, componentDefinitions: {} }
|
|
1357
1372
|
aiState.value = createInitialAIState()
|
|
1358
1373
|
blockEditorState.value = createInitialBlockEditorState()
|
package/src/editor/types.ts
CHANGED
|
@@ -170,13 +170,13 @@ export interface SavedBackgroundImageEdits {
|
|
|
170
170
|
[cmsId: string]: SavedBackgroundImageEdit
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
/**
|
|
174
|
-
export interface
|
|
175
|
-
/** The
|
|
173
|
+
/** Style change details for updating element classes (colors, text styles, bg images) */
|
|
174
|
+
export interface StyleChangePayload {
|
|
175
|
+
/** The class to replace (e.g., 'bg-blue-500') */
|
|
176
176
|
oldClass: string
|
|
177
|
-
/** The new
|
|
177
|
+
/** The new class (e.g., 'bg-red-500') */
|
|
178
178
|
newClass: string
|
|
179
|
-
/** Type of
|
|
179
|
+
/** Type of style change */
|
|
180
180
|
type:
|
|
181
181
|
| 'bg'
|
|
182
182
|
| 'text'
|
|
@@ -220,8 +220,8 @@ export interface ChangePayload {
|
|
|
220
220
|
newSrc: string
|
|
221
221
|
newAlt?: string
|
|
222
222
|
}
|
|
223
|
-
/**
|
|
224
|
-
|
|
223
|
+
/** Style class change (colors, text styles, bg images) */
|
|
224
|
+
styleChange?: StyleChangePayload
|
|
225
225
|
/** Attribute changes (for links, forms, etc.) */
|
|
226
226
|
attributeChanges?: AttributeChangePayload[]
|
|
227
227
|
}
|
|
@@ -630,12 +630,32 @@ export interface UndoSeoAction {
|
|
|
630
630
|
wasDirty: boolean
|
|
631
631
|
}
|
|
632
632
|
|
|
633
|
+
export interface UndoBgImageAction {
|
|
634
|
+
type: 'bgImage'
|
|
635
|
+
cmsId: string
|
|
636
|
+
element: HTMLElement
|
|
637
|
+
previousClassName: string
|
|
638
|
+
currentClassName: string
|
|
639
|
+
previousStyleCssText: string
|
|
640
|
+
currentStyleCssText: string
|
|
641
|
+
previousBgImageClass: string
|
|
642
|
+
currentBgImageClass: string
|
|
643
|
+
previousBgSize: string
|
|
644
|
+
currentBgSize: string
|
|
645
|
+
previousBgPosition: string
|
|
646
|
+
currentBgPosition: string
|
|
647
|
+
previousBgRepeat: string
|
|
648
|
+
currentBgRepeat: string
|
|
649
|
+
wasDirty: boolean
|
|
650
|
+
}
|
|
651
|
+
|
|
633
652
|
export type UndoAction =
|
|
634
653
|
| UndoTextAction
|
|
635
654
|
| UndoImageAction
|
|
636
655
|
| UndoColorAction
|
|
637
656
|
| UndoAttributeAction
|
|
638
657
|
| UndoSeoAction
|
|
658
|
+
| UndoBgImageAction
|
|
639
659
|
|
|
640
660
|
declare global {
|
|
641
661
|
interface Window {
|
|
@@ -1,53 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs/promises'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { getProjectRoot } from '../config'
|
|
4
|
+
import type { AttributeChangePayload, ChangePayload, SaveBatchRequest } from '../editor/types'
|
|
4
5
|
import type { ManifestWriter } from '../manifest-writer'
|
|
5
6
|
import type { CmsManifest, ManifestEntry } from '../types'
|
|
6
7
|
import { acquireFileLock, escapeReplacement, normalizePagePath, resolveAndValidatePath } from '../utils'
|
|
7
8
|
|
|
8
|
-
export interface ColorChangePayload {
|
|
9
|
-
oldClass: string
|
|
10
|
-
newClass: string
|
|
11
|
-
type: 'bg' | 'text' | 'border' | 'hoverBg' | 'hoverText' | 'fontWeight' | 'fontStyle' | 'textDecoration' | 'fontSize'
|
|
12
|
-
sourcePath?: string
|
|
13
|
-
sourceLine?: number
|
|
14
|
-
sourceSnippet?: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface ImageChangePayload {
|
|
18
|
-
newSrc: string
|
|
19
|
-
newAlt: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface AttributeChangePayload {
|
|
23
|
-
attributeName: string
|
|
24
|
-
oldValue: string | undefined
|
|
25
|
-
newValue: string | undefined
|
|
26
|
-
sourcePath?: string
|
|
27
|
-
sourceLine?: number
|
|
28
|
-
sourceSnippet?: string
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface ChangePayload {
|
|
32
|
-
cmsId: string
|
|
33
|
-
newValue: string
|
|
34
|
-
originalValue: string
|
|
35
|
-
sourcePath: string
|
|
36
|
-
sourceLine: number
|
|
37
|
-
sourceSnippet: string
|
|
38
|
-
htmlValue?: string
|
|
39
|
-
childCmsIds?: string[]
|
|
40
|
-
hasStyledContent?: boolean
|
|
41
|
-
colorChange?: ColorChangePayload
|
|
42
|
-
imageChange?: ImageChangePayload
|
|
43
|
-
attributeChanges?: AttributeChangePayload[]
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface SaveBatchRequest {
|
|
47
|
-
changes: ChangePayload[]
|
|
48
|
-
meta: { source: string; url: string }
|
|
49
|
-
}
|
|
50
|
-
|
|
51
9
|
export interface SaveBatchResponse {
|
|
52
10
|
updated: number
|
|
53
11
|
errors?: Array<{ cmsId: string; error: string }>
|
|
@@ -157,8 +115,8 @@ function applyChanges(
|
|
|
157
115
|
continue
|
|
158
116
|
}
|
|
159
117
|
|
|
160
|
-
// Handle
|
|
161
|
-
if (change.
|
|
118
|
+
// Handle style class changes (colors, text styles, bg images)
|
|
119
|
+
if (change.styleChange) {
|
|
162
120
|
const result = applyColorChange(newContent, change)
|
|
163
121
|
if (result.success) {
|
|
164
122
|
newContent = result.content
|
|
@@ -323,10 +281,10 @@ function applyColorChange(
|
|
|
323
281
|
content: string,
|
|
324
282
|
change: ChangePayload,
|
|
325
283
|
): { success: true; content: string } | { success: false; error: string } {
|
|
326
|
-
const { oldClass, newClass } = change.
|
|
327
|
-
// Prefer
|
|
284
|
+
const { oldClass, newClass } = change.styleChange!
|
|
285
|
+
// Prefer styleChange's own sourceLine (points to the class attribute)
|
|
328
286
|
// over the outer change.sourceLine (may point to a data declaration)
|
|
329
|
-
const sourceLine = change.
|
|
287
|
+
const sourceLine = change.styleChange!.sourceLine ?? change.sourceLine
|
|
330
288
|
|
|
331
289
|
// When oldClass is empty, we're adding a new color class (not replacing)
|
|
332
290
|
if (!oldClass) {
|
|
@@ -536,7 +494,7 @@ function applyAttributeChanges(
|
|
|
536
494
|
return { content: newContent, appliedCount: attrApplied, failedChanges }
|
|
537
495
|
}
|
|
538
496
|
|
|
539
|
-
function applyTextChange(
|
|
497
|
+
export function applyTextChange(
|
|
540
498
|
content: string,
|
|
541
499
|
change: ChangePayload,
|
|
542
500
|
manifest: CmsManifest,
|
|
@@ -546,7 +504,14 @@ function applyTextChange(
|
|
|
546
504
|
let newText = htmlValue ?? newValue
|
|
547
505
|
newText = resolveCmsPlaceholders(newText, manifest)
|
|
548
506
|
|
|
549
|
-
|
|
507
|
+
// Resolve CMS placeholders in originalValue too — when a parent element has
|
|
508
|
+
// child CMS elements, originalValue contains {{cms:cms-N}} placeholders but
|
|
509
|
+
// the sourceSnippet contains the actual HTML for those children.
|
|
510
|
+
const resolvedOriginal = originalValue
|
|
511
|
+
? resolveCmsPlaceholders(originalValue, manifest)
|
|
512
|
+
: originalValue
|
|
513
|
+
|
|
514
|
+
if (!sourceSnippet || !resolvedOriginal) {
|
|
550
515
|
if (change.attributeChanges && change.attributeChanges.length > 0) {
|
|
551
516
|
return { success: true, content }
|
|
552
517
|
}
|
|
@@ -557,19 +522,30 @@ function applyTextChange(
|
|
|
557
522
|
return { success: false, error: 'Source snippet not found in file' }
|
|
558
523
|
}
|
|
559
524
|
|
|
560
|
-
// Replace
|
|
561
|
-
const updatedSnippet = sourceSnippet.replace(
|
|
525
|
+
// Replace resolvedOriginal with newText WITHIN the sourceSnippet
|
|
526
|
+
const updatedSnippet = sourceSnippet.replace(resolvedOriginal, newText)
|
|
562
527
|
|
|
563
528
|
if (updatedSnippet === sourceSnippet) {
|
|
564
|
-
//
|
|
565
|
-
const matchedText = findTextInSnippet(sourceSnippet,
|
|
529
|
+
// resolvedOriginal wasn't found in snippet - try HTML entity handling
|
|
530
|
+
const matchedText = findTextInSnippet(sourceSnippet, resolvedOriginal)
|
|
566
531
|
if (matchedText) {
|
|
567
532
|
const updatedWithEntity = sourceSnippet.replace(matchedText, newText)
|
|
568
533
|
return { success: true, content: content.replace(sourceSnippet, updatedWithEntity) }
|
|
569
534
|
}
|
|
535
|
+
// Try inner content replacement for text spanning inline HTML elements
|
|
536
|
+
// (e.g., <h3>text part 1 <span class="...">text part 2</span></h3>)
|
|
537
|
+
const innerMatch = sourceSnippet.match(/^(\s*<(\w+)\b[^>]*>)([\s\S]*)(<\/\2>\s*)$/)
|
|
538
|
+
if (innerMatch) {
|
|
539
|
+
const [, openTag, , innerContent, closeTag] = innerMatch
|
|
540
|
+
const textOnly = innerContent!.replace(/<[^>]+>/g, '')
|
|
541
|
+
if (textOnly === resolvedOriginal) {
|
|
542
|
+
return { success: true, content: content.replace(sourceSnippet, openTag + newText + closeTag) }
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
570
546
|
return {
|
|
571
547
|
success: false,
|
|
572
|
-
error: `Original text "${
|
|
548
|
+
error: `Original text "${resolvedOriginal.substring(0, 50)}..." not found in source snippet`,
|
|
573
549
|
}
|
|
574
550
|
}
|
|
575
551
|
|