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

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