ng-devui
Version:
DevUI components based on Angular
1 lines • 139 kB
Source Map (JSON)
{"version":3,"file":"ng-devui-category-search.mjs","sources":["../../devui/category-search/category-search.type.ts","../../devui/category-search/content-template.directive.ts","../../devui/category-search/default-template.directive.ts","../../devui/category-search/category-search.component.ts","../../devui/category-search/category-search.component.html","../../devui/category-search/category-search.module.ts","../../devui/category-search/ng-devui-category-search.ts"],"sourcesContent":["import { TemplateRef } from '@angular/core';\r\nimport { OperableTreeComponent } from 'ng-devui/tree';\r\n\r\nexport type CategorySearchTagType = 'radio' | 'checkbox' | 'dateRange' | 'label' | 'textInput' | 'numberRange' | 'treeSelect' | 'keyword';\r\n\r\nexport interface ITagOption {\r\n /**\r\n * 选项,label和color默认都会取对应的 filterKey 和 colorKey,如未设置取默认值\r\n */\r\n label?: string; // 通用默认属性,用于设置分类名称\r\n color?: string; // label 专用,用于设置标签颜色\r\n [propName: string]: any;\r\n}\r\n\r\n/**\r\n * 候选tag数据配置项\r\n */\r\nexport interface ICategorySearchTagItem {\r\n /**\r\n * 搜索字段,tag的键,用于区分不同的分类,需要唯一\r\n */\r\n field: string;\r\n /**\r\n * tag 键的显示值\r\n */\r\n label: string;\r\n /**\r\n * 配置项可生产的tag类型\r\n */\r\n type?: CategorySearchTagType;\r\n /**\r\n * 配置项所属的分组\r\n */\r\n group?: string;\r\n /**\r\n * tag 值的选择项数据\r\n */\r\n options?: Array<ITagOption>;\r\n /**\r\n * 用于显示的 tag 值的键值,如未设置默认取label\r\n */\r\n filterKey?: string | 'label';\r\n /**\r\n * 用于显示的label类型中色值的键值,如未设置默认取color\r\n */\r\n colorKey?: string | 'color';\r\n /**\r\n * 自定义下拉模板\r\n */\r\n customTemplate?: TemplateRef<any>;\r\n /**\r\n * 自定义已选标签内容模板\r\n */\r\n tagCustomTemplate?: TemplateRef<any>;\r\n /**\r\n * 当前分类选中后是否可以删除\r\n */\r\n deletable?: boolean;\r\n /**\r\n * 已选中值\r\n */\r\n value?: {\r\n label?: string;\r\n value?: string | ITagOption | Array<ITagOption | number | string | Date>;\r\n cache?: string | ITagOption | Array<ITagOption | number | string | Date>;\r\n [propName: string]: any;\r\n };\r\n /**\r\n * checkbox | label 类型是否显示全选\r\n */\r\n showSelectAll?: boolean;\r\n /**\r\n * dateRange 类型是否显示时分秒\r\n */\r\n showTime?: boolean;\r\n /**\r\n * dateRange 类型默认激活开始或者结束日期\r\n */\r\n activeRangeType?: 'start' | 'end';\r\n /**\r\n * textInput | numberRange 类型设置最大长度,numberRange 需传入对象分别设置左右\r\n */\r\n maxLength?: number | { left?: number; right?: number };\r\n /**\r\n * textInput | numberRange 类型设置占位符,numberRange 需传入对象分别设置左右\r\n */\r\n placeholder?: string | { left?: string; right?: string };\r\n /**\r\n * numberRange 步进值,需传入对象分别设置左右\r\n */\r\n step?: { left?: number; right?: number };\r\n /**\r\n * numberRange 最大值,需传入对象分别设置左右\r\n */\r\n max?: { left?: number; right?: number };\r\n /**\r\n * numberRange 最小值,需传入对象分别设置左右\r\n */\r\n min?: { left?: number; right?: number };\r\n /**\r\n * numberRange 限制输入的正则或正则字符串,需传入对象分别设置左右\r\n */\r\n reg?: { left?: RegExp | string; right?: RegExp | string };\r\n /**\r\n * numberRange 限制小数点后的位数,需传入对象分别设置左右\r\n */\r\n decimalLimit?: { left?: number; right?: number };\r\n /**\r\n * numberRange 校验方法,点击确定时执行,返回 true 通过\r\n */\r\n validateFunc?: (start: number, end: number, tag: ICategorySearchTagItem) => boolean;\r\n /**\r\n * treeSelect 类型是否为多选,并显示已选择列表\r\n */\r\n multiple?: boolean;\r\n /**\r\n * treeSelect 类型是否显示搜索框\r\n */\r\n searchable?: boolean;\r\n /**\r\n * treeSelect 类型设置搜索框占位符\r\n */\r\n searchPlaceholder?: string;\r\n /**\r\n * treeSelect 类型自定义搜索方法,参数为搜索关键字和 d-operable-tree 组件实例\r\n */\r\n searchFn?: (value: string, treeInstance: OperableTreeComponent) => boolean | Array<any>;\r\n /**\r\n * treeSelect 类型相关配置,请参考treeSelect组件API中同名配置\r\n */\r\n checkableRelation?: 'upward' | 'downward' | 'both' | 'none';\r\n treeNodeIdKey?: string;\r\n treeNodeChildrenKey?: string;\r\n treeNodeTitleKey?: string;\r\n disabledKey?: string;\r\n leafOnly?: boolean;\r\n iconParentOpen?: string;\r\n iconParentClose?: string;\r\n iconLeaf?: string;\r\n [propName: string]: any;\r\n}\r\n\r\nexport interface SelectedTagsEvent {\r\n selectedTags: Array<ICategorySearchTagItem>;\r\n currentChangeTag: ICategorySearchTagItem;\r\n operation: 'add' | 'delete' | 'clear';\r\n}\r\n\r\nexport interface CreateFilterEvent {\r\n name: string;\r\n selectedTags: Array<ICategorySearchTagItem>;\r\n keyword: string;\r\n}\r\n\r\nexport interface SearchEvent {\r\n selectedTags: Array<ICategorySearchTagItem>;\r\n searchKey: string;\r\n}\r\n\r\nexport interface SearchConfig {\r\n keyword?: boolean;\r\n keywordDescription?: (searchKey: string) => string;\r\n field?: boolean;\r\n fieldDescription?: (label: string) => string;\r\n category?: boolean;\r\n categoryDescription?: string;\r\n noCategoriesAvailableTip?: boolean;\r\n searchInputMaxLength?: number;\r\n}\r\n\r\nexport interface TextConfig {\r\n keywordName?: string;\r\n createFilter?: string;\r\n filterTitle?: string;\r\n labelConnector?: string;\r\n noCategoriesAvailable?: string;\r\n}\r\n\r\nexport interface ExtendedConfig {\r\n show?: boolean;\r\n clear?: {\r\n show?: boolean;\r\n disabled?: boolean;\r\n template?: TemplateRef<any>;\r\n };\r\n save?: {\r\n show?: boolean;\r\n disabled?: boolean;\r\n template?: TemplateRef<any>;\r\n };\r\n more?: {\r\n show?: boolean;\r\n disabled?: boolean;\r\n template?: TemplateRef<any>;\r\n };\r\n customTemplate?: TemplateRef<any>;\r\n}\r\n\r\nexport const COLORS = [\r\n '#f2f5fc',\r\n '#e9edfa',\r\n '#beccfa',\r\n '#96adfa',\r\n '#7693f5',\r\n '#5e7ce0',\r\n '#526ecc',\r\n '#465eb8',\r\n '#3c51a6',\r\n '#344899',\r\n '#2a3c85',\r\n '#ebf6ff',\r\n '#d1ebff',\r\n '#b8e0ff',\r\n '#9ed5ff',\r\n '#85caff',\r\n '#6cbfff',\r\n '#4ea6e6',\r\n '#3590cc',\r\n '#207ab3',\r\n '#0f6999',\r\n '#035880',\r\n '#edfff9',\r\n '#cffcee',\r\n '#acf2dc',\r\n '#8be8cb',\r\n '#6ddebb',\r\n '#50d4ab',\r\n '#3ac295',\r\n '#27b080',\r\n '#169e6c',\r\n '#088c58',\r\n '#007a45',\r\n '#f0ffe6',\r\n '#e5ffd4',\r\n '#d8fcc0',\r\n '#c5f2a7',\r\n '#b3e890',\r\n '#a6dd82',\r\n '#92cc68',\r\n '#7eba50',\r\n '#6ca83b',\r\n '#5e9629',\r\n '#518519',\r\n '#fffbf0',\r\n '#fff1c2',\r\n '#ffe794',\r\n '#ffdc66',\r\n '#ffd138',\r\n '#fac20a',\r\n '#e3aa00',\r\n '#cc9600',\r\n '#b58200',\r\n '#9e6f00',\r\n '#875c00',\r\n '#fff3e8',\r\n '#ffe1c7',\r\n '#ffd0a6',\r\n '#ffbf85',\r\n '#ffad63',\r\n '#fa9841',\r\n '#e37d29',\r\n '#cc6414',\r\n '#b54e04',\r\n '#9e3f00',\r\n '#873400',\r\n '#ffeeed',\r\n '#ffd5d4',\r\n '#ffbcba',\r\n '#ffa4a1',\r\n '#ff8b87',\r\n '#f66f6a',\r\n '#de504e',\r\n '#c73636',\r\n '#b02121',\r\n '#991111',\r\n '#820404',\r\n '#ffedf3',\r\n '#ffd4e3',\r\n '#ffbad2',\r\n '#ffa1c2',\r\n '#fc86b0',\r\n '#f3689a',\r\n '#db4d83',\r\n '#c4356e',\r\n '#ad215b',\r\n '#96114d',\r\n '#800440',\r\n '#f5f0ff',\r\n '#e7d9ff',\r\n '#d8c2ff',\r\n '#caabff',\r\n '#bc94ff',\r\n '#a97af8',\r\n '#8a5ce0',\r\n '#6f42c9',\r\n '#572db3',\r\n '#3f1a9c',\r\n '#2a0c85',\r\n];\r\n","import { Directive, Input, TemplateRef } from '@angular/core';\n\n@Directive({\n /* eslint-disable */\n selector: 'ng-template[field], ng-template[dField]',\n})\nexport class ContentTemplateDirective {\n @Input() field: string;\n @Input() dField: string;\n\n constructor(public template: TemplateRef<any>) {}\n}\n","import { Directive, Input, TemplateRef } from '@angular/core';\n\n@Directive({\n /* eslint-disable */\n selector: 'ng-template[type], ng-template[dType]',\n})\nexport class DefaultTemplateDirective {\n @Input() type: string;\n @Input() dType: string;\n\n constructor(public template: TemplateRef<any>) {}\n}\n","import { DOCUMENT } from '@angular/common';\r\nimport {\r\n AfterContentInit,\r\n AfterViewInit,\r\n Component,\r\n ContentChildren,\r\n ElementRef,\r\n EventEmitter,\r\n HostBinding,\r\n Inject,\r\n Input,\r\n OnChanges,\r\n OnDestroy,\r\n Output,\r\n QueryList,\r\n SimpleChanges,\r\n TemplateRef,\r\n ViewChild,\r\n ViewChildren,\r\n} from '@angular/core';\r\nimport { NgForm } from '@angular/forms';\r\nimport { DatepickerProCalendarComponent } from 'ng-devui/datepicker-pro';\r\nimport { DropDownAppendToBodyComponent, DropDownDirective } from 'ng-devui/dropdown';\r\nimport { DValidateRules } from 'ng-devui/form';\r\nimport { I18nInterface, I18nService } from 'ng-devui/i18n';\r\nimport { ITreeItem, OperableTreeComponent } from 'ng-devui/tree';\r\nimport { DefaultIcons } from 'ng-devui/tree-select';\r\nimport { DateConverter, DefaultDateConverter, DevConfigService, WithConfig } from 'ng-devui/utils';\r\nimport { cloneDeep, intersectionBy, isEqual, merge, mergeWith } from 'lodash-es';\r\nimport { Observable, Subject, fromEvent } from 'rxjs';\r\nimport { debounceTime, takeUntil, tap } from 'rxjs/operators';\r\nimport {\r\n COLORS,\r\n CreateFilterEvent,\r\n ExtendedConfig,\r\n ICategorySearchTagItem,\r\n ITagOption,\r\n SearchConfig,\r\n SearchEvent,\r\n SelectedTagsEvent,\r\n TextConfig,\r\n} from './category-search.type';\r\nimport { ContentTemplateDirective } from './content-template.directive';\r\nimport { DefaultTemplateDirective } from './default-template.directive';\r\n\r\n@Component({\r\n selector: 'd-category-search',\r\n templateUrl: './category-search.component.html',\r\n styleUrls: ['./category-search.component.scss'],\r\n})\r\nexport class CategorySearchComponent implements OnChanges, OnDestroy, AfterViewInit, AfterContentInit {\r\n static ID_SEED = 0;\r\n @Input() category: ICategorySearchTagItem[] = [];\r\n @Input() defaultSearchField = [];\r\n @Input() selectedTags: ICategorySearchTagItem[] = [];\r\n /**\r\n * @deprecated\r\n */\r\n @Input() allowClear = true;\r\n /**\r\n * @deprecated\r\n */\r\n @Input() allowSave = true;\r\n /**\r\n * @deprecated\r\n */\r\n @Input() allowShowMore = false;\r\n @Input() disabled = false;\r\n @Input() extendedConfig: ExtendedConfig;\r\n @Input() toggleScrollToTail = false;\r\n @Input() searchKey = '';\r\n @Input() placeholderText: string;\r\n @Input() inputReadOnly = false;\r\n @Input() inputAutofocus = true;\r\n @Input() dropdownBoundary = false;\r\n @Input() showSearchCategory: SearchConfig | boolean = true; // 配置是否显示搜索相关下拉选项\r\n @Input() categoryInGroup = false; // 是否按组别显示分类下拉列表\r\n @Input() groupOrderConfig: string[]; // 用户配置组顺序\r\n @Input() customGroupNameTemplate: TemplateRef<any>; // 用户自定义组名称显示模板\r\n @Input() customCategoryNameTemplate: TemplateRef<any>; // 用户自定义分类名称显示模板\r\n @Input() tagMaxWidth: number;\r\n @Input() textConfig: TextConfig = {\r\n keywordName: '',\r\n createFilter: '',\r\n filterTitle: '',\r\n labelConnector: '|',\r\n noCategoriesAvailable: '',\r\n };\r\n @Input() filterNameRules: DValidateRules = [];\r\n @Input() beforeTagChange: (tag, searchKey, operation) => boolean | Promise<boolean> | Observable<boolean>;\r\n @Input() toggleEvent: (dropdown, tag?, currentSelectTag?) => void;\r\n @Input() @WithConfig() styleType = 'default';\r\n @Input() @WithConfig() showGlowStyle = true;\r\n @HostBinding('class.devui-glow-style') get hasGlowStyle() {\r\n return this.showGlowStyle;\r\n }\r\n @Output() searchEvent = new EventEmitter<SearchEvent>();\r\n @Output() selectedTagsChange = new EventEmitter<SelectedTagsEvent>();\r\n @Output() createFilterEvent = new EventEmitter<CreateFilterEvent>();\r\n @Output() clearAllEvent = new EventEmitter<MouseEvent>();\r\n @Output() searchKeyChange = new EventEmitter<String>();\r\n @ViewChild('InputEle', { static: true }) inputEle: ElementRef;\r\n @ViewChild('ScrollBarContainer', { static: true }) scrollBarContainer: ElementRef;\r\n @ViewChild('PrimeContainer', { static: true }) primeContainer: ElementRef;\r\n @ViewChild('OperableTree') treeInstance: OperableTreeComponent;\r\n @ViewChildren('selectedDropdown') selectedDropdownList: QueryList<DropDownDirective>;\r\n @ViewChildren(DefaultTemplateDirective) defaultTemplates: QueryList<DefaultTemplateDirective>;\r\n @ContentChildren(ContentTemplateDirective) contentTemplates: QueryList<ContentTemplateDirective>;\r\n\r\n id: number;\r\n searchField: ICategorySearchTagItem[];\r\n categoryDisplay: ICategorySearchTagItem[];\r\n currentSearchCategory: ICategorySearchTagItem[];\r\n currentSelectTag: ICategorySearchTagItem;\r\n dateConverter: DateConverter;\r\n treeSearchKey = '';\r\n filterName = '';\r\n searchKeyCache = '';\r\n enterSearch = false;\r\n isShowSavePanel = false;\r\n isSearchCategory = false;\r\n isHover = false;\r\n isFocus = false;\r\n treeNoRecord = false;\r\n showNoDataTips = false;\r\n icons = DefaultIcons;\r\n destroy$ = new Subject<void>();\r\n i18nCommonText: I18nInterface['common'];\r\n i18nCategorySearchText: I18nInterface['categorySearch'];\r\n showSearchConfig: SearchConfig;\r\n currentOpenDropdown: DropDownDirective;\r\n currentScrollTagIndex: number; // 当前要滚动至的标签索引\r\n blurTimer: any; // 失焦关闭下拉延时器,失焦后立刻展开下拉需清除该延时\r\n scrollTimeout: any; // 如果标签在可视范围内则延时展开下拉的定时器\r\n scrollToTailFlag = true; // 是否在更新标签内容后滚动至输入框的开关\r\n DROPDOWN_ANIMATION_TIMEOUT = 200; // 下拉动画延迟\r\n DELAY = 300; // 防抖延迟\r\n templates = {}; // 所有类型默认模板\r\n customTemplates = {}; // 按field标记的自定义模板集合\r\n treeFactories = {}; // 按field存储treeSelect类型的treefactory数据\r\n joinLabelTypes = ['checkbox', 'label'];\r\n valueIsArrayTypes = ['dateRange', 'numberRange', 'treeSelect'];\r\n document: Document;\r\n operationConfig: ExtendedConfig = {\r\n clear: { show: true },\r\n save: { show: true },\r\n more: { show: false },\r\n };\r\n\r\n get showFilterNameClear() {\r\n return typeof this.filterName === 'string' && this.filterName.length > 0;\r\n }\r\n\r\n get showExtendedConfig() {\r\n return this.operationConfig.show ?? Boolean(this.selectedTags.length || this.searchKey);\r\n }\r\n\r\n private categoryOrder = [];\r\n private categoryDictionary = {};\r\n\r\n constructor(\r\n @Inject(DOCUMENT) private doc: any,\r\n private devConfigService: DevConfigService,\r\n private i18n: I18nService,\r\n private el: ElementRef\r\n ) {\r\n this.document = this.doc;\r\n this.dateConverter = new DefaultDateConverter();\r\n this.id = CategorySearchComponent.ID_SEED++;\r\n this.showSearchConfig = { keyword: true, field: true, category: true, noCategoriesAvailableTip: true };\r\n this.i18n\r\n .langChange()\r\n .pipe(takeUntil(this.destroy$))\r\n .subscribe((data) => this.setI18nAndFilterText(data));\r\n }\r\n\r\n ngOnChanges(changes: SimpleChanges): void {\r\n const {\r\n allowClear,\r\n allowSave,\r\n allowShowMore,\r\n defaultSearchField,\r\n disabled,\r\n category,\r\n extendedConfig,\r\n selectedTags,\r\n searchKey,\r\n showSearchCategory,\r\n textConfig,\r\n } = changes;\r\n const hasExtendChanged = [allowClear, allowSave, allowShowMore, extendedConfig].some((change) => change);\r\n if (searchKey) {\r\n // searchKey输入变化更新缓存,优先处理searchKey避免初始化时再添加关键字\r\n this.searchKeyCache = this.searchKey;\r\n this.setSearchKeyTag(false);\r\n }\r\n if (selectedTags?.previousValue?.length) {\r\n const result = intersectionBy([selectedTags.previousValue], selectedTags.currentValue, 'field');\r\n result.forEach((selectedTag: any) => {\r\n const item = this.category.find((categoryItem) => categoryItem.field === selectedTag.field);\r\n if (item?.value.value) {\r\n this.resetValue(item);\r\n }\r\n });\r\n }\r\n if (defaultSearchField || category || selectedTags) {\r\n this.init();\r\n }\r\n if (disabled) {\r\n this.setDisabled();\r\n }\r\n if (showSearchCategory) {\r\n this.setSearchShow();\r\n }\r\n if (textConfig) {\r\n this.setI18nAndFilterText(this.i18n.getI18nText());\r\n }\r\n if (hasExtendChanged) {\r\n this.setExtendedConfig();\r\n }\r\n }\r\n\r\n ngAfterViewInit() {\r\n // 获取所有默认模板,规避脏检查添加延时\r\n setTimeout(() =>\r\n this.defaultTemplates.forEach((item) => {\r\n this.templates[item.type || item.dType] = item.template;\r\n })\r\n );\r\n\r\n if (this.scrollBarContainer && this.inputEle) {\r\n // 初始化如果有滚动条直接位移至输入框\r\n this.scrollToTail(true);\r\n // 监听滚动事件,在跳转至指定标签后触发其下拉列表\r\n fromEvent(this.scrollBarContainer.nativeElement, 'scroll')\r\n .pipe(\r\n takeUntil(this.destroy$),\r\n tap(() => {\r\n // 指定标签在可视范围则展开下拉,不在可视范围,即触发滚动后清除定时的展开下拉动作\r\n if (this.scrollTimeout) {\r\n clearTimeout(this.scrollTimeout);\r\n this.scrollTimeout = undefined;\r\n }\r\n // TODO: 无法区分手动和自动滚动,待处理滚动时关闭已打开的下拉\r\n }),\r\n // 300毫秒内不再触发滚动事件则展开下拉列表\r\n debounceTime(this.DELAY)\r\n )\r\n .subscribe((event: Event) => this.openCurrentScrollTagMenu(event));\r\n }\r\n }\r\n\r\n ngAfterContentInit() {\r\n this.setCustomTemplate(this.contentTemplates);\r\n this.contentTemplates.changes.subscribe((data) => this.setCustomTemplate(data));\r\n }\r\n\r\n ngOnDestroy() {\r\n this.destroy$.next();\r\n this.destroy$.complete();\r\n }\r\n\r\n setCustomTemplate(data) {\r\n if (data?.length && this.category) {\r\n this.customTemplates = {};\r\n data.forEach((item) => {\r\n this.customTemplates[item.field || item.dField] = item.template;\r\n });\r\n this.category.forEach((tag) => {\r\n tag.customTemplate = this.customTemplates[tag.field];\r\n });\r\n this.selectedTags.forEach((tag) => {\r\n tag.customTemplate = this.customTemplates[tag.field];\r\n });\r\n }\r\n }\r\n\r\n setDisabled() {\r\n if (this.disabled) {\r\n this.searchKey = '';\r\n this.operationConfig.show = false;\r\n } else {\r\n this.operationConfig.show = undefined;\r\n this.setExtendedConfig();\r\n }\r\n }\r\n\r\n setI18nAndFilterText(data) {\r\n this.i18nCommonText = data.common;\r\n this.i18nCategorySearchText = data.categorySearch;\r\n this.textConfig.createFilter = this.textConfig.createFilter || this.i18nCategorySearchText?.saveFilter;\r\n this.textConfig.filterTitle = this.textConfig.filterTitle || this.i18nCategorySearchText?.filterTitle;\r\n this.showSearchConfig.keywordDescription =\r\n (this.showSearchCategory as SearchConfig).keywordDescription || this.i18nCategorySearchText.getSearchMessage;\r\n this.showSearchConfig.fieldDescription =\r\n (this.showSearchCategory as SearchConfig).fieldDescription || this.i18nCategorySearchText.getFindingMessage;\r\n this.showSearchConfig.categoryDescription =\r\n (this.showSearchCategory as SearchConfig).categoryDescription || this.i18nCategorySearchText.selectFilterCondition;\r\n // 关键字分类内文本不能随语言对象变化,需重新赋值\r\n const keyword = this.selectedTags.find((item) => item.field === 'devuiCategorySearchKeyword');\r\n if (keyword) {\r\n keyword.label = this.textConfig.keywordName || this.i18nCategorySearchText.keyword;\r\n keyword.title = `${keyword.label}:${keyword.value?.label}`;\r\n }\r\n }\r\n\r\n setSearchShow() {\r\n const customConfig =\r\n typeof this.showSearchCategory === 'boolean'\r\n ? {\r\n keyword: this.showSearchCategory,\r\n field: this.showSearchCategory,\r\n category: this.showSearchCategory,\r\n noCategoriesAvailableTip: this.showSearchCategory,\r\n }\r\n : this.showSearchCategory;\r\n this.showSearchConfig = { ...this.showSearchConfig, ...customConfig };\r\n }\r\n\r\n setSearchKeyTag(isSearch = true) {\r\n const result = this.searchKey || this.searchKeyCache;\r\n if (this.showSearchConfig.keyword) {\r\n const existingSearchKeyTag = this.selectedTags.find((tag) => tag.field === 'devuiCategorySearchKeyword');\r\n if (existingSearchKeyTag && !isSearch && this.searchKey === '') {\r\n this.removeTag(existingSearchKeyTag);\r\n } else if (this.searchKey && this.searchKey !== existingSearchKeyTag?.value.value) {\r\n this.createSearchKeyTag(isSearch);\r\n }\r\n }\r\n this.searchKey = '';\r\n if (isSearch) {\r\n setTimeout(() => {\r\n this.enterSearch = false;\r\n }, this.DELAY);\r\n }\r\n return result;\r\n }\r\n\r\n createSearchKeyTag(isSearch: boolean) {\r\n const label = this.textConfig.keywordName || this.i18nCategorySearchText.keyword;\r\n const searchKeyTag: ICategorySearchTagItem = {\r\n options: [],\r\n field: 'devuiCategorySearchKeyword',\r\n label: label,\r\n type: 'keyword',\r\n title: `${label}:${this.searchKey}`,\r\n value: {\r\n label: this.searchKey,\r\n value: this.searchKey,\r\n cache: this.searchKey,\r\n },\r\n };\r\n this.updateSelectedTags(searchKeyTag, isSearch);\r\n this.searchKeyCache = this.searchKey;\r\n this.searchKey = '';\r\n }\r\n\r\n setExtendedConfig() {\r\n const oldConfig: ExtendedConfig = {\r\n clear: { show: this.allowClear },\r\n save: { show: this.allowSave },\r\n more: { show: this.allowShowMore },\r\n };\r\n merge(oldConfig, this.extendedConfig);\r\n merge(this.operationConfig, oldConfig);\r\n }\r\n\r\n init() {\r\n this.setValue(this.category);\r\n this.setValue(this.selectedTags, true);\r\n this.initCategoryDisplay(true);\r\n if (this.defaultSearchField?.length && this.category?.length) {\r\n this.searchField = this.category.filter(\r\n (item) => this.defaultSearchField.includes(item.field) && !this.valueIsArrayTypes.includes(item.type)\r\n );\r\n }\r\n // 初始化时判断已选中分类中最后一项是否赋值,未赋值则识别为正在处理的分类,优先显示赋值下拉列表\r\n if (this.selectedTags.length) {\r\n const [lastItem] = this.selectedTags.slice(-1);\r\n const isNull = lastItem.value[lastItem.filterKey || 'label'] === undefined;\r\n this.currentSelectTag =\r\n isNull && (lastItem.value.value === undefined || (Array.isArray(lastItem.value.value) && lastItem.value.value.length === 0))\r\n ? lastItem\r\n : undefined;\r\n }\r\n if (this.searchKeyCache) {\r\n this.searchKey = this.searchKeyCache;\r\n this.setSearchKeyTag(false);\r\n }\r\n }\r\n\r\n // 初始化tag的value属性:{filterKey | label, value, data}\r\n initCategoryItem(item) {\r\n const preValue = this.valueIsArrayTypes.includes(item.type) ? { value: [] } : { value: undefined };\r\n preValue[item.filterKey || 'label'] = undefined;\r\n if (item.value) {\r\n for (const prop in preValue) {\r\n if (item.value[prop] === undefined) {\r\n item.value[prop] = preValue[prop];\r\n }\r\n }\r\n } else {\r\n item.value = preValue;\r\n }\r\n item.value.cache = (item.value.value && typeof item.value.value === 'object' && cloneDeep(item.value.value)) || item.value.value;\r\n item.customTemplate = this.customTemplates[item.field || item.dField] || item.customTemplate;\r\n return item;\r\n }\r\n\r\n mergeCheck(objValue, srcValue, key) {\r\n if (key === 'options' && objValue !== srcValue) {\r\n return srcValue;\r\n }\r\n }\r\n\r\n setValue(data: any, isSelectedTags = false) {\r\n if (Array.isArray(data) && data.length) {\r\n data.forEach((item) => {\r\n if (!isSelectedTags || !this.category) {\r\n item = this.initCategoryItem(item);\r\n return;\r\n }\r\n let result = '';\r\n const originItem = this.category.find((categoryItem) => categoryItem.field === item.field);\r\n mergeWith(item, originItem, this.mergeCheck);\r\n if (item.value.value) {\r\n if (item.type === 'treeSelect') {\r\n this.getTreeValue(item, false);\r\n return;\r\n }\r\n item.value.cache = cloneDeep(item.value.value);\r\n if (this.joinLabelTypes.includes(item.type)) {\r\n result = this.getItemValue(item.value.value, item.filterKey || 'label');\r\n this.isSelectAll(item);\r\n }\r\n }\r\n item.title = this.setTitle(item, item.type, result);\r\n if (item.type === 'label' && item.options[0] && !item.options[0].$label) {\r\n this.mergeToLabel(item);\r\n }\r\n });\r\n }\r\n }\r\n\r\n setTitle(tag: ICategorySearchTagItem, type: string, result?: string) {\r\n return this.joinLabelTypes.includes(type)\r\n ? `${tag.label}: ${result || ''}`\r\n : `${tag.label}: ${result || (tag.value && tag.value[tag.filterKey || 'label']) || ''}`;\r\n }\r\n\r\n initCategoryDisplay(isInit = false) {\r\n const selectedTagsField = this.selectedTags.map((item) => item.field);\r\n if (isInit) {\r\n this.category = this.category || [];\r\n this.categoryOrder = [];\r\n this.categoryDictionary = {};\r\n this.initGroupAndDictionary();\r\n this.initCategoryOrder();\r\n }\r\n this.categoryDisplay = this.categoryOrder.map((item) => {\r\n item.isSelected = selectedTagsField.includes(item.field);\r\n this.handleGroupLength(item, item.isSelected, isInit);\r\n return item;\r\n });\r\n this.showNoDataTips = this.showSearchConfig.noCategoriesAvailableTip ? this.categoryDisplay.every((item) => item.isSelected) : false;\r\n }\r\n\r\n initGroupAndDictionary() {\r\n this.category.forEach((item) => {\r\n if (this.categoryInGroup && item.group) {\r\n if (this.categoryDictionary[item.group]) {\r\n this.categoryDictionary[item.group].groupLength++;\r\n } else {\r\n this.categoryDictionary[item.group] = { groupName: item.group, groupLength: 1, children: [] };\r\n }\r\n this.categoryDictionary[item.group].children.push(item);\r\n }\r\n this.categoryDictionary[item.field] = item;\r\n });\r\n }\r\n\r\n initCategoryOrder() {\r\n const keys = this.groupOrderConfig || Object.keys(this.categoryDictionary);\r\n keys.forEach((key) => {\r\n const item = this.categoryDictionary[key];\r\n if (item) {\r\n if (this.categoryInGroup) {\r\n if (item.groupName) {\r\n this.categoryOrder.push(item, ...item.children);\r\n } else if (!item.group) {\r\n this.categoryOrder.push(item);\r\n }\r\n } else {\r\n this.categoryOrder.push(item);\r\n }\r\n }\r\n });\r\n }\r\n\r\n handleGroupLength(tag: ICategorySearchTagItem, isSelected: boolean, isInit = false) {\r\n if (this.categoryInGroup && tag.group) {\r\n const group = this.categoryDictionary[tag.group];\r\n const len = group.groupLength;\r\n const resInit = isInit ? len : len + 1;\r\n group.groupLength = isSelected ? len - 1 : resInit;\r\n group.isSelected = group.groupLength === 0;\r\n }\r\n }\r\n\r\n insertCategoryToGroup(groupName: string, groupObj: object, item: ICategorySearchTagItem, isInsert: boolean) {\r\n if (!groupName) {\r\n return;\r\n }\r\n groupObj[groupName] = groupObj[groupName] || [];\r\n if (isInsert) {\r\n groupObj[groupName].push(item);\r\n }\r\n }\r\n\r\n searchCategory(item: ICategorySearchTagItem, value?: string) {\r\n if (this.valueIsArrayTypes.includes(item.type)) {\r\n return;\r\n }\r\n this.updateFieldValue(item, value || this.searchKey);\r\n this.updateSelectedTags(item);\r\n this.searchKey = '';\r\n this.enterSearch = false;\r\n this.finishChoose();\r\n }\r\n\r\n searchInputValue(event?: Event) {\r\n if (event) {\r\n event.preventDefault();\r\n event.stopPropagation();\r\n }\r\n // 当有分类正在选择时输入关键字不处理\r\n if (!this.currentSelectTag) {\r\n this.searchEvent.emit({\r\n selectedTags: this.getSelectedTagsExceptKeyword(),\r\n searchKey: this.setSearchKeyTag(),\r\n });\r\n }\r\n this.isFocus = true;\r\n }\r\n\r\n chooseCategory(item: ICategorySearchTagItem, inputDropdown: DropDownDirective) {\r\n // 点选分组名称不处理\r\n if (item.groupLength !== undefined) {\r\n return;\r\n }\r\n setTimeout(() => {\r\n this.currentSelectTag = item;\r\n if (this.currentSelectTag.type === 'label') {\r\n this.currentSelectTag = this.mergeToLabel(this.currentSelectTag);\r\n }\r\n this.currentSelectTag.title = this.setTitle(this.currentSelectTag, this.currentSelectTag.type, '');\r\n inputDropdown.isOpen = true;\r\n }, this.DROPDOWN_ANIMATION_TIMEOUT);\r\n this.updateSelectedTags(item, false);\r\n }\r\n\r\n getSelectedTagsExceptKeyword(): ICategorySearchTagItem[] {\r\n return this.showSearchConfig.keyword\r\n ? this.selectedTags.filter((item) => item.field !== 'devuiCategorySearchKeyword')\r\n : this.selectedTags;\r\n }\r\n\r\n updateSelectedTags(tag: ICategorySearchTagItem, valueChanged = true) {\r\n this.canChange(tag, 'add').then((val) => {\r\n if (!val) {\r\n return;\r\n }\r\n const index = this.selectedTags.map((item) => item.field).indexOf(tag.field);\r\n if (index > -1) {\r\n if (!tag.value.value) {\r\n // 通过输入选择分类时避免空值覆盖已选值\r\n merge(tag, this.selectedTags[index]);\r\n }\r\n this.selectedTags[index] = tag;\r\n } else {\r\n this.selectedTags.push(tag);\r\n }\r\n if (valueChanged) {\r\n // 只在新增标签时位移滚动条\r\n if (this.scrollToTailFlag) {\r\n setTimeout(() => this.scrollToTail());\r\n }\r\n this.selectedTagsChange.emit({\r\n selectedTags: this.getSelectedTagsExceptKeyword(),\r\n currentChangeTag: tag,\r\n operation: 'add',\r\n });\r\n this.isSearchCategory = false;\r\n } else {\r\n this.resolveCategoryDisplay(tag, 'delete');\r\n }\r\n });\r\n }\r\n\r\n // 判断滚动条是否存在,如果存在自动滚动到末尾的输入框\r\n scrollToTail(isInit?: boolean) {\r\n const dom = this.scrollBarContainer.nativeElement;\r\n if (this.toggleScrollToTail && dom.scrollWidth > dom.clientWidth) {\r\n if (isInit) {\r\n dom.scrollLeft = dom.scrollWidth - dom.clientWidth;\r\n } else {\r\n this.inputEle.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });\r\n }\r\n } else if (!isInit && this.inputAutofocus) {\r\n // 初始化不聚焦,避免展开下拉\r\n this.inputEle.nativeElement.focus();\r\n }\r\n }\r\n\r\n // 滚动至指定的tag\r\n scrollToTag(index: number, event: Event) {\r\n const tags = this.scrollBarContainer?.nativeElement.querySelectorAll('ul.devui-category-search-line>li');\r\n if (tags[index]) {\r\n this.currentScrollTagIndex = index;\r\n tags[index].scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });\r\n this.scrollTimeout = setTimeout(() => this.openCurrentScrollTagMenu(event), this.DELAY);\r\n }\r\n }\r\n\r\n openCurrentScrollTagMenu(event: Event) {\r\n if (this.currentScrollTagIndex !== undefined) {\r\n const dropdownArr = this.selectedDropdownList.toArray();\r\n this.openMenu(dropdownArr[this.currentScrollTagIndex], event);\r\n this.currentScrollTagIndex = undefined;\r\n }\r\n }\r\n\r\n updateFieldValue(field: ICategorySearchTagItem, value: any) {\r\n const result: any = {};\r\n const filterKey = field.filterKey || 'label';\r\n const colorKey = field.colorKey || 'color';\r\n result[filterKey] = value;\r\n if (field.type === 'radio') {\r\n field.value.value = { [filterKey]: value };\r\n }\r\n if (field.type === 'textInput') {\r\n field.value.value = value;\r\n }\r\n if (field.type === 'label') {\r\n if (field.options[0] && !field.options[0].$label) {\r\n this.mergeToLabel(field);\r\n }\r\n result[colorKey] = COLORS[Math.floor(COLORS.length * Math.random())];\r\n result.$label = `${value}_${result[colorKey]}`;\r\n }\r\n if (this.joinLabelTypes.includes(field.type)) {\r\n field.value.value = [result];\r\n }\r\n field.value[filterKey] = value;\r\n field.value.cache = cloneDeep(field.value.value);\r\n field.title = this.setTitle(field, field.type, value);\r\n }\r\n\r\n removeTag(tag: ICategorySearchTagItem, event?: Event) {\r\n this.canChange(tag, 'delete').then((val) => {\r\n if (!val) {\r\n if (this.beforeTagChange && event) {\r\n event.stopPropagation();\r\n }\r\n return;\r\n }\r\n tag = this.resetValue(tag);\r\n this.selectedTags = this.selectedTags.filter((item) => item.field !== tag.field);\r\n const result = this.getSelectedTagsExceptKeyword();\r\n if (tag.type === 'keyword') {\r\n this.searchKey = this.searchKey === this.searchKeyCache ? '' : this.searchKey;\r\n this.searchKeyCache = '';\r\n this.enterSearch = this.searchKey !== '';\r\n this.searchEvent.emit({ selectedTags: result, searchKey: this.searchKey });\r\n } else {\r\n if(tag.type === 'treeSelect'){\r\n this.treeFactories[tag.field] = undefined;\r\n }\r\n this.resolveCategoryDisplay(tag, 'add');\r\n this.selectedTagsChange.emit({ selectedTags: result, currentChangeTag: tag, operation: 'delete' });\r\n }\r\n this.currentSelectTag = undefined;\r\n });\r\n }\r\n\r\n checkInputSearching() {\r\n this.isFocus = true;\r\n if (this.searchKey === '') {\r\n this.enterSearch = false;\r\n } else {\r\n this.enterSearch = true;\r\n }\r\n }\r\n\r\n // 解决dropdownMenu展开时焦点会自动到toggle上的问题\r\n focusInput() {\r\n setTimeout(() => {\r\n if (this.inputAutofocus) {\r\n this.inputEle.nativeElement.focus();\r\n }\r\n });\r\n }\r\n\r\n // 失焦时关闭当前下拉列表,延时用以防止关闭掉点击分类展开的内容列表\r\n blurInput(event: FocusEvent) {\r\n this.blurTimer = setTimeout(() => {\r\n const className = event.relatedTarget && (event.relatedTarget as any).className;\r\n const outsideHost =\r\n !className || (typeof className === 'string' && className.indexOf(`devui-category-dropdown-menu-${this.id}`) === -1);\r\n if (!this.currentSelectTag && this.currentOpenDropdown && outsideHost) {\r\n this.currentOpenDropdown.isOpen = false;\r\n }\r\n }, this.DELAY);\r\n }\r\n\r\n openMenu(inputDropdown: DropDownDirective, event: Event) {\r\n const code = (event as any).code;\r\n if (inputDropdown.isOpen || (event.type === 'keyup' && ['Enter', 'Backspace', 'Escape'].includes(code))) {\r\n return;\r\n }\r\n setTimeout(() => {\r\n if (!inputDropdown.isOpen) {\r\n inputDropdown.toggle();\r\n }\r\n });\r\n }\r\n\r\n closeMenu(inputDropdown: DropDownDirective) {\r\n if (inputDropdown.isOpen) {\r\n inputDropdown.toggle();\r\n }\r\n }\r\n\r\n backspaceEvent(inputDropdown: DropDownDirective) {\r\n if (this.searchKey) {\r\n return;\r\n }\r\n if (this.currentSelectTag) {\r\n this.currentSelectTag = undefined;\r\n this.closeMenu(inputDropdown);\r\n return;\r\n }\r\n if (this.selectedTags.length) {\r\n const tag = this.selectedTags[this.selectedTags.length - 1];\r\n this.removeTag(tag);\r\n }\r\n this.closeMenu(inputDropdown);\r\n }\r\n\r\n canChange(tag: ICategorySearchTagItem, operation: 'delete' | 'add') {\r\n let changeResult = Promise.resolve(true);\r\n if (this.beforeTagChange) {\r\n const result: any = this.beforeTagChange(tag, this.searchKey, operation);\r\n if (typeof result !== 'undefined') {\r\n if (result.then) {\r\n changeResult = result;\r\n } else if (result.subscribe) {\r\n changeResult = (result as Observable<boolean>).toPromise();\r\n } else {\r\n changeResult = Promise.resolve(result);\r\n }\r\n }\r\n }\r\n return changeResult;\r\n }\r\n\r\n finishChoose() {\r\n this.currentSelectTag = undefined;\r\n if (this.inputAutofocus) {\r\n this.inputEle.nativeElement.focus();\r\n }\r\n }\r\n\r\n clearFilter(event: MouseEvent) {\r\n if (this.selectedTags.length) {\r\n this.selectedTags = this.selectedTags.filter((item) => {\r\n if (item.deletable === false) {\r\n return item;\r\n }\r\n this.resetValue(item);\r\n return undefined;\r\n });\r\n }\r\n if (this.searchKey || this.searchKeyCache) {\r\n this.searchKey = '';\r\n this.searchKeyCache = '';\r\n setTimeout(() => {\r\n this.enterSearch = false;\r\n }, this.DELAY);\r\n }\r\n if (this.currentSelectTag) {\r\n this.currentSelectTag = undefined;\r\n }\r\n this.selectedTagsChange.emit({ selectedTags: this.selectedTags, currentChangeTag: undefined, operation: 'clear' });\r\n this.clearAllEvent.emit(event);\r\n this.initCategoryDisplay();\r\n }\r\n\r\n resolveCategoryDisplay(tag: ICategorySearchTagItem, type: string) {\r\n if (tag.field === 'devuiCategorySearchKeyword' || !this.categoryDictionary[tag.field]) {\r\n return;\r\n }\r\n this.handleGroupLength(tag, type === 'delete');\r\n this.categoryDictionary[tag.field].isSelected = type === 'delete';\r\n }\r\n\r\n createFilterFn() {\r\n this.createFilterEvent.emit({ name: this.filterName, selectedTags: this.getSelectedTagsExceptKeyword(), keyword: this.searchKey });\r\n setTimeout(() => {\r\n this.filterName = '';\r\n }, this.DELAY);\r\n }\r\n\r\n createFilterInputAutoFocus(dropdown: DropDownDirective, inputElm: HTMLElement, filterNameForm: NgForm) {\r\n if (dropdown.isOpen) {\r\n filterNameForm.form.reset();\r\n setTimeout(() => inputElm.focus());\r\n }\r\n }\r\n\r\n searchKeyChangeEvent(event: string) {\r\n this.searchKey = event;\r\n this.enterSearch = !!event;\r\n this.currentSearchCategory = event ? this.category.filter((item) => item.label.toLowerCase().includes(event.toLowerCase())) : [];\r\n this.searchKeyChange.emit(event);\r\n }\r\n\r\n checkType(tag: ICategorySearchTagItem) {\r\n return tag && tag.type === 'radio' ? 'all' : 'blank';\r\n }\r\n\r\n resetValue(tag: ICategorySearchTagItem) {\r\n tag.value = this.valueIsArrayTypes.includes(tag.type) ? { value: [] } : { value: undefined };\r\n tag.value[tag.filterKey || 'label'] = undefined;\r\n return tag;\r\n }\r\n\r\n afterDropdownClosed() {\r\n setTimeout(() => {\r\n this.currentSelectTag = undefined;\r\n }, this.DROPDOWN_ANIMATION_TIMEOUT + 100);\r\n }\r\n\r\n /* 各类型模板调用方法 */\r\n // radio 单选 处理选中项方法\r\n chooseItem(tag: ICategorySearchTagItem, chooseItem: ITagOption) {\r\n this.afterDropdownClosed();\r\n const key = tag.filterKey || 'label';\r\n tag.value = { value: chooseItem, cache: cloneDeep(chooseItem) };\r\n tag.value[key] = chooseItem[key];\r\n tag.title = this.setTitle(tag, 'radio');\r\n this.updateSelectedTags(tag);\r\n }\r\n\r\n // date 改变输入值相应变化\r\n dateChange(datepicker: DatepickerProCalendarComponent, tag: ICategorySearchTagItem) {\r\n const index = datepicker.currentActiveInput === 'start' ? 0 : 1;\r\n const dateFormat = tag.showTime ? 'y/MM/dd HH:mm:ss' : 'y/MM/dd';\r\n const targetDate = datepicker.dateValue[index];\r\n const inputDate = datepicker.datepickerConvert.parse(targetDate);\r\n const valueFormat = inputDate && !isNaN(inputDate.getTime()) && datepicker.datepickerConvert.format(inputDate, dateFormat);\r\n if (targetDate === valueFormat) {\r\n tag.value.value[index] = inputDate;\r\n if (tag.showTime) {\r\n datepicker.pickerSrv.updateTimeChange.next({\r\n activeInput: datepicker.currentActiveInput,\r\n hour: inputDate.getHours(),\r\n min: inputDate.getMinutes(),\r\n seconds: inputDate.getSeconds(),\r\n });\r\n }\r\n }\r\n }\r\n\r\n confirmDate(datepicker: DatepickerProCalendarComponent, tag: ICategorySearchTagItem, dropdown: DropDownDirective) {\r\n if (!tag.value.value) {\r\n return;\r\n }\r\n if (datepicker.currentActiveInput === 'start') {\r\n datepicker.focusChange('end');\r\n } else if (!tag.value.value[0]) {\r\n datepicker.focusChange('start');\r\n } else if (tag.value.value.length > 1) {\r\n this.closeMenu(dropdown);\r\n this.afterDropdownClosed();\r\n const start = tag.value.value[0] as Date;\r\n const end = tag.value.value[1] as Date;\r\n tag.value.cache = cloneDeep(tag.value.value);\r\n tag.value[tag.filterKey || 'label'] = tag.showTime\r\n ? `${this.dateConverter.formatDateTime(start)} - ${this.dateConverter.formatDateTime(end)}`\r\n : `${this.dateConverter.format(start)} - ${this.dateConverter.format(end)}`;\r\n tag.title = this.setTitle(tag, 'dateRange');\r\n this.updateSelectedTags(tag);\r\n }\r\n }\r\n\r\n // checkbox | label 多选 处理选中项方法\r\n chooseItems(tag: ICategorySearchTagItem) {\r\n this.afterDropdownClosed();\r\n const key = tag.filterKey || 'label';\r\n const result = this.getItemValue(tag.value.value, key);\r\n if (result) {\r\n tag.title = this.setTitle(tag, 'checkbox', result);\r\n tag.value[key] = result;\r\n tag.value.cache = cloneDeep(tag.value.value);\r\n this.updateSelectedTags(tag);\r\n } else {\r\n this.removeTag(tag);\r\n }\r\n }\r\n\r\n // checkbox | label 将选中项对应filterKey的值合并的方法\r\n getItemValue(value: any, key: string) {\r\n if (value && Array.isArray(value)) {\r\n const result = value.map((item) => item[key]);\r\n return result.join(' | ');\r\n }\r\n }\r\n\r\n checkDropdownBoundary(dropdown: DropDownDirective, dropdownMenu: DropDownAppendToBodyComponent) {\r\n if (this.dropdownBoundary && dropdownMenu) {\r\n const tagRect = dropdown.el.nativeElement.getBoundingClientRect();\r\n const hostRect = this.el.nativeElement.getBoundingClientRect();\r\n if (tagRect.left < hostRect.left) {\r\n dropdownMenu.overlay.offsetX = hostRect.left - tagRect.left;\r\n dropdownMenu.overlay.overlayRef.updatePosition();\r\n } else if (tagRect.left + tagRect.width > hostRect.left + hostRect.width) {\r\n dropdownMenu.overlay.offsetX = 0;\r\n dropdownMenu.overlay.overlayRef.updatePosition();\r\n }\r\n }\r\n }\r\n\r\n // checkbox | label 当下拉菜单展开重置多选的选中状态\r\n resetContent(dropdown: DropDownDirective, tag?: ICategorySearchTagItem, dropdownMenu?: DropDownAppendToBodyComponent) {\r\n const tagItem = tag || this.currentSelectTag || { type: '', options: [] };\r\n if (this.blurTimer) {\r\n clearTimeout(this.blurTimer);\r\n }\r\n if (this.toggleEvent) {\r\n this.toggleEvent(dropdown, tag, this.currentSelectTag);\r\n }\r\n if (dropdown.isOpen) {\r\n this.checkDropdownBoundary(dropdown, dropdownMenu);\r\n this.currentOpenDropdown = dropdown;\r\n } else {\r\n const item = tagItem as ICategorySearchTagItem;\r\n this.clearCurrentSelectTagFromSearch();\r\n this.currentOpenDropdown = undefined;\r\n this.showNoDataTips = false;\r\n delete item.allChecked;\r\n delete item.halfChecked;\r\n return;\r\n }\r\n if (tag && tag !== this.currentSelectTag) {\r\n this.scrollToTailFlag = false;\r\n if (!isEqual(tag.value.value, tag.value.cache)) {\r\n tag.value.value = cloneDeep(tag.value.cache);\r\n }\r\n this.handleAccordingType(tag, dropdown, false);\r\n } else if (this.currentSelectTag) {\r\n this.scrollToTailFlag = true;\r\n // checkbox 值不是必须数组,但重置时需空数组才能触发,undefined 不行\r\n this.currentSelectTag.value.value = ['checkbox', ...this.valueIsArrayTypes].includes(this.currentSelectTag.type) ? [] : undefined;\r\n this.handleAccordingType(this.currentSelectTag, dropdown, true);\r\n }\r\n this.showNoDataTips = this.showSearchConfig.noCategoriesAvailableTip ? this.categoryDisplay.every((item) => item.isSelected) : false;\r\n }\r\n\r\n cacheTreeData(tag: ICategorySearchTagItem) {\r\n if (tag.type === 'treeSelect') {\r\n // 下拉关闭时存储当前分类树的treeFactory\r\n this.treeFactories[tag.field] = this.treeInstance.treeFactory;\r\n }\r\n }\r\n\r\n showCurrentSearchCategory(tag: ICategorySearchTagItem, inputDropdown: DropDownDirective) {\r\n this.isSearchCategory = true;\r\n this.searchKey = '';\r\n this.closeMenu(inputDropdown);\r\n this.chooseCategory(tag, inputDropdown);\r\n setTimeout(() => this.checkInputSearching(), this.DELAY);\r\n }\r\n\r\n clearCurrentSelectTagFromSearch() {\r\n if (this.currentSelectTag && this.isSearchCategory) {\r\n this.isSearchCategory = false;\r\n setTimeout(() => this.finishChoose(), this.DELAY);\r\n }\r\n }\r\n\r\n handleAccordingType(tag: ICategorySearchTagItem, dropdown: DropDownDirective, isCurrentSelectTag: boolean) {\r\n switch (tag.type) {\r\n case 'keyword':\r\n this.searchKey = this.searchKeyCache;\r\n this.searchKeyChangeEvent(this.searchKey);\r\n this.inputEle.nativeElement.focus();\r\n dropdown.isOpen = false;\r\n break;\r\n case 'treeSelect':\r\n // 新选分类时使用原始tree数据,已选分类时使用空数据,待渲染后用存储数据覆盖\r\n if (!isCurrentSelectTag) {\r\n setTimeout(() => {\r\n // 下拉展开将存储的数据合并覆盖当前树实例,刷新当前树显示。\r\n merge(this.treeInstance.treeFactory, this.treeFactories[tag.field]);\r\n this.treeSearch('');\r\n });\r\n }\r\n setTimeout(() => {\r\n const dom = tag.searchable && dropdown.menuEl.nativeElement.querySelector('.devui-search-container .devui-search>input');\r\n if (dom) {\r\n dom.focus();\r\n }\r\n }, this.DROPDOWN_ANIMATION_TIMEOUT);\r\n break;\r\n case 'textInput':\r\n setTimeout(() => {\r\n const inputDom: HTMLElement = this.document.querySelector('.devui-category-search-type-text-input');\r\n if (inputDom) {\r\n inputDom.focus();\r\n }\r\n }, this.DROPDOWN_ANIMATION_TIMEOUT);\r\n break;\r\n case 'checkbox':\r\n case 'label':\r\n if (tag.showSelectAll) {\r\n this.isSelectAll(tag, false);\r\n }\r\n break;\r\n default:\r\n }\r\n }\r\n\r\n // label 合并名称和颜色字段赋给tag,待[tag]支持传入对象后可移除\r\n mergeToLabel(obj: any) {\r\n if (obj?.options && Array.isArray(obj.options)) {\r\n obj.options.forEach((item) => {\r\n item.$label = `${item[obj.filterKey || 'label']}_${item[obj.colorKey || 'color']}`;\r\n });\r\n }\r\n return obj;\r\n }\r\n\r\n // label 拆分名称和颜色用于下拉选项显示\r\n splitLabel(key: string, value: any) {\r\n // 初始化选中类别生成标签时,value为label的对象,不需要对值进行操作\r\n if (typeof value !== 'string') {\r\n return;\r\n }\r\n const res = value && value.split('_');\r\n const obj = res && { label: res[0], color: res[1] };\r\n return obj && obj[key];\r\n }\r\n\r\n selectAll(tag: ICategorySearchTagItem) {\r\n tag.halfChecked = false;\r\n if (tag.allChecked) {\r\n tag.value.value = cloneDeep(tag.options);\r\n } else {\r\n tag.value.value = [];\r\n }\r\n }\r\n\r\n isSelectAll(tag: ICategorySearchTagItem, isValue = true) {\r\n const value = isValue ? tag.value.value : tag.value.cache;\r\n tag.allChecked = isEqual(value, tag.options);\r\n tag.halfChecked = !tag.allChecked && !!value.length;\r\n }\r\n\r\n // textInput 文本输入框 处理选中项方法\r\n getTextInputValue(tag: ICategorySearchTagItem) {\r\n this.afterDropdownClosed();\r\n tag.value[tag.filterKey || 'label'] = tag.value.cache = tag.value.value;\r\n tag.title = this.setTitle(tag, 'textInput');\r\n this.updateSelectedTags(tag);\r\n }\r\n\r\n // numberRange 数字范围 处理选中项方法\r\n getNumberRangeValue(tag: ICategorySearchTagItem, dropdown?: DropDownDirective) {\r\n const startNum = tag.value.value[0] || 0;\r\n const endNum = tag.value.value[1] || 0;\r\n if (tag.validateFunc) {\r\n if (!tag.validateFunc(startNum, endNum, tag)) {\r\n return;\r\n }\r\n if (dropdown) {\r\n this.closeMenu(dropdown);\r\n }\r\n }\r\n this.afterDropdownClosed();\r\n tag.value.value = [startNum, endNum];\r\n tag.value.cache = [startNum, endNum];\r\n tag.value[tag.filterKey || 'label'] = `${startNum} - ${endNum}`;\r\n tag.title = this.setTitle(tag, 'numberRange');\r\n this.updateSelectedTags(tag);\r\n }\r\n\r\n // treeSelect 树 处理选中项方法\r\n getTreeValue(tag: ICategorySearchTagItem, isConfirm = true) {\r\n if (isConfirm) {\r\n this.afterDropdownClosed();\r\n }\r\n const result = [];\r\n const selectedIds = [];\r\n const data = tag.value.value as ITagOption[];\r\n data.forEach((item) => {\r\n result.push(item[tag.filterKey || tag.treeNodeTitleKey || 'title']);\r\n selectedIds.push(item[tag.treeNodeIdKey || 'id']);\r\n });\r\n if (result.length) {\r\n tag.value.cache = cloneDeep(tag.value.value);\r\n tag.value[tag.filterKey || 'label'] = result.join(',');\r\n tag.title = this.setTitle(tag, 'treeSelect');\r\n if (isConfirm) {\r\n this.cacheTreeData(tag);\r\n this.updateSelectedTags(tag);\r\n } else {\r\n tag.options = this.initTreeChecked(tag, tag.options, selectedIds);\r\n }\r\n } else {\r\n this.removeTag(tag);\r\n }\r\n }\r\n\r\n initTreeChecked(tag: ICategorySearchTagItem, tree: any, selectedIds: string[]) {\r\n if (Array.isArray(tree)) {\r\n return tree.map((item) => this.initTreeChecked(tag, item, selectedIds));\r\n } else if (tree) {\r\n const idKey = tag.treeNodeIdKey || 'id';\r\n const childKey = tag.treeNodeChildrenKey || 'children';\r\n if (Array.isArray(tree[childKey]) && ['upward', 'both', undefined].includes(tag.checkableRelation)) {\r\n let checkedChild = 0;\r\n tree[childKey] = this.initTreeChecked(tag, tree[childKey], selectedIds);\r\n tree[childKey].forEach((child) => child.isChecked && checkedChild++);\r\n tree.halfChecked = checkedChild > 0 && checkedChild