A very unusual bug appeared in the shared Content Blocks editor in ICanUp: after reordering blocks, text that I had never entered could appear inside Rich Text content.
These were not random characters. The saved content could contain interface labels from the block itself: editor labels, locale, block position, anchor ID help, and paragraph controls.
Drag and drop was therefore capable of affecting more than block order. In a certain scenario, it could corrupt the Rich Text content itself.
That is a particularly dangerous editor bug because UI text can silently enter the content model and later be persisted as real public content.

Vue Rich Text Content Contained UI Text After Drag and Drop
The observed result looked roughly like this: normal content could be mixed with block interface labels.
Text (editor) #5EN Position: 5 Anchor ID Enter without #.Link as #service-inquiry. ParagraphEvery one of those labels legitimately belongs to the admin interface.
None of them belongs inside the Rich Text value rendered on the public page.
Why This Bug Is More Dangerous Than a Normal Drag-and-Drop Failure
If a block ends up in the wrong position after a reorder, the error is immediately visible.
Content corruption can be much harder to notice.
- interface text may appear deep inside a long document;
- an editor may not notice it before saving;
- after Save it becomes persisted data;
- the problem may repeat after another reorder;
- the same shared editor is used by multiple content types.
It is therefore not enough to make drag and drop look correct again. The content integrity contract itself needs protection.
contenteditable Does Not Know Which Text Is Application UI
From the browser perspective, contenteditable works with DOM and selection.
It does not know that Position: 5 is Vue component metadata while the paragraph next to it is author content.
The application structure and editor integration have to enforce that boundary.
<div class="content-block"> <header class="block-ui"> <span>EN</span> <span>Position: 5</span> <button>Move</button> </header> <div class="editor-shell"> <div contenteditable="true"> Real article content </div> </div> </div>Visually, all of this may be one card.
For the editor, however, the valid content boundary is much narrower: only the editor root belongs to the document model.
Visual proximity does not mean that elements belong to the same editable content model.
First Check: Does Rich Text Read Only Its Own Editor Root?
One risky approach in a complex editor is serializing a DOM container that is broader than the actual editor.
// Too broad for a Rich Text content boundary.const html = blockElement.innerHTML;If blockElement contains both Rich Text and block UI, the structure itself allows more data to be collected than intended.
A safer contract takes content from the editor API or from its exact root element.
const html = editor.getHTML();Stable Vue Keys Matter When Blocks Are Reordered
The next important check is component identity after reorder.
If the key depends on array position, Vue may reuse a component instance in a way that a stateful Rich Text editor does not expect.
<ContentBlockEditor v-for="(block, index) in blocks" :key="index" :block="block"/>The issue is not that index is universally forbidden.
The issue is that after reorder, position is no longer the identity of a specific ContentBlock.
<ContentBlockEditor v-for="block in blocks" :key="block.clientKey" :block="block"/>A persisted block may use a stable ID. A new unsaved block may need a stable client-side key.
Reordering should change position without changing component identity.
Reusing the Wrong Editor Instance Can Corrupt State
A Rich Text editor often owns internal state, selection, listeners, and DOM references.
It is not equivalent to a simple static textarea.
Before reorder: Block A -> Editor instance ABlock B -> Editor instance BBlock C -> Editor instance C After reorder should still be: Block C -> Editor instance CBlock A -> Editor instance ABlock B -> Editor instance BIf an editor instance starts serving a different block after reorder, the resulting symptoms can be much more serious than incorrect ordering.
This is why stable keys and editor lifecycle are among the first boundaries I inspect.
The Drag Handle Should Be Separate from the Editable Area
Another important boundary is the region that can initiate block movement.
If the entire card including contenteditable is draggable, the browser needs to distinguish two complex interactions at once:
- the user is selecting or editing text;
- the user is starting a block reorder.
Those interactions are easier to control when movement starts from an explicit drag handle.
<div class="content-block"> <button class="drag-handle" type="button" > Move </button> <RichTextEditor /> </div>The editor remains responsible for selection and text input, while the separate control is responsible for moving the block.
Browser Selection and Drag Events Can Interact in Unexpected Ways
Browsers already support dragging selected text.
Selected content can be dragged into another editable region, so block reordering cannot be investigated as if text selection did not exist.
User selection | vdragstart | +--> block reorder? | +--> native text drag? | +--> DataTransfer content? | vdrop targetA particularly useful regression scenario is starting a reorder immediately after the cursor or selection has been active inside the editor.
Reordering Must Not Change the Rich Text Model
The core contract can be expressed very simply.
Before reorder: Block A:position = 1content = "<p>Alpha</p>" Block B:position = 2content = "<p>Beta</p>" After reorder: Block B:position = 1content = "<p>Beta</p>"If content changes together with position, two independent responsibilities have crossed their boundary.
Editor State and ContentBlock Model Need an Explicit Synchronization Contract
A complex editor contains at least two layers of state:
Layer | What it owns |
|---|---|
Rich Text editor | Document state, selection, internal editor state |
Vue ContentBlock model | Block type, position, locale, data, and metadata |
The integration needs a clear rule for when editor HTML is synchronized into the ContentBlock model and what happens during reorder.
Moving a block should not accidentally trigger serialization of unrelated surrounding DOM.
Locale Isolation Needs Its Own Regression Check
Content Blocks in ICanUp are localized.
Reordering must therefore protect not only Rich Text content but also UK and EN isolation.
UK block:locale = ukcontent = Ukrainian text EN block:locale = encontent = English text After repeated reorder: UK stays UKEN stays ENcontent stays attached to its own blockWhy an Entity-Specific Fix Would Be the Wrong Boundary
The problem may first become visible in one particular editor.
But when Post, Page, and News all use the same ContentBlocks editor, fixing only one screen hides the shared architectural problem.
Post ----+ |Page ----+--> Shared ContentBlocks Editor |News ----+The reorder plus Rich Text contract needs to be correct in the shared component.
The Block DOM Is Not the ContentBlock Data Model
This bug is a useful reminder of a fundamental boundary.
The admin editor DOM contains much more information than the public content model.
Admin block DOM: - block number- locale badge- position- action buttons- drag handle- anchor field- help text- editor toolbar- Rich Text content Persisted Rich Text: - user-authored content onlyThe HTML of an admin component should never become an implicit serialization format for public content.
A Safer Contract for Rich Text Plus Drag and Drop
- Every ContentBlock has stable identity independent of position.
- The Rich Text editor owns one explicit root.
- Content comes from the editor model or API, not the entire block DOM.
- Block movement starts from a dedicated drag handle.
- The editable region is not used as the drag handle.
- Reordering changes order but not Rich Text data.
- An editor instance remains attached to the same ContentBlock.
- Destroy and remount happen only when required by the editor lifecycle.
- Locale remains part of block identity.
- Save and reload restore only user-authored content.
Vue key Should Represent Identity, Not Position
This is especially important for stateful components.
const block = { id: null, clientKey: crypto.randomUUID(), position: 5, type: 'rich_text',};position may change ten times. clientKey should not change with it.
Do Not Fix Content Corruption by Removing Known UI Phrases
Once this kind of bug appears, it can be tempting to strip known labels before saving.
// Wrong direction.html = html .replace('Position:', '') .replace('Anchor ID', '') .replace('Paragraph', '');That approach is dangerous.
The words may be legitimate article content, new UI labels may appear later, and the original corruption path remains unresolved.
The goal is not to clean contaminated HTML. The goal is to prevent interface DOM from entering the editor model.
Regression Tests Need to Protect Content Integrity After Reorder
A test proving that the block array changed order is not enough.
The content must be checked as well.
const before = { first: '<p>Alpha</p>', second: '<p>Beta</p>',}; // Reorder blocks. expect(firstBlock.content).toBe( '<p>Beta</p>'); expect(secondBlock.content).toBe( '<p>Alpha</p>');Rich Text Drag-and-Drop Regression Test Matrix
Scenario | Expected result |
|---|---|
Single reorder | Only position changes |
Repeated reorder | Rich Text remains unchanged |
Unsaved content | Content is not lost while moving the block |
UK and EN | Locale content does not mix |
Rich Text plus another block type | Editor state remains attached to its block |
Save plus reload | Only real authored content is restored |
Post | Shared editor behaves correctly |
Page | Shared editor behaves correctly |
News | Shared editor behaves correctly |
Manual Browser Testing Still Matters for Selection and Drag Behavior
Some browser selection and drag interactions are difficult to reproduce completely with unit tests, so manual browser verification remains valuable.
- Enter visibly different content into two Rich Text blocks.
- Move the lower block above the first.
- Repeat reordering several times.
- Reorder immediately after placing the caret inside the editor.
- Reorder after selecting some text.
- Verify both UK and EN.
- Save the content.
- Reload the page.
- Confirm that no block UI labels entered the content.
- Repeat the scenario for Post, Page, and News.
What I Avoid After This Kind of Bug
- Do not use position as the only identity of a stateful Vue editor.
- Do not serialize the entire ContentBlock DOM as Rich Text content.
- Do not make the complete editable card the drag handle.
- Do not assume reorder cannot affect editor lifecycle.
- Do not strip known UI labels from persisted HTML as the primary fix.
- Do not fix a shared ContentBlocks bug only for one entity.
- Do not test only array ordering while ignoring Rich Text integrity.
- Do not forget locale isolation.
Vue Rich Text Editor Drag-and-Drop Checklist
- Define the exact editor root.
- Keep block UI outside the content model.
- Verify stable Vue keys.
- Do not tie component identity to index or position.
- Inspect editor lifecycle after reorder.
- Inspect destroy, remount, and instance reuse.
- Use a dedicated drag handle.
- Test native text selection and drag events.
- Update the ContentBlock model only from editor state.
- Guarantee that reorder does not modify content.
- Verify UK and EN isolation.
- Run repeated reorder plus Save plus reload regression.
- Verify every entity using the shared editor.
Block order, administrative UI, and user-authored content may live inside one Vue component, but they must not share the same data boundary.
Conclusion: Drag and Drop Should Move the ContentBlock, Not Its Rich Text
The Rich Text bug in ICanUp was unusual because drag and drop could affect more than visual ordering. It could affect the actual content.
Block interface labels existed beside the editor and, in a certain scenario, could appear inside Rich Text. That required looking beyond the draggable layer itself and checking Vue component identity, editor lifecycle, DOM boundaries, selection, drag events, and synchronization between editor state and the ContentBlock model.
The core contract is now very clear to me: reorder may change position. Text, locale, and the relationship between a Rich Text editor and its ContentBlock must remain stable.
The main lesson is that when a stateful editor lives inside draggable UI, the boundary between interface DOM and persisted content needs explicit protection.



