@mirus/tiptap-editor 3.0.1 → 3.0.3

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.
@@ -1,114 +1,114 @@
1
1
  <template>
2
- <div>
3
- <div class="tiptap-editor" tabindex="0">
4
- <div
5
- v-if="showMenu && editor"
6
- class="menubar"
7
- role="toolbar"
8
- :aria-controls="id || null"
9
- >
10
- <button
11
- :aria-pressed="`${editor.isActive('bold') ? 'true' : 'false'}`"
12
- :class="{ 'is-active': editor.isActive('bold') }"
13
- @keyup.left="toolbarGoLeft"
14
- @keyup.right="toolbarGoRight"
15
- @click="editor.chain().focus().toggleBold().run()"
16
- aria-label="bold"
17
- value="bold"
18
- type="button"
19
- >
20
- <b>B</b>
21
- </button>
22
- <button
23
- :aria-pressed="`${editor.isActive('italic') ? 'true' : 'false'}`"
24
- :class="{ 'is-active': editor.isActive('italic') }"
25
- @click="editor.chain().focus().toggleItalic().run()"
26
- @keyup.left="toolbarGoLeft"
27
- @keyup.right="toolbarGoRight"
28
- value="italic"
29
- type="button"
30
- >
31
- <i>I</i>
32
- </button>
33
- <button
34
- @click="editor.chain().focus().toggleBulletList().run()"
35
- :class="{ 'is-active': editor.isActive('bulletList') }"
36
- @keyup.left="toolbarGoLeft"
37
- @keyup.right="toolbarGoRight"
38
- aria-label="bullet list"
39
- value="bulletlist"
40
- type="button"
41
- >
42
- <svg
43
- aria-hidden="true"
44
- focusable="false"
45
- data-prefix="fas"
46
- data-icon="list-ul"
47
- class="svg-inline--fa fa-list-ul fa-w-16"
48
- role="img"
49
- xmlns="http://www.w3.org/2000/svg"
50
- viewBox="0 0 512 512"
51
- >
52
- <path
53
- fill="currentColor"
54
- d="M96 96c0 26.51-21.49 48-48 48S0 122.51 0 96s21.49-48 48-48 48 21.49 48 48zM48 208c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zm0 160c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zm96-236h352c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H144c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h352c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H144c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h352c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H144c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"
55
- ></path>
56
- </svg>
57
- </button>
58
- <div class="character-count" v-if="maxCharacterCount && editor">
59
- <svg
60
- height="20"
61
- width="20"
62
- viewBox="0 0 20 20"
63
- :class="
64
- maxCharacterCountExceeded
65
- ? 'character-count__graph--warning'
66
- : 'character-count__graph'
67
- "
68
- >
69
- <circle r="10" cx="10" cy="10" class="character-count__outer-circle" />
70
- <circle
71
- r="5"
72
- cx="10"
73
- cy="10"
74
- fill="transparent"
75
- stroke="currentColor"
76
- stroke-width="10"
77
- :stroke-dasharray="`calc(${characterCountPercentage}px * 31.4 / 100) 31.4`"
78
- transform="rotate(-90) translate(-20)"
79
- />
80
- <circle r="6" cx="10" cy="10" class="character-count__inner-circle" />
81
- </svg>
82
- <div class="character-count__text" aria-live="polite">
83
- {{ currentCharacterCount }} / {{ maxCharacterCount }} characters
84
- </div>
85
- </div>
86
- </div>
87
- <editor-content
88
- :editor="editor"
89
- :style="{ height: height }"
90
- :id="id || null"
91
- role="textbox"
92
- class="editor__content"
93
- aria-label="text area"
94
- tabindex="-1"
95
- />
96
- </div>
97
- <div class="error-list" :v-show="false" ref="renderedErrors">
98
- <template v-if="currentWarning">
99
- <b>{{ currentWarning.message }}</b>
100
- <div
101
- v-for="(option, index) in currentOptions"
102
- :key="option.id"
103
- class="error-list__item"
104
- :class="{ selected: navigatedOptionIndex === index }"
105
- @click="selectOption(option)"
106
- >
107
- {{ option.value }}
108
- </div>
109
- </template>
110
- </div>
111
- </div>
2
+ <div>
3
+ <div class="tiptap-editor" tabindex="0">
4
+ <div
5
+ v-if="showMenu && editor"
6
+ class="menubar"
7
+ role="toolbar"
8
+ :aria-controls="id || null"
9
+ >
10
+ <button
11
+ :aria-pressed="`${editor.isActive('bold') ? 'true' : 'false'}`"
12
+ :class="{ 'is-active': editor.isActive('bold') }"
13
+ @keyup.left="toolbarGoLeft"
14
+ @keyup.right="toolbarGoRight"
15
+ @click="editor.chain().focus().toggleBold().run()"
16
+ aria-label="bold"
17
+ value="bold"
18
+ type="button"
19
+ >
20
+ <b>B</b>
21
+ </button>
22
+ <button
23
+ :aria-pressed="`${editor.isActive('italic') ? 'true' : 'false'}`"
24
+ :class="{ 'is-active': editor.isActive('italic') }"
25
+ @click="editor.chain().focus().toggleItalic().run()"
26
+ @keyup.left="toolbarGoLeft"
27
+ @keyup.right="toolbarGoRight"
28
+ value="italic"
29
+ type="button"
30
+ >
31
+ <i>I</i>
32
+ </button>
33
+ <button
34
+ @click="editor.chain().focus().toggleBulletList().run()"
35
+ :class="{ 'is-active': editor.isActive('bulletList') }"
36
+ @keyup.left="toolbarGoLeft"
37
+ @keyup.right="toolbarGoRight"
38
+ aria-label="bullet list"
39
+ value="bulletlist"
40
+ type="button"
41
+ >
42
+ <svg
43
+ aria-hidden="true"
44
+ focusable="false"
45
+ data-prefix="fas"
46
+ data-icon="list-ul"
47
+ class="svg-inline--fa fa-list-ul fa-w-16"
48
+ role="img"
49
+ xmlns="http://www.w3.org/2000/svg"
50
+ viewBox="0 0 512 512"
51
+ >
52
+ <path
53
+ fill="currentColor"
54
+ d="M96 96c0 26.51-21.49 48-48 48S0 122.51 0 96s21.49-48 48-48 48 21.49 48 48zM48 208c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zm0 160c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48zm96-236h352c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H144c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h352c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H144c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h352c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H144c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"
55
+ ></path>
56
+ </svg>
57
+ </button>
58
+ <div class="character-count" v-if="maxCharacterCount && editor">
59
+ <svg
60
+ height="20"
61
+ width="20"
62
+ viewBox="0 0 20 20"
63
+ :class="
64
+ maxCharacterCountExceeded
65
+ ? 'character-count__graph--warning'
66
+ : 'character-count__graph'
67
+ "
68
+ >
69
+ <circle r="10" cx="10" cy="10" class="character-count__outer-circle" />
70
+ <circle
71
+ r="5"
72
+ cx="10"
73
+ cy="10"
74
+ fill="transparent"
75
+ stroke="currentColor"
76
+ stroke-width="10"
77
+ :stroke-dasharray="`calc(${characterCountPercentage}px * 31.4 / 100) 31.4`"
78
+ transform="rotate(-90) translate(-20)"
79
+ />
80
+ <circle r="6" cx="10" cy="10" class="character-count__inner-circle" />
81
+ </svg>
82
+ <div class="character-count__text" aria-live="polite">
83
+ {{ currentCharacterCount }} / {{ maxCharacterCount }} characters
84
+ </div>
85
+ </div>
86
+ </div>
87
+ <editor-content
88
+ :editor="editor"
89
+ :style="{ height: height }"
90
+ :id="id || null"
91
+ role="textbox"
92
+ class="editor__content"
93
+ aria-label="text area"
94
+ tabindex="-1"
95
+ />
96
+ </div>
97
+ <div class="error-list" :v-show="false" ref="renderedErrors">
98
+ <template v-if="currentWarning">
99
+ <b>{{ currentWarning.message }}</b>
100
+ <div
101
+ v-for="(option, index) in currentOptions"
102
+ :key="option.id"
103
+ class="error-list__item"
104
+ :class="{ selected: navigatedOptionIndex === index }"
105
+ @click="selectOption(option)"
106
+ >
107
+ {{ option.value }}
108
+ </div>
109
+ </template>
110
+ </div>
111
+ </div>
112
112
  </template>
113
113
 
114
114
  <script setup>
@@ -131,102 +131,102 @@ import unescape from 'lodash.unescape';
131
131
 
132
132
  const emit = defineEmits(['update:value', 'new-character-count']);
133
133
  const props = defineProps({
134
- height: { type: String, default: '300px' },
135
- id: { type: String, default: null },
136
- value: { type: String, default: '' },
137
- warnings: { type: Array, default: () => [] },
138
- maxCharacterCount: { type: Number, default: null },
139
- placeholder: { type: String, default: 'write your content here...' },
140
- showMenu: { type: Boolean, default: true },
134
+ height: { type: String, default: '300px' },
135
+ id: { type: String, default: null },
136
+ value: { type: String, default: '' },
137
+ warnings: { type: Array, default: () => [] },
138
+ maxCharacterCount: { type: Number, default: null },
139
+ placeholder: { type: String, default: 'write your content here...' },
140
+ showMenu: { type: Boolean, default: true },
141
141
  });
142
142
 
143
143
  const currentValue = ref(props.value);
144
144
  const editor = useEditor({
145
- content: props.value,
146
- parseOptions: { preserveWhitespace: 'full' },
147
- onCreate: ({ editor }) => {
148
- currentCharacterCount.value = editor.storage.characterCount.characters();
149
- },
150
- onUpdate: ({ getJSON, getHTML, editor }) => {
151
- currentCharacterCount.value = editor.storage.characterCount.characters();
152
- currentValue.value = editor.getHTML();
153
-
154
- emit('update:value', currentValue.value);
155
-
156
- props.warnings.forEach((warning) => {
157
- if (warning.length && warning.offset) {
158
- if (editor.state.selection.head - 1 <= warning.offset) {
159
- let charCountDiff = currentCharacterCount.value - previousCharacterCount.value;
160
- charCountDiff += adjustForNewlines(editor);
161
- warning.offset += charCountDiff;
162
- }
163
- }
164
- });
165
- previousCharacterCount.value = currentCharacterCount.value;
166
- previousHTML.value = editor.getHTML();
167
- editor.commands.focus();
168
- },
169
- extensions: [
170
- Bold,
171
- BulletList,
172
- CharacterCount.configure({ limit: props.maxCharacterCount }),
173
- Document,
174
- History,
175
- Italic,
176
- ListItem,
177
- Paragraph,
178
- Placeholder.configure({ placeholder: props.placeholder }),
179
- Text,
180
- Warning.configure({
181
- getErrorWords: getErrorWords,
182
- onEnter: ({ range, command, virtualNode, text }) => {
183
- currentWarning.value = errors.value.find((err) => err.value === text);
184
- currentOptions.value = currentWarning.value.options || [];
185
- navigatedOptionIndex.value = 0;
186
- optionRange.value = range;
187
- renderPopup(virtualNode);
188
- insertOption.value = command;
189
- },
190
- onChange: ({ range, virtualNode, text }) => {
191
- currentWarning.value = errors.value.find((err) => err.value === text);
192
- currentOptions.value = currentWarning.value.options || [];
193
- navigatedOptionIndex.value = 0;
194
- optionRange.value = range;
195
- renderPopup(virtualNode);
196
- },
197
- onExit: () => {
198
- navigatedOptionIndex.value = 0;
199
- currentOptions.value = null;
200
- optionRange.value = null;
201
- destroyPopup();
202
- },
203
- onKeyDown: ({ event }) => {
204
- // pressing up arrow
205
- if (event.keyCode === 38 && currentOptions.value !== null) {
206
- upHandler();
207
- return true;
208
- }
209
- // pressing down arrow
210
- if (event.keyCode === 40 && currentOptions !== null) {
211
- downHandler();
212
- return true;
213
- }
214
- // pressing enter
215
- if (event.keyCode === 13) {
216
- return enterHandler();
217
- }
218
- // pressing escape
219
- if (event.keyCode === 27) {
220
- navigatedOptionIndex.value = 0;
221
- optionRange.value = null;
222
- currentOptions.value = null;
223
- destroyPopup();
224
- return true;
225
- }
226
- return false;
227
- },
228
- }),
229
- ],
145
+ content: props.value,
146
+ parseOptions: { preserveWhitespace: 'full' },
147
+ onCreate: ({ editor }) => {
148
+ currentCharacterCount.value = editor.storage.characterCount.characters();
149
+ },
150
+ onUpdate: ({ getJSON, getHTML, editor }) => {
151
+ currentCharacterCount.value = editor.storage.characterCount.characters();
152
+ currentValue.value = editor.getHTML();
153
+
154
+ emit('update:value', currentValue.value);
155
+
156
+ props.warnings.forEach((warning) => {
157
+ if (warning.length && warning.offset) {
158
+ if (editor.state.selection.head - 1 <= warning.offset) {
159
+ let charCountDiff = currentCharacterCount.value - previousCharacterCount.value;
160
+ charCountDiff += adjustForNewlines(editor);
161
+ warning.offset += charCountDiff;
162
+ }
163
+ }
164
+ });
165
+ previousCharacterCount.value = currentCharacterCount.value;
166
+ previousHTML.value = editor.getHTML();
167
+ editor.commands.focus();
168
+ },
169
+ extensions: [
170
+ Bold,
171
+ BulletList,
172
+ CharacterCount.configure({ limit: props.maxCharacterCount }),
173
+ Document,
174
+ History,
175
+ Italic,
176
+ ListItem,
177
+ Paragraph,
178
+ Placeholder.configure({ placeholder: props.placeholder }),
179
+ Text,
180
+ Warning.configure({
181
+ getErrorWords: getErrorWords,
182
+ onEnter: ({ range, command, virtualNode, text }) => {
183
+ currentWarning.value = errors.value.find((err) => err.value === text);
184
+ currentOptions.value = currentWarning.value.options || [];
185
+ navigatedOptionIndex.value = 0;
186
+ optionRange.value = range;
187
+ renderPopup(virtualNode);
188
+ insertOption.value = command;
189
+ },
190
+ onChange: ({ range, virtualNode, text }) => {
191
+ currentWarning.value = errors.value.find((err) => err.value === text);
192
+ currentOptions.value = currentWarning.value.options || [];
193
+ navigatedOptionIndex.value = 0;
194
+ optionRange.value = range;
195
+ renderPopup(virtualNode);
196
+ },
197
+ onExit: () => {
198
+ navigatedOptionIndex.value = 0;
199
+ currentOptions.value = null;
200
+ optionRange.value = null;
201
+ destroyPopup();
202
+ },
203
+ onKeyDown: ({ event }) => {
204
+ // pressing up arrow
205
+ if (event.keyCode === 38 && currentOptions.value !== null) {
206
+ upHandler();
207
+ return true;
208
+ }
209
+ // pressing down arrow
210
+ if (event.keyCode === 40 && currentOptions !== null) {
211
+ downHandler();
212
+ return true;
213
+ }
214
+ // pressing enter
215
+ if (event.keyCode === 13) {
216
+ return enterHandler();
217
+ }
218
+ // pressing escape
219
+ if (event.keyCode === 27) {
220
+ navigatedOptionIndex.value = 0;
221
+ optionRange.value = null;
222
+ currentOptions.value = null;
223
+ destroyPopup();
224
+ return true;
225
+ }
226
+ return false;
227
+ },
228
+ }),
229
+ ],
230
230
  });
231
231
 
232
232
  // Warnings & errors popup
@@ -239,64 +239,64 @@ const popup = ref(null);
239
239
  const renderedErrors = ref(null);
240
240
 
241
241
  watch(
242
- () => props.warnings,
243
- (n, o) => {
244
- if (editor) {
245
- props.warnings.forEach((warning) => {
246
- if (warning.length && warning.offset >= 0) {
247
- warning.offset += getOffsetAdjustment(warning);
248
- }
249
- });
250
-
251
- // preserve selection after updating warnings
252
- /* const oldSelection = editor.view.state.selection; */
253
-
254
- /* editor.commands.setTextSelection({
242
+ () => props.warnings,
243
+ (n, o) => {
244
+ if (editor) {
245
+ props.warnings.forEach((warning) => {
246
+ if (warning.length && warning.offset >= 0) {
247
+ warning.offset += getOffsetAdjustment(warning);
248
+ }
249
+ });
250
+
251
+ // preserve selection after updating warnings
252
+ /* const oldSelection = editor.view.state.selection; */
253
+
254
+ /* editor.commands.setTextSelection({
255
255
  from: oldSelection.from,
256
256
  to: oldSelection.to,
257
257
  }); */
258
258
 
259
- // record length of text that was used to generate the list of warnings
260
- initialCharacterCount.value = currentCharacterCount.value;
261
- }
262
- }
259
+ // record length of text that was used to generate the list of warnings
260
+ initialCharacterCount.value = currentCharacterCount.value;
261
+ }
262
+ }
263
263
  );
264
264
 
265
265
  tippy.setDefaultProps({
266
- content: renderedErrors.value,
267
- trigger: 'mouseenter',
268
- interactive: true,
269
- theme: 'tiptap',
270
- placement: 'top-start',
271
- inertia: true,
272
- duration: [400, 200],
273
- showOnCreate: true,
274
- arrow: true,
275
- hideOnClick: false,
266
+ content: renderedErrors.value,
267
+ trigger: 'mouseenter',
268
+ interactive: true,
269
+ theme: 'tiptap',
270
+ placement: 'top-start',
271
+ inertia: true,
272
+ duration: [400, 200],
273
+ showOnCreate: true,
274
+ arrow: true,
275
+ hideOnClick: false,
276
276
  });
277
277
 
278
278
  props.warnings.forEach((warning) => {
279
- if (warning.length && warning.offset >= 0) {
280
- warning.offset += getOffsetAdjustment(warning);
281
- }
279
+ if (warning.length && warning.offset >= 0) {
280
+ warning.offset += getOffsetAdjustment(warning);
281
+ }
282
282
  });
283
283
 
284
284
  const errors = computed(() => {
285
- if (props.warnings.length < 1) {
286
- return [];
287
- }
288
- return props.warnings.map((mistake) => {
289
- const isWord = mistake.isWord === undefined ? true : mistake.isWord;
290
- return {
291
- overrideClass: mistake.overrideClass,
292
- isWord: isWord,
293
- value: isWord ? mistake.value : unescape(mistake.value),
294
- message: mistake.message,
295
- offset: mistake.offset,
296
- length: mistake.length,
297
- options: (mistake.options || []).map((value, id) => ({ value, id })),
298
- };
299
- });
285
+ if (props.warnings.length < 1) {
286
+ return [];
287
+ }
288
+ return props.warnings.map((mistake) => {
289
+ const isWord = mistake.isWord === undefined ? true : mistake.isWord;
290
+ return {
291
+ overrideClass: mistake.overrideClass,
292
+ isWord: isWord,
293
+ value: isWord ? mistake.value : unescape(mistake.value),
294
+ message: mistake.message,
295
+ offset: mistake.offset,
296
+ length: mistake.length,
297
+ options: (mistake.options || []).map((value, id) => ({ value, id })),
298
+ };
299
+ });
300
300
  });
301
301
 
302
302
  // character counting
@@ -306,399 +306,400 @@ const previousCharacterCount = ref(currentCharacterCount.value);
306
306
  const previousHTML = ref('');
307
307
 
308
308
  const maxCharacterCountExceeded = computed(
309
- () => currentCharacterCount.value >= props.maxCharacterCount
309
+ () => currentCharacterCount.value >= props.maxCharacterCount
310
310
  );
311
311
  const characterCountPercentage = computed(() =>
312
- Math.round((100 / props.maxCharacterCount) * currentCharacterCount.value)
312
+ Math.round((100 / props.maxCharacterCount) * currentCharacterCount.value)
313
313
  );
314
314
 
315
315
  // compliance checks base offset data off the HTML value of the text
316
316
  // ProseMirror uses a unique token sequence indexing system - see https://prosemirror.net/docs/guide/#doc.indexing
317
317
  // we need to convert warning offsets to the index values ProseMirror expects
318
318
  function getOffsetAdjustment(warning) {
319
- // compliance check offsets count all HTML entities as one character, whereas the editor value does not. compensate for
320
- // this by adding to the offset until it matches (or equals the length of the editor text as a failsafe break from the loop)
321
- const normalizedString = props.value.normalize('NFKC').replace(/&nbsp;/g, ' ');
322
- while (
323
- normalizedString.substr(warning.offset, warning.value.length) !== warning.value &&
324
- warning.offset <= normalizedString.length
325
- ) {
326
- warning.offset++;
327
- }
328
-
329
- // obtain the substring up until the beginning of the warning
330
- const originalLength = warning.offset;
331
- let substr = props.value.substr(0, warning.offset);
332
-
333
- // these tags count as one token, so we replace them with a single space
334
- substr = substr.replace(/<p>|<\/p>|<li>|<ul>|<\/ul>|<\/li>|<div>|<\/div>/g, ' ');
335
-
336
- // these tags don't count as a token, so we remove them
337
- const knownTagRegex = /<strong>|<\/strong>|<em>|<\/em>/g;
338
- substr = substr.replace(knownTagRegex, '');
339
-
340
- // return the number of chars the offset should be adjusted by
341
- return substr.length - originalLength;
319
+ // compliance check offsets count all HTML entities as one character, whereas the editor value does not. compensate for
320
+ // this by adding to the offset until it matches (or equals the length of the editor text as a failsafe break from the loop)
321
+ const normalizedString = props.value.normalize('NFKC').replace(/&nbsp;/g, ' ');
322
+ while (
323
+ normalizedString.substr(warning.offset, warning.value.length) !== warning.value &&
324
+ warning.offset <= normalizedString.length
325
+ ) {
326
+ warning.offset++;
327
+ }
328
+
329
+ // obtain the substring up until the beginning of the warning
330
+ const originalLength = warning.offset;
331
+ let substr = props.value.substr(0, warning.offset);
332
+
333
+ // these tags count as one token, so we replace them with a single space
334
+ substr = substr.replace(/<p>|<\/p>|<li>|<ul>|<\/ul>|<\/li>|<div>|<\/div>/g, ' ');
335
+
336
+ // these tags don't count as a token, so we remove them
337
+ const knownTagRegex = /<strong>|<\/strong>|<em>|<\/em>/g;
338
+ substr = substr.replace(knownTagRegex, '');
339
+
340
+ // return the number of chars the offset should be adjusted by
341
+ return substr.length - originalLength;
342
342
  }
343
343
 
344
344
  // when the user adds a newline to the text of the editor, the character count stays the same, but ProseMirror's token
345
345
  // sequence indexing system adds 2 tokens to the content. We need to adjust warning offsets to account for that
346
346
  function adjustForNewlines(editor) {
347
- const regex = /<\/p><p>/g;
348
- const previousNewlines = (previousHTML.value.match(regex) || []).length;
349
- const newlines = (editor.getHTML().match(regex) || []).length;
347
+ const regex = /<\/p><p>/g;
348
+ const previousNewlines = (previousHTML.value.match(regex) || []).length;
349
+ const newlines = (editor.getHTML().match(regex) || []).length;
350
350
 
351
- // multiply the difference in newlines by 2 since each instance counts as 2 tokens
352
- return (newlines - previousNewlines) * 2;
351
+ // multiply the difference in newlines by 2 since each instance counts as 2 tokens
352
+ return (newlines - previousNewlines) * 2;
353
353
  }
354
354
 
355
355
  function getErrorWords() {
356
- if (errors.value.length < 1) {
357
- return [];
358
- }
359
- return errors.value.map((err) => ({
360
- value: err.value,
361
- overrideClass: err.overrideClass,
362
- isWord: err.isWord,
363
- offset: err.offset,
364
- length: err.length,
365
- }));
356
+ if (errors.value.length < 1) {
357
+ return [];
358
+ }
359
+ return errors.value.map((err) => ({
360
+ value: err.value,
361
+ overrideClass: err.overrideClass,
362
+ isWord: err.isWord,
363
+ offset: err.offset,
364
+ length: err.length,
365
+ }));
366
366
  }
367
367
 
368
368
  function upHandler() {
369
- optionsLength = currentOptions.value.length;
370
- navigatedOptionIndex.value = (navigatedOptionIndex.value + optionsLength - 1) % optionsLength;
369
+ optionsLength = currentOptions.value.length;
370
+ navigatedOptionIndex.value = (navigatedOptionIndex.value + optionsLength - 1) % optionsLength;
371
371
  }
372
372
 
373
373
  function downHandler() {
374
- navigatedOptionIndex.value = (navigatedOptionIndex.value + 1) % currentOptions.value.length;
374
+ navigatedOptionIndex.value = (navigatedOptionIndex.value + 1) % currentOptions.value.length;
375
375
  }
376
376
 
377
377
  function enterHandler() {
378
- if (currentOptions.value.length === 0) {
379
- return false;
380
- }
381
-
382
- const option = currentOptions.value[navigatedOptionIndex.value];
383
- if (option) {
384
- selectOption(option);
385
- }
386
- return true;
378
+ if (currentOptions.value.length === 0) {
379
+ return false;
380
+ }
381
+
382
+ const option = currentOptions.value[navigatedOptionIndex.value];
383
+ if (option) {
384
+ selectOption(option);
385
+ }
386
+ return true;
387
387
  }
388
388
 
389
389
  function selectOption(option) {
390
- insertOption.value({
391
- range: optionRange.value,
392
- attrs: { id: option.id, label: option.value },
393
- });
390
+ insertOption.value({
391
+ range: optionRange.value,
392
+ attrs: { id: option.id, label: option.value },
393
+ });
394
394
  }
395
395
 
396
396
  function renderPopup(virtualNode) {
397
- if (!popup.value && virtualNode) {
398
- const rect = virtualNode.getBoundingClientRect();
399
- popup.value = tippy(document.body, {
400
- content: renderedErrors.value,
401
- getReferenceClientRect: () => ({
402
- width: rect.width,
403
- height: rect.height,
404
- left: rect.left,
405
- right: rect.right,
406
- top: rect.top,
407
- bottom: rect.bottom,
408
- }),
409
- });
410
- }
397
+ if (!popup.value && virtualNode) {
398
+ const rect = virtualNode.getBoundingClientRect();
399
+ popup.value = tippy(document.body, {
400
+ content: renderedErrors.value,
401
+ getReferenceClientRect: () => ({
402
+ width: rect.width,
403
+ height: rect.height,
404
+ left: rect.left,
405
+ right: rect.right,
406
+ top: rect.top,
407
+ bottom: rect.bottom,
408
+ }),
409
+ });
410
+ }
411
411
  }
412
412
 
413
413
  function destroyPopup() {
414
- if (popup.value) {
415
- popup.value.destroy();
416
- popup.value = null;
417
- }
414
+ if (popup.value) {
415
+ popup.value.destroy();
416
+ popup.value = null;
417
+ }
418
418
  }
419
419
 
420
420
  function toolbarGoLeft(evt) {
421
- evt.preventDefault();
422
- const prevSibling = evt.target.previousSibling;
421
+ evt.preventDefault();
422
+ const prevSibling = evt.target.previousSibling;
423
423
 
424
- if (prevSibling && prevSibling.focus !== undefined) {
425
- prevSibling.focus();
426
- }
424
+ if (prevSibling && prevSibling.focus !== undefined) {
425
+ prevSibling.focus();
426
+ }
427
427
  }
428
428
 
429
429
  function toolbarGoRight(evt) {
430
- evt.preventDefault();
431
- const nextSibling = evt.target.nextSibling;
430
+ evt.preventDefault();
431
+ const nextSibling = evt.target.nextSibling;
432
432
 
433
- if (nextSibling && nextSibling.focus !== undefined) {
434
- nextSibling.focus();
435
- }
433
+ if (nextSibling && nextSibling.focus !== undefined) {
434
+ nextSibling.focus();
435
+ }
436
436
  }
437
437
 
438
438
  onBeforeUnmount(() => {
439
- if (popup.value) {
440
- popup.value.destroy();
441
- }
439
+ if (popup.value) {
440
+ popup.value.destroy();
441
+ }
442
442
  });
443
443
  </script>
444
444
 
445
445
  <style lang="scss">
446
446
  .error-list {
447
- .error-list__item {
448
- padding: 3px;
449
- border-radius: 2px;
450
-
451
- &.selected,
452
- &:hover {
453
- background-color: rgba(white, 0.2);
454
- }
455
- }
447
+ .error-list__item {
448
+ padding: 3px;
449
+ border-radius: 2px;
450
+
451
+ &.selected,
452
+ &:hover {
453
+ background-color: rgba(white, 0.2);
454
+ }
455
+ }
456
456
  }
457
457
 
458
458
  .tiptap-editor {
459
- border: 1px solid #e5e7eb;
460
- background: white;
461
- border-radius: 8px;
462
- padding: 4px;
463
-
464
- p.is-empty:first-child::before {
465
- content: attr(data-placeholder);
466
- float: left;
467
- color: #aaa;
468
- pointer-events: none;
469
- height: 0;
470
- }
471
-
472
- .menubar {
473
- // border-bottom: 1px solid #e5e7eb;
474
- padding: 4px;
475
- border-radius: 4px;
476
- background: #f4f4f5;
477
- display: flex;
478
-
479
- button {
480
- font-size: 14px;
481
- background-color: transparent;
482
- border: none;
483
- cursor: pointer;
484
- height: 30px;
485
- outline: 50;
486
- width: 35px;
487
- vertical-align: bottom;
488
- border-radius: 4px;
489
- margin-right: 3px;
490
-
491
- &:focus {
492
- outline: 2px solid #3b82f6;
493
- transition: all 0.08s ease-in-out;
494
- }
495
-
496
- &.is-active {
497
- background-color: #d3e3fd;
498
- }
499
-
500
- &.is-active:focus {
501
- background-color: #bfd2f9;
502
- }
503
-
504
- &:not(.is-active):hover {
505
- background-color: #e5e7eb;
506
- }
507
-
508
- &:not(.is-active):focus {
509
- background-color: #e5e7eb;
510
- }
511
-
512
- svg {
513
- width: 12px;
514
- }
515
- }
516
- }
517
-
518
- .editor__content {
519
- font-size: 16px;
520
- outline: 0;
521
- overflow-y: auto;
522
- padding: 6px 2px 4px 2px;
523
-
524
- .underline-red {
525
- border-bottom: 3px red solid;
526
- }
527
-
528
- .underline-orange {
529
- border-bottom: 3px orange solid;
530
- }
531
-
532
- .underline-green {
533
- border-bottom: 3px lightgreen solid;
534
- }
535
-
536
- .underline-blue {
537
- border-bottom: 3px #3b82f6 solid;
538
- }
539
-
540
- ul {
541
- padding: 0px 40px;
542
- }
543
-
544
- .ProseMirror {
545
- height: 100%;
546
- padding: 2px 8px 2px 8px;
547
- border-radius: 4px;
548
-
549
- &:focus {
550
- outline: 2px solid #3b82f6;
551
- transition: all 0.08s ease-in-out;
552
- }
553
- }
554
- }
459
+ border: 1px solid #e5e7eb;
460
+ background: white;
461
+ border-radius: 8px;
462
+ padding: 4px;
463
+
464
+ p.is-empty:first-child::before {
465
+ content: attr(data-placeholder);
466
+ float: left;
467
+ color: #aaa;
468
+ pointer-events: none;
469
+ height: 0;
470
+ }
471
+
472
+ .menubar {
473
+ // border-bottom: 1px solid #e5e7eb;
474
+ padding: 4px;
475
+ border-radius: 4px;
476
+ background: #f4f4f5;
477
+ display: flex;
478
+
479
+ button {
480
+ font-size: 14px;
481
+ background-color: transparent;
482
+ border: none;
483
+ cursor: pointer;
484
+ height: 30px;
485
+ outline: 50;
486
+ width: 35px;
487
+ vertical-align: bottom;
488
+ border-radius: 4px;
489
+ margin-right: 3px;
490
+
491
+ &:focus {
492
+ outline: 2px solid #3b82f6;
493
+ transition: all 0.08s ease-in-out;
494
+ }
495
+
496
+ &.is-active {
497
+ background-color: #d3e3fd;
498
+ }
499
+
500
+ &.is-active:focus {
501
+ background-color: #bfd2f9;
502
+ }
503
+
504
+ &:not(.is-active):hover {
505
+ background-color: #e5e7eb;
506
+ }
507
+
508
+ &:not(.is-active):focus {
509
+ background-color: #e5e7eb;
510
+ }
511
+
512
+ svg {
513
+ width: 12px;
514
+ }
515
+ }
516
+ }
517
+
518
+ .editor__content {
519
+ font-size: 16px;
520
+ outline: 0;
521
+ overflow-y: auto;
522
+ padding: 6px 2px 4px 2px;
523
+
524
+ .underline-red {
525
+ border-bottom: 3px red solid;
526
+ }
527
+
528
+ .underline-orange {
529
+ border-bottom: 3px orange solid;
530
+ }
531
+
532
+ .underline-green {
533
+ border-bottom: 3px lightgreen solid;
534
+ }
535
+
536
+ .underline-blue {
537
+ border-bottom: 3px #3b82f6 solid;
538
+ }
539
+
540
+ ul {
541
+ padding: 0px 40px;
542
+ }
543
+
544
+ .ProseMirror {
545
+ height: 100%;
546
+ padding: 2px 8px 2px 8px;
547
+ border-radius: 4px;
548
+
549
+ &:focus {
550
+ outline: 2px solid #3b82f6;
551
+ transition: all 0.08s ease-in-out;
552
+ }
553
+ }
554
+ }
555
555
  }
556
556
 
557
557
  .character-count {
558
- padding: 4px;
559
- border-radius: 4px;
560
- text-align: right;
561
- padding-right: 15px;
562
- display: flex;
563
- gap: 8px;
564
- justify-content: flex-end;
565
- flex-grow: 2;
566
-
567
- &__graph {
568
- color: #a8c2f7;
569
-
570
- &--warning {
571
- color: #fb7373;
572
- }
573
- }
574
-
575
- &__inner-circle {
576
- fill: #f4f4f5;
577
- }
578
-
579
- &__outer-circle {
580
- fill: #ffffff;
581
- }
558
+ padding: 4px;
559
+ border-radius: 4px;
560
+ text-align: right;
561
+ padding-right: 15px;
562
+ display: flex;
563
+ gap: 8px;
564
+ justify-content: flex-end;
565
+ align-items: center;
566
+ flex-grow: 2;
567
+
568
+ &__graph {
569
+ color: #a8c2f7;
570
+
571
+ &--warning {
572
+ color: #fb7373;
573
+ }
574
+ }
575
+
576
+ &__inner-circle {
577
+ fill: #f4f4f5;
578
+ }
579
+
580
+ &__outer-circle {
581
+ fill: #ffffff;
582
+ }
582
583
  }
583
584
 
584
585
  .tippy-box[data-theme~='tiptap'] {
585
- background-color: #0a0a0a;
586
+ background-color: #0a0a0a;
586
587
  }
587
588
  .tippy-box[data-theme~='tiptap'] > .tippy-arrow {
588
- width: 14px;
589
- height: 14px;
589
+ width: 14px;
590
+ height: 14px;
590
591
  }
591
592
  .tippy-box[data-theme~='tiptap'][data-placement^='top'] > .tippy-arrow:before {
592
- border-width: 7px 7px 0;
593
- border-top-color: #0a0a0a;
593
+ border-width: 7px 7px 0;
594
+ border-top-color: #0a0a0a;
594
595
  }
595
596
  .tippy-box[data-theme~='tiptap'][data-placement^='bottom'] > .tippy-arrow:before {
596
- border-width: 0 7px 7px;
597
- border-bottom-color: #0a0a0a;
597
+ border-width: 0 7px 7px;
598
+ border-bottom-color: #0a0a0a;
598
599
  }
599
600
  .tippy-box[data-theme~='tiptap'][data-placement^='left'] > .tippy-arrow:before {
600
- border-width: 7px 0 7px 7px;
601
- border-left-color: #0a0a0a;
601
+ border-width: 7px 0 7px 7px;
602
+ border-left-color: #0a0a0a;
602
603
  }
603
604
  .tippy-box[data-theme~='tiptap'][data-placement^='right'] > .tippy-arrow:before {
604
- border-width: 7px 7px 7px 0;
605
- border-right-color: #0a0a0a;
605
+ border-width: 7px 7px 7px 0;
606
+ border-right-color: #0a0a0a;
606
607
  }
607
608
  .tippy-box[data-theme~='tiptap'] > .tippy-backdrop {
608
- background-color: #0a0a0a;
609
+ background-color: #0a0a0a;
609
610
  }
610
611
  .tippy-box[data-theme~='tiptap'] > .tippy-svg-arrow {
611
- fill: #0a0a0a;
612
+ fill: #0a0a0a;
612
613
  }
613
614
 
614
615
  html[color-scheme='dark'] {
615
- .tiptap-editor {
616
- border: 2px solid #374151;
617
- background: #111827;
618
- color: #d1d5db;
619
-
620
- .menubar {
621
- background: #1f2937;
622
- color: #d1d5db;
623
-
624
- button {
625
- color: #d1d5db;
626
-
627
- &:focus {
628
- outline: 2px solid #3b82f6;
629
- transition: all 0.08s ease-in-out;
630
- }
631
-
632
- &.is-active {
633
- background-color: #374151;
634
- }
635
-
636
- &.is-active:focus {
637
- background-color: #4b5563;
638
- }
639
-
640
- &:not(.is-active):hover {
641
- background-color: #374151;
642
- }
643
-
644
- &:not(.is-active):focus {
645
- background-color: #374151;
646
- }
647
-
648
- svg {
649
- width: 12px;
650
- }
651
- }
652
- }
653
- }
654
-
655
- .character-count {
656
- &__graph {
657
- color: #a8c2f7;
658
-
659
- &--warning {
660
- color: #fb7373;
661
- }
662
- }
663
-
664
- &__inner-circle {
665
- fill: #1f2937;
666
- }
667
-
668
- &__outer-circle {
669
- fill: #111827;
670
- }
671
- }
672
-
673
- .tippy-box[data-theme~='tiptap'] {
674
- background-color: #d1d5db;
675
- color: #111827;
676
- }
677
- .tippy-box[data-theme~='tiptap'][data-placement^='top'] > .tippy-arrow:before {
678
- border-top-color: #d1d5db;
679
- }
680
- .tippy-box[data-theme~='tiptap'][data-placement^='bottom'] > .tippy-arrow:before {
681
- border-bottom-color: #d1d5db;
682
- }
683
- .tippy-box[data-theme~='tiptap'][data-placement^='left'] > .tippy-arrow:before {
684
- border-left-color: #d1d5db;
685
- }
686
- .tippy-box[data-theme~='tiptap'][data-placement^='right'] > .tippy-arrow:before {
687
- border-right-color: #d1d5db;
688
- }
689
- .tippy-box[data-theme~='tiptap'] > .tippy-backdrop {
690
- background-color: #d1d5db;
691
- }
692
- .tippy-box[data-theme~='tiptap'] > .tippy-svg-arrow {
693
- fill: #d1d5db;
694
- }
695
- .error-list {
696
- .error-list__item {
697
- &.selected,
698
- &:hover {
699
- background-color: rgba(black, 0.15);
700
- }
701
- }
702
- }
616
+ .tiptap-editor {
617
+ border: 2px solid #27272a;
618
+ background: #18181b;
619
+ color: #e4e4e7;
620
+
621
+ .menubar {
622
+ background: #27272a;
623
+ color: #d1d5db;
624
+
625
+ button {
626
+ color: #e4e4e7;
627
+
628
+ &:focus {
629
+ outline: 2px solid #3b82f6;
630
+ transition: all 0.08s ease-in-out;
631
+ }
632
+
633
+ &.is-active {
634
+ background-color: #515e75;
635
+ }
636
+
637
+ &.is-active:focus {
638
+ background-color: #515e75;
639
+ }
640
+
641
+ &:not(.is-active):hover {
642
+ background-color: #3f3f46;
643
+ }
644
+
645
+ &:not(.is-active):focus {
646
+ background-color: #3f3f46;
647
+ }
648
+
649
+ svg {
650
+ width: 12px;
651
+ }
652
+ }
653
+ }
654
+ }
655
+
656
+ .character-count {
657
+ &__graph {
658
+ color: #a8c2f7;
659
+
660
+ &--warning {
661
+ color: #fb7373;
662
+ }
663
+ }
664
+
665
+ &__inner-circle {
666
+ fill: #27272a;
667
+ }
668
+
669
+ &__outer-circle {
670
+ fill: #27272a;
671
+ }
672
+ }
673
+
674
+ .tippy-box[data-theme~='tiptap'] {
675
+ background-color: #d1d5db;
676
+ color: #111827;
677
+ }
678
+ .tippy-box[data-theme~='tiptap'][data-placement^='top'] > .tippy-arrow:before {
679
+ border-top-color: #d1d5db;
680
+ }
681
+ .tippy-box[data-theme~='tiptap'][data-placement^='bottom'] > .tippy-arrow:before {
682
+ border-bottom-color: #d1d5db;
683
+ }
684
+ .tippy-box[data-theme~='tiptap'][data-placement^='left'] > .tippy-arrow:before {
685
+ border-left-color: #d1d5db;
686
+ }
687
+ .tippy-box[data-theme~='tiptap'][data-placement^='right'] > .tippy-arrow:before {
688
+ border-right-color: #d1d5db;
689
+ }
690
+ .tippy-box[data-theme~='tiptap'] > .tippy-backdrop {
691
+ background-color: #d1d5db;
692
+ }
693
+ .tippy-box[data-theme~='tiptap'] > .tippy-svg-arrow {
694
+ fill: #d1d5db;
695
+ }
696
+ .error-list {
697
+ .error-list__item {
698
+ &.selected,
699
+ &:hover {
700
+ background-color: rgba(black, 0.15);
701
+ }
702
+ }
703
+ }
703
704
  }
704
705
  </style>