Merge branch 'dev'

This commit is contained in:
Benoit Schweblin 2018-03-15 13:53:35 +00:00
commit beac7fb1a3
41 changed files with 770 additions and 538 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View File

@ -18,6 +18,7 @@ import SplashScreen from './SplashScreen';
import syncSvc from '../services/syncSvc'; import syncSvc from '../services/syncSvc';
import networkSvc from '../services/networkSvc'; import networkSvc from '../services/networkSvc';
import sponsorSvc from '../services/sponsorSvc'; import sponsorSvc from '../services/sponsorSvc';
import tempFileSvc from '../services/tempFileSvc';
import timeSvc from '../services/timeSvc'; import timeSvc from '../services/timeSvc';
import store from '../store'; import store from '../store';
@ -94,6 +95,7 @@ export default {
networkSvc.init(); networkSvc.init();
sponsorSvc.init(); sponsorSvc.init();
this.ready = true; this.ready = true;
tempFileSvc.setReady();
}) })
.catch((err) => { .catch((err) => {
if (err && err.message !== 'reload') { if (err && err.message !== 'reload') {

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="button-bar"> <div class="button-bar">
<div class="button-bar__inner button-bar__inner--top"> <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> <icon-navigation-bar></icon-navigation-bar>
</button> </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'"> <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> </template>
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
export default { export default {
computed: mapGetters('data', [ computed: {
...mapState([
'light',
]),
...mapGetters('data', [
'layoutSettings', 'layoutSettings',
]), ]),
},
methods: mapActions('data', [ methods: mapActions('data', [
'toggleNavigationBar', 'toggleNavigationBar',
'toggleEditor', 'toggleEditor',

View File

@ -3,7 +3,7 @@
<pre class="editor__inner markdown-highlighting" :style="{padding: styles.editorPadding}" :class="{monospaced: computedSettings.editor.monospacedFontOnly}"></pre> <pre class="editor__inner markdown-highlighting" :style="{padding: styles.editorPadding}" :class="{monospaced: computedSettings.editor.monospacedFontOnly}"></pre>
<div class="gutter" :style="{left: styles.editorGutterLeft + 'px'}"> <div class="gutter" :style="{left: styles.editorGutterLeft + 'px'}">
<comment-list v-if="styles.editorGutterWidth"></comment-list> <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>
</div> </div>
</template> </template>
@ -19,6 +19,9 @@ export default {
EditorNewDiscussionButton, EditorNewDiscussionButton,
}, },
computed: { computed: {
...mapGetters('file', [
'isCurrentTemp',
]),
...mapGetters('layout', [ ...mapGetters('layout', [
'styles', 'styles',
]), ]),

View File

@ -19,7 +19,7 @@
<icon-close></icon-close> <icon-close></icon-close>
</button> </button>
</div> </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> <explorer-node :node="rootNode" :depth="0"></explorer-node>
</div> </div>
</div> </div>
@ -34,6 +34,9 @@ export default {
ExplorerNode, ExplorerNode,
}, },
computed: { computed: {
...mapState([
'light',
]),
...mapState('explorer', [ ...mapState('explorer', [
'newChildNode', 'newChildNode',
]), ]),
@ -58,7 +61,7 @@ export default {
}, },
}, },
created() { created() {
this.$store.watch( this.$watch(
() => this.$store.getters['file/current'].id, () => this.$store.getters['file/current'].id,
(currentFileId) => { (currentFileId) => {
this.$store.commit('explorer/setSelectedId', currentFileId); this.$store.commit('explorer/setSelectedId', currentFileId);

View File

@ -1,5 +1,5 @@
<template> <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> <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"> <input type="text" class="text-input" v-focus @blur="submitEdit()" @keydown.enter="submitEdit()" @keydown.esc="submitEdit(true)" v-model="editingNodeName">
</div> </div>
@ -169,7 +169,7 @@ export default {
perform: () => this.newItem(false), perform: () => this.newItem(false),
}, { }, {
name: 'New folder', name: 'New folder',
disabled: !this.node.isFolder || this.node.isTrash, disabled: !this.node.isFolder || this.node.isTrash || this.node.isTemp,
perform: () => this.newItem(true), perform: () => this.newItem(true),
}, { }, {
type: 'separator', type: 'separator',
@ -179,7 +179,6 @@ export default {
perform: () => this.setEditingId(this.node.item.id), perform: () => this.setEditingId(this.node.item.id),
}, { }, {
name: 'Delete', name: 'Delete',
disabled: this.node.isTrash || this.node.item.parentId === 'trash',
perform: () => this.deleteItem(), perform: () => this.deleteItem(),
}], }],
}) })

View File

@ -44,7 +44,7 @@
<side-bar></side-bar> <side-bar></side-bar>
</div> </div>
</div> </div>
<tour v-if="!layoutSettings.welcomeTourFinished"></tour> <tour v-if="!light && !layoutSettings.welcomeTourFinished"></tour>
</div> </div>
</template> </template>
@ -78,6 +78,9 @@ export default {
FindReplace, FindReplace,
}, },
computed: { computed: {
...mapState([
'light',
]),
...mapState('content', [ ...mapState('content', [
'revisionContent', 'revisionContent',
]), ]),
@ -116,8 +119,13 @@ export default {
editorSvc.init(editorElt, previewElt, tocElt); editorSvc.init(editorElt, previewElt, tocElt);
// Focus on the editor every time reader mode is disabled // Focus on the editor every time reader mode is disabled
this.$watch(() => this.styles.showEditor, const focus = () => {
showEditor => showEditor && editorSvc.clEditor.focus()); if (this.styles.showEditor) {
editorSvc.clEditor.focus();
}
};
setTimeout(focus, 100);
this.$watch(() => this.styles.showEditor, focus);
}, },
destroyed() { destroyed() {
window.removeEventListener('resize', this.updateStyle); window.removeEventListener('resize', this.updateStyle);

View File

@ -1,40 +1,6 @@
<template> <template>
<div class="modal" @keydown.esc="onEscape" @keydown.tab="onTab"> <div class="modal" @keydown.esc="onEscape" @keydown.tab="onTab">
<file-properties-modal v-if="config.type === 'fileProperties'"></file-properties-modal> <component v-if="currentModalComponent" :is="currentModalComponent"></component>
<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>
<modal-inner v-else aria-label="Dialog"> <modal-inner v-else aria-label="Dialog">
<div class="modal__content" v-html="config.content"></div> <div class="modal__content" v-html="config.content"></div>
<div class="modal__button-bar"> <div class="modal__button-bar">
@ -129,9 +95,22 @@ export default {
CouchdbWorkspaceModal, CouchdbWorkspaceModal,
CouchdbCredentialsModal, CouchdbCredentialsModal,
}, },
computed: mapGetters('modal', [ computed: {
...mapGetters('modal', [
'config', '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: { methods: {
onEscape() { onEscape() {
this.config.reject(); this.config.reject();

View File

@ -2,11 +2,13 @@
<nav class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor && !revisionContent}"> <nav class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor && !revisionContent}">
<!-- Explorer --> <!-- Explorer -->
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button"> <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> </div>
<!-- Side bar --> <!-- Side bar -->
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button"> <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>
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--title flex flex--row"> <div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--title flex flex--row">
<!-- Spinner --> <!-- Spinner -->
@ -57,6 +59,7 @@ import editorSvc from '../services/editorSvc';
import syncSvc from '../services/syncSvc'; import syncSvc from '../services/syncSvc';
import publishSvc from '../services/publishSvc'; import publishSvc from '../services/publishSvc';
import animationSvc from '../services/animationSvc'; import animationSvc from '../services/animationSvc';
import tempFileSvc from '../services/tempFileSvc';
import utils from '../services/utils'; import utils from '../services/utils';
export default { export default {
@ -68,6 +71,7 @@ export default {
}), }),
computed: { computed: {
...mapState([ ...mapState([
'light',
'offline', 'offline',
]), ]),
...mapState('queue', [ ...mapState('queue', [
@ -104,9 +108,7 @@ export default {
} }
this.titleFakeElt.textContent = this.title; this.titleFakeElt.textContent = this.title;
const width = this.titleFakeElt.getBoundingClientRect().width + 2; // 2px for the caret const width = this.titleFakeElt.getBoundingClientRect().width + 2; // 2px for the caret
return width < this.styles.titleMaxWidth return Math.min(width, this.styles.titleMaxWidth);
? width
: this.styles.titleMaxWidth;
}, },
titleScrolling() { titleScrolling() {
const result = this.titleHover && !this.titleFocus; const result = this.titleHover && !this.titleFocus;
@ -178,9 +180,12 @@ export default {
} }
this.titleInputElt.blur(); this.titleInputElt.blur();
}, },
close() {
tempFileSvc.close();
},
}, },
created() { created() {
this.$store.watch( this.$watch(
() => this.$store.getters['file/current'].name, () => this.$store.getters['file/current'].name,
(name) => { (name) => {
this.title = name; this.title = name;
@ -213,7 +218,7 @@ export default {
float: left; float: left;
&.navigation-bar__inner--button { &.navigation-bar__inner--button {
margin-right: 15px; margin-right: 12px;
} }
} }
@ -255,6 +260,7 @@ $button-size: 36px;
.navigation-bar__button { .navigation-bar__button {
width: $button-size; width: $button-size;
padding: 0 8px; padding: 0 8px;
transition: opacity 0.25s;
.navigation-bar__inner--button & { .navigation-bar__inner--button & {
padding: 0 4px; padding: 0 4px;
@ -290,7 +296,7 @@ $button-size: 36px;
.navigation-bar__title { .navigation-bar__title {
margin: 0 4px; margin: 0 4px;
font-size: 22px; font-size: 21px;
.layout--revision & { .layout--revision & {
position: absolute; position: absolute;

View File

@ -5,7 +5,7 @@
</div> </div>
<div class="gutter" :style="{left: styles.previewGutterLeft + 'px'}"> <div class="gutter" :style="{left: styles.previewGutterLeft + 'px'}">
<comment-list v-if="styles.previewGutterWidth"></comment-list> <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> </div>
<div v-if="!styles.showEditor" class="preview__corner"> <div v-if="!styles.showEditor" class="preview__corner">
@ -32,9 +32,14 @@ export default {
data: () => ({ data: () => ({
previewTop: true, previewTop: true,
}), }),
computed: mapGetters('layout', [ computed: {
...mapGetters('file', [
'isCurrentTemp',
]),
...mapGetters('layout', [
'styles', 'styles',
]), ]),
},
methods: { methods: {
...mapActions('data', [ ...mapActions('data', [
'toggleEditor', 'toggleEditor',

View File

@ -59,7 +59,7 @@ export default {
created() { created() {
editorSvc.$on('sectionList', () => this.computeText()); editorSvc.$on('sectionList', () => this.computeText());
editorSvc.$on('selectionRange', () => this.computeText()); editorSvc.$on('selectionRange', () => this.computeText());
editorSvc.$on('previewText', () => this.computeHtml()); editorSvc.$on('previewCtx', () => this.computeHtml());
editorSvc.$on('previewSelectionRange', () => this.computeHtml()); editorSvc.$on('previewSelectionRange', () => this.computeHtml());
}, },
@ -92,7 +92,7 @@ export default {
this.htmlSelection = true; this.htmlSelection = true;
if (!text) { if (!text) {
this.htmlSelection = false; this.htmlSelection = false;
text = editorSvc.previewText; text = editorSvc.previewCtx.text;
} }
if (text != null) { if (text != null) {
this.htmlStats.forEach((stat) => { this.htmlStats.forEach((stat) => {

View File

@ -30,7 +30,7 @@ export default {
e.preventDefault(); e.preventDefault();
const y = e.clientY - tocElt.getBoundingClientRect().top; const y = e.clientY - tocElt.getBoundingClientRect().top;
editorSvc.sectionDescList.some((sectionDesc) => { editorSvc.previewCtx.sectionDescList.some((sectionDesc) => {
if (y >= sectionDesc.tocDimension.endOffset) { if (y >= sectionDesc.tocDimension.endOffset) {
return false; return false;
} }
@ -64,7 +64,7 @@ export default {
const updateMaskY = () => { const updateMaskY = () => {
const scrollPosition = editorSvc.getScrollPosition(); const scrollPosition = editorSvc.getScrollPosition();
if (scrollPosition) { if (scrollPosition) {
const sectionDesc = editorSvc.sectionDescList[scrollPosition.sectionIdx]; const sectionDesc = editorSvc.previewCtx.sectionDescList[scrollPosition.sectionIdx];
this.maskY = sectionDesc.tocDimension.startOffset + this.maskY = sectionDesc.tocDimension.startOffset +
(scrollPosition.posInSection * sectionDesc.tocDimension.height); (scrollPosition.posInSection * sectionDesc.tocDimension.height);
} }

View File

@ -23,7 +23,7 @@ export default class PreviewClassApplier {
this.lastEltCount = this.eltCollection.length; this.lastEltCount = this.eltCollection.length;
this.restoreClass = () => { this.restoreClass = () => {
if (!editorSvc.sectionDescWithDiffsList) { if (!editorSvc.previewCtxWithDiffs) {
this.removeClass(); this.removeClass();
} else if (!this.eltCollection.length || this.eltCollection.length !== this.lastEltCount) { } else if (!this.eltCollection.length || this.eltCollection.length !== this.lastEltCount) {
this.removeClass(); this.removeClass();
@ -31,15 +31,17 @@ export default class PreviewClassApplier {
} }
}; };
editorSvc.$on('sectionDescWithDiffsList', this.restoreClass); editorSvc.$on('previewCtxWithDiffs', this.restoreClass);
nextTick(() => this.applyClass()); nextTick(() => this.applyClass());
} }
applyClass() { applyClass() {
const offset = this.offsetGetter(); const offset = this.offsetGetter();
if (offset) { if (offset) {
const offsetStart = editorSvc.getPreviewOffset(offset.start, editorSvc.sectionDescList); const offsetStart = editorSvc.getPreviewOffset(
const offsetEnd = editorSvc.getPreviewOffset(offset.end, editorSvc.sectionDescList); offset.start, editorSvc.previewCtx.sectionDescList);
const offsetEnd = editorSvc.getPreviewOffset(
offset.end, editorSvc.previewCtx.sectionDescList);
if (offsetStart != null && offsetEnd != null && offsetStart !== offsetEnd) { if (offsetStart != null && offsetEnd != null && offsetStart !== offsetEnd) {
const start = cledit.Utils.findContainer( const start = cledit.Utils.findContainer(
editorSvc.previewElt, Math.min(offsetStart, offsetEnd)); editorSvc.previewElt, Math.min(offsetStart, offsetEnd));
@ -63,8 +65,7 @@ export default class PreviewClassApplier {
} }
stop() { stop() {
editorSvc.$off('previewHtml', this.restoreClass); editorSvc.$off('previewCtxWithDiffs', this.restoreClass);
editorSvc.$off('sectionDescWithDiffsList', this.restoreClass);
nextTick(() => this.removeClass()); nextTick(() => this.removeClass());
} }
} }

View File

@ -163,9 +163,9 @@ export default {
() => this.updateSticky(), () => this.updateSticky(),
{ immediate: true }); { immediate: true });
// Move preview discussions once sectionDescWithDiffsList have been calculated // Move preview discussions once previewCtxWithDiffs has been calculated
if (!editorSvc.sectionDescWithDiffsList) { if (!editorSvc.previewCtxWithDiffs) {
editorSvc.$once('sectionDescWithDiffsList', () => { editorSvc.$once('previewCtxWithDiffs', () => {
this.updateTops(); this.updateTops();
this.updateSticky(); this.updateSticky();
}); });

View File

@ -48,6 +48,7 @@ export default {
'previousDiscussionId', 'previousDiscussionId',
'nextDiscussionId', 'nextDiscussionId',
'currentFileDiscussions', 'currentFileDiscussions',
'currentDiscussionLastCommentId',
]), ]),
...mapGetters('layout', [ ...mapGetters('layout', [
'constants', 'constants',
@ -59,7 +60,7 @@ export default {
return this.nextDiscussionId && this.nextDiscussionId !== this.currentDiscussionId; return this.nextDiscussionId && this.nextDiscussionId !== this.currentDiscussionId;
}, },
showRemove() { showRemove() {
return this.currentFileDiscussions[this.currentDiscussionId]; return this.currentDiscussionLastCommentId;
}, },
}, },
methods: { methods: {

View File

@ -28,7 +28,7 @@ export default {
) { ) {
this.selection = editorSvc.getTrimmedSelection(); this.selection = editorSvc.getTrimmedSelection();
if (this.selection) { if (this.selection) {
const text = editorSvc.previewTextWithDiffsList; const text = editorSvc.previewCtxWithDiffs.text;
offset = editorSvc.getPreviewOffset(this.selection.end); offset = editorSvc.getPreviewOffset(this.selection.end);
while (offset && text[offset - 1] === '\n') { while (offset && text[offset - 1] === '\n') {
offset -= 1; offset -= 1;

View File

@ -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>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> <p><b>Note:</b> This will sync your main workspace.</p>
</div> </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="revision" v-for="revision in revisionsWithSpacer" :key="revision.id">
<div class="history__spacer" v-if="revision.spacer"></div> <div class="history__spacer" v-if="revision.spacer"></div>
<a class="revision__button button flex flex--row" href="javascript:void(0)" @click="open(revision)"> <a class="revision__button button flex flex--row" href="javascript:void(0)" @click="open(revision)">
@ -53,12 +59,16 @@ export default {
}, },
data: () => ({ data: () => ({
allRevisions: [], allRevisions: [],
loading: false,
showCount: pageSize, showCount: pageSize,
}), }),
computed: { computed: {
...mapGetters('workspace', [ ...mapGetters('workspace', [
'syncToken', 'syncToken',
]), ]),
currentFileName() {
return this.$store.getters['file/current'].name;
},
revisions() { revisions() {
return this.allRevisions.slice(0, this.showCount); return this.allRevisions.slice(0, this.showCount);
}, },
@ -127,7 +137,7 @@ export default {
previewClassAppliers.forEach(previewClassApplier => previewClassApplier.stop()); previewClassAppliers.forEach(previewClassApplier => previewClassApplier.stop());
previewClassAppliers = []; previewClassAppliers = [];
if (revisionContent) { if (revisionContent) {
editorSvc.$once('sectionDescWithDiffsList', () => { editorSvc.$once('previewCtxWithDiffs', () => {
let offset = 0; let offset = 0;
revisionContent.diffs.forEach(([type, text]) => { revisionContent.diffs.forEach(([type, text]) => {
if (type) { if (type) {
@ -177,7 +187,9 @@ export default {
}); });
} }
if (revisionsPromise) { if (revisionsPromise) {
this.loading = true;
revisionsPromise.then((revisions) => { revisionsPromise.then((revisions) => {
this.loading = false;
this.allRevisions = revisions; this.allRevisions = revisions;
}); });
} }

View File

@ -61,14 +61,14 @@
Markdown cheat sheet Markdown cheat sheet
</menu-entry> </menu-entry>
<hr> <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')"> <menu-entry @click.native="setPanel('import')">
<icon-content-save slot="icon"></icon-content-save> <icon-content-save slot="icon"></icon-content-save>
Import from disk Import from disk
</menu-entry> </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"> <menu-entry @click.native="print">
<icon-printer slot="icon"></icon-printer> <icon-printer slot="icon"></icon-printer>
Print Print

View File

@ -1,5 +1,9 @@
<template> <template>
<div class="side-bar__panel side-bar__panel--menu"> <div class="side-bar__panel side-bar__panel--menu">
<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 v-else>
<div class="side-bar__info" v-if="noToken"> <div class="side-bar__info" v-if="noToken">
<p>You have to <b>link an account</b> to start publishing files.</p> <p>You have to <b>link an account</b> to start publishing files.</p>
</div> </div>
@ -95,6 +99,7 @@
<span>Add Zendesk account</span> <span>Add Zendesk account</span>
</menu-entry> </menu-entry>
</div> </div>
</div>
</template> </template>
<script> <script>
@ -126,6 +131,9 @@ export default {
...mapState('queue', [ ...mapState('queue', [
'isPublishRequested', 'isPublishRequested',
]), ]),
...mapGetters('file', [
'isCurrentTemp',
]),
...mapGetters('publishLocation', { ...mapGetters('publishLocation', {
publishLocations: 'current', publishLocations: 'current',
}), }),

View File

@ -1,5 +1,9 @@
<template> <template>
<div class="side-bar__panel side-bar__panel--menu"> <div class="side-bar__panel side-bar__panel--menu">
<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 v-else>
<div class="side-bar__info" v-if="noToken"> <div class="side-bar__info" v-if="noToken">
<p>You have to <b>link an account</b> to start syncing files.</p> <p>You have to <b>link an account</b> to start syncing files.</p>
</div> </div>
@ -72,6 +76,7 @@
<span>Add GitHub account</span> <span>Add GitHub account</span>
</menu-entry> </menu-entry>
</div> </div>
</div>
</template> </template>
<script> <script>
@ -107,6 +112,9 @@ export default {
...mapGetters('workspace', [ ...mapGetters('workspace', [
'syncToken', 'syncToken',
]), ]),
...mapGetters('file', [
'isCurrentTemp',
]),
...mapGetters('syncLocation', { ...mapGetters('syncLocation', {
syncLocations: 'current', syncLocations: 'current',
}), }),

View File

@ -75,5 +75,6 @@ export default {
font-size: 0.9em; font-size: 0.9em;
padding: 0.75em 1.5em; padding: 0.75em 1.5em;
margin-bottom: 1.2em; margin-bottom: 1.2em;
line-height: 1.55;
} }
</style> </style>

View File

@ -341,16 +341,23 @@ function SelectionMgr(editor) {
offsetInContainer = offset.offsetInContainer; offsetInContainer = offset.offsetInContainer;
} }
let containerElt = container; let containerElt = container;
if (!containerElt.hasChildNodes()) { if (!containerElt.hasChildNodes() && container.parentNode) {
containerElt = container.parentNode; containerElt = container.parentNode;
} }
let isInvisible = false; let isInvisible = false;
while (containerElt.offsetHeight === 0) { while (!containerElt.offsetHeight) {
isInvisible = true; isInvisible = true;
if (containerElt.previousSibling) { if (containerElt.previousSibling) {
containerElt = containerElt.previousSibling; containerElt = containerElt.previousSibling;
} else { } else {
containerElt = containerElt.parentNode; containerElt = containerElt.parentNode;
if (!containerElt) {
return {
top: 0,
height: 0,
left: 0,
};
}
} }
} }
let rect; let rect;

View File

@ -54,15 +54,15 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
converter: null, converter: null,
parsingCtx: null, parsingCtx: null,
conversionCtx: null, conversionCtx: null,
sectionList: null, previewCtx: {
sectionDescList: [], sectionDescList: [],
sectionDescMeasuredList: null, },
sectionDescWithDiffsList: null, previewCtxMeasured: null,
previewCtxWithDiffs: null,
sectionList: null,
selectionRange: null, selectionRange: null,
previewSelectionRange: null, previewSelectionRange: null,
previewSelectionStartOffset: null, previewSelectionStartOffset: null,
previewHtml: null,
previewText: null,
/** /**
* Initialize the Prism grammar with the options * 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 * Initialize the cledit editor with markdown-it section parser and Prism highlighter
*/ */
initClEditor() { initClEditor() {
this.sectionDescMeasuredList = null; this.previewCtxMeasured = null;
this.sectionDescWithDiffsList = null; editorSvc.$emit('previewCtxMeasured', null);
this.previewCtxWithDiffs = null;
editorSvc.$emit('previewCtxWithDiffs', null);
const options = { const options = {
sectionHighlighter: section => Prism.highlight( sectionHighlighter: section => Prism.highlight(
section.text, this.prismGrammars[section.data]), 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()` * Refresh the preview with the result of `convert()`
*/ */
refreshPreview() { refreshPreview() {
const newSectionDescList = []; const sectionDescList = [];
let sectionPreviewElt; let sectionPreviewElt;
let sectionTocElt; let sectionTocElt;
let sectionIdx = 0; 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) { for (let i = 0; i < item[1].length; i += 1) {
const section = this.conversionCtx.sectionList[sectionIdx]; const section = this.conversionCtx.sectionList[sectionIdx];
if (item[0] === 0) { if (item[0] === 0) {
let sectionDesc = this.sectionDescList[sectionDescIdx]; let sectionDesc = this.previewCtx.sectionDescList[sectionDescIdx];
sectionDescIdx += 1; sectionDescIdx += 1;
if (sectionDesc.editorElt !== section.elt) { if (sectionDesc.editorElt !== section.elt) {
// Force textToPreviewDiffs computation // Force textToPreviewDiffs computation
sectionDesc = new SectionDesc( sectionDesc = new SectionDesc(
section, sectionDesc.previewElt, sectionDesc.tocElt, sectionDesc.html); section, sectionDesc.previewElt, sectionDesc.tocElt, sectionDesc.html);
} }
newSectionDescList.push(sectionDesc); sectionDescList.push(sectionDesc);
previewHtml += sectionDesc.html; previewHtml += sectionDesc.html;
sectionIdx += 1; sectionIdx += 1;
insertBeforePreviewElt = insertBeforePreviewElt.nextSibling; insertBeforePreviewElt = insertBeforePreviewElt.nextSibling;
@ -187,20 +189,22 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
} }
previewHtml += html; 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.classList[
this.tocElt.querySelector('.cl-toc-section *') ? 'remove' : 'add' this.tocElt.querySelector('.cl-toc-section *') ? 'remove' : 'add'
]('toc-tab--empty'); ]('toc-tab--empty');
this.previewText = this.previewElt.textContent; this.previewCtx = {
this.$emit('previewText', this.previewText); markdown: this.conversionCtx.text,
html: previewHtml.replace(/^\s+|\s+$/g, ''),
text: this.previewElt.textContent,
sectionDescList,
};
this.$emit('previewCtx', this.previewCtx);
this.makeTextToPreviewDiffs(); this.makeTextToPreviewDiffs();
// Wait for images to load // Wait for images to load
@ -217,22 +221,20 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
Promise.all(loadedPromises) Promise.all(loadedPromises)
// Debounce if sections have already been measured // 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. * Measure the height of each section in editor, preview and toc.
*/ */
measureSectionDimensions: allowDebounce((restoreScrollPosition) => { measureSectionDimensions: allowDebounce((restoreScrollPosition = false, force = false) => {
if (editorSvc.sectionDescList && if (force || editorSvc.previewCtx !== editorSvc.previewCtxMeasured) {
this.sectionDescList !== editorSvc.sectionDescMeasuredList
) {
sectionUtils.measureSectionDimensions(editorSvc); sectionUtils.measureSectionDimensions(editorSvc);
editorSvc.sectionDescMeasuredList = editorSvc.sectionDescList; editorSvc.previewCtxMeasured = editorSvc.previewCtx;
if (restoreScrollPosition) { if (restoreScrollPosition) {
editorSvc.restoreScrollPosition(); editorSvc.restoreScrollPosition();
} }
editorSvc.$emit('sectionDescMeasuredList', editorSvc.sectionDescMeasuredList); editorSvc.$emit('previewCtxMeasured', editorSvc.previewCtxMeasured);
} }
}, 500), }, 500),
@ -241,12 +243,10 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
* asynchronously unless there is only one section to compute. * asynchronously unless there is only one section to compute.
*/ */
makeTextToPreviewDiffs() { makeTextToPreviewDiffs() {
if (editorSvc.sectionDescList && if (editorSvc.previewCtx !== editorSvc.previewCtxWithDiffs) {
editorSvc.sectionDescList !== editorSvc.sectionDescWithDiffsList
) {
const makeOne = () => { const makeOne = () => {
let hasOne = false; let hasOne = false;
const hasMore = editorSvc.sectionDescList const hasMore = editorSvc.previewCtx.sectionDescList
.some((sectionDesc) => { .some((sectionDesc) => {
if (!sectionDesc.textToPreviewDiffs) { if (!sectionDesc.textToPreviewDiffs) {
if (hasOne) { if (hasOne) {
@ -264,9 +264,8 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
if (hasMore) { if (hasMore) {
setTimeout(() => makeOne(), 10); setTimeout(() => makeOne(), 10);
} else { } else {
editorSvc.previewTextWithDiffsList = editorSvc.previewText; editorSvc.previewCtxWithDiffs = editorSvc.previewCtx;
editorSvc.sectionDescWithDiffsList = editorSvc.sectionDescList; editorSvc.$emit('previewCtxWithDiffs', editorSvc.previewCtxWithDiffs);
editorSvc.$emit('sectionDescWithDiffsList', editorSvc.sectionDescWithDiffsList);
} }
}; };
makeOne(); makeOne();
@ -517,7 +516,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
let lastContentId = null; let lastContentId = null;
let lastProperties; let lastProperties;
store.watch( store.watch(
() => store.getters['content/current'].hash, () => store.getters['content/currentChangeTrigger'],
() => { () => {
const content = store.getters['content/current']; const content = store.getters['content/current'];
// Track ID changes // Track ID changes
@ -541,7 +540,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
if (initClEditor) { if (initClEditor) {
this.initClEditor(); this.initClEditor();
} }
// Apply possible text and discussion changes // Apply potential text and discussion changes
this.applyContent(); this.applyContent();
}, { }, {
immediate: true, immediate: true,
@ -555,7 +554,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
}); });
store.watch(() => utils.serializeObject(store.getters['layout/styles']), store.watch(() => utils.serializeObject(store.getters['layout/styles']),
() => this.measureSectionDimensions(false, true)); () => this.measureSectionDimensions(false, true, true));
this.initHighlighters(); this.initHighlighters();
this.$emit('inited'); this.$emit('inited');

View File

@ -18,8 +18,8 @@ export default {
: 'previewDimension'; : 'previewDimension';
const scrollTop = elt.parentNode.scrollTop; const scrollTop = elt.parentNode.scrollTop;
let result; let result;
if (this.sectionDescMeasuredList) { if (this.previewCtxMeasured) {
this.sectionDescMeasuredList.some((sectionDesc, sectionIdx) => { this.previewCtxMeasured.sectionDescList.some((sectionDesc, sectionIdx) => {
if (scrollTop >= sectionDesc[dimensionKey].endOffset) { if (scrollTop >= sectionDesc[dimensionKey].endOffset) {
return false; return false;
} }
@ -40,8 +40,8 @@ export default {
*/ */
restoreScrollPosition() { restoreScrollPosition() {
const scrollPosition = store.getters['contentState/current'].scrollPosition; const scrollPosition = store.getters['contentState/current'].scrollPosition;
if (scrollPosition && this.sectionDescMeasuredList) { if (scrollPosition && this.previewCtxMeasured) {
const sectionDesc = this.sectionDescMeasuredList[scrollPosition.sectionIdx]; const sectionDesc = this.previewCtxMeasured.sectionDescList[scrollPosition.sectionIdx];
if (sectionDesc) { if (sectionDesc) {
const editorScrollTop = sectionDesc.editorDimension.startOffset + const editorScrollTop = sectionDesc.editorDimension.startOffset +
(sectionDesc.editorDimension.height * scrollPosition.posInSection); (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 * 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) { if (!sectionDescList) {
return null; return null;
} }
@ -81,7 +84,10 @@ export default {
/** /**
* Get the offset of the markdown in the editor corresponding to the offset in the preview * 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) { if (!sectionDescList) {
return null; return null;
} }

View File

@ -6,6 +6,7 @@ import welcomeFile from '../data/welcomeFile.md';
const dbVersion = 1; const dbVersion = 1;
const dbStoreName = 'objects'; const dbStoreName = 'objects';
const exportWorkspace = utils.queryParams.exportWorkspace; const exportWorkspace = utils.queryParams.exportWorkspace;
const silent = utils.queryParams.silent;
const resetApp = utils.queryParams.reset; const resetApp = utils.queryParams.reset;
const deleteMarkerMaxAge = 1000; const deleteMarkerMaxAge = 1000;
const checkSponsorshipAfter = (5 * 60 * 1000) + (30 * 1000); // tokenExpirationMargin + 30 sec const checkSponsorshipAfter = (5 * 60 * 1000) + (30 * 1000); // tokenExpirationMargin + 30 sec
@ -99,142 +100,6 @@ const localDbSvc = {
hashMap, hashMap,
connection: null, 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. * Sync data items stored in the localStorage.
*/ */
@ -331,6 +196,10 @@ const localDbSvc = {
* Write all changes from the store since previous transaction. * Write all changes from the store since previous transaction.
*/ */
writeAll(storeItemMap, tx) { writeAll(storeItemMap, tx) {
if (silent) {
// Skip writing to DB in silent mode
return;
}
const dbStore = tx.objectStore(dbStoreName); const dbStore = tx.objectStore(dbStoreName);
const incrementedTx = this.lastTx + 1; const incrementedTx = this.lastTx + 1;
@ -475,6 +344,142 @@ const localDbSvc = {
localStorage.removeItem(`${id}/lastWindowFocus`); 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}`) const loader = type => fileId => localDbSvc.loadItem(`${fileId}/${type}`)

View File

@ -165,6 +165,7 @@ const markdownConversionSvc = {
lines.pop(); lines.pop();
} }
const parsingCtx = { const parsingCtx = {
text,
sections: [], sections: [],
converter, converter,
markdownState, markdownState,
@ -248,6 +249,7 @@ const markdownConversionSvc = {
]; ];
} }
return { return {
text: parsingCtx.text,
sectionList: parsingCtx.sectionList, sectionList: parsingCtx.sectionList,
htmlSectionList, htmlSectionList,
htmlSectionDiff, htmlSectionDiff,

View File

@ -11,7 +11,7 @@ let isScrollEditor;
let isScrollPreview; let isScrollPreview;
let isEditorMoving; let isEditorMoving;
let isPreviewMoving; let isPreviewMoving;
let sectionDescList; let sectionDescList = [];
let throttleTimeoutId; let throttleTimeoutId;
let throttleLastTime = 0; let throttleLastTime = 0;
@ -34,7 +34,7 @@ function throttle(func, wait) {
const doScrollSync = () => { const doScrollSync = () => {
const localSkipAnimation = skipAnimation || !store.getters['layout/styles'].showSidePreview; const localSkipAnimation = skipAnimation || !store.getters['layout/styles'].showSidePreview;
skipAnimation = false; skipAnimation = false;
if (!store.getters['data/layoutSettings'].scrollSync || !sectionDescList || sectionDescList.length === 0) { if (!store.getters['data/layoutSettings'].scrollSync || sectionDescList.length === 0) {
return; return;
} }
let editorScrollTop = editorScrollerElt.scrollTop; let editorScrollTop = editorScrollerElt.scrollTop;
@ -144,10 +144,10 @@ editorSvc.$on('inited', () => {
editorSvc.$on('sectionList', () => { editorSvc.$on('sectionList', () => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
isPreviewRefreshing = true; isPreviewRefreshing = true;
sectionDescList = undefined; sectionDescList = [];
}); });
editorSvc.$on('previewText', () => { editorSvc.$on('previewCtx', () => {
// Assume the user is writing in the editor // Assume the user is writing in the editor
isScrollEditor = store.getters['layout/styles'].showEditor; isScrollEditor = store.getters['layout/styles'].showEditor;
// A preview scrolling event can occur if height is smaller // A preview scrolling event can occur if height is smaller
@ -170,7 +170,9 @@ store.watch(
skipAnimation = true; skipAnimation = true;
}); });
editorSvc.$on('sectionDescMeasuredList', (sectionDescMeasuredList) => { editorSvc.$on('previewCtxMeasured', (previewCtxMeasured) => {
sectionDescList = sectionDescMeasuredList; if (previewCtxMeasured) {
sectionDescList = previewCtxMeasured.sectionDescList;
forceScrollSync(); forceScrollSync();
}
}); });

View File

@ -111,6 +111,11 @@ function publishFile(fileId) {
} }
function requestPublish() { function requestPublish() {
// No publish in light mode
if (store.state.light) {
return;
}
store.dispatch('queue/enqueuePublishRequest', () => new Promise((resolve, reject) => { store.dispatch('queue/enqueuePublishRequest', () => new Promise((resolve, reject) => {
let intervalId; let intervalId;
const attempt = () => { const attempt = () => {

View File

@ -6,7 +6,8 @@ function SectionDimension(startOffset, endOffset) {
function dimensionNormalizer(dimensionName) { function dimensionNormalizer(dimensionName) {
return (editorSvc) => { return (editorSvc) => {
const dimensionList = editorSvc.sectionDescList.map(sectionDesc => sectionDesc[dimensionName]); const dimensionList = editorSvc.previewCtx.sectionDescList.map(
sectionDesc => sectionDesc[dimensionName]);
let dimension; let dimension;
let i; let i;
let j; let j;
@ -43,11 +44,11 @@ function measureSectionDimensions(editorSvc) {
let editorSectionOffset = 0; let editorSectionOffset = 0;
let previewSectionOffset = 0; let previewSectionOffset = 0;
let tocSectionOffset = 0; let tocSectionOffset = 0;
let sectionDesc = editorSvc.sectionDescList[0]; let sectionDesc = editorSvc.previewCtx.sectionDescList[0];
let nextSectionDesc; let nextSectionDesc;
let i = 1; let i = 1;
for (; i < editorSvc.sectionDescList.length; i += 1) { for (; i < editorSvc.previewCtx.sectionDescList.length; i += 1) {
nextSectionDesc = editorSvc.sectionDescList[i]; nextSectionDesc = editorSvc.previewCtx.sectionDescList[i];
// Measure editor section // Measure editor section
let newEditorSectionOffset = nextSectionDesc.editorElt let newEditorSectionOffset = nextSectionDesc.editorElt
@ -84,7 +85,7 @@ function measureSectionDimensions(editorSvc) {
} }
// Last section // Last section
sectionDesc = editorSvc.sectionDescList[i - 1]; sectionDesc = editorSvc.previewCtx.sectionDescList[i - 1];
if (sectionDesc) { if (sectionDesc) {
sectionDesc.editorDimension = new SectionDimension( sectionDesc.editorDimension = new SectionDimension(
editorSectionOffset, editorSvc.editorElt.scrollHeight); editorSectionOffset, editorSvc.editorElt.scrollHeight);

View File

@ -7,6 +7,7 @@ import providerRegistry from './providers/providerRegistry';
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider'; import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
import './providers/googleDriveWorkspaceProvider'; import './providers/googleDriveWorkspaceProvider';
import './providers/couchdbWorkspaceProvider'; import './providers/couchdbWorkspaceProvider';
import tempFileSvc from './tempFileSvc';
const inactivityThreshold = 3 * 1000; // 3 sec const inactivityThreshold = 3 * 1000; // 3 sec
const restartSyncAfter = 30 * 1000; // 30 sec const restartSyncAfter = 30 * 1000; // 30 sec
@ -169,25 +170,16 @@ function createSyncLocation(syncLocation) {
} }
class SyncContext { class SyncContext {
constructor() { restart = false;
this.restart = false; attempted = {};
this.synced = {};
}
}
class FileSyncContext {
constructor() {
this.downloaded = {};
this.errors = {};
}
} }
/** /**
* Sync one file with all its locations. * Sync one file with all its locations.
*/ */
function syncFile(fileId, syncContext = new SyncContext()) { function syncFile(fileId, syncContext = new SyncContext()) {
const fileSyncContext = new FileSyncContext(); syncContext.attempted[`${fileId}/content`] = true;
syncContext.synced[`${fileId}/content`] = true;
return localDbSvc.loadSyncedContent(fileId) return localDbSvc.loadSyncedContent(fileId)
.then(() => localDbSvc.loadItem(`${fileId}/content`) .then(() => localDbSvc.loadItem(`${fileId}/content`)
.catch(() => {})) // Item may not exist if content has not been downloaded yet .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 getSyncedContent = () => store.state.syncedContent.itemMap[`${fileId}/syncedContent`];
const getSyncHistoryItem = syncLocationId => getSyncedContent().syncHistory[syncLocationId]; const getSyncHistoryItem = syncLocationId => getSyncedContent().syncHistory[syncLocationId];
const isLocationSynced = (syncLocation) => { const isTempFile = () => {
const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
return syncHistoryItem && syncHistoryItem[LAST_SENT] === getContent().hash;
};
const isWelcomeFile = () => {
if (store.getters['data/syncDataByItemId'][`${fileId}/content`]) { 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; return false;
} }
const file = getFile(); const file = getFile();
@ -212,12 +199,29 @@ function syncFile(fileId, syncContext = new SyncContext()) {
if (!file || !content) { if (!file || !content) {
return false; 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 welcomeFileHashes = store.getters['data/localSettings'].welcomeFileHashes;
const hash = utils.hash(content.text); const hash = utils.hash(content.text);
const hasDiscussions = Object.keys(content.discussions).length; const hasDiscussions = Object.keys(content.discussions).length;
return file.name === 'Welcome file' && welcomeFileHashes[hash] && !hasDiscussions; return file.name === 'Welcome file' && welcomeFileHashes[hash] && !hasDiscussions;
}; };
if (isTempFile()) {
return null;
}
const attemptedLocations = {};
const syncOneContentLocation = () => { const syncOneContentLocation = () => {
const syncLocations = [ const syncLocations = [
...store.getters['syncLocation/groupedByFileId'][fileId] || [], ...store.getters['syncLocation/groupedByFileId'][fileId] || [],
@ -229,20 +233,17 @@ function syncFile(fileId, syncContext = new SyncContext()) {
syncLocations.some((syncLocation) => { syncLocations.some((syncLocation) => {
const provider = providerRegistry.providers[syncLocation.providerId]; const provider = providerRegistry.providers[syncLocation.providerId];
if ( if (
// Skip if it previously threw an error // Skip if it has been attempted already
!fileSyncContext.errors[syncLocation.id] && !attemptedLocations[syncLocation.id] &&
// Skip if it has previously been downloaded and has not changed since then // Skip temp file
(!fileSyncContext.downloaded[syncLocation.id] || !isLocationSynced(syncLocation)) && !isTempFile()
// Skip welcome file if not synchronized explicitly
(syncLocations.length > 1 || !isWelcomeFile())
) { ) {
attemptedLocations[syncLocation.id] = true;
const token = provider && provider.getToken(syncLocation); const token = provider && provider.getToken(syncLocation);
result = token && store.dispatch('queue/doWithLocation', { result = token && store.dispatch('queue/doWithLocation', {
location: syncLocation, location: syncLocation,
promise: provider.downloadContent(token, syncLocation) promise: provider.downloadContent(token, syncLocation)
.then((serverContent = null) => { .then((serverContent = null) => {
fileSyncContext.downloaded[syncLocation.id] = true;
const syncedContent = getSyncedContent(); const syncedContent = getSyncedContent();
const syncHistoryItem = getSyncHistoryItem(syncLocation.id); const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
let mergedContent = (() => { let mergedContent = (() => {
@ -275,7 +276,6 @@ function syncFile(fileId, syncContext = new SyncContext()) {
})(); })();
if (!mergedContent) { if (!mergedContent) {
fileSyncContext.errors[syncLocation.id] = true;
return null; return null;
} }
@ -370,7 +370,6 @@ function syncFile(fileId, syncContext = new SyncContext()) {
} }
console.error(err); // eslint-disable-line no-console console.error(err); // eslint-disable-line no-console
store.dispatch('notification/error', err); store.dispatch('notification/error', err);
fileSyncContext.errors[syncLocation.id] = true;
}), }),
}) })
.then(() => syncOneContentLocation()); .then(() => syncOneContentLocation());
@ -584,7 +583,7 @@ function syncWorkspace() {
const syncData = store.getters['data/syncDataByItemId'][contentId]; const syncData = store.getters['data/syncDataByItemId'][contentId];
if ( if (
// Sync if content syncing was not attempted yet // 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 // And if syncData does not exist or if content hash and syncData hash are inconsistent
(!syncData || syncData.hash !== hash) (!syncData || syncData.hash !== hash)
) { ) {
@ -643,6 +642,11 @@ function syncWorkspace() {
* Enqueue a sync task, if possible. * Enqueue a sync task, if possible.
*/ */
function requestSync() { function requestSync() {
// No sync in light mode
if (store.state.light) {
return;
}
store.dispatch('queue/enqueueSyncRequest', () => new Promise((resolve, reject) => { store.dispatch('queue/enqueueSyncRequest', () => new Promise((resolve, reject) => {
let intervalId; let intervalId;
const attempt = () => { const attempt = () => {
@ -734,7 +738,9 @@ export default {
return actionProvider && actionProvider.performAction && actionProvider.performAction() return actionProvider && actionProvider.performAction && actionProvider.performAction()
.then(newSyncLocation => newSyncLocation && this.createSyncLocation(newSyncLocation)); .then(newSyncLocation => newSyncLocation && this.createSyncLocation(newSyncLocation));
}) })
.then(() => tempFileSvc.init())
.then(() => { .then(() => {
if (!store.state.light) {
// Sync periodically // Sync periodically
utils.setInterval(() => { utils.setInterval(() => {
if (isSyncPossible() if (isSyncPossible()
@ -753,6 +759,7 @@ export default {
localDbSvc.unloadContents(); localDbSvc.unloadContents();
} }
}, 5000); }, 5000);
}
}); });
}, },
isSyncPossible, isSyncPossible,

View 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);
});
},
};

View File

@ -35,9 +35,9 @@ export default {
this.queryParams = params; this.queryParams = params;
const serializedParams = Object.entries(this.queryParams).map(([key, value]) => const serializedParams = Object.entries(this.queryParams).map(([key, value]) =>
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&'); `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&');
const hash = serializedParams && `#${serializedParams}`; const hash = `#${serializedParams}`;
if (location.hash !== hash) { if (location.hash !== hash) {
location.hash = hash; location.replace(hash);
} }
}, },
types: [ types: [
@ -185,6 +185,10 @@ export default {
} }
return result; return result;
}, },
getHostname(url) {
urlParser.href = url;
return urlParser.hostname;
},
createHiddenIframe(url) { createHiddenIframe(url) {
const iframeElt = document.createElement('iframe'); const iframeElt = document.createElement('iframe');
iframeElt.style.position = 'absolute'; iframeElt.style.position = 'absolute';

View File

@ -37,6 +37,14 @@ module.getters = {
} }
return state.itemMap[`${rootGetters['file/current'].id}/content`] || empty(); 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), currentProperties: (state, getters) => utils.computeProperties(getters.current.properties),
isCurrentEditable: (state, getters, rootState, rootGetters) => isCurrentEditable: (state, getters, rootState, rootGetters) =>
!state.revisionContent && !state.revisionContent &&

View File

@ -170,6 +170,7 @@ export default {
...getters.templates, ...getters.templates,
...additionalTemplates, ...additionalTemplates,
}), }),
lastCreated: getter('lastCreated'),
lastOpened: getter('lastOpened'), lastOpened: getter('lastOpened'),
lastOpenedIds: (state, getters, rootState) => { lastOpenedIds: (state, getters, rootState) => {
const lastOpened = { const lastOpened = {
@ -261,21 +262,18 @@ export default {
}); });
commit('setItem', itemTemplate('templates', dataToCommit)); commit('setItem', itemTemplate('templates', dataToCommit));
}, },
setLastCreated: setter('lastCreated'),
setLastOpenedId: ({ getters, commit, dispatch, rootState }, fileId) => { setLastOpenedId: ({ getters, commit, dispatch, rootState }, fileId) => {
const lastOpened = { ...getters.lastOpened }; const lastOpened = { ...getters.lastOpened };
lastOpened[fileId] = Date.now(); lastOpened[fileId] = Date.now();
commit('setItem', itemTemplate('lastOpened', lastOpened)); // Remove entries that don't exist anymore
dispatch('cleanLastOpenedId'); const cleanedLastOpened = {};
}, Object.entries(lastOpened).forEach(([id, value]) => {
cleanLastOpenedId: ({ getters, commit, rootState }) => { if (rootState.file.itemMap[id]) {
const lastOpened = {}; cleanedLastOpened[id] = value;
const oldLastOpened = getters.lastOpened;
Object.entries(oldLastOpened).forEach(([fileId, date]) => {
if (rootState.file.itemMap[fileId]) {
lastOpened[fileId] = date;
} }
}); });
commit('setItem', itemTemplate('lastOpened', lastOpened)); commit('setItem', itemTemplate('lastOpened', cleanedLastOpened));
}, },
setSyncData: setter('syncData'), setSyncData: setter('syncData'),
patchSyncData: patcher('syncData'), patchSyncData: patcher('syncData'),

View File

@ -69,13 +69,29 @@ export default {
}, },
getters: { getters: {
nodeStructure: (state, getters, rootState, rootGetters) => { nodeStructure: (state, getters, rootState, rootGetters) => {
const rootNode = new Node(emptyFolder(), [], true, true);
// Create Trash node
const trashFolderNode = new Node(emptyFolder(), [], true); const trashFolderNode = new Node(emptyFolder(), [], true);
trashFolderNode.item.id = 'trash'; trashFolderNode.item.id = 'trash';
trashFolderNode.item.name = 'Trash'; trashFolderNode.item.name = 'Trash';
trashFolderNode.isTrash = true;
trashFolderNode.noDrag = 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 = { const nodeMap = {
trash: trashFolderNode, trash: trashFolderNode,
temp: tempFolderNode,
}; };
rootGetters['folder/items'].forEach((item) => { rootGetters['folder/items'].forEach((item) => {
nodeMap[item.id] = new Node(item, [], true); nodeMap[item.id] = new Node(item, [], true);
@ -89,11 +105,12 @@ export default {
]; ];
nodeMap[item.id] = new Node(item, locations); nodeMap[item.id] = new Node(item, locations);
}); });
const rootNode = new Node(emptyFolder(), [], true, true);
// Build the tree
Object.entries(nodeMap).forEach(([, node]) => { Object.entries(nodeMap).forEach(([, node]) => {
let parentNode = nodeMap[node.item.parentId]; let parentNode = nodeMap[node.item.parentId];
if (!parentNode || !parentNode.isFolder) { if (!parentNode || !parentNode.isFolder) {
if (node.isTrash) { if (node.isTrash || node.isTemp) {
return; return;
} }
parentNode = rootNode; parentNode = rootNode;
@ -103,12 +120,20 @@ export default {
} else { } else {
parentNode.files.push(node); parentNode.files.push(node);
} }
node.parentNode = parentNode;
}); });
rootNode.sortChildren(); rootNode.sortChildren();
// Add Trash and Temp nodes
rootNode.folders.unshift(tempFolderNode);
tempFolderNode.files.forEach((node) => {
node.noDrop = true;
});
if (trashFolderNode.files.length) { if (trashFolderNode.files.length) {
rootNode.folders.unshift(trashFolderNode); 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); rootNode.files.push(fakeFileNode);
return { return {
nodeMap, nodeMap,
@ -169,7 +194,9 @@ export default {
}, },
newItem({ getters, commit, dispatch }, isFolder) { newItem({ getters, commit, dispatch }, isFolder) {
let parentId = getters.selectedNodeFolder.item.id; 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; parentId = null;
} }
dispatch('openNode', parentId); dispatch('openNode', parentId);
@ -186,34 +213,50 @@ export default {
if (selectedNode.isTrash || selectedNode.item.parentId === 'trash') { if (selectedNode.isTrash || selectedNode.item.parentId === 'trash') {
return dispatch('modal/trashDeletion', null, { root: true }); return dispatch('modal/trashDeletion', null, { root: true });
} }
return dispatch(selectedNode.isFolder
? 'modal/folderDeletion' // See if we have a dialog to show
: 'modal/fileDeletion', let modalAction;
selectedNode.item, let moveToTrash = true;
{ root: 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(() => { .then(() => {
const deleteFile = (id) => {
if (moveToTrash) {
commit('file/patchItem', {
id,
parentId: 'trash',
}, { root: true });
} else {
dispatch('deleteFile', id, { root: true });
}
};
if (selectedNode === getters.selectedNode) { if (selectedNode === getters.selectedNode) {
const currentFileId = rootGetters['file/current'].id; const currentFileId = rootGetters['file/current'].id;
let doClose = selectedNode.item.id === currentFileId; let doClose = selectedNode.item.id === currentFileId;
if (selectedNode.isFolder) { if (selectedNode.isFolder) {
const recursiveMoveToTrash = (folderNode) => { const recursiveDelete = (folderNode) => {
folderNode.folders.forEach(recursiveMoveToTrash); folderNode.folders.forEach(recursiveDelete);
folderNode.files.forEach((fileNode) => { folderNode.files.forEach((fileNode) => {
commit('file/patchItem', {
id: fileNode.item.id,
parentId: 'trash',
}, { root: true });
doClose = doClose || fileNode.item.id === currentFileId; doClose = doClose || fileNode.item.id === currentFileId;
deleteFile(fileNode.item.id);
}); });
commit('folder/deleteItem', folderNode.item.id, { root: true }); commit('folder/deleteItem', folderNode.item.id, { root: true });
}; };
recursiveMoveToTrash(selectedNode); recursiveDelete(selectedNode);
} else { } else {
commit('file/patchItem', { deleteFile(selectedNode.item.id);
id: selectedNode.item.id,
parentId: 'trash',
}, { root: true });
} }
if (doClose) { if (doClose) {
// Close the current file by opening the last opened, not deleted one // Close the current file by opening the last opened, not deleted one

View File

@ -11,6 +11,7 @@ module.state = {
module.getters = { module.getters = {
...module.getters, ...module.getters,
current: state => state.itemMap[state.currentId] || empty(), current: state => state.itemMap[state.currentId] || empty(),
isCurrentTemp: (state, getters) => getters.current.parentId === 'temp',
lastOpened: (state, getters, rootState, rootGetters) => lastOpened: (state, getters, rootState, rootGetters) =>
state.itemMap[rootGetters['data/lastOpenedIds'][0]] || getters.items[0] || empty(), state.itemMap[rootGetters['data/lastOpenedIds'][0]] || getters.items[0] || empty(),
}; };

View File

@ -47,6 +47,7 @@ const store = new Vuex.Store({
workspace, workspace,
}, },
state: { state: {
light: false,
offline: false, offline: false,
lastOfflineCheck: 0, lastOfflineCheck: 0,
minuteCounter: 0, minuteCounter: 0,
@ -64,6 +65,9 @@ const store = new Vuex.Store({
}, },
}, },
mutations: { mutations: {
setLight: (state, value) => {
state.light = value;
},
setOffline: (state, value) => { setOffline: (state, value) => {
state.offline = value; state.offline = value;
}, },
@ -109,16 +113,14 @@ const store = new Vuex.Store({
return Promise.resolve(state.file.itemMap[id]); return Promise.resolve(state.file.itemMap[id]);
}, },
deleteFile({ getters, commit }, fileId) { 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('file/deleteItem', fileId);
commit('content/deleteItem', `${fileId}/content`); commit('content/deleteItem', `${fileId}/content`);
commit('syncedContent/deleteItem', `${fileId}/syncedContent`); commit('syncedContent/deleteItem', `${fileId}/syncedContent`);
commit('contentState/deleteItem', `${fileId}/contentState`); 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, strict: debug,

View File

@ -1,7 +1,7 @@
const minPadding = 20; const minPadding = 20;
const editorTopPadding = 10; const editorTopPadding = 10;
const navigationBarEditButtonsWidth = (36 * 14) + 8; // 14 buttons + 1 spacer const navigationBarEditButtonsWidth = (36 * 14) + 8; // 14 buttons + 1 spacer
const navigationBarLeftButtonWidth = 38 + 4 + 15; const navigationBarLeftButtonWidth = 38 + 4 + 12;
const navigationBarRightButtonWidth = 38 + 8; const navigationBarRightButtonWidth = 38 + 8;
const navigationBarSpinnerWidth = 24 + 8 + 5; // 5 for left margin const navigationBarSpinnerWidth = 24 + 8 + 5; // 5 for left margin
const navigationBarLocationWidth = 20; const navigationBarLocationWidth = 20;
@ -21,14 +21,18 @@ const constants = {
}; };
function computeStyles(state, getters, layoutSettings = getters['data/layoutSettings'], styles = { 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, showStatusBar: layoutSettings.showStatusBar,
showEditor: layoutSettings.showEditor, showEditor: layoutSettings.showEditor,
showSidePreview: layoutSettings.showSidePreview && layoutSettings.showEditor, showSidePreview: layoutSettings.showSidePreview && layoutSettings.showEditor,
showPreview: layoutSettings.showSidePreview || !layoutSettings.showEditor, showPreview: layoutSettings.showSidePreview || !layoutSettings.showEditor,
showSideBar: layoutSettings.showSideBar, showSideBar: layoutSettings.showSideBar && !state.light,
showExplorer: layoutSettings.showExplorer, showExplorer: layoutSettings.showExplorer && !state.light,
layoutOverflow: false, layoutOverflow: false,
hideLocations: state.light,
}) { }) {
styles.innerHeight = state.layout.bodyHeight; styles.innerHeight = state.layout.bodyHeight;
if (styles.showNavigationBar) { if (styles.showNavigationBar) {
@ -52,7 +56,8 @@ function computeStyles(state, getters, layoutSettings = getters['data/layoutSett
} }
let doublePanelWidth = styles.innerWidth - constants.buttonBarWidth; 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) { if (showGutter) {
doublePanelWidth -= constants.gutterWidth; doublePanelWidth -= constants.gutterWidth;
} }

View File

@ -47,16 +47,21 @@ export default {
throw err; throw err;
}); });
}, },
fileDeletion: ({ dispatch }, item) => dispatch('open', { folderDeletion: ({ dispatch }, item) => dispatch('open', {
content: `<p>You are about to delete the file <b>${item.name}</b>. Are you sure?</p>`, 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', resolveText: 'Yes, delete',
rejectText: 'No', rejectText: 'No',
}), }),
folderDeletion: ({ dispatch }, item) => dispatch('open', { tempFileDeletion: ({ 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>`, content: `<p>You are about to permanently delete the temporary file <b>${item.name}</b>. Are you sure?</p>`,
resolveText: 'Yes, delete', resolveText: 'Yes, delete',
rejectText: 'No', 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', { discussionDeletion: ({ dispatch }) => dispatch('open', {
content: '<p>You are about to delete a discussion. Are you sure?</p>', content: '<p>You are about to delete a discussion. Are you sure?</p>',
resolveText: 'Yes, delete', resolveText: 'Yes, delete',

View File

@ -2,31 +2,12 @@ import Vue from 'vue';
import utils from '../services/utils'; import utils from '../services/utils';
export default (empty, simpleHash = false) => { 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({ const hashFunc = simpleHash ? Date.now : item => utils.hash(utils.serializeObject({
...item, ...item,
hash: undefined, 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 { return {
namespaced: true, namespaced: true,
state: { state: {
@ -36,8 +17,23 @@ export default (empty, simpleHash = false) => {
items: state => Object.entries(state.itemMap).map(([, item]) => item), items: state => Object.entries(state.itemMap).map(([, item]) => item),
}, },
mutations: { mutations: {
setItem, setItem(state, value) {
patchItem, 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) { deleteItem(state, id) {
Vue.delete(state.itemMap, 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