0%

JS 自定义 Quill 插入标签

需求

Quill里面加一个话题标签或者艾特标签,删除的时候能将标签作为整体进行删除。

示例代码:https://codepen.io/mtrucc/pen/JjMpLNX

自己写一个

定制自己的组件,参考官方 demo 怎么写都写不出来,后来发现这样就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
var Base = Quill.import('blots/embed');

/**
* This custom type of Blot is used to represent a Mention.
* It stores 2 values, a Name and an ID.
* The Name is used as a text to display within the input.
*/
class MentionBlot extends Base {

static create(data) {
const node = super.create(data.name);
node.innerHTML = data.name;
node.setAttribute('spellcheck', "false");
node.setAttribute('autocomplete', "off");
node.setAttribute('autocorrect', "off");
node.setAttribute('autocapitalize', "off");

// store data
node.setAttribute('data-name', data.name);
node.setAttribute('data-id', data.id);
return node;
}

static value(domNode) {
const { name, id } = domNode.dataset;
return { name, id };
}

constructor(domNode, value) {
super(domNode);
this._data = value;
this._removedBlot = false;
}

// eslint-disable-next-line no-unused-vars
index(node, offset) {
// See leaf definition as reference:
// https://github.com/quilljs/parchment/blob/master/src/blot/abstract/leaf.ts
// NOTE: sometimes `node` contains the actual dom node and sometimes just
// the text
return 1;
}

/**
* Replace the current Mention Blot with the given text.
*
* @param { String } text the text to replace the mention with.
*/
_replaceBlotWithText(text) {
// The steps to replace the Blot with its text must be in this order:
// 1. insert text - source:API
// using API we won't react to changes
// 2. set selection - source:API
// set the cursor position in place
// 3. remove blot - source:USER
// using USER we react to the text-change event and it "looks" like we
// did a blot->text replacement in one step.
//
// If we don't do these actions in the specified order, the text update and
// the cursor position won't be as it should for the autocompletion list.

if (this._removedBlot) return;
this._removedBlot = true;

const cursorPosition = quill.getSelection().index;
const blotCursorPosition = quill.selection.getNativeRange().end.offset;
const realPosition = cursorPosition + blotCursorPosition;

quill.insertText(cursorPosition - 1, text, Quill.sources.API);
quill.setSelection(realPosition - 1, Quill.sources.API);
quill.deleteText(cursorPosition + text.length - 1, 1, Quill.sources.USER);

// We use API+USER to be able to hook just USER from the outside and the
// content edit will look like is done in "one action".
}

changeText(oldText, newText) {
const name = this._data.name;

const valid = (oldText == name) && (newText != oldText);
if (!valid) return;

let cursorPosition = quill.getSelection().index;
if (cursorPosition == -1) {
// This case was found just a couple of times and it may not appear again
// due to improvements made on the MentionBlot. I'm leaving the fix here
// in case that happens again to debug it.
cursorPosition = 1;
console.warning("[changeText] cursorPosition was -1 ... changed to 1");
}

const blotCursorPosition = quill.selection.getNativeRange().end.offset;
let realPosition = cursorPosition;

if (!this._removedBlot) {
realPosition += blotCursorPosition;
} else {
// Right after the blot is removed we may need to handle a Mutation.
// If that's the case, considering that the length of the text is 1 would
// be wrong since it no longer is an Embed but a text.
console.warning("[changeText] removedBlot is set!");
}

if (newText.startsWith(name) && oldText == name) { // append
// An append happens as follows:
// Text: <@Name|> -> <@NameX|>
// We need to move the inserted letter X outside the blot.
const extra = newText.substr(name.length);

this.domNode.innerHTML = name;

// append the text outside the blot
quill.insertText(cursorPosition, extra, Quill.sources.USER);
quill.setSelection(cursorPosition + extra.length, Quill.sources.API);
// quill.insertText(cursorPosition + 2, extra, Quill.sources.USER);
// quill.setSelection(cursorPosition + extra.length + 3, Quill.sources.API);

return;
} else if (newText.endsWith(name) && oldText == name) { // prepend
// A prepend may be handled in two different ways depending on the
// browser and the text/cursor state.
//
// Case A: (not a problem)
// Text: |<@Name> -> X|<@Name>
// Case B: (problem)
// Text: <|@Name> -> <X|@Name>
//
// If we reach this point, it means that we need to tackle Case B.
// We need to move the inserted letter X outside the blot.
const end = newText.length - name.length;
const extra = newText.substr(0, end);

// The cursor position is set right after the inserted character.
// In some cases the cursor position gets updated before the text-edit
// event is emited and in some cases afterwards.
// This difference manifests itself when the Blot is at the beginning and
// this conditional assignment handles the issue.
const pos = cursorPosition > 0 ? cursorPosition - 1 : cursorPosition;

this.domNode.innerHTML = name;

// append the text outside the blot
quill.insertText(pos, extra, Quill.sources.USER);
quill.setSelection(pos + extra.length, Quill.sources.API);

return;
}
// no append, no prepend, text has changed in a different way.

// We need to trigger these changes right after the update callback
// finishes, otherwise errors may appear due to ranges not updating
// correctly.
// See: https://github.com/quilljs/quill/issues/1134
setTimeout(() => this._replaceBlotWithText(newText), 0)
}

update(mutations) {
// See as reference:
// https://github.com/quilljs/quill/blob/develop/blots/cursor.js

mutations
.filter(mutation => mutation.type == 'characterData')
.forEach(m => {
const oldText = m.oldValue;
const newText = m.target.data;
this.changeText(oldText, newText);
});

// I'm not sure whether this is needed or not, keeping it just in case.
super.update(mutations.filter(mutation => mutation.type != 'characterData'));
}

}

MentionBlot.blotName = 'mention';
MentionBlot.className = 'quill-mention';
MentionBlot.tagName = 'span';

/* } end of MentionBlot definition, mention.js */

Quill.register({
'formats/mention': MentionBlot
});

var quill = new Quill('#editor-container', {
// debug: 'info',
// theme: 'bubble'
placeholder: 'type something...',
});


const insertMention = (data) => {
// this is null when the editor doesn't have focus
// const range = quill.getSelection();

const range = quill.selection.savedRange; // cursor position

if (!range || range.length != 0) return;
const position = range.index;

quill.insertEmbed(position, 'mention', data, Quill.sources.API);
quill.insertText(position + 1, ' ', Quill.sources.API);
quill.setSelection(position + 2, Quill.sources.API);
}

const addMention = () => {
const data = {
name: "@John Doe",
id: 'asdf12345',
};

insertMention(data);
};

const logs = () => {
const text = quill.getText();
const contents = quill.getContents();
console.log(quill);
console.log(text);
console.log(contents);
}

document.getElementById('add-mention').addEventListener('click', addMention);
document.getElementById('logs').addEventListener('click', logs);

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var Embed = Quill.import('blots/embed');
class MyCustomTag extends Embed {
static create(paramValue) {
let node = super.create();
node.innerHTML = paramValue;
//node.setAttribute('contenteditable', 'false');
//node.addEventListener('click', MyCustomTag.onClick);
return node;
}

static value(node) {
return node.innerHTML;
}
}

MyCustomTag.blotName = 'my-custom-tag';
MyCustomTag.className = 'my-custom-tag';
MyCustomTag.tagName = 'my-custom-tag';
//in case you need to inject an event from outside
/* MyCustomTag.onClick = function(){
//do something
}*/

Quill.register(MyCustomTag);

其他

还有个监听删除的代码

1
2
3
4
5
6
7
8
9
quill.on("text-change", (delta, oldDelta, source) => {
if (source === "user") {
let currrentContents = quill.getContents();
let diff = currrentContents.diff(oldDelta);
try {
console.log(diff.ops[0].insert.image);
} catch (_error) {}
}
});

参考文章