cm-md-editor 2.4.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +13 -0
- package/README.md +146 -15
- package/example-addon-tools/DummyText.js +42 -0
- package/example-addon-tools/bi-body-text.svg +3 -0
- package/index.html +9 -22
- package/package.json +1 -1
- package/src/MdEditor.js +79 -118
- package/src/cm-md-editor.css +37 -0
- package/src/tools/Bold.js +11 -0
- package/src/tools/DefaultTools.js +14 -0
- package/src/tools/Headings.js +34 -0
- package/src/tools/InsertImage.js +22 -0
- package/src/tools/InsertLink.js +22 -0
- package/src/tools/Italic.js +11 -0
- package/src/tools/OrderedList.js +19 -0
- package/src/tools/Separator.js +8 -0
- package/src/tools/Strikethrough.js +8 -0
- package/src/tools/UnorderedList.js +18 -0
- package/src/tools/icons/LICENSE +21 -0
- package/src/tools/icons/card-image.svg +4 -0
- package/src/tools/icons/link-45deg.svg +4 -0
- package/src/tools/icons/list-ol.svg +4 -0
- package/src/tools/icons/list-ul.svg +3 -0
- package/src/tools/icons/text-wrap.svg +3 -0
- package/src/tools/icons/type-bold.svg +3 -0
- package/src/tools/icons/type-h1.svg +3 -0
- package/src/tools/icons/type-h2.svg +3 -0
- package/src/tools/icons/type-h3.svg +3 -0
- package/src/tools/icons/type-italic.svg +3 -0
- package/src/tools/icons/type-strikethrough.svg +3 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Read(//private/tmp/**)",
|
|
5
|
+
"Bash(npm pack:*)",
|
|
6
|
+
"Bash(for f:*)",
|
|
7
|
+
"Bash(do echo:*)",
|
|
8
|
+
"Read(//Users/shaack/Development/repositories/MyProjects/cm-md-editor/**)",
|
|
9
|
+
"Bash(done)",
|
|
10
|
+
"Bash(npm whoami:*)"
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
}
|
package/README.md
CHANGED
|
@@ -9,11 +9,11 @@ A minimal, dependency-free markdown editor as a vanilla JavaScript ES6 module.
|
|
|
9
9
|
## Key features
|
|
10
10
|
|
|
11
11
|
- Vanilla JavaScript module, zero dependencies
|
|
12
|
-
- Syntax highlighting for headings, bold, italic, code, lists, links, images, blockquotes, HTML tags, horizontal rules, front matter and more
|
|
13
|
-
-
|
|
12
|
+
- Syntax highlighting for headings, bold, italic, strikethrough, code, lists, links, images, blockquotes, HTML tags, horizontal rules, front matter and more
|
|
13
|
+
- Modular toolbar built from composable tools
|
|
14
14
|
- Word wrap toggle with persistent state (localStorage)
|
|
15
15
|
- List mode: Tab/Shift-Tab to indent/outdent, auto-continuation on Enter
|
|
16
|
-
- Bold with Ctrl/Cmd+B, italic with Ctrl/Cmd+I
|
|
16
|
+
- Bold with Ctrl/Cmd+B, italic with Ctrl/Cmd+I (provided by tools)
|
|
17
17
|
- Native undo/redo support (Ctrl/Cmd+Z / Ctrl/Cmd+Shift+Z)
|
|
18
18
|
- Lightweight, fast, easy to use
|
|
19
19
|
|
|
@@ -35,12 +35,32 @@ npm install cm-md-editor
|
|
|
35
35
|
</script>
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
This creates an editor with the default toolbar: Headings (h1–h3), Bold, Italic, Strikethrough, Unordered List, Ordered List, Insert Link, Insert Image.
|
|
39
|
+
|
|
40
|
+
### Custom toolbar
|
|
41
|
+
|
|
42
|
+
Compose your own toolbar by passing a `tools` array:
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
import {MdEditor} from "cm-md-editor/src/MdEditor.js"
|
|
46
|
+
import {Headings} from "cm-md-editor/src/tools/Headings.js"
|
|
47
|
+
import {Bold} from "cm-md-editor/src/tools/Bold.js"
|
|
48
|
+
import {Italic} from "cm-md-editor/src/tools/Italic.js"
|
|
49
|
+
import {Separator} from "cm-md-editor/src/tools/Separator.js"
|
|
50
|
+
import {InsertLink} from "cm-md-editor/src/tools/InsertLink.js"
|
|
51
|
+
|
|
52
|
+
new MdEditor(document.getElementById("editor"), {
|
|
53
|
+
tools: [Headings, Separator, Bold, Italic, Separator, InsertLink]
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Configuring tools
|
|
58
|
+
|
|
59
|
+
Tools that accept options can be passed as `[ToolClass, props]` tuples:
|
|
39
60
|
|
|
40
61
|
```javascript
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
toolbarButtons: ["h1", "h2", "bold", "italic", "ul", "link"]
|
|
62
|
+
new MdEditor(document.getElementById("editor"), {
|
|
63
|
+
tools: [[Headings, {minLevel: 2, maxLevel: 4}], Bold, Italic]
|
|
44
64
|
})
|
|
45
65
|
```
|
|
46
66
|
|
|
@@ -50,8 +70,8 @@ All props are optional. Pass them as the second argument to the constructor.
|
|
|
50
70
|
|
|
51
71
|
| Prop | Type | Default | Description |
|
|
52
72
|
|------|------|---------|-------------|
|
|
73
|
+
| `tools` | `array` | `defaultTools` | Array of tool classes (or `[class, props]` tuples). See [Tools](#tools) |
|
|
53
74
|
| `wordWrap` | `boolean` | `true` | Default word wrap state. Overridden by localStorage if the user has toggled it |
|
|
54
|
-
| `toolbarButtons` | `string[]` | `["h1", "h2", "h3", "bold", "italic", "ul", "ol", "link", "image"]` | Which toolbar buttons to show. Available: `h1`, `h2`, `h3`, `bold`, `italic`, `ul`, `ol`, `link`, `image` |
|
|
55
75
|
| `colorHeading` | `string` | `"100,160,255"` | RGB color for headings |
|
|
56
76
|
| `colorCode` | `string` | `"130,170,200"` | RGB color for code spans and fenced code blocks |
|
|
57
77
|
| `colorComment` | `string` | `"128,128,128"` | RGB color for HTML comments |
|
|
@@ -68,15 +88,126 @@ All props are optional. Pass them as the second argument to the constructor.
|
|
|
68
88
|
|
|
69
89
|
Colors are specified as RGB strings (e.g. `"255,180,80"`) and rendered at full opacity.
|
|
70
90
|
|
|
91
|
+
## Tools
|
|
92
|
+
|
|
93
|
+
The toolbar is built entirely from tools. Each tool is a class that provides toolbar buttons, keyboard shortcuts, and/or syntax highlighting extensions.
|
|
94
|
+
|
|
95
|
+
### Built-in tools
|
|
96
|
+
|
|
97
|
+
| Tool | Buttons | Shortcut | Description |
|
|
98
|
+
|------|---------|----------|-------------|
|
|
99
|
+
| `Headings` | h1, h2, h3 | — | Toggle heading levels. Props: `{minLevel, maxLevel}` (defaults: 1–3) |
|
|
100
|
+
| `Bold` | bold | Ctrl/Cmd+B | Toggle bold (`**`) |
|
|
101
|
+
| `Italic` | italic | Ctrl/Cmd+I | Toggle italic (`_`) |
|
|
102
|
+
| `Strikethrough` | strikethrough | — | Toggle strikethrough (`~~`) |
|
|
103
|
+
| `UnorderedList` | ul | — | Toggle unordered list prefix (`- `) |
|
|
104
|
+
| `OrderedList` | ol | — | Toggle ordered list prefix (`1. `) |
|
|
105
|
+
| `InsertLink` | link | — | Insert markdown link |
|
|
106
|
+
| `InsertImage` | image | — | Insert markdown image |
|
|
107
|
+
| `Separator` | — | — | Visual divider in the toolbar. Can be used multiple times |
|
|
108
|
+
|
|
109
|
+
All built-in tools are exported from `src/tools/DefaultTools.js`:
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
import {defaultTools} from "cm-md-editor/src/tools/DefaultTools.js"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The default toolbar order is:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
Headings | Bold, Italic, Strikethrough | UnorderedList, OrderedList | InsertLink, InsertImage
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Writing a custom tool
|
|
122
|
+
|
|
123
|
+
A tool is a class that receives the editor instance (and optional props) in its constructor. It can implement any combination of three optional methods:
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
export class MyTool {
|
|
127
|
+
constructor(editor, props = {}) {
|
|
128
|
+
this.editor = editor
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Optional: add buttons to the toolbar
|
|
132
|
+
toolbarButtons() {
|
|
133
|
+
return [{
|
|
134
|
+
name: 'mytool',
|
|
135
|
+
title: 'My Tool',
|
|
136
|
+
// Icon options (use one):
|
|
137
|
+
icon: '<path d="..."/>', // inline SVG path for a 16x16 viewBox
|
|
138
|
+
iconFile: 'my-icon.svg', // filename in src/tools/icons/
|
|
139
|
+
iconUrl: 'https://...', // full URL to an SVG file
|
|
140
|
+
action: () => { /* ... */ }
|
|
141
|
+
}]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Optional: register keyboard shortcuts
|
|
145
|
+
keyboardShortcuts() {
|
|
146
|
+
return [{
|
|
147
|
+
key: 'e', // the key to match (KeyboardEvent.key)
|
|
148
|
+
ctrlOrMeta: true, // require Ctrl (Windows/Linux) or Cmd (Mac)
|
|
149
|
+
action: (e) => { /* ... */ }
|
|
150
|
+
}]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Optional: extend syntax highlighting (receives already-escaped HTML)
|
|
154
|
+
highlightInline(html) {
|
|
155
|
+
return html.replace(...)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
For tools with co-located icons, use `import.meta.url` to resolve the icon path:
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
toolbarButtons() {
|
|
164
|
+
return [{
|
|
165
|
+
name: 'mytool',
|
|
166
|
+
title: 'My Tool',
|
|
167
|
+
iconUrl: new URL("my-icon.svg", import.meta.url).href,
|
|
168
|
+
action: () => { /* ... */ }
|
|
169
|
+
}]
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Editor API available to tools
|
|
174
|
+
|
|
175
|
+
These public methods and properties are available via `this.editor`:
|
|
176
|
+
|
|
177
|
+
| Method / Property | Description |
|
|
178
|
+
|-------------------|-------------|
|
|
179
|
+
| `editor.element` | The textarea element (read `value`, `selectionStart`, `selectionEnd`) |
|
|
180
|
+
| `editor.insertTextAtCursor(text)` | Insert text at cursor, preserving native undo/redo |
|
|
181
|
+
| `editor.getCurrentLineInfo()` | Returns `{lineStart, lineEnd, line}` for the current line |
|
|
182
|
+
| `editor.selectLineRange(start, end)` | Set the textarea selection range |
|
|
183
|
+
| `editor.toggleWrap(marker)` | Toggle wrapping markers around the selection (e.g. `**` for bold) |
|
|
184
|
+
| `editor.escapeHtml(str)` | Escape a string for use in the highlight layer |
|
|
185
|
+
| `editor.colorSpan(colorProp, content)` | Wrap content in a colored `<span>` using an RGB color prop |
|
|
186
|
+
|
|
187
|
+
### Example: DummyText tool
|
|
188
|
+
|
|
189
|
+
A tool that inserts lorem ipsum text (see `example-addon-tools/DummyText.js`):
|
|
190
|
+
|
|
191
|
+
```javascript
|
|
192
|
+
import {MdEditor} from "cm-md-editor/src/MdEditor.js"
|
|
193
|
+
import {defaultTools} from "cm-md-editor/src/tools/DefaultTools.js"
|
|
194
|
+
import {Separator} from "cm-md-editor/src/tools/Separator.js"
|
|
195
|
+
import {DummyText} from "./example-addon-tools/DummyText.js"
|
|
196
|
+
|
|
197
|
+
new MdEditor(document.getElementById("editor"), {
|
|
198
|
+
tools: [...defaultTools, Separator, DummyText]
|
|
199
|
+
})
|
|
200
|
+
```
|
|
201
|
+
|
|
71
202
|
## Keyboard shortcuts
|
|
72
203
|
|
|
73
|
-
| Shortcut | Action |
|
|
74
|
-
|
|
75
|
-
| Ctrl/Cmd + B | Toggle bold |
|
|
76
|
-
| Ctrl/Cmd + I | Toggle italic |
|
|
77
|
-
| Tab | Indent list item or insert tab |
|
|
78
|
-
| Shift + Tab | Outdent list item |
|
|
79
|
-
| Enter | Auto-continue list (unordered and ordered) |
|
|
204
|
+
| Shortcut | Action | Provided by |
|
|
205
|
+
|----------|--------|-------------|
|
|
206
|
+
| Ctrl/Cmd + B | Toggle bold | `Bold` tool |
|
|
207
|
+
| Ctrl/Cmd + I | Toggle italic | `Italic` tool |
|
|
208
|
+
| Tab | Indent list item or insert tab | Core editor |
|
|
209
|
+
| Shift + Tab | Outdent list item | Core editor |
|
|
210
|
+
| Enter | Auto-continue list (unordered and ordered) | Core editor |
|
|
80
211
|
|
|
81
212
|
## License
|
|
82
213
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const WORDS = [
|
|
2
|
+
"lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit",
|
|
3
|
+
"sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore",
|
|
4
|
+
"magna", "aliqua", "enim", "ad", "minim", "veniam", "quis", "nostrud",
|
|
5
|
+
"exercitation", "ullamco", "laboris", "nisi", "aliquip", "ex", "ea", "commodo",
|
|
6
|
+
"consequat", "duis", "aute", "irure", "in", "reprehenderit", "voluptate",
|
|
7
|
+
"velit", "esse", "cillum", "fugiat", "nulla", "pariatur", "excepteur", "sint",
|
|
8
|
+
"occaecat", "cupidatat", "non", "proident", "sunt", "culpa", "qui", "officia",
|
|
9
|
+
"deserunt", "mollit", "anim", "id", "est", "laborum"
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
function generateDummyText(wordCount) {
|
|
13
|
+
const result = []
|
|
14
|
+
for (let i = 0; i < wordCount; i++) {
|
|
15
|
+
result.push(WORDS[i % WORDS.length])
|
|
16
|
+
}
|
|
17
|
+
result[0] = result[0].charAt(0).toUpperCase() + result[0].slice(1)
|
|
18
|
+
return result.join(" ") + "."
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class DummyText {
|
|
22
|
+
constructor(editor) {
|
|
23
|
+
this.editor = editor
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
toolbarButtons() {
|
|
27
|
+
const iconUrl = new URL("bi-body-text.svg", import.meta.url).href
|
|
28
|
+
return [{
|
|
29
|
+
name: "dummy-text",
|
|
30
|
+
title: "Insert dummy text",
|
|
31
|
+
iconUrl: iconUrl,
|
|
32
|
+
action: () => this.insertDummyText()
|
|
33
|
+
}]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
insertDummyText() {
|
|
37
|
+
const input = prompt("Word count (1\u2013100):", "20")
|
|
38
|
+
if (input === null) return
|
|
39
|
+
const count = Math.max(1, Math.min(100, parseInt(input) || 20))
|
|
40
|
+
this.editor.insertTextAtCursor(generateDummyText(count))
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-body-text" viewBox="0 0 16 16">
|
|
2
|
+
<path fill-rule="evenodd" d="M0 .5A.5.5 0 0 1 .5 0h4a.5.5 0 0 1 0 1h-4A.5.5 0 0 1 0 .5m0 2A.5.5 0 0 1 .5 2h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5m9 0a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m-9 2A.5.5 0 0 1 .5 4h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5m5 0a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m7 0a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5m-12 2A.5.5 0 0 1 .5 6h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5m8 0a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m-8 2A.5.5 0 0 1 .5 8h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m7 0a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5m-7 2a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1h-8a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5"/>
|
|
3
|
+
</svg>
|
package/index.html
CHANGED
|
@@ -1,28 +1,13 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html lang="en"
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>cm-md-editor</title>
|
|
7
|
-
<link href="
|
|
8
|
-
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
|
9
|
-
<style>
|
|
10
|
-
body {
|
|
11
|
-
padding: 1rem; margin-top: 0rem;
|
|
12
|
-
}
|
|
13
|
-
#editor {
|
|
14
|
-
width: 100%;
|
|
15
|
-
height: calc(100vh - 5rem);
|
|
16
|
-
padding: 0.7rem;
|
|
17
|
-
font-family: monospace;
|
|
18
|
-
tab-size: 4;
|
|
19
|
-
line-height: 1.3;
|
|
20
|
-
background-color: #111;
|
|
21
|
-
}
|
|
22
|
-
</style>
|
|
7
|
+
<link rel="stylesheet" href="src/cm-md-editor.css">
|
|
23
8
|
</head>
|
|
24
9
|
<body>
|
|
25
|
-
<div
|
|
10
|
+
<div>
|
|
26
11
|
<label for="editor" style="display: none">cm-md-editor</label>
|
|
27
12
|
<textarea id="editor">
|
|
28
13
|
---
|
|
@@ -88,13 +73,15 @@ Literal stars: \*not bold\* and backslash: \\
|
|
|
88
73
|
|
|
89
74
|
This has <mark>inline HTML</mark> and a <br> tag.</textarea>
|
|
90
75
|
</div>
|
|
91
|
-
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
|
92
|
-
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
|
93
|
-
crossorigin="anonymous"></script>
|
|
94
76
|
<script type="module">
|
|
95
77
|
import {MdEditor} from "./src/MdEditor.js"
|
|
78
|
+
import {defaultTools} from "./src/tools/DefaultTools.js"
|
|
79
|
+
import {Separator} from "./src/tools/Separator.js"
|
|
80
|
+
import {DummyText} from "./example-addon-tools/DummyText.js"
|
|
96
81
|
|
|
97
|
-
new MdEditor(document.getElementById('editor')
|
|
82
|
+
new MdEditor(document.getElementById('editor'), {
|
|
83
|
+
tools: [...defaultTools, Separator, DummyText]
|
|
84
|
+
})
|
|
98
85
|
</script>
|
|
99
86
|
</body>
|
|
100
87
|
</html>
|
package/package.json
CHANGED
package/src/MdEditor.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Author: Stefan Haack (https://shaack.com)
|
|
3
3
|
* Date: 2023-11-12
|
|
4
4
|
*/
|
|
5
|
+
import {defaultTools} from "./tools/DefaultTools.js"
|
|
6
|
+
|
|
5
7
|
export class MdEditor {
|
|
6
8
|
|
|
7
9
|
constructor(element, props) {
|
|
@@ -21,9 +23,17 @@ export class MdEditor {
|
|
|
21
23
|
colorEscape: "128,128,128",
|
|
22
24
|
colorFrontMatter: "128,128,200",
|
|
23
25
|
wordWrap: true,
|
|
24
|
-
|
|
26
|
+
iconsPath: new URL('./tools/icons/', import.meta.url).href,
|
|
27
|
+
tools: defaultTools,
|
|
25
28
|
...props
|
|
26
29
|
}
|
|
30
|
+
this.tools = this.props.tools.map(entry => {
|
|
31
|
+
if (Array.isArray(entry)) {
|
|
32
|
+
const [Tool, toolProps] = entry
|
|
33
|
+
return new Tool(this, toolProps)
|
|
34
|
+
}
|
|
35
|
+
return new entry(this)
|
|
36
|
+
})
|
|
27
37
|
this.element.addEventListener('keydown', (e) => this.handleKeyDown(e))
|
|
28
38
|
this.createToolbar()
|
|
29
39
|
this.createHighlightBackdrop()
|
|
@@ -41,35 +51,11 @@ export class MdEditor {
|
|
|
41
51
|
toolbar.style.cssText = 'display:flex;gap:1px;padding:2px;flex-wrap:wrap;background:rgba(128,128,128,0.15);border:1px solid rgba(128,128,128,0.3);border-bottom:none;border-radius:4px 4px 0 0;box-sizing:border-box;width:100%;'
|
|
42
52
|
wrapper.insertBefore(toolbar, this.element)
|
|
43
53
|
this.element.style.borderRadius = '0 0 4px 4px'
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
{name: 'italic', title: 'Italic', icon: '<path d="M7.991 11.674 9.53 4.455c.123-.595.246-.71 1.347-.807l.11-.52H7.211l-.11.52c1.06.096 1.128.212 1.005.807L6.57 11.674c-.123.595-.246.71-1.346.806l-.11.52h3.774l.11-.52c-1.06-.095-1.129-.211-1.006-.806z"/>', action: () => this.toggleItalic()},
|
|
50
|
-
{name: 'ul', title: 'Unordered List', icon: '<path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m-3 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2m0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2m0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/>', action: () => this.insertUnorderedList()},
|
|
51
|
-
{name: 'ol', title: 'Ordered List', icon: '<path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5"/><path d="M1.713 11.865v-.474H2c.217 0 .363-.137.363-.317 0-.185-.158-.31-.361-.31-.223 0-.367.152-.373.31h-.59c.016-.467.373-.787.986-.787.588-.002.954.291.957.703a.595.595 0 0 1-.492.594v.033a.615.615 0 0 1 .569.631c.003.533-.502.8-1.051.8-.656 0-1-.37-1.008-.794h.582c.008.178.186.306.422.309.254 0 .424-.145.422-.35-.002-.195-.155-.348-.414-.348h-.3zm-.004-4.699h-.604v-.035c0-.408.295-.844.958-.844.583 0 .96.326.96.756 0 .389-.257.617-.476.848l-.537.572v.03h1.054V9H1.143v-.395l.957-.99c.138-.142.293-.304.293-.508 0-.18-.147-.32-.342-.32a.33.33 0 0 0-.342.338zM2.564 5h-.635V2.924h-.031l-.598.42v-.567l.629-.443h.635z"/>', action: () => this.insertOrderedList()},
|
|
52
|
-
{name: 'link', title: 'Insert Link', icon: '<path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1 1 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4 4 0 0 1-.128-1.287z"/><path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"/>', action: () => this.insertLink()},
|
|
53
|
-
{name: 'image', title: 'Insert Image', icon: '<path d="M6.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0"/><path d="M1.5 2A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2zm13 1a.5.5 0 0 1 .5.5v6l-3.775-1.947a.5.5 0 0 0-.577.093l-3.71 3.71-2.66-1.772a.5.5 0 0 0-.63.062L1.002 12v.54L1 12.5v-9a.5.5 0 0 1 .5-.5z"/>', action: () => this.insertImage()},
|
|
54
|
-
]
|
|
55
|
-
const buttons = allButtons.filter(btn => this.props.toolbarButtons.includes(btn.name))
|
|
56
|
-
buttons.forEach(btn => {
|
|
57
|
-
const button = document.createElement('button')
|
|
58
|
-
button.type = 'button'
|
|
59
|
-
button.title = btn.title
|
|
60
|
-
button.style.cssText = 'background:none;border:none;border-radius:3px;cursor:pointer;padding:4px 6px;display:flex;align-items:center;justify-content:center;color:inherit;opacity:0.6;transition:opacity 0.15s,background 0.15s;'
|
|
61
|
-
button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 16 16" fill="currentColor">${btn.icon}</svg>`
|
|
62
|
-
button.addEventListener('mouseenter', () => { button.style.opacity = '1'; button.style.background = 'rgba(128,128,128,0.2)' })
|
|
63
|
-
button.addEventListener('mouseleave', () => { button.style.opacity = '0.6'; button.style.background = 'none' })
|
|
64
|
-
button.addEventListener('mousedown', (e) => {
|
|
65
|
-
e.preventDefault()
|
|
66
|
-
})
|
|
67
|
-
button.addEventListener('click', (e) => {
|
|
68
|
-
e.preventDefault()
|
|
69
|
-
btn.action()
|
|
70
|
-
})
|
|
71
|
-
toolbar.appendChild(button)
|
|
72
|
-
})
|
|
54
|
+
for (const tool of this.tools) {
|
|
55
|
+
if (typeof tool.toolbarButtons === 'function') {
|
|
56
|
+
tool.toolbarButtons().forEach(btn => this.createToolbarButton(toolbar, btn))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
73
59
|
|
|
74
60
|
// Spacer to push wrap toggle to the right
|
|
75
61
|
const spacer = document.createElement('div')
|
|
@@ -84,7 +70,9 @@ export class MdEditor {
|
|
|
84
70
|
this.wrapButton.type = 'button'
|
|
85
71
|
this.wrapButton.title = 'Toggle word wrap'
|
|
86
72
|
this.wrapButton.style.cssText = 'background:none;border:none;border-radius:3px;cursor:pointer;padding:4px 6px;display:flex;align-items:center;justify-content:center;color:inherit;opacity:0.6;transition:opacity 0.15s,background 0.15s;'
|
|
87
|
-
this.wrapButton.
|
|
73
|
+
this.wrapButton.style.width = '32px'
|
|
74
|
+
this.wrapButton.style.height = '28px'
|
|
75
|
+
this.loadIcon('text-wrap.svg').then(svg => { this.wrapButton.innerHTML = svg })
|
|
88
76
|
this.wrapButton.addEventListener('mouseenter', () => { this.wrapButton.style.opacity = '1'; this.wrapButton.style.background = 'rgba(128,128,128,0.2)' })
|
|
89
77
|
this.wrapButton.addEventListener('mouseleave', () => { this.wrapButton.style.opacity = this.wrapEnabled ? '0.9' : '0.4'; this.wrapButton.style.background = 'none' })
|
|
90
78
|
this.wrapButton.addEventListener('mousedown', (e) => e.preventDefault())
|
|
@@ -101,6 +89,48 @@ export class MdEditor {
|
|
|
101
89
|
}
|
|
102
90
|
}
|
|
103
91
|
|
|
92
|
+
createToolbarButton(toolbar, btn) {
|
|
93
|
+
if (btn.separator) {
|
|
94
|
+
const sep = document.createElement('div')
|
|
95
|
+
sep.style.cssText = 'width:1px;align-self:stretch;margin:3px 5px;background:rgba(128,128,128,0.4);'
|
|
96
|
+
toolbar.appendChild(sep)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
const button = document.createElement('button')
|
|
100
|
+
button.type = 'button'
|
|
101
|
+
button.title = btn.title
|
|
102
|
+
button.style.cssText = 'background:none;border:none;border-radius:3px;cursor:pointer;padding:4px 6px;display:flex;align-items:center;justify-content:center;color:inherit;opacity:0.6;transition:opacity 0.15s,background 0.15s;'
|
|
103
|
+
if (btn.icon) {
|
|
104
|
+
button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 16 16" fill="currentColor">${btn.icon}</svg>`
|
|
105
|
+
} else if (btn.iconUrl) {
|
|
106
|
+
button.style.width = '32px'
|
|
107
|
+
button.style.height = '28px'
|
|
108
|
+
this.loadIconFromUrl(btn.iconUrl).then(svg => { button.innerHTML = svg })
|
|
109
|
+
} else if (btn.iconFile) {
|
|
110
|
+
button.style.width = '32px'
|
|
111
|
+
button.style.height = '28px'
|
|
112
|
+
this.loadIcon(btn.iconFile).then(svg => { button.innerHTML = svg })
|
|
113
|
+
}
|
|
114
|
+
button.addEventListener('mouseenter', () => { button.style.opacity = '1'; button.style.background = 'rgba(128,128,128,0.2)' })
|
|
115
|
+
button.addEventListener('mouseleave', () => { button.style.opacity = '0.6'; button.style.background = 'none' })
|
|
116
|
+
button.addEventListener('mousedown', (e) => e.preventDefault())
|
|
117
|
+
button.addEventListener('click', (e) => {
|
|
118
|
+
e.preventDefault()
|
|
119
|
+
btn.action()
|
|
120
|
+
})
|
|
121
|
+
toolbar.appendChild(button)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
loadIcon(filename) {
|
|
125
|
+
return this.loadIconFromUrl(new URL(filename, this.props.iconsPath).href)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
loadIconFromUrl(url) {
|
|
129
|
+
return fetch(url)
|
|
130
|
+
.then(r => r.text())
|
|
131
|
+
.then(svg => svg.replace(/width="16"/, 'width="20"').replace(/height="16"/, 'height="20"'))
|
|
132
|
+
}
|
|
133
|
+
|
|
104
134
|
createHighlightBackdrop() {
|
|
105
135
|
const container = this.element.parentNode
|
|
106
136
|
container.style.position = 'relative'
|
|
@@ -338,6 +368,13 @@ export class MdEditor {
|
|
|
338
368
|
result = result.replace(/(<)(\/?[a-zA-Z]\w*)(.*?)(>)/g, (_, p1, p2, p3, p4) =>
|
|
339
369
|
this.colorSpan('colorHtmlTag', p1 + p2 + p3 + p4))
|
|
340
370
|
|
|
371
|
+
// Tool inline highlighting
|
|
372
|
+
for (const tool of this.tools) {
|
|
373
|
+
if (typeof tool.highlightInline === 'function') {
|
|
374
|
+
result = tool.highlightInline(result)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
341
378
|
return result
|
|
342
379
|
}
|
|
343
380
|
|
|
@@ -372,21 +409,6 @@ export class MdEditor {
|
|
|
372
409
|
this.element.selectionEnd = lineEnd
|
|
373
410
|
}
|
|
374
411
|
|
|
375
|
-
toggleHeading(level) {
|
|
376
|
-
this.element.focus()
|
|
377
|
-
const {lineStart, lineEnd, line} = this.getCurrentLineInfo()
|
|
378
|
-
const prefix = '#'.repeat(level) + ' '
|
|
379
|
-
const headingMatch = line.match(/^(#{1,6}) /)
|
|
380
|
-
this.selectLineRange(lineStart, lineEnd)
|
|
381
|
-
if (headingMatch && headingMatch[1].length === level) {
|
|
382
|
-
this.insertTextAtCursor(line.substring(prefix.length))
|
|
383
|
-
} else if (headingMatch) {
|
|
384
|
-
this.insertTextAtCursor(prefix + line.substring(headingMatch[0].length))
|
|
385
|
-
} else {
|
|
386
|
-
this.insertTextAtCursor(prefix + line)
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
412
|
toggleWrap(marker) {
|
|
391
413
|
const start = this.element.selectionStart
|
|
392
414
|
const end = this.element.selectionEnd
|
|
@@ -413,73 +435,13 @@ export class MdEditor {
|
|
|
413
435
|
}
|
|
414
436
|
}
|
|
415
437
|
|
|
416
|
-
toggleBold() {
|
|
417
|
-
this.toggleWrap('**')
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
toggleItalic() {
|
|
421
|
-
this.toggleWrap('_')
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
insertUnorderedList() {
|
|
425
|
-
const {lineStart, lineEnd, line} = this.getCurrentLineInfo()
|
|
426
|
-
this.selectLineRange(lineStart, lineEnd)
|
|
427
|
-
if (line.startsWith('- ')) {
|
|
428
|
-
this.insertTextAtCursor(line.substring(2))
|
|
429
|
-
} else {
|
|
430
|
-
this.insertTextAtCursor('- ' + line)
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
insertOrderedList() {
|
|
435
|
-
const {lineStart, lineEnd, line} = this.getCurrentLineInfo()
|
|
436
|
-
this.selectLineRange(lineStart, lineEnd)
|
|
437
|
-
const olMatch = line.match(/^\d+\. /)
|
|
438
|
-
if (olMatch) {
|
|
439
|
-
this.insertTextAtCursor(line.substring(olMatch[0].length))
|
|
440
|
-
} else {
|
|
441
|
-
this.insertTextAtCursor('1. ' + line)
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
insertLink() {
|
|
446
|
-
const start = this.element.selectionStart
|
|
447
|
-
const end = this.element.selectionEnd
|
|
448
|
-
const selected = this.element.value.substring(start, end)
|
|
449
|
-
const url = prompt('Enter URL:')
|
|
450
|
-
if (url === null) return
|
|
451
|
-
const linkText = selected || 'link'
|
|
452
|
-
this.element.focus()
|
|
453
|
-
this.selectLineRange(start, end)
|
|
454
|
-
this.insertTextAtCursor('[' + linkText + '](' + url + ')')
|
|
455
|
-
this.element.selectionStart = start + 1
|
|
456
|
-
this.element.selectionEnd = start + 1 + linkText.length
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
insertImage() {
|
|
460
|
-
const start = this.element.selectionStart
|
|
461
|
-
const end = this.element.selectionEnd
|
|
462
|
-
const selected = this.element.value.substring(start, end)
|
|
463
|
-
const url = prompt('Enter image URL:')
|
|
464
|
-
if (url === null) return
|
|
465
|
-
const altText = selected || 'image'
|
|
466
|
-
this.element.focus()
|
|
467
|
-
this.selectLineRange(start, end)
|
|
468
|
-
this.insertTextAtCursor('')
|
|
469
|
-
this.element.selectionStart = start + 2
|
|
470
|
-
this.element.selectionEnd = start + 2 + altText.length
|
|
471
|
-
}
|
|
472
|
-
|
|
473
438
|
insertTextAtCursor(text) {
|
|
474
439
|
// execCommand is deprecated, but without alternative to insert text and preserve the correct undo/redo stack
|
|
475
440
|
document.execCommand("insertText", false, text)
|
|
476
441
|
}
|
|
477
442
|
|
|
478
443
|
handleKeyDown(e) {
|
|
479
|
-
const
|
|
480
|
-
const end = this.element.selectionEnd
|
|
481
|
-
const before = this.element.value.substring(0, start)
|
|
482
|
-
const selected = this.element.value.substring(start, end)
|
|
444
|
+
const before = this.element.value.substring(0, this.element.selectionStart)
|
|
483
445
|
const currentLine = before.substring(before.lastIndexOf('\n') + 1)
|
|
484
446
|
const isListMode = currentLine.match(/^\t*- /) || currentLine.match(/^\t*\d+\. /)
|
|
485
447
|
if (e.key === 'Tab') {
|
|
@@ -496,17 +458,16 @@ export class MdEditor {
|
|
|
496
458
|
} else if (e.key === 'Enter') {
|
|
497
459
|
this.handleEnterKey(e)
|
|
498
460
|
} else if (e.ctrlKey || e.metaKey) {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
this.element.selectionEnd = start + 10 + selected.length
|
|
461
|
+
for (const tool of this.tools) {
|
|
462
|
+
if (typeof tool.keyboardShortcuts === 'function') {
|
|
463
|
+
for (const shortcut of tool.keyboardShortcuts()) {
|
|
464
|
+
if (shortcut.ctrlOrMeta && shortcut.key === e.key) {
|
|
465
|
+
e.preventDefault()
|
|
466
|
+
shortcut.action(e)
|
|
467
|
+
return
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
510
471
|
}
|
|
511
472
|
}
|
|
512
473
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
body {
|
|
2
|
+
margin: 0;
|
|
3
|
+
padding: 1rem;
|
|
4
|
+
background-color: #f8f9fa;
|
|
5
|
+
color: #212529;
|
|
6
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
*, *::before, *::after {
|
|
10
|
+
box-sizing: border-box;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
textarea {
|
|
14
|
+
width: 100%;
|
|
15
|
+
height: calc(100vh - 4rem);
|
|
16
|
+
padding: 0.7rem;
|
|
17
|
+
font-family: monospace;
|
|
18
|
+
font-size: 1rem;
|
|
19
|
+
tab-size: 4;
|
|
20
|
+
line-height: 1.3;
|
|
21
|
+
background-color: #fff;
|
|
22
|
+
color: #212529;
|
|
23
|
+
border: 1px solid rgba(128, 128, 128, 0.3);
|
|
24
|
+
border-radius: 4px;
|
|
25
|
+
resize: vertical;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@media (prefers-color-scheme: dark) {
|
|
29
|
+
body {
|
|
30
|
+
background-color: #212529;
|
|
31
|
+
color: #dee2e6;
|
|
32
|
+
}
|
|
33
|
+
textarea {
|
|
34
|
+
background-color: #111;
|
|
35
|
+
color: #dee2e6;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export class Bold {
|
|
2
|
+
constructor(editor) {
|
|
3
|
+
this.editor = editor
|
|
4
|
+
}
|
|
5
|
+
toolbarButtons() {
|
|
6
|
+
return [{name: 'bold', title: 'Bold', iconFile: 'type-bold.svg', action: () => this.editor.toggleWrap('**')}]
|
|
7
|
+
}
|
|
8
|
+
keyboardShortcuts() {
|
|
9
|
+
return [{key: 'b', ctrlOrMeta: true, action: () => this.editor.toggleWrap('**')}]
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {Headings} from "./Headings.js"
|
|
2
|
+
import {Bold} from "./Bold.js"
|
|
3
|
+
import {Italic} from "./Italic.js"
|
|
4
|
+
import {Strikethrough} from "./Strikethrough.js"
|
|
5
|
+
import {UnorderedList} from "./UnorderedList.js"
|
|
6
|
+
import {OrderedList} from "./OrderedList.js"
|
|
7
|
+
import {InsertLink} from "./InsertLink.js"
|
|
8
|
+
import {InsertImage} from "./InsertImage.js"
|
|
9
|
+
import {Separator} from "./Separator.js"
|
|
10
|
+
|
|
11
|
+
export const defaultTools = [Headings, Separator,
|
|
12
|
+
Bold, Italic, Strikethrough, Separator,
|
|
13
|
+
UnorderedList, OrderedList, Separator,
|
|
14
|
+
InsertLink, InsertImage]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export class Headings {
|
|
2
|
+
constructor(editor, props = {}) {
|
|
3
|
+
this.editor = editor
|
|
4
|
+
this.minLevel = props.minLevel || 1
|
|
5
|
+
this.maxLevel = props.maxLevel || 3
|
|
6
|
+
}
|
|
7
|
+
toolbarButtons() {
|
|
8
|
+
const buttons = []
|
|
9
|
+
for (let level = this.minLevel; level <= this.maxLevel; level++) {
|
|
10
|
+
buttons.push({
|
|
11
|
+
name: 'h' + level,
|
|
12
|
+
title: 'Heading ' + level,
|
|
13
|
+
iconFile: 'type-h' + level + '.svg',
|
|
14
|
+
action: () => this.toggleHeading(level)
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
return buttons
|
|
18
|
+
}
|
|
19
|
+
toggleHeading(level) {
|
|
20
|
+
const editor = this.editor
|
|
21
|
+
editor.element.focus()
|
|
22
|
+
const {lineStart, lineEnd, line} = editor.getCurrentLineInfo()
|
|
23
|
+
const prefix = '#'.repeat(level) + ' '
|
|
24
|
+
const headingMatch = line.match(/^(#{1,6}) /)
|
|
25
|
+
editor.selectLineRange(lineStart, lineEnd)
|
|
26
|
+
if (headingMatch && headingMatch[1].length === level) {
|
|
27
|
+
editor.insertTextAtCursor(line.substring(prefix.length))
|
|
28
|
+
} else if (headingMatch) {
|
|
29
|
+
editor.insertTextAtCursor(prefix + line.substring(headingMatch[0].length))
|
|
30
|
+
} else {
|
|
31
|
+
editor.insertTextAtCursor(prefix + line)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class InsertImage {
|
|
2
|
+
constructor(editor) {
|
|
3
|
+
this.editor = editor
|
|
4
|
+
}
|
|
5
|
+
toolbarButtons() {
|
|
6
|
+
return [{name: 'image', title: 'Insert Image', iconFile: 'card-image.svg', action: () => this.insertImage()}]
|
|
7
|
+
}
|
|
8
|
+
insertImage() {
|
|
9
|
+
const el = this.editor.element
|
|
10
|
+
const start = el.selectionStart
|
|
11
|
+
const end = el.selectionEnd
|
|
12
|
+
const selected = el.value.substring(start, end)
|
|
13
|
+
const url = prompt('Enter image URL:')
|
|
14
|
+
if (url === null) return
|
|
15
|
+
const altText = selected || 'image'
|
|
16
|
+
el.focus()
|
|
17
|
+
this.editor.selectLineRange(start, end)
|
|
18
|
+
this.editor.insertTextAtCursor('')
|
|
19
|
+
el.selectionStart = start + 2
|
|
20
|
+
el.selectionEnd = start + 2 + altText.length
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class InsertLink {
|
|
2
|
+
constructor(editor) {
|
|
3
|
+
this.editor = editor
|
|
4
|
+
}
|
|
5
|
+
toolbarButtons() {
|
|
6
|
+
return [{name: 'link', title: 'Insert Link', iconFile: 'link-45deg.svg', action: () => this.insertLink()}]
|
|
7
|
+
}
|
|
8
|
+
insertLink() {
|
|
9
|
+
const el = this.editor.element
|
|
10
|
+
const start = el.selectionStart
|
|
11
|
+
const end = el.selectionEnd
|
|
12
|
+
const selected = el.value.substring(start, end)
|
|
13
|
+
const url = prompt('Enter URL:')
|
|
14
|
+
if (url === null) return
|
|
15
|
+
const linkText = selected || 'link'
|
|
16
|
+
el.focus()
|
|
17
|
+
this.editor.selectLineRange(start, end)
|
|
18
|
+
this.editor.insertTextAtCursor('[' + linkText + '](' + url + ')')
|
|
19
|
+
el.selectionStart = start + 1
|
|
20
|
+
el.selectionEnd = start + 1 + linkText.length
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export class Italic {
|
|
2
|
+
constructor(editor) {
|
|
3
|
+
this.editor = editor
|
|
4
|
+
}
|
|
5
|
+
toolbarButtons() {
|
|
6
|
+
return [{name: 'italic', title: 'Italic', iconFile: 'type-italic.svg', action: () => this.editor.toggleWrap('_')}]
|
|
7
|
+
}
|
|
8
|
+
keyboardShortcuts() {
|
|
9
|
+
return [{key: 'i', ctrlOrMeta: true, action: () => this.editor.toggleWrap('_')}]
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class OrderedList {
|
|
2
|
+
constructor(editor) {
|
|
3
|
+
this.editor = editor
|
|
4
|
+
}
|
|
5
|
+
toolbarButtons() {
|
|
6
|
+
return [{name: 'ol', title: 'Ordered List', iconFile: 'list-ol.svg', action: () => this.insertOrderedList()}]
|
|
7
|
+
}
|
|
8
|
+
insertOrderedList() {
|
|
9
|
+
const editor = this.editor
|
|
10
|
+
const {lineStart, lineEnd, line} = editor.getCurrentLineInfo()
|
|
11
|
+
editor.selectLineRange(lineStart, lineEnd)
|
|
12
|
+
const olMatch = line.match(/^\d+\. /)
|
|
13
|
+
if (olMatch) {
|
|
14
|
+
editor.insertTextAtCursor(line.substring(olMatch[0].length))
|
|
15
|
+
} else {
|
|
16
|
+
editor.insertTextAtCursor('1. ' + line)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class UnorderedList {
|
|
2
|
+
constructor(editor) {
|
|
3
|
+
this.editor = editor
|
|
4
|
+
}
|
|
5
|
+
toolbarButtons() {
|
|
6
|
+
return [{name: 'ul', title: 'Unordered List', iconFile: 'list-ul.svg', action: () => this.insertUnorderedList()}]
|
|
7
|
+
}
|
|
8
|
+
insertUnorderedList() {
|
|
9
|
+
const editor = this.editor
|
|
10
|
+
const {lineStart, lineEnd, line} = editor.getCurrentLineInfo()
|
|
11
|
+
editor.selectLineRange(lineStart, lineEnd)
|
|
12
|
+
if (line.startsWith('- ')) {
|
|
13
|
+
editor.insertTextAtCursor(line.substring(2))
|
|
14
|
+
} else {
|
|
15
|
+
editor.insertTextAtCursor('- ' + line)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019-2024 The Bootstrap Authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-card-image" viewBox="0 0 16 16">
|
|
2
|
+
<path d="M6.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0"/>
|
|
3
|
+
<path d="M1.5 2A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2zm13 1a.5.5 0 0 1 .5.5v6l-3.775-1.947a.5.5 0 0 0-.577.093l-3.71 3.71-2.66-1.772a.5.5 0 0 0-.63.062L1.002 12v.54L1 12.5v-9a.5.5 0 0 1 .5-.5z"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-link-45deg" viewBox="0 0 16 16">
|
|
2
|
+
<path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1 1 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4 4 0 0 1-.128-1.287z"/>
|
|
3
|
+
<path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-list-ol" viewBox="0 0 16 16">
|
|
2
|
+
<path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5"/>
|
|
3
|
+
<path d="M1.713 11.865v-.474H2c.217 0 .363-.137.363-.317 0-.185-.158-.31-.361-.31-.223 0-.367.152-.373.31h-.59c.016-.467.373-.787.986-.787.588-.002.954.291.957.703a.595.595 0 0 1-.492.594v.033a.615.615 0 0 1 .569.631c.003.533-.502.8-1.051.8-.656 0-1-.37-1.008-.794h.582c.008.178.186.306.422.309.254 0 .424-.145.422-.35-.002-.195-.155-.348-.414-.348h-.3zm-.004-4.699h-.604v-.035c0-.408.295-.844.958-.844.583 0 .96.326.96.756 0 .389-.257.617-.476.848l-.537.572v.03h1.054V9H1.143v-.395l.957-.99c.138-.142.293-.304.293-.508 0-.18-.147-.32-.342-.32a.33.33 0 0 0-.342.338zM2.564 5h-.635V2.924h-.031l-.598.42v-.567l.629-.443h.635z"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-list-ul" viewBox="0 0 16 16">
|
|
2
|
+
<path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m-3 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2m0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2m0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-text-wrap" viewBox="0 0 16 16">
|
|
2
|
+
<path fill-rule="evenodd" d="M2 3.5a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11a.5.5 0 0 1-.5-.5m0 4a.5.5 0 0 1 .5-.5h9a2.5 2.5 0 0 1 0 5h-1.293l.647.646a.5.5 0 0 1-.708.708l-1.5-1.5a.5.5 0 0 1 0-.708l1.5-1.5a.5.5 0 0 1 .708.708l-.647.646H11.5a1.5 1.5 0 0 0 0-3h-9a.5.5 0 0 1-.5-.5m0 4a.5.5 0 0 1 .5-.5H7a.5.5 0 0 1 0 1H2.5a.5.5 0 0 1-.5-.5"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-bold" viewBox="0 0 16 16">
|
|
2
|
+
<path d="M8.21 13c2.106 0 3.412-1.087 3.412-2.823 0-1.306-.984-2.283-2.324-2.386v-.055a2.176 2.176 0 0 0 1.852-2.14c0-1.51-1.162-2.46-3.014-2.46H3.843V13zM5.908 4.674h1.696c.963 0 1.517.451 1.517 1.244 0 .834-.629 1.32-1.73 1.32H5.908V4.673zm0 6.788V8.598h1.73c1.217 0 1.88.492 1.88 1.415 0 .943-.643 1.449-1.832 1.449H5.907z"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-h2" viewBox="0 0 16 16">
|
|
2
|
+
<path d="M7.495 13V3.201H6.174v4.15H1.32V3.2H0V13h1.32V8.513h4.854V13zm3.174-7.071v-.05c0-.934.66-1.752 1.801-1.752 1.005 0 1.76.639 1.76 1.651 0 .898-.582 1.58-1.12 2.19l-3.69 4.2V13h6.331v-1.149h-4.458v-.079L13.9 8.786c.919-1.048 1.666-1.874 1.666-3.101C15.565 4.149 14.35 3 12.499 3 10.46 3 9.384 4.393 9.384 5.879v.05z"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-h3" viewBox="0 0 16 16">
|
|
2
|
+
<path d="M11.07 8.4h1.049c1.174 0 1.99.69 2.004 1.724s-.802 1.786-2.068 1.779c-1.11-.007-1.905-.605-1.99-1.357h-1.21C8.926 11.91 10.116 13 12.028 13c1.99 0 3.439-1.188 3.404-2.87-.028-1.553-1.287-2.221-2.096-2.313v-.07c.724-.127 1.814-.935 1.772-2.293-.035-1.392-1.21-2.468-3.038-2.454-1.927.007-2.94 1.196-2.981 2.426h1.23c.064-.71.732-1.336 1.744-1.336 1.027 0 1.744.64 1.744 1.568.007.95-.738 1.639-1.744 1.639h-.991V8.4ZM7.495 13V3.201H6.174v4.15H1.32V3.2H0V13h1.32V8.513h4.854V13z"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-italic" viewBox="0 0 16 16">
|
|
2
|
+
<path d="M7.991 11.674 9.53 4.455c.123-.595.246-.71 1.347-.807l.11-.52H7.211l-.11.52c1.06.096 1.128.212 1.005.807L6.57 11.674c-.123.595-.246.71-1.346.806l-.11.52h3.774l.11-.52c-1.06-.095-1.129-.211-1.006-.806z"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type-strikethrough" viewBox="0 0 16 16">
|
|
2
|
+
<path d="M6.333 5.686c0 .31.083.581.27.814H5.166a2.8 2.8 0 0 1-.099-.76c0-1.627 1.436-2.768 3.48-2.768 1.969 0 3.39 1.175 3.445 2.85h-1.23c-.11-1.08-.964-1.743-2.25-1.743-1.23 0-2.18.602-2.18 1.607zm2.194 7.478c-2.153 0-3.589-1.107-3.705-2.81h1.23c.144 1.06 1.129 1.703 2.544 1.703 1.34 0 2.31-.705 2.31-1.675 0-.827-.547-1.374-1.914-1.675L8.046 8.5H1v-1h14v1h-3.504c.468.437.675.994.675 1.697 0 1.826-1.436 2.967-3.644 2.967"/>
|
|
3
|
+
</svg>
|