feat(17-02): richeditor field on TipTap with the untouched rule

- RichEditorField: Reka toolbar, link popover with the http/https/mailto/tel
  and relative URL policy, Edit HTML view, lossy note, skeleton while the
  lazily imported TipTap chunk loads, HTML view fallback when it fails
- untouched values are submitted byte-identical; after an edit the value is
  the editor HTML (empty paragraph -> ""); a value from outside (the save's
  sanitized answer) reloads the editor
- read-only renders through a client display allowlist, never as stored
- TipTap 3.31.4 (core, pm, starter-kit) pinned exact after the package check
- richeditor.* phrase keys (en, pl), smoke specs, forms docs
This commit is contained in:
2026-10-12 15:56:59 +02:00
parent 2181c2b5f6
commit cb2987c146
17 changed files with 1668 additions and 2 deletions

View File

@@ -0,0 +1,27 @@
{
"data": {
"name": "Link",
"modelClass": "Link",
"fields": [
{ "name": "title", "type": "text", "label": "Title", "span": "left" },
{ "name": "description", "type": "richeditor", "label": "Description", "comment": "Shown under the title." }
],
"messages": {
"create": { "other": "New link" },
"update": { "other": "Edit link" },
"saved": { "other": "Saved" },
"deleteConfirm": { "other": "Delete this record?" },
"deleted": { "other": "Record deleted" },
"preview": { "other": "Link details" },
"edit": { "other": "Edit link" }
},
"redirects": {
"default": "acme/companions/links",
"create": { "redirect": "acme/companions/links/:id", "redirectClose": "acme/companions/links" },
"update": { "redirect": "acme/companions/links", "redirectClose": "acme/companions/links" }
},
"meta": { "locale": "en" },
"assets": { "scripts": [], "styles": [] }
},
"meta": { "locale": "en" }
}

View File

@@ -609,6 +609,81 @@
"backend::lang.relation.unlink": {
"other": "Odłącz"
},
"backend::lang.richeditor.bold": {
"other": "Pogrubienie"
},
"backend::lang.richeditor.bullet_list": {
"other": "Lista punktowana"
},
"backend::lang.richeditor.heading": {
"other": "Nagłówek"
},
"backend::lang.richeditor.italic": {
"other": "Kursywa"
},
"backend::lang.richeditor.link": {
"other": "Link"
},
"backend::lang.richeditor.link_apply": {
"other": "Zastosuj link"
},
"backend::lang.richeditor.link_invalid": {
"other": "Link musi używać http, https, mailto, tel albo adresu względnego."
},
"backend::lang.richeditor.link_url": {
"other": "Adres linku"
},
"backend::lang.richeditor.load_failed": {
"other": "Nie udało się wczytać edytora. Możesz edytować HTML bezpośrednio."
},
"backend::lang.richeditor.lossy_note": {
"other": "Części formatowania tego tekstu nie da się pokazać w edytorze. Użyj „Edytuj HTML”, aby zmienić tekst bez jej utraty."
},
"backend::lang.richeditor.markup_changed": {
"other": "Filtr HTML zmienił część znaczników."
},
"backend::lang.richeditor.ordered_list": {
"other": "Lista numerowana"
},
"backend::lang.richeditor.quote": {
"other": "Cytat"
},
"backend::lang.richeditor.redo": {
"other": "Ponów"
},
"backend::lang.richeditor.removed_attributes": {
"one": "Usunięto :count atrybut: :names",
"few": "Usunięto :count atrybuty: :names",
"many": "Usunięto :count atrybutów: :names",
"other": "Usunięto :count atrybutu: :names"
},
"backend::lang.richeditor.removed_comments": {
"one": "Usunięto :count komentarz HTML",
"few": "Usunięto :count komentarze HTML",
"many": "Usunięto :count komentarzy HTML",
"other": "Usunięto :count komentarza HTML"
},
"backend::lang.richeditor.removed_elements": {
"one": "Usunięto :count element: :names",
"few": "Usunięto :count elementy: :names",
"many": "Usunięto :count elementów: :names",
"other": "Usunięto :count elementu: :names"
},
"backend::lang.richeditor.source": {
"other": "Edytuj HTML"
},
"backend::lang.richeditor.toolbar": {
"other": "Formatowanie"
},
"backend::lang.richeditor.underline": {
"other": "Podkreślenie"
},
"backend::lang.richeditor.undo": {
"other": "Cofnij"
},
"backend::lang.richeditor.unlink": {
"other": "Usuń link"
},
"backend::lang.settings.back": {
"other": "Wróć do ustawień"
},

View File

@@ -7,6 +7,7 @@ import type { components } from '../../src/api/schema'
import langJson from './lang.json'
import navigationJson from './navigation.json'
import settingsJson from './settings.json'
import companionsFormSchemaJson from './companions.form-schema.json'
import extensionFormSchemaJson from './extension.form-schema.json'
import extensionListSchemaJson from './extension.list-schema.json'
import extensionPartialJson from './extension.partial.json'
@@ -46,6 +47,8 @@ export const deferredRelationSchemaFixture: {
} = deferredRelationSchemaJson
/** A protected file list: a captioned image, a pending image and a PDF (Phase 12.2). */
export const deferredFilesFixture: S['cabana.Envelope-array_cabana_FileItem'] = deferredFilesJson
/** A link form with the Phase 17 field types: richeditor, repeater and a dependent dropdown. */
export const companionsFormSchemaFixture: S['cabana.Envelope-cabana_FormView'] = companionsFormSchemaJson
/** A form with a plugin widget and its controller script (Phase 10.1). */
export const extensionFormSchemaFixture: S['cabana.Envelope-cabana_FormView'] = extensionFormSchemaJson
/** A list with a header partial and a controller stylesheet (Phase 10.1). */