Added favicons.
Added sponsorship options. Added pdf and pandoc export. Added emoji and mermaid extensions. Added find/replace support. Added HTML template with TOC. Updated welcome file.
This commit is contained in:
parent
43af45a6ed
commit
abd0890512
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
dist
|
||||||
|
.history
|
16
Dockerfile
Normal file
16
Dockerfile
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
FROM benweet/stackedit-base
|
||||||
|
|
||||||
|
RUN mkdir -p /usr/src/app
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY package.json /usr/src/app/
|
||||||
|
COPY yarn.lock /usr/src/app/
|
||||||
|
COPY gulpfile.js /usr/src/app/
|
||||||
|
RUN yarn && yarn cache clean
|
||||||
|
COPY . /usr/src/app
|
||||||
|
ENV NODE_ENV production
|
||||||
|
RUN yarn run build
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD [ "node", "." ]
|
@ -4,6 +4,7 @@ var utils = require('./utils')
|
|||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var vueLoaderConfig = require('./vue-loader.conf')
|
var vueLoaderConfig = require('./vue-loader.conf')
|
||||||
var StylelintPlugin = require('stylelint-webpack-plugin')
|
var StylelintPlugin = require('stylelint-webpack-plugin')
|
||||||
|
var FaviconsWebpackPlugin = require('favicons-webpack-plugin')
|
||||||
|
|
||||||
function resolve (dir) {
|
function resolve (dir) {
|
||||||
return path.join(__dirname, '..', dir)
|
return path.join(__dirname, '..', dir)
|
||||||
@ -13,6 +14,10 @@ module.exports = {
|
|||||||
entry: {
|
entry: {
|
||||||
app: './src/'
|
app: './src/'
|
||||||
},
|
},
|
||||||
|
node: {
|
||||||
|
// For mermaid
|
||||||
|
fs: 'empty' // jison generated code requires 'fs'
|
||||||
|
},
|
||||||
output: {
|
output: {
|
||||||
path: config.build.assetsRoot,
|
path: config.build.assetsRoot,
|
||||||
filename: '[name].js',
|
filename: '[name].js',
|
||||||
@ -45,7 +50,14 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
loader: 'babel-loader',
|
loader: 'babel-loader',
|
||||||
include: [resolve('src'), resolve('test')]
|
include: [resolve('src'), resolve('test'), resolve('node_modules/mermaid/src')],
|
||||||
|
exclude: [
|
||||||
|
resolve('node_modules/mermaid/src/diagrams/classDiagram/parser'),
|
||||||
|
resolve('node_modules/mermaid/src/diagrams/flowchart/parser'),
|
||||||
|
resolve('node_modules/mermaid/src/diagrams/gantt/parser'),
|
||||||
|
resolve('node_modules/mermaid/src/diagrams/gitGraph/parser'),
|
||||||
|
resolve('node_modules/mermaid/src/diagrams/sequenceDiagram/parser'),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||||
@ -56,13 +68,9 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(ttf|eot|otf|woff2)$/, loader: 'ignore-loader'
|
test: /\.(ttf|eot|otf|woff2?)(\?.*)?$/,
|
||||||
},
|
loader: 'file-loader',
|
||||||
{
|
|
||||||
test: /\.woff(\?.*)?$/,
|
|
||||||
loader: 'url-loader',
|
|
||||||
options: {
|
options: {
|
||||||
limit: 10000,
|
|
||||||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -76,6 +84,10 @@ module.exports = {
|
|||||||
new StylelintPlugin({
|
new StylelintPlugin({
|
||||||
files: ['**/*.vue', '**/*.scss']
|
files: ['**/*.vue', '**/*.scss']
|
||||||
}),
|
}),
|
||||||
|
new FaviconsWebpackPlugin({
|
||||||
|
logo: resolve('src/assets/favicon.png'),
|
||||||
|
title: 'StackEdit',
|
||||||
|
}),
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
VERSION: JSON.stringify(require('../package.json').version)
|
VERSION: JSON.stringify(require('../package.json').version)
|
||||||
})
|
})
|
||||||
|
@ -96,7 +96,7 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||||||
ServiceWorker: {
|
ServiceWorker: {
|
||||||
events: true
|
events: true
|
||||||
},
|
},
|
||||||
excludes: ['**/.*', '**/*.map', '**/index.html', '**/static/oauth2/callback.html'],
|
excludes: ['**/.*', '**/*.map', '**/index.html', '**/static/oauth2/callback.html', '**/icons-*/*.png', '**/static/fonts/KaTeX_*'],
|
||||||
externals: ['/app', '/oauth2/callback']
|
externals: ['/app', '/oauth2/callback']
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
|
57
build/webpack.style.conf.js
Normal file
57
build/webpack.style.conf.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
var path = require('path')
|
||||||
|
var utils = require('./utils')
|
||||||
|
var webpack = require('webpack')
|
||||||
|
var utils = require('./utils')
|
||||||
|
var config = require('../config')
|
||||||
|
var vueLoaderConfig = require('./vue-loader.conf')
|
||||||
|
var StylelintPlugin = require('stylelint-webpack-plugin')
|
||||||
|
var FaviconsWebpackPlugin = require('favicons-webpack-plugin')
|
||||||
|
var ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||||
|
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
|
||||||
|
|
||||||
|
function resolve (dir) {
|
||||||
|
return path.join(__dirname, '..', dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: {
|
||||||
|
style: './src/components/style.scss'
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [{
|
||||||
|
test: /\.(ttf|eot|otf|woff2?)(\?.*)?$/,
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
.concat(utils.styleLoaders({
|
||||||
|
sourceMap: config.build.productionSourceMap,
|
||||||
|
extract: true
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: config.build.assetsRoot,
|
||||||
|
filename: '[name].js',
|
||||||
|
publicPath: config.build.assetsPublicPath
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.optimize.UglifyJsPlugin({
|
||||||
|
compress: {
|
||||||
|
warnings: false
|
||||||
|
},
|
||||||
|
sourceMap: true
|
||||||
|
}),
|
||||||
|
// extract css into its own file
|
||||||
|
new ExtractTextPlugin({
|
||||||
|
filename: '[name].css',
|
||||||
|
}),
|
||||||
|
// Compress extracted CSS. We are using this plugin so that possible
|
||||||
|
// duplicated CSS from different components can be deduped.
|
||||||
|
new OptimizeCSSPlugin({
|
||||||
|
cssProcessorOptions: {
|
||||||
|
safe: true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
@ -4,11 +4,6 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>StackEdit</title>
|
<title>StackEdit</title>
|
||||||
<link rel="canonical" href="https://stackedit.io/app">
|
<link rel="canonical" href="https://stackedit.io/app">
|
||||||
<!-- <link rel="icon" href="res-min/img/stackedit-32.ico" type="image/x-icon"> -->
|
|
||||||
<!-- <link rel="icon" sizes="192x192" href="res-min/img/logo-highres.png"> -->
|
|
||||||
<!-- <link rel="shortcut icon" href="res-min/img/stackedit-32.ico" type="image/x-icon"> -->
|
|
||||||
<!-- <link rel="shortcut icon" sizes="192x192" href="res-min/img/logo-highres.png"> -->
|
|
||||||
<!-- <link rel="apple-touch-icon-precomposed" sizes="152x152" href="res-min/img/logo-ipad-retina.png"> -->
|
|
||||||
<meta name="description" content="Free, open-source, full-featured Markdown editor.">
|
<meta name="description" content="Free, open-source, full-featured Markdown editor.">
|
||||||
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
|
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
@ -18,5 +13,6 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
|
<script src="//cdn.monetizejs.com/api/js/latest/monetize.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
13
package.json
13
package.json
@ -11,7 +11,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "gulp build-prism",
|
"postinstall": "gulp build-prism",
|
||||||
"start": "node build/dev-server.js",
|
"start": "node build/dev-server.js",
|
||||||
"build": "node build/build.js",
|
"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",
|
"lint": "eslint --ext .js,.vue src server",
|
||||||
"preversion": "npm run lint",
|
"preversion": "npm run lint",
|
||||||
"postversion": "git push origin master --tags && npm publish",
|
"postversion": "git push origin master --tags && npm publish",
|
||||||
@ -20,11 +21,14 @@
|
|||||||
"major": "npm version major -m \"Tag v%s\""
|
"major": "npm version major -m \"Tag v%s\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"aws-sdk": "^2.133.0",
|
||||||
"bezier-easing": "^1.1.0",
|
"bezier-easing": "^1.1.0",
|
||||||
|
"body-parser": "^1.18.2",
|
||||||
"clipboard": "^1.7.1",
|
"clipboard": "^1.7.1",
|
||||||
"compression": "^1.7.0",
|
"compression": "^1.7.0",
|
||||||
"diff-match-patch": "^1.0.0",
|
"diff-match-patch": "^1.0.0",
|
||||||
"file-saver": "^1.3.3",
|
"file-saver": "^1.3.3",
|
||||||
|
"google-id-token-verifier": "^0.2.3",
|
||||||
"handlebars": "^4.0.10",
|
"handlebars": "^4.0.10",
|
||||||
"indexeddbshim": "^3.0.4",
|
"indexeddbshim": "^3.0.4",
|
||||||
"js-yaml": "^3.9.1",
|
"js-yaml": "^3.9.1",
|
||||||
@ -37,12 +41,13 @@
|
|||||||
"markdown-it-pandoc-renderer": "1.1.3",
|
"markdown-it-pandoc-renderer": "1.1.3",
|
||||||
"markdown-it-sub": "^1.0.0",
|
"markdown-it-sub": "^1.0.0",
|
||||||
"markdown-it-sup": "^1.0.0",
|
"markdown-it-sup": "^1.0.0",
|
||||||
|
"mermaid": "^7.1.0",
|
||||||
"mousetrap": "^1.6.1",
|
"mousetrap": "^1.6.1",
|
||||||
"normalize-scss": "^7.0.0",
|
"normalize-scss": "^7.0.0",
|
||||||
"prismjs": "^1.6.0",
|
"prismjs": "^1.6.0",
|
||||||
"raw-loader": "^0.5.1",
|
|
||||||
"request": "^2.82.0",
|
"request": "^2.82.0",
|
||||||
"serve-static": "^1.12.6",
|
"serve-static": "^1.12.6",
|
||||||
|
"tmp": "^0.0.33",
|
||||||
"vue": "^2.3.3",
|
"vue": "^2.3.3",
|
||||||
"vuex": "^2.3.1"
|
"vuex": "^2.3.1"
|
||||||
},
|
},
|
||||||
@ -59,7 +64,7 @@
|
|||||||
"chalk": "^1.1.3",
|
"chalk": "^1.1.3",
|
||||||
"connect-history-api-fallback": "^1.3.0",
|
"connect-history-api-fallback": "^1.3.0",
|
||||||
"copy-webpack-plugin": "^4.0.1",
|
"copy-webpack-plugin": "^4.0.1",
|
||||||
"css-loader": "^0.28.4",
|
"css-loader": "^0.28.7",
|
||||||
"eslint": "^3.19.0",
|
"eslint": "^3.19.0",
|
||||||
"eslint-config-airbnb-base": "^11.1.3",
|
"eslint-config-airbnb-base": "^11.1.3",
|
||||||
"eslint-friendly-formatter": "^2.0.7",
|
"eslint-friendly-formatter": "^2.0.7",
|
||||||
@ -70,6 +75,7 @@
|
|||||||
"eventsource-polyfill": "^0.9.6",
|
"eventsource-polyfill": "^0.9.6",
|
||||||
"express": "^4.15.5",
|
"express": "^4.15.5",
|
||||||
"extract-text-webpack-plugin": "^2.0.0",
|
"extract-text-webpack-plugin": "^2.0.0",
|
||||||
|
"favicons-webpack-plugin": "^0.0.7",
|
||||||
"file-loader": "^0.11.1",
|
"file-loader": "^0.11.1",
|
||||||
"friendly-errors-webpack-plugin": "^1.1.3",
|
"friendly-errors-webpack-plugin": "^1.1.3",
|
||||||
"gulp": "^3.9.1",
|
"gulp": "^3.9.1",
|
||||||
@ -83,6 +89,7 @@
|
|||||||
"opn": "^4.0.2",
|
"opn": "^4.0.2",
|
||||||
"optimize-css-assets-webpack-plugin": "^1.3.0",
|
"optimize-css-assets-webpack-plugin": "^1.3.0",
|
||||||
"ora": "^1.2.0",
|
"ora": "^1.2.0",
|
||||||
|
"raw-loader": "^0.5.1",
|
||||||
"rimraf": "^2.6.0",
|
"rimraf": "^2.6.0",
|
||||||
"sass-loader": "^6.0.5",
|
"sass-loader": "^6.0.5",
|
||||||
"semver": "^5.3.0",
|
"semver": "^5.3.0",
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
const compression = require('compression');
|
const compression = require('compression');
|
||||||
const serveStatic = require('serve-static');
|
const serveStatic = require('serve-static');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const user = require('./user');
|
||||||
const github = require('./github');
|
const github = require('./github');
|
||||||
|
const pdf = require('./pdf');
|
||||||
|
const pandoc = require('./pandoc');
|
||||||
|
|
||||||
|
const resolvePath = pathToResolve => path.join(__dirname, '..', pathToResolve);
|
||||||
|
|
||||||
module.exports = (app, serveV4) => {
|
module.exports = (app, serveV4) => {
|
||||||
// Use gzip compression
|
// Use gzip compression
|
||||||
@ -22,10 +28,20 @@ module.exports = (app, serveV4) => {
|
|||||||
app.use(compression());
|
app.use(compression());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse body mostly for PayPal IPN
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
app.use(bodyParser.urlencoded({
|
||||||
|
extended: false,
|
||||||
|
}));
|
||||||
|
|
||||||
app.get('/oauth2/githubToken', github.githubToken);
|
app.get('/oauth2/githubToken', github.githubToken);
|
||||||
|
app.get('/userInfo', user.userInfo);
|
||||||
|
app.post('/paypalIpn', user.paypalIpn);
|
||||||
|
app.post('/pdfExport', pdf.generate);
|
||||||
|
app.post('/pandocExport', pandoc.generate);
|
||||||
|
|
||||||
if (serveV4) {
|
if (serveV4) {
|
||||||
/* eslint-disable global-require, import/no-unresolved */
|
/* eslint-disable global-require, import/no-unresolved */
|
||||||
app.post('/pdfExport', require('../stackedit_v4/app/pdf').export);
|
|
||||||
app.post('/sshPublish', require('../stackedit_v4/app/ssh').publish);
|
app.post('/sshPublish', require('../stackedit_v4/app/ssh').publish);
|
||||||
app.post('/picasaImportImg', require('../stackedit_v4/app/picasa').importImg);
|
app.post('/picasaImportImg', require('../stackedit_v4/app/picasa').importImg);
|
||||||
app.get('/downloadImport', require('../stackedit_v4/app/download').importPublic);
|
app.get('/downloadImport', require('../stackedit_v4/app/download').importPublic);
|
||||||
@ -33,29 +49,39 @@ module.exports = (app, serveV4) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Serve callback.html in /app
|
// Serve callback.html in /app
|
||||||
app.get('/oauth2/callback', (req, res) => res.sendFile(path.join(__dirname, '../static/oauth2/callback.html')));
|
app.get('/oauth2/callback', (req, res) => res.sendFile(resolvePath('static/oauth2/callback.html')));
|
||||||
|
|
||||||
// Serve static resources
|
// Serve static resources
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
if (serveV4) {
|
if (serveV4) {
|
||||||
// Serve landing.html in /
|
// Serve landing.html in /
|
||||||
app.get('/', (req, res) => res.sendFile(require.resolve('../stackedit_v4/views/landing.html')));
|
app.get('/', (req, res) => res.sendFile(resolvePath('stackedit_v4/views/landing.html')));
|
||||||
// Serve editor.html in /viewer
|
// Serve editor.html in /viewer
|
||||||
app.get('/editor', (req, res) => res.sendFile(require.resolve('../stackedit_v4/views/editor.html')));
|
app.get('/editor', (req, res) => res.sendFile(resolvePath('stackedit_v4/views/editor.html')));
|
||||||
// Serve viewer.html in /viewer
|
// Serve viewer.html in /viewer
|
||||||
app.get('/viewer', (req, res) => res.sendFile(require.resolve('../stackedit_v4/views/viewer.html')));
|
app.get('/viewer', (req, res) => res.sendFile(resolvePath('stackedit_v4/views/viewer.html')));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve index.html in /app
|
// Serve index.html in /app
|
||||||
app.get('/app', (req, res) => res.sendFile(path.join(__dirname, '../dist/index.html')));
|
app.get('/app', (req, res) => res.sendFile(resolvePath('dist/index.html')));
|
||||||
|
|
||||||
app.use(serveStatic(path.join(__dirname, '../dist')));
|
// Serve style.css with 1 day max-age
|
||||||
|
app.get('/style.css', (req, res) => res.sendFile(resolvePath('dist/style.css'), {
|
||||||
|
maxAge: '1d',
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Serve the static folder with 1 year max-age
|
||||||
|
app.use('/static', serveStatic(resolvePath('dist/static'), {
|
||||||
|
maxAge: '1y',
|
||||||
|
}));
|
||||||
|
|
||||||
|
app.use(serveStatic(resolvePath('dist')));
|
||||||
|
|
||||||
if (serveV4) {
|
if (serveV4) {
|
||||||
app.use(serveStatic(path.dirname(require.resolve('../stackedit_v4/public/cache.manifest'))));
|
app.use(serveStatic(path.dirname(resolvePath('stackedit_v4/public/cache.manifest'))));
|
||||||
|
|
||||||
// Error 404
|
// Error 404
|
||||||
app.use((req, res) => res.status(404).sendFile(require.resolve('../stackedit_v4/views/error_404.html')));
|
app.use((req, res) => res.status(404).sendFile(resolvePath('stackedit_v4/views/error_404.html')));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
152
server/pandoc.js
Normal file
152
server/pandoc.js
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
/* global window */
|
||||||
|
const spawn = require('child_process').spawn;
|
||||||
|
const fs = require('fs');
|
||||||
|
const tmp = require('tmp');
|
||||||
|
const user = require('./user');
|
||||||
|
|
||||||
|
const outputFormats = {
|
||||||
|
asciidoc: 'text/plain',
|
||||||
|
context: 'application/x-latex',
|
||||||
|
epub: 'application/epub+zip',
|
||||||
|
epub3: 'application/epub+zip',
|
||||||
|
latex: 'application/x-latex',
|
||||||
|
odt: 'application/vnd.oasis.opendocument.text',
|
||||||
|
pdf: 'application/pdf',
|
||||||
|
rst: 'text/plain',
|
||||||
|
rtf: 'application/rtf',
|
||||||
|
textile: 'text/plain',
|
||||||
|
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
};
|
||||||
|
|
||||||
|
const highlightStyles = [
|
||||||
|
'pygments',
|
||||||
|
'kate',
|
||||||
|
'monochrome',
|
||||||
|
'espresso',
|
||||||
|
'zenburn',
|
||||||
|
'haddock',
|
||||||
|
'tango',
|
||||||
|
];
|
||||||
|
|
||||||
|
const readJson = (str) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(str);
|
||||||
|
} catch (e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.generate = (req, res) => {
|
||||||
|
let pandocError = '';
|
||||||
|
const outputFormat = Object.prototype.hasOwnProperty.call(outputFormats, req.query.format)
|
||||||
|
? req.query.format
|
||||||
|
: 'pdf';
|
||||||
|
Promise.all([
|
||||||
|
user.checkSponsor(req.query.idToken),
|
||||||
|
user.checkMonetize(req.query.token),
|
||||||
|
])
|
||||||
|
.then(([isSponsor, isMonetize]) => {
|
||||||
|
if (!isSponsor && !isMonetize) {
|
||||||
|
throw new Error('unauthorized');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
tmp.file({
|
||||||
|
postfix: `.${outputFormat}`,
|
||||||
|
}, (err, filePath, fd, cleanupCallback) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
filePath,
|
||||||
|
cleanupCallback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(({ filePath, cleanupCallback }) => new Promise((resolve, reject) => {
|
||||||
|
const options = readJson(req.query.options);
|
||||||
|
const metadata = readJson(req.query.metadata);
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
params.push('--latex-engine=xelatex');
|
||||||
|
params.push('--webtex=http://chart.apis.google.com/chart?cht=tx&chf=bg,s,FFFFFF00&chco=000000&chl=');
|
||||||
|
if (options.toc) {
|
||||||
|
params.push('--toc');
|
||||||
|
}
|
||||||
|
options.tocDepth = parseInt(options.tocDepth, 10);
|
||||||
|
if (!isNaN(options.tocDepth)) {
|
||||||
|
params.push('--toc-depth', options.tocDepth);
|
||||||
|
}
|
||||||
|
options.highlightStyle = highlightStyles.indexOf(options.highlightStyle) !== -1 ? options.highlightStyle : 'kate';
|
||||||
|
params.push('--highlight-style', options.highlightStyle);
|
||||||
|
Object.keys(metadata).forEach((key) => {
|
||||||
|
params.push('-M', `${key}=${metadata[key]}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
let finished = false;
|
||||||
|
|
||||||
|
function onError(error) {
|
||||||
|
finished = true;
|
||||||
|
cleanupCallback();
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const binPath = process.env.PANDOC_PATH || 'pandoc';
|
||||||
|
const format = outputFormat === 'pdf' ? 'latex' : outputFormat;
|
||||||
|
params.push('-f', 'json', '-t', format, '-o', filePath);
|
||||||
|
const pandoc = spawn(binPath, params, {
|
||||||
|
stdio: [
|
||||||
|
'pipe',
|
||||||
|
'ignore',
|
||||||
|
'pipe',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
let timeoutId = setTimeout(() => {
|
||||||
|
timeoutId = null;
|
||||||
|
pandoc.kill();
|
||||||
|
}, 50000);
|
||||||
|
pandoc.on('error', onError);
|
||||||
|
pandoc.stdin.on('error', onError);
|
||||||
|
pandoc.stderr.on('data', (data) => {
|
||||||
|
pandocError += `${data}`;
|
||||||
|
});
|
||||||
|
pandoc.on('close', (code) => {
|
||||||
|
if (!finished) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
if (!timeoutId) {
|
||||||
|
res.statusCode = 408;
|
||||||
|
cleanupCallback();
|
||||||
|
reject(new Error('timeout'));
|
||||||
|
} else if (code) {
|
||||||
|
cleanupCallback();
|
||||||
|
reject();
|
||||||
|
} else {
|
||||||
|
res.set('Content-Type', outputFormats[outputFormat]);
|
||||||
|
const readStream = fs.createReadStream(filePath);
|
||||||
|
readStream.on('open', () => readStream.pipe(res));
|
||||||
|
readStream.on('close', () => cleanupCallback());
|
||||||
|
readStream.on('error', () => {
|
||||||
|
cleanupCallback();
|
||||||
|
reject();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
req.pipe(pandoc.stdin);
|
||||||
|
}))
|
||||||
|
.catch((err) => {
|
||||||
|
const message = err && err.message;
|
||||||
|
if (message === 'unauthorized') {
|
||||||
|
res.statusCode = 401;
|
||||||
|
res.end('Unauthorized.');
|
||||||
|
} else if (message === 'timeout') {
|
||||||
|
res.statusCode = 408;
|
||||||
|
res.end('Request timeout.');
|
||||||
|
} else {
|
||||||
|
res.statusCode = 400;
|
||||||
|
res.end(pandocError || 'Unknown error.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
188
server/pdf.js
Normal file
188
server/pdf.js
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
/* global window,MathJax */
|
||||||
|
const spawn = require('child_process').spawn;
|
||||||
|
const fs = require('fs');
|
||||||
|
const tmp = require('tmp');
|
||||||
|
const user = require('./user');
|
||||||
|
|
||||||
|
/* eslint-disable no-var, prefer-arrow-callback, func-names */
|
||||||
|
function waitForJavaScript() {
|
||||||
|
if (window.MathJax) {
|
||||||
|
// Amazon EC2: fix TeX font detection
|
||||||
|
MathJax.Hub.Register.StartupHook('HTML-CSS Jax Startup', function () {
|
||||||
|
var htmlCss = MathJax.OutputJax['HTML-CSS'];
|
||||||
|
htmlCss.Font.checkWebFont = function (check, font, callback) {
|
||||||
|
if (check.time(callback)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (check.total === 0) {
|
||||||
|
htmlCss.Font.testFont(font);
|
||||||
|
setTimeout(check, 200);
|
||||||
|
} else {
|
||||||
|
callback(check.STATUS.OK);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
MathJax.Hub.Queue(function () {
|
||||||
|
window.status = 'done';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setTimeout(function () {
|
||||||
|
window.status = 'done';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* eslint-disable no-var, prefer-arrow-callback, func-names */
|
||||||
|
|
||||||
|
const authorizedPageSizes = [
|
||||||
|
'A3',
|
||||||
|
'A4',
|
||||||
|
'Legal',
|
||||||
|
'Letter',
|
||||||
|
];
|
||||||
|
|
||||||
|
const readJson = (str) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(str);
|
||||||
|
} catch (e) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.generate = (req, res) => {
|
||||||
|
let wkhtmltopdfError = '';
|
||||||
|
Promise.all([
|
||||||
|
user.checkSponsor(req.query.idToken),
|
||||||
|
user.checkMonetize(req.query.token),
|
||||||
|
])
|
||||||
|
.then(([isSponsor, isMonetize]) => {
|
||||||
|
if (!isSponsor && !isMonetize) {
|
||||||
|
throw new Error('unauthorized');
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
tmp.file((err, filePath, fd, cleanupCallback) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
filePath,
|
||||||
|
cleanupCallback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(({ filePath, cleanupCallback }) => new Promise((resolve, reject) => {
|
||||||
|
let finished = false;
|
||||||
|
|
||||||
|
function onError(err) {
|
||||||
|
finished = true;
|
||||||
|
cleanupCallback();
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
const options = readJson(req.query.options);
|
||||||
|
const params = [];
|
||||||
|
|
||||||
|
// Margins
|
||||||
|
const marginTop = parseInt(`${options.marginTop}`, 10);
|
||||||
|
params.push('-T', isNaN(marginTop) ? 25 : marginTop);
|
||||||
|
const marginRight = parseInt(`${options.marginRight}`, 10);
|
||||||
|
params.push('-R', isNaN(marginRight) ? 25 : marginRight);
|
||||||
|
const marginBottom = parseInt(`${options.marginBottom}`, 10);
|
||||||
|
params.push('-B', isNaN(marginBottom) ? 25 : marginBottom);
|
||||||
|
const marginLeft = parseInt(`${options.marginLeft}`, 10);
|
||||||
|
params.push('-L', isNaN(marginLeft) ? 25 : marginLeft);
|
||||||
|
|
||||||
|
// Header
|
||||||
|
if (options.headerCenter) {
|
||||||
|
params.push('--header-center', `${options.headerCenter}`);
|
||||||
|
}
|
||||||
|
if (options.headerLeft) {
|
||||||
|
params.push('--header-left', `${options.headerLeft}`);
|
||||||
|
}
|
||||||
|
if (options.headerRight) {
|
||||||
|
params.push('--header-right', `${options.headerRight}`);
|
||||||
|
}
|
||||||
|
if (options.headerFontName) {
|
||||||
|
params.push('--header-font-name', `${options.headerFontName}`);
|
||||||
|
}
|
||||||
|
if (options.headerFontSize) {
|
||||||
|
params.push('--header-font-size', `${options.headerFontSize}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
if (options.footerCenter) {
|
||||||
|
params.push('--footer-center', `${options.footerCenter}`);
|
||||||
|
}
|
||||||
|
if (options.footerLeft) {
|
||||||
|
params.push('--footer-left', `${options.footerLeft}`);
|
||||||
|
}
|
||||||
|
if (options.footerRight) {
|
||||||
|
params.push('--footer-right', `${options.footerRight}`);
|
||||||
|
}
|
||||||
|
if (options.footerFontName) {
|
||||||
|
params.push('--footer-font-name', `${options.footerFontName}`);
|
||||||
|
}
|
||||||
|
if (options.footerFontSize) {
|
||||||
|
params.push('--footer-font-size', `${options.footerFontSize}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page size
|
||||||
|
params.push('--page-size', authorizedPageSizes.indexOf(options.pageSize) === -1 ? 'A4' : options.pageSize);
|
||||||
|
|
||||||
|
// Use a temp file as wkhtmltopdf can't access /dev/stdout on Amazon EC2 for some reason
|
||||||
|
const binPath = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf';
|
||||||
|
params.push('--run-script', `${waitForJavaScript.toString()}waitForJavaScript()`);
|
||||||
|
params.push('--window-status', 'done');
|
||||||
|
const wkhtmltopdf = spawn(binPath, params.concat('-', filePath), {
|
||||||
|
stdio: [
|
||||||
|
'pipe',
|
||||||
|
'ignore',
|
||||||
|
'pipe',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
let timeoutId = setTimeout(function () {
|
||||||
|
timeoutId = null;
|
||||||
|
wkhtmltopdf.kill();
|
||||||
|
}, 50000);
|
||||||
|
wkhtmltopdf.on('error', onError);
|
||||||
|
wkhtmltopdf.stdin.on('error', onError);
|
||||||
|
wkhtmltopdf.stderr.on('data', (data) => {
|
||||||
|
wkhtmltopdfError += `${data}`;
|
||||||
|
});
|
||||||
|
wkhtmltopdf.on('close', (code) => {
|
||||||
|
if (!finished) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
if (!timeoutId) {
|
||||||
|
cleanupCallback();
|
||||||
|
reject(new Error('timeout'));
|
||||||
|
} else if (code) {
|
||||||
|
cleanupCallback();
|
||||||
|
reject();
|
||||||
|
} else {
|
||||||
|
res.set('Content-Type', 'application/pdf');
|
||||||
|
const readStream = fs.createReadStream(filePath);
|
||||||
|
readStream.on('open', () => readStream.pipe(res));
|
||||||
|
readStream.on('close', () => cleanupCallback());
|
||||||
|
readStream.on('error', () => {
|
||||||
|
cleanupCallback();
|
||||||
|
reject();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
req.pipe(wkhtmltopdf.stdin);
|
||||||
|
}))
|
||||||
|
.catch((err) => {
|
||||||
|
const message = err && err.message;
|
||||||
|
if (message === 'unauthorized') {
|
||||||
|
res.statusCode = 401;
|
||||||
|
res.end('Unauthorized.');
|
||||||
|
} else if (message === 'timeout') {
|
||||||
|
res.statusCode = 408;
|
||||||
|
res.end('Request timeout.');
|
||||||
|
} else {
|
||||||
|
res.statusCode = 400;
|
||||||
|
res.end(wkhtmltopdfError || 'Unknown error.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
129
server/user.js
Normal file
129
server/user.js
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
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 = '241271498917-t4t7d07qis7oc0ahaskbif3ft6tk63cd.apps.googleusercontent.com';
|
||||||
|
const s3Client = new AWS.S3();
|
||||||
|
|
||||||
|
const cb = (resolve, reject) => (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.getUser = id => new Promise((resolve, reject) => {
|
||||||
|
s3Client.getObject({
|
||||||
|
Bucket: BUCKET_NAME,
|
||||||
|
Key: id,
|
||||||
|
}, cb(resolve, reject));
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
res => JSON.parse(`${res.Body}`),
|
||||||
|
(err) => {
|
||||||
|
if (err.code !== 'NoSuchKey') {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
exports.putUser = (id, user) => new Promise((resolve, reject) => {
|
||||||
|
s3Client.putObject({
|
||||||
|
Bucket: BUCKET_NAME,
|
||||||
|
Key: id,
|
||||||
|
Body: JSON.stringify(user),
|
||||||
|
}, cb(resolve, reject));
|
||||||
|
});
|
||||||
|
|
||||||
|
exports.removeUser = id => new Promise((resolve, reject) => {
|
||||||
|
s3Client.deleteObject({
|
||||||
|
Bucket: BUCKET_NAME,
|
||||||
|
Key: id,
|
||||||
|
}, cb(resolve, reject));
|
||||||
|
});
|
||||||
|
|
||||||
|
exports.getUserFromToken = idToken => new Promise(
|
||||||
|
(resolve, reject) => verifier.verify(idToken, GOOGLE_CLIENT_ID, cb(resolve, reject)))
|
||||||
|
.then(tokenInfo => exports.getUser(tokenInfo.sub));
|
||||||
|
|
||||||
|
exports.userInfo = (req, res) => exports.getUserFromToken(req.query.idToken)
|
||||||
|
.then(user => res.send(Object.assign({
|
||||||
|
sponsorUntil: 0,
|
||||||
|
}, user)),
|
||||||
|
err => res.status(400).send(err ? err.message || err.toString() : 'invalid_token'));
|
||||||
|
|
||||||
|
exports.paypalIpn = (req, res, next) => Promise.resolve()
|
||||||
|
.then(() => {
|
||||||
|
const userId = req.body.custom;
|
||||||
|
const paypalEmail = req.body.payer_email;
|
||||||
|
const gross = parseFloat(req.body.mc_gross);
|
||||||
|
let sponsorUntil;
|
||||||
|
if (gross === 5) {
|
||||||
|
sponsorUntil = Date.now() + (3 * 31 * 24 * 60 * 60 * 1000); // 3 months
|
||||||
|
} else if (gross === 15) {
|
||||||
|
sponsorUntil = Date.now() + (366 * 24 * 60 * 60 * 1000); // 1 year
|
||||||
|
} else if (gross === 25) {
|
||||||
|
sponsorUntil = Date.now() + (2 * 366 * 24 * 60 * 60 * 1000); // 2 years
|
||||||
|
} else if (gross === 50) {
|
||||||
|
sponsorUntil = Date.now() + (5 * 366 * 24 * 60 * 60 * 1000); // 5 years
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
req.body.receiver_email !== PAYPAL_RECEIVER_EMAIL ||
|
||||||
|
req.body.payment_status !== 'Completed' ||
|
||||||
|
req.body.mc_currency !== 'USD' ||
|
||||||
|
(req.body.txn_type !== 'web_accept' && req.body.txn_type !== 'subscr_payment') ||
|
||||||
|
!userId || !sponsorUntil
|
||||||
|
) {
|
||||||
|
// Ignoring PayPal IPN
|
||||||
|
return res.end();
|
||||||
|
}
|
||||||
|
// Processing PayPal IPN
|
||||||
|
req.body.cmd = '_notify-validate';
|
||||||
|
return new Promise((resolve, reject) => request.post({
|
||||||
|
uri: PAYPAL_URI,
|
||||||
|
form: req.body,
|
||||||
|
}, (err, response, body) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else if (body !== 'VERIFIED') {
|
||||||
|
reject(new Error('PayPal IPN unverified'));
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.then(() => exports.putUser(userId, {
|
||||||
|
paypalEmail,
|
||||||
|
sponsorUntil,
|
||||||
|
}))
|
||||||
|
.then(() => res.end());
|
||||||
|
})
|
||||||
|
.catch(next);
|
||||||
|
|
||||||
|
exports.checkSponsor = (idToken) => {
|
||||||
|
if (!idToken) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
return exports.getUserFromToken(idToken)
|
||||||
|
.then(userInfo => userInfo && userInfo.sponsorUntil > Date.now(), () => false);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.checkMonetize = (token) => {
|
||||||
|
if (!token) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
return new Promise(resolve => request({
|
||||||
|
uri: 'https://monetizejs.com/api/payments',
|
||||||
|
qs: {
|
||||||
|
access_token: token,
|
||||||
|
},
|
||||||
|
json: true,
|
||||||
|
}, (err, paymentsRes, payments) => {
|
||||||
|
const authorized = payments && payments.app === 'ESTHdCYOi18iLhhO' && (
|
||||||
|
(payments.chargeOption && payments.chargeOption.alias === 'once') ||
|
||||||
|
(payments.subscriptionOption && payments.subscriptionOption.alias === 'yearly'));
|
||||||
|
resolve(!err && paymentsRes.statusCode === 200 && authorized);
|
||||||
|
}));
|
||||||
|
};
|
BIN
src/assets/favicon.png
Normal file
BIN
src/assets/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 79 KiB |
@ -1,26 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="button-bar">
|
<div class="button-bar">
|
||||||
<div class="button-bar__inner button-bar__inner--top">
|
<div class="button-bar__inner button-bar__inner--top">
|
||||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showNavigationBar }" @click="toggleNavigationBar()" v-title="'Toggle navigation bar'">
|
<button class="button-bar__button button" :class="{ 'button-bar__button--on': localSettings.showNavigationBar }" @click="toggleNavigationBar()" v-title="'Toggle navigation bar'">
|
||||||
<icon-navigation-bar></icon-navigation-bar>
|
<icon-navigation-bar></icon-navigation-bar>
|
||||||
</div>
|
</button>
|
||||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showSidePreview }" @click="toggleSidePreview()" v-title="'Toggle side preview'">
|
<button class="button-bar__button button" :class="{ 'button-bar__button--on': localSettings.showSidePreview }" @click="toggleSidePreview()" v-title="'Toggle side preview'">
|
||||||
<icon-side-preview></icon-side-preview>
|
<icon-side-preview></icon-side-preview>
|
||||||
</div>
|
</button>
|
||||||
<div class="button-bar__button" @click="toggleEditor(false)" v-title="'Reader mode'">
|
<button class="button-bar__button button" @click="toggleEditor(false)" v-title="'Reader mode'">
|
||||||
<icon-eye></icon-eye>
|
<icon-eye></icon-eye>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-bar__inner button-bar__inner--bottom">
|
<div class="button-bar__inner button-bar__inner--bottom">
|
||||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.focusMode }" @click="toggleFocusMode()" v-title="'Toggle focus mode'">
|
<button class="button-bar__button button" :class="{ 'button-bar__button--on': localSettings.focusMode }" @click="toggleFocusMode()" v-title="'Toggle focus mode'">
|
||||||
<icon-target></icon-target>
|
<icon-target></icon-target>
|
||||||
</div>
|
</button>
|
||||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.scrollSync }" @click="toggleScrollSync()" v-title="'Toggle scroll sync'">
|
<button class="button-bar__button button" :class="{ 'button-bar__button--on': localSettings.scrollSync }" @click="toggleScrollSync()" v-title="'Toggle scroll sync'">
|
||||||
<icon-scroll-sync></icon-scroll-sync>
|
<icon-scroll-sync></icon-scroll-sync>
|
||||||
</div>
|
</button>
|
||||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showStatusBar }" @click="toggleStatusBar()" v-title="'Toggle status bar'">
|
<button class="button-bar__button button" :class="{ 'button-bar__button--on': localSettings.showStatusBar }" @click="toggleStatusBar()" v-title="'Toggle status bar'">
|
||||||
<icon-status-bar></icon-status-bar>
|
<icon-status-bar></icon-status-bar>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -60,19 +60,27 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.button-bar__button {
|
.button-bar__button {
|
||||||
cursor: pointer;
|
display: block;
|
||||||
color: rgba(0, 0, 0, 0.2);
|
color: rgba(0, 0, 0, 0.2);
|
||||||
width: 26px;
|
width: 26px;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
margin: 3px 0;
|
margin: 3px 0;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
color: rgba(0, 0, 0, 0.4);
|
color: rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-bar__button--on {
|
.button-bar__button--on {
|
||||||
color: rgba(0, 0, 0, 0.4);
|
color: rgba(0, 0, 0, 0.4);
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
color: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -35,7 +35,6 @@ export default {
|
|||||||
font-family: $font-family-monospace;
|
font-family: $font-family-monospace;
|
||||||
font-size: $font-size-monospace;
|
font-size: $font-size-monospace;
|
||||||
font-variant-ligatures: no-common-ligatures;
|
font-variant-ligatures: no-common-ligatures;
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
word-wrap: normal;
|
word-wrap: normal;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
@ -22,7 +22,7 @@ import { mapMutations, mapActions } from 'vuex';
|
|||||||
import utils from '../services/utils';
|
import utils from '../services/utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'explorer-node',
|
name: 'explorer-node', // Required for recursivity
|
||||||
props: ['node', 'depth'],
|
props: ['node', 'depth'],
|
||||||
data: () => ({
|
data: () => ({
|
||||||
editingValue: '',
|
editingValue: '',
|
||||||
|
359
src/components/FindReplace.vue
Normal file
359
src/components/FindReplace.vue
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
<template>
|
||||||
|
<div class="find-replace" @keyup.esc="onEscape">
|
||||||
|
<button class="find-replace__close-button button not-tabbable" @click="close()" v-title="'Close'">
|
||||||
|
<icon-close></icon-close>
|
||||||
|
</button>
|
||||||
|
<div class="find-replace__row">
|
||||||
|
<input type="text" class="find-replace__text-input find-replace__text-input--find text-input" @keyup.enter="find('forward')" v-model="findText">
|
||||||
|
<div class="find-replace__find-stats">
|
||||||
|
{{findPosition}} of {{findCount}}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex--row flex--space-between">
|
||||||
|
<div class="flex flex--row">
|
||||||
|
<button class="find-replace__button find-replace__button--find-option button" :class="{'find-replace__button--on': findCaseSensitive}" @click="findCaseSensitive = !findCaseSensitive" title="Case sensitive">Aa</button>
|
||||||
|
<button class="find-replace__button find-replace__button--find-option button" :class="{'find-replace__button--on': findUseRegexp}" @click="findUseRegexp = !findUseRegexp" title="Regular expression">.<sup>⁕</sup></button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex--row">
|
||||||
|
<button class="find-replace__button button" @click="find('backward')">Previous</button>
|
||||||
|
<button class="find-replace__button button" @click="find('forward')">Next</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="type === 'replace'">
|
||||||
|
<div class="find-replace__row">
|
||||||
|
<input type="text" class="find-replace__text-input find-replace__text-input--replace text-input" @keyup.enter="replace" v-model="replaceText">
|
||||||
|
</div>
|
||||||
|
<div class="find-replace__row flex flex--row flex--end">
|
||||||
|
<button class="find-replace__button button" @click="replace">Replace</button>
|
||||||
|
<button class="find-replace__button button" @click="replaceAll">All</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { mapState } from 'vuex';
|
||||||
|
import editorEngineSvc from '../services/editorEngineSvc';
|
||||||
|
import cledit from '../libs/cledit';
|
||||||
|
import store from '../store';
|
||||||
|
import EditorClassApplier from './common/EditorClassApplier';
|
||||||
|
|
||||||
|
const accessor = (fieldName, setterName) => ({
|
||||||
|
get() {
|
||||||
|
return store.state.findReplace[fieldName];
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
store.commit(`findReplace/${setterName}`, value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const computedLocalSetting = key => ({
|
||||||
|
get() {
|
||||||
|
return store.getters['data/localSettings'][key];
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
store.dispatch('data/patchLocalSettings', {
|
||||||
|
[key]: value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
class DynamicClassApplier {
|
||||||
|
constructor(cssClass, offset, silent) {
|
||||||
|
this.startMarker = new cledit.Marker(offset.start);
|
||||||
|
this.endMarker = new cledit.Marker(offset.end);
|
||||||
|
editorEngineSvc.clEditor.addMarker(this.startMarker);
|
||||||
|
editorEngineSvc.clEditor.addMarker(this.endMarker);
|
||||||
|
if (!silent) {
|
||||||
|
this.classApplier = new EditorClassApplier(
|
||||||
|
[`find-replace-${this.startMarker.id}`, cssClass],
|
||||||
|
() => ({
|
||||||
|
start: this.startMarker.offset,
|
||||||
|
end: this.endMarker.offset,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clean = () => {
|
||||||
|
editorEngineSvc.clEditor.removeMarker(this.startMarker);
|
||||||
|
editorEngineSvc.clEditor.removeMarker(this.endMarker);
|
||||||
|
if (this.classApplier) {
|
||||||
|
this.classApplier.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
findCount: 0,
|
||||||
|
findPosition: 0,
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapState('findReplace', [
|
||||||
|
'type',
|
||||||
|
'lastOpen',
|
||||||
|
]),
|
||||||
|
findText: accessor('findText', 'setFindText'),
|
||||||
|
replaceText: accessor('replaceText', 'setReplaceText'),
|
||||||
|
findCaseSensitive: computedLocalSetting('findCaseSensitive'),
|
||||||
|
findUseRegexp: computedLocalSetting('findUseRegexp'),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
highlightOccurrences() {
|
||||||
|
const oldClassAppliers = {};
|
||||||
|
Object.keys(this.classAppliers).forEach((key) => {
|
||||||
|
const classApplier = this.classAppliers[key];
|
||||||
|
const newKey = `${classApplier.startMarker.offset}:${classApplier.endMarker.offset}`;
|
||||||
|
oldClassAppliers[newKey] = classApplier;
|
||||||
|
});
|
||||||
|
const offsetList = [];
|
||||||
|
this.classAppliers = {};
|
||||||
|
if (this.state !== 'destroyed' && this.findText) {
|
||||||
|
try {
|
||||||
|
this.searchRegex = this.findText;
|
||||||
|
if (!this.findUseRegexp) {
|
||||||
|
this.searchRegex = this.searchRegex.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
|
||||||
|
}
|
||||||
|
this.replaceRegex = new RegExp(this.searchRegex, this.findCaseSensitive ? 'm' : 'mi');
|
||||||
|
this.searchRegex = new RegExp(this.searchRegex, this.findCaseSensitive ? 'gm' : 'gmi');
|
||||||
|
editorEngineSvc.clEditor.getContent().replace(this.searchRegex, (...params) => {
|
||||||
|
const match = params[0];
|
||||||
|
const offset = params[params.length - 2];
|
||||||
|
offsetList.push({
|
||||||
|
start: offset,
|
||||||
|
end: offset + match.length,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
offsetList.forEach((offset, i) => {
|
||||||
|
const key = `${offset.start}:${offset.end}`;
|
||||||
|
this.classAppliers[key] = oldClassAppliers[key] || new DynamicClassApplier(
|
||||||
|
'find-replace-highlighting', offset, i > 200);
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
if (this.state !== 'created') {
|
||||||
|
this.find('selection');
|
||||||
|
this.state = 'created';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object.keys(oldClassAppliers).forEach((key) => {
|
||||||
|
const classApplier = oldClassAppliers[key];
|
||||||
|
if (!this.classAppliers[key]) {
|
||||||
|
classApplier.clean();
|
||||||
|
if (classApplier === this.selectedClassApplier) {
|
||||||
|
this.selectedClassApplier.child.clean();
|
||||||
|
this.selectedClassApplier = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.findCount = offsetList.length;
|
||||||
|
},
|
||||||
|
unselectClassApplier() {
|
||||||
|
if (this.selectedClassApplier) {
|
||||||
|
this.selectedClassApplier.child.clean();
|
||||||
|
this.selectedClassApplier.child = null;
|
||||||
|
this.selectedClassApplier = null;
|
||||||
|
}
|
||||||
|
this.findPosition = 0;
|
||||||
|
},
|
||||||
|
find(mode = 'forward') {
|
||||||
|
const selectedClassApplier = this.selectedClassApplier;
|
||||||
|
this.unselectClassApplier();
|
||||||
|
const selectionMgr = editorEngineSvc.clEditor.selectionMgr;
|
||||||
|
const startOffset = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd);
|
||||||
|
const endOffset = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd);
|
||||||
|
const keys = Object.keys(this.classAppliers);
|
||||||
|
const finder = checker => (key) => {
|
||||||
|
if (checker(this.classAppliers[key]) && selectedClassApplier !== this.classAppliers[key]) {
|
||||||
|
this.selectedClassApplier = this.classAppliers[key];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (mode === 'backward') {
|
||||||
|
this.selectedClassApplier = this.classAppliers[keys[keys.length - 1]];
|
||||||
|
keys.reverse().some(finder(classApplier => classApplier.startMarker.offset <= startOffset));
|
||||||
|
} else if (mode === 'selection') {
|
||||||
|
keys.some(finder(classApplier => classApplier.startMarker.offset === startOffset &&
|
||||||
|
classApplier.endMarker.offset === endOffset));
|
||||||
|
} else if (mode === 'forward') {
|
||||||
|
this.selectedClassApplier = this.classAppliers[keys[0]];
|
||||||
|
keys.some(finder(classApplier => classApplier.endMarker.offset >= endOffset));
|
||||||
|
}
|
||||||
|
if (this.selectedClassApplier) {
|
||||||
|
selectionMgr.setSelectionStartEnd(
|
||||||
|
this.selectedClassApplier.startMarker.offset,
|
||||||
|
this.selectedClassApplier.endMarker.offset,
|
||||||
|
);
|
||||||
|
this.selectedClassApplier.child = new DynamicClassApplier('find-replace-selection', {
|
||||||
|
start: this.selectedClassApplier.startMarker.offset,
|
||||||
|
end: this.selectedClassApplier.endMarker.offset,
|
||||||
|
});
|
||||||
|
selectionMgr.updateCursorCoordinates(this.$el.parentNode.clientHeight);
|
||||||
|
// Deduce the findPosition
|
||||||
|
Object.keys(this.classAppliers).forEach((key, i) => {
|
||||||
|
if (this.selectedClassApplier !== this.classAppliers[key]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.findPosition = i + 1;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
replace() {
|
||||||
|
if (this.searchRegex) {
|
||||||
|
if (!this.selectedClassApplier) {
|
||||||
|
this.find();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editorEngineSvc.clEditor.replaceAll(
|
||||||
|
this.replaceRegex, this.replaceText, this.selectedClassApplier.startMarker.offset);
|
||||||
|
Vue.nextTick(() => this.find());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
replaceAll() {
|
||||||
|
if (this.searchRegex) {
|
||||||
|
editorEngineSvc.clEditor.replaceAll(this.searchRegex, this.replaceText);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.$store.commit('findReplace/setType');
|
||||||
|
},
|
||||||
|
onEscape() {
|
||||||
|
editorEngineSvc.clEditor.focus();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.classAppliers = {};
|
||||||
|
|
||||||
|
// Highlight occurences
|
||||||
|
this.debouncedHighlightOccurrences = cledit.Utils.debounce(
|
||||||
|
() => this.highlightOccurrences(), 25);
|
||||||
|
// Refresh highlighting when find text changes or changing options
|
||||||
|
this.$watch(() => this.findText, this.debouncedHighlightOccurrences);
|
||||||
|
this.$watch(() => this.findCaseSensitive, this.debouncedHighlightOccurrences);
|
||||||
|
this.$watch(() => this.findUseRegexp, this.debouncedHighlightOccurrences);
|
||||||
|
// Refresh highlighting when content changes
|
||||||
|
editorEngineSvc.clEditor.on('contentChanged', this.debouncedHighlightOccurrences);
|
||||||
|
|
||||||
|
// Last open changes trigger focus on text input and find occurence in selection
|
||||||
|
this.$watch(() => this.lastOpen, () => {
|
||||||
|
const elt = this.$el.querySelector(`.find-replace__text-input--${this.type}`);
|
||||||
|
elt.focus();
|
||||||
|
elt.setSelectionRange(0, this[`${this.type}Text`].length);
|
||||||
|
// Highlight and find in selection
|
||||||
|
this.state = null;
|
||||||
|
this.debouncedHighlightOccurrences();
|
||||||
|
}, {
|
||||||
|
immediate: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close on escape
|
||||||
|
this.onKeyup = (evt) => {
|
||||||
|
if (evt.which === 27) {
|
||||||
|
// Esc key
|
||||||
|
this.$store.commit('findReplace/setType');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keyup', this.onKeyup);
|
||||||
|
|
||||||
|
// Unselect class applier when focus is out of the panel
|
||||||
|
this.onFocusIn = () => this.$el.contains(document.activeElement) ||
|
||||||
|
setTimeout(() => this.unselectClassApplier(), 15);
|
||||||
|
window.addEventListener('focusin', this.onFocusIn);
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
// Unregister listeners
|
||||||
|
editorEngineSvc.clEditor.off('contentChanged', this.debouncedHighlightOccurrences);
|
||||||
|
window.removeEventListener('keyup', this.onKeyup);
|
||||||
|
window.removeEventListener('focusin', this.onFocusIn);
|
||||||
|
this.state = 'destroyed';
|
||||||
|
this.debouncedHighlightOccurrences();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import 'common/variables.scss';
|
||||||
|
|
||||||
|
.find-replace {
|
||||||
|
padding: 0 35px 0 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-replace__row {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-replace__button {
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 0 8px;
|
||||||
|
line-height: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-replace__button--find-option {
|
||||||
|
padding: 0;
|
||||||
|
width: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-replace__button--on {
|
||||||
|
color: rgba(0, 0, 0, 0.67);
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
color: rgba(0, 0, 0, 0.67);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-replace__text-input {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 2px 5px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: $link-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-replace__close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
color: rgba(0, 0, 0, 0.25);
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
padding: 2px;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
color: rgba(0, 0, 0, 0.33);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-replace__find-stats {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 0.75em;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-replace-highlighting {
|
||||||
|
background-color: #ff0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.find-replace-selection {
|
||||||
|
background-color: #ff9632;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<div class="layout__panel flex flex--row" :class="{'flex--end': styles.showSideBar}">
|
<div class="layout__panel flex flex--row" :class="{'flex--end': styles.showSideBar}">
|
||||||
<div class="layout__panel layout__panel--explorer" v-show="styles.showExplorer" :aria-hidden="!styles.showExplorer" :style="{ width: constants.explorerWidth + 'px' }">
|
<div class="layout__panel layout__panel--explorer" v-show="styles.showExplorer" :aria-hidden="!styles.showExplorer" :style="{ width: styles.layoutOverflow ? '100%' : constants.explorerWidth + 'px' }">
|
||||||
<explorer></explorer>
|
<explorer></explorer>
|
||||||
</div>
|
</div>
|
||||||
<div class="layout__panel flex flex--column" :style="{ width: styles.innerWidth + 'px' }">
|
<div class="layout__panel flex flex--column" :style="{ width: styles.innerWidth + 'px' }">
|
||||||
@ -11,6 +11,9 @@
|
|||||||
<div class="layout__panel flex flex--row" :style="{ height: styles.innerHeight + 'px' }">
|
<div class="layout__panel flex flex--row" :style="{ height: styles.innerHeight + 'px' }">
|
||||||
<div class="layout__panel layout__panel--editor" v-show="styles.showEditor" :style="{ width: styles.editorWidth + 'px', 'font-size': styles.fontSize + 'px' }">
|
<div class="layout__panel layout__panel--editor" v-show="styles.showEditor" :style="{ width: styles.editorWidth + 'px', 'font-size': styles.fontSize + 'px' }">
|
||||||
<editor></editor>
|
<editor></editor>
|
||||||
|
<div v-if="showFindReplace" class="layout__panel layout__panel--find-replace">
|
||||||
|
<find-replace></find-replace>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="layout__panel layout__panel--button-bar" v-show="styles.showEditor" :style="{ width: constants.buttonBarWidth + 'px' }">
|
<div class="layout__panel layout__panel--button-bar" v-show="styles.showEditor" :style="{ width: constants.buttonBarWidth + 'px' }">
|
||||||
<button-bar></button-bar>
|
<button-bar></button-bar>
|
||||||
@ -23,7 +26,7 @@
|
|||||||
<status-bar></status-bar>
|
<status-bar></status-bar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="layout__panel layout__panel--side-bar" v-show="styles.showSideBar" :style="{ width: constants.sideBarWidth + 'px' }">
|
<div class="layout__panel layout__panel--side-bar" v-show="styles.showSideBar" :style="{ width: styles.layoutOverflow ? '100%' : constants.sideBarWidth + 'px' }">
|
||||||
<side-bar></side-bar>
|
<side-bar></side-bar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -39,7 +42,9 @@ import Explorer from './Explorer';
|
|||||||
import SideBar from './SideBar';
|
import SideBar from './SideBar';
|
||||||
import Editor from './Editor';
|
import Editor from './Editor';
|
||||||
import Preview from './Preview';
|
import Preview from './Preview';
|
||||||
|
import FindReplace from './FindReplace';
|
||||||
import editorSvc from '../services/editorSvc';
|
import editorSvc from '../services/editorSvc';
|
||||||
|
import editorEngineSvc from '../services/editorEngineSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -50,12 +55,16 @@ export default {
|
|||||||
SideBar,
|
SideBar,
|
||||||
Editor,
|
Editor,
|
||||||
Preview,
|
Preview,
|
||||||
|
FindReplace,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('layout', [
|
...mapGetters('layout', [
|
||||||
'constants',
|
'constants',
|
||||||
'styles',
|
'styles',
|
||||||
]),
|
]),
|
||||||
|
showFindReplace() {
|
||||||
|
return !!this.$store.state.findReplace.type;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations('layout', [
|
...mapMutations('layout', [
|
||||||
@ -75,6 +84,10 @@ export default {
|
|||||||
const previewElt = this.$el.querySelector('.preview__inner-2');
|
const previewElt = this.$el.querySelector('.preview__inner-2');
|
||||||
const tocElt = this.$el.querySelector('.toc__inner');
|
const tocElt = this.$el.querySelector('.toc__inner');
|
||||||
editorSvc.init(editorElt, previewElt, tocElt);
|
editorSvc.init(editorElt, previewElt, tocElt);
|
||||||
|
|
||||||
|
// Focus on the editor every time reader mode is disabled
|
||||||
|
this.$watch(() => this.styles.showEditor,
|
||||||
|
showEditor => showEditor && editorEngineSvc.clEditor.focus());
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
window.removeEventListener('resize', this.updateStyle);
|
window.removeEventListener('resize', this.updateStyle);
|
||||||
@ -122,4 +135,14 @@ export default {
|
|||||||
.layout__panel--side-bar {
|
.layout__panel--side-bar {
|
||||||
background-color: #dadada;
|
background-color: #dadada;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.layout__panel--find-replace {
|
||||||
|
background-color: #e6e6e6;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 300px;
|
||||||
|
height: auto;
|
||||||
|
border-top-right-radius: $border-radius-base;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -5,18 +5,23 @@
|
|||||||
<templates-modal v-else-if="config.type === 'templates'"></templates-modal>
|
<templates-modal v-else-if="config.type === 'templates'"></templates-modal>
|
||||||
<about-modal v-else-if="config.type === 'about'"></about-modal>
|
<about-modal v-else-if="config.type === 'about'"></about-modal>
|
||||||
<html-export-modal v-else-if="config.type === 'htmlExport'"></html-export-modal>
|
<html-export-modal v-else-if="config.type === 'htmlExport'"></html-export-modal>
|
||||||
|
<pdf-export-modal v-else-if="config.type === 'pdfExport'"></pdf-export-modal>
|
||||||
|
<pandoc-export-modal v-else-if="config.type === 'pandocExport'"></pandoc-export-modal>
|
||||||
<link-modal v-else-if="config.type === 'link'"></link-modal>
|
<link-modal v-else-if="config.type === 'link'"></link-modal>
|
||||||
<image-modal v-else-if="config.type === 'image'"></image-modal>
|
<image-modal v-else-if="config.type === 'image'"></image-modal>
|
||||||
<google-photo-modal v-else-if="config.type === 'googlePhoto'"></google-photo-modal>
|
|
||||||
<sync-management-modal v-else-if="config.type === 'syncManagement'"></sync-management-modal>
|
<sync-management-modal v-else-if="config.type === 'syncManagement'"></sync-management-modal>
|
||||||
<publish-management-modal v-else-if="config.type === 'publishManagement'"></publish-management-modal>
|
<publish-management-modal v-else-if="config.type === 'publishManagement'"></publish-management-modal>
|
||||||
<google-drive-sync-modal v-else-if="config.type === 'googleDriveSync'"></google-drive-sync-modal>
|
<sponsor-modal v-else-if="config.type === 'sponsor'"></sponsor-modal>
|
||||||
|
<!-- Providers -->
|
||||||
|
<google-photo-modal v-else-if="config.type === 'googlePhoto'"></google-photo-modal>
|
||||||
|
<google-drive-save-modal v-else-if="config.type === 'googleDriveSave'"></google-drive-save-modal>
|
||||||
<google-drive-publish-modal v-else-if="config.type === 'googleDrivePublish'"></google-drive-publish-modal>
|
<google-drive-publish-modal v-else-if="config.type === 'googleDrivePublish'"></google-drive-publish-modal>
|
||||||
<dropbox-account-modal v-else-if="config.type === 'dropboxAccount'"></dropbox-account-modal>
|
<dropbox-account-modal v-else-if="config.type === 'dropboxAccount'"></dropbox-account-modal>
|
||||||
<dropbox-sync-modal v-else-if="config.type === 'dropboxSync'"></dropbox-sync-modal>
|
<dropbox-save-modal v-else-if="config.type === 'dropboxSave'"></dropbox-save-modal>
|
||||||
<dropbox-publish-modal v-else-if="config.type === 'dropboxPublish'"></dropbox-publish-modal>
|
<dropbox-publish-modal v-else-if="config.type === 'dropboxPublish'"></dropbox-publish-modal>
|
||||||
<github-account-modal v-else-if="config.type === 'githubAccount'"></github-account-modal>
|
<github-account-modal v-else-if="config.type === 'githubAccount'"></github-account-modal>
|
||||||
<github-sync-modal v-else-if="config.type === 'githubSync'"></github-sync-modal>
|
<github-open-modal v-else-if="config.type === 'githubOpen'"></github-open-modal>
|
||||||
|
<github-save-modal v-else-if="config.type === 'githubSave'"></github-save-modal>
|
||||||
<github-publish-modal v-else-if="config.type === 'githubPublish'"></github-publish-modal>
|
<github-publish-modal v-else-if="config.type === 'githubPublish'"></github-publish-modal>
|
||||||
<gist-sync-modal v-else-if="config.type === 'gistSync'"></gist-sync-modal>
|
<gist-sync-modal v-else-if="config.type === 'gistSync'"></gist-sync-modal>
|
||||||
<gist-publish-modal v-else-if="config.type === 'gistPublish'"></gist-publish-modal>
|
<gist-publish-modal v-else-if="config.type === 'gistPublish'"></gist-publish-modal>
|
||||||
@ -25,70 +30,81 @@
|
|||||||
<blogger-page-publish-modal v-else-if="config.type === 'bloggerPagePublish'"></blogger-page-publish-modal>
|
<blogger-page-publish-modal v-else-if="config.type === 'bloggerPagePublish'"></blogger-page-publish-modal>
|
||||||
<zendesk-account-modal v-else-if="config.type === 'zendeskAccount'"></zendesk-account-modal>
|
<zendesk-account-modal v-else-if="config.type === 'zendeskAccount'"></zendesk-account-modal>
|
||||||
<zendesk-publish-modal v-else-if="config.type === 'zendeskPublish'"></zendesk-publish-modal>
|
<zendesk-publish-modal v-else-if="config.type === 'zendeskPublish'"></zendesk-publish-modal>
|
||||||
<div v-else class="modal__inner-1">
|
<modal-inner v-else aria-label="Dialog">
|
||||||
<div class="modal__inner-2">
|
|
||||||
<div class="modal__content" v-html="config.content"></div>
|
<div class="modal__content" v-html="config.content"></div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button v-if="config.rejectText" class="button" @click="config.reject()">{{config.rejectText}}</button>
|
<button v-if="config.rejectText" class="button" @click="config.reject()">{{config.rejectText}}</button>
|
||||||
<button v-if="config.resolveText" class="button" @click="config.resolve()">{{config.resolveText}}</button>
|
<button v-if="config.resolveText" class="button" @click="config.resolve()">{{config.resolveText}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import editorEngineSvc from '../services/editorEngineSvc';
|
import editorEngineSvc from '../services/editorEngineSvc';
|
||||||
|
import ModalInner from './modals/common/ModalInner';
|
||||||
import FilePropertiesModal from './modals/FilePropertiesModal';
|
import FilePropertiesModal from './modals/FilePropertiesModal';
|
||||||
import SettingsModal from './modals/SettingsModal';
|
import SettingsModal from './modals/SettingsModal';
|
||||||
import TemplatesModal from './modals/TemplatesModal';
|
import TemplatesModal from './modals/TemplatesModal';
|
||||||
import AboutModal from './modals/AboutModal';
|
import AboutModal from './modals/AboutModal';
|
||||||
import HtmlExportModal from './modals/HtmlExportModal';
|
import HtmlExportModal from './modals/HtmlExportModal';
|
||||||
|
import PdfExportModal from './modals/PdfExportModal';
|
||||||
|
import PandocExportModal from './modals/PandocExportModal';
|
||||||
import LinkModal from './modals/LinkModal';
|
import LinkModal from './modals/LinkModal';
|
||||||
import ImageModal from './modals/ImageModal';
|
import ImageModal from './modals/ImageModal';
|
||||||
import GooglePhotoModal from './modals/GooglePhotoModal';
|
|
||||||
import SyncManagementModal from './modals/SyncManagementModal';
|
import SyncManagementModal from './modals/SyncManagementModal';
|
||||||
import PublishManagementModal from './modals/PublishManagementModal';
|
import PublishManagementModal from './modals/PublishManagementModal';
|
||||||
import GoogleDriveSyncModal from './modals/GoogleDriveSyncModal';
|
import SponsorModal from './modals/SponsorModal';
|
||||||
import GoogleDrivePublishModal from './modals/GoogleDrivePublishModal';
|
|
||||||
import DropboxAccountModal from './modals/DropboxAccountModal';
|
// Providers
|
||||||
import DropboxSyncModal from './modals/DropboxSyncModal';
|
import GooglePhotoModal from './modals/providers/GooglePhotoModal';
|
||||||
import DropboxPublishModal from './modals/DropboxPublishModal';
|
import GoogleDriveSaveModal from './modals/providers/GoogleDriveSaveModal';
|
||||||
import GithubAccountModal from './modals/GithubAccountModal';
|
import GoogleDrivePublishModal from './modals/providers/GoogleDrivePublishModal';
|
||||||
import GithubSyncModal from './modals/GithubSyncModal';
|
import DropboxAccountModal from './modals/providers/DropboxAccountModal';
|
||||||
import GithubPublishModal from './modals/GithubPublishModal';
|
import DropboxSaveModal from './modals/providers/DropboxSaveModal';
|
||||||
import GistSyncModal from './modals/GistSyncModal';
|
import DropboxPublishModal from './modals/providers/DropboxPublishModal';
|
||||||
import GistPublishModal from './modals/GistPublishModal';
|
import GithubAccountModal from './modals/providers/GithubAccountModal';
|
||||||
import WordpressPublishModal from './modals/WordpressPublishModal';
|
import GithubOpenModal from './modals/providers/GithubOpenModal';
|
||||||
import BloggerPublishModal from './modals/BloggerPublishModal';
|
import GithubSaveModal from './modals/providers/GithubSaveModal';
|
||||||
import BloggerPagePublishModal from './modals/BloggerPagePublishModal';
|
import GithubPublishModal from './modals/providers/GithubPublishModal';
|
||||||
import ZendeskAccountModal from './modals/ZendeskAccountModal';
|
import GistSyncModal from './modals/providers/GistSyncModal';
|
||||||
import ZendeskPublishModal from './modals/ZendeskPublishModal';
|
import GistPublishModal from './modals/providers/GistPublishModal';
|
||||||
|
import WordpressPublishModal from './modals/providers/WordpressPublishModal';
|
||||||
|
import BloggerPublishModal from './modals/providers/BloggerPublishModal';
|
||||||
|
import BloggerPagePublishModal from './modals/providers/BloggerPagePublishModal';
|
||||||
|
import ZendeskAccountModal from './modals/providers/ZendeskAccountModal';
|
||||||
|
import ZendeskPublishModal from './modals/providers/ZendeskPublishModal';
|
||||||
|
|
||||||
const getTabbables = container => container.querySelectorAll('a[href], button, .textfield')
|
const getTabbables = container => container.querySelectorAll('a[href], button, .textfield')
|
||||||
// Filter enabled and visible element
|
// Filter enabled and visible element
|
||||||
.cl_filter(el => !el.disabled && el.offsetParent !== null);
|
.cl_filter(el => !el.disabled && el.offsetParent !== null && !el.classList.contains('not-tabbable'));
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
ModalInner,
|
||||||
FilePropertiesModal,
|
FilePropertiesModal,
|
||||||
SettingsModal,
|
SettingsModal,
|
||||||
TemplatesModal,
|
TemplatesModal,
|
||||||
AboutModal,
|
AboutModal,
|
||||||
HtmlExportModal,
|
HtmlExportModal,
|
||||||
|
PdfExportModal,
|
||||||
|
PandocExportModal,
|
||||||
LinkModal,
|
LinkModal,
|
||||||
ImageModal,
|
ImageModal,
|
||||||
GooglePhotoModal,
|
|
||||||
SyncManagementModal,
|
SyncManagementModal,
|
||||||
PublishManagementModal,
|
PublishManagementModal,
|
||||||
GoogleDriveSyncModal,
|
SponsorModal,
|
||||||
|
// Providers
|
||||||
|
GooglePhotoModal,
|
||||||
|
GoogleDriveSaveModal,
|
||||||
GoogleDrivePublishModal,
|
GoogleDrivePublishModal,
|
||||||
DropboxAccountModal,
|
DropboxAccountModal,
|
||||||
DropboxSyncModal,
|
DropboxSaveModal,
|
||||||
DropboxPublishModal,
|
DropboxPublishModal,
|
||||||
GithubAccountModal,
|
GithubAccountModal,
|
||||||
GithubSyncModal,
|
GithubOpenModal,
|
||||||
|
GithubSaveModal,
|
||||||
GithubPublishModal,
|
GithubPublishModal,
|
||||||
GistSyncModal,
|
GistSyncModal,
|
||||||
GistPublishModal,
|
GistPublishModal,
|
||||||
@ -160,7 +176,7 @@ export default {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: rgba(128, 128, 128, 0.5);
|
background-color: rgba(160, 160, 160, 0.5);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,7 +188,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal__inner-2 {
|
.modal__inner-2 {
|
||||||
margin: 50px 10px 100px;
|
margin: 40px 10px 100px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
padding: 40px 50px 30px;
|
padding: 40px 50px 30px;
|
||||||
border-radius: $border-radius-base;
|
border-radius: $border-radius-base;
|
||||||
@ -200,7 +216,8 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal__content :first-child {
|
.modal__content > :first-child,
|
||||||
|
.modal__content > .modal__image:first-child + * {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,7 +326,7 @@ export default {
|
|||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
border-bottom: 1px solid $hr-color;
|
border-bottom: 1px solid $hr-color;
|
||||||
margin-bottom: 2em;
|
margin: 1em 0 2em;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor}">
|
<nav class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor}">
|
||||||
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
|
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
|
||||||
<button class="navigation-bar__button button" @click="toggleExplorer()" v-title="'Toggle explorer'">
|
<button class="navigation-bar__button button" @click="toggleExplorer()" v-title="'Toggle explorer'">
|
||||||
<icon-folder></icon-folder>
|
<icon-folder></icon-folder>
|
||||||
@ -78,7 +78,7 @@
|
|||||||
<icon-format-horizontal-rule></icon-format-horizontal-rule>
|
<icon-format-horizontal-rule></icon-format-horizontal-rule>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -31,7 +31,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapActions } from 'vuex';
|
import { mapActions } from 'vuex';
|
||||||
import Toc from './Toc';
|
import Toc from './Toc';
|
||||||
import MenuEntry from './menus/MenuEntry';
|
|
||||||
import MainMenu from './menus/MainMenu';
|
import MainMenu from './menus/MainMenu';
|
||||||
import SyncMenu from './menus/SyncMenu';
|
import SyncMenu from './menus/SyncMenu';
|
||||||
import PublishMenu from './menus/PublishMenu';
|
import PublishMenu from './menus/PublishMenu';
|
||||||
@ -53,7 +52,6 @@ const panelNames = {
|
|||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Toc,
|
Toc,
|
||||||
MenuEntry,
|
|
||||||
MainMenu,
|
MainMenu,
|
||||||
SyncMenu,
|
SyncMenu,
|
||||||
PublishMenu,
|
PublishMenu,
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<div class="stat-panel__block stat-panel__block--left" v-if="styles.showEditor">
|
<div class="stat-panel__block stat-panel__block--left" v-if="styles.showEditor">
|
||||||
<span class="stat-panel__block-name">
|
<span class="stat-panel__block-name">
|
||||||
Markdown
|
Markdown
|
||||||
<small v-if="textSelection">(selection)</small>
|
<span v-if="textSelection">selection</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-for="stat in textStats" :key="stat.id">
|
<span v-for="stat in textStats" :key="stat.id">
|
||||||
<span class="stat-panel__value">{{stat.value}}</span> {{stat.name}}
|
<span class="stat-panel__value">{{stat.value}}</span> {{stat.name}}
|
||||||
@ -13,7 +13,7 @@
|
|||||||
<div class="stat-panel__block stat-panel__block--right">
|
<div class="stat-panel__block stat-panel__block--right">
|
||||||
<span class="stat-panel__block-name">
|
<span class="stat-panel__block-name">
|
||||||
HTML
|
HTML
|
||||||
<small v-if="htmlSelection">(selection)</small>
|
<span v-if="htmlSelection">selection</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-for="stat in htmlStats" :key="stat.id">
|
<span v-for="stat in htmlStats" :key="stat.id">
|
||||||
<span class="stat-panel__value">{{stat.value}}</span> {{stat.name}}
|
<span class="stat-panel__value">{{stat.value}}</span> {{stat.name}}
|
||||||
@ -123,10 +123,6 @@ export default {
|
|||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-panel__block-name {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-panel__value {
|
.stat-panel__value {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
|
@ -105,29 +105,39 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cl-toc-section {
|
.cl-toc-section {
|
||||||
* {
|
h1,
|
||||||
margin: 0.2em 0;
|
h2 {
|
||||||
padding: 0.2em 0;
|
&::after {
|
||||||
border-bottom: 0;
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
|
margin: 0.5rem 0;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
|
margin: 0.33rem 0;
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
|
margin: 0.22rem 0;
|
||||||
margin-left: 24px;
|
margin-left: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h5 {
|
h5 {
|
||||||
|
margin: 0.11rem 0;
|
||||||
margin-left: 32px;
|
margin-left: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h6 {
|
h6 {
|
||||||
|
margin: 0;
|
||||||
margin-left: 40px;
|
margin-left: 40px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
85
src/components/common/EditorClassApplier.js
Normal file
85
src/components/common/EditorClassApplier.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import cledit from '../../libs/cledit';
|
||||||
|
import editorSvc from '../../services/editorSvc';
|
||||||
|
import editorEngineSvc from '../../services/editorEngineSvc';
|
||||||
|
import utils from '../../services/utils';
|
||||||
|
|
||||||
|
let savedSelection;
|
||||||
|
const nextTickCbs = [];
|
||||||
|
const nextTickExecCbs = cledit.Utils.debounce(() => {
|
||||||
|
while (nextTickCbs.length) {
|
||||||
|
nextTickCbs.shift()();
|
||||||
|
}
|
||||||
|
if (savedSelection) {
|
||||||
|
editorEngineSvc.clEditor.selectionMgr.setSelectionStartEnd(
|
||||||
|
savedSelection.start, savedSelection.end);
|
||||||
|
}
|
||||||
|
savedSelection = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextTick = (cb) => {
|
||||||
|
nextTickCbs.push(cb);
|
||||||
|
nextTickExecCbs();
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextTickRestoreSelection = () => {
|
||||||
|
savedSelection = {
|
||||||
|
start: editorEngineSvc.clEditor.selectionMgr.selectionStart,
|
||||||
|
end: editorEngineSvc.clEditor.selectionMgr.selectionEnd,
|
||||||
|
};
|
||||||
|
nextTickExecCbs();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class EditorClassApplier {
|
||||||
|
constructor(classGetter, offsetGetter, properties) {
|
||||||
|
this.classGetter = typeof classGetter === 'function' ? classGetter : () => classGetter;
|
||||||
|
this.offsetGetter = typeof offsetGetter === 'function' ? offsetGetter : () => offsetGetter;
|
||||||
|
this.properties = properties || {};
|
||||||
|
this.eltCollection = editorSvc.editorElt.getElementsByClassName(this.classGetter()[0]);
|
||||||
|
this.lastEltCount = this.eltCollection.length;
|
||||||
|
|
||||||
|
this.restoreClass = () => {
|
||||||
|
if (!this.eltCollection.length || this.eltCollection.length !== this.lastEltCount) {
|
||||||
|
this.removeClass();
|
||||||
|
this.applyClass();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
editorEngineSvc.clEditor.on('contentChanged', this.restoreClass);
|
||||||
|
nextTick(() => this.applyClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
applyClass() {
|
||||||
|
const offset = this.offsetGetter();
|
||||||
|
if (offset && offset.start !== offset.end) {
|
||||||
|
const range = editorEngineSvc.clEditor.selectionMgr.createRange(
|
||||||
|
Math.min(offset.start, offset.end),
|
||||||
|
Math.max(offset.start, offset.end),
|
||||||
|
);
|
||||||
|
const properties = {
|
||||||
|
...this.properties,
|
||||||
|
className: this.classGetter().join(' '),
|
||||||
|
};
|
||||||
|
editorEngineSvc.clEditor.watcher.noWatch(() => {
|
||||||
|
utils.wrapRange(range, properties);
|
||||||
|
});
|
||||||
|
if (editorEngineSvc.clEditor.selectionMgr.hasFocus()) {
|
||||||
|
nextTickRestoreSelection();
|
||||||
|
}
|
||||||
|
this.lastEltCount = this.eltCollection.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeClass() {
|
||||||
|
editorEngineSvc.clEditor.watcher.noWatch(() => {
|
||||||
|
utils.unwrapRange(this.eltCollection);
|
||||||
|
});
|
||||||
|
if (editorEngineSvc.clEditor.selectionMgr.hasFocus()) {
|
||||||
|
nextTickRestoreSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
editorEngineSvc.clEditor.off('contentChanged', this.restoreClass);
|
||||||
|
nextTick(() => this.removeClass());
|
||||||
|
}
|
||||||
|
}
|
@ -30,6 +30,16 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pre > code {
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
input,
|
input,
|
||||||
select,
|
select,
|
||||||
@ -78,6 +88,7 @@ textarea {
|
|||||||
background-image: none;
|
background-image: none;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: $border-radius-base;
|
border-radius: $border-radius-base;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
@ -195,6 +206,12 @@ textarea {
|
|||||||
background-size: contain;
|
background-size: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden-rendering-container {
|
||||||
|
position: absolute;
|
||||||
|
width: 500px;
|
||||||
|
left: -1000px;
|
||||||
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
body {
|
body {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
@import '../../node_modules/normalize-scss/sass/normalize';
|
@import '../../../node_modules/normalize-scss/sass/normalize';
|
||||||
@import '../../node_modules/katex/dist/katex.css';
|
@import '../../node_modules/katex/dist/katex.css';
|
||||||
@import './variables.scss';
|
@import './variables.scss';
|
||||||
@import './fonts.scss';
|
@import './fonts.scss';
|
||||||
@import './prism';
|
@import './prism';
|
||||||
|
@import './mermaid';
|
||||||
|
|
||||||
@include normalize();
|
@include normalize();
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
color: rgba(0, 0, 0, 0.75);
|
color: rgba(0, 0, 0, 0.75);
|
||||||
|
font-size: 16px;
|
||||||
font-family: $font-family-main;
|
font-family: $font-family-main;
|
||||||
font-variant-ligatures: common-ligatures;
|
font-variant-ligatures: common-ligatures;
|
||||||
line-height: $line-height-base;
|
line-height: $line-height-base;
|
||||||
@ -16,12 +18,6 @@ body {
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
|
||||||
h2,
|
|
||||||
h3,
|
|
||||||
h4,
|
|
||||||
h5,
|
|
||||||
h6,
|
|
||||||
p,
|
p,
|
||||||
blockquote,
|
blockquote,
|
||||||
pre,
|
pre,
|
||||||
@ -37,26 +33,19 @@ h3,
|
|||||||
h4,
|
h4,
|
||||||
h5,
|
h5,
|
||||||
h6 {
|
h6 {
|
||||||
margin: 1.8em 0 1.2em;
|
margin: 1.8em 0;
|
||||||
line-height: $line-height-title;
|
line-height: $line-height-title;
|
||||||
padding: 0.33em 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
h2 {
|
h2 {
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
top: 0.33em;
|
||||||
border-bottom: 1px solid $hr-color;
|
border-bottom: 1px solid $hr-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 1.22em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ol ul,
|
ol ul,
|
||||||
@ -68,11 +57,12 @@ ol ol {
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
color: $link-color;
|
color: $link-color;
|
||||||
text-decoration: none;
|
text-decoration: underline;
|
||||||
|
text-decoration-skip: ink;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
text-decoration: underline;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,8 +79,8 @@ samp {
|
|||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
color: rgba(0, 0, 0, 0.5);
|
color: rgba(0, 0, 0, 0.5);
|
||||||
margin-left: 1em;
|
padding-left: 1.5em;
|
||||||
margin-right: 1em;
|
border-left: 5px solid rgba(0, 0, 0, 0.075);
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
@ -108,9 +98,9 @@ hr {
|
|||||||
pre > code {
|
pre > code {
|
||||||
background-color: $code-bg;
|
background-color: $code-bg;
|
||||||
display: block;
|
display: block;
|
||||||
overflow-x: auto;
|
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
-webkit-text-size-adjust: none;
|
-webkit-text-size-adjust: none;
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toc ul {
|
.toc ul {
|
||||||
@ -126,7 +116,7 @@ table {
|
|||||||
|
|
||||||
td,
|
td,
|
||||||
th {
|
th {
|
||||||
border-right: 1px solid #e0e0e0;
|
border-right: 1px solid #dcdcdc;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
@ -135,7 +125,7 @@ th {
|
|||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
border-top: 1px solid #e0e0e0;
|
border-top: 1px solid #dcdcdc;
|
||||||
}
|
}
|
||||||
|
|
||||||
kbd {
|
kbd {
|
||||||
@ -163,11 +153,6 @@ img {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-wrapper {
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footnote {
|
.footnote {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -175,18 +160,93 @@ img {
|
|||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.export-container {
|
.stackedit__html {
|
||||||
margin-bottom: 180px;
|
margin-bottom: 180px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
padding-left: 30px;
|
||||||
|
padding-right: 30px;
|
||||||
|
|
||||||
> :not(h1):not(h2):not(h3):not(h4):not(h5):not(h6):not([align]) {
|
@media (min-width: 810px) {
|
||||||
text-align: justify;
|
.stackedit__html {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.export-container {
|
|
||||||
width: 750px;
|
width: 750px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stackedit__toc {
|
||||||
|
ul {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
a {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.9em;
|
||||||
|
|
||||||
|
a {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.1rem 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.075);
|
||||||
|
border-radius: $border-radius-base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stackedit__left {
|
||||||
|
position: fixed;
|
||||||
|
display: none;
|
||||||
|
width: 250px;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
|
||||||
|
@media (min-width: 1060px) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stackedit__right {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
@media (min-width: 1060px) {
|
||||||
|
left: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stackedit--pdf {
|
||||||
|
blockquote {
|
||||||
|
// wkhtmltopdf doesn't like borders with transparency
|
||||||
|
border-left-color: #ececec;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stackedit__html {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
359
src/components/common/mermaid.scss
Normal file
359
src/components/common/mermaid.scss
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
/* stylelint-disable color-hex-case, color-hex-length, rule-empty-line-before, comment-empty-line-before */
|
||||||
|
.mermaid {
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: rgba(0, 0, 0, 0.75);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: $font-family-main;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .label {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.node rect,
|
||||||
|
.node circle,
|
||||||
|
.node ellipse,
|
||||||
|
.node polygon {
|
||||||
|
fill: #eee;
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 1px;
|
||||||
|
}
|
||||||
|
.edgePath .path {
|
||||||
|
stroke: #666;
|
||||||
|
stroke-width: 1.5px;
|
||||||
|
}
|
||||||
|
.edgeLabel {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
.cluster rect {
|
||||||
|
fill: #eaf2fb !important;
|
||||||
|
rx: 4 !important;
|
||||||
|
stroke: #26a !important;
|
||||||
|
stroke-width: 1px !important;
|
||||||
|
}
|
||||||
|
.cluster text {
|
||||||
|
fill: #333;
|
||||||
|
}
|
||||||
|
.actor {
|
||||||
|
stroke: #999;
|
||||||
|
fill: #eee;
|
||||||
|
}
|
||||||
|
text.actor {
|
||||||
|
fill: #333;
|
||||||
|
stroke: none;
|
||||||
|
}
|
||||||
|
.actor-line {
|
||||||
|
stroke: #666;
|
||||||
|
}
|
||||||
|
.messageLine0 {
|
||||||
|
stroke-width: 1.5;
|
||||||
|
stroke-dasharray: "2 2";
|
||||||
|
marker-end: "url(#arrowhead)";
|
||||||
|
stroke: #333;
|
||||||
|
}
|
||||||
|
.messageLine1 {
|
||||||
|
stroke-width: 1.5;
|
||||||
|
stroke-dasharray: "2 2";
|
||||||
|
stroke: #333;
|
||||||
|
}
|
||||||
|
#arrowhead {
|
||||||
|
fill: #333;
|
||||||
|
}
|
||||||
|
#crosshead path {
|
||||||
|
fill: #333 !important;
|
||||||
|
stroke: #333 !important;
|
||||||
|
}
|
||||||
|
.messageText {
|
||||||
|
fill: #333;
|
||||||
|
stroke: none;
|
||||||
|
}
|
||||||
|
.labelBox {
|
||||||
|
stroke: #999;
|
||||||
|
fill: #eee;
|
||||||
|
}
|
||||||
|
.labelText {
|
||||||
|
fill: white;
|
||||||
|
stroke: none;
|
||||||
|
}
|
||||||
|
.loopText {
|
||||||
|
fill: white;
|
||||||
|
stroke: none;
|
||||||
|
}
|
||||||
|
.loopLine {
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-dasharray: "2 2";
|
||||||
|
marker-end: "url(#arrowhead)";
|
||||||
|
stroke: #999;
|
||||||
|
}
|
||||||
|
.note {
|
||||||
|
stroke: #777700;
|
||||||
|
fill: #ffa;
|
||||||
|
}
|
||||||
|
.noteText {
|
||||||
|
fill: black;
|
||||||
|
stroke: none;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
/** Section styling */
|
||||||
|
.section {
|
||||||
|
stroke: none;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
.section0 {
|
||||||
|
fill: #7fb2e6;
|
||||||
|
}
|
||||||
|
.section2 {
|
||||||
|
fill: #7fb2e6;
|
||||||
|
}
|
||||||
|
.section1,
|
||||||
|
.section3 {
|
||||||
|
fill: white;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
.sectionTitle0 {
|
||||||
|
fill: #333;
|
||||||
|
}
|
||||||
|
.sectionTitle1 {
|
||||||
|
fill: #333;
|
||||||
|
}
|
||||||
|
.sectionTitle2 {
|
||||||
|
fill: #333;
|
||||||
|
}
|
||||||
|
.sectionTitle3 {
|
||||||
|
fill: #333;
|
||||||
|
}
|
||||||
|
.sectionTitle {
|
||||||
|
text-anchor: start;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
/* Grid and axis */
|
||||||
|
.grid .tick {
|
||||||
|
stroke: #e5e5e5;
|
||||||
|
opacity: 0.3;
|
||||||
|
shape-rendering: crispEdges;
|
||||||
|
}
|
||||||
|
.grid path {
|
||||||
|
stroke-width: 0;
|
||||||
|
}
|
||||||
|
/* Today line */
|
||||||
|
.today {
|
||||||
|
fill: none;
|
||||||
|
stroke: #d42;
|
||||||
|
stroke-width: 2px;
|
||||||
|
}
|
||||||
|
/* Task styling */
|
||||||
|
/* Default task */
|
||||||
|
.task {
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
.taskText {
|
||||||
|
text-anchor: middle;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.taskTextOutsideRight {
|
||||||
|
fill: #333;
|
||||||
|
text-anchor: start;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.taskTextOutsideLeft {
|
||||||
|
fill: #333;
|
||||||
|
text-anchor: end;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
/* Specific task settings for the sections */
|
||||||
|
.taskText0,
|
||||||
|
.taskText1,
|
||||||
|
.taskText2,
|
||||||
|
.taskText3 {
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
.task0,
|
||||||
|
.task1,
|
||||||
|
.task2,
|
||||||
|
.task3 {
|
||||||
|
fill: #26a;
|
||||||
|
stroke: #194c7f;
|
||||||
|
}
|
||||||
|
.taskTextOutside0,
|
||||||
|
.taskTextOutside2 {
|
||||||
|
fill: #333;
|
||||||
|
}
|
||||||
|
.taskTextOutside1,
|
||||||
|
.taskTextOutside3 {
|
||||||
|
fill: #333;
|
||||||
|
}
|
||||||
|
/* Active task */
|
||||||
|
.active0,
|
||||||
|
.active1,
|
||||||
|
.active2,
|
||||||
|
.active3 {
|
||||||
|
fill: #eee;
|
||||||
|
stroke: #194c7f;
|
||||||
|
}
|
||||||
|
.activeText0,
|
||||||
|
.activeText1,
|
||||||
|
.activeText2,
|
||||||
|
.activeText3 {
|
||||||
|
fill: #333 !important;
|
||||||
|
}
|
||||||
|
/* Completed task */
|
||||||
|
.done0,
|
||||||
|
.done1,
|
||||||
|
.done2,
|
||||||
|
.done3 {
|
||||||
|
stroke: #666;
|
||||||
|
fill: #bbb;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
.doneText0,
|
||||||
|
.doneText1,
|
||||||
|
.doneText2,
|
||||||
|
.doneText3 {
|
||||||
|
fill: #333 !important;
|
||||||
|
}
|
||||||
|
/* Tasks on the critical line */
|
||||||
|
.crit0,
|
||||||
|
.crit1,
|
||||||
|
.crit2,
|
||||||
|
.crit3 {
|
||||||
|
stroke: #b1361b;
|
||||||
|
fill: #d42;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
.activeCrit0,
|
||||||
|
.activeCrit1,
|
||||||
|
.activeCrit2,
|
||||||
|
.activeCrit3 {
|
||||||
|
stroke: #b1361b;
|
||||||
|
fill: #eee;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
.doneCrit0,
|
||||||
|
.doneCrit1,
|
||||||
|
.doneCrit2,
|
||||||
|
.doneCrit3 {
|
||||||
|
stroke: #b1361b;
|
||||||
|
fill: #bbb;
|
||||||
|
stroke-width: 2;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.doneCritText0,
|
||||||
|
.doneCritText1,
|
||||||
|
.doneCritText2,
|
||||||
|
.doneCritText3 {
|
||||||
|
fill: #333 !important;
|
||||||
|
}
|
||||||
|
.activeCritText0,
|
||||||
|
.activeCritText1,
|
||||||
|
.activeCritText2,
|
||||||
|
.activeCritText3 {
|
||||||
|
fill: #333 !important;
|
||||||
|
}
|
||||||
|
.titleText {
|
||||||
|
text-anchor: middle;
|
||||||
|
font-size: 18px;
|
||||||
|
fill: #333;
|
||||||
|
}
|
||||||
|
g.classGroup text {
|
||||||
|
fill: #999;
|
||||||
|
stroke: none;
|
||||||
|
font-family: 'trebuchet ms', verdana, arial;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
g.classGroup rect {
|
||||||
|
fill: #eee;
|
||||||
|
stroke: #999;
|
||||||
|
}
|
||||||
|
g.classGroup line {
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
svg .classLabel .box {
|
||||||
|
stroke: none;
|
||||||
|
stroke-width: 0;
|
||||||
|
fill: #eee;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
svg .classLabel .label {
|
||||||
|
fill: #999;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.relation {
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 1;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
.composition {
|
||||||
|
fill: #999;
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
#compositionStart {
|
||||||
|
fill: #999;
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
#compositionEnd {
|
||||||
|
fill: #999;
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
.aggregation {
|
||||||
|
fill: #eee;
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
#aggregationStart {
|
||||||
|
fill: #eee;
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
#aggregationEnd {
|
||||||
|
fill: #eee;
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
#dependencyStart {
|
||||||
|
fill: #999;
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
#dependencyEnd {
|
||||||
|
fill: #999;
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
#extensionStart {
|
||||||
|
fill: #999;
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
#extensionEnd {
|
||||||
|
fill: #999;
|
||||||
|
stroke: #999;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
.node text {
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
div.mermaidTooltip {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 200px;
|
||||||
|
padding: 2px;
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #eaf2fb;
|
||||||
|
border: 1px solid #26a;
|
||||||
|
border-radius: 2px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
}
|
@ -12,14 +12,19 @@
|
|||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="exportPdf">
|
<menu-entry @click.native="exportPdf">
|
||||||
<icon-download slot="icon"></icon-download>
|
<icon-download slot="icon"></icon-download>
|
||||||
<div>Export as PDF</div>
|
<div><div class="menu-entry__sponsor">sponsor</div> Export as PDF</div>
|
||||||
<span>Produce a PDF from an HTML template.</span>
|
<span>Produce a PDF from an HTML template.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="exportPandoc">
|
||||||
|
<icon-download slot="icon"></icon-download>
|
||||||
|
<div><div class="menu-entry__sponsor">sponsor</div> Export with Pandoc</div>
|
||||||
|
<span>Convert file to PDF, Word, EPUB...</span>
|
||||||
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MenuEntry from './MenuEntry';
|
import MenuEntry from './common/MenuEntry';
|
||||||
import exportSvc from '../../services/exportSvc';
|
import exportSvc from '../../services/exportSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -37,7 +42,12 @@ export default {
|
|||||||
.catch(() => {}); // Cancel
|
.catch(() => {}); // Cancel
|
||||||
},
|
},
|
||||||
exportPdf() {
|
exportPdf() {
|
||||||
return this.$store.dispatch('modal/notImplemented');
|
return this.$store.dispatch('modal/open', 'pdfExport')
|
||||||
|
.catch(() => {}); // Cancel
|
||||||
|
},
|
||||||
|
exportPandoc() {
|
||||||
|
return this.$store.dispatch('modal/open', 'pandocExport')
|
||||||
|
.catch(() => {}); // Cancel
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
<menu-entry @click.native="setPanel('publish')">
|
<menu-entry @click.native="setPanel('publish')">
|
||||||
<icon-upload slot="icon"></icon-upload>
|
<icon-upload slot="icon"></icon-upload>
|
||||||
<div>Publish</div>
|
<div>Publish</div>
|
||||||
<span>Export to the web.</span>
|
<span>Export your file to the web.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<hr>
|
<hr>
|
||||||
<menu-entry @click.native="fileProperties">
|
<menu-entry @click.native="fileProperties">
|
||||||
@ -63,7 +63,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters, mapActions } from 'vuex';
|
import { mapGetters, mapActions } from 'vuex';
|
||||||
import MenuEntry from './MenuEntry';
|
import MenuEntry from './common/MenuEntry';
|
||||||
import UserImage from '../UserImage';
|
import UserImage from '../UserImage';
|
||||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
import syncSvc from '../../services/syncSvc';
|
import syncSvc from '../../services/syncSvc';
|
||||||
|
@ -34,21 +34,22 @@
|
|||||||
<icon-help-circle slot="icon"></icon-help-circle>
|
<icon-help-circle slot="icon"></icon-help-circle>
|
||||||
<span>About StackEdit</span>
|
<span>About StackEdit</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<a href="editor" target="_blank" class="menu-entry button flex flex--row flex--align-center">
|
<menu-entry @click.native="welcomeFile">
|
||||||
<div class="menu-entry__icon flex flex--column flex--center">
|
<icon-file slot="icon"></icon-file>
|
||||||
<icon-open-in-new></icon-open-in-new>
|
<span>Welcome file</span>
|
||||||
</div>
|
</menu-entry>
|
||||||
<div class="flex flex--column">
|
<menu-entry href="editor" target="_blank">
|
||||||
<span>Go back to StackEdit 4</span>
|
<icon-open-in-new slot="icon"></icon-open-in-new>
|
||||||
</div>
|
<span>StackEdit 4 (deprecated)</span>
|
||||||
</a>
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MenuEntry from './MenuEntry';
|
import MenuEntry from './common/MenuEntry';
|
||||||
import localDbSvc from '../../services/localDbSvc';
|
import localDbSvc from '../../services/localDbSvc';
|
||||||
import backupSvc from '../../services/backupSvc';
|
import backupSvc from '../../services/backupSvc';
|
||||||
|
import welcomeFile from '../../data/welcomeFile.md';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -92,6 +93,13 @@ export default {
|
|||||||
() => {}, // Cancel
|
() => {}, // Cancel
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
welcomeFile() {
|
||||||
|
return this.$store.dispatch('createFile', {
|
||||||
|
name: 'Welcome file',
|
||||||
|
text: welcomeFile,
|
||||||
|
})
|
||||||
|
.then(createdFile => this.$store.commit('file/setCurrentId', createdFile.id));
|
||||||
|
},
|
||||||
about() {
|
about() {
|
||||||
return this.$store.dispatch('modal/open', 'about');
|
return this.$store.dispatch('modal/open', 'about');
|
||||||
},
|
},
|
||||||
|
@ -96,7 +96,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters } from 'vuex';
|
import { mapState, mapGetters } from 'vuex';
|
||||||
import MenuEntry from './MenuEntry';
|
import MenuEntry from './common/MenuEntry';
|
||||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
import dropboxHelper from '../../services/providers/helpers/dropboxHelper';
|
import dropboxHelper from '../../services/providers/helpers/dropboxHelper';
|
||||||
import githubHelper from '../../services/providers/helpers/githubHelper';
|
import githubHelper from '../../services/providers/helpers/githubHelper';
|
||||||
|
@ -44,6 +44,11 @@
|
|||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="token in githubTokens" :key="token.sub">
|
<div v-for="token in githubTokens" :key="token.sub">
|
||||||
|
<menu-entry @click.native="openGithub(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||||
|
<div>Open from GitHub</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
<menu-entry @click.native="saveGithub(token)">
|
<menu-entry @click.native="saveGithub(token)">
|
||||||
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||||
<div>Save on GitHub</div>
|
<div>Save on GitHub</div>
|
||||||
@ -73,12 +78,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters } from 'vuex';
|
import { mapState, mapGetters } from 'vuex';
|
||||||
import MenuEntry from './MenuEntry';
|
import MenuEntry from './common/MenuEntry';
|
||||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
import dropboxHelper from '../../services/providers/helpers/dropboxHelper';
|
import dropboxHelper from '../../services/providers/helpers/dropboxHelper';
|
||||||
import githubHelper from '../../services/providers/helpers/githubHelper';
|
import githubHelper from '../../services/providers/helpers/githubHelper';
|
||||||
import googleDriveProvider from '../../services/providers/googleDriveProvider';
|
import googleDriveProvider from '../../services/providers/googleDriveProvider';
|
||||||
import dropboxProvider from '../../services/providers/dropboxProvider';
|
import dropboxProvider from '../../services/providers/dropboxProvider';
|
||||||
|
import githubProvider from '../../services/providers/githubProvider';
|
||||||
import syncSvc from '../../services/syncSvc';
|
import syncSvc from '../../services/syncSvc';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
|
||||||
@ -161,15 +167,23 @@ export default {
|
|||||||
() => dropboxProvider.openFiles(token, paths)));
|
() => dropboxProvider.openFiles(token, paths)));
|
||||||
},
|
},
|
||||||
saveGoogleDrive(token) {
|
saveGoogleDrive(token) {
|
||||||
return openSyncModal(token, 'googleDriveSync')
|
return openSyncModal(token, 'googleDriveSave')
|
||||||
.catch(() => {}); // Cancel
|
.catch(() => {}); // Cancel
|
||||||
},
|
},
|
||||||
saveDropbox(token) {
|
saveDropbox(token) {
|
||||||
return openSyncModal(token, 'dropboxSync')
|
return openSyncModal(token, 'dropboxSave')
|
||||||
.catch(() => {}); // Cancel
|
.catch(() => {}); // Cancel
|
||||||
},
|
},
|
||||||
|
openGithub(token) {
|
||||||
|
return store.dispatch('modal/open', {
|
||||||
|
type: 'githubOpen',
|
||||||
|
token,
|
||||||
|
})
|
||||||
|
.then(syncLocation => this.$store.dispatch('queue/enqueue',
|
||||||
|
() => githubProvider.openFile(token, syncLocation)));
|
||||||
|
},
|
||||||
saveGithub(token) {
|
saveGithub(token) {
|
||||||
return openSyncModal(token, 'githubSync')
|
return openSyncModal(token, 'githubSave')
|
||||||
.catch(() => {}); // Cancel
|
.catch(() => {}); // Cancel
|
||||||
},
|
},
|
||||||
saveGist(token) {
|
saveGist(token) {
|
||||||
|
@ -3,26 +3,34 @@
|
|||||||
<div class="menu-entry__icon flex flex--column flex--center">
|
<div class="menu-entry__icon flex flex--column flex--center">
|
||||||
<slot name="icon"></slot>
|
<slot name="icon"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex--column">
|
<div class="menu-entry__text flex flex--column">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../common/variables.scss';
|
@import '../../common/variables.scss';
|
||||||
|
|
||||||
.menu-entry {
|
.menu-entry {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
|
div div {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-skip: ink;
|
||||||
|
|
||||||
|
&.menu-entry__sponsor {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 0.75em;
|
font-size: 0.75rem;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
line-height: 1.4;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,4 +50,18 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
top: -999px;
|
top: -999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-entry__sponsor {
|
||||||
|
float: right;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.05em 0.25em;
|
||||||
|
background-color: darken($error-color, 10);
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-entry__text {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1 modal__inner-1--about-modal" role="dialog" aria-label="About">
|
<modal-inner class="modal__inner-1--about-modal" aria-label="About">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="logo-background"></div>
|
<div class="logo-background"></div>
|
||||||
<div class="app-version">v{{version}} — © 2017 Benoit Schweblin</div>
|
<div class="app-version">v{{version}} — © 2017 Benoit Schweblin</div>
|
||||||
<hr>
|
<hr>
|
||||||
@ -12,17 +12,21 @@
|
|||||||
<a target="_blank" href="https://twitter.com/stackedit/">StackEdit on Twitter</a>
|
<a target="_blank" href="https://twitter.com/stackedit/">StackEdit on Twitter</a>
|
||||||
<hr>
|
<hr>
|
||||||
<a target="_blank" href="privacy_policy.html">Privacy Policy</a>
|
<a target="_blank" href="privacy_policy.html">Privacy Policy</a>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.resolve()">Close</button>
|
<button class="button" @click="config.resolve()">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import ModalInner from './common/ModalInner';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
ModalInner,
|
||||||
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
version: VERSION,
|
version: VERSION,
|
||||||
}),
|
}),
|
||||||
@ -38,7 +42,7 @@ export default {
|
|||||||
|
|
||||||
.logo-background {
|
.logo-background {
|
||||||
height: 75px;
|
height: 75px;
|
||||||
margin-bottom: 0.5rem;
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-version {
|
.app-version {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1 modal__inner-1--file-properties" role="dialog" aria-label="File properties">
|
<modal-inner class="modal__inner-1--file-properties" aria-label="File properties">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="tabs flex flex--row">
|
<div class="tabs flex flex--row">
|
||||||
<tab :active="tab === 'custom'" @click="tab = 'custom'">
|
<tab :active="tab === 'custom'" @click="tab = 'custom'">
|
||||||
Current file properties
|
Current file properties
|
||||||
@ -22,26 +22,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__error modal__error--file-properties">{{error}}</div>
|
<div class="modal__error modal__error--file-properties">{{error}}</div>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import Tab from './Tab';
|
import ModalInner from './common/ModalInner';
|
||||||
|
import Tab from './common/Tab';
|
||||||
import CodeEditor from '../CodeEditor';
|
import CodeEditor from '../CodeEditor';
|
||||||
import utils from '../../services/utils';
|
import utils from '../../services/utils';
|
||||||
import defaultProperties from '../../data/defaultFileProperties.yml';
|
import defaultProperties from '../../data/defaultFileProperties.yml';
|
||||||
|
|
||||||
const emptyProperties = '# Add custom properties for the current file here to override the default properties.\n';
|
const emptyProperties = `# Add custom properties for the current file here
|
||||||
|
# to override the default properties.
|
||||||
|
`;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
ModalInner,
|
||||||
Tab,
|
Tab,
|
||||||
CodeEditor,
|
CodeEditor,
|
||||||
},
|
},
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1" role="dialog" aria-label="Export to HTML">
|
<modal-inner aria-label="Export to HTML">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
|
<p>Please choose a template for your <b>HTML export</b>.</p>
|
||||||
<form-entry label="Template">
|
<form-entry label="Template">
|
||||||
<select slot="field" class="textfield" v-model="selectedTemplate" @keyup.enter="resolve()">
|
<select class="textfield" slot="field" v-model="selectedTemplate" @keyup.enter="resolve()">
|
||||||
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
|
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
|
||||||
{{ template.name }}
|
{{ template.name }}
|
||||||
</option>
|
</option>
|
||||||
@ -11,19 +12,19 @@
|
|||||||
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
|
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
|
||||||
</div>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button button--copy">Copy to clipboard</button>
|
<button class="button button--copy">Copy to clipboard</button>
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Clipboard from 'clipboard';
|
import Clipboard from 'clipboard';
|
||||||
import exportSvc from '../../services/exportSvc';
|
import exportSvc from '../../services/exportSvc';
|
||||||
import modalTemplate from './modalTemplate';
|
import modalTemplate from './common/modalTemplate';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
data: () => ({
|
data: () => ({
|
||||||
@ -33,12 +34,16 @@ export default modalTemplate({
|
|||||||
selectedTemplate: 'htmlExportTemplate',
|
selectedTemplate: 'htmlExportTemplate',
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
let timeoutId;
|
||||||
this.$watch('selectedTemplate', (selectedTemplate) => {
|
this.$watch('selectedTemplate', (selectedTemplate) => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
const currentFile = this.$store.getters['file/current'];
|
const currentFile = this.$store.getters['file/current'];
|
||||||
exportSvc.applyTemplate(currentFile.id, this.allTemplates[selectedTemplate])
|
exportSvc.applyTemplate(currentFile.id, this.allTemplates[selectedTemplate])
|
||||||
.then((html) => {
|
.then((html) => {
|
||||||
this.result = html;
|
this.result = html;
|
||||||
});
|
});
|
||||||
|
}, 10);
|
||||||
}, {
|
}, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
});
|
||||||
@ -51,9 +56,10 @@ export default modalTemplate({
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resolve() {
|
resolve() {
|
||||||
|
const config = this.config;
|
||||||
const currentFile = this.$store.getters['file/current'];
|
const currentFile = this.$store.getters['file/current'];
|
||||||
|
config.resolve();
|
||||||
exportSvc.exportToDisk(currentFile.id, 'html', this.allTemplates[this.selectedTemplate]);
|
exportSvc.exportToDisk(currentFile.id, 'html', this.allTemplates[this.selectedTemplate]);
|
||||||
this.config.resolve();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1" role="dialog" aria-label="Insert image">
|
<modal-inner aria-label="Insert image">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<p>Please provide a <b>URL</b> for your image.
|
<p>Please provide a <b>URL</b> for your image.</p>
|
||||||
<form-entry label="URL" error="url">
|
<form-entry label="URL" error="url">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="url" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="url" @keyup.enter="resolve()">
|
||||||
</form-entry>
|
</form-entry>
|
||||||
@ -14,17 +14,17 @@
|
|||||||
<icon-provider slot="icon" provider-id="googlePhotos"></icon-provider>
|
<icon-provider slot="icon" provider-id="googlePhotos"></icon-provider>
|
||||||
<span>Add Google Photos account</span>
|
<span>Add Google Photos account</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="reject()">Cancel</button>
|
<button class="button" @click="reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import modalTemplate from './modalTemplate';
|
import modalTemplate from './common/modalTemplate';
|
||||||
import MenuEntry from '../menus/MenuEntry';
|
import MenuEntry from '../menus/common/MenuEntry';
|
||||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1" role="dialog" aria-label="Insert link">
|
<modal-inner aria-label="Insert link">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<p>Please provide a <b>URL</b> for your link.
|
<p>Please provide a <b>URL</b> for your link.</p>
|
||||||
<form-entry label="URL" error="url">
|
<form-entry label="URL" error="url">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="url" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="url" @keyup.enter="resolve()">
|
||||||
</form-entry>
|
</form-entry>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="reject()">Cancel</button>
|
<button class="button" @click="reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import modalTemplate from './modalTemplate';
|
import modalTemplate from './common/modalTemplate';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
82
src/components/modals/PandocExportModal.vue
Normal file
82
src/components/modals/PandocExportModal.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<modal-inner aria-label="Export with Pandoc">
|
||||||
|
<div class="modal__content">
|
||||||
|
<p>Please choose a format for your <b>Pandoc export</b>.</p>
|
||||||
|
<form-entry label="Template">
|
||||||
|
<select class="textfield" slot="field" v-model="selectedFormat" @keyup.enter="resolve()">
|
||||||
|
<option value="asciidoc">AsciiDoc</option>
|
||||||
|
<option value="context">ConTeXt</option>
|
||||||
|
<option value="epub">EPUB</option>
|
||||||
|
<option value="epub3">EPUB v3</option>
|
||||||
|
<option value="latex">LaTeX</option>
|
||||||
|
<option value="odt">OpenOffice</option>
|
||||||
|
<option value="pdf">PDF</option>
|
||||||
|
<option value="rst">reStructuredText</option>
|
||||||
|
<option value="rtf">Rich Text Format</option>
|
||||||
|
<option value="textile">Textile</option>
|
||||||
|
<option value="docx">Word</option>
|
||||||
|
</select>
|
||||||
|
</form-entry>
|
||||||
|
</div>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
|
</div>
|
||||||
|
</modal-inner>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import FileSaver from 'file-saver';
|
||||||
|
import sponsorSvc from '../../services/sponsorSvc';
|
||||||
|
import networkSvc from '../../services/networkSvc';
|
||||||
|
import editorSvc from '../../services/editorSvc';
|
||||||
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
|
import modalTemplate from './common/modalTemplate';
|
||||||
|
|
||||||
|
export default modalTemplate({
|
||||||
|
computedLocalSettings: {
|
||||||
|
selectedFormat: 'pandocExportFormat',
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
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([
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
const loginToken = this.$store.getters['data/loginToken'];
|
||||||
|
return loginToken && googleHelper.refreshToken(loginToken);
|
||||||
|
}),
|
||||||
|
sponsorSvc.getToken(),
|
||||||
|
])
|
||||||
|
.then(([loginToken, token]) => networkSvc.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: 'pandocExport',
|
||||||
|
params: {
|
||||||
|
token,
|
||||||
|
idToken: loginToken && loginToken.idToken,
|
||||||
|
format: selectedFormat,
|
||||||
|
options: JSON.stringify(this.$store.getters['data/computedSettings'].pandoc),
|
||||||
|
metadata: JSON.stringify(currentContent.properties),
|
||||||
|
},
|
||||||
|
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) => {
|
||||||
|
console.error(err); // eslint-disable-line no-console
|
||||||
|
this.$store.dispatch('notification/error', err);
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
75
src/components/modals/PdfExportModal.vue
Normal file
75
src/components/modals/PdfExportModal.vue
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<modal-inner aria-label="Export to PDF">
|
||||||
|
<div class="modal__content">
|
||||||
|
<p>Please choose a template for your <b>PDF export</b>.</p>
|
||||||
|
<form-entry label="Template">
|
||||||
|
<select class="textfield" slot="field" v-model="selectedTemplate" @keyup.enter="resolve()">
|
||||||
|
<option v-for="(template, id) in allTemplates" :key="id" :value="id">
|
||||||
|
{{ template.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-entry__actions">
|
||||||
|
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</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>
|
||||||
|
</div>
|
||||||
|
</modal-inner>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import FileSaver from 'file-saver';
|
||||||
|
import exportSvc from '../../services/exportSvc';
|
||||||
|
import sponsorSvc from '../../services/sponsorSvc';
|
||||||
|
import networkSvc from '../../services/networkSvc';
|
||||||
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
|
import modalTemplate from './common/modalTemplate';
|
||||||
|
|
||||||
|
export default modalTemplate({
|
||||||
|
computedLocalSettings: {
|
||||||
|
selectedTemplate: 'pdfExportTemplate',
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resolve() {
|
||||||
|
this.config.resolve();
|
||||||
|
const currentFile = this.$store.getters['file/current'];
|
||||||
|
this.$store.dispatch('queue/enqueue', () => Promise.all([
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
const loginToken = this.$store.getters['data/loginToken'];
|
||||||
|
return loginToken && googleHelper.refreshToken(loginToken);
|
||||||
|
}),
|
||||||
|
sponsorSvc.getToken(),
|
||||||
|
exportSvc.applyTemplate(
|
||||||
|
currentFile.id, this.allTemplates[this.selectedTemplate], true),
|
||||||
|
])
|
||||||
|
.then(([loginToken, token, html]) => networkSvc.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: 'pdfExport',
|
||||||
|
params: {
|
||||||
|
token,
|
||||||
|
idToken: loginToken && loginToken.idToken,
|
||||||
|
options: JSON.stringify(this.$store.getters['data/computedSettings'].wkhtmltopdf),
|
||||||
|
},
|
||||||
|
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) => {
|
||||||
|
console.error(err); // eslint-disable-line no-console
|
||||||
|
this.$store.dispatch('notification/error', err);
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1 modal__inner-1--publish-management" role="dialog" aria-label="Manage publication locations">
|
<modal-inner class="modal__inner-1--publish-management" aria-label="Manage publication locations">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<p v-if="publishLocations.length"><b>{{currentFileName}}</b> is published to the following location(s):</p>
|
<p v-if="publishLocations.length"><b>{{currentFileName}}</b> is published to the following location(s):</p>
|
||||||
<p v-else><b>{{currentFileName}}</b> is not published yet.</p>
|
<p v-else><b>{{currentFileName}}</b> is not published yet.</p>
|
||||||
<div>
|
<div>
|
||||||
@ -24,18 +24,22 @@
|
|||||||
<div class="modal__info" v-if="publishLocations.length">
|
<div class="modal__info" v-if="publishLocations.length">
|
||||||
<b>Note:</b> Removing a synchronized location won't delete any file.
|
<b>Note:</b> Removing a synchronized location won't delete any file.
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="config.resolve()">Ok</button>
|
<button class="button" @click="config.resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import ModalInner from './common/ModalInner';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
ModalInner,
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('modal', [
|
...mapGetters('modal', [
|
||||||
'config',
|
'config',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1 modal__inner-1--settings" role="dialog" aria-label="Settings">
|
<modal-inner class="modal__inner-1--settings" aria-label="Settings">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="tabs flex flex--row">
|
<div class="tabs flex flex--row">
|
||||||
<tab :active="tab === 'custom'" @click="tab = 'custom'">
|
<tab :active="tab === 'custom'" @click="tab = 'custom'">
|
||||||
Custom settings
|
Custom settings
|
||||||
@ -22,25 +22,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__error modal__error--settings">{{error}}</div>
|
<div class="modal__error modal__error--settings">{{error}}</div>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="!error && config.resolve(strippedCustomSettings)">Ok</button>
|
<button class="button" @click="!error && config.resolve(strippedCustomSettings)">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import Tab from './Tab';
|
import ModalInner from './common/ModalInner';
|
||||||
|
import Tab from './common/Tab';
|
||||||
import CodeEditor from '../CodeEditor';
|
import CodeEditor from '../CodeEditor';
|
||||||
import defaultSettings from '../../data/defaultSettings.yml';
|
import defaultSettings from '../../data/defaultSettings.yml';
|
||||||
|
|
||||||
const emptySettings = '# Add your custom settings here to override the default settings.\n';
|
const emptySettings = `# Add your custom settings here to override the
|
||||||
|
# default settings.
|
||||||
|
`;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
ModalInner,
|
||||||
Tab,
|
Tab,
|
||||||
CodeEditor,
|
CodeEditor,
|
||||||
},
|
},
|
||||||
|
98
src/components/modals/SponsorModal.vue
Normal file
98
src/components/modals/SponsorModal.vue
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<modal-inner class="modal__inner-1--sponsor" aria-label="Sponsor">
|
||||||
|
<div class="modal__content">
|
||||||
|
<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>
|
||||||
|
<span>{{button.description}}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</modal-inner>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import ModalInner from './common/ModalInner';
|
||||||
|
import utils from '../../services/utils';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ModalInner,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
const loginToken = this.$store.getters['data/loginToken'];
|
||||||
|
const makeButton = (id, price, description, offer) => {
|
||||||
|
const params = {
|
||||||
|
cmd: '_s-xclick',
|
||||||
|
hosted_button_id: id,
|
||||||
|
custom: loginToken.sub,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
price,
|
||||||
|
description,
|
||||||
|
offer,
|
||||||
|
link: utils.addQueryParams('https://www.paypal.com/cgi-bin/webscr', params),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
buttons: loginToken ? [
|
||||||
|
makeButton('TDAPH47B3J2JW', '$5', '3 months sponsorship'),
|
||||||
|
makeButton('6CTKPKF8868UA', '$15', '1 year sponsorship', '-25%'),
|
||||||
|
makeButton('A5ZDYW6SYDLBE', '$25', '2 years sponsorship', '-37%'),
|
||||||
|
makeButton('3DMD3TT2RDPQA', '$50', '5 years sponsorship', '-50%'),
|
||||||
|
] : [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('modal', [
|
||||||
|
'config',
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
sponsor() {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../common/variables.scss';
|
||||||
|
|
||||||
|
.modal__inner-1--sponsor {
|
||||||
|
max-width: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paypal-option {
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
height: auto;
|
||||||
|
font-size: 2.3em;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paypal-option__offer {
|
||||||
|
float: right;
|
||||||
|
font-size: 0.6rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.1em 0.2em;
|
||||||
|
background-color: darken($error-color, 10);
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #fff;
|
||||||
|
margin-left: -0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1 modal__inner-1--sync-management" role="dialog" aria-label="Manage synchronized locations">
|
<modal-inner class="modal__inner-1--sync-management" aria-label="Manage synchronized locations">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<p v-if="syncLocations.length"><b>{{currentFileName}}</b> is synchronized with the following location(s):</p>
|
<p v-if="syncLocations.length"><b>{{currentFileName}}</b> is synchronized with the following location(s):</p>
|
||||||
<p v-else><b>{{currentFileName}}</b> is not synchronized yet.</p>
|
<p v-else><b>{{currentFileName}}</b> is not synchronized yet.</p>
|
||||||
<div>
|
<div>
|
||||||
@ -24,18 +24,22 @@
|
|||||||
<div class="modal__info" v-if="syncLocations.length">
|
<div class="modal__info" v-if="syncLocations.length">
|
||||||
<b>Note:</b> Removing a synchronized location won't delete any file.
|
<b>Note:</b> Removing a synchronized location won't delete any file.
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="config.resolve()">Ok</button>
|
<button class="button" @click="config.resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import ModalInner from './common/ModalInner';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
ModalInner,
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('modal', [
|
...mapGetters('modal', [
|
||||||
'config',
|
'config',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1 modal__inner-1--templates" role="dialog" aria-label="Manage templates">
|
<modal-inner class="modal__inner-1--templates" aria-label="Manage templates">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="form-entry">
|
<div class="form-entry">
|
||||||
<label class="form-entry__label" for="template">Template</label>
|
<label class="form-entry__label" for="template">Template</label>
|
||||||
<div class="form-entry__field">
|
<div class="form-entry__field">
|
||||||
@ -33,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isReadOnly">
|
<div v-if="!isReadOnly">
|
||||||
<a href="javascript:void(0)" v-if="!showHelpers" @click="showHelpers = true">Add helpers ▾</a>
|
<a href="javascript:void(0)" v-if="!showHelpers" @click="showHelpers = true">Add helpers</a>
|
||||||
<div class="form-entry" v-else>
|
<div class="form-entry" v-else>
|
||||||
<br>
|
<br>
|
||||||
<label class="form-entry__label">Helpers</label>
|
<label class="form-entry__label">Helpers</label>
|
||||||
@ -42,17 +42,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import utils from '../../services/utils';
|
import utils from '../../services/utils';
|
||||||
|
import ModalInner from './common/ModalInner';
|
||||||
import CodeEditor from '../CodeEditor';
|
import CodeEditor from '../CodeEditor';
|
||||||
import emptyTemplateValue from '../../data/emptyTemplateValue.html';
|
import emptyTemplateValue from '../../data/emptyTemplateValue.html';
|
||||||
import emptyTemplateHelpers from '!raw-loader!../../data/emptyTemplateHelpers.js'; // eslint-disable-line
|
import emptyTemplateHelpers from '!raw-loader!../../data/emptyTemplateHelpers.js'; // eslint-disable-line
|
||||||
@ -70,6 +71,7 @@ function fillEmptyFields(template) {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
ModalInner,
|
||||||
CodeEditor,
|
CodeEditor,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import utils from '../../services/utils';
|
import utils from '../../../services/utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['label', 'error'],
|
props: ['label', 'error'],
|
71
src/components/modals/common/ModalInner.vue
Normal file
71
src/components/modals/common/ModalInner.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<div class="modal__inner-1" role="dialog">
|
||||||
|
<div class="modal__inner-2">
|
||||||
|
<button class="modal__close-button button not-tabbable" @click="config.reject()" v-title="'Close modal'">
|
||||||
|
<icon-close></icon-close>
|
||||||
|
</button>
|
||||||
|
<div class="modal__sponsor-button" v-if="showSponsorButton">
|
||||||
|
Please consider
|
||||||
|
<a class="not-tabbable" href="javascript:void(0)" @click="sponsor">sponsoring StackEdit</a> for just $5.
|
||||||
|
</div>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import googleHelper from '../../../services/providers/helpers/googleHelper';
|
||||||
|
import syncSvc from '../../../services/syncSvc';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
...mapGetters('modal', [
|
||||||
|
'config',
|
||||||
|
]),
|
||||||
|
showSponsorButton() {
|
||||||
|
return !this.$store.getters.isSponsor && this.$store.getters['modal/config'].type !== 'sponsor';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
sponsor() {
|
||||||
|
Promise.resolve()
|
||||||
|
.then(() => !this.$store.getters['data/loginToken'] &&
|
||||||
|
this.$store.dispatch('modal/signInRequired') // If user has to sign in
|
||||||
|
.then(() => googleHelper.signin())
|
||||||
|
.then(() => syncSvc.requestSync()))
|
||||||
|
.then(() => this.$store.dispatch('modal/open', 'sponsor'))
|
||||||
|
.catch(() => { }); // Cancel
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../common/variables.scss';
|
||||||
|
|
||||||
|
.modal__close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
color: rgba(0, 0, 0, 0.2);
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 2px;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
color: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal__sponsor-button {
|
||||||
|
display: inline-block;
|
||||||
|
color: darken($error-color, 10%);
|
||||||
|
background-color: transparentize($error-color, 0.85);
|
||||||
|
border-radius: $border-radius-base;
|
||||||
|
padding: 1em;
|
||||||
|
margin-bottom: 1.2em;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,5 +1,8 @@
|
|||||||
|
import ModalInner from './ModalInner';
|
||||||
import FormEntry from './FormEntry';
|
import FormEntry from './FormEntry';
|
||||||
import store from '../../store';
|
import store from '../../../store';
|
||||||
|
|
||||||
|
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
|
||||||
|
|
||||||
export default (desc) => {
|
export default (desc) => {
|
||||||
const component = {
|
const component = {
|
||||||
@ -10,6 +13,7 @@ export default (desc) => {
|
|||||||
}),
|
}),
|
||||||
components: {
|
components: {
|
||||||
...desc.components || {},
|
...desc.components || {},
|
||||||
|
ModalInner,
|
||||||
FormEntry,
|
FormEntry,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -49,8 +53,18 @@ export default (desc) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (key === 'selectedTemplate') {
|
if (key === 'selectedTemplate') {
|
||||||
component.computed.allTemplates = () => store.getters['data/allTemplates'];
|
component.computed.allTemplates = () => {
|
||||||
component.methods.configureTemplates = () => {
|
const allTemplates = store.getters['data/allTemplates'];
|
||||||
|
const sortedTemplates = {};
|
||||||
|
Object.keys(allTemplates)
|
||||||
|
.sort((id1, id2) => collator.compare(allTemplates[id1].name, allTemplates[id2].name))
|
||||||
|
.forEach((templateId) => {
|
||||||
|
sortedTemplates[templateId] = allTemplates[templateId];
|
||||||
|
});
|
||||||
|
return sortedTemplates;
|
||||||
|
};
|
||||||
|
// Make use of `function` to have `this` bound to the component
|
||||||
|
component.methods.configureTemplates = function () { // eslint-disable-line func-names
|
||||||
store.dispatch('modal/open', {
|
store.dispatch('modal/open', {
|
||||||
type: 'templates',
|
type: 'templates',
|
||||||
selectedId: this.selectedTemplate,
|
selectedId: this.selectedTemplate,
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1" role="dialog" aria-label="Publish to Blogger Page">
|
<modal-inner aria-label="Publish to Blogger Page">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="bloggerPage"></icon-provider>
|
<icon-provider provider-id="bloggerPage"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
@ -27,17 +27,17 @@
|
|||||||
<div class="modal__info">
|
<div class="modal__info">
|
||||||
<b>ProTip:</b> You can provide a value for <code>title</code> in the <a href="javascript:void(0)" @click="openFileProperties">file properties</a>.
|
<b>ProTip:</b> You can provide a value for <code>title</code> in the <a href="javascript:void(0)" @click="openFileProperties">file properties</a>.
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import bloggerPageProvider from '../../services/providers/bloggerPageProvider';
|
import bloggerPageProvider from '../../../services/providers/bloggerPageProvider';
|
||||||
import modalTemplate from './modalTemplate';
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
data: () => ({
|
data: () => ({
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1" role="dialog" aria-label="Publish to Blogger">
|
<modal-inner aria-label="Publish to Blogger">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="blogger"></icon-provider>
|
<icon-provider provider-id="blogger"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
@ -28,17 +28,17 @@
|
|||||||
<b>ProTip:</b> You can provide values for <code>title</code>, <code>tags</code>,
|
<b>ProTip:</b> You can provide values for <code>title</code>, <code>tags</code>,
|
||||||
<code>status</code> and <code>date</code> in the <a href="javascript:void(0)" @click="openFileProperties">file properties</a>.
|
<code>status</code> and <code>date</code> in the <a href="javascript:void(0)" @click="openFileProperties">file properties</a>.
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import bloggerProvider from '../../services/providers/bloggerProvider';
|
import bloggerProvider from '../../../services/providers/bloggerProvider';
|
||||||
import modalTemplate from './modalTemplate';
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
data: () => ({
|
data: () => ({
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1" role="dialog" aria-label="Link Dropbox account">
|
<modal-inner aria-label="Link Dropbox account">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="dropbox"></icon-provider>
|
<icon-provider provider-id="dropbox"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
@ -15,16 +15,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="config.resolve()">Ok</button>
|
<button class="button" @click="config.resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import modalTemplate from './modalTemplate';
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
computedLocalSettings: {
|
computedLocalSettings: {
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1" role="dialog" aria-label="Publish to Dropbox">
|
<modal-inner aria-label="Publish to Dropbox">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="dropbox"></icon-provider>
|
<icon-provider provider-id="dropbox"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
@ -22,17 +22,17 @@
|
|||||||
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
|
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
|
||||||
</div>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import dropboxProvider from '../../services/providers/dropboxProvider';
|
import dropboxProvider from '../../../services/providers/dropboxProvider';
|
||||||
import modalTemplate from './modalTemplate';
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
data: () => ({
|
data: () => ({
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1" role="dialog" aria-label="Synchronize with Dropbox">
|
<modal-inner aria-label="Synchronize with Dropbox">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="dropbox"></icon-provider>
|
<icon-provider provider-id="dropbox"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
@ -12,17 +12,17 @@
|
|||||||
If the file exists, it will be replaced.
|
If the file exists, it will be replaced.
|
||||||
</div>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import dropboxProvider from '../../services/providers/dropboxProvider';
|
import dropboxProvider from '../../../services/providers/dropboxProvider';
|
||||||
import modalTemplate from './modalTemplate';
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
data: () => ({
|
data: () => ({
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1" role="dialog" aria-label="Publish to Gist">
|
<modal-inner aria-label="Publish to Gist">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="gist"></icon-provider>
|
<icon-provider provider-id="gist"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
@ -34,17 +34,17 @@
|
|||||||
<div class="modal__info">
|
<div class="modal__info">
|
||||||
<b>ProTip:</b> You can provide a value for <code>title</code> in the <a href="javascript:void(0)" @click="openFileProperties">file properties</a>.
|
<b>ProTip:</b> You can provide a value for <code>title</code> in the <a href="javascript:void(0)" @click="openFileProperties">file properties</a>.
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import gistProvider from '../../services/providers/gistProvider';
|
import gistProvider from '../../../services/providers/gistProvider';
|
||||||
import modalTemplate from './modalTemplate';
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
data: () => ({
|
data: () => ({
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1" role="dialog" aria-label="Synchronize with Gist">
|
<modal-inner aria-label="Synchronize with Gist">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="gist"></icon-provider>
|
<icon-provider provider-id="gist"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
@ -21,17 +21,17 @@
|
|||||||
If the file exists in the Gist, it will be replaced.
|
If the file exists in the Gist, it will be replaced.
|
||||||
</div>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import gistProvider from '../../services/providers/gistProvider';
|
import gistProvider from '../../../services/providers/gistProvider';
|
||||||
import modalTemplate from './modalTemplate';
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
data: () => ({
|
data: () => ({
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1" role="dialog" aria-label="Link GitHub account">
|
<modal-inner aria-label="Link GitHub account">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="github"></icon-provider>
|
<icon-provider provider-id="github"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
@ -8,20 +8,20 @@
|
|||||||
<div class="form-entry">
|
<div class="form-entry">
|
||||||
<div class="form-entry__checkbox">
|
<div class="form-entry__checkbox">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" v-model="repoFullAccess"> Request access to private repositories
|
<input type="checkbox" v-model="repoFullAccess"> Grant access to my <b>private repositories</b>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="config.resolve()">Ok</button>
|
<button class="button" @click="config.resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import modalTemplate from './modalTemplate';
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
computedLocalSettings: {
|
computedLocalSettings: {
|
68
src/components/modals/providers/GithubOpenModal.vue
Normal file
68
src/components/modals/providers/GithubOpenModal.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<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>This will open a file from your <b>GitHub</b> repository and keep it synchronized.</p>
|
||||||
|
<form-entry label="Repository URL" error="repoUrl">
|
||||||
|
<input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keyup.enter="resolve()">
|
||||||
|
<div class="form-entry__info">
|
||||||
|
<b>Example:</b> https://github.com/benweet/stackedit
|
||||||
|
</div>
|
||||||
|
</form-entry>
|
||||||
|
<form-entry label="Branch (optional)">
|
||||||
|
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keyup.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" @keyup.enter="resolve()">
|
||||||
|
<div class="form-entry__info">
|
||||||
|
<b>Example:</b> docs/README.md
|
||||||
|
</div>
|
||||||
|
</form-entry>
|
||||||
|
</div>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
|
</div>
|
||||||
|
</modal-inner>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import githubProvider from '../../../services/providers/githubProvider';
|
||||||
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
|
||||||
|
export default modalTemplate({
|
||||||
|
data: () => ({
|
||||||
|
branch: '',
|
||||||
|
path: '',
|
||||||
|
}),
|
||||||
|
computedLocalSettings: {
|
||||||
|
repoUrl: 'githubRepoUrl',
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resolve() {
|
||||||
|
if (!this.repoUrl) {
|
||||||
|
this.setError('repoUrl');
|
||||||
|
}
|
||||||
|
if (!this.path) {
|
||||||
|
this.setError('path');
|
||||||
|
}
|
||||||
|
if (this.repoUrl && this.path) {
|
||||||
|
const parsedRepo = githubProvider.parseRepoUrl(this.repoUrl);
|
||||||
|
if (!parsedRepo) {
|
||||||
|
this.setError('repoUrl');
|
||||||
|
} else {
|
||||||
|
// Return new location
|
||||||
|
const location = githubProvider.makeLocation(
|
||||||
|
this.config.token, parsedRepo.owner, parsedRepo.repo, this.branch || 'master', this.path);
|
||||||
|
this.config.resolve(location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1" role="dialog" aria-label="Publish to GitHub">
|
<modal-inner aria-label="Publish to GitHub">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="github"></icon-provider>
|
<icon-provider provider-id="github"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
@ -34,17 +34,17 @@
|
|||||||
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
|
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
|
||||||
</div>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import githubProvider from '../../services/providers/githubProvider';
|
import githubProvider from '../../../services/providers/githubProvider';
|
||||||
import modalTemplate from './modalTemplate';
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
data: () => ({
|
data: () => ({
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1" role="dialog" aria-label="Synchronize with GitHub">
|
<modal-inner aria-label="Synchronize with GitHub">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="github"></icon-provider>
|
<icon-provider provider-id="github"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
@ -14,7 +14,7 @@
|
|||||||
<form-entry label="Branch (optional)">
|
<form-entry label="Branch (optional)">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="branch" @keyup.enter="resolve()">
|
||||||
<div class="form-entry__info">
|
<div class="form-entry__info">
|
||||||
If not provided, the master branch will be used.
|
If not provided, the <code>master</code> branch will be used.
|
||||||
</div>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
<form-entry label="File path" error="path">
|
<form-entry label="File path" error="path">
|
||||||
@ -24,17 +24,17 @@
|
|||||||
If the file exists, it will be replaced.
|
If the file exists, it will be replaced.
|
||||||
</div>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import githubProvider from '../../services/providers/githubProvider';
|
import githubProvider from '../../../services/providers/githubProvider';
|
||||||
import modalTemplate from './modalTemplate';
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
data: () => ({
|
data: () => ({
|
||||||
@ -56,13 +56,13 @@ export default modalTemplate({
|
|||||||
this.setError('path');
|
this.setError('path');
|
||||||
}
|
}
|
||||||
if (this.repoUrl && this.path) {
|
if (this.repoUrl && this.path) {
|
||||||
const parsedRepo = this.repoUrl.match(/[/:]?([^/:]+)\/([^/]+?)(?:\.git|\/)?$/);
|
const parsedRepo = githubProvider.parseRepoUrl(this.repoUrl);
|
||||||
if (!parsedRepo) {
|
if (!parsedRepo) {
|
||||||
this.setError('repoUrl');
|
this.setError('repoUrl');
|
||||||
} else {
|
} else {
|
||||||
// Return new location
|
// Return new location
|
||||||
const location = githubProvider.makeLocation(
|
const location = githubProvider.makeLocation(
|
||||||
this.config.token, parsedRepo[1], parsedRepo[2], this.branch || 'master', this.path);
|
this.config.token, parsedRepo.owner, parsedRepo.repo, this.branch || 'master', this.path);
|
||||||
this.config.resolve(location);
|
this.config.resolve(location);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1" role="dialog" aria-label="Publish to Google Drive">
|
<modal-inner aria-label="Publish to Google Drive">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="googleDrive"></icon-provider>
|
<icon-provider provider-id="googleDrive"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
@ -45,18 +45,18 @@
|
|||||||
<div class="modal__info">
|
<div class="modal__info">
|
||||||
<b>ProTip:</b> You can provide a value for <code>title</code> in the <a href="javascript:void(0)" @click="openFileProperties">file properties</a>.
|
<b>ProTip:</b> You can provide a value for <code>title</code> in the <a href="javascript:void(0)" @click="openFileProperties">file properties</a>.
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
import googleHelper from '../../../services/providers/helpers/googleHelper';
|
||||||
import googleDriveProvider from '../../services/providers/googleDriveProvider';
|
import googleDriveProvider from '../../../services/providers/googleDriveProvider';
|
||||||
import modalTemplate from './modalTemplate';
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
data: () => ({
|
data: () => ({
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1" role="dialog" aria-label="Synchronize with Google Drive">
|
<modal-inner aria-label="Synchronize with Google Drive">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="googleDrive"></icon-provider>
|
<icon-provider provider-id="googleDrive"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
@ -20,18 +20,18 @@
|
|||||||
This will overwrite the file on the server.
|
This will overwrite the file on the server.
|
||||||
</div>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
import googleHelper from '../../../services/providers/helpers/googleHelper';
|
||||||
import googleDriveProvider from '../../services/providers/googleDriveProvider';
|
import googleDriveProvider from '../../../services/providers/googleDriveProvider';
|
||||||
import modalTemplate from './modalTemplate';
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
data: () => ({
|
data: () => ({
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1 modal__inner-1--google-photo" role="dialog" aria-label="Import Google Photo">
|
<modal-inner class="modal__inner-1--google-photo" aria-label="Import Google Photo">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="google-photo__tumbnail" :style="{'background-image': thumbnailUrl}"></div>
|
<div class="google-photo__tumbnail" :style="{'background-image': thumbnailUrl}"></div>
|
||||||
<form-entry label="Title (optional)">
|
<form-entry label="Title (optional)">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="title" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="title" @keyup.enter="resolve()">
|
||||||
@ -8,17 +8,17 @@
|
|||||||
<form-entry label="Size limit (optional)">
|
<form-entry label="Size limit (optional)">
|
||||||
<input slot="field" class="textfield" type="text" v-model.trim="size" @keyup.enter="resolve()">
|
<input slot="field" class="textfield" type="text" v-model.trim="size" @keyup.enter="resolve()">
|
||||||
</form-entry>
|
</form-entry>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="reject()">Cancel</button>
|
<button class="button" @click="reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import FormEntry from './FormEntry';
|
import FormEntry from '../common/FormEntry';
|
||||||
|
|
||||||
const makeThumbnail = (url, size) => `${url}=s${size}`;
|
const makeThumbnail = (url, size) => `${url}=s${size}`;
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1" role="dialog" aria-label="Publish to WordPress">
|
<modal-inner aria-label="Publish to WordPress">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="wordpress"></icon-provider>
|
<icon-provider provider-id="wordpress"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
@ -30,17 +30,17 @@
|
|||||||
<code>categories</code>, <code>excerpt</code>, <code>author</code>, <code>featuredImage</code>,
|
<code>categories</code>, <code>excerpt</code>, <code>author</code>, <code>featuredImage</code>,
|
||||||
<code>status</code> and <code>date</code> in the <a href="javascript:void(0)" @click="openFileProperties">file properties</a>.
|
<code>status</code> and <code>date</code> in the <a href="javascript:void(0)" @click="openFileProperties">file properties</a>.
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import wordpressProvider from '../../services/providers/wordpressProvider';
|
import wordpressProvider from '../../../services/providers/wordpressProvider';
|
||||||
import modalTemplate from './modalTemplate';
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
data: () => ({
|
data: () => ({
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1" role="dialog" aria-label="Link Zendesk account">
|
<modal-inner aria-label="Link Zendesk account">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="zendesk"></icon-provider>
|
<icon-provider provider-id="zendesk"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
@ -18,17 +18,17 @@
|
|||||||
<a href="https://support.zendesk.com/hc/en-us/articles/203663836" target="_blank"><b>More info</b></a>
|
<a href="https://support.zendesk.com/hc/en-us/articles/203663836" target="_blank"><b>More info</b></a>
|
||||||
</div>
|
</div>
|
||||||
</form-entry>
|
</form-entry>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import modalTemplate from './modalTemplate';
|
import utils from '../../../services/utils';
|
||||||
import utils from '../../services/utils';
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
data: () => ({
|
data: () => ({
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1" role="dialog" aria-label="Publish to Zendesk">
|
<modal-inner aria-label="Publish to Zendesk">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__content">
|
||||||
<div class="modal__image">
|
<div class="modal__image">
|
||||||
<icon-provider provider-id="zendesk"></icon-provider>
|
<icon-provider provider-id="zendesk"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
@ -34,17 +34,17 @@
|
|||||||
<b>ProTip:</b> You can provide values for <code>title</code>, <code>tags</code> and
|
<b>ProTip:</b> You can provide values for <code>title</code>, <code>tags</code> and
|
||||||
<code>status</code> in the <a href="javascript:void(0)" @click="openFileProperties">file properties</a>.
|
<code>status</code> in the <a href="javascript:void(0)" @click="openFileProperties">file properties</a>.
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
<button class="button" @click="resolve()">Ok</button>
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</modal-inner>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import zendeskProvider from '../../services/providers/zendeskProvider';
|
import zendeskProvider from '../../../services/providers/zendeskProvider';
|
||||||
import modalTemplate from './modalTemplate';
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
data: () => ({
|
data: () => ({
|
1
src/components/style.scss
Normal file
1
src/components/style.scss
Normal file
@ -0,0 +1 @@
|
|||||||
|
@import './common/base';
|
@ -1,4 +1,5 @@
|
|||||||
# File properties can contain metadata used for your publications (Wordpress, Blogger...).
|
# File properties can contain metadata used
|
||||||
|
# for your publications (Wordpress, Blogger...).
|
||||||
|
|
||||||
# For example:
|
# For example:
|
||||||
#title: My article
|
#title: My article
|
||||||
@ -26,7 +27,7 @@ extensions:
|
|||||||
sup: true
|
sup: true
|
||||||
table: true
|
table: true
|
||||||
typographer: true
|
typographer: true
|
||||||
# For strict CommonMark:
|
# Enable strict CommonMark:
|
||||||
#abbr: false
|
#abbr: false
|
||||||
#deflist: false
|
#deflist: false
|
||||||
#del: false
|
#del: false
|
||||||
@ -37,6 +38,25 @@ extensions:
|
|||||||
#table: false
|
#table: false
|
||||||
#typographer: false
|
#typographer: false
|
||||||
|
|
||||||
|
# Emoji extension
|
||||||
|
emoji:
|
||||||
|
# Enable support for emojis & emoticons
|
||||||
|
enabled: true
|
||||||
|
# Enable shortcuts like :) :-(
|
||||||
|
shortcuts: false
|
||||||
|
|
||||||
# Katex extension
|
# Katex extension
|
||||||
|
# Render LaTeX mathematical expressions by using:
|
||||||
|
# $...$ for inline formulas
|
||||||
|
# $$...$$ for displayed formulas.
|
||||||
|
# See https://math.meta.stackexchange.com/questions/5020
|
||||||
katex:
|
katex:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
# Mermaid extension
|
||||||
|
# Convert code blocks starting with:
|
||||||
|
# ```mermaid
|
||||||
|
# into diagrams and flowcharts.
|
||||||
|
# See https://mermaidjs.github.io/
|
||||||
|
mermaid:
|
||||||
|
enabled: true
|
||||||
|
@ -7,8 +7,12 @@ export default () => ({
|
|||||||
showExplorer: false,
|
showExplorer: false,
|
||||||
scrollSync: true,
|
scrollSync: true,
|
||||||
focusMode: false,
|
focusMode: false,
|
||||||
|
findCaseSensitive: false,
|
||||||
|
findUseRegexp: false,
|
||||||
sideBarPanel: 'menu',
|
sideBarPanel: 'menu',
|
||||||
htmlExportTemplate: 'styledHtml',
|
htmlExportTemplate: 'styledHtml',
|
||||||
|
pdfExportTemplate: 'styledHtml',
|
||||||
|
pandocExportFormat: 'pdf',
|
||||||
googleDriveFolderId: '',
|
googleDriveFolderId: '',
|
||||||
googleDrivePublishFormat: 'markdown',
|
googleDrivePublishFormat: 'markdown',
|
||||||
googleDrivePublishTemplate: 'styledHtml',
|
googleDrivePublishTemplate: 'styledHtml',
|
||||||
|
@ -10,9 +10,13 @@ editor:
|
|||||||
# Use monospaced font only
|
# Use monospaced font only
|
||||||
monospacedFontOnly: false
|
monospacedFontOnly: false
|
||||||
|
|
||||||
# Keyboard shortcuts (see https://craig.is/killing/mice)
|
# Keyboard shortcuts
|
||||||
|
# See https://craig.is/killing/mice
|
||||||
shortcuts:
|
shortcuts:
|
||||||
mod+s: sync
|
mod+s: sync
|
||||||
|
mod+f: find
|
||||||
|
mod+alt+f: replace
|
||||||
|
mod+g: replace
|
||||||
mod+shift+b: bold
|
mod+shift+b: bold
|
||||||
mod+shift+i: italic
|
mod+shift+i: italic
|
||||||
mod+shift+l: link
|
mod+shift+l: link
|
||||||
@ -34,6 +38,23 @@ shortcuts:
|
|||||||
- '<== '
|
- '<== '
|
||||||
- '⇐ '
|
- '⇐ '
|
||||||
|
|
||||||
|
# Options passed to wkhtmltopdf
|
||||||
|
# See https://wkhtmltopdf.org/usage/wkhtmltopdf.txt
|
||||||
|
wkhtmltopdf:
|
||||||
|
marginTop: 25
|
||||||
|
marginRight: 25
|
||||||
|
marginBottom: 25
|
||||||
|
marginLeft: 25
|
||||||
|
# A3, A4, Legal or Letter
|
||||||
|
pageSize: A4
|
||||||
|
|
||||||
|
# Options passed to pandoc
|
||||||
|
# See https://pandoc.org/MANUAL.html
|
||||||
|
pandoc:
|
||||||
|
highlightStyle: kate
|
||||||
|
toc: true
|
||||||
|
tocDepth: 3
|
||||||
|
|
||||||
# Default content for new files
|
# Default content for new files
|
||||||
newFileContent: |
|
newFileContent: |
|
||||||
|
|
||||||
|
@ -19,6 +19,16 @@ The following JavaScript context will be passed to the template:
|
|||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
As an example:
|
||||||
|
|
||||||
|
<html><body>{{{files.0.content.html}}}</body></html>
|
||||||
|
|
||||||
|
will produce:
|
||||||
|
|
||||||
|
<html><body><p>The file content</p></body></html>
|
||||||
|
|
||||||
|
|
||||||
You can use Handlebars built-in helpers and the custom StackEdit ones:
|
You can use Handlebars built-in helpers and the custom StackEdit ones:
|
||||||
|
|
||||||
{{#tocToHtml files.0.content.toc}}{{/tocToHtml}} will produce a clickable TOC.
|
{{#tocToHtml files.0.content.toc}}{{/tocToHtml}} will produce a clickable TOC.
|
||||||
|
@ -5,12 +5,15 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{{files.0.name}}</title>
|
<title>{{files.0.name}}</title>
|
||||||
<link rel="stylesheet" href="http://app.classeur.io/base-min.css" />
|
<link rel="stylesheet" href="https://stackedit.io/style.css" />
|
||||||
<script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"></script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
{{#if pdf}}
|
||||||
<div class="export-container">{{{files.0.content.html}}}</div>
|
<body class="stackedit stackedit--pdf">
|
||||||
|
{{else}}
|
||||||
|
<body class="stackedit">
|
||||||
|
{{/if}}
|
||||||
|
<div class="stackedit__html">{{{files.0.content.html}}}</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
28
src/data/styledHtmlWithTocTemplate.html
Normal file
28
src/data/styledHtmlWithTocTemplate.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{files.0.name}}</title>
|
||||||
|
<link rel="stylesheet" href="https://stackedit.io/style.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
{{#if pdf}}
|
||||||
|
<body class="stackedit stackedit--pdf">
|
||||||
|
{{else}}
|
||||||
|
<body class="stackedit">
|
||||||
|
{{/if}}
|
||||||
|
<div class="stackedit__left">
|
||||||
|
<div class="stackedit__toc">
|
||||||
|
{{#tocToHtml files.0.content.toc 2}}{{/tocToHtml}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stackedit__right">
|
||||||
|
<div class="stackedit__html">
|
||||||
|
{{{files.0.content.html}}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -1,196 +1,117 @@
|
|||||||
# Welcome to StackEdit!
|
# Welcome to StackEdit!
|
||||||
|
|
||||||
Hey! I'm your first Markdown document in **StackEdit**[^stackedit]. Don't delete me, I'm here to help! I can be recovered anyway in the **Utils** tab of the <i class="icon-cog"></i> **Settings** dialog.
|
Hi! I'm your first Markdown file in **StackEdit**. If you want to learn about StackEdit, you can read me. If you want to play with Markdown, you can edit me. If you have finished with me, you can just create new files by opening the **file explorer** on left corner of the navigation bar.
|
||||||
|
|
||||||
|
|
||||||
## Documents
|
# Files
|
||||||
|
|
||||||
StackEdit stores your documents in your browser, which means all your documents are automatically saved locally and are accessible **offline!**
|
StackEdit stores your files in your browser, which means all your files are automatically saved locally and are accessible **offline!**
|
||||||
|
|
||||||
> **Note:**
|
> **Note:**
|
||||||
|
>
|
||||||
|
> - StackEdit can be used offline thanks to the application cache.
|
||||||
|
> - Your local files are not shared between different browsers or computers unless you use the [synchronization mechanism](#synchronization).
|
||||||
|
> - Clearing your browser's data may **delete all your files!** Make sure you sign in with Google to have all your files and settings backed up and synced.
|
||||||
|
|
||||||
> - StackEdit is accessible offline after the application has been loaded for the first time.
|
## Create files and folders
|
||||||
> - Your local documents are not shared between different browsers or computers.
|
|
||||||
> - Clearing your browser's data may **delete all your local documents!** Make sure your documents are synchronized with **Google Drive** or **Dropbox** (check out the [<i class="icon-refresh"></i> Synchronization](#synchronization) section).
|
|
||||||
|
|
||||||
#### <i class="icon-file"></i> Create a document
|
The file explorer is accessible using the button in left corner of the navigation bar. You can create a new file by clicking the **New file** button in the file explorer. You can also create folders by clicking the **New folder** button.
|
||||||
|
|
||||||
The document panel is accessible using the <i class="icon-folder-open"></i> button in the navigation bar. You can create a new document by clicking <i class="icon-file"></i> **New document** in the document panel.
|
## Switch to another file
|
||||||
|
|
||||||
#### <i class="icon-folder-open"></i> Switch to another document
|
All your files are listed in the file explorer. You can switch from one to another by clicking a file in the list.
|
||||||
|
|
||||||
All your local documents are listed in the document panel. You can switch from one to another by clicking a document in the list or you can toggle documents using <kbd>Ctrl+[</kbd> and <kbd>Ctrl+]</kbd>.
|
## Rename a file
|
||||||
|
|
||||||
#### <i class="icon-pencil"></i> Rename a document
|
You can rename the current file by clicking the file name in the navigation bar or by clicking the **Rename** button in the file explorer.
|
||||||
|
|
||||||
You can rename the current document by clicking the document title in the navigation bar.
|
## Delete a file
|
||||||
|
|
||||||
#### <i class="icon-trash"></i> Delete a document
|
You can delete the current file by clicking the **Remove** button in the file explorer. The file will be moved into the **Trash** folder and automatically deleted after 7 days of inactivity.
|
||||||
|
|
||||||
You can delete the current document by clicking <i class="icon-trash"></i> **Delete document** in the document panel.
|
## Export a file
|
||||||
|
|
||||||
#### <i class="icon-hdd"></i> Export a document
|
You can export the current file by clicking **Export to disk** in the menu. You can choose to export the file as plain Markdown, as HTML using a Handlebars template or as a PDF.
|
||||||
|
|
||||||
You can save the current document to a file by clicking <i class="icon-hdd"></i> **Export to disk** from the <i class="icon-provider-stackedit"></i> menu panel.
|
|
||||||
|
|
||||||
> **Tip:** Check out the [<i class="icon-upload"></i> Publish a document](#publish-a-document) section for a description of the different output formats.
|
# Synchronization
|
||||||
|
|
||||||
|
Synchronization is one of the biggest features of StackEdit. It enables you to synchronize any file in your workspace with other files stored in your **Google Drive**, your **Dropbox** and your **GitHub** accounts. This allows you to keep writing on other devices, collaborate with people you share the file with, integrate easily into your workflow... The synchronization mechanism takes place every minute in the background, downloading, merging, and uploading file modifications.
|
||||||
|
|
||||||
## Synchronization
|
There are two types of synchronization and they can complement each other:
|
||||||
|
|
||||||
StackEdit can be combined with <i class="icon-provider-gdrive"></i> **Google Drive** and <i class="icon-provider-dropbox"></i> **Dropbox** to have your documents saved in the *Cloud*. The synchronization mechanism takes care of uploading your modifications or downloading the latest version of your documents.
|
- The workspace synchronization will sync all your files, folders and settings automatically. This will allow you to fetch your workspace on any other device.
|
||||||
|
> To start syncing your workspace, just sign in with Google in the menu.
|
||||||
|
|
||||||
> **Note:**
|
- The file synchronization will keep one file of the workspace synced with one or multiple files in **Google Drive**, **Dropbox** or **GitHub**.
|
||||||
|
> Before starting to sync files, you must link an account in the **Synchronize** sub-menu.
|
||||||
|
|
||||||
> - Full access to **Google Drive** or **Dropbox** is required to be able to import any document in StackEdit. Permission restrictions can be configured in the settings.
|
## Open a file
|
||||||
> - Imported documents are downloaded in your browser and are not transmitted to a server.
|
|
||||||
> - If you experience problems saving your documents on Google Drive, check and optionally disable browser extensions, such as Disconnect.
|
|
||||||
|
|
||||||
#### <i class="icon-refresh"></i> Open a document
|
You can open a file from **Google Drive**, **Dropbox** or **GitHub** by opening the **Synchronize** sub-menu and clicking **Open from**. Once opened in the workspace, any modification in the file will be automatically synced.
|
||||||
|
|
||||||
You can open a document from <i class="icon-provider-gdrive"></i> **Google Drive** or the <i class="icon-provider-dropbox"></i> **Dropbox** by opening the <i class="icon-refresh"></i> **Synchronize** sub-menu and by clicking **Open from...**. Once opened, any modification in your document will be automatically synchronized with the file in your **Google Drive** / **Dropbox** account.
|
## Save a file
|
||||||
|
|
||||||
#### <i class="icon-refresh"></i> Save a document
|
You can save any file of the workspace to **Google Drive**, **Dropbox** or **GitHub** by opening the **Synchronize** sub-menu and clicking **Save on**. Even if a file in the workspace is already synced, you can save it to another location. StackEdit can sync one file with multiple locations and accounts.
|
||||||
|
|
||||||
You can save any document by opening the <i class="icon-refresh"></i> **Synchronize** sub-menu and by clicking **Save on...**. Even if your document is already synchronized with **Google Drive** or **Dropbox**, you can export it to a another location. StackEdit can synchronize one document with multiple locations and accounts.
|
## Synchronize a file
|
||||||
|
|
||||||
#### <i class="icon-refresh"></i> Synchronize a document
|
Once your file is linked to a synchronized location, StackEdit will periodically synchronize it by downloading/uploading any modification. A merge will be performed if necessary and conflicts will be resolved.
|
||||||
|
|
||||||
Once your document is linked to a <i class="icon-provider-gdrive"></i> **Google Drive** or a <i class="icon-provider-dropbox"></i> **Dropbox** file, StackEdit will periodically (every 3 minutes) synchronize it by downloading/uploading any modification. A merge will be performed if necessary and conflicts will be detected.
|
If you just have modified your file and you want to force syncing, click the **Synchronize now** button in the navigation bar.
|
||||||
|
|
||||||
If you just have modified your document and you want to force the synchronization, click the <i class="icon-refresh"></i> button in the navigation bar.
|
> **Note:** The **Synchronize now** button is disabled if you have no file to synchronize.
|
||||||
|
|
||||||
> **Note:** The <i class="icon-refresh"></i> button is disabled when you have no document to synchronize.
|
## Manage file synchronization
|
||||||
|
|
||||||
#### <i class="icon-refresh"></i> Manage document synchronization
|
Since one file can be synced with multiple locations, you can list and manage synchronized locations by clicking **File synchronization** in the **Synchronize** sub-menu. This allows you to list and remove synchronized locations that are linked to your file.
|
||||||
|
|
||||||
Since one document can be synchronized with multiple locations, you can list and manage synchronized locations by clicking <i class="icon-refresh"></i> **Manage synchronization** in the <i class="icon-refresh"></i> **Synchronize** sub-menu. This will let you remove synchronization locations that are associated to your document.
|
|
||||||
|
|
||||||
> **Note:** If you delete the file from **Google Drive** or from **Dropbox**, the document will no longer be synchronized with that location.
|
# Publication
|
||||||
|
|
||||||
|
Publishing in StackEdit makes it simple for you to publish online your files. Once you're happy with a file, you can publish it to different hosting platforms like **Blogger**, **Dropbox**, **Gist**, **GitHub**, **Google Drive**, **WordPress** and **Zendesk**. With [Handlebars templates](http://handlebarsjs.com/), you have full control over what you export.
|
||||||
|
|
||||||
## Publication
|
> Before starting to publish, you must link an account in the **Publish** sub-menu.
|
||||||
|
|
||||||
Once you are happy with your document, you can publish it on different websites directly from StackEdit. As for now, StackEdit can publish on **Blogger**, **Dropbox**, **Gist**, **GitHub**, **Google Drive**, **Tumblr**, **WordPress** and on any SSH server.
|
## Publish a File
|
||||||
|
|
||||||
#### <i class="icon-upload"></i> Publish a document
|
You can publish your file by opening the **Publish** sub-menu and by clicking **Publish to**. For some locations, you can choose between the following formats:
|
||||||
|
|
||||||
You can publish your document by opening the <i class="icon-upload"></i> **Publish** sub-menu and by choosing a website. In the dialog box, you can choose the publication format:
|
- Markdown: publish the Markdown text on a website that can interpret it (**GitHub** for instance),
|
||||||
|
- HTML: publish the file converted to HTML via a Handlebars template (on a blog for example).
|
||||||
|
|
||||||
- Markdown, to publish the Markdown text on a website that can interpret it (**GitHub** for instance),
|
## Update a publication
|
||||||
- HTML, to publish the document converted into HTML (on a blog for example),
|
|
||||||
- Template, to have a full control of the output.
|
|
||||||
|
|
||||||
> **Note:** The default template is a simple webpage wrapping your document in HTML format. You can customize it in the **Advanced** tab of the <i class="icon-cog"></i> **Settings** dialog.
|
After publishing, StackEdit keeps your file linked to that publication which makes it easy for you to re-publish it. Once you have modified your file and you want to update your publication, click on the **Publish now** button in the navigation bar.
|
||||||
|
|
||||||
#### <i class="icon-upload"></i> Update a publication
|
> **Note:** The **Publish now** button is disabled if your file has not been published yet.
|
||||||
|
|
||||||
After publishing, StackEdit will keep your document linked to that publication which makes it easy for you to update it. Once you have modified your document and you want to update your publication, click on the <i class="icon-upload"></i> button in the navigation bar.
|
## Manage file publication
|
||||||
|
|
||||||
> **Note:** The <i class="icon-upload"></i> button is disabled when your document has not been published yet.
|
Since one file can be published to multiple locations, you can list and manage publish locations by clicking **File publication** in the **Publish** sub-menu. This allows you to list and remove publication locations that are linked to your file.
|
||||||
|
|
||||||
#### <i class="icon-upload"></i> Manage document publication
|
|
||||||
|
|
||||||
Since one document can be published on multiple locations, you can list and manage publish locations by clicking <i class="icon-upload"></i> **Manage publication** in the <i class="icon-provider-stackedit"></i> menu panel. This will let you remove publication locations that are associated to your document.
|
# Markdown extensions
|
||||||
|
|
||||||
> **Note:** If the file has been removed from the website or the blog, the document will no longer be published on that location.
|
StackEdit extends the standard Markdown syntax by adding extra **Markdown extensions**, providing you with some nice features.
|
||||||
|
|
||||||
|
> **ProTip:** You can disable any **Markdown extension** in the **File properties** dialog.
|
||||||
|
|
||||||
## Markdown Extra
|
|
||||||
|
|
||||||
StackEdit supports **Markdown Extra**, which extends **Markdown** syntax with some nice features.
|
## SmartyPants
|
||||||
|
|
||||||
> **Tip:** You can disable any **Markdown Extra** feature in the **Extensions** tab of the <i class="icon-cog"></i> **Settings** dialog.
|
|
||||||
|
|
||||||
> **Note:** You can find more information about **Markdown** syntax [here][2] and **Markdown Extra** extension [here][3].
|
|
||||||
|
|
||||||
|
|
||||||
### Tables
|
|
||||||
|
|
||||||
**Markdown Extra** has a special syntax for tables:
|
|
||||||
|
|
||||||
Item | Value
|
|
||||||
-------- | ---
|
|
||||||
Computer | $1600
|
|
||||||
Phone | $12
|
|
||||||
Pipe | $1
|
|
||||||
|
|
||||||
You can specify column alignment with one or two colons:
|
|
||||||
|
|
||||||
| Item | Value | Qty |
|
|
||||||
| :------- | ----: | :---: |
|
|
||||||
| Computer | $1600 | 5 |
|
|
||||||
| Phone | $12 | 12 |
|
|
||||||
| Pipe | $1 | 234 |
|
|
||||||
|
|
||||||
|
|
||||||
### Definition Lists
|
|
||||||
|
|
||||||
**Markdown Extra** has a special syntax for definition lists too:
|
|
||||||
|
|
||||||
Term 1
|
|
||||||
Term 2
|
|
||||||
: Definition A
|
|
||||||
: Definition B
|
|
||||||
|
|
||||||
Term 3
|
|
||||||
|
|
||||||
: Definition C
|
|
||||||
|
|
||||||
: Definition D
|
|
||||||
|
|
||||||
> part of definition D
|
|
||||||
|
|
||||||
|
|
||||||
### Fenced code blocks
|
|
||||||
|
|
||||||
GitHub's fenced code blocks are also supported with **Highlight.js** syntax highlighting:
|
|
||||||
|
|
||||||
```
|
|
||||||
// Foo
|
|
||||||
var bar = 0;
|
|
||||||
```
|
|
||||||
|
|
||||||
```js
|
|
||||||
var foo = 'bar'; // baz
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Tip:** To use **Prettify** instead of **Highlight.js**, just configure the **Markdown Extra** extension in the <i class="icon-cog"></i> **Settings** dialog.
|
|
||||||
|
|
||||||
> **Note:** You can find more information:
|
|
||||||
|
|
||||||
> - about **Prettify** syntax highlighting [here][5],
|
|
||||||
> - about **Highlight.js** syntax highlighting [here][6].
|
|
||||||
|
|
||||||
|
|
||||||
### Footnotes
|
|
||||||
|
|
||||||
You can create footnotes like this[^footnote].
|
|
||||||
|
|
||||||
[^footnote]: Here is the *text* of the **footnote**.
|
|
||||||
|
|
||||||
|
|
||||||
### SmartyPants
|
|
||||||
|
|
||||||
SmartyPants converts ASCII punctuation characters into "smart" typographic punctuation HTML entities. For example:
|
SmartyPants converts ASCII punctuation characters into "smart" typographic punctuation HTML entities. For example:
|
||||||
|
|
||||||
| |ASCII |HTML |
|
| |ASCII |HTML |
|
||||||
----------------- | ---------------------------- | ------------------
|
|----------------|-------------------------------|-----------------------------|
|
||||||
|Single backticks|`'Isn't this fun?'` |'Isn't this fun?' |
|
|Single backticks|`'Isn't this fun?'` |'Isn't this fun?' |
|
||||||
|Quotes |`"Isn't this fun?"` |"Isn't this fun?" |
|
|Quotes |`"Isn't this fun?"` |"Isn't this fun?" |
|
||||||
|Dashes |`-- is en-dash, --- is em-dash`|-- is en-dash, --- is em-dash|
|
|Dashes |`-- is en-dash, --- is em-dash`|-- is en-dash, --- is em-dash|
|
||||||
|
|
||||||
|
|
||||||
### Table of contents
|
## KaTeX
|
||||||
|
|
||||||
You can insert a table of contents using the marker `[TOC]`:
|
You can render LaTeX mathematical expressions using [KaTeX](https://khan.github.io/KaTeX/):
|
||||||
|
|
||||||
[TOC]
|
|
||||||
|
|
||||||
|
|
||||||
### MathJax
|
|
||||||
|
|
||||||
You can render *LaTeX* mathematical expressions using **MathJax**, as on [math.stackexchange.com][1]:
|
|
||||||
|
|
||||||
The *Gamma function* satisfying $\Gamma(n) = (n-1)!\quad\forall n\in\mathbb N$ is via the Euler integral
|
The *Gamma function* satisfying $\Gamma(n) = (n-1)!\quad\forall n\in\mathbb N$ is via the Euler integral
|
||||||
|
|
||||||
@ -198,55 +119,31 @@ $$
|
|||||||
\Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,.
|
\Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,.
|
||||||
$$
|
$$
|
||||||
|
|
||||||
> **Tip:** To make sure mathematical expressions are rendered properly on your website, include **MathJax** into your template:
|
> You can find more information about **LaTeX** mathematical expressions [here](http://meta.math.stackexchange.com/questions/5020/mathjax-basic-tutorial-and-quick-reference).
|
||||||
|
|
||||||
```
|
|
||||||
<script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"></script>
|
## UML diagrams
|
||||||
|
|
||||||
|
You can render UML diagrams using [Mermaid](https://mermaidjs.github.io/). For example, this will produce a sequence diagram:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
Alice ->> Bob: Hello Bob, how are you?
|
||||||
|
Bob-->>John: How about you John?
|
||||||
|
Bob--x Alice: I am good thanks!
|
||||||
|
Bob-x John: I am good thanks!
|
||||||
|
Note right of John: Bob thinks a long<br/>long time, so long<br/>that the text does<br/>not fit on a row.
|
||||||
|
|
||||||
|
Bob-->Alice: Checking with John...
|
||||||
|
Alice->John: Yes... John, how are you?
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** You can find more information about **LaTeX** mathematical expressions [here][4].
|
And this will produce a flow chart:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
### UML diagrams
|
graph LR
|
||||||
|
A[Square Rect] -- Link text --> B((Circle))
|
||||||
You can also render sequence diagrams like this:
|
A --> C(Round Rect)
|
||||||
|
B --> D{Rhombus}
|
||||||
```sequence
|
C --> D
|
||||||
Alice->Bob: Hello Bob, how are you?
|
|
||||||
Note right of Bob: Bob thinks
|
|
||||||
Bob-->Alice: I am good thanks!
|
|
||||||
```
|
```
|
||||||
|
|
||||||
And flow charts like this:
|
|
||||||
|
|
||||||
```flow
|
|
||||||
st=>start: Start
|
|
||||||
e=>end
|
|
||||||
op=>operation: My Operation
|
|
||||||
cond=>condition: Yes or No?
|
|
||||||
|
|
||||||
st->op->cond
|
|
||||||
cond(yes)->e
|
|
||||||
cond(no)->op
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Note:** You can find more information:
|
|
||||||
|
|
||||||
> - about **Sequence diagrams** syntax [here][7],
|
|
||||||
> - about **Flow charts** syntax [here][8].
|
|
||||||
|
|
||||||
### Support StackEdit
|
|
||||||
|
|
||||||
[![](https://cdn.monetizejs.com/resources/button-32.png)](https://monetizejs.com/authorize?client_id=ESTHdCYOi18iLhhO&summary=true)
|
|
||||||
|
|
||||||
[^stackedit]: [StackEdit](https://stackedit.io/) is a full-featured, open-source Markdown editor based on PageDown, the Markdown library used by Stack Overflow and the other Stack Exchange sites.
|
|
||||||
|
|
||||||
|
|
||||||
[1]: http://math.stackexchange.com/
|
|
||||||
[2]: http://daringfireball.net/projects/markdown/syntax "Markdown"
|
|
||||||
[3]: https://github.com/jmcmanus/pagedown-extra "Pagedown Extra"
|
|
||||||
[4]: http://meta.math.stackexchange.com/questions/5020/mathjax-basic-tutorial-and-quick-reference
|
|
||||||
[5]: https://code.google.com/p/google-code-prettify/
|
|
||||||
[6]: http://highlightjs.org/
|
|
||||||
[7]: http://bramp.github.io/js-sequence-diagrams/
|
|
||||||
[8]: http://adrai.github.io/flowchart.js/
|
|
||||||
|
13
src/extensions/emojiExtension.js
Normal file
13
src/extensions/emojiExtension.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import markdownItEmoji from 'markdown-it-emoji';
|
||||||
|
import extensionSvc from '../services/extensionSvc';
|
||||||
|
|
||||||
|
extensionSvc.onGetOptions((options, properties) => {
|
||||||
|
options.emoji = properties.extensions.emoji.enabled;
|
||||||
|
options.emojiShortcuts = properties.extensions.emoji.shortcuts;
|
||||||
|
});
|
||||||
|
|
||||||
|
extensionSvc.onInitConverter(1, (markdown, options) => {
|
||||||
|
if (options.emoji) {
|
||||||
|
markdown.use(markdownItEmoji, options.emojiShortcuts ? {} : { shortcuts: {} });
|
||||||
|
}
|
||||||
|
});
|
@ -1,2 +1,4 @@
|
|||||||
import './katexExt';
|
import './emojiExtension';
|
||||||
import './markdownExt';
|
import './katexExtension';
|
||||||
|
import './markdownExtension';
|
||||||
|
import './mermaidExtension';
|
||||||
|
@ -178,9 +178,9 @@ extensionSvc.onInitConverter(0, (markdown, options) => {
|
|||||||
|
|
||||||
extensionSvc.onSectionPreview((elt) => {
|
extensionSvc.onSectionPreview((elt) => {
|
||||||
elt.querySelectorAll('.prism').cl_each((prismElt) => {
|
elt.querySelectorAll('.prism').cl_each((prismElt) => {
|
||||||
if (!prismElt.highlighted) {
|
if (!prismElt.highlightedWithPrism) {
|
||||||
Prism.highlightElement(prismElt);
|
Prism.highlightElement(prismElt);
|
||||||
|
prismElt.highlightedWithPrism = true;
|
||||||
}
|
}
|
||||||
prismElt.highlighted = true;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
122
src/extensions/mermaidExtension.js
Normal file
122
src/extensions/mermaidExtension.js
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import mermaidUtils from 'mermaid/src/utils';
|
||||||
|
import flowRenderer from 'mermaid/src/diagrams/flowchart/flowRenderer';
|
||||||
|
import seq from 'mermaid/src/diagrams/sequenceDiagram/sequenceRenderer';
|
||||||
|
import info from 'mermaid/src/diagrams/example/exampleRenderer';
|
||||||
|
import gantt from 'mermaid/src/diagrams/gantt/ganttRenderer';
|
||||||
|
import classRenderer from 'mermaid/src/diagrams/classDiagram/classRenderer';
|
||||||
|
import gitGraphRenderer from 'mermaid/src/diagrams/gitGraph/gitGraphRenderer';
|
||||||
|
import extensionSvc from '../services/extensionSvc';
|
||||||
|
import utils from '../services/utils';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
logLevel: 5,
|
||||||
|
startOnLoad: false,
|
||||||
|
arrowMarkerAbsolute: false,
|
||||||
|
flowchart: {
|
||||||
|
htmlLabels: true,
|
||||||
|
useMaxWidth: true,
|
||||||
|
},
|
||||||
|
sequenceDiagram: {
|
||||||
|
diagramMarginX: 50,
|
||||||
|
diagramMarginY: 10,
|
||||||
|
actorMargin: 50,
|
||||||
|
width: 150,
|
||||||
|
height: 65,
|
||||||
|
boxMargin: 10,
|
||||||
|
boxTextMargin: 5,
|
||||||
|
noteMargin: 10,
|
||||||
|
messageMargin: 35,
|
||||||
|
mirrorActors: true,
|
||||||
|
bottomMarginAdj: 1,
|
||||||
|
useMaxWidth: true,
|
||||||
|
},
|
||||||
|
gantt: {
|
||||||
|
titleTopMargin: 25,
|
||||||
|
barHeight: 20,
|
||||||
|
barGap: 4,
|
||||||
|
topPadding: 50,
|
||||||
|
leftPadding: 75,
|
||||||
|
gridLineStartPadding: 35,
|
||||||
|
fontSize: 11,
|
||||||
|
fontFamily: '"Open-Sans", "sans-serif"',
|
||||||
|
numberSectionStyles: 3,
|
||||||
|
axisFormatter: [
|
||||||
|
// Within a day
|
||||||
|
['%I:%M', d => d.getHours()],
|
||||||
|
// Monday a week
|
||||||
|
['w. %U', d => d.getDay() === 1],
|
||||||
|
// Day within a week (not monday)
|
||||||
|
['%a %d', d => d.getDay() && d.getDate() !== 1],
|
||||||
|
// within a month
|
||||||
|
['%b %d', d => d.getDate() !== 1],
|
||||||
|
// Month
|
||||||
|
['%m-%y', d => d.getMonth()],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
classDiagram: {},
|
||||||
|
gitGraph: {},
|
||||||
|
info: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerElt = document.createElement('div');
|
||||||
|
containerElt.className = 'hidden-rendering-container';
|
||||||
|
document.body.appendChild(containerElt);
|
||||||
|
|
||||||
|
const render = (elt) => {
|
||||||
|
const svgId = `mermaid-svg-${utils.uid()}`;
|
||||||
|
const txt = elt.textContent;
|
||||||
|
containerElt.innerHTML = `<div class="mermaid"><svg xmlns="http://www.w3.org/2000/svg" id="${svgId}"><g></g></svg></div>`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const graphType = mermaidUtils.detectType(txt);
|
||||||
|
switch (graphType) {
|
||||||
|
case 'gitGraph':
|
||||||
|
config.flowchart.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
|
||||||
|
gitGraphRenderer.setConf(config.gitGraph);
|
||||||
|
gitGraphRenderer.draw(txt, svgId, false);
|
||||||
|
break;
|
||||||
|
case 'graph':
|
||||||
|
config.flowchart.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
|
||||||
|
flowRenderer.setConf(config.flowchart);
|
||||||
|
flowRenderer.draw(txt, svgId, false);
|
||||||
|
break;
|
||||||
|
case 'dotGraph':
|
||||||
|
config.flowchart.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
|
||||||
|
flowRenderer.setConf(config.flowchart);
|
||||||
|
flowRenderer.draw(txt, svgId, true);
|
||||||
|
break;
|
||||||
|
case 'sequenceDiagram':
|
||||||
|
config.sequenceDiagram.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
|
||||||
|
seq.setConf(config.sequenceDiagram);
|
||||||
|
seq.draw(txt, svgId);
|
||||||
|
break;
|
||||||
|
case 'gantt':
|
||||||
|
config.gantt.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
|
||||||
|
gantt.setConf(config.gantt);
|
||||||
|
gantt.draw(txt, svgId);
|
||||||
|
break;
|
||||||
|
case 'classDiagram':
|
||||||
|
config.classDiagram.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
|
||||||
|
classRenderer.setConf(config.classDiagram);
|
||||||
|
classRenderer.draw(txt, svgId);
|
||||||
|
break;
|
||||||
|
case 'info':
|
||||||
|
default:
|
||||||
|
config.info.arrowMarkerAbsolute = config.arrowMarkerAbsolute;
|
||||||
|
info.draw(txt, svgId, 'Unknown');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
elt.parentNode.replaceChild(containerElt.firstChild, elt);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
extensionSvc.onGetOptions((options, properties) => {
|
||||||
|
options.mermaid = properties.extensions.mermaid.enabled;
|
||||||
|
});
|
||||||
|
|
||||||
|
extensionSvc.onSectionPreview((elt) => {
|
||||||
|
elt.querySelectorAll('.prism.language-mermaid').cl_each(
|
||||||
|
diagramElt => render(diagramElt.parentNode));
|
||||||
|
});
|
5
src/icons/File.vue
Normal file
5
src/icons/File.vue
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
|
||||||
|
<path d="M13,9V3.5L18.5,9M6,2C4.89,2 4,2.89 4,4V20C4,21.1 4.9,22 6,22H18C19.1,22 20,21.1 20,20V8L14,2H6Z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
@ -40,6 +40,7 @@ import Information from './Information';
|
|||||||
import Alert from './Alert';
|
import Alert from './Alert';
|
||||||
import SignalOff from './SignalOff';
|
import SignalOff from './SignalOff';
|
||||||
import Folder from './Folder';
|
import Folder from './Folder';
|
||||||
|
import File from './File';
|
||||||
import ScrollSync from './ScrollSync';
|
import ScrollSync from './ScrollSync';
|
||||||
import Printer from './Printer';
|
import Printer from './Printer';
|
||||||
import Undo from './Undo';
|
import Undo from './Undo';
|
||||||
@ -87,6 +88,7 @@ Vue.component('iconInformation', Information);
|
|||||||
Vue.component('iconAlert', Alert);
|
Vue.component('iconAlert', Alert);
|
||||||
Vue.component('iconSignalOff', SignalOff);
|
Vue.component('iconSignalOff', SignalOff);
|
||||||
Vue.component('iconFolder', Folder);
|
Vue.component('iconFolder', Folder);
|
||||||
|
Vue.component('iconFile', File);
|
||||||
Vue.component('iconScrollSync', ScrollSync);
|
Vue.component('iconScrollSync', ScrollSync);
|
||||||
Vue.component('iconPrinter', Printer);
|
Vue.component('iconPrinter', Printer);
|
||||||
Vue.component('iconUndo', Undo);
|
Vue.component('iconUndo', Undo);
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import 'babel-polyfill';
|
||||||
|
import 'indexeddbshim/dist/indexeddbshim';
|
||||||
import * as OfflinePluginRuntime from 'offline-plugin/runtime';
|
import * as OfflinePluginRuntime from 'offline-plugin/runtime';
|
||||||
import './extensions/';
|
import './extensions/';
|
||||||
import './services/optional';
|
import './services/optional';
|
||||||
@ -7,6 +9,10 @@ import App from './components/App';
|
|||||||
import store from './store';
|
import store from './store';
|
||||||
import localDbSvc from './services/localDbSvc';
|
import localDbSvc from './services/localDbSvc';
|
||||||
|
|
||||||
|
if (!indexedDB) {
|
||||||
|
throw new Error('Your browser is not supported. Please upgrade to the latest version.');
|
||||||
|
}
|
||||||
|
|
||||||
if (NODE_ENV === 'production') {
|
if (NODE_ENV === 'production') {
|
||||||
OfflinePluginRuntime.install({
|
OfflinePluginRuntime.install({
|
||||||
onUpdateReady: () => {
|
onUpdateReady: () => {
|
||||||
|
@ -103,12 +103,13 @@ function cledit(contentElt, scrollElt, windowParam) {
|
|||||||
selectionMgr.updateCursorCoordinates(true)
|
selectionMgr.updateCursorCoordinates(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceAll(search, replacement) {
|
function replaceAll(search, replacement, startOffset = 0) {
|
||||||
undoMgr.setDefaultMode('single')
|
undoMgr.setDefaultMode('single')
|
||||||
var textContent = getTextContent()
|
var text = getTextContent()
|
||||||
var value = textContent.replace(search, replacement)
|
var subtext = getTextContent().slice(startOffset);
|
||||||
if (value !== textContent) {
|
var value = subtext.replace(search, replacement)
|
||||||
var offset = editor.setContent(value)
|
if (value !== subtext) {
|
||||||
|
var offset = editor.setContent(text.slice(0, startOffset) + value);
|
||||||
selectionMgr.setSelectionStartEnd(offset.end, offset.end)
|
selectionMgr.setSelectionStartEnd(offset.end, offset.end)
|
||||||
selectionMgr.updateCursorCoordinates(true)
|
selectionMgr.updateCursorCoordinates(true)
|
||||||
}
|
}
|
||||||
@ -130,7 +131,7 @@ function cledit(contentElt, scrollElt, windowParam) {
|
|||||||
|
|
||||||
var triggerSpellCheck = debounce(function () {
|
var triggerSpellCheck = debounce(function () {
|
||||||
var selection = editor.$window.getSelection()
|
var selection = editor.$window.getSelection()
|
||||||
if (!selectionMgr.hasFocus || highlighter.isComposing || selectionMgr.selectionStart !== selectionMgr.selectionEnd || !selection.modify) {
|
if (!selectionMgr.hasFocus() || highlighter.isComposing || selectionMgr.selectionStart !== selectionMgr.selectionEnd || !selection.modify) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Hack for Chrome to trigger the spell checker
|
// Hack for Chrome to trigger the spell checker
|
||||||
@ -209,7 +210,8 @@ function cledit(contentElt, scrollElt, windowParam) {
|
|||||||
if (!editor.$window.document.contains(contentElt)) {
|
if (!editor.$window.document.contains(contentElt)) {
|
||||||
watcher.stopWatching()
|
watcher.stopWatching()
|
||||||
editor.$window.removeEventListener('keydown', windowKeydownListener)
|
editor.$window.removeEventListener('keydown', windowKeydownListener)
|
||||||
editor.$window.removeEventListener('mouseup', windowMouseupListener)
|
editor.$window.removeEventListener('mousedown', windowMouseListener)
|
||||||
|
editor.$window.removeEventListener('mouseup', windowMouseListener)
|
||||||
editor.$trigger('destroy')
|
editor.$trigger('destroy')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -226,12 +228,13 @@ function cledit(contentElt, scrollElt, windowParam) {
|
|||||||
editor.$window.addEventListener('keydown', windowKeydownListener, false)
|
editor.$window.addEventListener('keydown', windowKeydownListener, false)
|
||||||
|
|
||||||
// Mouseup can happen outside the editor element
|
// Mouseup can happen outside the editor element
|
||||||
function windowMouseupListener() {
|
function windowMouseListener() {
|
||||||
if (!tryDestroy()) {
|
if (!tryDestroy()) {
|
||||||
selectionMgr.saveSelectionState(true, false)
|
selectionMgr.saveSelectionState(true, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
editor.$window.addEventListener('mouseup', windowMouseupListener)
|
editor.$window.addEventListener('mousedown', windowMouseListener)
|
||||||
|
editor.$window.addEventListener('mouseup', windowMouseListener)
|
||||||
// This can also provoke selection changes and does not fire mouseup event on Chrome/OSX
|
// This can also provoke selection changes and does not fire mouseup event on Chrome/OSX
|
||||||
contentElt.addEventListener('contextmenu', selectionMgr.saveSelectionState.cl_bind(selectionMgr, true, false))
|
contentElt.addEventListener('contextmenu', selectionMgr.saveSelectionState.cl_bind(selectionMgr, true, false))
|
||||||
|
|
||||||
@ -297,12 +300,10 @@ function cledit(contentElt, scrollElt, windowParam) {
|
|||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
contentElt.addEventListener('focus', function () {
|
contentElt.addEventListener('focus', function () {
|
||||||
selectionMgr.hasFocus = true
|
|
||||||
editor.$trigger('focus')
|
editor.$trigger('focus')
|
||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
contentElt.addEventListener('blur', function () {
|
contentElt.addEventListener('blur', function () {
|
||||||
selectionMgr.hasFocus = false
|
|
||||||
editor.$trigger('blur')
|
editor.$trigger('blur')
|
||||||
}, false)
|
}, false)
|
||||||
|
|
||||||
|
@ -49,12 +49,16 @@ function SelectionMgr(editor) {
|
|||||||
this.$trigger('cursorCoordinatesChanged', coordinates)
|
this.$trigger('cursorCoordinatesChanged', coordinates)
|
||||||
}
|
}
|
||||||
if (adjustScroll) {
|
if (adjustScroll) {
|
||||||
var adjustment = scrollElt.clientHeight / 2 * editor.options.getCursorFocusRatio()
|
var scrollEltHeight = scrollElt.clientHeight;
|
||||||
|
if (typeof adjustScroll === 'number') {
|
||||||
|
scrollEltHeight -= adjustScroll
|
||||||
|
}
|
||||||
|
var adjustment = scrollEltHeight / 2 * editor.options.getCursorFocusRatio()
|
||||||
var cursorTop = this.cursorCoordinates.top + this.cursorCoordinates.height / 2
|
var cursorTop = this.cursorCoordinates.top + this.cursorCoordinates.height / 2
|
||||||
// Adjust cursorTop with contentElt position relative to scrollElt
|
// Adjust cursorTop with contentElt position relative to scrollElt
|
||||||
cursorTop += contentElt.getBoundingClientRect().top - scrollElt.getBoundingClientRect().top + scrollElt.scrollTop;
|
cursorTop += contentElt.getBoundingClientRect().top - scrollElt.getBoundingClientRect().top + scrollElt.scrollTop;
|
||||||
var minScrollTop = cursorTop - adjustment
|
var minScrollTop = cursorTop - adjustment
|
||||||
var maxScrollTop = cursorTop + adjustment - scrollElt.clientHeight
|
var maxScrollTop = cursorTop + adjustment - scrollEltHeight
|
||||||
if (scrollElt.scrollTop > minScrollTop) {
|
if (scrollElt.scrollTop > minScrollTop) {
|
||||||
scrollElt.scrollTop = minScrollTop
|
scrollElt.scrollTop = minScrollTop
|
||||||
} else if (scrollElt.scrollTop < maxScrollTop) {
|
} else if (scrollElt.scrollTop < maxScrollTop) {
|
||||||
@ -84,6 +88,10 @@ function SelectionMgr(editor) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.hasFocus = function() {
|
||||||
|
return contentElt === editor.$document.activeElement;
|
||||||
|
}
|
||||||
|
|
||||||
this.restoreSelection = function () {
|
this.restoreSelection = function () {
|
||||||
var min = Math.min(this.selectionStart, this.selectionEnd)
|
var min = Math.min(this.selectionStart, this.selectionEnd)
|
||||||
var max = Math.max(this.selectionStart, this.selectionEnd)
|
var max = Math.max(this.selectionStart, this.selectionEnd)
|
||||||
@ -132,9 +140,9 @@ function SelectionMgr(editor) {
|
|||||||
saveLastSelection()
|
saveLastSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setSelectionStartEnd = function (start, end, focus) {
|
this.setSelectionStartEnd = function (start, end) {
|
||||||
setSelection(start, end)
|
setSelection(start, end)
|
||||||
return focus !== false && this.restoreSelection()
|
return this.hasFocus() && this.restoreSelection()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.saveSelectionState = (function () {
|
this.saveSelectionState = (function () {
|
||||||
@ -230,10 +238,11 @@ function SelectionMgr(editor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
|
var result
|
||||||
|
if (self.hasFocus()) {
|
||||||
var selectionStart = self.selectionStart
|
var selectionStart = self.selectionStart
|
||||||
var selectionEnd = self.selectionEnd
|
var selectionEnd = self.selectionEnd
|
||||||
var selection = editor.$window.getSelection()
|
var selection = editor.$window.getSelection()
|
||||||
var result
|
|
||||||
if (selection.rangeCount > 0) {
|
if (selection.rangeCount > 0) {
|
||||||
var selectionRange = selection.getRangeAt(0)
|
var selectionRange = selection.getRangeAt(0)
|
||||||
var node = selectionRange.startContainer
|
var node = selectionRange.startContainer
|
||||||
@ -275,6 +284,7 @@ function SelectionMgr(editor) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ function mergeText(serverText, clientText, lastMergedText) {
|
|||||||
const lastMergedTextDiffs = diffMatchPatch.diff_main(lastMergedText, intersectionText)
|
const lastMergedTextDiffs = diffMatchPatch.diff_main(lastMergedText, intersectionText)
|
||||||
// Keep only equalities and deletions
|
// Keep only equalities and deletions
|
||||||
.filter(diff => diff[0] !== DiffMatchPatch.DIFF_INSERT);
|
.filter(diff => diff[0] !== DiffMatchPatch.DIFF_INSERT);
|
||||||
diffMatchPatch.diff_cleanupSemantic(serverClientDiffs);
|
diffMatchPatch.diff_cleanupSemantic(lastMergedTextDiffs);
|
||||||
// Make a patch with deletions only
|
// Make a patch with deletions only
|
||||||
const patches = diffMatchPatch.patch_make(lastMergedText, lastMergedTextDiffs);
|
const patches = diffMatchPatch.patch_make(lastMergedText, lastMergedTextDiffs);
|
||||||
// Apply patch to fusion text
|
// Apply patch to fusion text
|
||||||
|
@ -131,24 +131,25 @@ export default {
|
|||||||
initClEditor(opts) {
|
initClEditor(opts) {
|
||||||
const content = store.getters['content/current'];
|
const content = store.getters['content/current'];
|
||||||
if (content) {
|
if (content) {
|
||||||
const options = Object.assign({}, opts);
|
const contentState = store.getters['contentState/current'];
|
||||||
|
const options = Object.assign({
|
||||||
|
selectionStart: contentState.selectionStart,
|
||||||
|
selectionEnd: contentState.selectionEnd,
|
||||||
|
patchHandler: {
|
||||||
|
makePatches,
|
||||||
|
applyPatches,
|
||||||
|
reversePatches,
|
||||||
|
},
|
||||||
|
}, opts);
|
||||||
|
|
||||||
if (contentId !== content.id) {
|
if (contentId !== content.id) {
|
||||||
contentId = content.id;
|
contentId = content.id;
|
||||||
currentPatchableText = diffUtils.makePatchableText(content, markerKeys, markerIdxMap);
|
currentPatchableText = diffUtils.makePatchableText(content, markerKeys, markerIdxMap);
|
||||||
previousPatchableText = currentPatchableText;
|
previousPatchableText = currentPatchableText;
|
||||||
syncDiscussionMarkers(content, false);
|
syncDiscussionMarkers(content, false);
|
||||||
const contentState = store.getters['contentState/current'];
|
|
||||||
options.content = content.text;
|
options.content = content.text;
|
||||||
options.selectionStart = contentState.selectionStart;
|
|
||||||
options.selectionEnd = contentState.selectionEnd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
options.patchHandler = {
|
|
||||||
makePatches,
|
|
||||||
applyPatches,
|
|
||||||
reversePatches,
|
|
||||||
};
|
|
||||||
clEditor.init(options);
|
clEditor.init(options);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -208,6 +208,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
let insertBeforePreviewElt = this.previewElt.firstChild;
|
let insertBeforePreviewElt = this.previewElt.firstChild;
|
||||||
let insertBeforeTocElt = this.tocElt.firstChild;
|
let insertBeforeTocElt = this.tocElt.firstChild;
|
||||||
let previewHtml = '';
|
let previewHtml = '';
|
||||||
|
let loadingImages = [];
|
||||||
this.conversionCtx.htmlSectionDiff.forEach((item) => {
|
this.conversionCtx.htmlSectionDiff.forEach((item) => {
|
||||||
for (let i = 0; i < item[1].length; i += 1) {
|
for (let i = 0; i < item[1].length; i += 1) {
|
||||||
const section = this.conversionCtx.sectionList[sectionIdx];
|
const section = this.conversionCtx.sectionList[sectionIdx];
|
||||||
@ -218,9 +219,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
newSectionDescList.push(sectionDesc);
|
newSectionDescList.push(sectionDesc);
|
||||||
previewHtml += sectionDesc.html;
|
previewHtml += sectionDesc.html;
|
||||||
sectionIdx += 1;
|
sectionIdx += 1;
|
||||||
insertBeforePreviewElt.classList.remove('modified');
|
|
||||||
insertBeforePreviewElt = insertBeforePreviewElt.nextSibling;
|
insertBeforePreviewElt = insertBeforePreviewElt.nextSibling;
|
||||||
insertBeforeTocElt.classList.remove('modified');
|
|
||||||
insertBeforeTocElt = insertBeforeTocElt.nextSibling;
|
insertBeforeTocElt = insertBeforeTocElt.nextSibling;
|
||||||
} else if (item[0] === -1) {
|
} else if (item[0] === -1) {
|
||||||
sectionDescIdx += 1;
|
sectionDescIdx += 1;
|
||||||
@ -236,7 +235,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
|
|
||||||
// Create preview section element
|
// Create preview section element
|
||||||
sectionPreviewElt = document.createElement('div');
|
sectionPreviewElt = document.createElement('div');
|
||||||
sectionPreviewElt.className = 'cl-preview-section modified';
|
sectionPreviewElt.className = 'cl-preview-section';
|
||||||
sectionPreviewElt.innerHTML = html;
|
sectionPreviewElt.innerHTML = html;
|
||||||
if (insertBeforePreviewElt) {
|
if (insertBeforePreviewElt) {
|
||||||
this.previewElt.insertBefore(sectionPreviewElt, insertBeforePreviewElt);
|
this.previewElt.insertBefore(sectionPreviewElt, insertBeforePreviewElt);
|
||||||
@ -244,10 +243,14 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
this.previewElt.appendChild(sectionPreviewElt);
|
this.previewElt.appendChild(sectionPreviewElt);
|
||||||
}
|
}
|
||||||
extensionSvc.sectionPreview(sectionPreviewElt, this.options);
|
extensionSvc.sectionPreview(sectionPreviewElt, this.options);
|
||||||
|
loadingImages = [
|
||||||
|
...loadingImages,
|
||||||
|
...Array.prototype.slice.call(sectionPreviewElt.getElementsByTagName('img')),
|
||||||
|
];
|
||||||
|
|
||||||
// Create TOC section element
|
// Create TOC section element
|
||||||
sectionTocElt = document.createElement('div');
|
sectionTocElt = document.createElement('div');
|
||||||
sectionTocElt.className = 'cl-toc-section modified';
|
sectionTocElt.className = 'cl-toc-section';
|
||||||
const headingElt = sectionPreviewElt.querySelector('h1, h2, h3, h4, h5, h6');
|
const headingElt = sectionPreviewElt.querySelector('h1, h2, h3, h4, h5, h6');
|
||||||
if (headingElt) {
|
if (headingElt) {
|
||||||
const clonedElt = headingElt.cloneNode(true);
|
const clonedElt = headingElt.cloneNode(true);
|
||||||
@ -278,8 +281,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
]('toc-tab--empty');
|
]('toc-tab--empty');
|
||||||
|
|
||||||
// Run preview async operations (image loading, mathjax...)
|
// Run preview async operations (image loading, mathjax...)
|
||||||
const loadingImages = this.previewElt.querySelectorAll('.cl-preview-section.modified img');
|
const loadedPromises = loadingImages.map(imgElt => new Promise((resolve) => {
|
||||||
const loadedPromises = loadingImages.cl_map(imgElt => new Promise((resolve) => {
|
|
||||||
if (!imgElt.src) {
|
if (!imgElt.src) {
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
@ -392,7 +394,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||||||
const editorEndOffset = editorSvc.getEditorOffset(previewSelectionEndOffset);
|
const editorEndOffset = editorSvc.getEditorOffset(previewSelectionEndOffset);
|
||||||
if (editorStartOffset !== undefined && editorEndOffset !== undefined) {
|
if (editorStartOffset !== undefined && editorEndOffset !== undefined) {
|
||||||
editorEngineSvc.clEditor.selectionMgr.setSelectionStartEnd(
|
editorEngineSvc.clEditor.selectionMgr.setSelectionStartEnd(
|
||||||
editorStartOffset, editorEndOffset, false);
|
editorStartOffset, editorEndOffset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
editorSvc.previewSelectionRange = range;
|
editorSvc.previewSelectionRange = range;
|
||||||
|
@ -34,6 +34,10 @@ function groupHeadings(headings, level = 1) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const containerElt = document.createElement('div');
|
||||||
|
containerElt.className = 'hidden-rendering-container';
|
||||||
|
document.body.appendChild(containerElt);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
* Apply the template to the file content
|
* Apply the template to the file content
|
||||||
@ -41,7 +45,7 @@ export default {
|
|||||||
applyTemplate(fileId, template = {
|
applyTemplate(fileId, template = {
|
||||||
value: '{{{files.0.content.text}}}',
|
value: '{{{files.0.content.text}}}',
|
||||||
helpers: '',
|
helpers: '',
|
||||||
}) {
|
}, pdf = false) {
|
||||||
const file = store.state.file.itemMap[fileId];
|
const file = store.state.file.itemMap[fileId];
|
||||||
return localDbSvc.loadItem(`${fileId}/content`)
|
return localDbSvc.loadItem(`${fileId}/content`)
|
||||||
.then((content) => {
|
.then((content) => {
|
||||||
@ -51,19 +55,19 @@ export default {
|
|||||||
const parsingCtx = markdownConversionSvc.parseSections(converter, content.text);
|
const parsingCtx = markdownConversionSvc.parseSections(converter, content.text);
|
||||||
const conversionCtx = markdownConversionSvc.convert(parsingCtx);
|
const conversionCtx = markdownConversionSvc.convert(parsingCtx);
|
||||||
const html = conversionCtx.htmlSectionList.map(htmlSanitizer.sanitizeHtml).join('');
|
const html = conversionCtx.htmlSectionList.map(htmlSanitizer.sanitizeHtml).join('');
|
||||||
const elt = document.createElement('div');
|
containerElt.innerHTML = html;
|
||||||
elt.innerHTML = html;
|
extensionSvc.sectionPreview(containerElt, options);
|
||||||
|
|
||||||
// Unwrap tables
|
// Unwrap tables
|
||||||
elt.querySelectorAll('.table-wrapper').cl_each((wrapperElt) => {
|
containerElt.querySelectorAll('.table-wrapper').cl_each((wrapperElt) => {
|
||||||
while (wrapperElt.firstChild) {
|
while (wrapperElt.firstChild) {
|
||||||
wrapperElt.parentNode.appendChild(wrapperElt.firstChild);
|
wrapperElt.parentNode.insertBefore(wrapperElt.firstChild, wrapperElt.nextSibling);
|
||||||
}
|
}
|
||||||
wrapperElt.parentNode.removeChild(wrapperElt);
|
wrapperElt.parentNode.removeChild(wrapperElt);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Make TOC
|
// Make TOC
|
||||||
const headings = elt.querySelectorAll('h1,h2,h3,h4,h5,h6').cl_map(headingElt => ({
|
const headings = containerElt.querySelectorAll('h1,h2,h3,h4,h5,h6').cl_map(headingElt => ({
|
||||||
title: headingElt.textContent,
|
title: headingElt.textContent,
|
||||||
anchor: headingElt.id,
|
anchor: headingElt.id,
|
||||||
level: parseInt(headingElt.tagName.slice(1), 10),
|
level: parseInt(headingElt.tagName.slice(1), 10),
|
||||||
@ -71,17 +75,19 @@ export default {
|
|||||||
}));
|
}));
|
||||||
const toc = groupHeadings(headings);
|
const toc = groupHeadings(headings);
|
||||||
const view = {
|
const view = {
|
||||||
|
pdf,
|
||||||
files: [{
|
files: [{
|
||||||
name: file.name,
|
name: file.name,
|
||||||
content: {
|
content: {
|
||||||
text: content.text,
|
text: content.text,
|
||||||
properties,
|
properties,
|
||||||
yamlProperties: content.properties,
|
yamlProperties: content.properties,
|
||||||
html: elt.innerHTML,
|
html: containerElt.innerHTML,
|
||||||
toc,
|
toc,
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
containerElt.innerHTML = '';
|
||||||
|
|
||||||
// Run template conversion in a Worker to prevent attacks from helpers
|
// Run template conversion in a Worker to prevent attacks from helpers
|
||||||
const worker = new TemplateWorker();
|
const worker = new TemplateWorker();
|
||||||
@ -111,8 +117,8 @@ export default {
|
|||||||
exportToDisk(fileId, type, template) {
|
exportToDisk(fileId, type, template) {
|
||||||
const file = store.state.file.itemMap[fileId];
|
const file = store.state.file.itemMap[fileId];
|
||||||
return this.applyTemplate(fileId, template)
|
return this.applyTemplate(fileId, template)
|
||||||
.then((res) => {
|
.then((html) => {
|
||||||
const blob = new Blob([res], {
|
const blob = new Blob([html], {
|
||||||
type: 'text/plain;charset=utf-8',
|
type: 'text/plain;charset=utf-8',
|
||||||
});
|
});
|
||||||
FileSaver.saveAs(blob, `${file.name}.${type}`);
|
FileSaver.saveAs(blob, `${file.name}.${type}`);
|
||||||
|
@ -1,21 +1,18 @@
|
|||||||
import 'babel-polyfill';
|
|
||||||
import 'indexeddbshim/dist/indexeddbshim';
|
|
||||||
import FileSaver from 'file-saver';
|
import FileSaver from 'file-saver';
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
import welcomeFile from '../data/welcomeFile.md';
|
import welcomeFile from '../data/welcomeFile.md';
|
||||||
|
|
||||||
const indexedDB = window.indexedDB;
|
|
||||||
const dbVersion = 1;
|
const dbVersion = 1;
|
||||||
const dbVersionKey = `${utils.workspaceId}/localDbVersion`;
|
const dbVersionKey = `${utils.workspaceId}/localDbVersion`;
|
||||||
const dbStoreName = 'objects';
|
const dbStoreName = 'objects';
|
||||||
const exportBackup = utils.queryParams.exportBackup;
|
const exportBackup = utils.queryParams.exportBackup;
|
||||||
|
if (exportBackup) {
|
||||||
if (!indexedDB) {
|
location.hash = '';
|
||||||
throw new Error('Your browser is not supported. Please upgrade to the latest version.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteMarkerMaxAge = 1000;
|
const deleteMarkerMaxAge = 1000;
|
||||||
|
const checkSponsorshipAfter = (5 * 60 * 1000) + (30 * 1000); // tokenExpirationMargin + 30 sec
|
||||||
|
|
||||||
class Connection {
|
class Connection {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -328,6 +325,15 @@ localDbSvc.sync()
|
|||||||
// Set the ready flag
|
// Set the ready flag
|
||||||
store.commit('setReady');
|
store.commit('setReady');
|
||||||
|
|
||||||
|
// Save welcome file content hash if not done already
|
||||||
|
const hash = utils.hash(welcomeFile);
|
||||||
|
const welcomeFileHashes = store.getters['data/welcomeFileHashes'];
|
||||||
|
if (!welcomeFileHashes[hash]) {
|
||||||
|
store.dispatch('data/patchWelcomeFileHashes', {
|
||||||
|
[hash]: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// If app was last opened 7 days ago and synchronization is off
|
// If app was last opened 7 days ago and synchronization is off
|
||||||
if (!store.getters['data/loginToken'] &&
|
if (!store.getters['data/loginToken'] &&
|
||||||
(utils.lastOpened + utils.cleanTrashAfter < Date.now())
|
(utils.lastOpened + utils.cleanTrashAfter < Date.now())
|
||||||
@ -338,6 +344,21 @@ localDbSvc.sync()
|
|||||||
.forEach(file => store.dispatch('deleteFile', file.id));
|
.forEach(file => store.dispatch('deleteFile', file.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable sponsorship
|
||||||
|
if (utils.queryParams.paymentSuccess) {
|
||||||
|
location.hash = '';
|
||||||
|
store.dispatch('modal/paymentSuccess');
|
||||||
|
const loginToken = store.getters['data/loginToken'];
|
||||||
|
// Force check sponsorship after a few seconds
|
||||||
|
const currentDate = Date.now();
|
||||||
|
if (loginToken && loginToken.expiresOn > currentDate - checkSponsorshipAfter) {
|
||||||
|
store.dispatch('data/setGoogleToken', {
|
||||||
|
...loginToken,
|
||||||
|
expiresOn: currentDate - checkSponsorshipAfter,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// watch file changing
|
// watch file changing
|
||||||
store.watch(
|
store.watch(
|
||||||
() => store.getters['file/current'].id,
|
() => store.getters['file/current'].id,
|
||||||
|
@ -93,6 +93,7 @@ export default {
|
|||||||
resolve({
|
resolve({
|
||||||
accessToken: data.access_token,
|
accessToken: data.access_token,
|
||||||
code: data.code,
|
code: data.code,
|
||||||
|
idToken: data.id_token,
|
||||||
expiresIn: data.expires_in,
|
expiresIn: data.expires_in,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -107,7 +108,7 @@ export default {
|
|||||||
},
|
},
|
||||||
request(configParam, offlineCheck = false) {
|
request(configParam, offlineCheck = false) {
|
||||||
let retryAfter = 500; // 500 ms
|
let retryAfter = 500; // 500 ms
|
||||||
const maxRetryAfter = 30 * 1000; // 30 sec
|
const maxRetryAfter = 10 * 1000; // 10 sec
|
||||||
const config = Object.assign({}, configParam);
|
const config = Object.assign({}, configParam);
|
||||||
config.timeout = config.timeout || networkTimeout;
|
config.timeout = config.timeout || networkTimeout;
|
||||||
config.headers = Object.assign({}, config.headers);
|
config.headers = Object.assign({}, config.headers);
|
||||||
@ -151,9 +152,9 @@ export default {
|
|||||||
const result = {
|
const result = {
|
||||||
status: xhr.status,
|
status: xhr.status,
|
||||||
headers: parseHeaders(xhr),
|
headers: parseHeaders(xhr),
|
||||||
body: xhr.responseText,
|
body: config.blob ? xhr.response : xhr.responseText,
|
||||||
};
|
};
|
||||||
if (!config.raw) {
|
if (!config.raw && !config.blob) {
|
||||||
try {
|
try {
|
||||||
result.body = JSON.parse(result.body);
|
result.body = JSON.parse(result.body);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -197,6 +198,9 @@ export default {
|
|||||||
xhr.setRequestHeader(key, `${value}`);
|
xhr.setRequestHeader(key, `${value}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (config.blob) {
|
||||||
|
xhr.responseType = 'blob';
|
||||||
|
}
|
||||||
xhr.send(config.body || null);
|
xhr.send(config.body || null);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -227,17 +231,18 @@ function checkOffline() {
|
|||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
let timeout;
|
let timeout;
|
||||||
const cleaner = (cb, res) => () => {
|
let clean = (cb) => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
cb(res);
|
|
||||||
document.head.removeChild(script);
|
document.head.removeChild(script);
|
||||||
|
clean = () => {}; // Prevent from cleaning several times
|
||||||
|
cb();
|
||||||
};
|
};
|
||||||
script.onload = cleaner(resolve);
|
script.onload = () => clean(resolve);
|
||||||
script.onerror = cleaner(reject);
|
script.onerror = () => clean(reject);
|
||||||
script.src = `https://apis.google.com/js/api.js?${Date.now()}`;
|
script.src = `https://apis.google.com/js/api.js?${Date.now()}`;
|
||||||
try {
|
try {
|
||||||
document.head.appendChild(script); // This can fail with bad network
|
document.head.appendChild(script); // This can fail with bad network
|
||||||
timeout = setTimeout(cleaner(reject), networkTimeout);
|
timeout = setTimeout(() => clean(reject), networkTimeout);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,15 @@ const pagedownHandler = name => () => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findReplaceOpener = type => () => {
|
||||||
|
store.dispatch('findReplace/open', {
|
||||||
|
type,
|
||||||
|
findText: editorEngineSvc.clEditor.selectionMgr.hasFocus() &&
|
||||||
|
editorEngineSvc.clEditor.selectionMgr.getSelectedText(),
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const methods = {
|
const methods = {
|
||||||
bold: pagedownHandler('bold'),
|
bold: pagedownHandler('bold'),
|
||||||
italic: pagedownHandler('italic'),
|
italic: pagedownHandler('italic'),
|
||||||
@ -30,13 +39,15 @@ const methods = {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
find: findReplaceOpener('find'),
|
||||||
|
replace: findReplaceOpener('replace'),
|
||||||
expand(param1, param2) {
|
expand(param1, param2) {
|
||||||
const text = `${param1 || ''}`;
|
const text = `${param1 || ''}`;
|
||||||
const replacement = `${param2 || ''}`;
|
const replacement = `${param2 || ''}`;
|
||||||
if (text && replacement) {
|
if (text && replacement) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const selectionMgr = editorEngineSvc.clEditor.selectionMgr;
|
const selectionMgr = editorEngineSvc.clEditor.selectionMgr;
|
||||||
let offset = editorEngineSvc.clEditor.selectionMgr.selectionStart;
|
let offset = selectionMgr.selectionStart;
|
||||||
if (offset === selectionMgr.selectionEnd) {
|
if (offset === selectionMgr.selectionEnd) {
|
||||||
const range = selectionMgr.createRange(offset - text.length, offset);
|
const range = selectionMgr.createRange(offset - text.length, offset);
|
||||||
if (`${range}` === text) {
|
if (`${range}` === text) {
|
||||||
|
@ -2,6 +2,7 @@ import store from '../../store';
|
|||||||
import githubHelper from './helpers/githubHelper';
|
import githubHelper from './helpers/githubHelper';
|
||||||
import providerUtils from './providerUtils';
|
import providerUtils from './providerUtils';
|
||||||
import providerRegistry from './providerRegistry';
|
import providerRegistry from './providerRegistry';
|
||||||
|
import utils from '../utils';
|
||||||
|
|
||||||
const savedSha = {};
|
const savedSha = {};
|
||||||
|
|
||||||
@ -65,6 +66,55 @@ export default providerRegistry.register({
|
|||||||
})
|
})
|
||||||
.then(() => publishLocation);
|
.then(() => publishLocation);
|
||||||
},
|
},
|
||||||
|
openFile(token, syncLocation) {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => {
|
||||||
|
if (providerUtils.openFileWithLocation(store.getters['syncLocation/items'], syncLocation)) {
|
||||||
|
// File exists and has just been opened. Next...
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Download content from GitHub and create the file
|
||||||
|
return this.downloadContent(token, syncLocation)
|
||||||
|
.then((content) => {
|
||||||
|
const id = utils.uid();
|
||||||
|
delete content.history;
|
||||||
|
store.commit('content/setItem', {
|
||||||
|
...content,
|
||||||
|
id: `${id}/content`,
|
||||||
|
});
|
||||||
|
let name = syncLocation.path;
|
||||||
|
const slashPos = name.lastIndexOf('/');
|
||||||
|
if (slashPos > -1 && slashPos < name.length - 1) {
|
||||||
|
name = name.slice(slashPos + 1);
|
||||||
|
}
|
||||||
|
const dotPos = name.lastIndexOf('.');
|
||||||
|
if (dotPos > 0 && slashPos < name.length) {
|
||||||
|
name = name.slice(0, dotPos);
|
||||||
|
}
|
||||||
|
store.commit('file/setItem', {
|
||||||
|
id,
|
||||||
|
name: utils.sanitizeName(name),
|
||||||
|
parentId: store.getters['file/current'].parentId,
|
||||||
|
});
|
||||||
|
store.commit('syncLocation/setItem', {
|
||||||
|
...syncLocation,
|
||||||
|
id: utils.uid(),
|
||||||
|
fileId: id,
|
||||||
|
});
|
||||||
|
store.commit('file/setCurrentId', id);
|
||||||
|
store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from GitHub.`);
|
||||||
|
}, () => {
|
||||||
|
store.dispatch('notification/error', `Could not open file ${syncLocation.path}.`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
parseRepoUrl(url) {
|
||||||
|
const parsedRepo = url.match(/[/:]?([^/:]+)\/([^/]+?)(?:\.git|\/)?$/);
|
||||||
|
return parsedRepo && {
|
||||||
|
owner: parsedRepo[1],
|
||||||
|
repo: parsedRepo[2],
|
||||||
|
};
|
||||||
|
},
|
||||||
makeLocation(token, owner, repo, branch, path) {
|
makeLocation(token, owner, repo, branch, path) {
|
||||||
return {
|
return {
|
||||||
providerId: this.id,
|
providerId: this.id,
|
||||||
|
@ -19,6 +19,16 @@ const photosScopes = ['https://www.googleapis.com/auth/photos'];
|
|||||||
|
|
||||||
const libraries = ['picker'];
|
const libraries = ['picker'];
|
||||||
|
|
||||||
|
const checkIdToken = (idToken) => {
|
||||||
|
try {
|
||||||
|
const token = idToken.split('.');
|
||||||
|
const payload = JSON.parse(utils.decodeBase64(token[1]));
|
||||||
|
return payload.aud === clientId && Date.now() + tokenExpirationMargin < payload.exp * 1000;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
request(token, options) {
|
request(token, options) {
|
||||||
return networkSvc.request({
|
return networkSvc.request({
|
||||||
@ -37,7 +47,7 @@ export default {
|
|||||||
expiresOn: 0,
|
expiresOn: 0,
|
||||||
});
|
});
|
||||||
// Refresh token and retry
|
// Refresh token and retry
|
||||||
return this.refreshToken(token.scopes, token)
|
return this.refreshToken(token, token.scopes)
|
||||||
.then(refreshedToken => this.request(refreshedToken, options));
|
.then(refreshedToken => this.request(refreshedToken, options));
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
@ -115,11 +125,12 @@ export default {
|
|||||||
return networkSvc.startOauth2(
|
return networkSvc.startOauth2(
|
||||||
'https://accounts.google.com/o/oauth2/v2/auth', {
|
'https://accounts.google.com/o/oauth2/v2/auth', {
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
response_type: 'token',
|
response_type: 'token id_token',
|
||||||
scope: ['openid', ...scopes].join(' '), // Need openid for user info
|
scope: ['openid', ...scopes].join(' '),
|
||||||
hd: appsDomain,
|
hd: appsDomain,
|
||||||
login_hint: sub,
|
login_hint: sub,
|
||||||
prompt: silent ? 'none' : null,
|
prompt: silent ? 'none' : null,
|
||||||
|
nonce: utils.uid(),
|
||||||
}, silent)
|
}, silent)
|
||||||
// Call the token info endpoint
|
// Call the token info endpoint
|
||||||
.then(data => networkSvc.request({
|
.then(data => networkSvc.request({
|
||||||
@ -142,9 +153,11 @@ export default {
|
|||||||
scopes,
|
scopes,
|
||||||
accessToken: data.accessToken,
|
accessToken: data.accessToken,
|
||||||
expiresOn: Date.now() + (data.expiresIn * 1000),
|
expiresOn: Date.now() + (data.expiresIn * 1000),
|
||||||
|
idToken: data.idToken,
|
||||||
sub: `${res.body.sub}`,
|
sub: `${res.body.sub}`,
|
||||||
isLogin: !store.getters['data/loginToken'] &&
|
isLogin: !store.getters['data/loginToken'] &&
|
||||||
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
|
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
|
||||||
|
isSponsor: false,
|
||||||
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
|
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
|
||||||
scopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1,
|
scopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1,
|
||||||
isBlogger: scopes.indexOf('https://www.googleapis.com/auth/blogger') !== -1,
|
isBlogger: scopes.indexOf('https://www.googleapis.com/auth/blogger') !== -1,
|
||||||
@ -163,19 +176,33 @@ export default {
|
|||||||
// That's no problem, token will be refreshed later with merged scopes.
|
// That's no problem, token will be refreshed later with merged scopes.
|
||||||
// Save flags
|
// Save flags
|
||||||
token.isLogin = existingToken.isLogin || token.isLogin;
|
token.isLogin = existingToken.isLogin || token.isLogin;
|
||||||
|
token.isSponsor = existingToken.isSponsor;
|
||||||
token.isDrive = existingToken.isDrive || token.isDrive;
|
token.isDrive = existingToken.isDrive || token.isDrive;
|
||||||
token.isBlogger = existingToken.isBlogger || token.isBlogger;
|
token.isBlogger = existingToken.isBlogger || token.isBlogger;
|
||||||
token.isPhotos = existingToken.isPhotos || token.isPhotos;
|
token.isPhotos = existingToken.isPhotos || token.isPhotos;
|
||||||
token.driveFullAccess = existingToken.driveFullAccess || token.driveFullAccess;
|
token.driveFullAccess = existingToken.driveFullAccess || token.driveFullAccess;
|
||||||
// Save nextPageToken
|
|
||||||
token.nextPageToken = existingToken.nextPageToken;
|
token.nextPageToken = existingToken.nextPageToken;
|
||||||
}
|
}
|
||||||
|
return token.isLogin && networkSvc.request({
|
||||||
|
method: 'GET',
|
||||||
|
url: 'userInfo',
|
||||||
|
params: {
|
||||||
|
idToken: token.idToken,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
token.isSponsor = res.body.sponsorUntil > Date.now();
|
||||||
|
}, () => {
|
||||||
|
// Ignore error
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
// Add token to googleTokens
|
// Add token to googleTokens
|
||||||
store.dispatch('data/setGoogleToken', token);
|
store.dispatch('data/setGoogleToken', token);
|
||||||
return token;
|
return token;
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
refreshToken(scopes, token) {
|
refreshToken(token, scopes = []) {
|
||||||
const sub = token.sub;
|
const sub = token.sub;
|
||||||
const lastToken = store.getters['data/googleTokens'][sub];
|
const lastToken = store.getters['data/googleTokens'][sub];
|
||||||
const mergedScopes = [...new Set([
|
const mergedScopes = [...new Set([
|
||||||
@ -185,8 +212,13 @@ export default {
|
|||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (mergedScopes.length === lastToken.scopes.length &&
|
if (
|
||||||
lastToken.expiresOn > Date.now() + tokenExpirationMargin
|
// If we already have permissions for the requested scopes
|
||||||
|
mergedScopes.length === lastToken.scopes.length &&
|
||||||
|
// And lastToken is not expired
|
||||||
|
lastToken.expiresOn > Date.now() + tokenExpirationMargin &&
|
||||||
|
// And in case of a login token, ID token is still valid
|
||||||
|
(!lastToken.isLogin || checkIdToken(lastToken.idToken))
|
||||||
) {
|
) {
|
||||||
return lastToken;
|
return lastToken;
|
||||||
}
|
}
|
||||||
@ -222,6 +254,16 @@ export default {
|
|||||||
google = window.google;
|
google = window.google;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
getSponsorship(token) {
|
||||||
|
return this.refreshToken(token)
|
||||||
|
.then(refreshedToken => networkSvc.request({
|
||||||
|
method: 'GET',
|
||||||
|
url: 'userInfo',
|
||||||
|
params: {
|
||||||
|
idToken: refreshedToken.idToken,
|
||||||
|
},
|
||||||
|
}, true));
|
||||||
|
},
|
||||||
signin() {
|
signin() {
|
||||||
return this.startOauth2(driveAppDataScopes);
|
return this.startOauth2(driveAppDataScopes);
|
||||||
},
|
},
|
||||||
@ -238,7 +280,7 @@ export default {
|
|||||||
const result = {
|
const result = {
|
||||||
changes: [],
|
changes: [],
|
||||||
};
|
};
|
||||||
return this.refreshToken(driveAppDataScopes, token)
|
return this.refreshToken(token, driveAppDataScopes)
|
||||||
.then((refreshedToken) => {
|
.then((refreshedToken) => {
|
||||||
const getPage = (pageToken = '1') => this.request(refreshedToken, {
|
const getPage = (pageToken = '1') => this.request(refreshedToken, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@ -262,25 +304,25 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
uploadFile(token, name, parents, media, mediaType, fileId, ifNotTooLate) {
|
uploadFile(token, name, parents, media, mediaType, fileId, ifNotTooLate) {
|
||||||
return this.refreshToken(getDriveScopes(token), token)
|
return this.refreshToken(token, getDriveScopes(token))
|
||||||
.then(refreshedToken => this.uploadFileInternal(
|
.then(refreshedToken => this.uploadFileInternal(
|
||||||
refreshedToken, name, parents, media, mediaType, fileId, ifNotTooLate));
|
refreshedToken, name, parents, media, mediaType, fileId, ifNotTooLate));
|
||||||
},
|
},
|
||||||
uploadAppDataFile(token, name, parents, media, fileId, ifNotTooLate) {
|
uploadAppDataFile(token, name, parents, media, fileId, ifNotTooLate) {
|
||||||
return this.refreshToken(driveAppDataScopes, token)
|
return this.refreshToken(token, driveAppDataScopes)
|
||||||
.then(refreshedToken => this.uploadFileInternal(
|
.then(refreshedToken => this.uploadFileInternal(
|
||||||
refreshedToken, name, parents, media, undefined, fileId, ifNotTooLate));
|
refreshedToken, name, parents, media, undefined, fileId, ifNotTooLate));
|
||||||
},
|
},
|
||||||
downloadFile(token, id) {
|
downloadFile(token, id) {
|
||||||
return this.refreshToken(getDriveScopes(token), token)
|
return this.refreshToken(token, getDriveScopes(token))
|
||||||
.then(refreshedToken => this.downloadFileInternal(refreshedToken, id));
|
.then(refreshedToken => this.downloadFileInternal(refreshedToken, id));
|
||||||
},
|
},
|
||||||
downloadAppDataFile(token, id) {
|
downloadAppDataFile(token, id) {
|
||||||
return this.refreshToken(driveAppDataScopes, token)
|
return this.refreshToken(token, driveAppDataScopes)
|
||||||
.then(refreshedToken => this.downloadFileInternal(refreshedToken, id));
|
.then(refreshedToken => this.downloadFileInternal(refreshedToken, id));
|
||||||
},
|
},
|
||||||
removeAppDataFile(token, id, ifNotTooLate = cb => res => cb(res)) {
|
removeAppDataFile(token, id, ifNotTooLate = cb => res => cb(res)) {
|
||||||
return this.refreshToken(driveAppDataScopes, token)
|
return this.refreshToken(token, driveAppDataScopes)
|
||||||
// Refreshing a token can take a while if an oauth window pops up, so check if it's too late
|
// Refreshing a token can take a while if an oauth window pops up, so check if it's too late
|
||||||
.then(ifNotTooLate(refreshedToken => this.request(refreshedToken, {
|
.then(ifNotTooLate(refreshedToken => this.request(refreshedToken, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
@ -290,7 +332,7 @@ export default {
|
|||||||
uploadBlogger(
|
uploadBlogger(
|
||||||
token, blogUrl, blogId, postId, title, content, labels, isDraft, published, isPage,
|
token, blogUrl, blogId, postId, title, content, labels, isDraft, published, isPage,
|
||||||
) {
|
) {
|
||||||
return this.refreshToken(bloggerScopes, token)
|
return this.refreshToken(token, bloggerScopes)
|
||||||
.then(refreshedToken => Promise.resolve()
|
.then(refreshedToken => Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (blogId) {
|
if (blogId) {
|
||||||
@ -356,7 +398,7 @@ export default {
|
|||||||
openPicker(token, type = 'doc') {
|
openPicker(token, type = 'doc') {
|
||||||
const scopes = type === 'img' ? photosScopes : getDriveScopes(token);
|
const scopes = type === 'img' ? photosScopes : getDriveScopes(token);
|
||||||
return this.loadClientScript()
|
return this.loadClientScript()
|
||||||
.then(() => this.refreshToken(scopes, token))
|
.then(() => this.refreshToken(token, scopes))
|
||||||
.then(refreshedToken => new Promise((resolve) => {
|
.then(refreshedToken => new Promise((resolve) => {
|
||||||
let picker;
|
let picker;
|
||||||
const pickerBuilder = new google.picker.PickerBuilder()
|
const pickerBuilder = new google.picker.PickerBuilder()
|
||||||
|
53
src/services/sponsorSvc.js
Normal file
53
src/services/sponsorSvc.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import store from '../store';
|
||||||
|
import networkSvc from './networkSvc';
|
||||||
|
import utils from './utils';
|
||||||
|
|
||||||
|
const checkPaymentEvery = 15 * 60 * 1000; // 15 min
|
||||||
|
let lastCheck = 0;
|
||||||
|
|
||||||
|
const appId = 'ESTHdCYOi18iLhhO';
|
||||||
|
let monetize;
|
||||||
|
|
||||||
|
const getMonetize = () => Promise.resolve()
|
||||||
|
.then(() => networkSvc.loadScript('https://cdn.monetizejs.com/api/js/latest/monetize.min.js'))
|
||||||
|
.then(() => {
|
||||||
|
monetize = monetize || new window.MonetizeJS({
|
||||||
|
applicationID: appId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const isGoogleSponsor = () => {
|
||||||
|
const loginToken = store.getters['data/loginToken'];
|
||||||
|
return loginToken && loginToken.isSponsor;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkPayment = () => {
|
||||||
|
const currentDate = Date.now();
|
||||||
|
if (!isGoogleSponsor() && utils.isUserActive() && !store.state.offline &&
|
||||||
|
lastCheck + checkPaymentEvery < currentDate
|
||||||
|
) {
|
||||||
|
lastCheck = currentDate;
|
||||||
|
getMonetize()
|
||||||
|
.then(() => monetize.getPaymentsImmediate((err, payments) => {
|
||||||
|
const isSponsor = payments && payments.app === appId && (
|
||||||
|
(payments.chargeOption && payments.chargeOption.alias === 'once') ||
|
||||||
|
(payments.subscriptionOption && payments.subscriptionOption.alias === 'yearly'));
|
||||||
|
if (isSponsor !== store.state.monetizeSponsor) {
|
||||||
|
store.commit('setMonetizeSponsor', isSponsor);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
utils.setInterval(checkPayment, 2000);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getToken() {
|
||||||
|
if (isGoogleSponsor() || store.state.offline) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return getMonetize()
|
||||||
|
.then(() => new Promise(resolve =>
|
||||||
|
monetize.getTokenImmediate((err, result) => resolve(result))));
|
||||||
|
},
|
||||||
|
};
|
@ -123,23 +123,45 @@ function createSyncLocation(syncLocation) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncFile(fileId, needSyncRestartParam = false) {
|
class SyncContext {
|
||||||
let needSyncRestart = needSyncRestartParam;
|
constructor() {
|
||||||
|
this.restart = false;
|
||||||
|
this.synced = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileSyncContext {
|
||||||
|
constructor() {
|
||||||
|
this.downloaded = {};
|
||||||
|
this.errors = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncFile(fileId, syncContext = new SyncContext()) {
|
||||||
|
const fileSyncContext = new FileSyncContext();
|
||||||
|
syncContext.synced[`${fileId}/content`] = true;
|
||||||
return localDbSvc.loadSyncedContent(fileId)
|
return localDbSvc.loadSyncedContent(fileId)
|
||||||
.then(() => localDbSvc.loadItem(`${fileId}/content`)
|
.then(() => localDbSvc.loadItem(`${fileId}/content`)
|
||||||
.catch(() => {})) // Item may not exist if content has not been downloaded yet
|
.catch(() => {})) // Item may not exist if content has not been downloaded yet
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
const getFile = () => store.state.file.itemMap[fileId];
|
||||||
const getContent = () => store.state.content.itemMap[`${fileId}/content`];
|
const getContent = () => store.state.content.itemMap[`${fileId}/content`];
|
||||||
const getSyncedContent = () => store.state.syncedContent.itemMap[`${fileId}/syncedContent`];
|
const getSyncedContent = () => store.state.syncedContent.itemMap[`${fileId}/syncedContent`];
|
||||||
const getSyncHistoryItem = syncLocationId => getSyncedContent().syncHistory[syncLocationId];
|
const getSyncHistoryItem = syncLocationId => getSyncedContent().syncHistory[syncLocationId];
|
||||||
const downloadedLocations = {};
|
|
||||||
const errorLocations = {};
|
|
||||||
|
|
||||||
const isLocationSynced = (syncLocation) => {
|
const isLocationSynced = (syncLocation) => {
|
||||||
const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
|
const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
|
||||||
return syncHistoryItem && syncHistoryItem[LAST_SENT] === getContent().hash;
|
return syncHistoryItem && syncHistoryItem[LAST_SENT] === getContent().hash;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isWelcomeFile = () => {
|
||||||
|
const file = getFile();
|
||||||
|
const content = getContent();
|
||||||
|
const welcomeFileHashes = store.getters['data/welcomeFileHashes'];
|
||||||
|
const hash = content ? utils.hash(content.text) : 0;
|
||||||
|
return file.name === 'Welcome file' && welcomeFileHashes[hash];
|
||||||
|
};
|
||||||
|
|
||||||
const syncOneContentLocation = () => {
|
const syncOneContentLocation = () => {
|
||||||
const syncLocations = [
|
const syncLocations = [
|
||||||
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
|
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
|
||||||
@ -149,16 +171,21 @@ function syncFile(fileId, needSyncRestartParam = false) {
|
|||||||
}
|
}
|
||||||
let result;
|
let result;
|
||||||
syncLocations.some((syncLocation) => {
|
syncLocations.some((syncLocation) => {
|
||||||
if (!errorLocations[syncLocation.id] &&
|
|
||||||
(!downloadedLocations[syncLocation.id] || !isLocationSynced(syncLocation))
|
|
||||||
) {
|
|
||||||
const provider = providerRegistry.providers[syncLocation.providerId];
|
const provider = providerRegistry.providers[syncLocation.providerId];
|
||||||
|
if (
|
||||||
|
// Skip if it previously threw an error
|
||||||
|
!fileSyncContext.errors[syncLocation.id] &&
|
||||||
|
// Skip if it has previously been downloaded and has not changed since then
|
||||||
|
(!fileSyncContext.downloaded[syncLocation.id] || !isLocationSynced(syncLocation)) &&
|
||||||
|
// Skip welcome file if not synchronized explicitly
|
||||||
|
(syncLocations.length > 1 || !isWelcomeFile())
|
||||||
|
) {
|
||||||
const token = provider.getToken(syncLocation);
|
const token = provider.getToken(syncLocation);
|
||||||
result = provider && token && store.dispatch('queue/doWithLocation', {
|
result = provider && token && store.dispatch('queue/doWithLocation', {
|
||||||
location: syncLocation,
|
location: syncLocation,
|
||||||
promise: provider.downloadContent(token, syncLocation)
|
promise: provider.downloadContent(token, syncLocation)
|
||||||
.then((serverContent = null) => {
|
.then((serverContent = null) => {
|
||||||
downloadedLocations[syncLocation.id] = true;
|
fileSyncContext.downloaded[syncLocation.id] = true;
|
||||||
|
|
||||||
const syncedContent = getSyncedContent();
|
const syncedContent = getSyncedContent();
|
||||||
const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
|
const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
|
||||||
@ -192,7 +219,7 @@ function syncFile(fileId, needSyncRestartParam = false) {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
if (!mergedContent) {
|
if (!mergedContent) {
|
||||||
errorLocations[syncLocation.id] = true;
|
fileSyncContext.errors[syncLocation.id] = true;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,9 +301,10 @@ function syncFile(fileId, needSyncRestartParam = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If content was just created, restart sync to create the file as well
|
// If content was just created, restart sync to create the file as well
|
||||||
const syncDataByItemId = store.getters['data/syncDataByItemId'];
|
if (provider === mainProvider &&
|
||||||
if (!syncDataByItemId[fileId]) {
|
!store.getters['data/syncDataByItemId'][fileId]
|
||||||
needSyncRestart = true;
|
) {
|
||||||
|
syncContext.restart = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -286,7 +314,7 @@ function syncFile(fileId, needSyncRestartParam = false) {
|
|||||||
}
|
}
|
||||||
console.error(err); // eslint-disable-line no-console
|
console.error(err); // eslint-disable-line no-console
|
||||||
store.dispatch('notification/error', err);
|
store.dispatch('notification/error', err);
|
||||||
errorLocations[syncLocation.id] = true;
|
fileSyncContext.errors[syncLocation.id] = true;
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.then(() => syncOneContentLocation());
|
.then(() => syncOneContentLocation());
|
||||||
@ -304,12 +332,10 @@ function syncFile(fileId, needSyncRestartParam = false) {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
throw err;
|
throw err;
|
||||||
}))
|
}))
|
||||||
.then(
|
.catch((err) => {
|
||||||
() => needSyncRestart,
|
|
||||||
(err) => {
|
|
||||||
if (err && err.message === 'TOO_LATE') {
|
if (err && err.message === 'TOO_LATE') {
|
||||||
// Restart sync
|
// Restart sync
|
||||||
return syncFile(fileId, needSyncRestart);
|
return syncFile(fileId, syncContext);
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
@ -385,6 +411,7 @@ function syncDataItem(dataId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sync() {
|
function sync() {
|
||||||
|
const syncContext = new SyncContext();
|
||||||
const mainToken = store.getters['data/loginToken'];
|
const mainToken = store.getters['data/loginToken'];
|
||||||
return mainProvider.getChanges(mainToken)
|
return mainProvider.getChanges(mainToken)
|
||||||
.then((changes) => {
|
.then((changes) => {
|
||||||
@ -481,8 +508,12 @@ function sync() {
|
|||||||
const loadedContent = store.state.content.itemMap[contentId];
|
const loadedContent = store.state.content.itemMap[contentId];
|
||||||
const hash = loadedContent ? loadedContent.hash : localDbSvc.hashMap.content[contentId];
|
const hash = loadedContent ? loadedContent.hash : localDbSvc.hashMap.content[contentId];
|
||||||
const syncData = store.getters['data/syncDataByItemId'][contentId];
|
const syncData = store.getters['data/syncDataByItemId'][contentId];
|
||||||
// Sync if item hash and syncData hash are inconsistent
|
if (
|
||||||
if (!syncData || hash !== syncData.hash) {
|
// Sync if syncData does not exist and content syncing was not attempted yet
|
||||||
|
(!syncData && !syncContext.synced[contentId]) ||
|
||||||
|
// Or if content hash and syncData hash are inconsistent
|
||||||
|
(syncData && hash !== syncData.hash)
|
||||||
|
) {
|
||||||
[fileId] = contentId.split('/');
|
[fileId] = contentId.split('/');
|
||||||
}
|
}
|
||||||
return fileId;
|
return fileId;
|
||||||
@ -490,13 +521,13 @@ function sync() {
|
|||||||
return fileId;
|
return fileId;
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncNextFile = (needSyncRestartParam) => {
|
const syncNextFile = () => {
|
||||||
const fileId = getOneFileIdToSync();
|
const fileId = getOneFileIdToSync();
|
||||||
if (!fileId) {
|
if (!fileId) {
|
||||||
return needSyncRestartParam;
|
return null;
|
||||||
}
|
}
|
||||||
return syncFile(fileId, needSyncRestartParam)
|
return syncFile(fileId, syncContext)
|
||||||
.then(needSyncRestart => syncNextFile(needSyncRestart));
|
.then(() => syncNextFile());
|
||||||
};
|
};
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
@ -508,14 +539,14 @@ function sync() {
|
|||||||
const currentFileId = store.getters['file/current'].id;
|
const currentFileId = store.getters['file/current'].id;
|
||||||
if (currentFileId) {
|
if (currentFileId) {
|
||||||
// Sync current file first
|
// Sync current file first
|
||||||
return syncFile(currentFileId)
|
return syncFile(currentFileId, syncContext)
|
||||||
.then(needSyncRestart => syncNextFile(needSyncRestart));
|
.then(() => syncNextFile());
|
||||||
}
|
}
|
||||||
return syncNextFile();
|
return syncNextFile();
|
||||||
})
|
})
|
||||||
.then(
|
.then(
|
||||||
(needSyncRestart) => {
|
() => {
|
||||||
if (needSyncRestart) {
|
if (syncContext.restart) {
|
||||||
// Restart sync
|
// Restart sync
|
||||||
return sync();
|
return sync();
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
|
import '../libs/clunderscore';
|
||||||
import defaultProperties from '../data/defaultFileProperties.yml';
|
import defaultProperties from '../data/defaultFileProperties.yml';
|
||||||
|
|
||||||
const workspaceId = 'main';
|
const workspaceId = 'main';
|
||||||
@ -157,4 +158,57 @@ export default {
|
|||||||
urlParser.search += keys.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`).join('&');
|
urlParser.search += keys.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`).join('&');
|
||||||
return urlParser.href;
|
return urlParser.href;
|
||||||
},
|
},
|
||||||
|
wrapRange(range, eltProperties) {
|
||||||
|
const rangeLength = `${range}`.length;
|
||||||
|
let wrappedLength = 0;
|
||||||
|
const treeWalker = document.createTreeWalker(
|
||||||
|
range.commonAncestorContainer, NodeFilter.SHOW_TEXT);
|
||||||
|
let startOffset = range.startOffset;
|
||||||
|
treeWalker.currentNode = range.startContainer;
|
||||||
|
if (treeWalker.currentNode.nodeType === Node.TEXT_NODE || treeWalker.nextNode()) {
|
||||||
|
do {
|
||||||
|
if (treeWalker.currentNode.nodeValue !== '\n') {
|
||||||
|
if (treeWalker.currentNode === range.endContainer &&
|
||||||
|
range.endOffset < treeWalker.currentNode.nodeValue.length
|
||||||
|
) {
|
||||||
|
treeWalker.currentNode.splitText(range.endOffset);
|
||||||
|
}
|
||||||
|
if (startOffset) {
|
||||||
|
treeWalker.currentNode = treeWalker.currentNode.splitText(startOffset);
|
||||||
|
startOffset = 0;
|
||||||
|
}
|
||||||
|
const elt = document.createElement('span');
|
||||||
|
Object.keys(eltProperties).forEach((key) => {
|
||||||
|
elt[key] = eltProperties[key];
|
||||||
|
});
|
||||||
|
treeWalker.currentNode.parentNode.insertBefore(elt, treeWalker.currentNode);
|
||||||
|
elt.appendChild(treeWalker.currentNode);
|
||||||
|
}
|
||||||
|
wrappedLength += treeWalker.currentNode.nodeValue.length;
|
||||||
|
if (wrappedLength >= rangeLength) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (treeWalker.nextNode());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unwrapRange(eltCollection) {
|
||||||
|
Array.prototype.slice.call(eltCollection).forEach((elt) => {
|
||||||
|
// Loop in case another wrapper has been added inside
|
||||||
|
for (let child = elt.firstChild; child; child = elt.firstChild) {
|
||||||
|
if (child.nodeType === 3) {
|
||||||
|
if (elt.previousSibling && elt.previousSibling.nodeType === 3) {
|
||||||
|
child.nodeValue = elt.previousSibling.nodeValue + child.nodeValue;
|
||||||
|
elt.parentNode.removeChild(elt.previousSibling);
|
||||||
|
}
|
||||||
|
if (!child.nextSibling && elt.nextSibling && elt.nextSibling.nodeType === 3) {
|
||||||
|
child.nodeValue += elt.nextSibling.nodeValue;
|
||||||
|
elt.parentNode.removeChild(elt.nextSibling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elt.parentNode.insertBefore(child, elt);
|
||||||
|
}
|
||||||
|
elt.parentNode.removeChild(elt);
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -6,6 +6,7 @@ import contentState from './modules/contentState';
|
|||||||
import syncedContent from './modules/syncedContent';
|
import syncedContent from './modules/syncedContent';
|
||||||
import content from './modules/content';
|
import content from './modules/content';
|
||||||
import file from './modules/file';
|
import file from './modules/file';
|
||||||
|
import findReplace from './modules/findReplace';
|
||||||
import folder from './modules/folder';
|
import folder from './modules/folder';
|
||||||
import publishLocation from './modules/publishLocation';
|
import publishLocation from './modules/publishLocation';
|
||||||
import syncLocation from './modules/syncLocation';
|
import syncLocation from './modules/syncLocation';
|
||||||
@ -26,6 +27,7 @@ const store = new Vuex.Store({
|
|||||||
ready: false,
|
ready: false,
|
||||||
offline: false,
|
offline: false,
|
||||||
lastOfflineCheck: 0,
|
lastOfflineCheck: 0,
|
||||||
|
monetizeSponsor: false,
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
allItemMap: (state) => {
|
allItemMap: (state) => {
|
||||||
@ -33,6 +35,10 @@ const store = new Vuex.Store({
|
|||||||
utils.types.forEach(type => Object.assign(result, state[type].itemMap));
|
utils.types.forEach(type => Object.assign(result, state[type].itemMap));
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
isSponsor: (state, getters) => {
|
||||||
|
const loginToken = getters['data/loginToken'];
|
||||||
|
return state.monetizeSponsor || (loginToken && loginToken.isSponsor);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setReady: (state) => {
|
setReady: (state) => {
|
||||||
@ -44,6 +50,12 @@ const store = new Vuex.Store({
|
|||||||
updateLastOfflineCheck: (state) => {
|
updateLastOfflineCheck: (state) => {
|
||||||
state.lastOfflineCheck = Date.now();
|
state.lastOfflineCheck = Date.now();
|
||||||
},
|
},
|
||||||
|
setMonetizeSponsor: (state, value) => {
|
||||||
|
state.monetizeSponsor = value;
|
||||||
|
},
|
||||||
|
setGoogleSponsor: (state, value) => {
|
||||||
|
state.googleSponsor = value;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setOffline: ({ state, commit, dispatch }, value) => {
|
setOffline: ({ state, commit, dispatch }, value) => {
|
||||||
@ -91,6 +103,7 @@ const store = new Vuex.Store({
|
|||||||
syncedContent,
|
syncedContent,
|
||||||
content,
|
content,
|
||||||
file,
|
file,
|
||||||
|
findReplace,
|
||||||
folder,
|
folder,
|
||||||
publishLocation,
|
publishLocation,
|
||||||
syncLocation,
|
syncLocation,
|
||||||
|
@ -6,6 +6,7 @@ import defaultSettings from '../../data/defaultSettings.yml';
|
|||||||
import defaultLocalSettings from '../../data/defaultLocalSettings';
|
import defaultLocalSettings from '../../data/defaultLocalSettings';
|
||||||
import plainHtmlTemplate from '../../data/plainHtmlTemplate.html';
|
import plainHtmlTemplate from '../../data/plainHtmlTemplate.html';
|
||||||
import styledHtmlTemplate from '../../data/styledHtmlTemplate.html';
|
import styledHtmlTemplate from '../../data/styledHtmlTemplate.html';
|
||||||
|
import styledHtmlWithTocTemplate from '../../data/styledHtmlWithTocTemplate.html';
|
||||||
import jekyllSiteTemplate from '../../data/jekyllSiteTemplate.html';
|
import jekyllSiteTemplate from '../../data/jekyllSiteTemplate.html';
|
||||||
|
|
||||||
const itemTemplate = (id, data = {}) => ({ id, type: 'data', data, hash: 0 });
|
const itemTemplate = (id, data = {}) => ({ id, type: 'data', data, hash: 0 });
|
||||||
@ -61,15 +62,37 @@ module.actions.toggleNavigationBar = localSettingsToggler('showNavigationBar');
|
|||||||
module.actions.toggleEditor = localSettingsToggler('showEditor');
|
module.actions.toggleEditor = localSettingsToggler('showEditor');
|
||||||
module.actions.toggleSidePreview = localSettingsToggler('showSidePreview');
|
module.actions.toggleSidePreview = localSettingsToggler('showSidePreview');
|
||||||
module.actions.toggleStatusBar = localSettingsToggler('showStatusBar');
|
module.actions.toggleStatusBar = localSettingsToggler('showStatusBar');
|
||||||
module.actions.toggleSideBar = ({ getters, dispatch }, value) => {
|
|
||||||
dispatch('setSideBarPanel'); // Reset side bar
|
|
||||||
dispatch('patchLocalSettings', {
|
|
||||||
showSideBar: value === undefined ? !getters.localSettings.showSideBar : value,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
module.actions.toggleExplorer = localSettingsToggler('showExplorer');
|
|
||||||
module.actions.toggleScrollSync = localSettingsToggler('scrollSync');
|
module.actions.toggleScrollSync = localSettingsToggler('scrollSync');
|
||||||
module.actions.toggleFocusMode = localSettingsToggler('focusMode');
|
module.actions.toggleFocusMode = localSettingsToggler('focusMode');
|
||||||
|
const notEnoughSpace = (rootGetters) => {
|
||||||
|
const constants = rootGetters['layout/constants'];
|
||||||
|
return document.body.clientWidth < constants.editorMinWidth +
|
||||||
|
constants.explorerWidth +
|
||||||
|
constants.sideBarWidth +
|
||||||
|
constants.buttonBarWidth;
|
||||||
|
};
|
||||||
|
module.actions.toggleSideBar = ({ getters, dispatch, rootGetters }, value) => {
|
||||||
|
// Reset side bar
|
||||||
|
dispatch('setSideBarPanel');
|
||||||
|
// Close explorer if not enough space
|
||||||
|
const patch = {
|
||||||
|
showSideBar: value === undefined ? !getters.localSettings.showSideBar : value,
|
||||||
|
};
|
||||||
|
if (patch.showSideBar && notEnoughSpace(rootGetters)) {
|
||||||
|
patch.showExplorer = false;
|
||||||
|
}
|
||||||
|
dispatch('patchLocalSettings', patch);
|
||||||
|
};
|
||||||
|
module.actions.toggleExplorer = ({ getters, dispatch, rootGetters }, value) => {
|
||||||
|
// Close side bar if not enough space
|
||||||
|
const patch = {
|
||||||
|
showExplorer: value === undefined ? !getters.localSettings.showExplorer : value,
|
||||||
|
};
|
||||||
|
if (patch.showExplorer && notEnoughSpace(rootGetters)) {
|
||||||
|
patch.showSideBar = false;
|
||||||
|
}
|
||||||
|
dispatch('patchLocalSettings', patch);
|
||||||
|
};
|
||||||
module.actions.setSideBarPanel = ({ dispatch }, value) => dispatch('patchLocalSettings', {
|
module.actions.setSideBarPanel = ({ dispatch }, value) => dispatch('patchLocalSettings', {
|
||||||
sideBarPanel: value === undefined ? 'menu' : value,
|
sideBarPanel: value === undefined ? 'menu' : value,
|
||||||
});
|
});
|
||||||
@ -112,6 +135,7 @@ const additionalTemplates = {
|
|||||||
plainText: makeAdditionalTemplate('Plain text', '{{{files.0.content.text}}}'),
|
plainText: makeAdditionalTemplate('Plain text', '{{{files.0.content.text}}}'),
|
||||||
plainHtml: makeAdditionalTemplate('Plain HTML', plainHtmlTemplate),
|
plainHtml: makeAdditionalTemplate('Plain HTML', plainHtmlTemplate),
|
||||||
styledHtml: makeAdditionalTemplate('Styled HTML', styledHtmlTemplate),
|
styledHtml: makeAdditionalTemplate('Styled HTML', styledHtmlTemplate),
|
||||||
|
styledHtmlWithToc: makeAdditionalTemplate('Styled HTML with TOC', styledHtmlWithTocTemplate),
|
||||||
jekyllSite: makeAdditionalTemplate('Jekyll site', jekyllSiteTemplate),
|
jekyllSite: makeAdditionalTemplate('Jekyll site', jekyllSiteTemplate),
|
||||||
};
|
};
|
||||||
module.getters.allTemplates = (state, getters) => ({
|
module.getters.allTemplates = (state, getters) => ({
|
||||||
@ -187,6 +211,10 @@ module.actions.setSyncData = setter('syncData');
|
|||||||
module.getters.dataSyncData = getter('dataSyncData');
|
module.getters.dataSyncData = getter('dataSyncData');
|
||||||
module.actions.patchDataSyncData = patcher('dataSyncData');
|
module.actions.patchDataSyncData = patcher('dataSyncData');
|
||||||
|
|
||||||
|
// Welcome file content hashes (used as a file sync blacklist)
|
||||||
|
module.getters.welcomeFileHashes = getter('welcomeFileHashes');
|
||||||
|
module.actions.patchWelcomeFileHashes = patcher('welcomeFileHashes');
|
||||||
|
|
||||||
// Tokens
|
// Tokens
|
||||||
module.getters.tokens = getter('tokens');
|
module.getters.tokens = getter('tokens');
|
||||||
module.getters.googleTokens = (state, getters) => getters.tokens.google || {};
|
module.getters.googleTokens = (state, getters) => getters.tokens.google || {};
|
||||||
|
32
src/store/modules/findReplace.js
Normal file
32
src/store/modules/findReplace.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
type: null,
|
||||||
|
lastOpen: 0,
|
||||||
|
findText: '',
|
||||||
|
replaceText: '',
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setType: (state, value) => {
|
||||||
|
state.type = value;
|
||||||
|
},
|
||||||
|
setLastOpen: (state) => {
|
||||||
|
state.lastOpen = Date.now();
|
||||||
|
},
|
||||||
|
setFindText: (state, value) => {
|
||||||
|
state.findText = value;
|
||||||
|
},
|
||||||
|
setReplaceText: (state, value) => {
|
||||||
|
state.replaceText = value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
open({ commit, state }, { type, findText }) {
|
||||||
|
commit('setType', type);
|
||||||
|
if (findText) {
|
||||||
|
commit('setFindText', findText);
|
||||||
|
}
|
||||||
|
commit('setLastOpen');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -1,4 +1,3 @@
|
|||||||
const editorMinWidth = 320;
|
|
||||||
const minPadding = 20;
|
const minPadding = 20;
|
||||||
const previewButtonWidth = 55;
|
const previewButtonWidth = 55;
|
||||||
const editorTopPadding = 10;
|
const editorTopPadding = 10;
|
||||||
@ -13,6 +12,7 @@ const maxTitleMaxWidth = 800;
|
|||||||
const minTitleMaxWidth = 200;
|
const minTitleMaxWidth = 200;
|
||||||
|
|
||||||
const constants = {
|
const constants = {
|
||||||
|
editorMinWidth: 320,
|
||||||
explorerWidth: 250,
|
explorerWidth: 250,
|
||||||
sideBarWidth: 280,
|
sideBarWidth: 280,
|
||||||
navigationBarHeight: 44,
|
navigationBarHeight: 44,
|
||||||
@ -28,6 +28,7 @@ function computeStyles(state, localSettings, getters, styles = {
|
|||||||
showPreview: localSettings.showSidePreview || !localSettings.showEditor,
|
showPreview: localSettings.showSidePreview || !localSettings.showEditor,
|
||||||
showSideBar: localSettings.showSideBar,
|
showSideBar: localSettings.showSideBar,
|
||||||
showExplorer: localSettings.showExplorer,
|
showExplorer: localSettings.showExplorer,
|
||||||
|
layoutOverflow: false,
|
||||||
}) {
|
}) {
|
||||||
styles.innerHeight = state.bodyHeight;
|
styles.innerHeight = state.bodyHeight;
|
||||||
if (styles.showNavigationBar) {
|
if (styles.showNavigationBar) {
|
||||||
@ -46,14 +47,16 @@ function computeStyles(state, localSettings, getters, styles = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let doublePanelWidth = styles.innerWidth - constants.buttonBarWidth;
|
let doublePanelWidth = styles.innerWidth - constants.buttonBarWidth;
|
||||||
if (doublePanelWidth < editorMinWidth) {
|
if (doublePanelWidth < constants.editorMinWidth) {
|
||||||
doublePanelWidth = editorMinWidth;
|
doublePanelWidth = constants.editorMinWidth;
|
||||||
styles.innerWidth = editorMinWidth + constants.buttonBarWidth;
|
styles.innerWidth = constants.editorMinWidth + constants.buttonBarWidth;
|
||||||
|
styles.layoutOverflow = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (styles.showSidePreview && doublePanelWidth / 2 < editorMinWidth) {
|
if (styles.showSidePreview && doublePanelWidth / 2 < constants.editorMinWidth) {
|
||||||
styles.showSidePreview = false;
|
styles.showSidePreview = false;
|
||||||
styles.showPreview = false;
|
styles.showPreview = false;
|
||||||
|
styles.layoutOverflow = false;
|
||||||
return computeStyles(state, localSettings, getters, styles);
|
return computeStyles(state, localSettings, getters, styles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user