Store things in localDb
This commit is contained in:
parent
97172e28b3
commit
02a4696b40
@ -5,7 +5,7 @@
|
||||
<div class="spinner"></div>
|
||||
</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 class="navigation-bar__inner navigation-bar__inner--left">
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('bold')">
|
||||
@ -55,7 +55,7 @@ import animationSvc from '../services/animationSvc';
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
titleFocused: false,
|
||||
titleFocus: false,
|
||||
titleHover: false,
|
||||
}),
|
||||
computed: {
|
||||
@ -65,14 +65,14 @@ export default {
|
||||
}),
|
||||
title: {
|
||||
get() {
|
||||
return this.$store.state.files.currentFile.name;
|
||||
return this.$store.getters['files/current'].name;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('files/setCurrentFileName', value);
|
||||
set(name) {
|
||||
this.$store.dispatch('files/patchCurrent', { name });
|
||||
},
|
||||
},
|
||||
titleScrolling() {
|
||||
const result = this.titleHover && !this.titleFocused;
|
||||
const result = this.titleHover && !this.titleFocus;
|
||||
if (this.titleInputElt) {
|
||||
if (result) {
|
||||
const scrollLeft = this.titleInputElt.scrollWidth - this.titleInputElt.offsetWidth;
|
||||
@ -95,7 +95,7 @@ export default {
|
||||
editorSvc.pagedownEditor.uiManager.doClick(name);
|
||||
},
|
||||
editTitle(toggle) {
|
||||
this.titleFocused = toggle;
|
||||
this.titleFocus = toggle;
|
||||
if (toggle) {
|
||||
this.titleInputElt.setSelectionRange(0, this.titleInputElt.value.length);
|
||||
}
|
||||
@ -197,7 +197,7 @@ export default {
|
||||
.navigation-bar__title--input {
|
||||
cursor: pointer;
|
||||
|
||||
&.navigation-bar__title--focused {
|
||||
&.navigation-bar__title--focus {
|
||||
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 currentPatchableText;
|
||||
let discussionMarkers;
|
||||
let content;
|
||||
let isChangePatch;
|
||||
let contentId;
|
||||
|
||||
function getDiscussionMarkers(discussion, discussionId, onMarker) {
|
||||
function getMarker(offsetName) {
|
||||
@ -35,6 +35,7 @@ function getDiscussionMarkers(discussion, discussionId, onMarker) {
|
||||
}
|
||||
|
||||
function syncDiscussionMarkers() {
|
||||
const content = store.getters['contents/current'];
|
||||
Object.keys(discussionMarkers)
|
||||
.forEach((markerKey) => {
|
||||
const marker = discussionMarkers[markerKey];
|
||||
@ -99,8 +100,9 @@ export default {
|
||||
markerIdxMap = Object.create(null);
|
||||
discussionMarkers = {};
|
||||
clEditor.on('contentChanged', (text) => {
|
||||
store.commit('files/setCurrentFileContentText', text);
|
||||
store.dispatch('contents/patchCurrent', { text });
|
||||
syncDiscussionMarkers();
|
||||
const content = store.getters('contents/current');
|
||||
if (!isChangePatch) {
|
||||
previousPatchableText = currentPatchableText;
|
||||
currentPatchableText = clDiffUtils.makePatchableText(content, markerKeys, markerIdxMap);
|
||||
@ -121,11 +123,12 @@ export default {
|
||||
clEditor.addMarker(newDiscussionMarker1);
|
||||
},
|
||||
initClEditor(opts, reinit) {
|
||||
if (store.state.files.currentFile.content) {
|
||||
const content = store.getters('contents/current');
|
||||
if (content) {
|
||||
const options = Object.assign({}, opts);
|
||||
|
||||
if (content !== store.state.files.currentFile.content) {
|
||||
content = store.state.files.currentFile.content;
|
||||
if (contentId !== content.id) {
|
||||
contentId = content.id;
|
||||
currentPatchableText = clDiffUtils.makePatchableText(content, markerKeys, markerIdxMap);
|
||||
previousPatchableText = currentPatchableText;
|
||||
syncDiscussionMarkers();
|
||||
@ -153,6 +156,7 @@ export default {
|
||||
this.lastExternalChange = Date.now();
|
||||
}
|
||||
syncDiscussionMarkers();
|
||||
const content = store.getters('contents/current');
|
||||
return clEditor.setContent(content.text, isExternal);
|
||||
},
|
||||
};
|
||||
|
@ -29,8 +29,9 @@ const allowDebounce = (action, wait) => {
|
||||
};
|
||||
|
||||
const diffMatchPatch = new DiffMatchPatch();
|
||||
let lastContentId = null;
|
||||
let reinitClEditor = true;
|
||||
let isPreviewRefreshed = false;
|
||||
let instantPreview = true;
|
||||
let tokens;
|
||||
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
|
||||
*/
|
||||
initClEditor() {
|
||||
if (!store.state.files.currentFile.isLoaded) {
|
||||
const contentId = store.getters['contents/current'].id;
|
||||
if (contentId !== lastContentId) {
|
||||
reinitClEditor = true;
|
||||
isPreviewRefreshed = false;
|
||||
instantPreview = true;
|
||||
lastContentId = contentId;
|
||||
}
|
||||
if (!contentId) {
|
||||
// This means the content is not loaded yet
|
||||
editorEngineSvc.clEditor.toggleEditable(false);
|
||||
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.
|
||||
*/
|
||||
makeTextToPreviewDiffs: allowDebounce(() => {
|
||||
if (editorSvc.sectionDescList
|
||||
&& editorSvc.sectionDescList !== editorSvc.sectionDescMeasuredList) {
|
||||
if (editorSvc.sectionDescList &&
|
||||
editorSvc.sectionDescList !== editorSvc.sectionDescMeasuredList) {
|
||||
editorSvc.sectionDescList
|
||||
.forEach((sectionDesc) => {
|
||||
if (!sectionDesc.textToPreviewDiffs) {
|
||||
@ -355,19 +361,21 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
*/
|
||||
saveContentState: allowDebounce(() => {
|
||||
const scrollPosition = editorSvc.getScrollPosition() ||
|
||||
store.state.files.currentFile.content.state.scrollPosition;
|
||||
store.commit('files/setCurrentFileContentState', {
|
||||
store.getters['contents/current'].state.scrollPosition;
|
||||
store.dispatch('contents/patchCurrent', {
|
||||
state: {
|
||||
selectionStart: editorEngineSvc.clEditor.selectionMgr.selectionStart,
|
||||
selectionEnd: editorEngineSvc.clEditor.selectionMgr.selectionEnd,
|
||||
scrollPosition,
|
||||
}, { root: true });
|
||||
},
|
||||
});
|
||||
}, 100),
|
||||
|
||||
/**
|
||||
* Restore the scroll position from the current file content state.
|
||||
*/
|
||||
restoreScrollPosition() {
|
||||
const scrollPosition = store.state.files.currentFile.content.state.scrollPosition;
|
||||
const scrollPosition = store.getters['contents/current'].state.scrollPosition;
|
||||
if (scrollPosition && this.sectionDescMeasuredList) {
|
||||
const objectToScroll = this.getObjectToScroll();
|
||||
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 (
|
||||
/* eslint-disable no-bitwise */
|
||||
!(editorSvc.previewElt.compareDocumentPosition(range.startContainer)
|
||||
& window.Node.DOCUMENT_POSITION_CONTAINED_BY) ||
|
||||
!(editorSvc.previewElt.compareDocumentPosition(range.endContainer)
|
||||
& window.Node.DOCUMENT_POSITION_CONTAINED_BY)
|
||||
!(editorSvc.previewElt.compareDocumentPosition(range.startContainer) &
|
||||
window.Node.DOCUMENT_POSITION_CONTAINED_BY) ||
|
||||
!(editorSvc.previewElt.compareDocumentPosition(range.endContainer) &
|
||||
window.Node.DOCUMENT_POSITION_CONTAINED_BY)
|
||||
/* eslint-enable no-bitwise */
|
||||
) {
|
||||
range = null;
|
||||
@ -561,14 +569,14 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
|
||||
const refreshPreview = () => {
|
||||
this.convert();
|
||||
if (!isPreviewRefreshed) {
|
||||
if (instantPreview) {
|
||||
this.refreshPreview();
|
||||
this.measureSectionDimensions();
|
||||
this.restoreScrollPosition();
|
||||
} else {
|
||||
setTimeout(() => this.refreshPreview(), 10);
|
||||
}
|
||||
isPreviewRefreshed = true;
|
||||
instantPreview = false;
|
||||
};
|
||||
|
||||
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) {
|
||||
this.sectionList = newSectionList;
|
||||
this.$emit('sectionList', this.sectionList);
|
||||
if (isPreviewRefreshed) {
|
||||
debouncedRefreshPreview();
|
||||
} else {
|
||||
if (instantPreview) {
|
||||
refreshPreview();
|
||||
} else {
|
||||
debouncedRefreshPreview();
|
||||
}
|
||||
}
|
||||
if (this.selectionRange !== newSelectionRange) {
|
||||
@ -710,7 +718,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
// })
|
||||
|
||||
store.watch(
|
||||
state => state.files.currentFile.content.properties,
|
||||
() => store.getters['contents/current'].properties,
|
||||
(properties) => {
|
||||
const options = properties && extensionSvc.getOptions(properties, true);
|
||||
if (JSON.stringify(options) !== JSON.stringify(editorSvc.options)) {
|
||||
|
@ -1,286 +1,34 @@
|
||||
import 'babel-polyfill';
|
||||
import 'indexeddbshim';
|
||||
import utils from './utils';
|
||||
import debug from 'debug';
|
||||
import store from '../store';
|
||||
|
||||
const dbg = debug('stackedit:localDbSvc');
|
||||
|
||||
let indexedDB = window.indexedDB;
|
||||
const localStorage = window.localStorage;
|
||||
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))) {
|
||||
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;
|
||||
|
||||
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 {
|
||||
constructor() {
|
||||
this.getTxCbs = [];
|
||||
|
||||
// Init connexion
|
||||
const request = indexedDB.open('classeur-db', dbVersion);
|
||||
const request = indexedDB.open('stackedit-db', dbVersion);
|
||||
|
||||
request.onerror = () => {
|
||||
throw new Error("Can't connect to IndexedDB.");
|
||||
@ -298,23 +46,21 @@ class Connection {
|
||||
request.onupgradeneeded = (event) => {
|
||||
const eventDb = event.target.result;
|
||||
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,
|
||||
// the fall-through behaviour is what we want.
|
||||
/* eslint-disable no-fallthrough */
|
||||
switch (oldVersion) {
|
||||
case 0:
|
||||
[
|
||||
'contents',
|
||||
'files',
|
||||
'folders',
|
||||
'objects',
|
||||
'app',
|
||||
].forEach(createStore);
|
||||
{
|
||||
// Create store
|
||||
const dbStore = eventDb.createObjectStore(dbStoreName, {
|
||||
keyPath: 'id',
|
||||
});
|
||||
dbStore.createIndex('tx', 'tx', {
|
||||
unique: false,
|
||||
});
|
||||
}
|
||||
default:
|
||||
}
|
||||
/* eslint-enable no-fallthrough */
|
||||
@ -333,31 +79,144 @@ class Connection {
|
||||
return;
|
||||
}
|
||||
const tx = this.db.transaction(this.db.objectStoreNames, 'readwrite');
|
||||
// tx.onerror = (evt) => {
|
||||
// dbg('Rollback transaction', evt);
|
||||
// };
|
||||
const store = tx.objectStore('app');
|
||||
const request = store.get('txCounter');
|
||||
tx.onerror = (evt) => {
|
||||
dbg('Rollback transaction', evt);
|
||||
};
|
||||
const dbStore = tx.objectStore(dbStoreName);
|
||||
const request = dbStore.get('txCounter');
|
||||
request.onsuccess = () => {
|
||||
tx.txCounter = request.result ? request.result.value : 0;
|
||||
tx.txCounter = request.result ? request.result.tx : 0;
|
||||
tx.txCounter += 1;
|
||||
store.put({
|
||||
dbStore.put({
|
||||
id: 'txCounter',
|
||||
value: tx.txCounter,
|
||||
tx: tx.txCounter,
|
||||
});
|
||||
cb(tx);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class LocalDbStorage {
|
||||
init(store) {
|
||||
this.store = store;
|
||||
store.subscribe((mutation, state) => {
|
||||
console.log(mutation, state);
|
||||
});
|
||||
class Storage {
|
||||
constructor() {
|
||||
this.lastTx = 0;
|
||||
this.updatedMap = Object.create(null);
|
||||
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 layout from './modules/layout';
|
||||
import editor from './modules/editor';
|
||||
import LocalDbStorage from '../services/localDbSvc';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const debug = process.env.NODE_ENV !== 'production';
|
||||
|
||||
const localDbStorage = new LocalDbStorage();
|
||||
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
files,
|
||||
@ -19,7 +16,7 @@ const store = new Vuex.Store({
|
||||
editor,
|
||||
},
|
||||
strict: debug,
|
||||
plugins: [_store => localDbStorage.init(_store)].concat(debug ? [createLogger()] : []),
|
||||
plugins: debug ? [createLogger()] : [],
|
||||
});
|
||||
|
||||
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 {
|
||||
namespaced: true,
|
||||
state: {
|
||||
files: [],
|
||||
currentFile: {
|
||||
name: 'Test 123456 abcdefghijkl 123456 abcdefghijkl 123456 abcdefghijkl 123456 abcdefghijkl',
|
||||
folderId: null,
|
||||
isLoaded: true,
|
||||
content: {
|
||||
state: {},
|
||||
text: mdSample,
|
||||
properties: {},
|
||||
discussions: {},
|
||||
comments: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
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;
|
||||
},
|
||||
const module = moduleTemplate();
|
||||
|
||||
module.state = {
|
||||
...module.state,
|
||||
currentId: null,
|
||||
};
|
||||
|
||||
module.getters = {
|
||||
...module.getters,
|
||||
current: state => state.itemMap[state.currentId] || defaultFile(),
|
||||
};
|
||||
|
||||
module.actions = {
|
||||
...module.actions,
|
||||
patchCurrent({ getters, commit }, value) {
|
||||
commit('patchItem', {
|
||||
...value,
|
||||
id: getters.current.id,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
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