Editor.tsx 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. /** @jsx h */
  2. import { h, render } from "preact";
  3. import { useEffect, useState, useRef } from "preact/hooks";
  4. import showdown, { Converter } from "showdown";
  5. interface EditorProps {
  6. content: string;
  7. allowMode: "edit" | "read" | "both";
  8. }
  9. let shadow: ShadowRoot | null = null;
  10. let shadowRoot: HTMLDivElement | null = null;
  11. let converter: Converter | null = null;
  12. export default function Editor(props: EditorProps) {
  13. const [mode, setMode] = useState(props.allowMode);
  14. const [displayContent, setDisplayContent] = useState("");
  15. const [convertedContent, setConvertedContent] = useState("");
  16. // DOM to contain shadow root
  17. const shadowRootRef = useRef(null);
  18. // Render converted content to shadow root
  19. const renderContentToShadow = () => {
  20. if (shadowRootRef && shadowRootRef.current) {
  21. if (!shadow) {
  22. shadow = (shadowRootRef.current as HTMLDivElement).attachShadow({
  23. mode: "open",
  24. });
  25. }
  26. if (!shadowRoot) {
  27. shadowRoot = document.createElement("div");
  28. shadowRoot.id = "shadow-root";
  29. shadow?.appendChild(shadowRoot);
  30. }
  31. render(
  32. <div dangerouslySetInnerHTML={{ __html: convertedContent }} />,
  33. shadowRoot
  34. );
  35. }
  36. };
  37. // Event listener
  38. const modeChangeListener = (e: CustomEvent) => {
  39. if (
  40. e.detail &&
  41. (props.allowMode === e.detail || props.allowMode === "both")
  42. ) {
  43. setMode(e.detail);
  44. }
  45. };
  46. // Init event listeners
  47. useEffect(() => {
  48. addEventListener("ModeChange", modeChangeListener);
  49. return () => {
  50. removeEventListener("ModeChange", modeChangeListener);
  51. };
  52. }, []);
  53. // Re-render when converted content changes
  54. useEffect(() => {
  55. renderContentToShadow();
  56. }, [convertedContent, shadowRootRef]);
  57. // Init conversion
  58. useEffect(() => {
  59. if (props.content) {
  60. convertText(props.content);
  61. }
  62. }, [props.content]);
  63. const convertText = (text: string) => {
  64. // Init converter
  65. if (!converter) {
  66. converter = new showdown.Converter();
  67. }
  68. // Save display text
  69. setDisplayContent(text);
  70. // Convert text and save
  71. setConvertedContent(converter.makeHtml(text));
  72. };
  73. return (
  74. <div className={`pd-editor pd-mode-${mode}`}>
  75. {props.allowMode !== "read" ? (
  76. <div className="pd-edit-view">
  77. <textarea
  78. placeholder="Some Markdown here"
  79. onInput={(e) => {
  80. convertText((e.target as HTMLInputElement).value);
  81. }}
  82. value={displayContent}
  83. />
  84. </div>
  85. ) : null}
  86. {props.allowMode !== "edit" ? (
  87. <div className="pd-read-view" ref={shadowRootRef} />
  88. ) : null}
  89. </div>
  90. );
  91. }