文件
chen 301194963b
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
feat: Initial clean release of CHM
2026-11-04 11:29:39 +08:00

149 行
4.1 KiB
TypeScript

import { Plugin } from 'prosemirror-state';
import { Node, NodeSpec } from 'prosemirror-model';
import { EditorView, NodeView } from 'prosemirror-view';
import trimEmptyParagraphs from '../utils/trimEmptyParagraphs';
// See the fold example for more information about
// writing similar ProseMirror plugins:
// https://prosemirror.net/examples/fold/
const listGroup = 'block';
// Note: Use snake_case for schema keys for compatibility with certain default ProseMirror code
// (e.g. default input rules from prosemirror-example-setup).
export const nodeSpecs = {
task_list: {
content: 'task_list_item+',
group: listGroup,
parseDOM: [{ tag: 'ul[data-is-checklist]' }],
toDOM: () => ['ul', { class: 'task-list', 'data-is-checklist': '1' }, 0],
},
ordered_list: {
content: 'list_item+',
group: listGroup,
// Match attributes from https://github.com/ProseMirror/prosemirror-schema-list/blob/master/src/schema-list.ts
attrs: { order: { default: 1, validate: 'number' } },
parseDOM: [
{
tag: 'ol',
getAttrs: node => {
const start = node.hasAttribute('start') ? Number(node.getAttribute('start')) : 1;
return {
order: isFinite(start) ? start : 1,
};
},
},
],
toDOM: node => (
node.attrs.order === 1 ? ['ol', 0] : ['ol', { start: node.attrs.order }, 0]
),
},
bullet_list: {
content: 'list_item+',
group: listGroup,
parseDOM: [{ tag: 'ul:not([data-is-checklist])' }],
toDOM: () => ['ul', 0],
},
list_item: {
content: 'paragraph block*',
defining: true,
draggable: true,
parseDOM: [{ tag: 'li:not(.md-checkbox)' }],
toDOM: () => ['li', 0],
},
task_list_item: {
content: 'paragraph block*',
attrs: {
checked: { default: false, validate: 'boolean' },
},
defining: true,
draggable: true,
parseDOM: [
{
tag: 'li.md-checkbox',
getAttrs(node) {
const checkbox = node.querySelector<HTMLInputElement>('input[type=checkbox]');
return { checked: checkbox?.checked ?? false };
},
contentElement(node) {
const result = node.cloneNode(true) as HTMLElement;
// Empty paragraphs can cause rendering issues.
trimEmptyParagraphs(result);
const firstChild = result.children[0];
if (firstChild?.matches('div.checkbox-wrapper')) {
firstChild.remove();
// Trim empty paragraphs without the first child.
// Without this, multiple empty paragraphs can accumulate between
// the list item and its sub-lists.
trimEmptyParagraphs(result);
result.prepend(...firstChild.childNodes);
}
return result;
},
},
],
toDOM: (node) => {
return [
'li', { class: 'md-checkbox' },
['input', { type: 'checkbox', checked: node.attrs.checked ? true : undefined }],
['div', 0],
];
},
},
} satisfies Record<string, NodeSpec>;
type GetPosition = ()=> number;
let idCounter = 0;
class TaskListItemView implements NodeView {
public readonly dom: HTMLElement;
public contentDOM: HTMLElement;
private checkbox_: HTMLInputElement;
public constructor(node: Node, view: EditorView, getPosition: GetPosition) {
this.dom = document.createElement('li');
this.dom.id = `${(idCounter++)}-checkbox-container`;
this.dom.classList.add('checklist-item', 'md-checkbox', '-flex');
this.checkbox_ = document.createElement('input');
this.checkbox_.type = 'checkbox';
this.checkbox_.checked = node.attrs.checked;
// Don't use a <label> element as a container since clicking on the container
// should move focus to the task item, not focus the checkbox.
// Instead use aria-labelledby for accessibility:
this.checkbox_.setAttribute('aria-labelledby', this.dom.id);
this.contentDOM = document.createElement('div');
this.contentDOM.classList.add('content');
this.dom.appendChild(this.checkbox_);
this.dom.appendChild(this.contentDOM);
this.checkbox_.onchange = () => {
if (node.attrs.checked !== this.checkbox_.checked) {
view.dispatch(view.state.tr.setNodeAttribute(
getPosition(), 'checked', this.checkbox_.checked,
));
}
};
}
}
const listPlugin = new Plugin({
props: {
nodeViews: {
task_list_item: (node, view, getPos) => new TaskListItemView(node, view, getPos),
},
},
});
export default listPlugin;