import { PopsCommonCSSClassName } from "../../config/CommonCSSClassName"; import { GlobalConfig } from "../../config/GlobalConfig"; import { EventEmiter } from "../../event/EventEmiter"; import { PopsElementHandler } from "../../handler/PopsElementHandler"; import { PopsHandler } from "../../handler/PopsHandler"; import { PopsCSS } from "../../PopsCSS"; import type { EventMap } from "../../types/EventEmitter"; import type { PopsType } from "../../types/main"; import { popsDOMUtils } from "../../utils/PopsDOMUtils"; import { PopsSafeUtils } from "../../utils/PopsSafeUtils"; import { popsUtils } from "../../utils/PopsUtils"; import { PopsSearchSuggestionDefaultConfig } from "./defaultConfig"; import type { PopsSearchSuggestionConfig, PopsSearchSuggestionData } from "./types/index"; export const PopsSearchSuggestion = { init(__config__: PopsSearchSuggestionConfig) { const guid = popsUtils.getRandomGUID(); // 设置当前类型 const popsType: PopsType = "searchSuggestion"; let config = PopsSearchSuggestionDefaultConfig(); config = popsUtils.assign(config, GlobalConfig.getGlobalConfig()); config = popsUtils.assign(config, __config__); // 如果$inputTarget为空,则根据$target if (config.$inputTarget == null) { config.$inputTarget = config.$target as HTMLInputElement; } const emitter = config.emitter ?? new EventEmiter(popsType); const { $shadowContainer, $shadowRoot } = PopsHandler.handlerShadow(config); PopsHandler.handleInit($shadowRoot, [ { name: "index", css: PopsCSS.index, }, { name: "anim", css: PopsCSS.anim, }, { name: "common", css: PopsCSS.common, }, { name: "skeleton", css: PopsCSS.skeletonCSS, }, ]); // 添加自定义style PopsElementHandler.addStyle($shadowRoot, config.style); // 添加自定义浅色style PopsElementHandler.addLightStyle($shadowRoot, config.lightStyle); // 添加自定义深色style PopsElementHandler.addDarkStyle($shadowRoot, config.darkStyle); /** * 监听器的默认配置 */ const defaultListenerOption: AddEventListenerOptions = { capture: true, passive: true, }; const SearchSuggestion = { emitter: emitter, /** * 当前的环境,可以是document,可以是shadowroot,默认是document */ selfDocument: config.$selfDocument, $el: { /** 根元素 */ root: null as any as HTMLElement, /** * 包裹ul的容器元素 */ $dropdownWrapper: null as any as HTMLElement, /** ul元素 */ $dropdownContainer: null as any as HTMLUListElement, /** * 箭头元素 */ $arrow: null as any as HTMLDivElement, /** 动态更新CSS */ $dynamicCSS: null as any as HTMLStyleElement, }, $evt: { offInputChangeEvtHandler: [] as ((...args: any[]) => any)[], }, $data: { /** 是否结果为空 */ isEmpty: true, /** * 存储在元素上的操作的键名 */ storeNodeHandlerKey: "data-SearchSuggestion", }, /** * 初始化 * @param $parent 父元素 * @example * .init(); * .setAllEvent(); */ init($parent = document.body || document.documentElement) { SearchSuggestion.initEl(); SearchSuggestion.update(SearchSuggestion.getData()); SearchSuggestion.updateStyleSheet(); SearchSuggestion.hide(); $shadowRoot.appendChild(SearchSuggestion.$el.root); $parent.appendChild($shadowContainer); }, /** * 初始化元素变量 */ initEl() { SearchSuggestion.$el.root = SearchSuggestion.createSearchSelectElement(); Reflect.set(SearchSuggestion.$el.root, this.$data.storeNodeHandlerKey, SearchSuggestion); SearchSuggestion.$el.$dynamicCSS = SearchSuggestion.$el.root.querySelector("style[data-dynamic]")!; SearchSuggestion.$el.$dropdownWrapper = SearchSuggestion.$el.root.querySelector( `.pops-${popsType}-search-suggestion-dropdown-wrapper` )!; SearchSuggestion.$el.$dropdownContainer = SearchSuggestion.$el.root.querySelector( `ul.pops-${popsType}-search-suggestion-dropdown-container` )!; SearchSuggestion.$el.$arrow = SearchSuggestion.$el.root.querySelector( `.pops-${popsType}-search-suggestion-arrow` )!; }, /** * 获取数据 */ getData(): PopsSearchSuggestionData[] { return typeof config.data === "function" ? config.data() : config.data; }, /** * 更新数据 * @param data 数据 */ setData(data: PopsSearchSuggestionData[]) { (["data"]>config.data) = data; }, /** * 获取显示出搜索建议框的html */ createSearchSelectElement() { const $el = popsDOMUtils.createElement( "div", { className: `pops pops-${popsType}-search-suggestion`, innerHTML: /*html*/ `
    ${ config.toSearhNotResultHTML }
${config.useArrow ? /*html*/ `
` : ""} `, }, { "data-guid": guid, "type-value": popsType, } ); if (config.className !== "" && config.className != null) { popsDOMUtils.addClassName($el, config.className); } if (config.isAnimation) { popsDOMUtils.addClassName($el, `pops-${popsType}-animation`); } if (config.useFoldAnimation) { popsDOMUtils.addClassName($el, "el-zoom-in-top-animation"); } return $el; }, /** * 动态获取CSS */ getDynamicCSS() { return /*css*/ ` .pops-${popsType}-search-suggestion{ --search-suggestion-bg-color: #ffffff; --search-suggestion-box-shadow-color: rgb(0 0 0 / 20%); --search-suggestion-item-color: #515a6e; --search-suggestion-item-none-color: #8e8e8e; --search-suggestion-item-is-hover-bg-color: #f5f7fa; --search-suggestion-item-is-select-bg-color: #409eff; } .pops-${popsType}-search-suggestion{ border: initial; overflow: initial; position: ${config.isAbsolute ? "absolute" : "fixed"}; z-index: ${PopsHandler.getTargerOrFunctionValue(config.zIndex)}; } .pops-${popsType}-search-suggestion-dropdown-wrapper{ max-height: ${config.maxHeight}; border-radius: 4px; box-shadow: 0 1px 6px var(--search-suggestion-box-shadow-color); background-color: var(--search-suggestion-bg-color); padding: 5px 0; overflow-x: hidden; overflow-y: auto; } .pops-${popsType}-search-suggestion-dropdown-wrapper ul.pops-${popsType}-search-suggestion-dropdown-container{ box-sizing: border-box; } // 建议框在上面时 .pops-${popsType}-search-suggestion[data-top-reverse] ul.pops-${popsType}-search-suggestion-dropdown-container{ display: flex; flex-direction: column-reverse; } .pops-${popsType}-search-suggestion[data-top-reverse] ul.pops-${popsType}-search-suggestion-dropdown-container li{ flex-shrink: 0; } ul.pops-${popsType}-search-suggestion-dropdown-container li{ padding: 7px; margin: 0; clear: both; color: var(--search-suggestion-item-color); font-size: 14px; list-style: none; cursor: pointer; transition: background .2s ease-in-out; overflow: hidden; text-overflow: ellipsis; width: 100%; } ul.pops-${popsType}-search-suggestion-dropdown-container li[data-none]{ text-align: center; font-size: 12px; color: var(--search-suggestion-item-none-color); } ul.pops-${popsType}-search-suggestion-dropdown-container li:not([data-none]):hover{ background-color: var(--search-suggestion-item-is-hover-bg-color); } @media (prefers-color-scheme: dark){ .pops-${popsType}-search-suggestion{ --search-suggestion-bg-color: #1d1e1f; --search-suggestion-item-color: #cfd3d4; --search-suggestion-item-is-hover-bg-color: rgba(175, 175, 175, .1); } } `; }, /** * 获取data-value值 * @param data 数据项 */ getItemDataValue(data: PopsSearchSuggestionData) { return data; }, /** * 获取显示出搜索建议框的每一项的html * @param dataItem 当前项的值 * @param dateItemIndex 当前项的下标 */ createSearchItemLiElement(dataItem: PopsSearchSuggestionData, dateItemIndex: number) { const dataValue = SearchSuggestion.getItemDataValue(dataItem); const $li = popsDOMUtils.createElement("li", { className: `pops-${popsType}-search-suggestion-dropdown-item`, "data-index": dateItemIndex, "data-value": dataValue, }); Reflect.set($li, "data-index", dateItemIndex); Reflect.set($li, "data-value", dataValue); // 项内容 const $itemInner = dataItem.itemView(dataItem, $li, config); if (typeof $itemInner === "string") { PopsSafeUtils.setSafeHTML($li, $itemInner); } else { popsDOMUtils.append($li, $itemInner); } // 删除按钮 const enableDeleteButton = dataItem.enableDeleteButton; if (typeof enableDeleteButton === "boolean" && enableDeleteButton) { const $deleteIcon = SearchSuggestion.createItemDeleteIcon(); popsDOMUtils.append($li, $deleteIcon); } popsDOMUtils.addClassName($li, PopsCommonCSSClassName.flexCenter); popsDOMUtils.addClassName($li, PopsCommonCSSClassName.flexYCenter); return $li; }, /** * 设置搜索建议框每一项的点击事件 * @param $searchItem 当前项的元素 */ setSearchItemClickEvent($searchItem: HTMLLIElement) { popsDOMUtils.on( $searchItem, "click", async (event) => { popsDOMUtils.preventEvent(event); const $click = event.target as HTMLLIElement; const data = SearchSuggestion.getData(); const dataItem = Reflect.get($searchItem, "data-value") as PopsSearchSuggestionData; const isDelete = Boolean($click.closest(`.pops-${popsType}-delete-icon`)); if (isDelete) { // 删除 if (typeof dataItem.deleteButtonClickCallback === "function") { const result = await dataItem.deleteButtonClickCallback(event, $searchItem, dataItem, config); if (typeof result === "boolean" && result) { data.splice(data.indexOf(dataItem), 1); popsDOMUtils.remove($searchItem); } } if (!SearchSuggestion.$el.$dropdownContainer.children.length) { // 全删完了 SearchSuggestion.clear(); } SearchSuggestion.updateStyleSheet(); } else { // 点击选择项 if (typeof dataItem.clickCallback === "function") { const result = await dataItem.clickCallback(event, $searchItem, dataItem, config); if (typeof result === "boolean" && result) { if ( config.$inputTarget instanceof HTMLInputElement || config.$inputTarget instanceof HTMLTextAreaElement ) { config.$inputTarget.value = String(dataItem.value); } } } } }, { capture: true, } ); }, /** * 设置搜索建议框每一项的选中事件 * @param $li 每一项元素 */ // eslint-disable-next-line @typescript-eslint/no-unused-vars setSearchItemSelectEvent($li: HTMLLIElement) { // TODO }, /** * 监听输入框内容改变 */ setInputChangeEvent(option: AddEventListenerOptions = defaultListenerOption) { // 必须是input或者textarea才有input事件 if (!(config.$inputTarget instanceof HTMLInputElement || config.$inputTarget instanceof HTMLTextAreaElement)) { return; } // 是input输入框 // 禁用输入框自动提示 config.$inputTarget.setAttribute("autocomplete", "off"); // 内容改变事件 const listenerHandler = popsDOMUtils.onInput( config.$inputTarget, async () => { const data = SearchSuggestion.getData(); const queryDataResult = await config.inputTargetChangeRefreshShowDataCallback( config.$inputTarget.value, data, config ); SearchSuggestion.update(queryDataResult); SearchSuggestion.updateStyleSheet(); }, option ); SearchSuggestion.$evt.offInputChangeEvtHandler.push(listenerHandler.off); }, /** * 移除输入框内容改变的监听 */ removeInputChangeEvent() { for (let index = 0; index < SearchSuggestion.$evt.offInputChangeEvtHandler.length; index++) { const handler = SearchSuggestion.$evt.offInputChangeEvtHandler[index]; handler(); SearchSuggestion.$evt.offInputChangeEvtHandler.splice(index, 1); index--; } }, /** * 显示搜索建议框的事件 */ showEvent() { SearchSuggestion.updateStyleSheet(); if (config.toHideWithNotResult) { if (SearchSuggestion.$data.isEmpty) { SearchSuggestion.hide(true); } else { SearchSuggestion.show(); } } else { SearchSuggestion.show(); } }, /** * 设置显示搜索建议框的事件 */ setShowEvent(option: AddEventListenerOptions = defaultListenerOption) { /* 焦点|点击事件*/ if (config.followPosition === "target") { popsDOMUtils.on([config.$target], ["focus", "click"], SearchSuggestion.showEvent, option); } else if (config.followPosition === "input") { popsDOMUtils.on([config.$inputTarget], ["focus", "click"], SearchSuggestion.showEvent, option); } else if (config.followPosition === "inputCursor") { popsDOMUtils.on([config.$inputTarget], ["focus", "click", "input"], SearchSuggestion.showEvent, option); } else { throw new Error("未知followPosition:" + config.followPosition); } }, /** * 移除显示搜索建议框的事件 */ removeShowEvent(option: AddEventListenerOptions = defaultListenerOption) { /* 焦点|点击事件*/ popsDOMUtils.off([config.$target, config.$inputTarget], ["focus", "click"], SearchSuggestion.showEvent, option); // 内容改变事件 popsDOMUtils.off([config.$inputTarget], ["input"], SearchSuggestion.showEvent, option); }, /** * 隐藏搜索建议框的事件 * @param event */ hideEvent(event: PointerEvent | MouseEvent) { if (event.target instanceof Node) { if ($shadowContainer.contains(event.target)) { // 点击在shadow上的 return; } if (config.$target.contains(event.target)) { // 点击在目标元素内 return; } if (config.$inputTarget.contains(event.target)) { // 点击在目标input内 return; } SearchSuggestion.hide(true); } }, /** * 设置隐藏搜索建议框的事件 */ setHideEvent(option: AddEventListenerOptions = defaultListenerOption) { // 全局点击事件 // 全局触摸屏点击事件 if (Array.isArray(SearchSuggestion.selfDocument)) { SearchSuggestion.selfDocument.forEach(($checkParent) => { popsDOMUtils.on($checkParent, ["click", "touchstart"], SearchSuggestion.hideEvent, option); }); } else { popsDOMUtils.on(SearchSuggestion.selfDocument, ["click", "touchstart"], SearchSuggestion.hideEvent, option); } }, /** * 移除隐藏搜索建议框的事件 */ removeHideEvent(option: AddEventListenerOptions = defaultListenerOption) { if (Array.isArray(SearchSuggestion.selfDocument)) { SearchSuggestion.selfDocument.forEach(($checkParent) => { popsDOMUtils.off($checkParent, ["click", "touchstart"], SearchSuggestion.hideEvent, option); }); } else { popsDOMUtils.off(SearchSuggestion.selfDocument, ["click", "touchstart"], SearchSuggestion.hideEvent, option); } }, /** * 设置所有监听,包括(input值改变、全局点击判断显示/隐藏建议框) */ setAllEvent(option: AddEventListenerOptions = defaultListenerOption) { SearchSuggestion.setInputChangeEvent(option); SearchSuggestion.setHideEvent(option); SearchSuggestion.setShowEvent(option); }, /** * 移除所有监听 */ removeAllEvent(option: AddEventListenerOptions = defaultListenerOption) { SearchSuggestion.removeInputChangeEvent(); SearchSuggestion.removeHideEvent(option); SearchSuggestion.removeShowEvent(option); }, /** * 获取删除按钮的html */ createItemDeleteIcon(size = 16, fill = "#bababa") { const $svg = popsDOMUtils.parseTextToDOM(/*html*/ ` `); return $svg; }, /** * 设置当前正在搜索中的提示 */ setPromptsInSearch() { const $isSearching = popsDOMUtils.createElement("li", { className: `pops-${popsType}-search-suggestion-dropdown-searching-item`, innerHTML: config.searchingTip, }); SearchSuggestion.addItem($isSearching); }, /** * 移除正在搜索中的提示 */ removePromptsInSearch() { popsDOMUtils.remove( SearchSuggestion.$el.$dropdownContainer.querySelector( `li.pops-${popsType}-search-suggestion-dropdown-searching-item` ) ); }, /** * 更新搜索建议框的位置(top、left) * 因为目标元素可能是动态隐藏的 * @param target 目标元素 * @param checkPositonAgain 是否在更新位置信息后检测更新位置信息,默认true */ changeHintULElementPosition(target = config.$target ?? config.$inputTarget, checkPositonAgain: boolean = true) { let targetRect: DOMRect | null = null; if (config.followPosition === "inputCursor") { targetRect = popsDOMUtils.getTextBoundingRect( config.$inputTarget, config.$inputTarget.selectionStart || 0, config.$inputTarget.selectionEnd || 0, false ); } else { targetRect = config.isAbsolute ? popsDOMUtils.offset(target) : target.getBoundingClientRect(); } if (targetRect == null) { return; } // 文档最大高度 let documentHeight = document.documentElement.clientHeight; if (config.isAbsolute) { // 绝对定位 documentHeight = popsDOMUtils.height(document); } // 文档最大宽度 const documentWidth = popsDOMUtils.width(document); // 箭头 const arrowHeight = config.useArrow ? popsDOMUtils.height(SearchSuggestion.$el.$arrow) : 0; let position = config.position; if (config.position === "auto") { // 需目标高度+搜索建议框高度大于文档高度,则显示在上面 const targetBottom = targetRect.bottom; // 容器整体的高度 const searchSuggestionContainerHeight = popsDOMUtils.height(SearchSuggestion.$el.$dropdownWrapper) + arrowHeight; if (targetBottom + searchSuggestionContainerHeight > documentHeight) { // 在上面 position = "top"; } else { // 在下面 position = "bottom"; } } if (position === "top") { // 在上面 if (config.positionTopToReverse) { // 自动翻转 SearchSuggestion.$el.root.setAttribute("data-top-reverse", "true"); } if (config.useFoldAnimation) { // 翻转折叠 SearchSuggestion.$el.root.setAttribute("data-popper-placement", "top"); } const bottom = documentHeight - targetRect.top + config.topDistance + arrowHeight; SearchSuggestion.$el.root.style.top = ""; SearchSuggestion.$el.root.style.bottom = bottom + "px"; } else if (position === "bottom") { // 在下面 if (config.useFoldAnimation) { SearchSuggestion.$el.root.setAttribute("data-popper-placement", "bottom-center"); } const top = targetRect.height + targetRect.top + config.topDistance + arrowHeight; SearchSuggestion.$el.root.removeAttribute("data-top-reverse"); SearchSuggestion.$el.root.style.bottom = ""; SearchSuggestion.$el.root.style.top = top + "px"; } let left = targetRect.left; const hintUIWidth = popsDOMUtils.width(SearchSuggestion.$el.$dropdownWrapper); if (hintUIWidth > documentWidth) { // 超出宽度 left = left + documentWidth - hintUIWidth; } SearchSuggestion.$el.root.style.left = left + "px"; // 如果更新前在下面的话且高度超出了屏幕 // 这时候会有滚动条,会造成位置偏移 // 更新后的位置却在上面,这时候的位置信息不对齐 // 需重新更新位置 // 此情况一般是config.position === "auto" if (checkPositonAgain) { SearchSuggestion.changeHintULElementPosition(target, !checkPositonAgain); } }, /** * 更新搜索建议框的width * 因为目标元素可能是动态隐藏的 * @param target 目标元素 */ changeHintULElementWidth(target = config.$target ?? config.$inputTarget) { const targetRect = target.getBoundingClientRect(); if (config.followTargetWidth) { SearchSuggestion.$el.$dropdownWrapper.style.width = targetRect.width + "px"; } else { SearchSuggestion.$el.$dropdownWrapper.style.width = config.width; } }, /** * 动态更新CSS */ updateDynamicCSS() { const cssText = SearchSuggestion.getDynamicCSS(); PopsSafeUtils.setSafeHTML(SearchSuggestion.$el.$dynamicCSS, cssText); }, /** * 数据项的数量改变时调用 * * - 更新css * - 更新建议框的宽度 * - 更新建议框的位置 */ updateStyleSheet() { // 更新z-index SearchSuggestion.updateDynamicCSS(); // 更新宽度 SearchSuggestion.changeHintULElementWidth(); // 更新位置 SearchSuggestion.changeHintULElementPosition(); }, /** * 添加搜索结果元素 * @param $item 项元素 */ addItem($item: HTMLElement | DocumentFragment) { SearchSuggestion.$el.$dropdownContainer.appendChild($item); }, /** * 更新页面显示的搜索结果 * @param updateData */ update(updateData: PopsSearchSuggestionData[] = []) { if (!Array.isArray(updateData)) { throw new TypeError("传入的数据不是数组"); } const data = updateData; // 清空已有的搜索结果 if (data.length) { SearchSuggestion.$data.isEmpty = false; if (config.toHideWithNotResult) { SearchSuggestion.show(); } SearchSuggestion.clear(true); // 添加进ul中 const fragment = document.createDocumentFragment(); data.forEach((item, index) => { const $item = SearchSuggestion.createSearchItemLiElement(item, index); SearchSuggestion.setSearchItemClickEvent($item); SearchSuggestion.setSearchItemSelectEvent($item); fragment.appendChild($item); }); SearchSuggestion.addItem(fragment); } else { // 清空 SearchSuggestion.clear(); } }, /** * 清空当前的搜索结果并显示无结果 * @param [onlyClearView=false] 是否仅清空元素,默认false */ clear(onlyClearView: boolean = false) { PopsSafeUtils.setSafeHTML(SearchSuggestion.$el.$dropdownContainer, ""); if (onlyClearView) { return; } SearchSuggestion.$data.isEmpty = true; let $noResult; if (typeof config.toSearhNotResultHTML === "string") { $noResult = popsDOMUtils.parseTextToDOM(config.toSearhNotResultHTML); } else { $noResult = config.toSearhNotResultHTML(); } SearchSuggestion.addItem($noResult); if (config.toHideWithNotResult) { SearchSuggestion.hide(); } }, /** * 隐藏搜索建议框 * @param useAnimationToHide 是否使用动画隐藏 */ hide(useAnimationToHide: boolean = false) { if (config.useFoldAnimation) { if (!useAnimationToHide) { // 去掉动画 popsDOMUtils.removeClassName(SearchSuggestion.$el.root, "el-zoom-in-top-animation"); } popsDOMUtils.addClassName(SearchSuggestion.$el.root, "el-zoom-in-top-animation"); popsDOMUtils.addClassName(SearchSuggestion.$el.root, "el-zoom-in-top-animation-hide"); popsDOMUtils.removeClassName(SearchSuggestion.$el.root, "el-zoom-in-top-animation-show"); } else { SearchSuggestion.$el.root.style.display = "none"; } }, /** * 显示搜索建议框 */ show() { SearchSuggestion.$el.root.style.display = ""; if (config.useFoldAnimation) { popsDOMUtils.addClassName(SearchSuggestion.$el.root, "el-zoom-in-top-animation"); popsDOMUtils.removeClassName(SearchSuggestion.$el.root, "el-zoom-in-top-animation-hide"); popsDOMUtils.addClassName(SearchSuggestion.$el.root, "el-zoom-in-top-animation-show"); } }, }; return SearchSuggestion; }, };