This commit is contained in:
benweet 2017-08-06 01:58:39 +01:00
parent 8b3e3a898b
commit d09375dc4c
13 changed files with 406 additions and 232 deletions

View File

@ -34,10 +34,10 @@ export default {
@import 'common/app'; @import 'common/app';
.app__spash-screen { .app__spash-screen {
position: absolute; margin: 0 auto;
width: 100%; max-width: 600px;
height: 100%; height: 100%;
background: no-repeat center url('../assets/logo.svg'); background: no-repeat center url('../assets/logo.svg');
background-color: #fff; background-size: contain;
} }
</style> </style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="layout"> <div class="layout">
<div class="layout__panel flex flex--row"> <div class="layout__panel flex flex--row" :class="{'flex--end': styles.showSideBar && !styles.showExplorer}">
<div class="layout__panel layout__panel--explorer" v-show="styles.showExplorer" :style="{ width: constants.explorerWidth + 'px' }"> <div class="layout__panel layout__panel--explorer" v-show="styles.showExplorer" :style="{ width: constants.explorerWidth + 'px' }">
<explorer></explorer> <explorer></explorer>
</div> </div>
@ -31,7 +31,7 @@
</template> </template>
<script> <script>
import { mapState, mapGetters, mapMutations } from 'vuex'; import { mapGetters, mapMutations } from 'vuex';
import NavigationBar from './NavigationBar'; import NavigationBar from './NavigationBar';
import ButtonBar from './ButtonBar'; import ButtonBar from './ButtonBar';
import StatusBar from './StatusBar'; import StatusBar from './StatusBar';
@ -52,10 +52,8 @@ export default {
Preview, Preview,
}, },
computed: { computed: {
...mapState('layout', [
'constants',
]),
...mapGetters('layout', [ ...mapGetters('layout', [
'constants',
'styles', 'styles',
]), ]),
}, },
@ -77,48 +75,12 @@ export default {
const previewElt = this.$el.querySelector('.preview__inner-2'); const previewElt = this.$el.querySelector('.preview__inner-2');
const tocElt = this.$el.querySelector('.toc__inner'); const tocElt = this.$el.querySelector('.toc__inner');
editorSvc.init(editorElt, previewElt, tocElt); editorSvc.init(editorElt, previewElt, tocElt);
// TOC click behaviour
let isMousedown;
function onClick(e) {
if (!isMousedown) {
return;
}
e.preventDefault();
const y = e.clientY - tocElt.getBoundingClientRect().top;
editorSvc.sectionDescList.some((sectionDesc) => {
if (y >= sectionDesc.tocDimension.endOffset) {
return false;
}
const posInSection = (y - sectionDesc.tocDimension.startOffset)
/ (sectionDesc.tocDimension.height || 1);
const editorScrollTop = sectionDesc.editorDimension.startOffset
+ (sectionDesc.editorDimension.height * posInSection);
editorElt.parentNode.scrollTop = editorScrollTop;
const previewScrollTop = sectionDesc.previewDimension.startOffset
+ (sectionDesc.previewDimension.height * posInSection);
previewElt.parentNode.scrollTop = previewScrollTop;
return true;
});
}
tocElt.addEventListener('mouseup', () => {
isMousedown = false;
});
tocElt.addEventListener('mouseleave', () => {
isMousedown = false;
});
tocElt.addEventListener('mousedown', (e) => {
isMousedown = e.which === 1;
onClick(e);
});
tocElt.addEventListener('mousemove', (e) => {
onClick(e);
});
}, },
destroyed() { destroyed() {
window.removeEventListener('resize', this.updateStyle); window.removeEventListener('resize', this.updateStyle);
window.removeEventListener('keyup', this.saveSelection);
window.removeEventListener('mouseup', this.saveSelection);
window.removeEventListener('contextmenu', this.saveSelection);
}, },
}; };
</script> </script>
@ -152,6 +114,11 @@ export default {
background-color: #fff; background-color: #fff;
} }
.layout__panel--button-bar,
.layout__panel--preview {
background-color: #f3f3f3;
}
.layout__panel--explorer, .layout__panel--explorer,
.layout__panel--side-bar { .layout__panel--side-bar {
background-color: #dadada; background-color: #dadada;

View File

@ -1,33 +1,34 @@
<template> <template>
<div class="side-bar flex flex--column"> <div class="side-bar flex flex--column">
<div class="side-title flex flex--row flex--space-between"> <div class="side-title flex flex--row">
<div class="flex flex--row">
<button v-if="panel !== 'menu'" class="side-title__button button" @click="panel = 'menu'"> <button v-if="panel !== 'menu'" class="side-title__button button" @click="panel = 'menu'">
<icon-arrow-left></icon-arrow-left> <icon-arrow-left></icon-arrow-left>
</button> </button>
<div class="side-title__title"> <div class="side-title__title">
{{panelName}} {{panelName}}
</div> </div>
</div>
<button class="side-title__button button" @click="toggleSideBar(false)"> <button class="side-title__button button" @click="toggleSideBar(false)">
<icon-close></icon-close> <icon-close></icon-close>
</button> </button>
</div> </div>
<div class="side-bar__inner"> <div class="side-bar__inner">
<div v-if="panel === 'menu'" class="side-bar__panel side-bar__panel--menu"> <div v-if="panel === 'menu'" class="side-bar__panel side-bar__panel--menu">
<menu-item> <side-bar-item @click.native="signin">
<icon-login slot="icon"></icon-login> <icon-login slot="icon"></icon-login>
<div>Sign in with Google</div> <div>Sign in with Google</div>
<span>Have all your files and settings backed up and synced.</span> <span>Have all your files and settings backed up and synced.</span>
</menu-item> </side-bar-item>
<menu-item @click.native="panel = 'toc'"> <side-bar-item @click.native="panel = 'toc'">
<icon-toc slot="icon"></icon-toc> <icon-toc slot="icon"></icon-toc>
Table of Contents Table of contents
</menu-item> </side-bar-item>
<menu-item @click.native="panel = 'help'"> <side-bar-item @click.native="panel = 'help'">
<icon-help-circle slot="icon"></icon-help-circle> <icon-help-circle slot="icon"></icon-help-circle>
Markdown help Markdown cheat sheet
</menu-item> </side-bar-item>
</div>
<div v-else-if="panel === 'help'" class="side-bar__panel side-bar__panel--help">
<pre class="markdown-highlighting" v-html="markdownSample"></pre>
</div> </div>
<div class="side-bar__panel side-bar__panel--toc" :class="{'side-bar__panel--hidden': panel !== 'toc'}"> <div class="side-bar__panel side-bar__panel--toc" :class="{'side-bar__panel--hidden': panel !== 'toc'}">
<toc> <toc>
@ -40,25 +41,30 @@
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import Toc from './Toc'; import Toc from './Toc';
import MenuItem from './MenuItem'; import SideBarItem from './SideBarItem';
import markdownSample from '../data/markdownSample.md';
import markdownConversionSvc from '../services/markdownConversionSvc';
import userSvc from '../services/userSvc';
const panelNames = { const panelNames = {
menu: 'Menu', menu: 'Menu',
help: 'Markdown help', help: 'Markdown cheat sheet',
toc: 'Table of Contents', toc: 'Table of contents',
}; };
export default { export default {
components: { components: {
Toc, Toc,
MenuItem, SideBarItem,
}, },
data: () => ({ data: () => ({
panel: 'menu', panel: 'menu',
panelNames: { panelNames: {
menu: 'Menu', menu: 'Menu',
toc: 'Table of Contents', toc: 'Table of Contents',
help: 'Markdown cheat sheet',
}, },
markdownSample: markdownConversionSvc.highlight(markdownSample),
}), }),
computed: { computed: {
panelName() { panelName() {
@ -69,6 +75,9 @@ export default {
...mapActions('data', [ ...mapActions('data', [
'toggleSideBar', 'toggleSideBar',
]), ]),
signin() {
userSvc.signinWithGoogle();
},
}, },
}; };
</script> </script>
@ -82,18 +91,38 @@ export default {
} }
.side-bar__inner { .side-bar__inner {
overflow: auto; position: relative;
height: 100%; height: 100%;
padding: 10px 0;
} }
.side-bar__panel { .side-bar__panel {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: auto;
} }
.side-bar__panel--hidden { .side-bar__panel--hidden {
left: 1000px; left: 1000px;
} }
.side-bar__panel--help {
padding: 0 10px 40px 20px;
pre {
font-size: 0.9em;
font-variant-ligatures: no-common-ligatures;
line-height: 1.25;
white-space: pre-wrap;
word-break: break-word;
word-wrap: break-word;
}
.code,
.img,
.imgref,
.cl-toc {
background-color: rgba(0, 0, 0, 0.05);
}
}
</style> </style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="menu-item button flex flex--row flex--align-center"> <div class="side-bar-item button flex flex--row flex--align-center">
<div class="menu-item__icon flex flex--column flex--center"> <div class="side-bar-item__icon flex flex--column flex--center">
<slot name="icon"></slot> <slot name="icon"></slot>
</div> </div>
<div class="flex flex--column"> <div class="flex flex--column">
@ -10,7 +10,7 @@
</template> </template>
<style lang="scss"> <style lang="scss">
.menu-item { .side-bar-item {
text-align: left; text-align: left;
padding: 10px 12px; padding: 10px 12px;
height: auto; height: auto;
@ -25,7 +25,7 @@
} }
} }
.menu-item__icon { .side-bar-item__icon {
height: 20px; height: 20px;
width: 20px; width: 20px;
margin-right: 12px; margin-right: 12px;

View File

@ -4,3 +4,98 @@
</div> </div>
</div> </div>
</template> </template>
<script>
import editorSvc from '../services/editorSvc';
export default {
mounted() {
const tocElt = this.$el.querySelector('.toc__inner');
// TOC click behaviour
let isMousedown;
function onClick(e) {
if (!isMousedown) {
return;
}
e.preventDefault();
const y = e.clientY - tocElt.getBoundingClientRect().top;
editorSvc.sectionDescList.some((sectionDesc) => {
if (y >= sectionDesc.tocDimension.endOffset) {
return false;
}
const posInSection = (y - sectionDesc.tocDimension.startOffset)
/ (sectionDesc.tocDimension.height || 1);
const editorScrollTop = sectionDesc.editorDimension.startOffset
+ (sectionDesc.editorDimension.height * posInSection);
editorSvc.editorElt.parentNode.scrollTop = editorScrollTop;
const previewScrollTop = sectionDesc.previewDimension.startOffset
+ (sectionDesc.previewDimension.height * posInSection);
editorSvc.previewElt.parentNode.scrollTop = previewScrollTop;
return true;
});
}
tocElt.addEventListener('mouseup', () => {
isMousedown = false;
});
tocElt.addEventListener('mouseleave', () => {
isMousedown = false;
});
tocElt.addEventListener('mousedown', (e) => {
isMousedown = e.which === 1;
onClick(e);
});
tocElt.addEventListener('mousemove', (e) => {
onClick(e);
});
},
};
</script>
<style lang="scss">
.toc__inner {
color: rgba(0, 0, 0, 0.75);
cursor: pointer;
font-size: 10px;
padding: 10px 20px;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
* {
font-weight: inherit;
pointer-events: none;
}
.cl-toc-section {
* {
margin: 0.25em 0;
padding: 0.25em 0;
}
h2 {
margin-left: 8px;
}
h3 {
margin-left: 16px;
}
h4 {
margin-left: 24px;
}
h5 {
margin-left: 32px;
}
h6 {
margin-left: 40px;
}
}
}
</style>

View File

@ -3,7 +3,7 @@
@import './markdownHighlighting'; @import './markdownHighlighting';
body { body {
background-color: #f3f3f3; background-color: #fff;
top: 0; top: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
@ -110,6 +110,11 @@ textarea {
justify-content: center; justify-content: center;
} }
.flex--end {
-webkit-justify-content: flex-end;
justify-content: flex-end;
}
.flex--space-between { .flex--space-between {
-webkit-justify-content: space-between; -webkit-justify-content: space-between;
justify-content: space-between; justify-content: space-between;
@ -123,7 +128,7 @@ textarea {
.side-title { .side-title {
height: 44px; height: 44px;
line-height: 36px; line-height: 36px;
padding: 4px 6px 0; padding: 4px 4px 0;
background-color: rgba(0, 0, 0, 0.1); background-color: rgba(0, 0, 0, 0.1);
-webkit-flex: none; -webkit-flex: none;
flex: none; flex: none;
@ -136,6 +141,8 @@ textarea {
display: inline-block; display: inline-block;
background-color: transparent; background-color: transparent;
opacity: 0.75; opacity: 0.75;
-webkit-flex: none;
flex: none;
/* prevent from seeing wrapped buttons */ /* prevent from seeing wrapped buttons */
margin-bottom: 20px; margin-bottom: 20px;
@ -150,4 +157,8 @@ textarea {
.side-title__title { .side-title__title {
text-transform: uppercase; text-transform: uppercase;
padding: 0 5px; padding: 0 5px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
} }

View File

@ -1,5 +1,5 @@
Headers Headers
---------------------------- ---------------------------
# Header 1 # Header 1
@ -9,7 +9,7 @@ Headers
Styling Styling
---------------------------- ---------------------------
*Emphasize* _emphasize_ *Emphasize* _emphasize_
@ -25,7 +25,7 @@ H~2~O is a liquid.
Lists Lists
---------------------------- ---------------------------
- Item - Item
* Item * Item
@ -37,7 +37,7 @@ Lists
Links Links
---------------------------- ---------------------------
A [link](http://example.com). A [link](http://example.com).
@ -45,7 +45,7 @@ An image: ![Alt](img.jpg)
Code Code
---------------------------- ---------------------------
Some `inline code`. Some `inline code`.
@ -61,7 +61,7 @@ var foo = 'bar';
Tables Tables
---------------------------- ---------------------------
Item | Value Item | Value
-------- | ----- -------- | -----
@ -76,7 +76,7 @@ Pipe | $1
Definition lists Definition lists
---------------------------- ---------------------------
Markdown Markdown
: Text-to-HTML conversion tool : Text-to-HTML conversion tool
@ -86,7 +86,7 @@ Classeur
: A Markdown editing app : A Markdown editing app
Footnotes Footnotes
---------------------------- ---------------------------
Some text with a footnote.[^1] Some text with a footnote.[^1]
@ -94,7 +94,7 @@ Some text with a footnote.[^1]
Abbreviations Abbreviations
---------------------------- ---------------------------
Markdown converts text to HTML. Markdown converts text to HTML.
@ -102,7 +102,7 @@ Markdown converts text to HTML.
LaTeX math LaTeX math
---------------------------- ---------------------------
The Gamma function satisfying $\Gamma(n) = (n-1)!\quad\forall The Gamma function satisfying $\Gamma(n) = (n-1)!\quad\forall
n\in\mathbb N$ is via the Euler integral n\in\mathbb N$ is via the Euler integral

View File

@ -1,10 +1,10 @@
import Vue from 'vue'; import Vue from 'vue';
import App from './components/App';
import store from './store';
import './services/syncSvc';
import './extensions/'; import './extensions/';
import './services/syncSvc';
import './services/optional'; import './services/optional';
import './icons/'; import './icons/';
import App from './components/App';
import store from './store';
Vue.config.productionTip = false; Vue.config.productionTip = false;

View File

@ -93,10 +93,6 @@ class Connection {
request.onsuccess = () => { request.onsuccess = () => {
tx.txCounter = request.result ? request.result.tx : 0; tx.txCounter = request.result ? request.result.tx : 0;
tx.txCounter += 1; tx.txCounter += 1;
dbStore.put({
id: 'txCounter',
tx: tx.txCounter,
});
cb(tx); cb(tx);
}; };
} }
@ -137,45 +133,38 @@ export default {
* Read and apply all changes from the DB since previous transaction. * Read and apply all changes from the DB since previous transaction.
*/ */
readAll(storeItemMap, tx, cb) { readAll(storeItemMap, tx, cb) {
let resetMap; let lastTx = this.lastTx;
// We may have missed some delete markers
if (this.lastTx && tx.txCounter - this.lastTx > deleteMarkerMaxAge) {
// 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 dbStore = tx.objectStore(dbStoreName);
const index = dbStore.index('tx'); const index = dbStore.index('tx');
const range = window.IDBKeyRange.lowerBound(this.lastTx, true); const range = window.IDBKeyRange.lowerBound(this.lastTx, true);
const items = []; const changes = [];
const itemsToDelete = [];
index.openCursor(range).onsuccess = (event) => { index.openCursor(range).onsuccess = (event) => {
const cursor = event.target.result; const cursor = event.target.result;
if (cursor) { if (cursor) {
const item = cursor.value; const item = cursor.value;
items.push(item); if (item.tx > lastTx) {
// Remove old delete markers lastTx = item.tx;
if (!item.updated && tx.txCounter - item.tx > deleteMarkerMaxAge) { if (this.lastTx && item.tx - this.lastTx > deleteMarkerMaxAge) {
itemsToDelete.push(item); // We may have missed some delete markers
window.location.reload();
return;
} }
}
// Collect change
changes.push(item);
cursor.continue(); cursor.continue();
} else { } else {
itemsToDelete.forEach((item) => { if (changes.length) {
dbg(`Got ${changes.length} changes`);
}
changes.forEach((item) => {
this.readDbItem(item, storeItemMap);
// If item is an old delete marker, remove it from the DB
if (!item.updated && lastTx - item.tx > deleteMarkerMaxAge) {
dbStore.delete(item.id); 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); this.lastTx = lastTx;
}
items.forEach(item => this.readDbItem(item, storeItemMap));
cb(); cb();
} }
}; };
@ -185,8 +174,8 @@ export default {
* Write all changes from the store since previous transaction. * Write all changes from the store since previous transaction.
*/ */
writeAll(storeItemMap, tx) { writeAll(storeItemMap, tx) {
this.lastTx = tx.txCounter;
const dbStore = tx.objectStore(dbStoreName); const dbStore = tx.objectStore(dbStoreName);
const incrementedTx = this.lastTx + 1;
// Remove deleted store items // Remove deleted store items
Object.keys(this.updatedMap).forEach((id) => { Object.keys(this.updatedMap).forEach((id) => {
@ -194,9 +183,10 @@ export default {
// Put a delete marker to notify other tabs // Put a delete marker to notify other tabs
dbStore.put({ dbStore.put({
id, id,
tx: this.lastTx, tx: incrementedTx,
}); });
delete this.updatedMap[id]; delete this.updatedMap[id];
this.lastTx = incrementedTx; // No need to read what we just wrote
} }
}); });
@ -207,11 +197,12 @@ export default {
if (this.updatedMap[storeItem.id] !== storeItem.updated) { if (this.updatedMap[storeItem.id] !== storeItem.updated) {
const item = { const item = {
...storeItem, ...storeItem,
tx: this.lastTx, tx: incrementedTx,
}; };
dbg('Putting 1 item'); dbg('Putting 1 item');
dbStore.put(item); dbStore.put(item);
this.updatedMap[item.id] = item.updated; this.updatedMap[item.id] = item.updated;
this.lastTx = incrementedTx; // No need to read what we just wrote
} }
}); });
}, },
@ -220,18 +211,18 @@ export default {
* Read and apply one DB change. * Read and apply one DB change.
*/ */
readDbItem(dbItem, storeItemMap) { readDbItem(dbItem, storeItemMap) {
const existingStoreItem = storeItemMap[dbItem.id];
if (!dbItem.updated) { if (!dbItem.updated) {
// DB item is a delete marker
delete this.updatedMap[dbItem.id]; delete this.updatedMap[dbItem.id];
const existingStoreItem = storeItemMap[dbItem.id];
if (existingStoreItem) { if (existingStoreItem) {
const prefix = getStorePrefixFromType(existingStoreItem.type);
if (prefix) {
delete storeItemMap[existingStoreItem.id]; delete storeItemMap[existingStoreItem.id];
// Remove object from the store // Remove object from the store
const prefix = getStorePrefixFromType(existingStoreItem.type);
store.commit(`${prefix}/deleteItem`, existingStoreItem.id); store.commit(`${prefix}/deleteItem`, existingStoreItem.id);
} }
}
} else if (this.updatedMap[dbItem.id] !== dbItem.updated) { } else if (this.updatedMap[dbItem.id] !== dbItem.updated) {
// DB item is different from the corresponding store item
this.updatedMap[dbItem.id] = dbItem.updated; this.updatedMap[dbItem.id] = dbItem.updated;
storeItemMap[dbItem.id] = dbItem; storeItemMap[dbItem.id] = dbItem;
// Put object in the store // Put object in the store

View File

@ -255,6 +255,20 @@ const markdownConversionSvc = {
htmlSectionDiff, htmlSectionDiff,
}; };
}, },
/**
* Helper to highlight arbitrary markdown
* @param {Object} markdown The markdown content to highlight.
* @param {Object} converter An optional converter.
* @param {Object} grammars Optional grammars.
* @returns {Object} The highlighted markdown in HTML format.
*/
highlight(markdown, converter = this.defaultConverter, grammars = this.defaultPrismGrammars) {
const parsingCtx = this.parseSections(converter, markdown);
return parsingCtx.sections.map(
section => Prism.highlight(section.text, grammars[section.data]),
).join('');
},
}; };
markdownConversionSvc.defaultConverter = markdownConversionSvc.createConverter(defaultOptions); markdownConversionSvc.defaultConverter = markdownConversionSvc.createConverter(defaultOptions);

63
src/services/userSvc.js Normal file
View File

@ -0,0 +1,63 @@
import utils from './utils';
const googleClientId = '241271498917-t4t7d07qis7oc0ahaskbif3ft6tk63cd.apps.googleusercontent.com';
const appUri = 'http://localhost:8080/';
const googleAppsDomain = null;
const origin = `${location.protocol}//${location.host}`;
export default {
oauth2Context: null,
signinWithGoogle() {
this.cleanOauth2Context();
const state = utils.uid();
let authorizeUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
authorizeUrl = utils.addQueryParam(authorizeUrl, 'client_id', googleClientId);
authorizeUrl = utils.addQueryParam(authorizeUrl, 'response_type', 'code');
authorizeUrl = utils.addQueryParam(authorizeUrl, 'redirect_uri', `${appUri}oauth2/google/callback`);
authorizeUrl = utils.addQueryParam(authorizeUrl, 'state', state);
if (googleAppsDomain) {
authorizeUrl = utils.addQueryParam(authorizeUrl, 'scope', 'openid email');
authorizeUrl = utils.addQueryParam(authorizeUrl, 'hd', googleAppsDomain);
} else {
authorizeUrl = utils.addQueryParam(authorizeUrl, 'scope', 'profile email');
}
const wnd = window.open(authorizeUrl);
if (!wnd) {
return Promise.resolve();
}
return new Promise((resolve) => {
const msgHandler = (event) => {
if (event.source === wnd
&& event.origin === origin
&& event.data
&& event.data.state === state
) {
this.cleanOauth2Context();
console.log(event.data);
resolve();
}
};
window.addEventListener('message', msgHandler);
const checkClosedInterval = setInterval(() => {
if (this.oauth2Context && this.oauth2Context.wnd.closed) {
this.cleanOauth2Context();
}
}, 200);
this.oauth2Context = {
wnd,
msgHandler,
checkClosedInterval,
};
});
},
cleanOauth2Context() {
if (this.oauth2Context) {
clearInterval(this.oauth2Context.checkClosedInterval);
if (!this.oauth2Context.wnd.closed) {
this.oauth2Context.wnd.close();
}
window.removeEventListener('message', this.oauth2Context.msgHandler);
this.oauth2Context = null;
}
},
};

View File

@ -2,6 +2,7 @@ const crypto = window.crypto || window.msCrypto;
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const radix = alphabet.length; const radix = alphabet.length;
const array = new Uint32Array(20); const array = new Uint32Array(20);
const urlParser = window.document.createElement('a');
export default { export default {
uid() { uid() {
@ -12,4 +13,17 @@ export default {
const randomizedInterval = Math.floor((1 + ((Math.random() - 0.5) * 0.1)) * interval); const randomizedInterval = Math.floor((1 + ((Math.random() - 0.5) * 0.1)) * interval);
setInterval(() => func(), randomizedInterval); setInterval(() => func(), randomizedInterval);
}, },
addQueryParam(url, key, value) {
if (!url || !key || !value) {
return url;
}
urlParser.href = url;
if (urlParser.search) {
urlParser.search += '&';
} else {
urlParser.search = '?';
}
urlParser.search += `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
return urlParser.href;
},
}; };

View File

@ -7,37 +7,15 @@ const navigationBarLeftWidth = 570;
const maxTitleMaxWidth = 800; const maxTitleMaxWidth = 800;
const minTitleMaxWidth = 200; const minTitleMaxWidth = 200;
const setter = propertyName => (state, value) => { const constants = {
state[propertyName] = value;
};
export default {
namespaced: true,
state: {
constants: {
explorerWidth: 250, explorerWidth: 250,
sideBarWidth: 280, sideBarWidth: 280,
navigationBarHeight: 44, navigationBarHeight: 44,
buttonBarWidth: 30, buttonBarWidth: 30,
statusBarHeight: 20, statusBarHeight: 20,
}, };
editorWidthFactor: 1,
fontSizeFactor: 1, function computeStyles(state, localSettings, styles = {
bodyWidth: 0,
bodyHeight: 0,
},
mutations: {
setEditorWidthFactor: setter('editorWidthFactor'),
setFontSizeFactor: setter('fontSizeFactor'),
updateBodySize: (state) => {
state.bodyWidth = document.body.clientWidth;
state.bodyHeight = document.body.clientHeight;
},
},
getters: {
styles: (state, getters, rootState, rootGetters) => {
const localSettings = rootGetters['data/localSettings'];
const styles = {
showNavigationBar: !localSettings.showEditor || localSettings.showNavigationBar, showNavigationBar: !localSettings.showEditor || localSettings.showNavigationBar,
showStatusBar: localSettings.showStatusBar, showStatusBar: localSettings.showStatusBar,
showEditor: localSettings.showEditor, showEditor: localSettings.showEditor,
@ -45,45 +23,33 @@ export default {
showPreview: localSettings.showSidePreview || !localSettings.showEditor, showPreview: localSettings.showSidePreview || !localSettings.showEditor,
showSideBar: localSettings.showSideBar, showSideBar: localSettings.showSideBar,
showExplorer: localSettings.showExplorer, showExplorer: localSettings.showExplorer,
}; }) {
function computeStyles() {
styles.innerHeight = state.bodyHeight; styles.innerHeight = state.bodyHeight;
if (styles.showNavigationBar) { if (styles.showNavigationBar) {
styles.innerHeight -= state.constants.navigationBarHeight; styles.innerHeight -= constants.navigationBarHeight;
} }
if (styles.showStatusBar) { if (styles.showStatusBar) {
styles.innerHeight -= state.constants.statusBarHeight; styles.innerHeight -= constants.statusBarHeight;
} }
styles.innerWidth = state.bodyWidth; styles.innerWidth = state.bodyWidth;
if (styles.showSideBar) { if (styles.showSideBar) {
styles.innerWidth -= state.constants.sideBarWidth; styles.innerWidth -= constants.sideBarWidth;
} }
if (styles.showExplorer) { if (styles.showExplorer) {
styles.innerWidth -= state.constants.explorerWidth; styles.innerWidth -= constants.explorerWidth;
} }
let doublePanelWidth = styles.innerWidth - state.constants.buttonBarWidth; let doublePanelWidth = styles.innerWidth - constants.buttonBarWidth;
if (doublePanelWidth < editorMinWidth) { if (doublePanelWidth < editorMinWidth) {
if (styles.showSideBar) {
styles.showSideBar = false;
computeStyles();
return;
}
if (styles.showExplorer) {
styles.showExplorer = false;
computeStyles();
return;
}
doublePanelWidth = editorMinWidth; doublePanelWidth = editorMinWidth;
styles.innerWidth = editorMinWidth + constants.buttonBarWidth;
} }
if (styles.showSidePreview && doublePanelWidth / 2 < editorMinWidth) { if (styles.showSidePreview && doublePanelWidth / 2 < editorMinWidth) {
styles.showSidePreview = false; styles.showSidePreview = false;
styles.showPreview = false; styles.showPreview = false;
computeStyles(); return computeStyles(state, localSettings, styles);
return;
} }
styles.fontSize = 18; styles.fontSize = 18;
@ -129,10 +95,34 @@ export default {
} }
styles.titleMaxWidth = Math.min(styles.titleMaxWidth, maxTitleMaxWidth); styles.titleMaxWidth = Math.min(styles.titleMaxWidth, maxTitleMaxWidth);
styles.titleMaxWidth = Math.max(styles.titleMaxWidth, minTitleMaxWidth); styles.titleMaxWidth = Math.max(styles.titleMaxWidth, minTitleMaxWidth);
return styles;
} }
computeStyles(); const setter = propertyName => (state, value) => {
return styles; state[propertyName] = value;
};
export default {
namespaced: true,
state: {
editorWidthFactor: 1,
fontSizeFactor: 1,
bodyWidth: 0,
bodyHeight: 0,
},
mutations: {
setEditorWidthFactor: setter('editorWidthFactor'),
setFontSizeFactor: setter('fontSizeFactor'),
updateBodySize: (state) => {
state.bodyWidth = document.body.clientWidth;
state.bodyHeight = document.body.clientHeight;
},
},
getters: {
constants: () => constants,
styles: (state, getters, rootState, rootGetters) => {
const localSettings = rootGetters['data/localSettings'];
return computeStyles(state, localSettings);
}, },
}, },
}; };