Table options, DFW-Button
-
Hello Andrew,
It’s been a long time since I wrote these two posts:
https://wordpress.org/support/topic/no-flyover-toolbar-on-tables-tab-key-doesnt-select-text/
https://wordpress.org/support/topic/two-separate-fullscreen-options/
But I haven’t actually found any solutions to the issues raised in all these years – until now.
With the help of Copilot, I was able to create a PHP script that actually solved most of these issues. The script has three parts. The first part does everything related to the tables:- Tab/Shift+Tab now selects the entire cell content after each press
- Ctrl+A selects the entire content of the cell where the cursor is located
- If you press Ctrl+A multiple times, the entire row is selected in the next step, followed by the entire table, and then the cell again (as in OneNote)
Then I mentioned the WordPress native full screen or DFW button, which cannot be hidden (because it does not really provide a correct full screen view). As you replied, it can be hidden via the menu at the top right. However, if you have multiple users, this is a bit tedious because it cannot be controlled globally. Therefore, the second script simply hides this button. I actually wanted to completely disable the DFW function (with button and option), but I couldn’t do that with the script because my theme or WPBakery keeps reactivating this mode.
Then the last part of the script activates the flyout menu for tables. Then I don’t have to install “Advanced TinyMCE Configuration” separately if I can solve it right here.
So I would be particularly interested to hear what you think about the first script for tables. I can’t judge how good the quality of the PHP code is. But would it be possible to check this and build such a function directly into “Advanced Editor Tools”? Of course, it would be great if you could also hide the WP’s own full-screen button there, as only the full-screen button that can be displayed with “Advanced Editor Tools” works correctly! But I think that if you did, you wouldn’t want to simply hide the button using CSS, but rather have an option to completely disable DFW mode. But I can’t implement that. Then there’s the functionality of “Advanced TinyMCE Configuration.” Couldn’t you integrate those options into the “Advanced Editor Tools” plugin? Then you wouldn’t have to install another plugin, and the functionalities are very closely related thematically.
So, those are my “nice to haves.” Let’s see what you think… 😉
Many thanks, Stefan/**
* Plugin Name: SK TinyMCE Modifier
* Description: Sammel-Snippet für TinyMCE/Classic-Editor-Optimierungen
* Version: 1.2.2
* 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=122');
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
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;
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);The page I need help with: [log in to see the link]
You must be logged in to reply to this topic.