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

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