@nuasite/cms 0.31.0 → 0.32.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/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "directory": "packages/astro-cms"
15
15
  },
16
16
  "license": "Apache-2.0",
17
- "version": "0.31.0",
17
+ "version": "0.32.0",
18
18
  "module": "src/index.ts",
19
19
  "types": "src/index.ts",
20
20
  "type": "module",
@@ -109,6 +109,9 @@ export const CSS = {
109
109
  HIGHLIGHT_ELEMENT: 'cms-highlight-overlay',
110
110
  /** Data attribute for background image elements */
111
111
  BG_IMAGE_ATTRIBUTE: 'data-cms-bg-img',
112
+ /** Data attribute set during edit mode on elements whose manifest entry has no source path;
113
+ * suppresses hover outlines and blocks interaction so the user doesn't type into a dead-end. */
114
+ LOCKED_ATTRIBUTE: 'data-cms-locked',
112
115
  } as const
113
116
 
114
117
  /**
package/src/editor/dom.ts CHANGED
@@ -76,6 +76,8 @@ export function getCmsElementAtPosition(
76
76
  if (!el.hasAttribute(CSS.ID_ATTRIBUTE)) continue
77
77
  // Skip component roots - they should be handled separately
78
78
  if (el.hasAttribute(CSS.COMPONENT_ID_ATTRIBUTE)) continue
79
+ // Skip elements locked because their manifest entry has no source path
80
+ if (el.hasAttribute(CSS.LOCKED_ATTRIBUTE)) continue
79
81
 
80
82
  const cmsId = el.getAttribute(CSS.ID_ATTRIBUTE)
81
83
 
@@ -96,6 +98,7 @@ export function getCmsElementAtPosition(
96
98
  if (!(el instanceof HTMLElement)) continue
97
99
  if (!el.hasAttribute(CSS.ID_ATTRIBUTE)) continue
98
100
  if (el.hasAttribute(CSS.COMPONENT_ID_ATTRIBUTE)) continue
101
+ if (el.hasAttribute(CSS.LOCKED_ATTRIBUTE)) continue
99
102
 
100
103
  const cmsId = el.getAttribute(CSS.ID_ATTRIBUTE)
101
104
 
@@ -76,6 +76,7 @@ const INLINE_STYLE_ELEMENTS = [
76
76
  // merge a burst, short enough that the next deliberate action still explains itself.
77
77
  const FORMATTING_BLOCKED_TOAST_COOLDOWN_MS = 3000
78
78
  let lastFormattingBlockedToastAt = 0
79
+ let lastLockedToastAt = 0
79
80
 
80
81
  // Signals listener cleanup on stopEditMode. Aborting removes every listener
81
82
  // attached with { signal } in the current edit session in one shot.
@@ -90,6 +91,21 @@ function notifyFormattingBlocked(): void {
90
91
  signals.showToast("Formatting isn't available — this text is used as a plain value", 'info')
91
92
  }
92
93
 
94
+ export function notifyLockedElement(): void {
95
+ const now = Date.now()
96
+ if (now - lastLockedToastAt < FORMATTING_BLOCKED_TOAST_COOLDOWN_MS) {
97
+ return
98
+ }
99
+ lastLockedToastAt = now
100
+ signals.showToast("This text can't be edited here — no source file is linked to it", 'info')
101
+ }
102
+
103
+ /** Test-only: reset toast throttle state between test cases. */
104
+ export function _resetToastThrottles(): void {
105
+ lastFormattingBlockedToastAt = 0
106
+ lastLockedToastAt = 0
107
+ }
108
+
93
109
  // Uses the Selection/Range API rather than the deprecated document.execCommand('insertText').
94
110
  export function insertPlainTextAtRange(range: Range, text: string): boolean {
95
111
  if (!text) return false
@@ -224,6 +240,16 @@ export async function startEditMode(
224
240
  return
225
241
  }
226
242
 
243
+ // Without a source path, the writer has nowhere to persist text edits — lock
244
+ // the element so it can't be typed into and the user gets told why on click.
245
+ if (!manifestEntry?.sourcePath) {
246
+ logDebug(config.debug, 'Skipping element without source path:', cmsId)
247
+ makeElementNonEditable(el)
248
+ el.setAttribute(CSS.LOCKED_ATTRIBUTE, 'true')
249
+ el.addEventListener('click', notifyLockedElement, { signal: editModeSignal })
250
+ return
251
+ }
252
+
227
253
  makeElementEditable(el)
228
254
 
229
255
  const stylingAllowed = manifestEntry?.allowStyling !== false
@@ -434,6 +460,7 @@ export function stopEditMode(onStateChange?: () => void): void {
434
460
 
435
461
  getAllCmsElements().forEach(el => {
436
462
  makeElementNonEditable(el)
463
+ el.removeAttribute(CSS.LOCKED_ATTRIBUTE)
437
464
  })
438
465
  }
439
466
 
@@ -65,10 +65,24 @@ const toISODate = (v: unknown) => (v instanceof Date ? v.toISOString().slice(0,
65
65
  /** Normalize YAML Date objects to ISO datetime strings */
66
66
  const toISODatetime = (v: unknown) => (v instanceof Date ? v.toISOString() : v)
67
67
 
68
- /** Add a chainable `.orderBy()` method to a Zod schema. The scanner detects it from source code. */
68
+ const WRAPPING_METHODS = ['default', 'optional', 'nullable', 'nullish'] as const
69
+
70
+ /**
71
+ * Add a chainable `.orderBy()` method to a Zod schema. The scanner detects it from source code.
72
+ *
73
+ * Also wraps the Zod methods that produce a new schema instance (`.default()`, `.optional()`,
74
+ * `.nullable()`, `.nullish()`) so the marker survives those wrappers — chain order doesn't matter.
75
+ */
69
76
  function withOrderBy<T extends z.ZodTypeAny>(schema: T): WithOrderBy<T> {
70
77
  const s = schema as WithOrderBy<T>
71
78
  s.orderBy = (_direction?: OrderByDirection) => schema
79
+ for (const method of WRAPPING_METHODS) {
80
+ const original = (schema as unknown as Record<string, unknown>)[method]
81
+ if (typeof original !== 'function') continue
82
+ ;(s as unknown as Record<string, unknown>)[method] = function(this: unknown, ...args: unknown[]) {
83
+ return withOrderBy((original as (...a: unknown[]) => z.ZodTypeAny).apply(this, args))
84
+ }
85
+ }
72
86
  return s
73
87
  }
74
88