mirror of
https://gitee.com/mafgwo/stackedit
synced 2024-11-16 11:42:23 +08:00
Merge branch 'dev'
This commit is contained in:
commit
beac7fb1a3
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
@ -18,6 +18,7 @@ import SplashScreen from './SplashScreen';
|
|||
import syncSvc from '../services/syncSvc';
|
||||
import networkSvc from '../services/networkSvc';
|
||||
import sponsorSvc from '../services/sponsorSvc';
|
||||
import tempFileSvc from '../services/tempFileSvc';
|
||||
import timeSvc from '../services/timeSvc';
|
||||
import store from '../store';
|
||||
|
||||
|
@ -94,6 +95,7 @@ export default {
|
|||
networkSvc.init();
|
||||
sponsorSvc.init();
|
||||
this.ready = true;
|
||||
tempFileSvc.setReady();
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err && err.message !== 'reload') {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="button-bar">
|
||||
<div class="button-bar__inner button-bar__inner--top">
|
||||
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.showNavigationBar }" @click="toggleNavigationBar()" v-title="'Toggle navigation bar'">
|
||||
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.showNavigationBar }" v-if="!light" @click="toggleNavigationBar()" v-title="'Toggle navigation bar'">
|
||||
<icon-navigation-bar></icon-navigation-bar>
|
||||
</button>
|
||||
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.showSidePreview }" tour-step-anchor="editor" @click="toggleSidePreview()" v-title="'Toggle side preview'">
|
||||
|
@ -26,12 +26,17 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
computed: mapGetters('data', [
|
||||
'layoutSettings',
|
||||
]),
|
||||
computed: {
|
||||
...mapState([
|
||||
'light',
|
||||
]),
|
||||
...mapGetters('data', [
|
||||
'layoutSettings',
|
||||
]),
|
||||
},
|
||||
methods: mapActions('data', [
|
||||
'toggleNavigationBar',
|
||||
'toggleEditor',
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<pre class="editor__inner markdown-highlighting" :style="{padding: styles.editorPadding}" :class="{monospaced: computedSettings.editor.monospacedFontOnly}"></pre>
|
||||
<div class="gutter" :style="{left: styles.editorGutterLeft + 'px'}">
|
||||
<comment-list v-if="styles.editorGutterWidth"></comment-list>
|
||||
<editor-new-discussion-button></editor-new-discussion-button>
|
||||
<editor-new-discussion-button v-if="!isCurrentTemp"></editor-new-discussion-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -19,6 +19,9 @@ export default {
|
|||
EditorNewDiscussionButton,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('file', [
|
||||
'isCurrentTemp',
|
||||
]),
|
||||
...mapGetters('layout', [
|
||||
'styles',
|
||||
]),
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<icon-close></icon-close>
|
||||
</button>
|
||||
</div>
|
||||
<div class="explorer__tree" :class="{'explorer__tree--new-item': !newChildNode.isNil}" tabindex="0">
|
||||
<div class="explorer__tree" :class="{'explorer__tree--new-item': !newChildNode.isNil}" v-if="!light" tabindex="0" @keyup.delete="deleteItem()">
|
||||
<explorer-node :node="rootNode" :depth="0"></explorer-node>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -34,6 +34,9 @@ export default {
|
|||
ExplorerNode,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'light',
|
||||
]),
|
||||
...mapState('explorer', [
|
||||
'newChildNode',
|
||||
]),
|
||||
|
@ -58,7 +61,7 @@ export default {
|
|||
},
|
||||
},
|
||||
created() {
|
||||
this.$store.watch(
|
||||
this.$watch(
|
||||
() => this.$store.getters['file/current'].id,
|
||||
(currentFileId) => {
|
||||
this.$store.commit('explorer/setSelectedId', currentFileId);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--open': isOpen, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="setDragTarget(node.item.id)" @dragleave.stop="isDragTarget && setDragTargetId()" @drop.prevent.stop="onDrop" @contextmenu="onContextMenu">
|
||||
<div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--open': isOpen, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="node.noDrop || setDragTarget(node.item.id)" @dragleave.stop="isDragTarget && setDragTargetId()" @drop.prevent.stop="onDrop" @contextmenu="onContextMenu">
|
||||
<div class="explorer-node__item-editor" v-if="isEditing" :class="['explorer-node__item-editor--' + node.item.type]" :style="{paddingLeft: leftPadding}" draggable="true" @dragstart.stop.prevent>
|
||||
<input type="text" class="text-input" v-focus @blur="submitEdit()" @keydown.enter="submitEdit()" @keydown.esc="submitEdit(true)" v-model="editingNodeName">
|
||||
</div>
|
||||
|
@ -169,7 +169,7 @@ export default {
|
|||
perform: () => this.newItem(false),
|
||||
}, {
|
||||
name: 'New folder',
|
||||
disabled: !this.node.isFolder || this.node.isTrash,
|
||||
disabled: !this.node.isFolder || this.node.isTrash || this.node.isTemp,
|
||||
perform: () => this.newItem(true),
|
||||
}, {
|
||||
type: 'separator',
|
||||
|
@ -179,7 +179,6 @@ export default {
|
|||
perform: () => this.setEditingId(this.node.item.id),
|
||||
}, {
|
||||
name: 'Delete',
|
||||
disabled: this.node.isTrash || this.node.item.parentId === 'trash',
|
||||
perform: () => this.deleteItem(),
|
||||
}],
|
||||
})
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
<side-bar></side-bar>
|
||||
</div>
|
||||
</div>
|
||||
<tour v-if="!layoutSettings.welcomeTourFinished"></tour>
|
||||
<tour v-if="!light && !layoutSettings.welcomeTourFinished"></tour>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -78,6 +78,9 @@ export default {
|
|||
FindReplace,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
'light',
|
||||
]),
|
||||
...mapState('content', [
|
||||
'revisionContent',
|
||||
]),
|
||||
|
@ -116,8 +119,13 @@ export default {
|
|||
editorSvc.init(editorElt, previewElt, tocElt);
|
||||
|
||||
// Focus on the editor every time reader mode is disabled
|
||||
this.$watch(() => this.styles.showEditor,
|
||||
showEditor => showEditor && editorSvc.clEditor.focus());
|
||||
const focus = () => {
|
||||
if (this.styles.showEditor) {
|
||||
editorSvc.clEditor.focus();
|
||||
}
|
||||
};
|
||||
setTimeout(focus, 100);
|
||||
this.$watch(() => this.styles.showEditor, focus);
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('resize', this.updateStyle);
|
||||
|
|
|
@ -1,40 +1,6 @@
|
|||
<template>
|
||||
<div class="modal" @keydown.esc="onEscape" @keydown.tab="onTab">
|
||||
<file-properties-modal v-if="config.type === 'fileProperties'"></file-properties-modal>
|
||||
<settings-modal v-else-if="config.type === 'settings'"></settings-modal>
|
||||
<templates-modal v-else-if="config.type === 'templates'"></templates-modal>
|
||||
<about-modal v-else-if="config.type === 'about'"></about-modal>
|
||||
<html-export-modal v-else-if="config.type === 'htmlExport'"></html-export-modal>
|
||||
<pdf-export-modal v-else-if="config.type === 'pdfExport'"></pdf-export-modal>
|
||||
<pandoc-export-modal v-else-if="config.type === 'pandocExport'"></pandoc-export-modal>
|
||||
<link-modal v-else-if="config.type === 'link'"></link-modal>
|
||||
<image-modal v-else-if="config.type === 'image'"></image-modal>
|
||||
<sync-management-modal v-else-if="config.type === 'syncManagement'"></sync-management-modal>
|
||||
<publish-management-modal v-else-if="config.type === 'publishManagement'"></publish-management-modal>
|
||||
<workspace-management-modal v-else-if="config.type === 'workspaceManagement'"></workspace-management-modal>
|
||||
<sponsor-modal v-else-if="config.type === 'sponsor'"></sponsor-modal>
|
||||
<!-- Providers -->
|
||||
<google-photo-modal v-else-if="config.type === 'googlePhoto'"></google-photo-modal>
|
||||
<google-drive-account-modal v-else-if="config.type === 'googleDriveAccount'"></google-drive-account-modal>
|
||||
<google-drive-save-modal v-else-if="config.type === 'googleDriveSave'"></google-drive-save-modal>
|
||||
<google-drive-workspace-modal v-else-if="config.type === 'googleDriveWorkspace'"></google-drive-workspace-modal>
|
||||
<google-drive-publish-modal v-else-if="config.type === 'googleDrivePublish'"></google-drive-publish-modal>
|
||||
<dropbox-account-modal v-else-if="config.type === 'dropboxAccount'"></dropbox-account-modal>
|
||||
<dropbox-save-modal v-else-if="config.type === 'dropboxSave'"></dropbox-save-modal>
|
||||
<dropbox-publish-modal v-else-if="config.type === 'dropboxPublish'"></dropbox-publish-modal>
|
||||
<github-account-modal v-else-if="config.type === 'githubAccount'"></github-account-modal>
|
||||
<github-open-modal v-else-if="config.type === 'githubOpen'"></github-open-modal>
|
||||
<github-save-modal v-else-if="config.type === 'githubSave'"></github-save-modal>
|
||||
<github-publish-modal v-else-if="config.type === 'githubPublish'"></github-publish-modal>
|
||||
<gist-sync-modal v-else-if="config.type === 'gistSync'"></gist-sync-modal>
|
||||
<gist-publish-modal v-else-if="config.type === 'gistPublish'"></gist-publish-modal>
|
||||
<wordpress-publish-modal v-else-if="config.type === 'wordpressPublish'"></wordpress-publish-modal>
|
||||
<blogger-publish-modal v-else-if="config.type === 'bloggerPublish'"></blogger-publish-modal>
|
||||
<blogger-page-publish-modal v-else-if="config.type === 'bloggerPagePublish'"></blogger-page-publish-modal>
|
||||
<zendesk-account-modal v-else-if="config.type === 'zendeskAccount'"></zendesk-account-modal>
|
||||
<zendesk-publish-modal v-else-if="config.type === 'zendeskPublish'"></zendesk-publish-modal>
|
||||
<couchdb-workspace-modal v-else-if="config.type === 'couchdbWorkspace'"></couchdb-workspace-modal>
|
||||
<couchdb-credentials-modal v-else-if="config.type === 'couchdbCredentials'"></couchdb-credentials-modal>
|
||||
<component v-if="currentModalComponent" :is="currentModalComponent"></component>
|
||||
<modal-inner v-else aria-label="Dialog">
|
||||
<div class="modal__content" v-html="config.content"></div>
|
||||
<div class="modal__button-bar">
|
||||
|
@ -129,9 +95,22 @@ export default {
|
|||
CouchdbWorkspaceModal,
|
||||
CouchdbCredentialsModal,
|
||||
},
|
||||
computed: mapGetters('modal', [
|
||||
'config',
|
||||
]),
|
||||
computed: {
|
||||
...mapGetters('modal', [
|
||||
'config',
|
||||
]),
|
||||
currentModalComponent() {
|
||||
if (this.config.type) {
|
||||
let componentName = this.config.type[0].toUpperCase();
|
||||
componentName += this.config.type.slice(1);
|
||||
componentName += 'Modal';
|
||||
if (this.$options.components[componentName]) {
|
||||
return componentName;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onEscape() {
|
||||
this.config.reject();
|
||||
|
|
|
@ -2,11 +2,13 @@
|
|||
<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" tour-step-anchor="explorer" @click="toggleExplorer()" v-title="'Toggle explorer'"><icon-folder></icon-folder></button>
|
||||
<button class="navigation-bar__button button" v-if="light" @click="close()" v-title="'Close StackEdit'"><icon-close></icon-close></button>
|
||||
<button class="navigation-bar__button button" v-else tour-step-anchor="explorer" @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" tour-step-anchor="menu" @click="toggleSideBar()" v-title="'Toggle side bar'"><icon-provider provider-id="stackedit"></icon-provider></button>
|
||||
<a class="navigation-bar__button navigation-bar__button--stackedit button" v-if="light" href="app" target="_blank" v-title="'Open StackEdit'"><icon-provider provider-id="stackedit"></icon-provider></a>
|
||||
<button class="navigation-bar__button navigation-bar__button--stackedit button" v-else tour-step-anchor="menu" @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 -->
|
||||
|
@ -57,6 +59,7 @@ import editorSvc from '../services/editorSvc';
|
|||
import syncSvc from '../services/syncSvc';
|
||||
import publishSvc from '../services/publishSvc';
|
||||
import animationSvc from '../services/animationSvc';
|
||||
import tempFileSvc from '../services/tempFileSvc';
|
||||
import utils from '../services/utils';
|
||||
|
||||
export default {
|
||||
|
@ -68,6 +71,7 @@ export default {
|
|||
}),
|
||||
computed: {
|
||||
...mapState([
|
||||
'light',
|
||||
'offline',
|
||||
]),
|
||||
...mapState('queue', [
|
||||
|
@ -104,9 +108,7 @@ export default {
|
|||
}
|
||||
this.titleFakeElt.textContent = this.title;
|
||||
const width = this.titleFakeElt.getBoundingClientRect().width + 2; // 2px for the caret
|
||||
return width < this.styles.titleMaxWidth
|
||||
? width
|
||||
: this.styles.titleMaxWidth;
|
||||
return Math.min(width, this.styles.titleMaxWidth);
|
||||
},
|
||||
titleScrolling() {
|
||||
const result = this.titleHover && !this.titleFocus;
|
||||
|
@ -178,9 +180,12 @@ export default {
|
|||
}
|
||||
this.titleInputElt.blur();
|
||||
},
|
||||
close() {
|
||||
tempFileSvc.close();
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$store.watch(
|
||||
this.$watch(
|
||||
() => this.$store.getters['file/current'].name,
|
||||
(name) => {
|
||||
this.title = name;
|
||||
|
@ -213,7 +218,7 @@ export default {
|
|||
float: left;
|
||||
|
||||
&.navigation-bar__inner--button {
|
||||
margin-right: 15px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -255,6 +260,7 @@ $button-size: 36px;
|
|||
.navigation-bar__button {
|
||||
width: $button-size;
|
||||
padding: 0 8px;
|
||||
transition: opacity 0.25s;
|
||||
|
||||
.navigation-bar__inner--button & {
|
||||
padding: 0 4px;
|
||||
|
@ -290,7 +296,7 @@ $button-size: 36px;
|
|||
|
||||
.navigation-bar__title {
|
||||
margin: 0 4px;
|
||||
font-size: 22px;
|
||||
font-size: 21px;
|
||||
|
||||
.layout--revision & {
|
||||
position: absolute;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</div>
|
||||
<div class="gutter" :style="{left: styles.previewGutterLeft + 'px'}">
|
||||
<comment-list v-if="styles.previewGutterWidth"></comment-list>
|
||||
<preview-new-discussion-button></preview-new-discussion-button>
|
||||
<preview-new-discussion-button v-if="!isCurrentTemp"></preview-new-discussion-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!styles.showEditor" class="preview__corner">
|
||||
|
@ -32,9 +32,14 @@ export default {
|
|||
data: () => ({
|
||||
previewTop: true,
|
||||
}),
|
||||
computed: mapGetters('layout', [
|
||||
'styles',
|
||||
]),
|
||||
computed: {
|
||||
...mapGetters('file', [
|
||||
'isCurrentTemp',
|
||||
]),
|
||||
...mapGetters('layout', [
|
||||
'styles',
|
||||
]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions('data', [
|
||||
'toggleEditor',
|
||||
|
|
|
@ -59,7 +59,7 @@ export default {
|
|||
created() {
|
||||
editorSvc.$on('sectionList', () => this.computeText());
|
||||
editorSvc.$on('selectionRange', () => this.computeText());
|
||||
editorSvc.$on('previewText', () => this.computeHtml());
|
||||
editorSvc.$on('previewCtx', () => this.computeHtml());
|
||||
editorSvc.$on('previewSelectionRange', () => this.computeHtml());
|
||||
},
|
||||
|
||||
|
@ -92,7 +92,7 @@ export default {
|
|||
this.htmlSelection = true;
|
||||
if (!text) {
|
||||
this.htmlSelection = false;
|
||||
text = editorSvc.previewText;
|
||||
text = editorSvc.previewCtx.text;
|
||||
}
|
||||
if (text != null) {
|
||||
this.htmlStats.forEach((stat) => {
|
||||
|
|
|
@ -30,7 +30,7 @@ export default {
|
|||
e.preventDefault();
|
||||
const y = e.clientY - tocElt.getBoundingClientRect().top;
|
||||
|
||||
editorSvc.sectionDescList.some((sectionDesc) => {
|
||||
editorSvc.previewCtx.sectionDescList.some((sectionDesc) => {
|
||||
if (y >= sectionDesc.tocDimension.endOffset) {
|
||||
return false;
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ export default {
|
|||
const updateMaskY = () => {
|
||||
const scrollPosition = editorSvc.getScrollPosition();
|
||||
if (scrollPosition) {
|
||||
const sectionDesc = editorSvc.sectionDescList[scrollPosition.sectionIdx];
|
||||
const sectionDesc = editorSvc.previewCtx.sectionDescList[scrollPosition.sectionIdx];
|
||||
this.maskY = sectionDesc.tocDimension.startOffset +
|
||||
(scrollPosition.posInSection * sectionDesc.tocDimension.height);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ export default class PreviewClassApplier {
|
|||
this.lastEltCount = this.eltCollection.length;
|
||||
|
||||
this.restoreClass = () => {
|
||||
if (!editorSvc.sectionDescWithDiffsList) {
|
||||
if (!editorSvc.previewCtxWithDiffs) {
|
||||
this.removeClass();
|
||||
} else if (!this.eltCollection.length || this.eltCollection.length !== this.lastEltCount) {
|
||||
this.removeClass();
|
||||
|
@ -31,15 +31,17 @@ export default class PreviewClassApplier {
|
|||
}
|
||||
};
|
||||
|
||||
editorSvc.$on('sectionDescWithDiffsList', this.restoreClass);
|
||||
editorSvc.$on('previewCtxWithDiffs', this.restoreClass);
|
||||
nextTick(() => this.applyClass());
|
||||
}
|
||||
|
||||
applyClass() {
|
||||
const offset = this.offsetGetter();
|
||||
if (offset) {
|
||||
const offsetStart = editorSvc.getPreviewOffset(offset.start, editorSvc.sectionDescList);
|
||||
const offsetEnd = editorSvc.getPreviewOffset(offset.end, editorSvc.sectionDescList);
|
||||
const offsetStart = editorSvc.getPreviewOffset(
|
||||
offset.start, editorSvc.previewCtx.sectionDescList);
|
||||
const offsetEnd = editorSvc.getPreviewOffset(
|
||||
offset.end, editorSvc.previewCtx.sectionDescList);
|
||||
if (offsetStart != null && offsetEnd != null && offsetStart !== offsetEnd) {
|
||||
const start = cledit.Utils.findContainer(
|
||||
editorSvc.previewElt, Math.min(offsetStart, offsetEnd));
|
||||
|
@ -63,8 +65,7 @@ export default class PreviewClassApplier {
|
|||
}
|
||||
|
||||
stop() {
|
||||
editorSvc.$off('previewHtml', this.restoreClass);
|
||||
editorSvc.$off('sectionDescWithDiffsList', this.restoreClass);
|
||||
editorSvc.$off('previewCtxWithDiffs', this.restoreClass);
|
||||
nextTick(() => this.removeClass());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -163,9 +163,9 @@ export default {
|
|||
() => this.updateSticky(),
|
||||
{ immediate: true });
|
||||
|
||||
// Move preview discussions once sectionDescWithDiffsList have been calculated
|
||||
if (!editorSvc.sectionDescWithDiffsList) {
|
||||
editorSvc.$once('sectionDescWithDiffsList', () => {
|
||||
// Move preview discussions once previewCtxWithDiffs has been calculated
|
||||
if (!editorSvc.previewCtxWithDiffs) {
|
||||
editorSvc.$once('previewCtxWithDiffs', () => {
|
||||
this.updateTops();
|
||||
this.updateSticky();
|
||||
});
|
||||
|
|
|
@ -48,6 +48,7 @@ export default {
|
|||
'previousDiscussionId',
|
||||
'nextDiscussionId',
|
||||
'currentFileDiscussions',
|
||||
'currentDiscussionLastCommentId',
|
||||
]),
|
||||
...mapGetters('layout', [
|
||||
'constants',
|
||||
|
@ -59,7 +60,7 @@ export default {
|
|||
return this.nextDiscussionId && this.nextDiscussionId !== this.currentDiscussionId;
|
||||
},
|
||||
showRemove() {
|
||||
return this.currentFileDiscussions[this.currentDiscussionId];
|
||||
return this.currentDiscussionLastCommentId;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -28,7 +28,7 @@ export default {
|
|||
) {
|
||||
this.selection = editorSvc.getTrimmedSelection();
|
||||
if (this.selection) {
|
||||
const text = editorSvc.previewTextWithDiffsList;
|
||||
const text = editorSvc.previewCtxWithDiffs.text;
|
||||
offset = editorSvc.getPreviewOffset(this.selection.end);
|
||||
while (offset && text[offset - 1] === '\n') {
|
||||
offset -= 1;
|
||||
|
|
|
@ -4,6 +4,12 @@
|
|||
<p>You have to <a href="javascript:void(0)" @click="signin">sign in with Google</a> to enable revision history.</p>
|
||||
<p><b>Note:</b> This will sync your main workspace.</p>
|
||||
</div>
|
||||
<div class="side-bar__info" v-if="loading">
|
||||
<p>Loading history…</p>
|
||||
</div>
|
||||
<div class="side-bar__info" v-else-if="!revisionsWithSpacer.length">
|
||||
<p><b>{{currentFileName}}</b> has no history.</p>
|
||||
</div>
|
||||
<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)">
|
||||
|
@ -53,12 +59,16 @@ export default {
|
|||
},
|
||||
data: () => ({
|
||||
allRevisions: [],
|
||||
loading: false,
|
||||
showCount: pageSize,
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters('workspace', [
|
||||
'syncToken',
|
||||
]),
|
||||
currentFileName() {
|
||||
return this.$store.getters['file/current'].name;
|
||||
},
|
||||
revisions() {
|
||||
return this.allRevisions.slice(0, this.showCount);
|
||||
},
|
||||
|
@ -127,7 +137,7 @@ export default {
|
|||
previewClassAppliers.forEach(previewClassApplier => previewClassApplier.stop());
|
||||
previewClassAppliers = [];
|
||||
if (revisionContent) {
|
||||
editorSvc.$once('sectionDescWithDiffsList', () => {
|
||||
editorSvc.$once('previewCtxWithDiffs', () => {
|
||||
let offset = 0;
|
||||
revisionContent.diffs.forEach(([type, text]) => {
|
||||
if (type) {
|
||||
|
@ -177,7 +187,9 @@ export default {
|
|||
});
|
||||
}
|
||||
if (revisionsPromise) {
|
||||
this.loading = true;
|
||||
revisionsPromise.then((revisions) => {
|
||||
this.loading = false;
|
||||
this.allRevisions = revisions;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -61,14 +61,14 @@
|
|||
Markdown cheat sheet
|
||||
</menu-entry>
|
||||
<hr>
|
||||
<menu-entry @click.native="setPanel('export')">
|
||||
<icon-content-save slot="icon"></icon-content-save>
|
||||
Export to disk
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="setPanel('import')">
|
||||
<icon-content-save slot="icon"></icon-content-save>
|
||||
Import from disk
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="setPanel('export')">
|
||||
<icon-content-save slot="icon"></icon-content-save>
|
||||
Export to disk
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="print">
|
||||
<icon-printer slot="icon"></icon-printer>
|
||||
Print
|
||||
|
|
|
@ -1,99 +1,104 @@
|
|||
<template>
|
||||
<div class="side-bar__panel side-bar__panel--menu">
|
||||
<div class="side-bar__info" v-if="noToken">
|
||||
<p>You have to <b>link an account</b> to start publishing files.</p>
|
||||
<div class="side-bar__info" v-if="isCurrentTemp">
|
||||
<p><b>{{currentFileName}}</b> can not be published as it's a temporary file.</p>
|
||||
</div>
|
||||
<div class="side-bar__info" v-if="publishLocations.length">
|
||||
<p><b>{{currentFileName}}</b> is already published.</p>
|
||||
<menu-entry @click.native="requestPublish">
|
||||
<icon-upload slot="icon"></icon-upload>
|
||||
<div>Publish now</div>
|
||||
<span>Update current file publications.</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="managePublish">
|
||||
<icon-view-list slot="icon"></icon-view-list>
|
||||
<div>File publication</div>
|
||||
<span>Manage current file publication locations.</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<hr>
|
||||
<div v-for="token in googleDriveTokens" :key="token.sub">
|
||||
<menu-entry @click.native="publishGoogleDrive(token)">
|
||||
<div v-else>
|
||||
<div class="side-bar__info" v-if="noToken">
|
||||
<p>You have to <b>link an account</b> to start publishing files.</p>
|
||||
</div>
|
||||
<div class="side-bar__info" v-if="publishLocations.length">
|
||||
<p><b>{{currentFileName}}</b> is already published.</p>
|
||||
<menu-entry @click.native="requestPublish">
|
||||
<icon-upload slot="icon"></icon-upload>
|
||||
<div>Publish now</div>
|
||||
<span>Update current file publications.</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="managePublish">
|
||||
<icon-view-list slot="icon"></icon-view-list>
|
||||
<div>File publication</div>
|
||||
<span>Manage current file publication locations.</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<hr>
|
||||
<div v-for="token in googleDriveTokens" :key="token.sub">
|
||||
<menu-entry @click.native="publishGoogleDrive(token)">
|
||||
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||
<div>Publish to Google Drive</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<div v-for="token in dropboxTokens" :key="token.sub">
|
||||
<menu-entry @click.native="publishDropbox(token)">
|
||||
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||
<div>Publish to Dropbox</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<div v-for="token in githubTokens" :key="token.sub">
|
||||
<menu-entry @click.native="publishGithub(token)">
|
||||
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||
<div>Publish to GitHub</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="publishGist(token)">
|
||||
<icon-provider slot="icon" provider-id="gist"></icon-provider>
|
||||
<div>Publish to Gist</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<div v-for="token in wordpressTokens" :key="token.sub">
|
||||
<menu-entry @click.native="publishWordpress(token)">
|
||||
<icon-provider slot="icon" provider-id="wordpress"></icon-provider>
|
||||
<div>Publish to WordPress</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<div v-for="token in bloggerTokens" :key="token.sub">
|
||||
<menu-entry @click.native="publishBlogger(token)">
|
||||
<icon-provider slot="icon" provider-id="blogger"></icon-provider>
|
||||
<div>Publish to Blogger</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="publishBloggerPage(token)">
|
||||
<icon-provider slot="icon" provider-id="bloggerPage"></icon-provider>
|
||||
<div>Publish to Blogger Page</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<div v-for="token in zendeskTokens" :key="token.sub">
|
||||
<menu-entry @click.native="publishZendesk(token)">
|
||||
<icon-provider slot="icon" provider-id="zendesk"></icon-provider>
|
||||
<div>Publish to Zendesk Help Center</div>
|
||||
<span>{{token.name}} — {{token.subdomain}}</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<hr>
|
||||
<menu-entry @click.native="addGoogleDriveAccount">
|
||||
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||
<div>Publish to Google Drive</div>
|
||||
<span>{{token.name}}</span>
|
||||
<span>Add Google Drive account</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<div v-for="token in dropboxTokens" :key="token.sub">
|
||||
<menu-entry @click.native="publishDropbox(token)">
|
||||
<menu-entry @click.native="addDropboxAccount">
|
||||
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||
<div>Publish to Dropbox</div>
|
||||
<span>{{token.name}}</span>
|
||||
<span>Add Dropbox account</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<div v-for="token in githubTokens" :key="token.sub">
|
||||
<menu-entry @click.native="publishGithub(token)">
|
||||
<menu-entry @click.native="addGithubAccount">
|
||||
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||
<div>Publish to GitHub</div>
|
||||
<span>{{token.name}}</span>
|
||||
<span>Add GitHub account</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="publishGist(token)">
|
||||
<icon-provider slot="icon" provider-id="gist"></icon-provider>
|
||||
<div>Publish to Gist</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<div v-for="token in wordpressTokens" :key="token.sub">
|
||||
<menu-entry @click.native="publishWordpress(token)">
|
||||
<menu-entry @click.native="addWordpressAccount">
|
||||
<icon-provider slot="icon" provider-id="wordpress"></icon-provider>
|
||||
<div>Publish to WordPress</div>
|
||||
<span>{{token.name}}</span>
|
||||
<span>Add WordPress account</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<div v-for="token in bloggerTokens" :key="token.sub">
|
||||
<menu-entry @click.native="publishBlogger(token)">
|
||||
<menu-entry @click.native="addBloggerAccount">
|
||||
<icon-provider slot="icon" provider-id="blogger"></icon-provider>
|
||||
<div>Publish to Blogger</div>
|
||||
<span>{{token.name}}</span>
|
||||
<span>Add Blogger account</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="publishBloggerPage(token)">
|
||||
<icon-provider slot="icon" provider-id="bloggerPage"></icon-provider>
|
||||
<div>Publish to Blogger Page</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<div v-for="token in zendeskTokens" :key="token.sub">
|
||||
<menu-entry @click.native="publishZendesk(token)">
|
||||
<menu-entry @click.native="addZendeskAccount">
|
||||
<icon-provider slot="icon" provider-id="zendesk"></icon-provider>
|
||||
<div>Publish to Zendesk Help Center</div>
|
||||
<span>{{token.name}} — {{token.subdomain}}</span>
|
||||
<span>Add Zendesk account</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<hr>
|
||||
<menu-entry @click.native="addGoogleDriveAccount">
|
||||
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||
<span>Add Google Drive account</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addDropboxAccount">
|
||||
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||
<span>Add Dropbox account</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addGithubAccount">
|
||||
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||
<span>Add GitHub account</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addWordpressAccount">
|
||||
<icon-provider slot="icon" provider-id="wordpress"></icon-provider>
|
||||
<span>Add WordPress account</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addBloggerAccount">
|
||||
<icon-provider slot="icon" provider-id="blogger"></icon-provider>
|
||||
<span>Add Blogger account</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addZendeskAccount">
|
||||
<icon-provider slot="icon" provider-id="zendesk"></icon-provider>
|
||||
<span>Add Zendesk account</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -126,6 +131,9 @@ export default {
|
|||
...mapState('queue', [
|
||||
'isPublishRequested',
|
||||
]),
|
||||
...mapGetters('file', [
|
||||
'isCurrentTemp',
|
||||
]),
|
||||
...mapGetters('publishLocation', {
|
||||
publishLocations: 'current',
|
||||
}),
|
||||
|
|
|
@ -1,76 +1,81 @@
|
|||
<template>
|
||||
<div class="side-bar__panel side-bar__panel--menu">
|
||||
<div class="side-bar__info" v-if="noToken">
|
||||
<p>You have to <b>link an account</b> to start syncing files.</p>
|
||||
<div class="side-bar__info" v-if="isCurrentTemp">
|
||||
<p><b>{{currentFileName}}</b> can not be synchronized as it's a temporary file.</p>
|
||||
</div>
|
||||
<div class="side-bar__info" v-if="syncLocations.length">
|
||||
<p><b>{{currentFileName}}</b> is already synchronized.</p>
|
||||
<menu-entry @click.native="requestSync">
|
||||
<icon-sync slot="icon"></icon-sync>
|
||||
<div>Synchronize now</div>
|
||||
<span>Download / upload file changes.</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="manageSync">
|
||||
<icon-view-list slot="icon"></icon-view-list>
|
||||
<div>File synchronization</div>
|
||||
<span>Manage current file synchronized locations.</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<hr>
|
||||
<div v-for="token in googleDriveTokens" :key="token.sub">
|
||||
<menu-entry @click.native="openGoogleDrive(token)">
|
||||
<div v-else>
|
||||
<div class="side-bar__info" v-if="noToken">
|
||||
<p>You have to <b>link an account</b> to start syncing files.</p>
|
||||
</div>
|
||||
<div class="side-bar__info" v-if="syncLocations.length">
|
||||
<p><b>{{currentFileName}}</b> is already synchronized.</p>
|
||||
<menu-entry @click.native="requestSync">
|
||||
<icon-sync slot="icon"></icon-sync>
|
||||
<div>Synchronize now</div>
|
||||
<span>Download / upload file changes.</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="manageSync">
|
||||
<icon-view-list slot="icon"></icon-view-list>
|
||||
<div>File synchronization</div>
|
||||
<span>Manage current file synchronized locations.</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<hr>
|
||||
<div v-for="token in googleDriveTokens" :key="token.sub">
|
||||
<menu-entry @click.native="openGoogleDrive(token)">
|
||||
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||
<div>Open from Google Drive</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="saveGoogleDrive(token)">
|
||||
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||
<div>Save on Google Drive</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<div v-for="token in dropboxTokens" :key="token.sub">
|
||||
<menu-entry @click.native="openDropbox(token)">
|
||||
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||
<div>Open from Dropbox</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="saveDropbox(token)">
|
||||
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||
<div>Save on Dropbox</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<div v-for="token in githubTokens" :key="token.sub">
|
||||
<menu-entry @click.native="openGithub(token)">
|
||||
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||
<div>Open from GitHub</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="saveGithub(token)">
|
||||
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||
<div>Save on GitHub</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="saveGist(token)">
|
||||
<icon-provider slot="icon" provider-id="gist"></icon-provider>
|
||||
<div>Save on Gist</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<hr>
|
||||
<menu-entry @click.native="addGoogleDriveAccount">
|
||||
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||
<div>Open from Google Drive</div>
|
||||
<span>{{token.name}}</span>
|
||||
<span>Add Google Drive account</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="saveGoogleDrive(token)">
|
||||
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||
<div>Save on Google Drive</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<div v-for="token in dropboxTokens" :key="token.sub">
|
||||
<menu-entry @click.native="openDropbox(token)">
|
||||
<menu-entry @click.native="addDropboxAccount">
|
||||
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||
<div>Open from Dropbox</div>
|
||||
<span>{{token.name}}</span>
|
||||
<span>Add Dropbox account</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="saveDropbox(token)">
|
||||
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||
<div>Save on Dropbox</div>
|
||||
<span>{{token.name}}</span>
|
||||
<menu-entry @click.native="addGithubAccount">
|
||||
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||
<span>Add GitHub account</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<div v-for="token in githubTokens" :key="token.sub">
|
||||
<menu-entry @click.native="openGithub(token)">
|
||||
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||
<div>Open from GitHub</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="saveGithub(token)">
|
||||
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||
<div>Save on GitHub</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="saveGist(token)">
|
||||
<icon-provider slot="icon" provider-id="gist"></icon-provider>
|
||||
<div>Save on Gist</div>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
<hr>
|
||||
<menu-entry @click.native="addGoogleDriveAccount">
|
||||
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||
<span>Add Google Drive account</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addDropboxAccount">
|
||||
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||
<span>Add Dropbox account</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="addGithubAccount">
|
||||
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||
<span>Add GitHub account</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -107,6 +112,9 @@ export default {
|
|||
...mapGetters('workspace', [
|
||||
'syncToken',
|
||||
]),
|
||||
...mapGetters('file', [
|
||||
'isCurrentTemp',
|
||||
]),
|
||||
...mapGetters('syncLocation', {
|
||||
syncLocations: 'current',
|
||||
}),
|
||||
|
|
|
@ -75,5 +75,6 @@ export default {
|
|||
font-size: 0.9em;
|
||||
padding: 0.75em 1.5em;
|
||||
margin-bottom: 1.2em;
|
||||
line-height: 1.55;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -341,16 +341,23 @@ function SelectionMgr(editor) {
|
|||
offsetInContainer = offset.offsetInContainer;
|
||||
}
|
||||
let containerElt = container;
|
||||
if (!containerElt.hasChildNodes()) {
|
||||
if (!containerElt.hasChildNodes() && container.parentNode) {
|
||||
containerElt = container.parentNode;
|
||||
}
|
||||
let isInvisible = false;
|
||||
while (containerElt.offsetHeight === 0) {
|
||||
while (!containerElt.offsetHeight) {
|
||||
isInvisible = true;
|
||||
if (containerElt.previousSibling) {
|
||||
containerElt = containerElt.previousSibling;
|
||||
} else {
|
||||
containerElt = containerElt.parentNode;
|
||||
if (!containerElt) {
|
||||
return {
|
||||
top: 0,
|
||||
height: 0,
|
||||
left: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
let rect;
|
||||
|
|
|
@ -54,15 +54,15 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||
converter: null,
|
||||
parsingCtx: null,
|
||||
conversionCtx: null,
|
||||
previewCtx: {
|
||||
sectionDescList: [],
|
||||
},
|
||||
previewCtxMeasured: null,
|
||||
previewCtxWithDiffs: null,
|
||||
sectionList: null,
|
||||
sectionDescList: [],
|
||||
sectionDescMeasuredList: null,
|
||||
sectionDescWithDiffsList: null,
|
||||
selectionRange: null,
|
||||
previewSelectionRange: null,
|
||||
previewSelectionStartOffset: null,
|
||||
previewHtml: null,
|
||||
previewText: null,
|
||||
|
||||
/**
|
||||
* Initialize the Prism grammar with the options
|
||||
|
@ -86,8 +86,10 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||
* Initialize the cledit editor with markdown-it section parser and Prism highlighter
|
||||
*/
|
||||
initClEditor() {
|
||||
this.sectionDescMeasuredList = null;
|
||||
this.sectionDescWithDiffsList = null;
|
||||
this.previewCtxMeasured = null;
|
||||
editorSvc.$emit('previewCtxMeasured', null);
|
||||
this.previewCtxWithDiffs = null;
|
||||
editorSvc.$emit('previewCtxWithDiffs', null);
|
||||
const options = {
|
||||
sectionHighlighter: section => Prism.highlight(
|
||||
section.text, this.prismGrammars[section.data]),
|
||||
|
@ -119,7 +121,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||
* Refresh the preview with the result of `convert()`
|
||||
*/
|
||||
refreshPreview() {
|
||||
const newSectionDescList = [];
|
||||
const sectionDescList = [];
|
||||
let sectionPreviewElt;
|
||||
let sectionTocElt;
|
||||
let sectionIdx = 0;
|
||||
|
@ -132,14 +134,14 @@ 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) {
|
||||
let sectionDesc = this.sectionDescList[sectionDescIdx];
|
||||
let sectionDesc = this.previewCtx.sectionDescList[sectionDescIdx];
|
||||
sectionDescIdx += 1;
|
||||
if (sectionDesc.editorElt !== section.elt) {
|
||||
// Force textToPreviewDiffs computation
|
||||
sectionDesc = new SectionDesc(
|
||||
section, sectionDesc.previewElt, sectionDesc.tocElt, sectionDesc.html);
|
||||
}
|
||||
newSectionDescList.push(sectionDesc);
|
||||
sectionDescList.push(sectionDesc);
|
||||
previewHtml += sectionDesc.html;
|
||||
sectionIdx += 1;
|
||||
insertBeforePreviewElt = insertBeforePreviewElt.nextSibling;
|
||||
|
@ -187,20 +189,22 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||
}
|
||||
|
||||
previewHtml += html;
|
||||
newSectionDescList.push(new SectionDesc(section, sectionPreviewElt, sectionTocElt, html));
|
||||
sectionDescList.push(new SectionDesc(section, sectionPreviewElt, sectionTocElt, html));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.sectionDescList = newSectionDescList;
|
||||
this.previewHtml = previewHtml.replace(/^\s+|\s+$/g, '');
|
||||
this.$emit('previewHtml', this.previewHtml);
|
||||
this.tocElt.classList[
|
||||
this.tocElt.querySelector('.cl-toc-section *') ? 'remove' : 'add'
|
||||
]('toc-tab--empty');
|
||||
|
||||
this.previewText = this.previewElt.textContent;
|
||||
this.$emit('previewText', this.previewText);
|
||||
this.previewCtx = {
|
||||
markdown: this.conversionCtx.text,
|
||||
html: previewHtml.replace(/^\s+|\s+$/g, ''),
|
||||
text: this.previewElt.textContent,
|
||||
sectionDescList,
|
||||
};
|
||||
this.$emit('previewCtx', this.previewCtx);
|
||||
this.makeTextToPreviewDiffs();
|
||||
|
||||
// Wait for images to load
|
||||
|
@ -217,22 +221,20 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||
|
||||
Promise.all(loadedPromises)
|
||||
// Debounce if sections have already been measured
|
||||
.then(() => this.measureSectionDimensions(!!this.sectionDescMeasuredList));
|
||||
.then(() => this.measureSectionDimensions(!!this.previewCtxMeasured));
|
||||
},
|
||||
|
||||
/**
|
||||
* Measure the height of each section in editor, preview and toc.
|
||||
*/
|
||||
measureSectionDimensions: allowDebounce((restoreScrollPosition) => {
|
||||
if (editorSvc.sectionDescList &&
|
||||
this.sectionDescList !== editorSvc.sectionDescMeasuredList
|
||||
) {
|
||||
measureSectionDimensions: allowDebounce((restoreScrollPosition = false, force = false) => {
|
||||
if (force || editorSvc.previewCtx !== editorSvc.previewCtxMeasured) {
|
||||
sectionUtils.measureSectionDimensions(editorSvc);
|
||||
editorSvc.sectionDescMeasuredList = editorSvc.sectionDescList;
|
||||
editorSvc.previewCtxMeasured = editorSvc.previewCtx;
|
||||
if (restoreScrollPosition) {
|
||||
editorSvc.restoreScrollPosition();
|
||||
}
|
||||
editorSvc.$emit('sectionDescMeasuredList', editorSvc.sectionDescMeasuredList);
|
||||
editorSvc.$emit('previewCtxMeasured', editorSvc.previewCtxMeasured);
|
||||
}
|
||||
}, 500),
|
||||
|
||||
|
@ -241,12 +243,10 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||
* asynchronously unless there is only one section to compute.
|
||||
*/
|
||||
makeTextToPreviewDiffs() {
|
||||
if (editorSvc.sectionDescList &&
|
||||
editorSvc.sectionDescList !== editorSvc.sectionDescWithDiffsList
|
||||
) {
|
||||
if (editorSvc.previewCtx !== editorSvc.previewCtxWithDiffs) {
|
||||
const makeOne = () => {
|
||||
let hasOne = false;
|
||||
const hasMore = editorSvc.sectionDescList
|
||||
const hasMore = editorSvc.previewCtx.sectionDescList
|
||||
.some((sectionDesc) => {
|
||||
if (!sectionDesc.textToPreviewDiffs) {
|
||||
if (hasOne) {
|
||||
|
@ -264,9 +264,8 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||
if (hasMore) {
|
||||
setTimeout(() => makeOne(), 10);
|
||||
} else {
|
||||
editorSvc.previewTextWithDiffsList = editorSvc.previewText;
|
||||
editorSvc.sectionDescWithDiffsList = editorSvc.sectionDescList;
|
||||
editorSvc.$emit('sectionDescWithDiffsList', editorSvc.sectionDescWithDiffsList);
|
||||
editorSvc.previewCtxWithDiffs = editorSvc.previewCtx;
|
||||
editorSvc.$emit('previewCtxWithDiffs', editorSvc.previewCtxWithDiffs);
|
||||
}
|
||||
};
|
||||
makeOne();
|
||||
|
@ -517,7 +516,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||
let lastContentId = null;
|
||||
let lastProperties;
|
||||
store.watch(
|
||||
() => store.getters['content/current'].hash,
|
||||
() => store.getters['content/currentChangeTrigger'],
|
||||
() => {
|
||||
const content = store.getters['content/current'];
|
||||
// Track ID changes
|
||||
|
@ -541,7 +540,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||
if (initClEditor) {
|
||||
this.initClEditor();
|
||||
}
|
||||
// Apply possible text and discussion changes
|
||||
// Apply potential text and discussion changes
|
||||
this.applyContent();
|
||||
}, {
|
||||
immediate: true,
|
||||
|
@ -555,7 +554,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||
});
|
||||
|
||||
store.watch(() => utils.serializeObject(store.getters['layout/styles']),
|
||||
() => this.measureSectionDimensions(false, true));
|
||||
() => this.measureSectionDimensions(false, true, true));
|
||||
|
||||
this.initHighlighters();
|
||||
this.$emit('inited');
|
||||
|
|
|
@ -18,8 +18,8 @@ export default {
|
|||
: 'previewDimension';
|
||||
const scrollTop = elt.parentNode.scrollTop;
|
||||
let result;
|
||||
if (this.sectionDescMeasuredList) {
|
||||
this.sectionDescMeasuredList.some((sectionDesc, sectionIdx) => {
|
||||
if (this.previewCtxMeasured) {
|
||||
this.previewCtxMeasured.sectionDescList.some((sectionDesc, sectionIdx) => {
|
||||
if (scrollTop >= sectionDesc[dimensionKey].endOffset) {
|
||||
return false;
|
||||
}
|
||||
|
@ -40,8 +40,8 @@ export default {
|
|||
*/
|
||||
restoreScrollPosition() {
|
||||
const scrollPosition = store.getters['contentState/current'].scrollPosition;
|
||||
if (scrollPosition && this.sectionDescMeasuredList) {
|
||||
const sectionDesc = this.sectionDescMeasuredList[scrollPosition.sectionIdx];
|
||||
if (scrollPosition && this.previewCtxMeasured) {
|
||||
const sectionDesc = this.previewCtxMeasured.sectionDescList[scrollPosition.sectionIdx];
|
||||
if (sectionDesc) {
|
||||
const editorScrollTop = sectionDesc.editorDimension.startOffset +
|
||||
(sectionDesc.editorDimension.height * scrollPosition.posInSection);
|
||||
|
@ -56,7 +56,10 @@ export default {
|
|||
/**
|
||||
* Get the offset in the preview corresponding to the offset of the markdown in the editor
|
||||
*/
|
||||
getPreviewOffset(editorOffset, sectionDescList = this.sectionDescWithDiffsList) {
|
||||
getPreviewOffset(
|
||||
editorOffset,
|
||||
sectionDescList = (this.previewCtxWithDiffs || {}).sectionDescList,
|
||||
) {
|
||||
if (!sectionDescList) {
|
||||
return null;
|
||||
}
|
||||
|
@ -81,7 +84,10 @@ export default {
|
|||
/**
|
||||
* Get the offset of the markdown in the editor corresponding to the offset in the preview
|
||||
*/
|
||||
getEditorOffset(previewOffset, sectionDescList = this.sectionDescWithDiffsList) {
|
||||
getEditorOffset(
|
||||
previewOffset,
|
||||
sectionDescList = (this.previewCtxWithDiffs || {}).sectionDescList,
|
||||
) {
|
||||
if (!sectionDescList) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import welcomeFile from '../data/welcomeFile.md';
|
|||
const dbVersion = 1;
|
||||
const dbStoreName = 'objects';
|
||||
const exportWorkspace = utils.queryParams.exportWorkspace;
|
||||
const silent = utils.queryParams.silent;
|
||||
const resetApp = utils.queryParams.reset;
|
||||
const deleteMarkerMaxAge = 1000;
|
||||
const checkSponsorshipAfter = (5 * 60 * 1000) + (30 * 1000); // tokenExpirationMargin + 30 sec
|
||||
|
@ -99,142 +100,6 @@ const localDbSvc = {
|
|||
hashMap,
|
||||
connection: null,
|
||||
|
||||
/**
|
||||
* Create the connection and start syncing.
|
||||
*/
|
||||
init() {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
// Reset the app if reset flag was passed
|
||||
if (resetApp) {
|
||||
return Promise.all(
|
||||
Object.keys(store.getters['data/workspaces'])
|
||||
.map(workspaceId => localDbSvc.removeWorkspace(workspaceId)),
|
||||
)
|
||||
.then(() => utils.localStorageDataIds.forEach((id) => {
|
||||
// Clean data stored in localStorage
|
||||
localStorage.removeItem(`data/${id}`);
|
||||
}))
|
||||
.then(() => {
|
||||
location.reload();
|
||||
throw new Error('reload');
|
||||
});
|
||||
}
|
||||
|
||||
// Create the connection
|
||||
this.connection = new Connection();
|
||||
|
||||
// Load the DB
|
||||
return localDbSvc.sync();
|
||||
})
|
||||
.then(() => {
|
||||
// If exportWorkspace parameter was provided
|
||||
if (exportWorkspace) {
|
||||
const backup = JSON.stringify(store.getters.allItemMap);
|
||||
const blob = new Blob([backup], {
|
||||
type: 'text/plain;charset=utf-8',
|
||||
});
|
||||
FileSaver.saveAs(blob, 'StackEdit workspace.json');
|
||||
return;
|
||||
}
|
||||
|
||||
// Save welcome file content hash if not done already
|
||||
const hash = utils.hash(welcomeFile);
|
||||
const welcomeFileHashes = store.getters['data/localSettings'].welcomeFileHashes;
|
||||
if (!welcomeFileHashes[hash]) {
|
||||
store.dispatch('data/patchLocalSettings', {
|
||||
welcomeFileHashes: {
|
||||
...welcomeFileHashes,
|
||||
[hash]: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// If app was last opened 7 days ago and synchronization is off
|
||||
if (!store.getters['workspace/syncToken'] &&
|
||||
(store.state.workspace.lastFocus + utils.cleanTrashAfter < Date.now())
|
||||
) {
|
||||
// Clean files
|
||||
store.getters['file/items']
|
||||
.filter(file => file.parentId === 'trash') // If file is in the trash
|
||||
.forEach(file => store.dispatch('deleteFile', file.id));
|
||||
}
|
||||
|
||||
// Enable sponsorship
|
||||
if (utils.queryParams.paymentSuccess) {
|
||||
location.hash = ''; // PaymentSuccess param is always on its own
|
||||
store.dispatch('modal/paymentSuccess');
|
||||
const sponsorToken = store.getters['workspace/sponsorToken'];
|
||||
// Force check sponsorship after a few seconds
|
||||
const currentDate = Date.now();
|
||||
if (sponsorToken && sponsorToken.expiresOn > currentDate - checkSponsorshipAfter) {
|
||||
store.dispatch('data/setGoogleToken', {
|
||||
...sponsorToken,
|
||||
expiresOn: currentDate - checkSponsorshipAfter,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sync local DB periodically
|
||||
utils.setInterval(() => localDbSvc.sync(), 1000);
|
||||
|
||||
// watch current file changing
|
||||
store.watch(
|
||||
() => store.getters['file/current'].id,
|
||||
() => {
|
||||
// See if currentFile is real, ie it has an ID
|
||||
const currentFile = store.getters['file/current'];
|
||||
// If current file has no ID, get the most recent file
|
||||
if (!currentFile.id) {
|
||||
const recentFile = store.getters['file/lastOpened'];
|
||||
// Set it as the current file
|
||||
if (recentFile.id) {
|
||||
store.commit('file/setCurrentId', recentFile.id);
|
||||
} else {
|
||||
// If still no ID, create a new file
|
||||
store.dispatch('createFile', {
|
||||
name: 'Welcome file',
|
||||
text: welcomeFile,
|
||||
})
|
||||
// Set it as the current file
|
||||
.then(newFile => store.commit('file/setCurrentId', newFile.id));
|
||||
}
|
||||
} else {
|
||||
Promise.resolve()
|
||||
// Load contentState from DB
|
||||
.then(() => localDbSvc.loadContentState(currentFile.id))
|
||||
// Load syncedContent from DB
|
||||
.then(() => localDbSvc.loadSyncedContent(currentFile.id))
|
||||
// Load content from DB
|
||||
.then(() => localDbSvc.loadItem(`${currentFile.id}/content`))
|
||||
.then(
|
||||
() => {
|
||||
// Set last opened file
|
||||
store.dispatch('data/setLastOpenedId', currentFile.id);
|
||||
// Cancel new discussion
|
||||
store.commit('discussion/setCurrentDiscussionId');
|
||||
// Open the gutter if file contains discussions
|
||||
store.commit('discussion/setCurrentDiscussionId',
|
||||
store.getters['discussion/nextDiscussionId']);
|
||||
},
|
||||
(err) => {
|
||||
// Failure (content is not available), go back to previous file
|
||||
const lastOpenedFile = store.getters['file/lastOpened'];
|
||||
store.commit('file/setCurrentId', lastOpenedFile.id);
|
||||
throw err;
|
||||
},
|
||||
)
|
||||
.catch((err) => {
|
||||
console.error(err); // eslint-disable-line no-console
|
||||
store.dispatch('notification/error', err);
|
||||
});
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sync data items stored in the localStorage.
|
||||
*/
|
||||
|
@ -331,6 +196,10 @@ const localDbSvc = {
|
|||
* Write all changes from the store since previous transaction.
|
||||
*/
|
||||
writeAll(storeItemMap, tx) {
|
||||
if (silent) {
|
||||
// Skip writing to DB in silent mode
|
||||
return;
|
||||
}
|
||||
const dbStore = tx.objectStore(dbStoreName);
|
||||
const incrementedTx = this.lastTx + 1;
|
||||
|
||||
|
@ -475,6 +344,142 @@ const localDbSvc = {
|
|||
localStorage.removeItem(`${id}/lastWindowFocus`);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Create the connection and start syncing.
|
||||
*/
|
||||
init() {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
// Reset the app if reset flag was passed
|
||||
if (resetApp) {
|
||||
return Promise.all(
|
||||
Object.keys(store.getters['data/workspaces'])
|
||||
.map(workspaceId => localDbSvc.removeWorkspace(workspaceId)),
|
||||
)
|
||||
.then(() => utils.localStorageDataIds.forEach((id) => {
|
||||
// Clean data stored in localStorage
|
||||
localStorage.removeItem(`data/${id}`);
|
||||
}))
|
||||
.then(() => {
|
||||
location.reload();
|
||||
throw new Error('reload');
|
||||
});
|
||||
}
|
||||
|
||||
// Create the connection
|
||||
this.connection = new Connection();
|
||||
|
||||
// Load the DB
|
||||
return localDbSvc.sync();
|
||||
})
|
||||
.then(() => {
|
||||
// If exportWorkspace parameter was provided
|
||||
if (exportWorkspace) {
|
||||
const backup = JSON.stringify(store.getters.allItemMap);
|
||||
const blob = new Blob([backup], {
|
||||
type: 'text/plain;charset=utf-8',
|
||||
});
|
||||
FileSaver.saveAs(blob, 'StackEdit workspace.json');
|
||||
return;
|
||||
}
|
||||
|
||||
// Save welcome file content hash if not done already
|
||||
const hash = utils.hash(welcomeFile);
|
||||
const welcomeFileHashes = store.getters['data/localSettings'].welcomeFileHashes;
|
||||
if (!welcomeFileHashes[hash]) {
|
||||
store.dispatch('data/patchLocalSettings', {
|
||||
welcomeFileHashes: {
|
||||
...welcomeFileHashes,
|
||||
[hash]: 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// If app was last opened 7 days ago and synchronization is off
|
||||
if (!store.getters['workspace/syncToken'] &&
|
||||
(store.state.workspace.lastFocus + utils.cleanTrashAfter < Date.now())
|
||||
) {
|
||||
// Clean files
|
||||
store.getters['file/items']
|
||||
.filter(file => file.parentId === 'trash') // If file is in the trash
|
||||
.forEach(file => store.dispatch('deleteFile', file.id));
|
||||
}
|
||||
|
||||
// Enable sponsorship
|
||||
if (utils.queryParams.paymentSuccess) {
|
||||
location.hash = ''; // PaymentSuccess param is always on its own
|
||||
store.dispatch('modal/paymentSuccess');
|
||||
const sponsorToken = store.getters['workspace/sponsorToken'];
|
||||
// Force check sponsorship after a few seconds
|
||||
const currentDate = Date.now();
|
||||
if (sponsorToken && sponsorToken.expiresOn > currentDate - checkSponsorshipAfter) {
|
||||
store.dispatch('data/setGoogleToken', {
|
||||
...sponsorToken,
|
||||
expiresOn: currentDate - checkSponsorshipAfter,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sync local DB periodically
|
||||
utils.setInterval(() => localDbSvc.sync(), 1000);
|
||||
|
||||
// watch current file changing
|
||||
store.watch(
|
||||
() => store.getters['file/current'].id,
|
||||
() => {
|
||||
// See if currentFile is real, ie it has an ID
|
||||
const currentFile = store.getters['file/current'];
|
||||
// If current file has no ID, get the most recent file
|
||||
if (!currentFile.id) {
|
||||
const recentFile = store.getters['file/lastOpened'];
|
||||
// Set it as the current file
|
||||
if (recentFile.id) {
|
||||
store.commit('file/setCurrentId', recentFile.id);
|
||||
} else {
|
||||
// If still no ID, create a new file
|
||||
store.dispatch('createFile', {
|
||||
name: 'Welcome file',
|
||||
text: welcomeFile,
|
||||
})
|
||||
// Set it as the current file
|
||||
.then(newFile => store.commit('file/setCurrentId', newFile.id));
|
||||
}
|
||||
} else {
|
||||
Promise.resolve()
|
||||
// Load contentState from DB
|
||||
.then(() => localDbSvc.loadContentState(currentFile.id))
|
||||
// Load syncedContent from DB
|
||||
.then(() => localDbSvc.loadSyncedContent(currentFile.id))
|
||||
// Load content from DB
|
||||
.then(() => localDbSvc.loadItem(`${currentFile.id}/content`))
|
||||
.then(
|
||||
() => {
|
||||
// Set last opened file
|
||||
store.dispatch('data/setLastOpenedId', currentFile.id);
|
||||
// Cancel new discussion
|
||||
store.commit('discussion/setCurrentDiscussionId');
|
||||
// Open the gutter if file contains discussions
|
||||
store.commit('discussion/setCurrentDiscussionId',
|
||||
store.getters['discussion/nextDiscussionId']);
|
||||
},
|
||||
(err) => {
|
||||
// Failure (content is not available), go back to previous file
|
||||
const lastOpenedFile = store.getters['file/lastOpened'];
|
||||
store.commit('file/setCurrentId', lastOpenedFile.id);
|
||||
throw err;
|
||||
},
|
||||
)
|
||||
.catch((err) => {
|
||||
console.error(err); // eslint-disable-line no-console
|
||||
store.dispatch('notification/error', err);
|
||||
});
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const loader = type => fileId => localDbSvc.loadItem(`${fileId}/${type}`)
|
||||
|
|
|
@ -165,6 +165,7 @@ const markdownConversionSvc = {
|
|||
lines.pop();
|
||||
}
|
||||
const parsingCtx = {
|
||||
text,
|
||||
sections: [],
|
||||
converter,
|
||||
markdownState,
|
||||
|
@ -248,6 +249,7 @@ const markdownConversionSvc = {
|
|||
];
|
||||
}
|
||||
return {
|
||||
text: parsingCtx.text,
|
||||
sectionList: parsingCtx.sectionList,
|
||||
htmlSectionList,
|
||||
htmlSectionDiff,
|
||||
|
|
|
@ -11,7 +11,7 @@ let isScrollEditor;
|
|||
let isScrollPreview;
|
||||
let isEditorMoving;
|
||||
let isPreviewMoving;
|
||||
let sectionDescList;
|
||||
let sectionDescList = [];
|
||||
|
||||
let throttleTimeoutId;
|
||||
let throttleLastTime = 0;
|
||||
|
@ -34,7 +34,7 @@ function throttle(func, wait) {
|
|||
const doScrollSync = () => {
|
||||
const localSkipAnimation = skipAnimation || !store.getters['layout/styles'].showSidePreview;
|
||||
skipAnimation = false;
|
||||
if (!store.getters['data/layoutSettings'].scrollSync || !sectionDescList || sectionDescList.length === 0) {
|
||||
if (!store.getters['data/layoutSettings'].scrollSync || sectionDescList.length === 0) {
|
||||
return;
|
||||
}
|
||||
let editorScrollTop = editorScrollerElt.scrollTop;
|
||||
|
@ -144,10 +144,10 @@ editorSvc.$on('inited', () => {
|
|||
editorSvc.$on('sectionList', () => {
|
||||
clearTimeout(timeoutId);
|
||||
isPreviewRefreshing = true;
|
||||
sectionDescList = undefined;
|
||||
sectionDescList = [];
|
||||
});
|
||||
|
||||
editorSvc.$on('previewText', () => {
|
||||
editorSvc.$on('previewCtx', () => {
|
||||
// Assume the user is writing in the editor
|
||||
isScrollEditor = store.getters['layout/styles'].showEditor;
|
||||
// A preview scrolling event can occur if height is smaller
|
||||
|
@ -170,7 +170,9 @@ store.watch(
|
|||
skipAnimation = true;
|
||||
});
|
||||
|
||||
editorSvc.$on('sectionDescMeasuredList', (sectionDescMeasuredList) => {
|
||||
sectionDescList = sectionDescMeasuredList;
|
||||
forceScrollSync();
|
||||
editorSvc.$on('previewCtxMeasured', (previewCtxMeasured) => {
|
||||
if (previewCtxMeasured) {
|
||||
sectionDescList = previewCtxMeasured.sectionDescList;
|
||||
forceScrollSync();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -111,6 +111,11 @@ function publishFile(fileId) {
|
|||
}
|
||||
|
||||
function requestPublish() {
|
||||
// No publish in light mode
|
||||
if (store.state.light) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.dispatch('queue/enqueuePublishRequest', () => new Promise((resolve, reject) => {
|
||||
let intervalId;
|
||||
const attempt = () => {
|
||||
|
|
|
@ -6,7 +6,8 @@ function SectionDimension(startOffset, endOffset) {
|
|||
|
||||
function dimensionNormalizer(dimensionName) {
|
||||
return (editorSvc) => {
|
||||
const dimensionList = editorSvc.sectionDescList.map(sectionDesc => sectionDesc[dimensionName]);
|
||||
const dimensionList = editorSvc.previewCtx.sectionDescList.map(
|
||||
sectionDesc => sectionDesc[dimensionName]);
|
||||
let dimension;
|
||||
let i;
|
||||
let j;
|
||||
|
@ -43,11 +44,11 @@ function measureSectionDimensions(editorSvc) {
|
|||
let editorSectionOffset = 0;
|
||||
let previewSectionOffset = 0;
|
||||
let tocSectionOffset = 0;
|
||||
let sectionDesc = editorSvc.sectionDescList[0];
|
||||
let sectionDesc = editorSvc.previewCtx.sectionDescList[0];
|
||||
let nextSectionDesc;
|
||||
let i = 1;
|
||||
for (; i < editorSvc.sectionDescList.length; i += 1) {
|
||||
nextSectionDesc = editorSvc.sectionDescList[i];
|
||||
for (; i < editorSvc.previewCtx.sectionDescList.length; i += 1) {
|
||||
nextSectionDesc = editorSvc.previewCtx.sectionDescList[i];
|
||||
|
||||
// Measure editor section
|
||||
let newEditorSectionOffset = nextSectionDesc.editorElt
|
||||
|
@ -84,7 +85,7 @@ function measureSectionDimensions(editorSvc) {
|
|||
}
|
||||
|
||||
// Last section
|
||||
sectionDesc = editorSvc.sectionDescList[i - 1];
|
||||
sectionDesc = editorSvc.previewCtx.sectionDescList[i - 1];
|
||||
if (sectionDesc) {
|
||||
sectionDesc.editorDimension = new SectionDimension(
|
||||
editorSectionOffset, editorSvc.editorElt.scrollHeight);
|
||||
|
|
|
@ -7,6 +7,7 @@ import providerRegistry from './providers/providerRegistry';
|
|||
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
|
||||
import './providers/googleDriveWorkspaceProvider';
|
||||
import './providers/couchdbWorkspaceProvider';
|
||||
import tempFileSvc from './tempFileSvc';
|
||||
|
||||
const inactivityThreshold = 3 * 1000; // 3 sec
|
||||
const restartSyncAfter = 30 * 1000; // 30 sec
|
||||
|
@ -169,25 +170,16 @@ function createSyncLocation(syncLocation) {
|
|||
}
|
||||
|
||||
class SyncContext {
|
||||
constructor() {
|
||||
this.restart = false;
|
||||
this.synced = {};
|
||||
}
|
||||
}
|
||||
|
||||
class FileSyncContext {
|
||||
constructor() {
|
||||
this.downloaded = {};
|
||||
this.errors = {};
|
||||
}
|
||||
restart = false;
|
||||
attempted = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync one file with all its locations.
|
||||
*/
|
||||
function syncFile(fileId, syncContext = new SyncContext()) {
|
||||
const fileSyncContext = new FileSyncContext();
|
||||
syncContext.synced[`${fileId}/content`] = true;
|
||||
syncContext.attempted[`${fileId}/content`] = true;
|
||||
|
||||
return localDbSvc.loadSyncedContent(fileId)
|
||||
.then(() => localDbSvc.loadItem(`${fileId}/content`)
|
||||
.catch(() => {})) // Item may not exist if content has not been downloaded yet
|
||||
|
@ -197,14 +189,9 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||
const getSyncedContent = () => store.state.syncedContent.itemMap[`${fileId}/syncedContent`];
|
||||
const getSyncHistoryItem = syncLocationId => getSyncedContent().syncHistory[syncLocationId];
|
||||
|
||||
const isLocationSynced = (syncLocation) => {
|
||||
const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
|
||||
return syncHistoryItem && syncHistoryItem[LAST_SENT] === getContent().hash;
|
||||
};
|
||||
|
||||
const isWelcomeFile = () => {
|
||||
const isTempFile = () => {
|
||||
if (store.getters['data/syncDataByItemId'][`${fileId}/content`]) {
|
||||
// If file has already been synced, keep on syncing
|
||||
// If file has already been synced, it's not a temp file
|
||||
return false;
|
||||
}
|
||||
const file = getFile();
|
||||
|
@ -212,12 +199,29 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||
if (!file || !content) {
|
||||
return false;
|
||||
}
|
||||
if (file.parentId === 'temp') {
|
||||
return true;
|
||||
}
|
||||
const locations = [
|
||||
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
|
||||
...store.getters['publishLocation/groupedByFileId'][fileId] || [],
|
||||
];
|
||||
if (locations.length) {
|
||||
// If file has explicit sync/publish locations, it's not a temp file
|
||||
return false;
|
||||
}
|
||||
// Return true if it's a welcome file that has no discussion
|
||||
const welcomeFileHashes = store.getters['data/localSettings'].welcomeFileHashes;
|
||||
const hash = utils.hash(content.text);
|
||||
const hasDiscussions = Object.keys(content.discussions).length;
|
||||
return file.name === 'Welcome file' && welcomeFileHashes[hash] && !hasDiscussions;
|
||||
};
|
||||
|
||||
if (isTempFile()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const attemptedLocations = {};
|
||||
const syncOneContentLocation = () => {
|
||||
const syncLocations = [
|
||||
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
|
||||
|
@ -229,20 +233,17 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||
syncLocations.some((syncLocation) => {
|
||||
const provider = providerRegistry.providers[syncLocation.providerId];
|
||||
if (
|
||||
// Skip if it previously threw an error
|
||||
!fileSyncContext.errors[syncLocation.id] &&
|
||||
// Skip if it has previously been downloaded and has not changed since then
|
||||
(!fileSyncContext.downloaded[syncLocation.id] || !isLocationSynced(syncLocation)) &&
|
||||
// Skip welcome file if not synchronized explicitly
|
||||
(syncLocations.length > 1 || !isWelcomeFile())
|
||||
// Skip if it has been attempted already
|
||||
!attemptedLocations[syncLocation.id] &&
|
||||
// Skip temp file
|
||||
!isTempFile()
|
||||
) {
|
||||
attemptedLocations[syncLocation.id] = true;
|
||||
const token = provider && provider.getToken(syncLocation);
|
||||
result = token && store.dispatch('queue/doWithLocation', {
|
||||
location: syncLocation,
|
||||
promise: provider.downloadContent(token, syncLocation)
|
||||
.then((serverContent = null) => {
|
||||
fileSyncContext.downloaded[syncLocation.id] = true;
|
||||
|
||||
const syncedContent = getSyncedContent();
|
||||
const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
|
||||
let mergedContent = (() => {
|
||||
|
@ -275,7 +276,6 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||
})();
|
||||
|
||||
if (!mergedContent) {
|
||||
fileSyncContext.errors[syncLocation.id] = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -370,7 +370,6 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||
}
|
||||
console.error(err); // eslint-disable-line no-console
|
||||
store.dispatch('notification/error', err);
|
||||
fileSyncContext.errors[syncLocation.id] = true;
|
||||
}),
|
||||
})
|
||||
.then(() => syncOneContentLocation());
|
||||
|
@ -584,7 +583,7 @@ function syncWorkspace() {
|
|||
const syncData = store.getters['data/syncDataByItemId'][contentId];
|
||||
if (
|
||||
// Sync if content syncing was not attempted yet
|
||||
!syncContext.synced[contentId] &&
|
||||
!syncContext.attempted[contentId] &&
|
||||
// And if syncData does not exist or if content hash and syncData hash are inconsistent
|
||||
(!syncData || syncData.hash !== hash)
|
||||
) {
|
||||
|
@ -643,6 +642,11 @@ function syncWorkspace() {
|
|||
* Enqueue a sync task, if possible.
|
||||
*/
|
||||
function requestSync() {
|
||||
// No sync in light mode
|
||||
if (store.state.light) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.dispatch('queue/enqueueSyncRequest', () => new Promise((resolve, reject) => {
|
||||
let intervalId;
|
||||
const attempt = () => {
|
||||
|
@ -734,25 +738,28 @@ export default {
|
|||
return actionProvider && actionProvider.performAction && actionProvider.performAction()
|
||||
.then(newSyncLocation => newSyncLocation && this.createSyncLocation(newSyncLocation));
|
||||
})
|
||||
.then(() => tempFileSvc.init())
|
||||
.then(() => {
|
||||
// Sync periodically
|
||||
utils.setInterval(() => {
|
||||
if (isSyncPossible()
|
||||
&& networkSvc.isUserActive()
|
||||
&& isSyncWindow()
|
||||
&& isAutoSyncReady()
|
||||
) {
|
||||
requestSync();
|
||||
}
|
||||
}, 1000);
|
||||
if (!store.state.light) {
|
||||
// Sync periodically
|
||||
utils.setInterval(() => {
|
||||
if (isSyncPossible()
|
||||
&& networkSvc.isUserActive()
|
||||
&& isSyncWindow()
|
||||
&& isAutoSyncReady()
|
||||
) {
|
||||
requestSync();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Unload contents from memory periodically
|
||||
utils.setInterval(() => {
|
||||
// Wait for sync and publish to finish
|
||||
if (store.state.queue.isEmpty) {
|
||||
localDbSvc.unloadContents();
|
||||
}
|
||||
}, 5000);
|
||||
// Unload contents from memory periodically
|
||||
utils.setInterval(() => {
|
||||
// Wait for sync and publish to finish
|
||||
if (store.state.queue.isEmpty) {
|
||||
localDbSvc.unloadContents();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
},
|
||||
isSyncPossible,
|
||||
|
|
95
src/services/tempFileSvc.js
Normal file
95
src/services/tempFileSvc.js
Normal file
|
@ -0,0 +1,95 @@
|
|||
import cledit from './cledit';
|
||||
import store from '../store';
|
||||
import utils from './utils';
|
||||
import editorSvc from './editorSvc';
|
||||
|
||||
const origin = utils.queryParams.origin;
|
||||
const fileName = utils.queryParams.fileName;
|
||||
const contentText = utils.queryParams.contentText;
|
||||
const contentProperties = utils.queryParams.contentProperties;
|
||||
|
||||
export default {
|
||||
setReady() {
|
||||
if (origin && window.parent) {
|
||||
window.parent.postMessage({ type: 'ready' }, origin);
|
||||
}
|
||||
},
|
||||
closed: false,
|
||||
close() {
|
||||
if (!this.closed && origin && window.parent) {
|
||||
window.parent.postMessage({ type: 'close' }, origin);
|
||||
}
|
||||
this.closed = true;
|
||||
},
|
||||
init() {
|
||||
if (!origin || !window.parent) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
store.commit('setLight', true);
|
||||
|
||||
return store.dispatch('createFile', {
|
||||
name: fileName || utils.getHostname(origin),
|
||||
text: contentText || '\n',
|
||||
properties: contentProperties,
|
||||
parentId: 'temp',
|
||||
})
|
||||
.then((file) => {
|
||||
const fileItemMap = store.state.file.itemMap;
|
||||
|
||||
// Sanitize file creations
|
||||
const lastCreated = {};
|
||||
Object.entries(store.getters['data/lastCreated']).forEach(([id, createdOn]) => {
|
||||
if (fileItemMap[id] && fileItemMap[id].parentId === 'temp') {
|
||||
lastCreated[id] = createdOn;
|
||||
}
|
||||
});
|
||||
|
||||
// Track file creation from other site
|
||||
lastCreated[file.id] = {
|
||||
created: Date.now(),
|
||||
};
|
||||
|
||||
// Keep only the last 10 temp files created by other sites
|
||||
Object.entries(lastCreated)
|
||||
.sort(([, createdOn1], [, createdOn2]) => createdOn2 - createdOn1)
|
||||
.splice(10)
|
||||
.forEach(([id]) => {
|
||||
delete lastCreated[id];
|
||||
store.dispatch('deleteFile', id);
|
||||
});
|
||||
|
||||
// Store file creations and open the file
|
||||
store.dispatch('data/setLastCreated', lastCreated);
|
||||
store.commit('file/setCurrentId', file.id);
|
||||
|
||||
const onChange = cledit.Utils.debounce(() => {
|
||||
const currentFile = store.getters['file/current'];
|
||||
if (currentFile.id !== file.id) {
|
||||
// Close editor if file has changed for some reason
|
||||
this.close();
|
||||
} else if (!this.closed && editorSvc.previewCtx.html != null) {
|
||||
const content = store.getters['content/current'];
|
||||
const properties = utils.computeProperties(content.properties);
|
||||
window.parent.postMessage({
|
||||
type: 'fileChange',
|
||||
payload: {
|
||||
id: file.id,
|
||||
name: currentFile.name,
|
||||
content: {
|
||||
text: content.text.slice(0, -1), // Remove trailing LF
|
||||
properties,
|
||||
yamlProperties: content.properties,
|
||||
html: editorSvc.previewCtx.html,
|
||||
},
|
||||
},
|
||||
}, origin);
|
||||
}
|
||||
}, 25);
|
||||
|
||||
// Watch preview refresh and file name changes
|
||||
editorSvc.$on('previewCtx', onChange);
|
||||
store.watch(() => store.getters['file/current'].name, onChange);
|
||||
});
|
||||
},
|
||||
};
|
|
@ -35,9 +35,9 @@ export default {
|
|||
this.queryParams = params;
|
||||
const serializedParams = Object.entries(this.queryParams).map(([key, value]) =>
|
||||
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&');
|
||||
const hash = serializedParams && `#${serializedParams}`;
|
||||
const hash = `#${serializedParams}`;
|
||||
if (location.hash !== hash) {
|
||||
location.hash = hash;
|
||||
location.replace(hash);
|
||||
}
|
||||
},
|
||||
types: [
|
||||
|
@ -185,6 +185,10 @@ export default {
|
|||
}
|
||||
return result;
|
||||
},
|
||||
getHostname(url) {
|
||||
urlParser.href = url;
|
||||
return urlParser.hostname;
|
||||
},
|
||||
createHiddenIframe(url) {
|
||||
const iframeElt = document.createElement('iframe');
|
||||
iframeElt.style.position = 'absolute';
|
||||
|
|
|
@ -37,6 +37,14 @@ module.getters = {
|
|||
}
|
||||
return state.itemMap[`${rootGetters['file/current'].id}/content`] || empty();
|
||||
},
|
||||
currentChangeTrigger: (state, getters) => {
|
||||
const current = getters.current;
|
||||
return utils.serializeObject([
|
||||
current.id,
|
||||
current.text,
|
||||
current.hash,
|
||||
]);
|
||||
},
|
||||
currentProperties: (state, getters) => utils.computeProperties(getters.current.properties),
|
||||
isCurrentEditable: (state, getters, rootState, rootGetters) =>
|
||||
!state.revisionContent &&
|
||||
|
|
|
@ -170,6 +170,7 @@ export default {
|
|||
...getters.templates,
|
||||
...additionalTemplates,
|
||||
}),
|
||||
lastCreated: getter('lastCreated'),
|
||||
lastOpened: getter('lastOpened'),
|
||||
lastOpenedIds: (state, getters, rootState) => {
|
||||
const lastOpened = {
|
||||
|
@ -261,21 +262,18 @@ export default {
|
|||
});
|
||||
commit('setItem', itemTemplate('templates', dataToCommit));
|
||||
},
|
||||
setLastCreated: setter('lastCreated'),
|
||||
setLastOpenedId: ({ getters, commit, dispatch, rootState }, fileId) => {
|
||||
const lastOpened = { ...getters.lastOpened };
|
||||
lastOpened[fileId] = Date.now();
|
||||
commit('setItem', itemTemplate('lastOpened', lastOpened));
|
||||
dispatch('cleanLastOpenedId');
|
||||
},
|
||||
cleanLastOpenedId: ({ getters, commit, rootState }) => {
|
||||
const lastOpened = {};
|
||||
const oldLastOpened = getters.lastOpened;
|
||||
Object.entries(oldLastOpened).forEach(([fileId, date]) => {
|
||||
if (rootState.file.itemMap[fileId]) {
|
||||
lastOpened[fileId] = date;
|
||||
// Remove entries that don't exist anymore
|
||||
const cleanedLastOpened = {};
|
||||
Object.entries(lastOpened).forEach(([id, value]) => {
|
||||
if (rootState.file.itemMap[id]) {
|
||||
cleanedLastOpened[id] = value;
|
||||
}
|
||||
});
|
||||
commit('setItem', itemTemplate('lastOpened', lastOpened));
|
||||
commit('setItem', itemTemplate('lastOpened', cleanedLastOpened));
|
||||
},
|
||||
setSyncData: setter('syncData'),
|
||||
patchSyncData: patcher('syncData'),
|
||||
|
|
|
@ -69,13 +69,29 @@ export default {
|
|||
},
|
||||
getters: {
|
||||
nodeStructure: (state, getters, rootState, rootGetters) => {
|
||||
const rootNode = new Node(emptyFolder(), [], true, true);
|
||||
|
||||
// Create Trash node
|
||||
const trashFolderNode = new Node(emptyFolder(), [], true);
|
||||
trashFolderNode.item.id = 'trash';
|
||||
trashFolderNode.item.name = 'Trash';
|
||||
trashFolderNode.isTrash = true;
|
||||
trashFolderNode.noDrag = true;
|
||||
trashFolderNode.isTrash = true;
|
||||
trashFolderNode.parentNode = rootNode;
|
||||
|
||||
// Create Temp node
|
||||
const tempFolderNode = new Node(emptyFolder(), [], true);
|
||||
tempFolderNode.item.id = 'temp';
|
||||
tempFolderNode.item.name = 'Temp';
|
||||
tempFolderNode.noDrag = true;
|
||||
tempFolderNode.noDrop = true;
|
||||
tempFolderNode.isTemp = true;
|
||||
tempFolderNode.parentNode = rootNode;
|
||||
|
||||
// Fill nodeMap with all file and folder nodes
|
||||
const nodeMap = {
|
||||
trash: trashFolderNode,
|
||||
temp: tempFolderNode,
|
||||
};
|
||||
rootGetters['folder/items'].forEach((item) => {
|
||||
nodeMap[item.id] = new Node(item, [], true);
|
||||
|
@ -89,11 +105,12 @@ export default {
|
|||
];
|
||||
nodeMap[item.id] = new Node(item, locations);
|
||||
});
|
||||
const rootNode = new Node(emptyFolder(), [], true, true);
|
||||
|
||||
// Build the tree
|
||||
Object.entries(nodeMap).forEach(([, node]) => {
|
||||
let parentNode = nodeMap[node.item.parentId];
|
||||
if (!parentNode || !parentNode.isFolder) {
|
||||
if (node.isTrash) {
|
||||
if (node.isTrash || node.isTemp) {
|
||||
return;
|
||||
}
|
||||
parentNode = rootNode;
|
||||
|
@ -103,12 +120,20 @@ export default {
|
|||
} else {
|
||||
parentNode.files.push(node);
|
||||
}
|
||||
node.parentNode = parentNode;
|
||||
});
|
||||
rootNode.sortChildren();
|
||||
|
||||
// Add Trash and Temp nodes
|
||||
rootNode.folders.unshift(tempFolderNode);
|
||||
tempFolderNode.files.forEach((node) => {
|
||||
node.noDrop = true;
|
||||
});
|
||||
if (trashFolderNode.files.length) {
|
||||
rootNode.folders.unshift(trashFolderNode);
|
||||
}
|
||||
// Add a fake file at the end of the root folder to allow drag and drop into it.
|
||||
|
||||
// Add a fake file at the end of the root folder to allow drag and drop into it
|
||||
rootNode.files.push(fakeFileNode);
|
||||
return {
|
||||
nodeMap,
|
||||
|
@ -169,7 +194,9 @@ export default {
|
|||
},
|
||||
newItem({ getters, commit, dispatch }, isFolder) {
|
||||
let parentId = getters.selectedNodeFolder.item.id;
|
||||
if (parentId === 'trash') {
|
||||
if (parentId === 'trash' // Not allowed to create new items in the trash
|
||||
|| (isFolder && parentId === 'temp') // Not allowed to create new folders in the temp folder
|
||||
) {
|
||||
parentId = null;
|
||||
}
|
||||
dispatch('openNode', parentId);
|
||||
|
@ -186,34 +213,50 @@ export default {
|
|||
if (selectedNode.isTrash || selectedNode.item.parentId === 'trash') {
|
||||
return dispatch('modal/trashDeletion', null, { root: true });
|
||||
}
|
||||
return dispatch(selectedNode.isFolder
|
||||
? 'modal/folderDeletion'
|
||||
: 'modal/fileDeletion',
|
||||
selectedNode.item,
|
||||
{ root: true },
|
||||
)
|
||||
|
||||
// See if we have a dialog to show
|
||||
let modalAction;
|
||||
let moveToTrash = true;
|
||||
if (selectedNode.isTemp) {
|
||||
modalAction = 'modal/tempFolderDeletion';
|
||||
moveToTrash = false;
|
||||
} else if (selectedNode.item.parentId === 'temp') {
|
||||
modalAction = 'modal/tempFileDeletion';
|
||||
moveToTrash = false;
|
||||
} else if (selectedNode.isFolder) {
|
||||
modalAction = 'modal/folderDeletion';
|
||||
}
|
||||
|
||||
return (modalAction
|
||||
? dispatch(modalAction, selectedNode.item, { root: true })
|
||||
: Promise.resolve())
|
||||
.then(() => {
|
||||
const deleteFile = (id) => {
|
||||
if (moveToTrash) {
|
||||
commit('file/patchItem', {
|
||||
id,
|
||||
parentId: 'trash',
|
||||
}, { root: true });
|
||||
} else {
|
||||
dispatch('deleteFile', id, { root: true });
|
||||
}
|
||||
};
|
||||
|
||||
if (selectedNode === getters.selectedNode) {
|
||||
const currentFileId = rootGetters['file/current'].id;
|
||||
let doClose = selectedNode.item.id === currentFileId;
|
||||
if (selectedNode.isFolder) {
|
||||
const recursiveMoveToTrash = (folderNode) => {
|
||||
folderNode.folders.forEach(recursiveMoveToTrash);
|
||||
const recursiveDelete = (folderNode) => {
|
||||
folderNode.folders.forEach(recursiveDelete);
|
||||
folderNode.files.forEach((fileNode) => {
|
||||
commit('file/patchItem', {
|
||||
id: fileNode.item.id,
|
||||
parentId: 'trash',
|
||||
}, { root: true });
|
||||
doClose = doClose || fileNode.item.id === currentFileId;
|
||||
deleteFile(fileNode.item.id);
|
||||
});
|
||||
commit('folder/deleteItem', folderNode.item.id, { root: true });
|
||||
};
|
||||
recursiveMoveToTrash(selectedNode);
|
||||
recursiveDelete(selectedNode);
|
||||
} else {
|
||||
commit('file/patchItem', {
|
||||
id: selectedNode.item.id,
|
||||
parentId: 'trash',
|
||||
}, { root: true });
|
||||
deleteFile(selectedNode.item.id);
|
||||
}
|
||||
if (doClose) {
|
||||
// Close the current file by opening the last opened, not deleted one
|
||||
|
|
|
@ -11,6 +11,7 @@ module.state = {
|
|||
module.getters = {
|
||||
...module.getters,
|
||||
current: state => state.itemMap[state.currentId] || empty(),
|
||||
isCurrentTemp: (state, getters) => getters.current.parentId === 'temp',
|
||||
lastOpened: (state, getters, rootState, rootGetters) =>
|
||||
state.itemMap[rootGetters['data/lastOpenedIds'][0]] || getters.items[0] || empty(),
|
||||
};
|
||||
|
|
|
@ -47,6 +47,7 @@ const store = new Vuex.Store({
|
|||
workspace,
|
||||
},
|
||||
state: {
|
||||
light: false,
|
||||
offline: false,
|
||||
lastOfflineCheck: 0,
|
||||
minuteCounter: 0,
|
||||
|
@ -64,6 +65,9 @@ const store = new Vuex.Store({
|
|||
},
|
||||
},
|
||||
mutations: {
|
||||
setLight: (state, value) => {
|
||||
state.light = value;
|
||||
},
|
||||
setOffline: (state, value) => {
|
||||
state.offline = value;
|
||||
},
|
||||
|
@ -109,16 +113,14 @@ const store = new Vuex.Store({
|
|||
return Promise.resolve(state.file.itemMap[id]);
|
||||
},
|
||||
deleteFile({ getters, commit }, fileId) {
|
||||
(getters['syncLocation/groupedByFileId'][fileId] || [])
|
||||
.forEach(item => commit('syncLocation/deleteItem', item.id));
|
||||
(getters['publishLocation/groupedByFileId'][fileId] || [])
|
||||
.forEach(item => commit('publishLocation/deleteItem', item.id));
|
||||
commit('file/deleteItem', fileId);
|
||||
commit('content/deleteItem', `${fileId}/content`);
|
||||
commit('syncedContent/deleteItem', `${fileId}/syncedContent`);
|
||||
commit('contentState/deleteItem', `${fileId}/contentState`);
|
||||
getters['syncLocation/items']
|
||||
.filter(item => item.fileId === fileId)
|
||||
.forEach(item => commit('syncLocation/deleteItem', item.id));
|
||||
getters['publishLocation/items']
|
||||
.filter(item => item.fileId === fileId)
|
||||
.forEach(item => commit('publishLocation/deleteItem', item.id));
|
||||
},
|
||||
},
|
||||
strict: debug,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const minPadding = 20;
|
||||
const editorTopPadding = 10;
|
||||
const navigationBarEditButtonsWidth = (36 * 14) + 8; // 14 buttons + 1 spacer
|
||||
const navigationBarLeftButtonWidth = 38 + 4 + 15;
|
||||
const navigationBarLeftButtonWidth = 38 + 4 + 12;
|
||||
const navigationBarRightButtonWidth = 38 + 8;
|
||||
const navigationBarSpinnerWidth = 24 + 8 + 5; // 5 for left margin
|
||||
const navigationBarLocationWidth = 20;
|
||||
|
@ -21,14 +21,18 @@ const constants = {
|
|||
};
|
||||
|
||||
function computeStyles(state, getters, layoutSettings = getters['data/layoutSettings'], styles = {
|
||||
showNavigationBar: !layoutSettings.showEditor || layoutSettings.showNavigationBar,
|
||||
showNavigationBar: layoutSettings.showNavigationBar
|
||||
|| !layoutSettings.showEditor
|
||||
|| state.content.revisionContent
|
||||
|| state.light,
|
||||
showStatusBar: layoutSettings.showStatusBar,
|
||||
showEditor: layoutSettings.showEditor,
|
||||
showSidePreview: layoutSettings.showSidePreview && layoutSettings.showEditor,
|
||||
showPreview: layoutSettings.showSidePreview || !layoutSettings.showEditor,
|
||||
showSideBar: layoutSettings.showSideBar,
|
||||
showExplorer: layoutSettings.showExplorer,
|
||||
showSideBar: layoutSettings.showSideBar && !state.light,
|
||||
showExplorer: layoutSettings.showExplorer && !state.light,
|
||||
layoutOverflow: false,
|
||||
hideLocations: state.light,
|
||||
}) {
|
||||
styles.innerHeight = state.layout.bodyHeight;
|
||||
if (styles.showNavigationBar) {
|
||||
|
@ -52,7 +56,8 @@ function computeStyles(state, getters, layoutSettings = getters['data/layoutSett
|
|||
}
|
||||
|
||||
let doublePanelWidth = styles.innerWidth - constants.buttonBarWidth;
|
||||
const showGutter = !!getters['discussion/currentDiscussion'];
|
||||
// No commenting for temp files
|
||||
const showGutter = !getters['file/isCurrentTemp'] && !!getters['discussion/currentDiscussion'];
|
||||
if (showGutter) {
|
||||
doublePanelWidth -= constants.gutterWidth;
|
||||
}
|
||||
|
|
|
@ -47,16 +47,21 @@ export default {
|
|||
throw err;
|
||||
});
|
||||
},
|
||||
fileDeletion: ({ dispatch }, item) => dispatch('open', {
|
||||
content: `<p>You are about to delete the file <b>${item.name}</b>. Are you sure?</p>`,
|
||||
folderDeletion: ({ dispatch }, item) => dispatch('open', {
|
||||
content: `<p>You are about to delete the folder <b>${item.name}</b>. Its files will be moved to Trash. Are you sure?</p>`,
|
||||
resolveText: 'Yes, delete',
|
||||
rejectText: 'No',
|
||||
}),
|
||||
folderDeletion: ({ dispatch }, item) => dispatch('open', {
|
||||
content: `<p>You are about to delete the folder <b>${item.name}</b> and all its files. Are you sure?</p>`,
|
||||
tempFileDeletion: ({ dispatch }, item) => dispatch('open', {
|
||||
content: `<p>You are about to permanently delete the temporary file <b>${item.name}</b>. Are you sure?</p>`,
|
||||
resolveText: 'Yes, delete',
|
||||
rejectText: 'No',
|
||||
}),
|
||||
tempFolderDeletion: ({ dispatch }) => dispatch('open', {
|
||||
content: '<p>You are about to permanently delete all the temporary files. Are you sure?</p>',
|
||||
resolveText: 'Yes, delete all',
|
||||
rejectText: 'No',
|
||||
}),
|
||||
discussionDeletion: ({ dispatch }) => dispatch('open', {
|
||||
content: '<p>You are about to delete a discussion. Are you sure?</p>',
|
||||
resolveText: 'Yes, delete',
|
||||
|
|
|
@ -2,31 +2,12 @@ import Vue from 'vue';
|
|||
import utils from '../services/utils';
|
||||
|
||||
export default (empty, simpleHash = false) => {
|
||||
// Use Date.now as a simple hash function, which is ok for not-synced types
|
||||
// Use Date.now() as a simple hash function, which is ok for not-synced types
|
||||
const hashFunc = simpleHash ? Date.now : item => utils.hash(utils.serializeObject({
|
||||
...item,
|
||||
hash: undefined,
|
||||
}));
|
||||
|
||||
function setItem(state, value) {
|
||||
const item = Object.assign(empty(value.id), value);
|
||||
if (!item.hash) {
|
||||
item.hash = hashFunc(item);
|
||||
}
|
||||
Vue.set(state.itemMap, item.id, item);
|
||||
}
|
||||
|
||||
function patchItem(state, patch) {
|
||||
const item = state.itemMap[patch.id];
|
||||
if (item) {
|
||||
Object.assign(item, patch);
|
||||
item.hash = hashFunc(item);
|
||||
Vue.set(state.itemMap, item.id, item);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
namespaced: true,
|
||||
state: {
|
||||
|
@ -36,8 +17,23 @@ export default (empty, simpleHash = false) => {
|
|||
items: state => Object.entries(state.itemMap).map(([, item]) => item),
|
||||
},
|
||||
mutations: {
|
||||
setItem,
|
||||
patchItem,
|
||||
setItem(state, value) {
|
||||
const item = Object.assign(empty(value.id), value);
|
||||
if (!item.hash) {
|
||||
item.hash = hashFunc(item);
|
||||
}
|
||||
Vue.set(state.itemMap, item.id, item);
|
||||
},
|
||||
patchItem(state, patch) {
|
||||
const item = state.itemMap[patch.id];
|
||||
if (item) {
|
||||
Object.assign(item, patch);
|
||||
item.hash = hashFunc(item);
|
||||
Vue.set(state.itemMap, item.id, item);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
deleteItem(state, id) {
|
||||
Vue.delete(state.itemMap, id);
|
||||
},
|
||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
Loading…
Reference in New Issue
Block a user