jui.defineUI("ui.property", ['jquery', 'util.base'], function ($, _) {
/**
*
* Property view is a list of properties
*
* ## Property Items Types
*
* * group
* * text
* * checkbox
* * switch
* * select
* * color
* * colors
* * range - native range slider (simple design)
* * number
* * html
* * textarea
* * property - support nested property view
*
* ## Item Attributes
*
* * type - group, text, color , colors, range, checkbox, number , textarea, html
* * title - item's title
* * description
* * key - item's key name
* * value - item's value (array, text, boolean)
*
* @class ui.property
* @extends core
* @alias PropertyView
* @requires jquery
* @requires util.base
*/
var PropertyView = function () {
var $root, $propertyContainer;
var items = [];
var self;
var renderer = {};
function removeJuiComponent (ui) {
var list = jui.getAll();
var i = 0;
for(len = list.length; i < len; i++) {
if (list[i][0] == ui) {
break;
}
}
jui.remove(i);
}
function each(callback) {
for(var i = 0, len = items.length; i < len; i++) {
callback.call(self, items[i], i);
}
}
// refer to underscore.js
function debounce(func, wait, context) {
var timeout;
return function() {
var args = arguments;
var later = function() {
timeout = null;
func.apply(context, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
this.init = function () {
self = this;
$root = $(this.root);
$propertyContainer = $("<div class='property-table' />").css({
'position' : 'relative'
});
$root.append($propertyContainer);
this.loadItems(this.options.items);
}
this.loadItems = function (newItems) {
items = _.clone(newItems);
this.initProperty();
this.emit("load.items");
}
this.initProperty = function () {
// 정렬 방식에 따라 그리는 방법이 다르다.
$propertyContainer.empty();
each(function (item, index) {
$propertyContainer.append(this.renderItem(item, index));
});
}
this.addItem = function (item) {
if (!_.typeCheck('array', item)) {
item = [item];
}
items = items.concat(item);
// 정렬에 따라 렌더링이 달라짐
// add 하면 전체를 새로 그려야겠다.
this.initProperty();
}
// remove item by key or title
this.removeItem = function (item) {
var result = [];
for(var i = 0, len = items.length; i < len; i++) {
var it = items[i];
if (it.key == item.key || it.title == item.title ) {
result.push(it);
}
}
items = result;
}
/**
* @method getGroupList
*
* get a list of group's title.
*/
this.getGroupList = function () {
var result = [];
$propertyContainer.find(".property-header-item").each(function() {
var it = $(this).data('item');
result.push({
name : it.title,
id : $(this).attr('id')
});
});
return result ;
}
/**
* @method collapsed
*
* collapse group's children
*
* @param {String} id
*/
this.collapsed = function (id) {
var $dom = $root.find('#' + id);
$dom.addClass('collapsed').removeClass('expanded');
$dom.find('.expand-btn i').removeClass('icon-minus').addClass('icon-plus');
var $next = $dom.next();
while($next.length && !$next.hasClass('property-header-item')) {
$next.hide();
$next = $next.next();
}
}
/**
* @method expanded
*
* expand group's children
*
* @param {String} id
*/
this.expanded = function (id) {
// 접기
var $dom = $root.find('#' + id);
$dom.removeClass('collapsed').addClass('expanded');
$dom.find('.expand-btn i').removeClass('icon-plus').addClass('icon-minus');
var $next = $dom.next();
while($next.length && !$next.hasClass('property-header-item')) {
$next.show();
$next = $next.next();
}
}
this.renderItem = function (item, index) {
var $dom = $("<div class='property-item' />").attr('data-index', index);
if (item.type == 'group') {
$dom.addClass('property-header-item expanded');
$dom.attr('id', 'property-header-item-' + index);
$dom.data('item', item);
var $name = $("<div class='property-header' />").html(item.title);
if (item.description) {
$name.append("<small class='description'>"+item.description+"</small>");
}
$name.append("<a class='expand-btn'><i class='icon-minus' ></i></a>");
$dom.on('click', function (e) {
if ($(this).hasClass('collapsed')) {
self.expanded($dom.attr('id'));
} else {
self.collapsed($dom.attr('id'));
$}
});
$dom.append($name);
} else {
if (_.typeCheck("array", item.value) || item.vertical)
{
$dom.addClass('vertical');
}
$dom.attr('data-key', item.key);//.hide();
var $name = $("<div class='property-title' />").html(item.title);
var $input = $("<div class='property-render' />");
var $renderedInput = this.render($dom, item) ;
$input.append( $("<div class='item' />").html($renderedInput) );
if (item.description)
{
$input.append("<div class='description' >"+item.description+"</div>");
}
$dom.append($name);
$dom.append($input);
}
return $dom;
}
this.render = function ($dom, item) {
var type = item.type || 'text';
var render = item.render || renderer[type] || renderer.defaultRenderer;
return render($dom, item);
}
/**
* @method getValue
*
* get a list of property's value
*
* @param {String} [key=null] if key is null, value is all properties.
*/
this.getValue = function (key) {
if (key) {
return this.getItem(key).value;
} else {
return this.getAllValue();
}
}
this.getDefaultValue = function () {
var result = {};
for(var i = 0, len = this.options.items; i < len; i++) {
var it = this.options.items[i];
if (typeof it.value != 'undefined') {
result[it.key] = it.value;
}
}
return result;
}
this.initValue = function (obj) {
each(function (item, index) {
item.value = '';
});
this.initProperty();
if (obj) {
this.setValue(obj);
}
}
/**
* @method getValue
*
* set a list of property's value
*
* @param {Object} obj
*/
this.setValue = function (obj) {
obj = obj || {};
if (Object.keys(obj).length) {
for(var key in obj) {
this.updateValue(key, obj[key]);
}
}
}
this.findRender = function (key) {
return this.findItem(key).find(".property-render .item");
}
this.findItem = function (key) {
return $propertyContainer.find("[data-key='"+key+"']");
}
this.getItem = function ($item) {
var item;
if (_.typeCheck("number", $item)) {
item = items[$item];
} else if (_.typeCheck('string', $item)) {
item = items[parseInt(this.findItem($item).attr('data-index'))];
} else {
item = items[parseInt($item.attr('data-index'))];
}
return item;
}
this.updateValue = function (key, value) {
var $item = this.findItem(key);
var it = this.getItem(key);
if (!it) return;
it.value = value;
var $render = this.findRender(key);
$render.empty();
$render.html(this.render($item, it));
}
this.getAllValue = function (key) {
var results = {};
each(function (item, index) {
if (item.type !== 'group') {
results[item.key] = item.value;
}
});
return results;
}
this.refreshValue = function ($dom, newValue) {
var item = this.getItem($dom);
var oldValue = item.value;
item.value = newValue;
this.emit("change", [ item, newValue, oldValue ] );
}
/* Implements Item Renderer */
renderer.str2array = function (value, splitter) {
splitter = splitter || ",";
if (typeof value == 'string') {
return value.split(splitter);
}
return value;
}
renderer.defaultRenderer = function ($dom, item) {
return renderer.text($dom, item);
}
renderer.select = function ($dom, item) {
var $input = $("<select />").css({
'max-width': '100%'
});
var list = item.items || [];
for(var i = 0, len = list.length; i < len; i++) {
var it = list[i];
if (typeof it == 'string') {
it = { text : it, value : it }
}
$input.append($("<option >").val(it.value).text(it.text));
}
$input.val(item.value);
$input.on('change', debounce(function () {
var value = $(this).val();
value = (_.typeCheck('array', item.value)) ? renderer.str2array(value) : value;
self.refreshValue($(this).closest('.property-item'), value);
}, 250, $input));
return $input;
}
renderer.text = function ($dom, item) {
var $text = $("<input type='text' />").css({
width: '100%'
}).attr({
placeholder : 'Type here'
});
if (item.readonly) {
$input.attr('readonly', true);
}
$text.val(item.value);
$text.on('input', debounce(function () {
var value = $(this).val();
value = (_.typeCheck('array', item.value)) ? renderer.str2array(value) : value;
self.refreshValue($dom, value);
}, 250, $text));
return $([$text[0]]);
}
renderer.textarea = function ($dom, item) {
var $input = $("<textarea />").css({
width: '100%',
height: item.height || 100
}).attr({
placeholder : 'Type here'
});
if (item.readonly) {
$input.attr('readonly', true);
}
$input.val(item.value);
$input.on('input', debounce(function () {
var value = $(this).val();
value = (_.typeCheck('array', item.value)) ? renderer.str2array(value) : value;
self.refreshValue($(this).closest('.property-item'), value);
}, 250, $input));
return $input;
}
renderer.html = function ($dom, item) {
var $input = $("<div class='html' contenteditable=true />").css({
width: '100%',
height: item.height || 100
});
if (item.readonly) {
$input.attr('contenteditable', false);
}
$input.html(item.value);
$input.on('input', debounce(function () {
var value = $(this).html();
self.refreshValue($(this).closest('.property-item'), value);
}, 250, $input));
return $input;
}
renderer.number = function ($dom, item) {
var $input = $("<input type='number' />").css({
'text-align' : 'center'
});
$input.attr('max', item.max || 100);
$input.attr('min', item.min || 0);
$input.attr('step', item.step || 1);
$input.val(item.value);
$input.on('input', debounce(function () {
self.refreshValue($(this).closest('.property-item'), +$(this)[0].value);
}, 250, $input));
return $input;
}
renderer.range = function ($dom, item) {
var $group = $("<div />").css({
position: 'relative'
});
var $input = $("<input type='range' />").css({
width: '100px',
'z-index' : 1
});
value = item.value;
var postfix = item.postfix || "";
if (item.postfix)
{
value = value.replace(postfix, "");
}
$input.attr('max', item.max || 100);
$input.attr('min', item.min || 0);
$input.attr('step', item.step || 1);
$input.val(+value);
if (item.readonly) {
$input.attr('readonly', true);
}
var $progress = $("<div class='range-progress' />");
$progress.width((value / (+$input.attr('max') - +$input.attr('min'))) * $input.width());
var $inputText = $("<span />");
$inputText.text(value +postfix);
$input.on('input', function () {
var $el = $(this);
var value = +$el[0].value;
var width = (value / (+$el.attr('max') - +$el.attr('min'))) * $(this).width();
$progress.width(width);
$inputText.text(value + postfix);
});
$input.on('input', debounce(function () {
var $el = $(this);
var value = +$el[0].value;
self.refreshValue($el.closest('.property-item'), value + postfix);
}, 250, $input));
$group.append([ $input, $progress, $inputText ]);
return $group;
}
renderer.checkbox = function ($dom, item) {
var $input = $("<input type='checkbox' /><i ></i>");
$($input[0]).hide();
$input[0].checked = (item.value == 'true' || item.value === true) ? true : false ;
if ($input[0].checked) {
$($input[1]).addClass('icon-checkbox');
} else {
$($input[1]).addClass('icon-checkbox2');
}
$input.on('click', debounce(function () {
var is_checked = $(this).hasClass('icon-checkbox');
if (is_checked) {
$(this).addClass('icon-checkbox2').removeClass('icon-checkbox');
} else {
$(this).addClass('icon-checkbox').removeClass('icon-checkbox2');
}
is_checked = !is_checked;
self.refreshValue($dom, is_checked);
}, 100, $input));
return $input;
}
renderer.switch = function ($dom, item) {
var $input = $("<div class='switch inner small' />");
var is_checked = (item.value == 'true' || item.value === true) ? true : false;
jui.create('ui.switch',$input, {
checked : is_checked,
event: {
change: function(is_on) {
self.refreshValue($dom, is_on);
}
}
});
return $input;
}
renderer.property = function ($dom, item) {
var $input = $("<div class='property inner' />");
var propertyObj = jui.create('ui.property', $input, {
items : item.items,
event : {
change : function () {
self.refreshValue($dom, this.getValue());
}
}
});
propertyObj.setValue(item.value);
return $input;
}
renderer.date = function ($dom, item) {
var $container = $propertyContainer;
var $input = $("<div class='datepicker-input' />");
var $valueText = $("<span class='datepicker-value-text'></span>").css({
cursor: 'pointer'
});
$input.on('click', function () {
var offset = $input.offset();
var containerOffset = $container.offset()
var maxWidth = $container.outerWidth();
var maxHeight = $container.outerHeight();
var left = offset.left - containerOffset.left;
if (left + $input.outerWidth() >= maxWidth)
{
left = maxWidth - $input.outerWidth() - 20;
}
var top = offset.top - containerOffset.top + 80;
if (top + $input.outerHeight() >= maxHeight)
{
top = maxHeight - $input.outerHeight() - 20;
}
$datepicker.css({
position: 'absolute',
'z-index' : 100000,
left: left,
top: top
});
$datepicker.show();
});
var $icon = $("<i class='icon-calendar' />");
$input.html($icon);
$input.append($valueText);
var $datepicker = $("<div class='datepicker' />").css({
position: 'absolute',
top: '0px',
display: 'none'
});
var $datepicker_head = $("<div class='head' />");
var $datepicker_body = $("<table class='body' />");
$datepicker_head.html('<div class="prev"><i class="icon-chevron-left"></i></div><div class="title"></div><div class="next"><i class="icon-chevron-right"></i></div>');
$datepicker_body.html('<tr><th>SU</th><th>MO</th><th>TU</th><th>WE</th><th>TH</th><th>FR</th><th>SA</th></tr>');
$datepicker.append($datepicker_head);
$datepicker.append($datepicker_body);
$container.after($datepicker);
var datepicker = jui.create('ui.datepicker', $datepicker, {
date : item.value || (+new Date),
titleFormat: item.titleFormat || "yyyy. MM",
format: item.format || "yyyy/MM/dd",
tpl : {
date : item.tpl_date || "<td><!= date !></td>"
},
event : {
select: function(date, e) {
$valueText.html(date);
self.refreshValue($dom, date);
}
}
});
$('body').on('click', function (e) {
var $c = $(e.target).closest('.datepicker');
var $c2 = $(e.target).closest($input);
if (!$c.length && !$c2.length) {
$datepicker.hide();
}
});
$valueText.html(datepicker.getDate());
return $input;
}
renderer.colors = function ($dom, item) {
var colors = item.value;
var arr = [];
for(var i = 0, len = colors.length; i < len; i++) {
var $input = renderer.color($dom, item, i);
arr.push($input[0]);
}
return $(arr);
}
renderer.color = function ($dom, item, index) {
index = typeof index == 'undefined' ? -1 : index;
var $input = $("<a class='color-input' />");
var $container = $propertyContainer;
var colorValue = index == -1 ? item.value : item.value[index];
var $colorPanel = $("<span />").css({
'background-color': colorValue,
}).html(' ');
var $colorCode = $("<span />").html(colorValue || '');
var $noneButton = $("<span class='none-color' title='Delete a color'/>").html("<i class='icon-more'></i>");
$input.append($colorPanel);
$input.append($colorCode);
$input.append($noneButton);
$input.on('click', function(e) {
if ($(e.target).closest('.none-color').length) {
e.preventDefault();
$colorPanel.css('background-color', '');
$colorCode.text('');
self.refreshValue($input.closest('.property-item'), '');
return;
}
var offset = $(this).offset();
var $colorPicker = $container.next('.colorpicker');
if (!$colorPicker.length) {
$colorPicker = $('<div class="colorpicker" />');
$container.after($colorPicker);
var colorpicker = jui.create('ui.colorpicker', $colorPicker, {
color: colorValue,
event: {
change: debounce(function() {
var color = colorpicker.getColor('hex');
if (color.indexOf('NAN') > -1)
{
return;
}
$colorPanel.css('background-color', color);
$colorCode.html(color);
if (index == -1) {
self.refreshValue($input.closest('.property-item'), color);
} else {
var colors = item.value;
colors[index] = color;
self.refreshValue($input.closest('.property-item'), colors);
}
}, 100, colorpicker)
}
});
$('body').on('click', function (e) {
var $c = $(e.target).closest('.colorpicker');
var $c2 = $(e.target).closest($input);
if (!$c.length && !$c2.length) {
removeJuiComponent(colorpicker);
$colorPicker.remove();
}
});
} else {
$colorPicker[0].jui.setColor(colorValue || "");
}
var containerOffset = $container.offset()
var maxWidth = $container.outerWidth();
var maxHeight = $container.outerHeight();
var left = offset.left - containerOffset.left;
if (left + $colorPicker.outerWidth() >= maxWidth)
{
left = maxWidth - $colorPicker.outerWidth() - 20;
}
var top = offset.top - containerOffset.top + 50;
if (top + $colorPicker.outerHeight() >= maxHeight)
{
top = maxHeight - $colorPicker.outerHeight() - 20;
}
$colorPicker.css({
position: 'absolute',
'z-index' : 100000,
left: left,
top: top
});
$colorPicker.show();
});
return $input;
}
}
PropertyView.setup = function () {
return {
sort : 'group', // name, group, type
viewport : 'default',
items : []
}
}
/**
* @event change
* Event that occurs when property view is changed
*/
return PropertyView;
});