apostrophe
Version:
The Apostrophe Content Management System.
248 lines (245 loc) • 10.2 kB
JavaScript
// The admin bar module implements Apostrophe's admin bar at the top of the screen. Any module
// can register a button (or more than one) for this bar by calling the `add` method of this
// module. Buttons can also be grouped into dropdown menus and restricted to those with
// particular permissions. [apostrophe-pieces](/core-concepts/reusable-content-pieces/README.md) automatically
// takes advantage of this module.
//
// The admin bar slides out on all pages by default. It's possible to modify this behavior
// by adding one of the following options in app.js:
// ```
// modules: {
// 'apostrophe-admin-bar': {
// openOnLoad: false,
// openOnHomepageLoad: true,
// closeDelay: 5000
// },
// .... more modules ...
// }
// ```
// In the above example, the admin bar stays closed on all sub pages of the site, but opens on the
// homepage and stays open for 5 seconds (default is 3 seconds).
// `closeDelay` is a global configuration and changes both the homepage and all subpages.
//
// On the browser side there are also conveniences to implement jQuery handlers for these
// menu items.
var _ = require('@sailshq/lodash');
module.exports = {
alias: 'adminBar',
// Push the assets and the browser call to create the browser-side singleton
// that provides `apos.adminBar.link`. Most server-side initialization happens
// in `self.modulesReady` rather than here.
afterConstruct: function(self) {
self.pushAssets();
},
construct: function(self, options) {
self.items = [];
self.groups = [];
self.groupLabels = {};
// Add an item to the admin bar.
//
// The name argument becomes the value of the `data-apos-admin-bar-item`
// attribute of the admin bar item.
//
// `permission` should be a permission name such as `admin`
// (the user must be a full admin) or `edit-apostrophe-event`
// (the user can create events and edit their own). If
// `permission` is null then being logged in is
// good enough to see the item. (Securing your actual routes that
// respond to these items is up to you.)
//
// Usually just one admin bar item per module makes sense, so it's
// common to pass `self.__meta.name` (the module's name) as the name argument.
//
// For example, the pieces module does this:
//
// ```
// self.apos.adminBar.add(self.__meta.name, self.pluralLabel, 'edit')
// ```
//
// If you have an `events` module that subclasses pieces, then this
// creates an admin bar item with a data-apos-admin-item="events" attribute.
//
// Browser side, you can call `apos.adminBar.link('item-name', function() { ...})`
// to conveniently set up an event handler that fires when this button is clicked.
// Or, if you wish to create an ordinary link, you can pass the `href` option
// as part of the `options` object (fourth argument).
//
// You can use the `after` option to specify an admin bar item name
// this item should appear immediately following.
self.add = function(name, label, permission, options) {
var index;
var item = {
name: name,
label: label,
permission: permission,
options: options || {}
};
if (options && options.after) {
index = _.findIndex(self.items, { name: options.after });
if (index !== -1) {
self.items.splice(index + 1, 0, item);
return;
}
}
self.items.push(item);
};
// Group several menu items together in the interface (currently
// implemented as a dropdown menu). If `items` is an array of menu
// item names, then the group's label is the same as the label of
// the first item. If you wish the label to differ from the label
// of the first item, instead pass an object with a `label` property
// and an `items` property.
self.group = function(items) {
self.groups.push(items);
};
self.addHelpers({
// Render the admin bar. If the user is not able to see any items,
// nothing is rendered
output: function() {
// Find the subset of admin bar items this user is permitted to see
var user = self.apos.templates.contextReq.user;
if (!user) {
return '';
}
var items = _.filter(self.items, function(item) {
return self.itemIsVisible(self.apos.templates.contextReq, item);
});
if (!items.length) {
return '';
}
// Find the combined items and group them up into menus.
// groupedItems becomes an array that mixes standalone
// admin bar items with menus.
var groupedItems = [];
var menu = false;
_.each(items, function(item, i) {
if (menu) {
// We are already building up a grouped menu, but stop doing that
// if this next item isn't part of it
if (item.menuLeader === menu.leader.name) {
menu.items.push(item);
return;
} else {
menu = false;
}
}
// Only build a menu if there are at least two items after filtering
// for permissions
if ((item.menuLeader === item.name) && (items[i + 1] && items[i + 1].menuLeader === item.name)) {
menu = {
menu: true,
items: [ item ],
leader: item,
label: self.groupLabels[item.name] || item.label
};
groupedItems.push(menu);
} else {
groupedItems.push(item);
}
});
return self.partial('adminBar', { items: groupedItems });
}
});
// Like the assets module, we wait as long as humanly possible
// to lock down the list of admin bar items, so that other modules
// can bicker amongst themselves even during the `modulesReady` stage.
// When we get to `afterInit`, we can no longer wait! Order and
// group the admin bar items.
self.afterInit = function() {
self.orderItems();
self.groupItems();
};
// Implement the `order` option. This insertion sort results
// in putting everything otherwise unspecified at the end, as desired.
// Items with the `last: true` option are moved to the end before the
// explicit order is applied.
//
// Called by `afterInit`
self.orderItems = function() {
// Items with a preference to go last go last...
var moving = [];
while (true) {
var moveIndex = _.findIndex(self.items, function(item) {
return item.options.last;
});
if (moveIndex === -1) {
break;
}
moving.push(self.items[moveIndex]);
self.items.splice(moveIndex, 1);
}
self.items = self.items.concat(moving);
// ... But then explicit order kicks in
_.each(self.options.order || [], function(name) {
var item = _.find(self.items, { name: name });
if (item) {
self.items = [ item ].concat(_.filter(self.items, function(item) {
return item.name !== name;
}));
}
});
};
// Marks items that have been grouped via the `groups` option — or via
// `group` calls from modules, combined with the `addGroups` option —
// with a `menuLeader` property and ensures that the items in a group
// are consecutive in the order. We'll figure out the final menus at
// render time so we can handle it properly if an individual
// user only sees one of them, etc. Called by `afterInit`
self.groupItems = function() {
// Implement the groups and addGroups options. Mark the grouped items with a
// `menuLeader` property.
_.each(self.options.groups || self.groups.concat(self.options.addGroups || []), function(group) {
if (group.label) {
self.groupLabels[group.items[0]] = group.label;
group = group.items;
}
_.each(group, function(name, groupIndex) {
var item = _.find(self.items, { name: name });
if (item) {
item.menuLeader = group[0];
} else {
return;
}
// Make sure the submenu items wind up following the leader
// in self.items in the appropriate order
if (name !== item.menuLeader) {
var indexLeader = _.findIndex(self.items, { name: item.menuLeader });
if (indexLeader === -1) {
throw new Error('Admin bar grouping error: no match for ' + item.menuLeader + ' in menu item ' + item.name);
}
var indexMe = _.findIndex(self.items, { name: name });
if (indexMe !== (indexLeader + groupIndex)) {
// Swap ourselves into the right position following our leader
if (indexLeader + groupIndex < indexMe) {
indexMe++;
}
self.items.splice(indexLeader + groupIndex, 0, item);
self.items.splice(indexMe, 1);
}
}
});
});
};
self.pushAssets = function() {
self.pushAsset('script', 'user', { when: 'user' });
options.browser = options.browser || {};
var closeDelay = (self.options.closeDelay || self.options.closeDelay === 0 ? self.apos.launder.integer(self.options.closeDelay) : null);
_.extend(options.browser, {
openOnLoad: (!!(typeof self.options.openOnLoad === 'undefined' || self.options.openOnLoad)),
openOnHomepageLoad: (!!(typeof self.options.openOnHomepageLoad === 'undefined' || self.options.openOnHomepageLoad)),
closeDelay: typeof closeDelay === 'number' ? closeDelay : 3000
});
self.apos.push.browserCall('user', 'apos.create(?, ?)', self.__meta.name, options.browser);
};
// Determine if the specified admin bar item object should
// be rendered or not, for the given req; based on item.permission
// if any. `req.user` is guaranteed to exist at this point.
self.itemIsVisible = function(req, item) {
if (!item.permission) {
// Being logged in is good enough to see this
return true;
}
return self.apos.permissions.can(req, item.permission);
};
}
};