Skip to content

Commit 92f94b8

Browse files
authored
Merge pull request #11 from madeyoga/v0.2.0
Add image upload command
2 parents 85b7e10 + 5b02280 commit 92f94b8

File tree

7 files changed

+234
-48
lines changed

7 files changed

+234
-48
lines changed

dist/chunmde.bundle.min.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

index.html

+13-1
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,23 @@ <h2>Preview:</h2>
4848

4949
<script src="dist/chunmde.bundle.min.js"></script>
5050
<script>
51-
let editor = new ChunMDE('editor-container', {
51+
const { createChunEditor, createImageUploadPlugin } = Chun
52+
53+
const imageUploadPlugin = createImageUploadPlugin({
54+
imageUploadUrl: "",
55+
imageFormats: ["image/jpg", "image/jpeg", "image/gif", "image/png", "image/bmp", "image/webp"],
56+
})
57+
58+
const editor = createChunEditor({
5259
doc: initialContent.value,
5360
lineWrapping: true,
5461
indentWithTab: true,
62+
toolbar: true,
5563
})
64+
.use(imageUploadPlugin)
65+
.mount("editor-container")
66+
67+
console.log(editor)
5668
</script>
5769

5870
</body>

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "chunmde",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"description": "Markdown editor based on codemirror 6",
55
"main": "dist/chunmde.bundle.js",
66
"type": "module",

src/components/Toolbar.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class Toolbar {
1717
public buttonActions: ToolbarButton[]
1818
public dom: Element
1919

20-
constructor(editor: EditorView) {
20+
constructor(editor: EditorView, toolbarItems: ((editor: EditorView) => HTMLElement)[] = []) {
2121
this.buttonActions = [
2222
{
2323
text: "Add heading text",
@@ -79,6 +79,10 @@ export class Toolbar {
7979
toolbarElement.appendChild(buttonElement)
8080
}
8181

82+
for (let createToolbarItem of toolbarItems) {
83+
toolbarElement.appendChild(createToolbarItem(editor))
84+
}
85+
8286
this.dom = toolbarElement
8387
}
8488
}

src/index.ts

+101-44
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { markdown, markdownLanguage } from "@codemirror/lang-markdown"
22
import { EditorState, EditorStateConfig } from '@codemirror/state'
3-
import { keymap, ViewUpdate, EditorView } from '@codemirror/view'
3+
import { keymap, ViewUpdate, EditorView, KeyBinding } from '@codemirror/view'
44
import { basicSetup } from 'codemirror'
55
import { indentWithTab } from "@codemirror/commands"
66
import { italicKeyBinding } from './commands/Italic'
@@ -10,6 +10,8 @@ import { linkKeyBinding } from "./commands/Link"
1010
import { quoteKeyBinding } from "./commands/Quote"
1111
import { ulKeyBinding } from "./commands/BulletedList"
1212
import { Toolbar } from "./components/Toolbar"
13+
import IEditorPlugin from "./plugins/IEditorPlugin"
14+
import { createImageUploadPlugin } from "./plugins/ImageUpload"
1315

1416
interface ChunInterface {
1517
dom: Element,
@@ -20,59 +22,39 @@ interface ChunInterface {
2022

2123
interface ChunConfig extends EditorStateConfig {
2224
onUpdateListener?: (update: ViewUpdate) => void,
23-
indentWithTab?: boolean,
24-
lineWrapping?: boolean,
25+
toolbar: boolean,
26+
indentWithTab: boolean,
27+
lineWrapping: boolean,
28+
toolbarItems: ((editor: EditorView) => HTMLElement)[],
2529
}
2630

27-
function ChunMDE(this: ChunInterface, containerId: string, customConfig?: ChunConfig) {
31+
const ChunMDE = function ChunMDE(this: ChunInterface, containerId: string, customConfig: ChunConfig = {
32+
doc: "",
33+
toolbar: true,
34+
indentWithTab: true,
35+
lineWrapping: false,
36+
toolbarItems: []
37+
}) {
2838
const parentElement = document.getElementById(containerId) as Element
2939

30-
const defaultKeybinds = [
31-
italicKeyBinding,
32-
boldKeyBinding,
33-
codeKeyBinding,
34-
linkKeyBinding,
35-
quoteKeyBinding,
36-
ulKeyBinding,
37-
]
38-
39-
const defaultExtensions = [
40-
keymap.of(defaultKeybinds),
41-
markdown({ base: markdownLanguage }),
42-
basicSetup,
43-
]
44-
45-
let config: EditorStateConfig = {
46-
doc: "Start writing!",
47-
extensions: defaultExtensions,
48-
}
49-
50-
if (customConfig) {
51-
if (customConfig.onUpdateListener) {
52-
defaultExtensions.push(EditorView.updateListener.of(customConfig.onUpdateListener!))
53-
}
54-
if (customConfig.lineWrapping) {
55-
defaultExtensions.push(EditorView.lineWrapping)
56-
}
57-
if (customConfig.indentWithTab) {
58-
defaultExtensions.push(keymap.of([indentWithTab]))
59-
}
60-
config.doc = customConfig.doc ? customConfig.doc : config.doc
61-
config.extensions = customConfig.extensions ? customConfig.extensions : defaultExtensions
40+
const config: EditorStateConfig = {
41+
doc: customConfig.doc,
42+
extensions: customConfig.extensions,
6243
}
6344

6445
/** CodeMirror6's EditorView */
6546
const editorView = new EditorView({
66-
// parent: parentElement,
6747
state: EditorState.create(config)
6848
})
6949

7050
parentElement.className += " chunmde-container"
7151

7252
// toolbar
73-
const toolbar = new Toolbar(editorView)
74-
75-
parentElement.appendChild(toolbar.dom)
53+
if (customConfig.toolbar) {
54+
const toolbar = new Toolbar(editorView, customConfig.toolbarItems)
55+
parentElement.appendChild(toolbar.dom)
56+
this.toolbar = toolbar
57+
}
7658
parentElement.appendChild(editorView.dom)
7759

7860
/** Shortcut to get the editor value */
@@ -81,14 +63,89 @@ function ChunMDE(this: ChunInterface, containerId: string, customConfig?: ChunCo
8163
}
8264

8365
this.dom = parentElement
84-
this.toolbar = toolbar
8566
this.editor = editorView
67+
} as any as { new (containerId: string, customConfig?: ChunConfig): ChunInterface; }
68+
69+
interface IEditorBuilder {
70+
use: (plugin: IEditorPlugin) => IEditorBuilder;
71+
mount: (selector: string) => void;
72+
}
73+
74+
export function createChunEditor(customConfig: ChunConfig = {
75+
doc: "",
76+
toolbar: true,
77+
indentWithTab: true,
78+
lineWrapping: false,
79+
toolbarItems: [],
80+
}): IEditorBuilder {
81+
82+
const defaultKeybinds = [
83+
italicKeyBinding,
84+
boldKeyBinding,
85+
codeKeyBinding,
86+
linkKeyBinding,
87+
quoteKeyBinding,
88+
ulKeyBinding,
89+
]
90+
91+
const defaultExtensions = [
92+
keymap.of(defaultKeybinds),
93+
markdown({ base: markdownLanguage }),
94+
basicSetup,
95+
]
96+
97+
const keybinds: KeyBinding[] = []
98+
const toolbarItemDelegates: ((editor: EditorView) => HTMLButtonElement)[] = []
99+
100+
return {
101+
use(plugin) {
102+
if (plugin.keybind) {
103+
keybinds.push(plugin.keybind)
104+
}
105+
106+
if (plugin.createToolbarItem) {
107+
toolbarItemDelegates.push(plugin.createToolbarItem)
108+
}
109+
110+
return this
111+
},
112+
mount(selector) {
113+
114+
if (customConfig.onUpdateListener) {
115+
defaultExtensions.push(EditorView.updateListener.of(customConfig.onUpdateListener!))
116+
}
117+
if (customConfig.lineWrapping) {
118+
defaultExtensions.push(EditorView.lineWrapping)
119+
}
120+
if (customConfig.indentWithTab) {
121+
keybinds.push(indentWithTab)
122+
}
123+
124+
defaultExtensions.push(keymap.of(keybinds))
125+
customConfig.extensions = defaultExtensions
126+
customConfig.toolbarItems = toolbarItemDelegates
127+
128+
return new ChunMDE(selector, customConfig)
129+
},
130+
}
131+
}
132+
133+
interface IGlobalChunEditor {
134+
createChunEditor: () => IEditorBuilder,
135+
createImageUploadPlugin: (imageUploadUrl: string, imageFormats: string[]) => IEditorPlugin,
86136
}
87137

88138
declare global {
89-
interface Window { ChunMDE: typeof ChunMDE; }
139+
interface Window { ChunMDE: typeof ChunMDE; Chun: IGlobalChunEditor }
90140
}
91141

92-
window.ChunMDE = ChunMDE;
142+
const Chun = {
143+
createChunEditor,
144+
createImageUploadPlugin,
145+
}
146+
147+
window.ChunMDE = ChunMDE
148+
window.Chun = Chun;
149+
150+
export default Chun;
93151

94-
export default ChunMDE;

src/plugins/IEditorPlugin.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { EditorView } from "@codemirror/view";
2+
3+
export default interface IEditorPlugin {
4+
createToolbarItem?: (editor: EditorView) => HTMLButtonElement,
5+
keybind?: {
6+
key: string;
7+
run: (view: EditorView) => boolean;
8+
},
9+
}

src/plugins/ImageUpload.ts

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { EditorView } from "@codemirror/view";
2+
import IEditorPlugin from "./IEditorPlugin"
3+
import { EditorSelection } from "@codemirror/state";
4+
import { trimSelection } from "../commands/Utilities";
5+
6+
7+
function wrapImageUrl(view: EditorView, imageUrl: string) {
8+
9+
const transaction = view.state.changeByRange((range) => {
10+
const originalText = view.state.sliceDoc(range.from, range.to)
11+
const { text, rangeFrom, rangeTo } = trimSelection(originalText, range)
12+
13+
const newText = `![${text}](${imageUrl})`
14+
15+
const transaction = {
16+
changes: {
17+
from: rangeFrom,
18+
insert: newText,
19+
to: rangeTo,
20+
},
21+
range: EditorSelection.range(rangeFrom + 2, rangeFrom + 2 + text.length),
22+
}
23+
24+
return transaction
25+
})
26+
27+
view.dispatch(transaction)
28+
return
29+
}
30+
31+
32+
export const createImageUploadPlugin: (config: any) => IEditorPlugin = (
33+
config: any = {
34+
imageUploadUrl: "",
35+
imageFormats: ["image/jpg", "image/jpeg", "image/gif", "image/png", "image/bmp", "image/webp"],
36+
csrfToken: "",
37+
csrfFieldName: "",
38+
}
39+
) => {
40+
41+
return {
42+
createToolbarItem: (editor: EditorView) => {
43+
44+
const fileInput = document.createElement("input")
45+
fileInput.type = "file"
46+
fileInput.style.display = "none";
47+
48+
fileInput.setAttribute("accept", config.imageFormats.join(","))
49+
fileInput.addEventListener("change", async (event) => {
50+
const files = (event.target as HTMLInputElement).files
51+
if (files && files[0]) {
52+
const formData = new FormData();
53+
54+
formData.append('image', files[0]);
55+
56+
if (config.csrfFieldName && config.csrfToken) {
57+
formData.append(config.csrfFieldName, config.csrfToken)
58+
}
59+
60+
const resp = await fetch(config.imageUploadUrl, {
61+
method: "POST",
62+
body: formData,
63+
})
64+
65+
if (resp.ok) {
66+
const obj = await resp.json()
67+
wrapImageUrl(editor, obj.url)
68+
}
69+
else {
70+
const text = await resp.text()
71+
alert("Error uploading image: " + resp.status + resp.statusText + text)
72+
}
73+
}
74+
})
75+
76+
const icon = document.createElement("iconify-icon")
77+
icon.className += "iconify "
78+
icon.setAttribute("icon", "mdi:image-outline")
79+
icon.setAttribute("inline", "false")
80+
icon.setAttribute("width", "16")
81+
icon.setAttribute("height", "16")
82+
83+
const buttonElement = document.createElement("button")
84+
buttonElement.appendChild(icon)
85+
buttonElement.setAttribute("alt", "Select and upload an image")
86+
buttonElement.setAttribute("title", "Select and upload an image")
87+
buttonElement.setAttribute("type", "button")
88+
buttonElement.addEventListener("click", () => {
89+
fileInput.click()
90+
})
91+
92+
buttonElement.appendChild(fileInput)
93+
94+
return buttonElement
95+
},
96+
// keybind: {
97+
// key: "",
98+
// run: (editor) => {
99+
// fileInput.click()
100+
// return true
101+
// },
102+
// }
103+
}
104+
}

0 commit comments

Comments
 (0)