window.carbon = window.carbon || {};
(function($) {
var carbon = window.carbon;
/*
|--------------------------------------------------------------------------
| Base Container MODEL
|--------------------------------------------------------------------------
|
| This class represents the default model for a container.
| The app will fallback to this class if a container has no dedicated model.
|
| A model is responsible for holding the containers current state (data).
| It also holds all the logic surrounding the data management, like:
| - conversion
| - validation
| - access control
|
*/
carbon.containers.Model = Backbone.Model.extend({
defaults: {
'error': false,
'visible': true,
'has_changes': false,
'classes': [],
},
initialize: function() {
this.addClass(['carbon-container', 'carbon-container-' + this.get('type')]);
},
addClass: function(newClass) {
if (!_.isArray(newClass)) {
newClass = [newClass];
}
var oldClasses = this.get('classes') || [];
var classes = _.union(oldClasses, newClass);
if (classes.length !== oldClasses.length) {
this.set('classes', classes);
}
},
/*
* The validate method is an internal Backbone method.
* @see http://backbonejs.org/#Model-validate
*/
validate: function(attrs, options) {
if (attrs.visible === false) {
return false;
}
var hasErrors = false;
var view = carbon.views[this.get('id')];
if (!view) {
return;
}
_.each(view.fieldsCollection.models, function(field) {
if (!field.isRequired()) {
return;
}
if (field.isValid()) {
field.set('error', false);
} else {
field.set('error', true);
hasErrors = true;
return;
}
});
this.set('error', hasErrors);
if (hasErrors) {
return carbon_containers_l10n.please_fill_the_required_fields;
}
}
});
/*
|--------------------------------------------------------------------------
| Base Container VIEW
|--------------------------------------------------------------------------
|
| Responsible for fields initialization and rendering.
| Updates the container DOM (visibility, errors, etc..).
| The app will fallback to this class if a container has no dedicated view.
|
| Views reflect what the applications data models look like.
| They also listen to events and react accordingly.
|
| @element: .container-[id]
| @holder: carbon.views[id]
|
*/
carbon.containers.View = Backbone.View.extend({
/*
* Set the view DOM events
*/
events: {
"click ul.carbon-tabs-nav a": "switchTab",
"switchToTab .carbon-tab": "switchTab"
},
/*
* Used to include additional variables that can be used inside the template
* Can be extended on the "field:beforeRender" event.
*/
templateVariables: {},
initialize: function() {
this.fieldsCollection = new carbon.fields.Collection(this.model.get('fields'));
// Listen for adding fields in the collection and render them
this.listenTo(this.fieldsCollection, 'add', this.renderField);
// Listen for model changes in the fields collection.
this.listenToOnce(this.fieldsCollection, 'change:value', this.changeListener);
// Disable/enable the container's inputs when visibility changes
this.listenTo(this.model, 'change:visible', this.disableInputs);
// Listen for container class updates
this.listenTo(this.model, 'change:classes', this.updateClass);
// Check the container visibility before it's rendered.
// Containers are visible by default. Overwrite the checkVisibility method for custom logic.
this.on('container:beforeRender', this.checkVisibility);
// Initialize fields rendering after the container has rendered
this.on('container:rendered', this.afterRenderInit);
// Handle tab setup
this.on('container:rendered', this.setupTabs);
// Propagate an event to all fields in this container
this.on('propagate', function(event) {
this.eventPropagator( this.fieldsCollection, event );
});
// Wrap fields that should be on one row
this.on('layoutUpdated', this.addFieldRows);
// Check for not saved changes on "onbeforeunload"
this.on('container:rendered form:invalid', this.onSaveAlert);
this.on('container:rendered form:invalid', this.showFirstHightlithedFieldTab);
this.on('form:valid', this.resetOnBeforeUnload);
// Listen to visibility change
this.listenTo(this.model, 'change:visible', this.toggleVisibility);
// Initial visibility check
this.toggleVisibility(this.model);
},
changeListener: function(model, collection) {
this.model.set('has_changes', true);
},
checkVisibility: function() {
this.model.set('visible', true);
},
toggleVisibility: function(model) {
var id = model.get('id');
var visible = model.get('visible');
var $holder = carbon.views.main.$body.find('#' + id);
$holder.toggle(visible);
},
eventPropagator: function(collection, event) {
var collection = collection.toJSON();
for (var i = 0; i < collection.length; i++) {
var fieldId = collection[i].id;
var view = carbon.views[fieldId];
if ( !_.isUndefined(view) ) {
view.trigger(event.type, event);
view.trigger('propagate', event);
}
}
},
addFieldRows: function() {
var $fields = this.$(':not(.carbon-fields-row) > .carbon-field.has-width:not(.carbon-subrow)');
var $subfields = this.$(':not(.carbon-fields-row) > .carbon-field.has-width.carbon-subrow:not(.carbon-Complex)');
var $complex_groups = this.$(':not(.carbon-fields-row) > .carbon-field.has-width.carbon-subrow.carbon-Complex');
var fields_groups = new Array($fields, $subfields, $complex_groups);
var $group = $();
var groupWidth = 0;
var wrapGroup = function() {
if ($group.length > 0) {
$group.wrapAll('
');
$group = $();
groupWidth = 0;
}
}
for ( i=0; i 100) {
wrapGroup();
}
groupWidth += width;
$group = $group.add($(this));
if (!$(this).next().hasClass('has-width') ) {
wrapGroup();
}
});
}
},
disableInputs: function(model) {
if ( ! this.$el.is('fieldset') ) {
return;
}
var disabled = !model.get('visible');
this.$el.attr('disabled', disabled);
},
updateClass: function(model) {
var classes = model.get('classes');
this.$el.closest('.carbon-container').addClass(classes.join(' '));
},
showFirstHightlithedFieldTab: function() {
this.$el.find('.carbon-highlight').first().closest('.carbon-tab').trigger('switchToTab');
},
onSaveAlert: function() {
var _this = this;
var oldCallback = window.onbeforeunload || $.noop;
window.onbeforeunload = function() {
var hasChanges = _this.model.get('has_changes');
var postL10n = postL10n || {};
var alert = postL10n.saveAlert ? postL10n.saveAlert : carbon_containers_l10n.changes_made_save_alert;
if (hasChanges && alert) {
return alert;
}
return oldCallback();
}
},
resetOnBeforeUnload: function() {
window.onbeforeunload = null;
},
render: function() {
var type = this.model.get('type');
var tabsTemplate = carbon.template('tabs');
var template = carbon.template(type);
var settings = this.model.attributes.settings;
var fields = this.fieldsCollection.toJSON();
var tabs = {};
var templateVariables, containerHTML;
this.trigger('container:beforeRender');
if (settings.tabs) {
_.each(settings.tabs, function (fieldNames, tab) {
var tabFields = [];
var field;
for (var i = 0; i < fieldNames.length; i++) {
for (var j = 0; j < fields.length; j++) {
field = fields[j];
if (fieldNames[i] === field.name) {
tabFields.push(field)
}
}
}
tabs[tab] = {
id: tab.toLowerCase().replace(/[^\w]+/g, '-'),
fields: tabFields
};
});
_.map(tabs, function (tab) {
templateVariables = _.extend(this.templateVariables, this.model.attributes, {
container: this.model,
fields: tab.fields
});
tab.html = template(templateVariables);
return tab;
}, this);
containerHTML = tabsTemplate({
tabs: tabs
});
} else {
this.templateVariables = _.extend(this.templateVariables, this.model.attributes, {
container: this.model,
fields: fields
});
containerHTML = template(this.templateVariables);
}
this.$el.html(containerHTML);
this.trigger('container:rendered');
return this;
},
afterRenderInit: function() {
var _this = this;
// Trigger the add event on the collection, this should initialize the fields rendering
this.fieldsCollection.each(function(model) {
_this.fieldsCollection.trigger('add', model);
});
},
setupTabs: function() {
var $tabsContainer = this.$el.find('.carbon-tabs');
if ($tabsContainer.length === 0) {
// this is not a tabbed container, ignore
return;
}
var topPositions = this.$('.carbon-tabs-nav li').map(function() {
return $(this).offset().top;
});
if(_.uniq(topPositions).length > 1) {
$tabsContainer.addClass('carbon-tabs-stacked');
}
// Open the first tab, if none is open yet.
if ( ! $tabsContainer.find('ul.carbon-tabs-nav li.active').length ) {
$tabsContainer.find('ul.carbon-tabs-nav a:first').trigger('click');
}
},
switchTab: function (e) {
var $element = $(e.target);
var $li, activeTabIndex;
if ($element.is('a')) {
$li = $(e.target).closest('li');
activeTabIndex = $li.index();
} else if ($element.is('.carbon-tab')) {
activeTabIndex = $element.index();
$li = $element.closest('.carbon-tabs').find('.carbon-tabs-nav li:eq(' + activeTabIndex + ')');
}
var $tabs = $('.carbon-tab', this.$el);
$tabs
.removeClass('active')
.eq(activeTabIndex).addClass('active');
$li.addClass('active').siblings().removeClass('active');
e.preventDefault();
},
// Render a field when its added in the fieldsCollection
renderField: function(model) {
var _this = this;
var type = model.get('type');
var id = model.get('id');
var FieldView = carbon.fields.View[type];
if ( _.isUndefined(FieldView) ) {
FieldView = carbon.fields.View; // Fallback to the base view
}
carbon.views[id] = new FieldView({
el: '.' + id,
model: model
});
carbon.views[id].on('field:rendered layoutUpdated', function() {
_this.trigger('layoutUpdated');
});
carbon.views[id].render();
},
validateForm: function(event) {
var _this = event.data; // the view object
var $target = $(event.currentTarget);
var $errorHolder = $('.carbon-error-required');
var $spinner = $target.find('#publishing-action .spinner');
var valid = _this.model.isValid(); // this method will also set the validationError
var errorText = _this.model.validationError;
$spinner.addClass('is-active');
if (valid) { // valid
_this.trigger('form:valid');
if (event.type === 'click') {
$errorHolder.slideUp(function() {
$(this).remove();
});
}
} else { // invalid
_this.trigger('form:invalid');
$spinner.addClass('disabled');
if (errorText) {
if ($errorHolder.length) {
$errorHolder.find('strong').text(errorText);
} else {
$errorHolder = $('');
$errorHolder.insertAfter('#wpbody-content > .wrap > h2').slideDown();
}
var $firstErrorField = $('.carbon-highlight :input:first');
// Expand the post meta box if it's closed
$firstErrorField.closest('.postbox').removeClass('closed');
// Focus the first error field.
$firstErrorField.focus();
}
setTimeout(function() {
if ($target.is('#post')) {
$target.find('#publish').removeClass('button-primary-disabled button-disabled disabled');
$target.find('#ajax-loading, #publishing-action .spinner').attr('style','');
}
$spinner.removeClass('disabled');
$spinner.removeClass('is-active');
}, 0);
if (event.type === 'click') {
event.stopImmediatePropagation();
}
event.preventDefault();
}
}
});
/*
|--------------------------------------------------------------------------
| Base Container COLLECTION
|--------------------------------------------------------------------------
|
| Holds a set of container models.
| Also includes model class initialization logic.
|
*/
carbon.containers.Collection = Backbone.Collection.extend({
model: function(attrs, options) {
var ContainerModel = carbon.containers.Model[attrs.type];
// Set the container model. If the model is not found, fallback to the base model
if ( _.isUndefined(ContainerModel) ) {
ContainerModel = carbon.containers.Model; // Fallback to the base model
}
return new ContainerModel(attrs, options);
}
});
/******************************** BASE END ********************************/
/*--------------------------------------------------------------------------
* CUSTOM FIELDS
*------------------------------------------------------------------------*/
// Post_Meta MODEL
carbon.containers.Model.Post_Meta = carbon.containers.Model.extend({
defaults: {
'page_template': 'default',
'level': 1,
'parent_id': null,
'post_format': null,
'terms': []
}
});
// Post_Meta VIEW
carbon.containers.View.Post_Meta = carbon.containers.View.extend({
initialize: function() {
carbon.containers.View.prototype.initialize.apply(this);
this.$form = this.$el.closest('form#post');
this.$form.on('submit', null, this, this.validateForm);
this.syncTemplate();
this.syncParent();
this.syncLevel();
this.syncPostFormat();
this.syncTaxonomy();
this.listenTo(this.model, 'change:page_template change:parent_id change:level change:post_format change:terms', this.checkVisibility);
},
syncTemplate: function() {
var _this = this;
var $select = $('select#page_template');
$select
.on('change', function(event) {
var template = $(this).val();
_this.model.set('page_template', template);
})
.trigger('change');
},
syncParent: function() {
var _this = this;
var $select = $('select#parent_id');
$select
.on('change', function(event) {
var parentId = parseInt($(this).val());
_this.model.set('parent_id', parentId);
})
.trigger('change');
},
syncLevel: function() {
var _this = this;
var $select = $('select#parent_id');
$select
.on('change', function(event) {
var levelClass = $(this).find('option:checked').attr('class');
var level = levelClass ? parseInt(levelClass.match(/^level-(\d+)/)[1]) + 2: 1;
_this.model.set('level', level);
})
.trigger('change');
},
syncPostFormat: function() {
var _this = this;
var $radio = $('input[name="post_format"]');
$radio
.on('change', function(event) {
var checked = $(this).is(':checked');
var postFormat = $(this).val();
if (checked) {
_this.model.set('post_format', postFormat);
}
})
.trigger('change');
},
syncTaxonomy: function() {
var _this = this;
var settings = this.model.get('settings');
var taxonomy = settings.show_on.tax_slug;
var termId = settings.show_on.tax_term_id;
var $holder = $('#taxonomy-' + taxonomy);
var $input = $('#' + taxonomy + 'checklist input');
if (!taxonomy || !termId || !$holder.length) {
return false;
}
$holder.on('change', $input.selector, function(event) {
var checked = $(this).is(':checked');
var termId = parseInt($(this).val());
var terms = _this.model.get('terms');
if (checked) {
terms.push(termId);
} else {
var index = terms.indexOf(termId);
if (index !== -1) {
terms.splice(index, 1);
}
}
terms = _.uniq(terms);
_this.model.set('terms', terms);
_this.model.trigger('change:terms');
});
$input.trigger('change')
},
checkVisibility: function(model) {
var _this = this;
var settings = this.model.get('settings');
var visible = true;
_.each(settings.show_on, function(req, type) {
if ( !req || ( _.isArray(req) && _.isEmpty(req) ) ) {
return;
}
switch(type) {
case 'template_names':
var template = _this.model.get('page_template');
var isPage = typeof typenow !== 'undefined' && typenow === 'page';
if (isPage && $.inArray(template, req) === -1) {
visible = false;
}
break;
case 'not_in_template_names':
var template = _this.model.get('page_template');
var isPage = typeof typenow !== 'undefined' && typenow === 'page';
if (isPage && $.inArray(template, req) !== -1) {
visible = false;
}
break;
case 'parent_page_id':
var parentId = _this.model.get('parent_id');
if (parentId != req) {
visible = false;
}
break;
case 'level_limit':
var level = _this.model.get('level');
if (level != req) {
visible = false;
}
break;
case 'post_formats':
var post_format = _this.model.get('post_format');
if ($.inArray(post_format, req) === -1) {
visible = false;
}
break;
case 'tax_slug':
var terms = _this.model.get('terms');
var termId = settings.show_on.tax_term_id;
if ($.inArray(termId, terms) === -1) {
visible = false;
}
break;
}
});
this.model.set('visible', visible);
}
});
/*--------------------------------------------------------------------------
* COMMENTS META
*------------------------------------------------------------------------*/
// Comment_Meta VIEW
carbon.containers.View.Comment_Meta = carbon.containers.View.extend({
initialize: function() {
carbon.containers.View.prototype.initialize.apply(this);
this.$form = this.$el.closest('form#post');
this.$form.on('submit', null, this, this.validateForm);
},
});
/*--------------------------------------------------------------------------
* THEME OPTIONS
*------------------------------------------------------------------------*/
// Theme_Options VIEW
carbon.containers.View.Theme_Options = carbon.containers.View.extend({
initialize: function() {
carbon.containers.View.prototype.initialize.apply(this);
var _this = this;
this.$form = this.$el.closest('form#theme-options-form');
this.$form.on('submit', null, this, this.validateForm);
$(window).on('scroll', function() {
_this.positionActionPanel.apply(_this);
});
},
positionActionPanel: function() {
var $panel = $('#postbox-container-1');
var topOffset = $('#wpadminbar').height() + 10;
var top = this.$el.offset().top - topOffset;
var scrollTop = $(window).scrollTop();
if (scrollTop >= top) {
$panel.addClass('fixed').css('top', topOffset);
} else {
$panel.removeClass('fixed');
}
},
setupTabs: function() {
var $tabsContainer = this.$el.find('.carbon-tabs');
if ($tabsContainer.length === 0) {
// this is not a tabbed container, ignore
return;
}
var $tabLinks = $tabsContainer.find('ul.carbon-tabs-nav a');
var currentTabString = window.location.hash.replace(/^#/, '');
// Retrieve the current tab
var $currentTabLink = $tabLinks.filter(function() {
return '!' + $(this).data('id') === currentTabString;
}).eq(0);
// If there is no current tab, use the first one
if ( ! $currentTabLink.length ) {
var $currentTabLink = $tabLinks.eq(0);
}
// Open the current tab
$currentTabLink.trigger('click');
carbon.containers.View.prototype.setupTabs.apply(this);
},
switchTab: function (e) {
carbon.containers.View.prototype.switchTab.apply(this, [e]);
var $element = $(e.target);
var $li;
if ($element.is('a')) {
$li = $element.closest('li');
} else if ($element.is('.carbon-tab')) {
var activeTabIndex = $element.index();
$li = $element.closest('.carbon-tabs').find('.carbon-tabs-nav li:eq(' + activeTabIndex + ')');
}
window.location.hash = '!' + $li.find('a:eq(0)').data('id');
}
});
/*--------------------------------------------------------------------------
* TERM META
*------------------------------------------------------------------------*/
// Term_Meta MODEL
carbon.containers.Model.Term_Meta = carbon.containers.Model.extend({
defaults: {
'level': 0,
}
});
// Term_Meta VIEW
carbon.containers.View.Term_Meta = carbon.containers.View.extend({
initialize: function() {
carbon.containers.View.prototype.initialize.apply(this);
var _this = this;
/* Edit Form */
this.$editForm = this.$el.closest('form#edittag');
this.$editForm.on('submit', null, this, this.validateForm);
/* Add Form */
this.$addForm = this.$el.closest('form#addtag');
this.$submitButton = this.$addForm.find('#submit');
// Fields validation should run before WP validation.
// The 'bindFirst'custom method is used to move our event at the top of the events stack.
// This is required because WP stops the click event propagation.
this.$submitButton.bindFirst('click', this, this.validateForm);
/* Ajax Monitor */
carbon.views.main.$el.ajaxSuccess(function() {
_this.initMonitor.apply(_this, arguments);
});
this.syncLevel();
this.listenTo(this.model, 'change:level', this.checkVisibility);
},
initMonitor: function(event, jqXHR, ajaxOptions) {
if (jqXHR.status != 200 || !ajaxOptions.data.length) {
return;
}
var id = this.model.get('id');
if (ajaxOptions.data.indexOf('carbon_panel_' + id) !== -1) {
carbon.collections.containers.reset();
}
},
syncLevel: function() {
var _this = this;
var $select = $('select#parent');
$select
.on('change', function(event) {
var levelClass = $(this).find('option:checked').attr('class');
var level = levelClass ? parseInt(levelClass.match(/^level-(\d+)/)[1]) + 2 : 1;
_this.model.set('level', level);
})
.trigger('change');
},
checkVisibility: function(model) {
var _this = this;
var settings = this.model.get('settings');
var visible = true;
var level = _this.model.get('level');
var term_container_level = settings.show_on_level;
if ( !term_container_level ) {
visible = true;
} else if ( level !== term_container_level ) {
visible = false;
}
this.model.set('visible', visible);
},
});
/*--------------------------------------------------------------------------
* USER META
*------------------------------------------------------------------------*/
// User_Meta MODEL
carbon.containers.Model.User_Meta = carbon.containers.Model.extend({
defaults: {
'role': null
}
});
// User_Meta VIEW
carbon.containers.View.User_Meta = carbon.containers.View.extend({
initialize: function() {
carbon.containers.View.prototype.initialize.apply(this);
this.$form = this.$el.closest('form#your-profile, form#createuser');
this.$form.on('submit', null, this, this.validateForm);
this.syncRole();
this.listenTo(this.model, 'change:role', this.checkVisibility);
},
syncRole: function() {
var _this = this;
var $select = $('select#role');
var profileRole = this.$el.data('profile-role');
this.model.set('role', profileRole);
$select.on('change', function(event) {
var role = $(this).val();
_this.model.set('role', role);
});
},
checkVisibility: function(model) {
var _this = this;
var settings = this.model.get('settings');
var profileRole = this.model.get('role');
var roles = settings.show_on.role || [];
var visible = true;
if (roles.length && $.inArray(profileRole, roles) === -1) {
visible = false;
}
this.model.set('visible', visible);
}
});
/*--------------------------------------------------------------------------
* WIDGETS
*------------------------------------------------------------------------*/
// Widget MODEL
carbon.containers.Model.Widget = carbon.containers.Model.extend({
initialize: function() {
carbon.containers.Model.prototype.initialize.apply(this);
var cid = this.cid;
var fields = this.get('fields');
_.each(fields, function(field) {
field.id = field.id + '-' + cid;
field.lazyload = false; // disable lazyloading in widgets
});
this.set('fields', fields);
}
});
// Widget VIEW
carbon.containers.View.Widget = carbon.containers.View.extend({
initialize: function() {
carbon.containers.View.prototype.initialize.apply(this);
this.$submitButton = this.$el.closest('form').find('input[type="submit"]');
this.$submitButton
.off('click', this.validateForm)
.on('click', null, this, this.validateForm);
}
});
}(jQuery));