ohayolibs
Version:
Ohayo is a set of essential modules for ohayojp.
278 lines (235 loc) • 7.37 kB
text/typescript
import { Inject, Injectable, OnDestroy, Optional } from '@angular/core';
import { ACLService } from '@ohayo/acl';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { share } from 'rxjs/operators';
import { OhayoI18NService, OHAYO_I18N_TOKEN } from '../i18n/i18n';
import { Menu, MenuIcon, MenuInner } from './interface';
/**
* 菜单服务,[在线文档](https://ohayojp.com/theme/menu)
*/
export class MenuService implements OnDestroy {
private _change$: BehaviorSubject<Menu[]> = new BehaviorSubject<Menu[]>([]);
private i18n$: Subscription;
private data: Menu[] = [];
constructor(
private i18nSrv: OhayoI18NService,
private aclService: ACLService,
) {
this.i18n$ = this.i18nSrv.change.subscribe(() => this.resume());
}
get change(): Observable<Menu[]> {
return this._change$.pipe(share());
}
visit(data: Menu[], callback: (item: Menu, parentMenum: Menu | null, depth?: number) => void): void {
const inFn = (list: Menu[], parentMenu: Menu | null, depth: number) => {
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: Menu[]): void {
this.data = items;
this.resume();
}
private fixItem(item: MenuInner): void {
item._aclResult = true;
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 } as any;
}
if (item.icon != null) {
item.icon = { theme: 'outline', spin: false, ...(item.icon as MenuIcon) };
}
item.text = item.i18n && this.i18nSrv ? 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 ? this.aclService.can(item.acl) : true;
}
/**
* 重置菜单,可能I18N、用户权限变动时需要调用刷新
*/
resume(callback?: (item: Menu, parentMenum: Menu | null, depth?: number) => void): void {
let i = 1;
const shortcuts: Menu[] = [];
this.visit(this.data, (item: MenuInner, 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节点位置
*/
private loadShortcut(shortcuts: MenuInner[]): void {
if (shortcuts.length === 0 || this.data.length === 0) {
return;
}
const ls = this.data[0].children as MenuInner[];
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: [],
} as MenuInner;
this.data[0].children!.splice(pos, 0, shortcutMenu);
}
let _data = this.data[0].children![pos];
if (_data.i18n && this.i18nSrv) _data.text = this.i18nSrv.fanyi(_data.i18n);
// tslint:disable-next-line:prefer-object-spread
_data = Object.assign(_data, {
shortcutRoot: true,
_id: -1,
_parent: null,
_depth: 1,
} as MenuInner);
_data.children = shortcuts.map(i => {
i._depth = 2;
i._parent = _data;
return i;
});
}
get menus(): Menu[] {
return this.data;
}
/**
* 清空菜单
*/
clear(): void {
this.data = [];
this._change$.next(this.data);
}
getHit(data: Menu[], url: string, recursive: boolean = false, cb: ((i: Menu) => void) | null = null): Menu | null {
let item: Menu | null = null;
while (!item && url) {
this.visit(data, i => {
if (cb) {
cb(i);
}
if (i.link != null && i.link === url) {
item = i;
}
});
if (!recursive) break;
if (/[?;]/g.test(url)) {
url = url.split(/[?;]/g)[0];
} else {
url = url.split('/').slice(0, -1).join('/');
}
}
return item;
}
/**
* 根据URL设置菜单 `_open` 属性
* - 若 `recursive: true` 则会自动向上递归查找
* - 菜单数据源包含 `/ware`,则 `/ware/1` 也视为 `/ware` 项
*/
openedByUrl(url: string | null, recursive: boolean = false): void {
if (!url) return;
let findItem = this.getHit(this.data, url, recursive, (i: MenuInner) => {
i._selected = false;
i._open = false;
}) as MenuInner;
if (findItem == null) return;
do {
findItem._selected = true;
findItem._open = true;
findItem = findItem._parent!;
} while (findItem);
}
/**
* 根据url获取菜单列表
* - 若 `recursive: true` 则会自动向上递归查找
* - 菜单数据源包含 `/ware`,则 `/ware/1` 也视为 `/ware` 项
*/
getPathByUrl(url: string, recursive: boolean = false): Menu[] {
const ret: Menu[] = [];
let item = this.getHit(this.data, url, recursive) as MenuInner;
if (!item) return ret;
do {
ret.splice(0, 0, item);
item = item._parent!;
} while (item);
return ret;
}
/**
* Get menu based on `key`
*/
getItem(key: string): Menu | null {
let res: Menu | null = null;
this.visit(this.data, item => {
if (res == null && item.key === key) {
res = item;
}
});
return res;
}
/**
* Set menu based on `key`
*/
setItem(key: string, value: Menu): void {
const item = this.getItem(key);
if (item == null) return;
Object.keys(value).forEach(k => {
item[k] = value[k];
});
this.fixItem(item);
this._change$.next(this.data);
}
ngOnDestroy(): void {
this._change$.unsubscribe();
this.i18n$.unsubscribe();
}
}