init
This commit is contained in:
144
src/component/tagInput/index.less
Normal file
144
src/component/tagInput/index.less
Normal file
@@ -0,0 +1,144 @@
|
||||
@import "../tag/index.less";
|
||||
@import "../popper/index.less";
|
||||
@import "../tooltip/index.less";
|
||||
|
||||
@tag-input-lg: 44px;
|
||||
@tag-input-md: 38px;
|
||||
@tag-input-sm: 32px;
|
||||
@tag-input-xs: 26px;
|
||||
@tag-input-boeder: 1px;
|
||||
@tag-input-inner-padding-lg: 2px;
|
||||
@tag-input-inner-padding-md: 2px;
|
||||
@tag-input-inner-padding-sm: 1px;
|
||||
@tag-input-inner-padding-xs: 1px;
|
||||
@tag-margin-top-lg: 2px;
|
||||
@tag-margin-top-md: 2px;
|
||||
@tag-margin-top-sm: 1px;
|
||||
@tag-margin-top-xs: 1px;
|
||||
@tag-margin-bottom-lg: 2px;
|
||||
@tag-margin-bottom-md: 2px;
|
||||
@tag-margin-bottom-sm: 1px;
|
||||
@tag-margin-bottom-xs: 1px;
|
||||
|
||||
.layui-tag-input {
|
||||
display: inline-flex;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
border-width: @tag-input-boeder;
|
||||
border-style: solid;
|
||||
border-color: var(--input-border-color);
|
||||
border-radius: var(--input-border-radius);
|
||||
cursor: text;
|
||||
|
||||
&-inner {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
line-height: 0;
|
||||
padding: @tag-input-inner-padding-md 0;
|
||||
}
|
||||
|
||||
&-mirror {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
white-space: pre;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&-clear {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
cursor: pointer;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&-clear:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
& &-inner-input {
|
||||
box-sizing: border-box;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&-disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&-disabled * {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.layui-tag {
|
||||
margin-right: 5px;
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
&-collapsed-panel {
|
||||
white-space: normal;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
width: fit-content;
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
|
||||
.layui-tag {
|
||||
margin-right: 5px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.set-size(@size) {
|
||||
@height: ~'tag-input-@{size}';
|
||||
@tag-margin-top: ~'tag-margin-top-@{size}';
|
||||
@tag-margin-bottom: ~'tag-margin-bottom-@{size}';
|
||||
@inner-padding: ~'tag-input-inner-padding-@{size}';
|
||||
|
||||
&.layui-tag-input-@{size} {
|
||||
min-height: @@height;
|
||||
|
||||
.layui-tag-input-inner-input {
|
||||
height: @@height - (@@inner-padding + @tag-input-boeder)* 2;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.layui-tag-input-inner {
|
||||
padding: @@inner-padding 5px;
|
||||
}
|
||||
|
||||
.layui-tag {
|
||||
margin-top: @@tag-margin-top;
|
||||
margin-bottom: @@tag-margin-bottom;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.set-size(lg);
|
||||
.set-size(md);
|
||||
.set-size(sm);
|
||||
.set-size(xs);
|
||||
}
|
||||
|
||||
.layui-tag-input-suffix {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.layui-tag-input:not(.layui-tag-input-disabled):hover,
|
||||
.layui-tag-input:not(.layui-tag-input-disabled):focus-within {
|
||||
border-color: #d2d2d2!important;
|
||||
.layui-tag-input-clear{
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
5
src/component/tagInput/index.ts
Normal file
5
src/component/tagInput/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { withInstall, WithInstallType } from "../../utils";
|
||||
import Component from "./index.vue";
|
||||
|
||||
const component: WithInstallType<typeof Component> = withInstall(Component);
|
||||
export default component;
|
||||
332
src/component/tagInput/index.vue
Normal file
332
src/component/tagInput/index.vue
Normal file
@@ -0,0 +1,332 @@
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "LayTagInput",
|
||||
};
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import "./index.less";
|
||||
import LayTag, { TagProps } from "../tag/index.vue";
|
||||
import LayToopTip from "../tooltip/index.vue";
|
||||
import {
|
||||
onMounted,
|
||||
shallowRef,
|
||||
ref,
|
||||
watch,
|
||||
computed,
|
||||
reactive,
|
||||
nextTick,
|
||||
} from "vue";
|
||||
import { isObject, reactiveOmit, useResizeObserver } from "@vueuse/core";
|
||||
import { LayIcon } from "@layui/icons-vue";
|
||||
import { TagInputSize } from "./inerface";
|
||||
|
||||
export interface TagData {
|
||||
value?: string | number;
|
||||
label?: string;
|
||||
closable?: boolean;
|
||||
[other: string]: any;
|
||||
}
|
||||
|
||||
export interface TagInputProps {
|
||||
modelValue?: (string | number | TagData)[];
|
||||
inputValue?: string;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
readonly?: boolean;
|
||||
allowClear?: boolean;
|
||||
max?: number;
|
||||
minCollapsedNum?: number;
|
||||
collapseTagsTooltip?: boolean;
|
||||
size?: TagInputSize;
|
||||
tagProps?: TagProps;
|
||||
disabledInput?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<TagInputProps>(), {
|
||||
placeholder: undefined,
|
||||
minCollapsedNum: 0,
|
||||
size: "md",
|
||||
});
|
||||
|
||||
const emit = defineEmits([
|
||||
"update:modelValue",
|
||||
"update:inputValue",
|
||||
"change",
|
||||
"inputValueChange",
|
||||
"remove",
|
||||
"clear",
|
||||
"focus",
|
||||
"blur",
|
||||
"pressEnter",
|
||||
]);
|
||||
|
||||
const mirrorRefEl = shallowRef<HTMLElement | undefined>(undefined);
|
||||
const inputRefEl = shallowRef<HTMLInputElement | undefined>(undefined);
|
||||
const oldInputValue = ref<string>("");
|
||||
const compositionValue = ref<string>("");
|
||||
const isComposing = ref(false);
|
||||
const inputStyle = reactive({ width: "15px" });
|
||||
const _tagProps = reactive(props.tagProps ?? {});
|
||||
const tagProps = reactiveOmit(_tagProps, "closable", "size", "disabled");
|
||||
const inputValue = computed({
|
||||
get() {
|
||||
return props.inputValue;
|
||||
},
|
||||
set(val) {
|
||||
emit("update:inputValue", val);
|
||||
emit("inputValueChange", val);
|
||||
},
|
||||
});
|
||||
|
||||
const tagData = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
},
|
||||
set(val) {
|
||||
emit("update:modelValue", val);
|
||||
emit("change", val);
|
||||
},
|
||||
});
|
||||
|
||||
const normalizedTags = computed(() => normalizedTagData(tagData.value ?? []));
|
||||
|
||||
const computedTagData = computed(() => {
|
||||
if (!normalizedTags.value) return;
|
||||
return props.minCollapsedNum
|
||||
? normalizedTags.value?.slice(0, props.minCollapsedNum)
|
||||
: normalizedTags.value;
|
||||
});
|
||||
|
||||
const collapsedTagData = computed(() => {
|
||||
if (!normalizedTags.value) return;
|
||||
return props.minCollapsedNum &&
|
||||
normalizedTags.value?.length > props.minCollapsedNum
|
||||
? normalizedTags.value?.slice(props.minCollapsedNum)
|
||||
: [];
|
||||
});
|
||||
|
||||
const handleInput = function (e: Event) {
|
||||
if (isComposing.value) {
|
||||
return;
|
||||
}
|
||||
inputValue.value = (e.target as HTMLInputElement).value;
|
||||
};
|
||||
|
||||
const handleComposition = (e: CompositionEvent) => {
|
||||
if (e.type === "compositionend") {
|
||||
isComposing.value = false;
|
||||
compositionValue.value = "";
|
||||
handleInput(e);
|
||||
} else {
|
||||
isComposing.value = true;
|
||||
compositionValue.value = inputValue.value + (e.data ?? "");
|
||||
}
|
||||
};
|
||||
|
||||
const handleEnter = (e: KeyboardEvent) => {
|
||||
e.preventDefault();
|
||||
const valueStr = inputValue.value ? String(inputValue.value).trim() : "";
|
||||
if (!valueStr || !tagData.value) return;
|
||||
const isLimit = props.max && tagData.value?.length >= props.max;
|
||||
if (!isLimit) {
|
||||
tagData.value =
|
||||
tagData.value instanceof Array
|
||||
? tagData.value.concat(String(valueStr))
|
||||
: [valueStr];
|
||||
inputValue.value = "";
|
||||
}
|
||||
emit("pressEnter", inputValue.value, e);
|
||||
};
|
||||
|
||||
const handleBackspaceKeyUp = (e: KeyboardEvent) => {
|
||||
if (!tagData.value || !tagData.value.length) return;
|
||||
if (!oldInputValue.value && ["Backspace", "Delete"].includes(e.code)) {
|
||||
const lastIndex = normalizedTags.value.length - 1;
|
||||
handleClose(normalizedTags.value[lastIndex].value, lastIndex, e);
|
||||
}
|
||||
oldInputValue.value = inputValue.value ?? "";
|
||||
};
|
||||
|
||||
const handleFocus = (e: FocusEvent) => {
|
||||
emit("focus", e);
|
||||
(inputRefEl.value as HTMLInputElement)?.focus();
|
||||
};
|
||||
|
||||
const handleBlur = (e: FocusEvent) => {
|
||||
emit("blur", e);
|
||||
(inputRefEl.value as HTMLInputElement)?.blur();
|
||||
};
|
||||
|
||||
const handleClearClick = (e: MouseEvent) => {
|
||||
if (props.disabled || props.readonly || !props.allowClear) {
|
||||
return;
|
||||
}
|
||||
tagData.value = [];
|
||||
emit("clear", e);
|
||||
};
|
||||
|
||||
const handleClose = (
|
||||
value: string | number | undefined,
|
||||
index: number,
|
||||
e: Event
|
||||
) => {
|
||||
if (!tagData.value) return;
|
||||
const arr = [...tagData.value];
|
||||
arr.splice(index, 1);
|
||||
tagData.value = arr;
|
||||
emit("remove", value, e);
|
||||
};
|
||||
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
if (inputRefEl.value) {
|
||||
e.preventDefault();
|
||||
inputRefEl.value.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const setInputWidth = (width: number) => {
|
||||
if (width > 15) {
|
||||
inputStyle.width = `${width}px`;
|
||||
} else {
|
||||
inputStyle.width = "15px";
|
||||
}
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
if (mirrorRefEl.value) {
|
||||
setInputWidth(mirrorRefEl.value.offsetWidth);
|
||||
}
|
||||
};
|
||||
|
||||
const cls = computed(() => [
|
||||
`layui-tag-input`,
|
||||
`layui-tag-input-${props.size}`,
|
||||
{
|
||||
"layui-tag-input-disabled": props.disabled,
|
||||
},
|
||||
]);
|
||||
|
||||
const normalizedTagData = (value: Array<string | number | TagData>) =>
|
||||
value.map((item) => {
|
||||
if (isObject(item)) return item;
|
||||
return {
|
||||
value: item,
|
||||
label: String(item),
|
||||
closable: true,
|
||||
};
|
||||
});
|
||||
|
||||
useResizeObserver(mirrorRefEl, () => {
|
||||
handleResize();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => inputValue.value,
|
||||
(val) => {
|
||||
if (inputRefEl.value && !isComposing.value) {
|
||||
nextTick(() => {
|
||||
inputRefEl.value!.value = val ?? "";
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const moreCount = computed(() => {
|
||||
if (tagData.value && computedTagData.value) {
|
||||
return tagData.value.length - computedTagData.value.length;
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
handleResize();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
focus: handleFocus,
|
||||
blur: handleBlur,
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div :class="cls" @mousedown="handleMouseDown">
|
||||
<span ref="mirrorRefEl" class="layui-tag-input-mirror">
|
||||
{{ compositionValue || inputValue || placeholder }}
|
||||
</span>
|
||||
<span v-if="$slots.prefix">
|
||||
<slot name="prefix"></slot>
|
||||
</span>
|
||||
<span class="layui-tag-input-inner">
|
||||
<template
|
||||
v-for="(item, index) of computedTagData"
|
||||
:key="`${item}-${index}`"
|
||||
>
|
||||
<LayTag
|
||||
v-bind="tagProps"
|
||||
:closable="!readonly && !disabled && item.closable"
|
||||
:size="size"
|
||||
@close="handleClose(item.value, index, $event)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</LayTag>
|
||||
</template>
|
||||
<template v-if="computedTagData?.length != tagData?.length">
|
||||
<LayToopTip
|
||||
:isDark="false"
|
||||
trigger="hover"
|
||||
popperStyle="padding:6px"
|
||||
:disabled="!collapseTagsTooltip"
|
||||
>
|
||||
<LayTag v-bind="tagProps" key="more" :closable="false" :size="size">
|
||||
+{{ moreCount }}...
|
||||
</LayTag>
|
||||
<template #content>
|
||||
<div class="layui-tag-input-collapsed-panel">
|
||||
<LayTag
|
||||
v-for="(item, index) of collapsedTagData"
|
||||
:key="`${item}-${index}`"
|
||||
v-bind="tagProps"
|
||||
:closable="!readonly && !disabled && item.closable"
|
||||
:size="size"
|
||||
@close="
|
||||
handleClose(
|
||||
item.value,
|
||||
index + (minCollapsedNum ?? 0),
|
||||
$event
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ item.label }}
|
||||
</LayTag>
|
||||
</div>
|
||||
</template>
|
||||
</LayToopTip>
|
||||
</template>
|
||||
<template v-if="!disabledInput">
|
||||
<input
|
||||
ref="inputRefEl"
|
||||
class="layui-tag-input-inner-input"
|
||||
:style="inputStyle"
|
||||
:disabled="disabled"
|
||||
:placeholder="placeholder"
|
||||
:readonly="readonly"
|
||||
@keydown.enter="handleEnter"
|
||||
@keyup="handleBackspaceKeyUp"
|
||||
@input="handleInput"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@compositionstart="handleComposition"
|
||||
@compositionupdate="handleComposition"
|
||||
@compositionend="handleComposition"
|
||||
/>
|
||||
</template>
|
||||
</span>
|
||||
<span
|
||||
v-if="allowClear && tagData?.length && !disabled"
|
||||
class="layui-tag-input-clear"
|
||||
>
|
||||
<lay-icon type="layui-icon-close-fill" @click.stop="handleClearClick" />
|
||||
</span>
|
||||
<span class="layui-tag-input-suffix" v-if="$slots.suffix">
|
||||
<slot name="suffix"></slot>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
1
src/component/tagInput/inerface.ts
Normal file
1
src/component/tagInput/inerface.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type TagInputSize = "lg" | "md" | "sm" | "xs";
|
||||
Reference in New Issue
Block a user