@lemonadejs/timeline
Version:
LemonadeJS timeline component
312 lines (260 loc) • 10.4 kB
JavaScript
if (!lemonade && typeof (require) === 'function') {
var lemonade = require('lemonadejs');
}
if (! utils && typeof (require) === 'function') {
var utils = require('@jsuites/utils');
}
; (function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
global.Timeline = factory();
}(this, (function () {
let dateSignature = null;
// Dispatcher
const Dispatch = function (type, ...args) {
if (typeof this[type] === 'function') {
this[type](this, ...args);
}
}
const extractFromHtml = function(element) {
let data = [];
// Content
for (let i = 0; i < element.children.length; i++) {
let e = element.children[i];
let item = {
title: e.textContent || e.getAttribute('title') || '',
date: e.getAttribute('data-date'),
borderColor: e.getAttribute('data-color') || '',
borderStyle: e.getAttribute('data-style') || '',
}
data.push(item);
}
return data;
}
const extract = function(children) {
let data = [];
if (this.tagName) {
data = extractFromHtml(this);
// Remove all elements
this.textContent = '';
if (!Array.isArray(this.data)) {
this.data = [];
}
if (data && data.length) {
// Compatibility legacy jsuites
data.forEach((v) => {
this.data.push(data);
});
}
}
}
const Timeline = function (children, { onload, onchange, track }) {
let self = this;
extract.call(this, children);
const toDate = function(d) {
return d instanceof Date ? d : new Date(d);
}
let date = self.value ? toDate(self.value) : new Date();
self.result = [];
if (!Array.isArray(self.data)) {
self.data = [];
}
if (! self.format) {
if (self.type === 'monthly') {
self.format = 'dd mmm yyyy';
} else {
self.format = 'dddd, dd';
}
}
self.year = date.getFullYear();
self.month = 1 + date.getMonth();
self.months = utils.Helpers.months;
// Error message
if (! self.message) {
self.message = 'No records found';
}
if (! self.order) {
self.order = 'asc';
}
// Make sure to align has a default
if (!['top', 'right', 'bottom', 'left'].includes(self.align)) {
self.align = 'left';
}
if (typeof self.controls === 'undefined') {
self.controls = self.type === 'monthly';
}
self.next = function () {
if (self.month === 12) {
self.year++;
self.month = 1;
} else {
self.month++;
}
}
self.prev = function () {
if (self.month === 1) {
self.year--;
self.month = 12;
} else {
self.month--;
}
}
const isRemote = function() {
return self.remote && self.url && self.type === 'monthly';
}
const getDate = function(d) {
let date = d && d === dateSignature ? '' : d;
dateSignature = d;
return date;
}
const updateResult = function () {
let result = [];
if (self.type === 'monthly') {
for (let i = 0; i < self.data.length; i++) {
let d = toDate(self.data[i].date);
if ((d.getMonth() + 1) === self.month && d.getFullYear() === self.year) {
result.push(self.data[i]);
}
}
} else {
result = self.data;
}
result = result.sort((a, b) => (self.order === 'desc' ? -1 : 1) * (toDate(a.date).getTime() - toDate(b.date).getTime()));
for (let i = 0; i < result.length; i++) {
result[i].day = utils.Mask.render(result[i].date, self.format);
}
self.result = result;
// Reset the date signature to avoid interference in the next rendering
dateSignature = null;
// Event
Dispatch.call(self, 'onupdate', result);
}
const fetchRemote = function () {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
let res = JSON.parse(xhr.responseText);
let result = [];
if (Array.isArray(res.result)) {
result = res.result;
} else if (Array.isArray(res)) {
result = res
}
for (let i = 0; i < result.length; i++) {
result[i].date = new Date(result[i].date);
result[i].day = getDate(result[i].date.toLocaleDateString('en-GB', { year: 'numeric', month: 'short', day: '2-digit' }));
}
if (isRemote()) {
self.result = result;
} else {
self.data = result;
}
} else {
console.error('Failed to fetch data. Status code: ' + xhr.status);
}
}
};
let url = self.url;
if (isRemote()) {
url += `?year=${self.year}&month=${self.month}`;
url += `&asc=${self.order === 'asc'}`
}
xhr.open('GET', url, true);
xhr.setRequestHeader('Content-Type', 'text/json')
xhr.send();
}
const updateBorders = function() {
self.result.forEach(entry => {
if (entry.borderColor) {
entry.el.style.setProperty('--lm-border-color', entry.borderColor);
}
if (entry.borderStyle) {
entry.el.style.setProperty('--lm-border-style', entry.borderStyle);
}
if (Array.isArray(entry.tags) && entry.tags.length) {
const tagEls = entry.el.querySelectorAll('.lm-timeline-tag');
entry.tags.forEach((tag, i) => {
if (tagEls[i] && tag.color) {
tagEls[i].style.backgroundColor = tag.color;
}
});
}
});
}
const edition = function(e, s) {
if (typeof self.onedition === 'function') {
self.onedition(s);
}
}
const tagClick = function(e, s) {
if (s.onclick) {
s.onclick(e, s);
}
}
onchange((prop) => {
if (prop === 'value') {
date = toDate(self.value);
self.year = date.getFullYear();
self.month = 1 + date.getMonth();
} else if (prop === 'data' || prop === 'month' || prop === 'order') {
if (isRemote()) {
fetchRemote();
} else {
updateResult();
}
} else if (prop === 'result') {
updateBorders();
}
})
onload(() => {
if (self.url) {
fetchRemote();
} else {
updateResult();
}
if (typeof(self.width) !== 'undefined') {
self.el.style.width = parseInt(self.width) + 'px';
}
if (typeof(self.height) !== 'undefined') {
self.el.style.height = parseInt(self.height) + 'px';
}
});
track('data');
track('order');
return render => render`<div class="lm-timeline">
<div class="lm-timeline-header" data-visible="{{self.controls}}" data-type="{{self.type}}">
<div class="lm-timeline-label">
<div class="lm-timeline-year">${self.year}</div>
<div class="lm-timeline-month">${self.months[self.month - 1]}</div>
</div>
<div class="lm-timeline-navigation">
<button type="button" class="lm-timeline-icon lm-ripple" onclick="${self.prev}" tabindex="0">expand_less</button>
<button type="button" class="lm-timeline-icon lm-ripple" onclick="${self.next}" tabindex="0">expand_more</button>
</div>
</div>
<div :loop="self.result" data-message="{{self.message}}" data-mode="{{self.position}}" data-align="{{self.align}}" class="lm-timeline-data">
<div class="lm-timeline-item" data-bullet="{{self.day}}">
<div class="lm-timeline-edit" :render="self.editable"><button type="button" class="lm-timeline-icon lm-ripple lm-cursor" onclick="${edition}" tabindex="0">edit</button></div>
<div class="lm-timeline-title">{{self.title}}</div>
<div class="lm-timeline-subtitle">{{self.subtitle}}</div>
<div class="lm-timeline-description">{{self.description}}</div>
<div class="lm-timeline-tags" :loop="self.tags">
<span class="lm-timeline-tag" onclick="${tagClick}">{{self.title}}</span>
</div>
</div>
</div>
</div>`;
}
lemonade.setComponents({ Timeline: Timeline });
// Register the web component
lemonade.createWebComponent('timeline', Timeline);
return function (root, options) {
if (typeof (root) === 'object') {
lemonade.render(Timeline, root, options);
return options;
} else {
return Timeline.call(this, root);
}
}
})));