ace markdown editor 原生web components

src/index.html:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <ace-markdown></ace-markdown>
    <ace-markdown></ace-markdown>
    <script type="module" src="./ace.markdown.ts"></script>
</body>

</html>

src/ace.markdown.ts:

import ace from "ace-builds/src-min-noconflict/ace.js";
import { marked } from "marked";

class AceMarkdown extends HTMLElement {
	attatched = false;
	containerElement!: HTMLDivElement;
	content = "# Hello world";
	private editor!: any;
	editorElement!: HTMLDivElement;
	fullElement!: HTMLButtonElement;
	fullscreen = false;
	previewerElement!: HTMLDivElement;
	saveElement!: HTMLButtonElement;

	connectedCallback() {
		if (!this.attatched) {
			this.attatched = true;

			this.containerElement = this.shadowRoot?.querySelector("#container") as HTMLDivElement;
			this.editorElement = this.shadowRoot?.querySelector("#editor") as HTMLDivElement;
			this.fullElement = this.shadowRoot?.querySelector("#full") as HTMLButtonElement;
			this.previewerElement = this.shadowRoot?.querySelector("#previewer") as HTMLDivElement;
			this.saveElement = this.shadowRoot?.querySelector("#save") as HTMLButtonElement;

			ace.config.set("basePath", "/");
			const editor = ace.edit(this.editorElement, {
				// fontSize: "14px",
				mode: "ace/mode/markdown",
				// theme: "ace/theme/monokai",
				value: this.content,
				wrap: true,
			});
			this.editor = editor;
			editor.renderer.attachToShadowRoot(); // !!!important
			editor.on("input", () => {
				this.updatePreview();
			});
			this.updatePreview();

			this.fullElement.onclick = () => {
				this.fullscreenSwitch();
			}

			this.saveElement.onclick = () => {
				this.save();
			}
		}
	}

	constructor() {
		super();
		this.attachShadow({ mode: "open" });
		this.render();
	}

	fullscreenIcon() {
		return this.fullscreen ? "-" : "+";
	}

	fullscreenSwitch() {
		this.fullscreen = !this.fullscreen;
		if (this.fullscreen) {
			this.containerElement.requestFullscreen();
			this.containerElement.style.height = "100vh";
		} else {
			document.exitFullscreen();
			this.containerElement.style.height = "40vh";
		}
	}

	render() {
		if (this.shadowRoot) {
			this.shadowRoot.innerHTML = `<div id="container" style="height: 40vh">
			<div>
				<div style="display: flex; justify-content: end">
					<button id="save">下载</button>
					<button id="full" title="全屏/还原">${this.fullscreenIcon()}</button>
				</div>
			</div>
			<div style="display: flex; height: 100%">
				<div style="flex: 50%">
					<div id="editor" style="width: 100%; height: 100%;">${this.content}</div>
				</div>
				<div style="flex: 50%">
					<div id="previewer" style="width: 100%; height: 100%; overflow: auto"></div>
				</div>
			</div>
		</div>`;
		}
	}

	save() {
		const fileParts = [this.content];
		const blob = new Blob(fileParts, { type: "text/plain" });
		const a = document.createElement("a");
		a.href = URL.createObjectURL(blob);
		a.download = "paper.md";
		document.body.appendChild(a);
		a.click();
		document.body.removeChild(a);
	}

	updatePreview() {
		this.content = this.editor.getValue();
		this.previewerElement.innerHTML = marked(this.content).toString();
	}
}

customElements.define("ace-markdown", AceMarkdown);

declare global {
	interface HTMLElementTagNameMap {
		"ace-markdown": AceMarkdown;
	}
}

justfile:

build:
    #!/usr/bin/env bash
    rm -rf dist
    parcel build ./src/**.html --public-url . --no-source-maps
    cp node_modules/ace-builds/src-min-noconflict/mode-markdown.js dist/
    cp node_modules/ace-builds/src-min-noconflict/theme-monokai.js dist/
posted @   卓能文  阅读(25)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示