merge master
This commit is contained in:
commit
beb695e53c
2
.babelrc
2
.babelrc
@ -8,7 +8,7 @@
|
||||
"env": {
|
||||
"test": {
|
||||
"presets": ["env", "stage-2"],
|
||||
"plugins": [ "istanbul" ]
|
||||
"plugins": ["transform-es2015-modules-commonjs", "dynamic-import-node"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -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/
|
||||
|
46
CHANGELOG.md
46
CHANGELOG.md
@ -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
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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']
|
||||
}),
|
||||
|
@ -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']
|
||||
})
|
||||
|
@ -14,7 +14,7 @@ function resolve (dir) {
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
style: './src/components/style.scss'
|
||||
style: './src/styles/'
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
|
15687
package-lock.json
generated
15687
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
107
package.json
107
package.json
@ -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"
|
||||
]
|
||||
}
|
||||
|
@ -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'),
|
||||
);
|
||||
};
|
||||
|
@ -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';
|
||||
|
@ -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) {
|
||||
|
@ -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,7 +20,7 @@ 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));
|
||||
})
|
||||
@ -28,11 +30,12 @@ exports.getUser = id => new Promise((resolve, reject) => {
|
||||
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({
|
||||
.then(
|
||||
user => res.send(Object.assign({
|
||||
sponsorUntil: 0,
|
||||
}, user)),
|
||||
err => res.status(400).send(err ? err.message || err.toString() : 'invalid_token'));
|
||||
err => res
|
||||
.status(400)
|
||||
.send(err ? err.message || err.toString() : 'invalid_token'),
|
||||
);
|
||||
|
||||
exports.paypalIpn = (req, res, next) => Promise.resolve()
|
||||
.then(() => {
|
||||
|
@ -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();
|
||||
async created() {
|
||||
try {
|
||||
await syncSvc.init();
|
||||
await networkSvc.init();
|
||||
await sponsorSvc.init();
|
||||
this.ready = true;
|
||||
tempFileSvc.setReady();
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err && err.message !== 'reload') {
|
||||
} 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>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
close(item = null) {
|
||||
this.resolve(item);
|
||||
}
|
||||
this.$store.dispatch('contextMenu/close');
|
||||
},
|
||||
},
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
try {
|
||||
if (newChildNode.isFolder) {
|
||||
const id = utils.uid();
|
||||
this.$store.commit('folder/setItem', {
|
||||
...newChildNode.item,
|
||||
id,
|
||||
name: utils.sanitizeName(newChildNode.item.name),
|
||||
});
|
||||
this.select(id);
|
||||
const item = await workspaceSvc.storeItem(newChildNode.item);
|
||||
this.select(item.id);
|
||||
} else {
|
||||
this.$store.dispatch('createFile', newChildNode.item)
|
||||
.then(file => this.select(file.id));
|
||||
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: '▾';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -140,7 +140,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/variables.scss';
|
||||
@import '../styles/variables.scss';
|
||||
|
||||
.layout {
|
||||
position: absolute;
|
||||
|
@ -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);
|
||||
this.$watch(
|
||||
() => this.config,
|
||||
(isOpen) => {
|
||||
if (isOpen) {
|
||||
const tabbables = getTabbables(this.$el);
|
||||
if (tabbables[0]) {
|
||||
tabbables[0].focus();
|
||||
}
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('focusin', this.onFocusInOut);
|
||||
window.removeEventListener('focusout', this.onFocusInOut);
|
||||
{ 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 {
|
||||
|
@ -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,9 +64,7 @@ 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;
|
||||
@ -73,7 +72,8 @@ const getShortcut = (method) => {
|
||||
// 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();
|
||||
if (title) {
|
||||
this.$store.dispatch('file/patchCurrent', { name: utils.sanitizeName(title) });
|
||||
} else {
|
||||
this.title = this.$store.getters['file/current'].name;
|
||||
if (title) {
|
||||
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;
|
||||
|
@ -23,7 +23,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/variables.scss';
|
||||
@import '../styles/variables.scss';
|
||||
|
||||
.notification {
|
||||
position: absolute;
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
@ -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;
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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);
|
||||
|
80
src/components/common/vueGlobals.js
Normal file
80
src/components/common/vueGlobals.js
Normal 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));
|
||||
|
@ -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 };
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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', {
|
||||
async removeDiscussion() {
|
||||
try {
|
||||
await this.$store.dispatch('modal/open', 'discussionDeletion');
|
||||
this.$store.dispatch('discussion/cleanCurrentFile', {
|
||||
filterDiscussion: this.currentDiscussion,
|
||||
}),
|
||||
() => {}); // Cancel
|
||||
});
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../common/variables.scss';
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
.current-discussion {
|
||||
position: absolute;
|
||||
|
@ -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', [
|
||||
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 };
|
||||
};
|
||||
|
@ -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();
|
||||
});
|
||||
},
|
||||
|
@ -33,7 +33,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../common/variables.scss';
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
.sticky-comment {
|
||||
position: absolute;
|
||||
|
@ -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 */ });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -1,15 +1,29 @@
|
||||
<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 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="side-bar__info" v-if="loading">
|
||||
<p>Loading history…</p>
|
||||
<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 class="side-bar__info" v-else-if="!revisionsWithSpacer.length">
|
||||
<p><b>{{currentFileName}}</b> has no history.</p>
|
||||
</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)">
|
||||
@ -22,6 +36,7 @@
|
||||
</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">
|
||||
<button class="history__button button" @click="showMore">More</button>
|
||||
@ -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));
|
||||
});
|
||||
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(() => {
|
||||
revisionContentPromise.catch((err) => {
|
||||
this.$store.dispatch('notification/error', err);
|
||||
revisionContentPromises[revision.id] = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
if (revisionContentPromise) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
const content = await readFile(file);
|
||||
const item = await workspaceSvc.createFile({
|
||||
...Provider.parseContent(content),
|
||||
name: file.name,
|
||||
})
|
||||
.then(item => this.$store.commit('file/setCurrentId', item.id)));
|
||||
});
|
||||
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(/ /g, ' '), // Replace non-breaking spaces with classic spaces
|
||||
)),
|
||||
const content = await readFile(file);
|
||||
const sanitizedContent = htmlSanitizer.sanitizeHtml(content)
|
||||
.replace(/ /g, ' '); // Replace non-breaking spaces with classic spaces
|
||||
const item = await workspaceSvc.createFile({
|
||||
...Provider.parseContent(turndownService.turndown(sanitizedContent)),
|
||||
name: file.name,
|
||||
}))
|
||||
.then(item => this.$store.commit('file/setCurrentId', item.id));
|
||||
});
|
||||
this.$store.commit('file/setCurrentId', item.id);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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', {
|
||||
const publishModalOpener = type => async (token) => {
|
||||
try {
|
||||
const publishLocation = await store.dispatch('modal/open', {
|
||||
type,
|
||||
token,
|
||||
}).then(publishLocation => publishSvc.createPublishLocation(publishLocation));
|
||||
});
|
||||
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>
|
||||
|
@ -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', {
|
||||
async openGithub(token) {
|
||||
try {
|
||||
const syncLocation = await store.dispatch('modal/open', {
|
||||
type: 'githubOpen',
|
||||
token,
|
||||
})
|
||||
.then(syncLocation => this.$store.dispatch('queue/enqueue',
|
||||
() => githubProvider.openFile(token, syncLocation)));
|
||||
});
|
||||
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 */ }
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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) => {
|
||||
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]);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -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,
|
||||
}));
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -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);
|
||||
}));
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -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([
|
||||
const [sponsorToken, token, html] = 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(),
|
||||
exportSvc.applyTemplate(
|
||||
currentFile.id, this.allTemplates[this.selectedTemplate], true),
|
||||
])
|
||||
.then(([sponsorToken, token, html]) => networkSvc.request({
|
||||
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);
|
||||
}));
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -1,10 +1,14 @@
|
||||
<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 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>
|
||||
@ -12,27 +16,38 @@
|
||||
{{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)">
|
||||
<button class="publish-entry__button button" @click="remove(location)" v-title="'Remove location'">
|
||||
<icon-delete></icon-delete>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<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 {
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
<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 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>
|
||||
@ -12,27 +16,38 @@
|
||||
{{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)">
|
||||
<button class="sync-entry__button button" @click="remove(location)" v-title="'Remove location'">
|
||||
<icon-delete></icon-delete>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<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) {
|
||||
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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
<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>
|
||||
<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>
|
||||
<div class="workspace-entry__url">
|
||||
{{workspace.url}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="workspace-entry__buttons flex flex--row flex--center">
|
||||
<button class="workspace-entry__button button" @click="edit(id)">
|
||||
<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" v-if="id !== currentWorkspace.id && id !== mainWorkspace.id" @click="remove(id)">
|
||||
<button class="workspace-entry__button button" @click="remove(id)" v-title="'Remove'">
|
||||
<icon-delete></icon-delete>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<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,24 +77,25 @@ 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', {
|
||||
const workspace = this.workspacesById[this.editedId];
|
||||
if (workspace) {
|
||||
if (!cancel && this.editingName) {
|
||||
this.$store.dispatch('workspace/patchWorkspacesById', {
|
||||
[this.editedId]: {
|
||||
...workspace,
|
||||
name: this.editingName,
|
||||
@ -73,79 +104,101 @@ export default {
|
||||
} 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 {
|
||||
|
@ -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');
|
||||
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();
|
||||
}
|
||||
})
|
||||
.catch(() => {}); // Cancel
|
||||
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,
|
||||
|
@ -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/setTemplatesById', templates);
|
||||
store.dispatch('data/patchLocalSettings', {
|
||||
[id]: selectedId,
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,23 +50,23 @@ 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
|
||||
if (parsedRepo && this.path) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
65
src/components/modals/providers/GithubWorkspaceModal.vue
Normal file
65
src/components/modals/providers/GithubWorkspaceModal.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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) => {
|
||||
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);
|
||||
|
@ -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) => {
|
||||
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);
|
||||
},
|
||||
},
|
||||
|
@ -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) => {
|
||||
if (folders[0]) {
|
||||
this.$store.dispatch('data/patchLocalSettings', {
|
||||
googleDriveWorkspaceFolderId: folders[0].id,
|
||||
});
|
||||
}));
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
resolve() {
|
||||
const url = utils.addQueryParams('app', {
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
@import './common/base';
|
30
src/data/constants.js
Normal file
30
src/data/constants.js
Normal 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',
|
||||
};
|
@ -15,6 +15,7 @@ export default () => ({
|
||||
dropboxPublishTemplate: 'styledHtml',
|
||||
githubRepoFullAccess: false,
|
||||
githubRepoUrl: '',
|
||||
githubWorkspaceRepoUrl: '',
|
||||
githubPublishTemplate: 'jekyllSite',
|
||||
gistIsPublic: false,
|
||||
gistPublishTemplate: 'plainText',
|
@ -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: |
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user