【NextJS】Online HTML Editor with Tiptap

1. 概要

前回はAmplify(Gen1)を使い、AppSync APIにアクセスする内容内容についてでした。今回はオンラインHTMLエディターTiptapのシンプルな内容についてです。

対象としては開発を1年程やってて自分で最初から開発してみたい方になります。そのため細かい用語などの説明はしません。

2. nodeのインストール

こちらを参考

3. プロジェクトを作成

3-1-1. こちらを参考

4. 必要なライブラリをインストール

4-1-1. こちらを参考

npm i @tiptap/react @tiptap/starter-kit @tiptap/extension-image @tiptap/extension-task-item @tiptap/extension-task-list @tiptap/extension-text-align @tiptap/extension-typography @tiptap/extension-text-style @tiptap/extension-link @tiptap/extension-underline @tiptap/extension-highlight

5. ソースコード

※前回より差分のみを記載

5-1-1. src/app/components/component15/heading-select.tsx

import { useState, useRef, useEffect } from "react";
import scss from "./page.module.scss";
import MySvgIcon, { SvgIconNames } from "./my-svg-icon";

const headingOptions = [
  { label: "P", level: null },
  { label: "H1", level: 1 },
  { label: "H2", level: 2 },
  { label: "H3", level: 3 },
  { label: "H4", level: 4 },
  { label: "H5", level: 5 },
  { label: "H6", level: 6 },
];

const HeadingSelect = ({ editor }: { editor: any }) => {
  const [open, setOpen] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const handleClickOutside = (e: MouseEvent) => {
      if (ref.current && !ref.current.contains(e.target as Node)) {
        setOpen(false);
      }
    };
    document.addEventListener("mousedown", handleClickOutside);
    return () => document.removeEventListener("mousedown", handleClickOutside);
  }, []);

  const handleApply = (level: number | null) => {
    if (level === null) {
      editor.chain().focus().setParagraph().run();
    } else {
      editor.chain().focus().toggleHeading({ level }).run();
    }
    setOpen(false);
  };

  const getIsActive = (level: number | null) => {
    if (!editor) return false;
    return level === null
      ? editor.isActive("paragraph")
      : editor.isActive("heading", { level });
  };

  return (
    <div ref={ref} style={{ position: "relative" }}>
      <button
        onClick={() => setOpen((prev) => !prev)}
        className={getIsActive(null) ? scss.isActive : ""}
      >
        <MySvgIcon svgIconName={SvgIconNames.ParagraphLetter} />
      </button>

      {open && (
        <div
          style={{
            position: "absolute",
            top: "100%",
            left: 0,
            zIndex: 10,
            background: "#fff",
            border: "1px solid #ccc",
            borderRadius: "4px",
            padding: "4px",
            marginTop: "4px",
            display: "flex",
            flexDirection: "column",
            gap: "4px",
          }}
        >
          {headingOptions.map(({ label, level }) => (
            <button
              key={label}
              onClick={() => handleApply(level)}
              className={getIsActive(level) ? scss.isActive : ""}
              style={{ padding: "4px 8px", minWidth: "40px" }}
            >
              {label}
            </button>
          ))}
        </div>
      )}
    </div>
  );
};

export default HeadingSelect;

5-1-2. src/app/components/component15/highlight-select.tsx

import { useState, useRef, useEffect } from "react";
import MySvgIcon, { SvgIconNames } from "./my-svg-icon";
import scss from "./page.module.scss";

const highlightColors = [
  { name: "Yellow", color: "yellow" },
  { name: "Pink", color: "pink" },
  { name: "Green", color: "lightgreen" },
  { name: "Blue", color: "lightskyblue" },
  { name: "Purple", color: "violet" },
];

const HighlightSelect = ({
  editor,
  editorState,
}: {
  editor: any;
  editorState: any;
}) => {
  const [showPalette, setShowPalette] = useState(false);
  const containerRef = useRef<HTMLDivElement>(null);

  const activeColor = editor?.getAttributes("highlight").color ?? "";

  const applyColor = (color: string) => {
    setShowPalette(false);

    if (color) {
      editor?.chain().focus().setMark("highlight", { color }).run();
    } else {
      editor?.chain().focus().unsetHighlight().run();
    }
  };

  useEffect(() => {
    const handleClickOutside = (e: MouseEvent) => {
      if (
        containerRef.current &&
        !containerRef.current.contains(e.target as Node)
      ) {
        setShowPalette(false);
      }
    };
    document.addEventListener("mousedown", handleClickOutside);
    return () => document.removeEventListener("mousedown", handleClickOutside);
  }, []);

  return (
    <div ref={containerRef} style={{ position: "relative" }}>
      <button
        onClick={() => setShowPalette((prev) => !prev)}
        disabled={!editorState.canHighlight}
        className={editorState.isHighlight ? scss.isActive : ""}
      >
        <MySvgIcon svgIconName={SvgIconNames.Highlight} />
      </button>

      {showPalette && (
        <div
          style={{
            position: "absolute",
            top: "100%",
            left: 0,
            background: "#fff",
            border: "1px solid #ccc",
            borderRadius: "4px",
            padding: "6px",
            marginTop: "4px",
            display: "flex",
            gap: "8px",
            zIndex: 100,
          }}
        >
          {highlightColors.map(({ name, color }) => (
            <button
              key={color}
              onClick={() => applyColor(color)}
              style={{
                width: "24px",
                height: "24px",
                backgroundColor: color,
                border:
                  activeColor === color ? "2px solid #000" : "1px solid #ccc",
                borderRadius: "50%",
                cursor: "pointer",
              }}
              title={name}
            />
          ))}
          <button
            onClick={() => applyColor("")}
            style={{
              width: "24px",
              height: "24px",
              backgroundColor: "#fff",
              border: "1px dashed #999",
              borderRadius: "50%",
              cursor: "pointer",
            }}
            title="ハイライト解除"
          >
            ×
          </button>
        </div>
      )}
    </div>
  );
};

export default HighlightSelect;

5-1-3. src/app/components/component15/text-align-buttons.tsx

import MySvgIcon, { SvgIconNames } from "./my-svg-icon";
import scss from "./page.module.scss";

const alignOptions = [
  { value: "left", icon: SvgIconNames.AlignLeft, title: "Left" },
  { value: "center", icon: SvgIconNames.AlignCenter, title: "Center" },
  { value: "right", icon: SvgIconNames.AlignRight, title: "Right" },
  { value: "justify", icon: SvgIconNames.AlignJustify, title: "Justify" },
];

const TextAlignButtons = ({ editor }: { editor: any }) => {
  return (
    <div style={{ display: "flex", gap: "4px" }}>
      {alignOptions.map(({ value, icon, title }) => (
        <button
          key={value}
          onClick={() => editor.chain().focus().setTextAlign(value).run()}
          className={editor.isActive({ textAlign: value }) ? scss.isActive : ""}
          title={title}
        >
          <MySvgIcon svgIconName={icon} />
        </button>
      ))}
    </div>
  );
};

export default TextAlignButtons;

5-1-4. src/app/components/component15/my-svg-icon.tsx

import SvgIcon from "@mui/material/SvgIcon";
import { ReactNode } from "react";

const MySvgIcon = ({ svgIconName }: { svgIconName: string }) => {
  return <SvgIcon>{svgIconMap.get(svgIconName)}</SvgIcon>;
};

const iconNames = [
  "Bold",
  "Italic",
  "HorizontalRule",
  "StrikeThrough",
  "Code",
  "ListBullet",
  "ListNumber",
  "CodeBlock",
  "BlockQuote",
  "Undo",
  "Redo",
  "Link",
  "Underline",
  "Highlight",
  "ParagraphLetter",
  "AlignLeft",
  "AlignCenter",
  "AlignRight",
  "AlignJustify",
  "HardBreak",
  "Reset",
] as const;

export const SvgIconNames = Object.fromEntries(
  iconNames.map((name: string) => [name, name])
) as Record<(typeof iconNames)[number], string>;

type MySvgIconType = {
  name: string;
  svg: ReactNode;
};

const svgIconMapList: MySvgIconType[] = [
  {
    name: SvgIconNames.Bold,
    svg: (
      <svg
        fill="currentColor"
        viewBox="0 0 56 56"
        xmlns="http://www.w3.org/2000/svg"
      >
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <path d="M 18.0273 44.7578 L 30.2617 44.7578 C 37.4102 44.7578 41.9336 40.9844 41.9336 35.1953 C 41.9336 30.6250 38.6524 27.3203 33.8711 27.0625 L 33.8711 26.8750 C 37.7617 26.3359 40.4102 23.4063 40.4102 19.6797 C 40.4102 14.4531 36.4258 11.2422 29.9102 11.2422 L 18.0273 11.2422 C 15.5195 11.2422 14.0664 12.7188 14.0664 15.2968 L 14.0664 40.7266 C 14.0664 43.2812 15.5195 44.7578 18.0273 44.7578 Z M 21.8008 25.1641 L 21.8008 16.7500 L 27.5664 16.7500 C 30.8476 16.7500 32.7929 18.2500 32.7929 20.8281 C 32.7929 23.5469 30.6367 25.1641 26.9805 25.1641 Z M 21.8008 39.3437 L 21.8008 30.0390 L 27.6836 30.0390 C 31.8086 30.0390 34.0820 31.6328 34.0820 34.6328 C 34.0820 37.7031 31.8789 39.3437 27.8476 39.3437 Z"></path>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.Italic,
    svg: (
      <svg
        viewBox="0 0 24 24"
        fill="currentColor"
        xmlns="http://www.w3.org/2000/svg"
      >
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <path
            d="M18.5 5H14.5M10.5 5L14.5 5M10 20L14.5 5"
            stroke="currentColor"
            strokeWidth="2"
            strokeLinecap="round"
            strokeLinejoin="round"
          ></path>
          <path
            d="M14 20H6"
            stroke="currentColor"
            strokeWidth="2"
            strokeLinecap="round"
            strokeLinejoin="round"
          ></path>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.HorizontalRule,
    svg: (
      <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <path
            d="M4 12L20 12"
            stroke="#000000"
            strokeWidth="2"
            strokeLinecap="round"
            strokeLinejoin="round"
          ></path>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.StrikeThrough,
    svg: (
      <svg
        viewBox="0 0 24 24"
        version="1.1"
        xmlns="http://www.w3.org/2000/svg"
        fill="#000000"
      >
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <title>strikethrough_line</title>
          <g
            id="页面-1"
            stroke="none"
            strokeWidth="1"
            fill="none"
            fillRule="evenodd"
          >
            <g
              id="Editor"
              transform="translate(-1008.000000, 0.000000)"
              fillRule="nonzero"
            >
              <g
                id="strikethrough_line"
                transform="translate(1008.000000, 0.000000)"
              >
                <path
                  d="M24,0 L24,24 L0,24 L0,0 L24,0 Z M12.5934901,23.257841 L12.5819402,23.2595131 L12.5108777,23.2950439 L12.4918791,23.2987469 L12.4918791,23.2987469 L12.4767152,23.2950439 L12.4056548,23.2595131 C12.3958229,23.2563662 12.3870493,23.2590235 12.3821421,23.2649074 L12.3780323,23.275831 L12.360941,23.7031097 L12.3658947,23.7234994 L12.3769048,23.7357139 L12.4804777,23.8096931 L12.4953491,23.8136134 L12.4953491,23.8136134 L12.5071152,23.8096931 L12.6106902,23.7357139 L12.6232938,23.7196733 L12.6232938,23.7196733 L12.6266527,23.7031097 L12.609561,23.275831 C12.6075724,23.2657013 12.6010112,23.2592993 12.5934901,23.257841 L12.5934901,23.257841 Z M12.8583906,23.1452862 L12.8445485,23.1473072 L12.6598443,23.2396597 L12.6498822,23.2499052 L12.6498822,23.2499052 L12.6471943,23.2611114 L12.6650943,23.6906389 L12.6699349,23.7034178 L12.6699349,23.7034178 L12.678386,23.7104931 L12.8793402,23.8032389 C12.8914285,23.8068999 12.9022333,23.8029875 12.9078286,23.7952264 L12.9118235,23.7811639 L12.8776777,23.1665331 C12.8752882,23.1545897 12.8674102,23.1470016 12.8583906,23.1452862 L12.8583906,23.1452862 Z M12.1430473,23.1473072 C12.1332178,23.1423925 12.1221763,23.1452606 12.1156365,23.1525954 L12.1099173,23.1665331 L12.0757714,23.7811639 C12.0751323,23.7926639 12.0828099,23.8018602 12.0926481,23.8045676 L12.108256,23.8032389 L12.3092106,23.7104931 L12.3186497,23.7024347 L12.3186497,23.7024347 L12.3225043,23.6906389 L12.340401,23.2611114 L12.337245,23.2485176 L12.337245,23.2485176 L12.3277531,23.2396597 L12.1430473,23.1473072 Z"
                  id="MingCute"
                  fillRule="nonzero"
                ></path>
                <path
                  d="M19,12 C19.5523,12 20,12.4477 20,13 C20,13.51285 19.613973,13.9355092 19.1166239,13.9932725 L19,14 L16.8907,14 C17.1073,14.3274 17.2822,14.6843 17.4085,15.0632 C18.3612872,17.9217574 16.2991816,20.8716985 13.3247561,20.9959339 L13.1295,21 L11.4276,21 C9.33508148,21 7.41547616,19.8615567 6.40836368,18.0415942 L6.29625,17.8286 L6.11406,17.4642 C6.07774,17.3951 6.04927,17.3211 6.02987,17.2436 C5.99808,17.1175 5.99194,16.9887 6.00918,16.8639 C6.04771,16.5852 6.20307,16.3268 6.44999,16.1647 C6.54553,16.1017 6.65252,16.0546 6.76716,16.0272 C6.88516,15.9989 7.00534,15.9929 7.12218,16.0074 C7.41451,16.043 7.6679,16.2048 7.82628,16.4366 L7.86803875,16.5031625 L7.86803875,16.5031625 L8.08511,16.9342 C8.6864885,18.136995 9.88435633,18.918864 11.216254,18.9940439 L11.4276,19 L13.1295,19 C14.843,19 16.053,17.3213 15.5111,15.6957 C15.32165,15.12717 14.935856,14.647416 14.4264722,14.3400543 L14.2522,14.2441 L13.7639,14 L5,14 C4.44772,14 4,13.5523 4,13 C4,12.48715 4.38604429,12.0644908 4.88337975,12.0067275 L5,12 L19,12 Z M12.5723,3 C14.7453,3 16.7319,4.22775 17.7037,6.17137 L17.8849,6.53388 C17.926,6.61173 17.9572,6.69566 17.9766,6.78396 C18.0345,7.04488 17.984,7.31519 17.8448,7.53528 C17.7729,7.64908 17.6772,7.74945 17.5607,7.82816 C17.4608,7.89591 17.348,7.94607 17.2268,7.97418 C16.9685,8.03452 16.7003,7.98813 16.48,7.85433 C16.3558,7.77888 16.2468,7.67563 16.1634,7.54805 L16.1279125,7.48969875 L16.1279125,7.48969875 L15.9148,7.0658 C15.2818,5.79974 13.9878,5 12.5723,5 L10.8705,5 C9.15693,5 7.94698,6.67873 8.48884,8.30432 C8.69939,8.93596 9.15223,9.45809 9.74775,9.75585 L12.236,11 L8.01262,11 C7.35876,10.4645 6.86299,9.75131 6.59147,8.93677 C5.61792,6.01612 7.79182,3 10.8705,3 L12.5723,3 Z"
                  id="形状"
                  fill="currentColor"
                ></path>
              </g>
            </g>
          </g>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.Code,
    svg: (
      <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <path
            d="M14.1809 4.2755C14.581 4.3827 14.8185 4.79396 14.7113 5.19406L10.7377 20.0238C10.6304 20.4239 10.2192 20.6613 9.81909 20.5541C9.41899 20.4469 9.18156 20.0356 9.28876 19.6355L13.2624 4.80583C13.3696 4.40573 13.7808 4.16829 14.1809 4.2755Z"
            fill="currentColor"
          ></path>
          <path
            d="M16.4425 7.32781C16.7196 7.01993 17.1938 6.99497 17.5017 7.27206L19.2392 8.8358C19.9756 9.49847 20.5864 10.0482 21.0058 10.5467C21.4468 11.071 21.7603 11.6342 21.7603 12.3295C21.7603 13.0248 21.4468 13.5881 21.0058 14.1123C20.5864 14.6109 19.9756 15.1606 19.2392 15.8233L17.5017 17.387C17.1938 17.6641 16.7196 17.6391 16.4425 17.3313C16.1654 17.0234 16.1904 16.5492 16.4983 16.2721L18.1947 14.7452C18.9826 14.0362 19.5138 13.5558 19.8579 13.1467C20.1882 12.7541 20.2603 12.525 20.2603 12.3295C20.2603 12.1341 20.1882 11.9049 19.8579 11.5123C19.5138 11.1033 18.9826 10.6229 18.1947 9.91383L16.4983 8.387C16.1904 8.10991 16.1654 7.63569 16.4425 7.32781Z"
            fill="currentColor"
          ></path>
          <path
            d="M7.50178 8.387C7.80966 8.10991 7.83462 7.63569 7.55752 7.32781C7.28043 7.01993 6.80621 6.99497 6.49833 7.27206L4.76084 8.8358C4.0245 9.49847 3.41369 10.0482 2.99428 10.5467C2.55325 11.071 2.23975 11.6342 2.23975 12.3295C2.23975 13.0248 2.55325 13.5881 2.99428 14.1123C3.41369 14.6109 4.02449 15.1606 4.76082 15.8232L6.49833 17.387C6.80621 17.6641 7.28043 17.6391 7.55752 17.3313C7.83462 17.0234 7.80966 16.5492 7.50178 16.2721L5.80531 14.7452C5.01743 14.0362 4.48623 13.5558 4.14213 13.1467C3.81188 12.7541 3.73975 12.525 3.73975 12.3295C3.73975 12.1341 3.81188 11.9049 4.14213 11.5123C4.48623 11.1033 5.01743 10.6229 5.80531 9.91383L7.50178 8.387Z"
            fill="currentColor"
          ></path>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.ListBullet,
    svg: (
      <svg
        viewBox="0 -4 28 28"
        version="1.1"
        xmlns="http://www.w3.org/2000/svg"
        fill="currentColor"
      >
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <title>bullet-list</title> <desc>Created with Sketch Beta.</desc>
          <defs> </defs>
          <g
            id="Page-1"
            stroke="none"
            strokeWidth="1"
            fill="none"
            fillRule="evenodd"
          >
            <g
              id="Icon-Set"
              transform="translate(-570.000000, -209.000000)"
              fill="currentColor"
            >
              <path
                d="M597,226 L579,226 C578.447,226 578,226.448 578,227 C578,227.553 578.447,228 579,228 L597,228 C597.553,228 598,227.553 598,227 C598,226.448 597.553,226 597,226 L597,226 Z M572,209 C570.896,209 570,209.896 570,211 C570,212.104 570.896,213 572,213 C573.104,213 574,212.104 574,211 C574,209.896 573.104,209 572,209 L572,209 Z M579,212 L597,212 C597.553,212 598,211.553 598,211 C598,210.447 597.553,210 597,210 L579,210 C578.447,210 578,210.447 578,211 C578,211.553 578.447,212 579,212 L579,212 Z M597,218 L579,218 C578.447,218 578,218.448 578,219 C578,219.553 578.447,220 579,220 L597,220 C597.553,220 598,219.553 598,219 C598,218.448 597.553,218 597,218 L597,218 Z M572,217 C570.896,217 570,217.896 570,219 C570,220.104 570.896,221 572,221 C573.104,221 574,220.104 574,219 C574,217.896 573.104,217 572,217 L572,217 Z M572,225 C570.896,225 570,225.896 570,227 C570,228.104 570.896,229 572,229 C573.104,229 574,228.104 574,227 C574,225.896 573.104,225 572,225 L572,225 Z"
                id="bullet-list"
              ></path>
            </g>
          </g>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.ListNumber,
    svg: (
      <svg
        fill="currentColor"
        viewBox="0 0 1024 1024"
        xmlns="http://www.w3.org/2000/svg"
      >
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <path d="M920 760H336c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h584c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0-568H336c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h584c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 284H336c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h584c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM216 712H100c-2.2 0-4 1.8-4 4v34c0 2.2 1.8 4 4 4h72.4v20.5h-35.7c-2.2 0-4 1.8-4 4v34c0 2.2 1.8 4 4 4h35.7V838H100c-2.2 0-4 1.8-4 4v34c0 2.2 1.8 4 4 4h116c2.2 0 4-1.8 4-4V716c0-2.2-1.8-4-4-4zM100 188h38v120c0 2.2 1.8 4 4 4h40c2.2 0 4-1.8 4-4V152c0-4.4-3.6-8-8-8h-78c-2.2 0-4 1.8-4 4v36c0 2.2 1.8 4 4 4zm116 240H100c-2.2 0-4 1.8-4 4v36c0 2.2 1.8 4 4 4h68.4l-70.3 77.7a8.3 8.3 0 0 0-2.1 5.4V592c0 2.2 1.8 4 4 4h116c2.2 0 4-1.8 4-4v-36c0-2.2-1.8-4-4-4h-68.4l70.3-77.7a8.3 8.3 0 0 0 2.1-5.4V432c0-2.2-1.8-4-4-4z"></path>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.CodeBlock,
    svg: (
      <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <path
            fillRule="evenodd"
            clipRule="evenodd"
            d="M12.25 2.83422C11.7896 2.75598 11.162 2.75005 10.0298 2.75005C8.11311 2.75005 6.75075 2.75163 5.71785 2.88987C4.70596 3.0253 4.12453 3.27933 3.7019 3.70195C3.27869 4.12516 3.02502 4.70481 2.88976 5.7109C2.75159 6.73856 2.75 8.09323 2.75 10.0001V14.0001C2.75 15.9069 2.75159 17.2615 2.88976 18.2892C3.02502 19.2953 3.27869 19.8749 3.7019 20.2981C4.12511 20.7214 4.70476 20.975 5.71085 21.1103C6.73851 21.2485 8.09318 21.2501 10 21.2501H14C15.9068 21.2501 17.2615 21.2485 18.2892 21.1103C19.2952 20.975 19.8749 20.7214 20.2981 20.2981C20.7213 19.8749 20.975 19.2953 21.1102 18.2892C21.2484 17.2615 21.25 15.9069 21.25 14.0001V13.5629C21.25 12.0269 21.2392 11.2988 21.0762 10.7501H17.9463C16.8135 10.7501 15.8877 10.7501 15.1569 10.6518C14.3929 10.5491 13.7306 10.3268 13.2019 9.79815C12.6732 9.26945 12.4509 8.60712 12.3482 7.84317C12.25 7.1123 12.25 6.18657 12.25 5.05374V2.83422ZM13.75 3.6095V5.00005C13.75 6.19976 13.7516 7.0241 13.8348 7.64329C13.9152 8.24091 14.059 8.53395 14.2626 8.73749C14.4661 8.94103 14.7591 9.08486 15.3568 9.16521C15.976 9.24846 16.8003 9.25005 18 9.25005H20.0195C19.723 8.9625 19.3432 8.61797 18.85 8.17407L14.8912 4.61117C14.4058 4.17433 14.0446 3.85187 13.75 3.6095ZM10.1755 1.25002C11.5601 1.24965 12.4546 1.24942 13.2779 1.56535C14.1012 1.88129 14.7632 2.47735 15.7873 3.39955C15.8226 3.43139 15.8584 3.46361 15.8947 3.49623L19.8534 7.05912C19.8956 7.09705 19.9372 7.1345 19.9783 7.17149C21.162 8.23614 21.9274 8.92458 22.3391 9.84902C22.7508 10.7734 22.7505 11.8029 22.75 13.3949C22.75 13.4502 22.75 13.5062 22.75 13.5629V14.0565C22.75 15.8942 22.75 17.3499 22.5969 18.4891C22.4392 19.6615 22.1071 20.6104 21.3588 21.3588C20.6104 22.1072 19.6614 22.4393 18.489 22.5969C17.3498 22.7501 15.8942 22.7501 14.0564 22.7501H9.94359C8.10583 22.7501 6.65019 22.7501 5.51098 22.5969C4.33856 22.4393 3.38961 22.1072 2.64124 21.3588C1.89288 20.6104 1.56076 19.6615 1.40314 18.4891C1.24997 17.3499 1.24998 15.8942 1.25 14.0565V9.94363C1.24998 8.10587 1.24997 6.65024 1.40314 5.51103C1.56076 4.33861 1.89288 3.38966 2.64124 2.64129C3.39019 1.89235 4.34232 1.56059 5.51887 1.40313C6.66283 1.25002 8.1257 1.25003 9.97352 1.25005L10.0298 1.25005C10.0789 1.25005 10.1275 1.25004 10.1755 1.25002Z"
            fill="currentColor"
          ></path>
          <path
            fillRule="evenodd"
            clipRule="evenodd"
            d="M10.2633 13.2978C10.6512 13.4432 10.8477 13.8756 10.7022 14.2634L9.20225 18.2634C9.05681 18.6512 8.6245 18.8477 8.23666 18.7023C7.84882 18.5569 7.65231 18.1245 7.79775 17.7367L9.29775 13.7367C9.44319 13.3489 9.8755 13.1524 10.2633 13.2978ZM7.53033 13.4697C7.82322 13.7626 7.82322 14.2375 7.53033 14.5304L7.06066 15L7.53033 15.4697C7.82322 15.7626 7.82322 16.2375 7.53033 16.5304C7.23744 16.8233 6.76256 16.8233 6.46967 16.5304L5.46967 15.5304C5.17678 15.2375 5.17678 14.7626 5.46967 14.4697L6.46967 13.4697C6.76256 13.1768 7.23744 13.1768 7.53033 13.4697ZM10.9697 15.4697C11.2626 15.1768 11.7374 15.1768 12.0303 15.4697L13.0303 16.4697C13.3232 16.7626 13.3232 17.2375 13.0303 17.5304L12.0303 18.5304C11.7374 18.8233 11.2626 18.8233 10.9697 18.5304C10.6768 18.2375 10.6768 17.7626 10.9697 17.4697L11.4393 17L10.9697 16.5304C10.6768 16.2375 10.6768 15.7626 10.9697 15.4697Z"
            fill="currentColor"
          ></path>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.BlockQuote,
    svg: (
      <svg
        fill="currentColor"
        viewBox="0 0 24 24"
        xmlns="http://www.w3.org/2000/svg"
      >
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <path d="M.78,8.89c0-3.07,1.53-4.3,4.3-4.34L5.38,6C3.78,6.17,3,7,3.1,8.31H4.54V12H.78Zm5.9,0c0-3.07,1.53-4.3,4.3-4.34L11.28,6C9.68,6.17,8.89,7,9,8.31h1.44V12H6.68Z"></path>
          <path d="M16.94,15.11c0,3.07-1.53,4.3-4.3,4.34L12.35,18c1.6-.16,2.39-1,2.28-2.3H13.18V12h3.76Zm5.9,0c0,3.07-1.53,4.3-4.3,4.34L18.24,18c1.6-.16,2.39-1,2.28-2.3H19.08V12h3.76Z"></path>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.Undo,
    svg: (
      <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <path
            d="M4 7H15C16.8692 7 17.8039 7 18.5 7.40193C18.9561 7.66523 19.3348 8.04394 19.5981 8.49999C20 9.19615 20 10.1308 20 12C20 13.8692 20 14.8038 19.5981 15.5C19.3348 15.9561 18.9561 16.3348 18.5 16.5981C17.8039 17 16.8692 17 15 17H8.00001M4 7L7 4M4 7L7 10"
            stroke="#1C274C"
            strokeWidth="1.5"
            strokeLinecap="round"
            strokeLinejoin="round"
          ></path>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.Redo,
    svg: (
      <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <path
            d="M20 7H9.00001C7.13077 7 6.19615 7 5.5 7.40193C5.04395 7.66523 4.66524 8.04394 4.40193 8.49999C4 9.19615 4 10.1308 4 12C4 13.8692 4 14.8038 4.40192 15.5C4.66523 15.9561 5.04394 16.3348 5.5 16.5981C6.19615 17 7.13077 17 9 17H16M20 7L17 4M20 7L17 10"
            stroke="#1C274C"
            strokeWidth="1.5"
            strokeLinecap="round"
            strokeLinejoin="round"
          ></path>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.Link,
    svg: (
      <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <path
            d="M14.1625 18.4876L13.4417 19.2084C11.053 21.5971 7.18019 21.5971 4.79151 19.2084C2.40283 16.8198 2.40283 12.9469 4.79151 10.5583L5.51236 9.8374"
            stroke="currentColor"
            strokeWidth="1.5"
            strokeLinecap="round"
          ></path>
          <path
            d="M9.8374 14.1625L14.1625 9.8374"
            stroke="currentColor"
            strokeWidth="1.5"
            strokeLinecap="round"
          ></path>
          <path
            d="M9.8374 5.51236L10.5583 4.79151C12.9469 2.40283 16.8198 2.40283 19.2084 4.79151M18.4876 14.1625L19.2084 13.4417C20.4324 12.2177 21.0292 10.604 20.9988 9"
            stroke="currentColor"
            strokeWidth="1.5"
            strokeLinecap="round"
          ></path>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.Underline,
    svg: (
      <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <path
            d="M18 4V11C18 14.3137 15.3137 17 12 17C8.68629 17 6 14.3137 6 11V4M4 21H20"
            stroke="currentColor"
            strokeWidth="2"
            strokeLinecap="round"
            strokeLinejoin="round"
          ></path>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.Highlight,
    svg: (
      <svg
        fill="currentColor"
        viewBox="0 0 1024 1024"
        xmlns="http://www.w3.org/2000/svg"
      >
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <path d="M957.6 507.4L603.2 158.2a7.9 7.9 0 0 0-11.2 0L353.3 393.4a8.03 8.03 0 0 0-.1 11.3l.1.1 40 39.4-117.2 115.3a8.03 8.03 0 0 0-.1 11.3l.1.1 39.5 38.9-189.1 187H72.1c-4.4 0-8.1 3.6-8.1 8V860c0 4.4 3.6 8 8 8h344.9c2.1 0 4.1-.8 5.6-2.3l76.1-75.6 40.4 39.8a7.9 7.9 0 0 0 11.2 0l117.1-115.6 40.1 39.5a7.9 7.9 0 0 0 11.2 0l238.7-235.2c3.4-3 3.4-8 .3-11.2zM389.8 796.2H229.6l134.4-133 80.1 78.9-54.3 54.1zm154.8-62.1L373.2 565.2l68.6-67.6 171.4 168.9-68.6 67.6zM713.1 658L450.3 399.1 597.6 254l262.8 259-147.3 145z"></path>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.ParagraphLetter,
    svg: (
      <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <path
            d="M18 5H8.5C6.567 5 5 6.567 5 8.5C5 10.433 6.567 12 8.5 12H10M11 5V19M15 5V19"
            stroke="currentColor"
            strokeWidth="2"
            strokeLinecap="round"
            strokeLinejoin="round"
          ></path>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.AlignLeft,
    svg: (
      <svg
        viewBox="0 0 24 24"
        xmlns="http://www.w3.org/2000/svg"
        fill="#000000"
      >
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <title></title>
          <g id="Complete">
            <g id="align-left">
              <g>
                <polygon
                  fill="#ffffff"
                  points="12.9 18 4.1 18 4.1 18 12.9 18 12.9 18"
                  stroke="currentColor"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth="2"
                ></polygon>
                <polygon
                  fill="#ffffff"
                  points="20 14 4 14 4 14 20 14 20 14"
                  stroke="currentColor"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth="2"
                ></polygon>
                <polygon
                  fill="#ffffff"
                  points="12.9 10 4.1 10 4.1 10 12.9 10 12.9 10"
                  stroke="currentColor"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth="2"
                ></polygon>
                <polygon
                  fill="#ffffff"
                  points="20 6 4 6 4 6 20 6 20 6"
                  stroke="currentColor"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth="2"
                ></polygon>
              </g>
            </g>
          </g>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.AlignCenter,
    svg: (
      <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <path
            d="M5 6H19M7 10H17M5 14H19M7 18H17"
            stroke="currentColor"
            strokeWidth="2"
            strokeLinecap="round"
            strokeLinejoin="round"
          ></path>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.AlignRight,
    svg: (
      <svg
        viewBox="0 0 24 24"
        xmlns="http://www.w3.org/2000/svg"
        fill="#000000"
      >
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <title></title>
          <g id="Complete">
            <g id="align-right">
              <g>
                <polygon
                  fill="#ffffff"
                  points="19.9 18 11.1 18 11.1 18 19.9 18 19.9 18"
                  stroke="currentColor"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth="2"
                ></polygon>
                <polygon
                  fill="#ffffff"
                  points="20 14 4 14 4 14 20 14 20 14"
                  stroke="currentColor"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth="2"
                ></polygon>
                <polygon
                  fill="#ffffff"
                  points="19.9 10 11.1 10 11.1 10 19.9 10 19.9 10"
                  stroke="currentColor"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth="2"
                ></polygon>
                <polygon
                  fill="#ffffff"
                  points="20 6 4 6 4 6 20 6 20 6"
                  stroke="currentColor"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth="2"
                ></polygon>
              </g>
            </g>
          </g>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.AlignJustify,
    svg: (
      <svg
        viewBox="0 0 24 24"
        xmlns="http://www.w3.org/2000/svg"
        fill="#000000"
      >
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <title></title>
          <g id="Complete">
            <g id="align-justify">
              <g>
                <polygon
                  fill="#ffffff"
                  points="20 18 4 18 4 18 20 18 20 18"
                  stroke="currentColor"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth="2"
                ></polygon>
                <polygon
                  fill="#ffffff"
                  points="20 14 4 14 4 14 20 14 20 14"
                  stroke="currentColor"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth="2"
                ></polygon>
                <polygon
                  fill="#ffffff"
                  points="20 10 4 10 4 10 20 10 20 10"
                  stroke="currentColor"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth="2"
                ></polygon>
                <polygon
                  fill="#ffffff"
                  points="20 6 4 6 4 6 20 6 20 6"
                  stroke="currentColor"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth="2"
                ></polygon>
              </g>
            </g>
          </g>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.HardBreak,
    svg: (
      <svg
        viewBox="0 0 18 18"
        xmlns="http://www.w3.org/2000/svg"
        fill="#000000"
      >
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <path
            fill="#494c4e"
            d="M16 4H2c-.6 0-1-.4-1-1s.4-1 1-1h14c.6 0 1 .4 1 1s-.4 1-1 1zm0 4H2c-.6 0-1-.4-1-1s.4-1 1-1h14c.6 0 1 .4 1 1s-.4 1-1 1zm1 4v2c0 1-1 2-2 2h-2c0 .3 0 .5-.3.7-.2.2-.4.3-.7.3s-.5 0-.7-.3l-1-1c-.4-.4-.4-1 0-1.4l1-1c.4-.4 1-.4 1.4 0 .2.2.3.4.3.7h2v-2H2c-.6 0-1-.5-1-1s.5-1 1-1h13c1 0 2 1 2 2z"
          ></path>
        </g>
      </svg>
    ),
  },
  {
    name: SvgIconNames.Reset,
    svg: (
      <svg viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
        <g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
        <g
          id="SVGRepo_tracerCarrier"
          strokeLinecap="round"
          strokeLinejoin="round"
        ></g>
        <g id="SVGRepo_iconCarrier">
          <path
            d="M4.56189 13.5L4.14285 13.9294L4.5724 14.3486L4.99144 13.9189L4.56189 13.5ZM9.92427 15.9243L15.9243 9.92427L15.0757 9.07574L9.07574 15.0757L9.92427 15.9243ZM9.07574 9.92426L15.0757 15.9243L15.9243 15.0757L9.92426 9.07574L9.07574 9.92426ZM19.9 12.5C19.9 16.5869 16.5869 19.9 12.5 19.9V21.1C17.2496 21.1 21.1 17.2496 21.1 12.5H19.9ZM5.1 12.5C5.1 8.41309 8.41309 5.1 12.5 5.1V3.9C7.75035 3.9 3.9 7.75035 3.9 12.5H5.1ZM12.5 5.1C16.5869 5.1 19.9 8.41309 19.9 12.5H21.1C21.1 7.75035 17.2496 3.9 12.5 3.9V5.1ZM5.15728 13.4258C5.1195 13.1227 5.1 12.8138 5.1 12.5H3.9C3.9 12.8635 3.92259 13.2221 3.9665 13.5742L5.15728 13.4258ZM12.5 19.9C9.9571 19.9 7.71347 18.6179 6.38048 16.6621L5.38888 17.3379C6.93584 19.6076 9.54355 21.1 12.5 21.1V19.9ZM4.99144 13.9189L7.42955 11.4189L6.57045 10.5811L4.13235 13.0811L4.99144 13.9189ZM4.98094 13.0706L2.41905 10.5706L1.58095 11.4294L4.14285 13.9294L4.98094 13.0706Z"
            fill="#121923"
          ></path>
        </g>
      </svg>
    ),
  },
  { name: "", svg: <></> },
];

const svgIconMap = new Map<string, ReactNode>();
svgIconMapList.forEach((svgIcon: MySvgIconType) =>
  svgIconMap.set(svgIcon.name, svgIcon.svg)
);

export default MySvgIcon;

5-1-5. src/app/components/component15/menubar.tsx

import React, { useCallback } from "react";
import type { Editor } from "@tiptap/react";
import { useEditorState } from "@tiptap/react";
import MySvgIcon, { SvgIconNames } from "./my-svg-icon";
import scss from "./page.module.scss";
import HighlightSelect from "./highlight-select";
import HeadingSelect from "./heading-select";
import TextAlignButtons from "./text-align-buttons";

const MenuBar = ({ editor }: { editor: Editor }) => {
  // Read the current editor's state, and re-render the component when it changes
  const editorState = useEditorState({
    editor,
    selector: (ctx) => {
      return {
        isBold: ctx.editor.isActive("bold"),
        canBold: ctx.editor.can().chain().focus().toggleBold().run(),
        isItalic: ctx.editor.isActive("italic"),
        canItalic: ctx.editor.can().chain().focus().toggleItalic().run(),
        isUnderline: ctx.editor.isActive("underline"),
        canUnderline: ctx.editor.can().chain().focus().toggleUnderline().run(),
        isHighlight: ctx.editor.isActive("highlight"),
        canHighlight: ctx.editor.can().chain().focus().toggleHighlight().run(),
        isStrike: ctx.editor.isActive("strike"),
        canStrike: ctx.editor.can().chain().focus().toggleStrike().run(),
        isCode: ctx.editor.isActive("code"),
        canCode: ctx.editor.can().chain().focus().toggleCode().run(),
        canClearMarks: ctx.editor.can().chain().focus().unsetAllMarks().run(),
        isParagraph: ctx.editor.isActive("paragraph"),
        isHeading1: ctx.editor.isActive("heading", { level: 1 }),
        isHeading2: ctx.editor.isActive("heading", { level: 2 }),
        isHeading3: ctx.editor.isActive("heading", { level: 3 }),
        isHeading4: ctx.editor.isActive("heading", { level: 4 }),
        isHeading5: ctx.editor.isActive("heading", { level: 5 }),
        isHeading6: ctx.editor.isActive("heading", { level: 6 }),
        isBulletList: ctx.editor.isActive("bulletList"),
        isOrderedList: ctx.editor.isActive("orderedList"),
        isCodeBlock: ctx.editor.isActive("codeBlock"),
        isBlockquote: ctx.editor.isActive("blockquote"),
        canInsertHorizontalRule: ctx.editor
          .can()
          .chain()
          .focus()
          .setHorizontalRule()
          .run(),
        canUndo: ctx.editor.can().chain().focus().undo().run(),
        canRedo: ctx.editor.can().chain().focus().redo().run(),
        isLink: ctx.editor.isActive("link"),
        canSetLink: ctx.editor
          .can()
          .chain()
          .focus()
          .setLink({ href: "https://isub.co.jp" })
          .setUnderline()
          .run(),
      };
    },
  });

  const setLink = useCallback(() => {
    const previousUrl = editor.getAttributes("link").href;
    const url = window.prompt("URL", previousUrl);

    if (url === null) {
      return;
    }

    if (url === "") {
      editor.chain().focus().extendMarkRange("link").unsetLink().run();

      return;
    }

    try {
      editor
        .chain()
        .focus()
        .extendMarkRange("link")
        .setLink({ href: url })
        .setUnderline()
        .run();
    } catch (e: any) {
      alert(e.message);
    }
  }, [editor]);

  return (
    <div className={scss.controlGroup}>
      <div className={scss.buttonGroup}>
        <HeadingSelect editor={editor} />
        <button
          onClick={() => editor.chain().focus().toggleBulletList().run()}
          className={editorState.isBulletList ? scss.isActive : ""}
        >
          <MySvgIcon svgIconName={SvgIconNames.ListBullet} />
        </button>
        <button
          onClick={() => editor.chain().focus().toggleOrderedList().run()}
          className={editorState.isOrderedList ? scss.isActive : ""}
        >
          <MySvgIcon svgIconName={SvgIconNames.ListNumber} />
        </button>
        <button
          onClick={() => editor.chain().focus().toggleBold().run()}
          disabled={!editorState.canBold}
          className={editorState.isBold ? scss.isActive : ""}
        >
          <MySvgIcon svgIconName={SvgIconNames.Bold} />
        </button>
        <button
          onClick={() => editor.chain().focus().toggleItalic().run()}
          disabled={!editorState.canItalic}
          className={editorState.isItalic ? scss.isActive : ""}
        >
          <MySvgIcon svgIconName={SvgIconNames.Italic} />
        </button>
        <button
          onClick={() => editor?.chain().focus().toggleUnderline().run()}
          disabled={!editorState.canUnderline}
          className={editorState.isUnderline ? scss.isActive : ""}
        >
          <MySvgIcon svgIconName={SvgIconNames.Underline} />
        </button>

        <HighlightSelect editor={editor} editorState={editorState} />

        <button
          onClick={() => editor.chain().focus().toggleStrike().run()}
          disabled={!editorState.canStrike}
          className={editorState.isStrike ? scss.isActive : ""}
        >
          <MySvgIcon svgIconName={SvgIconNames.StrikeThrough} />
        </button>
        <TextAlignButtons editor={editor} />
        <button
          onClick={() => editor.chain().focus().setHorizontalRule().run()}
          disabled={!editorState.canInsertHorizontalRule}
        >
          <MySvgIcon svgIconName={SvgIconNames.HorizontalRule} />
        </button>
        <button
          onClick={() => editor.chain().focus().toggleBlockquote().run()}
          className={editorState.isBlockquote ? scss.isActive : ""}
        >
          <MySvgIcon svgIconName={SvgIconNames.BlockQuote} />
        </button>
        <button
          onClick={() => editor.chain().focus().toggleCodeBlock().run()}
          className={editorState.isCodeBlock ? scss.isActive : ""}
        >
          <MySvgIcon svgIconName={SvgIconNames.CodeBlock} />
        </button>
        <button
          onClick={() => editor.chain().focus().toggleCode().run()}
          disabled={!editorState.canCode}
          className={editorState.isCode ? scss.isActive : ""}
        >
          <MySvgIcon svgIconName={SvgIconNames.Code} />
        </button>
        <button
          onClick={setLink}
          className={editorState.isLink ? scss.isActive : ""}
        >
          <MySvgIcon svgIconName={SvgIconNames.Link} />
        </button>
        <button onClick={() => editor.chain().focus().setHardBreak().run()}>
          <MySvgIcon svgIconName={SvgIconNames.HardBreak} />
        </button>
        <button
          onClick={() => editor.chain().focus().undo().run()}
          disabled={!editorState.canUndo}
        >
          <MySvgIcon svgIconName={SvgIconNames.Undo} />
        </button>
        <button
          onClick={() => editor.chain().focus().redo().run()}
          disabled={!editorState.canRedo}
        >
          <MySvgIcon svgIconName={SvgIconNames.Redo} />
        </button>
        <button
          onClick={() => {
            editor.chain().focus().unsetAllMarks().run();
            editor.chain().focus().clearNodes().run();
          }}
        >
          <MySvgIcon svgIconName={SvgIconNames.Reset} />
        </button>
      </div>
    </div>
  );
};

export default MenuBar;

5-1-6. src/app/components/component15/page.module.scss

.component {
  & .controlGroup {
    align-items: flex-start;
    background-color: white;
    display: flex;
    flex-direction: column;
    gap: 1rem;
    padding: 1.5rem;

    & .buttonGroup {
      display: flex;
      flex-wrap: wrap;
      gap: 0.25rem;
      border: 1px solid #e7e5e3;
      border-radius: 0.5rem;
      padding: 6px;

      & button {
        background: #e7e5e3;
        border-radius: 0.5rem;
        border: none;
        color: black;
        font-family: inherit;
        font-size: 0.875rem;
        font-weight: 500;
        line-height: 1.15;
        padding: 0.375rem 0.625rem;
        transition: all 0.2s cubic-bezier(0.65, 0.05, 0.36, 1);
      }

      & button:not([disabled]),
      select:not([disabled]) {
        cursor: pointer;
      }

      & .isActive {
        background: #5704c5;
        color: white;
      }
    }
  }

  & .tiptap {
    caret-color: var(--purple-light);
    margin: 1.5rem;
    :first-child {
      margin-top: 0;
    }

    & ul {
      display: block;
      list-style-type: disc;
      margin-block-start: 1em;
      margin-block-end: 1em;
      padding-inline-start: 40px;
      unicode-bidi: isolate;

      & li p {
        margin-top: 0.25em;
        margin-bottom: 0.25em;
      }
    }

    & ol {
      display: block;
      list-style-type: decimal;
      margin-block-start: 1em;
      margin-block-end: 1em;
      padding-inline-start: 40px;
      unicode-bidi: isolate;

      & li p {
        margin-top: 0.25em;
        margin-bottom: 0.25em;
      }
    }

    & h1,
    & h2,
    & h3,
    & h4,
    & h5,
    & h6 {
      line-height: 1.1;
      margin-top: 2.5rem;
      text-wrap: pretty;
    }

    & h1,
    & h2 {
      margin-top: 3.5rem;
      margin-bottom: 1.5rem;
    }

    & h1 {
      font-size: 1.4rem;
    }

    & h2 {
      font-size: 1.2rem;
    }

    & h3 {
      font-size: 1.1rem;
    }

    & h4,
    & h5,
    & h6 {
      font-size: 1rem;
    }

    & code {
      background-color: var(--purple-light);
      border-radius: 0.4rem;
      color: var(--black);
      font-size: 0.85rem;
      padding: 0.25em 0.3em;
    }

    & pre {
      background-color: black;
      border-radius: 0.5rem;
      color: white;
      font-family: "JetBrainsMono", monospace;
      margin: 1.5rem 0;
      padding: 0.75rem 1rem;

      & code {
        background: none;
        color: inherit;
        font-size: 0.8rem;
        padding: 0;
      }
    }

    & blockquote {
      border-left: 3px solid lightgray;
      margin: 1.5rem 0;
      padding-left: 1rem;
      & p {
        display: block;
        margin-block-start: 1em;
        margin-block-end: 1em;
        margin-inline-start: 0px;
        margin-inline-end: 0px;
        unicode-bidi: isolate;
      }
    }

    & hr {
      border: 1.5px solid #e7e5e3;
      border-top: 1px solid var(--gray-2);
      margin: 2rem 0;
    }

    & p {
      display: block;
      margin-block-start: 1em;
      margin-block-end: 1em;
      margin-inline-start: 0px;
      margin-inline-end: 0px;
      unicode-bidi: isolate;
      & em {
        font-style: italic;
      }
      & strong {
        font-weight: bolder;
      }
      & a {
        text-decoration: underline;
        color: #2563eb;
        text-underline-offset: 1px;
      }
      & code {
        background-color: #eee;
        border-radius: 3px;
        font-family: courier, monospace;
        padding: 0 3px;
      }
      & s {
        text-decoration: line-through;
      }
      & u {
        text-decoration: underline;
      }
      & mark {
        background-color: yellow;
        padding: 0 2px;
        border-radius: 2px;
      }
    }
  }
}

5-1-7. src/app/components/component15/page.tsx

"use client";

import scss from "./page.module.scss";

import { TextStyle } from "@tiptap/extension-text-style";
import type { Editor } from "@tiptap/react";
import { EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Link from "@tiptap/extension-link";
import Underline from "@tiptap/extension-underline";
import Highlight from "@tiptap/extension-highlight";
import TextAlign from "@tiptap/extension-text-align";
import Image from "@tiptap/extension-image";
import React from "react";
import MenuBar from "./menubar";
import { Button, Divider } from "@mui/material";
import HtmlIcon from "@mui/icons-material/Html";

const extensions = [
  StarterKit,
  TextStyle,
  Link.configure({
    openOnClick: false,
    autolink: true,
    linkOnPaste: true,
    defaultProtocol: "https",
    protocols: ["http", "https"],
    isAllowedUri: (url, ctx) => {
      try {
        const parsedUrl = url.includes(":")
          ? new URL(url)
          : new URL(`${ctx.defaultProtocol}://${url}`);

        if (!ctx.defaultValidate(parsedUrl.href)) {
          return false;
        }

        const disallowedProtocols = ["ftp", "file", "mailto"];
        const protocol = parsedUrl.protocol.replace(":", "");

        if (disallowedProtocols.includes(protocol)) {
          return false;
        }

        const allowedProtocols = ctx.protocols.map((p) =>
          typeof p === "string" ? p : p.scheme
        );

        if (!allowedProtocols.includes(protocol)) {
          return false;
        }

        const disallowedDomains = [
          "example-phishing.com",
          "malicious-site.net",
        ];
        const domain = parsedUrl.hostname;

        if (disallowedDomains.includes(domain)) {
          return false;
        }

        return true;
      } catch {
        return false;
      }
    },
    shouldAutoLink: (url) => {
      try {
        const parsedUrl = url.includes(":")
          ? new URL(url)
          : new URL(`https://${url}`);

        const disallowedDomains = [
          "example-no-autolink.com",
          "another-no-autolink.com",
        ];
        const domain = parsedUrl.hostname;

        return !disallowedDomains.includes(domain);
      } catch {
        return false;
      }
    },
  }),
  Underline,
  Highlight.configure({ multicolor: true }),
  TextAlign.configure({ types: ["heading", "paragraph"] }),
  Image,
];

const Component15 = () => {
  const editor: Editor | null = useEditor({
    extensions,
    injectCSS: true,
    immediatelyRender: false,
    content: `
    <h2>
      Hi there,
    </h2>
    <p>
      this is a <em>basic</em> example of <strong>Tiptap</strong>. Sure, there are all kind of basic text styles you’d probably expect from a text editor. But wait until you see the lists:
    </p>
    <ul>
      <li>
        That’s a bullet list with one …
      </li>
      <li>
        … or two list items.
      </li>
    </ul>
    <p>
      Isn’t that great? And all of that is editable. But wait, there’s more. Let’s try a code block:
    </p>
    <pre><code class="language-css">body {
      display: none;
    }</code></pre>
    <p>
      I know, I know, this is impressive. It’s only the tip of the iceberg though. Give it a try and click a little bit around. Don’t forget to check the other examples too.
    </p>
    <blockquote>
      Wow, that’s amazing. Good work, boy! 👏
      <br />
      — Mom
    </blockquote>
    `,
  });

  const getContent = () => {
    console.log(editor?.getHTML());
  };

  return editor ? (
    <div className={scss.component}>
      <MenuBar editor={editor} />
      <div className={scss.tiptap}>
        <EditorContent editor={editor} role="presentation" />
      </div>
      <Divider sx={{ marginTop: 1, marginBottom: 1 }} />
      <Button
        variant="contained"
        startIcon={<HtmlIcon />}
        size="small"
        color="primary"
        onClick={() => getContent()}
      >
        Get content
      </Button>
    </div>
  ) : (
    <div>Loading...</div>
  );
};

export default Component15;

5-1-8. src/app/components/page.module.scss

.components {
  color: blue;
  & ul {
    margin-left: 20px;
    & li {
      list-style: disc;
    }
  }
}

5-1-9. src/app/components/page.tsx

"use client";

import React from "react";
import { Link } from "@mui/material";

import scss from "./page.module.scss";

const Components = () => {
  return (
    <div className={scss.components}>
      <ul>
        <li>
          <Link href="/components/component01" underline="hover">
            Component01
          </Link>
        </li>
        <li>
          <Link href="/components/component02" underline="hover">
            Component02
          </Link>
        </li>
        <li>
          <Link href="/components/component03" underline="hover">
            Component03
          </Link>
        </li>
        <li>
          <Link href="/components/component04" underline="hover">
            Component04
          </Link>
        </li>
        <li>
          <Link href="/components/component05" underline="hover">
            Component05
          </Link>
        </li>
        <li>
          <Link href="/components/component06" underline="hover">
            Component06
          </Link>
        </li>
        <li>
          <Link href="/components/component07" underline="hover">
            Component07
          </Link>
        </li>
        <li>
          <Link href="/components/component08" underline="hover">
            Component08
          </Link>
        </li>
        <li>
          <Link href="/components/component09" underline="hover">
            Component09
          </Link>
        </li>
        <li>
          <Link href="/components/component10" underline="hover">
            Component10
          </Link>
        </li>
        <li>
          <Link href="/components/component11" underline="hover">
            Component11
          </Link>
        </li>
        <li>
          <Link href="/components/component12" underline="hover">
            Component12
          </Link>
        </li>
        <li>
          <Link href="/components/component13" underline="hover">
            Component13
          </Link>
        </li>
        <li>
          <Link href="/components/component14" underline="hover">
            Component14
          </Link>
        </li>
        <li>
          <Link href="/components/component15" underline="hover">
            Component15
          </Link>
        </li>
      </ul>
    </div>
  );
};

export default Components;

6. サーバーを起動

npm run dev

7. ブラウザで確認

  • http://localhost:3000

7-1-1. 画面

8. ディレクトリの構造

省略

9. 備考

今回はオンラインHTMLエディターTiptapのシンプルな内容についてでした。

10. 参考

投稿者プロフィール

Sondon
開発好きなシステムエンジニアです。
卓球にハマってます。

関連記事

  1. 【NextJS】OAuth authentication with A…

  2. 【NextJS】NextJS・TypeScript・Apollo Cl…

  3. 【NextJS】Error Handling

  4. 【NextJS】Firestore

  5. 【NextJS】Server Actions with MySQL

  6. 【NextJS】Dynamic Routes

最近の記事

  1. AWS
  2. Node.js

制作実績一覧

  1. Checkeys