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": {
"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",

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 {
components: {
Layout,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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