mirror of https://github.com/xwiki-labs/cryptpad
390 lines
14 KiB
JavaScript
390 lines
14 KiB
JavaScript
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
||
//
|
||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||
|
||
define([
|
||
'jquery',
|
||
'/common/common-interface.js',
|
||
'/common/hyperscript.js',
|
||
], function ($, UI, h) {
|
||
//var ChainPad = window.ChainPad;
|
||
var History = {};
|
||
|
||
History.create = function (common, config) {
|
||
if (!config.$toolbar) { return void console.error("config.$toolbar is undefined");}
|
||
if (History.loading) { return void console.error("History is already being loaded..."); }
|
||
History.loading = true;
|
||
var $toolbar = config.$toolbar;
|
||
var sframeChan = common.getSframeChannel();
|
||
History.readOnly = common.getMetadataMgr().getPrivateData().readOnly || !common.isLoggedIn();
|
||
|
||
if (!config.onlyoffice || !config.setHistory || !config.onCheckpoint || !config.onPatch || !config.makeSnapshot) {
|
||
throw new Error("Missing config element");
|
||
}
|
||
|
||
var cpIndex = -1;
|
||
var msgIndex = -1;
|
||
var ooMessages = {};
|
||
var loading = false;
|
||
var update = function () {};
|
||
var currentTime;
|
||
|
||
// Get an array of the checkpoint IDs sorted their patch index
|
||
var hashes = config.onlyoffice.hashes;
|
||
var sortedCp = Object.keys(hashes).map(Number).sort(function (a, b) {
|
||
return hashes[a].index - hashes[b].index;
|
||
});
|
||
|
||
var endWithCp = sortedCp.length &&
|
||
config.onlyoffice.lastHash === hashes[sortedCp[sortedCp.length - 1]].hash;
|
||
|
||
var fillOO = function (id, messages) {
|
||
if (!id) { return; }
|
||
if (ooMessages[id]) { return; }
|
||
ooMessages[id] = messages;
|
||
update();
|
||
};
|
||
|
||
if (endWithCp) { cpIndex = 0; }
|
||
|
||
var $version, $time, $share;
|
||
var $hist = $toolbar.find('.cp-toolbar-history');
|
||
$hist.addClass('cp-smallpatch');
|
||
$hist.addClass('cp-history-oo');
|
||
var $bottom = $toolbar.find('.cp-toolbar-bottom');
|
||
var Messages = common.Messages;
|
||
|
||
var getVersion = function () {
|
||
var major = sortedCp.length - cpIndex;
|
||
return major + '.' + (msgIndex+1);
|
||
};
|
||
var showVersion = function (initial) {
|
||
var v = getVersion();
|
||
if (initial) {
|
||
v = Messages.oo_version_latest;
|
||
}
|
||
$version.text(Messages.oo_version + v);
|
||
|
||
var $pos = $hist.find('.cp-history-timeline-pos');
|
||
var cps = sortedCp.length;
|
||
var id = sortedCp[cps - cpIndex -1] || -1;
|
||
if (!ooMessages[id]) { return; }
|
||
var msgs = ooMessages[id];
|
||
var p = 100*((msgIndex+1) / (msgs.length));
|
||
$pos.css('margin-left', p+'%');
|
||
|
||
var time = msgs[msgIndex] && msgs[msgIndex].time;
|
||
currentTime = time;
|
||
if (time) { $time.text(new Date(time).toLocaleString()); }
|
||
else { $time.text(''); }
|
||
};
|
||
|
||
// We want to load a checkpoint (or initial state)
|
||
var loadMoreOOHistory = function () {
|
||
if (!Array.isArray(sortedCp)) { return void console.error("Wrong type"); }
|
||
|
||
var cp = {};
|
||
|
||
// Get the checkpoint ID
|
||
var id = -1;
|
||
if (cpIndex < sortedCp.length) {
|
||
id = sortedCp[sortedCp.length - 1 - cpIndex];
|
||
cp = hashes[id];
|
||
}
|
||
var nextId = sortedCp[sortedCp.length - cpIndex];
|
||
|
||
// Get the history between "toHash" and "fromHash". This function is using
|
||
// "getOlderHistory", that's why we start from the more recent hash
|
||
// and we go back in time to an older hash
|
||
|
||
// We need to get all the patches between the current cp hash and the next cp hash
|
||
|
||
// Current cp or initial hash (invalid hash ==> initial hash)
|
||
var toHash = cp.hash || 'NONE';
|
||
// Next cp or last hash
|
||
var fromHash = nextId ? hashes[nextId].hash : config.onlyoffice.lastHash;
|
||
|
||
msgIndex = -1;
|
||
|
||
showVersion();
|
||
if (ooMessages[id]) {
|
||
// Cp already loaded: reload OO
|
||
loading = false;
|
||
return void config.onCheckpoint(cp);
|
||
}
|
||
|
||
sframeChan.query('Q_GET_HISTORY_RANGE', {
|
||
channel: config.onlyoffice.channel,
|
||
lastKnownHash: fromHash,
|
||
toHash: toHash,
|
||
}, function (err, data) {
|
||
if (err) { return void console.error(err); }
|
||
if (!Array.isArray(data.messages)) { return void console.error('Not an array!'); }
|
||
|
||
// The first "cp" in history is the empty doc. It doesn't include the first patch
|
||
// of the history
|
||
var initialCp = cpIndex === sortedCp.length || !cp.hash;
|
||
|
||
var messages = (data.messages || []).slice(initialCp ? 0 : 1);
|
||
|
||
if (config.debug) { console.log(data.messages); }
|
||
fillOO(id, messages);
|
||
loading = false;
|
||
config.onCheckpoint(cp);
|
||
$share.show();
|
||
});
|
||
};
|
||
|
||
var onClose = function () { config.setHistory(false); };
|
||
var onRevert = function () {
|
||
config.onRevert();
|
||
};
|
||
|
||
config.setHistory(true);
|
||
|
||
$hist.html('').css('display', 'flex');
|
||
$bottom.hide();
|
||
|
||
UI.spinner($hist).get().show();
|
||
|
||
var $fastPrev, $fastNext, $next;
|
||
|
||
var getId = function () {
|
||
var cps = sortedCp.length;
|
||
return sortedCp[cps - cpIndex -1] || -1;
|
||
};
|
||
|
||
update = function () {
|
||
var cps = sortedCp.length;
|
||
$fastPrev.show();
|
||
$next.show();
|
||
$fastNext.show();
|
||
$hist.find('.cp-toolbar-history-next, .cp-toolbar-history-previous')
|
||
.prop('disabled', '');
|
||
if (cpIndex >= cps && msgIndex === 0) {
|
||
$fastPrev.prop('disabled', 'disabled');
|
||
}
|
||
if (cpIndex === 0) {
|
||
$fastNext.prop('disabled', 'disabled');
|
||
}
|
||
var id = getId();
|
||
var msgs = (ooMessages[id] || []).length;
|
||
if (msgIndex >= (msgs-1)) {
|
||
$next.prop('disabled', 'disabled');
|
||
}
|
||
};
|
||
|
||
var next = function () {
|
||
var id = getId();
|
||
if (!ooMessages[id]) { loading = false; return; }
|
||
var msgs = ooMessages[id];
|
||
msgIndex++;
|
||
var patch = msgs[msgIndex];
|
||
if (!patch) { loading = false; return; }
|
||
config.onPatch(patch);
|
||
showVersion();
|
||
setTimeout(function () {
|
||
$('iframe').blur();
|
||
loading = false;
|
||
}, 200);
|
||
};
|
||
|
||
|
||
// Create the history toolbar
|
||
var display = function () {
|
||
$hist.html('');
|
||
|
||
var fastPrev = h('button.cp-toolbar-history-previous', { title: Messages.history_prev }, [
|
||
h('i.fa.fa-fast-backward'),
|
||
]);
|
||
var fastNext = h('button.cp-toolbar-history-next', { title: Messages.history_next }, [
|
||
h('i.fa.fa-fast-forward'),
|
||
]);
|
||
var _next = h('button.cp-toolbar-history-next', { title: Messages.history_next }, [
|
||
h('i.fa.fa-step-forward')
|
||
]);
|
||
$fastPrev = $(fastPrev);
|
||
$fastNext = $(fastNext).prop('disabled', 'disabled');
|
||
$next = $(_next).prop('disabled', 'disabled');
|
||
|
||
var pos = h('span.cp-history-timeline-pos.fa.fa-caret-down');
|
||
var time = h('div.cp-history-timeline-time');
|
||
var version = h('div.cp-history-timeline-version');
|
||
$time = $(time);
|
||
$version = $(version);
|
||
var bar;
|
||
var timeline = h('div.cp-toolbar-history-timeline', [
|
||
h('div.cp-history-timeline-line', [
|
||
bar = h('span.cp-history-timeline-container')
|
||
]),
|
||
h('div.cp-history-timeline-actions', [
|
||
h('span.cp-history-timeline-prev', [
|
||
fastPrev,
|
||
]),
|
||
time,
|
||
version,
|
||
h('span.cp-history-timeline-next', [
|
||
_next,
|
||
fastNext
|
||
])
|
||
])
|
||
]);
|
||
var snapshot = h('button', {
|
||
title: Messages.snapshots_new,
|
||
}, [
|
||
h('i.fa.fa-camera')
|
||
]);
|
||
var share = h('button', { title: Messages.history_shareTitle }, [
|
||
h('i.fa.fa-shhare-alt'),
|
||
h('span', Messages.shareButton)
|
||
]);
|
||
var restore = h('button', {
|
||
title: Messages.history_restoreTitle,
|
||
}, [
|
||
h('i.fa.fa-check'),
|
||
h('span', Messages.history_restore)
|
||
]);
|
||
var close = h('button', { title: Messages.history_closeTitle }, [
|
||
h('i.fa.fa-times'),
|
||
h('span', Messages.history_close)
|
||
]);
|
||
var actions = h('div.cp-toolbar-history-actions', [
|
||
h('span.cp-history-actions-first', [
|
||
snapshot,
|
||
share
|
||
]),
|
||
h('span.cp-history-actions-last', [
|
||
restore,
|
||
close
|
||
])
|
||
]);
|
||
|
||
if (History.readOnly) {
|
||
snapshot.disabled = true;
|
||
restore.disabled = true;
|
||
}
|
||
|
||
$share = $(share);
|
||
$hist.append([timeline, actions]);
|
||
$(bar).append(pos);
|
||
|
||
var onKeyDown, onKeyUp;
|
||
var closeUI = function () {
|
||
$hist.hide();
|
||
$bottom.show();
|
||
$(window).trigger('resize');
|
||
$(window).off('keydown', onKeyDown);
|
||
$(window).off('keyup', onKeyUp);
|
||
};
|
||
|
||
// Push one patch
|
||
$next.click(function () {
|
||
if (loading) { return; }
|
||
loading = true;
|
||
next();
|
||
update();
|
||
});
|
||
// Go to previous checkpoint
|
||
$fastNext.click(function () {
|
||
if (loading) { return; }
|
||
loading = true;
|
||
cpIndex--;
|
||
loadMoreOOHistory();
|
||
update();
|
||
});
|
||
// Go to next checkpoint
|
||
$fastPrev.click(function () {
|
||
if (loading) { return; }
|
||
loading = true;
|
||
if (msgIndex === -1) {
|
||
cpIndex++;
|
||
}
|
||
loadMoreOOHistory();
|
||
update();
|
||
});
|
||
onKeyDown = function (e) {
|
||
var p = function () { e.preventDefault(); };
|
||
if ([38, 39].indexOf(e.which) >= 0) { p(); return $next.click(); } // Right
|
||
if (e.which === 33) { p(); return $fastNext.click(); } // PageUp
|
||
if (e.which === 34) { p(); return $fastPrev.click(); } // PageUp
|
||
if (e.which === 27) { p(); return $(close).click(); }
|
||
};
|
||
onKeyUp = function (e) { e.stopPropagation(); };
|
||
$(window).on('keydown', onKeyDown).on('keyup', onKeyUp).focus();
|
||
|
||
// Versioned link
|
||
$share.click(function () {
|
||
common.getSframeChannel().event('EV_SHARE_OPEN', {
|
||
versionHash: getVersion()
|
||
});
|
||
});
|
||
$(snapshot).click(function () {
|
||
if (cpIndex === -1 && msgIndex === -1) { return void UI.warn(Messages.snapshots_ooPickVersion); }
|
||
var input = h('input', {
|
||
placeholder: Messages.snapshots_placeholder
|
||
});
|
||
var $input = $(input);
|
||
var content = h('div', [
|
||
h('h5', Messages.snapshots_new),
|
||
input
|
||
]);
|
||
|
||
var buttons = [{
|
||
className: 'cancel',
|
||
name: Messages.filePicker_close,
|
||
onClick: function () {},
|
||
keys: [27],
|
||
}, {
|
||
className: 'primary',
|
||
iconClass: '.fa.fa-camera',
|
||
name: Messages.snapshots_new,
|
||
onClick: function () {
|
||
var val = $input.val();
|
||
if (!val) { return true; }
|
||
config.makeSnapshot(val, function (err) {
|
||
if (err) { return; }
|
||
$input.val('');
|
||
UI.log(Messages.saved);
|
||
}, {
|
||
hash: getVersion(),
|
||
time: currentTime || 0
|
||
});
|
||
},
|
||
keys: [13],
|
||
}];
|
||
|
||
UI.openCustomModal(UI.dialog.customModal(content, {buttons: buttons }));
|
||
setTimeout(function () {
|
||
$input.focus();
|
||
});
|
||
});
|
||
|
||
// Close & restore buttons
|
||
$(close).click(function () {
|
||
History.loading = false;
|
||
onClose();
|
||
closeUI();
|
||
});
|
||
$(restore).click(function () {
|
||
UI.confirm(Messages.history_restorePrompt, function (yes) {
|
||
if (!yes) { return; }
|
||
closeUI();
|
||
History.loading = false;
|
||
onRevert();
|
||
UI.log(Messages.history_restoreDone);
|
||
});
|
||
});
|
||
};
|
||
|
||
display();
|
||
|
||
showVersion(true);
|
||
|
||
//return void loadMoreOOHistory();
|
||
};
|
||
|
||
return History;
|
||
});
|
||
|
||
|