`fixInLISelection` cancels drag-select when clicking to the right of a plain list item's text (regression in 8.8.0)
Provide detailed reproduction steps (if any)
Quirks.fixInLISelection calls preventDefault() on mousedown for any click landing to the right of a list item's text, including list items that contain no block child. That cancels the browser's native drag-selection, so a selection gesture started there produces nothing.
Minimal demo (paste into a fiddle — inline mode is not required, a classic editor reproduces it too):
<div id="editor">
<p>Paragraph above the list.</p>
<ul>
<li>First list item</li>
<li>Second list item, click to the right of this text</li>
</ul>
<h2>A heading after the list</h2>
<p>Paragraph below the heading.</p>
</div>
<script>
tinymce.init({ selector: '#editor', plugins: 'lists', license_key: 'gpl' });
</script>- Press the left mouse button in the empty space to the right of the text of the second
<li>(same line as the text, past its last character). - Keeping the button down, drag anywhere — up into the first list item, or down past the heading.
- Release.
Both drag directions fail. For contrast, performing the identical gesture starting to the right of the <h2> text works normally — the difference is the isListItem guard in the handler.
✔️ Expected result
A text selection is created, anchored at the end of the list item's text and extending to wherever the pointer was released — the same as starting the drag to the right of a paragraph or heading.
❌ Actual result
No selection is created. The caret is placed at the end of the list item's text and the selection stays collapsed for the whole gesture. Measured on mousedown: event.defaultPrevented === true; throughout mousemove and after mouseup: selection.isCollapsed === true, anchor and focus both at the end offset of the list item's text node.
The stack at the cancelling call is executeHandlers → dispatch → dispatch → fixInLISelection handler → Optional.fold → Arr.each → Arr.each → selectPos → preventDefault.
❓ Possible solution
The preventDefault() lives in the shared helper:
const selectPos = (editor, e, pos) => {
e.preventDefault(); // cancels the whole gesture, drag included
editor.focus();
editor.selection.setRng(pos.toRange());
};Two options, in order of preference:
Correct the caret without cancelling the gesture. Instead of repositioning on
mousedown, do it onmouseup/clickand only when the resulting selection is still collapsed. A drag then leaves its own selection intact, while a plain click still gets the corrected caret — this keeps both branches offixInLISelectionworking, including the original Chromium case.Minimal change: drop the
preventDefault()from the no-block-child branch. The Chromium bug this quirk works around (issues.chromium.org/issues/40767343) concerns aLIthat contains a block element — as the function's own comment states. For a plain<li>text</li>the browser already places the caret at the end of the text, so the correction is a no-op and thepreventDefault()is pure loss.
For what it's worth, the following works as a local workaround and leaves the block-child branch untouched (prepended so it runs before the quirk handler):
const hasBlockChild = (li) => Array.from(li.childNodes).some((c) =>
c.nodeName === 'BR' || (c.nodeType === 1 && getComputedStyle(c).display === 'block'));
editor.on('mousedown', (e) => {
if (e.detail !== 1 || e.button !== 0) return;
const li = e.target?.closest?.('li');
if (!li || hasBlockChild(li) || !li.lastChild) return;
const rng = document.createRange();
rng.selectNodeContents(li);
const rects = [...rng.getClientRects()];
const line = rects[rects.length - 1];
if (!line) return;
if (e.clientX >= line.right && e.clientY >= line.top && e.clientY <= line.bottom) {
e.preventDefault = () => {};
}
}, true);️ Other details
- Browser:
- Chrome / Chromium 148
- Vivaldi 8.1.4087.64 / Chromium 150.0.7871.232
- The quirk is Chromium-guarded, so Firefox and Safari are unaffected
- OS: Windows 10 and macOS 15
- First affected version: 8.8.0
- Worked in version: 8.7.0
In 8.7.0 the handler was firstBlockChildOrNewLine(target).each(...), so it only ran for list items that actually contain a block child. PR #11172 (milestone 8.8) turned that into .fold(...) and added the click-after-last-child branch, which is what brings plain list items into scope.
Source: tinymce/tinymce