Stackedit/src/components/menus/HistoryMenu.vue
2018-02-01 22:39:14 +00:00

334 lines
8.9 KiB
Vue

<template>
<div class="history side-bar__panel">
<div class="revision" v-for="revision in revisionsWithSpacer" :key="revision.id">
<div class="history__spacer" v-if="revision.spacer"></div>
<a class="revision__button button flex flex--row" href="javascript:void(0)" @click="open(revision)">
<div class="revision__icon">
<user-image :user-id="revision.sub"></user-image>
</div>
<div class="revision__header flex flex--column">
<user-name :user-id="revision.sub"></user-name>
<div class="revision__created">{{revision.created | formatTime}}</div>
</div>
</a>
</div>
<div class="history__spacer history__spacer--last" v-if="revisions.length"></div>
<div class="flex flex--row flex--end" v-if="showMoreButton">
<button class="history__button button" @click="showMore">More</button>
</div>
</div>
</template>
<script>
import { mapMutations } from 'vuex';
import providerRegistry from '../../services/providers/providerRegistry';
import MenuEntry from './common/MenuEntry';
import UserImage from '../UserImage';
import UserName from '../UserName';
import EditorClassApplier from '../common/EditorClassApplier';
import PreviewClassApplier from '../common/PreviewClassApplier';
import utils from '../../services/utils';
import editorSvc from '../../services/editorSvc';
let editorClassAppliers = [];
let previewClassAppliers = [];
let cachedFileId;
let revisionsPromise;
let revisionContentPromises;
const pageSize = 30;
const spacerThreshold = 12 * 60 * 60 * 1000; // 12h
export default {
components: {
MenuEntry,
UserImage,
UserName,
},
data: () => ({
allRevisions: [],
showCount: pageSize,
}),
computed: {
revisions() {
return this.allRevisions.slice(0, this.showCount);
},
revisionsWithSpacer() {
let previousCreated = 0;
return this.revisions.map((revision) => {
const revisionWithSpacer = {
...revision,
spacer: revision.created + spacerThreshold < previousCreated,
};
previousCreated = revision.created;
return revisionWithSpacer;
});
},
showMoreButton() {
return this.showCount < this.allRevisions.length;
},
},
methods: {
...mapMutations('content', [
'setRevisionContent',
]),
close() {
this.$store.dispatch('data/setSideBarPanel', 'menu');
},
showMore() {
this.showCount += pageSize;
},
open(revision) {
let revisionContentPromise = revisionContentPromises[revision.id];
if (!revisionContentPromise) {
revisionContentPromise = new Promise((resolve, reject) => {
const syncToken = this.$store.getters['workspace/syncToken'];
const currentFile = this.$store.getters['file/current'];
this.$store.dispatch('queue/enqueue',
() => Promise.resolve()
.then(() => this.workspaceProvider.getRevisionContent(
syncToken, currentFile.id, revision.id))
.then(resolve, reject));
});
revisionContentPromises[revision.id] = revisionContentPromise;
revisionContentPromise.catch(() => {
revisionContentPromises[revision.id] = null;
});
}
revisionContentPromise.then(revisionContent =>
this.$store.dispatch('content/setRevisionContent', revisionContent));
},
refreshHighlighters() {
const revisionContent = this.$store.state.content.revisionContent;
editorClassAppliers.forEach(editorClassApplier => editorClassApplier.stop());
editorClassAppliers = [];
previewClassAppliers.forEach(previewClassApplier => previewClassApplier.stop());
previewClassAppliers = [];
if (revisionContent) {
editorSvc.$once('sectionDescWithDiffsList', () => {
let offset = 0;
revisionContent.diffs.forEach(([type, text]) => {
if (type) {
const classes = ['revision-diff', `revision-diff--${type > 0 ? 'insert' : 'delete'}`];
const offsets = {
start: offset,
end: offset + text.length,
};
editorClassAppliers.push(new EditorClassApplier(
[`revision-diff--${utils.uid()}`, ...classes], offsets));
previewClassAppliers.push(new PreviewClassApplier(
[`revision-diff--${utils.uid()}`, ...classes], offsets));
}
offset += text.length;
});
});
}
},
},
created() {
// Find the workspace provider
const workspace = this.$store.getters['workspace/currentWorkspace'];
this.workspaceProvider = providerRegistry.providers[workspace.providerId];
// Watch file changes
this.$watch(
() => this.$store.getters['file/current'].id,
(id) => {
this.allRevisions = [];
if (id) {
if (id !== cachedFileId) {
this.setRevisionContent();
cachedFileId = id;
revisionContentPromises = {};
const syncToken = this.$store.getters['workspace/syncToken'];
const currentFile = this.$store.getters['file/current'];
revisionsPromise = new Promise((resolve, reject) => {
this.$store.dispatch('queue/enqueue',
() => Promise.resolve()
.then(() => this.workspaceProvider.listRevisions(syncToken, currentFile.id))
.then(resolve, reject));
});
revisionsPromise.catch(() => {
cachedFileId = null;
return [];
});
}
revisionsPromise.then((revisions) => {
this.allRevisions = revisions;
});
}
}, { immediate: true });
const loadOne = () => {
if (!this.destroyed) {
this.$store.dispatch('queue/enqueue',
() => {
let loadPromise;
this.revisions.some((revision) => {
if (!revision.created) {
const syncToken = this.$store.getters['workspace/syncToken'];
const currentFile = this.$store.getters['file/current'];
loadPromise = this.workspaceProvider
.loadRevision(syncToken, currentFile.id, revision)
.then(() => loadOne());
}
return loadPromise;
});
return loadPromise;
});
}
};
this.$watch(
() => this.revisions,
() => loadOne(),
{ immediate: true });
// Watch diffs changes
this.$watch(
() => this.$store.state.content.revisionContent,
() => this.refreshHighlighters());
// Close revision
this.onKeyup = (evt) => {
if (evt.which === 27) {
// Esc key
this.setRevisionContent();
}
};
window.addEventListener('keyup', this.onKeyup);
},
destroyed() {
// Close revision
this.setRevisionContent();
// Remove highlighters
this.refreshHighlighters();
// Remove event listener
window.removeEventListener('keyup', this.onKeyup);
// Cancel loading revisions
this.destroyed = true;
},
};
</script>
<style lang="scss">
@import '../common/variables.scss';
.history {
padding: 5px 5px 50px;
}
.history__button {
font-size: 14px;
margin-top: 0.5em;
}
.history__spacer {
position: relative;
height: 40px;
&::before {
content: '';
position: absolute;
height: 100%;
top: 0;
left: 24px;
border-left: 2px dotted $hr-color;
}
}
.history__spacer--last {
height: 20px;
}
.revision__button {
text-align: left;
padding: 15px;
height: auto;
text-transform: none;
position: relative;
&::before {
content: '';
position: absolute;
height: 100%;
top: 0;
left: 24px;
border-left: 2px solid $hr-color;
}
&:active,
&:focus,
&:hover {
&::before {
display: none;
}
}
.revision:first-child &::before {
height: 67%;
top: 33%;
}
}
.revision__icon {
height: 20px;
width: 20px;
margin-right: 12px;
flex: none;
border-radius: $border-radius-base;
overflow: hidden;
position: relative;
}
.revision__header {
font-size: 15px;
width: 100%;
}
.revision__created {
font-size: 0.75em;
opacity: 0.5;
}
.layout--revision {
.cledit-section *,
.cl-preview-section * {
color: transparentize($editor-color-light, 0.67) !important;
.app--dark & {
color: transparentize($editor-color-dark, 0.67) !important;
}
}
.cledit-section .revision-diff {
color: $editor-color-light !important;
.app--dark & {
color: $editor-color-dark !important;
}
}
.cl-preview-section .revision-diff {
color: $body-color-light !important;
.app--dark & {
color: $body-color-dark !important;
}
}
.revision-diff {
padding: 0.25em 0;
&.revision-diff--insert {
background-color: mix(#fff, $selection-highlighting-color, 60%);
}
&.revision-diff--delete {
background-color: mix(#fff, $error-color, 60%);
text-decoration: line-through;
}
}
}
</style>