tinymceの設定例(コード)

<?php
/**
 * TinyMCE 共通読み込みファイル
 * edit.php などから require される想定
 *
 * 前提:
 * - BASE_URL が定義済み
 * - textarea の id が #editor
 */
?>
<!-- TinyMCE -->
<script src="<?= BASE_URL ?>/tinymce/tinymce.min.js"></script>

<script>
document.addEventListener("DOMContentLoaded", function () {

  if (!document.querySelector('#editor')) return;

  const baseUrl = window.EDITOR_BASE_URL;  // 例: /newblog or ''

  tinymce.init({
    selector: '#editor',
    height: 'auto',
    min_height: 400,

    menubar: true,
    plugins: 'autoresize link lists table code',
    autoresize_bottom_margin: 30,
    autoresize_overflow_padding: 10,
    toolbar:
      'undo redo | formatselect | bold italic underline | ' +
      'bullist numlist | link table | code',
    forced_root_block: 'p',
    convert_urls: false,
    relative_urls: false,

    content_css: baseUrl + "/inc/editor-tinymce.css",

  });

});
</script>

/**************************************
 * TinyMCE 初期化+右クリックメニュー
 **************************************/
document.addEventListener("DOMContentLoaded", function () {

    let __editorReady = false;
    const dirRel  = window.EDITOR_DIR_REL;   // 例: /article/xxx/
    const baseUrl = window.EDITOR_BASE_URL;  // 例: /newblog or ''

    /******** TinyMCE 初期化 ********/
    tinymce.init({
        selector: '#editor',
        menubar: true,
        contextmenu: false,   // TinyMCE 標準の右クリックメニューを無効化

        paste_as_text: false,
        paste_word_valid_elements: "table,tr,td,th,tbody,thead,tfoot",
        paste_retain_style_properties: "",
        paste_remove_styles_if_match: ".*",
        paste_remove_styles: true,
        paste_remove_spans: true,
        paste_strip_class_attributes: "all",
        paste_auto_cleanup_on_paste: true,
        
        valid_children: '+div[p|ul|ol],+ul[li],+ol[li],+li[ul|ol]',

        forced_root_block: 'p',   // Enter を押したとき、見出しのまま次行にしない
        block_formats: '段落=p; 見出し2=h2; 見出し3=h3; 見出し4=h4',   // H2/H3 の Enter で段落(p)を作る設定

        plugins: 'link lists table code image media fullscreen preview autoresize',

        autoresize_bottom_margin: 20,
        autoresize_overflow_padding: 20,
        autoresize_on_init: true,

        toolbar: `
          undo redo | formatselect |
          bold italic underline forecolor backcolor |
          alignleft aligncenter alignright |
          bullist numlist outdent indent |
          link image media table |
          insertImageFromFolder |
          to_paragraph |
          insert_introbox |
          insert_empty_line |
          clean_table |
          insert_box | code fullscreen preview
        `,

        convert_urls: false,
        relative_urls: false,
        remove_script_host: false,

        paste_data_images: true,
        valid_elements: '*[*]',
        extended_valid_elements: '*[*]',
        sandbox_iframes: false,
        invalid_elements: '',
        media_live_embeds: true,

        content_css: baseUrl + "/inc/editor-style.css",

        content_style: `
          img {
            max-width: 100%;
            height: auto !important;
            display: block !important;
            min-height: 20px;
          }
          figure { max-width: 100%; }
        `,

        /* ==========================
         * 画像アップロード設定
         * ========================== */
        automatic_uploads: true,
        image_uploadtab: false,
        file_picker_types: 'image',

        file_picker_callback: function (callback, value, meta) {
            if (meta.filetype !== 'image') return;

            const input = document.createElement('input');
            input.type = 'file';
            input.accept = 'image/*';

            input.onchange = function () {
                const file = input.files[0];
                const formData = new FormData();
                formData.append('file', file);
                formData.append('dir', dirRel);

                const xhr = new XMLHttpRequest();
                xhr.open('POST', baseUrl + '/inc/upload_image.php');

                xhr.onload = function () {
                    if (xhr.status !== 200) {
                        alert("Upload error");
                        return;
                    }
                    const json = JSON.parse(xhr.responseText);
                    if (!json.location) {
                        alert("Invalid response");
                        return;
                    }
                    callback(json.location);
                };

                xhr.send(formData);
            };

            input.click();
        },

        images_upload_handler: function (blobInfo) {

            return new Promise((resolve, reject) => {

                const xhr = new XMLHttpRequest();
                xhr.open('POST', baseUrl + '/inc/upload_image.php');

                xhr.onload = function () {
                    if (xhr.status !== 200) {
                        reject('HTTP Error: ' + xhr.status);
                        return;
                    }

                    let json;
                    try {
                        json = JSON.parse(xhr.responseText);
                    } catch (e) {
                        reject('Invalid JSON');
                        return;
                    }

                    if (!json.location) {
                        reject('Invalid response');
                        return;
                    }

                    resolve(json.location);
                };

                xhr.onerror = function () {
                    reject('Upload failed');
                };

                const formData = new FormData();
                formData.append('file', blobInfo.blob(), blobInfo.filename());
                formData.append('dir', dirRel);

                xhr.send(formData);
            });
        },


        /* ==========================
         * カスタムボタンなど
         * ========================== */
        setup: function (editor) {

            // グローバルに保持(必要なら)
            window.__EDITOR__ = editor;

            // 画像挿入ボタン(ツールバー)
            editor.ui.registry.addButton('insertImageFromFolder', {
              icon: 'image',
              tooltip: '画像を挿入(imgフォルダから)',
              onAction: function () {
                if (window.__openImagePicker) {
                  window.__openImagePicker(editor);
                }
              }
            });

            editor.on('change input undo redo', () => {
              if (!__editorReady) return;
              window.markDirty && window.markDirty();
            });

            editor.on('keydown', (e) => {
              if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
                e.preventDefault();
                window.doSave && window.doSave();
              }
            });


            // ■ ctrlを押してる場合リンクを開く。
            editor.on('click', function (e) {
              const a = e.target.closest('a');
              if (!a) return;

              if (e.ctrlKey || e.metaKey) {
                // 何もしない
                // → ブラウザの標準 Ctrl+クリックに任せる
                return;
              }
            });

            // ■ 空行挿入ボタン
            editor.ui.registry.addButton('insert_empty_line', {
                text: '空行挿入',
                tooltip: '現在の要素の下に空行を挿入',
                onAction: function () {
                    insertEmptyLineAfterCurrent(editor);
                }
            });

            // ■ ボックス挿入ボタン
            editor.ui.registry.addMenuButton('insert_box', {
                text: 'ボックス',
                fetch: function(callback) {
                    const items = [
                        {
                            type: 'menuitem',
                            text: '青ボックス',
                            onAction: function() {
                                editor.insertContent('<div class="box-blue">ここに文章</div><p></p>');
                            }
                        },
                        {
                            type: 'menuitem',
                            text: '白ボックス',
                            onAction: function() {
                                editor.insertContent('<div class="box-white">ここに文章</div><p></p>');
                            }
                        },
                        {
                            type: 'menuitem',
                            text: '黄色ボックス',
                            onAction: function() {
                                editor.insertContent('<div class="box-yellow">ここに文章</div><p></p>');
                            }
                        },
                        {
                            type: 'menuitem',
                            text: '緑ボックス',
                            onAction: function() {
                                editor.insertContent('<div class="box-green">ここに文章</div><p></p>');
                            }
                        }
                    ];
                    callback(items);
                }
            });

            // ■ イントロボックス挿入ボタン
            editor.ui.registry.addButton('insert_introbox', {
                text: 'イントロBOX',
                tooltip: 'イントロボックスを挿入',
                onAction: function () {
                    const html = `
                    <div class="intro-box">
                      <div class="intro-box-title">ここにタイトル</div>
                      <ul>
                        <li>ここに項目を書きます</li>
                        <li>ここに項目を書きます</li>
                        <li>ここに項目を書きます</li>
                      </ul>
                    </div>
                    `;
                    editor.insertContent(html);
                }
            });

            // ■ 見出しを段落に戻すボタン
            editor.ui.registry.addButton('to_paragraph', {
                text: '段落に戻す',
                tooltip: '見出しを通常の段落に戻す',
                onAction: function () {
                    editor.execCommand('FormatBlock', false, 'p');
                }
            });

            // ===============================
            // 見出し(H2/H3/H4) の先頭で Enter を押しても複製しない
            // 常に <p> を作成
            // ===============================
            editor.on("keydown", function (e) {

                // Enter キーのみ対象
                if (e.key !== "Enter") return;

                const node = editor.selection.getNode();

                // Hタグなら処理
                if (/^H[1-6]$/.test(node.tagName)) {

                    // デフォルト挙動をキャンセル(複製される動作)
                    e.preventDefault();

                    // 見出し直後に <p> を挿入
                    editor.selection.collapse(false);  // カーソルを見出しの“後ろ”へ
                    editor.insertContent("<p><br></p>");

                    // 追加した p の中へカーソル移動
                    const rng = editor.dom.createRng();
                    const newP = node.nextSibling;
                    if (newP) {
                        rng.setStart(newP, 0);
                        rng.setEnd(newP, 0);
                        editor.selection.setRng(rng);
                    }
                }
            });


            // 🔵 画像クリック時の動作(空画像 → 画像選択 / 通常 → 編集モーダル)
            editor.on('click', function (e) {
                if (e.target.nodeName !== 'IMG') return;

                const img = e.target;
                const src = img.getAttribute("src");

                // ★ 横並び画像の空画像は image-row.js が処理するので何もしない
                if (!src || src.trim() === "") {
                    return;
                }

                // ★ 通常画像 → alt/caption 編集モーダルへ
                window.openImageEditModal(editor, img);
            });


            // 🔵 画像をダブルクリック → 画像挿入モーダルを開く
            editor.on("dblclick", function (e) {
                if (e.target.nodeName === "IMG") {
                    // 画像選択モーダルを開く
                    if (typeof window.__openImagePicker === "function") {
                        window.__openImagePicker(editor);
                    }
                }
            });


            // =============== F4 キーの捕捉 ===============
            editor.on("keydown", function(e) {
                if (e.key === "F4") {
                    if (typeof window.__lastRepeatAction === "function") {
                        window.__lastRepeatAction();
                        e.preventDefault();
                        e.stopPropagation();
                    }
                }
            });

            // =======================
            // 文字数カウンター
            // =======================
            editor.on("input change undo redo setcontent", () => {
              if (window.updateCharCountSafe) {
                window.updateCharCountSafe(editor);
              }

              if (__editorReady) {
                window.markDirty();
              }
            });

            editor.on("init", () => {
              __editorReady = true;

              // 文字数初期表示
              window.updateCharCountSafe && window.updateCharCountSafe(editor);

              // 初期状態は「未編集」
              window.markSaved && window.markSaved();
            });

            // ■ 表を整える(ツールバー版)
            editor.ui.registry.addButton('clean_table', {
                text: '表を整える',
                tooltip: '貼り付け後の表を最適化',
                onAction: function () {
                    let html = editor.getContent();

                    // 1) style="" 削除
                    html = html.replace(/ style="[^"]*"/g, '');
                    // 2) class="" 削除
                    html = html.replace(/ class="[^"]*"/g, '');
                    // 3) span 削除
                    html = html.replace(/<span[^>]*>/g, '').replace(/<\/span>/g, '');
                    // 4) table 内の p 削除
                    html = html.replace(/(<table[\s\S]*?<\/table>)/g, function(tableHtml) {
                        return tableHtml
                            .replace(/<p[^>]*>/g, '')
                            .replace(/<\/p>/g, '');
                    });
                    // 5) table の style 統一
                    html = html.replace(
                        /<table/g,
                        '<table style="width:100%; border-collapse:collapse;"'
                    );
                    // 6) td/th のスタイル
                    html = html.replace(
                        /<t(d|h)(.*?)>/g,
                        '<t

<?php
/**
 * TinyMCE 共通読み込みファイル
 * edit.php などから require される想定
 *
 * 前提:
 * - BASE_URL が定義済み
 * - textarea の id が #editor
 */
?>
<!-- TinyMCE -->
<script src="<?= BASE_URL ?>/tinymce/tinymce.min.js"></script>

<script>
document.addEventListener("DOMContentLoaded", function () {

  if (!document.querySelector('#editor')) return;

  const baseUrl = window.EDITOR_BASE_URL;  // 例: /newblog or ''

  tinymce.init({
    selector: '#editor',
    height: 'auto',
    min_height: 400,

    menubar: true,
    plugins: 'autoresize link lists table code',
    autoresize_bottom_margin: 30,
    autoresize_overflow_padding: 10,
    toolbar:
      'undo redo | formatselect | bold italic underline | ' +
      'bullist numlist | link table | code',
    forced_root_block: 'p',
    convert_urls: false,
    relative_urls: false,

    content_css: baseUrl + "/inc/editor-tinymce.css",

  });

});
</script>

 

style="border:1px solid #ccc; padding:10px;">'
                    );
                    // 7) tbody 補完
                    html = html.replace(/<table([^>]*)>(?![\s\S]*?<tbody)/g, '<table

<?php
/**
 * TinyMCE 共通読み込みファイル
 * edit.php などから require される想定
 *
 * 前提:
 * - BASE_URL が定義済み
 * - textarea の id が #editor
 */
?>
<!-- TinyMCE -->
<script src="<?= BASE_URL ?>/tinymce/tinymce.min.js"></script>

<script>
document.addEventListener("DOMContentLoaded", function () {

  if (!document.querySelector('#editor')) return;

  const baseUrl = window.EDITOR_BASE_URL;  // 例: /newblog or ''

  tinymce.init({
    selector: '#editor',
    height: 'auto',
    min_height: 400,

    menubar: true,
    plugins: 'autoresize link lists table code',
    autoresize_bottom_margin: 30,
    autoresize_overflow_padding: 10,
    toolbar:
      'undo redo | formatselect | bold italic underline | ' +
      'bullist numlist | link table | code',
    forced_root_block: 'p',
    convert_urls: false,
    relative_urls: false,

    content_css: baseUrl + "/inc/editor-tinymce.css",

  });

});
</script>

 

><tbody>');
                    html = html.replace(/<\/table>/g, '</tbody></table>');

                    editor.setContent(html);
                    editor.notificationManager.open({
                        text: '表を最適化しました',
                        type: 'success',
                        timeout: 2000
                    });
                }
            });
        } // setup end
    }); // tinymce.init end
}); // DOMContentLoaded end