jii
Version:
Jii - Full-Stack JavaScript Framework
388 lines (336 loc) • 12.1 kB
JavaScript
'use strict';
const Jii = require('../index');
const MenuHelper = require('./MenuHelper');
const SiteMenuItem = require('./SiteMenuItem');
const Component = require('../base/Component');
const _forIn = require('lodash/forIn');
const _merge = require('lodash/merge');
const _sortBy = require('lodash/sortBy');
const _difference = require('lodash/difference');
const _keys = require('lodash/keys');
/**
* Class SiteMenu
* @property {object} $items
* @property-read {object} $activeItem
*/
class SiteMenu extends Component {
preInit() {
this._items = {};
super.preInit(...arguments);
}
init() {
super.init();
Jii.app.urlManager.setRules(MenuHelper.menuToRules(this._items));
}
/**
* Set menu items
* @param {object} items
*/
setItems(items) {
this.addItems(items);
}
/**
* Get all tree menu items
* @return {SiteMenuItem[]}
*/
getItems() {
return this._items;
}
/**
* Add tree menu items
* @param {object} items
* @param {boolean} append
*/
addItems(items, append = true) {
this._items = this.mergeItems(this._items, items, append);
}
/**
* Merge MenuItems and object with this.items
* @param baseItems
* @param items
* @param append
* @returns {*}
*/
mergeItems(baseItems, items, append) {
_forIn(items, (item, id) => {
// Merge item with group (as key)
if (typeof(id) == 'string' && baseItems['_' + id]) {
_forIn(item, (value, key) => {
if (key === 'items') {
baseItems['_' + id][key] = this.mergeItems(baseItems['_' + id][key], value, append);
}
else if (typeof(baseItems['_' + id]) == 'object' && typeof(value) == 'object') {
baseItems['_' + id][key] = append
? _merge(baseItems['_' + id][key], value)
: _merge(value, baseItems['_' + id][key]);
}
else if (append || baseItems['_' + id][key] === null) {
baseItems['_' + id][key] = value;
}
});
} else {
// Create instance
if (!(item instanceof SiteMenuItem)) {
item = new SiteMenuItem(_merge(item, {'owner': this}));
item.items = this.mergeItems([], item.items, true);
}
// Append or prepend item
if (append) {
baseItems['_' + id] = item;
} else {
baseItems = _merge({['_' + id]: item}, baseItems);
}
}
});
//sort
let sortedBaseItems = {};
Object.keys(baseItems)
.sort((key1, key2) => baseItems[key1].order > baseItems[key2].order)
.forEach(key => sortedBaseItems[key] = baseItems[key]);
return sortedBaseItems;
}
/**
* @return {SiteMenuItem}
*/
getActiveItem() {
return this.getItem(MenuHelper.getRequestedRoute());
}
/**
* Recursive find menu item by param item (set null for return root) and return tree menu
* items (in format for Jii\bootstrap\Nav.items). In param custom you can overwrite items
* configuration, if set it as array. Set param custom as integer for limit tree levels.
* For example, getMenu(null, 2) return two-level menu
* @param {object|boolean} fromItem
* @param {object} custom Items or level limit
* @return {object}
*/
getMenu(fromItem = null, custom = []) {
let itemModels = [];
if (fromItem) {
const item = this.getItem(fromItem);
if (item !== null) {
itemModels = item.items;
}
} else {
itemModels = this.getItems();
}
if (typeof(custom) == 'number') {
// Level limit
return this.sliceTreeItems(itemModels, custom);
}
let menu = [];
if (!custom || !custom.length) {
// All
menu = itemModels;
} else {
// Custom
/** @TODO */
/* foreach (custom as item) {
menuItemModel = this.getItem(item);
// Process items
if (item['items']) {
menuItemModel['items'] = this.getMenu(item['items']);
} else {
unset(menuItemModel['items']);
}
// Extend item
menuItemModel = array_merge(menuItemModel, item);
menu.push(menuItemModel;
}*/
}
return menu.map(itemModel => {
/** @var {SiteMenuItem} itemModel */
let arrayModel = itemModel.toArray();
arrayModel['url'] = MenuHelper.normalizeUrl(arrayModel['url'], arrayModel['urlRule']);
return arrayModel;
});
}
/**
* Find item by url (ot current page) label and return it
* @param {object} url Child url or route, default - current route
* @return {string}
*/
getTitle(url = null) {
const titles = this.getBreadcrumbs(url).reverse();
return titles && titles.length ? titles[0]['label'] : '';
}
/**
* Find item by url (or current page) and return item label with all parent labels
* @param {object} url Child url or route, default - current route
* @param {string} separator Separator, default is ' - '
* @return {string}
*/
getFullTitle(url = null, separator = ' — ') {
let title = [];
this.getBreadcrumbs(url).reverse().map(item => {
title.push(item['label']);
});
title.push(Jii.app.name);
return title.join(separator);
}
/**
* Return breadcrumbs links for widget \Jii-react\widgets\Breadcrumbs
* @param {object} url Child url or route, default - current route
* @return {object}
*/
getBreadcrumbs(url = null) {
url = url ? url : MenuHelper.getRequestedRoute();
// Find child and it parents by url
let parents = [];
const itemModel = this.getItem(url, parents, true);
if (!itemModel || (!parents.length && this.isHomeUrl(itemModel.url))) {
return [];
}
parents.reverse().push({
'label': itemModel.label,
'url': itemModel.url,
'urlRule': itemModel.urlRule,
'linkOptions': typeof(itemModel.linkOptions) == 'object' ? itemModel.linkOptions : [],
});
parents.map(parent => {
if (parent['linkOptions']) {
parent = _merge(parent, parent['linkOptions']);
delete parent['linkOptions'];
}
parent['url'] = MenuHelper.normalizeUrl(parent['url'], parent['urlRule']);
delete parent['urlRule'];
});
// @todo ugly solution - rework needed
parents[parents.length - 1]['url'] = false;
return parents;
}
/**
* Find menu item by item url or route. In param parents will be added all parent items
* @param {string|object} item
* @param {object} parents
* @param {boolean} forBreadcrumbs
* @return SiteMenuItem|null
*/
getItem(item, parents = [], forBreadcrumbs = false) {
const url = typeof(item) == 'object' && !this.isRoute(item) ?
item['url'] :
item;
return this.findItemRecursive(url, this.getItems(), parents, forBreadcrumbs);
}
/**
* Find item by url or route and return it url
* @param item
* @return {object}|null|string
*/
getItemUrl(item) {
item = this.getItem(item || []);
return item ? item.url : null;
}
/**
* @param {string|object|SiteMenuItem} url1
* @param {string|object|SiteMenuItem} url2
* @return {boolean}
*/
isUrlEquals(url1, url2) {
if (url1 instanceof SiteMenuItem) {
url1 = url1.url;
}
if (url2 instanceof SiteMenuItem) {
url2 = url2.url;
}
// Is routes
if (this.isRoute(url1) && this.isRoute(url2)) {
if (MenuHelper.normalizeRoute(url1[0]) !== MenuHelper.normalizeRoute(url2[0])) {
return false;
}
// Compare routes' parameters by checking if keys are identical
const keysUrl1 = Object.keys(url1);
const keysUrl2 = Object.keys(url2);
if (keysUrl1.length != keysUrl2.length || _difference(keysUrl1, keysUrl2).length || _difference(keysUrl2, keysUrl1).length) {
return false;
}
Object.keys(url1).map((value, key) => {
if (typeof(key) == 'string' && key !== '#') {
if (!url2[key] || typeof(url2[key]) == 'object' && !url2[key].length) {
return false;
}
if (value !== null && url2[key] !== null && url2[key] !== value) {
return false;
}
}
});
return true;
}
// Is urls
if (typeof(url1) == 'string' && typeof(url2) == 'string') {
return url1 === url2;
}
return false;
}
/**
* @param {string|object} url
* @return {boolean}
*/
isHomeUrl(url) {
if (this.isRoute(url)) {
return this.isUrlEquals(['/' + Jii.app.defaultRoute], url);
}
return url === Jii.app.getHomeUrl();
}
/**
* @param {string|object} value
* @return {boolean}
*/
isRoute(value) {
return typeof(value) == 'object' && value[0] && typeof(value[0]) == 'string';
}
/**
* @param {SiteMenuItem[]} items
* @param {number} level
* @return {object}
*/
sliceTreeItems(items, level = 1) {
if (level <= 0) {
return [];
}
let menu = [];
_forIn(items, itemModel => {
let item = itemModel.toArray();
if (itemModel.items) {
let nextLevel = level;
if (itemModel.url !== null) {
nextLevel--;
}
item['items'] = this.sliceTreeItems(itemModel.items, nextLevel);
}
item['url'] = MenuHelper.normalizeUrl(item['url'], item['urlRule']);
if (!item['items'] || !item['items'].length) {
item['items'] = null;
}
menu.push(item);
});
return menu;
}
/**
* @param {string|object} url
* @param {SiteMenuItem[]} items
* @param {object} parents
* @param {boolean} forBreadcrumbs
* @return SiteMenuItem
*/
findItemRecursive(url, items, parents, forBreadcrumbs = false) {
for (var key in items) {
if (items.hasOwnProperty(key)) {
if (items[key].url && this.isUrlEquals(url, items[key].url)) {
return items[key];
}
if (items[key].items) {
let foundItem = this.findItemRecursive(url, items[key].items, parents, forBreadcrumbs);
if (foundItem) {
let parentItem = items[key].toArray(forBreadcrumbs);
delete parentItem['items'];
parents.push(parentItem);
return foundItem;
}
}
}
}
return null;
}
}
module.exports = SiteMenu;