Support for ARIA. Added trash
This commit is contained in:
parent
cbb44f4ea6
commit
74bceaf1ee
@ -22,7 +22,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bezier-easing": "^1.1.0",
|
"bezier-easing": "^1.1.0",
|
||||||
"clipboard": "^1.7.1",
|
"clipboard": "^1.7.1",
|
||||||
"clunderscore": "^1.0.3",
|
|
||||||
"compression": "^1.7.0",
|
"compression": "^1.7.0",
|
||||||
"diff-match-patch": "^1.0.0",
|
"diff-match-patch": "^1.0.0",
|
||||||
"file-saver": "^1.3.3",
|
"file-saver": "^1.3.3",
|
||||||
|
@ -22,6 +22,32 @@ Vue.directive('focus', {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setVisible = (el, value) => {
|
||||||
|
el.style.display = value ? '' : 'none';
|
||||||
|
if (value) {
|
||||||
|
el.removeAttribute('aria-hidden');
|
||||||
|
} else {
|
||||||
|
el.setAttribute('aria-hidden', 'true');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Vue.directive('show', {
|
||||||
|
bind(el, { value }) {
|
||||||
|
setVisible(el, value);
|
||||||
|
},
|
||||||
|
update(el, { value, oldValue }) {
|
||||||
|
if (value !== oldValue) {
|
||||||
|
setVisible(el, value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Vue.directive('title', {
|
||||||
|
bind(el, { value }) {
|
||||||
|
el.title = value;
|
||||||
|
el.setAttribute('aria-label', value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Layout,
|
Layout,
|
||||||
|
@ -1,24 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="button-bar">
|
<div class="button-bar">
|
||||||
<div class="button-bar__inner button-bar__inner--top">
|
<div class="button-bar__inner button-bar__inner--top">
|
||||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showNavigationBar }" @click="toggleNavigationBar()">
|
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showNavigationBar }" @click="toggleNavigationBar()" v-title="'Toggle navigation bar'">
|
||||||
<icon-navigation-bar></icon-navigation-bar>
|
<icon-navigation-bar></icon-navigation-bar>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showSidePreview }" @click="toggleSidePreview()">
|
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showSidePreview }" @click="toggleSidePreview()" v-title="'Toggle side preview'">
|
||||||
<icon-side-preview></icon-side-preview>
|
<icon-side-preview></icon-side-preview>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-bar__button" @click="toggleEditor(false)">
|
<div class="button-bar__button" @click="toggleEditor(false)" v-title="'Reader mode'">
|
||||||
<icon-eye></icon-eye>
|
<icon-eye></icon-eye>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-bar__inner button-bar__inner--bottom">
|
<div class="button-bar__inner button-bar__inner--bottom">
|
||||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.focusMode }" @click="toggleFocusMode()">
|
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.focusMode }" @click="toggleFocusMode()" v-title="'Toggle focus mode'">
|
||||||
<icon-target></icon-target>
|
<icon-target></icon-target>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.scrollSync }" @click="toggleScrollSync()">
|
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.scrollSync }" @click="toggleScrollSync()" v-title="'Toggle scroll sync'">
|
||||||
<icon-scroll-sync></icon-scroll-sync>
|
<icon-scroll-sync></icon-scroll-sync>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showStatusBar }" @click="toggleStatusBar()">
|
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showStatusBar }" @click="toggleStatusBar()" v-title="'Toggle status bar'">
|
||||||
<icon-status-bar></icon-status-bar>
|
<icon-status-bar></icon-status-bar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,20 +2,20 @@
|
|||||||
<div class="explorer flex flex--column">
|
<div class="explorer flex flex--column">
|
||||||
<div class="side-title flex flex--row flex--space-between">
|
<div class="side-title flex flex--row flex--space-between">
|
||||||
<div class="flex flex--row">
|
<div class="flex flex--row">
|
||||||
<button class="side-title__button button" @click="newItem()">
|
<button class="side-title__button button" @click="newItem()" v-title="'New file'">
|
||||||
<icon-file-plus></icon-file-plus>
|
<icon-file-plus></icon-file-plus>
|
||||||
</button>
|
</button>
|
||||||
<button class="side-title__button button" @click="newItem(true)">
|
<button class="side-title__button button" @click="newItem(true)" v-title="'New folder'">
|
||||||
<icon-folder-plus></icon-folder-plus>
|
<icon-folder-plus></icon-folder-plus>
|
||||||
</button>
|
</button>
|
||||||
<button class="side-title__button button" @click="editItem()">
|
<button class="side-title__button button" @click="editItem()" v-title="'Rename'">
|
||||||
<icon-pen></icon-pen>
|
<icon-pen></icon-pen>
|
||||||
</button>
|
</button>
|
||||||
<button class="side-title__button button" @click="deleteItem()">
|
<button class="side-title__button button" @click="deleteItem()" v-title="'Remove'">
|
||||||
<icon-delete></icon-delete>
|
<icon-delete></icon-delete>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="side-title__button button" @click="toggleExplorer(false)">
|
<button class="side-title__button button" @click="toggleExplorer(false)" v-title="'Close explorer'">
|
||||||
<icon-close></icon-close>
|
<icon-close></icon-close>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<div class="layout__panel flex flex--row" :class="{'flex--end': styles.showSideBar}">
|
<div class="layout__panel flex flex--row" :class="{'flex--end': styles.showSideBar}">
|
||||||
<div class="layout__panel layout__panel--explorer" v-show="styles.showExplorer" :style="{ width: constants.explorerWidth + 'px' }">
|
<div class="layout__panel layout__panel--explorer" v-show="styles.showExplorer" :aria-hidden="!styles.showExplorer" :style="{ width: constants.explorerWidth + 'px' }">
|
||||||
<explorer></explorer>
|
<explorer></explorer>
|
||||||
</div>
|
</div>
|
||||||
<div class="layout__panel flex flex--column" :style="{ width: styles.innerWidth + 'px' }">
|
<div class="layout__panel flex flex--column" :style="{ width: styles.innerWidth + 'px' }">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal" @keyup.esc="onEscape">
|
<div class="modal" @keyup.esc="onEscape" @keydown.tab="onTab">
|
||||||
<file-properties-modal v-if="config.type === 'fileProperties'"></file-properties-modal>
|
<file-properties-modal v-if="config.type === 'fileProperties'"></file-properties-modal>
|
||||||
<settings-modal v-else-if="config.type === 'settings'"></settings-modal>
|
<settings-modal v-else-if="config.type === 'settings'"></settings-modal>
|
||||||
<templates-modal v-else-if="config.type === 'templates'"></templates-modal>
|
<templates-modal v-else-if="config.type === 'templates'"></templates-modal>
|
||||||
@ -66,6 +66,10 @@ import BloggerPagePublishModal from './modals/BloggerPagePublishModal';
|
|||||||
import ZendeskAccountModal from './modals/ZendeskAccountModal';
|
import ZendeskAccountModal from './modals/ZendeskAccountModal';
|
||||||
import ZendeskPublishModal from './modals/ZendeskPublishModal';
|
import ZendeskPublishModal from './modals/ZendeskPublishModal';
|
||||||
|
|
||||||
|
const getTabbables = container => container.querySelectorAll('a[href], button, .textfield')
|
||||||
|
// Filter enabled and visible element
|
||||||
|
.cl_filter(el => !el.disabled && el.offsetParent !== null);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
FilePropertiesModal,
|
FilePropertiesModal,
|
||||||
@ -102,6 +106,18 @@ export default {
|
|||||||
this.config.reject();
|
this.config.reject();
|
||||||
editorEngineSvc.clEditor.focus();
|
editorEngineSvc.clEditor.focus();
|
||||||
},
|
},
|
||||||
|
onTab(evt) {
|
||||||
|
const tabbables = getTabbables(this.$el);
|
||||||
|
const firstTabbable = tabbables[0];
|
||||||
|
const lastTabbable = tabbables[tabbables.length - 1];
|
||||||
|
if (evt.shiftKey && firstTabbable === evt.target) {
|
||||||
|
evt.preventDefault();
|
||||||
|
lastTabbable.focus();
|
||||||
|
} else if (!evt.shiftKey && lastTabbable === evt.target) {
|
||||||
|
evt.preventDefault();
|
||||||
|
firstTabbable.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
onFocusInOut(evt) {
|
onFocusInOut(evt) {
|
||||||
const isFocusIn = evt.type === 'focusin';
|
const isFocusIn = evt.type === 'focusin';
|
||||||
if (evt.target.parentNode && evt.target.parentNode.parentNode) {
|
if (evt.target.parentNode && evt.target.parentNode.parentNode) {
|
||||||
@ -127,12 +143,8 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener('focusin', this.onFocusInOut);
|
window.addEventListener('focusin', this.onFocusInOut);
|
||||||
window.addEventListener('focusout', this.onFocusInOut);
|
window.addEventListener('focusout', this.onFocusInOut);
|
||||||
const eltToFocus = this.$el.querySelector('input.text-input')
|
const tabbables = getTabbables(this.$el);
|
||||||
|| this.$el.querySelector('.textfield')
|
tabbables[0].focus();
|
||||||
|| this.$el.querySelector('.button');
|
|
||||||
if (eltToFocus) {
|
|
||||||
eltToFocus.focus();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
window.removeEventListener('focusin', this.onFocusInOut);
|
window.removeEventListener('focusin', this.onFocusInOut);
|
||||||
@ -237,6 +249,10 @@ export default {
|
|||||||
.form-entry--focused & {
|
.form-entry--focused & {
|
||||||
color: darken($link-color, 10%);
|
color: darken($link-color, 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-entry--error & {
|
||||||
|
color: darken($error-color, 10%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-entry__field {
|
.form-entry__field {
|
||||||
@ -248,6 +264,10 @@ export default {
|
|||||||
.form-entry--focused & {
|
.form-entry--focused & {
|
||||||
border-color: $link-color;
|
border-color: $link-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-entry--error & {
|
||||||
|
border-color: $error-color;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-entry__actions {
|
.form-entry__actions {
|
||||||
@ -286,4 +306,51 @@ export default {
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
margin: 0.25em 0;
|
margin: 0.25em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
border-bottom: 1px solid $hr-color;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs__tab {
|
||||||
|
width: 50%;
|
||||||
|
float: left;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs__tab > a {
|
||||||
|
width: 100%;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0.67em 0.33em;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
border-top-left-radius: $border-radius-base;
|
||||||
|
border-top-right-radius: $border-radius-base;
|
||||||
|
color: $link-color;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
background-color: rgba(0, 0, 0, 0.067);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs__tab--active > a {
|
||||||
|
border-bottom: 2px solid $link-color;
|
||||||
|
color: inherit;
|
||||||
|
cursor: auto;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor}">
|
<div class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor}">
|
||||||
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
|
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
|
||||||
<button class="navigation-bar__button button" @click="toggleExplorer()">
|
<button class="navigation-bar__button button" @click="toggleExplorer()" v-title="'Toggle explorer'">
|
||||||
<icon-folder></icon-folder>
|
<icon-folder></icon-folder>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button">
|
<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()">
|
<button class="navigation-bar__button navigation-bar__button--stackedit button" @click="toggleSideBar()" v-title="'Toggle side bar'">
|
||||||
<icon-provider provider-id="stackedit"></icon-provider>
|
<icon-provider provider-id="stackedit"></icon-provider>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -19,55 +19,55 @@
|
|||||||
<div class="navigation-bar__title navigation-bar__title--text text-input" :style="{width: titleWidth + 'px'}">{{title}}</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">
|
<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}">
|
<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">
|
<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>
|
<icon-provider :provider-id="location.providerId"></icon-provider>
|
||||||
</a>
|
</a>
|
||||||
<button class="navigation-bar__button navigation-bar__button--sync button" :disabled="!isSyncPossible || isSyncRequested || offline" @click="requestSync">
|
<button class="navigation-bar__button navigation-bar__button--sync button" :disabled="!isSyncPossible || isSyncRequested || offline" @click="requestSync" v-title="'Synchronize now'">
|
||||||
<icon-sync></icon-sync>
|
<icon-sync></icon-sync>
|
||||||
</button>
|
</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">
|
<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>
|
<icon-provider :provider-id="location.providerId"></icon-provider>
|
||||||
</a>
|
</a>
|
||||||
<button class="navigation-bar__button navigation-bar__button--publish button" :disabled="!publishLocations.length || isPublishRequested || offline" @click="requestPublish">
|
<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>
|
<icon-upload></icon-upload>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="navigation-bar__inner navigation-bar__inner--edit-buttons">
|
<div class="navigation-bar__inner navigation-bar__inner--edit-buttons">
|
||||||
<button class="navigation-bar__button button" @click="pagedownClick('bold')">
|
<button class="navigation-bar__button button" @click="pagedownClick('bold')" v-title="'Bold'">
|
||||||
<icon-format-bold></icon-format-bold>
|
<icon-format-bold></icon-format-bold>
|
||||||
</button>
|
</button>
|
||||||
<button class="navigation-bar__button button" @click="pagedownClick('italic')">
|
<button class="navigation-bar__button button" @click="pagedownClick('italic')" v-title="'Italic'">
|
||||||
<icon-format-italic></icon-format-italic>
|
<icon-format-italic></icon-format-italic>
|
||||||
</button>
|
</button>
|
||||||
<button class="navigation-bar__button button" @click="pagedownClick('strikethrough')">
|
<button class="navigation-bar__button button" @click="pagedownClick('strikethrough')" v-title="'Strikethrough'">
|
||||||
<icon-format-strikethrough></icon-format-strikethrough>
|
<icon-format-strikethrough></icon-format-strikethrough>
|
||||||
</button>
|
</button>
|
||||||
<button class="navigation-bar__button button" @click="pagedownClick('heading')">
|
<button class="navigation-bar__button button" @click="pagedownClick('heading')" v-title="'Heading'">
|
||||||
<icon-format-size></icon-format-size>
|
<icon-format-size></icon-format-size>
|
||||||
</button>
|
</button>
|
||||||
<button class="navigation-bar__button button" @click="pagedownClick('ulist')">
|
<button class="navigation-bar__button button" @click="pagedownClick('ulist')" v-title="'Unordered list'">
|
||||||
<icon-format-list-bulleted></icon-format-list-bulleted>
|
<icon-format-list-bulleted></icon-format-list-bulleted>
|
||||||
</button>
|
</button>
|
||||||
<button class="navigation-bar__button button" @click="pagedownClick('olist')">
|
<button class="navigation-bar__button button" @click="pagedownClick('olist')" v-title="'Ordered list'">
|
||||||
<icon-format-list-numbers></icon-format-list-numbers>
|
<icon-format-list-numbers></icon-format-list-numbers>
|
||||||
</button>
|
</button>
|
||||||
<button class="navigation-bar__button button" @click="pagedownClick('table')">
|
<button class="navigation-bar__button button" @click="pagedownClick('table')" v-title="'Table'">
|
||||||
<icon-table></icon-table>
|
<icon-table></icon-table>
|
||||||
</button>
|
</button>
|
||||||
<button class="navigation-bar__button button" @click="pagedownClick('quote')">
|
<button class="navigation-bar__button button" @click="pagedownClick('quote')" v-title="'Blockquote'">
|
||||||
<icon-format-quote-close></icon-format-quote-close>
|
<icon-format-quote-close></icon-format-quote-close>
|
||||||
</button>
|
</button>
|
||||||
<button class="navigation-bar__button button" @click="pagedownClick('code')">
|
<button class="navigation-bar__button button" @click="pagedownClick('code')" v-title="'Code'">
|
||||||
<icon-code-tags></icon-code-tags>
|
<icon-code-tags></icon-code-tags>
|
||||||
</button>
|
</button>
|
||||||
<button class="navigation-bar__button button" @click="pagedownClick('link')">
|
<button class="navigation-bar__button button" @click="pagedownClick('link')" v-title="'Link'">
|
||||||
<icon-link-variant></icon-link-variant>
|
<icon-link-variant></icon-link-variant>
|
||||||
</button>
|
</button>
|
||||||
<button class="navigation-bar__button button" @click="pagedownClick('image')">
|
<button class="navigation-bar__button button" @click="pagedownClick('image')" v-title="'Image'">
|
||||||
<icon-file-image></icon-file-image>
|
<icon-file-image></icon-file-image>
|
||||||
</button>
|
</button>
|
||||||
<button class="navigation-bar__button button" @click="pagedownClick('hr')">
|
<button class="navigation-bar__button button" @click="pagedownClick('hr')" v-title="'Horizontal rule'">
|
||||||
<icon-format-horizontal-rule></icon-format-horizontal-rule>
|
<icon-format-horizontal-rule></icon-format-horizontal-rule>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -368,14 +368,14 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$r: 9px;
|
$r: 10px;
|
||||||
$d: $r * 2;
|
$d: $r * 2;
|
||||||
$b: $d/10;
|
$b: $d/10;
|
||||||
$t: 3000ms;
|
$t: 3000ms;
|
||||||
|
|
||||||
.navigation-bar__spinner {
|
.navigation-bar__spinner {
|
||||||
width: 22px;
|
width: 22px;
|
||||||
margin: 8px 0 0 8px;
|
margin: 7px 0 0 8px;
|
||||||
color: #b2b2b2;
|
color: #b2b2b2;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="side-bar flex flex--column">
|
<div class="side-bar flex flex--column">
|
||||||
<div class="side-title flex flex--row">
|
<div class="side-title flex flex--row">
|
||||||
<button v-if="panel !== 'menu'" class="side-title__button button" @click="setPanel('menu')">
|
<button v-if="panel !== 'menu'" class="side-title__button button" @click="setPanel('menu')" v-title="'Main menu'">
|
||||||
<icon-arrow-left></icon-arrow-left>
|
<icon-arrow-left></icon-arrow-left>
|
||||||
</button>
|
</button>
|
||||||
<div class="side-title__title">
|
<div class="side-title__title">
|
||||||
{{panelName}}
|
{{panelName}}
|
||||||
</div>
|
</div>
|
||||||
<button class="side-title__button button" @click="toggleSideBar(false)">
|
<button class="side-title__button button" @click="toggleSideBar(false)" v-title="'Close side bar'">
|
||||||
<icon-close></icon-close>
|
<icon-close></icon-close>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<div class="stat-panel__block stat-panel__block--left" v-if="styles.showEditor">
|
<div class="stat-panel__block stat-panel__block--left" v-if="styles.showEditor">
|
||||||
<span class="stat-panel__block-name">
|
<span class="stat-panel__block-name">
|
||||||
Markdown
|
Markdown
|
||||||
<small v-show="textSelection">(selection)</small>
|
<small v-if="textSelection">(selection)</small>
|
||||||
</span>
|
</span>
|
||||||
<span v-for="stat in textStats" :key="stat.id">
|
<span v-for="stat in textStats" :key="stat.id">
|
||||||
<span class="stat-panel__value">{{stat.value}}</span> {{stat.name}}
|
<span class="stat-panel__value">{{stat.value}}</span> {{stat.name}}
|
||||||
@ -13,7 +13,7 @@
|
|||||||
<div class="stat-panel__block stat-panel__block--right">
|
<div class="stat-panel__block stat-panel__block--right">
|
||||||
<span class="stat-panel__block-name">
|
<span class="stat-panel__block-name">
|
||||||
HTML
|
HTML
|
||||||
<small v-show="htmlSelection">(selection)</small>
|
<small v-if="htmlSelection">(selection)</small>
|
||||||
</span>
|
</span>
|
||||||
<span v-for="stat in htmlStats" :key="stat.id">
|
<span v-for="stat in htmlStats" :key="stat.id">
|
||||||
<span class="stat-panel__value">{{stat.value}}</span> {{stat.name}}
|
<span class="stat-panel__value">{{stat.value}}</span> {{stat.name}}
|
||||||
|
@ -189,48 +189,6 @@ textarea {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
|
||||||
border-bottom: 1px solid $hr-color;
|
|
||||||
margin-bottom: 2em;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs__tab {
|
|
||||||
width: 50%;
|
|
||||||
float: left;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.4;
|
|
||||||
padding: 0.67em 0.33em;
|
|
||||||
cursor: pointer;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
border-top-left-radius: $border-radius-base;
|
|
||||||
border-top-right-radius: $border-radius-base;
|
|
||||||
color: $link-color;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 1.1em;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus {
|
|
||||||
background-color: rgba(0, 0, 0, 0.067);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs__tab--active {
|
|
||||||
border-bottom: 2px solid $link-color;
|
|
||||||
color: inherit;
|
|
||||||
cursor: auto;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-background {
|
.logo-background {
|
||||||
background: no-repeat center url('../assets/logo.svg');
|
background: no-repeat center url('../assets/logo.svg');
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1 modal__inner-1--about-modal">
|
<div class="modal__inner-1 modal__inner-1--about-modal" role="dialog" aria-label="About">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="logo-background"></div>
|
<div class="logo-background"></div>
|
||||||
<div class="app-version">v{{version}} — © 2017 Benoit Schweblin</div>
|
<div class="app-version">v{{version}} — © 2017 Benoit Schweblin</div>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1">
|
<div class="modal__inner-1" role="dialog" aria-label="Publish to Blogger Page">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="bloggerPage"></icon-provider>
|
<icon-provider provider-id="bloggerPage"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>This will publish <b>{{currentFileName}}</b> to your <b>Blogger Page</b>.</p>
|
<p>This will publish <b>{{currentFileName}}</b> to your <b>Blogger Page</b>.</p>
|
||||||
<form-entry label="Blog URL">
|
<form-entry label="Blog URL" error="blogUrl">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="blogUrl" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="blogUrl" @keyup.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
<b>Example:</b> http://example.blogger.com/
|
<b>Example:</b> http://example.blogger.com/
|
||||||
@ -49,7 +49,9 @@ export default modalTemplate({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resolve() {
|
resolve() {
|
||||||
if (this.blogUrl) {
|
if (!this.blogUrl) {
|
||||||
|
this.setError('blogUrl');
|
||||||
|
} else {
|
||||||
// Return new location
|
// Return new location
|
||||||
const location = bloggerPageProvider.makeLocation(
|
const location = bloggerPageProvider.makeLocation(
|
||||||
this.config.token, this.blogUrl, this.pageId);
|
this.config.token, this.blogUrl, this.pageId);
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1">
|
<div class="modal__inner-1" role="dialog" aria-label="Publish to Blogger">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="blogger"></icon-provider>
|
<icon-provider provider-id="blogger"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>This will publish <b>{{currentFileName}}</b> to your <b>Blogger</b> site.</p>
|
<p>This will publish <b>{{currentFileName}}</b> to your <b>Blogger</b> site.</p>
|
||||||
<form-entry label="Blog URL">
|
<form-entry label="Blog URL" error="blogUrl">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="blogUrl" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="blogUrl" @keyup.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
<b>Example:</b> http://example.blogger.com/
|
<b>Example:</b> http://example.blogger.com/
|
||||||
@ -50,7 +50,9 @@ export default modalTemplate({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resolve() {
|
resolve() {
|
||||||
if (this.blogUrl) {
|
if (!this.blogUrl) {
|
||||||
|
this.setError('blogUrl');
|
||||||
|
} else {
|
||||||
// Return new location
|
// Return new location
|
||||||
const location = bloggerProvider.makeLocation(
|
const location = bloggerProvider.makeLocation(
|
||||||
this.config.token, this.blogUrl, this.postId);
|
this.config.token, this.blogUrl, this.postId);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1">
|
<div class="modal__inner-1" role="dialog" aria-label="Link Dropbox account">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="dropbox"></icon-provider>
|
<icon-provider provider-id="dropbox"></icon-provider>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1">
|
<div class="modal__inner-1" role="dialog" aria-label="Publish to Dropbox">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="dropbox"></icon-provider>
|
<icon-provider provider-id="dropbox"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>This will publish <b>{{currentFileName}}</b> to your <b>Dropbox</b>.</p>
|
<p>This will publish <b>{{currentFileName}}</b> to your <b>Dropbox</b>.</p>
|
||||||
<form-entry label="File path">
|
<form-entry label="File path" error="path">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="path" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="path" @keyup.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
<b>Example:</b> {{config.token.fullAccess ? '' : '/Applications/StackEdit (restricted)'}}/path/to/My Document.html<br>
|
<b>Example:</b> {{config.token.fullAccess ? '' : '/Applications/StackEdit (restricted)'}}/path/to/My Document.html<br>
|
||||||
@ -46,7 +46,9 @@ export default modalTemplate({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resolve() {
|
resolve() {
|
||||||
if (dropboxProvider.checkPath(this.path)) {
|
if (!dropboxProvider.checkPath(this.path)) {
|
||||||
|
this.setError('path');
|
||||||
|
} else {
|
||||||
// Return new location
|
// Return new location
|
||||||
const location = dropboxProvider.makeLocation(this.config.token, this.path);
|
const location = dropboxProvider.makeLocation(this.config.token, this.path);
|
||||||
location.templateId = this.selectedTemplate;
|
location.templateId = this.selectedTemplate;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1">
|
<div class="modal__inner-1" role="dialog" aria-label="Synchronize with Dropbox">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="dropbox"></icon-provider>
|
<icon-provider provider-id="dropbox"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>This will save <b>{{currentFileName}}</b> to your <b>Dropbox</b> and keep it synchronized.</p>
|
<p>This will save <b>{{currentFileName}}</b> to your <b>Dropbox</b> and keep it synchronized.</p>
|
||||||
<form-entry label="File path">
|
<form-entry label="File path" error="path">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="path" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="path" @keyup.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
<b>Example:</b> {{config.token.fullAccess ? '' : '/Applications/StackEdit (restricted)'}}/path/to/My Document.md<br>
|
<b>Example:</b> {{config.token.fullAccess ? '' : '/Applications/StackEdit (restricted)'}}/path/to/My Document.md<br>
|
||||||
@ -33,7 +33,9 @@ export default modalTemplate({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resolve() {
|
resolve() {
|
||||||
if (dropboxProvider.checkPath(this.path)) {
|
if (!dropboxProvider.checkPath(this.path)) {
|
||||||
|
this.setError('path');
|
||||||
|
} else {
|
||||||
// Return new location
|
// Return new location
|
||||||
const location = dropboxProvider.makeLocation(this.config.token, this.path);
|
const location = dropboxProvider.makeLocation(this.config.token, this.path);
|
||||||
this.config.resolve(location);
|
this.config.resolve(location);
|
||||||
|
@ -1,19 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1 modal__inner-1--file-properties">
|
<div class="modal__inner-1 modal__inner-1--file-properties" role="dialog" aria-label="File properties">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="tabs flex flex--row">
|
<div class="tabs flex flex--row">
|
||||||
<div class="tabs__tab flex flex--column flex--center" :class="{'tabs__tab--active': tab === 'custom'}" @click="tab = 'custom'">
|
<tab :active="tab === 'custom'" @click="tab = 'custom'">
|
||||||
Current file properties
|
Current file properties
|
||||||
</div>
|
</tab>
|
||||||
<div class="tabs__tab flex flex--column flex--center" :class="{'tabs__tab--active': tab === 'default'}" @click="tab = 'default'">
|
<tab :active="tab === 'default'" @click="tab = 'default'">
|
||||||
Default properties
|
Default properties
|
||||||
</div>
|
</tab>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-entry">
|
<div class="form-entry" v-if="tab === 'custom'" role="tabpanel" aria-label="Current file properties">
|
||||||
<label class="form-entry__label">YAML</label>
|
<label class="form-entry__label">YAML</label>
|
||||||
<div class="form-entry__field">
|
<div class="form-entry__field">
|
||||||
<code-editor v-if="tab === 'custom'" lang="yaml" :value="customProperties" key="custom-properties" @changed="setCustomProperties"></code-editor>
|
<code-editor lang="yaml" :value="customProperties" key="custom-properties" @changed="setCustomProperties"></code-editor>
|
||||||
<code-editor v-else lang="yaml" :value="defaultProperties" disabled="true" key="default-properties"></code-editor>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry" v-else-if="tab === 'default'" role="tabpanel" aria-label="Default properties">
|
||||||
|
<label class="form-entry__label">YAML</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<code-editor lang="yaml" :value="defaultProperties" key="default-properties" disabled="true"></code-editor>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__error modal__error--file-properties">{{error}}</div>
|
<div class="modal__error modal__error--file-properties">{{error}}</div>
|
||||||
@ -28,6 +33,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import Tab from './Tab';
|
||||||
import CodeEditor from '../CodeEditor';
|
import CodeEditor from '../CodeEditor';
|
||||||
import defaultProperties from '../../data/defaultFileProperties.yml';
|
import defaultProperties from '../../data/defaultFileProperties.yml';
|
||||||
|
|
||||||
@ -35,6 +41,7 @@ const emptyProperties = '# Add custom properties for the current file here to ov
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
Tab,
|
||||||
CodeEditor,
|
CodeEditor,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="form-entry">
|
<div class="form-entry" :error="error">
|
||||||
<label class="form-entry__label" :for="uid">{{label}}</label>
|
<label class="form-entry__label" :for="uid">{{label}}</label>
|
||||||
<div class="form-entry__field">
|
<div class="form-entry__field">
|
||||||
<slot name="field"></slot>
|
<slot name="field"></slot>
|
||||||
@ -12,7 +12,7 @@
|
|||||||
import utils from '../../services/utils';
|
import utils from '../../services/utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['label'],
|
props: ['label', 'error'],
|
||||||
data: () => ({
|
data: () => ({
|
||||||
uid: utils.uid(),
|
uid: utils.uid(),
|
||||||
}),
|
}),
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1">
|
<div class="modal__inner-1" role="dialog" aria-label="Publish to Gist">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="gist"></icon-provider>
|
<icon-provider provider-id="gist"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>This will publish <b>{{currentFileName}}</b> to a <b>Gist</b>.</p>
|
<p>This will publish <b>{{currentFileName}}</b> to a <b>Gist</b>.</p>
|
||||||
<form-entry label="Filename">
|
<form-entry label="Filename" error="filename">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keyup.enter="resolve()">
|
||||||
</form-entry>
|
</form-entry>
|
||||||
<div class="form-entry">
|
<div class="form-entry">
|
||||||
@ -60,7 +60,9 @@ export default modalTemplate({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resolve() {
|
resolve() {
|
||||||
if (this.filename) {
|
if (!this.filename) {
|
||||||
|
this.setError('filename');
|
||||||
|
} else {
|
||||||
// Return new location
|
// Return new location
|
||||||
const location = gistProvider.makeLocation(
|
const location = gistProvider.makeLocation(
|
||||||
this.config.token, this.filename, this.isPublic, this.gistId);
|
this.config.token, this.filename, this.isPublic, this.gistId);
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1">
|
<div class="modal__inner-1" role="dialog" aria-label="Synchronize with Gist">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="gist"></icon-provider>
|
<icon-provider provider-id="gist"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>This will save <b>{{currentFileName}}</b> to a <b>Gist</b> and keep it synchronized.</p>
|
<p>This will save <b>{{currentFileName}}</b> to a <b>Gist</b> and keep it synchronized.</p>
|
||||||
<form-entry label="Filename">
|
<form-entry label="Filename" error="filename">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keyup.enter="resolve()">
|
||||||
</form-entry>
|
</form-entry>
|
||||||
<div class="form-entry">
|
<div class="form-entry">
|
||||||
@ -46,7 +46,9 @@ export default modalTemplate({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resolve() {
|
resolve() {
|
||||||
if (this.filename) {
|
if (!this.filename) {
|
||||||
|
this.setError('filename');
|
||||||
|
} else {
|
||||||
// Return new location
|
// Return new location
|
||||||
const location = gistProvider.makeLocation(
|
const location = gistProvider.makeLocation(
|
||||||
this.config.token, this.filename, this.isPublic, this.gistId);
|
this.config.token, this.filename, this.isPublic, this.gistId);
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1">
|
<div class="modal__inner-1" role="dialog" aria-label="Link GitHub account">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="github"></icon-provider>
|
<icon-provider provider-id="github"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>This will link your <b>Github</b> account to your <b>StackEdit</b> workspace.</p>
|
<p>This will link your <b>GitHub</b> account to your <b>StackEdit</b> workspace.</p>
|
||||||
<div class="form-entry">
|
<div class="form-entry">
|
||||||
<div class="form-entry__checkbox">
|
<div class="form-entry__checkbox">
|
||||||
<label>
|
<label>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1">
|
<div class="modal__inner-1" role="dialog" aria-label="Publish to GitHub">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="github"></icon-provider>
|
<icon-provider provider-id="github"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>This will publish <b>{{currentFileName}}</b> to your <b>GitHub</b> repository.</p>
|
<p>This will publish <b>{{currentFileName}}</b> to your <b>GitHub</b> repository.</p>
|
||||||
<form-entry label="Repository URL">
|
<form-entry label="Repository URL" error="repoUrl">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keyup.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
<b>Example:</b> https://github.com/benweet/stackedit
|
<b>Example:</b> https://github.com/benweet/stackedit
|
||||||
@ -17,7 +17,7 @@
|
|||||||
If not provided, the master branch will be used.
|
If not provided, the master branch will be used.
|
||||||
</div>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
<form-entry label="File path">
|
<form-entry label="File path" error="path">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="path" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="path" @keyup.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
<b>Example:</b> docs/README.md<br>
|
<b>Example:</b> docs/README.md<br>
|
||||||
@ -60,9 +60,17 @@ export default modalTemplate({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resolve() {
|
resolve() {
|
||||||
|
if (!this.repoUrl) {
|
||||||
|
this.setError('repoUrl');
|
||||||
|
}
|
||||||
|
if (!this.path) {
|
||||||
|
this.setError('path');
|
||||||
|
}
|
||||||
if (this.repoUrl && this.path) {
|
if (this.repoUrl && this.path) {
|
||||||
const parsedRepo = this.repoUrl.match(/[/:]?([^/:]+)\/([^/]+?)(?:\.git)?$/);
|
const parsedRepo = this.repoUrl.match(/[/:]?([^/:]+)\/([^/]+?)(?:\.git|\/)?$/);
|
||||||
if (parsedRepo) {
|
if (!parsedRepo) {
|
||||||
|
this.setError('repoUrl');
|
||||||
|
} else {
|
||||||
// Return new location
|
// Return new location
|
||||||
const location = githubProvider.makeLocation(
|
const location = githubProvider.makeLocation(
|
||||||
this.config.token, parsedRepo[1], parsedRepo[2], this.branch || 'master', this.path);
|
this.config.token, parsedRepo[1], parsedRepo[2], this.branch || 'master', this.path);
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1">
|
<div class="modal__inner-1" role="dialog" aria-label="Synchronize with GitHub">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="github"></icon-provider>
|
<icon-provider provider-id="github"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>This will save <b>{{currentFileName}}</b> to your <b>GitHub</b> repository and keep it synchronized.</p>
|
<p>This will save <b>{{currentFileName}}</b> to your <b>GitHub</b> repository and keep it synchronized.</p>
|
||||||
<form-entry label="Repository URL">
|
<form-entry label="Repository URL" error="repoUrl">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keyup.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
<b>Example:</b> https://github.com/benweet/stackedit
|
<b>Example:</b> https://github.com/benweet/stackedit
|
||||||
@ -17,7 +17,7 @@
|
|||||||
If not provided, the master branch will be used.
|
If not provided, the master branch will be used.
|
||||||
</div>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
<form-entry label="File path">
|
<form-entry label="File path" error="path">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="path" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="path" @keyup.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
<b>Example:</b> docs/README.md<br>
|
<b>Example:</b> docs/README.md<br>
|
||||||
@ -49,9 +49,17 @@ export default modalTemplate({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resolve() {
|
resolve() {
|
||||||
|
if (!this.repoUrl) {
|
||||||
|
this.setError('repoUrl');
|
||||||
|
}
|
||||||
|
if (!this.path) {
|
||||||
|
this.setError('path');
|
||||||
|
}
|
||||||
if (this.repoUrl && this.path) {
|
if (this.repoUrl && this.path) {
|
||||||
const parsedRepo = this.repoUrl.match(/[/:]?([^/:]+)\/([^/]+?)(?:\.git)?$/);
|
const parsedRepo = this.repoUrl.match(/[/:]?([^/:]+)\/([^/]+?)(?:\.git|\/)?$/);
|
||||||
if (parsedRepo) {
|
if (!parsedRepo) {
|
||||||
|
this.setError('repoUrl');
|
||||||
|
} else {
|
||||||
// Return new location
|
// Return new location
|
||||||
const location = githubProvider.makeLocation(
|
const location = githubProvider.makeLocation(
|
||||||
this.config.token, parsedRepo[1], parsedRepo[2], this.branch || 'master', this.path);
|
this.config.token, parsedRepo[1], parsedRepo[2], this.branch || 'master', this.path);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1">
|
<div class="modal__inner-1" role="dialog" aria-label="Publish to Google Drive">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="googleDrive"></icon-provider>
|
<icon-provider provider-id="googleDrive"></icon-provider>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1">
|
<div class="modal__inner-1" role="dialog" aria-label="Synchronize with Google Drive">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="googleDrive"></icon-provider>
|
<icon-provider provider-id="googleDrive"></icon-provider>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1 modal__inner-1--google-photo">
|
<div class="modal__inner-1 modal__inner-1--google-photo" role="dialog" aria-label="Import Google Photo">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="google-photo__tumbnail" :style="{'background-image': thumbnailUrl}"></div>
|
<div class="google-photo__tumbnail" :style="{'background-image': thumbnailUrl}"></div>
|
||||||
<form-entry label="Title (optional)">
|
<form-entry label="Title (optional)">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1">
|
<div class="modal__inner-1" role="dialog" aria-label="Export to HTML">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<form-entry label="Template">
|
<form-entry label="Template">
|
||||||
<select slot="field" class="textfield" v-model="selectedTemplate" @keyup.enter="resolve()">
|
<select slot="field" class="textfield" v-model="selectedTemplate" @keyup.enter="resolve()">
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1">
|
<div class="modal__inner-1" role="dialog" aria-label="Insert image">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<p>Please provide a <b>URL</b> for your image.
|
<p>Please provide a <b>URL</b> for your image.
|
||||||
<form-entry label="URL">
|
<form-entry label="URL" error="url">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="url" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="url" @keyup.enter="resolve()">
|
||||||
</form-entry>
|
</form-entry>
|
||||||
<menu-entry @click.native="openGooglePhotos(token)" v-for="token in googlePhotosTokens" :key="token.sub">
|
<menu-entry @click.native="openGooglePhotos(token)" v-for="token in googlePhotosTokens" :key="token.sub">
|
||||||
@ -23,23 +23,18 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import modalTemplate from './modalTemplate';
|
||||||
import MenuEntry from '../menus/MenuEntry';
|
import MenuEntry from '../menus/MenuEntry';
|
||||||
import FormEntry from './FormEntry';
|
|
||||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
|
|
||||||
export default {
|
export default modalTemplate({
|
||||||
components: {
|
components: {
|
||||||
FormEntry,
|
|
||||||
MenuEntry,
|
MenuEntry,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
url: '',
|
url: '',
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('modal', [
|
|
||||||
'config',
|
|
||||||
]),
|
|
||||||
googlePhotosTokens() {
|
googlePhotosTokens() {
|
||||||
const googleToken = this.$store.getters['data/googleTokens'];
|
const googleToken = this.$store.getters['data/googleTokens'];
|
||||||
return Object.keys(googleToken)
|
return Object.keys(googleToken)
|
||||||
@ -50,7 +45,9 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resolve() {
|
resolve() {
|
||||||
if (this.url) {
|
if (!this.url) {
|
||||||
|
this.setError('url');
|
||||||
|
} else {
|
||||||
const callback = this.config.callback;
|
const callback = this.config.callback;
|
||||||
this.config.resolve();
|
this.config.resolve();
|
||||||
callback(this.url);
|
callback(this.url);
|
||||||
@ -75,5 +72,5 @@ export default {
|
|||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1">
|
<div class="modal__inner-1" role="dialog" aria-label="Insert link">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<p>Please provide a <b>URL</b> for your link.
|
<p>Please provide a <b>URL</b> for your link.
|
||||||
<form-entry label="URL">
|
<form-entry label="URL" error="url">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="url" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="url" @keyup.enter="resolve()">
|
||||||
</form-entry>
|
</form-entry>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
@ -14,22 +14,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import modalTemplate from './modalTemplate';
|
||||||
import FormEntry from './FormEntry';
|
|
||||||
|
|
||||||
export default {
|
export default modalTemplate({
|
||||||
components: {
|
|
||||||
FormEntry,
|
|
||||||
},
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
url: '',
|
url: '',
|
||||||
}),
|
}),
|
||||||
computed: mapGetters('modal', [
|
|
||||||
'config',
|
|
||||||
]),
|
|
||||||
methods: {
|
methods: {
|
||||||
resolve() {
|
resolve() {
|
||||||
if (this.url) {
|
if (!this.url) {
|
||||||
|
this.setError('url');
|
||||||
|
} else {
|
||||||
const callback = this.config.callback;
|
const callback = this.config.callback;
|
||||||
this.config.resolve();
|
this.config.resolve();
|
||||||
callback(this.url);
|
callback(this.url);
|
||||||
@ -41,5 +36,5 @@ export default {
|
|||||||
callback(null);
|
callback(null);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1 modal__inner-1--publish-management">
|
<div class="modal__inner-1 modal__inner-1--publish-management" role="dialog" aria-label="Manage publication locations">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<p v-if="publishLocations.length"><b>{{currentFileName}}</b> is published to the following location(s):</p>
|
<p v-if="publishLocations.length"><b>{{currentFileName}}</b> is published to the following location(s):</p>
|
||||||
<p v-else><b>{{currentFileName}}</b> is not published yet.</p>
|
<p v-else><b>{{currentFileName}}</b> is not published yet.</p>
|
||||||
|
@ -1,19 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1 modal__inner-1--settings">
|
<div class="modal__inner-1 modal__inner-1--settings" role="dialog" aria-label="Settings">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="tabs flex flex--row">
|
<div class="tabs flex flex--row">
|
||||||
<div class="tabs__tab flex flex--column flex--center" :class="{'tabs__tab--active': tab === 'custom'}" @click="tab = 'custom'">
|
<tab :active="tab === 'custom'" @click="tab = 'custom'">
|
||||||
Custom settings
|
Custom settings
|
||||||
</div>
|
</tab>
|
||||||
<div class="tabs__tab flex flex--column flex--center" :class="{'tabs__tab--active': tab === 'default'}" @click="tab = 'default'">
|
<tab :active="tab === 'default'" @click="tab = 'default'">
|
||||||
Default settings
|
Default settings
|
||||||
</div>
|
</tab>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-entry">
|
<div class="form-entry" v-if="tab === 'custom'" role="tabpanel" aria-label="Custom settings">
|
||||||
<label class="form-entry__label">YAML</label>
|
<label class="form-entry__label">YAML</label>
|
||||||
<div class="form-entry__field form-entry__field--code-editor">
|
<div class="form-entry__field form-entry__field--code-editor">
|
||||||
<code-editor v-if="tab === 'custom'" lang="yaml" :value="customSettings" key="custom-settings" @changed="setCustomSettings"></code-editor>
|
<code-editor lang="yaml" :value="customSettings" key="custom-settings" @changed="setCustomSettings"></code-editor>
|
||||||
<code-editor v-else lang="yaml" :value="defaultSettings" disabled="true" key="default-settings"></code-editor>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry" v-else-if="tab === 'default'" role="tabpanel" aria-label="Default settings">
|
||||||
|
<label class="form-entry__label">YAML</label>
|
||||||
|
<div class="form-entry__field form-entry__field--code-editor">
|
||||||
|
<code-editor lang="yaml" :value="defaultSettings" key="default-settings" disabled="true"></code-editor>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__error modal__error--settings">{{error}}</div>
|
<div class="modal__error modal__error--settings">{{error}}</div>
|
||||||
@ -28,6 +33,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import Tab from './Tab';
|
||||||
import CodeEditor from '../CodeEditor';
|
import CodeEditor from '../CodeEditor';
|
||||||
import defaultSettings from '../../data/defaultSettings.yml';
|
import defaultSettings from '../../data/defaultSettings.yml';
|
||||||
|
|
||||||
@ -35,6 +41,7 @@ const emptySettings = '# Add your custom settings here to override the default s
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
Tab,
|
||||||
CodeEditor,
|
CodeEditor,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1 modal__inner-1--sync-management">
|
<div class="modal__inner-1 modal__inner-1--sync-management" role="dialog" aria-label="Manage synchronized locations">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<p v-if="syncLocations.length"><b>{{currentFileName}}</b> is synchronized with the following location(s):</p>
|
<p v-if="syncLocations.length"><b>{{currentFileName}}</b> is synchronized with the following location(s):</p>
|
||||||
<p v-else><b>{{currentFileName}}</b> is not synchronized yet.</p>
|
<p v-else><b>{{currentFileName}}</b> is not synchronized yet.</p>
|
||||||
|
13
src/components/modals/Tab.vue
Normal file
13
src/components/modals/Tab.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tabs__tab flex flex--row" :class="{'tabs__tab--active': active}" role="tab">
|
||||||
|
<a class="flex flex--column flex--center" href="javascript:void(0)" @click="$emit('click')">
|
||||||
|
<slot></slot>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: ['active'],
|
||||||
|
};
|
||||||
|
</script>
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1 modal__inner-1--templates">
|
<div class="modal__inner-1 modal__inner-1--templates" role="dialog" aria-label="Manage templates">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="form-entry">
|
<div class="form-entry">
|
||||||
<label class="form-entry__label" for="template">Template</label>
|
<label class="form-entry__label" for="template">Template</label>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1">
|
<div class="modal__inner-1" role="dialog" aria-label="Publish to WordPress">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="wordpress"></icon-provider>
|
<icon-provider provider-id="wordpress"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>This will publish <b>{{currentFileName}}</b> to your <b>WordPress</b> site.</p>
|
<p>This will publish <b>{{currentFileName}}</b> to your <b>WordPress</b> site.</p>
|
||||||
<form-entry label="Site domain">
|
<form-entry label="Site domain" error="domain">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="domain" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="domain" @keyup.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
<b>Example:</b> example.wordpress.com<br>
|
<b>Example:</b> example.wordpress.com<br>
|
||||||
@ -52,7 +52,9 @@ export default modalTemplate({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resolve() {
|
resolve() {
|
||||||
if (this.domain) {
|
if (!this.domain) {
|
||||||
|
this.setError('domain');
|
||||||
|
} else {
|
||||||
// Return new location
|
// Return new location
|
||||||
const location = wordpressProvider.makeLocation(
|
const location = wordpressProvider.makeLocation(
|
||||||
this.config.token, this.domain, this.postId);
|
this.config.token, this.domain, this.postId);
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1">
|
<div class="modal__inner-1" role="dialog" aria-label="Link Zendesk account">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="zendesk"></icon-provider>
|
<icon-provider provider-id="zendesk"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>This will link your <b>Zendesk</b> account to your <b>StackEdit</b> workspace.</p>
|
<p>This will link your <b>Zendesk</b> account to your <b>StackEdit</b> workspace.</p>
|
||||||
<form-entry label="Site URL">
|
<form-entry label="Site URL" error="siteUrl">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="siteUrl" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="siteUrl" @keyup.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
<b>Example:</b> https://example.zendesk.com/
|
<b>Example:</b> https://example.zendesk.com/
|
||||||
</div>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
<form-entry label="Client Unique Identifier">
|
<form-entry label="Client Unique Identifier" error="clientId">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="clientId" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="clientId" @keyup.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
You have to configure an OAuth Client with redirect URL <b>{{redirectUrl}}</b><br>
|
You have to configure an OAuth Client with redirect URL <b>{{redirectUrl}}</b><br>
|
||||||
@ -40,9 +40,17 @@ export default modalTemplate({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resolve() {
|
resolve() {
|
||||||
|
if (!this.siteUrl) {
|
||||||
|
this.setError('siteUrl');
|
||||||
|
}
|
||||||
|
if (!this.clientId) {
|
||||||
|
this.setError('clientId');
|
||||||
|
}
|
||||||
if (this.siteUrl && this.clientId) {
|
if (this.siteUrl && this.clientId) {
|
||||||
const parsedUrl = this.siteUrl.match(/^https:\/\/([^.]+)\.zendesk\.com/);
|
const parsedUrl = this.siteUrl.match(/^https:\/\/([^.]+)\.zendesk\.com/);
|
||||||
if (parsedUrl) {
|
if (!parsedUrl) {
|
||||||
|
this.setError('siteUrl');
|
||||||
|
} else {
|
||||||
this.config.resolve({
|
this.config.resolve({
|
||||||
subdomain: parsedUrl[1],
|
subdomain: parsedUrl[1],
|
||||||
clientId: this.clientId,
|
clientId: this.clientId,
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1">
|
<div class="modal__inner-1" role="dialog" aria-label="Publish to Zendesk">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="zendesk"></icon-provider>
|
<icon-provider provider-id="zendesk"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<p>This will publish <b>{{currentFileName}}</b> to your <b>Zendesk Help Center</b>.</p>
|
<p>This will publish <b>{{currentFileName}}</b> to your <b>Zendesk Help Center</b>.</p>
|
||||||
<form-entry label="Section ID">
|
<form-entry label="Section ID" error="sectionId">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="sectionId" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="sectionId" @keyup.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
https://example.zendesk.com/hc/en-us/sections/<b>21857469</b>-Section-name
|
https://example.zendesk.com/hc/en-us/sections/<b>21857469</b>-Section-name
|
||||||
@ -57,7 +57,9 @@ export default modalTemplate({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resolve() {
|
resolve() {
|
||||||
if (this.sectionId || this.articleId) {
|
if (!this.sectionId && !this.articleId) {
|
||||||
|
this.setError('sectionId');
|
||||||
|
} else {
|
||||||
// Return new location
|
// Return new location
|
||||||
const location = zendeskProvider.makeLocation(
|
const location = zendeskProvider.makeLocation(
|
||||||
this.config.token, this.sectionId, this.locale || 'en-us', this.articleId);
|
this.config.token, this.sectionId, this.locale || 'en-us', this.articleId);
|
||||||
|
@ -4,7 +4,12 @@ import store from '../../store';
|
|||||||
export default (desc) => {
|
export default (desc) => {
|
||||||
const component = {
|
const component = {
|
||||||
...desc,
|
...desc,
|
||||||
|
data: () => ({
|
||||||
|
...desc.data ? desc.data() : {},
|
||||||
|
errorTimeouts: {},
|
||||||
|
}),
|
||||||
components: {
|
components: {
|
||||||
|
...desc.components || {},
|
||||||
FormEntry,
|
FormEntry,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -19,6 +24,16 @@ export default (desc) => {
|
|||||||
methods: {
|
methods: {
|
||||||
...desc.methods || {},
|
...desc.methods || {},
|
||||||
openFileProperties: () => store.dispatch('modal/open', 'fileProperties'),
|
openFileProperties: () => store.dispatch('modal/open', 'fileProperties'),
|
||||||
|
setError(name) {
|
||||||
|
clearTimeout(this.errorTimeouts[name]);
|
||||||
|
const formEntry = this.$el.querySelector(`.form-entry[error=${name}]`);
|
||||||
|
if (formEntry) {
|
||||||
|
formEntry.classList.add('form-entry--error');
|
||||||
|
this.errorTimeouts[name] = setTimeout(() => {
|
||||||
|
formEntry.classList.remove('form-entry--error');
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
Object.keys(desc.computedLocalSettings || {}).forEach((key) => {
|
Object.keys(desc.computedLocalSettings || {}).forEach((key) => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import 'clunderscore';
|
import './clunderscore';
|
||||||
import cledit from './cleditCore';
|
import cledit from './cleditCore';
|
||||||
import './cleditHighlighter';
|
import './cleditHighlighter';
|
||||||
import './cleditKeystroke';
|
import './cleditKeystroke';
|
||||||
|
@ -59,6 +59,10 @@ liveCollectionProperties.cl_map = function (cb) {
|
|||||||
return slice.call(this).cl_map(cb)
|
return slice.call(this).cl_map(cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
liveCollectionProperties.cl_filter = function (cb) {
|
||||||
|
return slice.call(this).cl_filter(cb)
|
||||||
|
}
|
||||||
|
|
||||||
liveCollectionProperties.cl_reduce = function (cb, memo) {
|
liveCollectionProperties.cl_reduce = function (cb, memo) {
|
||||||
return slice.call(this).cl_reduce(cb, memo)
|
return slice.call(this).cl_reduce(cb, memo)
|
||||||
}
|
}
|
||||||
|
@ -107,9 +107,6 @@ const localDbSvc = {
|
|||||||
this.connection.createTx((tx) => {
|
this.connection.createTx((tx) => {
|
||||||
this.readAll(tx, (storeItemMap) => {
|
this.readAll(tx, (storeItemMap) => {
|
||||||
this.writeAll(storeItemMap, tx);
|
this.writeAll(storeItemMap, tx);
|
||||||
if (!store.state.ready) {
|
|
||||||
store.commit('setReady');
|
|
||||||
}
|
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
}, () => reject(new Error('Local DB access error.')));
|
}, () => reject(new Error('Local DB access error.')));
|
||||||
@ -316,57 +313,74 @@ const ifNoId = cb => (obj) => {
|
|||||||
|
|
||||||
// Load the DB on boot
|
// Load the DB on boot
|
||||||
localDbSvc.sync()
|
localDbSvc.sync()
|
||||||
// And watch file changing
|
.then(() => {
|
||||||
.then(() => store.watch(
|
store.commit('setReady');
|
||||||
() => store.getters['file/current'].id,
|
|
||||||
() => Promise.resolve(store.getters['file/current'])
|
// If app was last opened 7 days ago and synchronization is off
|
||||||
// If current file has no ID, get the most recent file
|
if (!store.getters['data/loginToken'] &&
|
||||||
.then(ifNoId(() => store.getters['file/lastOpened']))
|
(utils.lastOpened + utils.cleanTrashAfter < Date.now())
|
||||||
// If still no ID, create a new file
|
) {
|
||||||
.then(ifNoId(() => {
|
// Clean files
|
||||||
const id = utils.uid();
|
store.getters['file/items'].forEach((file) => {
|
||||||
store.commit('content/setItem', {
|
// If file is in the trash
|
||||||
id: `${id}/content`,
|
if (file.parentId === 'trash') {
|
||||||
text: welcomeFile,
|
store.dispatch('deleteFile', file.id);
|
||||||
});
|
|
||||||
store.commit('file/setItem', {
|
|
||||||
id,
|
|
||||||
name: 'Welcome file',
|
|
||||||
});
|
|
||||||
return store.state.file.itemMap[id];
|
|
||||||
}))
|
|
||||||
.then((currentFile) => {
|
|
||||||
// Fix current file ID
|
|
||||||
if (store.getters['file/current'].id !== currentFile.id) {
|
|
||||||
store.commit('file/setCurrentId', currentFile.id);
|
|
||||||
// Wait for the next watch tick
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
return Promise.resolve()
|
});
|
||||||
// Load contentState from DB
|
}
|
||||||
.then(() => localDbSvc.loadContentState(currentFile.id))
|
|
||||||
// Load syncedContent from DB
|
// watch file changing
|
||||||
.then(() => localDbSvc.loadSyncedContent(currentFile.id))
|
store.watch(
|
||||||
// Load content from DB
|
() => store.getters['file/current'].id,
|
||||||
.then(() => localDbSvc.loadItem(`${currentFile.id}/content`))
|
() => Promise.resolve(store.getters['file/current'])
|
||||||
.then(
|
// If current file has no ID, get the most recent file
|
||||||
// Success, set last opened file
|
.then(ifNoId(() => store.getters['file/lastOpened']))
|
||||||
() => store.dispatch('data/setLastOpenedId', currentFile.id),
|
// If still no ID, create a new file
|
||||||
(err) => {
|
.then(ifNoId(() => {
|
||||||
// Failure (content is not available), go back to previous file
|
const id = utils.uid();
|
||||||
const lastOpenedFile = store.getters['file/lastOpened'];
|
store.commit('content/setItem', {
|
||||||
store.commit('file/setCurrentId', lastOpenedFile.id);
|
id: `${id}/content`,
|
||||||
throw err;
|
text: welcomeFile,
|
||||||
},
|
});
|
||||||
);
|
store.commit('file/setItem', {
|
||||||
})
|
id,
|
||||||
.catch((err) => {
|
name: 'Welcome file',
|
||||||
console.error(err); // eslint-disable-line no-console
|
});
|
||||||
store.dispatch('notification/error', err);
|
return store.state.file.itemMap[id];
|
||||||
}),
|
}))
|
||||||
{
|
.then((currentFile) => {
|
||||||
immediate: true,
|
// Fix current file ID
|
||||||
}));
|
if (store.getters['file/current'].id !== currentFile.id) {
|
||||||
|
store.commit('file/setCurrentId', currentFile.id);
|
||||||
|
// Wait for the next watch tick
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Promise.resolve()
|
||||||
|
// Load contentState from DB
|
||||||
|
.then(() => localDbSvc.loadContentState(currentFile.id))
|
||||||
|
// Load syncedContent from DB
|
||||||
|
.then(() => localDbSvc.loadSyncedContent(currentFile.id))
|
||||||
|
// Load content from DB
|
||||||
|
.then(() => localDbSvc.loadItem(`${currentFile.id}/content`))
|
||||||
|
.then(
|
||||||
|
// Success, set last opened file
|
||||||
|
() => store.dispatch('data/setLastOpenedId', currentFile.id),
|
||||||
|
(err) => {
|
||||||
|
// Failure (content is not available), go back to previous file
|
||||||
|
const lastOpenedFile = store.getters['file/lastOpened'];
|
||||||
|
store.commit('file/setCurrentId', lastOpenedFile.id);
|
||||||
|
throw err;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err); // eslint-disable-line no-console
|
||||||
|
store.dispatch('notification/error', err);
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Sync local DB periodically
|
// Sync local DB periodically
|
||||||
utils.setInterval(() => localDbSvc.sync(), 1000);
|
utils.setInterval(() => localDbSvc.sync(), 1000);
|
||||||
|
@ -28,17 +28,24 @@ export default providerRegistry.register({
|
|||||||
.catch(() => null); // Ignore error, without the sha upload is going to fail anyway
|
.catch(() => null); // Ignore error, without the sha upload is going to fail anyway
|
||||||
},
|
},
|
||||||
uploadContent(token, content, syncLocation) {
|
uploadContent(token, content, syncLocation) {
|
||||||
const sha = savedSha[syncLocation.id];
|
let result = Promise.resolve();
|
||||||
delete savedSha[syncLocation.id];
|
if (!savedSha[syncLocation.id]) {
|
||||||
return githubHelper.uploadFile(
|
result = this.downloadContent(token, syncLocation); // Get the last sha
|
||||||
token,
|
}
|
||||||
syncLocation.owner,
|
return result
|
||||||
syncLocation.repo,
|
.then(() => {
|
||||||
syncLocation.branch,
|
const sha = savedSha[syncLocation.id];
|
||||||
syncLocation.path,
|
delete savedSha[syncLocation.id];
|
||||||
providerUtils.serializeContent(content),
|
return githubHelper.uploadFile(
|
||||||
sha,
|
token,
|
||||||
)
|
syncLocation.owner,
|
||||||
|
syncLocation.repo,
|
||||||
|
syncLocation.branch,
|
||||||
|
syncLocation.path,
|
||||||
|
providerUtils.serializeContent(content),
|
||||||
|
sha,
|
||||||
|
);
|
||||||
|
})
|
||||||
.then(() => syncLocation);
|
.then(() => syncLocation);
|
||||||
},
|
},
|
||||||
publish(token, html, metadata, publishLocation) {
|
publish(token, html, metadata, publishLocation) {
|
||||||
|
@ -40,6 +40,10 @@ export default {
|
|||||||
// Ignore
|
// Ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
result.hash = utils.hash(utils.serializeObject({
|
||||||
|
...result,
|
||||||
|
hash: undefined,
|
||||||
|
}));
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,7 @@ import mainProvider from './providers/googleDriveAppDataProvider';
|
|||||||
|
|
||||||
const lastSyncActivityKey = `${utils.workspaceId}/lastSyncActivity`;
|
const lastSyncActivityKey = `${utils.workspaceId}/lastSyncActivity`;
|
||||||
let lastSyncActivity;
|
let lastSyncActivity;
|
||||||
const getStoredLastSyncActivity = () => parseInt(localStorage[lastSyncActivityKey], 10) || 0;
|
const getLastStoredSyncActivity = () => parseInt(localStorage[lastSyncActivityKey], 10) || 0;
|
||||||
const inactivityThreshold = 3 * 1000; // 3 sec
|
const inactivityThreshold = 3 * 1000; // 3 sec
|
||||||
const restartSyncAfter = 30 * 1000; // 30 sec
|
const restartSyncAfter = 30 * 1000; // 30 sec
|
||||||
const autoSyncAfter = utils.randomize(60 * 1000); // 60 sec
|
const autoSyncAfter = utils.randomize(60 * 1000); // 60 sec
|
||||||
@ -19,13 +19,13 @@ const isSyncPossible = () => !store.state.offline &&
|
|||||||
(isDataSyncPossible() || hasCurrentFileSyncLocations());
|
(isDataSyncPossible() || hasCurrentFileSyncLocations());
|
||||||
|
|
||||||
function isSyncWindow() {
|
function isSyncWindow() {
|
||||||
const storedLastSyncActivity = getStoredLastSyncActivity();
|
const storedLastSyncActivity = getLastStoredSyncActivity();
|
||||||
return lastSyncActivity === storedLastSyncActivity ||
|
return lastSyncActivity === storedLastSyncActivity ||
|
||||||
Date.now() > inactivityThreshold + storedLastSyncActivity;
|
Date.now() > inactivityThreshold + storedLastSyncActivity;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAutoSyncReady() {
|
function isAutoSyncReady() {
|
||||||
const storedLastSyncActivity = getStoredLastSyncActivity();
|
const storedLastSyncActivity = getLastStoredSyncActivity();
|
||||||
return Date.now() > autoSyncAfter + storedLastSyncActivity;
|
return Date.now() > autoSyncAfter + storedLastSyncActivity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -542,6 +542,20 @@ function requestSync() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine if we have to clean files
|
||||||
|
const fileHashesToClean = {};
|
||||||
|
if (getLastStoredSyncActivity() + utils.cleanTrashAfter < Date.now()) {
|
||||||
|
// Last synchronization happened 7 days ago
|
||||||
|
const syncDataByItemId = store.getters['data/syncDataByItemId'];
|
||||||
|
store.getters['file/items'].forEach((file) => {
|
||||||
|
// If file is in the trash and has not been modified since it was last synced
|
||||||
|
const syncData = syncDataByItemId[file.id];
|
||||||
|
if (syncData && file.parentId === 'trash' && file.hash === syncData.hash) {
|
||||||
|
fileHashesToClean[file.id] = file.hash;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Call setLastSyncActivity periodically
|
// Call setLastSyncActivity periodically
|
||||||
intervalId = utils.setInterval(() => setLastSyncActivity(), 1000);
|
intervalId = utils.setInterval(() => setLastSyncActivity(), 1000);
|
||||||
setLastSyncActivity();
|
setLastSyncActivity();
|
||||||
@ -549,6 +563,7 @@ function requestSync() {
|
|||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
cb(res);
|
cb(res);
|
||||||
};
|
};
|
||||||
|
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (isDataSyncPossible()) {
|
if (isDataSyncPossible()) {
|
||||||
@ -562,6 +577,15 @@ function requestSync() {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
// Clean files
|
||||||
|
Object.keys(fileHashesToClean).forEach((fileId) => {
|
||||||
|
const file = store.state.file.itemMap[fileId];
|
||||||
|
if (file && file.hash === fileHashesToClean[fileId]) {
|
||||||
|
store.dispatch('deleteFile', fileId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
.then(cleaner(resolve), cleaner(reject));
|
.then(cleaner(resolve), cleaner(reject));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -24,6 +24,7 @@ window.document.addEventListener('touchstart', setLastActivity);
|
|||||||
// For isWindowFocused
|
// For isWindowFocused
|
||||||
let lastFocus;
|
let lastFocus;
|
||||||
const lastFocusKey = `${workspaceId}/lastWindowFocus`;
|
const lastFocusKey = `${workspaceId}/lastWindowFocus`;
|
||||||
|
const lastOpened = parseInt(localStorage[lastFocusKey], 10) || 0;
|
||||||
const setLastFocus = () => {
|
const setLastFocus = () => {
|
||||||
lastFocus = Date.now();
|
lastFocus = Date.now();
|
||||||
localStorage[lastFocusKey] = lastFocus;
|
localStorage[lastFocusKey] = lastFocus;
|
||||||
@ -39,6 +40,8 @@ export default {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
origin,
|
origin,
|
||||||
oauth2RedirectUri: `${origin}/oauth2/callback`,
|
oauth2RedirectUri: `${origin}/oauth2/callback`,
|
||||||
|
lastOpened,
|
||||||
|
cleanTrashAfter: 7 * 1000, // 7 days
|
||||||
types: [
|
types: [
|
||||||
'contentState',
|
'contentState',
|
||||||
'syncedContent',
|
'syncedContent',
|
||||||
|
@ -56,6 +56,18 @@ const store = new Vuex.Store({
|
|||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
|
deleteFile({ getters, commit }, fileId) {
|
||||||
|
commit('file/deleteItem', fileId);
|
||||||
|
commit('content/deleteItem', `${fileId}/content`);
|
||||||
|
commit('syncedContent/deleteItem', `${fileId}/syncedContent`);
|
||||||
|
commit('contentState/deleteItem', `${fileId}/contentState`);
|
||||||
|
getters['syncLocation/items']
|
||||||
|
.filter(item => item.fileId === fileId)
|
||||||
|
.forEach(item => commit('syncLocation/deleteItem', item.id));
|
||||||
|
getters['publishLocation/items']
|
||||||
|
.filter(item => item.fileId === fileId)
|
||||||
|
.forEach(item => commit('publishLocation/deleteItem', item.id));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
modules: {
|
modules: {
|
||||||
contentState,
|
contentState,
|
||||||
|
Loading…
Reference in New Issue
Block a user