Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
398 changes: 398 additions & 0 deletions htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php

Large diffs are not rendered by default.

193 changes: 193 additions & 0 deletions htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/**
* Self-contained styling for the shared XOOPS DHTML editor toolbar (XoopsDhtmlToolbar).
*
* Framework-neutral: every rule targets the `xo-edtb-*` prefix only, so this renders the same
* whether or not Bootstrap/Tailwind/DaisyUI is loaded — the admin themes load no CSS framework.
* Every colour, radius, spacing and size is a CSS custom property declared on the toolbar root,
* so a theme can restyle the toolbar by overriding variables instead of rewriting rules.
*
* You may not change or alter any portion of this comment or credits of supporting developers
* from this source code or any supporting source code which is considered copyrighted (c)
* material of the original comment or credit authors. This program is distributed in the hope
* that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*
* @copyright 2000-2026 XOOPS Project (https://xoops.org)
* @license GNU GPL 2.0 or later (https://www.gnu.org/licenses/gpl-2.0.html)
*/

.xo-edtb-toolbar {
--xo-edtb-bg: #f8f9fa;
--xo-edtb-border: #ced4da;
--xo-edtb-radius: 4px;
--xo-edtb-gap: 4px;
--xo-edtb-padding: 4px;
--xo-edtb-font-size: 0.8125rem;

--xo-edtb-btn-size: 1.75rem;
--xo-edtb-btn-bg: #ffffff;
--xo-edtb-btn-fg: #212529;
--xo-edtb-btn-border: #ced4da;
--xo-edtb-btn-hover-bg: #e9ecef;
--xo-edtb-btn-active-bg: #dee2e6;

--xo-edtb-menu-bg: #ffffff;
--xo-edtb-menu-fg: #212529;
--xo-edtb-menu-border: #ced4da;
--xo-edtb-menu-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
Comment on lines +19 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Declare the custom properties outside the toolbar wrapper too.

Every variable is declared on .xo-edtb-toolbar. The buttons resolve them by inheritance, which works for render(). It does not work for the compatibility path this PR keeps alive: renderFormDhtmlTAXoopsCode() and renderFormDhtmlTATypography() return .xo-edtb-group rows with no .xo-edtb-toolbar ancestor. A subclass that calls those helpers directly gets unstyled buttons, because min-width, height, background and border all resolve against undefined variables.

Declare the defaults on :root (or on both .xo-edtb-toolbar and .xo-edtb-group) and keep the toolbar block for overrides.

♻️ Proposed fix
-.xo-edtb-toolbar {
+.xo-edtb-toolbar,
+.xo-edtb-group {
     --xo-edtb-bg: `#f8f9fa`;
     --xo-edtb-border: `#ced4da`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.xo-edtb-toolbar {
--xo-edtb-bg: #f8f9fa;
--xo-edtb-border: #ced4da;
--xo-edtb-radius: 4px;
--xo-edtb-gap: 4px;
--xo-edtb-padding: 4px;
--xo-edtb-font-size: 0.8125rem;
--xo-edtb-btn-size: 1.75rem;
--xo-edtb-btn-bg: #ffffff;
--xo-edtb-btn-fg: #212529;
--xo-edtb-btn-border: #ced4da;
--xo-edtb-btn-hover-bg: #e9ecef;
--xo-edtb-btn-active-bg: #dee2e6;
--xo-edtb-menu-bg: #ffffff;
--xo-edtb-menu-fg: #212529;
--xo-edtb-menu-border: #ced4da;
--xo-edtb-menu-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
.xo-edtb-toolbar,
.xo-edtb-group {
--xo-edtb-bg: `#f8f9fa`;
--xo-edtb-border: `#ced4da`;
--xo-edtb-radius: 4px;
--xo-edtb-gap: 4px;
--xo-edtb-padding: 4px;
--xo-edtb-font-size: 0.8125rem;
--xo-edtb-btn-size: 1.75rem;
--xo-edtb-btn-bg: `#ffffff`;
--xo-edtb-btn-fg: `#212529`;
--xo-edtb-btn-border: `#ced4da`;
--xo-edtb-btn-hover-bg: `#e9ecef`;
--xo-edtb-btn-active-bg: `#dee2e6`;
--xo-edtb-menu-bg: `#ffffff`;
--xo-edtb-menu-fg: `#212529`;
--xo-edtb-menu-border: `#ced4da`;
--xo-edtb-menu-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.css` around lines 19 -
37, Move the default custom-property declarations from only `.xo-edtb-toolbar`
to `:root`, while retaining the `.xo-edtb-toolbar` block for toolbar-specific
overrides. Ensure standalone `.xo-edtb-group` rows returned by
`renderFormDhtmlTAXoopsCode()` and `renderFormDhtmlTATypography()` resolve the
button sizing, colors, and border variables without a toolbar ancestor.


display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--xo-edtb-gap);
padding: var(--xo-edtb-padding);
background: var(--xo-edtb-bg);
border: 1px solid var(--xo-edtb-border);
border-radius: var(--xo-edtb-radius);
font-size: var(--xo-edtb-font-size);
line-height: 1;
}

.xo-edtb-toolbar,
.xo-edtb-toolbar * {
box-sizing: border-box;
}

.xo-edtb-group {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--xo-edtb-gap);
}

.xo-edtb-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
min-width: var(--xo-edtb-btn-size);
height: var(--xo-edtb-btn-size);
padding: 0 0.4rem;
background: var(--xo-edtb-btn-bg);
color: var(--xo-edtb-btn-fg);
border: 1px solid var(--xo-edtb-btn-border);
border-radius: var(--xo-edtb-radius);
font-size: inherit;
line-height: 1;
cursor: pointer;
user-select: none;
}

.xo-edtb-btn:hover {
background: var(--xo-edtb-btn-hover-bg);
}

.xo-edtb-btn:active {
background: var(--xo-edtb-btn-active-bg);
}

.xo-edtb-btn-sm {
padding: 0 0.3rem;
}
Comment on lines +81 to +91

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a visible focus indicator for the buttons and menu items.

The file documents that the admin themes ship a universal reset. Those resets commonly drop outline, and this stylesheet defines :hover and :active but no focus state. Keyboard users then cannot see which toolbar control has focus.

Add an explicit :focus-visible rule for .xo-edtb-btn and .xo-edtb-menu-item.

♿ Proposed fix
 .xo-edtb-btn:active {
     background: var(--xo-edtb-btn-active-bg);
 }
+
+.xo-edtb-btn:focus-visible,
+.xo-edtb-menu-item:focus-visible {
+    outline: 2px solid currentColor;
+    outline-offset: 1px;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.xo-edtb-btn:hover {
background: var(--xo-edtb-btn-hover-bg);
}
.xo-edtb-btn:active {
background: var(--xo-edtb-btn-active-bg);
}
.xo-edtb-btn-sm {
padding: 0 0.3rem;
}
.xo-edtb-btn:hover {
background: var(--xo-edtb-btn-hover-bg);
}
.xo-edtb-btn:active {
background: var(--xo-edtb-btn-active-bg);
}
.xo-edtb-btn:focus-visible,
.xo-edtb-menu-item:focus-visible {
outline: 2px solid currentColor;
outline-offset: 1px;
}
.xo-edtb-btn-sm {
padding: 0 0.3rem;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.css` around lines 81 -
91, Add explicit :focus-visible styles for both .xo-edtb-btn and
.xo-edtb-menu-item, providing a clear visible focus indicator that remains
effective despite the admin theme reset. Keep the existing hover, active, and
sizing rules unchanged.


/* Dropdowns are native <details>/<summary> — no JS required for the open/close interaction. */
.xo-edtb-dropdown {
position: relative;
}

.xo-edtb-dropdown > summary.xo-edtb-btn {
list-style: none;
}

.xo-edtb-dropdown > summary::-webkit-details-marker {
display: none;
}

.xo-edtb-dropdown > summary::marker {
content: '';
}

.xo-edtb-menu {
position: absolute;
z-index: 1000;
top: 100%;
left: 0;
margin: 2px 0 0;
padding: 4px 0;
min-width: 9rem;
max-height: 16rem;
overflow-y: auto;
list-style: none;
background: var(--xo-edtb-menu-bg);
color: var(--xo-edtb-menu-fg);
border: 1px solid var(--xo-edtb-menu-border);
border-radius: var(--xo-edtb-radius);
box-shadow: var(--xo-edtb-menu-shadow);
}

/*
* Restore the resize grip on the editor's textarea.
*
* The "default" and "transition" ADMIN themes ship a universal reset —
* `* { … resize:none; }` (modules/system/themes/{default,transition}/css/reset.css:13) — which
* strips the grip from every element on the page, so the control panel's editor could not be
* resized while the front end's could. The textarea is rendered as a sibling immediately after
* the toolbar, so this sibling selector reaches it without needing a class on the element (which
* would mean touching all five renderers). Specificity (0,1,1) comfortably beats `*` (0,0,0).
*/
.xo-edtb-toolbar ~ textarea {
resize: vertical;
}

/*
* Reset the list markers on the ITEMS, not just the list. The admin "default" theme ships a
* bare `li { list-style: square inside; }` (modules/system/themes/default/css/style.css:59-62),
* which applies directly to the <li> and therefore beats a `list-style: none` set only on the
* parent <ul>. That is why square bullets appeared in the control panel but not on the front
* end. `.xo-edtb-menu > li` (0,1,1) outranks a bare `li` (0,0,1) without needing !important.
*/
.xo-edtb-menu > li {
list-style: none;
margin: 0;
padding: 0;
}

.xo-edtb-menu-item {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.25rem 0.6rem;
color: inherit;
text-decoration: none;
white-space: nowrap;
}

.xo-edtb-menu-item:hover {
background: var(--xo-edtb-btn-hover-bg);
}

.xo-edtb-swatch {
display: inline-block;
width: 0.9rem;
height: 0.9rem;
border: 1px solid var(--xo-edtb-menu-border);
border-radius: 2px;
}

@media (prefers-color-scheme: dark) {
.xo-edtb-toolbar {
--xo-edtb-bg: #2b2f33;
--xo-edtb-border: #495057;

--xo-edtb-btn-bg: #343a40;
--xo-edtb-btn-fg: #e9ecef;
--xo-edtb-btn-border: #495057;
--xo-edtb-btn-hover-bg: #495057;
--xo-edtb-btn-active-bg: #5a6268;

--xo-edtb-menu-bg: #343a40;
--xo-edtb-menu-fg: #e9ecef;
--xo-edtb-menu-border: #495057;
--xo-edtb-menu-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
}
71 changes: 71 additions & 0 deletions htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Shared DHTML editor toolbar behaviour.
*
* The dropdowns are native <details>/<summary>, and each toolbar's set shares a name= so modern
* browsers make them mutually exclusive on their own. This file supplies the rest:
*
* 1. the same exclusivity for browsers that predate the name= "exclusive accordion" attribute
* (name= on <details> is only supported in newer engines);
* 2. closing the open menu when the user clicks anywhere outside it;
* 3. closing it on Escape, and returning focus to the toggle.
*
* Everything is delegated from document, so toolbars added to the page later (AJAX forms, a
* second editor) are handled without re-initialising anything.
*/
(function () {
'use strict';

if (window.xoopsEditorToolbarInit) {
return;
}
window.xoopsEditorToolbarInit = true;

var DROPDOWN = 'details.xo-edtb-dropdown';

/**
* Close every open dropdown except the one passed in.
*
* @param {Element|null} keepOpen dropdown to leave alone
* @param {Element|null} scope toolbar to limit the sweep to, or null for the document
*/
function closeOthers(keepOpen, scope) {
var root = scope || document;
var open = root.querySelectorAll(DROPDOWN + '[open]');
for (var i = 0; i < open.length; i++) {
if (open[i] !== keepOpen) {
open[i].removeAttribute('open');
}
}
}

// 1 + 2: opening one closes the others. 'toggle' fires on open AND close, so only act on open.
document.addEventListener('toggle', function (event) {
var details = event.target;
if (!details || !details.matches || !details.matches(DROPDOWN) || !details.hasAttribute('open')) {
return;
}
// Limit to the owning toolbar so a second editor on the page is unaffected.
closeOthers(details, details.closest('.xo-edtb-toolbar'));
}, true); // capture: 'toggle' does not bubble

// 2: click anywhere outside an open dropdown closes it.
document.addEventListener('click', function (event) {
var inside = event.target.closest ? event.target.closest(DROPDOWN) : null;
closeOthers(inside, null);
});

// 3: Escape closes the open dropdown and puts focus back on its toggle.
document.addEventListener('keydown', function (event) {
if (event.key !== 'Escape' && event.keyCode !== 27) {
return;
}
var open = document.querySelectorAll(DROPDOWN + '[open]');
for (var i = 0; i < open.length; i++) {
var summary = open[i].querySelector('summary');
open[i].removeAttribute('open');
if (summary && typeof summary.focus === 'function') {
summary.focus();
}
}
});
Comment on lines +58 to +70

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Escape moves focus even when the user is typing elsewhere.

The handler closes every open dropdown and calls summary.focus() for each one. Two consequences:

  1. If a dropdown stays open while the user types in the textarea, Escape pulls focus out of the textarea and onto a toolbar toggle.
  2. If two dropdowns are open, focus lands on the last one in document order, which is arbitrary.

Restrict the focus restore to the dropdown that currently contains focus.

🐛 Proposed fix
         var open = document.querySelectorAll(DROPDOWN + '[open]');
+        var active = document.activeElement;
         for (var i = 0; i < open.length; i++) {
             var summary = open[i].querySelector('summary');
+            var hadFocus = !!(active && open[i].contains(active));
             open[i].removeAttribute('open');
-            if (summary && typeof summary.focus === 'function') {
+            if (hadFocus && summary && typeof summary.focus === 'function') {
                 summary.focus();
             }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
document.addEventListener('keydown', function (event) {
if (event.key !== 'Escape' && event.keyCode !== 27) {
return;
}
var open = document.querySelectorAll(DROPDOWN + '[open]');
for (var i = 0; i < open.length; i++) {
var summary = open[i].querySelector('summary');
open[i].removeAttribute('open');
if (summary && typeof summary.focus === 'function') {
summary.focus();
}
}
});
document.addEventListener('keydown', function (event) {
if (event.key !== 'Escape' && event.keyCode !== 27) {
return;
}
var open = document.querySelectorAll(DROPDOWN + '[open]');
var active = document.activeElement;
for (var i = 0; i < open.length; i++) {
var summary = open[i].querySelector('summary');
var hadFocus = !!(active && open[i].contains(active));
open[i].removeAttribute('open');
if (hadFocus && summary && typeof summary.focus === 'function') {
summary.focus();
}
}
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.js` around lines 58 -
70, Update the Escape key handler in the document keydown listener to restore
focus only for the open dropdown that currently contains the active element.
Continue closing all open dropdowns, but avoid moving focus when typing outside
a dropdown and ensure only the focused dropdown’s summary receives focus.

})();
Loading