Initial commit for v3
parent
abead2a068
commit
e6a9359b1d
@ -0,0 +1,3 @@
|
|||||||
|
coverage
|
||||||
|
dist
|
||||||
|
node_modules
|
@ -1,27 +1,12 @@
|
|||||||
{
|
{
|
||||||
"root": true,
|
"root": true,
|
||||||
|
"extends": ["@ionaru", "prettier"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"strict": 0,
|
"jest/no-deprecated-functions": "off",
|
||||||
"no-console": 0,
|
"jest/unbound-method": "off",
|
||||||
"quotes": [
|
"jest/require-hook": "off",
|
||||||
"error",
|
"import/extensions": "off",
|
||||||
"single"
|
"import/no-unresolved": "off",
|
||||||
],
|
"unicorn/no-null": "off"
|
||||||
"semi": [
|
}
|
||||||
"error",
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"comma-dangle": [
|
|
||||||
"error",
|
|
||||||
"always-multiline"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": 2018
|
|
||||||
},
|
|
||||||
"extends": "eslint:recommended"
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
<!-- Please help me process issues faster by providing the following information -->
|
<!-- Please help me process issues faster by providing the following information -->
|
||||||
|
|
||||||
### I'm submitting a...
|
### I'm submitting a...
|
||||||
- [x] Bug report
|
|
||||||
- [ ] Feature request
|
- [x] Bug report
|
||||||
|
- [ ] Feature request
|
||||||
|
|
||||||
### Reproduction steps
|
### Reproduction steps
|
||||||
|
|
||||||
<!-- Bonus points if you set up a [JSFiddle](https://jsfiddle.net/) that replicates the bug and link it in the issue. -->
|
<!-- Bonus points if you set up a [JSFiddle](https://jsfiddle.net/) that replicates the bug and link it in the issue. -->
|
||||||
|
|
||||||
1. ...
|
1. ...
|
||||||
2. ...
|
2. ...
|
||||||
|
|
||||||
### Version information
|
### Version information
|
||||||
|
|
||||||
Browser type and version:
|
Browser type and version:
|
||||||
EasyMDE version:
|
EasyMDE version:
|
||||||
|
@ -1,95 +1,91 @@
|
|||||||
name: Test & Deploy
|
name: Test & Deploy
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- '*'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
audit:
|
||||||
audit:
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
steps:
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
steps:
|
with:
|
||||||
- uses: actions/setup-node@v3
|
node-version: current
|
||||||
with:
|
|
||||||
node-version: current
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- run: npm audit --omit=dev
|
||||||
|
|
||||||
- run: npm audit --omit=dev
|
test:
|
||||||
|
needs: [audit]
|
||||||
test:
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
needs: [ audit ]
|
strategy:
|
||||||
runs-on: ubuntu-latest
|
matrix:
|
||||||
|
node-version: ['14', '16', '18']
|
||||||
strategy:
|
|
||||||
matrix:
|
steps:
|
||||||
node-version: [ '14', '16', '18' ]
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
steps:
|
node-version: ${{ matrix.node-version }}
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
- uses: actions/checkout@v3
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
|
- run: npm ci
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
- name: Test
|
||||||
- run: npm ci
|
run: npm test
|
||||||
|
|
||||||
- name: Test
|
# - uses: actions/upload-artifact@v3
|
||||||
run: npm test
|
# if: failure()
|
||||||
|
# with:
|
||||||
- uses: actions/upload-artifact@v3
|
# name: cypress-screenshots
|
||||||
if: failure()
|
# path: cypress/screenshots
|
||||||
with:
|
# retention-days: 7
|
||||||
name: cypress-screenshots
|
#
|
||||||
path: cypress/screenshots
|
# - uses: actions/upload-artifact@v3
|
||||||
retention-days: 7
|
# if: always()
|
||||||
|
# with:
|
||||||
- uses: actions/upload-artifact@v3
|
# name: cypress-videos
|
||||||
if: always()
|
# path: cypress/videos
|
||||||
with:
|
# retention-days: 7
|
||||||
name: cypress-videos
|
|
||||||
path: cypress/videos
|
# deploy:
|
||||||
retention-days: 7
|
# needs: [test]
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
deploy:
|
# if: github.event_name == 'push'
|
||||||
|
#
|
||||||
needs: [ test ]
|
# steps:
|
||||||
runs-on: ubuntu-latest
|
# - uses: actions/setup-node@v3
|
||||||
if: github.event_name == 'push'
|
# with:
|
||||||
|
# node-version: current
|
||||||
steps:
|
#
|
||||||
- uses: actions/setup-node@v3
|
# - uses: actions/checkout@v3
|
||||||
with:
|
#
|
||||||
node-version: current
|
# - run: npm ci
|
||||||
|
#
|
||||||
- uses: actions/checkout@v3
|
# - name: Deploy @latest version to npm
|
||||||
|
# if: startsWith(github.ref, 'refs/tags/')
|
||||||
- run: npm ci
|
# uses: JS-DevTools/npm-publish@v1
|
||||||
|
# with:
|
||||||
- name: Deploy @latest version to npm
|
# token: ${{ secrets.NPM_TOKEN }}
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
#
|
||||||
uses: JS-DevTools/npm-publish@v1
|
# - name: Update @next version
|
||||||
with:
|
# if: startsWith(github.ref, 'refs/heads/')
|
||||||
token: ${{ secrets.NPM_TOKEN }}
|
# run: npm version prerelease --no-git-tag-version --preid "$GITHUB_RUN_NUMBER"
|
||||||
|
#
|
||||||
- name: Update @next version
|
# - name: Deploy @next version to npm
|
||||||
if: startsWith(github.ref, 'refs/heads/')
|
# if: startsWith(github.ref, 'refs/heads/')
|
||||||
run: npm version prerelease --no-git-tag-version --preid "$GITHUB_RUN_NUMBER"
|
# uses: JS-DevTools/npm-publish@v1
|
||||||
|
# with:
|
||||||
- name: Deploy @next version to npm
|
# tag: next
|
||||||
if: startsWith(github.ref, 'refs/heads/')
|
# token: ${{ secrets.NPM_TOKEN }}
|
||||||
uses: JS-DevTools/npm-publish@v1
|
# check-version: false
|
||||||
with:
|
|
||||||
tag: next
|
|
||||||
token: ${{ secrets.NPM_TOKEN }}
|
|
||||||
check-version: false
|
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
# Add files here to ignore them from prettier formatting
|
||||||
|
|
||||||
|
coverage
|
||||||
|
dist
|
||||||
|
node_modules
|
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 80,
|
||||||
|
"singleQuote": true,
|
||||||
|
"quoteProps": "consistent",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
@ -1,9 +0,0 @@
|
|||||||
import { defineConfig } from 'cypress';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
e2e: {
|
|
||||||
excludeSpecPattern: [
|
|
||||||
'**/*.html',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"plugins": [
|
|
||||||
"cypress"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"node": true,
|
|
||||||
"es6": true,
|
|
||||||
"cypress/globals": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"../.eslintrc",
|
|
||||||
"plugin:cypress/recommended"
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Default</title>
|
|
||||||
<link rel="stylesheet" href="../../../dist/easymde.min.css">
|
|
||||||
<script src="../../../dist/easymde.min.js"></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<textarea id="textarea"></textarea>
|
|
||||||
<script>
|
|
||||||
const easyMDE = new EasyMDE();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,31 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
describe('Preview', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit(__dirname + '/index.html');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can show a preview of markdown text', () => {
|
|
||||||
cy.get('.EasyMDEContainer').should('be.visible');
|
|
||||||
cy.get('.EasyMDEContainer .editor-preview').should('not.be.visible');
|
|
||||||
|
|
||||||
// Enter markdown text.
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').type('# My Big Title');
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').type('{enter}');
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').type('This is some **important** text!');
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror-line').should('contain', '# My Big Title');
|
|
||||||
cy.get('.EasyMDEContainer .cm-header.cm-header-1').should('contain', '#');
|
|
||||||
cy.get('.EasyMDEContainer .cm-header.cm-header-1').should('contain', 'My Big Title');
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror-line').should('contain', 'This is some **important** text!');
|
|
||||||
cy.get('.EasyMDEContainer .cm-strong').should('contain', '**');
|
|
||||||
cy.get('.EasyMDEContainer .cm-strong').should('contain', 'important');
|
|
||||||
|
|
||||||
cy.previewOn();
|
|
||||||
|
|
||||||
// Check preview window for rendered markdown.
|
|
||||||
cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '<h1 id="my-big-title">My Big Title</h1>');
|
|
||||||
cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '<p>This is some <strong>important</strong> text!</p>');
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,56 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
describe('Default statusbar', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit(__dirname + '/index.html');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the editor with default statusbar', () => {
|
|
||||||
cy.get('.EasyMDEContainer').should('be.visible');
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar').should('be.visible');
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .autosave').should('be.empty');
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .lines').before('content').should('contain', 'lines: ');
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .lines').should('contain', '1');
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .words').before('content').should('contain', 'words: ');
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .words').should('contain', '0');
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .cursor').should('contain', '1:1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates the statusbar when typing', () => {
|
|
||||||
cy.get('.EasyMDEContainer').should('be.visible');
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar').should('be.visible');
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').type('Hello');
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .autosave').should('be.empty');
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .lines').should('contain', '1');
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .words').should('contain', '1');
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .cursor').should('contain', '1:6');
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').type(' World');
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .lines').should('contain', '1');
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .words').should('contain', '2');
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .cursor').should('contain', '1:12');
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').type('{enter}');
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .lines').should('contain', '2');
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .words').should('contain', '2');
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .cursor').should('contain', '2:1');
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').type('This is a sample text.{enter}We\'re testing the statusbar.{enter}Did it work?');
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .autosave').should('be.empty');
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .lines').before('content').should('contain', 'lines: ');
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .lines').should('contain', '4');
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .words').before('content').should('contain', 'words: ');
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .words').should('contain', '15');
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar .cursor').should('contain', '4:13');
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,17 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
describe('Default editor', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit(__dirname + '/index.html');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the editor with default settings', () => {
|
|
||||||
cy.get('.EasyMDEContainer').should('be.visible');
|
|
||||||
cy.get('#textarea').should('not.be.visible');
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-toolbar').should('be.visible');
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').should('be.visible');
|
|
||||||
cy.get('.EasyMDEContainer .editor-preview').should('not.be.visible');
|
|
||||||
cy.get('.EasyMDEContainer .editor-statusbar').should('be.visible');
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,20 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Default</title>
|
|
||||||
<link rel="stylesheet" href="../../../dist/easymde.min.css">
|
|
||||||
<script src="../../../dist/easymde.min.js"></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<textarea id="textarea"></textarea>
|
|
||||||
<script>
|
|
||||||
const easyMDE = new EasyMDE({
|
|
||||||
promptURLs: true,
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,228 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
describe('URL prompts', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit(__dirname + '/index.html');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('must show the correct text for a link prompt', () => {
|
|
||||||
cy.get('.EasyMDEContainer').should('be.visible');
|
|
||||||
cy.get('#textarea').should('not.be.visible');
|
|
||||||
|
|
||||||
cy.window().then(($win) => {
|
|
||||||
const stub = cy.stub($win, 'prompt');
|
|
||||||
cy.get('button.link').click().then(() => {
|
|
||||||
expect(stub).to.be.calledWith('URL for the link:', 'https://');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('must show the correct text for an image prompt', () => {
|
|
||||||
cy.get('.EasyMDEContainer').should('be.visible');
|
|
||||||
cy.get('#textarea').should('not.be.visible');
|
|
||||||
|
|
||||||
cy.window().then(($win) => {
|
|
||||||
const stub = cy.stub($win, 'prompt');
|
|
||||||
cy.get('button.image').click().then(() => {
|
|
||||||
expect(stub).to.be.calledWith('URL of the image:', 'https://');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('must enter a link correctly through a prompt', () => {
|
|
||||||
cy.get('.EasyMDEContainer').should('be.visible');
|
|
||||||
cy.get('#textarea').should('not.be.visible');
|
|
||||||
|
|
||||||
cy.window().then(($win) => {
|
|
||||||
cy.stub($win, 'prompt').returns('https://example.com');
|
|
||||||
cy.get('button.link').click();
|
|
||||||
});
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com)');
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a website!');
|
|
||||||
|
|
||||||
cy.previewOn();
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '<p><a href="https://example.com" target="_blank">Link to a website!</a></p>');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can use the prompt multiple times', () => {
|
|
||||||
cy.get('.EasyMDEContainer').should('be.visible');
|
|
||||||
cy.get('#textarea').should('not.be.visible');
|
|
||||||
|
|
||||||
cy.window().then(($win) => {
|
|
||||||
const stub = cy.stub($win, 'prompt');
|
|
||||||
stub.returns('https://example.com');
|
|
||||||
cy.get('button.link').click().then(() => {
|
|
||||||
expect(stub).to.be.calledWith('URL for the link:', 'https://');
|
|
||||||
stub.restore();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com)');
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a website!{end}{enter}');
|
|
||||||
|
|
||||||
cy.window().then(($win) => {
|
|
||||||
const stub = cy.stub($win, 'prompt');
|
|
||||||
stub.returns('https://example.eu');
|
|
||||||
cy.get('button.link').click().then(() => {
|
|
||||||
expect(stub).to.be.calledWith('URL for the link:', 'https://');
|
|
||||||
stub.restore();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.eu)');
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a second website!');
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').contains('[Link to a website!](https://example.com)');
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').contains('[Link to a second website!](https://example.eu)');
|
|
||||||
|
|
||||||
cy.previewOn();
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-preview').should(
|
|
||||||
'contain.html',
|
|
||||||
'<p><a href="https://example.com" target="_blank">Link to a website!</a><br><a href="https://example.eu" target="_blank">Link to a second website!</a></p>',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('must be able to deal with parameters in links', () => {
|
|
||||||
cy.get('.EasyMDEContainer').should('be.visible');
|
|
||||||
cy.get('#textarea').should('not.be.visible');
|
|
||||||
|
|
||||||
cy.window().then(($win) => {
|
|
||||||
cy.stub($win, 'prompt').returns('https://example.com?some=param&moo=cow');
|
|
||||||
cy.get('button.link').click();
|
|
||||||
});
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com?some=param&moo=cow)');
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a website!');
|
|
||||||
|
|
||||||
cy.previewOn();
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '<p><a href="https://example.com?some=param&moo=cow" target="_blank">Link to a website!</a></p>');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('must be able to deal with brackets in links', () => {
|
|
||||||
cy.get('.EasyMDEContainer').should('be.visible');
|
|
||||||
cy.get('#textarea').should('not.be.visible');
|
|
||||||
|
|
||||||
cy.window().then(($win) => {
|
|
||||||
cy.stub($win, 'prompt').returns('https://example.com?some=[]param');
|
|
||||||
cy.get('button.link').click();
|
|
||||||
});
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com?some=%5B%5Dparam)');
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a website!');
|
|
||||||
|
|
||||||
cy.previewOn();
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '<p><a href="https://example.com?some=%5B%5Dparam" target="_blank">Link to a website!</a></p>');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('must be able to deal with parentheses in links', () => {
|
|
||||||
cy.get('.EasyMDEContainer').should('be.visible');
|
|
||||||
cy.get('#textarea').should('not.be.visible');
|
|
||||||
|
|
||||||
cy.window().then(($win) => {
|
|
||||||
cy.stub($win, 'prompt').returns('https://example.com?some=(param)');
|
|
||||||
cy.get('button.link').click();
|
|
||||||
});
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com?some=\\(param\\))');
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a website!');
|
|
||||||
|
|
||||||
cy.previewOn();
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '<p><a href="https://example.com?some=(param)" target="_blank">Link to a website!</a></p>');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('must be able to deal with parentheses in links (multiple)', () => {
|
|
||||||
cy.get('.EasyMDEContainer').should('be.visible');
|
|
||||||
cy.get('#textarea').should('not.be.visible');
|
|
||||||
|
|
||||||
cy.window().then(($win) => {
|
|
||||||
cy.stub($win, 'prompt').returns('https://example.com?some=(param1,param2)&more=(param3,param4)&end=true');
|
|
||||||
cy.get('button.link').click();
|
|
||||||
});
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com?some=\\(param1,param2\\)&more=\\(param3,param4\\)&end=true)');
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a website!');
|
|
||||||
|
|
||||||
cy.previewOn();
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '<p><a href="https://example.com?some=(param1,param2)&more=(param3,param4)&end=true" target="_blank">Link to a website!</a></p>');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('must be able to deal with unbalanced parentheses in links (opening)', () => {
|
|
||||||
cy.get('.EasyMDEContainer').should('be.visible');
|
|
||||||
cy.get('#textarea').should('not.be.visible');
|
|
||||||
|
|
||||||
cy.window().then(($win) => {
|
|
||||||
cy.stub($win, 'prompt').returns('https://example.com?some=(param');
|
|
||||||
cy.get('button.link').click();
|
|
||||||
});
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com?some=\\(param)');
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a website!');
|
|
||||||
|
|
||||||
cy.previewOn();
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '<p><a href="https://example.com?some=(param" target="_blank">Link to a website!</a></p>');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('must be able to deal with unbalanced parentheses in links (closing)', () => {
|
|
||||||
cy.get('.EasyMDEContainer').should('be.visible');
|
|
||||||
cy.get('#textarea').should('not.be.visible');
|
|
||||||
|
|
||||||
cy.window().then(($win) => {
|
|
||||||
cy.stub($win, 'prompt').returns('https://example.com?some=)param');
|
|
||||||
cy.get('button.link').click();
|
|
||||||
});
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com?some=\\)param)');
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a website!');
|
|
||||||
|
|
||||||
cy.previewOn();
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '<p><a href="https://example.com?some=)param" target="_blank">Link to a website!</a></p>');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('must be able to deal with inequality symbols in links', () => {
|
|
||||||
cy.get('.EasyMDEContainer').should('be.visible');
|
|
||||||
cy.get('#textarea').should('not.be.visible');
|
|
||||||
|
|
||||||
cy.window().then(($win) => {
|
|
||||||
cy.stub($win, 'prompt').returns('https://example.com?some=<param');
|
|
||||||
cy.get('button.link').click();
|
|
||||||
});
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com?some=%3Cparam');
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a website!');
|
|
||||||
|
|
||||||
cy.previewOn();
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '<p><a href="https://example.com?some=%3Cparam" target="_blank">Link to a website!</a></p>');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('must be able to deal with emoji in links', () => {
|
|
||||||
cy.get('.EasyMDEContainer').should('be.visible');
|
|
||||||
cy.get('#textarea').should('not.be.visible');
|
|
||||||
|
|
||||||
cy.window().then(($win) => {
|
|
||||||
cy.stub($win, 'prompt').returns('https://example.com?some=👷♂️');
|
|
||||||
cy.get('button.link').click();
|
|
||||||
});
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com?some=%F0%9F%91%B7%E2%80%8D%E2%99%82%EF%B8%8F');
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a 👌 website!');
|
|
||||||
|
|
||||||
cy.previewOn();
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '<p><a href="https://example.com?some=%F0%9F%91%B7%E2%80%8D%E2%99%82%EF%B8%8F" target="_blank">Link to a 👌 website!</a></p>');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('must be able to deal with spaces in links', () => {
|
|
||||||
cy.get('.EasyMDEContainer').should('be.visible');
|
|
||||||
cy.get('#textarea').should('not.be.visible');
|
|
||||||
|
|
||||||
cy.window().then(($win) => {
|
|
||||||
cy.stub($win, 'prompt').returns('https://example.com?some=very special param');
|
|
||||||
cy.get('button.link').click();
|
|
||||||
});
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').contains('[](https://example.com?some=very%20special%20param');
|
|
||||||
cy.get('.EasyMDEContainer .CodeMirror').type('{home}{rightarrow}Link to a website!');
|
|
||||||
|
|
||||||
cy.previewOn();
|
|
||||||
|
|
||||||
cy.get('.EasyMDEContainer .editor-preview').should('contain.html', '<p><a href="https://example.com?some=very%20special%20param" target="_blank">Link to a website!</a></p>');
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,17 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
describe('Default editor', () => {
|
|
||||||
it('table', () => {
|
|
||||||
cy.visit(__dirname + '/index-no-prefix.html');
|
|
||||||
|
|
||||||
cy.get('button.table').should('be.visible');
|
|
||||||
cy.get('button.table').invoke('outerWidth').should('not.equal', 30);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads the editor with default settings', () => {
|
|
||||||
cy.visit(__dirname + '/index.html');
|
|
||||||
|
|
||||||
cy.get('button.mde-table').should('be.visible');
|
|
||||||
cy.get('button.mde-table').invoke('outerWidth').should('equal', 30);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,21 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Default</title>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
|
||||||
<link rel="stylesheet" href="../../../dist/easymde.min.css">
|
|
||||||
<script src="../../../dist/easymde.min.js"></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<textarea id="textarea"></textarea>
|
|
||||||
<script>
|
|
||||||
const easyMDE = new EasyMDE({
|
|
||||||
showIcons: ["table"],
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,22 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Default</title>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
|
||||||
<link rel="stylesheet" href="../../../dist/easymde.min.css">
|
|
||||||
<script src="../../../dist/easymde.min.js"></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<textarea id="textarea"></textarea>
|
|
||||||
<script>
|
|
||||||
const easyMDE = new EasyMDE({
|
|
||||||
toolbarButtonClassPrefix: 'mde',
|
|
||||||
showIcons: ["table"],
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,10 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {Cypress.PluginConfig}
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
module.exports = (on, config) => {
|
|
||||||
// `on` is used to hook into various events Cypress emits
|
|
||||||
// `config` is the resolved Cypress config
|
|
||||||
};
|
|
@ -1,31 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
const unquote = (str) => str.replace(/(^")|("$)/g, '');
|
|
||||||
|
|
||||||
Cypress.Commands.add(
|
|
||||||
'before',
|
|
||||||
{
|
|
||||||
prevSubject: 'element',
|
|
||||||
},
|
|
||||||
(element, property) => {
|
|
||||||
const win = element[0].ownerDocument.defaultView;
|
|
||||||
const before = win.getComputedStyle(element[0], 'before');
|
|
||||||
return unquote(before.getPropertyValue(property));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Cypress.Commands.add('previewOn' , () => {
|
|
||||||
cy.get('.EasyMDEContainer .editor-preview').should('not.be.visible');
|
|
||||||
cy.get('.EasyMDEContainer .editor-toolbar button.preview').should('not.have.class', 'active');
|
|
||||||
cy.get('.EasyMDEContainer .editor-toolbar button.preview').click();
|
|
||||||
cy.get('.EasyMDEContainer .editor-toolbar button.preview').should('have.class', 'active');
|
|
||||||
cy.get('.EasyMDEContainer .editor-preview').should('be.visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
Cypress.Commands.add('previewOff' , () => {
|
|
||||||
cy.get('.EasyMDEContainer .editor-preview').should('be.visible');
|
|
||||||
cy.get('.EasyMDEContainer .editor-toolbar button.preview').should('have.class', 'active');
|
|
||||||
cy.get('.EasyMDEContainer .editor-toolbar button.preview').click();
|
|
||||||
cy.get('.EasyMDEContainer .editor-toolbar button.preview').should('not.have.class', 'active');
|
|
||||||
cy.get('.EasyMDEContainer .editor-preview').should('not.be.visible');
|
|
||||||
});
|
|
@ -1 +0,0 @@
|
|||||||
require('./commands');
|
|
@ -1,20 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>Example / Preview</title>
|
|
||||||
<link rel="stylesheet" href="../dist/easymde.min.css">
|
|
||||||
<script src="../dist/easymde.min.js"></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<textarea></textarea>
|
|
||||||
<script>
|
|
||||||
const easyMDE = new EasyMDE();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,20 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>Example / Preview / sideBySideFullscreen : false</title>
|
|
||||||
<link rel="stylesheet" href="../dist/easymde.min.css">
|
|
||||||
<script src="../dist/easymde.min.js"></script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<textarea></textarea>
|
|
||||||
<script>
|
|
||||||
const easyMDE = new EasyMDE({sideBySideFullscreen: false});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,66 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
var gulp = require('gulp');
|
|
||||||
var cleanCSS = require('gulp-clean-css');
|
|
||||||
var terser = require('gulp-terser');
|
|
||||||
var concat = require('gulp-concat');
|
|
||||||
var header = require('gulp-header');
|
|
||||||
var buffer = require('vinyl-buffer');
|
|
||||||
var pkg = require('./package.json');
|
|
||||||
var eslint = require('gulp-eslint');
|
|
||||||
var browserify = require('browserify');
|
|
||||||
var source = require('vinyl-source-stream');
|
|
||||||
var rename = require('gulp-rename');
|
|
||||||
|
|
||||||
var banner = ['/**',
|
|
||||||
' * <%= pkg.name %> v<%= pkg.version %>',
|
|
||||||
' * Copyright <%= pkg.author %>',
|
|
||||||
' * @link https://github.com/ionaru/easy-markdown-editor',
|
|
||||||
' * @license <%= pkg.license %>',
|
|
||||||
' */',
|
|
||||||
''].join('\n');
|
|
||||||
|
|
||||||
|
|
||||||
var css_files = [
|
|
||||||
'./node_modules/codemirror/lib/codemirror.css',
|
|
||||||
'./src/css/*.css',
|
|
||||||
'./node_modules/codemirror-spell-checker/src/css/spell-checker.css',
|
|
||||||
];
|
|
||||||
|
|
||||||
function lint() {
|
|
||||||
return gulp.src('./src/js/**/*.js')
|
|
||||||
.pipe(eslint())
|
|
||||||
.pipe(eslint.format())
|
|
||||||
.pipe(eslint.failAfterError());
|
|
||||||
}
|
|
||||||
|
|
||||||
function scripts() {
|
|
||||||
return browserify({entries: './src/js/easymde.js', standalone: 'EasyMDE'}).bundle()
|
|
||||||
.pipe(source('easymde.min.js'))
|
|
||||||
.pipe(buffer())
|
|
||||||
.pipe(terser())
|
|
||||||
.pipe(header(banner, {pkg: pkg}))
|
|
||||||
.pipe(gulp.dest('./dist/'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function styles() {
|
|
||||||
return gulp.src(css_files)
|
|
||||||
.pipe(concat('easymde.css'))
|
|
||||||
.pipe(cleanCSS())
|
|
||||||
.pipe(rename('easymde.min.css'))
|
|
||||||
.pipe(buffer())
|
|
||||||
.pipe(header(banner, {pkg: pkg}))
|
|
||||||
.pipe(gulp.dest('./dist/'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watch for file changes
|
|
||||||
function watch() {
|
|
||||||
gulp.watch('./src/js/**/*.js', scripts);
|
|
||||||
gulp.watch(css_files, styles);
|
|
||||||
}
|
|
||||||
|
|
||||||
var build = gulp.parallel(gulp.series(lint, scripts), styles);
|
|
||||||
|
|
||||||
gulp.task('default', build);
|
|
||||||
gulp.task('watch', gulp.series(build, watch));
|
|
||||||
gulp.task('lint', lint);
|
|
File diff suppressed because it is too large
Load Diff
@ -1,55 +1,70 @@
|
|||||||
{
|
{
|
||||||
"name": "easymde",
|
"name": "easymde",
|
||||||
"version": "2.18.0",
|
"version": "3.0.0",
|
||||||
|
"type": "module",
|
||||||
"description": "A simple, beautiful, and embeddable JavaScript Markdown editor that easy to use. Features include autosaving and spell checking.",
|
"description": "A simple, beautiful, and embeddable JavaScript Markdown editor that easy to use. Features include autosaving and spell checking.",
|
||||||
"files": [
|
|
||||||
"dist/**/*",
|
|
||||||
"src/**/*",
|
|
||||||
"types/easymde.d.ts"
|
|
||||||
],
|
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"embeddable",
|
"embeddable",
|
||||||
"markdown",
|
"markdown",
|
||||||
"editor",
|
"editor",
|
||||||
"javascript",
|
"javascript"
|
||||||
"fontawesome"
|
|
||||||
],
|
],
|
||||||
"main": "src/js/easymde.js",
|
"main": "dist/node/easymde.js",
|
||||||
"types": "types/easymde.d.ts",
|
"browser": "dist/browser/easymde.min.js",
|
||||||
|
"types": "dist/types/index.d.ts",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Jeroen Akkerman",
|
"author": "Jeroen Akkerman",
|
||||||
|
"repository": "github:Ionaru/easy-markdown-editor",
|
||||||
|
"scripts": {
|
||||||
|
"clean": "npx rimraf dist",
|
||||||
|
"prebuild": "npm run clean",
|
||||||
|
"build": "rollup -c",
|
||||||
|
"build:watch": "rollup -c -w",
|
||||||
|
"build:types": "tsc --emitDeclarationOnly --declaration --outDir dist/types",
|
||||||
|
"postbuild": "npm run build:types",
|
||||||
|
"pretest": "npm run lint",
|
||||||
|
"test": "npm run test:unit",
|
||||||
|
"test:unit": "vitest run",
|
||||||
|
"test:e2e": "cypress run",
|
||||||
|
"lint": "prettier --check . && eslint .",
|
||||||
|
"format": "prettier --write . && eslint --fix ."
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/codemirror": "^5.60.4",
|
"@codemirror/lang-markdown": "^6.1.1",
|
||||||
"@types/marked": "^4.0.7",
|
"@codemirror/language": "^6.6.0",
|
||||||
"codemirror": "^5.63.1",
|
"@codemirror/state": "^6.2.0",
|
||||||
"codemirror-spell-checker": "1.1.2",
|
"@codemirror/view": "^6.10.0",
|
||||||
"marked": "^4.1.0"
|
"@lezer/highlight": "^1.1.4",
|
||||||
|
"@lezer/markdown": "^1.0.2",
|
||||||
|
"escape-string-regexp": "^5.0.0",
|
||||||
|
"marked": "^4.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"browserify": "^17.0.0",
|
"@ionaru/eslint-config": "^9.2.1-53.0",
|
||||||
"cypress": "^10.8.0",
|
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||||
"eslint": "^8.23.1",
|
"@rollup/plugin-terser": "^0.4.1",
|
||||||
"eslint-plugin-cypress": "^2.12.1",
|
"@rollup/plugin-typescript": "^11.1.0",
|
||||||
"gulp": "^4.0.2",
|
"@types/marked": "^4.0.8",
|
||||||
"gulp-clean-css": "^4.2.0",
|
"@types/node": "^18.16.2",
|
||||||
"gulp-concat": "^2.6.1",
|
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
||||||
"gulp-eslint": "^6.0.0",
|
"@vitest/coverage-c8": "^0.30.1",
|
||||||
"gulp-header": "^2.0.9",
|
"@vitest/ui": "^0.30.1",
|
||||||
"gulp-rename": "^2.0.0",
|
"cypress": "^12.11.0",
|
||||||
"gulp-terser": "^2.1.0",
|
"eslint": "^8.39.0",
|
||||||
"gulp-uglify": "^3.0.2",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
"typescript": "^4.8.3",
|
"eslint-plugin-import": "^2.27.5",
|
||||||
"vinyl-buffer": "^1.0.0",
|
"eslint-plugin-jest": "^27.2.1",
|
||||||
"vinyl-source-stream": "^2.0.0"
|
"eslint-plugin-prefer-arrow": "^1.2.3",
|
||||||
},
|
"eslint-plugin-sonarjs": "^0.19.0",
|
||||||
"repository": "github:Ionaru/easy-markdown-editor",
|
"eslint-plugin-unicorn": "^46.0.0",
|
||||||
"scripts": {
|
"jsdom": "^21.1.1",
|
||||||
"prepare": "gulp",
|
"prettier": "^2.8.8",
|
||||||
"test": "npm run lint && npm run test:types && npm run e2e",
|
"rollup": "^3.21.0",
|
||||||
"lint": "gulp lint",
|
"rollup-plugin-cleaner": "^1.0.0",
|
||||||
"cypress:lint": "eslint cypress",
|
"rollup-plugin-scss": "^4.0.0",
|
||||||
"cypress:run": "cypress run",
|
"sass": "^1.62.1",
|
||||||
"e2e": "gulp && npm run cypress:lint && npm run cypress:run",
|
"tslib": "^2.5.0",
|
||||||
"test:types": "tsc --project types/tsconfig.json"
|
"typescript": "^5.0.4",
|
||||||
|
"vitest": "^0.30.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||||
|
import terser from '@rollup/plugin-terser';
|
||||||
|
import typescript from '@rollup/plugin-typescript';
|
||||||
|
import cleaner from 'rollup-plugin-cleaner';
|
||||||
|
import scss from 'rollup-plugin-scss';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
// Browser configuration
|
||||||
|
{
|
||||||
|
input: 'src/index.ts',
|
||||||
|
output: {
|
||||||
|
file: 'dist/browser/easymde.min.js',
|
||||||
|
inlineDynamicImports: true,
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
cleaner({
|
||||||
|
targets: ['./dist/'],
|
||||||
|
}),
|
||||||
|
nodeResolve(),
|
||||||
|
scss({
|
||||||
|
fileName: 'easymde.css',
|
||||||
|
}),
|
||||||
|
typescript(),
|
||||||
|
terser(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
@ -1,403 +0,0 @@
|
|||||||
.EasyMDEContainer {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.CodeMirror-rtl pre {
|
|
||||||
direction: rtl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.EasyMDEContainer.sided--no-fullscreen {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.EasyMDEContainer .CodeMirror {
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: auto;
|
|
||||||
border: 1px solid #ced4da;
|
|
||||||
border-bottom-left-radius: 4px;
|
|
||||||
border-bottom-right-radius: 4px;
|
|
||||||
padding: 10px;
|
|
||||||
font: inherit;
|
|
||||||
z-index: 0;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.EasyMDEContainer .CodeMirror-scroll {
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.EasyMDEContainer .CodeMirror-fullscreen {
|
|
||||||
background: #fff;
|
|
||||||
position: fixed !important;
|
|
||||||
top: 50px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
height: auto;
|
|
||||||
z-index: 8;
|
|
||||||
border-right: none !important;
|
|
||||||
border-bottom-right-radius: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.EasyMDEContainer .CodeMirror-sided {
|
|
||||||
width: 50% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.EasyMDEContainer.sided--no-fullscreen .CodeMirror-sided {
|
|
||||||
border-right: none !important;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
position: relative;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.EasyMDEContainer .CodeMirror-placeholder {
|
|
||||||
opacity: .5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.EasyMDEContainer .CodeMirror-focused .CodeMirror-selected {
|
|
||||||
background: #d9d9d9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar {
|
|
||||||
position: relative;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
-o-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
padding: 9px 10px;
|
|
||||||
border-top: 1px solid #ced4da;
|
|
||||||
border-left: 1px solid #ced4da;
|
|
||||||
border-right: 1px solid #ced4da;
|
|
||||||
border-top-left-radius: 4px;
|
|
||||||
border-top-right-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar.fullscreen {
|
|
||||||
width: 100%;
|
|
||||||
height: 50px;
|
|
||||||
padding-top: 10px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background: #fff;
|
|
||||||
border: 0;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
opacity: 1;
|
|
||||||
z-index: 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar.fullscreen::before {
|
|
||||||
width: 20px;
|
|
||||||
height: 50px;
|
|
||||||
background: -moz-linear-gradient(left, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
|
|
||||||
background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 1)), color-stop(100%, rgba(255, 255, 255, 0)));
|
|
||||||
background: -webkit-linear-gradient(left, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
|
|
||||||
background: -o-linear-gradient(left, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
|
|
||||||
background: -ms-linear-gradient(left, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
|
|
||||||
background: linear-gradient(to right, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar.fullscreen::after {
|
|
||||||
width: 20px;
|
|
||||||
height: 50px;
|
|
||||||
background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%);
|
|
||||||
background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(100%, rgba(255, 255, 255, 1)));
|
|
||||||
background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%);
|
|
||||||
background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%);
|
|
||||||
background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%);
|
|
||||||
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%);
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.EasyMDEContainer.sided--no-fullscreen .editor-toolbar {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar button, .editor-toolbar .easymde-dropdown {
|
|
||||||
background: transparent;
|
|
||||||
display: inline-block;
|
|
||||||
text-align: center;
|
|
||||||
text-decoration: none !important;
|
|
||||||
height: 30px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar button {
|
|
||||||
font-weight: bold;
|
|
||||||
min-width: 30px;
|
|
||||||
padding: 0 6px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar button.active,
|
|
||||||
.editor-toolbar button:hover {
|
|
||||||
background: #fcfcfc;
|
|
||||||
border-color: #95a5a6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar i.separator {
|
|
||||||
display: inline-block;
|
|
||||||
width: 0;
|
|
||||||
border-left: 1px solid #d9d9d9;
|
|
||||||
border-right: 1px solid #fff;
|
|
||||||
color: transparent;
|
|
||||||
text-indent: -10px;
|
|
||||||
margin: 0 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar button:after {
|
|
||||||
font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
|
|
||||||
font-size: 65%;
|
|
||||||
vertical-align: text-bottom;
|
|
||||||
position: relative;
|
|
||||||
top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar button.heading-1:after {
|
|
||||||
content: "1";
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar button.heading-2:after {
|
|
||||||
content: "2";
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar button.heading-3:after {
|
|
||||||
content: "3";
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar button.heading-bigger:after {
|
|
||||||
content: "▲";
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar button.heading-smaller:after {
|
|
||||||
content: "▼";
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar.disabled-for-preview button:not(.no-disable) {
|
|
||||||
opacity: .6;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 700px) {
|
|
||||||
.editor-toolbar i.no-mobile {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-statusbar {
|
|
||||||
padding: 8px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #959694;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.EasyMDEContainer.sided--no-fullscreen .editor-statusbar {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-statusbar span {
|
|
||||||
display: inline-block;
|
|
||||||
min-width: 4em;
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-statusbar .lines:before {
|
|
||||||
content: 'lines: '
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-statusbar .words:before {
|
|
||||||
content: 'words: '
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-statusbar .characters:before {
|
|
||||||
content: 'characters: '
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-preview-full {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 7;
|
|
||||||
overflow: auto;
|
|
||||||
display: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-preview-side {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
width: 50%;
|
|
||||||
top: 50px;
|
|
||||||
right: 0;
|
|
||||||
z-index: 9;
|
|
||||||
overflow: auto;
|
|
||||||
display: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-preview-active-side {
|
|
||||||
display: block
|
|
||||||
}
|
|
||||||
|
|
||||||
.EasyMDEContainer.sided--no-fullscreen .editor-preview-active-side {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
height: auto;
|
|
||||||
position: static;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-preview-active {
|
|
||||||
display: block
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-preview {
|
|
||||||
padding: 10px;
|
|
||||||
background: #fafafa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-preview > p {
|
|
||||||
margin-top: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-preview pre {
|
|
||||||
background: #eee;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-preview table td,
|
|
||||||
.editor-preview table th {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-easymde .cm-tag {
|
|
||||||
color: #63a35c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-easymde .cm-attribute {
|
|
||||||
color: #795da3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-easymde .cm-string {
|
|
||||||
color: #183691;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-easymde .cm-header-1 {
|
|
||||||
font-size: calc(1.375rem + 1.5vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-easymde .cm-header-2 {
|
|
||||||
font-size: calc(1.325rem + .9vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-easymde .cm-header-3 {
|
|
||||||
font-size: calc(1.3rem + .6vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-easymde .cm-header-4 {
|
|
||||||
font-size: calc(1.275rem + .3vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-easymde .cm-header-5 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-easymde .cm-header-6 {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-easymde .cm-header-1,
|
|
||||||
.cm-s-easymde .cm-header-2,
|
|
||||||
.cm-s-easymde .cm-header-3,
|
|
||||||
.cm-s-easymde .cm-header-4,
|
|
||||||
.cm-s-easymde .cm-header-5,
|
|
||||||
.cm-s-easymde .cm-header-6 {
|
|
||||||
margin-bottom: .5rem;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-easymde .cm-comment {
|
|
||||||
background: rgba(0, 0, 0, .05);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-easymde .cm-link {
|
|
||||||
color: #7f8c8d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-easymde .cm-url {
|
|
||||||
color: #aab2b3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-s-easymde .cm-quote {
|
|
||||||
color: #7f8c8d;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar .easymde-dropdown {
|
|
||||||
position: relative;
|
|
||||||
background: linear-gradient(to bottom right, #fff 0%, #fff 84%, #333 50%, #333 100%);
|
|
||||||
border-radius: 0;
|
|
||||||
border: 1px solid #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar .easymde-dropdown:hover {
|
|
||||||
background: linear-gradient(to bottom right, #fff 0%, #fff 84%, #333 50%, #333 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.easymde-dropdown-content {
|
|
||||||
display: block;
|
|
||||||
visibility: hidden;
|
|
||||||
position: absolute;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
|
|
||||||
padding: 8px;
|
|
||||||
z-index: 2;
|
|
||||||
top: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.easymde-dropdown:active .easymde-dropdown-content,
|
|
||||||
.easymde-dropdown:focus .easymde-dropdown-content,
|
|
||||||
.easymde-dropdown:focus-within .easymde-dropdown-content {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.easymde-dropdown-content button {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
span[data-img-src]::after {
|
|
||||||
content: '';
|
|
||||||
/*noinspection CssUnresolvedCustomProperty, added through JS*/
|
|
||||||
background-image: var(--bg-image);
|
|
||||||
display: block;
|
|
||||||
max-height: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
background-size: contain;
|
|
||||||
height: 0;
|
|
||||||
/*noinspection CssUnresolvedCustomProperty, added through JS*/
|
|
||||||
padding-top: var(--height);
|
|
||||||
/*noinspection CssUnresolvedCustomProperty, added through JS*/
|
|
||||||
width: var(--width);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
|
@ -0,0 +1,241 @@
|
|||||||
|
/* eslint-disable sort-keys,@typescript-eslint/member-ordering,max-classes-per-file */
|
||||||
|
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||||
|
// import { languages } from "@codemirror/language-data"; // Costs 800KB, probably should be a manual plugin
|
||||||
|
import {
|
||||||
|
HighlightStyle,
|
||||||
|
defaultHighlightStyle,
|
||||||
|
syntaxHighlighting,
|
||||||
|
// HighlightStyle,
|
||||||
|
// tags
|
||||||
|
} from '@codemirror/language';
|
||||||
|
import { EditorState } from '@codemirror/state';
|
||||||
|
import { drawSelection, EditorView } from '@codemirror/view';
|
||||||
|
import { tags } from '@lezer/highlight';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
|
import { InputOptions, Options } from './options';
|
||||||
|
|
||||||
|
import './styles.scss';
|
||||||
|
import { importDefaultToolbar, importToolbar } from '.';
|
||||||
|
|
||||||
|
class NotConstructedError extends Error {
|
||||||
|
public constructor() {
|
||||||
|
super(
|
||||||
|
'EasyMDE is not initialized, run the "construct()" method to do so.',
|
||||||
|
);
|
||||||
|
this.name = 'NotConstructedError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AlreadyConstructedError extends Error {
|
||||||
|
public constructor() {
|
||||||
|
super('EasyMDE is already initialized.');
|
||||||
|
this.name = 'AlreadyConstructedError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EasyMDE {
|
||||||
|
private readonly element: HTMLTextAreaElement;
|
||||||
|
private _container?: HTMLDivElement;
|
||||||
|
private _codemirror?: EditorView;
|
||||||
|
// private rendered = false;
|
||||||
|
private readonly _options: Options;
|
||||||
|
|
||||||
|
private readonly plugins: IEasyMDEPlugin[] = [];
|
||||||
|
|
||||||
|
public constructor(options: InputOptions) {
|
||||||
|
this._options = {
|
||||||
|
...options,
|
||||||
|
blockStyles: {
|
||||||
|
bold: '**',
|
||||||
|
italic: '*',
|
||||||
|
strikethrough: '~~',
|
||||||
|
code: '`',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.element = EasyMDE.verifyAndReturnElement(options.element);
|
||||||
|
marked.parse('# EasyMDE');
|
||||||
|
this.construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get container(): HTMLDivElement {
|
||||||
|
if (!this._container) {
|
||||||
|
throw new NotConstructedError();
|
||||||
|
}
|
||||||
|
return this._container;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get codemirror(): EditorView {
|
||||||
|
if (!this._codemirror) {
|
||||||
|
throw new NotConstructedError();
|
||||||
|
}
|
||||||
|
return this._codemirror;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get options(): Readonly<Options> {
|
||||||
|
return Object.freeze(this._options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static verifyAndReturnElement(
|
||||||
|
element?: HTMLElement,
|
||||||
|
): HTMLTextAreaElement {
|
||||||
|
if (!element) {
|
||||||
|
throw new Error('EasyMDE: Parameter "element" is null.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(element instanceof HTMLTextAreaElement)) {
|
||||||
|
throw new TypeError(
|
||||||
|
'EasyMDE: Parameter "element" must be a TextArea.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isRendered(): boolean {
|
||||||
|
return Boolean(this.container && this.codemirror);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async construct(): Promise<void> {
|
||||||
|
if (this._container && this._codemirror) {
|
||||||
|
throw new AlreadyConstructedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Customize the markdown highlight style.
|
||||||
|
const highlightStyle = HighlightStyle.define([
|
||||||
|
{
|
||||||
|
tag: tags.heading1,
|
||||||
|
fontSize: '200%',
|
||||||
|
lineHeight: '200%',
|
||||||
|
textDecoration: 'none',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: tags.heading2,
|
||||||
|
fontSize: '160%',
|
||||||
|
lineHeight: '160%',
|
||||||
|
textDecoration: 'none',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: tags.heading3,
|
||||||
|
fontSize: '125%',
|
||||||
|
lineHeight: '125%',
|
||||||
|
textDecoration: 'none',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: tags.heading4,
|
||||||
|
fontSize: '110%',
|
||||||
|
lineHeight: '110%',
|
||||||
|
textDecoration: 'none',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: tags.heading5,
|
||||||
|
fontSize: '105%',
|
||||||
|
lineHeight: '105%',
|
||||||
|
textDecoration: 'none',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: tags.heading6,
|
||||||
|
fontSize: '100%',
|
||||||
|
lineHeight: '100%',
|
||||||
|
textDecoration: 'none',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: tags.monospace,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
textDecoration: 'none',
|
||||||
|
background: 'rgba(0, 0, 0, 0.05)',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.element.hidden = true;
|
||||||
|
this._codemirror = new EditorView({
|
||||||
|
state: EditorState.create({
|
||||||
|
doc: this.element.value,
|
||||||
|
extensions: [
|
||||||
|
syntaxHighlighting(highlightStyle),
|
||||||
|
syntaxHighlighting(defaultHighlightStyle),
|
||||||
|
markdown({
|
||||||
|
base: markdownLanguage,
|
||||||
|
// codeLanguages: languages,
|
||||||
|
}),
|
||||||
|
drawSelection(),
|
||||||
|
],
|
||||||
|
selection: {
|
||||||
|
anchor: this.element.value.length,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
// parent: this.element.parentElement || document.body,
|
||||||
|
});
|
||||||
|
|
||||||
|
const easyMDEContainer = this.createContainer();
|
||||||
|
|
||||||
|
if (this.options.toolbar !== false) {
|
||||||
|
easyMDEContainer.append(await this.createToolbar());
|
||||||
|
}
|
||||||
|
|
||||||
|
easyMDEContainer.append(this.codemirror.dom);
|
||||||
|
|
||||||
|
if (this.options.statusbar !== false) {
|
||||||
|
easyMDEContainer.append(await this.createStatusBar());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.element.insertAdjacentElement('afterend', easyMDEContainer);
|
||||||
|
|
||||||
|
this.codemirror.focus();
|
||||||
|
|
||||||
|
this._container = easyMDEContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public destruct(): void {
|
||||||
|
this.element.value = this.codemirror.state.doc.toString();
|
||||||
|
|
||||||
|
for (const plugin of this.plugins) {
|
||||||
|
plugin.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.codemirror.destroy();
|
||||||
|
this.container.remove();
|
||||||
|
|
||||||
|
this._container = undefined;
|
||||||
|
this._codemirror = undefined;
|
||||||
|
|
||||||
|
this.element.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async addPlugin(plugin: IEasyMDEPlugin): Promise<IEasyMDEPlugin> {
|
||||||
|
this.plugins.push(plugin);
|
||||||
|
return plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createToolbar(): Promise<HTMLDivElement> {
|
||||||
|
const [{ Toolbar }, { defaultToolbar }] = await Promise.all([
|
||||||
|
importToolbar(),
|
||||||
|
importDefaultToolbar(),
|
||||||
|
]);
|
||||||
|
const toolbar = new Toolbar(this, defaultToolbar);
|
||||||
|
await this.addPlugin(toolbar);
|
||||||
|
// await toolbar.build(defaultToolbar);
|
||||||
|
return toolbar.element;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createStatusBar(): Promise<HTMLDivElement> {
|
||||||
|
const { StatusBar } = await import('./status-bar/status-bar');
|
||||||
|
const statusBar = new StatusBar(this);
|
||||||
|
return statusBar.element;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createContainer(): HTMLDivElement {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.classList.add('easymde-container');
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IEasyMDEPluginClass = new (easyMDE: EasyMDE) => IEasyMDEPlugin;
|
||||||
|
|
||||||
|
export interface IEasyMDEPlugin {
|
||||||
|
// new (editor: EasyMDE, ...args: any): IEasyMDEPlugin;
|
||||||
|
build(arguments_: any): Promise<void>;
|
||||||
|
|
||||||
|
destroy(): Promise<void>;
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import { EasyMDE } from '../easymde';
|
||||||
|
import { toggleBlock } from '../utils/toggle-block';
|
||||||
|
|
||||||
|
export const toggleBold = (editor: EasyMDE) =>
|
||||||
|
toggleBlock(editor.codemirror, '**');
|
@ -0,0 +1,5 @@
|
|||||||
|
import { EasyMDE } from '../easymde';
|
||||||
|
import { toggleBlock } from '../utils/toggle-block';
|
||||||
|
|
||||||
|
export const toggleItalic = (editor: EasyMDE) =>
|
||||||
|
toggleBlock(editor.codemirror, '*');
|
@ -0,0 +1,4 @@
|
|||||||
|
export { EasyMDE } from './easymde';
|
||||||
|
|
||||||
|
export const importToolbar = () => import('./toolbar/toolbar');
|
||||||
|
export const importDefaultToolbar = () => import('./toolbar/default-toolbar');
|
@ -1,42 +0,0 @@
|
|||||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
|
||||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
|
||||||
|
|
||||||
var CodeMirror = require('codemirror');
|
|
||||||
|
|
||||||
CodeMirror.commands.tabAndIndentMarkdownList = function (cm) {
|
|
||||||
var ranges = cm.listSelections();
|
|
||||||
var pos = ranges[0].head;
|
|
||||||
var eolState = cm.getStateAfter(pos.line);
|
|
||||||
var inList = eolState.list !== false;
|
|
||||||
|
|
||||||
if (inList) {
|
|
||||||
cm.execCommand('indentMore');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cm.options.indentWithTabs) {
|
|
||||||
cm.execCommand('insertTab');
|
|
||||||
} else {
|
|
||||||
var spaces = Array(cm.options.tabSize + 1).join(' ');
|
|
||||||
cm.replaceSelection(spaces);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
CodeMirror.commands.shiftTabAndUnindentMarkdownList = function (cm) {
|
|
||||||
var ranges = cm.listSelections();
|
|
||||||
var pos = ranges[0].head;
|
|
||||||
var eolState = cm.getStateAfter(pos.line);
|
|
||||||
var inList = eolState.list !== false;
|
|
||||||
|
|
||||||
if (inList) {
|
|
||||||
cm.execCommand('indentLess');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cm.options.indentWithTabs) {
|
|
||||||
cm.execCommand('insertTab');
|
|
||||||
} else {
|
|
||||||
var spaces = Array(cm.options.tabSize + 1).join(' ');
|
|
||||||
cm.replaceSelection(spaces);
|
|
||||||
}
|
|
||||||
};
|
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,239 @@
|
|||||||
|
import { marked } from 'marked';
|
||||||
|
|
||||||
|
import { EasyMDE } from './easymde';
|
||||||
|
|
||||||
|
interface ArrayOneOrMore<T> extends Array<T> {
|
||||||
|
0: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ToolbarButton =
|
||||||
|
| 'bold'
|
||||||
|
| 'italic'
|
||||||
|
| 'quote'
|
||||||
|
| 'unordered-list'
|
||||||
|
| 'ordered-list'
|
||||||
|
| 'link'
|
||||||
|
| 'image'
|
||||||
|
| 'strikethrough'
|
||||||
|
| 'code'
|
||||||
|
| 'table'
|
||||||
|
| 'redo'
|
||||||
|
| 'heading'
|
||||||
|
| 'undo'
|
||||||
|
| 'heading-bigger'
|
||||||
|
| 'heading-smaller'
|
||||||
|
| 'heading-1'
|
||||||
|
| 'heading-2'
|
||||||
|
| 'heading-3'
|
||||||
|
| 'clean-block'
|
||||||
|
| 'horizontal-rule'
|
||||||
|
| 'preview'
|
||||||
|
| 'side-by-side'
|
||||||
|
| 'fullscreen'
|
||||||
|
| 'guide';
|
||||||
|
|
||||||
|
interface TimeFormatOptions {
|
||||||
|
locale?: string | string[];
|
||||||
|
format?: Intl.DateTimeFormatOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AutoSaveOptions {
|
||||||
|
enabled?: boolean;
|
||||||
|
delay?: number;
|
||||||
|
submit_delay?: number;
|
||||||
|
uniqueId: string;
|
||||||
|
timeFormat?: TimeFormatOptions;
|
||||||
|
text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlockStyleOptions {
|
||||||
|
bold?: string;
|
||||||
|
code?: string;
|
||||||
|
strikethrough?: string;
|
||||||
|
italic?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CustomAttributes {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InsertTextOptions {
|
||||||
|
horizontalRule?: readonly string[];
|
||||||
|
image?: readonly string[];
|
||||||
|
link?: readonly string[];
|
||||||
|
table?: readonly string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParsingOptions {
|
||||||
|
allowAtxHeaderWithoutSpace?: boolean;
|
||||||
|
strikethrough?: boolean;
|
||||||
|
underscoresBreakWords?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PromptTexts {
|
||||||
|
image?: string;
|
||||||
|
link?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RenderingOptions {
|
||||||
|
codeSyntaxHighlighting?: boolean;
|
||||||
|
hljs?: any;
|
||||||
|
markedOptions?: marked.MarkedOptions;
|
||||||
|
sanitizerFunction?: (html: string) => string;
|
||||||
|
singleLineBreaks?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Shortcuts {
|
||||||
|
[action: string]: string | undefined | null;
|
||||||
|
|
||||||
|
toggleBlockquote?: string | null;
|
||||||
|
toggleBold?: string | null;
|
||||||
|
cleanBlock?: string | null;
|
||||||
|
toggleHeadingSmaller?: string | null;
|
||||||
|
toggleItalic?: string | null;
|
||||||
|
drawLink?: string | null;
|
||||||
|
toggleUnorderedList?: string | null;
|
||||||
|
togglePreview?: string | null;
|
||||||
|
toggleCodeBlock?: string | null;
|
||||||
|
drawImage?: string | null;
|
||||||
|
toggleOrderedList?: string | null;
|
||||||
|
toggleHeadingBigger?: string | null;
|
||||||
|
toggleSideBySide?: string | null;
|
||||||
|
toggleFullScreen?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatusBarItem {
|
||||||
|
className: string;
|
||||||
|
defaultValue: (element: HTMLElement) => void;
|
||||||
|
onUpdate: (element: HTMLElement) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolbarDropdownIcon {
|
||||||
|
name: string;
|
||||||
|
children: ArrayOneOrMore<ToolbarIcon | ToolbarButton>;
|
||||||
|
className: string;
|
||||||
|
title: string;
|
||||||
|
noDisable?: boolean;
|
||||||
|
noMobile?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolbarIcon {
|
||||||
|
name: string;
|
||||||
|
action: string | ((editor: EasyMDE) => void);
|
||||||
|
className: string;
|
||||||
|
title: string;
|
||||||
|
noDisable?: boolean;
|
||||||
|
noMobile?: boolean;
|
||||||
|
icon?: string;
|
||||||
|
attributes?: CustomAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImageTextsOptions {
|
||||||
|
sbInit?: string;
|
||||||
|
sbOnDragEnter?: string;
|
||||||
|
sbOnDrop?: string;
|
||||||
|
sbProgress?: string;
|
||||||
|
sbOnUploaded?: string;
|
||||||
|
sizeUnits?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImageErrorTextsOptions {
|
||||||
|
noFileGiven?: string;
|
||||||
|
typeNotAllowed?: string;
|
||||||
|
fileTooLarge?: string;
|
||||||
|
importError?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OverlayModeOptions {
|
||||||
|
// mode: CodeMirror.Mode<any>;
|
||||||
|
combine?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||||
|
interface SpellCheckerOptions {
|
||||||
|
// codeMirrorInstance: CodeMirror.Editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InputOptions {
|
||||||
|
autoDownloadFontAwesome?: boolean;
|
||||||
|
autofocus?: boolean;
|
||||||
|
autosave?: AutoSaveOptions;
|
||||||
|
autoRefresh?: boolean | { delay: number };
|
||||||
|
blockStyles?: BlockStyleOptions;
|
||||||
|
element: HTMLTextAreaElement;
|
||||||
|
forceSync?: boolean;
|
||||||
|
hideIcons?: readonly string[];
|
||||||
|
indentWithTabs?: boolean;
|
||||||
|
initialValue?: string;
|
||||||
|
insertTexts?: InsertTextOptions;
|
||||||
|
lineNumbers?: boolean;
|
||||||
|
lineWrapping?: boolean;
|
||||||
|
minHeight?: string;
|
||||||
|
maxHeight?: string;
|
||||||
|
parsingConfig?: ParsingOptions;
|
||||||
|
placeholder?: string;
|
||||||
|
previewClass?: string | readonly string[];
|
||||||
|
previewImagesInEditor?: boolean;
|
||||||
|
previewRender?: (
|
||||||
|
markdownPlaintext: string,
|
||||||
|
previewElement: HTMLElement,
|
||||||
|
) => string;
|
||||||
|
promptURLs?: boolean;
|
||||||
|
renderingConfig?: RenderingOptions;
|
||||||
|
shortcuts?: Shortcuts;
|
||||||
|
showIcons?: readonly ToolbarButton[];
|
||||||
|
spellChecker?: boolean | ((options: SpellCheckerOptions) => void);
|
||||||
|
inputStyle?: 'textarea' | 'contenteditable';
|
||||||
|
nativeSpellcheck?: boolean;
|
||||||
|
sideBySideFullscreen?: boolean;
|
||||||
|
status?: boolean | ReadonlyArray<string | StatusBarItem>;
|
||||||
|
styleSelectedText?: boolean;
|
||||||
|
tabSize?: number;
|
||||||
|
toolbar?:
|
||||||
|
| boolean
|
||||||
|
| ReadonlyArray<
|
||||||
|
'|' | ToolbarButton | ToolbarIcon | ToolbarDropdownIcon
|
||||||
|
>;
|
||||||
|
toolbarTips?: boolean;
|
||||||
|
onToggleFullScreen?: (goingIntoFullScreen: boolean) => void;
|
||||||
|
theme?: string;
|
||||||
|
scrollbarStyle?: string;
|
||||||
|
unorderedListStyle?: '*' | '-' | '+';
|
||||||
|
|
||||||
|
uploadImage?: boolean;
|
||||||
|
imageMaxSize?: number;
|
||||||
|
imageAccept?: string;
|
||||||
|
imageUploadFunction?: (
|
||||||
|
file: File,
|
||||||
|
onSuccess: (url: string) => void,
|
||||||
|
onError: (error: string) => void,
|
||||||
|
) => void;
|
||||||
|
imageUploadEndpoint?: string;
|
||||||
|
imagePathAbsolute?: boolean;
|
||||||
|
imageCSRFToken?: string;
|
||||||
|
imageTexts?: ImageTextsOptions;
|
||||||
|
errorMessages?: ImageErrorTextsOptions;
|
||||||
|
errorCallback?: (errorMessage: string) => void;
|
||||||
|
|
||||||
|
promptTexts?: PromptTexts;
|
||||||
|
syncSideBySidePreviewScroll?: boolean;
|
||||||
|
|
||||||
|
overlayMode?: OverlayModeOptions;
|
||||||
|
|
||||||
|
direction?: 'ltr' | 'rtl';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Options {
|
||||||
|
statusbar?: boolean;
|
||||||
|
toolbar?:
|
||||||
|
| boolean
|
||||||
|
| ReadonlyArray<
|
||||||
|
'|' | ToolbarButton | ToolbarIcon | ToolbarDropdownIcon
|
||||||
|
>;
|
||||||
|
blockStyles: {
|
||||||
|
bold: string;
|
||||||
|
code: string;
|
||||||
|
strikethrough: string;
|
||||||
|
italic: string;
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,104 @@
|
|||||||
|
import { SelectionRange, StateEffect, Line } from '@codemirror/state';
|
||||||
|
import { ViewPlugin, ViewUpdate } from '@codemirror/view';
|
||||||
|
|
||||||
|
import { EasyMDE } from '../easymde';
|
||||||
|
import { countWords } from '../utils/count-words';
|
||||||
|
|
||||||
|
export class StatusBar {
|
||||||
|
public element: HTMLDivElement;
|
||||||
|
|
||||||
|
private characterCount = 0;
|
||||||
|
private wordCount = 0;
|
||||||
|
private lineCount = 1;
|
||||||
|
private cursorLine = 1;
|
||||||
|
private cursorColumn = 1;
|
||||||
|
|
||||||
|
private selectionStart = 0;
|
||||||
|
private selectionEnd = 0;
|
||||||
|
|
||||||
|
public constructor(private editor: EasyMDE) {
|
||||||
|
this.element = document.createElement('div');
|
||||||
|
this.element.className = 'easymde-statusbar';
|
||||||
|
|
||||||
|
// Initial values
|
||||||
|
this.characterCount = this.editor.codemirror.state.doc.length;
|
||||||
|
this.wordCount = countWords(this.editor.codemirror.state.doc);
|
||||||
|
this.lineCount = this.editor.codemirror.state.doc.lines;
|
||||||
|
|
||||||
|
const line = this.editor.codemirror.state.doc.lineAt(
|
||||||
|
this.editor.codemirror.state.selection.main.to,
|
||||||
|
);
|
||||||
|
this.cursorLine = line.number;
|
||||||
|
this.cursorColumn =
|
||||||
|
this.editor.codemirror.state.selection.main.to - line.from + 1;
|
||||||
|
this.selectionStart = this.editor.codemirror.state.selection.main.from;
|
||||||
|
this.selectionEnd = this.editor.codemirror.state.selection.main.to;
|
||||||
|
|
||||||
|
this.editor.codemirror.dispatch({
|
||||||
|
effects: StateEffect.appendConfig.of(
|
||||||
|
ViewPlugin.define(() => ({
|
||||||
|
update: (update: ViewUpdate) => {
|
||||||
|
const document = update.state.doc;
|
||||||
|
const selection = update.state.selection.main;
|
||||||
|
|
||||||
|
this.characterCount = document.length;
|
||||||
|
this.wordCount = countWords(document);
|
||||||
|
this.lineCount = document.lines;
|
||||||
|
|
||||||
|
const direction = this.getSelectionDirection(selection);
|
||||||
|
const toLine = document.lineAt(selection.to);
|
||||||
|
const fromLine = document.lineAt(selection.from);
|
||||||
|
|
||||||
|
let cursorLine: Line;
|
||||||
|
|
||||||
|
if (direction === 'left') {
|
||||||
|
// Cursor is at the start of the selection.
|
||||||
|
cursorLine = fromLine;
|
||||||
|
this.cursorColumn =
|
||||||
|
selection.from - cursorLine.from;
|
||||||
|
} else {
|
||||||
|
// Cursor is at the end of the selection, or there is no selection.
|
||||||
|
cursorLine = toLine;
|
||||||
|
this.cursorColumn = selection.to - cursorLine.from;
|
||||||
|
|
||||||
|
if (this.cursorColumn > toLine.length) {
|
||||||
|
// Column is incorrect, can happen when Ctrl+A is used. We need to manually adjust it.
|
||||||
|
this.cursorColumn = toLine.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cursorLine = cursorLine.number;
|
||||||
|
this.selectionStart = selection.from;
|
||||||
|
this.selectionEnd = selection.to;
|
||||||
|
|
||||||
|
// We start counting columns at 1.
|
||||||
|
this.cursorColumn++;
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
this.element.innerHTML = `
|
||||||
|
<span class="status-bar-element">Lines: ${this.lineCount}</span>
|
||||||
|
<span class="status-bar-element">Words: ${this.wordCount}</span>
|
||||||
|
<span class="status-bar-element">Characters: ${this.characterCount}</span>
|
||||||
|
<span class="status-bar-element">Pos: ${this.cursorLine}:${this.cursorColumn}</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSelectionDirection(
|
||||||
|
selection: SelectionRange,
|
||||||
|
): 'right' | 'left' | undefined {
|
||||||
|
return selection.from === this.selectionStart
|
||||||
|
? 'right'
|
||||||
|
: selection.to === this.selectionEnd
|
||||||
|
? 'left'
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,106 @@
|
|||||||
|
$light-border-color: #d1d1d1;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--easymde-border-color: #{$light-border-color};
|
||||||
|
--easymde-enabled-color: #{lighten($light-border-color, 5%)};
|
||||||
|
--easymde-hover-color: #{$light-border-color};
|
||||||
|
--easymde-active-color: #{darken($light-border-color, 5%)};
|
||||||
|
}
|
||||||
|
|
||||||
|
// @media (prefers-color-scheme: dark) {
|
||||||
|
// :root {
|
||||||
|
// --easymde-border-color: blue;
|
||||||
|
// --easymde-hover-color: red;
|
||||||
|
// // --easymde-active-color: lighten(var(--easymde-hover-color), 10%);
|
||||||
|
// // --easymde-click-color: darken(var(--easymde-hover-color), 10%);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
.easymde-toolbar {
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
padding: 9px 10px;
|
||||||
|
border: 1px solid $light-border-color;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: transparent;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none !important;
|
||||||
|
height: 30px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 30px;
|
||||||
|
|
||||||
|
&.enabled {
|
||||||
|
background-color: var(--easymde-enabled-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--easymde-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--easymde-active-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span.separator {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0;
|
||||||
|
border-left: 1px solid var(--easymde-border-color);
|
||||||
|
border-right: 1px solid var(--easymde-border-color);
|
||||||
|
color: transparent;
|
||||||
|
text-indent: -10px;
|
||||||
|
margin: 0 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.easymde-container {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.easymde-toolbar {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-editor {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid var(--easymde-border-color);
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
font: inherit;
|
||||||
|
z-index: 0;
|
||||||
|
word-wrap: break-word;
|
||||||
|
|
||||||
|
&.cm-focused {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-scroller {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-content {
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.easymde-statusbar {
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #595959;
|
||||||
|
text-align: right;
|
||||||
|
// display: grid;
|
||||||
|
// grid-template-columns: repeat(4, 1fr);
|
||||||
|
|
||||||
|
.status-bar-element {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
import { ViewUpdate } from '@codemirror/view';
|
||||||
|
|
||||||
|
import { EasyMDE } from '../..';
|
||||||
|
import { IToolbarButtonOptions } from '../default-toolbar';
|
||||||
|
|
||||||
|
export class ToolbarButton implements IToolbarButtonOptions {
|
||||||
|
public action?: any;
|
||||||
|
public active?:
|
||||||
|
| boolean
|
||||||
|
| ((editor: EasyMDE, update: ViewUpdate) => boolean)
|
||||||
|
| ((editor: EasyMDE, update: ViewUpdate) => Promise<boolean>);
|
||||||
|
public icon = '';
|
||||||
|
public title = '';
|
||||||
|
private _name = '';
|
||||||
|
|
||||||
|
public get name() {
|
||||||
|
return this._name;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
import { ViewUpdate } from '@codemirror/view';
|
||||||
|
|
||||||
|
import { EasyMDE } from '../../easymde';
|
||||||
|
import { checkBlock, toggleBlock } from '../../utils/toggle-block';
|
||||||
|
import { IToolbarButtonOptions } from '../default-toolbar';
|
||||||
|
|
||||||
|
export const toggleBold = (editor: EasyMDE) =>
|
||||||
|
toggleBlock(editor.codemirror, editor.options.blockStyles.bold);
|
||||||
|
|
||||||
|
export const checkBold = (editor: EasyMDE, _update: ViewUpdate) =>
|
||||||
|
Boolean(checkBlock(editor.codemirror, editor.options.blockStyles.bold));
|
||||||
|
|
||||||
|
export const toggleBoldButton: IToolbarButtonOptions = {
|
||||||
|
action: toggleBold,
|
||||||
|
active: checkBold,
|
||||||
|
icon: 'fas fa-bold',
|
||||||
|
name: 'bold',
|
||||||
|
title: 'Bold',
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
import { EasyMDE } from '../../easymde';
|
||||||
|
import { toggleBlock } from '../../utils/toggle-block';
|
||||||
|
import { IToolbarButtonOptions } from '../default-toolbar';
|
||||||
|
|
||||||
|
export const toggleCode = (editor: EasyMDE) =>
|
||||||
|
toggleBlock(editor.codemirror, editor.options.blockStyles.code);
|
||||||
|
|
||||||
|
export const toggleCodeButton: IToolbarButtonOptions = {
|
||||||
|
action: toggleCode,
|
||||||
|
icon: 'fas fa-code',
|
||||||
|
name: 'code',
|
||||||
|
title: 'Code',
|
||||||
|
};
|
@ -0,0 +1,19 @@
|
|||||||
|
import { ViewUpdate } from '@codemirror/view';
|
||||||
|
|
||||||
|
import { EasyMDE } from '../../easymde';
|
||||||
|
import { checkBlock, toggleBlock } from '../../utils/toggle-block';
|
||||||
|
import { IToolbarButtonOptions } from '../default-toolbar';
|
||||||
|
|
||||||
|
export const toggleItalic = (editor: EasyMDE) =>
|
||||||
|
toggleBlock(editor.codemirror, editor.options.blockStyles.italic);
|
||||||
|
|
||||||
|
export const checkItalic = (editor: EasyMDE, _update: ViewUpdate) =>
|
||||||
|
Boolean(checkBlock(editor.codemirror, editor.options.blockStyles.italic));
|
||||||
|
|
||||||
|
export const toggleItalicButton: IToolbarButtonOptions = {
|
||||||
|
action: toggleItalic,
|
||||||
|
active: checkItalic,
|
||||||
|
icon: 'fas fa-italic',
|
||||||
|
name: 'italic',
|
||||||
|
title: 'Italic',
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
import { EasyMDE } from '../../easymde';
|
||||||
|
import { toggleBlock } from '../../utils/toggle-block';
|
||||||
|
import { IToolbarButtonOptions } from '../default-toolbar';
|
||||||
|
|
||||||
|
export const toggleStrikethrough = (editor: EasyMDE) =>
|
||||||
|
toggleBlock(editor.codemirror, editor.options.blockStyles.strikethrough);
|
||||||
|
|
||||||
|
export const toggleStrikethroughButton: IToolbarButtonOptions = {
|
||||||
|
action: toggleStrikethrough,
|
||||||
|
icon: 'fas fa-strikethrough',
|
||||||
|
name: 'strikethrough',
|
||||||
|
title: 'Strikethrough',
|
||||||
|
};
|
@ -0,0 +1,121 @@
|
|||||||
|
import { ViewUpdate } from '@codemirror/view';
|
||||||
|
|
||||||
|
import { EasyMDE } from '../easymde';
|
||||||
|
|
||||||
|
import { toggleBoldButton } from './buttons/toggle-bold';
|
||||||
|
import { toggleCodeButton } from './buttons/toggle-code';
|
||||||
|
import { toggleItalicButton } from './buttons/toggle-italic';
|
||||||
|
// import { toggleStrikethroughButton } from "./buttons/toggle-strikethrough";
|
||||||
|
|
||||||
|
export interface IToolbarButtonOptions {
|
||||||
|
action?: any;
|
||||||
|
active?:
|
||||||
|
| boolean
|
||||||
|
| ((editor: EasyMDE, update: ViewUpdate) => boolean)
|
||||||
|
| ((editor: EasyMDE, update: ViewUpdate) => Promise<boolean>);
|
||||||
|
icon: string;
|
||||||
|
readonly name: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultToolbar: IToolbarButtonOptions[][] = [
|
||||||
|
[
|
||||||
|
toggleBoldButton,
|
||||||
|
toggleItalicButton,
|
||||||
|
// toggleStrikethroughButton,
|
||||||
|
{
|
||||||
|
// action: toggleHeadingSmaller,
|
||||||
|
icon: 'fas fa-header fa-heading',
|
||||||
|
name: 'heading',
|
||||||
|
title: 'Heading',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
toggleCodeButton,
|
||||||
|
{
|
||||||
|
// action: toggleBlockquote,
|
||||||
|
icon: 'fas fa-quote-left',
|
||||||
|
name: 'quote',
|
||||||
|
title: 'Quote',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// action: toggleUnorderedList,
|
||||||
|
icon: 'fas fa-list-ul',
|
||||||
|
name: 'unordered-list',
|
||||||
|
title: 'Generic List',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// action: toggleOrderedList,
|
||||||
|
icon: 'fas fa-list-ol',
|
||||||
|
name: 'ordered-list',
|
||||||
|
title: 'Numbered List',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// action: cleanBlock,
|
||||||
|
icon: 'fas fa-eraser',
|
||||||
|
name: 'clean-block',
|
||||||
|
title: 'Clean block',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
// action: drawLink,
|
||||||
|
icon: 'fas fa-link',
|
||||||
|
name: 'link',
|
||||||
|
title: 'Create Link',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// action: drawImage,
|
||||||
|
icon: 'fas fa-image',
|
||||||
|
name: 'image',
|
||||||
|
title: 'Insert Image',
|
||||||
|
// }, {
|
||||||
|
// // action: drawHorizontalRule,
|
||||||
|
// icon: 'fas fa-minus',
|
||||||
|
// name: 'horizontal-rule',
|
||||||
|
// title: 'Insert Horizontal Line',
|
||||||
|
// }], [{
|
||||||
|
// action: NewMDE.togglePreview,
|
||||||
|
// icon: 'fas fa-eye',
|
||||||
|
// name: 'preview',
|
||||||
|
// // noDisable: true,
|
||||||
|
// // noMobile: true,
|
||||||
|
// title: 'Toggle Preview',
|
||||||
|
// }, {
|
||||||
|
// action: NewMDE.toggleSideBySide,
|
||||||
|
// icon: 'fas fa-columns',
|
||||||
|
// name: 'side-by-side',
|
||||||
|
// // noDisable: true,
|
||||||
|
// // noMobile: true,
|
||||||
|
// title: 'Toggle Side by Side',
|
||||||
|
// }, {
|
||||||
|
// action: NewMDE.toggleFullScreen,
|
||||||
|
// icon: 'fas fa-arrows-alt',
|
||||||
|
// name: 'fullscreen',
|
||||||
|
// // noDisable: true,
|
||||||
|
// // noMobile: true,
|
||||||
|
// title: 'Toggle Fullscreen',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
action: 'https://simplemde.com/markdown-guide',
|
||||||
|
icon: 'fas fa-question',
|
||||||
|
name: 'guide',
|
||||||
|
// noDisable: true,
|
||||||
|
title: 'Markdown Guide',
|
||||||
|
// }], [{
|
||||||
|
// action: NewMDE.undo,
|
||||||
|
// icon: 'fas fa-undo',
|
||||||
|
// name: 'undo',
|
||||||
|
// // noDisable: true,
|
||||||
|
// title: 'Undo',
|
||||||
|
// }, {
|
||||||
|
// action: NewMDE.redo,
|
||||||
|
// icon: 'fas fa-repeat',
|
||||||
|
// name: 'redo',
|
||||||
|
// // noDisable: true,
|
||||||
|
// title: 'Redo',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
@ -0,0 +1,53 @@
|
|||||||
|
import { EasyMDE } from '../easymde';
|
||||||
|
|
||||||
|
interface CustomAttributes {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArrayOneOrMore<T> extends Array<T> {
|
||||||
|
0: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolbarButtonOptions {
|
||||||
|
name: string;
|
||||||
|
action: string | ((editor: EasyMDE) => void);
|
||||||
|
className: string;
|
||||||
|
title: string;
|
||||||
|
noDisable?: boolean;
|
||||||
|
noMobile?: boolean;
|
||||||
|
icon?: string;
|
||||||
|
attributes?: CustomAttributes | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolbarDropdownButtonOptions extends ToolbarButtonOptions {
|
||||||
|
children: ArrayOneOrMore<ToolbarButtonOptions>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ToolbarButton {
|
||||||
|
private readonly element = document.createElement('button');
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private options: ToolbarButtonOptions | ToolbarDropdownButtonOptions,
|
||||||
|
) {
|
||||||
|
this.setCustomAttributes();
|
||||||
|
this.element.setAttribute('type', 'button');
|
||||||
|
// Shortcuts
|
||||||
|
// Tooltip
|
||||||
|
if (options.noDisable) {
|
||||||
|
// Disable on previes
|
||||||
|
this.element.classList.add('no-disable');
|
||||||
|
}
|
||||||
|
if (options.noMobile) {
|
||||||
|
// Hide on mobile
|
||||||
|
this.element.classList.add('no-mobile');
|
||||||
|
}
|
||||||
|
this.element.tabIndex = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setCustomAttributes(): void {
|
||||||
|
const attributes = this.options.attributes || {};
|
||||||
|
for (const [key, value] of Object.entries(attributes)) {
|
||||||
|
this.element.setAttribute(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,150 @@
|
|||||||
|
import { StateEffect } from '@codemirror/state';
|
||||||
|
import { ViewPlugin, ViewUpdate } from '@codemirror/view';
|
||||||
|
|
||||||
|
import { EasyMDE, IEasyMDEPlugin } from '../easymde';
|
||||||
|
|
||||||
|
import { IToolbarButtonOptions } from './default-toolbar';
|
||||||
|
|
||||||
|
export class Toolbar implements IEasyMDEPlugin {
|
||||||
|
private static readonly activeClass = 'enabled';
|
||||||
|
|
||||||
|
public element: HTMLDivElement;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private editor: EasyMDE,
|
||||||
|
toolbarLayout: IToolbarButtonOptions[][],
|
||||||
|
) {
|
||||||
|
this.element = document.createElement('div');
|
||||||
|
this.element.className = 'easymde-toolbar';
|
||||||
|
|
||||||
|
for (const toolBarButtonSection of toolbarLayout) {
|
||||||
|
const toolBarSection: Array<HTMLButtonElement | HTMLSpanElement> =
|
||||||
|
[];
|
||||||
|
|
||||||
|
for (const toolBarButtonOptions of toolBarButtonSection) {
|
||||||
|
toolBarSection.push(
|
||||||
|
this.createToolBarButton(toolBarButtonOptions),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a separator if this is not the last toolbar section.
|
||||||
|
if (
|
||||||
|
toolbarLayout.indexOf(toolBarButtonSection) !==
|
||||||
|
toolbarLayout.length - 1
|
||||||
|
) {
|
||||||
|
toolBarSection.push(this.createToolBarSeparator());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const toolBarEntry of toolBarSection) {
|
||||||
|
this.element.append(toolBarEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editor.codemirror.dispatch();
|
||||||
|
|
||||||
|
// Add the toolbar to the editor.
|
||||||
|
|
||||||
|
// const cmWrapper = this.codemirror.getWrapperElement();
|
||||||
|
// if (cmWrapper.parentNode) {
|
||||||
|
// cmWrapper.parentNode.insertBefore(toolBar, cmWrapper);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return toolBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async build(toolbarLayout: IToolbarButtonOptions[][]) {
|
||||||
|
for (const toolBarButtonSection of toolbarLayout) {
|
||||||
|
const toolBarSection: Array<HTMLButtonElement | HTMLSpanElement> =
|
||||||
|
[];
|
||||||
|
|
||||||
|
for (const toolBarButtonOptions of toolBarButtonSection) {
|
||||||
|
toolBarSection.push(
|
||||||
|
this.createToolBarButton(toolBarButtonOptions),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a separator if this is not the last toolbar section.
|
||||||
|
if (
|
||||||
|
toolbarLayout.indexOf(toolBarButtonSection) !==
|
||||||
|
toolbarLayout.length - 1
|
||||||
|
) {
|
||||||
|
toolBarSection.push(this.createToolBarSeparator());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const toolBarEntry of toolBarSection) {
|
||||||
|
this.element.append(toolBarEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public async destroy() {
|
||||||
|
this.element.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
private createToolBarSeparator() {
|
||||||
|
const separatorElement = document.createElement('span');
|
||||||
|
separatorElement.className = 'separator';
|
||||||
|
separatorElement.innerHTML = '|';
|
||||||
|
return separatorElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createToolBarButton(
|
||||||
|
toolBarButtonOptions: IToolbarButtonOptions,
|
||||||
|
): HTMLButtonElement {
|
||||||
|
const buttonElement: HTMLButtonElement =
|
||||||
|
document.createElement('button');
|
||||||
|
buttonElement.tabIndex = -1;
|
||||||
|
buttonElement.classList.add(toolBarButtonOptions.name);
|
||||||
|
|
||||||
|
// Set the button tooltip.
|
||||||
|
buttonElement.title = toolBarButtonOptions.title;
|
||||||
|
|
||||||
|
// Set the button onclick action.
|
||||||
|
if (typeof toolBarButtonOptions.action === 'function') {
|
||||||
|
buttonElement.addEventListener('click', () =>
|
||||||
|
toolBarButtonOptions.action(this.editor),
|
||||||
|
);
|
||||||
|
// buttonElement.addEventListener()
|
||||||
|
} else if (typeof toolBarButtonOptions.action === 'string') {
|
||||||
|
buttonElement.addEventListener('click', () =>
|
||||||
|
window.open(toolBarButtonOptions.action),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof toolBarButtonOptions.active === 'boolean') {
|
||||||
|
buttonElement.classList.toggle(
|
||||||
|
Toolbar.activeClass,
|
||||||
|
toolBarButtonOptions.active,
|
||||||
|
);
|
||||||
|
} else if (typeof toolBarButtonOptions.active === 'function') {
|
||||||
|
this.editor.codemirror.dispatch({
|
||||||
|
effects: StateEffect.appendConfig.of(
|
||||||
|
ViewPlugin.define(() => ({
|
||||||
|
update: async (update: ViewUpdate) => {
|
||||||
|
if (
|
||||||
|
typeof toolBarButtonOptions.active ===
|
||||||
|
'function'
|
||||||
|
) {
|
||||||
|
const result =
|
||||||
|
await toolBarButtonOptions.active(
|
||||||
|
this.editor,
|
||||||
|
update,
|
||||||
|
);
|
||||||
|
buttonElement.classList.toggle(
|
||||||
|
Toolbar.activeClass,
|
||||||
|
result,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the button icon.
|
||||||
|
const buttonIcon = document.createElement('i');
|
||||||
|
buttonIcon.className = toolBarButtonOptions.icon;
|
||||||
|
|
||||||
|
buttonElement.append(buttonIcon);
|
||||||
|
return buttonElement;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import { Text } from '@codemirror/state';
|
||||||
|
|
||||||
|
export const countWords = (document: Text) =>
|
||||||
|
document
|
||||||
|
.toJSON()
|
||||||
|
.reduce(
|
||||||
|
(previous, current) =>
|
||||||
|
previous +
|
||||||
|
(current ? current.split(' ').filter(Boolean).length : 0),
|
||||||
|
0,
|
||||||
|
);
|
@ -0,0 +1,292 @@
|
|||||||
|
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||||
|
import { EditorState } from '@codemirror/state';
|
||||||
|
import { EditorView } from '@codemirror/view';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { checkBlock } from './toggle-block';
|
||||||
|
|
||||||
|
const getEditor = (
|
||||||
|
document: string,
|
||||||
|
selection?: { anchor: number; head?: number },
|
||||||
|
) =>
|
||||||
|
new EditorView({
|
||||||
|
state: EditorState.create({
|
||||||
|
doc: document,
|
||||||
|
extensions: [
|
||||||
|
markdown({
|
||||||
|
base: markdownLanguage,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
selection,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each(['*', '**', '`', '_', '__', '~~'])(
|
||||||
|
'checkBlock simple %s',
|
||||||
|
(character) => {
|
||||||
|
const wordSimple = `${character}foo${character}`;
|
||||||
|
const wordSimpleWithSpace = `${character} foo ${character}`;
|
||||||
|
const wordSquished = `bla${character}foo${character}bla`;
|
||||||
|
const wordMultiWord = `${character}foo${character} ${character}boo${character}`;
|
||||||
|
const wordMultiLine = `${character}foo${character}\n${character}boo${character}`;
|
||||||
|
const wordMultiTab = `${character}foo${character}\t${character}boo${character}`;
|
||||||
|
|
||||||
|
it('must detect an active block with selection point in the middle', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const anchor = Math.floor(wordSimple.length / 2);
|
||||||
|
const result = checkBlock(
|
||||||
|
getEditor(wordSimple, { anchor }),
|
||||||
|
character,
|
||||||
|
);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must detect an active block with selection point in the middle of a word', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const anchor = Math.floor(wordSquished.length / 2);
|
||||||
|
const result = checkBlock(
|
||||||
|
getEditor(wordSquished, { anchor }),
|
||||||
|
character,
|
||||||
|
);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must not detect an active block with selection point in the middle of a word surrounded by spaces', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const anchor = Math.floor(wordSimpleWithSpace.length / 2);
|
||||||
|
const result = checkBlock(
|
||||||
|
getEditor(wordSimpleWithSpace, { anchor }),
|
||||||
|
character,
|
||||||
|
);
|
||||||
|
expect(result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must detect an active block with selection point at the start', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const result = checkBlock(getEditor(wordSimple), character);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must detect an active block with selection point at the end', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const anchor = wordSimple.length;
|
||||||
|
const result = checkBlock(
|
||||||
|
getEditor(wordSimple, { anchor }),
|
||||||
|
character,
|
||||||
|
);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must detect an active block with partial selection range in the middle', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const anchor = Math.floor(wordSimple.length / 2);
|
||||||
|
const head = anchor + 1;
|
||||||
|
const result = checkBlock(
|
||||||
|
getEditor(wordSimple, { anchor, head }),
|
||||||
|
character,
|
||||||
|
);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must detect an active block with selection range of the full word', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const anchor = character.length;
|
||||||
|
const head = wordSimple.length - character.length;
|
||||||
|
const result = checkBlock(
|
||||||
|
getEditor(wordSimple, { anchor, head }),
|
||||||
|
character,
|
||||||
|
);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must detect an active block with selection range of the full text', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const head = wordSimple.length;
|
||||||
|
const result = checkBlock(
|
||||||
|
getEditor(wordSimple, { anchor: 0, head }),
|
||||||
|
character,
|
||||||
|
);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must detect an active block with selection on the first of multiple lines', () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
|
||||||
|
const anchor = Math.floor(wordMultiLine.split('\n')[0].length / 2);
|
||||||
|
const result = checkBlock(
|
||||||
|
getEditor(wordMultiLine, { anchor }),
|
||||||
|
character,
|
||||||
|
);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(result[1]).toBe('foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must detect an active block with selection on the second of multiple lines', () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
|
||||||
|
const multiLineSplit = wordMultiLine.split('\n');
|
||||||
|
const anchor =
|
||||||
|
Math.floor(multiLineSplit[1].length / 2) +
|
||||||
|
multiLineSplit[0].length;
|
||||||
|
const result = checkBlock(
|
||||||
|
getEditor(wordMultiLine, { anchor }),
|
||||||
|
character,
|
||||||
|
);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(result[1]).toBe('boo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must detect an active block with selection at the end of multiple lines', () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
|
||||||
|
const anchor = wordMultiLine.length;
|
||||||
|
const result = checkBlock(
|
||||||
|
getEditor(wordMultiLine, { anchor }),
|
||||||
|
character,
|
||||||
|
);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(result[1]).toBe('boo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must detect an active block with selection on the first of multiple words', () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
|
||||||
|
const anchor = Math.floor(wordMultiWord.split(' ')[0].length / 2);
|
||||||
|
const result = checkBlock(
|
||||||
|
getEditor(wordMultiWord, { anchor }),
|
||||||
|
character,
|
||||||
|
);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(result[1]).toBe('foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must detect an active block with selection on the second of multiple words', () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
|
||||||
|
const multiWordSplit = wordMultiWord.split(' ');
|
||||||
|
const anchor =
|
||||||
|
Math.floor(multiWordSplit[1].length / 2) +
|
||||||
|
multiWordSplit[0].length;
|
||||||
|
const result = checkBlock(
|
||||||
|
getEditor(wordMultiWord, { anchor }),
|
||||||
|
character,
|
||||||
|
);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(result[1]).toBe('boo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must detect an active block with selection at the end of multiple words', () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
|
||||||
|
const anchor = wordMultiWord.length;
|
||||||
|
const result = checkBlock(
|
||||||
|
getEditor(wordMultiWord, { anchor }),
|
||||||
|
character,
|
||||||
|
);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(result[1]).toBe('boo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must detect an active block with selection on the first of multiple words separated by tab', () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
|
||||||
|
const anchor = Math.floor(wordMultiTab.split('\t')[0].length / 2);
|
||||||
|
const result = checkBlock(
|
||||||
|
getEditor(wordMultiTab, { anchor }),
|
||||||
|
character,
|
||||||
|
);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(result[1]).toBe('foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must detect an active block with selection on the second of multiple words separated by tab', () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
|
||||||
|
const multiWordSplit = wordMultiTab.split('\t');
|
||||||
|
const anchor =
|
||||||
|
Math.floor(multiWordSplit[1].length / 2) +
|
||||||
|
multiWordSplit[0].length;
|
||||||
|
const result = checkBlock(
|
||||||
|
getEditor(wordMultiTab, { anchor }),
|
||||||
|
character,
|
||||||
|
);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(result[1]).toBe('boo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must detect an active block with selection at the end of multiple words separated by tab', () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
|
||||||
|
const anchor = wordMultiTab.length;
|
||||||
|
const result = checkBlock(
|
||||||
|
getEditor(wordMultiTab, { anchor }),
|
||||||
|
character,
|
||||||
|
);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(result[1]).toBe('boo');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('checkBlock special cases', () => {
|
||||||
|
it('must not detect an active block when another markdown block is used with more of the same characters', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const result = checkBlock(getEditor('**foo**', { anchor: 4 }), '*');
|
||||||
|
expect(result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must not detect an active block when another markdown block is used with more of the same characters in a bigger context', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const result = checkBlock(
|
||||||
|
getEditor('Some text **foo** more text', { anchor: 14 }),
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
expect(result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must detect an active block in a bigger context', () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
|
||||||
|
const result = checkBlock(
|
||||||
|
getEditor('Some text **foo** more text', { anchor: 14 }),
|
||||||
|
'**',
|
||||||
|
);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
expect(result[1]).toBe('foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must detect an active block when another markdown block is used with different characters', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const result = checkBlock(getEditor('__*foo*__', { anchor: 6 }), '*');
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('must not detect an active block when another markdown block is used with less of the same characters', () => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const result = checkBlock(getEditor('*foo*', { anchor: 3 }), '**');
|
||||||
|
expect(result).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(['*', '**'])(
|
||||||
|
'must detect an active block when the characters are part of another styling block',
|
||||||
|
(c) => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
|
const result = checkBlock(getEditor('***foo***', { anchor: 5 }), c);
|
||||||
|
expect(result).toBeTruthy();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,130 @@
|
|||||||
|
import { EditorSelection, EditorState } from '@codemirror/state';
|
||||||
|
import { EditorView } from '@codemirror/view';
|
||||||
|
import escapeStringRegexp from 'escape-string-regexp';
|
||||||
|
|
||||||
|
export const checkBlock = (
|
||||||
|
editor: EditorView,
|
||||||
|
characters: string,
|
||||||
|
): RegExpExecArray | null => {
|
||||||
|
// Checks whether the selection matches a block of formatted text.
|
||||||
|
|
||||||
|
const { state } = editor;
|
||||||
|
const { from, to } = getExpandedSelection(state, characters);
|
||||||
|
const text = state.sliceDoc(from, to);
|
||||||
|
const escapedCharacters = escapeStringRegexp(characters);
|
||||||
|
const regularExpression = new RegExp(
|
||||||
|
`^${escapedCharacters}(.*)${escapedCharacters}$`,
|
||||||
|
'gs',
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkResult = regularExpression.exec(text);
|
||||||
|
|
||||||
|
let doubleCharactersCheckResult = null;
|
||||||
|
let tripleCharactersCheckResult = null;
|
||||||
|
if (characters.length === 1) {
|
||||||
|
doubleCharactersCheckResult = checkBlock(editor, characters.repeat(2));
|
||||||
|
tripleCharactersCheckResult = checkBlock(editor, characters.repeat(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(checkResult &&
|
||||||
|
doubleCharactersCheckResult &&
|
||||||
|
tripleCharactersCheckResult) ||
|
||||||
|
(checkResult &&
|
||||||
|
!doubleCharactersCheckResult &&
|
||||||
|
tripleCharactersCheckResult) ||
|
||||||
|
(checkResult &&
|
||||||
|
!doubleCharactersCheckResult &&
|
||||||
|
!tripleCharactersCheckResult)
|
||||||
|
) {
|
||||||
|
return checkResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toggleBlock = (editor: EditorView, characters: string) => {
|
||||||
|
const { state } = editor;
|
||||||
|
const { from, to } = getExpandedSelection(state, characters);
|
||||||
|
const text = state.sliceDoc(from, to);
|
||||||
|
const textMatch = checkBlock(editor, characters);
|
||||||
|
|
||||||
|
editor.dispatch(
|
||||||
|
state.changeByRange(() =>
|
||||||
|
textMatch
|
||||||
|
? {
|
||||||
|
changes: [{ from, insert: textMatch[1], to }],
|
||||||
|
range: EditorSelection.range(
|
||||||
|
from,
|
||||||
|
to - (characters.length + characters.length),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
from,
|
||||||
|
insert: `${characters}${text}${characters}`,
|
||||||
|
to,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
range: EditorSelection.range(
|
||||||
|
from,
|
||||||
|
to + (characters.length + characters.length),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
editor.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getExpandedSelection = (
|
||||||
|
state: EditorState,
|
||||||
|
characters: string,
|
||||||
|
): { from: number; to: number } => {
|
||||||
|
let { from, to } = state.selection.main;
|
||||||
|
|
||||||
|
let fromPosition = from;
|
||||||
|
while (fromPosition > 0) {
|
||||||
|
const newText = state.sliceDoc(fromPosition, to);
|
||||||
|
if (
|
||||||
|
newText.startsWith('\n') ||
|
||||||
|
newText.startsWith('\t') ||
|
||||||
|
newText.startsWith(' ')
|
||||||
|
) {
|
||||||
|
fromPosition++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
newText.length > characters.length &&
|
||||||
|
newText.startsWith(characters)
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
fromPosition--;
|
||||||
|
}
|
||||||
|
from = fromPosition;
|
||||||
|
|
||||||
|
let toPosition = to;
|
||||||
|
while (toPosition < state.doc.length) {
|
||||||
|
const newText = state.sliceDoc(from, toPosition);
|
||||||
|
if (
|
||||||
|
newText.endsWith('\n') ||
|
||||||
|
newText.endsWith('\t') ||
|
||||||
|
newText.endsWith(' ')
|
||||||
|
) {
|
||||||
|
toPosition--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
newText.length > characters.length &&
|
||||||
|
newText.endsWith(characters)
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
toPosition++;
|
||||||
|
}
|
||||||
|
to = toPosition;
|
||||||
|
|
||||||
|
return { from, to };
|
||||||
|
};
|
@ -0,0 +1,72 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Document</title>
|
||||||
|
<link rel="stylesheet" href="../dist/browser/easymde.css" />
|
||||||
|
<script
|
||||||
|
src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/js/fontawesome.min.js"
|
||||||
|
integrity="sha512-PoFg70xtc+rAkD9xsjaZwIMkhkgbl1TkoaRrgucfsct7SVy9KvTj5LtECit+ZjQ3ts+7xWzgfHOGzdolfWEgrw=="
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
></script>
|
||||||
|
<script
|
||||||
|
src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/js/solid.min.js"
|
||||||
|
integrity="sha512-wabaor0DW08KSK5TQlRIyYOpDrAfJxl5J0FRzH0dNNhGJbeUpHaNj7up3Kr2Bwz/abLvVcJvDrJL+RLFcyGIkg=="
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
/* background-color: grey; */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<button onclick="x.destruct()">DESTROY</button>
|
||||||
|
<button onclick="x.construct()">Render</button>
|
||||||
|
<hr />
|
||||||
|
<div id="custom-toolbar-container"></div>
|
||||||
|
<br />
|
||||||
|
<textarea id="my-text-area">
|
||||||
|
**Moooooo**
|
||||||
|
ooo
|
||||||
|
`code`
|
||||||
|
*sdhjk*
|
||||||
|
# head?
|
||||||
|
~~hjhj~~
|
||||||
|
***Bold & Brash***
|
||||||
|
last
|
||||||
|
|
||||||
|
`const x = "moooo";`
|
||||||
|
|
||||||
|
```js
|
||||||
|
const x = "moooo";
|
||||||
|
```
|
||||||
|
</textarea
|
||||||
|
>
|
||||||
|
<script type="module">
|
||||||
|
// import { EasyMDE, Toolbar, defaultToolbar } from './dist/browser/index.js';
|
||||||
|
import {
|
||||||
|
EasyMDE,
|
||||||
|
importToolbar,
|
||||||
|
importDefaultToolbar,
|
||||||
|
} from '../dist/browser/easymde.min.js';
|
||||||
|
window.x = new EasyMDE({
|
||||||
|
element: document.getElementById('my-text-area'),
|
||||||
|
toolbar: true,
|
||||||
|
statusbar: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// const [{ Toolbar }, { defaultToolbar }] = await Promise.all([await importToolbar(), await importDefaultToolbar()]);
|
||||||
|
// const toolbar = new Toolbar(window.x, defaultToolbar);
|
||||||
|
|
||||||
|
// document.getElementById('custom-toolbar-container').appendChild(toolbar.element);
|
||||||
|
// window.x.destruct();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Document</title>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css"
|
||||||
|
/>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<button onclick="x.toTextArea()">DESTROY</button>
|
||||||
|
<button onclick="x.render()">Render</button>
|
||||||
|
<hr />
|
||||||
|
<br />
|
||||||
|
<textarea id="text">
|
||||||
|
**Moooooo**
|
||||||
|
ooo
|
||||||
|
`code`
|
||||||
|
*sdhjk*
|
||||||
|
# head?
|
||||||
|
~~hjhj~~
|
||||||
|
***Bold & Brash***
|
||||||
|
last
|
||||||
|
|
||||||
|
`const x = "moooo";`
|
||||||
|
|
||||||
|
```js
|
||||||
|
const x = "moooo";
|
||||||
|
```
|
||||||
|
</textarea
|
||||||
|
>
|
||||||
|
<script type="module">
|
||||||
|
window.x = new EasyMDE({
|
||||||
|
element: document.getElementById('text'),
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"declaration": false,
|
||||||
|
"lib": ["esnext", "dom", "dom.iterable"],
|
||||||
|
"module": "es2022",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"removeComments": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"strict": true,
|
||||||
|
"target": "es2021",
|
||||||
|
"skipLibCheck": false,
|
||||||
|
"outDir": "dist/browser"
|
||||||
|
},
|
||||||
|
"files": ["src/index.ts"]
|
||||||
|
}
|
@ -1,238 +0,0 @@
|
|||||||
// Create new instance
|
|
||||||
import EasyMDE = require('./easymde');
|
|
||||||
|
|
||||||
const editor = new EasyMDE({
|
|
||||||
autoDownloadFontAwesome: false,
|
|
||||||
element: document.getElementById('mdEditor')!,
|
|
||||||
hideIcons: ['side-by-side', 'fullscreen'],
|
|
||||||
inputStyle: 'textarea',
|
|
||||||
shortcuts: {
|
|
||||||
drawTable: 'Cmd-Alt-T',
|
|
||||||
toggleFullScreen: null,
|
|
||||||
},
|
|
||||||
previewClass: 'my-custom-class',
|
|
||||||
spellChecker: false,
|
|
||||||
onToggleFullScreen: (full: boolean) => {
|
|
||||||
console.log('FullscreenToggled', full);
|
|
||||||
},
|
|
||||||
theme: 'someOtherTheme',
|
|
||||||
minHeight: '200px',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Editor functions
|
|
||||||
const value = editor.value() as string;
|
|
||||||
editor.value(value.toUpperCase());
|
|
||||||
|
|
||||||
const sbs = editor.isSideBySideActive() as boolean;
|
|
||||||
const fullscreen = editor.isFullscreenActive() as boolean;
|
|
||||||
|
|
||||||
// Access to codemirror object
|
|
||||||
editor.codemirror.setOption('readOnly', true);
|
|
||||||
|
|
||||||
// Static properties
|
|
||||||
EasyMDE.toggleItalic = (editor: EasyMDE) => {
|
|
||||||
console.log('SomeButtonOverride');
|
|
||||||
};
|
|
||||||
|
|
||||||
const editor2 = new EasyMDE({
|
|
||||||
autoDownloadFontAwesome: undefined,
|
|
||||||
previewClass: ['my-custom-class', 'some-other-class'],
|
|
||||||
nativeSpellcheck: true,
|
|
||||||
unorderedListStyle: '-',
|
|
||||||
inputStyle: 'contenteditable',
|
|
||||||
toolbar: [
|
|
||||||
{
|
|
||||||
name: 'bold',
|
|
||||||
action: EasyMDE.toggleBold,
|
|
||||||
className: 'fa fas fa-bolt',
|
|
||||||
title: 'Bold',
|
|
||||||
},
|
|
||||||
'|',
|
|
||||||
'undo',
|
|
||||||
{
|
|
||||||
name: 'alert',
|
|
||||||
action: (editor: EasyMDE) => {
|
|
||||||
alert('This is from a custom button action!');
|
|
||||||
// Custom functions have access to the `editor` instance.
|
|
||||||
},
|
|
||||||
className: 'fa fas fa-star',
|
|
||||||
title: 'A Custom Button',
|
|
||||||
noDisable: undefined,
|
|
||||||
noMobile: false,
|
|
||||||
attributes: {
|
|
||||||
'data-custom': 'attribute',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'|',
|
|
||||||
{
|
|
||||||
name: 'link',
|
|
||||||
action: 'https://github.com/Ionaru/easy-markdown-editor',
|
|
||||||
className: 'fa fab fa-github',
|
|
||||||
title: 'A Custom Link',
|
|
||||||
noDisable: true,
|
|
||||||
noMobile: true,
|
|
||||||
},
|
|
||||||
'preview',
|
|
||||||
{
|
|
||||||
name: 'links',
|
|
||||||
className: 'fa fas fa-arrow-down',
|
|
||||||
title: 'A Custom Link',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: 'link',
|
|
||||||
action: 'https://github.com/Ionaru/easy-markdown-editor',
|
|
||||||
className: 'fa fab fa-github',
|
|
||||||
title: 'A Custom Link',
|
|
||||||
noDisable: true,
|
|
||||||
noMobile: true,
|
|
||||||
},
|
|
||||||
'preview',
|
|
||||||
{
|
|
||||||
name: 'bold',
|
|
||||||
action: EasyMDE.toggleBold,
|
|
||||||
className: 'fa fas fa-bold',
|
|
||||||
title: 'Bold',
|
|
||||||
attributes: {
|
|
||||||
'data-custom': 'some value',
|
|
||||||
'data-custom-2': 'another value',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
editor2.clearAutosavedValue();
|
|
||||||
editor2.updateStatusBar('upload-image', 'Drag & drop images!');
|
|
||||||
|
|
||||||
EasyMDE.togglePreview(editor2);
|
|
||||||
EasyMDE.toggleSideBySide(editor2);
|
|
||||||
|
|
||||||
const editorImages = new EasyMDE({
|
|
||||||
uploadImage: true,
|
|
||||||
previewImagesInEditor: false,
|
|
||||||
imageAccept: 'image/png, image/bmp',
|
|
||||||
imageCSRFToken: undefined,
|
|
||||||
unorderedListStyle: '+',
|
|
||||||
imageMaxSize: 10485760,
|
|
||||||
imageUploadEndpoint: 'https://my.domain/image-upload/',
|
|
||||||
imageTexts: {
|
|
||||||
sbInit: 'Drag & drop images!',
|
|
||||||
sbOnDragEnter: 'Let it go, let it go',
|
|
||||||
sbOnDrop: 'Uploading...',
|
|
||||||
sbProgress: 'Uploading... (#progress#)',
|
|
||||||
sbOnUploaded: 'Upload complete!',
|
|
||||||
sizeUnits: 'b,Kb,Mb',
|
|
||||||
},
|
|
||||||
errorMessages: {
|
|
||||||
noFileGiven: 'Please select a file',
|
|
||||||
typeNotAllowed: 'This file type is not allowed!',
|
|
||||||
fileTooLarge: 'Image too big',
|
|
||||||
importError: 'Something went oops!',
|
|
||||||
},
|
|
||||||
errorCallback: errorMessage => {
|
|
||||||
console.error(errorMessage);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const editorImagesCustom = new EasyMDE({
|
|
||||||
uploadImage: true,
|
|
||||||
imageAccept: 'image/png, image/bmp',
|
|
||||||
imageCSRFToken: undefined,
|
|
||||||
imageMaxSize: 10485760,
|
|
||||||
imageUploadFunction: (file: File, onSuccess, onError) => {
|
|
||||||
console.log(file);
|
|
||||||
onSuccess('http://image.url/9.png');
|
|
||||||
onError('Failed because reasons.');
|
|
||||||
},
|
|
||||||
imageTexts: {
|
|
||||||
sbInit: 'Drag & drop images!',
|
|
||||||
sbOnDragEnter: 'Let it go, let it go',
|
|
||||||
sbOnDrop: 'Uploading...',
|
|
||||||
sbProgress: 'Uploading... (#progress#)',
|
|
||||||
sbOnUploaded: 'Upload complete!',
|
|
||||||
sizeUnits: 'b,Kb,Mb',
|
|
||||||
},
|
|
||||||
errorMessages: {
|
|
||||||
noFileGiven: 'Please select a file',
|
|
||||||
typeNotAllowed: 'This file type is not allowed!',
|
|
||||||
fileTooLarge: 'Image too big',
|
|
||||||
importError: 'Something went oops!',
|
|
||||||
},
|
|
||||||
errorCallback: errorMessage => {
|
|
||||||
console.error(errorMessage);
|
|
||||||
},
|
|
||||||
renderingConfig: {
|
|
||||||
codeSyntaxHighlighting: true,
|
|
||||||
markedOptions: {
|
|
||||||
silent: true,
|
|
||||||
highlight(code: string, lang: string, callback?: (error: (any | undefined), code: string) => void): string {
|
|
||||||
return 'something';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
promptTexts: {
|
|
||||||
image: 'Insert URL',
|
|
||||||
},
|
|
||||||
syncSideBySidePreviewScroll: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
new EasyMDE({
|
|
||||||
toolbarButtonClassPrefix: 'mde',
|
|
||||||
sideBySideFullscreen: true,
|
|
||||||
lineNumbers: false,
|
|
||||||
unorderedListStyle: '*',
|
|
||||||
autosave: {
|
|
||||||
enabled: true,
|
|
||||||
delay: 2000,
|
|
||||||
submit_delay: 10000,
|
|
||||||
uniqueId: 'abc',
|
|
||||||
timeFormat: {
|
|
||||||
locale: 'en-GB',
|
|
||||||
format: {
|
|
||||||
month: 'long',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
text: 'Stored: ',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
new EasyMDE({
|
|
||||||
sideBySideFullscreen: false,
|
|
||||||
lineNumbers: true,
|
|
||||||
maxHeight: '500px',
|
|
||||||
toolbar: [
|
|
||||||
'bold',
|
|
||||||
'italic',
|
|
||||||
'heading',
|
|
||||||
'|',
|
|
||||||
'quote',
|
|
||||||
'unordered-list',
|
|
||||||
'ordered-list',
|
|
||||||
'table',
|
|
||||||
'upload-image',
|
|
||||||
'|',
|
|
||||||
'link',
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
new EasyMDE({
|
|
||||||
direction: 'ltr',
|
|
||||||
});
|
|
||||||
|
|
||||||
new EasyMDE({
|
|
||||||
direction: 'rtl',
|
|
||||||
});
|
|
||||||
|
|
||||||
new EasyMDE({
|
|
||||||
previewRender: (plainText: string) => {
|
|
||||||
return '<pre>' + plainText + '</pre>';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
new EasyMDE({
|
|
||||||
previewRender: (plainText: string, preview) => {
|
|
||||||
preview.innerHTML = '<pre>' + plainText + '</pre>';
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,293 +0,0 @@
|
|||||||
// This file is based on https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/simplemde/index.d.ts,
|
|
||||||
// which is written by Scalesoft <https://github.com/Scalesoft> and licensed under the MIT license:
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included in all
|
|
||||||
// copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
|
|
||||||
/// <reference types="codemirror"/>
|
|
||||||
|
|
||||||
import { marked } from 'marked';
|
|
||||||
|
|
||||||
interface ArrayOneOrMore<T> extends Array<T> {
|
|
||||||
0: T;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ToolbarButton =
|
|
||||||
'bold'
|
|
||||||
| 'italic'
|
|
||||||
| 'quote'
|
|
||||||
| 'unordered-list'
|
|
||||||
| 'ordered-list'
|
|
||||||
| 'link'
|
|
||||||
| 'image'
|
|
||||||
| 'upload-image'
|
|
||||||
| 'strikethrough'
|
|
||||||
| 'code'
|
|
||||||
| 'table'
|
|
||||||
| 'redo'
|
|
||||||
| 'heading'
|
|
||||||
| 'undo'
|
|
||||||
| 'heading-bigger'
|
|
||||||
| 'heading-smaller'
|
|
||||||
| 'heading-1'
|
|
||||||
| 'heading-2'
|
|
||||||
| 'heading-3'
|
|
||||||
| 'clean-block'
|
|
||||||
| 'horizontal-rule'
|
|
||||||
| 'preview'
|
|
||||||
| 'side-by-side'
|
|
||||||
| 'fullscreen'
|
|
||||||
| 'guide';
|
|
||||||
|
|
||||||
declare namespace EasyMDE {
|
|
||||||
|
|
||||||
interface TimeFormatOptions {
|
|
||||||
locale?: string | string[];
|
|
||||||
format?: Intl.DateTimeFormatOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AutoSaveOptions {
|
|
||||||
enabled?: boolean;
|
|
||||||
delay?: number;
|
|
||||||
submit_delay?: number;
|
|
||||||
uniqueId: string;
|
|
||||||
timeFormat?: TimeFormatOptions;
|
|
||||||
text?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BlockStyleOptions {
|
|
||||||
bold?: string;
|
|
||||||
code?: string;
|
|
||||||
italic?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CustomAttributes {
|
|
||||||
[key: string]: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InsertTextOptions {
|
|
||||||
horizontalRule?: ReadonlyArray<string>;
|
|
||||||
image?: ReadonlyArray<string>;
|
|
||||||
link?: ReadonlyArray<string>;
|
|
||||||
table?: ReadonlyArray<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ParsingOptions {
|
|
||||||
allowAtxHeaderWithoutSpace?: boolean;
|
|
||||||
strikethrough?: boolean;
|
|
||||||
underscoresBreakWords?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PromptTexts {
|
|
||||||
image?: string;
|
|
||||||
link?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RenderingOptions {
|
|
||||||
codeSyntaxHighlighting?: boolean;
|
|
||||||
hljs?: any;
|
|
||||||
markedOptions?: marked.MarkedOptions;
|
|
||||||
sanitizerFunction?: (html: string) => string;
|
|
||||||
singleLineBreaks?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Shortcuts {
|
|
||||||
[action: string]: string | undefined | null;
|
|
||||||
|
|
||||||
toggleBlockquote?: string | null;
|
|
||||||
toggleBold?: string | null;
|
|
||||||
cleanBlock?: string | null;
|
|
||||||
toggleHeadingSmaller?: string | null;
|
|
||||||
toggleItalic?: string | null;
|
|
||||||
drawLink?: string | null;
|
|
||||||
toggleUnorderedList?: string | null;
|
|
||||||
togglePreview?: string | null;
|
|
||||||
toggleCodeBlock?: string | null;
|
|
||||||
drawImage?: string | null;
|
|
||||||
toggleOrderedList?: string | null;
|
|
||||||
toggleHeadingBigger?: string | null;
|
|
||||||
toggleSideBySide?: string | null;
|
|
||||||
toggleFullScreen?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StatusBarItem {
|
|
||||||
className: string;
|
|
||||||
defaultValue: (element: HTMLElement) => void;
|
|
||||||
onUpdate: (element: HTMLElement) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ToolbarDropdownIcon {
|
|
||||||
name: string;
|
|
||||||
children: ArrayOneOrMore<ToolbarIcon | ToolbarButton>;
|
|
||||||
className: string;
|
|
||||||
title: string;
|
|
||||||
noDisable?: boolean;
|
|
||||||
noMobile?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ToolbarIcon {
|
|
||||||
name: string;
|
|
||||||
action: string | ((editor: EasyMDE) => void);
|
|
||||||
className: string;
|
|
||||||
title: string;
|
|
||||||
noDisable?: boolean;
|
|
||||||
noMobile?: boolean;
|
|
||||||
icon?: string;
|
|
||||||
attributes?: CustomAttributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ImageTextsOptions {
|
|
||||||
sbInit?: string;
|
|
||||||
sbOnDragEnter?: string;
|
|
||||||
sbOnDrop?: string;
|
|
||||||
sbProgress?: string;
|
|
||||||
sbOnUploaded?: string;
|
|
||||||
sizeUnits?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ImageErrorTextsOptions {
|
|
||||||
noFileGiven?: string;
|
|
||||||
typeNotAllowed?: string;
|
|
||||||
fileTooLarge?: string;
|
|
||||||
importError?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OverlayModeOptions {
|
|
||||||
mode: CodeMirror.Mode<any>;
|
|
||||||
combine?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SpellCheckerOptions {
|
|
||||||
codeMirrorInstance: CodeMirror.Editor;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Options {
|
|
||||||
autoDownloadFontAwesome?: boolean;
|
|
||||||
autofocus?: boolean;
|
|
||||||
autosave?: AutoSaveOptions;
|
|
||||||
autoRefresh?: boolean | { delay: number; };
|
|
||||||
blockStyles?: BlockStyleOptions;
|
|
||||||
element?: HTMLElement;
|
|
||||||
forceSync?: boolean;
|
|
||||||
hideIcons?: ReadonlyArray<ToolbarButton>;
|
|
||||||
indentWithTabs?: boolean;
|
|
||||||
initialValue?: string;
|
|
||||||
insertTexts?: InsertTextOptions;
|
|
||||||
lineNumbers?: boolean;
|
|
||||||
lineWrapping?: boolean;
|
|
||||||
minHeight?: string;
|
|
||||||
maxHeight?: string;
|
|
||||||
parsingConfig?: ParsingOptions;
|
|
||||||
placeholder?: string;
|
|
||||||
previewClass?: string | ReadonlyArray<string>;
|
|
||||||
previewImagesInEditor?: boolean;
|
|
||||||
imagesPreviewHandler?: (src: string) => string,
|
|
||||||
previewRender?: (markdownPlaintext: string, previewElement: HTMLElement) => string | null;
|
|
||||||
promptURLs?: boolean;
|
|
||||||
renderingConfig?: RenderingOptions;
|
|
||||||
shortcuts?: Shortcuts;
|
|
||||||
showIcons?: ReadonlyArray<ToolbarButton>;
|
|
||||||
spellChecker?: boolean | ((options: SpellCheckerOptions) => void);
|
|
||||||
inputStyle?: 'textarea' | 'contenteditable';
|
|
||||||
nativeSpellcheck?: boolean;
|
|
||||||
sideBySideFullscreen?: boolean;
|
|
||||||
status?: boolean | ReadonlyArray<string | StatusBarItem>;
|
|
||||||
styleSelectedText?: boolean;
|
|
||||||
tabSize?: number;
|
|
||||||
toolbar?: boolean | ReadonlyArray<'|' | ToolbarButton | ToolbarIcon | ToolbarDropdownIcon>;
|
|
||||||
toolbarTips?: boolean;
|
|
||||||
toolbarButtonClassPrefix?: string;
|
|
||||||
onToggleFullScreen?: (goingIntoFullScreen: boolean) => void;
|
|
||||||
theme?: string;
|
|
||||||
scrollbarStyle?: string;
|
|
||||||
unorderedListStyle?: '*' | '-' | '+';
|
|
||||||
|
|
||||||
uploadImage?: boolean;
|
|
||||||
imageMaxSize?: number;
|
|
||||||
imageAccept?: string;
|
|
||||||
imageUploadFunction?: (file: File, onSuccess: (url: string) => void, onError: (error: string) => void) => void;
|
|
||||||
imageUploadEndpoint?: string;
|
|
||||||
imagePathAbsolute?: boolean;
|
|
||||||
imageCSRFToken?: string;
|
|
||||||
imageCSRFName?: string;
|
|
||||||
imageCSRFHeader?: boolean;
|
|
||||||
imageTexts?: ImageTextsOptions;
|
|
||||||
errorMessages?: ImageErrorTextsOptions;
|
|
||||||
errorCallback?: (errorMessage: string) => void;
|
|
||||||
|
|
||||||
promptTexts?: PromptTexts;
|
|
||||||
syncSideBySidePreviewScroll?: boolean;
|
|
||||||
|
|
||||||
overlayMode?: OverlayModeOptions;
|
|
||||||
|
|
||||||
direction?: 'ltr' | 'rtl';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare class EasyMDE {
|
|
||||||
constructor(options?: EasyMDE.Options);
|
|
||||||
|
|
||||||
value(): string;
|
|
||||||
value(val: string): void;
|
|
||||||
|
|
||||||
codemirror: CodeMirror.Editor;
|
|
||||||
|
|
||||||
cleanup(): void;
|
|
||||||
|
|
||||||
toTextArea(): void;
|
|
||||||
|
|
||||||
isPreviewActive(): boolean;
|
|
||||||
|
|
||||||
isSideBySideActive(): boolean;
|
|
||||||
|
|
||||||
isFullscreenActive(): boolean;
|
|
||||||
|
|
||||||
clearAutosavedValue(): void;
|
|
||||||
|
|
||||||
updateStatusBar(itemName: string, content: string): void;
|
|
||||||
|
|
||||||
static toggleBold: (editor: EasyMDE) => void;
|
|
||||||
static toggleItalic: (editor: EasyMDE) => void;
|
|
||||||
static toggleStrikethrough: (editor: EasyMDE) => void;
|
|
||||||
static toggleHeadingSmaller: (editor: EasyMDE) => void;
|
|
||||||
static toggleHeadingBigger: (editor: EasyMDE) => void;
|
|
||||||
static toggleHeading1: (editor: EasyMDE) => void;
|
|
||||||
static toggleHeading2: (editor: EasyMDE) => void;
|
|
||||||
static toggleHeading3: (editor: EasyMDE) => void;
|
|
||||||
static toggleHeading4: (editor: EasyMDE) => void;
|
|
||||||
static toggleHeading5: (editor: EasyMDE) => void;
|
|
||||||
static toggleHeading6: (editor: EasyMDE) => void;
|
|
||||||
static toggleCodeBlock: (editor: EasyMDE) => void;
|
|
||||||
static toggleBlockquote: (editor: EasyMDE) => void;
|
|
||||||
static toggleUnorderedList: (editor: EasyMDE) => void;
|
|
||||||
static toggleOrderedList: (editor: EasyMDE) => void;
|
|
||||||
static cleanBlock: (editor: EasyMDE) => void;
|
|
||||||
static drawLink: (editor: EasyMDE) => void;
|
|
||||||
static drawImage: (editor: EasyMDE) => void;
|
|
||||||
static drawUploadedImage: (editor: EasyMDE) => void;
|
|
||||||
static drawTable: (editor: EasyMDE) => void;
|
|
||||||
static drawHorizontalRule: (editor: EasyMDE) => void;
|
|
||||||
static togglePreview: (editor: EasyMDE) => void;
|
|
||||||
static toggleSideBySide: (editor: EasyMDE) => void;
|
|
||||||
static toggleFullScreen: (editor: EasyMDE) => void;
|
|
||||||
static undo: (editor: EasyMDE) => void;
|
|
||||||
static redo: (editor: EasyMDE) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export as namespace EasyMDE;
|
|
||||||
export = EasyMDE;
|
|
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es3",
|
|
||||||
"strict": true,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noEmit": true
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
coverage: {
|
||||||
|
all: true,
|
||||||
|
clean: true,
|
||||||
|
enabled: true,
|
||||||
|
include: ['src/**/*.ts'],
|
||||||
|
provider: 'c8',
|
||||||
|
},
|
||||||
|
dir: 'src',
|
||||||
|
environment: 'jsdom',
|
||||||
|
include: ['**/*.spec.ts'],
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in New Issue