@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.
@@ -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
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue2'
3
+
4
+ const path = require("path");
5
+
6
+ export default defineConfig({
7
+ plugins: [vue()],
8
+ })
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "vitest/config";
2
+ import vue from "@vitejs/plugin-vue2";
3
+
4
+ export default defineConfig({
5
+ plugins: [vue()],
6
+ test: {
7
+ globals: true,
8
+ environment: "happy-dom",
9
+ },
10
+ });
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
+ };