@wiajs/ui
Version:
wia app ui packages
403 lines (402 loc) • 13.6 kB
JavaScript
/** @jsxImportSource @wiajs/core */ import { jsx as _jsx } from "@wiajs/core/jsx-runtime";
import { Page, Event, Device as device } from '@wiajs/core';
import { log as Log } from '@wiajs/util';
const log = Log({
m: 'autoComplete'
}) // 创建日志实例
;
/**
* @typedef {import('jquery')} $
* @typedef {JQuery} Dom
*/ /** @typedef {object} Opts
* @prop {string | Dom} el -
* @prop {boolean} [status] - 状态
* @prop {boolean} [search] - 搜索
* @prop {boolean} [clear] - 清除
* @prop {number} [maxItems] - 显示数量,避免性能问题
* @prop {*[]} [data] - 数据
* @prop {HTMLElement[]} [refEl] - 关联元素,点击不关闭列表
* @prop {{url:string, token:string}} [source] - 显示数量,避免性能问题
*/ /** @typedef {object} Opt
* @prop {Dom} el -
* @prop {boolean} status
* @prop {boolean} search
* @prop {boolean} clear
* @prop {*[]} data - 数据
* @prop {number} maxItems - 显示数量,避免性能问题
* @prop {HTMLElement[]} refEl - 关联元素,点击不关闭列表
* @prop {{url:string, token:string}} source - 显示数量,避免性能问题
*/ /** @type {Opt} */ const def = {
el: 'autocompelete',
status: true,
search: true,
clear: true,
/** @type {*[]} */ data: [],
/** @type {HTMLElement[]} */ refEl: [],
maxItems: 50,
source: undefined
};
export default class Autocomplete extends Event {
/**
* 构造
* @param {Page} page 页面实例
* @param {*} opts 选项,激活名称
*/ constructor(page, opts){
/** @type{Opt} */ const opt = {
...def,
...opts
};
super(opt, [
page
]), this.currentFocus = -1 // 当前高亮选项索引
, this.isComposing = false // 是否在中文输入过程中
, this.lastValue = '' // 上一次的值(用于防抖)
;
const _ = this;
_.page = page // Page实例
;
_.opt = opt;
const { el, data } = opt;
if (data) _.data = data;
if (typeof el === 'string') el = $(opt.el);
if (el.dom) {
_.el = el;
el.dom.wiaAutocomplete = _;
_.wrapper = el.find('.ac-wrapper');
_.input = el.find('.ac-input');
_.btnClear = el.find('.ac-clear');
_.btnSearch = el.find('.ac-search');
_.dvStatus = el.find('.ac-status');
_.init();
_.bind();
}
}
init() {
const _ = this;
try {
const { el, opt, wrapper } = _;
const { status, search, clear } = opt;
if (clear) {
wrapper.append(/*#__PURE__*/ _jsx("button", {
type: "button",
class: "ac-clear",
children: /*#__PURE__*/ _jsx("i", {
class: "icon wiaicon",
children: ""
})
}));
_.btnClear = el.find('.ac-clear');
}
if (search) {
wrapper.append(/*#__PURE__*/ _jsx("button", {
type: "button",
class: "ac-search",
children: /*#__PURE__*/ _jsx("i", {
class: "icon wiaicon",
children: ""
})
}));
_.btnSearch = el.find('.ac-search');
}
if (status) {
wrapper.append(/*#__PURE__*/ _jsx("div", {
class: "ac-status"
}));
_.dvStatus = el.find('.ac-status');
}
el.append(/*#__PURE__*/ _jsx("div", {
class: "ac-list"
}));
_.dvList = el.find('.ac-list');
// 初始状态隐藏下拉框
_.hideList();
} catch (e) {
log.err(e, 'init');
}
}
bind() {
const _ = this;
try {
const { el, opt, wrapper, data } = _;
const { source } = opt;
_.input.focus((ev)=>{
_.showAllList();
});
// 输入事件监听
_.input.input(async function(ev) {
const inputValue = this.value.trim();
if (_.isComposing) // 中文输入过程中忽略
return;
// 更新清除按钮状态
_.upClearButton();
// 值未变化时忽略
if (inputValue === _.lastValue) return;
_.lastValue = inputValue;
// 空值处理
if (!inputValue.trim()) {
_.hideList();
_.hideStatus();
return;
}
await _.search(source, inputValue);
});
// 中文输入法开始
_.input.dom.addEventListener('compositionstart', ()=>{
_.isComposing = true;
_.input.addClass('composing');
_.showStatus('输入中...');
});
// 中文输入法结束
_.input.dom.addEventListener('compositionend', async function() {
_.isComposing = false;
_.input.removeClass('composing');
const inputValue = this.value.trim();
_.lastValue = inputValue;
// 更新清除按钮状态
_.upClearButton();
if (!inputValue) {
_.hideList();
_.hideStatus();
return;
}
await _.search(source, inputValue);
});
// 键盘事件监听
_.input.keydown((ev)=>{
const items = _.dvList.find('.ac-item');
if (items.length > 0) {
// 向下键
if (ev.key === 'ArrowDown') {
ev.preventDefault();
_.currentFocus = (_.currentFocus + 1) % items.length;
_.setActive(items);
} else if (ev.key === 'ArrowUp') {
ev.preventDefault();
_.currentFocus = (_.currentFocus - 1 + items.length) % items.length;
_.setActive(items);
} else if (ev.key === 'Enter') {
ev.preventDefault();
if (_.currentFocus > -1) {
items[_.currentFocus].click();
}
}
}
});
// 点击页面其他区域关闭下拉框
_.page.view.click((ev)=>{
if (!_.el.dom.contains(ev.target) && _.opt.refEl.every((el)=>!el.contains(ev.target))) {
_.hideList();
}
});
// 清除按钮事件
_.btnClear.click(()=>{
_.input.val('');
_.input.focus();
_.showAllList();
_.upClearButton();
_.hideStatus();
});
// 搜索按钮事件
_.btnSearch.click(()=>{
const inputValue = _.input.val().trim();
// 显示加载状态
_.showStatus('查询中...');
// 触发查询
setTimeout(()=>{
if (inputValue) {
const filteredData = _.filter(inputValue);
_.showList(filteredData, inputValue);
} else _.showAllList();
_.hideStatus();
}, 300);
});
} catch (e) {
log.err(e, 'bind');
}
}
/**
* 查询选项
* @param {*} source
* @param {string} inputValue
*/ async search(source, inputValue) {
const _ = this;
try {
if (!source?.url) return;
// 显示加载状态
_.showStatus('查询中...');
const { url, token } = source;
let { param } = source;
const tk = token ? $.store.get(token) : '';
if (param) param.value = inputValue;
else param = {
value: inputValue
};
const rs = await $.post(url, param, {
'x-wia-token': tk
});
// 输入完成后再触发查询
if (rs) {
_.data = rs;
const filteredData = _.filter(inputValue);
_.showList(filteredData, inputValue);
_.hideStatus();
}
} catch (e) {
log.err(e, 'search');
}
}
// 更新清除按钮状态
upClearButton() {
const _ = this;
if (_.input.val().trim()) {
_.btnClear.addClass('show');
} else {
_.btnClear.removeClass('show');
}
}
/**
* 过滤数据函数
* @param {*} input
* @returns
*/ filter(input) {
const _ = this;
let R = [];
try {
const { data } = _;
if (data?.length) {
if (Array.isArray(data[0])) R = data.filter((d)=>d[1].toLowerCase().includes(input.toLowerCase()));
else R = data.filter((d)=>d.toLowerCase().includes(input.toLowerCase()));
}
} catch (e) {
log.err(e, 'filter');
}
return R;
}
//
/**
* 显示所有结果
* @param {*[]} [data]
* @returns
*/ showAllList(data) {
const _ = this;
if (data?.length) _.data = data;
data = _.data;
if (!data?.length) return;
// 限制显示数量,避免性能问题
const { maxItems } = _.opt;
const rs = data.slice(0, maxItems);
_.dvList.empty();
for (const r of rs){
const el = document.createElement('div');
el.className = 'ac-item';
/** @type {string|number} */ let key;
let val = r;
if (Array.isArray(r) && r.length > 1) {
;
[key, val] = r;
$(el).data('key', key);
}
el.textContent = val;
// 点击事件
el.addEventListener('click', ()=>{
_.lastValue = val;
_.input.val(val);
if (key) _.input.data('key', key);
_.upClearButton();
_.hideList();
});
_.dvList.append(el);
}
if (data.length > maxItems) {
// 添加提示信息
const info = document.createElement('div');
info.className = 'ac-item';
info.innerHTML = `<i class="fas fa-info-circle"></i> 显示前 ${maxItems} 条,输入查看更多`;
_.dvList.append(info);
}
_.dvList.show();
_.currentFocus = -1;
}
/**
* 显示结果函数
* @param {string[]} data
* @param {*} inputValue
* @returns
*/ showList(data, inputValue) {
const _ = this;
if (data.length === 0) {
_.hideList();
_.dvList.html('<div class="autocomplete-item"><i class="fas fa-exclamation-circle"></i> 未找到匹配结果</div>');
_.dvList.show();
return;
}
_.dvList.empty();
for (const r of data){
const el = document.createElement('div');
el.className = 'ac-item';
/** @type {string|number} */ let key;
let val = r;
if (Array.isArray(r) && r.length > 1) {
;
[key, val] = r;
$(el).data('key', key);
}
// 高亮匹配字符
const regex = new RegExp(inputValue, 'gi');
const highlightedText = val.replace(regex, (match)=>`<span class="ac-highlight">${match}</span>`);
el.innerHTML = highlightedText;
// 点击事件
el.addEventListener('click', ()=>{
_.lastValue = val;
_.input.val(val);
if (key) _.input.data('key', key);
_.upClearButton();
_.hideList();
});
_.dvList.append(el);
}
_.dvList.show();
_.currentFocus = -1;
}
// 隐藏结果
hideList() {
const _ = this;
// debugger
_.dvList.hide();
}
/**
* 设置高亮选项
* @param {JQuery} items
*/ setActive(items) {
const _ = this;
if (!items?.length) return;
// 移除之前的高亮
for (const item of items.get())item.classList.remove('highlighted');
if (_.currentFocus >= 0 && _.currentFocus < items.length) {
items[_.currentFocus].classList.add('highlighted');
// 滚动到可见区域
items[_.currentFocus].scrollIntoView({
block: 'nearest',
behavior: 'smooth'
});
}
}
/**
* 显示状态指示器
* @param {string} text
*/ showStatus(text) {
const _ = this;
_.btnClear.removeClass('show');
_.dvStatus.dom.textContent = text;
_.dvStatus.addClass('show');
if (text === '查询中...') {
_.dvStatus.html(`${text}<span class="loading-spinner"></span>`);
}
}
// 隐藏状态指示器
hideStatus() {
const _ = this;
_.dvStatus.removeClass('show');
_.upClearButton();
}
}