@yelon/abc
Version:
Common business components of ng-yunzai.
1 lines • 195 kB
Source Map (JSON)
{"version":3,"file":"st.mjs","sources":["../../../../packages/abc/st/st-row.directive.ts","../../../../packages/abc/st/st-widget.ts","../../../../packages/abc/st/st-column-source.ts","../../../../packages/abc/st/st-data-source.ts","../../../../packages/abc/st/st-export.ts","../../../../packages/abc/st/st-widget-host.directive.ts","../../../../packages/abc/st/st-filter.component.ts","../../../../packages/abc/st/st.config.ts","../../../../packages/abc/st/st.component.ts","../../../../packages/abc/st/st-td.component.html","../../../../packages/abc/st/st.component.html","../../../../packages/abc/st/st.module.ts","../../../../packages/abc/st/provide.ts","../../../../packages/abc/st/st.ts"],"sourcesContent":["import { Directive, Injectable, Input, OnInit, TemplateRef, inject } from '@angular/core';\n\n@Injectable()\nexport class STRowSource {\n private titles: Record<string, TemplateRef<void>> = {};\n private rows: Record<string, TemplateRef<void>> = {};\n\n add(type: string | undefined, path: string, ref: TemplateRef<void>): void {\n this[type === 'title' ? 'titles' : 'rows'][path] = ref;\n }\n\n getTitle(path: string): TemplateRef<void> {\n return this.titles[path];\n }\n\n getRow(path: string): TemplateRef<void> {\n return this.rows[path];\n }\n}\n\n@Directive({\n selector: '[st-row]'\n})\nexport class STRowDirective implements OnInit {\n private readonly source = inject(STRowSource, { host: true });\n private readonly ref = inject(TemplateRef);\n @Input('st-row') id!: string;\n\n @Input() type?: 'title';\n\n ngOnInit(): void {\n this.source.add(this.type, this.id, this.ref);\n }\n}\n","import { Injectable } from '@angular/core';\n\n@Injectable({ providedIn: 'root' })\nexport class STWidgetRegistry {\n private _widgets: Record<string, any> = {};\n\n get widgets(): any {\n return this._widgets;\n }\n\n register(type: string, widget: any): void {\n this._widgets[type] = widget;\n }\n\n has(type: string): boolean {\n return Object.prototype.hasOwnProperty.call(this._widgets, type);\n }\n\n get(type: string): any {\n return this._widgets[type];\n }\n}\n","import { inject, Injectable, TemplateRef } from '@angular/core';\nimport { DomSanitizer } from '@angular/platform-browser';\n\nimport { ACLService } from '@yelon/acl';\nimport { YUNZAI_I18N_TOKEN } from '@yelon/theme';\nimport { YunzaiSTConfig } from '@yelon/util/config';\nimport { deepCopy, warn } from '@yelon/util/other';\nimport type { NgClassInterface, NzSafeAny } from 'ng-zorro-antd/core/types';\n\nimport { STRowSource } from './st-row.directive';\nimport { STWidgetRegistry } from './st-widget';\nimport {\n STColumn,\n STColumnButton,\n STColumnButtonPop,\n STColumnFilter,\n STColumnGroupType,\n STColumnSafeType,\n STIcon,\n STResizable,\n STSortMap,\n STWidthMode\n} from './st.interfaces';\nimport { _STColumn, _STHeader } from './st.types';\n\nexport interface STColumnSourceProcessOptions {\n widthMode: STWidthMode;\n resizable?: STResizable;\n safeType: STColumnSafeType;\n expand: boolean;\n}\n\n@Injectable()\nexport class STColumnSource {\n private readonly dom = inject(DomSanitizer);\n private readonly rowSource = inject(STRowSource, { host: true });\n private readonly acl = inject(ACLService, { optional: true });\n private readonly i18nSrv = inject(YUNZAI_I18N_TOKEN, { optional: true });\n private readonly stWidgetRegistry = inject(STWidgetRegistry);\n private cog!: YunzaiSTConfig;\n\n setCog(val: YunzaiSTConfig): void {\n this.cog = val;\n }\n\n private fixPop(i: STColumnButton, def: STColumnButtonPop): void {\n if (i.pop == null || i.pop === false) {\n i.pop = false;\n return;\n }\n\n let pop: STColumnButtonPop = {\n okType: 'primary',\n ...def\n };\n if (typeof i.pop === 'string') {\n pop.title = i.pop;\n } else if (typeof i.pop === 'object') {\n pop = {\n ...pop,\n ...i.pop\n };\n }\n\n if (typeof pop.condition !== 'function') {\n pop.condition = () => false;\n }\n\n if (this.i18nSrv) {\n if (pop.titleI18n) pop.title = this.i18nSrv.fanyi(pop.titleI18n);\n if (pop.okTextI18n) pop.okText = this.i18nSrv.fanyi(pop.okTextI18n);\n if (pop.cancelTextI18n) pop.cancelText = this.i18nSrv.fanyi(pop.cancelTextI18n);\n }\n\n i.pop = pop;\n }\n\n private btnCoerce(list: STColumnButton[]): STColumnButton[] {\n if (!list) return [];\n const ret: STColumnButton[] = [];\n const { modal, drawer, pop, btnIcon } = this.cog;\n\n for (const item of list) {\n if (this.acl && item.acl && !this.acl.can(item.acl)) {\n continue;\n }\n\n if (item.type === 'modal' || item.type === 'static') {\n if (item.modal == null || item.modal.component == null) {\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\n console.warn(`[st] Should specify modal parameter when type is modal or static`);\n }\n item.type = 'none';\n } else {\n item.modal = { paramsName: 'record', size: 'lg', ...modal, ...item.modal };\n }\n }\n\n if (item.type === 'drawer') {\n if (item.drawer == null || item.drawer.component == null) {\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\n console.warn(`[st] Should specify drawer parameter when type is drawer`);\n }\n item.type = 'none';\n } else {\n item.drawer = { paramsName: 'record', size: 'lg', ...drawer, ...item.drawer };\n }\n }\n\n if (item.type === 'del' && typeof item.pop === 'undefined') {\n item.pop = true;\n }\n\n // pop\n this.fixPop(item, pop!);\n\n if (typeof item.icon !== 'function') {\n item.icon = {\n ...btnIcon,\n ...(typeof item.icon === 'string' ? { type: item.icon } : item.icon)\n } as STIcon;\n }\n\n item.children = item.children && item.children.length > 0 ? this.btnCoerce(item.children) : [];\n\n // i18n\n if (item.i18n && this.i18nSrv) {\n item.text = this.i18nSrv.fanyi(item.i18n);\n }\n\n ret.push(item);\n }\n this.btnCoerceIf(ret);\n return ret;\n }\n\n private btnCoerceIf(list: STColumnButton[]): void {\n for (const item of list) {\n item.iifBehavior = item.iifBehavior || this.cog.iifBehavior;\n if (item.children && item.children.length > 0) {\n this.btnCoerceIf(item.children);\n } else {\n item.children = [];\n }\n }\n }\n\n private fixedCoerce(list: _STColumn[], expand: boolean): void {\n const countReduce = (a: number, b: _STColumn): number => a + +b.width!.toString().replace('px', '');\n const expandWidth = expand ? 50 : 0;\n // left width\n list\n .filter(w => w.fixed && w.fixed === 'left' && w.width)\n .forEach((item, idx) => (item._left = `${list.slice(0, idx).reduce(countReduce, 0) + expandWidth}px`));\n // right width\n list\n .filter(w => w.fixed && w.fixed === 'right' && w.width)\n .reverse()\n .forEach((item, idx) => (item._right = `${idx > 0 ? list.slice(-idx).reduce(countReduce, 0) : 0}px`));\n }\n\n private sortCoerce(item: _STColumn): STSortMap {\n const res = this.fixSortCoerce(item);\n res.reName = {\n ...this.cog.sortReName,\n ...res.reName\n };\n return res;\n }\n\n private fixSortCoerce(item: _STColumn): STSortMap {\n if (typeof item.sort === 'undefined') {\n return { enabled: false };\n }\n\n let res: STSortMap = {};\n\n if (typeof item.sort === 'string') {\n if (item.sort === 'ascend' || item.sort === 'descend') {\n res.directions = [item.sort, null];\n } else {\n res.key = item.sort;\n }\n } else if (typeof item.sort !== 'boolean') {\n res = item.sort;\n } else if (typeof item.sort === 'boolean') {\n res.compare = (a, b) => a[item.indexKey!] - b[item.indexKey!];\n }\n\n if (!res.key) {\n res.key = item.indexKey;\n }\n\n if (!Array.isArray(res.directions)) {\n res.directions = this.cog.sortDirections ?? ['ascend', 'descend', null];\n }\n\n res.enabled = true;\n\n return res;\n }\n\n private filterCoerce(item: _STColumn): STColumnFilter | null {\n if (item.filter == null) {\n return null;\n }\n\n let res: STColumnFilter | null = item.filter;\n res.type = res.type || 'default';\n res.showOPArea = res.showOPArea !== false;\n\n let icon = 'filter';\n let iconTheme = 'fill';\n let fixMenus = true;\n let value: NzSafeAny = undefined;\n switch (res.type) {\n case 'keyword':\n icon = 'search';\n iconTheme = 'outline';\n break;\n case 'number':\n icon = 'search';\n iconTheme = 'outline';\n res.number = {\n step: 1,\n min: -Infinity,\n max: Infinity,\n ...res.number\n };\n break;\n case 'date':\n icon = 'calendar';\n iconTheme = 'outline';\n res.date = {\n range: false,\n mode: 'date',\n showToday: true,\n showNow: false,\n ...res.date\n };\n break;\n case 'custom':\n break;\n default:\n fixMenus = false;\n break;\n }\n if (fixMenus && (res.menus == null || res.menus!.length === 0)) {\n res.menus = [{ value }];\n }\n\n if (res.menus?.length === 0) {\n return null;\n }\n\n if (typeof res.multiple === 'undefined') {\n res.multiple = true;\n }\n\n res.confirmText = res.confirmText || this.cog.filterConfirmText;\n res.clearText = res.clearText || this.cog.filterClearText;\n res.key = res.key || item.indexKey;\n res.icon = res.icon || icon;\n\n const baseIcon = { type: icon, theme: iconTheme } as STIcon;\n if (typeof res.icon === 'string') {\n res.icon = { ...baseIcon, type: res.icon } as STIcon;\n } else {\n res.icon = { ...baseIcon, ...res.icon };\n }\n\n this.updateDefault(res);\n\n if (this.acl) {\n res.menus = res.menus?.filter(w => this.acl!.can(w.acl!));\n }\n\n return res.menus?.length === 0 ? null : res;\n }\n\n private restoreRender(item: _STColumn): void {\n if (item.renderTitle) {\n item.__renderTitle =\n typeof item.renderTitle === 'string'\n ? this.rowSource.getTitle(item.renderTitle)\n : (item.renderTitle as TemplateRef<void>);\n }\n if (item.render) {\n item.__render =\n typeof item.render === 'string' ? this.rowSource.getRow(item.render) : (item.render as TemplateRef<void>);\n }\n }\n\n private widgetCoerce(item: _STColumn): void {\n if (item.type !== 'widget') return;\n if (item.widget == null || !this.stWidgetRegistry.has(item.widget.type)) {\n delete item.type;\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\n warn(`st: No widget for type \"${item.widget?.type}\"`);\n }\n }\n }\n\n private genHeaders(rootColumns: _STColumn[]): { headers: _STHeader[][]; headerWidths: string[] | null } {\n const rows: _STHeader[][] = [];\n const widths: string[] = [];\n const fillRowCells = (columns: _STColumn[], colIndex: number, rowIndex = 0): number[] => {\n // Init rows\n rows[rowIndex] = rows[rowIndex] || [];\n\n let currentColIndex = colIndex;\n const colSpans: number[] = columns.map(column => {\n const cell: STColumnGroupType = {\n column,\n colStart: currentColIndex,\n hasSubColumns: false\n };\n\n let colSpan = 1;\n\n const subColumns = column.children;\n if (Array.isArray(subColumns) && subColumns.length > 0) {\n colSpan = fillRowCells(subColumns, currentColIndex, rowIndex + 1).reduce((total, count) => total + count, 0);\n cell.hasSubColumns = true;\n } else {\n widths.push((cell.column.width as string) || '');\n }\n\n if ('colSpan' in column) {\n colSpan = column.colSpan!;\n }\n\n if ('rowSpan' in column) {\n cell.rowSpan = column.rowSpan;\n }\n\n cell.colSpan = colSpan;\n cell.colEnd = cell.colStart + colSpan - 1;\n rows[rowIndex].push(cell as NzSafeAny);\n\n currentColIndex += colSpan;\n\n return colSpan;\n });\n\n return colSpans;\n };\n\n fillRowCells(rootColumns, 0);\n\n // Handle `rowSpan`\n const rowCount = rows.length;\n for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {\n rows[rowIndex].forEach(cell => {\n if (!('rowSpan' in cell) && !(cell as _STHeader).hasSubColumns) {\n (cell as _STHeader).rowSpan = rowCount - rowIndex;\n }\n });\n }\n\n return { headers: rows, headerWidths: rowCount > 1 ? widths : null };\n }\n\n private cleanCond(list: _STColumn[]): _STColumn[] {\n const res: _STColumn[] = [];\n const copyList = deepCopy(list);\n for (const item of copyList) {\n if (typeof item.iif === 'function' && !item.iif(item)) {\n continue;\n }\n if (this.acl && item.acl && !this.acl.can(item.acl)) {\n continue;\n }\n if (Array.isArray(item.children) && item.children.length > 0) {\n item.children = this.cleanCond(item.children);\n }\n res.push(item);\n }\n return res;\n }\n\n private mergeClass(item: _STColumn): void {\n const builtInClassNames: string[] = [];\n if (item._isTruncate) {\n builtInClassNames.push('text-truncate');\n }\n const rawClassName = item.className;\n if (!rawClassName) {\n const typeClass = (\n {\n number: 'text-right',\n currency: 'text-right',\n date: 'text-center'\n } as NzSafeAny\n )[item.type!];\n if (typeClass) {\n builtInClassNames.push(typeClass);\n }\n item._className = builtInClassNames;\n return;\n }\n\n const rawClassNameIsArray = Array.isArray(rawClassName);\n if (!rawClassNameIsArray && typeof rawClassName === 'object') {\n const objClassNames: NgClassInterface = rawClassName;\n builtInClassNames.forEach(key => (objClassNames[key] = true));\n item._className = objClassNames;\n return;\n }\n\n const arrayClassNames = rawClassNameIsArray ? Array.from(rawClassName as string[]) : [rawClassName];\n arrayClassNames.splice(0, 0, ...builtInClassNames);\n item._className = [...new Set(arrayClassNames)].filter(w => !!w);\n }\n\n process(\n list: STColumn[],\n options: STColumnSourceProcessOptions\n ): { columns: _STColumn[]; headers: _STHeader[][]; headerWidths: string[] | null } {\n if (!list || list.length === 0) {\n return { columns: [], headers: [], headerWidths: null };\n }\n const { noIndex } = this.cog;\n let checkboxCount = 0;\n let radioCount = 0;\n const columns: _STColumn[] = [];\n\n const processItem = (item: _STColumn): _STColumn => {\n // index\n if (item.index) {\n if (!Array.isArray(item.index)) {\n item.index = item.index.toString().split('.');\n }\n item.indexKey = item.index.join('.');\n }\n\n // #region title\n\n const tit = (typeof item.title === 'string' ? { text: item.title } : item.title) || {};\n if (tit.i18n && this.i18nSrv) {\n tit.text = this.i18nSrv.fanyi(tit.i18n);\n }\n if (tit.text) {\n tit._text = this.dom.bypassSecurityTrustHtml(tit.text);\n }\n item.title = tit;\n\n // #endregion\n\n // no\n if (item.type === 'no') {\n item.noIndex = item.noIndex == null ? noIndex : item.noIndex;\n }\n // checkbox\n if (item.selections == null) {\n item.selections = [];\n }\n if (item.type === 'checkbox') {\n ++checkboxCount;\n if (!item.width) {\n item.width = `${item.selections.length > 0 ? 62 : 50}px`;\n }\n }\n if (this.acl) {\n item.selections = item.selections.filter(w => this.acl!.can(w.acl!));\n }\n // radio\n if (item.type === 'radio') {\n ++radioCount;\n item.selections = [];\n if (!item.width) {\n item.width = '50px';\n }\n }\n // cell\n if (item.cell != null) {\n item.type = 'cell';\n }\n // types\n if (item.type === 'yn') {\n item.yn = { truth: true, ...this.cog.yn, ...item.yn };\n }\n // date\n if (item.type === 'date') {\n item.dateFormat = item.dateFormat || this.cog.date?.format;\n }\n if (\n (item.type === 'link' && typeof item.click !== 'function') ||\n (item.type === 'badge' && item.badge == null) ||\n (item.type === 'tag' && item.tag == null) ||\n (item.type === 'enum' && item.enum == null)\n ) {\n item.type = '';\n }\n item._isTruncate = !!item.width && options.widthMode.strictBehavior === 'truncate' && item.type !== 'img';\n // className\n this.mergeClass(item);\n // width\n if (typeof item.width === 'number') {\n item._width = item.width;\n item.width = `${item.width}px`;\n }\n item._left = false;\n item._right = false;\n item.safeType = item.safeType ?? options.safeType;\n\n // sorter\n item._sort = this.sortCoerce(item);\n // filter\n item.filter = this.filterCoerce(item) as STColumnFilter;\n // buttons\n item.buttons = this.btnCoerce(item.buttons!);\n // widget\n this.widgetCoerce(item);\n // restore custom row\n this.restoreRender(item);\n // resizable\n item.resizable = {\n disabled: true,\n bounds: 'window',\n minWidth: 60,\n maxWidth: 360,\n preview: true,\n ...options.resizable,\n ...(typeof item.resizable === 'boolean' ? ({ disabled: !item.resizable } as STResizable) : item.resizable)\n };\n\n return item;\n };\n\n const processList = (data: _STColumn[]): void => {\n for (const item of data) {\n columns.push(processItem(item));\n if (Array.isArray(item.children)) {\n processList(item.children);\n }\n }\n };\n\n const copyList = this.cleanCond(list as _STColumn[]);\n processList(copyList);\n\n if (checkboxCount > 1) {\n throw new Error(`[st]: just only one column checkbox`);\n }\n if (radioCount > 1) {\n throw new Error(`[st]: just only one column radio`);\n }\n\n this.fixedCoerce(columns as _STColumn[], options.expand);\n return {\n columns: columns.filter(w => !Array.isArray(w.children) || w.children.length === 0),\n ...this.genHeaders(copyList)\n };\n }\n\n restoreAllRender(columns: _STColumn[]): void {\n columns.forEach(i => this.restoreRender(i));\n }\n\n updateDefault(filter: STColumnFilter): this {\n if (filter.menus == null) return this;\n\n if (filter.type === 'default') {\n filter.default = filter.menus!.findIndex(w => w.checked!) !== -1;\n } else {\n filter.default = !!filter.menus![0].value;\n }\n return this;\n }\n\n cleanFilter(col: _STColumn): this {\n const f = col.filter!;\n f.default = false;\n if (f.type === 'default') {\n f.menus!.forEach(i => (i.checked = false));\n } else {\n f.menus![0].value = undefined;\n }\n return this;\n }\n}\n","/* eslint-disable @angular-eslint/prefer-inject */\nimport { DecimalPipe } from '@angular/common';\nimport { HttpParams } from '@angular/common/http';\nimport { Host, Injectable } from '@angular/core';\nimport { DomSanitizer } from '@angular/platform-browser';\nimport { Observable, of, map } from 'rxjs';\n\nimport type { CellOptions } from '@yelon/abc/cell';\nimport { DatePipe, YNPipe, _HttpClient } from '@yelon/theme';\nimport type { YunzaiSTConfig } from '@yelon/util/config';\nimport { CurrencyService } from '@yelon/util/format';\nimport { deepCopy, deepGet } from '@yelon/util/other';\n\nimport type {\n STColumn,\n STColumnFilter,\n STColumnFilterMenu,\n STColumnMaxMultipleButton,\n STCustomRequestOptions,\n STData,\n STIcon,\n STMultiSort,\n STMultiSortResultType,\n STOnCellResult,\n STPage,\n STReq,\n STReqReNameType,\n STRequestOptions,\n STRes,\n STRowClassName,\n STSingleSort,\n STSortMap,\n STStatistical,\n STStatisticalResult,\n STStatisticalResults,\n STStatisticalType\n} from './st.interfaces';\nimport type { _STColumn, _STColumnButton, _STDataValue, _STHeader } from './st.types';\n\nexport interface STDataSourceOptions {\n pi: number;\n ps: number;\n paginator: boolean;\n data?: string | STData[] | Observable<STData[]>;\n total: number;\n req: STReq;\n res: STRes;\n page: STPage;\n columns: _STColumn[];\n headers: _STHeader[][];\n singleSort?: STSingleSort | null;\n multiSort?: STMultiSort;\n rowClassName?: STRowClassName | null;\n customRequest?: (options: STCustomRequestOptions) => Observable<any>;\n}\n\nexport interface STDataSourceResult {\n /** 是否需要显示分页器 */\n pageShow: boolean;\n /** 新 `pi`,若返回 `undefined` 表示用户受控 */\n pi: number;\n /** 新 `ps`,若返回 `undefined` 表示用户受控 */\n ps: number;\n /** 新 `total`,若返回 `undefined` 表示用户受控 */\n total: number;\n /** 数据 */\n list: STData[];\n /** 统计数据 */\n statistical: STStatisticalResults;\n}\n\n@Injectable()\nexport class STDataSource {\n private cog!: YunzaiSTConfig;\n private sortTick = 0;\n\n constructor(\n private http: _HttpClient,\n @Host() private datePipe: DatePipe,\n @Host() private ynPipe: YNPipe,\n @Host() private numberPipe: DecimalPipe,\n private currencySrv: CurrencyService,\n private dom: DomSanitizer\n ) {}\n\n setCog(val: YunzaiSTConfig): void {\n this.cog = val;\n }\n\n process(options: STDataSourceOptions): Observable<STDataSourceResult> {\n let data$: Observable<STData[]>;\n let isRemote = false;\n const { data, res, total, page, pi, ps, paginator, columns, headers } = options;\n let retTotal: number;\n let retPs: number;\n let retList: STData[];\n let retPi: number;\n let rawData: any;\n let showPage = page.show;\n\n if (typeof data === 'string') {\n isRemote = true;\n data$ = this.getByRemote(data, options).pipe(\n map(result => {\n rawData = result;\n let ret: STData[];\n if (Array.isArray(result)) {\n ret = result;\n retTotal = ret.length;\n retPs = retTotal;\n showPage = false;\n } else {\n const reName = res.reName!;\n if (typeof reName === 'function') {\n const fnRes = reName(result, { pi, ps, total });\n ret = fnRes.list;\n retTotal = fnRes.total;\n } else {\n // list\n ret = deepGet(result, reName.list as string[], []);\n if (ret == null || !Array.isArray(ret)) {\n ret = [];\n }\n // total\n const resultTotal = reName.total && deepGet(result, reName.total as string[], null);\n retTotal = resultTotal == null ? total || 0 : +resultTotal;\n }\n }\n return deepCopy(ret);\n })\n );\n } else if (data == null || Array.isArray(data)) {\n data$ = of(data ?? []);\n } else {\n // a cold observable\n data$ = data;\n }\n\n if (!isRemote) {\n data$ = data$.pipe(\n // sort\n map((result: STData[]) => {\n rawData = result;\n let copyResult = deepCopy(result);\n const sorterFn = this.getSorterFn(headers);\n if (sorterFn) {\n copyResult = copyResult.sort(sorterFn);\n }\n return copyResult;\n }),\n // filter\n map((result: STData[]) => {\n columns\n .filter(w => w.filter)\n .forEach(c => {\n const filter = c.filter!;\n const values = this.getFilteredData(filter);\n if (values.length === 0) return;\n const onFilter = filter.fn;\n if (typeof onFilter !== 'function') {\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\n console.warn(`[st] Muse provide the fn function in filter`);\n }\n return;\n }\n result = result.filter(record => values.some(v => onFilter(v, record)));\n });\n return result;\n }),\n // paging\n map((result: STData[]) => {\n if (paginator && page.front) {\n const maxPageIndex = Math.ceil(result.length / ps);\n retPi = Math.max(1, pi > maxPageIndex ? maxPageIndex : pi);\n retTotal = result.length;\n if (page.show === true) {\n return result.slice((retPi - 1) * ps, retPi * ps);\n }\n }\n return result;\n })\n );\n }\n\n // pre-process\n if (typeof res.process === 'function') {\n data$ = data$.pipe(map(result => res.process!(result, rawData)));\n }\n\n data$ = data$.pipe(map(result => this.optimizeData({ result, columns, rowClassName: options.rowClassName })));\n\n return data$.pipe(\n map(result => {\n retList = result;\n const realTotal = retTotal || total;\n const realPs = retPs || ps;\n\n return {\n pi: retPi,\n ps: retPs,\n total: retTotal,\n list: retList,\n statistical: this.genStatistical(columns as _STColumn[], retList, rawData),\n pageShow: typeof showPage === 'undefined' ? realTotal > realPs : showPage\n } as STDataSourceResult;\n })\n );\n }\n\n private get(item: STData, col: _STColumn, idx: number): _STDataValue {\n try {\n const safeHtml = col.safeType === 'safeHtml';\n if (col.format) {\n const formatRes = col.format(item, col, idx) || '';\n return {\n text: formatRes,\n _text: safeHtml ? this.dom.bypassSecurityTrustHtml(formatRes) : formatRes,\n org: formatRes,\n safeType: col.safeType!\n };\n }\n\n const value = deepGet(item, col.index as string[], col.default);\n\n let text = value;\n let color: string | undefined;\n let tooltip: string | undefined;\n switch (col.type) {\n case 'no':\n text = this.getNoIndex(item, col, idx);\n break;\n case 'img':\n text = value ? `<img src=\"${value}\" class=\"img\">` : '';\n break;\n case 'number':\n text = this.numberPipe.transform(value, col.numberDigits);\n break;\n case 'currency':\n text = this.currencySrv.format(value, col.currency?.format);\n break;\n case 'date':\n text =\n value == null || value === col.default || (typeof value === 'number' && value <= 0)\n ? col.default\n : this.datePipe.transform(value, col.dateFormat);\n break;\n case 'yn':\n text = this.ynPipe.transform(value === col.yn!.truth, col.yn!.yes!, col.yn!.no!, col.yn!.mode!, false);\n break;\n case 'enum':\n text = col.enum![value];\n break;\n case 'tag':\n case 'badge': {\n const data = col.type === 'tag' ? col.tag : col.badge;\n if (data && data[text]) {\n const dataItem = data[text];\n text = dataItem.text;\n color = dataItem.color;\n tooltip = dataItem.tooltip;\n } else {\n text = '';\n }\n break;\n }\n }\n if (text == null) text = '';\n return {\n text,\n _text: safeHtml ? this.dom.bypassSecurityTrustHtml(text) : text,\n org: value,\n color,\n tooltip,\n safeType: col.safeType!,\n buttons: []\n };\n } catch (ex) {\n const text = `INVALID DATA`;\n console.error(`Failed to get data`, item, col, ex);\n return { text, _text: text, org: text, buttons: [], safeType: 'text' };\n }\n }\n\n private getByRemote(url: string, options: STDataSourceOptions): Observable<unknown> {\n const { req, page, paginator, pi, ps, singleSort, multiSort, columns, headers } = options;\n const method = (req.method || 'GET').toUpperCase();\n\n let params: Record<string, any> = {};\n const reName = req.reName as STReqReNameType;\n if (paginator) {\n if (req.type === 'page') {\n params = {\n [reName.pi as string]: page.zeroIndexed ? pi - 1 : pi,\n [reName.ps as string]: ps\n };\n } else {\n params = {\n [reName.skip as string]: (pi - 1) * ps,\n [reName.limit as string]: ps\n };\n }\n }\n params = {\n ...params,\n ...req.params,\n ...this.getReqSortMap(singleSort, multiSort, headers),\n ...this.getReqFilterMap(columns)\n };\n if (options.req.ignoreParamNull == true) {\n Object.keys(params).forEach(key => {\n if (params[key] == null) delete params[key];\n });\n }\n\n let reqOptions: STRequestOptions = {\n params,\n body: req.body,\n headers: req.headers\n };\n if (method === 'POST' && req.allInBody === true) {\n reqOptions = {\n body: { ...req.body, ...params },\n headers: req.headers\n };\n }\n if (typeof req.process === 'function') {\n reqOptions = req.process(reqOptions);\n }\n if (!(reqOptions.params instanceof HttpParams)) {\n reqOptions.params = new HttpParams({ fromObject: reqOptions.params });\n }\n if (typeof options.customRequest === 'function') {\n return options.customRequest({ method, url, options: reqOptions });\n }\n return this.http.request(method, url, reqOptions);\n }\n\n getCell(c: STColumn, item: STData, idx: number): STOnCellResult {\n const onCellResult = typeof c.onCell === 'function' ? c.onCell(item, idx) : null;\n const mergedColSpan = onCellResult?.colSpan ?? 1;\n const mergedRowSpan = onCellResult?.rowSpan ?? 1;\n return {\n colSpan: mergedColSpan <= 0 ? null : mergedColSpan,\n rowSpan: mergedRowSpan <= 0 ? null : mergedRowSpan\n } as STOnCellResult;\n }\n\n optimizeData(options: { columns: _STColumn[]; result: STData[]; rowClassName?: STRowClassName | null }): STData[] {\n const { result, columns, rowClassName } = options;\n for (let i = 0, len = result.length; i < len; i++) {\n result[i]._values = columns.map(c => {\n const props = this.getCell(c, result[i], i);\n\n if (Array.isArray(c.buttons) && c.buttons.length > 0) {\n return { buttons: this.genButtons(c.buttons, result[i], c), _text: '', props };\n }\n\n let cell: CellOptions | undefined;\n if (typeof c.cell === 'function') {\n cell = c.cell(result[i], c);\n }\n return { ...this.get(result[i], c, i), props, cell };\n });\n result[i]._rowClassName = [rowClassName ? rowClassName(result[i], i) : null, result[i].className]\n .filter(w => !!w)\n .join(' ');\n }\n return result;\n }\n\n getNoIndex(item: STData, col: _STColumn, idx: number): number {\n return typeof col.noIndex === 'function' ? col.noIndex(item, col, idx) : col.noIndex! + idx;\n }\n\n private genButtons(_btns: _STColumnButton[], item: STData, col: STColumn): _STColumnButton[] {\n const fn = (btns: _STColumnButton[]): _STColumnButton[] => {\n return deepCopy(btns).filter(btn => {\n const result = typeof btn.iif === 'function' ? btn.iif(item, btn, col) : true;\n const isRenderDisabled = btn.iifBehavior === 'disabled';\n btn._result = result;\n btn._disabled = !result && isRenderDisabled;\n if (btn.children?.length) {\n btn.children = fn(btn.children!);\n }\n return result || isRenderDisabled;\n });\n };\n\n const res = fn(_btns);\n\n const fnText = (btns: _STColumnButton[]): _STColumnButton[] => {\n for (const btn of btns) {\n btn._text = typeof btn.text === 'function' ? btn.text(item, btn) : btn.text || '';\n btn._className = typeof btn.className === 'function' ? btn.className(item, btn) : btn.className;\n btn._icon = typeof btn.icon === 'function' ? btn.icon(item, btn) : (btn.icon as STIcon);\n if (btn.children?.length) {\n btn.children = fnText(btn.children!);\n }\n }\n return btns;\n };\n\n return this.fixMaxMultiple(fnText(res), col);\n }\n\n private fixMaxMultiple(btns: _STColumnButton[], col: STColumn): _STColumnButton[] {\n const curCog = col.maxMultipleButton;\n const btnSize = btns.length;\n if (curCog == null || btnSize <= 0) return btns;\n\n const cog: STColumnMaxMultipleButton = {\n ...this.cog.maxMultipleButton,\n ...(typeof curCog === 'number' ? { count: curCog } : curCog)\n };\n\n if (cog.count! >= btnSize) return btns;\n\n const newBtns: _STColumnButton[] = btns.slice(0, cog.count);\n newBtns.push({ _text: cog.text, children: btns.slice(cog.count) });\n return newBtns;\n }\n\n // #region sort\n\n private getValidSort(headers: _STHeader[][]): STSortMap[] {\n return headers.reduce((a, header) => {\n const ls = header\n .map(i => i.column)\n .filter(item => item._sort && item._sort.enabled && item._sort.default)\n .map(item => item._sort!);\n return a.concat(...ls);\n }, [] as STSortMap[]);\n }\n\n private getSorterFn(headers: _STHeader[][]): ((a: STData, b: STData) => number) | void {\n const sortList = this.getValidSort(headers);\n if (sortList.length === 0) {\n return;\n }\n const sortItem = sortList[0];\n if (sortItem.compare === null) {\n return;\n }\n if (typeof sortItem.compare !== 'function') {\n if (typeof ngDevMode === 'undefined' || ngDevMode) {\n console.warn(`[st] Muse provide the compare function in sort`);\n }\n return;\n }\n\n return (a: STData, b: STData) => {\n const result = sortItem.compare!(a, b);\n if (result !== 0) {\n return sortItem.default === 'descend' ? -result : result;\n }\n return 0;\n };\n }\n\n get nextSortTick(): number {\n return ++this.sortTick;\n }\n\n getReqSortMap(\n singleSort: STSingleSort | undefined | null,\n multiSort: STMultiSort | undefined,\n headers: _STHeader[][]\n ): STMultiSortResultType {\n let ret: STMultiSortResultType = {};\n const sortList = this.getValidSort(headers);\n\n if (multiSort) {\n const ms: STMultiSort = {\n key: 'sort',\n separator: '-',\n nameSeparator: '.',\n keepEmptyKey: true,\n arrayParam: false,\n ...multiSort\n };\n\n const sortMap = sortList\n .sort((a, b) => a.tick - b.tick)\n .map(item => item.key! + ms.nameSeparator + ((item.reName || {})[item.default!] || item.default));\n\n ret = { [ms.key!]: ms.arrayParam ? sortMap : sortMap.join(ms.separator) };\n\n return sortMap.length === 0 && ms.keepEmptyKey === false ? {} : ret;\n }\n\n if (sortList.length === 0) return ret;\n\n const mapData = sortList[0];\n let sortFiled = mapData.key;\n let sortValue = (sortList[0].reName || {})[mapData.default!] || mapData.default;\n if (singleSort) {\n sortValue = sortFiled + (singleSort.nameSeparator || '.') + sortValue;\n sortFiled = singleSort.key || 'sort';\n }\n ret[sortFiled as string] = sortValue as string;\n return ret;\n }\n\n // #endregion\n\n // #region filter\n\n private getFilteredData(filter: STColumnFilter): STColumnFilterMenu[] {\n return filter.type === 'default' ? filter.menus!.filter(f => f.checked === true) : filter.menus!.slice(0, 1);\n }\n\n private getReqFilterMap(columns: _STColumn[]): Record<string, string> {\n let ret = {};\n columns\n .filter(w => w.filter && w.filter.default === true)\n .forEach(col => {\n const filter = col.filter!;\n const values = this.getFilteredData(filter);\n let obj: Record<string, any> = {};\n if (filter.reName) {\n obj = filter.reName!(filter.menus!, col);\n } else {\n obj[filter.key!] = values.map(i => i.value).join(',');\n }\n ret = { ...ret, ...obj };\n });\n return ret;\n }\n\n // #endregion\n\n // #region statistical\n\n private genStatistical(columns: _STColumn[], list: STData[], rawData: any): STStatisticalResults {\n const res: Record<string, any> = {};\n columns.forEach((col, index) => {\n res[col.key || col.indexKey || index] =\n col.statistical == null ? {} : this.getStatistical(col, index, list, rawData);\n });\n return res;\n }\n\n private getStatistical(col: _STColumn, index: number, list: STData[], rawData: any): STStatisticalResult {\n const val = col.statistical;\n const item: STStatistical = {\n digits: 2,\n currency: undefined,\n ...(typeof val === 'string' ? { type: val as STStatisticalType } : (val as STStatistical))\n };\n let res: STStatisticalResult = { value: 0 };\n let currency = false;\n if (typeof item.type === 'function') {\n res = item.type(this.getValues(index, list), col, list, rawData);\n currency = true;\n } else {\n switch (item.type) {\n case 'count':\n res.value = list.length;\n break;\n case 'distinctCount':\n res.value = this.getValues(index, list).filter((value, idx, self) => self.indexOf(value) === idx).length;\n break;\n case 'sum':\n res.value = this.toFixed(this.getSum(index, list), item.digits!);\n currency = true;\n break;\n case 'average':\n res.value = this.toFixed(this.getSum(index, list) / list.length, item.digits!);\n currency = true;\n break;\n case 'max':\n res.value = Math.max(...this.getValues(index, list));\n currency = true;\n break;\n case 'min':\n res.value = Math.min(...this.getValues(index, list));\n currency = true;\n break;\n }\n }\n if (item.currency === true || (item.currency == null && currency === true)) {\n res.text = this.currencySrv.format(res.value, col.currency?.format) as string;\n } else {\n res.text = String(res.value);\n }\n return res;\n }\n\n private toFixed(val: number, digits: number): number {\n if (isNaN(val) || !isFinite(val)) {\n return 0;\n }\n return parseFloat(val.toFixed(digits));\n }\n\n private getValues(index: number, list: STData[]): number[] {\n return list.map(i => i._values[index].org).map(i => (i === '' || i == null ? 0 : i));\n }\n\n private getSum(index: number, list: STData[]): number {\n return this.getValues(index, list).reduce((p, i) => (p += parseFloat(String(i))), 0);\n }\n\n // #endregion\n}\n","import { Injectable, inject } from '@angular/core';\n\nimport { XlsxExportResult, XlsxService } from '@yelon/abc/xlsx';\nimport { deepGet } from '@yelon/util/other';\n\nimport { STColumn, STExportOptions } from './st.interfaces';\nimport { _STColumn } from './st.types';\n\n@Injectable()\nexport class STExport {\n private readonly xlsxSrv = inject(XlsxService);\n\n private _stGet(item: any, col: STColumn, index: number, colIndex: number): any {\n const ret: Record<string, any> = { t: 's', v: '' };\n\n if (col.format) {\n ret.v = col.format(item, col, index);\n } else {\n const val = item._values ? item._values[colIndex].text : deepGet(item, col.index as string[], '');\n ret.v = val;\n if (val != null) {\n switch (col.type) {\n case 'currency':\n ret.t = 'n';\n break;\n case 'date':\n // Can't be a empty value, it will cause `#NULL!`\n // See https://github.com/SheetJS/sheetjs/blob/master/docbits/52_datatype.md\n if (`${val}`.length > 0) {\n ret.t = 'd';\n // Number Formats: https://github.com/SheetJS/sheetjs/blob/master/docbits/63_numfmt.md\n ret.z = col.dateFormat;\n }\n break;\n case 'yn': {\n const yn = col.yn!;\n ret.v = val === yn.truth ? yn.yes : yn.no;\n break;\n }\n }\n }\n }\n\n ret.v = ret.v ?? '';\n\n return ret;\n }\n\n private genSheet(opt: STExportOptions): Record<string, unknown> {\n const sheets: Record<string, Record<string, any>> = {};\n const sheet: Record<string, any> = (sheets[opt.sheetname || 'Sheet1'] = {});\n const dataLen = opt.data!.length;\n const columns = opt.columens! as _STColumn[];\n let validColCount = 0;\n let wpx = false;\n const invalidFn = (col: _STColumn): boolean =>\n col.exported === false || !col.index || !(!col.buttons || col.buttons.length === 0);\n for (const [idx, col] of columns.entries()) {\n if (invalidFn(col)) continue;\n if (!wpx && col._width != null) wpx = true;\n ++validColCount;\n const columnName = this.xlsxSrv!.numberToSchema(validColCount);\n sheet[`${columnName}1`] = {\n t: 's',\n v: typeof col.title === 'object' ? col.title.text : col.title\n };\n for (let dataIdx = 0; dataIdx < dataLen; dataIdx++) {\n sheet[`${columnName}${dataIdx + 2}`] = this._stGet(opt.data![dataIdx], col, dataIdx, idx);\n }\n }\n if (wpx) {\n // wpx: width in screen pixels https://github.com/SheetJS/sheetjs#column-properties\n sheet['!cols'] = columns.filter(col => !invalidFn(col)).map(col => ({ wpx: col._width }));\n }\n\n if (validColCount > 0 && dataLen > 0) {\n sheet['!ref'] = `A1:${this.xlsxSrv!.numberToSchema(validColCount)}${dataLen + 1}`;\n }\n\n return sheets;\n }\n\n async export(opt: STExportOptions): Promise<XlsxExportResult> {\n const sheets = this.genSheet(opt);\n return this.xlsxSrv.export({\n sheets,\n filename: opt.filename,\n callback: opt.callback\n });\n }\n}\n","import { Directive, Input, OnInit, ViewContainerRef, inject } from '@angular/core';\n\nimport { STWidgetRegistry } from './st-widget';\nimport { STColumn, STData } from './st.interfaces';\n\n@Directive({\n selector: '[st-widget-host]'\n})\nexport class STWidgetHostDirective implements OnInit {\n private readonly stWidgetRegistry = inject(STWidgetRegistry);\n private readonly viewContainerRef = inject(ViewContainerRef);\n\n @Input() record!: STData;\n @Input() column!: STColumn;\n\n ngOnInit(): void {\n const widget = this.column.widget!;\n const componentType = this.stWidgetRegistry.get(widget.type);\n\n this.viewContainerRef.clear();\n const componentRef = this.viewContainerRef.createComponent(componentType);\n const { record, column } = this;\n const data: Record<string, any> = widget.params ? widget.params({ record, column }) : { record };\n Object.keys(data).forEach(key => {\n (componentRef.instance as any)[key] = data[key];\n });\n }\n}\n","import { NgTemplateOutlet } from '@angular/common';\nimport {\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n EventEmitter,\n Input,\n Output,\n ViewEncapsulation,\n inject\n} from '@angular/core';\nimport { FormsModule } from '@angular/forms';\n\nimport type { LocaleData } from '@yelon/theme';\nimport { NzCheckboxComponent } from 'ng-zorro-antd/checkbox';\nimport { NzDatePickerComponent, NzRangePickerComponent } from 'ng-zorro-antd/date-picker';\nimport { NzDropDownModule } from 'ng-zorro-antd/dropdown';\nimport { NzIconDirective } from 'ng-zorro-antd/icon';\nimport { NzInputDirective } from 'ng-zorro-antd/input';\nimport { NzInputNumberComponent } from 'ng-zorro-antd/input-number';\nimport { NzMenuModule } from 'ng-zorro-antd/menu';\nimport { NzRadioComponent } from 'ng-zorro-antd/radio';\n\nimport type { STColumnFilter, STColumnFilterMenu, STIcon } from './st.interfaces';\nimport type { _STColumn } from './st.types';\n\n@Component({\n selector: 'st-filter',\n template: `\n <span\n class=\"ant-table-filter-trigger\"\n [class.active]=\"visible || f.default\"\n nz-dropdown\n [nzDropdownMenu]=\"filterMenu\"\n nzTrigger=\"click\"\n [nzClickHide]=\"false\"\n [(nzVisible)]=\"visible\"\n nzOverlayClassName=\"st__filter-wrap\"\n (click)=\"stopPropagation($event)\"\n >\n <nz-icon [nzType]=\"icon.type\" [nzTheme]=\"icon.theme!\" />\n </span>\n <nz-dropdown-menu #filterMenu=\"nzDropdownMenu\">\n <div class=\"ant-table-filter-dropdown\">\n @switch (f.type) {\n @case ('keyword') {\n <div class=\"st__filter-keyword\">\n <input\n type=\"text\"\n nz-input\n [attr.placeholder]=\"f.placeholder\"\n [(ngModel)]=\"f.menus![0]!.value\"\n (ngModelChange)=\"n.emit($event)\"\n (keyup.enter)=\"confirm()\"\n />\n </div>\n }\n @case ('number') {\n <div class=\"p-sm st__filter-number\">\n <nz-input-number\n [(ngModel)]=\"f.menus![0]!.value\"\n (ngModelChange)=\"n.emit($event)\"\n [nzMin]=\"f.number!.min!\"\n [nzMax]=\"f.number!.max!\"\n [nzStep]=\"f.number!.step!\"\n [nzPrecision]=\"f.number!.precision || null\"\n [nzPlaceHolder]=\"f.placeholder!\"\n class=\"width-100\"\n />\n </div>\n }\n @case ('date') {\n <div class=\"p-sm st__filter-date\">\n @if (f.date!.range) {\n <nz-range-picker\n nzInline\n [nzMode]=\"$any(f.date!.mode)\"\n [(ngModel)]=\"f.menus![0]!.value\"\n (ngModelChange)=\"n.emit($event)\"\n [nzShowNow]=\"f.date!.showNow\"\n [nzShowToday]=\"f.date!.showToday\"\n [nzDisabledDate]=\"f.date!.disabledDate\"\n [nzDisabledTime]=\"f.date!.disabledTime\"\n />\n } @else {\n <nz-date-picker\n nzInline\n [nzMode]=\"$any(f.date!.mode)\"\n [(ngModel)]=\"f.menus![0]!.value\"\n (ngModelChange)=\"n.emit($event)\"\n [nzShowNow]=\"f.date!.showNow\"\n [nzShowToday]=\"f.date!.showToday\"\n [nzDisabledDate]=\"f.date!.disabledDate\"\n [nzDisabledTime]=\"f.date!.disabledTime\"\n />\n }\n </div>\n }\n @case ('custom') {\n <div class=\"st__filter-custom\">\n <ng-template\n [ngTemplateOutlet]=\"f.custom!\"\n [ngTemplateOutletContext]=\"{ $implicit: f, col: col, handle: this }\"\n />\n </div>\n }\n @default {\n <ul nz-menu>\n @for (filter of f.menus; track $index) {\n <li nz-menu-item>\n @if (f.multiple) {\n <label nz-checkbox [(ngModel)]=\"filter.checked\" (ngModelChange)=\"checkboxChange()\">\n {{ filter.text }}\n </label>\n } @else {\n <label nz-radio [ngModel]=\"filter.checked\" (ngModelChange)=\"radioChange(filter)\">\n {{ filter.text }}\n </label>\n }\n </li>\n }\n </ul>\n }\n }\n @if (f.showOPArea) {\n <div class=\"ant-table-filter-dropdown-btns\">\n <a class=\"ant-table-filter-dropdown-link confirm\" (click)=\"confirm()\">\n <span>{{ f.confirmText || locale.filterConfirm }}</span>\n </a>\n <a class=\"ant-table-filter-dropdown-link clear\" (click)=\"reset()\">\n <span>{{ f.clearText || locale.filterReset }}</span>\n </a>\n </div>\n }\n </div>\n </nz-dropdown-menu>\n `,\n host: {\n '[class.ant-table-filter-trigger-container]': `true`,\n '[class.st__filter]': `true`,\n '[class.ant-table-filter-trigger-container-open]': `visible`\n },\n\n changeDetection: ChangeDetectionStrategy.OnPush,\n encapsulation: ViewEncapsulation.None,\n imports: [\n FormsModule,\n NzDropDownModule,\n NzIconDirective,\n NzInputDirective,\n NzInputNumberComponent,\n NzRangePickerComponent,\n NzDatePickerComponent,\n NgTemplateOutlet,\n NzMenuModule,\n NzCheckboxComponent,\n NzRadioComponent\n ]\n})\nexport class STFilterComponent {\n private readonly cdr = inject(ChangeDetectorRef);\n\n visible = false;\n @Input() col!: _STColumn;\n @Input() locale: LocaleData = {};\n @Input() f!: STColumnFilter;\n @Output() readonly n = new EventEmitter<unknown>();\n @Output() readonly handle = new EventEmitter<boolean>();\n get icon(): STIcon {\n return this.f.icon as STIcon;\n }\n\n stopPropagation($event: MouseEvent): void {\n $event.stopPropagation();\n }\n\n checkboxChange(): void {\n this.n.emit(this.f.menus?.filter(w => w.checked));\n }\n\n radioChange(item: STColumnFilterMenu): void {\n this.f.menus!.forEach(i => (i.checked = false));\n item.checked = !item.checked;\n this.n.emit(item);\n }\n\n close(result?: boolean): void {\n if (result != null) this.handle.emit(result);\n\n this.visible = false;\n this.cdr.detectChanges();\n }\n\n confirm(): this {\n this.handle.emit(true);\n return this;\n }\n\n reset(): this {\n this.handle.emit(false);\n return this;\n }\n}\n","import { YunzaiSTConfig }