470 lines
14 KiB
Vue
470 lines
14 KiB
Vue
<template>
|
|
<nav class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor && !revisionContent}">
|
|
<!-- 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></icon-close></button>
|
|
<button class="navigation-bar__button 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()" @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-buttons">
|
|
<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 class="navigation-bar__spacer"></div>
|
|
<button class="navigation-bar__button button" @click="pagedownClick('bold')" v-title="'Bold'"><icon-format-bold></icon-format-bold></button>
|
|
<button class="navigation-bar__button button" @click="pagedownClick('italic')" v-title="'Italic'"><icon-format-italic></icon-format-italic></button>
|
|
<button class="navigation-bar__button button" @click="pagedownClick('strikethrough')" v-title="'Strikethrough'"><icon-format-strikethrough></icon-format-strikethrough></button>
|
|
<button class="navigation-bar__button button" @click="pagedownClick('heading')" v-title="'Heading'"><icon-format-size></icon-format-size></button>
|
|
<button class="navigation-bar__button button" @click="pagedownClick('ulist')" v-title="'Unordered list'"><icon-format-list-bulleted></icon-format-list-bulleted></button>
|
|
<button class="navigation-bar__button button" @click="pagedownClick('olist')" v-title="'Ordered list'"><icon-format-list-numbers></icon-format-list-numbers></button>
|
|
<button class="navigation-bar__button button" @click="pagedownClick('table')" v-title="'Table'"><icon-table></icon-table></button>
|
|
<button class="navigation-bar__button button" @click="pagedownClick('quote')" v-title="'Blockquote'"><icon-format-quote-close></icon-format-quote-close></button>
|
|
<button class="navigation-bar__button button" @click="pagedownClick('code')" v-title="'Code'"><icon-code-tags></icon-code-tags></button>
|
|
<button class="navigation-bar__button button" @click="pagedownClick('link')" v-title="'Link'"><icon-link-variant></icon-link-variant></button>
|
|
<button class="navigation-bar__button button" @click="pagedownClick('image')" v-title="'Image'"><icon-file-image></icon-file-image></button>
|
|
<button class="navigation-bar__button button" @click="pagedownClick('hr')" v-title="'Horizontal rule'"><icon-format-horizontal-rule></icon-format-horizontal-rule></button>
|
|
</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';
|
|
|
|
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',
|
|
}),
|
|
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;
|
|
},
|
|
},
|
|
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);
|
|
}
|
|
},
|
|
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: utils.sanitizeName(title) });
|
|
} else {
|
|
this.title = this.$store.getters['file/current'].name;
|
|
}
|
|
}
|
|
},
|
|
submitTitle(reset) {
|
|
if (reset) {
|
|
this.title = '';
|
|
}
|
|
this.titleInputElt.blur();
|
|
},
|
|
close() {
|
|
tempFileSvc.close();
|
|
},
|
|
},
|
|
created() {
|
|
this.$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: 12px;
|
|
}
|
|
}
|
|
|
|
.navigation-bar__inner--right {
|
|
float: right;
|
|
|
|
/* prevent from seeing wrapped buttons */
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.navigation-bar__inner--button {
|
|
margin: 0 4px;
|
|
}
|
|
|
|
.navigation-bar__inner--edit-buttons {
|
|
margin-left: 15px;
|
|
|
|
.navigation-bar__button,
|
|
.navigation-bar__spacer {
|
|
float: left;
|
|
}
|
|
}
|
|
|
|
.navigation-bar__inner--title * {
|
|
flex: none;
|
|
}
|
|
|
|
$button-size: 36px;
|
|
|
|
.navigation-bar__button,
|
|
.navigation-bar__spacer {
|
|
height: $button-size;
|
|
padding: 0 4px;
|
|
|
|
/* prevent from seeing wrapped buttons */
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.navigation-bar__button {
|
|
width: $button-size;
|
|
padding: 0 8px;
|
|
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-buttons {
|
|
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;
|
|
}
|
|
}
|
|
|
|
$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>
|