From 47e77f9fac1155c536b2b87fec552c7fcce66fa6 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 01 Jun 2026 18:06:34 +0000
Subject: [PATCH] =Timeline block fixes. Next up: adding article schema classes
---
src/feed/viewOld.js | 2237 +++++++++++++++++-----------------------------------------
1 files changed, 675 insertions(+), 1,562 deletions(-)
diff --git a/src/feed/viewOld.js b/src/feed/viewOld.js
index 347e997..c65be60 100644
--- a/src/feed/viewOld.js
+++ b/src/feed/viewOld.js
@@ -1,1657 +1,770 @@
-class FeedBlock {
- static LOADING_QUIPS = JSON.parse(feedSettings.quips);
- constructor(container){
-
- this.cache = window.jvbCache;
- this.eventHandlers = new Map();
-
- this.rendered = {};
-
- // Store container references
- this.container = container;
- this.a11y = window.jvbA11y;
-
-
- //For tracking cache and current requests
- this.currentRequest = null;
- this.timeoutId = null;
-
- // Initialize Config
- this.initConfig();
-
-
- // Setup error handler
- this.error = window.jvbError;
-
- this.resetState();
- this.state.firstLoad = false;
- this.state.URLProcessed = false;
- this.highlightGot = false;
-
- this.selectorInstances = {};
-
- this.elements = {
- filters: this.container.querySelector('form.feed-filters'),
- selected: this.container.querySelector('.selected-items'),
- clearFilters: this.container.querySelector('button.clear-filters'),
- grid: this.container.querySelector('.feed-grid'),
- loadMore: this.container.querySelector('button.load-more'),
- spinner: this.container.querySelector('.loading-spinner'),
- loading: this.container.querySelector('.feed-overlay'),
- matchAll: this.container.querySelector('.filter-actions .toggle-text'),
- }
- this.filters = {
- content: this.getCurrentContent(),
- taxonomies: {},
- favourites: false,
- orderby: 'date',
- order: 'desc',
- }
-
- this.feed = {
- imageLoadThreshold: 5,
- lazyLoadOffset: '100px',
- gallery: [],
- loaded: 0,
- intersectionObserver: null,
- templates: new Map()
- }
- this.imageObserver = null;
- this.resizeObserver = null;
-
- this.highlight = null;
- //Loading settings
- this.loadingIndex = 0;
- this.quips = this.initializeQuips();
- this.loadingMessage = this.container.querySelector('.loading-message');
- this.dotsElement = this.container.querySelector('.loading-dots');
- this.quipInterval = null;
- this.loadingOptions = {
- loadingMessages: {},
- cycleInterval: 2000, //time between loading messages
- }
-
- this.initFilters();
- if(this.container.dataset.gallery){
- this.initGallery();
- this.setupGalleryAccessibility();
- }
- this.initListeners();
- if(!this.state.URLProcessed){
- this.updateFilters();
- }
-
- this.selectedListeners = this.checkSelectedClicks.bind(this);
- this.updateSelectedListeners();
- }
-
- initConfig() {
- // Get settings from container data attribute
- const settings = JSON.parse(this.container.dataset.settings || '{}');
- let content = Array.from(this.container.querySelectorAll('input[name="content"]')).map(content=> content.value);
- this.config = {
- api: feedSettings.apiUrl,
- nonce: feedSettings.nonce,
- currentUser: jvbSettings.currentUser || null,
-
- content: content[0],
- contentTypes: content,
- taxonomies: Array.from(this.container.querySelectorAll('.jvb-selector')).map(taxonomy => taxonomy.dataset.taxonomy),
-
- // Source information for analytics
- source: this.container.dataset.source || '',
- context: this.container.dataset.context || '',
-
- // Optional highlight
- highlight: null,
-
- // Gallery mode
- isGallery: this.container.dataset.gallery || false,
- showAuthor: !this.container.dataset.gallery || true,
- showDate: this.container.dataset.gallery || false,
-
-
- // User preferences
- viewMode: localStorage.getItem('feedViewMode') || 'grid',
- }
-
- if(settings.isGallery){
- this.config.highlight = this.getHighlight();
- }
- }
-
- initFilters(){
- this.updateContentFor(this.getCurrentContent());
- }
-
- checkSelectedClicks(e){
- if(e.target.closest('.remove-item')){
- let tag = e.target.closest('.selected-item');
-
- // Uncheck checkbox if it exists
- let taxonomy = tag.dataset.taxonomy;
- this.clearSelectedTerm(tag.dataset.id, taxonomy);
- // Remove tag
- tag.remove();
-
- // Update clear filters button visibility
- this.updateClearFiltersButton();
- this.updateFilters();
- this.updateSelectedListeners();
- }
- }
-
- updateSelectedListeners(){
- this.elements.selected.removeEventListener('click', this.selectedListeners);
- if(this.elements.selected.children.length>0){
- this.elements.selected.addEventListener('click', this.selectedListeners);
- }
- }
-
- handleFilterChange(e){
- if(e.target.closest('.jvb-selector')){
- return;
- }
- this.resetPage();
- if(e.target.name === 'content'){
- let content = e.target.value;
- this.updateContentFor(content);
- }
- this.updateFilters();
- }
- handleLoadMore(){
- if (this.state.loading || !this.state.hasMore) {
- return;
- }
- this.fetchFeed();
- }
- initListeners(){
- window.addEventListener('popstate', this.handlePopState.bind(this));
- this.addEvent(this.elements.filters, 'change', (e) => this.handleFilterChange(e));
- this.addEvent(this.elements.filters, 'submit', e => e.preventDefault());
- this.addEvent(this.elements.loadMore, 'click', () => this.handleLoadMore());
- this.addEvent(this.elements.clearFilters, 'click', () => this.clearSelectedTaxonomies());
- if(this.config.isGallery){
- this.addEvent(this.elements.grid, 'click', (e) =>this.handleGalleryOpen(e));
- }
-
- // Intersection observer for lazy loading
- if ('IntersectionObserver' in window) {
- this.imageObserver = new IntersectionObserver(entries => {
- entries.forEach(entry => {
- if (entry.isIntersecting) {
- this.loadImage(entry.target);
- this.imageObserver.unobserve(entry.target);
- }
- });
- }, {
- rootMargin: this.feed.lazyLoadOffset,
- threshold: 0.1
- });
- }
- // Resize observer for responsive images
- if ('ResizeObserver' in window) {
- this.resizeObserver = new ResizeObserver(window.debounce(() => {
- this.updateImageSizes();
- }, 250));
-
- // Observe the container
- this.resizeObserver.observe(this.container);
- } else {
- // Fallback to window resize
- window.addEventListener('resize', window.debounce(() => {
- this.updateImageSizes();
- }, 250));
- }
- }
- handleGalleryOpen(e){
- const feedImage = e.target.closest('.feed-image');
- if(feedImage){
- const item = feedImage.closest('.feed-item');
- if(item) {
- const index = Array.from(this.container.querySelectorAll('.feed-item')).indexOf(item);
- if(index !== -1){
- this.openGallery(index);
- }
- }
- }
- }
- initGallery(){
- this.gallery = {
- items: this.getGalleryItems() || [],
- index: 0,
- touchStart: null,
- touchEnd: null,
- minSwipe: 50,
- modal: this.createGalleryModal(),
- keyHandler: null,
- loading: false
- }
- document.body.appendChild(this.gallery.modal);
- }
-
- /**
- * Handle browser history navigation
- */
- handlePopState(e) {
- if (e.state && e.state.filters) {
- if(this.processURLFilters()){
-
- // Load items with updated filters
- this.resetPage();
- this.fetchFeed();
-
- // Announce to screen readers
- this.a11y.announce('Feed filters updated from browser history.');
- }
- }
- }
-
- processURLFilters(){
- const params = new URLSearchParams(window.location.search);
- if (!params.toString()) {
- return false; // No parameters to process
- }
- // Initialize content type
- const content = params.get('f_content');
- params.delete('f_content');
- if (content && this.elements.filters.querySelector(`input[value="${content}"]`)) {
- this.elements.filters.querySelector(`input[value="${content}"]`).checked = true;
- this.filters.content = content;
- }
-
- // Initialize order
- const order = params.get('f_order');
- params.delete('f_order');
- if (order && this.elements.filters.querySelector(`input[name="order"][value="${order}"]`)) {
- this.elements.filters.querySelector(`input[name="order"][value="${order}"]`).checked = true;
- this.filters.order = order;
- }
-
- // Initialize orderby
- const orderby = params.get('f_orderby');
- params.delete('f_orderby');
- if (orderby && this.elements.filters.querySelector(`input[name="orderby"][value="${orderby}"]`)) {
- this.elements.filters.querySelector(`input[name="orderby"][value="${orderby}"]`).checked = true;
- this.filters.orderby = orderby;
- }
-
- // Initialize favourites filter
- if (params.get('f_favourites') === 'true' && this.config.currentUser !== null) {
- params.delete('f_favourites');
- const favCheckbox = this.elements.filters.querySelector(`input[name="favourites_only"]`);
- if (favCheckbox) favCheckbox.checked = true;
- this.filters.favourites = true;
- }
-
- // Initialize match, if present
- if(params.get('f_match') === 'all'){
- params.delete('f_match');
- this.elements.matchAll.querySelector('input').checked = true;
- }
-
- window.removeChildren(this.elements.selected);
-
- let filters = JSON.parse(JSON.stringify(this.filters));
- let unprocessed = {};
- for (var [key, value] of Object.entries(Object.fromEntries(params))) {
- if (key.startsWith('f_') ) {
- var taxName = key.replace('f_', '');
- let cache = this.cache.getItem(taxName+'List');
- if(!Object.keys(filters['taxonomies']).includes(taxName)){
- filters.taxonomies[taxName] = {};
- }
-
- // Handle both single values and comma-separated values
- const termIds = value.includes(',') ? value.split(',') : [value];
- termIds.forEach(termId=>{
- if(cache && cache.hasOwnProperty(termId)){
- filters.taxonomies[taxName][termId] = cache[termId].name;
- let tag = this.createFilterTag(taxName, termId, cache[termId].name);
- this.elements.selected.appendChild(tag);
- }else{
- if(!unprocessed.hasOwnProperty(taxName)){
- unprocessed[taxName] = [];
- }
- unprocessed[taxName].push(termId);
- }
- });
- }
- }
-
- this.filters = filters;
-
- if(!isEmptyObject(unprocessed)){
- this.fetchTermDetails(unprocessed);
- }
-
- if(this.config.isGallery){
- this.getHighlight();
- }
-
- this.updateContentFor(content);
- this.updateSelectedListeners();
- return true;
-
- }
-
- async loadFromURL() {
-
- if(this.processURLFilters()){
- // Announce to screen readers
- this.a11y.announce('Feed filters updated from URL.');
- }
- return true;
- }
-
- //We already checked the cache, now fetch the remaining term information
- async fetchTermDetails(terms) {
- let params = new URLSearchParams(terms);
-
- // Otherwise fetch the term details
- try {
- // Format the URL - might need to adjust based on your API structure
- const url = `${this.config.api}terms/check?`+params.toString();
-
- const termData = await this.cache.fetchWithCache(
- url, {
- method: 'GET',
- },
- {
- });
-
- for(const [taxonomy, terms] of Object.entries(termData.terms)){
- if(!this.filters.taxonomies.hasOwnProperty(taxonomy)){
- this.filters.taxonomies[taxonomy] = {};
- }
-
- this.cache.setItem(taxonomy+'List', terms, taxonomy);
-
- for(const [termId, termData] of Object.entries(terms)){
- this.filters.taxonomies[taxonomy][termId] = termData.name;
- let tag = this.createFilterTag(taxonomy, termId, termData.name);
- this.elements.selected.appendChild(tag);
- }
- this.updateSelectedListeners();
- }
-
- } catch (error) {
- console.error(`Error fetching term details for ${terms}:`, error);
-
- }
- }
-
- //State Management
- nextPage(hasMore = true){
- this.state.page++;
- this.state.hasMore = hasMore;
- }
- resetPage(hasMore = true){
- this.state.page = 1;
- this.state.hasMore = hasMore;
- }
- resetState() {
- this.state = {
- page: 1,
- loading: false,
- hasMore: true,
- retries: {
- count: 0,
- max: 3,
- delay: 1000
- }
- }
- }
-
- updateFilters(reset = true){
- let updated = false;
- let content = this.getCurrentContent();
- let favourites = this.getFavouritesOnly();
- let order = this.getCurrentOrder();
- let orderby = this.getCurrentOrderby();
- let taxonomies = this.getSelectedTaxonomies();
-
- if(this.filters.content !== content ||
- this.filters.favourites !== favourites ||
- this.filters.order !== order ||
- this.filters.orderby !== orderby ||
- this.filters.taxonomies !== taxonomies ){
- updated = true;
- }
- (this.filters.content !== content)??this.updateContentFor(this.filters.content);
-
- this.filters.content = content;
- this.filters.favourites = favourites;
- this.filters.order = order;
- this.filters.orderby = orderby;
- this.filters.taxonomies = taxonomies;
-
- if(this.state.firstLoad){
- this.updateURL();
- }else{
- this.state.firstLoad = true;
- }
-
-
- if(reset){
- this.resetPage();
- }
- this.fetchFeed();
- }
- getCurrentContent(){
- return this.elements.filters.querySelector('input[name="content"]:checked').value;
- }
- getFavouritesOnly(){
- return this.elements.filters.querySelector('input[name="favourites_only"]')?.checked ??false;
- }
- getCurrentOrder(){
- return this.elements.filters.querySelector('input[name="order"]:checked').value;
- }
- getCurrentOrderby(){
- return this.elements.filters.querySelector('input[name="orderby"]:checked').value;
- }
-
- getCurrentTaxonomies(){
- let taxonomies = this.filters.taxonomies;
- let out = {};
- if(!isEmptyObject(taxonomies)){
- for(var [tax, terms] of Object.entries(taxonomies)){
- if(!isEmptyObject(terms)){
- out[tax] = Object.values(terms);
- }
- }
- }
- return out;
- }
- updateContentFor(content){
-
- this.elements.filters.querySelectorAll('.jvb-selector').forEach(tax=> {
- tax.hidden = !tax.dataset.for.includes(content);
- //Ensure any selected Taxonomies are removed
- if(!tax.dataset.for.includes(content)){
- this.clearSelectedTerms(tax);
- }
- //TODO: Ensure clean up of filtered out window.jvbTaxonomySelector instances?
- //Maybe cache them?
- let taxonomy = tax.dataset.taxonomy;
- if(tax.dataset.for.includes(content)){
- if(!this.selectorInstances.hasOwnProperty(taxonomy)){
- this.selectorInstances[taxonomy] = new window.jvbTaxonomySelector(tax, {
- multiple: true,
- feed: true,
- selected: this.filters[taxonomy]??{},
- onClose: () => this.setSelectedTerms(taxonomy)
- });
- }
- }else if(this.selectorInstances.hasOwnProperty(taxonomy)){
- this.clearSelectedTerms(taxonomy);
- delete this.selectorInstances[taxonomy];
- }
- });
- this.elements.filters.querySelectorAll('input[data-for]').forEach(toggle=>{
- toggle.hidden = !toggle.dataset.for.includes(content);
- });
- this.elements.filters.querySelectorAll('input[name="order"]').forEach(order=>{
- order.hidden = this.filters.orderby === 'random';
- });
- }
- updateURL(){
- const params = new URLSearchParams();
-
- let taxonomies = this.filters.taxonomies;
- if(!isEmptyObject(taxonomies)){
- for(var [tax, terms] of Object.entries(taxonomies)){
- if(!isEmptyObject(terms)){
- params.set('f_'+tax, Object.keys(terms));
- }
-
- }
- }
-
- // Clone to avoid modifying original
- let filters = JSON.parse(JSON.stringify(this.filters));
- delete filters.taxonomies;
- for(const [key, value] of Object.entries(filters)){
- if(value !== false){
- params.set('f_'+key, value);
- }
- }
- if(this.elements.matchAll.querySelector('input:checked')){
- params.set('f_match', 'all');
- }
- const newUrl = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
- history.pushState({ filters }, '', newUrl);
- }
-
- /**
- * Feed Fetching
- * @param reset clear grid
- * @param force force cache busting
- * @returns {Promise<void>}
- */
- async fetchFeed(reset = false, force = false){
- if(this.state.loading){
- return;
- }
-
- this.updateLoading(true);
- try{
- // Track URL processing
- if(this.state.page === 1){
- reset = true;
- await this.loadFromURL();
- }
-
- const params = this.buildFilterRequest();
- if(this.elements.matchAll.querySelector('input:checked')){
- params.append('match', true);
- }
- params.append('page', this.state.page);
- params.append('source', this.config.source);
- params.append('context', this.config.context);
- if(this.config.highlight){
- params.append('highlight', JSON.stringify(this.config.highlight));
- }
-
- //fetch data with cache busting
- const data = await this.cache.fetchWithCache(
- `${this.config.api}feed?${params.toString()}`,
- {
- method: 'GET',
- },
- {
- context: 'feed',
- forceRefresh: true,
- // forceRefresh: force,
- }
- )
-
- console.log(data, 'Fetched data: ');
-
- //Clear grid on first page
- if(this.state.page === 1){
- window.removeChildren(this.elements.grid);
- }
-
- //Handle empty results
- if(!data || !data.items || data.items.length === 0){
- if(this.state.page === 1){
- this.showEmptyState();
- }
- this.state.hasMore = false;
- }else{
- this.state.hasMore = data.hasMore;
- if(this.state.hasMore){
- this.nextPage();
- }
- //Render items
- this.renderItems(data.items, this.state.page > 1);
-
- }
-
- }catch(error){
- this.handleError(error);
- }finally{
- this.updateLoading(false);
- this.elements.loadMore.hidden = !this.state.hasMore;
- }
-
- }
-
- buildFilterRequest(){
- // Clone to avoid modifying original
- const filters = JSON.parse(JSON.stringify(this.filters));
-
- if(filters.taxonomies && !isEmptyObject(filters.taxonomies)){
- let temp = {};
- for(var [tax, terms] of Object.entries(filters.taxonomies)){
- if(!isEmptyObject(terms)){
- temp[tax] =Object.keys(terms);
- }
- }
- delete filters.taxonomies;
- if(!isEmptyObject(temp)){
- filters.taxonomies = JSON.stringify(temp);
- }
- }else{
- delete filters.taxonomies;
- }
-
-
- if(!filters.favourites){
- delete filters.favourites;
- }
- return new URLSearchParams(filters);
- }
-
-
- handleError(error){
- return this.error.handleApiError(
- error,
- {
- component: 'Feed Block',
- action: 'loaditems'
- },
- () => this.fetchFeed()
- );
- }
-
-
-
- getHighlight() {
- if(!this.config.highlight){
- const searchParams = new URLSearchParams(window.location.search);
-
- // Check for content type parameters
- this.config.contentTypes.forEach(type => {
- if (searchParams.has(type)) {
- this.config.highlight = {};
- this.config.highlight[type] = searchParams.get(type);
- }
- });
- }
- return this.config.highlight;
- }
-
- /**
- * Loading
- */
- updateLoading(loading) {
- this.state.loading = loading;
- if (loading) {
- this.showLoading();
-
- let content = this.filters.content;
- content = (content === 'artwork') ? content : content+'s';
- let tax = '';
- let taxonomies = this.getCurrentTaxonomies();
- if(!isEmptyObject(taxonomies)){
- let total = 0;
- total = Object.values(taxonomies).map((tax)=> total+tax.length);
- let all = [];
- let join = (total[0] === 2) ? ' and ' : ', ';
- tax = Object.values(taxonomies).map((tax) => all.push(tax.join(join)));
- tax = all.join(', ');
- let index = tax.lastIndexOf(',')+1;
- if(index> 0){
- tax = tax.substr(0, index)+' and'+tax.substr(index);
- }
-
- }
-
-
- this.a11y.announce(`Checking for more ${tax} ${content}...`);
- } else {
- this.hideLoading();
- }
-
- // Update loading spinner
- this.elements.spinner.hidden = !loading;
-
- // Update load more button
- this.elements.loadMore.disabled = loading;
-
- }
-
-
- /**
- * Show the loading overlay
- */
- showLoading() {
- this.hideBody();
- this.elements.loading.classList.add('active');
- this.startQuipCycle();
- document.body.classList.add('loading');
- }
-
- /**
- * Hide the loading overlay
- */
- hideLoading() {
- this.showBody();
- this.container.classList.remove('active');
- this.stopQuipCycle();
- document.body.classList.remove('loading');
- }
-
-
- /**
- * Start cycling through loading messages
- */
- startQuipCycle() {
- if (this.quipInterval) {
- clearInterval(this.quipInterval);
- }
-
- if (!this.quips.length) return;
-
- // Set initial message
- this.updateMessage(this.quips[0]);
- this.container.classList.remove('changing');
-
- this.quipInterval = setInterval(() => {
- this.container.classList.add('changing');
-
- setTimeout(() => {
- this.loadingIndex = (this.loadingIndex + 1) % this.quips.length;
- this.updateMessage(this.quips[this.loadingIndex]);
-
- setTimeout(() => {
- this.container.classList.remove('changing');
- }, 50);
- }, 350);
- }, this.loadingOptions.cycleInterval);
- }
-
- /**
- * Stop cycling through loading messages
- */
- stopQuipCycle() {
- if (this.quipInterval) {
- clearInterval(this.quipInterval);
- this.quipInterval = null;
- }
- }
-
- /**
- * Update the loading message
- */
- updateMessage(quipData) {
- if (!this.loadingMessage) return;
-
- const icon = feedSettings?.icons?.[quipData.icon] || '';
- this.loadingMessage.innerHTML = `${icon}<p>${quipData.quip}</p>`;
- }
-
- /**
- * Set a custom loading message
- */
- setMessage(message, icon = null) {
- if (!this.loadingMessage) return;
-
- this.stopQuipCycle();
-
- const iconHtml = icon ? feedSettings?.icons?.[icon] || '' : '';
- this.loadingMessage.innerHTML = `${iconHtml}<p>${message}</p>`;
- }
-
- /**
- * Shuffle an array (Fisher-Yates algorithm)
- */
- shuffleArray(array) {
- const newArray = [...array];
- for (let i = newArray.length - 1; i > 0; i--) {
- const j = Math.floor(Math.random() * (i + 1));
- [newArray[i], newArray[j]] = [newArray[j], newArray[i]];
- }
- return newArray;
- }
-
- /**
- * Initialize quips for loading messages
- */
- initializeQuips() {
- // Map pstyle, artstyle, etc. to their base types for quips
- const typeMapping = {
- 'pstyle': 'style',
- 'artstyle': 'style',
- 'arttheme': 'theme',
- 'artmedium': 'style',
- 'artform': 'style',
- 'placement': 'style',
- 'colour': 'style'
- };
-
- // Gather all quips based on content types and taxonomies
- const allQuips = [];
-
- // Add content type quips
- if (FeedBlock.LOADING_QUIPS[this.filters.content]) {
- FeedBlock.LOADING_QUIPS[this.filters.content].forEach(quip => {
- allQuips.push({
- icon: this.filters.content,
- quip: quip
- });
- });
- }
-
- // Add taxonomy quips
- if(this.filters.taxonomies && !isEmptyObject(this.filters.taxonomies)){
- Object.keys(this.filters.taxonomies).forEach(taxonomy => {
- const baseType = typeMapping[taxonomy] || taxonomy;
- if (FeedBlock.LOADING_QUIPS[baseType]) {
- FeedBlock.LOADING_QUIPS[baseType].forEach(quip => {
- allQuips.push({
- icon: taxonomy,
- quip: quip
- });
- });
- }
- });
- }
-
-
- // Shuffle the quips array
- return this.shuffleArray(allQuips);
- }
-
- /**
- * Feed Grid
- */
-
- renderItems(items, append = false) {
- // Reset if not appending
- if (!append) {
- window.removeChildren(this.elements.grid);
- this.feed.loaded = 0;
- this.feed.gallery = [];
- }
-
- // Add items to gallery if in gallery mode
- if (this.config.isGallery) {
- this.feed.gallery = this.feed.gallery.concat(items);
- }
-
- // Bail early if no items
- if (items.length === 0) {
- this.a11y.announceUpdate(0, append);
- return;
- }
-
- // Use DocumentFragment for better performance
- const fragment = document.createDocumentFragment();
-
- // Process items in batches for better performance
- const batchSize = 10;
- const processBatch = (startIndex) => {
- const endIndex = Math.min(startIndex + batchSize, items.length);
-
- // Process this batch
- for (let i = startIndex; i < endIndex; i++) {
- const item = items[i];
- const element = this.createItemElement(item);
- fragment.appendChild(element);
-
- // Lazy load images beyond threshold
- if (this.feed.loaded >= this.feed.imageLoadThreshold && this.imageObserver) {
- this.imageObserver.observe(element);
- } else {
- this.loadImage(element);
- }
-
- this.feed.loaded++;
- }
-
- // If we have more items, process next batch in next frame
- if (endIndex < items.length) {
- requestAnimationFrame(() => {
- processBatch(endIndex);
- });
- } else {
- // All batches processed, append fragment
- this.elements.grid.appendChild(fragment);
- if(this.container.dataset.gallery){
- console.log(this.getGalleryItems());
- this.updateGalleryItems(this.getGalleryItems());
- //pagination already updated, so it'll be page 2
- if(this.getHighlight() && !this.highlightGot){
- this.highlightGot = true;
- this.openGallery(0);
- }
- }
- this.a11y.makeNavigable(this.elements.grid.querySelectorAll('.feed-item:not([data-keyboard-nav])'));
- this.a11y.announceItems(items.length, append, this.state.hasMore);
- }
- };
-
- // Start processing the first batch
- if (items.length > 0) {
- processBatch(0);
- } else {
- this.a11y.announceUpdate(0, append);
- }
- }
-
- /**
- * Load image for an item
- */
- loadImage(element) {
- const img = element.querySelector('img');
- if (!img) return;
- const size = this.getImageSize();
- img.src = img.dataset[size] || img.dataset.src;
- delete img.dataset.src;
-
- element.setAttribute('data-loaded', 'true');
- }
-
- /**
- * Update image sizes based on screen width
- */
- updateImageSizes() {
- const size = this.getImageSize();
-
- // Update only visible images that aren't already loaded with the right size
- const items = this.elements.grid.querySelectorAll('.feed-item[data-loaded="true"]');
- items.forEach(item => {
- const img = item.querySelector('img');
- if (img && img.dataset[size] && img.src !== img.dataset[size]) {
- img.src = img.dataset[size];
- }
- });
- }
-
- /**
- * Create an element for a feed item
- */
- createItemElement(item) {
-
- if(!this.rendered[item.icon]){
- this.rendered[item.icon] = new Map();
- }
-
- if(this.rendered[item.icon].has(item.id)){
- return this.rendered[item.icon].get(item.id);
- }
-
- const favourited = window.isFavourited(item.icon, item.id)??false;
- const template = window.getTemplate('feed-item');
-
- // Set unique attributes
- template.id = `${item.icon}-${item.id}`;
- template.classList.add(item.icon);
-
- if (item.umami_view) {
- this.buildUmamiData(template, item.umami_view);
- }
-
- let favouriteButton = template.querySelector('button.favourite');
- [
- favouriteButton.dataset.id,
- favouriteButton.dataset.type,
- favouriteButton.dataset.artist,
- favouriteButton.title
- ] = [
- item.id,
- item.icon,
- item.user_id,
- (favourited) ? 'Remove from Favourites' : 'Add to Favourites'
- ];
-
- let order = item.order;
- let single = template.querySelector('.item');
- let list = template.querySelector('.item-list');
- let img = template.querySelector('.feed-images');
- let summary = template.querySelector('summary');
- let info = template.querySelector('.item-info');
-
- for (let [index, id] of Object.entries(order)){
- let target;
- let config = item[id];
- if (id === 'title') {
- target = template.querySelector('h3 a');
- if (item.title !== '') {
- [
- target.textContent,
- target.href,
- target.url
- ] = [
- item.title,
- item.url,
- `Learn more about this ${item.icon}`
- ];
- if (item.icon !== '') {
- target.closest('h3').prepend(window.getIcon(item.icon));
- }
- if (item.umami_click) {
- this.buildUmamiData(target, item.umami_click);
- }
- } else {
- target.remove();
- }
- } else if (Object.hasOwn(config, 'terms')) {
- //Taxonomy list
- if (config.terms.length === 0) {
- continue;
- }
- let taxonomy = list.cloneNode(true);
- let label = taxonomy.querySelector('.label');
- let termList = taxonomy.querySelector('ul');
- let listItem = taxonomy.querySelector('li');
-
- if (config.label) {
- label.textContent = config.label;
- }
- if (config.icon) {
- label.prepend(window.getIcon(config.icon));
- }
- if (!config.label && !config.icon){
- label.remove();
- }
-
- config.terms.forEach(term => {
- let termItem = listItem.cloneNode(true);
- let link = termItem.querySelector('a');
- [
- link.href,
- link.title,
- link.textContent
- ] = [
- term.url,
- `Learn more about ${term.title}`,
- term.title
- ];
- if (term.umami_click.length > 0) {
- this.buildUmamiData(link, term.umami_click);
- }
- termList.append(termItem);
- });
-
- listItem.remove();
- info.appendChild(taxonomy);
- } else if (Object.hasOwn(config, 'value') && config.value !== '') {
- let itemInfo = single.cloneNode(true);
- let label = itemInfo.querySelector('.label');
- let link = itemInfo.querySelector('a');
- let p = itemInfo.querySelector('p');
- if (Object.hasOwn(config, 'label')) {
- label.textContent = config.label;
- }
- if (Object.hasOwn(config, 'icon')) {
- label.prepend(window.getIcon(config.icon));
- }
- if (!Object.hasOwn(config, 'icon') && !Object.hasOwn(config, 'label')) {
- label.remove();
- }
- if (Object.hasOwn(config, 'url')) {
- p.remove();
- [
- link.textContent,
- link.href,
- link.title
- ] = [
- config.value,
- config.url,
- `Learn more about ${config.value}`
- ];
- } else {
- link.remove();
- p.textContent = config.value;
- }
- info.appendChild(itemInfo);
- } else if (id === 'image') {
- let images = summary.querySelector('.feed-images');
- let img = images.querySelector('a');
-
- let main = img.cloneNode(true);
- main.href = item.url;
- main.classList.add('feed-image');
- this.buildImageData(main.querySelector('img'), item.image);
- images.append(main);
-
- if (item.content?.length > 0) {
- images.classList.add('multi');
- item.content.forEach(c => {
- let image = img.cloneNode(true);
- image.href = c.url;
- let itemImg = image.querySelector('img');
- itemImg.src = c.image.small;
- itemImg.alt = c.image.alt;
- images.append(image);
- });
- }
- img.remove();
- }
- }
- single.remove();
- list.remove();
-
- this.rendered[item.icon].set(item.id, template);
-
- return template;
- }
-
- buildImageData(img, data){
- if (typeof data.tiny !== 'string') {
+class FeedBlockOld {
+ constructor() {
+ this.container = document.querySelector('section.feed-block');
+ if (!this.container) {
return;
}
- [
- img.src,
- img.dataset.small,
- img.dataset.medium,
- img.dataset.large,
- img.alt
- ] =
- [
- data.tiny,
- data.small,
- data.medium,
- data.large,
- data.alt
- ];
- }
- buildUmamiData(item, data){
- for(let [key, value] of Object.entries(data)){
- item.dataset[key] = value;
- }
- }
+ this.a11y = window.jvbA11y;
+ this.cache = new window.jvbCache('feed');
+ this.error = window.jvbError;
- /**
- * Show empty state
- */
- showEmptyState() {
- const message = this.filters.favourites
- ? `<div class="feed-empty-state">
- <h3>♡ BLANK CANVAS ♡</h3>
- <p>You haven't fallen in love with any pieces... yet!</p>
- <p>Hit that heart icon when something stops your scroll.</p>
- <p>Your dream collection is waiting to start.</p>
- </div>`
- : `<div class="feed-empty-state">
- <h3>NOTHING HERE...</h3>
- <p>Try tweaking those filters.</p>
- <p>Edmonton's got talent - let's find it.</p>
- </div>`;
+ this.config = {
+ source: '',
+ context: '',
+ highlight: null,
+ gallery: false,
+ view: this.cache.get('feedView') || 'grid',
+ ... this.container.dataset
+ };
+ this.initElements();
+ this.initFilters();
- this.elements.grid.innerHTML = message;
- this.a11y.announceEmpty(this.filters.favourites);
- }
- /**
- * Clear the grid
- */
- clearGrid() {
- this.a11y.announce('Items cleared.');
- window.removeChildren(this.elements.grid);
- this.feed.loaded = 0;
- }
+ this.loadWhenAble();
+ }
- /**
- * Get image size based on screen width
- */
- getImageSize() {
- const width = window.innerWidth;
- if (width > 1024) return 'medium';
- if (width > 500) return 'medium';
- return 'small';
- }
+ loadWhenAble() {
+ if ('requestIdleCallback' in window) {
+ requestIdleCallback(() => {
+ this.initTaxonomies();
+ this.initStore();
+ this.initListeners();
+ this.initGallery();
+ }, { timeout: 2000 });
+ } else {
+ setTimeout(() => {
+ this.initTaxonomies();
+ this.initStore();
+ this.initListeners();
+ this.initGallery();
+ }, 100);
+ }
+ }
- /**
- * Get gallery items for the gallery modal
- */
- getGalleryItems() {
- return Array.from(this.container.querySelectorAll('.feed-item'))
- .map(item => {
- const img = item.querySelector('img');
- if (!img) return null;
+ initElements() {
+ this.currentTaxonomies = new Set(); // Allowed Taxonomies, grabbed from active buttons
+ this.taxonomyFilters = {};
+ this.elements = {
+ filterTrigger: '[data-filter]',
+ filters: {
+ content: '[data-filter="content"]',
+ orderby: '[data-filter="orderby"]',
+ order: '[data-filter="order"]',
+ match: '[data-filter="match"]',
+ favourites: '[data-filter="favourites"]',
+ taxonomy: '[data-filter^="taxonomy"]'
+ },
+ selectedTax: '.selected-items',
+ clearFilter: 'button.clear-filters',
+ loadMore: 'button.load-more',
+ filterContainer: '.filters',
+ grid: '.item-grid',
+ };
+ this.ui = window.uiFromSelectors(this.elements);
- return {
- id: item.querySelector('button.favourite').dataset.id,
- small: img.dataset.small || img.src,
- large: img.dataset.medium || img.src,
- full: img.dataset.full || img.src,
- alt: img.alt || '',
- fav: item.querySelector('button.favourite')?.cloneNode(true),
- info: item.querySelector('.item-info')?.cloneNode(true)
- };
- })
- .filter(Boolean);
- }
- /**
- * Clean up resources when component is destroyed
- */
- addEvent(element, event, handler, options) {
- if (!element) return;
+ this.ui.content = this.ui.filterContainer.querySelectorAll('[name="content"]')??false;
+ this.ui.taxonomies = this.ui.filterContainer.querySelectorAll('[data-taxonomy]');
+ if (this.ui.content && this.ui.content.length > 0) {
+ this.contentTypes = Array.from(
+ this.ui.content
+ ).map(content => content.value);
+ } else {
+ this.contentTypes = [this.container.dataset['content']];
+ }
- const boundHandler = handler.bind(this);
- element.addEventListener(event, boundHandler, options);
+ if (this.ui.taxonomies.length>0) {
+ this.taxonomies = Array.from(
+ this.ui.taxonomies,
+ ).map(content => content.dataset.taxonomy);
+ } else {
+ this.taxonomies = [];
+ }
- // Store for cleanup
- if (!this.eventHandlers.has(element)) {
- this.eventHandlers.set(element, []);
- }
- this.eventHandlers.get(element).push({ event, handler: boundHandler });
- }
- destroy() {
- // Clean up observers
- if (this.imageObserver) {
- this.imageObserver.disconnect();
- this.imageObserver = null;
- }
- if (this.resizeObserver) {
- this.resizeObserver.disconnect();
- this.resizeObserver = null;
- }
+ }
- // Clean up all event listeners
- this.eventHandlers.forEach((handlers, element) => {
- handlers.forEach(({ event, handler }) => {
- element.removeEventListener(event, handler);
- });
- });
- this.eventHandlers.clear();
+ async initTaxonomies() {
+ this.selector = window.jvbSelector;
+ const buttons = document.querySelectorAll('[data-filter="taxonomy"]');
- // Clean up timers
- if (this.quipInterval) {
- clearInterval(this.quipInterval);
- }
+ this.selector.isInitializing = true;
+ buttons.forEach((button) => {
+ const taxonomy = button.dataset.taxonomy;
+ this.currentTaxonomies.add(taxonomy);
- if (this.timeoutId) {
- clearTimeout(this.timeoutId);
- }
+ this.selector.registerFilterButton(button, {
+ button: button,
+ buttonSelector: '[data-filter="taxonomy"]',
+ selected: this.ui.selectedTax
+ });
- // Clear template cache and other state
- this.feed.templates.clear();
- this.feed.gallery = [];
- this.feed.loaded = 0;
- }
- /** Extra Term Handling **/
+ // Add preload listeners
+ this.addTaxonomyPreloadListeners(button, taxonomy);
+ });
- getSelectedTaxonomies(){
- let taxonomies = {};
- for(var [taxonomy, instance] of Object.entries(this.selectorInstances)){
- taxonomies[taxonomy] = instance.selectedItems;
- }
- return taxonomies;
- }
- /**
- * Get selected values for a taxonomy
- */
- getSelectedTerms(taxonomy) {
- const selectedItems = this.elements.selected.querySelectorAll(
- `.selected-item[data-taxonomy="${taxonomy}"]`
- );
+ this.selector.isInitializing = false;
- return Array.from(selectedItems).map(item => item.dataset.id);
- }
+ this.selector.subscribe((event, data) => {
+ if (event === 'selected-terms') this.handleTaxonomyChange(data);
+ });
+ }
- clearSelectedTaxonomies(){
- window.removeChildren(this.elements.selected);
- if(!isEmptyObject(this.selectorInstances)){
- for(var [taxonomy, instance] of Object.entries(this.selectorInstances)){
- instance.selectedItems = {};
- }
- }
+ addTaxonomyPreloadListeners(button, taxonomy) {
+ const preload = () => {
+ this.selector.preloadTaxonomy(taxonomy);
+ };
- this.elements.matchAll.querySelector(input).checked = false;
+ // Desktop hover
+ button.addEventListener('mouseenter', preload, { once: true });
- this.filters.taxonomies = {};
- this.updateFilters();
- }
- setSelectedTerms(taxonomy){
- if(this.selectorInstances[taxonomy]){
- let selected = this.selectorInstances[taxonomy].selectedItems;
- if(!isEmptyObject(selected)){
+ // Touch/keyboard (fires before click)
+ button.addEventListener('pointerdown', preload, { once: true });
- this.filters.taxonomies[taxonomy] = selected;
- for(var [id, name] of Object.entries(selected)){
- this.elements.selected.appendChild(this.createFilterTag(taxonomy, id, name));
- }
- this.updateFilters();
- this.updateSelectedListeners();
- }else{
- delete this.filters.taxonomies[taxonomy];
- }
+ // Keyboard focus
+ button.addEventListener('focus', preload, { once: true });
+ }
- }
- }
- clearSelectedTerm(termId, taxonomy){
- let container = this.container.querySelector('.filters');
- let input = container.querySelector(`li[data-id="${termId}"] input`);
- input.checked = false
- delete this.selectorInstances[taxonomy].selectedItems[termId];
- }
- clearSelectedTerms(taxonomy){
+ handleTaxonomyChange(data) {
+ const { terms, taxonomy } = data;
- if(!isEmptyObject(this.filters.taxonomies) && this.filters.taxonomies.hasOwnProperty(taxonomy)){
- delete this.filters.taxonomies[taxonomy];
- }
- if(!isEmptyObject(this.selectorInstances) && this.selectorInstances.hasOwnProperty(taxonomy)){
- this.selectorInstances[taxonomy].selectedItems = {};
- }
+ // Update only the current taxonomy's terms
+ if (terms.size > 0) {
+ this.taxonomyFilters[taxonomy] = Array.from(terms.keys());
+ } else {
+ // Remove taxonomy if no terms selected
+ delete this.taxonomyFilters[taxonomy];
+ }
+ // Build filters object with all taxonomies
+ let filters = {
+ page: 1
+ };
- const selectedItems = this.elements.selected.querySelectorAll(
- `.selected-item[data-taxonomy="${taxonomy}"]`
- );
+ // Add taxonomy filters if any exist
+ if (Object.keys(this.taxonomyFilters).length > 0) {
+ filters.taxonomy = this.taxonomyFilters;
+ }
- if(selectedItems.length > 0){
- selectedItems.forEach(item => {
- item.remove();
- });
- }
+ this.updateFilter(filters);
+ }
- // Update clear filters button visibility
- this.updateClearFiltersButton();
- }
- updateClearFiltersButton(){
- if (!this.elements.clearFilters) return;
+ clearAllTaxonomies() {
+ this.taxonomyFilters = {};
+ window.removeChildren(this.ui.selectedTax);
- let filters = this.elements.selected.children.length;
+ this.updateFilter({
+ taxonomy: null,
+ page: 1
+ });
+ }
- const hasFilters = filters > 0;
+ initFilters() {
+ //defaults
+ this.filters = {
+ content: this.contentTypes[0],
+ orderby: 'date',
+ order: 'desc',
+ page: 1
+ };
+ if (this.config.context) this.filters.context = this.config.context;
+ if (this.config.source) this.filters.source = this.config.source;
- const hasMultiple = filters > 1;
+ //check the cache
+ this.processCachedFilters();
+ //check url
+ this.processURLFilters();
- this.elements.clearFilters.hidden = !hasFilters;
+ // Set initial UI state
+ this.syncUIToFilters();
+ }
+ syncUIToFilters() {
+ if (this.ui.filterContainer) {
+ // Check radio buttons
+ Object.entries(this.filters).forEach(([key, value]) => {
+ const input = this.ui.filterContainer.querySelector(`[data-filter="${key}"][value="${value}"]`);
+ if (input) {
+ input.checked = true;
+ }
+ });
+ }
- this.elements.filters.classList.toggle('has-filters', hasFilters);
+ // Update content-specific visibility
+ this.updateContentFor(this.filters.content);
+ }
+ nextPage() {
+ this.store.setFilter('page', this.store.filters.page++);
+ }
- this.elements.matchAll.hidden = !hasMultiple;
- }
+ initStore() {
+ const store = window.jvbStore.register(
+ 'feed',
+ {
+ storeName: 'feed',
+ endpoint: 'feed',
+ keyPath: 'id',
+ indexes: [
+ { name: 'content', keyPath: 'content'},
+ { name: 'taxonomy', keyPath: 'taxonomy'},
+ { name: 'user', keyPath: 'user'},
+ { name: 'date', keyPath: 'modified'},
+ { name: 'title', keyPath: 'title'}
+ ],
+ filters: this.filters,
+ TTL: 6 * 60 * 60 * 1000,
+ showLoading: true,
+ required: 'content',
+ delayFetch: true
+ }
+ );
+ this.store = store.feed;
+ this.store.subscribe((event, data) => {
+ switch (event) {
+ case 'data-loaded':
+ this.renderItems();
+ this.ui.loadMore.hidden = true;
+ if (this.store.lastResponse && this.store.lastResponse['has_more']) {
+ this.ui.loadMore.hidden = !this.store.lastResponse['has_more'];
+ }
+ break;
+ }
+ });
+ }
- /**
- * Create a filter tag element
- */
- createFilterTag(taxonomy, id, name) {
- const tag = window.getTemplate('selectedTerm');
+ initGallery() {
+ this.gallery = (this.config.gallery) ? window.jvbGallery : false;
+ if (this.gallery) {
+ this.gallery.subscribe((event, data) => {
+ if (event === 'load-more' && this.store.lastResponse) {
+ if (this.store.lastResponse['has_more']) {
+ this.nextPage();
+ }
+ }
+ });
+ }
+ }
- tag.dataset.taxonomy = taxonomy;
- tag.dataset.id = id;
- let icon = window.getIcon(taxonomy);
- let span = tag.querySelector('span');
- let button = tag.querySelector('button');
- tag.prepend(icon);
- span.classList.add('filter-name');
- span.classList.remove('item-name');
- [span.textContent, button.remove] =
- [escapeHtml(name), `Remove ${escapeHtml(name)}`];
+ processCachedFilters() {
+ Object.keys(this.filters).forEach(filter => {
+ let cached = this.cache.get(`${this.config.source}_${this.config.context}_${filter}`);
+ if (cached && cached !== this.filters[filter]){
+ this.filters[filter] = cached;
+ }
+ });
+ }
+ processURLFilters() {
+ if (this.filters.page > 1) {
+ return false;
+ }
+ const params = new URLSearchParams(window.location.search);
+ if (!params.toString()) {
+ return false;
+ }
+ let filters = ['content', 'order', 'orderby', 'favourites', 'match'];
+ filters.forEach(filter => {
+ let value = params.get(`f_${filter}`);
+ if (value) {
+ this.filters[filter] = value;
+ let input = this.ui.filters[filter];
+ if (input) {
+ input.checked = true;
+ }
+ }
+ });
- return tag;
- }
+ let hasTaxonomy = false;
+ // Load taxonomy filters from URL
+ params.forEach((value, key) => {
+ if (key.startsWith('f_tax_')) {
+ hasTaxonomy = true;
+ const taxonomy = key.replace('f_tax_', '');
+ if (!this.taxonomyFilters[taxonomy]) {
+ this.taxonomyFilters[taxonomy] = [];
+ }
+ this.taxonomyFilters[taxonomy] = value.split(',').map(Number);
+ }
+ });
+ if (this.ui.filterContainer && hasTaxonomy) {
+ for (let [tax, ids] in Object.entries(this.taxonomyFilters)) {
+ let button = this.ui.filterContainer.querySelector(`[data-taxonomy="${tax}"]`);
+ if (button) {
+ if (button.dataset.fieldId) {
+ let field = this.selector.get(button.dataset.fieldId);
+ field.selectedTerms = new Set(ids);
+ this.selector.initFieldDisplay(button.dataset.fieldId);
+ } else {
+ this.selector.registerField(button, {
+ button: button,
+ buttonSelector: '[data-filter="taxonomy"]',
+ selected: this.ui.selectedTax,
+ selectedItems: ids
+ });
+ }
+ }
+ }
+ }
+ return true;
+ }
- /**
- * Gallery
- **/
- openGallery(index){
- this.gallery.index = index;
- this.gallery.modal.showModal();
- this.hideBody();
+ /**
+ * Update URL with current filters (for sharing/bookmarking)
+ */
+ updateURL() {
+ const params = new URLSearchParams();
- this.bindGalleryEvents();
- //show current image
- this.updateDisplay(index);
- //preload adjacent images
- this.preloadImages();
+ // Add simple filters
+ ['content', 'order', 'orderby', 'match'].forEach(key => {
+ if (this.filters[key]) {
+ params.set(`f_${key}`, this.filters[key]);
+ }
+ });
- // Announce initial state
- this.a11y.announce(`Image ${this.gallery.index + 1} of ${this.gallery.items.length}. Use arrow keys to navigate.`)
- this.a11y.trapFocus(this.gallery.modal);
+ // Add taxonomy filters
+ Object.entries(this.taxonomyFilters).forEach(([taxonomy, terms]) => {
+ if (terms.length > 0) {
+ params.set(`f_tax_${taxonomy}`, terms.join(','));
+ }
+ });
- }
+ // Update URL without reload
+ const newURL = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
+ window.history.pushState({ filters: this.filters }, '', newURL);
+ }
- /**
- * Create the modal element
- */
- createGalleryModal() {
- const modal = document.createElement('dialog');
- modal.className = 'gallery-modal';
- modal.setAttribute('aria-modal', 'true');
- modal.setAttribute('aria-label', 'Image Gallery');
+ renderItems() {
+ let items = this.store.getFiltered();
+ if (this.store.filters['page'] === 1) {
+ window.removeChildren(this.ui.grid);
+ }
- modal.innerHTML = `
- <button class="gallery-close" aria-label="Close gallery">
- ${jvbSettings.icons.close}
- </button>
+ if (items.length === 0) {
+ this.a11y.announceItems(0, this.store.filters['page'] > 0);
+ return;
+ }
- <button class="gallery-nav gallery-prev" aria-label="Previous image">
- ${jvbSettings.icons.prev}
- </button>
+ const fragment = document.createDocumentFragment();
+ const batchSize = 10;
- <button class="gallery-nav gallery-next" aria-label="Next image">
- ${jvbSettings.icons.next}
- </button>
+ const processBatch = (startIndex) => {
+ const endIndex = Math.min(startIndex + batchSize, items.length);
- <div class="gallery-content">
- <img src="" alt="" class="gallery-image">
- <details>
- <summary>DETAILS</summary>
- <div class="item-info"></div>
- </details>
- </div>
+ for (let i = startIndex; i < endIndex; i++) {
+ const item = items[i];
+ const element = this.createItemElement(item);
+
+ fragment.appendChild(element);
+ }
- <div class="gallery-favourite"></div>
- <div class="gallery-counter"><span id="gallery-index">1</span> / <span class="total"></span></div>
- `;
+ if (endIndex < items.length) {
+ requestAnimationFrame(() => processBatch(endIndex));
+ } else {
+ this.removePlaceholders();
+ this.ui.grid.append(fragment);
- return modal;
- }
+ if (this.config.gallery) {
+ this.gallery.updateGalleryItems(this.gallery.getGalleryItems());
+ }
+ this.a11y.makeNavigable(this.ui.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
+ this.a11y.announceItems(items.length, this.store.filters['page'] > 1, this.store.hasMore);
+ }
+ };
- /**
- * Bind event handlers
- */
- bindGalleryEvents() {
- // Close button
- this.gallery.modal.querySelector('.gallery-close').addEventListener('click', () => this.closeGallery());
+ if (items.length > 0) {
+ processBatch(0);
+ } else {
+ this.a11y.announceItems(0, this.store.filters['page'] >1, false);
+ }
- // Navigation buttons
- const prevBtn = this.gallery.modal.querySelector('.gallery-prev');
- const nextBtn = this.gallery.modal.querySelector('.gallery-next');
+ if (this.ui.filters.match) {
+ this.ui.filters.match.hidden = Object.keys(this.taxonomyFilters).length === 0;
+ }
+ if (this.ui.clearFilter) {
+ this.ui.clearFilter.hidden = Object.keys(this.taxonomyFilters).length === 0;
+ }
+ }
- prevBtn.addEventListener('click', () => this.navigate(-1));
- nextBtn.addEventListener('click', () => this.navigate(1));
+ /**
+ *
+ * @param {object} item
+ */
+ createItemElement(item) {
+ let template = window.getTemplate(`feedItem${window.uppercaseFirst(item.content)}`);
- // Keyboard navigation
- this.gallery.keyHandler = (e) => {
- switch (e.key) {
- case 'ArrowLeft':
- this.navigate(-1);
- break;
- case 'ArrowRight':
- this.navigate(1);
- break;
- case 'Escape':
- this.closeGallery();
- break;
- }
- };
- document.addEventListener('keydown', this.gallery.keyHandler);
+ const isTimeline = Object.hasOwn(template.dataset, 'timeline');
- // Touch events
- this.gallery.modal.addEventListener('touchstart', (e) => {
- this.gallery.touchStart = e.touches[0].clientX;
- });
+ // Format fields using helpers
+ for (let [fieldName, value] of Object.entries(item.fields)) {
+ if (isTimeline && ['timeline', 'number'].includes(fieldName)) continue;
+ let el = template.querySelector(`[data-field="${fieldName}"]`);
+ if (!el) continue;
- this.gallery.modal.addEventListener('touchmove', (e) => {
- this.gallery.touchEnd = e.touches[0].clientX;
- });
+ if (value === '') {
+ el.remove();
+ continue;
+ }
- this.gallery.modal.addEventListener('touchend', () => {
- if (!this.gallery.touchStart || !this.gallery.touchEnd) return;
+ if (this.isImageField(item, value)) {
+ this.formatImageFields(el, value, item);
+ } else if (this.isTaxonomyField(item, fieldName)) {
+ this.formatTaxonomyField(el, item, fieldName, value);
+ } else if (this.isTimeField(el)) {
+ this.formatTimeField(el, value);
+ } else {
+ this.formatField(el, value);
+ }
+ }
- const distance = this.gallery.touchStart - this.gallery.touchEnd;
- const isLeftSwipe = distance > this.gallery.minSwipe;
- const isRightSwipe = distance < -this.gallery.minSwipe;
+ // Handle link
+ let link = template.querySelector('a');
+ if (link && item.url !== '') {
+ [
+ link.href,
+ link.title
+ ] = [
+ item.url,
+ `View ${item.fields['post_title']??'Item'}`
+ ];
+ }
- if (isLeftSwipe) {
- this.navigate(1);
- } else if (isRightSwipe) {
- this.navigate(-1);
- }
+ if (isTimeline) {
+ this.addTimelineElements(item, template);
+ }
- this.gallery.touchStart = null;
- this.gallery.touchEnd = null;
- });
- }
+ return template;
+ }
+ splitIDs(value) {
+ return String(value).split(',').map((value) => parseInt(value.trim())).filter(value=>value);
+ }
+ isImageField(item, value) {
+ if (!Object.hasOwn(item, 'images') || Object.keys(item.images).length === 0) {
+ return false;
+ }
+ let values = this.splitIDs(value);
- /**
- * Navigate to previous/next image
- */
- async navigate(direction) {
- const newIndex = this.gallery.index + direction;
+ return values.some(v =>
+ Object.keys(item.images).map(k => parseInt(k)).includes(parseInt(v))
+ );
+ }
+ formatImageFields(element, value, item) {
+ let values = this.splitIDs(value); // Convert string to array first
+ if (values.length === 0) return;
- // Check if out of bounds
- if (newIndex < 0 || newIndex >= this.gallery.items.length) {
- this.a11y.announceNavigation(newIndex, this.gallery.items.length,direction < 0, direction > 0)
- return;
- }
+ if (values.length > 1) {
+ let image = element.querySelector('img');
+ if (!image) return;
+ values.forEach(imgID => {
+ let img = image.cloneNode(true);
+ this.formatImageField(img, imgID, item);
+ element.append(img);
+ });
+ image.remove();
+ } else {
+ if (element.tagName !== 'IMG') {
+ element = element.querySelector('img');
+ if (!element) return;
+ }
+ this.formatImageField(element, values[0], item);
+ }
+ }
+ formatImageField(element, value, item) {
+ let imgData = item.images[value]??false;
+ if (!imgData) return;
+ [
+ element.src,
+ element.srcset,
+ element.alt
+ ] = [
+ imgData.tiny,
+ `${imgData.tiny} 50w, ${imgData.small} 300w, ${imgData.medium} 1024w`,
+ imgData['image-alt-text']
+ ]
+ }
+ isTaxonomyField(item, field) {
+ if (!Object.hasOwn(item, 'taxonomies') || Object.keys(item.taxonomies).length === 0) {
+ return false;
+ }
- // Update current index
- this.gallery.index = newIndex;
+ return Object.keys(item.taxonomies).includes(field);
+ }
+ formatTaxonomyField(element, item, field, value) {
+ if (element.tagName !== 'UL' || !element.querySelector('li')) return;
+ let values = this.splitIDs(value);
+ if (values.length === 0) {
+ element.remove();
+ }
+ let listItem = element.querySelector('li');
+ for (let termID of values) {
+ let term = item.taxonomies[field][termID]??false;
+ if (!term) continue;
+ let termItem = listItem.cloneNode(true);
+ let link = termItem.querySelector('a');
+ if (!link) continue;
- // Update display
- this.updateDisplay(newIndex);
+ [
+ link.href,
+ link.title,
+ link.textContent
+ ] = [
+ term.url,
+ `See more ${term.title}`,
+ term.title
+ ];
+ element.append(termItem);
+ }
+ listItem.remove();
+ }
+ isTimeField(el) {
+ return el.tagName === 'TIME' || el.querySelector('time') !== null;
+ }
+ formatTimeField(element, value) {
+ if (element.tagName !== 'TIME') {
+ element = element.querySelector('time');
+ if (!element) return;
+ }
+ element.setAttribute('datetime', value);
+ element.textContent = window.formatTimeAgo(value, 'F Y');
+ }
+ formatField(element, value) {
+ element.textContent = value;
+ }
- // Preload adjacent images
- this.preloadImages();
+ addTimelineElements(item, template) {
+ let [
+ afterEl,
+ number,
+ started,
+ last
+ ] = [
+ template.querySelector('span.after-text'),
+ template.querySelector('[data-field="number"] b'),
+ template.querySelector('[data-field="started"] time'),
+ template.querySelector('[data-field="updated"] time')
+ ];
- // Announce to screen readers
- this.a11y.announceNavigation(this.gallery.index,this.gallery.items.length);
+ if (afterEl) {
+ afterEl.textContent = `After ${item.fields.number} Tx`;
+ }
+ if (number) {
+ number.textContent = item.fields.number;
+ }
+ if (started) {
+ this.formatTimeField(started, item.fields.timeline[0]['post_date']);
+ }
+ if (last) {
+ this.formatTimeField(last, item.fields.timeline[item.fields.timeline.length - 1]['post_date']);
+ }
+ }
- //Load more if we're near the end
- if (direction > 0 && newIndex >= (this.gallery.items.length - 3) && this.state.hasMore) {
- await this.fetchFeed();
- this.updateGalleryItems(this.getGalleryItems());
- }
- }
+ removePlaceholders() {
+ const placeholders = this.ui.grid.querySelectorAll('.placeholder');
+ if (placeholders.length > 0) {
+ placeholders.forEach(p => p.remove());
+ }
+ }
- /**
- * Preload adjacent images
- */
- preloadImages() {
- // Preload current, previous and next images
- [-1, 0, 1].forEach(offset => {
- const index = this.gallery.index + offset;
- if (index >= 0 && index < this.gallery.items.length) {
- const img = new Image();
- const item = this.gallery.items[index];
- if (window.innerWidth < 1000) {
- img.src = item.large || item.src;
- } else {
- img.src = item.full || item.src;
- }
- }
- });
- }
+ addPlaceholders() {
+ let total = this.contentTypes.length;
+ const fragment = document.createDocumentFragment();
+ for (let i = 0; i < 12; i++) {
+ let template = window.getTemplate('placeholderTemplate');
- /**
- * Update display with current image
- */
- updateDisplay(index) {
- const item = this.gallery.items[index];
- if (!item) return;
+ let rand = Math.floor(Math.random() * total);
+ let icon;
+ if (this.ui.content && this.ui.content.length > 0) {
+ icon = this.ui.content.filter((content) => { return content.value === this.contentTypes[rand]}).querySelector('.icon').cloneNode(true);
+ } else {
+ icon = window.getIcon(this.container.dataset.icon);
+ }
+ template.append(icon);
+ fragment.append(template);
+ }
+ this.ui.grid.append(fragment);
+ }
- // Get elements
- const favourite = this.gallery.modal.querySelector('.gallery-favourite');
- const image = this.gallery.modal.querySelector('.gallery-image');
- const counter = this.gallery.modal.querySelector('.gallery-counter');
- const info = this.gallery.modal.querySelector('.item-info');
- // Update image
- image.src = window.innerWidth < 1000 ?
- (item.large || item.src) :
- (item.full || item.src);
- image.alt = item.alt || '';
+ /**
+ *
+ * @param {object} filters {name: value}
+ */
+ updateFilter(filters) {
+ //double check filters are what we're expecting
+ let allowed = ['taxonomy','favourites','match', ... Object.keys(this.filters)];
- // Update favourite button
- if (favourite && item.fav) {
- window.removeChildren(favourite);
- favourite.appendChild(item.fav.cloneNode(true));
- }
+ filters = Object.keys(filters)
+ .filter(key => allowed.includes(key))
+ .reduce((obj, key) => {
+ obj[key] = filters[key];
+ return obj;
+ }, {});
- // Update info
- if (info && item.info) {
- window.removeChildren(info);
- const clone = item.info.cloneNode(true);
- info.appendChild(clone);
- }
+ if (window.getDifferences.map(this.filters, filters)) {
+ this.filters = { ...this.filters, ...filters }; // Merge instead of replace
+ this.updateURL();
+ this.store.setFilters(filters);
+ }
+ }
+ /**
+ * Update visible filters based on selected content type
+ */
+ updateContentFor(contentType) {
+ // Update taxonomy filter visibility
+ const taxonomyButtons = this.ui.filterContainer.querySelectorAll('[data-filter="taxonomy"]');
+ taxonomyButtons.forEach(button => {
+ const forTypes = button.dataset.for?.split(',') || [];
+ button.hidden = forTypes.length > 0 && !forTypes.includes(contentType);
+ });
- // Update counter
- counter.textContent = `${this.gallery.index + 1} / ${this.gallery.items.length}`;
+ // Update ordering options
+ const orderButtons = this.ui.filterContainer.querySelectorAll('[data-for]');
+ orderButtons.forEach(button => {
+ const forTypes = button.dataset.for?.split(',') || [];
+ if (forTypes.length > 0) {
+ button.hidden = !forTypes.includes(contentType);
+ // Uncheck if hiding
+ if (button.hidden && button.checked) {
+ button.checked = false;
+ }
+ }
+ });
- // Update navigation buttons
- this.updateNavigationButtons();
- }
+ // Update order direction visibility based on selected orderby
+ const orderBy = this.ui.filterContainer.querySelector('[name="orderby"]:checked');
+ this.updateOrderDirectionVisibility(orderBy?.value);
+ }
- /**
- * Update navigation button visibility
- */
- updateNavigationButtons() {
- const prevBtn = this.gallery.modal.querySelector('.gallery-prev');
- const nextBtn = this.gallery.modal.querySelector('.gallery-next');
+ /**
+ * Show/hide order direction based on orderby selection
+ */
+ updateOrderDirectionVisibility(orderBy) {
+ const orderDirection = this.ui.filterContainer.querySelector('.order-direction');
+ if (orderDirection) {
+ const forOrders = orderDirection.dataset.forOrder?.split(',') || [];
+ orderDirection.hidden = forOrders.length > 0 && !forOrders.includes(orderBy);
+ }
+ }
+ /*********************************************************************
+ LISTENERS
+ *********************************************************************/
+ initListeners() {
+ this.popStateHandler = this.handlePopState.bind(this);
+ this.clickHandler = this.handleClick.bind(this);
+ this.changeHandler = this.handleChange.bind(this);
+ this.imageObserver = null;
+ this.resizeObserver = null;
+ if ('IntersectionObserver' in window) {
+ this.imageObserver = new IntersectionObserver(entries => {
+ entries.forEach(entry => {
+ this.loadImage(entry.target);
+ this.imageObserver.unobserve(entry.target);
+ });
+ }, {
+ rootMargin: '100px',
+ threshold: .1
+ });
+ }
- prevBtn.classList.toggle('end', this.gallery.index > 0 ? '' : 'none');
- nextBtn.classList.toggle('end', this.gallery.index < this.gallery.items.length - 1 ? '' : 'none');
- }
+ if ('ResizeObserver' in window) {
+ this.resizeObserver = new ResizeObserver(() => {
+ window.debouncer.schedule(
+ 'feed-update-images',
+ () => this.updateImageSizes(),
+ 250
+ );
+ });
+ } else {
+ window.addEventListener('resize', () => {
+ window.debouncer.schedule(
+ 'feed-update-images',
+ () => this.updateImageSizes(),
+ 250
+ );
+ });
+ }
- /**
- * Close the gallery
- */
- closeGallery() {
- this.showBody();
- // Remove event listeners
- document.removeEventListener('keydown', this.gallery.keyHandler);
- this.a11y.announce('Gallery closed.');
+ window.addEventListener('popstate', this.popStateHandler);
+ document.addEventListener('click', this.clickHandler);
+ document.addEventListener('change', this.changeHandler);
+ }
- this.gallery.modal.close();
+ handlePopState(e) {
+ if (e.state?.filters) {
+ if (this.processURLFilters()) {
+ this.store.setFilters(this.filters);
+ this.a11y.announce('Feed filters updated from browser history');
+ }
+ }
+ }
- // Reset state
- this.gallery.keyHandler = null;
- }
+ handleClick(e) {
+ if (window.targetCheck(e, this.elements.loadMore)) {
+ this.nextPage();
+ } else if (window.targetCheck(e, this.elements.clearFilter)) {
+ this.clearAllTaxonomies();
+ } else if (window.targetCheck(e, '.remove-item')) {
+ this.handleRemoveSelectedTerm(e);
+ }
+ }
- /**
- * Update gallery items
- * @param {Array} newItems - New gallery items
- */
- updateGalleryItems(newItems) {
- // Store original current index and item
- const currentItem = this.gallery.items[this.gallery.index];
+ handleRemoveSelectedTerm(e) {
+ const selectedItem = e.target.closest('.selected-item');
+ if (!selectedItem) return;
- // Update items array
- this.gallery.items = newItems;
+ const termId = parseInt(selectedItem.dataset.id);
+ const taxonomy = selectedItem.dataset.taxonomy;
- // Try to keep the same item selected
- if (currentItem) {
- // Find the same item in the new array by matching source
- const newIndex = this.gallery.items.findIndex(item =>
- item.full === currentItem.full ||
- item.large === currentItem.large
- );
+ // Remove from filters
+ if (this.taxonomyFilters[taxonomy]) {
+ this.taxonomyFilters[taxonomy] = this.taxonomyFilters[taxonomy]
+ .filter(id => id !== termId);
- if (newIndex !== -1) {
- this.gallery.index = newIndex;
- }
- }
+ if (this.taxonomyFilters[taxonomy].length === 0) {
+ delete this.taxonomyFilters[taxonomy];
+ }
+ }
- // Update navigation buttons
- this.updateNavigationButtons();
- }
+ // Remove from UI
+ selectedItem.remove();
- /**
- * Ensure gallery is accessible
- */
- setupGalleryAccessibility() {
- // Add ARIA attributes
- this.gallery.modal.setAttribute('aria-modal', 'true');
- this.gallery.modal.setAttribute('aria-label', 'Image Gallery');
- }
+ // Update filters
+ this.updateFilter({
+ taxonomy: Object.keys(this.taxonomyFilters).length > 0
+ ? this.taxonomyFilters
+ : null,
+ page: 1
+ });
+ }
- hideBody(){
- document.body.style.overflow = 'hidden';
- }
- showBody(){
- document.body.style.overflow = '';
- }
+ handleChange(e) {
+ let target = e.target;
+ if (Object.hasOwn(target.dataset, 'filter')) {
+ if (target.dataset.filter === 'content') {
+ this.updateContentFor(target.value);
+ this.updateFilter({ content: target.value, page: 1 });
+ } else if (target.dataset.filter === 'orderby') {
+ this.updateOrderDirectionVisibility(target.value);
+ this.updateFilter({ orderby: target.value, page: 1 });
+ } else if (target.dataset.filter === 'order') {
+ this.updateFilter({ order: target.value, page: 1 });
+ } else if (target.dataset.filter === 'match') {
+ this.updateFilter({ match: target.checked ? 'all' : 'any', page: 1 });
+ } else if (target.dataset.filter === 'favourites') {
+ this.updateFilter({ favourites: target.checked, page: 1 });
+ }
+ }
+ }
}
-// Initialize feed blocks when DOM is loaded
-document.addEventListener('DOMContentLoaded', () => {
- document.querySelectorAll('.feed-block').forEach(container => {
- // Initialize with both the container and overlay
- window.feedBlock = new FeedBlock(container);
- });
+document.addEventListener('DOMContentLoaded', async function() {
+ window.auth.subscribe(event => {
+ if (event === 'auth-loaded') {
+ window.feedBlock = new FeedBlock();
+ }
+ });
});
-
-
-function isEmptyObject(obj) {
- return Object.keys(obj).length === 0;
-}
--
Gitblit v1.10.0