app/assets/javascripts/slices/app/slices.js
// Slices.js
// (c) 2011 With Associates
// http://slices.withassociates.com/
var slices = {
defaultSettings: {
mainTemplate: '_page_main',
metaTemplate: '_page_meta',
templatesPath: '/slices/templates/',
loadPagePath: '/admin/pages/{{id}}.json',
savePagePath: '/admin/pages/{{id}}.json'
},
templates: {},
availableSlices: {},
availableContainers: {},
model: {},
fieldId: function(context, field) {
// If `this` is the current page, then we’ll use a `meta-{{field}}` ID.
if (context === slices.model.Page.data()) {
return ['meta', field].join('-');
// If this has an id, we’re dealing with a slice, so we’ll use a `slices-{{id}}-{{field}}` ID.
} else if (context.hasOwnProperty('id')) {
return ['slices', context.id, field].join('-');
// Otherwise, we just need a unique id.
} else {
slices.__uid__ = slices.__uid__ || 0;
return 'field-' + ++slices.__uid__;
}
},
controller: function SlicesController() {
// private vars
var settings,
busy = true;
// private methods
function init(pageId, passed_settings) {
settings = $.extend({}, slices.defaultSettings, passed_settings);
observeFormEvents();
slices.model.Page.init(settings);
slices.model.Page.id(pageId);
slices.availableContainers = settings.availableContainers;
slices.availableSlices = settings.availableSlices;
slices.i18n = settings.i18n;
addSliceOptions(slices.availableSlices);
templates = _.flatten([
'slice',
settings.mainTemplate,
settings.metaTemplate,
settings.mainExtraTemplates,
settings.metaExtraTemplates
]);
loadSliceTemplates(pageId);
loadTemplates(templates, pageId, loadPageModel);
};
function addSliceOptions(slices) {
var select = $('#add-slice-fields select');
select.find('option:not(:disabled)').remove();
_.each(slices, function (slice, machineName) {
if (!restrictedAndNotSuper(slice)) {
select.append(
'<option value="' + machineName + '">' + slice.name + '</option>'
);
}
});
};
function templateUrl(name, pageId) {
name = name.split('{{id}}').join(pageId);
if (name.indexOf('/') === 0) {
return name + '.hbs';
} else {
return settings.templatesPath + name + '.hbs';
}
};
function loadSliceTemplates(pageId) {
_.each(slices.availableSlices, function(slice, name) {
$.ajax({
url: templateUrl(slice.template, pageId),
success: function(raw_template, textStatus, XMLHttpRequest) {
slices.templates[slice.template] = Handlebars.compile(raw_template);
}
});
})
};
function loadTemplates(templates, pageId, callback) {
templates = _.compact(templates);
var numTemplates = templates.length;
_.each(templates, function (templateName) {
$.ajax({
url: templateUrl(templateName, pageId),
success: function (raw_template, textStatus, XMLHttpRequest) {
slices.templates[templateName] = Handlebars.compile(raw_template);
if (--numTemplates == 0) callback();
}
});
});
};
function loadPageModel() {
slices.model.Page.load(onPageLoaded);
};
function addSliceTemplate(slice) {
var templateData = {},
sliceBlock,
sliceContent,
sliceContentTemplate = slices.availableSlices[slice.type].template,
isOpen = false;
var sliceBlock = $(
slices.templates['slice']({
id: slice.id,
name: slice.name,
css_class: slice.type + '-slice',
content: function () {
return slices.templates[sliceContentTemplate](slice)
}
})
);
var sliceContent = sliceBlock.find('.slice-content'),
controlBar = sliceBlock.find('.control-bar'),
preview = sliceBlock.find('.slice-preview');
if (_.isFunction(window.customSlicePreview)) {
var slicePreviewHelper = window.customSlicePreview;
delete window.customSlicePreview;
} else {
var slicePreviewHelper = slices.defaultSlicePreview;
}
var updateSlicePreview = function() {
preview.html(slicePreviewHelper.call(sliceContent));
}
updateSlicePreview();
controlBar.on('click', function () {
var closed = sliceBlock.is('.closed');
if (closed) {
sliceContent.slideDown('fast');
sliceBlock.removeClass('closed');
} else {
updateSlicePreview();
sliceContent.slideUp('fast');
sliceBlock.addClass('closed');
}
updateMinimiseAllSlices();
return false;
});
sliceBlock.find('a.sort').on('click', false);
if (restrictedAndNotSuper(slice)) {
sliceBlock.find('a.delete').remove();
} else {
sliceBlock.find('a.delete').on('click', function () {
slice._destroy = 1;
sliceBlock.slideUp('fast');
sliceBlock.trigger('change');
enableSaveButton();
return false;
});
}
$('#container-' + slice.container + '>ul').append(sliceBlock);
sliceContent.applyDataValues();
sliceContent.initDataPlugins();
enableContainerSelect(slice, sliceBlock);
}
function onPageLoaded() {
var tabControls = $('#container-tab-controls'),
containersHolder = $('#containers-holder').empty(),
addSliceFields = $('#add-slice-fields');
initMeta();
_.each(slices.availableContainers, function (container, machineName) {
// Create ourselves a tab button.
var tabButton = $(
'<a href="#container-' + machineName + '">' +
container.name +
'</a>'
);
if (container.primary) tabButton.addClass('primary');
container.availableSlices = _.clone(slices.availableSlices);
// Filter available slices to those specified in the layout
if (container.only) {
_.each(slices.availableSlices, function(slice, machineName) {
if (!_.contains(container.only, machineName)) {
delete container.availableSlices[machineName];
}
});
}
if (container.except) {
_.each(slices.availableSlices, function(slice, machineName) {
if (_.contains(container.except, machineName)) {
delete container.availableSlices[machineName];
}
});
}
tabButton.data('container', container);
// Bind the tab button click event.
tabButton.on('click', function() {
var tab = $(this);
var container = tab.data('container');
tabControls.find('a').removeClass('active');
containersHolder.find('>div').hide();
$('#container-' + machineName).show();
$(this).addClass('active');
updateMinimiseAllSlices();
addSliceOptions(container.availableSlices);
return false;
});
// Append our tab button (in an li) to the tab controls container.
tabControls.append($('<li />').append(tabButton));
// Create ourselves a container, holder and adder.
var container = $('<div id="container-' + machineName + '" class="container" />');
var holder = $('<ul class="slices-holder" />');
var adder = addSliceFields.clone();
// Remove redundant id attribute on our cloned adder.
// adder.attr({ id: '' });
// Bind the behaviour of our slice selector.
adder.find('select').bind('change', function() {
var sliceType = $(this).val();
if (sliceType === '') return false;
addNewSlice(
machineName,
$(this).val(),
$('#container-' + machineName + ' ul li').length
);
$('#container-' + machineName + ' ul').trigger('sortstop');
$(this).find('option:selected').removeAttr('selected');
$(this).find('option:disabled').attr('selected', 'selected');
return false;
});
// Glue everything together.
container.append(holder, adder);
containersHolder.append(container);
});
addSliceFields.remove();
tabControls.find('a:first, a.primary').trigger('click');
addSliceContainers();
addMinimiseAllSlices();
if ($('#page-meta').children().length > 0) {
$('<div id="show-meta"><a href="#">advanced options…</a></div>').
insertAfter('#page-meta').
toggle(function() {
$('#page-meta').slideDown('fast');
$(this).html('<a href="#">hide advanced options</a>');
$(this).addClass('open');
$(document).trigger('slices:didShowAdvancedOptions');
}, function() {
$('#page-meta').slideUp('fast');
$(this).html('<a href="#">advanced options…</a>');
$(this).removeClass('open');
$(document).trigger('slices:didHideAdvancedOptions');
});
if (slices.model.Page.seemsNew()) $('#show-meta').trigger('click');
}
busy = false;
}
function initMeta() {
$('#page-main').html(renderMetaFields(settings.mainTemplate));
$('#page-meta').html(renderMetaFields(settings.metaTemplate));
$('#page-extra-main').empty();
$.each(settings.metaExtraTemplates, function(i, template) {
$('#page-meta').append(renderMetaFields(template));
});
$.each(settings.mainExtraTemplates, function(i, template) {
$('#page-extra-main').append(renderMetaFields(template));
});
$('#page-meta-fields').applyDataValues().initDataPlugins();
Tagging.detect();
}
function renderMetaFields(name) {
var template = slices.templates[name],
page = slices.model.Page,
data = page.data();
return template(data);
}
function addNewSlice(container, type, pos) {
var slice = _.extend({}, slices.availableSlices[type], {
type: type,
container: container,
position: pos
});
slices.model.Page.addSlice(slice);
addSliceTemplate(slice);
}
function addSliceContainers() {
_.each(slices.availableContainers, function (container, machineName) {
$('#container-' + machineName + ' ul').empty();
});
_.each(slices.availableContainers, function (container, machineName) {
var container_slices = slices.model.Page.getSlices(machineName);
container_slices = _.map(container_slices, function(slice) {
return $.extend(true, {},
slices.availableSlices[slice.type],
slice
);
});
slices.model.Page.setSlices(machineName, container_slices);
_.each(container_slices, addSliceTemplate);
var ul = $('#container-' + machineName + '>ul');
ul.sortable({
handle: '.control-bar',
scroll: false,
beforeStart: function() {
ul.freezeHeight();
window.autoscroll.start();
},
stop: function() {
window.autoscroll.stop();
ul.thawHeight();
}
});
ul.on('sortstop', function (event, ui) {
ul.find('>li').each(function (i) {
slices.model.Page.positionSlice($(this).attr('rel'), i);
});
onReorder();
});
ul.trigger('sortstop');
});
}
function addMinimiseAllSlices(){
var minimise = $('#minimise');
minimise.text('Minimise all slices');
minimise.on('click', function () {
if (minimise.text().indexOf('Minimise') != -1){
$('.container:visible .slice:not(.closed) .control-bar').click();
} else {
$('.container:visible .slice.closed .control-bar').click();
}
return false;
})
}
function updateMinimiseAllSlices() {
var slices = $('.container:visible .slice');
if (slices.length === 0) return;
var closed = slices.filter('.closed'),
allClosed = closed.length === slices.length,
allOpen = closed.length === 0;
if (allClosed) {
$('#minimise').text('Expand all slices');
} else if (allOpen) {
$('#minimise').text('Minimise all slices');
}
}
function getArrayInputValuesFor(key) {
var result = [];
$('input[name="meta-' + key + '"]').each(function() {
var input = $(this);
switch (input.attr('type')) {
case 'checkbox':
if (input.is(':checked')) result.push(input.val());
break;
default:
result.push(input.val());
break;
}
});
return result;
}
function updateMeta() {
var model = slices.model.Page,
inputs = $('[id^="meta-"]');
inputs.each(function() {
var input = $(this),
id = input.attr('id'),
key = id.match(/meta-(.+)/)[1],
oldValue = model.getMeta(key),
newValue = slices.getValueForId(id);
if (newValue === undefined || newValue == oldValue) return;
model.setMeta(key, newValue);
model.changed = true;
});
}
function updateSlices() {
var model = slices.model.Page;
_.each(model.slices(), function(slice) {
var prefix = 'slices-' + slice.id + '-',
inputs = $('[id^="' + prefix + '"]');
inputs.each(function() {
var input = $(this),
id = input.attr('id'),
key = id.substr(prefix.length),
oldValue = slice[key],
newValue = slices.getValueForId(id);
if (newValue === undefined || newValue == oldValue) return;
slice[key] = newValue;
model.changed = true;
});
});
}
function updateModel() {
var model = slices.model.Page;
model.changed = false;
updateMeta()
updateSlices();
if (model.changed) enableSaveButton();
}
function observeFormEvents() {
$('#slices-form')
.on('submit', onFormSubmitted)
.on('change', onChange)
.on('keyup', 'input, textarea', _.debounce(function() {
$(this).trigger('change');
}, 100));
}
function onChange() {
if (!busy) updateModel();
}
function onReorder() {
if (!busy) enableSaveButton();
}
function onFormSubmitted(event) {
event.preventDefault();
busy = true;
$(document).trigger('slices:willSubmit');
updateModel();
disableSaveButton();
$('.error-message').remove();
$('.field-with-errors').removeClass('field-with-errors');
$('#container').freezeHeight();
// Save it...
slices.model.Page.save()
.done(function() {
initMeta();
addSliceContainers();
updateViewLink();
updateBreadcrumbs();
})
.fail(function(errors) {
_.each(errors, function(value, key) {
if (key == 'slices') {
value = _.flatten([value])[0];
_.each(value, function(fields, sliceId) {
_.each(fields, function(errorMsgs, fieldName) {
var inpId = ['slices', sliceId, fieldName].join('-'),
input = $('#' + inpId);
input.closest('li').addClass('field-with-errors');
var errorMsg = _.flatten([errorMsgs]).join(', ');
input.after(
$('<div class="error-message" />').text(
$('label[for=' + inpId + ']').text() + ' ' + errorMsg
)
);
var containerId = input.closest('.container').attr('id');
$('#container-tab-controls a[href=#' + containerId + ']')
.addClass('field-with-errors');
});
});
} else {
var li = $('#meta-' + key).closest('li');
li
.addClass('field-with-errors')
.append(
$('<div class="error-message" />').text(
li.find('.label').text() + ' ' + value
)
);
}
});
}).
always(function() {
disableSaveButton();
updateMinimiseAllSlices();
busy = false;
_.defer(function() { $('#container').thawHeight() });
});
}
function restrictedAndNotSuper(slice) {
return (slice.restricted && !settings.super_user);
}
// Rig-up the little container select widget in the control bar.
// This is not the prettiest, but you don't need me to tell you that,
// you can just look below.
function enableContainerSelect(slice, sliceBlock) {
var potentialContainers = {};
_.each(slices.availableContainers, function(container, machineName) {
if (container.availableSlices.hasOwnProperty(slice.type)) {
potentialContainers[machineName] = container;
}
});
if (Object.keys(potentialContainers).length <= 1) {
sliceBlock.find('.container-select').remove();
return;
}
sliceBlock.find('.container-select').each(function() {
var select = $(this);
// Add the options
select.append('<option value="default" selected disabled>Move to another container</option>');
_.each(potentialContainers, function(container, machineName) {
select.append('<option value="' + machineName + '">' + container.name + '</option>');
});
// On change, we'll move our slice to the selected container
// and notify all the appropriate objects that things have changed.
select.bind('change', function() {
// Attach to the new container, both in data and dom.
var newMachineName = select.val();
var newContainer = $('#container-' + newMachineName + ' > ul');
slice.container = newMachineName;
sliceBlock.detach().appendTo(newContainer);
// Reset the widget.
select.val('default');
// Make sure slice positions are accurate.
newContainer.find('> li').each(function(i) {
slices.model.Page.positionSlice($(this).attr('rel'), i);
});
// Fire re-order hooks.
onReorder();
});
});
}
// Enables/disables the 'Save changes' button.
function enableSaveButton() {
$('#save-changes').attr('disabled', false);
}
function disableSaveButton() {
$('#save-changes').attr('disabled', true);
}
function updateViewLink() {
$('#page-view-on-site').attr('href', slices.model.Page.data().path);
}
function updateBreadcrumbs() {
$('#breadcrumbs .current a').html(slices.model.Page.data().name);
}
// public API
return {
init: function (pageId, settings) {
return init(pageId, settings);
}
}
}()
};