apostrophe
Version:
The Apostrophe Content Management System.
1,005 lines (883 loc) • 35.6 kB
JavaScript
// A "manage" modal for pieces, displaying them list and/or grid views and providing
// filtering and sorting features. The manager modal is also extended on the fly
// by the chooser for use as a more full-featured chooser when selecting pieces
// to appear in a widget, such as a slideshow.
apos.define('apostrophe-pieces-manager-modal', {
extend: 'apostrophe-modal',
source: 'manager-modal',
construct: function(self, options) {
self.page = 1;
self.schema = self.options.schema;
var jobs = apos.modules['apostrophe-jobs'];
self.decorate = function() {
if (options.decorate) {
options.decorate(self, options);
}
};
// turn a filter config object into a working filter
self.generateFilter = function(filter) {
return {
name: filter.name,
multiple: filter.multiple,
setDefault: function() {
self.currentFilters[filter.name] = filter.def;
}
};
};
self.beforeShow = function(callback) {
self.$manageView = self.$el.find('[data-apos-manage-view]');
self.$filters = self.$modalFilters.find('[data-filters]');
self.$batch = self.$el.find('[data-batch]');
self.$batchOperation = self.$batch.find('[name="batch-operation"]');
self.$pager = self.$el.find('[data-pager]');
self.enableChooseViews();
self.enableFilters();
self.enableSorts();
self.enableSearch();
self.enableCheckboxEvents();
self.enableBatchOperations();
self.enableInsertViaUpload();
self.enableSelectEverything();
apos.on('change', self.onChange);
self.refresh();
return callback();
};
self.enableFilters = function() {
self.filters = [
{
name: 'page',
handler: function($el, value) {
self.currentFilters.page = value;
self.refresh();
}
}
];
self.filters = self.filters.concat(_.map(self.options.filters, function(filterConfig) {
return self.generateFilter(filterConfig);
}));
self.currentFilters = {};
_.each(self.filters, function(filter) {
filter.setDefault = filter.setDefault || function() {
self.currentFilters[filter.name] = 1;
};
filter.handler = filter.handler || function($el, value) {
if (filter.multiple) {
// Multiple select requires that we look at the
// checkboxes of the existing selections plus the
// value of the select element
var vals = [];
self.$filters.findByName(filter.name).each(function() {
var $el = $(this);
if ($el.is('[type="checkbox"]') && (!$el.prop('checked'))) {
// skip, this was just unchecked
} else {
// An existing checkbox, or a new choice from the select element
var val = $(this).val();
if (val !== '**CHOOSE**') {
vals.push($(this).val());
}
}
});
self.currentFilters[filter.name] = vals;
self.currentFilters.page = 1;
self.refresh();
} else {
if (value === '**ANY**') {
value = null;
}
self.currentFilters[filter.name] = value;
self.currentFilters.page = 1;
self.refresh();
}
};
filter.setDefault();
// One step of indirection to make overrides after this point possible
self.link('apos-' + filter.name, function($el, value) {
// For pill button links
filter.handler($el, value);
});
self.$filters.on('change', '[name="' + filter.name + '"]', function() {
// For select elements
filter.handler($(this), $(this).val());
});
});
// If the user toggles between viewing trash and viewing live items,
// clear the current selection so that we don't wind up trying to trash
// what is already in the trash or vice versa
var trashFilter = _.find(self.filters, { name: 'trash' });
if (trashFilter) {
var superTrashHandler = trashFilter.handler;
trashFilter.handler = function($el, value) {
self.choices = [];
superTrashHandler($el, value);
};
}
};
// Enables batch operations, such as moving every selected
// item to the trash. Maps the operations found in options.batchOperations
// to methods, for instance `{ name: 'trash'}` maps to
// a call to `self.batchTrash()`. Also implements the UI for
// selecting and invoking a batch operation.
self.enableBatchOperations = function(callback) {
if (self.parentChooser || self.options.liveChooser) {
// It would make sense for decorateManager to just kill this method,
// except that would break bc for anyone who already
// overrode that. As the newcomer we're responsible for
// playing nice and recognizing batch operations are
// incompatible with the chooser. -Tom
return;
}
self.$batchOperationTemplate = self.$batchOperation.clone();
self.batchOperations = self.options.batchOperations;
self.reflectBatchOperation();
self.$batchOperation.on('change', function() {
self.reflectBatchOperation();
});
self.link('apos-batch-operation', function($el, action) {
self.batchOperations[action].handler();
return false;
});
return async.eachSeries(self.batchOperations, function(batchOperation, callback) {
return self.enableBatchOperation(batchOperation, callback);
}, callback);
};
// Invoked when a new batch operation is chosen to reflect it in the UI
// by displaying the appropriate button and, where relevant, the
// appropriate string field. Also invoked when the manage view is refreshed,
// so that filters can impact which operations are currently enabled.
self.reflectBatchOperation = function() {
if (!self.$batchOperation.length) {
// Batch operations not present
return;
}
// We just want to hide the options you can't pick right now,
// but that's not possible with option elements, so we have to
// rebuild the list each time this is an issue and then remove
// the inappropriate items. What a PITA.
var val = self.$batchOperation.val();
self.$batchOperation.find('option').remove();
_.each(self.batchOperations, function(batchOperation) {
var disable = false;
var $option;
_.each(batchOperation.unlessFilter, function(val, key) {
var filterVal = self.currentFilters[key];
if (val === true) {
if ((filterVal === true) || (filterVal === '1')) {
disable = true;
}
} else if (val === false) {
if ((filterVal === false) || (filterVal === '0')) {
disable = true;
}
} else if (val === null) {
if (filterVal === 'any') {
disable = true;
}
} else if (val === filterVal) {
disable = true;
}
});
if (!disable) {
$option = self.$batchOperationTemplate.find('[value="' + batchOperation.name + '"]').clone();
self.$batchOperation.append($option);
}
});
var $selected = self.$batchOperation.find('[value="' + val + '"]');
if ($selected.length) {
self.$batchOperation.val(val);
} else {
self.$batchOperation[0].selectedIndex = 0;
val = self.$batchOperation.val();
}
self.$batch.find('[data-apos-batch-operation-form]').removeClass('apos-active');
self.$batch.find('[data-apos-batch-operation-form="' + val + '"]').addClass('apos-active');
self.$batch.find('[data-apos-batch-operation]').addClass('apos-hidden');
self.$batch.find('[data-apos-batch-operation="' + val + '"]').removeClass('apos-hidden');
// Reflect current count of selected items
var count = self.getIds().length;
self.$batch.find('[name="batch-operation"] option').each(function() {
var $option = $(this);
$option.text($option.text().replace(/\([\d]+\)/, '(' + count + ')'));
});
// Availability based on whether there is a selection
var $buttons = self.$batch.find('.apos-button');
if (count) {
$buttons.removeClass('apos-button--disabled');
} else {
$buttons.addClass('apos-button--disabled');
}
};
self.batchOperations = {};
// Preps for supporting a single batch operation, matching the operation name
// to a method name such as `batchTrash` via the `name` property.
// Also populates the subform for it, if any. Requires callback.
// Invoked for you by `enableBatchOperations`. Do not invoke directly.
self.enableBatchOperation = function(batchOperation, callback) {
self.batchOperations[batchOperation.name] = batchOperation;
batchOperation.handler = self['batch' + apos.utils.capitalizeFirst(apos.utils.camelName(batchOperation.name))];
if (!batchOperation.schema) {
return setImmediate(callback);
}
var data = apos.schemas.newInstance(batchOperation.schema);
return apos.schemas.populate(
self.$batch.find('[data-apos-batch-operation-form="' + batchOperation.name + '"]'),
batchOperation.schema,
data,
callback
);
};
self.enableInsertViaUpload = function() {
if (!self.options.insertViaUpload) {
return;
}
// Additional parameters to be passed to the route. Used to ensure
// we get things like `minSize` via the override the chooser puts in place
var query = {};
self.beforeList(query);
// The actual input element, hidden, which gets clicked by the button
// (see below).
getUploader().fileupload({
dataType: 'json',
dropZone: self.$el,
formData: {
// Because jquery fileupload is not smart enough to serialize
// arrays and objects by itself
filters: JSON.stringify(query)
},
url: self.action + '/insert-via-upload',
start: function (e) {
// Only way to know this is the beginning of the whole show
if (!getUploader().fileupload('active')) {
busy(true);
self.insertViaUploadStatus = { total: 0, unsuitable: 0, errors: 0 };
}
},
// Even on an error we should note we're not spinning anymore
always: function (e, data) {
// Asinine but this still returns 1 as of when always is invoked
if (getUploader().fileupload('active') <= 1) {
busy(false);
}
},
fail: function (e, data) {
self.insertViaUploadStatus.errors++;
feedback();
},
done: function (e, data) {
if (data.result.status === 'ok') {
self.addChoice(data.result.piece._id);
// Refreshing after each one gives a sense of activity
apos.change(self.options.name);
self.insertViaUploadStatus.total++;
} else if (data.result.status === 'unsuitable') {
self.insertViaUploadStatus.unsuitable++;
} else {
self.insertViaUploadStatus.errors++;
}
feedback(data);
},
add: function(e, data) {
return data.submit();
}
});
self.$controls.on('click', '[data-apos-upload-' + self.options.name + ']', function() {
// For bc we have to be able to deal with the possibility the uploader is
// replaced after each use
getUploader().click();
return false;
});
function feedback(data) {
if (getUploader().fileupload('active') <= 1) {
if (self.insertViaUploadStatus.errors) {
apos.notify('Some files were damaged, of an unsuitable type or too large to be uploaded.', { type: 'error' });
} else if (self.insertViaUploadStatus.unsuitable) {
apos.notify('Some uploaded files were unsuitable for the current placement.', { type: 'error' });
} else {
// If we only tried to upload one and it worked, trigger the schema modal next per Stuart
if (self.insertViaUploadStatus.total === 1) {
self.options.manager.edit(data.result.piece._id);
}
}
}
}
function busy(state) {
apos.ui.globalBusy(state);
}
function getUploader() {
return self.$controls.find('[data-apos-uploader-' + self.options.name + ']');
}
};
// Moves all selected items (`self.choices`) to the trash, after
// asking for user confirmation.
self.batchTrash = function() {
if (self.choices.length > 0) {
return self.batchSimple(
'trash',
'Are you sure you want to trash ' + self.choices.length + ' item' + (self.choices.length !== 1 ? 's' : '') + '?',
{}
);
}
};
// Rescues all selected items (`self.choices`) from the trash, after
// asking for user confirmation.
self.batchRescue = function() {
if (self.choices.length > 0) {
return self.batchSimple(
'rescue',
'Are you sure you want to rescue ' + self.choices.length + ' item' + (self.choices.length !== 1 ? 's' : '') + ' from the trash?',
{}
);
}
};
// Publishes all selected items (`self.choices`), after asking for
// user confirmation.
self.batchPublish = function() {
if (self.choices.length > 0) {
return self.batchSimple(
'publish',
'Are you sure you want to publish ' + self.choices.length + ' item' + (self.choices.length !== 1 ? 's' : '') + '?',
{}
);
}
};
// Unpublishes all selected items (`self.choices`), after asking for
// user confirmation.
self.batchUnpublish = function() {
if (self.choices.length > 0) {
return self.batchSimple(
'unpublish',
'Are you sure you want to unpublish ' + self.choices.length + ' item' + (self.choices.length !== 1 ? 's' : '') + '?',
{}
);
}
};
// Tags all selected items (`self.choices`), after asking for
// user confirmation.
self.batchTag = function() {
if (self.choices.length > 0) {
return self.batchSimple(
'tag',
'Are you sure you want to tag ' + self.choices.length + ' item' + (self.choices.length !== 1 ? 's' : '') + '?',
{}
);
}
};
// Untags all selected items (`self.choices`), after asking for
// user confirmation.
self.batchUntag = function() {
if (self.choices.length > 0) {
return self.batchSimple(
'untag',
'Are you sure you want to untag ' + self.choices.length + ' item' + (self.choices.length !== 1 ? 's' : '') + '?',
{}
);
}
};
self.batchPermissions = function() {
if (self.choices.length > 0) {
return self.batchSimple(
'permissions',
false,
{
dataSource: self.getBatchPermissions
}
);
}
};
self.getBatchPermissions = function(data, callback) {
self.options.manager.launchBatchPermissionsModal(data, callback);
};
// Carry out a named batch operation, such as `trash`, displaying the
// provided prompt and, if confirmed by the user, invoking the
// corresponding verb in this module's API.
//
// If `confirmationPrompt` is falsy, no prompt is displayed. Often
// appropriate if `options.dataSource` presents another chance to cancel.
//
// `options.dataSource` can be used to specify a function
// to be invoked to gather more input before calling the API.
// It receives `(data, callback)`, where `data.ids` and any
// input gathered from the schema are already present, and
// should update `data` and invoke `callback` with
// null on success or with an error on failure.
//
// `options.success` is invoked only if the operation
// succeeds. It receives `(result, callback)` where
// `result` is the response from the API and `callback`
// *must* be invoked by the success function after
// completing its additional operations, even if the user
// chooses to cancel or skip those operations.
self.batchSimple = function(operationName, confirmationPrompt, options) {
var operation = self.batchOperations[operationName];
if (confirmationPrompt && (!confirm(confirmationPrompt))) {
return;
}
var data = {
ids: self.choices,
job: true
};
// So we don't still say "unpublish (4)" when there are
// now 0 visible things after unpublishing all 4
self.clearChoices();
return async.series([ convert, dataSource, save ], function(err) {
if (err) {
if (Array.isArray(err)) {
// Schemas module already highlighted it
return;
}
apos.notify(err, { type: 'error' });
return;
}
self.choices = [];
self.refresh();
});
function convert(callback) {
if (!operation.schema) {
return callback(null);
}
return apos.schemas.convert(
self.$batch.find('[data-apos-batch-operation-form="' + operationName + '"]'),
operation.schema,
data,
{},
callback
);
}
function dataSource(callback) {
if (!options.dataSource) {
return callback(null);
}
return options.dataSource(data, callback);
}
function save(callback) {
apos.ui.globalBusy(true);
return self.api(operation.route || operationName, data, function(result) {
apos.ui.globalBusy(false);
if (result.status !== 'ok') {
return callback('An error occurred. Please try again.');
}
if (result.jobId) {
return jobs.progress(result.jobId, {
success: function(result) {
if (options.success) {
return options.success(result, callback);
} else {
return callback(null);
}
},
change: self.options.name
});
}
if (options.success) {
return options.success(result, callback);
} else {
return callback(null);
}
}, function() {
apos.ui.globalBusy(false);
return callback('An error occurred. Please try again.');
});
}
};
self.enableSorts = function() {
self.$el.on('click', '[data-sort]', function() {
var $column = $(this);
var direction = $column.attr('data-sort');
var defaultDirection = $column.attr('data-default-sort-direction');
direction = self.getNextDirection(defaultDirection, direction);
if (direction) {
self.sort = {
column: $column.attr('data-column-name'),
direction: direction
};
} else {
self.sort = undefined;
}
self.refresh();
});
};
self.getNextDirection = function(defaultDirection, direction) {
if (!direction) {
direction = defaultDirection;
} else if (direction === defaultDirection) {
direction = (-parseInt(defaultDirection)).toString();
} else if (direction) {
direction = '';
}
return direction;
};
self.reflectSort = function() {
var $columns = self.$el.find('[data-column-name]');
$columns.removeClass('apos-manage-column--forward')
.removeClass('apos-manage-column--backward')
.removeClass('apos-manage-column--next-forward')
.removeClass('apos-manage-column--next-backward')
.removeClass('apos-manage-column--next-none');
$columns.filter('[data-sort]').attr('data-sort', '');
var $current;
if (self.sort) {
$current = self.$el.find('[data-column-name="' + self.sort.column + '"]');
}
$columns.each(function() {
var $column = $(this);
if ((!$current) || ($column[0] !== $current[0])) {
var direction = self.getNextDirection($column.attr('data-default-sort-direction'), '');
indicateNext($column, direction);
}
});
if (self.sort) {
$current.attr('data-sort', self.sort.direction);
if (self.sort.direction === '1') {
$current.addClass('apos-manage-column--forward');
} else {
$current.addClass('apos-manage-column--backward');
}
indicateNext($current, self.getNextDirection($current.attr('data-default-sort-direction'), self.sort.direction));
}
function indicateNext($column, direction) {
if (direction === '1') {
$column.addClass('apos-manage-column--next-forward');
} else if (direction === '-1') {
$column.addClass('apos-manage-column--next-backward');
} else {
$column.addClass('apos-manage-column--next-none');
}
}
};
self.enableChooseViews = function() {
self.link('apos-choose-manage-view', function($el, value) {
self.viewName = value;
self.refresh();
});
};
self.enableSearch = function() {
self.$filters.on('change', '[name="search-' + self.options.name + '"]', function() {
self.search = $(this).val();
self.currentFilters.page = 1;
self.refresh(function() {
var $input = self.$filters.find('[name="search-' + self.options.name + '"]');
// refocus input element after search
$input.focus();
// put the search query before the cursor
$input.val($input.val());
});
});
};
self.choices = [];
// Enable checkbox selection of pieces. The ids of the chosen pieces are added
// to `self.choices`. This mechanism is used for ordinary manager modals and their
// bulk features, like "Trash All Selected". The chooser used for selecting
// pieces for joins overrides this with an empty method and substitutes its
// own implementation.
self.enableCheckboxEvents = function() {
self.onElOrFilters('change', 'input[type="checkbox"][name="select-all"]', selectAllHandler);
function selectAllHandler(e) {
var checked = $(this).prop('checked');
var $pieces = self.$el.find('[data-piece]');
if (checked) {
$pieces.each(function() {
self.addChoice($(this).attr('data-piece'));
});
} else {
$pieces.each(function() {
self.removeChoice($(this).attr('data-piece'));
});
}
self.reflectChoiceCount();
}
self.$el.on('change', '[data-piece] input[type="checkbox"]', function(e) {
var $box = $(this);
var id = $box.closest('[data-piece]').attr('data-piece');
if ($box.prop('checked')) {
self.addChoice(id);
} else {
self.removeChoice(id);
}
});
// Add ability to select multiple checkboxes (Using Left Shift)
var lastChecked;
// Clicks on checkbox directly are not possible because as visibility:hidden is set on it and clicks won't be detected.
self.$el.on('click', '.apos-field-input-checkbox-indicator', function (e) {
var box = $(this).siblings('.apos-field-input-checkbox')[0];
// Store a variable called lastchecked to point to the last checked checkbox. If it is undefined it's the first checkbox that's selected.
if (!lastChecked) {
lastChecked = box;
return;
}
// If shift key is pressed and the checkbox is not checked.
if (e.shiftKey && !box.checked) {
// Get the siblings for the checkboxes that are being checked.
var $checkboxesInScope = $(box).closest('[data-items]').find('input') || [];
// Get the Index of the currently selected checkbox. (The one checked with holiding shift)
var startIndex = $checkboxesInScope.index(box);
// Get the index of the previously selected checkbox.
var endIndex = $checkboxesInScope.index(lastChecked);
// Get a list of all checkboxes inbetween both the indexes and make them checked.
$checkboxesInScope.slice(Math.min(startIndex, endIndex), Math.max(startIndex, endIndex) + 1).each(function (i, el) {
if (el !== box) {
$(el).prop('checked', true);
$(el).trigger('change');
}
});
}
lastChecked = box;
});
};
// shrink and grow make visual reflectments to accommodate the the new Select Everything element
self.shrinkGrid = function() {
self.$el.find('.apos-modal-footer').css('bottom', '72px');
self.$el.find('.apos-modal-body, .apos-chooser').css('height', 'calc(100% - 144px)');
};
self.growGrid = function() {
self.$el.find('.apos-modal-footer').css('bottom', '0px');
self.$el.find('.apos-modal-body, .apos-chooser').css('height', 'calc(100% - 72px)');
};
// reflect the modal's layout and size in response to
// whether the select everything box should appear
// at this time. Also reflect the state of the select
// everything checkbox based on what is actually selected
self.reflectSelectEverything = function () {
var $pieces = self.$el.find('[data-piece]');
var $checked = $pieces.find('input[type="checkbox"]:checked');
var $selectEverything = self.getSelectEverything();
if ($pieces.length === $checked.length) {
self.getSelectAll().prop('checked', true);
self.shrinkGrid();
if (!$selectEverything.hasClass('apos-active')) {
// We need to asynchronously go get all ids at this point
// before actually showing it
self.refreshSelectEverything();
}
// We will call reflectSelectEverythingCheckbox after we have allIds
} else {
self.getSelectAll().prop('checked', false);
self.growGrid();
$selectEverything.removeClass('apos-active');
self.reflectSelectEverythingCheckbox();
}
};
self.reflectSelectEverythingCheckbox = function() {
var checkIt = (self.getIds().length && self.allIds && (self.allIds.length === self.getIds().length));
self.getSelectEverything().find('input[name="select-everything"]').prop('checked', checkIt);
};
self.getSelectAll = function() {
return self.$el.find('input[type="checkbox"][name="select-all"]').add(self.$filters.find('input[type="checkbox"][name="select-all"]'));
};
self.enableSelectEverything = function() {
self.onElOrFilters('change', 'input[name="select-everything"]', function() {
var checked = $(this).prop('checked');
if (checked) {
self.checkEverythingChoices();
} else {
self.clearEverythingChoices();
}
});
};
// Execute `fn` when the event `e` fires on the delegated selector `sel`.
// If `self.$filters` is contained in `self.$el` the delegation is done
// via `self.$el`, otherwise via `self.$filters`.
self.onElOrFilters = function(e, sel, fn) {
// A simple .and() should solve this problem, but that gives us double
// event firing, for reasons that are unclear - event delegation bug
// in jQuery? -Tom
if (!$.contains(self.$el[0], self.$filters[0])) {
self.$filters.on(e, sel, fn);
} else {
self.$el.on(e, sel, fn);
}
};
self.addChoice = function(id) {
self.addChoiceToState(id);
self.reflectChoiceInCheckbox(id);
self.reflectChoiceCount();
};
self.addChoiceToState = function(id) {
if (!_.contains(self.choices, id)) {
self.choices.push(id);
}
};
self.removeChoice = function(id) {
self.removeChoiceFromState(id);
self.reflectChoiceInCheckbox(id);
self.reflectChoiceCount();
};
self.removeChoiceFromState = function(id) {
self.choices = _.filter(self.choices, function(_id) {
return id !== _id;
});
};
// Return just the ids of the choices. Subclasses
// might need to extend this to avoid returning
// other data associated with a choice. Unlike get()
// this does not require a callback
self.getIds = function() {
return self.choices;
};
self.clearChoices = function() {
self.choices = [];
self.$el.removeClass('apos-manager-modal--has-choices');
self.$el.addClass('apos-manager-modal--has-no-choices');
self.reflectChoiceCount();
};
// When the "select everything" checkbox is checked,
// we select all of the content
self.checkEverythingChoices = function() {
_.each(self.allIds, function(id) {
self.addChoiceToState(id);
});
var ids = self.getVisibleChoiceIds();
_.each(ids, function(id) {
self.reflectChoiceInCheckbox(id);
});
self.reflectChoiceCount();
};
// When the "select everything" checkbox is cleared,
// we go back to selecting just the current page
// of content
self.clearEverythingChoices = function() {
self.clearChoices();
_.each(self.getVisibleChoiceIds(), function(id) {
self.addChoice(id);
});
self.reflectChoiceCount();
};
// Get the ids of the currently visible choices (not necessarily checked)
self.getVisibleChoiceIds = function() {
var $pieces = self.$el.find('[data-piece]');
return $pieces.map(function() {
return $(this).attr('data-piece');
}).get();
};
self.refreshSelectEverything = function() {
var listOptions = self.getListOptions({ format: 'allIds' });
apos.ui.globalBusy(true);
return self.api('list', listOptions, function(response) {
apos.ui.globalBusy(false);
if (response.status !== 'ok') {
return;
}
self.allIds = response.data.ids;
self.getSelectEverything().find('[data-label]').text(response.data.label);
self.getSelectEverything().addClass('apos-active');
self.reflectSelectEverythingCheckbox();
});
};
self.getSelectEverything = function() {
return self.$el.find('[data-result-select-everything]').add(self.$filters.find('[data-result-select-everything]')).first();
};
// Reflect existing choices in checkboxes. Invoked by `self.refresh` after
// the main view is refreshed. Important when the user is selecting items
// while paginating. This mechanism is used for ordinary manager modals and their
// bulk features, like "Trash All Selected". The chooser used for selecting
// pieces for joins overrides this with an empty method and substitutes its
// own implementation.
self.reflectChoicesInCheckboxes = function() {
_.each(self.getVisibleIds(), function(id) {
self.reflectChoiceInCheckbox(id);
});
self.reflectChoiceCount();
};
// Reflect the current selection state of the given id
// by checking or unchecking the relevant box based on
// whether it is included in `self.getIds()`
self.reflectChoiceInCheckbox = function(id) {
var state = _.includes(self.getIds(), id);
self.displayChoiceInCheckbox(id, state);
};
// Return a jquery object referencing the checkbox for the given piece id
self.getCheckbox = function(id) {
return self.$el.find('[data-piece="' + id + '"] input[type="checkbox"]');
};
// Return array of ids corresponding to the items currently visible
// in the modal's list view, whether checked or not
self.getVisibleIds = function() {
var ids = [];
self.$el.find('[data-piece]').each(function() {
ids.push($(this).attr('data-piece'));
});
return ids;
};
// Set the display state of the given checkbox. returns
// a jQuery object referencing the checkbox, for the convenience
// of subclasses that extend this
self.displayChoiceInCheckbox = function(id, checked) {
var $checkbox = self.getCheckbox(id);
$checkbox.prop('checked', checked);
return $checkbox;
};
self.reflectChoiceCount = function() {
self.reflectBatchOperation();
self.reflectSelectEverything();
self.reflectHasChoices();
};
self.reflectHasChoices = function() {
if (!self.getIds().length) {
self.$el.removeClass('apos-manager-modal--has-choices');
self.$el.addClass('apos-manager-modal--has-no-choices');
} else {
self.$el.addClass('apos-manager-modal--has-choices');
self.$el.removeClass('apos-manager-modal--has-no-choices');
}
};
// Given an options object, returns a new object with
// those options plus standard options for the list API,
// such as `sort`, `search` and `manageView`. Also invokes
// `self.beforeList`. Called by `refresh`.
self.getListOptions = function(options) {
var listOptions = _.assign({ filters: {} }, options);
var filters = listOptions.filters;
_.extend(filters, self.currentFilters);
filters.sortColumn = self.sort;
filters.search = self.search;
listOptions.manageView = self.viewName;
self.beforeList(listOptions);
return listOptions;
};
self.refresh = function(callback) {
var listOptions = self.getListOptions({ format: 'managePage' });
return self.api('list', listOptions, function(response) {
if (!response.status === 'ok') {
apos.notify('An error occurred. Please try again.', { type: 'error', dismiss: true });
return;
}
self.$filters.html(response.data.filters);
self.$manageView.html(response.data.view);
self.$pager.html(response.data.pager);
apos.emit('enhance', self.$filters);
apos.emit('enhance', self.$manageView);
apos.emit('enhance', self.$pager);
self.resizeContentHeight();
self.afterRefresh();
self.reflectChoicesInCheckboxes();
self.reflectSort();
if (callback) {
return callback(response);
}
});
};
// An initially empty method you can override to add properties to the
// query object sent to the server to fetch another page of results. Also
// used to build the query that goes to the server in a insert-via-upload
// operation in order to make sure things like the `minSize` filter
// of `apostrophe-images` are honored.
self.beforeList = function(listOptions) {
// Overridable hook
};
self.afterRefresh = function() {
// Overridable hook
};
self.onChange = function(type) {
if (type === self.options.name) {
self.refresh();
}
};
self.afterHide = function() {
// So we don't leak memory and keep refreshing
// after we're gone
apos.off('change', self.onChange);
};
// Decorate at the end of the construct method, so that we can override
// methods that were added by the decorator in subclasses.
self.decorate();
}
});