TOC
This commit is contained in:
parent
8b3e3a898b
commit
d09375dc4c
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
@ -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>
|
||||||
|
@ -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%;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
63
src/services/userSvc.js
Normal 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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
@ -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;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
}
|
|
||||||
|
|
||||||
computeStyles();
|
|
||||||
return styles;
|
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);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user