feat: Initial clean release of CHM
一些检查失败了
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
一些检查失败了
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
这个提交包含在:
@@ -0,0 +1,195 @@
|
||||
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';
|
||||
|
||||
|
||||
class CheckboxWidget extends WidgetType {
|
||||
public constructor(private checked: boolean, private depth: number, private label: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
public eq(other: CheckboxWidget) {
|
||||
return other.checked === this.checked && other.depth === this.depth && other.label === this.label;
|
||||
}
|
||||
|
||||
private applyContainerClasses(container: HTMLElement) {
|
||||
container.classList.add(checkboxClassName);
|
||||
|
||||
for (const className of [...container.classList]) {
|
||||
if (className.startsWith('-depth-')) {
|
||||
container.classList.remove(className);
|
||||
}
|
||||
}
|
||||
|
||||
container.classList.add(`-depth-${this.depth}`);
|
||||
}
|
||||
|
||||
public toDOM(view: EditorView) {
|
||||
const container = document.createElement('span');
|
||||
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.checked = this.checked;
|
||||
checkbox.ariaLabel = this.label;
|
||||
checkbox.title = this.label;
|
||||
container.appendChild(checkbox);
|
||||
|
||||
checkbox.oninput = () => {
|
||||
const checkboxPosition = view.posAtDOM(container);
|
||||
view.dispatch({
|
||||
selection: { anchor: view.state.doc.lineAt(checkboxPosition).from },
|
||||
userEvent: 'select.checkbox',
|
||||
});
|
||||
toggleCheckboxAt(checkboxPosition)(view);
|
||||
};
|
||||
|
||||
this.applyContainerClasses(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
public updateDOM(dom: HTMLElement): boolean {
|
||||
this.applyContainerClasses(dom);
|
||||
|
||||
const input = dom.querySelector('input');
|
||||
if (input) {
|
||||
input.checked = this.checked;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public ignoreEvent() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const completedTaskClassName = 'cm-md-completed-item';
|
||||
const completedListItemDecoration = Decoration.line({ class: completedTaskClassName, isFullLine: true });
|
||||
|
||||
const replaceCheckboxes = [
|
||||
EditorView.theme({
|
||||
[`& .${checkboxClassName}`]: {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
verticalAlign: 'middle',
|
||||
transform: 'translateY(calc(-0.05em - 1px))',
|
||||
'& > input': {
|
||||
appearance: 'none',
|
||||
WebkitAppearance: 'none',
|
||||
fontSize: 'inherit',
|
||||
width: '1.15em',
|
||||
height: '1.15em',
|
||||
borderRadius: '50%',
|
||||
border: '1.5px solid var(--joplin-color-faded, #8a8a8e)',
|
||||
backgroundColor: 'transparent',
|
||||
margin: '0 8px 0 2px',
|
||||
verticalAlign: 'middle',
|
||||
cursor: 'pointer',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
transition: 'all 0.18s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
'&:hover': {
|
||||
borderColor: 'var(--joplin-color-accent, #007aff)',
|
||||
backgroundColor: 'rgba(0, 122, 255, 0.08)',
|
||||
},
|
||||
'&:checked': {
|
||||
backgroundColor: 'var(--joplin-color-accent, #007aff)',
|
||||
borderColor: 'var(--joplin-color-accent, #007aff)',
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14' fill='none' stroke='white' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 7.5 5.8 10.3 11 4.5'%3E%3C/polyline%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'center',
|
||||
backgroundSize: '75% 75%',
|
||||
animation: 'cm-checkbox-pop 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275)',
|
||||
},
|
||||
},
|
||||
'&:not(.-depth-1) > input': {
|
||||
marginInlineStart: 0,
|
||||
},
|
||||
},
|
||||
'@keyframes cm-checkbox-pop': {
|
||||
'0%': { transform: 'scale(0.8)' },
|
||||
'60%': { transform: 'scale(1.15)' },
|
||||
'100%': { transform: 'scale(1)' },
|
||||
},
|
||||
[`& .${completedTaskClassName}`]: {
|
||||
textDecoration: 'line-through',
|
||||
textDecorationColor: 'var(--joplin-color-faded, #8a8a8e)',
|
||||
opacity: 0.55,
|
||||
transition: 'opacity 0.2s ease, text-decoration-color 0.2s ease',
|
||||
},
|
||||
}),
|
||||
EditorView.domEventHandlers({
|
||||
mousedown: (event) => {
|
||||
const target = event.target as Element;
|
||||
if (target.nodeName === 'INPUT' && target.parentElement?.classList?.contains(checkboxClassName)) {
|
||||
// Let the checkbox handle the event
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
makeReplaceExtension({
|
||||
getRevealStrategy: (node, state) => {
|
||||
if (node.name === 'TaskMarker') {
|
||||
const container = node.node.parent?.parent;
|
||||
const listMarker = container?.getChild('ListMark');
|
||||
|
||||
// Intersection check logic similar to nodeIntersectsSelection but with custom range
|
||||
const selection = state.selection.main;
|
||||
const rangeFrom = listMarker ? listMarker.from : node.from;
|
||||
const rangeTo = node.to;
|
||||
|
||||
if (selection.empty && selection.from === rangeFrom) return false;
|
||||
const rangeContains = (point: number) => point >= rangeFrom && point <= rangeTo;
|
||||
const selectionContains = (point: number) => point >= selection.from && point <= selection.to;
|
||||
|
||||
// Reveal if cursor touches the checkbox or the list bullet point
|
||||
return rangeContains(selection.from) || rangeContains(selection.to)
|
||||
|| selectionContains(rangeFrom) || selectionContains(rangeTo);
|
||||
}
|
||||
return 'line';
|
||||
},
|
||||
createDecoration: (node, state, parentTags) => {
|
||||
const markerIsChecked = (marker: SyntaxNodeRef) => {
|
||||
const content = state.doc.sliceString(marker.from, marker.to);
|
||||
return content.toLowerCase().indexOf('x') !== -1;
|
||||
};
|
||||
|
||||
if (node.name === 'TaskMarker') {
|
||||
const containerLine = state.doc.lineAt(node.from);
|
||||
const labelText = state.doc.sliceString(node.to, containerLine.to);
|
||||
|
||||
return new CheckboxWidget(markerIsChecked(node), parentTags.get('ListItem') ?? 0, labelText);
|
||||
} else if (node.name === 'Task') {
|
||||
const marker = node.node.getChild('TaskMarker');
|
||||
if (marker && markerIsChecked(marker)) {
|
||||
return completedListItemDecoration;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getDecorationRange: (node, state) => {
|
||||
if (node.name === 'TaskMarker') {
|
||||
const container = node.node.parent?.parent;
|
||||
const listMarker = container?.getChild('ListMark');
|
||||
if (!listMarker) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [listMarker.from, node.to];
|
||||
} else if (node.name === 'Task') {
|
||||
const taskLine = state.doc.lineAt(node.from);
|
||||
return [taskLine.from];
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
export default replaceCheckboxes;
|
||||
在新工单中引用
屏蔽一个用户