From 60f1a9110c9ba5e1613d1bff4dd3aa7aa6b010d1 Mon Sep 17 00:00:00 2001 From: Itay <102981181+N0S0I@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:23:22 +0200 Subject: [PATCH 1/4] Update ve-editable-text.js --- .../Frontend/components/ve-editable-text.js | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js b/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js index d7c7596..add0c1e 100644 --- a/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js +++ b/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js @@ -105,9 +105,10 @@ export class VeEditableText extends LitElement { firstUpdated(changedProperties) { this.placeholder = this.name; this.skipNextValueNormalization = true; - this.#setSlotText(this.valueInitial); - dataHandlerStore.setInitialData(this.table, this.uid, this.field, this.valueInitial); - this.#applyValidationState(this.valueInitial); + const normalizedInitialValue = this.#htmlEntitiesToText(this.valueInitial); + this.#setSlotText(normalizedInitialValue); + dataHandlerStore.setInitialData(this.table, this.uid, this.field, normalizedInitialValue); + this.#applyValidationState(normalizedInitialValue); } updated(changedProperties) { @@ -134,11 +135,11 @@ export class VeEditableText extends LitElement { } onReset = () => { - this.value = this.valueInitial; - this.#setSlotText(this.valueInitial); + this.value = this.#htmlEntitiesToText(this.valueInitial); + this.#setSlotText(this.value); this.skipNextValueNormalization = true; - this.#applyValidationState(this.valueInitial); - dataHandlerStore.setData(this.table, this.uid, this.field, this.valueInitial); + this.#applyValidationState(this.value); + dataHandlerStore.setData(this.table, this.uid, this.field, this.value); }; /** @@ -350,7 +351,7 @@ export class VeEditableText extends LitElement { const isFocused = this.matches(':focus-within'); if (!isFocused && storedValue?.trim() !== slot?.innerText?.trim()) { this.skipNextValueNormalization = true; - this.value = storedValue ?? this.value; + this.value = this.#htmlEntitiesToText(storedValue ?? this.value); this.#setSlotText(this.value); } } @@ -373,6 +374,18 @@ export class VeEditableText extends LitElement { return this.shadowRoot?.querySelector('.slot'); } + #htmlEntitiesToText(value) { + return String(value ?? '') + .replace(/­/gi, '\u00ad') + .replace(/ /gi, '\u00a0'); + } + + #textToHtmlEntities(value) { + return String(value ?? '') + .replace(/\u00ad/g, '­') + .replace(/\u00a0/g, ' '); + } + /** * @param {HTMLElement} element * @param {InputEvent} event @@ -432,11 +445,12 @@ export class VeEditableText extends LitElement { #handleFocus() { this.focused = true; + this.#setSlotText(this.#textToHtmlEntities(this.value)); } #handleBlur() { this.focused = false; - this.#setSlotText(this.#validateAndStore(this.#getSlotText())); + this.#setSlotText(this.#htmlEntitiesToText(this.#validateAndStore(this.#getSlotText()))); } /** @@ -490,10 +504,10 @@ export class VeEditableText extends LitElement { * @returns {string} */ #validateAndStore(value) { - this.value = value; - this.#applyValidationState(value); + const normalizedText = this.#htmlEntitiesToText(value); + this.#applyValidationState(normalizedText); - let normalizedValue = normalizeValue(value, this.validation).text; + let normalizedValue = normalizeValue(normalizedText, this.validation).text; const min = Number(this.validation?.min || 0); if (normalizedValue.length < min && !this.isRequired()) { @@ -505,6 +519,7 @@ export class VeEditableText extends LitElement { normalizedValue = normalizedValue.slice(0, max); } + this.value = normalizedValue; dataHandlerStore.setData(this.table, this.uid, this.field, normalizedValue); return normalizedValue; } From ef7e624e187115c5d76d6e2f9812f600c0b8474f Mon Sep 17 00:00:00 2001 From: Itay <102981181+N0S0I@users.noreply.github.com> Date: Mon, 8 Jun 2026 09:11:03 +0200 Subject: [PATCH 2/4] Distinguish plain-text ­ from soft-hyphen --- .../Frontend/components/ve-editable-text.js | 48 +++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js b/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js index add0c1e..513af04 100644 --- a/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js +++ b/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js @@ -105,10 +105,10 @@ export class VeEditableText extends LitElement { firstUpdated(changedProperties) { this.placeholder = this.name; this.skipNextValueNormalization = true; - const normalizedInitialValue = this.#htmlEntitiesToText(this.valueInitial); - this.#setSlotText(normalizedInitialValue); - dataHandlerStore.setInitialData(this.table, this.uid, this.field, normalizedInitialValue); - this.#applyValidationState(normalizedInitialValue); + const initialValue = String(this.valueInitial ?? ''); + this.#setSlotText(initialValue); + dataHandlerStore.setInitialData(this.table, this.uid, this.field, initialValue); + this.#applyValidationState(initialValue); } updated(changedProperties) { @@ -135,7 +135,7 @@ export class VeEditableText extends LitElement { } onReset = () => { - this.value = this.#htmlEntitiesToText(this.valueInitial); + this.value = String(this.valueInitial ?? ''); this.#setSlotText(this.value); this.skipNextValueNormalization = true; this.#applyValidationState(this.value); @@ -349,10 +349,11 @@ export class VeEditableText extends LitElement { const storedValue = dataHandlerStore.data[this.table]?.[this.uid]?.[this.field] ?? this.valueInitial; const slot = this.#getSlot(); const isFocused = this.matches(':focus-within'); - if (!isFocused && storedValue?.trim() !== slot?.innerText?.trim()) { + const slotText = String(storedValue ?? this.value); + if (!isFocused && slotText.trim() !== slot?.innerText?.trim()) { this.skipNextValueNormalization = true; - this.value = this.#htmlEntitiesToText(storedValue ?? this.value); - this.#setSlotText(this.value); + this.value = String(storedValue ?? this.value); + this.#setSlotText(slotText); } } @@ -362,7 +363,7 @@ export class VeEditableText extends LitElement { #setSlotText(value) { const element = this.#getSlot(); if (element) { - element.innerText = value; + element.textContent = value; } } @@ -374,16 +375,18 @@ export class VeEditableText extends LitElement { return this.shadowRoot?.querySelector('.slot'); } - #htmlEntitiesToText(value) { + #storedTextToEditableText(value) { return String(value ?? '') - .replace(/­/gi, '\u00ad') - .replace(/ /gi, '\u00a0'); + .replace(/&/g, '&') + .replace(/\u00ad/g, '­') + .replace(/\u00a0/g, ' '); } - #textToHtmlEntities(value) { + #editableTextToStoredText(value) { return String(value ?? '') - .replace(/\u00ad/g, '­') - .replace(/\u00a0/g, ' '); + .replace(/­/gi, '\u00ad') + .replace(/ /gi, '\u00a0') + .replace(/&/gi, '&'); } /** @@ -435,22 +438,23 @@ export class VeEditableText extends LitElement { if (insertedText !== edit.insertedText) { event.preventDefault(); insertTextAtSelection(element, insertedText); - this.#validateAndStore(this.#getSlotText()); + this.#storeSlotText(this.#getSlotText()); } } #handleInput() { - this.#validateAndStore(this.#getSlotText()); + this.#storeSlotText(this.#getSlotText()); } #handleFocus() { this.focused = true; - this.#setSlotText(this.#textToHtmlEntities(this.value)); + this.#setSlotText(this.#storedTextToEditableText(this.value)); } #handleBlur() { this.focused = false; - this.#setSlotText(this.#htmlEntitiesToText(this.#validateAndStore(this.#getSlotText()))); + this.#storeSlotText(this.#getSlotText()); + this.#setSlotText(this.value); } /** @@ -503,8 +507,12 @@ export class VeEditableText extends LitElement { * @param {string} value * @returns {string} */ + #storeSlotText(value) { + return this.#validateAndStore(this.#editableTextToStoredText(value)); + } + #validateAndStore(value) { - const normalizedText = this.#htmlEntitiesToText(value); + const normalizedText = String(value ?? ''); this.#applyValidationState(normalizedText); let normalizedValue = normalizeValue(normalizedText, this.validation).text; From fd7acad6035bbbc01db47aa374bb27976d7451cb Mon Sep 17 00:00:00 2001 From: Matthias Vogel Date: Wed, 10 Jun 2026 13:42:59 +0200 Subject: [PATCH 3/4] [TASK] Keep editable text handling focused Remove extra adjustments around initial values, reset handling, and slot updates. This avoids mixing unrelated cleanup into the editable text conversion change. --- .../Frontend/components/ve-editable-text.js | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js b/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js index 513af04..2980680 100644 --- a/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js +++ b/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js @@ -105,10 +105,9 @@ export class VeEditableText extends LitElement { firstUpdated(changedProperties) { this.placeholder = this.name; this.skipNextValueNormalization = true; - const initialValue = String(this.valueInitial ?? ''); - this.#setSlotText(initialValue); - dataHandlerStore.setInitialData(this.table, this.uid, this.field, initialValue); - this.#applyValidationState(initialValue); + this.#setSlotText(this.valueInitial); + dataHandlerStore.setInitialData(this.table, this.uid, this.field, this.valueInitial); + this.#applyValidationState(this.valueInitial); } updated(changedProperties) { @@ -135,11 +134,11 @@ export class VeEditableText extends LitElement { } onReset = () => { - this.value = String(this.valueInitial ?? ''); - this.#setSlotText(this.value); + this.value = this.valueInitial; + this.#setSlotText(this.valueInitial); this.skipNextValueNormalization = true; - this.#applyValidationState(this.value); - dataHandlerStore.setData(this.table, this.uid, this.field, this.value); + this.#applyValidationState(this.valueInitial); + dataHandlerStore.setData(this.table, this.uid, this.field, this.valueInitial); }; /** @@ -349,11 +348,10 @@ export class VeEditableText extends LitElement { const storedValue = dataHandlerStore.data[this.table]?.[this.uid]?.[this.field] ?? this.valueInitial; const slot = this.#getSlot(); const isFocused = this.matches(':focus-within'); - const slotText = String(storedValue ?? this.value); - if (!isFocused && slotText.trim() !== slot?.innerText?.trim()) { + if (!isFocused && storedValue?.trim() !== slot?.innerText?.trim()) { this.skipNextValueNormalization = true; - this.value = String(storedValue ?? this.value); - this.#setSlotText(slotText); + this.value = storedValue ?? this.value; + this.#setSlotText(this.value); } } @@ -363,7 +361,7 @@ export class VeEditableText extends LitElement { #setSlotText(value) { const element = this.#getSlot(); if (element) { - element.textContent = value; + element.innerText = value; } } @@ -375,15 +373,23 @@ export class VeEditableText extends LitElement { return this.shadowRoot?.querySelector('.slot'); } + /** + * @param {string} value + * @return {string} + */ #storedTextToEditableText(value) { - return String(value ?? '') + return value .replace(/&/g, '&') .replace(/\u00ad/g, '­') .replace(/\u00a0/g, ' '); } + /** + * @param {string} value + * @return {string} + */ #editableTextToStoredText(value) { - return String(value ?? '') + return value .replace(/­/gi, '\u00ad') .replace(/ /gi, '\u00a0') .replace(/&/gi, '&'); @@ -438,12 +444,12 @@ export class VeEditableText extends LitElement { if (insertedText !== edit.insertedText) { event.preventDefault(); insertTextAtSelection(element, insertedText); - this.#storeSlotText(this.#getSlotText()); + this.#validateAndStore(this.#editableTextToStoredText(this.#getSlotText())); } } #handleInput() { - this.#storeSlotText(this.#getSlotText()); + this.#validateAndStore(this.#editableTextToStoredText(this.#getSlotText())); } #handleFocus() { @@ -453,8 +459,7 @@ export class VeEditableText extends LitElement { #handleBlur() { this.focused = false; - this.#storeSlotText(this.#getSlotText()); - this.#setSlotText(this.value); + this.#setSlotText(this.#validateAndStore(this.#editableTextToStoredText(this.#getSlotText()))); } /** @@ -507,15 +512,11 @@ export class VeEditableText extends LitElement { * @param {string} value * @returns {string} */ - #storeSlotText(value) { - return this.#validateAndStore(this.#editableTextToStoredText(value)); - } - #validateAndStore(value) { - const normalizedText = String(value ?? ''); - this.#applyValidationState(normalizedText); + this.value = value; + this.#applyValidationState(value); - let normalizedValue = normalizeValue(normalizedText, this.validation).text; + let normalizedValue = normalizeValue(value, this.validation).text; const min = Number(this.validation?.min || 0); if (normalizedValue.length < min && !this.isRequired()) { @@ -527,7 +528,6 @@ export class VeEditableText extends LitElement { normalizedValue = normalizedValue.slice(0, max); } - this.value = normalizedValue; dataHandlerStore.setData(this.table, this.uid, this.field, normalizedValue); return normalizedValue; } From 611cc2bf9d62a2fb41bd06053247f2eae08ab12a Mon Sep 17 00:00:00 2001 From: Matthias Vogel Date: Wed, 10 Jun 2026 13:57:25 +0200 Subject: [PATCH 4/4] [BUGFIX] Preserve literal ampersands in editable text Avoid converting every ampersand to `&` when plain text fields enter edit mode. This keeps normal text easier to edit while preserving the special handling for entity-like sequences. --- README.md | 11 +++++++++++ .../Frontend/components/ve-editable-text.js | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d9e221..06b932e 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ This extension provides visual editing features for content elements in TYPO3 CM - 🔦 **Finding editable areas:** use Spotlight to highlight editable text, rich text, images, and content elements. - 👻 **Showing empty fields:** use "show empty" when editable but currently empty fields are hard to see. - ↔️ **Moving content:** drag content elements by their handle. Hold Ctrl while dropping to copy instead of moving. +- ✍️ **Editing special text characters:** type `­` for a soft hyphen and ` ` for a non-breaking space. Entity-like text that starts with `&`, such as ` `, is shown with `&` while editing so it stays literal text. ## Template Integration @@ -77,6 +78,16 @@ If you do not have a Record object yet, you can create one with the `record-tran lib.contentElement.dataProcessing.1768551979 = record-transformation ```` +#### Editable text entities +For plain editable text fields, Visual Editor makes some otherwise hard-to-see characters explicit while the field is focused. +Soft hyphens are shown as `­`, and non-breaking spaces are shown as ` `. +Plain ampersands stay visible as `&`, but ampersands that start an entity-like sequence, such as ` `, ` `, or ` `, are shown as `&` while editing so the sequence stays literal text. +When the editor changes or leaves the field, these values are converted back before validation and storage. + +````html +

{record -> f:render.text(field: 'header')}

+```` + #### Fluid components When you use Fluid components, render the editable text outside the component and pass the rendered value into the component. This keeps the component decoupled from records and TCA fields, and lets callers pass either a plain string or the result of `f:render.text`. diff --git a/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js b/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js index 2980680..6e210db 100644 --- a/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js +++ b/Resources/Public/JavaScript/Frontend/components/ve-editable-text.js @@ -379,7 +379,7 @@ export class VeEditableText extends LitElement { */ #storedTextToEditableText(value) { return value - .replace(/&/g, '&') + .replace(/&(?=#\d+;|#x[0-9a-fA-F]+;|[a-zA-Z][a-zA-Z0-9]+;)/g, '&') .replace(/\u00ad/g, '­') .replace(/\u00a0/g, ' '); }