/* BIOTEN Observer V7.16.0: homepage latest-eight, search, tags, pagination and resilient covers. */ (() => { 'use strict'; const DEFAULT_COMPANY_COVER = 'assets/images/bioten-observer/company-news-default.jpg'; const DEFAULT_INDUSTRY_COVER = 'assets/images/bioten-observer/industry-insights-default.jpg'; const HOME_ITEM_LIMIT = 8; const isEnglish = () => document.documentElement.lang.toLowerCase().startsWith('en'); const text = (cn, en) => (isEnglish() ? en : cn); function updateLanguageAttributes(root = document) { root.querySelectorAll('[data-aria-cn][data-aria-en]').forEach((element) => { element.setAttribute('aria-label', isEnglish() ? element.dataset.ariaEn : element.dataset.ariaCn); }); root.querySelectorAll('[data-placeholder-cn][data-placeholder-en]').forEach((element) => { element.setAttribute('placeholder', isEnglish() ? element.dataset.placeholderEn : element.dataset.placeholderCn); }); } function installCoverFallbacks() { document.querySelectorAll('.observer-card__media img, .observer-home-card__media img, .observer-article-cover img, .observer-empty > img').forEach((image) => { image.addEventListener('error', () => { const isIndustry = image.closest('[data-observer-category="industry"], [data-observer-mode="industry"]'); const fallback = isIndustry ? DEFAULT_INDUSTRY_COVER : DEFAULT_COMPANY_COVER; if (!image.src.endsWith(fallback)) image.src = fallback; }, { once: true }); }); } function initializeHomeFeed() { const grid = document.querySelector('[data-bioten-observer-home] .observer-home__grid'); if (!grid) return; const observer = new MutationObserver(() => { sortAndLimit(); }); function sortAndLimit() { observer.disconnect(); const cards = Array.from(grid.querySelectorAll(':scope > .observer-home-card')) .map((card, sourceIndex) => { const publishedAt = Date.parse(card.querySelector('time[datetime]')?.getAttribute('datetime') || ''); return { card, sourceIndex, publishedAt: Number.isFinite(publishedAt) ? publishedAt : 0, }; }) .sort((left, right) => right.publishedAt - left.publishedAt || left.sourceIndex - right.sourceIndex); cards.forEach(({ card }, index) => { card.hidden = index >= HOME_ITEM_LIMIT; card.setAttribute('aria-posinset', String(index + 1)); card.setAttribute('aria-setsize', String(Math.min(cards.length, HOME_ITEM_LIMIT))); grid.append(card); }); grid.dataset.observerHomeLimit = String(HOME_ITEM_LIMIT); observer.observe(grid, { childList: true }); } sortAndLimit(); } function initializeList(page) { const items = Array.from(page.querySelectorAll('[data-observer-item]')); const form = page.querySelector('[data-observer-search-form]'); const input = page.querySelector('[data-observer-search-input]'); const resetButton = page.querySelector('[data-observer-reset]'); const tagButtons = Array.from(page.querySelectorAll('[data-observer-tag]')); const pagination = page.querySelector('[data-observer-pagination]'); const status = page.querySelector('[data-observer-status]'); const noResults = page.querySelector('[data-observer-no-results]'); const grids = Array.from(page.querySelectorAll('[data-observer-grid]')); const pageSize = Math.max(1, Number.parseInt(page.dataset.observerPageSize || '12', 10)); const filterEnabled = Boolean(form && input && !form.closest('[hidden]')); if (!items.length || !pagination || !grids.length) return; const state = { query: '', tag: 'all', page: 1 }; function readUrlState() { const params = new URLSearchParams(window.location.search); state.query = filterEnabled ? (params.get('q') || '').trim() : ''; state.tag = filterEnabled ? (params.get('tag') || 'all') : 'all'; state.page = Math.max(1, Number.parseInt(params.get('page') || '1', 10) || 1); if (filterEnabled && !tagButtons.some((button) => button.dataset.observerTag === state.tag)) state.tag = 'all'; if (input) input.value = state.query; } function writeUrlState(replace = false) { const url = new URL(window.location.href); url.searchParams.delete('category'); if (state.query) url.searchParams.set('q', state.query); else url.searchParams.delete('q'); if (state.tag !== 'all') url.searchParams.set('tag', state.tag); else url.searchParams.delete('tag'); if (state.page > 1) url.searchParams.set('page', String(state.page)); else url.searchParams.delete('page'); const method = replace ? 'replaceState' : 'pushState'; window.history[method]({}, '', `${url.pathname}${url.search}#observer-list`); } function matchingItems() { const normalizedQuery = state.query.toLocaleLowerCase(); return items.filter((item) => { const matchesTag = state.tag === 'all' || (item.dataset.observerTags || '').split(/\s+/).includes(state.tag); const matchesSearch = !normalizedQuery || (item.dataset.observerSearch || '').includes(normalizedQuery); return matchesTag && matchesSearch; }); } function button(labelCn, labelEn, targetPage, options = {}) { const control = document.createElement('button'); control.type = 'button'; control.textContent = text(labelCn, labelEn); control.dataset.labelCn = labelCn; control.dataset.labelEn = labelEn; control.dataset.page = String(targetPage); if (options.current) control.setAttribute('aria-current', 'page'); if (options.disabled) control.disabled = true; if (options.ariaCn && options.ariaEn) { control.dataset.ariaCn = options.ariaCn; control.dataset.ariaEn = options.ariaEn; control.setAttribute('aria-label', text(options.ariaCn, options.ariaEn)); } return control; } function paginationRange(current, total) { if (total <= 7) return Array.from({ length: total }, (_, index) => index + 1); const values = new Set([1, total, current - 1, current, current + 1]); return Array.from(values).filter((value) => value >= 1 && value <= total).sort((a, b) => a - b); } function renderPagination(totalPages) { pagination.replaceChildren(); if (totalPages <= 1) { pagination.hidden = true; return; } pagination.hidden = false; pagination.append(button('上一页', 'Previous', Math.max(1, state.page - 1), { disabled: state.page === 1, ariaCn: '上一页', ariaEn: 'Previous page', })); const values = paginationRange(state.page, totalPages); let previous = 0; values.forEach((value) => { if (previous && value - previous > 1) { const gap = document.createElement('span'); gap.textContent = '…'; gap.setAttribute('aria-hidden', 'true'); pagination.append(gap); } pagination.append(button(String(value), String(value), value, { current: value === state.page, ariaCn: `第${value}页`, ariaEn: `Page ${value}`, })); previous = value; }); pagination.append(button('下一页', 'Next', Math.min(totalPages, state.page + 1), { disabled: state.page === totalPages, ariaCn: '下一页', ariaEn: 'Next page', })); } function render({ updateUrl = false, replaceUrl = false, focusList = false } = {}) { const matched = matchingItems(); const totalPages = Math.max(1, Math.ceil(matched.length / pageSize)); state.page = Math.min(state.page, totalPages); const firstIndex = (state.page - 1) * pageSize; const visible = new Set(matched.slice(firstIndex, firstIndex + pageSize)); items.forEach((item) => { item.hidden = !visible.has(item); }); grids.forEach((grid) => { const gridItems = Array.from(grid.querySelectorAll('[data-observer-item]')); grid.hidden = !gridItems.some((item) => visible.has(item)); }); if (noResults) noResults.hidden = matched.length !== 0; tagButtons.forEach((control) => { const active = control.dataset.observerTag === state.tag; control.classList.toggle('is-active', active); control.setAttribute('aria-pressed', String(active)); }); if (status) { const start = matched.length ? firstIndex + 1 : 0; const end = Math.min(firstIndex + pageSize, matched.length); status.textContent = text( `显示 ${start}-${end},共 ${matched.length} 篇`, `Showing ${start}-${end} of ${matched.length}`, ); } renderPagination(totalPages); updateLanguageAttributes(page); if (updateUrl) writeUrlState(replaceUrl); if (focusList && visible.size) { const firstLink = Array.from(visible)[0].querySelector('h2 a, h3 a'); if (firstLink) firstLink.focus({ preventScroll: false }); } } form?.addEventListener('submit', (event) => { event.preventDefault(); state.query = input.value.trim(); state.page = 1; render({ updateUrl: true }); }); resetButton?.addEventListener('click', () => { input.value = ''; state.query = ''; state.tag = 'all'; state.page = 1; render({ updateUrl: true }); input.focus(); }); tagButtons.forEach((control) => { control.addEventListener('click', () => { state.tag = control.dataset.observerTag || 'all'; state.page = 1; render({ updateUrl: true }); }); }); pagination.addEventListener('click', (event) => { const control = event.target.closest('button[data-page]'); if (!control || control.disabled) return; state.page = Number.parseInt(control.dataset.page, 10) || 1; render({ updateUrl: true, focusList: true }); }); window.addEventListener('popstate', () => { readUrlState(); render(); }); document.addEventListener('dbc:languagechange', () => render()); readUrlState(); render({ updateUrl: /(?:category|tag|q)=/.test(window.location.search), replaceUrl: true }); } function initialize() { updateLanguageAttributes(); installCoverFallbacks(); initializeHomeFeed(); const page = document.querySelector('[data-observer-list-page]'); if (page) initializeList(page); document.addEventListener('dbc:languagechange', () => updateLanguageAttributes()); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initialize, { once: true }); else initialize(); })();