quill-react-commercial
Version:
A Quill component for React and more maturely.
179 lines (162 loc) • 6.14 kB
JavaScript
import normalizeUrl from 'normalize-url';
import Quill from 'quill';
import { getI18nText } from '../../i18n';
import { isEmail, isUrl } from '../../utils';
import { genIconDom, iconsConfig } from '../iconTitle/iconsConfig';
const Module = Quill.import('core/module');
export class LinkHandler extends Module {
constructor(quill, options) {
super(quill, options);
this.quill = quill;
this.options = options || {};
this.toolbar = quill.getModule('toolbar');
this.editorContainer = quill.root.parentNode;
this.tooltip = quill.theme?.tooltip;
if (typeof this.toolbar !== 'undefined') {
this.toolbar.addHandler('link', this.handleLinkClick.bind(this));
}
quill.on('selection-change', (range, oldRange, source) => {
try {
if (range == null || !quill?.hasFocus()) return;
// 当选中link格式时,弹出tooltip并能修改保存
if (source === 'user') {
const LinkBlot = Quill.import('formats/link');
const [link, offset] = quill.scroll.descendant(LinkBlot, range.index);
if (link !== null) {
// quill!.theme?.tooltip?.root?.classList.add('ql-editing');
const [leaf] = quill.getLeaf(range.index + 1);
this.tooltip?.edit('link', '');
const start = range.index - offset;
const len = link.length();
const url = quill.getFormat(start, len).link;
if (this.editorContainer) {
if (!this.wordDom) {
this.getDom(); // this.wordDom在没有点击toolbar时为空,没有初始化
}
this.wordDom.value = leaf.text;
this.urlDom.value = url;
this.saveDom.onclick = () => {
this.saveLink(start, len);
};
this.clearDom.onclick = () => this.clearLink(start, len);
}
} else {
this.tooltipClose();
}
}
} catch (e) {
console.log('Link Listener Error: ', e);
}
});
// 添加全局 Link Tooltip Dom,替换原 snow 的 tooltip
if (this.quill.theme && this.quill.theme.tooltip) {
this.quill.theme.tooltip.root.innerHTML = LinkHandler.linkTooltip(options.i18n);
}
// 编辑器有 text-change,关闭 tooltip
quill.on('text-change', this.tooltipClose);
}
getDom = () => {
if (this.editorContainer) {
this.wordDom = this.editorContainer.querySelector('#link-words');
this.urlDom = this.editorContainer.querySelector('#link-url');
this.saveDom = this.editorContainer.querySelector('a.ql-link-save');
this.jumpDom = this.editorContainer.querySelector('a.ql-link-jump');
this.clearDom = this.editorContainer.querySelector('a.ql-link-clear');
this.selection = this.quill.getSelection();
this.jumpDom.onclick = this.jumpLink;
this.clearDom.onclick = this.clearLink;
}
};
handleLinkClick() {
this.tooltip?.edit('link', '');
this.getDom();
// 当没有选中任何文本,直接弹出tooltip并能插入文本、超链接
if (this.selection === null || this.selection.length === 0) {
this.wordDom.value = '';
this.urlDom.value = '';
this.saveDom.onclick = () => {
this.saveLink();
};
} else {
// 当有选中文本,弹出tooltip后允许修改此文本,并能插入超链接
const text = this.quill.getText(this.selection.index, this.selection.length);
this.wordDom.value = text;
let href = ''; // 最终超链接的地址value
if (isEmail(text) && text.indexOf('mailto:') !== 0) {
href = `mailto:${text}`;
} else if (isUrl(text)) {
href = normalizeUrl(text, { stripWWW: false });
}
this.urlDom.value = href;
this.saveDom.onclick = () => {
this.saveLink(this.selection.index, this.selection.length);
};
}
}
// 保存 Link 到编辑器,有startIndex且length>0代表需要替换
saveLink(startIndex, length) {
const url = this.urlDom.value;
if (url && isUrl(url)) {
const words = this.wordDom.value || url;
if (startIndex !== undefined && length && length > 0) {
this.quill.deleteText(startIndex, length);
}
this.quill.insertText(
startIndex ?? (this.selection?.index || 0),
words,
'link',
normalizeUrl(url, { stripWWW: false }),
'user',
);
this.tooltipClose();
} else {
this.invalidUrlhandler();
}
}
jumpLink = () => {
const url = this.urlDom.value;
if (url && isUrl(url)) {
window.open(normalizeUrl(url, { stripWWW: false }), '_blank');
this.tooltipClose();
} else {
this.invalidUrlhandler();
}
};
clearLink = (startIndex, length) => {
const url = this.urlDom.value;
if (url && isUrl(url) && this.selection) {
this.quill.removeFormat(startIndex ?? this.selection.index, length ?? this.selection.length);
}
this.tooltipClose();
};
tooltipClose = () => {
if (this.tooltip && this.tooltip.hide) this.tooltip.hide();
};
invalidUrlhandler = () => {
if (this.tooltip && this.tooltip.root) {
const err = getI18nText('linkUrlErr', this.options.i18n);
const tips = this.tooltip.root.querySelector('.err-tips');
tips.innerText = err;
this.urlDom.addEventListener('input', () => {
if (tips.innerText) tips.innerText = '';
});
}
this.urlDom.focus();
};
static linkTooltip = (i18n) => {
const words = getI18nText(
['linkWords', 'linkUrl', 'linkSave', 'linkTarget', 'linkClear'],
i18n,
);
return `
<p><span>${words[0]}:</span><input id="link-words" class="text-input" type="text" /></p>
<p class="flex flex-center">
<span>${words[1]}:</span><input id="link-url" class="text-input" type="text" />
<a class="ql-link-save">${words[2]}</a>
<a class="ql-link-jump" target="_blank">${genIconDom(iconsConfig.jumpIcon, words[3], 'ql-icon')}</a>
<a class="ql-link-clear">${genIconDom(iconsConfig.unlinkIcon, words[4], 'ql-icon')}</a>
</p>
<p class="err-tips"></p>
`;
};
}