-
Notifications
You must be signed in to change notification settings - Fork 15
Mobile Friendly Knowledge
Knowledge is the fourth module made mobileβfriendly, after Tickets, Assets and Calendar. Same hard rule: one @media (max-width: 768px) block, one matchMediaβgated script, desktop byteβidentical.
π§° For the codeβlevel catalogue of the CSS/JS tricks used here, see Mobile: Techniques & Tricks.
Why Knowledge? It is the module with the strongest claim on a phone. Looking something up is what you do standing at somebody's desk, in a comms room, or in a car park β not sitting in front of the thing you use to write articles.
Shipped in two rounds: #1000 (the module) and #1001 (buying back the screen above the article text, after Ed's device pass).
| Page | State |
|---|---|
knowledge/ β list, article, editor |
Done deep β search into the subβbar, tags into a sheet, authorβHTML containment, editor reflow |
knowledge/review/ β the review schedule |
Done β sixβcolumn table becomes a card feed (17f) |
knowledge/assistant/ β the gap finder |
Done β stacked header, scrolling tab strip (17g) |
knowledge/settings/ |
Done β inherits LAYER 15e via data-mobile-page="settings"; it is forms only, no table to rescue |
knowledge/help.php |
Done β inherits 16h (the app shell + a scroll container) from the Calendar round |
Two of those five cost no new CSS. That is 15e and 16h earning their keep β and unlike the Calendar settings page, this time the inheritance really was the whole job, because it was checked rather than assumed.
1. There is no pane stack, and for a better reason than the Calendar's. .knowledge-main already shows exactly one of three views at a time β list, article, editor β toggled by showView() setting three display values. There is nothing to slide. What that state doesn't do is reach CSS, so mobile.js mirrors it onto body[data-kb-view] and the layer reacts to it.
2. The primary action is SEARCH, and search lived in the sidebar that had to go. Every previous module put its sidebar behind a button and left it there. Here that would be the wrong call: you open a knowledge base to find one article, so the search box is relocated into the subβbar where it is always visible. The tag filters, New article and the recycle bin go into the sheet. Search does not.
// Moved, not rebuilt β #articleSearch keeps its id and its inline
// onkeyup="debounceSearch()", so the module's own search needs no rewiring.
var box = sb.querySelector('.search-box');
bar.insertBefore(box, bar.firstChild);The corollary is that the subβbar belongs to the list. On the article and editor views it is hidden entirely β searching or tagβfiltering from inside an article would silently rearrange a list you can't see, both views already carry their own Back/Cancel, and it is one less strip of chrome above the thing you came to read.
An article body is authorβwritten HTML from TinyMCE. It can hold a screenshot at its original pixel width, a sevenβcolumn table of firewall rules, a registry path with no spaces in it, a pageβwide block of PowerShell. Every one of those is wider than 360px, and per Β§3 a wide thing loose in the layout doesn't merely overflow β it reflows the whole page to desktop width and switches the media query off.
pre was already an overflow-x: auto island. Everything else is the same treatment applied to the rest:
.article-content-body img,
.article-content-body video,
.article-content-body iframe { max-width: 100%; height: auto; }
.article-content-body table { display: block; overflow-x: auto; overscroll-behavior-x: contain; }
.article-content-body th,
.article-content-body td { white-space: nowrap; }
.article-content-body { overflow-wrap: anywhere; } /* registry paths, URLs, hashes */π
nowrapis doing the real work in that table rule, not the overflow. Without it a sevenβcolumn table doesn't scroll β it crushes, wrapping every cell down to one word per line inside 360px. That is contained, so it passes the overflow measurement, and it is unreadable.display: blockis separately required: overflow does nothing on adisplay: tablebox.The trade is that a cell holding a full sentence becomes a long sideways swipe. For the tables people actually put in a runbook β rules, ports, versions, paths β that is the right way round, and it matches 15c and 15f.
The Share menu is an absolutelyβpositioned dropdown pinned to a button that is now full width, so it becomes a bottom sheet β the same pureβCSS move as the Calendar's quickβview popup.
Everything between the app bar and the first paragraph is overhead. Ed's device pass found three ways to reduce it, and one genuine bug.
Back joins the action row. It was taking a whole row to itself because "Back to list" is too long to sit beside Share, Edit and Archive. On mobile the label shortens to just Back and all four share one 42px row. The label comes from a new shared common.back β see the translation note below.
The meta block collapses, Gmailβstyle. Four lines β author, created, modified, views β become one. Collapsed you see Modified and the tags; tapping reveals the rest.
.article-content-header .kb-meta-by,
.article-content-header .kb-meta-created,
.article-content-header .kb-meta-views { display: none; }
.article-content-header.kb-meta-open .kb-meta-by { display: block; } /* β¦etc */π The whole meta row is the control, not a chevron. It is a much bigger tap target, and β the reason it was designed that way β its accessible name is the visible "Modified: β¦" text, so the toggle needs no
aria-labeland therefore no new string in 24 languages.role="button",tabindex,aria-expandedand a keyboard handler are set bymobile.js. The chevron is drawn with borders, not aβΎglyph: at 11px the character rendered as a faint dot in the app's font stack, and a chevron that reads as a full stop is not an affordance.
knowledge.css:729 makes .article-content-header position: sticky; top: 0. A sticky element sticks to the top of its scroll container's content box β below its padding. .knowledge-main carries 12px of it, so the title pinned 12px down and paragraphs scrolled up through the transparent strip above it.
body[data-kb-view="detail"] .knowledge-main { padding-top: 0; } /* nothing to stick below */
body[data-kb-view="detail"] .article-detail-header { margin-top: 12px; } /* the space, but scrollable */
.article-content-header { margin: -16px -16px 16px; padding: 14px 16px; } /* cancels the card's 16px exactly */Two details worth keeping: the space isn't deleted, it is moved onto something that scrolls away (the button row's margin), and the header's negative margin must match the card's padding exactly β the inherited -20px against a 16pxβpadded card would lift the bar clear and open a strip of its own. body[data-kb-view] is what makes this scopeable to the article without touching the list.
π The peekβthrough is now the second sighting in two different modules. If a module has a
position: stickyheader, check what padding its scroll container carries before looking anywhere else.
common.back is the only string added β and it was harvested, not written: all 24 locales already translate the bare word "Back" in change-management.php, so each locale's common.php got its own existing wording. Zero invention, zero silent English fallback, and every future module gets a translated Back for free. The generic word also belongs in common rather than in a fifth moduleβspecific copy of it.
The only other markup change is four class names (.kb-meta-by, .kb-meta-created, .kb-meta-modified, .kb-meta-views) added to the renderer's meta spans. Nothing targets them on desktop; they exist so the collapse rule can name the line it hides instead of counting to it β the #937 lesson, applied by adding the missing class rather than reaching for :nth-child.
This is the #762 tickets bug in a different module, and it is worth treating as a pattern to go looking for rather than a coincidence:
function applyEditorPopoutFromPref() {
const prefersPopout = localStorage.getItem('knowledge_editor_popout') === '1';
container.classList.toggle('editor-popout', prefersPopout);
}.editor-popout turns the editor form into a row-reverse flex with a fixed 340px property panel. At 360px that panel is the screen and the editor itself gets nothing. And because the preference lives in localStorage, an analyst who ever turned it on at their desk carries it to their phone β the page looks broken for them and fine for everyone else.
Neutralised the same way as tickets: wrap at the source, leave the stored preference alone so desktop is unchanged, and keep a CSS backstop because the backstop is what saved the tickets one.
function stripEditorPopout() { if (mq.matches) container.classList.remove('editor-popout'); }
['applyEditorPopoutFromPref', 'toggleEditorPopout'].forEach(function (fn) { /* wrap */ });The popβout toggle button is also hidden on mobile β the editor is already the whole screen, and leaving the button would let you reβarm the preference.
π When you bring a module along, grep it for
localStoragebefore you start. Two of four modules have had a saved desktop mode that breaks the phone, and neither showed up until it was looked for.
TinyMCE renders into an iframe, so no rule in mobile.css reaches the text you are typing. It has to be 16px on a touch device or iOS zooms on focus and springs the reflow trap. That means content_style in knowledge.js β the same single justified edit inbox.js took in #766, and keyed on the pointer rather than a width so a narrow desktop window is unaffected:
content_style: 'body { β¦ font-size: 14px; β¦ }' +
' @media (pointer: coarse) { body { font-size: 16px; } }',if (document.querySelector('.knowledge-container')) { initKnowledgeMobile(); return; }Keyed on .knowledge-container so the module's other four pages take the shared shell and nothing else β the review page is .review-container, the assistant is .ka-page.
| Layer | Purpose |
|---|---|
| 1β13 | Tickets |
| 14β15 | Assets |
| 16 | Calendar |
| 17 | Knowledge β 17a shell + search relocation Β· 17b list Β· 17c article body containment Β· 17d editor + popβout neutralisation Β· 17e the tags sheet Β· 17f the review card feed Β· 17g the assistant Β· 17h the article-reading refinements (#1001) |
<link rel="stylesheet" href="../assets/css/mobile.css?v=35"> <!-- after the page's own <style> -->
<script src="../assets/js/mobile.js?v=18"></script> <!-- last -->mobile.css and mobile.js are shared. Bump the ?v= on every page that links them β fifteen now β not just the one you edited.
| Challenge | Solution |
|---|---|
| Hiding the sidebar hides search, which is the whole point of the module on a phone. | Relocate the real .search-box into the subβbar rather than into the sheet. Moved, not copied, so #articleSearch keeps its id and its inline handler. |
Moving the search box out leaves its <h3>Search Articles</h3> section in the sheet with nothing under it. |
mobile.js marks that section .kb-dup as it lifts the box out, rather than the CSS guessing at a position. |
| The sheet said "Tags" twice β its own title and the panel's "Filter by Tags". | Hide only the duplicate: .sidebar-section:has(#tagFilterList) h3. |
| A sevenβcolumn table passed the overflow check while being unreadable. | See above β white-space: nowrap is what turns crushing into scrolling. |
The review card feed showed a bare 139 with nothing saying it meant days overdue β the word lived only in the column header the card feed drops, and there is no translated string to relabel it with. |
Hide the cell: td:has(.days-overdue). It says nothing the line above doesn't, because the review date immediately preceding it is already rendered red for exactly that case. |
| Once the headers go, a stack of bare values reads as unexplained strings. | Hierarchy instead of labels: the title stays fullβstrength, td + td is muted, and .review-date.overdue keeps its red on top β so the one cell that matters is the one that stands out. No CSSβgenerated labels; they would be hardcoded English. |
| Both the editor popβout and the leftβpanel hover preference are perβanalyst desktop modes that follow the user to the phone. | Popβout is stripped on mobile. The hover mode needs nothing: relocating the sidebar into a sheet takes it out of .knowledge-container, so .sidebar-hover's 16px hotβzone rules stop applying by construction. |
Same harness as the Calendar round β the real authenticated pages driven in headless Chrome, asserted rather than eyeballed β with two additions:
-
A hostileβauthor payload. After a real article renders, its body is replaced with the worst thing somebody can paste into TinyMCE: a 1400px image, a sevenβcolumn table, a 120βcharacter registry path, a wide
pre. Injected into the DOM, so no database write. Result: the image scales to 296px, the table scrolls atscrollWidth 636 > clientWidth 296, anddocScrollW === innerWidththroughout. -
The popβout preference deliberately ARMED. The harness sets
knowledge_editor_popout = '1'in the iframe'slocalStoragebefore opening the editor, so the #762 defence is tested in the state that breaks it rather than the state that doesn't. A defence only exercised in the safe case is not a test. - Desktop positive control at 1400px asserting the inverse of every mobile claim, plus a regression sweep over Tickets, Assets and Calendar.
-
w.articleEditorwas alwaysundefined. knowledge.js declares it withlet, which lives in the script's global lexical scope and is therefore not a property ofwindowβ unreadable from another window. The registry (w.tinymce.get('articleBody')) is a real global. This is the sameletβvsβfunctiondistinction the Calendar branch relies on from the inside; from the outside it cuts the other way. - A "desktop sidebar is 16px, expected 280px" failure was the analyst's Left panel: hover preference doing exactly what it should. An assertion that hardβcodes one of two legitimate states reads a preference as a regression.
At a 1100px window several modules' headers overflow horizontally. It looked like a regression until a control measured contracts/, which does not link mobile.css at all and overflows to 1176px just the same β and mobile.css has no rules above 768px to begin with. It is the app's own desktop header needing more than 1100px on navβheavy modules. Recorded here so the next person doesn't reβdiagnose it: a desktopβwidth failure on a page that never opted in is the control, not the bug.
Still owed: a real device pass with Ed.
-
knowledge/assistant/is hardcoded English in the markup β "Assistant", "Look for gaps", "To write", "Written", "Not needed" and its intro paragraph are literals in the PHP, nott()calls. A module bug rather than a mobile one, so it was flagged rather than folded into this change. Same class of thing as the Calendar's Englishβonly day names. - A table cell holding a full sentence becomes a long sideways swipe (the
nowraptrade, above). -
knowledge.jsstill renders article HTML raw rather than throughsafe-html.jsβ unrelated to mobile, but this work spent a lot of time looking at exactly that innerHTML, so it is worth writing down again. - The keyboardβvsβbottomβinput problem applies to the editor.
-
aria-labelsweep across the injected chrome is still owed, moduleβwide.
- CSS:
assets/css/mobile.cssβ LAYER 17 (17aβ17h). - JS:
assets/js/mobile.jsβinitKnowledgeMobile(). Plus the one edit outside it:assets/js/knowledge.jscontent_style. - Optβin wiring:
knowledge/index.php,review/index.php,settings/index.php,assistant/index.php,help.php(currentlymobile.css?v=35,mobile.js?v=18β on all fifteen pages that link them). - Changelog: #1000 (round 1) and #1001 (round 2).
- Parent: MobileβFriendly Β· Siblings: Mobile: Tickets, Mobile: Assets, Mobile: Calendar Β· Techniques: Mobile: Techniques & Tricks Β· Module: Knowledge.
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
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
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
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- 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)