I've taken a stab at making a context menu using BackboneJS.
Here's the overall structure:
Collections:
- /collection/contextMenuGroups
- /collection/contextMenuItems
Models:
- /model/contextMenu
- /model/contextMenuGroup
- /model/contextMenuItem
Views:
- /view/contextMenuView
To start off, lets take a look at my template:
<script type="text/template" id="contextMenuTemplate">
<% groups.each(function(group){ %>
<ul id="group_<%= group.cid %>">
<% group.get('items').each(function(item){ %>
<li>
<a id="groupItem_<%= item.cid %>" href="#"><%= item.get('text') %></a>
</li>
<% }); %>
</ul>
<% }); %>
</script>
My template is created such that each contextMenuGroup
provided to it creates a new unordered list. Each item in each group creates a link inside of a listItem
for the given unordered list.
Here's my view:
define(['contextMenu'], function (ContextMenu) {
'use strict';
var ContextMenuView = Backbone.View.extend({
className: 'contextMenu',
template: _.template($('#contextMenuTemplate').html()),
parentSelector: 'body',
render: function () {
this.$el.html(this.template(this.model.toJSON()));
// Prevent display outside viewport.
var offsetTop = this.top;
var needsVerticalFlip = offsetTop + this.$el.height() > $(this.parentSelector).height();
if (needsVerticalFlip) {
offsetTop = offsetTop - this.$el.height();
}
var offsetLeft = this.left;
var needsHorizontalFlip = offsetLeft + this.$el.width() > $(this.parentSelector).width();
if (needsHorizontalFlip) {
offsetLeft = offsetLeft - this.$el.width();
}
// Show the element before setting offset to ensure correct positioning.
this.$el.show().offset({
top: offsetTop,
left: offsetLeft
});
return this;
},
initialize: function () {
// TODO: If I implement Backbone View's more properly, then 'body' should be responsible for this, but for now this is fine.
this.$el.appendTo(this.parentSelector);
var self = this;
// Hide the context menu whenever any click occurs not just when selecting an item.
$(this.parentSelector).on('click contextmenu', function () {
self.$el.hide();
});
},
show: function (options) {
if (options.top === undefined || options.left === undefined) throw "ContextMenu must be shown with top/left coordinates.";
if (options.groups === undefined) throw "ContextMenu needs ContextMenuGroups to be shown.";
this.top = options.top;
this.left = options.left;
this.model = new ContextMenu({
groups: options.groups
});
this.render();
}
});
return ContextMenuView;
});
To display a contextMenu
, I call show
and pass to show top/left coordinates as well as a model to render inside of the context menu.
Usage:
showContextMenu: function (event, queueItem) {
var queueContextMenuGroups = [{
position: 0,
items: [{
position: 0,
text: 'Clear Queue'
}]
}];
if (queueItem) {
queueContextMenuGroups.push({
position: 1,
items: [{
position: 0,
text: 'Remove ' + queueItem.get('title')
}]
});
}
this.contextMenuView.show({
top: event.pageY,
left: event.pageX + 1,
groups: queueContextMenuGroups
});
return false;
}
Here, I create an anonymous array of objects (groups) which contains some items to render.
This all works great, but I have no idea how to bind generic events! Should I be extending my ContextMenuView
with a 'specific' implementation which can define its events?