UNPKG

apostrophe

Version:

Apostrophe is a user-friendly content management system. This core module of Apostrophe provides rich content editing and essential facilities to integrate Apostrophe into your Express project. Apostrophe also includes simple facilities for storing your r

529 lines (474 loc) • 17 kB
/* global apos, _, confirm */ function AposMediaLibrary(options) { var self = this; if (!options) { options = {}; } // PUBLIC API // Call this method after constructing the object self.modal = function() { self.$el = apos.modalFromTemplate(options.template || '.apos-media-library', self); self.$index = self.$el.find('[data-index]'); self.$show = self.$el.find('[data-show]'); self.$normal = self.$show.find('[data-normal-view]'); self.$bar = self.$el.find('[data-bar]'); self.$owner = self.$el.findByName('owner'); self.$owner.val(options.owner || 'all'); self.enableUploads(); self.$index.bottomless({ url: options.browseUrl || '/apos/browse-files', now: true, perPage: 20, skipAndLimit: true, criteria: self.getCriteria(), dataType: 'json', success: self.addResults, reset: self.resetCallback }); // Make sure we can indirectly click on an upload button that's been visually replaced with // another element. Otherwise only dragging is permissible and in IE nothing is permissible self.$el.on('click', '.apos-file-styled', function() { $(this).parent().children('input').click(); return false; }); self.$el.on('click', '[data-index] [data-item]', function() { $.each($('[data-index] [data-item]'), function() { var $item = $(this); $item.removeClass('active'); }); $(this).addClass('active'); self.showItem($(this).data('item')); return false; }); self.$el.on('click', '[data-grid]', function() { self.$el.find('[data-index]').removeClass('apos-list-view').addClass('apos-grid-view'); self.$el.find('.apos-generic-button').removeClass('active'); $(this).addClass('active'); return false; }); self.$el.on('click', '[data-list]', function() { self.$el.find('[data-index]').removeClass('apos-grid-view').addClass('apos-list-view'); self.$el.find('.apos-generic-button').removeClass('active'); $(this).addClass('active'); return false; }); self.$el.on('click', '[data-show] [data-edit]', function() { self.editItem(self.$el.find('[data-show]').data('item')); return false; }); self.$el.on('click', '[data-show] [data-rescue]', function() { self.rescueItem(self.$el.find('[data-show]').data('item')); return false; }); self.$el.on('click', '[data-show] [data-edit-view] [data-cancel-item]', function() { self.$edit.remove(); self.$normal.show(); return false; }); self.$el.on('click', '[data-show] [data-edit-view] [data-save-item]', function() { self.saveItem(function() { self.$edit.remove(); self.$normal.show(); }); return false; }); self.$el.on('click', '[data-show] [data-edit-view] [data-delete-item]', function() { self.deleteItem(function() { self.$edit.remove(); self.$normal.show(); }); return false; }); // Filters self.$el.on('change', '[name="owner"],[name="trash"],[name="sort"],[name="group"],[name="tag"],[name="extension"]', function() { self.resetIndex(); return false; }); self.$search = self.$el.find('[name="search"]'); self.enableSearchField(); // Buttons in the show view that make sense only after an item is chosen self.$show.find('[data-edit]').hide(); self.$show.find('[data-rescue]').hide(); }; self.enableSearchField = function() { // Debounce the textchange event. If we let it fire rapidly, // the second set of results may arrive before the first, with // irrational results var pending = false; self.$search.bind('textchange', function() { if (!pending) { pending = true; setTimeout(function() { self.resetIndex(); pending = false; }, 500); } }); }; self.enableUploads = function() { var $uploader = self.$el.find('[data-uploader]'); $uploader.fileupload({ dataType: 'json', dropZone: self.$el.find('.apos-index-pane'), // Best to keep it this way to avoid slamming when multiple users are active sequentialUploads: true, start: function (e) { busy(true); }, // Even on an error we should note we're not spinning anymore always: function (e, data) { busy(false); }, done: function (e, data) { if (data.result.files) { _.each(data.result.files, function (file) { self.annotateItem(file); }); // Simplest way to show the new files self.resetIndex(); apos.change('media'); } }, add: function(e, data) { return data.submit(); } }); function busy(state) { apos.busy(self.$el.find('.apos-add-files'), state); } // setup drag-over states self.$el.find('.apos-modal-body').bind('dragover', function (e) { var dropZone = self.$el.find('.apos-file-container'), timeout = window.dropZoneTimeout; if (!timeout) { dropZone.addClass('apos-media-file-in'); } else { clearTimeout(timeout); } if (e.target === dropZone[0]) { dropZone.addClass('apos-media-file-hover'); } else { dropZone.removeClass('apos-media-file-hover'); } window.dropZoneTimeout = setTimeout(function () { window.dropZoneTimeout = null; dropZone.removeClass('apos-media-file-in apos-media-file-hover'); }, 100); }); }; self.annotateItem = function(item) { if (!self.annotator) { var Annotator = options.Annotator || window.AposAnnotator; self.annotator = new Annotator({ receive: function(aItems, callback) { // Modified the files, so reset the list view self.resetIndex(); apos.change('media'); return callback(null); }, 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); }; self.addResults = function(results) { _.each(results.files, function(item) { self.addIndexItem(item); }); if (options.browseByTag && (!self.haveTags)) { // get the element var $tag = self.$el.findByName('tag')[0]; var tag = $tag.selectize.getValue(); // reset selectize $tag.selectize.clear(); $tag.selectize.clearOptions(); // load all our tags $tag.selectize.load(function(callback) { var tags = []; // all tags option tags.push({ value: '', text: 'All Tags'}); _.each(results.tags, function(tag) { tags.push({ value: tag, text: tag }); }); self.haveTags = true; callback(tags); }); $tag.selectize.setValue(tag); if (!results.length) { self.$el.trigger('aposScrollEnded'); } } }; self.addIndexItem = function(item) { var $item = apos.fromTemplate(self.$index.find('[data-item].apos-template')); self.populateItem($item, item); self.$index.append($item); }; self.populateItem = function($item, item) { $item.data('item', item); if (item.group === 'images') { var $img = $('<div class="apos-preview-image" style="background-image:url('+self.getImagePath(item, 'one-third')+');" ></div>'); // $img.attr('src', apos.filePath(item, { size: 'one-third' })); $item.find('[data-preview]').html($img); } else { $item.find('[data-preview]').html('<span class="apos-file-format apos-'+item.extension+'"></span>'); $item.addClass('apos-file'); } $item.find('[data-title]').text(item.title || item.name); $item.find('[data-list-details]').append('<span>Group: ' + item.group || + '</span>'); $item.find('[data-list-details]').append('<span>Extension: ' + item.extension || + '</span>'); $item.find('[data-list-details]').append('<span>Tags: ' + (item.tags || []).join(', ') + '</span>'); }; self.allShow = [ 'title', 'name', 'tags', 'credit', 'description', 'group', 'type', 'createdAt', 'credit', 'extension', 'downloadOriginal', 'owner' ]; self.simpleShow = [ 'title', 'name', 'description', 'group', 'type', 'credit', 'extension' ]; // self.listShow = [ 'title', 'name', 'group', 'type']; self.simpleEditable = [ 'title', 'credit', 'description' ]; self.showItem = function(item) { self.$show.data('item', item); if (self.$edit) { self.$edit.remove(); } self.moveToScrollTop(self.$normal); self.$normal.show(); if (item.group === 'images') { var $img = $('<img class="apos-preview-image" />'); $img.attr('src', self.getImagePath(item, 'one-half')); self.$normal.find('[data-preview]').html($img); } else { self.$normal.find('[data-preview]').html(''); } _.each(self.simpleShow, function(field) { if (item[field]){ self.$normal.find('[data-name="' + field + '"]').text(item[field]); } else{ self.$normal.find('[data-name="' + field + '"]').html("&mdash;"); } }); // This isn't worth introducing a dependency on momentjs var date = item.createdAt.replace(/T.*$/, ''); self.$normal.find('[data-name="createdAt"]').text(date); self.$normal.find('[data-name="owner"]').text(item._owner ? item._owner.title : 'admin'); self.$normal.find('[data-name="tags"]').text((item.tags || []).join(', ')); var $link = $('<a></a>'); $link.attr('href', apos.filePath(item)); $link.text(apos.filePath(item)); var $downloadOriginal = self.$normal.find('[data-name="downloadOriginal"]'); $downloadOriginal.html(''); // Downloads won't work while an item is in the trash. -Tom if (!item.trash) { $downloadOriginal.append($link); } // Show the edit button or the rescue button, but only if we can edit if (item._edit) { if (item.trash) { self.$show.find('[data-rescue]').show(); self.$show.find('[data-edit]').hide(); } else { self.$show.find('[data-rescue]').hide(); self.$show.find('[data-edit]').show(); } } else { self.$show.find('[data-rescue]').hide(); self.$show.find('[data-edit]').hide(); } }; self.editItem = function(item) { self.$show.data('item', item); self.$edit = apos.fromTemplate(self.$show.find('[data-edit-view]')); self.$edit.show(); if (item.group === 'images') { var $img = $('<img class="apos-preview-image" />'); $img.attr('src', self.getImagePath(item, 'one-sixth')); self.$edit.find('[data-preview]').html($img); } else { self.$edit.find('[data-preview]').html(''); } _.each(self.simpleEditable, function(field) { self.$edit.findByName(field).val(item[field]); }); var url = options.replaceFileUrl || '/apos/replace-file'; url += '?id=' + item._id; var $upload = self.$edit.find('[data-uploader]'); $upload.attr('data-url', url); $upload.fileupload({ dataType: 'json', dropZone: self.$edit, sequentialUploads: true, add: function(e, data) { // TODO think about a way to undo even this if (!confirm('Are you sure you want to replace this file? This cannot be undone!')) { return false; } return data.submit(); }, start: function (e) { busy(true); }, // Even on an error we should note we're not spinning anymore always: function (e, data) { 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.status === 'ok') { // Refresh our knowledge of this file self.updateItem(data.result.file); // Make sure both views are updated. This could flash a // little but it's sure to be correct self.showItem(data.result.file); self.editItem(data.result.file); apos.change('media'); } }, }); function busy(state) { apos.busy(self.$el.find('.apos-replace-file'), state); } apos.enableTags(self.$edit.find('[data-name="tags"]'), item.tags); self.$normal.hide(); self.$show.append(self.$edit); self.moveToScrollTop(self.$edit); }; self.saveItem = function(callback) { var item = self.$show.data('item'); $.jsonCall(options.annotateFilesUrl || '/apos/annotate-files', [ { _id: item._id, title: self.$edit.findByName('title').val(), credit: self.$edit.findByName('credit').val(), description: self.$edit.findByName('description').val(), tags: self.$edit.find('[data-name="tags"]').selective('get', { incomplete: true }) } ], function(items) { _.each(items, function(item) { // We know we can edit this because we just did. // Fixes a bug that hid the "Edit Details" button // after a save item._edit = true; self.updateItem(item); self.showItem(item); }); apos.change('media'); return callback(); }); }; self.deleteItem = function(callback) { var item = self.$show.data('item'); $.jsonCall(options.deleteFileUrl || '/apos/delete-file', { _id: item._id }, function(result) { if (result.status === 'ok') { // Deletion causes issues with pagination, even with // infinite scroll - is this page empty now? Simplest to reload. // Later we might finesse this more self.resetIndex(); apos.change('media'); return callback(); } else { alert('Error (already deleted?)'); } }); }; self.rescueItem = function() { var item = self.$show.data('item'); $.jsonCall(options.rescueFileUrl || '/apos/rescue-file', { _id: item._id }, function(result) { if (result.status === 'ok') { // Undeletion causes issues with pagination, even with // infinite scroll - is this page empty now? Simplest to reload. // Later we might finesse this more self.resetIndex(); } else { alert('Error (already rescued?)'); } }); }; self.updateItem = function(item) { var $item = self.findItem(item); $item.data('item', item); self.populateItem($item, item); }; self.findItem = function(item) { var result; var $items = self.$index.find('[data-item]:not(.apos-template)'); $.each($items, function() { var $item = $(this); if ($item.data('item')._id === item._id) { result = $item; } }); return result; }; self.resetIndex = function() { // Clear show pane if (self.$edit) { self.$edit.remove(); } self.$normal.find('[data-preview]').html(''); _.each(self.allShow, function(field) { self.$normal.find('[data-name="' + field + '"]').html('&mdash;'); }); self.$normal.find('[data-edit]').hide(); self.$normal.find('[data-rescue]').hide(); self.$normal.show(); self.$index.trigger('aposScrollReset', self.getCriteria()); }; // aposScrollReset causes this to be called self.resetCallback = function() { self.$index.find('[data-item]:not(.apos-template)').remove(); }; self.getCriteria = function() { return { owners: true, owner: self.$owner.val(), sort: self.$el.findByName('sort').val(), extension: self.$el.findByName('extension').val(), trash: self.$el.findByName('trash').val(), group: self.$el.findByName('group').val(), tag: self.$el.findByName('tag').val(), q: self.$el.findByName('search').val() }; }; self.moveToScrollTop = function($el) { var offset = $el.offset(); var scrollTop = $(document).scrollTop(); var showTop = self.$show.offset().top; if (scrollTop > (showTop + 20)) { offset.top = scrollTop + 20; } else { offset.top = showTop; } $el.offset(offset); }; self.getImagePath = function(image, size) { if (image.trash) { // Only size available while in trash size = 'one-sixth'; } return apos.filePath(image, { size: size }); }; self.fixedHeader = function() { }; // MODAL CALLBACKS self.init = function(callback) { return callback(null); }; self.afterHide = function(callback) { // Stop bottomless interval timer self.$el.trigger('aposScrollDestroy'); return callback(null); }; }