Support for ARIA. Added trash
This commit is contained in:
parent
cbb44f4ea6
commit
74bceaf1ee
@ -22,7 +22,6 @@
|
||||
"dependencies": {
|
||||
"bezier-easing": "^1.1.0",
|
||||
"clipboard": "^1.7.1",
|
||||
"clunderscore": "^1.0.3",
|
||||
"compression": "^1.7.0",
|
||||
"diff-match-patch": "^1.0.0",
|
||||
"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 {
|
||||
components: {
|
||||
Layout,
|
||||
|
@ -1,24 +1,24 @@
|
||||
<template>
|
||||
<div class="button-bar">
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,20 +2,20 @@
|
||||
<div class="explorer flex flex--column">
|
||||
<div class="side-title flex flex--row flex--space-between">
|
||||
<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>
|
||||
</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>
|
||||
</button>
|
||||
<button class="side-title__button button" @click="editItem()">
|
||||
<button class="side-title__button button" @click="editItem()" v-title="'Rename'">
|
||||
<icon-pen></icon-pen>
|
||||
</button>
|
||||
<button class="side-title__button button" @click="deleteItem()">
|
||||
<button class="side-title__button button" @click="deleteItem()" v-title="'Remove'">
|
||||
<icon-delete></icon-delete>
|
||||
</button>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<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>
|
||||
</div>
|
||||
<div class="layout__panel flex flex--column" :style="{ width: styles.innerWidth + 'px' }">
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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>
|
||||
<settings-modal v-else-if="config.type === 'settings'"></settings-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 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 {
|
||||
components: {
|
||||
FilePropertiesModal,
|
||||
@ -102,6 +106,18 @@ export default {
|
||||
this.config.reject();
|
||||
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) {
|
||||
const isFocusIn = evt.type === 'focusin';
|
||||
if (evt.target.parentNode && evt.target.parentNode.parentNode) {
|
||||
@ -127,12 +143,8 @@ export default {
|
||||
mounted() {
|
||||
window.addEventListener('focusin', this.onFocusInOut);
|
||||
window.addEventListener('focusout', this.onFocusInOut);
|
||||
const eltToFocus = this.$el.querySelector('input.text-input')
|
||||
|| this.$el.querySelector('.textfield')
|
||||
|| this.$el.querySelector('.button');
|
||||
if (eltToFocus) {
|
||||
eltToFocus.focus();
|
||||
}
|
||||
const tabbables = getTabbables(this.$el);
|
||||
tabbables[0].focus();
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('focusin', this.onFocusInOut);
|
||||
@ -237,6 +249,10 @@ export default {
|
||||
.form-entry--focused & {
|
||||
color: darken($link-color, 10%);
|
||||
}
|
||||
|
||||
.form-entry--error & {
|
||||
color: darken($error-color, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.form-entry__field {
|
||||
@ -248,6 +264,10 @@ export default {
|
||||
.form-entry--focused & {
|
||||
border-color: $link-color;
|
||||
}
|
||||
|
||||
.form-entry--error & {
|
||||
border-color: $error-color;
|
||||
}
|
||||
}
|
||||
|
||||
.form-entry__actions {
|
||||
@ -286,4 +306,51 @@ export default {
|
||||
line-height: 1.4;
|
||||
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>
|
||||
|
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor}">
|
||||
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
|
||||
<button class="navigation-bar__button button" @click="toggleExplorer()">
|
||||
<button class="navigation-bar__button button" @click="toggleExplorer()" v-title="'Toggle explorer'">
|
||||
<icon-folder></icon-folder>
|
||||
</button>
|
||||
</div>
|
||||
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button">
|
||||
<button class="navigation-bar__button navigation-bar__button--stackedit button" @click="toggleSideBar()">
|
||||
<button class="navigation-bar__button navigation-bar__button--stackedit button" @click="toggleSideBar()" v-title="'Toggle side bar'">
|
||||
<icon-provider provider-id="stackedit"></icon-provider>
|
||||
</button>
|
||||
</div>
|
||||
@ -19,55 +19,55 @@
|
||||
<div class="navigation-bar__title navigation-bar__title--text text-input" :style="{width: titleWidth + 'px'}">{{title}}</div>
|
||||
<input class="navigation-bar__title navigation-bar__title--input text-input" :class="{'navigation-bar__title--focus': titleFocus, 'navigation-bar__title--scrolling': titleScrolling}" :style="{width: titleWidth + 'px'}" @focus="editTitle(true)" @blur="editTitle(false)" @keyup.enter="submitTitle()" @keyup.esc="submitTitle(true)" @mouseenter="titleHover = true" @mouseleave="titleHover = false" v-model="title">
|
||||
<div class="flex flex--row" :class="{'navigation-bar__hidden': styles.hideLocations}">
|
||||
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in syncLocations" :key="location.id" :href="location.url" target="_blank">
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
@ -368,14 +368,14 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
$r: 9px;
|
||||
$r: 10px;
|
||||
$d: $r * 2;
|
||||
$b: $d/10;
|
||||
$t: 3000ms;
|
||||
|
||||
.navigation-bar__spinner {
|
||||
width: 22px;
|
||||
margin: 8px 0 0 8px;
|
||||
margin: 7px 0 0 8px;
|
||||
color: #b2b2b2;
|
||||
|
||||
.icon {
|
||||
|
@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="side-bar flex flex--column">
|
||||
<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>
|
||||
</button>
|
||||
<div class="side-title__title">
|
||||
{{panelName}}
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<div class="stat-panel__block stat-panel__block--left" v-if="styles.showEditor">
|
||||
<span class="stat-panel__block-name">
|
||||
Markdown
|
||||
<small v-show="textSelection">(selection)</small>
|
||||
<small v-if="textSelection">(selection)</small>
|
||||
</span>
|
||||
<span v-for="stat in textStats" :key="stat.id">
|
||||
<span class="stat-panel__value">{{stat.value}}</span> {{stat.name}}
|
||||
@ -13,7 +13,7 @@
|
||||
<div class="stat-panel__block stat-panel__block--right">
|
||||
<span class="stat-panel__block-name">
|
||||
HTML
|
||||
<small v-show="htmlSelection">(selection)</small>
|
||||
<small v-if="htmlSelection">(selection)</small>
|
||||
</span>
|
||||
<span v-for="stat in htmlStats" :key="stat.id">
|
||||
<span class="stat-panel__value">{{stat.value}}</span> {{stat.name}}
|
||||
|
@ -189,48 +189,6 @@ textarea {
|
||||
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 {
|
||||
background: no-repeat center url('../assets/logo.svg');
|
||||
background-size: contain;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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="logo-background"></div>
|
||||
<div class="app-version">v{{version}} — © 2017 Benoit Schweblin</div>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<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__image">
|
||||
<icon-provider provider-id="bloggerPage"></icon-provider>
|
||||
</div>
|
||||
<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()">
|
||||
<div class="form-entry__info">
|
||||
<b>Example:</b> http://example.blogger.com/
|
||||
@ -49,7 +49,9 @@ export default modalTemplate({
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
if (this.blogUrl) {
|
||||
if (!this.blogUrl) {
|
||||
this.setError('blogUrl');
|
||||
} else {
|
||||
// Return new location
|
||||
const location = bloggerPageProvider.makeLocation(
|
||||
this.config.token, this.blogUrl, this.pageId);
|
||||
|
@ -1,11 +1,11 @@
|
||||
<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__image">
|
||||
<icon-provider provider-id="blogger"></icon-provider>
|
||||
</div>
|
||||
<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()">
|
||||
<div class="form-entry__info">
|
||||
<b>Example:</b> http://example.blogger.com/
|
||||
@ -50,7 +50,9 @@ export default modalTemplate({
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
if (this.blogUrl) {
|
||||
if (!this.blogUrl) {
|
||||
this.setError('blogUrl');
|
||||
} else {
|
||||
// Return new location
|
||||
const location = bloggerProvider.makeLocation(
|
||||
this.config.token, this.blogUrl, this.postId);
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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__image">
|
||||
<icon-provider provider-id="dropbox"></icon-provider>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<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__image">
|
||||
<icon-provider provider-id="dropbox"></icon-provider>
|
||||
</div>
|
||||
<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()">
|
||||
<div class="form-entry__info">
|
||||
<b>Example:</b> {{config.token.fullAccess ? '' : '/Applications/StackEdit (restricted)'}}/path/to/My Document.html<br>
|
||||
@ -46,7 +46,9 @@ export default modalTemplate({
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
if (dropboxProvider.checkPath(this.path)) {
|
||||
if (!dropboxProvider.checkPath(this.path)) {
|
||||
this.setError('path');
|
||||
} else {
|
||||
// Return new location
|
||||
const location = dropboxProvider.makeLocation(this.config.token, this.path);
|
||||
location.templateId = this.selectedTemplate;
|
||||
|
@ -1,11 +1,11 @@
|
||||
<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__image">
|
||||
<icon-provider provider-id="dropbox"></icon-provider>
|
||||
</div>
|
||||
<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()">
|
||||
<div class="form-entry__info">
|
||||
<b>Example:</b> {{config.token.fullAccess ? '' : '/Applications/StackEdit (restricted)'}}/path/to/My Document.md<br>
|
||||
@ -33,7 +33,9 @@ export default modalTemplate({
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
if (dropboxProvider.checkPath(this.path)) {
|
||||
if (!dropboxProvider.checkPath(this.path)) {
|
||||
this.setError('path');
|
||||
} else {
|
||||
// Return new location
|
||||
const location = dropboxProvider.makeLocation(this.config.token, this.path);
|
||||
this.config.resolve(location);
|
||||
|
@ -1,19 +1,24 @@
|
||||
<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="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
|
||||
</div>
|
||||
<div class="tabs__tab flex flex--column flex--center" :class="{'tabs__tab--active': tab === 'default'}" @click="tab = 'default'">
|
||||
</tab>
|
||||
<tab :active="tab === 'default'" @click="tab = 'default'">
|
||||
Default properties
|
||||
</div>
|
||||
</tab>
|
||||
</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>
|
||||
<div class="form-entry__field">
|
||||
<code-editor v-if="tab === 'custom'" 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>
|
||||
<code-editor lang="yaml" :value="customProperties" key="custom-properties" @changed="setCustomProperties"></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 class="modal__error modal__error--file-properties">{{error}}</div>
|
||||
@ -28,6 +33,7 @@
|
||||
<script>
|
||||
import yaml from 'js-yaml';
|
||||
import { mapGetters } from 'vuex';
|
||||
import Tab from './Tab';
|
||||
import CodeEditor from '../CodeEditor';
|
||||
import defaultProperties from '../../data/defaultFileProperties.yml';
|
||||
|
||||
@ -35,6 +41,7 @@ const emptyProperties = '# Add custom properties for the current file here to ov
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Tab,
|
||||
CodeEditor,
|
||||
},
|
||||
data: () => ({
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="form-entry">
|
||||
<div class="form-entry" :error="error">
|
||||
<label class="form-entry__label" :for="uid">{{label}}</label>
|
||||
<div class="form-entry__field">
|
||||
<slot name="field"></slot>
|
||||
@ -12,7 +12,7 @@
|
||||
import utils from '../../services/utils';
|
||||
|
||||
export default {
|
||||
props: ['label'],
|
||||
props: ['label', 'error'],
|
||||
data: () => ({
|
||||
uid: utils.uid(),
|
||||
}),
|
||||
|
@ -1,11 +1,11 @@
|
||||
<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__image">
|
||||
<icon-provider provider-id="gist"></icon-provider>
|
||||
</div>
|
||||
<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()">
|
||||
</form-entry>
|
||||
<div class="form-entry">
|
||||
@ -60,7 +60,9 @@ export default modalTemplate({
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
if (this.filename) {
|
||||
if (!this.filename) {
|
||||
this.setError('filename');
|
||||
} else {
|
||||
// Return new location
|
||||
const location = gistProvider.makeLocation(
|
||||
this.config.token, this.filename, this.isPublic, this.gistId);
|
||||
|
@ -1,11 +1,11 @@
|
||||
<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__image">
|
||||
<icon-provider provider-id="gist"></icon-provider>
|
||||
</div>
|
||||
<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()">
|
||||
</form-entry>
|
||||
<div class="form-entry">
|
||||
@ -46,7 +46,9 @@ export default modalTemplate({
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
if (this.filename) {
|
||||
if (!this.filename) {
|
||||
this.setError('filename');
|
||||
} else {
|
||||
// Return new location
|
||||
const location = gistProvider.makeLocation(
|
||||
this.config.token, this.filename, this.isPublic, this.gistId);
|
||||
|
@ -1,10 +1,10 @@
|
||||
<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__image">
|
||||
<icon-provider provider-id="github"></icon-provider>
|
||||
</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__checkbox">
|
||||
<label>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<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__image">
|
||||
<icon-provider provider-id="github"></icon-provider>
|
||||
</div>
|
||||
<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()">
|
||||
<div class="form-entry__info">
|
||||
<b>Example:</b> https://github.com/benweet/stackedit
|
||||
@ -17,7 +17,7 @@
|
||||
If not provided, the master branch will be used.
|
||||
</div>
|
||||
</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()">
|
||||
<div class="form-entry__info">
|
||||
<b>Example:</b> docs/README.md<br>
|
||||
@ -60,9 +60,17 @@ export default modalTemplate({
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
if (!this.repoUrl) {
|
||||
this.setError('repoUrl');
|
||||
}
|
||||
if (!this.path) {
|
||||
this.setError('path');
|
||||
}
|
||||
if (this.repoUrl && this.path) {
|
||||
const parsedRepo = this.repoUrl.match(/[/:]?([^/:]+)\/([^/]+?)(?:\.git)?$/);
|
||||
if (parsedRepo) {
|
||||
const parsedRepo = this.repoUrl.match(/[/:]?([^/:]+)\/([^/]+?)(?:\.git|\/)?$/);
|
||||
if (!parsedRepo) {
|
||||
this.setError('repoUrl');
|
||||
} else {
|
||||
// Return new location
|
||||
const location = githubProvider.makeLocation(
|
||||
this.config.token, parsedRepo[1], parsedRepo[2], this.branch || 'master', this.path);
|
||||
|
@ -1,11 +1,11 @@
|
||||
<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__image">
|
||||
<icon-provider provider-id="github"></icon-provider>
|
||||
</div>
|
||||
<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()">
|
||||
<div class="form-entry__info">
|
||||
<b>Example:</b> https://github.com/benweet/stackedit
|
||||
@ -17,7 +17,7 @@
|
||||
If not provided, the master branch will be used.
|
||||
</div>
|
||||
</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()">
|
||||
<div class="form-entry__info">
|
||||
<b>Example:</b> docs/README.md<br>
|
||||
@ -49,9 +49,17 @@ export default modalTemplate({
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
if (!this.repoUrl) {
|
||||
this.setError('repoUrl');
|
||||
}
|
||||
if (!this.path) {
|
||||
this.setError('path');
|
||||
}
|
||||
if (this.repoUrl && this.path) {
|
||||
const parsedRepo = this.repoUrl.match(/[/:]?([^/:]+)\/([^/]+?)(?:\.git)?$/);
|
||||
if (parsedRepo) {
|
||||
const parsedRepo = this.repoUrl.match(/[/:]?([^/:]+)\/([^/]+?)(?:\.git|\/)?$/);
|
||||
if (!parsedRepo) {
|
||||
this.setError('repoUrl');
|
||||
} else {
|
||||
// Return new location
|
||||
const location = githubProvider.makeLocation(
|
||||
this.config.token, parsedRepo[1], parsedRepo[2], this.branch || 'master', this.path);
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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__image">
|
||||
<icon-provider provider-id="googleDrive"></icon-provider>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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__image">
|
||||
<icon-provider provider-id="googleDrive"></icon-provider>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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="google-photo__tumbnail" :style="{'background-image': thumbnailUrl}"></div>
|
||||
<form-entry label="Title (optional)">
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="modal__inner-1">
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Export to HTML">
|
||||
<div class="modal__inner-2">
|
||||
<form-entry label="Template">
|
||||
<select slot="field" class="textfield" v-model="selectedTemplate" @keyup.enter="resolve()">
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="modal__inner-1">
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Insert image">
|
||||
<div class="modal__inner-2">
|
||||
<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()">
|
||||
</form-entry>
|
||||
<menu-entry @click.native="openGooglePhotos(token)" v-for="token in googlePhotosTokens" :key="token.sub">
|
||||
@ -23,23 +23,18 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import modalTemplate from './modalTemplate';
|
||||
import MenuEntry from '../menus/MenuEntry';
|
||||
import FormEntry from './FormEntry';
|
||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||
|
||||
export default {
|
||||
export default modalTemplate({
|
||||
components: {
|
||||
FormEntry,
|
||||
MenuEntry,
|
||||
},
|
||||
data: () => ({
|
||||
url: '',
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters('modal', [
|
||||
'config',
|
||||
]),
|
||||
googlePhotosTokens() {
|
||||
const googleToken = this.$store.getters['data/googleTokens'];
|
||||
return Object.keys(googleToken)
|
||||
@ -50,7 +45,9 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
if (this.url) {
|
||||
if (!this.url) {
|
||||
this.setError('url');
|
||||
} else {
|
||||
const callback = this.config.callback;
|
||||
this.config.resolve();
|
||||
callback(this.url);
|
||||
@ -75,5 +72,5 @@ export default {
|
||||
}));
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="modal__inner-1">
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Insert link">
|
||||
<div class="modal__inner-2">
|
||||
<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()">
|
||||
</form-entry>
|
||||
<div class="modal__button-bar">
|
||||
@ -14,22 +14,17 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import FormEntry from './FormEntry';
|
||||
import modalTemplate from './modalTemplate';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FormEntry,
|
||||
},
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
||||
url: '',
|
||||
}),
|
||||
computed: mapGetters('modal', [
|
||||
'config',
|
||||
]),
|
||||
methods: {
|
||||
resolve() {
|
||||
if (this.url) {
|
||||
if (!this.url) {
|
||||
this.setError('url');
|
||||
} else {
|
||||
const callback = this.config.callback;
|
||||
this.config.resolve();
|
||||
callback(this.url);
|
||||
@ -41,5 +36,5 @@ export default {
|
||||
callback(null);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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">
|
||||
<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>
|
||||
|
@ -1,19 +1,24 @@
|
||||
<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="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
|
||||
</div>
|
||||
<div class="tabs__tab flex flex--column flex--center" :class="{'tabs__tab--active': tab === 'default'}" @click="tab = 'default'">
|
||||
</tab>
|
||||
<tab :active="tab === 'default'" @click="tab = 'default'">
|
||||
Default settings
|
||||
</div>
|
||||
</tab>
|
||||
</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>
|
||||
<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 v-else lang="yaml" :value="defaultSettings" disabled="true" key="default-settings"></code-editor>
|
||||
<code-editor lang="yaml" :value="customSettings" key="custom-settings" @changed="setCustomSettings"></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 class="modal__error modal__error--settings">{{error}}</div>
|
||||
@ -28,6 +33,7 @@
|
||||
<script>
|
||||
import yaml from 'js-yaml';
|
||||
import { mapGetters } from 'vuex';
|
||||
import Tab from './Tab';
|
||||
import CodeEditor from '../CodeEditor';
|
||||
import defaultSettings from '../../data/defaultSettings.yml';
|
||||
|
||||
@ -35,6 +41,7 @@ const emptySettings = '# Add your custom settings here to override the default s
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Tab,
|
||||
CodeEditor,
|
||||
},
|
||||
data: () => ({
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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">
|
||||
<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>
|
||||
|
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>
|
||||
<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="form-entry">
|
||||
<label class="form-entry__label" for="template">Template</label>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<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__image">
|
||||
<icon-provider provider-id="wordpress"></icon-provider>
|
||||
</div>
|
||||
<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()">
|
||||
<div class="form-entry__info">
|
||||
<b>Example:</b> example.wordpress.com<br>
|
||||
@ -52,7 +52,9 @@ export default modalTemplate({
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
if (this.domain) {
|
||||
if (!this.domain) {
|
||||
this.setError('domain');
|
||||
} else {
|
||||
// Return new location
|
||||
const location = wordpressProvider.makeLocation(
|
||||
this.config.token, this.domain, this.postId);
|
||||
|
@ -1,17 +1,17 @@
|
||||
<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__image">
|
||||
<icon-provider provider-id="zendesk"></icon-provider>
|
||||
</div>
|
||||
<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()">
|
||||
<div class="form-entry__info">
|
||||
<b>Example:</b> https://example.zendesk.com/
|
||||
</div>
|
||||
</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()">
|
||||
<div class="form-entry__info">
|
||||
You have to configure an OAuth Client with redirect URL <b>{{redirectUrl}}</b><br>
|
||||
@ -40,9 +40,17 @@ export default modalTemplate({
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
if (!this.siteUrl) {
|
||||
this.setError('siteUrl');
|
||||
}
|
||||
if (!this.clientId) {
|
||||
this.setError('clientId');
|
||||
}
|
||||
if (this.siteUrl && this.clientId) {
|
||||
const parsedUrl = this.siteUrl.match(/^https:\/\/([^.]+)\.zendesk\.com/);
|
||||
if (parsedUrl) {
|
||||
if (!parsedUrl) {
|
||||
this.setError('siteUrl');
|
||||
} else {
|
||||
this.config.resolve({
|
||||
subdomain: parsedUrl[1],
|
||||
clientId: this.clientId,
|
||||
|
@ -1,11 +1,11 @@
|
||||
<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__image">
|
||||
<icon-provider provider-id="zendesk"></icon-provider>
|
||||
</div>
|
||||
<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()">
|
||||
<div class="form-entry__info">
|
||||
https://example.zendesk.com/hc/en-us/sections/<b>21857469</b>-Section-name
|
||||
@ -57,7 +57,9 @@ export default modalTemplate({
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
if (this.sectionId || this.articleId) {
|
||||
if (!this.sectionId && !this.articleId) {
|
||||
this.setError('sectionId');
|
||||
} else {
|
||||
// Return new location
|
||||
const location = zendeskProvider.makeLocation(
|
||||
this.config.token, this.sectionId, this.locale || 'en-us', this.articleId);
|
||||
|
@ -4,7 +4,12 @@ import store from '../../store';
|
||||
export default (desc) => {
|
||||
const component = {
|
||||
...desc,
|
||||
data: () => ({
|
||||
...desc.data ? desc.data() : {},
|
||||
errorTimeouts: {},
|
||||
}),
|
||||
components: {
|
||||
...desc.components || {},
|
||||
FormEntry,
|
||||
},
|
||||
computed: {
|
||||
@ -19,6 +24,16 @@ export default (desc) => {
|
||||
methods: {
|
||||
...desc.methods || {},
|
||||
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) => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'clunderscore';
|
||||
import './clunderscore';
|
||||
import cledit from './cleditCore';
|
||||
import './cleditHighlighter';
|
||||
import './cleditKeystroke';
|
||||
|
@ -59,6 +59,10 @@ liveCollectionProperties.cl_map = function (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) {
|
||||
return slice.call(this).cl_reduce(cb, memo)
|
||||
}
|
||||
|
@ -107,9 +107,6 @@ const localDbSvc = {
|
||||
this.connection.createTx((tx) => {
|
||||
this.readAll(tx, (storeItemMap) => {
|
||||
this.writeAll(storeItemMap, tx);
|
||||
if (!store.state.ready) {
|
||||
store.commit('setReady');
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
}, () => reject(new Error('Local DB access error.')));
|
||||
@ -316,57 +313,74 @@ const ifNoId = cb => (obj) => {
|
||||
|
||||
// Load the DB on boot
|
||||
localDbSvc.sync()
|
||||
// And watch file changing
|
||||
.then(() => store.watch(
|
||||
() => store.getters['file/current'].id,
|
||||
() => Promise.resolve(store.getters['file/current'])
|
||||
// If current file has no ID, get the most recent file
|
||||
.then(ifNoId(() => store.getters['file/lastOpened']))
|
||||
// If still no ID, create a new file
|
||||
.then(ifNoId(() => {
|
||||
const id = utils.uid();
|
||||
store.commit('content/setItem', {
|
||||
id: `${id}/content`,
|
||||
text: welcomeFile,
|
||||
});
|
||||
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;
|
||||
.then(() => {
|
||||
store.commit('setReady');
|
||||
|
||||
// If app was last opened 7 days ago and synchronization is off
|
||||
if (!store.getters['data/loginToken'] &&
|
||||
(utils.lastOpened + utils.cleanTrashAfter < Date.now())
|
||||
) {
|
||||
// Clean files
|
||||
store.getters['file/items'].forEach((file) => {
|
||||
// If file is in the trash
|
||||
if (file.parentId === 'trash') {
|
||||
store.dispatch('deleteFile', file.id);
|
||||
}
|
||||
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,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
// watch file changing
|
||||
store.watch(
|
||||
() => store.getters['file/current'].id,
|
||||
() => Promise.resolve(store.getters['file/current'])
|
||||
// If current file has no ID, get the most recent file
|
||||
.then(ifNoId(() => store.getters['file/lastOpened']))
|
||||
// If still no ID, create a new file
|
||||
.then(ifNoId(() => {
|
||||
const id = utils.uid();
|
||||
store.commit('content/setItem', {
|
||||
id: `${id}/content`,
|
||||
text: welcomeFile,
|
||||
});
|
||||
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
|
||||
.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
|
||||
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
|
||||
},
|
||||
uploadContent(token, content, syncLocation) {
|
||||
const sha = savedSha[syncLocation.id];
|
||||
delete savedSha[syncLocation.id];
|
||||
return githubHelper.uploadFile(
|
||||
token,
|
||||
syncLocation.owner,
|
||||
syncLocation.repo,
|
||||
syncLocation.branch,
|
||||
syncLocation.path,
|
||||
providerUtils.serializeContent(content),
|
||||
sha,
|
||||
)
|
||||
let result = Promise.resolve();
|
||||
if (!savedSha[syncLocation.id]) {
|
||||
result = this.downloadContent(token, syncLocation); // Get the last sha
|
||||
}
|
||||
return result
|
||||
.then(() => {
|
||||
const sha = savedSha[syncLocation.id];
|
||||
delete savedSha[syncLocation.id];
|
||||
return githubHelper.uploadFile(
|
||||
token,
|
||||
syncLocation.owner,
|
||||
syncLocation.repo,
|
||||
syncLocation.branch,
|
||||
syncLocation.path,
|
||||
providerUtils.serializeContent(content),
|
||||
sha,
|
||||
);
|
||||
})
|
||||
.then(() => syncLocation);
|
||||
},
|
||||
publish(token, html, metadata, publishLocation) {
|
||||
|
@ -40,6 +40,10 @@ export default {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
result.hash = utils.hash(utils.serializeObject({
|
||||
...result,
|
||||
hash: undefined,
|
||||
}));
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ import mainProvider from './providers/googleDriveAppDataProvider';
|
||||
|
||||
const lastSyncActivityKey = `${utils.workspaceId}/lastSyncActivity`;
|
||||
let lastSyncActivity;
|
||||
const getStoredLastSyncActivity = () => parseInt(localStorage[lastSyncActivityKey], 10) || 0;
|
||||
const getLastStoredSyncActivity = () => parseInt(localStorage[lastSyncActivityKey], 10) || 0;
|
||||
const inactivityThreshold = 3 * 1000; // 3 sec
|
||||
const restartSyncAfter = 30 * 1000; // 30 sec
|
||||
const autoSyncAfter = utils.randomize(60 * 1000); // 60 sec
|
||||
@ -19,13 +19,13 @@ const isSyncPossible = () => !store.state.offline &&
|
||||
(isDataSyncPossible() || hasCurrentFileSyncLocations());
|
||||
|
||||
function isSyncWindow() {
|
||||
const storedLastSyncActivity = getStoredLastSyncActivity();
|
||||
const storedLastSyncActivity = getLastStoredSyncActivity();
|
||||
return lastSyncActivity === storedLastSyncActivity ||
|
||||
Date.now() > inactivityThreshold + storedLastSyncActivity;
|
||||
}
|
||||
|
||||
function isAutoSyncReady() {
|
||||
const storedLastSyncActivity = getStoredLastSyncActivity();
|
||||
const storedLastSyncActivity = getLastStoredSyncActivity();
|
||||
return Date.now() > autoSyncAfter + storedLastSyncActivity;
|
||||
}
|
||||
|
||||
@ -542,6 +542,20 @@ function requestSync() {
|
||||
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
|
||||
intervalId = utils.setInterval(() => setLastSyncActivity(), 1000);
|
||||
setLastSyncActivity();
|
||||
@ -549,6 +563,7 @@ function requestSync() {
|
||||
clearInterval(intervalId);
|
||||
cb(res);
|
||||
};
|
||||
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
if (isDataSyncPossible()) {
|
||||
@ -562,6 +577,15 @@ function requestSync() {
|
||||
}
|
||||
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));
|
||||
}
|
||||
};
|
||||
|
@ -24,6 +24,7 @@ window.document.addEventListener('touchstart', setLastActivity);
|
||||
// For isWindowFocused
|
||||
let lastFocus;
|
||||
const lastFocusKey = `${workspaceId}/lastWindowFocus`;
|
||||
const lastOpened = parseInt(localStorage[lastFocusKey], 10) || 0;
|
||||
const setLastFocus = () => {
|
||||
lastFocus = Date.now();
|
||||
localStorage[lastFocusKey] = lastFocus;
|
||||
@ -39,6 +40,8 @@ export default {
|
||||
workspaceId,
|
||||
origin,
|
||||
oauth2RedirectUri: `${origin}/oauth2/callback`,
|
||||
lastOpened,
|
||||
cleanTrashAfter: 7 * 1000, // 7 days
|
||||
types: [
|
||||
'contentState',
|
||||
'syncedContent',
|
||||
|
@ -56,6 +56,18 @@ const store = new Vuex.Store({
|
||||
}
|
||||
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: {
|
||||
contentState,
|
||||
|
Loading…
Reference in New Issue
Block a user