Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[69] Tag Location #70

Merged
merged 3 commits into from
Apr 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
</select>
</div>
<!-- District -->
<div class="col-lg-4 mb-3">
<div class="col-lg-6 mb-3">
<label for="districtList" class="form-label">District</label>
<select
id="districtList"
Expand All @@ -38,7 +38,7 @@
</select>
</div>
<!-- Department -->
<div class="col-lg-4 mb-3">
<div class="col-lg-6 mb-3">
<label for="departmentList" class="form-label">Department</label>
<select
id="departmentList"
Expand All @@ -50,13 +50,13 @@
</select>
</div>
<!-- Location -->
<div class="col-lg-4">
<Input
label="Location"
type="text"
id="locationTag"
placeholder="Add Location Tag"

<div class="col-lg-12 col-md-12">
<UITagInput
v-model="location"
label="Location"
customDelimiter=","
placeholder="Enter Location Tag"
/>
</div>
</div>
Expand All @@ -66,8 +66,8 @@
<script setup>
import { ref, inject } from "vue";

import Input from "../BIOMD-UI/UI-Input.vue";
import Section from "../BIOMD-UI/UI-Section.vue";
import UITagInput from "../BIOMD-UI/UI-TagInput.vue";

const facilityId = inject("facilityId");
const departmentId = inject("departmentId");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,14 @@
v-model="facilityInfo.zipCode"
/>
</div>

<!-- Department List -->
<div class="col-12">
<Input
label="Department"
type="text"
id="departmentTag"
placeholder="Add Department Tag"
<UITagInput
v-model="facilityInfo.departments"
label="Department"
customDelimiter=","
placeholder="Enter Department Tag"
showCount="true"
/>
</div>
</div>
Expand All @@ -127,12 +126,15 @@ import { ref, inject, onMounted } from "vue";
import { useStore } from "vuex";
import Input from "../BIOMD-UI/UI-Input.vue";
import Section from "../BIOMD-UI/UI-Section.vue";
import UITagInput from "../BIOMD-UI/UI-TagInput.vue";

const store = useStore();
const facilityInfo = inject("facilityInfo");
const Global_Facility_Definition = inject("Global_Facility_Definition");
const countryList = ref(null);
const stateList = ref(null);
const districtList = ref(null);

const sendSocketReq = (request) => {
store.dispatch("sendSocketReq", request);
};
Expand Down
306 changes: 306 additions & 0 deletions src/client/web/src/components/BIOMD/BIOMD/BIOMD-UI/UI-TagInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
// ui tag reference from https://github.com/mayank1513/tag-input

<template>
<label :for="id" v-if="label" class="form-label">{{ label }}</label>
<div
class="tag-input"
:class="{ 'with-count': showCount, duplicate: noMatchingTag }"
>
<input
v-model="newTag"
type="text"
:list="id"
autocomplete="off"
@keydown.prevent.enter="addTag(newTag)"
@keydown.prevent.tab="addTag(newTag)"
@keydown.delete="newTag.length || removeTag(tags.length - 1)"
@input="addTagIfDelem(newTag)"
:style="{ 'padding-left': `${paddingLeft}px` }"
:placeholder="placeholder"
/>

<datalist v-if="options" :id="id">
<option v-for="option in availableOptions" :key="option" :value="option">
{{ option }}
</option>
</datalist>

<ul class="tags" ref="tagsUl">
<li
v-for="(tag, index) in tags"
:key="tag"
:class="{
duplicate: tag === duplicate,
tag: tagsClass.length == 0,
[tagsClass]: true,
}"
>
<span class="badge bg-secondary rounded-pill">
{{ tag }}
<button
type="button"
class="btn-close btn-close-white ms-2"
@click="removeTag(index)"
aria-label="Close"
></button>
</span>
</li>
</ul>
<div v-if="showCount" class="count">
<span>{{ tags.length }}</span> tags
</div>
</div>
<small v-show="noMatchingTag" class="err">Custom tags not allowed</small>
</template>

<script>
import { ref, watch, nextTick, onMounted, computed } from "vue";

export default {
name: "TagInput",
props: {
name: { type: String, default: "" },
label: { type: String, default: "" },
placeholder: { type: String, default: "Enter a tag" },
modelValue: { type: Array, default: () => [] },
options: { type: Array, default: () => [] },
allowCustom: { type: Boolean, default: true },
showCount: { type: Boolean, default: false },
tagTextColor: { type: String, default: "white" },
tagBgColor: { type: String, default: "rgb(250, 104, 104)" },
tagClass: { type: String, default: "" },
customDelimiter: {
type: [String, Array],
default: () => [],
validator: (val) => {
if (typeof val == "string") return val.length == 1;
for (let i = 0; i < val.length; i++) {
if (typeof val[i] != "string" || val[i].length != 1) return false;
}
return true;
},
},
},
setup(props, { emit }) {
// Tags
const tags = ref(props.modelValue);
const tagsClass = ref(props.tagClass);
const newTag = ref("");
const id = Math.random().toString(36).substring(7);
const customDelimiter = [
...new Set(
(typeof props.customDelimiter == "string"
? [props.customDelimiter]
: props.customDelimiter
).filter((it) => it.length == 1)
),
];

// handling duplicates
const duplicate = ref(null);
const handleDuplicate = (tag) => {
duplicate.value = tag;
setTimeout(() => (duplicate.value = null), 1000);
newTag.value = "";
};
const noMatchingTag = ref(false);
function handleNoMatchingTag() {
noMatchingTag.value = true;
setTimeout(() => (noMatchingTag.value = false), 500);
let v = newTag.value;
if (customDelimiter.includes(v.charAt(v.length - 1)))
newTag.value = v.substr(0, v.length - 1);
}
const addTag = (tag) => {
tag = tag.trim();
if (!tag) return; // prevent empty tag
// only allow predefined tags when allowCustom is false
if (!props.allowCustom && !props.options.includes(tag)) {
// display not a valid tag
handleNoMatchingTag();
return;
}
// return early if duplicate
if (tags.value.includes(tag)) {
handleDuplicate(tag);
return;
}
tags.value.push(tag);
newTag.value = ""; // reset newTag
};
const addTagIfDelem = (tag) => {
if (!customDelimiter || customDelimiter.length == 0) return;
if (customDelimiter.includes(tag.charAt(tag.length - 1)))
addTag(tag.substr(0, tag.length - 1));
};
const removeTag = (index) => {
tags.value.splice(index, 1);
};

// positioning and handling tag change
const paddingLeft = ref(10);
const tagsUl = ref(null);
const onTagsChange = () => {
// position cursor
const extraCushion = 15;
// tagsUl.value.style.setProperty("--tagBgColor", props.tagBgColor);
// tagsUl.value.style.setProperty("--tagTextColor", props.tagTextColor);
paddingLeft.value = tagsUl.value.clientWidth + extraCushion;
// scroll to end of tags
tagsUl.value.scrollTo(tagsUl.value.scrollWidth, 0);
// emit value on tags change
emit("update:modelValue", tags.value);
};
watch(tags, () => nextTick(onTagsChange), { deep: true });
onMounted(onTagsChange);

// options
const availableOptions = computed(() => {
if (!props.options) return false;
return props.options.filter((option) => !tags.value.includes(option));
});

return {
tags,
tagsClass,
newTag,
addTag,
addTagIfDelem,
removeTag,
paddingLeft,
tagsUl,
availableOptions,
id,
duplicate,
noMatchingTag,
};
},
};
</script>

<style scoped>
* {
box-sizing: border-box;
}

.tag-input {
position: relative;
}

ul {
list-style: none;
display: flex;
align-items: center;
gap: 7px;
margin: 0;
padding: 0;
position: absolute;
top: 0;
bottom: 0;
left: 10px;
max-width: 100%;
overflow-x: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
}

ul::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}

/* .tag {
background: rgb(94, 94, 94);
padding: 5px;
border-radius: 15px;
color: var(--tagTextColor);
white-space: nowrap;
transition: 0.1s ease background;
align-items: center;
} */

.btn-close {
box-shadow: none;
}

.tag.duplicate {
animation: shake 1s;
}

.duplicate input {
outline: rgb(40, 182, 229);
border: 1px solid rgb(235, 27, 27);
animation: shake1 0.5s;
}

input {
width: 100%;
padding: 10px;
}

.count {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 10px;
display: block;
font-size: 0.8rem;
white-space: nowrap;
}

.count span {
background: #eee;
padding: 2px;
border-radius: 2px;
}

.with-count input {
padding-right: 60px;
}

.with-count ul {
max-width: 60%;
}

.err {
color: red;
}

@keyframes shake {
10%,
90% {
transform: scale(0.9) translate3d(-1px, 0, 0);
}
20%,
80% {
transform: scale(0.9) translate3d(2px, 0, 0);
}
30%,
50%,
70% {
transform: scale(0.9) translate3d(-4px, 0, 0);
}
40%,
60% {
transform: scale(0.9) translate3d(4px, 0, 0);
}
}
@keyframes shake1 {
10%,
90% {
transform: scale(0.99) translate3d(-1px, 0, 0);
}
20%,
80% {
transform: scale(0.98) translate3d(2px, 0, 0);
}
30%,
50%,
70% {
transform: scale(1) translate3d(-4px, 0, 0);
}
40%,
60% {
transform: scale(0.98) translate3d(4px, 0, 0);
}
}
</style>