am-editor11212/plugins/tasklist/src/index.ts

311 lines
7.8 KiB
TypeScript

import {
$,
NodeInterface,
ListPlugin,
CARD_KEY,
SchemaBlock,
isEngine,
PluginEntry,
PluginOptions,
} from '@aomao/engine';
import CheckboxComponent, { CheckboxValue } from './checkbox';
import './index.css';
export interface TasklistOptions extends PluginOptions {
hotkey?: string | Array<string>;
markdown?: boolean | string[];
}
export default class<
T extends TasklistOptions = TasklistOptions,
> extends ListPlugin<T> {
static get pluginName() {
return 'tasklist';
}
cardName = 'checkbox';
tagName = 'ul';
attributes = {
class: '@var0',
'data-indent': '@var1',
};
variable = {
'@var0': {
required: true,
value: [this.editor.list.CUSTOMZIE_UL_CLASS, 'data-list-task'],
},
'@var1': '@number',
};
allowIn = ['blockquote', '$root'];
init() {
super.init();
this.editor.on('parse:html', (node) => this.parseHtml(node));
if (isEngine(this.editor)) {
this.editor.on(
'paste:markdown-check',
(child) => !this.checkMarkdown(child)?.match,
);
this.editor.on('paste:markdown', (child) =>
this.pasteMarkdown(child),
);
this.editor.on('paste:each-after', (root) => {
const liNodes = root.find(
`li.${this.editor.list.CUSTOMZIE_LI_CLASS}`,
);
liNodes.each((_, index) => {
const child = liNodes.eq(index);
if (!child) return;
const firstChild = child.first();
if (
firstChild &&
firstChild.name === CheckboxComponent.cardName
) {
const card =
this.editor.card.find<CheckboxValue>(firstChild);
if (card) {
const parent = child.parent();
parent?.addClass('data-list-task');
const value = card.getValue();
if (value && value.checked) {
parent?.attributes('checked', 'true');
} else {
parent?.removeAttributes('checked');
}
}
}
});
});
}
}
schema(): Array<SchemaBlock> {
const scheam = super.schema() as SchemaBlock;
return [
scheam,
{
name: 'li',
type: 'block',
attributes: {
class: {
required: true,
value: this.editor.list.CUSTOMZIE_LI_CLASS,
},
checked: ['true', 'false'],
},
allowIn: ['ul'],
},
];
}
isCurrent(node: NodeInterface) {
if (node.name === 'li')
return (
node.hasClass(this.editor.list.CUSTOMZIE_LI_CLASS) &&
node.first()?.attributes(CARD_KEY) === 'checkbox'
);
return node.hasClass('data-list') && node.hasClass('data-list-task');
}
execute(value?: any) {
if (!isEngine(this.editor)) return;
const { change, list, block } = this.editor;
list.split();
const range = change.range.get();
const activeBlocks = block.findBlocks(range);
if (activeBlocks) {
const selection = range.createSelection('tasklist-execute');
if (list.isSpecifiedType(activeBlocks, 'ul', 'checkbox')) {
list.unwrap(activeBlocks);
} else {
const listBlocks = list.toCustomize(
activeBlocks,
'checkbox',
value,
) as Array<NodeInterface>;
listBlocks.forEach((list) => {
if (this.editor.node.isList(list))
list.addClass('data-list-task');
});
}
selection.move();
if (
range.collapsed &&
range.startContainer.nodeType === Node.ELEMENT_NODE &&
range.startContainer.childNodes.length === 0 &&
range.startContainer.parentNode
) {
const brNode = document.createElement('br');
range.startNode.before(brNode);
range.startContainer.parentNode.removeChild(
range.startContainer,
);
range.select(brNode);
range.collapse(false);
}
change.apply(range);
list.merge();
}
}
hotkey() {
return this.options.hotkey || 'mod+shift+9';
}
parseHtml(root: NodeInterface) {
const getBox = (inner: string = '') => {
return `<span style="${
inner
? 'background:#347eff;position:relative;'
: 'background:#fff;'
}width: 16px;height: 16px;display: inline-block;border: 1px solid #347eff;border-radius: 2px;transition: all 0.3s;border-collapse: separate;">${inner}</span>`;
};
root.find(`[${CARD_KEY}=checkbox`).each((checkboxNode) => {
const node = $(checkboxNode);
const checkbox = $(
`<span>${
node.find('.data-checkbox-checked').length > 0
? getBox(
'<span style="transform: rotate(45deg) scale(1);position: absolute;display: block;border: 2px solid #fff;border-top: 0;border-left: 0;width:5.71428571px;height:9.14285714px;transition: all 0.2s cubic-bezier(0.12, 0.4, 0.29, 1.46) 0.1s;opacity: 1;left:3.57142857px;top:0.14285714px;"></span>',
)
: getBox()
}</span>`,
);
checkbox.css({
margin: '3px 0.5ex',
'vertical-align': 'middle',
width: '16px',
height: '16px',
color: 'color: rgba(0, 0, 0, 0.65)',
});
node.empty();
node.append(checkbox);
});
root.find('.data-list-task').css({
'list-style': 'none',
});
}
//设置markdown
markdown(event: KeyboardEvent, text: string, block: NodeInterface) {
const { markdown } = this.options;
if (!isEngine(this.editor) || markdown === false) return;
const { node, command } = this.editor;
const blockApi = this.editor.block;
const plugin = blockApi.findPlugin(block);
// fix: 列表、引用等 markdown 快捷方式不应该在标题内生效
if (
block.name !== 'p' ||
(plugin &&
(plugin.constructor as PluginEntry).pluginName === 'heading')
) {
return;
}
let markdownWords = ['[]', '[ ]', '[x]'];
if (Array.isArray(markdown)) {
markdownWords = markdown;
}
if (markdownWords.indexOf(text) < 0) return;
event.preventDefault();
blockApi.removeLeftText(block);
if (node.isEmpty(block)) {
block.empty();
block.append('<br />');
}
command.execute(
(this.constructor as PluginEntry).pluginName,
text === '[x]' ? { checked: true } : undefined,
);
return false;
}
checkMarkdown(node: NodeInterface) {
if (!isEngine(this.editor) || !this.markdown || !node.isText()) return;
const text = node.text();
if (!text) return;
const reg = /(^|\r\n|\n)(-\s*)?(\[[\sx]{0,1}\])/;
const match = reg.exec(text);
return {
reg,
match,
};
}
pasteMarkdown(node: NodeInterface) {
const result = this.checkMarkdown(node);
if (!result) return;
const { match } = result;
if (!match) return;
const { list, card } = this.editor;
const createList = (nodes: Array<string>, indent?: number) => {
const listNode = $(
`<${this.tagName} class="${
list.CUSTOMZIE_UL_CLASS
} data-list-task">${nodes.join('')}</${this.tagName}>`,
);
if (indent) {
listNode.attributes(this.editor.list.INDENT_KEY, indent);
}
list.addBr(listNode);
return listNode.get<Element>()?.outerHTML;
};
const text = node.text();
let newText = '';
const rows = text.split(/\n|\r\n/);
let nodes: Array<string> = [];
let indent = 0;
rows.forEach((row) => {
const match = /^(\s*)(-\s*)?(\[[\sx]{0,1}\])/.exec(row);
if (match && !/(\[(.*)\]\(([\S]+?)\))/.test(row)) {
const codeLength = match[0].length;
const content = row.substr(
/^\s+/.test(row.substr(codeLength))
? codeLength + 1
: codeLength,
);
const tempNode = $('<span />');
const cardNode = card.replaceNode<CheckboxValue>(
tempNode,
this.cardName,
{
checked: match[0].indexOf('x') > 0,
},
);
tempNode.remove();
if (match[1].length !== indent && nodes.length > 0) {
newText += createList(nodes, indent);
nodes = [];
indent = Math.ceil(match[1].length / 2);
}
nodes.push(
`<li class="${list.CUSTOMZIE_LI_CLASS}">${
cardNode.get<Element>()?.outerHTML
}${content}</li>`,
);
} else if (nodes.length > 0) {
newText += createList(nodes, indent) + '\n' + row + '\n';
nodes = [];
} else {
newText += row + '\n';
}
});
if (nodes.length > 0) {
newText += createList(nodes, indent) + '\n';
}
node.text(newText);
}
}
export { CheckboxComponent };
export type { CheckboxValue };