/* globals $, Vue */
/**
* The global SMAPI module.
*/
var smapi = smapi || {};
/**
* The Vue app for the current page.
* @type {Vue}
*/
var app;
// Use a scroll event to apply a sticky effect to the filters / pagination
// bar. We can't just use "position: sticky" due to how the page is structured
// but this works well enough.
$(function () {
let sticking = false;
document.addEventListener("scroll", function () {
const filters = document.getElementById("filters");
const holder = document.getElementById("filterHolder");
if (!filters || !holder)
return;
const offset = holder.offsetTop;
const shouldStick = window.pageYOffset > offset;
if (shouldStick === sticking)
return;
sticking = shouldStick;
if (sticking) {
holder.style.marginBottom = `calc(1em + ${filters.offsetHeight}px)`;
filters.classList.add("sticky");
}
else {
filters.classList.remove("sticky");
holder.style.marginBottom = "";
}
});
});
/**
* Initialize a log parser view on the current page.
* @param {object} state The state options to use.
* @returns {void}
*/
smapi.logParser = function (state) {
if (!state)
state = {};
// internal helpers
const helpers = {
/**
* Get a handler which invokes the callback after a set delay, resetting the delay each time it's called.
* @param {(...*) => void} action The callback to invoke when the delay ends.
* @param {number} delay The number of milliseconds to delay the action after each call.
* @returns {() => void}
*/
getDebouncedHandler(action, delay) {
let timeoutId = null;
return function () {
clearTimeout(timeoutId);
const args = arguments;
const self = this;
timeoutId = setTimeout(
function () {
action.apply(self, args);
},
delay
);
}
},
/**
* Escape regex special characters in the given string.
* @param {string} text
* @returns {string}
*/
escapeRegex(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
},
/**
* Format a number for the user's locale.
* @param {number} value The number to format.
* @returns {string}
*/
formatNumber(value) {
const formatter = window.Intl && Intl.NumberFormat && new Intl.NumberFormat();
return formatter && formatter.format
? formatter.format(value)
: `${value}`;
},
/**
* Try parsing the value as a base-10 integer.
* @param {string} value The value to parse.
* @param {number} defaultValue The value to return if parsing fails.
* @param {() => boolean} criteria An optional callback to check whether a parsed number is valid.
* @returns {number} The parsed number if it's valid, else the default value.
*/
tryParseNumber(value, defaultValue, criteria = null) {
value = parseInt(value, 10);
return !isNaN(value) && isFinite(value) && (!criteria || criteria(value))
? value
: defaultValue;
},
/**
* Get whether two objects are equivalent based on their top-level properties.
* @param {Object} left The first value to compare.
* @param {Object} right The second value to compare.
* @returns {Boolean}
*/
shallowEquals(left, right) {
if (typeof left !== "object" || typeof right !== "object")
return left === right;
if (left == null || right == null)
return left == null && right == null;
if (Array.isArray(left) !== Array.isArray(right))
return false;
const leftKeys = Object.keys(left);
const rightKeys = Object.keys(right);
if (leftKeys.length != rightKeys.length)
return false;
for (const key of leftKeys) {
if (!rightKeys.includes(key) || left[key] !== right[key])
return false;
}
return true;
}
};
// internal event handlers
const handlers = {
/**
* Method called when the user clicks a log line to toggle the visibility of a section. Binding methods is problematic with functional components so we just use the `data-section` parameter and our global reference to the app.
* @param {any} event
* @returns {false}
*/
clickLogLine(event) {
app.toggleSection(event.currentTarget.dataset.section);
event.preventDefault();
return false;