// ==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.`);
})();