From b5abd615697146beeca6dba4acd057d049554a30 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 02 Jan 2026 00:16:00 +0000
Subject: [PATCH] Merge branch 'main' of https://github.com/jakevdwerf/jvb
---
src/feed/viewOld.js | 1996 +++++++++++++++------------------------------------------
1 files changed, 542 insertions(+), 1,454 deletions(-)
diff --git a/src/feed/viewOld.js b/src/feed/viewOld.js
index 347e997..29859f9 100644
--- a/src/feed/viewOld.js
+++ b/src/feed/viewOld.js
@@ -1,968 +1,529 @@
class FeedBlock {
- static LOADING_QUIPS = JSON.parse(feedSettings.quips);
- constructor(container){
+ constructor() {
+ this.cache = window.jvbCache;
+ this.a11y = window.jvbA11y;
+ this.loading = window.jvbLoading;
+ this.error = window.jvbError;
- this.cache = window.jvbCache;
- this.eventHandlers = new Map();
- this.rendered = {};
+ this.container = document.querySelector('section.feed-block');
+ if (!this.container) {
+ return;
+ }
- // Store container references
- this.container = container;
- this.a11y = window.jvbA11y;
+ this.openGallery = false;
+ this.initElements();
+ this.addPlaceholders();
+ this.config = {
+ api: feedSettings.apiUrl,
+ nonce: feedSettings.nonce,
+ user: jvbSettings.currentUser || null,
+ source: '',
+ context: '',
+ highlight: null,
+ gallery: false,
+ showAuthor: true,
+ showDate: false,
+ view: localStorage.getItem('feedViewMode') || 'grid',
+ ... this.container.dataset
+ };
+ this.taxonomies = {};
+ this.rendered = {};
- //For tracking cache and current requests
- this.currentRequest = null;
- this.timeoutId = null;
+ this.feed = {
+ imageLoadThreshold: 5,
+ lazyLoadOffset: '100px',
+ gallery: [],
+ loaded: 0,
+ intsersectionObserver: null,
+ templates: new Map()
+ };
- // Initialize Config
- this.initConfig();
+ this.isLoading = false;
+ this.hasMore = true;
+ this.retries = {
+ count: 0,
+ max: 3,
+ delay: 1000
+ };
+ this.page = 1;
+ this.order = 'DESC';
+ this.orderby = 'date';
+ this.gallery = (this.config.gallery) ? new window.jvbGallery(document.querySelector('dialog.gallery'), {
+ imageWrapper: '.item',
+ loadMore: ()=>this.fetchFeed.bind(this)
+ }) : false;
+ this.initListeners();
+ if (this.page === 1) {
+ this.processURLFilters();
+ } else {
+ this.updateFilters();
+ }
- // 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);
- }
+ }
+ initElements() {
+ this.filterSelector = 'form.feed-filters';
+ this.filterForm = this.container.querySelector(this.filterSelector);
+ this.grid = this.container.querySelector('.item-grid');
+ this.loadMore = this.container.querySelector('.load-more');
+ this.filterControls = this.container.querySelector('.filter-actions');
+ this.contentTypes = Array.from(this.filterForm.querySelectorAll('input[name="content"]')).map(
+ content => {
+ return content.value;
});
- }
- return this.config.highlight;
- }
+ this.selectedTerms = this.container.querySelector('.selected-items-section .selected-items');
+ }
- /**
- * 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);
- }
-
- }
+ initListeners() {
+ window.addEventListener('popstate', this.handlePopState.bind(this));
+ document.addEventListener('click', this.handleClick.bind(this));
+ document.addEventListener('change', this.handleChange.bind(this));
- this.a11y.announce(`Checking for more ${tax} ${content}...`);
- } else {
- this.hideLoading();
- }
+ // 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: '100px',
+ threshold: 0.1
+ });
+ }
+ // Resize observer for responsive images
+ if ('ResizeObserver' in window) {
+ this.resizeObserver = new ResizeObserver(window.debounce(() => {
+ this.updateImageSizes();
+ }, 250));
- // Update loading spinner
- this.elements.spinner.hidden = !loading;
+ // Observe the container
+ this.resizeObserver.observe(this.container);
+ } else {
+ // Fallback to window resize
+ window.addEventListener('resize', window.debounce(() => {
+ this.updateImageSizes();
+ }, 250));
+ }
- // Update load more button
- this.elements.loadMore.disabled = loading;
-
- }
+ this.taxonomies = {};
+ this.container.querySelectorAll('.jvb-selector:not([hidden])').forEach(selector => {
+ let taxonomy = selector.dataset.taxonomy;
+ if (!Object.hasOwn(this.taxonomies, taxonomy)) {
+ this.taxonomies[taxonomy] = new window.jvbTaxonomySelector(
+ selector,
+ {
+ multiple: true,
+ feed: true,
+ selected: {},
+ onClose: () => this.setSelectedTerms(taxonomy),
+ }
+ );
+ }
+ });
+ }
- /**
- * Show the loading overlay
- */
- showLoading() {
- this.hideBody();
- this.elements.loading.classList.add('active');
- this.startQuipCycle();
- document.body.classList.add('loading');
- }
+ /**
+ * Handle browser history navigation
+ */
+ handlePopState(e) {
+ if (e.state && e.state.filters) {
+ if(this.processURLFilters()){
+ // Load items with updated filters
+ this.resetPage();
+ this.fetchFeed();
- /**
- * Hide the loading overlay
- */
- hideLoading() {
- this.showBody();
- this.container.classList.remove('active');
- this.stopQuipCycle();
- document.body.classList.remove('loading');
- }
+ // Announce to screen readers
+ this.a11y.announce('Feed filters updated from browser history.');
+ }
+ }
+ }
+
+ processURLFilters() {
+ const params = new URLSearchParams(window.location.search);
+ //No parameters to process
+ if (!params.toString()) {
+ this.updateFilters();
+ return;
+ }
+
+ let filters = ['content', 'order', 'orderby', 'favourites','match'];
+
+ filters.forEach(filter => {
+ let value = params.get('f_'+filter);
+ params.delete('f_'+filter);
+ if (value && this.filterForm.querySelector(`input[name="${filter}"][value="${value}"]`)) {
+ this.filterForm.querySelector(`input[name="${filter}"][value="${value}"]`).checked = true;
+ }
+ });
+
+ let unprocessed = {};
+ for (var [key, value] of Object.entries(Object.fromEntries(params))) {
+
+ key = key.replace('f_','');
+ if (this.contentTypes.includes(key)) {
+ this.openGallery = value;
+ } else {
+ this.taxonomies[key].addTermsFromURL(value);
+ this.setSelectedTerms(key);
+ }
+ }
+
+ this.updateFilters();
+ }
+
+ handleClick(e) {
+ if (e.target.classList.contains('load-more') || e.target.closest('.load-more')) {
+ this.fetchFeed(false);
+ e.target.disabled = true;
+ } else if (e.target.classList.contains('clear-filters') || e.target.closest('.clear-filters')) {
+ this.resetFilters();
+ } else if (this.config.gallery && e.target.closest('.feed-image')) {
+ this.gallery.handleGalleryOpen(e);
+ } else if (e.target.classList.contains('.remove-item') || e.target.closest('.remove-item')) {
+ let tag = e.target.closest('.selected-item');
+ let taxonomy = tag.dataset.taxonomy;
+ this.taxonomies[taxonomy].removeSelectedTerm(tag.dataset.id);
+ this.setSelectedTerms(taxonomy);
+ this.updateFilters();
+ }
+ }
+ handleChange(e) {
+ if (e.target.closest(this.filterSelector)) {
+ this.resetPage();
+ window.removeChildren(this.grid);
+ this.addPlaceholders();
+ //update filters
+ this.updateFilters();
+ }
+ }
+
+ updateFilters() {
+ this.page = 1;
+ const params = new URLSearchParams(window.location.search);
+
+ let filters = Object.fromEntries(new FormData(this.filterForm));
+
+ let contents = [];
+ for (let [key, value] of Object.entries(filters)) {
+ let set = false;
+ switch (key) {
+ case 'content':
+ if (value !== this.contentTypes[0]) {
+ set = true;
+ } else {
+ params.delete('f_'+key);
+ }
+ break;
+ case 'orderby':
+ if (value !== 'date') {
+ set = true;
+ }
+ break;
+ case 'order':
+ if (value !== 'desc') {
+ set = true;
+ }
+ break;
+ default:
+ set = true;
+ }
+ if (!set) {
+ params.delete('f_'+key);
+ }
- /**
- * Start cycling through loading messages
- */
- startQuipCycle() {
- if (this.quipInterval) {
- clearInterval(this.quipInterval);
- }
+ if (set && value !== false && value !== '') {
+ params.set('f_'+key, value);
+ }
+ if (value !== '') {
+ contents.push(value);
+ }
- if (!this.quips.length) return;
+ const newURL = `${window.location.pathname}?${params.toString()}`;
+ history.pushState(filters, '', newURL);
- // Set initial message
- this.updateMessage(this.quips[0]);
- this.container.classList.remove('changing');
+ }
- this.quipInterval = setInterval(() => {
- this.container.classList.add('changing');
+ this.filters = filters;
+ this.updateContentFor(filters.content);
- setTimeout(() => {
- this.loadingIndex = (this.loadingIndex + 1) % this.quips.length;
- this.updateMessage(this.quips[this.loadingIndex]);
+ this.updateFilterControls();
- setTimeout(() => {
- this.container.classList.remove('changing');
- }, 50);
- }, 350);
- }, this.loadingOptions.cycleInterval);
- }
+ this.loading.setContent(contents);
+ this.fetchFeed(true);
+ }
- /**
- * Stop cycling through loading messages
- */
- stopQuipCycle() {
- if (this.quipInterval) {
- clearInterval(this.quipInterval);
- this.quipInterval = null;
- }
- }
+ updateFilterControls() {
+ this.filterControls.hidden = this.selectedTerms.children.length < 2;
+ }
- /**
- * Update the loading message
- */
- updateMessage(quipData) {
- if (!this.loadingMessage) return;
+ /**
+ * Toggles taxonomy selectors and certain order/orderby options
+ * depending on current content
+ * @param content
+ */
+ updateContentFor(content) {
+ this.filterForm.querySelectorAll('.jvb-selector').forEach(tax => {
+ let hasContent = tax.dataset.for.includes(content);
+ tax.hidden = !hasContent;
+ if (!hasContent) {
+ let t = tax.dataset.taxonomy;
+ this.clearSelectedTerms(t);
+ }
+ });
+ this.filterForm.querySelectorAll('input[data-for]').forEach(toggle => {
+ toggle.hidden = !toggle.dataset.for.includes(content);
+ });
+ this.filterForm.querySelectorAll('input[name="order"]').forEach(order => {
+ order.hidden = this.filters.order === 'random';
+ });
+ }
- const icon = feedSettings?.icons?.[quipData.icon] || '';
- this.loadingMessage.innerHTML = `${icon}<p>${quipData.quip}</p>`;
- }
+ clearSelectedTerms(taxonomy) {
+ this.filterForm.querySelector(`input[name="${taxonomy}"]`).value = '';
+ if (Object.hasOwn(this.taxonomies, taxonomy)) {
+ this.taxonomies[taxonomy].selectedItems = {};
+ }
+ }
- /**
- * 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
- });
- });
- }
- });
- }
+ setSelectedTerms(taxonomy) {
+ let input = this.filterForm.querySelector(`input[name="${taxonomy}"]`);
+ input.value = '';
+ let selected = this.taxonomies[taxonomy].selectedTerms;
+ if (!window.isEmptyObject(selected)) {
+ let ids = Object.keys(selected);
+ input.value = ids.join(',');
+ }
+ this.updateFilters();
+ }
- // Shuffle the quips array
- return this.shuffleArray(allQuips);
- }
+ nextPage() {
+ if (this.hasMore) {
+ this.page++;
+ }
+ }
+ resetPage() {
+ this.page = 1;
+ this.hasMore = true;
+ }
+ resetState() {
+ this.resetPage(true);
+ this.isLoading = false;
+ this.retries = {
+ count: 0,
+ max: 3,
+ delay: 1000
+ };
+ }
- /**
- * Feed Grid
- */
+ resetFilters() {
+ this.filterForm.reset();
+ //check the first content
+ this.filterForm.querySelector('input[name="content"]').checked = true;
+ this.filterForm.querySelector('input[name="orderby"][value="date"]').checked = true;
+ this.page = 1;
+ this.updateFilters();
+ }
- 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);
- }
+ buildFilterRequest() {
- // Bail early if no items
- if (items.length === 0) {
- this.a11y.announceUpdate(0, append);
- return;
- }
+ let filters = {};
- // Use DocumentFragment for better performance
- const fragment = document.createDocumentFragment();
+ for (let [filter, value] of Object.entries(this.filters)) {
+ if (value !== false && value !== '') {
+ filters[filter] = value;
+ }
+ }
+ filters.page = parseInt(this.page);
+ if (this.container.dataset.context) {
+ filters.context = this.container.dataset.context;
+ }
+ if (this.container.dataset.source) {
+ filters.source = this.container.dataset.source;
+ }
+ return new URLSearchParams(filters).toString();
+ }
- // Process items in batches for better performance
- const batchSize = 10;
- const processBatch = (startIndex) => {
- const endIndex = Math.min(startIndex + batchSize, items.length);
+ async fetchFeed(reset = false, force = false) {
+ if (this.isLoading) {
+ return false;
+ }
+ this.loading.showLoading(this.filters);
+ try {
+ if (this.page === 1) {
+ window.removeChildren(this.grid);
+ this.addPlaceholders();
+ }
- // Process this batch
- for (let i = startIndex; i < endIndex; i++) {
- const item = items[i];
- const element = this.createItemElement(item);
- fragment.appendChild(element);
+ const data = await this.cache.fetchWithCache(
+ `${this.config.api}feed?${this.buildFilterRequest()}`,
+ {
+ method: 'GET',
+ },
+ {
+ context: 'feed',
+ forceRefresh: true
+ // forceRefresh: force
+ }
+ );
- // Lazy load images beyond threshold
- if (this.feed.loaded >= this.feed.imageLoadThreshold && this.imageObserver) {
- this.imageObserver.observe(element);
- } else {
- this.loadImage(element);
- }
+ //Handle empty results
+ if (!data || !data.items || data.items.length === 0) {
+ if (this.page === 1) {
+ this.showEmptyState();
+ }
+ this.hasMore = false;
+ return false;
+ } else {
+ this.hasMore = data['has_more'];
- this.feed.loaded++;
- }
+ this.renderItems(data.items, this.page > 1);
- // 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);
- }
- };
+ if (this.hasMore) {
+ this.nextPage();
+ }
+ return true;
+ }
+ } catch (error) {
+ this.handleError(error);
+ } finally {
+ this.loading.hideLoading();
+ if (this.openGallery !== false) {
+ this.gallery.openWhenReady = this.openGallery;
+ this.openGallery = false;
+ }
+ this.loadMore.disabled = false;
+ this.loadMore.hidden = !this.hasMore;
+ }
+ }
- // Start processing the first batch
- if (items.length > 0) {
- processBatch(0);
- } else {
- this.a11y.announceUpdate(0, append);
- }
- }
+ removePlaceholders() {
+ if (this.grid.querySelector('.placeholder')) {
+ window.removeChildren(this.grid);
+ }
+ }
+ showEmptyState() {
+ window.removeChildren(this.grid);
+ let template = window.getTemplate('emptyState');
+ let isFavourite = Object.hasOwn(this.filters, 'favourites') && this.filters.favourites === true;
+ if (isFavourite) {
+ [
+ template.querySelector('h3').textContent,
+ template.querySelector('p:first-of-type').textContent,
+ template.querySelector('p:last-of-type').textContent,
+ ] = [
+ '♡ BLANK CANVAS ♡',
+ 'You haven\'t fallen in love with any pieces... yet!',
+ 'Hit that heart icon when something stops your scroll — your dream collection is waiting to start.'
+ ];
+ }
+ this.grid.append(template);
+ this.a11y.announceEmpty(isFavourite);
- /**
- * 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;
+ }
+ handleError(error){
+ return this.error.handleApiError(
+ error,
+ {
+ component: 'Feed Block',
+ action: 'loaditems'
+ },
+ () => this.fetchFeed()
+ );
+ }
- element.setAttribute('data-loaded', 'true');
- }
+ addPlaceholders() {
+ let total = this.contentTypes.length - 1;
+ for (let i = 0; i < 9; i++) {
+ let template = window.getTemplate('placeholderTemplate');
+ let rand = Math.floor(Math.random()*total+1);
+ let icon = window.getIcon(this.contentTypes[rand]).cloneNode(true);
- /**
- * Update image sizes based on screen width
- */
- updateImageSizes() {
- const size = this.getImageSize();
+ template.append(icon);
+ this.grid.append(template);
+ }
+ }
+ renderItems(items, append = false) {
+ //Clear the grid if we aren't appending
+ if (!append) {
+ window.removeChildren(this.grid);
+ this.addPlaceholders();
+ }
- // 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) {
+ //Bail early if no items
+ if (items.length === 0) {
+ this.a11y.announceUpdate(0, append);
+ return;
+ }
- if(!this.rendered[item.icon]){
- this.rendered[item.icon] = new Map();
- }
+ //Use DocumentFragment for better performance
+ const fragment = document.createDocumentFragment();
- if(this.rendered[item.icon].has(item.id)){
- return this.rendered[item.icon].get(item.id);
- }
+ const batchSize = 10;
+ const processBatch = (startIndex) => {
+ const endIndex = Math.min(startIndex + batchSize, items.length);
- const favourited = window.isFavourited(item.icon, item.id)??false;
- const template = window.getTemplate('feed-item');
+ for (let i = startIndex; i < endIndex; i++) {
+ const item = items[i];
+ const element = this.createItemElement(item);
+ fragment.appendChild(element);
- // Set unique attributes
- template.id = `${item.icon}-${item.id}`;
- template.classList.add(item.icon);
+ this.imageObserver.observe(element);
+ }
- if (item.umami_view) {
- this.buildUmamiData(template, item.umami_view);
+ if (endIndex < items.length) {
+ requestAnimationFrame(() => {
+ processBatch(endIndex);
+ });
+ } else {
+ this.removePlaceholders();
+ //all batches are processed, append fragment
+ this.grid.appendChild(fragment);
+ if (this.config.gallery) {
+ this.gallery.updateGalleryItems(this.gallery.getGalleryItems());
+ }
+ this.a11y.makeNavigable(this.grid.querySelectorAll('.item:not([data-keyboard-nav])'));
+ this.a11y.announceItems(items.length, append, this.hasMore);
+ }
+ };
+
+ if (items.length > 0) {
+ processBatch(0);
+ } else {
+ this.a11y.announceUpdate(0, append);
+ }
+ }
+
+ /**
+ * Creates a feed-item. Used by RenderItems
+ */
+ 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');
+
+ template.id = `${item.icon}-${item.id}`;
+ template.dataset.id = item.id;
+ template.classList.add(item.icon);
+
+ if (item['umami_view']) {
+ this.buildUmamiData(template, item['umami_view']);
}
let favouriteButton = template.querySelector('button.favourite');
@@ -974,7 +535,7 @@
] = [
item.id,
item.icon,
- item.user_id,
+ item['user_id'],
(favourited) ? 'Remove from Favourites' : 'Add to Favourites'
];
@@ -985,7 +546,7 @@
let summary = template.querySelector('summary');
let info = template.querySelector('.item-info');
- for (let [index, id] of Object.entries(order)){
+ for (let [index, id] of Object.entries(order)) {
let target;
let config = item[id];
if (id === 'title') {
@@ -1009,7 +570,7 @@
} else {
target.remove();
}
- } else if (Object.hasOwn(config, 'terms')) {
+ } else if (Object.hasOwn(config, 'terms')) {
//Taxonomy list
if (config.terms.length === 0) {
continue;
@@ -1084,7 +645,10 @@
let img = images.querySelector('a');
let main = img.cloneNode(true);
- main.href = item.url;
+ if (!this.config.gallery) {
+ main.href = item.url;
+ }
+
main.classList.add('feed-image');
this.buildImageData(main.querySelector('img'), item.image);
images.append(main);
@@ -1093,7 +657,9 @@
images.classList.add('multi');
item.content.forEach(c => {
let image = img.cloneNode(true);
- image.href = c.url;
+ if (!this.config.gallery) {
+ image.href = c.url;
+ }
let itemImg = image.querySelector('img');
itemImg.src = c.image.small;
itemImg.alt = c.image.alt;
@@ -1106,552 +672,74 @@
single.remove();
list.remove();
- this.rendered[item.icon].set(item.id, template);
+ this.rendered[item.icon].set(item.id, template);
- return template;
- }
+ return template;
+ }
- buildImageData(img, data){
+ buildImageData(img, data){
if (typeof data.tiny !== 'string') {
return;
}
- [
- img.src,
- img.dataset.small,
- img.dataset.medium,
- img.dataset.large,
- img.alt
- ] =
- [
- data.tiny,
- data.small,
- data.medium,
- data.large,
- data.alt
- ];
- }
+ [
+ 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;
- }
- }
+ buildUmamiData(item, data){
+ for(let [key, value] of Object.entries(data)){
+ item.dataset[key] = value;
+ }
+ }
- /**
- * 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>`;
+ /**
+ * Load Image, used by renderItems
+ * @param element
+ */
+ loadImage(element) {
+ const img = element.querySelector('img');
+ if (!img) return;
+ const size = this.getImageSize();
- this.elements.grid.innerHTML = message;
- this.a11y.announceEmpty(this.filters.favourites);
- }
+ img.src = img.dataset[size] || img.dataset.src;
+ element.setAttribute('data-loaded', 'true');
+ }
- /**
- * Clear the grid
- */
- clearGrid() {
- this.a11y.announce('Items cleared.');
- window.removeChildren(this.elements.grid);
- this.feed.loaded = 0;
- }
-
- /**
- * Get image size based on screen width
- */
- getImageSize() {
- const width = window.innerWidth;
- if (width > 1024) return 'medium';
- if (width > 500) return 'medium';
- return 'small';
- }
-
- /**
- * 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;
-
- 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;
-
- const boundHandler = handler.bind(this);
- element.addEventListener(event, boundHandler, options);
-
- // 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();
-
- // Clean up timers
- if (this.quipInterval) {
- clearInterval(this.quipInterval);
- }
-
- if (this.timeoutId) {
- clearTimeout(this.timeoutId);
- }
-
- // Clear template cache and other state
- this.feed.templates.clear();
- this.feed.gallery = [];
- this.feed.loaded = 0;
- }
- /** Extra Term Handling **/
-
- 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}"]`
- );
-
- return Array.from(selectedItems).map(item => item.dataset.id);
- }
-
- clearSelectedTaxonomies(){
- window.removeChildren(this.elements.selected);
- if(!isEmptyObject(this.selectorInstances)){
- for(var [taxonomy, instance] of Object.entries(this.selectorInstances)){
- instance.selectedItems = {};
- }
- }
-
- this.elements.matchAll.querySelector(input).checked = false;
-
- this.filters.taxonomies = {};
- this.updateFilters();
- }
- setSelectedTerms(taxonomy){
- if(this.selectorInstances[taxonomy]){
- let selected = this.selectorInstances[taxonomy].selectedItems;
- if(!isEmptyObject(selected)){
-
- 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];
- }
-
- }
- }
- 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){
-
- 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 = {};
- }
-
-
- const selectedItems = this.elements.selected.querySelectorAll(
- `.selected-item[data-taxonomy="${taxonomy}"]`
- );
-
- if(selectedItems.length > 0){
- selectedItems.forEach(item => {
- item.remove();
- });
- }
-
- // Update clear filters button visibility
- this.updateClearFiltersButton();
- }
- updateClearFiltersButton(){
- if (!this.elements.clearFilters) return;
-
- let filters = this.elements.selected.children.length;
-
- const hasFilters = filters > 0;
-
- const hasMultiple = filters > 1;
-
- this.elements.clearFilters.hidden = !hasFilters;
-
- this.elements.filters.classList.toggle('has-filters', hasFilters);
-
- this.elements.matchAll.hidden = !hasMultiple;
- }
-
-
- /**
- * Create a filter tag element
- */
- createFilterTag(taxonomy, id, name) {
- const tag = window.getTemplate('selectedTerm');
-
- 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)}`];
-
-
-
- return tag;
- }
-
- /**
- * Gallery
- **/
- openGallery(index){
- this.gallery.index = index;
- this.gallery.modal.showModal();
- this.hideBody();
-
- this.bindGalleryEvents();
- //show current image
- this.updateDisplay(index);
- //preload adjacent images
- this.preloadImages();
-
- // 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);
-
- }
-
- /**
- * 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');
-
- modal.innerHTML = `
- <button class="gallery-close" aria-label="Close gallery">
- ${jvbSettings.icons.close}
- </button>
-
- <button class="gallery-nav gallery-prev" aria-label="Previous image">
- ${jvbSettings.icons.prev}
- </button>
-
- <button class="gallery-nav gallery-next" aria-label="Next image">
- ${jvbSettings.icons.next}
- </button>
-
- <div class="gallery-content">
- <img src="" alt="" class="gallery-image">
- <details>
- <summary>DETAILS</summary>
- <div class="item-info"></div>
- </details>
- </div>
-
- <div class="gallery-favourite"></div>
- <div class="gallery-counter"><span id="gallery-index">1</span> / <span class="total"></span></div>
- `;
-
- return modal;
- }
-
-
- /**
- * Bind event handlers
- */
- bindGalleryEvents() {
- // Close button
- this.gallery.modal.querySelector('.gallery-close').addEventListener('click', () => this.closeGallery());
-
- // Navigation buttons
- const prevBtn = this.gallery.modal.querySelector('.gallery-prev');
- const nextBtn = this.gallery.modal.querySelector('.gallery-next');
-
- prevBtn.addEventListener('click', () => this.navigate(-1));
- nextBtn.addEventListener('click', () => this.navigate(1));
-
- // 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);
-
- // Touch events
- this.gallery.modal.addEventListener('touchstart', (e) => {
- this.gallery.touchStart = e.touches[0].clientX;
- });
-
- this.gallery.modal.addEventListener('touchmove', (e) => {
- this.gallery.touchEnd = e.touches[0].clientX;
- });
-
- this.gallery.modal.addEventListener('touchend', () => {
- if (!this.gallery.touchStart || !this.gallery.touchEnd) return;
-
- const distance = this.gallery.touchStart - this.gallery.touchEnd;
- const isLeftSwipe = distance > this.gallery.minSwipe;
- const isRightSwipe = distance < -this.gallery.minSwipe;
-
- if (isLeftSwipe) {
- this.navigate(1);
- } else if (isRightSwipe) {
- this.navigate(-1);
- }
-
- this.gallery.touchStart = null;
- this.gallery.touchEnd = null;
- });
- }
-
- /**
- * Navigate to previous/next image
- */
- async navigate(direction) {
- const newIndex = this.gallery.index + direction;
-
- // 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;
- }
-
- // Update current index
- this.gallery.index = newIndex;
-
- // Update display
- this.updateDisplay(newIndex);
-
- // Preload adjacent images
- this.preloadImages();
-
- // Announce to screen readers
- this.a11y.announceNavigation(this.gallery.index,this.gallery.items.length);
-
- //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());
- }
- }
-
- /**
- * 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;
- }
- }
- });
- }
-
- /**
- * Update display with current image
- */
- updateDisplay(index) {
- const item = this.gallery.items[index];
- if (!item) return;
-
- // 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 || '';
-
- // Update favourite button
- if (favourite && item.fav) {
- window.removeChildren(favourite);
- favourite.appendChild(item.fav.cloneNode(true));
- }
-
- // Update info
- if (info && item.info) {
- window.removeChildren(info);
- const clone = item.info.cloneNode(true);
- info.appendChild(clone);
- }
-
- // Update counter
- counter.textContent = `${this.gallery.index + 1} / ${this.gallery.items.length}`;
-
- // Update navigation buttons
- this.updateNavigationButtons();
- }
-
- /**
- * Update navigation button visibility
- */
- updateNavigationButtons() {
- const prevBtn = this.gallery.modal.querySelector('.gallery-prev');
- const nextBtn = this.gallery.modal.querySelector('.gallery-next');
-
- prevBtn.classList.toggle('end', this.gallery.index > 0 ? '' : 'none');
- nextBtn.classList.toggle('end', this.gallery.index < this.gallery.items.length - 1 ? '' : 'none');
- }
-
- /**
- * Close the gallery
- */
- closeGallery() {
- this.showBody();
- // Remove event listeners
- document.removeEventListener('keydown', this.gallery.keyHandler);
- this.a11y.announce('Gallery closed.');
-
- this.gallery.modal.close();
-
- // Reset state
- this.gallery.keyHandler = null;
- }
-
- /**
- * 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];
-
- // Update items array
- this.gallery.items = newItems;
-
- // 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
- );
-
- if (newIndex !== -1) {
- this.gallery.index = newIndex;
- }
- }
-
- // Update navigation buttons
- this.updateNavigationButtons();
- }
-
- /**
- * Ensure gallery is accessible
- */
- setupGalleryAccessibility() {
- // Add ARIA attributes
- this.gallery.modal.setAttribute('aria-modal', 'true');
- this.gallery.modal.setAttribute('aria-label', 'Image Gallery');
- }
+ /**
+ * Updates the image size according to screen size
+ */
+ updateImageSizes() {
+ const size = this.getImageSize();
+ const items = this.grid.querySelectorAll('.item');
+ items.forEach(item => {
+ const img = item.querySelector('img');
+ if (img && img.dataset[size] && img.src !== img.dataset[size]) {
+ img.src = img.dataset[size];
+ }
+ });
+ }
+ /**
+ * Get image size based on screen width
+ */
+ getImageSize() {
+ const width = window.innerWidth;
+ if (width > 1024) return 'medium';
+ if (width > 500) return 'medium';
+ return 'small';
+ }
- hideBody(){
- document.body.style.overflow = 'hidden';
- }
- showBody(){
- document.body.style.overflow = '';
- }
}
-
-// 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);
- });
+ window.feedBlock = new FeedBlock();
});
-
-
-function isEmptyObject(obj) {
- return Object.keys(obj).length === 0;
-}
--
Gitblit v1.10.0