Files
Bubberstation/tgui/packages/tgui-setup/helpers.js
Jeremiah 9f7eae046a Minifies tgui helpers (#90340)
## About The Pull Request
Instead of just writing out the helper functions and styles directly
inside tgui.html, this minifies them and injects them into the document
on tgui init. The result should be a smaller bundle and more manageable
code, with clear separation of page, style, and javascript
## Why It's Good For The Game
Minification is generally a small perf gain but I want to be clear that
I'm doing this 100% for organization & separation of concerns
## Changelog
N/A
2025-04-06 09:23:44 +12:00

529 lines
14 KiB
JavaScript

/* eslint-disable */
(function () {
// Utility functions
var hasOwn = Object.prototype.hasOwnProperty;
var assign = function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (hasOwn.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
var parseMetaTag = function (name) {
var content = document.getElementById(name).getAttribute('content');
if (content === '[' + name + ']') {
return null;
}
return content;
};
// BYOND API object
// ------------------------------------------------------
var Byond = (window.Byond = {});
// Expose inlined metadata
Byond.windowId = parseMetaTag('tgui:windowId');
// Backwards compatibility
window.__windowId__ = Byond.windowId;
// Trident engine version
Byond.TRIDENT = (function () {
var groups = navigator.userAgent.match(/Trident\/(\d+).+?;/i);
var majorVersion = groups && groups[1];
return majorVersion ? parseInt(majorVersion, 10) : null;
})();
// Blink engine version
Byond.BLINK = (function () {
var groups = navigator.userAgent.match(/Chrome\/(\d+)\./);
var majorVersion = groups && groups[1];
return majorVersion ? parseInt(majorVersion, 10) : null;
})();
// Basic checks to detect whether this page runs in BYOND
var isByond =
(Byond.TRIDENT !== null || Byond.BLINK !== null || window.cef_to_byond) &&
location.hostname === '127.0.0.1' &&
location.search !== '?external';
//As of BYOND 515 the path doesn't seem to include tmp dir anymore if you're trying to open tgui in external browser and looking why it doesn't work
//&& location.pathname.indexOf('/tmp') === 0
// Version constants
Byond.IS_BYOND = isByond;
// Strict mode flag
Byond.strictMode = Boolean(Number(parseMetaTag('tgui:strictMode')));
// Callbacks for asynchronous calls
Byond.__callbacks__ = [];
// Reviver for BYOND JSON
var byondJsonReviver = function (key, value) {
if (typeof value === 'object' && value !== null && value.__number__) {
return parseFloat(value.__number__);
}
return value;
};
// Makes a BYOND call.
// See: https://secure.byond.com/docs/ref/skinparams.html
Byond.call = function (path, params) {
// Not running in BYOND, abort.
if (!isByond) {
return;
}
// Build the URL
var url = (path || '') + '?';
var i = 0;
if (params) {
for (var key in params) {
if (hasOwn.call(params, key)) {
if (i++ > 0) {
url += '&';
}
var value = params[key];
if (value === null || value === undefined) {
value = '';
}
url += encodeURIComponent(key) + '=' + encodeURIComponent(value);
}
}
}
// If we're a Chromium client, just use the fancy method
if (window.cef_to_byond) {
cef_to_byond('byond://' + url);
return;
}
// Perform a standard call via location.href
if (url.length < 2048) {
location.href = 'byond://' + url;
return;
}
// Send an HTTP request to DreamSeeker's HTTP server.
// Allows sending much bigger payloads.
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.send();
};
Byond.callAsync = function (path, params) {
if (!window.Promise) {
throw new Error('Async calls require API level of ES2015 or later.');
}
var index = Byond.__callbacks__.length;
var promise = new window.Promise(function (resolve) {
Byond.__callbacks__.push(resolve);
});
Byond.call(
path,
assign({}, params, {
callback: 'Byond.__callbacks__[' + index + ']',
})
);
return promise;
};
Byond.topic = function (params) {
return Byond.call('', params);
};
Byond.command = function (command) {
return Byond.call('winset', {
command: command,
});
};
Byond.winget = function (id, propName) {
if (id === null) {
id = '';
}
var isArray = propName instanceof Array;
var isSpecific = propName && propName !== '*' && !isArray;
var promise = Byond.callAsync('winget', {
id: id,
property: (isArray && propName.join(',')) || propName || '*',
});
if (isSpecific) {
promise = promise.then(function (props) {
return props[propName];
});
}
return promise;
};
Byond.winset = function (id, propName, propValue) {
if (id === null) {
id = '';
} else if (typeof id === 'object') {
return Byond.call('winset', id);
}
var props = {};
if (typeof propName === 'string') {
props[propName] = propValue;
} else {
assign(props, propName);
}
props.id = id;
return Byond.call('winset', props);
};
Byond.parseJson = function (json) {
try {
return JSON.parse(json, byondJsonReviver);
} catch (err) {
throw new Error('JSON parsing error: ' + (err && err.message));
}
};
Byond.sendMessage = function (type, payload) {
var message =
typeof type === 'string' ? { type: type, payload: payload } : type;
// JSON-encode the payload
if (message.payload !== null && message.payload !== undefined) {
message.payload = JSON.stringify(message.payload);
}
// Append an identifying header
assign(message, {
tgui: 1,
window_id: Byond.windowId,
});
Byond.topic(message);
};
// This function exists purely for debugging, do not use it in code!
Byond.injectMessage = function (type, payload) {
window.update(JSON.stringify({ type: type, payload: payload }));
};
Byond.subscribe = function (listener) {
window.update.flushQueue(listener);
window.update.listeners.push(listener);
};
Byond.subscribeTo = function (type, listener) {
var _listener = function (_type, payload) {
if (_type === type) {
listener(payload);
}
};
window.update.flushQueue(_listener);
window.update.listeners.push(_listener);
};
// Asset loaders
// ------------------------------------------------------
var RETRY_ATTEMPTS = 5;
var RETRY_WAIT_INITIAL = 500;
var RETRY_WAIT_INCREMENT = 500;
var loadedAssetByUrl = {};
var isStyleSheetLoaded = function (node, url) {
var styleSheet = node.sheet;
if (styleSheet) {
return styleSheet.rules.length > 0;
}
return false;
};
var injectNode = function (node) {
if (!document.body) {
setTimeout(function () {
injectNode(node);
});
return;
}
var refs = document.body.childNodes;
var ref = refs[refs.length - 1];
ref.parentNode.insertBefore(node, ref.nextSibling);
};
var loadAsset = function (options) {
var url = options.url;
var type = options.type;
var sync = options.sync;
var attempt = options.attempt || 0;
if (loadedAssetByUrl[url]) {
return;
}
loadedAssetByUrl[url] = options;
// Generic retry function
var retry = function () {
if (attempt >= RETRY_ATTEMPTS) {
var errorMessage =
'Error: Failed to load the asset ' +
"'" +
url +
"' after several attempts.";
if (type === 'css') {
errorMessage +=
+'\nStylesheet was either not found, ' +
"or you're trying to load an empty stylesheet " +
'that has no CSS rules in it.';
}
throw new Error(errorMessage);
}
setTimeout(
function () {
loadedAssetByUrl[url] = null;
options.attempt += 1;
loadAsset(options);
},
RETRY_WAIT_INITIAL + attempt * RETRY_WAIT_INCREMENT
);
};
// JS specific code
if (type === 'js') {
var node = document.createElement('script');
node.type = 'text/javascript';
node.crossOrigin = 'anonymous';
node.src = url;
if (sync) {
node.defer = true;
} else {
node.async = true;
}
node.onerror = function () {
node.onerror = null;
node.parentNode.removeChild(node);
node = null;
retry();
};
injectNode(node);
return;
}
// CSS specific code
if (type === 'css') {
var node = document.createElement('link');
node.type = 'text/css';
node.rel = 'stylesheet';
node.crossOrigin = 'anonymous';
node.href = url;
// Temporarily set media to something inapplicable
// to ensure it'll fetch without blocking render
if (!sync) {
node.media = 'only x';
}
var removeNodeAndRetry = function () {
node.parentNode.removeChild(node);
node = null;
retry();
};
// 516: Chromium won't call onload() if there is a 404 error
// Legacy IE doesn't use onerror, so we retain that
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link#stylesheet_load_events
node.onerror = function () {
node.onerror = null;
removeNodeAndRetry();
};
node.onload = function () {
node.onload = null;
if (isStyleSheetLoaded(node, url)) {
// Render the stylesheet
node.media = 'all';
return;
}
removeNodeAndRetry();
};
injectNode(node);
return;
}
};
Byond.loadJs = function (url, sync) {
loadAsset({ url: url, sync: sync, type: 'js' });
};
Byond.loadCss = function (url, sync) {
loadAsset({ url: url, sync: sync, type: 'css' });
};
Byond.saveBlob = function (blob, filename, ext) {
if (window.navigator.msSaveBlob) {
window.navigator.msSaveBlob(blob, filename);
} else if (window.showSaveFilePicker) {
var accept = {};
accept[blob.type] = [ext];
var opts = {
suggestedName: filename,
types: [
{
description: 'SS13 file',
accept: accept,
},
],
};
window
.showSaveFilePicker(opts)
.then(function (file) {
return file.createWritable();
})
.then(function (file) {
return file.write(blob).then(function () {
return file.close();
});
})
.catch(function () {});
}
};
// Icon cache
Byond.iconRefMap = {};
})();
// Error handling
// ------------------------------------------------------
window.onerror = function (msg, url, line, col, error) {
window.onerror.errorCount = (window.onerror.errorCount || 0) + 1;
// Proper stacktrace
var stack = error && error.stack;
// Ghetto stacktrace
if (!stack) {
stack = msg + '\n at ' + url + ':' + line;
if (col) {
stack += ':' + col;
}
}
// Augment the stack
stack = window.__augmentStack__(stack, error);
// Print error to the page
if (Byond.strictMode) {
var errorRoot = document.getElementById('FatalError');
var errorStack = document.getElementById('FatalError__stack');
if (errorRoot) {
errorRoot.className = 'FatalError FatalError--visible';
if (window.onerror.__stack__) {
window.onerror.__stack__ += '\n\n' + stack;
} else {
window.onerror.__stack__ = stack;
}
var textProp = 'textContent';
errorStack[textProp] = window.onerror.__stack__;
}
// Set window geometry
var setFatalErrorGeometry = function () {
Byond.winset(Byond.windowId, {
titlebar: true,
'is-visible': true,
'can-resize': true,
});
};
setFatalErrorGeometry();
setInterval(setFatalErrorGeometry, 1000);
}
// Send logs to the game server
if (Byond.strictMode) {
Byond.sendMessage({
type: 'log',
fatal: 1,
message: stack,
});
} else if (window.onerror.errorCount <= 1) {
stack += '\nWindow is in non-strict mode, future errors are suppressed.';
Byond.sendMessage({
type: 'log',
message: stack,
});
}
// Short-circuit further updates
if (Byond.strictMode) {
window.update = function () {};
window.update.queue = [];
}
// Prevent default action
return true;
};
// Catch unhandled promise rejections
window.onunhandledrejection = function (e) {
var msg = 'UnhandledRejection';
if (e.reason) {
msg += ': ' + (e.reason.message || e.reason.description || e.reason);
if (e.reason.stack) {
e.reason.stack = 'UnhandledRejection: ' + e.reason.stack;
}
}
window.onerror(msg, null, null, null, e.reason);
};
// Helper for augmenting stack traces on fatal errors
window.__augmentStack__ = function (stack, error) {
return stack + '\nUser Agent: ' + navigator.userAgent;
};
// Incoming message handling
// ------------------------------------------------------
// Message handler
window.update = function (rawMessage) {
// Push onto the queue (active during initialization)
if (window.update.queueActive) {
window.update.queue.push(rawMessage);
return;
}
// Parse the message
var message = Byond.parseJson(rawMessage);
// Notify listeners
var listeners = window.update.listeners;
for (var i = 0; i < listeners.length; i++) {
listeners[i](message.type, message.payload);
}
};
// Properties and variables of this specific handler
window.update.listeners = [];
window.update.queue = [];
window.update.queueActive = true;
window.update.flushQueue = function (listener) {
// Disable and clear the queue permanently on short delay
if (window.update.queueActive) {
window.update.queueActive = false;
if (window.setTimeout) {
window.setTimeout(function () {
window.update.queue = [];
}, 0);
}
}
// Process queued messages on provided listener
var queue = window.update.queue;
for (var i = 0; i < queue.length; i++) {
var message = Byond.parseJson(queue[i]);
listener(message.type, message.payload);
}
};
window.replaceHtml = function (inline_html) {
var children = document.body.childNodes;
for (var i = 0; i < children.length; i++) {
if (children[i].nodeValue == ' tgui:inline-html-start ') {
while (children[i].nodeValue != ' tgui:inline-html-end ') {
children[i].remove();
}
children[i].remove();
}
}
document.body.insertAdjacentHTML(
'afterbegin',
'<!-- tgui:inline-html-start -->' +
inline_html +
'<!-- tgui:inline-html-end -->'
);
};