TOC
This commit is contained in:
parent
8b3e3a898b
commit
d09375dc4c
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -1,13 +1,11 @@
|
||||
<template>
|
||||
<div class="side-bar flex flex--column">
|
||||
<div class="side-title flex flex--row flex--space-between">
|
||||
<div class="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 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>
|
||||
<button class="side-title__button button" @click="toggleSideBar(false)">
|
||||
<icon-close></icon-close>
|
||||
@ -15,19 +13,22 @@
|
||||
</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>
|
||||
|
@ -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;
|
@ -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>
|
||||
|
@ -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%;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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) => {
|
||||
dbStore.delete(item.id);
|
||||
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) {
|
||||
delete storeItemMap[existingStoreItem.id];
|
||||
// Remove object from the store
|
||||
const prefix = getStorePrefixFromType(existingStoreItem.type);
|
||||
if (prefix) {
|
||||
delete storeItemMap[existingStoreItem.id];
|
||||
// Remove object from the store
|
||||
store.commit(`${prefix}/deleteItem`, existingStoreItem.id);
|
||||
}
|
||||
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
|
||||
|
@ -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
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 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;
|
||||
},
|
||||
};
|
||||
|
@ -7,6 +7,97 @@ const navigationBarLeftWidth = 570;
|
||||
const maxTitleMaxWidth = 800;
|
||||
const minTitleMaxWidth = 200;
|
||||
|
||||
const constants = {
|
||||
explorerWidth: 250,
|
||||
sideBarWidth: 280,
|
||||
navigationBarHeight: 44,
|
||||
buttonBarWidth: 30,
|
||||
statusBarHeight: 20,
|
||||
};
|
||||
|
||||
function computeStyles(state, localSettings, styles = {
|
||||
showNavigationBar: !localSettings.showEditor || localSettings.showNavigationBar,
|
||||
showStatusBar: localSettings.showStatusBar,
|
||||
showEditor: localSettings.showEditor,
|
||||
showSidePreview: localSettings.showSidePreview && localSettings.showEditor,
|
||||
showPreview: localSettings.showSidePreview || !localSettings.showEditor,
|
||||
showSideBar: localSettings.showSideBar,
|
||||
showExplorer: localSettings.showExplorer,
|
||||
}) {
|
||||
styles.innerHeight = state.bodyHeight;
|
||||
if (styles.showNavigationBar) {
|
||||
styles.innerHeight -= constants.navigationBarHeight;
|
||||
}
|
||||
if (styles.showStatusBar) {
|
||||
styles.innerHeight -= constants.statusBarHeight;
|
||||
}
|
||||
|
||||
styles.innerWidth = state.bodyWidth;
|
||||
if (styles.showSideBar) {
|
||||
styles.innerWidth -= constants.sideBarWidth;
|
||||
}
|
||||
if (styles.showExplorer) {
|
||||
styles.innerWidth -= constants.explorerWidth;
|
||||
}
|
||||
|
||||
let doublePanelWidth = styles.innerWidth - constants.buttonBarWidth;
|
||||
if (doublePanelWidth < editorMinWidth) {
|
||||
doublePanelWidth = editorMinWidth;
|
||||
styles.innerWidth = editorMinWidth + constants.buttonBarWidth;
|
||||
}
|
||||
|
||||
if (styles.showSidePreview && doublePanelWidth / 2 < editorMinWidth) {
|
||||
styles.showSidePreview = false;
|
||||
styles.showPreview = false;
|
||||
return computeStyles(state, localSettings, styles);
|
||||
}
|
||||
|
||||
styles.fontSize = 18;
|
||||
styles.textWidth = 990;
|
||||
if (doublePanelWidth < 1120) {
|
||||
styles.fontSize -= 1;
|
||||
styles.textWidth = 910;
|
||||
}
|
||||
if (doublePanelWidth < 1040) {
|
||||
styles.textWidth = 830;
|
||||
}
|
||||
styles.textWidth *= state.editorWidthFactor;
|
||||
if (doublePanelWidth < styles.textWidth) {
|
||||
styles.textWidth = doublePanelWidth;
|
||||
}
|
||||
if (styles.textWidth < 640) {
|
||||
styles.fontSize -= 1;
|
||||
}
|
||||
styles.fontSize *= state.fontSizeFactor;
|
||||
|
||||
const bottomPadding = Math.floor(styles.innerHeight / 2);
|
||||
const panelWidth = Math.floor(doublePanelWidth / 2);
|
||||
styles.previewWidth = styles.showSidePreview ?
|
||||
panelWidth :
|
||||
styles.innerWidth;
|
||||
const previewLeftPadding = Math.max(
|
||||
Math.floor((styles.previewWidth - styles.textWidth) / 2), minPadding);
|
||||
let previewRightPadding = previewLeftPadding;
|
||||
if (!styles.showEditor && previewRightPadding < previewButtonWidth) {
|
||||
previewRightPadding = previewButtonWidth;
|
||||
}
|
||||
styles.previewPadding = `${editorTopPadding}px ${previewRightPadding}px ${bottomPadding}px ${previewLeftPadding}px`;
|
||||
styles.editorWidth = styles.showSidePreview ?
|
||||
panelWidth :
|
||||
doublePanelWidth;
|
||||
const editorSidePadding = Math.max(
|
||||
Math.floor((styles.editorWidth - styles.textWidth) / 2), minPadding);
|
||||
styles.editorPadding = `${editorTopPadding}px ${editorSidePadding}px ${bottomPadding}px`;
|
||||
|
||||
styles.titleMaxWidth = styles.innerWidth - navigationBarSpaceWidth;
|
||||
if (styles.showEditor) {
|
||||
styles.titleMaxWidth -= navigationBarLeftWidth;
|
||||
}
|
||||
styles.titleMaxWidth = Math.min(styles.titleMaxWidth, maxTitleMaxWidth);
|
||||
styles.titleMaxWidth = Math.max(styles.titleMaxWidth, minTitleMaxWidth);
|
||||
return styles;
|
||||
}
|
||||
|
||||
const setter = propertyName => (state, value) => {
|
||||
state[propertyName] = value;
|
||||
};
|
||||
@ -14,13 +105,6 @@ const setter = propertyName => (state, value) => {
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
constants: {
|
||||
explorerWidth: 250,
|
||||
sideBarWidth: 280,
|
||||
navigationBarHeight: 44,
|
||||
buttonBarWidth: 30,
|
||||
statusBarHeight: 20,
|
||||
},
|
||||
editorWidthFactor: 1,
|
||||
fontSizeFactor: 1,
|
||||
bodyWidth: 0,
|
||||
@ -35,104 +119,10 @@ export default {
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
constants: () => constants,
|
||||
styles: (state, getters, rootState, rootGetters) => {
|
||||
const localSettings = rootGetters['data/localSettings'];
|
||||
const styles = {
|
||||
showNavigationBar: !localSettings.showEditor || localSettings.showNavigationBar,
|
||||
showStatusBar: localSettings.showStatusBar,
|
||||
showEditor: localSettings.showEditor,
|
||||
showSidePreview: localSettings.showSidePreview && localSettings.showEditor,
|
||||
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;
|
||||
}
|
||||
if (styles.showStatusBar) {
|
||||
styles.innerHeight -= state.constants.statusBarHeight;
|
||||
}
|
||||
|
||||
styles.innerWidth = state.bodyWidth;
|
||||
if (styles.showSideBar) {
|
||||
styles.innerWidth -= state.constants.sideBarWidth;
|
||||
}
|
||||
if (styles.showExplorer) {
|
||||
styles.innerWidth -= state.constants.explorerWidth;
|
||||
}
|
||||
|
||||
let doublePanelWidth = styles.innerWidth - state.constants.buttonBarWidth;
|
||||
if (doublePanelWidth < editorMinWidth) {
|
||||
if (styles.showSideBar) {
|
||||
styles.showSideBar = false;
|
||||
computeStyles();
|
||||
return;
|
||||
}
|
||||
if (styles.showExplorer) {
|
||||
styles.showExplorer = false;
|
||||
computeStyles();
|
||||
return;
|
||||
}
|
||||
doublePanelWidth = editorMinWidth;
|
||||
}
|
||||
|
||||
if (styles.showSidePreview && doublePanelWidth / 2 < editorMinWidth) {
|
||||
styles.showSidePreview = false;
|
||||
styles.showPreview = false;
|
||||
computeStyles();
|
||||
return;
|
||||
}
|
||||
|
||||
styles.fontSize = 18;
|
||||
styles.textWidth = 990;
|
||||
if (doublePanelWidth < 1120) {
|
||||
styles.fontSize -= 1;
|
||||
styles.textWidth = 910;
|
||||
}
|
||||
if (doublePanelWidth < 1040) {
|
||||
styles.textWidth = 830;
|
||||
}
|
||||
styles.textWidth *= state.editorWidthFactor;
|
||||
if (doublePanelWidth < styles.textWidth) {
|
||||
styles.textWidth = doublePanelWidth;
|
||||
}
|
||||
if (styles.textWidth < 640) {
|
||||
styles.fontSize -= 1;
|
||||
}
|
||||
styles.fontSize *= state.fontSizeFactor;
|
||||
|
||||
const bottomPadding = Math.floor(styles.innerHeight / 2);
|
||||
const panelWidth = Math.floor(doublePanelWidth / 2);
|
||||
styles.previewWidth = styles.showSidePreview ?
|
||||
panelWidth :
|
||||
styles.innerWidth;
|
||||
const previewLeftPadding = Math.max(
|
||||
Math.floor((styles.previewWidth - styles.textWidth) / 2), minPadding);
|
||||
let previewRightPadding = previewLeftPadding;
|
||||
if (!styles.showEditor && previewRightPadding < previewButtonWidth) {
|
||||
previewRightPadding = previewButtonWidth;
|
||||
}
|
||||
styles.previewPadding = `${editorTopPadding}px ${previewRightPadding}px ${bottomPadding}px ${previewLeftPadding}px`;
|
||||
styles.editorWidth = styles.showSidePreview ?
|
||||
panelWidth :
|
||||
doublePanelWidth;
|
||||
const editorSidePadding = Math.max(
|
||||
Math.floor((styles.editorWidth - styles.textWidth) / 2), minPadding);
|
||||
styles.editorPadding = `${editorTopPadding}px ${editorSidePadding}px ${bottomPadding}px`;
|
||||
|
||||
styles.titleMaxWidth = styles.innerWidth - navigationBarSpaceWidth;
|
||||
if (styles.showEditor) {
|
||||
styles.titleMaxWidth -= navigationBarLeftWidth;
|
||||
}
|
||||
styles.titleMaxWidth = Math.min(styles.titleMaxWidth, maxTitleMaxWidth);
|
||||
styles.titleMaxWidth = Math.max(styles.titleMaxWidth, minTitleMaxWidth);
|
||||
}
|
||||
|
||||
computeStyles();
|
||||
return styles;
|
||||
return computeStyles(state, localSettings);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user