diff --git a/src/components/v-input/v-input.vue b/src/components/v-input/v-input.vue
index d020bcb20e..44a88a6a34 100644
--- a/src/components/v-input/v-input.vue
+++ b/src/components/v-input/v-input.vue
@@ -122,6 +122,7 @@ export default defineComponent({
const _listeners = computed(() => ({
...listeners,
input: emitValue,
+ keydown: processValue,
}));
const hasClick = computed(() => {
@@ -130,11 +131,48 @@ export default defineComponent({
return { _listeners, hasClick, stepUp, stepDown, input };
+ function processValue(event: KeyboardEvent) {
+ const key = event.key.toLowerCase();
+ const systemKeys = ['meta', 'shift', 'alt', 'backspace'];
+ const value = (event.target as HTMLInputElement).value;
+
+ if (props.slug === true) {
+ const slugSafeCharacters = 'abcdefghijklmnopqrstuvwxyz01234567890-_~ '.split('');
+
+ const isAllowed = slugSafeCharacters.includes(key) || systemKeys.includes(key);
+
+ if (isAllowed === false) {
+ event.preventDefault();
+ }
+
+ if (key === ' ' && value.endsWith(props.slugSeparator)) {
+ event.preventDefault();
+ }
+ }
+
+ if (props.slug === true) {
+ const dbSafeCharacters = 'abcdefghijklmnopqrstuvwxyz01234567890-_~ '.split('');
+
+ const isAllowed = dbSafeCharacters.includes(key) || systemKeys.includes(key);
+
+ if (isAllowed === false) {
+ event.preventDefault();
+ }
+
+ // Prevent leading number
+ if (value.length === 0 && '0123456789'.split('').includes(key)) {
+ event.preventDefault();
+ }
+ }
+ }
+
function emitValue(event: InputEvent) {
let value = (event.target as HTMLInputElement).value;
if (props.slug === true) {
+ const endsWithSpace = value.endsWith(' ');
value = slugify(value, { separator: props.slugSeparator });
+ if (endsWithSpace) value += props.slugSeparator;
}
if (props.dbSafe === true) {
diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts
index ac1330544e..9d83fa4057 100644
--- a/src/interfaces/index.ts
+++ b/src/interfaces/index.ts
@@ -16,6 +16,7 @@ import InterfaceIcon from './icon';
import InterfaceManyToOne from './many-to-one';
import InterfaceOneToMany from './one-to-many';
import InterfaceHash from './hash';
+import InterfaceSlug from './slug';
export const interfaces = [
InterfaceTextInput,
@@ -36,6 +37,7 @@ export const interfaces = [
InterfaceManyToOne,
InterfaceOneToMany,
InterfaceHash,
+ InterfaceSlug,
];
export default interfaces;
diff --git a/src/interfaces/slug/index.ts b/src/interfaces/slug/index.ts
new file mode 100644
index 0000000000..5456924470
--- /dev/null
+++ b/src/interfaces/slug/index.ts
@@ -0,0 +1,10 @@
+import { defineInterface } from '@/interfaces/define';
+import InterfaceSlug from './slug.vue';
+
+export default defineInterface(({ i18n }) => ({
+ id: 'slug',
+ name: i18n.t('slug'),
+ icon: 'link',
+ component: InterfaceSlug,
+ options: [],
+}));
diff --git a/src/interfaces/slug/slug.vue b/src/interfaces/slug/slug.vue
new file mode 100644
index 0000000000..6d10c77b90
--- /dev/null
+++ b/src/interfaces/slug/slug.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+