diff --git a/packages/ui-vue/components/button-edit/src/button-edit.component.tsx b/packages/ui-vue/components/button-edit/src/button-edit.component.tsx index 6465c41c3e7f4ff079a8784d199ba9817ef14101..1389629e05196c7b32b4266e9d9129a446c8ad99 100644 --- a/packages/ui-vue/components/button-edit/src/button-edit.component.tsx +++ b/packages/ui-vue/components/button-edit/src/button-edit.component.tsx @@ -91,7 +91,7 @@ export default defineComponent({ return () => { return ( <> -
+
(); - // is panel visible - const { isPanelVisible, panelContainerClass, displayText, onClear, onButtonClick, onValueChange } = useComboList(props, context, modelValue, panelRef); + + // component logic + const { onValueChange, displayText, modelValue, isPanelVisible, position } = useComboList(props, context); + // event logic + const { onClear, onButtonClick, onPanelHidden } = useComboListEvent({ props, context, position, isPanelVisible, modelValue }); + const comboListContext = reactive({ + isPanelVisible, + comboListProps: props, + onValueChange, + modelValue + }); /** * provider */ - provide( - COMBO_LIST_TOKEN, - reactive({ - onValueChange, - modelValue - }) - ); + provide(COMBO_LIST_TOKEN, comboListContext); return () => { return ( <> {/** main component */} - {/** tag area */} - {props.viewType === ViewType.Tag && ( -
-
-
-
- - - -
-
- -
-
-
- - - - -
-
-
- )} - {/** panel area */} - {isPanelVisible.value && ( -
-
-
- -
-
-
- )} + onClickButton={onButtonClick}> + +
); }; diff --git a/packages/ui-vue/components/combo-list/src/combo-list.props.ts b/packages/ui-vue/components/combo-list/src/combo-list.props.ts index 068830f2defa62955d2a998c1c8cc44a8ddf1d52..1418a77e55c76609aec15e5d617e7172697d43a8 100644 --- a/packages/ui-vue/components/combo-list/src/combo-list.props.ts +++ b/packages/ui-vue/components/combo-list/src/combo-list.props.ts @@ -1,5 +1,5 @@ import { ExtractPropTypes, PropType } from "vue"; -import { Options, ViewType } from './types'; +import { Options, ViewType, Position } from './types'; /** * 下拉列表属性 @@ -112,7 +112,12 @@ export const comboListProps = { /** * 绑定值 */ - modelValue: { type: String } + modelValue: { type: [String, Number] }, + /** + * 可选,下拉图标 + * 默认为'' + */ + dropDownIcon: { type: String, default: '' } }; export type ComboListProps = ExtractPropTypes; @@ -136,6 +141,26 @@ export const optionProps = { disabled: { default: false, type: Boolean } }; export type OptionProps = ExtractPropTypes; + +/** + * options 属性 + */ +export const optionsContainerProps = { + /** + * 下拉面板是否可见 + */ + isPanelVisible: { type: Boolean, default: false }, + /** + * 下拉数据源 + */ + data: { type: Array as PropType }, + /** + * 下拉面板弹出位置 + */ + position: { type: Object as PropType } +}; +export type OptionsContainerProps = ExtractPropTypes; + /** * options 属性 */ diff --git a/packages/ui-vue/components/combo-list/src/components/option.tsx b/packages/ui-vue/components/combo-list/src/components/option.component.tsx similarity index 95% rename from packages/ui-vue/components/combo-list/src/components/option.tsx rename to packages/ui-vue/components/combo-list/src/components/option.component.tsx index e8b92e4fc3fe449753a37d60758686c6fd65c0e5..3175f593d59a0ca17fc245b4170dc3919cda8fb5 100644 --- a/packages/ui-vue/components/combo-list/src/components/option.tsx +++ b/packages/ui-vue/components/combo-list/src/components/option.component.tsx @@ -5,8 +5,7 @@ import { useOption } from '../composition/use-option'; export default defineComponent({ name: 'FOption', props: optionProps, - emits: [], - inheritAttrs: false, + emits: ['itemClick'], setup(props: OptionProps, context: SetupContext) { const { name, optionClass, onOptionClick } = useOption(props, context); diff --git a/packages/ui-vue/components/combo-list/src/components/options-container.component.tsx b/packages/ui-vue/components/combo-list/src/components/options-container.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..dfe72905bc1cfb147103cf5c334647708812bd9a --- /dev/null +++ b/packages/ui-vue/components/combo-list/src/components/options-container.component.tsx @@ -0,0 +1,36 @@ +import { defineComponent, ref, SetupContext, toRefs } from 'vue'; +import { OptionsContainerProps, optionsContainerProps } from '../combo-list.props'; +import FOverlay from '../../../overlay/src/overlay.component'; +import FOptions from './options.component'; +import { useOptionsContainerEvent } from '../composition/use-options-container'; +import { EVENTS } from '../const'; + +export default defineComponent({ + name: 'FOptionsContainer', + props: optionsContainerProps, + emits: [ + EVENTS.panelHidden, + EVENTS.panelShow + ], + setup(props: OptionsContainerProps, context: SetupContext) { + const panelContainerRef = ref(); + const panelRef = ref(); + const { isPanelVisible, position } = toRefs(props); + const { onPanelContainerClick, onPanelItemClick, panelContainerClass } = useOptionsContainerEvent({ panelRef, isPanelVisible, position: position, context }); + return () => { + return ( + isPanelVisible.value && ( + +
+
+
+ +
+
+
+
+ ) + ); + }; + } +}); \ No newline at end of file diff --git a/packages/ui-vue/components/combo-list/src/components/options.component.tsx b/packages/ui-vue/components/combo-list/src/components/options.component.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fc84f0589045c9b184a97419e91332d95d431c27 --- /dev/null +++ b/packages/ui-vue/components/combo-list/src/components/options.component.tsx @@ -0,0 +1,24 @@ +import { defineComponent, SetupContext } from 'vue'; +import { optionsProps, OptionsProps } from '../combo-list.props'; +import { Option } from '../types'; +import FOption from './option.component'; + +export default defineComponent({ + name: 'FOptions', + props: optionsProps, + emits: ['itemClick'], + setup(props: OptionsProps, context: SetupContext) { + const onItemClick = function () { + context.emit('itemClick'); + }; + return () => { + return ( +
    + {props?.options?.map((option: Option) => ( + + ))} +
+ ); + }; + } +}); \ No newline at end of file diff --git a/packages/ui-vue/components/combo-list/src/components/options.tsx b/packages/ui-vue/components/combo-list/src/components/options.tsx deleted file mode 100644 index 15883d91a9cbd68f7c59165d98cf7f24ef88655f..0000000000000000000000000000000000000000 --- a/packages/ui-vue/components/combo-list/src/components/options.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { defineComponent, SetupContext } from 'vue'; -import { optionsProps, OptionsProps } from '../combo-list.props'; -import { IOption } from '../types'; -import FOption from '../components/option'; - -export default defineComponent({ - name: 'FOptions', - props: optionsProps, - emits: [], - inheritAttrs: false, - setup(props: OptionsProps, context: SetupContext) { - return () => { - return ( -
    - {props?.options?.map((option: IOption) => ( - - ))} -
- ); - }; - } -}); \ No newline at end of file diff --git a/packages/ui-vue/components/combo-list/src/composition/index.ts b/packages/ui-vue/components/combo-list/src/composition/index.ts index d61051f3f3a58b49fef2de39e8d103d1c7ebf4ea..5932263a8cb8016772c8fd3e43d7f3d7570f7486 100644 --- a/packages/ui-vue/components/combo-list/src/composition/index.ts +++ b/packages/ui-vue/components/combo-list/src/composition/index.ts @@ -1,3 +1,3 @@ -export * from './use-state'; -// export * from './use-event-handlers'; export * from './use-option'; +export * from './use-combo-list'; +export * from './use-options-container'; \ No newline at end of file diff --git a/packages/ui-vue/components/combo-list/src/composition/use-combo-list.ts b/packages/ui-vue/components/combo-list/src/composition/use-combo-list.ts index 379860b6a76c042c60b7d4f7ad6dd8dc6423f5e7..90fe4e83ae8e6649fe0a62f8fd021d0edd6cad47 100644 --- a/packages/ui-vue/components/combo-list/src/composition/use-combo-list.ts +++ b/packages/ui-vue/components/combo-list/src/composition/use-combo-list.ts @@ -1,65 +1,102 @@ -import { computed, reactive, ref, Ref, SetupContext } from "vue"; +import { ref, Ref, SetupContext, watch } from "vue"; import { ComboListProps } from "../combo-list.props"; -import { IOption, ModelValue } from "../types"; +import { Option, Position, UseComboListEvent, UseComboListEventHandlers } from "../types"; -export function useComboList(props: ComboListProps, context: SetupContext, modelValue: Ref, panelRef: Ref) { - const isPanelVisible = ref(false); +export function useComboList(props: ComboListProps, context: SetupContext) { const displayText = ref(''); - const panelContainerClass = computed(() => ({ - comboPanel: true, - 'f-area-hide': !isPanelVisible.value, - 'f-area-show': isPanelVisible.value - })); + const modelValue = ref(props.modelValue); + const isPanelVisible = ref(false); + const position: Ref = ref(undefined); + /** + * 值变化事件处理器 + * @param item item + */ + function onValueChange(item: Option) { + if (modelValue) { + modelValue.value = item.value; + } + if (props.multiSelect) { + + } else { + displayText.value = item.name; + context.emit('update:modelValue', item.value); + } + } + return { + displayText, + modelValue, + isPanelVisible, + position, + onValueChange + }; +} +/** + * 下拉列表事件处理钩子 + * @param options 上下文 + */ +export function useComboListEvent(options: UseComboListEvent): UseComboListEventHandlers { + // 监听输入框下拉按钮事件 + const { isPanelVisible, context, position, modelValue } = options; + /** + * window窗口大小变化事件处理器 + * @param event + */ + const windowResizeEventHandler = (event: UIEvent) => { + if (isPanelVisible.value !== false) { + isPanelVisible.value = false; + } + }; + /** + * window窗口滚动条滚动事件处理器 + * @param event + */ + const windowScrollEventHandler = (event: Event) => { + if (isPanelVisible.value !== false) { + isPanelVisible.value = false; + } + }; + watch([isPanelVisible], ([isPanelVisibleValue]) => { + if (isPanelVisibleValue) { + window.addEventListener('resize', windowResizeEventHandler); + window.addEventListener('scroll', windowScrollEventHandler); + } else { + window.removeEventListener('resize', windowResizeEventHandler); + window.removeEventListener('scroll', windowScrollEventHandler); + } + }); + //#region events /** * 清空事件 * @param $event event */ function onClear($event: Event) { - //modelValue.value = ''; + modelValue.value = ''; context.emit('clear'); } /** * 下拉按钮点击 * @param $event event */ - function onButtonClick($event: Event) { - const clickOutsideEventHandler = (event: any) => { - if (panelRef.value == event.target || panelRef?.value?.contains(event.target)) { - event.stopPropagation(); - } else { - isPanelVisible.value = false; - } - }; + function onButtonClick($event: any) { + // 下拉组件dom + const combolistEl = $event?.origin?.target?.closest('.input-group'); + // 记录事件上下文 + position.value = combolistEl?.getBoundingClientRect(); isPanelVisible.value = !isPanelVisible.value; - if (isPanelVisible.value === true) { - // 即将展示下拉面板 - document.addEventListener("click", clickOutsideEventHandler); - } else { - document.removeEventListener('click', clickOutsideEventHandler); - } } /** - * - * @param item 值变化事件处理器 + * 下拉面板收起事件处理器 + * @param $event */ - function onValueChange(item: IOption) { - modelValue.value = item.value; - if (props.multiSelect) { - - } else { - displayText.value = item.name; - context.emit('update:modelValue', item.value); - } + function onPanelHidden($event: any) { isPanelVisible.value = false; } //#endregion + return { - panelContainerClass, - isPanelVisible, - displayText, onClear, onButtonClick, - onValueChange + onPanelHidden }; } \ No newline at end of file diff --git a/packages/ui-vue/components/combo-list/src/composition/use-option.ts b/packages/ui-vue/components/combo-list/src/composition/use-option.ts index adb4b09af1b445ea5ba2661a927d94d64c8e848c..a13c49a81a95f3035c68ec0380a31c433a903a0a 100644 --- a/packages/ui-vue/components/combo-list/src/composition/use-option.ts +++ b/packages/ui-vue/components/combo-list/src/composition/use-option.ts @@ -1,24 +1,25 @@ -import { computed, inject, Ref, SetupContext } from "vue"; -import { IUseOption } from "../types"; +import { computed, inject, SetupContext } from "vue"; +import { UseOption } from "../types"; import { OptionProps } from "../combo-list.props"; import { COMBO_LIST_TOKEN } from "../const"; -export function useOption(props: OptionProps, context: SetupContext): IUseOption { - const comboList = inject(COMBO_LIST_TOKEN, null); +export function useOption(props: OptionProps, context: SetupContext): UseOption { + const comboListContext = inject(COMBO_LIST_TOKEN, null); const name = computed(() => { return props.name || props.value; }); const onOptionClick = ($event: Event) => { if (!props.disabled) { - comboList?.onValueChange({ + comboListContext?.onValueChange({ value: props.value, name: props.name, disabled: props.disabled }); + context.emit('itemClick'); } }; const isOptionSelected = computed(() => { - return comboList?.modelValue === props.value; + return comboListContext?.modelValue === props.value; }); const optionClass = computed(() => ({ active: isOptionSelected.value, diff --git a/packages/ui-vue/components/combo-list/src/composition/use-options-container.ts b/packages/ui-vue/components/combo-list/src/composition/use-options-container.ts new file mode 100644 index 0000000000000000000000000000000000000000..61917bf613d56e5927c56e6eb0845d8faeb56920 --- /dev/null +++ b/packages/ui-vue/components/combo-list/src/composition/use-options-container.ts @@ -0,0 +1,47 @@ +import { computed, watch } from "vue"; +import { EVENTS } from "../const"; +import { UseOptionsContainerEvent, UseOptionsContainerEventHandlers } from "../types"; + +export function useOptionsContainer() { +} +/** + * 下拉列表事件处理钩子 + * @param options 上下文 + */ +export function useOptionsContainerEvent(options: UseOptionsContainerEvent): UseOptionsContainerEventHandlers { + // 监听输入框下拉按钮事件 + const { isPanelVisible, panelRef, position, context } = options; + // 下拉面板样式 + const panelContainerClass = computed(() => ({ + 'f-area-hide': !isPanelVisible.value, + 'f-area-show': isPanelVisible.value + })); + // 监听面板显隐变化 + watch([panelRef, isPanelVisible], ([panelEl, isPanelVisibleValue]) => { + if (isPanelVisibleValue && panelEl && position) { + panelEl.style.top = ((position.value?.top || 0) + (position.value?.height || 0)) + 'px'; + panelEl.style.left = position.value?.left + 'px'; + panelEl.style.width = position.value?.width + 'px'; + // 触发面板展示事件 + context.emit(EVENTS.panelShow); + } + }); + + //#region events + /** + * 下拉列表容器点击事件处理器 + * @param $event event + */ + function onPanelContainerClick($event: any) { + context.emit(EVENTS.panelHidden); + } + function onPanelItemClick($event: any){ + context.emit(EVENTS.panelHidden); + } + //#endregion + return { + onPanelContainerClick, + onPanelItemClick, + panelContainerClass + }; +} diff --git a/packages/ui-vue/components/combo-list/src/composition/use-state.ts b/packages/ui-vue/components/combo-list/src/composition/use-state.ts deleted file mode 100644 index f25ca7abbb0f93e581d42a89d77c214eb55613ac..0000000000000000000000000000000000000000 --- a/packages/ui-vue/components/combo-list/src/composition/use-state.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ref } from 'vue'; -import { IUseState } from '../types'; - -/** - * state hook - * @param initial 初始值 - * @returns [state,action] - */ -export function useState(initial: T): IUseState { - if (typeof initial === 'undefined') { - throw new Error('invalid initial: initial must have value.'); - } - const state = ref(initial); - const action = (value: any) => { - state.value = value; - }; - return [state, action]; -} \ No newline at end of file diff --git a/packages/ui-vue/components/combo-list/src/const.ts b/packages/ui-vue/components/combo-list/src/const.ts index c72d4eb47a0478c87a0c6f6a7a8d2a75ef350e9d..23225d1ec589dca7e7f148bf258cf90a3ce4b06a 100644 --- a/packages/ui-vue/components/combo-list/src/const.ts +++ b/packages/ui-vue/components/combo-list/src/const.ts @@ -1,8 +1,15 @@ -import { InjectionKey } from "vue"; +import { InjectionKey, Ref } from "vue"; import { ComboListContext } from "./types"; export const groupIcon = ''; /** * combo list injector token */ -export const COMBO_LIST_TOKEN: InjectionKey = Symbol('fComboList'); \ No newline at end of file +export const COMBO_LIST_TOKEN: InjectionKey> = Symbol('fComboList'); + +export const EVENTS = { + clear: 'clear', + panelShow: 'panelShow', + panelHidden: 'panelHidden', + itemClick: 'itemClick' +}; \ No newline at end of file diff --git a/packages/ui-vue/components/combo-list/src/types.ts b/packages/ui-vue/components/combo-list/src/types.ts index 4911ceeb34d647fa1d013c5619ddee48c6be7153..48d31e3606cb9cbe708ef7344b4431310bc6aa55 100644 --- a/packages/ui-vue/components/combo-list/src/types.ts +++ b/packages/ui-vue/components/combo-list/src/types.ts @@ -1,5 +1,6 @@ //import EventEmitter from 'events'; -import { ComputedRef, Ref, UnwrapRef } from 'vue'; +import { ComputedRef,Ref, SetupContext } from 'vue'; +import { ComboListProps } from './combo-list.props'; export type ModelValue = number | string | undefined | Array; /** * 数据展现方式 @@ -8,34 +9,82 @@ export enum ViewType { Text = 'text', Tag = 'tag' }; -/** - * IUseState type - * @description 受vue类型系统限制,仅在类型定义中约定state只读,实际返回对象可写 - */ -export type IUseState = [Readonly>>, (state: UnwrapRef) => void]; - -export interface IUseEventHandler { - /** - * 清除事件 - */ - onClear: ($event: Event) => void; - onButtonClick: ($event: Event) => void; -} -export interface IUseOption { +export interface UseOption { name: ComputedRef; onOptionClick: ($event: Event) => void; optionClass: ComputedRef; } -export interface IOption { +export interface Option { value: string | number | undefined; name: string | undefined; disabled: boolean; } -export type Options = Array; +export type Options = Array