apostrophe
Version:
Apostrophe is a user-friendly content management system. You'll need more than this core module. See apostrophenow.org to get started.
947 lines (846 loc) • 34 kB
JavaScript
/* global rangy, $, _ */
/* global alert, confirm, prompt, AposWidgetEditor, apos */
function AposSlideshowWidgetEditor(options)
{
var self = this;
var $items;
if (self.fileGroup === undefined) {
self.fileGroup = 'images';
}
var showImages = (options.showImages === undefined) ? true : options.showImages;
// Options passed from template or other context
var templateOptions = options.options || {};
if (templateOptions.group) {
self.fileGroup = templateOptions.group;
}
var widgetClass = templateOptions.widgetClass;
var aspectRatio = templateOptions.aspectRatio;
var setSelect = templateOptions.setSelect;
var minSize = templateOptions.minSize;
var limit = templateOptions.limit;
var extraFields = templateOptions.extraFields;
var liveItem = '[data-item]:not(.apos-template)';
var userOptions = templateOptions.userOptions || {};
if (userOptions) {
var orientation = userOptions.orientation || false;
}
if (!options.messages) {
options.messages = {};
}
if (!options.messages.missing) {
options.messages.missing = 'Upload an image file first.';
}
if (!options.alwaysExtraFields) {
options.alwaysExtraFields = false;
}
if(!options.type) {
self.type = 'slideshow';
} else {
self.type = options.type;
}
if(!options.template) {
options.template = '.apos-slideshow-editor';
}
// Calls to self.busy are cumulative, so we can figure out when
// all uploads have stopped
self._busy = 0;
// Parent class constructor shared by all widget editors
AposWidgetEditor.call(self, options);
// Override methods for this subclass
self.busy = function(state) {
apos.busy(self.$el, state);
};
// Confirm before cancelling the slideshow widget as it can contain
// large amounts of new work that are lost forever. TODO: stop using
// "confirm" once we have a nicer confirmation modal in A2. Find a
// good way to avoid being a nag if no work has really been done yet.
self.beforeCancel = function(callback) {
if (confirm('Are you sure you want to cancel your edits to this slideshow?')) {
$('.apos-slideshow-editor-item--info').css('display', 'none');
return callback(null);
} else {
return callback('canceled');
}
};
// Our current thinking is that preview is redundant for slideshows.
// Another approach would be to make it much smaller. We might want that
// once we start letting people switch arrows and titles and descriptions
// on and off and so forth. Make sure we still invoke prePreview
self.preview = function() {
self.prePreview(function() { });
};
self.afterCreatingEl = function() {
$items = self.$el.find('[data-items]');
$items.sortable({
update: function(event, ui) {
reflect();
self.preview();
}
});
self.files = [];
self.$showTitles = self.$el.findByName('showTitles');
self.$showDescriptions = self.$el.findByName('showDescriptions');
self.$showCredits = self.$el.findByName('showCredits');
self.$showTitles.val(self.data.showTitles ? '1' : '0');
self.$showDescriptions.val(self.data.showDescriptions ? '1' : '0');
self.$showCredits.val(self.data.showCredits ? '1' : '0');
if (userOptions.disableTitles) {
self.$el.find('[data-name="showTitles"]').hide();
}
if (userOptions.disableDescriptions) {
self.$el.find('[data-name="showDescriptions"]').hide();
}
if (userOptions.disableCredits) {
self.$el.find('[data-name="showCredits"]').hide();
}
var $uploader = self.$el.find('[data-uploader]');
$uploader.fileupload({
dataType: 'json',
dropZone: self.$el.find('.apos-ui-modal-body'),
// This is nice in a multiuser scenario, it prevents slamming,
// but I need to figure out why it's necessary to avoid issues
// with node-imagemagick
sequentialUploads: true,
start: function (e) {
self.busy(true);
},
// Even on an error we should note we're not spinning anymore
always: function (e, data) {
self.busy(false);
},
// This is not the same thing as really being ready to work with the files,
// so wait for 'done'
// stop: function (e) {
// },
// Progress percentages are just misleading due to image rendering time,
// so just show a spinner
// progressall: function (e, data) {
// var progress = parseInt(data.loaded / data.total * 100, 10);
// self.$el.find('[data-progress-percentage]').text(progress);
// },
done: function (e, data) {
if (data.result.files) {
_.each(data.result.files, function (file) {
addItem(file);
annotateItem(file);
});
apos.afterYield(self.autocropIfNeeded);
reflect();
self.preview();
}
},
add: function(e, data) {
if (limit && (self.count() >= limit)) {
alert('You must remove an image before adding another.');
return false;
}
return data.submit();
}
});
var warning = getSizeWarning({ width: 0, height: 0 });
if (warning) {
self.$el.find('[data-size-warning]').show().text(warning);
}
// setup drag-over states
self.$el.find('.apos-ui-modal-body').bind('dragover', function (e) {
var dropZone = self.$el.find('.apos-ui-modal-body'),
timeout = window.dropZoneTimeout;
if (!timeout) {
dropZone.addClass('apos-slideshow-file-in');
} else {
clearTimeout(timeout);
}
if (e.target === dropZone[0]) {
dropZone.addClass('apos-slideshow-file-hover');
} else {
dropZone.removeClass('apos-slideshow-file-hover');
}
window.dropZoneTimeout = setTimeout(function () {
window.dropZoneTimeout = null;
dropZone.removeClass('apos-slideshow-file-in apos-slideshow-file-hover');
}, 100);
});
// Implement the aspect ratio constraints of the currently
// selected orientation and make the appropriate orientation
// UI button appear active
self.reflectOrientation = function() {
self.$el.find('[data-orientation-button]').each(function(){
$(this).removeClass('active');
});
self.$el.find('[data-orientation-button="' + self.data.orientation + '"]').addClass('active');
var info = _.find(orientation.choices, function(choice) {
return choice.name === self.data.orientation;
});
if (info && info.hasOwnProperty('aspectRatio')) {
aspectRatio = info.aspectRatio;
}
self.autocropIfNeeded();
};
// Template offers several orientation/constraint choices
if (orientation && orientation.choices && orientation.choices.length) {
var $orientation = self.$el.find('[data-orientation-select]');
var $oTemplate = $orientation.find('[data-orientation-button].apos-template');
$oTemplate.remove();
_.each(orientation.choices, function(choice) {
var $choice = apos.fromTemplate($oTemplate);
$choice.attr('title', choice.label);
$choice.attr('data-orientation-button', choice.name);
$choice.addClass(choice.css);
$orientation.append($choice);
});
self.$el.find('.apos-ui-modal-body').addClass('apos-select-orientation');
self.data.orientation = self.data.orientation || orientation.choices[0].name;
self.reflectOrientation();
}
// if template passed extraFields as an object, it is trying to disable certain fields
// it also needs to be enabled
if (typeof(extraFields) === 'object'){
$.each(extraFields, function(key, value) {
self.$el.find('.apos-ui-modal-body [data-extra-fields-'+key+']').remove();
});
}
// in the event of unstyleable input (firefox), pass clicks from the placeholder to the input
self.$el.find('[data-file-uploader-status]').on('click', function(){
self.$el.find('[data-uploader]').trigger('click');
return false;
});
self.$el.find('[data-enable-extra-fields]').on('click', function(){
self.$el.find('[data-items]').toggleClass('apos-extra-fields-enabled');
reflect();
});
// on Edit button click, reveal extra fields
self.$el.on('click', '[data-extra-fields-edit]', function(){
self.$el.find('[data-item]').removeClass('apos-slideshow-reveal-extra-fields');
var $button = $(this);
$button.closest('[data-item]').toggleClass('apos-slideshow-reveal-extra-fields');
return false;
});
// on Extra Fields Save, reflect and close Extra Fields
self.$el.on('click', '[data-extra-fields-save]', function(){
reflect();
var $button = $(this);
$button.closest('[data-item]').removeClass('apos-slideshow-reveal-extra-fields');
return false;
});
// on Crop button click, configure and reveal cropping modal
self.$el.on('click', '[data-crop]', function() {
crop($(this).closest('[data-item]'));
});
// on Info button click, configure and reveal image information modal
self.$el.on('click', '[data-info]', function() {
var info = $(this).closest('[data-item]').data('item');
var item = $('.apos-slideshow-editor-item--info');
$(item).css('display', 'block');
var title = $(item).find('.apos-value[data-name="title"]');
$(title).text(info.title);
var date = $(item).find('.apos-value[data-name="date"]');
$(date).text(info.createdAt );
var tags = $(item).find('.apos-value[data-name="tags"]');
var allTags = info.tags;
var tagString = "";
for(var i = 0; i < allTags.length; i++)
{
if(i == 0)
tagString += allTags[i];
else
tagString += ", " + allTags[i];
}
$(tags).text(tagString);
//change background of image to match info box
$('.apos-slideshow-editor-item-box').css('background-color', '#989898');
var background = $(this).closest('.apos-slideshow-editor-item-box')[0];
$(background).css('background-color', '#e6e6e6');
});
self.$el.on('click', '.apos-slideshow-editor-item--info div[info-exit]', function() {
$('.apos-slideshow-editor-item-box').css('background-color', '#989898');
var item = $('.apos-slideshow-editor-item--info');
$(item).css('display', 'none');
});
// Select new orientation
self.$el.on('click', '[data-orientation-button]', function() {
self.data.orientation = $(this).attr('data-orientation-button');
self.reflectOrientation();
return false;
});
self.enableChooser = function($chooser) {
// This is what we drag to. Easier than dragging to a ul that doesn't
// know the height of its li's
var $target = self.$el.find('[data-drag-container]');
var $items = $chooser.find('[data-chooser-items]');
var $search = $chooser.find('[name="search"]');
var $previous = $chooser.find('[data-previous]');
var $next = $chooser.find('[data-next]');
var $removeSearch = $chooser.find('[data-remove-search]');
var tags = $chooser.find('[data-tags]').data('tags') || '';
var tagsArray = tags ? tags.split(',') : [];
var notTags = $chooser.find('[data-not-tags]').data('not-tags') || '';
var notTagsArray = notTags ? notTags.split(',') : [];
var perPage = 21;
var page = 0;
var pages = 0;
function refreshChooser() {
self.busy(true);
$.get('/apos/browse-files', {
skip: page * perPage,
limit: perPage,
group: self.fileGroup,
tags: tagsArray,
notTags: notTagsArray,
minSize: minSize,
q: $search.val()
}, function(results) {
self.busy(false);
pages = Math.ceil(results.total / perPage);
// do pretty active/inactive states instead of
// hide / show
if (page + 1 >= pages) {
// $next.hide();
$next.addClass('inactive');
} else {
// $next.show();
$next.removeClass('inactive');
}
if (page === 0) {
// $previous.hide();
$previous.addClass('inactive');
} else {
// $previous.show();
$previous.removeClass('inactive');
}
if ($search.val().length) {
$removeSearch.show();
} else {
$removeSearch.hide();
}
$items.find('[data-chooser-item]:not(.apos-template)').remove();
_.each(results.files, function(file) {
var $item = apos.fromTemplate($items.find('[data-chooser-item]'));
$item.data('file', file);
if (showImages) {
$item.css('background-image', 'url(' + apos.filePath(file, { size: 'one-sixth' }) + ')');
}
$item.attr('title', file.name + '.' + file.extension);
if ((self.fileGroup === 'images') && showImages) {
$item.find('[data-image]').attr('src', apos.filePath(file, { size: 'one-sixth' }));
} else {
// Display everything like a plain filename, after all we're offering
// a download interface only here and we need to accommodate all types of
// files in the same media chooser list
$item.addClass('apos-not-image');
$item.text(file.name + '.' + file.extension);
}
$items.append($item);
// DRAG AND DROP FROM LIBRARY TO SLIDESHOW
// Reimplemented with love by Tom because jquery sortable connectWith,
// jquery draggable connectToSortable and straight-up jquery draggable all
// refused to play nice. Was it the file uploader? Was it float vs. non-float?
// Who knows? This code works!
(function() {
var dragging = false;
var dropping = false;
var origin;
var gapX;
var gapY;
var width;
var height;
var fileUploadDropZone;
$item.on('mousedown', function(e) {
// Temporarily disable file upload drop zone so it doesn't interfere
// with drag and drop of existing "files"
fileUploadDropZone = $uploader.fileupload('option', 'dropZone');
// Don't do a redundant regular click event
$items.off('click');
$uploader.fileupload('option', 'dropZone', '[data-no-drop-zone-right-now]');
dragging = true;
origin = $item.offset();
gapX = e.pageX - origin.left;
gapY = e.pageY - origin.top;
width = $item.width();
height = $item.height();
var file = $item.data('file');
// Track on document so we can see it even if
// something steals our event
$(document).on('mouseup.aposChooser', function(e) {
if (dragging) {
dragging = false;
// Restore file uploader drop zone
$uploader.fileupload('option', 'dropZone', fileUploadDropZone);
// Kill our document-level events
$(document).off('mouseup.aposChooser');
$(document).off('mousemove.aposChooser');
var iOffset = $target.offset();
var iWidth = $target.width();
var iHeight = $target.height();
// Just intersect with the entire slideshow and add it at the end
// if there's a match. TODO: it would be slicker to detect where
// we fell in the list, but doing that really well probably requires
// getting jQuery sortable connectWith to play nicely with us
if ((e.pageX <= iOffset.left + iWidth) &&
(e.pageX + width >= iOffset.left) &&
(e.pageY <= iOffset.top + iHeight) &&
(e.pageY + height >= iOffset.top)) {
addItem(file);
self.autocropIfNeeded();
}
// Snap back so we're available in the chooser again
$item.css('top', 'auto');
$item.css('left', 'auto');
$item.css('position', 'relative');
$('[data-uploader-container]').removeClass('apos-chooser-drag-enabled');
return false;
}
return true;
});
$(document).on('mousemove.aposChooser', function(e) {
if (dragging) {
dropping = true;
$('[data-uploader-container]').addClass('apos-chooser-drag-enabled');
$item.offset({ left: e.pageX - gapX, top: e.pageY - gapY });
}
});
return false;
});
$item.on('click', function(e){
var file = $item.data('file');
if (dropping){
dropping = false;
}
else{
addItem(file);
event.stopPropagation();
self.autocropIfNeeded();
}
});
})();
});
}).error(function() {
self.busy(false);
});
};
$previous.on('click', function() {
if (page > 0) {
page--;
refreshChooser();
}
return false;
});
$next.on('click', function() {
if ((page + 1) < pages) {
page++;
refreshChooser();
}
return false;
});
$chooser.on('click', '[name="search-submit"]', function() {
search();
return false;
});
$removeSearch.on('click', function() {
$search.val('');
search();
return false;
});
$search.on('keydown', function(e) {
if (e.keyCode === 13) {
search();
return false;
}
return true;
});
function search() {
page = 0;
refreshChooser();
}
// Initial load of chooser contents. Do this after yield so that
// a subclass like the file widget has time to change self.fileGroup
apos.afterYield(function() { refreshChooser(); });
};
$choosers = self.$el.find('[data-chooser]');
$choosers.each(function() {
self.enableChooser($(this));
});
//self.enableChooser();
};
function crop($item) {
var item = $item.data('item');
var width;
var height;
// jcrop includes some tools for scaling coordinates but they are
// not consistent throughout jcrop, so do it ourselves
// TODO: get this from the CSS without interfering with the
// ability of the image to report its true size
var cropWidth = 770;
function down(coord) {
return Math.round(coord * cropWidth / width);
}
function up(coord) {
return Math.round(coord * width / cropWidth);
}
function cropToJcrop(crop) {
return [ down(item.crop.left), down(item.crop.top), down(item.crop.left + item.crop.width), down(item.crop.top + item.crop.height) ];
}
function jcropToCrop(jcrop) {
return {
top: up(jcrop.y),
left: up(jcrop.x),
width: up(jcrop.w),
height: up(jcrop.h)
};
}
var $cropModal;
// Cropping modal needs its own busy indicator
function busy(state) {
if (state) {
$cropModal.find('[data-progress]').show();
$cropModal.find('[data-finished]').hide();
} else {
$cropModal.find('[data-progress]').hide();
$cropModal.find('[data-finished]').show();
}
}
$cropModal = apos.modalFromTemplate('.apos-slideshow-crop', {
init: function(callback) {
// Cropping should use the full size original. This gives us both the right
// coordinates and a chance to implement zoom if desired
var $cropImage = $cropModal.find('[data-crop-image]');
busy(true);
// Load the image at its full size while hidden to discover its dimensions
// (TODO: record those in the database and skip this performance-lowering hack)
$cropImage.css('visibility', 'hidden');
$cropImage.attr('src', apos.data.uploadsUrl + '/files/' + item._id + '-' + item.name + '.' + item.extension);
$cropImage.imagesReady(function(widthArg, heightArg) {
// Now we know the true dimensions, record them and scale down the image
width = widthArg;
height = heightArg;
var viewWidth = down(width);
var viewHeight = down(height);
$cropImage.css('width', viewWidth + 'px');
$cropImage.css('height', viewHeight + 'px');
$cropImage.css('visibility', 'visible');
var jcropArgs = {};
if (setSelect) {
jcropArgs.setSelect = setSelect;
}
if (item.crop) {
jcropArgs.setSelect = cropToJcrop(item.crop);
}
if (minSize) {
jcropArgs.minSize = [ down(minSize[0]), down(minSize[1]) ];
}
if (aspectRatio) {
jcropArgs.aspectRatio = aspectRatio[0] / aspectRatio[1];
}
// Pass jcrop arguments and capture the jcrop API object so we can call
// tellSelect at a convenient time
$cropImage.Jcrop(jcropArgs, function() {
$item.data('jcrop', this);
});
busy(false);
});
return callback(null);
},
save: function(callback) {
var c = $item.data('jcrop').tellSelect();
// If no crop is possible there may
// be NaN present. Just cancel with no crop performed
if ((c.w === undefined) || isNaN(c.w) || isNaN(c.h)) {
return callback(null);
}
// Ask the server to render this crop
busy(true);
item.crop = jcropToCrop(c);
$.post('/apos/crop', { _id: item._id, crop: item.crop }, function(data) {
reflect();
$item.removeClass('apos-slideshow-reveal-crop');
busy(false);
// update the modal after crop rendered
self.setItemThumbnail(item, $item);
return callback(null);
}).error(function() {
busy(false);
alert('Server error, please retry');
return callback('fail');
});
}
});
}
// Counts the list items in the DOM. Useful when still populating it.
self.count = function() {
return $items.find(liveItem).length;
};
// The server will render an actual slideshow, but we also want to see
// thumbnails of everything with draggability for reordering and
// remove buttons.
//
// The ids and extras properties are what matters to the server, but we
// need to maintain the self.data._items field to get the name
// of the file, etc. for preview purposes.
self.prePreview = function(callback) {
$items.find(liveItem).remove();
var ids = self.data.ids || [];
var extras = self.data.extras || {};
var items = self.data._items || [];
_.each(ids, function(id) {
var item = _.find(items, function(item) { return item._id === id; });
// ALWAYS tolerate files that have been removed, as the
// pages collection doesn't know they are gone
if (item) {
// The existing items are not subject to complaints about being too small,
// pass the existing flag
addItem(item, true);
}
});
};
// Prep the data to be saved. If any images need to be autocropped,
// pause and do that and auto-retry this function.
self.preSave = function (callback) {
reflect();
self.autocropIfNeeded(function() {
self.data.showTitles = (userOptions.disableTitles) ? false : (self.$showTitles.val() === '1');
self.data.showDescriptions = (userOptions.disableDescriptions) ? false : (self.$showDescriptions.val() === '1');
self.data.showCredits = (userOptions.disableCredits) ? false : (self.$showCredits.val() === '1');
return callback(null);
});
};
// Given a file and its jquery element, this method will
// update the thumbnail to reflect that file's image or filename,
// as appropriate to the type
self.setItemThumbnail = function(item, $item)
{
if (_.contains(['gif', 'jpg', 'png'], item.extension)) {
var url = 'url(' + apos.filePath(item, { size: 'full', crop: item.crop }) + ')';
$item.find('[data-image-background]').css('background-image', url);
} else {
$item.find('[data-image]').parent().addClass('apos-not-image');
$item.find('[data-image]').parent().append('<span class="apos-file-name">' + item.name + '.' + item.extension + '</span>');
}
};
self.autocropIfNeeded = function(callback) {
// Perform autocrops if needed
if (!aspectRatio) {
return callback && callback();
}
// On this invocation we'll crop *one* item, then
// recursively invoke ourselves until there are no
// more items, then invoke the callback.
var found = false;
$items.find(liveItem).each(function() {
if (found) {
return;
}
var $item = $(this);
var item = $item.data('item');
if (!item) {
return;
}
// Look for an aspect ratio match within 1%. Perfect match is unrealistic because
// we crop a scaled view and jcrop is only so precise
// Look at the existing crop's aspect ratio, or the
// actual image's aspect ratio if there is no existing crop;
// compare to the current required aspect ratio to decide
// if we have work to do
var eWidth = item.crop ? item.crop.width : item.width;
var eHeight = item.crop ? item.crop.height : item.height;
if (!within(eWidth / eHeight, aspectRatio[0] / aspectRatio[1], 1.0)) {
var width, height;
width = item.width;
height = Math.round(item.width * aspectRatio[1] / aspectRatio[0]);
if (height > item.height) {
height = item.height;
width = Math.round(item.height * aspectRatio[0] / aspectRatio[1]);
}
if (width > item.width) {
width = item.width;
}
if (height > item.height) {
height = item.height;
}
item.crop = {
top: Math.floor((item.height - height) / 2),
left: Math.floor((item.width - width) / 2),
width: width,
height: height
};
self.busy(true);
var $autocropping = self.$el.find('.apos-autocropping');
$autocropping.show();
found = true;
return $.post('/apos/crop', { _id: item._id, crop: item.crop }, function(data) {
self.busy(false);
$autocropping.hide();
self.setItemThumbnail(item, $item);
reflect();
return self.autocropIfNeeded(callback);
}).error(function() {
// Update the modal after crop rendered
self.busy(false);
$autocropping.hide();
alert('Server error, please retry');
return callback && callback('fail');
});
}
});
if (!found) {
// No more to autocrop
return callback && callback();
}
};
// Returns true if b is within 'percent' of a, as a percentage of a
function within(a, b, percent) {
var portion = (Math.abs(a - b) / a);
return (portion < (percent / 100.0));
}
function getSizeWarning(item) {
if (minSize && (((minSize[0]) && (item.width < minSize[0])) ||
((minSize[1]) && (item.height < minSize[1])))) {
if (minSize[0] && minSize[1]) {
return 'Images must be at least ' + minSize[0] + 'x' + minSize[1] + ' pixels.';
} else if (minSize.width) {
return 'Images must be at least ' + minSize[0] + ' pixels wide.';
} else {
return 'Images must be at least ' + minSize[1] + ' pixels tall.';
}
}
return undefined;
}
function addItem(item, existing) {
var count = self.count();
// Refuse to exceed the limit if one was specified
if (limit && (count >= limit)) {
alert('You must remove an image before adding another.');
return;
}
if (!existing) {
var warning = getSizeWarning(item);
if (warning) {
alert(warning);
return;
}
if (self.fileGroup && (item.group !== self.fileGroup)) {
// TODO: push list of allowed file extensions per group to browser side and
// just list those
if (self.fileGroup === 'images') {
alert('Please upload a .gif, .jpg or .png file.');
} else {
alert('That file is not in an appropriate format.');
}
return;
}
}
var $item = apos.fromTemplate($items.find('[data-item]'));
self.setItemThumbnail(item, $item);
// Some derivatives of slideshows use these, some don't. These are
// not editable fields, they are immutable facts about the file
$item.find('[data-extension]').text(item.extension);
/**
* There is an assumption here that this form will never contain a formBoolean
*
*/
$item.find('[data-name]:not(.apos-fieldset-select )').text(item.name);
$item.find('[data-hyperlink]').val(item.hyperlink);
$item.find('[data-hyperlink-title]').val(item.hyperlinkTitle);
$item.findByName('newWindow').val(item.hyperlinkTarget ? '1' : '0' );
if (extraFields || typeof(extraFields) === 'object') {
$item.find('[data-remove]').after('<a class="apos-slideshow-control apos-edit" data-extra-fields-edit><i class="icon-cog"></i></a>');
}
$item.data('item', item);
$item.find('[data-remove]').click(function() {
$item.remove();
reflect();
self.preview();
self.$el.find('[data-limit-reached]').hide();
self.$el.find('[data-uploader]').prop('disabled',false);
self.$el.find('[data-drag-container]').removeClass('apos-upload-disabled');
self.$el.find('[data-drag-message]').text('Drop Files Here.');
self.$el.find('[data-drag-container]').off('drop');
return false;
});
// So selectize can work
apos.emit('enhance', $item);
$items.append($item);
count++;
if (limit && (count >= limit)) {
self.$el.find('[data-limit]').text(limit);
self.$el.find('[data-limit-reached]').show();
self.$el.find('[data-uploader]').prop('disabled',true);
self.$el.find('[data-drag-container]').addClass('apos-upload-disabled');
self.$el.find('[data-drag-message]').text('Oops! This slideshow is full.');
// prevents drop action so that users dropping files into
// a a 'full' slideshow dont get thrown to an image file
self.$el.find('[data-drag-container]').on(
'drop',
function(e){
if(e.originalEvent.dataTransfer){
if(e.originalEvent.dataTransfer.files.length) {
e.preventDefault();
e.stopPropagation();
}
}
}
);
}
}
// Update the data attributes to match what is found in the
// list of items. This is called after remove and reorder events
function reflect() {
var $itemElements = $items.find(liveItem);
// What really matters is self.data.ids and self.data.extras.
// self.data._items is just a copy of the file object with its
// extras merged in, provided for read only convenience. But we
// keep that up to date too so we can render previews and display
// fields that come from the file and reopen widgets after saving
// them to the editor but not all the way to the server.
self.data.ids = [];
self.data.extras = {};
self.data._items = [];
$.each($itemElements, function(i, item) {
var $item = $(item);
var info = $item.data('item');
self.data.ids.push(info._id);
self.data.extras[info._id] = {
hyperlink: $item.find('[data-hyperlink]').val(),
hyperlinkTitle: $item.find('[data-hyperlink-title]').val(),
hyperlinkTarget: $item.findByName('newWindow').val() === '1',
crop: info.crop
};
// Make sure it's all also visible in ._items
$.extend(true, info, self.data.extras[info._id]);
self.data._items.push(info);
});
// An empty slideshow is allowed, so permit it to be saved
// even if nothing has been added
self.exists = true;
}
function annotateItem(item) {
if (!self.annotator) {
var Annotator = options.Annotator || window.AposAnnotator;
self.annotator = new Annotator({
receive: function(aItems, callback) {
// If we wanted we could display the title, description and
// credit somewhere in our preview and editing interface
return callback(null);
},
remove: function(aItem) {
// They removed (deleted) one of the items during annotation.
// Trigger the button to remove it from the slideshow too
var $itemElements = $items.find(liveItem);
var el = _.find($itemElements, function(el) {
var $el = $(el);
return ($el.data('item') === aItem);
});
if (el) {
$(el).find('[data-remove]').click();
}
},
destroyed: function() {
// End of life cycle for previous annotator, note that so
// we can open another one
self.annotator = undefined;
}
});
self.annotator.modal();
}
self.annotator.addItem(item);
}
}
AposSlideshowWidgetEditor.label = 'Slideshow';