// ==========================================================================
// Project: SproutCore - JavaScript Application Framework
// Copyright: ©2006-2009 Sprout Systems, Inc. and contributors.
// Portions ©2008-2009 Apple Inc. All rights reserved.
// License: Licened under MIT license (see license.js)
// ==========================================================================
sc_require('views/button') ;
sc_require('views/separator') ;
// Constants
SC.BENCHMARK_MENU_ITEM_RENDER = YES ;
/**
@class SC.MenuItemView
@extends SC.ButtonView
@since SproutCore 1.0
*/
SC.MenuItemView = SC.ButtonView.extend( SC.ContentDisplay,
/** @scope SC.MenuItemView.prototype */{
classNames: ['sc-menu-item'],
tagName: 'div',
/**
This provides the parentPane for the current MenuItemView
*/
parentPane: null,
/**
@private
@property
@type {Boolean}
*/
acceptsFirstResponder: YES,
// ..........................................................
// KEY PROPERTIES
//
/**
The content object the menu view will display.
@type Object
*/
content: null,
/**
This returns true if the child view is a menu list view.
This property can be over written to have other child views as well.
@type Boolean
*/
isSubMenuViewVisible: null,
/**
This will return true if the menu item is a separator.
@type Boolean
*/
isSeparator: NO,
/**
(displayDelegate) The name of the property used for label itself
If null, then the content object itself will be used.
@readOnly
@type String
*/
contentValueKey: null,
/**
(displayDelegate) The name of the property used to determine if the menu
item is a branch or leaf (i.e. if the branch arow should be displayed to
the right edge.)
If this is null, then the branch arrow will be collapsed.
@readOnly
@type String
*/
contentIsBranchKey: null,
/**
The name of the property which will set the image for the short cut keys
@readOnly
@type String
*/
shortCutKey: null,
/**
The name of the property which will set the icon image for the menu item.
@readOnly
@type String
*/
contentIconKey: null,
/**
The name of the property which will set the checkbox image for the menu
item.
@readOnly
@type String
*/
contentCheckboxKey: null,
/**
The name of the property which will set the checkbox image for the menu
item.
@readOnly
@type String
*/
contentActionKey: null,
/**
The name of the property which will set the checkbox state
@type Boolean
*/
isCheckboxChecked: NO,
/**
Describes the width of the menu item
Default it to 100
@type Integer
*/
itemWidth: 100,
/**
Describes the height of the menu item
Default it to 20
@type Integer
*/
itemHeight: 20,
/**
Property specifies which menu item the mouseover stops at
@type Boolean
*/
isSelected : NO,
/**
Sub Menu Items
If this is null then there is no branching
@type MenuPane
*/
subMenu: null,
/**
This property specifies whether this menu item is currently in focus
@type Boolean
*/
hasMouseExited: NO,
/**
Anchor for the Parent Menu of which the Menu Item is part of
@type ButtonView/MenuItemView
*/
anchor: null,
/**
This will hold the properties that can trigger a change in the diplay
*/
displayProperties: ['contentValueKey', 'contentIconKey', 'shortCutKey',
'contentIsBranchKey','isCheckboxChecked','itemHeight',
'subMenu','isEnabled','content'],
contentDisplayProperties: 'title value icon separator action checkbox shortcut branchItem subMenu'.w(),
/**
Fills the passed html-array with strings that can be joined to form the
innerHTML of the receiver element. Also populates an array of classNames
to set on the outer element.
@param {SC.RenderContext} context
@param {Boolean} firstTime
@returns {void}
*/
render: function(context, firstTime) {
var bkey ;
if (SC.BENCHMARK_MENU_ITEM_RENDER) {
bkey = '%@.render'.fmt(this) ;
SC.Benchmark.start(bkey) ;
}
var content = this.get('content') ;
var del = this.displayDelegate ;
var key, val ;
var ic ;
var menu = this.parentMenu() ;
var itemWidth = this.get('itemWidth') || menu.layout.width ;
var itemHeight = this.get('itemHeight') || 20 ;
this.set('itemWidth',itemWidth) ;
this.set('itemHeight',itemHeight) ;
if(!this.get('isEnabled')) context.addClass('disabled') ;
//handle separator
ic = context.begin('a').attr('href', 'javascript: ;') ;
key = this.getDelegateProperty('isSeparatorKey', del) ;
val = (key && content) ? (content.get ? content.get(key) : content[key]) : null ;
if (val) {
ic.push("") ;
context.addClass('disabled') ;
} else {
// handle checkbox
key = this.getDelegateProperty('contentCheckboxKey', del) ;
if (key) {
val = content ? (content.get ? content.get(key) : content[key]) : NO ;
if (val) {
ic.begin('div').addClass('checkbox').end() ;
}
}
// handle image -- always invoke
key = this.getDelegateProperty('contentIconKey', del) ;
val = (key && content) ? (content.get ? content.get(key) : content[key]) : null ;
if(val && SC.typeOf(val) !== SC.T_STRING) val = val.toString() ;
if(val) this.renderImage(ic, val) ;
// handle label -- always invoke
key = this.getDelegateProperty('contentValueKey', del) ;
val = (key && content) ? (content.get ? content.get(key) : content[key]) : content ;
if (val && SC.typeOf(val) !== SC.T_STRING) val = val.toString() ;
this.renderLabel(ic, val||'') ;
// handle branch
key = this.getDelegateProperty('contentIsBranchKey', del) ;
val = (key && content) ? (content.get ? content.get(key) : content[key]) : NO ;
if (val) {
this.renderBranch(ic, val) ;
ic.addClass('has-branch') ;
} else { // handle action
key = this.getDelegateProperty('action', del) ;
val = (key && content) ? (content.get ? content.get(key) : content[key]) : null ;
if (val && isNaN(val)) this.set('action', val) ;
key = this.getDelegateProperty('target', del) ;
val = (key && content) ? (content.get ? content.get(key) : content[key]) : null ;
if (val && isNaN(val)) this.set('target', val) ;
// handle short cut keys
if (this.getDelegateProperty('shortCutKey', del)) {
key = this.getDelegateProperty('shortCutKey', del) ;
val = (key && content) ? (content.get ? content.get(key) : content[key]) : null ;
if (val) {
this.renderShortcut(ic, val) ;
ic.addClass('shortcutkey') ;
}
}
}
}
ic.end() ;
if (SC.BENCHMARK_MENU_ITEM_RENDER) SC.Benchmark.end(bkey) ;
},
/**
Generates the image used to represent the image icon. override this to
return your own custom HTML
@param {SC.RenderContext} context the render context
@param {String} the source path of the image
@returns {void}
*/
renderImage: function(context, image) {
// get a class name and url to include if relevant
var url, className ;
if (image && SC.ImageView.valueIsUrl(image)) {
url = image ;
className = '' ;
} else {
className = image ;
url = sc_static('blank.gif') ;
}
// generate the img element...
context.begin('img').addClass('image').addClass(className).attr('src', url).end() ;
},
/**
Generates the label used to represent the menu item. override this to
return your own custom HTML
@param {SC.RenderContext} context the render context
@param {String} menu item name
@returns {void}
*/
renderLabel: function(context, label) {
context.push(""+label+"") ;
},
/**
Generates the string used to represent the branch arrow. override this to
return your own custom HTML
@param {SC.RenderContext} context the render context
@param {Boolean} hasBranch YES if the item has a branch
@returns {void}
*/
renderBranch: function(context, hasBranch) {
var a = '>' ;
var url = sc_static('blank.gif') ;
context.push(''+a+'') ;
},
/**
Generates the string used to represent the short cut keys. override this to
return your own custom HTML
@param {SC.RenderContext} context the render context
@param {String} the shortcut key string to be displayed with menu item name
@returns {void}
*/
renderShortcut: function(context, shortcut) {
context.push('' + shortcut + '') ;
},
/**
This method is used to fetch the Menu Item View to which the
Parent Menu Pane is anchored
to
@param {}
@returns MenuPane
*/
getAnchor: function() {
var anchor = this.get('anchor') ;
if(anchor && anchor.kindOf && anchor.kindOf(SC.MenuItemView)) return anchor ;
return null ;
},
isCurrent: NO,
/**
This method checks if the menu item is a separator.
@param {}
@returns Boolean
*/
isSeparator: function() {
var content = this.get('content') ;
var del = this.displayDelegate ;
var key = this.getDelegateProperty('isSeparatorKey', del) ;
var val = (key && content) ? (content.get ? content.get(key) : content[key]) : null ;
if (val) return YES ;
return NO ;
},
/**
Checks if a menu is a sub menu, during branching.
@param {}
@returns MenuPane
*/
isSubMenuAMenuPane: function() {
var content = this.get('content') ;
var subMenu = content.get('subMenu') ;
if(subMenu && subMenu.kindOf(SC.MenuPane)) return subMenu ;
return NO ;
},
/**
This method will check whether the current Menu Item is still
selected and then create a submenu accordignly.
@param {}
@returns void
*/
branching: function() {
if(this.get('hasMouseExited')) {
this.set('hasMouseExited',NO) ;
return ;
}
this.createSubMenu() ;
},
/**
This method will remove the focus of the current selected menu item.
@param {}
*/
loseFocus: function() {
if(!this.isSubMenuAMenuPane()) {
this.set('hasMouseExited',YES) ;
this.set('isSelected',NO) ;
this.$().removeClass('focus') ;
}
},
/**
This method will create the sub Menu with the current Menu Item as anchor
@param {}
@returns void
*/
createSubMenu: function() {
var subMenu = this.isSubMenuAMenuPane() ;
if(subMenu) {
subMenu.set('anchor', this) ;
subMenu.popup(this,[0,0,0]) ;
var context = SC.RenderContext(this) ;
context = context.begin(subMenu.get('tagName')) ;
subMenu.prepareContext(context, YES) ;
context = context.end() ;
var menuItemViews = subMenu.get('menuItemViews') ;
if(menuItemViews && menuItemViews.length>0) {
subMenu.set('currentSelectedMenuItem',menuItemViews[0]) ;
subMenu.set('keyPane',YES) ;
}
}
},
parentMenu: function() {
return this.get('parentPane') ;
},
//..........................................
//Mouse Events Handling
//..........................................
mouseUp: function(evt) {
if (!this.get('isEnabled')) return YES ;
this.set('hasMouseExited',NO) ;
this.isSelected = YES ;
var key = this.get('contentCheckboxKey') ;
var content = this.get('content') ;
if (key) {
if (content && content.get(key)) {
this.$('.checkbox').setClass('inactive', YES) ;
content.set(key, NO) ;
} else if( content.get(key)!== undefined ) {
this.$('.checkbox').removeClass('inactive') ;
content.set(key, YES) ;
}
}
this._action(evt) ;
var anchor = this.getAnchor() ;
if(anchor) anchor.mouseUp(evt) ;
else {
this.resignFirstResponder() ;
}
this.closeParent() ;
return YES ;
},
/** @private*/
mouseDown: function(evt) {
return YES ;
},
/** @private
This has been over ridden from button view to prevent calling of render
method (When isActive property is changed).
Also based on whether the menu item has a sub Branch we create a sub Menu
@returns Boolean
*/
mouseEntered: function(evt) {
if (!this.get('isEnabled') && !this.isSeparator()) return YES ;
this.isSelected = YES ;
var parentPane = this.parentMenu() ;
if(parentPane) parentPane.set('currentSelectedMenuItem', this) ;
var key = this.get('contentIsBranchKey') ;
if(key) {
var content = this.get('content') ;
var val = (key && content) ? (content.get ? content.get(key) : content[key]) : NO ;
if(val) this.invokeLater(this.branching(),100) ;
}
return YES ;
},
/** @private
Set the focus based on whether the current Menu item is selected or not.
@returns Boolean
*/
mouseExited: function(evt) {
this.loseFocus() ;
var parentMenu = this.parentMenu() ;
if(parentMenu) {
parentMenu.set('previousSelectedMenuItem', this) ;
this.resignFirstResponder() ;
}
return YES ;
},
/** @private
Call the moveUp function on the parent Menu
@returns Boolean
*/
moveUp: function(sender,evt) {
var menu = this.parentMenu() ;
if(menu) {
menu.moveUp(this) ;
}
return YES ;
},
/** @private
Call the moveDown function on the parent Menu
@returns Boolean
*/
moveDown: function(sender,evt) {
var menu = this.parentMenu() ;
if(menu) {
menu.moveDown(this) ;
}
return YES ;
},
/** @private
Call the function to create a branch
@returns Boolean
*/
moveRight: function(sender,evt) {
this.createSubMenu() ;
return YES ;
},
/** @private*/
keyDown: function(evt) {
return this.interpretKeyEvents(evt) ;
},
/** @private*/
keyUp: function(evt) {
return YES ;
},
/** @private*/
cancel: function(evt) {
this.loseFocus() ;
return YES ;
},
/** @private*/
didBecomeFirstResponder: function(responder) {
if (responder !== this) return;
if(!this.isSeparator()) this.$().addClass('focus') ;
},
/** @private*/
willLoseFirstResponder: function(responder) {
if (responder !== this) return;
this.$().removeClass('focus') ;
},
/** @private*/
insertNewline: function(sender, evt) {
this.mouseUp(evt) ;
},
/**
Close the parent Menu and remove the focus of the current Selected
Menu Item
@returns void
*/
closeParent: function() {
this.$().removeClass('focus') ;
var menu = this.parentMenu() ;
if(menu) {
menu.remove() ;
}
},
/** @private*/
clickInside: function(frame, evt) {
return SC.pointInRect({ x: evt.pageX, y: evt.pageY }, frame) ;
}
}) ;