Temporary folder

This commit is contained in:
benweet 2018-03-12 00:45:54 +00:00
parent 906e672e49
commit 623c265adc
36 changed files with 598 additions and 346 deletions

View File

@ -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',
]),

View File

@ -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">
<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);

View File

@ -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>

View File

@ -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',
]),

View File

@ -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', [
@ -178,9 +182,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;
@ -255,6 +262,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 +298,7 @@ $button-size: 36px;
.navigation-bar__title {
margin: 0 4px;
font-size: 22px;
font-size: 21px;
.layout--revision & {
position: absolute;

View File

@ -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', [
computed: {
...mapGetters('file', [
'isCurrentTemp',
]),
...mapGetters('layout', [
'styles',
]),
},
methods: {
...mapActions('data', [
'toggleEditor',

View File

@ -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) => {

View File

@ -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);
}

View File

@ -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());
}
}

View File

@ -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();
});

View File

@ -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: {

View File

@ -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;

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

View File

@ -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

View File

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

View File

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

View File

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

View File

@ -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;

View File

@ -54,15 +54,15 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
converter: null,
parsingCtx: null,
conversionCtx: null,
sectionList: null,
previewCtx: {
sectionDescList: [],
sectionDescMeasuredList: null,
sectionDescWithDiffsList: null,
},
previewCtxMeasured: null,
previewCtxWithDiffs: null,
sectionList: 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');

View File

@ -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;
}

View File

@ -5,6 +5,7 @@ import welcomeFile from '../data/welcomeFile.md';
const dbVersion = 1;
const dbStoreName = 'objects';
const fileIdToOpen = utils.queryParams.fileId;
const exportWorkspace = utils.queryParams.exportWorkspace;
const resetApp = utils.queryParams.reset;
const deleteMarkerMaxAge = 1000;
@ -178,6 +179,10 @@ const localDbSvc = {
// Sync local DB periodically
utils.setInterval(() => localDbSvc.sync(), 1000);
if (fileIdToOpen) {
store.commit('file/setCurrentId', fileIdToOpen);
}
// watch current file changing
store.watch(
() => store.getters['file/current'].id,
@ -216,6 +221,11 @@ const localDbSvc = {
// Open the gutter if file contains discussions
store.commit('discussion/setCurrentDiscussionId',
store.getters['discussion/nextDiscussionId']);
// Add the fileId to the queryParams
utils.setQueryParams({
...utils.queryParams,
fileId: currentFile.id,
});
},
(err) => {
// Failure (content is not available), go back to previous file

View File

@ -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,

View File

@ -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;
editorSvc.$on('previewCtxMeasured', (previewCtxMeasured) => {
if (previewCtxMeasured) {
sectionDescList = previewCtxMeasured.sectionDescList;
forceScrollSync();
}
});

View File

@ -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 = () => {

View File

@ -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);

View File

@ -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,7 +738,9 @@ export default {
return actionProvider && actionProvider.performAction && actionProvider.performAction()
.then(newSyncLocation => newSyncLocation && this.createSyncLocation(newSyncLocation));
})
.then(() => tempFileSvc.init())
.then(() => {
if (!store.state.light) {
// Sync periodically
utils.setInterval(() => {
if (isSyncPossible()
@ -753,6 +759,7 @@ export default {
localDbSvc.unloadContents();
}
}, 5000);
}
});
},
isSyncPossible,

113
src/services/tempFileSvc.js Normal file
View File

@ -0,0 +1,113 @@
import cledit from './cledit';
import store from '../store';
import utils from './utils';
import editorSvc from './editorSvc';
const origin = utils.queryParams.origin;
const existingFileId = utils.queryParams.fileId;
const fileName = utils.queryParams.fileName;
const contentText = utils.queryParams.contentText;
const contentProperties = utils.queryParams.contentProperties;
export default {
close() {
if (origin && window.parent) {
window.parent.postMessage({ type: 'close' }, origin);
}
},
init() {
if (!origin || !window.parent) {
return Promise.resolve();
}
store.commit('setLight', true);
return Promise.resolve()
.then(() => {
const file = store.state.file.itemMap[existingFileId];
if (file) {
// If file exists, check that the origin site has created it
const fileCreation = store.getters['data/fileCreations'][file.id];
if (fileCreation && fileCreation.origin === origin) {
return file;
}
}
// Create a new temp file
return store.dispatch('createFile', {
name: fileName,
text: contentText,
properties: contentProperties,
parentId: 'temp',
});
})
.then((file) => {
const fileItemMap = store.state.file.itemMap;
// Sanitize file creations
const fileCreations = {};
Object.entries(store.getters['data/fileCreations']).forEach(([id, fileCreation]) => {
if (fileItemMap[id]) {
fileCreations[id] = fileCreation;
}
});
// Track file creation from the origin site
fileCreations[file.id] = {
created: Date.now(),
origin,
};
// List temp files
const tempFileCreations = [];
Object.entries(fileCreations).forEach(([id, fileCreation]) => {
if (fileItemMap[id].parentId === 'temp') {
tempFileCreations.push({
id,
created: fileCreation.created,
});
}
});
// Keep only the last 10 temp files
tempFileCreations
.sort((fileCreation1, fileCreation2) => fileCreation2.created - fileCreation1.created)
.splice(10)
.forEach((fileCreation) => {
delete fileCreations[fileCreation.id];
store.dispatch('deleteFile', fileCreation.id);
});
// Store file creations and open the file
store.dispatch('data/setFileCreations', fileCreations);
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 (editorSvc.previewCtx.html != null) {
const content = store.getters['content/current'];
const properties = utils.computeProperties(content.properties);
window.parent.postMessage({
type: 'fileChange',
file: {
id: file.id,
name: currentFile.name,
content: {
text: content.text,
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;
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';

View File

@ -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 &&

View File

@ -170,6 +170,7 @@ export default {
...getters.templates,
...additionalTemplates,
}),
fileCreations: getter('fileCreations'),
lastOpened: getter('lastOpened'),
lastOpenedIds: (state, getters, rootState) => {
const lastOpened = {
@ -261,21 +262,18 @@ export default {
});
commit('setItem', itemTemplate('templates', dataToCommit));
},
setFileCreations: setter('fileCreations'),
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'),

View File

@ -72,10 +72,17 @@ export default {
const trashFolderNode = new Node(emptyFolder(), [], true);
trashFolderNode.item.id = 'trash';
trashFolderNode.item.name = 'Trash';
trashFolderNode.isTrash = true;
trashFolderNode.noDrag = true;
trashFolderNode.isTrash = true;
const tempFolderNode = new Node(emptyFolder(), [], true);
tempFolderNode.item.id = 'temp';
tempFolderNode.item.name = 'Temp';
tempFolderNode.noDrag = true;
tempFolderNode.noDrop = true;
tempFolderNode.isTemp = true;
const nodeMap = {
trash: trashFolderNode,
temp: tempFolderNode,
};
rootGetters['folder/items'].forEach((item) => {
nodeMap[item.id] = new Node(item, [], true);
@ -93,7 +100,7 @@ export default {
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;
@ -105,6 +112,10 @@ export default {
}
});
rootNode.sortChildren();
rootNode.folders.unshift(tempFolderNode);
tempFolderNode.files.forEach((node) => {
node.noDrop = true;
});
if (trashFolderNode.files.length) {
rootNode.folders.unshift(trashFolderNode);
}
@ -169,7 +180,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 +199,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

View File

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

View File

@ -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,

View File

@ -21,14 +21,17 @@ const constants = {
};
function computeStyles(state, getters, layoutSettings = getters['data/layoutSettings'], styles = {
showNavigationBar: !layoutSettings.showEditor || layoutSettings.showNavigationBar,
showNavigationBar: layoutSettings.showNavigationBar
|| !layoutSettings.showEditor
|| state.content.revisionContent,
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 +55,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;
}

View File

@ -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',

View File

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