{"version":3,"file":"index.mjs","sources":["../../../../../../packages/components/cascader-panel/src/index.vue"],"sourcesContent":["<template>\r\n  <div\r\n    :class=\"['sg-cascader-panel', border && 'is-bordered']\"\r\n    @keydown=\"handleKeyDown\"\r\n  >\r\n    <sg-cascader-menu\r\n      v-for=\"(menu, index) in menus\"\r\n      :key=\"index\"\r\n      :ref=\"(item) => (menuList[index] = item)\"\r\n      :index=\"index\"\r\n      :nodes=\"menu\"\r\n      :emptyTips=\"emptyTips\"\r\n    />\r\n  </div>\r\n</template>\r\n\r\n<script lang=\"ts\">\r\nimport {\r\n  computed,\r\n  defineComponent,\r\n  nextTick,\r\n  onBeforeUpdate,\r\n  onMounted,\r\n  provide,\r\n  reactive,\r\n  ref,\r\n  watch,\r\n} from 'vue'\r\nimport isEqual from 'lodash/isEqual'\r\nimport { isClient } from '@vueuse/core'\r\nimport { EVENT_CODE, focusNode, getSibling } from '@sgui-plus/utils/aria'\r\nimport { UPDATE_MODEL_EVENT, CHANGE_EVENT } from '@sgui-plus/utils/constants'\r\nimport scrollIntoView from '@sgui-plus/utils/scroll-into-view'\r\nimport {\r\n  arrayFlat,\r\n  coerceTruthyValueToArray,\r\n  deduplicate,\r\n  isEmpty,\r\n} from '@sgui-plus/utils/util'\r\n\r\nimport SgCascaderMenu from './menu.vue'\r\nimport Store from './store'\r\nimport Node, { ExpandTrigger } from './node'\r\nimport { CommonProps, useCascaderConfig } from './config'\r\nimport { checkNode, getMenuIndex, sortByOriginalOrder } from './utils'\r\nimport { CASCADER_PANEL_INJECTION_KEY } from './types'\r\n\r\nimport type { PropType } from 'vue'\r\nimport type { Nullable } from '@sgui-plus/utils/types'\r\nimport type {\r\n  CascaderValue,\r\n  CascaderNodeValue,\r\n  CascaderOption,\r\n  RenderLabel,\r\n  default as CascaderNode,\r\n} from './node'\r\n\r\nimport type { SgCascaderPanelContext } from './types'\r\n\r\nexport default defineComponent({\r\n  name: 'SgCascaderPanel',\r\n\r\n  components: {\r\n    SgCascaderMenu,\r\n  },\r\n\r\n  props: {\r\n    ...CommonProps,\r\n    border: {\r\n      type: Boolean,\r\n      default: true,\r\n    },\r\n    emptyTips: String,\r\n    renderLabel: Function as PropType<RenderLabel>,\r\n  },\r\n\r\n  emits: [UPDATE_MODEL_EVENT, CHANGE_EVENT, 'close', 'expand-change'],\r\n\r\n  setup(props, { emit, slots }) {\r\n    // for interrupt sync check status in lazy mode\r\n    let manualChecked = false\r\n\r\n    const config = useCascaderConfig(props)\r\n\r\n    let store: Nullable<Store> = null\r\n    const initialLoaded = ref(true)\r\n    const menuList = ref<any[]>([])\r\n    const checkedValue = ref<Nullable<CascaderValue>>(null)\r\n    const menus = ref<CascaderNode[][]>([])\r\n    const expandingNode = ref<Nullable<CascaderNode>>(null)\r\n    const checkedNodes = ref<CascaderNode[]>([])\r\n\r\n    const isHoverMenu = computed(\r\n      () => config.value.expandTrigger === ExpandTrigger.HOVER\r\n    )\r\n    const renderLabelFn = computed(() => props.renderLabel || slots.default)\r\n\r\n    const initStore = () => {\r\n      const { options } = props\r\n      const cfg = config.value\r\n\r\n      manualChecked = false\r\n      store = new Store(options, cfg)\r\n      menus.value = [store.getNodes()]\r\n\r\n      if (cfg.lazy && isEmpty(props.options)) {\r\n        initialLoaded.value = false\r\n        lazyLoad(undefined, (list) => {\r\n          if (list) {\r\n            store = new Store(list, cfg)\r\n            menus.value = [store.getNodes()]\r\n          }\r\n          initialLoaded.value = true\r\n          syncCheckedValue(false, true)\r\n        })\r\n      } else {\r\n        syncCheckedValue(false, true)\r\n      }\r\n    }\r\n\r\n    const lazyLoad: SgCascaderPanelContext['lazyLoad'] = (node, cb) => {\r\n      const cfg = config.value\r\n      node! = node || new Node({}, cfg, undefined, true)\r\n      node.loading = true\r\n\r\n      const resolve = (dataList: CascaderOption[]) => {\r\n        const _node = node as Node\r\n        const parent = _node.root ? null : _node\r\n        dataList && store?.appendNodes(dataList, parent as any)\r\n        _node.loading = false\r\n        _node.loaded = true\r\n        _node.childrenData = _node.childrenData || []\r\n        cb && cb(dataList)\r\n      }\r\n\r\n      cfg.lazyLoad(node, resolve as any)\r\n    }\r\n\r\n    const expandNode: SgCascaderPanelContext['expandNode'] = (node, silent) => {\r\n      const { level } = node\r\n      const newMenus = menus.value.slice(0, level)\r\n      let newExpandingNode: Nullable<CascaderNode>\r\n\r\n      if (node.isLeaf) {\r\n        newExpandingNode = node.pathNodes[level - 2]\r\n      } else {\r\n        newExpandingNode = node\r\n        newMenus.push(node.children)\r\n      }\r\n\r\n      if (expandingNode.value?.uid !== newExpandingNode?.uid) {\r\n        expandingNode.value = node\r\n        menus.value = newMenus\r\n        !silent && emit('expand-change', node?.pathValues || [])\r\n      }\r\n    }\r\n\r\n    const handleCheckChange: SgCascaderPanelContext['handleCheckChange'] = (\r\n      node,\r\n      checked,\r\n      emitClose = true\r\n    ) => {\r\n      const { checkStrictly, multiple } = config.value\r\n      const oldNode = checkedNodes.value[0]\r\n      manualChecked = true\r\n\r\n      !multiple && oldNode?.doCheck(false)\r\n      node.doCheck(checked)\r\n      calculateCheckedValue()\r\n      emitClose && !multiple && !checkStrictly && emit('close')\r\n      !emitClose && !multiple && !checkStrictly && expandParentNode(node)\r\n    }\r\n\r\n    const expandParentNode = (node) => {\r\n      if (!node) return\r\n      node = node.parent\r\n      expandParentNode(node)\r\n      node && expandNode(node)\r\n    }\r\n\r\n    const getFlattedNodes = (leafOnly: boolean) => {\r\n      return store?.getFlattedNodes(leafOnly)\r\n    }\r\n\r\n    const getCheckedNodes = (leafOnly: boolean) => {\r\n      return getFlattedNodes(leafOnly)?.filter((node) => node.checked !== false)\r\n    }\r\n\r\n    const clearCheckedNodes = () => {\r\n      checkedNodes.value.forEach((node) => node.doCheck(false))\r\n      calculateCheckedValue()\r\n    }\r\n\r\n    const calculateCheckedValue = () => {\r\n      const { checkStrictly, multiple } = config.value\r\n      const oldNodes = checkedNodes.value\r\n      const newNodes = getCheckedNodes(!checkStrictly)!\r\n      // ensure the original order\r\n      const nodes = sortByOriginalOrder(oldNodes, newNodes)\r\n      const values = nodes.map((node) => node.valueByOption)\r\n      checkedNodes.value = nodes\r\n      checkedValue.value = multiple ? values : values[0] ?? null\r\n    }\r\n\r\n    const syncCheckedValue = (loaded = false, forced = false) => {\r\n      const { modelValue } = props\r\n      const { lazy, multiple, checkStrictly } = config.value\r\n      const leafOnly = !checkStrictly\r\n\r\n      if (\r\n        !initialLoaded.value ||\r\n        manualChecked ||\r\n        (!forced && isEqual(modelValue, checkedValue.value))\r\n      )\r\n        return\r\n\r\n      if (lazy && !loaded) {\r\n        const values: CascaderNodeValue[] = deduplicate(\r\n          arrayFlat(coerceTruthyValueToArray(modelValue))\r\n        )\r\n        const nodes = values\r\n          .map((val) => store?.getNodeByValue(val))\r\n          .filter((node) => !!node && !node.loaded && !node.loading) as Node[]\r\n\r\n        if (nodes.length) {\r\n          nodes.forEach((node) => {\r\n            lazyLoad(node, () => syncCheckedValue(false, forced))\r\n          })\r\n        } else {\r\n          syncCheckedValue(true, forced)\r\n        }\r\n      } else {\r\n        const values = multiple\r\n          ? coerceTruthyValueToArray(modelValue)\r\n          : [modelValue]\r\n        const nodes = deduplicate(\r\n          values.map((val) => store?.getNodeByValue(val, leafOnly))\r\n        ) as Node[]\r\n        syncMenuState(nodes, false)\r\n        checkedValue.value = modelValue!\r\n      }\r\n    }\r\n\r\n    const syncMenuState = (\r\n      newCheckedNodes: CascaderNode[],\r\n      reserveExpandingState = true\r\n    ) => {\r\n      const { checkStrictly } = config.value\r\n      const oldNodes = checkedNodes.value\r\n      const newNodes = newCheckedNodes.filter(\r\n        (node) => !!node && (checkStrictly || node.isLeaf)\r\n      )\r\n      const oldExpandingNode = store?.getSameNode(expandingNode.value!)\r\n      const newExpandingNode =\r\n        (reserveExpandingState && oldExpandingNode) || newNodes[0]\r\n\r\n      if (newExpandingNode) {\r\n        newExpandingNode.pathNodes.forEach((node) => expandNode(node, true))\r\n      } else {\r\n        expandingNode.value = null\r\n      }\r\n\r\n      oldNodes.forEach((node) => node.doCheck(false))\r\n      newNodes.forEach((node) => node.doCheck(true))\r\n\r\n      checkedNodes.value = newNodes\r\n      nextTick(scrollToExpandingNode)\r\n    }\r\n\r\n    const scrollToExpandingNode = () => {\r\n      if (!isClient) return\r\n\r\n      menuList.value.forEach((menu) => {\r\n        const menuElement = menu?.$el\r\n        if (menuElement) {\r\n          const container = menuElement.querySelector('.sg-scrollbar__wrap')\r\n          const activeNode =\r\n            menuElement.querySelector('.sg-cascader-node.is-active') ||\r\n            menuElement.querySelector('.sg-cascader-node.in-active-path')\r\n          scrollIntoView(container, activeNode)\r\n        }\r\n      })\r\n    }\r\n\r\n    const handleKeyDown = (e: KeyboardEvent) => {\r\n      const target = e.target as HTMLElement\r\n      const { code } = e\r\n\r\n      switch (code) {\r\n        case EVENT_CODE.up:\r\n        case EVENT_CODE.down: {\r\n          e.preventDefault()\r\n          const distance = code === EVENT_CODE.up ? -1 : 1\r\n          focusNode(\r\n            getSibling(target, distance, '.sg-cascader-node[tabindex=\"-1\"]')\r\n          )\r\n          break\r\n        }\r\n        case EVENT_CODE.left: {\r\n          e.preventDefault()\r\n          const preMenu = menuList.value[getMenuIndex(target) - 1]\r\n          const expandedNode = preMenu?.$el.querySelector(\r\n            '.sg-cascader-node[aria-expanded=\"true\"]'\r\n          )\r\n          focusNode(expandedNode)\r\n          break\r\n        }\r\n        case EVENT_CODE.right: {\r\n          e.preventDefault()\r\n          const nextMenu = menuList.value[getMenuIndex(target) + 1]\r\n          const firstNode = nextMenu?.$el.querySelector(\r\n            '.sg-cascader-node[tabindex=\"-1\"]'\r\n          )\r\n          focusNode(firstNode)\r\n          break\r\n        }\r\n        case EVENT_CODE.enter:\r\n          checkNode(target)\r\n          break\r\n        case EVENT_CODE.esc:\r\n        case EVENT_CODE.tab:\r\n          emit('close')\r\n          break\r\n      }\r\n    }\r\n\r\n    provide(\r\n      CASCADER_PANEL_INJECTION_KEY,\r\n      reactive({\r\n        config,\r\n        expandingNode,\r\n        checkedNodes,\r\n        isHoverMenu,\r\n        initialLoaded,\r\n        renderLabelFn,\r\n        lazyLoad,\r\n        expandNode,\r\n        handleCheckChange,\r\n      })\r\n    )\r\n\r\n    watch([config, () => props.options], initStore, {\r\n      deep: true,\r\n      immediate: true,\r\n    })\r\n\r\n    watch(\r\n      () => props.modelValue,\r\n      () => {\r\n        manualChecked = false\r\n        syncCheckedValue()\r\n      }\r\n    )\r\n\r\n    watch(checkedValue, (val) => {\r\n      if (!isEqual(val, props.modelValue)) {\r\n        emit(UPDATE_MODEL_EVENT, val)\r\n        emit(CHANGE_EVENT, val)\r\n      }\r\n    })\r\n\r\n    onBeforeUpdate(() => (menuList.value = []))\r\n\r\n    onMounted(() => !isEmpty(props.modelValue) && syncCheckedValue())\r\n\r\n    return {\r\n      menuList,\r\n      menus,\r\n      checkedNodes,\r\n      handleKeyDown,\r\n      handleCheckChange,\r\n      getFlattedNodes,\r\n      getCheckedNodes,\r\n      clearCheckedNodes,\r\n      calculateCheckedValue,\r\n      scrollToExpandingNode,\r\n    }\r\n  },\r\n})\r\n</script>\r\n"],"names":["_resolveComponent","_openBlock","_createElementBlock","_normalizeClass","_Fragment","_renderList"],"mappings":";;;;;sCACEA,gBAYM,CAAA,kBAAA,CAAA,CAAA;SAVHC,SAAO,EAAA,EAAAC,kBAAA,CAAA,KAAA,EAAA;AAAA,IAAA,KAAA,EAAAC,cAAA,CAAA,CAAA,mBAAA,EAAA,IAAA,CAAA,MAAA,IAAA,aAAA,CAAA,CAAA;6BAER,MAOE,CAAA,CAAA,CAAA,GAAA,CAAA,GAAA,IAAA,KAAA,IAAA,CAAA,aAAA,IANwB,KAAK,aAAV,CAAA,GAAA,IAAA,CAAA,CAAA;AAAA,GAAA,EAAA;eACb,IAAK,CAAA,EAAAD,kBAAA,CAAAE,QAAA,EAAA,IAAA,EAAAC,UAAA,CAAA,IAAA,CAAA,KAAA,EAAA,CAAA,IAAA,EAAA,KAAA,KAAA;;QACV,GAAG,EAAA,KAAA;AAAA,QACH,OAAO,EAAA,IAAA;AAAA,QACP,KAAK,CAAM,IAAA,KAAA,IAAA,CAAA,QAAA,CAAA,KAAA,CAAA,GAAA,IAAA;AAAA,QACX,KAAA;AAAA,QAAA,KAAA,EAAA,IAAA;;;;;;;;;;"}