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 vueLoaderConfig = require('./vue-loader.conf')
|
||||
var StylelintPlugin = require('stylelint-webpack-plugin')
|
||||
var FaviconsWebpackPlugin = require('favicons-webpack-plugin')
|
||||
|
||||
function resolve (dir) {
|
||||
return path.join(__dirname, '..', dir)
|
||||
@ -13,6 +14,10 @@ module.exports = {
|
||||
entry: {
|
||||
app: './src/'
|
||||
},
|
||||
node: {
|
||||
// For mermaid
|
||||
fs: 'empty' // jison generated code requires 'fs'
|
||||
},
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
filename: '[name].js',
|
||||
@ -45,7 +50,14 @@ module.exports = {
|
||||
{
|
||||
test: /\.js$/,
|
||||
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)(\?.*)?$/,
|
||||
@ -56,13 +68,9 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(ttf|eot|otf|woff2)$/, loader: 'ignore-loader'
|
||||
},
|
||||
{
|
||||
test: /\.woff(\?.*)?$/,
|
||||
loader: 'url-loader',
|
||||
test: /\.(ttf|eot|otf|woff2?)(\?.*)?$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
|
||||
}
|
||||
},
|
||||
@ -76,6 +84,10 @@ module.exports = {
|
||||
new StylelintPlugin({
|
||||
files: ['**/*.vue', '**/*.scss']
|
||||
}),
|
||||
new FaviconsWebpackPlugin({
|
||||
logo: resolve('src/assets/favicon.png'),
|
||||
title: 'StackEdit',
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
VERSION: JSON.stringify(require('../package.json').version)
|
||||
})
|
||||
|
@ -96,7 +96,7 @@ var webpackConfig = merge(baseWebpackConfig, {
|
||||
ServiceWorker: {
|
||||
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']
|
||||
})
|
||||
]
|
||||
|
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">
|
||||
<title>StackEdit</title>
|
||||
<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="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
@ -18,5 +13,6 @@
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
<script src="//cdn.monetizejs.com/api/js/latest/monetize.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
13
package.json
13
package.json
@ -11,7 +11,8 @@
|
||||
"scripts": {
|
||||
"postinstall": "gulp build-prism",
|
||||
"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",
|
||||
"preversion": "npm run lint",
|
||||
"postversion": "git push origin master --tags && npm publish",
|
||||
@ -20,11 +21,14 @@
|
||||
"major": "npm version major -m \"Tag v%s\""
|
||||
},
|
||||
"dependencies": {
|
||||
"aws-sdk": "^2.133.0",
|
||||
"bezier-easing": "^1.1.0",
|
||||
"body-parser": "^1.18.2",
|
||||
"clipboard": "^1.7.1",
|
||||
"compression": "^1.7.0",
|
||||
"diff-match-patch": "^1.0.0",
|
||||
"file-saver": "^1.3.3",
|
||||
"google-id-token-verifier": "^0.2.3",
|
||||
"handlebars": "^4.0.10",
|
||||
"indexeddbshim": "^3.0.4",
|
||||
"js-yaml": "^3.9.1",
|
||||
@ -37,12 +41,13 @@
|
||||
"markdown-it-pandoc-renderer": "1.1.3",
|
||||
"markdown-it-sub": "^1.0.0",
|
||||
"markdown-it-sup": "^1.0.0",
|
||||
"mermaid": "^7.1.0",
|
||||
"mousetrap": "^1.6.1",
|
||||
"normalize-scss": "^7.0.0",
|
||||
"prismjs": "^1.6.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"request": "^2.82.0",
|
||||
"serve-static": "^1.12.6",
|
||||
"tmp": "^0.0.33",
|
||||
"vue": "^2.3.3",
|
||||
"vuex": "^2.3.1"
|
||||
},
|
||||
@ -59,7 +64,7 @@
|
||||
"chalk": "^1.1.3",
|
||||
"connect-history-api-fallback": "^1.3.0",
|
||||
"copy-webpack-plugin": "^4.0.1",
|
||||
"css-loader": "^0.28.4",
|
||||
"css-loader": "^0.28.7",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-config-airbnb-base": "^11.1.3",
|
||||
"eslint-friendly-formatter": "^2.0.7",
|
||||
@ -70,6 +75,7 @@
|
||||
"eventsource-polyfill": "^0.9.6",
|
||||
"express": "^4.15.5",
|
||||
"extract-text-webpack-plugin": "^2.0.0",
|
||||
"favicons-webpack-plugin": "^0.0.7",
|
||||
"file-loader": "^0.11.1",
|
||||
"friendly-errors-webpack-plugin": "^1.1.3",
|
||||
"gulp": "^3.9.1",
|
||||
@ -83,6 +89,7 @@
|
||||
"opn": "^4.0.2",
|
||||
"optimize-css-assets-webpack-plugin": "^1.3.0",
|
||||
"ora": "^1.2.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"rimraf": "^2.6.0",
|
||||
"sass-loader": "^6.0.5",
|
||||
"semver": "^5.3.0",
|
||||
|
@ -1,7 +1,13 @@
|
||||
const compression = require('compression');
|
||||
const serveStatic = require('serve-static');
|
||||
const bodyParser = require('body-parser');
|
||||
const path = require('path');
|
||||
const user = require('./user');
|
||||
const github = require('./github');
|
||||
const pdf = require('./pdf');
|
||||
const pandoc = require('./pandoc');
|
||||
|
||||
const resolvePath = pathToResolve => path.join(__dirname, '..', pathToResolve);
|
||||
|
||||
module.exports = (app, serveV4) => {
|
||||
// Use gzip compression
|
||||
@ -22,10 +28,20 @@ module.exports = (app, serveV4) => {
|
||||
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('/userInfo', user.userInfo);
|
||||
app.post('/paypalIpn', user.paypalIpn);
|
||||
app.post('/pdfExport', pdf.generate);
|
||||
app.post('/pandocExport', pandoc.generate);
|
||||
|
||||
if (serveV4) {
|
||||
/* 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('/picasaImportImg', require('../stackedit_v4/app/picasa').importImg);
|
||||
app.get('/downloadImport', require('../stackedit_v4/app/download').importPublic);
|
||||
@ -33,29 +49,39 @@ module.exports = (app, serveV4) => {
|
||||
}
|
||||
|
||||
// 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
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (serveV4) {
|
||||
// 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
|
||||
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
|
||||
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
|
||||
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) {
|
||||
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
|
||||
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>
|
||||
<div class="button-bar">
|
||||
<div class="button-bar__inner button-bar__inner--top">
|
||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showNavigationBar }" @click="toggleNavigationBar()" 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>
|
||||
</div>
|
||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showSidePreview }" @click="toggleSidePreview()" v-title="'Toggle side preview'">
|
||||
</button>
|
||||
<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>
|
||||
</div>
|
||||
<div class="button-bar__button" @click="toggleEditor(false)" v-title="'Reader mode'">
|
||||
</button>
|
||||
<button class="button-bar__button button" @click="toggleEditor(false)" v-title="'Reader mode'">
|
||||
<icon-eye></icon-eye>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.scrollSync }" @click="toggleScrollSync()" v-title="'Toggle scroll sync'">
|
||||
</button>
|
||||
<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>
|
||||
</div>
|
||||
<div class="button-bar__button" :class="{ 'button-bar__button--on': localSettings.showStatusBar }" @click="toggleStatusBar()" v-title="'Toggle status bar'">
|
||||
</button>
|
||||
<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>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -60,19 +60,27 @@ export default {
|
||||
}
|
||||
|
||||
.button-bar__button {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
color: rgba(0, 0, 0, 0.2);
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 2px;
|
||||
margin: 3px 0;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.button-bar__button--on {
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -35,7 +35,6 @@ export default {
|
||||
font-family: $font-family-monospace;
|
||||
font-size: $font-size-monospace;
|
||||
font-variant-ligatures: no-common-ligatures;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
word-wrap: normal;
|
||||
height: auto;
|
||||
|
@ -22,7 +22,7 @@ import { mapMutations, mapActions } from 'vuex';
|
||||
import utils from '../services/utils';
|
||||
|
||||
export default {
|
||||
name: 'explorer-node',
|
||||
name: 'explorer-node', // Required for recursivity
|
||||
props: ['node', 'depth'],
|
||||
data: () => ({
|
||||
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>
|
||||
<div class="layout">
|
||||
<div class="layout__panel flex flex--row" :class="{'flex--end': styles.showSideBar}">
|
||||
<div class="layout__panel layout__panel--explorer" v-show="styles.showExplorer" :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>
|
||||
</div>
|
||||
<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 layout__panel--editor" v-show="styles.showEditor" :style="{ width: styles.editorWidth + 'px', 'font-size': styles.fontSize + 'px' }">
|
||||
<editor></editor>
|
||||
<div v-if="showFindReplace" class="layout__panel layout__panel--find-replace">
|
||||
<find-replace></find-replace>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layout__panel layout__panel--button-bar" v-show="styles.showEditor" :style="{ width: constants.buttonBarWidth + 'px' }">
|
||||
<button-bar></button-bar>
|
||||
@ -23,7 +26,7 @@
|
||||
<status-bar></status-bar>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@ -39,7 +42,9 @@ import Explorer from './Explorer';
|
||||
import SideBar from './SideBar';
|
||||
import Editor from './Editor';
|
||||
import Preview from './Preview';
|
||||
import FindReplace from './FindReplace';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
import editorEngineSvc from '../services/editorEngineSvc';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -50,12 +55,16 @@ export default {
|
||||
SideBar,
|
||||
Editor,
|
||||
Preview,
|
||||
FindReplace,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('layout', [
|
||||
'constants',
|
||||
'styles',
|
||||
]),
|
||||
showFindReplace() {
|
||||
return !!this.$store.state.findReplace.type;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('layout', [
|
||||
@ -75,6 +84,10 @@ export default {
|
||||
const previewElt = this.$el.querySelector('.preview__inner-2');
|
||||
const tocElt = this.$el.querySelector('.toc__inner');
|
||||
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() {
|
||||
window.removeEventListener('resize', this.updateStyle);
|
||||
@ -122,4 +135,14 @@ export default {
|
||||
.layout__panel--side-bar {
|
||||
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>
|
||||
|
@ -5,18 +5,23 @@
|
||||
<templates-modal v-else-if="config.type === 'templates'"></templates-modal>
|
||||
<about-modal v-else-if="config.type === 'about'"></about-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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<gist-sync-modal v-else-if="config.type === 'gistSync'"></gist-sync-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>
|
||||
<zendesk-account-modal v-else-if="config.type === 'zendeskAccount'"></zendesk-account-modal>
|
||||
<zendesk-publish-modal v-else-if="config.type === 'zendeskPublish'"></zendesk-publish-modal>
|
||||
<div v-else class="modal__inner-1">
|
||||
<div class="modal__inner-2">
|
||||
<div class="modal__content" v-html="config.content"></div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</div>
|
||||
<modal-inner v-else aria-label="Dialog">
|
||||
<div class="modal__content" v-html="config.content"></div>
|
||||
<div class="modal__button-bar">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import editorEngineSvc from '../services/editorEngineSvc';
|
||||
import ModalInner from './modals/common/ModalInner';
|
||||
import FilePropertiesModal from './modals/FilePropertiesModal';
|
||||
import SettingsModal from './modals/SettingsModal';
|
||||
import TemplatesModal from './modals/TemplatesModal';
|
||||
import AboutModal from './modals/AboutModal';
|
||||
import HtmlExportModal from './modals/HtmlExportModal';
|
||||
import PdfExportModal from './modals/PdfExportModal';
|
||||
import PandocExportModal from './modals/PandocExportModal';
|
||||
import LinkModal from './modals/LinkModal';
|
||||
import ImageModal from './modals/ImageModal';
|
||||
import GooglePhotoModal from './modals/GooglePhotoModal';
|
||||
import SyncManagementModal from './modals/SyncManagementModal';
|
||||
import PublishManagementModal from './modals/PublishManagementModal';
|
||||
import GoogleDriveSyncModal from './modals/GoogleDriveSyncModal';
|
||||
import GoogleDrivePublishModal from './modals/GoogleDrivePublishModal';
|
||||
import DropboxAccountModal from './modals/DropboxAccountModal';
|
||||
import DropboxSyncModal from './modals/DropboxSyncModal';
|
||||
import DropboxPublishModal from './modals/DropboxPublishModal';
|
||||
import GithubAccountModal from './modals/GithubAccountModal';
|
||||
import GithubSyncModal from './modals/GithubSyncModal';
|
||||
import GithubPublishModal from './modals/GithubPublishModal';
|
||||
import GistSyncModal from './modals/GistSyncModal';
|
||||
import GistPublishModal from './modals/GistPublishModal';
|
||||
import WordpressPublishModal from './modals/WordpressPublishModal';
|
||||
import BloggerPublishModal from './modals/BloggerPublishModal';
|
||||
import BloggerPagePublishModal from './modals/BloggerPagePublishModal';
|
||||
import ZendeskAccountModal from './modals/ZendeskAccountModal';
|
||||
import ZendeskPublishModal from './modals/ZendeskPublishModal';
|
||||
import SponsorModal from './modals/SponsorModal';
|
||||
|
||||
// Providers
|
||||
import GooglePhotoModal from './modals/providers/GooglePhotoModal';
|
||||
import GoogleDriveSaveModal from './modals/providers/GoogleDriveSaveModal';
|
||||
import GoogleDrivePublishModal from './modals/providers/GoogleDrivePublishModal';
|
||||
import DropboxAccountModal from './modals/providers/DropboxAccountModal';
|
||||
import DropboxSaveModal from './modals/providers/DropboxSaveModal';
|
||||
import DropboxPublishModal from './modals/providers/DropboxPublishModal';
|
||||
import GithubAccountModal from './modals/providers/GithubAccountModal';
|
||||
import GithubOpenModal from './modals/providers/GithubOpenModal';
|
||||
import GithubSaveModal from './modals/providers/GithubSaveModal';
|
||||
import GithubPublishModal from './modals/providers/GithubPublishModal';
|
||||
import GistSyncModal from './modals/providers/GistSyncModal';
|
||||
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')
|
||||
// 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 {
|
||||
components: {
|
||||
ModalInner,
|
||||
FilePropertiesModal,
|
||||
SettingsModal,
|
||||
TemplatesModal,
|
||||
AboutModal,
|
||||
HtmlExportModal,
|
||||
PdfExportModal,
|
||||
PandocExportModal,
|
||||
LinkModal,
|
||||
ImageModal,
|
||||
GooglePhotoModal,
|
||||
SyncManagementModal,
|
||||
PublishManagementModal,
|
||||
GoogleDriveSyncModal,
|
||||
SponsorModal,
|
||||
// Providers
|
||||
GooglePhotoModal,
|
||||
GoogleDriveSaveModal,
|
||||
GoogleDrivePublishModal,
|
||||
DropboxAccountModal,
|
||||
DropboxSyncModal,
|
||||
DropboxSaveModal,
|
||||
DropboxPublishModal,
|
||||
GithubAccountModal,
|
||||
GithubSyncModal,
|
||||
GithubOpenModal,
|
||||
GithubSaveModal,
|
||||
GithubPublishModal,
|
||||
GistSyncModal,
|
||||
GistPublishModal,
|
||||
@ -160,7 +176,7 @@ export default {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(128, 128, 128, 0.5);
|
||||
background-color: rgba(160, 160, 160, 0.5);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@ -172,7 +188,7 @@ export default {
|
||||
}
|
||||
|
||||
.modal__inner-2 {
|
||||
margin: 50px 10px 100px;
|
||||
margin: 40px 10px 100px;
|
||||
background-color: #fff;
|
||||
padding: 40px 50px 30px;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -309,7 +326,7 @@ export default {
|
||||
|
||||
.tabs {
|
||||
border-bottom: 1px solid $hr-color;
|
||||
margin-bottom: 2em;
|
||||
margin: 1em 0 2em;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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">
|
||||
<button class="navigation-bar__button button" @click="toggleExplorer()" v-title="'Toggle explorer'">
|
||||
<icon-folder></icon-folder>
|
||||
@ -78,7 +78,7 @@
|
||||
<icon-format-horizontal-rule></icon-format-horizontal-rule>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -31,7 +31,6 @@
|
||||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import Toc from './Toc';
|
||||
import MenuEntry from './menus/MenuEntry';
|
||||
import MainMenu from './menus/MainMenu';
|
||||
import SyncMenu from './menus/SyncMenu';
|
||||
import PublishMenu from './menus/PublishMenu';
|
||||
@ -53,7 +52,6 @@ const panelNames = {
|
||||
export default {
|
||||
components: {
|
||||
Toc,
|
||||
MenuEntry,
|
||||
MainMenu,
|
||||
SyncMenu,
|
||||
PublishMenu,
|
||||
|
@ -3,7 +3,7 @@
|
||||
<div class="stat-panel__block stat-panel__block--left" v-if="styles.showEditor">
|
||||
<span class="stat-panel__block-name">
|
||||
Markdown
|
||||
<small v-if="textSelection">(selection)</small>
|
||||
<span v-if="textSelection">selection</span>
|
||||
</span>
|
||||
<span v-for="stat in textStats" :key="stat.id">
|
||||
<span class="stat-panel__value">{{stat.value}}</span> {{stat.name}}
|
||||
@ -13,7 +13,7 @@
|
||||
<div class="stat-panel__block stat-panel__block--right">
|
||||
<span class="stat-panel__block-name">
|
||||
HTML
|
||||
<small v-if="htmlSelection">(selection)</small>
|
||||
<span v-if="htmlSelection">selection</span>
|
||||
</span>
|
||||
<span v-for="stat in htmlStats" :key="stat.id">
|
||||
<span class="stat-panel__value">{{stat.value}}</span> {{stat.name}}
|
||||
@ -123,10 +123,6 @@ export default {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.stat-panel__block-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.stat-panel__value {
|
||||
font-weight: 600;
|
||||
margin-left: 5px;
|
||||
|
@ -105,29 +105,39 @@ export default {
|
||||
}
|
||||
|
||||
.cl-toc-section {
|
||||
* {
|
||||
margin: 0.2em 0;
|
||||
padding: 0.2em 0;
|
||||
border-bottom: 0;
|
||||
h1,
|
||||
h2 {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0.5rem 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0.33rem 0;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0.22rem 0;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 0.11rem 0;
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
margin: 0;
|
||||
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,
|
||||
input,
|
||||
select,
|
||||
@ -78,6 +88,7 @@ textarea {
|
||||
background-image: none;
|
||||
border: 0;
|
||||
border-radius: $border-radius-base;
|
||||
text-decoration: none;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
@ -195,6 +206,12 @@ textarea {
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.hidden-rendering-container {
|
||||
position: absolute;
|
||||
width: 500px;
|
||||
left: -1000px;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
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 './variables.scss';
|
||||
@import './fonts.scss';
|
||||
@import './prism';
|
||||
@import './mermaid';
|
||||
|
||||
@include normalize();
|
||||
|
||||
html,
|
||||
body {
|
||||
color: rgba(0, 0, 0, 0.75);
|
||||
font-size: 16px;
|
||||
font-family: $font-family-main;
|
||||
font-variant-ligatures: common-ligatures;
|
||||
line-height: $line-height-base;
|
||||
@ -16,12 +18,6 @@ body {
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
blockquote,
|
||||
pre,
|
||||
@ -37,26 +33,19 @@ h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 1.8em 0 1.2em;
|
||||
margin: 1.8em 0;
|
||||
line-height: $line-height-title;
|
||||
padding: 0.33em 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
border-bottom: 1px solid $hr-color;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.2em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.22em;
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: relative;
|
||||
top: 0.33em;
|
||||
border-bottom: 1px solid $hr-color;
|
||||
}
|
||||
}
|
||||
|
||||
ol ul,
|
||||
@ -68,11 +57,12 @@ ol ol {
|
||||
|
||||
a {
|
||||
color: $link-color;
|
||||
text-decoration: none;
|
||||
text-decoration: underline;
|
||||
text-decoration-skip: ink;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: underline;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,8 +79,8 @@ samp {
|
||||
|
||||
blockquote {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
padding-left: 1.5em;
|
||||
border-left: 5px solid rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
code {
|
||||
@ -108,9 +98,9 @@ hr {
|
||||
pre > code {
|
||||
background-color: $code-bg;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
-webkit-text-size-adjust: none;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.toc ul {
|
||||
@ -126,7 +116,7 @@ table {
|
||||
|
||||
td,
|
||||
th {
|
||||
border-right: 1px solid #e0e0e0;
|
||||
border-right: 1px solid #dcdcdc;
|
||||
padding: 8px 12px;
|
||||
|
||||
&:last-child {
|
||||
@ -135,7 +125,7 @@ th {
|
||||
}
|
||||
|
||||
td {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
border-top: 1px solid #dcdcdc;
|
||||
}
|
||||
|
||||
kbd {
|
||||
@ -163,11 +153,6 @@ img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.footnote {
|
||||
font-size: 0.8em;
|
||||
position: relative;
|
||||
@ -175,18 +160,93 @@ img {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.export-container {
|
||||
.stackedit__html {
|
||||
margin-bottom: 180px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
|
||||
> :not(h1):not(h2):not(h3):not(h4):not(h5):not(h6):not([align]) {
|
||||
text-align: justify;
|
||||
@media (min-width: 810px) {
|
||||
.stackedit__html {
|
||||
width: 750px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.export-container {
|
||||
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 @click.native="exportPdf">
|
||||
<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>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MenuEntry from './MenuEntry';
|
||||
import MenuEntry from './common/MenuEntry';
|
||||
import exportSvc from '../../services/exportSvc';
|
||||
|
||||
export default {
|
||||
@ -37,7 +42,12 @@ export default {
|
||||
.catch(() => {}); // Cancel
|
||||
},
|
||||
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')">
|
||||
<icon-upload slot="icon"></icon-upload>
|
||||
<div>Publish</div>
|
||||
<span>Export to the web.</span>
|
||||
<span>Export your file to the web.</span>
|
||||
</menu-entry>
|
||||
<hr>
|
||||
<menu-entry @click.native="fileProperties">
|
||||
@ -63,7 +63,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import MenuEntry from './MenuEntry';
|
||||
import MenuEntry from './common/MenuEntry';
|
||||
import UserImage from '../UserImage';
|
||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||
import syncSvc from '../../services/syncSvc';
|
||||
|
@ -34,21 +34,22 @@
|
||||
<icon-help-circle slot="icon"></icon-help-circle>
|
||||
<span>About StackEdit</span>
|
||||
</menu-entry>
|
||||
<a href="editor" target="_blank" class="menu-entry button flex flex--row flex--align-center">
|
||||
<div class="menu-entry__icon flex flex--column flex--center">
|
||||
<icon-open-in-new></icon-open-in-new>
|
||||
</div>
|
||||
<div class="flex flex--column">
|
||||
<span>Go back to StackEdit 4</span>
|
||||
</div>
|
||||
</a>
|
||||
<menu-entry @click.native="welcomeFile">
|
||||
<icon-file slot="icon"></icon-file>
|
||||
<span>Welcome file</span>
|
||||
</menu-entry>
|
||||
<menu-entry href="editor" target="_blank">
|
||||
<icon-open-in-new slot="icon"></icon-open-in-new>
|
||||
<span>StackEdit 4 (deprecated)</span>
|
||||
</menu-entry>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MenuEntry from './MenuEntry';
|
||||
import MenuEntry from './common/MenuEntry';
|
||||
import localDbSvc from '../../services/localDbSvc';
|
||||
import backupSvc from '../../services/backupSvc';
|
||||
import welcomeFile from '../../data/welcomeFile.md';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -92,6 +93,13 @@ export default {
|
||||
() => {}, // Cancel
|
||||
);
|
||||
},
|
||||
welcomeFile() {
|
||||
return this.$store.dispatch('createFile', {
|
||||
name: 'Welcome file',
|
||||
text: welcomeFile,
|
||||
})
|
||||
.then(createdFile => this.$store.commit('file/setCurrentId', createdFile.id));
|
||||
},
|
||||
about() {
|
||||
return this.$store.dispatch('modal/open', 'about');
|
||||
},
|
||||
|
@ -96,7 +96,7 @@
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import MenuEntry from './MenuEntry';
|
||||
import MenuEntry from './common/MenuEntry';
|
||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||
import dropboxHelper from '../../services/providers/helpers/dropboxHelper';
|
||||
import githubHelper from '../../services/providers/helpers/githubHelper';
|
||||
|
@ -44,6 +44,11 @@
|
||||
</menu-entry>
|
||||
</div>
|
||||
<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)">
|
||||
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||
<div>Save on GitHub</div>
|
||||
@ -73,12 +78,13 @@
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import MenuEntry from './MenuEntry';
|
||||
import MenuEntry from './common/MenuEntry';
|
||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||
import dropboxHelper from '../../services/providers/helpers/dropboxHelper';
|
||||
import githubHelper from '../../services/providers/helpers/githubHelper';
|
||||
import googleDriveProvider from '../../services/providers/googleDriveProvider';
|
||||
import dropboxProvider from '../../services/providers/dropboxProvider';
|
||||
import githubProvider from '../../services/providers/githubProvider';
|
||||
import syncSvc from '../../services/syncSvc';
|
||||
import store from '../../store';
|
||||
|
||||
@ -161,15 +167,23 @@ export default {
|
||||
() => dropboxProvider.openFiles(token, paths)));
|
||||
},
|
||||
saveGoogleDrive(token) {
|
||||
return openSyncModal(token, 'googleDriveSync')
|
||||
return openSyncModal(token, 'googleDriveSave')
|
||||
.catch(() => {}); // Cancel
|
||||
},
|
||||
saveDropbox(token) {
|
||||
return openSyncModal(token, 'dropboxSync')
|
||||
return openSyncModal(token, 'dropboxSave')
|
||||
.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) {
|
||||
return openSyncModal(token, 'githubSync')
|
||||
return openSyncModal(token, 'githubSave')
|
||||
.catch(() => {}); // Cancel
|
||||
},
|
||||
saveGist(token) {
|
||||
|
@ -3,26 +3,34 @@
|
||||
<div class="menu-entry__icon flex flex--column flex--center">
|
||||
<slot name="icon"></slot>
|
||||
</div>
|
||||
<div class="flex flex--column">
|
||||
<div class="menu-entry__text flex flex--column">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../common/variables.scss';
|
||||
@import '../../common/variables.scss';
|
||||
|
||||
.menu-entry {
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
height: auto;
|
||||
|
||||
div div {
|
||||
text-decoration: underline;
|
||||
text-decoration-skip: ink;
|
||||
|
||||
&.menu-entry__sponsor {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
font-size: 0.75em;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.5;
|
||||
white-space: normal;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,4 +50,18 @@
|
||||
position: fixed;
|
||||
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>
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="modal__inner-1 modal__inner-1--about-modal" role="dialog" aria-label="About">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner class="modal__inner-1--about-modal" aria-label="About">
|
||||
<div class="modal__content">
|
||||
<div class="logo-background"></div>
|
||||
<div class="app-version">v{{version}} — © 2017 Benoit Schweblin</div>
|
||||
<hr>
|
||||
@ -12,17 +12,21 @@
|
||||
<a target="_blank" href="https://twitter.com/stackedit/">StackEdit on Twitter</a>
|
||||
<hr>
|
||||
<a target="_blank" href="privacy_policy.html">Privacy Policy</a>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.resolve()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.resolve()">Close</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ModalInner from './common/ModalInner';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ModalInner,
|
||||
},
|
||||
data: () => ({
|
||||
version: VERSION,
|
||||
}),
|
||||
@ -38,7 +42,7 @@ export default {
|
||||
|
||||
.logo-background {
|
||||
height: 75px;
|
||||
margin-bottom: 0.5rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.app-version {
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="modal__inner-1 modal__inner-1--file-properties" role="dialog" aria-label="File properties">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner class="modal__inner-1--file-properties" aria-label="File properties">
|
||||
<div class="modal__content">
|
||||
<div class="tabs flex flex--row">
|
||||
<tab :active="tab === 'custom'" @click="tab = 'custom'">
|
||||
Current file properties
|
||||
@ -22,26 +22,30 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__error modal__error--file-properties">{{error}}</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 yaml from 'js-yaml';
|
||||
import { mapGetters } from 'vuex';
|
||||
import Tab from './Tab';
|
||||
import ModalInner from './common/ModalInner';
|
||||
import Tab from './common/Tab';
|
||||
import CodeEditor from '../CodeEditor';
|
||||
import utils from '../../services/utils';
|
||||
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 {
|
||||
components: {
|
||||
ModalInner,
|
||||
Tab,
|
||||
CodeEditor,
|
||||
},
|
||||
|
@ -1,8 +1,9 @@
|
||||
<template>
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Export to HTML">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner aria-label="Export to HTML">
|
||||
<div class="modal__content">
|
||||
<p>Please choose a template for your <b>HTML export</b>.</p>
|
||||
<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">
|
||||
{{ template.name }}
|
||||
</option>
|
||||
@ -11,19 +12,19 @@
|
||||
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
|
||||
</div>
|
||||
</form-entry>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button button--copy">Copy to clipboard</button>
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button button--copy">Copy to clipboard</button>
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Clipboard from 'clipboard';
|
||||
import exportSvc from '../../services/exportSvc';
|
||||
import modalTemplate from './modalTemplate';
|
||||
import modalTemplate from './common/modalTemplate';
|
||||
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
||||
@ -33,12 +34,16 @@ export default modalTemplate({
|
||||
selectedTemplate: 'htmlExportTemplate',
|
||||
},
|
||||
mounted() {
|
||||
let timeoutId;
|
||||
this.$watch('selectedTemplate', (selectedTemplate) => {
|
||||
const currentFile = this.$store.getters['file/current'];
|
||||
exportSvc.applyTemplate(currentFile.id, this.allTemplates[selectedTemplate])
|
||||
.then((html) => {
|
||||
this.result = html;
|
||||
});
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
const currentFile = this.$store.getters['file/current'];
|
||||
exportSvc.applyTemplate(currentFile.id, this.allTemplates[selectedTemplate])
|
||||
.then((html) => {
|
||||
this.result = html;
|
||||
});
|
||||
}, 10);
|
||||
}, {
|
||||
immediate: true,
|
||||
});
|
||||
@ -51,9 +56,10 @@ export default modalTemplate({
|
||||
},
|
||||
methods: {
|
||||
resolve() {
|
||||
const config = this.config;
|
||||
const currentFile = this.$store.getters['file/current'];
|
||||
config.resolve();
|
||||
exportSvc.exportToDisk(currentFile.id, 'html', this.allTemplates[this.selectedTemplate]);
|
||||
this.config.resolve();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Insert image">
|
||||
<div class="modal__inner-2">
|
||||
<p>Please provide a <b>URL</b> for your image.
|
||||
<modal-inner aria-label="Insert image">
|
||||
<div class="modal__content">
|
||||
<p>Please provide a <b>URL</b> for your image.</p>
|
||||
<form-entry label="URL" error="url">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="url" @keyup.enter="resolve()">
|
||||
</form-entry>
|
||||
@ -14,17 +14,17 @@
|
||||
<icon-provider slot="icon" provider-id="googlePhotos"></icon-provider>
|
||||
<span>Add Google Photos account</span>
|
||||
</menu-entry>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import modalTemplate from './modalTemplate';
|
||||
import MenuEntry from '../menus/MenuEntry';
|
||||
import modalTemplate from './common/modalTemplate';
|
||||
import MenuEntry from '../menus/common/MenuEntry';
|
||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||
|
||||
export default modalTemplate({
|
||||
|
@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Insert link">
|
||||
<div class="modal__inner-2">
|
||||
<p>Please provide a <b>URL</b> for your link.
|
||||
<modal-inner aria-label="Insert link">
|
||||
<div class="modal__content">
|
||||
<p>Please provide a <b>URL</b> for your link.</p>
|
||||
<form-entry label="URL" error="url">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="url" @keyup.enter="resolve()">
|
||||
</form-entry>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import modalTemplate from './modalTemplate';
|
||||
import modalTemplate from './common/modalTemplate';
|
||||
|
||||
export default modalTemplate({
|
||||
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>
|
||||
<div class="modal__inner-1 modal__inner-1--publish-management" role="dialog" aria-label="Manage publication locations">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner class="modal__inner-1--publish-management" aria-label="Manage publication locations">
|
||||
<div class="modal__content">
|
||||
<p v-if="publishLocations.length"><b>{{currentFileName}}</b> is published to the following location(s):</p>
|
||||
<p v-else><b>{{currentFileName}}</b> is not published yet.</p>
|
||||
<div>
|
||||
@ -24,18 +24,22 @@
|
||||
<div class="modal__info" v-if="publishLocations.length">
|
||||
<b>Note:</b> Removing a synchronized location won't delete any file.
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="config.resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="config.resolve()">Ok</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ModalInner from './common/ModalInner';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ModalInner,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('modal', [
|
||||
'config',
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="modal__inner-1 modal__inner-1--settings" role="dialog" aria-label="Settings">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner class="modal__inner-1--settings" aria-label="Settings">
|
||||
<div class="modal__content">
|
||||
<div class="tabs flex flex--row">
|
||||
<tab :active="tab === 'custom'" @click="tab = 'custom'">
|
||||
Custom settings
|
||||
@ -22,25 +22,29 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__error modal__error--settings">{{error}}</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="!error && config.resolve(strippedCustomSettings)">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="!error && config.resolve(strippedCustomSettings)">Ok</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import yaml from 'js-yaml';
|
||||
import { mapGetters } from 'vuex';
|
||||
import Tab from './Tab';
|
||||
import ModalInner from './common/ModalInner';
|
||||
import Tab from './common/Tab';
|
||||
import CodeEditor from '../CodeEditor';
|
||||
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 {
|
||||
components: {
|
||||
ModalInner,
|
||||
Tab,
|
||||
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>
|
||||
<div class="modal__inner-1 modal__inner-1--sync-management" role="dialog" aria-label="Manage synchronized locations">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner class="modal__inner-1--sync-management" aria-label="Manage synchronized locations">
|
||||
<div class="modal__content">
|
||||
<p v-if="syncLocations.length"><b>{{currentFileName}}</b> is synchronized with the following location(s):</p>
|
||||
<p v-else><b>{{currentFileName}}</b> is not synchronized yet.</p>
|
||||
<div>
|
||||
@ -24,18 +24,22 @@
|
||||
<div class="modal__info" v-if="syncLocations.length">
|
||||
<b>Note:</b> Removing a synchronized location won't delete any file.
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="config.resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="config.resolve()">Ok</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ModalInner from './common/ModalInner';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ModalInner,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('modal', [
|
||||
'config',
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="modal__inner-1 modal__inner-1--templates" role="dialog" aria-label="Manage templates">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner class="modal__inner-1--templates" aria-label="Manage templates">
|
||||
<div class="modal__content">
|
||||
<div class="form-entry">
|
||||
<label class="form-entry__label" for="template">Template</label>
|
||||
<div class="form-entry__field">
|
||||
@ -33,7 +33,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<br>
|
||||
<label class="form-entry__label">Helpers</label>
|
||||
@ -42,17 +42,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 { mapGetters } from 'vuex';
|
||||
import utils from '../../services/utils';
|
||||
import ModalInner from './common/ModalInner';
|
||||
import CodeEditor from '../CodeEditor';
|
||||
import emptyTemplateValue from '../../data/emptyTemplateValue.html';
|
||||
import emptyTemplateHelpers from '!raw-loader!../../data/emptyTemplateHelpers.js'; // eslint-disable-line
|
||||
@ -70,6 +71,7 @@ function fillEmptyFields(template) {
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ModalInner,
|
||||
CodeEditor,
|
||||
},
|
||||
data: () => ({
|
||||
|
@ -9,7 +9,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import utils from '../../services/utils';
|
||||
import utils from '../../../services/utils';
|
||||
|
||||
export default {
|
||||
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 store from '../../store';
|
||||
import store from '../../../store';
|
||||
|
||||
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
|
||||
|
||||
export default (desc) => {
|
||||
const component = {
|
||||
@ -10,6 +13,7 @@ export default (desc) => {
|
||||
}),
|
||||
components: {
|
||||
...desc.components || {},
|
||||
ModalInner,
|
||||
FormEntry,
|
||||
},
|
||||
computed: {
|
||||
@ -49,8 +53,18 @@ export default (desc) => {
|
||||
},
|
||||
};
|
||||
if (key === 'selectedTemplate') {
|
||||
component.computed.allTemplates = () => store.getters['data/allTemplates'];
|
||||
component.methods.configureTemplates = () => {
|
||||
component.computed.allTemplates = () => {
|
||||
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', {
|
||||
type: 'templates',
|
||||
selectedId: this.selectedTemplate,
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Publish to Blogger Page">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner aria-label="Publish to Blogger Page">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="bloggerPage"></icon-provider>
|
||||
</div>
|
||||
@ -27,17 +27,17 @@
|
||||
<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>.
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 bloggerPageProvider from '../../services/providers/bloggerPageProvider';
|
||||
import modalTemplate from './modalTemplate';
|
||||
import bloggerPageProvider from '../../../services/providers/bloggerPageProvider';
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Publish to Blogger">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner aria-label="Publish to Blogger">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="blogger"></icon-provider>
|
||||
</div>
|
||||
@ -28,17 +28,17 @@
|
||||
<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>.
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 bloggerProvider from '../../services/providers/bloggerProvider';
|
||||
import modalTemplate from './modalTemplate';
|
||||
import bloggerProvider from '../../../services/providers/bloggerProvider';
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Link Dropbox account">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner aria-label="Link Dropbox account">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="dropbox"></icon-provider>
|
||||
</div>
|
||||
@ -15,16 +15,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="config.resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="config.resolve()">Ok</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import modalTemplate from './modalTemplate';
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
|
||||
export default modalTemplate({
|
||||
computedLocalSettings: {
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Publish to Dropbox">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner aria-label="Publish to Dropbox">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="dropbox"></icon-provider>
|
||||
</div>
|
||||
@ -22,17 +22,17 @@
|
||||
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
|
||||
</div>
|
||||
</form-entry>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 dropboxProvider from '../../services/providers/dropboxProvider';
|
||||
import modalTemplate from './modalTemplate';
|
||||
import dropboxProvider from '../../../services/providers/dropboxProvider';
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Synchronize with Dropbox">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner aria-label="Synchronize with Dropbox">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="dropbox"></icon-provider>
|
||||
</div>
|
||||
@ -12,17 +12,17 @@
|
||||
If the file exists, it will be replaced.
|
||||
</div>
|
||||
</form-entry>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 dropboxProvider from '../../services/providers/dropboxProvider';
|
||||
import modalTemplate from './modalTemplate';
|
||||
import dropboxProvider from '../../../services/providers/dropboxProvider';
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Publish to Gist">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner aria-label="Publish to Gist">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="gist"></icon-provider>
|
||||
</div>
|
||||
@ -34,17 +34,17 @@
|
||||
<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>.
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 gistProvider from '../../services/providers/gistProvider';
|
||||
import modalTemplate from './modalTemplate';
|
||||
import gistProvider from '../../../services/providers/gistProvider';
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Synchronize with Gist">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner aria-label="Synchronize with Gist">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="gist"></icon-provider>
|
||||
</div>
|
||||
@ -21,17 +21,17 @@
|
||||
If the file exists in the Gist, it will be replaced.
|
||||
</div>
|
||||
</form-entry>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 gistProvider from '../../services/providers/gistProvider';
|
||||
import modalTemplate from './modalTemplate';
|
||||
import gistProvider from '../../../services/providers/gistProvider';
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Link GitHub account">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner aria-label="Link GitHub account">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="github"></icon-provider>
|
||||
</div>
|
||||
@ -8,20 +8,20 @@
|
||||
<div class="form-entry">
|
||||
<div class="form-entry__checkbox">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="config.resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="config.resolve()">Ok</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import modalTemplate from './modalTemplate';
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
|
||||
export default modalTemplate({
|
||||
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>
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Publish to GitHub">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner aria-label="Publish to GitHub">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="github"></icon-provider>
|
||||
</div>
|
||||
@ -34,17 +34,17 @@
|
||||
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
|
||||
</div>
|
||||
</form-entry>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 './modalTemplate';
|
||||
import githubProvider from '../../../services/providers/githubProvider';
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Synchronize with GitHub">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner aria-label="Synchronize with GitHub">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="github"></icon-provider>
|
||||
</div>
|
||||
@ -14,7 +14,7 @@
|
||||
<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 master branch will be used.
|
||||
If not provided, the <code>master</code> branch will be used.
|
||||
</div>
|
||||
</form-entry>
|
||||
<form-entry label="File path" error="path">
|
||||
@ -24,17 +24,17 @@
|
||||
If the file exists, it will be replaced.
|
||||
</div>
|
||||
</form-entry>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 './modalTemplate';
|
||||
import githubProvider from '../../../services/providers/githubProvider';
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
||||
@ -56,13 +56,13 @@ export default modalTemplate({
|
||||
this.setError('path');
|
||||
}
|
||||
if (this.repoUrl && this.path) {
|
||||
const parsedRepo = this.repoUrl.match(/[/:]?([^/:]+)\/([^/]+?)(?:\.git|\/)?$/);
|
||||
const parsedRepo = githubProvider.parseRepoUrl(this.repoUrl);
|
||||
if (!parsedRepo) {
|
||||
this.setError('repoUrl');
|
||||
} else {
|
||||
// Return new location
|
||||
const location = githubProvider.makeLocation(
|
||||
this.config.token, parsedRepo[1], parsedRepo[2], this.branch || 'master', this.path);
|
||||
this.config.token, parsedRepo.owner, parsedRepo.repo, this.branch || 'master', this.path);
|
||||
this.config.resolve(location);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Publish to Google Drive">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner aria-label="Publish to Google Drive">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="googleDrive"></icon-provider>
|
||||
</div>
|
||||
@ -45,18 +45,18 @@
|
||||
<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>.
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 googleHelper from '../../services/providers/helpers/googleHelper';
|
||||
import googleDriveProvider from '../../services/providers/googleDriveProvider';
|
||||
import modalTemplate from './modalTemplate';
|
||||
import googleHelper from '../../../services/providers/helpers/googleHelper';
|
||||
import googleDriveProvider from '../../../services/providers/googleDriveProvider';
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Synchronize with Google Drive">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner aria-label="Synchronize with Google Drive">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="googleDrive"></icon-provider>
|
||||
</div>
|
||||
@ -20,18 +20,18 @@
|
||||
This will overwrite the file on the server.
|
||||
</div>
|
||||
</form-entry>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 googleHelper from '../../services/providers/helpers/googleHelper';
|
||||
import googleDriveProvider from '../../services/providers/googleDriveProvider';
|
||||
import modalTemplate from './modalTemplate';
|
||||
import googleHelper from '../../../services/providers/helpers/googleHelper';
|
||||
import googleDriveProvider from '../../../services/providers/googleDriveProvider';
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="modal__inner-1 modal__inner-1--google-photo" role="dialog" aria-label="Import Google Photo">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner class="modal__inner-1--google-photo" aria-label="Import Google Photo">
|
||||
<div class="modal__content">
|
||||
<div class="google-photo__tumbnail" :style="{'background-image': thumbnailUrl}"></div>
|
||||
<form-entry label="Title (optional)">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="title" @keyup.enter="resolve()">
|
||||
@ -8,17 +8,17 @@
|
||||
<form-entry label="Size limit (optional)">
|
||||
<input slot="field" class="textfield" type="text" v-model.trim="size" @keyup.enter="resolve()">
|
||||
</form-entry>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import FormEntry from './FormEntry';
|
||||
import FormEntry from '../common/FormEntry';
|
||||
|
||||
const makeThumbnail = (url, size) => `${url}=s${size}`;
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Publish to WordPress">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner aria-label="Publish to WordPress">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="wordpress"></icon-provider>
|
||||
</div>
|
||||
@ -30,17 +30,17 @@
|
||||
<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>.
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 wordpressProvider from '../../services/providers/wordpressProvider';
|
||||
import modalTemplate from './modalTemplate';
|
||||
import wordpressProvider from '../../../services/providers/wordpressProvider';
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Link Zendesk account">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner aria-label="Link Zendesk account">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="zendesk"></icon-provider>
|
||||
</div>
|
||||
@ -18,17 +18,17 @@
|
||||
<a href="https://support.zendesk.com/hc/en-us/articles/203663836" target="_blank"><b>More info</b></a>
|
||||
</div>
|
||||
</form-entry>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 modalTemplate from './modalTemplate';
|
||||
import utils from '../../services/utils';
|
||||
import utils from '../../../services/utils';
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="modal__inner-1" role="dialog" aria-label="Publish to Zendesk">
|
||||
<div class="modal__inner-2">
|
||||
<modal-inner aria-label="Publish to Zendesk">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-provider provider-id="zendesk"></icon-provider>
|
||||
</div>
|
||||
@ -34,17 +34,17 @@
|
||||
<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>.
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button" @click="resolve()">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 zendeskProvider from '../../services/providers/zendeskProvider';
|
||||
import modalTemplate from './modalTemplate';
|
||||
import zendeskProvider from '../../../services/providers/zendeskProvider';
|
||||
import modalTemplate from '../common/modalTemplate';
|
||||
|
||||
export default modalTemplate({
|
||||
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:
|
||||
#title: My article
|
||||
@ -26,7 +27,7 @@ extensions:
|
||||
sup: true
|
||||
table: true
|
||||
typographer: true
|
||||
# For strict CommonMark:
|
||||
# Enable strict CommonMark:
|
||||
#abbr: false
|
||||
#deflist: false
|
||||
#del: false
|
||||
@ -37,6 +38,25 @@ extensions:
|
||||
#table: false
|
||||
#typographer: false
|
||||
|
||||
# Emoji extension
|
||||
emoji:
|
||||
# Enable support for emojis & emoticons
|
||||
enabled: true
|
||||
# Enable shortcuts like :) :-(
|
||||
shortcuts: false
|
||||
|
||||
# Katex extension
|
||||
# Render LaTeX mathematical expressions by using:
|
||||
# $...$ for inline formulas
|
||||
# $$...$$ for displayed formulas.
|
||||
# See https://math.meta.stackexchange.com/questions/5020
|
||||
katex:
|
||||
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,
|
||||
scrollSync: true,
|
||||
focusMode: false,
|
||||
findCaseSensitive: false,
|
||||
findUseRegexp: false,
|
||||
sideBarPanel: 'menu',
|
||||
htmlExportTemplate: 'styledHtml',
|
||||
pdfExportTemplate: 'styledHtml',
|
||||
pandocExportFormat: 'pdf',
|
||||
googleDriveFolderId: '',
|
||||
googleDrivePublishFormat: 'markdown',
|
||||
googleDrivePublishTemplate: 'styledHtml',
|
||||
|
@ -10,9 +10,13 @@ editor:
|
||||
# Use monospaced font only
|
||||
monospacedFontOnly: false
|
||||
|
||||
# Keyboard shortcuts (see https://craig.is/killing/mice)
|
||||
# Keyboard shortcuts
|
||||
# See https://craig.is/killing/mice
|
||||
shortcuts:
|
||||
mod+s: sync
|
||||
mod+f: find
|
||||
mod+alt+f: replace
|
||||
mod+g: replace
|
||||
mod+shift+b: bold
|
||||
mod+shift+i: italic
|
||||
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
|
||||
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:
|
||||
|
||||
{{#tocToHtml files.0.content.toc}}{{/tocToHtml}} will produce a clickable TOC.
|
||||
|
@ -5,12 +5,15 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{files.0.name}}</title>
|
||||
<link rel="stylesheet" href="http://app.classeur.io/base-min.css" />
|
||||
<script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"></script>
|
||||
<link rel="stylesheet" href="https://stackedit.io/style.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="export-container">{{{files.0.content.html}}}</div>
|
||||
{{#if pdf}}
|
||||
<body class="stackedit stackedit--pdf">
|
||||
{{else}}
|
||||
<body class="stackedit">
|
||||
{{/if}}
|
||||
<div class="stackedit__html">{{{files.0.content.html}}}</div>
|
||||
</body>
|
||||
|
||||
</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!
|
||||
|
||||
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:**
|
||||
>
|
||||
> - 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.
|
||||
> - 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).
|
||||
## Create files and folders
|
||||
|
||||
#### <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.
|
||||
> - 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.
|
||||
## Open a file
|
||||
|
||||
#### <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),
|
||||
- HTML, to publish the document converted into HTML (on a blog for example),
|
||||
- Template, to have a full control of the output.
|
||||
## Update a publication
|
||||
|
||||
> **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.
|
||||
|
||||
> **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
|
||||
|
||||
SmartyPants converts ASCII punctuation characters into "smart" typographic punctuation HTML entities. For example:
|
||||
|
||||
| | ASCII | HTML |
|
||||
----------------- | ---------------------------- | ------------------
|
||||
| Single backticks | `'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 |
|
||||
| |ASCII |HTML |
|
||||
|----------------|-------------------------------|-----------------------------|
|
||||
|Single backticks|`'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|
|
||||
|
||||
|
||||
### Table of contents
|
||||
## KaTeX
|
||||
|
||||
You can insert a table of contents using the marker `[TOC]`:
|
||||
|
||||
[TOC]
|
||||
|
||||
|
||||
### MathJax
|
||||
|
||||
You can render *LaTeX* mathematical expressions using **MathJax**, as on [math.stackexchange.com][1]:
|
||||
You can render LaTeX mathematical expressions using [KaTeX](https://khan.github.io/KaTeX/):
|
||||
|
||||
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\,.
|
||||
$$
|
||||
|
||||
> **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:
|
||||
|
||||
|
||||
### UML diagrams
|
||||
|
||||
You can also render sequence diagrams like this:
|
||||
|
||||
```sequence
|
||||
Alice->Bob: Hello Bob, how are you?
|
||||
Note right of Bob: Bob thinks
|
||||
Bob-->Alice: I am good thanks!
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Square Rect] -- Link text --> B((Circle))
|
||||
A --> C(Round Rect)
|
||||
B --> D{Rhombus}
|
||||
C --> D
|
||||
```
|
||||
|
||||
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 './markdownExt';
|
||||
import './emojiExtension';
|
||||
import './katexExtension';
|
||||
import './markdownExtension';
|
||||
import './mermaidExtension';
|
||||
|
@ -178,9 +178,9 @@ extensionSvc.onInitConverter(0, (markdown, options) => {
|
||||
|
||||
extensionSvc.onSectionPreview((elt) => {
|
||||
elt.querySelectorAll('.prism').cl_each((prismElt) => {
|
||||
if (!prismElt.highlighted) {
|
||||
if (!prismElt.highlightedWithPrism) {
|
||||
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 SignalOff from './SignalOff';
|
||||
import Folder from './Folder';
|
||||
import File from './File';
|
||||
import ScrollSync from './ScrollSync';
|
||||
import Printer from './Printer';
|
||||
import Undo from './Undo';
|
||||
@ -87,6 +88,7 @@ Vue.component('iconInformation', Information);
|
||||
Vue.component('iconAlert', Alert);
|
||||
Vue.component('iconSignalOff', SignalOff);
|
||||
Vue.component('iconFolder', Folder);
|
||||
Vue.component('iconFile', File);
|
||||
Vue.component('iconScrollSync', ScrollSync);
|
||||
Vue.component('iconPrinter', Printer);
|
||||
Vue.component('iconUndo', Undo);
|
||||
|
@ -1,4 +1,6 @@
|
||||
import Vue from 'vue';
|
||||
import 'babel-polyfill';
|
||||
import 'indexeddbshim/dist/indexeddbshim';
|
||||
import * as OfflinePluginRuntime from 'offline-plugin/runtime';
|
||||
import './extensions/';
|
||||
import './services/optional';
|
||||
@ -7,6 +9,10 @@ import App from './components/App';
|
||||
import store from './store';
|
||||
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') {
|
||||
OfflinePluginRuntime.install({
|
||||
onUpdateReady: () => {
|
||||
|
@ -103,12 +103,13 @@ function cledit(contentElt, scrollElt, windowParam) {
|
||||
selectionMgr.updateCursorCoordinates(true)
|
||||
}
|
||||
|
||||
function replaceAll(search, replacement) {
|
||||
function replaceAll(search, replacement, startOffset = 0) {
|
||||
undoMgr.setDefaultMode('single')
|
||||
var textContent = getTextContent()
|
||||
var value = textContent.replace(search, replacement)
|
||||
if (value !== textContent) {
|
||||
var offset = editor.setContent(value)
|
||||
var text = getTextContent()
|
||||
var subtext = getTextContent().slice(startOffset);
|
||||
var value = subtext.replace(search, replacement)
|
||||
if (value !== subtext) {
|
||||
var offset = editor.setContent(text.slice(0, startOffset) + value);
|
||||
selectionMgr.setSelectionStartEnd(offset.end, offset.end)
|
||||
selectionMgr.updateCursorCoordinates(true)
|
||||
}
|
||||
@ -130,7 +131,7 @@ function cledit(contentElt, scrollElt, windowParam) {
|
||||
|
||||
var triggerSpellCheck = debounce(function () {
|
||||
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
|
||||
}
|
||||
// Hack for Chrome to trigger the spell checker
|
||||
@ -209,7 +210,8 @@ function cledit(contentElt, scrollElt, windowParam) {
|
||||
if (!editor.$window.document.contains(contentElt)) {
|
||||
watcher.stopWatching()
|
||||
editor.$window.removeEventListener('keydown', windowKeydownListener)
|
||||
editor.$window.removeEventListener('mouseup', windowMouseupListener)
|
||||
editor.$window.removeEventListener('mousedown', windowMouseListener)
|
||||
editor.$window.removeEventListener('mouseup', windowMouseListener)
|
||||
editor.$trigger('destroy')
|
||||
return true
|
||||
}
|
||||
@ -226,12 +228,13 @@ function cledit(contentElt, scrollElt, windowParam) {
|
||||
editor.$window.addEventListener('keydown', windowKeydownListener, false)
|
||||
|
||||
// Mouseup can happen outside the editor element
|
||||
function windowMouseupListener() {
|
||||
function windowMouseListener() {
|
||||
if (!tryDestroy()) {
|
||||
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
|
||||
contentElt.addEventListener('contextmenu', selectionMgr.saveSelectionState.cl_bind(selectionMgr, true, false))
|
||||
|
||||
@ -297,12 +300,10 @@ function cledit(contentElt, scrollElt, windowParam) {
|
||||
}, false)
|
||||
|
||||
contentElt.addEventListener('focus', function () {
|
||||
selectionMgr.hasFocus = true
|
||||
editor.$trigger('focus')
|
||||
}, false)
|
||||
|
||||
contentElt.addEventListener('blur', function () {
|
||||
selectionMgr.hasFocus = false
|
||||
editor.$trigger('blur')
|
||||
}, false)
|
||||
|
||||
|
@ -49,12 +49,16 @@ function SelectionMgr(editor) {
|
||||
this.$trigger('cursorCoordinatesChanged', coordinates)
|
||||
}
|
||||
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
|
||||
// Adjust cursorTop with contentElt position relative to scrollElt
|
||||
cursorTop += contentElt.getBoundingClientRect().top - scrollElt.getBoundingClientRect().top + scrollElt.scrollTop;
|
||||
var minScrollTop = cursorTop - adjustment
|
||||
var maxScrollTop = cursorTop + adjustment - scrollElt.clientHeight
|
||||
var maxScrollTop = cursorTop + adjustment - scrollEltHeight
|
||||
if (scrollElt.scrollTop > minScrollTop) {
|
||||
scrollElt.scrollTop = minScrollTop
|
||||
} else if (scrollElt.scrollTop < maxScrollTop) {
|
||||
@ -84,6 +88,10 @@ function SelectionMgr(editor) {
|
||||
}
|
||||
}
|
||||
|
||||
this.hasFocus = function() {
|
||||
return contentElt === editor.$document.activeElement;
|
||||
}
|
||||
|
||||
this.restoreSelection = function () {
|
||||
var min = Math.min(this.selectionStart, this.selectionEnd)
|
||||
var max = Math.max(this.selectionStart, this.selectionEnd)
|
||||
@ -132,9 +140,9 @@ function SelectionMgr(editor) {
|
||||
saveLastSelection()
|
||||
}
|
||||
|
||||
this.setSelectionStartEnd = function (start, end, focus) {
|
||||
this.setSelectionStartEnd = function (start, end) {
|
||||
setSelection(start, end)
|
||||
return focus !== false && this.restoreSelection()
|
||||
return this.hasFocus() && this.restoreSelection()
|
||||
}
|
||||
|
||||
this.saveSelectionState = (function () {
|
||||
@ -230,48 +238,50 @@ function SelectionMgr(editor) {
|
||||
}
|
||||
|
||||
function save() {
|
||||
var selectionStart = self.selectionStart
|
||||
var selectionEnd = self.selectionEnd
|
||||
var selection = editor.$window.getSelection()
|
||||
var result
|
||||
if (selection.rangeCount > 0) {
|
||||
var selectionRange = selection.getRangeAt(0)
|
||||
var node = selectionRange.startContainer
|
||||
if ((contentElt.compareDocumentPosition(node) & window.Node.DOCUMENT_POSITION_CONTAINED_BY) || contentElt === node) {
|
||||
var offset = selectionRange.startOffset
|
||||
if (node.firstChild && offset > 0) {
|
||||
node = node.childNodes[offset - 1]
|
||||
offset = node.textContent.length
|
||||
}
|
||||
var container = node
|
||||
while (node !== contentElt) {
|
||||
while ((node = node.previousSibling)) {
|
||||
offset += (node.textContent || '').length
|
||||
if (self.hasFocus()) {
|
||||
var selectionStart = self.selectionStart
|
||||
var selectionEnd = self.selectionEnd
|
||||
var selection = editor.$window.getSelection()
|
||||
if (selection.rangeCount > 0) {
|
||||
var selectionRange = selection.getRangeAt(0)
|
||||
var node = selectionRange.startContainer
|
||||
if ((contentElt.compareDocumentPosition(node) & window.Node.DOCUMENT_POSITION_CONTAINED_BY) || contentElt === node) {
|
||||
var offset = selectionRange.startOffset
|
||||
if (node.firstChild && offset > 0) {
|
||||
node = node.childNodes[offset - 1]
|
||||
offset = node.textContent.length
|
||||
}
|
||||
var container = node
|
||||
while (node !== contentElt) {
|
||||
while ((node = node.previousSibling)) {
|
||||
offset += (node.textContent || '').length
|
||||
}
|
||||
node = container = container.parentNode
|
||||
}
|
||||
var selectionText = selectionRange + ''
|
||||
// Fix end of line when only br is selected
|
||||
var brElt = selectionRange.endContainer.firstChild
|
||||
if (brElt && brElt.tagName === 'BR' && selectionRange.endOffset === 1) {
|
||||
selectionText += '\n'
|
||||
}
|
||||
if (comparePoints(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset) === 1) {
|
||||
selectionStart = offset + selectionText.length
|
||||
selectionEnd = offset
|
||||
} else {
|
||||
selectionStart = offset
|
||||
selectionEnd = offset + selectionText.length
|
||||
}
|
||||
node = container = container.parentNode
|
||||
}
|
||||
var selectionText = selectionRange + ''
|
||||
// Fix end of line when only br is selected
|
||||
var brElt = selectionRange.endContainer.firstChild
|
||||
if (brElt && brElt.tagName === 'BR' && selectionRange.endOffset === 1) {
|
||||
selectionText += '\n'
|
||||
}
|
||||
if (comparePoints(selection.anchorNode, selection.anchorOffset, selection.focusNode, selection.focusOffset) === 1) {
|
||||
selectionStart = offset + selectionText.length
|
||||
selectionEnd = offset
|
||||
} else {
|
||||
selectionStart = offset
|
||||
selectionEnd = offset + selectionText.length
|
||||
}
|
||||
|
||||
if (selectionStart === selectionEnd && selectionStart === editor.getContent().length) {
|
||||
// If cursor is after the trailingNode
|
||||
selectionStart = --selectionEnd
|
||||
result = self.setSelectionStartEnd(selectionStart, selectionEnd)
|
||||
} else {
|
||||
setSelection(selectionStart, selectionEnd)
|
||||
result = checkSelection(selectionRange)
|
||||
result = result || lastSelectionStart !== self.selectionStart // selectionRange doesn't change when selection is at the start of a section
|
||||
if (selectionStart === selectionEnd && selectionStart === editor.getContent().length) {
|
||||
// If cursor is after the trailingNode
|
||||
selectionStart = --selectionEnd
|
||||
result = self.setSelectionStartEnd(selectionStart, selectionEnd)
|
||||
} else {
|
||||
setSelection(selectionStart, selectionEnd)
|
||||
result = checkSelection(selectionRange)
|
||||
result = result || lastSelectionStart !== self.selectionStart // selectionRange doesn't change when selection is at the start of a section
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,7 +103,7 @@ function mergeText(serverText, clientText, lastMergedText) {
|
||||
const lastMergedTextDiffs = diffMatchPatch.diff_main(lastMergedText, intersectionText)
|
||||
// Keep only equalities and deletions
|
||||
.filter(diff => diff[0] !== DiffMatchPatch.DIFF_INSERT);
|
||||
diffMatchPatch.diff_cleanupSemantic(serverClientDiffs);
|
||||
diffMatchPatch.diff_cleanupSemantic(lastMergedTextDiffs);
|
||||
// Make a patch with deletions only
|
||||
const patches = diffMatchPatch.patch_make(lastMergedText, lastMergedTextDiffs);
|
||||
// Apply patch to fusion text
|
||||
|
@ -131,24 +131,25 @@ export default {
|
||||
initClEditor(opts) {
|
||||
const content = store.getters['content/current'];
|
||||
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) {
|
||||
contentId = content.id;
|
||||
currentPatchableText = diffUtils.makePatchableText(content, markerKeys, markerIdxMap);
|
||||
previousPatchableText = currentPatchableText;
|
||||
syncDiscussionMarkers(content, false);
|
||||
const contentState = store.getters['contentState/current'];
|
||||
options.content = content.text;
|
||||
options.selectionStart = contentState.selectionStart;
|
||||
options.selectionEnd = contentState.selectionEnd;
|
||||
}
|
||||
|
||||
options.patchHandler = {
|
||||
makePatches,
|
||||
applyPatches,
|
||||
reversePatches,
|
||||
};
|
||||
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 insertBeforeTocElt = this.tocElt.firstChild;
|
||||
let previewHtml = '';
|
||||
let loadingImages = [];
|
||||
this.conversionCtx.htmlSectionDiff.forEach((item) => {
|
||||
for (let i = 0; i < item[1].length; i += 1) {
|
||||
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);
|
||||
previewHtml += sectionDesc.html;
|
||||
sectionIdx += 1;
|
||||
insertBeforePreviewElt.classList.remove('modified');
|
||||
insertBeforePreviewElt = insertBeforePreviewElt.nextSibling;
|
||||
insertBeforeTocElt.classList.remove('modified');
|
||||
insertBeforeTocElt = insertBeforeTocElt.nextSibling;
|
||||
} else if (item[0] === -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
|
||||
sectionPreviewElt = document.createElement('div');
|
||||
sectionPreviewElt.className = 'cl-preview-section modified';
|
||||
sectionPreviewElt.className = 'cl-preview-section';
|
||||
sectionPreviewElt.innerHTML = html;
|
||||
if (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);
|
||||
}
|
||||
extensionSvc.sectionPreview(sectionPreviewElt, this.options);
|
||||
loadingImages = [
|
||||
...loadingImages,
|
||||
...Array.prototype.slice.call(sectionPreviewElt.getElementsByTagName('img')),
|
||||
];
|
||||
|
||||
// Create TOC section element
|
||||
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');
|
||||
if (headingElt) {
|
||||
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');
|
||||
|
||||
// Run preview async operations (image loading, mathjax...)
|
||||
const loadingImages = this.previewElt.querySelectorAll('.cl-preview-section.modified img');
|
||||
const loadedPromises = loadingImages.cl_map(imgElt => new Promise((resolve) => {
|
||||
const loadedPromises = loadingImages.map(imgElt => new Promise((resolve) => {
|
||||
if (!imgElt.src) {
|
||||
resolve();
|
||||
return;
|
||||
@ -392,7 +394,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||
const editorEndOffset = editorSvc.getEditorOffset(previewSelectionEndOffset);
|
||||
if (editorStartOffset !== undefined && editorEndOffset !== undefined) {
|
||||
editorEngineSvc.clEditor.selectionMgr.setSelectionStartEnd(
|
||||
editorStartOffset, editorEndOffset, false);
|
||||
editorStartOffset, editorEndOffset);
|
||||
}
|
||||
}
|
||||
editorSvc.previewSelectionRange = range;
|
||||
|
@ -34,6 +34,10 @@ function groupHeadings(headings, level = 1) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const containerElt = document.createElement('div');
|
||||
containerElt.className = 'hidden-rendering-container';
|
||||
document.body.appendChild(containerElt);
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Apply the template to the file content
|
||||
@ -41,7 +45,7 @@ export default {
|
||||
applyTemplate(fileId, template = {
|
||||
value: '{{{files.0.content.text}}}',
|
||||
helpers: '',
|
||||
}) {
|
||||
}, pdf = false) {
|
||||
const file = store.state.file.itemMap[fileId];
|
||||
return localDbSvc.loadItem(`${fileId}/content`)
|
||||
.then((content) => {
|
||||
@ -51,19 +55,19 @@ export default {
|
||||
const parsingCtx = markdownConversionSvc.parseSections(converter, content.text);
|
||||
const conversionCtx = markdownConversionSvc.convert(parsingCtx);
|
||||
const html = conversionCtx.htmlSectionList.map(htmlSanitizer.sanitizeHtml).join('');
|
||||
const elt = document.createElement('div');
|
||||
elt.innerHTML = html;
|
||||
containerElt.innerHTML = html;
|
||||
extensionSvc.sectionPreview(containerElt, options);
|
||||
|
||||
// Unwrap tables
|
||||
elt.querySelectorAll('.table-wrapper').cl_each((wrapperElt) => {
|
||||
containerElt.querySelectorAll('.table-wrapper').cl_each((wrapperElt) => {
|
||||
while (wrapperElt.firstChild) {
|
||||
wrapperElt.parentNode.appendChild(wrapperElt.firstChild);
|
||||
wrapperElt.parentNode.insertBefore(wrapperElt.firstChild, wrapperElt.nextSibling);
|
||||
}
|
||||
wrapperElt.parentNode.removeChild(wrapperElt);
|
||||
});
|
||||
|
||||
// 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,
|
||||
anchor: headingElt.id,
|
||||
level: parseInt(headingElt.tagName.slice(1), 10),
|
||||
@ -71,17 +75,19 @@ export default {
|
||||
}));
|
||||
const toc = groupHeadings(headings);
|
||||
const view = {
|
||||
pdf,
|
||||
files: [{
|
||||
name: file.name,
|
||||
content: {
|
||||
text: content.text,
|
||||
properties,
|
||||
yamlProperties: content.properties,
|
||||
html: elt.innerHTML,
|
||||
html: containerElt.innerHTML,
|
||||
toc,
|
||||
},
|
||||
}],
|
||||
};
|
||||
containerElt.innerHTML = '';
|
||||
|
||||
// Run template conversion in a Worker to prevent attacks from helpers
|
||||
const worker = new TemplateWorker();
|
||||
@ -111,8 +117,8 @@ export default {
|
||||
exportToDisk(fileId, type, template) {
|
||||
const file = store.state.file.itemMap[fileId];
|
||||
return this.applyTemplate(fileId, template)
|
||||
.then((res) => {
|
||||
const blob = new Blob([res], {
|
||||
.then((html) => {
|
||||
const blob = new Blob([html], {
|
||||
type: 'text/plain;charset=utf-8',
|
||||
});
|
||||
FileSaver.saveAs(blob, `${file.name}.${type}`);
|
||||
|
@ -1,21 +1,18 @@
|
||||
import 'babel-polyfill';
|
||||
import 'indexeddbshim/dist/indexeddbshim';
|
||||
import FileSaver from 'file-saver';
|
||||
import utils from './utils';
|
||||
import store from '../store';
|
||||
import welcomeFile from '../data/welcomeFile.md';
|
||||
|
||||
const indexedDB = window.indexedDB;
|
||||
const dbVersion = 1;
|
||||
const dbVersionKey = `${utils.workspaceId}/localDbVersion`;
|
||||
const dbStoreName = 'objects';
|
||||
const exportBackup = utils.queryParams.exportBackup;
|
||||
|
||||
if (!indexedDB) {
|
||||
throw new Error('Your browser is not supported. Please upgrade to the latest version.');
|
||||
if (exportBackup) {
|
||||
location.hash = '';
|
||||
}
|
||||
|
||||
const deleteMarkerMaxAge = 1000;
|
||||
const checkSponsorshipAfter = (5 * 60 * 1000) + (30 * 1000); // tokenExpirationMargin + 30 sec
|
||||
|
||||
class Connection {
|
||||
constructor() {
|
||||
@ -328,6 +325,15 @@ localDbSvc.sync()
|
||||
// Set the ready flag
|
||||
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 (!store.getters['data/loginToken'] &&
|
||||
(utils.lastOpened + utils.cleanTrashAfter < Date.now())
|
||||
@ -338,6 +344,21 @@ localDbSvc.sync()
|
||||
.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
|
||||
store.watch(
|
||||
() => store.getters['file/current'].id,
|
||||
|
@ -93,6 +93,7 @@ export default {
|
||||
resolve({
|
||||
accessToken: data.access_token,
|
||||
code: data.code,
|
||||
idToken: data.id_token,
|
||||
expiresIn: data.expires_in,
|
||||
});
|
||||
}
|
||||
@ -107,7 +108,7 @@ export default {
|
||||
},
|
||||
request(configParam, offlineCheck = false) {
|
||||
let retryAfter = 500; // 500 ms
|
||||
const maxRetryAfter = 30 * 1000; // 30 sec
|
||||
const maxRetryAfter = 10 * 1000; // 10 sec
|
||||
const config = Object.assign({}, configParam);
|
||||
config.timeout = config.timeout || networkTimeout;
|
||||
config.headers = Object.assign({}, config.headers);
|
||||
@ -151,9 +152,9 @@ export default {
|
||||
const result = {
|
||||
status: xhr.status,
|
||||
headers: parseHeaders(xhr),
|
||||
body: xhr.responseText,
|
||||
body: config.blob ? xhr.response : xhr.responseText,
|
||||
};
|
||||
if (!config.raw) {
|
||||
if (!config.raw && !config.blob) {
|
||||
try {
|
||||
result.body = JSON.parse(result.body);
|
||||
} catch (e) {
|
||||
@ -197,6 +198,9 @@ export default {
|
||||
xhr.setRequestHeader(key, `${value}`);
|
||||
}
|
||||
});
|
||||
if (config.blob) {
|
||||
xhr.responseType = 'blob';
|
||||
}
|
||||
xhr.send(config.body || null);
|
||||
})
|
||||
.catch((err) => {
|
||||
@ -227,17 +231,18 @@ function checkOffline() {
|
||||
new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
let timeout;
|
||||
const cleaner = (cb, res) => () => {
|
||||
let clean = (cb) => {
|
||||
clearTimeout(timeout);
|
||||
cb(res);
|
||||
document.head.removeChild(script);
|
||||
clean = () => {}; // Prevent from cleaning several times
|
||||
cb();
|
||||
};
|
||||
script.onload = cleaner(resolve);
|
||||
script.onerror = cleaner(reject);
|
||||
script.onload = () => clean(resolve);
|
||||
script.onerror = () => clean(reject);
|
||||
script.src = `https://apis.google.com/js/api.js?${Date.now()}`;
|
||||
try {
|
||||
document.head.appendChild(script); // This can fail with bad network
|
||||
timeout = setTimeout(cleaner(reject), networkTimeout);
|
||||
timeout = setTimeout(() => clean(reject), networkTimeout);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
|
@ -13,6 +13,15 @@ const pagedownHandler = name => () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const findReplaceOpener = type => () => {
|
||||
store.dispatch('findReplace/open', {
|
||||
type,
|
||||
findText: editorEngineSvc.clEditor.selectionMgr.hasFocus() &&
|
||||
editorEngineSvc.clEditor.selectionMgr.getSelectedText(),
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
const methods = {
|
||||
bold: pagedownHandler('bold'),
|
||||
italic: pagedownHandler('italic'),
|
||||
@ -30,13 +39,15 @@ const methods = {
|
||||
}
|
||||
return true;
|
||||
},
|
||||
find: findReplaceOpener('find'),
|
||||
replace: findReplaceOpener('replace'),
|
||||
expand(param1, param2) {
|
||||
const text = `${param1 || ''}`;
|
||||
const replacement = `${param2 || ''}`;
|
||||
if (text && replacement) {
|
||||
setTimeout(() => {
|
||||
const selectionMgr = editorEngineSvc.clEditor.selectionMgr;
|
||||
let offset = editorEngineSvc.clEditor.selectionMgr.selectionStart;
|
||||
let offset = selectionMgr.selectionStart;
|
||||
if (offset === selectionMgr.selectionEnd) {
|
||||
const range = selectionMgr.createRange(offset - text.length, offset);
|
||||
if (`${range}` === text) {
|
||||
|
@ -2,6 +2,7 @@ import store from '../../store';
|
||||
import githubHelper from './helpers/githubHelper';
|
||||
import providerUtils from './providerUtils';
|
||||
import providerRegistry from './providerRegistry';
|
||||
import utils from '../utils';
|
||||
|
||||
const savedSha = {};
|
||||
|
||||
@ -65,6 +66,55 @@ export default providerRegistry.register({
|
||||
})
|
||||
.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) {
|
||||
return {
|
||||
providerId: this.id,
|
||||
|
@ -19,6 +19,16 @@ const photosScopes = ['https://www.googleapis.com/auth/photos'];
|
||||
|
||||
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 {
|
||||
request(token, options) {
|
||||
return networkSvc.request({
|
||||
@ -37,7 +47,7 @@ export default {
|
||||
expiresOn: 0,
|
||||
});
|
||||
// Refresh token and retry
|
||||
return this.refreshToken(token.scopes, token)
|
||||
return this.refreshToken(token, token.scopes)
|
||||
.then(refreshedToken => this.request(refreshedToken, options));
|
||||
}
|
||||
throw err;
|
||||
@ -115,11 +125,12 @@ export default {
|
||||
return networkSvc.startOauth2(
|
||||
'https://accounts.google.com/o/oauth2/v2/auth', {
|
||||
client_id: clientId,
|
||||
response_type: 'token',
|
||||
scope: ['openid', ...scopes].join(' '), // Need openid for user info
|
||||
response_type: 'token id_token',
|
||||
scope: ['openid', ...scopes].join(' '),
|
||||
hd: appsDomain,
|
||||
login_hint: sub,
|
||||
prompt: silent ? 'none' : null,
|
||||
nonce: utils.uid(),
|
||||
}, silent)
|
||||
// Call the token info endpoint
|
||||
.then(data => networkSvc.request({
|
||||
@ -142,9 +153,11 @@ export default {
|
||||
scopes,
|
||||
accessToken: data.accessToken,
|
||||
expiresOn: Date.now() + (data.expiresIn * 1000),
|
||||
idToken: data.idToken,
|
||||
sub: `${res.body.sub}`,
|
||||
isLogin: !store.getters['data/loginToken'] &&
|
||||
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
|
||||
isSponsor: false,
|
||||
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
|
||||
scopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -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.
|
||||
// Save flags
|
||||
token.isLogin = existingToken.isLogin || token.isLogin;
|
||||
token.isSponsor = existingToken.isSponsor;
|
||||
token.isDrive = existingToken.isDrive || token.isDrive;
|
||||
token.isBlogger = existingToken.isBlogger || token.isBlogger;
|
||||
token.isPhotos = existingToken.isPhotos || token.isPhotos;
|
||||
token.driveFullAccess = existingToken.driveFullAccess || token.driveFullAccess;
|
||||
// Save 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
|
||||
store.dispatch('data/setGoogleToken', token);
|
||||
return token;
|
||||
}));
|
||||
},
|
||||
refreshToken(scopes, token) {
|
||||
refreshToken(token, scopes = []) {
|
||||
const sub = token.sub;
|
||||
const lastToken = store.getters['data/googleTokens'][sub];
|
||||
const mergedScopes = [...new Set([
|
||||
@ -185,8 +212,13 @@ export default {
|
||||
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
if (mergedScopes.length === lastToken.scopes.length &&
|
||||
lastToken.expiresOn > Date.now() + tokenExpirationMargin
|
||||
if (
|
||||
// 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;
|
||||
}
|
||||
@ -222,6 +254,16 @@ export default {
|
||||
google = window.google;
|
||||
});
|
||||
},
|
||||
getSponsorship(token) {
|
||||
return this.refreshToken(token)
|
||||
.then(refreshedToken => networkSvc.request({
|
||||
method: 'GET',
|
||||
url: 'userInfo',
|
||||
params: {
|
||||
idToken: refreshedToken.idToken,
|
||||
},
|
||||
}, true));
|
||||
},
|
||||
signin() {
|
||||
return this.startOauth2(driveAppDataScopes);
|
||||
},
|
||||
@ -238,7 +280,7 @@ export default {
|
||||
const result = {
|
||||
changes: [],
|
||||
};
|
||||
return this.refreshToken(driveAppDataScopes, token)
|
||||
return this.refreshToken(token, driveAppDataScopes)
|
||||
.then((refreshedToken) => {
|
||||
const getPage = (pageToken = '1') => this.request(refreshedToken, {
|
||||
method: 'GET',
|
||||
@ -262,25 +304,25 @@ export default {
|
||||
});
|
||||
},
|
||||
uploadFile(token, name, parents, media, mediaType, fileId, ifNotTooLate) {
|
||||
return this.refreshToken(getDriveScopes(token), token)
|
||||
return this.refreshToken(token, getDriveScopes(token))
|
||||
.then(refreshedToken => this.uploadFileInternal(
|
||||
refreshedToken, name, parents, media, mediaType, fileId, ifNotTooLate));
|
||||
},
|
||||
uploadAppDataFile(token, name, parents, media, fileId, ifNotTooLate) {
|
||||
return this.refreshToken(driveAppDataScopes, token)
|
||||
return this.refreshToken(token, driveAppDataScopes)
|
||||
.then(refreshedToken => this.uploadFileInternal(
|
||||
refreshedToken, name, parents, media, undefined, fileId, ifNotTooLate));
|
||||
},
|
||||
downloadFile(token, id) {
|
||||
return this.refreshToken(getDriveScopes(token), token)
|
||||
return this.refreshToken(token, getDriveScopes(token))
|
||||
.then(refreshedToken => this.downloadFileInternal(refreshedToken, id));
|
||||
},
|
||||
downloadAppDataFile(token, id) {
|
||||
return this.refreshToken(driveAppDataScopes, token)
|
||||
return this.refreshToken(token, driveAppDataScopes)
|
||||
.then(refreshedToken => this.downloadFileInternal(refreshedToken, id));
|
||||
},
|
||||
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
|
||||
.then(ifNotTooLate(refreshedToken => this.request(refreshedToken, {
|
||||
method: 'DELETE',
|
||||
@ -290,7 +332,7 @@ export default {
|
||||
uploadBlogger(
|
||||
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(() => {
|
||||
if (blogId) {
|
||||
@ -356,7 +398,7 @@ export default {
|
||||
openPicker(token, type = 'doc') {
|
||||
const scopes = type === 'img' ? photosScopes : getDriveScopes(token);
|
||||
return this.loadClientScript()
|
||||
.then(() => this.refreshToken(scopes, token))
|
||||
.then(() => this.refreshToken(token, scopes))
|
||||
.then(refreshedToken => new Promise((resolve) => {
|
||||
let picker;
|
||||
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) {
|
||||
let needSyncRestart = needSyncRestartParam;
|
||||
class SyncContext {
|
||||
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)
|
||||
.then(() => localDbSvc.loadItem(`${fileId}/content`)
|
||||
.catch(() => {})) // Item may not exist if content has not been downloaded yet
|
||||
.then(() => {
|
||||
const getFile = () => store.state.file.itemMap[fileId];
|
||||
const getContent = () => store.state.content.itemMap[`${fileId}/content`];
|
||||
const getSyncedContent = () => store.state.syncedContent.itemMap[`${fileId}/syncedContent`];
|
||||
const getSyncHistoryItem = syncLocationId => getSyncedContent().syncHistory[syncLocationId];
|
||||
const downloadedLocations = {};
|
||||
const errorLocations = {};
|
||||
|
||||
const isLocationSynced = (syncLocation) => {
|
||||
const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
|
||||
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 syncLocations = [
|
||||
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
|
||||
@ -149,16 +171,21 @@ function syncFile(fileId, needSyncRestartParam = false) {
|
||||
}
|
||||
let result;
|
||||
syncLocations.some((syncLocation) => {
|
||||
if (!errorLocations[syncLocation.id] &&
|
||||
(!downloadedLocations[syncLocation.id] || !isLocationSynced(syncLocation))
|
||||
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 provider = providerRegistry.providers[syncLocation.providerId];
|
||||
const token = provider.getToken(syncLocation);
|
||||
result = provider && token && store.dispatch('queue/doWithLocation', {
|
||||
location: syncLocation,
|
||||
promise: provider.downloadContent(token, syncLocation)
|
||||
.then((serverContent = null) => {
|
||||
downloadedLocations[syncLocation.id] = true;
|
||||
fileSyncContext.downloaded[syncLocation.id] = true;
|
||||
|
||||
const syncedContent = getSyncedContent();
|
||||
const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
|
||||
@ -192,7 +219,7 @@ function syncFile(fileId, needSyncRestartParam = false) {
|
||||
})();
|
||||
|
||||
if (!mergedContent) {
|
||||
errorLocations[syncLocation.id] = true;
|
||||
fileSyncContext.errors[syncLocation.id] = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -274,9 +301,10 @@ function syncFile(fileId, needSyncRestartParam = false) {
|
||||
}
|
||||
|
||||
// If content was just created, restart sync to create the file as well
|
||||
const syncDataByItemId = store.getters['data/syncDataByItemId'];
|
||||
if (!syncDataByItemId[fileId]) {
|
||||
needSyncRestart = true;
|
||||
if (provider === mainProvider &&
|
||||
!store.getters['data/syncDataByItemId'][fileId]
|
||||
) {
|
||||
syncContext.restart = true;
|
||||
}
|
||||
});
|
||||
})
|
||||
@ -286,7 +314,7 @@ function syncFile(fileId, needSyncRestartParam = false) {
|
||||
}
|
||||
console.error(err); // eslint-disable-line no-console
|
||||
store.dispatch('notification/error', err);
|
||||
errorLocations[syncLocation.id] = true;
|
||||
fileSyncContext.errors[syncLocation.id] = true;
|
||||
}),
|
||||
})
|
||||
.then(() => syncOneContentLocation());
|
||||
@ -304,15 +332,13 @@ function syncFile(fileId, needSyncRestartParam = false) {
|
||||
.then(() => {
|
||||
throw err;
|
||||
}))
|
||||
.then(
|
||||
() => needSyncRestart,
|
||||
(err) => {
|
||||
if (err && err.message === 'TOO_LATE') {
|
||||
// Restart sync
|
||||
return syncFile(fileId, needSyncRestart);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
.catch((err) => {
|
||||
if (err && err.message === 'TOO_LATE') {
|
||||
// Restart sync
|
||||
return syncFile(fileId, syncContext);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -385,6 +411,7 @@ function syncDataItem(dataId) {
|
||||
}
|
||||
|
||||
function sync() {
|
||||
const syncContext = new SyncContext();
|
||||
const mainToken = store.getters['data/loginToken'];
|
||||
return mainProvider.getChanges(mainToken)
|
||||
.then((changes) => {
|
||||
@ -481,8 +508,12 @@ function sync() {
|
||||
const loadedContent = store.state.content.itemMap[contentId];
|
||||
const hash = loadedContent ? loadedContent.hash : localDbSvc.hashMap.content[contentId];
|
||||
const syncData = store.getters['data/syncDataByItemId'][contentId];
|
||||
// Sync if item hash and syncData hash are inconsistent
|
||||
if (!syncData || hash !== syncData.hash) {
|
||||
if (
|
||||
// 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('/');
|
||||
}
|
||||
return fileId;
|
||||
@ -490,13 +521,13 @@ function sync() {
|
||||
return fileId;
|
||||
};
|
||||
|
||||
const syncNextFile = (needSyncRestartParam) => {
|
||||
const syncNextFile = () => {
|
||||
const fileId = getOneFileIdToSync();
|
||||
if (!fileId) {
|
||||
return needSyncRestartParam;
|
||||
return null;
|
||||
}
|
||||
return syncFile(fileId, needSyncRestartParam)
|
||||
.then(needSyncRestart => syncNextFile(needSyncRestart));
|
||||
return syncFile(fileId, syncContext)
|
||||
.then(() => syncNextFile());
|
||||
};
|
||||
|
||||
return Promise.resolve()
|
||||
@ -508,14 +539,14 @@ function sync() {
|
||||
const currentFileId = store.getters['file/current'].id;
|
||||
if (currentFileId) {
|
||||
// Sync current file first
|
||||
return syncFile(currentFileId)
|
||||
.then(needSyncRestart => syncNextFile(needSyncRestart));
|
||||
return syncFile(currentFileId, syncContext)
|
||||
.then(() => syncNextFile());
|
||||
}
|
||||
return syncNextFile();
|
||||
})
|
||||
.then(
|
||||
(needSyncRestart) => {
|
||||
if (needSyncRestart) {
|
||||
() => {
|
||||
if (syncContext.restart) {
|
||||
// Restart sync
|
||||
return sync();
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import yaml from 'js-yaml';
|
||||
import '../libs/clunderscore';
|
||||
import defaultProperties from '../data/defaultFileProperties.yml';
|
||||
|
||||
const workspaceId = 'main';
|
||||
@ -157,4 +158,57 @@ export default {
|
||||
urlParser.search += keys.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`).join('&');
|
||||
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 content from './modules/content';
|
||||
import file from './modules/file';
|
||||
import findReplace from './modules/findReplace';
|
||||
import folder from './modules/folder';
|
||||
import publishLocation from './modules/publishLocation';
|
||||
import syncLocation from './modules/syncLocation';
|
||||
@ -26,6 +27,7 @@ const store = new Vuex.Store({
|
||||
ready: false,
|
||||
offline: false,
|
||||
lastOfflineCheck: 0,
|
||||
monetizeSponsor: false,
|
||||
},
|
||||
getters: {
|
||||
allItemMap: (state) => {
|
||||
@ -33,6 +35,10 @@ const store = new Vuex.Store({
|
||||
utils.types.forEach(type => Object.assign(result, state[type].itemMap));
|
||||
return result;
|
||||
},
|
||||
isSponsor: (state, getters) => {
|
||||
const loginToken = getters['data/loginToken'];
|
||||
return state.monetizeSponsor || (loginToken && loginToken.isSponsor);
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
setReady: (state) => {
|
||||
@ -44,6 +50,12 @@ const store = new Vuex.Store({
|
||||
updateLastOfflineCheck: (state) => {
|
||||
state.lastOfflineCheck = Date.now();
|
||||
},
|
||||
setMonetizeSponsor: (state, value) => {
|
||||
state.monetizeSponsor = value;
|
||||
},
|
||||
setGoogleSponsor: (state, value) => {
|
||||
state.googleSponsor = value;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setOffline: ({ state, commit, dispatch }, value) => {
|
||||
@ -91,6 +103,7 @@ const store = new Vuex.Store({
|
||||
syncedContent,
|
||||
content,
|
||||
file,
|
||||
findReplace,
|
||||
folder,
|
||||
publishLocation,
|
||||
syncLocation,
|
||||
|
@ -6,6 +6,7 @@ import defaultSettings from '../../data/defaultSettings.yml';
|
||||
import defaultLocalSettings from '../../data/defaultLocalSettings';
|
||||
import plainHtmlTemplate from '../../data/plainHtmlTemplate.html';
|
||||
import styledHtmlTemplate from '../../data/styledHtmlTemplate.html';
|
||||
import styledHtmlWithTocTemplate from '../../data/styledHtmlWithTocTemplate.html';
|
||||
import jekyllSiteTemplate from '../../data/jekyllSiteTemplate.html';
|
||||
|
||||
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.toggleSidePreview = localSettingsToggler('showSidePreview');
|
||||
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.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', {
|
||||
sideBarPanel: value === undefined ? 'menu' : value,
|
||||
});
|
||||
@ -112,6 +135,7 @@ const additionalTemplates = {
|
||||
plainText: makeAdditionalTemplate('Plain text', '{{{files.0.content.text}}}'),
|
||||
plainHtml: makeAdditionalTemplate('Plain HTML', plainHtmlTemplate),
|
||||
styledHtml: makeAdditionalTemplate('Styled HTML', styledHtmlTemplate),
|
||||
styledHtmlWithToc: makeAdditionalTemplate('Styled HTML with TOC', styledHtmlWithTocTemplate),
|
||||
jekyllSite: makeAdditionalTemplate('Jekyll site', jekyllSiteTemplate),
|
||||
};
|
||||
module.getters.allTemplates = (state, getters) => ({
|
||||
@ -187,6 +211,10 @@ module.actions.setSyncData = setter('syncData');
|
||||
module.getters.dataSyncData = getter('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
|
||||
module.getters.tokens = getter('tokens');
|
||||
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 previewButtonWidth = 55;
|
||||
const editorTopPadding = 10;
|
||||
@ -13,6 +12,7 @@ const maxTitleMaxWidth = 800;
|
||||
const minTitleMaxWidth = 200;
|
||||
|
||||
const constants = {
|
||||
editorMinWidth: 320,
|
||||
explorerWidth: 250,
|
||||
sideBarWidth: 280,
|
||||
navigationBarHeight: 44,
|
||||
@ -28,6 +28,7 @@ function computeStyles(state, localSettings, getters, styles = {
|
||||
showPreview: localSettings.showSidePreview || !localSettings.showEditor,
|
||||
showSideBar: localSettings.showSideBar,
|
||||
showExplorer: localSettings.showExplorer,
|
||||
layoutOverflow: false,
|
||||
}) {
|
||||
styles.innerHeight = state.bodyHeight;
|
||||
if (styles.showNavigationBar) {
|
||||
@ -46,14 +47,16 @@ function computeStyles(state, localSettings, getters, styles = {
|
||||
}
|
||||
|
||||
let doublePanelWidth = styles.innerWidth - constants.buttonBarWidth;
|
||||
if (doublePanelWidth < editorMinWidth) {
|
||||
doublePanelWidth = editorMinWidth;
|
||||
styles.innerWidth = editorMinWidth + constants.buttonBarWidth;
|
||||
if (doublePanelWidth < constants.editorMinWidth) {
|
||||
doublePanelWidth = constants.editorMinWidth;
|
||||
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.showPreview = false;
|
||||
styles.layoutOverflow = false;
|
||||
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