UNPKG

gmenu

Version:
397 lines (323 loc) 9.2 kB
/*jshint esversion:6, node:true*/ 'use strict'; const slug = require('./slug'); const extend = require('gextend'); let _instances = new Map(); /** * Creates a new Menu instance. * Internally, Menu will create new items * that are also instances of the Menu class. * * @param {String} name Menu ID * @param {Object} config Configuration * @private {Menu} parent */ class Menu { constructor(name, config = {}, parent = undefined) { this.name = name; if (parent) this.parent = parent; this.init(config); } init(config = {}) { this.nodes = []; this._breadcrumbs = []; extend(this, Menu.DEFAULTS, config); if (!this.parent) { this.logger.info('set %s', this.id, this.name); const node = _instances.get(this.id); if (!node) { return _instances.set(this.id, this); } if (!node._lazy) return; this.attachLazyNode(node); _instances.set(this.id, this); } } attachLazyNode(node) { //We need to update the properties of the other //menu... figure a better way!!! :) // node.segment = this.segment; const nodes = node.nodes.concat(); nodes.map(node => node.parent = this); this.nodes = this.nodes.concat(nodes); } /** * Attach this node's root node to a connect * application as a middleware handler. * * TODO: We might want to have a global/static * middleware method, so that we can export either * once or mark a Menu to be exported. Otherwhise * we only have a menu available when we are using * that router. * * @param {Object} req Connect request * @param {Object} res Connect response * @param {Function} next Connect next handler * @return {void} */ middleware(req, res, next) { /* * Added to support fastify */ this.root.request = (req.raw ? req.raw : req); this.logger.info('Generate "%s"', this.id + 'Menu'); this.logger.info('path "%s"', this.root.request.path); this.logger.info('Export menu as %s', this.localName); this.root.reset(); let locals = res.locals; let name = this.root.localName; if (locals && !locals[name]) { locals[name] = this.exportLocalVariables(req); } else { this.logger.warn('We are not exporting %s.', name); this.logger.warn('There exists a local with that name'); } next(); } /** * Stub implementation of function to * filter out nodes. * * @param {http.IncommingRequest} * @returns {Object} Object with nodes */ exportLocalVariables(req) { return this.root.toJSON(); } filterNode(node) { if(this.parent && typeof this.parent.filterNode === 'function') { return this.parent.filterNode(node); } return node; } /** * Given a keypath find a leaf node: * * @example ```js * root.find('admin.users.create'); * ``` * * @param {String} keypath Path to node * @param {Menu} [element=this] Node to start search from * @returns {Object} Node matching keypath. If no node found * we return undefined. */ find(keypath, element=this) { if (element.keypath == keypath) return element; if (!element.nodes) return; let result; for(let node of element.nodes) { result = this.find(keypath, node); if(result) return result; } } /** * In order to calculate the active state * we need to reset the state of the menu * on each request. */ reset() { this._isActive = false; this._breadcrumbs = []; function resetNode(node) { node.isActive = false; if (node.nodes) { node.nodes.map(resetNode); } } this.nodes.map(resetNode); } addNode(name, config) { let node; if (typeof name === 'object') { node = name; node.parent = this; } else { node = new Menu(name, config, this); } this.nodes.push(node); this.logger.info('Menu %s adding node %s', this.name, node.name); return node; } toCLI() { let label = (new Array(this.depth + 1)).join(' ') + '%s'; this.logger.info(label, this.name); if (this.isLeaf) return this.name; this.nodes.map(node => { return node.toCLI(); }); } toJSON() { //TODO: should we do outside of toJSON, do it //on setActiveURL() if (this.requestPath === this.uri) { this.isActive = true; } let item = { id: this.id, uri: this.uri, name: this.name, depth: this.depth, isActive: null, isExternal: !!this.link, keypath: this.keypath, data: extend({}, this.data), breadcrumbs: [], nodes: [], }; if (this.isLeaf) { delete item.nodes; } else { item.nodes = this.nodes.map(node => node.toJSON()).filter(Boolean); } item.isActive = this.isActive; //handle breadcrumbs this.updateBreadcrumbs(); if (this.depth === 0) { item.breadcrumbs = this.breadcrumbs; } else { delete item.breadcrumbs; } item = this.filterNode(item); return item; } updateBreadcrumbs() { if (this.isActive) { this.root._breadcrumbs[this.depth] = this.breadcrumb; } } get breadcrumbs() { return this._breadcrumbs.filter(Boolean); } get breadcrumb() { return { name: this.name, uri: this.uri, depth: this.depth }; } get isRoot() { return this.root === this; } get isLeaf() { return this.nodes.length === 0; } get depth() { if (this.isRoot) return 0; return this.parent.depth + 1; } get root() { if (this.parent) { return this.parent.root; } return this; } get requestPath() { return this.root.request.path; } /** * This will return something like: * `main.admin.debug.console` */ get keypath() { if(this.parent) return this.parent.keypath + '.' + this.id; return this.id; } /** * * Returns a normalized string used * to identify the menu globally. * * Under the hood we use `slug` to * remove extraneous characters and * to normalize the menu's given name. * * Also, the name will be lowercased. * * If we use the middleware option, * this property plus the value * of `localExportSuffix` to export the * menu as a local variable to make it * available to express rendered pages. * * @return {String} Normalized string. */ get id() { return slug(this.name, { lowercase: true }); } get uri() { if (this.link) { return this.link; } let uri = this.id; if (this._segment === false) { uri = ''; } else if (typeof this._segment === 'string') { uri = this._segment; } uri = (this.parent ? this.parent.uri : '') + '/' + uri; uri = require('path').normalize(uri); return uri; } get isActive() { return this._active || false; } set isActive(value) { if (this.parent && value) { this.parent.isActive = true; } this._active = value; } set request(value) { this._request = value; } get request() { if (this._request) return this._request; return { path: '/' }; } get localName() { return this.id + this.localExportSuffix; } set segment(v) { this._segment = v; } get segment() { return this._segment; } get parent() { return this._parent; } set parent(v) { this._parent = v; } } Menu.DEFAULTS = { logger: console, localExportSuffix: 'Menu' }; /** * Get a menu by id. If it does * not exist yet it will create a * lazy instance. * Meaning that it supports attaching * sub-menus before we have created * the actual menu. * * This is useful to not have to worry * abbout order of menu creation. * * @param {String} id Menu identifier */ Menu.get = function(id, config = {}) { if (_instances.has(id)) { return _instances.get(id); } /* * Support attaching sub menus * before we have created the * actual Menu. This is useful * so that we don't have to worry * about order of creation. */ let node = new Menu(id, config); node._lazy = true; return node; }; module.exports = Menu;