Tab Swap: Technical Details
Technical details of Tab Swap: object.view signal, the inline script, the four required DOM elements, PJAX re-run, version check, CSP nonce and file structure.
Tab Swap: Technical Details
Flow
graph TD
A["osTicket starts, plugin bootstrap()"] --> B["checkVersion(): store installed_version"]
B --> C{"Enable Tab Swap on?"}
C -->|"No"| D["No signal, no script"]
C -->|"Yes"| E["object.view signal connected"]
E --> F{"Object is a ticket?"}
F -->|"No"| G["Nothing happens"]
F -->|"Yes"| H["Inline script is output"]
H --> I["Browser: swap tabs"]
Plugin metadata
| Property | Value |
|---|---|
| Plugin ID | net.markus-michalski:tab-swap |
| Name | Tab Swap |
| Plugin class | TabSwapPlugin in class.TabSwapPlugin.php |
| Configuration class | TabSwapConfig in config.php |
| Instances | Singleton: exactly one instance, enable() creates it if needed |
| PHP | >=7.4 (composer.json) |
Server side
bootstrap()callscheckVersion(). Ifinstalled_versionis empty or lower than the version fromplugin.php,performUpdate()writes the new version to the configuration. A message goes to the error log only if theDEBUGconstant is set and truthy.- If the Enable Tab Swap option is on, the plugin connects
onObjectView()to theobject.viewsignal. onObjectView()only acts if the passed object is aTicket(for example not a task). In that case the script is output.- The plugin does not check permissions. It outputs the script for every ticket view in which the signal fires. Whether the swap takes effect is decided by the elements in the browser (see below).
- There are no database tables, no routes, no file copies and no changes to osTicket files.
Output script
The plugin outputs an element of the form <script data-plugin="tab-swap" data-version="..." nonce="...">:
data-versionholdsinstalled_version, but only if the value matches the patternnumber.numberornumber.number.number. Otherwise1.0.0is used. The value is also HTML-escaped.nonceis only set if theCSP_NONCEconstant is defined. The value is HTML-escaped.- The script is hard-coded in the plugin and contains no user input.
Browser side
The script only acts once all prerequisites are met:
- The elements
#post-reply-tab,#post-note-tab,#replyand#noteexist. - Both tabs are visible (
offsetParentis notnull).
Then the following happens:
- The
lielement of the note tab is moved before theliof the reply tab inside#response-tabs. - The class
activeis removed from the reply tab andaria-selected="false"is set. On the note tab,activeis added andaria-selected="true"is set. #replygetsdisplay: none,#notegetsdisplay: block.
The swap runs once per page build. So that it also applies to pages built later, the script uses:
- a
MutationObserverthat waits for#post-reply-taband stops after a successful swap, - a direct call and
DOMContentLoaded, pjax:successand, if jQuery is present, theredraw.staffevent. In both cases the state is reset and the swap runs again.
jQuery is not strictly required. Without it, only the reaction to redraw.staff is omitted.
File structure
tab-swap/
├── plugin.php # plugin metadata
├── class.TabSwapPlugin.php # plugin class, signal handler, inline script
└── config.php # TabSwapConfig with the two fields
The repository additionally contains composer.json, PHPUnit tests, a CI workflow and documentation. CI checks syntax and runs the tests with PHP 7.4 to 8.3.