Skip to content

0026. A click into a link places the caret, and an upstream editing behavior changed for Drupal authors is an editor toggle, on by default

Status: Accepted

Date: 2026-09-20

Context

Upstream opens any link on a plain left click in an editable view (node_modules/@blocknote/core/src/extensions/tiptap-extensions/Link/helpers/clickHandler.ts:21-77), with window.open(href, target) at lines 73-75 and target="_blank" coming from the link mark's own render defaults (node_modules/@blocknote/core/src/extensions/tiptap-extensions/Link/link.ts:79-84), unless links.onClick is set (node_modules/@blocknote/core/src/editor/BlockNoteEditor.ts:151-160, which documents returning false as "let ProseMirror continue"). The module passed links.isValidLink alone (js/src/editor.tsx:302 before this change), so an author could not click into linked text to edit it; the maintainer met this from a media caption that the link button had linked whole (0025).

ProseMirror runs handleClick on mouseup and prevents the mouseup's default only for a true result (node_modules/prosemirror-view/src/input.ts:415-419), so a handler that returns false leaves the caret where the browser's own click already put it, which is what places the caret in the text: nothing further has to be done for that half of the behavior.

Drupal authors' convention is CKEditor 5's: a click places the caret, Ctrl+click or Cmd+click opens the link. Core's own implementation, LinkEditing._enableLinkOpen(), is vendored minified at web/core/assets/vendor/ckeditor5/link/link.js; it opens with window.open(href, "_blank", "noopener") only when the platform's own modifier is held. Some sites migrating an editor whose own behavior matched upstream's, opening a link on any click, will want that back, which is why this is a toggle rather than a fixed change.

0016 places a setting only the client reads on the editor entity, beside seven toggles at the time it was written; no record before this one said what makes a behavior a toggle rather than a fixed change or a matter for the format's policy.

Decision

(1) The setting. ui.click_into_links, boolean, the eighth UI_TOGGLES entry, on by default, labeled "Click into links". On, createEditor() sets links.onClick to openLinkOnModifiedClick() (js/src/links.tsx): with Ctrl or Cmd held it finds the anchor with upstream's own selector, a[data-inline-content-type="link"], opens its href with window.open(href, '_blank', 'noopener') and returns true; otherwise it returns false and the click places the caret, as ProseMirror already does for any click inside the editable. Off, links.onClick is left undefined and upstream's own handler runs exactly as shipped. Both keys are accepted on every platform rather than switching on the operating system the way CKEditor 5 does, because in Chromium a macOS Ctrl+click is reported as a secondary button and never reaches the handler at all, so accepting both costs nothing and needs no platform check. The link toolbar's Open in new tab button stays the keyboard route either way (0011). Amended 2026-09-26. That button is the module's own OpenLinkAction, named "Open link", now that "Open in a new tab" names the separate per-link toggle 0032 adds. Amended 2026-09-26. "Open in a new tab" no longer names a toolbar button. It is the link form's checkbox label now, per 0032's amendment of the same date; OpenLinkAction, named "Open link", stays the keyboard route this sentence describes.

(2) The rule. An upstream editing behavior changed for Drupal authors' expectations is a toggle on the editor entity, on by default, with upstream's own behavior restorable by switching it off. Not a toggle by this rule: the format's policy (0010, 0016, 0017), which is enforced on save and is not a preference; behavior upstream lacks, which has no upstream state to restore (0019's toolbar chrome, 0021's caption guards, 0022's Escape); and a defect fix, which restores nothing because there was nothing correct to restore. paste_markdown (0016) is the precedent this rule reads back through: it too changed an upstream default for Drupal authors, on by default, restorable off. Amended 2026-09-20. The precedent is stated backwards: for paste_markdown, on is BlockNote's own behavior and off is the module's alternative (js/src/editor.tsx, pasteAsMarkdown()), while for click_into_links on is the changed behavior and off restores BlockNote's. What the two share is the home 0016 gives a setting only the client reads, not the direction. The rule therefore reads: the toggle's default is what a Drupal author expects, BlockNote's own behavior stays reachable through the other position, and the record for each toggle says which position is BlockNote's. Amended 2026-11-02. Not a toggle either: a change in where upstream draws a surface, when what an author can do there and what the editor writes stay the same. Since BlockNote 0.57.0, on a touch device with the on-screen keyboard open, FormattingToolbarController (node_modules/@blocknote/react/src/components/FormattingToolbar/) draws the formatting toolbar, the module's own items included, in .bn-mobile-formatting-toolbar, a bar pinned above the keyboard and portalled into a themed .bn-root of its own appended to document.body, rather than beside the selection. That is upstream's presentation and the module keeps it, with no setting, because overriding it would be a fork to carry through every upgrade. What the module owns there is that its stylesheet reaches the bar and that the bar is not drawn behind a dialog holding the editor: js/src/theme-isolation.css declares the text colors and the link form's refusal color on that root as well, and lifts the root to z-index 1261, above core's off-canvas tray at 501 and the dialogs of Claro and Default Admin at 1260, where upstream stacks the bar at 40 and a menu opened from it at 300. tests/src/FunctionalJavascript/TouchToolbarTest.php measures both and taps the module's Subscript in the bar, under Chrome's touch emulation sent through ChromeDriver's DevTools passthrough, with the window shrunk by more than the 150 pixels upstream reads as an open keyboard. That emulation has limits the test cannot get past: the window does not grow back when focus leaves the editable, as a real keyboard closes, so a tap is checked by where focus is afterwards; it is Chromium, not Safari on iOS; and in it the color menu opened from the bar opens below the bar, into the few pixels left above the window's edge, with or without the module's stylesheet. The maintainer tries the bar on a real phone. Amended 2026-11-02. A modal dialog's focus trap is the module's to answer too. jQuery UI's modal dialog takes focus back from anything outside it, and the bar and the link form it opens are outside the dialog holding the editor, so the form's URL field could not keep focus and an address typed for the link went into the document over the selection. js/blocknote.dialog.js lets focus into a .bn-root that is a child of the body, through the dialog's _allowInteraction(); hook_library_info_alter() adds it to core's drupal.dialog library, after jQuery UI's dialog, as core's CKEditor 5 module adds its own exception for CKEditor 5's panels. TouchToolbarTest opens the link form from the bar inside a modal dialog and links the selection with it. Amended 2026-11-02. The bar was checked on a real phone: the maintainer used it on an iPhone, on the development site over the local network, and it worked there, which the touch emulation of TouchToolbarTest could not show. No Android phone was tried.

Alternatives considered

Fixing the click without a setting. Rejected by the maintainer: a site migrating from a Notion-like editing flow wants the click to open a link the way it always has, and the toggle costs one checkbox against that.

A filter setting. Rejected. Nothing on the save or render path reads it; by 0016's rule a setting only the client reads belongs on the editor entity, not the filter.

isMac ? metaKey : ctrlKey, as CKEditor 5 does. Not taken. Both keys are accepted on every platform instead, the shape agreed above; in Chromium a macOS Ctrl+click is reported as a secondary button and never reaches the handler, so branching on the platform would add a check that changes nothing observable.

An update hook. Not needed. Nothing is released yet. A stored editor entity that lacks the key falls back to on in getJSSettings(), the same fallback every other toggle already has, and is completed by the next save of the format form.

Consequences

BlockNote::UI_TOGGLES, the schema's ui mapping, config/optional/editor.editor.blocknote.yml, the demo recipe's editor, EditorUiKey and the README's toggle count and list all grow by one. EditorPluginTest::testJsSettingsCarryTheUiToggles() and the new testClickIntoLinksDefaultsToOn(), ShippedConfigurationTest::testTheShippedEditorNamesEveryInterfaceToggle(), js/src/__tests__/editor.test.ts's clickIntoLinks cases and its two new createEditor cases, js/src/__tests__/links.test.tsx's openLinkOnModifiedClick cases and the new tests/src/FunctionalJavascript/LinkClickTest.php pin it.

A site whose editor entity was stored before this key existed is not flagged at runtime and is completed on its next save; because editor.editor.* opts into FullyValidatable, typed-config validation reports the missing key until then, the situation blocknote_post_update_editor_settings_surface() handled for paste_markdown and which nothing released yet has to handle for this key too.

Browsers that follow a link inside an editable region natively would open a modified click twice, once natively and once through window.open(); Chromium and Firefox do not follow links inside a contenteditable, and the module does not itself prevent the click's default, so this does not happen in either tested browser.

0016's table gains a row for this setting. The per-link "open in a new tab" choice some sites will also want is a separate design not decided by this record. Amended 2026-09-26. Decided in 0032.

References

  • node_modules/@blocknote/core/src/extensions/tiptap-extensions/Link/helpers/clickHandler.ts, node_modules/@blocknote/core/src/editor/BlockNoteEditor.ts.
  • node_modules/prosemirror-view/src/input.ts.
  • web/core/assets/vendor/ckeditor5/link/link.js.
  • node_modules/@blocknote/core/src/extensions/tiptap-extensions/Link/link.ts.
  • src/Plugin/Editor/BlockNote.php, config/schema/blocknote.schema.yml, config/optional/editor.editor.blocknote.yml, recipes/blocknote_demo/config/editor.editor.blocknote_basic.yml, js/src/editor.tsx, js/src/links.tsx, js/src/drupal.d.ts.
  • Tests: js/src/__tests__/editor.test.ts, js/src/__tests__/links.test.tsx, tests/src/Kernel/EditorPluginTest.php, tests/src/Kernel/ShippedConfigurationTest.php, tests/src/FunctionalJavascript/LinkClickTest.php, tests/src/FunctionalJavascript/LinkToolbarTest.php.
  • 0010, 0011, 0016, 0017, 0025.