Lit로 리치 텍스트 편집기 빌드

35769 단어
이 문서에서는 Lit 웹 구성 요소를 설정하고 이를 사용하여 서식 있는 텍스트 편집기를 만드는 방법을 살펴보겠습니다.

TLDR 최종 소스here 및 온라인demo .

전제 조건 #


  • Vscode
  • 노드 >= 12
  • 타자기

  • 시작하기 #



    터미널에서 프로젝트 위치로 이동하여 다음을 실행하여 시작할 수 있습니다.

    npm init @vitejs/app --template lit-element-ts
    


    그런 다음 프로젝트 이름lit-rich-text-editor을 입력하고 이제 vscode에서 프로젝트를 열고 종속 항목을 설치합니다.

    cd lit-rich-text-editornpm i @material/mwc-icon-buttonnpm i -D @types/nodecode .
    

    vite.config.ts를 다음과 같이 업데이트합니다.

    import { defineConfig } from "vite";import { resolve } from "path";export default defineConfig({ base: '/lit-rich-text-editor/', build: { lib: { entry: "src/lit-rich-text-editor.ts", formats: ["es"], }, rollupOptions: { input: { main: resolve(__dirname, "index.html"), }, }, },});
    


    템플릿 번호


    index.html를 열고 다음과 같이 업데이트합니다.

    <!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link href="https://fonts.googleapis.com/css?family=Material+Icons&display=block" rel="stylesheet" /> <title>Lit Rich Text Editor</title> <script type="module" src="/src/lit-rich-text-editor.ts"></script> <style> body { padding: 0; margin: 0; } lit-rich-text-editor { --editor-width: 100%; --editor-height: 100vh; } </style> </head> <body> <lit-rich-text-editor> <template> <h1>Headline 1</h1> <p>This is a paragraph.</p> <p> <span style="background-color: rgb(255, 0, 0)" ><font color="#ffffff">Styled Text</font></span > </p> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. </p> </template> </lit-rich-text-editor> </body></html>
    


    빼야 할 중요한 사항은 본문 패딩을 제거하고 전체 뷰포트를 차지하도록 편집기에 크기CSS Custom Properties를 보내기 위해 추가된 스타일입니다.
    lit-rich-text-editor 태그 내부에는 렌더링되지 않지만 액세스할 수 있는 html을 제공하기 위해 슬롯으로 전달된 template 이 있습니다.

    Material Icons에 대한 가져오기도 있으므로 나중에 편집기에서 사용할 수 있습니다.

    편집자 #



    다음으로 생성할 것은 편집기 자체입니다. src/lit-rich-text-editor.ts를 열고 다음과 같이 업데이트합니다.

    import { LitElement, html, customElement, css, state } from "lit-element";import "@material/mwc-icon-button";@customElement("lit-rich-text-editor")export class LitRichTextEditor extends LitElement { @state() content: string = ""; @state() root: Element | null = null; static styles = css` :host { --editor-width: 600px; --editor-height: 600px; --editor-background: #f1f1f1; --editor-toolbar-height: 33px; --editor-toolbar-background: black; --editor-toolbar-on-background: white; --editor-toolbar-on-active-background: #a4a4a4; } main { width: var(--editor-width); height: var(--editor-height); display: grid; grid-template-areas: "toolbar toolbar" "editor editor"; grid-template-rows: var(--editor-toolbar-height) auto; grid-template-columns: auto auto; } #editor-actions { grid-area: toolbar; width: var(--editor-width); height: var(--editor-toolbar-height); background-color: var(--editor-toolbar-background); color: var(--editor-toolbar-on-background); overscroll-behavior: contain; overflow-y: auto; -ms-overflow-style: none; scrollbar-width: none; } #editor-actions::-webkit-scrollbar { display: none; } #editor { width: var(--editor-width); grid-area: editor; background-color: var(--editor-background); } #toolbar { width: 1090px; height: var(--editor-toolbar-height); } [contenteditable] { outline: 0px solid transparent; } #toolbar > mwc-icon-button { color: var(--editor-toolbar-on-background); --mdc-icon-size: 20px; --mdc-icon-button-size: 30px; cursor: pointer; } #toolbar > .active { color: var(--editor-toolbar-on-active-background); } select { margin-top: 5px; height: calc(var(--editor-toolbar-height) - 10px); } input[type="color"] { height: calc(var(--editor-toolbar-height) - 15px); -webkit-appearance: none; border: none; width: 22px; } input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; } input[type="color"]::-webkit-color-swatch { border: none; } `; render() { return html`<main> <input id="bg" type="color" style="display:none" /> <input id="fg" type="color" style="display:none" /> <div id="editor-actions"> <div id="toolbar"> </div> </div> <div id="editor">${this.root}</div> </main> `; } async firstUpdated() { const elem = this.parentElement!.querySelector("lit-rich-text-editor template"); this.content = elem?.innerHTML ?? ""; this.reset(); } reset() { const parser = new DOMParser(); const doc = parser.parseFromString(this.content, "text/html"); document.execCommand("defaultParagraphSeparator", false, "br"); document.addEventListener("selectionchange", () => { this.requestUpdate(); }); const root = doc.querySelector("body"); root!.setAttribute("contenteditable", "true"); this.root = root; }}
    


    모든 업데이트가 실행되면npm run dev 브라우저에 다음이 표시되어야 합니다.



    아직 특별한 일이 일어나지는 않지만 템플릿을 읽고 요소로 전달하고 구문 분석하고 contenteditable 특성을 true 로 설정합니다.

    이는 슬롯에 액세스하고 노드를 사용하여 렌더링에 사용되지 않는 데이터를 보유하는 방법입니다. 이렇게 하면 HTML 소스를 사용할 수 있는 형식으로 변환할 수 있습니다.

    도구 모음 #



    마지막 클래스} 앞의 클래스 맨 아래에 다음을 추가합니다.

    renderToolbar(command: (c: string, val: string | undefined) => void) { // TODO: Selection does not work on Safari iOS const selection = this.shadowRoot?.getSelection ? this.shadowRoot!.getSelection() : null; const tags: string[] = []; if (selection?.type === "Range") { // @ts-ignore let parentNode = selection?.baseNode; if (parentNode) { const checkNode = () => { const parentTagName = parentNode?.tagName?.toLowerCase()?.trim(); if (parentTagName) tags.push(parentTagName); }; while (parentNode != null) { checkNode(); parentNode = parentNode?.parentNode; } } } const commands: { icon: string; command: string | (() => void); active?: boolean; type?: string; values?: { value: string; name: string; font?: boolean }[]; command_value?: string; }[] = [{ icon: "format_clear", command: "removeFormat", }, { icon: "format_bold", command: "bold", active: tags.includes("b"), }, { icon: "format_italic", command: "italic", active: tags.includes("i"), }, { icon: "format_underlined", command: "underline", active: tags.includes("u"), }, { icon: "format_align_left", command: "justifyleft", }, { icon: "format_align_center", command: "justifycenter", }, { icon: "format_align_right", command: "justifyright", }, { icon: "format_list_numbered", command: "insertorderedlist", active: tags.includes("ol"), }, { icon: "format_list_bulleted", command: "insertunorderedlist", active: tags.includes("ul"), }, { icon: "format_quote", command: "formatblock", command_value: "blockquote", }, { icon: "format_indent_decrease", command: "outdent", }, { icon: "format_indent_increase", command: "indent", }, { icon: "add_link", command: () => { const newLink = prompt("Write the URL here", "http://"); if (newLink && newLink != "" && newLink != "http://") { command("createlink", newLink); } }, }, { icon: "link_off", command: "unlink" }, { icon: "format_color_text", command: () => { const input = this.shadowRoot!.querySelector( "#fg" )! as HTMLInputElement; input.addEventListener("input", (e: any) => { const val = e.target.value; command("forecolor", val); }); input.click(); }, type: "color", }, { icon: "border_color", command: () => { const input = this.shadowRoot!.querySelector( "#bg" )! as HTMLInputElement; input.addEventListener("input", (e: any) => { const val = e.target.value; command("backcolor", val); }); input.click(); }, type: "color", }, { icon: "title", command: "formatblock", values: [ { name: "Normal Text", value: "--" }, { name: "Heading 1", value: "h1" }, { name: "Heading 2", value: "h2" }, { name: "Heading 3", value: "h3" }, { name: "Heading 4", value: "h4" }, { name: "Heading 5", value: "h5" }, { name: "Heading 6", value: "h6" }, { name: "Paragraph", value: "p" }, { name: "Pre-Formatted", value: "pre" },], }, { icon: "text_format", command: "fontname", values: [{ name: "Font Name", value: "--" }, ...[...checkFonts()].map((f) => ({ name: f, value: f, font: true, })), ], }, { icon: "format_size", command: "fontsize", values: [{ name: "Font Size", value: "--" }, { name: "Very Small", value: "1" }, { name: "Small", value: "2" }, { name: "Normal", value: "3" }, { name: "Medium Large", value: "4" }, { name: "Large", value: "5" }, { name: "Very Large", value: "6" }, { name: "Maximum", value: "7" },], }, { icon: "undo", command: "undo", }, { icon: "redo", command: "redo", }, { icon: "content_cut", command: "cut", }, { icon: "content_copy", command: "copy", }, { icon: "content_paste", command: "paste", }, ]; return html` ${commands.map((n) => { return html` ${n.values ? html` <select id="${n.icon}" @change=${(e: any) => { const val = e.target.value; if (val === "--") { command("removeFormat", undefined); } else if (typeof n.command === "string") { command(n.command, val); } }} > ${n.values.map( (v) => html` <option value=${v.value}>${v.name}</option>` )} </select>` : html` <mwc-icon-button icon="${n.icon}" class="${n.active ? "active" : "inactive"}" @click=${() => { if (n.values) { } else if (typeof n.command === "string") { command(n.command, n.command_value); } else { n.command(); } }} ></mwc-icon-button>`} `; })} `;}
    


    전달된 값에 따라 mwc-icon-button 또는 select에 매핑할 수 있는 개체 배열을 사용합니다. 이것은 또한 이벤트 리스너를 설정하고 주어진 작업에 대한 명령을 실행합니다.
    <div id="toolbar"> 태그 안에 다음을 추가합니다.

    ${this.renderToolbar((command, val) => { document.execCommand(command, false, val); console.log("command", command, val);})}
    


    이것은 콜백을 수신하고 문서에서 명령을 실행하고 콘솔에 기록합니다.

    마지막으로 파일 맨 아래에 다음을 추가합니다.

    export function checkFonts(): string[] { const fontCheck = new Set( [// Windows 10 "Arial", "Arial Black", "Bahnschrift", "Calibri", "Cambria", "Cambria Math", "Candara", "Comic Sans MS", "Consolas", "Constantia", "Corbel", "Courier New", "Ebrima", "Franklin Gothic Medium", "Gabriola", "Gadugi", "Georgia", "HoloLens MDL2 Assets", "Impact", "Ink Free", "Javanese Text", "Leelawadee UI", "Lucida Console", "Lucida Sans Unicode", "Malgun Gothic", "Marlett", "Microsoft Himalaya", "Microsoft JhengHei", "Microsoft New Tai Lue", "Microsoft PhagsPa", "Microsoft Sans Serif", "Microsoft Tai Le", "Microsoft YaHei", "Microsoft Yi Baiti", "MingLiU-ExtB", "Mongolian Baiti", "MS Gothic", "MV Boli", "Myanmar Text", "Nirmala UI", "Palatino Linotype", "Segoe MDL2 Assets", "Segoe Print", "Segoe Script", "Segoe UI", "Segoe UI Historic", "Segoe UI Emoji", "Segoe UI Symbol", "SimSun", "Sitka", "Sylfaen", "Symbol", "Tahoma", "Times New Roman", "Trebuchet MS", "Verdana", "Webdings", "Wingdings", "Yu Gothic", // macOS "American Typewriter", "Andale Mono", "Arial", "Arial Black", "Arial Narrow", "Arial Rounded MT Bold", "Arial Unicode MS", "Avenir", "Avenir Next", "Avenir Next Condensed", "Baskerville", "Big Caslon", "Bodoni 72", "Bodoni 72 Oldstyle", "Bodoni 72 Smallcaps", "Bradley Hand", "Brush Script MT", "Chalkboard", "Chalkboard SE", "Chalkduster", "Charter", "Cochin", "Comic Sans MS", "Copperplate", "Courier", "Courier New", "Didot", "DIN Alternate", "DIN Condensed", "Futura", "Geneva", "Georgia", "Gill Sans", "Helvetica", "Helvetica Neue", "Herculanum", "Hoefler Text", "Impact", "Lucida Grande", "Luminari", "Marker Felt", "Menlo", "Microsoft Sans Serif", "Monaco", "Noteworthy", "Optima", "Palatino", "Papyrus", "Phosphate", "Rockwell", "Savoye LET", "SignPainter", "Skia", "Snell Roundhand", "Tahoma", "Times", "Times New Roman", "Trattatello", "Trebuchet MS", "Verdana", "Zapfino",].sort() ); const fontAvailable = new Set<string>(); // @ts-ignore for (const font of fontCheck.values()) { // @ts-ignore if (document.fonts.check(`12px "${font}"`)) { fontAvailable.add(font); } } // @ts-ignore return fontAvailable.values();}
    


    이 훌륭한 제안here에 따라 문서 검사는 브라우저 및 지정된 문서에 사용 가능한 모든 글꼴을 확인합니다.

    달리기 #



    명령npm run dev이 실행될 때 모든 것이 잘 진행되면 뷰포트에 다음이 표시되어야 합니다.



    결론 #



    Lit로 빌드하는 방법에 대해 자세히 알아보려면 문서here를 참조하세요. Lit 놀이터here에도 예제가 있습니다.

    이 예제의 소스는 here에서 찾을 수 있습니다.

    좋은 웹페이지 즐겨찾기