// Copyright © 2011-2025 Cambridge Intelligence Limited.
// All rights reserved.
//
// Sample Code
//! Interact with the time bar to explore keyboard, mouse and touch events.
import KeyLines from "keylines";
import data from "./tbevents-data.js";
let timebar;
const selections = [
{ id: "07086312446-07691640598", index: 0 },
{ id: "07456622368-07252637450", index: 1 },
];
const events = [
"change",
"click",
"context-menu",
"double-click",
"drag-end",
"drag-start",
"end",
"hover",
"key-down",
"key-up",
"pause",
"play",
"pointer-down",
"pointer-move",
"pointer-up",
"start",
"wheel",
];
const ignoreList = ["pointer-move"];
let eventDetails = {};
let eventCounter = 0;
function positionModifierKeys() {
const tbDivRect = document
.getElementById("kltimebar")
.getBoundingClientRect();
const modifierKeysDiv = document.getElementById("modifier-keys");
modifierKeysDiv.style.top = `${tbDivRect.top}px`;
modifierKeysDiv.style.left = `${tbDivRect.left}px`;
modifierKeysDiv.style.opacity = 1;
}
function initialiseInteractions() {
// Create a map of which events should be listened to
const onEvents = new Map(
events
.filter((name) => ignoreList.indexOf(name) === -1)
.map((event) => [event, true])
);
// Get all the elements we wish to be able to modify
const eventsDropdown = document.getElementById("eventsDropdown");
const eventsContainer = document.getElementById("eventsContainer");
const eventsList = document.getElementById("eventsList");
const noEventsButton = document.getElementById("noEvents");
const allEventsButton = document.getElementById("allEvents");
const eventNameDisplay = document.getElementById("eventName");
const eventDetailsPre = document.getElementById("eventDetails");
const prevEventButton = document.getElementById("prevEvent");
const nextEventButton = document.getElementById("nextEvent");
const eventLog = document.getElementById("eventLog");
const eventsTable = document.getElementById("events");
const clearButton = document.getElementById("clear");
// Get the bounds of the event log so can ensure the highlighted event is in view
const {
top: eventLogTop,
bottom: eventLogBottom,
} = eventLog.getBoundingClientRect();
function displayEvent(evtId) {
const details = eventDetails[evtId];
const json = JSON.stringify(details.event, undefined, 2);
eventDetailsPre.innerHTML = json;
eventDetailsPre.dataset.id = evtId;
eventNameDisplay.innerHTML = `: '${details.name}'`;
// Disable the previous / next event buttons if no such event exists
prevEventButton.disabled = !eventDetails[evtId - 1];
nextEventButton.disabled = !eventDetails[evtId + 1];
}
function highlightRow(eventRow) {
[...document.getElementsByClassName("selectedEvent")].forEach((el) => {
el.classList.remove("selectedEvent");
});
eventRow.classList.add("selectedEvent");
// Check if row is visible, if not scroll into view
const distToTop = eventRow.getBoundingClientRect().top - eventLogTop;
if (distToTop < 0) {
eventLog.scrollTop += distToTop;
} else {
const distToBottom =
eventRow.getBoundingClientRect().bottom - eventLogBottom;
if (distToBottom > 0) {
eventLog.scrollTop += distToBottom;
}
}
}
function onRowClick(rowClickEvt) {
const row = rowClickEvt.currentTarget;
highlightRow(row);
displayEvent(parseInt(row.dataset.lastId, 10));
}
function displayNextEvent() {
// Get the ID of the next event
const evtId = parseInt(eventDetailsPre.dataset.id, 10) + 1;
// Check if it is grouped in the currently highlighted row
const currEvtRow = document.getElementsByClassName("selectedEvent")[0];
if (parseInt(currEvtRow.dataset.lastId, 10) < evtId) {
highlightRow(currEvtRow.previousSibling);
}
// Display the details of this event
displayEvent(evtId);
}
function displayPrevEvent() {
// Get the ID of the previous event
const evtId = parseInt(eventDetailsPre.dataset.id, 10) - 1;
// Check if it is grouped in the currently highlighted row
const currEvtRow = document.getElementsByClassName("selectedEvent")[0];
if (parseInt(currEvtRow.dataset.firstId, 10) > evtId) {
highlightRow(currEvtRow.nextSibling);
}
// Display the details of this event
displayEvent(evtId);
}
// Add the HTML for the event filter
events.forEach((event) => {
// Create a new element
const eventLabel = document.createElement("label");
eventLabel.innerHTML = `<input type="checkbox" id="${event}" ${
onEvents.has(event) ? 'checked="checked"' : ""
}"> ${event}`;
const eventLabelinput = eventLabel.querySelector("input");
// Add the on change listener
eventLabel.addEventListener("change", () => {
onEvents.set(event, eventLabelinput.checked);
});
// Make the no events button disable this input
noEventsButton.addEventListener("click", () => {
eventLabelinput.checked = false;
onEvents.set(event, false);
});
// Make the all events button enable this input
allEventsButton.addEventListener("click", () => {
eventLabelinput.checked = true;
onEvents.set(event, true);
});
// Add it to the event list
eventsList.append(eventLabel);
});
clearButton.addEventListener("click", () => {
clearButton.classList.add("hide");
eventsTable.innerHTML = "";
eventNameDisplay.innerHTML = "";
eventDetailsPre.innerHTML = "No event selected";
eventDetailsPre.dataset.id = undefined;
prevEventButton.disabled = true;
nextEventButton.disabled = true;
eventDetails = {};
});
eventsDropdown.addEventListener("click", (event) => {
const classList = eventsContainer.classList;
if (classList.contains("hide")) classList.remove("hide");
else classList.add("hide");
event.stopPropagation();
});
document.addEventListener("click", (event) => {
if (event.target.closest("#eventsContainer") === null) {
eventsContainer.classList.add("hide");
}
});
nextEventButton.addEventListener("click", displayNextEvent);
prevEventButton.addEventListener("click", displayPrevEvent);
// Attach an event handler to all the time bar events
timebar.on("all", (evt) => {
// Update the active modifier keys
if (evt.event && evt.event.modifierKeys) {
Object.keys(evt.event.modifierKeys).forEach((key) => {
if (evt.event.modifierKeys[key]) {
document.getElementById(key).classList.add("active");
} else {
document.getElementById(key).classList.remove("active");
}
});
}
if (onEvents.get(evt.name)) {
// Add this event to the log
const eventObject = {};
if (evt.event) {
Object.keys(evt.event).forEach((prop) => {
if (prop !== "defaultPrevented") eventObject[prop] = evt.event[prop];
});
}
const lastEvt = eventDetails[eventCounter++];
eventDetails[eventCounter] = { name: evt.name, event: eventObject };
// Check if a repeat of the previous event
if (
eventObject &&
lastEvt &&
evt.name === lastEvt.name &&
eventObject.id === lastEvt.event.id
) {
// Group it with the previous event and up the counter for that event
const td = document.getElementsByClassName("eventname")[0];
const rowData = td.parentElement.dataset;
const countSpan = td.children[0];
countSpan.innerText = `(${eventCounter - rowData.firstId + 1})`;
rowData.lastId = eventCounter;
} else {
// Add it to the top of the event log
const tr = document.createElement("tr");
tr.innerHTML = `<td id='${eventCounter}' class='eventname'>'${evt.name}'<span></span></td>`;
tr.dataset.firstId = eventCounter;
tr.dataset.lastId = eventCounter;
tr.onclick = onRowClick;
eventsTable.insertBefore(tr, eventsTable.firstChild);
}
// Ensure the top row is highlighed
highlightRow(
document.getElementsByClassName("eventname")[0].parentElement
);
// Update the displayed event details to show this event
displayEvent(eventCounter);
// Unhide the clear button
clearButton.classList.remove("hide");
}
});
// Hide the modifier keys when open the source tab
[...document.getElementsByClassName("tablinks")].forEach((el) => {
el.addEventListener("click", () => {
document.getElementById("modifier-keys").style.opacity =
el.name === "sourceTab" ? 0 : 1;
});
});
}
async function startKeyLines() {
const options = {
showExtend: true,
};
timebar = await KeyLines.create({
container: "kltimebar",
type: "timebar",
options,
});
await timebar.load(data);
timebar.selection(selections);
timebar.zoom("fit", { animate: false });
// Position the modifier keys overlay on the time bar
positionModifierKeys();
window.addEventListener("resize", positionModifierKeys);
// Deactivate them when the time bar isn't focussed
document.getElementById("kltimebar").addEventListener("focusout", () => {
[...document.getElementsByClassName("key")].forEach((el) =>
el.classList.remove("active")
);
});
initialiseInteractions();
}
window.addEventListener("DOMContentLoaded", startKeyLines);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
<head>
<meta charset="UTF-8">
<title>Time Bar Events</title>
<link rel="stylesheet" type="text/css" href="/css/keylines.css">
<link rel="stylesheet" type="text/css" href="/css/minimalsdk.css">
<link rel="stylesheet" type="text/css" href="/css/sdk-layout.css">
<link rel="stylesheet" type="text/css" href="/css/demo.css">
<link rel="stylesheet" type="text/css" href="/tbevents.css">
<script src="/vendor/jquery.js" defer type="text/javascript"></script>
<script id="keylines" src="/public/keylines.umd.cjs" defer type="text/javascript"></script>
<script src="/tbevents.js" crossorigin="use-credentials" defer type="module"></script>
</head>
<body>
<div class="chart-wrapper demo-cols">
<div class="tab-content-panel flex1" id="lhs" data-tab-group="rhs">
<div class="toggle-content is-visible" id="lhsVisual" style="width:100%; height: 100%;">
<div class="klchart" id="kltimebar">
</div>
</div>
</div>
<div class="rhs citext closed" id="demorhscontent">
<div class="title-bar">
<svg viewBox="0 0 360 60" style="max-width: 360px; max-height: 60px; font-size: 24px; font-family: Raleway; flex: 1;">
<text x="0" y="38" style="width: 100%;">Time Bar Events</text>
</svg>
</div>
<div class="tab-content-panel" data-tab-group="rhs">
<div class="toggle-content is-visible tabcontent" id="controlsTab">
<p>Interact with the time bar using mouse, keyboard and touch.</p>
<form autocomplete="off" onsubmit="return false" id="rhsForm">
<div class="cicontent">
<div id="eventsDropdown">Filter events<i class="icon-chevron-down pull-right"></i></div>
<div class="hide span4" id="eventsContainer">
<div id="eventsList"></div>
<input class="btn" id="noEvents" type="button" value="None">
<input class="btn" id="allEvents" type="button" value="All">
</div>
<fieldset>
<legend>Event details<span id="eventName"></span></legend>
<pre id="eventDetails" style="-webkit-user-select: text; -khtml-user-select: text; -moz-user-select: text; -ms-user-select: text; word-break: keep-all;">No event selected</pre>
<div>
<input class="pull-right" id="nextEvent" type="button" value="Next event" disabled>
<input id="prevEvent" type="button" value="Previous event" disabled>
</div>
</fieldset>
<fieldset>
<legend>Event list</legend>
<div id="eventLog">
<table class="table table-hover table-condensed">
<tbody id="events"></tbody>
</table>
</div>
<input class="btn pull-right hide" type="button" value="Clear" id="clear">
</fieldset>
</div>
</form>
</div>
</div>
</div>
</div>
<div id="moreParent">
<div id="moreContainer">
<div id="modifier-keys" style="position: absolute; pointer-events: none;"><span class="key" id="alt">alt</span><span class="key" id="ctrl">ctrl</span><span class="key" id="consistentCtrl">consistentCtrl</span><span class="key" id="meta">meta</span><span class="key" id="shift">shift</span></div>
<script type="text/html" id="eventsTmpl">{{#events}}
<label class="checkbox">
<input type="checkbox" id="check{{.}}" checked="checked">{{.}}
</label>{{/events}}
</script>
</div>
</div>
<div id="lazyScripts">
</div>
</body>
</html>
#events .eventname{
color: #00aa70;
font-family: Monaco, Andale Mono, Courier New, monospace;
padding: 4px 5px;
font-size: 14px;
cursor: pointer;
}
#eventsDropdown {
border-color: #cacdcf;
border-style: solid;
border-width: 1px;
border-radius: 3px;
padding: 6px;
cursor: pointer;
background-color: #fff;
margin-top: 10px;
}
#eventsDropdown .icon-chevron-down {
width: 12px;
height: 12px;
border-color: #333;
border-style: solid;
border-width: 4px 4px 0px 0px;
transform: rotate(135deg);
margin-right: 5px;
}
#eventsContainer {
width: 368px;
border-width: 1px;
border-color: #cacdcf;
border-top: 0px;
border-style: solid;
border-radius: 3px;
padding-bottom: 8px;
background-color: #ffffff;
position: absolute;
margin-left: 0px;
z-index: 1000;
scrollbar-width: none;
}
#eventsList {
padding: 6px;
width: 100%;
}
#eventsList label {
display: inline-block;
margin: 4px 50px 4px 0px;
width: calc(49% - 50px);
cursor: pointer;
}
#noEvents {
margin-left: 6px;
}
#eventLog {
height:250px;
overflow-y: auto;
margin-bottom: 10px;
clear:both;
border: 1px solid #ccc;
background-color: #fff;
cursor: ns-resize;
}
#eventLog table {
margin-bottom: 5px;
}
#events .selectedEvent {
background-color: #ddd;
}
#eventDetails {
height: 250px;
width: 100%;
overflow-y: auto;
}
#modifier-keys {
opacity: 0;
}
.key {
background-color: #b2df8a;
border: 2px solid #ffffff;
display: inline-block;
color: #5E5E5E;
font: 14px arial;
text-decoration: none;
text-align: center;
margin: 3px 3px;
padding: 6px 6px;
}
.key.active {
border-color: #ffaf00;
background-color: #ffaf00;
}
/* make the time bar fill the area vertically */
.kltimebar {
height: 100%;
}
@media (max-width: 979px) {
#eventsContainer {
width: 326px;
}
#eventsList label {
margin-right: 11px;
width: calc(49% - 11px);
}
}
@media (max-width: 767px) {
#eventsContainer {
width: 266px;
}
#eventsList label {
margin-right: 30px;
width: calc(49% - 30px);
}
}
Loading source
