126 lines
4.4 KiB
JavaScript
126 lines
4.4 KiB
JavaScript
// save.js — writing the canvas back.
|
|
//
|
|
// The whole document goes up in one request: a canvas is a single JSON object
|
|
// whose edges reference node ids, so a partial patch would have to reason about
|
|
// cross-references the server would then have to re-validate anyway.
|
|
//
|
|
// Two server rejections are worth handling rather than dumping on the user:
|
|
// a 409 (someone else changed the file) and the empty-canvas guard (which asks
|
|
// for explicit confirmation before letting a save blank a populated canvas).
|
|
window.Canvas = window.Canvas || {};
|
|
|
|
Canvas.Save = (function () {
|
|
var Doc = Canvas.Doc;
|
|
|
|
var action = '';
|
|
var saving = false;
|
|
var listeners = [];
|
|
|
|
function emit(state, message) {
|
|
for (var i = 0; i < listeners.length; i++) listeners[i](state, message);
|
|
}
|
|
|
|
function post(url) {
|
|
return fetch(url, {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Canvas-Version': Doc.version()
|
|
},
|
|
body: JSON.stringify(Doc.get())
|
|
});
|
|
}
|
|
|
|
function run(force) {
|
|
if (saving) return Promise.resolve(false);
|
|
if (!Doc.isDirty() && !force) {
|
|
emit('clean', 'no changes');
|
|
return Promise.resolve(true);
|
|
}
|
|
saving = true;
|
|
emit('saving', 'saving…');
|
|
|
|
var url = action + (force ? '&empty=1' : '');
|
|
return post(url).then(function (res) {
|
|
if (res.status === 204) {
|
|
Doc.markSaved(res.headers.get('X-Canvas-Version') || '');
|
|
emit('clean', 'saved');
|
|
return true;
|
|
}
|
|
return res.text().then(function (msg) {
|
|
if (res.status === 409) {
|
|
conflict(msg);
|
|
} else if (res.status === 400 && msg.indexOf('empty canvas') !== -1) {
|
|
confirmEmpty(msg);
|
|
} else {
|
|
emit('error', msg || ('save failed (' + res.status + ')'));
|
|
alert(msg || ('Save failed (' + res.status + ')'));
|
|
}
|
|
return false;
|
|
});
|
|
}).catch(function () {
|
|
emit('error', 'network error');
|
|
alert('Could not reach the server — your changes are still here, try again.');
|
|
return false;
|
|
}).finally(function () {
|
|
saving = false;
|
|
});
|
|
}
|
|
|
|
// conflict: the file changed underneath us. Overwriting silently would
|
|
// discard whatever the other writer did, so the choice is the user's — and
|
|
// reloading would discard *their* work, so that is spelled out too.
|
|
function conflict(msg) {
|
|
emit('error', 'conflict');
|
|
openModal({
|
|
title: 'Canvas changed on disk',
|
|
body: msg + '\n\nReload to take the version on disk (your unsaved changes here are lost), ' +
|
|
'or keep editing and save again to overwrite it.',
|
|
confirm: {
|
|
label: 'RELOAD',
|
|
danger: true,
|
|
onConfirm: function () {
|
|
closeModal();
|
|
window.location.reload();
|
|
}
|
|
},
|
|
cancel: { label: 'KEEP EDITING', autofocus: true }
|
|
});
|
|
}
|
|
|
|
function confirmEmpty(msg) {
|
|
emit('error', 'refused');
|
|
openModal({
|
|
title: 'Save an empty canvas?',
|
|
body: msg + '\n\nThis removes every node from the file.',
|
|
confirm: {
|
|
label: 'EMPTY IT',
|
|
danger: true,
|
|
onConfirm: function () {
|
|
closeModal();
|
|
run(true);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return {
|
|
init: function (postURL, name) {
|
|
action = postURL + '?canvas=' + encodeURIComponent(name);
|
|
// Unsaved work must not vanish on a stray Back or tab close. The
|
|
// browser shows its own generic wording; the return value is only a
|
|
// signal that there is something to lose.
|
|
window.addEventListener('beforeunload', function (e) {
|
|
if (!Doc.isDirty()) return;
|
|
e.preventDefault();
|
|
e.returnValue = '';
|
|
});
|
|
},
|
|
|
|
run: function () { return run(false); },
|
|
isSaving: function () { return saving; },
|
|
onState: function (fn) { listeners.push(fn); }
|
|
};
|
|
})();
|