PluginProbe
The Innovative Form Builder – IvyForms / 0.7
The Innovative Form Builder – IvyForms v0.7
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 / components / IvyLinkPopover.vue

IvyLinkPopover.vue in The Innovative Form Builder – IvyForms 0.7, at frontend/src/views/_components/html-editor/components/IvyLinkPopover.vue

178 lines 4.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <template>
2 <IvyPopover
3 ref="popoverRef"
4 class="ivyforms-link-popover"
5 trigger="click"
6 popper-background="level-2-foreground"
7 width="250"
8 @show="onShow"
9 @hide="onHide"
10 >
11 <div class="ivyforms-flex ivyforms-gap-8 ivyforms-mt-4">
12 <IvyTextInput
13 v-model="href"
14 placeholder="Enter Link"
15 autofocus
16 :error="linkError"
17 @blur="validateLink"
18 @input="clearError"
19 />
20 </div>
21 <div v-if="linkError" class="ivyforms-error-message ivyforms-ml-8 ivyforms-mt-4">
22 {{ linkError }}
23 </div>
24
25 <IvyCheckbox v-model="openInNewTab" class="ivyforms-ml-8">
26 {{ getLabel('open_in_new_tab') }}
27 </IvyCheckbox>
28
29 <div
30 class="ivyforms-flex ivyforms-justify-content-between ivyforms-align-self-stretch ivyforms-gap-16 ivyforms-mt-4"
31 >
32 <IvyButtonAction
33 class=""
34 priority="white"
35 type="fill"
36 size="d"
37 :disabled="!editor.isActive('link')"
38 @click.stop.prevent="unlink"
39 >
40 {{ getLabel('clear') }}
41 </IvyButtonAction>
42
43 <IvyButtonAction class="" size="d" @click.prevent="applyLink">
44 {{ getLabel('save') }}
45 </IvyButtonAction>
46 </div>
47
48 <template #reference>
49 <IvyButtonOption
50 size="s"
51 type="ghost"
52 priority="white"
53 icon-start-category="text-formatting"
54 icon-start="link"
55 :active="editor.isActive('link')"
56 @click.stop.prevent
57 />
58 </template>
59 </IvyPopover>
60 </template>
61
62 <script setup lang="ts">
63 import { ref, unref } from 'vue'
64 import { Editor } from '@tiptap/vue-3'
65 import IvyButtonOption from '@/views/_components/button/IvyButtonOption.vue'
66 import IvyButtonAction from '@/views/_components/button/IvyButtonAction.vue'
67 import IvyTextInput from '@/views/_components/input/IvyTextInput.vue'
68 import IvyCheckbox from '@/views/_components/checkbox/IvyCheckbox.vue'
69 import { useLabels } from '@/composables/useLabels'
70 const { getLabel } = useLabels()
71
72 const props = defineProps<{
73 editor: Editor
74 }>()
75
76 const popoverRef = ref(null)
77 const href = ref<string>('')
78 const openInNewTab = ref<boolean>(false)
79 const linkError = ref<string>('')
80
81 const isValidUrl = (url: string): boolean => {
82 if (!url || url.trim() === '') {
83 return false
84 }
85
86 // Allow relative URLs (starting with /, #, or ./)
87 if (url.startsWith('/') || url.startsWith('#') || url.startsWith('./') || url.startsWith('../')) {
88 return true
89 }
90
91 // Check for valid URL pattern
92 const urlPattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/i
93 const emailPattern = /^mailto:[^\s@]+@[^\s@]+\.[^\s@]+$/i
94 const telPattern = /^tel:\+?[\d\s\-()]+$/i
95
96 return urlPattern.test(url) || emailPattern.test(url) || telPattern.test(url)
97 }
98
99 const validateLink = () => {
100 if (!href.value) {
101 linkError.value = getLabel('link_required') || 'Link is required'
102 return false
103 }
104
105 if (!isValidUrl(href.value)) {
106 linkError.value = getLabel('invalid_link')
107 return false
108 }
109
110 linkError.value = ''
111 return true
112 }
113
114 const clearError = () => {
115 if (linkError.value) {
116 linkError.value = ''
117 }
118 }
119
120 const applyLink = () => {
121 if (!validateLink()) {
122 return
123 }
124
125 let finalHref = href.value.trim()
126
127 // Auto-add https:// if no protocol is specified and it's not a relative/special URL
128 if (
129 !finalHref.startsWith('http://') &&
130 !finalHref.startsWith('https://') &&
131 !finalHref.startsWith('mailto:') &&
132 !finalHref.startsWith('tel:') &&
133 !finalHref.startsWith('/') &&
134 !finalHref.startsWith('#') &&
135 !finalHref.startsWith('./') &&
136 !finalHref.startsWith('../')
137 ) {
138 finalHref = 'https://' + finalHref
139 }
140
141 props.editor
142 .chain()
143 .focus()
144 .extendMarkRange('link')
145 .setLink({
146 href: finalHref,
147 target: openInNewTab.value === true ? '_blank' : '_self',
148 })
149 .run()
150 unref(popoverRef).hide()
151 }
152
153 const unlink = () => {
154 props.editor.chain().focus().extendMarkRange('link').unsetLink().run()
155 unref(popoverRef).hide()
156 }
157
158 const onShow = () => {
159 const currentValue = props.editor.getAttributes('link')
160 href.value = currentValue.href
161 openInNewTab.value = currentValue.target === '_blank'
162 }
163
164 const onHide = () => {
165 href.value = ''
166 openInNewTab.value = false
167 linkError.value = ''
168 }
169 </script>
170
171 <style lang="scss" scoped>
172 .ivyforms-error-message {
173 color: var(--map-status-error-symbol-0);
174 font-size: 12px;
175 line-height: 1.4;
176 }
177 </style>
178