IdlePixel+ Zlef's Modal & Settings Manager

Modal framework for Idle-Pixel with a Settings manager, modal optional for settings

Script này sẽ không được không được cài đặt trực tiếp. Nó là một thư viện cho các script khác để bao gồm các chỉ thị meta // @require https://update.greasyfork.org/scripts/500003/1408507/IdlePixel%2B%20Zlef%27s%20Modal%20%20Settings%20Manager.js

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