merge master

This commit is contained in:
Felix Wu 2018-08-20 11:50:25 +02:00
commit beb695e53c
219 changed files with 18874 additions and 10659 deletions

View File

@ -8,7 +8,7 @@
"env": {
"test": {
"presets": ["env", "stage-2"],
"plugins": [ "istanbul" ]
"plugins": ["transform-es2015-modules-commonjs", "dynamic-import-node"]
}
}
}

3
.gitignore vendored
View File

@ -3,8 +3,7 @@ node_modules/
dist/
.history
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.vscode
stackedit_v4
chrome-app/*.zip
/test/unit/coverage/

View File

@ -1,46 +0,0 @@
### v5.11
- New file properties modal with extension presets
- Added new Markdown extensions: task lists, image size, mark
### v5.10
- Added temporary folder
- New iframe mode (see [here](https://benweet.github.io/stackedit.js/))
### v5.9
- Added explorer context menu
### v5.8
- New import menu with HTML to Markdown conversion
- HTML to Markdown conversion when pasting rich text in the editor
- Custom scrollbars on webkit
### v5.7
- Support for CouchDB workspaces
- Added FAQ
- Added welcome tour
### v5.6
- Themes support with new dark theme
### v5.5
- Integration with Google Drive
- New landing page
### v5.4
- Multi-workspaces capabilities
### v5.3
- Revision history
### v5.2
- Support for discussions/comments

View File

@ -8,7 +8,9 @@ ENV V4_VERSION 4.3.22
RUN npm pack stackedit@$V4_VERSION \
&& tar xzf stackedit-*.tgz --strip 1 \
&& yarn \
&& yarn cache clean
&& yarn cache clean \
&& rm -rf ~/.cache/bower \
&& rm -rf ~/.local/share/bower
WORKDIR /opt/stackedit

View File

@ -6,11 +6,12 @@
https://stackedit.io/
### NEW!!! Embed StackEdit in any website!
### Ecosystem
See https://github.com/benweet/stackedit.js
Chrome extension: https://chrome.google.com/webstore/detail/ajehldoplanpchfokmeempkekhnhmoha
- [Chrome app](https://chrome.google.com/webstore/detail/iiooodelglhkcpgbajoejffhijaclcdg)
- NEW! Embed StackEdit in any website with [stackedit.js](https://github.com/benweet/stackedit.js)
- NEW! [Chrome extension](https://chrome.google.com/webstore/detail/ajehldoplanpchfokmeempkekhnhmoha) that uses stackedit.js
- [Community](https://community.stackedit.io/)
### Build Setup

View File

@ -2,6 +2,7 @@ var path = require('path')
var webpack = require('webpack')
var utils = require('./utils')
var config = require('../config')
var VueLoaderPlugin = require('vue-loader/lib/plugin')
var vueLoaderConfig = require('./vue-loader.conf')
var StylelintPlugin = require('stylelint-webpack-plugin')
var FaviconsWebpackPlugin = require('favicons-webpack-plugin')
@ -81,6 +82,7 @@ module.exports = {
]
},
plugins: [
new VueLoaderPlugin(),
new StylelintPlugin({
files: ['**/*.vue', '**/*.scss']
}),

View File

@ -98,6 +98,7 @@ var webpackConfig = merge(baseWebpackConfig, {
ServiceWorker: {
events: true
},
AppCache: true,
excludes: ['**/.*', '**/*.map', '**/index.html', '**/static/oauth2/callback.html', '**/icons-*/*.png', '**/static/fonts/KaTeX_*'],
externals: ['/', '/app', '/oauth2/callback']
})

View File

@ -14,7 +14,7 @@ function resolve (dir) {
module.exports = {
entry: {
style: './src/components/style.scss'
style: './src/styles/'
},
module: {
rules: [{

15565
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "stackedit",
"version": "5.11.4",
"version": "5.12.0",
"description": "Free, open-source, full-featured Markdown editor",
"author": "Benoit Schweblin",
"license": "Apache-2.0",
@ -14,7 +14,9 @@
"build": "node build/build.js && npm run build-style",
"build-style": "webpack --config build/webpack.style.conf.js",
"lint": "eslint --ext .js,.vue src server",
"test": "npm run lint",
"unit": "jest --config test/unit/jest.conf.js --runInBand",
"unit-with-coverage": "jest --config test/unit/jest.conf.js --runInBand --coverage",
"test": "npm run lint && npm run unit",
"preversion": "npm run test",
"postversion": "git push origin master --tags && npm publish",
"patch": "npm version patch -m \"Tag v%s\"",
@ -22,20 +24,22 @@
"major": "npm version major -m \"Tag v%s\""
},
"dependencies": {
"@vue/test-utils": "^1.0.0-beta.16",
"abcjs": "^5.2.0",
"aws-sdk": "^2.133.0",
"babel-runtime": "^6.26.0",
"bezier-easing": "^1.1.0",
"body-parser": "^1.18.2",
"clipboard": "^1.7.1",
"compression": "^1.7.0",
"diff-match-patch": "^1.0.0",
"file-saver": "^1.3.3",
"file-saver": "^1.3.8",
"google-id-token-verifier": "^0.2.3",
"handlebars": "^4.0.10",
"indexeddbshim": "^3.0.4",
"js-yaml": "^3.9.1",
"katex": "^0.9.0-alpha1",
"markdown-it": "^8.3.1",
"indexeddbshim": "^3.6.2",
"js-yaml": "^3.11.0",
"katex": "^v0.10.0-alpha",
"markdown-it": "^8.4.1",
"markdown-it-abbr": "^1.0.4",
"markdown-it-deflist": "^2.0.2",
"markdown-it-emoji": "^1.3.0",
@ -47,21 +51,24 @@
"markdown-it-sup": "^1.0.0",
"mermaid": "^7.1.0",
"mousetrap": "^1.6.1",
"normalize-scss": "^7.0.0",
"normalize-scss": "^7.0.1",
"prismjs": "^1.6.0",
"request": "^2.82.0",
"serve-static": "^1.12.6",
"request": "^2.85.0",
"serve-static": "^1.13.2",
"tmp": "^0.0.33",
"turndown": "^4.0.1",
"vue": "^2.3.3",
"vuex": "^2.3.1"
"turndown": "^4.0.2",
"vue": "^2.5.16",
"vuex": "^3.0.1"
},
"devDependencies": {
"autoprefixer": "^6.7.2",
"babel-core": "^6.22.1",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.2.10",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.3",
"babel-jest": "^21.0.2",
"babel-loader": "^7.1.4",
"babel-plugin-dynamic-import-node": "^1.2.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.23.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
@ -69,57 +76,63 @@
"chalk": "^1.1.3",
"connect-history-api-fallback": "^1.3.0",
"copy-webpack-plugin": "^4.5.1",
"css-loader": "^0.28.7",
"eslint": "^3.19.0",
"eslint-config-airbnb-base": "^11.1.3",
"eslint-friendly-formatter": "^2.0.7",
"eslint-import-resolver-webpack": "^0.8.1",
"eslint-loader": "^1.7.1",
"eslint-plugin-html": "^2.0.0",
"eslint-plugin-import": "^2.2.0",
"css-loader": "^0.28.11",
"eslint": "^4.19.1",
"eslint-config-airbnb-base": "^12.1.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-import-resolver-webpack": "^0.9.0",
"eslint-loader": "^2.0.0",
"eslint-plugin-html": "^4.0.3",
"eslint-plugin-import": "^2.11.0",
"eventsource-polyfill": "^0.9.6",
"express": "^4.15.5",
"express": "^4.16.3",
"extract-text-webpack-plugin": "^2.0.0",
"favicons-webpack-plugin": "^0.0.7",
"file-loader": "^0.11.1",
"friendly-errors-webpack-plugin": "^1.1.3",
"favicons-webpack-plugin": "^0.0.9",
"file-loader": "^1.1.11",
"friendly-errors-webpack-plugin": "^1.7.0",
"gulp": "^3.9.1",
"gulp-concat": "^2.6.1",
"html-webpack-plugin": "^2.28.0",
"http-proxy-middleware": "^0.17.3",
"html-webpack-plugin": "^3.2.0",
"http-proxy-middleware": "^0.18.0",
"identity-obj-proxy": "^3.0.0",
"ignore-loader": "^0.1.2",
"node-sass": "^4.5.3",
"jest": "^23.0.0",
"jest-raw-loader": "^1.0.1",
"jest-serializer-vue": "^0.3.0",
"node-sass": "^4.9.0",
"npm-bump": "^0.0.23",
"offline-plugin": "^4.8.4",
"offline-plugin": "^5.0.3",
"opn": "^4.0.2",
"optimize-css-assets-webpack-plugin": "^1.3.0",
"optimize-css-assets-webpack-plugin": "^1.3.2",
"ora": "^1.2.0",
"raw-loader": "^0.5.1",
"rimraf": "^2.6.0",
"sass-loader": "^6.0.5",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"sass-loader": "^7.0.1",
"semver": "^5.5.0",
"shelljs": "^0.8.1",
"stylelint": "^9.2.0",
"stylelint-config-standard": "^16.0.0",
"stylelint-processor-html": "^1.0.0",
"stylelint-webpack-plugin": "^0.7.0",
"url-loader": "^0.5.8",
"vue-loader": "^12.1.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.3.3",
"stylelint-webpack-plugin": "^0.10.4",
"url-loader": "^1.0.1",
"vue-jest": "^1.0.2",
"vue-loader": "^15.0.9",
"vue-style-loader": "^4.1.0",
"vue-template-compiler": "^2.5.16",
"webpack": "^2.6.1",
"webpack-bundle-analyzer": "^2.2.1",
"webpack-dev-middleware": "^1.10.0",
"webpack-hot-middleware": "^2.18.0",
"webpack-merge": "^4.1.0",
"worker-loader": "^0.8.1"
"webpack-merge": "^4.1.2",
"worker-loader": "^1.1.1"
},
"engines": {
"node": ">= 4.0.0",
"npm": ">= 3.0.0"
"node": ">= 8.0.0",
"npm": ">= 5.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
"not ie <= 10"
]
}

View File

@ -29,5 +29,8 @@ exports.githubToken = (req, res) => {
githubToken(req.query.clientId, req.query.code)
.then(
token => res.send(token),
err => res.status(400).send(err ? err.message || err.toString() : 'bad_code'));
err => res
.status(400)
.send(err ? err.message || err.toString() : 'bad_code'),
);
};

View File

@ -1,5 +1,5 @@
/* global window */
const spawn = require('child_process').spawn;
const { spawn } = require('child_process');
const fs = require('fs');
const tmp = require('tmp');
const user = require('./user');
@ -76,7 +76,7 @@ exports.generate = (req, res) => {
params.push('--toc');
}
options.tocDepth = parseInt(options.tocDepth, 10);
if (!isNaN(options.tocDepth)) {
if (!Number.isNaN(options.tocDepth)) {
params.push('--toc-depth', options.tocDepth);
}
options.highlightStyle = highlightStyles.indexOf(options.highlightStyle) !== -1 ? options.highlightStyle : 'kate';

View File

@ -1,5 +1,5 @@
/* global window,MathJax */
const spawn = require('child_process').spawn;
const { spawn } = require('child_process');
const fs = require('fs');
const tmp = require('tmp');
const user = require('./user');
@ -84,13 +84,13 @@ exports.generate = (req, res) => {
// Margins
const marginTop = parseInt(`${options.marginTop}`, 10);
params.push('-T', isNaN(marginTop) ? 25 : marginTop);
params.push('-T', Number.isNaN(marginTop) ? 25 : marginTop);
const marginRight = parseInt(`${options.marginRight}`, 10);
params.push('-R', isNaN(marginRight) ? 25 : marginRight);
params.push('-R', Number.isNaN(marginRight) ? 25 : marginRight);
const marginBottom = parseInt(`${options.marginBottom}`, 10);
params.push('-B', isNaN(marginBottom) ? 25 : marginBottom);
params.push('-B', Number.isNaN(marginBottom) ? 25 : marginBottom);
const marginLeft = parseInt(`${options.marginLeft}`, 10);
params.push('-L', isNaN(marginLeft) ? 25 : marginLeft);
params.push('-L', Number.isNaN(marginLeft) ? 25 : marginLeft);
// Header
if (options.headerCenter) {

View File

@ -2,10 +2,12 @@ const request = require('request');
const AWS = require('aws-sdk');
const verifier = require('google-id-token-verifier');
const BUCKET_NAME = process.env.USER_BUCKET_NAME || 'stackedit-users';
const PAYPAL_URI = process.env.PAYPAL_URI || 'https://www.paypal.com/cgi-bin/webscr';
const PAYPAL_RECEIVER_EMAIL = process.env.PAYPAL_RECEIVER_EMAIL || 'stackedit.project@gmail.com';
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const {
USER_BUCKET_NAME = 'stackedit-users',
PAYPAL_URI = 'https://www.paypal.com/cgi-bin/webscr',
PAYPAL_RECEIVER_EMAIL = 'stackedit.project@gmail.com',
GOOGLE_CLIENT_ID,
} = process.env;
const s3Client = new AWS.S3();
const cb = (resolve, reject) => (err, res) => {
@ -18,21 +20,22 @@ const cb = (resolve, reject) => (err, res) => {
exports.getUser = id => new Promise((resolve, reject) => {
s3Client.getObject({
Bucket: BUCKET_NAME,
Bucket: USER_BUCKET_NAME,
Key: id,
}, cb(resolve, reject));
})
.then(
res => JSON.parse(`${res.Body}`),
(err) => {
if (err.code !== 'NoSuchKey') {
throw err;
}
});
res => JSON.parse(`${res.Body}`),
(err) => {
if (err.code !== 'NoSuchKey') {
throw err;
}
},
);
exports.putUser = (id, user) => new Promise((resolve, reject) => {
s3Client.putObject({
Bucket: BUCKET_NAME,
Bucket: USER_BUCKET_NAME,
Key: id,
Body: JSON.stringify(user),
}, cb(resolve, reject));
@ -40,20 +43,24 @@ exports.putUser = (id, user) => new Promise((resolve, reject) => {
exports.removeUser = id => new Promise((resolve, reject) => {
s3Client.deleteObject({
Bucket: BUCKET_NAME,
Bucket: USER_BUCKET_NAME,
Key: id,
}, cb(resolve, reject));
});
exports.getUserFromToken = idToken => new Promise(
(resolve, reject) => verifier.verify(idToken, GOOGLE_CLIENT_ID, cb(resolve, reject)))
exports.getUserFromToken = idToken => new Promise((resolve, reject) => verifier
.verify(idToken, GOOGLE_CLIENT_ID, cb(resolve, reject)))
.then(tokenInfo => exports.getUser(tokenInfo.sub));
exports.userInfo = (req, res) => exports.getUserFromToken(req.query.idToken)
.then(user => res.send(Object.assign({
sponsorUntil: 0,
}, user)),
err => res.status(400).send(err ? err.message || err.toString() : 'invalid_token'));
.then(
user => res.send(Object.assign({
sponsorUntil: 0,
}, user)),
err => res
.status(400)
.send(err ? err.message || err.toString() : 'invalid_token'),
);
exports.paypalIpn = (req, res, next) => Promise.resolve()
.then(() => {

View File

@ -2,14 +2,16 @@
<div class="app" :class="classes">
<splash-screen v-if="!ready"></splash-screen>
<layout v-else></layout>
<modal v-if="showModal"></modal>
<modal></modal>
<notification></notification>
<context-menu></context-menu>
</div>
</template>
<script>
import Vue from 'vue';
import '../styles';
import '../styles/markdownHighlighting.scss';
import '../styles/app.scss';
import Layout from './Layout';
import Modal from './Modal';
import Notification from './Notification';
@ -19,50 +21,7 @@ import syncSvc from '../services/syncSvc';
import networkSvc from '../services/networkSvc';
import sponsorSvc from '../services/sponsorSvc';
import tempFileSvc from '../services/tempFileSvc';
import timeSvc from '../services/timeSvc';
import store from '../store';
// Global directives
Vue.directive('focus', {
inserted(el) {
el.focus();
const value = el.value;
if (value && el.setSelectionRange) {
el.setSelectionRange(0, value.length);
}
},
});
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);
},
});
// Global filters
Vue.filter('formatTime', time =>
// Access the minute counter for reactive refresh
timeSvc.format(time, store.state.minuteCounter));
import './common/vueGlobals';
const themeClasses = {
light: ['app--light'],
@ -85,28 +44,22 @@ export default {
const result = themeClasses[this.$store.getters['data/computedSettings'].colorTheme];
return Array.isArray(result) ? result : themeClasses.light;
},
showModal() {
return !!this.$store.getters['modal/config'];
},
},
created() {
syncSvc.init()
.then(() => {
networkSvc.init();
sponsorSvc.init();
this.ready = true;
tempFileSvc.setReady();
})
.catch((err) => {
if (err && err.message !== 'reload') {
console.error(err); // eslint-disable-line no-console
this.$store.dispatch('notification/error', err);
}
});
async created() {
try {
await syncSvc.init();
await networkSvc.init();
await sponsorSvc.init();
this.ready = true;
tempFileSvc.setReady();
} catch (err) {
if (err && err.message === 'RELOAD') {
window.location.reload();
} else if (err && err.message !== 'RELOAD') {
console.error(err); // eslint-disable-line no-console
this.$store.dispatch('notification/error', err);
}
}
},
};
</script>
<style lang="scss">
@import 'common/app';
</style>

View File

@ -1,24 +1,24 @@
<template>
<div class="button-bar">
<div class="button-bar__inner button-bar__inner--top">
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.showNavigationBar }" v-if="!light" @click="toggleNavigationBar()" v-title="'Toggle navigation bar'">
<button class="button-bar__button button-bar__button--navigation-bar-toggler button" :class="{ 'button-bar__button--on': layoutSettings.showNavigationBar }" v-if="!light" @click="toggleNavigationBar()" v-title="'Toggle navigation bar'">
<icon-navigation-bar></icon-navigation-bar>
</button>
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.showSidePreview }" tour-step-anchor="editor" @click="toggleSidePreview()" v-title="'Toggle side preview'">
<button class="button-bar__button button-bar__button--side-preview-toggler button" :class="{ 'button-bar__button--on': layoutSettings.showSidePreview }" tour-step-anchor="editor" @click="toggleSidePreview()" v-title="'Toggle side preview'">
<icon-side-preview></icon-side-preview>
</button>
<button class="button-bar__button button" @click="toggleEditor(false)" v-title="'Reader mode'">
<button class="button-bar__button button-bar__button--editor-toggler button" @click="toggleEditor(false)" v-title="'Reader mode'">
<icon-eye></icon-eye>
</button>
</div>
<div class="button-bar__inner button-bar__inner--bottom">
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.focusMode }" @click="toggleFocusMode()" v-title="'Toggle focus mode'">
<button class="button-bar__button button-bar__button--focus-mode-toggler button" :class="{ 'button-bar__button--on': layoutSettings.focusMode }" @click="toggleFocusMode()" v-title="'Toggle focus mode'">
<icon-target></icon-target>
</button>
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.scrollSync }" @click="toggleScrollSync()" v-title="'Toggle scroll sync'">
<button class="button-bar__button button-bar__button--scroll-sync-toggler button" :class="{ 'button-bar__button--on': layoutSettings.scrollSync }" @click="toggleScrollSync()" v-title="'Toggle scroll sync'">
<icon-scroll-sync></icon-scroll-sync>
</button>
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.showStatusBar }" @click="toggleStatusBar()" v-title="'Toggle status bar'">
<button class="button-bar__button button-bar__button--status-bar-toggler button" :class="{ 'button-bar__button--on': layoutSettings.showStatusBar }" @click="toggleStatusBar()" v-title="'Toggle status bar'">
<icon-status-bar></icon-status-bar>
</button>
</div>
@ -49,7 +49,7 @@ export default {
</script>
<style lang="scss">
@import 'common/variables.scss';
@import '../styles/variables.scss';
.button-bar {
position: absolute;

View File

@ -4,7 +4,7 @@
<script>
import Prism from 'prismjs';
import cledit from '../services/cledit';
import cledit from '../services/editor/cledit';
export default {
props: ['value', 'lang', 'disabled'],
@ -28,7 +28,7 @@ export default {
</script>
<style lang="scss">
@import 'common/variables.scss';
@import '../styles/variables.scss';
.code-editor {
margin: 0;

View File

@ -4,7 +4,7 @@
<div v-for="(item, idx) in items" :key="idx">
<div class="context-menu__separator" v-if="item.type === 'separator'"></div>
<div class="context-menu__item context-menu__item--disabled" v-else-if="item.disabled">{{item.name}}</div>
<a class="context-menu__item" href="javascript:void(0)" v-else @click.stop="close(item)">{{item.name}}</a>
<a class="context-menu__item" href="javascript:void(0)" v-else @click="close(item)">{{item.name}}</a>
</div>
</div>
</div>
@ -22,10 +22,8 @@ export default {
]),
},
methods: {
close(item) {
if (item) {
this.resolve(item);
}
close(item = null) {
this.resolve(item);
this.$store.dispatch('contextMenu/close');
},
},

View File

@ -66,13 +66,14 @@ export default {
editorElt.querySelectorAll(`.discussion-editor-highlighting--${discussionId}`)
.cl_each(elt => elt.classList.add('discussion-editor-highlighting--selected'));
}
});
},
);
},
};
</script>
<style lang="scss">
@import 'common/variables.scss';
@import '../styles/variables.scss';
.editor {
position: absolute;

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()" v-title="'New file'">
<button class="side-title__button side-title__button--new-file button" @click="newItem()" v-title="'New file'">
<icon-file-plus></icon-file-plus>
</button>
<button class="side-title__button button" @click="newItem(true)" v-title="'New folder'">
<button class="side-title__button side-title__button--new-folder button" @click="newItem(true)" v-title="'New folder'">
<icon-folder-plus></icon-folder-plus>
</button>
<button class="side-title__button button" @click="deleteItem()" v-title="'Delete'">
<button class="side-title__button side-title__button--delete button" @click="deleteItem()" v-title="'Delete'">
<icon-delete></icon-delete>
</button>
<button class="side-title__button button" @click="editItem()" v-title="'Rename'">
<button class="side-title__button side-title__button--rename button" @click="editItem()" v-title="'Rename'">
<icon-pen></icon-pen>
</button>
</div>
<button class="side-title__button button" @click="toggleExplorer(false)" v-title="'Close explorer'">
<button class="side-title__button side-title__button--close button" @click="toggleExplorer(false)" v-title="'Close explorer'">
<icon-close></icon-close>
</button>
</div>
@ -28,6 +28,7 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import ExplorerNode from './ExplorerNode';
import explorerSvc from '../services/explorerSvc';
export default {
components: {
@ -49,10 +50,8 @@ export default {
...mapActions('data', [
'toggleExplorer',
]),
...mapActions('explorer', [
'newItem',
'deleteItem',
]),
newItem: isFolder => explorerSvc.newItem(isFolder),
deleteItem: () => explorerSvc.deleteItem(),
editItem() {
const node = this.selectedNode;
if (!node.isTrash && !node.isTemp) {
@ -68,7 +67,8 @@ export default {
this.$store.dispatch('explorer/openNode', currentFileId);
}, {
immediate: true,
});
},
);
},
};
</script>

View File

@ -1,15 +1,15 @@
<template>
<div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--open': isOpen, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="node.noDrop || setDragTarget(node.item.id)" @dragleave.stop="isDragTarget && setDragTargetId()" @drop.prevent.stop="onDrop" @contextmenu="onContextMenu">
<div class="explorer-node__item-editor" v-if="isEditing" :class="['explorer-node__item-editor--' + node.item.type]" :style="{paddingLeft: leftPadding}" draggable="true" @dragstart.stop.prevent>
<div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--folder': node.isFolder, 'explorer-node--open': isOpen, 'explorer-node--trash': node.isTrash, 'explorer-node--temp': node.isTemp, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="node.noDrop || setDragTarget(node)" @dragleave.stop="isDragTarget && setDragTarget()" @drop.prevent.stop="onDrop" @contextmenu="onContextMenu">
<div class="explorer-node__item-editor" v-if="isEditing" :style="{paddingLeft: leftPadding}" draggable="true" @dragstart.stop.prevent>
<input type="text" class="text-input" v-focus @blur="submitEdit()" @keydown.stop @keydown.enter="submitEdit()" @keydown.esc="submitEdit(true)" v-model="editingNodeName">
</div>
<div class="explorer-node__item" v-else :class="['explorer-node__item--' + node.item.type]" :style="{paddingLeft: leftPadding}" @click="select()" draggable="true" @dragstart.stop="setDragSourceId" @dragend.stop="setDragTargetId()">
<div class="explorer-node__item" v-else :style="{paddingLeft: leftPadding}" @click="select()" draggable="true" @dragstart.stop="setDragSourceId" @dragend.stop="setDragTarget()">
{{node.item.name}}
<icon-provider class="explorer-node__location" v-for="location in node.locations" :key="location.id" :provider-id="location.providerId"></icon-provider>
</div>
<div class="explorer-node__children" v-if="node.isFolder && isOpen">
<explorer-node v-for="node in node.folders" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
<div v-if="newChild" class="explorer-node__new-child" :class="['explorer-node__new-child--' + newChild.item.type]" :style="{paddingLeft: childLeftPadding}">
<div v-if="newChild" class="explorer-node__new-child" :class="{'explorer-node__new-child--folder': newChild.isFolder}" :style="{paddingLeft: childLeftPadding}">
<input type="text" class="text-input" v-focus @blur="submitNewChild()" @keydown.stop @keydown.enter="submitNewChild()" @keydown.esc="submitNewChild(true)" v-model.trim="newChildName">
</div>
<explorer-node v-for="node in node.files" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
@ -19,7 +19,8 @@
<script>
import { mapMutations, mapActions } from 'vuex';
import utils from '../services/utils';
import workspaceSvc from '../services/workspaceSvc';
import explorerSvc from '../services/explorerSvc';
export default {
name: 'explorer-node', // Required for recursivity
@ -72,13 +73,10 @@ export default {
},
methods: {
...mapMutations('explorer', [
'setDragTargetId',
'setEditingId',
]),
...mapActions('explorer', [
'setDragTarget',
'newItem',
'deleteItem',
]),
select(id = this.node.item.id, doOpen = true) {
const node = this.$store.getters['explorer/nodeMap'][id];
@ -98,35 +96,37 @@ export default {
}
return true;
},
submitNewChild(cancel) {
const newChildNode = this.$store.state.explorer.newChildNode;
async submitNewChild(cancel) {
const { newChildNode } = this.$store.state.explorer;
if (!cancel && !newChildNode.isNil && newChildNode.item.name) {
if (newChildNode.isFolder) {
const id = utils.uid();
this.$store.commit('folder/setItem', {
...newChildNode.item,
id,
name: utils.sanitizeName(newChildNode.item.name),
});
this.select(id);
} else {
this.$store.dispatch('createFile', newChildNode.item)
.then(file => this.select(file.id));
try {
if (newChildNode.isFolder) {
const item = await workspaceSvc.storeItem(newChildNode.item);
this.select(item.id);
} else {
const item = await workspaceSvc.createFile(newChildNode.item);
this.select(item.id);
}
} catch (e) {
// Cancel
}
}
this.$store.commit('explorer/setNewItem', null);
},
submitEdit(cancel) {
const editingNode = this.$store.getters['explorer/editingNode'];
const id = editingNode.item.id;
async submitEdit(cancel) {
const { item } = this.$store.getters['explorer/editingNode'];
const value = this.editingValue;
if (!cancel && id && value) {
this.$store.commit(editingNode.isFolder ? 'folder/patchItem' : 'file/patchItem', {
id,
name: utils.sanitizeName(value),
});
}
this.setEditingId(null);
if (!cancel && item.id && value) {
try {
await workspaceSvc.storeItem({
...item,
name: value,
});
} catch (e) {
// Cancel
}
}
},
setDragSourceId(evt) {
if (this.node.noDrag) {
@ -141,27 +141,22 @@ export default {
onDrop() {
const sourceNode = this.$store.getters['explorer/dragSourceNode'];
const targetNode = this.$store.getters['explorer/dragTargetNodeFolder'];
this.setDragTargetId();
this.setDragTarget();
if (!sourceNode.isNil
&& !targetNode.isNil
&& sourceNode.item.id !== targetNode.item.id
) {
const patch = {
id: sourceNode.item.id,
workspaceSvc.storeItem({
...sourceNode.item,
parentId: targetNode.item.id,
};
if (sourceNode.isFolder) {
this.$store.commit('folder/patchItem', patch);
} else {
this.$store.commit('file/patchItem', patch);
}
});
}
},
onContextMenu(evt) {
async onContextMenu(evt) {
if (this.select(undefined, false)) {
evt.preventDefault();
evt.stopPropagation();
this.$store.dispatch('contextMenu/open', {
const item = await this.$store.dispatch('contextMenu/open', {
coordinates: {
left: evt.clientX,
top: evt.clientY,
@ -169,11 +164,11 @@ export default {
items: [{
name: 'New file',
disabled: !this.node.isFolder || this.node.isTrash,
perform: () => this.newItem(false),
perform: () => explorerSvc.newItem(false),
}, {
name: 'New folder',
disabled: !this.node.isFolder || this.node.isTrash || this.node.isTemp,
perform: () => this.newItem(true),
perform: () => explorerSvc.newItem(true),
}, {
type: 'separator',
}, {
@ -182,10 +177,12 @@ export default {
perform: () => this.setEditingId(this.node.item.id),
}, {
name: 'Delete',
perform: () => this.deleteItem(),
perform: () => explorerSvc.deleteItem(),
}],
})
.then(item => item.perform());
});
if (item) {
item.perform();
}
}
},
},
@ -229,17 +226,25 @@ $item-font-size: 14px;
}
}
.explorer-node__item--folder,
.explorer-node__item-editor--folder,
.explorer-node--trash,
.explorer-node--temp {
color: rgba(0, 0, 0, 0.5);
}
.explorer-node--folder > .explorer-node__item,
.explorer-node--folder > .explorer-node__item-editor,
.explorer-node__new-child--folder {
&::before {
content: '▹';
position: absolute;
margin-left: -13px;
}
}
.explorer-node--open > & {
content: '▾';
}
.explorer-node--folder.explorer-node--open > .explorer-node__item,
.explorer-node--folder.explorer-node--open > .explorer-node__item-editor {
&::before {
content: '▾';
}
}

View File

@ -34,7 +34,7 @@
<script>
import { mapState } from 'vuex';
import editorSvc from '../services/editorSvc';
import cledit from '../services/cledit';
import cledit from '../services/editor/cledit';
import store from '../store';
import EditorClassApplier from './common/EditorClassApplier';
@ -70,7 +70,8 @@ class DynamicClassApplier {
() => ({
start: this.startMarker.offset,
end: this.endMarker.offset,
}));
}),
);
}
}
@ -126,7 +127,10 @@ export default {
offsetList.forEach((offset, i) => {
const key = `${offset.start}:${offset.end}`;
this.classAppliers[key] = oldClassAppliers[key] || new DynamicClassApplier(
'find-replace-highlighting', offset, i > 200);
'find-replace-highlighting',
offset,
i > 200,
);
});
} catch (e) {
// Ignore
@ -156,9 +160,9 @@ export default {
this.findPosition = 0;
},
find(mode = 'forward') {
const selectedClassApplier = this.selectedClassApplier;
const { selectedClassApplier } = this;
this.unselectClassApplier();
const selectionMgr = editorSvc.clEditor.selectionMgr;
const { selectionMgr } = editorSvc.clEditor;
const startOffset = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd);
const endOffset = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd);
const keys = Object.keys(this.classAppliers);
@ -206,7 +210,10 @@ export default {
return;
}
editorSvc.clEditor.replaceAll(
this.replaceRegex, this.replaceText, this.selectedClassApplier.startMarker.offset);
this.replaceRegex,
this.replaceText,
this.selectedClassApplier.startMarker.offset,
);
this.$nextTick(() => this.find());
}
},
@ -227,7 +234,9 @@ export default {
// Highlight occurences
this.debouncedHighlightOccurrences = cledit.Utils.debounce(
() => this.highlightOccurrences(), 25);
() => this.highlightOccurrences(),
25,
);
// Refresh highlighting when find text changes or changing options
this.$watch(() => this.findText, this.debouncedHighlightOccurrences);
this.$watch(() => this.findCaseSensitive, this.debouncedHighlightOccurrences);
@ -273,7 +282,7 @@ export default {
</script>
<style lang="scss">
@import 'common/variables.scss';
@import '../styles/variables.scss';
.find-replace {
padding: 0 35px 0 25px;
@ -344,7 +353,7 @@ export default {
.find-replace__find-stats {
text-align: right;
font-size: 0.75em;
opacity: 0.5;
opacity: 0.6;
}
.find-replace-highlighting {

View File

@ -140,7 +140,7 @@ export default {
</script>
<style lang="scss">
@import 'common/variables.scss';
@import '../styles/variables.scss';
.layout {
position: absolute;

View File

@ -1,11 +1,11 @@
<template>
<div class="modal" @keydown.esc="onEscape" @keydown.tab="onTab">
<div class="modal" v-if="config" @keydown.esc="onEscape" @keydown.tab="onTab" @focusin="onFocusInOut" @focusout="onFocusInOut">
<component v-if="currentModalComponent" :is="currentModalComponent"></component>
<modal-inner v-else aria-label="Dialog">
<div class="modal__content" v-html="config.content"></div>
<div class="modal__content" v-html="simpleModal.contentHtml(config)"></div>
<div class="modal__button-bar">
<button class="button" v-if="config.rejectText" @click="config.reject()">{{config.rejectText}}</button>
<button class="button" v-if="config.resolveText" @click="config.resolve()">{{config.resolveText}}</button>
<button class="button" v-if="simpleModal.rejectText" @click="config.reject()">{{simpleModal.rejectText}}</button>
<button class="button button--resolve" v-if="simpleModal.resolveText" @click="config.resolve()">{{simpleModal.resolveText}}</button>
</div>
</modal-inner>
</div>
@ -13,6 +13,7 @@
<script>
import { mapGetters } from 'vuex';
import simpleModals from '../data/simpleModals';
import editorSvc from '../services/editorSvc';
import ModalInner from './modals/common/ModalInner';
import FilePropertiesModal from './modals/FilePropertiesModal';
@ -41,6 +42,7 @@ import DropboxPublishModal from './modals/providers/DropboxPublishModal';
import GithubAccountModal from './modals/providers/GithubAccountModal';
import GithubOpenModal from './modals/providers/GithubOpenModal';
import GithubSaveModal from './modals/providers/GithubSaveModal';
import GithubWorkspaceModal from './modals/providers/GithubWorkspaceModal';
import GithubPublishModal from './modals/providers/GithubPublishModal';
import GistSyncModal from './modals/providers/GistSyncModal';
import GistPublishModal from './modals/providers/GistPublishModal';
@ -84,6 +86,7 @@ export default {
GithubAccountModal,
GithubOpenModal,
GithubSaveModal,
GithubWorkspaceModal,
GithubPublishModal,
GistSyncModal,
GistPublishModal,
@ -110,6 +113,9 @@ export default {
}
return null;
},
simpleModal() {
return simpleModals[this.config.type] || {};
},
},
methods: {
onEscape() {
@ -132,14 +138,14 @@ export default {
const isFocusIn = evt.type === 'focusin';
if (evt.target.parentNode && evt.target.parentNode.parentNode) {
// Focus effect
if (evt.target.parentNode.classList.contains('form-entry__field') &&
evt.target.parentNode.parentNode.classList.contains('form-entry')) {
if (evt.target.parentNode.classList.contains('form-entry__field')
&& evt.target.parentNode.parentNode.classList.contains('form-entry')) {
evt.target.parentNode.parentNode.classList.toggle('form-entry--focused', isFocusIn);
}
}
if (isFocusIn && this.config) {
const modalInner = this.$el.querySelector('.modal__inner-2');
let target = evt.target;
let { target } = evt;
while (target) {
if (target === modalInner) {
return;
@ -151,20 +157,24 @@ export default {
},
},
mounted() {
window.addEventListener('focusin', this.onFocusInOut);
window.addEventListener('focusout', this.onFocusInOut);
const tabbables = getTabbables(this.$el);
tabbables[0].focus();
},
destroyed() {
window.removeEventListener('focusin', this.onFocusInOut);
window.removeEventListener('focusout', this.onFocusInOut);
this.$watch(
() => this.config,
(isOpen) => {
if (isOpen) {
const tabbables = getTabbables(this.$el);
if (tabbables[0]) {
tabbables[0].focus();
}
}
},
{ immediate: true },
);
},
};
</script>
<style lang="scss">
@import 'common/variables.scss';
@import '../styles/variables.scss';
.modal {
position: absolute;
@ -173,8 +183,8 @@ export default {
background-color: rgba(160, 160, 160, 0.5);
overflow: auto;
hr {
margin: 0.5em 0;
p {
line-height: 1.5;
}
}
@ -188,7 +198,7 @@ export default {
.modal__inner-2 {
margin: 40px 10px 100px;
background-color: #f8f8f8;
padding: 40px 50px 30px;
padding: 50px 50px 40px;
border-radius: $border-radius-base;
position: relative;
overflow: hidden;
@ -221,9 +231,9 @@ export default {
.modal__image {
float: left;
width: 64px;
height: 64px;
margin: 1.5em 1.5em 0.5em 0;
width: 60px;
height: 60px;
margin: 1.5em 1.2em 0.5em 0;
& + *::after {
content: '';
@ -240,7 +250,7 @@ export default {
}
.modal__sub-title {
opacity: 0.5;
opacity: 0.6;
font-size: 0.75rem;
margin-bottom: 1.5rem;
}
@ -262,9 +272,16 @@ export default {
}
}
.modal__info--multiline {
padding-top: 0.1em;
padding-bottom: 0.1em;
}
.modal__button-bar {
margin-top: 1.75rem;
text-align: right;
margin-top: 2rem;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.form-entry {

View File

@ -3,7 +3,7 @@
<!-- Explorer -->
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
<button class="navigation-bar__button button" v-if="light" @click="close()" v-title="'Close StackEdit'"><icon-close-circle></icon-close-circle></button>
<button class="navigation-bar__button button" v-else tour-step-anchor="explorer" @click="toggleExplorer()" v-title="'Toggle explorer'"><icon-folder></icon-folder></button>
<button class="navigation-bar__button navigation-bar__button--explorer-toggler button" v-else tour-step-anchor="explorer" @click="toggleExplorer()" v-title="'Toggle explorer'"><icon-folder></icon-folder></button>
</div>
<!-- Side bar -->
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button">
@ -19,7 +19,7 @@
<!-- Title -->
<div class="navigation-bar__title navigation-bar__title--fake text-input"></div>
<div class="navigation-bar__title navigation-bar__title--text text-input" :style="{width: titleWidth + 'px'}">{{title}}</div>
<input class="navigation-bar__title navigation-bar__title--input text-input" :class="{'navigation-bar__title--focus': titleFocus, 'navigation-bar__title--scrolling': titleScrolling}" :style="{width: titleWidth + 'px'}" @focus="editTitle(true)" @blur="editTitle(false)" @keydown.enter="submitTitle()" @keydown.esc="submitTitle(true)" @mouseenter="titleHover = true" @mouseleave="titleHover = false" v-model="title">
<input class="navigation-bar__title navigation-bar__title--input text-input" :class="{'navigation-bar__title--focus': titleFocus, 'navigation-bar__title--scrolling': titleScrolling}" :style="{width: titleWidth + 'px'}" @focus="editTitle(true)" @blur="editTitle(false)" @keydown.enter="submitTitle(false)" @keydown.esc="submitTitle(true)" @mouseenter="titleHover = true" @mouseleave="titleHover = false" v-model="title">
<!-- Sync/Publish -->
<div class="flex flex--row" :class="{'navigation-bar__hidden': styles.hideLocations}">
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in syncLocations" :key="location.id" :href="location.url" target="_blank" v-title="'Synchronized location'"><icon-provider :provider-id="location.providerId"></icon-provider></a>
@ -56,6 +56,7 @@ import tempFileSvc from '../services/tempFileSvc';
import utils from '../services/utils';
import pagedownButtons from '../data/pagedownButtons';
import store from '../store';
import workspaceSvc from '../services/workspaceSvc';
// According to mousetrap
const mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl';
@ -63,17 +64,16 @@ const mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl';
const getShortcut = (method) => {
let result = '';
Object.entries(store.getters['data/computedSettings'].shortcuts).some(([keys, shortcut]) => {
if (`${shortcut.method || shortcut}` !== method) {
return false;
if (`${shortcut.method || shortcut}` === method) {
result = keys.split('+').map(key => key.toLowerCase()).map((key) => {
if (key === 'mod') {
return mod;
}
// Capitalize
return key && `${key[0].toUpperCase()}${key.slice(1)}`;
}).join('+');
}
result = keys.split('+').map(key => key.toLowerCase()).map((key) => {
if (key === 'mod') {
return mod;
}
// Capitalize
return key && `${key[0].toUpperCase()}${key.slice(1)}`;
}).join('+');
return true;
return result;
});
return result && ` ${result}`;
};
@ -151,6 +151,13 @@ export default {
}
return result;
},
editCancelTrigger() {
const current = this.$store.getters['file/current'];
return utils.serializeObject([
current.id,
current.name,
]);
},
},
methods: {
...mapMutations('content', [
@ -184,16 +191,22 @@ export default {
editorSvc.pagedownEditor.uiManager.doClick(name);
}
},
editTitle(toggle) {
async editTitle(toggle) {
this.titleFocus = toggle;
if (toggle) {
this.titleInputElt.setSelectionRange(0, this.titleInputElt.value.length);
} else {
const title = this.title.trim();
this.title = this.$store.getters['file/current'].name;
if (title) {
this.$store.dispatch('file/patchCurrent', { name: utils.sanitizeName(title) });
} else {
this.title = this.$store.getters['file/current'].name;
try {
await workspaceSvc.storeItem({
...this.$store.getters['file/current'],
name: title,
});
} catch (e) {
// Cancel
}
}
}
},
@ -209,10 +222,13 @@ export default {
},
created() {
this.$watch(
() => this.$store.getters['file/current'].name,
(name) => {
this.title = name;
}, { immediate: true });
() => this.editCancelTrigger,
() => {
this.title = '';
this.editTitle(false);
},
{ immediate: true },
);
},
mounted() {
this.titleFakeElt = this.$el.querySelector('.navigation-bar__title--fake');
@ -223,7 +239,7 @@ export default {
</script>
<style lang="scss">
@import 'common/variables.scss';
@import '../styles/variables.scss';
.navigation-bar {
position: absolute;

View File

@ -23,7 +23,7 @@ export default {
</script>
<style lang="scss">
@import 'common/variables.scss';
@import '../styles/variables.scss';
.notification {
position: absolute;

View File

@ -22,7 +22,7 @@ import { mapGetters, mapActions } from 'vuex';
import CommentList from './gutters/CommentList';
import PreviewNewDiscussionButton from './gutters/PreviewNewDiscussionButton';
const appUri = `${location.protocol}//${location.host}`;
const appUri = `${window.location.protocol}//${window.location.host}`;
export default {
components: {
@ -98,13 +98,14 @@ export default {
previewElt.querySelectorAll(`.discussion-preview-highlighting--${discussionId}`)
.cl_each(elt => elt.classList.add('discussion-preview-highlighting--selected'));
}
});
},
);
},
};
</script>
<style lang="scss">
@import 'common/variables.scss';
@import '../styles/variables.scss';
.preview,
.preview__inner-1 {

View File

@ -1,22 +0,0 @@
<template>
<span class="provider-name">{{name}}</span>
</template>
<script>
import userSvc from '../services/userSvc';
export default {
props: ['providerId'],
computed: {
name() {
switch (this.userId) {
default:
return 'Google Drive';
}
},
},
created() {
userSvc.getInfo(this.userId);
},
};
</script>

View File

@ -13,7 +13,7 @@
</div>
<div class="side-bar__inner">
<main-menu v-if="panel === 'menu'"></main-menu>
<workspaces-menu v-if="panel === 'workspaces'"></workspaces-menu>
<workspaces-menu v-else-if="panel === 'workspaces'"></workspaces-menu>
<sync-menu v-else-if="panel === 'sync'"></sync-menu>
<publish-menu v-else-if="panel === 'publish'"></publish-menu>
<history-menu v-else-if="panel === 'history'"></history-menu>
@ -75,7 +75,11 @@ export default {
}),
computed: {
panel() {
return !this.$store.state.light && this.$store.getters['data/layoutSettings'].sideBarPanel;
if (this.$store.state.light) {
return null; // No menu in light mode
}
const result = this.$store.getters['data/layoutSettings'].sideBarPanel;
return panelNames[result] ? result : 'menu';
},
panelName() {
return panelNames[this.panel];
@ -93,7 +97,7 @@ export default {
</script>
<style lang="scss">
@import 'common/variables.scss';
@import '../styles/variables.scss';
.side-bar {
overflow: hidden;
@ -112,6 +116,11 @@ export default {
hr + hr {
display: none;
}
.textfield {
font-size: 14px;
height: 26px;
}
}
.side-bar__inner {
@ -164,7 +173,7 @@ export default {
padding: 10px;
margin: -10px -10px 10px;
background-color: $info-bg;
font-size: 0.95em;
font-size: 0.9em;
p {
margin: 10px;

View File

@ -92,7 +92,7 @@ export default {
this.htmlSelection = true;
if (!text) {
this.htmlSelection = false;
text = editorSvc.previewCtx.text;
({ text } = editorSvc.previewCtx);
}
if (text != null) {
this.htmlStats.forEach((stat) => {

View File

@ -64,7 +64,7 @@ export default {
const updateMaskY = () => {
const scrollPosition = editorSvc.getScrollPosition();
if (scrollPosition) {
const sectionDesc = editorSvc.previewCtx.sectionDescList[scrollPosition.sectionIdx];
const sectionDesc = editorSvc.previewCtxMeasured.sectionDescList[scrollPosition.sectionIdx];
this.maskY = sectionDesc.tocDimension.startOffset +
(scrollPosition.posInSection * sectionDesc.tocDimension.height);
}

View File

@ -2,21 +2,21 @@
<div class="tour" @keydown.esc="skip">
<div class="tour-step" :class="'tour-step--' + step" :style="stepStyle">
<div class="tour-step__inner" v-if="step === 'welcome'">
<h2>Welcome to StackEdit!</h2>
<p>Greater, lighter, faster... <b>StackEdit 5</b> is here!</p>
<h2>Welcome back!</h2>
<p>The new <b>StackEdit 5</b> is here!</p>
<p>Please click <b>Next</b> to take a quick tour.</p>
<div class="tour-step__button-bar">
<button class="button" @click="finish">Skip</button>
<button class="button" @click="next">Next</button>
<button class="button button--resolve" @click="next">Next</button>
</div>
</div>
<div class="tour-step__inner" v-else-if="step === 'editor'">
<h2>Your Markdown editor</h2>
<p>StackEdit renders your Markdown into HTML in real-time.</p>
<p>StackEdit converts your Markdown to HTML in real-time.</p>
<p>Click <icon-side-preview></icon-side-preview> to toggle the side preview.</p>
<div class="tour-step__button-bar">
<button class="button" @click="finish">Skip</button>
<button class="button" @click="next">Next</button>
<button class="button button--resolve" @click="next">Next</button>
</div>
</div>
<div class="tour-step__inner" v-else-if="step === 'explorer'">
@ -25,7 +25,7 @@
<p>Click <icon-folder></icon-folder> to open the file explorer.</p>
<div class="tour-step__button-bar">
<button class="button" @click="finish">Skip</button>
<button class="button" @click="next">Next</button>
<button class="button button--resolve" @click="next">Next</button>
</div>
</div>
<div class="tour-step__inner" v-else-if="step === 'menu'">
@ -34,7 +34,7 @@
<p>Click <icon-provider provider-id="stackedit"></icon-provider> to explore the menu.</p>
<div class="tour-step__button-bar">
<button class="button" @click="finish">Skip</button>
<button class="button" @click="next">Next</button>
<button class="button button--resolve" @click="next">Next</button>
</div>
</div>
<div class="tour-step__inner" v-else-if="step === 'end'">
@ -42,7 +42,7 @@
<p>If you like StackEdit, please rate 5 stars on the <a target="_blank" href="https://chrome.google.com/webstore/detail/iiooodelglhkcpgbajoejffhijaclcdg/reviews">Chrome Web Store</a>.</p>
<p>You can also star the project on <a target="_blank" href="https://github.com/benweet/stackedit">GitHub</a> and join the <a target="_blank" href="https://community.stackedit.io/">community</a>.</p>
<div class="tour-step__button-bar">
<button class="button" @click="finish">Ok</button>
<button class="button button--resolve" @click="finish">Ok</button>
</div>
</div>
</div>
@ -126,7 +126,7 @@ export default {
<style lang="scss">
@import 'common/variables.scss';
@import '../styles/variables.scss';
.tour {
position: absolute;
@ -139,12 +139,12 @@ export default {
}
$tour-step-background: mix(#f3f3f3, $selection-highlighting-color, 75%);
$tour-step-width: 220px;
$tour-step-width: 240px;
.tour-step__inner {
position: absolute;
background-color: $tour-step-background;
padding: 1.5em 1em 1em;
padding: 1.5em;
font-size: 0.9em;
line-height: 1.33;
width: $tour-step-width;
@ -213,6 +213,13 @@ $tour-step-width: 220px;
}
.tour-step__button-bar {
text-align: right;
margin-top: 1.5em;
display: flex;
flex-direction: row;
justify-content: flex-end;
.button {
font-size: 1.1em;
}
}
</style>

View File

@ -10,13 +10,11 @@ export default {
props: ['userId'],
computed: {
url() {
const userInfo = this.$store.state.userInfo.itemMap[this.userId];
userSvc.getInfo(this.userId);
const userInfo = this.$store.state.userInfo.itemsById[this.userId];
return userInfo && userInfo.imageUrl && `url('${userInfo.imageUrl}')`;
},
},
created() {
userSvc.getInfo(this.userId);
},
};
</script>

View File

@ -9,12 +9,10 @@ export default {
props: ['userId'],
computed: {
name() {
const userInfo = this.$store.state.userInfo.itemMap[this.userId];
userSvc.getInfo(this.userId);
const userInfo = this.$store.state.userInfo.itemsById[this.userId];
return userInfo ? userInfo.name : 'Someone';
},
},
created() {
userSvc.getInfo(this.userId);
},
};
</script>

View File

@ -1,4 +1,4 @@
import cledit from '../../services/cledit';
import cledit from '../../services/editor/cledit';
import editorSvc from '../../services/editorSvc';
import utils from '../../services/utils';
@ -10,7 +10,9 @@ const nextTickExecCbs = cledit.Utils.debounce(() => {
}
if (savedSelection) {
editorSvc.clEditor.selectionMgr.setSelectionStartEnd(
savedSelection.start, savedSelection.end);
savedSelection.start,
savedSelection.end,
);
}
savedSelection = null;
});

View File

@ -1,4 +1,4 @@
import cledit from '../../services/cledit';
import cledit from '../../services/editor/cledit';
import editorSvc from '../../services/editorSvc';
import utils from '../../services/utils';
@ -40,14 +40,22 @@ export default class PreviewClassApplier {
const offset = this.offsetGetter();
if (offset) {
const offsetStart = editorSvc.getPreviewOffset(
offset.start, editorSvc.previewCtx.sectionDescList);
offset.start,
editorSvc.previewCtx.sectionDescList,
);
const offsetEnd = editorSvc.getPreviewOffset(
offset.end, editorSvc.previewCtx.sectionDescList);
offset.end,
editorSvc.previewCtx.sectionDescList,
);
if (offsetStart != null && offsetEnd != null && offsetStart !== offsetEnd) {
const start = cledit.Utils.findContainer(
editorSvc.previewElt, Math.min(offsetStart, offsetEnd));
editorSvc.previewElt,
Math.min(offsetStart, offsetEnd),
);
const end = cledit.Utils.findContainer(
editorSvc.previewElt, Math.max(offsetStart, offsetEnd));
editorSvc.previewElt,
Math.max(offsetStart, offsetEnd),
);
const range = document.createRange();
range.setStart(start.container, start.offsetInContainer);
range.setEnd(end.container, end.offsetInContainer);

View File

@ -0,0 +1,80 @@
import Vue from 'vue';
import Clipboard from 'clipboard';
import timeSvc from '../../services/timeSvc';
import store from '../../store';
// Global directives
Vue.directive('focus', {
inserted(el) {
el.focus();
const { value } = el;
if (value && el.setSelectionRange) {
el.setSelectionRange(0, value.length);
}
},
});
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);
}
},
});
const setElTitle = (el, title) => {
el.title = title;
el.setAttribute('aria-label', title);
};
Vue.directive('title', {
bind(el, { value }) {
setElTitle(el, value);
},
update(el, { value, oldValue }) {
if (value !== oldValue) {
setElTitle(el, value);
}
},
});
// Clipboard directive
const createClipboard = (el, value) => {
el.seClipboard = new Clipboard(el, { text: () => value });
};
const destroyClipboard = (el) => {
if (el.seClipboard) {
el.seClipboard.destroy();
el.seClipboard = null;
}
};
Vue.directive('clipboard', {
bind(el, { value }) {
createClipboard(el, value);
},
update(el, { value, oldValue }) {
if (value !== oldValue) {
destroyClipboard(el);
createClipboard(el, value);
}
},
unbind(el) {
destroyClipboard(el);
},
});
// Global filters
Vue.filter('formatTime', time =>
// Access the minute counter for reactive refresh
timeSvc.format(time, store.state.minuteCounter));

View File

@ -47,11 +47,13 @@ export default {
...mapMutations('discussion', [
'setIsCommenting',
]),
removeComment() {
this.$store.dispatch('modal/commentDeletion')
.then(
() => this.$store.dispatch('discussion/cleanCurrentFile', { filterComment: this.comment }),
() => {}); // Cancel
async removeComment() {
try {
await this.$store.dispatch('modal/open', 'commentDeletion');
this.$store.dispatch('discussion/cleanCurrentFile', { filterComment: this.comment });
} catch (e) {
// Cancel
}
},
},
mounted() {
@ -63,8 +65,7 @@ export default {
let scrollerMirrorElt;
const getScrollerMirrorElt = () => {
if (!scrollerMirrorElt) {
scrollerMirrorElt = document.querySelector(
`.comment-list .comment--${commentId} .comment__text-inner`);
scrollerMirrorElt = document.querySelector(`.comment-list .comment--${commentId} .comment__text-inner`);
}
return scrollerMirrorElt || { scrollTop: 0 };
};

View File

@ -107,10 +107,13 @@ export default {
this.currentDiscussionLastCommentId
&& this.$el.querySelector(`.comment--${this.currentDiscussionLastCommentId}`),
this.$el.querySelector('.comment--new'),
true);
true,
);
} else {
tops[discussionId] = getTop(discussion,
this.$el.querySelector(`.comment--discussion-${discussionId}`));
tops[discussionId] = getTop(
discussion,
this.$el.querySelector(`.comment--discussion-${discussionId}`),
);
}
});
this.tops = tops;
@ -120,7 +123,8 @@ export default {
this.$watch(
() => this.updateTopsTrigger,
() => this.updateTops(),
{ immediate: true });
{ immediate: true },
);
const layoutSettings = this.$store.getters['data/layoutSettings'];
this.scrollerElt = layoutSettings.showEditor
@ -161,7 +165,8 @@ export default {
this.$watch(
() => this.updateStickyTrigger,
() => this.updateSticky(),
{ immediate: true });
{ immediate: true },
);
// Move preview discussions once previewCtxWithDiffs has been calculated
if (!editorSvc.previewCtxWithDiffs) {
@ -178,7 +183,7 @@ export default {
</script>
<style lang="scss">
@import '../common/variables.scss';
@import '../../styles/variables.scss';
.comment-list {
position: absolute;

View File

@ -28,7 +28,7 @@
</template>
<script>
import { mapState, mapGetters, mapMutations } from 'vuex';
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
import editorSvc from '../../services/editorSvc';
import animationSvc from '../../services/animationSvc';
import markdownConversionSvc from '../../services/markdownConversionSvc';
@ -67,6 +67,9 @@ export default {
...mapMutations('discussion', [
'setCurrentDiscussionId',
]),
...mapActions('notification', [
'info',
]),
goToDiscussion(discussionId = this.currentDiscussionId) {
this.setCurrentDiscussionId(discussionId);
const layoutSettings = this.$store.getters['data/layoutSettings'];
@ -75,7 +78,7 @@ export default {
? editorSvc.clEditor.selectionMgr.getCoordinates(discussion.end)
: editorSvc.getPreviewOffsetCoordinates(editorSvc.getPreviewOffset(discussion.end));
if (!coordinates) {
this.$store.dispatch('notification/info', "Discussion can't be located in the file.");
this.info("Discussion can't be located in the file.");
} else {
const scrollerElt = layoutSettings.showEditor
? editorSvc.editorElt.parentNode
@ -93,20 +96,22 @@ export default {
.start();
}
},
removeDiscussion() {
this.$store.dispatch('modal/discussionDeletion')
.then(
() => this.$store.dispatch('discussion/cleanCurrentFile', {
filterDiscussion: this.currentDiscussion,
}),
() => {}); // Cancel
async removeDiscussion() {
try {
await this.$store.dispatch('modal/open', 'discussionDeletion');
this.$store.dispatch('discussion/cleanCurrentFile', {
filterDiscussion: this.currentDiscussion,
});
} catch (e) {
// Cancel
}
},
},
};
</script>
<style lang="scss">
@import '../common/variables.scss';
@import '../../styles/variables.scss';
.current-discussion {
position: absolute;

View File

@ -3,7 +3,7 @@
<div class="comment__header flex flex--row flex--space-between flex--align-center">
<div class="comment__user flex flex--row flex--align-center">
<div class="comment__user-image">
<user-image :user-id="loginToken.sub"></user-image>
<user-image :user-id="userId"></user-image>
</div>
<span class="user-name">{{loginToken.name}}</span>
</div>
@ -24,7 +24,7 @@
import { mapGetters, mapMutations, mapActions } from 'vuex';
import Prism from 'prismjs';
import UserImage from '../UserImage';
import cledit from '../../services/cledit';
import cledit from '../../services/editor/cledit';
import editorSvc from '../../services/editorSvc';
import markdownConversionSvc from '../../services/markdownConversionSvc';
import utils from '../../services/utils';
@ -33,9 +33,12 @@ export default {
components: {
UserImage,
},
computed: mapGetters('workspace', [
'loginToken',
]),
computed: {
...mapGetters('workspace', [
'loginToken',
'userId',
]),
},
methods: {
...mapMutations('discussion', [
'setNewCommentFocus',
@ -53,7 +56,7 @@ export default {
const discussionId = this.$store.state.discussion.currentDiscussionId;
const comment = {
discussionId,
sub: this.loginToken.sub,
sub: this.userId,
text,
created: Date.now(),
};
@ -83,9 +86,11 @@ export default {
const clEditor = cledit(preElt, scrollerElt, true);
clEditor.init({
sectionHighlighter: section => Prism.highlight(
section.text, editorSvc.prismGrammars[section.data]),
sectionParser: text => markdownConversionSvc.parseSections(
editorSvc.converter, text).sections,
section.text,
editorSvc.prismGrammars[section.data],
),
sectionParser: text => markdownConversionSvc
.parseSections(editorSvc.converter, text).sections,
content: this.$store.state.discussion.newCommentText,
selectionStart: this.$store.state.discussion.newCommentSelection.start,
selectionEnd: this.$store.state.discussion.newCommentSelection.end,
@ -111,14 +116,14 @@ export default {
clEditor.focus();
}
}),
{ immediate: true });
{ immediate: true },
);
if (isSticky) {
let scrollerMirrorElt;
const getScrollerMirrorElt = () => {
if (!scrollerMirrorElt) {
scrollerMirrorElt = document.querySelector(
'.comment-list .comment--new .comment__text-inner');
scrollerMirrorElt = document.querySelector('.comment-list .comment--new .comment__text-inner');
}
return scrollerMirrorElt || { scrollTop: 0 };
};
@ -147,7 +152,7 @@ export default {
);
this.$watch(
() => this.$store.state.discussion.newCommentText,
newCommentText => clEditor.setContent(newCommentText),
newCommentText => clEditor.setContent(newCommentText),
);
}
},

View File

@ -28,7 +28,7 @@ export default {
) {
this.selection = editorSvc.getTrimmedSelection();
if (this.selection) {
const text = editorSvc.previewCtxWithDiffs.text;
const { text } = editorSvc.previewCtxWithDiffs;
offset = editorSvc.getPreviewOffset(this.selection.end);
while (offset && text[offset - 1] === '\n') {
offset -= 1;
@ -46,7 +46,8 @@ export default {
editorSvc.$on('previewSelectionRange', () => this.checkSelection());
this.$watch(
() => this.$store.getters['layout/styles'].previewWidth,
() => this.checkSelection());
() => this.checkSelection(),
);
this.checkSelection();
});
},

View File

@ -33,7 +33,7 @@ export default {
</script>
<style lang="scss">
@import '../common/variables.scss';
@import '../../styles/variables.scss';
.sticky-comment {
position: absolute;

View File

@ -12,18 +12,19 @@
</menu-entry>
<menu-entry @click.native="exportPdf">
<icon-download slot="icon"></icon-download>
<div><div class="menu-entry__label">sponsor</div> Export as PDF</div>
<div><div class="menu-entry__label" :class="{'menu-entry__label--warning': !isSponsor}">sponsor</div> Export as PDF</div>
<span>Produce a PDF from an HTML template.</span>
</menu-entry>
<menu-entry @click.native="exportPandoc">
<icon-download slot="icon"></icon-download>
<div><div class="menu-entry__label">sponsor</div> Export with Pandoc</div>
<div><div class="menu-entry__label" :class="{'menu-entry__label--warning': !isSponsor}">sponsor</div> Export with Pandoc</div>
<span>Convert to PDF, Word, EPUB...</span>
</menu-entry>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import MenuEntry from './common/MenuEntry';
import exportSvc from '../../services/exportSvc';
@ -31,23 +32,24 @@ export default {
components: {
MenuEntry,
},
computed: mapGetters(['isSponsor']),
methods: {
exportMarkdown() {
const currentFile = this.$store.getters['file/current'];
return exportSvc.exportToDisk(currentFile.id, 'md')
.catch(() => {}); // Cancel
.catch(() => { /* Cancel */ });
},
exportHtml() {
return this.$store.dispatch('modal/open', 'htmlExport')
.catch(() => {}); // Cancel
.catch(() => { /* Cancel */ });
},
exportPdf() {
return this.$store.dispatch('modal/open', 'pdfExport')
.catch(() => {}); // Cancel
.catch(() => { /* Cancel */ });
},
exportPandoc() {
return this.$store.dispatch('modal/open', 'pandocExport')
.catch(() => {}); // Cancel
.catch(() => { /* Cancel */ });
},
},
};

View File

@ -1,26 +1,41 @@
<template>
<div class="history side-bar__panel">
<div class="side-bar__info" v-if="!syncToken">
<p>You have to <a href="javascript:void(0)" @click="signin">sign in with Google</a> to enable revision history.</p>
<p><b>Note:</b> This will sync your main workspace.</p>
</div>
<div class="side-bar__info" v-if="loading">
<p>Loading history</p>
</div>
<div class="side-bar__info" v-else-if="!revisionsWithSpacer.length">
<p><b>{{currentFileName}}</b> has no history.</p>
</div>
<div class="revision" v-for="revision in revisionsWithSpacer" :key="revision.id">
<div class="history__spacer" v-if="revision.spacer"></div>
<a class="revision__button button flex flex--row" href="javascript:void(0)" @click="open(revision)">
<div class="revision__icon">
<user-image :user-id="revision.sub"></user-image>
<div class="history side-bar__panel side-bar__panel--menu">
<div class="side-bar__info">
<p v-if="syncLocations.length > 1">
<select slot="field" class="textfield" v-model="syncLocationId" @keydown.enter="resolve()">
<option v-for="location in syncLocations" :key="location.id" :value="location.id">
{{ location.description }}
</option>
</select>
</p>
<p v-if="!historyContext">Synchronize <b>{{currentFileName}}</b> to enable revision history or <a href="javascript:void(0)" @click="signin">sign in with Google</a> to synchronize your main workspace.</p>
<p v-else-if="loading">Loading history</p>
<p v-else-if="!revisionsWithSpacer.length"><b>{{currentFileName}}</b> has no history.</p>
<div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-else>
<div class="menu-entry__icon menu-entry__icon--image">
<icon-provider :provider-id="syncLocation.providerId"></icon-provider>
</div>
<div class="revision__header flex flex--column">
<user-name :user-id="revision.sub"></user-name>
<div class="revision__created">{{revision.created | formatTime}}</div>
</div>
</a>
<span v-if="syncLocation.url">
The following revisions are stored in <a :href="syncLocation.url" target="_blank">{{ syncLocationProviderName }}</a>.
</span>
<span v-else>
The following revisions are stored in {{ syncLocationProviderName }}.
</span>
</div>
</div>
<div>
<div class="revision" v-for="revision in revisionsWithSpacer" :key="revision.id">
<div class="history__spacer" v-if="revision.spacer"></div>
<a class="revision__button button flex flex--row" href="javascript:void(0)" @click="open(revision)">
<div class="revision__icon">
<user-image :user-id="revision.sub"></user-image>
</div>
<div class="revision__header flex flex--column">
<user-name :user-id="revision.sub"></user-name>
<div class="revision__created">{{revision.created | formatTime}}</div>
</div>
</a>
</div>
</div>
<div class="history__spacer history__spacer--last" v-if="revisions.length"></div>
<div class="flex flex--row flex--end" v-if="showMoreButton">
@ -30,8 +45,8 @@
</template>
<script>
import { mapMutations, mapGetters } from 'vuex';
import providerRegistry from '../../services/providers/providerRegistry';
import { mapState, mapMutations, mapGetters } from 'vuex';
import providerRegistry from '../../services/providers/common/providerRegistry';
import MenuEntry from './common/MenuEntry';
import UserImage from '../UserImage';
import UserName from '../UserName';
@ -44,11 +59,11 @@ import syncSvc from '../../services/syncSvc';
let editorClassAppliers = [];
let previewClassAppliers = [];
let cachedFileId;
let cachedHistoryContextHash;
let revisionsPromise;
let revisionContentPromises;
const pageSize = 30;
const spacerThreshold = 12 * 60 * 60 * 1000; // 12h
const spacerThreshold = 60 * 60 * 1000; // 1h
export default {
components: {
@ -60,16 +75,73 @@ export default {
allRevisions: [],
loading: false,
showCount: pageSize,
syncLocationId: null,
}),
computed: {
...mapGetters('workspace', [
'syncToken',
...mapGetters('data', [
'syncDataByItemId',
]),
...mapGetters('syncLocation', {
syncLocations: 'currentWithWorkspaceSyncLocation',
}),
...mapState('content', [
'revisionContent',
]),
syncLocation() {
return utils.someResult(this.syncLocations, (syncLocation) => {
if (syncLocation.id === this.syncLocationId) {
return syncLocation;
}
return null;
});
},
syncLocationProviderName() {
if (!this.syncLocation) {
return null;
}
return providerRegistry.providersById[this.syncLocation.providerId].name;
},
currentFileName() {
return this.$store.getters['file/current'].name;
},
historyContext() {
const { syncLocation } = this;
if (syncLocation) {
const provider = providerRegistry.providersById[syncLocation.providerId];
const token = provider.getToken(syncLocation);
const fileId = this.$store.getters['file/current'].id;
const contentId = `${fileId}/content`;
const historyContext = {
token,
fileId,
contentId,
syncLocation: this.syncLocation,
};
if (syncLocation.id !== 'main') {
return historyContext;
}
// Add syncData for workspace sync location
const { syncDataByItemId } = this;
const fileSyncData = syncDataByItemId[fileId];
const contentSyncData = syncDataByItemId[contentId];
if (fileSyncData && contentSyncData) {
return {
...historyContext,
fileSyncData,
contentSyncData,
};
}
}
return null;
},
historyContextHash() {
return utils.serializeObject(this.historyContext);
},
revisions() {
return this.allRevisions.slice(0, this.showCount);
return this.allRevisions.slice()
.sort((revision1, revision2) => revision2.created - revision1.created)
.slice(0, this.showCount);
},
revisionsWithSpacer() {
let previousCreated = 0;
@ -85,23 +157,18 @@ export default {
showMoreButton() {
return this.showCount < this.allRevisions.length;
},
refreshTrigger() {
return utils.serializeObject([
this.$store.getters['file/current'].id,
this.syncToken,
]);
},
},
methods: {
...mapMutations('content', [
'setRevisionContent',
]),
signin() {
return googleHelper.signin()
.then(
() => syncSvc.requestSync(),
() => {}, // Cancel
);
async signin() {
try {
await googleHelper.signin();
syncSvc.requestSync();
} catch (e) {
// Cancel
}
},
close() {
this.$store.dispatch('data/setSideBarPanel', 'menu');
@ -112,25 +179,31 @@ export default {
open(revision) {
let revisionContentPromise = revisionContentPromises[revision.id];
if (!revisionContentPromise) {
revisionContentPromise = new Promise((resolve, reject) => {
const syncToken = this.syncToken;
const currentFile = this.$store.getters['file/current'];
this.$store.dispatch('queue/enqueue',
() => Promise.resolve()
.then(() => this.workspaceProvider.getRevisionContent(
syncToken, currentFile.id, revision.id))
.then(resolve, reject));
});
revisionContentPromises[revision.id] = revisionContentPromise;
revisionContentPromise.catch(() => {
revisionContentPromises[revision.id] = null;
});
const historyContext = utils.deepCopy(this.historyContext);
if (historyContext) {
const provider = providerRegistry.providersById[this.syncLocation.providerId];
revisionContentPromise = new Promise((resolve, reject) => this.$store.dispatch(
'queue/enqueue',
() => provider.getFileRevisionContent({
...historyContext,
revisionId: revision.id,
})
.then(resolve, reject),
));
revisionContentPromises[revision.id] = revisionContentPromise;
revisionContentPromise.catch((err) => {
this.$store.dispatch('notification/error', err);
revisionContentPromises[revision.id] = null;
});
}
}
if (revisionContentPromise) {
revisionContentPromise.then(revisionContent =>
this.$store.dispatch('content/setRevisionContent', revisionContent));
}
revisionContentPromise.then(revisionContent =>
this.$store.dispatch('content/setRevisionContent', revisionContent));
},
refreshHighlighters() {
const revisionContent = this.$store.state.content.revisionContent;
const { revisionContent } = this;
editorClassAppliers.forEach(editorClassApplier => editorClassApplier.stop());
editorClassAppliers = [];
previewClassAppliers.forEach(previewClassApplier => previewClassApplier.stop());
@ -145,41 +218,53 @@ export default {
end: offset + text.length,
};
editorClassAppliers.push(new EditorClassApplier(
[`revision-diff--${utils.uid()}`, ...classes], offsets));
[`revision-diff--${utils.uid()}`, ...classes],
offsets,
));
previewClassAppliers.push(new PreviewClassApplier(
[`revision-diff--${utils.uid()}`, ...classes], offsets));
[`revision-diff--${utils.uid()}`, ...classes],
offsets,
));
}
offset += text.length;
});
}
},
},
created() {
// Find the workspace provider
const workspace = this.$store.getters['workspace/currentWorkspace'];
this.workspaceProvider = providerRegistry.providers[workspace.providerId];
// Watch file changes
this.$watch(
() => this.refreshTrigger,
() => {
watch: {
// Fix syncLocationId
syncLocation: {
immediate: true,
handler(value) {
if (!value) {
const firstSyncLocation = this.syncLocations[0];
if (firstSyncLocation) {
this.syncLocationId = firstSyncLocation.id;
}
}
},
},
// Load revision list on context changes
historyContextHash: {
immediate: true,
handler() {
this.allRevisions = [];
const id = this.$store.getters['file/current'].id;
const syncToken = this.syncToken;
if (id && syncToken) {
if (id !== cachedFileId) {
const historyContext = utils.deepCopy(this.historyContext);
if (historyContext) {
if (this.historyContextHash !== cachedHistoryContextHash) {
this.setRevisionContent();
cachedFileId = id;
cachedHistoryContextHash = this.historyContextHash;
revisionContentPromises = {};
const currentFile = this.$store.getters['file/current'];
revisionsPromise = new Promise((resolve, reject) => {
this.$store.dispatch('queue/enqueue',
() => Promise.resolve()
.then(() => this.workspaceProvider.listRevisions(syncToken, currentFile.id))
.then(resolve, reject));
})
.catch(() => {
cachedFileId = null;
const provider = providerRegistry.providersById[this.syncLocation.providerId];
revisionsPromise = new Promise((resolve, reject) => this.$store.dispatch(
'queue/enqueue',
() => provider
.listFileRevisions(historyContext)
.then(resolve, reject),
))
.catch((err) => {
this.$store.dispatch('notification/error', err);
cachedHistoryContextHash = null;
return [];
});
}
@ -191,39 +276,40 @@ export default {
});
}
}
}, { immediate: true });
const loadOne = () => {
if (!this.destroyed) {
this.$store.dispatch('queue/enqueue',
() => {
let loadPromise;
this.revisions.some((revision) => {
if (!revision.created) {
const syncToken = this.syncToken;
const currentFile = this.$store.getters['file/current'];
loadPromise = this.workspaceProvider
.loadRevision(syncToken, currentFile.id, revision)
.then(() => loadOne());
}
return loadPromise;
});
return loadPromise;
});
},
},
// Load each revision on revision list changes
revisions(revisions) {
const { historyContext } = this;
if (historyContext) {
this.$store.dispatch(
'queue/enqueue',
() => utils.awaitSequence(revisions, async (revision) => {
// Make sure revisions and historyContext haven't changed
if (!this.destroyed
&& this.revisions === revisions
&& this.historyContext === historyContext
) {
const provider = providerRegistry.providersById[this.syncLocation.providerId];
await provider.loadFileRevision({
...historyContext,
revision,
});
}
}),
);
}
};
this.$watch(
() => this.revisions,
() => loadOne(),
{ immediate: true });
// Watch diffs changes
this.$watch(
() => this.$store.state.content.revisionContent,
() => this.refreshHighlighters());
// Close revision
},
// Refresh highlighters on open/close revision
revisionContent: {
immediate: true,
handler() {
this.refreshHighlighters();
},
},
},
created() {
// Close revision on escape
this.onKeyup = (evt) => {
if (evt.which === 27) {
// Esc key
@ -246,11 +332,7 @@ export default {
</script>
<style lang="scss">
@import '../common/variables.scss';
.history {
padding: 5px 5px 50px;
}
@import '../../styles/variables.scss';
.history__button {
font-size: 14px;
@ -266,7 +348,7 @@ export default {
position: absolute;
height: 100%;
top: 0;
left: 24px;
left: 19px;
border-left: 2px dotted $hr-color;
}
}
@ -277,7 +359,7 @@ export default {
.revision__button {
text-align: left;
padding: 15px;
padding: 10px;
height: auto;
text-transform: none;
position: relative;
@ -287,7 +369,7 @@ export default {
position: absolute;
height: 100%;
top: 0;
left: 24px;
left: 19px;
border-left: 2px solid $hr-color;
}
@ -318,20 +400,21 @@ export default {
.revision__header {
font-size: 15px;
width: 100%;
line-height: 1.33;
}
.revision__created {
font-size: 0.75em;
opacity: 0.5;
opacity: 0.6;
}
.layout--revision {
.cledit-section *,
.cl-preview-section * {
color: transparentize($editor-color-light, 0.67) !important;
color: transparentize($editor-color-light, 0.5) !important;
.app--dark & {
color: transparentize($editor-color-dark, 0.67) !important;
color: transparentize($editor-color-dark, 0.5) !important;
}
}

View File

@ -7,7 +7,7 @@
</div>
<div class="flex flex--column">
<div>Import Markdown</div>
<span>Open a plain text file.</span>
<span>Import a plain text file.</span>
</div>
</label>
<input class="hidden-file" id="import-html-file-input" type="file" @change="onImportHtml">
@ -27,8 +27,9 @@
import TurndownService from 'turndown/lib/turndown.browser.umd';
import htmlSanitizer from '../../libs/htmlSanitizer';
import MenuEntry from './common/MenuEntry';
import providerUtils from '../../services/providers/providerUtils';
import Provider from '../../services/providers/common/Provider';
import store from '../../store';
import workspaceSvc from '../../services/workspaceSvc';
const turndownService = new TurndownService(store.getters['data/computedSettings'].turndown);
@ -52,27 +53,25 @@ export default {
MenuEntry,
},
methods: {
onImportMarkdown(evt) {
async onImportMarkdown(evt) {
const file = evt.target.files[0];
readFile(file)
.then(content => this.$store.dispatch('createFile', {
...providerUtils.parseContent(content),
name: file.name,
})
.then(item => this.$store.commit('file/setCurrentId', item.id)));
const content = await readFile(file);
const item = await workspaceSvc.createFile({
...Provider.parseContent(content),
name: file.name,
});
this.$store.commit('file/setCurrentId', item.id);
},
onImportHtml(evt) {
async onImportHtml(evt) {
const file = evt.target.files[0];
readFile(file)
.then(content => this.$store.dispatch('createFile', {
...providerUtils.parseContent(
turndownService.turndown(
htmlSanitizer.sanitizeHtml(content)
.replace(/&#160;/g, ' '), // Replace non-breaking spaces with classic spaces
)),
name: file.name,
}))
.then(item => this.$store.commit('file/setCurrentId', item.id));
const content = await readFile(file);
const sanitizedContent = htmlSanitizer.sanitizeHtml(content)
.replace(/&#160;/g, ' '); // Replace non-breaking spaces with classic spaces
const item = await workspaceSvc.createFile({
...Provider.parseContent(turndownService.turndown(sanitizedContent)),
name: file.name,
});
this.$store.commit('file/setCurrentId', item.id);
},
},
};

View File

@ -1,9 +1,9 @@
<template>
<div class="side-bar__panel side-bar__panel--menu">
<div class="menu-info-entries">
<div class="side-bar__info">
<div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-if="loginToken">
<div class="menu-entry__icon menu-entry__icon--image">
<user-image :user-id="loginToken.sub"></user-image>
<user-image :user-id="userId"></user-image>
</div>
<span>Signed in as <b>{{loginToken.name}}</b>.</span>
</div>
@ -11,7 +11,18 @@
<div class="menu-entry__icon menu-entry__icon--image">
<icon-provider :provider-id="currentWorkspace.providerId"></icon-provider>
</div>
<span><b>{{currentWorkspace.name}}</b> synced.</span>
<span v-if="currentWorkspace.providerId === 'googleDriveAppData'">
<b>{{currentWorkspace.name}}</b> synced with your Google Drive app data folder.
</span>
<span v-else-if="currentWorkspace.providerId === 'googleDriveWorkspace'">
<b>{{currentWorkspace.name}}</b> synced with a <a :href="workspaceLocationUrl" target="_blank">Google Drive folder</a>.
</span>
<span v-else-if="currentWorkspace.providerId === 'couchdbWorkspace'">
<b>{{currentWorkspace.name}}</b> synced with a <a :href="workspaceLocationUrl" target="_blank">CouchDB database</a>.
</span>
<span v-else-if="currentWorkspace.providerId === 'githubWorkspace'">
<b>{{currentWorkspace.name}}</b> synced with a <a :href="workspaceLocationUrl" target="_blank">GitHub repo</a>.
</span>
</div>
<div class="menu-entry menu-entry--info flex flex--row flex--align-center" v-else>
<div class="menu-entry__icon menu-entry__icon--disabled">
@ -27,7 +38,7 @@
</menu-entry>
<menu-entry @click.native="setPanel('workspaces')">
<icon-database slot="icon"></icon-database>
<div>Workspaces</div>
<div><div class="menu-entry__label menu-entry__label--warning">new</div> Workspaces</div>
<span>Switch to another workspace.</span>
</menu-entry>
<hr>
@ -83,6 +94,7 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import MenuEntry from './common/MenuEntry';
import providerRegistry from '../../services/providers/common/providerRegistry';
import UserImage from '../UserImage';
import googleHelper from '../../services/providers/helpers/googleHelper';
import syncSvc from '../../services/syncSvc';
@ -97,25 +109,34 @@ export default {
'currentWorkspace',
'syncToken',
'loginToken',
'userId',
]),
workspaceLocationUrl() {
const provider = providerRegistry.providersById[this.currentWorkspace.providerId];
return provider.getWorkspaceLocationUrl(this.currentWorkspace);
},
},
methods: {
...mapActions('data', {
setPanel: 'setSideBarPanel',
}),
signin() {
return googleHelper.signin()
.then(
() => syncSvc.requestSync(),
() => {}, // Cancel
);
async signin() {
try {
await googleHelper.signin();
syncSvc.requestSync();
} catch (e) {
// Cancel
}
},
fileProperties() {
return this.$store.dispatch('modal/open', 'fileProperties')
.catch(() => {}); // Cancel
async fileProperties() {
try {
await this.$store.dispatch('modal/open', 'fileProperties');
} catch (e) {
// Cancel
}
},
print() {
print();
window.print();
},
},
};

View File

@ -7,7 +7,7 @@
</menu-entry>
<menu-entry @click.native="templates">
<icon-code-braces slot="icon"></icon-code-braces>
<div>Templates</div>
<div><div class="menu-entry__label menu-entry__label--count">{{templateCount}}</div> Templates</div>
<span>Configure Handlebars templates for your exports.</span>
</menu-entry>
<menu-entry @click.native="reset">
@ -50,6 +50,11 @@ export default {
components: {
MenuEntry,
},
computed: {
templateCount() {
return Object.keys(this.$store.getters['data/allTemplatesById']).length;
},
},
methods: {
onImportBackup(evt) {
const file = evt.target.files[0];
@ -72,35 +77,36 @@ export default {
...utils.queryParams,
exportWorkspace: true,
}, true);
const iframeElt = utils.createHiddenIframe(url);
document.body.appendChild(iframeElt);
setTimeout(() => {
document.body.removeChild(iframeElt);
}, 60000);
window.location.href = url;
window.location.reload(true);
},
settings() {
return this.$store.dispatch('modal/open', 'settings')
.then(
settings => this.$store.dispatch('data/setSettings', settings),
() => {}, // Cancel
);
async settings() {
try {
const settings = await this.$store.dispatch('modal/open', 'settings');
this.$store.dispatch('data/setSettings', settings);
} catch (e) {
// Cancel
}
},
templates() {
return this.$store.dispatch('modal/open', 'templates')
.then(
({ templates }) => this.$store.dispatch('data/setTemplates', templates),
() => {}, // Cancel
);
async templates() {
try {
const { templates } = await this.$store.dispatch('modal/open', 'templates');
this.$store.dispatch('data/setTemplatesById', templates);
} catch (e) {
// Cancel
}
},
reset() {
return this.$store.dispatch('modal/reset')
.then(() => {
location.href = '#reset=true';
location.reload();
});
async reset() {
try {
await this.$store.dispatch('modal/open', 'reset');
window.location.href = '#reset=true';
window.location.reload();
} catch (e) {
// Cancel
}
},
about() {
return this.$store.dispatch('modal/open', 'about');
this.$store.dispatch('modal/open', 'about');
},
},
};

View File

@ -16,7 +16,7 @@
</menu-entry>
<menu-entry @click.native="managePublish">
<icon-view-list slot="icon"></icon-view-list>
<div>File publication</div>
<div><div class="menu-entry__label menu-entry__label--count">{{locationCount}}</div> File publication</div>
<span>Manage current file publication locations.</span>
</menu-entry>
</div>
@ -113,15 +113,19 @@ import zendeskHelper from '../../services/providers/helpers/zendeskHelper';
import publishSvc from '../../services/publishSvc';
import store from '../../store';
const tokensToArray = (tokens, filter = () => true) => Object.keys(tokens)
.map(sub => tokens[sub])
const tokensToArray = (tokens, filter = () => true) => Object.values(tokens)
.filter(token => filter(token))
.sort((token1, token2) => token1.name.localeCompare(token2.name));
const openPublishModal = (token, type) => store.dispatch('modal/open', {
type,
token,
}).then(publishLocation => publishSvc.createPublishLocation(publishLocation));
const publishModalOpener = type => async (token) => {
try {
const publishLocation = await store.dispatch('modal/open', {
type,
token,
});
publishSvc.createPublishLocation(publishLocation);
} catch (e) { /* cancel */ }
};
export default {
components: {
@ -137,26 +141,29 @@ export default {
...mapGetters('publishLocation', {
publishLocations: 'current',
}),
locationCount() {
return Object.keys(this.publishLocations).length;
},
currentFileName() {
return this.$store.getters['file/current'].name;
},
googleDriveTokens() {
return tokensToArray(this.$store.getters['data/googleTokens'], token => token.isDrive);
return tokensToArray(this.$store.getters['data/googleTokensBySub'], token => token.isDrive);
},
dropboxTokens() {
return tokensToArray(this.$store.getters['data/dropboxTokens']);
return tokensToArray(this.$store.getters['data/dropboxTokensBySub']);
},
githubTokens() {
return tokensToArray(this.$store.getters['data/githubTokens']);
return tokensToArray(this.$store.getters['data/githubTokensBySub']);
},
wordpressTokens() {
return tokensToArray(this.$store.getters['data/wordpressTokens']);
return tokensToArray(this.$store.getters['data/wordpressTokensBySub']);
},
bloggerTokens() {
return tokensToArray(this.$store.getters['data/googleTokens'], token => token.isBlogger);
return tokensToArray(this.$store.getters['data/googleTokensBySub'], token => token.isBlogger);
},
zendeskTokens() {
return tokensToArray(this.$store.getters['data/zendeskTokens']);
return tokensToArray(this.$store.getters['data/zendeskTokensBySub']);
},
noToken() {
return !this.googleDriveTokens.length
@ -173,77 +180,53 @@ export default {
publishSvc.requestPublish();
}
},
managePublish() {
return this.$store.dispatch('modal/open', 'publishManagement');
async managePublish() {
try {
await this.$store.dispatch('modal/open', 'publishManagement');
} catch (e) { /* cancel */ }
},
addGoogleDriveAccount() {
return this.$store.dispatch('modal/open', {
type: 'googleDriveAccount',
onResolve: () => googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess),
})
.catch(() => {}); // Cancel
async addGoogleDriveAccount() {
try {
await this.$store.dispatch('modal/open', { type: 'googleDriveAccount' });
await googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess);
} catch (e) { /* cancel */ }
},
addDropboxAccount() {
return this.$store.dispatch('modal/open', {
type: 'dropboxAccount',
onResolve: () => dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess),
})
.catch(() => {}); // Cancel
async addDropboxAccount() {
try {
await this.$store.dispatch('modal/open', { type: 'dropboxAccount' });
await dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess);
} catch (e) { /* cancel */ }
},
addGithubAccount() {
return this.$store.dispatch('modal/open', {
type: 'githubAccount',
onResolve: () => githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess),
})
.catch(() => {}); // Cancel
async addGithubAccount() {
try {
await this.$store.dispatch('modal/open', { type: 'githubAccount' });
await githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess);
} catch (e) { /* cancel */ }
},
addWordpressAccount() {
return wordpressHelper.addAccount()
.catch(() => {}); // Cancel
async addWordpressAccount() {
try {
await wordpressHelper.addAccount();
} catch (e) { /* cancel */ }
},
addBloggerAccount() {
return googleHelper.addBloggerAccount()
.catch(() => {}); // Cancel
async addBloggerAccount() {
try {
await googleHelper.addBloggerAccount();
} catch (e) { /* cancel */ }
},
addZendeskAccount() {
return this.$store.dispatch('modal/open', {
type: 'zendeskAccount',
onResolve: ({ subdomain, clientId }) => zendeskHelper.addAccount(subdomain, clientId),
})
.catch(() => {}); // Cancel
},
publishGoogleDrive(token) {
return openPublishModal(token, 'googleDrivePublish')
.catch(() => {}); // Cancel
},
publishDropbox(token) {
return openPublishModal(token, 'dropboxPublish')
.catch(() => {}); // Cancel
},
publishGithub(token) {
return openPublishModal(token, 'githubPublish')
.catch(() => {}); // Cancel
},
publishGist(token) {
return openPublishModal(token, 'gistPublish')
.catch(() => {}); // Cancel
},
publishWordpress(token) {
return openPublishModal(token, 'wordpressPublish')
.catch(() => {}); // Cancel
},
publishBlogger(token) {
return openPublishModal(token, 'bloggerPublish')
.catch(() => {}); // Cancel
},
publishBloggerPage(token) {
return openPublishModal(token, 'bloggerPagePublish')
.catch(() => {}); // Cancel
},
publishZendesk(token) {
return openPublishModal(token, 'zendeskPublish')
.catch(() => {}); // Cancel
async addZendeskAccount() {
try {
const { subdomain, clientId } = await this.$store.dispatch('modal/open', { type: 'zendeskAccount' });
await zendeskHelper.addAccount(subdomain, clientId);
} catch (e) { /* cancel */ }
},
publishGoogleDrive: publishModalOpener('googleDrivePublish'),
publishDropbox: publishModalOpener('dropboxPublish'),
publishGithub: publishModalOpener('githubPublish'),
publishGist: publishModalOpener('gistPublish'),
publishWordpress: publishModalOpener('wordpressPublish'),
publishBlogger: publishModalOpener('bloggerPublish'),
publishBloggerPage: publishModalOpener('bloggerPagePublish'),
publishZendesk: publishModalOpener('zendeskPublish'),
},
};
</script>

View File

@ -1,7 +1,7 @@
<template>
<div class="side-bar__panel side-bar__panel--menu">
<div class="side-bar__info" v-if="isCurrentTemp">
<p><b>{{currentFileName}}</b> can not be synchronized as it's a temporary file.</p>
<p><b>{{currentFileName}}</b> can not be synced as it's a temporary file.</p>
</div>
<div v-else>
<div class="side-bar__info" v-if="noToken">
@ -16,7 +16,7 @@
</menu-entry>
<menu-entry @click.native="manageSync">
<icon-view-list slot="icon"></icon-view-list>
<div>File synchronization</div>
<div><div class="menu-entry__label menu-entry__label--count">{{locationCount}}</div> File synchronization</div>
<span>Manage current file synchronized locations.</span>
</menu-entry>
</div>
@ -91,8 +91,7 @@ import githubProvider from '../../services/providers/githubProvider';
import syncSvc from '../../services/syncSvc';
import store from '../../store';
const tokensToArray = (tokens, filter = () => true) => Object.keys(tokens)
.map(sub => tokens[sub])
const tokensToArray = (tokens, filter = () => true) => Object.values(tokens)
.filter(token => filter(token))
.sort((token1, token2) => token1.name.localeCompare(token2.name));
@ -116,19 +115,22 @@ export default {
'isCurrentTemp',
]),
...mapGetters('syncLocation', {
syncLocations: 'current',
syncLocations: 'currentWithWorkspaceSyncLocation',
}),
locationCount() {
return Object.keys(this.syncLocations).length;
},
currentFileName() {
return this.$store.getters['file/current'].name;
},
googleDriveTokens() {
return tokensToArray(this.$store.getters['data/googleTokens'], token => token.isDrive);
return tokensToArray(this.$store.getters['data/googleTokensBySub'], token => token.isDrive);
},
dropboxTokens() {
return tokensToArray(this.$store.getters['data/dropboxTokens']);
return tokensToArray(this.$store.getters['data/dropboxTokensBySub']);
},
githubTokens() {
return tokensToArray(this.$store.getters['data/githubTokens']);
return tokensToArray(this.$store.getters['data/githubTokensBySub']);
},
noToken() {
return !this.googleDriveTokens.length
@ -142,63 +144,74 @@ export default {
syncSvc.requestSync();
}
},
manageSync() {
return this.$store.dispatch('modal/open', 'syncManagement');
async manageSync() {
try {
await this.$store.dispatch('modal/open', 'syncManagement');
} catch (e) { /* cancel */ }
},
addGoogleDriveAccount() {
return this.$store.dispatch('modal/open', {
type: 'googleDriveAccount',
onResolve: () => googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess),
})
.catch(() => {}); // Cancel
async addGoogleDriveAccount() {
try {
await this.$store.dispatch('modal/open', { type: 'googleDriveAccount' });
await googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess);
} catch (e) { /* cancel */ }
},
addDropboxAccount() {
return this.$store.dispatch('modal/open', {
type: 'dropboxAccount',
onResolve: () => dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess),
})
.catch(() => {}); // Cancel
async addDropboxAccount() {
try {
await this.$store.dispatch('modal/open', { type: 'dropboxAccount' });
await dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess);
} catch (e) { /* cancel */ }
},
addGithubAccount() {
return this.$store.dispatch('modal/open', {
type: 'githubAccount',
onResolve: () => githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess),
})
.catch(() => {}); // Cancel
async addGithubAccount() {
try {
await this.$store.dispatch('modal/open', { type: 'githubAccount' });
await githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess);
} catch (e) { /* cancel */ }
},
openGoogleDrive(token) {
return googleHelper.openPicker(token, 'doc')
.then(files => this.$store.dispatch('queue/enqueue',
() => googleDriveProvider.openFiles(token, files)));
async openGoogleDrive(token) {
const files = await googleHelper.openPicker(token, 'doc');
this.$store.dispatch(
'queue/enqueue',
() => googleDriveProvider.openFiles(token, files),
);
},
openDropbox(token) {
return dropboxHelper.openChooser(token)
.then(paths => this.$store.dispatch('queue/enqueue',
() => dropboxProvider.openFiles(token, paths)));
async openDropbox(token) {
const paths = await dropboxHelper.openChooser(token);
this.$store.dispatch(
'queue/enqueue',
() => dropboxProvider.openFiles(token, paths),
);
},
saveGoogleDrive(token) {
return openSyncModal(token, 'googleDriveSave')
.catch(() => {}); // Cancel
async saveGoogleDrive(token) {
try {
await openSyncModal(token, 'googleDriveSave');
} catch (e) { /* cancel */ }
},
saveDropbox(token) {
return openSyncModal(token, 'dropboxSave')
.catch(() => {}); // Cancel
async saveDropbox(token) {
try {
await openSyncModal(token, 'dropboxSave');
} catch (e) { /* cancel */ }
},
openGithub(token) {
return store.dispatch('modal/open', {
type: 'githubOpen',
token,
})
.then(syncLocation => this.$store.dispatch('queue/enqueue',
() => githubProvider.openFile(token, syncLocation)));
async openGithub(token) {
try {
const syncLocation = await store.dispatch('modal/open', {
type: 'githubOpen',
token,
});
this.$store.dispatch(
'queue/enqueue',
() => githubProvider.openFile(token, syncLocation),
);
} catch (e) { /* cancel */ }
},
saveGithub(token) {
return openSyncModal(token, 'githubSave')
.catch(() => {}); // Cancel
async saveGithub(token) {
try {
await openSyncModal(token, 'githubSave');
} catch (e) { /* cancel */ }
},
saveGist(token) {
return openSyncModal(token, 'gistSync')
.catch(() => {}); // Cancel
async saveGist(token) {
try {
await openSyncModal(token, 'gistSync');
} catch (e) { /* cancel */ }
},
},
};

View File

@ -1,23 +1,30 @@
<template>
<div class="side-bar__panel side-bar__panel--menu">
<div class="workspace" v-for="(workspace, id) in sanitizedWorkspaces" :key="id">
<div class="workspace" v-for="(workspace, id) in workspacesById" :key="id">
<menu-entry :href="workspace.url" target="_blank">
<icon-provider slot="icon" :provider-id="workspace.providerId"></icon-provider>
<div class="workspace__name"><div class="menu-entry__label" v-if="currentWorkspace === workspace">current</div>{{workspace.name}}</div>
</menu-entry>
</div>
<hr>
<menu-entry @click.native="addGoogleDriveWorkspace">
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
<span>Add Google Drive workspace</span>
</menu-entry>
<menu-entry @click.native="addCouchdbWorkspace">
<icon-provider slot="icon" provider-id="couchdbWorkspace"></icon-provider>
<span>Add CouchDB workspace</span>
<div>CouchDB workspace</div>
<span>Add a workspace synced with a CouchDB database.</span>
</menu-entry>
<menu-entry @click.native="addGithubWorkspace">
<icon-provider slot="icon" provider-id="githubWorkspace"></icon-provider>
<div>GitHub workspace</div>
<span>Add a workspace synced with a GitHub repository.</span>
</menu-entry>
<menu-entry @click.native="addGoogleDriveWorkspace">
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
<div>Google Drive workspace</div>
<span>Add a workspace synced with a Google Drive folder.</span>
</menu-entry>
<menu-entry @click.native="manageWorkspaces">
<icon-database slot="icon"></icon-database>
<span>Manage workspaces</span>
<span><div class="menu-entry__label menu-entry__label--count">{{workspaceCount}}</div> Manage workspaces</span>
</menu-entry>
</div>
</template>
@ -32,37 +39,53 @@ export default {
MenuEntry,
},
computed: {
...mapGetters('data', [
'sanitizedWorkspaces',
]),
...mapGetters('workspace', [
'workspacesById',
'currentWorkspace',
]),
workspaceCount() {
return Object.keys(this.workspacesById).length;
},
},
methods: {
addGoogleDriveWorkspace() {
return googleHelper.addDriveAccount(true)
.then(token => this.$store.dispatch('modal/open', {
async addCouchdbWorkspace() {
try {
this.$store.dispatch('modal/open', {
type: 'couchdbWorkspace',
});
} catch (e) {
// Cancel
}
},
async addGithubWorkspace() {
try {
this.$store.dispatch('modal/open', {
type: 'githubWorkspace',
});
} catch (e) {
// Cancel
}
},
async addGoogleDriveWorkspace() {
try {
const token = await googleHelper.addDriveAccount(true);
this.$store.dispatch('modal/open', {
type: 'googleDriveWorkspace',
token,
}))
.catch(() => {}); // Cancel
},
addCouchdbWorkspace() {
return this.$store.dispatch('modal/open', {
type: 'couchdbWorkspace',
})
.catch(() => {}); // Cancel
});
} catch (e) {
// Cancel
}
},
manageWorkspaces() {
return this.$store.dispatch('modal/open', 'workspaceManagement');
this.$store.dispatch('modal/open', 'workspaceManagement');
},
},
};
</script>
<style lang="scss">
@import '../common/variables.scss';
@import '../../styles/variables.scss';
.workspace .menu-entry {
padding-top: 12px;

View File

@ -10,7 +10,7 @@
</template>
<style lang="scss">
@import '../../common/variables.scss';
@import '../../../styles/variables.scss';
.menu-entry {
text-align: left;
@ -24,9 +24,13 @@
span {
display: inline-block;
font-size: 0.75rem;
opacity: 0.5;
opacity: 0.67;
line-height: 1.3;
.menu-entry__label {
opacity: 1;
}
span {
display: inline;
opacity: 1;
@ -34,12 +38,6 @@
}
}
.menu-info-entries {
padding: 10px;
margin: -10px -10px 10px;
background-color: rgba(255, 255, 255, 0.2);
}
.menu-entry--info {
padding-top: 3px;
padding-bottom: 3px;
@ -70,10 +68,22 @@
float: right;
font-size: 0.6rem;
font-weight: 600;
padding: 0.05em 0.25em;
background-color: darken($error-color, 10);
line-height: 1;
padding: 0.15em 0.25em;
background-color: #fff;
border-radius: 3px;
opacity: 0.6;
}
.menu-entry__label--warning {
color: #fff;
background-color: darken($error-color, 10);
opacity: 1;
}
.menu-entry__label--count {
font-size: 0.75rem;
font-weight: 400;
}
.menu-entry__text {

View File

@ -2,17 +2,17 @@
<modal-inner class="modal__inner-1--about-modal" aria-label="About">
<div class="modal__content">
<div class="logo-background"></div>
<small>v{{version}} © 2018 Dock5 Software</small>
<small>© 2013-2018 Dock5 Software Ltd.<br>v{{version}}</small>
<hr>
StackEdit on <a target="_blank" href="https://github.com/benweet/stackedit/">GitHub</a>
<br>
<a target="_blank" href="https://github.com/benweet/stackedit/issues">Issue tracker</a> <a target="_blank" href="https://github.com/benweet/stackedit/blob/master/CHANGELOG.md">Changelog</a>
<a target="_blank" href="https://github.com/benweet/stackedit/issues">Issue tracker</a> <a target="_blank" href="https://github.com/benweet/stackedit/releases">Changelog</a>
<br>
<a target="_blank" href="https://chrome.google.com/webstore/detail/iiooodelglhkcpgbajoejffhijaclcdg">Chrome app</a> <a target="_blank" href="https://chrome.google.com/webstore/detail/ajehldoplanpchfokmeempkekhnhmoha">Chrome extension</a>
<br>
StackEdit on <a target="_blank" href="https://twitter.com/stackedit/">Twitter</a>
<a target="_blank" href="https://community.stackedit.io/">Community</a> <a target="_blank" href="https://community.stackedit.io/c/how-to">Tutos and How To</a>
<br>
<a target="_blank" href="https://community.stackedit.io/">Community</a>
StackEdit on <a target="_blank" href="https://twitter.com/stackedit/">Twitter</a>
<div class="modal__info">
For commercial support or custom development, please <a href="mailto:stackedit.project@gmail.com">send us an email</a>.
</div>
@ -24,7 +24,7 @@
<a target="_blank" href="privacy_policy.html">Privacy Policy</a>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.resolve()">Close</button>
<button class="button button--resolve" @click="config.resolve()">Close</button>
</div>
</modal-inner>
</template>
@ -59,7 +59,7 @@ export default {
.logo-background {
height: 75px;
margin: 0.5rem 0;
margin: 0.5em 0;
}
small {

View File

@ -41,6 +41,9 @@
</form-entry>
<form-entry label="Status">
<input slot="field" class="textfield" type="text" v-model.trim="status" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> draft
</div>
</form-entry>
<form-entry label="Date" info="YYYY-MM-DD">
<input slot="field" class="textfield" type="text" v-model.trim="date" @keydown.enter="resolve()">
@ -54,7 +57,7 @@
</div>
</div>
<div class="modal__error modal__error--file-properties">{{error}}</div>
<div class="modal__info">
<div class="modal__info modal__info--multiline">
<p><strong>ProTip:</strong> You can manually toggle extensions:</p>
<pre class=" language-yaml"><code class="prism language-yaml"><span class="token key atrule">extensions</span><span class="token punctuation">:</span>
<span class="token key atrule">emoji</span><span class="token punctuation">:</span>
@ -75,7 +78,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -223,10 +226,10 @@ export default {
</script>
<style lang="scss">
@import '../common/variables.scss';
@import '../../styles/variables.scss';
.modal__inner-1--file-properties {
max-width: 540px;
.modal__inner-1.modal__inner-1--file-properties {
max-width: 520px;
}
.modal__error--file-properties {

View File

@ -4,7 +4,7 @@
<p>Please choose a template for your <b>HTML export</b>.</p>
<form-entry label="Template">
<select class="textfield" slot="field" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
<option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }}
</option>
</select>
@ -14,15 +14,15 @@
</form-entry>
</div>
<div class="modal__button-bar">
<button class="button button--copy">Copy to clipboard</button>
<button class="button button--copy" v-clipboard="result" @click="info('HTML copied to clipboard!')">Copy</button>
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
<script>
import Clipboard from 'clipboard';
import { mapActions } from 'vuex';
import exportSvc from '../../services/exportSvc';
import modalTemplate from './common/modalTemplate';
@ -37,29 +37,27 @@ export default modalTemplate({
let timeoutId;
this.$watch('selectedTemplate', (selectedTemplate) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = setTimeout(async () => {
const currentFile = this.$store.getters['file/current'];
exportSvc.applyTemplate(currentFile.id, this.allTemplates[selectedTemplate])
.then((html) => {
this.result = html;
});
const html = await exportSvc.applyTemplate(
currentFile.id,
this.allTemplatesById[selectedTemplate],
);
this.result = html;
}, 10);
}, {
immediate: true,
});
this.clipboard = new Clipboard(this.$el.querySelector('.button--copy'), {
text: () => this.result,
});
},
destroyed() {
this.clipboard.destroy();
},
methods: {
...mapActions('notification', [
'info',
]),
resolve() {
const config = this.config;
const { config } = this;
const currentFile = this.$store.getters['file/current'];
config.resolve();
exportSvc.exportToDisk(currentFile.id, 'html', this.allTemplates[this.selectedTemplate]);
exportSvc.exportToDisk(currentFile.id, 'html', this.allTemplatesById[this.selectedTemplate]);
},
},
});

View File

@ -17,7 +17,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -36,9 +36,8 @@ export default modalTemplate({
}),
computed: {
googlePhotosTokens() {
const googleTokens = this.$store.getters['data/googleTokens'];
return Object.entries(googleTokens)
.map(([, token]) => token)
const googleTokensBySub = this.$store.getters['data/googleTokensBySub'];
return Object.values(googleTokensBySub)
.filter(token => token.isPhotos)
.sort((token1, token2) => token1.name.localeCompare(token2.name));
},
@ -48,28 +47,30 @@ export default modalTemplate({
if (!this.url) {
this.setError('url');
} else {
const callback = this.config.callback;
const { callback } = this.config;
this.config.resolve();
callback(this.url);
}
},
reject() {
const callback = this.config.callback;
const { callback } = this.config;
this.config.reject();
callback(null);
},
addGooglePhotosAccount() {
return googleHelper.addPhotosAccount();
},
openGooglePhotos(token) {
const callback = this.config.callback;
async openGooglePhotos(token) {
const { callback } = this.config;
this.config.reject();
googleHelper.openPicker(token, 'img')
.then(res => res[0] && this.$store.dispatch('modal/open', {
const res = await googleHelper.openPicker(token, 'img');
if (res[0]) {
this.$store.dispatch('modal/open', {
type: 'googlePhoto',
url: res[0].url,
callback,
}));
});
}
},
},
});

View File

@ -8,7 +8,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -25,13 +25,13 @@ export default modalTemplate({
if (!this.url) {
this.setError('url');
} else {
const callback = this.config.callback;
const { callback } = this.config;
this.config.resolve();
callback(this.url);
}
},
reject() {
const callback = this.config.callback;
const { callback } = this.config;
this.config.reject();
callback(null);
},

View File

@ -20,7 +20,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -38,19 +38,20 @@ export default modalTemplate({
selectedFormat: 'pandocExportFormat',
},
methods: {
resolve() {
async resolve() {
this.config.resolve();
const currentFile = this.$store.getters['file/current'];
const currentContent = this.$store.getters['content/current'];
const selectedFormat = this.selectedFormat;
this.$store.dispatch('queue/enqueue', () => Promise.all([
const { selectedFormat } = this;
const [sponsorToken, token] = await this.$store.dispatch('queue/enqueue', () => Promise.all([
Promise.resolve().then(() => {
const sponsorToken = this.$store.getters['workspace/sponsorToken'];
return sponsorToken && googleHelper.refreshToken(sponsorToken);
const tokenToRefresh = this.$store.getters['workspace/sponsorToken'];
return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh);
}),
sponsorSvc.getToken(),
])
.then(([sponsorToken, token]) => networkSvc.request({
]));
try {
const { body } = await networkSvc.request({
method: 'POST',
url: 'pandocExport',
params: {
@ -63,19 +64,16 @@ export default modalTemplate({
body: JSON.stringify(editorSvc.getPandocAst()),
blob: true,
timeout: 60000,
})
.then((res) => {
FileSaver.saveAs(res.body, `${currentFile.name}.${selectedFormat}`);
}, (err) => {
if (err.status !== 401) {
throw err;
}
this.$store.dispatch('modal/sponsorOnly');
}))
.catch((err) => {
});
FileSaver.saveAs(body, `${currentFile.name}.${selectedFormat}`);
} catch (err) {
if (err.status === 401) {
this.$store.dispatch('modal/open', 'sponsorOnly');
} else {
console.error(err); // eslint-disable-line no-console
this.$store.dispatch('notification/error', err);
}));
}
}
},
},
});

View File

@ -4,7 +4,7 @@
<p>Please choose a template for your <b>PDF export</b>.</p>
<form-entry label="Template">
<select class="textfield" slot="field" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
<option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }}
</option>
</select>
@ -15,7 +15,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -33,19 +33,24 @@ export default modalTemplate({
selectedTemplate: 'pdfExportTemplate',
},
methods: {
resolve() {
async resolve() {
this.config.resolve();
const currentFile = this.$store.getters['file/current'];
this.$store.dispatch('queue/enqueue', () => Promise.all([
Promise.resolve().then(() => {
const sponsorToken = this.$store.getters['workspace/sponsorToken'];
return sponsorToken && googleHelper.refreshToken(sponsorToken);
}),
sponsorSvc.getToken(),
exportSvc.applyTemplate(
currentFile.id, this.allTemplates[this.selectedTemplate], true),
])
.then(([sponsorToken, token, html]) => networkSvc.request({
const [sponsorToken, token, html] = await this.$store
.dispatch('queue/enqueue', () => Promise.all([
Promise.resolve().then(() => {
const tokenToRefresh = this.$store.getters['workspace/sponsorToken'];
return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh);
}),
sponsorSvc.getToken(),
exportSvc.applyTemplate(
currentFile.id,
this.allTemplatesById[this.selectedTemplate],
true,
),
]));
try {
const { body } = await networkSvc.request({
method: 'POST',
url: 'pdfExport',
params: {
@ -56,19 +61,16 @@ export default modalTemplate({
body: html,
blob: true,
timeout: 60000,
})
.then((res) => {
FileSaver.saveAs(res.body, `${currentFile.name}.pdf`);
}, (err) => {
if (err.status !== 401) {
throw err;
}
this.$store.dispatch('modal/sponsorOnly');
}))
.catch((err) => {
});
FileSaver.saveAs(body, `${currentFile.name}.pdf`);
} catch (err) {
if (err.status === 401) {
this.$store.dispatch('modal/open', 'sponsorOnly');
} else {
console.error(err); // eslint-disable-line no-console
this.$store.dispatch('notification/error', err);
}));
}
}
},
},
});

View File

@ -1,38 +1,53 @@
<template>
<modal-inner class="modal__inner-1--publish-management" aria-label="Manage publication locations">
<div class="modal__content">
<div class="modal__image">
<icon-upload></icon-upload>
</div>
<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>
<div>
<div class="publish-entry flex flex--row flex--align-center" v-for="location in publishLocations" :key="location.id">
<div class="publish-entry__icon flex flex--column flex--center">
<icon-provider :provider-id="location.providerId"></icon-provider>
<div class="publish-entry flex flex--column" v-for="location in publishLocations" :key="location.id">
<div class="publish-entry__header flex flex--row flex--align-center">
<div class="publish-entry__icon flex flex--column flex--center">
<icon-provider :provider-id="location.providerId"></icon-provider>
</div>
<div class="publish-entry__description">
{{location.description}}
</div>
<div class="publish-entry__buttons flex flex--row flex--center">
<button class="publish-entry__button button" @click="remove(location)" v-title="'Remove location'">
<icon-delete></icon-delete>
</button>
</div>
</div>
<div class="publish-entry__description">
{{location.description}}
</div>
<div class="publish-entry__buttons flex flex--row flex--center">
<a class="publish-entry__button button" :href="location.url" target="_blank">
<icon-open-in-new></icon-open-in-new>
</a>
<button class="publish-entry__button button" @click="remove(location)">
<icon-delete></icon-delete>
</button>
<div class="publish-entry__row flex flex--row flex--align-center">
<div class="publish-entry__url">
{{location.url}}
</div>
<div class="publish-entry__buttons flex flex--row flex--center" v-if="location.url">
<button class="publish-entry__button button" v-clipboard="location.url" @click="info('Location URL copied to clipboard!')" v-title="'Copy URL'">
<icon-content-copy></icon-content-copy>
</button>
<a class="publish-entry__button button" v-if="location.url" :href="location.url" target="_blank" v-title="'Open location'">
<icon-open-in-new></icon-open-in-new>
</a>
</div>
</div>
</div>
</div>
<div class="modal__info" v-if="publishLocations.length">
<b>Note:</b> Removing a synchronized location won't delete any file.
<b>Tip:</b> Removing a location won't delete any file.
</div>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.resolve()">Close</button>
<button class="button button--resolve" @click="config.resolve()">Close</button>
</div>
</modal-inner>
</template>
<script>
import { mapGetters } from 'vuex';
import { mapGetters, mapActions } from 'vuex';
import ModalInner from './common/ModalInner';
export default {
@ -51,6 +66,9 @@ export default {
},
},
methods: {
...mapActions('notification', [
'info',
]),
remove(location) {
this.$store.commit('publishLocation/deleteItem', location.id);
},
@ -59,47 +77,73 @@ export default {
</script>
<style lang="scss">
@import '../common/variables.scss';
.modal__inner-1--publish-management {
max-width: 560px;
}
@import '../../styles/variables.scss';
.publish-entry {
padding: 0.5rem 0.25rem;
border-bottom: 1px solid $hr-color;
margin: 1.5em 0;
height: auto;
font-size: 17px;
line-height: 1.5;
}
&:last-child {
border-bottom: none;
}
$button-size: 30px;
$small-button-size: 22px;
.publish-entry__header {
line-height: $button-size;
}
.publish-entry__row {
margin-top: 1px;
padding-top: 1px;
border-top: 1px solid rgba(128, 128, 128, 0.15);
line-height: $small-button-size;
}
.publish-entry__icon {
height: 30px;
width: 30px;
height: 22px;
width: 22px;
margin-right: 0.75rem;
flex: none;
}
.publish-entry__description {
opacity: 0.5;
line-height: 1.4;
font-size: 0.9em;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.publish-entry__url {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
opacity: 0.5;
font-size: 0.67em;
}
.publish-entry__buttons {
margin-left: 0.75rem;
.publish-entry__row & {
margin-left: 0.5rem;
}
}
.publish-entry__button {
width: 38px;
height: 38px;
padding: 6px;
width: $button-size;
height: $button-size;
padding: 4px;
background-color: transparent;
opacity: 0.75;
.publish-entry__row & {
width: $small-button-size;
height: $small-button-size;
padding: 4px;
}
&:active,
&:focus,
&:hover {

View File

@ -25,7 +25,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="!error && config.resolve(strippedCustomSettings)">Ok</button>
<button class="button button--resolve" @click="!error && config.resolve(strippedCustomSettings)">Ok</button>
</div>
</modal-inner>
</template>
@ -36,7 +36,7 @@ import { mapGetters } from 'vuex';
import ModalInner from './common/ModalInner';
import Tab from './common/Tab';
import CodeEditor from '../CodeEditor';
import defaultSettings from '../../data/defaultSettings.yml';
import defaultSettings from '../../data/defaults/defaultSettings.yml';
const emptySettings = `# Add your custom settings here to override the
# default settings.
@ -81,10 +81,10 @@ export default {
</script>
<style lang="scss">
@import '../common/variables.scss';
@import '../../styles/variables.scss';
.modal__inner-1--settings {
max-width: 600px;
.modal__inner-1.modal__inner-1--settings {
max-width: 560px;
}
.modal__error--settings {

View File

@ -1,7 +1,7 @@
<template>
<modal-inner class="modal__inner-1--sponsor" aria-label="Sponsor">
<div class="modal__content">
<p>Please choose a <b>PayPal</b> option.</p>
<p>Please choose a <b>PayPal</b> option:</p>
<a class="paypal-option button flex flex--row flex--center" v-for="button in buttons" :key="button.id" :href="button.link">
<div class="flex flex--column">
<div>{{button.price}}<div class="paypal-option__offer" v-if="button.offer">{{button.offer}}</div></div>
@ -63,10 +63,10 @@ export default {
</script>
<style lang="scss">
@import '../common/variables.scss';
@import '../../styles/variables.scss';
.modal__inner-1--sponsor {
max-width: 380px;
.modal__inner-1.modal__inner-1--sponsor {
max-width: 400px;
}
.paypal-option {
@ -81,7 +81,7 @@ export default {
span {
display: inline-block;
font-size: 0.75rem;
opacity: 0.5;
opacity: 0.6;
white-space: normal;
line-height: 1.5;
}

View File

@ -1,38 +1,53 @@
<template>
<modal-inner class="modal__inner-1--sync-management" aria-label="Manage synchronized locations">
<div class="modal__content">
<div class="modal__image">
<icon-sync></icon-sync>
</div>
<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>
<div>
<div class="sync-entry flex flex--row flex--align-center" v-for="location in syncLocations" :key="location.id">
<div class="sync-entry__icon flex flex--column flex--center">
<icon-provider :provider-id="location.providerId"></icon-provider>
<div class="sync-entry flex flex--column" v-for="location in syncLocations" :key="location.id">
<div class="sync-entry__header flex flex--row flex--align-center">
<div class="sync-entry__icon flex flex--column flex--center">
<icon-provider :provider-id="location.providerId"></icon-provider>
</div>
<div class="sync-entry__description">
{{location.description}}
</div>
<div class="sync-entry__buttons flex flex--row flex--center">
<button class="sync-entry__button button" @click="remove(location)" v-title="'Remove location'">
<icon-delete></icon-delete>
</button>
</div>
</div>
<div class="sync-entry__description">
{{location.description}}
</div>
<div class="sync-entry__buttons flex flex--row flex--center">
<a class="sync-entry__button button" :href="location.url" target="_blank">
<icon-open-in-new></icon-open-in-new>
</a>
<button class="sync-entry__button button" @click="remove(location)">
<icon-delete></icon-delete>
</button>
<div class="sync-entry__row flex flex--row flex--align-center">
<div class="sync-entry__url">
{{location.url || 'Google Drive app data'}}
</div>
<div class="sync-entry__buttons flex flex--row flex--center" v-if="location.url">
<button class="sync-entry__button button" v-clipboard="location.url" @click="info('Location URL copied to clipboard!')" v-title="'Copy URL'">
<icon-content-copy></icon-content-copy>
</button>
<a class="sync-entry__button button" v-if="location.url" :href="location.url" target="_blank" v-title="'Open location'">
<icon-open-in-new></icon-open-in-new>
</a>
</div>
</div>
</div>
</div>
<div class="modal__info" v-if="syncLocations.length">
<b>Note:</b> Removing a synchronized location won't delete any file.
<b>Tip:</b> Removing a location won't delete any file.
</div>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.resolve()">Close</button>
<button class="button button--resolve" @click="config.resolve()">Close</button>
</div>
</modal-inner>
</template>
<script>
import { mapGetters } from 'vuex';
import { mapGetters, mapActions } from 'vuex';
import ModalInner from './common/ModalInner';
export default {
@ -44,66 +59,100 @@ export default {
'config',
]),
...mapGetters('syncLocation', {
syncLocations: 'current',
syncLocations: 'currentWithWorkspaceSyncLocation',
}),
currentFileName() {
return this.$store.getters['file/current'].name;
},
},
methods: {
...mapActions('notification', [
'info',
]),
remove(location) {
this.$store.commit('syncLocation/deleteItem', location.id);
if (location.id === 'main') {
this.info('This location can not be removed.');
} else {
this.$store.commit('syncLocation/deleteItem', location.id);
}
},
},
};
</script>
<style lang="scss">
@import '../common/variables.scss';
.modal__inner-1--sync-management {
max-width: 560px;
}
@import '../../styles/variables.scss';
.sync-entry {
padding: 0.5rem 0.25rem;
border-bottom: 1px solid $hr-color;
margin: 1.5em 0;
height: auto;
font-size: 17px;
line-height: 1.5;
}
&:last-child {
border-bottom: none;
}
$button-size: 30px;
$small-button-size: 22px;
.sync-entry__header {
line-height: $button-size;
}
.sync-entry__row {
margin-top: 1px;
padding-top: 1px;
border-top: 1px solid rgba(128, 128, 128, 0.15);
line-height: $small-button-size;
}
.sync-entry__icon {
height: 30px;
width: 30px;
height: 22px;
width: 22px;
margin-right: 0.75rem;
flex: none;
}
.sync-entry__description {
opacity: 0.5;
line-height: 1.4;
font-size: 0.9em;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.sync-entry__url {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
opacity: 0.5;
font-size: 0.67em;
}
.sync-entry__buttons {
margin-left: 0.75rem;
.sync-entry__row & {
margin-left: 0.5rem;
}
}
.sync-entry__button {
width: 38px;
height: 38px;
padding: 6px;
width: $button-size;
height: $button-size;
padding: 4px;
background-color: transparent;
opacity: 0.75;
.sync-entry__row & {
width: $small-button-size;
height: $small-button-size;
padding: 4px;
}
&:active,
&:focus,
&:hover {
opacity: 1;
background-color: rgba(0, 0, 0, 0.1);
}
}
</style>

View File

@ -45,7 +45,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -55,8 +55,8 @@ import { mapGetters } from 'vuex';
import utils from '../../services/utils';
import ModalInner from './common/ModalInner';
import CodeEditor from '../CodeEditor';
import emptyTemplateValue from '../../data/emptyTemplateValue.html';
import emptyTemplateHelpers from '!raw-loader!../../data/emptyTemplateHelpers.js'; // eslint-disable-line
import emptyTemplateValue from '../../data/empties/emptyTemplateValue.html';
import emptyTemplateHelpers from '!raw-loader!../../data/empties/emptyTemplateHelpers.js'; // eslint-disable-line
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
@ -91,11 +91,11 @@ export default {
},
created() {
this.$watch(
() => this.$store.getters['data/allTemplates'],
(allTemplates) => {
() => this.$store.getters['data/allTemplatesById'],
(allTemplatesById) => {
const templates = {};
// Sort templates by name
Object.entries(allTemplates)
Object.entries(allTemplatesById)
.sort(([, template1], [, template2]) => collator.compare(template1.name, template2.name))
.forEach(([id, template]) => {
const templateClone = utils.deepCopy(template);
@ -105,10 +105,12 @@ export default {
this.templates = templates;
this.selectedId = this.config.selectedId;
if (!templates[this.selectedId]) {
this.selectedId = Object.keys(templates)[0];
[this.selectedId] = Object.keys(templates);
}
this.isEditing = false;
}, { immediate: true });
},
{ immediate: true },
);
this.$watch('selectedId', (selectedId) => {
const template = this.templates[selectedId];
this.showHelpers = template.helpers !== emptyTemplateHelpers;
@ -137,7 +139,7 @@ export default {
},
remove() {
delete this.templates[this.selectedId];
this.selectedId = Object.keys(this.templates)[0];
[this.selectedId] = Object.keys(this.templates);
},
submitEdit(cancel) {
const template = this.templates[this.selectedId];
@ -161,7 +163,7 @@ export default {
</script>
<style lang="scss">
.modal__inner-1--templates {
max-width: 680px;
.modal__inner-1.modal__inner-1--templates {
max-width: 600px;
}
</style>

View File

@ -1,39 +1,69 @@
<template>
<modal-inner class="modal__inner-1--workspace-management" aria-label="Manage workspaces">
<div class="modal__content">
<div class="workspace-entry flex flex--row flex--align-center" v-for="(workspace, id) in sanitizedWorkspaces" :key="id">
<div class="workspace-entry__icon flex flex--column flex--center">
<icon-provider :provider-id="workspace.providerId"></icon-provider>
</div>
<div class="workspace-entry__description flex flex--column">
<input class="text-input" type="text" v-if="editedId === id" v-focus @blur="submitEdit()" @keydown.enter="submitEdit()" @keydown.esc.stop="submitEdit(true)" v-model="editingName">
<div class="workspace-entry__name" v-else>
{{workspace.name}}
<div class="modal__image">
<icon-database></icon-database>
</div>
<p>The following workspaces are locally available:</p>
<div class="workspace-entry flex flex--column" v-for="(workspace, id) in workspacesById" :key="id">
<div class="flex flex--column">
<div class="workspace-entry__header flex flex--row flex--align-center">
<div class="workspace-entry__icon">
<icon-provider :provider-id="workspace.providerId"></icon-provider>
</div>
<input class="text-input" type="text" v-if="editedId === id" v-focus @blur="submitEdit()" @keydown.enter="submitEdit()" @keydown.esc.stop="submitEdit(true)" v-model="editingName">
<div class="workspace-entry__name" v-else>{{workspace.name}}</div>
<div class="workspace-entry__buttons flex flex--row">
<button class="workspace-entry__button button" @click="edit(id)" v-title="'Edit name'">
<icon-pen></icon-pen>
</button>
<button class="workspace-entry__button button" @click="remove(id)" v-title="'Remove'">
<icon-delete></icon-delete>
</button>
</div>
</div>
<div class="workspace-entry__url">
{{workspace.url}}
<div class="workspace-entry__row flex flex--row flex--align-center">
<div class="workspace-entry__url">
{{workspace.url}}
</div>
<div class="workspace-entry__buttons flex flex--row">
<button class="workspace-entry__button button" v-clipboard="workspace.url" @click="info('Workspace URL copied to clipboard!')" v-title="'Copy URL'">
<icon-content-copy></icon-content-copy>
</button>
<a class="workspace-entry__button button" :href="workspace.url" target="_blank" v-title="'Open workspace'">
<icon-open-in-new></icon-open-in-new>
</a>
</div>
</div>
<div class="workspace-entry__row flex flex--row flex--align-center" v-if="workspace.locationUrl">
<div class="workspace-entry__url">
{{workspace.locationUrl}}
</div>
<div class="workspace-entry__buttons flex flex--row">
<button class="workspace-entry__button button" v-clipboard="workspace.locationUrl" @click="info('Workspace URL copied to clipboard!')" v-title="'Copy URL'">
<icon-content-copy></icon-content-copy>
</button>
<a class="workspace-entry__button button" :href="workspace.locationUrl" target="_blank" v-title="'Open workspace location'">
<icon-open-in-new></icon-open-in-new>
</a>
</div>
</div>
</div>
<div class="workspace-entry__buttons flex flex--row flex--center">
<button class="workspace-entry__button button" @click="edit(id)">
<icon-pen></icon-pen>
</button>
<button class="workspace-entry__button button" v-if="id !== currentWorkspace.id && id !== mainWorkspace.id" @click="remove(id)">
<icon-delete></icon-delete>
</button>
</div>
</div>
<div class="modal__info">
<b>ProTip:</b> A workspace is accessible <b>offline</b> once it has been opened for the first time.
</div>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.resolve()">Close</button>
<button class="button button--resolve" @click="config.resolve()">Close</button>
</div>
</modal-inner>
</template>
<script>
import { mapGetters } from 'vuex';
import { mapGetters, mapActions } from 'vuex';
import ModalInner from './common/ModalInner';
import localDbSvc from '../../services/localDbSvc';
import workspaceSvc from '../../services/workspaceSvc';
export default {
components: {
@ -47,105 +77,128 @@ export default {
...mapGetters('modal', [
'config',
]),
...mapGetters('data', [
'workspaces',
'sanitizedWorkspaces',
]),
...mapGetters('workspace', [
'workspacesById',
'mainWorkspace',
'currentWorkspace',
]),
},
methods: {
...mapActions('notification', [
'info',
]),
edit(id) {
this.editedId = id;
this.editingName = this.workspaces[id].name;
this.editingName = this.workspacesById[id].name;
},
submitEdit(cancel) {
const workspace = this.workspaces[this.editedId];
if (workspace && !cancel && this.editingName) {
this.$store.dispatch('data/patchWorkspaces', {
[this.editedId]: {
...workspace,
name: this.editingName,
},
});
} else {
this.editingName = workspace.name;
const workspace = this.workspacesById[this.editedId];
if (workspace) {
if (!cancel && this.editingName) {
this.$store.dispatch('workspace/patchWorkspacesById', {
[this.editedId]: {
...workspace,
name: this.editingName,
},
});
} else {
this.editingName = workspace.name;
}
}
this.editedId = null;
},
remove(id) {
return this.$store.dispatch('modal/removeWorkspace')
.then(
() => localDbSvc.removeWorkspace(id),
() => {}, // Cancel
);
async remove(id) {
if (id === this.mainWorkspace.id) {
this.info('Your main workspace can not be removed.');
} else if (id === this.currentWorkspace.id) {
this.info('Please close the workspace before removing it.');
} else {
try {
await this.$store.dispatch('modal/open', 'removeWorkspace');
workspaceSvc.removeWorkspace(id);
} catch (e) { /* Cancel */ }
}
},
},
};
</script>
<style lang="scss">
@import '../common/variables.scss';
.modal__inner-1--workspace-management {
max-width: 560px;
}
@import '../../styles/variables.scss';
.workspace-entry {
text-align: left;
padding-left: 10px;
margin: 15px 0;
margin: 1.75em 0;
height: auto;
font-size: 17px;
line-height: 1.5;
text-transform: none;
}
&:last-child {
border-bottom: none;
}
$button-size: 30px;
$small-button-size: 22px;
span {
text-overflow: ellipsis;
overflow: hidden;
.workspace-entry__header {
line-height: $button-size;
.text-input {
border: 1px solid $link-color;
padding: 0 5px;
line-height: $button-size;
height: $button-size;
}
}
.workspace-entry__row {
margin-top: 1px;
padding-top: 1px;
border-top: 1px solid rgba(128, 128, 128, 0.15);
line-height: $small-button-size;
}
.workspace-entry__icon {
height: 20px;
width: 20px;
margin-right: 12px;
height: 22px;
width: 22px;
margin-right: 0.75rem;
flex: none;
}
.workspace-entry__description {
width: 100%;
word-wrap: break-word;
overflow: hidden;
}
.workspace-entry__name {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: bold;
}
.workspace-entry__url {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
opacity: 0.5;
font-size: 0.75em;
font-size: 0.67em;
}
.workspace-entry__buttons {
margin-left: 0.75rem;
.workspace-entry__row & {
margin-left: 0.5rem;
}
}
.workspace-entry__button {
width: 36px;
height: 36px;
padding: 6px;
width: $button-size;
height: $button-size;
padding: 4px;
background-color: transparent;
opacity: 0.75;
.workspace-entry__row & {
width: $small-button-size;
height: $small-button-size;
padding: 4px;
}
&:active,
&:focus,
&:hover {

View File

@ -5,7 +5,7 @@
<icon-close></icon-close>
</button>
<div class="modal__sponsor-button" v-if="showSponsorButton">
StackEdit is <a class="not-tabbable" target="_blank" href="https://github.com/benweet/stackedit/">open source</a>. Please consider
StackEdit is <a class="not-tabbable" target="_blank" href="https://github.com/benweet/stackedit/">open source</a>, please consider
<a class="not-tabbable" href="javascript:void(0)" @click="sponsor">sponsoring</a> for just $5.
</div>
<slot></slot>
@ -24,40 +24,38 @@ export default {
'config',
]),
showSponsorButton() {
const type = this.$store.getters['modal/config'].type;
const { type } = this.$store.getters['modal/config'];
return !this.$store.getters.isSponsor && type !== 'sponsor' && type !== 'signInForSponsorship';
},
},
methods: {
sponsor() {
Promise.resolve()
.then(() => !this.$store.getters['workspace/sponsorToken'] &&
// If user has to sign in
this.$store.dispatch('modal/signInForSponsorship', {
onResolve: () => googleHelper.signin()
.then(() => syncSvc.requestSync()),
}))
.then(() => {
if (!this.$store.getters.isSponsor) {
this.$store.dispatch('modal/open', 'sponsor');
}
})
.catch(() => {}); // Cancel
async sponsor() {
try {
if (!this.$store.getters['workspace/sponsorToken']) {
// User has to sign in
await this.$store.dispatch('modal/open', 'signInForSponsorship');
await googleHelper.signin();
syncSvc.requestSync();
}
if (!this.$store.getters.isSponsor) {
await this.$store.dispatch('modal/open', 'sponsor');
}
} catch (e) { /* cancel */ }
},
},
};
</script>
<style lang="scss">
@import '../../common/variables.scss';
@import '../../../styles/variables.scss';
.modal__close-button {
position: absolute;
top: 8px;
right: 8px;
color: rgba(0, 0, 0, 0.5);
width: 30px;
height: 30px;
width: 32px;
height: 32px;
padding: 2px;
&:active,

View File

@ -52,28 +52,26 @@ export default (desc) => {
},
};
if (key === 'selectedTemplate') {
component.computed.allTemplates = () => {
const allTemplates = store.getters['data/allTemplates'];
const sortedTemplates = {};
Object.entries(allTemplates)
component.computed.allTemplatesById = () => {
const allTemplatesById = store.getters['data/allTemplatesById'];
const sortedTemplatesById = {};
Object.entries(allTemplatesById)
.sort(([, template1], [, template2]) => collator.compare(template1.name, template2.name))
.forEach(([templateId, template]) => {
sortedTemplates[templateId] = template;
sortedTemplatesById[templateId] = template;
});
return sortedTemplates;
return sortedTemplatesById;
};
// Make use of `function` to have `this` bound to the component
component.methods.configureTemplates = function () { // eslint-disable-line func-names
store.dispatch('modal/open', {
component.methods.configureTemplates = async function () { // eslint-disable-line func-names
const { templates, selectedId } = await store.dispatch('modal/open', {
type: 'templates',
selectedId: this.selectedTemplate,
})
.then(({ templates, selectedId }) => {
store.dispatch('data/setTemplates', templates);
store.dispatch('data/patchLocalSettings', {
[id]: selectedId,
});
});
});
store.dispatch('data/setTemplatesById', templates);
store.dispatch('data/patchLocalSettings', {
[id]: selectedId,
});
};
}
});

View File

@ -4,7 +4,7 @@
<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>
<p>Publish <b>{{currentFileName}}</b> to your <b>Blogger Page</b>.</p>
<form-entry label="Blog URL" error="blogUrl">
<input slot="field" class="textfield" type="text" v-model.trim="blogUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
@ -16,7 +16,7 @@
</form-entry>
<form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
<option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }}
</option>
</select>
@ -30,7 +30,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -54,7 +54,10 @@ export default modalTemplate({
} else {
// Return new location
const location = bloggerPageProvider.makeLocation(
this.config.token, this.blogUrl, this.pageId);
this.config.token,
this.blogUrl,
this.pageId,
);
location.templateId = this.selectedTemplate;
this.config.resolve(location);
}

View File

@ -4,7 +4,7 @@
<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>
<p>Publish <b>{{currentFileName}}</b> to your <b>Blogger</b> site.</p>
<form-entry label="Blog URL" error="blogUrl">
<input slot="field" class="textfield" type="text" v-model.trim="blogUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
@ -16,7 +16,7 @@
</form-entry>
<form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
<option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }}
</option>
</select>
@ -31,7 +31,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -55,7 +55,10 @@ export default modalTemplate({
} else {
// Return new location
const location = bloggerProvider.makeLocation(
this.config.token, this.blogUrl, this.postId);
this.config.token,
this.blogUrl,
this.postId,
);
location.templateId = this.selectedTemplate;
this.config.resolve(location);
}

View File

@ -14,7 +14,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -45,7 +45,7 @@ export default modalTemplate({
name: this.name,
password: this.password,
};
this.$store.dispatch('data/setCouchdbToken', token);
this.$store.dispatch('data/addCouchdbToken', token);
this.config.resolve();
}
},

View File

@ -4,20 +4,20 @@
<div class="modal__image">
<icon-provider provider-id="couchdb"></icon-provider>
</div>
<p>This will create a workspace synchronized with a <b>CouchDB</b> database.</p>
<p>Create a workspace synced with a <b>CouchDB</b> database.</p>
<form-entry label="Database URL" error="dbUrl">
<input slot="field" class="textfield" type="text" v-model.trim="dbUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> https://instance.smileupps.com/stackedit-workspace
</div>
<div class="form-entry__actions">
<a href="https://community.stackedit.io/t/couchdb-workspace-setup/" target="_blank">More info</a>
<a href="https://community.stackedit.io/t/couchdb-workspace-setup/" target="_blank">How to setup?</a>
</div>
</form-entry>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="dropbox"></icon-provider>
</div>
<p>This will link your <b>Dropbox</b> account to <b>StackEdit</b>.</p>
<p>Link your <b>Dropbox</b> account to <b>StackEdit</b>.</p>
<div class="form-entry">
<div class="form-entry__checkbox">
<label>
@ -18,7 +18,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="config.resolve()">Ok</button>
<button class="button button--resolve" @click="config.resolve()">Ok</button>
</div>
</modal-inner>
</template>

View File

@ -4,17 +4,17 @@
<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>
<p>Publish <b>{{currentFileName}}</b> to your <b>Dropbox</b>.</p>
<form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> {{config.token.fullAccess ? '' : '/Applications/StackEdit (restricted)'}}/path/to/My Document.html<br>
If the file exists, it will be replaced.
If the file exists, it will be overwritten.
</div>
</form-entry>
<form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
<option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }}
</option>
</select>
@ -25,7 +25,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>

View File

@ -4,18 +4,18 @@
<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>
<p>Save <b>{{currentFileName}}</b> to your <b>Dropbox</b> and keep it synced.</p>
<form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> {{config.token.fullAccess ? '' : '/Applications/StackEdit (restricted)'}}/path/to/My Document.md<br>
If the file exists, it will be replaced.
If the file exists, it will be overwritten.
</div>
</form-entry>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>

View File

@ -4,7 +4,7 @@
<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>
<p>Publish <b>{{currentFileName}}</b> to a <b>Gist</b>.</p>
<form-entry label="Filename" error="filename">
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keydown.enter="resolve()">
</form-entry>
@ -18,12 +18,12 @@
<form-entry label="Existing Gist ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="gistId" @keydown.enter="resolve()">
<div class="form-entry__info">
If the file exists in the Gist, it will be replaced.
If the file exists in the Gist, it will be overwritten.
</div>
</form-entry>
<form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
<option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }}
</option>
</select>
@ -37,7 +37,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -65,7 +65,11 @@ export default modalTemplate({
} else {
// Return new location
const location = gistProvider.makeLocation(
this.config.token, this.filename, this.isPublic, this.gistId);
this.config.token,
this.filename,
this.isPublic,
this.gistId,
);
location.templateId = this.selectedTemplate;
this.config.resolve(location);
}

View File

@ -4,7 +4,7 @@
<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>
<p>Save <b>{{currentFileName}}</b> to a <b>Gist</b> and keep it synced.</p>
<form-entry label="Filename" error="filename">
<input slot="field" class="textfield" type="text" v-model.trim="filename" @keydown.enter="resolve()">
</form-entry>
@ -18,13 +18,13 @@
<form-entry label="Existing Gist ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="gistId" @keydown.enter="resolve()">
<div class="form-entry__info">
If the file exists in the Gist, it will be replaced.
If the file exists in the Gist, it will be overwritten.
</div>
</form-entry>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -51,7 +51,11 @@ export default modalTemplate({
} else {
// Return new location
const location = gistProvider.makeLocation(
this.config.token, this.filename, this.isPublic, this.gistId);
this.config.token,
this.filename,
this.isPublic,
this.gistId,
);
this.config.resolve(location);
}
},

View File

@ -4,18 +4,18 @@
<div class="modal__image">
<icon-provider provider-id="github"></icon-provider>
</div>
<p>This will link your <b>GitHub</b> account to <b>StackEdit</b>.</p>
<p>Link your <b>GitHub</b> account to <b>StackEdit</b>.</p>
<div class="form-entry">
<div class="form-entry__checkbox">
<label>
<input type="checkbox" v-model="repoFullAccess"> Grant access to my <b>private repositories</b>
<input type="checkbox" v-model="repoFullAccess"> Grant access to your private repositories
</label>
</div>
</div>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="config.resolve()">Ok</button>
<button class="button button--resolve" @click="config.resolve()">Ok</button>
</div>
</modal-inner>
</template>

View File

@ -4,29 +4,29 @@
<div class="modal__image">
<icon-provider provider-id="github"></icon-provider>
</div>
<p>This will open a file from your <b>GitHub</b> repository and keep it synchronized.</p>
<p>Open a file from your <b>GitHub</b> repository and keep it synced.</p>
<form-entry label="Repository URL" error="repoUrl">
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> https://github.com/benweet/stackedit
</div>
</form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not provided, the <code>master</code> branch will be used.
</div>
</form-entry>
<form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> docs/README.md
<b>Example:</b> path/to/README.md
</div>
</form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -34,6 +34,7 @@
<script>
import githubProvider from '../../../services/providers/githubProvider';
import modalTemplate from '../common/modalTemplate';
import utils from '../../../services/utils';
export default modalTemplate({
data: () => ({
@ -52,13 +53,18 @@ export default modalTemplate({
this.setError('path');
}
if (this.repoUrl && this.path) {
const parsedRepo = githubProvider.parseRepoUrl(this.repoUrl);
const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl);
if (!parsedRepo) {
this.setError('repoUrl');
} else {
// Return new location
const location = githubProvider.makeLocation(
this.config.token, parsedRepo.owner, parsedRepo.repo, this.branch || 'master', this.path);
this.config.token,
parsedRepo.owner,
parsedRepo.repo,
this.branch || 'master',
this.path,
);
this.config.resolve(location);
}
}

View File

@ -4,29 +4,29 @@
<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>
<p>Publish <b>{{currentFileName}}</b> to your <b>GitHub</b> repository.</p>
<form-entry label="Repository URL" error="repoUrl">
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> https://github.com/benweet/stackedit
</div>
</form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not provided, the master branch will be used.
</div>
</form-entry>
<form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> docs/README.md<br>
If the file exists, it will be replaced.
<b>Example:</b> path/to/README.md<br>
If the file exists, it will be overwritten.
</div>
</form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
<form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
<option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }}
</option>
</select>
@ -37,7 +37,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -73,7 +73,12 @@ export default modalTemplate({
} else {
// Return new location
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,
);
location.templateId = this.selectedTemplate;
this.config.resolve(location);
}

View File

@ -4,7 +4,7 @@
<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>
<p>Save <b>{{currentFileName}}</b> to your <b>GitHub</b> repository and keep it synced.</p>
<form-entry label="Repository URL" error="repoUrl">
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
@ -14,20 +14,20 @@
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not provided, the <code>master</code> branch will be used.
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
<form-entry label="File path" error="path">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> docs/README.md<br>
If the file exists, it will be replaced.
<b>Example:</b> path/to/README.md<br>
If the file exists, it will be overwritten.
</div>
</form-entry>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -35,6 +35,7 @@
<script>
import githubProvider from '../../../services/providers/githubProvider';
import modalTemplate from '../common/modalTemplate';
import utils from '../../../services/utils';
export default modalTemplate({
data: () => ({
@ -49,22 +50,22 @@ export default modalTemplate({
},
methods: {
resolve() {
if (!this.repoUrl) {
const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl);
if (!parsedRepo) {
this.setError('repoUrl');
}
if (!this.path) {
this.setError('path');
}
if (this.repoUrl && this.path) {
const parsedRepo = githubProvider.parseRepoUrl(this.repoUrl);
if (!parsedRepo) {
this.setError('repoUrl');
} else {
// Return new location
const location = githubProvider.makeLocation(
this.config.token, parsedRepo.owner, parsedRepo.repo, this.branch || 'master', this.path);
this.config.resolve(location);
}
if (parsedRepo && this.path) {
const location = githubProvider.makeLocation(
this.config.token,
parsedRepo.owner,
parsedRepo.repo,
this.branch || 'master',
this.path,
);
this.config.resolve(location);
}
},
},

View File

@ -0,0 +1,65 @@
<template>
<modal-inner aria-label="Synchronize with GitHub">
<div class="modal__content">
<div class="modal__image">
<icon-provider provider-id="github"></icon-provider>
</div>
<p>Create a workspace synced with a <b>GitHub</b> repository folder.</p>
<form-entry label="Repository URL" error="repoUrl">
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> https://github.com/benweet/stackedit
</div>
</form-entry>
<form-entry label="Branch" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the <code>master</code> branch will be used.
</div>
</form-entry>
<form-entry label="Folder path" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
<div class="form-entry__info">
If not supplied, the root folder will be used.
</div>
</form-entry>
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
<script>
import utils from '../../../services/utils';
import modalTemplate from '../common/modalTemplate';
export default modalTemplate({
data: () => ({
branch: '',
path: '',
}),
computedLocalSettings: {
repoUrl: 'githubWorkspaceRepoUrl',
},
methods: {
resolve() {
const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl);
if (!parsedRepo) {
this.setError('repoUrl');
} else {
const path = this.path && this.path.replace(/^\//, '');
const url = utils.addQueryParams('app', {
...parsedRepo,
providerId: 'githubWorkspace',
branch: this.branch || 'master',
path: path || undefined,
}, true);
this.config.resolve();
window.open(url);
}
},
},
});
</script>

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="googleDrive"></icon-provider>
</div>
<p>This will link your <b>Google Drive</b> account to <b>StackEdit</b>.</p>
<p>Link your <b>Google Drive</b> account to <b>StackEdit</b>.</p>
<div class="form-entry">
<div class="form-entry__checkbox">
<label>
@ -18,7 +18,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="config.resolve()">Ok</button>
<button class="button button--resolve" @click="config.resolve()">Ok</button>
</div>
</modal-inner>
</template>

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="googleDrive"></icon-provider>
</div>
<p>This will publish <b>{{currentFileName}}</b> to your <b>Google Drive</b> account.</p>
<p>Publish <b>{{currentFileName}}</b> to your <b>Google Drive</b> account.</p>
<form-entry label="Folder ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
<div class="form-entry__info">
@ -32,9 +32,9 @@
</label>
</div>
</div>
<form-entry label="Template">
<form-entry label="Template" v-if="format === 'html'">
<select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
<option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }}
</option>
</select>
@ -48,7 +48,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -73,16 +73,18 @@ export default modalTemplate({
'modal/hideUntil',
googleHelper.openPicker(this.config.token, 'folder')
.then((folders) => {
this.$store.dispatch('data/patchLocalSettings', {
googleDriveFolderId: folders[0].id,
});
}));
if (folders[0]) {
this.$store.dispatch('data/patchLocalSettings', {
googleDriveFolderId: folders[0].id,
});
}
}),
);
},
resolve() {
// Return new location
const location = googleDriveProvider.makeLocation(
this.config.token, this.fileId);
if (this.format) {
const location = googleDriveProvider.makeLocation(this.config.token, this.fileId);
if (this.format === 'html') {
location.templateId = this.selectedTemplate;
}
this.config.resolve(location);

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="googleDrive"></icon-provider>
</div>
<p>This will save <b>{{currentFileName}}</b> to your <b>Google Drive</b> account and keep it synchronized.</p>
<p>Save <b>{{currentFileName}}</b> to your <b>Google Drive</b> account and keep it synced.</p>
<form-entry label="Folder ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
<div class="form-entry__info">
@ -23,7 +23,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -46,15 +46,21 @@ export default modalTemplate({
'modal/hideUntil',
googleHelper.openPicker(this.config.token, 'folder')
.then((folders) => {
this.$store.dispatch('data/patchLocalSettings', {
googleDriveFolderId: folders[0].id,
});
}));
if (folders[0]) {
this.$store.dispatch('data/patchLocalSettings', {
googleDriveFolderId: folders[0].id,
});
}
}),
);
},
resolve() {
// Return new location
const location = googleDriveProvider.makeLocation(
this.config.token, this.fileId, this.folderId);
this.config.token,
this.fileId,
this.folderId,
);
this.config.resolve(location);
},
},

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="googleDrive"></icon-provider>
</div>
<p>This will create a workspace synchronized with a <b>Google Drive</b> folder.</p>
<p>Create a workspace synced with a <b>Google Drive</b> folder.</p>
<form-entry label="Folder ID" info="optional">
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
<div class="form-entry__info">
@ -17,7 +17,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -37,10 +37,13 @@ export default modalTemplate({
'modal/hideUntil',
googleHelper.openPicker(this.config.token, 'folder')
.then((folders) => {
this.$store.dispatch('data/patchLocalSettings', {
googleDriveWorkspaceFolderId: folders[0].id,
});
}));
if (folders[0]) {
this.$store.dispatch('data/patchLocalSettings', {
googleDriveWorkspaceFolderId: folders[0].id,
});
}
}),
);
},
resolve() {
const url = utils.addQueryParams('app', {

View File

@ -11,7 +11,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -42,20 +42,20 @@ export default {
},
methods: {
resolve() {
let url = this.config.url;
let { url } = this.config;
const size = parseInt(this.size, 10);
if (!isNaN(size)) {
if (!Number.isNaN(size)) {
url = makeThumbnail(url, size);
}
if (this.title) {
url += ` "${this.title}"`;
}
const callback = this.config.callback;
const { callback } = this.config;
this.config.resolve();
callback(url);
},
reject() {
const callback = this.config.callback;
const { callback } = this.config;
this.config.reject();
callback(null);
},

View File

@ -4,12 +4,12 @@
<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>
<p>Publish <b>{{currentFileName}}</b> to your <b>WordPress</b> site.</p>
<form-entry label="Site domain" error="domain">
<input slot="field" class="textfield" type="text" v-model.trim="domain" @keydown.enter="resolve()">
<div class="form-entry__info">
<b>Example:</b> example.wordpress.com<br>
<b>Jetpack plugin</b> is required for self-hosted sites.
<b>Note:</b> Jetpack is required for self-hosted sites.
</div>
</form-entry>
<form-entry label="Existing post ID" info="optional">
@ -17,7 +17,7 @@
</form-entry>
<form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
<option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }}
</option>
</select>
@ -33,7 +33,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -57,7 +57,10 @@ export default modalTemplate({
} else {
// Return new location
const location = wordpressProvider.makeLocation(
this.config.token, this.domain, this.postId);
this.config.token,
this.domain,
this.postId,
);
location.templateId = this.selectedTemplate;
this.config.resolve(location);
}

View File

@ -4,7 +4,7 @@
<div class="modal__image">
<icon-provider provider-id="zendesk"></icon-provider>
</div>
<p>This will link your <b>Zendesk</b> account to <b>StackEdit</b>.</p>
<p>Link your <b>Zendesk</b> account to <b>StackEdit</b>.</p>
<form-entry label="Site URL" error="siteUrl">
<input slot="field" class="textfield" type="text" v-model.trim="siteUrl" @keydown.enter="resolve()">
<div class="form-entry__info">
@ -21,18 +21,18 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
<script>
import utils from '../../../services/utils';
import modalTemplate from '../common/modalTemplate';
import constants from '../../../data/constants';
export default modalTemplate({
data: () => ({
redirectUrl: utils.oauth2RedirectUri,
redirectUrl: constants.oauth2RedirectUri,
}),
computedLocalSettings: {
siteUrl: 'zendeskSiteUrl',

View File

@ -4,7 +4,7 @@
<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>
<p>Publish <b>{{currentFileName}}</b> to your <b>Zendesk Help Center</b>.</p>
<form-entry label="Section ID" error="sectionId">
<input slot="field" class="textfield" type="text" v-model.trim="sectionId" @keydown.enter="resolve()">
<div class="form-entry__info">
@ -22,7 +22,7 @@
</form-entry>
<form-entry label="Template">
<select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
<option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
{{ template.name }}
</option>
</select>
@ -37,7 +37,7 @@
</div>
<div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button>
<button class="button" @click="resolve()">Ok</button>
<button class="button button--resolve" @click="resolve()">Ok</button>
</div>
</modal-inner>
</template>
@ -62,7 +62,11 @@ export default modalTemplate({
} else {
// Return new location
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,
);
location.templateId = this.selectedTemplate;
this.config.resolve(location);
}

View File

@ -1 +0,0 @@
@import './common/base';

30
src/data/constants.js Normal file
View File

@ -0,0 +1,30 @@
const origin = `${window.location.protocol}//${window.location.host}`;
export default {
cleanTrashAfter: 0 * 24 * 60 * 60 * 1000, // 7 days
origin,
oauth2RedirectUri: `${origin}/oauth2/callback`,
types: [
'contentState',
'syncedContent',
'content',
'file',
'folder',
'syncLocation',
'publishLocation',
'data',
],
localStorageDataIds: [
'workspaces',
'settings',
'layoutSettings',
'tokens',
],
userIdPrefixes: {
db: 'dropbox',
gh: 'github',
go: 'google',
},
textMaxLength: 250000,
defaultName: 'Untitled',
};

View File

@ -15,6 +15,7 @@ export default () => ({
dropboxPublishTemplate: 'styledHtml',
githubRepoFullAccess: false,
githubRepoUrl: '',
githubWorkspaceRepoUrl: '',
githubPublishTemplate: 'jekyllSite',
gistIsPublic: false,
gistPublishTemplate: 'plainText',

View File

@ -1,11 +1,11 @@
# light or dark
colorTheme: light
# Auto-sync frequency (in ms). Minimum is 60000.
autoSyncEvery: 60000
# Adjust font size in editor and preview
fontSizeFactor: 1
# Adjust maximum text width in editor and preview
maxWidthFactor: 1
# Auto-sync frequency (in ms). Minimum is 60000.
autoSyncEvery: 90000
# Editor settings
editor:
@ -54,7 +54,7 @@ wkhtmltopdf:
marginRight: 25
marginBottom: 25
marginLeft: 25
# `A3`, `A4`, `Legal` or `Letter`
# A3, A4, Legal or Letter
pageSize: A4
# Options passed to pandoc
@ -77,6 +77,12 @@ turndown:
linkStyle: inlined
linkReferenceStyle: full
# GitHub commit messages
github:
createFileMessage: '{{path}} created from https://stackedit.io/'
updateFileMessage: '{{path}} updated from https://stackedit.io/'
deleteFileMessage: '{{path}} deleted from https://stackedit.io/'
# Default content for new files
newFileContent: |

View File

@ -1,6 +1,6 @@
export default () => ({
main: {
name: 'Main workspace',
// The rest will be filled by the data/sanitizedWorkspaces getter
// The rest will be filled by the workspace/workspacesById getter
},
});

Some files were not shown because too many files have changed in this diff Show More