Merge branch 'feat-tag-input' into next

This commit is contained in:
sight 2022-09-22 11:48:18 +08:00
commit 50e8e2a419
5 changed files with 304 additions and 184 deletions

View File

@ -36,8 +36,14 @@ import {
computed, computed,
toRef, toRef,
StyleValue, StyleValue,
Ref,
} from "vue"; } from "vue";
import { onClickOutside, useEventListener, useThrottleFn } from "@vueuse/core"; import {
onClickOutside,
useEventListener,
useResizeObserver,
useThrottleFn,
} from "@vueuse/core";
export type PopperTrigger = "click" | "hover" | "focus" | "contextMenu"; export type PopperTrigger = "click" | "hover" | "focus" | "contextMenu";
@ -209,18 +215,35 @@ onClickOutside(
} }
); );
useResizeObserver(triggerRefEl, () => {
updatePosistion();
});
let popperObserver: { stop: any; isSupported?: Ref<boolean> } | undefined =
undefined;
watch(innerVisible, (isShow) => {
updatePosistion();
if (isShow) {
popperObserver = useResizeObserver(popperRefEl, () => {
updatePosistion();
});
} else {
popperObserver && popperObserver.stop();
}
});
watch( watch(
() => props.visible, () => props.visible,
(isShow) => (isShow ? doShow() : doHidden()) (isShow) => (isShow ? doShow() : doHidden())
); );
watch(innerVisible, (val) => { watch(
updatePosistion(); () => props.content,
}); () => {
updatePosistion();
watch([() => props.content, () => slots.content && slots?.content()], (val) => { }
innerVisible.value && updatePosistion(); );
});
const isScrollElement = function (element: HTMLElement) { const isScrollElement = function (element: HTMLElement) {
return ( return (

View File

@ -1,64 +0,0 @@
<script lang="ts">
export default {
name: "Tag",
};
</script>
<script lang="ts" setup>
import { ref } from "vue";
export interface LayTagProps {
theme?: string;
closable?: boolean;
size?: string;
}
const props = withDefaults(defineProps<LayTagProps>(), {
closable: true,
});
const emit = defineEmits(["close"]);
const visible = ref(true);
const handleClose = (e: MouseEvent) => {
visible.value = false;
emit("close", e);
};
</script>
<template>
<lay-badge v-if="visible" theme="green">
<template v-if="$slots.default" #default>
<slot name="default"></slot>
<lay-icon
v-if="closable"
type="layui-icon-close"
@click.stop="handleClose"
></lay-icon>
</template>
</lay-badge>
</template>
<!-- <template v-for="(item, index) in selectItem.label" :key="index">
<lay-badge theme="green">
<span>{{ item }}</span>
<i
:class="['layui-icon', { 'layui-icon-close': true }]"
v-if="
!disabled &&
!(
Array.isArray(selectItem.value) &&
selectItem.value.length > 0 &&
disabledItemMap[selectItem.value[index]]
)
"
@click="
removeItemHandle($event, {
label: item,
value: Array.isArray(selectItem.value)
? selectItem.value[index]
: null,
})
"
></i>
</lay-badge>
</template> -->

View File

@ -1,65 +1,138 @@
.layui-input-tag { @import "../tag/index.less";
position: relative; @import "../popper/index.less";
display: block; @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: auto;
border-width: @tag-input-boeder;
border-style: solid;
border-color: var(--input-border-color);
border-radius: var(--input-border-radius);
padding: 0 5px; padding: 0 5px;
height: auto; cursor: text;
overflow: hidden;
.layui-input-prefix { &-inner {
display: inline;
text-align: left;
height: 100%;
flex: unset;
}
.layui-input-suffix{
position: absolute;
right: 3px;
bottom: 0;
height: 100%;
}
.layui-input {
display: inline-block;
padding-left: 0;
width: auto;
flex: 1; flex: 1;
max-width: 100%; overflow: hidden;
min-width: 12px; line-height: 0;
padding: @tag-input-inner-padding-md 0;
} }
}
.layui-input-tag-collapsed-panel, &-mirror {
.layui-input-tag { position: absolute;
.layui-badge { top: 0;
margin-right: 5px; left: 0;
height: 28px; white-space: pre;
line-height: 28px; visibility: hidden;
user-select: none; pointer-events: none;
white-space: pre-wrap; }
.layui-icon { &-clear {
font-size: 12px; display: none;
padding-left: 3px; align-items: center;
cursor: pointer;
}
&:hover { &-clear:hover {
cursor: pointer; opacity: 0.5;
color: #ff5722; }
}
& &-inner-input {
box-sizing: border-box;
border: none;
}
&-disabled {
cursor: not-allowed;
opacity: 0.4;
.layui-tag-input-clear {
cursor: not-allowed;
opacity: 0.4;
} }
} }
.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;
max-width: 240px;
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 0;
}
.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-input-tag-collapsed-panel { .layui-tag-input:hover,
white-space: normal; .layui-tag-input:focus-within {
display: flex; border-color: #d2d2d2 !important;
align-items: center; .layui-tag-input-clear{
flex-wrap: wrap; display: flex;
width: fit-content;
max-width: 200px;
height: auto;
overflow: hidden;
.layui-badge{
margin-bottom: 4px;
} }
} }

View File

@ -5,9 +5,18 @@ export default {
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
import "./index.less"; import "./index.less";
import Tag from "./Tag.vue"; import LayTag, { LayTagProps } from "../tag/index.vue";
import LayToopTip from "../tooltip/index.vue"; import LayToopTip from "../tooltip/index.vue";
import { onMounted, shallowRef, ref, watch, computed } from "vue"; import {
onMounted,
shallowRef,
ref,
watch,
computed,
reactive,
nextTick,
} from "vue";
import { reactiveOmit, useResizeObserver, useVModel } from "@vueuse/core";
export interface TagData { export interface TagData {
value?: string | number; value?: string | number;
@ -17,7 +26,7 @@ export interface TagData {
} }
export interface LayInputTagProps { export interface LayInputTagProps {
modelValue?: TagData[]; modelValue?: (string | number | TagData)[];
inputValue?: string; inputValue?: string;
disabled?: boolean; disabled?: boolean;
placeholder?: string; placeholder?: string;
@ -26,7 +35,8 @@ export interface LayInputTagProps {
max?: number; max?: number;
minCollapsedNum?: number; minCollapsedNum?: number;
collapseTagsTooltip?: boolean; collapseTagsTooltip?: boolean;
size?: "md" | "sm" | "xs"; size?: "lg" | "md" | "sm" | "xs";
tagProps?: LayTagProps;
} }
const props = withDefaults(defineProps<LayInputTagProps>(), { const props = withDefaults(defineProps<LayInputTagProps>(), {
@ -34,33 +44,60 @@ const props = withDefaults(defineProps<LayInputTagProps>(), {
placeholder: "", placeholder: "",
readonly: false, readonly: false,
allowClear: true, allowClear: true,
minCollapsedNum: 10, minCollapsedNum: 3,
size: "md",
//max:3 //max:3
}); });
const emit = defineEmits(["update:modelValue", "update:inputValue"]); const emit = defineEmits(["update:modelValue", "update:inputValue"]);
const mirrorRef = shallowRef<HTMLElement | undefined>(undefined); const mirrorRefEl = shallowRef<HTMLElement | undefined>(undefined);
const inputRef = shallowRef<HTMLElement | undefined>(undefined); const inputRefEl = shallowRef<HTMLInputElement | undefined>(undefined);
const oldInputValue = ref<string>("");
const tagData = ref<TagData[]>(props.modelValue ?? []); const compositionValue = ref<string>("");
const inputValue = ref<string>(); const isComposing = ref(false);
const oldInputValue = ref<string>(); const inputStyle = reactive({ width: "15px" });
const inputValue = useVModel(props, "inputValue", emit, { defaultValue: "" });
const tagData = useVModel(props, "modelValue", emit, {
deep: true,
defaultValue: [] as TagData[],
});
const tagProps = reactiveOmit(props.tagProps ?? {}, 'closable','size', 'disabled')
const computedTagData = computed(() => { const computedTagData = computed(() => {
if (!tagData.value) return;
return props.minCollapsedNum return props.minCollapsedNum
? tagData.value?.slice(0, props.minCollapsedNum) ? tagData.value?.slice(0, props.minCollapsedNum)
: tagData.value; : tagData.value;
}); });
const collapsedTagData = computed(() => { const collapsedTagData = computed(() => {
if (!tagData.value) return;
return props.minCollapsedNum && tagData.value?.length > props.minCollapsedNum return props.minCollapsedNum && tagData.value?.length > props.minCollapsedNum
? tagData.value?.slice(props.minCollapsedNum) ? tagData.value?.slice(props.minCollapsedNum)
: []; : [];
}); });
const handleInputEnter = (e: KeyboardEvent) => { const handleInput = function (event: Event) {
e.preventDefault(); if (isComposing.value) {
return;
}
inputValue.value = (event.target as HTMLInputElement).value;
};
const handleComposition = (event: CompositionEvent) => {
if (event.type === "compositionend") {
isComposing.value = false;
compositionValue.value = "";
handleInput(event);
} else {
isComposing.value = true;
compositionValue.value = inputValue.value + (event.data ?? "");
}
};
const handleEnter = (event: KeyboardEvent) => {
event.preventDefault();
const valueStr = inputValue.value ? String(inputValue.value).trim() : ""; const valueStr = inputValue.value ? String(inputValue.value).trim() : "";
if (!valueStr || !tagData.value) return; if (!valueStr || !tagData.value) return;
const isLimit = props.max && tagData.value?.length >= props.max; const isLimit = props.max && tagData.value?.length >= props.max;
@ -73,86 +110,137 @@ const handleInputEnter = (e: KeyboardEvent) => {
} }
}; };
const handlerInputBackspaceKeyUp = (e: KeyboardEvent) => { const handleBackspaceKeyUp = (event: KeyboardEvent) => {
if (!tagData.value || !tagData.value.length) return; if (!tagData.value || !tagData.value.length) return;
if (!oldInputValue.value && ["Backspace", "Delete"].includes(e.code)) { if (!oldInputValue.value && ["Backspace", "Delete"].includes(event.code)) {
tagData.value = tagData.value.slice(0, -1); tagData.value = tagData.value.slice(0, -1);
} }
oldInputValue.value = inputValue.value; oldInputValue.value = inputValue.value ?? "";
}; };
const handlerClearClick = (e: MouseEvent) => { const handleFocus = () => {
(inputRefEl.value as HTMLInputElement)?.focus();
};
const handleBlur = () => {
(inputRefEl.value as HTMLInputElement)?.blur();
};
const handleClearClick = (e: MouseEvent) => {
if (props.disabled || props.readonly || !props.allowClear) {
return;
}
tagData.value = []; tagData.value = [];
}; };
const handlerClose = (index: number) => { const handleClose = (index: number) => {
if (!tagData.value) return; if (!tagData.value) return;
const arr = [...tagData.value]; const arr = [...tagData.value];
arr.splice(index, 1); arr.splice(index, 1);
tagData.value = arr; tagData.value = arr;
}; };
const handlerFocus = (e: MouseEvent) => { const setInputWidth = (width: number) => {
( if (width > 15) {
(e.target as HTMLElement).querySelector(".layui-input") as HTMLInputElement inputStyle.width = `${width}px`;
)?.focus(); } else {
inputStyle.width = "15px";
}
}; };
watch(tagData, (val) => { const handleResize = () => {
emit("update:modelValue", val); if (mirrorRefEl.value) {
setInputWidth(mirrorRefEl.value.offsetWidth);
}
};
const cls = computed(() => [
`layui-tag-input`,
`layui-tag-input-${props.size}`,
{
"layui-tag-input-disabled": props.disabled,
},
]);
useResizeObserver(mirrorRefEl, () => {
handleResize();
}); });
watch(inputValue, (val) => { watch(
emit("update:inputValue", val); () => inputValue.value,
(val) => {
if (inputRefEl.value && !isComposing.value) {
nextTick(() => {
inputRefEl.value!.value = val ?? "";
});
}
}
);
onMounted(() => {
handleResize();
}); });
onMounted(() => {}); defineExpose({
focus: handleFocus,
blur: handleBlur,
});
</script> </script>
<template> <template>
<lay-input <div :class="cls" @click="handleFocus">
class="layui-input-tag" <span ref="mirrorRefEl" class="layui-tag-input-mirror">
v-model="inputValue" {{ compositionValue || inputValue || placeholder }}
:placeholder="placeholder" </span>
:readonly="readonly" <span v-if="$slots.prefix">
@keydown.enter="handleInputEnter" <slot name="prefix"></slot>
@keyup="handlerInputBackspaceKeyUp" </span>
@click="handlerFocus" <span class="layui-tag-input-inner">
>
<template #prefix>
<template <template
v-for="(item, index) of computedTagData" v-for="(item, index) of computedTagData"
:key="`${item}-${index}`" :key="`${item}-${index}`"
> >
<Tag :closable="!readonly" @close="handlerClose(index)"> <LayTag v-bind="tagProps" :closable="!readonly && !disabled" :size="size" @close="handleClose(index)">
{{ item }} {{ item }}
</Tag> </LayTag>
</template> </template>
<template v-if="computedTagData?.length != tagData?.length"> <template v-if="computedTagData?.length != tagData?.length">
<LayToopTip :isDark="false"> <LayToopTip :isDark="false" trigger="click" popperStyle="padding:6px">
<Tag key="more" :closable="false" <LayTag v-bind="tagProps" key="more" :closable="false" :size="size">
>+{{ tagData?.length - computedTagData?.length }}...</Tag +{{tagData!.length - computedTagData!.length }}...
> </LayTag>
<template #content> <template #content>
<div class="layui-input-tag-collapsed-panel"> <div class="layui-tag-input-collapsed-panel">
<template <LayTag
v-for="(item, index) of tagData" v-for="(item, index) of collapsedTagData"
:key="`${item}-${index}`" :key="`${item}-${index}`"
v-bind="tagProps"
:closable="!readonly && !disabled"
:size="size"
@close="handleClose(index + (minCollapsedNum ?? 0))"
> >
<Tag {{ item }}
v-if="index >= minCollapsedNum" </LayTag>
:closable="!readonly"
@close="handlerClose(index)"
>
{{ item }}
</Tag>
</template>
</div> </div>
</template> </template>
</LayToopTip> </LayToopTip>
</template> </template>
</template> <input
<template #suffix v-if="allowClear && tagData?.length"> ref="inputRefEl"
<lay-icon type="layui-icon-close-fill" @click.stop="handlerClearClick" /> class="layui-tag-input-inner-input"
</template> :style="inputStyle"
</lay-input> :disabled="disabled"
:placeholder="placeholder"
:readonly="readonly"
@keydown.enter="handleEnter"
@keyup="handleBackspaceKeyUp"
@input="handleInput"
@compositionstart="handleComposition"
@compositionupdate="handleComposition"
@compositionend="handleComposition"
/>
</span>
<span v-if="allowClear && tagData?.length" class="layui-tag-input-clear">
<lay-icon type="layui-icon-close-fill" @click.stop="handleClearClick" />
</span>
</div>
</template> </template>

View File

@ -62,7 +62,7 @@ const props = defineProps({
type: [String, Array, Object], type: [String, Array, Object],
}, },
popperStyle: { popperStyle: {
type: Object as PropType<StyleValue>, type: [String, Object] as PropType<StyleValue>,
}, },
}); });
const vm = getCurrentInstance()!; const vm = getCurrentInstance()!;