@delon/theme
Version:
ng-alain theme system library.
1,465 lines (1,452 loc) • 118 kB
JavaScript
import { DOCUMENT, isPlatformServer, CommonModule, registerLocaleData } from '@angular/common';
import * as i0 from '@angular/core';
import { inject, PLATFORM_ID, InjectionToken, Injectable, DestroyRef, Injector, Pipe, Optional, SkipSelf, NgModule, importProvidersFrom, LOCALE_ID, provideEnvironmentInitializer, makeEnvironmentProviders, Version } from '@angular/core';
import { BehaviorSubject, filter, share, Subject, map, of, delay, isObservable, switchMap, Observable, take, tap, finalize, throwError, catchError } from 'rxjs';
import { ACLService } from '@delon/acl';
import { AlainConfigService, ALAIN_CONFIG } from '@delon/util/config';
import { Platform } from '@angular/cdk/platform';
import { Directionality } from '@angular/cdk/bidi';
import { NzConfigService } from 'ng-zorro-antd/core/config';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { Title, DomSanitizer } from '@angular/platform-browser';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { DragDrop } from '@angular/cdk/drag-drop';
import { SIGNAL } from '@angular/core/primitives/signals';
import { deepMerge } from '@delon/util/other';
import { NzModalService, NzModalModule } from 'ng-zorro-antd/modal';
import { NzDrawerService, NzDrawerModule } from 'ng-zorro-antd/drawer';
import { HttpClient, HttpParams, HttpContextToken } from '@angular/common/http';
import { formatDate } from '@delon/util/date-time';
import { NzI18nService, NzI18nModule, provideNzI18n, NZ_DATE_LOCALE } from 'ng-zorro-antd/i18n';
import { OverlayModule } from '@angular/cdk/overlay';
import { BellOutline, DeleteOutline, PlusOutline, InboxOutline, MenuFoldOutline, MenuUnfoldOutline } from '@ant-design/icons-angular/icons';
import * as i1 from 'ng-zorro-antd/icon';
import { NzIconService } from 'ng-zorro-antd/icon';
function stepPreloader() {
const doc = inject(DOCUMENT);
const ssr = isPlatformServer(inject(PLATFORM_ID));
if (ssr) {
return () => { };
}
const body = doc.querySelector('body');
body.style.overflow = 'hidden';
let done = false;
return () => {
if (done)
return;
done = true;
const preloader = doc.querySelector('.preloader');
if (preloader == null)
return;
const CLS = 'preloader-hidden';
preloader.addEventListener('transitionend', () => {
preloader.className = CLS;
});
preloader.className += ` ${CLS}-add ${CLS}-add-active`;
body.style.overflow = '';
};
}
const ALAIN_I18N_TOKEN = new InjectionToken('alainI18nToken', {
providedIn: 'root',
factory: () => new AlainI18NServiceFake()
});
class AlainI18nBaseService {
cogSrv = inject(AlainConfigService);
cog;
_change$ = new BehaviorSubject(null);
_currentLang = '';
_defaultLang = '';
_data = {};
get change() {
return this._change$.asObservable().pipe(filter(w => w != null));
}
get defaultLang() {
return this._defaultLang;
}
get currentLang() {
return this._currentLang;
}
get data() {
return this._data;
}
constructor() {
this.cog = this.cogSrv.merge('themeI18n', {
interpolation: ['{{', '}}']
});
}
/**
* Flattened data source
*
* @example
* {
* "name": "Name",
* "sys": {
* "": "System",
* "title": "Title"
* }
* }
* =>
* {
* "name": "Name",
* "sys": "System",
* "sys.title": "Title"
* }
*/
flatData(data, parentKey) {
const res = {};
for (const key of Object.keys(data)) {
const value = data[key];
if (typeof value === 'object') {
const child = this.flatData(value, parentKey.concat(key));
Object.keys(child).forEach(childKey => (res[childKey] = child[childKey]));
}
else {
res[(key ? parentKey.concat(key) : parentKey).join('.')] = `${value}`;
}
}
return res;
}
fanyi(path, params) {
let content = this._data[path] || '';
if (!content)
return path;
if (!params)
return content;
if (typeof params === 'object') {
const interpolation = this.cog.interpolation;
const objParams = params;
Object.keys(objParams).forEach(key => {
content = content.replace(new RegExp(`${interpolation[0]}\\s?${key}\\s?${interpolation[1]}`, 'g'), `${objParams[key]}`);
});
}
(Array.isArray(params) ? params : [params]).forEach((item, index) => (content = content.replace(new RegExp(`\\{\\s?${index}\\s?\\}`, 'g'), `${item}`)));
return content;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AlainI18nBaseService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AlainI18nBaseService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AlainI18nBaseService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
class AlainI18NServiceFake extends AlainI18nBaseService {
use(lang, data) {
this._data = this.flatData(data ?? {}, []);
this._currentLang = lang;
this._change$.next(lang);
}
getLangs() {
return [];
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AlainI18NServiceFake, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AlainI18NServiceFake, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AlainI18NServiceFake, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
/**
* 菜单服务,[在线文档](https://ng-alain.com/theme/menu)
*/
class MenuService {
i18nSrv = inject(ALAIN_I18N_TOKEN);
aclService = inject(ACLService);
_change$ = new BehaviorSubject([]);
i18n$;
data = [];
/**
* 是否完全受控菜单打开状态,默认:`false`
*/
openStrictly = false;
constructor() {
this.i18n$ = this.i18nSrv.change.subscribe(() => this.resume());
}
get change() {
return this._change$.pipe(share());
}
get menus() {
return this.data;
}
/**
* Returns a default menu link
*
* 返回一个默认跳转的菜单链接
*/
getDefaultRedirect(opt = {}) {
let ret;
this.visit(this.menus, (item) => {
if (typeof item.link !== 'string' || item.link.length <= 0 || !item._aclResult || item._hidden === true) {
return;
}
if (ret == null || ret.length <= 0 || item.link == opt?.redirectUrl) {
ret = item.link;
}
});
return ret;
}
visit(data, callback) {
const inFn = (list, parentMenu, depth) => {
for (const item of list) {
callback(item, parentMenu, depth);
if (item.children && item.children.length > 0) {
inFn(item.children, item, depth + 1);
}
else {
item.children = [];
}
}
};
inFn(data, null, 0);
}
add(items) {
this.data = items;
this.resume();
}
fixItem(item) {
item._aclResult = true;
if (!item.render_type)
item.render_type = 'item';
if (!item.link)
item.link = '';
if (!item.externalLink)
item.externalLink = '';
// badge
if (item.badge) {
if (item.badgeDot !== true) {
item.badgeDot = false;
}
if (!item.badgeStatus) {
item.badgeStatus = 'error';
}
}
if (!Array.isArray(item.children)) {
item.children = [];
}
// icon
if (typeof item.icon === 'string') {
let type = 'class';
let value = item.icon;
// compatible `anticon anticon-user`
if (~item.icon.indexOf(`anticon-`)) {
type = 'icon';
value = value.split('-').slice(1).join('-');
}
else if (/^https?:\/\//.test(item.icon)) {
type = 'img';
}
item.icon = { type, value };
}
if (item.icon != null) {
item.icon = { theme: 'outline', spin: false, ...item.icon };
}
item.text = item.i18n ? this.i18nSrv.fanyi(item.i18n) : item.text;
// group
item.group = item.group !== false;
// hidden
item._hidden = typeof item.hide === 'undefined' ? false : item.hide;
// disabled
item.disabled = typeof item.disabled === 'undefined' ? false : item.disabled;
// acl
item._aclResult = item.acl ? this.aclService.can(item.acl) : true;
item.open = item.open != null ? item.open : false;
}
resume(callback) {
let i = 1;
const shortcuts = [];
this.visit(this.data, (item, parent, depth) => {
item._id = i++;
item._parent = parent;
item._depth = depth;
this.fixItem(item);
// shortcut
if (parent && item.shortcut === true && parent.shortcutRoot !== true) {
shortcuts.push(item);
}
if (callback)
callback(item, parent, depth);
});
this.loadShortcut(shortcuts);
this._change$.next(this.data);
}
/**
* 加载快捷菜单,加载位置规则如下:
* 1、统一在下标0的节点下(即【主导航】节点下方)
* 1、若 children 存在 【shortcutRoot: true】则最优先【推荐】这种方式
* 2、否则查找带有【dashboard】字样链接,若存在则在此菜单的下方创建快捷入口
* 3、否则放在0节点位置
*/
loadShortcut(shortcuts) {
if (shortcuts.length === 0 || this.data.length === 0) {
return;
}
const ls = this.data[0].children;
let pos = ls.findIndex(w => w.shortcutRoot === true);
if (pos === -1) {
pos = ls.findIndex(w => w.link.includes('dashboard'));
pos = (pos !== -1 ? pos : -1) + 1;
const shortcutMenu = {
text: '快捷菜单',
i18n: 'shortcut',
icon: 'icon-rocket',
children: []
};
this.data[0].children.splice(pos, 0, shortcutMenu);
}
let _data = this.data[0].children[pos];
if (_data.i18n)
_data.text = this.i18nSrv.fanyi(_data.i18n);
_data = Object.assign(_data, {
shortcutRoot: true,
_id: -1,
_parent: null,
_depth: 1
});
_data.children = shortcuts.map(i => {
i._depth = 2;
i._parent = _data;
return i;
});
}
/**
* 清空菜单
*/
clear() {
this.data = [];
this._change$.next(this.data);
}
/**
* Use `url` or `key` to find menus
*
* 利用 `url` 或 `key` 查找菜单
*/
find(options) {
const opt = { recursive: false, ignoreHide: false, last: false, ...options };
if (opt.key != null) {
return this.getItem(opt.key);
}
let url = opt.url;
let item = null;
while (!item && url) {
this.visit(opt.data ?? this.data, i => {
if (!opt.last && item != null) {
return;
}
if (opt.ignoreHide && i.hide) {
return;
}
if (opt.cb) {
const res = opt.cb(i);
if (typeof res === 'boolean' && res) {
item = i;
}
}
if (i.link != null && i.link === url) {
item = i;
}
});
if (!opt.recursive)
break;
if (/[?;]/g.test(url)) {
url = url.split(/[?;]/g)[0];
}
else {
url = url.split('/').slice(0, -1).join('/');
}
}
return item;
}
/**
* 根据url获取菜单列表
* - 若 `recursive: true` 则会自动向上递归查找
* - 菜单数据源包含 `/ware`,则 `/ware/1` 也视为 `/ware` 项
*/
getPathByUrl(url, recursive = false) {
const ret = [];
let item = this.find({ url, recursive });
if (!item)
return ret;
do {
ret.splice(0, 0, item);
item = item._parent;
} while (item);
return ret;
}
/**
* Get menu based on `key`
*/
getItem(key) {
let res = null;
this.visit(this.data, item => {
if (res == null && item.key === key) {
res = item;
}
});
return res;
}
/**
* Set menu based on `key`
*/
setItem(key, value, options) {
const item = typeof key === 'string' ? this.getItem(key) : key;
if (item == null)
return;
Object.keys(value).forEach(k => {
item[k] = value[k];
});
this.fixItem(item);
if (options?.emit !== false)
this._change$.next(this.data);
}
/**
* Open menu based on `key` or menu object
*/
open(keyOrItem, options) {
let item = typeof keyOrItem === 'string' ? this.find({ key: keyOrItem }) : keyOrItem;
if (item == null)
return;
this.visit(this.menus, (i) => {
i._selected = false;
if (!this.openStrictly)
i.open = false;
});
do {
item._selected = true;
item.open = true;
item = item._parent;
} while (item);
if (options?.emit !== false)
this._change$.next(this.data);
}
openAll(status) {
this.toggleOpen(null, { allStatus: status });
}
toggleOpen(keyOrItem, options) {
let item = typeof keyOrItem === 'string' ? this.find({ key: keyOrItem }) : keyOrItem;
if (item == null) {
this.visit(this.menus, (i) => {
i._selected = false;
i.open = options?.allStatus === true;
});
}
else {
if (!this.openStrictly) {
this.visit(this.menus, (i) => {
if (i !== item)
i.open = false;
});
let pItem = item._parent;
while (pItem) {
pItem.open = true;
pItem = pItem._parent;
}
}
item.open = !item.open;
}
if (options?.emit !== false)
this._change$.next(this.data);
}
ngOnDestroy() {
this._change$.unsubscribe();
this.i18n$?.unsubscribe();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: MenuService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: MenuService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: MenuService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
const ALAIN_SETTING_KEYS = new InjectionToken('ALAIN_SETTING_KEYS');
const ALAIN_SETTING_DEFAULT = {
provide: ALAIN_SETTING_KEYS,
useValue: {
layout: 'layout',
user: 'user',
app: 'app'
}
};
class SettingsService {
KEYS = inject(ALAIN_SETTING_KEYS);
platform = inject(Platform);
notify$ = new Subject();
_app = null;
_user = null;
_layout = null;
getData(key) {
if (!this.platform.isBrowser) {
return null;
}
return JSON.parse(localStorage.getItem(key) || 'null') || null;
}
setData(key, value) {
if (!this.platform.isBrowser) {
return;
}
localStorage.setItem(key, JSON.stringify(value));
}
get layout() {
if (!this._layout) {
this._layout = {
fixed: true,
collapsed: false,
boxed: false,
lang: null,
...this.getData(this.KEYS.layout)
};
this.setData(this.KEYS.layout, this._layout);
}
return this._layout;
}
get app() {
if (!this._app) {
this._app = {
year: new Date().getFullYear(),
...this.getData(this.KEYS.app)
};
this.setData(this.KEYS.app, this._app);
}
return this._app;
}
get user() {
if (!this._user) {
this._user = { ...this.getData(this.KEYS.user) };
this.setData(this.KEYS.user, this._user);
}
return this._user;
}
get notify() {
return this.notify$.asObservable();
}
setLayout(name, value) {
if (typeof name === 'string') {
this.layout[name] = value;
}
else {
this._layout = name;
}
this.setData(this.KEYS.layout, this._layout);
this.notify$.next({ type: 'layout', name, value });
return true;
}
getLayout() {
return this._layout;
}
setApp(value) {
this._app = value;
this.setData(this.KEYS.app, value);
this.notify$.next({ type: 'app', value });
}
getApp() {
return this._app;
}
setUser(value) {
this._user = value;
this.setData(this.KEYS.user, value);
this.notify$.next({ type: 'user', value });
}
getUser() {
return this._user;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: SettingsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: SettingsService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: SettingsService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
const REP_MAX = 6;
const SPAN_MAX = 24;
class ResponsiveService {
cogSrv = inject(AlainConfigService);
cog;
constructor() {
this.cog = this.cogSrv.merge('themeResponsive', {
rules: {
1: { xs: 24 },
2: { xs: 24, sm: 12 },
3: { xs: 24, sm: 12, md: 8 },
4: { xs: 24, sm: 12, md: 8, lg: 6 },
5: { xs: 24, sm: 12, md: 8, lg: 6, xl: 4 },
6: { xs: 24, sm: 12, md: 8, lg: 6, xl: 4, xxl: 2 }
}
});
if (Object.keys(this.cog.rules)
.map(i => +i)
.some((i) => i < 1 || i > REP_MAX)) {
throw new Error(`[theme] the responseive rule index value range must be 1-${REP_MAX}`);
}
}
genCls(count, defaultCol = 1) {
const rule = { ...this.cog.rules[count > REP_MAX ? REP_MAX : Math.max(count, 1)] };
const antColClass = 'ant-col';
const itemMaxSpan = SPAN_MAX / defaultCol;
const paddingSpan = (value) => {
if (value == null || defaultCol <= 1 || count >= defaultCol)
return value;
return Math.max(value, count * itemMaxSpan);
};
const clsMap = [`${antColClass}-xs-${paddingSpan(rule.xs)}`];
if (rule.sm)
clsMap.push(`${antColClass}-sm-${paddingSpan(rule.sm)}`);
if (rule.md)
clsMap.push(`${antColClass}-md-${paddingSpan(rule.md)}`);
if (rule.lg)
clsMap.push(`${antColClass}-lg-${paddingSpan(rule.lg)}`);
if (rule.xl)
clsMap.push(`${antColClass}-xl-${paddingSpan(rule.xl)}`);
if (rule.xxl)
clsMap.push(`${antColClass}-xxl-${paddingSpan(rule.xxl)}`);
return clsMap;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ResponsiveService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ResponsiveService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ResponsiveService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
const HTML_DIR = 'dir';
const RTL_DIRECTION = 'direction';
const RTL_NZ_COMPONENTS = ['modal', 'drawer', 'message', 'notification', 'image'];
const RTL_DELON_COMPONENTS = ['loading', 'onboarding'];
const LTR = 'ltr';
const RTL = 'rtl';
class RTLService {
d = inject(Directionality);
nz = inject(NzConfigService);
delon = inject(AlainConfigService);
platform = inject(Platform);
doc = inject(DOCUMENT);
srv = inject(SettingsService);
_dir = LTR;
/**
* Get or Set the current text direction
*
* 获取或设置当前文字方向
*/
get dir() {
return this._dir;
}
set dir(value) {
this._dir = value;
this.updateLibConfig();
this.updateHtml();
// Should be wait inited
Promise.resolve().then(() => {
this.d.valueSignal.set(value);
this.d.change.emit(value);
this.srv.setLayout(RTL_DIRECTION, value);
});
}
/**
* Get the next text direction
*
* 获取下一次文字方向
*/
get nextDir() {
return this.dir === LTR ? RTL : LTR;
}
/**
* Subscription change notification
*
* 订阅变更通知
*/
get change() {
return this.srv.notify.pipe(filter(w => w.name === RTL_DIRECTION), map(v => v.value));
}
constructor() {
this.dir = this.srv.layout.direction === RTL ? RTL : LTR;
}
/**
* Toggle text direction
*
* 切换文字方向
*/
toggle() {
this.dir = this.nextDir;
}
updateHtml() {
if (!this.platform.isBrowser) {
return;
}
const htmlEl = this.doc.querySelector('html');
if (htmlEl) {
const dir = this.dir;
htmlEl.style.direction = dir;
htmlEl.classList.remove(RTL, LTR);
htmlEl.classList.add(dir);
htmlEl.setAttribute(HTML_DIR, dir);
}
}
updateLibConfig() {
RTL_NZ_COMPONENTS.forEach(name => {
this.nz.set(name, { nzDirection: this.dir });
});
RTL_DELON_COMPONENTS.forEach(name => {
this.delon.set(name, { direction: this.dir });
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: RTLService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: RTLService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: RTLService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
class TitleService {
destroy$ = inject(DestroyRef);
_prefix = '';
_suffix = '';
_separator = ' - ';
_reverse = false;
tit$;
DELAY_TIME = 25;
doc = inject(DOCUMENT);
injector = inject(Injector);
title = inject(Title);
menuSrv = inject(MenuService);
i18nSrv = inject(ALAIN_I18N_TOKEN);
constructor() {
this.i18nSrv.change.pipe(takeUntilDestroyed()).subscribe(() => this.setTitle());
}
/**
* Set separator
*
* 设置分隔符
*/
set separator(value) {
this._separator = value;
}
/**
* Set prefix
*
* 设置前缀
*/
set prefix(value) {
this._prefix = value;
}
/**
* Set suffix
*
* 设置后缀
*/
set suffix(value) {
this._suffix = value;
}
/**
* Set whether to reverse
*
* 设置是否反转
*/
set reverse(value) {
this._reverse = value;
}
/**
* Set the default CSS selector string
*
* 设置默认CSS选择器字符串
*/
selector;
/**
* Set default title name
*
* 设置默认标题名
*/
default = `Not Page Name`;
getByElement() {
return of('').pipe(delay(this.DELAY_TIME), map(() => {
const el = ((this.selector != null ? this.doc.querySelector(this.selector) : null) ||
this.doc.querySelector('.alain-default__content-title h1') ||
this.doc.querySelector('.page-header__title'));
if (el) {
let text = '';
el.childNodes.forEach(val => {
if (!text && val.nodeType === 3) {
text = val.textContent.trim();
}
});
return text || el.firstChild.textContent.trim();
}
return '';
}));
}
getByRoute() {
let next = this.injector.get(ActivatedRoute);
while (next.firstChild)
next = next.firstChild;
const data = (next.snapshot && next.snapshot.data) || {};
if (data.titleI18n)
data.title = this.i18nSrv.fanyi(data.titleI18n);
return isObservable(data.title) ? data.title : of(data.title);
}
getByMenu() {
const menus = this.menuSrv.getPathByUrl(this.injector.get(Router).url);
if (!menus || menus.length <= 0)
return of('');
const item = menus[menus.length - 1];
let title;
if (item.i18n)
title = this.i18nSrv.fanyi(item.i18n);
return of(title || item.text);
}
/**
* Set the document title
*/
setTitle(title) {
this.tit$?.unsubscribe();
this.tit$ = of(title)
.pipe(switchMap(tit => (tit ? of(tit) : this.getByRoute())), switchMap(tit => (tit ? of(tit) : this.getByMenu())), switchMap(tit => (tit ? of(tit) : this.getByElement())), map(tit => tit || this.default), map(title => (!Array.isArray(title) ? [title] : title)), takeUntilDestroyed(this.destroy$))
.subscribe(titles => {
let newTitles = [];
if (this._prefix) {
newTitles.push(this._prefix);
}
newTitles.push(...titles.filter(title => !!title));
if (this._suffix) {
newTitles.push(this._suffix);
}
if (this._reverse) {
newTitles = newTitles.reverse();
}
this.title.setTitle(newTitles.join(this._separator));
});
}
/**
* Set i18n key of the document title
*/
setTitleByI18n(key, params) {
this.setTitle(this.i18nSrv.fanyi(key, params));
}
ngOnDestroy() {
this.tit$?.unsubscribe();
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: TitleService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: TitleService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: TitleService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
class I18nPipe {
i18n = inject(ALAIN_I18N_TOKEN);
transform(key, params) {
return this.i18n.fanyi(key, params);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: I18nPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "20.3.0", ngImport: i0, type: I18nPipe, isStandalone: true, name: "i18n" });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: I18nPipe, decorators: [{
type: Pipe,
args: [{ name: 'i18n' }]
}] });
class AlainI18NGuardService {
i18nSrv = inject(ALAIN_I18N_TOKEN, { optional: true });
cogSrv = inject(AlainConfigService);
process(route) {
const lang = route.params && route.params[this.cogSrv.get('themeI18n')?.paramNameOfUrlGuard ?? 'i18n'];
if (lang != null) {
this.i18nSrv?.use(lang);
}
return of(true);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AlainI18NGuardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AlainI18NGuardService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: AlainI18NGuardService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
/**
* Internationalization guard, automatically recognizes the language in Url and triggers the `ALAIN_I18N_TOKEN.use` method
*
* 国际化守卫,自动识别Url中的语言,并触发 `ALAIN_I18N_TOKEN.use` 方法
*
* ```ts
* data: {
* path: 'home',
* canActivate: [ alainI18nCanActivate ]
* }
* ```
*/
const alainI18nCanActivate = childRoute => inject(AlainI18NGuardService).process(childRoute);
/**
* Internationalization guard, automatically recognizes the language in Url and triggers the `ALAIN_I18N_TOKEN.use` method
*
* 国际化守卫,自动识别Url中的语言,并触发 `ALAIN_I18N_TOKEN.use` 方法
*
* ```ts
* data: {
* path: 'home',
* canActivateChild: [ alainI18nCanActivateChild ]
* }
* ```
*/
const alainI18nCanActivateChild = route => inject(AlainI18NGuardService).process(route);
const CLS_DRAG = 'MODAL-DRAG';
/**
* 对话框辅助类
*/
class ModalHelper {
srv = inject(NzModalService);
drag = inject(DragDrop);
doc = inject(DOCUMENT);
createDragRef(options, wrapCls) {
const wrapEl = this.doc.querySelector(wrapCls);
const modalEl = wrapEl.firstChild;
const handelEl = options.handleCls ? wrapEl.querySelector(options.handleCls) : null;
if (handelEl) {
handelEl.classList.add(`${CLS_DRAG}-HANDLE`);
}
return this.drag
.createDrag(handelEl ?? modalEl)
.withHandles([handelEl ?? modalEl])
.withBoundaryElement(wrapEl)
.withRootElement(modalEl);
}
/**
* 构建一个对话框
*
* @param comp 组件
* @param params 组件参数
* @param options 额外参数
*
* @example
* this.modalHelper.create(FormEditComponent, { i }).subscribe(res => this.load());
* // 对于组件的成功&关闭的处理说明
* // 成功,其中 `nzModalRef` 指目标组件在构造函数 `NzModalRef` 变量名
* this.nzModalRef.close(data);
* this.nzModalRef.close();
* // 关闭
* this.nzModalRef.destroy();
*/
create(comp, params, options) {
const isBuildIn = typeof comp === 'string';
options = deepMerge({
size: 'lg',
exact: true,
includeTabs: false
}, isBuildIn && arguments.length === 2 ? params : options);
return new Observable((observer) => {
const { size, includeTabs, modalOptions, drag, useNzData, focus } = options;
let cls = [];
let width = '';
if (size) {
if (typeof size === 'number') {
width = `${size}px`;
}
else if (['sm', 'md', 'lg', 'xl'].includes(size)) {
cls.push(`modal-${size}`);
}
else {
width = size;
}
}
if (includeTabs) {
cls.push(`modal-include-tabs`);
}
if (modalOptions && modalOptions.nzWrapClassName) {
cls.push(modalOptions.nzWrapClassName);
delete modalOptions.nzWrapClassName;
}
let dragOptions;
let dragWrapCls = `${CLS_DRAG}-${+new Date()}`;
let dragRef;
if (drag != null && drag !== false) {
dragOptions = {
handleCls: `.modal-header, .ant-modal-title`,
...(typeof drag === 'object' ? drag : {})
};
cls.push(CLS_DRAG, dragWrapCls);
}
const mth = isBuildIn ? this.srv[comp] : this.srv.create;
const subject = mth.call(this.srv, {
nzWrapClassName: cls.join(' '),
nzContent: isBuildIn ? undefined : comp,
nzWidth: width ? width : undefined,
nzFooter: null,
nzData: params,
nzDraggable: false,
...modalOptions
});
// 保留 nzComponentParams 原有风格,但依然可以通过 @Inject(NZ_MODAL_DATA) 获取
if (subject.componentInstance != null && useNzData !== true) {
Object.entries(params).forEach(([key, value]) => {
const t = subject.componentInstance;
const s = t[key]?.[SIGNAL];
if (s != null) {
s.value = value;
}
else {
t[key] = value;
}
});
}
subject.afterOpen
.pipe(take(1), tap(() => {
if (dragOptions != null) {
dragRef = this.createDragRef(dragOptions, `.${dragWrapCls}`);
}
}), filter(() => focus != null), delay(modalOptions?.nzNoAnimation ? 10 : 241))
.subscribe(() => {
const btns = subject
.getElement()
.querySelector('.ant-modal-confirm-btns, .modal-footer')
?.querySelectorAll('.ant-btn');
const btnSize = btns?.length ?? 0;
let el = null;
if (btnSize === 1) {
el = btns[0];
}
else if (btnSize > 1) {
el = btns[focus === 'ok' ? 1 : 0];
}
if (el != null) {
el.focus();
el.dataset.focused = focus;
}
});
subject.afterClose.pipe(take(1)).subscribe((res) => {
if (options.exact === true) {
if (res != null) {
observer.next(res);
}
}
else {
observer.next(res);
}
observer.complete();
dragRef?.dispose();
});
});
}
/**
* 构建静态框,点击蒙层不允许关闭
*
* @param comp 组件
* @param params 组件参数
* @param options 额外参数
*
* @example
* this.modalHelper.open(FormEditComponent, { i }).subscribe(res => this.load());
* // 对于组件的成功&关闭的处理说明
* // 成功,其中 `nzModalRef` 指目标组件在构造函数 `NzModalRef` 变量名
* this.nzModalRef.close(data);
* this.nzModalRef.close();
* // 关闭
* this.nzModalRef.destroy();
*/
createStatic(comp, params, options) {
const modalOptions = {
nzMaskClosable: false,
...(options && options.modalOptions)
};
return this.create(comp, params, { ...options, modalOptions });
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ModalHelper, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ModalHelper, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: ModalHelper, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
/**
* 抽屉辅助类
*
* **注意:** 构建结果都可被订阅,但永远都不会触发 `observer.error`
*
* @example
* this.drawerHelper.create('Edit', FormEditComponent, { i }).subscribe(res => this.load());
* // 对于组件的成功&关闭的处理说明
* // 成功
* this.NzDrawerRef.close(data);
* this.NzDrawerRef.close(true);
* // 关闭
* this.NzDrawerRef.close();
* this.NzDrawerRef.close(false);
*/
class DrawerHelper {
srv = inject(NzDrawerService);
parentDrawer = inject(DrawerHelper, { optional: true, skipSelf: true });
openDrawersAtThisLevel = [];
get openDrawers() {
return this.parentDrawer ? this.parentDrawer.openDrawers : this.openDrawersAtThisLevel;
}
/**
* 构建一个抽屉
*/
create(title, comp, params, options) {
options = deepMerge({
size: 'md',
footer: true,
footerHeight: 50,
exact: true,
drawerOptions: {
nzPlacement: 'right',
nzWrapClassName: ''
}
}, options);
return new Observable((observer) => {
const { size, footer, footerHeight, drawerOptions } = options;
const defaultOptions = {
nzContent: comp,
nzContentParams: params,
nzTitle: title
};
if (typeof size === 'number') {
defaultOptions[drawerOptions.nzPlacement === 'top' || drawerOptions.nzPlacement === 'bottom' ? 'nzHeight' : 'nzWidth'] = options.size;
}
else if (!drawerOptions.nzWidth) {
defaultOptions.nzWrapClassName = `${drawerOptions.nzWrapClassName} drawer-${options.size}`.trim();
delete drawerOptions.nzWrapClassName;
}
if (footer) {
// The 24 value is @drawer-body-padding
defaultOptions.nzBodyStyle = {
'padding-bottom': `${footerHeight + 24}px`
};
}
const ref = this.srv.create({ ...defaultOptions, ...drawerOptions });
this.openDrawers.push(ref);
const afterClose$ = ref.afterClose.subscribe((res) => {
if (options.exact === true) {
if (res != null) {
observer.next(res);
}
}
else {
observer.next(res);
}
observer.complete();
afterClose$.unsubscribe();
this.close(ref);
});
});
}
close(ref) {
const idx = this.openDrawers.indexOf(ref);
if (idx === -1)
return;
this.openDrawers.splice(idx, 1);
}
closeAll() {
let i = this.openDrawers.length;
while (i--) {
this.openDrawers[i].close();
}
}
/**
* 构建一个抽屉,点击蒙层不允许关闭
*/
static(title, comp, params, options) {
const drawerOptions = {
nzMaskClosable: false,
...(options && options.drawerOptions)
};
return this.create(title, comp, params, { ...options, drawerOptions });
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: DrawerHelper, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: DrawerHelper, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: DrawerHelper, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
/**
* 封装HttpClient,主要解决:
* + 优化HttpClient在参数上便利性
* + 统一实现 loading
* + 统一处理时间格式问题
*/
class _HttpClient {
http = inject(HttpClient);
cogSrv = inject(AlainConfigService);
cog;
constructor() {
this.cog = this.cogSrv.merge('themeHttp', {
nullValueHandling: 'include',
dateValueHandling: 'timestamp'
});
}
lc = 0;
/**
* Get whether it's loading
*
* 获取是否正在加载中
*/
get loading() {
return this.lc > 0;
}
/**
* Get the currently loading count
*
* 获取当前加载中的数量
*/
get loadingCount() {
return this.lc;
}
parseParams(params) {
const newParams = {};
if (params instanceof HttpParams) {
return params;
}
const { nullValueHandling, dateValueHandling } = this.cog;
Object.keys(params).forEach(key => {
let paramValue = params[key];
// 忽略空值
if (nullValueHandling === 'ignore' && paramValue == null)
return;
// 将时间转化为:时间戳 (秒)
if (paramValue instanceof Date &&
(dateValueHandling === 'timestamp' || dateValueHandling === 'timestampSecond')) {
paramValue = dateValueHandling === 'timestamp' ? paramValue.valueOf() : Math.trunc(paramValue.valueOf() / 1000);
}
newParams[key] = paramValue;
});
return new HttpParams({ fromObject: newParams });
}
appliedUrl(url, params) {
if (!params)
return url;
url += ~url.indexOf('?') ? '' : '?';
const arr = [];
Object.keys(params).forEach(key => {
arr.push(`${key}=${params[key]}`);
});
return url + arr.join('&');
}
setCount(count) {
Promise.resolve(null).then(() => (this.lc = count <= 0 ? 0 : count));
}
push() {
this.setCount(++this.lc);
}
pop() {
this.setCount(--this.lc);
}
/**
* Clean loading count
*
* 清空加载中
*/
cleanLoading() {
this.setCount(0);
}
get(url, params, options = {}) {
return this.request('GET', url, {
params,
...options
});
}
post(url, body, params, options = {}) {
return this.request('POST', url, {
body,
params,
...options
});
}
delete(url, params, options = {}) {
return this.request('DELETE', url, {
params,
...options
});
}
// #endregion
// #region jsonp
/**
* **JSONP Request**
*
* @param callbackParam CALLBACK值,默认:JSONP_CALLBACK
*/
jsonp(url, params, callbackParam = 'JSONP_CALLBACK') {
return of(null).pipe(
// Make sure to always be asynchronous, see issues: https://github.com/ng-alain/ng-alain/issues/1954
delay(0), tap(() => this.push()), switchMap(() => this.http.jsonp(this.appliedUrl(url, params), callbackParam)), finalize(() => this.pop()));
}
patch(url, body, params, options = {}) {
return this.request('PATCH', url, {
body,
params,
...options
});
}
put(url, body, params, options = {}) {
return this.request('PUT', url, {
body,
params,
...options
});
}
form(url, body, params, options = {}) {
return this.request('POST', url, {
body,
params,
...options,
headers: {
'content-type': `application/x-www-form-urlencoded`
}
});
}
request(method, url, options = {}) {
if (options.params)
options.params = this.parseParams(options.params);
return of(null).pipe(
// Make sure to always be asynchronous, see issues: https://github.com/ng-alain/ng-alain/issues/1954
delay(0), tap(() => this.push()), switchMap(() => this.http.request(method, url, options)), finalize(() => this.pop()));
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: _HttpClient, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: _HttpClient, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: _HttpClient, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
/**
* Every http decorator must be based on `BaseAPI`, Like this:
* ```ts
* \@Injectable()
* class DataService extends BaseApi {}
* ```
*/
class BaseApi {
injector = inject(Injector);
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: BaseApi, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: BaseApi });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: BaseApi, decorators: [{
type: Injectable
}] });
const paramKey = `__api_params`;
function setParam(target, key = paramKey) {
let params = target[key];
if (typeof params === 'undefined') {
params = target[key] = {};
}
return params;
}
/**
* 默认基准URL
* - 有效范围:类
*/
function BaseUrl(url) {
return function (target) {
const params = setParam(target.prototype);
params.baseUrl = url;
return target;
};
}
/**
* 默认 `headers`
* - 有效范围:类
*/
function BaseHeaders(headers) {
return function (target) {
const params = setParam(target.prototype);
params.baseHeaders = headers;
return target;
};
}
function makeParam(paramName) {
return function (key) {
return function (target, propertyKey, index) {
const params = setParam(setParam(target), propertyKey);
let tParams = params[paramName];
if (typeof tParams === 'undefined') {
tParams = params[paramName] = [];
}
tParams.push({
key,
index
});
};
};
}
/**
* URL路由参数
* - 有效范围:方法参数
*/
const Path = makeParam('path');
/**
* URL 参数 `QueryString`
* - 有效范围:方法参数
*/
const Query = makeParam('query');
/**
* 参数 `Body`
* - 有效范围:方法参数
*/
const Body = makeParam('body')();
/**
* 参数 `headers`
* - 有效范围:方法参数
* - 合并 `BaseHeaders`
*/
const Headers = makeParam('headers');
/**
* Request Payload
* - Supported body (like`POST`, `PUT`) as a body data, equivalent to `@Body`
* - Not supported body (like `GET`, `DELETE` etc) as a `QueryString`
*/
const Payload = makeParam('payload')();
function getValidArgs(data, key, args) {
if (!data[key] || !Array.isArray(data[key]) || data[key].length <= 0) {
return undefined;
}
return args[data[key][0].index];
}
function genBody(data, payload) {
if (Array.isArray(data) || Array.isArray(payload)) {
return Object.assign([], data, payload);
}
return { ...data, ...payload };
}
function makeMethod(method) {
return function (url = '', options) {
return (_target, targetKey, descriptor) => {
descriptor.value = function (...args) {
options = options || {};
const injector = this.injector;
const http = injector.get(_HttpClient, null);
if (http == null) {
throw new TypeError(`Not found '_HttpClient', You can import 'AlainThemeModule' && 'HttpClientModule' in your root module.`);
}
const baseData = setParam(this);
const data = setParam(baseData, targetKey);
let requestUrl = url || '';
requestUrl = [baseData.baseUrl || '', requestUrl.startsWith('/') ? requestUrl.substring(1) : requestUrl].join('/');
// fix last split
if (requestUrl.length > 1 && requestUrl.endsWith('/')) {
requestUrl = requestUrl.substring(0, requestUrl.length - 1);
}
if (options.acl) {
const aclSrv = injector.get(ACLService, null);
if (aclSrv && !aclSrv.can(options.acl)) {
return throwError(() => ({
url: requestUrl,
status: 401,
statusText: `From Http Decorator`
}));
}
delete options.acl;
}
requestUrl = requestUrl.replace(/::/g, '^^');
(data.path || [])
.filter(w => typeof args[w.index] !== 'undefined')
.forEach((i) => {
requestUrl = requestUrl.replace(new RegExp(`:${i.key}`, 'g'), encodeURIComponent(args[i.index]));
});
requestUrl = requestUrl.replace(/\^\^/g, `:`);
const params = (data.query || []).reduce((p, i) => {
p[i.key] = args[i.index];
return p;
}, {});
const headers = (data.headers || []).reduce((p, i) => {
p[i.key] = args[i.index];
return p;
}, {});
if (method === 'FORM') {
headers['content-type'] = 'application/x-www-form-urlencoded';
}
const payload = getValidArgs(data, 'payload', args);
const supportedBody = ['POST', 'PUT', 'PATCH', 'DELETE'].some(v => v === method);
return http.request(method, requestUrl, {
body: supportedBody ? genBody(getValidArgs(data, 'body', args), payload) : null,
params: !sup