'use strict'; /* Hey there! This is the client file for your theme. If you need to do any client-side work in javascript, this is where it needs to go. You can listen for page changes by writing something like this: $(window).on('action:ajaxify.end', function(ev, data) { var url = data.url; console.log('I am now at: ' + url); }); */ (function () { const WG_TABLE_SCROLL_CLASS = 'wg-mobile-table-scroll'; const WIKI_TABLE_SELECTOR = [ '.westgate-wiki .wiki-page-content.wiki-article-prose > .card-body table', 'li[component="post"] .content table', ].join(', '); const WIKI_TABLE_MIN_COLUMN_WIDTH = 40; const WIKI_TABLE_DESKTOP_MIN_WIDTH = 720; const SKIP_TABLE_SELECTOR = [ `.${WG_TABLE_SCROLL_CLASS}`, '.tableWrapper', '.table-responsive', '.table-responsive-sm', '.table-responsive-md', 'figure.table', '.wiki-infobox', '.wiki-alignment-table', ].join(', '); function toArray(list) { return Array.prototype.slice.call(list || []); } function parsePixelLength(value) { if (!value) { return 0; } const match = String(value).trim().match(/^([0-9]+(?:\.[0-9]+)?)px$/i); return match ? parseFloat(match[1]) : 0; } function getInlinePixelWidth(element) { const styledWidth = element && element.style ? parsePixelLength(element.style.width) : 0; if (styledWidth) { return styledWidth; } if (!element || typeof element.getAttribute !== 'function') { return 0; } const styleAttribute = element.getAttribute('style') || ''; const widthMatch = styleAttribute.match(/(?:^|;)\s*width\s*:\s*([0-9]+(?:\.[0-9]+)?)px(?:\s*;|$)/i); return widthMatch ? parseFloat(widthMatch[1]) : 0; } function tableUsesFluidWidth(table) { const styledWidth = table && table.style ? String(table.style.width || '') : ''; if (/%$/.test(styledWidth.trim())) { return true; } if (!table || typeof table.getAttribute !== 'function') { return false; } const styleAttribute = table.getAttribute('style') || ''; return /(?:^|;)\s*width\s*:\s*[0-9]+(?:\.[0-9]+)?%(?:\s*;|$)/i.test(styleAttribute); } function getTableColumnCount(table) { let columnCount = 0; if (table && typeof table.querySelectorAll === 'function') { columnCount = Math.max(columnCount, table.querySelectorAll('colgroup col').length); toArray(table.querySelectorAll('tr')).forEach((row) => { const rowColumnCount = toArray(row.children).reduce((count, cell) => { const span = parseInt(cell.getAttribute && cell.getAttribute('colspan'), 10) || cell.colSpan || 1; return count + Math.max(1, span); }, 0); columnCount = Math.max(columnCount, rowColumnCount); }); } return columnCount; } function getTableColumnWidthFloor(table, columnCount) { if (!table || typeof table.querySelectorAll !== 'function' || !columnCount) { return 0; } const explicitColumns = toArray(table.querySelectorAll('colgroup col')) .map(getInlinePixelWidth) .filter(width => width > 0); const explicitWidth = explicitColumns.reduce((total, width) => total + width, 0); const inferredColumns = Math.max(0, columnCount - explicitColumns.length); return explicitWidth + (inferredColumns * WIKI_TABLE_MIN_COLUMN_WIDTH); } function getRenderedWidth(element) { if (element && typeof element.getBoundingClientRect === 'function') { return element.getBoundingClientRect().width || 0; } return element && element.offsetWidth ? element.offsetWidth : 0; } function measureMobileTableMinWidth(table) { const columnCount = getTableColumnCount(table); const measuredWidth = getRenderedWidth(table); const columnWidthFloor = getTableColumnWidthFloor(table, columnCount); const desktopWidthFloor = tableUsesFluidWidth(table) && columnCount >= 8 ? WIKI_TABLE_DESKTOP_MIN_WIDTH : 0; return Math.ceil(Math.max(measuredWidth, columnWidthFloor, desktopWidthFloor)); } function applyMobileTableMinWidth(wrapper, minWidth) { if (minWidth > 0 && wrapper && wrapper.style && typeof wrapper.style.setProperty === 'function') { wrapper.style.setProperty('--wg-mobile-table-min-width', `${minWidth}px`); } } function wrapWestgateWikiTables(root) { const scope = root && typeof root.querySelectorAll === 'function' ? root : document; if (!scope || typeof scope.querySelectorAll !== 'function') { return; } const wrapped = []; toArray(scope.querySelectorAll(WIKI_TABLE_SELECTOR)).forEach((table) => { if (!table || !table.parentNode || table.closest(SKIP_TABLE_SELECTOR)) { return; } const wrapper = document.createElement('div'); wrapper.className = WG_TABLE_SCROLL_CLASS; wrapper.setAttribute('tabindex', '0'); wrapper.setAttribute('aria-label', 'Scrollable table'); table.parentNode.insertBefore(wrapper, table); wrapper.appendChild(table); wrapped.push({ table, wrapper }); }); // batch: measure every table first, then write, so reads and writes // don't interleave and force repeated reflows wrapped .map(({ table, wrapper }) => ({ wrapper, minWidth: measureMobileTableMinWidth(table) })) .forEach(({ wrapper, minWidth }) => { applyMobileTableMinWidth(wrapper, minWidth); }); } function renderUnreadMenu(menuEl, topics) { if (!menuEl) { return; } const relativePath = (window.config && window.config.relative_path) || ''; // remove the loading row and any previously injected topic rows; // the empty-state and footer/divider rows are server-rendered and stay put const loading = menuEl.querySelector('[data-wg-unread-loading]'); if (loading) { loading.remove(); } toArray(menuEl.querySelectorAll('.wg-unread-topic')).forEach((li) => { li.remove(); }); const emptyEl = menuEl.querySelector('[data-wg-unread-empty]'); const footerEl = menuEl.querySelector('[data-wg-unread-footer]'); const list = (topics || []).slice(0, 10); if (emptyEl) { emptyEl.classList.toggle('hidden', list.length > 0); } list.forEach((topic) => { const link = document.createElement('a'); link.className = 'dropdown-item wg-unread-item text-truncate'; link.href = `${relativePath}/topic/${topic.slug}`; link.textContent = topic.title; if (topic.category && topic.category.name) { const category = document.createElement('small'); category.className = 'wg-unread-item__category'; category.textContent = topic.category.name; link.appendChild(category); } const li = document.createElement('li'); li.className = 'wg-unread-topic'; li.appendChild(link); if (footerEl) { menuEl.insertBefore(li, footerEl); } else { menuEl.appendChild(li); } }); } /* * Support inbox drawer. The support plugin owns its own notification list * (core's unread count is one aggregate with no hook to carve a type out * of it), but everything the user sees is core's: the rows are rendered * with core's own `partials/notifications_list` partial and driven by the * same handlers as the bell — filter buttons, per-entry read dot, mark all * read. Only the API paths differ. */ function setSupportBadge(count) { toArray(document.querySelectorAll('[component="support/count"]')).forEach((badge) => { badge.textContent = count > 99 ? '99+' : String(count); badge.classList.toggle('hidden', count <= 0); }); } function adjustSupportBadge(delta) { const badge = document.querySelector('[component="support/count"]'); const current = parseInt(badge && badge.textContent, 10) || 0; setSupportBadge(Math.max(0, current + delta)); } // Same shape as core's markNotification: PUT marks read, DELETE marks unread. function markSupportEntry(entryEl, read) { const nid = encodeURIComponent(entryEl.attr('data-nid')); const path = '/plugins/support/notifications/' + nid + '/read'; require(['api'], function (api) { (read ? api.put(path) : api.del(path)).then(function () { entryEl.toggleClass('unread', !read); entryEl.find('.mark-read .unread').toggleClass('hidden', read); entryEl.find('.mark-read .read').toggleClass('hidden', !read); adjustSupportBadge(read ? -1 : 1); }); }); } function renderSupportMenu(triggerEl) { const dropdownEl = triggerEl.parent().find('.dropdown-menu'); const listEl = dropdownEl.find('[component="support/list"]'); dropdownEl.find('[data-filter="all"]').addClass('active'); dropdownEl.find('[data-filter="unread"]').removeClass('active'); require(['api', 'translator'], function (api, translator) { api.get('/plugins/support/notifications', {}).then(function (data) { const notifications = (data && data.notifications) || []; const relativePath = (window.config && window.config.relative_path) || ''; notifications.forEach((notification) => { notification.path = relativePath + notification.path; notification.timeagoLong = $.timeago(new Date(notification.datetime)); }); app.parseAndTranslate('partials/notifications_list', { notifications }, function (html) { listEl.html(html); setSupportBadge((data && data.unreadCount) || 0); // core's partial says "no notifications"; this box only ever holds // support activity, so say that instead. translator.translate('[[westgate:support-inbox.empty]]', function (text) { listEl.find('.no-notifs .fw-semibold').text(text); }); listEl.off('click').on('click', '[component="notifications/item/link"]', function () { const entryEl = $(this).parents('[data-nid]'); if (entryEl.hasClass('unread')) { markSupportEntry(entryEl, true); } triggerEl.dropdown('toggle'); }); listEl.on('click', '.mark-read', function () { const entryEl = $(this).parents('[data-nid]'); markSupportEntry(entryEl, entryEl.hasClass('unread')); }); }); }); }); } function getWestgateTopbar() { if (!document || typeof document.querySelector !== 'function') { return null; } return document.querySelector('[data-wg-topbar]'); } function closeWestgateTopbarDrawer(topbar) { if (!topbar) { return; } topbar.classList.remove('is-drawer-open'); const burger = topbar.querySelector('[data-wg-burger]'); if (burger) { burger.setAttribute('aria-expanded', 'false'); } } function initWestgateTopbar() { const topbar = getWestgateTopbar(); if (!topbar) { return; } if (!window.westgateTheme.topbarEventsBound) { document.addEventListener('click', (event) => { const currentTopbar = getWestgateTopbar(); if (!currentTopbar) { return; } const burger = event.target.closest && event.target.closest('[data-wg-burger]'); if (burger && currentTopbar.contains(burger)) { event.preventDefault(); event.stopPropagation(); const isOpen = !currentTopbar.classList.contains('is-drawer-open'); currentTopbar.classList.toggle('is-drawer-open', isOpen); burger.setAttribute('aria-expanded', String(isOpen)); return; } if (!currentTopbar.contains(event.target)) { closeWestgateTopbarDrawer(currentTopbar); } }); document.addEventListener('keydown', (event) => { if (event.key !== 'Escape') { return; } const currentTopbar = getWestgateTopbar(); closeWestgateTopbarDrawer(currentTopbar); }); window.westgateTheme.topbarEventsBound = true; } $(document) .off('shown.bs.dropdown.westgateTopbar') .on('shown.bs.dropdown.westgateTopbar', '.wg-topbar [component="sidebar/search"]', function () { $(this).find('[component="search/fields"] input[name="query"]').trigger('focus'); }); $(document) .off('show.bs.dropdown.westgateUnread') .on('show.bs.dropdown.westgateUnread', '.wg-topbar [component="unread"]', function () { const menuEl = this.querySelector('[data-wg-unread-menu]'); const relativePath = (window.config && window.config.relative_path) || ''; fetch(relativePath + '/api/unread', { headers: { accept: 'application/json' }, }).then(function (res) { if (!res.ok) { throw new Error('unread fetch failed: ' + res.status); } return res.json(); }).then(function (data) { renderUnreadMenu(menuEl, data && data.topics); }).catch(function () { renderUnreadMenu(menuEl, []); }); }); // ponytail: unlike core's bell, this badge gets no socket push, so it // only catches up when the drawer is opened or the page reloads. Add a // socket event in the support plugin if that gap ever matters. $(document) .off('show.bs.dropdown.westgateSupport') .on('show.bs.dropdown.westgateSupport', '.wg-topbar [component="support"] [data-bs-toggle="dropdown"]', function () { renderSupportMenu($(this)); }); $(document) .off('click.westgateSupport') .on('click.westgateSupport', '.wg-topbar [component="support"] .mark-all-read', function (event) { event.preventDefault(); const entries = $(this).closest('.dropdown-menu').find('[data-nid]'); require(['api'], function (api) { api.post('/plugins/support/notifications/read', {}).then(function () { setSupportBadge(0); entries.removeClass('unread'); entries.find('.mark-read .unread').addClass('hidden'); entries.find('.mark-read .read').removeClass('hidden'); }); }); }) // Core's own filter behaviour: hide read rows client-side, nothing refetched. .on('click.westgateSupport', '.wg-topbar [component="support"] [data-filter]', function () { const dropdownEl = $(this).closest('.dropdown-menu'); dropdownEl.find('[data-filter]').removeClass('active'); this.classList.add('active'); const unreadOnly = this.getAttribute('data-filter') === 'unread'; dropdownEl.find('[data-nid]').each(function () { this.classList.toggle('hidden', unreadOnly && !this.classList.contains('unread')); }); dropdownEl.find('.no-notifs').toggleClass('hidden', dropdownEl.find('[data-nid]:not(.hidden)').length !== 0); }); } window.westgateTheme = window.westgateTheme || {}; window.westgateTheme.wrapWikiTables = wrapWestgateWikiTables; window.westgateTheme.initTopbar = initWestgateTopbar; window.westgateTheme.renderUnreadMenu = renderUnreadMenu; window.westgateTheme.renderSupportMenu = renderSupportMenu; $(document).ready(function () { wrapWestgateWikiTables(document); initWestgateTopbar(); // topic-select checkboxes are elements; make them keyboard-operable $(document).on('keydown', 'i[component="topic/select"]', function (ev) { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); $(this).trigger('click'); } }); $(document).on('click', 'i[component="topic/select"]', function () { // core toggles the icon class in its own click handler; sync after it runs setTimeout(() => { this.setAttribute('aria-checked', this.classList.contains('fa-check-square') ? 'true' : 'false'); }, 0); }); $(window).on('action:ajaxify.end', function () { wrapWestgateWikiTables(document); initWestgateTopbar(); closeWestgateTopbarDrawer(getWestgateTopbar()); }); require(['api'], function (api) { const originalGet = api.get.bind(api); let categoryClassMapPromise; function getCategoryClassMap() { if (!categoryClassMapPromise) { categoryClassMapPromise = new Promise((resolve) => { originalGet('/categories', {}, function (err, data) { if (err || !data || !Array.isArray(data.categories)) { return resolve({}); } const classMap = data.categories.reduce((memo, category) => { if (category && category.cid !== undefined) { memo[String(category.cid)] = category.class || ''; } return memo; }, {}); resolve(classMap); }); }); } return categoryClassMapPromise; } api.get = function (url, data, callback) { if (url !== '/search/categories' || typeof callback !== 'function') { return originalGet(url, data, callback); } return originalGet(url, data, function (err, payload) { if (err || !payload || !Array.isArray(payload.categories)) { return callback(err, payload); } getCategoryClassMap().then((classMap) => { payload.categories = payload.categories.map(category => ({ ...category, class: category.class || classMap[String(category.cid)] || '', })); callback(null, payload); }).catch(() => { // class map is cosmetic; never block search results on it callback(null, payload); }); }); }; }); }); }());