// 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); } }; })();