From 2fb1f8330806e29efc298648a7bc51fdf0edcffd Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Tue, 17 Feb 2026 08:33:38 -0300 Subject: [PATCH 01/16] docs(structured-content): add lock mode documentation Document the w:lock support for structured content nodes: - Add lockMode attribute and lock modes section to extension docs - Add StructuredContentLockMode type definition - Add code examples for inserting and updating lock modes - Update interactive demo with lock/unlock buttons - Update template builder docs with lockMode in field definitions --- apps/docs/extensions/structured-content.mdx | 226 +++++++++++++++++- .../extensions/structured-content.mdx | 65 ++++- .../template-builder/api-reference.mdx | 4 + .../template-builder/configuration.mdx | 23 ++ 4 files changed, 300 insertions(+), 18 deletions(-) diff --git a/apps/docs/extensions/structured-content.mdx b/apps/docs/extensions/structured-content.mdx index fe7dd0c19c..d406e46824 100644 --- a/apps/docs/extensions/structured-content.mdx +++ b/apps/docs/extensions/structured-content.mdx @@ -46,11 +46,138 @@ Node attributes that can be set and retrieved: Display name for the block + + Controls editing and deletion restrictions on the structured content node. See [Lock modes](#lock-modes) below. + + +## Lock modes + +Structured content nodes support four lock modes based on the OOXML [`w:lock` element](https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.lock) (ISO/IEC 29500 §17.5.2.23). Lock modes control whether users can edit the content inside a field and whether they can delete the field wrapper itself. + +| Lock mode | Wrapper | Content | Use case | +|-----------|---------|---------|----------| +| `unlocked` | Deletable | Editable | Default — no restrictions | +| `sdtLocked` | Protected | Editable | Protect field structure, allow value changes | +| `contentLocked` | Deletable | Read-only | Display a computed value users can remove | +| `sdtContentLocked` | Protected | Read-only | Fully protected field (e.g., system-generated ID) | + +Set the lock mode when inserting: + + +```javascript Usage +editor.commands.insertStructuredContentInline({ + attrs: { + id: '1', + alias: 'Customer Name', + lockMode: 'sdtLocked', + }, + text: 'John Doe', +}); +``` + +```javascript Full Example +import { SuperDoc } from 'superdoc'; +import 'superdoc/style.css'; + +const superdoc = new SuperDoc({ + selector: '#editor', + document: yourFile, + onReady: (superdoc) => { + const editor = superdoc.activeEditor; + editor.commands.insertStructuredContentInline({ + attrs: { + id: '1', + alias: 'Customer Name', + lockMode: 'sdtLocked', + }, + text: 'John Doe', + }); + }, +}); +``` + + +Change the lock mode on an existing field: + + +```javascript Usage +editor.commands.updateStructuredContentById('1', { + attrs: { lockMode: 'contentLocked' }, +}); +``` + +```javascript Full Example +import { SuperDoc } from 'superdoc'; +import 'superdoc/style.css'; + +const superdoc = new SuperDoc({ + selector: '#editor', + document: yourFile, + onReady: (superdoc) => { + const editor = superdoc.activeEditor; + editor.commands.updateStructuredContentById('1', { + attrs: { lockMode: 'contentLocked' }, + }); + }, +}); +``` + + +Lock modes are enforced at the editor plugin level using a three-layer defense: + +1. **Key interception** — Delete, Backspace, and Cut are blocked before a transaction is created, preventing cursor jumps +2. **Text input blocking** — Typing is silently blocked in content-locked nodes +3. **Transaction filter** — Safety net that catches paste, drag-drop, and programmatic edits + +Users can still place their cursor inside locked content and select text for copying. Only modifications are blocked. + + + Lock modes round-trip through DOCX. A document with `w:lock` elements in + its SDT properties will import with the correct lock mode and export the + `w:lock` element back to the saved file. + + ## Commands ### `insertStructuredContentInline` -Inserts a structured content inline at selection. +Inserts a structured content inline at the current selection. + +**Example:** + + +```javascript Usage +editor.commands.insertStructuredContentInline({ + attrs: { + id: '1', + alias: 'Customer Name', + lockMode: 'sdtLocked', // optional, defaults to 'unlocked' + }, + text: 'John Doe', +}); +``` + +```javascript Full Example +import { SuperDoc } from 'superdoc'; +import 'superdoc/style.css'; + +const superdoc = new SuperDoc({ + selector: '#editor', + document: yourFile, + onReady: (superdoc) => { + const editor = superdoc.activeEditor; + editor.commands.insertStructuredContentInline({ + attrs: { + id: '1', + alias: 'Customer Name', + lockMode: 'sdtLocked', + }, + text: 'John Doe', + }); + }, +}); +``` + **Parameters:** @@ -62,7 +189,43 @@ Inserts a structured content inline at selection. ### `insertStructuredContentBlock` -Inserts a structured content block at selection. +Inserts a structured content block at the current selection. + +**Example:** + + +```javascript Usage +editor.commands.insertStructuredContentBlock({ + attrs: { + id: '2', + alias: 'Terms Section', + lockMode: 'sdtContentLocked', // optional, defaults to 'unlocked' + }, + html: '

These terms are non-negotiable.

', +}); +``` + +```javascript Full Example +import { SuperDoc } from 'superdoc'; +import 'superdoc/style.css'; + +const superdoc = new SuperDoc({ + selector: '#editor', + document: yourFile, + onReady: (superdoc) => { + const editor = superdoc.activeEditor; + editor.commands.insertStructuredContentBlock({ + attrs: { + id: '2', + alias: 'Terms Section', + lockMode: 'sdtContentLocked', + }, + html: '

These terms are non-negotiable.

', + }); + }, +}); +``` +
**Parameters:** @@ -78,6 +241,53 @@ Updates a single structured content field by its unique ID. IDs are unique identifiers, so this will update at most one field. If the updated node does not match the schema, it will not be updated. +Pass `attrs` alone to change attributes (like `lockMode`) without replacing content: + +**Example:** + + +```javascript Usage +// Update content +editor.commands.updateStructuredContentById('1', { + text: 'Jane Smith', +}); + +// Update lock mode only (preserves content) +editor.commands.updateStructuredContentById('1', { + attrs: { lockMode: 'contentLocked' }, +}); + +// Update both content and attributes +editor.commands.updateStructuredContentById('1', { + text: 'New value', + attrs: { alias: 'Updated Label', lockMode: 'sdtLocked' }, +}); +``` + +```javascript Full Example +import { SuperDoc } from 'superdoc'; +import 'superdoc/style.css'; + +const superdoc = new SuperDoc({ + selector: '#editor', + document: yourFile, + onReady: (superdoc) => { + const editor = superdoc.activeEditor; + + // Update content + editor.commands.updateStructuredContentById('1', { + text: 'Jane Smith', + }); + + // Update lock mode only (preserves content) + editor.commands.updateStructuredContentById('1', { + attrs: { lockMode: 'contentLocked' }, + }); + }, +}); +``` + + **Parameters:** @@ -456,6 +666,12 @@ const superdoc = new SuperDoc({ ## Types +### `StructuredContentLockMode` + +```typescript +type StructuredContentLockMode = 'unlocked' | 'sdtLocked' | 'contentLocked' | 'sdtContentLocked' +``` + ### `StructuredContentInlineInsert` @@ -466,7 +682,7 @@ const superdoc = new SuperDoc({ ProseMirror JSON - Node attributes + Node attributes (`id`, `alias`, `tag`, `lockMode`, `group`) @@ -480,7 +696,7 @@ const superdoc = new SuperDoc({ ProseMirror JSON - Node attributes + Node attributes (`id`, `alias`, `tag`, `lockMode`) @@ -497,7 +713,7 @@ const superdoc = new SuperDoc({ Replace content with ProseMirror JSON (overrides html) - Update attributes only (preserves content) + Update attributes only (preserves content). Supports `lockMode`, `alias`, `tag`. diff --git a/apps/docs/snippets/extensions/structured-content.mdx b/apps/docs/snippets/extensions/structured-content.mdx index 3ec575f151..15144fbae2 100644 --- a/apps/docs/snippets/extensions/structured-content.mdx +++ b/apps/docs/snippets/extensions/structured-content.mdx @@ -2,9 +2,10 @@ Native Word SDT (w:sdt) fields for documents. Supports inline and block structur ## Use case -- Form templates - Create fillable documents with inline text fields and block content areas -- Contract generation - Dynamic clauses and terms that map to Word content controls -- Document automation - Programmatically update specific sections while preserving structure +- Form templates — Create fillable documents with inline text fields and block content areas +- Contract generation — Dynamic clauses and terms that map to Word content controls +- Document automation — Programmatically update specific sections while preserving structure +- Protected fields — Lock modes control which fields users can edit or delete (ECMA-376 `w:lock`) import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' @@ -29,6 +30,22 @@ people will never forget how you made them feel."

Maya Angelou { + const editor = superdoc?.activeEditor || superdoc?.editor + if (!editor?.commands) return + + editor.commands.insertStructuredContentInline({ + attrs: { + id: '3', + alias: 'Account ID', + lockMode: 'sdtContentLocked', + }, + text: 'ACC-00042', + }); + } + }, { label: 'Insert block field', onClick: (superdoc) => { @@ -39,6 +56,7 @@ people will never forget how you made them feel."

Maya Angelou

Maya Angelou { const editor = superdoc?.activeEditor || superdoc?.editor if (!editor?.commands) return - editor.commands.updateStructuredContentById('2', { - json: { type: 'paragraph', content: [{ type: 'text', text: 'Updated legal content...' }] } + editor.commands.updateStructuredContentById('1', { + attrs: { lockMode: 'contentLocked' }, + }); + } + }, + { + label: 'Unlock inline field', + onClick: (superdoc) => { + const editor = superdoc?.activeEditor || superdoc?.editor + if (!editor?.commands) return + + editor.commands.updateStructuredContentById('1', { + attrs: { lockMode: 'unlocked' }, }); } }, @@ -85,29 +114,39 @@ people will never forget how you made them feel."

Maya AngelouPlease review the terms...

' }); -// Update inline field content +// Update field content editor.commands.updateStructuredContentById('1', { text: 'Jane Smith' }); -// Update block field content and attributes -editor.commands.updateStructuredContentById('2', { - html: '

Updated terms and conditions...

', - attrs: { alias: 'Legal Terms' } +// Change lock mode without changing content +editor.commands.updateStructuredContentById('1', { + attrs: { lockMode: 'contentLocked' } }); // Get all structured content tags diff --git a/apps/docs/solutions/template-builder/api-reference.mdx b/apps/docs/solutions/template-builder/api-reference.mdx index 52b82e2ed7..bdb34fd091 100644 --- a/apps/docs/solutions/template-builder/api-reference.mdx +++ b/apps/docs/solutions/template-builder/api-reference.mdx @@ -138,9 +138,12 @@ interface FieldDefinition { metadata?: Record; // Custom metadata mode?: "inline" | "block"; // Field insertion mode (default: "inline") group?: string; // Category/group name + lockMode?: StructuredContentLockMode; // Editing restrictions (default: "unlocked") } ``` +The `lockMode` property controls what users can do with the field once inserted. See the [Structured Content extension](/extensions/structured-content#lock-modes) for details on each mode. + ### TemplateField Fields that exist in the template document: @@ -153,6 +156,7 @@ interface TemplateField { position?: number; // Position in document mode?: "inline" | "block"; // Rendering mode group?: string; // Group ID for related fields + lockMode?: StructuredContentLockMode; // Current lock state } ``` diff --git a/apps/docs/solutions/template-builder/configuration.mdx b/apps/docs/solutions/template-builder/configuration.mdx index 01625993c8..110f87a95b 100644 --- a/apps/docs/solutions/template-builder/configuration.mdx +++ b/apps/docs/solutions/template-builder/configuration.mdx @@ -43,12 +43,35 @@ Define which fields users can insert: label: "Signature", mode: "block", // Force block-level insertion }, + { + id: "3", + label: "Account ID", + lockMode: "sdtContentLocked", // Fully protected — no edits, no deletion + }, + { + id: "4", + label: "Editable Clause", + lockMode: "sdtLocked", // Users can edit content but not delete the field + }, ], allowCreate: true, // Let users create new fields on the fly }} /> ``` +### Lock modes + +Control what users can do with each field using the `lockMode` property: + +| Lock mode | Field wrapper | Field content | Use case | +|-----------|---------------|---------------|----------| +| `unlocked` | Deletable | Editable | Default — no restrictions | +| `sdtLocked` | Protected | Editable | Protect field structure, allow value changes | +| `contentLocked` | Deletable | Read-only | Display a computed value users can remove | +| `sdtContentLocked` | Protected | Read-only | Fully protected (e.g., system-generated ID) | + +Lock modes are compatible with Microsoft Word — exported documents preserve the `w:lock` element per the ECMA-376 standard. See the [Structured Content extension](/extensions/structured-content#lock-modes) for full details. + ### Field creation Allow users to create new fields while building templates: From f952739d73eee731b500a3f3edc82247fb427ee9 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Tue, 17 Feb 2026 08:37:15 -0300 Subject: [PATCH 02/16] docs(template-builder): revert lock mode additions Template builder does not support lockMode in FieldDefinition or TemplateField types yet. Remove premature documentation. --- .../template-builder/api-reference.mdx | 4 ---- .../template-builder/configuration.mdx | 23 ------------------- 2 files changed, 27 deletions(-) diff --git a/apps/docs/solutions/template-builder/api-reference.mdx b/apps/docs/solutions/template-builder/api-reference.mdx index bdb34fd091..52b82e2ed7 100644 --- a/apps/docs/solutions/template-builder/api-reference.mdx +++ b/apps/docs/solutions/template-builder/api-reference.mdx @@ -138,12 +138,9 @@ interface FieldDefinition { metadata?: Record; // Custom metadata mode?: "inline" | "block"; // Field insertion mode (default: "inline") group?: string; // Category/group name - lockMode?: StructuredContentLockMode; // Editing restrictions (default: "unlocked") } ``` -The `lockMode` property controls what users can do with the field once inserted. See the [Structured Content extension](/extensions/structured-content#lock-modes) for details on each mode. - ### TemplateField Fields that exist in the template document: @@ -156,7 +153,6 @@ interface TemplateField { position?: number; // Position in document mode?: "inline" | "block"; // Rendering mode group?: string; // Group ID for related fields - lockMode?: StructuredContentLockMode; // Current lock state } ``` diff --git a/apps/docs/solutions/template-builder/configuration.mdx b/apps/docs/solutions/template-builder/configuration.mdx index 110f87a95b..01625993c8 100644 --- a/apps/docs/solutions/template-builder/configuration.mdx +++ b/apps/docs/solutions/template-builder/configuration.mdx @@ -43,35 +43,12 @@ Define which fields users can insert: label: "Signature", mode: "block", // Force block-level insertion }, - { - id: "3", - label: "Account ID", - lockMode: "sdtContentLocked", // Fully protected — no edits, no deletion - }, - { - id: "4", - label: "Editable Clause", - lockMode: "sdtLocked", // Users can edit content but not delete the field - }, ], allowCreate: true, // Let users create new fields on the fly }} /> ``` -### Lock modes - -Control what users can do with each field using the `lockMode` property: - -| Lock mode | Field wrapper | Field content | Use case | -|-----------|---------------|---------------|----------| -| `unlocked` | Deletable | Editable | Default — no restrictions | -| `sdtLocked` | Protected | Editable | Protect field structure, allow value changes | -| `contentLocked` | Deletable | Read-only | Display a computed value users can remove | -| `sdtContentLocked` | Protected | Read-only | Fully protected (e.g., system-generated ID) | - -Lock modes are compatible with Microsoft Word — exported documents preserve the `w:lock` element per the ECMA-376 standard. See the [Structured Content extension](/extensions/structured-content#lock-modes) for full details. - ### Field creation Allow users to create new fields while building templates: From fe39e02bf68922ba26ffc9e016e44f7af74d79be Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Tue, 17 Feb 2026 08:40:53 -0300 Subject: [PATCH 03/16] fix(docs): remove lockMode from interactive demo buttons lockMode is not yet available in the published editor version. Restore original block field and update buttons that work today. --- .../extensions/structured-content.mdx | 34 ++----------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/apps/docs/snippets/extensions/structured-content.mdx b/apps/docs/snippets/extensions/structured-content.mdx index 15144fbae2..0da51a3470 100644 --- a/apps/docs/snippets/extensions/structured-content.mdx +++ b/apps/docs/snippets/extensions/structured-content.mdx @@ -30,22 +30,6 @@ people will never forget how you made them feel."

Maya Angelou { - const editor = superdoc?.activeEditor || superdoc?.editor - if (!editor?.commands) return - - editor.commands.insertStructuredContentInline({ - attrs: { - id: '3', - alias: 'Account ID', - lockMode: 'sdtContentLocked', - }, - text: 'ACC-00042', - }); - } - }, { label: 'Insert block field', onClick: (superdoc) => { @@ -56,7 +40,6 @@ people will never forget how you made them feel."

Maya Angelou

Maya Angelou { - const editor = superdoc?.activeEditor || superdoc?.editor - if (!editor?.commands) return - - editor.commands.updateStructuredContentById('1', { - attrs: { lockMode: 'contentLocked' }, - }); - } - }, - { - label: 'Unlock inline field', + label: 'Update block field', onClick: (superdoc) => { const editor = superdoc?.activeEditor || superdoc?.editor if (!editor?.commands) return - editor.commands.updateStructuredContentById('1', { - attrs: { lockMode: 'unlocked' }, + editor.commands.updateStructuredContentById('2', { + json: { type: 'paragraph', content: [{ type: 'text', text: 'Updated legal content...' }] } }); } }, From 8fd418c608cea28aebdc212e1152f0f56d2cf9a8 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Tue, 17 Feb 2026 08:42:51 -0300 Subject: [PATCH 04/16] docs(structured-content): add lock/unlock buttons to interactive demo Add buttons to lock and unlock both inline and block fields using updateStructuredContentById with lockMode attribute. --- .../extensions/structured-content.mdx | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/apps/docs/snippets/extensions/structured-content.mdx b/apps/docs/snippets/extensions/structured-content.mdx index 0da51a3470..b08e2b968a 100644 --- a/apps/docs/snippets/extensions/structured-content.mdx +++ b/apps/docs/snippets/extensions/structured-content.mdx @@ -65,6 +65,50 @@ people will never forget how you made them feel."

Maya Angelou { + const editor = superdoc?.activeEditor || superdoc?.editor + if (!editor?.commands) return + + editor.commands.updateStructuredContentById('1', { + attrs: { lockMode: 'sdtContentLocked' }, + }); + } + }, + { + label: 'Unlock inline field', + onClick: (superdoc) => { + const editor = superdoc?.activeEditor || superdoc?.editor + if (!editor?.commands) return + + editor.commands.updateStructuredContentById('1', { + attrs: { lockMode: 'unlocked' }, + }); + } + }, + { + label: 'Lock block field', + onClick: (superdoc) => { + const editor = superdoc?.activeEditor || superdoc?.editor + if (!editor?.commands) return + + editor.commands.updateStructuredContentById('2', { + attrs: { lockMode: 'sdtContentLocked' }, + }); + } + }, + { + label: 'Unlock block field', + onClick: (superdoc) => { + const editor = superdoc?.activeEditor || superdoc?.editor + if (!editor?.commands) return + + editor.commands.updateStructuredContentById('2', { + attrs: { lockMode: 'unlocked' }, + }); + } + }, { label: 'Delete all fields', onClick: (superdoc) => { From 233b941dd5f30455e7d52b9dc977effcb54609bd Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Tue, 17 Feb 2026 08:44:27 -0300 Subject: [PATCH 05/16] docs(structured-content): improve interactive demo example text Replace placeholder quote with a realistic service agreement template that demonstrates practical use of inline and block fields. --- .../snippets/extensions/structured-content.mdx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/docs/snippets/extensions/structured-content.mdx b/apps/docs/snippets/extensions/structured-content.mdx index b08e2b968a..bfeee4e849 100644 --- a/apps/docs/snippets/extensions/structured-content.mdx +++ b/apps/docs/snippets/extensions/structured-content.mdx @@ -10,8 +10,7 @@ Native Word SDT (w:sdt) fields for documents. Supports inline and block structur import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx'

"People will forget what you said, people will forget what you did, but -people will never forget how you made them feel."

Maya Angelou

`} + html={`

Service Agreement

This agreement is entered into by and between Acme Corp ("Company") and the client named below. The Company agrees to provide consulting services as outlined in the attached statement of work.

Client Information

Name: ________    Date: January 1, 2026

Terms

Payment is due within 30 days of invoice date. All work products remain property of the Company until final payment is received.

`} height="300px" customButtons={[ [ @@ -24,9 +23,9 @@ people will never forget how you made them feel."

Maya Angelou

Maya Angelou

Maya Angelou

Maya Angelou Date: Tue, 17 Feb 2026 08:45:32 -0300 Subject: [PATCH 06/16] docs(structured-content): use sdtLocked on insert buttons Insert inline and block fields with sdtLocked by default. Add sdtContentLocked lock buttons for both field types and a single unlock-all button. --- .../extensions/structured-content.mdx | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/apps/docs/snippets/extensions/structured-content.mdx b/apps/docs/snippets/extensions/structured-content.mdx index bfeee4e849..ee9de42afc 100644 --- a/apps/docs/snippets/extensions/structured-content.mdx +++ b/apps/docs/snippets/extensions/structured-content.mdx @@ -15,7 +15,7 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' customButtons={[ [ { - label: 'Insert inline field', + label: 'Insert inline (sdtLocked)', onClick: (superdoc) => { const editor = superdoc?.activeEditor || superdoc?.editor if (!editor?.commands) return @@ -24,13 +24,14 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' attrs: { id: '1', alias: 'Client Name', + lockMode: 'sdtLocked', }, text: 'John Doe', }); } }, { - label: 'Insert block field', + label: 'Insert block (sdtLocked)', onClick: (superdoc) => { const editor = superdoc?.activeEditor || superdoc?.editor if (!editor?.commands) return @@ -39,6 +40,7 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' attrs: { id: '2', alias: 'Payment Terms', + lockMode: 'sdtLocked', }, json: { type: 'paragraph', content: [{ type: 'text', text: 'Payment is due within 30 days of invoice date. Late payments incur a 1.5% monthly fee. All disputes must be submitted in writing within 10 business days.' }] } }); @@ -65,7 +67,7 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' } }, { - label: 'Lock inline field', + label: 'Lock inline (sdtContentLocked)', onClick: (superdoc) => { const editor = superdoc?.activeEditor || superdoc?.editor if (!editor?.commands) return @@ -76,18 +78,7 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' } }, { - label: 'Unlock inline field', - onClick: (superdoc) => { - const editor = superdoc?.activeEditor || superdoc?.editor - if (!editor?.commands) return - - editor.commands.updateStructuredContentById('1', { - attrs: { lockMode: 'unlocked' }, - }); - } - }, - { - label: 'Lock block field', + label: 'Lock block (sdtContentLocked)', onClick: (superdoc) => { const editor = superdoc?.activeEditor || superdoc?.editor if (!editor?.commands) return @@ -98,11 +89,14 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' } }, { - label: 'Unlock block field', + label: 'Unlock all fields', onClick: (superdoc) => { const editor = superdoc?.activeEditor || superdoc?.editor if (!editor?.commands) return + editor.commands.updateStructuredContentById('1', { + attrs: { lockMode: 'unlocked' }, + }); editor.commands.updateStructuredContentById('2', { attrs: { lockMode: 'unlocked' }, }); From f99ded38fa86d2c0ae8bea536deaba190a30904c Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Tue, 17 Feb 2026 08:48:43 -0300 Subject: [PATCH 07/16] docs(structured-content): add live SDT fields to interactive demo Use onReady callback to auto-insert inline and block sdtLocked fields into the example document on load, replacing placeholder text with real structured content fields. Reorganize buttons into logical groups: update actions, lock mode actions, and delete. --- .../extensions/structured-content.mdx | 101 +++++++++--------- 1 file changed, 49 insertions(+), 52 deletions(-) diff --git a/apps/docs/snippets/extensions/structured-content.mdx b/apps/docs/snippets/extensions/structured-content.mdx index ee9de42afc..0de986db09 100644 --- a/apps/docs/snippets/extensions/structured-content.mdx +++ b/apps/docs/snippets/extensions/structured-content.mdx @@ -10,68 +10,69 @@ Native Word SDT (w:sdt) fields for documents. Supports inline and block structur import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' Service Agreement

This agreement is entered into by and between Acme Corp ("Company") and the client named below. The Company agrees to provide consulting services as outlined in the attached statement of work.

Client Information

Name: ________    Date: January 1, 2026

Terms

Payment is due within 30 days of invoice date. All work products remain property of the Company until final payment is received.

`} - height="300px" + html={`

Service Agreement

This agreement is entered into by and between Acme Corp ("Company") and the client identified below.

Client name: ________

The Company agrees to provide consulting services under the following terms:

________

`} + height="350px" + onReady={(superdoc) => { + const editor = superdoc?.activeEditor || superdoc?.editor + if (!editor?.commands) return + + // Find "________" in "Client name:" paragraph and replace with inline sdtLocked field + let inlinePos = null + editor.state.doc.descendants((node, pos) => { + if (node.isText && node.text.includes('________') && inlinePos === null) { + inlinePos = pos + node.text.indexOf('________') + } + }) + if (inlinePos !== null) { + editor.commands.setTextSelection({ from: inlinePos, to: inlinePos + 8 }) + editor.commands.insertStructuredContentInline({ + attrs: { id: '1', alias: 'Client Name', lockMode: 'sdtLocked' }, + text: 'John Doe', + }) + } + + // Find the second "________" paragraph and replace with block sdtLocked field + let blockPos = null + editor.state.doc.descendants((node, pos) => { + if (node.isText && node.text.includes('________')) { + blockPos = pos + node.text.indexOf('________') + } + }) + if (blockPos !== null) { + editor.commands.setTextSelection({ from: blockPos, to: blockPos + 8 }) + editor.commands.insertStructuredContentBlock({ + attrs: { id: '2', alias: 'Payment Terms', lockMode: 'sdtLocked' }, + json: { type: 'paragraph', content: [{ type: 'text', text: 'Payment is due within 30 days of invoice date. Late payments incur a 1.5% monthly fee.' }] }, + }) + } + }} customButtons={[ [ { - label: 'Insert inline (sdtLocked)', + label: 'Update client name', onClick: (superdoc) => { const editor = superdoc?.activeEditor || superdoc?.editor if (!editor?.commands) return - - editor.commands.insertStructuredContentInline({ - attrs: { - id: '1', - alias: 'Client Name', - lockMode: 'sdtLocked', - }, - text: 'John Doe', - }); - } - }, - { - label: 'Insert block (sdtLocked)', - onClick: (superdoc) => { - const editor = superdoc?.activeEditor || superdoc?.editor - if (!editor?.commands) return - - editor.commands.insertStructuredContentBlock({ - attrs: { - id: '2', - alias: 'Payment Terms', - lockMode: 'sdtLocked', - }, - json: { type: 'paragraph', content: [{ type: 'text', text: 'Payment is due within 30 days of invoice date. Late payments incur a 1.5% monthly fee. All disputes must be submitted in writing within 10 business days.' }] } - }); - }, - }, - { - label: 'Update inline field', - onClick: (superdoc) => { - const editor = superdoc?.activeEditor || superdoc?.editor - if (!editor?.commands) return - editor.commands.updateStructuredContentById('1', { text: 'Jane Smith' }); } }, { - label: 'Update block field', + label: 'Update payment terms', onClick: (superdoc) => { const editor = superdoc?.activeEditor || superdoc?.editor if (!editor?.commands) return - editor.commands.updateStructuredContentById('2', { json: { type: 'paragraph', content: [{ type: 'text', text: 'Net-15 payment terms apply. A 2% early payment discount is available for invoices settled within 7 days.' }] } }); } }, + ], + [ { label: 'Lock inline (sdtContentLocked)', onClick: (superdoc) => { const editor = superdoc?.activeEditor || superdoc?.editor if (!editor?.commands) return - editor.commands.updateStructuredContentById('1', { attrs: { lockMode: 'sdtContentLocked' }, }); @@ -82,32 +83,27 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' onClick: (superdoc) => { const editor = superdoc?.activeEditor || superdoc?.editor if (!editor?.commands) return - editor.commands.updateStructuredContentById('2', { attrs: { lockMode: 'sdtContentLocked' }, }); } }, { - label: 'Unlock all fields', + label: 'Unlock all', onClick: (superdoc) => { const editor = superdoc?.activeEditor || superdoc?.editor if (!editor?.commands) return - - editor.commands.updateStructuredContentById('1', { - attrs: { lockMode: 'unlocked' }, - }); - editor.commands.updateStructuredContentById('2', { - attrs: { lockMode: 'unlocked' }, - }); + editor.commands.updateStructuredContentById('1', { attrs: { lockMode: 'unlocked' } }); + editor.commands.updateStructuredContentById('2', { attrs: { lockMode: 'unlocked' } }); } }, + ], + [ { label: 'Delete all fields', onClick: (superdoc) => { const editor = superdoc?.activeEditor || superdoc?.editor if (!editor?.commands) return - const fields = editor.helpers.structuredContentCommands.getStructuredContentTags(editor.state); editor.commands.deleteStructuredContent(fields); } @@ -119,12 +115,12 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' ## Quick start ```javascript -// Insert inline field for customer name +// Insert inline field with sdtLocked — users can edit content but not delete the field editor.commands.insertStructuredContentInline({ attrs: { id: '1', alias: 'Customer Name', - lockMode: 'sdtLocked', // users can edit the value but not delete the field + lockMode: 'sdtLocked', }, text: 'John Doe' }); @@ -139,11 +135,12 @@ editor.commands.insertStructuredContentInline({ text: 'ACC-00042' }); -// Insert block field for terms section +// Insert block field with sdtLocked — content is editable, wrapper is protected editor.commands.insertStructuredContentBlock({ attrs: { id: '2', alias: 'Terms & Conditions', + lockMode: 'sdtLocked', }, html: '

Please review the terms...

' }); From 218cb0b5477ce310c5b3fd370dfeebaf226e3ef8 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Tue, 17 Feb 2026 08:50:46 -0300 Subject: [PATCH 08/16] fix(docs): remove lockMode from onReady SDT insertions lockMode is not available in the published version yet (PR #1939). Remove it from onReady field insertions so the demo works with the current superdoc@latest on unpkg. --- apps/docs/snippets/extensions/structured-content.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/docs/snippets/extensions/structured-content.mdx b/apps/docs/snippets/extensions/structured-content.mdx index 0de986db09..bd6897fdea 100644 --- a/apps/docs/snippets/extensions/structured-content.mdx +++ b/apps/docs/snippets/extensions/structured-content.mdx @@ -16,7 +16,7 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' const editor = superdoc?.activeEditor || superdoc?.editor if (!editor?.commands) return - // Find "________" in "Client name:" paragraph and replace with inline sdtLocked field + // Find "________" in "Client name:" paragraph and replace with inline field let inlinePos = null editor.state.doc.descendants((node, pos) => { if (node.isText && node.text.includes('________') && inlinePos === null) { @@ -26,12 +26,12 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' if (inlinePos !== null) { editor.commands.setTextSelection({ from: inlinePos, to: inlinePos + 8 }) editor.commands.insertStructuredContentInline({ - attrs: { id: '1', alias: 'Client Name', lockMode: 'sdtLocked' }, + attrs: { id: '1', alias: 'Client Name' }, text: 'John Doe', }) } - // Find the second "________" paragraph and replace with block sdtLocked field + // Find the second "________" paragraph and replace with block field let blockPos = null editor.state.doc.descendants((node, pos) => { if (node.isText && node.text.includes('________')) { @@ -41,7 +41,7 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' if (blockPos !== null) { editor.commands.setTextSelection({ from: blockPos, to: blockPos + 8 }) editor.commands.insertStructuredContentBlock({ - attrs: { id: '2', alias: 'Payment Terms', lockMode: 'sdtLocked' }, + attrs: { id: '2', alias: 'Payment Terms' }, json: { type: 'paragraph', content: [{ type: 'text', text: 'Payment is due within 30 days of invoice date. Late payments incur a 1.5% monthly fee.' }] }, }) } From 7737922e21c46be22b1bd608a1b9e71cd48145ec Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Tue, 17 Feb 2026 08:51:56 -0300 Subject: [PATCH 09/16] fix(docs): remove lock/unlock buttons from interactive demo Lock mode buttons depend on PR #1939 which isn't published yet. Remove them until lockMode is available in superdoc@latest. --- .../extensions/structured-content.mdx | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/apps/docs/snippets/extensions/structured-content.mdx b/apps/docs/snippets/extensions/structured-content.mdx index bd6897fdea..c1b3fe36a7 100644 --- a/apps/docs/snippets/extensions/structured-content.mdx +++ b/apps/docs/snippets/extensions/structured-content.mdx @@ -67,37 +67,6 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' } }, ], - [ - { - label: 'Lock inline (sdtContentLocked)', - onClick: (superdoc) => { - const editor = superdoc?.activeEditor || superdoc?.editor - if (!editor?.commands) return - editor.commands.updateStructuredContentById('1', { - attrs: { lockMode: 'sdtContentLocked' }, - }); - } - }, - { - label: 'Lock block (sdtContentLocked)', - onClick: (superdoc) => { - const editor = superdoc?.activeEditor || superdoc?.editor - if (!editor?.commands) return - editor.commands.updateStructuredContentById('2', { - attrs: { lockMode: 'sdtContentLocked' }, - }); - } - }, - { - label: 'Unlock all', - onClick: (superdoc) => { - const editor = superdoc?.activeEditor || superdoc?.editor - if (!editor?.commands) return - editor.commands.updateStructuredContentById('1', { attrs: { lockMode: 'unlocked' } }); - editor.commands.updateStructuredContentById('2', { attrs: { lockMode: 'unlocked' } }); - } - }, - ], [ { label: 'Delete all fields', From 53638cd85001acd8134738bf7482d77ca2de9152 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Tue, 17 Feb 2026 08:54:39 -0300 Subject: [PATCH 10/16] =?UTF-8?q?fix(docs):=20restore=20lockMode=20in=20de?= =?UTF-8?q?mo=20=E2=80=94=20feature=20is=20published?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lockMode is available in superdoc@latest (v1.13.1). Restore sdtLocked on onReady field insertions and lock/unlock buttons. --- .../extensions/structured-content.mdx | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/apps/docs/snippets/extensions/structured-content.mdx b/apps/docs/snippets/extensions/structured-content.mdx index c1b3fe36a7..0de986db09 100644 --- a/apps/docs/snippets/extensions/structured-content.mdx +++ b/apps/docs/snippets/extensions/structured-content.mdx @@ -16,7 +16,7 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' const editor = superdoc?.activeEditor || superdoc?.editor if (!editor?.commands) return - // Find "________" in "Client name:" paragraph and replace with inline field + // Find "________" in "Client name:" paragraph and replace with inline sdtLocked field let inlinePos = null editor.state.doc.descendants((node, pos) => { if (node.isText && node.text.includes('________') && inlinePos === null) { @@ -26,12 +26,12 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' if (inlinePos !== null) { editor.commands.setTextSelection({ from: inlinePos, to: inlinePos + 8 }) editor.commands.insertStructuredContentInline({ - attrs: { id: '1', alias: 'Client Name' }, + attrs: { id: '1', alias: 'Client Name', lockMode: 'sdtLocked' }, text: 'John Doe', }) } - // Find the second "________" paragraph and replace with block field + // Find the second "________" paragraph and replace with block sdtLocked field let blockPos = null editor.state.doc.descendants((node, pos) => { if (node.isText && node.text.includes('________')) { @@ -41,7 +41,7 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' if (blockPos !== null) { editor.commands.setTextSelection({ from: blockPos, to: blockPos + 8 }) editor.commands.insertStructuredContentBlock({ - attrs: { id: '2', alias: 'Payment Terms' }, + attrs: { id: '2', alias: 'Payment Terms', lockMode: 'sdtLocked' }, json: { type: 'paragraph', content: [{ type: 'text', text: 'Payment is due within 30 days of invoice date. Late payments incur a 1.5% monthly fee.' }] }, }) } @@ -67,6 +67,37 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' } }, ], + [ + { + label: 'Lock inline (sdtContentLocked)', + onClick: (superdoc) => { + const editor = superdoc?.activeEditor || superdoc?.editor + if (!editor?.commands) return + editor.commands.updateStructuredContentById('1', { + attrs: { lockMode: 'sdtContentLocked' }, + }); + } + }, + { + label: 'Lock block (sdtContentLocked)', + onClick: (superdoc) => { + const editor = superdoc?.activeEditor || superdoc?.editor + if (!editor?.commands) return + editor.commands.updateStructuredContentById('2', { + attrs: { lockMode: 'sdtContentLocked' }, + }); + } + }, + { + label: 'Unlock all', + onClick: (superdoc) => { + const editor = superdoc?.activeEditor || superdoc?.editor + if (!editor?.commands) return + editor.commands.updateStructuredContentById('1', { attrs: { lockMode: 'unlocked' } }); + editor.commands.updateStructuredContentById('2', { attrs: { lockMode: 'unlocked' } }); + } + }, + ], [ { label: 'Delete all fields', From 88edde8397143f95663eba8180429a3e505bfb3e Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Tue, 17 Feb 2026 08:55:39 -0300 Subject: [PATCH 11/16] =?UTF-8?q?Revert=20"fix(docs):=20restore=20lockMode?= =?UTF-8?q?=20in=20demo=20=E2=80=94=20feature=20is=20published"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 53638cd85001acd8134738bf7482d77ca2de9152. --- .../extensions/structured-content.mdx | 39 ++----------------- 1 file changed, 4 insertions(+), 35 deletions(-) diff --git a/apps/docs/snippets/extensions/structured-content.mdx b/apps/docs/snippets/extensions/structured-content.mdx index 0de986db09..c1b3fe36a7 100644 --- a/apps/docs/snippets/extensions/structured-content.mdx +++ b/apps/docs/snippets/extensions/structured-content.mdx @@ -16,7 +16,7 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' const editor = superdoc?.activeEditor || superdoc?.editor if (!editor?.commands) return - // Find "________" in "Client name:" paragraph and replace with inline sdtLocked field + // Find "________" in "Client name:" paragraph and replace with inline field let inlinePos = null editor.state.doc.descendants((node, pos) => { if (node.isText && node.text.includes('________') && inlinePos === null) { @@ -26,12 +26,12 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' if (inlinePos !== null) { editor.commands.setTextSelection({ from: inlinePos, to: inlinePos + 8 }) editor.commands.insertStructuredContentInline({ - attrs: { id: '1', alias: 'Client Name', lockMode: 'sdtLocked' }, + attrs: { id: '1', alias: 'Client Name' }, text: 'John Doe', }) } - // Find the second "________" paragraph and replace with block sdtLocked field + // Find the second "________" paragraph and replace with block field let blockPos = null editor.state.doc.descendants((node, pos) => { if (node.isText && node.text.includes('________')) { @@ -41,7 +41,7 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' if (blockPos !== null) { editor.commands.setTextSelection({ from: blockPos, to: blockPos + 8 }) editor.commands.insertStructuredContentBlock({ - attrs: { id: '2', alias: 'Payment Terms', lockMode: 'sdtLocked' }, + attrs: { id: '2', alias: 'Payment Terms' }, json: { type: 'paragraph', content: [{ type: 'text', text: 'Payment is due within 30 days of invoice date. Late payments incur a 1.5% monthly fee.' }] }, }) } @@ -67,37 +67,6 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' } }, ], - [ - { - label: 'Lock inline (sdtContentLocked)', - onClick: (superdoc) => { - const editor = superdoc?.activeEditor || superdoc?.editor - if (!editor?.commands) return - editor.commands.updateStructuredContentById('1', { - attrs: { lockMode: 'sdtContentLocked' }, - }); - } - }, - { - label: 'Lock block (sdtContentLocked)', - onClick: (superdoc) => { - const editor = superdoc?.activeEditor || superdoc?.editor - if (!editor?.commands) return - editor.commands.updateStructuredContentById('2', { - attrs: { lockMode: 'sdtContentLocked' }, - }); - } - }, - { - label: 'Unlock all', - onClick: (superdoc) => { - const editor = superdoc?.activeEditor || superdoc?.editor - if (!editor?.commands) return - editor.commands.updateStructuredContentById('1', { attrs: { lockMode: 'unlocked' } }); - editor.commands.updateStructuredContentById('2', { attrs: { lockMode: 'unlocked' } }); - } - }, - ], [ { label: 'Delete all fields', From 7e8ecf54895d9006f0ad35d63cf265d8bc0ed924 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Tue, 17 Feb 2026 08:56:12 -0300 Subject: [PATCH 12/16] docs(structured-content): add toggle lock buttons for inline and block --- .../extensions/structured-content.mdx | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/apps/docs/snippets/extensions/structured-content.mdx b/apps/docs/snippets/extensions/structured-content.mdx index c1b3fe36a7..0b5c15a585 100644 --- a/apps/docs/snippets/extensions/structured-content.mdx +++ b/apps/docs/snippets/extensions/structured-content.mdx @@ -67,6 +67,34 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' } }, ], + [ + { + label: 'Toggle inline lock', + onClick: (superdoc) => { + const editor = superdoc?.activeEditor || superdoc?.editor + if (!editor?.commands) return + const tags = editor.helpers.structuredContentCommands.getStructuredContentTags(editor.state) + const field = tags.find(t => String(t.id) === '1') + const locked = field?.lockMode && field.lockMode !== 'unlocked' + editor.commands.updateStructuredContentById('1', { + attrs: { lockMode: locked ? 'unlocked' : 'sdtContentLocked' }, + }); + } + }, + { + label: 'Toggle block lock', + onClick: (superdoc) => { + const editor = superdoc?.activeEditor || superdoc?.editor + if (!editor?.commands) return + const tags = editor.helpers.structuredContentCommands.getStructuredContentTags(editor.state) + const field = tags.find(t => String(t.id) === '2') + const locked = field?.lockMode && field.lockMode !== 'unlocked' + editor.commands.updateStructuredContentById('2', { + attrs: { lockMode: locked ? 'unlocked' : 'sdtContentLocked' }, + }); + } + }, + ], [ { label: 'Delete all fields', From e346c877f31f9068975c0192ca19ef0011414ffb Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Tue, 17 Feb 2026 08:58:02 -0300 Subject: [PATCH 13/16] fix(docs): access node.attrs for lock toggle state detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getStructuredContentTags returns {node, pos} objects — attrs are on node.attrs, not directly on the object. --- apps/docs/snippets/extensions/structured-content.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/docs/snippets/extensions/structured-content.mdx b/apps/docs/snippets/extensions/structured-content.mdx index 0b5c15a585..65bcdf173b 100644 --- a/apps/docs/snippets/extensions/structured-content.mdx +++ b/apps/docs/snippets/extensions/structured-content.mdx @@ -74,8 +74,8 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' const editor = superdoc?.activeEditor || superdoc?.editor if (!editor?.commands) return const tags = editor.helpers.structuredContentCommands.getStructuredContentTags(editor.state) - const field = tags.find(t => String(t.id) === '1') - const locked = field?.lockMode && field.lockMode !== 'unlocked' + const field = tags.find(t => String(t.node.attrs.id) === '1') + const locked = field?.node.attrs.lockMode && field.node.attrs.lockMode !== 'unlocked' editor.commands.updateStructuredContentById('1', { attrs: { lockMode: locked ? 'unlocked' : 'sdtContentLocked' }, }); @@ -87,8 +87,8 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' const editor = superdoc?.activeEditor || superdoc?.editor if (!editor?.commands) return const tags = editor.helpers.structuredContentCommands.getStructuredContentTags(editor.state) - const field = tags.find(t => String(t.id) === '2') - const locked = field?.lockMode && field.lockMode !== 'unlocked' + const field = tags.find(t => String(t.node.attrs.id) === '2') + const locked = field?.node.attrs.lockMode && field.node.attrs.lockMode !== 'unlocked' editor.commands.updateStructuredContentById('2', { attrs: { lockMode: locked ? 'unlocked' : 'sdtContentLocked' }, }); From 1b147952d838613165ba8bcbc6311872c29d03d2 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Tue, 17 Feb 2026 09:00:18 -0300 Subject: [PATCH 14/16] fix(docs): use setNodeAttribute for lock toggle to bypass filterTransaction updateStructuredContentById uses replaceWith which creates a ReplaceStep that the lock plugin blocks. Use tr.setNodeAttribute instead, which creates an AttrStep that the lock plugin explicitly skips. --- .../extensions/structured-content.mdx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/apps/docs/snippets/extensions/structured-content.mdx b/apps/docs/snippets/extensions/structured-content.mdx index 65bcdf173b..845d905d07 100644 --- a/apps/docs/snippets/extensions/structured-content.mdx +++ b/apps/docs/snippets/extensions/structured-content.mdx @@ -72,26 +72,28 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' label: 'Toggle inline lock', onClick: (superdoc) => { const editor = superdoc?.activeEditor || superdoc?.editor - if (!editor?.commands) return + if (!editor?.state) return const tags = editor.helpers.structuredContentCommands.getStructuredContentTags(editor.state) const field = tags.find(t => String(t.node.attrs.id) === '1') - const locked = field?.node.attrs.lockMode && field.node.attrs.lockMode !== 'unlocked' - editor.commands.updateStructuredContentById('1', { - attrs: { lockMode: locked ? 'unlocked' : 'sdtContentLocked' }, - }); + if (!field) return + const locked = field.node.attrs.lockMode && field.node.attrs.lockMode !== 'unlocked' + const tr = editor.state.tr + tr.setNodeAttribute(field.pos, 'lockMode', locked ? 'unlocked' : 'sdtContentLocked') + editor.view.dispatch(tr) } }, { label: 'Toggle block lock', onClick: (superdoc) => { const editor = superdoc?.activeEditor || superdoc?.editor - if (!editor?.commands) return + if (!editor?.state) return const tags = editor.helpers.structuredContentCommands.getStructuredContentTags(editor.state) const field = tags.find(t => String(t.node.attrs.id) === '2') - const locked = field?.node.attrs.lockMode && field.node.attrs.lockMode !== 'unlocked' - editor.commands.updateStructuredContentById('2', { - attrs: { lockMode: locked ? 'unlocked' : 'sdtContentLocked' }, - }); + if (!field) return + const locked = field.node.attrs.lockMode && field.node.attrs.lockMode !== 'unlocked' + const tr = editor.state.tr + tr.setNodeAttribute(field.pos, 'lockMode', locked ? 'unlocked' : 'sdtContentLocked') + editor.view.dispatch(tr) } }, ], From 1e7ee833582bb25e7f4306da55e7bca6bb9728d7 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Tue, 17 Feb 2026 16:53:52 -0300 Subject: [PATCH 15/16] docs(structured-content): address review feedback - Differentiate lock mode insert example from command section example (use sdtContentLocked/Account ID instead of duplicate sdtLocked/Customer Name) - Wrap enforcement details in Expandable for scannability - Add missing `group` to block insert attrs type - Replace raw tr.setNodeAttribute with updateStructuredContentById in demo --- apps/docs/extensions/structured-content.mdx | 20 ++++++++++--------- .../extensions/structured-content.mdx | 16 +++++++-------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/apps/docs/extensions/structured-content.mdx b/apps/docs/extensions/structured-content.mdx index d406e46824..35163304c9 100644 --- a/apps/docs/extensions/structured-content.mdx +++ b/apps/docs/extensions/structured-content.mdx @@ -67,11 +67,11 @@ Set the lock mode when inserting: ```javascript Usage editor.commands.insertStructuredContentInline({ attrs: { - id: '1', - alias: 'Customer Name', - lockMode: 'sdtLocked', + id: '3', + alias: 'Account ID', + lockMode: 'sdtContentLocked', }, - text: 'John Doe', + text: 'ACC-00042', }); ``` @@ -86,11 +86,11 @@ const superdoc = new SuperDoc({ const editor = superdoc.activeEditor; editor.commands.insertStructuredContentInline({ attrs: { - id: '1', - alias: 'Customer Name', - lockMode: 'sdtLocked', + id: '3', + alias: 'Account ID', + lockMode: 'sdtContentLocked', }, - text: 'John Doe', + text: 'ACC-00042', }); }, }); @@ -123,6 +123,7 @@ const superdoc = new SuperDoc({ ``` + Lock modes are enforced at the editor plugin level using a three-layer defense: 1. **Key interception** — Delete, Backspace, and Cut are blocked before a transaction is created, preventing cursor jumps @@ -130,6 +131,7 @@ Lock modes are enforced at the editor plugin level using a three-layer defense: 3. **Transaction filter** — Safety net that catches paste, drag-drop, and programmatic edits Users can still place their cursor inside locked content and select text for copying. Only modifications are blocked. + Lock modes round-trip through DOCX. A document with `w:lock` elements in @@ -696,7 +698,7 @@ type StructuredContentLockMode = 'unlocked' | 'sdtLocked' | 'contentLocked' | 's ProseMirror JSON - Node attributes (`id`, `alias`, `tag`, `lockMode`) + Node attributes (`id`, `alias`, `tag`, `lockMode`, `group`) diff --git a/apps/docs/snippets/extensions/structured-content.mdx b/apps/docs/snippets/extensions/structured-content.mdx index 845d905d07..af171ad828 100644 --- a/apps/docs/snippets/extensions/structured-content.mdx +++ b/apps/docs/snippets/extensions/structured-content.mdx @@ -72,28 +72,28 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' label: 'Toggle inline lock', onClick: (superdoc) => { const editor = superdoc?.activeEditor || superdoc?.editor - if (!editor?.state) return + if (!editor?.commands) return const tags = editor.helpers.structuredContentCommands.getStructuredContentTags(editor.state) const field = tags.find(t => String(t.node.attrs.id) === '1') if (!field) return const locked = field.node.attrs.lockMode && field.node.attrs.lockMode !== 'unlocked' - const tr = editor.state.tr - tr.setNodeAttribute(field.pos, 'lockMode', locked ? 'unlocked' : 'sdtContentLocked') - editor.view.dispatch(tr) + editor.commands.updateStructuredContentById('1', { + attrs: { lockMode: locked ? 'unlocked' : 'sdtContentLocked' }, + }) } }, { label: 'Toggle block lock', onClick: (superdoc) => { const editor = superdoc?.activeEditor || superdoc?.editor - if (!editor?.state) return + if (!editor?.commands) return const tags = editor.helpers.structuredContentCommands.getStructuredContentTags(editor.state) const field = tags.find(t => String(t.node.attrs.id) === '2') if (!field) return const locked = field.node.attrs.lockMode && field.node.attrs.lockMode !== 'unlocked' - const tr = editor.state.tr - tr.setNodeAttribute(field.pos, 'lockMode', locked ? 'unlocked' : 'sdtContentLocked') - editor.view.dispatch(tr) + editor.commands.updateStructuredContentById('2', { + attrs: { lockMode: locked ? 'unlocked' : 'sdtContentLocked' }, + }) } }, ], From 3d924fcfccfea0ee37c632f4dfbe0bf9d3302346 Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Tue, 17 Feb 2026 17:42:23 -0300 Subject: [PATCH 16/16] docs(structured-content): remove toggle lock buttons from demo --- .../extensions/structured-content.mdx | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/apps/docs/snippets/extensions/structured-content.mdx b/apps/docs/snippets/extensions/structured-content.mdx index af171ad828..c1b3fe36a7 100644 --- a/apps/docs/snippets/extensions/structured-content.mdx +++ b/apps/docs/snippets/extensions/structured-content.mdx @@ -67,36 +67,6 @@ import { SuperDocEditor } from '/snippets/components/superdoc-editor.jsx' } }, ], - [ - { - label: 'Toggle inline lock', - onClick: (superdoc) => { - const editor = superdoc?.activeEditor || superdoc?.editor - if (!editor?.commands) return - const tags = editor.helpers.structuredContentCommands.getStructuredContentTags(editor.state) - const field = tags.find(t => String(t.node.attrs.id) === '1') - if (!field) return - const locked = field.node.attrs.lockMode && field.node.attrs.lockMode !== 'unlocked' - editor.commands.updateStructuredContentById('1', { - attrs: { lockMode: locked ? 'unlocked' : 'sdtContentLocked' }, - }) - } - }, - { - label: 'Toggle block lock', - onClick: (superdoc) => { - const editor = superdoc?.activeEditor || superdoc?.editor - if (!editor?.commands) return - const tags = editor.helpers.structuredContentCommands.getStructuredContentTags(editor.state) - const field = tags.find(t => String(t.node.attrs.id) === '2') - if (!field) return - const locked = field.node.attrs.lockMode && field.node.attrs.lockMode !== 'unlocked' - editor.commands.updateStructuredContentById('2', { - attrs: { lockMode: locked ? 'unlocked' : 'sdtContentLocked' }, - }) - } - }, - ], [ { label: 'Delete all fields',