mirror of
https://gitee.com/mafgwo/stackedit
synced 2024-11-16 11:42:23 +08:00
Revision history
This commit is contained in:
parent
815c87b5e3
commit
6b6ef52bb4
|
@ -21,6 +21,10 @@ import store from '../store';
|
|||
Vue.directive('focus', {
|
||||
inserted(el) {
|
||||
el.focus();
|
||||
const value = el.value;
|
||||
if (value && el.setSelectionRange) {
|
||||
el.setSelectionRange(0, value.length);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -16,11 +16,11 @@ export default {
|
|||
}
|
||||
if (scrollElt) {
|
||||
const clEditor = cledit(preElt, scrollElt);
|
||||
clEditor.on('contentChanged', value => this.$emit('changed', value));
|
||||
clEditor.init({
|
||||
content: this.value,
|
||||
sectionHighlighter: section => Prism.highlight(section.text, Prism.languages[this.lang]),
|
||||
});
|
||||
clEditor.on('contentChanged', value => this.$emit('changed', value));
|
||||
clEditor.toggleEditable(!this.disabled);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="layout">
|
||||
<div class="layout" :class="{'layout--revision': revisionContent}">
|
||||
<div class="layout__panel flex flex--row" :class="{'flex--end': styles.showSideBar}">
|
||||
<div class="layout__panel layout__panel--explorer" v-show="styles.showExplorer" :aria-hidden="!styles.showExplorer" :style="{width: styles.layoutOverflow ? '100%' : constants.explorerWidth + 'px'}">
|
||||
<explorer></explorer>
|
||||
|
@ -75,6 +75,9 @@ export default {
|
|||
FindReplace,
|
||||
},
|
||||
computed: {
|
||||
...mapState('content', [
|
||||
'revisionContent',
|
||||
]),
|
||||
...mapState('discussion', [
|
||||
'stickyComment',
|
||||
]),
|
||||
|
@ -155,11 +158,6 @@ $editor-background: #fff;
|
|||
.sticky-comment,
|
||||
.current-discussion {
|
||||
background-color: mix(#000, $editor-background, 6.7%);
|
||||
}
|
||||
|
||||
.comment-list__current-discussion,
|
||||
.sticky-comment,
|
||||
.current-discussion {
|
||||
border-color: $editor-background;
|
||||
}
|
||||
}
|
||||
|
@ -177,11 +175,6 @@ $preview-background: #f3f3f3;
|
|||
.sticky-comment,
|
||||
.current-discussion {
|
||||
background-color: mix(#000, $preview-background, 6.7%);
|
||||
}
|
||||
|
||||
.comment-list__current-discussion,
|
||||
.sticky-comment,
|
||||
.current-discussion {
|
||||
border-color: $preview-background;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -189,7 +189,7 @@ export default {
|
|||
|
||||
.modal__inner-2 {
|
||||
margin: 40px 10px 100px;
|
||||
background-color: #fff;
|
||||
background-color: #f8f8f8;
|
||||
padding: 40px 50px 30px;
|
||||
border-radius: $border-radius-base;
|
||||
position: relative;
|
||||
|
|
|
@ -1,88 +1,58 @@
|
|||
<template>
|
||||
<nav class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor}">
|
||||
<nav class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor && !revisionContent}">
|
||||
<!-- Explorer -->
|
||||
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
|
||||
<button class="navigation-bar__button button" @click="toggleExplorer()" v-title="'Toggle explorer'">
|
||||
<icon-folder></icon-folder>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="toggleExplorer()" v-title="'Toggle explorer'"><icon-folder></icon-folder></button>
|
||||
</div>
|
||||
<!-- Side bar -->
|
||||
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button">
|
||||
<button class="navigation-bar__button navigation-bar__button--stackedit button" @click="toggleSideBar()" v-title="'Toggle side bar'">
|
||||
<icon-provider provider-id="stackedit"></icon-provider>
|
||||
</button>
|
||||
<button class="navigation-bar__button navigation-bar__button--stackedit button" @click="toggleSideBar()" v-title="'Toggle side bar'"><icon-provider provider-id="stackedit"></icon-provider></button>
|
||||
</div>
|
||||
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--title flex flex--row">
|
||||
<!-- Spinner -->
|
||||
<div class="navigation-bar__spinner">
|
||||
<div v-if="!offline && showSpinner" class="spinner"></div>
|
||||
<icon-sync-off v-if="offline"></icon-sync-off>
|
||||
</div>
|
||||
<!-- Title -->
|
||||
<div class="navigation-bar__title navigation-bar__title--fake text-input"></div>
|
||||
<div class="navigation-bar__title navigation-bar__title--text text-input" :style="{width: titleWidth + 'px'}">{{title}}</div>
|
||||
<input class="navigation-bar__title navigation-bar__title--input text-input" :class="{'navigation-bar__title--focus': titleFocus, 'navigation-bar__title--scrolling': titleScrolling}" :style="{width: titleWidth + 'px'}" @focus="editTitle(true)" @blur="editTitle(false)" @keyup.enter="submitTitle()" @keyup.esc="submitTitle(true)" @mouseenter="titleHover = true" @mouseleave="titleHover = false" v-model="title">
|
||||
<!-- Sync/Publish -->
|
||||
<div class="flex flex--row" :class="{'navigation-bar__hidden': styles.hideLocations}">
|
||||
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in syncLocations" :key="location.id" :href="location.url" target="_blank" v-title="'Synchronized location'">
|
||||
<icon-provider :provider-id="location.providerId"></icon-provider>
|
||||
</a>
|
||||
<button class="navigation-bar__button navigation-bar__button--sync button" :disabled="!isSyncPossible || isSyncRequested || offline" @click="requestSync" v-title="'Synchronize now'">
|
||||
<icon-sync></icon-sync>
|
||||
</button>
|
||||
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in publishLocations" :key="location.id" :href="location.url" target="_blank" v-title="'Publish location'">
|
||||
<icon-provider :provider-id="location.providerId"></icon-provider>
|
||||
</a>
|
||||
<button class="navigation-bar__button navigation-bar__button--publish button" :disabled="!publishLocations.length || isPublishRequested || offline" @click="requestPublish"v-title="'Publish now'">
|
||||
<icon-upload></icon-upload>
|
||||
</button>
|
||||
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in syncLocations" :key="location.id" :href="location.url" target="_blank" v-title="'Synchronized location'"><icon-provider :provider-id="location.providerId"></icon-provider></a>
|
||||
<button class="navigation-bar__button navigation-bar__button--sync button" :disabled="!isSyncPossible || isSyncRequested || offline" @click="requestSync" v-title="'Synchronize now'"><icon-sync></icon-sync></button>
|
||||
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in publishLocations" :key="location.id" :href="location.url" target="_blank" v-title="'Publish location'"><icon-provider :provider-id="location.providerId"></icon-provider></a>
|
||||
<button class="navigation-bar__button navigation-bar__button--publish button" :disabled="!publishLocations.length || isPublishRequested || offline" @click="requestPublish"v-title="'Publish now'"><icon-upload></icon-upload></button>
|
||||
</div>
|
||||
<!-- Revision -->
|
||||
<div class="flex flex--row" v-if="revisionContent">
|
||||
<button class="navigation-bar__button navigation-bar__button--revision navigation-bar__button--restore button" @click="restoreRevision">Restore</button>
|
||||
<button class="navigation-bar__button navigation-bar__button--revision button" @click="setRevisionContent()" v-title="'Close revision'"><icon-close></icon-close></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="navigation-bar__inner navigation-bar__inner--edit-buttons">
|
||||
<button class="navigation-bar__button button" @click="undo" v-title="'Undo'" :disabled="!canUndo">
|
||||
<icon-undo></icon-undo>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="redo" v-title="'Redo'" :disabled="!canRedo">
|
||||
<icon-redo></icon-redo>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="undo" v-title="'Undo'" :disabled="!canUndo"><icon-undo></icon-undo></button>
|
||||
<button class="navigation-bar__button button" @click="redo" v-title="'Redo'" :disabled="!canRedo"><icon-redo></icon-redo></button>
|
||||
<div class="navigation-bar__spacer"></div>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('bold')" v-title="'Bold'">
|
||||
<icon-format-bold></icon-format-bold>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('italic')" v-title="'Italic'">
|
||||
<icon-format-italic></icon-format-italic>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('strikethrough')" v-title="'Strikethrough'">
|
||||
<icon-format-strikethrough></icon-format-strikethrough>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('heading')" v-title="'Heading'">
|
||||
<icon-format-size></icon-format-size>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('ulist')" v-title="'Unordered list'">
|
||||
<icon-format-list-bulleted></icon-format-list-bulleted>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('olist')" v-title="'Ordered list'">
|
||||
<icon-format-list-numbers></icon-format-list-numbers>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('table')" v-title="'Table'">
|
||||
<icon-table></icon-table>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('quote')" v-title="'Blockquote'">
|
||||
<icon-format-quote-close></icon-format-quote-close>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('code')" v-title="'Code'">
|
||||
<icon-code-tags></icon-code-tags>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('link')" v-title="'Link'">
|
||||
<icon-link-variant></icon-link-variant>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('image')" v-title="'Image'">
|
||||
<icon-file-image></icon-file-image>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('hr')" v-title="'Horizontal rule'">
|
||||
<icon-format-horizontal-rule></icon-format-horizontal-rule>
|
||||
</button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('bold')" v-title="'Bold'"><icon-format-bold></icon-format-bold></button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('italic')" v-title="'Italic'"><icon-format-italic></icon-format-italic></button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('strikethrough')" v-title="'Strikethrough'"><icon-format-strikethrough></icon-format-strikethrough></button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('heading')" v-title="'Heading'"><icon-format-size></icon-format-size></button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('ulist')" v-title="'Unordered list'"><icon-format-list-bulleted></icon-format-list-bulleted></button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('olist')" v-title="'Ordered list'"><icon-format-list-numbers></icon-format-list-numbers></button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('table')" v-title="'Table'"><icon-table></icon-table></button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('quote')" v-title="'Blockquote'"><icon-format-quote-close></icon-format-quote-close></button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('code')" v-title="'Code'"><icon-code-tags></icon-code-tags></button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('link')" v-title="'Link'"><icon-link-variant></icon-link-variant></button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('image')" v-title="'Image'"><icon-file-image></icon-file-image></button>
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('hr')" v-title="'Horizontal rule'"><icon-format-horizontal-rule></icon-format-horizontal-rule></button>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import { mapState, mapMutations, mapGetters, mapActions } from 'vuex';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
import syncSvc from '../services/syncSvc';
|
||||
import publishSvc from '../services/publishSvc';
|
||||
|
@ -109,6 +79,9 @@ export default {
|
|||
'canUndo',
|
||||
'canRedo',
|
||||
]),
|
||||
...mapState('content', [
|
||||
'revisionContent',
|
||||
]),
|
||||
...mapGetters('layout', [
|
||||
'styles',
|
||||
]),
|
||||
|
@ -155,6 +128,12 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('content', [
|
||||
'setRevisionContent',
|
||||
]),
|
||||
...mapActions('content', [
|
||||
'restoreRevision',
|
||||
]),
|
||||
...mapActions('data', [
|
||||
'toggleExplorer',
|
||||
'toggleSideBar',
|
||||
|
@ -176,9 +155,7 @@ export default {
|
|||
}
|
||||
},
|
||||
pagedownClick(name) {
|
||||
if (this.$store.getters['content/current'].id &&
|
||||
this.$store.getters['layout/styles'].showEditor
|
||||
) {
|
||||
if (this.$store.getters['content/isCurrentEditable']) {
|
||||
editorSvc.pagedownEditor.uiManager.doClick(name);
|
||||
}
|
||||
},
|
||||
|
@ -292,8 +269,30 @@ $button-size: 36px;
|
|||
}
|
||||
}
|
||||
|
||||
.navigation-bar__button--revision {
|
||||
width: 38px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-bar__button--restore {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.navigation-bar__title {
|
||||
margin: 0 4px;
|
||||
font-size: 22px;
|
||||
|
||||
.layout--revision & {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-bar__title,
|
||||
|
@ -301,7 +300,6 @@ $button-size: 36px;
|
|||
display: inline-block;
|
||||
color: $navbar-color;
|
||||
background-color: transparent;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.navigation-bar__button--sync,
|
||||
|
@ -385,6 +383,10 @@ $button-size: 36px;
|
|||
}
|
||||
}
|
||||
|
||||
.navigation-bar__button--revision {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.navigation-bar__title--input {
|
||||
cursor: pointer;
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
<main-menu v-if="panel === 'menu'"></main-menu>
|
||||
<sync-menu v-else-if="panel === 'sync'"></sync-menu>
|
||||
<publish-menu v-else-if="panel === 'publish'"></publish-menu>
|
||||
<history-menu v-else-if="panel === 'history'"></history-menu>
|
||||
<export-menu v-else-if="panel === 'export'"></export-menu>
|
||||
<more-menu v-else-if="panel === 'more'"></more-menu>
|
||||
<div v-else-if="panel === 'help'" class="side-bar__panel side-bar__panel--help">
|
||||
|
@ -34,6 +35,7 @@ import Toc from './Toc';
|
|||
import MainMenu from './menus/MainMenu';
|
||||
import SyncMenu from './menus/SyncMenu';
|
||||
import PublishMenu from './menus/PublishMenu';
|
||||
import HistoryMenu from './menus/HistoryMenu';
|
||||
import ExportMenu from './menus/ExportMenu';
|
||||
import MoreMenu from './menus/MoreMenu';
|
||||
import markdownSample from '../data/markdownSample.md';
|
||||
|
@ -45,6 +47,7 @@ const panelNames = {
|
|||
toc: 'Table of contents',
|
||||
sync: 'Synchronize',
|
||||
publish: 'Publish',
|
||||
history: 'File history',
|
||||
export: 'Export to disk',
|
||||
more: 'More',
|
||||
};
|
||||
|
@ -55,6 +58,7 @@ export default {
|
|||
MainMenu,
|
||||
SyncMenu,
|
||||
PublishMenu,
|
||||
HistoryMenu,
|
||||
ExportMenu,
|
||||
MoreMenu,
|
||||
},
|
||||
|
@ -90,6 +94,7 @@ export default {
|
|||
hr {
|
||||
margin: 10px 40px;
|
||||
display: none;
|
||||
border-top: 1px solid $hr-color;
|
||||
}
|
||||
|
||||
* + hr {
|
||||
|
|
|
@ -24,6 +24,7 @@ export default {
|
|||
.user-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
|
|
|
@ -37,20 +37,24 @@ export default class PreviewClassApplier {
|
|||
|
||||
applyClass() {
|
||||
const offset = this.offsetGetter();
|
||||
if (offset && offset.start !== offset.end) {
|
||||
const start = cledit.Utils.findContainer(
|
||||
editorSvc.previewElt, Math.min(offset.start, offset.end));
|
||||
const end = cledit.Utils.findContainer(
|
||||
editorSvc.previewElt, Math.max(offset.start, offset.end));
|
||||
const range = document.createRange();
|
||||
range.setStart(start.container, start.offsetInContainer);
|
||||
range.setEnd(end.container, end.offsetInContainer);
|
||||
const properties = {
|
||||
...this.properties,
|
||||
className: this.classGetter().join(' '),
|
||||
};
|
||||
utils.wrapRange(range, properties);
|
||||
this.lastEltCount = this.eltCollection.length;
|
||||
if (offset) {
|
||||
const offsetStart = editorSvc.getPreviewOffset(offset.start, editorSvc.sectionDescList);
|
||||
const offsetEnd = editorSvc.getPreviewOffset(offset.end, editorSvc.sectionDescList);
|
||||
if (offsetStart != null && offsetEnd != null && offsetStart !== offsetEnd) {
|
||||
const start = cledit.Utils.findContainer(
|
||||
editorSvc.previewElt, Math.min(offsetStart, offsetEnd));
|
||||
const end = cledit.Utils.findContainer(
|
||||
editorSvc.previewElt, Math.max(offsetStart, offsetEnd));
|
||||
const range = document.createRange();
|
||||
range.setStart(start.container, start.offsetInContainer);
|
||||
range.setEnd(end.container, end.offsetInContainer);
|
||||
const properties = {
|
||||
...this.properties,
|
||||
className: this.classGetter().join(' '),
|
||||
};
|
||||
utils.wrapRange(range, properties);
|
||||
this.lastEltCount = this.eltCollection.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -113,7 +113,7 @@ textarea {
|
|||
}
|
||||
|
||||
.textfield {
|
||||
background-color: transparent;
|
||||
background-color: #fff;
|
||||
border: 0;
|
||||
font-family: inherit;
|
||||
font-weight: 400;
|
||||
|
@ -131,7 +131,7 @@ textarea {
|
|||
|
||||
&[disabled] {
|
||||
cursor: not-allowed;
|
||||
background-color: #f8f8f8;
|
||||
background-color: #f2f2f2;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
@ -164,6 +164,10 @@ textarea {
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.side-title {
|
||||
height: 44px;
|
||||
line-height: 36px;
|
||||
|
@ -210,6 +214,10 @@ textarea {
|
|||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
|
||||
& > * {
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.gutter__background {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
html,
|
||||
body {
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
color: $body-color;
|
||||
font-size: 16px;
|
||||
font-family: $font-family-main;
|
||||
font-variant-ligatures: common-ligatures;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
$font-family-main: Lato, 'Helvetica Neue', Helvetica, sans-serif;
|
||||
$font-family-monospace: 'Roboto Mono', 'Lucida Sans Typewriter', 'Lucida Console', monaco, Courrier, monospace;
|
||||
$body-color: rgba(0, 0, 0, 0.75);
|
||||
$line-height-base: 1.67;
|
||||
$line-height-title: 1.33;
|
||||
$font-size-monospace: 0.85em;
|
||||
|
@ -9,7 +10,7 @@ $selection-highlighting-color: #ff9632;
|
|||
$info-bg: transparentize($selection-highlighting-color, 0.85);
|
||||
$code-border-radius: 2px;
|
||||
$link-color: #0c93e4;
|
||||
$error-color: #f20;
|
||||
$error-color: #f31;
|
||||
$border-radius-base: 2px;
|
||||
$hr-color: rgba(128, 128, 128, 0.2);
|
||||
$navbar-color: rgba(255, 255, 255, 0.67);
|
||||
|
|
|
@ -163,6 +163,14 @@ export default {
|
|||
() => this.updateStickyTrigger,
|
||||
() => this.updateSticky(),
|
||||
{ immediate: true });
|
||||
|
||||
// Move preview discussions once sectionDescWithDiffsList have been calculated
|
||||
if (!editorSvc.sectionDescWithDiffsList) {
|
||||
editorSvc.$once('sectionDescWithDiffsList', () => {
|
||||
this.updateTops();
|
||||
this.updateSticky();
|
||||
});
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.scrollerElt.removeEventListener('scroll', this.updateSticky);
|
||||
|
@ -199,10 +207,6 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* use div selector to avoid collision with Prism */
|
||||
div.comment {
|
||||
padding: 5px 10px 10px;
|
||||
|
|
|
@ -21,7 +21,11 @@ export default {
|
|||
clearTimeout(this.timeout);
|
||||
this.timeout = setTimeout(() => {
|
||||
let offset;
|
||||
if (editorSvc.clEditor.selectionMgr.hasFocus()) {
|
||||
// Show the button if content is not a revision and has the focus
|
||||
if (
|
||||
!this.$store.state.content.revisionContent &&
|
||||
editorSvc.clEditor.selectionMgr.hasFocus()
|
||||
) {
|
||||
this.selection = editorSvc.getTrimmedSelection();
|
||||
if (this.selection) {
|
||||
const text = editorSvc.clEditor.getContent();
|
||||
|
|
|
@ -21,7 +21,11 @@ export default {
|
|||
clearTimeout(this.timeout);
|
||||
this.timeout = setTimeout(() => {
|
||||
let offset;
|
||||
if (editorSvc.previewSelectionRange) {
|
||||
// Show the button if content is not a revision and preview selection is not empty
|
||||
if (
|
||||
!this.$store.state.content.revisionContent &&
|
||||
editorSvc.previewSelectionRange
|
||||
) {
|
||||
this.selection = editorSvc.getTrimmedSelection();
|
||||
if (this.selection) {
|
||||
const text = editorSvc.previewTextWithDiffsList;
|
||||
|
|
260
src/components/menus/HistoryMenu.vue
Normal file
260
src/components/menus/HistoryMenu.vue
Normal file
|
@ -0,0 +1,260 @@
|
|||
<template>
|
||||
<div class="side-bar__panel side-bar__panel--history">
|
||||
<a class="revision button flex flex--row" href="javascript:void(0)" v-for="revision in revisions" :key="revision.id" @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 class="flex flex--row flex--end" v-if="showMoreButton">
|
||||
<button class="revision__button button" @click="showMore">More</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations } from 'vuex';
|
||||
import googleDriveAppDataProvider from '../../services/providers/googleDriveAppDataProvider';
|
||||
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 = 50;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MenuEntry,
|
||||
UserImage,
|
||||
UserName,
|
||||
},
|
||||
data: () => ({
|
||||
allRevisions: [],
|
||||
showCount: pageSize,
|
||||
}),
|
||||
computed: {
|
||||
revisions() {
|
||||
return this.allRevisions.slice(0, this.showCount);
|
||||
},
|
||||
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 loginToken = this.$store.getters['data/loginToken'];
|
||||
const currentFile = this.$store.getters['file/current'];
|
||||
this.$store.dispatch('queue/enqueue',
|
||||
() => Promise.resolve()
|
||||
.then(() => googleDriveAppDataProvider.getRevisionContent(
|
||||
loginToken, 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() {
|
||||
// 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 loginToken = this.$store.getters['data/loginToken'];
|
||||
const currentFile = this.$store.getters['file/current'];
|
||||
revisionsPromise = new Promise((resolve, reject) => {
|
||||
this.$store.dispatch('queue/enqueue',
|
||||
() => Promise.resolve()
|
||||
.then(() => googleDriveAppDataProvider.listRevisions(loginToken, currentFile.id))
|
||||
.then((revisions) => {
|
||||
resolve(revisions.sort(
|
||||
(revision1, revision2) => revision2.created - revision1.created));
|
||||
})
|
||||
.catch(reject));
|
||||
});
|
||||
revisionsPromise.catch(() => {
|
||||
cachedFileId = null;
|
||||
return [];
|
||||
});
|
||||
}
|
||||
revisionsPromise.then((revisions) => {
|
||||
this.allRevisions = revisions;
|
||||
});
|
||||
}
|
||||
}, { 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);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../common/variables.scss';
|
||||
|
||||
.side-bar__panel--history {
|
||||
padding: 5px 5px 50px;
|
||||
}
|
||||
|
||||
.revision {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
&: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;
|
||||
}
|
||||
|
||||
.revision__button {
|
||||
font-size: 14px;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.layout--revision {
|
||||
.cledit-section *,
|
||||
.cl-preview-section * {
|
||||
color: rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.cledit-section .revision-diff {
|
||||
color: $editor-color !important;
|
||||
}
|
||||
|
||||
.cl-preview-section .revision-diff {
|
||||
color: $body-color !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>
|
|
@ -28,6 +28,12 @@
|
|||
<div>File properties</div>
|
||||
<span>Add metadata and configure extensions.</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="history">
|
||||
<icon-history slot="icon"></icon-history>
|
||||
<div>File history</div>
|
||||
<span>Track and restore file revisions.</span>
|
||||
</menu-entry>
|
||||
<hr>
|
||||
<menu-entry @click.native="setPanel('toc')">
|
||||
<icon-toc slot="icon"></icon-toc>
|
||||
Table of contents
|
||||
|
@ -113,6 +119,17 @@ export default {
|
|||
return this.$store.dispatch('modal/open', 'fileProperties')
|
||||
.catch(() => {}); // Cancel
|
||||
},
|
||||
history() {
|
||||
const loginToken = this.$store.getters['data/loginToken'];
|
||||
if (!loginToken) {
|
||||
this.$store.dispatch('modal/signInForHistory')
|
||||
.then(() => googleHelper.signin())
|
||||
.then(() => syncSvc.requestSync())
|
||||
.catch(() => { }); // Cancel
|
||||
} else {
|
||||
this.setPanel('history');
|
||||
}
|
||||
},
|
||||
print() {
|
||||
print();
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<a href="javascript:void(0)" class="menu-entry button flex flex--row flex--align-center">
|
||||
<a class="menu-entry button flex flex--row flex--align-center" href="javascript:void(0)">
|
||||
<div class="menu-entry__icon flex flex--column flex--center">
|
||||
<slot name="icon"></slot>
|
||||
</div>
|
||||
|
@ -17,6 +17,7 @@
|
|||
padding: 10px;
|
||||
height: auto;
|
||||
font-size: 17px;
|
||||
line-height: 1.5;
|
||||
text-transform: none;
|
||||
|
||||
div div {
|
||||
|
|
|
@ -53,7 +53,7 @@ export default {
|
|||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
color: rgba(0, 0, 0, 0.2);
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
padding: 2px;
|
||||
|
@ -61,7 +61,7 @@ export default {
|
|||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
color: rgba(0, 0, 0, 0.67);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
5
src/icons/History.vue
Normal file
5
src/icons/History.vue
Normal file
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
|
||||
<path d="M11,7V12.11L15.71,14.9L16.5,13.62L12.5,11.25V7M12.5,2C8.97,2 5.91,3.92 4.27,6.77L2,4.5V11H8.5L5.75,8.25C6.96,5.73 9.5,4 12.5,4C16.64,4 20,7.36 20,11.5C20,15.64 16.64,19 12.5,19C9.23,19 6.47,16.91 5.44,14H3.34C4.44,18.03 8.11,21 12.5,21C17.74,21 22,16.75 22,11.5C22,6.25 17.75,2 12.5,2Z" />
|
||||
</svg>
|
||||
</template>
|
|
@ -47,6 +47,7 @@ import Undo from './Undo';
|
|||
import Redo from './Redo';
|
||||
import ContentSave from './ContentSave';
|
||||
import Message from './Message';
|
||||
import History from './History';
|
||||
|
||||
Vue.component('iconProvider', Provider);
|
||||
Vue.component('iconFormatBold', FormatBold);
|
||||
|
@ -96,3 +97,4 @@ Vue.component('iconUndo', Undo);
|
|||
Vue.component('iconRedo', Redo);
|
||||
Vue.component('iconContentSave', ContentSave);
|
||||
Vue.component('iconMessage', Message);
|
||||
Vue.component('iconHistory', History);
|
||||
|
|
|
@ -124,9 +124,17 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||
for (let i = 0; i < item[1].length; i += 1) {
|
||||
const section = this.conversionCtx.sectionList[sectionIdx];
|
||||
if (item[0] === 0) {
|
||||
const sectionDesc = this.sectionDescList[sectionDescIdx];
|
||||
let sectionDesc = this.sectionDescList[sectionDescIdx];
|
||||
sectionDescIdx += 1;
|
||||
sectionDesc.editorElt = section.elt;
|
||||
if (sectionDesc.editorElt !== section.elt) {
|
||||
// Force textToPreviewDiffs computation
|
||||
sectionDesc = {
|
||||
...sectionDesc,
|
||||
section,
|
||||
editorElt: section.elt,
|
||||
textToPreviewDiffs: null,
|
||||
};
|
||||
}
|
||||
newSectionDescList.push(sectionDesc);
|
||||
previewHtml += sectionDesc.html;
|
||||
sectionIdx += 1;
|
||||
|
@ -230,7 +238,8 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||
}, 500),
|
||||
|
||||
/**
|
||||
* Make the diff between editor's markdown and preview's html.
|
||||
* Compute the diffs between editor's markdown and preview's html
|
||||
* asynchronously unless there is only one section to compute.
|
||||
*/
|
||||
makeTextToPreviewDiffs() {
|
||||
if (editorSvc.sectionDescList &&
|
||||
|
@ -244,7 +253,9 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||
if (hasOne) {
|
||||
return true;
|
||||
}
|
||||
sectionDesc.previewText = sectionDesc.previewElt.textContent;
|
||||
if (!sectionDesc.previewText) {
|
||||
sectionDesc.previewText = sectionDesc.previewElt.textContent;
|
||||
}
|
||||
sectionDesc.textToPreviewDiffs = diffMatchPatch.diff_main(
|
||||
sectionDesc.section.text, sectionDesc.previewText);
|
||||
hasOne = true;
|
||||
|
@ -540,7 +551,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||
|
||||
// Disable editor if hidden or if no content is loaded
|
||||
store.watch(
|
||||
() => store.getters['content/current'].id && store.getters['layout/styles'].showEditor,
|
||||
() => store.getters['content/isCurrentEditable'],
|
||||
editable => this.clEditor.toggleEditable(!!editable), {
|
||||
immediate: true,
|
||||
});
|
||||
|
|
|
@ -217,26 +217,28 @@ export default {
|
|||
store.watch(
|
||||
() => store.getters['discussion/currentFileDiscussions'],
|
||||
(discussions) => {
|
||||
const classGetter = (type, discussionId) => () => {
|
||||
const classes = [`discussion-${type}-highlighting--${discussionId}`, `discussion-${type}-highlighting`];
|
||||
if (store.state.discussion.currentDiscussionId === discussionId) {
|
||||
classes.push(`discussion-${type}-highlighting--selected`);
|
||||
}
|
||||
return classes;
|
||||
};
|
||||
const offsetGetter = discussionId => () => {
|
||||
const startMarker = discussionMarkers[`${discussionId}:start`];
|
||||
const endMarker = discussionMarkers[`${discussionId}:end`];
|
||||
return startMarker && endMarker && {
|
||||
start: startMarker.offset,
|
||||
end: endMarker.offset,
|
||||
};
|
||||
};
|
||||
|
||||
// Editor class appliers
|
||||
const oldEditorClassAppliers = editorClassAppliers;
|
||||
editorClassAppliers = {};
|
||||
Object.keys(discussions).forEach((discussionId) => {
|
||||
const classApplier = oldEditorClassAppliers[discussionId] || new EditorClassApplier(
|
||||
() => {
|
||||
const classes = [`discussion-editor-highlighting--${discussionId}`, 'discussion-editor-highlighting'];
|
||||
if (store.state.discussion.currentDiscussionId === discussionId) {
|
||||
classes.push('discussion-editor-highlighting--selected');
|
||||
}
|
||||
return classes;
|
||||
},
|
||||
() => {
|
||||
const startMarker = discussionMarkers[`${discussionId}:start`];
|
||||
const endMarker = discussionMarkers[`${discussionId}:end`];
|
||||
return startMarker && endMarker && {
|
||||
start: startMarker.offset,
|
||||
end: endMarker.offset,
|
||||
};
|
||||
}, { discussionId });
|
||||
classGetter('editor', discussionId), offsetGetter(discussionId), { discussionId });
|
||||
editorClassAppliers[discussionId] = classApplier;
|
||||
});
|
||||
Object.keys(oldEditorClassAppliers).forEach((discussionId) => {
|
||||
|
@ -250,21 +252,7 @@ export default {
|
|||
previewClassAppliers = {};
|
||||
Object.keys(discussions).forEach((discussionId) => {
|
||||
const classApplier = oldPreviewClassAppliers[discussionId] || new PreviewClassApplier(
|
||||
() => {
|
||||
const classes = [`discussion-preview-highlighting--${discussionId}`, 'discussion-preview-highlighting'];
|
||||
if (store.state.discussion.currentDiscussionId === discussionId) {
|
||||
classes.push('discussion-preview-highlighting--selected');
|
||||
}
|
||||
return classes;
|
||||
},
|
||||
() => {
|
||||
const startMarker = discussionMarkers[`${discussionId}:start`];
|
||||
const endMarker = discussionMarkers[`${discussionId}:end`];
|
||||
return startMarker && endMarker && {
|
||||
start: this.getPreviewOffset(startMarker.offset),
|
||||
end: this.getPreviewOffset(endMarker.offset),
|
||||
};
|
||||
}, { discussionId });
|
||||
classGetter('preview', discussionId), offsetGetter(discussionId), { discussionId });
|
||||
previewClassAppliers[discussionId] = classApplier;
|
||||
});
|
||||
Object.keys(oldPreviewClassAppliers).forEach((discussionId) => {
|
||||
|
|
|
@ -52,12 +52,14 @@ export default {
|
|||
restoreScrollPosition() {
|
||||
const scrollPosition = store.getters['contentState/current'].scrollPosition;
|
||||
if (scrollPosition && this.sectionDescMeasuredList) {
|
||||
const objectToScroll = this.getObjectToScroll();
|
||||
const sectionDesc = this.sectionDescMeasuredList[scrollPosition.sectionIdx];
|
||||
if (sectionDesc) {
|
||||
const scrollTop = sectionDesc[objectToScroll.dimensionKey].startOffset +
|
||||
(sectionDesc[objectToScroll.dimensionKey].height * scrollPosition.posInSection);
|
||||
objectToScroll.elt.scrollTop = Math.floor(scrollTop);
|
||||
const editorScrollTop = sectionDesc.editorDimension.startOffset +
|
||||
(sectionDesc.editorDimension.height * scrollPosition.posInSection);
|
||||
this.editorElt.parentNode.scrollTop = Math.floor(editorScrollTop);
|
||||
const previewScrollTop = sectionDesc.previewDimension.startOffset +
|
||||
(sectionDesc.previewDimension.height * scrollPosition.posInSection);
|
||||
this.previewElt.parentNode.scrollTop = Math.floor(previewScrollTop);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -65,13 +67,17 @@ export default {
|
|||
/**
|
||||
* Get the offset in the preview corresponding to the offset of the markdown in the editor
|
||||
*/
|
||||
getPreviewOffset(editorOffset) {
|
||||
if (!this.sectionDescWithDiffsList) {
|
||||
getPreviewOffset(editorOffset, sectionDescList = this.sectionDescWithDiffsList) {
|
||||
if (!sectionDescList) {
|
||||
return null;
|
||||
}
|
||||
let offset = editorOffset;
|
||||
let previewOffset = 0;
|
||||
this.sectionDescWithDiffsList.some((sectionDesc) => {
|
||||
sectionDescList.some((sectionDesc) => {
|
||||
if (!sectionDesc.textToPreviewDiffs) {
|
||||
previewOffset = null;
|
||||
return true;
|
||||
}
|
||||
if (sectionDesc.section.text.length >= offset) {
|
||||
previewOffset += diffMatchPatch.diff_xIndex(sectionDesc.textToPreviewDiffs, offset);
|
||||
return true;
|
||||
|
@ -86,13 +92,17 @@ export default {
|
|||
/**
|
||||
* Get the offset of the markdown in the editor corresponding to the offset in the preview
|
||||
*/
|
||||
getEditorOffset(previewOffset) {
|
||||
if (!this.sectionDescWithDiffsList) {
|
||||
getEditorOffset(previewOffset, sectionDescList = this.sectionDescWithDiffsList) {
|
||||
if (!sectionDescList) {
|
||||
return null;
|
||||
}
|
||||
let offset = previewOffset;
|
||||
let editorOffset = 0;
|
||||
this.sectionDescWithDiffsList.some((sectionDesc) => {
|
||||
sectionDescList.some((sectionDesc) => {
|
||||
if (!sectionDesc.textToPreviewDiffs) {
|
||||
editorOffset = null;
|
||||
return true;
|
||||
}
|
||||
if (sectionDesc.previewText.length >= offset) {
|
||||
const previewToTextDiffs = sectionDesc.textToPreviewDiffs
|
||||
.map(diff => [-diff[0], diff[1]]);
|
||||
|
|
|
@ -4,8 +4,7 @@ import editorSvc from '../../services/editorSvc';
|
|||
import syncSvc from '../../services/syncSvc';
|
||||
|
||||
// Skip shortcuts if modal is open or editor is hidden
|
||||
Mousetrap.prototype.stopCallback = () => store.getters['modal/config'] ||
|
||||
!store.getters['content/current'].id || !store.getters['layout/styles'].showEditor;
|
||||
Mousetrap.prototype.stopCallback = () => store.getters['modal/config'] || !store.getters['content/isCurrentEditable'];
|
||||
|
||||
const pagedownHandler = name => () => {
|
||||
editorSvc.pagedownEditor.uiManager.doClick(name);
|
||||
|
|
|
@ -114,4 +114,18 @@ export default providerRegistry.register({
|
|||
},
|
||||
}));
|
||||
},
|
||||
listRevisions(token, fileId) {
|
||||
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
|
||||
return googleHelper.getFileRevisions(token, syncData.id)
|
||||
.then(revisions => revisions.map(revision => ({
|
||||
id: revision.id,
|
||||
sub: revision.lastModifyingUser && revision.lastModifyingUser.permissionId,
|
||||
created: new Date(revision.modifiedTime).getTime(),
|
||||
})));
|
||||
},
|
||||
getRevisionContent(token, fileId, revisionId) {
|
||||
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
|
||||
return googleHelper.downloadFileRevision(token, syncData.id, revisionId)
|
||||
.then(content => JSON.parse(content));
|
||||
},
|
||||
});
|
||||
|
|
|
@ -337,6 +337,44 @@ export default {
|
|||
url: `https://www.googleapis.com/drive/v3/files/${id}`,
|
||||
})));
|
||||
},
|
||||
getFileRevisions(token, id) {
|
||||
return this.refreshToken(token, driveAppDataScopes)
|
||||
.then((refreshedToken) => {
|
||||
const revisions = [];
|
||||
const getPage = pageToken => this.request(refreshedToken, {
|
||||
method: 'GET',
|
||||
url: `https://www.googleapis.com/drive/v3/files/${id}/revisions`,
|
||||
params: {
|
||||
pageToken,
|
||||
pageSize: 1000,
|
||||
fields: 'nextPageToken,revisions(id,modifiedTime,lastModifyingUser/permissionId,lastModifyingUser/displayName,lastModifyingUser/photoLink)',
|
||||
},
|
||||
}).then((res) => {
|
||||
res.body.revisions.forEach((revision) => {
|
||||
store.commit('userInfo/addItem', {
|
||||
id: revision.lastModifyingUser.permissionId,
|
||||
name: revision.lastModifyingUser.displayName,
|
||||
imageUrl: revision.lastModifyingUser.photoLink,
|
||||
});
|
||||
revisions.push(revision);
|
||||
});
|
||||
if (res.body.nextPageToken) {
|
||||
return getPage(res.body.nextPageToken);
|
||||
}
|
||||
return revisions;
|
||||
});
|
||||
|
||||
return getPage();
|
||||
});
|
||||
},
|
||||
downloadFileRevision(token, fileId, revisionId) {
|
||||
return this.refreshToken(token, driveAppDataScopes)
|
||||
.then(refreshedToken => this.request(refreshedToken, {
|
||||
method: 'GET',
|
||||
url: `https://www.googleapis.com/drive/v3/files/${fileId}/revisions/${revisionId}?alt=media`,
|
||||
raw: true,
|
||||
}).then(res => res.body));
|
||||
},
|
||||
uploadBlogger(
|
||||
token, blogUrl, blogId, postId, title, content, labels, isDraft, published, isPage,
|
||||
) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import googleHelper from '../services/providers/helpers/googleHelper';
|
||||
import googleHelper from './providers/helpers/googleHelper';
|
||||
import store from '../store';
|
||||
|
||||
const promised = {};
|
||||
|
@ -19,8 +19,10 @@ export default {
|
|||
if (!store.state.offline) {
|
||||
promised[userId] = true;
|
||||
googleHelper.getUser(userId)
|
||||
.catch(() => {
|
||||
promised[userId] = false;
|
||||
.catch((err) => {
|
||||
if (err.status !== 404) {
|
||||
promised[userId] = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,103 @@
|
|||
import DiffMatchPatch from 'diff-match-patch';
|
||||
import moduleTemplate from './moduleTemplate';
|
||||
import empty from '../data/emptyContent';
|
||||
import utils from '../services/utils';
|
||||
import cledit from '../libs/cledit';
|
||||
|
||||
const diffMatchPatch = new DiffMatchPatch();
|
||||
|
||||
const module = moduleTemplate(empty);
|
||||
|
||||
module.state = {
|
||||
...module.state,
|
||||
revisionContent: null,
|
||||
};
|
||||
|
||||
module.mutations = {
|
||||
...module.mutations,
|
||||
setRevisionContent: (state, value) => {
|
||||
if (value) {
|
||||
state.revisionContent = {
|
||||
...empty(),
|
||||
...value,
|
||||
id: utils.uid(),
|
||||
hash: Date.now(),
|
||||
};
|
||||
} else {
|
||||
state.revisionContent = null;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.getters = {
|
||||
...module.getters,
|
||||
current: (state, getters, rootState, rootGetters) =>
|
||||
state.itemMap[`${rootGetters['file/current'].id}/content`] || empty(),
|
||||
current: (state, getters, rootState, rootGetters) => {
|
||||
if (state.revisionContent) {
|
||||
return state.revisionContent;
|
||||
}
|
||||
return state.itemMap[`${rootGetters['file/current'].id}/content`] || empty();
|
||||
},
|
||||
currentProperties: (state, getters) => utils.computeProperties(getters.current.properties),
|
||||
isCurrentEditable: (state, getters, rootState, rootGetters) =>
|
||||
!state.revisionContent &&
|
||||
getters.current.id &&
|
||||
rootGetters['layout/styles'].showEditor,
|
||||
};
|
||||
|
||||
module.actions = {
|
||||
...module.actions,
|
||||
patchCurrent({ getters, commit }, value) {
|
||||
commit('patchItem', {
|
||||
...value,
|
||||
id: getters.current.id,
|
||||
});
|
||||
patchCurrent({ state, getters, commit }, value) {
|
||||
const id = getters.current.id;
|
||||
if (id && !state.revisionContent) {
|
||||
commit('patchItem', {
|
||||
...value,
|
||||
id,
|
||||
});
|
||||
}
|
||||
},
|
||||
setRevisionContent({ state, rootGetters, commit }, value) {
|
||||
const currentFile = rootGetters['file/current'];
|
||||
const currentContent = state.itemMap[`${currentFile.id}/content`];
|
||||
if (currentContent) {
|
||||
const diffs = diffMatchPatch.diff_main(currentContent.text, value.text);
|
||||
diffMatchPatch.diff_cleanupSemantic(diffs);
|
||||
commit('setRevisionContent', {
|
||||
text: diffs.map(([, text]) => text).join(''),
|
||||
diffs,
|
||||
originalText: value.text,
|
||||
});
|
||||
}
|
||||
},
|
||||
restoreRevision({ state, getters, commit, dispatch }) {
|
||||
const revisionContent = state.revisionContent;
|
||||
if (revisionContent) {
|
||||
dispatch('modal/fileRestoration', null, { root: true })
|
||||
.then(() => {
|
||||
// Close revision
|
||||
commit('setRevisionContent');
|
||||
const currentContent = utils.deepCopy(getters.current);
|
||||
if (currentContent) {
|
||||
// Restore text and move discussions
|
||||
const diffs = diffMatchPatch.diff_main(
|
||||
currentContent.text, revisionContent.originalText);
|
||||
diffMatchPatch.diff_cleanupSemantic(diffs);
|
||||
Object.keys(currentContent.discussions).forEach((discussionId) => {
|
||||
const discussion = currentContent.discussions[discussionId];
|
||||
const adjustOffset = (offsetName) => {
|
||||
const marker = new cledit.Marker(discussion[offsetName], offsetName === 'end');
|
||||
marker.adjustOffset(diffs);
|
||||
discussion[offsetName] = marker.offset;
|
||||
};
|
||||
adjustOffset('start');
|
||||
adjustOffset('end');
|
||||
});
|
||||
dispatch('patchCurrent', {
|
||||
...currentContent,
|
||||
text: revisionContent.originalText,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -70,6 +70,11 @@ export default {
|
|||
content: '<p>Files in the trash are automatically deleted after 7 days of inactivity.</p>',
|
||||
resolveText: 'Ok',
|
||||
}),
|
||||
fileRestoration: ({ dispatch }) => dispatch('open', {
|
||||
content: '<p>You are about to revert some changes. Are you sure?</p>',
|
||||
resolveText: 'Yes, revert',
|
||||
rejectText: 'No',
|
||||
}),
|
||||
reset: ({ dispatch }) => dispatch('open', {
|
||||
content: '<p>This will clean your local files and settings. Are you sure?</p>',
|
||||
resolveText: 'Yes, clean',
|
||||
|
@ -83,13 +88,19 @@ export default {
|
|||
}),
|
||||
signInForSponsorship: ({ dispatch }) => dispatch('open', {
|
||||
type: 'signInForSponsorship',
|
||||
content: `<p>You have to sign in with <b>Google</b> to enable your sponsorship.</p>
|
||||
content: `<p>You have to sign in with Google to enable your sponsorship.</p>
|
||||
<div class="modal__info"><b>Note:</b> This will sync all your files and settings.</div>`,
|
||||
resolveText: 'Ok, sign in',
|
||||
rejectText: 'Cancel',
|
||||
}),
|
||||
signInForComment: ({ dispatch }) => dispatch('open', {
|
||||
content: `<p>You have to sign in with <b>Google</b> to start commenting.</p>
|
||||
content: `<p>You have to sign in with Google to start commenting.</p>
|
||||
<div class="modal__info"><b>Note:</b> This will sync all your files and settings.</div>`,
|
||||
resolveText: 'Ok, sign in',
|
||||
rejectText: 'Cancel',
|
||||
}),
|
||||
signInForHistory: ({ dispatch }) => dispatch('open', {
|
||||
content: `<p>You have to sign in with Google to enable revision history.</p>
|
||||
<div class="modal__info"><b>Note:</b> This will sync all your files and settings.</div>`,
|
||||
resolveText: 'Ok, sign in',
|
||||
rejectText: 'Cancel',
|
||||
|
|
|
@ -37,7 +37,8 @@ export default {
|
|||
}
|
||||
const newQueue = queue
|
||||
.then(() => checkOffline())
|
||||
.then(() => cb()
|
||||
.then(() => Promise.resolve()
|
||||
.then(() => cb())
|
||||
.catch((err) => {
|
||||
console.error(err); // eslint-disable-line no-console
|
||||
checkOffline();
|
||||
|
|
Loading…
Reference in New Issue
Block a user