diff --git a/core/code/artifact.js b/core/code/artifact.js
index 12fa98bce..576d0d063 100644
--- a/core/code/artifact.js
+++ b/core/code/artifact.js
@@ -1,3 +1,5 @@
+/* global IITC -- eslint */
+
/**
* @file Provides functions related to Ingress artifacts, including setup, data request, and processing functions.
* Added as part of the ingress #13magnus in november 2013, artifacts
@@ -35,14 +37,12 @@ window.artifact.setup = function() {
artifact._layer = new L.LayerGroup();
window.layerChooser.addOverlay(artifact._layer, 'Artifacts');
- $('')
- .html('Artifacts')
- .attr({
- id: 'artifacts-toolbox-link',
- title: 'Show artifact portal list'
- })
- .click(window.artifact.showArtifactList)
- .appendTo('#toolbox');
+ IITC.toolbox.addButton({
+ id: 'artifacts-toolbox-link',
+ label: 'Artifacts',
+ title: 'Show artifact portal list',
+ action: window.artifact.showArtifactList,
+ });
}
/**
diff --git a/core/code/ornaments.js b/core/code/ornaments.js
index 2ce29e64b..ebab0925b 100644
--- a/core/code/ornaments.js
+++ b/core/code/ornaments.js
@@ -1,4 +1,4 @@
-/* global L, dialog, log */
+/* global L, dialog, log, IITC */
/**
* @namespace window.ornaments
@@ -97,13 +97,13 @@ window.ornaments = {
window.layerChooser.addOverlay(this.layers['Ornaments'], 'Ornaments');
window.layerChooser.addOverlay(this.layers['Excluded ornaments'], 'Excluded ornaments', {default: false});
- $('', {
- text:'Ornaments Opt',
+ IITC.toolbox.addButton({
id: 'ornaments-toolbox-link',
+ label: 'Ornaments Opt',
title: 'Edit ornament exclusions',
accesskey: 'o',
- click: window.ornaments.ornamentsOpt})
- .appendTo('#toolbox');
+ action: window.ornaments.ornamentsOpt,
+ });
},
/**
diff --git a/core/code/region_scoreboard.js b/core/code/region_scoreboard.js
index 28241422b..81768b97b 100644
--- a/core/code/region_scoreboard.js
+++ b/core/code/region_scoreboard.js
@@ -1,3 +1,5 @@
+/* global IITC -- eslint */
+
/**
* @file This file contains the code for displaying and handling the regional scoreboard.
* @module region_scoreboard
@@ -478,14 +480,12 @@ window.RegionScoreboardSetup = (function() {
}
});
} else {
- $('')
- .html('Region scores')
- .attr({
- id: 'scoreboard',
- title: 'View regional scoreboard'
- })
- .click(showDialog)
- .appendTo('#toolbox');
+ IITC.toolbox.addButton({
+ id: 'scoreboard',
+ label: 'Region scores',
+ title: 'View regional scoreboard',
+ action: showDialog,
+ });
}
}
}());
diff --git a/core/code/sidebar.js b/core/code/sidebar.js
index afa96639c..2192ec10b 100644
--- a/core/code/sidebar.js
+++ b/core/code/sidebar.js
@@ -1,3 +1,5 @@
+/* global IITC -- eslint */
+
/**
* @file This file provides functions for working with the sidebar.
* @module sidebar
@@ -181,24 +183,20 @@ function setPermaLink () {
* @function setupAddons
*/
function setupAddons () {
- $('')
- .html('Permalink')
- .attr({
- id: 'permalink',
- title: 'URL link to this map view'
- })
- .on({
- mouseover: setPermaLink,
- click: setPermaLink
- })
- .appendTo('#toolbox');
-
- $('')
- .html('About IITC')
- .attr('id', 'about-iitc')
- .css('cursor', 'help')
- .click(aboutIITC)
- .appendTo('#toolbox');
+ IITC.toolbox.addButton({
+ id: 'permalink',
+ label: 'Permalink',
+ title: 'URL link to this map view',
+ action: setPermaLink,
+ mouseover: setPermaLink,
+ });
+
+ IITC.toolbox.addButton({
+ id: 'about-iitc',
+ label: 'About IITC',
+ action: window.aboutIITC,
+ class: 'cursor_help',
+ });
window.artifact.setup();
diff --git a/core/code/toolbox.js b/core/code/toolbox.js
new file mode 100644
index 000000000..b6daf5a8b
--- /dev/null
+++ b/core/code/toolbox.js
@@ -0,0 +1,239 @@
+/* global IITC */
+
+/**
+ * Toolbox API
+ *
+ * @memberof IITC
+ * @namespace toolbox
+ */
+
+/**
+ * @typedef {Object} ButtonArgs
+ * @property {string} [id] - Optional. The ID of the button.
+ * @property {string|undefined} label - The label text of the button.
+ * @property {Function|undefined} action - The onclick action for the button.
+ * @property {string|null} [class] - Optional. The class(es) for the button.
+ * @property {string|null} [title] - Optional. The title (tooltip) for the button.
+ * @property {string|null} [access_key] - Optional. The access key for the button.
+ * @property {Function|null} [mouseover] - Optional. The mouseover event for the button.
+ * @property {string|null} [icon] - Optional. Icon name from FontAwesome for the button.
+ */
+
+IITC.toolbox = {
+ buttons: {},
+ _defaultSortMethod: (a, b) => a.label.localeCompare(b.label),
+ sortMethod: (...args) => IITC.toolbox._defaultSortMethod(...args),
+
+ /**
+ * Adds a button to the toolbox.
+ *
+ * @param {ButtonArgs} buttonArgs - The arguments for the button.
+ * @returns {string|null} The ID of the added button or null if required parameters are missing.
+ *
+ * @example
+ * const buttonId = IITC.toolbox.addButton({
+ * label: 'AboutIITC',
+ * action: window.AboutIITC
+ * });
+ *
+ * @example
+ * const buttonId = IITC.toolbox.addButton({
+ * label: 'Test Button',
+ * action: () => alert('Clicked!')
+ * });
+ */
+ addButton(buttonArgs) {
+ if (!buttonArgs.label) {
+ console.warn('Required parameter "label" are missing.');
+ return null;
+ }
+
+ if (!buttonArgs.action) {
+ console.warn('Required parameter "action" are missing.');
+ return null;
+ }
+
+ let id = buttonArgs.id || `toolbox-btn-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
+ this.buttons[id] = buttonArgs;
+
+ this._renderButton(id);
+ this._applySort();
+
+ return id;
+ },
+
+ /**
+ * Updates an existing button in the toolbox.
+ *
+ * @param {string} buttonId - The ID of the button to update.
+ * @param {ButtonArgs} newButtonArgs - The new arguments for the button.
+ * @returns {boolean} True if the button is successfully updated, false otherwise.
+ *
+ * @example
+ * const isUpdated = IITC.toolbox.updateButton(buttonId, { label: 'Updated Button', action: () => console.log('New Action') });
+ */
+ updateButton(buttonId, newButtonArgs) {
+ if (this.buttons[buttonId]) {
+ Object.assign(this.buttons[buttonId], newButtonArgs);
+ this._renderButton(buttonId);
+ this._applySort();
+ return true;
+ } else {
+ console.warn(`Button with ID ${buttonId} not found.`);
+ return false;
+ }
+ },
+
+ /**
+ * Removes a button from the toolbox.
+ *
+ * @param {string} buttonId - The ID of the button to remove.
+ * @returns {boolean} True if the button is successfully removed, false otherwise.
+ *
+ * @example
+ * const isRemoved = IITC.toolbox.removeButton(buttonId);
+ */
+ removeButton(buttonId) {
+ if (this.buttons[buttonId]) {
+ delete this.buttons[buttonId];
+ const buttonElement = document.getElementById(buttonId);
+ if (buttonElement) {
+ buttonElement.remove();
+ }
+ this._applySort();
+ return true;
+ } else {
+ console.warn(`Button with ID ${buttonId} not found for removal.`);
+ return false;
+ }
+ },
+
+ /**
+ * Internal method to render a button.
+ *
+ * @private
+ * @param {string} buttonId - The ID of the button to render.
+ */
+ _renderButton(buttonId) {
+ const buttonData = this.buttons[buttonId];
+ if (!buttonData) return; // The button with the given ID was not found
+
+ let buttonElement = document.getElementById(buttonId) || document.createElement('a');
+ buttonElement.id = buttonId;
+ buttonElement.textContent = buttonData.label;
+ buttonElement.onclick = buttonData.action;
+
+ if (typeof buttonData.title === 'string') buttonElement.title = buttonData.title;
+ if (typeof buttonData.class === 'string') buttonElement.className = buttonData.class;
+ if (typeof buttonData.access_key === 'string') buttonElement.accessKey = buttonData.access_key;
+ if (typeof buttonData.mouseover === 'string') buttonElement.mouseover = buttonData.mouseover;
+
+ if (typeof buttonData.icon === 'string') {
+ const iconHTML = ``;
+ buttonElement.innerHTML = iconHTML + buttonElement.innerHTML;
+ }
+
+ const toolbox_component = document.querySelector('#toolbox_component');
+ if (!document.getElementById(buttonId)) {
+ toolbox_component.appendChild(buttonElement);
+ }
+ },
+
+ /**
+ * Internal method to apply sorting to the buttons.
+ *
+ * @private
+ */
+ _applySort() {
+ const toolbox_component = document.querySelector('#toolbox_component');
+ const buttonElements = Array.from(toolbox_component.children);
+
+ try {
+ buttonElements.sort((a, b) => this.sortMethod(this.buttons[a.id], this.buttons[b.id]));
+ } catch (e) {
+ console.error('Sorting function produced error', e);
+ buttonElements.sort((a, b) => this._defaultSortMethod(this.buttons[a.id], this.buttons[b.id]));
+ }
+ buttonElements.forEach((buttonElement) => toolbox_component.appendChild(buttonElement));
+ },
+
+ /**
+ * Sets the sorting method for the toolbox buttons.
+ *
+ * @param {Function} sortMethod - The sorting method to be used.
+ * @returns {void}
+ *
+ * @example
+ * IITC.toolbox.setSortMethod((a, b) => a.label.localeCompare(b.label));
+ */
+ setSortMethod(sortMethod) {
+ this.sortMethod = sortMethod;
+ this._applySort();
+ },
+
+ /**
+ * Internal method to synchronize the toolbox with the legacy toolbox.
+ *
+ * @private
+ * @returns {void}
+ */
+ _syncWithLegacyToolbox() {
+ // Select the old toolbox element
+ const oldToolbox = document.querySelector('#toolbox');
+
+ // Function to process an individual button
+ const processButton = (node) => {
+ // Check if the node is an 'A' tag (anchor/link, which represents a button)
+ if (node.tagName === 'A') {
+ let iconClass = null;
+ // Find an icon element within the button, if it exists
+ const iconElement = node.querySelector('i.fa');
+ if (iconElement) {
+ // Extract the icon class
+ const iconClasses = Array.from(iconElement.classList).filter((cls) => cls.startsWith('fa-'));
+ if (iconClasses.length > 0) iconClass = iconClasses[0];
+ }
+
+ // Prepare the button arguments for either updating or adding the button
+ const buttonArgs = {
+ id: node.id,
+ label: node.textContent.trim(),
+ action: () => node.click(),
+ class: node.className,
+ title: node.title,
+ access_key: node.accessKey,
+ mouseover: node.mouseover,
+ icon: iconClass,
+ };
+
+ // Update an existing button or add a new one
+ buttonArgs['id'] = `legacy-toolbox-btn-${buttonArgs.id || buttonArgs.label}`;
+ if (this.buttons[buttonArgs.id]) {
+ this.updateButton(buttonArgs.id, buttonArgs);
+ } else {
+ this.addButton(buttonArgs);
+ }
+ }
+ };
+
+ // Initialize for existing buttons in the toolbox
+ oldToolbox.querySelectorAll('a').forEach(processButton);
+
+ // Mutation observer to watch for changes in the toolbox
+ const observer = new MutationObserver((mutations) => {
+ // Iterate through mutations
+ mutations.forEach((mutation) => {
+ // Process each added node and attribute changes
+ mutation.addedNodes.forEach(processButton);
+ if (mutation.type === 'attributes') {
+ processButton(mutation.target);
+ }
+ });
+ });
+
+ // Start observing the toolbox for changes
+ observer.observe(oldToolbox, { childList: true, subtree: true, attributes: true });
+ },
+};
+
+IITC.toolbox._syncWithLegacyToolbox();
diff --git a/core/smartphone.css b/core/smartphone.css
index a98f996a0..f480fef89 100644
--- a/core/smartphone.css
+++ b/core/smartphone.css
@@ -238,7 +238,7 @@ body {
border: 2px outset #20A8B1;
}
-#toolbox > a {
+#toolbox > a, #toolbox_component > a {
padding: 5px;
margin-top: 3px;
margin-bottom: 3px;
diff --git a/core/style.css b/core/style.css
index 6759ad88e..3fb0b8879 100644
--- a/core/style.css
+++ b/core/style.css
@@ -840,10 +840,14 @@ h3.title {
}
#toolbox {
+ display: none;
+}
+
+#toolbox, #toolbox_component {
text-align: left; /* centre didn't look as nice here as it did above in .linkdetails */
}
-#toolbox > a {
+#toolbox > a, #toolbox_component > a {
margin-left: 5px;
margin-right: 5px;
white-space: nowrap;
@@ -1276,6 +1280,10 @@ table.artifact .portal {
text-align: center;
}
+.cursor_help {
+ cursor: help;
+}
+
/* region scores */
.cellscore .ui-accordion-header, .cellscore .ui-accordion-content {
border: 1px solid #20a8b1;
diff --git a/core/total-conversion-build.js b/core/total-conversion-build.js
index 631bd245c..e043661ad 100644
--- a/core/total-conversion-build.js
+++ b/core/total-conversion-build.js
@@ -119,42 +119,45 @@ document.head.innerHTML = ''
// remove body element entirely to remove event listeners
document.body = document.createElement('body');
-document.body.innerHTML = ''
- + '