Stackedit/src/components/NavigationBar.vue

511 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<nav class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor && !revisionContent, 'navigation-bar--light': light}">
<!-- Explorer -->
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
<button class="navigation-bar__button button" v-if="light" @click="close()" v-title="'Close StackEdit'"><icon-close-circle></icon-close-circle></button>
<button class="navigation-bar__button navigation-bar__button--explorer-toggler button" v-else tour-step-anchor="explorer" @click="toggleExplorer()" v-title="'Toggle explorer'"><icon-folder></icon-folder></button>
</div>
<!-- Side bar -->
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button">
<a class="navigation-bar__button navigation-bar__button--stackedit button" v-if="light" href="app" target="_blank" v-title="'Open StackEdit'"><icon-provider provider-id="stackedit"></icon-provider></a>
<button class="navigation-bar__button navigation-bar__button--stackedit button" v-else tour-step-anchor="menu" @click="toggleSideBar()" v-title="'Toggle side bar'"><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">
<!-- Spinner -->
<div class="navigation-bar__spinner">
<div v-if="!offline && showSpinner" class="spinner"></div>
<icon-sync-off v-if="offline"></icon-sync-off>
</div>
<!-- Title -->
<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)" @keydown.enter="submitTitle(false)" @keydown.esc="submitTitle(true)" @mouseenter="titleHover = true" @mouseleave="titleHover = false" v-model="title">
<!-- Sync/Publish -->
<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" v-title="'Synchronized location'"><icon-provider :provider-id="location.providerId"></icon-provider></a>
<button class="navigation-bar__button navigation-bar__button--sync button" :disabled="!isSyncPossible || isSyncRequested || offline" @click="requestSync" v-title="'Synchronize now'"><icon-sync></icon-sync></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" v-title="'Publish location'"><icon-provider :provider-id="location.providerId"></icon-provider></a>
<button class="navigation-bar__button navigation-bar__button--publish button" :disabled="!publishLocations.length || isPublishRequested || offline" @click="requestPublish" v-title="'Publish now'"><icon-upload></icon-upload></button>
</div>
<!-- Revision -->
<div class="flex flex--row" v-if="revisionContent">
<button class="navigation-bar__button navigation-bar__button--revision navigation-bar__button--restore button" @click="restoreRevision">Restore</button>
<button class="navigation-bar__button navigation-bar__button--revision button" @click="setRevisionContent()" v-title="'Close revision'"><icon-close></icon-close></button>
</div>
</div>
<div class="navigation-bar__inner navigation-bar__inner--edit-pagedownButtons">
<button class="navigation-bar__button button" @click="undo" v-title="'Undo'" :disabled="!canUndo"><icon-undo></icon-undo></button>
<button class="navigation-bar__button button" @click="redo" v-title="'Redo'" :disabled="!canRedo"><icon-redo></icon-redo></button>
<div v-for="button in pagedownButtons" :key="button.method">
<button class="navigation-bar__button button" v-if="button.method" @click="pagedownClick(button.method)" v-title="button.titleWithShortcut">
<component :is="button.iconClass"></component>
</button>
<div class="navigation-bar__spacer" v-else></div>
</div>
</div>
</nav>
</template>
<script>
import { mapState, mapMutations, mapGetters, mapActions } from 'vuex';
import editorSvc from '../services/editorSvc';
import syncSvc from '../services/syncSvc';
import publishSvc from '../services/publishSvc';
import animationSvc from '../services/animationSvc';
import tempFileSvc from '../services/tempFileSvc';
import utils from '../services/utils';
import pagedownButtons from '../data/pagedownButtons';
import store from '../store';
import workspaceSvc from '../services/workspaceSvc';
// According to mousetrap
const mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl';
const getShortcut = (method) => {
let result = '';
Object.entries(store.getters['data/computedSettings'].shortcuts).some(([keys, shortcut]) => {
if (`${shortcut.method || shortcut}` === method) {
result = keys.split('+').map(key => key.toLowerCase()).map((key) => {
if (key === 'mod') {
return mod;
}
// Capitalize
return key && `${key[0].toUpperCase()}${key.slice(1)}`;
}).join('+');
}
return result;
});
return result && ` ${result}`;
};
export default {
data: () => ({
mounted: false,
title: '',
titleFocus: false,
titleHover: false,
}),
computed: {
...mapState([
'light',
'offline',
]),
...mapState('queue', [
'isSyncRequested',
'isPublishRequested',
'currentLocation',
]),
...mapState('layout', [
'canUndo',
'canRedo',
]),
...mapState('content', [
'revisionContent',
]),
...mapGetters('layout', [
'styles',
]),
...mapGetters('syncLocation', {
syncLocations: 'current',
}),
...mapGetters('publishLocation', {
publishLocations: 'current',
}),
pagedownButtons() {
return pagedownButtons.map(button => ({
...button,
titleWithShortcut: `${button.title}${getShortcut(button.method)}`,
iconClass: `icon-${button.icon}`,
}));
},
isSyncPossible() {
return this.$store.getters['workspace/syncToken'] ||
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 Math.min(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;
},
editCancelTrigger() {
const current = this.$store.getters['file/current'];
return utils.serializeObject([
current.id,
current.name,
]);
},
},
methods: {
...mapMutations('content', [
'setRevisionContent',
]),
...mapActions('content', [
'restoreRevision',
]),
...mapActions('data', [
'toggleExplorer',
'toggleSideBar',
]),
undo() {
return editorSvc.clEditor.undoMgr.undo();
},
redo() {
return editorSvc.clEditor.undoMgr.redo();
},
requestSync() {
if (this.isSyncPossible && !this.isSyncRequested) {
syncSvc.requestSync();
}
},
requestPublish() {
if (this.publishLocations.length && !this.isPublishRequested) {
publishSvc.requestPublish();
}
},
pagedownClick(name) {
if (this.$store.getters['content/isCurrentEditable']) {
editorSvc.pagedownEditor.uiManager.doClick(name);
}
},
async editTitle(toggle) {
this.titleFocus = toggle;
if (toggle) {
this.titleInputElt.setSelectionRange(0, this.titleInputElt.value.length);
} else {
const title = this.title.trim();
this.title = this.$store.getters['file/current'].name;
if (title) {
try {
await workspaceSvc.storeItem({
...this.$store.getters['file/current'],
name: title,
});
} catch (e) {
// Cancel
}
}
}
},
submitTitle(reset) {
if (reset) {
this.title = '';
}
this.titleInputElt.blur();
},
close() {
tempFileSvc.close();
},
},
created() {
this.$watch(
() => this.editCancelTrigger,
() => {
this.title = '';
this.editTitle(false);
},
{ 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 '../styles/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: 12px;
}
}
.navigation-bar__inner--right {
float: right;
/* prevent from seeing wrapped pagedownButtons */
margin-bottom: 20px;
}
.navigation-bar__inner--button {
margin: 0 4px;
}
.navigation-bar__inner--edit-pagedownButtons {
margin-left: 15px;
.navigation-bar__button,
.navigation-bar__spacer {
float: left;
}
}
.navigation-bar__inner--title * {
flex: none;
}
.navigation-bar__button,
.navigation-bar__spacer {
height: 36px;
padding: 0 4px;
/* prevent from seeing wrapped pagedownButtons */
margin-bottom: 20px;
}
.navigation-bar__button {
width: 34px;
padding: 0 7px;
transition: opacity 0.25s;
.navigation-bar__inner--button & {
padding: 0 4px;
width: 38px;
&.navigation-bar__button--stackedit {
opacity: 0.85;
&:active,
&:focus,
&:hover {
opacity: 1;
}
}
}
}
.navigation-bar__button--revision {
width: 38px;
&:first-child {
margin-left: 10px;
}
&:last-child {
margin-right: 10px;
}
}
.navigation-bar__button--restore {
width: auto;
}
.navigation-bar__title {
margin: 0 4px;
font-size: 21px;
.layout--revision & {
position: absolute;
left: -9999px;
}
}
.navigation-bar__title,
.navigation-bar__button {
display: inline-block;
color: $navbar-color;
background-color: transparent;
}
.navigation-bar__button--sync,
.navigation-bar__button--publish {
padding: 0 6px;
margin: 0 5px;
}
.navigation-bar__button[disabled] {
&,
&:active,
&:focus,
&:hover {
color: $navbar-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;
height: 20px;
border-radius: 10px;
padding: 2px;
margin-top: 8px;
opacity: 0.5;
background-color: rgba(255, 255, 255, 0.2);
&:active,
&:focus,
&:hover {
opacity: 1;
background-color: rgba(255, 255, 255, 0.2);
}
}
.navigation-bar__button--blink {
animation: blink 1s linear infinite;
}
.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-pagedownButtons {
display: none;
.navigation-bar--editor & {
display: block;
}
}
.navigation-bar__button {
display: none;
.navigation-bar__inner--button &,
.navigation-bar--editor & {
display: inline-block;
}
}
.navigation-bar__button--revision {
display: inline-block;
}
.navigation-bar__title--input {
cursor: pointer;
&.navigation-bar__title--focus {
cursor: text;
}
.navigation-bar--light & {
display: none;
}
}
$r: 10px;
$d: $r * 2;
$b: $d/10;
$t: 3000ms;
.navigation-bar__spinner {
width: 24px;
margin: 7px 0 0 8px;
.icon {
width: 24px;
height: 24px;
color: transparentize($error-color, 0.5);
}
}
.spinner {
width: $d;
height: $d;
display: block;
position: relative;
border: $b solid transparentize($navbar-color, 0.5);
border-radius: 50%;
margin: 2px;
&::before,
&::after {
content: "";
position: absolute;
display: block;
width: $b;
background-color: $navbar-color;
border-radius: $b * 0.5;
transform-origin: 50% 0;
}
&::before {
height: $r * 0.4;
left: $r - $b * 1.5;
top: 50%;
animation: spin $t linear infinite;
}
&::after {
height: $r * 0.6;
left: $r - $b * 1.5;
top: 50%;
animation: spin $t/4 linear infinite;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes blink {
50% {
opacity: 1;
}
}
</style>