I have updated my script because I noticed that Advanced Editor Tool actually had a function that automatically created a new row in the last cell of a table when you pressed the tab key. This no longer worked with my script. I have now corrected this. Here is the updated version. I also tweaked the table flyout menu a bit with CSS to make it more visible:
/**
* Plugin Name: Synor TinyMCE Modifier
* Description: Sammel-Snippet für TinyMCE/Classic-Editor-Optimierungen
* Version: 1.2.6
* Author: Synor Media/Stefan Krapf mit Copilot 365
*/
if (!defined('ABSPATH')) exit;
/* =============================================================================
* SECTION A — TinyMCE Tabellen-Booster
* -----------------------------------------------------------------------------
* FUNKTION:
* - Tab / Shift+Tab:
* springt zur nächsten/vorigen Tabellenzelle und markiert
* den *kompletten ZellINHALT*. Funktioniert auch in WPBakery/Impreza-Overlays.
*
* - Ctrl/Cmd + A (nur wenn Cursor in einer Tabellenzelle steht):
* 1× = Zelle, 2× = ganze Zeile (TR), 3× = ganze Tabelle (TABLE),
* dann wieder Zelle (Zyklus wie in OneNote).
*
* - Beschränkung auf Editor-IDs:
* • content (Haupteditor)
* • wpb_tinymce_content (WPBakery/Impreza-Modal)
* ========================================================================== */
/**
* Externes TinyMCE-Plugin registrieren (JavaScript wird via admin-ajax ausgeliefert).
* Hinweis: Verwende in der URL normales '&' (kein HTML-escaped '&').
* Erhöhe 'ver=' als Cachebuster, wenn du änderst.
*/
add_filter('mce_external_plugins', function($external, $editor_id){
$allow = array('content', 'wpb_tinymce_content');
if (!in_array($editor_id, $allow, true)) return $external;
$external['selectnextcell'] = admin_url('admin-ajax.php?action=sm_selectnextcell_js&ver=126');
return $external;
}, 10, 2);
/**
* Pluginname 'selectnextcell' in die TinyMCE-Pluginliste hängen.
* Hinweis: 'table' NICHT erzwingen (kann 404 erzeugen, wenn im Stack nicht vorhanden).
*/
add_filter('tiny_mce_before_init', function($init, $editor_id){
$allow = array('content', 'wpb_tinymce_content');
if (!in_array($editor_id, $allow, true)) return $init;
$plugins = isset($init['plugins']) ? $init['plugins'] : '';
if (is_array($plugins)) {
if (!in_array('selectnextcell', $plugins, true)) $plugins[] = 'selectnextcell';
$init['plugins'] = $plugins;
} else {
$set = array_filter(array_map('trim', preg_split('/[\s,]+/', (string)$plugins)));
if (!in_array('selectnextcell', $set, true)) $set[] = 'selectnextcell';
$init['plugins'] = implode(' ', $set);
}
return $init;
}, 10, 2);
/**
* AJAX: Auslieferung des TinyMCE-Plugins (JavaScript)
*/
add_action('wp_ajax_sm_selectnextcell_js', function(){
header('Content-Type: application/javascript; charset=utf-8');
?>
(function(){
if (!window.tinymce || !tinymce.PluginManager) return;
// --- Timing-Helfer: fn in n Frames ausführen ---
function afterFrames(fn, n){
var raf = window.requestAnimationFrame || function(f){ return setTimeout(f, 16); };
(function step(i){
if (i <= 0) { try { fn(); } catch(e){} return; }
raf(function(){ step(i-1); });
})(n||1);
}
// --- Auswahl-Helfer (TinyMCE-Selection, stabil für MCE4) ---
function setRangeOnNodeContents(editor, node){
var rng = editor.dom.createRng();
rng.setStart(node, 0);
rng.setEnd(node, node.childNodes.length);
editor.selection.setRng(rng);
editor.nodeChanged();
}
function selectCell(editor, cell){
if (!cell) return;
try {
editor.focus();
if (editor.getWin && editor.getWin().focus) editor.getWin().focus();
setRangeOnNodeContents(editor, cell);
} catch(ex){
try { editor.selection.select(cell, true); editor.nodeChanged(); } catch(_){}
}
}
function selectRow(editor, row){
if (!row) return;
try { setRangeOnNodeContents(editor, row); } catch(e){}
}
function selectTable(editor, table){
if (!table) return;
try { setRangeOnNodeContents(editor, table); } catch(e){}
}
// --- Nachbarzelle mit Wrap-Around innerhalb *derselben* Tabelle ---
function getNeighborCell(editor, curCell, dir){
if (!curCell) return null;
var table = editor.dom.getParent(curCell, 'table');
if (!table) return null;
// Alle Zellen (thead/tbody/tfoot inkl.)
var cells = editor.dom.select('td,th', table);
if (!cells || !cells.length) return null;
var idx = Array.prototype.indexOf.call(cells, curCell);
if (idx < 0) return null;
var nextIdx = (idx + (dir > 0 ? 1 : -1) + cells.length) % cells.length;
return cells[nextIdx];
}
// --- Kontext-Resolver: Bleib im Tabellenkontext, auch wenn Cursor auf TR/TABLE liegt ---
function getTableContext(editor){
// Nutze sowohl getNode() als auch den Range-Start
var node = editor.selection.getNode();
var rng = editor.selection.getRng ? editor.selection.getRng() : null;
var sc = rng ? rng.startContainer : null;
// Finde die Tabelle zuverlässig
var table = editor.dom.getParent(node, 'table') ||
(sc && editor.dom.getParent(sc, 'table')) || null;
if (!table) return {};
// Versuche eine Zelle zu finden
var cell = editor.dom.getParent(node, 'td,th') ||
(sc && editor.dom.getParent(sc, 'td,th')) || null;
// Versuche die Zeile zu finden (falls gebraucht)
var row = editor.dom.getParent(node, 'tr') ||
(cell && editor.dom.getParent(cell, 'tr')) ||
(sc && editor.dom.getParent(sc, 'tr')) || null;
// Falls keine Zelle ermittelbar: nimm letzte gemerkte Zelle innerhalb derselben Tabelle…
if (!cell) {
var st = editor.__sm_cycle && editor.__sm_cycle.lastCell;
if (st && editor.dom.getParent(st, 'table') === table) {
cell = st;
} else {
// …oder fallback auf die erste Zelle der Tabelle
var cells = editor.dom.select('td,th', table);
cell = (cells && cells[0]) ? cells[0] : null;
// Row ggf. aus dieser Zelle ableiten
if (!row && cell) row = editor.dom.getParent(cell, 'tr');
}
}
return { table: table, row: row, cell: cell };
}
// --- OneNote-Style Ctrl/Cmd + A: 1) Zelle 2) Zeile 3) Tabelle (dann wieder Zelle) ---
function cycleSelect(editor, cell){
var row = editor.dom.getParent(cell, 'tr');
var table = editor.dom.getParent(cell, 'table');
if (!row || !table) { selectCell(editor, cell); return; }
editor.__sm_cycle = editor.__sm_cycle || { lastTable: null, lastCell: null, level: 0 };
// Kontextwechsel? Dann mit Level 1 beginnen
if (editor.__sm_cycle.lastTable !== table || editor.__sm_cycle.lastCell !== cell) {
editor.__sm_cycle.level = 1;
} else {
editor.__sm_cycle.level = (editor.__sm_cycle.level % 3) + 1; // 1..3
}
editor.__sm_cycle.lastTable = table;
editor.__sm_cycle.lastCell = cell;
var doSelect = function(){
if (editor.__sm_cycle.level === 1) selectCell(editor, cell);
else if (editor.__sm_cycle.level === 2) selectRow(editor, row);
else selectTable(editor, table);
};
// Sofort + verzögert wiederholen (Caret/DOM-Resets überfahren)
doSelect();
afterFrames(doSelect, 1);
afterFrames(doSelect, 2);
}
function attach(editor){
if (!editor || editor.settings.__sm_attached) return;
editor.settings.__sm_attached = true;
// --- TAB / SHIFT+TAB: nächste/vorige Zelle + ZellINHALT markieren ---
function onTab(e){
if (e.keyCode !== 9) return; // Tab
var node = editor.selection.getNode();
var curCell = editor.dom.getParent(node, 'td,th');
if (!curCell) return; // außerhalb von Tabellen: Standardverhalten beibehalten
var table = editor.dom.getParent(curCell, 'table');
if (!table) return;
var cells = editor.dom.select('td,th', table);
if (!cells || !cells.length) return;
// LETZTE ZELLE + TAB (ohne Shift): Zeile selbst einfügen + in neue Zeile springen (Fokus sicher halten)
if (!e.shiftKey && curCell === cells[cells.length - 1]) {
e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation();
// aktuelle TR bestimmen
var curRow = editor.dom.getParent(curCell, 'tr');
// Zeile *nach* der aktuellen einfügen (TinyMCE Table-Command)
try { editor.execCommand('mceTableInsertRowAfter'); } catch(_){}
// Nach Einfügen die nächste Zeile ermitteln und deren erste Zelle fokussieren
afterFrames(function(){
try {
var tableNow = editor.dom.getParent(curCell, 'table') || table;
var rows = editor.dom.select('tr', tableNow);
var rowIdx = Array.prototype.indexOf.call(rows, curRow);
var nextRow = (rowIdx >= 0) ? rows[rowIdx + 1] : null;
var firstCell = nextRow ? editor.dom.select('td,th', nextRow)[0] : null;
if (firstCell) {
// Deine gewohnte Auswahl-Logik beibehalten (ZellINHALT markieren)
selectCell(editor, firstCell);
afterFrames(function(){ selectCell(editor, firstCell); }, 1);
afterFrames(function(){ selectCell(editor, firstCell); }, 2);
}
} catch(_){}
}, 1);
return;
}
// --- Standardfall: Navigation innerhalb der Tabelle (inkl. Wrap-Around) ---
e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation();
var dir = e.shiftKey ? -1 : 1;
var target = getNeighborCell(editor, curCell, dir);
if (!target) return; // theoretisch nie, wegen Wrap-Around
// sofort + 1-2 Frames später auswählen (überfährt Caret-Resets)
selectCell(editor, target);
afterFrames(function(){ selectCell(editor, target); }, 1);
afterFrames(function(){ selectCell(editor, target); }, 2);
}
// --- CTRL/CMD + A im Tabellenkontext: Zelle → Zeile → Tabelle (Zyklus) ---
function onCtrlA(e){
var isCtrlA = (e.keyCode === 65) && (e.ctrlKey || e.metaKey);
if (!isCtrlA) return;
// Neu: Kontext ermitteln (auch wenn Cursor auf TR/TABLE sitzt)
var ctx = getTableContext(editor);
if (!ctx.table) return; // außerhalb von Tabellen: Standardverhalten
e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation();
cycleSelect(editor, ctx.cell);
}
editor.on('keydown', onTab);
editor.on('keydown', onCtrlA);
// Capturing im IFRAME (hilft bei Modals/Overlays)
editor.on('init', function(){
try {
var doc = editor.getDoc();
var capTab = function(e){
if (e.keyCode !== 9) return;
var node = editor.selection.getNode();
var curCell = editor.dom.getParent(node, 'td,th');
if (!curCell) return;
var table = editor.dom.getParent(curCell, 'table');
if (!table) return;
var cells = editor.dom.select('td,th', table);
if (!cells || !cells.length) return;
// LETZTE ZELLE – Zeile selbst einfügen + in neue Zeile springen (Fokus sicher halten)
if (!e.shiftKey && curCell === cells[cells.length - 1]) {
e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation();
var curRow = editor.dom.getParent(curCell, 'tr');
try { editor.execCommand('mceTableInsertRowAfter'); } catch(_){}
afterFrames(function(){
try {
var tableNow = editor.dom.getParent(curCell, 'table') || table;
var rows = editor.dom.select('tr', tableNow);
var rowIdx = Array.prototype.indexOf.call(rows, curRow);
var nextRow = (rowIdx >= 0) ? rows[rowIdx + 1] : null;
var firstCell = nextRow ? editor.dom.select('td,th', nextRow)[0] : null;
if (firstCell) {
selectCell(editor, firstCell);
afterFrames(function(){ selectCell(editor, firstCell); }, 1);
afterFrames(function(){ selectCell(editor, firstCell); }, 2);
}
} catch(_){}
}, 1);
return;
}
e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation();
var dir = e.shiftKey ? -1 : 1;
var target = getNeighborCell(editor, curCell, dir);
if (!target) return;
selectCell(editor, target);
afterFrames(function(){ selectCell(editor, target); }, 1);
afterFrames(function(){ selectCell(editor, target); }, 2);
};
var capCtrlA = function(e){
var isCtrlA = (e.keyCode === 65) && (e.ctrlKey || e.metaKey);
if (!isCtrlA) return;
var ctx = getTableContext(editor);
if (!ctx.table) return;
e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation();
cycleSelect(editor, ctx.cell);
};
doc.addEventListener('keydown', capTab, true);
doc.addEventListener('keydown', capCtrlA, true);
editor.on('remove', function(){
try {
doc.removeEventListener('keydown', capTab, true);
doc.removeEventListener('keydown', capCtrlA, true);
} catch(_){}
});
} catch(_) {}
});
}
// Registrierung als TinyMCE-Plugin
tinymce.PluginManager.add('selectnextcell', function(editor){
attach(editor);
return {};
});
// Für später erzeugte Editoren (z. B. Modal)
tinymce.on('AddEditor', function(e){ try { attach(e.editor); } catch(_){ } });
})();
<?php
exit;
});
/* =============================================================================
* SECTION B — Classic Editor: DFW/Vollhöhe-Buttons ausblenden
* -----------------------------------------------------------------------------
* Ziel: Nur UI verstecken. (Keine Änderung an der WP-Option "editor_expand"/DFW.)
* - TinyMCE (visuell): .mce-wp-dfw (Container + Icon)
* - Quicktags (Text/HTML): .qt-dfw und alle IDs qt_*_dfw (z. B. qt_content_dfw, qt_wpb_tinymce_content_dfw)
* ========================================================================== */
add_action('admin_head', function () {
if ( ! is_admin() ) return;
?>
<style id="sm-hide-dfw-classic-editor">
/* (1) Quicktags – DFW-Buttons im Text-/HTML-Modus ausblenden
- deckt Haupteditor UND WPBakery/Overlays ab
- Beispiele: #qt_content_dfw, #qt_wpb_tinymce_content_dfw */
.quicktags-toolbar .qt-dfw,
[id^="qt_"][id$="_dfw"] {
display: none !important;
}
/* (2) TinyMCE – kompletter DFW-Button-Container + Inhalte ausblenden */
.mce-toolbar .mce-wp-dfw,
.mce-toolbar .mce-wp-dfw * {
display: none !important;
}
/* Lücke/Spacing vermeiden */
.mce-toolbar .mce-wp-dfw {
width: 0 !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
}
/* Optional enger scopen – nur Haupteditor:
#wp-content-editor-container .mce-toolbar .mce-wp-dfw { display:none !important; } */
</style>
<?php
});
/* =============================================================================
* SECTION C — TinyMCE: Table Toolbar Preset (ersetzt Advanced TinyMCE Config)
* -----------------------------------------------------------------------------
* Setzt die table_toolbar wie im ATE-Plugin und stellt sicher, dass das
* eingebaute 'table'-Plugin aktiv ist.
* Gilt nur für die Editor-IDs: 'content', 'wpb_tinymce_content'
* ========================================================================= */
add_filter('tiny_mce_before_init', function($init, $editor_id){
// Nur unsere gewünschten Editoren
$allow = array('content', 'wpb_tinymce_content');
if (!in_array($editor_id, $allow, true)) return $init;
// 1) table_toolbar wie definiert
$init['table_toolbar'] =
'tableprops tablerowprops tablecellprops | ' .
'tableinsertrowbefore tableinsertrowafter tabledeleterow | ' .
'tableinsertcolbefore tableinsertcolafter tabledeletecol | ' .
'tablemergecells tablesplitcells | tabledelete';
// 2) Sicherstellen, dass das 'table'-Plugin aktiv ist
$plugins = isset($init['plugins']) ? $init['plugins'] : '';
if (is_array($plugins)) {
if (!in_array('table', $plugins, true)) $plugins[] = 'table';
$init['plugins'] = $plugins;
} else {
$set = array_filter(array_map('trim', preg_split('/[\s,]+/', (string)$plugins)));
if (!in_array('table', $set, true)) $set[] = 'table';
$init['plugins'] = implode(' ', $set);
}
return $init;
}, 10, 2);
/* -----------------------------------------------------------------------------
* TinyMCE Table-Flyout: Schatten (richtiger Container: .mce-floatpanel)
* -------------------------------------------------------------------------- */
add_action('admin_head', function () {
?>
<style id="sm-tinymce-flyout-shadow">
/* Flyout-Container */
.mce-floatpanel.mce-tinymce-inline {
box-shadow:
0 1px 2px rgba(0,0,0,0.25),
0 4px 10px rgba(0,0,0,0.12),
0 12px 24px rgba(0,0,0,0.10);
border-radius: 6px; /* dezente Abrundung */
border-color: #5c5c5c;
border-style: solid;
overflow: visible; /* falls TinyMCE clipped */
}
</style>
<?php
});