初次实现,可能有考虑不周的地方,欢迎各位大佬提点~
基本功能介绍:
1、允许一个页面内创建多个实例,互相之间不会有影响
2、允许 JSON 数据格式写入
3、允许鼠标悬浮触发或点击触发下级及联数据展示
4、允许设置默认值
5、支持输入框提示
6、提供选中后的回调函数
Cascader.js
class Cascader {
constructor(selector, options = {}) {
this.container = document.querySelector(selector);
if (!this.container) throw new Error(`Container not found: ${selector}`);
this.options = options;
this.value = [];
this.labelValue = []; // 用于存储选中的标签值
this.menus = null;
// 标志是否处于初始化阶段
this.initializing = true;
Cascader.injectStyles();
this.init();
// 设置默认值
const valueElement = this.container.querySelector(".cascader-value");
if (options.defaultValue && options.defaultValue.length) {
this.setDefaultValue(options.defaultValue);
valueElement.classList.remove("placeholderStyles");
} else if (options.placeholder) {
// 设置提示字体颜色
valueElement.classList.add("placeholderStyles");
}
// 初始化完成
this.initializing = false;
}
static injectStyles() {
if (Cascader._stylesInjected) return;
Cascader._stylesInjected = true;
const styles = `
.cascader-container { position: relative; }
.cascader-value { border: 1px solid #ccc; cursor: pointer; }
.placeholderStyles {color: #999999;}
.cascader-menus { position: absolute; top: 100%; left: 0; display: flex; background: #fff; box-shadow: 0px 6px 16px 0px rgba(0,0,0,0.08), 0px 3px 6px -4px rgba(0,0,0,0.12), 0px 9px 28px 8px rgba(0,0,0,0.05);border-radius: 4px;border: 0px solid rgba(0,0,0,0.88); z-index: 1000; }
.cascader-menu { margin: 0; padding: 0; list-style: none; min-width: 150px; border-right: 1px solid rgba(229, 229, 229, 1); }
.cascader-menu:last-child { border-right: none; }
.cascader-menu-item { padding: 10px; cursor: pointer; }
.cascader-menu-item:hover { background: #f0f0f0; }
.cascader-menu-item.selected {background: #fff7f5;color: #fa6634; }
.cascader-menu-item.selected span {color: #fa6634!important; }
.cascader-menu-item.disabled { color: #ccc; cursor: not-allowed; }
`;
const styleElement = document.createElement("style");
styleElement.textContent = styles;
document.head.appendChild(styleElement);
}
init() {
this.container.innerHTML = `
<div class="cascader-container">
<div class="cascader-value">${
this.options.placeholder || "Select"
}</div>
<div class="cascader-menus"></div>
</div>
`;
this.menus = this.container.querySelector(".cascader-menus");
this.attachEvents();
}
attachEvents() {
const valueElement = this.container.querySelector(".cascader-value");
valueElement.addEventListener("click", () => {
// valueElement.classList.add("cascader-selected-value");
this.toggleMenu();
});
const trigger = this.options.trigger || "click"; // 支持 hover 和 click
this.menus.addEventListener("click", (event) =>
this.handleMenuEvent(event)
);
if (trigger === "hover") {
this.menus.addEventListener("mouseover", (event) =>
this.handleMenuEvent(event, true)
);
}
document.addEventListener("click", (event) => {
// 优化悬浮记值
if (!this.container.contains(event.target)) {
// console.log(111);
// setTimeout(() => {
// valueElement.classList.remove("cascader-selected-value");
// },100)
this.closeMenu();
}
});
}
handleMenuEvent(event, isHover = false) {
const item = event.target.closest(".cascader-menu-item");
if (item && !item.classList.contains("disabled")) {
const level = parseInt(item.dataset.level, 10);
const value = item.dataset.value;
// 判断是否为叶子节点(即是否存在 children)
const isLeaf = item.querySelector("span") === null;
if (isLeaf && isHover && this.options.trigger === "hover") {
return;
}
if ((isHover && this.options.trigger === "hover") || !isHover) {
const siblingItems = item.parentNode.querySelectorAll(
".cascader-menu-item"
);
siblingItems.forEach((sibling) => sibling.classList.remove("selected"));
// 为当前选中项添加样式
item.classList.add("selected");
// 更新选择项
let textContent = item.textContent.trim();
if (item.querySelector("span")) {
textContent = textContent.slice(0, -1);
}
// 如果是末级菜单,且是点击触发,则更新选中项
this.updateSelection(level, value, textContent);
}
}
}
toggleMenu() {
// 根据当前选中的值进行回显
if (this.value.length) {
this.setDefaultValue(this.value);
}
const isVisible = this.menus.style.display === "flex";
this.menus.style.display = isVisible ? "none" : "flex";
if (!isVisible) {
// 如果有默认值,展开所有菜单
if (this.value && this.value.length > 0) {
this.expandDefaultMenus();
} else {
this.renderMenu();
}
}
}
expandDefaultMenus() {
let options = this.options.options;
let currentLevel = 0;
this.value.forEach((value) => {
const option = options.find((opt) => opt.value == value);
if (option) {
// 渲染当前级菜单
this.renderMenu(currentLevel, options);
// 更新到下一层级
options = option.children || [];
currentLevel++;
}
});
}
closeMenu() {
this.menus.style.display = "none";
}
// 渲染菜单时支持多级联动
renderMenu(level = 0, options = this.options.options) {
// 清除当前级别之后的菜单
const menus = this.menus.querySelectorAll(".cascader-menu");
menus.forEach((menu, index) => {
if (index >= level) menu.remove();
});
const menu = document.createElement("ul");
menu.classList.add("cascader-menu");
options.forEach((option) => {
const item = document.createElement("li");
item.classList.add("cascader-menu-item");
if (option.disabled) item.classList.add("disabled");
item.dataset.value = option.value;
item.dataset.level = level;
item.textContent = option.label;
// 判断当前项是否已选中
if (this.value[level] == option.value) {
item.classList.add("selected");
}
// 如果该项有子菜单,添加箭头
if (option.children) {
const arrow = document.createElement("span");
arrow.textContent = ">";
arrow.style.float = "right";
arrow.style.color = "rgba(0, 0, 0, 0.45)";
item.appendChild(arrow);
}
menu.appendChild(item);
});
this.menus.appendChild(menu);
// 为超出宽度的项添加 title 提示
this.menus.querySelectorAll(".cascader-menu-item").forEach((item) => {
if (item.scrollWidth > item.offsetWidth) {
// 判断内容是否超出
item.setAttribute("title", item.textContent.trim());
}
});
}
setDefaultValue(defaultValue) {
let options = this.options.options;
let currentLevel = 0;
defaultValue.forEach((value) => {
const option = options.find((opt) => opt.value == value);
if (option) {
// 保存选中的值和标签
this.value[currentLevel] = value;
this.labelValue[currentLevel] = option.label;
// 渲染当前级菜单
this.renderMenu(currentLevel, options);
// 更新到下一层级
options = option.children || [];
currentLevel++; // 递增当前级别
}
});
// 更新显示;
this.updateValueDisplay();
}
updateSelection(level, value, label) {
this.value = this.value.slice(0, level);
this.labelValue = this.labelValue.slice(0, level);
this.value.push(value);
this.labelValue.push(label);
const option = this.findOption(value, this.options.options);
// 更新样式:清除同一级的选中状态,设置当前选中项的样式
const menuItems = this.menus.querySelectorAll(
`.cascader-menu-item[data-level="${level}"]`
);
menuItems.forEach((item) => {
item.classList.remove("selected");
if (item.dataset.value === value) {
item.classList.add("selected");
}
});
if (option && option.children) {
this.renderMenu(level + 1, option.children);
} else {
this.closeMenu();
this.updateValueDisplay();
if (!this.initializing && this.options.onChange) {
this.options.onChange(this.value, this.labelValue);
}
}
}
findOption(value, options) {
for (const option of options) {
if (option.value === value) return option;
if (option.children) {
const found = this.findOption(value, option.children);
if (found) return found;
}
}
return null;
}
updateValueDisplay() {
const separator = this.options.separator || " / ";
const valueElement = this.container.querySelector(".cascader-value");
valueElement.classList.remove("placeholderStyles"); // 已选择新值
valueElement.textContent = this.labelValue.join(separator);
}
}
// 暴露为全局对象
if (typeof window !== "undefined") {
window.Cascader = Cascader;
} else if (typeof global !== "undefined") {
global.Cascader = Cascader;
}
使用如下
<div class="main-search-select label-select-search "id="tree-select-list" ></div>
const transformTreeData = (data) => {
return data.map((item) => ({
value: item.id,
label: item.name,
children: item.children ? transformTreeData(item.children) : undefined,
}));
};
// 树状结构数据
const treeData = [
{
id: "Chinese Mainland",
name: "Chinese Mainland",
children: [
{ id: 1, name: "Dairy Product" },
{ id: 26, name: "Beverage" },
{ id: 29, name: "Alcoholic Beverage" },
{ id: 31, name: "Health Supplement" },
{ id: 35, name: "Infant Formula" },
{ id: 38, name: "Food for Special Medical Purpose" },
],
},
{
id: "Hong Kong S.A.R.",
name: "Hong Kong S.A.R.",
children: [
{ id: 28, name: "Beverage" },
{ id: 34, name: "Milk and Milk Product " },
{ id: 36, name: "Infant and Follow-up Formula" },
{ id: 53, name: "Alcohol Drink" },
{ id: 55, name: "Health Food" },
{ id: 62, name: "Formula for Special Medical Purposes" },
],
},
{
id: "South Korea",
name: "South Korea",
children: [
{ id: 19, name: "Dairy Product" },
{ id: 21, name: "Beverage" },
{ id: 44, name: "Liquor " },
{ id: 46, name: "Health Functional Food" },
{ id: 49, name: "Infant formula" },
{ id: 59, name: "Food for Special Medical Purpose" },
],
},
{
id: "Japan",
name: "Japan",
children: [
{ id: 90, name: "Dairy Product" },
{ id: 95, name: "Beverage" },
{ id: 98, name: "Alcoholic Beverage" },
{ id: 106, name: "Health Supplement" },
{ id: 108, name: "Infant Formula" },
{ id: 110, name: "Food for Special Dietary Uses" },
],
},
{
id: "Singapore",
name: "Singapore",
children: [
{ id: 113, name: "Dairy Product" },
{ id: 114, name: "Beverage" },
{ id: 115, name: "Alcoholic Drink" },
{ id: 116, name: "Health Supplement" },
{ id: 117, name: "Infant Formula" },
{ id: 118, name: "Special Purpose Food" },
],
},
{
id: "Canada",
name: "Canada",
children: [
{ id: 45, name: "Dairy Product" },
{ id: 47, name: "Beverage" },
{ id: 50, name: "Alcoholic Beverage" },
{ id: 54, name: "Natural Health Product" },
{ id: 56, name: "Infant Formula" },
{ id: 57, name: "Foods for Special Dietary Use (FSDU)" },
],
},
{
id: "USA",
name: "USA",
children: [
{ id: 63, name: "Dairy Product" },
{ id: 77, name: "Beverage" },
{ id: 78, name: "Alcoholic Beverage " },
{ id: 79, name: "Dietary Supplement" },
{ id: 80, name: "Infant Formula " },
{ id: 82, name: "Medical Food" },
],
}
];
const newTreeData = transformTreeData(treeData);
var cascader1 = new Cascader("#tree-select-list", {
width: 1200,
placeholder:
"请输入",
options: newTreeData,
trigger: "hover", // 子级及联展开的触发方式
separator: " > ", // 自定义连接符
// defaultValue: [ '1','19'], // 默认数据
onChange: function (value, labelValue) {
// value 输入为 url
// window.location.href = value[value.length - 1];
console.log(value, labelValue);
},
});
CSS 样式
.cascader-container .cascader-menus {
display: none;
}
.cascader-container .cascader-menus.active {
z-index: 999;
height: 196px;
}
.cascader-container {
border: none !important;
}
.cascader-container .cascader-menus .cascader-menu .cascader-menu-item.active {
background: #fff7f5 !important;
}
.cascader-menu-item.active .cascader-menu-item-label {
color: #fa6634;
}
.cascader-container .cascader-menus .cascader-menu {
width: 393px;
max-height: 300px;
overflow: auto;
}
.cascader-menu-item .cascader-menu-item-label {
width: 335px;
color: #293a4c;
white-space: normal !important;
word-wrap: break-word;
}
.cascader-container .cascader-menus .cascader-menu .cascader-menu-item {
position: relative;
height: auto !important;
padding: 10px 40px 10px 16px !important;
line-height: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cascader-container .cascader-menus .cascader-menu .cascader-menu-item span {
position: absolute;
right: 16px;
}
.cascader-menus.active {
top: 59px;
}
.cascader-container .cascader-menus .cascader-menu {
padding: 8px 0px !important;
width: 292px;
}
.cascader-container {
width: 100%;
height: 100%;
line-height: 38px;
}
.cascader-value {
border: none !important;
padding: 0 40px 0 26px !important;
}
.cascader-menus {
top: 55px !important;
}
.cascader-menus::-webkit-scrollbar {
width: 6px; /* 滚动条宽度 */
}
.cascader-menus::-webkit-scrollbar-thumb {
background: #cccccc;
border-radius: 4px; /* 圆角 */
}
.cascader-value::after {
position: absolute;
top: 50%;
right: 16px;
display: block;
color: #656565;
font-size: 12px;
font-family: "iconfont";
border-radius: 2px;
transform: rotate(0deg) translateY(-50%);
content: "\e68e";
}
.cascader-selected-value::after {
transform: rotate(180deg) translateY(50%);
}