<?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