1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-24 20:19:10 +02:00

Desktop,Mobile: Markdown editor: Toggle checkboxes on ctrl-click (#12927)

This commit is contained in:
Henry Heino
2025-08-19 23:32:16 -07:00
committed by GitHub
parent af5c0135dc
commit c142c5c5c0
9 changed files with 132 additions and 45 deletions

View File

@@ -1004,6 +1004,8 @@ packages/editor/CodeMirror/editorCommands/sortSelectedLines.test.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.js
packages/editor/CodeMirror/editorCommands/supportsCommand.js
packages/editor/CodeMirror/extensions/biDirectionalTextExtension.js
packages/editor/CodeMirror/extensions/ctrlClickActionExtension.js
packages/editor/CodeMirror/extensions/ctrlClickCheckboxExtension.js
packages/editor/CodeMirror/extensions/keyUpHandlerExtension.js
packages/editor/CodeMirror/extensions/links/ctrlClickLinksExtension.js
packages/editor/CodeMirror/extensions/links/followLinkTooltipExtension.test.js
@@ -1074,9 +1076,11 @@ packages/editor/CodeMirror/utils/isInSyntaxNode.js
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/allLanguages.js
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/defaultLanguage.js
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/lookUpLanguage.js
packages/editor/CodeMirror/utils/markdown/getCheckboxAtPosition.js
packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.test.js
packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.js
packages/editor/CodeMirror/utils/markdown/stripBlockquote.js
packages/editor/CodeMirror/utils/markdown/toggleCheckboxAt.js
packages/editor/CodeMirror/utils/setupVim.js
packages/editor/ProseMirror/commands.test.js
packages/editor/ProseMirror/commands.js

4
.gitignore vendored
View File

@@ -977,6 +977,8 @@ packages/editor/CodeMirror/editorCommands/sortSelectedLines.test.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.js
packages/editor/CodeMirror/editorCommands/supportsCommand.js
packages/editor/CodeMirror/extensions/biDirectionalTextExtension.js
packages/editor/CodeMirror/extensions/ctrlClickActionExtension.js
packages/editor/CodeMirror/extensions/ctrlClickCheckboxExtension.js
packages/editor/CodeMirror/extensions/keyUpHandlerExtension.js
packages/editor/CodeMirror/extensions/links/ctrlClickLinksExtension.js
packages/editor/CodeMirror/extensions/links/followLinkTooltipExtension.test.js
@@ -1047,9 +1049,11 @@ packages/editor/CodeMirror/utils/isInSyntaxNode.js
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/allLanguages.js
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/defaultLanguage.js
packages/editor/CodeMirror/utils/markdown/codeBlockLanguages/lookUpLanguage.js
packages/editor/CodeMirror/utils/markdown/getCheckboxAtPosition.js
packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.test.js
packages/editor/CodeMirror/utils/markdown/renumberSelectedLists.js
packages/editor/CodeMirror/utils/markdown/stripBlockquote.js
packages/editor/CodeMirror/utils/markdown/toggleCheckboxAt.js
packages/editor/CodeMirror/utils/setupVim.js
packages/editor/ProseMirror/commands.test.js
packages/editor/ProseMirror/commands.js

View File

@@ -39,6 +39,7 @@ import selectedNoteIdExtension, { setNoteIdEffect } from './extensions/selectedN
import ctrlKeyStateClassExtension from './extensions/modifierKeyCssExtension';
import ctrlClickLinksExtension from './extensions/links/ctrlClickLinksExtension';
import { RenderedContentContext } from './extensions/rendering/types';
import ctrlClickCheckboxExtension from './extensions/ctrlClickCheckboxExtension';
// Newer versions of CodeMirror by default use Chrome's EditContext API.
// While this might be stable enough for desktop use, it causes significant
@@ -255,6 +256,7 @@ const createEditor = (
ctrlClickLinksExtension(link => {
props.onEvent({ kind: EditorEventType.FollowLink, link });
}),
ctrlClickCheckboxExtension(),
highlightSpecialChars(),
indentOnInput(),

View File

@@ -0,0 +1,33 @@
import { EditorView } from '@codemirror/view';
import { Prec } from '@codemirror/state';
const hasMultipleCursors = (view: EditorView) => {
return view.state.selection.ranges.length > 1;
};
type OnCtrlClick = (view: EditorView, event: MouseEvent)=> boolean;
const ctrlClickActionExtension = (onCtrlClick: OnCtrlClick) => {
return [
Prec.high([
EditorView.domEventHandlers({
mousedown: (event: MouseEvent, view: EditorView) => {
const hasModifier = event.ctrlKey || event.metaKey;
// The default CodeMirror action for ctrl-click is to add another cursor
// to the document. If the user already has multiple cursors, assume that
// the ctrl-click action is intended to add another.
if (hasModifier && !hasMultipleCursors(view)) {
const handled = onCtrlClick(view, event);
if (handled) {
event.preventDefault();
return true;
}
}
return false;
},
}),
]),
];
};
export default ctrlClickActionExtension;

View File

@@ -0,0 +1,30 @@
import { EditorView } from '@codemirror/view';
import modifierKeyCssExtension from './modifierKeyCssExtension';
import { syntaxTree } from '@codemirror/language';
import getCheckboxAtPosition from '../utils/markdown/getCheckboxAtPosition';
import toggleCheckboxAt from '../utils/markdown/toggleCheckboxAt';
import ctrlClickActionExtension from './ctrlClickActionExtension';
const ctrlClickCheckboxExtension = () => {
return [
modifierKeyCssExtension,
EditorView.theme({
'&.-ctrl-or-cmd-pressed .cm-taskMarker': {
cursor: 'pointer',
},
}),
ctrlClickActionExtension((view, event) => {
const target = view.posAtCoords(event);
const taskMarker = getCheckboxAtPosition(target, syntaxTree(view.state));
if (taskMarker) {
toggleCheckboxAt(target)(view);
return true;
}
return false;
}),
];
};
export default ctrlClickCheckboxExtension;

View File

@@ -4,12 +4,10 @@ import modifierKeyCssExtension from '../modifierKeyCssExtension';
import openLink from './utils/openLink';
import getUrlAtPosition from './utils/getUrlAtPosition';
import { syntaxTree } from '@codemirror/language';
import { Prec } from '@codemirror/state';
import ctrlClickActionExtension from '../ctrlClickActionExtension';
type OnOpenLink = (url: string, view: EditorView)=> void;
const ctrlClickLinksExtension = (onOpenExternalLink: OnOpenLink) => {
return [
modifierKeyCssExtension,
@@ -19,27 +17,16 @@ const ctrlClickLinksExtension = (onOpenExternalLink: OnOpenLink) => {
cursor: 'pointer',
},
}),
Prec.high([
EditorView.domEventHandlers({
mousedown: (event: MouseEvent, view: EditorView) => {
if (event.ctrlKey || event.metaKey) {
const target = view.posAtCoords(event);
const url = getUrlAtPosition(target, syntaxTree(view.state), view.state);
const hasMultipleCursors = view.state.selection.ranges.length > 1;
ctrlClickActionExtension((view: EditorView, event: MouseEvent) => {
const target = view.posAtCoords(event);
const url = getUrlAtPosition(target, syntaxTree(view.state), view.state);
// The default CodeMirror action for ctrl-click is to add another cursor
// to the document. If the user already has multiple cursors, assume that
// the ctrl-click action is intended to add another.
if (url && !hasMultipleCursors) {
openLink(url.url, view, onOpenExternalLink);
event.preventDefault();
return true;
}
}
return false;
},
}),
]),
if (url) {
openLink(url.url, view, onOpenExternalLink);
return true;
}
return false;
}),
];
};

View File

@@ -1,30 +1,10 @@
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
import { SyntaxNodeRef } from '@lezer/common';
import makeReplaceExtension from './utils/makeInlineReplaceExtension';
import toggleCheckboxAt from '../../utils/markdown/toggleCheckboxAt';
const checkboxClassName = 'cm-ext-checkbox-toggle';
const toggleCheckbox = (view: EditorView, linePos: number) => {
if (linePos >= view.state.doc.length) {
// Position out of range
return false;
}
const line = view.state.doc.lineAt(linePos);
const checkboxMarkup = line.text.match(/\[(x|\s)\]/);
if (!checkboxMarkup) {
// Couldn't find the checkbox
return false;
}
const isChecked = checkboxMarkup[0] === '[x]';
const checkboxPos = checkboxMarkup.index! + line.from;
view.dispatch({
changes: [{ from: checkboxPos, to: checkboxPos + 3, insert: isChecked ? '[ ]' : '[x]' }],
});
return true;
};
class CheckboxWidget extends WidgetType {
public constructor(private checked: boolean, private depth: number, private label: string) {
@@ -58,7 +38,7 @@ class CheckboxWidget extends WidgetType {
container.appendChild(checkbox);
checkbox.oninput = () => {
toggleCheckbox(view, view.posAtDOM(container));
toggleCheckboxAt(view.posAtDOM(container))(view);
};
this.applyContainerClasses(container);

View File

@@ -0,0 +1,21 @@
import { Tree } from '@lezer/common';
const getCheckboxAtPosition = (pos: number, tree: Tree) => {
let iterator = tree.resolveStack(pos);
while (true) {
if (iterator.node.name === 'TaskMarker') {
return iterator.node;
}
if (!iterator.next) {
break;
} else {
iterator = iterator.next;
}
}
return null;
};
export default getCheckboxAtPosition;

View File

@@ -0,0 +1,26 @@
import { Command, EditorView } from '@codemirror/view';
const toggleCheckbox = (linePos: number): Command => (target: EditorView) => {
const state = target.state;
if (linePos >= state.doc.length) {
// Position out of range
return false;
}
const line = state.doc.lineAt(linePos);
const checkboxMarkup = line.text.match(/\[(x|\s)\]/);
if (!checkboxMarkup) {
// Couldn't find the checkbox
return false;
}
const isChecked = checkboxMarkup[0] === '[x]';
const checkboxPos = checkboxMarkup.index! + line.from;
target.dispatch({
changes: [{ from: checkboxPos, to: checkboxPos + 3, insert: isChecked ? '[ ]' : '[x]' }],
});
return true;
};
export default toggleCheckbox;