apostrophe
Version:
The Apostrophe Content Management System.
224 lines (211 loc) • 7.71 kB
JavaScript
apos.define('apostrophe-docs', {
afterConstruct: function(self) {
self.enableFix();
},
beforeConstruct: function(self, options) {
self.options = options;
self.action = options.action;
},
construct: function(self, options) {
self.trashInSchema = self.options.trashInSchema;
self.managers = {};
self.getManager = function(type) {
return self.managers[type];
};
self.setManager = function(type, manager) {
self.managers[type] = manager;
};
self.locks = {};
self.watchingLocks = {};
// Obtain a lock on the given doc _id, interacting with the user
// if necessary to give them the option of shattering another
// user's lock. Invokes the callback with null on a successful lock,
// an error otherwise. If `id` is falsy, the callback succeeds
// immediately, because this indicates a new doc that no one
// else could know about or have a lock on. The lock is
// granted to the current html page.
self.lock = function(_id, callback) {
if (!_id) {
return callback(null);
}
return $.jsonCall(self.action + '/lock',
{
_id: _id
},
function(result) {
if (result.status === 'ok') {
self.locks[_id] = true;
return callback(null);
} else if ((result.status === 'locked') || (result.status === 'locked-by-me')) {
// If the lock belonged to us, we should just seize it, and if that tab is really
// still open, we'll get a notice there that it happened. This is necessary going
// forward because we can't reliably unlock on refresh in modern browsers, which
// restrict or disable synchronous requests at unload time. But if the lock belonged
// to someone else, ask nicely first
if (result.status === 'locked') {
if (!confirm(result.message)) {
return callback('locked');
}
}
return $.jsonCall(self.action + '/lock',
{
force: true,
_id: _id
},
function(result) {
if (result.status === 'ok') {
self.locks[_id] = true;
return callback(null);
} else {
return callback(result.status);
}
},
fail
);
} else {
fail();
}
},
fail
);
function fail() {
apos.notify('You were unable to take control of the document.', { type: 'error' });
return callback('error');
}
};
// Obtain a lock via the lock method, then
// watch the lock by periodically verifying
// whether the lock is still held. If the
// lock is lost the user is notified and
// the page is reloaded. This method is used
// by the modal editor for pieces, since they
// do not otherwise carry out autosave operations
// that would quickly make the user aware a lock has
// been seized by someone else.
//
// The callback is invoked as soon as
// the lock method succeeds, and monitoring
// then proceeds in the background, stopping if unlock()
// is invoked successfully.
self.lockAndWatch = function(_id, callback) {
if (self.watchingLocks[_id] && self.locks[_id]) {
// Relocking would be redundant and risk a race condition
return callback(null);
}
return self.lock(_id, function(err) {
// LACK OF RETURN STATEMENT IS INTENTIONAL
callback(err);
if (self.watchingLocks[_id]) {
// Don't queue up more and more pending calls
return;
}
self.watchingLocks[_id] = true;
var interval = setInterval(function() {
if (!_.has(self.locks, _id)) {
// We voluntarily unlocked, stop watching
clearInterval(interval);
return;
}
$.jsonCall(self.action + '/verify-lock', {
_id: _id
}, function(result) {
if (self.reloading) {
// Don't display the alert repeatedly while waiting for the refresh after
// we lose control
return;
}
if (!_.has(self.locks, _id)) {
// We voluntarily unlocked while the API call was in flight
clearInterval(interval);
self.watchingLocks[_id] = false;
return;
}
if (result.status !== 'ok') {
self.watchingLocks[_id] = false;
alert(result.message);
$(window).off('beforeunload');
self.reloading = true;
window.location.reload(true);
}
});
}, 5000);
});
};
// Release a lock on the given doc _id, if any.
// Callback succeeds (receives `null`) as long as the
// current HTML page does not have a lock anymore after
// the call, whether they initially had one or not.
//
// If `sync` is true a synchronous call is made.
// This is normally a bad thing, but it is appropriate
// in a beforeunload handler.
//
// `callback` is optional.
self.unlock = function(_id, sync, callback) {
// Stop checking for this lock right away
// so we don't get false positives for
// a conflict and refresh the page every time
// we close a modal and lockAndWatch races with us
delete self.locks[_id];
return $.jsonCall(self.action + '/unlock',
{
async: !sync
},
{
_id: _id
},
function(result) {
if (result.status === 'ok') {
return callback && callback(null);
}
return callback && callback(result.status);
},
function(err) {
return callback && callback(err);
}
);
};
// Watch for clicks on links with a [data-apos-fix-id], and open
// the relevant document for editing, displaying the
// [data-apos-fix-hint] first to explain why the document is being
// opened and ask the user to confirm. This is designed for use cases
// such as editing a document that is "camping" on the slug we want to
// use for another document. The implementation of the hint is currently
// an alert, but this API allows for us to gracefully upgrade that
// at any time.
//
// The data-apos-fix-id, data-apos-fix-type and data-apos-fix-url
// attributes should be populated on your link or button.
// If you know the doc is a piece, not a page, you can skip
// the url attribute.
//
// If data-apos-fix-error is present, the error with the same name
// is removed from the enclosing fieldset when the link is clicked.
self.enableFix = function() {
$('body').on('click', '[data-apos-fix-id]', function() {
var $el = $(this);
var hint = $el.attr('data-apos-fix-hint');
var type = $el.attr('data-apos-fix-type');
var id = $el.attr('data-apos-fix-id');
var manager = apos.docs.getManager(type);
if (!confirm(hint)) {
return false;
}
apos.schemas.removeError($el.closest('[data-name]'), $el.attr('data-apos-fix-error'));
if (manager && manager.edit) {
manager.edit(id, { field: 'slug' });
} else {
apos.pages.pageSettings({
page: {
type: type,
_id: id
},
field: 'slug'
});
}
return false;
});
};
apos.docs = self;
}
});