|
<div class="docs-content" data-render>
|
|
# Frontend and components
|
|
|
|
The client side is vanilla JavaScript that enhances server-rendered HTML in place. This page covers the no-framework rule, the `Application` root, the custom web components, the content rendering pipeline, and the declarative `data-*` contract between markup and controllers. See also [Styling and templates](/docs/architecture-styling.html).
|
|
|
|
> Audience: administrators and maintainers. These pages are hidden from members and guests, in the sidebar, the search index, and the documentation export.
|
|
|
|
## Vanilla JavaScript, no framework
|
|
|
|
All JavaScript lives in `static/js/` as **ES6 modules, one class per file**. There is no React, Vue, or Angular, no NPM, and no bundler: the browser loads native modules directly. A small set of libraries is loaded from `static/vendor/` over CDN-style script tags: `marked` (markdown), `highlight.js` (syntax highlighting), `DOMPurify` (sanitization), and `emoji-picker-element`.
|
|
|
|
Those vendor scripts MUST be loaded with `defer` or `type="module"`. The Playwright tests wait on `domcontentloaded`; a blocking script would make that event time out.
|
|
|
|
## The Application root
|
|
|
|
`Application.js` is the browser-side root. A single instance is created and exposed globally:
|
|
|
|
```javascript
|
|
const app = new Application();
|
|
window.app = app;
|
|
```
|
|
|
|
Its constructor imports `components/index.js` (which defines every web component) and instantiates every page controller as a property (`app.modals`, `app.forms`, `app.votes`, `app.comments`, and so on). It also creates the three component singletons once and exposes them as `app.dialog`, `app.contextMenu`, and `app.toast`. Page code reaches any capability through `window.app`.
|
|
|
|
## Custom web components
|
|
|
|
Reusable UI widgets are custom elements with the `dp-` prefix, living in `static/js/components/`. Each extends a thin `Component` base that adds typed attribute helpers:
|
|
|
|
```javascript
|
|
export class Component extends HTMLElement {
|
|
attr(name, fallback = "") { ... }
|
|
boolAttr(name) { ... }
|
|
intAttr(name, fallback = 0) { ... }
|
|
}
|
|
```
|
|
|
|
Every component **renders into the light DOM (no shadow root)** so the global CSS applies, and **self-registers** with `customElements.define` at the bottom of its file. `components/index.js` imports them all and is imported by `Application.js`, so every element is defined site-wide. The catalog:
|
|
|
|
| Element | Purpose |
|
|
|---------|---------|
|
|
| `dp-avatar` | Renders a user avatar. |
|
|
| `dp-code` | Renders a syntax-highlighted code block. |
|
|
| `dp-content` | Wraps the content renderer behind `data-render`. |
|
|
| `dp-upload` | The single file-upload button used everywhere. |
|
|
| `dp-toast` | Transient notifications (`app.toast`). |
|
|
| `dp-dialog` | Confirm / prompt / alert modals (`app.dialog`). |
|
|
| `dp-context-menu` | Right-click and long-press menus (`app.contextMenu`). |
|
|
|
|
**The component-vs-module rule.** Self-contained presentational widgets become components. Partial-bound controllers that enhance server-rendered markup (votes, reactions, comments) and pure utilities stay plain modules, because they augment existing HTML rather than render standalone UI.
|
|
|
|
## Content rendering pipeline
|
|
|
|
`ContentRenderer.js` runs on every element marked `data-render`, in this exact order:
|
|
|
|
1. emoji shortcode -> unicode
|
|
2. markdown via `marked`
|
|
3. **`DOMPurify.sanitize`**
|
|
4. `highlight.js` on `<pre><code>`
|
|
5. image URL -> `<img>`
|
|
6. YouTube URL -> trusted iframe
|
|
7. autolink remaining URLs
|
|
|
|
A `NodeIterator` skips `CODE`, `PRE`, `SCRIPT`, and `STYLE` so URLs inside code are never rewritten. Sanitization runs on the raw `marked` output before the trusted YouTube embeds are injected, so user content cannot inject script, event-handler, or iframe payloads while our own embeds survive.
|
|
|
|
This step is **fail-closed**: `render()` throws if `DOMPurify` is not loaded rather than emitting unsanitized HTML. Because content is rendered client-side from `element.textContent`, Jinja autoescaping alone does not protect it. This sanitize step is the XSS control.
|
|
|
|
## The data-* contract
|
|
|
|
Controllers find and enhance markup through declarative `data-*` attributes rather than inline scripts. This keeps behavior HTML-driven and auditable (grep for the attribute):
|
|
|
|
| Attribute | Behavior |
|
|
|-----------|----------|
|
|
| `data-render` | Render markdown content through the pipeline. |
|
|
| `data-modal` | Open the named `.modal-overlay`. |
|
|
| `data-confirm` | Show a confirm dialog before the action. |
|
|
| `data-copy` | Copy the referenced content to the clipboard. |
|
|
| `data-share` | Copy a share URL. |
|
|
| `data-mention` | Attach `@mention` autocomplete to a textarea. |
|
|
</div>
|