PluginProbe
The Innovative Form Builder – IvyForms / 0.5
The Innovative Form Builder – IvyForms v0.5
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / frontend / src / views / _components / html-editor / IvyEditor.vue

IvyEditor.vue in The Innovative Form Builder – IvyForms 0.5, at frontend/src/views/_components/html-editor/IvyEditor.vue

454 lines 11.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <template>
2 <div
3 class="ivyforms-editor"
4 :class="{
5 'is-focused': editor?.isFocused,
6 }"
7 >
8 <div
9 class="ivyforms-editor__tabs-row ivyforms-flex ivyforms-justify-content-between ivyforms-align-items-center"
10 >
11 <IvyTabs
12 v-model="activeTab"
13 type="tonal"
14 priority="secondary"
15 size="l"
16 :tabs="tabs.map((item) => ({ name: item.index, label: item.label }))"
17 />
18 <IvyPlaceholdersPopover
19 v-if="fieldsPlaceholders && generalPlaceholders"
20 :fields="fieldsPlaceholders"
21 :general="generalPlaceholders"
22 @insert-placeholder="onInsertPlaceholder"
23 >
24 <template #reference>
25 <IvyTooltip :content="getLabel('insert_placeholders')">
26 <IvyButtonAction
27 size="s"
28 priority="tertiary"
29 icon-only
30 icon-start="context-menu-dot"
31 icon-start-type="fill"
32 :aria-label="getLabel('placeholders')"
33 class="ivyforms-editor__placeholders-btn"
34 />
35 </IvyTooltip>
36 </template>
37 </IvyPlaceholdersPopover>
38 </div>
39 <div class="tab-content">
40 <div v-if="activeTab === 'visual'">
41 <editor-content
42 class="ivyforms-editor__content ivyforms-p-12"
43 :editor="editor"
44 @mousedown="focus"
45 @keydown="handleKeydown"
46 />
47
48 <div v-if="editor" class="ivyforms-editor__control">
49 <IvyButtonOption
50 size="s"
51 icon-only
52 type="ghost"
53 priority="white"
54 icon-start-category="text-formatting"
55 icon-start="text-bold"
56 :active="editor.isActive('bold')"
57 @click.stop.prevent="editor.chain().focus().toggleBold().run()"
58 />
59
60 <IvyButtonOption
61 size="s"
62 type="ghost"
63 priority="white"
64 icon-only
65 icon-start-category="text-formatting"
66 icon-start="text-italic"
67 :active="editor.isActive('italic')"
68 @click.stop.prevent="editor.chain().focus().toggleItalic().run()"
69 />
70
71 <IvyButtonOption
72 size="s"
73 type="ghost"
74 priority="white"
75 icon-only
76 icon-start-category="text-formatting"
77 icon-start="text-underline"
78 :active="editor.isActive('underline')"
79 @click.stop.prevent="editor.chain().focus().toggleUnderline().run()"
80 />
81
82 <IvyButtonOption
83 size="s"
84 type="ghost"
85 priority="white"
86 icon-only
87 icon-start-category="text-formatting"
88 icon-start="text-strikethrough"
89 :active="editor.isActive('strike')"
90 @click.stop.prevent="editor.chain().focus().toggleStrike().run()"
91 />
92
93 <IvyButtonOption
94 size="s"
95 type="ghost"
96 priority="white"
97 icon-only
98 icon-start-category="text-formatting"
99 icon-start="list-bulleted"
100 :active="editor.isActive('bulletList')"
101 @click.stop.prevent="editor.chain().focus().toggleBulletList().run()"
102 />
103
104 <IvyButtonOption
105 size="s"
106 type="ghost"
107 priority="white"
108 icon-only
109 icon-start-category="text-formatting"
110 icon-start="list-numbered"
111 :active="editor.isActive('orderedList')"
112 @click.stop.prevent="editor.chain().focus().toggleOrderedList().run()"
113 />
114
115 <IvyButtonOption
116 size="s"
117 type="ghost"
118 priority="white"
119 icon-only
120 icon-start-category="text-formatting"
121 icon-start="quotes"
122 :active="editor.isActive('blockquote')"
123 @click.stop.prevent="editor.chain().focus().toggleBlockquote().run()"
124 />
125
126 <IvyTextAlignPopoover :editor="editor" />
127
128 <IvyColorPickerPopover :editor="editor" text-style-property="color">
129 <template #reference>
130 <IvyButtonOption
131 size="s"
132 type="ghost"
133 priority="white"
134 icon-only
135 icon-start-category="text-formatting"
136 icon-start="text-color"
137 @click.stop.prevent
138 />
139 </template>
140 </IvyColorPickerPopover>
141
142 <IvyColorPickerPopover :editor="editor" text-style-property="backgroundColor">
143 <template #reference>
144 <IvyButtonOption
145 size="s"
146 type="ghost"
147 priority="white"
148 icon-only
149 icon-start-category="text-formatting"
150 icon-start="bg-highlight"
151 name="bg-highlight"
152 @click.stop.prevent
153 />
154 </template>
155 </IvyColorPickerPopover>
156
157 <IvyLinkPopover :editor="editor" />
158
159 <IvyImagePopover :editor="editor" />
160
161 <IvyHeadingPopover :editor="editor" />
162
163 <IvyFontFamilyPopover :editor="editor" />
164 </div>
165 </div>
166 <div v-if="activeTab === 'html'">
167 <editor-content
168 class="ivyforms-editor__content ivyforms-p-12 ivyforms-flex-grow ivyforms-editor-html"
169 :editor="editor"
170 @mousedown="focus"
171 @keydown="handleKeydown"
172 />
173 </div>
174 </div>
175 </div>
176 </template>
177
178 <script setup lang="ts">
179 import { useEditor, EditorContent } from '@tiptap/vue-3'
180 import { Document } from '@tiptap/extension-document'
181 import { Text } from '@tiptap/extension-text'
182 import { Paragraph } from '@tiptap/extension-paragraph'
183 import { computed, watch } from 'vue'
184 import { Color } from '@tiptap/extension-color'
185 import { Bold } from '@tiptap/extension-bold'
186 import TextStyle from '@tiptap/extension-text-style'
187 import TextAlign from '@tiptap/extension-text-align'
188 import { Heading } from '@tiptap/extension-heading'
189 import { Italic } from '@tiptap/extension-italic'
190 import { Underline } from '@tiptap/extension-underline'
191 import { Strike } from '@tiptap/extension-strike'
192 import { Blockquote } from '@tiptap/extension-blockquote'
193 import { BulletList } from '@tiptap/extension-bullet-list'
194 import { ListItem } from '@tiptap/extension-list-item'
195 import { OrderedList } from '@tiptap/extension-ordered-list'
196 import { Superscript } from '@tiptap/extension-superscript'
197 import { Subscript } from '@tiptap/extension-subscript'
198 import { FontFamily } from '@tiptap/extension-font-family'
199 import { Link } from '@tiptap/extension-link'
200 import { Image } from '@tiptap/extension-image'
201 import IvyEditorClassExtension from '@/views/_components/html-editor/extensions/ivyEditorClassExtension.ts'
202 import { IvyEditorColorExtension } from '@/views/_components/html-editor/extensions/ivyEditorColorExtension.ts'
203 import { ref } from 'vue'
204 import { useLabels } from '@/composables/useLabels'
205 const { getLabel } = useLabels()
206
207 const activeTab = ref('visual')
208
209 const tabs = [
210 { label: getLabel('visual'), index: 'visual' },
211 { label: getLabel('html'), index: 'html' },
212 ]
213 const props = defineProps<{
214 modelValue?: string
215 fieldsPlaceholders?: Array<Record<string, unknown>>
216 generalPlaceholders?: Array<Record<string, unknown>>
217 ariaLabel?: string
218 }>()
219
220 const emit = defineEmits(['update:modelValue', 'input', 'insertPlaceholder'])
221
222 function onInsertPlaceholder(placeholder: string) {
223 emit('insertPlaceholder', placeholder)
224 }
225
226 const localModelValue = computed({
227 get() {
228 return props.modelValue
229 },
230 set(value) {
231 emit('update:modelValue', value)
232 },
233 })
234
235 const editor = useEditor({
236 extensions: [
237 Document,
238 Text,
239 Paragraph,
240 Bold,
241 Italic,
242 Underline,
243 Strike,
244 Blockquote,
245 TextStyle,
246 Color,
247 TextAlign.configure({
248 types: ['heading', 'paragraph'],
249 }),
250 Heading,
251 BulletList,
252 OrderedList,
253 ListItem,
254 Superscript,
255 Subscript,
256 FontFamily,
257 Link.configure({
258 openOnClick: true,
259 defaultProtocol: 'https',
260 }),
261 Image,
262 IvyEditorClassExtension,
263 IvyEditorColorExtension,
264 ],
265 content: localModelValue.value,
266 editorProps: {
267 attributes: {
268 'aria-label': props.ariaLabel || getLabel('text_editor'),
269 },
270 },
271 onUpdate: () => {
272 localModelValue.value = editor.value.getHTML()
273 },
274 })
275
276 watch(localModelValue, () => {
277 const isSame = editor.value.getHTML() === localModelValue.value
278
279 if (isSame) {
280 return
281 }
282
283 editor.value.commands.setContent(localModelValue.value)
284 })
285
286 const focus = () => {
287 editor.value.commands.focus()
288 }
289
290 const handleKeydown = (event: KeyboardEvent) => {
291 const isArrowKey = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)
292
293 if (editor?.value?.isFocused && isArrowKey) {
294 event.stopPropagation()
295 }
296 }
297
298 const hasContent = () => {
299 const nodesArray = editor?.value?.getJSON()?.content
300
301 return !(
302 Array.isArray(nodesArray) &&
303 nodesArray.length === 1 &&
304 !Object.hasOwn(nodesArray[0], 'content')
305 )
306 }
307
308 defineExpose({
309 editor,
310 focus,
311 hasContent,
312 })
313 </script>
314
315 <style lang="scss">
316 .ivyforms-editor {
317 display: flex;
318 padding: 8px;
319 flex-direction: column;
320 align-items: flex-start;
321 gap: 6px;
322 align-self: stretch;
323 border-radius: 8px;
324 border: 1px solid var(--map-base-dusk-stroke--2);
325
326 .tab-content > div {
327 width: 100% !important;
328 }
329 .tab-content {
330 width: 100%;
331 }
332 ::v-deep(.tab-content > div) {
333 width: 100%;
334 }
335
336 &__content {
337 min-height: 80px;
338 cursor: text;
339 display: flex;
340 flex-direction: column;
341 align-items: flex-start;
342 gap: 6px;
343 align-self: stretch;
344 color: var(--map-base-text--2);
345 border-radius: 8px;
346 border: 1px solid var(--map-base-dusk-stroke--2);
347 background: var(--map-base-dusk-o05);
348 box-sizing: border-box;
349 width: 100%;
350
351 &.ivyforms-editor-html {
352 }
353
354 & > :first-child {
355 margin-top: 0;
356 outline: none;
357 }
358
359 img {
360 display: block;
361 height: auto;
362 margin: 1.5rem 0;
363 max-width: 100%;
364 }
365
366 a {
367 cursor: pointer;
368 }
369
370 ul {
371 list-style-type: disc;
372 }
373
374 ol {
375 list-style-type: decimal;
376 }
377
378 ul,
379 ol {
380 padding: 0 1rem;
381
382 li p {
383 margin-top: 0.25em;
384 margin-bottom: 0.25em;
385 }
386 }
387
388 blockquote {
389 border-left: 3px solid var(--map-base-brand-stroke--2);
390 margin: 1.5rem 0;
391 padding-left: 1rem;
392 }
393
394 h1,
395 h2,
396 h3,
397 h4,
398 h5,
399 h6 {
400 line-height: 1.1;
401 color: var(--map-base-text--1);
402 }
403
404 h1 {
405 font-size: 1.6rem;
406 }
407
408 h2 {
409 font-size: 1.5rem;
410 }
411
412 h3 {
413 font-size: 1.4rem;
414 }
415
416 h4 {
417 font-size: 1.3rem;
418 }
419 h5 {
420 font-size: 1.2rem;
421 }
422 h6 {
423 font-size: 1.1rem;
424 }
425 }
426
427 &__control {
428 padding: 4px;
429 display: flex;
430 flex-wrap: wrap;
431 gap: 4px;
432
433 .ivyforms-form-item-select {
434 width: 150px;
435 }
436 button[name='bg-highlight'] {
437 svg {
438 path:first-of-type {
439 stroke: var(--map-base-dusk-symbol-2);
440 opacity: 0.5;
441 }
442 }
443 }
444 }
445
446 &__tabs-row {
447 display: flex;
448 align-items: center;
449 gap: 8px;
450 width: 100%;
451 }
452 }
453 </style>
454