Store things in localDb
This commit is contained in:
parent
97172e28b3
commit
02a4696b40
@ -5,7 +5,7 @@
|
|||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="navigation-bar__title navigation-bar__title--text text-input" v-bind:style="{maxWidth: titleMaxWidth + 'px'}"></div>
|
<div class="navigation-bar__title navigation-bar__title--text text-input" v-bind:style="{maxWidth: titleMaxWidth + 'px'}"></div>
|
||||||
<input class="navigation-bar__title navigation-bar__title--input text-input" v-bind:class="{'navigation-bar__title--focused': titleFocused, 'navigation-bar__title--scrolling': titleScrolling}" v-bind:style="{maxWidth: titleMaxWidth + 'px'}" @focus="editTitle(true)" @blur="editTitle(false)" @keyup.enter="submitTitle()" @keyup.esc="submitTitle(true)" v-model.lazy.trim="title">
|
<input class="navigation-bar__title navigation-bar__title--input text-input" v-bind:class="{'navigation-bar__title--focus': titleFocus, 'navigation-bar__title--scrolling': titleScrolling}" v-bind:style="{maxWidth: titleMaxWidth + 'px'}" @focus="editTitle(true)" @blur="editTitle(false)" @keyup.enter="submitTitle()" @keyup.esc="submitTitle(true)" v-model.lazy.trim="title">
|
||||||
</div>
|
</div>
|
||||||
<div class="navigation-bar__inner navigation-bar__inner--left">
|
<div class="navigation-bar__inner navigation-bar__inner--left">
|
||||||
<button class="navigation-bar__button button" @click="pagedownClick('bold')">
|
<button class="navigation-bar__button button" @click="pagedownClick('bold')">
|
||||||
@ -55,7 +55,7 @@ import animationSvc from '../services/animationSvc';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
data: () => ({
|
data: () => ({
|
||||||
titleFocused: false,
|
titleFocus: false,
|
||||||
titleHover: false,
|
titleHover: false,
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
@ -65,14 +65,14 @@ export default {
|
|||||||
}),
|
}),
|
||||||
title: {
|
title: {
|
||||||
get() {
|
get() {
|
||||||
return this.$store.state.files.currentFile.name;
|
return this.$store.getters['files/current'].name;
|
||||||
},
|
},
|
||||||
set(value) {
|
set(name) {
|
||||||
this.$store.commit('files/setCurrentFileName', value);
|
this.$store.dispatch('files/patchCurrent', { name });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
titleScrolling() {
|
titleScrolling() {
|
||||||
const result = this.titleHover && !this.titleFocused;
|
const result = this.titleHover && !this.titleFocus;
|
||||||
if (this.titleInputElt) {
|
if (this.titleInputElt) {
|
||||||
if (result) {
|
if (result) {
|
||||||
const scrollLeft = this.titleInputElt.scrollWidth - this.titleInputElt.offsetWidth;
|
const scrollLeft = this.titleInputElt.scrollWidth - this.titleInputElt.offsetWidth;
|
||||||
@ -95,7 +95,7 @@ export default {
|
|||||||
editorSvc.pagedownEditor.uiManager.doClick(name);
|
editorSvc.pagedownEditor.uiManager.doClick(name);
|
||||||
},
|
},
|
||||||
editTitle(toggle) {
|
editTitle(toggle) {
|
||||||
this.titleFocused = toggle;
|
this.titleFocus = toggle;
|
||||||
if (toggle) {
|
if (toggle) {
|
||||||
this.titleInputElt.setSelectionRange(0, this.titleInputElt.value.length);
|
this.titleInputElt.setSelectionRange(0, this.titleInputElt.value.length);
|
||||||
}
|
}
|
||||||
@ -197,7 +197,7 @@ export default {
|
|||||||
.navigation-bar__title--input {
|
.navigation-bar__title--input {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&.navigation-bar__title--focused {
|
&.navigation-bar__title--focus {
|
||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
7
src/data/emptyContent.js
Normal file
7
src/data/emptyContent.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default () => ({
|
||||||
|
state: {},
|
||||||
|
text: '\n',
|
||||||
|
properties: {},
|
||||||
|
discussions: {},
|
||||||
|
comments: {},
|
||||||
|
});
|
5
src/data/emptyFile.js
Normal file
5
src/data/emptyFile.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default () => ({
|
||||||
|
name: '',
|
||||||
|
folderId: null,
|
||||||
|
contentId: null,
|
||||||
|
});
|
@ -11,8 +11,8 @@ let markerIdxMap;
|
|||||||
let previousPatchableText;
|
let previousPatchableText;
|
||||||
let currentPatchableText;
|
let currentPatchableText;
|
||||||
let discussionMarkers;
|
let discussionMarkers;
|
||||||
let content;
|
|
||||||
let isChangePatch;
|
let isChangePatch;
|
||||||
|
let contentId;
|
||||||
|
|
||||||
function getDiscussionMarkers(discussion, discussionId, onMarker) {
|
function getDiscussionMarkers(discussion, discussionId, onMarker) {
|
||||||
function getMarker(offsetName) {
|
function getMarker(offsetName) {
|
||||||
@ -35,6 +35,7 @@ function getDiscussionMarkers(discussion, discussionId, onMarker) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function syncDiscussionMarkers() {
|
function syncDiscussionMarkers() {
|
||||||
|
const content = store.getters['contents/current'];
|
||||||
Object.keys(discussionMarkers)
|
Object.keys(discussionMarkers)
|
||||||
.forEach((markerKey) => {
|
.forEach((markerKey) => {
|
||||||
const marker = discussionMarkers[markerKey];
|
const marker = discussionMarkers[markerKey];
|
||||||
@ -99,8 +100,9 @@ export default {
|
|||||||
markerIdxMap = Object.create(null);
|
markerIdxMap = Object.create(null);
|
||||||
discussionMarkers = {};
|
discussionMarkers = {};
|
||||||
clEditor.on('contentChanged', (text) => {
|
clEditor.on('contentChanged', (text) => {
|
||||||
store.commit('files/setCurrentFileContentText', text);
|
store.dispatch('contents/patchCurrent', { text });
|
||||||
syncDiscussionMarkers();
|
syncDiscussionMarkers();
|
||||||
|
const content = store.getters('contents/current');
|
||||||
if (!isChangePatch) {
|
if (!isChangePatch) {
|
||||||
previousPatchableText = currentPatchableText;
|
previousPatchableText = currentPatchableText;
|
||||||
currentPatchableText = clDiffUtils.makePatchableText(content, markerKeys, markerIdxMap);
|
currentPatchableText = clDiffUtils.makePatchableText(content, markerKeys, markerIdxMap);
|
||||||
@ -121,11 +123,12 @@ export default {
|
|||||||
clEditor.addMarker(newDiscussionMarker1);
|
clEditor.addMarker(newDiscussionMarker1);
|
||||||
},
|
},
|
||||||
initClEditor(opts, reinit) {
|
initClEditor(opts, reinit) {
|
||||||
if (store.state.files.currentFile.content) {
|
const content = store.getters('contents/current');
|
||||||
|
if (content) {
|
||||||
const options = Object.assign({}, opts);
|
const options = Object.assign({}, opts);
|
||||||
|
|
||||||
if (content !== store.state.files.currentFile.content) {
|
if (contentId !== content.id) {
|
||||||
content = store.state.files.currentFile.content;
|
contentId = content.id;
|
||||||
currentPatchableText = clDiffUtils.makePatchableText(content, markerKeys, markerIdxMap);
|
currentPatchableText = clDiffUtils.makePatchableText(content, markerKeys, markerIdxMap);
|
||||||
previousPatchableText = currentPatchableText;
|
previousPatchableText = currentPatchableText;
|
||||||
syncDiscussionMarkers();
|
syncDiscussionMarkers();
|
||||||
@ -153,6 +156,7 @@ export default {
|
|||||||
this.lastExternalChange = Date.now();
|
this.lastExternalChange = Date.now();
|
||||||
}
|
}
|
||||||
syncDiscussionMarkers();
|
syncDiscussionMarkers();
|
||||||
|
const content = store.getters('contents/current');
|
||||||
return clEditor.setContent(content.text, isExternal);
|
return clEditor.setContent(content.text, isExternal);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -29,8 +29,9 @@ const allowDebounce = (action, wait) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const diffMatchPatch = new DiffMatchPatch();
|
const diffMatchPatch = new DiffMatchPatch();
|
||||||
|
let lastContentId = null;
|
||||||
let reinitClEditor = true;
|
let reinitClEditor = true;
|
||||||
let isPreviewRefreshed = false;
|
let instantPreview = true;
|
||||||
let tokens;
|
let tokens;
|
||||||
const anchorHash = {};
|
const anchorHash = {};
|
||||||
|
|
||||||
@ -171,9 +172,14 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
* 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() {
|
||||||
if (!store.state.files.currentFile.isLoaded) {
|
const contentId = store.getters['contents/current'].id;
|
||||||
|
if (contentId !== lastContentId) {
|
||||||
reinitClEditor = true;
|
reinitClEditor = true;
|
||||||
isPreviewRefreshed = false;
|
instantPreview = true;
|
||||||
|
lastContentId = contentId;
|
||||||
|
}
|
||||||
|
if (!contentId) {
|
||||||
|
// This means the content is not loaded yet
|
||||||
editorEngineSvc.clEditor.toggleEditable(false);
|
editorEngineSvc.clEditor.toggleEditable(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -336,8 +342,8 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
* Make the diff between editor's markdown and preview's html.
|
* Make the diff between editor's markdown and preview's html.
|
||||||
*/
|
*/
|
||||||
makeTextToPreviewDiffs: allowDebounce(() => {
|
makeTextToPreviewDiffs: allowDebounce(() => {
|
||||||
if (editorSvc.sectionDescList
|
if (editorSvc.sectionDescList &&
|
||||||
&& editorSvc.sectionDescList !== editorSvc.sectionDescMeasuredList) {
|
editorSvc.sectionDescList !== editorSvc.sectionDescMeasuredList) {
|
||||||
editorSvc.sectionDescList
|
editorSvc.sectionDescList
|
||||||
.forEach((sectionDesc) => {
|
.forEach((sectionDesc) => {
|
||||||
if (!sectionDesc.textToPreviewDiffs) {
|
if (!sectionDesc.textToPreviewDiffs) {
|
||||||
@ -355,19 +361,21 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
*/
|
*/
|
||||||
saveContentState: allowDebounce(() => {
|
saveContentState: allowDebounce(() => {
|
||||||
const scrollPosition = editorSvc.getScrollPosition() ||
|
const scrollPosition = editorSvc.getScrollPosition() ||
|
||||||
store.state.files.currentFile.content.state.scrollPosition;
|
store.getters['contents/current'].state.scrollPosition;
|
||||||
store.commit('files/setCurrentFileContentState', {
|
store.dispatch('contents/patchCurrent', {
|
||||||
|
state: {
|
||||||
selectionStart: editorEngineSvc.clEditor.selectionMgr.selectionStart,
|
selectionStart: editorEngineSvc.clEditor.selectionMgr.selectionStart,
|
||||||
selectionEnd: editorEngineSvc.clEditor.selectionMgr.selectionEnd,
|
selectionEnd: editorEngineSvc.clEditor.selectionMgr.selectionEnd,
|
||||||
scrollPosition,
|
scrollPosition,
|
||||||
}, { root: true });
|
},
|
||||||
|
});
|
||||||
}, 100),
|
}, 100),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restore the scroll position from the current file content state.
|
* Restore the scroll position from the current file content state.
|
||||||
*/
|
*/
|
||||||
restoreScrollPosition() {
|
restoreScrollPosition() {
|
||||||
const scrollPosition = store.state.files.currentFile.content.state.scrollPosition;
|
const scrollPosition = store.getters['contents/current'].state.scrollPosition;
|
||||||
if (scrollPosition && this.sectionDescMeasuredList) {
|
if (scrollPosition && this.sectionDescMeasuredList) {
|
||||||
const objectToScroll = this.getObjectToScroll();
|
const objectToScroll = this.getObjectToScroll();
|
||||||
const sectionDesc = this.sectionDescMeasuredList[scrollPosition.sectionIdx];
|
const sectionDesc = this.sectionDescMeasuredList[scrollPosition.sectionIdx];
|
||||||
@ -388,10 +396,10 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
if (range) {
|
if (range) {
|
||||||
if (
|
if (
|
||||||
/* eslint-disable no-bitwise */
|
/* eslint-disable no-bitwise */
|
||||||
!(editorSvc.previewElt.compareDocumentPosition(range.startContainer)
|
!(editorSvc.previewElt.compareDocumentPosition(range.startContainer) &
|
||||||
& window.Node.DOCUMENT_POSITION_CONTAINED_BY) ||
|
window.Node.DOCUMENT_POSITION_CONTAINED_BY) ||
|
||||||
!(editorSvc.previewElt.compareDocumentPosition(range.endContainer)
|
!(editorSvc.previewElt.compareDocumentPosition(range.endContainer) &
|
||||||
& window.Node.DOCUMENT_POSITION_CONTAINED_BY)
|
window.Node.DOCUMENT_POSITION_CONTAINED_BY)
|
||||||
/* eslint-enable no-bitwise */
|
/* eslint-enable no-bitwise */
|
||||||
) {
|
) {
|
||||||
range = null;
|
range = null;
|
||||||
@ -561,14 +569,14 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
|
|
||||||
const refreshPreview = () => {
|
const refreshPreview = () => {
|
||||||
this.convert();
|
this.convert();
|
||||||
if (!isPreviewRefreshed) {
|
if (instantPreview) {
|
||||||
this.refreshPreview();
|
this.refreshPreview();
|
||||||
this.measureSectionDimensions();
|
this.measureSectionDimensions();
|
||||||
this.restoreScrollPosition();
|
this.restoreScrollPosition();
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => this.refreshPreview(), 10);
|
setTimeout(() => this.refreshPreview(), 10);
|
||||||
}
|
}
|
||||||
isPreviewRefreshed = true;
|
instantPreview = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const debouncedRefreshPreview = debounce(refreshPreview, 20);
|
const debouncedRefreshPreview = debounce(refreshPreview, 20);
|
||||||
@ -579,10 +587,10 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
if (this.sectionList !== newSectionList) {
|
if (this.sectionList !== newSectionList) {
|
||||||
this.sectionList = newSectionList;
|
this.sectionList = newSectionList;
|
||||||
this.$emit('sectionList', this.sectionList);
|
this.$emit('sectionList', this.sectionList);
|
||||||
if (isPreviewRefreshed) {
|
if (instantPreview) {
|
||||||
debouncedRefreshPreview();
|
|
||||||
} else {
|
|
||||||
refreshPreview();
|
refreshPreview();
|
||||||
|
} else {
|
||||||
|
debouncedRefreshPreview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.selectionRange !== newSelectionRange) {
|
if (this.selectionRange !== newSelectionRange) {
|
||||||
@ -710,7 +718,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
// })
|
// })
|
||||||
|
|
||||||
store.watch(
|
store.watch(
|
||||||
state => state.files.currentFile.content.properties,
|
() => store.getters['contents/current'].properties,
|
||||||
(properties) => {
|
(properties) => {
|
||||||
const options = properties && extensionSvc.getOptions(properties, true);
|
const options = properties && extensionSvc.getOptions(properties, true);
|
||||||
if (JSON.stringify(options) !== JSON.stringify(editorSvc.options)) {
|
if (JSON.stringify(options) !== JSON.stringify(editorSvc.options)) {
|
||||||
|
@ -1,286 +1,34 @@
|
|||||||
import 'babel-polyfill';
|
import 'babel-polyfill';
|
||||||
import 'indexeddbshim';
|
import 'indexeddbshim';
|
||||||
import utils from './utils';
|
import debug from 'debug';
|
||||||
|
import store from '../store';
|
||||||
|
|
||||||
|
const dbg = debug('stackedit:localDbSvc');
|
||||||
|
|
||||||
let indexedDB = window.indexedDB;
|
let indexedDB = window.indexedDB;
|
||||||
const localStorage = window.localStorage;
|
const localStorage = window.localStorage;
|
||||||
const dbVersion = 1;
|
const dbVersion = 1;
|
||||||
|
const dbStoreName = 'objects';
|
||||||
|
|
||||||
// Use the shim on Safari or if indexedDB is not available
|
// Use the shim on Safari or when indexedDB is not available
|
||||||
if (window.shimIndexedDB && (!indexedDB || (navigator.userAgent.indexOf('Chrome') === -1 && navigator.userAgent.indexOf('Safari') !== -1))) {
|
if (window.shimIndexedDB && (!indexedDB || (navigator.userAgent.indexOf('Chrome') === -1 && navigator.userAgent.indexOf('Safari') !== -1))) {
|
||||||
indexedDB = window.shimIndexedDB;
|
indexedDB = window.shimIndexedDB;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getStorePrefixFromType(type) {
|
||||||
|
// Return `files` for type `file`, `folders` for type `folder`, etc...
|
||||||
|
const prefix = `${type}s`;
|
||||||
|
return store.state[prefix] && prefix;
|
||||||
|
}
|
||||||
|
|
||||||
const deletedMarkerMaxAge = 1000;
|
const deletedMarkerMaxAge = 1000;
|
||||||
|
|
||||||
function identity(value) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeStore(storeName, schemaParameter) {
|
|
||||||
const schema = {
|
|
||||||
...schemaParameter,
|
|
||||||
updated: 'int',
|
|
||||||
};
|
|
||||||
|
|
||||||
const schemaKeys = Object.keys(schema);
|
|
||||||
const schemaKeysLen = schemaKeys.length;
|
|
||||||
const complexKeys = [];
|
|
||||||
let complexKeysLen = 0;
|
|
||||||
const attributeCheckers = {};
|
|
||||||
const attributeReaders = {};
|
|
||||||
const attributeWriters = {};
|
|
||||||
|
|
||||||
class Dao {
|
|
||||||
constructor(id, skipInit) {
|
|
||||||
this.id = id || utils.uid();
|
|
||||||
if (!skipInit) {
|
|
||||||
const fakeItem = {};
|
|
||||||
for (let i = 0; i < schemaKeysLen; i += 1) {
|
|
||||||
attributeReaders[schemaKeys[i]](fakeItem, this);
|
|
||||||
}
|
|
||||||
this.$dirty = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDao(id) {
|
|
||||||
return new Dao(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(schema).forEach((key) => {
|
|
||||||
const value = schema[key];
|
|
||||||
const storedValueKey = `_${key}`;
|
|
||||||
let defaultValue = value.default === undefined ? '' : value.default;
|
|
||||||
let serializer = value.serializer || identity;
|
|
||||||
let parser = value.parser || identity;
|
|
||||||
if (value === 'int') {
|
|
||||||
defaultValue = 0;
|
|
||||||
} else if (value === 'object') {
|
|
||||||
defaultValue = 'null';
|
|
||||||
parser = JSON.parse;
|
|
||||||
serializer = JSON.stringify;
|
|
||||||
}
|
|
||||||
|
|
||||||
attributeReaders[key] = (dbItem, dao) => {
|
|
||||||
dao[storedValueKey] = dbItem[key] || defaultValue;
|
|
||||||
};
|
|
||||||
attributeWriters[key] = (dbItem, dao) => {
|
|
||||||
const storedValue = dao[storedValueKey];
|
|
||||||
if (storedValue && storedValue !== defaultValue) {
|
|
||||||
dbItem[key] = storedValue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function getter() {
|
|
||||||
return this[storedValueKey];
|
|
||||||
}
|
|
||||||
|
|
||||||
function setter(param) {
|
|
||||||
const val = param || defaultValue;
|
|
||||||
if (this[storedValueKey] === val) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this[storedValueKey] = val;
|
|
||||||
this.$dirty = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === 'updated') {
|
|
||||||
Object.defineProperty(Dao.prototype, key, {
|
|
||||||
get: getter,
|
|
||||||
set: (value) => {
|
|
||||||
if (setter.call(this, value)) {
|
|
||||||
this.$dirtyUpdated = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else if (value === 'string' || value === 'int') {
|
|
||||||
Object.defineProperty(Dao.prototype, key, {
|
|
||||||
get: getter,
|
|
||||||
set: setter
|
|
||||||
})
|
|
||||||
} else if (![64, 128] // Handle string64 and string128
|
|
||||||
.cl_some(function (length) {
|
|
||||||
if (value === 'string' + length) {
|
|
||||||
Object.defineProperty(Dao.prototype, key, {
|
|
||||||
get: getter,
|
|
||||||
set: function (value) {
|
|
||||||
if (value && value.length > length) {
|
|
||||||
value = value.slice(0, length)
|
|
||||||
}
|
|
||||||
setter.call(this, value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
// Other types go to complexKeys list
|
|
||||||
complexKeys.push(key)
|
|
||||||
complexKeysLen++
|
|
||||||
|
|
||||||
// And have complex readers/writers
|
|
||||||
attributeReaders[key] = function (dbItem, dao) {
|
|
||||||
const storedValue = dbItem[key]
|
|
||||||
if (!storedValue) {
|
|
||||||
storedValue = defaultValue
|
|
||||||
}
|
|
||||||
dao[storedValueKey] = storedValue
|
|
||||||
dao[key] = parser(storedValue)
|
|
||||||
}
|
|
||||||
attributeWriters[key] = function (dbItem, dao) {
|
|
||||||
const storedValue = serializer(dao[key])
|
|
||||||
dao[storedValueKey] = storedValue
|
|
||||||
if (storedValue && storedValue !== defaultValue) {
|
|
||||||
dbItem[key] = storedValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checkers are only for complex types
|
|
||||||
attributeCheckers[key] = function (dao) {
|
|
||||||
return serializer(dao[key]) !== dao[storedValueKey]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const lastTx = 0
|
|
||||||
const storedSeqs = Object.create(null)
|
|
||||||
|
|
||||||
function readDbItem(item, daoMap) {
|
|
||||||
const dao = daoMap[item.id] || new Dao(item.id, true)
|
|
||||||
if (!item.updated) {
|
|
||||||
delete storedSeqs[item.id]
|
|
||||||
if (dao.updated) {
|
|
||||||
delete daoMap[item.id]
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (storedSeqs[item.id] === item.seq) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
storedSeqs[item.id] = item.seq
|
|
||||||
for (const i = 0; i < schemaKeysLen; i++) {
|
|
||||||
attributeReaders[schemaKeys[i]](item, dao)
|
|
||||||
}
|
|
||||||
dao.$dirty = false
|
|
||||||
dao.$dirtyUpdated = false
|
|
||||||
daoMap[item.id] = dao
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPatch(tx, cb) {
|
|
||||||
let resetMap;
|
|
||||||
|
|
||||||
// We may have missed some deleted markers
|
|
||||||
if (lastTx && tx.txCounter - lastTx > deletedMarkerMaxAge) {
|
|
||||||
// Delete all dirty daos, user was asleep anyway...
|
|
||||||
resetMap = true
|
|
||||||
// And retrieve everything from DB
|
|
||||||
lastTx = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasChanged = !lastTx
|
|
||||||
const store = tx.objectStore(storeName)
|
|
||||||
const index = store.index('seq')
|
|
||||||
const range = $window.IDBKeyRange.lowerBound(lastTx, true)
|
|
||||||
const items = []
|
|
||||||
const itemsToDelete = []
|
|
||||||
index.openCursor(range).onsuccess = function (event) {
|
|
||||||
const cursor = event.target.result
|
|
||||||
if (!cursor) {
|
|
||||||
itemsToDelete.cl_each(function (item) {
|
|
||||||
store.delete(item.id)
|
|
||||||
})
|
|
||||||
items.length && debug('Got ' + items.length + ' ' + storeName + ' items')
|
|
||||||
// Return a patch, to apply changes later
|
|
||||||
return cb(function (daoMap) {
|
|
||||||
if (resetMap) {
|
|
||||||
Object.keys(daoMap).cl_each(function (key) {
|
|
||||||
delete daoMap[key]
|
|
||||||
})
|
|
||||||
storedSeqs = Object.create(null)
|
|
||||||
}
|
|
||||||
items.cl_each(function (item) {
|
|
||||||
hasChanged |= readDbItem(item, daoMap)
|
|
||||||
})
|
|
||||||
return hasChanged
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const item = cursor.value
|
|
||||||
items.push(item)
|
|
||||||
// Remove old deleted markers
|
|
||||||
if (!item.updated && tx.txCounter - item.seq > deletedMarkerMaxAge) {
|
|
||||||
itemsToDelete.push(item)
|
|
||||||
}
|
|
||||||
cursor.continue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeAll(daoMap, tx) {
|
|
||||||
lastTx = tx.txCounter
|
|
||||||
const store = tx.objectStore(storeName)
|
|
||||||
|
|
||||||
// Remove deleted daos
|
|
||||||
const storedIds = Object.keys(storedSeqs)
|
|
||||||
const storedIdsLen = storedIds.length
|
|
||||||
for (const i = 0; i < storedIdsLen; i++) {
|
|
||||||
const id = storedIds[i]
|
|
||||||
if (!daoMap[id]) {
|
|
||||||
// Put a deleted marker to notify other tabs
|
|
||||||
store.put({
|
|
||||||
id: id,
|
|
||||||
seq: lastTx
|
|
||||||
})
|
|
||||||
delete storedSeqs[id]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put changes
|
|
||||||
const daoIds = Object.keys(daoMap)
|
|
||||||
const daoIdsLen = daoIds.length
|
|
||||||
for (i = 0; i < daoIdsLen; i++) {
|
|
||||||
const dao = daoMap[daoIds[i]]
|
|
||||||
const dirty = dao.$dirty
|
|
||||||
for (const j = 0; !dirty && j < complexKeysLen; j++) {
|
|
||||||
dirty |= attributeCheckers[complexKeys[j]](dao)
|
|
||||||
}
|
|
||||||
if (dirty) {
|
|
||||||
if (!dao.$dirtyUpdated) {
|
|
||||||
// Force update the `updated` attribute
|
|
||||||
dao.updated = Date.now()
|
|
||||||
}
|
|
||||||
const item = {
|
|
||||||
id: daoIds[i],
|
|
||||||
seq: lastTx
|
|
||||||
}
|
|
||||||
for (j = 0; j < schemaKeysLen; j++) {
|
|
||||||
attributeWriters[schemaKeys[j]](item, dao)
|
|
||||||
}
|
|
||||||
debug('Put ' + storeName + ' item')
|
|
||||||
store.put(item)
|
|
||||||
storedSeqs[item.id] = item.seq
|
|
||||||
dao.$dirty = false
|
|
||||||
dao.$dirtyUpdated = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
getPatch,
|
|
||||||
writeAll,
|
|
||||||
createDao,
|
|
||||||
Dao,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class Connection {
|
class Connection {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.getTxCbs = [];
|
this.getTxCbs = [];
|
||||||
|
|
||||||
// Init connexion
|
// Init connexion
|
||||||
const request = indexedDB.open('classeur-db', dbVersion);
|
const request = indexedDB.open('stackedit-db', dbVersion);
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
throw new Error("Can't connect to IndexedDB.");
|
throw new Error("Can't connect to IndexedDB.");
|
||||||
@ -298,23 +46,21 @@ class Connection {
|
|||||||
request.onupgradeneeded = (event) => {
|
request.onupgradeneeded = (event) => {
|
||||||
const eventDb = event.target.result;
|
const eventDb = event.target.result;
|
||||||
const oldVersion = event.oldVersion || 0;
|
const oldVersion = event.oldVersion || 0;
|
||||||
function createStore(name) {
|
|
||||||
const store = eventDb.createObjectStore(name, { keyPath: 'id' });
|
|
||||||
store.createIndex('seq', 'seq', { unique: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't use 'break' in this switch statement,
|
// We don't use 'break' in this switch statement,
|
||||||
// the fall-through behaviour is what we want.
|
// the fall-through behaviour is what we want.
|
||||||
/* eslint-disable no-fallthrough */
|
/* eslint-disable no-fallthrough */
|
||||||
switch (oldVersion) {
|
switch (oldVersion) {
|
||||||
case 0:
|
case 0:
|
||||||
[
|
{
|
||||||
'contents',
|
// Create store
|
||||||
'files',
|
const dbStore = eventDb.createObjectStore(dbStoreName, {
|
||||||
'folders',
|
keyPath: 'id',
|
||||||
'objects',
|
});
|
||||||
'app',
|
dbStore.createIndex('tx', 'tx', {
|
||||||
].forEach(createStore);
|
unique: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
/* eslint-enable no-fallthrough */
|
/* eslint-enable no-fallthrough */
|
||||||
@ -333,31 +79,144 @@ class Connection {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tx = this.db.transaction(this.db.objectStoreNames, 'readwrite');
|
const tx = this.db.transaction(this.db.objectStoreNames, 'readwrite');
|
||||||
// tx.onerror = (evt) => {
|
tx.onerror = (evt) => {
|
||||||
// dbg('Rollback transaction', evt);
|
dbg('Rollback transaction', evt);
|
||||||
// };
|
};
|
||||||
const store = tx.objectStore('app');
|
const dbStore = tx.objectStore(dbStoreName);
|
||||||
const request = store.get('txCounter');
|
const request = dbStore.get('txCounter');
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
tx.txCounter = request.result ? request.result.value : 0;
|
tx.txCounter = request.result ? request.result.tx : 0;
|
||||||
tx.txCounter += 1;
|
tx.txCounter += 1;
|
||||||
store.put({
|
dbStore.put({
|
||||||
id: 'txCounter',
|
id: 'txCounter',
|
||||||
value: tx.txCounter,
|
tx: tx.txCounter,
|
||||||
});
|
});
|
||||||
cb(tx);
|
cb(tx);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocalDbStorage {
|
class Storage {
|
||||||
init(store) {
|
constructor() {
|
||||||
this.store = store;
|
this.lastTx = 0;
|
||||||
store.subscribe((mutation, state) => {
|
this.updatedMap = Object.create(null);
|
||||||
console.log(mutation, state);
|
|
||||||
});
|
|
||||||
this.connection = new Connection();
|
this.connection = new Connection();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sync() {
|
||||||
|
const storeItemMap = {};
|
||||||
|
[
|
||||||
|
store.state.files,
|
||||||
|
].forEach(moduleState => Object.assign(storeItemMap, moduleState.itemMap));
|
||||||
|
const tx = this.connection.createTx();
|
||||||
|
this.readAll(storeItemMap, tx, () => this.writeAll(storeItemMap, tx));
|
||||||
|
}
|
||||||
|
|
||||||
|
readAll(storeItemMap, tx, cb) {
|
||||||
|
let resetMap;
|
||||||
|
|
||||||
|
// We may have missed some deleted markers
|
||||||
|
if (this.lastTx && tx.txCounter - this.lastTx > deletedMarkerMaxAge) {
|
||||||
|
// Delete all dirty store items (user was asleep anyway...)
|
||||||
|
resetMap = true;
|
||||||
|
// And retrieve everything from DB
|
||||||
|
this.lastTx = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbStore = tx.objectStore(dbStoreName);
|
||||||
|
const index = dbStore.index('tx');
|
||||||
|
const range = window.IDBKeyRange.lowerBound(this.lastTx, true);
|
||||||
|
const items = [];
|
||||||
|
const itemsToDelete = [];
|
||||||
|
index.openCursor(range).onsuccess = (event) => {
|
||||||
|
const cursor = event.target.result;
|
||||||
|
if (!cursor) {
|
||||||
|
itemsToDelete.forEach((item) => {
|
||||||
|
dbStore.delete(item.id);
|
||||||
|
});
|
||||||
|
if (items.length) {
|
||||||
|
dbg(`Got ${items.length} items`);
|
||||||
|
}
|
||||||
|
if (resetMap) {
|
||||||
|
Object.keys(storeItemMap).forEach((id) => {
|
||||||
|
delete storeItemMap[id];
|
||||||
|
});
|
||||||
|
this.updatedMap = Object.create(null);
|
||||||
|
}
|
||||||
|
items.forEach(item => this.readDbItem(item, storeItemMap));
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
const item = cursor.value;
|
||||||
|
items.push(item);
|
||||||
|
// Remove old deleted markers
|
||||||
|
if (!item.updated && tx.txCounter - item.tx > deletedMarkerMaxAge) {
|
||||||
|
itemsToDelete.push(item);
|
||||||
|
}
|
||||||
|
cursor.continue();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
writeAll(storeItemMap, tx) {
|
||||||
|
this.lastTx = tx.txCounter;
|
||||||
|
const dbStore = tx.objectStore(dbStoreName);
|
||||||
|
|
||||||
|
// Remove deleted store items
|
||||||
|
const storedIds = Object.keys(this.updatedMap);
|
||||||
|
const storedIdsLen = storedIds.length;
|
||||||
|
for (let i = 0; i < storedIdsLen; i += 1) {
|
||||||
|
const id = storedIds[i];
|
||||||
|
if (!storeItemMap[id]) {
|
||||||
|
// Put a deleted marker to notify other tabs
|
||||||
|
dbStore.put({
|
||||||
|
id,
|
||||||
|
tx: this.lastTx,
|
||||||
|
});
|
||||||
|
delete this.updatedMap[id];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LocalDbStorage;
|
// Put changes
|
||||||
|
const storeItemIds = Object.keys(storeItemMap);
|
||||||
|
const storeItemIdsLen = storeItemIds.length;
|
||||||
|
for (let i = 0; i < storeItemIdsLen; i += 1) {
|
||||||
|
const storeItem = storeItemMap[storeItemIds[i]];
|
||||||
|
// Store object has changed
|
||||||
|
if (this.updatedMap[storeItem.id] !== storeItem.updated) {
|
||||||
|
const item = {
|
||||||
|
...storeItem,
|
||||||
|
tx: this.lastTx,
|
||||||
|
};
|
||||||
|
dbg('Putting 1 item');
|
||||||
|
dbStore.put(item);
|
||||||
|
this.updatedMap[item.id] = item.updated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readDbItem(dbItem, storeItemMap) {
|
||||||
|
const existingStoreItem = storeItemMap[dbItem.id];
|
||||||
|
if (!dbItem.updated) {
|
||||||
|
delete this.updatedMap[dbItem.id];
|
||||||
|
if (existingStoreItem) {
|
||||||
|
const prefix = getStorePrefixFromType(existingStoreItem.type);
|
||||||
|
if (prefix) {
|
||||||
|
delete storeItemMap[existingStoreItem.id];
|
||||||
|
// Remove object from the store
|
||||||
|
store.commit(`${prefix}/deleteItem`, existingStoreItem.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.updatedMap[dbItem.id] !== dbItem.updated) {
|
||||||
|
const storeItem = {
|
||||||
|
...dbItem,
|
||||||
|
tx: undefined,
|
||||||
|
};
|
||||||
|
this.updatedMap[storeItem.id] = storeItem.updated;
|
||||||
|
storeItemMap[storeItem.id] = storeItem;
|
||||||
|
// Put object in the store
|
||||||
|
const prefix = getStorePrefixFromType(storeItem.type);
|
||||||
|
store.commit(`${prefix}/setItem`, storeItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Storage;
|
||||||
|
@ -4,14 +4,11 @@ import Vuex from 'vuex';
|
|||||||
import files from './modules/files';
|
import files from './modules/files';
|
||||||
import layout from './modules/layout';
|
import layout from './modules/layout';
|
||||||
import editor from './modules/editor';
|
import editor from './modules/editor';
|
||||||
import LocalDbStorage from '../services/localDbSvc';
|
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
const debug = process.env.NODE_ENV !== 'production';
|
const debug = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
const localDbStorage = new LocalDbStorage();
|
|
||||||
|
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
modules: {
|
modules: {
|
||||||
files,
|
files,
|
||||||
@ -19,7 +16,7 @@ const store = new Vuex.Store({
|
|||||||
editor,
|
editor,
|
||||||
},
|
},
|
||||||
strict: debug,
|
strict: debug,
|
||||||
plugins: [_store => localDbStorage.init(_store)].concat(debug ? [createLogger()] : []),
|
plugins: debug ? [createLogger()] : [],
|
||||||
});
|
});
|
||||||
|
|
||||||
export default store;
|
export default store;
|
||||||
|
22
src/store/modules/contents.js
Normal file
22
src/store/modules/contents.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import moduleTemplate from './moduleTemplate';
|
||||||
|
import emptyContent from '../../data/emptyContent';
|
||||||
|
|
||||||
|
const module = moduleTemplate();
|
||||||
|
|
||||||
|
module.getters = {
|
||||||
|
...module.getters,
|
||||||
|
current: (state, getters, rootState, rootGetters) =>
|
||||||
|
state.itemMap[rootGetters['files/current'].contentId] || emptyContent(),
|
||||||
|
};
|
||||||
|
|
||||||
|
module.actions = {
|
||||||
|
...module.actions,
|
||||||
|
patchCurrent({ getters, commit }, value) {
|
||||||
|
commit('patchItem', {
|
||||||
|
...value,
|
||||||
|
id: getters.current.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default module;
|
@ -1,36 +1,26 @@
|
|||||||
import mdSample from '../../markdown/sample.md';
|
import moduleTemplate from './moduleTemplate';
|
||||||
|
import defaultFile from '../../data/emptyFile';
|
||||||
|
|
||||||
export default {
|
const module = moduleTemplate();
|
||||||
namespaced: true,
|
|
||||||
state: {
|
module.state = {
|
||||||
files: [],
|
...module.state,
|
||||||
currentFile: {
|
currentId: null,
|
||||||
name: 'Test 123456 abcdefghijkl 123456 abcdefghijkl 123456 abcdefghijkl 123456 abcdefghijkl',
|
};
|
||||||
folderId: null,
|
|
||||||
isLoaded: true,
|
module.getters = {
|
||||||
content: {
|
...module.getters,
|
||||||
state: {},
|
current: state => state.itemMap[state.currentId] || defaultFile(),
|
||||||
text: mdSample,
|
};
|
||||||
properties: {},
|
|
||||||
discussions: {},
|
module.actions = {
|
||||||
comments: {},
|
...module.actions,
|
||||||
},
|
patchCurrent({ getters, commit }, value) {
|
||||||
},
|
commit('patchItem', {
|
||||||
},
|
...value,
|
||||||
mutations: {
|
id: getters.current.id,
|
||||||
setCurrentFile: (state, value) => {
|
});
|
||||||
state.currentFile = value;
|
|
||||||
},
|
|
||||||
setCurrentFileName: (state, value) => {
|
|
||||||
if (value) {
|
|
||||||
state.currentFile.name = value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setCurrentFileContentText: (state, value) => {
|
|
||||||
state.currentFile.content.text = value;
|
|
||||||
},
|
|
||||||
setCurrentFileContentState: (state, value) => {
|
|
||||||
state.currentFile.content.state = value;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default module;
|
||||||
|
28
src/store/modules/moduleTemplate.js
Normal file
28
src/store/modules/moduleTemplate.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default () => ({
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
itemMap: {},
|
||||||
|
},
|
||||||
|
getters: {},
|
||||||
|
mutations: {
|
||||||
|
setItem(state, item) {
|
||||||
|
Vue.set(state.itemMap, item.id, item);
|
||||||
|
},
|
||||||
|
patchItem(state, patch) {
|
||||||
|
const item = state.itemMap[patch.id];
|
||||||
|
if (item) {
|
||||||
|
Vue.set(state.itemMap, item.id, {
|
||||||
|
...item,
|
||||||
|
...patch,
|
||||||
|
updated: Date.now(), // Trigger sync
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteItem(state, id) {
|
||||||
|
Vue.delete(state.itemMap, id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user