-
-
-
-
-
`
const main_wrapper = shadow.querySelector('.main_wrapper')
- const timeline_wrapper = shadow.querySelector('.timeline_wrapper')
const filter_wrapper = shadow.querySelector('.filter_wrapper')
const month_wrapper = shadow.querySelector('.month_wrapper')
- const empty_wrapper = shadow.querySelector('.empty_wrapper')
- const current_separator = shadow.querySelector('.current_separator')
// ----------------------------------------
- const windowbar_shadow = shadow.querySelector('.windowbar').attachShadow(shopts)
// ----------------------------------------
// ELEMENTS
// ----------------------------------------
- var timeline_cards
{ // timeline cards
- const on = {}
- function make_card (card_data, i) {
- const protocol = use_protocol(`card_${i}`)({ state, on })
- const opts = card_data
- const element = shadowfy()(timeline_card(opts, protocol))
- const slice = card_data.date.slice(-4)
-
- // if(i === Object.keys(cards_data).length - 1){
- // const latest = visitor ? Math.min(...status.years) : Math.max(...status.years)
- // let oldest = Number(year_cache) - 1
-
- // while(visitor ? latest <= oldest : latest >= oldest){
- // const separator = document.createElement('div')
- // separator.innerHTML = oldest
- // separator.classList.add('separator')
- // card_groups.push(separator)
- // visitor ? oldest-- : oldest++
- // }
- // }
- if (year_cache !== slice) {
- const latest = Number(slice)
- let oldest = year_cache ? sorting ? Number(year_cache) - 1 : Number(year_cache) + 1 : Number(slice)
-
- while(sorting ? latest <= oldest : latest >= oldest){
- const separator = document.createElement('div')
- separator.innerHTML = oldest
- separator.classList.add('separator')
- status.separators.push(separator)
- card_groups.push(separator)
- sorting ? oldest-- : oldest++
- }
-
- card_group = document.createElement('div')
- card_group.classList.add('card_group')
- card_groups.push(card_group)
- year_cache = slice
- }
- card_group.append(element)
- element.idx = i
- return element
+ const on = {
+ 'update_calendar': update_calendar,
+ 'update_timeline_filter': update_timeline_filter
}
- timeline_cards = cards_data.map(make_card)
- timeline_wrapper.append(...card_groups)
- timeline_wrapper.onscroll = onscroll
+ const protocol = use_protocol('timeline_cards')({ state, on })
+ const opts = { data, cards_data, years: status.years }
+ const element = shadowfy()(timeline_cards(opts, protocol))
+ month_wrapper.append(element)
+
}
{ // timeline filter
const on = {
@@ -193,7 +110,7 @@ function app_timeline (opts = default_opts, protocol) {
}
const protocol = use_protocol('timeline_filter')({ state, on })
const opts = {
- data, tags: Array.from(tags),
+ data, tags: [],
latest_date: cards_data[0].date_raw
}
const element = shadowfy()(timeline_filter(opts, protocol))
@@ -210,7 +127,6 @@ function app_timeline (opts = default_opts, protocol) {
{ // year filter
const on = { 'set_scroll': on_set_scroll }
const protocol = use_protocol('year_filter')({ state, on })
-
const opts = {
data, latest_year: Math.max(...status.years), oldest_year: Math.min(...status.years), sorting
}
@@ -220,9 +136,14 @@ function app_timeline (opts = default_opts, protocol) {
year_filter_wrapper.classList.add('hide')
}
- function on_set_scroll ({ data }) {
- set_scroll(data)
- updateCalendar()
+ async function on_set_scroll ({ data }) {
+ const channel = state.net[state.aka.timeline_cards]
+ channel.send({
+ head: [id, channel.send.id, channel.mid++],
+ type: 'set_scroll',
+ data
+ })
+ update_calendar({ data: { check: false, year: data.value }})
}
}
year_filter_wrapper.classList.add('year_filter_wrapper')
@@ -234,266 +155,58 @@ function app_timeline (opts = default_opts, protocol) {
month_filter_wrapper = shadowfy()(month_filter(opts, protocol))
month_filter_wrapper.classList.add('month_filter_wrapper')
month_wrapper.append(month_filter_wrapper)
- function on_set_scroll ({ data }) {
- set_scroll(data)
- updateCalendar()
- }
- }
- { // scrollbar
- const on = { 'set_scroll': on_set_scroll, status: onstatus }
- const protocol = use_protocol('scrollbar')({ state, on })
- opts.data.img_src.icon_arrow_start = opts.data.img_src.icon_arrow_up
- opts.data.img_src.icon_arrow_end = opts.data.img_src.icon_arrow_down
- const scroll_opts = { data }
- const element = shadowfy()(scrollbar(scroll_opts, protocol))
- filter_wrapper.append(element)
-
- const channel = state.net[state.aka.scrollbar]
- ro.observe(timeline_wrapper)
- function on_set_scroll (message) { setScrollTop(message.data) }
- function onstatus (message) {
+ async function on_set_scroll ({ data }) {
+ const channel = state.net[state.aka.timeline_cards]
channel.send({
head: [id, channel.send.id, channel.mid++],
- refs: { cause: message.head },
- type: 'update_size',
- data: {
- sh: timeline_wrapper.scrollHeight,
- ch: timeline_wrapper.clientHeight,
- st: timeline_wrapper.scrollTop
- }
+ type: 'set_scroll',
+ data
})
+ update_calendar({ data: { check: false, year: prev_year }})
}
}
// ----------------------------------------
// INIT
// ----------------------------------------
- updateCalendar()
- current_separator.innerHTML = sorting ? Math.max(...status.years) : Math.min(...status.years)
+ update_calendar({ data: { check: false, year: prev_year }})
return el
- function onscroll (event) {
- const scroll_channel = state.net[state.aka.scrollbar]
- scroll_channel.send({
- head: [id, scroll_channel.send.id, scroll_channel.mid++],
- type: 'handle_scroll'
- })
- let check = true
- const parent_top = timeline_wrapper.getBoundingClientRect().top
- status.separators.some(separator => {
- const child_top = separator.getBoundingClientRect().top
- if (child_top && child_top >= parent_top && child_top < parent_top + 10) {
- const year = separator.innerHTML
- status.YEAR = year
- updateCalendar()
- const channel = state.net[state.aka.year_filter]
- channel.send({
- head: [id, channel.send.id, channel.mid++],
- type: 'update_year_filter',
- data: year
- })
- check = false
- return true
- }
- })
- if (check)
- timeline_cards.some(card => {
- const { idx } = card
- const child_top = card.getBoundingClientRect().top
- if (child_top && child_top >= parent_top - 180 && child_top < parent_top + 40) {
- const year = cards_data[idx].date.slice(-4)
- status.YEAR = year
- updateCalendar()
- const channel = state.net[state.aka.year_filter]
- channel.send({
- head: [id, channel.send.id, channel.mid++],
- type: 'update_year_filter',
- data: year
- })
- return true
- }
- })
- const channel = state.net[state.aka.timeline_filter]
+ async function update_timeline_filter ({ data }) {
+ let channel = state.net[state.aka.timeline_filter]
channel.send({
head: [id, channel.send.id, channel.mid++],
type: 'update_timeline_filter',
- data: { month: status.MONTH , year: status.YEAR }
+ data
})
- current_separator.innerHTML = status.YEAR
- }
- function convert_time_format (time) {
- let temp = time.slice(0, 2)
- if (time.includes('PM')) { temp = parseInt(temp) + 12 }
- return temp + time.slice(2, -2)
- }
- async function set_scroll (data) {
- //for scroll by year we can use separators
- if (data.filter === 'YEAR'){
- status[data.filter] = data.value
- status.separators.some(separator => {
- const year = separator.innerHTML
- if(year.includes(data.value)){
- setScrollTop(separator.getBoundingClientRect().top - timeline_wrapper.getBoundingClientRect().top + timeline_wrapper.scrollTop)
- return true
- }
- })
- }//otherwise we need to use the cards
- else if (data.value){
- status[data.filter] = data.value
- let check = true
- timeline_cards.some(card => {
- const { idx } = card
- const card_data = cards_data[idx]
- if(cardfilter.includes(card_data)){
- const card_date = card_data.date
-
- if (card_date.includes(data.value) && card_date.includes(status.YEAR)) {
- if(check && status.cards){
- setScrollTop(card.getBoundingClientRect().top - timeline_wrapper.getBoundingClientRect().top + timeline_wrapper.scrollTop)
-
- check = false
- status.cards.forEach(status_card => {
- status_card.classList.remove('active')
- })
- if(status.cards[0] === card){
- status.cards = []
- return true
- }
- status.cards = []
- }
- if(data.filter === 'DATE'){
- card.classList.add('active')
- status.cards.push(card)
- }
- }
- else if(!check){
- return true
- }
- }
- })
- const timeline_channel = state.net[state.aka.timeline_filter]
- timeline_channel.send({
- head: [id, timeline_channel.send.id, timeline_channel.mid++],
- type: 'update_timeline_filter',
- data: { month: status.MONTH , year: status.YEAR }
- })
- }//otherwise it means we need to remove highlight
- else if(status.cards){
- status.cards.forEach(status_card => {
- status_card.classList.remove('active')
- })
- status.cards = []
- return
- }
- //update year_filter
- const year_channel = state.net[state.aka.year_filter]
- year_channel.send({
- head: [id, year_channel.send.id, year_channel.mid++],
+ channel = state.net[state.aka.year_filter]
+ channel.send({
+ head: [id, channel.send.id, channel.mid++],
type: 'update_year_filter',
- data: status.YEAR
+ data: data.year
})
-
- }
- async function setScrollTop (value) {
- timeline_wrapper.scrollTop = value
}
async function set_filter (data) {
- //Store filter value
- status[data.filter] = data.value
- timeline_wrapper.innerHTML = ''
- cardfilter = [...cards_data]
- if (status.SEARCH) cardfilter = cardfilter.filter((card_data) => {
- return card_data.title.toLowerCase().match(status.SEARCH.toLowerCase())
- })
- if (status.STATUS && status.STATUS !== 'ALL') cardfilter = cardfilter.filter((card_data) => {
- return card_data.active_state === status.STATUS && card_data
- })
- if (status.TAGS && status.TAGS !== 'ALL') {
- cardfilter = cardfilter.filter((card_data) => {
- return card_data.tags.includes(status.TAGS) && card_data
- })
- }
- //update timeline_cards
- status.separators = []
- status.years = []
- const card_groups = []
- let year_cache, card_group
-
- timeline_cards.forEach((card, i) => {
- const { idx } = card
- const card_data = cards_data[idx]
- //if the main loop never runs or misses last iterations
- if(i === Object.keys(cards_data).length - 1){
- const latest = sorting ? Math.min(...status.years_max) : Math.max(...status.years_max)
- let oldest = year_cache ? sorting ? Number(year_cache) - 1 : Number(year_cache) + 1 : sorting ? Math.max(...status.years_max) : Math.min(...status.years_max)
- while(sorting ? latest <= oldest : latest >= oldest){
- const separator = document.createElement('div')
- separator.innerHTML = oldest
- separator.classList.add('separator')
- status.separators.push(separator)
- card_groups.push(separator)
- sorting ? oldest-- : oldest++
- }
- }
- if (cardfilter.includes(card_data)) {
- const date = new Date(card_data.date)
- if(!status.years.includes(date.getFullYear()))
- status.years.push(date.getFullYear())
- const slice = card_data.date.slice(-4)
- //main loop
- if (year_cache !== slice) {
- if(i < Object.keys(cards_data).length - 1){
- const latest = Number(slice)
- let oldest = year_cache ? sorting ? Number(year_cache) - 1 : Number(year_cache) + 1 : sorting ? Math.max(...status.years_max) : Math.min(...status.years_max)
-
- while(sorting ? latest <= oldest : latest >= oldest){
- const separator = document.createElement('div')
- separator.innerHTML = oldest
- separator.classList.add('separator')
- status.separators.push(separator)
- card_groups.push(separator)
- sorting ? oldest-- : oldest++
- }
- }
-
- card_group = document.createElement('div')
- card_group.classList.add('card_group')
- card_groups.push(card_group)
- year_cache = slice
- }
- card_group.append(card)
- }
- })
- card_groups.forEach((card_group) => {
- timeline_wrapper.append(card_group)
- })
-
- //Update scrollbar and calendar
- const channel = state.net[state.aka.scrollbar]
+ const channel = state.net[state.aka.timeline_cards]
channel.send({
head: [id, channel.send.id, channel.mid++],
- type: 'handle_scroll'
+ type: 'set_filter',
+ data
})
- if (!cardfilter[0]) return
-
- set_scroll({
- filter: 'YEAR',
- value: sorting ? Math.max(...status.years_max) : Math.min(...status.years_max)
- })
- updateCalendar(true)//boolean argument indicates that this request is coming from set_filter
}
- async function updateCalendar (check = false) {
+ async function update_calendar ({ data, head }) {
+ const { check, year } = data
let dates = []
- if (status.YEAR) cardfilter.forEach(card_data => {
- if (card_data.date.includes(status.YEAR)) dates.push(card_data.date)
+ if (year) card_filter.forEach(card_data => {
+ if (card_data.date.includes(year)) dates.push(card_data.date)
})
const channel = state.net[state.aka.month_filter]
- if(prev_year !== String(status.YEAR) || check){
+ if(prev_year !== String(year) || check){
channel.send({
head: [id, channel.send.id, channel.mid++],
type: 'update_calendar',
- data: {dates, year: Number(status.YEAR)}
+ data: {dates, year: Number(year)}
})
- prev_year = String(status.YEAR).slice(0)
+ prev_year = String(year).slice(0)
if(status.cards){
status.cards.forEach(status_card => {
status_card.classList.remove('active')
@@ -506,10 +219,6 @@ function app_timeline (opts = default_opts, protocol) {
}
function get_theme () {
return`
- .timeline_section {
- display: flex;
- flex-direction: column;
- }
.main_wrapper {
box-sizing: border-box;
display: flex;
@@ -531,73 +240,13 @@ function get_theme () {
.main_wrapper .filter_wrapper .month_wrapper {
width: 100%;
height: 100%;
+ min-height: 500px;
overflow: hidden;
position: relative;
- margin: 0 20px;
- }
- .main_wrapper .filter_wrapper .timeline_wrapper {
- display: flex;
- flex-direction: column;
- width: 100%;
- height: 500px;
- overflow: scroll;
- gap: 20px;
- scrollbar-width: none; /* For Firefox */
- }
- .main_wrapper .filter_wrapper .timeline_wrapper.hide > div {
- display: none;
- }
- .main_wrapper .filter_wrapper .empty_wrapper {
- display: none;
- position: absolute;
- width: 100%;
- height: 100%;
- justify-content: center;
- align-items: center;
- top: 0;
- }
- .main_wrapper .filter_wrapper .empty_wrapper > div {
- background-color: white;
- }
- .main_wrapper .filter_wrapper .empty_wrapper.active {
- display: flex;
- }
- .main_wrapper .filter_wrapper .timeline_wrapper .card_group {
- width: 100%;
- padding: 0px;
- display: grid;
- gap: 20px;
- grid-template-columns: 12fr;
- border: 4px solid transparent;
- }
- .main_wrapper .filter_wrapper .timeline_wrapper .card_group > .active{
- outline: 4px solid var(--ac-1);
- }
- .main_wrapper .filter_wrapper .timeline_wrapper::-webkit-scrollbar {
- display: none;
- }
- .main_wrapper .filter_wrapper .timeline_wrapper .separator{
- background-color: var(--ac-1);
- text-align: center;
- margin: 0 4px;
- border: 1px solid var(--ac-3);
- position: relative;
- z-index: 2;
}
.main_wrapper .filter_wrapper > div:last-child{
border-left: 1px solid var(--ac-3);
}
- .main_wrapper .filter_wrapper .month_wrapper .current_separator{
- position: absolute;
- display: block;
- top: 0;
- width: calc(100% - 9px);
- background-color: var(--ac-1);
- text-align: center;
- margin: 0 4px;
- border: 1px solid var(--ac-3);
- z-index: 1;
- }
.main_wrapper .filter_wrapper .year_filter_wrapper{
border-left:1px solid var(--ac-3);
padding: 1px;
@@ -616,23 +265,6 @@ function get_theme () {
.month_filter_wrapper.show{
display: block;
}
- @container(min-width: 400px) {
- .main_wrapper .filter_wrapper .timeline_wrapper .card_group:last-child,
- .main_wrapper .filter_wrapper .timeline_wrapper .separator:last-child{
- margin-bottom: 300px;
- }
- }
- @container(min-width: 768px) {
- .main_wrapper .filter_wrapper .timeline_wrapper .card_group {
- grid-template-columns: repeat(2, 6fr);
- }
- }
- @container(min-width: 1200px) {
- .main_wrapper .filter_wrapper .timeline_wrapper .card_group {
- grid-template-columns: repeat(3, 4fr);
- }
- }
-
`
}
// ----------------------------------------------------------------------------
diff --git a/src/node_modules/scrollbar/scrollbar.js b/src/node_modules/scrollbar/scrollbar.js
index bcc61d3..e65a2ed 100644
--- a/src/node_modules/scrollbar/scrollbar.js
+++ b/src/node_modules/scrollbar/scrollbar.js
@@ -142,11 +142,11 @@ function scrollbar (opts = default_opts, protocol) {
else el.style.cssText = 'display: inline;'
const [prop1, prop2] = horizontal ? ['width', 'left'] : ['height', 'top']
const percent1 = Math.max(ratio * 100, 10);
- if(check){
- if(ratio * 100 < 10)
- bar_wrapper.classList.add('shrink');
- check = false
- }
+ if(ratio * 100 < 10)
+ bar_wrapper.classList.add('shrink');
+ else
+ bar_wrapper.classList.remove('shrink');
+
const percent2 = (size.content_scrollStart / size.content_scrollSize ) * 100
bar.style.cssText = `${prop1}: ${percent1}%; ${prop2}: ${percent2}%;`
}
@@ -210,13 +210,13 @@ function get_theme () {
}
.scrollbar_wrapper .vertical-bar-wrapper {
flex-direction: column;
- height: 100%;
+ height: 88%;
}
.scrollbar_wrapper .vertical-bar-wrapper.shrink {
height: 80%;
}
.scrollbar_wrapper .horizontal-bar-wrapper {
- width: 100%;
+ width: 88%;
}
.scrollbar_wrapper .horizontal-bar-wrapper.shrink {
width: 80%;
diff --git a/src/node_modules/sparkle-effect/package.json b/src/node_modules/sparkle-effect/package.json
new file mode 100644
index 0000000..215afe7
--- /dev/null
+++ b/src/node_modules/sparkle-effect/package.json
@@ -0,0 +1,3 @@
+{
+ "main": "./sparkle-effect.js"
+}
\ No newline at end of file
diff --git a/src/node_modules/sparkle-effect/sparkle-effect.js b/src/node_modules/sparkle-effect/sparkle-effect.js
new file mode 100644
index 0000000..4bf1ade
--- /dev/null
+++ b/src/node_modules/sparkle-effect/sparkle-effect.js
@@ -0,0 +1,164 @@
+const shopts = { mode: 'closed' }
+// ----------------------------------------
+module.exports = sparkle_effect
+// ----------------------------------------
+function sparkle_effect(opts) {
+ let possibleColors = (opts && opts.colors) || [
+ "#D61C59",
+ "#E7D84B",
+ "#1B8798",
+ ];
+
+ let width = window.innerWidth;
+ let height = window.innerHeight;
+ const cursor = { x: width / 2, y: width / 2 };
+ const lastPos = { x: width / 2, y: width / 2 };
+ const particles = [];
+ const canvImages = [];
+ let canvas, context;
+ const char = "*";
+ const el = document.createElement('div')
+ const shadow = el.attachShadow(shopts)
+
+ function init() {
+ canvas = document.createElement("canvas");
+ context = canvas.getContext("2d");
+ canvas.style.top = "0px";
+ canvas.style.left = "0px";
+ canvas.style.pointerEvents = "none";
+ canvas.style.position = "fixed";
+ canvas.width = width;
+ canvas.height = height;
+ context.font = "21px serif";
+ context.textBaseline = "middle";
+ context.textAlign = "center";
+
+ possibleColors.forEach((color) => {
+ let measurements = context.measureText(char);
+ let bgCanvas = document.createElement("canvas");
+ let bgContext = bgCanvas.getContext("2d");
+
+ bgCanvas.width = measurements.width;
+ bgCanvas.height =
+ measurements.actualBoundingBoxAscent +
+ measurements.actualBoundingBoxDescent;
+
+ bgContext.fillStyle = color;
+ bgContext.textAlign = "center";
+ bgContext.font = "21px serif";
+ bgContext.textBaseline = "middle";
+ bgContext.fillText(
+ char,
+ bgCanvas.width / 2,
+ measurements.actualBoundingBoxAscent
+ );
+
+ canvImages.push(bgCanvas);
+ });
+
+ bindEvents();
+ loop();
+ }
+ // Bind events that are needed
+ async function bindEvents() {
+ document.body.addEventListener("mousemove", onMouseMove);
+ document.body.addEventListener("touchmove", onTouchMove, { passive: true });
+ document.body.addEventListener("touchstart", onTouchMove, { passive: true });
+ window.addEventListener("resize", onWindowResize);
+ }
+
+ async function onWindowResize(e) {
+ canvas.width = window.innerWidth;
+ canvas.height = window.innerHeight;
+ }
+
+ async function onTouchMove(e) {
+ if (e.touches.length > 0) {
+ for (let i = 0; i < e.touches.length; i++) {
+ addParticle(
+ e.touches[i].clientX,
+ e.touches[i].clientY,
+ canvImages[Math.floor(Math.random() * canvImages.length)]
+ );
+ }
+ }
+ }
+
+ async function onMouseMove(e) {
+ window.requestAnimationFrame(() => {
+ cursor.x = e.clientX;
+ cursor.y = e.clientY;
+
+ const distBetweenPoints = Math.hypot(
+ cursor.x - lastPos.x,
+ cursor.y - lastPos.y
+ );
+
+ if (distBetweenPoints > 1.5) {
+ addParticle(
+ cursor.x,
+ cursor.y,
+ canvImages[Math.floor(Math.random() * possibleColors.length)]
+ );
+
+ lastPos.x = cursor.x;
+ lastPos.y = cursor.y;
+ }
+ });
+ }
+ async function addParticle(x, y, color) {
+ particles.push(new Particle(x, y, color));
+ }
+ async function updateParticles() {
+ context.clearRect(0, 0, width, height);
+
+ // Update
+ for (let i = 0; i < particles.length; i++) {
+ particles[i].update(context);
+ }
+ // Remove dead particles
+ for (let i = particles.length - 1; i >= 0; i--) {
+ if (particles[i].lifeSpan < 0) {
+ particles.splice(i, 1);
+ }
+ }
+ }
+ async function loop() {
+ updateParticles();
+ requestAnimationFrame(loop);
+ }
+
+ function Particle(x, y, canvasItem) {
+ const lifeSpan = Math.floor(Math.random() * 30 + 60);
+ this.initialLifeSpan = lifeSpan; //
+ this.lifeSpan = lifeSpan; //ms
+ this.velocity = {
+ x: (Math.random() < 0.5 ? -1 : 1) * (Math.random() / 2),
+ y: Math.random() * 0.7 + 0.9,
+ };
+ this.position = { x: x, y: y };
+ this.canv = canvasItem;
+
+ this.update = function (context) {
+ this.position.x += this.velocity.x;
+ this.position.y += this.velocity.y;
+ this.lifeSpan--;
+
+ this.velocity.y += 0.02;
+
+ const scale = Math.max(this.lifeSpan / this.initialLifeSpan, 0);
+
+ context.drawImage(
+ this.canv,
+ this.position.x - (this.canv.width / 2) * scale,
+ this.position.y - this.canv.height / 2,
+ this.canv.width * scale,
+ this.canv.height * scale
+ );
+ };
+ }
+ init();
+
+ shadow.append(canvas)
+ return el
+}
diff --git a/src/node_modules/timeline-card/timeline-card.js b/src/node_modules/timeline-card/timeline-card.js
index 0c3db30..d702b05 100644
--- a/src/node_modules/timeline-card/timeline-card.js
+++ b/src/node_modules/timeline-card/timeline-card.js
@@ -114,11 +114,10 @@ function get_theme () {
display: flex;
font-size: 14px;
line-height: 16px;
- max-height: 96px;
+ max-height: 50px;
overflow: hidden;
text-overflow: ellipsis;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 3; /* Limit the number of displayed lines */
+ word-break: break-word;
}
.tags_wrapper {
display: flex;
diff --git a/src/node_modules/timeline-cards/timeline-cards.js b/src/node_modules/timeline-cards/timeline-cards.js
new file mode 100644
index 0000000..324602b
--- /dev/null
+++ b/src/node_modules/timeline-cards/timeline-cards.js
@@ -0,0 +1,483 @@
+const timeline_card = require('timeline-card')
+const scrollbar = require('scrollbar')
+/******************************************************************************
+ APP TIMELINE COMPONENT
+******************************************************************************/
+// ----------------------------------------
+// MODULE STATE & ID
+var count = 0
+const [cwd, dir] = [process.cwd(), __filename].map(x => new URL(x, 'file://').href)
+const ID = dir.slice(cwd.length)
+const STATE = { ids: {}, net: {} } // all state of component module
+// ----------------------------------------
+const sheet = new CSSStyleSheet
+sheet.replaceSync(get_theme())
+const default_opts = { }
+const shopts = { mode: 'closed' }
+// ----------------------------------------
+module.exports = timeline_cards
+// ----------------------------------------
+function timeline_cards (opts = default_opts, protocol) {
+ // ----------------------------------------
+ // RESOURCE POOL (can't be serialized)
+ // ----------------------------------------
+ const ro = new ResizeObserver(entries => {
+ console.log('ResizeObserver:terminal:resize')
+ // !sorting && set_scroll_top(timeline_wrapper.scrollHeight)
+ const scroll_channel = state.net[state.aka.scrollbar]
+ scroll_channel.send({
+ head: [id, scroll_channel.send.id, scroll_channel.mid++],
+ refs: { },
+ type: 'handle_scroll',
+ })
+ })
+ // ----------------------------------------
+ // ID + JSON STATE
+ // ----------------------------------------
+ const id = `${ID}:${count++}` // assigns their own name
+ const status = {}
+ const state = STATE.ids[id] = { id, status, wait: {}, net: {}, aka: {} } // all state of component instance
+ const cache = resources({})
+ status.separators = []
+ status.years = []
+ status.cards = []
+ let sorting = true // latest to oldest
+ let card_group, card_group_rev, card_filter, fragment
+ let card_index = 0
+ let card_index_rev = 0
+ let scroll_dir = true
+ let timeline_container = []
+ // ----------------------------------------
+ // OPTS
+ // ----------------------------------------
+ const { data, cards_data, years } = opts
+ // Data preprocessing
+ status.years = years
+ status.years_max = [...years]
+ card_filter = [...cards_data]
+ const tags = new Set(cards_data.flatMap(card => card.tags))
+ status.YEAR = new Date(cards_data[0].date_raw).getFullYear()
+ // ----------------------------------------
+ // PROTOCOL
+ // ----------------------------------------
+ const PROTOCOL = {}
+ const on = {
+ 'set_scroll': set_scroll,
+ 'set_filter': set_filter
+ }
+ const up_channel = use_protocol('up')({ protocol, state, on })
+ // ----------------------------------------
+ // TEMPLATE
+ // ----------------------------------------
+ const el = document.createElement('div')
+ const shadow = el.attachShadow(shopts)
+ shadow.adoptedStyleSheets = [sheet]
+ shadow.innerHTML = `
+
+ `
+ const scrollbar_wrapper = shadow.querySelector('.scrollbar_wrapper')
+ const timeline_wrapper = shadow.querySelector('.timeline_wrapper')
+ const current_separator = shadow.querySelector('.current_separator')
+ // ----------------------------------------
+ // ----------------------------------------
+ // ELEMENTS
+ // ----------------------------------------
+ { // timeline cards
+ const on = {}
+ append_cards(0, 10)
+ timeline_wrapper.onscroll = onscroll
+ }
+ { // scrollbar
+ const on = { 'set_scroll': on_set_scroll, status: onstatus }
+ const protocol = use_protocol('scrollbar')({ state, on })
+ opts.data.img_src.icon_arrow_start = opts.data.img_src.icon_arrow_up
+ opts.data.img_src.icon_arrow_end = opts.data.img_src.icon_arrow_down
+ const scroll_opts = { data }
+ const element = shadowfy()(scrollbar(scroll_opts, protocol))
+ scrollbar_wrapper.append(element)
+
+ const channel = state.net[state.aka.scrollbar]
+ ro.observe(timeline_wrapper)
+ async function on_set_scroll (message) { set_scroll_top(message.data) }
+ async function onstatus (message) {
+ channel.send({
+ head: [id, channel.send.id, channel.mid++],
+ refs: { cause: message.head },
+ type: 'update_size',
+ data: {
+ sh: timeline_wrapper.scrollHeight,
+ ch: timeline_wrapper.clientHeight,
+ st: timeline_wrapper.scrollTop
+ }
+ })
+ }
+ }
+ // ----------------------------------------
+ // INIT
+ // ----------------------------------------
+ current_separator.innerHTML = sorting ? Math.max(...status.years) : Math.min(...status.years)
+ return el
+
+ async function make_card (card_data) {
+ const index = scroll_dir ? card_index : card_index_rev
+ const protocol = use_protocol(`card_${index}`)({ state, on })
+ const opts = card_data
+ const element = shadowfy()(timeline_card(opts, protocol))
+ const year = card_data.date.slice(-4)
+ let last_year
+ if(scroll_dir)
+ last_year = index == 0 ? undefined : card_filter[index - 1].date.slice(-4)
+ else
+ last_year = card_filter.length == index ? undefined : card_filter[index].date.slice(-4)
+
+ if (last_year !== year) {
+ const separator = document.createElement('div')
+ separator.innerHTML = year
+ separator.classList.add('separator')
+ status.separators.push(separator)
+ fragment.appendChild(separator)
+
+ if(scroll_dir){
+ card_group = document.createElement('div')
+ card_group.classList.add('card_group')
+ fragment.appendChild(card_group)
+ }
+ else{
+ card_group_rev = document.createElement('div')
+ card_group_rev.classList.add('card_group')
+ fragment.appendChild(card_group_rev)
+ }
+ }
+ element.idx = index
+ if(scroll_dir){
+ card_group.append(element)
+ card_index++
+ }
+ else{
+ card_group_rev.prepend(element)
+ card_index_rev--
+ }
+ timeline_container.push(element)
+ }
+ async function clear_timeline(){
+ timeline_wrapper.innerHTML = ''
+ timeline_container = []
+ status.separators = []
+ }
+ async function append_cards(start, end){
+ scroll_dir = true
+ fragment = document.createDocumentFragment()
+ card_filter.slice(start, end).map(make_card)
+ timeline_wrapper.appendChild(fragment)
+ }
+ async function prepend_cards(start, end){
+ scroll_dir = false
+ fragment = document.createDocumentFragment()
+ card_filter.slice(start, end).reverse().map(make_card)
+ timeline_wrapper.prepend(fragment)
+ }
+ async function onscroll () {
+ const timeline_top = timeline_wrapper.getBoundingClientRect().top
+ const timeline_height = timeline_wrapper.scrollHeight
+ const timeline_scrollTop = timeline_wrapper.scrollTop
+ //Bottom or top has reached load more cards
+ if(0 < card_index_rev && timeline_scrollTop < timeline_height/10){
+ set_scroll_top(timeline_height/10)
+ prepend_cards(card_index_rev < 10 ? 0 : card_index_rev - 10, card_index_rev)
+ }
+ else if(card_filter.length > card_index && timeline_height < timeline_scrollTop + 1000){
+ append_cards(card_index, card_index+10)
+ }
+
+ const scroll_channel = state.net[state.aka.scrollbar]
+ scroll_channel.send({
+ head: [id, scroll_channel.send.id, scroll_channel.mid++],
+ type: 'handle_scroll'
+ })
+
+ let check = true //to check if scrolling was successful using separators
+ //scroll using separators as ref
+ status.separators.some(separator => {
+ const child_top = separator.getBoundingClientRect().top
+ if (child_top && child_top >= timeline_top && child_top < timeline_top + 10) {
+ const year = separator.innerHTML
+ status.YEAR = year
+ check = false
+ return true
+ }
+ })
+ //else scroll using cards as ref
+ if (check)
+ timeline_container.some(card => {
+ const { idx } = card
+ const child_top = card.getBoundingClientRect().top
+ if (child_top && child_top >= timeline_top - 180 && child_top < timeline_top + 40) {
+ const year = cards_data[idx].date.slice(-4)
+ status.YEAR = year
+ return true
+ }
+ })
+ // Update the year_button
+ up_channel.send({
+ head: [id, up_channel.send.id, up_channel.mid++],
+ type: 'update_timeline_filter',
+ data: { month: status.MONTH , year: status.YEAR }
+ })
+ up_channel.send({
+ head: [id, up_channel.send.id, up_channel.mid++],
+ type: 'update_calendar',
+ data: { check: false, year: status.YEAR}
+ })
+ current_separator.innerHTML = status.YEAR
+ }
+ async function set_scroll ({ data }) {
+ scroll_dir = true
+ //for scroll by year
+ if (data.filter === 'YEAR'){
+ //Remove all cards
+ clear_timeline()
+ //Find the first card of the year
+ card_index = card_index < card_filter.length && Number(card_filter[card_index].date.slice(-5)) > data.value ? card_index : 0
+ for (const card_data of card_filter.slice(card_index)){
+ const temp = Number(card_data.date.slice(-5))
+ if(temp <= data.value){
+ card_index_rev = card_index
+ break
+ }
+ card_index++
+ }
+ //Populate somecards cards around the first card
+ const scroll_index = card_index < 10 ? card_index : 10
+ append_cards(card_index, card_index + 10)
+ //scroll to the first card
+ status[data.filter] = data.value
+ set_scroll_top(1)
+ current_separator.innerHTML = data.value
+ prepend_cards(card_index_rev - scroll_index, card_index_rev)
+ }//otherwise cards are needed
+ else if (data.value){
+ //load the cards
+ const filter_date = new Date(data.value + ' ' + status.YEAR).getTime()
+ fragment = document.createDocumentFragment()
+ scroll_dir = false
+ for(i=card_index_rev; i >= 0 && card_filter[i].date_raw <= filter_date; i--){
+ make_card(card_filter[i])
+ }
+ scroll_dir = true
+ for(i=card_index; i < card_filter.length && card_filter[i].date_raw >= filter_date; i++){
+ make_card(card_filter[i])
+ }
+ timeline_wrapper.appendChild(fragment)
+
+ //scroll to the cards
+ status[data.filter] = data.value
+ let check = true
+ timeline_container.some(card => {
+ const { idx } = card
+ const card_data = cards_data[idx]
+ const card_date = card_data.date_raw
+ if (card_date === filter_date) {
+ //remove highlight on previous cards
+ if(check && status.cards){
+ set_scroll_top(card.getBoundingClientRect().top - timeline_wrapper.getBoundingClientRect().top + timeline_wrapper.scrollTop)
+
+ check = false
+ status.cards.forEach(status_card => {
+ status_card.classList.remove('active')
+ })
+ if(status.cards[0] === card){
+ status.cards = []
+ return true
+ }
+ status.cards = []
+ }
+ //add highlight
+ if(data.filter === 'DATE'){
+ card.classList.add('active')
+ status.cards.push(card)
+ }
+ }
+ else if(!check){
+ return true
+ }
+ })
+ }//otherwise it means we need to remove highlight
+ else if(status.cards){
+ status.cards.forEach(status_card => {
+ status_card.classList.remove('active')
+ })
+ status.cards = []
+ return
+ }
+
+ }
+ async function set_scroll_top (value) {
+ timeline_wrapper.scrollTop = value
+ }
+ async function set_filter ({ data }) {
+ //Store filter value
+ status[data.filter] = data.value
+ card_filter = [...cards_data]
+ //filter the json data
+ if (status.SEARCH) card_filter = card_filter.filter((card_data) => {
+ return card_data.title.toLowerCase().match(status.SEARCH.toLowerCase())
+ })
+ if (status.STATUS && status.STATUS !== 'ALL') card_filter = card_filter.filter((card_data) => {
+ return card_data.active_state === status.STATUS && card_data
+ })
+ if (status.TAGS && status.TAGS !== 'ALL') {
+ card_filter = card_filter.filter((card_data) => {
+ return card_data.tags.includes(status.TAGS) && card_data
+ })
+ }
+ //update timeline_cards
+ card_index = card_index_rev = 0
+ clear_timeline()
+ append_cards(0, card_filter.length < 10 ? card_filter.length : 10)
+ //Update scrollbar and calendar
+ const channel = state.net[state.aka.scrollbar]
+ channel.send({
+ head: [id, channel.send.id, channel.mid++],
+ type: 'handle_scroll'
+ })
+ if (!card_filter[0]) return
+
+ set_scroll_top(0)
+ up_channel.send({
+ head: [id, up_channel.send.id, up_channel.mid++],
+ type: 'update_calendar',
+ data: { check: true, year: status.YEAR}
+ })//boolean argument indicates that this request is coming from set_filter
+ }
+}
+function get_theme () {
+ return`
+ *{
+ box-sizing: border-box;
+ }
+ .timeline_wrapper {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 500px;
+ overflow: scroll;
+ gap: 20px;
+ scrollbar-width: none; /* For Firefox */
+ }
+ .timeline_wrapper.hide {
+ display: none;
+ }
+ .timeline_wrapper .card_group {
+ width: 100%;
+ display: grid;
+ gap: 20px;
+ grid-template-columns: 12fr;
+ padding: 0 2px;
+ }
+ .timeline_wrapper .card_group > .active{
+ outline: 4px solid var(--ac-1);
+ }
+ .timeline_wrapper .card_group > .hide{
+ display: none;
+ }
+ .timeline_wrapper::-webkit-scrollbar {
+ display: none;
+ }
+ .timeline_wrapper .separator{
+ background-color: var(--ac-1);
+ text-align: center;
+ margin: 0 4px;
+ border: 1px solid var(--ac-3);
+ position: relative;
+ z-index: 2;
+ }
+ > div:last-child{
+ border-left: 1px solid var(--ac-3);
+ }
+ .current_separator{
+ position: absolute;
+ display: block;
+ top: 0;
+ width: calc(100% - 39px);
+ background-color: var(--ac-1);
+ text-align: center;
+ margin: 0 4px;
+ border: 1px solid var(--ac-3);
+ z-index: 1;
+ }
+ .scrollbar_wrapper{
+ display: flex;
+ }
+ @container(min-width: 400px) {
+ .timeline_wrapper .card_group:last-child,
+ .timeline_wrapper .separator:last-child{
+ margin-bottom: 300px;
+ }
+ }
+ @container(min-width: 768px) {
+ .timeline_wrapper .card_group {
+ grid-template-columns: repeat(2, 6fr);
+ }
+ }
+ @container(min-width: 1200px) {
+ .timeline_wrapper .card_group {
+ grid-template-columns: repeat(3, 4fr);
+ }
+ }
+ `
+}
+// ----------------------------------------------------------------------------
+function shadowfy (props = {}, sheets = []) {
+ return element => {
+ const el = Object.assign(document.createElement('div'), { ...props })
+ const sh = el.attachShadow(shopts)
+ sh.adoptedStyleSheets = sheets
+ sh.append(element)
+ return el
+ }
+}
+function use_protocol (petname) {
+ return ({ protocol, state, on = { } }) => {
+ if (petname in state.aka) throw new Error('petname already initialized')
+ const { id } = state
+ const invalid = on[''] || (message => console.error('invalid type', message))
+ if (protocol) return handshake(protocol(Object.assign(listen, { id })))
+ else return handshake
+ // ----------------------------------------
+ // @TODO: how to disconnect channel
+ // ----------------------------------------
+ function handshake (send) {
+ state.aka[petname] = send.id
+ const channel = state.net[send.id] = { petname, mid: 0, send, on }
+ return protocol ? channel : Object.assign(listen, { id })
+ }
+ function listen (message) {
+ const [from] = message.head
+ const by = state.aka[petname]
+ if (from !== by) return invalid(message) // @TODO: maybe forward
+ console.log(`[${id}]:${petname}>`, message)
+ const { on } = state.net[by]
+ const action = on[message.type] || invalid
+ action(message)
+ }
+ }
+}
+// ----------------------------------------------------------------------------
+function resources (pool) {
+ var num = 0
+ return factory => {
+ const prefix = num++
+ const get = name => {
+ const id = prefix + name
+ if (pool[id]) return pool[id]
+ const type = factory[name]
+ return pool[id] = type()
+ }
+ return Object.assign(get, factory)
+ }
+}
\ No newline at end of file
diff --git a/src/node_modules/year-filter/year-filter.js b/src/node_modules/year-filter/year-filter.js
index 485536b..b85fffa 100644
--- a/src/node_modules/year-filter/year-filter.js
+++ b/src/node_modules/year-filter/year-filter.js
@@ -72,7 +72,7 @@ function year_filter (opts = default_opts, protocol) {
channel.send({
head: [id, channel.send.id, channel.mid++],
type: 'set_scroll',
- data: { value: active_state, filter: 'YEAR' }
+ data: { value: Number(active_state), filter: 'YEAR' }
})
}
function on_active_state ({ data: year_button }) {