<template>
  <div
    :class="[ns.b(), ns.is('disabled', disabled), ns.is('multiple', multiple)]"
    :style="themeStyle"
    ref="selectRef"
  >
    <div
      :class="[ns.e('trigger'), ns.is('active', visible)]"
      @click="handleTriggerClick"
      @mouseenter="showClear = true"
      @mouseleave="showClear = false"
    >
      <div :class="ns.e('tags')">
        <template v-if="multiple && Array.isArray(modelValue)">
          <!-- 如果开启折叠，仅渲染 maxCollapseTags 数量的标签 -->
          <div
            v-for="item in collapseTags ? selectedLabels.slice(0, maxCollapseTags) : selectedLabels"
            :key="item.value"
            :class="ns.e('tag')"
          >
            <span>{{ item.label }}</span>
            <span :class="ns.e('tag-close')" @click.stop="removeTag(item.value)">
              <svg viewBox="0 0 1024 1024" width="1em" height="1em">
                <path
                  fill="currentColor"
                  d="M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896zm0 393.664L407.936 353.6a38.4 38.4 0 1 0-54.336 54.336L457.664 512 353.6 616.064a38.4 38.4 0 1 0 54.336 54.336L512 566.336 616.064 670.4a38.4 38.4 0 1 0 54.336-54.336L566.336 512 670.4 407.936a38.4 38.4 0 1 0-54.336-54.336L512 457.664z"
                />
              </svg>
            </span>
          </div>
          <YhTooltip
            v-if="collapseTags && selectedLabels.length > maxCollapseTags && collapseTagsTooltip"
            placement="top"
            :class="[ns.e('tag'), ns.m('info')]"
          >
            <span>+ {{ selectedLabels.length - maxCollapseTags }}</span>
            <template #content>
              <div
                :class="ns.e('tooltip-content')"
                style="display: flex; flex-wrap: wrap; gap: 4px; padding: 4px"
              >
                <span
                  v-for="(item, idx) in selectedLabels.slice(maxCollapseTags)"
                  :key="idx"
                  :class="ns.e('tooltip-tag')"
                  style="white-space: nowrap"
                >
                  {{ item.label }}
                </span>
              </div>
            </template>
          </YhTooltip>
          <div
            v-else-if="collapseTags && selectedLabels.length > maxCollapseTags"
            :class="[ns.e('tag'), ns.m('info')]"
          >
            <span>+ {{ selectedLabels.length - maxCollapseTags }}</span>
          </div>
        </template>

        <input
          ref="inputRef"
          v-model="query"
          :placeholder="inputPlaceholder"
          :disabled="disabled"
          :readonly="!filterable"
          :class="[ns.e('inner'), ns.is('invisible', !multiple && hasValue && !visible && !query)]"
          autocomplete="off"
          @input="handleInput"
          @focus="handleInputFocus"
        />

        <span :class="ns.e('suffix')">
          <span
            v-if="showClear && clearable && hasValue"
            :class="ns.e('clear')"
            @click.stop="handleClear"
          >
            <svg viewBox="0 0 1024 1024" width="1em" height="1em">
              <path
                fill="currentColor"
                d="M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896zm0 393.664L407.936 353.6a38.4 38.4 0 1 0-54.336 54.336L457.664 512 353.6 616.064a38.4 38.4 0 1 0 54.336 54.336L512 566.336 616.064 670.4a38.4 38.4 0 1 0 54.336-54.336L566.336 512 670.4 407.936a38.4 38.4 0 1 0-54.336-54.336L512 457.664z"
              />
            </svg>
          </span>
          <span :class="[ns.e('caret'), ns.is('reverse', visible)]">
            <svg viewBox="0 0 1024 1024" width="1em" height="1em">
              <path
                fill="currentColor"
                d="M831.872 340.864L512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.592 30.592 0 0 0-42.752 0z"
              />
            </svg>
          </span>
        </span>
      </div>

      <div v-if="!multiple && hasValue && !visible && !query" :class="ns.e('selected-label')">
        {{ singleLabel }}
      </div>
    </div>

    <!-- Dropdown -->
    <Teleport to="body" :disabled="!teleported">
      <Transition name="yh-zoom-in-top">
        <div
          v-if="visible"
          ref="popperRef"
          :class="[ns.e('popper'), popperClass]"
          :style="dropdownStyle"
        >
          <div :class="ns.e('dropdown')">
            <div
              ref="virtualWrapperRef"
              :class="ns.e('options-wrapper')"
              :style="{
  maxHeight: heightNumber + 'px',
  overflow: 'auto'
}"
              @scroll="handleScroll"
            >
              <!-- 列表区域 -->
              <div v-if="flatData.length > 0" :style="listContainerStyle">
                <div :style="listShiftStyle">
                  <div
                    v-for="node in renderedNodes"
                    :key="node.key"
                    :class="[ns.e('node'), ns.is('selected', node.checked), ns.is('disabled', node.disabled)]"
                    :style="{
  paddingLeft: `${node.level * indent + 8}px`,
  height: itemSize + 'px'
}"
                    @click.stop="handleNodeClick(node, $event)"
                  >
                    <div :class="ns.e('node-content')">
                      <!-- 展开图标 -->
                      <span
                        :class="[ns.e('expand-icon'), ns.is('leaf', node.isLeaf), ns.is('expanded', node.expanded)]"
                        @click.stop="toggleExpand(node)"
                      >
                        <svg v-if="!node.loading" viewBox="0 0 1024 1024" width="1em" height="1em">
                          <path
                            fill="currentColor"
                            d="M340.864 149.312l470.4 362.688-470.4 362.688c-26.432 20.416-64 1.6-64-32V181.312c0-33.6 37.568-52.416 64-32z"
                          />
                        </svg>
                        <svg
                          v-else
                          :class="ns.is('loading')"
                          viewBox="0 0 1024 1024"
                          width="1em"
                          height="1em"
                        >
                          <path
                            fill="currentColor"
                            d="M512 64a32 32 0 0 1 32 32v192a32 32 0 0 1-64 0V96a32 32 0 0 1 32-32zm0 640a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V736a32 32 0 0 1 32-32zm448-192a32 32 0 0 1-32 32H736a32 32 0 1 1 0-64h192a32 32 0 0 1 32 32zm-640 0a32 32 0 0 1-32 32H96a32 32 0 0 1 0-64h192a32 32 0 0 1 32 32z"
                          />
                        </svg>
                      </span>

                      <!-- 复选框 -->
                      <span
                        v-if="showCheckbox"
                        :class="[ns.e('checkbox'), ns.is('checked', node.checked), ns.is('indeterminate', node.indeterminate), ns.is('disabled', node.disabled)]"
                      >
                        <span :class="ns.e('checkbox-inner')">
                          <svg
                            v-if="node.checked && !node.indeterminate"
                            viewBox="0 0 1024 1024"
                            width="1em"
                            height="1em"
                          >
                            <path
                              fill="currentColor"
                              d="M406.656 706.944L195.84 496.256a32 32 0 1 0-45.248 45.248l256 256 512-512a32 32 0 0 0-45.248-45.248L406.592 706.944z"
                            />
                          </svg>
                          <svg
                            v-else-if="node.indeterminate"
                            viewBox="0 0 1024 1024"
                            width="1em"
                            height="1em"
                          >
                            <path fill="currentColor" d="M192 448h640v128H192z" />
                          </svg>
                        </span>
                      </span>

                      <!-- 文本 -->
                      <span :class="ns.e('node-label')">
                        <slot name="default" :node="node" :data="node.raw">
                          {{ node.label }}
                        </slot>
                      </span>
                    </div>
                  </div>
                </div>
              </div>

              <div v-else :class="ns.e('empty')">{{ emptyText || t("treeselect.emptyText") }}</div>
            </div>
          </div>
        </div>
      </Transition>
    </Teleport>
  </div>
</template>

<script setup>
import { ref, computed, watch, nextTick, onMounted, onBeforeUnmount } from "vue";
import { useNamespace, useLocale } from "@yh-ui/hooks";
import { useComponentTheme } from "@yh-ui/theme";
import YhTooltip from "../../tooltip";
import { treeSelectProps, treeSelectEmits } from "./tree-select";
import { useTree } from "./use-tree";
defineOptions({ name: "YhTreeSelect" });
const props = defineProps(treeSelectProps);
const emit = defineEmits(treeSelectEmits);
const ns = useNamespace("tree-select");
const { t } = useLocale();
const { themeStyle } = useComponentTheme(
  "tree-select",
  computed(() => props.themeOverrides)
);
const visible = ref(false);
const query = ref("");
const dropdownStyle = ref({});
const inputRef = ref(null);
const selectRef = ref(null);
const popperRef = ref(null);
const virtualWrapperRef = ref(null);
const scrollTop = ref(0);
const showClear = ref(false);
const { flatData, nodeMap, mapVersion, checkNode, toggleExpand, filter, getNode } = useTree(
  props,
  emit
);
const heightNumber = computed(() => {
  if (typeof props.height === "number") return props.height;
  const parsed = parseInt(String(props.height));
  return isNaN(parsed) ? 274 : parsed;
});
const renderedNodes = computed(() => {
  if (!props.virtual) return flatData.value;
  const start = Math.max(0, Math.floor(scrollTop.value / props.itemSize) - 5);
  const end = Math.min(
    flatData.value.length,
    start + Math.ceil(heightNumber.value / props.itemSize) + 10
  );
  return flatData.value.slice(start, end);
});
const listContainerStyle = computed(() => {
  if (!props.virtual) return {};
  return {
    height: `${flatData.value.length * props.itemSize}px`,
    position: "relative"
  };
});
const listShiftStyle = computed(() => {
  if (!props.virtual) return {};
  const start = Math.max(0, Math.floor(scrollTop.value / props.itemSize) - 5);
  return {
    transform: `translateY(${start * props.itemSize}px)`,
    position: "absolute",
    top: 0,
    left: 0,
    right: 0
  };
});
const handleScroll = (e) => {
  if (props.virtual) {
    scrollTop.value = e.target.scrollTop;
  }
};
const hasValue = computed(() => {
  if (props.multiple) return Array.isArray(props.modelValue) && props.modelValue.length > 0;
  return props.modelValue !== void 0 && props.modelValue !== null && props.modelValue !== "";
});
const singleLabel = computed(() => {
  void mapVersion.value;
  if (props.multiple || !hasValue.value) return "";
  const node = getNode(props.modelValue);
  return node ? node.label : String(props.modelValue ?? "");
});
const inputPlaceholder = computed(() => {
  if (props.multiple)
    return hasValue.value ? "" : props.placeholder ?? t("treeselect.placeholder");
  if (hasValue.value && props.filterable && visible.value) return singleLabel.value;
  return props.placeholder ?? t("treeselect.placeholder");
});
const selectedLabels = computed(() => {
  void mapVersion.value;
  const value = props.modelValue;
  const keys = Array.isArray(value) ? value : value !== void 0 && value !== null && value !== "" ? [value] : [];
  return keys.map((k) => {
    const node = getNode(k);
    return { value: k, label: node ? node.label : String(k) };
  });
});
const handleTriggerClick = () => {
  if (props.disabled) return;
  visible.value = !visible.value;
};
const closeMenu = () => {
  visible.value = false;
  query.value = "";
  filter("");
};
const handleOutsideClick = (e) => {
  const target = e.target;
  if (visible.value && selectRef.value && !selectRef.value.contains(target) && popperRef.value && !popperRef.value.contains(target)) {
    closeMenu();
  }
};
const handleNodeClick = (node, e) => {
  if (node.disabled) return;
  emit("node-click", node.raw, node, e);
  if (props.lazy && !node.loaded && !node.isLeaf) {
    toggleExpand(node);
    return;
  }
  if (props.showCheckbox || props.checkStrictly || props.checkOnClickNode) {
    checkNode(node, !node.checked);
    if (!props.multiple && !props.showCheckbox) {
      nextTick(() => closeMenu());
    }
  } else {
    if (node.isLeaf) {
      checkNode(node, !node.checked);
      if (!props.multiple) {
        nextTick(() => closeMenu());
      }
    } else if (props.expandOnClickNode) {
      toggleExpand(node);
    }
  }
};
const removeTag = (key) => {
  const node = getNode(key);
  if (node) checkNode(node, false);
  else if (Array.isArray(props.modelValue)) {
    emit(
      "update:modelValue",
      props.modelValue.filter((v) => v !== key)
    );
  }
};
const handleClear = () => {
  emit("update:modelValue", props.multiple ? [] : void 0);
  emit("clear");
  nextTick(() => {
    nodeMap.value.forEach((n) => {
      n.checked = false;
      n.indeterminate = false;
    });
    mapVersion.value++;
  });
};
const handleInput = () => {
  if (!visible.value) visible.value = true;
  filter(query.value);
};
const handleInputFocus = () => {
  if (!props.disabled && props.filterable) visible.value = true;
};
const updatePopper = () => {
  nextTick(() => {
    if (selectRef.value) {
      const rect = selectRef.value.getBoundingClientRect();
      const styles = window.getComputedStyle(selectRef.value);
      const primary = styles.getPropertyValue("--yh-color-primary").trim();
      const primaryRgb = styles.getPropertyValue("--yh-color-primary-rgb").trim();
      dropdownStyle.value = {
        width: `${rect.width}px`,
        position: "fixed",
        top: `${rect.bottom + 4}px`,
        left: `${rect.left}px`,
        zIndex: "2000",
        "--yh-color-primary": primary,
        "--yh-color-primary-rgb": primaryRgb
      };
    }
  });
};
watch(visible, (val) => {
  emit("visible-change", val);
  if (val) {
    scrollTop.value = 0;
    updatePopper();
    window.addEventListener("click", handleOutsideClick, true);
    window.addEventListener("scroll", updatePopper, true);
    window.addEventListener("resize", updatePopper);
    nextTick(() => {
      if (virtualWrapperRef.value) virtualWrapperRef.value.scrollTop = 0;
    });
  } else {
    window.removeEventListener("click", handleOutsideClick, true);
    window.removeEventListener("scroll", updatePopper, true);
    window.removeEventListener("resize", updatePopper);
  }
});
onMounted(() => {
  if (visible.value) updatePopper();
});
onBeforeUnmount(() => {
  window.removeEventListener("click", handleOutsideClick, true);
  window.removeEventListener("scroll", updatePopper, true);
  window.removeEventListener("resize", updatePopper);
});
</script>

<style>
@charset "UTF-8";
/**
 * YH-UI Sass Variables
 * 原具体的 CSS 变量输出规则 (:root, html.dark 等) 已移至 root.scss，以防止各组件样式编译时产生重复的全局变量 CSS 冗余。
 */
.yh-tree-select {
  position: relative;
  display: inline-block;
  width: 100%;
  --yh-tree-select-node-hover-bg: var(--yh-fill-color-light, #f5f7fa);
  --yh-tree-select-node-active-bg: var(--yh-color-primary-light-9, #ecf5ff);
  --yh-tree-select-active-color: var(--yh-color-primary, #409eff);
}
.yh-tree-select__trigger {
  position: relative;
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  border: 1px solid var(--yh-border-color, #dcdfe6);
  border-radius: var(--yh-border-radius-base, 4px);
  padding: 0 12px;
  min-height: 32px;
  cursor: pointer;
  transition: all 0.25s cubic-bezier(0.645, 0.045, 0.355, 1);
  background-color: var(--yh-bg-color, #fff);
  box-sizing: border-box;
}
.yh-tree-select__trigger:hover:not(.is-disabled) {
  border-color: var(--yh-border-color-hover, #c0c4cc);
}
.yh-tree-select__trigger.is-active {
  border-color: var(--yh-color-primary, #409eff) !important;
  box-shadow: 0 0 0 2px rgba(var(--yh-color-primary-rgb), 0.15);
  outline: none;
}

.yh-tree-select__tags {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  flex: 1;
  overflow: hidden;
  padding: 3px 0;
  min-width: 0;
}

.yh-tree-select__tag {
  display: inline-flex;
  align-items: center;
  background: var(--yh-fill-color-light, #f4f4f5);
  border: 1px solid var(--yh-border-color-light, #e9e9eb);
  border-radius: 4px;
  padding: 0 8px;
  height: 24px;
  line-height: 22px;
  font-size: 12px;
  color: var(--yh-text-color-regular, #606266);
  white-space: nowrap;
  box-sizing: border-box;
}
.yh-tree-select__tag-close {
  margin-left: 4px;
  cursor: pointer;
  font-size: 14px;
  display: flex;
  align-items: center;
  color: var(--yh-text-color-placeholder);
  transition: color 0.2s;
}
.yh-tree-select__tag-close:hover {
  color: var(--yh-color-danger, #f56c6c);
}

.yh-tree-select__tag--info {
  background-color: var(--yh-fill-color-light, #f4f4f5);
  color: var(--yh-text-color-secondary, #909399);
  border-color: var(--yh-border-color-light, #e9e9eb);
}

.yh-tree-select__inner {
  flex: 1;
  min-width: 4px;
  width: 0;
  border: none !important;
  outline: none !important;
  padding: 0;
  margin: 0;
  color: var(--yh-text-color-regular, #606266);
  font-size: 14px;
  background: transparent;
  cursor: pointer;
}
.yh-tree-select__inner::placeholder {
  color: var(--yh-text-color-placeholder, #a8abb2);
}
.yh-tree-select__inner.is-invisible {
  opacity: 0;
}

.yh-tree-select__suffix {
  display: flex;
  align-items: center;
  gap: 8px;
  color: var(--yh-text-color-placeholder, #a8abb2);
  flex-shrink: 0;
}

.yh-tree-select__caret {
  transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
  font-size: 14px;
  display: flex;
  align-items: center;
}
.yh-tree-select__caret.is-reverse {
  transform: rotate(180deg);
}

.yh-tree-select__clear {
  cursor: pointer;
  font-size: 14px;
  display: flex;
  align-items: center;
  transition: color 0.2s;
}
.yh-tree-select__clear:hover {
  color: var(--yh-text-color-secondary, #909399);
}

.yh-tree-select__selected-label {
  position: absolute;
  left: 12px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--yh-text-color-regular, #606266);
  font-size: 14px;
  pointer-events: none;
  white-space: nowrap;
  max-width: calc(100% - 40px);
  overflow: hidden;
  text-overflow: ellipsis;
}

.yh-tree-select__popper {
  position: fixed;
  z-index: var(--yh-z-index-popup, 2000);
  margin-top: 8px;
  filter: drop-shadow(var(--yh-box-shadow-light));
  animation: yh-tree-select-fade-in 0.2s cubic-bezier(0.23, 1, 0.32, 1);
}

.yh-tree-select__dropdown {
  background: var(--yh-bg-color-overlay, #fff);
  border: 1px solid var(--yh-border-color-lighter, #ebeef5);
  border-radius: 8px;
  overflow: hidden;
  backdrop-filter: blur(10px);
}

.yh-tree-select__options-wrapper {
  scrollbar-width: thin;
}
.yh-tree-select__options-wrapper::-webkit-scrollbar {
  width: 6px;
}
.yh-tree-select__options-wrapper::-webkit-scrollbar-thumb {
  background: var(--yh-border-color-lighter);
  border-radius: 3px;
}

.yh-tree-select__empty {
  padding: 24px 0;
  text-align: center;
  color: var(--yh-text-color-placeholder, #a8abb2);
  font-size: 14px;
}

.yh-tree-select__node {
  display: flex;
  align-items: center;
  cursor: pointer;
  transition: background 0.25s, color 0.25s;
  font-size: 14px;
  color: var(--yh-text-color-regular, #606266);
  position: relative;
}
.yh-tree-select__node:hover {
  background-color: var(--yh-tree-select-node-hover-bg);
}
.yh-tree-select__node.is-selected {
  color: var(--yh-tree-select-active-color);
  background-color: var(--yh-tree-select-node-active-bg);
  font-weight: 600;
}
.yh-tree-select__node.is-selected::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background-color: var(--yh-tree-select-active-color);
}
.yh-tree-select__node.is-disabled {
  cursor: not-allowed;
  opacity: 0.5;
  background-color: transparent !important;
}
.yh-tree-select__node-content {
  display: flex;
  align-items: center;
  width: 100%;
  height: 100%;
}

.yh-tree-select__node-label {
  flex: 1;
  margin-left: 6px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.yh-tree-select__expand-icon {
  width: 24px;
  height: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  color: var(--yh-text-color-placeholder);
  transition: transform 0.3s;
  flex-shrink: 0;
  cursor: pointer;
}
.yh-tree-select__expand-icon:hover {
  color: var(--yh-text-color-regular);
}
.yh-tree-select__expand-icon.is-expanded {
  transform: rotate(90deg);
}
.yh-tree-select__expand-icon.is-leaf {
  visibility: hidden;
}

.yh-tree-select__checkbox {
  margin-right: 8px;
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  flex-shrink: 0;
}
.yh-tree-select__checkbox-inner {
  width: 14px;
  height: 14px;
  border: 1px solid var(--yh-border-color);
  border-radius: 3px;
  background: var(--yh-bg-color);
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}

.yh-tree-select__checkbox.is-checked .yh-tree-select__checkbox-inner {
  background-color: var(--yh-color-primary, #409eff);
  border-color: var(--yh-color-primary, #409eff);
}

.yh-tree-select__checkbox.is-indeterminate .yh-tree-select__checkbox-inner {
  background-color: var(--yh-color-primary, #409eff);
  border-color: var(--yh-color-primary, #409eff);
}

.yh-tree-select__checkbox.is-disabled {
  cursor: not-allowed;
}
.yh-tree-select__checkbox.is-disabled .yh-tree-select__checkbox-inner {
  background-color: var(--yh-fill-color-light);
  border-color: var(--yh-border-color-lighter);
}

.yh-tree-select.is-loading {
  animation: yh-tree-select-rotate 2s linear infinite;
}
.yh-tree-select.is-disabled {
  cursor: not-allowed;
}
.yh-tree-select.is-disabled .yh-tree-select__trigger {
  background-color: var(--yh-fill-color-light);
  border-color: var(--yh-border-color-lighter);
  cursor: not-allowed;
}

.yh-tree-select.is-disabled .yh-tree-select__inner {
  cursor: not-allowed;
  color: var(--yh-text-color-disabled);
}

@keyframes yh-tree-select-rotate {
  from {
    transform: rotate(0);
  }
  to {
    transform: rotate(360deg);
  }
}
@keyframes yh-tree-select-fade-in {
  from {
    opacity: 0;
    transform: translateY(-10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
.yh-zoom-in-top-enter-active,
.yh-zoom-in-top-leave-active {
  transition: opacity 0.25s cubic-bezier(0.23, 1, 0.32, 1), transform 0.25s cubic-bezier(0.23, 1, 0.32, 1);
  transform-origin: center top;
}

.yh-zoom-in-top-enter-from,
.yh-zoom-in-top-leave-to {
  opacity: 0;
  transform: scaleY(0);
}
</style>
