Skip to content

Feat/shared dhtml toolbar - #151

Open
mambax7 wants to merge 2 commits into
XOOPS:masterfrom
mambax7:feat/shared-dhtml-toolbar
Open

Feat/shared dhtml toolbar#151
mambax7 wants to merge 2 commits into
XOOPS:masterfrom
mambax7:feat/shared-dhtml-toolbar

Conversation

@mambax7

@mambax7 mambax7 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary by Sourcery

Introduce a shared, framework-neutral DHTML editor toolbar for XoopsFormDhtmlTextArea and update all form renderers to use it, while hardening related JavaScript helpers and test infrastructure.

New Features:

  • Add XoopsDhtmlToolbar as a single source of truth for DHTML editor toolbar markup across Legacy, Bootstrap3/4/5, and Tailwind form renderers.
  • Provide dedicated toolbar CSS and JS assets to render and manage dropdowns in a framework-neutral way.
  • Extend test bootstrap with YouTube-related language constants needed by TextSanitizer extensions.

Enhancements:

  • Refactor all DHTML textarea form renderers to delegate toolbar construction to the shared XoopsDhtmlToolbar while keeping protected helper methods for backward compatibility.
  • Improve JavaScript helpers for the DHTML editor (xoopsCodeText, xoopsSetElementAttribute, xoopsMakeStyle) to avoid null dereferences, remove eval-based dispatch, and ensure useful behavior even without text selection.
  • Add unit tests validating XoopsDhtmlToolbar behavior, renderer delegation, TextSanitizer extension integration, and stylesheet injection.
  • Ensure LocaleRegistry test runs are isolated by purging its cache snapshot at test bootstrap so results are consistent across machines.

Tests:

  • Add XoopsDhtmlToolbarTest to verify identical toolbar output across all form renderers, preload event behavior, TextSanitizer integration, global overrides, check-length handling, and stylesheet injection.
  • Update tests bootstrap to define missing editor-related constants and clear LocaleRegistry cache for deterministic locale handling.

Summary by Gitar

  • Editor Toolbar:
    • Extracted a shared XoopsDhtmlToolbar class to unify editor markup across all renderers
    • Added framework-neutral CSS styling and JavaScript behavior for native <details> dropdowns
    • Repaired no-selection actions and modernised legacy JavaScript helpers in formdhtmltextarea.js
  • Testing:
    • Added comprehensive unit tests in XoopsDhtmlToolbarTest.php to verify renderer output parity and event contracts

This will update automatically on new commits.

Summary by CodeRabbit

  • New Features

    • Added a consistent DHTML editor toolbar across all supported display styles.
    • Added code formatting, typography, alignment, color, extension-button, and length-check controls.
    • Added responsive styling with theme customization, resizable text areas, and dark-mode support.
    • Added improved dropdown behavior, keyboard navigation, focus restoration, and support for dynamically added editors.
  • Bug Fixes

    • Improved editor handling when expected elements or previews are unavailable.
    • Prevented duplicate toolbar styling and improved safe handling of editor actions.

mambax7 added 2 commits August 1, 2026 04:32
Clicking Size, Font, Colour or B/I/U/S with no text selected did nothing at
all. The no-selection branch called setVisible() on an element with the id
held in $_hiddenText (default xoopsHiddenText), but no renderer has ever
emitted such an element, so xoopsGetElementById() returned null and the
handler threw a TypeError before it could act.

For xoopsSetElementAttribute() that branch was dead twice over: besides the
missing element, it eval'd a setElementSize/Font/Color() helper and no
setElement* function exists in the file. It is removed, so with no selection
the tag pair is simply inserted at the caret, which is what clicking the
button asks for. xoopsMakeStyle() keeps its branch for any custom renderer
that does emit the element, but only takes it when the element and the
handler both genuinely exist, and otherwise falls through to inserting the
pair.

Both eval() calls are gone with it. The remaining dispatch is an explicit map
of the four style helpers, which is all the callers ever pass.

xoopsGetSelect() returns null on browsers without a selection API, so the
selection is normalised to an empty string before use: without that, removing
the old length guard would have inserted the literal text "null" between the
tags.

xoopsCodeText() is a leftover from an older toolbar layout that had a
separate "add text" box. Nothing in core or any bundled extension calls it and
no renderer emits the elements it reads, so every path through it dereferenced
null. It is kept, because it is a global a third-party module could still
call, but now returns harmlessly instead of throwing.
The same editor showed a different toolbar in the control panel than on the
front end, and different again between front-end themes. The cause was not
CSS: renderFormDhtmlTAXoopsCode() and renderFormDhtmlTATypography() were
independently hand-written HTML in five renderers, and no admin theme selects
a renderer, so the control panel silently fell back to the Legacy one whose
Bootstrap class names no admin theme defines.

XoopsDhtmlToolbar now produces the toolbar once and all five renderers
delegate to it. Only renderFormDhtmlTextArea() is on XoopsFormRendererInterface;
the other two are protected helpers, so they are kept as thin delegates for any
third-party subclass that calls or overrides them. Each renderer still supplies
its own chrome -- the textarea, the preview fieldset and the script loader.

The markup is framework-neutral with its own xo-edtb-* classes and a
self-contained stylesheet, because the admin themes load no CSS framework at
all; every colour, radius and size is a custom property so a theme can restyle
it without touching markup. Dropdowns are native <details>, grouped per
textarea with name= so opening one closes the others, with a small script for
older engines plus click-outside and Escape handling. The typography row is
emitted server-side rather than written by document.write, and the colour list
is a curated palette in a class constant instead of 216 entries generated by a
nested loop. role="toolbar" and role="group" are now present, which only the
Bootstrap 5 renderer had.

Four contracts are preserved deliberately. The codeicon preload event still
fires with $code by reference at the same point, so modules can keep appending
buttons. TextSanitizer extensions keep their [$html, $js] return shape, and the
three divergent rewrites of their hardcoded btn-default classes become one.
The global JS function names are untouched, since extensions call them by name.
XoopsFormDhtmlTextArea's public surface is unchanged.

The stylesheet also restores resize:vertical on the editor's textarea: the
default and transition admin themes ship a universal `* { resize:none }` reset
which stripped the grip, so the control panel's editor could not be resized
while the front end's could.

A test asserts the substantive property: all five renderers emit an identical
toolbar for the same element.
Copilot AI review requested due to automatic review settings August 1, 2026 09:16
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0b92e038-a0f0-4602-a571-e6f6a7db83e1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Introduce a shared, framework-neutral XoopsDhtmlToolbar for the DHTML textarea editor and refactor all form renderers and JS helpers to use it consistently, with supporting CSS/JS assets and unit tests, plus a small test bootstrap fix for locale registry and constants.

Sequence diagram for rendering DHTML textarea with shared toolbar

sequenceDiagram
    participant Controller
    participant Renderer as XoopsFormRenderer* 
    participant Toolbar as XoopsDhtmlToolbar
    participant Element as XoopsFormDhtmlTextArea

    Controller->>Renderer: renderFormDhtmlTextArea(Element)
    Renderer->>Toolbar: new XoopsDhtmlToolbar()
    Renderer->>Toolbar: render(Element)
    activate Toolbar
    Toolbar->>Toolbar: injectStylesheet()
    Toolbar->>Toolbar: renderCodeButtons(Element)
    Toolbar->>Toolbar: renderTypography(Element)
    Toolbar->>Toolbar: renderCheckLength(Element)
    Toolbar-->>Renderer: toolbarHtml
    deactivate Toolbar

    Renderer->>Renderer: build <textarea> markup
    Renderer-->>Controller: toolbarHtml + textareaHtml
Loading

File-Level Changes

Change Details Files
Refactor all form renderers to delegate toolbar rendering to the new shared XoopsDhtmlToolbar instead of duplicating markup per framework.
  • Require XoopsDhtmlToolbar in Legacy, Bootstrap3/4/5, and Tailwind renderer classes.
  • Replace inline xoopscode, typography, and length-check HTML with a single toolbar->render($element) call in renderFormDhtmlTextArea().
  • Convert existing protected renderFormDhtmlTAXoopsCode()/renderFormDhtmlTATypography() methods into thin delegates that call XoopsDhtmlToolbar->renderCodeButtons()/renderTypography()/renderCheckLength().
  • Preserve protected methods so third-party subclasses can still override or call them.
htdocs/class/xoopsform/renderer/XoopsFormRendererLegacy.php
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap3.php
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap4.php
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap5.php
htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php
Add the shared, framework-neutral XoopsDhtmlToolbar implementation with support for extensions, preload codeicon events, and stylesheet/script injection.
  • Implement XoopsDhtmlToolbar with render(), renderCodeButtons(), renderTypography(), and renderCheckLength() APIs.
  • Centralize TextSanitizer extension button class rewriting to neutral xo-edtb-* classes and append extension JS to the element.
  • Emit codeicon preload event with the code row passed by reference to preserve third-party button injection contract.
  • Implement framework-neutral dropdowns via
    / and toolbar.css/toolbar.js assets, including mutual exclusivity and accessibility behaviours.
  • Inject toolbar.css and toolbar.js once per request via xoTheme->addStylesheet/addScript or inline /<script> when no theme is present.
htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php
htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.css
htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.js
Harden legacy DHTML editor JS helpers and remove dead eval/document.write paths while preserving external API surface.
  • Update xoopsCodeText() to guard against missing DOM elements and return early instead of throwing.
  • Update xoopsSetElementAttribute() to handle null selections and always insert a tag pair, replacing dead setVisible/setElement* eval path.
  • Introduce xoopsStylePreviewFuncs dispatch table and update xoopsMakeStyle() to avoid eval, guard missing elements, and insert tags when no preview element exists.
  • Ensure xoopsMakeStyle() and xoopsSetElementAttribute() treat null selection as empty string to avoid TypeErrors.
htdocs/include/formdhtmltextarea.js
Improve test bootstrap to define missing form constants and stabilise locale registry cache across test runs.
  • Define _XOOPS_FORM_ALT_ENTERHEIGHT and _XOOPS_FORM_ALTYOUTUBE constants used by youtube extension tests.
  • Add purgeCache() call on Xmf\I18n\Xoops\Registry\LocaleRegistry to clear locale snapshot under XOOPS_VAR_PATH before tests run, avoiding cross-contamination from live sites.
tests/bootstrap.php
Add unit tests to validate that all renderers use the shared toolbar and that toolbar behaviour and contracts are correct.
  • Create XoopsDhtmlToolbarTest covering identical toolbar output across Legacy, Bootstrap3/4/5, and Tailwind renderers.
  • Test delegation to XoopsDhtmlToolbar, codeicon preload event firing and by-reference modification, and TextSanitizer extension button class rewriting and JS injection.
  • Validate absence of document.write/eval in toolbar output, honouring of global formtextdhtml_sizes/fonts overrides, and correctness of check-length button maxlength handling.
  • Test stylesheet injection guard so multiple editors only emit toolbar.css/toolbar.js once.
tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest.php

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@gitar-bot

gitar-bot Bot commented Aug 1, 2026

Copy link
Copy Markdown

Important

You are using the Gitar free plan. Upgrade to unlock code review, CI analysis, auto-apply, custom automations, and more.

Gitar

@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.00000% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 19.56%. Comparing base (d2b5bde) to head (4ce640f).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
...xoopsform/renderer/XoopsFormRendererBootstrap3.php 60.00% 2 Missing ⚠️
...xoopsform/renderer/XoopsFormRendererBootstrap4.php 60.00% 2 Missing ⚠️
...xoopsform/renderer/XoopsFormRendererBootstrap5.php 60.00% 2 Missing ⚠️
...ass/xoopsform/renderer/XoopsFormRendererLegacy.php 60.00% 2 Missing ⚠️
...s/xoopsform/renderer/XoopsFormRendererTailwind.php 60.00% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master     #151      +/-   ##
============================================
+ Coverage     19.29%   19.56%   +0.27%     
+ Complexity     8227     8190      -37     
============================================
  Files           672      672              
  Lines         44266    43831     -435     
============================================
+ Hits           8539     8576      +37     
+ Misses        35727    35255     -472     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In XoopsDhtmlToolbar::renderCodeButtons() the imagemanager and smilies URLs now use $element->getName() directly without rawurlencode(), whereas the Tailwind renderer previously encoded the target ID; consider restoring URL encoding to avoid issues when textarea IDs contain characters that are not URL-safe.
  • Each renderer currently creates a new XoopsDhtmlToolbar instance on every renderFormDhtmlTextArea() call; if these toolbars become heavier over time, you may want to share a single instance (e.g. via a static accessor) to avoid repeated construction and keep renderer wiring lean.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In XoopsDhtmlToolbar::renderCodeButtons() the imagemanager and smilies URLs now use $element->getName() directly without rawurlencode(), whereas the Tailwind renderer previously encoded the target ID; consider restoring URL encoding to avoid issues when textarea IDs contain characters that are not URL-safe.
- Each renderer currently creates a new XoopsDhtmlToolbar instance on every renderFormDhtmlTextArea() call; if these toolbars become heavier over time, you may want to share a single instance (e.g. via a static accessor) to avoid repeated construction and keep renderer wiring lean.

## Individual Comments

### Comment 1
<location path="htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php" line_range="153-154" />
<code_context>
+        $textareaId = $element->getName();
+        $btn        = self::BTN_SM;
+
+        $code  = '<a name="moresmiley"></a>';
+        $code .= '<div class="' . self::GROUP_CLASS . '" role="group" aria-label="' . _XOOPS_FORM_ALT_IMG . '">';
+        $code .= $this->button($btn, $this->jsCall('xoopsCodeUrl', [$textareaId, _ENTERURL, _ENTERWEBTITLE]), _XOOPS_FORM_ALT_URL, 'fa-solid fa-link');
+        $code .= $this->button($btn, $this->jsCall('xoopsCodeEmail', [$textareaId, _ENTEREMAIL, _ENTERWEBTITLE]), _XOOPS_FORM_ALT_EMAIL, 'fa-solid fa-envelope');
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The aria-label for the code button group is tied to the image alt text, which is misleading for screen readers.

The group’s aria-label `_XOOPS_FORM_ALT_IMG` doesn’t match its contents (URL, email, image, smilies, extension, code, quote), so screen readers treat the whole group as image-related. Please use a more generic label (e.g. a new constant like `_XOOPS_FORM_ARIA_CODEBAR`) or remove the group label and rely on the individual button labels instead.

Suggested implementation:

```
        $code  = '<a name="moresmiley"></a>';
        $code .= '<div class="' . self::GROUP_CLASS . '" role="group">';

```

If you prefer a descriptive group label instead of relying solely on individual button labels, you can:
1. Define a new language constant (e.g. `_XOOPS_FORM_ARIA_CODEBAR`) in the appropriate language file.
2. Replace the edited line with:  
   `$code .= '<div class="' . self::GROUP_CLASS . '" role="group" aria-label="' . _XOOPS_FORM_ARIA_CODEBAR . '">';`
This keeps the toolbar accessible while making the label semantically accurate.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +153 to +154
$code = '<a name="moresmiley"></a>';
$code .= '<div class="' . self::GROUP_CLASS . '" role="group" aria-label="' . _XOOPS_FORM_ALT_IMG . '">';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): The aria-label for the code button group is tied to the image alt text, which is misleading for screen readers.

The group’s aria-label _XOOPS_FORM_ALT_IMG doesn’t match its contents (URL, email, image, smilies, extension, code, quote), so screen readers treat the whole group as image-related. Please use a more generic label (e.g. a new constant like _XOOPS_FORM_ARIA_CODEBAR) or remove the group label and rely on the individual button labels instead.

Suggested implementation:

        $code  = '<a name="moresmiley"></a>';
        $code .= '<div class="' . self::GROUP_CLASS . '" role="group">';

If you prefer a descriptive group label instead of relying solely on individual button labels, you can:

  1. Define a new language constant (e.g. _XOOPS_FORM_ARIA_CODEBAR) in the appropriate language file.
  2. Replace the edited line with:
    $code .= '<div class="' . self::GROUP_CLASS . '" role="group" aria-label="' . _XOOPS_FORM_ARIA_CODEBAR . '">';
    This keeps the toolbar accessible while making the label semantically accurate.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a shared, framework-neutral DHTML editor toolbar (XoopsDhtmlToolbar) and updates all XOOPS form renderers to delegate toolbar markup generation to it, while also modernizing legacy JavaScript helpers and expanding unit test coverage around toolbar behavior and extension integration.

Changes:

  • Add XoopsDhtmlToolbar as the single source of truth for DHTML toolbar markup (buttons, typography dropdowns, check-length) and shared assets (CSS/JS).
  • Refactor Legacy/Bootstrap3/4/5/Tailwind renderers to delegate toolbar construction to the shared class (keeping protected helpers for compatibility).
  • Improve formdhtmltextarea.js helpers to avoid null dereferences and remove eval(), plus add tests and test bootstrap constants/cache cleanup.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest.php Adds unit coverage validating renderer parity, preload event behavior, extension integration, and stylesheet injection.
tests/bootstrap.php Defines missing editor-related language constants for tests and purges LocaleRegistry cache for deterministic runs.
htdocs/include/formdhtmltextarea.js Hardens legacy editor JS helpers (null-safety, removes eval-based dispatch, improves no-selection behavior).
htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php Delegates DHTML textarea toolbar rendering to the shared toolbar.
htdocs/class/xoopsform/renderer/XoopsFormRendererLegacy.php Delegates DHTML textarea toolbar rendering to the shared toolbar.
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap5.php Delegates DHTML textarea toolbar rendering to the shared toolbar.
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap4.php Delegates DHTML textarea toolbar rendering to the shared toolbar.
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap3.php Delegates DHTML textarea toolbar rendering to the shared toolbar.
htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php New shared toolbar implementation (markup generation, extension button class rewrite, stylesheet/script injection).
htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.js Adds framework-neutral <details> dropdown behavior (mutual exclusivity, click-outside, Escape handling).
htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.css Adds framework-neutral toolbar styling and targeted CSS fixes for admin-theme resets.
Suppressed comments (2)

htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php:208

  • renderTypography() also uses $element->getName() when generating JS handlers. Since getName() HTML-escapes quotes, the string passed into xoopsSetElementAttribute() can diverge from the real textarea id. Use the raw name via getName(false).
    public function renderTypography(XoopsFormDhtmlTextArea $element): string
    {
        $textareaId = $element->getName();
        $hiddenText = (string) $element->_hiddenText;

htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php:253

  • renderCheckLength() passes $element->getName() into the JS call builder. As with the other toolbar rows, this can pass an HTML-escaped id into JavaScript and fail to find the textarea. Prefer the raw name (getName(false)) for JS arguments.
            $maxlength = (int) $element->configs['maxlength'];
        }

        $onclick = $this->jsCall('XoopsCheckLength', [$element->getName(), (string) $maxlength, _XOOPS_FORM_ALT_LENGTH, _XOOPS_FORM_ALT_LENGTH_MAX]);

Comment on lines +72 to +78
private const RENDERER_CLASSES = [
'XoopsFormRendererLegacy',
'XoopsFormRendererBootstrap3',
'XoopsFormRendererBootstrap4',
'XoopsFormRendererBootstrap5',
'XoopsFormRendererTailwind',
];
Comment on lines +357 to +369
protected function jsCall(string $fn, array $args): string
{
$parts = [];
foreach ($args as $arg) {
if (is_int($arg) || is_float($arg)) {
$parts[] = (string) $arg;
} else {
$parts[] = '"' . htmlspecialchars((string) $arg, ENT_QUOTES | ENT_HTML5, 'UTF-8') . '"';
}
}

return $fn . '(' . implode(', ', $parts) . ');';
}
Comment on lines +150 to +159
$textareaId = $element->getName();
$btn = self::BTN_SM;

$code = '<a name="moresmiley"></a>';
$code .= '<div class="' . self::GROUP_CLASS . '" role="group" aria-label="' . _XOOPS_FORM_ALT_IMG . '">';
$code .= $this->button($btn, $this->jsCall('xoopsCodeUrl', [$textareaId, _ENTERURL, _ENTERWEBTITLE]), _XOOPS_FORM_ALT_URL, 'fa-solid fa-link');
$code .= $this->button($btn, $this->jsCall('xoopsCodeEmail', [$textareaId, _ENTEREMAIL, _ENTERWEBTITLE]), _XOOPS_FORM_ALT_EMAIL, 'fa-solid fa-envelope');
$code .= $this->button($btn, $this->jsCall('xoopsCodeImg', [$textareaId, _ENTERIMGURL, _ENTERIMGPOS, _IMGPOSRORL, _ERRORIMGPOS, _XOOPS_FORM_ALT_ENTERWIDTH]), _XOOPS_FORM_ALT_IMG, 'fa-solid fa-file-image');
$code .= $this->button($btn, $this->jsCall('openWithSelfMain', [XOOPS_URL . '/imagemanager.php?target=' . $textareaId, 'imgmanager', 400, 430]), _XOOPS_FORM_ALT_IMAGE, 'fa-solid fa-file-image', '<span style="font-size:75%;"> Manager</span>');
$code .= $this->button($btn, $this->jsCall('openWithSelfMain', [XOOPS_URL . '/misc.php?action=showpopups&type=smilies&target=' . $textareaId, 'smilies', 300, 475]), _XOOPS_FORM_ALT_SMILEY, 'fa-solid fa-face-smile');

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php (1)

485-495: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unused Tailwind toolbar helper.

renderEditorButton(), BTN_NEUTRAL_SM, and DROPDOWN_MENU_CLS are not called inside XoopsFormRendererTailwind.php; the xoopscode and typography renderers delegate to XoopsDhtmlToolbar. Remove this dead protected helper plus the two private constants, or mark it deprecated if intentional for downstream subclasses.

🤖 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/xoopsform/renderer/XoopsFormRendererTailwind.php` around lines
485 - 495, Remove the unused renderEditorButton() helper from
XoopsFormRendererTailwind along with the private constants BTN_NEUTRAL_SM and
DROPDOWN_MENU_CLS. Do not alter the xoopscode or typography renderer delegation
through XoopsDhtmlToolbar.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.css`:
- Around line 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.
- Around line 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.

In `@htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.js`:
- Around line 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.

In `@htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php`:
- Around line 212-230: Escape every language-derived aria-label in
XoopsDhtmlToolbar, including the existing call sites near the toolbar rendering
logic and the three group labels shown here, using a small shared esc helper
that applies htmlspecialchars with ENT_QUOTES | ENT_HTML5 and UTF-8. Update only
these aria-label interpolations; keep button() and dropdown() handling
unchanged.
- Around line 209-215: In the toolbar-building method, validate and normalize
both global overrides before use: accept only arrays for formtextdhtml_sizes and
formtextdhtml_fonts, otherwise use safe defaults, and ensure an empty sizes
array does not render an unusable Size dropdown. Keep font handling compatible
with array_combine by passing only a valid non-empty font list, using the
existing default-font symbol where needed.

In `@htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap3.php`:
- Around line 309-313: Choose one consistent contract for
renderFormDhtmlTATypography() and apply it to all five renderers:
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap3.php#L309-L313,
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap4.php#L314-L318,
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap5.php#L315-L319,
htdocs/class/xoopsform/renderer/XoopsFormRendererLegacy.php#L224-L228, and
htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php#L524-L528. Either
remove renderCheckLength() and provide a separate renderFormDhtmlTACheckLength()
helper, or retain the combined output and update every method docblock to
document both controls; keep the Tailwind change aligned with the dead-helper
cleanup at Line 485.

In `@tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest.php`:
- Around line 290-299: Make
testStylesheetIsInjectedOnceAcrossMultipleEditorsOnOnePage deterministic by
explicitly controlling the $GLOBALS['xoTheme'] state, preferably clearing it in
setUp() or at the start of the test. Preserve the existing assertions for
first-render injection and second-render suppression.
- Around line 262-265: Update testCheckLengthButtonCarriesConfiguredMaxlength
and the XoopsFormDhtmlTextArea configuration path to avoid dynamically assigning
configs; declare a typed public array $configs initialized to [] on the
appropriate class, or reuse an existing configuration accessor, while preserving
the test’s configured maxlength value.

---

Outside diff comments:
In `@htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php`:
- Around line 485-495: Remove the unused renderEditorButton() helper from
XoopsFormRendererTailwind along with the private constants BTN_NEUTRAL_SM and
DROPDOWN_MENU_CLS. Do not alter the xoopscode or typography renderer delegation
through XoopsDhtmlToolbar.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: fb047ae1-dd84-44c6-bfb8-b2284a65f624

📥 Commits

Reviewing files that changed from the base of the PR and between ce1531b and 4ce640f.

📒 Files selected for processing (11)
  • htdocs/class/xoopseditor/dhtmltextarea/XoopsDhtmlToolbar.php
  • htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.css
  • htdocs/class/xoopseditor/dhtmltextarea/assets/toolbar.js
  • htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap3.php
  • htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap4.php
  • htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap5.php
  • htdocs/class/xoopsform/renderer/XoopsFormRendererLegacy.php
  • htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php
  • htdocs/include/formdhtmltextarea.js
  • tests/bootstrap.php
  • tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest.php

Comment on lines +19 to +37
.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);

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.

Comment on lines +81 to +91
.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;
}

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.

Comment on lines +58 to +70
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();
}
}
});

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.

Comment on lines +209 to +215
$sizes = $GLOBALS['formtextdhtml_sizes'] ?? [];
$fonts = !empty($GLOBALS['formtextdhtml_fonts']) ? $GLOBALS['formtextdhtml_fonts'] : self::DEFAULT_FONTS;

$ret = '<div class="' . self::GROUP_CLASS . '" role="group" aria-label="' . _SIZE . '/' . _FONT . '/' . _COLOR . '">';
$ret .= $this->dropdown($textareaId, $hiddenText, 'size', _SIZE, 'fa-solid fa-text-height', $sizes);
$ret .= $this->dropdown($textareaId, $hiddenText, 'font', _FONT, 'fa-solid fa-font', array_combine($fonts, $fonts));
$ret .= $this->dropdown($textareaId, $hiddenText, 'color', _COLOR, 'fa-solid fa-palette', array_flip(self::COLOR_PALETTE), true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard the global font and size overrides before you use them.

$GLOBALS['formtextdhtml_fonts'] is a documented override point. !empty() also passes for a non-empty string or object. array_combine($fonts, $fonts) on Line 214 then throws a TypeError and the whole form render dies. $GLOBALS['formtextdhtml_sizes'] has the same exposure: a non-array value flows straight into dropdown() and foreach emits a warning.

Also note that an empty $sizes renders a dropdown toggle with an empty <ul>, so the Size control appears but does nothing.

🛡️ Proposed fix to normalize both overrides
-        $sizes = $GLOBALS['formtextdhtml_sizes'] ?? [];
-        $fonts = !empty($GLOBALS['formtextdhtml_fonts']) ? $GLOBALS['formtextdhtml_fonts'] : self::DEFAULT_FONTS;
+        $sizes = isset($GLOBALS['formtextdhtml_sizes']) && is_array($GLOBALS['formtextdhtml_sizes'])
+            ? $GLOBALS['formtextdhtml_sizes']
+            : [];
+        $fonts = !empty($GLOBALS['formtextdhtml_fonts']) && is_array($GLOBALS['formtextdhtml_fonts'])
+            ? array_values(array_filter(array_map('strval', $GLOBALS['formtextdhtml_fonts']), 'strlen'))
+            : self::DEFAULT_FONTS;
+        if ([] === $fonts) {
+            $fonts = self::DEFAULT_FONTS;
+        }
📝 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
$sizes = $GLOBALS['formtextdhtml_sizes'] ?? [];
$fonts = !empty($GLOBALS['formtextdhtml_fonts']) ? $GLOBALS['formtextdhtml_fonts'] : self::DEFAULT_FONTS;
$ret = '<div class="' . self::GROUP_CLASS . '" role="group" aria-label="' . _SIZE . '/' . _FONT . '/' . _COLOR . '">';
$ret .= $this->dropdown($textareaId, $hiddenText, 'size', _SIZE, 'fa-solid fa-text-height', $sizes);
$ret .= $this->dropdown($textareaId, $hiddenText, 'font', _FONT, 'fa-solid fa-font', array_combine($fonts, $fonts));
$ret .= $this->dropdown($textareaId, $hiddenText, 'color', _COLOR, 'fa-solid fa-palette', array_flip(self::COLOR_PALETTE), true);
$sizes = isset($GLOBALS['formtextdhtml_sizes']) && is_array($GLOBALS['formtextdhtml_sizes'])
? $GLOBALS['formtextdhtml_sizes']
: [];
$fonts = !empty($GLOBALS['formtextdhtml_fonts']) && is_array($GLOBALS['formtextdhtml_fonts'])
? array_values(array_filter(array_map('strval', $GLOBALS['formtextdhtml_fonts']), 'strlen'))
: self::DEFAULT_FONTS;
if ([] === $fonts) {
$fonts = self::DEFAULT_FONTS;
}
$ret = '<div class="' . self::GROUP_CLASS . '" role="group" aria-label="' . _SIZE . '/' . _FONT . '/' . _COLOR . '">';
$ret .= $this->dropdown($textareaId, $hiddenText, 'size', _SIZE, 'fa-solid fa-text-height', $sizes);
$ret .= $this->dropdown($textareaId, $hiddenText, 'font', _FONT, 'fa-solid fa-font', array_combine($fonts, $fonts));
$ret .= $this->dropdown($textareaId, $hiddenText, 'color', _COLOR, 'fa-solid fa-palette', array_flip(self::COLOR_PALETTE), true);
🤖 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/XoopsDhtmlToolbar.php` around lines
209 - 215, In the toolbar-building method, validate and normalize both global
overrides before use: accept only arrays for formtextdhtml_sizes and
formtextdhtml_fonts, otherwise use safe defaults, and ensure an empty sizes
array does not render an unusable Size dropdown. Keep font handling compatible
with array_combine by passing only a valid non-empty font list, using the
existing default-font symbol where needed.

Comment on lines +212 to +230
$ret = '<div class="' . self::GROUP_CLASS . '" role="group" aria-label="' . _SIZE . '/' . _FONT . '/' . _COLOR . '">';
$ret .= $this->dropdown($textareaId, $hiddenText, 'size', _SIZE, 'fa-solid fa-text-height', $sizes);
$ret .= $this->dropdown($textareaId, $hiddenText, 'font', _FONT, 'fa-solid fa-font', array_combine($fonts, $fonts));
$ret .= $this->dropdown($textareaId, $hiddenText, 'color', _COLOR, 'fa-solid fa-palette', array_flip(self::COLOR_PALETTE), true);
$ret .= '</div>';

$btn = self::BTN_SM;
$ret .= '<div class="' . self::GROUP_CLASS . '" role="group" aria-label="' . _XOOPS_FORM_ALT_BOLD . '">';
$ret .= $this->button($btn, $this->jsCall('xoopsMakeBold', [$hiddenText, $textareaId]), _XOOPS_FORM_ALT_BOLD, 'fa-solid fa-bold');
$ret .= $this->button($btn, $this->jsCall('xoopsMakeItalic', [$hiddenText, $textareaId]), _XOOPS_FORM_ALT_ITALIC, 'fa-solid fa-italic');
$ret .= $this->button($btn, $this->jsCall('xoopsMakeUnderline', [$hiddenText, $textareaId]), _XOOPS_FORM_ALT_UNDERLINE, 'fa-solid fa-underline');
$ret .= $this->button($btn, $this->jsCall('xoopsMakeLineThrough', [$hiddenText, $textareaId]), _XOOPS_FORM_ALT_LINETHROUGH, 'fa-solid fa-strikethrough');
$ret .= '</div>';

$ret .= '<div class="' . self::GROUP_CLASS . '" role="group" aria-label="' . _XOOPS_FORM_ALT_LEFT . '/' . _XOOPS_FORM_ALT_CENTER . '/' . _XOOPS_FORM_ALT_RIGHT . '">';
$ret .= $this->button($btn, $this->jsCall('xoopsMakeLeft', [$hiddenText, $textareaId]), _XOOPS_FORM_ALT_LEFT, 'fa-solid fa-align-left');
$ret .= $this->button($btn, $this->jsCall('xoopsMakeCenter', [$hiddenText, $textareaId]), _XOOPS_FORM_ALT_CENTER, 'fa-solid fa-align-center');
$ret .= $this->button($btn, $this->jsCall('xoopsMakeRight', [$hiddenText, $textareaId]), _XOOPS_FORM_ALT_RIGHT, 'fa-solid fa-align-right');
$ret .= '</div>';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Escape the language constants that land inside aria-label.

The group labels come from language packs. They are interpolated raw into double-quoted attributes here and on Line 154 and Line 254. One translated string containing " breaks the attribute and injects markup. button() and dropdown() already escape their titles, so this is the only unescaped path left in the class.

Route these through the same htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8') call. A small private helper keeps the five call sites tidy.

As per coding guidelines: "Escape all output with htmlspecialchars($value, ENT_QUOTES, 'UTF-8') or use Smarty auto-escaping".

🔒️ Proposed fix
-        $ret  = '<div class="' . self::GROUP_CLASS . '" role="group" aria-label="' . _SIZE . '/' . _FONT . '/' . _COLOR . '">';
+        $ret  = '<div class="' . self::GROUP_CLASS . '" role="group" aria-label="'
+            . $this->esc(_SIZE . '/' . _FONT . '/' . _COLOR) . '">';

Add the helper and reuse it on Lines 154, 219, 226 and 254:

    /**
     * Escape a value for an HTML attribute.
     *
     * `@param` string $value raw value
     *
     * `@return` string escaped value
     */
    protected function esc(string $value): string
    {
        return htmlspecialchars($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    }
🤖 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/XoopsDhtmlToolbar.php` around lines
212 - 230, Escape every language-derived aria-label in XoopsDhtmlToolbar,
including the existing call sites near the toolbar rendering logic and the three
group labels shown here, using a small shared esc helper that applies
htmlspecialchars with ENT_QUOTES | ENT_HTML5 and UTF-8. Update only these
aria-label interpolations; keep button() and dropdown() handling unchanged.

Source: Coding guidelines

Comment on lines 309 to +313
protected function renderFormDhtmlTATypography(XoopsFormDhtmlTextArea $element)
{
$textarea_id = $element->getName();
$hiddentext = $element->_hiddenText;

$fontarray = !empty($GLOBALS['formtextdhtml_fonts']) ? $GLOBALS['formtextdhtml_fonts'] : [
'Arial',
'Courier',
'Georgia',
'Helvetica',
'Impact',
'Verdana',
'Haettenschweiler',
];

$colorArray = [
'Black' => '000000',
'Blue' => '38AAFF',
'Brown' => '987857',
'Green' => '79D271',
'Grey' => '888888',
'Orange' => 'FFA700',
'Paper' => 'E0E0E0',
'Purple' => '363E98',
'Red' => 'FF211E',
'White' => 'FEFEFE',
'Yellow' => 'FFD628',
];

$fontStr = '<div class="row"><div class="col-md-12"><div class="btn-group" role="toolbar">';
$fontStr .= '<div class="btn-group">'
. '<button type="button" class="btn btn-default btn-sm dropdown-toggle" title="' . _SIZE . '"'
. ' data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">'
. '<span class = "fa-solid fa-text-height"></span><span class="caret"></span></button>'
. '<ul class="dropdown-menu">';
//. _SIZE . '&nbsp;&nbsp;<span class="caret"></span></button><ul class="dropdown-menu">';
foreach ($GLOBALS['formtextdhtml_sizes'] as $value => $name) {
$fontStr .= '<li><a href="javascript:xoopsSetElementAttribute(\'size\', \'' . $value . '\', \''
. $textarea_id . '\', \'' . $hiddentext . '\');">' . $name . '</a></li>';
}
$fontStr .= '</ul></div>';

$fontStr .= '<div class="btn-group">'
. '<button type="button" class="btn btn-default btn-sm dropdown-toggle" title="' . _FONT . '"'
. ' data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">'
. '<span class = "fa-solid fa-font"></span><span class="caret"></span></button>'
. '<ul class="dropdown-menu">';
//. _FONT . '&nbsp;&nbsp;<span class="caret"></span></button><ul class="dropdown-menu">';
foreach ($fontarray as $font) {
$fontStr .= '<li><a href="javascript:xoopsSetElementAttribute(\'font\', \'' . $font . '\', \''
. $textarea_id . '\', \'' . $hiddentext . '\');">' . $font . '</a></li>';
}
$fontStr .= '</ul></div>';

$fontStr .= '<div class="btn-group">'
. '<button type="button" class="btn btn-default btn-sm dropdown-toggle" title="' . _COLOR . '"'
. ' data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">'
. '<span class = "fa-solid fa-palette"></span><span class="caret"></span></button>'
. '<ul class="dropdown-menu">';
//. _COLOR . '&nbsp;&nbsp;<span class="caret"></span></button><ul class="dropdown-menu">';
foreach ($colorArray as $color => $hex) {
$fontStr .= '<li><a href="javascript:xoopsSetElementAttribute(\'color\', \'' . $hex . '\', \''
. $textarea_id . '\', \'' . $hiddentext . '\');">'
. '<span style="color:#' . $hex . ';">' . $color . '</span></a></li>';
}
$fontStr .= '</ul></div>';
$fontStr .= '</div>';

//$styleStr = "<div class='row'><div class='col-md-12'>";
$styleStr = "<div class='btn-group' role='group'>";
$styleStr .= "<button type='button' class='btn btn-default btn-sm' onclick='xoopsMakeBold(\"{$hiddentext}\", \"{$textarea_id}\");' title='" . _XOOPS_FORM_ALT_BOLD . "' aria-label='Left Align'><span class='fa-solid fa-bold' aria-hidden='true'></span></button>";
$styleStr .= "<button type='button' class='btn btn-default btn-sm' onclick='xoopsMakeItalic(\"{$hiddentext}\", \"{$textarea_id}\");' title='" . _XOOPS_FORM_ALT_ITALIC . "' aria-label='Left Align'><span class='fa-solid fa-italic' aria-hidden='true'></span></button>";
$styleStr .= "<button type='button' class='btn btn-default btn-sm' onclick='xoopsMakeUnderline(\"{$hiddentext}\", \"{$textarea_id}\");' title='" . _XOOPS_FORM_ALT_UNDERLINE . "' aria-label='Left Align'>" . '<span class="fa-solid fa-underline"></span></button>';
$styleStr .= "<button type='button' class='btn btn-default btn-sm' onclick='xoopsMakeLineThrough(\"{$hiddentext}\", \"{$textarea_id}\");' title='" . _XOOPS_FORM_ALT_LINETHROUGH . "' aria-label='Left Align'>" . '<span class="fa-solid fa-strikethrough"></span></button>';
$styleStr .= "</div>";

$alignStr = "<div class='btn-group' role='group'>";
$alignStr .= "<button type='button' class='btn btn-default btn-sm' onclick='xoopsMakeLeft(\"{$hiddentext}\", \"{$textarea_id}\");' title='" . _XOOPS_FORM_ALT_LEFT . "' aria-label='Left Align'><span class='fa-solid fa-align-left' aria-hidden='true'></span></button>";
$alignStr .= "<button type='button' class='btn btn-default btn-sm' onclick='xoopsMakeCenter(\"{$hiddentext}\", \"{$textarea_id}\");' title='" . _XOOPS_FORM_ALT_CENTER . "' aria-label='Left Align'><span class='fa-solid fa-align-center' aria-hidden='true'></span></button>";
$alignStr .= "<button type='button' class='btn btn-default btn-sm' onclick='xoopsMakeRight(\"{$hiddentext}\", \"{$textarea_id}\");' title='" . _XOOPS_FORM_ALT_RIGHT . "' aria-label='Left Align'><span class='fa-solid fa-align-right' aria-hidden='true'></span></button>";
$alignStr .= "</div>";

$fontStr .= "&nbsp;{$styleStr}&nbsp;{$alignStr}&nbsp;\n";

$maxlength = $element->configs['maxlength'] ?? 0;
$fontStr .= "<button type='button' class='btn btn-default btn-sm' onclick=\"XoopsCheckLength('"
. $element->getName() . "', '" . $maxlength . "', '"
. _XOOPS_FORM_ALT_LENGTH . "', '" . _XOOPS_FORM_ALT_LENGTH_MAX . "');\" title='"
. _XOOPS_FORM_ALT_CHECKLENGTH . "'><span class='fa-solid fa-square-check' aria-hidden='true'></span></button>";
$fontStr .= "</div></div>";

return $fontStr;
$toolbar = new \XoopsDhtmlToolbar();

return $toolbar->renderTypography($element) . $toolbar->renderCheckLength($element);

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

renderFormDhtmlTATypography() silently absorbed the check-length button in all five renderers. The PR keeps these protected helpers precisely so third-party subclasses can still call them, but the shared root cause is a contract change: each helper now returns renderTypography() plus renderCheckLength() while its docblock still promises "rendered typography controls". A subclass that composed typography with its own length control now renders two length buttons. Pick one resolution and apply it consistently: either drop the renderCheckLength() call and expose a separate renderFormDhtmlTACheckLength() helper, or keep the combined output and state it in every docblock.

  • htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap3.php#L309-L313: decide the contract here first, then update the docblock or drop the renderCheckLength() call.
  • htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap4.php#L314-L318: apply the identical resolution.
  • htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap5.php#L315-L319: apply the identical resolution.
  • htdocs/class/xoopsform/renderer/XoopsFormRendererLegacy.php#L224-L228: apply the identical resolution.
  • htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php#L524-L528: apply the identical resolution, and keep it aligned with the dead-helper cleanup flagged at Line 485.
🧰 Tools
🪛 PHPMD (2.15.0)

[error] 311-311: Missing class import via use statement (line '311', column '24'). (undefined)

(MissingImport)

📍 Affects 5 files
  • htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap3.php#L309-L313 (this comment)
  • htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap4.php#L314-L318
  • htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap5.php#L315-L319
  • htdocs/class/xoopsform/renderer/XoopsFormRendererLegacy.php#L224-L228
  • htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php#L524-L528
🤖 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/xoopsform/renderer/XoopsFormRendererBootstrap3.php` around lines
309 - 313, Choose one consistent contract for renderFormDhtmlTATypography() and
apply it to all five renderers:
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap3.php#L309-L313,
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap4.php#L314-L318,
htdocs/class/xoopsform/renderer/XoopsFormRendererBootstrap5.php#L315-L319,
htdocs/class/xoopsform/renderer/XoopsFormRendererLegacy.php#L224-L228, and
htdocs/class/xoopsform/renderer/XoopsFormRendererTailwind.php#L524-L528. Either
remove renderCheckLength() and provide a separate renderFormDhtmlTACheckLength()
helper, or retain the combined output and update every method docblock to
document both controls; keep the Tailwind change aligned with the dead-helper
cleanup at Line 485.

Comment on lines +262 to +265
public function testCheckLengthButtonCarriesConfiguredMaxlength(): void
{
$element = $this->makeElement();
$element->configs = ['maxlength' => 12345];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether XoopsFormDhtmlTextArea declares configs or allows dynamic properties, and how PHPUnit treats deprecations.
set -euo pipefail

fd -t f 'formdhtmltextarea.php' htdocs/class | while IFS= read -r f; do
  echo "===== $f ====="
  rg -n -C3 'AllowDynamicProperties|\$configs|class XoopsFormDhtmlTextArea' "$f"
done

echo "===== phpunit config: deprecation handling ====="
fd -t f -g 'phpunit*.xml*' | while IFS= read -r c; do
  echo "--- $c ---"
  rg -n 'convertDeprecations|failOnDeprecation|failOnWarning|failOnNotice' "$c" || echo "no deprecation flags set"
done

Repository: XOOPS/XoopsCore27

Length of output: 700


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===== locate XoopsFormDhtmlTextArea and parent ====="
fd -t f 'formdhtmltextarea.php|formtextarea.php|xoopsformphp' htdocs/class/tests htdocs/class/xoopsform | sort

echo "===== formdhtmltextarea outline ====="
ast-grep outline htdocs/class/xoopsform/formdhtmltextarea.php --view expanded || true

echo "===== relevant formdhtmltextarea sources ====="
sed -n '1,220p' htdocs/class/xoopsform/formdhtmltextarea.php

echo "===== XoopsFormTextArea property declarations ====="
fd -t f 'formtextarea.php' htdocs/class | while IFS= read -r f; do
  echo "--- $f ---"
  rg -n -C2 'AllowDynamicProperties|\$configs|class XoopsFormTextArea|function __construct|property_exists|maxlength' "$f"
done

echo "===== target test context ====="
sed -n '240,275p' tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest.php

Repository: XOOPS/XoopsCore27

Length of output: 9378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===== formtextarea.php ====="
sed -n '1,160p' htdocs/class/xoopsform/formtextarea.php

echo "===== locate XoopsFormElement ====="
fd -t f 'formelement.php|xoopsformelement.php' htdocs/class/xoopsform|sort

echo "===== XoopsFormElement declarations/usages ====="
fd -t f 'formelement.php' htdocs/class/xoopsform | while IFS= read -r f; do
  echo "--- $f ---"
  rg -n -C3 'AllowDynamicProperties|\$configs|class XoopsFormElement|function __construct|maxlength|getName|setConfig|getConfig' "$f"
done

echo "===== XoopsFormDhtmlToolbar usages of configs ====="
fd -t f 'dhtmltoolbar.php' htdocs/class/xoopsform | while IFS= read -r f; do
  echo "--- $f ---"
  rg -n -C4 'property_exists|configs|maxlength|renderCheckLength' "$f"
done

echo "===== inspect target property occurrences ====="
rg -n '\$configs|\bconfigs\b' htdocs/class/xoopsform/formdhtmltextarea.php htdocs/class/xoopsform/formtextarea.php tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest.php

Repository: XOOPS/XoopsCore27

Length of output: 4981


🏁 Script executed:

#!/bin/bash
set -euo pipefail

composer config platform.php --unset >/dev/null 2>&1 || true

echo "===== PHPUnit version ====="
if [ -x vendor/bin/phpunit ]; then
  vendor/bin/phpunit --version 2>/dev/null || true
  php --version 2>/dev/null | head -1
else
  php -v 2>/dev/null | head -1 || true
  ls -l vendor/bin/phpunit 2>/dev/null || true
fi

echo "===== Composer config ====="
cat composer.json | python3 - <<'PY'
import json, sys
data=json.load(sys.stdin)
print("platform.php=", data.get("config", {}).get("platform", {}).get("php"))
print("phpunit=", data.get("require-dev", {}).get("phpunit/phpunit"))
PY

echo "===== isolated dynamic-property probe ====="
php -r '
class X { }
$e = new X();
$e->configs = ["maxlength" => 12345];
'
python3 <<'PY'
import subprocess
out = subprocess.run(
    ["php", "-r", 'error_reporting(E_ALL); class X { } $e = new X(); $e->configs = ["maxlength" => 12345];'],
    stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
print("rc=", out.returncode)
print("stderr=", out.stderr)
print("stdout=", out.stdout)
PY

Repository: XOOPS/XoopsCore27

Length of output: 926


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 <<'PY'
import subprocess
code='error_reporting(E_ALL); class X { } $e = new X(); $e->configs = ["maxlength" => 12345];'
out = subprocess.run(["php", "-r", code], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
print("php_version=", subprocess.run(["php", "-r", "echo PHP_VERSION;"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True).stdout.strip())
print("return_code=", out.returncode)
print("stderr=", repr(out.stderr))
print("stdout=", repr(out.stdout))
PY

echo "===== repo files at root ====="
git ls-files | rg '(^|/)composer\.json$|(^|/)phpunit.*\.xml|htdocs/class/xoopsform/(formdhtmltextarea|formtextarea|formelement)\.php|tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest\.php' | sort

Repository: XOOPS/XoopsCore27

Length of output: 2193


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "===== tests/phpunit.xml.dist first 120 lines ====="
sed -n '1,120p' tests/phpunit.xml.dist

echo "===== XoopsFormElement top/property list with line numbers ====="
rg -n -A8 -B2 'class XoopsFormElement|AllowDynamicProperties|`@var`|public|private|protected|^ {4}\$' htdocs/class/xoopsform/formelement.php

Repository: XOOPS/XoopsCore27

Length of output: 11380


🌐 Web query:

PHP 8.2 creation of dynamic property deprecated documentation

💡 Result:

As of PHP 8.2, the creation of dynamic properties (setting a value to an undeclared property) is deprecated [1][2][3]. When a dynamic property is created in code not opting into this behavior, PHP will emit a deprecation notice during execution [2][3]. This change is intended to help developers catch typos and errors in property access [1][4]. There are three primary ways to avoid this deprecation notice: 1. Declare the properties: The recommended approach is to explicitly declare all properties within the class definition [3][4]. 2. Use the #[\AllowDynamicProperties] attribute: You can mark a class with the #[AllowDynamicProperties] attribute to explicitly opt-in to dynamic property usage [1][2]. This effect is inherited, meaning child classes will also allow dynamic properties [2][5]. 3. Use magic methods: Classes that implement the get and set magic methods are exempt from this deprecation [1][3][4]. Additionally, stdClass and its subclasses are exempt from this deprecation because stdClass is internally marked with the #[AllowDynamicProperties] attribute [1][3]. It is important to note that while this behavior is merely deprecated in PHP 8.2, it is scheduled to be removed in PHP 9.0, at which point creating dynamic properties will result in a fatal Error exception [3][4].

Citations:


Declare config or avoid dynamic configs assignment.

$element->configs creates a dynamic property on XoopsFormDhtmlTextArea because neither it nor its parent declares configs and the classes do not use #[\AllowDynamicProperties]. PHP 8.2+ emits E_DEPRECATED for this, so this test can be converted into a PHPUnit failure on PHP 8.4+ or older with deprecation-as-error settings. Add a declaration such as public array $configs = []; or use an existing config accessor.

🤖 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 `@tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest.php` around lines 262
- 265, Update testCheckLengthButtonCarriesConfiguredMaxlength and the
XoopsFormDhtmlTextArea configuration path to avoid dynamically assigning
configs; declare a typed public array $configs initialized to [] on the
appropriate class, or reuse an existing configuration accessor, while preserving
the test’s configured maxlength value.

Source: Path instructions

Comment on lines +290 to +299
public function testStylesheetIsInjectedOnceAcrossMultipleEditorsOnOnePage(): void
{
$toolbar = new XoopsDhtmlToolbar();

$first = $toolbar->render($this->makeElement('editorOne'));
$second = $toolbar->render($this->makeElement('editorTwo'));

$this->assertStringContainsString('toolbar.css', $first);
$this->assertStringNotContainsString('toolbar.css', $second);
}

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 | 🟡 Minor | ⚡ Quick win

Pin $GLOBALS['xoTheme'] for the stylesheet test.

injectStylesheet() branches on $GLOBALS['xoTheme']. When a theme object is present it calls addStylesheet() and returns an empty string. If any earlier test in the process leaves an xoTheme global behind, $first contains no toolbar.css and Line 297 fails. The result then depends on test execution order.

Unset the global in setUp(), or set it explicitly in this test.

As per path instructions: "Review test code for proper assertions, test isolation, and edge case coverage."

💚 Proposed fix
     public function testStylesheetIsInjectedOnceAcrossMultipleEditorsOnOnePage(): void
     {
+        unset($GLOBALS['xoTheme']);
         $toolbar = new XoopsDhtmlToolbar();
📝 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
public function testStylesheetIsInjectedOnceAcrossMultipleEditorsOnOnePage(): void
{
$toolbar = new XoopsDhtmlToolbar();
$first = $toolbar->render($this->makeElement('editorOne'));
$second = $toolbar->render($this->makeElement('editorTwo'));
$this->assertStringContainsString('toolbar.css', $first);
$this->assertStringNotContainsString('toolbar.css', $second);
}
public function testStylesheetIsInjectedOnceAcrossMultipleEditorsOnOnePage(): void
{
unset($GLOBALS['xoTheme']);
$toolbar = new XoopsDhtmlToolbar();
$first = $toolbar->render($this->makeElement('editorOne'));
$second = $toolbar->render($this->makeElement('editorTwo'));
$this->assertStringContainsString('toolbar.css', $first);
$this->assertStringNotContainsString('toolbar.css', $second);
}
🤖 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 `@tests/unit/htdocs/class/xoopsform/XoopsDhtmlToolbarTest.php` around lines 290
- 299, Make testStylesheetIsInjectedOnceAcrossMultipleEditorsOnOnePage
deterministic by explicitly controlling the $GLOBALS['xoTheme'] state,
preferably clearing it in setUp() or at the start of the test. Preserve the
existing assertions for first-render injection and second-render suppression.

Source: Path instructions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants