This document describes the internal architecture of FrameTrail for developers who want to understand, extend, or contribute to the codebase.
The global FrameTrail object is defined in src/_shared/frametrail-core/frametrail-core.js and provides the foundation for the entire application:
window.FrameTrail = {
defineModule: // Register a module definition
defineType: // Register a type definition
init: // Create a new FrameTrail instance
autoInit: // Scan for [data-frametrail] video elements and init each
instances: // Array of all active instances
};When FrameTrail.init() is called, it returns an instance with:
{
start: // Start the application
initModule: // Initialize a module
unloadModule: // Unload a module
module: // Get a module's public interface
modules: // Get all loaded modules
getState: // Read global state
changeState: // Update global state (triggers listeners)
type: // Get a type constructor
newObject: // Create instance of a type
triggerEvent: // Fire custom events
addEventListener: // Listen to custom events
}The global FrameTrail object is the factory/registry. Instance methods like module(), changeState(), getState() are only available on initialized instances — the FrameTrail parameter passed into defineModule callbacks.
- Modules defined via
FrameTrail.defineModule()receive the instance as their closure argument and can freely callFrameTrail.module('X'). - Plain classes (e.g.
StorageAdaptersubclasses insrc/_shared/frametrail-core/storage/) are not FrameTrail modules and do not have access to any instance. If they need to call module APIs, the caller must pass the FrameTrail instance explicitly. - Every module must be initialized with
FrameTrail.initModule('ModuleName')before it can be accessed viaFrameTrail.module('ModuleName').
Modules encapsulate related functionality. They're defined with FrameTrail.defineModule():
FrameTrail.defineModule('ModuleName', function(FrameTrail) {
// Private scope — variables and functions here are not accessible outside
var privateVar = 'hidden';
function privateFunction() {
// Can access other modules
var db = FrameTrail.module('Database');
// Can read/write global state
var editMode = FrameTrail.getState('editMode');
FrameTrail.changeState('editMode', true);
}
// Return public interface
return {
publicMethod: privateFunction,
// State change listeners (called automatically)
onChange: {
'editMode': function(newVal, oldVal) { /* react */ },
'viewMode': function(newVal, oldVal) { /* react */ }
},
// Called when module is unloaded
onUnload: function() { /* cleanup */ }
};
});- Definition:
FrameTrail.defineModule()registers the factory function - Initialization:
FrameTrail.initModule('Name')calls the factory and stores the public interface - Usage:
FrameTrail.module('Name').method()accesses the public interface - State reactions:
onChangehandlers are called automatically when state changes - Unload:
FrameTrail.unloadModule('Name')callsonUnloadand removes the module
| Module | Purpose |
|---|---|
Database |
Loads/saves all JSON data via the active storage adapter |
StorageManager |
Selects and initializes the appropriate storage adapter; exposes canSave() / canSaveToServer() |
RouteNavigation |
URL parsing, hash parameters, environment detection |
UserManagement |
Login, registration, user settings, and guest editing (name-only, no account required) |
Localization |
Multi-language string management (en, de) |
ResourceManager |
Resource CRUD operations |
ViewResources |
Resource gallery/grid view |
TagModel |
Tag definitions and filtering |
HypervideoPicker |
Hypervideo selection dialog |
HypervideoFormBuilder |
Hypervideo creation/edit forms |
UserTraces |
User activity tracking |
UndoManager |
Undo/redo for editing operations |
| Module | Purpose |
|---|---|
PlayerLauncher |
Bootstrap and initialization orchestration |
HypervideoModel |
Current hypervideo data model |
HypervideoController |
Playback control, timing |
AnnotationsController |
Annotation lifecycle management |
OverlaysController |
Overlay lifecycle management |
SubtitlesController |
Subtitle loading and display |
CodeSnippetsController |
Code snippet execution at timestamps |
InteractionController |
Drag/drop, resize for editing |
TimelineController |
Timeline UI and scrubbing |
Interface |
Main UI coordinator |
InterfaceModal |
Dialogs, loading screens |
Sidebar |
Sidebar panel management |
Titlebar |
Top bar with controls |
ViewVideo |
Video player view |
ViewOverview |
Hypervideo selection grid |
ViewLayout |
Content view layout areas |
HypervideoSettingsDialog |
Hypervideo configuration |
AdminSettingsDialog |
Admin settings panel |
| Module | Purpose |
|---|---|
ResourceManagerLauncher |
Bootstrap for standalone resource manager |
The PlayerLauncher module orchestrates initialization:
1. Localization
2. InterfaceModal (shows loading screen)
3. RouteNavigation
4. StorageManager (detects storage mode)
5. UserManagement
6. Database (loads data via storage adapter)
7. TagModel
8. ResourceManager
9. HypervideoFormBuilder
10. HypervideoModel
11. Interface (initializes sub-modules)
12. HypervideoController (if viewing a hypervideo)
13. UserTraces
14. UndoManager
Types define data structures with inheritance support:
FrameTrail.defineType('TypeName', function(FrameTrail) {
return {
parent: 'ParentType', // Optional inheritance
constructor: function(data) {
this.data = data;
},
prototype: {
render: function() { /* ... */ },
get name() { return this.data.name; }
}
};
});var overlay = FrameTrail.newObject('Overlay', overlayData);
overlay.render();Resource (abstract base)
├── ResourceVideo
├── ResourceImage
├── ResourceAudio
├── ResourceYoutube
├── ResourceVimeo
├── ResourceWistia
├── ResourceLoom
├── ResourceTwitch
├── ResourceSoundcloud
├── ResourceSpotify
├── ResourceWebpage
├── ResourceWikipedia
├── ResourcePDF
├── ResourceText
├── ResourceHtml
├── ResourceLocation
├── ResourceQuiz
├── ResourceHotspot
├── ResourceEntity
├── ResourceMastodon
├── ResourceCodepen
├── ResourceFigma
└── ResourceUrlPreview
Overlay (video overlay with position + time range)
Annotation (sidebar annotation with time range)
Hypervideo (hypervideo data wrapper)
Subtitle (VTT subtitle cue)
CodeSnippet (timed JavaScript execution)
ContentView (layout area content)
Each Resource type implements:
renderContent()— Display the resource in the playerrenderThumb()— Thumbnail for the resource managerrenderPropertiesControls()— Editing UI for overlaysrenderTimeControls()— Editing UI for annotations
State is managed centrally and changes trigger reactive updates:
// Read state
var editMode = FrameTrail.getState('editMode');
var allState = FrameTrail.getState(); // Returns full state object
// Update state (triggers onChange handlers in all modules)
FrameTrail.changeState('editMode', true);| Property | Type | Description |
|---|---|---|
target |
String/Element | CSS selector or DOM element for mount point |
editMode |
Boolean/String | false, 'overlays', 'annotations', etc. |
viewMode |
String | 'video', 'overview', 'resources' |
storageMode |
String | 'server', 'local', 'needsFolder', 'download', 'static' |
loggedIn |
Boolean | User authentication status |
username |
String | Current user's name |
fullscreen |
Boolean | Fullscreen state |
sidebarOpen |
Boolean | Sidebar visibility |
viewSize |
Array | [width, height] of viewport |
unsavedChanges |
Boolean | Dirty flag for editing |
slidePosition |
String | 'left', 'middle', 'right' |
videoElement |
String/Element | Selector or ref to an existing <video> to adopt (shorthand API) |
videoSource |
String | Video URL to use when creating a new <video> (shorthand API) |
annotations |
String/Array | URL string or array of W3C annotation URLs / inline objects (shorthand API) |
dataPath |
String|null | Base URL for the _data/ directory (e.g. '../_data/'). null = auto-detect. |
server |
String|null | Base URL for the _server/ PHP directory (e.g. '../_server/'). null = auto-detect or no server. |
When state changes, all modules with matching onChange handlers are notified:
// In Module A
FrameTrail.changeState('editMode', 'overlays');
// In Module B (automatic callback)
onChange: {
'editMode': function(newValue, oldValue) {
// newValue = 'overlays', oldValue = false
this.updateUI();
}
}FrameTrail uses a strategy pattern for data persistence. The StorageManager module detects the environment and initializes the appropriate adapter:
| Adapter | Class | When Used |
|---|---|---|
| Server | StorageAdapterServer |
HTTP/HTTPS with PHP backend responding at _server/ajaxServer.php |
| Local | StorageAdapterLocal |
File System Access API available (Chrome/Edge) and folder selected |
| Download | StorageAdapterDownload |
No server and no File System Access API (Firefox/Safari, or file://). Stores data in memory; canSave is false; exports via Save As. Also available as a supplemental export tool in server and local modes. |
| Static | StorageAdapterStatic |
Explicit dataPath init option with no server option. Reads JSON from the CDN; inherits in-memory writes and Save As export from StorageAdapterDownload. |
All adapters implement the same interface, so the rest of the application doesn't need to know which storage backend is active.
StorageManager.init() determines the storage mode at startup:
- Shorthand API or inline contents (
videoElement/videoSourcepresent, orcontentsis not null) →'download'immediately (no server needed) - Explicit
serveroption → probe that URL; if PHP responds →'server'; if unreachable → fall through to local detection - Explicit
dataPath+ noserver→'static'(reads JSON fromdataPath, in-memory writes, Save As export) - Auto-detect on HTTP/HTTPS (neither option set) → probe
_server/ajaxServer.php- PHP responds → set
server+dataPathstate to defaults, use'server' - PHP unreachable → fall through to local detection
- PHP responds → set
- Local detection (no server found, or
file://protocol):- File System Access API supported (Chrome/Edge) → try to restore a previously saved folder handle
- Handle restored →
'local' - No handle →
'needsFolder'(folder picker shown)
- Handle restored →
- File System Access API not supported (Firefox/Safari) →
'download'
- File System Access API supported (Chrome/Edge) → try to restore a previously saved folder handle
In 'download' and 'static' modes data is stored in memory and users can export their work via Save As. Once a folder is selected in 'needsFolder' mode, the state transitions to 'local'.
All data is stored as JSON files in _data/:
_data/
├── config.json # Instance configuration
├── users.json # User accounts
├── tagdefinitions.json # Tag definitions
├── custom.css # Custom global CSS
├── resources/
│ ├── _index.json # Resource metadata
│ └── [files...] # Uploaded media
└── hypervideos/
├── _index.json # Hypervideo list
└── [id]/
├── hypervideo.json # Hypervideo data + content
├── annotations/
│ ├── _index.json # Annotation file index
│ └── [userId].json # Per-user annotations
└── subtitles/ # VTT subtitle files
{
"meta": {
"name": "Video Title",
"description": "Description",
"thumb": "thumbnail.jpg",
"creator": "username",
"creatorId": "user-id",
"created": 1234567890,
"lastchanged": 1234567890
},
"config": {
"layoutArea": { "areaTop": [], "areaBottom": [], "areaLeft": [], "areaRight": [] },
"hidden": false,
"slidingMode": "overlay"
},
"clips": [
{ "resourceId": "resource-id", "duration": 120, "start": 0, "end": 120 }
],
"contents": [ /* overlays and code snippets (W3C Web Annotation format) */ ],
"subtitles": { "en": "subtitles/en.vtt" },
"globalEvents": { "onReady": "", "onPlay": "", "onPause": "", "onEnded": "" },
"customCSS": ""
}Overlays and annotations use the W3C Web Annotation data model with frametrail: extensions for position, type, and attributes.
{
"resource-id": {
"name": "My Image",
"type": "image",
"src": "image.jpg",
"thumb": "image_thumb.jpg",
"licenseType": "cc-by-sa",
"attributes": {},
"tags": ["nature", "landscape"]
}
}Modules can communicate via custom events:
// Fire event
FrameTrail.triggerEvent('myCustomEvent', { data: 'value' });
// Listen to event
FrameTrail.addEventListener('myCustomEvent', function(event) {
console.log(event.detail.data);
});UI components use standard DOM event handling (addEventListener, dispatchEvent).
The RouteNavigation module provides environment info:
var env = FrameTrail.module('RouteNavigation').environment;
env.server // Boolean: true if running on HTTP server
env.hostname // String: current hostname
env.iframe // Boolean: true if embedded in iframeFrameTrail uses hash fragments for navigation:
index.html#hypervideo=abc123&t=30.5
| Parameter | Description |
|---|---|
hypervideo |
Hypervideo ID to load |
t |
Start time in seconds |
Themes are defined in src/_shared/styles/variables.css using two scoping layers.
Layer 1 — base selector: Variables are declared on .frametrail-body plus a list of detached elements (body > .ft-dialog, drag clones, popups, etc.) that are appended directly to <body> and therefore outside the DOM tree. This ensures consistent defaults everywhere. The base block also declares color-mix() derivations for the three semi-transparent variants so custom themes need only set 4 core variables:
.frametrail-body, .ft-dialog, /* ... */ {
--primary-bg-color: rgba(47, 50, 58, 1);
--secondary-bg-color: rgba(73, 76, 81, .6);
--primary-fg-color: rgba(255, 255, 255, 1);
--secondary-fg-color: rgba(220, 220, 220, 1);
--semi-transparent-bg-color: rgba(47, 50, 58, .8); /* fallback */
--semi-transparent-bg-color: color-mix(in srgb, var(--primary-bg-color) 80%, transparent);
/* ... similar for --semi-transparent-fg-color and --semi-transparent-fg-highlight-color */
}Layer 2 — per-theme selectors: Each theme targets the sub-contexts that should be themed (main content, loading screen, login overlay, titlebar, layout manager). Edit modes like annotations, overlays, and codesnippets are excluded so the editor UI falls back to the default theme's known-good contrast:
.frametrail-body[data-frametrail-theme="bright"] :is(
.mainContainer:not([data-edit-mode="settings"], [data-edit-mode="overlays"],
[data-edit-mode="codesnippets"], [data-edit-mode="annotations"]),
.loadingScreen, .userLoginOverlay, .titlebar:not(.editActive), .layoutManager
),
.themeItem[data-theme="bright"] {
--primary-bg-color: rgba(255, 255, 255, 1);
--primary-fg-color: rgba(80, 80, 80, 1);
/* ... only variables that differ from base or computed defaults */
}Themes are activated by setting data-frametrail-theme on .frametrail-body.
src/_shared/styles/variables.css— Theme definitions (CSS custom properties)src/_shared/styles/generic.css— Common styles, custom select dropdownssrc/_shared/styles/frametrail-webfont.css— FrameTrail icon fontsrc/player/types/[Type]/style.css— Player type stylessrc/_shared/types/[Type]/style.css— Resource type stylessrc/player/modules/[Module]/style.css— Player module stylessrc/_shared/modules/[Module]/style.css— Shared module styles
All AJAX requests go through src/_server/ajaxServer.php:
| Action | Description |
|---|---|
setupCheck |
Check if initial setup has been completed |
setupInit |
Run first-time setup |
userRegister |
Create new user |
userLogin |
Authenticate user |
userLogout |
End session |
userChange |
Update user settings |
hypervideoAdd |
Create hypervideo |
hypervideoChange |
Update hypervideo |
hypervideoClone |
Duplicate hypervideo |
hypervideoDelete |
Remove hypervideo |
resourcesAdd |
Upload resource |
resourcesDelete |
Remove resource |
configChange |
Update config |
annotationfileSave |
Save user annotations |
PHP sessions are used for authentication. Session data is stored in $_SESSION['ohv'].
The complete init signature — used when loading data from a PHP server or passing it fully inline:
FrameTrail.init({
// ── Mount point ───────────────────────────────────────────────────────────
target: '#container', // CSS selector or DOM element (default: 'body')
// ── Data sources ──────────────────────────────────────────────────────────
startID: 'hypervideo-id',// ID of the hypervideo to open (skips overview)
config: { /* … */ }, // Inline config object — skips _data/config.json.
// Pass null to load from the server instead.
contents: null, // Pre-loaded hypervideo data (see inline-data pattern)
resources: [{ /* … */ }], // Resource pool definitions (see below)
tagdefinitions: null, // Tag definitions (loaded from server if null)
// Language is configured via config.defaultLanguage (see config option above)
// ── Data / server paths ───────────────────────────────────────────────────
dataPath: null, // Base URL for the _data/ directory. null = auto-detect.
// Caller must include the trailing slash and directory name.
// dataPath: '../_data/' (one level up)
// dataPath: 'https://cdn.example.com/project/_data/'
server: null, // Base URL for the _server/ PHP directory. null = auto-detect
// (probes '_server/ajaxServer.php' on HTTP/HTTPS).
// server: '../_server/' (one level up)
// server: 'https://api.example.com/ft/_server/'
// ── Advanced ──────────────────────────────────────────────────────────────
contentTargets: {} // Custom DOM targets for content views
}, 'PlayerLauncher');Three patterns for initializing a player with a single video, without any _data/ folder or server:
FrameTrail auto-creates a wrapper div immediately before the video element and uses that as the player container. The video's computed width and height are copied to the wrapper so the layout is a seamless replacement.
// HTML: <video id="my-video" src="video.mp4" playsinline=""></video>
FrameTrail.init({
videoElement: '#my-video', // CSS selector or DOM element ref — no target needed
annotations: 'annotations.json', // URL string, array of URLs, or inline W3C objects
config: { defaultLanguage: 'en', autohideControls: true }
}, 'PlayerLauncher');FrameTrail creates a <video> element inside the given container and sources it from the URL.
// HTML: <div id="player"></div>
FrameTrail.init({
target: '#player',
videoSource: 'https://example.com/video.mp4',
annotations: [
'https://example.com/annotations.json', // URL string
{ /* inline W3C Annotation object */ } // or inline object
],
config: { defaultLanguage: 'en' }
}, 'PlayerLauncher');Decorate <video> tags with data-frametrail and call FrameTrail.autoInit() once.
<video data-frametrail
data-frametrail-annotations="annotations.json"
data-frametrail-config='{"autohideControls": true}'
src="video.mp4"
playsinline="">
</video>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialises all [data-frametrail] video elements on the page.
// Pass a DOM element or selector to limit the scan to a subtree:
// FrameTrail.autoInit(document.getElementById('article'));
FrameTrail.autoInit();
});
</script>Supported data attributes:
| Attribute | Maps to | Example |
|---|---|---|
data-frametrail |
presence flag — triggers auto-init | |
data-frametrail-annotations |
annotations |
"path/to/file.json" |
data-frametrail-language |
config.defaultLanguage |
"de" |
data-frametrail-config |
config (inline JSON) |
'{"autohideControls":true}' |
In all three shorthand scenarios:
storageModeis forced to'download'(in-memory, no persistence needed)startIDis set to'0'automatically — the overview is skipped- Overview mode is never shown (Titlebar hides the toggle when only one video is present)
Use these when FrameTrail is loaded from a subdirectory (e.g. examples/) or when the data and PHP backend live at different origins. Both options take a full base URL including the directory name and a trailing slash.
// Page in examples/, FrameTrail installed at parent directory
FrameTrail.init({
dataPath: '../_data/',
server: '../_server/',
// … other options …
}, 'PlayerLauncher');
// PHP backend on a separate server (the remote server must send CORS headers)
FrameTrail.init({
dataPath: 'https://cdn.example.com/project/_data/',
server: 'https://api.example.com/ft/_server/',
// … other options …
}, 'PlayerLauncher');
// Static / CDN hosting — explicit dataPath + no server → storageMode 'static'
// Data is read from the CDN; edits are stored in memory; export via Save As.
FrameTrail.init({
dataPath: 'https://cdn.example.com/project/_data/',
// no server option → StorageManager uses static mode (dataPath present, no PHP probe)
// … other options …
}, 'PlayerLauncher');When both are omitted, StorageManager auto-detects: it probes _server/ajaxServer.php on HTTP(S) (server mode if found, local-folder mode if not), or falls back to the File System Access API on file://.
When server is provided, the PHP backend uses dataPath to resolve the correct _data directory on the filesystem. The client sends the resolved absolute URL path (via StorageAdapterServer.dataPathAbsolute) with every AJAX request. The server validates the path is within the sandbox boundary (parent of _server/) and locks it into the session at login time. All data directories must live under the same root as _server/.
Resolver helpers (available on the RouteNavigation module) provide the resolved URLs throughout the codebase:
resolveDataURL(relativePath)— prependsdataPath(or'_data/'as default)resolveServerURL(relativePath)— prependsserver; returnsnullif no server configuredhasServer()— returnstruewhen aserveris configured
Multiple FrameTrail instances can coexist on one page. Each has completely independent state, modules, and storage. All instances are accessible via FrameTrail.instances:
var ftLeft = FrameTrail.init({ target: '#left', startID: 'id-1' }, 'PlayerLauncher');
var ftRight = FrameTrail.init({ target: '#right', startID: 'id-2' }, 'PlayerLauncher');
// FrameTrail.instances[0] === ftLeft
// FrameTrail.instances[1] === ftRightAccess modules in the browser console:
// Get first instance
var ft = FrameTrail.instances[0];
// Inspect modules
ft.modules();
ft.module('Database').hypervideos;
// Check state
ft.getState();
// Control playback
ft.module('HypervideoController').play();
ft.module('HypervideoController').setCurrentTime(30);