First commit

This commit is contained in:
Theodotos Andreou 2018-01-14 13:10:16 +00:00
commit c6e2478c40
13918 changed files with 2303184 additions and 0 deletions

View file

@ -0,0 +1,239 @@
/**
* Bootstrap Modal wrapper for use with Backbone.
*
* Takes care of instantiation, manages multiple modals,
* adds several options and removes the element from the DOM when closed
*
* @author Charles Davison <charlie@powmedia.co.uk>
*
* Events:
* shown: Fired when the modal has finished animating in
* hidden: Fired when the modal has finished animating out
* cancel: The user dismissed the modal
* ok: The user clicked OK
*/
(function($, _, Backbone) {
//Set custom template settings
var _interpolateBackup = _.templateSettings;
_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g,
evaluate: /<%([\s\S]+?)%>/g
}
var template = _.template('\
<% if (title) { %>\
<div class="modal-header">\
<% if (allowCancel) { %>\
<a class="close">×</a>\
<% } %>\
<h3>{{title}}</h3>\
</div>\
<% } %>\
<div class="modal-body">{{content}}</div>\
<div class="modal-footer">\
<% if (allowCancel) { %>\
<% if (cancelText) { %>\
<a href="#" class="btn cancel">{{cancelText}}</a>\
<% } %>\
<% } %>\
<a href="#" class="btn ok btn-primary">{{okText}}</a>\
</div>\
');
//Reset to users' template settings
_.templateSettings = _interpolateBackup;
var Modal = Backbone.View.extend({
className: 'modal',
events: {
'click .close': function(event) {
event.preventDefault();
this.trigger('cancel');
},
'click .cancel': function(event) {
event.preventDefault();
this.trigger('cancel');
},
'click .ok': function(event) {
event.preventDefault();
this.trigger('ok');
this.close();
}
},
/**
* Creates an instance of a Bootstrap Modal
*
* @see http://twitter.github.com/bootstrap/javascript.html#modals
*
* @param {Object} options
* @param {String|View} [options.content] Modal content. Default: none
* @param {String} [options.title] Title. Default: none
* @param {String} [options.okText] Text for the OK button. Default: 'OK'
* @param {String} [options.cancelText] Text for the cancel button. Default: 'Cancel'. If passed a falsey value, the button will be removed
* @param {Boolean} [options.allowCancel Whether the modal can be closed, other than by pressing OK. Default: true
* @param {Boolean} [options.escape] Whether the 'esc' key can dismiss the modal. Default: true, but false if options.cancellable is true
* @param {Boolean} [options.animate] Whether to animate in/out. Default: false
* @param {Function} [options.template] Compiled underscore template to override the default one
*/
initialize: function(options) {
this.options = _.extend({
title: null,
okText: 'OK',
cancelText: 'Cancel',
allowCancel: true,
escape: true,
animate: false,
template: template
}, options);
},
/**
* Creates the DOM element
*
* @api private
*/
render: function() {
var $el = this.$el,
options = this.options,
content = options.content;
//Create the modal container
$el.html(options.template(options));
var $content = this.$content = $el.find('.modal-body')
//Insert the main content if it's a view
if (content.$el) {
$el.find('.modal-body').html(content.render().$el);
}
if (options.animate) $el.addClass('fade');
this.isRendered = true;
return this;
},
/**
* Renders and shows the modal
*
* @param {Function} [cb] Optional callback that runs only when OK is pressed.
*/
open: function(cb) {
if (!this.isRendered) this.render();
var self = this,
$el = this.$el;
//Create it
$el.modal({
keyboard: this.options.allowCancel,
backdrop: this.options.allowCancel ? true : 'static'
});
//Focus OK button
$el.one('shown', function() {
$el.find('.btn.ok').focus();
self.trigger('shown');
});
//Adjust the modal and backdrop z-index; for dealing with multiple modals
var numModals = Modal.count,
$backdrop = $('.modal-backdrop:eq('+numModals+')'),
backdropIndex = $backdrop.css('z-index'),
elIndex = $backdrop.css('z-index');
$backdrop.css('z-index', backdropIndex + numModals);
this.$el.css('z-index', elIndex + numModals);
if (this.options.allowCancel) {
$backdrop.one('click', function() {
self.trigger('cancel');
});
$(document).one('keyup.dismiss.modal', function (e) {
e.which == 27 && self.trigger('cancel');
});
}
this.on('cancel', function() {
self.close();
});
Modal.count++;
//Run callback on OK if provided
if (cb) {
self.on('ok', cb);
}
return this;
},
/**
* Closes the modal
*/
close: function() {
var self = this,
$el = this.$el;
//Check if the modal should stay open
if (this._preventClose) {
this._preventClose = false;
return;
}
$el.modal('hide');
$el.one('hidden', function() {
self.remove();
self.trigger('hidden');
});
Modal.count--;
},
/**
* Stop the modal from closing.
* Can be called from within a 'close' or 'ok' event listener.
*/
preventClose: function() {
this._preventClose = true;
}
}, {
//STATICS
//The number of modals on display
count: 0
});
//EXPORTS
//CommonJS
if (typeof require == 'function' && typeof module !== 'undefined' && exports) {
module.exports = Modal;
}
//AMD / RequireJS
if (typeof define === 'function' && define.amd) {
return define(function() {
Backbone.BootstrapModal = Modal;
})
}
//Regular; add to Backbone.Bootstrap.Modal
else {
Backbone.BootstrapModal = Modal;
}
})(jQuery, _, Backbone);

View file

@ -0,0 +1 @@
(function(e,t,n){var r=t.templateSettings;t.templateSettings={interpolate:/\{\{(.+?)\}\}/g,evaluate:/<%([\s\S]+?)%>/g};var i=t.template(' <% if (title) { %> <div class="modal-header"> <% if (allowCancel) { %> <a class="close">×</a> <% } %> <h3>{{title}}</h3> </div> <% } %> <div class="modal-body">{{content}}</div> <div class="modal-footer"> <% if (allowCancel) { %> <% if (cancelText) { %> <a href="#" class="btn cancel">{{cancelText}}</a> <% } %> <% } %> <a href="#" class="btn ok btn-primary">{{okText}}</a> </div> ');t.templateSettings=r;var s=n.View.extend({className:"modal",events:{"click .close":function(e){e.preventDefault(),this.trigger("cancel")},"click .cancel":function(e){e.preventDefault(),this.trigger("cancel")},"click .ok":function(e){e.preventDefault(),this.trigger("ok"),this.close()}},initialize:function(e){this.options=t.extend({title:null,okText:"OK",cancelText:"Cancel",allowCancel:!0,escape:!0,animate:!1,template:i},e)},render:function(){var e=this.$el,t=this.options,n=t.content;e.html(t.template(t));var r=this.$content=e.find(".modal-body");return n.$el&&e.find(".modal-body").html(n.render().$el),t.animate&&e.addClass("fade"),this.isRendered=!0,this},open:function(t){this.isRendered||this.render();var n=this,r=this.$el;r.modal({keyboard:this.options.allowCancel,backdrop:this.options.allowCancel?!0:"static"}),r.one("shown",function(){r.find(".btn.ok").focus(),n.trigger("shown")});var i=s.count,o=e(".modal-backdrop:eq("+i+")"),u=o.css("z-index"),a=o.css("z-index");return o.css("z-index",u+i),this.$el.css("z-index",a+i),this.options.allowCancel&&(o.one("click",function(){n.trigger("cancel")}),e(document).one("keyup.dismiss.modal",function(e){e.which==27&&n.trigger("cancel")})),this.on("cancel",function(){n.close()}),s.count++,t&&n.on("ok",t),this},close:function(){var e=this,t=this.$el;if(this._preventClose){this._preventClose=!1;return}t.modal("hide"),t.one("hidden",function(){e.remove(),e.trigger("hidden")}),s.count--},preventClose:function(){this._preventClose=!0}},{count:0});typeof require=="function"&&typeof module!="undefined"&&exports&&(module.exports=s);if(typeof define=="function"&&define.amd)return define(function(){n.BootstrapModal=s});n.BootstrapModal=s})(jQuery,_,Backbone)

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,65 @@
.bbf-jui-date input, .bbf-jui-datetime input {
width: 100px;
text-align: center;
}
.bbf-jui-datetime input {
margin-right: 1em;
}
.bbf-jui-datetime select {
width: 50px;
text-align: center;
}
.bbf-jui-list {
list-style-type: none;
}
.bbf-jui-list ul {
border: 1px solid #ccc;
border-bottom: none;
max-height: 150px;
overflow: auto;
margin: 0;
padding: 0;
}
.bbf-jui-list li {
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
height: 16px;
background: #fff;
padding: 4px;
margin: 0;
list-style-type: none;
margin-top: -1px;
position: relative;
}
.bbf-jui-list .bbf-list-sortable li {
cursor: move;
}
.bbf-jui-list .bbf-list-actions {
position: absolute;
top: 2px;
right: 2px;
}
.bbf-jui-list .bbf-list-actions button {
width: 19px;
height: 19px;
}
.bbf-jui-list .bbf-list-add {
width: 100%;
height: 20px;
margin-top: -2px;
}
.bbf-jui-list .bbf-list-editor {
width: 98%;
}

View file

@ -0,0 +1,500 @@
;(function() {
var Form = Backbone.Form,
Base = Form.editors.Base,
createTemplate = Form.helpers.createTemplate,
triggerCancellableEvent = Form.helpers.triggerCancellableEvent,
exports = {};
/**
* Additional editors that depend on jQuery UI
*/
//DATE
exports['jqueryui.Date'] = Base.extend({
className: 'bbf-jui-date',
initialize: function(options) {
Base.prototype.initialize.call(this, options);
//Cast to Date
if (this.value && !_.isDate(this.value)) {
this.value = new Date(this.value);
}
//Set default date
if (!this.value) {
var date = new Date();
date.setSeconds(0);
date.setMilliseconds(0);
this.value = date;
}
},
render: function() {
var $el = this.$el;
$el.html('<input>');
var input = $('input', $el);
input.datepicker({
dateFormat: 'dd/mm/yy',
showButtonPanel: true
});
this._observeDatepickerEvents();
//Make sure setValue of this object is called, not of any objects extending it (e.g. DateTime)
exports['jqueryui.Date'].prototype.setValue.call(this, this.value);
return this;
},
/**
* @return {Date} Selected date
*/
getValue: function() {
var input = $('input', this.el),
date = input.datepicker('getDate');
return date;
},
setValue: function(value) {
$('input', this.el).datepicker('setDate', value);
},
focus: function() {
if (this.hasFocus) return;
this.$('input').datepicker('show');
},
blur: function() {
if (!this.hasFocus) return;
this.$('input').datepicker('hide');
},
_observeDatepickerEvents: function() {
var self = this;
this.$('input').datepicker('option', 'onSelect', function() {
self.trigger('change', self);
});
this.$('input').datepicker('option', 'onClose', function() {
if (!self.hasFocus) return;
self.trigger('blur', self);
});
this.$('input').datepicker('option', 'beforeShow', function() {
if (self.hasFocus) return {};
self.trigger('focus', self);
return {};
});
}
});
//DATETIME
exports['jqueryui.DateTime'] = exports['jqueryui.Date'].extend({
className: 'bbf-jui-datetime',
template: createTemplate('<select>{{hours}}</select> : <select>{{mins}}</select>'),
render: function() {
function pad(n) {
return n < 10 ? '0' + n : n;
}
//Render the date element first
exports['jqueryui.Date'].prototype.render.call(this);
//Setup hour options
var hours = _.range(0, 24),
hoursOptions = [];
_.each(hours, function(hour) {
hoursOptions.push('<option value="'+hour+'">' + pad(hour) + '</option>');
});
//Setup minute options
var minsInterval = this.schema.minsInterval || 15,
mins = _.range(0, 60, minsInterval),
minsOptions = [];
_.each(mins, function(min) {
minsOptions.push('<option value="'+min+'">' + pad(min) + '</option>');
});
//Render time selects
this.$el.append(this.template({
hours: hoursOptions.join(),
mins: minsOptions.join()
}));
this._observeDatepickerEvents();
//Store references to selects
this.$hours = $('select:eq(0)', this.el);
this.$mins = $('select:eq(1)', this.el);
//Set time
this.setValue(this.value);
return this;
},
/**
* @return {Date} Selected datetime
*/
getValue: function() {
var input = $('input', this.el),
date = input.datepicker('getDate');
date.setHours(this.$hours.val());
date.setMinutes(this.$mins.val());
date.setMilliseconds(0);
return date;
},
setValue: function(date) {
exports['jqueryui.Date'].prototype.setValue.call(this, date);
this.$hours.val(date.getHours());
this.$mins.val(date.getMinutes());
}
});
//LIST
exports['jqueryui.List'] = Base.extend({
className: 'bbf-jui-list',
//Note: The extra div around the <ul> is used to limit the drag area
template: createTemplate('\
<ul></ul>\
<div><button class="bbf-list-add">Add</div>\
'),
itemTemplate: createTemplate('\
<li rel="{{id}}">\
<span class="bbf-list-text">{{text}}</span>\
<div class="bbf-list-actions">\
<button class="bbf-list-edit">Edit</button>\
<button class="bbf-list-del">Delete</button>\
</div>\
</li>\
'),
editorTemplate: createTemplate('\
<div class="bbf-field">\
<div class="bbf-list-editor"></div>\
</div>\
'),
events: {
'click .bbf-list-add': 'addNewItem',
'click .bbf-list-edit': 'editItem',
'click .bbf-list-del': 'deleteItem'
},
initialize: function(options) {
Base.prototype.initialize.call(this, options);
if (!this.schema) throw "Missing required option 'schema'";
this.schema.listType = this.schema.listType || 'Text';
if (this.schema.listType === 'NestedModel' && !this.schema.model)
throw "Missing required option 'schema.model'";
},
render: function() {
var $el = this.$el;
//Main element
$el.html(this.template());
//Create list
var self = this,
data = this.value || [],
schema = this.schema,
itemToString = this.itemToString,
itemTemplate = this.itemTemplate,
listEl = $('ul', $el);
_.each(data, function(itemData) {
var text = itemToString.call(self, itemData);
//Create DOM element
var li = $(itemTemplate({
id: itemData.id || '',
text: text
}));
//Attach data
$.data(li[0], 'data', itemData);
listEl.append(li);
});
//Make sortable
if (schema.sortable !== false) {
listEl.sortable({
axis: 'y',
cursor: 'move',
containment: 'parent'
});
$el.addClass('bbf-list-sortable');
}
//jQuery UI buttonize
$('button.bbf-list-add', $el).button({
text: false,
icons: { primary: 'ui-icon-plus' }
});
$('button.bbf-list-edit', $el).button({
text: false,
icons: { primary: 'ui-icon-pencil' }
});
$('button.bbf-list-del', $el).button({
text: false,
icons: { primary: 'ui-icon-trash' }
});
if (this.hasFocus) this.trigger('blur', this);
return this;
},
/**
* Formats an item for display in the list
* For example objects, dates etc. can have a custom
* itemToString method which says how it should be formatted.
*/
itemToString: function(data) {
if (!data) return data;
var schema = this.schema;
//If there's a specified toString use that
if (schema.itemToString) return schema.itemToString(data);
//Otherwise check if it's NestedModel with it's own toString() method
if (this.schema.listType === 'NestedModel') {
var model = new (this.schema.model)(data);
return model.toString();
}
//Last resort, just return the data as is
return data;
},
/**
* Add a new item to the list if it is completed in the editor
*/
addNewItem: function(event) {
if (event) event.preventDefault();
var self = this;
this.openEditor(null, function(value, editor) {
//Fire 'addItem' cancellable event
triggerCancellableEvent(self, 'addItem', [value, editor], function() {
var text = self.itemToString(value);
//Create DOM element
var li = $(self.itemTemplate({
id: value.id || '',
text: text
}));
//Store data
$.data(li[0], 'data', value);
$('ul', self.el).append(li);
//jQuery UI buttonize
$('button.bbf-list-edit', this.el).button({
text: false,
icons: { primary: 'ui-icon-pencil' }
});
$('button.bbf-list-del', this.el).button({
text: false,
icons: { primary: 'ui-icon-trash' }
});
self.trigger('add', self, value);
self.trigger('item:change', self, editor);
self.trigger('change', self);
});
});
},
/**
* Edit an existing item in the list
*/
editItem: function(event) {
event.preventDefault();
var self = this,
li = $(event.target).closest('li'),
originalValue = $.data(li[0], 'data');
this.openEditor(originalValue, function(newValue, editor) {
//Fire 'editItem' cancellable event
triggerCancellableEvent(self, 'editItem', [newValue, editor], function() {
//Update display
$('.bbf-list-text', li).html(self.itemToString(newValue));
//Store data
$.data(li[0], 'data', newValue);
self.trigger('item:change', self, editor);
self.trigger('change', self);
});
});
},
deleteItem: function(event) {
event.preventDefault();
var self = this,
li = $(event.target).closest('li'),
data = $.data(li[0], 'data');
var confirmDelete = (this.schema.confirmDelete) ? this.schema.confirmDelete : false,
confirmMsg = this.schema.confirmDeleteMsg || 'Are you sure?';
function remove() {
triggerCancellableEvent(self, 'removeItem', [data], function() {
li.remove();
self.trigger('remove', self, data);
self.trigger('change', self);
});
}
if (this.schema.confirmDelete) {
if (confirm(confirmMsg)) remove();
} else {
remove();
}
},
/**
* Opens the sub editor dialog
* @param {Mixed} Data (if editing existing list item, null otherwise)
* @param {Function} Save callback. receives: value
*/
openEditor: function(data, callback) {
var self = this,
schema = this.schema,
listType = schema.listType || 'Text';
var editor = Form.helpers.createEditor(listType, {
key: '',
schema: schema,
value: data
}).render();
var container = this.editorContainer = $(this.editorTemplate());
$('.bbf-list-editor', container).html(editor.el);
var saveAndClose = function() {
var errs = editor.validate();
if (errs) return;
callback(editor.getValue(), editor);
container.dialog('close');
};
var handleEnterPressed = function(event) {
if (event.keyCode !== 13) return;
saveAndClose();
};
$(container).dialog({
resizable: false,
modal: true,
width: 500,
title: data ? 'Edit item' : 'New item',
buttons: {
'OK': saveAndClose,
'Cancel': function() {
container.dialog('close');
}
},
close: function() {
self.editorContainer = null;
$(document).unbind('keydown', handleEnterPressed);
editor.remove();
container.remove();
self.trigger('item:close', self, editor);
self.trigger('item:blur', self, editor);
self.trigger('blur', self);
}
});
this.trigger('item:open', this, editor);
this.trigger('item:focus', this, editor);
this.trigger('focus', this);
//Save and close dialog on Enter keypress
$(document).bind('keydown', handleEnterPressed);
},
getValue: function() {
var data = [];
$('li', this.el).each(function(index, li) {
data.push($.data(li, 'data'));
});
return data;
},
setValue: function(value) {
this.value = value;
this.render();
},
focus: function() {
if (this.hasFocus) return;
var item = this.$('li .bbf-list-edit').first();
if (item.length > 0) {
item.click();
}
else {
this.addNewItem();
}
},
blur: function() {
if (!this.hasFocus) return;
if (this.editorContainer) this.editorContainer.dialog('close');
}
});
//Exports
_.extend(Form.editors, exports);
})();

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,579 @@
;(function() {
var Form = Backbone.Form,
editors = Form.editors;
/**
* LIST
*
* An array editor. Creates a list of other editor items.
*
* Special options:
* @param {String} [options.schema.itemType] The editor type for each item in the list. Default: 'Text'
* @param {String} [options.schema.confirmDelete] Text to display in a delete confirmation dialog. If falsey, will not ask for confirmation.
*/
editors.List = editors.Base.extend({
events: {
'click [data-action="add"]': function(event) {
event.preventDefault();
this.addItem(null, true);
}
},
initialize: function(options) {
editors.Base.prototype.initialize.call(this, options);
var schema = this.schema;
if (!schema) throw "Missing required option 'schema'";
//List schema defaults
this.schema = _.extend({
listTemplate: 'list',
listItemTemplate: 'listItem'
}, schema);
//Determine the editor to use
this.Editor = (function() {
var type = schema.itemType;
//Default to Text
if (!type) return editors.Text;
//Use List-specific version if available
if (editors.List[type]) return editors.List[type];
//Or whichever was passed
return editors[type];
})();
this.items = [];
},
render: function() {
var self = this,
value = this.value || [];
//Create main element
var $el = $(Form.templates[this.schema.listTemplate]({
items: '<b class="bbf-tmp"></b>'
}));
//Store a reference to the list (item container)
this.$list = $el.find('.bbf-tmp').parent().empty();
//Add existing items
if (value.length) {
_.each(value, function(itemValue) {
self.addItem(itemValue);
});
}
//If no existing items create an empty one, unless the editor specifies otherwise
else {
if (!this.Editor.isAsync) this.addItem();
}
this.setElement($el);
this.$el.attr('id', this.id);
this.$el.attr('name', this.key);
if (this.hasFocus) this.trigger('blur', this);
return this;
},
/**
* Add a new item to the list
* @param {Mixed} [value] Value for the new item editor
* @param {Boolean} [userInitiated] If the item was added by the user clicking 'add'
*/
addItem: function(value, userInitiated) {
var self = this;
//Create the item
var item = new editors.List.Item({
list: this,
schema: this.schema,
value: value,
Editor: this.Editor,
key: this.key
}).render();
var _addItem = function() {
self.items.push(item);
self.$list.append(item.el);
item.editor.on('all', function(event) {
if (event === 'change') return;
// args = ["key:change", itemEditor, fieldEditor]
var args = _.toArray(arguments);
args[0] = 'item:' + event;
args.splice(1, 0, self);
// args = ["item:key:change", this=listEditor, itemEditor, fieldEditor]
editors.List.prototype.trigger.apply(this, args);
}, self);
item.editor.on('change', function() {
if (!item.addEventTriggered) {
item.addEventTriggered = true;
this.trigger('add', this, item.editor);
}
this.trigger('item:change', this, item.editor);
this.trigger('change', this);
}, self);
item.editor.on('focus', function() {
if (this.hasFocus) return;
this.trigger('focus', this);
}, self);
item.editor.on('blur', function() {
if (!this.hasFocus) return;
var self = this;
setTimeout(function() {
if (_.find(self.items, function(item) { return item.editor.hasFocus; })) return;
self.trigger('blur', self);
}, 0);
}, self);
if (userInitiated || value) {
item.addEventTriggered = true;
}
if (userInitiated) {
self.trigger('add', self, item.editor);
self.trigger('change', self);
}
};
//Check if we need to wait for the item to complete before adding to the list
if (this.Editor.isAsync) {
item.editor.on('readyToAdd', _addItem, this);
}
//Most editors can be added automatically
else {
_addItem();
}
return item;
},
/**
* Remove an item from the list
* @param {List.Item} item
*/
removeItem: function(item) {
//Confirm delete
var confirmMsg = this.schema.confirmDelete;
if (confirmMsg && !confirm(confirmMsg)) return;
var index = _.indexOf(this.items, item);
this.items[index].remove();
this.items.splice(index, 1);
if (item.addEventTriggered) {
this.trigger('remove', this, item.editor);
this.trigger('change', this);
}
if (!this.items.length && !this.Editor.isAsync) this.addItem();
},
getValue: function() {
var values = _.map(this.items, function(item) {
return item.getValue();
});
//Filter empty items
return _.without(values, undefined, '');
},
setValue: function(value) {
this.value = value;
this.render();
},
focus: function() {
if (this.hasFocus) return;
if (this.items[0]) this.items[0].editor.focus();
},
blur: function() {
if (!this.hasFocus) return;
var focusedItem = _.find(this.items, function(item) { return item.editor.hasFocus; });
if (focusedItem) focusedItem.editor.blur();
},
/**
* Override default remove function in order to remove item views
*/
remove: function() {
_.invoke(this.items, 'remove');
editors.Base.prototype.remove.call(this);
},
/**
* Run validation
*
* @return {Object|Null}
*/
validate: function() {
if (!this.validators) return null;
//Collect errors
var errors = _.map(this.items, function(item) {
return item.validate();
});
//Check if any item has errors
var hasErrors = _.compact(errors).length ? true : false;
if (!hasErrors) return null;
//If so create a shared error
var fieldError = {
type: 'list',
message: 'Some of the items in the list failed validation',
errors: errors
};
return fieldError;
}
});
/**
* A single item in the list
*
* @param {editors.List} options.list The List editor instance this item belongs to
* @param {Function} options.Editor Editor constructor function
* @param {String} options.key Model key
* @param {Mixed} options.value Value
* @param {Object} options.schema Field schema
*/
editors.List.Item = Backbone.View.extend({
events: {
'click [data-action="remove"]': function(event) {
event.preventDefault();
this.list.removeItem(this);
},
'keydown input[type=text]': function(event) {
if(event.keyCode !== 13) return;
event.preventDefault();
this.list.addItem();
this.list.$list.find("> li:last input").focus();
}
},
initialize: function(options) {
this.list = options.list;
this.schema = options.schema || this.list.schema;
this.value = options.value;
this.Editor = options.Editor || editors.Text;
this.key = options.key;
},
render: function() {
//Create editor
this.editor = new this.Editor({
key: this.key,
schema: this.schema,
value: this.value,
list: this.list,
item: this
}).render();
//Create main element
var $el = $(Form.templates[this.schema.listItemTemplate]({
editor: '<b class="bbf-tmp"></b>'
}));
$el.find('.bbf-tmp').replaceWith(this.editor.el);
//Replace the entire element so there isn't a wrapper tag
this.setElement($el);
return this;
},
getValue: function() {
return this.editor.getValue();
},
setValue: function(value) {
this.editor.setValue(value);
},
focus: function() {
this.editor.focus();
},
blur: function() {
this.editor.blur();
},
remove: function() {
this.editor.remove();
Backbone.View.prototype.remove.call(this);
},
validate: function() {
var value = this.getValue(),
formValues = this.list.form ? this.list.form.getValue() : {},
validators = this.schema.validators,
getValidator = Form.helpers.getValidator;
if (!validators) return null;
//Run through validators until an error is found
var error = null;
_.every(validators, function(validator) {
error = getValidator(validator)(value, formValues);
return error ? false : true;
});
//Show/hide error
if (error){
this.setError(error);
} else {
this.clearError();
}
//Return error to be aggregated by list
return error ? error : null;
},
/**
* Show a validation error
*/
setError: function(err) {
this.$el.addClass(Form.classNames.error);
this.$el.attr('title', err.message);
},
/**
* Hide validation errors
*/
clearError: function() {
this.$el.removeClass(Form.classNames.error);
this.$el.attr('title', null);
}
});
/**
* Modal object editor for use with the List editor.
* To use it, set the 'itemType' property in a List schema to 'Object' or 'NestedModel'
*/
editors.List.Modal = editors.List.Object = editors.List.NestedModel = editors.Base.extend({
events: {
'click': 'openEditor'
},
/**
* @param {Object} options
* @param {Function} [options.schema.itemToString] Function to transform the value for display in the list.
* @param {String} [options.schema.itemType] Editor type e.g. 'Text', 'Object'.
* @param {Object} [options.schema.subSchema] Schema for nested form,. Required when itemType is 'Object'
* @param {Function} [options.schema.model] Model constructor function. Required when itemType is 'NestedModel'
*/
initialize: function(options) {
editors.Base.prototype.initialize.call(this, options);
var schema = this.schema;
//Dependencies
if (!editors.List.Modal.ModalAdapter) throw 'A ModalAdapter is required';
//Get nested schema if Object
if (schema.itemType === 'Object') {
if (!schema.subSchema) throw 'Missing required option "schema.subSchema"';
this.nestedSchema = schema.subSchema;
}
//Get nested schema if NestedModel
if (schema.itemType === 'NestedModel') {
if (!schema.model) throw 'Missing required option "schema.model"';
this.nestedSchema = schema.model.prototype.schema;
if (_.isFunction(this.nestedSchema)) this.nestedSchema = this.nestedSchema();
}
},
/**
* Render the list item representation
*/
render: function() {
var self = this;
//New items in the list are only rendered when the editor has been OK'd
if (_.isEmpty(this.value)) {
this.openEditor();
}
//But items with values are added automatically
else {
this.renderSummary();
setTimeout(function() {
self.trigger('readyToAdd');
}, 0);
}
if (this.hasFocus) this.trigger('blur', this);
return this;
},
/**
* Renders the list item representation
*/
renderSummary: function() {
var template = Form.templates['list.Modal'];
this.$el.html(template({
summary: this.getStringValue()
}));
},
/**
* Function which returns a generic string representation of an object
*
* @param {Object} value
*
* @return {String}
*/
itemToString: function(value) {
value = value || {};
//Pretty print the object keys and values
var parts = [];
_.each(this.nestedSchema, function(schema, key) {
var desc = schema.title ? schema.title : Form.helpers.keyToTitle(key),
val = value[key];
if (_.isUndefined(val) || _.isNull(val)) val = '';
parts.push(desc + ': ' + val);
});
return parts.join('<br />');
},
/**
* Returns the string representation of the object value
*/
getStringValue: function() {
var schema = this.schema,
value = this.getValue();
if (_.isEmpty(value)) return '[Empty]';
//If there's a specified toString use that
if (schema.itemToString) return schema.itemToString(value);
//Otherwise check if it's NestedModel with it's own toString() method
if (schema.itemType === 'NestedModel') {
return new (schema.model)(value).toString();
}
//Otherwise use the generic method or custom overridden method
return this.itemToString(value);
},
openEditor: function() {
var self = this;
var form = new Form({
schema: this.nestedSchema,
data: this.value
});
var modal = this.modal = new Backbone.BootstrapModal({
content: form,
animate: true
}).open();
this.trigger('open', this);
this.trigger('focus', this);
modal.on('cancel', function() {
this.modal = null;
this.trigger('close', this);
this.trigger('blur', this);
}, this);
modal.on('ok', _.bind(this.onModalSubmitted, this, form, modal));
},
/**
* Called when the user clicks 'OK'.
* Runs validation and tells the list when ready to add the item
*/
onModalSubmitted: function(form, modal) {
var isNew = !this.value;
//Stop if there are validation errors
var error = form.validate();
if (error) return modal.preventClose();
this.modal = null;
//If OK, render the list item
this.value = form.getValue();
this.renderSummary();
if (isNew) this.trigger('readyToAdd');
this.trigger('change', this);
this.trigger('close', this);
this.trigger('blur', this);
},
getValue: function() {
return this.value;
},
setValue: function(value) {
this.value = value;
},
focus: function() {
if (this.hasFocus) return;
this.openEditor();
},
blur: function() {
if (!this.hasFocus) return;
if (this.modal) {
this.modal.trigger('cancel');
this.modal.close();
}
}
}, {
//STATICS
//The modal adapter that creates and manages the modal dialog.
//Defaults to BootstrapModal (http://github.com/powmedia/backbone.bootstrap-modal)
//Can be replaced with another adapter that implements the same interface.
ModalAdapter: Backbone.BootstrapModal,
//Make the wait list for the 'ready' event before adding the item to the list
isAsync: true
});
})();

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,43 @@
/* Date */
.bbf-date .bbf-date {
width: 4em
}
.bbf-date .bbf-month {
width: 9em;
}
.bbf-date .bbf-year {
width: 5em;
}
/* DateTime */
.bbf-datetime select {
width: 4em;
}
/* List */
.bbf-list .bbf-add {
margin-top: -10px
}
.bbf-list li {
margin-bottom: 5px
}
.bbf-list .bbf-del {
margin-left: 4px
}
/* List.Modal */
.bbf-list-modal {
cursor: pointer;
border: 1px solid #ccc;
width: 208px;
border-radius: 3px;
padding: 4px;
color: #555;
}

View file

@ -0,0 +1,95 @@
/**
* Include this file _after_ the main backbone-forms file to override the default templates.
* You only need to include templates you want to override.
*
* Requirements when customising templates:
* - Each template must have one 'parent' element tag.
* - "data-type" attributes are required.
* - The main placeholder tags such as the following are required: fieldsets, fields
*/
;(function() {
var Form = Backbone.Form;
//TWITTER BOOTSTRAP TEMPLATES
//Requires Bootstrap 2.x
Form.setTemplates({
//HTML
form: '\
<form class="form-horizontal">{{fieldsets}}</form>\
',
fieldset: '\
<fieldset>\
<legend>{{legend}}</legend>\
{{fields}}\
</fieldset>\
',
field: '\
<div class="control-group field-{{key}}">\
<label class="control-label" for="{{id}}">{{title}}</label>\
<div class="controls">\
{{editor}}\
<div class="help-inline">{{error}}</div>\
<div class="help-block">{{help}}</div>\
</div>\
</div>\
',
nestedField: '\
<div class="field-{{key}}">\
<div title="{{title}}" class="input-xlarge">{{editor}}\
<div class="help-inline">{{error}}</div>\
</div>\
<div class="help-block">{{help}}</div>\
</div>\
',
list: '\
<div class="bbf-list">\
<ul class="unstyled clearfix">{{items}}</ul>\
<button class="btn bbf-add" data-action="add">Add</button>\
</div>\
',
listItem: '\
<li class="clearfix">\
<div class="pull-left">{{editor}}</div>\
<button type="button" class="btn bbf-del" data-action="remove">&times;</button>\
</li>\
',
date: '\
<div class="bbf-date">\
<select data-type="date" class="bbf-date">{{dates}}</select>\
<select data-type="month" class="bbf-month">{{months}}</select>\
<select data-type="year" class="bbf-year">{{years}}</select>\
</div>\
',
dateTime: '\
<div class="bbf-datetime">\
<p>{{date}}</p>\
<p>\
<select data-type="hour" style="width: 4em">{{hours}}</select>\
:\
<select data-type="min" style="width: 4em">{{mins}}</select>\
</p>\
</div>\
',
'list.Modal': '\
<div class="bbf-list-modal">\
{{summary}}\
</div>\
'
}, {
//CLASSNAMES
error: 'error' //Set on the field tag when validation fails
});
})();

View file

@ -0,0 +1,140 @@
/* Form */
.bbf-form {
margin: 0;
padding: 0;
border: none;
}
/* Field */
.bbf-field {
margin: 1em 0;
list-style-type: none;
position: relative;
clear: both;
}
.bbf-field label {
float: left;
width: 25%;
}
.bbf-field .bbf-editor {
margin-left: 25%;
width: 74%;
}
.bbf-field input, .bbf-field textarea, .bbf-field select {
width: 100%;
}
.bbf-field .bbf-help {
margin-left: 25%;
width: 74%;
color: #999;
}
.bbf-field .bbf-error {
margin-left: 25%;
width: 74%;
color: red;
}
.bbf-field.bbf-error .bbf-editor {
outline: 1px solid red;
}
/* Radio */
.bbf-radio {
list-style-type: none;
}
.bbf-radio input {
width: auto;
}
.bbf-radio label {
float: none;
}
/* Checkboxes */
.bbf-checkboxes {
list-style-type: none;
}
.bbf-checkboxes input {
width: auto;
}
.bbf-checkboxes label {
float: none;
}
/* List */
.bbf-list ul {
list-style-type: none;
margin: 0;
padding: 0;
}
.bbf-list .bbf-error {
border: 1px solid red;
}
.bbf-list li {
clear: both;
width: 100%;
}
.bbf-list li .bbf-editor-container {
margin-right: 2em;
}
.bbf-list li .bbf-remove {
float: right;
width: 2em;
}
.bbf-list .bbf-actions {
text-align: center;
clear: both;
}
/* List.Modal */
.bbf-list-modal {
cursor: pointer;
border: 1px solid #ccc;
width: 208px;
border-radius: 3px;
padding: 4px;
color: #555;
}
/* Date */
.bbf-date .bbf-date {
width: 4em;
}
.bbf-date .bbf-month {
width: 9em;
}
.bbf-date .bbf-year {
width: 6em;
}
/* DateTime */
.bbf-datetime .bbf-date-container {
float: left;
margin-right: 1em;
}
.bbf-datetime select {
width: 4em;
}

View file

@ -0,0 +1,91 @@
/**
* Include this file _after_ the main backbone-forms file to override the default templates.
* You only need to include templates you want to override.
*
* Requirements when customising templates:
* - Each template must have one 'parent' element tag.
* - "data-type" attributes are required.
* - The main placeholder tags such as the following are required: fieldsets, fields
*/
;(function() {
var Form = Backbone.Form;
//DEFAULT TEMPLATES
Form.setTemplates({
//HTML
form: '\
<form class="bbf-form">{{fieldsets}}</form>\
',
fieldset: '\
<fieldset>\
<legend>{{legend}}</legend>\
<ul>{{fields}}</ul>\
</fieldset>\
',
field: '\
<li class="bbf-field field-{{key}}">\
<label for="{{id}}">{{title}}</label>\
<div class="bbf-editor">{{editor}}</div>\
<div class="bbf-help">{{help}}</div>\
<div class="bbf-error">{{error}}</div>\
</li>\
',
nestedField: '\
<li class="bbf-field bbf-nested-field field-{{key}}" title="{{title}}">\
<label for="{{id}}">{{title}}</label>\
<div class="bbf-editor">{{editor}}</div>\
<div class="bbf-help">{{help}}</div>\
<div class="bbf-error">{{error}}</div>\
</li>\
',
list: '\
<div class="bbf-list">\
<ul>{{items}}</ul>\
<div class="bbf-actions"><button type="button" data-action="add">Add</div>\
</div>\
',
listItem: '\
<li>\
<button type="button" data-action="remove" class="bbf-remove">&times;</button>\
<div class="bbf-editor-container">{{editor}}</div>\
</li>\
',
date: '\
<div class="bbf-date">\
<select data-type="date" class="bbf-date">{{dates}}</select>\
<select data-type="month" class="bbf-month">{{months}}</select>\
<select data-type="year" class="bbf-year">{{years}}</select>\
</div>\
',
dateTime: '\
<div class="bbf-datetime">\
<div class="bbf-date-container">{{date}}</div>\
<select data-type="hour">{{hours}}</select>\
:\
<select data-type="min">{{mins}}</select>\
</div>\
',
'list.Modal': '\
<div class="bbf-list-modal">\
{{summary}}\
</div>\
'
}, {
//CLASSNAMES
error: 'bbf-error'
});
})();