Store things in localDb

This commit is contained in:
Benoit Schweblin 2017-07-27 21:19:52 +01:00
parent 97172e28b3
commit 02a4696b40
11 changed files with 283 additions and 363 deletions

View File

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

@ -0,0 +1,7 @@
export default () => ({
state: {},
text: '\n',
properties: {},
discussions: {},
comments: {},
});

5
src/data/emptyFile.js Normal file
View File

@ -0,0 +1,5 @@
export default () => ({
name: '',
folderId: null,
contentId: null,
});

View File

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

View File

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

View File

@ -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];
}
}
// 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 LocalDbStorage; export default Storage;

View File

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

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

View File

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

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