-
Notifications
You must be signed in to change notification settings - Fork 16
Issue 101 Source Code Editor Opened Behind The Modal
Editing an email template, pressing the <> button on the toolbar opened the Source Code box underneath the template window. You could see it through the dimmed background, read the HTML in it, and do absolutely nothing with it.
Reported in issue #101 by mbsouth, who diagnosed it correctly in the report: "The ticket pop-up has a z-index of 2000, but the source code pop-up only has z-index: 1".
Fixed in 950fbd73, released as update #1197.
Tickets β Settings β Email templates, edit a template, press <>.
The Source Code panel appeared at the left of the screen, greyed out behind the template window's overlay. Its title was legible, its contents were legible, and it would not accept a click or a keystroke. Cancelling the template window revealed it sitting there perfectly happily underneath.
That is a particular kind of infuriating: the feature clearly worked, the box clearly opened, and it was clearly unusable.
FreeITSM's rich text editing is TinyMCE. When TinyMCE opens a dialog, it does not put it inside the editor, or inside whatever opened the editor. It appends it to the very end of <body>, into a container of its own called the sink:
<body>
β¦the whole applicationβ¦
<div class="modal" id="templateModal">β¦the template window, with the editor insideβ¦</div>
<div class="tox-tinymce-aux">β¦every dialog and menu TinyMCE opensβ¦</div>
</body>That sink carries a fixed depth chosen by TinyMCE, not by us:
/* TinyMCE's own skin */
.tox-tinymce-aux { z-index: 1300 }FreeITSM's shared pop-up window is deeper:
/* assets/css/inbox.css */
.modal { z-index: 2000 }1300 < 2000, so the dialog rendered behind. Being later in the document does not help: an explicit z-index beats document order, and TinyMCE had picked a smaller one.
Nothing was misconfigured, and no page had done anything wrong. Two components had each chosen a sensible number in isolation and nobody had ever put them in the same room.
The reporter's diagnosis was right in every way that mattered β a dialog stacked below a 2000 modal. The specific 1 they read in the inspector was one of TinyMCE's inner elements, .tox-dialog-wrap__backdrop, which really is z-index: 1 relative to its own parent. The number that actually decided the outcome was the sink's 1300 further up the tree.
The sink holds everything TinyMCE pops up. So the trap applied to:
| Dialog | Where it appears |
|---|---|
| Source code | the reported one |
| Insert/edit link | the toolbar of every rich-text editor in FreeITSM |
| Insert table | reply templates |
| Special character | reply templates |
and to both editors on the settings page, not just the reported one:
| Editor | Its window |
|---|---|
#templateBody β email templates |
<div class="modal" id="templateModal"> |
#replyTemplateBody β reply templates |
<div class="modal" id="replyTemplateModal"> |
The condition is simply a rich-text editor inside a pop-up. Anything meeting it was affected, on every dialog it offered.
One rule, placed directly above the .modal rule it has to clear β because the two numbers only make sense read together:
/* assets/css/inbox.css */
body .tox-tinymce-aux {
z-index: 2500;
}Why 2500. It has to clear every container that can hold an editor, and stay under the things that should still cover a dialog:
| Layer | Depth | |
|---|---|---|
.modal, and the two mobile full-screen editors |
2000 | must be cleared |
| TinyMCE sink | 2500 | β the fix |
.search-modal |
3000 | should still cover a dialog |
.toast |
10000 | should still be readable over one |
Why body is there. It is a specificity bump, not decoration. TinyMCE injects its skin stylesheet at runtime, so it lands after inbox.css and would win any tie on document order. Out-specifying it avoids reaching for !important.
Why one selector is enough. Everything TinyMCE opens lives inside that sink, including the dialog wrapper with its own z-index: 1100. Raising the sink moves the whole stacking context, and the numbers inside it become relative to the new position. Nothing else needs touching β and any editor placed inside a pop-up in future is covered without anyone having to remember this page exists.
assets/css/inbox.css was referenced 148 times with no cache-buster at all, plus three different versions elsewhere (v22, v37, v60). A CSS fix that browsers do not refetch is not a fix. All 165 references are now ?v=61.
| File | What changed |
|---|---|
assets/css/inbox.css |
The one rule, above .modal. |
| 165 pages |
inbox.css β ?v=61, so the rule actually arrives. |
| Why | |
|---|---|
| The TinyMCE skin files | Vendor code. Editing it would be undone by the next upgrade. |
.tox-dialog-wrap |
The skin styles it as .tox .tox-dialog-wrap, which outranks anything reasonable we could write β and it is inside the sink, so it does not need to be targeted. |
The page was driven for real in a headless browser at a desktop viewport, opening the actual template window and calling the actual toolbar command.
The bug was reproduced first. Before any change, the topmost element at the centre of the Source Code dialog was tox-edit-area__iframe β the editor behind it. That is the proof the dialog was unreachable.
The assertion is elementFromPoint, not the computed z-index. What matters is what a person can actually click. A computed number can look correct while a stacking context somewhere above makes it irrelevant.
After the fix, the topmost element at that same point is tox-textarea β inside the dialog. Checked on Source code and Insert link on the email template window, and Source code on the reply template window.
A negative control on the layer stack confirms the fix did not overshoot: the dialog clears .modal, the search window still covers the dialog, and a toast is still readable over it.
Both had been read straight out of the CSS, and both were wrong:
- The sink's effective depth is 1300, not the
1201visible in one skin file β there are several skins carrying different values, and only the loaded one counts. -
.tox-dialog-wrapappeared to sit outside the sink, which would have meant a second selector was needed. Asking the live DOM (dialog.closest('.tox-dialog-wrap')) showed it is inside. The second selector was therefore both unnecessary and inert, and was removed rather than left in beside a comment claiming it was load-bearing.
Source code, Insert link, Insert table and Special character now open in front of the window that asked for them, on email templates and reply templates alike.
If you had previously worked around this by writing template HTML somewhere else and pasting it in, you no longer need to.
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)