blossom
Version:
Modern, Cross-Platform Application Framework
504 lines (411 loc) • 14.9 kB
JavaScript
// ==========================================================================
// Project: Blossom - Modern, Cross-Platform Application Framework
// Copyright: ©2012 Fohr Motion Picture Studios. All rights reserved.
// License: Licensed under the GPLv3 license (see BLOSSOM-LICENSE).
// ==========================================================================
/*globals sc_assert */
sc_require('widgets/widget');
sc_require('mixins/control');
var base03 = "#002b36";
var base02 = "#073642";
var base01 = "#586e75";
var base00 = "#657b83";
var base0 = "#839496";
var base1 = "#93a1a1";
var base2 = "#eee8d5";
var base3 = "#fdf6e3";
var yellow = "#b58900";
var orange = "#cb4b16";
var red = "#dc322f";
var magenta = "#d33682";
var violet = "#6c71c4";
var blue = "#268bd2";
var cyan = "#2aa198";
var green = "#859900";
var white = "white";
/** @class
A RadioWidget is used to create a group of radio buttons. The user can use
these buttons to pick from a choice of options.
This view renders simulated radio buttons that can display a mixed state and
has other features not found in platform-native controls.
RadioWidget accepts a number of properties, for example:
items: [{ title: "Red",
value: "red",
enabled: true,
icon: "button_red" },
{ title: "Green",
value: "green",
enabled: true,
icon: 'button_green' }],
value: 'red',
itemTitleKey: 'title',
itemValueKey: 'value',
itemIconKey: 'icon',
itemIsEnabledKey: 'enabled',
isEnabled: true,
layoutDirection: SC.LAYOUT_HORIZONTAL
Default layoutDirection is vertical.
Default isEnabled is true.
The value property can be either a string, as above, or an array of strings
for pre-checking multiple values.
The items array can contain either strings, or as in the example above a
hash. When using a hash, make sure to also specify the itemTitleKey
and itemValueKey you are using. Similarly, you will have to provide
itemIconKey if you are using icons radio buttons. The individual items
enabled property is true by default, and the icon is optional.
@extends SC.Widget
@since Blossom 1.0
*/
SC.RadioWidget = SC.Widget.extend(SC.Control, {
/**
The value of the currently selected item, and which will be checked in the
UI. This can be either a string or an array with strings for checking
multiple values.
*/
value: null,
/**
This property indicates how the radio buttons are arranged.
*/
layoutDirection: SC.LAYOUT_VERTICAL,
/**
The items property can be either an array with strings, or a
hash. When using a hash, make sure to also specify the appropriate
itemTitleKey, itemValueKey, itemIsEnabledKey and itemIconKey.
*/
items: [],
/**
If items property is a hash, specify which property will function as
the title with this itemTitleKey property.
*/
itemTitleKey: null,
/**
If items property is a hash, specify which property will function as
the item width with this itemWidthKey property. This is only used when
layoutDirection is set to SC.LAYOUT_HORIONZTAL and can be used to override
the default value provided by the framework or theme CSS.
@property {String}
@default null
*/
itemWidthKey: null,
/**
If items property is a hash, specify which property will function as
the value with this itemValueKey property.
*/
itemValueKey: null,
/**
If items property is a hash, specify which property will function as
the value with this itemIsEnabledKey property.
*/
itemIsEnabledKey: null,
/**
If items property is a hash, specify which property will function as
the value with this itemIconKey property.
*/
itemIconKey: null,
// ..........................................................
// PRIVATE SUPPORT
//
/** @private
Will iterate the items property to return an array with items that is
indexed in the following structure:
[0] => Title (or label)
[1] => Value
[2] => Enabled (true default)
[3] => Icon (image URL)
*/
displayItems: function() {
var items = this.get('items'),
loc = this.get('localize'),
titleKey = this.get('itemTitleKey'),
valueKey = this.get('itemValueKey'),
widthKey = this.get('itemWidthKey'),
isHorizontal = this.get('layoutDirection') === SC.LAYOUT_HORIZONTAL,
isEnabledKey = this.get('itemIsEnabledKey'),
iconKey = this.get('itemIconKey'),
ret = [], max = (items)? items.get('length') : 0,
item, title, width, value, idx, isArray, isEnabled, icon;
for (idx=0;idx<max;idx++) {
item = items.objectAt(idx);
// if item is an array, just use the items...
if (SC.typeOf(item) === SC.T_ARRAY) {
title = item[0];
value = item[1];
// otherwise, possibly use titleKey,etc.
} else if (item) {
// get title. either use titleKey or try to convert the value to a
// string.
if (titleKey) {
title = item.get ? item.get(titleKey) : item[titleKey];
} else title = (item.toString) ? item.toString() : null;
if (widthKey && isHorizontal) {
width = item.get ? item.get(widthKey) : item[widthKey];
}
if (valueKey) {
value = item.get ? item.get(valueKey) : item[valueKey];
} else value = item;
if (isEnabledKey) {
isEnabled = item.get ? item.get(isEnabledKey) : item[isEnabledKey];
} else isEnabled = true;
if (iconKey) {
icon = item.get ? item.get(iconKey) : item[iconKey];
} else icon = null;
// if item is nil, use somedefaults...
} else {
title = value = icon = null;
isEnabled = false;
}
// localize title if needed
if (loc) title = title.loc();
ret.push([title, value, isEnabled, icon, width]);
}
return ret; // done!
}.property('items', 'itemTitleKey', 'itemWidthKey', 'itemValueKey',
'itemIsEnabledKey', 'localize', 'itemIconKey'),
render: function(ctx) {},
// ..........................................................
// EVENT HANDLING
//
/** @private
Remove the active class on mouseExited if mouse is down.
*/
mouseExited: function(evt) {
var active = this._sc_activeRadioButton;
document.body.style.cursor = "default";
if (this.isMouseDown) { this.set('isActive', false); }
if (active) active.set('isActive', false);
return true;
},
/** @private
If mouse was down and we renter the button area, set the active state
again.
*/
mouseEntered: function(evt) {
var active = this._sc_activeRadioButton,
button = evt.layer;
if (button !== this && button.get('isEnabled') && this.get('isEnabled')) {
document.body.style.cursor = "pointer";
}
if (this.isMouseDown) { this.set('isActive', true); }
if (active) active.set('isActive', true);
return true;
},
/**
If the user clicks on of the items mark it as active on mouseDown unless
is disabled.
Save the element that was clicked on so we can remove the active state on
mouseUp.
*/
mouseDown: function(evt) {
var button = evt.layer;
// Nothing to do if we're not enabled.
if (!this.get('isEnabled')) {
return true;
// Nothing to do if a radio button wasn't actually clicked on.
} else if (button === this) {
return false;
// Nothing to do if the radio buttion isn't enabled.
} else if (!button.get('isEnabled')) {
return true;
// Mark the radio button as active.
} else {
this._sc_activeRadioButton = button;
button.set('isActive', true);
// Even if radiobuttons are not set to get firstResponder, allow
// default action, that way textfields loose focus as expected.
evt.allowDefault();
return true;
}
},
/**
If we have a radio element that was clicked on previously, make sure we
remove the active state. Then update the value if the item clicked is
enabled.
*/
mouseUp: function(evt) {
var active = this._sc_activeRadioButton;
if (active) active.set('isActive', false);
this._sc_activeRadioButton = null;
// Nothing to do if we're not enabled.
if (!this.get('isEnabled')) {
return true;
// Nothing to do if there's no active radio button.
} else if (!active) {
return true;
// Okay, we need to deal with the mouseUp event.
} else {
sc_assert(active);
var button = evt.layer;
// Nothing to do if the mouse did not go up over a radio button.
if (button === this) {
return true;
// Nothing to do if the mouse did not go up over the active radio button.
} else if (button !== active) {
return true;
// Okay, the mouse went up over the active radio button. We need to
// update our value with it's value, select it, and unselected the
// currently selected radio button.
} else {
var index = this.get('sublayers').indexOf(active),
item = this.get('displayItems').objectAt(index);
sc_assert(index >= 0);
sc_assert(item);
// Update our 'value' property. _sc_valueDidChange() will handle
// updating the radio button's isSelected values.
this.set('value', item[1]);
return true;
}
}
},
_sc_valueDidChange: function() {
var value = this.get('value'),
displayItems = this.get('displayItems'),
buttons = this.get('sublayers'),
selected;
displayItems.forEach(function(item, idx) {
if (item[1] === value) selected = buttons[idx];
});
if (selected) {
selected.set('isSelected', true);
buttons.without(selected).invoke('setIfChanged', 'isSelected', false);
} else {
buttons.invoke('setIfChanged', 'isSelected', false);
}
}.observes('value'),
/** @private
If the items array itself changes, add/remove observer on item...
*/
_sc_itemsDidChange: function() {
var func = this._sc_itemContentDidChange,
old = this._sc_items,
cur = this.get('items');
if (old === cur) return;
if (old) old.removeObserver('[]', this, func);
this._sc_items = cur;
if (cur) cur.addObserver('[]', this, func);
this._sc_itemContentDidChange();
}.observes('items'),
/**
Invoked whenever the item array or an item in the array is changed.
This method will regenerate the layer's subsurfaces.
*/
_sc_itemContentDidChange: function() {
var displayItems = this.get('displayItems'),
value = this.get('value'),
isEnabled = this.get('isEnabled');
// console.log(displayItems);
var buttons = [];
displayItems.forEach(function(item, idx) {
var button = SC.RadioButtonWidget.create({
layout: { left: 0, right: 0, top: idx*24, height: 24 },
title: item[0],
isEnabled: item[2],
isSelected: item[1] === value? true : false
});
buttons.push(button);
});
this.set('sublayers', buttons);
},
init: function() {
arguments.callee.base.apply(this, arguments);
this._sc_itemsDidChange();
}
});
sc_require('widgets/button');
/** @private */
SC.RadioButtonWidget = SC.ButtonWidget.extend({
behavior: null, // Allows SC.RadioWidget to define our behavior.
buttonBehavior: SC.TOGGLE_BEHAVIOR,
theme: 'radio',
render: function(ctx) {
// console.log('SC.RadioButtonWidget#render()', SC.guidFor(this));
var title = this.get('displayTitle') || "(no title)",
selected = this.get('isSelected'),
disabled = !this.get('isEnabled'),
mixed = (selected === SC.MIXED_STATE),
active = this.get('isActive'),
bounds = this.get('bounds'),
w = bounds.width, h = bounds.h;
selected = (selected && (selected !== SC.MIXED_STATE));
disabled = disabled || !this.getPath('superlayer.isEnabled');
sc_assert(this.get('theme') === 'radio');
ctx.beginPath();
ctx.arc(9, 12, 7, 0, Math.PI*2);
ctx.closePath();
var radgrad = ctx.createRadialGradient(9, 2, 0.5, 9, 14, 7);
radgrad.addColorStop(0, 'rgb(252,188,126)');
radgrad.addColorStop(1, 'rgb(255,102,0)');
if ((disabled && !selected) || (disabled && !active && !selected)) {
ctx.globalAlpha = 0.5;
ctx.fillStyle = radgrad;
ctx.fill();
ctx.globalAlpha = 0.5;
ctx.strokeStyle = white;
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.font = "10pt Helvetica";
ctx.textBaseline = "middle";
ctx.textAlign = "left";
ctx.shadowBlur = 0;
ctx.shadowColor = "rgba(0,0,0,0)";
ctx.fillText(title, 22, bounds.height/2 + 1);
} else if (disabled) {
ctx.globalAlpha = 0.5;
ctx.fillStyle = radgrad;
ctx.fill();
ctx.strokeStyle = white;
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = 'rgba(255,255,255,0.5)';
ctx.font = "10pt Helvetica";
ctx.textBaseline = "middle";
ctx.textAlign = "left";
ctx.shadowBlur = 0;
ctx.shadowColor = "rgba(0,0,0,0)";
ctx.fillText(title, 22, bounds.height/2 + 1);
} else if (active) {
radgrad = ctx.createRadialGradient(9, 2, 0.5, 9, 14, 7);
radgrad.addColorStop(0, 'rgb(252,188,126)');
radgrad.addColorStop(1, 'rgb(255,178,128)');
ctx.fillStyle = radgrad;
ctx.fill();
ctx.strokeStyle = white;
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = white;
ctx.font = "10pt Helvetica";
ctx.textBaseline = "middle";
ctx.textAlign = "left";
ctx.shadowBlur = 0;
ctx.shadowColor = "rgba(0,0,0,0)";
ctx.fillText(title, 22, bounds.height/2 + 1);
} else {
// console.log('rendering normally');
ctx.globalAlpha = 1.0;
ctx.fillStyle = radgrad;
ctx.fill();
ctx.strokeStyle = white;
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = white;
ctx.font = "10pt Helvetica";
ctx.textBaseline = "middle";
ctx.textAlign = "left";
ctx.shadowBlur = 0;
ctx.shadowColor = "rgba(0,0,0,0)";
ctx.fillText(title, 22, bounds.height/2 + 1);
}
if ((selected && !active) || (!selected && active)) {
// Draw the button mark.
ctx.beginPath();
ctx.arc(9, 12, 3, 0, Math.PI*2);
ctx.closePath();
ctx.fillStyle = (active || disabled)? 'rgba(255,255,255,0.7)' : white;
ctx.fill();
ctx.strokeStyle = (active || disabled)? 'rgba(255,255,255,0.7)' : white;
ctx.lineCap = 'round';
ctx.lineWidth = 0.5;
ctx.stroke();
}
}
});