Stackedit/src/components/NavigationBar.vue
2017-09-23 20:01:50 +01:00

426 lines
11 KiB
Vue

<template>
<div class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor}">
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
<button class="navigation-bar__button button" @click="toggleExplorer()">
<icon-folder-open></icon-folder-open>
</button>
</div>
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button">
<button class="navigation-bar__button navigation-bar__button--stackedit button" @click="toggleSideBar()">
<icon-provider provider-id="stackedit"></icon-provider>
</button>
</div>
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--title flex flex--row">
<div class="navigation-bar__spinner">
<div v-show="showSpinner" class="spinner"></div>
</div>
<div class="navigation-bar__title navigation-bar__title--fake text-input"></div>
<div class="navigation-bar__title navigation-bar__title--text text-input" :style="{width: titleWidth + 'px'}">{{title}}</div>
<input class="navigation-bar__title navigation-bar__title--input text-input" :class="{'navigation-bar__title--focus': titleFocus, 'navigation-bar__title--scrolling': titleScrolling}" :style="{width: titleWidth + 'px'}" @focus="editTitle(true)" @blur="editTitle(false)" @keyup.enter="submitTitle()" @keyup.esc="submitTitle(true)" @mouseenter="titleHover = true" @mouseleave="titleHover = false" v-model="title">
<div class="flex flex--row" :class="{'navigation-bar__hidden': styles.hideLocations}">
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in syncLocations" :key="location.id" :href="location.url" target="_blank">
<icon-provider :provider-id="location.providerId"></icon-provider>
</a>
<button class="navigation-bar__button navigation-bar__button--sync button" v-if="!offline && isSyncPossible" :disabled="isSyncRequested" @click="requestSync">
<icon-sync></icon-sync>
</button>
<button class="navigation-bar__button navigation-bar__button--sync-off button" v-if="offline && isSyncPossible" disabled="disabled">
<icon-sync-off></icon-sync-off>
</button>
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in publishLocations" :key="location.id" :href="location.url" target="_blank">
<icon-provider :provider-id="location.providerId"></icon-provider>
</a>
<button class="navigation-bar__button navigation-bar__button--publish button" v-if="publishLocations.length" :disabled="isPublishRequested || offline" @click="requestPublish">
<icon-upload></icon-upload>
</button>
</div>
</div>
<div class="navigation-bar__inner navigation-bar__inner--edit-buttons">
<button class="navigation-bar__button button" @click="pagedownClick('bold')">
<icon-format-bold></icon-format-bold>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('italic')">
<icon-format-italic></icon-format-italic>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('strikethrough')">
<icon-format-strikethrough></icon-format-strikethrough>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('heading')">
<icon-format-size></icon-format-size>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('ulist')">
<icon-format-list-bulleted></icon-format-list-bulleted>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('olist')">
<icon-format-list-numbers></icon-format-list-numbers>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('table')">
<icon-table></icon-table>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('quote')">
<icon-format-quote-close></icon-format-quote-close>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('code')">
<icon-code-tags></icon-code-tags>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('link')">
<icon-link-variant></icon-link-variant>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('image')">
<icon-file-image></icon-file-image>
</button>
<button class="navigation-bar__button button" @click="pagedownClick('hr')">
<icon-format-horizontal-rule></icon-format-horizontal-rule>
</button>
</div>
</div>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import editorSvc from '../services/editorSvc';
import syncSvc from '../services/syncSvc';
import publishSvc from '../services/publishSvc';
import animationSvc from '../services/animationSvc';
export default {
data: () => ({
mounted: false,
title: '',
titleFocus: false,
titleHover: false,
}),
computed: {
...mapState([
'offline',
]),
...mapState('queue', [
'isSyncRequested',
'isPublishRequested',
'currentLocation',
]),
...mapGetters('layout', [
'styles',
]),
...mapGetters('syncLocation', {
syncLocations: 'current',
}),
...mapGetters('publishLocation', {
publishLocations: 'current',
}),
isSyncPossible() {
return this.$store.getters['data/loginToken'] ||
this.$store.getters['syncLocation/current'].length;
},
showSpinner() {
return !this.$store.state.queue.isEmpty;
},
titleWidth() {
if (!this.mounted) {
return 0;
}
this.titleFakeElt.textContent = this.title;
const width = this.titleFakeElt.getBoundingClientRect().width + 2; // 2px for the caret
return width < this.styles.titleMaxWidth
? width
: this.styles.titleMaxWidth;
},
titleScrolling() {
const result = this.titleHover && !this.titleFocus;
if (this.titleInputElt) {
if (result) {
const scrollLeft = this.titleInputElt.scrollWidth - this.titleInputElt.offsetWidth;
animationSvc.animate(this.titleInputElt)
.scrollLeft(scrollLeft)
.duration(scrollLeft * 10)
.easing('inOut')
.start();
} else {
animationSvc.animate(this.titleInputElt)
.scrollLeft(0)
.start();
}
}
return result;
},
},
methods: {
...mapActions('data', [
'toggleExplorer',
'toggleSideBar',
]),
requestSync() {
if (!this.isSyncRequested) {
syncSvc.requestSync();
}
},
requestPublish() {
if (!this.isPublishRequested) {
publishSvc.requestPublish();
}
},
pagedownClick(name) {
editorSvc.pagedownEditor.uiManager.doClick(name);
},
editTitle(toggle) {
this.titleFocus = toggle;
if (toggle) {
this.titleInputElt.setSelectionRange(0, this.titleInputElt.value.length);
} else {
const title = this.title.trim();
if (title) {
this.$store.dispatch('file/patchCurrent', { name: title.slice(0, 250) });
} else {
this.title = this.$store.getters['file/current'].name;
}
}
},
submitTitle(reset) {
if (reset) {
this.title = '';
}
this.titleInputElt.blur();
},
},
created() {
this.$store.watch(
() => this.$store.getters['file/current'].name,
(name) => {
this.title = name;
}, { immediate: true });
},
mounted() {
this.titleFakeElt = this.$el.querySelector('.navigation-bar__title--fake');
this.titleInputElt = this.$el.querySelector('.navigation-bar__title--input');
this.mounted = true;
},
};
</script>
<style lang="scss">
@import 'common/variables.scss';
.navigation-bar {
position: absolute;
width: 100%;
height: 100%;
padding-top: 4px;
overflow: hidden;
}
.navigation-bar__hidden {
display: none;
}
.navigation-bar__inner--left {
float: left;
&.navigation-bar__inner--button {
margin-right: 15px;
}
}
.navigation-bar__inner--right {
float: right;
}
.navigation-bar__inner--button {
margin: 0 4px;
}
.navigation-bar__inner--edit-buttons {
margin-left: 15px;
.navigation-bar__button {
float: left;
}
}
.navigation-bar__inner--title * {
flex: none;
}
.navigation-bar__button {
width: 36px;
height: 36px;
padding: 0 8px;
/* prevent from seeing wrapped buttons */
margin-bottom: 20px;
.navigation-bar__inner--button & {
padding: 0 4px;
width: 38px;
&.navigation-bar__button--stackedit {
opacity: 0.8;
&:active,
&:focus,
&:hover {
opacity: 1;
}
}
}
}
.navigation-bar__title {
margin: 0 4px;
}
.navigation-bar__title,
.navigation-bar__button {
display: inline-block;
color: $navbar-color;
background-color: transparent;
font-size: 22px;
}
.navigation-bar__button--sync,
.navigation-bar__button--sync-off,
.navigation-bar__button--publish {
padding: 0 6px;
margin: 0 5px;
}
.navigation-bar__button[disabled] {
&,
&:active,
&:focus,
&:hover {
color: $navbar-color;
}
}
.navigation-bar__button--sync-off[disabled] {
&,
&:active,
&:focus,
&:hover {
color: $error-color;
}
}
.navigation-bar__title--input,
.navigation-bar__button {
&:active,
&:focus,
&:hover {
color: $navbar-hover-color;
background-color: $navbar-hover-background;
}
}
.navigation-bar__button--location {
width: 20px;
padding: 0 2px 12px;
opacity: 0.5;
&:active,
&:focus,
&:hover {
background-color: transparent;
opacity: 1;
}
}
.navigation-bar__button--blink {
animation: blink 1s linear infinite;
}
@keyframes blink {
50% {
opacity: 1;
filter: contrast(0.8) brightness(1.25);
}
}
.navigation-bar__title--fake {
position: absolute;
left: -9999px;
width: auto;
white-space: pre-wrap;
}
.navigation-bar__title--text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
.navigation-bar--editor & {
display: none;
}
}
.navigation-bar__title--input,
.navigation-bar__inner--edit-buttons,
.navigation-bar__inner--button,
.navigation-bar__spinner {
display: none;
.navigation-bar--editor & {
display: block;
}
}
.navigation-bar__button {
display: none;
.navigation-bar--editor & {
display: inline-block;
}
}
.navigation-bar__title--input {
cursor: pointer;
&.navigation-bar__title--focus {
cursor: text;
}
}
$r: 9px;
$d: $r * 2;
$b: $d/10;
$t: 1500ms;
.navigation-bar__spinner {
width: $d;
margin: 10px 5px 0 10px;
color: rgba(255, 255, 255, 0.67);
}
.spinner {
width: $d;
height: $d;
display: block;
position: relative;
border: $b solid currentColor;
border-radius: 50%;
&::before,
&::after {
content: "";
position: absolute;
display: block;
width: $b;
background-color: currentColor;
border-radius: $b * 0.5;
transform-origin: 50% 0;
}
&::before {
height: $r * 0.5;
left: $r - $b * 1.5;
top: 50%;
animation: spin $t linear infinite;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>