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';
.app__spash-screen {
position: absolute;
width: 100%;
margin: 0 auto;
max-width: 600px;
height: 100%;
background: no-repeat center url('../assets/logo.svg');
background-color: #fff;
background-size: contain;
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<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' }">
<explorer></explorer>
</div>
@ -31,7 +31,7 @@
</template>
<script>
import { mapState, mapGetters, mapMutations } from 'vuex';
import { mapGetters, mapMutations } from 'vuex';
import NavigationBar from './NavigationBar';
import ButtonBar from './ButtonBar';
import StatusBar from './StatusBar';
@ -52,10 +52,8 @@ export default {
Preview,
},
computed: {
...mapState('layout', [
'constants',
]),
...mapGetters('layout', [
'constants',
'styles',
]),
},
@ -77,48 +75,12 @@ export default {
const previewElt = this.$el.querySelector('.preview__inner-2');
const tocElt = this.$el.querySelector('.toc__inner');
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() {
window.removeEventListener('resize', this.updateStyle);
window.removeEventListener('keyup', this.saveSelection);
window.removeEventListener('mouseup', this.saveSelection);
window.removeEventListener('contextmenu', this.saveSelection);
},
};
</script>
@ -152,6 +114,11 @@ export default {
background-color: #fff;
}
.layout__panel--button-bar,
.layout__panel--preview {
background-color: #f3f3f3;
}
.layout__panel--explorer,
.layout__panel--side-bar {
background-color: #dadada;

View File

@ -1,33 +1,34 @@
<template>
<div class="side-bar flex flex--column">
<div class="side-title flex flex--row flex--space-between">
<div class="flex flex--row">
<div class="side-title flex flex--row">
<button v-if="panel !== 'menu'" class="side-title__button button" @click="panel = 'menu'">
<icon-arrow-left></icon-arrow-left>
</button>
<div class="side-title__title">
{{panelName}}
</div>
</div>
<button class="side-title__button button" @click="toggleSideBar(false)">
<icon-close></icon-close>
</button>
</div>
<div class="side-bar__inner">
<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>
<div>Sign in with Google</div>
<span>Have all your files and settings backed up and synced.</span>
</menu-item>
<menu-item @click.native="panel = 'toc'">
</side-bar-item>
<side-bar-item @click.native="panel = 'toc'">
<icon-toc slot="icon"></icon-toc>
Table of Contents
</menu-item>
<menu-item @click.native="panel = 'help'">
Table of contents
</side-bar-item>
<side-bar-item @click.native="panel = 'help'">
<icon-help-circle slot="icon"></icon-help-circle>
Markdown help
</menu-item>
Markdown cheat sheet
</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 class="side-bar__panel side-bar__panel--toc" :class="{'side-bar__panel--hidden': panel !== 'toc'}">
<toc>
@ -40,25 +41,30 @@
<script>
import { mapActions } from 'vuex';
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 = {
menu: 'Menu',
help: 'Markdown help',
toc: 'Table of Contents',
help: 'Markdown cheat sheet',
toc: 'Table of contents',
};
export default {
components: {
Toc,
MenuItem,
SideBarItem,
},
data: () => ({
panel: 'menu',
panelNames: {
menu: 'Menu',
toc: 'Table of Contents',
help: 'Markdown cheat sheet',
},
markdownSample: markdownConversionSvc.highlight(markdownSample),
}),
computed: {
panelName() {
@ -69,6 +75,9 @@ export default {
...mapActions('data', [
'toggleSideBar',
]),
signin() {
userSvc.signinWithGoogle();
},
},
};
</script>
@ -82,18 +91,38 @@ export default {
}
.side-bar__inner {
overflow: auto;
position: relative;
height: 100%;
padding: 10px 0;
}
.side-bar__panel {
position: absolute;
width: 100%;
height: 100%;
overflow: auto;
}
.side-bar__panel--hidden {
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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -255,6 +255,20 @@ const markdownConversionSvc = {
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);

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 radix = alphabet.length;
const array = new Uint32Array(20);
const urlParser = window.document.createElement('a');
export default {
uid() {
@ -12,4 +13,17 @@ export default {
const randomizedInterval = Math.floor((1 + ((Math.random() - 0.5) * 0.1)) * interval);
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 minTitleMaxWidth = 200;
const setter = propertyName => (state, value) => {
state[propertyName] = value;
};
export default {
namespaced: true,
state: {
constants: {
const constants = {
explorerWidth: 250,
sideBarWidth: 280,
navigationBarHeight: 44,
buttonBarWidth: 30,
statusBarHeight: 20,
},
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: {
styles: (state, getters, rootState, rootGetters) => {
const localSettings = rootGetters['data/localSettings'];
const styles = {
};
function computeStyles(state, localSettings, styles = {
showNavigationBar: !localSettings.showEditor || localSettings.showNavigationBar,
showStatusBar: localSettings.showStatusBar,
showEditor: localSettings.showEditor,
@ -45,45 +23,33 @@ export default {
showPreview: localSettings.showSidePreview || !localSettings.showEditor,
showSideBar: localSettings.showSideBar,
showExplorer: localSettings.showExplorer,
};
function computeStyles() {
}) {
styles.innerHeight = state.bodyHeight;
if (styles.showNavigationBar) {
styles.innerHeight -= state.constants.navigationBarHeight;
styles.innerHeight -= constants.navigationBarHeight;
}
if (styles.showStatusBar) {
styles.innerHeight -= state.constants.statusBarHeight;
styles.innerHeight -= constants.statusBarHeight;
}
styles.innerWidth = state.bodyWidth;
if (styles.showSideBar) {
styles.innerWidth -= state.constants.sideBarWidth;
styles.innerWidth -= constants.sideBarWidth;
}
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 (styles.showSideBar) {
styles.showSideBar = false;
computeStyles();
return;
}
if (styles.showExplorer) {
styles.showExplorer = false;
computeStyles();
return;
}
doublePanelWidth = editorMinWidth;
styles.innerWidth = editorMinWidth + constants.buttonBarWidth;
}
if (styles.showSidePreview && doublePanelWidth / 2 < editorMinWidth) {
styles.showSidePreview = false;
styles.showPreview = false;
computeStyles();
return;
return computeStyles(state, localSettings, styles);
}
styles.fontSize = 18;
@ -129,10 +95,34 @@ export default {
}
styles.titleMaxWidth = Math.min(styles.titleMaxWidth, maxTitleMaxWidth);
styles.titleMaxWidth = Math.max(styles.titleMaxWidth, minTitleMaxWidth);
return styles;
}
computeStyles();
return styles;
const setter = propertyName => (state, value) => {
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);
},
},
};