import { defineComponent, ref, onMounted, watch, onUnmounted, computed, getCurrentInstance } from 'vue';
import { IMentionSuggestionItem, mentionProps, type MentionProps } from './mention-types';
import DTextarea from '../../textarea/src/textarea';
import DIcon from '../../icon/src/icon';
import './mention.scss';
import { useNamespace } from '../../shared/hooks/use-namespace';
import { debounce } from 'lodash';
export default defineComponent({
name: 'DMention',
components: {
DTextarea,
DIcon,
},
props: mentionProps,
emits: ['select', 'change'],
setup(props: MentionProps, { slots, emit }) {
const ns = useNamespace('mention');
const textContext = ref<string>('');
const showSuggestions = ref<boolean>(false);
const currentIndex = ref<number>(0);
const suggestionsTop = ref<number>();
const suggestions = ref<IMentionSuggestionItem[]>([]);
const filteredSuggestions = ref<IMentionSuggestionItem[]>([]);
const suggestionsDom = ref<HTMLDivElement>();
const loading = computed(() => props.loading);
const instance = getCurrentInstance();
const handleUpdate = debounce((val: string) => {
if (props.trigger.includes(val[0])) {
showSuggestions.value = true;
if (props.position === 'top') {
setTimeout(() => {
const height = window.getComputedStyle(suggestionsDom.value as Element, null).height;
suggestionsTop.value = -Number(height.replace('px', ''));
}, 0);
}
filteredSuggestions.value = (suggestions.value as IMentionSuggestionItem[]).filter((item: IMentionSuggestionItem) =>
String(item[props.dmValueParse.value as keyof IMentionSuggestionItem])
.toLocaleLowerCase()
.includes(val.slice(1).toLocaleLowerCase())
);
} else {
showSuggestions.value = false;
}
emit('change', val.slice(1));
}, 300);
const handleBlur = (e: Event) => {
const { target } = e;
const ele = document.querySelector('.devui-mention');
if (!ele?.contains(target as Element)) {
setTimeout(() => {
showSuggestions.value = false;
}, 100);
}
};
const handleFocus = () => {
if (props.trigger.includes(textContext.value)) {
showSuggestions.value = true;
}
};
const clickItem = (item: IMentionSuggestionItem, e: MouseEvent) => {
emit('select', item);
e.stopPropagation();
e.preventDefault();
showSuggestions.value = false;
textContext.value = textContext.value.substring(0, 1) + item[props.dmValueParse.value as keyof IMentionSuggestionItem];
};
const arrowKeyDown = (e: KeyboardEvent) => {
if (showSuggestions.value && filteredSuggestions.value.length) {
if (e.key === 'ArrowDown') {
currentIndex.value++;
if (currentIndex.value === filteredSuggestions.value.length) {
currentIndex.value = 0;
}
}
if (e.key === 'ArrowUp') {
currentIndex.value--;
if (currentIndex.value === -1) {
currentIndex.value = filteredSuggestions.value.length - 1;
}
}
const itemDom = instance?.proxy?.$refs[`devui-suggestions-item-${currentIndex.value}`];
const itemOffsetTop = (itemDom as HTMLElement)?.offsetTop;
const clientHeight = (suggestionsDom.value as HTMLElement)?.clientHeight;
const itemTotal = Math.ceil(clientHeight / (itemDom as HTMLElement).clientHeight);
if ((e.key === 'ArrowDown' && currentIndex.value >= itemTotal) || e.key === 'ArrowUp') {
suggestionsDom.value?.scrollTo({
top: itemOffsetTop,
});
}
}
};
const enterKeyDown = (e: KeyboardEvent) => {
if (showSuggestions.value && filteredSuggestions.value.length) {
if (e.key === 'Enter') {
e.stopPropagation();
e.preventDefault();
showSuggestions.value = false;
textContext.value =
textContext.value.substring(0, 1) +
filteredSuggestions.value[currentIndex.value][props.dmValueParse.value as keyof IMentionSuggestionItem];
emit('select', filteredSuggestions.value[currentIndex.value]);
}
}
};
watch(
() => props.suggestions,
(val) => {
suggestions.value = val as IMentionSuggestionItem[];
filteredSuggestions.value = val as IMentionSuggestionItem[];
},
{ immediate: true, deep: true }
);
onMounted(() => {
window.addEventListener('keydown', arrowKeyDown);
window.addEventListener('keydown', enterKeyDown);
document.addEventListener('click', handleBlur);
});
onUnmounted(() => {
window.removeEventListener('keydown', arrowKeyDown);
window.removeEventListener('keydown', enterKeyDown);
document.removeEventListener('click', handleBlur);
});
return () => {
return (
<div class={ns.b()}>
<d-textarea
v-model={textContext.value}
show-glow-style={props.showGlowStyle}
onUpdate={handleUpdate}
onFocus={handleFocus}></d-textarea>
{showSuggestions.value ? (
loading.value ? (
<div class={[`${ns.e('suggestions')} ${ns.e('suggestions-loading')}`]}>加载中... </div>
) : (
<div
class={ns.e('suggestions')}
ref={suggestionsDom}
style={{
marginTop: props.position === 'top' ? '0px' : '-16px',
top: suggestionsTop.value ? suggestionsTop.value + 'px' : 'inherit',
}}>
{filteredSuggestions.value.length > 0 ? (
filteredSuggestions.value?.map((item, index) => {
return (
<div
ref={`devui-suggestions-item-${index}`}
class={`${ns.e('suggestions-item')}
${currentIndex.value === index ? `${ns.e('suggestions-item-active')}` : ''}`}
key={item.id}
onClick={(e) => clickItem(item, e)}>
{slots.template ? slots.template({ item }) : item.value}
</div>
);
})
) : (
<div>{props.notFoundContent}</div>
)}
</div>
)
) : null}
</div>
);
};
},
});