<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" 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"> <button class="navigation-bar__button navigation-bar__button--stackedit button" 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 utils from '../services/utils'; export default { data: () => ({ mounted: false, title: '', titleFocus: false, titleHover: false, }), computed: { ...mapState([ '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 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: { ...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(); }, }, 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; /* 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; .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: 22px; .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>