@fy-/fws-vue 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import { LinkIcon } from "@heroicons/vue/24/solid";
3
3
  import { computed, ref, toRef } from "vue";
4
4
  import type { ErrorObject } from "@vuelidate/core";
5
5
  import { useTranslation } from "../../composables/translations";
6
-
6
+ import DefaultTagInput from "./DefaultTagInput.vue";
7
7
  type modelValueType = string | number | string[] | number[] | undefined;
8
8
 
9
9
  type checkboxValueType = any[] | Set<any> | undefined | boolean;
@@ -25,6 +25,7 @@ const props = withDefaults(
25
25
  options?: string[][];
26
26
  help?: string;
27
27
  error?: string;
28
+ color?: string;
28
29
  errorVuelidate?: ErrorObject[];
29
30
  disabled?: boolean;
30
31
  }>(),
@@ -106,7 +107,7 @@ defineExpose({ focus, blur, getInputRef });
106
107
  'select',
107
108
  'phone',
108
109
  'chips',
109
- 'opts',
110
+ 'tags',
110
111
  'mask',
111
112
  ].includes(type)
112
113
  "
@@ -128,6 +129,12 @@ defineExpose({ focus, blur, getInputRef });
128
129
  "
129
130
  class="relative"
130
131
  >
132
+ <label
133
+ :for="id"
134
+ v-if="label"
135
+ class="block mb-2 text-sm font-medium text-fv-neutral-900 dark:text-white"
136
+ >{{ label }}
137
+ </label>
131
138
  <input
132
139
  ref="inputRef"
133
140
  :type="type"
@@ -136,27 +143,41 @@ defineExpose({ focus, blur, getInputRef });
136
143
  :class="{
137
144
  error: checkErrors,
138
145
  }"
146
+ v-model="model"
139
147
  :autocomplete="autocomplete"
140
148
  :disabled="disabled"
141
149
  :aria-describedby="help ? `${id}-help` : id"
142
- class="block px-2.5 pb-2.5 pt-4 w-full text-sm text-fv-neutral-900 bg-transparent rounded-lg border-1 border-fv-neutral-300 appearance-none dark:text-white dark:border-fv-neutral-600 dark:focus:border-fv-primary-500 focus:outline-none focus:ring-0 focus:border-fv-primary-600 peer"
150
+ class="bg-fv-neutral-50 border border-fv-neutral-300 text-fv-neutral-900 text-sm rounded-lg focus:ring-fv-primary-500 focus:border-fv-primary-500 block w-full p-2.5 dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-400 dark:text-white dark:focus:ring-fv-primary-500 dark:focus:border-fv-primary-500"
143
151
  :required="req"
144
152
  @focus="handleFocus"
145
153
  @blur="handleBlur"
146
154
  />
155
+ </div>
156
+ <div v-if="type == 'chips' || type == 'tags'">
147
157
  <label
148
158
  :for="id"
149
159
  v-if="label || placeholder"
150
- class="absolute text-sm text-fv-neutral-500 dark:text-fv-neutral-400 duration-300 transform -translate-y-4 scale-75 top-2 z-10 origin-[0] bg-white dark:bg-fv-neutral-900 px-2 peer-focus:px-2 peer-focus:text-fv-primary-600 peer-focus:dark:text-fv-primary-500 peer-placeholder-shown:scale-100 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:top-1/2 peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-4 rtl:peer-focus:translate-x-1/4 rtl:peer-focus:left-auto start-1"
160
+ class="block mb-2 text-sm font-medium text-fv-neutral-900 dark:text-white"
151
161
  >{{ label ? label : placeholder }}
152
162
  </label>
163
+ <!-- @vue-skip -->
164
+ <DefaultTagInput
165
+ v-model="model"
166
+ :id="id"
167
+ :disabled="disabled"
168
+ :color="color"
169
+ :error="checkErrors"
170
+ :help="help"
171
+ />
153
172
  </div>
154
173
  <div class="group relative" v-else-if="type == 'textarea'">
155
174
  <label
175
+ v-if="label"
156
176
  :for="id"
157
177
  class="block mb-2 text-sm font-medium text-fv-neutral-900 dark:text-white"
158
- >Your message</label
178
+ >{{ label }}</label
159
179
  >
180
+ <!-- @vue-skip -->
160
181
  <textarea
161
182
  :id="id"
162
183
  :name="id"
@@ -164,6 +185,7 @@ defineExpose({ focus, blur, getInputRef });
164
185
  :class="{
165
186
  error: checkErrors,
166
187
  }"
188
+ v-model="model"
167
189
  :placeholder="placeholder"
168
190
  :disabled="disabled"
169
191
  :aria-describedby="help ? `${id}-help` : id"
@@ -184,6 +206,7 @@ defineExpose({ focus, blur, getInputRef });
184
206
  :id="id"
185
207
  :name="id"
186
208
  ref="inputRef"
209
+ v-model="model"
187
210
  :disabled="disabled"
188
211
  :aria-describedby="help ? `${id}-help` : id"
189
212
  :required="req"
@@ -215,12 +238,14 @@ defineExpose({ focus, blur, getInputRef });
215
238
  <input
216
239
  type="checkbox"
217
240
  v-model="modelCheckbox"
241
+ :true-value="checkboxTrueValue"
242
+ :false-value="checkboxFalseValue"
218
243
  class="sr-only peer"
219
244
  @focus="handleFocus"
220
245
  @blur="handleBlur"
221
246
  />
222
247
  <div
223
- class="relative w-11 h-6 bg-fv-neutral-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-fv-primary-300 dark:peer-focus:ring-fv-primary-800 rounded-full peer dark:bg-fv-neutral-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-fv-neutral-300 after:border after:rounded-full after:w-5 after:h-5 after:transition-all dark:border-fv-neutral-600 peer-checked:bg-fv-primary-600"
248
+ class="relative flex-0 flex-shrink-0 w-11 h-6 bg-fv-neutral-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-fv-primary-300 dark:peer-focus:ring-fv-primary-800 rounded-full peer dark:bg-fv-neutral-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-fv-neutral-300 after:border after:rounded-full after:w-5 after:h-5 after:transition-all dark:border-fv-neutral-600 peer-checked:bg-fv-primary-600"
224
249
  ></div>
225
250
  <span
226
251
  class="ms-3 text-sm font-medium text-fv-neutral-900 dark:text-fv-neutral-300"
@@ -246,6 +271,8 @@ defineExpose({ focus, blur, getInputRef });
246
271
  :type="type"
247
272
  @focus="handleFocus"
248
273
  @blur="handleBlur"
274
+ :true-value="checkboxTrueValue"
275
+ :false-value="checkboxFalseValue"
249
276
  v-model="modelCheckbox"
250
277
  class="w-4 h-4 text-fv-primary-600 bg-fv-neutral-100 border-fv-neutral-300 rounded focus:ring-fv-primary-500 dark:focus:ring-fv-primary-600 dark:ring-offset-fv-neutral-800 dark:focus:ring-offset-fv-neutral-800 focus:ring-2 dark:bg-fv-neutral-700 dark:border-fv-neutral-600"
251
278
  />
@@ -253,13 +280,13 @@ defineExpose({ focus, blur, getInputRef });
253
280
  <div class="ms-2 text-sm">
254
281
  <label
255
282
  :for="id"
256
- class="font-medium text-gray-900 dark:text-gray-300"
283
+ class="font-medium text-fv-neutral-900 dark:text-fv-neutral-300"
257
284
  >{{ label }}</label
258
285
  >
259
286
  <p
260
287
  :id="`${id}-help`"
261
288
  v-if="help"
262
- class="text-xs font-normal text-gray-500 dark:text-gray-400"
289
+ class="text-xs font-normal text-fv-neutral-500 dark:text-fv-neutral-400"
263
290
  >
264
291
  {{ help }}
265
292
  </p>
@@ -0,0 +1,170 @@
1
+ <template>
2
+ <div
3
+ :class="`tags-input ${$props.error ? 'error' : ''}`"
4
+ @click="focusInput"
5
+ @keydown.delete.prevent="removeLastTag"
6
+ @keydown.enter.prevent="addTag"
7
+ >
8
+ <span v-for="(tag, index) in tags" :key="index" :class="`tag ${color}`">
9
+ {{ tag }}
10
+ <button type="button" @click.prevent="removeTag(index)">
11
+ <svg
12
+ class="w-4 h-4"
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ fill="none"
15
+ viewBox="0 0 24 24"
16
+ stroke-width="1.5"
17
+ stroke="currentColor"
18
+ >
19
+ <path
20
+ stroke-linecap="round"
21
+ stroke-linejoin="round"
22
+ d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
23
+ />
24
+ </svg>
25
+ </button>
26
+ </span>
27
+ <div
28
+ contenteditable
29
+ class="input"
30
+ :id="`tags_${id}`"
31
+ ref="textInput"
32
+ @input="handleInput"
33
+ @paste.prevent="handlePaste"
34
+ placeholder="Add a tag..."
35
+ ></div>
36
+ </div>
37
+ </template>
38
+
39
+ <script setup lang="ts">
40
+ import { ref, watch, onMounted } from "vue";
41
+ type colorType = "blue" | "red" | "green" | "purple" | "orange" | "neutral";
42
+
43
+ const props = withDefaults(
44
+ defineProps<{
45
+ modelValue: string[];
46
+ color?: colorType;
47
+ label?: string;
48
+ id: string;
49
+ separators?: string[];
50
+ autofocus?: boolean;
51
+ help?: string;
52
+ error?: string;
53
+ }>(),
54
+ {
55
+ color: "blue",
56
+ label: "Tags",
57
+ separators: () => [","],
58
+ autofocus: false,
59
+ },
60
+ );
61
+
62
+ const emit = defineEmits(["update:modelValue"]);
63
+ const tags = ref([...props.modelValue]);
64
+ const textInput = ref<HTMLElement>();
65
+
66
+ watch(
67
+ tags,
68
+ (newTags) => {
69
+ emit("update:modelValue", newTags);
70
+ },
71
+ { deep: true },
72
+ );
73
+
74
+ onMounted(() => {
75
+ if (props.autofocus) {
76
+ focusInput();
77
+ }
78
+ });
79
+
80
+ const handleInput = (event: any) => {
81
+ const separatorsRegex = new RegExp(props.separators.join("|"));
82
+ if (separatorsRegex.test(event.data)) {
83
+ addTag();
84
+ }
85
+ };
86
+
87
+ const addTag = () => {
88
+ if (!textInput.value) return;
89
+
90
+ const separatorsRegex = new RegExp(props.separators.join("|"));
91
+ const newTags = textInput.value.innerText
92
+ .split(separatorsRegex)
93
+ .map((tag: string) => tag.trim())
94
+ .filter((tag: string) => tag.length > 0);
95
+ tags.value.push(...newTags);
96
+ textInput.value.innerText = "";
97
+ };
98
+
99
+ const removeTag = (index: number) => {
100
+ tags.value.splice(index, 1);
101
+ focusInput();
102
+ };
103
+
104
+ const removeLastTag = () => {
105
+ if (!textInput.value) return;
106
+
107
+ if (textInput.value.innerText === "") {
108
+ tags.value.pop();
109
+ }
110
+ };
111
+
112
+ const focusInput = () => {
113
+ if (!textInput.value) return;
114
+
115
+ textInput.value.focus();
116
+ };
117
+
118
+ const handlePaste = (e: any) => {
119
+ if (!textInput.value) return;
120
+
121
+ // @ts-ignore
122
+ const text = (e.clipboardData || window.clipboardData).getData("text");
123
+ const separatorsRegex = new RegExp(props.separators.join("|"), "g");
124
+ const pasteText = text.replace(separatorsRegex, ",");
125
+ textInput.value.innerText += pasteText;
126
+ e.preventDefault();
127
+ addTag();
128
+ };
129
+ </script>
130
+
131
+ <style scoped>
132
+ .tags-input {
133
+ cursor: text;
134
+ @apply flex flex-wrap gap-2 items-center shadow-sm bg-fv-neutral-50 border border-fv-neutral-300 text-fv-neutral-900 text-sm rounded-sm focus:ring-fv-primary-500 focus:border-fv-primary-500 w-full p-2.5 dark:bg-fv-neutral-700 dark:border-fv-neutral-600 dark:placeholder-fv-neutral-400 dark:text-white dark:focus:ring-fv-primary-500 dark:focus:border-fv-primary-500;
135
+ &.error {
136
+ @apply border-red-500 dark:border-red-400 border !important;
137
+ }
138
+ }
139
+ .tag-label {
140
+ @apply block mb-2 text-sm font-medium text-fv-neutral-900 dark:text-white;
141
+ }
142
+ .tag {
143
+ @apply inline-flex gap-1 font-medium px-2.5 py-0.5 rounded text-black dark:text-white;
144
+ &.blue {
145
+ @apply bg-blue-400 dark:bg-blue-900;
146
+ }
147
+ &.red {
148
+ @apply bg-red-400 dark:bg-red-900;
149
+ }
150
+ &.green {
151
+ @apply bg-green-400 dark:bg-green-900;
152
+ }
153
+ &.purple {
154
+ @apply bg-purple-400 dark:bg-purple-900;
155
+ }
156
+ &.orange {
157
+ @apply bg-orange-400 dark:bg-orange-900;
158
+ }
159
+ &.neutral {
160
+ @apply bg-fv-neutral-400 dark:bg-fv-neutral-900;
161
+ }
162
+ }
163
+
164
+ .input {
165
+ flex-grow: 1;
166
+ min-width: 100px;
167
+ outline: none;
168
+ border: none;
169
+ }
170
+ </style>
package/index.ts CHANGED
@@ -41,6 +41,7 @@ import DefaultSidebar from "./components/ui/DefaultSidebar.vue";
41
41
  import DefaultGallery from "./components/ui/DefaultGallery.vue";
42
42
  import DefaultDropdown from "./components/ui/DefaultDropdown.vue";
43
43
  import DefaultDropdownLink from "./components/ui/DefaultDropdownLink.vue";
44
+ import DefaultTagInput from "./components/ui/DefaultTagInput.vue";
44
45
  // Components/FWS
45
46
  import UserFlow from "./components/fws/UserFlow.vue";
46
47
  import DataTable from "./components/fws/DataTable.vue";
@@ -125,6 +126,7 @@ export {
125
126
  DefaultGallery,
126
127
  DefaultDropdown,
127
128
  DefaultDropdownLink,
129
+ DefaultTagInput,
128
130
 
129
131
  // FWS
130
132
  UserFlow,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fy-/fws-vue",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "author": "Florian 'Fy' Gasquez <m@fy.to>",
5
5
  "license": "MIT",
6
6
  "repository": {