Skip to content

Mobile Friendly Knowledge

Ed Mozley edited this page Aug 9, 2026 · 4 revisions

Mobile: 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 one round: #1000.


Scope

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.


Two things make this module different

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.


Reading an article: the widest content in the product

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 */

πŸ”‘ nowrap is 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: block is separately required: overflow does nothing on a display: table box.

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.


⚠️ The localStorage desktop mode, second sighting

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 localStorage before 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.


The one justified edit outside mobile.css/js

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; } }',

How it works

Entry point

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.

mobile.css layers

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

Opting a page in

    <link rel="stylesheet" href="../assets/css/mobile.css?v=34">   <!-- after the page's own <style> -->
    <script src="../assets/js/mobile.js?v=17"></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.


Challenges & solutions

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.

Verification

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 at scrollWidth 636 > clientWidth 296, and docScrollW === innerWidth throughout.
  • The pop‑out preference deliberately ARMED. The harness sets knowledge_editor_popout = '1' in the iframe's localStorage before 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.

Two false alarms, both instructive

  • w.articleEditor was always undefined. knowledge.js declares it with let, which lives in the script's global lexical scope and is therefore not a property of window β€” unreadable from another window. The registry (w.tinymce.get('articleBody')) is a real global. This is the same let‑vs‑function distinction 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.

And one finding that was not this change

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.


Known rough edges / future polish

  • 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, not t() 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 nowrap trade, above).
  • knowledge.js still renders article HTML raw rather than through safe-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-label sweep across the injected chrome is still owed, module‑wide.

Reference

  • CSS: assets/css/mobile.css β€” LAYER 17 (17a–17g).
  • JS: assets/js/mobile.js β€” initKnowledgeMobile(). Plus the one edit outside it: assets/js/knowledge.js content_style.
  • Opt‑in wiring: knowledge/index.php, review/index.php, settings/index.php, assistant/index.php, help.php (currently mobile.css?v=34, mobile.js?v=17 β€” on all fifteen pages that link them).
  • Changelog: #1000.
  • Parent: Mobile‑Friendly Β· Siblings: Mobile: Tickets, Mobile: Assets, Mobile: Calendar Β· Techniques: Mobile: Techniques & Tricks Β· Module: Knowledge.

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally