Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit c482d904bd64460ee11f8cf211cc51c59101f8f1 0 parents
@tj tj authored
3  .gitignore
@@ -0,0 +1,3 @@
+node_modules
+test/*.js
+test/*.css
1  .npmignore
@@ -0,0 +1 @@
+test
5 History.md
@@ -0,0 +1,5 @@
+
+0.0.2 / 2012-07-05
+==================
+
+ * fix dialog.effect support
8 Makefile
@@ -0,0 +1,8 @@
+
+test/out.js: menu.js menu.css
+ component build package.json test/out
+
+clean:
+ rm -f test/out.{js,css}
+
+.PHONY: clean
157 Readme.md
@@ -0,0 +1,157 @@
+
+# Menu
+
+ Menu component with structural styling to give you a clean slate.
+
+ ![js menu component](http://f.cl.ly/items/1Z1d3B1j283y3e200g3E/Screen%20Shot%202012-07-31%20at%203.57.10%20PM.png)
+
+## Installation
+
+```
+$ npm install menu-component
+```
+
+## Features
+
+ - events for composition
+ - structural CSS letting you decide on style
+ - fluent API
+ - arrow key navigation
+
+## Events
+
+ - `show` when shown
+ - `hide` when hidden
+ - `remove` (item) when an item is removed
+ - `select` (item) when an item is selected
+ - `*` menu item events are emitted when clicked
+
+## Example
+
+```js
+var Menu = require('menu');
+
+var menu = new Menu;
+
+menu
+.add('Add item')
+.add('Edit item', function(){ console.log('edit'); })
+.add('Remove item', function(){ console.log('remove'); })
+.add('Remove "Add item"', function(){
+ menu.remove('Add item');
+ menu.remove('Remove "Add item"');
+});
+
+menu.on('select', function(item){
+ console.log('selected "%s"', item);
+});
+
+menu.on('Add item', function(){
+ console.log('added an item');
+});
+
+oncontextmenu = function(e){
+ e.preventDefault();
+ menu.moveTo(e.pageX, e.pageY);
+ menu.show();
+};
+```
+
+## API
+
+### Menu()
+
+ Create a new `Menu`:
+
+```js
+var Menu = require('menu');
+var menu = new Menu();
+var menu = Menu();
+```
+
+### Menu#add([slug], text, [fn])
+
+ Add a new menu item with the given `text`, optional `slug` and callback `fn`.
+
+ Using events to handle selection:
+
+```js
+menu.add('Hello');
+
+menu.on('Hello', function(){
+ console.log('clicked hello');
+});
+```
+
+ Using callbacks:
+
+```js
+menu.add('Hello', function(){
+ console.log('clicked hello');
+});
+```
+
+ Using a custom slug, otherwise "hello" is generated
+ from the `text` given, which may conflict with "rich"
+ styling like icons within menu items, or i18n.
+
+```js
+menu.add('add-item', 'Add Item');
+
+menu.on('add-item', function(){
+ console.log('clicked "Add Item"');
+});
+
+menu.add('add-item', 'Add Item', function(){
+ console.log('clicked "Add Item"');
+});
+```
+
+### Menu#remove(slug)
+
+ Remove an item by the given `slug`:
+
+```js
+menu.add('Add item');
+menu.remove('Add item');
+```
+
+ Or with custom slugs:
+
+```js
+menu.add('add-item', 'Add item');
+menu.remove('add-item');
+```
+
+### Menu#has(slug)
+
+ Check if a menu item is present.
+
+```js
+menu.add('Add item');
+
+menu.has('Add item');
+// => true
+
+menu.has('add-item');
+// => true
+
+menu.has('Foo');
+// => false
+```
+
+### Menu#moveTo(x, y)
+
+ Move the menu to `(x, y)`.
+
+### Menu#show()
+
+ Show the menu.
+
+### Menu#hide()
+
+ Hide the menu.
+
+## License
+
+ MIT
32 menu.css
@@ -0,0 +1,32 @@
+.menu {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 100;
+ margin: 0;
+ padding: 0;
+ background: white;
+ border: 1px solid rgba(0,0,0,0.2);
+}
+
+.menu li {
+ list-style: none;
+}
+
+.menu li a {
+ display: block;
+ padding: 5px 30px 5px 12px;
+ text-decoration: none;
+ border-top: 1px solid #eee;
+ color: #2e2e2e;
+ outline: none;
+}
+
+.menu li:first-child a {
+ border-top: none;
+}
+
+.menu li a:hover,
+.menu li.selected a {
+ background: #f1faff;
+}
249 menu.js
@@ -0,0 +1,249 @@
+
+/**
+ * Module dependencies.
+ */
+
+var Emitter = require('emitter')
+ , $ = require('jquery');
+
+/**
+ * Expose `Menu`.
+ */
+
+module.exports = Menu;
+
+/**
+ * Initialize a new `Menu`.
+ *
+ * Emits:
+ *
+ * - "show" when shown
+ * - "hide" when hidden
+ * - "remove" with the item name when an item is removed
+ * - "select" (item) when an item is selected
+ * - * menu item events are emitted when clicked
+ *
+ * @api public
+ */
+
+function Menu() {
+ if (!(this instanceof Menu)) return new Menu;
+ Emitter.call(this);
+ this.items = {};
+ this.el = $('<ul class=menu>').hide().appendTo('body');
+ this.el.hover(this.deselect.bind(this));
+ $('html').click(this.hide.bind(this));
+ this.on('show', this.bindKeyboardEvents.bind(this));
+ this.on('hide', this.unbindKeyboardEvents.bind(this));
+};
+
+/**
+ * Inherit from `Emitter.prototype`.
+ */
+
+Menu.prototype = new Emitter;
+
+/**
+ * Deselect selected menu items.
+ *
+ * @api private
+ */
+
+Menu.prototype.deselect = function(){
+ this.el.find('.selected').removeClass('selected');
+};
+
+/**
+ * Bind keyboard events.
+ *
+ * @api private
+ */
+
+Menu.prototype.bindKeyboardEvents = function(){
+ $(document).bind('keydown.menu', this.onkeydown.bind(this));
+ return this;
+};
+
+/**
+ * Unbind keyboard events.
+ *
+ * @api private
+ */
+
+Menu.prototype.unbindKeyboardEvents = function(){
+ $(document).unbind('keydown.menu');
+ return this;
+};
+
+/**
+ * Handle keydown events.
+ *
+ * @api private
+ */
+
+Menu.prototype.onkeydown = function(e){
+ switch (e.keyCode) {
+ // up
+ case 38:
+ e.preventDefault();
+ this.move('prev');
+ break;
+ // down
+ case 40:
+ e.preventDefault();
+ this.move('next');
+ break;
+ }
+};
+
+/**
+ * Focus on the next menu item in `direction`.
+ *
+ * @param {String} direction "prev" or "next"
+ * @api public
+ */
+
+Menu.prototype.move = function(direction){
+ var prev = this.el.find('.selected').eq(0);
+
+ var next = prev.length
+ ? prev[direction]()
+ : this.el.find('li:first-child');
+
+ if (next.length) {
+ prev.removeClass('selected');
+ next.addClass('selected');
+ next.find('a').focus();
+ }
+};
+
+/**
+ * Add menu item with the given `text` and optional callback `fn`.
+ *
+ * When the item is clicked `fn()` will be invoked
+ * and the `Menu` is immediately closed. When clicked
+ * an event of the name `text` is emitted regardless of
+ * the callback function being present.
+ *
+ * @param {String} text
+ * @param {Function} fn
+ * @return {Menu}
+ * @api public
+ */
+
+Menu.prototype.add = function(text, fn){
+ var slug;
+
+ // slug, text, [fn]
+ if ('string' == typeof fn) {
+ slug = text;
+ text = fn;
+ fn = arguments[2];
+ } else {
+ slug = createSlug(text);
+ }
+
+ var self = this
+ , el = $('<li><a href="#">' + text + '</a></li>')
+ .addClass(slug)
+ .appendTo(this.el)
+ .click(function(e){
+ e.preventDefault();
+ e.stopPropagation();
+ self.hide();
+ self.emit('select', slug);
+ self.emit(slug);
+ fn && fn();
+ });
+
+ this.items[slug] = el;
+ return this;
+};
+
+/**
+ * Remove menu item with the given `slug`.
+ *
+ * @param {String} slug
+ * @return {Menu}
+ * @api public
+ */
+
+Menu.prototype.remove = function(slug){
+ var item = this.items[slug] || this.items[createSlug(slug)];
+ if (!item) throw new Error('no menu item named "' + slug + '"');
+ this.emit('remove', slug);
+ item.remove();
+ delete this.items[slug];
+ delete this.items[createSlug(slug)];
+ return this;
+};
+
+/**
+ * Check if this menu has an item with the given `slug`.
+ *
+ * @param {String} slug
+ * @return {Boolean}
+ * @api public
+ */
+
+Menu.prototype.has = function(slug){
+ return !! (this.items[slug] || this.items[createSlug(slug)]);
+};
+
+/**
+ * Move context menu to `(x, y)`.
+ *
+ * @param {Number} x
+ * @param {Number} y
+ * @return {Menu}
+ * @api public
+ */
+
+Menu.prototype.moveTo = function(x, y){
+ this.el.css({
+ top: y,
+ left: x
+ });
+ return this;
+};
+
+/**
+ * Show the menu.
+ *
+ * @return {Menu}
+ * @api public
+ */
+
+Menu.prototype.show = function(){
+ this.emit('show');
+ this.el.show();
+ return this;
+};
+
+/**
+ * Hide the menu.
+ *
+ * @return {Menu}
+ * @api public
+ */
+
+Menu.prototype.hide = function(){
+ this.emit('hide');
+ this.el.hide();
+ return this;
+};
+
+/**
+ * Generate a slug from `str`.
+ *
+ * @param {String} str
+ * @return {String}
+ * @api private
+ */
+
+function createSlug(str) {
+ return str
+ .toLowerCase()
+ .replace(/ +/g, '-')
+ .replace(/[^a-z0-9-]/g, '');
+}
16 package.json
@@ -0,0 +1,16 @@
+{
+ "name": "menu-component",
+ "description": "Menu component",
+ "version": "0.0.1",
+ "keywords": ["menu", "component"],
+ "dependencies": {
+ "emitter-component": "*",
+ "jquery-component": "*"
+ },
+ "component": {
+ "styles": ["menu.css"],
+ "scripts": {
+ "menu": "menu.js"
+ }
+ }
+}
40 test/index.html
@@ -0,0 +1,40 @@
+<!DOCTYPE 5>
+<html>
+ <head>
+ <title>Menu</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <link rel="stylesheet" href="out.css" />
+ </head>
+ <body>
+ <title>Menu</title>
+ <script src="out.js"></script>
+ <script>
+ var menu = require('menu');
+
+ menu = menu();
+
+ menu
+ .add('add-item', 'Add <em>item</em>')
+ .add('Edit item', function(){ console.log('edit'); })
+ .add('Remove item', function(){ console.log('remove'); })
+ .add('Remove "Add item"', function(){
+ menu.remove('add-item');
+ menu.remove('Remove "Add item"');
+ });
+
+ menu.on('select', function(item){
+ console.log('selected "%s"', item);
+ });
+
+ menu.on('Add item', function(){
+ console.log('added an item');
+ });
+
+ oncontextmenu = function(e){
+ e.preventDefault();
+ menu.moveTo(e.pageX, e.pageY);
+ menu.show();
+ };
+ </script>
+ </body>
+</html>
Please sign in to comment.
Something went wrong with that request. Please try again.