PluginProbe
The Innovative Form Builder – IvyForms / 0.4
The Innovative Form Builder – IvyForms v0.4
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.4, at frontend/src/views/_components/html-editor/IvyEditor.vue

448 lines 10.9 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 }>()
218
219 const emit = defineEmits(['update:modelValue', 'input', 'insertPlaceholder'])
220
221 function onInsertPlaceholder(placeholder: string) {
222 emit('insertPlaceholder', placeholder)
223 }
224
225 const localModelValue = computed({
226 get() {
227 return props.modelValue
228 },
229 set(value) {
230 emit('update:modelValue', value)
231 },
232 })
233
234 const editor = useEditor({
235 extensions: [
236 Document,
237 Text,
238 Paragraph,
239 Bold,
240 Italic,
241 Underline,
242 Strike,
243 Blockquote,
244 TextStyle,
245 Color,
246 TextAlign.configure({
247 types: ['heading', 'paragraph'],
248 }),
249 Heading,
250 BulletList,
251 OrderedList,
252 ListItem,
253 Superscript,
254 Subscript,
255 FontFamily,
256 Link.configure({
257 openOnClick: true,
258 defaultProtocol: 'https',
259 }),
260 Image,
261 IvyEditorClassExtension,
262 IvyEditorColorExtension,
263 ],
264 content: localModelValue.value,
265 onUpdate: () => {
266 localModelValue.value = editor.value.getHTML()
267 },
268 })
269
270 watch(localModelValue, () => {
271 const isSame = editor.value.getHTML() === localModelValue.value
272
273 if (isSame) {
274 return
275 }
276
277 editor.value.commands.setContent(localModelValue.value)
278 })
279
280 const focus = () => {
281 editor.value.commands.focus()
282 }
283
284 const handleKeydown = (event: KeyboardEvent) => {
285 const isArrowKey = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)
286
287 if (editor?.value?.isFocused && isArrowKey) {
288 event.stopPropagation()
289 }
290 }
291
292 const hasContent = () => {
293 const nodesArray = editor?.value?.getJSON()?.content
294
295 return !(
296 Array.isArray(nodesArray) &&
297 nodesArray.length === 1 &&
298 !Object.hasOwn(nodesArray[0], 'content')
299 )
300 }
301
302 defineExpose({
303 editor,
304 focus,
305 hasContent,
306 })
307 </script>
308
309 <style lang="scss">
310 .ivyforms-editor {
311 display: flex;
312 padding: 8px;
313 flex-direction: column;
314 align-items: flex-start;
315 gap: 6px;
316 align-self: stretch;
317 border-radius: 8px;
318 border: 1px solid var(--map-base-dusk-stroke--2);
319
320 .tab-content > div {
321 width: 100% !important;
322 }
323 .tab-content {
324 width: 100%;
325 }
326 ::v-deep(.tab-content > div) {
327 width: 100%;
328 }
329
330 &__content {
331 min-height: 80px;
332 cursor: text;
333 display: flex;
334 flex-direction: column;
335 align-items: flex-start;
336 gap: 6px;
337 align-self: stretch;
338 color: var(--map-base-text--2);
339 border-radius: 8px;
340 border: 1px solid var(--map-base-dusk-stroke--2);
341 background: var(--map-base-dusk-o05);
342 box-sizing: border-box;
343 width: 100%;
344
345 &.ivyforms-editor-html {
346 }
347
348 & > :first-child {
349 margin-top: 0;
350 outline: none;
351 }
352
353 img {
354 display: block;
355 height: auto;
356 margin: 1.5rem 0;
357 max-width: 100%;
358 }
359
360 a {
361 cursor: pointer;
362 }
363
364 ul {
365 list-style-type: disc;
366 }
367
368 ol {
369 list-style-type: decimal;
370 }
371
372 ul,
373 ol {
374 padding: 0 1rem;
375
376 li p {
377 margin-top: 0.25em;
378 margin-bottom: 0.25em;
379 }
380 }
381
382 blockquote {
383 border-left: 3px solid var(--map-base-brand-stroke--2);
384 margin: 1.5rem 0;
385 padding-left: 1rem;
386 }
387
388 h1,
389 h2,
390 h3,
391 h4,
392 h5,
393 h6 {
394 line-height: 1.1;
395 color: var(--map-base-text--1);
396 }
397
398 h1 {
399 font-size: 1.6rem;
400 }
401
402 h2 {
403 font-size: 1.5rem;
404 }
405
406 h3 {
407 font-size: 1.4rem;
408 }
409
410 h4 {
411 font-size: 1.3rem;
412 }
413 h5 {
414 font-size: 1.2rem;
415 }
416 h6 {
417 font-size: 1.1rem;
418 }
419 }
420
421 &__control {
422 padding: 4px;
423 display: flex;
424 flex-wrap: wrap;
425 gap: 4px;
426
427 .ivyforms-form-item-select {
428 width: 150px;
429 }
430 button[name='bg-highlight'] {
431 svg {
432 path:first-of-type {
433 stroke: var(--map-base-dusk-symbol-2);
434 opacity: 0.5;
435 }
436 }
437 }
438 }
439
440 &__tabs-row {
441 display: flex;
442 align-items: center;
443 gap: 8px;
444 width: 100%;
445 }
446 }
447 </style>
448