tabs to spaces whitespace
This commit is contained in:
@@ -5,20 +5,20 @@ import config from './webpack.config'
|
|||||||
const spinner = ora('Building …')
|
const spinner = ora('Building …')
|
||||||
|
|
||||||
export default new Promise((resolve, reject) => {
|
export default new Promise((resolve, reject) => {
|
||||||
spinner.start()
|
spinner.start()
|
||||||
|
|
||||||
webpack(config, (error, stats) => {
|
webpack(config, (error, stats) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
return reject(error)
|
return reject(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stats.hasErrors()) {
|
if (stats.hasErrors()) {
|
||||||
process.stdout.write(stats.toString() + "\n");
|
process.stdout.write(stats.toString() + "\n");
|
||||||
return reject(new Error('Build failed with errors.'))
|
return reject(new Error('Build failed with errors.'))
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolve('Build complete.')
|
return resolve('Build complete.')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(success => spinner.succeed(success))
|
.then(success => spinner.succeed(success))
|
||||||
.catch(error => spinner.fail(error))
|
.catch(error => spinner.fail(error))
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ middlewares.push(historyApiFallbackMiddleware())
|
|||||||
|
|
||||||
// add webpack stuff
|
// add webpack stuff
|
||||||
middlewares.push(webpackDevMiddleware(bundler, {
|
middlewares.push(webpackDevMiddleware(bundler, {
|
||||||
publicPath: config.output.publicPath,
|
publicPath: config.output.publicPath,
|
||||||
stats: {
|
stats: {
|
||||||
colors: true,
|
colors: true,
|
||||||
chunks: false,
|
chunks: false,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// add hot reloading
|
// add hot reloading
|
||||||
@@ -30,25 +30,25 @@ middlewares.push(webpackHotMiddleware(bundler))
|
|||||||
const url = 'http://localhost'
|
const url = 'http://localhost'
|
||||||
const bs = browserSync.create()
|
const bs = browserSync.create()
|
||||||
const server = bs.init({
|
const server = bs.init({
|
||||||
server: {
|
server: {
|
||||||
baseDir: `${srcPath}/`,
|
baseDir: `${srcPath}/`,
|
||||||
middleware: middlewares,
|
middleware: middlewares,
|
||||||
},
|
},
|
||||||
files: [],
|
files: [],
|
||||||
logLevel: 'silent',
|
logLevel: 'silent',
|
||||||
open: false,
|
open: false,
|
||||||
notify: false,
|
notify: false,
|
||||||
injectChanges: false,
|
injectChanges: false,
|
||||||
ghostMode: {
|
ghostMode: {
|
||||||
clicks: false,
|
clicks: false,
|
||||||
forms: false,
|
forms: false,
|
||||||
scroll: false,
|
scroll: false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(`${url}:${server.options.get('port')}`)
|
console.log(`${url}:${server.options.get('port')}`)
|
||||||
|
|
||||||
// sass import
|
// sass import
|
||||||
bs.watch(path.join(sassImportPath, '**/!(index|index_sub).scss'), { ignoreInitial: true }, () => {
|
bs.watch(path.join(sassImportPath, '**/!(index|index_sub).scss'), { ignoreInitial: true }, () => {
|
||||||
sassImport(sassImportPath)
|
sassImport(sassImportPath)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import minimist from 'minimist'
|
|||||||
let argv = minimist(process.argv.slice(2))
|
let argv = minimist(process.argv.slice(2))
|
||||||
|
|
||||||
export function removeEmpty(array) {
|
export function removeEmpty(array) {
|
||||||
return array.filter(entry => !!entry)
|
return array.filter(entry => !!entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ifElse(condition) {
|
export function ifElse(condition) {
|
||||||
return (then, otherwise) => (condition ? then : otherwise)
|
return (then, otherwise) => (condition ? then : otherwise)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const env = argv.env || 'development'
|
export const env = argv.env || 'development'
|
||||||
@@ -23,25 +23,25 @@ export const ifProd = ifElse(isProd)
|
|||||||
export const ifTest = ifElse(isTest)
|
export const ifTest = ifElse(isTest)
|
||||||
|
|
||||||
export function sassImport(basePath) {
|
export function sassImport(basePath) {
|
||||||
const indexFileName = 'index.scss'
|
const indexFileName = 'index.scss'
|
||||||
glob.sync(`${basePath}/**/${indexFileName}`).forEach(sourceFile => {
|
glob.sync(`${basePath}/**/${indexFileName}`).forEach(sourceFile => {
|
||||||
fs.writeFileSync(sourceFile, '// This is a dynamically generated file \n\n')
|
fs.writeFileSync(sourceFile, '// This is a dynamically generated file \n\n')
|
||||||
glob.sync(`${path.dirname(sourceFile)}/*.scss`).forEach(file => {
|
glob.sync(`${path.dirname(sourceFile)}/*.scss`).forEach(file => {
|
||||||
if (path.basename(file) !== indexFileName) {
|
if (path.basename(file) !== indexFileName) {
|
||||||
fs.appendFileSync(sourceFile, `@import "${path.basename(file)}";\n`)
|
fs.appendFileSync(sourceFile, `@import "${path.basename(file)}";\n`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const indexSubFileName = 'index_sub.scss'
|
const indexSubFileName = 'index_sub.scss'
|
||||||
glob.sync(`${basePath}/**/${indexSubFileName}`).forEach(sourceFile => {
|
glob.sync(`${basePath}/**/${indexSubFileName}`).forEach(sourceFile => {
|
||||||
fs.writeFileSync(sourceFile, '// This is a dynamically generated file \n\n')
|
fs.writeFileSync(sourceFile, '// This is a dynamically generated file \n\n')
|
||||||
glob.sync(`${path.dirname(sourceFile)}/**/*.scss`).forEach(file => {
|
glob.sync(`${path.dirname(sourceFile)}/**/*.scss`).forEach(file => {
|
||||||
if (path.basename(file) !== indexSubFileName) {
|
if (path.basename(file) !== indexSubFileName) {
|
||||||
let importPath = (path.dirname(sourceFile) === path.dirname(file)) ? path.basename(file) : file
|
let importPath = (path.dirname(sourceFile) === path.dirname(file)) ? path.basename(file) : file
|
||||||
importPath = importPath.replace(`${path.dirname(sourceFile)}/`, '')
|
importPath = importPath.replace(`${path.dirname(sourceFile)}/`, '')
|
||||||
fs.appendFileSync(sourceFile, `@import "${importPath}";\n`)
|
fs.appendFileSync(sourceFile, `@import "${importPath}";\n`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,214 +13,214 @@ import { rootPath, srcPath, buildPath } from './paths'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
mode: ifDev('development', 'production'),
|
mode: ifDev('development', 'production'),
|
||||||
|
|
||||||
entry: {
|
entry: {
|
||||||
app: removeEmpty([
|
app: removeEmpty([
|
||||||
ifDev('webpack-hot-middleware/client?reload=true'),
|
ifDev('webpack-hot-middleware/client?reload=true'),
|
||||||
`${srcPath}/assets/sass/main.scss`,
|
`${srcPath}/assets/sass/main.scss`,
|
||||||
`${srcPath}/main.js`,
|
`${srcPath}/main.js`,
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
|
||||||
output: {
|
output: {
|
||||||
path: `${buildPath}/`,
|
path: `${buildPath}/`,
|
||||||
filename: `assets/js/[name]${ifProd('.[hash]', '')}.js`,
|
filename: `assets/js/[name]${ifProd('.[hash]', '')}.js`,
|
||||||
chunkFilename: `assets/js/[name]${ifProd('.[chunkhash]', '')}.js`,
|
chunkFilename: `assets/js/[name]${ifProd('.[chunkhash]', '')}.js`,
|
||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
},
|
},
|
||||||
|
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.js', '.scss', '.vue'],
|
extensions: ['.js', '.scss', '.vue'],
|
||||||
alias: {
|
alias: {
|
||||||
vue$: 'vue/dist/vue.esm.js',
|
vue$: 'vue/dist/vue.esm.js',
|
||||||
modules: path.resolve(rootPath, '../node_modules'),
|
modules: path.resolve(rootPath, '../node_modules'),
|
||||||
images: `${srcPath}/assets/images`,
|
images: `${srcPath}/assets/images`,
|
||||||
fonts: `${srcPath}/assets/fonts`,
|
fonts: `${srcPath}/assets/fonts`,
|
||||||
variables: `${srcPath}/assets/sass/variables`,
|
variables: `${srcPath}/assets/sass/variables`,
|
||||||
tiptap: path.resolve(rootPath, '../packages/tiptap/src'),
|
tiptap: path.resolve(rootPath, '../packages/tiptap/src'),
|
||||||
'tiptap-commands': path.resolve(rootPath, '../packages/tiptap-commands/src'),
|
'tiptap-commands': path.resolve(rootPath, '../packages/tiptap-commands/src'),
|
||||||
'tiptap-utils': path.resolve(rootPath, '../packages/tiptap-utils/src'),
|
'tiptap-utils': path.resolve(rootPath, '../packages/tiptap-utils/src'),
|
||||||
'tiptap-models': path.resolve(rootPath, '../packages/tiptap-models/src'),
|
'tiptap-models': path.resolve(rootPath, '../packages/tiptap-models/src'),
|
||||||
'tiptap-extensions': path.resolve(rootPath, '../packages/tiptap-extensions/src'),
|
'tiptap-extensions': path.resolve(rootPath, '../packages/tiptap-extensions/src'),
|
||||||
},
|
},
|
||||||
modules: [
|
modules: [
|
||||||
srcPath,
|
srcPath,
|
||||||
path.resolve(rootPath, '../node_modules'),
|
path.resolve(rootPath, '../node_modules'),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
devtool: ifDev('eval-source-map', 'source-map'),
|
devtool: ifDev('eval-source-map', 'source-map'),
|
||||||
|
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.vue$/,
|
test: /\.vue$/,
|
||||||
loader: 'vue-loader',
|
loader: 'vue-loader',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
exclude: [/node_modules/],
|
exclude: [/node_modules/],
|
||||||
use: {
|
use: {
|
||||||
loader: ifDev('babel-loader?cacheDirectory=true', 'babel-loader'),
|
loader: ifDev('babel-loader?cacheDirectory=true', 'babel-loader'),
|
||||||
options: {
|
options: {
|
||||||
presets: [
|
presets: [
|
||||||
'@babel/preset-env',
|
'@babel/preset-env',
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
'@babel/plugin-syntax-dynamic-import',
|
'@babel/plugin-syntax-dynamic-import',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
use: removeEmpty([
|
use: removeEmpty([
|
||||||
ifDev('vue-style-loader', MiniCssExtractPlugin.loader),
|
ifDev('vue-style-loader', MiniCssExtractPlugin.loader),
|
||||||
'css-loader',
|
'css-loader',
|
||||||
'postcss-loader',
|
'postcss-loader',
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.scss$/,
|
test: /\.scss$/,
|
||||||
use: removeEmpty([
|
use: removeEmpty([
|
||||||
ifDev('vue-style-loader', MiniCssExtractPlugin.loader),
|
ifDev('vue-style-loader', MiniCssExtractPlugin.loader),
|
||||||
'css-loader',
|
'css-loader',
|
||||||
'postcss-loader',
|
'postcss-loader',
|
||||||
'sass-loader',
|
'sass-loader',
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(png|jpe?g|gif|svg|ico)(\?.*)?$/,
|
test: /\.(png|jpe?g|gif|svg|ico)(\?.*)?$/,
|
||||||
use: {
|
use: {
|
||||||
loader: 'file-loader',
|
loader: 'file-loader',
|
||||||
options: {
|
options: {
|
||||||
name: `assets/images/[name]${ifProd('.[hash]', '')}.[ext]`,
|
name: `assets/images/[name]${ifProd('.[hash]', '')}.[ext]`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||||
use: {
|
use: {
|
||||||
loader: 'file-loader',
|
loader: 'file-loader',
|
||||||
options: {
|
options: {
|
||||||
name: `assets/fonts/[name]${ifProd('.[hash]', '')}.[ext]`,
|
name: `assets/fonts/[name]${ifProd('.[hash]', '')}.[ext]`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
// splitting out the vendor
|
// splitting out the vendor
|
||||||
optimization: {
|
optimization: {
|
||||||
namedModules: true,
|
namedModules: true,
|
||||||
splitChunks: {
|
splitChunks: {
|
||||||
name: 'vendor',
|
name: 'vendor',
|
||||||
minChunks: 2,
|
minChunks: 2,
|
||||||
},
|
},
|
||||||
noEmitOnErrors: true,
|
noEmitOnErrors: true,
|
||||||
// concatenateModules: true,
|
// concatenateModules: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
plugins: removeEmpty([
|
plugins: removeEmpty([
|
||||||
|
|
||||||
// create manifest file for server-side asset manipulation
|
// create manifest file for server-side asset manipulation
|
||||||
new ManifestPlugin({
|
new ManifestPlugin({
|
||||||
fileName: 'assets/manifest.json',
|
fileName: 'assets/manifest.json',
|
||||||
writeToFileEmit: true,
|
writeToFileEmit: true,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// define env
|
// define env
|
||||||
// new webpack.DefinePlugin({
|
// new webpack.DefinePlugin({
|
||||||
// 'process.env': {},
|
// 'process.env': {},
|
||||||
// }),
|
// }),
|
||||||
|
|
||||||
// copy static files
|
// copy static files
|
||||||
new CopyWebpackPlugin([
|
new CopyWebpackPlugin([
|
||||||
{
|
{
|
||||||
context: `${srcPath}/assets/static`,
|
context: `${srcPath}/assets/static`,
|
||||||
from: { glob: '**/*', dot: false },
|
from: { glob: '**/*', dot: false },
|
||||||
to: `${buildPath}/assets`,
|
to: `${buildPath}/assets`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
context: `${srcPath}/assets/static`,
|
context: `${srcPath}/assets/static`,
|
||||||
from: { glob: '**/*', dot: false },
|
from: { glob: '**/*', dot: false },
|
||||||
to: `${buildPath}/assets/[path][name].[hash].[ext]`,
|
to: `${buildPath}/assets/[path][name].[hash].[ext]`,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
|
|
||||||
// enable hot reloading
|
// enable hot reloading
|
||||||
ifDev(new webpack.HotModuleReplacementPlugin()),
|
ifDev(new webpack.HotModuleReplacementPlugin()),
|
||||||
|
|
||||||
// html
|
// html
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
filename: 'index.html',
|
filename: 'index.html',
|
||||||
template: `${srcPath}/index.html`,
|
template: `${srcPath}/index.html`,
|
||||||
inject: true,
|
inject: true,
|
||||||
minify: ifProd({
|
minify: ifProd({
|
||||||
removeComments: true,
|
removeComments: true,
|
||||||
collapseWhitespace: true,
|
collapseWhitespace: true,
|
||||||
removeAttributeQuotes: true,
|
removeAttributeQuotes: true,
|
||||||
}),
|
}),
|
||||||
buildVersion: new Date().valueOf(),
|
buildVersion: new Date().valueOf(),
|
||||||
chunksSortMode: 'none',
|
chunksSortMode: 'none',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
new VueLoaderPlugin(),
|
new VueLoaderPlugin(),
|
||||||
|
|
||||||
// create css files
|
// create css files
|
||||||
ifProd(new MiniCssExtractPlugin({
|
ifProd(new MiniCssExtractPlugin({
|
||||||
filename: `assets/css/[name]${ifProd('.[hash]', '')}.css`,
|
filename: `assets/css/[name]${ifProd('.[hash]', '')}.css`,
|
||||||
chunkFilename: `assets/css/[name]${ifProd('.[hash]', '')}.css`,
|
chunkFilename: `assets/css/[name]${ifProd('.[hash]', '')}.css`,
|
||||||
})),
|
})),
|
||||||
|
|
||||||
// minify css files
|
// minify css files
|
||||||
ifProd(new OptimizeCssAssetsPlugin({
|
ifProd(new OptimizeCssAssetsPlugin({
|
||||||
cssProcessorOptions: {
|
cssProcessorOptions: {
|
||||||
reduceIdents: false,
|
reduceIdents: false,
|
||||||
autoprefixer: false,
|
autoprefixer: false,
|
||||||
zindex: false,
|
zindex: false,
|
||||||
discardComments: {
|
discardComments: {
|
||||||
removeAll: true,
|
removeAll: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
|
|
||||||
// svg icons
|
// svg icons
|
||||||
new SvgStore({
|
new SvgStore({
|
||||||
prefix: 'icon--',
|
prefix: 'icon--',
|
||||||
svgoOptions: {
|
svgoOptions: {
|
||||||
plugins: [
|
plugins: [
|
||||||
{ cleanupIDs: false },
|
{ cleanupIDs: false },
|
||||||
{ collapseGroups: false },
|
{ collapseGroups: false },
|
||||||
{ removeTitle: true },
|
{ removeTitle: true },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// image optimization
|
// image optimization
|
||||||
new ImageminWebpackPlugin({
|
new ImageminWebpackPlugin({
|
||||||
optipng: ifDev(null, {
|
optipng: ifDev(null, {
|
||||||
optimizationLevel: 3,
|
optimizationLevel: 3,
|
||||||
}),
|
}),
|
||||||
jpegtran: ifDev(null, {
|
jpegtran: ifDev(null, {
|
||||||
progressive: true,
|
progressive: true,
|
||||||
quality: 80,
|
quality: 80,
|
||||||
}),
|
}),
|
||||||
svgo: ifDev(null, {
|
svgo: ifDev(null, {
|
||||||
plugins: [
|
plugins: [
|
||||||
{ cleanupIDs: false },
|
{ cleanupIDs: false },
|
||||||
{ removeViewBox: false },
|
{ removeViewBox: false },
|
||||||
{ removeUselessStrokeAndFill: false },
|
{ removeUselessStrokeAndFill: false },
|
||||||
{ removeEmptyAttrs: false },
|
{ removeEmptyAttrs: false },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
]),
|
]),
|
||||||
|
|
||||||
node: {
|
node: {
|
||||||
fs: 'empty',
|
fs: 'empty',
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import packagejson from '../../package.json'
|
|||||||
|
|
||||||
const { version } = packagejson
|
const { version } = packagejson
|
||||||
const banner = `
|
const banner = `
|
||||||
/*!
|
/*!
|
||||||
* tiptap v${version}
|
* tiptap v${version}
|
||||||
* (c) ${new Date().getFullYear()} Scrumpy UG (limited liability)
|
* (c) ${new Date().getFullYear()} Scrumpy UG (limited liability)
|
||||||
* @license MIT
|
* @license MIT
|
||||||
@@ -23,19 +23,19 @@ function genConfig(opts) {
|
|||||||
input: {
|
input: {
|
||||||
input: opts.input,
|
input: opts.input,
|
||||||
plugins: [
|
plugins: [
|
||||||
flow(),
|
flow(),
|
||||||
node(),
|
node(),
|
||||||
cjs(),
|
cjs(),
|
||||||
vue({
|
vue({
|
||||||
css: true,
|
css: true,
|
||||||
compileTemplate: true,
|
compileTemplate: true,
|
||||||
}),
|
}),
|
||||||
replace({
|
replace({
|
||||||
__VERSION__: version,
|
__VERSION__: version,
|
||||||
}),
|
}),
|
||||||
buble({
|
buble({
|
||||||
objectAssign: 'Object.assign',
|
objectAssign: 'Object.assign',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
external(id) { return !/^[\.\/]/.test(id) },
|
external(id) { return !/^[\.\/]/.test(id) },
|
||||||
},
|
},
|
||||||
@@ -44,7 +44,7 @@ function genConfig(opts) {
|
|||||||
format: opts.format,
|
format: opts.format,
|
||||||
banner,
|
banner,
|
||||||
name: 'tiptap',
|
name: 'tiptap',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.env) {
|
if (opts.env) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<a class="ad" href="https://scrumpy.io/" target="_blank">
|
<a class="ad" href="https://scrumpy.io/" target="_blank">
|
||||||
<img class="ad__image" src="https://drop.philipp-kuehn.com/api98lPyuw.png" alt="Scrumpy. Agile Planning, Made Simple. Get Started." />
|
<img class="ad__image" src="https://drop.philipp-kuehn.com/api98lPyuw.png" alt="Scrumpy. Agile Planning, Made Simple. Get Started." />
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" src="./style.scss" scoped></style>
|
<style lang="scss" src="./style.scss" scoped></style>
|
||||||
|
|||||||
@@ -1,42 +1,42 @@
|
|||||||
@import "~variables";
|
@import "~variables";
|
||||||
|
|
||||||
.ad {
|
.ad {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
transition: 0.2s transform;
|
transition: 0.2s transform;
|
||||||
margin: 3rem auto 0 auto;
|
margin: 3rem auto 0 auto;
|
||||||
width: 15rem;
|
width: 15rem;
|
||||||
|
|
||||||
@media (min-width: 1020px) {
|
@media (min-width: 1020px) {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__image {
|
&__image {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: 0.2s box-shadow;
|
transition: 0.2s box-shadow;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 2px 4px 0 rgba(black, 0.05),
|
0 2px 4px 0 rgba(black, 0.05),
|
||||||
0 2px 10px 0 rgba(black, 0.07)
|
0 2px 10px 0 rgba(black, 0.07)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover &__image {
|
&:hover &__image {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 2px 1px 0 rgba(black, 0.07),
|
0 2px 1px 0 rgba(black, 0.07),
|
||||||
0 5px 20px 0 rgba(black, 0.06),
|
0 5px 20px 0 rgba(black, 0.06),
|
||||||
0 8px 40px 0 rgba(black, 0.04)
|
0 8px 40px 0 rgba(black, 0.04)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
.page {
|
.page {
|
||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
padding: 4rem 1rem;
|
padding: 4rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__footer {
|
&__footer {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="hero">
|
<div class="hero">
|
||||||
<div class="hero__inner">
|
<div class="hero__inner">
|
||||||
<svg class="hero__logo" xmlns="http://www.w3.org/2000/svg" width="150" height="147" viewBox="0 0 150 147">
|
<svg class="hero__logo" xmlns="http://www.w3.org/2000/svg" width="150" height="147" viewBox="0 0 150 147">
|
||||||
<path fill-rule="evenodd" d="M26.078305,62.3613338 C25.7098682,65.5071869 26.7651499,68.7890473 29.2305323,71.1698395 C31.8780402,73.7265081 35.5095868,74.6264505 38.835659,73.8909922 C40.1685862,69.1852287 42.5994079,65.0738098 46.1185082,61.5792271 C46.7063387,60.995492 47.6560804,60.9988119 48.2398155,61.5866425 C48.8235507,62.1744731 48.8202307,63.1242147 48.2324001,63.7079499 C45.1597213,66.7592214 43.0224884,70.3159362 41.8118122,74.3988864 C42.3036563,76.1343979 43.2448226,77.7695377 44.6376666,79.1145915 C48.8244385,83.1577101 55.4712308,83.06028 59.4841755,78.9047541 C61.5413401,76.7744978 62.5014774,73.9953585 62.3942536,71.2471292 C64.9641607,68.2381852 67.396202,63.4176619 68.4179079,58.0024937 C80.3874868,49.1562654 93.8426151,32.568523 104.371445,5.59032072 C93.9174954,41.5317695 86.0252453,64.5167382 80.6946894,74.5452392 C78.8496917,78.0162771 77.2336561,81.3879265 75.8950435,84.6284349 C70.1999494,87.1232884 65.6357246,91.845809 62.9452121,95.9367699 C59.846433,96.4572059 57.0236331,98.34297 55.4397322,101.321854 C52.7260669,106.425517 54.6851579,112.773997 59.824176,115.506462 C61.0978472,116.183685 62.4470058,116.571386 63.7975953,116.696179 C64.912262,112.255705 67.1815584,108.483174 70.5954127,105.413494 C71.2114278,104.859583 72.15984,104.909929 72.7137505,105.525944 C73.267661,106.141959 73.2173152,107.090371 72.6013001,107.644282 C68.8800506,110.990367 66.7377922,115.262277 66.1547346,120.528599 C66.6676865,123.65414 68.5866912,126.517989 71.6128022,128.127001 C74.862469,129.854879 78.6014006,129.718966 81.5959091,128.095201 C81.5801274,123.204349 82.7835226,118.582195 85.2030509,114.25301 C85.6072107,113.52986 86.5210761,113.271267 87.2442264,113.675427 C87.9673767,114.079586 88.2259696,114.993452 87.8218099,115.716602 C85.7092106,119.496609 84.635134,123.504634 84.5967664,127.763114 C85.5479283,129.295814 86.9033354,130.608181 88.6129606,131.517205 C93.7519787,134.249669 100.11443,132.323909 102.826502,127.223244 C104.216797,124.608478 104.373706,121.672349 103.513121,119.060136 C104.520365,116.84997 105.289548,113.998031 105.615371,110.855954 C119.212334,100.723957 134.689881,80.6105128 145.607087,46.648385 C148.450672,54.5670114 150,63.1025732 150,72 C150,113.421356 116.421356,147 75,147 C33.5786438,147 0,113.421356 0,72 C0,38.4830506 21.9858616,10.1011743 52.3224198,0.48953606 C48.9402683,10.0685574 46.0643582,17.0871252 43.6946894,21.5452392 C41.7000425,25.2978155 39.9730067,28.9342266 38.5748172,32.4143498 C35.1216793,33.8554208 32.0687555,35.8637935 29.77154,37.837492 C26.6493508,37.4836279 23.4161144,38.5182716 21.0724792,40.9451768 C17.0571768,45.1031442 17.1904975,51.7456943 21.3772695,55.7888129 C22.4149324,56.7908723 23.6049613,57.5354331 24.8688331,58.0276644 C27.16428,54.0664499 30.3855184,51.0655629 34.5132451,49.0557819 C35.2580753,48.6931257 36.1558704,49.0029389 36.5185265,49.7477691 C36.8811827,50.4925993 36.5713695,51.3903944 35.8265393,51.7530505 C31.3271397,53.9437983 28.0903713,57.4597339 26.078305,62.3613338 Z"/>
|
<path fill-rule="evenodd" d="M26.078305,62.3613338 C25.7098682,65.5071869 26.7651499,68.7890473 29.2305323,71.1698395 C31.8780402,73.7265081 35.5095868,74.6264505 38.835659,73.8909922 C40.1685862,69.1852287 42.5994079,65.0738098 46.1185082,61.5792271 C46.7063387,60.995492 47.6560804,60.9988119 48.2398155,61.5866425 C48.8235507,62.1744731 48.8202307,63.1242147 48.2324001,63.7079499 C45.1597213,66.7592214 43.0224884,70.3159362 41.8118122,74.3988864 C42.3036563,76.1343979 43.2448226,77.7695377 44.6376666,79.1145915 C48.8244385,83.1577101 55.4712308,83.06028 59.4841755,78.9047541 C61.5413401,76.7744978 62.5014774,73.9953585 62.3942536,71.2471292 C64.9641607,68.2381852 67.396202,63.4176619 68.4179079,58.0024937 C80.3874868,49.1562654 93.8426151,32.568523 104.371445,5.59032072 C93.9174954,41.5317695 86.0252453,64.5167382 80.6946894,74.5452392 C78.8496917,78.0162771 77.2336561,81.3879265 75.8950435,84.6284349 C70.1999494,87.1232884 65.6357246,91.845809 62.9452121,95.9367699 C59.846433,96.4572059 57.0236331,98.34297 55.4397322,101.321854 C52.7260669,106.425517 54.6851579,112.773997 59.824176,115.506462 C61.0978472,116.183685 62.4470058,116.571386 63.7975953,116.696179 C64.912262,112.255705 67.1815584,108.483174 70.5954127,105.413494 C71.2114278,104.859583 72.15984,104.909929 72.7137505,105.525944 C73.267661,106.141959 73.2173152,107.090371 72.6013001,107.644282 C68.8800506,110.990367 66.7377922,115.262277 66.1547346,120.528599 C66.6676865,123.65414 68.5866912,126.517989 71.6128022,128.127001 C74.862469,129.854879 78.6014006,129.718966 81.5959091,128.095201 C81.5801274,123.204349 82.7835226,118.582195 85.2030509,114.25301 C85.6072107,113.52986 86.5210761,113.271267 87.2442264,113.675427 C87.9673767,114.079586 88.2259696,114.993452 87.8218099,115.716602 C85.7092106,119.496609 84.635134,123.504634 84.5967664,127.763114 C85.5479283,129.295814 86.9033354,130.608181 88.6129606,131.517205 C93.7519787,134.249669 100.11443,132.323909 102.826502,127.223244 C104.216797,124.608478 104.373706,121.672349 103.513121,119.060136 C104.520365,116.84997 105.289548,113.998031 105.615371,110.855954 C119.212334,100.723957 134.689881,80.6105128 145.607087,46.648385 C148.450672,54.5670114 150,63.1025732 150,72 C150,113.421356 116.421356,147 75,147 C33.5786438,147 0,113.421356 0,72 C0,38.4830506 21.9858616,10.1011743 52.3224198,0.48953606 C48.9402683,10.0685574 46.0643582,17.0871252 43.6946894,21.5452392 C41.7000425,25.2978155 39.9730067,28.9342266 38.5748172,32.4143498 C35.1216793,33.8554208 32.0687555,35.8637935 29.77154,37.837492 C26.6493508,37.4836279 23.4161144,38.5182716 21.0724792,40.9451768 C17.0571768,45.1031442 17.1904975,51.7456943 21.3772695,55.7888129 C22.4149324,56.7908723 23.6049613,57.5354331 24.8688331,58.0276644 C27.16428,54.0664499 30.3855184,51.0655629 34.5132451,49.0557819 C35.2580753,48.6931257 36.1558704,49.0029389 36.5185265,49.7477691 C36.8811827,50.4925993 36.5713695,51.3903944 35.8265393,51.7530505 C31.3271397,53.9437983 28.0903713,57.4597339 26.078305,62.3613338 Z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<h1>
|
<h1>
|
||||||
tiptap – a renderless rich-text editor for Vue.js
|
tiptap – a renderless rich-text editor for Vue.js
|
||||||
</h1>
|
</h1>
|
||||||
<p>
|
<p>
|
||||||
This editor is based on <a href="https://prosemirror.net">Prosemirror</a>, <em>fully extendable</em> and renderless. You can easily add custom nodes as <strong>Vue components</strong>.
|
This editor is based on <a href="https://prosemirror.net">Prosemirror</a>, <em>fully extendable</em> and renderless. You can easily add custom nodes as <strong>Vue components</strong>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" src="./style.scss" scoped></style>
|
<style lang="scss" src="./style.scss" scoped></style>
|
||||||
|
|||||||
@@ -2,23 +2,23 @@
|
|||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
|
|
||||||
background-color: $color-black;
|
background-color: $color-black;
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 3rem 1rem;
|
padding: 3rem 1rem;
|
||||||
|
|
||||||
&__inner {
|
&__inner {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: 30rem;
|
max-width: 30rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__logo {
|
&__logo {
|
||||||
width: 4rem;
|
width: 4rem;
|
||||||
height: 4rem;
|
height: 4rem;
|
||||||
|
|
||||||
path {
|
path {
|
||||||
fill: $color-white;
|
fill: $color-white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,57 +1,57 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="icon" :class="[`icon--${name}`, `icon--${size}`, { 'has-align-fix': fixAlign }]">
|
<div class="icon" :class="[`icon--${name}`, `icon--${size}`, { 'has-align-fix': fixAlign }]">
|
||||||
<svg class="icon__svg">
|
<svg class="icon__svg">
|
||||||
<use xmlns:xlink="http://www.w3.org/1999/xlink" :xlink:href="'#icon--' + name"></use>
|
<use xmlns:xlink="http://www.w3.org/1999/xlink" :xlink:href="'#icon--' + name"></use>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
name: {},
|
name: {},
|
||||||
size: {
|
size: {
|
||||||
default: 'normal',
|
default: 'normal',
|
||||||
},
|
},
|
||||||
modifier: {
|
modifier: {
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
fixAlign: {
|
fixAlign: {
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.icon {
|
.icon {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
width: 0.8rem;
|
width: 0.8rem;
|
||||||
height: 0.8rem;
|
height: 0.8rem;
|
||||||
margin: 0 .3rem;
|
margin: 0 .3rem;
|
||||||
top: -.05rem;
|
top: -.05rem;
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
|
|
||||||
// &.has-align-fix {
|
// &.has-align-fix {
|
||||||
// top: -.1rem;
|
// top: -.1rem;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
&__svg {
|
&__svg {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,16 +59,16 @@ export default {
|
|||||||
body > svg,
|
body > svg,
|
||||||
.icon use > svg,
|
.icon use > svg,
|
||||||
symbol {
|
symbol {
|
||||||
path,
|
path,
|
||||||
rect,
|
rect,
|
||||||
circle,
|
circle,
|
||||||
g {
|
g {
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
stroke: none;
|
stroke: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
*[d="M0 0h24v24H0z"] {
|
*[d="M0 0h24v24H0z"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="navigation">
|
<div class="navigation">
|
||||||
|
|
||||||
<h1 class="navigation__logo">
|
<h1 class="navigation__logo">
|
||||||
tiptap <span class="navigation__beta">beta</span>
|
tiptap <span class="navigation__beta">beta</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<a class="navigation__link" href="https://github.com/heyscrumpy/tiptap/blob/master/CONTRIBUTING.md" target="_blank">
|
<a class="navigation__link" href="https://github.com/heyscrumpy/tiptap/blob/master/CONTRIBUTING.md" target="_blank">
|
||||||
Contribute
|
Contribute
|
||||||
</a>
|
</a>
|
||||||
<a class="navigation__github-link" href="https://github.com/heyscrumpy/tiptap" target="_blank">
|
<a class="navigation__github-link" href="https://github.com/heyscrumpy/tiptap" target="_blank">
|
||||||
<icon class="navigation__icon" name="github" />
|
<icon class="navigation__icon" name="github" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Icon from 'Components/Icon'
|
import Icon from 'Components/Icon'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Icon,
|
Icon,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,54 +2,54 @@
|
|||||||
|
|
||||||
.navigation {
|
.navigation {
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
background-color: $color-black;
|
background-color: $color-black;
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
|
|
||||||
&__logo {
|
&__logo {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__beta {
|
&__beta {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
background-color: $color-white;
|
background-color: $color-white;
|
||||||
color: $color-black;
|
color: $color-black;
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.05rem;
|
letter-spacing: 0.05rem;
|
||||||
padding: 0.1rem 0.2rem;
|
padding: 0.1rem 0.2rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__link {
|
&__link {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: rgba($color-white, 0.5);
|
color: rgba($color-white, 0.5);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
padding: 0.1rem 0.5rem;
|
padding: 0.1rem 0.5rem;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
background-color: rgba($color-white, 0.1);
|
background-color: rgba($color-white, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__github-link {
|
&__github-link {
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,209 +1,209 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
<menu-bar :editor="editor">
|
<menu-bar :editor="editor">
|
||||||
<div class="menubar" slot-scope="{ commands, isActive }">
|
<div class="menubar" slot-scope="{ commands, isActive }">
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('bold') }"
|
:class="{ 'is-active': isActive('bold') }"
|
||||||
@click="commands.bold"
|
@click="commands.bold"
|
||||||
>
|
>
|
||||||
<icon name="bold" />
|
<icon name="bold" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('italic') }"
|
:class="{ 'is-active': isActive('italic') }"
|
||||||
@click="commands.italic"
|
@click="commands.italic"
|
||||||
>
|
>
|
||||||
<icon name="italic" />
|
<icon name="italic" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('strike') }"
|
:class="{ 'is-active': isActive('strike') }"
|
||||||
@click="commands.strike"
|
@click="commands.strike"
|
||||||
>
|
>
|
||||||
<icon name="strike" />
|
<icon name="strike" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('underline') }"
|
:class="{ 'is-active': isActive('underline') }"
|
||||||
@click="commands.underline"
|
@click="commands.underline"
|
||||||
>
|
>
|
||||||
<icon name="underline" />
|
<icon name="underline" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('code') }"
|
:class="{ 'is-active': isActive('code') }"
|
||||||
@click="commands.code"
|
@click="commands.code"
|
||||||
>
|
>
|
||||||
<icon name="code" />
|
<icon name="code" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('paragraph') }"
|
:class="{ 'is-active': isActive('paragraph') }"
|
||||||
@click="commands.paragraph"
|
@click="commands.paragraph"
|
||||||
>
|
>
|
||||||
<icon name="paragraph" />
|
<icon name="paragraph" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('heading', { level: 1 }) }"
|
:class="{ 'is-active': isActive('heading', { level: 1 }) }"
|
||||||
@click="commands.heading({ level: 1 })"
|
@click="commands.heading({ level: 1 })"
|
||||||
>
|
>
|
||||||
H1
|
H1
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('heading', { level: 2 }) }"
|
:class="{ 'is-active': isActive('heading', { level: 2 }) }"
|
||||||
@click="commands.heading({ level: 2 })"
|
@click="commands.heading({ level: 2 })"
|
||||||
>
|
>
|
||||||
H2
|
H2
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('heading', { level: 3 }) }"
|
:class="{ 'is-active': isActive('heading', { level: 3 }) }"
|
||||||
@click="commands.heading({ level: 3 })"
|
@click="commands.heading({ level: 3 })"
|
||||||
>
|
>
|
||||||
H3
|
H3
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('bullet_list') }"
|
:class="{ 'is-active': isActive('bullet_list') }"
|
||||||
@click="commands.bullet_list"
|
@click="commands.bullet_list"
|
||||||
>
|
>
|
||||||
<icon name="ul" />
|
<icon name="ul" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('ordered_list') }"
|
:class="{ 'is-active': isActive('ordered_list') }"
|
||||||
@click="commands.ordered_list"
|
@click="commands.ordered_list"
|
||||||
>
|
>
|
||||||
<icon name="ol" />
|
<icon name="ol" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('blockquote') }"
|
:class="{ 'is-active': isActive('blockquote') }"
|
||||||
@click="commands.blockquote"
|
@click="commands.blockquote"
|
||||||
>
|
>
|
||||||
<icon name="quote" />
|
<icon name="quote" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('code_block') }"
|
:class="{ 'is-active': isActive('code_block') }"
|
||||||
@click="commands.code_block"
|
@click="commands.code_block"
|
||||||
>
|
>
|
||||||
<icon name="code" />
|
<icon name="code" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
@click="commands.undo"
|
@click="commands.undo"
|
||||||
>
|
>
|
||||||
<icon name="undo" />
|
<icon name="undo" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
@click="commands.redo"
|
@click="commands.redo"
|
||||||
>
|
>
|
||||||
<icon name="redo" />
|
<icon name="redo" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</menu-bar>
|
</menu-bar>
|
||||||
|
|
||||||
<editor-content class="editor__content" :editor="editor" />
|
<editor-content class="editor__content" :editor="editor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Icon from 'Components/Icon'
|
import Icon from 'Components/Icon'
|
||||||
import { Editor, EditorContent, MenuBar } from 'tiptap'
|
import { Editor, EditorContent, MenuBar } from 'tiptap'
|
||||||
import {
|
import {
|
||||||
Blockquote,
|
Blockquote,
|
||||||
CodeBlock,
|
CodeBlock,
|
||||||
HardBreak,
|
HardBreak,
|
||||||
Heading,
|
Heading,
|
||||||
OrderedList,
|
OrderedList,
|
||||||
BulletList,
|
BulletList,
|
||||||
ListItem,
|
ListItem,
|
||||||
TodoItem,
|
TodoItem,
|
||||||
TodoList,
|
TodoList,
|
||||||
Bold,
|
Bold,
|
||||||
Code,
|
Code,
|
||||||
Italic,
|
Italic,
|
||||||
Link,
|
Link,
|
||||||
Strike,
|
Strike,
|
||||||
Underline,
|
Underline,
|
||||||
History,
|
History,
|
||||||
} from 'tiptap-extensions'
|
} from 'tiptap-extensions'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EditorContent,
|
EditorContent,
|
||||||
MenuBar,
|
MenuBar,
|
||||||
Icon,
|
Icon,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editor: new Editor({
|
editor: new Editor({
|
||||||
extensions: [
|
extensions: [
|
||||||
new Blockquote(),
|
new Blockquote(),
|
||||||
new BulletList(),
|
new BulletList(),
|
||||||
new CodeBlock(),
|
new CodeBlock(),
|
||||||
new HardBreak(),
|
new HardBreak(),
|
||||||
new Heading({ levels: [1, 2, 3] }),
|
new Heading({ levels: [1, 2, 3] }),
|
||||||
new ListItem(),
|
new ListItem(),
|
||||||
new OrderedList(),
|
new OrderedList(),
|
||||||
new TodoItem(),
|
new TodoItem(),
|
||||||
new TodoList(),
|
new TodoList(),
|
||||||
new Bold(),
|
new Bold(),
|
||||||
new Code(),
|
new Code(),
|
||||||
new Italic(),
|
new Italic(),
|
||||||
new Link(),
|
new Link(),
|
||||||
new Strike(),
|
new Strike(),
|
||||||
new Underline(),
|
new Underline(),
|
||||||
new History(),
|
new History(),
|
||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
<h2>
|
<h2>
|
||||||
Hi there,
|
Hi there,
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
this is a very <em>basic</em> example of tiptap.
|
this is a very <em>basic</em> example of tiptap.
|
||||||
</p>
|
</p>
|
||||||
<pre><code>body { display: none; }</code></pre>
|
<pre><code>body { display: none; }</code></pre>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
A regular list
|
A regular list
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
With regular items
|
With regular items
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<blockquote>
|
<blockquote>
|
||||||
It's amazing 👏
|
It's amazing 👏
|
||||||
<br />
|
<br />
|
||||||
– mom
|
– mom
|
||||||
</blockquote>
|
</blockquote>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.editor.destroy()
|
this.editor.destroy()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,136 +1,136 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
<editor-content class="editor__content" :editor="editor" />
|
<editor-content class="editor__content" :editor="editor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Editor, EditorContent } from 'tiptap'
|
import { Editor, EditorContent } from 'tiptap'
|
||||||
import {
|
import {
|
||||||
CodeBlockHighlight,
|
CodeBlockHighlight,
|
||||||
HardBreak,
|
HardBreak,
|
||||||
Heading,
|
Heading,
|
||||||
Bold,
|
Bold,
|
||||||
Code,
|
Code,
|
||||||
Italic,
|
Italic,
|
||||||
} from 'tiptap-extensions'
|
} from 'tiptap-extensions'
|
||||||
|
|
||||||
import javascript from 'highlight.js/lib/languages/javascript'
|
import javascript from 'highlight.js/lib/languages/javascript'
|
||||||
import css from 'highlight.js/lib/languages/css'
|
import css from 'highlight.js/lib/languages/css'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
JavaScriptExample,
|
JavaScriptExample,
|
||||||
CSSExample,
|
CSSExample,
|
||||||
ExplicitImportExample,
|
ExplicitImportExample,
|
||||||
} from './examples'
|
} from './examples'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EditorContent,
|
EditorContent,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editor: new Editor({
|
editor: new Editor({
|
||||||
extensions: [
|
extensions: [
|
||||||
new CodeBlockHighlight({
|
new CodeBlockHighlight({
|
||||||
languages: {
|
languages: {
|
||||||
javascript,
|
javascript,
|
||||||
css,
|
css,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
new HardBreak(),
|
new HardBreak(),
|
||||||
new Heading({ levels: [1, 2, 3] }),
|
new Heading({ levels: [1, 2, 3] }),
|
||||||
new Bold(),
|
new Bold(),
|
||||||
new Code(),
|
new Code(),
|
||||||
new Italic(),
|
new Italic(),
|
||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
<h2>
|
<h2>
|
||||||
Code Highlighting
|
Code Highlighting
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
These are code blocks with <strong>automatic syntax highlighting</strong> based on highlight.js.
|
These are code blocks with <strong>automatic syntax highlighting</strong> based on highlight.js.
|
||||||
</p>
|
</p>
|
||||||
<pre><code>${JavaScriptExample}</code></pre>
|
<pre><code>${JavaScriptExample}</code></pre>
|
||||||
<pre><code>${CSSExample}</code></pre>
|
<pre><code>${CSSExample}</code></pre>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Note: tiptap doesn't import syntax highlighting language definitions from highlight.js. You
|
Note: tiptap doesn't import syntax highlighting language definitions from highlight.js. You
|
||||||
<strong>must</strong> import them and initialize the extension with all languages you want to support:
|
<strong>must</strong> import them and initialize the extension with all languages you want to support:
|
||||||
</p>
|
</p>
|
||||||
<pre><code>${ExplicitImportExample}</code></pre>
|
<pre><code>${ExplicitImportExample}</code></pre>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
&::before {
|
&::before {
|
||||||
content: attr(data-language);
|
content: attr(data-language);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
display: block;
|
display: block;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
|
||||||
.hljs-comment,
|
.hljs-comment,
|
||||||
.hljs-quote {
|
.hljs-quote {
|
||||||
color: #999999;
|
color: #999999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs-variable,
|
.hljs-variable,
|
||||||
.hljs-template-variable,
|
.hljs-template-variable,
|
||||||
.hljs-attribute,
|
.hljs-attribute,
|
||||||
.hljs-tag,
|
.hljs-tag,
|
||||||
.hljs-name,
|
.hljs-name,
|
||||||
.hljs-regexp,
|
.hljs-regexp,
|
||||||
.hljs-link,
|
.hljs-link,
|
||||||
.hljs-name,
|
.hljs-name,
|
||||||
.hljs-selector-id,
|
.hljs-selector-id,
|
||||||
.hljs-selector-class {
|
.hljs-selector-class {
|
||||||
color: #f2777a;
|
color: #f2777a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs-number,
|
.hljs-number,
|
||||||
.hljs-meta,
|
.hljs-meta,
|
||||||
.hljs-built_in,
|
.hljs-built_in,
|
||||||
.hljs-builtin-name,
|
.hljs-builtin-name,
|
||||||
.hljs-literal,
|
.hljs-literal,
|
||||||
.hljs-type,
|
.hljs-type,
|
||||||
.hljs-params {
|
.hljs-params {
|
||||||
color: #f99157;
|
color: #f99157;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs-string,
|
.hljs-string,
|
||||||
.hljs-symbol,
|
.hljs-symbol,
|
||||||
.hljs-bullet {
|
.hljs-bullet {
|
||||||
color: #99cc99;
|
color: #99cc99;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs-title,
|
.hljs-title,
|
||||||
.hljs-section {
|
.hljs-section {
|
||||||
color: #ffcc66;
|
color: #ffcc66;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs-keyword,
|
.hljs-keyword,
|
||||||
.hljs-selector-tag {
|
.hljs-selector-tag {
|
||||||
color: #6699cc;
|
color: #6699cc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs-emphasis {
|
.hljs-emphasis {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hljs-strong {
|
.hljs-strong {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,57 +2,57 @@ import { Node } from 'tiptap'
|
|||||||
|
|
||||||
export default class Iframe extends Node {
|
export default class Iframe extends Node {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'iframe'
|
return 'iframe'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
attrs: {
|
attrs: {
|
||||||
src: {
|
src: {
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
group: 'block',
|
group: 'block',
|
||||||
selectable: false,
|
selectable: false,
|
||||||
parseDOM: [{
|
parseDOM: [{
|
||||||
tag: 'iframe',
|
tag: 'iframe',
|
||||||
getAttrs: dom => ({
|
getAttrs: dom => ({
|
||||||
src: dom.getAttribute('src'),
|
src: dom.getAttribute('src'),
|
||||||
}),
|
}),
|
||||||
}],
|
}],
|
||||||
toDOM: node => ['iframe', {
|
toDOM: node => ['iframe', {
|
||||||
src: node.attrs.src,
|
src: node.attrs.src,
|
||||||
frameborder: 0,
|
frameborder: 0,
|
||||||
allowfullscreen: 'true',
|
allowfullscreen: 'true',
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get view() {
|
get view() {
|
||||||
return {
|
return {
|
||||||
props: ['node', 'updateAttrs', 'editable'],
|
props: ['node', 'updateAttrs', 'editable'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
url: this.node.attrs.src,
|
url: this.node.attrs.src,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onChange(event) {
|
onChange(event) {
|
||||||
this.url = event.target.value
|
this.url = event.target.value
|
||||||
|
|
||||||
this.updateAttrs({
|
this.updateAttrs({
|
||||||
src: this.url,
|
src: this.url,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div class="iframe">
|
<div class="iframe">
|
||||||
<iframe class="iframe__embed" :src="url"></iframe>
|
<iframe class="iframe__embed" :src="url"></iframe>
|
||||||
<input class="iframe__input" type="text" :value="url" @input="onChange" v-if="editable" />
|
<input class="iframe__input" type="text" :value="url" @input="onChange" v-if="editable" />
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
<editor-content class="editor__content" :editor="editor" />
|
<editor-content class="editor__content" :editor="editor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Editor, EditorContent } from 'tiptap'
|
import { Editor, EditorContent } from 'tiptap'
|
||||||
import {
|
import {
|
||||||
HardBreak,
|
HardBreak,
|
||||||
Heading,
|
Heading,
|
||||||
Bold,
|
Bold,
|
||||||
Italic,
|
Italic,
|
||||||
History,
|
History,
|
||||||
} from 'tiptap-extensions'
|
} from 'tiptap-extensions'
|
||||||
import Iframe from './Iframe.js'
|
import Iframe from './Iframe.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EditorContent,
|
EditorContent,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editor: new Editor({
|
editor: new Editor({
|
||||||
extensions: [
|
extensions: [
|
||||||
new HardBreak(),
|
new HardBreak(),
|
||||||
new Heading({ levels: [1, 2, 3] }),
|
new Heading({ levels: [1, 2, 3] }),
|
||||||
new Bold(),
|
new Bold(),
|
||||||
new Italic(),
|
new Italic(),
|
||||||
new History(),
|
new History(),
|
||||||
// custom extension
|
// custom extension
|
||||||
new Iframe(),
|
new Iframe(),
|
||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
<h2>
|
<h2>
|
||||||
Embeds
|
Embeds
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
This is an example of a custom iframe node. This iframe is rendered as a <strong>vue component</strong>. This makes it possible to render the input below to change its source.
|
This is an example of a custom iframe node. This iframe is rendered as a <strong>vue component</strong>. This makes it possible to render the input below to change its source.
|
||||||
</p>
|
</p>
|
||||||
<iframe src="https://www.youtube.com/embed/XIMLoLxmTDw" frameborder="0" allowfullscreen></iframe>
|
<iframe src="https://www.youtube.com/embed/XIMLoLxmTDw" frameborder="0" allowfullscreen></iframe>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.editor.destroy()
|
this.editor.destroy()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -53,20 +53,20 @@ export default {
|
|||||||
@import "~variables";
|
@import "~variables";
|
||||||
|
|
||||||
.iframe {
|
.iframe {
|
||||||
&__embed {
|
&__embed {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 15rem;
|
height: 15rem;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__input {
|
&__input {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: rgba($color-black, 0.1);
|
background-color: rgba($color-black, 0.1);
|
||||||
padding: 0.3rem 0.5rem;
|
padding: 0.3rem 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,203 +1,203 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
<menu-bar :editor="editor">
|
<menu-bar :editor="editor">
|
||||||
<div class="menubar" slot-scope="{ commands, isActive }">
|
<div class="menubar" slot-scope="{ commands, isActive }">
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('bold') }"
|
:class="{ 'is-active': isActive('bold') }"
|
||||||
@click="commands.bold"
|
@click="commands.bold"
|
||||||
>
|
>
|
||||||
<icon name="bold" />
|
<icon name="bold" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('italic') }"
|
:class="{ 'is-active': isActive('italic') }"
|
||||||
@click="commands.italic"
|
@click="commands.italic"
|
||||||
>
|
>
|
||||||
<icon name="italic" />
|
<icon name="italic" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('code') }"
|
:class="{ 'is-active': isActive('code') }"
|
||||||
@click="commands.code"
|
@click="commands.code"
|
||||||
>
|
>
|
||||||
<icon name="code" />
|
<icon name="code" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('paragraph') }"
|
:class="{ 'is-active': isActive('paragraph') }"
|
||||||
@click="commands.paragraph"
|
@click="commands.paragraph"
|
||||||
>
|
>
|
||||||
<icon name="paragraph" />
|
<icon name="paragraph" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('heading', { level: 1 }) }"
|
:class="{ 'is-active': isActive('heading', { level: 1 }) }"
|
||||||
@click="commands.heading({ level: 1 })"
|
@click="commands.heading({ level: 1 })"
|
||||||
>
|
>
|
||||||
H1
|
H1
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('heading', { level: 2 }) }"
|
:class="{ 'is-active': isActive('heading', { level: 2 }) }"
|
||||||
@click="commands.heading({ level: 2 })"
|
@click="commands.heading({ level: 2 })"
|
||||||
>
|
>
|
||||||
H2
|
H2
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('heading', { level: 3 }) }"
|
:class="{ 'is-active': isActive('heading', { level: 3 }) }"
|
||||||
@click="commands.heading({ level: 3 })"
|
@click="commands.heading({ level: 3 })"
|
||||||
>
|
>
|
||||||
H3
|
H3
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('bullet_list') }"
|
:class="{ 'is-active': isActive('bullet_list') }"
|
||||||
@click="commands.bullet_list"
|
@click="commands.bullet_list"
|
||||||
>
|
>
|
||||||
<icon name="ul" />
|
<icon name="ul" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('ordered_list') }"
|
:class="{ 'is-active': isActive('ordered_list') }"
|
||||||
@click="commands.ordered_list"
|
@click="commands.ordered_list"
|
||||||
>
|
>
|
||||||
<icon name="ol" />
|
<icon name="ol" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('code_block') }"
|
:class="{ 'is-active': isActive('code_block') }"
|
||||||
@click="commands.code_block"
|
@click="commands.code_block"
|
||||||
>
|
>
|
||||||
<icon name="code" />
|
<icon name="code" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</menu-bar>
|
</menu-bar>
|
||||||
|
|
||||||
<editor-content class="editor__content" :editor="editor" />
|
<editor-content class="editor__content" :editor="editor" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="button" @click="clearContent">
|
<button class="button" @click="clearContent">
|
||||||
Clear Content
|
Clear Content
|
||||||
</button>
|
</button>
|
||||||
<button class="button" @click="setContent">
|
<button class="button" @click="setContent">
|
||||||
Set Content
|
Set Content
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="export">
|
<div class="export">
|
||||||
<h3>JSON</h3>
|
<h3>JSON</h3>
|
||||||
<pre><code v-html="json"></code></pre>
|
<pre><code v-html="json"></code></pre>
|
||||||
|
|
||||||
<h3>HTML</h3>
|
<h3>HTML</h3>
|
||||||
<pre><code>{{ html }}</code></pre>
|
<pre><code>{{ html }}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Icon from 'Components/Icon'
|
import Icon from 'Components/Icon'
|
||||||
import { Editor, EditorContent, MenuBar } from 'tiptap'
|
import { Editor, EditorContent, MenuBar } from 'tiptap'
|
||||||
import {
|
import {
|
||||||
Blockquote,
|
Blockquote,
|
||||||
CodeBlock,
|
CodeBlock,
|
||||||
HardBreak,
|
HardBreak,
|
||||||
Heading,
|
Heading,
|
||||||
OrderedList,
|
OrderedList,
|
||||||
BulletList,
|
BulletList,
|
||||||
ListItem,
|
ListItem,
|
||||||
TodoItem,
|
TodoItem,
|
||||||
TodoList,
|
TodoList,
|
||||||
Bold,
|
Bold,
|
||||||
Code,
|
Code,
|
||||||
Italic,
|
Italic,
|
||||||
Link,
|
Link,
|
||||||
History,
|
History,
|
||||||
} from 'tiptap-extensions'
|
} from 'tiptap-extensions'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EditorContent,
|
EditorContent,
|
||||||
MenuBar,
|
MenuBar,
|
||||||
Icon,
|
Icon,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editor: new Editor({
|
editor: new Editor({
|
||||||
extensions: [
|
extensions: [
|
||||||
new Blockquote(),
|
new Blockquote(),
|
||||||
new BulletList(),
|
new BulletList(),
|
||||||
new CodeBlock(),
|
new CodeBlock(),
|
||||||
new HardBreak(),
|
new HardBreak(),
|
||||||
new Heading({ levels: [1, 2, 3] }),
|
new Heading({ levels: [1, 2, 3] }),
|
||||||
new ListItem(),
|
new ListItem(),
|
||||||
new OrderedList(),
|
new OrderedList(),
|
||||||
new TodoItem(),
|
new TodoItem(),
|
||||||
new TodoList(),
|
new TodoList(),
|
||||||
new Bold(),
|
new Bold(),
|
||||||
new Code(),
|
new Code(),
|
||||||
new Italic(),
|
new Italic(),
|
||||||
new Link(),
|
new Link(),
|
||||||
new History(),
|
new History(),
|
||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
<h2>
|
<h2>
|
||||||
Export HTML or JSON
|
Export HTML or JSON
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
You are able to export your data as <code>HTML</code> or <code>JSON</code>. To pass <code>HTML</code> to the editor use the <code>content</code> slot. To pass <code>JSON</code> to the editor use the <code>doc</code> prop.
|
You are able to export your data as <code>HTML</code> or <code>JSON</code>. To pass <code>HTML</code> to the editor use the <code>content</code> slot. To pass <code>JSON</code> to the editor use the <code>doc</code> prop.
|
||||||
</p>
|
</p>
|
||||||
`,
|
`,
|
||||||
onUpdate: ({ getJSON, getHTML }) => {
|
onUpdate: ({ getJSON, getHTML }) => {
|
||||||
this.json = getJSON()
|
this.json = getJSON()
|
||||||
this.html = getHTML()
|
this.html = getHTML()
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
json: 'Update content to see changes',
|
json: 'Update content to see changes',
|
||||||
html: 'Update content to see changes',
|
html: 'Update content to see changes',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clearContent() {
|
clearContent() {
|
||||||
this.editor.clearContent(true)
|
this.editor.clearContent(true)
|
||||||
this.editor.focus()
|
this.editor.focus()
|
||||||
},
|
},
|
||||||
setContent() {
|
setContent() {
|
||||||
// you can pass a json document
|
// you can pass a json document
|
||||||
this.editor.setContent({
|
this.editor.setContent({
|
||||||
type: 'doc',
|
type: 'doc',
|
||||||
content: [{
|
content: [{
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: 'This is some inserted text. 👋',
|
text: 'This is some inserted text. 👋',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
}, true)
|
}, true)
|
||||||
|
|
||||||
// HTML string is also supported
|
// HTML string is also supported
|
||||||
// this.editor.setContent('<p>This is some inserted text. 👋</p>')
|
// this.editor.setContent('<p>This is some inserted text. 👋</p>')
|
||||||
|
|
||||||
this.editor.focus()
|
this.editor.focus()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -205,27 +205,27 @@ export default {
|
|||||||
@import "~variables";
|
@import "~variables";
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
max-width: 30rem;
|
max-width: 30rem;
|
||||||
margin: 0 auto 2rem auto;
|
margin: 0 auto 2rem auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.export {
|
.export {
|
||||||
|
|
||||||
max-width: 30rem;
|
max-width: 30rem;
|
||||||
margin: 0 auto 2rem auto;
|
margin: 0 auto 2rem auto;
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
background: rgba($color-black, 0.05);
|
background: rgba($color-black, 0.05);
|
||||||
color: rgba($color-black, 0.8);
|
color: rgba($color-black, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
display: block;
|
display: block;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,132 +1,132 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
<floating-menu :editor="editor">
|
<floating-menu :editor="editor">
|
||||||
<div
|
<div
|
||||||
slot-scope="{ commands, isActive, menu }"
|
slot-scope="{ commands, isActive, menu }"
|
||||||
class="editor__floating-menu"
|
class="editor__floating-menu"
|
||||||
:class="{ 'is-active': menu.isActive }"
|
:class="{ 'is-active': menu.isActive }"
|
||||||
:style="`top: ${menu.top}px`"
|
:style="`top: ${menu.top}px`"
|
||||||
>
|
>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('heading', { level: 1 }) }"
|
:class="{ 'is-active': isActive('heading', { level: 1 }) }"
|
||||||
@click="commands.heading({ level: 1 })"
|
@click="commands.heading({ level: 1 })"
|
||||||
>
|
>
|
||||||
H1
|
H1
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('heading', { level: 2 }) }"
|
:class="{ 'is-active': isActive('heading', { level: 2 }) }"
|
||||||
@click="commands.heading({ level: 2 })"
|
@click="commands.heading({ level: 2 })"
|
||||||
>
|
>
|
||||||
H2
|
H2
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('heading', { level: 3 }) }"
|
:class="{ 'is-active': isActive('heading', { level: 3 }) }"
|
||||||
@click="commands.heading({ level: 3 })"
|
@click="commands.heading({ level: 3 })"
|
||||||
>
|
>
|
||||||
H3
|
H3
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('bullet_list') }"
|
:class="{ 'is-active': isActive('bullet_list') }"
|
||||||
@click="commands.bullet_list"
|
@click="commands.bullet_list"
|
||||||
>
|
>
|
||||||
<icon name="ul" />
|
<icon name="ul" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('ordered_list') }"
|
:class="{ 'is-active': isActive('ordered_list') }"
|
||||||
@click="commands.ordered_list"
|
@click="commands.ordered_list"
|
||||||
>
|
>
|
||||||
<icon name="ol" />
|
<icon name="ol" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('blockquote') }"
|
:class="{ 'is-active': isActive('blockquote') }"
|
||||||
@click="commands.blockquote"
|
@click="commands.blockquote"
|
||||||
>
|
>
|
||||||
<icon name="quote" />
|
<icon name="quote" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('code_block') }"
|
:class="{ 'is-active': isActive('code_block') }"
|
||||||
@click="commands.code_block"
|
@click="commands.code_block"
|
||||||
>
|
>
|
||||||
<icon name="code" />
|
<icon name="code" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</floating-menu>
|
</floating-menu>
|
||||||
|
|
||||||
<editor-content class="editor__content" :editor="editor" />
|
<editor-content class="editor__content" :editor="editor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Icon from 'Components/Icon'
|
import Icon from 'Components/Icon'
|
||||||
import { Editor, EditorContent, FloatingMenu } from 'tiptap'
|
import { Editor, EditorContent, FloatingMenu } from 'tiptap'
|
||||||
import {
|
import {
|
||||||
Blockquote,
|
Blockquote,
|
||||||
BulletList,
|
BulletList,
|
||||||
CodeBlock,
|
CodeBlock,
|
||||||
HardBreak,
|
HardBreak,
|
||||||
Heading,
|
Heading,
|
||||||
ListItem,
|
ListItem,
|
||||||
OrderedList,
|
OrderedList,
|
||||||
TodoItem,
|
TodoItem,
|
||||||
TodoList,
|
TodoList,
|
||||||
Bold,
|
Bold,
|
||||||
Code,
|
Code,
|
||||||
Italic,
|
Italic,
|
||||||
Link,
|
Link,
|
||||||
History,
|
History,
|
||||||
} from 'tiptap-extensions'
|
} from 'tiptap-extensions'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EditorContent,
|
EditorContent,
|
||||||
FloatingMenu,
|
FloatingMenu,
|
||||||
Icon,
|
Icon,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editor: new Editor({
|
editor: new Editor({
|
||||||
extensions: [
|
extensions: [
|
||||||
new Blockquote(),
|
new Blockquote(),
|
||||||
new BulletList(),
|
new BulletList(),
|
||||||
new CodeBlock(),
|
new CodeBlock(),
|
||||||
new HardBreak(),
|
new HardBreak(),
|
||||||
new Heading({ levels: [1, 2, 3] }),
|
new Heading({ levels: [1, 2, 3] }),
|
||||||
new ListItem(),
|
new ListItem(),
|
||||||
new OrderedList(),
|
new OrderedList(),
|
||||||
new TodoItem(),
|
new TodoItem(),
|
||||||
new TodoList(),
|
new TodoList(),
|
||||||
new Bold(),
|
new Bold(),
|
||||||
new Code(),
|
new Code(),
|
||||||
new Italic(),
|
new Italic(),
|
||||||
new Link(),
|
new Link(),
|
||||||
new History(),
|
new History(),
|
||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
<h2>
|
<h2>
|
||||||
Floating Menu
|
Floating Menu
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
This is an example of a medium-like editor. Enter a new line and some buttons will appear.
|
This is an example of a medium-like editor. Enter a new line and some buttons will appear.
|
||||||
</p>
|
</p>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -135,20 +135,20 @@ export default {
|
|||||||
|
|
||||||
.editor {
|
.editor {
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&__floating-menu {
|
&__floating-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
margin-top: -0.25rem;
|
margin-top: -0.25rem;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s, visibility 0.2s;
|
transition: opacity 0.2s, visibility 0.2s;
|
||||||
|
|
||||||
&.is-active {
|
&.is-active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,185 +1,185 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
<menu-bar :editor="editor">
|
<menu-bar :editor="editor">
|
||||||
<div
|
<div
|
||||||
class="menubar is-hidden"
|
class="menubar is-hidden"
|
||||||
:class="{ 'is-focused': focused }"
|
:class="{ 'is-focused': focused }"
|
||||||
slot-scope="{ commands, isActive, focused }"
|
slot-scope="{ commands, isActive, focused }"
|
||||||
>
|
>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('bold') }"
|
:class="{ 'is-active': isActive('bold') }"
|
||||||
@click="commands.bold"
|
@click="commands.bold"
|
||||||
>
|
>
|
||||||
<icon name="bold" />
|
<icon name="bold" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('italic') }"
|
:class="{ 'is-active': isActive('italic') }"
|
||||||
@click="commands.italic"
|
@click="commands.italic"
|
||||||
>
|
>
|
||||||
<icon name="italic" />
|
<icon name="italic" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('strike') }"
|
:class="{ 'is-active': isActive('strike') }"
|
||||||
@click="commands.strike"
|
@click="commands.strike"
|
||||||
>
|
>
|
||||||
<icon name="strike" />
|
<icon name="strike" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('underline') }"
|
:class="{ 'is-active': isActive('underline') }"
|
||||||
@click="commands.underline"
|
@click="commands.underline"
|
||||||
>
|
>
|
||||||
<icon name="underline" />
|
<icon name="underline" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('code') }"
|
:class="{ 'is-active': isActive('code') }"
|
||||||
@click="commands.code"
|
@click="commands.code"
|
||||||
>
|
>
|
||||||
<icon name="code" />
|
<icon name="code" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('paragraph') }"
|
:class="{ 'is-active': isActive('paragraph') }"
|
||||||
@click="commands.paragraph"
|
@click="commands.paragraph"
|
||||||
>
|
>
|
||||||
<icon name="paragraph" />
|
<icon name="paragraph" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('heading', { level: 1 }) }"
|
:class="{ 'is-active': isActive('heading', { level: 1 }) }"
|
||||||
@click="commands.heading({ level: 1 })"
|
@click="commands.heading({ level: 1 })"
|
||||||
>
|
>
|
||||||
H1
|
H1
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('heading', { level: 2 }) }"
|
:class="{ 'is-active': isActive('heading', { level: 2 }) }"
|
||||||
@click="commands.heading({ level: 2 })"
|
@click="commands.heading({ level: 2 })"
|
||||||
>
|
>
|
||||||
H2
|
H2
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('heading', { level: 3 }) }"
|
:class="{ 'is-active': isActive('heading', { level: 3 }) }"
|
||||||
@click="commands.heading({ level: 3 })"
|
@click="commands.heading({ level: 3 })"
|
||||||
>
|
>
|
||||||
H3
|
H3
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('bullet_list') }"
|
:class="{ 'is-active': isActive('bullet_list') }"
|
||||||
@click="commands.bullet_list"
|
@click="commands.bullet_list"
|
||||||
>
|
>
|
||||||
<icon name="ul" />
|
<icon name="ul" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('ordered_list') }"
|
:class="{ 'is-active': isActive('ordered_list') }"
|
||||||
@click="commands.ordered_list"
|
@click="commands.ordered_list"
|
||||||
>
|
>
|
||||||
<icon name="ol" />
|
<icon name="ol" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('blockquote') }"
|
:class="{ 'is-active': isActive('blockquote') }"
|
||||||
@click="commands.blockquote"
|
@click="commands.blockquote"
|
||||||
>
|
>
|
||||||
<icon name="quote" />
|
<icon name="quote" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('code_block') }"
|
:class="{ 'is-active': isActive('code_block') }"
|
||||||
@click="commands.code_block"
|
@click="commands.code_block"
|
||||||
>
|
>
|
||||||
<icon name="code" />
|
<icon name="code" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</menu-bar>
|
</menu-bar>
|
||||||
|
|
||||||
<editor-content class="editor__content" :editor="editor" />
|
<editor-content class="editor__content" :editor="editor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Icon from 'Components/Icon'
|
import Icon from 'Components/Icon'
|
||||||
import { Editor, EditorContent, MenuBar } from 'tiptap'
|
import { Editor, EditorContent, MenuBar } from 'tiptap'
|
||||||
import {
|
import {
|
||||||
Blockquote,
|
Blockquote,
|
||||||
BulletList,
|
BulletList,
|
||||||
CodeBlock,
|
CodeBlock,
|
||||||
HardBreak,
|
HardBreak,
|
||||||
Heading,
|
Heading,
|
||||||
ListItem,
|
ListItem,
|
||||||
OrderedList,
|
OrderedList,
|
||||||
TodoItem,
|
TodoItem,
|
||||||
TodoList,
|
TodoList,
|
||||||
Bold,
|
Bold,
|
||||||
Code,
|
Code,
|
||||||
Italic,
|
Italic,
|
||||||
Link,
|
Link,
|
||||||
Strike,
|
Strike,
|
||||||
Underline,
|
Underline,
|
||||||
History,
|
History,
|
||||||
} from 'tiptap-extensions'
|
} from 'tiptap-extensions'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EditorContent,
|
EditorContent,
|
||||||
MenuBar,
|
MenuBar,
|
||||||
Icon,
|
Icon,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editor: new Editor({
|
editor: new Editor({
|
||||||
extensions: [
|
extensions: [
|
||||||
new Blockquote(),
|
new Blockquote(),
|
||||||
new BulletList(),
|
new BulletList(),
|
||||||
new CodeBlock(),
|
new CodeBlock(),
|
||||||
new HardBreak(),
|
new HardBreak(),
|
||||||
new Heading({ levels: [1, 2, 3] }),
|
new Heading({ levels: [1, 2, 3] }),
|
||||||
new ListItem(),
|
new ListItem(),
|
||||||
new OrderedList(),
|
new OrderedList(),
|
||||||
new TodoItem(),
|
new TodoItem(),
|
||||||
new TodoList(),
|
new TodoList(),
|
||||||
new Bold(),
|
new Bold(),
|
||||||
new Code(),
|
new Code(),
|
||||||
new Italic(),
|
new Italic(),
|
||||||
new Link(),
|
new Link(),
|
||||||
new Strike(),
|
new Strike(),
|
||||||
new Underline(),
|
new Underline(),
|
||||||
new History(),
|
new History(),
|
||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
<h2>
|
<h2>
|
||||||
Hiding Menu Bar
|
Hiding Menu Bar
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
Click into this text to see the menu. Click outside and the menu will disappear. It's like magic.
|
Click into this text to see the menu. Click outside and the menu will disappear. It's like magic.
|
||||||
</p>
|
</p>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.editor.destroy()
|
this.editor.destroy()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,68 +1,68 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
<menu-bar :editor="editor">
|
<menu-bar :editor="editor">
|
||||||
<div class="menubar" slot-scope="{ commands }">
|
<div class="menubar" slot-scope="{ commands }">
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
@click="showImagePrompt(commands.image)"
|
@click="showImagePrompt(commands.image)"
|
||||||
>
|
>
|
||||||
<icon name="image" />
|
<icon name="image" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</menu-bar>
|
</menu-bar>
|
||||||
|
|
||||||
<editor-content class="editor__content" :editor="editor" />
|
<editor-content class="editor__content" :editor="editor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Icon from 'Components/Icon'
|
import Icon from 'Components/Icon'
|
||||||
import { Editor, EditorContent, MenuBar } from 'tiptap'
|
import { Editor, EditorContent, MenuBar } from 'tiptap'
|
||||||
import {
|
import {
|
||||||
HardBreak,
|
HardBreak,
|
||||||
Heading,
|
Heading,
|
||||||
Image,
|
Image,
|
||||||
Bold,
|
Bold,
|
||||||
Code,
|
Code,
|
||||||
Italic,
|
Italic,
|
||||||
} from 'tiptap-extensions'
|
} from 'tiptap-extensions'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Icon,
|
Icon,
|
||||||
EditorContent,
|
EditorContent,
|
||||||
MenuBar,
|
MenuBar,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editor: new Editor({
|
editor: new Editor({
|
||||||
extensions: [
|
extensions: [
|
||||||
new HardBreak(),
|
new HardBreak(),
|
||||||
new Heading({ levels: [1, 2, 3] }),
|
new Heading({ levels: [1, 2, 3] }),
|
||||||
new Image(),
|
new Image(),
|
||||||
new Bold(),
|
new Bold(),
|
||||||
new Code(),
|
new Code(),
|
||||||
new Italic(),
|
new Italic(),
|
||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
<h2>
|
<h2>
|
||||||
Images
|
Images
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
This is basic example of implementing images. Try to drop new images here. Reordering also works.
|
This is basic example of implementing images. Try to drop new images here. Reordering also works.
|
||||||
</p>
|
</p>
|
||||||
<img src="https://ljdchost.com/8I2DeFn.gif" />
|
<img src="https://ljdchost.com/8I2DeFn.gif" />
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showImagePrompt(command) {
|
showImagePrompt(command) {
|
||||||
const src = prompt('Enter the url of your image here')
|
const src = prompt('Enter the url of your image here')
|
||||||
if (src !== null) {
|
if (src !== null) {
|
||||||
command({ src })
|
command({ src })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,113 +1,113 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
<menu-bubble class="menububble" :editor="editor">
|
<menu-bubble class="menububble" :editor="editor">
|
||||||
<div
|
<div
|
||||||
slot-scope="{ commands, isActive, markAttrs, menu }"
|
slot-scope="{ commands, isActive, markAttrs, menu }"
|
||||||
class="menububble"
|
class="menububble"
|
||||||
:class="{ 'is-active': menu.isActive }"
|
:class="{ 'is-active': menu.isActive }"
|
||||||
:style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`"
|
:style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`"
|
||||||
>
|
>
|
||||||
|
|
||||||
<form class="menububble__form" v-if="linkMenuIsActive" @submit.prevent="setLinkUrl(commands.link, linkUrl)">
|
<form class="menububble__form" v-if="linkMenuIsActive" @submit.prevent="setLinkUrl(commands.link, linkUrl)">
|
||||||
<input class="menububble__input" type="text" v-model="linkUrl" placeholder="https://" ref="linkInput" @keydown.esc="hideLinkMenu"/>
|
<input class="menububble__input" type="text" v-model="linkUrl" placeholder="https://" ref="linkInput" @keydown.esc="hideLinkMenu"/>
|
||||||
<button class="menububble__button" @click="setLinkUrl(commands.link, null)" type="button">
|
<button class="menububble__button" @click="setLinkUrl(commands.link, null)" type="button">
|
||||||
<icon name="remove" />
|
<icon name="remove" />
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<button
|
<button
|
||||||
class="menububble__button"
|
class="menububble__button"
|
||||||
@click="showLinkMenu(markAttrs('link'))"
|
@click="showLinkMenu(markAttrs('link'))"
|
||||||
:class="{ 'is-active': isActive('link') }"
|
:class="{ 'is-active': isActive('link') }"
|
||||||
>
|
>
|
||||||
<span>Add Link</span>
|
<span>Add Link</span>
|
||||||
<icon name="link" />
|
<icon name="link" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</menu-bubble>
|
</menu-bubble>
|
||||||
|
|
||||||
<editor-content class="editor__content" :editor="editor" />
|
<editor-content class="editor__content" :editor="editor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Icon from 'Components/Icon'
|
import Icon from 'Components/Icon'
|
||||||
import { Editor, EditorContent, MenuBubble } from 'tiptap'
|
import { Editor, EditorContent, MenuBubble } from 'tiptap'
|
||||||
import {
|
import {
|
||||||
Blockquote,
|
Blockquote,
|
||||||
BulletList,
|
BulletList,
|
||||||
CodeBlock,
|
CodeBlock,
|
||||||
HardBreak,
|
HardBreak,
|
||||||
Heading,
|
Heading,
|
||||||
ListItem,
|
ListItem,
|
||||||
OrderedList,
|
OrderedList,
|
||||||
TodoItem,
|
TodoItem,
|
||||||
TodoList,
|
TodoList,
|
||||||
Bold,
|
Bold,
|
||||||
Code,
|
Code,
|
||||||
Italic,
|
Italic,
|
||||||
Link,
|
Link,
|
||||||
History,
|
History,
|
||||||
} from 'tiptap-extensions'
|
} from 'tiptap-extensions'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EditorContent,
|
EditorContent,
|
||||||
MenuBubble,
|
MenuBubble,
|
||||||
Icon,
|
Icon,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editor: new Editor({
|
editor: new Editor({
|
||||||
extensions: [
|
extensions: [
|
||||||
new Blockquote(),
|
new Blockquote(),
|
||||||
new BulletList(),
|
new BulletList(),
|
||||||
new CodeBlock(),
|
new CodeBlock(),
|
||||||
new HardBreak(),
|
new HardBreak(),
|
||||||
new Heading({ levels: [1, 2, 3] }),
|
new Heading({ levels: [1, 2, 3] }),
|
||||||
new ListItem(),
|
new ListItem(),
|
||||||
new OrderedList(),
|
new OrderedList(),
|
||||||
new TodoItem(),
|
new TodoItem(),
|
||||||
new TodoList(),
|
new TodoList(),
|
||||||
new Bold(),
|
new Bold(),
|
||||||
new Code(),
|
new Code(),
|
||||||
new Italic(),
|
new Italic(),
|
||||||
new Link(),
|
new Link(),
|
||||||
new History(),
|
new History(),
|
||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
<h2>
|
<h2>
|
||||||
Links
|
Links
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
Try to add some links to the <a href="https://en.wikipedia.org/wiki/World_Wide_Web">world wide web</a>. By default every link will get a <code>rel="noopener noreferrer nofollow"</code> attribute.
|
Try to add some links to the <a href="https://en.wikipedia.org/wiki/World_Wide_Web">world wide web</a>. By default every link will get a <code>rel="noopener noreferrer nofollow"</code> attribute.
|
||||||
</p>
|
</p>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
linkUrl: null,
|
linkUrl: null,
|
||||||
linkMenuIsActive: false,
|
linkMenuIsActive: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showLinkMenu(attrs) {
|
showLinkMenu(attrs) {
|
||||||
this.linkUrl = attrs.href
|
this.linkUrl = attrs.href
|
||||||
this.linkMenuIsActive = true
|
this.linkMenuIsActive = true
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.linkInput.focus()
|
this.$refs.linkInput.focus()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
hideLinkMenu() {
|
hideLinkMenu() {
|
||||||
this.linkUrl = null
|
this.linkUrl = null
|
||||||
this.linkMenuIsActive = false
|
this.linkMenuIsActive = false
|
||||||
},
|
},
|
||||||
setLinkUrl(command, url) {
|
setLinkUrl(command, url) {
|
||||||
command({ href: url })
|
command({ href: url })
|
||||||
this.hideLinkMenu()
|
this.hideLinkMenu()
|
||||||
this.editor.focus()
|
this.editor.focus()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,69 +1,69 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
<editor-content class="editor__content" :editor="editor" />
|
<editor-content class="editor__content" :editor="editor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Icon from 'Components/Icon'
|
import Icon from 'Components/Icon'
|
||||||
import { Editor, EditorContent } from 'tiptap'
|
import { Editor, EditorContent } from 'tiptap'
|
||||||
import {
|
import {
|
||||||
Blockquote,
|
Blockquote,
|
||||||
CodeBlock,
|
CodeBlock,
|
||||||
HardBreak,
|
HardBreak,
|
||||||
Heading,
|
Heading,
|
||||||
OrderedList,
|
OrderedList,
|
||||||
BulletList,
|
BulletList,
|
||||||
ListItem,
|
ListItem,
|
||||||
TodoItem,
|
TodoItem,
|
||||||
TodoList,
|
TodoList,
|
||||||
Bold,
|
Bold,
|
||||||
Code,
|
Code,
|
||||||
Italic,
|
Italic,
|
||||||
Link,
|
Link,
|
||||||
History,
|
History,
|
||||||
} from 'tiptap-extensions'
|
} from 'tiptap-extensions'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EditorContent,
|
EditorContent,
|
||||||
Icon,
|
Icon,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editor: new Editor({
|
editor: new Editor({
|
||||||
extensions: [
|
extensions: [
|
||||||
new Blockquote(),
|
new Blockquote(),
|
||||||
new BulletList(),
|
new BulletList(),
|
||||||
new CodeBlock(),
|
new CodeBlock(),
|
||||||
new HardBreak(),
|
new HardBreak(),
|
||||||
new Heading({ levels: [1, 2, 3] }),
|
new Heading({ levels: [1, 2, 3] }),
|
||||||
new ListItem(),
|
new ListItem(),
|
||||||
new OrderedList(),
|
new OrderedList(),
|
||||||
new TodoItem(),
|
new TodoItem(),
|
||||||
new TodoList(),
|
new TodoList(),
|
||||||
new Bold(),
|
new Bold(),
|
||||||
new Code(),
|
new Code(),
|
||||||
new Italic(),
|
new Italic(),
|
||||||
new Link(),
|
new Link(),
|
||||||
new History(),
|
new History(),
|
||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
<h2>
|
<h2>
|
||||||
Markdown Shortcuts
|
Markdown Shortcuts
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
Start a new line and type <code>#</code> followed by a <code>space</code> and you will get an H1 headline.
|
Start a new line and type <code>#</code> followed by a <code>space</code> and you will get an H1 headline.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
This feature is called <strong>input rules</strong>. There are some of these shortcuts for the most basic nodes enabled by default. Try <code>#, ##, ###, …</code> for headlines, <code>></code> for blockquotes, <code>- or +</code> for bullet lists. And of course you can add your own input rules.
|
This feature is called <strong>input rules</strong>. There are some of these shortcuts for the most basic nodes enabled by default. Try <code>#, ##, ###, …</code> for headlines, <code>></code> for blockquotes, <code>- or +</code> for bullet lists. And of course you can add your own input rules.
|
||||||
</p>
|
</p>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.editor.destroy()
|
this.editor.destroy()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,106 +1,106 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
<menu-bubble :editor="editor">
|
<menu-bubble :editor="editor">
|
||||||
<div
|
<div
|
||||||
slot-scope="{ commands, isActive, menu }"
|
slot-scope="{ commands, isActive, menu }"
|
||||||
class="menububble"
|
class="menububble"
|
||||||
:class="{ 'is-active': menu.isActive }"
|
:class="{ 'is-active': menu.isActive }"
|
||||||
:style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`"
|
:style="`left: ${menu.left}px; bottom: ${menu.bottom}px;`"
|
||||||
>
|
>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menububble__button"
|
class="menububble__button"
|
||||||
:class="{ 'is-active': isActive('bold') }"
|
:class="{ 'is-active': isActive('bold') }"
|
||||||
@click="commands.bold"
|
@click="commands.bold"
|
||||||
>
|
>
|
||||||
<icon name="bold" />
|
<icon name="bold" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menububble__button"
|
class="menububble__button"
|
||||||
:class="{ 'is-active': isActive('italic') }"
|
:class="{ 'is-active': isActive('italic') }"
|
||||||
@click="commands.italic"
|
@click="commands.italic"
|
||||||
>
|
>
|
||||||
<icon name="italic" />
|
<icon name="italic" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menububble__button"
|
class="menububble__button"
|
||||||
:class="{ 'is-active': isActive('code') }"
|
:class="{ 'is-active': isActive('code') }"
|
||||||
@click="commands.code"
|
@click="commands.code"
|
||||||
>
|
>
|
||||||
<icon name="code" />
|
<icon name="code" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</menu-bubble>
|
</menu-bubble>
|
||||||
|
|
||||||
<editor-content class="editor__content" :editor="editor" />
|
<editor-content class="editor__content" :editor="editor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Icon from 'Components/Icon'
|
import Icon from 'Components/Icon'
|
||||||
import { Editor, EditorContent, MenuBubble } from 'tiptap'
|
import { Editor, EditorContent, MenuBubble } from 'tiptap'
|
||||||
import {
|
import {
|
||||||
Blockquote,
|
Blockquote,
|
||||||
BulletList,
|
BulletList,
|
||||||
CodeBlock,
|
CodeBlock,
|
||||||
HardBreak,
|
HardBreak,
|
||||||
Heading,
|
Heading,
|
||||||
ListItem,
|
ListItem,
|
||||||
OrderedList,
|
OrderedList,
|
||||||
TodoItem,
|
TodoItem,
|
||||||
TodoList,
|
TodoList,
|
||||||
Bold,
|
Bold,
|
||||||
Code,
|
Code,
|
||||||
Italic,
|
Italic,
|
||||||
Link,
|
Link,
|
||||||
Strike,
|
Strike,
|
||||||
Underline,
|
Underline,
|
||||||
History,
|
History,
|
||||||
} from 'tiptap-extensions'
|
} from 'tiptap-extensions'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EditorContent,
|
EditorContent,
|
||||||
MenuBubble,
|
MenuBubble,
|
||||||
Icon,
|
Icon,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editor: new Editor({
|
editor: new Editor({
|
||||||
extensions: [
|
extensions: [
|
||||||
new Blockquote(),
|
new Blockquote(),
|
||||||
new BulletList(),
|
new BulletList(),
|
||||||
new CodeBlock(),
|
new CodeBlock(),
|
||||||
new HardBreak(),
|
new HardBreak(),
|
||||||
new Heading({ levels: [1, 2, 3] }),
|
new Heading({ levels: [1, 2, 3] }),
|
||||||
new ListItem(),
|
new ListItem(),
|
||||||
new OrderedList(),
|
new OrderedList(),
|
||||||
new TodoItem(),
|
new TodoItem(),
|
||||||
new TodoList(),
|
new TodoList(),
|
||||||
new Bold(),
|
new Bold(),
|
||||||
new Code(),
|
new Code(),
|
||||||
new Italic(),
|
new Italic(),
|
||||||
new Link(),
|
new Link(),
|
||||||
new Strike(),
|
new Strike(),
|
||||||
new Underline(),
|
new Underline(),
|
||||||
new History(),
|
new History(),
|
||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
<h2>
|
<h2>
|
||||||
Menu Bubble
|
Menu Bubble
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
Hey, try to select some text here. There will popup a menu for selecting some inline styles. <em>Remember:</em> you have full control about content and styling of this menu.
|
Hey, try to select some text here. There will popup a menu for selecting some inline styles. <em>Remember:</em> you have full control about content and styling of this menu.
|
||||||
</p>
|
</p>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.editor.destroy()
|
this.editor.destroy()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,47 +1,47 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
<editor-content class="editor__content" :editor="editor" />
|
<editor-content class="editor__content" :editor="editor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Editor, EditorContent } from 'tiptap'
|
import { Editor, EditorContent } from 'tiptap'
|
||||||
import {
|
import {
|
||||||
BulletList,
|
BulletList,
|
||||||
ListItem,
|
ListItem,
|
||||||
Placeholder,
|
Placeholder,
|
||||||
} from 'tiptap-extensions'
|
} from 'tiptap-extensions'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EditorContent,
|
EditorContent,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editor: new Editor({
|
editor: new Editor({
|
||||||
extensions: [
|
extensions: [
|
||||||
new BulletList(),
|
new BulletList(),
|
||||||
new ListItem(),
|
new ListItem(),
|
||||||
new Placeholder({
|
new Placeholder({
|
||||||
emptyClass: 'is-empty',
|
emptyClass: 'is-empty',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.editor.destroy()
|
this.editor.destroy()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.editor p.is-empty:first-child::before {
|
.editor p.is-empty:first-child::before {
|
||||||
content: 'Start typing…';
|
content: 'Start typing…';
|
||||||
float: left;
|
float: left;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
height: 0;
|
height: 0;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,49 +1,49 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
<editor-content class="editor__content" :editor="editor" />
|
<editor-content class="editor__content" :editor="editor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Editor, EditorContent } from 'tiptap'
|
import { Editor, EditorContent } from 'tiptap'
|
||||||
import {
|
import {
|
||||||
HardBreak,
|
HardBreak,
|
||||||
Heading,
|
Heading,
|
||||||
Bold,
|
Bold,
|
||||||
Code,
|
Code,
|
||||||
Italic,
|
Italic,
|
||||||
Link,
|
Link,
|
||||||
} from 'tiptap-extensions'
|
} from 'tiptap-extensions'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EditorContent,
|
EditorContent,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editor: new Editor({
|
editor: new Editor({
|
||||||
editable: false,
|
editable: false,
|
||||||
extensions: [
|
extensions: [
|
||||||
new HardBreak(),
|
new HardBreak(),
|
||||||
new Heading({ levels: [1, 2, 3] }),
|
new Heading({ levels: [1, 2, 3] }),
|
||||||
new Bold(),
|
new Bold(),
|
||||||
new Code(),
|
new Code(),
|
||||||
new Italic(),
|
new Italic(),
|
||||||
new Link(),
|
new Link(),
|
||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
<h2>
|
<h2>
|
||||||
Read-Only
|
Read-Only
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
This text is <strong>read-only</strong>. You are not able to edit something. <a href="https://scrumpy.io/">Links to fancy websites</a> are still working.
|
This text is <strong>read-only</strong>. You are not able to edit something. <a href="https://scrumpy.io/">Links to fancy websites</a> are still working.
|
||||||
</p>
|
</p>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.editor.destroy()
|
this.editor.destroy()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
<editor-content class="editor__content" :editor="editor" />
|
<editor-content class="editor__content" :editor="editor" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="suggestion-list" v-show="showSuggestions" ref="suggestions">
|
<div class="suggestion-list" v-show="showSuggestions" ref="suggestions">
|
||||||
<template v-if="hasResults">
|
<template v-if="hasResults">
|
||||||
<div
|
<div
|
||||||
v-for="(user, index) in filteredUsers"
|
v-for="(user, index) in filteredUsers"
|
||||||
:key="user.id"
|
:key="user.id"
|
||||||
class="suggestion-list__item"
|
class="suggestion-list__item"
|
||||||
:class="{ 'is-selected': navigatedUserIndex === index }"
|
:class="{ 'is-selected': navigatedUserIndex === index }"
|
||||||
@click="selectUser(user)"
|
@click="selectUser(user)"
|
||||||
>
|
>
|
||||||
{{ user.name }}
|
{{ user.name }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="suggestion-list__item is-empty">
|
<div v-else class="suggestion-list__item is-empty">
|
||||||
No users found
|
No users found
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -30,204 +30,204 @@ import Fuse from 'fuse.js'
|
|||||||
import tippy from 'tippy.js'
|
import tippy from 'tippy.js'
|
||||||
import { Editor, EditorContent } from 'tiptap'
|
import { Editor, EditorContent } from 'tiptap'
|
||||||
import {
|
import {
|
||||||
HardBreak,
|
HardBreak,
|
||||||
Heading,
|
Heading,
|
||||||
Mention,
|
Mention,
|
||||||
Code,
|
Code,
|
||||||
Bold,
|
Bold,
|
||||||
Italic,
|
Italic,
|
||||||
} from 'tiptap-extensions'
|
} from 'tiptap-extensions'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
EditorContent,
|
EditorContent,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editor: new Editor({
|
editor: new Editor({
|
||||||
extensions: [
|
extensions: [
|
||||||
new HardBreak(),
|
new HardBreak(),
|
||||||
new Heading({ levels: [1, 2, 3] }),
|
new Heading({ levels: [1, 2, 3] }),
|
||||||
new Mention({
|
new Mention({
|
||||||
// a list of all suggested items
|
// a list of all suggested items
|
||||||
items: () => [
|
items: () => [
|
||||||
{ id: 1, name: 'Philipp Kühn' },
|
{ id: 1, name: 'Philipp Kühn' },
|
||||||
{ id: 2, name: 'Hans Pagel' },
|
{ id: 2, name: 'Hans Pagel' },
|
||||||
{ id: 3, name: 'Kris Siepert' },
|
{ id: 3, name: 'Kris Siepert' },
|
||||||
{ id: 4, name: 'Justin Schueler' },
|
{ id: 4, name: 'Justin Schueler' },
|
||||||
],
|
],
|
||||||
// is called when a suggestion starts
|
// is called when a suggestion starts
|
||||||
onEnter: ({
|
onEnter: ({
|
||||||
items, query, range, command, virtualNode,
|
items, query, range, command, virtualNode,
|
||||||
}) => {
|
}) => {
|
||||||
this.query = query
|
this.query = query
|
||||||
this.filteredUsers = items
|
this.filteredUsers = items
|
||||||
this.suggestionRange = range
|
this.suggestionRange = range
|
||||||
this.renderPopup(virtualNode)
|
this.renderPopup(virtualNode)
|
||||||
// we save the command for inserting a selected mention
|
// we save the command for inserting a selected mention
|
||||||
// this allows us to call it inside of our custom popup
|
// this allows us to call it inside of our custom popup
|
||||||
// via keyboard navigation and on click
|
// via keyboard navigation and on click
|
||||||
this.insertMention = command
|
this.insertMention = command
|
||||||
},
|
},
|
||||||
// is called when a suggestion has changed
|
// is called when a suggestion has changed
|
||||||
onChange: ({
|
onChange: ({
|
||||||
items, query, range, virtualNode,
|
items, query, range, virtualNode,
|
||||||
}) => {
|
}) => {
|
||||||
this.query = query
|
this.query = query
|
||||||
this.filteredUsers = items
|
this.filteredUsers = items
|
||||||
this.suggestionRange = range
|
this.suggestionRange = range
|
||||||
this.navigatedUserIndex = 0
|
this.navigatedUserIndex = 0
|
||||||
this.renderPopup(virtualNode)
|
this.renderPopup(virtualNode)
|
||||||
},
|
},
|
||||||
// is called when a suggestion is cancelled
|
// is called when a suggestion is cancelled
|
||||||
onExit: () => {
|
onExit: () => {
|
||||||
// reset all saved values
|
// reset all saved values
|
||||||
this.query = null
|
this.query = null
|
||||||
this.filteredUsers = []
|
this.filteredUsers = []
|
||||||
this.suggestionRange = null
|
this.suggestionRange = null
|
||||||
this.navigatedUserIndex = 0
|
this.navigatedUserIndex = 0
|
||||||
this.destroyPopup()
|
this.destroyPopup()
|
||||||
},
|
},
|
||||||
// is called on every keyDown event while a suggestion is active
|
// is called on every keyDown event while a suggestion is active
|
||||||
onKeyDown: ({ event }) => {
|
onKeyDown: ({ event }) => {
|
||||||
// pressing up arrow
|
// pressing up arrow
|
||||||
if (event.keyCode === 38) {
|
if (event.keyCode === 38) {
|
||||||
this.upHandler()
|
this.upHandler()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// pressing down arrow
|
// pressing down arrow
|
||||||
if (event.keyCode === 40) {
|
if (event.keyCode === 40) {
|
||||||
this.downHandler()
|
this.downHandler()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// pressing enter
|
// pressing enter
|
||||||
if (event.keyCode === 13) {
|
if (event.keyCode === 13) {
|
||||||
this.enterHandler()
|
this.enterHandler()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
// is called when a suggestion has changed
|
// is called when a suggestion has changed
|
||||||
// this function is optional because there is basic filtering built-in
|
// this function is optional because there is basic filtering built-in
|
||||||
// you can overwrite it if you prefer your own filtering
|
// you can overwrite it if you prefer your own filtering
|
||||||
// in this example we use fuse.js with support for fuzzy search
|
// in this example we use fuse.js with support for fuzzy search
|
||||||
onFilter: (items, query) => {
|
onFilter: (items, query) => {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
const fuse = new Fuse(items, {
|
const fuse = new Fuse(items, {
|
||||||
threshold: 0.2,
|
threshold: 0.2,
|
||||||
keys: ['name'],
|
keys: ['name'],
|
||||||
})
|
})
|
||||||
|
|
||||||
return fuse.search(query)
|
return fuse.search(query)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
new Code(),
|
new Code(),
|
||||||
new Bold(),
|
new Bold(),
|
||||||
new Italic(),
|
new Italic(),
|
||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
<h2>
|
<h2>
|
||||||
Suggestions
|
Suggestions
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
Sometimes it's useful to <strong>mention</strong> someone. That's a feature we're very used to. Under the hood this technique can also be used for other features likes <strong>hashtags</strong> and <strong>commands</strong> – lets call it <em>suggestions</em>.
|
Sometimes it's useful to <strong>mention</strong> someone. That's a feature we're very used to. Under the hood this technique can also be used for other features likes <strong>hashtags</strong> and <strong>commands</strong> – lets call it <em>suggestions</em>.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
This is an example how to mention some users like <span data-mention-id="1">Philipp Kühn</span> or <span data-mention-id="2">Hans Pagel</span>. Try to type <code>@</code> and a popup (rendered with tippy.js) will appear. You can navigate with arrow keys through a list of suggestions.
|
This is an example how to mention some users like <span data-mention-id="1">Philipp Kühn</span> or <span data-mention-id="2">Hans Pagel</span>. Try to type <code>@</code> and a popup (rendered with tippy.js) will appear. You can navigate with arrow keys through a list of suggestions.
|
||||||
</p>
|
</p>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
query: null,
|
query: null,
|
||||||
suggestionRange: null,
|
suggestionRange: null,
|
||||||
filteredUsers: [],
|
filteredUsers: [],
|
||||||
navigatedUserIndex: 0,
|
navigatedUserIndex: 0,
|
||||||
insertMention: () => {},
|
insertMention: () => {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
hasResults() {
|
hasResults() {
|
||||||
return this.filteredUsers.length
|
return this.filteredUsers.length
|
||||||
},
|
},
|
||||||
|
|
||||||
showSuggestions() {
|
showSuggestions() {
|
||||||
return this.query || this.hasResults
|
return this.query || this.hasResults
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
||||||
// navigate to the previous item
|
// navigate to the previous item
|
||||||
// if it's the first item, navigate to the last one
|
// if it's the first item, navigate to the last one
|
||||||
upHandler() {
|
upHandler() {
|
||||||
this.navigatedUserIndex = ((this.navigatedUserIndex + this.filteredUsers.length) - 1) % this.filteredUsers.length
|
this.navigatedUserIndex = ((this.navigatedUserIndex + this.filteredUsers.length) - 1) % this.filteredUsers.length
|
||||||
},
|
},
|
||||||
|
|
||||||
// navigate to the next item
|
// navigate to the next item
|
||||||
// if it's the last item, navigate to the first one
|
// if it's the last item, navigate to the first one
|
||||||
downHandler() {
|
downHandler() {
|
||||||
this.navigatedUserIndex = (this.navigatedUserIndex + 1) % this.filteredUsers.length
|
this.navigatedUserIndex = (this.navigatedUserIndex + 1) % this.filteredUsers.length
|
||||||
},
|
},
|
||||||
|
|
||||||
enterHandler() {
|
enterHandler() {
|
||||||
const user = this.filteredUsers[this.navigatedUserIndex]
|
const user = this.filteredUsers[this.navigatedUserIndex]
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
this.selectUser(user)
|
this.selectUser(user)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// we have to replace our suggestion text with a mention
|
// we have to replace our suggestion text with a mention
|
||||||
// so it's important to pass also the position of your suggestion text
|
// so it's important to pass also the position of your suggestion text
|
||||||
selectUser(user) {
|
selectUser(user) {
|
||||||
this.insertMention({
|
this.insertMention({
|
||||||
range: this.suggestionRange,
|
range: this.suggestionRange,
|
||||||
attrs: {
|
attrs: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
label: user.name,
|
label: user.name,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
this.editor.focus()
|
this.editor.focus()
|
||||||
},
|
},
|
||||||
|
|
||||||
// renders a popup with suggestions
|
// renders a popup with suggestions
|
||||||
// tiptap provides a virtualNode object for using popper.js (or tippy.js) for popups
|
// tiptap provides a virtualNode object for using popper.js (or tippy.js) for popups
|
||||||
renderPopup(node) {
|
renderPopup(node) {
|
||||||
if (this.popup) {
|
if (this.popup) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.popup = tippy(node, {
|
this.popup = tippy(node, {
|
||||||
content: this.$refs.suggestions,
|
content: this.$refs.suggestions,
|
||||||
trigger: 'mouseenter',
|
trigger: 'mouseenter',
|
||||||
interactive: true,
|
interactive: true,
|
||||||
theme: 'dark',
|
theme: 'dark',
|
||||||
placement: 'top-start',
|
placement: 'top-start',
|
||||||
performance: true,
|
performance: true,
|
||||||
inertia: true,
|
inertia: true,
|
||||||
duration: [400, 200],
|
duration: [400, 200],
|
||||||
showOnInit: true,
|
showOnInit: true,
|
||||||
arrow: true,
|
arrow: true,
|
||||||
arrowType: 'round',
|
arrowType: 'round',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
destroyPopup() {
|
destroyPopup() {
|
||||||
if (this.popup) {
|
if (this.popup) {
|
||||||
this.popup.destroyAll()
|
this.popup.destroyAll()
|
||||||
this.popup = null
|
this.popup = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -242,74 +242,74 @@ export default {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 0.2rem 0.5rem;
|
padding: 0.2rem 0.5rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mention-suggestion {
|
.mention-suggestion {
|
||||||
color: rgba($color-black, 0.6);
|
color: rgba($color-black, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.suggestion-list {
|
.suggestion-list {
|
||||||
padding: 0.2rem;
|
padding: 0.2rem;
|
||||||
border: 2px solid rgba($color-black, 0.1);
|
border: 2px solid rgba($color-black, 0.1);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
&__no-results {
|
&__no-results {
|
||||||
padding: 0.2rem 0.5rem;
|
padding: 0.2rem 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 0.2rem 0.5rem;
|
padding: 0.2rem 0.5rem;
|
||||||
margin-bottom: 0.2rem;
|
margin-bottom: 0.2rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-selected,
|
&.is-selected,
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba($color-white, 0.2);
|
background-color: rgba($color-white, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-empty {
|
&.is-empty {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-tooltip.dark-theme {
|
.tippy-tooltip.dark-theme {
|
||||||
background-color: $color-black;
|
background-color: $color-black;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
text-align: inherit;
|
text-align: inherit;
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
||||||
.tippy-backdrop {
|
.tippy-backdrop {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-roundarrow {
|
.tippy-roundarrow {
|
||||||
fill: $color-black;
|
fill: $color-black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-popper[x-placement^=top] & .tippy-arrow {
|
.tippy-popper[x-placement^=top] & .tippy-arrow {
|
||||||
border-top-color: $color-black;
|
border-top-color: $color-black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-popper[x-placement^=bottom] & .tippy-arrow {
|
.tippy-popper[x-placement^=bottom] & .tippy-arrow {
|
||||||
border-bottom-color: $color-black;
|
border-bottom-color: $color-black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-popper[x-placement^=left] & .tippy-arrow {
|
.tippy-popper[x-placement^=left] & .tippy-arrow {
|
||||||
border-left-color: $color-black;
|
border-left-color: $color-black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-popper[x-placement^=right] & .tippy-arrow {
|
.tippy-popper[x-placement^=right] & .tippy-arrow {
|
||||||
border-right-color: $color-black;
|
border-right-color: $color-black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,32 +3,32 @@ import { Node } from 'tiptap'
|
|||||||
|
|
||||||
export default class Paragraph extends Node {
|
export default class Paragraph extends Node {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'paragraph'
|
return 'paragraph'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
attrs: {
|
attrs: {
|
||||||
textAlign: {
|
textAlign: {
|
||||||
default: 'left',
|
default: 'left',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
content: 'inline*',
|
content: 'inline*',
|
||||||
group: 'block',
|
group: 'block',
|
||||||
draggable: false,
|
draggable: false,
|
||||||
parseDOM: [{
|
parseDOM: [{
|
||||||
tag: 'p',
|
tag: 'p',
|
||||||
getAttrs: node => ({
|
getAttrs: node => ({
|
||||||
textAlign: node.style.textAlign,
|
textAlign: node.style.textAlign,
|
||||||
}),
|
}),
|
||||||
}],
|
}],
|
||||||
toDOM: node => ['p', { style: `text-align: ${node.attrs.textAlign}` }, 0],
|
toDOM: node => ['p', { style: `text-align: ${node.attrs.textAlign}` }, 0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commands({ type }) {
|
commands({ type }) {
|
||||||
return attrs => setBlockType(type, attrs)
|
return attrs => setBlockType(type, attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,93 +1,93 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
<menu-bar :editor="editor">
|
<menu-bar :editor="editor">
|
||||||
<div class="menubar" slot-scope="{ commands, isActive }">
|
<div class="menubar" slot-scope="{ commands, isActive }">
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('paragraph', { textAlign: 'left' }) }"
|
:class="{ 'is-active': isActive('paragraph', { textAlign: 'left' }) }"
|
||||||
@click="commands.paragraph({ textAlign: 'left' })"
|
@click="commands.paragraph({ textAlign: 'left' })"
|
||||||
>
|
>
|
||||||
<icon name="align-left" />
|
<icon name="align-left" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('paragraph', { textAlign: 'center' }) }"
|
:class="{ 'is-active': isActive('paragraph', { textAlign: 'center' }) }"
|
||||||
@click="commands.paragraph({ textAlign: 'center' })"
|
@click="commands.paragraph({ textAlign: 'center' })"
|
||||||
>
|
>
|
||||||
<icon name="align-center" />
|
<icon name="align-center" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('paragraph', { textAlign: 'right' }) }"
|
:class="{ 'is-active': isActive('paragraph', { textAlign: 'right' }) }"
|
||||||
@click="commands.paragraph({ textAlign: 'right' })"
|
@click="commands.paragraph({ textAlign: 'right' })"
|
||||||
>
|
>
|
||||||
<icon name="align-right" />
|
<icon name="align-right" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</menu-bar>
|
</menu-bar>
|
||||||
|
|
||||||
<editor-content class="editor__content" :editor="editor" />
|
<editor-content class="editor__content" :editor="editor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Icon from 'Components/Icon'
|
import Icon from 'Components/Icon'
|
||||||
import { Editor, EditorContent, MenuBar } from 'tiptap'
|
import { Editor, EditorContent, MenuBar } from 'tiptap'
|
||||||
import {
|
import {
|
||||||
HardBreak,
|
HardBreak,
|
||||||
Code,
|
Code,
|
||||||
} from 'tiptap-extensions'
|
} from 'tiptap-extensions'
|
||||||
import ParagraphAlignment from './Paragraph.js'
|
import ParagraphAlignment from './Paragraph.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EditorContent,
|
EditorContent,
|
||||||
MenuBar,
|
MenuBar,
|
||||||
Icon,
|
Icon,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editor: new Editor({
|
editor: new Editor({
|
||||||
extensions: [
|
extensions: [
|
||||||
new HardBreak(),
|
new HardBreak(),
|
||||||
new Code(),
|
new Code(),
|
||||||
new ParagraphAlignment(),
|
new ParagraphAlignment(),
|
||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
<p style="text-align: left">
|
<p style="text-align: left">
|
||||||
Maybe you want to implement text alignment. If so, you're able to overwrite the default <code>ParagraphNode</code>. You can define some classes oder inline styles in your schema to achive that.
|
Maybe you want to implement text alignment. If so, you're able to overwrite the default <code>ParagraphNode</code>. You can define some classes oder inline styles in your schema to achive that.
|
||||||
</p>
|
</p>
|
||||||
<p style="text-align: right">
|
<p style="text-align: right">
|
||||||
Have fun! 🙌
|
Have fun! 🙌
|
||||||
</p>
|
</p>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.editor.destroy()
|
this.editor.destroy()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.text-align {
|
.text-align {
|
||||||
|
|
||||||
&--left {
|
&--left {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--center {
|
&--center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--right {
|
&--right {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,108 +1,108 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="editor">
|
<div class="editor">
|
||||||
<menu-bar :editor="editor">
|
<menu-bar :editor="editor">
|
||||||
<div class="menubar" slot-scope="{ commands, isActive }">
|
<div class="menubar" slot-scope="{ commands, isActive }">
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('bold') }"
|
:class="{ 'is-active': isActive('bold') }"
|
||||||
@click="commands.bold"
|
@click="commands.bold"
|
||||||
>
|
>
|
||||||
<icon name="bold" />
|
<icon name="bold" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('italic') }"
|
:class="{ 'is-active': isActive('italic') }"
|
||||||
@click="commands.italic"
|
@click="commands.italic"
|
||||||
>
|
>
|
||||||
<icon name="italic" />
|
<icon name="italic" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('code') }"
|
:class="{ 'is-active': isActive('code') }"
|
||||||
@click="commands.code"
|
@click="commands.code"
|
||||||
>
|
>
|
||||||
<icon name="code" />
|
<icon name="code" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="menubar__button"
|
class="menubar__button"
|
||||||
:class="{ 'is-active': isActive('todo_list') }"
|
:class="{ 'is-active': isActive('todo_list') }"
|
||||||
@click="commands.todo_list"
|
@click="commands.todo_list"
|
||||||
>
|
>
|
||||||
<icon name="checklist" />
|
<icon name="checklist" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</menu-bar>
|
</menu-bar>
|
||||||
|
|
||||||
<editor-content class="editor__content" :editor="editor" />
|
<editor-content class="editor__content" :editor="editor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Icon from 'Components/Icon'
|
import Icon from 'Components/Icon'
|
||||||
import { Editor, EditorContent, MenuBar } from 'tiptap'
|
import { Editor, EditorContent, MenuBar } from 'tiptap'
|
||||||
import {
|
import {
|
||||||
CodeBlock,
|
CodeBlock,
|
||||||
HardBreak,
|
HardBreak,
|
||||||
Heading,
|
Heading,
|
||||||
TodoItem,
|
TodoItem,
|
||||||
TodoList,
|
TodoList,
|
||||||
Bold,
|
Bold,
|
||||||
Code,
|
Code,
|
||||||
Italic,
|
Italic,
|
||||||
} from 'tiptap-extensions'
|
} from 'tiptap-extensions'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EditorContent,
|
EditorContent,
|
||||||
MenuBar,
|
MenuBar,
|
||||||
Icon,
|
Icon,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editor: new Editor({
|
editor: new Editor({
|
||||||
extensions: [
|
extensions: [
|
||||||
new CodeBlock(),
|
new CodeBlock(),
|
||||||
new HardBreak(),
|
new HardBreak(),
|
||||||
new Heading({ levels: [1, 2, 3] }),
|
new Heading({ levels: [1, 2, 3] }),
|
||||||
new TodoItem(),
|
new TodoItem(),
|
||||||
new TodoList(),
|
new TodoList(),
|
||||||
new Bold(),
|
new Bold(),
|
||||||
new Code(),
|
new Code(),
|
||||||
new Italic(),
|
new Italic(),
|
||||||
],
|
],
|
||||||
content: `
|
content: `
|
||||||
<h2>
|
<h2>
|
||||||
Todo List
|
Todo List
|
||||||
</h2>
|
</h2>
|
||||||
<p>
|
<p>
|
||||||
There is always something to do. Thankfully, there are checklists for that. Don't forget to call mom.
|
There is always something to do. Thankfully, there are checklists for that. Don't forget to call mom.
|
||||||
</p>
|
</p>
|
||||||
<ul data-type="todo_list">
|
<ul data-type="todo_list">
|
||||||
<li data-type="todo_item" data-done="true">
|
<li data-type="todo_item" data-done="true">
|
||||||
Buy beer
|
Buy beer
|
||||||
</li>
|
</li>
|
||||||
<li data-type="todo_item" data-done="true">
|
<li data-type="todo_item" data-done="true">
|
||||||
Buy meat
|
Buy meat
|
||||||
</li>
|
</li>
|
||||||
<li data-type="todo_item" data-done="true">
|
<li data-type="todo_item" data-done="true">
|
||||||
Buy milk
|
Buy milk
|
||||||
</li>
|
</li>
|
||||||
<li data-type="todo_item" data-done="false">
|
<li data-type="todo_item" data-done="false">
|
||||||
Call mom
|
Call mom
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.editor.destroy()
|
this.editor.destroy()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="subnavigation">
|
<div class="subnavigation">
|
||||||
<router-link class="subnavigation__link" to="/">
|
<router-link class="subnavigation__link" to="/">
|
||||||
Basic
|
Basic
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="subnavigation__link" to="/menu-bubble">
|
<router-link class="subnavigation__link" to="/menu-bubble">
|
||||||
Menu Bubble
|
Menu Bubble
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="subnavigation__link" to="/floating-menu">
|
<router-link class="subnavigation__link" to="/floating-menu">
|
||||||
Floating Menu
|
Floating Menu
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="subnavigation__link" to="/links">
|
<router-link class="subnavigation__link" to="/links">
|
||||||
Links
|
Links
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="subnavigation__link" to="/images">
|
<router-link class="subnavigation__link" to="/images">
|
||||||
Images
|
Images
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="subnavigation__link" to="/text-align">
|
<router-link class="subnavigation__link" to="/text-align">
|
||||||
Text Align
|
Text Align
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="subnavigation__link" to="/hiding-menu-bar">
|
<router-link class="subnavigation__link" to="/hiding-menu-bar">
|
||||||
Hiding Menu Bar
|
Hiding Menu Bar
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="subnavigation__link" to="/todo-list">
|
<router-link class="subnavigation__link" to="/todo-list">
|
||||||
Todo List
|
Todo List
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="subnavigation__link" to="/suggestions">
|
<router-link class="subnavigation__link" to="/suggestions">
|
||||||
Suggestions
|
Suggestions
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="subnavigation__link" to="/markdown-shortcuts">
|
<router-link class="subnavigation__link" to="/markdown-shortcuts">
|
||||||
Markdown Shortcuts
|
Markdown Shortcuts
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="subnavigation__link" to="/code-highlighting">
|
<router-link class="subnavigation__link" to="/code-highlighting">
|
||||||
Code Highlighting
|
Code Highlighting
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="subnavigation__link" to="/read-only">
|
<router-link class="subnavigation__link" to="/read-only">
|
||||||
Read-Only
|
Read-Only
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="subnavigation__link" to="/embeds">
|
<router-link class="subnavigation__link" to="/embeds">
|
||||||
Embeds
|
Embeds
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="subnavigation__link" to="/placeholder">
|
<router-link class="subnavigation__link" to="/placeholder">
|
||||||
Placeholder
|
Placeholder
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link class="subnavigation__link" to="/export">
|
<router-link class="subnavigation__link" to="/export">
|
||||||
Export HTML or JSON
|
Export HTML or JSON
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" src="./style.scss" scoped></style>
|
<style lang="scss" src="./style.scss" scoped></style>
|
||||||
@@ -2,35 +2,35 @@
|
|||||||
|
|
||||||
.subnavigation {
|
.subnavigation {
|
||||||
|
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
background-color: rgba($color-black, 0.9);
|
background-color: rgba($color-black, 0.9);
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
@media (min-width: 600px) {
|
@media (min-width: 600px) {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__link {
|
&__link {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: rgba($color-white, 0.5);
|
color: rgba($color-white, 0.5);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
padding: 0.1rem 0.5rem;
|
padding: 0.1rem 0.5rem;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
background-color: rgba($color-white, 0.1);
|
background-color: rgba($color-white, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-exact-active {
|
&.is-exact-active {
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
background-color: rgba($color-white, 0.2);
|
background-color: rgba($color-white, 0.2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,39 +1,39 @@
|
|||||||
@import "~variables";
|
@import "~variables";
|
||||||
|
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
text-size-adjust: 100%;
|
text-size-adjust: 100%;
|
||||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
-webkit-touch-callout: none;
|
-webkit-touch-callout: none;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
*::before,
|
*::before,
|
||||||
*::after {
|
*::after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, San Francisco, Roboto, Segoe UI, Helvetica Neue, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, San Francisco, Roboto, Segoe UI, Helvetica Neue, sans-serif;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: $color-black;
|
color: $color-black;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
@@ -44,11 +44,11 @@ ul,
|
|||||||
ol,
|
ol,
|
||||||
pre,
|
pre,
|
||||||
blockquote {
|
blockquote {
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@@ -58,20 +58,20 @@ blockquote {
|
|||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3 {
|
h3 {
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
color: $color-black;
|
color: $color-black;
|
||||||
padding: 0.2rem 0.5rem;
|
padding: 0.2rem 0.5rem;
|
||||||
margin-right: 0.2rem;
|
margin-right: 0.2rem;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: rgba($color-black, 0.1);
|
background-color: rgba($color-black, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@import "./editor";
|
@import "./editor";
|
||||||
|
|||||||
@@ -1,80 +1,80 @@
|
|||||||
;(function(window, document) {
|
;(function(window, document) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var isSvg = document.createElementNS && document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' ).createSVGRect;
|
var isSvg = document.createElementNS && document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' ).createSVGRect;
|
||||||
var localStorage = 'localStorage' in window && window['localStorage'] !== null ? window.localStorage : false;
|
var localStorage = 'localStorage' in window && window['localStorage'] !== null ? window.localStorage : false;
|
||||||
|
|
||||||
function svgSpriteInjector(source, opts) {
|
function svgSpriteInjector(source, opts) {
|
||||||
var file;
|
var file;
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
|
|
||||||
if (source instanceof Node) {
|
if (source instanceof Node) {
|
||||||
file = source.getAttribute('data-svg-sprite');
|
file = source.getAttribute('data-svg-sprite');
|
||||||
opts.revision = source.getAttribute('data-svg-sprite-revision') || opts.revision;
|
opts.revision = source.getAttribute('data-svg-sprite-revision') || opts.revision;
|
||||||
} else if (typeof source === 'string') {
|
} else if (typeof source === 'string') {
|
||||||
file = source;
|
file = source;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSvg) {
|
if (isSvg) {
|
||||||
if (file) {
|
if (file) {
|
||||||
injector(file, opts);
|
injector(file, opts);
|
||||||
} else {
|
} else {
|
||||||
console.error('svg-sprite-injector: undefined sprite filename!');
|
console.error('svg-sprite-injector: undefined sprite filename!');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('svg-sprite-injector require ie9 or greater!');
|
console.error('svg-sprite-injector require ie9 or greater!');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function injector(filepath, opts) {
|
function injector(filepath, opts) {
|
||||||
var name = 'injectedSVGSprite' + filepath,
|
var name = 'injectedSVGSprite' + filepath,
|
||||||
revision = opts.revision,
|
revision = opts.revision,
|
||||||
request;
|
request;
|
||||||
|
|
||||||
// localStorage cache
|
// localStorage cache
|
||||||
if (revision !== undefined && localStorage && localStorage[name + 'Rev'] == revision) {
|
if (revision !== undefined && localStorage && localStorage[name + 'Rev'] == revision) {
|
||||||
return injectOnLoad(localStorage[name]);
|
return injectOnLoad(localStorage[name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Async load
|
// Async load
|
||||||
request = new XMLHttpRequest();
|
request = new XMLHttpRequest();
|
||||||
request.open('GET', filepath, true);
|
request.open('GET', filepath, true);
|
||||||
request.onreadystatechange = function (e) {
|
request.onreadystatechange = function (e) {
|
||||||
var data;
|
var data;
|
||||||
|
|
||||||
if (request.readyState === 4 && request.status >= 200 && request.status < 400) {
|
if (request.readyState === 4 && request.status >= 200 && request.status < 400) {
|
||||||
injectOnLoad(data = request.responseText);
|
injectOnLoad(data = request.responseText);
|
||||||
if (revision !== undefined && localStorage) {
|
if (revision !== undefined && localStorage) {
|
||||||
localStorage[name] = data;
|
localStorage[name] = data;
|
||||||
localStorage[name + 'Rev'] = revision;
|
localStorage[name + 'Rev'] = revision;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
request.send();
|
request.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
function injectOnLoad(data) {
|
function injectOnLoad(data) {
|
||||||
if (data) {
|
if (data) {
|
||||||
if (document.body) {
|
if (document.body) {
|
||||||
injectData(data);
|
injectData(data);
|
||||||
} else {
|
} else {
|
||||||
document.addEventListener('DOMContentLoaded', injectData.bind(null, data));
|
document.addEventListener('DOMContentLoaded', injectData.bind(null, data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function injectData(data) {
|
function injectData(data) {
|
||||||
var body = document.body;
|
var body = document.body;
|
||||||
body.insertAdjacentHTML('afterbegin', data);
|
body.insertAdjacentHTML('afterbegin', data);
|
||||||
if (body.firstChild.tagName === 'svg') {
|
if (body.firstChild.tagName === 'svg') {
|
||||||
body.firstChild.style.display = 'none';
|
body.firstChild.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof exports === 'object') {
|
if (typeof exports === 'object') {
|
||||||
module.exports = svgSpriteInjector;
|
module.exports = svgSpriteInjector;
|
||||||
} else {
|
} else {
|
||||||
window.svgSpriteInjector = svgSpriteInjector;
|
window.svgSpriteInjector = svgSpriteInjector;
|
||||||
}
|
}
|
||||||
|
|
||||||
} (window, document));
|
} (window, document));
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
<title>tiptap</title>
|
<title>tiptap</title>
|
||||||
<meta name="description" content="A renderless & extendable rich-text editor for Vue.js">
|
<meta name="description" content="A renderless & extendable rich-text editor for Vue.js">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<link rel="shortcut icon" href="/assets/images/favicon.ico">
|
<link rel="shortcut icon" href="/assets/images/favicon.ico">
|
||||||
<meta property="og:image" content="/assets/images/open-graph.png">
|
<meta property="og:image" content="/assets/images/open-graph.png">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
export default function (text = '') {
|
export default function (text = '') {
|
||||||
return (state, dispatch) => {
|
return (state, dispatch) => {
|
||||||
const { $from } = state.selection
|
const { $from } = state.selection
|
||||||
const { pos } = $from.pos
|
const { pos } = $from.pos
|
||||||
|
|
||||||
dispatch(state.tr.insertText(text, pos))
|
dispatch(state.tr.insertText(text, pos))
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import { InputRule } from 'prosemirror-inputrules'
|
import { InputRule } from 'prosemirror-inputrules'
|
||||||
|
|
||||||
export default function (regexp, markType, getAttrs) {
|
export default function (regexp, markType, getAttrs) {
|
||||||
return new InputRule(regexp, (state, match, start, end) => {
|
return new InputRule(regexp, (state, match, start, end) => {
|
||||||
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs
|
const attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs
|
||||||
const { tr } = state
|
const { tr } = state
|
||||||
let markEnd = end
|
let markEnd = end
|
||||||
|
|
||||||
if (match[1]) {
|
if (match[1]) {
|
||||||
const startSpaces = match[0].search(/\S/)
|
const startSpaces = match[0].search(/\S/)
|
||||||
const textStart = start + match[0].indexOf(match[1])
|
const textStart = start + match[0].indexOf(match[1])
|
||||||
const textEnd = textStart + match[1].length
|
const textEnd = textStart + match[1].length
|
||||||
if (textEnd < end) {
|
if (textEnd < end) {
|
||||||
tr.delete(textEnd, end)
|
tr.delete(textEnd, end)
|
||||||
}
|
}
|
||||||
if (textStart > start) {
|
if (textStart > start) {
|
||||||
tr.delete(start + startSpaces, textStart)
|
tr.delete(start + startSpaces, textStart)
|
||||||
}
|
}
|
||||||
markEnd = start + startSpaces + match[1].length
|
markEnd = start + startSpaces + match[1].length
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.addMark(start, markEnd, markType.create(attrs))
|
tr.addMark(start, markEnd, markType.create(attrs))
|
||||||
tr.removeStoredMark(markType) // Do not continue with mark.
|
tr.removeStoredMark(markType) // Do not continue with mark.
|
||||||
return tr
|
return tr
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export default function (type) {
|
export default function (type) {
|
||||||
return (state, dispatch) => {
|
return (state, dispatch) => {
|
||||||
const { from, to } = state.selection
|
const { from, to } = state.selection
|
||||||
return dispatch(state.tr.removeMark(from, to, type))
|
return dispatch(state.tr.removeMark(from, to, type))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
export default function (range, type, attrs = {}) {
|
export default function (range, type, attrs = {}) {
|
||||||
return (state, dispatch) => {
|
return (state, dispatch) => {
|
||||||
const { $from } = state.selection
|
const { $from } = state.selection
|
||||||
const index = $from.index()
|
const index = $from.index()
|
||||||
|
|
||||||
if (!$from.parent.canReplaceWith(index, index, type)) {
|
if (!$from.parent.canReplaceWith(index, index, type)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dispatch) {
|
if (dispatch) {
|
||||||
dispatch(state.tr.replaceWith(range.from, range.to, type.create(attrs)))
|
dispatch(state.tr.replaceWith(range.from, range.to, type.create(attrs)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
export default function (type, attrs = {}) {
|
export default function (type, attrs = {}) {
|
||||||
return (state, dispatch) => {
|
return (state, dispatch) => {
|
||||||
const { $from } = state.selection
|
const { $from } = state.selection
|
||||||
const index = $from.index()
|
const index = $from.index()
|
||||||
|
|
||||||
if (!$from.parent.canReplaceWith(index, index, type)) {
|
if (!$from.parent.canReplaceWith(index, index, type)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dispatch) {
|
if (dispatch) {
|
||||||
dispatch(state.tr.replaceSelectionWith(type.create(attrs)))
|
dispatch(state.tr.replaceSelectionWith(type.create(attrs)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ index = $pos.index(d)
|
|||||||
if (node.type.spec.isolating) return false
|
if (node.type.spec.isolating) return false
|
||||||
let rest = node.content.cutByIndex(index, node.childCount)
|
let rest = node.content.cutByIndex(index, node.childCount)
|
||||||
const after = (typesAfter && typesAfter[i]) || node
|
const after = (typesAfter && typesAfter[i]) || node
|
||||||
if (after != node) rest = rest.replaceChild(0, after.type.create(after.attrs))
|
if (after != node) rest = rest.replaceChild(0, after.type.create(after.attrs))
|
||||||
|
|
||||||
/* Change starts from here */
|
/* Change starts from here */
|
||||||
// if (!node.canReplace(index + 1, node.childCount) || !after.type.validContent(rest))
|
// if (!node.canReplace(index + 1, node.childCount) || !after.type.validContent(rest))
|
||||||
// return false
|
// return false
|
||||||
if (!node.canReplace(index + 1, node.childCount)) return false
|
if (!node.canReplace(index + 1, node.childCount)) return false
|
||||||
/* Change ends here */
|
/* Change ends here */
|
||||||
}
|
}
|
||||||
const index = $pos.indexAfter(base)
|
const index = $pos.indexAfter(base)
|
||||||
const baseType = typesAfter && typesAfter[0]
|
const baseType = typesAfter && typesAfter[0]
|
||||||
@@ -43,7 +43,7 @@ export default function splitListItem(itemType) {
|
|||||||
// list item should be split. Otherwise, bail out and let next
|
// list item should be split. Otherwise, bail out and let next
|
||||||
// command handle lifting.
|
// command handle lifting.
|
||||||
if ($from.depth == 2 || $from.node(-3).type != itemType
|
if ($from.depth == 2 || $from.node(-3).type != itemType
|
||||||
|| $from.index(-2) != $from.node(-2).childCount - 1) return false
|
|| $from.index(-2) != $from.node(-2).childCount - 1) return false
|
||||||
|
|
||||||
if (dispatch) {
|
if (dispatch) {
|
||||||
let wrap = Fragment.empty; const
|
let wrap = Fragment.empty; const
|
||||||
@@ -52,23 +52,23 @@ keepItem = $from.index(-1) > 0
|
|||||||
// from the outer list item to the parent node of the cursor
|
// from the outer list item to the parent node of the cursor
|
||||||
for (let d = $from.depth - (keepItem ? 1 : 2); d >= $from.depth - 3; d--) wrap = Fragment.from($from.node(d).copy(wrap))
|
for (let d = $from.depth - (keepItem ? 1 : 2); d >= $from.depth - 3; d--) wrap = Fragment.from($from.node(d).copy(wrap))
|
||||||
// Add a second list item with an empty default start node
|
// Add a second list item with an empty default start node
|
||||||
wrap = wrap.append(Fragment.from(itemType.createAndFill()))
|
wrap = wrap.append(Fragment.from(itemType.createAndFill()))
|
||||||
const tr = state.tr.replace($from.before(keepItem ? null : -1), $from.after(-3), new Slice(wrap, keepItem ? 3 : 2, 2))
|
const tr = state.tr.replace($from.before(keepItem ? null : -1), $from.after(-3), new Slice(wrap, keepItem ? 3 : 2, 2))
|
||||||
tr.setSelection(state.selection.constructor.near(tr.doc.resolve($from.pos + (keepItem ? 3 : 2))))
|
tr.setSelection(state.selection.constructor.near(tr.doc.resolve($from.pos + (keepItem ? 3 : 2))))
|
||||||
dispatch(tr.scrollIntoView())
|
dispatch(tr.scrollIntoView())
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const nextType = $to.pos == $from.end() ? grandParent.contentMatchAt($from.indexAfter(-1)).defaultType : null
|
const nextType = $to.pos == $from.end() ? grandParent.contentMatchAt($from.indexAfter(-1)).defaultType : null
|
||||||
const tr = state.tr.delete($from.pos, $to.pos)
|
const tr = state.tr.delete($from.pos, $to.pos)
|
||||||
|
|
||||||
/* Change starts from here */
|
/* Change starts from here */
|
||||||
// let types = nextType && [null, {type: nextType}]
|
// let types = nextType && [null, {type: nextType}]
|
||||||
let types = nextType && [{ type: itemType }, { type: nextType }]
|
let types = nextType && [{ type: itemType }, { type: nextType }]
|
||||||
if (!types) types = [{ type: itemType }, null]
|
if (!types) types = [{ type: itemType }, null]
|
||||||
/* Change ends here */
|
/* Change ends here */
|
||||||
|
|
||||||
if (!canSplit(tr.doc, $from.pos, 2, types)) return false
|
if (!canSplit(tr.doc, $from.pos, 2, types)) return false
|
||||||
if (dispatch) dispatch(tr.split($from.pos, 2, [{ type: state.schema.nodes.todo_item, attrs: { done: false } }]).scrollIntoView())
|
if (dispatch) dispatch(tr.split($from.pos, 2, [{ type: state.schema.nodes.todo_item, attrs: { done: false } }]).scrollIntoView())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { setBlockType } from 'prosemirror-commands'
|
|||||||
import { nodeIsActive } from 'tiptap-utils'
|
import { nodeIsActive } from 'tiptap-utils'
|
||||||
|
|
||||||
export default function (type, toggletype, attrs = {}) {
|
export default function (type, toggletype, attrs = {}) {
|
||||||
return (state, dispatch, view) => {
|
return (state, dispatch, view) => {
|
||||||
const isActive = nodeIsActive(state, type, attrs)
|
const isActive = nodeIsActive(state, type, attrs)
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
return setBlockType(toggletype)(state, dispatch, view)
|
return setBlockType(toggletype)(state, dispatch, view)
|
||||||
}
|
}
|
||||||
|
|
||||||
return setBlockType(type, attrs)(state, dispatch, view)
|
return setBlockType(type, attrs)(state, dispatch, view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { wrapIn, lift } from 'prosemirror-commands'
|
|||||||
import { nodeIsActive } from 'tiptap-utils'
|
import { nodeIsActive } from 'tiptap-utils'
|
||||||
|
|
||||||
export default function (type) {
|
export default function (type) {
|
||||||
return (state, dispatch, view) => {
|
return (state, dispatch, view) => {
|
||||||
const isActive = nodeIsActive(state, type)
|
const isActive = nodeIsActive(state, type)
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
return lift(state, dispatch)
|
return lift(state, dispatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
return wrapIn(type)(state, dispatch, view)
|
return wrapIn(type)(state, dispatch, view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export default function (type, attrs) {
|
export default function (type, attrs) {
|
||||||
return (state, dispatch) => {
|
return (state, dispatch) => {
|
||||||
const { from, to } = state.selection
|
const { from, to } = state.selection
|
||||||
return dispatch(state.tr.addMark(from, to, type.create(attrs)))
|
return dispatch(state.tr.addMark(from, to, type.create(attrs)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
import {
|
import {
|
||||||
chainCommands,
|
chainCommands,
|
||||||
deleteSelection,
|
deleteSelection,
|
||||||
joinBackward,
|
joinBackward,
|
||||||
selectNodeBackward,
|
selectNodeBackward,
|
||||||
joinForward,
|
joinForward,
|
||||||
selectNodeForward,
|
selectNodeForward,
|
||||||
joinUp,
|
joinUp,
|
||||||
joinDown,
|
joinDown,
|
||||||
lift,
|
lift,
|
||||||
newlineInCode,
|
newlineInCode,
|
||||||
exitCode,
|
exitCode,
|
||||||
createParagraphNear,
|
createParagraphNear,
|
||||||
liftEmptyBlock,
|
liftEmptyBlock,
|
||||||
splitBlock,
|
splitBlock,
|
||||||
splitBlockKeepMarks,
|
splitBlockKeepMarks,
|
||||||
selectParentNode,
|
selectParentNode,
|
||||||
selectAll,
|
selectAll,
|
||||||
wrapIn,
|
wrapIn,
|
||||||
setBlockType,
|
setBlockType,
|
||||||
toggleMark,
|
toggleMark,
|
||||||
autoJoin,
|
autoJoin,
|
||||||
baseKeymap,
|
baseKeymap,
|
||||||
pcBaseKeymap,
|
pcBaseKeymap,
|
||||||
macBaseKeymap,
|
macBaseKeymap,
|
||||||
} from 'prosemirror-commands'
|
} from 'prosemirror-commands'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addListNodes,
|
addListNodes,
|
||||||
wrapInList,
|
wrapInList,
|
||||||
splitListItem,
|
splitListItem,
|
||||||
liftListItem,
|
liftListItem,
|
||||||
sinkListItem,
|
sinkListItem,
|
||||||
} from 'prosemirror-schema-list'
|
} from 'prosemirror-schema-list'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
wrappingInputRule,
|
wrappingInputRule,
|
||||||
textblockTypeInputRule,
|
textblockTypeInputRule,
|
||||||
} from 'prosemirror-inputrules'
|
} from 'prosemirror-inputrules'
|
||||||
|
|
||||||
import insertText from './commands/insertText'
|
import insertText from './commands/insertText'
|
||||||
@@ -50,52 +50,52 @@ import toggleWrap from './commands/toggleWrap'
|
|||||||
import updateMark from './commands/updateMark'
|
import updateMark from './commands/updateMark'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
// prosemirror-commands
|
// prosemirror-commands
|
||||||
chainCommands,
|
chainCommands,
|
||||||
deleteSelection,
|
deleteSelection,
|
||||||
joinBackward,
|
joinBackward,
|
||||||
selectNodeBackward,
|
selectNodeBackward,
|
||||||
joinForward,
|
joinForward,
|
||||||
selectNodeForward,
|
selectNodeForward,
|
||||||
joinUp,
|
joinUp,
|
||||||
joinDown,
|
joinDown,
|
||||||
lift,
|
lift,
|
||||||
newlineInCode,
|
newlineInCode,
|
||||||
exitCode,
|
exitCode,
|
||||||
createParagraphNear,
|
createParagraphNear,
|
||||||
liftEmptyBlock,
|
liftEmptyBlock,
|
||||||
splitBlock,
|
splitBlock,
|
||||||
splitBlockKeepMarks,
|
splitBlockKeepMarks,
|
||||||
selectParentNode,
|
selectParentNode,
|
||||||
selectAll,
|
selectAll,
|
||||||
wrapIn,
|
wrapIn,
|
||||||
setBlockType,
|
setBlockType,
|
||||||
toggleMark,
|
toggleMark,
|
||||||
autoJoin,
|
autoJoin,
|
||||||
baseKeymap,
|
baseKeymap,
|
||||||
pcBaseKeymap,
|
pcBaseKeymap,
|
||||||
macBaseKeymap,
|
macBaseKeymap,
|
||||||
|
|
||||||
// prosemirror-schema-list
|
// prosemirror-schema-list
|
||||||
addListNodes,
|
addListNodes,
|
||||||
wrapInList,
|
wrapInList,
|
||||||
splitListItem,
|
splitListItem,
|
||||||
liftListItem,
|
liftListItem,
|
||||||
sinkListItem,
|
sinkListItem,
|
||||||
|
|
||||||
// prosemirror-inputrules
|
// prosemirror-inputrules
|
||||||
wrappingInputRule,
|
wrappingInputRule,
|
||||||
textblockTypeInputRule,
|
textblockTypeInputRule,
|
||||||
|
|
||||||
// custom
|
// custom
|
||||||
insertText,
|
insertText,
|
||||||
markInputRule,
|
markInputRule,
|
||||||
removeMark,
|
removeMark,
|
||||||
replaceText,
|
replaceText,
|
||||||
setInlineBlockType,
|
setInlineBlockType,
|
||||||
splitToDefaultListItem,
|
splitToDefaultListItem,
|
||||||
toggleBlockType,
|
toggleBlockType,
|
||||||
toggleList,
|
toggleList,
|
||||||
toggleWrap,
|
toggleWrap,
|
||||||
updateMark,
|
updateMark,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,35 +3,35 @@ import { history, undo, redo } from 'prosemirror-history'
|
|||||||
|
|
||||||
export default class History extends Extension {
|
export default class History extends Extension {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'history'
|
return 'history'
|
||||||
}
|
}
|
||||||
|
|
||||||
keys() {
|
keys() {
|
||||||
const isMac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false
|
const isMac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'Mod-z': undo,
|
'Mod-z': undo,
|
||||||
'Shift-Mod-z': redo,
|
'Shift-Mod-z': redo,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isMac) {
|
if (!isMac) {
|
||||||
keymap['Mod-y'] = redo
|
keymap['Mod-y'] = redo
|
||||||
}
|
}
|
||||||
|
|
||||||
return keymap
|
return keymap
|
||||||
}
|
}
|
||||||
|
|
||||||
get plugins() {
|
get plugins() {
|
||||||
return [
|
return [
|
||||||
history(),
|
history(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
commands() {
|
commands() {
|
||||||
return {
|
return {
|
||||||
undo: () => undo,
|
undo: () => undo,
|
||||||
redo: () => redo,
|
redo: () => redo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,40 +3,40 @@ import { Decoration, DecorationSet } from 'prosemirror-view'
|
|||||||
|
|
||||||
export default class Placeholder extends Extension {
|
export default class Placeholder extends Extension {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'placeholder'
|
return 'placeholder'
|
||||||
}
|
}
|
||||||
|
|
||||||
get defaultOptions() {
|
get defaultOptions() {
|
||||||
return {
|
return {
|
||||||
emptyNodeClass: 'is-empty',
|
emptyNodeClass: 'is-empty',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get plugins() {
|
get plugins() {
|
||||||
return [
|
return [
|
||||||
new Plugin({
|
new Plugin({
|
||||||
props: {
|
props: {
|
||||||
decorations: ({ doc }) => {
|
decorations: ({ doc }) => {
|
||||||
const decorations = []
|
const decorations = []
|
||||||
const completelyEmpty = doc.textContent === '' && doc.childCount <= 1 && doc.content.size <= 2
|
const completelyEmpty = doc.textContent === '' && doc.childCount <= 1 && doc.content.size <= 2
|
||||||
|
|
||||||
doc.descendants((node, pos) => {
|
doc.descendants((node, pos) => {
|
||||||
if (!completelyEmpty) {
|
if (!completelyEmpty) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const decoration = Decoration.node(pos, pos + node.nodeSize, {
|
const decoration = Decoration.node(pos, pos + node.nodeSize, {
|
||||||
class: this.options.emptyNodeClass,
|
class: this.options.emptyNodeClass,
|
||||||
})
|
})
|
||||||
decorations.push(decoration)
|
decorations.push(decoration)
|
||||||
})
|
})
|
||||||
|
|
||||||
return DecorationSet.create(doc, decorations)
|
return DecorationSet.create(doc, decorations)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,43 +3,43 @@ import { toggleMark, markInputRule } from 'tiptap-commands'
|
|||||||
|
|
||||||
export default class Bold extends Mark {
|
export default class Bold extends Mark {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'bold'
|
return 'bold'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{
|
{
|
||||||
tag: 'strong',
|
tag: 'strong',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'b',
|
tag: 'b',
|
||||||
getAttrs: node => node.style.fontWeight !== 'normal' && null,
|
getAttrs: node => node.style.fontWeight !== 'normal' && null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
style: 'font-weight',
|
style: 'font-weight',
|
||||||
getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null,
|
getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
toDOM: () => ['strong', 0],
|
toDOM: () => ['strong', 0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keys({ type }) {
|
keys({ type }) {
|
||||||
return {
|
return {
|
||||||
'Mod-b': toggleMark(type),
|
'Mod-b': toggleMark(type),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commands({ type }) {
|
commands({ type }) {
|
||||||
return () => toggleMark(type)
|
return () => toggleMark(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
inputRules({ type }) {
|
inputRules({ type }) {
|
||||||
return [
|
return [
|
||||||
markInputRule(/(?:\*\*|__)([^*_]+)(?:\*\*|__)$/, type),
|
markInputRule(/(?:\*\*|__)([^*_]+)(?:\*\*|__)$/, type),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,33 +3,33 @@ import { toggleMark, markInputRule } from 'tiptap-commands'
|
|||||||
|
|
||||||
export default class Code extends Mark {
|
export default class Code extends Mark {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'code'
|
return 'code'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{ tag: 'code' },
|
{ tag: 'code' },
|
||||||
],
|
],
|
||||||
toDOM: () => ['code', 0],
|
toDOM: () => ['code', 0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keys({ type }) {
|
keys({ type }) {
|
||||||
return {
|
return {
|
||||||
'Mod-`': toggleMark(type),
|
'Mod-`': toggleMark(type),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commands({ type }) {
|
commands({ type }) {
|
||||||
return () => toggleMark(type)
|
return () => toggleMark(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
inputRules({ type }) {
|
inputRules({ type }) {
|
||||||
return [
|
return [
|
||||||
markInputRule(/(?:`)([^`]+)(?:`)$/, type),
|
markInputRule(/(?:`)([^`]+)(?:`)$/, type),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,35 +3,35 @@ import { toggleMark, markInputRule } from 'tiptap-commands'
|
|||||||
|
|
||||||
export default class Italic extends Mark {
|
export default class Italic extends Mark {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'italic'
|
return 'italic'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{ tag: 'i' },
|
{ tag: 'i' },
|
||||||
{ tag: 'em' },
|
{ tag: 'em' },
|
||||||
{ style: 'font-style=italic' },
|
{ style: 'font-style=italic' },
|
||||||
],
|
],
|
||||||
toDOM: () => ['em', 0],
|
toDOM: () => ['em', 0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keys({ type }) {
|
keys({ type }) {
|
||||||
return {
|
return {
|
||||||
'Mod-i': toggleMark(type),
|
'Mod-i': toggleMark(type),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commands({ type }) {
|
commands({ type }) {
|
||||||
return () => toggleMark(type)
|
return () => toggleMark(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
inputRules({ type }) {
|
inputRules({ type }) {
|
||||||
return [
|
return [
|
||||||
markInputRule(/(?:^|[^*_])(?:\*|_)([^*_]+)(?:\*|_)$/, type),
|
markInputRule(/(?:^|[^*_])(?:\*|_)([^*_]+)(?:\*|_)$/, type),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,41 +3,41 @@ import { updateMark, removeMark } from 'tiptap-commands'
|
|||||||
|
|
||||||
export default class Link extends Mark {
|
export default class Link extends Mark {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'link'
|
return 'link'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
attrs: {
|
attrs: {
|
||||||
href: {
|
href: {
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
inclusive: false,
|
inclusive: false,
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{
|
{
|
||||||
tag: 'a[href]',
|
tag: 'a[href]',
|
||||||
getAttrs: dom => ({
|
getAttrs: dom => ({
|
||||||
href: dom.getAttribute('href'),
|
href: dom.getAttribute('href'),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
toDOM: node => ['a', {
|
toDOM: node => ['a', {
|
||||||
...node.attrs,
|
...node.attrs,
|
||||||
rel: 'noopener noreferrer nofollow',
|
rel: 'noopener noreferrer nofollow',
|
||||||
}, 0],
|
}, 0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commands({ type }) {
|
commands({ type }) {
|
||||||
return attrs => {
|
return attrs => {
|
||||||
if (attrs.href) {
|
if (attrs.href) {
|
||||||
return updateMark(type, attrs)
|
return updateMark(type, attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
return removeMark(type)
|
return removeMark(type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,45 +3,45 @@ import { toggleMark, markInputRule } from 'tiptap-commands'
|
|||||||
|
|
||||||
export default class Strike extends Mark {
|
export default class Strike extends Mark {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'strike'
|
return 'strike'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{
|
{
|
||||||
tag: 's',
|
tag: 's',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'del',
|
tag: 'del',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'strike',
|
tag: 'strike',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
style: 'text-decoration',
|
style: 'text-decoration',
|
||||||
getAttrs: value => value === 'line-through',
|
getAttrs: value => value === 'line-through',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
toDOM: () => ['s', 0],
|
toDOM: () => ['s', 0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keys({ type }) {
|
keys({ type }) {
|
||||||
return {
|
return {
|
||||||
'Mod-d': toggleMark(type),
|
'Mod-d': toggleMark(type),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commands({ type }) {
|
commands({ type }) {
|
||||||
return () => toggleMark(type)
|
return () => toggleMark(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
inputRules({ type }) {
|
inputRules({ type }) {
|
||||||
return [
|
return [
|
||||||
markInputRule(/~([^~]+)~$/, type),
|
markInputRule(/~([^~]+)~$/, type),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,33 +3,33 @@ import { toggleMark } from 'tiptap-commands'
|
|||||||
|
|
||||||
export default class Underline extends Mark {
|
export default class Underline extends Mark {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'underline'
|
return 'underline'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{
|
{
|
||||||
tag: 'u',
|
tag: 'u',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
style: 'text-decoration',
|
style: 'text-decoration',
|
||||||
getAttrs: value => value === 'underline',
|
getAttrs: value => value === 'underline',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
toDOM: () => ['u', 0],
|
toDOM: () => ['u', 0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keys({ type }) {
|
keys({ type }) {
|
||||||
return {
|
return {
|
||||||
'Mod-u': toggleMark(type),
|
'Mod-u': toggleMark(type),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commands({ type }) {
|
commands({ type }) {
|
||||||
return () => toggleMark(type)
|
return () => toggleMark(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,37 +3,37 @@ import { wrappingInputRule, toggleWrap } from 'tiptap-commands'
|
|||||||
|
|
||||||
export default class Blockquote extends Node {
|
export default class Blockquote extends Node {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'blockquote'
|
return 'blockquote'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
content: 'block*',
|
content: 'block*',
|
||||||
group: 'block',
|
group: 'block',
|
||||||
defining: true,
|
defining: true,
|
||||||
draggable: false,
|
draggable: false,
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{ tag: 'blockquote' },
|
{ tag: 'blockquote' },
|
||||||
],
|
],
|
||||||
toDOM: () => ['blockquote', 0],
|
toDOM: () => ['blockquote', 0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commands({ type, schema }) {
|
commands({ type, schema }) {
|
||||||
return () => toggleWrap(type, schema.nodes.paragraph)
|
return () => toggleWrap(type, schema.nodes.paragraph)
|
||||||
}
|
}
|
||||||
|
|
||||||
keys({ type }) {
|
keys({ type }) {
|
||||||
return {
|
return {
|
||||||
'Ctrl->': toggleWrap(type),
|
'Ctrl->': toggleWrap(type),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inputRules({ type }) {
|
inputRules({ type }) {
|
||||||
return [
|
return [
|
||||||
wrappingInputRule(/^\s*>\s$/, type),
|
wrappingInputRule(/^\s*>\s$/, type),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,35 +3,35 @@ import { wrappingInputRule, toggleList } from 'tiptap-commands'
|
|||||||
|
|
||||||
export default class Bullet extends Node {
|
export default class Bullet extends Node {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'bullet_list'
|
return 'bullet_list'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
content: 'list_item+',
|
content: 'list_item+',
|
||||||
group: 'block',
|
group: 'block',
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{ tag: 'ul' },
|
{ tag: 'ul' },
|
||||||
],
|
],
|
||||||
toDOM: () => ['ul', 0],
|
toDOM: () => ['ul', 0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commands({ type, schema }) {
|
commands({ type, schema }) {
|
||||||
return () => toggleList(type, schema.nodes.list_item)
|
return () => toggleList(type, schema.nodes.list_item)
|
||||||
}
|
}
|
||||||
|
|
||||||
keys({ type, schema }) {
|
keys({ type, schema }) {
|
||||||
return {
|
return {
|
||||||
'Shift-Ctrl-8': toggleList(type, schema.nodes.list_item),
|
'Shift-Ctrl-8': toggleList(type, schema.nodes.list_item),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inputRules({ type }) {
|
inputRules({ type }) {
|
||||||
return [
|
return [
|
||||||
wrappingInputRule(/^\s*([-+*])\s$/, type),
|
wrappingInputRule(/^\s*([-+*])\s$/, type),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,39 +3,39 @@ import { toggleBlockType, setBlockType, textblockTypeInputRule } from 'tiptap-co
|
|||||||
|
|
||||||
export default class CodeBlock extends Node {
|
export default class CodeBlock extends Node {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'code_block'
|
return 'code_block'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
content: 'text*',
|
content: 'text*',
|
||||||
marks: '',
|
marks: '',
|
||||||
group: 'block',
|
group: 'block',
|
||||||
code: true,
|
code: true,
|
||||||
defining: true,
|
defining: true,
|
||||||
draggable: false,
|
draggable: false,
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{ tag: 'pre', preserveWhitespace: 'full' },
|
{ tag: 'pre', preserveWhitespace: 'full' },
|
||||||
],
|
],
|
||||||
toDOM: () => ['pre', ['code', 0]],
|
toDOM: () => ['pre', ['code', 0]],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commands({ type, schema }) {
|
commands({ type, schema }) {
|
||||||
return () => toggleBlockType(type, schema.nodes.paragraph)
|
return () => toggleBlockType(type, schema.nodes.paragraph)
|
||||||
}
|
}
|
||||||
|
|
||||||
keys({ type }) {
|
keys({ type }) {
|
||||||
return {
|
return {
|
||||||
'Shift-Ctrl-\\': setBlockType(type),
|
'Shift-Ctrl-\\': setBlockType(type),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inputRules({ type }) {
|
inputRules({ type }) {
|
||||||
return [
|
return [
|
||||||
textblockTypeInputRule(/^```$/, type),
|
textblockTypeInputRule(/^```$/, type),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,139 +5,139 @@ import { findBlockNodes } from 'prosemirror-utils'
|
|||||||
import low from 'lowlight/lib/core'
|
import low from 'lowlight/lib/core'
|
||||||
|
|
||||||
function getDecorations(doc) {
|
function getDecorations(doc) {
|
||||||
const decorations = []
|
const decorations = []
|
||||||
|
|
||||||
const blocks = findBlockNodes(doc)
|
const blocks = findBlockNodes(doc)
|
||||||
.filter(item => item.node.type.name === 'code_block')
|
.filter(item => item.node.type.name === 'code_block')
|
||||||
|
|
||||||
const flatten = list => list.reduce(
|
const flatten = list => list.reduce(
|
||||||
(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [],
|
(a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), [],
|
||||||
)
|
)
|
||||||
|
|
||||||
function parseNodes(nodes, className = []) {
|
function parseNodes(nodes, className = []) {
|
||||||
return nodes.map(node => {
|
return nodes.map(node => {
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
...className,
|
...className,
|
||||||
...node.properties ? node.properties.className : [],
|
...node.properties ? node.properties.className : [],
|
||||||
]
|
]
|
||||||
|
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
return parseNodes(node.children, classes)
|
return parseNodes(node.children, classes)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: node.value,
|
text: node.value,
|
||||||
classes,
|
classes,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
blocks.forEach(block => {
|
blocks.forEach(block => {
|
||||||
let startPos = block.pos + 1
|
let startPos = block.pos + 1
|
||||||
const nodes = low.highlightAuto(block.node.textContent).value
|
const nodes = low.highlightAuto(block.node.textContent).value
|
||||||
|
|
||||||
flatten(parseNodes(nodes))
|
flatten(parseNodes(nodes))
|
||||||
.map(node => {
|
.map(node => {
|
||||||
const from = startPos
|
const from = startPos
|
||||||
const to = from + node.text.length
|
const to = from + node.text.length
|
||||||
|
|
||||||
startPos = to
|
startPos = to
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...node,
|
...node,
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.forEach(node => {
|
.forEach(node => {
|
||||||
const decoration = Decoration.inline(node.from, node.to, {
|
const decoration = Decoration.inline(node.from, node.to, {
|
||||||
class: node.classes.join(' '),
|
class: node.classes.join(' '),
|
||||||
})
|
})
|
||||||
decorations.push(decoration)
|
decorations.push(decoration)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return DecorationSet.create(doc, decorations)
|
return DecorationSet.create(doc, decorations)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CodeBlockHighlight extends Node {
|
export default class CodeBlockHighlight extends Node {
|
||||||
|
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
super(options)
|
super(options)
|
||||||
try {
|
try {
|
||||||
Object.entries(this.options.languages).forEach(([name, mapping]) => {
|
Object.entries(this.options.languages).forEach(([name, mapping]) => {
|
||||||
low.registerLanguage(name, mapping)
|
low.registerLanguage(name, mapping)
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error('Invalid syntax highlight definitions: define at least one highlight.js language mapping')
|
throw new Error('Invalid syntax highlight definitions: define at least one highlight.js language mapping')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get defaultOptions() {
|
get defaultOptions() {
|
||||||
return {
|
return {
|
||||||
languages: {},
|
languages: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'code_block'
|
return 'code_block'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
content: 'text*',
|
content: 'text*',
|
||||||
marks: '',
|
marks: '',
|
||||||
group: 'block',
|
group: 'block',
|
||||||
code: true,
|
code: true,
|
||||||
defining: true,
|
defining: true,
|
||||||
draggable: false,
|
draggable: false,
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{ tag: 'pre', preserveWhitespace: 'full' },
|
{ tag: 'pre', preserveWhitespace: 'full' },
|
||||||
],
|
],
|
||||||
toDOM: () => ['pre', ['code', 0]],
|
toDOM: () => ['pre', ['code', 0]],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commands({ type, schema }) {
|
commands({ type, schema }) {
|
||||||
return () => toggleBlockType(type, schema.nodes.paragraph)
|
return () => toggleBlockType(type, schema.nodes.paragraph)
|
||||||
}
|
}
|
||||||
|
|
||||||
keys({ type }) {
|
keys({ type }) {
|
||||||
return {
|
return {
|
||||||
'Shift-Ctrl-\\': setBlockType(type),
|
'Shift-Ctrl-\\': setBlockType(type),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inputRules({ type }) {
|
inputRules({ type }) {
|
||||||
return [
|
return [
|
||||||
textblockTypeInputRule(/^```$/, type),
|
textblockTypeInputRule(/^```$/, type),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
get plugins() {
|
get plugins() {
|
||||||
return [
|
return [
|
||||||
new Plugin({
|
new Plugin({
|
||||||
state: {
|
state: {
|
||||||
init(_, { doc }) {
|
init(_, { doc }) {
|
||||||
return getDecorations(doc)
|
return getDecorations(doc)
|
||||||
},
|
},
|
||||||
apply(tr, set) {
|
apply(tr, set) {
|
||||||
// TODO: find way to cache decorations
|
// TODO: find way to cache decorations
|
||||||
// see: https://discuss.prosemirror.net/t/how-to-update-multiple-inline-decorations-on-node-change/1493
|
// see: https://discuss.prosemirror.net/t/how-to-update-multiple-inline-decorations-on-node-change/1493
|
||||||
if (tr.docChanged) {
|
if (tr.docChanged) {
|
||||||
return getDecorations(tr.doc)
|
return getDecorations(tr.doc)
|
||||||
}
|
}
|
||||||
return set.map(tr.mapping, tr.doc)
|
return set.map(tr.mapping, tr.doc)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
decorations(state) {
|
decorations(state) {
|
||||||
return this.getState(state)
|
return this.getState(state)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,31 +3,31 @@ import { chainCommands, exitCode } from 'tiptap-commands'
|
|||||||
|
|
||||||
export default class HardBreak extends Node {
|
export default class HardBreak extends Node {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'hard_break'
|
return 'hard_break'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
inline: true,
|
inline: true,
|
||||||
group: 'inline',
|
group: 'inline',
|
||||||
selectable: false,
|
selectable: false,
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{ tag: 'br' },
|
{ tag: 'br' },
|
||||||
],
|
],
|
||||||
toDOM: () => ['br'],
|
toDOM: () => ['br'],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keys({ type }) {
|
keys({ type }) {
|
||||||
const command = chainCommands(exitCode, (state, dispatch) => {
|
const command = chainCommands(exitCode, (state, dispatch) => {
|
||||||
dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView())
|
dispatch(state.tr.replaceSelectionWith(type.create()).scrollIntoView())
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
'Mod-Enter': command,
|
'Mod-Enter': command,
|
||||||
'Shift-Enter': command,
|
'Shift-Enter': command,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,55 +3,55 @@ import { setBlockType, textblockTypeInputRule, toggleBlockType } from 'tiptap-co
|
|||||||
|
|
||||||
export default class Heading extends Node {
|
export default class Heading extends Node {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'heading'
|
return 'heading'
|
||||||
}
|
}
|
||||||
|
|
||||||
get defaultOptions() {
|
get defaultOptions() {
|
||||||
return {
|
return {
|
||||||
levels: [1, 2, 3, 4, 5, 6],
|
levels: [1, 2, 3, 4, 5, 6],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
attrs: {
|
attrs: {
|
||||||
level: {
|
level: {
|
||||||
default: 1,
|
default: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
content: 'inline*',
|
content: 'inline*',
|
||||||
group: 'block',
|
group: 'block',
|
||||||
defining: true,
|
defining: true,
|
||||||
draggable: false,
|
draggable: false,
|
||||||
parseDOM: this.options.levels
|
parseDOM: this.options.levels
|
||||||
.map(level => ({
|
.map(level => ({
|
||||||
tag: `h${level}`,
|
tag: `h${level}`,
|
||||||
attrs: { level },
|
attrs: { level },
|
||||||
})),
|
})),
|
||||||
toDOM: node => [`h${node.attrs.level}`, 0],
|
toDOM: node => [`h${node.attrs.level}`, 0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commands({ type, schema }) {
|
commands({ type, schema }) {
|
||||||
return attrs => toggleBlockType(type, schema.nodes.paragraph, attrs)
|
return attrs => toggleBlockType(type, schema.nodes.paragraph, attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
keys({ type }) {
|
keys({ type }) {
|
||||||
return this.options.levels.reduce((items, level) => ({
|
return this.options.levels.reduce((items, level) => ({
|
||||||
...items,
|
...items,
|
||||||
...{
|
...{
|
||||||
[`Shift-Ctrl-${level}`]: setBlockType(type, { level }),
|
[`Shift-Ctrl-${level}`]: setBlockType(type, { level }),
|
||||||
},
|
},
|
||||||
}), {})
|
}), {})
|
||||||
}
|
}
|
||||||
|
|
||||||
inputRules({ type }) {
|
inputRules({ type }) {
|
||||||
return this.options.levels.map(level => textblockTypeInputRule(
|
return this.options.levels.map(level => textblockTypeInputRule(
|
||||||
new RegExp(`^(#{1,${level}})\\s$`),
|
new RegExp(`^(#{1,${level}})\\s$`),
|
||||||
type,
|
type,
|
||||||
match => ({ level }),
|
match => ({ level }),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,92 +2,92 @@ import { Node, Plugin } from 'tiptap'
|
|||||||
|
|
||||||
export default class Image extends Node {
|
export default class Image extends Node {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'image'
|
return 'image'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
inline: true,
|
inline: true,
|
||||||
attrs: {
|
attrs: {
|
||||||
src: {},
|
src: {},
|
||||||
alt: {
|
alt: {
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
group: 'inline',
|
group: 'inline',
|
||||||
draggable: true,
|
draggable: true,
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{
|
{
|
||||||
tag: 'img[src]',
|
tag: 'img[src]',
|
||||||
getAttrs: dom => ({
|
getAttrs: dom => ({
|
||||||
src: dom.getAttribute('src'),
|
src: dom.getAttribute('src'),
|
||||||
title: dom.getAttribute('title'),
|
title: dom.getAttribute('title'),
|
||||||
alt: dom.getAttribute('alt'),
|
alt: dom.getAttribute('alt'),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
toDOM: node => ['img', node.attrs],
|
toDOM: node => ['img', node.attrs],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commands({ type }) {
|
commands({ type }) {
|
||||||
return attrs => (state, dispatch) => {
|
return attrs => (state, dispatch) => {
|
||||||
const { selection } = state
|
const { selection } = state
|
||||||
const position = selection.$cursor ? selection.$cursor.pos : selection.$to.pos
|
const position = selection.$cursor ? selection.$cursor.pos : selection.$to.pos
|
||||||
const node = type.create(attrs)
|
const node = type.create(attrs)
|
||||||
const transaction = state.tr.insert(position, node)
|
const transaction = state.tr.insert(position, node)
|
||||||
dispatch(transaction)
|
dispatch(transaction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get plugins() {
|
get plugins() {
|
||||||
return [
|
return [
|
||||||
new Plugin({
|
new Plugin({
|
||||||
props: {
|
props: {
|
||||||
handleDOMEvents: {
|
handleDOMEvents: {
|
||||||
drop(view, event) {
|
drop(view, event) {
|
||||||
const hasFiles = event.dataTransfer
|
const hasFiles = event.dataTransfer
|
||||||
&& event.dataTransfer.files
|
&& event.dataTransfer.files
|
||||||
&& event.dataTransfer.files.length
|
&& event.dataTransfer.files.length
|
||||||
|
|
||||||
if (!hasFiles) {
|
if (!hasFiles) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const images = Array
|
const images = Array
|
||||||
.from(event.dataTransfer.files)
|
.from(event.dataTransfer.files)
|
||||||
.filter(file => (/image/i).test(file.type))
|
.filter(file => (/image/i).test(file.type))
|
||||||
|
|
||||||
if (images.length === 0) {
|
if (images.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
const { schema } = view.state
|
const { schema } = view.state
|
||||||
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY })
|
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY })
|
||||||
|
|
||||||
images.forEach(image => {
|
images.forEach(image => {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
|
|
||||||
reader.onload = readerEvent => {
|
reader.onload = readerEvent => {
|
||||||
const node = schema.nodes.image.create({
|
const node = schema.nodes.image.create({
|
||||||
src: readerEvent.target.result,
|
src: readerEvent.target.result,
|
||||||
})
|
})
|
||||||
const transaction = view.state.tr.insert(coordinates.pos, node)
|
const transaction = view.state.tr.insert(coordinates.pos, node)
|
||||||
view.dispatch(transaction)
|
view.dispatch(transaction)
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(image)
|
reader.readAsDataURL(image)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,28 +3,28 @@ import { splitListItem, liftListItem, sinkListItem } from 'tiptap-commands'
|
|||||||
|
|
||||||
export default class ListItem extends Node {
|
export default class ListItem extends Node {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'list_item'
|
return 'list_item'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
content: 'paragraph block*',
|
content: 'paragraph block*',
|
||||||
defining: true,
|
defining: true,
|
||||||
draggable: false,
|
draggable: false,
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{ tag: 'li' },
|
{ tag: 'li' },
|
||||||
],
|
],
|
||||||
toDOM: () => ['li', 0],
|
toDOM: () => ['li', 0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keys({ type }) {
|
keys({ type }) {
|
||||||
return {
|
return {
|
||||||
Enter: splitListItem(type),
|
Enter: splitListItem(type),
|
||||||
Tab: sinkListItem(type),
|
Tab: sinkListItem(type),
|
||||||
'Shift-Tab': liftListItem(type),
|
'Shift-Tab': liftListItem(type),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,68 +4,68 @@ import SuggestionsPlugin from '../plugins/Suggestions'
|
|||||||
|
|
||||||
export default class Mention extends Node {
|
export default class Mention extends Node {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'mention'
|
return 'mention'
|
||||||
}
|
}
|
||||||
|
|
||||||
get defaultOptions() {
|
get defaultOptions() {
|
||||||
return {
|
return {
|
||||||
matcher: {
|
matcher: {
|
||||||
char: '@',
|
char: '@',
|
||||||
allowSpaces: false,
|
allowSpaces: false,
|
||||||
startOfLine: false,
|
startOfLine: false,
|
||||||
},
|
},
|
||||||
mentionClass: 'mention',
|
mentionClass: 'mention',
|
||||||
suggestionClass: 'mention-suggestion',
|
suggestionClass: 'mention-suggestion',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
attrs: {
|
attrs: {
|
||||||
id: {},
|
id: {},
|
||||||
label: {},
|
label: {},
|
||||||
},
|
},
|
||||||
group: 'inline',
|
group: 'inline',
|
||||||
inline: true,
|
inline: true,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
atom: true,
|
atom: true,
|
||||||
toDOM: node => [
|
toDOM: node => [
|
||||||
'span',
|
'span',
|
||||||
{
|
{
|
||||||
class: this.options.mentionClass,
|
class: this.options.mentionClass,
|
||||||
'data-mention-id': node.attrs.id,
|
'data-mention-id': node.attrs.id,
|
||||||
},
|
},
|
||||||
`${this.options.matcher.char}${node.attrs.label}`,
|
`${this.options.matcher.char}${node.attrs.label}`,
|
||||||
],
|
],
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{
|
{
|
||||||
tag: 'span[data-mention-id]',
|
tag: 'span[data-mention-id]',
|
||||||
getAttrs: dom => {
|
getAttrs: dom => {
|
||||||
const id = dom.getAttribute('data-mention-id')
|
const id = dom.getAttribute('data-mention-id')
|
||||||
const label = dom.innerText.split(this.options.matcher.char).join('')
|
const label = dom.innerText.split(this.options.matcher.char).join('')
|
||||||
return { id, label }
|
return { id, label }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get plugins() {
|
get plugins() {
|
||||||
return [
|
return [
|
||||||
SuggestionsPlugin({
|
SuggestionsPlugin({
|
||||||
command: ({ range, attrs, schema }) => replaceText(range, schema.nodes.mention, attrs),
|
command: ({ range, attrs, schema }) => replaceText(range, schema.nodes.mention, attrs),
|
||||||
appendText: ' ',
|
appendText: ' ',
|
||||||
matcher: this.options.matcher,
|
matcher: this.options.matcher,
|
||||||
items: this.options.items,
|
items: this.options.items,
|
||||||
onEnter: this.options.onEnter,
|
onEnter: this.options.onEnter,
|
||||||
onChange: this.options.onChange,
|
onChange: this.options.onChange,
|
||||||
onExit: this.options.onExit,
|
onExit: this.options.onExit,
|
||||||
onKeyDown: this.options.onKeyDown,
|
onKeyDown: this.options.onKeyDown,
|
||||||
onFilter: this.options.onFilter,
|
onFilter: this.options.onFilter,
|
||||||
suggestionClass: this.options.suggestionClass,
|
suggestionClass: this.options.suggestionClass,
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,50 +3,50 @@ import { wrappingInputRule, toggleList } from 'tiptap-commands'
|
|||||||
|
|
||||||
export default class OrderedList extends Node {
|
export default class OrderedList extends Node {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'ordered_list'
|
return 'ordered_list'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
attrs: {
|
attrs: {
|
||||||
order: {
|
order: {
|
||||||
default: 1,
|
default: 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
content: 'list_item+',
|
content: 'list_item+',
|
||||||
group: 'block',
|
group: 'block',
|
||||||
parseDOM: [
|
parseDOM: [
|
||||||
{
|
{
|
||||||
tag: 'ol',
|
tag: 'ol',
|
||||||
getAttrs: dom => ({
|
getAttrs: dom => ({
|
||||||
order: dom.hasAttribute('start') ? +dom.getAttribute('start') : 1,
|
order: dom.hasAttribute('start') ? +dom.getAttribute('start') : 1,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
toDOM: node => (node.attrs.order === 1 ? ['ol', 0] : ['ol', { start: node.attrs.order }, 0]),
|
toDOM: node => (node.attrs.order === 1 ? ['ol', 0] : ['ol', { start: node.attrs.order }, 0]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commands({ type, schema }) {
|
commands({ type, schema }) {
|
||||||
return () => toggleList(type, schema.nodes.list_item)
|
return () => toggleList(type, schema.nodes.list_item)
|
||||||
}
|
}
|
||||||
|
|
||||||
keys({ type, schema }) {
|
keys({ type, schema }) {
|
||||||
return {
|
return {
|
||||||
'Shift-Ctrl-9': toggleList(type, schema.nodes.list_item),
|
'Shift-Ctrl-9': toggleList(type, schema.nodes.list_item),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inputRules({ type }) {
|
inputRules({ type }) {
|
||||||
return [
|
return [
|
||||||
wrappingInputRule(
|
wrappingInputRule(
|
||||||
/^(\d+)\.\s$/,
|
/^(\d+)\.\s$/,
|
||||||
type,
|
type,
|
||||||
match => ({ order: +match[1] }),
|
match => ({ order: +match[1] }),
|
||||||
(match, node) => node.childCount + node.attrs.order === +match[1],
|
(match, node) => node.childCount + node.attrs.order === +match[1],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,64 +3,64 @@ import { splitToDefaultListItem, liftListItem } from 'tiptap-commands'
|
|||||||
|
|
||||||
export default class TodoItem extends Node {
|
export default class TodoItem extends Node {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'todo_item'
|
return 'todo_item'
|
||||||
}
|
}
|
||||||
|
|
||||||
get view() {
|
get view() {
|
||||||
return {
|
return {
|
||||||
props: ['node', 'updateAttrs', 'editable'],
|
props: ['node', 'updateAttrs', 'editable'],
|
||||||
methods: {
|
methods: {
|
||||||
onChange() {
|
onChange() {
|
||||||
this.updateAttrs({
|
this.updateAttrs({
|
||||||
done: !this.node.attrs.done,
|
done: !this.node.attrs.done,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<li data-type="todo_item" :data-done="node.attrs.done.toString()">
|
<li data-type="todo_item" :data-done="node.attrs.done.toString()">
|
||||||
<span class="todo-checkbox" contenteditable="false" @click="onChange"></span>
|
<span class="todo-checkbox" contenteditable="false" @click="onChange"></span>
|
||||||
<div class="todo-content" ref="content" :contenteditable="editable.toString()"></div>
|
<div class="todo-content" ref="content" :contenteditable="editable.toString()"></div>
|
||||||
</li>
|
</li>
|
||||||
`,
|
`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
attrs: {
|
attrs: {
|
||||||
done: {
|
done: {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
draggable: false,
|
draggable: false,
|
||||||
content: 'paragraph',
|
content: 'paragraph',
|
||||||
toDOM(node) {
|
toDOM(node) {
|
||||||
const { done } = node.attrs
|
const { done } = node.attrs
|
||||||
|
|
||||||
return ['li', {
|
return ['li', {
|
||||||
'data-type': 'todo_item',
|
'data-type': 'todo_item',
|
||||||
'data-done': done.toString(),
|
'data-done': done.toString(),
|
||||||
},
|
},
|
||||||
['span', { class: 'todo-checkbox', contenteditable: 'false' }],
|
['span', { class: 'todo-checkbox', contenteditable: 'false' }],
|
||||||
['div', { class: 'todo-content' }, 0],
|
['div', { class: 'todo-content' }, 0],
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
parseDOM: [{
|
parseDOM: [{
|
||||||
priority: 51,
|
priority: 51,
|
||||||
tag: '[data-type="todo_item"]',
|
tag: '[data-type="todo_item"]',
|
||||||
getAttrs: dom => ({
|
getAttrs: dom => ({
|
||||||
done: dom.getAttribute('data-done') === 'true',
|
done: dom.getAttribute('data-done') === 'true',
|
||||||
}),
|
}),
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keys({ type }) {
|
keys({ type }) {
|
||||||
return {
|
return {
|
||||||
Enter: splitToDefaultListItem(type),
|
Enter: splitToDefaultListItem(type),
|
||||||
'Shift-Tab': liftListItem(type),
|
'Shift-Tab': liftListItem(type),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,30 +3,30 @@ import { wrapInList, wrappingInputRule } from 'tiptap-commands'
|
|||||||
|
|
||||||
export default class TodoList extends Node {
|
export default class TodoList extends Node {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'todo_list'
|
return 'todo_list'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
group: 'block',
|
group: 'block',
|
||||||
content: 'todo_item+',
|
content: 'todo_item+',
|
||||||
toDOM: () => ['ul', { 'data-type': 'todo_list' }, 0],
|
toDOM: () => ['ul', { 'data-type': 'todo_list' }, 0],
|
||||||
parseDOM: [{
|
parseDOM: [{
|
||||||
priority: 51,
|
priority: 51,
|
||||||
tag: '[data-type="todo_list"]',
|
tag: '[data-type="todo_list"]',
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commands({ type }) {
|
commands({ type }) {
|
||||||
return () => wrapInList(type)
|
return () => wrapInList(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
inputRules({ type }) {
|
inputRules({ type }) {
|
||||||
return [
|
return [
|
||||||
wrappingInputRule(/^\s*(\[ \])\s$/, type),
|
wrappingInputRule(/^\s*(\[ \])\s$/, type),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,239 +4,239 @@ import { insertText } from 'tiptap-commands'
|
|||||||
|
|
||||||
// Create a matcher that matches when a specific character is typed. Useful for @mentions and #tags.
|
// Create a matcher that matches when a specific character is typed. Useful for @mentions and #tags.
|
||||||
function triggerCharacter({
|
function triggerCharacter({
|
||||||
char = '@',
|
char = '@',
|
||||||
allowSpaces = false,
|
allowSpaces = false,
|
||||||
startOfLine = false,
|
startOfLine = false,
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
return $position => {
|
return $position => {
|
||||||
// Matching expressions used for later
|
// Matching expressions used for later
|
||||||
const suffix = new RegExp(`\\s${char}$`)
|
const suffix = new RegExp(`\\s${char}$`)
|
||||||
const prefix = startOfLine ? '^' : ''
|
const prefix = startOfLine ? '^' : ''
|
||||||
const regexp = allowSpaces
|
const regexp = allowSpaces
|
||||||
? new RegExp(`${prefix}${char}.*?(?=\\s${char}|$)`, 'gm')
|
? new RegExp(`${prefix}${char}.*?(?=\\s${char}|$)`, 'gm')
|
||||||
: new RegExp(`${prefix}(?:^)?${char}[^\\s${char}]*`, 'gm')
|
: new RegExp(`${prefix}(?:^)?${char}[^\\s${char}]*`, 'gm')
|
||||||
|
|
||||||
// Lookup the boundaries of the current node
|
// Lookup the boundaries of the current node
|
||||||
const textFrom = $position.before()
|
const textFrom = $position.before()
|
||||||
const textTo = $position.end()
|
const textTo = $position.end()
|
||||||
const text = $position.doc.textBetween(textFrom, textTo, '\0', '\0')
|
const text = $position.doc.textBetween(textFrom, textTo, '\0', '\0')
|
||||||
|
|
||||||
let match = regexp.exec(text)
|
let match = regexp.exec(text)
|
||||||
let position
|
let position
|
||||||
while (match !== null) {
|
while (match !== null) {
|
||||||
// JavaScript doesn't have lookbehinds; this hacks a check that first character is " "
|
// JavaScript doesn't have lookbehinds; this hacks a check that first character is " "
|
||||||
// or the line beginning
|
// or the line beginning
|
||||||
const matchPrefix = match.input.slice(Math.max(0, match.index - 1), match.index)
|
const matchPrefix = match.input.slice(Math.max(0, match.index - 1), match.index)
|
||||||
|
|
||||||
if (/^[\s\0]?$/.test(matchPrefix)) {
|
if (/^[\s\0]?$/.test(matchPrefix)) {
|
||||||
// The absolute position of the match in the document
|
// The absolute position of the match in the document
|
||||||
const from = match.index + $position.start()
|
const from = match.index + $position.start()
|
||||||
let to = from + match[0].length
|
let to = from + match[0].length
|
||||||
|
|
||||||
// Edge case handling; if spaces are allowed and we're directly in between
|
// Edge case handling; if spaces are allowed and we're directly in between
|
||||||
// two triggers
|
// two triggers
|
||||||
if (allowSpaces && suffix.test(text.slice(to - 1, to + 1))) {
|
if (allowSpaces && suffix.test(text.slice(to - 1, to + 1))) {
|
||||||
match[0] += ' '
|
match[0] += ' '
|
||||||
to += 1
|
to += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the $position is located within the matched substring, return that range
|
// If the $position is located within the matched substring, return that range
|
||||||
if (from < $position.pos && to >= $position.pos) {
|
if (from < $position.pos && to >= $position.pos) {
|
||||||
position = {
|
position = {
|
||||||
range: {
|
range: {
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
},
|
},
|
||||||
query: match[0].slice(char.length),
|
query: match[0].slice(char.length),
|
||||||
text: match[0],
|
text: match[0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
match = regexp.exec(text)
|
match = regexp.exec(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
return position
|
return position
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SuggestionsPlugin({
|
export default function SuggestionsPlugin({
|
||||||
matcher = {
|
matcher = {
|
||||||
char: '@',
|
char: '@',
|
||||||
allowSpaces: false,
|
allowSpaces: false,
|
||||||
startOfLine: false,
|
startOfLine: false,
|
||||||
},
|
},
|
||||||
appendText = null,
|
appendText = null,
|
||||||
suggestionClass = 'suggestion',
|
suggestionClass = 'suggestion',
|
||||||
command = () => false,
|
command = () => false,
|
||||||
items = [],
|
items = [],
|
||||||
onEnter = () => false,
|
onEnter = () => false,
|
||||||
onChange = () => false,
|
onChange = () => false,
|
||||||
onExit = () => false,
|
onExit = () => false,
|
||||||
onKeyDown = () => false,
|
onKeyDown = () => false,
|
||||||
onFilter = (searchItems, query) => {
|
onFilter = (searchItems, query) => {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return searchItems
|
return searchItems
|
||||||
}
|
}
|
||||||
|
|
||||||
return searchItems
|
return searchItems
|
||||||
.filter(item => JSON.stringify(item).toLowerCase().includes(query.toLowerCase()))
|
.filter(item => JSON.stringify(item).toLowerCase().includes(query.toLowerCase()))
|
||||||
},
|
},
|
||||||
}) {
|
}) {
|
||||||
return new Plugin({
|
return new Plugin({
|
||||||
key: new PluginKey('suggestions'),
|
key: new PluginKey('suggestions'),
|
||||||
|
|
||||||
view() {
|
view() {
|
||||||
return {
|
return {
|
||||||
update: (view, prevState) => {
|
update: (view, prevState) => {
|
||||||
const prev = this.key.getState(prevState)
|
const prev = this.key.getState(prevState)
|
||||||
const next = this.key.getState(view.state)
|
const next = this.key.getState(view.state)
|
||||||
|
|
||||||
// See how the state changed
|
// See how the state changed
|
||||||
const moved = prev.active && next.active && prev.range.from !== next.range.from
|
const moved = prev.active && next.active && prev.range.from !== next.range.from
|
||||||
const started = !prev.active && next.active
|
const started = !prev.active && next.active
|
||||||
const stopped = prev.active && !next.active
|
const stopped = prev.active && !next.active
|
||||||
const changed = !started && !stopped && prev.query !== next.query
|
const changed = !started && !stopped && prev.query !== next.query
|
||||||
const handleStart = started || moved
|
const handleStart = started || moved
|
||||||
const handleChange = changed && !moved
|
const handleChange = changed && !moved
|
||||||
const handleExit = stopped || moved
|
const handleExit = stopped || moved
|
||||||
|
|
||||||
// Cancel when suggestion isn't active
|
// Cancel when suggestion isn't active
|
||||||
if (!handleStart && !handleChange && !handleExit) {
|
if (!handleStart && !handleChange && !handleExit) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = handleExit ? prev : next
|
const state = handleExit ? prev : next
|
||||||
const decorationNode = document.querySelector(`[data-decoration-id="${state.decorationId}"]`)
|
const decorationNode = document.querySelector(`[data-decoration-id="${state.decorationId}"]`)
|
||||||
|
|
||||||
// build a virtual node for popper.js or tippy.js
|
// build a virtual node for popper.js or tippy.js
|
||||||
// this can be used for building popups without a DOM node
|
// this can be used for building popups without a DOM node
|
||||||
const virtualNode = decorationNode ? {
|
const virtualNode = decorationNode ? {
|
||||||
getBoundingClientRect() {
|
getBoundingClientRect() {
|
||||||
return decorationNode.getBoundingClientRect()
|
return decorationNode.getBoundingClientRect()
|
||||||
},
|
},
|
||||||
clientWidth: decorationNode.clientWidth,
|
clientWidth: decorationNode.clientWidth,
|
||||||
clientHeight: decorationNode.clientHeight,
|
clientHeight: decorationNode.clientHeight,
|
||||||
} : null
|
} : null
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
view,
|
view,
|
||||||
range: state.range,
|
range: state.range,
|
||||||
query: state.query,
|
query: state.query,
|
||||||
text: state.text,
|
text: state.text,
|
||||||
decorationNode,
|
decorationNode,
|
||||||
virtualNode,
|
virtualNode,
|
||||||
items: onFilter(Array.isArray(items) ? items : items(), state.query),
|
items: onFilter(Array.isArray(items) ? items : items(), state.query),
|
||||||
command: ({ range, attrs }) => {
|
command: ({ range, attrs }) => {
|
||||||
command({
|
command({
|
||||||
range,
|
range,
|
||||||
attrs,
|
attrs,
|
||||||
schema: view.state.schema,
|
schema: view.state.schema,
|
||||||
})(view.state, view.dispatch, view)
|
})(view.state, view.dispatch, view)
|
||||||
|
|
||||||
if (appendText) {
|
if (appendText) {
|
||||||
insertText(appendText)(view.state, view.dispatch, view)
|
insertText(appendText)(view.state, view.dispatch, view)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger the hooks when necessary
|
// Trigger the hooks when necessary
|
||||||
if (handleExit) {
|
if (handleExit) {
|
||||||
onExit(props)
|
onExit(props)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleChange) {
|
if (handleChange) {
|
||||||
onChange(props)
|
onChange(props)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleStart) {
|
if (handleStart) {
|
||||||
onEnter(props)
|
onEnter(props)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
state: {
|
state: {
|
||||||
|
|
||||||
// Initialize the plugin's internal state.
|
// Initialize the plugin's internal state.
|
||||||
init() {
|
init() {
|
||||||
return {
|
return {
|
||||||
active: false,
|
active: false,
|
||||||
range: {},
|
range: {},
|
||||||
query: null,
|
query: null,
|
||||||
text: null,
|
text: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Apply changes to the plugin state from a view transaction.
|
// Apply changes to the plugin state from a view transaction.
|
||||||
apply(tr, prev) {
|
apply(tr, prev) {
|
||||||
const { selection } = tr
|
const { selection } = tr
|
||||||
const next = { ...prev }
|
const next = { ...prev }
|
||||||
|
|
||||||
// We can only be suggesting if there is no selection
|
// We can only be suggesting if there is no selection
|
||||||
if (selection.from === selection.to) {
|
if (selection.from === selection.to) {
|
||||||
// Reset active state if we just left the previous suggestion range
|
// Reset active state if we just left the previous suggestion range
|
||||||
if (selection.from < prev.range.from || selection.from > prev.range.to) {
|
if (selection.from < prev.range.from || selection.from > prev.range.to) {
|
||||||
next.active = false
|
next.active = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to match against where our cursor currently is
|
// Try to match against where our cursor currently is
|
||||||
const $position = selection.$from
|
const $position = selection.$from
|
||||||
const match = triggerCharacter(matcher)($position)
|
const match = triggerCharacter(matcher)($position)
|
||||||
const decorationId = (Math.random() + 1).toString(36).substr(2, 5)
|
const decorationId = (Math.random() + 1).toString(36).substr(2, 5)
|
||||||
|
|
||||||
// If we found a match, update the current state to show it
|
// If we found a match, update the current state to show it
|
||||||
if (match) {
|
if (match) {
|
||||||
next.active = true
|
next.active = true
|
||||||
next.decorationId = prev.decorationId ? prev.decorationId : decorationId
|
next.decorationId = prev.decorationId ? prev.decorationId : decorationId
|
||||||
next.range = match.range
|
next.range = match.range
|
||||||
next.query = match.query
|
next.query = match.query
|
||||||
next.text = match.text
|
next.text = match.text
|
||||||
} else {
|
} else {
|
||||||
next.active = false
|
next.active = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
next.active = false
|
next.active = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure to empty the range if suggestion is inactive
|
// Make sure to empty the range if suggestion is inactive
|
||||||
if (!next.active) {
|
if (!next.active) {
|
||||||
next.decorationId = null
|
next.decorationId = null
|
||||||
next.range = {}
|
next.range = {}
|
||||||
next.query = null
|
next.query = null
|
||||||
next.text = null
|
next.text = null
|
||||||
}
|
}
|
||||||
|
|
||||||
return next
|
return next
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
|
||||||
// Call the keydown hook if suggestion is active.
|
// Call the keydown hook if suggestion is active.
|
||||||
handleKeyDown(view, event) {
|
handleKeyDown(view, event) {
|
||||||
const { active, range } = this.getState(view.state)
|
const { active, range } = this.getState(view.state)
|
||||||
|
|
||||||
if (!active) return false
|
if (!active) return false
|
||||||
|
|
||||||
return onKeyDown({ view, event, range })
|
return onKeyDown({ view, event, range })
|
||||||
},
|
},
|
||||||
|
|
||||||
// Setup decorator on the currently active suggestion.
|
// Setup decorator on the currently active suggestion.
|
||||||
decorations(editorState) {
|
decorations(editorState) {
|
||||||
const { active, range, decorationId } = this.getState(editorState)
|
const { active, range, decorationId } = this.getState(editorState)
|
||||||
|
|
||||||
if (!active) return null
|
if (!active) return null
|
||||||
|
|
||||||
return DecorationSet.create(editorState.doc, [
|
return DecorationSet.create(editorState.doc, [
|
||||||
Decoration.inline(range.from, range.to, {
|
Decoration.inline(range.from, range.to, {
|
||||||
nodeName: 'span',
|
nodeName: 'span',
|
||||||
class: suggestionClass,
|
class: suggestionClass,
|
||||||
'data-decoration-id': decorationId,
|
'data-decoration-id': decorationId,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
export default function (state, type) {
|
export default function (state, type) {
|
||||||
const { from, to } = state.selection
|
const { from, to } = state.selection
|
||||||
let marks = []
|
let marks = []
|
||||||
|
|
||||||
state.doc.nodesBetween(from, to, node => {
|
state.doc.nodesBetween(from, to, node => {
|
||||||
marks = [...marks, ...node.marks]
|
marks = [...marks, ...node.marks]
|
||||||
})
|
})
|
||||||
|
|
||||||
const mark = marks.find(markItem => markItem.type.name === type.name)
|
const mark = marks.find(markItem => markItem.type.name === type.name)
|
||||||
|
|
||||||
if (mark) {
|
if (mark) {
|
||||||
return mark.attrs
|
return mark.attrs
|
||||||
}
|
}
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
export default function (state, type) {
|
export default function (state, type) {
|
||||||
const {
|
const {
|
||||||
from,
|
from,
|
||||||
$from,
|
$from,
|
||||||
to,
|
to,
|
||||||
empty,
|
empty,
|
||||||
} = state.selection
|
} = state.selection
|
||||||
|
|
||||||
if (empty) {
|
if (empty) {
|
||||||
return !!type.isInSet(state.storedMarks || $from.marks())
|
return !!type.isInSet(state.storedMarks || $from.marks())
|
||||||
}
|
}
|
||||||
|
|
||||||
return !!state.doc.rangeHasMark(from, to, type)
|
return !!state.doc.rangeHasMark(from, to, type)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { findParentNode } from 'prosemirror-utils'
|
import { findParentNode } from 'prosemirror-utils'
|
||||||
|
|
||||||
export default function (state, type, attrs = {}) {
|
export default function (state, type, attrs = {}) {
|
||||||
const predicate = node => node.type === type
|
const predicate = node => node.type === type
|
||||||
const parent = findParentNode(predicate)(state.selection)
|
const parent = findParentNode(predicate)(state.selection)
|
||||||
|
|
||||||
if (!Object.keys(attrs).length || !parent) {
|
if (!Object.keys(attrs).length || !parent) {
|
||||||
return !!parent
|
return !!parent
|
||||||
}
|
}
|
||||||
|
|
||||||
return parent.node.hasMarkup(type, attrs)
|
return parent.node.hasMarkup(type, attrs)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
editor: {
|
editor: {
|
||||||
default: null,
|
default: null,
|
||||||
type: Object,
|
type: Object,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'editor.element': {
|
'editor.element': {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler(element) {
|
handler(element) {
|
||||||
if (element) {
|
if (element) {
|
||||||
this.$nextTick(() => this.$el.append(element.firstChild))
|
this.$nextTick(() => this.$el.append(element.firstChild))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
render(createElement) {
|
render(createElement) {
|
||||||
return createElement('div')
|
return createElement('div')
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,50 @@
|
|||||||
import FloatingMenu from '../Utils/FloatingMenu'
|
import FloatingMenu from '../Utils/FloatingMenu'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
editor: {
|
editor: {
|
||||||
default: null,
|
default: null,
|
||||||
type: Object,
|
type: Object,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
menu: {
|
menu: {
|
||||||
isActive: false,
|
isActive: false,
|
||||||
left: 0,
|
left: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
editor: {
|
editor: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler(editor) {
|
handler(editor) {
|
||||||
if (editor) {
|
if (editor) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
editor.registerPlugin(FloatingMenu({
|
editor.registerPlugin(FloatingMenu({
|
||||||
element: this.$el,
|
element: this.$el,
|
||||||
onUpdate: menu => {
|
onUpdate: menu => {
|
||||||
this.menu = menu
|
this.menu = menu
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
if (!this.editor) {
|
if (!this.editor) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.$scopedSlots.default({
|
return this.$scopedSlots.default({
|
||||||
focused: this.editor.view.focused,
|
focused: this.editor.view.focused,
|
||||||
focus: this.editor.focus,
|
focus: this.editor.focus,
|
||||||
commands: this.editor.commands,
|
commands: this.editor.commands,
|
||||||
isActive: this.editor.isActive.bind(this.editor),
|
isActive: this.editor.isActive.bind(this.editor),
|
||||||
markAttrs: this.editor.markAttrs.bind(this.editor),
|
markAttrs: this.editor.markAttrs.bind(this.editor),
|
||||||
menu: this.menu,
|
menu: this.menu,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
editor: {
|
editor: {
|
||||||
default: null,
|
default: null,
|
||||||
type: Object,
|
type: Object,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
if (!this.editor) {
|
if (!this.editor) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.$scopedSlots.default({
|
return this.$scopedSlots.default({
|
||||||
focused: this.editor.view.focused,
|
focused: this.editor.view.focused,
|
||||||
focus: this.editor.focus,
|
focus: this.editor.focus,
|
||||||
commands: this.editor.commands,
|
commands: this.editor.commands,
|
||||||
isActive: this.editor.isActive.bind(this.editor),
|
isActive: this.editor.isActive.bind(this.editor),
|
||||||
markAttrs: this.editor.markAttrs.bind(this.editor),
|
markAttrs: this.editor.markAttrs.bind(this.editor),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,50 @@
|
|||||||
import MenuBubble from '../Utils/MenuBubble'
|
import MenuBubble from '../Utils/MenuBubble'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
editor: {
|
editor: {
|
||||||
default: null,
|
default: null,
|
||||||
type: Object,
|
type: Object,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
menu: {
|
menu: {
|
||||||
isActive: false,
|
isActive: false,
|
||||||
left: 0,
|
left: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
editor: {
|
editor: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler(editor) {
|
handler(editor) {
|
||||||
if (editor) {
|
if (editor) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
editor.registerPlugin(MenuBubble({
|
editor.registerPlugin(MenuBubble({
|
||||||
element: this.$el,
|
element: this.$el,
|
||||||
onUpdate: menu => {
|
onUpdate: menu => {
|
||||||
this.menu = menu
|
this.menu = menu
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
render() {
|
render() {
|
||||||
if (!this.editor) {
|
if (!this.editor) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.$scopedSlots.default({
|
return this.$scopedSlots.default({
|
||||||
focused: this.editor.view.focused,
|
focused: this.editor.view.focused,
|
||||||
focus: this.editor.focus,
|
focus: this.editor.focus,
|
||||||
commands: this.editor.commands,
|
commands: this.editor.commands,
|
||||||
isActive: this.editor.isActive.bind(this.editor),
|
isActive: this.editor.isActive.bind(this.editor),
|
||||||
markAttrs: this.editor.markAttrs.bind(this.editor),
|
markAttrs: this.editor.markAttrs.bind(this.editor),
|
||||||
menu: this.menu,
|
menu: this.menu,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import Node from '../Utils/Node'
|
|||||||
|
|
||||||
export default class Doc extends Node {
|
export default class Doc extends Node {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'doc'
|
return 'doc'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
content: 'block+',
|
content: 'block+',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,24 +3,24 @@ import Node from '../Utils/Node'
|
|||||||
|
|
||||||
export default class Paragraph extends Node {
|
export default class Paragraph extends Node {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'paragraph'
|
return 'paragraph'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
content: 'inline*',
|
content: 'inline*',
|
||||||
group: 'block',
|
group: 'block',
|
||||||
draggable: false,
|
draggable: false,
|
||||||
parseDOM: [{
|
parseDOM: [{
|
||||||
tag: 'p',
|
tag: 'p',
|
||||||
}],
|
}],
|
||||||
toDOM: () => ['p', 0],
|
toDOM: () => ['p', 0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
commands({ type }) {
|
commands({ type }) {
|
||||||
return () => setBlockType(type)
|
return () => setBlockType(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import Node from '../Utils/Node'
|
|||||||
|
|
||||||
export default class Text extends Node {
|
export default class Text extends Node {
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'text'
|
return 'text'
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return {
|
return {
|
||||||
group: 'inline',
|
group: 'inline',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Paragraph from './Paragraph'
|
|||||||
import Text from './Text'
|
import Text from './Text'
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
new Doc(),
|
new Doc(),
|
||||||
new Text(),
|
new Text(),
|
||||||
new Paragraph(),
|
new Paragraph(),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,92 +1,92 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
|
||||||
export default class ComponentView {
|
export default class ComponentView {
|
||||||
constructor(component, {
|
constructor(component, {
|
||||||
parent,
|
parent,
|
||||||
node,
|
node,
|
||||||
view,
|
view,
|
||||||
getPos,
|
getPos,
|
||||||
decorations,
|
decorations,
|
||||||
editable,
|
editable,
|
||||||
}) {
|
}) {
|
||||||
this.parent = parent
|
this.parent = parent
|
||||||
this.component = component
|
this.component = component
|
||||||
this.node = node
|
this.node = node
|
||||||
this.view = view
|
this.view = view
|
||||||
this.getPos = getPos
|
this.getPos = getPos
|
||||||
this.decorations = decorations
|
this.decorations = decorations
|
||||||
this.editable = editable
|
this.editable = editable
|
||||||
|
|
||||||
this.dom = this.createDOM()
|
this.dom = this.createDOM()
|
||||||
this.contentDOM = this.vm.$refs.content
|
this.contentDOM = this.vm.$refs.content
|
||||||
}
|
}
|
||||||
|
|
||||||
createDOM() {
|
createDOM() {
|
||||||
const Component = Vue.extend(this.component)
|
const Component = Vue.extend(this.component)
|
||||||
this.vm = new Component({
|
this.vm = new Component({
|
||||||
parent: this.parent,
|
parent: this.parent,
|
||||||
propsData: {
|
propsData: {
|
||||||
node: this.node,
|
node: this.node,
|
||||||
view: this.view,
|
view: this.view,
|
||||||
getPos: this.getPos,
|
getPos: this.getPos,
|
||||||
decorations: this.decorations,
|
decorations: this.decorations,
|
||||||
editable: this.editable,
|
editable: this.editable,
|
||||||
updateAttrs: attrs => this.updateAttrs(attrs),
|
updateAttrs: attrs => this.updateAttrs(attrs),
|
||||||
updateContent: content => this.updateContent(content),
|
updateContent: content => this.updateContent(content),
|
||||||
},
|
},
|
||||||
}).$mount()
|
}).$mount()
|
||||||
return this.vm.$el
|
return this.vm.$el
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAttrs(attrs) {
|
updateAttrs(attrs) {
|
||||||
if (!this.editable) {
|
if (!this.editable) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const transaction = this.view.state.tr.setNodeMarkup(this.getPos(), null, {
|
const transaction = this.view.state.tr.setNodeMarkup(this.getPos(), null, {
|
||||||
...this.node.attrs,
|
...this.node.attrs,
|
||||||
...attrs,
|
...attrs,
|
||||||
})
|
})
|
||||||
this.view.dispatch(transaction)
|
this.view.dispatch(transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateContent(content) {
|
updateContent(content) {
|
||||||
if (!this.editable) {
|
if (!this.editable) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const transaction = this.view.state.tr.setNodeMarkup(this.getPos(), this.node.type, { content })
|
const transaction = this.view.state.tr.setNodeMarkup(this.getPos(), this.node.type, { content })
|
||||||
this.view.dispatch(transaction)
|
this.view.dispatch(transaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
ignoreMutation() {
|
ignoreMutation() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
stopEvent(event) {
|
stopEvent(event) {
|
||||||
// TODO: find a way to pass full extensions to ComponentView
|
// TODO: find a way to pass full extensions to ComponentView
|
||||||
// so we could check for schema.draggable
|
// so we could check for schema.draggable
|
||||||
// for now we're allowing all drag events for node views
|
// for now we're allowing all drag events for node views
|
||||||
return !/drag/.test(event.type)
|
return !/drag/.test(event.type)
|
||||||
}
|
}
|
||||||
|
|
||||||
update(node, decorations) {
|
update(node, decorations) {
|
||||||
if (node.type !== this.node.type) {
|
if (node.type !== this.node.type) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node === this.node && this.decorations === decorations) {
|
if (node === this.node && this.decorations === decorations) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
this.node = node
|
this.node = node
|
||||||
this.decorations = decorations
|
this.decorations = decorations
|
||||||
this.vm._props.node = node
|
this.vm._props.node = node
|
||||||
this.vm._props.decorations = decorations
|
this.vm._props.decorations = decorations
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.vm.$destroy()
|
this.vm.$destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,278 +9,278 @@ import { inputRules } from 'prosemirror-inputrules'
|
|||||||
import { markIsActive, nodeIsActive, getMarkAttrs } from 'tiptap-utils'
|
import { markIsActive, nodeIsActive, getMarkAttrs } from 'tiptap-utils'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ExtensionManager,
|
ExtensionManager,
|
||||||
initNodeViews,
|
initNodeViews,
|
||||||
builtInKeymap,
|
builtInKeymap,
|
||||||
} from '.'
|
} from '.'
|
||||||
|
|
||||||
import builtInNodes from '../Nodes'
|
import builtInNodes from '../Nodes'
|
||||||
|
|
||||||
export default class Editor {
|
export default class Editor {
|
||||||
|
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
this.setOptions(options)
|
this.setOptions(options)
|
||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
setOptions(options) {
|
setOptions(options) {
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
editable: true,
|
editable: true,
|
||||||
content: '',
|
content: '',
|
||||||
onUpdate: () => {},
|
onUpdate: () => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
this.options = {
|
this.options = {
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
...options,
|
...options,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.bus = new Vue()
|
this.bus = new Vue()
|
||||||
this.element = document.createElement('div')
|
this.element = document.createElement('div')
|
||||||
this.extensions = this.createExtensions()
|
this.extensions = this.createExtensions()
|
||||||
this.nodes = this.createNodes()
|
this.nodes = this.createNodes()
|
||||||
this.marks = this.createMarks()
|
this.marks = this.createMarks()
|
||||||
this.views = this.createViews()
|
this.views = this.createViews()
|
||||||
this.schema = this.createSchema()
|
this.schema = this.createSchema()
|
||||||
this.plugins = this.createPlugins()
|
this.plugins = this.createPlugins()
|
||||||
this.keymaps = this.createKeymaps()
|
this.keymaps = this.createKeymaps()
|
||||||
this.inputRules = this.createInputRules()
|
this.inputRules = this.createInputRules()
|
||||||
this.state = this.createState()
|
this.state = this.createState()
|
||||||
this.view = this.createView()
|
this.view = this.createView()
|
||||||
this.commands = this.createCommands()
|
this.commands = this.createCommands()
|
||||||
this.getActiveNodesAndMarks()
|
this.getActiveNodesAndMarks()
|
||||||
this.emit('init')
|
this.emit('init')
|
||||||
}
|
}
|
||||||
|
|
||||||
createExtensions() {
|
createExtensions() {
|
||||||
return new ExtensionManager([
|
return new ExtensionManager([
|
||||||
...builtInNodes,
|
...builtInNodes,
|
||||||
...this.options.extensions,
|
...this.options.extensions,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
createPlugins() {
|
createPlugins() {
|
||||||
return this.extensions.plugins
|
return this.extensions.plugins
|
||||||
}
|
}
|
||||||
|
|
||||||
createKeymaps() {
|
createKeymaps() {
|
||||||
return this.extensions.keymaps({
|
return this.extensions.keymaps({
|
||||||
schema: this.schema,
|
schema: this.schema,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
createInputRules() {
|
createInputRules() {
|
||||||
return this.extensions.inputRules({
|
return this.extensions.inputRules({
|
||||||
schema: this.schema,
|
schema: this.schema,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
createCommands() {
|
createCommands() {
|
||||||
return this.extensions.commands({
|
return this.extensions.commands({
|
||||||
schema: this.schema,
|
schema: this.schema,
|
||||||
view: this.view,
|
view: this.view,
|
||||||
editable: this.options.editable,
|
editable: this.options.editable,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
createNodes() {
|
createNodes() {
|
||||||
return this.extensions.nodes
|
return this.extensions.nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
createMarks() {
|
createMarks() {
|
||||||
return this.extensions.marks
|
return this.extensions.marks
|
||||||
}
|
}
|
||||||
|
|
||||||
createViews() {
|
createViews() {
|
||||||
return this.extensions.views
|
return this.extensions.views
|
||||||
}
|
}
|
||||||
|
|
||||||
createSchema() {
|
createSchema() {
|
||||||
return new Schema({
|
return new Schema({
|
||||||
nodes: this.nodes,
|
nodes: this.nodes,
|
||||||
marks: this.marks,
|
marks: this.marks,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
createState() {
|
createState() {
|
||||||
return EditorState.create({
|
return EditorState.create({
|
||||||
schema: this.schema,
|
schema: this.schema,
|
||||||
doc: this.createDocument(this.options.content),
|
doc: this.createDocument(this.options.content),
|
||||||
plugins: [
|
plugins: [
|
||||||
...this.plugins,
|
...this.plugins,
|
||||||
inputRules({
|
inputRules({
|
||||||
rules: this.inputRules,
|
rules: this.inputRules,
|
||||||
}),
|
}),
|
||||||
...this.keymaps,
|
...this.keymaps,
|
||||||
keymap(builtInKeymap),
|
keymap(builtInKeymap),
|
||||||
keymap(baseKeymap),
|
keymap(baseKeymap),
|
||||||
gapCursor(),
|
gapCursor(),
|
||||||
new Plugin({
|
new Plugin({
|
||||||
props: {
|
props: {
|
||||||
editable: () => this.options.editable,
|
editable: () => this.options.editable,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
createDocument(content) {
|
createDocument(content) {
|
||||||
if (typeof content === 'object') {
|
if (typeof content === 'object') {
|
||||||
return this.schema.nodeFromJSON(content)
|
return this.schema.nodeFromJSON(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof content === 'string') {
|
if (typeof content === 'string') {
|
||||||
const element = document.createElement('div')
|
const element = document.createElement('div')
|
||||||
element.innerHTML = content.trim()
|
element.innerHTML = content.trim()
|
||||||
|
|
||||||
return DOMParser.fromSchema(this.schema).parse(element)
|
return DOMParser.fromSchema(this.schema).parse(element)
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
createView() {
|
createView() {
|
||||||
const view = new EditorView(this.element, {
|
const view = new EditorView(this.element, {
|
||||||
state: this.state,
|
state: this.state,
|
||||||
dispatchTransaction: this.dispatchTransaction.bind(this),
|
dispatchTransaction: this.dispatchTransaction.bind(this),
|
||||||
nodeViews: initNodeViews({
|
nodeViews: initNodeViews({
|
||||||
nodes: this.views,
|
nodes: this.views,
|
||||||
editable: this.options.editable,
|
editable: this.options.editable,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
view.dom.style.whiteSpace = 'pre-wrap'
|
view.dom.style.whiteSpace = 'pre-wrap'
|
||||||
|
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchTransaction(transaction) {
|
dispatchTransaction(transaction) {
|
||||||
this.state = this.state.apply(transaction)
|
this.state = this.state.apply(transaction)
|
||||||
this.view.updateState(this.state)
|
this.view.updateState(this.state)
|
||||||
this.getActiveNodesAndMarks()
|
this.getActiveNodesAndMarks()
|
||||||
|
|
||||||
if (!transaction.docChanged) {
|
if (!transaction.docChanged) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emitUpdate()
|
this.emitUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
emitUpdate() {
|
emitUpdate() {
|
||||||
this.options.onUpdate({
|
this.options.onUpdate({
|
||||||
getHTML: this.getHTML.bind(this),
|
getHTML: this.getHTML.bind(this),
|
||||||
getJSON: this.getJSON.bind(this),
|
getJSON: this.getJSON.bind(this),
|
||||||
state: this.state,
|
state: this.state,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getHTML() {
|
getHTML() {
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
const fragment = DOMSerializer
|
const fragment = DOMSerializer
|
||||||
.fromSchema(this.schema)
|
.fromSchema(this.schema)
|
||||||
.serializeFragment(this.state.doc.content)
|
.serializeFragment(this.state.doc.content)
|
||||||
|
|
||||||
div.appendChild(fragment)
|
div.appendChild(fragment)
|
||||||
|
|
||||||
return div.innerHTML
|
return div.innerHTML
|
||||||
}
|
}
|
||||||
|
|
||||||
getJSON() {
|
getJSON() {
|
||||||
return this.state.doc.toJSON()
|
return this.state.doc.toJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
setContent(content = {}, emitUpdate = false) {
|
setContent(content = {}, emitUpdate = false) {
|
||||||
this.state = EditorState.create({
|
this.state = EditorState.create({
|
||||||
schema: this.state.schema,
|
schema: this.state.schema,
|
||||||
doc: this.createDocument(content),
|
doc: this.createDocument(content),
|
||||||
plugins: this.state.plugins,
|
plugins: this.state.plugins,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.view.updateState(this.state)
|
this.view.updateState(this.state)
|
||||||
|
|
||||||
if (emitUpdate) {
|
if (emitUpdate) {
|
||||||
this.emitUpdate()
|
this.emitUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearContent(emitUpdate = false) {
|
clearContent(emitUpdate = false) {
|
||||||
this.setContent({
|
this.setContent({
|
||||||
type: 'doc',
|
type: 'doc',
|
||||||
content: [{
|
content: [{
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
}],
|
}],
|
||||||
}, emitUpdate)
|
}, emitUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveNodesAndMarks() {
|
getActiveNodesAndMarks() {
|
||||||
this.activeMarks = Object
|
this.activeMarks = Object
|
||||||
.entries(this.schema.marks)
|
.entries(this.schema.marks)
|
||||||
.reduce((marks, [name, mark]) => ({
|
.reduce((marks, [name, mark]) => ({
|
||||||
...marks,
|
...marks,
|
||||||
[name]: (attrs = {}) => markIsActive(this.state, mark, attrs),
|
[name]: (attrs = {}) => markIsActive(this.state, mark, attrs),
|
||||||
}), {})
|
}), {})
|
||||||
|
|
||||||
this.activeMarkAttrs = Object
|
this.activeMarkAttrs = Object
|
||||||
.entries(this.schema.marks)
|
.entries(this.schema.marks)
|
||||||
.reduce((marks, [name, mark]) => ({
|
.reduce((marks, [name, mark]) => ({
|
||||||
...marks,
|
...marks,
|
||||||
[name]: getMarkAttrs(this.state, mark),
|
[name]: getMarkAttrs(this.state, mark),
|
||||||
}), {})
|
}), {})
|
||||||
|
|
||||||
this.activeNodes = Object
|
this.activeNodes = Object
|
||||||
.entries(this.schema.nodes)
|
.entries(this.schema.nodes)
|
||||||
.reduce((nodes, [name, node]) => ({
|
.reduce((nodes, [name, node]) => ({
|
||||||
...nodes,
|
...nodes,
|
||||||
[name]: (attrs = {}) => nodeIsActive(this.state, node, attrs),
|
[name]: (attrs = {}) => nodeIsActive(this.state, node, attrs),
|
||||||
}), {})
|
}), {})
|
||||||
}
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
this.view.focus()
|
this.view.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(event, ...data) {
|
emit(event, ...data) {
|
||||||
this.bus.$emit(event, ...data)
|
this.bus.$emit(event, ...data)
|
||||||
}
|
}
|
||||||
|
|
||||||
on(event, callback) {
|
on(event, callback) {
|
||||||
this.bus.$on(event, callback)
|
this.bus.$on(event, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
registerPlugin(plugin = null) {
|
registerPlugin(plugin = null) {
|
||||||
if (plugin) {
|
if (plugin) {
|
||||||
this.state = this.state.reconfigure({
|
this.state = this.state.reconfigure({
|
||||||
plugins: this.state.plugins.concat([plugin]),
|
plugins: this.state.plugins.concat([plugin]),
|
||||||
})
|
})
|
||||||
this.view.updateState(this.state)
|
this.view.updateState(this.state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
markAttrs(type = null) {
|
markAttrs(type = null) {
|
||||||
return this.activeMarkAttrs[type]
|
return this.activeMarkAttrs[type]
|
||||||
}
|
}
|
||||||
|
|
||||||
isActive(type = null, attrs = {}) {
|
isActive(type = null, attrs = {}) {
|
||||||
const types = {
|
const types = {
|
||||||
...this.activeMarks,
|
...this.activeMarks,
|
||||||
...this.activeNodes,
|
...this.activeNodes,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!types[type]) {
|
if (!types[type]) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return types[type](attrs)
|
return types[type](attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.emit('destroy')
|
this.emit('destroy')
|
||||||
|
|
||||||
if (this.view) {
|
if (this.view) {
|
||||||
this.view.destroy()
|
this.view.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,148 +2,148 @@ import { keymap } from 'prosemirror-keymap'
|
|||||||
|
|
||||||
export default class ExtensionManager {
|
export default class ExtensionManager {
|
||||||
|
|
||||||
constructor(extensions = []) {
|
constructor(extensions = []) {
|
||||||
this.extensions = extensions
|
this.extensions = extensions
|
||||||
}
|
}
|
||||||
|
|
||||||
get nodes() {
|
get nodes() {
|
||||||
return this.extensions
|
return this.extensions
|
||||||
.filter(extension => extension.type === 'node')
|
.filter(extension => extension.type === 'node')
|
||||||
.reduce((nodes, { name, schema }) => ({
|
.reduce((nodes, { name, schema }) => ({
|
||||||
...nodes,
|
...nodes,
|
||||||
[name]: schema,
|
[name]: schema,
|
||||||
}), {})
|
}), {})
|
||||||
}
|
}
|
||||||
|
|
||||||
get marks() {
|
get marks() {
|
||||||
return this.extensions
|
return this.extensions
|
||||||
.filter(extension => extension.type === 'mark')
|
.filter(extension => extension.type === 'mark')
|
||||||
.reduce((marks, { name, schema }) => ({
|
.reduce((marks, { name, schema }) => ({
|
||||||
...marks,
|
...marks,
|
||||||
[name]: schema,
|
[name]: schema,
|
||||||
}), {})
|
}), {})
|
||||||
}
|
}
|
||||||
|
|
||||||
get plugins() {
|
get plugins() {
|
||||||
return this.extensions
|
return this.extensions
|
||||||
.filter(extension => extension.plugins)
|
.filter(extension => extension.plugins)
|
||||||
.reduce((allPlugins, { plugins }) => ([
|
.reduce((allPlugins, { plugins }) => ([
|
||||||
...allPlugins,
|
...allPlugins,
|
||||||
...plugins,
|
...plugins,
|
||||||
]), [])
|
]), [])
|
||||||
}
|
}
|
||||||
|
|
||||||
get views() {
|
get views() {
|
||||||
return this.extensions
|
return this.extensions
|
||||||
.filter(extension => ['node', 'mark'].includes(extension.type))
|
.filter(extension => ['node', 'mark'].includes(extension.type))
|
||||||
.filter(extension => extension.view)
|
.filter(extension => extension.view)
|
||||||
.reduce((views, { name, view }) => ({
|
.reduce((views, { name, view }) => ({
|
||||||
...views,
|
...views,
|
||||||
[name]: view,
|
[name]: view,
|
||||||
}), {})
|
}), {})
|
||||||
}
|
}
|
||||||
|
|
||||||
keymaps({ schema }) {
|
keymaps({ schema }) {
|
||||||
const extensionKeymaps = this.extensions
|
const extensionKeymaps = this.extensions
|
||||||
.filter(extension => ['extension'].includes(extension.type))
|
.filter(extension => ['extension'].includes(extension.type))
|
||||||
.filter(extension => extension.keys)
|
.filter(extension => extension.keys)
|
||||||
.map(extension => extension.keys({ schema }))
|
.map(extension => extension.keys({ schema }))
|
||||||
|
|
||||||
const nodeMarkKeymaps = this.extensions
|
const nodeMarkKeymaps = this.extensions
|
||||||
.filter(extension => ['node', 'mark'].includes(extension.type))
|
.filter(extension => ['node', 'mark'].includes(extension.type))
|
||||||
.filter(extension => extension.keys)
|
.filter(extension => extension.keys)
|
||||||
.map(extension => extension.keys({
|
.map(extension => extension.keys({
|
||||||
type: schema[`${extension.type}s`][extension.name],
|
type: schema[`${extension.type}s`][extension.name],
|
||||||
schema,
|
schema,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...extensionKeymaps,
|
...extensionKeymaps,
|
||||||
...nodeMarkKeymaps,
|
...nodeMarkKeymaps,
|
||||||
].map(keys => keymap(keys))
|
].map(keys => keymap(keys))
|
||||||
}
|
}
|
||||||
|
|
||||||
inputRules({ schema }) {
|
inputRules({ schema }) {
|
||||||
const extensionInputRules = this.extensions
|
const extensionInputRules = this.extensions
|
||||||
.filter(extension => ['extension'].includes(extension.type))
|
.filter(extension => ['extension'].includes(extension.type))
|
||||||
.filter(extension => extension.inputRules)
|
.filter(extension => extension.inputRules)
|
||||||
.map(extension => extension.inputRules({ schema }))
|
.map(extension => extension.inputRules({ schema }))
|
||||||
|
|
||||||
const nodeMarkInputRules = this.extensions
|
const nodeMarkInputRules = this.extensions
|
||||||
.filter(extension => ['node', 'mark'].includes(extension.type))
|
.filter(extension => ['node', 'mark'].includes(extension.type))
|
||||||
.filter(extension => extension.inputRules)
|
.filter(extension => extension.inputRules)
|
||||||
.map(extension => extension.inputRules({
|
.map(extension => extension.inputRules({
|
||||||
type: schema[`${extension.type}s`][extension.name],
|
type: schema[`${extension.type}s`][extension.name],
|
||||||
schema,
|
schema,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...extensionInputRules,
|
...extensionInputRules,
|
||||||
...nodeMarkInputRules,
|
...nodeMarkInputRules,
|
||||||
].reduce((allInputRules, inputRules) => ([
|
].reduce((allInputRules, inputRules) => ([
|
||||||
...allInputRules,
|
...allInputRules,
|
||||||
...inputRules,
|
...inputRules,
|
||||||
]), [])
|
]), [])
|
||||||
}
|
}
|
||||||
|
|
||||||
commands({ schema, view, editable }) {
|
commands({ schema, view, editable }) {
|
||||||
return this.extensions
|
return this.extensions
|
||||||
.filter(extension => extension.commands)
|
.filter(extension => extension.commands)
|
||||||
.reduce((allCommands, { name, type, commands: provider }) => {
|
.reduce((allCommands, { name, type, commands: provider }) => {
|
||||||
|
|
||||||
const commands = {}
|
const commands = {}
|
||||||
const value = provider({
|
const value = provider({
|
||||||
schema,
|
schema,
|
||||||
...['node', 'mark'].includes(type) ? {
|
...['node', 'mark'].includes(type) ? {
|
||||||
type: schema[`${type}s`][name],
|
type: schema[`${type}s`][name],
|
||||||
} : {},
|
} : {},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
commands[name] = attrs => value
|
commands[name] = attrs => value
|
||||||
.forEach(callback => {
|
.forEach(callback => {
|
||||||
if (!editable) {
|
if (!editable) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
view.focus()
|
view.focus()
|
||||||
return callback(attrs)(view.state, view.dispatch, view)
|
return callback(attrs)(view.state, view.dispatch, view)
|
||||||
})
|
})
|
||||||
} else if (typeof value === 'function') {
|
} else if (typeof value === 'function') {
|
||||||
commands[name] = attrs => {
|
commands[name] = attrs => {
|
||||||
if (!editable) {
|
if (!editable) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
view.focus()
|
view.focus()
|
||||||
return value(attrs)(view.state, view.dispatch, view)
|
return value(attrs)(view.state, view.dispatch, view)
|
||||||
}
|
}
|
||||||
} else if (typeof value === 'object') {
|
} else if (typeof value === 'object') {
|
||||||
Object.entries(value).forEach(([commandName, commandValue]) => {
|
Object.entries(value).forEach(([commandName, commandValue]) => {
|
||||||
if (Array.isArray(commandValue)) {
|
if (Array.isArray(commandValue)) {
|
||||||
commands[commandName] = attrs => commandValue
|
commands[commandName] = attrs => commandValue
|
||||||
.forEach(callback => {
|
.forEach(callback => {
|
||||||
if (!editable) {
|
if (!editable) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
view.focus()
|
view.focus()
|
||||||
return callback(attrs)(view.state, view.dispatch, view)
|
return callback(attrs)(view.state, view.dispatch, view)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
commands[commandName] = attrs => {
|
commands[commandName] = attrs => {
|
||||||
if (!editable) {
|
if (!editable) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
view.focus()
|
view.focus()
|
||||||
return commandValue(attrs)(view.state, view.dispatch, view)
|
return commandValue(attrs)(view.state, view.dispatch, view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...allCommands,
|
...allCommands,
|
||||||
...commands,
|
...commands,
|
||||||
}
|
}
|
||||||
}, {})
|
}, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,81 +2,81 @@ import { Plugin } from 'prosemirror-state'
|
|||||||
|
|
||||||
class Menu {
|
class Menu {
|
||||||
|
|
||||||
constructor({ options, editorView }) {
|
constructor({ options, editorView }) {
|
||||||
this.options = {
|
this.options = {
|
||||||
...{
|
...{
|
||||||
element: null,
|
element: null,
|
||||||
onUpdate: () => false,
|
onUpdate: () => false,
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
}
|
}
|
||||||
this.editorView = editorView
|
this.editorView = editorView
|
||||||
this.isActive = false
|
this.isActive = false
|
||||||
this.top = 0
|
this.top = 0
|
||||||
|
|
||||||
this.editorView.dom.addEventListener('blur', this.hide.bind(this))
|
this.editorView.dom.addEventListener('blur', this.hide.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
update(view, lastState) {
|
update(view, lastState) {
|
||||||
const { state } = view
|
const { state } = view
|
||||||
|
|
||||||
// Don't do anything if the document/selection didn't change
|
// Don't do anything if the document/selection didn't change
|
||||||
if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
|
if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.selection.empty) {
|
if (!state.selection.empty) {
|
||||||
this.hide()
|
this.hide()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentDom = view.domAtPos(state.selection.$anchor.pos)
|
const currentDom = view.domAtPos(state.selection.$anchor.pos)
|
||||||
|
|
||||||
const isActive = currentDom.node.innerHTML === '<br>'
|
const isActive = currentDom.node.innerHTML === '<br>'
|
||||||
&& currentDom.node.tagName === 'P'
|
&& currentDom.node.tagName === 'P'
|
||||||
&& currentDom.node.parentNode === view.dom
|
&& currentDom.node.parentNode === view.dom
|
||||||
|
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
this.hide()
|
this.hide()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const editorBoundings = this.options.element.offsetParent.getBoundingClientRect()
|
const editorBoundings = this.options.element.offsetParent.getBoundingClientRect()
|
||||||
const cursorBoundings = view.coordsAtPos(state.selection.$anchor.pos)
|
const cursorBoundings = view.coordsAtPos(state.selection.$anchor.pos)
|
||||||
const top = cursorBoundings.top - editorBoundings.top
|
const top = cursorBoundings.top - editorBoundings.top
|
||||||
|
|
||||||
this.isActive = true
|
this.isActive = true
|
||||||
this.top = top
|
this.top = top
|
||||||
|
|
||||||
this.sendUpdate()
|
this.sendUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
sendUpdate() {
|
sendUpdate() {
|
||||||
this.options.onUpdate({
|
this.options.onUpdate({
|
||||||
isActive: this.isActive,
|
isActive: this.isActive,
|
||||||
top: this.top,
|
top: this.top,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
hide(event) {
|
hide(event) {
|
||||||
if (event && event.relatedTarget) {
|
if (event && event.relatedTarget) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isActive = false
|
this.isActive = false
|
||||||
this.sendUpdate()
|
this.sendUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.editorView.dom.removeEventListener('blur', this.hide)
|
this.editorView.dom.removeEventListener('blur', this.hide)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function (options) {
|
export default function (options) {
|
||||||
return new Plugin({
|
return new Plugin({
|
||||||
view(editorView) {
|
view(editorView) {
|
||||||
return new Menu({ editorView, options })
|
return new Menu({ editorView, options })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,84 +2,84 @@ import { Plugin } from 'prosemirror-state'
|
|||||||
|
|
||||||
class Menu {
|
class Menu {
|
||||||
|
|
||||||
constructor({ options, editorView }) {
|
constructor({ options, editorView }) {
|
||||||
this.options = {
|
this.options = {
|
||||||
...{
|
...{
|
||||||
element: null,
|
element: null,
|
||||||
onUpdate: () => false,
|
onUpdate: () => false,
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
}
|
}
|
||||||
this.editorView = editorView
|
this.editorView = editorView
|
||||||
this.isActive = false
|
this.isActive = false
|
||||||
this.left = 0
|
this.left = 0
|
||||||
this.bottom = 0
|
this.bottom = 0
|
||||||
|
|
||||||
this.editorView.dom.addEventListener('blur', this.hide.bind(this))
|
this.editorView.dom.addEventListener('blur', this.hide.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
update(view, lastState) {
|
update(view, lastState) {
|
||||||
const { state } = view
|
const { state } = view
|
||||||
|
|
||||||
// Don't do anything if the document/selection didn't change
|
// Don't do anything if the document/selection didn't change
|
||||||
if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
|
if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide the tooltip if the selection is empty
|
// Hide the tooltip if the selection is empty
|
||||||
if (state.selection.empty) {
|
if (state.selection.empty) {
|
||||||
this.hide()
|
this.hide()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, reposition it and update its content
|
// Otherwise, reposition it and update its content
|
||||||
const { from, to } = state.selection
|
const { from, to } = state.selection
|
||||||
|
|
||||||
// These are in screen coordinates
|
// These are in screen coordinates
|
||||||
const start = view.coordsAtPos(from)
|
const start = view.coordsAtPos(from)
|
||||||
const end = view.coordsAtPos(to)
|
const end = view.coordsAtPos(to)
|
||||||
|
|
||||||
// The box in which the tooltip is positioned, to use as base
|
// The box in which the tooltip is positioned, to use as base
|
||||||
const box = this.options.element.offsetParent.getBoundingClientRect()
|
const box = this.options.element.offsetParent.getBoundingClientRect()
|
||||||
|
|
||||||
// Find a center-ish x position from the selection endpoints (when
|
// Find a center-ish x position from the selection endpoints (when
|
||||||
// crossing lines, end may be more to the left)
|
// crossing lines, end may be more to the left)
|
||||||
const left = Math.max((start.left + end.left) / 2, start.left + 3)
|
const left = Math.max((start.left + end.left) / 2, start.left + 3)
|
||||||
|
|
||||||
this.isActive = true
|
this.isActive = true
|
||||||
this.left = parseInt(left - box.left, 10)
|
this.left = parseInt(left - box.left, 10)
|
||||||
this.bottom = parseInt(box.bottom - start.top, 10)
|
this.bottom = parseInt(box.bottom - start.top, 10)
|
||||||
|
|
||||||
this.sendUpdate()
|
this.sendUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
sendUpdate() {
|
sendUpdate() {
|
||||||
this.options.onUpdate({
|
this.options.onUpdate({
|
||||||
isActive: this.isActive,
|
isActive: this.isActive,
|
||||||
left: this.left,
|
left: this.left,
|
||||||
bottom: this.bottom,
|
bottom: this.bottom,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
hide(event) {
|
hide(event) {
|
||||||
if (event && event.relatedTarget) {
|
if (event && event.relatedTarget) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isActive = false
|
this.isActive = false
|
||||||
this.sendUpdate()
|
this.sendUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.editorView.dom.removeEventListener('blur', this.hide)
|
this.editorView.dom.removeEventListener('blur', this.hide)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function (options) {
|
export default function (options) {
|
||||||
return new Plugin({
|
return new Plugin({
|
||||||
view(editorView) {
|
view(editorView) {
|
||||||
return new Menu({ editorView, options })
|
return new Menu({ editorView, options })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { lift, selectParentNode } from 'prosemirror-commands'
|
|||||||
import { undoInputRule } from 'prosemirror-inputrules'
|
import { undoInputRule } from 'prosemirror-inputrules'
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'Mod-BracketLeft': lift,
|
'Mod-BracketLeft': lift,
|
||||||
Backspace: undoInputRule,
|
Backspace: undoInputRule,
|
||||||
Escape: selectParentNode,
|
Escape: selectParentNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default keymap
|
export default keymap
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
export default class Extension {
|
export default class Extension {
|
||||||
|
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
this.options = {
|
this.options = {
|
||||||
...this.defaultOptions,
|
...this.defaultOptions,
|
||||||
...options,
|
...options,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
get type() {
|
get type() {
|
||||||
return 'extension'
|
return 'extension'
|
||||||
}
|
}
|
||||||
|
|
||||||
get defaultOptions() {
|
get defaultOptions() {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
get plugins() {
|
get plugins() {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
inputRules() {
|
inputRules() {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
keys() {
|
keys() {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,24 +2,24 @@ import Extension from './Extension'
|
|||||||
|
|
||||||
export default class Mark extends Extension {
|
export default class Mark extends Extension {
|
||||||
|
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
super(options)
|
super(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
get type() {
|
get type() {
|
||||||
return 'mark'
|
return 'mark'
|
||||||
}
|
}
|
||||||
|
|
||||||
get view() {
|
get view() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
command() {
|
command() {
|
||||||
return () => {}
|
return () => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,24 +2,24 @@ import Extension from './Extension'
|
|||||||
|
|
||||||
export default class Node extends Extension {
|
export default class Node extends Extension {
|
||||||
|
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
super(options)
|
super(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
get type() {
|
get type() {
|
||||||
return 'node'
|
return 'node'
|
||||||
}
|
}
|
||||||
|
|
||||||
get view() {
|
get view() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
get schema() {
|
get schema() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
command() {
|
command() {
|
||||||
return () => {}
|
return () => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user