npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

editorjs-cross-block-format

v0.8.3

Published

Cross-block inline formatting (bold/italic/underline) for Editor.js — select text across several blocks and format it in one go

Downloads

45

Readme

editorjs-cross-block-format

Select from the middle of one Editor.js paragraph to the middle of another and apply bold, italic or underline to exactly that — not to the paragraphs it happens to touch. Images, embeds and other non-text blocks in between are skipped.

The partial part is the whole point. Editor.js gives every block its own contenteditable, and the moment a selection leaves the block it started in, the core throws the text selection away and replaces it with whole blocks marked ce-block--selected. Where the selection began and ended is gone by then, so anything built on top of that state can only format blocks end to end. This package keeps the real selection instead: it paints the Range itself, splits it per block, and formats the first block from the selection start, the last one up to the selection end, and only the middle ones entirely.

Three things stand in the way of that, and each is handled here — the browser will not extend a mouse selection past a block, Editor.js wipes the selection that does get built, and the browser mistakes a hand-built selection for a text drag and moves the text on drop.

Not an Editor.js plugin — nothing to put in tools, and no inline tool to subclass. It attaches to an editor that is already running.

Selecting from mid-word in one paragraph to mid-word in another, then formatting

Live demo — drag a selection across the blocks, or press Run self-check to watch the assertions run.

Install

npm install editorjs-cross-block-format

Usage

import EditorJS from "@editorjs/editorjs";
import { attachCrossBlockFormat } from "editorjs-cross-block-format";

const holder = document.getElementById("editor");

const editor = new EditorJS({
  holder,
  onReady: () => attachCrossBlockFormat(holder),
});

Drag a selection across two or more blocks: the editor's own Inline Toolbar appears above it — a working copy of it, since the core's buttons cannot act on more than one block. Ctrl/Cmd + B / I / U work on the same selection, Escape dismisses the toolbar. Single-block selections are left alone — the native Inline Toolbar keeps handling those.

Options

const detach = attachCrossBlockFormat(holder, {
  tools: ["bold", "italic"],       // default: bold, italic, underline
  titles: { bold: "Bold text" },   // button tooltips, for i18n
});

detach(); // removes listeners and the toolbar

The toolbar is a clone of the editor's own Inline Toolbar — the actual DOM Editor.js built for this instance, with whatever inline tools, icons and order this project configured — rewired to act on the whole selection. Nothing is imitated, so nothing drifts when the editor is upgraded or its tools change. Tools that cannot work across blocks (converting a block, links) stay visible but disabled, and the core's own toolbar is kept closed while this one is up: its buttons format a single block, and clicking one over a cross-block selection would format that block alone and drop the rest of the selection.

Editor.js builds its Inline Toolbar lazily, so on the very first selection of a session there may be nothing to clone yet; a plain toolbar with the same markup and icons stands in until a clone becomes available (that is the only place titles is used — a clone carries the editor's own tooltips). Each button also gets .ecbf-toolbar__button--{bold,italic,underline} for styling, and the root is .ecbf-toolbar.

How it works

  1. The selection is driven by hand. A browser does not extend a mouse selection past the editing host it started in — drag out of a block and the selection stops at its end. So the caret under the pointer is read on every mousemove (caretRangeFromPoint) and the Range is set from the mousedown anchor to it. Set programmatically, a Range may span several hosts, even though a mouse cannot build one.
  2. Editor.js is kept out of it. Its CrossBlockSelection listens for mouseover on document in the bubble phase and calls Selection.removeAllRanges() the moment the pointer crosses a block boundary. A capture-phase listener swallows that event for the duration of the drag, so the core never learns a cross-block drag is happening.
  3. The core's Inline Toolbar is cloned and kept closed while ours is up, watched by a MutationObserver — Editor.js reopens it on its own schedule, after ours is already placed. Its buttons format a single block, so clicking one over a cross-block selection would format that block alone and drop the rest of the selection.
  4. Native text drag-and-drop is suppressed. With a selection painted under a held button, the browser decides the pointer is dragging selected text and, on drop, MOVES it — paragraphs silently relocate. dragstart is cancelled while our drag is in flight. Drags that start outside a text area (a block's drag handle, a toolbar) are left alone, so Editor.js keeps its own block dragging.
  5. The Range is split per block on mouseup, clipped to each block's editable area: the first block from the selection start to its end, the middle blocks entirely, the last block up to the selection end. Blocks with no editable area — images, embeds, custom tools — drop out of that list, which is what lets pictures sit between formatted paragraphs. Blocks are matched by Range.intersectsNode, not by the Range's own containers: a drag starting in the whitespace between blocks puts a boundary outside every block.
  6. Each part is selected in turn and document.execCommand is applied to it. Editor.js picks the DOM changes up through its own ModificationsObserver, so onChange fires and editor.save() returns the new markup — nothing extra to wire up.

Two things bite when rebuilding state around all this. Selection.toString() returns "" for a selection spanning editing hosts even though its Range is intact, and Selection.isCollapsed is no more trustworthy there — read the Range. And Selection keeps the very Range object it is handed, which execCommand then rewrites: pass it a copy, or the parts still waiting their turn come out mangled.

Limitations

  • Mouse selection only. Cross-block selection via Shift + arrows is Editor.js's own keyboard flow and is left untouched.
  • Mixed formatting inside one block is resolved by the first block's state: the whole selection goes one way. A block that is half bold already may come out fully bold.
  • The toolbar hides on scroll rather than following the selection.
  • Built against the Editor.js 2.19–2.31 DOM (.codex-editor__redactor > .ce-block, [contenteditable="true"]). Check those selectors on a major upgrade.
  • Chrome/Firefox/Safari via document.execCommand, which is deprecated but has no replacement for this job and is not going anywhere.

Demo

npm install @editorjs/editorjs   # peer dependency, only needed for the demo
python3 -m http.server 8777      # or any static server

Try it live, or open http://localhost:8777/demo/index.html. The Run self-check button runs the test suite in the page: it drives a cross-block drag with mouse events only — the selection is built by the module under test, never by the test — then applies bold and asserts the resulting DOM. It includes a negative control (detached, the selection stops at the first block), a check that the native text drag was cancelled, and a check that block dragging by the handle still works.

License

MIT