/* BIOTEN Observer V7.31.8-R8: hub preview limits, date-sorted pagination, language and PDF routing. */
(() => {
'use strict';
const DEFAULT_PAGE_SIZE = 12;
const INDUSTRY_PAGE_SIZE = 8;
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 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-v7301-en-review]').forEach((notice) => {
notice.hidden = !isEnglish();
});
}
function updateObserverChrome() {
const title = document.querySelector('title[data-cn][data-en]');
if (title) document.title = isEnglish() ? title.dataset.en : title.dataset.cn;
const context = document.querySelector('.v500-context');
if (!context) return;
context.dataset.ariaCn = '博碳观察页面层级';
context.dataset.ariaEn = 'BIOTEN Observer context';
context.setAttribute('aria-label', text(context.dataset.ariaCn, context.dataset.ariaEn));
const first = context.querySelector('ol > li:first-child :is(a, span)');
if (first) {
first.dataset.newsCn = '博碳观察';
first.dataset.newsEn = 'BIOTEN Observer';
first.textContent = text(first.dataset.newsCn, first.dataset.newsEn);
}
const current = context.querySelector('ol > li:last-child :is(a, span)');
if (!current) return;
const view = document.body.dataset.observerView;
const labels = {
hub: ['公司动态 · 行业洞察', 'Company News · Industry Insights'],
company: ['公司动态', 'Company News'],
industry: ['行业洞察', 'Industry Insights'],
article: ['文章阅读', 'Article'],
document: ['PDF 阅读', 'PDF Reader'],
legacy: ['内容频道', 'Content Channels'],
};
const [cn, en] = labels[view] || labels.article;
current.dataset.newsCn = cn;
current.dataset.newsEn = en;
current.textContent = text(cn, en);
}
function protectObserverTitle() {
const title = document.querySelector('title[data-cn][data-en]');
if (!title) return;
const observer = new MutationObserver(() => {
const preferred = isEnglish() ? title.dataset.en : title.dataset.cn;
if (document.title !== preferred) document.title = preferred;
});
observer.observe(title, { childList: true, characterData: true, subtree: true });
}
function normalizeLegacyParameters() {
const url = new URL(window.location.href);
let changed = false;
['q', 'tag', 'category'].forEach((key) => {
if (url.searchParams.has(key)) {
url.searchParams.delete(key);
changed = true;
}
});
if (changed) window.history.replaceState({}, '', `${url.pathname}${url.search}${url.hash}`);
}
function installCoverFallbacks() {
document.querySelectorAll('.observer-publication-card__media img, .observer-card__media img, .observer-home-card__media img, .observer-article-cover img').forEach((image) => {
image.addEventListener('error', () => {
const industry = image.closest('[data-observer-category="industry"], [data-observer-mode="industry"]');
const fallback = industry ? DEFAULT_INDUSTRY_COVER : DEFAULT_COMPANY_COVER;
if (!image.src.endsWith(fallback)) image.src = fallback;
}, { once: true });
});
}
function initializeHubPreviews() {
const hub = document.querySelector('[data-observer-mode="hub"]');
if (!hub) return;
hub.querySelectorAll('[data-observer-preview-limit]').forEach((section) => {
const grid = section.querySelector('.observer-home-publications');
const limit = Math.max(1, Number.parseInt(section.dataset.observerPreviewLimit || '', 10) || 1);
if (!grid) return;
const observer = new MutationObserver(() => sortAndLimit());
function sortAndLimit() {
observer.disconnect();
const cards = Array.from(grid.querySelectorAll(':scope > [data-observer-item]'))
.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 >= limit;
card.setAttribute('aria-posinset', String(index + 1));
card.setAttribute('aria-setsize', String(Math.min(cards.length, limit)));
grid.append(card);
});
observer.observe(grid, { childList: true });
}
sortAndLimit();
});
}
function pageButton(labelCn, labelEn, pageNumber, 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(pageNumber);
if (options.current) control.setAttribute('aria-current', 'page');
if (options.disabled) control.disabled = true;
if (options.ariaCn) {
control.dataset.ariaCn = options.ariaCn;
control.dataset.ariaEn = options.ariaEn;
control.setAttribute('aria-label', text(options.ariaCn, options.ariaEn));
}
return control;
}
function initializeList(page) {
const pagination = page.querySelector('[data-observer-pagination]');
const status = page.querySelector('[data-observer-status]');
const grid = page.querySelector('[data-observer-grid]');
if (!pagination || !grid) return;
const items = Array.from(grid.querySelectorAll(':scope > [data-observer-item]'))
.map((item, sourceIndex) => {
const publishedAt = Date.parse(item.querySelector('time[datetime]')?.getAttribute('datetime') || '');
return {
item,
sourceIndex,
publishedAt: Number.isFinite(publishedAt) ? publishedAt : 0,
};
})
.sort((left, right) => right.publishedAt - left.publishedAt || left.sourceIndex - right.sourceIndex)
.map(({ item }) => item);
if (!items.length || !pagination || !grid) return;
items.forEach((item) => grid.append(item));
const configuredPageSize = Math.max(
1,
Number.parseInt(page.dataset.observerPageSize || String(DEFAULT_PAGE_SIZE), 10) || DEFAULT_PAGE_SIZE,
);
const pageSize = page.dataset.observerMode === 'industry'
? INDUSTRY_PAGE_SIZE
: configuredPageSize;
page.dataset.observerPageSize = String(pageSize);
const state = { page: 1 };
function readUrlState() {
const params = new URLSearchParams(window.location.search);
state.page = Math.max(1, Number.parseInt(params.get('page') || '1', 10) || 1);
}
function writeUrlState() {
const url = new URL(window.location.href);
if (state.page > 1) url.searchParams.set('page', String(state.page));
else url.searchParams.delete('page');
window.history.pushState({}, '', `${url.pathname}${url.search}#observer-list`);
}
function renderPagination(totalPages) {
pagination.replaceChildren();
pagination.hidden = totalPages <= 1;
if (totalPages <= 1) return;
pagination.append(pageButton('上一页', 'Previous', Math.max(1, state.page - 1), {
disabled: state.page === 1,
ariaCn: '上一页',
ariaEn: 'Previous page',
}));
const values = totalPages <= 7
? Array.from({ length: totalPages }, (_, index) => index + 1)
: Array.from(new Set([1, totalPages, state.page - 1, state.page, state.page + 1]))
.filter((value) => value > 0 && value <= totalPages)
.sort((left, right) => left - right);
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(pageButton(String(value), String(value), value, {
current: value === state.page,
ariaCn: `第${value}页`,
ariaEn: `Page ${value}`,
}));
previous = value;
});
pagination.append(pageButton('下一页', 'Next', Math.min(totalPages, state.page + 1), {
disabled: state.page === totalPages,
ariaCn: '下一页',
ariaEn: 'Next page',
}));
}
function render({ updateUrl = false, focusList = false } = {}) {
const totalPages = Math.max(1, Math.ceil(items.length / pageSize));
state.page = Math.min(state.page, totalPages);
const first = (state.page - 1) * pageSize;
const visibleItems = items.slice(first, first + pageSize);
const visible = new Set(visibleItems);
items.forEach((item) => { item.hidden = !visible.has(item); });
if (status) {
const start = items.length ? first + 1 : 0;
const end = Math.min(first + pageSize, items.length);
const statusCn = `第 ${start}-${end} 篇 · 共 ${items.length} 篇`;
const statusEn = `${start}-${end} of ${items.length}`;
status.dataset.newsCn = statusCn;
status.dataset.newsEn = statusEn;
status.textContent = text(statusCn, statusEn);
}
renderPagination(totalPages);
updateLanguageAttributes(page);
if (updateUrl) writeUrlState();
if (focusList) visibleItems[0]?.querySelector('h2 a, h3 a')?.focus({ preventScroll: false });
}
pagination.addEventListener('click', (event) => {
const button = event.target.closest('button[data-page]');
if (!button || button.disabled) return;
state.page = Number.parseInt(button.dataset.page, 10) || 1;
render({ updateUrl: true, focusList: true });
});
window.addEventListener('popstate', () => { readUrlState(); render(); });
document.addEventListener('dbc:languagechange', () => render());
readUrlState();
render();
}
function initializeDocument() {
const main = document.querySelector('[data-observer-view="document"], .observer-document-main');
if (!main) return;
const slug = new URLSearchParams(window.location.search).get('doc');
const catalog = Array.isArray(window.BiotenObserverCatalogV7318) ? window.BiotenObserverCatalogV7318 : [];
const article = catalog.find((entry) => entry.slug === slug);
const error = document.querySelector('[data-document-error]');
if (!article) {
if (error) error.hidden = false;
return;
}
const categoryPage = article.observerCategory === 'industry' ? 'news-insights.html' : 'news-company.html';
const categoryLink = document.querySelector('[data-document-category-link]');
const articleLink = document.querySelector('[data-document-article-link]');
const back = document.querySelector('[data-document-back]');
const open = document.querySelector('[data-document-open]');
const download = document.querySelector('[data-document-download]');
const frame = document.querySelector('[data-document-frame]');
const title = document.querySelector('[data-document-title]');
const category = document.querySelector('[data-document-category]');
if (categoryLink) categoryLink.href = categoryPage;
if (articleLink) articleLink.href = article.file;
if (back) back.href = article.file;
if (open) open.href = article.pdf;
if (download) {
download.href = article.pdf;
download.download = article.pdfDownloadName;
}
if (frame) frame.data = article.pdf;
if (title) {
title.dataset.newsCn = article.title;
title.dataset.newsEn = article.titleEn;
title.textContent = text(article.title, article.titleEn);
}
if (category) {
category.dataset.newsCn = article.observerCategoryCn;
category.dataset.newsEn = article.observerCategoryEn;
category.textContent = text(article.observerCategoryCn, article.observerCategoryEn);
}
if (error) error.hidden = true;
}
function initializeArticleShare(main) {
const meta = main.querySelector('.observer-article-hero__meta');
const pdfAction = meta?.querySelector('.observer-article-hero__pdf-action');
if (!meta || !pdfAction) return;
let button = meta.querySelector('[data-article-share]');
if (!button) {
button = document.createElement('button');
button.className = 'observer-article-hero__share-action';
button.type = 'button';
button.dataset.articleShare = '';
button.dataset.ariaCn = '分享阅读';
button.dataset.ariaEn = 'Share article';
button.setAttribute('aria-label', text('分享阅读', 'Share article'));
button.setAttribute('title', text('分享阅读', 'Share article'));
button.innerHTML = '分享阅读';
pdfAction.insertAdjacentElement('afterend', button);
}
if (button.dataset.shareReady === 'true') return;
button.dataset.shareReady = 'true';
const label = button.querySelector('[data-article-share-label]');
const setLabel = (cn, en) => {
const value = text(cn, en);
if (label) label.textContent = value;
button.setAttribute('aria-label', value);
button.setAttribute('title', value);
};
const restoreLabel = () => setLabel('分享阅读', 'Share article');
const flashLabel = (cn, en) => {
setLabel(cn, en);
button.dataset.shareMessage = text(cn, en);
window.setTimeout(() => {
delete button.dataset.shareMessage;
restoreLabel();
}, 2200);
};
const copyCurrentUrl = async () => {
const url = window.location.href;
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(url);
return true;
}
} catch (_) {
// Continue to the browser-compatible copy fallback.
}
const field = document.createElement('textarea');
field.value = url;
field.setAttribute('readonly', '');
field.style.cssText = 'position:fixed;left:-9999px;top:0;opacity:0;';
document.body.appendChild(field);
field.select();
const copied = document.execCommand('copy');
field.remove();
return copied;
};
button.addEventListener('click', async () => {
const title = main.querySelector('.observer-article-hero h1')?.textContent?.trim() || document.title;
const url = window.location.href;
if (navigator.share) {
try {
await navigator.share({ title, url });
return;
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') return;
}
}
const copied = await copyCurrentUrl();
flashLabel(copied ? '链接已复制' : '请复制当前链接', copied ? 'Link copied' : 'Copy this page URL');
});
document.addEventListener('dbc:languagechange', restoreLabel);
}
function initializeArticle() {
const main = document.querySelector('[data-observer-article]');
if (!main) return;
initializeArticleShare(main);
const catalog = Array.isArray(window.BiotenObserverCatalogV7318) ? window.BiotenObserverCatalogV7318 : [];
const article = catalog.find((entry) => entry.slug === main.dataset.observerArticle);
if (!article) return;
const publisher = main.querySelector('.observer-article-hero__meta > span:nth-child(2)');
if (publisher) {
const label = publisher.querySelector('b');
Array.from(publisher.childNodes).forEach((node) => { if (node !== label) node.remove(); });
const value = document.createElement('span');
value.dataset.observerAuthor = '';
value.dataset.newsCn = article.authorCn;
value.dataset.newsEn = article.authorEn;
value.textContent = text(article.authorCn, article.authorEn);
publisher.append(value);
}
const authorLabel = Array.from(main.querySelectorAll('.observer-article-data th')).find((cell) => cell.dataset.newsCn === '发布主体');
const authorCell = authorLabel?.closest('tr')?.querySelector('td');
if (authorCell) {
authorCell.dataset.newsCn = article.authorCn;
authorCell.dataset.newsEn = article.authorEn;
authorCell.textContent = text(article.authorCn, article.authorEn);
}
}
function initialize() {
normalizeLegacyParameters();
updateLanguageAttributes();
updateObserverChrome();
protectObserverTitle();
installCoverFallbacks();
initializeHubPreviews();
const list = document.querySelector('[data-observer-list-page]');
if (list) initializeList(list);
initializeDocument();
initializeArticle();
document.addEventListener('dbc:languagechange', () => {
updateLanguageAttributes();
updateObserverChrome();
initializeDocument();
initializeArticle();
});
window.addEventListener('load', updateObserverChrome, { once: true });
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initialize, { once: true });
else initialize();
})();