This guide explains how to extend FrameTrail with new resource types, modules, localization, themes, and backend actions.
Resource types define how different media (images, videos, social embeds, etc.) are displayed and edited in FrameTrail. All resource types inherit from the base Resource type.
Create src/_shared/types/ResourceMyType/type.js:
/**
* @module Shared
*/
/**
* I am the type definition of ResourceMyType.
*
* @class ResourceMyType
* @category TypeDefinition
* @extends Resource
*/
FrameTrail.defineType(
'ResourceMyType',
function(FrameTrail) {
return {
parent: 'Resource',
constructor: function(resourceData) {
this.resourceData = resourceData;
},
prototype: {
/**
* I render the resource content for display in the player.
* @method renderContent
* @return {HTMLElement}
*/
renderContent: function() {
var self = this;
var element = document.createElement('div');
element.className = 'resourceDetail';
element.dataset.type = 'mytype';
element.innerHTML = '<div class="myTypeContent"><!-- Your content here --></div>';
this.initializeContent(element);
return element;
},
/**
* I render a thumbnail for the resource manager.
* @method renderThumb
* @param {String} id
* @return {HTMLElement}
*/
renderThumb: function(id) {
var self = this;
var trueID = id || FrameTrail.module('Database').getIdOfResource(this.resourceData);
var thumbBackground = this.resourceData.thumb
? 'background-image: url(' + FrameTrail.module('RouteNavigation').getResourceURL(this.resourceData.thumb) + ');'
: '';
var tagList = (this.resourceData.tags ? this.resourceData.tags.join(' ') : '');
var thumbElement = document.createElement('div');
thumbElement.className = 'resourceThumb ' + tagList;
thumbElement.dataset.licenseType = this.resourceData.licenseType;
thumbElement.dataset.resourceid = trueID;
thumbElement.dataset.type = this.resourceData.type;
if (thumbBackground) { thumbElement.style.cssText = thumbBackground; }
thumbElement.innerHTML = '<div class="resourceOverlay">'
+ ' <div class="resourceIcon"><span class="icon-mytype"></span></div>'
+ '</div>'
+ '<div class="resourceTitle">' + this.resourceData.name + '</div>';
var previewButton = document.createElement('div');
previewButton.className = 'resourcePreviewButton';
previewButton.innerHTML = '<span class="icon-eye"></span>';
previewButton.addEventListener('click', function(evt) {
self.openPreview(thumbElement);
evt.stopPropagation();
evt.preventDefault();
});
thumbElement.appendChild(previewButton);
return thumbElement;
},
/**
* I render property controls for overlay editing.
* @method renderPropertiesControls
* @param {Overlay} overlay
* @return {Object}
*/
renderPropertiesControls: function(overlay) {
var basicControls = this.renderBasicPropertiesControls(overlay);
// Add custom controls if needed
var customControl = document.createElement('div');
customControl.className = 'customControl';
customControl.innerHTML = '<label>Custom Setting</label>'
+ '<input type="text" value="' + (overlay.data.attributes.customSetting || '') + '">';
customControl.querySelector('input').addEventListener('change', function() {
overlay.data.attributes.customSetting = this.value;
FrameTrail.module('HypervideoModel').newUnsavedChange('overlays');
});
basicControls.controlsContainer.querySelector('#OverlayOptions').appendChild(customControl);
return basicControls;
},
/**
* I render time controls for annotation editing.
* @method renderTimeControls
* @param {Annotation} annotation
* @return {Object}
*/
renderTimeControls: function(annotation) {
return this.renderBasicTimeControls(annotation);
},
initializeContent: function(element) {
// Load external scripts, initialize plugins, etc.
}
}
};
}
);Create src/_shared/types/ResourceMyType/style.css:
/* Resource detail (displayed in player) */
.resourceDetail[data-type="mytype"] {
width: 100%;
height: 100%;
}
.resourceDetail[data-type="mytype"] .myTypeContent {
/* Your styles */
}
/* Thumbnail in resource manager */
.resourceThumb[data-type="mytype"] .resourceOverlay {
background: rgba(0, 0, 0, 0.3);
}
/* Annotation tile icon */
.tileElement[data-type="mytype"] [class^="icon-"]::before {
content: '\e800'; /* Your icon code */
}
/* Edit properties panel icon */
.editPropertiesContainer .propertiesTypeIcon[data-type="mytype"] [class^="icon-"]::before {
content: '\e800';
}Add the CSS and JS to src/index.html and src/resources.html:
<!-- In the CSS section (after other type styles) -->
<link rel="stylesheet" type="text/css" href="_shared/types/ResourceMyType/style.css">
<!-- In the JS section (after Resource/type.js, with other resource types) -->
<script type="text/javascript" src="_shared/types/ResourceMyType/type.js"></script>Add entries to scripts/build.sh in the appropriate arrays:
# In CSS_FILES (after other resource type styles)
"_shared/types/ResourceMyType/style.css"
# In JS_FILES (after _shared/types/Resource/type.js, with other resource types)
"_shared/types/ResourceMyType/type.js"If your resource type should be creatable via the Resource Manager, update src/_shared/modules/ResourceManager/module.js to include your type in the add resource dialog.
If your resource type involves file uploads, update src/_server/files.php to handle the new file type.
/**
* @module Player
*/
/**
* I am MyCustomModule.
*
* @class MyCustomModule
* @static
*/
FrameTrail.defineModule('MyCustomModule', function(FrameTrail) {
var labels = FrameTrail.module('Localization').labels;
// Private variables
var domElement = null;
var isActive = false;
// Private methods
function create() {
domElement = document.createElement('div');
domElement.className = 'myCustomModule';
// Build UI...
}
function destroy() {
if (domElement) {
domElement.remove();
domElement = null;
}
}
// State change handlers
function onEditModeChange(newValue, oldValue) {
if (newValue) {
domElement.classList.add('active');
} else {
domElement.classList.remove('active');
}
}
function onUnload() {
destroy();
}
// Initialize on module load
create();
// Public interface
return {
get element() { return domElement; },
get isActive() { return isActive; },
onChange: {
'editMode': onEditModeChange
},
onUnload: onUnload
};
});-
Create files:
src/player/modules/MyCustomModule/module.jsand optionallystyle.css -
Register in HTML: Add
<script>and<link>tags tosrc/index.html -
Add to build script: Add to
JS_FILESandCSS_FILESinscripts/build.sh -
Initialize: In the appropriate launcher or parent module:
FrameTrail.initModule('MyCustomModule');
-
Use from other modules:
var myModule = FrameTrail.module('MyCustomModule');
Edit src/_shared/modules/Localization/locale/en.js:
window.FrameTrail_L10n['en'] = {
// ... existing strings ...
"MyModuleTitle": "My Module",
"MyModuleDescription": "Description of my module"
};Edit src/_shared/modules/Localization/locale/de.js (and others):
window.FrameTrail_L10n['de'] = {
// ... existing strings ...
"MyModuleTitle": "Mein Modul",
"MyModuleDescription": "Beschreibung meines Moduls"
};var labels = FrameTrail.module('Localization').labels;
var title = labels['MyModuleTitle'];Edit src/_shared/styles/variables.css:
.frametrail-body[data-frametrail-theme="mytheme"] :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="mytheme"] {
/* Required: the 4 core colors */
--primary-bg-color: #your-bg;
--secondary-bg-color: rgba(r, g, b, .6); /* keep semi-transparent */
--primary-fg-color: #your-text;
--secondary-fg-color: #your-secondary-text;
/*
* These are computed automatically from the 4 above — override only if needed:
* --semi-transparent-bg-color (primary-bg at 80%)
* --semi-transparent-fg-color (primary-fg at 30%)
* --semi-transparent-fg-highlight-color (primary-fg at 40%)
*
* These defaults are shared across all built-in themes — override if needed:
* --annotation-preview-bg-color: rgba(100, 100, 100, .2)
* --highlight-color: #D8D3AD
* --tooltip-bg-color: #D8D3AD
* --video-background-color: #000
*/
}The theme selector in HypervideoSettingsDialog automatically picks up themes defined in CSS.
Edit src/_server/ajaxServer.php:
case "myCustomAction":
require_once("myCustomModule.php");
$return = myCustomFunction($_POST["param1"], $_POST["param2"]);
break;Create src/_server/myCustomModule.php:
<?php
require_once("./config.php");
require_once("./user.php");
function myCustomFunction($param1, $param2) {
global $conf;
$login = userCheckLogin("user");
if ($login["code"] != 1) {
return array(
"status" => "fail",
"code" => 1,
"string" => "Not logged in"
);
}
// Your logic here...
return array(
"status" => "success",
"code" => 0,
"response" => $result
);
}// Via the FrameTrail Database module (preferred — uses resolveServerURL automatically)
FrameTrail.module('Database').ajax('myCustomAction', {
param1: 'value1',
param2: 'value2'
}, function(response) {
if (response.status === 'success') {
console.log(response.response);
}
});
// Or directly via fetch
fetch('_server/ajaxServer.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ a: 'myCustomAction', param1: 'value1', param2: 'value2' })
}).then(function(r) { return r.json(); }).then(function(response) {
if (response.status === 'success') {
console.log(response.response);
}
});FrameTrail.triggerEvent('myCustomEvent', {
timestamp: Date.now(),
data: 'some value'
});FrameTrail.addEventListener('myCustomEvent', function(event) {
console.log('Received:', event.detail);
});Users can add JavaScript that runs at specific lifecycle points via the globalEvents field in hypervideo.json:
{
"globalEvents": {
"onReady": "console.log('Hypervideo ready');",
"onPlay": "console.log('Playing');",
"onPause": "console.log('Paused');",
"onEnded": "console.log('Ended');"
}
}When making changes that should be undoable:
FrameTrail.module('UndoManager').register({
category: 'overlays',
description: 'Change overlay position',
undo: function() {
overlay.data.position = previousPosition;
overlay.updatePosition();
},
redo: function() {
overlay.data.position = newPosition;
overlay.updatePosition();
}
});
FrameTrail.module('HypervideoModel').newUnsavedChange('overlays');When adding a new resource type or module, make sure to:
- Create
type.js(ormodule.js) andstyle.cssin the appropriate directory undersrc/ - Add
<script>and<link>tags tosrc/index.html(andsrc/resources.htmlif applicable) - Add entries to
scripts/build.shinJS_FILESandCSS_FILESarrays (in correct order) - Add localization strings to
src/_shared/modules/Localization/locale/en.jsandde.js - Test in both development mode (
src/) and build mode (build/) - Test in Chrome and Firefox
- Test with edit mode enabled and disabled
- Follow existing patterns — Look at similar modules/types for guidance
- Use localization — Never hardcode user-facing strings
- Clean up on unload — Remove event listeners and DOM elements in
onUnload - Use CSS custom properties — For theme compatibility
- Test edge cases — Empty data, missing resources, network errors