/**
* bluebox.js - A webcomponent to display timelines
*
* Copyright (c) 2019, Luis Panadero Guardeño <luis.panadero(at)gmail.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
import cssText from 'bundle-text:../scss/bluebox.scss';
import htmlText from 'bundle-text:../html/bluebox.html';
import TimeUnit from './timeunit.jsm';
import { ChangeSelectedIndexEvent } from './events.jsm';
const SVGNS = "http://www.w3.org/2000/svg";
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
const YYYYMMDD_DATE_REGEX = /^-?\d{4}-\d{2}-\d{2}$/;
const YYYYMM_DATE_REGEX = /^-?\d{4}-\d{2}$/;
/**
* BlueBox Timeline Webcomponent
* @extends HTMLElement
*/
class BlueBoxTl extends HTMLElement {
/**
* @typedef {Object} EventData
* @property {number} start - Start date of the event, referred in the configured TimeUnit
* @property {number} [end] - End date of the event, referred in the configured TimeUnit
* @property {String} title - Title of the event
*/
/**
* @private
* @typedef {Object} MarkConfig
* @property {number} threshold
* @property {number} step
*/
/**
* @private
* @typedef {Object} EventBundleElements
* @property {HTMLElement} eventElement
* @property {HTMLElement} eventVerticalLineElement
* @property {HTMLElement} eventRangeElement
*/
#init = false;
#shadowRoot;
#styleElement;
#eventsContainer;
/** @type EventBundleElement[] */
#eventElements = [];
#currentTimeElement;
#currentTimeTextElement;
#marks = [];
#timeMarksContainer;
#zoomInButton;
#zoomOutButton;
#dataset = [];
// Config
#formatDateFun;
#dateTimeFormatOptions;
/** @type MarkConfig[] */
#marksConfig = [];
// Time intervals
#startTime;
#endTime;
#maxTimeInterval;
// Visible time window
#timeCenter;
#startWindow;
#endWindow;
#timeWindow;
// Drag events stuff
#dragging = false;
#initialPointerDrag;
/**
* Builds a instance of BlueBoxTl
*/
constructor() {
super();
this.#shadowRoot = this.attachShadow({mode: 'closed'});
this.#shadowRoot.innerHTML = htmlText;
this.#styleElement = document.createElement('style');
this.#styleElement.innerText = cssText;
this.#shadowRoot.appendChild(this.#styleElement);
this.#hydrateToolbar();
this.#hydrateContainer();
this.#shadowRoot.appendChild(this.#buildCurrentTimeElement());
}
// Hydrates the toolbar
#hydrateToolbar() {
this.#zoomInButton = this.#shadowRoot.getElementById('zoom-in');
this.#zoomInButton.addEventListener('click', this.#zoomButtonsEventListener.bind(this));
this.#zoomOutButton = this.#shadowRoot.getElementById('zoom-out');
this.#zoomOutButton.addEventListener('click', this.#zoomButtonsEventListener.bind(this));
}
// Hydrates the main container of time events
#hydrateContainer() {
this.#eventsContainer = this.#shadowRoot.getElementById('container');
this.addEventListener('pointerdown', this.#pointerDownEventListener.bind(this));
this.addEventListener('pointerup', this.#pointerUpEventListener.bind(this));
this.addEventListener('pointermove', this.#pointerMoveEventListener.bind(this));
this.addEventListener('keydown', this.#keyDownEventListener.bind(this));
this.#eventsContainer.appendChild(this.#buildTimeMarksSVG());
}
/**
* Browser calls this method when the element is added to the document
* (can be called many times if an element is repeatedly added/removed)
*/
connectedCallback() {
const scriptDataElement = this.querySelector('script[type="application/json"]');
if (scriptDataElement) {
this.dataset = JSON.parse(scriptDataElement.textContent, this.#reviveJsonData);
}
}
/**
* Load the dataset from an JSON on a url
* @param {string|URL} src - URL where fetch the dataset
* @param {Object} options - fetch() options
* @return {Promise} A promise with the parse json
*/
fetch(src, options = {}) {
return fetch(src, options)
.then(response => {
if (!response.ok) {
throw new Error(`HHTP error! Status: ${response.status}`);
}
return response.text();
})
.then(rawText => JSON.parse(rawText, this.#reviveJsonData.bind(this)))
.then(json => {
this.dataset = json;
return json;
});
}
#reviveJsonData(key, value) {
if (key !== 'start' && key !== 'end') {
return value;
}
// Matches strings like "2022-08-25T09:39:19.288Z"
if (typeof value === 'string'
&& (ISO_DATE_REGEX.test(value) || YYYYMMDD_DATE_REGEX.test(value) || YYYYMM_DATE_REGEX.test(value) )) {
return this.timeUnit.convertFromDate(new Date(value));
} else {
return value;
}
}
// Builds the SVG that shows the timeline
#buildTimeMarksSVG() {
this.#timeMarksContainer = document.createElementNS(SVGNS, "svg")
this.#timeMarksContainer.setAttribute("class", 'time-marks');
this.#timeMarksContainer.ariaHidden = "true";
const defs = document.createElementNS(SVGNS, "defs");
const tockMark = document.createElementNS(SVGNS, "rect");
tockMark.id = "tock"
tockMark.class = "tock-mark__line";
tockMark.setAttribute("width", "1px");
tockMark.setAttribute("height", "50%");
tockMark.setAttribute("x", "0");
tockMark.setAttribute("y", "0");
defs.appendChild(tockMark);
const pattern = document.createElementNS(SVGNS, "pattern");
pattern.setAttribute("id", "stripes");
pattern.setAttribute("width", "5px");
pattern.setAttribute("height", "1");
pattern.setAttribute("patternUnits", "userSpaceOnUse");
const pRect = document.createElementNS(SVGNS, "rect");
pRect.setAttribute("width", "1");
pRect.setAttribute("height", "2");
pRect.setAttribute("stroke", "none");
pattern.appendChild(pRect);
defs.appendChild(pattern);
this.#timeMarksContainer.appendChild(defs);
const rect = document.createElementNS(SVGNS, "rect");
rect.setAttribute("id", "background");
rect.setAttribute("width", "100%");
rect.setAttribute("stroke", "none");
rect.setAttribute("fill", "url(#stripes)");
this.#timeMarksContainer.appendChild(rect);
return this.#timeMarksContainer;
}
#buildCurrentTimeElement() {
this.#currentTimeElement = document.createElement('span');
this.#currentTimeElement.className = 'current-time';
this.#currentTimeTextElement = document.createElement('span');
this.#currentTimeTextElement.className = 'current-time__text';
this.#currentTimeElement.appendChild(this.#currentTimeTextElement);
const currentTimeTickElement = document.createElement('span');
currentTimeTickElement.className = 'current-time__tick';
currentTimeTickElement.role = "img";
currentTimeTickElement.ariaHidden = true;
this.#currentTimeElement.appendChild(currentTimeTickElement);
return this.#currentTimeElement;
}
/**
* browser calls this method when the element is removed from the document
* (can be called many times if an element is repeatedly added/removed)
*/
disconnectedCallback() {
}
/**
* Array of attrbutes names to monitor for changes
*/
static get observedAttributes() {
return [
'selected-index', 'time-center', 'zoom-factor', 'date-time-format', 'src',
// 'max-zoom-factor', 'min-zoom-factor', 'max-rows', 'locale', 'lang'
];
}
/**
* Called when an atribute it's modified
*/
attributeChangedCallback(attr, oldVal, newVal) {
if (oldVal == newVal || this.#init == false && attr !== 'src') {
return;
}
// called when one of attributes listed above is modified
switch (attr) {
case 'selected-index':
if (newVal) {
this.#updateSelectedIndex(newVal);
}
case 'time-center':
case 'zoom-factor':
if (newVal) {
this.#repositionContainer();
this.#recalculateHorizontal();
}
break;
case 'date-time-format':
if (newVal) {
this.#updateDateTimeFormat(newVal);
}
break;
case 'src':
if (newVal) {
this.fetch(newVal);
}
break;
};
}
adoptedCallback() {
// called when the element is moved to a new document
// (happens in document.adoptNode, very rarely used)
}
#initialize() {
if (!this.#init) {
if (this.#isDebugEnabled()) {
console.debug(this.#logId(), "Initializing");
console.debug(this.#logId(), "Dataset: ", this.#dataset);
}
this.#initializeAttributes();
this.#initializeTimeVariables();
this.#createEventsAndLines();
this.#init = true;
this.#updateSelectedIndex();
this.#repositionContainer();
this.#recalculateHorizontal();
if (this.#isDebugEnabled()) {
console.debug(this.#logId(), "current pixels per timeunit", this.timeUnitsPerPixels);
}
}
}
/**
* Login helper
* @returns {string} "[ID]" or empty string if the element has not id
*/
#logId() {
return this.id ? `[${this.id}]` : ' ';
}
#initializeAttributes() {
if ( this.getAttribute('tabindex') == null) {
this.setAttribute('tabindex', 0)
}
if ( this.selectedIndex == null) {
this.selectedIndex = -1;
}
if ( this.minZoomFactor == null ) {
this.minZoomFactor = 0.5;
}
if ( this.maxZoomFactor == null ) {
this.maxZoomFactor = 16;
}
if ( this.zoomFactor == null || Number.isNaN(this.zoomFactor) ) {
// Zoom factor 1, show all elements on the timeline
this.zoomFactor = 1;
}
if ( this.maxRows == null ) {
this.maxRows = 6;
}
if ( this.dateTimeFormat == null || this.dateStyle == "" ) {
this.dateTimeFormat = JSON.stringify(this.#dateTimeFormatOptions);
}
if ( this.locale == null || this.locale == "" ) {
if (this.getAttribute('lang')) {
this.locale = this.getAttribute('lang');
} else {
this.locale = this.#getBrowserLocale();
}
}
this.#formatDateFun = this.#formatDateFun || this.timeUnit.defaultFormatFunction(this.locale, this.#dateTimeFormatOptions);
for (let i = 1 ; i <= 5 ; i++) {
const markConfig = {threshold: Number.MAX_VALUE};
if (this.hasAttribute(`mark-threshold-${i}`)) {
markConfig.threshold = new Number(this.getAttribute(`mark-threshold-${i}`));
}
if (this.hasAttribute(`mark-step-${i}`)) {
markConfig.step = new Number(this.getAttribute(`mark-step-${i}`));
}
if (markConfig.step) {
this.#marksConfig.push(markConfig);
}
}
if (this.#isDebugEnabled()) {
console.debug(this.#logId(), "Loaded time marks config: ", this.#marksConfig);
}
}
#initializeTimeVariables() {
this.#startTime = Math.min.apply(null, this.#dataset.map(function (timeEvent) { return timeEvent.start }));
this.#endTime = Math.max.apply(null, this.#dataset.map(function (timeEvent) { return (timeEvent.end in timeEvent) ? timeEvent.end : timeEvent.start }));
this.#maxTimeInterval = this.#endTime - this.#startTime;
this.#timeCenter = (this.#maxTimeInterval / 2) + this.#startTime;
this.#currentTimeTextElement.innerText = this.#formatDateFun(this.zoomFactor, this.#timeCenter);
if (this.#isDebugEnabled()) {
console.debug(this.#logId(), "Start time: ", this.#startTime, "End time: ", this.#endTime);
}
}
#createEventsAndLines() {
// We add the vertical line and the time events, and store on arrays to allow to do a quick&easy access to it
for(const [index, timeEvent] of this.#dataset.entries()) {
const eventVerticalLineElement = this.#createEventVerticalLineElement(index, timeEvent);
this.#eventsContainer.appendChild(eventVerticalLineElement);
const eventElement = this.#createEventElement(index, timeEvent);
this.#eventsContainer.appendChild(eventElement);
const eventRangeElement = this.#createRangeElement(index, timeEvent);
this.#eventsContainer.appendChild(eventRangeElement);
const eventBundleElement = {
'eventElement': eventElement,
'eventVerticalLineElement' : eventVerticalLineElement,
'eventRangeElement': eventRangeElement
};
this.#eventElements.push(eventBundleElement);
};
}
#createEventElement(index, timeEvent) {
const start = timeEvent.start;
const eventElement = document.createElement('button');
eventElement.className = "event";
eventElement.role = "tab";
eventElement.tabIndex = -1;
eventElement.innerHTML = timeEvent.title || (index + "");
const relativeTime = (timeEvent.start - this.#startTime) * 100 / this.#maxTimeInterval;
eventElement.style.left = relativeTime + '%';
eventElement.style.top = 0;
// We attach an event listener to a time event. So an user selecting a timevent, changes the current selected time event
eventElement.addEventListener('click', (evt) => {
this.selectedIndex = index;
});
eventElement.addEventListener('mouseenter', (evt) => {
this.#triggerHoverOnEvent(index, true);
});
eventElement.addEventListener('mouseleave', (evt) => {
this.#triggerHoverOnEvent(index, false);
});
return eventElement;
}
#triggerHoverOnEvent(index, entering) {
if (entering) {
const eventBundleElement = this.#eventElements[index];
if (eventBundleElement) {
eventBundleElement.eventRangeElement.classList.add('event-range--hover');
}
} else {
for (const eventBundleElement of this.#eventElements) {
eventBundleElement.eventRangeElement.classList.remove('event-range--hover');
}
}
}
#createEventVerticalLineElement(index, timeEvent) {
const start = timeEvent.start;
const eventVerticalLineElement = document.createElement('span');
eventVerticalLineElement.className = "event-vertical-line";
eventVerticalLineElement.role = "img";
eventVerticalLineElement.ariaHidden = "true";
const relativeTime = (start - this.#startTime) * 100 / this.#maxTimeInterval;
eventVerticalLineElement.style.left = relativeTime + '%';
return eventVerticalLineElement;
}
#createRangeElement(index, timeEvent) {
const start = timeEvent.start;
const end = timeEvent.end || timeEvent.start;
const eventRangeElement = document.createElement('span');
eventRangeElement.className = "event-range";
eventRangeElement.role = "img";
eventRangeElement.ariaHidden = "true";
const startRelativeTime = (start - this.#startTime) * 100 / this.#maxTimeInterval;
eventRangeElement.style.left = startRelativeTime + '%';
const endRelativeTime = 100 - ((end - this.#startTime) * 100 / this.#maxTimeInterval);
eventRangeElement.style.right = endRelativeTime + '%';
const eventRangeStartElement = document.createElement('span');
eventRangeStartElement.className = "event-range__start";
eventRangeStartElement.role = "img";
eventRangeElement.appendChild(eventRangeStartElement)
return eventRangeElement;
}
/**
* Does the changes when the selected index changes to another
* @fires ChangeSelectedIndexEvent
*/
#updateSelectedIndex() {
if (this.#dataset[this.selectedIndex]) {
this.#timeCenter = this.#dataset[this.selectedIndex].start;
this.#currentTimeTextElement.innerText = this.#formatDateFun(this.zoomFactor, this.#timeCenter);
// Updated the current event elements
for (const eventBundleElements of this.#eventElements) {
eventBundleElements.eventVerticalLineElement.classList.remove('event-vertical-line--current');
eventBundleElements.eventElement.classList.remove('event--current');
eventBundleElements.eventElement.ariaSelected = false;
eventBundleElements.eventRangeElement.classList.remove('event-range--current');
}
// Updated the current event element
const currentEventElements = this.#eventElements[this.selectedIndex];
currentEventElements.eventVerticalLineElement.classList.add('event-vertical-line--current');
currentEventElements.eventElement.classList.add('event--current');
currentEventElements.eventElement.ariaSelected = true;
currentEventElements.eventRangeElement.classList.add('event-range--current');
this.dispatchEvent(new ChangeSelectedIndexEvent(parseInt(this.selectedIndex), this.dataset[this.selectedIndex] || null));
} else if (this.selectedIndex != -1) {
console.log(this.#logId(), "Not existing index");
}
}
#updateDateTimeFormat(newVal) {
if (newVal) {
try {
this.#dateTimeFormatOptions = JSON.parse(newVal);
this.#recalcTimeMarks();
} catch (error) {
console.error(this.#logId(), error, newVal);
}
}
}
// Disabled/Enables zoom toolbar buttons
#setToolbarButtonsDisable() {
if (this.#init) {
if (this.zoomFactor <= this.minZoomFactor) {
this.#zoomOutButton.disabled = true;
} else {
this.#zoomOutButton.disabled = false;
}
if (this.zoomFactor >= this.maxZoomFactor) {
this.#zoomInButton.disabled = true;
} else {
this.#zoomInButton.disabled = false;
}
}
}
#repositionContainer() {
if (this.#init) {
this.#timeWindow = this.#maxTimeInterval / this.zoomFactor;
const halfWindow = parseInt(this.#timeWindow) / 2;
const centerWindow = this.#timeCenter;
this.#startWindow = centerWindow - halfWindow;
this.#endWindow = centerWindow + halfWindow;
if (this.#isDebugEnabled() && this.debug == "trace") {
console.trace(this.#logId(), 'Window: ', this.#startWindow, '->', this.#endWindow, ' Time center: ', centerWindow);
}
const containerLeft = (-(this.#startWindow - this.#startTime) / this.#timeWindow) * this.clientWidth;
this.#eventsContainer.style.transform = `translateX(${containerLeft}px)`;
}
}
#recalculateHorizontal() {
if (this.#init) {
const containerWidth = 100 * (this.#maxTimeInterval / this.#timeWindow);
this.#eventsContainer.style.width = containerWidth + "%";
this.#recalcEventsTop();
this.#recalcTimeMarks();
}
}
#recalcEventsTop() {
if (this.#init) {
for(const [index, eventElement] of this.#eventElements.entries()) {
this.#recalcEventTop(index, eventElement.eventElement);
}
}
}
#recalcEventTop(index, eventElement) {
const gap = getComputedStyle(this).getPropertyValue('--row-gap');
if (index != 0) {
const previous = this.#eventElements[index - 1].eventElement;
if (previous.getBoundingClientRect().right > eventElement.getBoundingClientRect().left ) {
eventElement.dataset.row = parseInt(previous.dataset.row || 0, 10) + 1;
if ( parseInt(eventElement.dataset.row, 10) >= this.maxRows) {
eventElement.dataset.row = 0;
}
} else {
eventElement.dataset.row = 0;
}
const height = eventElement.getBoundingClientRect().height;
eventElement.style.top = `calc(${eventElement.dataset.row} * (${height}px + ${gap}))`;
}
}
#recalcTimeMarks() {
if ( this.#timeMarksContainer && this.#marksConfig.length > 0) {
if (this.#isDebugEnabled() && this.debug == "trace") {
console.debug(this.#logId(), "Recalculing time marks", "Current timeUnits per pixel", this.timeUnitsPerPixels);
}
for (const oldTockMark of this.#timeMarksContainer.querySelectorAll(".tock-mark")) {
oldTockMark.remove();
}
this.#marks.length = 0;
for (let markConfig of this.#marksConfig) {
if (this.timeUnitsPerPixels <= markConfig.threshold) {
// Start of the marks rounded to the steps. For example if the step it's setup for 10 years and the dataset begins at year 1911, it will be round to 1910
const startTime = Math.floor(this.#startTime / markConfig.step) * markConfig.step;
for (let t = startTime ; t < this.#endTime ; t += markConfig.step ) {
if (this.#marks.indexOf(t) == -1) {
const relativeTime = (t - this.#startTime) * 100 / this.#maxTimeInterval;
const group = document.createElementNS(SVGNS, "g");
group.setAttribute("class", "tock-mark");
const use = document.createElementNS(SVGNS, "use");
use.setAttribute("href", "#tock");
use.setAttribute("y", "0");
use.setAttribute("x", relativeTime + '%');
group.appendChild(use);
const text = document.createElementNS(SVGNS, "text");
text.setAttribute("class", "tock-mark__text");
text.setAttribute("text-anchor", "middle");
text.setAttribute("y", "100%");
text.setAttribute("x", relativeTime + '%');
const textNode = document.createTextNode(this.#formatDateFun(this.zoomFactor, t));
text.appendChild(textNode);
group.appendChild(text);
this.#marks.push(t);
this.#timeMarksContainer.appendChild(group);
}
}
}
}
}
}
// Gets browser current locale
#getBrowserLocale() {
return (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.language;
}
// Event listeners handling dragging
#pointerDownEventListener(evt) {
this.#dragging = true;
this.#initialPointerDrag = {clientX: evt.clientX, clientY: evt.clientY};
}
#pointerUpEventListener(evt) {
this.#dragging = false;
}
#pointerMoveEventListener(evt) {
if (this.#dragging) {
const deltaX = evt.clientX - this.#initialPointerDrag.clientX;
const deltaY = evt.clientY - this.#initialPointerDrag.clientY;
this.#initialPointerDrag = {clientX: evt.clientX, clientY: evt.clientY};
this.#timeCenter = this.#timeCenter - (deltaX * this.timeUnitsPerPixels);
this.#currentTimeTextElement.innerText = this.#formatDateFun(this.zoomFactor, this.#timeCenter);
this.#repositionContainer();
}
}
/**
* Current Time Unit per pixels . Aka, a screen pixel represent X Time Units
* @type number
*/
get timeUnitsPerPixels() {
return this.#maxTimeInterval / this.#eventsContainer.offsetWidth;
}
// Event listener handling zoom in/out
#keyDownEventListener(evt) {
if (!evt.isComposing ) {
if (evt.key == '+' ) {
this.increaseZoomFactor();
evt.preventDefault();
} else if (evt.key == '-') {
this.decreaseZoomFactor();
evt.preventDefault();
} else if (evt.key == 'ArrowLeft') {
this.selectPrevious();
evt.preventDefault();
} else if (evt.key == 'ArrowRight') {
this.selectNext()
evt.preventDefault();
} else if (evt.key == 'Home') {
this.selectFirst();
evt.preventDefault();
} else if (evt.key == 'End') {
this.selectLast();
evt.preventDefault();
}
}
}
/**
* Selectes the previous time event
* @fires ChangeSelectedIndexEvent
*/
selectPrevious() {
if (this.selectedIndex || this.selectedIndex == "-1") {
const currentIndex = parseInt(this.selectedIndex);
if (currentIndex == -1) {
this.selectedIndex = 0;
} else if (currentIndex > 0) {
this.selectedIndex = currentIndex - 1;
} else {
this.selectedIndex = this.dataset.length - 1;
}
}
}
/**
* Selects the next time event
* @fires ChangeSelectedIndexEvent
*/
selectNext() {
if (this.selectedIndex || this.selectedIndex == "-1") {
const currentIndex = parseInt(this.selectedIndex);
if (currentIndex == -1) {
this.selectedIndex = 0;
} else if (currentIndex < (this.dataset.length - 1)) {
this.selectedIndex = currentIndex + 1;
} else {
this.selectedIndex = 0;
}
}
}
/**
* Selects the first time event
* @fires ChangeSelectedIndexEvent
*/
selectFirst() {
this.selectedIndex = 0;
}
/**
* Selects the last time event
* @fires ChangeSelectedIndexEvent
*/
selectLast() {
this.selectedIndex = this.dataset.length - 1;
}
// event listener handling the toolbar zoom buttons
#zoomButtonsEventListener(evt) {
if (evt.currentTarget.className === 'zoom-in' ) {
this.increaseZoomFactor();
} else if (evt.currentTarget.className === 'zoom-out' ) {
this.decreaseZoomFactor();
}
}
/**
* Increases Zoom factor by 2
*/
increaseZoomFactor() {
let newZoomFactor = this.zoomFactor;
newZoomFactor = Math.min(newZoomFactor * 2, this.maxZoomFactor);
this.zoomFactor = newZoomFactor;
}
/**
* Decreases Zoom factor by 2
*/
decreaseZoomFactor() {
let newZoomFactor = this.zoomFactor;
newZoomFactor = Math.max(newZoomFactor / 2, this.minZoomFactor);
this.zoomFactor = newZoomFactor;
}
/**
* Dataset of the timeline
* @type EventData[]
*/
set dataset(value) {
this.#dataset = value;
this.#initialize();
}
get dataset() {
return this.#dataset;
}
get selectedIndex() {
return this.getAttribute('selected-index');
}
/**
* @fires ChangeSelectedIndexEvent
*/
set selectedIndex(value) {
if (this.#dataset[value] || value == -1) {
this.setAttribute('selected-index', value);
}
}
/**
* Max number of rows
*/
get maxRows() {
return this.getAttribute('max-rows');
}
set maxRows(value) {
this.setAttribute('max-rows', value);
}
/**
* Current zoom factor
*/
get zoomFactor() {
return parseFloat(this.getAttribute('zoom-factor'));
}
set zoomFactor(value) {
this.setAttribute('zoom-factor', value);
this.#setToolbarButtonsDisable();
}
/**
* Max zoom factor
*/
get maxZoomFactor() {
return this.getAttribute('max-zoom-factor');
}
set maxZoomFactor(value) {
this.setAttribute('max-zoom-factor', value)
}
/**
* Minimal zoom factor
*/
get minZoomFactor() {
return this.getAttribute('min-zoom-factor');
}
set minZoomFactor(value) {
this.setAttribute('min-zoom-factor', value)
}
/**
* Intl.DateTimeFormat configuration object
* @type Object
*/
get dateTimeFormat () {
return this.getAttribute('date-time-format');
}
set dateTimeFormat (value) {
return this.setAttribute('date-time-format', value);
}
/**
* Locale used to formate dates and times
*/
get locale () {
return this.getAttribute('locale');
}
set locale (value) {
return this.setAttribute('locale', value);
}
/**
* Togles debug mode
* @type (boolean|string)
*/
get debug () {
return this.getAttribute('debug');
}
set debug (value) {
return this.setAttribute('debug', value);
}
#isDebugEnabled() {
return this.debug && this.debug !== "false" && this.debug !== "off";
}
/**
* TimeUnit used in the dataset
* @type TimeUnit
*/
get timeUnit () {
return TimeUnit.fromName(this.getAttribute('time-unit')) || TimeUnit.Millisecond;
}
set timeUnit (value) {
return this.setAttribute('time-unit', value);
}
/**
* Function used to format dates
* @type Function
*/
get formatDateFun () {
return this.#formatDateFun;
}
set formatDateFun (fun) {
this.#formatDateFun = fun;
this.#recalcTimeMarks();
}
}
export default BlueBoxTl;
window.customElements.define('bluebox-tl', BlueBoxTl);