@mirus/tiptap-editor 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -0
- package/babel.config.js +10 -0
- package/dist/assets/index-40717dd0.js +115 -0
- package/dist/assets/index-7678a4fb.css +1 -0
- package/dist/index.html +14 -0
- package/package.json +80 -0
- package/src/App.vue +60 -0
- package/src/App.vue~ +46 -0
- package/src/index.js +3 -0
- package/src/main.js +8 -0
- package/src/main.js~ +9 -0
- package/src/max-character-count.js +48 -0
- package/src/max-character-count.js~ +33 -0
- package/src/tiptap-editor.vue +499 -0
- package/src/tiptap-editor.vue~ +501 -0
- package/src/warnings.js +244 -0
- package/vite.config.js +8 -0
- package/vitest.config.js +10 -0
- package/vue.config.js +18 -0
package/src/warnings.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { Node } from '@tiptap/core';
|
|
2
|
+
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
3
|
+
import { Decoration, DecorationSet } from 'prosemirror-view';
|
|
4
|
+
import get from 'lodash.get';
|
|
5
|
+
|
|
6
|
+
function dispatch(tr, state, view) {
|
|
7
|
+
state = state.apply(tr);
|
|
8
|
+
view.updateState(state);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isWord(w) {
|
|
12
|
+
const regex = new RegExp(/[!@#$^%^&*(),.?":{}|<>]/g);
|
|
13
|
+
const startsSpecial = regex.test(w.value[0]);
|
|
14
|
+
return w.isWord && !startsSpecial;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// cribbed from here https://stackoverflow.com/a/6969486/216154
|
|
18
|
+
// and here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
|
|
19
|
+
function escapeRegExp(string) {
|
|
20
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function lint(doc, position, prev, getErrorWords) {
|
|
24
|
+
const words = getErrorWords();
|
|
25
|
+
const regexString = words
|
|
26
|
+
.map((w) => (isWord(w) ? `\\b(${escapeRegExp(w.value)})\\b` : `(${escapeRegExp(w.value)})`))
|
|
27
|
+
.join('|');
|
|
28
|
+
const badWordsRegex = new RegExp(regexString, 'ig');
|
|
29
|
+
|
|
30
|
+
let highlights = [];
|
|
31
|
+
let on = { active: false };
|
|
32
|
+
|
|
33
|
+
if (words.length < 1) {
|
|
34
|
+
return { highlights, on };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function record(from, to, text) {
|
|
38
|
+
const word = words.find((w) => w.value === text);
|
|
39
|
+
|
|
40
|
+
if (word !== undefined) {
|
|
41
|
+
const overrideClass = word.overrideClass;
|
|
42
|
+
|
|
43
|
+
if (position && position.pos >= from && position.pos <= to) {
|
|
44
|
+
const decorationId = get(
|
|
45
|
+
prev,
|
|
46
|
+
'on.decorationId',
|
|
47
|
+
(Math.random() + 1).toString(36).substr(2, 5)
|
|
48
|
+
);
|
|
49
|
+
highlights.push({ from, to, text, decorationId, overrideClass });
|
|
50
|
+
on.active = true;
|
|
51
|
+
on.decorationId = decorationId;
|
|
52
|
+
on.range = { to, from };
|
|
53
|
+
on.query = text;
|
|
54
|
+
on.text = text;
|
|
55
|
+
} else {
|
|
56
|
+
highlights.push({ from, to, text, overrideClass });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// For each node in the document
|
|
62
|
+
doc.descendants((node, pos) => {
|
|
63
|
+
if (node.isText) {
|
|
64
|
+
// Scan text nodes for bad words
|
|
65
|
+
let m;
|
|
66
|
+
while ((m = badWordsRegex.exec(node.text))) {
|
|
67
|
+
record(pos + m.index, pos + m.index + m[0].length, m[0]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return { highlights, on };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const Warning = Node.create({
|
|
76
|
+
name: 'Warning',
|
|
77
|
+
|
|
78
|
+
addOptions() {
|
|
79
|
+
return {
|
|
80
|
+
getErrorWords: () => [],
|
|
81
|
+
onChange: () => {},
|
|
82
|
+
onEnter: () => {},
|
|
83
|
+
onExit: () => {},
|
|
84
|
+
onKeyDown: () => {},
|
|
85
|
+
defaultClass: 'underline-red',
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
group: 'inline*',
|
|
90
|
+
inline: true,
|
|
91
|
+
atom: false,
|
|
92
|
+
selectable: false,
|
|
93
|
+
|
|
94
|
+
parseHTML() {
|
|
95
|
+
return [
|
|
96
|
+
{
|
|
97
|
+
tag: '[data-mention-id]',
|
|
98
|
+
getAttrs: (dom) => {
|
|
99
|
+
const id = dom.getAttribute('data-mention-id');
|
|
100
|
+
const label = dom.innerText.split(this.options.matcher.char).join('');
|
|
101
|
+
return { id, label };
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
addProseMirrorPlugins() {
|
|
108
|
+
const self = this;
|
|
109
|
+
|
|
110
|
+
return [
|
|
111
|
+
// underline the words
|
|
112
|
+
new Plugin({
|
|
113
|
+
key: new PluginKey('warning'),
|
|
114
|
+
appendTransaction: (transactions, oldState, newState) => {
|
|
115
|
+
// make sure the position of the cursor never goes beyond the size of the doc
|
|
116
|
+
const maxPos = newState.doc.content.size;
|
|
117
|
+
const currentFrom = newState.selection.$from.pos;
|
|
118
|
+
const currentTo = newState.selection.$to.pos;
|
|
119
|
+
const transaction = newState.tr;
|
|
120
|
+
|
|
121
|
+
transaction.selection.$from.pos = Math.min(maxPos, currentFrom);
|
|
122
|
+
transaction.selection.$to.pos = Math.min(maxPos, currentTo);
|
|
123
|
+
|
|
124
|
+
return transaction;
|
|
125
|
+
},
|
|
126
|
+
view() {
|
|
127
|
+
return {
|
|
128
|
+
update: (view, prevState) => {
|
|
129
|
+
const prev = this.key.getState(prevState).on;
|
|
130
|
+
const next = this.key.getState(view.state).on;
|
|
131
|
+
const moved =
|
|
132
|
+
prev.active && next.active && prev.range.from !== next.range.from;
|
|
133
|
+
const started = !prev.active && next.active;
|
|
134
|
+
const stopped = prev.active && !next.active;
|
|
135
|
+
const changed = !started && !stopped && prev.query !== next.query;
|
|
136
|
+
|
|
137
|
+
const handleStart = started || moved;
|
|
138
|
+
const handleChange = changed; //&& !moved;
|
|
139
|
+
const handleExit = stopped || moved;
|
|
140
|
+
|
|
141
|
+
// Cancel when suggestion isn't active
|
|
142
|
+
if (!handleStart && !handleChange && !handleExit) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const state = handleExit && !handleChange ? prev : next;
|
|
147
|
+
const decorationNode = document.querySelector(
|
|
148
|
+
'[data-decoration-id="' + state.decorationId + '"]'
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// build a virtual node for popper.js or tippy.js
|
|
152
|
+
// this can be used for building popups without a DOM node
|
|
153
|
+
const virtualNode = decorationNode
|
|
154
|
+
? {
|
|
155
|
+
getBoundingClientRect() {
|
|
156
|
+
return decorationNode.getBoundingClientRect();
|
|
157
|
+
},
|
|
158
|
+
clientWidth: decorationNode.clientWidth,
|
|
159
|
+
clientHeight: decorationNode.clientHeight,
|
|
160
|
+
}
|
|
161
|
+
: null;
|
|
162
|
+
|
|
163
|
+
const props = {
|
|
164
|
+
view,
|
|
165
|
+
range: state.range,
|
|
166
|
+
text: state.text,
|
|
167
|
+
decorationNode,
|
|
168
|
+
virtualNode,
|
|
169
|
+
command: function ({ range, attrs }) {
|
|
170
|
+
const tr = view.state.tr.replaceWith(
|
|
171
|
+
range.from,
|
|
172
|
+
range.to,
|
|
173
|
+
view.state.schema.text(attrs.label)
|
|
174
|
+
);
|
|
175
|
+
const result = view.dispatch(tr);
|
|
176
|
+
|
|
177
|
+
// need to merge text nodes
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
document
|
|
180
|
+
.getElementsByClassName('editor__content')[0]
|
|
181
|
+
.normalize();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return result;
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Trigger the hooks when necessary
|
|
189
|
+
if (handleExit) {
|
|
190
|
+
self.options.onExit(props);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (handleChange) {
|
|
194
|
+
self.options.onChange(props);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (handleStart) {
|
|
198
|
+
self.options.onEnter(props);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
state: {
|
|
204
|
+
init(_, { doc }) {
|
|
205
|
+
return lint(doc, null, {}, self.options.getErrorWords);
|
|
206
|
+
},
|
|
207
|
+
apply(tr, prev) {
|
|
208
|
+
const { selection } = tr;
|
|
209
|
+
const next = Object.assign({}, prev);
|
|
210
|
+
const position = selection.$from;
|
|
211
|
+
return lint(tr.doc, position, prev, self.options.getErrorWords);
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
props: {
|
|
215
|
+
handleKeyDown(view, event) {
|
|
216
|
+
const { active, range } = this.getState(view.state).on;
|
|
217
|
+
if (!active) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
return self.options.onKeyDown({ event });
|
|
221
|
+
},
|
|
222
|
+
decorations(state) {
|
|
223
|
+
let decos = [];
|
|
224
|
+
this.getState(state).highlights.forEach((prob) => {
|
|
225
|
+
decos.push(
|
|
226
|
+
Decoration.inline(prob.from, prob.to, {
|
|
227
|
+
class: prob.overrideClass
|
|
228
|
+
? prob.overrideClass
|
|
229
|
+
: self.options.defaultClass,
|
|
230
|
+
'data-decoration-id': prob.decorationId
|
|
231
|
+
? prob.decorationId
|
|
232
|
+
: undefined,
|
|
233
|
+
})
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
return DecorationSet.create(state.doc, decos);
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
}),
|
|
240
|
+
];
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
export default Warning;
|
package/vite.config.js
ADDED
package/vitest.config.js
ADDED
package/vue.config.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const { resolve } = require('path');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
css: { extract: false },
|
|
5
|
+
transpileDependencies: ['tiptap'],
|
|
6
|
+
configureWebpack: {
|
|
7
|
+
// devtool: 'eval-source-map',
|
|
8
|
+
module: {
|
|
9
|
+
rules: [
|
|
10
|
+
{
|
|
11
|
+
test: /\.js$/,
|
|
12
|
+
include: [resolve('src')],
|
|
13
|
+
loader: 'babel-loader',
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
};
|