- // ==UserScript==
- // @name IdlePixel+ Zlef's Modal & Settings Manager
- // @namespace com.zlef.modallibrary
- // @version 1.0.2
- // @description Modal framework for Idle-Pixel with a Settings manager, modal optional for settings
- // @author Zlef
- // @match *://idle-pixel.com/*
- // @grant none
- // @license MIT
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- if(window.ZlefsModal ) {
- return;
- }
-
- class ZlefsModal {
- constructor(pluginName) {
- this.pluginName = pluginName;
- this.modals = [];
- this.overlay = null;
- this.closeCallbacks = [];
- this.initCustomCSS();
- }
-
- initCustomCSS() {
- const css = `
- .zlefs-modal {
- position: absolute; /* Ensure it works with draggable */
- background-color: #fff;
- border-radius: 8px;
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
- max-height: 80vh;
- max-width: 80vh;
- display: flex;
- flex-direction: column;
- overflow: hidden; /* Ensures rounded corners are visible */
- }
- .zlefs-modal-header {
- cursor: move;
- padding-bottom: 40px;
- margin-bottom: 10px;
- background-color: #f1f1f1;
- border-bottom: 1px solid #ccc;
- position: sticky;
- top: 0;
- left: 0;
- right: 0;
- width: 100%;
- box-sizing: border-box;
- border-top-left-radius: 8px; /* Rounded corners */
- border-top-right-radius: 8px; /* Rounded corners */
- z-index: 10; /* Ensure header is above the content */
- }
- .zlefs-modal-header-text {
- position: absolute;
- top: 50%;
- left: 10px;
- transform: translateY(-50%);
- pointer-events: none;
- height: 20px;
- font-weight: bold;
- }
- .zlefs-modal-content {
- padding: 0 20px 20px 20px;
- overflow-y: auto;
- flex-grow: 1; /* Allows the content to grow and fill the remaining space */
- }
- .zlefs-close-button {
- position: absolute;
- top: 10px;
- right: 10px;
- background: none;
- border: none;
- cursor: pointer;
- z-index: 11; /* Ensure close button is above the header */
- }
- #zlefs-modal-overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.7);
- z-index: 1000;
- display: flex;
- justify-content: center;
- align-items: center;
- }
- .zlefs-section {
- border: 1px solid black;
- background-color: white;
- padding: 10px 10px 0px 10px;
- margin-bottom: 10px;
- }
- .zlefs-section-title {
- margin-bottom: 10px;
- margin-left: 2px;
- font-weight: bold;
- cursor: pointer;
- }
- .zlefs-section-content {
- display: none;
- }
- .zlefs-section-content.zlefs-hidden {
- display: block;
- }
- .zlefs-item {
- width: 100%;
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 5px;
- }
- .zlefs-checkbox-container {
- display: flex;
- justify-content: space-between;
- align-items: center;
- width: 100%;
- max-width: 400px;
- }
- .zlefs-checkbox-label {
- flex: 1;
- text-align: left;
- margin-right: 10px;
- }
- .zlefs-checkbox-input {
- flex: 0;
- text-align: right;
- }
- .zlefs-vertical-divider {
- width: 1px;
- background-color: #ccc;
- padding: 0;
- display: flex;
- }
- `;
- this.addCSS(css)
-
- }
-
- addCSS(cssString) {
- const style = document.createElement('style');
- style.type = 'text/css';
- style.appendChild(document.createTextNode(cssString));
- document.head.appendChild(style);
- }
-
- createOverlay() {
- if (this.overlay) return;
-
- this.overlay = document.createElement('div');
- this.overlay.id = 'zlefs-modal-overlay';
-
- this.overlay.addEventListener('click', (event) => {
- if (event.target === this.overlay) {
- this.closeTopModal();
- }
- });
-
- document.body.appendChild(this.overlay);
- }
-
- addModal(content, name, width = 'auto', height = 'auto', closeCallback = null) {
- this.createOverlay();
-
- const modalBox = document.createElement('div');
- modalBox.className = 'zlefs-modal';
- modalBox.style.width = typeof width === 'number' ? `${width}px` : width;
- modalBox.style.height = typeof height === 'number' ? `${height}px` : height;
-
- const closeButton = document.createElement('button');
- closeButton.className = 'zlefs-close-button';
- closeButton.textContent = '✖';
- closeButton.addEventListener('click', () => {
- this.closeModal(modalBox);
- if (closeCallback) closeCallback();
- });
-
- const header = document.createElement('div');
- header.className = 'zlefs-modal-header';
-
- const nameSpan = document.createElement('span');
- nameSpan.textContent = name;
- nameSpan.className = 'zlefs-modal-header-text';
-
- header.appendChild(nameSpan);
-
- const contentWrapper = document.createElement('div');
- contentWrapper.className = 'zlefs-modal-content';
- contentWrapper.appendChild(content);
-
- modalBox.appendChild(header);
- modalBox.appendChild(closeButton);
- modalBox.appendChild(contentWrapper);
- this.overlay.appendChild(modalBox);
- this.modals.push(modalBox);
- this.closeCallbacks.push(closeCallback);
-
- this.makeDraggable(modalBox, header);
-
- // Ensure modal is fully rendered before centering
- setTimeout(() => {
- this.centreModal(modalBox);
- }, 0);
- }
-
- centreModal(modalBox) {
- if (!modalBox) {
- console.log("modalBox is not defined. If you're seeing this good luck lol.");
- return;
- }
- const rect = modalBox.getBoundingClientRect();
-
- // Window dimensions
- const windowHeight = window.innerHeight;
- const windowWidth = window.innerWidth;
- console.log(`windowHeight: ${windowHeight}`);
- console.log(`windowWidth: ${windowWidth}`);
- console.log(`modalBox height: ${rect.height}`);
- console.log(`modalBox width: ${rect.width}`);
-
- const newModalTop = (windowHeight - rect.height) / 2;
- const newModalLeft = (windowWidth - rect.width) / 2;
- console.log(`newModalTop: ${Math.floor(newModalTop)}`);
- console.log(`newModalLeft: ${Math.floor(newModalLeft)}`);
-
- // Set the top and left positions
- modalBox.style.position = 'absolute'; // Ensure the positioning context
- modalBox.style.top = `${Math.floor(newModalTop)}px`;
- modalBox.style.left = `${Math.floor(newModalLeft)}px`;
-
- // Set min width as moving the modal against the side can squish it. Should probably solve that behavior instead but here we are.
- modalBox.style.minWidth = `${rect.width}px`;
-
- // Additional log to check the final style
- console.log(`modalBox final top: ${modalBox.style.top}`);
- console.log(`modalBox final left: ${modalBox.style.left}`);
- console.log(`modalBox final minWidth: ${modalBox.style.minWidth}`);
-
- }
-
- closeModal(modal) {
- const modalIndex = this.modals.indexOf(modal);
- if (modalIndex !== -1) {
- this.modals.splice(modalIndex, 1);
- this.closeCallbacks.splice(modalIndex, 1);
- }
- if (modal && modal.parentElement) {
- this.overlay.removeChild(modal);
- }
- if (this.modals.length === 0 && this.overlay) {
- document.body.removeChild(this.overlay);
- this.overlay = null;
- }
- }
-
- closeTopModal() {
- if (this.modals.length > 0) {
- const topmodal = this.modals[this.modals.length - 1];
- const topCallback = this.closeCallbacks[this.closeCallbacks.length - 1];
- this.closeModal(topmodal);
- if (topCallback) topCallback();
- }
- }
-
- makeDraggable(modalBox, header) {
- let offsetX = 0, offsetY = 0, startX = 0, startY = 0;
-
- const onMouseDown = (e) => {
- e.preventDefault();
-
- startX = e.clientX;
- startY = e.clientY;
-
- offsetX = modalBox.offsetLeft;
- offsetY = modalBox.offsetTop;
- document.addEventListener('mousemove', onMouseMove);
- document.addEventListener('mouseup', onMouseUp);
- };
-
- const onMouseMove = (e) => {
- e.preventDefault();
- let dx = e.clientX - startX;
- let dy = e.clientY - startY;
-
- let newLeft = offsetX + dx;
- let newTop = offsetY + dy;
-
- modalBox.style.left = `${newLeft}px`;
- modalBox.style.top = `${newTop}px`;
- };
-
- const onMouseUp = (e) => {
- const rect = modalBox.getBoundingClientRect();
- const corners = {
- topLeft: { left: rect.left, top: rect.top },
- bottomRight: { left: rect.right, top: rect.bottom }
- };
-
- if (rect.left < 0) {
- modalBox.style.left = '0px';
- }
- if (rect.top < 0) {
- modalBox.style.top = '0px';
- }
- if (rect.right > window.innerWidth) {
- modalBox.style.left = (window.innerWidth - rect.width) + 'px';
- }
- if (rect.bottom > window.innerHeight) {
- modalBox.style.top = (window.innerHeight - rect.height) + 'px';
- }
-
- document.removeEventListener('mousemove', onMouseMove);
- document.removeEventListener('mouseup', onMouseUp);
- };
-
-
- header.addEventListener('mousedown', onMouseDown);
- }
-
- repositionModal(modalBox) {
- if (!modalBox) return;
-
- const rect = modalBox.getBoundingClientRect();
- const corners = {
- topLeft: { left: rect.left, top: rect.top },
- bottomRight: { left: rect.right, top: rect.bottom }
- };
-
- if (rect.left < 0) {
- modalBox.style.left = (rect.width) + 'px';
- }
- if (rect.top < 0) {
- modalBox.style.top = (rect.height) + 'px';
- }
- if (rect.right > window.innerWidth) {
- modalBox.style.left = (window.innerWidth - (rect.width)) + 'px';
- }
- if (rect.bottom > window.innerHeight) {
- modalBox.style.top = (window.innerHeight - (rect.height)) + 'px';
- }
- }
-
- titleCaseUnderscore(input) {
- const words = input.split('_');
- const convertedText = words.map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
- return convertedText;
- }
-
- addTitle(parent, text, level = 2, textAlign = 'left') {
- const title = document.createElement(`h${level}`);
- title.textContent = text;
- title.style.textAlign = textAlign;
- parent.appendChild(title);
- return title
- }
-
- addSection(parent, sectionTitleText) {
- const sectionDiv = document.createElement('div');
- sectionDiv.className = 'zlefs-section';
-
- const sectionTitle = document.createElement('div');
- sectionTitle.className = 'zlefs-section-title';
- sectionTitle.textContent = sectionTitleText;
-
- const sectionContent = document.createElement('div');
- sectionContent.className = 'zlefs-section-content';
-
- sectionTitle.addEventListener('click', () => {
- const modal = this.findParentmodal(sectionDiv);
-
- const originalTop = modal.getBoundingClientRect().top;
- const originalScrollTop = document.documentElement.scrollTop || document.body.scrollTop;
-
- sectionContent.classList.toggle('zlefs-hidden');
-
- const newTop = modal.getBoundingClientRect().top;
- const deltaHeight = parseInt((originalTop - newTop));
-
- const currentTop = parseInt(window.getComputedStyle(modal).top);
- modal.style.top = `${(currentTop + deltaHeight)}px`;
-
- this.repositionModal(modal);
-
- });
-
- sectionDiv.appendChild(sectionTitle);
- sectionDiv.appendChild(sectionContent);
- parent.appendChild(sectionDiv);
-
- return sectionContent;
- }
-
- findParentmodal(element) {
- while (element && !element.classList.contains('zlefs-modal')) {
- element = element.parentElement;
- }
- return element;
- }
-
- addDiv(parent) {
- const div = document.createElement('div');
- parent.appendChild(div);
- return div;
- }
-
- addContainer(parent) {
- const container = document.createElement('div');
- container.className = 'container';
- parent.appendChild(container);
- return container;
- }
-
- addRow(parent) {
- const row = document.createElement('div');
- row.className = 'row';
- parent.appendChild(row);
- return row;
- }
-
- addCol(parent, size = '', align = 'left') {
- const col = document.createElement('div');
- col.className = size ? `col-${size}` : 'col';
- col.style.textAlign = align === 'centre' ? 'center' : align;
- parent.appendChild(col);
- return col;
- }
-
- addDivider(parent, margin = 5) {
- const divider = document.createElement('hr');
- divider.style.width = `calc(100% - ${2 * margin}px)`;
- divider.style.marginLeft = `${margin}px`;
- divider.style.marginRight = `${margin}px`;
- parent.appendChild(divider);
- return divider;
- }
-
- addVDivider(parent) {
- const divider = document.createElement('div');
- divider.className = 'zlefs-vertical-divider';
- parent.appendChild(divider);
- }
-
- addInput(parent, type, value, placeholder, min, max, onChange, label = undefined) {
- const inputContainer = document.createElement('div');
- inputContainer.style.display = 'flex';
- inputContainer.style.alignItems = 'center';
- inputContainer.style.marginBottom = '10px';
-
- if (label){
- const inputLabel = document.createElement('label');
- inputLabel.textContent = this.titleCaseUnderscore(label);
- inputLabel.style.marginRight = '10px';
- inputLabel.style.flex = '1';
- inputLabel.style.cursor = 'pointer';
- inputContainer.appendChild(inputLabel);
- }
-
- const input = document.createElement('input');
- input.type = type;
- input.value = value;
- input.style.flex = '0';
- if (placeholder) input.placeholder = placeholder;
- if (min !== undefined) input.min = min;
- if (max !== undefined) input.max = max;
- input.addEventListener('input', (event) => {
- let inputValue = event.target.value;
- if (type === 'number') {
- if (inputValue < min) inputValue = min;
- if (inputValue > max) inputValue = max;
- event.target.value = inputValue;
- }
- onChange(inputValue);
- });
-
- inputContainer.appendChild(input);
- parent.appendChild(inputContainer);
- }
-
- addCheckbox(parent, checked, onChange, label = undefined) {
- const checkboxContainer = document.createElement('div');
- checkboxContainer.className = 'zlefs-checkbox-container';
- checkboxContainer.style.display = 'flex';
- checkboxContainer.style.alignItems = 'center';
- checkboxContainer.style.marginBottom = '10px';
-
- const checkbox = document.createElement('input');
- checkbox.type = 'checkbox';
- checkbox.checked = checked;
- checkbox.className = 'zlefs-checkbox-input';
- checkbox.style.flex = '0';
- checkbox.style.cursor = 'pointer';
- checkbox.addEventListener('change', (event) => onChange(event.target.checked));
-
-
- if (label){
- const checkboxLabel = document.createElement('label');
- checkboxLabel.textContent = this.titleCaseUnderscore(label);
- checkboxLabel.className = 'zlefs-checkbox-label';
- checkboxLabel.style.flex = '1';
- checkboxLabel.style.cursor = 'pointer';
-
- checkboxContainer.appendChild(checkboxLabel);
- checkboxLabel.addEventListener('click', () => checkbox.click());
- }
-
- checkboxContainer.appendChild(checkbox);
- parent.appendChild(checkboxContainer);
- }
-
- addButton(parent, text, onClick, className = 'btn') {
- const button = document.createElement('button');
- button.textContent = text;
- button.className = className;
- button.addEventListener('click', onClick);
- parent.appendChild(button);
- return button;
- }
-
- addCombobox(parent, options, selectedValue, onChange, label = undefined) {
- const comboboxContainer = document.createElement('div');
- comboboxContainer.style.display = 'flex';
- comboboxContainer.style.alignItems = 'center';
- comboboxContainer.style.marginBottom = '10px';
-
- if (label) {
- const comboboxLabel = document.createElement('label');
- comboboxLabel.textContent = this.titleCaseUnderscore(label);
- comboboxLabel.style.marginRight = '10px';
- comboboxLabel.style.flex = '1';
- comboboxLabel.style.cursor = 'pointer';
- comboboxContainer.appendChild(comboboxLabel);
- }
-
- const combobox = document.createElement('select');
- combobox.style.flex = '0';
- options.forEach(option => {
- const optionElement = document.createElement('option');
- optionElement.value = option;
- optionElement.text = option;
- if (option === selectedValue) optionElement.selected = true;
- combobox.appendChild(optionElement);
- });
-
- combobox.addEventListener('change', (event) => onChange(event.target.value));
-
- comboboxContainer.appendChild(combobox);
- parent.appendChild(comboboxContainer);
- return comboboxContainer;
- }
- }
-
- class ZlefsSettingsManager {
- constructor(prefix, settings) {
- this.prefix = prefix;
- this.defaultSettings = JSON.parse(JSON.stringify(settings));
- this.settings = settings;
- this.modalContent = null;
- this.loadSettings();
- }
-
- saveSettings() {
- const settingsToSave = {};
-
- const processSettings = (settings, saveObj) => {
- for (const key in settings) {
- const setting = settings[key];
- if (setting.type === 'button') {
- continue; // Skip button types
- }
- if (setting.type === 'section') {
- saveObj[key] = {
- type: 'section',
- name: setting.name,
- settings: {}
- };
- processSettings(setting.settings, saveObj[key].settings);
- } else if (setting.type === 'multicheckbox') {
- saveObj[key] = {
- type: 'multicheckbox',
- values: {}
- };
- for (const subKey in setting.values) {
- saveObj[key].values[subKey] = setting.values[subKey];
- }
- } else if (setting.type === 'combobox') {
- saveObj[key] = {
- type: 'combobox',
- options: setting.options,
- value: setting.value
- };
- } else {
- saveObj[key] = {
- type: setting.type,
- value: setting.value
- };
- }
- }
- };
-
- processSettings(this.settings, settingsToSave);
- localStorage.setItem(`${this.prefix}_settings`, JSON.stringify(settingsToSave));
- }
-
- loadSettings() {
- const savedSettings = JSON.parse(localStorage.getItem(`${this.prefix}_settings`));
- if (savedSettings) {
- const processLoadedSettings = (loadedSettings, currentSettings) => {
- for (const key in currentSettings) {
- if (loadedSettings[key] !== undefined) {
- const setting = currentSettings[key];
- if (setting.type === 'button') {
- continue; // Skip button types
- }
- if (setting.type === 'section') {
- processLoadedSettings(loadedSettings[key].settings, setting.settings);
- } else if (setting.type === 'multicheckbox') {
- for (const subKey in setting.values) {
- setting.values[subKey] = loadedSettings[key].values[subKey] !== undefined
- ? loadedSettings[key].values[subKey]
- : setting.values[subKey];
- }
- } else {
- setting.value = loadedSettings[key].value;
- }
- }
- }
-
- for (const key in loadedSettings) {
- if (currentSettings[key] === undefined) {
- delete loadedSettings[key];
- }
- }
- };
-
- processLoadedSettings(savedSettings, this.settings);
- localStorage.setItem(`${this.prefix}_settings`, JSON.stringify(this.settings));
- }
- }
-
- resetSettings() {
- this.settings = JSON.parse(JSON.stringify(this.defaultSettings));
- this.saveSettings();
- }
-
- deleteSettings() {
- localStorage.removeItem(`${this.prefix}_settings`);
- }
-
- getSettings() {
- return this.settings;
- }
-
- settingsChanged(fullKey, value, subKey = null) {
- const keys = fullKey.split('.');
- let currentSettings = this.settings;
-
- for (let i = 0; i < keys.length - 1; i++) {
- if (currentSettings[keys[i]].type === 'section') {
- currentSettings = currentSettings[keys[i]].settings;
- } else if (currentSettings[keys[i]].type === 'multicheckbox' && subKey) {
- currentSettings = currentSettings[keys[i]].values;
- } else {
- currentSettings = currentSettings[keys[i]];
- }
- }
-
- const finalKey = keys[keys.length - 1];
-
- if (!currentSettings[finalKey]) {
- console.error(`settingsChanged - Key ${finalKey} not found in settings`);
- return;
- }
-
- if (subKey) {
- currentSettings[finalKey].values[subKey] = value;
- } else {
- currentSettings[finalKey].value = value;
- }
- this.saveSettings();
- }
-
- createSettingsModal(modalFramework, width = 'auto', height = 'auto', closeCallback = null) {
- return () => {
- const content = document.createElement('div');
-
- const buildSettings = (settings, parent) => {
- if (!parent) {
- console.error('Parent element is null');
- return;
- }
-
- for (const key in settings) {
- const setting = settings[key];
- const fullKey = key;
-
- if (setting.type === 'section') {
- const sectionContent = modalFramework.addSection(parent, setting.name);
- buildSettings(setting.settings, sectionContent);
- } else if (setting.type === 'multicheckbox') {
- const sectionContent = modalFramework.addSection(parent, setting.name);
- for (const subKey in setting.values) {
- modalFramework.addCheckbox(sectionContent, setting.values[subKey], (value) => {
- this.settingsChanged(`${fullKey}.${subKey}`, value, subKey);
- }, subKey);
- }
- } else if (setting.type === 'checkbox') {
- modalFramework.addCheckbox(parent, setting.value, (value) => {
- this.settingsChanged(fullKey, value);
- }, key);
- } else if (setting.type === 'numinput') {
- modalFramework.addInput(parent, 'number', setting.value, '', setting.minValue, setting.maxValue, (value) => {
- this.settingsChanged(fullKey, value);
- }, key);
- } else if (setting.type === 'text') {
- modalFramework.addInput(parent, 'text', setting.value, setting.placeholder, undefined, undefined, (value) => {
- this.settingsChanged(fullKey, value);
- }, key);
- } else if (setting.type === 'combobox') {
- modalFramework.addCombobox(parent, setting.options, setting.value, (value) => {
- this.settingsChanged(fullKey, value);
- }, key);
- } else if (setting.type === 'button') {
- modalFramework.addButton(parent, setting.name, setting.function, 'btn');
- }
- }
- };
-
- buildSettings(this.settings, content);
-
- modalFramework.addModal(content, `${this.prefix} Settings`, width, height, closeCallback);
- };
- }
-
- }
-
- window.ZlefsModal = ZlefsModal;
- window.ZlefsSettingsManager = ZlefsSettingsManager;
- console.log(`Zlef's Modal and Settings Manager version ${GM_info.script.version} loaded.`);
- })();