Initial commit for v3
parent
abead2a068
commit
e6a9359b1d
@ -0,0 +1,3 @@
|
||||
coverage
|
||||
dist
|
||||
node_modules
|
@ -1,27 +1,12 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["@ionaru", "prettier"],
|
||||
"rules": {
|
||||
"strict": 0,
|
||||
"no-console": 0,
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"always-multiline"
|
||||
]
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
},
|
||||
"extends": "eslint:recommended"
|
||||
"jest/no-deprecated-functions": "off",
|
||||
"jest/unbound-method": "off",
|
||||
"jest/require-hook": "off",
|
||||
"import/extensions": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"unicorn/no-null": "off"
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,18 @@
|
||||
<!-- Please help me process issues faster by providing the following information -->
|
||||
|
||||
### I'm submitting a...
|
||||
|
||||
- [x] Bug report
|
||||
- [ ] Feature request
|
||||
|
||||
### Reproduction steps
|
||||
|
||||
<!-- Bonus points if you set up a [JSFiddle](https://jsfiddle.net/) that replicates the bug and link it in the issue. -->
|
||||
|
||||
1. ...
|
||||
2. ...
|
||||
|
||||
### Version information
|
||||
|
||||
Browser type and version:
|
||||
EasyMDE version:
|
||||
|
@ -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",
|
||||
"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.",
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"src/**/*",
|
||||
"types/easymde.d.ts"
|
||||
],
|
||||
"keywords": [
|
||||
"embeddable",
|
||||
"markdown",
|
||||
"editor",
|
||||
"javascript",
|
||||
"fontawesome"
|
||||
"javascript"
|
||||
],
|
||||
"main": "src/js/easymde.js",
|
||||
"types": "types/easymde.d.ts",
|
||||
"main": "dist/node/easymde.js",
|
||||
"browser": "dist/browser/easymde.min.js",
|
||||
"types": "dist/types/index.d.ts",
|
||||
"license": "MIT",
|
||||
"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": {
|
||||
"@types/codemirror": "^5.60.4",
|
||||
"@types/marked": "^4.0.7",
|
||||
"codemirror": "^5.63.1",
|
||||
"codemirror-spell-checker": "1.1.2",
|
||||
"marked": "^4.1.0"
|
||||
"@codemirror/lang-markdown": "^6.1.1",
|
||||
"@codemirror/language": "^6.6.0",
|
||||
"@codemirror/state": "^6.2.0",
|
||||
"@codemirror/view": "^6.10.0",
|
||||
"@lezer/highlight": "^1.1.4",
|
||||
"@lezer/markdown": "^1.0.2",
|
||||
"escape-string-regexp": "^5.0.0",
|
||||
"marked": "^4.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"browserify": "^17.0.0",
|
||||
"cypress": "^10.8.0",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-clean-css": "^4.2.0",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-eslint": "^6.0.0",
|
||||
"gulp-header": "^2.0.9",
|
||||
"gulp-rename": "^2.0.0",
|
||||
"gulp-terser": "^2.1.0",
|
||||
"gulp-uglify": "^3.0.2",
|
||||
"typescript": "^4.8.3",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^2.0.0"
|
||||
},
|
||||
"repository": "github:Ionaru/easy-markdown-editor",
|
||||
"scripts": {
|
||||
"prepare": "gulp",
|
||||
"test": "npm run lint && npm run test:types && npm run e2e",
|
||||
"lint": "gulp lint",
|
||||
"cypress:lint": "eslint cypress",
|
||||
"cypress:run": "cypress run",
|
||||
"e2e": "gulp && npm run cypress:lint && npm run cypress:run",
|
||||
"test:types": "tsc --project types/tsconfig.json"
|
||||
"@ionaru/eslint-config": "^9.2.1-53.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||
"@rollup/plugin-terser": "^0.4.1",
|
||||
"@rollup/plugin-typescript": "^11.1.0",
|
||||
"@types/marked": "^4.0.8",
|
||||
"@types/node": "^18.16.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
||||
"@vitest/coverage-c8": "^0.30.1",
|
||||
"@vitest/ui": "^0.30.1",
|
||||
"cypress": "^12.11.0",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"eslint-plugin-prefer-arrow": "^1.2.3",
|
||||
"eslint-plugin-sonarjs": "^0.19.0",
|
||||
"eslint-plugin-unicorn": "^46.0.0",
|
||||
"jsdom": "^21.1.1",
|
||||
"prettier": "^2.8.8",
|
||||
"rollup": "^3.21.0",
|
||||
"rollup-plugin-cleaner": "^1.0.0",
|
||||
"rollup-plugin-scss": "^4.0.0",
|
||||
"sass": "^1.62.1",
|
||||
"tslib": "^2.5.0",
|
||||
"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