jerryliao 1 год назад
Родитель
Сommit
e8122b83b5
3 измененных файлов с 94 добавлено и 37 удалено
  1. 51 25
      islands/Editor.tsx
  2. 16 7
      islands/TopBar.tsx
  3. 27 5
      static/global.css

+ 51 - 25
islands/Editor.tsx

@@ -1,6 +1,6 @@
 /** @jsx h */
-import { h } from "preact";
-import { useEffect, useState } from "preact/hooks";
+import { h, render } from "preact";
+import { useEffect, useState, useRef } from "preact/hooks";
 import showdown, { Converter } from "showdown";
 
 interface EditorProps {
@@ -8,28 +8,61 @@ interface EditorProps {
   allowMode: "edit" | "read" | "both";
 }
 
+let shadow: ShadowRoot | null = null;
+let shadowRoot: HTMLDivElement | null = null;
 let converter: Converter | null = null;
 export default function Editor(props: EditorProps) {
   const [mode, setMode] = useState(props.allowMode);
   const [displayContent, setDisplayContent] = useState("");
   const [convertedContent, setConvertedContent] = useState("");
 
+  // DOM to contain shadow root
+  const shadowRootRef = useRef(null);
+
+  // Render converted content to shadow root
+  const renderContentToShadow = () => {
+    if (shadowRootRef && shadowRootRef.current) {
+      if (!shadow) {
+        shadow = (shadowRootRef.current as HTMLDivElement).attachShadow({
+          mode: "open",
+        });
+      }
+      if (!shadowRoot) {
+        shadowRoot = document.createElement("div");
+        shadowRoot.id = "shadow-root";
+        shadow?.appendChild(shadowRoot);
+      }
+      render(
+        <div dangerouslySetInnerHTML={{ __html: convertedContent }} />,
+        shadowRoot
+      );
+    }
+  };
+
   // Event listener
   const modeChangeListener = (e: CustomEvent) => {
-    if (e.detail && props.allowMode === "both") {
+    if (
+      e.detail &&
+      (props.allowMode === e.detail || props.allowMode === "both")
+    ) {
       setMode(e.detail);
     }
   };
 
   // Init event listeners
   useEffect(() => {
-    window.addEventListener("ModeChange", modeChangeListener);
+    addEventListener("ModeChange", modeChangeListener);
 
     return () => {
-      window.removeEventListener("ModeChange", modeChangeListener);
+      removeEventListener("ModeChange", modeChangeListener);
     };
   }, []);
 
+  // Re-render when converted content changes
+  useEffect(() => {
+    renderContentToShadow();
+  }, [convertedContent, shadowRootRef]);
+
   // Init conversion
   useEffect(() => {
     if (props.content) {
@@ -52,27 +85,20 @@ export default function Editor(props: EditorProps) {
 
   return (
     <div className={`pd-editor pd-mode-${mode}`}>
-      {mode !== "read"
-        ? (
-          <div className="pd-edit-view">
-            <textarea
-              placeholder="Some Markdown here"
-              onInput={(e) => {
-                convertText((e.target as HTMLInputElement).value);
-              }}
-              value={displayContent}
-            />
-          </div>
-        )
-        : null}
-      {mode !== "edit"
-        ? (
-          <div
-            className="pd-read-view"
-            dangerouslySetInnerHTML={{ __html: convertedContent }}
+      {props.allowMode !== "read" ? (
+        <div className="pd-edit-view">
+          <textarea
+            placeholder="Some Markdown here"
+            onInput={(e) => {
+              convertText((e.target as HTMLInputElement).value);
+            }}
+            value={displayContent}
           />
-        )
-        : null}
+        </div>
+      ) : null}
+      {props.allowMode !== "edit" ? (
+        <div className="pd-read-view" ref={shadowRootRef} />
+      ) : null}
     </div>
   );
 }

+ 16 - 7
islands/TopBar.tsx

@@ -11,22 +11,25 @@ export default function TopBar(props: TopBarProps) {
 
   // Event listener
   const modeChangeListener = (e: CustomEvent) => {
-    if (e.detail && props.allowMode === "both") {
+    if (
+      e.detail &&
+      (props.allowMode === e.detail || props.allowMode === "both")
+    ) {
       setMode(e.detail);
     }
   };
 
   // Event dispatche
   const modeChangeDispatcher = (mode: string) => {
-    window.dispatchEvent(new CustomEvent("ModeChange", { detail: mode }));
+    dispatchEvent(new CustomEvent("ModeChange", { detail: mode }));
   };
 
   // Init event listeners
   useEffect(() => {
-    window.addEventListener("ModeChange", modeChangeListener);
+    addEventListener("ModeChange", modeChangeListener);
 
     return () => {
-      window.removeEventListener("ModeChange", modeChangeListener);
+      removeEventListener("ModeChange", modeChangeListener);
     };
   }, []);
 
@@ -34,7 +37,9 @@ export default function TopBar(props: TopBarProps) {
     <div className="pd-top-bar">
       <div className="pd-top-bar-mode-switcher">
         <button
-          className={`pd-top-bar-btn ${mode === "edit" ? "active" : ""}`}
+          className={`pd-top-bar-btn${mode === "edit" ? " active" : ""}${
+            props.allowMode === "read" ? " disabled" : ""
+          }`}
           id="edit"
           type="button"
           onClick={() => {
@@ -44,7 +49,9 @@ export default function TopBar(props: TopBarProps) {
           Edit
         </button>
         <button
-          className={`pd-top-bar-btn ${mode === "read" ? "active" : ""}`}
+          className={`pd-top-bar-btn${mode === "read" ? " active" : ""}${
+            props.allowMode === "edit" ? " disabled" : ""
+          }`}
           id="read"
           type="button"
           onClick={() => {
@@ -54,7 +61,9 @@ export default function TopBar(props: TopBarProps) {
           Read
         </button>
         <button
-          className={`pd-top-bar-btn ${mode === "both" ? "active" : ""}`}
+          className={`pd-top-bar-btn${mode === "both" ? " active" : ""}${
+            props.allowMode !== "both" ? " disabled" : ""
+          }`}
           id="both"
           type="button"
           onClick={() => {

+ 27 - 5
static/global.css

@@ -6,16 +6,18 @@
 .pd-page {
   width: 100vw;
   height: 100vh;
+  padding: 0.75rem;
   display: flex;
   flex-direction: column;
+  box-sizing: border-box;
   overflow: hidden;
 }
 
 /* TopBar styles start */
 .pd-top-bar {
   width: 100vw;
-  padding: 0.75rem 0.75rem 0 0.75rem;
   display: flex;
+  margin-bottom: 0.75rem;
   justify-content: space-between;
   box-sizing: border-box;
   flex-shrink: 0;
@@ -41,6 +43,13 @@
   color: #fff;
 }
 
+.pd-top-bar .pd-top-bar-mode-switcher .pd-top-bar-btn.disabled {
+  background-color: #e9ecef;
+  color: #212529;
+  cursor: not-allowed;
+  pointer-events: none;
+}
+
 .pd-top-bar .pd-top-bar-mode-switcher .pd-top-bar-btn:first-child {
   border-top-left-radius: 0.375rem;
   border-bottom-left-radius: 0.375rem;
@@ -60,8 +69,7 @@
 
 /* Editor styles start */
 .pd-editor {
-  width: 100vw;
-  padding: 0.75rem;
+  width: 100%;
   display: flex;
   justify-content: space-between;
   box-sizing: border-box;
@@ -72,11 +80,12 @@
 
 .pd-editor .pd-edit-view,
 .pd-editor .pd-read-view {
-  height: 100%;
+  height: calc(
+    100vh - 0.75rem * 3 - 28px
+  ); /* Exact height to prevent flex height expansion */
   border: 1px solid #ced4da;
   border-radius: 0.375rem;
   box-sizing: border-box;
-  padding: 0.75rem;
   color: #212529;
   overflow: auto;
   flex-shrink: 0;
@@ -89,12 +98,17 @@
   min-height: 100%;
   display: block;
   box-sizing: border-box;
+  padding: 0.375rem;
   border-radius: 0.375rem;
   border: none;
   resize: none;
   outline: none;
 }
 
+.pd-editor .pd-read-view {
+  padding: 0.375rem;
+}
+
 .pd-editor.pd-mode-both .pd-edit-view {
   margin-right: 0.375rem;
 }
@@ -102,4 +116,12 @@
 .pd-editor.pd-mode-both .pd-read-view {
   margin-left: 0.375rem;
 }
+
+.pd-editor.pd-mode-edit .pd-read-view {
+  display: none;
+}
+
+.pd-editor.pd-mode-read .pd-edit-view {
+  display: none;
+}
 /* Editor styles end */