Skip to content

Commit 669f351

Browse files
authored
2024.9.x feature link input field (#2)
Created Universal Link Input Field type
1 parent eec3039 commit 669f351

11 files changed

Lines changed: 581 additions & 1 deletion

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
*.sw*
1010
.run
1111
node_modules
12-
ignore
12+
ignore
13+
.kilo*
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<template>
2+
<div>
3+
<b-form-checkbox
4+
v-model="f.options.dynamicMode"
5+
v-b-tooltip.noninteractive.hover="{ title: $t('kind.link.dynamicModeTooltip'), boundary: 'viewport' }"
6+
>
7+
{{ $t('kind.link.dynamicMode') }}
8+
</b-form-checkbox>
9+
10+
<div
11+
v-if="!f.options.dynamicMode"
12+
class="ml-4 mb-3"
13+
>
14+
<div>
15+
<label>{{ $t('kind.link.protocol') }}</label>
16+
<b-form-select
17+
v-model="f.options.tempProtocol"
18+
:options="protocolOptions"
19+
@change="onProtocolChange"
20+
/>
21+
</div>
22+
23+
<div
24+
v-if="f.options.tempProtocol === '__CUSTOM__'"
25+
class="mt-2"
26+
>
27+
<div>
28+
<label>{{ $t('kind.link.customProtocol') }}</label>
29+
<b-form-input
30+
v-model="f.options.customProtocol"
31+
placeholder="e.g., skype:"
32+
/>
33+
</div>
34+
</div>
35+
</div>
36+
37+
<div v-if="showURLOptions">
38+
<div>
39+
<b-form-checkbox v-model="f.options.trimPath">
40+
{{ $t('kind.url.label') }}
41+
</b-form-checkbox>
42+
</div>
43+
<div>
44+
<b-form-checkbox v-model="f.options.trimFragment">
45+
{{ $t('kind.url.trimHash') }}
46+
</b-form-checkbox>
47+
</div>
48+
<div>
49+
<b-form-checkbox v-model="f.options.trimQuery">
50+
{{ $t('kind.url.trimQuestionMark') }}
51+
</b-form-checkbox>
52+
</div>
53+
<div>
54+
<b-form-checkbox v-model="f.options.onlySecure">
55+
{{ $t('kind.url.sshOnly') }}
56+
</b-form-checkbox>
57+
</div>
58+
</div>
59+
60+
<div>
61+
<b-form-checkbox v-model="f.options.outputPlain">
62+
{{ $t('kind.link.preventToLink') }}
63+
</b-form-checkbox>
64+
</div>
65+
</div>
66+
</template>
67+
68+
<script>
69+
import base from './base'
70+
71+
export default {
72+
i18nOptions: {
73+
namespaces: 'field',
74+
},
75+
76+
extends: base,
77+
78+
data () {
79+
return {
80+
tempProtocol: '',
81+
}
82+
},
83+
84+
computed: {
85+
isStaticURL () {
86+
return !this.f.options.dynamicMode && this.f.options.tempProtocol === 'https://'
87+
},
88+
89+
showURLOptions () {
90+
return this.f.options.dynamicMode || this.isStaticURL
91+
},
92+
93+
protocolOptions () {
94+
return [
95+
{ text: this.$t('kind.link.protocolOptions.none'), value: '' },
96+
{ text: this.$t('kind.link.protocolOptions.mailto'), value: 'mailto:' },
97+
{ text: this.$t('kind.link.protocolOptions.tel'), value: 'tel:' },
98+
{ text: this.$t('kind.link.protocolOptions.url'), value: 'https://' },
99+
{ text: this.$t('kind.link.protocolOptions.skype'), value: 'skype:' },
100+
{ text: this.$t('kind.link.protocolOptions.msteams'), value: 'msteams:' },
101+
{ text: this.$t('kind.link.protocolOptions.slack'), value: 'slack://' },
102+
{ text: this.$t('kind.link.protocolOptions.sms'), value: 'sms:' },
103+
{ text: this.$t('kind.link.protocolOptions.facetime'), value: 'facetime:' },
104+
{ text: this.$t('kind.link.protocolOptions.zoommtg'), value: 'zoommtg://' },
105+
{ text: this.$t('kind.link.protocolOptions.whatsapp'), value: 'whatsapp://' },
106+
{ text: this.$t('kind.link.protocolOptions.signal'), value: 'sgnl:' },
107+
{ text: this.$t('kind.link.protocolOptions.fb'), value: 'fb://' },
108+
{ text: this.$t('kind.link.protocolOptions.fbmessenger'), value: 'fb-messenger://' },
109+
{ text: this.$t('kind.link.protocolOptions.instagram'), value: 'instagram://' },
110+
{ text: this.$t('kind.link.protocolOptions.twitter'), value: 'twitter://' },
111+
{ text: this.$t('kind.link.protocolOptions.youtube'), value: 'youtube://' },
112+
{ text: this.$t('kind.link.protocolOptions.spotify'), value: 'spotify://' },
113+
{ text: this.$t('kind.link.protocolOptions.tiktok'), value: 'tiktok://' },
114+
{ text: this.$t('kind.link.protocolOptions.discord'), value: 'discord://' },
115+
{ text: this.$t('kind.link.protocolOptions.custom'), value: '__CUSTOM__' },
116+
]
117+
},
118+
},
119+
120+
watch: {
121+
'f.options.customProtocol': {
122+
handler (val) {
123+
if (val && val !== '__CUSTOM__') {
124+
this.tempProtocol = val
125+
}
126+
},
127+
},
128+
129+
'f.options.dynamicMode': {
130+
handler (val) {
131+
if (!val && this.f.options.customProtocol) {
132+
this.tempProtocol = this.f.options.customProtocol
133+
}
134+
},
135+
},
136+
},
137+
138+
mounted () {
139+
if (!this.f.options.tempProtocol) {
140+
if (this.f.options.customProtocol) {
141+
this.f.options.tempProtocol = this.f.options.customProtocol
142+
} else {
143+
this.f.options.tempProtocol = ''
144+
}
145+
} else {
146+
this.tempProtocol = this.f.options.tempProtocol
147+
}
148+
},
149+
150+
methods: {
151+
onProtocolChange () {
152+
if (this.tempProtocol !== '__CUSTOM__') {
153+
this.$set(this.field.options, 'customProtocol', this.tempProtocol)
154+
}
155+
},
156+
},
157+
}
158+
</script>

client/web/compose/src/components/ModuleFields/Configurator/loader.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export { default as Email } from './Email'
99
export { default as User } from './User'
1010
export { default as File } from './File'
1111
export { default as Geometry } from './Geometry'
12+
export { default as Link } from './Link'
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<template>
2+
<b-form-group
3+
:label-cols-md="horizontal && '5'"
4+
:label-cols-xl="horizontal && '4'"
5+
:content-cols-md="horizontal && '7'"
6+
:content-cols-xl="horizontal && '8'"
7+
:class="formGroupStyleClasses"
8+
>
9+
<template
10+
#label
11+
>
12+
<div
13+
v-if="!valueOnly"
14+
class="d-flex align-items-center text-primary px-0"
15+
>
16+
<span
17+
:title="label"
18+
class="d-inline-block mw-100"
19+
>
20+
{{ label }}
21+
</span>
22+
23+
<c-hint :tooltip="hint" />
24+
25+
<slot name="tools" />
26+
</div>
27+
<div
28+
class="small text-muted"
29+
:class="{ 'mb-1': description }"
30+
>
31+
{{ description }}
32+
</div>
33+
</template>
34+
35+
<multi
36+
v-if="field.isMulti"
37+
v-slot="ctx"
38+
:value.sync="value"
39+
:errors="errors"
40+
>
41+
<b-form-input
42+
:value="value[ctx.index]"
43+
:placeholder="placeholder"
44+
:formatter="formatValue"
45+
lazy-formatter
46+
@input="setMultiValue($event, ctx.index)"
47+
/>
48+
</multi>
49+
50+
<template
51+
v-else
52+
>
53+
<b-form-input
54+
v-model="value"
55+
:placeholder="placeholder"
56+
:formatter="formatValue"
57+
lazy-formatter
58+
/>
59+
<errors :errors="errors" />
60+
</template>
61+
</b-form-group>
62+
</template>
63+
64+
<script>
65+
import base from './base'
66+
import { trimUrlFragment, trimUrlQuery, trimUrlPath, onlySecureUrl } from '../url'
67+
68+
export default {
69+
i18nOptions: {
70+
namespaces: 'field',
71+
},
72+
73+
extends: base,
74+
75+
computed: {
76+
placeholder () {
77+
return this.$t('kind.link.example')
78+
},
79+
80+
effectiveType () {
81+
if (this.field.options.dynamicMode) {
82+
return this.detectType(this.value)
83+
} else {
84+
const protocol = this.field.options.tempProtocol === '__CUSTOM__'
85+
? this.field.options.customProtocol
86+
: this.field.options.tempProtocol
87+
return this.getTypeFromProtocol(protocol)
88+
}
89+
},
90+
},
91+
92+
methods: {
93+
detectType (input) {
94+
if (!input) return 'url'
95+
96+
const trimmed = input.trim()
97+
98+
if (trimmed.includes('@')) {
99+
return 'email'
100+
}
101+
102+
if (trimmed.startsWith('+') || /^[\d\s\-()]+$/.test(trimmed)) {
103+
return 'phone'
104+
}
105+
106+
return 'url'
107+
},
108+
109+
getTypeFromProtocol (protocol) {
110+
if (!protocol) return 'url'
111+
112+
const protocolToType = {
113+
'mailto:': 'email',
114+
'tel:': 'phone',
115+
'sms:': 'phone',
116+
'http://': 'url',
117+
'https://': 'url',
118+
}
119+
120+
const type = protocolToType[protocol]
121+
122+
if (type) {
123+
return type
124+
}
125+
126+
if (this.field.options.customProtocol && protocol.startsWith(this.field.options.customProtocol)) {
127+
return 'custom'
128+
}
129+
130+
return 'app'
131+
},
132+
133+
formatValue (value) {
134+
if (!value) return value
135+
136+
const type = this.effectiveType
137+
let formatted = value
138+
139+
switch (type) {
140+
case 'email':
141+
formatted = formatted.toLowerCase()
142+
break
143+
case 'phone':
144+
formatted = formatted.replace(/[^\d+]/g, '')
145+
break
146+
case 'url':
147+
if (this.field.options.trimFragment) {
148+
formatted = trimUrlFragment(formatted)
149+
}
150+
if (this.field.options.trimQuery) {
151+
formatted = trimUrlQuery(formatted)
152+
}
153+
if (this.field.options.trimPath) {
154+
formatted = trimUrlPath(formatted)
155+
}
156+
if (this.field.options.onlySecure) {
157+
formatted = onlySecureUrl(formatted)
158+
}
159+
break
160+
case 'app':
161+
break
162+
case 'custom':
163+
if (this.field.options.tempProtocol === '__CUSTOM__') {
164+
if (this.field.options.customProtocol && !formatted.startsWith(this.field.options.customProtocol)) {
165+
formatted = this.field.options.customProtocol + formatted
166+
}
167+
} else if (this.field.options.tempProtocol && this.field.options.tempProtocol !== '__CUSTOM__') {
168+
if (!formatted.startsWith(this.field.options.tempProtocol)) {
169+
formatted = this.field.options.tempProtocol + formatted
170+
}
171+
}
172+
break
173+
}
174+
175+
return formatted
176+
},
177+
},
178+
}
179+
</script>

client/web/compose/src/components/ModuleFields/Editor/loader.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export { default as User } from './User'
99
export { default as Record } from './Record'
1010
export { default as File } from './File'
1111
export { default as Geometry } from './Geometry'
12+
export { default as Link } from './Link'

0 commit comments

Comments
 (0)