Support for ARIA. Added trash

This commit is contained in:
Benoit Schweblin 2017-10-07 12:22:24 +01:00
parent cbb44f4ea6
commit 74bceaf1ee
46 changed files with 437 additions and 245 deletions

View File

@ -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",

View File

@ -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,

View File

@ -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>

View File

@ -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>

View File

@ -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' }">

View File

@ -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>

View File

@ -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 {

View File

@ -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>

View File

@ -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}}

View File

@ -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;

View File

@ -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>

View File

@ -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);

View File

@ -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);

View File

@ -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>

View File

@ -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;

View File

@ -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);

View File

@ -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: () => ({

View File

@ -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(),
}), }),

View File

@ -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);

View File

@ -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);

View File

@ -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>

View File

@ -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);

View File

@ -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);

View File

@ -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>

View File

@ -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>

View File

@ -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)">

View File

@ -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()">

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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: () => ({

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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);

View File

@ -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,

View File

@ -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);

View File

@ -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) => {

View File

@ -1,4 +1,4 @@
import 'clunderscore'; import './clunderscore';
import cledit from './cleditCore'; import cledit from './cleditCore';
import './cleditHighlighter'; import './cleditHighlighter';
import './cleditKeystroke'; import './cleditKeystroke';

View File

@ -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)
} }

View File

@ -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);

View File

@ -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) {

View File

@ -40,6 +40,10 @@ export default {
// Ignore // Ignore
} }
} }
result.hash = utils.hash(utils.serializeObject({
...result,
hash: undefined,
}));
return result; return result;
}, },
}; };

View File

@ -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));
} }
}; };

View File

@ -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',

View File

@ -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,