0% прочитано

Vue Rich Text Editor Drag and Drop: When Block UI Text Became Editable Content

After reordering Content Blocks in a Vue editor, block UI labels could unexpectedly appear inside Rich Text content: locale, position, anchor help, editor labels, and other interface text. This article explores why drag and drop around contenteditable needs stable Vue keys, strict DOM boundaries, correct editor lifecycle handling, and an explicit content synchronization contract.

21 вересня 2026 р. 12 хв читанняVue.js

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.

Drag and drop in a block editor should change only block order. Toolbar text, locale, position, anchor help, and other interface labels must never become part of Rich Text content.
Vue ContentBlocks drag and drop bug showing block UI labels outside a contenteditable Rich Text editor leaking into editable content after reorder
The Rich Text boundary must remain explicit: block UI can exist beside the editor without becoming part of its content model.

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.

Interface text that does not belong to content
Text (editor) #5EN Position: 5 Anchor ID Enter without #.Link as #service-inquiry. Paragraph

Every 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.

HTML - Block UI and editable content have different responsibilities
<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.

JavaScript - A DOM boundary that is too broad
// 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.

JavaScript - Read content from the editor model
const html = editor.getHTML();
These snippets illustrate the responsibility boundary. They are not a claim about the exact code that caused the ICanUp bug.

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.

HTML - Index keys are risky for stateful editors
<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.

HTML - Stable block identity
<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.

Position changes while editor identity remains stable
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 B

If 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.

HTML - Dedicated drag-start region
<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.

One gesture can cross several browser mechanisms
User selection      |      vdragstart      |      +--> block reorder?      |      +--> native text drag?      |      +--> DataTransfer content?      |      vdrop target

A 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.

Position changes, content does not
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.

Locale belongs to block integrity
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 block

Why 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.

One editor is reused by multiple entities
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 DOM is not a persistence format
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 only

The 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.

JavaScript - A new block can receive stable identity before persistence
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.

JavaScript - Filtering symptoms instead of fixing the boundary
// 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.

JavaScript - Verify ordering and content integrity together
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>');
The concrete test API depends on the editor implementation. The important contract is that reorder cannot add, remove, or transfer Rich Text between blocks.

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.