First commit
This commit is contained in:
commit
c6e2478c40
13918 changed files with 2303184 additions and 0 deletions
1263
sites/all/modules/civicrm/packages/backbone-forms/src/editors.js
Normal file
1263
sites/all/modules/civicrm/packages/backbone-forms/src/editors.js
Normal file
File diff suppressed because it is too large
Load diff
65
sites/all/modules/civicrm/packages/backbone-forms/src/editors/jquery-ui.css
vendored
Normal file
65
sites/all/modules/civicrm/packages/backbone-forms/src/editors/jquery-ui.css
vendored
Normal 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%;
|
||||
}
|
500
sites/all/modules/civicrm/packages/backbone-forms/src/editors/jquery-ui.js
vendored
Normal file
500
sites/all/modules/civicrm/packages/backbone-forms/src/editors/jquery-ui.js
vendored
Normal 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);
|
||||
|
||||
})();
|
|
@ -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
|
||||
});
|
||||
|
||||
})();
|
258
sites/all/modules/civicrm/packages/backbone-forms/src/field.js
Normal file
258
sites/all/modules/civicrm/packages/backbone-forms/src/field.js
Normal file
|
@ -0,0 +1,258 @@
|
|||
|
||||
//==================================================================================================
|
||||
//FIELD
|
||||
//==================================================================================================
|
||||
|
||||
Form.Field = (function() {
|
||||
|
||||
var helpers = Form.helpers,
|
||||
templates = Form.templates;
|
||||
|
||||
return Backbone.View.extend({
|
||||
|
||||
/**
|
||||
* @param {Object} Options
|
||||
* Required:
|
||||
* key {String} : The model attribute key
|
||||
* Optional:
|
||||
* schema {Object} : Schema for the field
|
||||
* value {Mixed} : Pass value when not using a model. Use getValue() to get out value
|
||||
* model {Backbone.Model} : Use instead of value, and use commit().
|
||||
* idPrefix {String} : Prefix to add to the editor DOM element's ID
|
||||
*/
|
||||
/**
|
||||
* Creates a new field
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {Object} [options.schema] Field schema. Defaults to { type: 'Text' }
|
||||
* @param {Model} [options.model] Model the field relates to. Required if options.data is not set.
|
||||
* @param {String} [options.key] Model key/attribute the field relates to.
|
||||
* @param {Mixed} [options.value] Field value. Required if options.model is not set.
|
||||
* @param {String} [options.idPrefix] Prefix for the editor ID. By default, the model's CID is used.
|
||||
*
|
||||
* @return {Field}
|
||||
*/
|
||||
initialize: function(options) {
|
||||
options = options || {};
|
||||
|
||||
this.form = options.form;
|
||||
this.key = options.key;
|
||||
this.value = options.value;
|
||||
this.model = options.model;
|
||||
|
||||
//Turn schema shorthand notation (e.g. 'Text') into schema object
|
||||
if (_.isString(options.schema)) options.schema = { type: options.schema };
|
||||
|
||||
//Set schema defaults
|
||||
this.schema = _.extend({
|
||||
type: 'Text',
|
||||
title: helpers.keyToTitle(this.key),
|
||||
template: 'field'
|
||||
}, options.schema);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Provides the context for rendering the field
|
||||
* Override this to extend the default context
|
||||
*
|
||||
* @param {Object} schema
|
||||
* @param {View} editor
|
||||
*
|
||||
* @return {Object} Locals passed to the template
|
||||
*/
|
||||
renderingContext: function(schema, editor) {
|
||||
return {
|
||||
key: this.key,
|
||||
title: schema.title,
|
||||
id: editor.id,
|
||||
type: schema.type,
|
||||
editor: '<b class="bbf-tmp-editor"></b>',
|
||||
help: '<b class="bbf-tmp-help"></b>',
|
||||
error: '<b class="bbf-tmp-error"></b>'
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Renders the field
|
||||
*/
|
||||
render: function() {
|
||||
var schema = this.schema,
|
||||
templates = Form.templates;
|
||||
|
||||
//Standard options that will go to all editors
|
||||
var options = {
|
||||
form: this.form,
|
||||
key: this.key,
|
||||
schema: schema,
|
||||
idPrefix: this.options.idPrefix,
|
||||
id: this.getId()
|
||||
};
|
||||
|
||||
//Decide on data delivery type to pass to editors
|
||||
if (this.model) {
|
||||
options.model = this.model;
|
||||
} else {
|
||||
options.value = this.value;
|
||||
}
|
||||
|
||||
//Decide on the editor to use
|
||||
var editor = this.editor = helpers.createEditor(schema.type, options);
|
||||
|
||||
//Create the element
|
||||
var $field = $(templates[schema.template](this.renderingContext(schema, editor)));
|
||||
|
||||
//Remove <label> if it's not wanted
|
||||
if (schema.title === false) {
|
||||
$field.find('label[for="'+editor.id+'"]').first().remove();
|
||||
}
|
||||
|
||||
//Render editor
|
||||
$field.find('.bbf-tmp-editor').replaceWith(editor.render().el);
|
||||
|
||||
//Set help text
|
||||
this.$help = $('.bbf-tmp-help', $field).parent();
|
||||
this.$help.empty();
|
||||
if (this.schema.help) this.$help.html(this.schema.help);
|
||||
|
||||
//Create error container
|
||||
this.$error = $($('.bbf-tmp-error', $field).parent()[0]);
|
||||
if (this.$error) this.$error.empty();
|
||||
|
||||
//Add custom CSS class names
|
||||
if (this.schema.fieldClass) $field.addClass(this.schema.fieldClass);
|
||||
|
||||
//Add custom attributes
|
||||
if (this.schema.fieldAttrs) $field.attr(this.schema.fieldAttrs);
|
||||
|
||||
//Replace the generated wrapper tag
|
||||
this.setElement($field);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates the ID that will be assigned to the editor
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
getId: function() {
|
||||
var prefix = this.options.idPrefix,
|
||||
id = this.key;
|
||||
|
||||
//Replace periods with underscores (e.g. for when using paths)
|
||||
id = id.replace(/\./g, '_');
|
||||
|
||||
//If a specific ID prefix is set, use it
|
||||
if (_.isString(prefix) || _.isNumber(prefix)) return prefix + id;
|
||||
if (_.isNull(prefix)) return id;
|
||||
|
||||
//Otherwise, if there is a model use it's CID to avoid conflicts when multiple forms are on the page
|
||||
if (this.model) return this.model.cid + '_' + id;
|
||||
|
||||
return id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the validity of the field
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
validate: function() {
|
||||
var error = this.editor.validate();
|
||||
|
||||
if (error) {
|
||||
this.setError(error.message);
|
||||
} else {
|
||||
this.clearError();
|
||||
}
|
||||
|
||||
return error;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the field into an error state, adding the error class and setting the error message
|
||||
*
|
||||
* @param {String} msg Error message
|
||||
*/
|
||||
setError: function(msg) {
|
||||
//Object and NestedModel types set their own errors internally
|
||||
if (this.editor.hasNestedForm) return;
|
||||
|
||||
var errClass = Form.classNames.error;
|
||||
|
||||
this.$el.addClass(errClass);
|
||||
|
||||
if (this.$error) {
|
||||
this.$error.html(msg);
|
||||
} else if (this.$help) {
|
||||
this.$help.html(msg);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear the error state and reset the help message
|
||||
*/
|
||||
clearError: function() {
|
||||
var errClass = Form.classNames.error;
|
||||
|
||||
this.$el.removeClass(errClass);
|
||||
|
||||
// some fields (e.g., Hidden), may not have a help el
|
||||
if (this.$error) {
|
||||
this.$error.empty();
|
||||
} else if (this.$help) {
|
||||
this.$help.empty();
|
||||
|
||||
//Reset help text if available
|
||||
var helpMsg = this.schema.help;
|
||||
if (helpMsg) this.$help.html(helpMsg);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the model with the new value from the editor
|
||||
*/
|
||||
commit: function() {
|
||||
return this.editor.commit();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the value from the editor
|
||||
*
|
||||
* @return {Mixed}
|
||||
*/
|
||||
getValue: function() {
|
||||
return this.editor.getValue();
|
||||
},
|
||||
|
||||
/**
|
||||
* Set/change the value of the editor
|
||||
*
|
||||
* @param {Mixed} value
|
||||
*/
|
||||
setValue: function(value) {
|
||||
this.editor.setValue(value);
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
this.editor.focus();
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
this.editor.blur();
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the field and editor views
|
||||
*/
|
||||
remove: function() {
|
||||
this.editor.remove();
|
||||
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})();
|
389
sites/all/modules/civicrm/packages/backbone-forms/src/form.js
Normal file
389
sites/all/modules/civicrm/packages/backbone-forms/src/form.js
Normal file
|
@ -0,0 +1,389 @@
|
|||
|
||||
//==================================================================================================
|
||||
//FORM
|
||||
//==================================================================================================
|
||||
|
||||
var Form = (function() {
|
||||
|
||||
return Backbone.View.extend({
|
||||
|
||||
hasFocus: false,
|
||||
|
||||
/**
|
||||
* Creates a new form
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {Model} [options.model] Model the form relates to. Required if options.data is not set
|
||||
* @param {Object} [options.data] Date to populate the form. Required if options.model is not set
|
||||
* @param {String[]} [options.fields] Fields to include in the form, in order
|
||||
* @param {String[]|Object[]} [options.fieldsets] How to divide the fields up by section. E.g. [{ legend: 'Title', fields: ['field1', 'field2'] }]
|
||||
* @param {String} [options.idPrefix] Prefix for editor IDs. By default, the model's CID is used.
|
||||
* @param {String} [options.template] Form template key/name
|
||||
* @param {String} [options.fieldsetTemplate] Fieldset template key/name
|
||||
* @param {String} [options.fieldTemplate] Field template key/name
|
||||
*
|
||||
* @return {Form}
|
||||
*/
|
||||
initialize: function(options) {
|
||||
//Check templates have been loaded
|
||||
if (!Form.templates.form) throw new Error('Templates not loaded');
|
||||
|
||||
//Get the schema
|
||||
this.schema = (function() {
|
||||
if (options.schema) return options.schema;
|
||||
|
||||
var model = options.model;
|
||||
if (!model) throw new Error('Could not find schema');
|
||||
|
||||
if (_.isFunction(model.schema)) return model.schema();
|
||||
|
||||
return model.schema;
|
||||
})();
|
||||
|
||||
//Option defaults
|
||||
options = _.extend({
|
||||
template: 'form',
|
||||
fieldsetTemplate: 'fieldset',
|
||||
fieldTemplate: 'field'
|
||||
}, options);
|
||||
|
||||
//Determine fieldsets
|
||||
if (!options.fieldsets) {
|
||||
var fields = options.fields || _.keys(this.schema);
|
||||
|
||||
options.fieldsets = [{ fields: fields }];
|
||||
}
|
||||
|
||||
//Store main attributes
|
||||
this.options = options;
|
||||
this.model = options.model;
|
||||
this.data = options.data;
|
||||
this.fields = {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the form and all fields
|
||||
*/
|
||||
render: function() {
|
||||
var self = this,
|
||||
options = this.options,
|
||||
template = Form.templates[options.template];
|
||||
|
||||
//Create el from template
|
||||
var $form = $(template({
|
||||
fieldsets: '<b class="bbf-tmp"></b>'
|
||||
}));
|
||||
|
||||
//Render fieldsets
|
||||
var $fieldsetContainer = $('.bbf-tmp', $form);
|
||||
|
||||
_.each(options.fieldsets, function(fieldset) {
|
||||
$fieldsetContainer.append(self.renderFieldset(fieldset));
|
||||
});
|
||||
|
||||
$fieldsetContainer.children().unwrap();
|
||||
|
||||
//Set the template contents as the main element; removes the wrapper element
|
||||
this.setElement($form);
|
||||
|
||||
if (this.hasFocus) this.trigger('blur', this);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders a fieldset and the fields within it
|
||||
*
|
||||
* Valid fieldset definitions:
|
||||
* ['field1', 'field2']
|
||||
* { legend: 'Some Fieldset', fields: ['field1', 'field2'] }
|
||||
*
|
||||
* @param {Object|Array} fieldset A fieldset definition
|
||||
*
|
||||
* @return {jQuery} The fieldset DOM element
|
||||
*/
|
||||
renderFieldset: function(fieldset) {
|
||||
var self = this,
|
||||
template = Form.templates[this.options.fieldsetTemplate],
|
||||
schema = this.schema,
|
||||
getNested = Form.helpers.getNested;
|
||||
|
||||
//Normalise to object
|
||||
if (_.isArray(fieldset)) {
|
||||
fieldset = { fields: fieldset };
|
||||
}
|
||||
|
||||
//Concatenating HTML as strings won't work so we need to insert field elements into a placeholder
|
||||
var $fieldset = $(template(_.extend({}, fieldset, {
|
||||
legend: '<b class="bbf-tmp-legend"></b>',
|
||||
fields: '<b class="bbf-tmp-fields"></b>'
|
||||
})));
|
||||
|
||||
//Set legend
|
||||
if (fieldset.legend) {
|
||||
$fieldset.find('.bbf-tmp-legend').replaceWith(fieldset.legend);
|
||||
}
|
||||
//or remove the containing tag if there isn't a legend
|
||||
else {
|
||||
$fieldset.find('.bbf-tmp-legend').parent().remove();
|
||||
}
|
||||
|
||||
var $fieldsContainer = $('.bbf-tmp-fields', $fieldset);
|
||||
|
||||
//Render fields
|
||||
_.each(fieldset.fields, function(key) {
|
||||
//Get the field schema
|
||||
var itemSchema = (function() {
|
||||
//Return a normal key or path key
|
||||
if (schema[key]) return schema[key];
|
||||
|
||||
//Return a nested schema, i.e. Object
|
||||
var path = key.replace(/\./g, '.subSchema.');
|
||||
return getNested(schema, path);
|
||||
})();
|
||||
|
||||
if (!itemSchema) throw "Field '"+key+"' not found in schema";
|
||||
|
||||
//Create the field
|
||||
var field = self.fields[key] = self.createField(key, itemSchema);
|
||||
|
||||
//Render the fields with editors, apart from Hidden fields
|
||||
var fieldEl = field.render().el;
|
||||
|
||||
field.editor.on('all', function(event) {
|
||||
// args = ["change", editor]
|
||||
var args = _.toArray(arguments);
|
||||
args[0] = key + ':' + event;
|
||||
args.splice(1, 0, this);
|
||||
// args = ["key:change", this=form, editor]
|
||||
|
||||
this.trigger.apply(this, args);
|
||||
}, self);
|
||||
|
||||
field.editor.on('change', function() {
|
||||
this.trigger('change', self);
|
||||
}, self);
|
||||
|
||||
field.editor.on('focus', function() {
|
||||
if (this.hasFocus) return;
|
||||
this.trigger('focus', this);
|
||||
}, self);
|
||||
field.editor.on('blur', function() {
|
||||
if (!this.hasFocus) return;
|
||||
var self = this;
|
||||
setTimeout(function() {
|
||||
if (_.find(self.fields, function(field) { return field.editor.hasFocus; })) return;
|
||||
self.trigger('blur', self);
|
||||
}, 0);
|
||||
}, self);
|
||||
|
||||
if (itemSchema.type !== 'Hidden') {
|
||||
$fieldsContainer.append(fieldEl);
|
||||
}
|
||||
});
|
||||
|
||||
$fieldsContainer = $fieldsContainer.children().unwrap();
|
||||
|
||||
return $fieldset;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders a field and returns it
|
||||
*
|
||||
* @param {String} key The key for the field in the form schema
|
||||
* @param {Object} schema Field schema
|
||||
*
|
||||
* @return {Field} The field view
|
||||
*/
|
||||
createField: function(key, schema) {
|
||||
schema.template = schema.template || this.options.fieldTemplate;
|
||||
|
||||
var options = {
|
||||
form: this,
|
||||
key: key,
|
||||
schema: schema,
|
||||
idPrefix: this.options.idPrefix,
|
||||
template: this.options.fieldTemplate
|
||||
};
|
||||
|
||||
if (this.model) {
|
||||
options.model = this.model;
|
||||
} else if (this.data) {
|
||||
options.value = this.data[key];
|
||||
} else {
|
||||
options.value = null;
|
||||
}
|
||||
|
||||
return new Form.Field(options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate the data
|
||||
*
|
||||
* @return {Object} Validation errors
|
||||
*/
|
||||
validate: function() {
|
||||
var self = this,
|
||||
fields = this.fields,
|
||||
model = this.model,
|
||||
errors = {};
|
||||
|
||||
//Collect errors from schema validation
|
||||
_.each(fields, function(field) {
|
||||
var error = field.validate();
|
||||
if (error) {
|
||||
errors[field.key] = error;
|
||||
}
|
||||
});
|
||||
|
||||
//Get errors from default Backbone model validator
|
||||
if (model && model.validate) {
|
||||
var modelErrors = model.validate(this.getValue());
|
||||
|
||||
if (modelErrors) {
|
||||
var isDictionary = _.isObject(modelErrors) && !_.isArray(modelErrors);
|
||||
|
||||
//If errors are not in object form then just store on the error object
|
||||
if (!isDictionary) {
|
||||
errors._others = errors._others || [];
|
||||
errors._others.push(modelErrors);
|
||||
}
|
||||
|
||||
//Merge programmatic errors (requires model.validate() to return an object e.g. { fieldKey: 'error' })
|
||||
if (isDictionary) {
|
||||
_.each(modelErrors, function(val, key) {
|
||||
//Set error on field if there isn't one already
|
||||
if (self.fields[key] && !errors[key]) {
|
||||
self.fields[key].setError(val);
|
||||
errors[key] = val;
|
||||
}
|
||||
|
||||
else {
|
||||
//Otherwise add to '_others' key
|
||||
errors._others = errors._others || [];
|
||||
var tmpErr = {};
|
||||
tmpErr[key] = val;
|
||||
errors._others.push(tmpErr);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _.isEmpty(errors) ? null : errors;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the model with all latest values.
|
||||
*
|
||||
* @return {Object} Validation errors
|
||||
*/
|
||||
commit: function() {
|
||||
//Validate
|
||||
var errors = this.validate();
|
||||
if (errors) return errors;
|
||||
|
||||
//Commit
|
||||
var modelError;
|
||||
this.model.set(this.getValue(), {
|
||||
error: function(model, e) {
|
||||
modelError = e;
|
||||
}
|
||||
});
|
||||
|
||||
if (modelError) return modelError;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get all the field values as an object.
|
||||
* Use this method when passing data instead of objects
|
||||
*
|
||||
* @param {String} [key] Specific field value to get
|
||||
*/
|
||||
getValue: function(key) {
|
||||
//Return only given key if specified
|
||||
if (key) return this.fields[key].getValue();
|
||||
|
||||
//Otherwise return entire form
|
||||
var values = {};
|
||||
_.each(this.fields, function(field) {
|
||||
values[field.key] = field.getValue();
|
||||
});
|
||||
|
||||
return values;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update field values, referenced by key
|
||||
* @param {Object|String} key New values to set, or property to set
|
||||
* @param val Value to set
|
||||
*/
|
||||
setValue: function(prop, val) {
|
||||
var data = {};
|
||||
if (typeof prop === 'string') {
|
||||
data[prop] = val;
|
||||
} else {
|
||||
data = prop;
|
||||
}
|
||||
|
||||
var key;
|
||||
for (key in this.schema) {
|
||||
if (data[key] !== undefined) {
|
||||
this.fields[key].setValue(data[key]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if (this.hasFocus) return;
|
||||
|
||||
var fieldset = this.options.fieldsets[0];
|
||||
if (fieldset) {
|
||||
var field;
|
||||
if (_.isArray(fieldset)) {
|
||||
field = fieldset[0];
|
||||
}
|
||||
else {
|
||||
field = fieldset.fields[0];
|
||||
}
|
||||
if (field) {
|
||||
this.fields[field].editor.focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
blur: function() {
|
||||
if (!this.hasFocus) return;
|
||||
|
||||
var focusedField = _.find(this.fields, function(field) { return field.editor.hasFocus; });
|
||||
|
||||
if (focusedField) focusedField.editor.blur();
|
||||
},
|
||||
|
||||
/**
|
||||
* Override default remove function in order to remove embedded views
|
||||
*/
|
||||
remove: function() {
|
||||
var fields = this.fields;
|
||||
|
||||
for (var key in fields) {
|
||||
fields[key].remove();
|
||||
}
|
||||
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
|
||||
trigger: function(event) {
|
||||
if (event === 'focus') {
|
||||
this.hasFocus = true;
|
||||
}
|
||||
else if (event === 'blur') {
|
||||
this.hasFocus = false;
|
||||
}
|
||||
|
||||
return Backbone.View.prototype.trigger.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
212
sites/all/modules/civicrm/packages/backbone-forms/src/helpers.js
Normal file
212
sites/all/modules/civicrm/packages/backbone-forms/src/helpers.js
Normal file
|
@ -0,0 +1,212 @@
|
|||
|
||||
//==================================================================================================
|
||||
//HELPERS
|
||||
//==================================================================================================
|
||||
|
||||
Form.helpers = (function() {
|
||||
|
||||
var helpers = {};
|
||||
|
||||
/**
|
||||
* Gets a nested attribute using a path e.g. 'user.name'
|
||||
*
|
||||
* @param {Object} obj Object to fetch attribute from
|
||||
* @param {String} path Attribute path e.g. 'user.name'
|
||||
* @return {Mixed}
|
||||
* @api private
|
||||
*/
|
||||
helpers.getNested = function(obj, path) {
|
||||
var fields = path.split(".");
|
||||
var result = obj;
|
||||
for (var i = 0, n = fields.length; i < n; i++) {
|
||||
result = result[fields[i]];
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* This function is used to transform the key from a schema into the title used in a label.
|
||||
* (If a specific title is provided it will be used instead).
|
||||
*
|
||||
* By default this converts a camelCase string into words, i.e. Camel Case
|
||||
* If you have a different naming convention for schema keys, replace this function.
|
||||
*
|
||||
* @param {String} Key
|
||||
* @return {String} Title
|
||||
*/
|
||||
helpers.keyToTitle = function(str) {
|
||||
//Add spaces
|
||||
str = str.replace(/([A-Z])/g, ' $1');
|
||||
|
||||
//Uppercase first character
|
||||
str = str.replace(/^./, function(str) { return str.toUpperCase(); });
|
||||
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to compile a template with the {{mustache}} style tags. Template settings are reset
|
||||
* to user's settings when done to avoid conflicts.
|
||||
* @param {String} Template string
|
||||
* @return {Template} Compiled template
|
||||
*/
|
||||
helpers.compileTemplate = function(str) {
|
||||
//Store user's template options
|
||||
var _interpolateBackup = _.templateSettings.interpolate;
|
||||
|
||||
//Set custom template settings
|
||||
_.templateSettings.interpolate = /\{\{(.+?)\}\}/g;
|
||||
|
||||
var template = _.template(str);
|
||||
|
||||
//Reset to users' template settings
|
||||
_.templateSettings.interpolate = _interpolateBackup;
|
||||
|
||||
return template;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to create a template with the {{mustache}} style tags.
|
||||
* If context is passed in, the template will be evaluated.
|
||||
* @param {String} Template string
|
||||
* @param {Object} Optional; values to replace in template
|
||||
* @return {Template|String} Compiled template or the evaluated string
|
||||
*/
|
||||
helpers.createTemplate = function(str, context) {
|
||||
var template = helpers.compileTemplate(str);
|
||||
|
||||
if (!context) {
|
||||
return template;
|
||||
} else {
|
||||
return template(context);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the template compiler to the given function
|
||||
* @param {Function} Template compiler function
|
||||
*/
|
||||
helpers.setTemplateCompiler = function(compiler) {
|
||||
helpers.compileTemplate = compiler;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the templates to be used.
|
||||
*
|
||||
* If the templates passed in are strings, they will be compiled, expecting Mustache style tags,
|
||||
* i.e. <div>{{varName}}</div>
|
||||
*
|
||||
* You can also pass in previously compiled Underscore templates, in which case you can use any style
|
||||
* tags.
|
||||
*
|
||||
* @param {Object} templates
|
||||
* @param {Object} classNames
|
||||
*/
|
||||
helpers.setTemplates = function(templates, classNames) {
|
||||
var createTemplate = helpers.createTemplate;
|
||||
|
||||
Form.templates = Form.templates || {};
|
||||
Form.classNames = Form.classNames || {};
|
||||
|
||||
//Set templates, compiling them if necessary
|
||||
_.each(templates, function(template, key, index) {
|
||||
if (_.isString(template)) template = createTemplate(template);
|
||||
|
||||
Form.templates[key] = template;
|
||||
});
|
||||
|
||||
//Set class names
|
||||
_.extend(Form.classNames, classNames);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Return the editor constructor for a given schema 'type'.
|
||||
* Accepts strings for the default editors, or the reference to the constructor function
|
||||
* for custom editors
|
||||
*
|
||||
* @param {String|Function} The schema type e.g. 'Text', 'Select', or the editor constructor e.g. editors.Date
|
||||
* @param {Object} Options to pass to editor, including required 'key', 'schema'
|
||||
* @return {Mixed} An instance of the mapped editor
|
||||
*/
|
||||
helpers.createEditor = function(schemaType, options) {
|
||||
var constructorFn;
|
||||
|
||||
if (_.isString(schemaType)) {
|
||||
constructorFn = Form.editors[schemaType];
|
||||
} else {
|
||||
constructorFn = schemaType;
|
||||
}
|
||||
|
||||
return new constructorFn(options);
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggers an event that can be cancelled. Requires the user to invoke a callback. If false
|
||||
* is passed to the callback, the action does not run.
|
||||
*
|
||||
* NOTE: This helper uses private Backbone apis so can break when Backbone is upgraded
|
||||
*
|
||||
* @param {Mixed} Instance of Backbone model, view, collection to trigger event on
|
||||
* @param {String} Event name
|
||||
* @param {Array} Arguments to pass to the event handlers
|
||||
* @param {Function} Callback to run after the event handler has run.
|
||||
* If any of them passed false or error, this callback won't run
|
||||
*/
|
||||
helpers.triggerCancellableEvent = function(subject, event, args, callback) {
|
||||
//Return if there are no event listeners
|
||||
if (!subject._callbacks || !subject._callbacks[event]) return callback();
|
||||
|
||||
var next = subject._callbacks[event].next;
|
||||
if (!next) return callback();
|
||||
|
||||
var fn = next.callback,
|
||||
context = next.context || this;
|
||||
|
||||
//Add the callback that will be used when done
|
||||
args.push(callback);
|
||||
|
||||
fn.apply(context, args);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a validation function based on the type defined in the schema
|
||||
*
|
||||
* @param {RegExp|String|Function} validator
|
||||
* @return {Function}
|
||||
*/
|
||||
helpers.getValidator = function(validator) {
|
||||
var validators = Form.validators;
|
||||
|
||||
//Convert regular expressions to validators
|
||||
if (_.isRegExp(validator)) {
|
||||
return validators.regexp({ regexp: validator });
|
||||
}
|
||||
|
||||
//Use a built-in validator if given a string
|
||||
if (_.isString(validator)) {
|
||||
if (!validators[validator]) throw new Error('Validator "'+validator+'" not found');
|
||||
|
||||
return validators[validator]();
|
||||
}
|
||||
|
||||
//Functions can be used directly
|
||||
if (_.isFunction(validator)) return validator;
|
||||
|
||||
//Use a customised built-in validator if given an object
|
||||
if (_.isObject(validator) && validator.type) {
|
||||
var config = validator;
|
||||
|
||||
return validators[config.type](config);
|
||||
}
|
||||
|
||||
//Unkown validator type
|
||||
throw new Error('Invalid validator: ' + validator);
|
||||
};
|
||||
|
||||
|
||||
return helpers;
|
||||
|
||||
})();
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
//SETUP
|
||||
|
||||
//Add function shortcuts
|
||||
Form.setTemplates = Form.helpers.setTemplates;
|
||||
Form.setTemplateCompiler = Form.helpers.setTemplateCompiler;
|
||||
|
||||
Form.templates = {};
|
43
sites/all/modules/civicrm/packages/backbone-forms/src/templates/bootstrap.css
vendored
Normal file
43
sites/all/modules/civicrm/packages/backbone-forms/src/templates/bootstrap.css
vendored
Normal 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;
|
||||
}
|
80
sites/all/modules/civicrm/packages/backbone-forms/src/templates/bootstrap.js
vendored
Normal file
80
sites/all/modules/civicrm/packages/backbone-forms/src/templates/bootstrap.js
vendored
Normal file
|
@ -0,0 +1,80 @@
|
|||
|
||||
//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">×</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
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
|
||||
//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">×</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'
|
||||
|
||||
});
|
|
@ -0,0 +1,105 @@
|
|||
|
||||
//==================================================================================================
|
||||
//VALIDATORS
|
||||
//==================================================================================================
|
||||
|
||||
Form.validators = (function() {
|
||||
|
||||
var validators = {};
|
||||
|
||||
validators.errMessages = {
|
||||
required: 'Required',
|
||||
regexp: 'Invalid',
|
||||
email: 'Invalid email address',
|
||||
url: 'Invalid URL',
|
||||
match: 'Must match field "{{field}}"'
|
||||
};
|
||||
|
||||
validators.required = function(options) {
|
||||
options = _.extend({
|
||||
type: 'required',
|
||||
message: this.errMessages.required
|
||||
}, options);
|
||||
|
||||
return function required(value) {
|
||||
options.value = value;
|
||||
|
||||
var err = {
|
||||
type: options.type,
|
||||
message: Form.helpers.createTemplate(options.message, options)
|
||||
};
|
||||
|
||||
if (value === null || value === undefined || value === false || value === '') return err;
|
||||
};
|
||||
};
|
||||
|
||||
validators.regexp = function(options) {
|
||||
if (!options.regexp) throw new Error('Missing required "regexp" option for "regexp" validator');
|
||||
|
||||
options = _.extend({
|
||||
type: 'regexp',
|
||||
message: this.errMessages.regexp
|
||||
}, options);
|
||||
|
||||
return function regexp(value) {
|
||||
options.value = value;
|
||||
|
||||
var err = {
|
||||
type: options.type,
|
||||
message: Form.helpers.createTemplate(options.message, options)
|
||||
};
|
||||
|
||||
//Don't check empty values (add a 'required' validator for this)
|
||||
if (value === null || value === undefined || value === '') return;
|
||||
|
||||
if (!options.regexp.test(value)) return err;
|
||||
};
|
||||
};
|
||||
|
||||
validators.email = function(options) {
|
||||
options = _.extend({
|
||||
type: 'email',
|
||||
message: this.errMessages.email,
|
||||
regexp: /^[\w\-]{1,}([\w\-\+.]{1,1}[\w\-]{1,}){0,}[@][\w\-]{1,}([.]([\w\-]{1,})){1,3}$/
|
||||
}, options);
|
||||
|
||||
return validators.regexp(options);
|
||||
};
|
||||
|
||||
validators.url = function(options) {
|
||||
options = _.extend({
|
||||
type: 'url',
|
||||
message: this.errMessages.url,
|
||||
regexp: /^(http|https):\/\/(([A-Z0-9][A-Z0-9_\-]*)(\.[A-Z0-9][A-Z0-9_\-]*)+)(:(\d+))?\/?/i
|
||||
}, options);
|
||||
|
||||
return validators.regexp(options);
|
||||
};
|
||||
|
||||
validators.match = function(options) {
|
||||
if (!options.field) throw new Error('Missing required "field" options for "match" validator');
|
||||
|
||||
options = _.extend({
|
||||
type: 'match',
|
||||
message: this.errMessages.match
|
||||
}, options);
|
||||
|
||||
return function match(value, attrs) {
|
||||
options.value = value;
|
||||
|
||||
var err = {
|
||||
type: options.type,
|
||||
message: Form.helpers.createTemplate(options.message, options)
|
||||
};
|
||||
|
||||
//Don't check empty values (add a 'required' validator for this)
|
||||
if (value === null || value === undefined || value === '') return;
|
||||
|
||||
if (value !== attrs[options.field]) return err;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
return validators;
|
||||
|
||||
})();
|
Loading…
Add table
Add a link
Reference in a new issue