// ==UserScript==
// @name Bubble Editor Enhancements
// @namespace http://tampermonkey.net/
// @version 2025.05.10-04
// @description Enhances Bubble.io editor: Data Type/Option Sets/Attributes (layout, colors). Modal dialogs - full keyboard support (Enter/Esc, Tab nav). Toggleable canvas padding. Ctrl+N for hinted "Create a new..." buttons.
// @author [email protected]
// @match https://*bubble.io/page*
// @icon https://www.google.com/s2/favicons?sz=64&domain=bubble.io
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const SCRIPT_NAME = "Bubble Enhancements";
const DEBUG_MODE = false;
const CANVAS_PADDING_ENABLED_KEY = "bubbleEnhancements_canvasPaddingEnabled";
let isCanvasPaddingEnabled = true;
let canvasPaddingMenuCommandId = null;
const NAME_INPUT_SELECTOR_IN_MODAL = '.new-field-name-wrapper input.new-field-name.bubble-ui';
const TYPE_DROPDOWN_SELECTOR_IN_MODAL = '.editor.dropdown.new-field-type .property-editor-control.composer-container';
const DROPDOWN_CAPTION_SELECTOR = '.dropdown-caption-container';
const DROPDOWN_CONTAINER_ACTUAL_SELECTOR = '.dropdown-container.new-composer';
const CHECKBOX_SELECTOR_IN_MODAL = '.editor.checkbox .new-composer.composer-checkbox .component-checkbox';
const MODAL_CONTENT_SELECTOR = '.popup-content';
const CREATE_NEW_BUTTON_SELECTOR = '.btn.add-new-type';
function logDebug(message, ...args) { if (DEBUG_MODE) { console.log(`[${SCRIPT_NAME}] DEBUG:`, message, ...args); } }
function logInfo(message, ...args) { console.log(`[${SCRIPT_NAME}] INFO:`, message, ...args); }
function logError(message, ...args) { console.error(`[${SCRIPT_NAME}] ERROR:`, message, ...args); }
function addGlobalStyle(css) {
const head = document.getElementsByTagName('head')[0];
if (!head) { logError("Could not find <head> to inject CSS."); return; }
const style = document.createElement('style');
style.type = 'text/css'; style.innerHTML = css; head.appendChild(style);
logDebug("Custom CSS injected.");
}
function applyCanvasPaddingState(enabled) { /* ... (no change) ... */
if (enabled) document.body.classList.add('canvas-padding-active');
else document.body.classList.remove('canvas-padding-active');
logDebug(`Canvas padding ${enabled ? 'enabled' : 'disabled'}.`);
}
function toggleCanvasPadding() { /* ... (no change) ... */
isCanvasPaddingEnabled = !isCanvasPaddingEnabled;
GM_setValue(CANVAS_PADDING_ENABLED_KEY, isCanvasPaddingEnabled);
applyCanvasPaddingState(isCanvasPaddingEnabled);
registerMenuCommands();
logInfo(`Canvas padding toggled ${isCanvasPaddingEnabled ? 'ON' : 'OFF'}.`);
}
function registerMenuCommands() { /* ... (no change) ... */
if (canvasPaddingMenuCommandId !== null) {
try { GM_unregisterMenuCommand(canvasPaddingMenuCommandId); } catch (e) { logError("Error unregistering menu command:", e); }
}
canvasPaddingMenuCommandId = GM_registerMenuCommand(
`Toggle Canvas Right Padding (Currently: ${isCanvasPaddingEnabled ? 'ON' : 'OFF'})`,
toggleCanvasPadding, 'c'
);
logDebug("Registered menu command:", canvasPaddingMenuCommandId);
}
function loadSettings() { /* ... (no change) ... */
isCanvasPaddingEnabled = GM_getValue(CANVAS_PADDING_ENABLED_KEY, true);
applyCanvasPaddingState(isCanvasPaddingEnabled);
}
loadSettings();
registerMenuCommands();
addGlobalStyle(`
body.canvas-padding-active .canvas-inner { justify-self: end !important; padding-right: 1%; }
/* a.logo { visibility: hidden; } */
.field-name { width: 50%; min-width: 250px; display: flex; }
.field-name > div.composer-textbox { flex: 1 1 auto; min-width: 168px; }
.field-name input { width: 100% !important; min-width: 168px; }
.custom-fields:not(.option-set-attributes) > div.field:not(.built-in) { display: flex; align-items: center; gap: 12px; width: 100%; padding: 8px 6px; box-sizing: border-box; }
.custom-fields:not(.option-set-attributes) > div.field:not(.built-in) > .new-composer.composer-textbox:first-child { width: 34%; min-width: 180px; flex-shrink: 0; }
.custom-fields:not(.option-set-attributes) > div.field:not(.built-in) > .new-composer.composer-textbox:first-child input { width: 100%; box-sizing: border-box; }
.custom-fields > div.field > .field-type { flex-shrink: 2; text-align: left; min-width: 0; }
.custom-fields:not(.option-set-attributes) > div.field:not(.built-in) > .field-default-caption { width: 60px; flex-shrink: 0; text-align: center; }
.custom-fields:not(.option-set-attributes) > div.field:not(.built-in) > .composer-dropdown.bubble-ui > .spot { width: 100%; box-sizing: border-box; }
.custom-fields > div.field > .delete-btn { order: 2; flex-shrink: 0; }
.custom-fields > div.field > .comment-btn { flex-shrink: 0; margin-left: auto; }
.custom-fields > div.field:not(.built-in):nth-child(even) { background-color: #f7f7f7; }
.custom-fields > div.field:not(.built-in):nth-child(odd) { background-color: transparent; }
.custom-fields .built-in-fields { margin-top: 10px; }
.custom-fields .built-in-fields > div.field.built-in { display: flex; align-items: center; gap: 12px; padding: 8px 6px; border-top: 1px solid #eee; }
.field:not(.built-in) > .field-default-caption ~ input { flex-grow:1; width: auto !important; min-width:0; }
.field:not(.built-in) > .field-default-caption ~ .composer-dropdown,
.field:not(.built-in) > .field-default-caption ~ .composer-textbox{ flex-grow:1; }
.field:not(.built-in) > .field-default-caption ~ .composer-image,
.field:not(.built-in) > .field-default-caption ~ .composer-file{ margin-right: auto; }
.composer-textbox > input { width: 100% !important; }
.custom-fields .built-in-fields > div.field.built-in .built-in-field-name { width: 33%; min-width: 150px; flex-shrink: 0; }
.custom-fields .built-in-fields > div.field.built-in .built-in-mention { margin-right: auto; white-space: nowrap; flex-shrink: 0; }
.custom-fields .built-in-fields > div.field.built-in:nth-child(even) { background-color: #fafafa; }
.custom-fields .built-in-fields > div.field.built-in:nth-child(odd) { background-color: transparent; }
.custom-fields.option-set-attributes > div.field:not(.built-in) { display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 6px; box-sizing: border-box; }
.custom-fields.option-set-attributes > div.field:not(.built-in) > .new-composer.composer-textbox:first-child { width: 34%; min-width: 180px; flex-shrink: 0; }
.custom-fields.option-set-attributes > div.field:not(.built-in) > .new-composer.composer-textbox:first-child input { width: 100%; box-sizing: border-box; }
.key-hint { text-decoration: underline; }
`);
function findTargetableCreateNewButton() { // Renamed for clarity
const buttons = document.querySelectorAll(
`${CREATE_NEW_BUTTON_SELECTOR}[data-key-hint-target="true"]` // Look for buttons marked as targets
);
for (const btn of buttons) {
if (btn.offsetParent !== null &&
getComputedStyle(btn).display !== 'none' &&
getComputedStyle(btn).visibility !== 'hidden' &&
!btn.closest('.bottom-stripe, .bottom-popup-row, .popup-content .children')) {
logDebug("Found targetable 'Create New...' button:", btn, btn.textContent);
return btn;
}
}
logDebug("No targetable 'Create New...' button found.");
return null;
}
function styleCreateNewButton(button, keyChar = 'N') {
if (!button || button.dataset.keyHintAdded === 'true') return; // Already processed
button.dataset.keyHintAdded = 'true'; // Mark as processed to avoid re-evaluating innerHTML
const originalText = button.textContent || button.innerText || "";
let newHtml = originalText;
let hintAppliedSuccessfully = false;
// We only want to target buttons that are clearly for "creating something NEW"
// Check if the text contains "new" or "create a new"
const isTargetContext = /new|create a new/i.test(originalText);
if (isTargetContext) {
// Try to find the keyChar (case-insensitive) within "New" or "Create" context
const contextPattern = new RegExp(`(new|create)([^${keyChar}]*?)(${keyChar})`, 'i');
let match = originalText.match(contextPattern);
if (match && match[3]) {
const charIndex = originalText.toLowerCase().indexOf(match[3].toLowerCase(), match.index + match[1].length + (match[2] ? match[2].length : 0) );
if (charIndex > -1) {
newHtml = originalText.substring(0, charIndex) +
`<span class="key-hint">${originalText[charIndex]}</span>` +
originalText.substring(charIndex + 1);
hintAppliedSuccessfully = true;
}
} else { // Fallback: if "new" or "create a new" is present, but the specific pattern above didn't match 'N' within it,
// try to find the first 'N' in the whole string if the context is right.
const firstNIndex = originalText.toUpperCase().indexOf("N");
if (firstNIndex > -1) {
newHtml = originalText.substring(0, firstNIndex) +
`<span class="key-hint">${originalText[firstNIndex]}</span>` +
originalText.substring(firstNIndex + 1);
hintAppliedSuccessfully = true;
}
}
if (hintAppliedSuccessfully) {
button.innerHTML = newHtml;
button.dataset.keyHintTarget = 'true'; // Mark as a valid target for Ctrl+N
logDebug("Added key hint and marked as target:", button.textContent);
} else {
// Text contains "new" or "create a new", but 'N' wasn't found to underline.
// Do not mark as keyHintTarget. It's a .btn.add-new-type but not one we can visually hint for 'N'.
logDebug("Button has target context but 'N' not found for hinting:", button.textContent);
}
} else {
// Button has class .btn.add-new-type, but text doesn't match "new" or "create a new"
// So, we don't consider it a target for this specific Ctrl+N functionality.
logDebug("Button class matches, but text context not for 'Create a new...':", button.textContent);
// No dataset.keyHintTarget = 'true' is set.
}
}
function handleGlobalKeyDown(event) {
const activeElement = document.activeElement;
const isInputFocused = activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable);
if (event.ctrlKey && event.key.toLowerCase() === 'n') {
if (isInputFocused) {
logDebug("Ctrl+N: Input focused, allowing browser default.");
return;
}
const anyModalOpen = document.querySelector(`${MODAL_CONTENT_SELECTOR}:not([style*="display: none"]):not([style*="visibility: hidden"]), .overlay:not([style*="display: none"]):not([style*="visibility: hidden"])`);
if (anyModalOpen && anyModalOpen.offsetParent !== null) {
logDebug("Ctrl+N: A modal is already open, ignoring.", anyModalOpen);
return;
}
const buttonToClick = findTargetableCreateNewButton(); // Now looks for hinted targets
if (buttonToClick) {
logInfo("Ctrl+N: Clicking hinted 'Create New...' button:", buttonToClick.textContent);
buttonToClick.click();
event.preventDefault();
event.stopPropagation();
} else {
logDebug("Ctrl+N: No suitable hinted 'Create New...' button found.");
}
return;
}
if (event.key !== 'Enter' && event.key !== 'Escape') return;
// ... (rest of modal Enter/Escape logic remains unchanged) ...
const activeModals = Array.from(document.querySelectorAll(MODAL_CONTENT_SELECTOR))
.filter(el => el.offsetParent !== null && getComputedStyle(el).display !== 'none' && getComputedStyle(el).visibility !== 'hidden');
if (activeModals.length === 0) return;
const currentModal = activeModals[activeModals.length - 1];
if (event.key === 'Enter' && activeElement &&
(activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable) &&
currentModal.contains(activeElement) &&
!activeElement.closest('.bottom-stripe, .bottom-popup-row')) {
const associatedDropdown = activeElement.closest('.composer-dropdown');
const isAssociatedDropdownOpen = associatedDropdown && associatedDropdown.querySelector('.dropdown-container.opened');
if (!isAssociatedDropdownOpen) {
logDebug("Modal Enter in input/textarea (not in footer, its dropdown not open). Allowing default.");
return;
}
}
if (event.key === 'Escape') {
const openDropdownInModal = currentModal.querySelector(DROPDOWN_CONTAINER_ACTUAL_SELECTOR + '.opened');
if (openDropdownInModal &&
(openDropdownInModal.contains(activeElement) || activeElement?.closest(DROPDOWN_CONTAINER_ACTUAL_SELECTOR + '.opened') === openDropdownInModal)) {
logDebug("Modal Esc: Active element is within an open dropdown. Allowing local/nav handler.");
return;
}
}
const buttonSearchScope = currentModal;
let buttonToClickModal = null;
let buttonSelectorModal = '';
const modalTitle = currentModal.querySelector('.popup-title')?.textContent.trim() || 'Unknown Modal';
if (event.key === 'Enter') {
const selectors = [
'.btn.btn-create.bubble-ui:not(.disabled)', '.btn.btn-primary:not(.disabled)', '.btn.btn-create:not(.disabled)',
'.bubble-button.primary:not(.disabled)', '.bubble-element.Button[class*="primary"]:not(.disabled)'
];
for (const sel of selectors) {
buttonToClickModal = buttonSearchScope.querySelector(sel);
if (buttonToClickModal && buttonToClickModal.offsetParent !== null && getComputedStyle(buttonToClickModal).display !== 'none' && getComputedStyle(buttonToClickModal).visibility !== 'hidden') {
buttonSelectorModal = sel; break;
}
buttonToClickModal = null;
}
if (!buttonToClickModal) {
const allButtons = buttonSearchScope.querySelectorAll('.btn:not(.disabled), .bubble-element.Button:not(.disabled)');
buttonToClickModal = Array.from(allButtons).find(btn =>
!btn.classList.contains('btn-cancel') && !(btn.classList.contains('cancel')) &&
!(btn.textContent || '').toLowerCase().includes('cancel') &&
btn.offsetParent !== null && getComputedStyle(btn).display !== 'none' && getComputedStyle(btn).visibility !== 'hidden'
);
if (buttonToClickModal) buttonSelectorModal = "Fallback: first visible non-cancel button";
}
} else if (event.key === 'Escape') {
const selectors = [
'.btn.btn-cancel:not(.disabled)', '.btn[class*="cancel"]:not(.disabled)',
'.bubble-button.cancel:not(.disabled)', '.bubble-element.Button[class*="cancel"]:not(.disabled)'
];
for (const sel of selectors) {
buttonToClickModal = buttonSearchScope.querySelector(sel);
if (buttonToClickModal && buttonToClickModal.offsetParent !== null && getComputedStyle(buttonToClickModal).display !== 'none' && getComputedStyle(buttonToClickModal).visibility !== 'hidden') {
buttonSelectorModal = sel; break;
}
buttonToClickModal = null;
}
if (!buttonToClickModal) {
const allButtons = buttonSearchScope.querySelectorAll('.btn:not(.disabled), .bubble-element.Button:not(.disabled)');
buttonToClickModal = Array.from(allButtons).find(btn =>
((btn.textContent || '').toLowerCase().includes('cancel') || btn.classList.contains('cancel')) &&
btn.offsetParent !== null && getComputedStyle(btn).display !== 'none' && getComputedStyle(btn).visibility !== 'hidden'
);
if (buttonToClickModal) buttonSelectorModal = "Fallback: first visible button with 'cancel' in text or class";
}
}
if (buttonToClickModal) {
logDebug(`Modal Click: '${buttonToClickModal.textContent.trim()}' (sel: ${buttonSelectorModal}) in modal ("${modalTitle}") via ${event.key}.`);
buttonToClickModal.click();
event.preventDefault();
event.stopPropagation();
} else {
if (DEBUG_MODE) {
logDebug(`Modal Click: No suitable button for ${event.key} in modal ("${modalTitle}").`);
}
}
}
document.addEventListener('keydown', handleGlobalKeyDown, true);
function dispatchMouseEvents(element) { /* ... (no change) ... */
if (!element) { logError("dispatchMouseEvents: element is null"); return false; }
logDebug("dispatchMouseEvents on:", element);
try {
const downEvent = new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window });
const upEvent = new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window });
const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window });
element.dispatchEvent(downEvent);
element.dispatchEvent(upEvent);
element.dispatchEvent(clickEvent);
return true;
} catch (e) { logError("dispatchMouseEvents Error:", e); return false; }
}
function standardClick(element) { /* ... (no change) ... */
if (!element) { logError("standardClick: element is null"); return false; }
logDebug("standardClick on:", element);
try { element.click(); return true;
} catch (e) { logError("standardClick Error:", e); return false; }
}
function isDropdownOpen(dropdownTriggerElement) { /* ... (no change) ... */
if (!dropdownTriggerElement) return false;
const actualDropdownContainer = dropdownTriggerElement.querySelector(DROPDOWN_CONTAINER_ACTUAL_SELECTOR);
return actualDropdownContainer ? actualDropdownContainer.classList.contains('opened') : false;
}
function handleGenericFieldAttributeModal(modalElement) { /* ... (no change) ... */
const nameInput = modalElement.querySelector(NAME_INPUT_SELECTOR_IN_MODAL);
const typeDropdown = modalElement.querySelector(TYPE_DROPDOWN_SELECTOR_IN_MODAL);
const listCheckbox = modalElement.querySelector(CHECKBOX_SELECTOR_IN_MODAL);
if (nameInput) { if (document.activeElement !== nameInput && !nameInput.contains(document.activeElement)) { nameInput.focus(); } nameInput.removeEventListener('keydown', handleModalNavKeyPress); nameInput.addEventListener('keydown', handleModalNavKeyPress); }
if (typeDropdown) { typeDropdown.removeEventListener('keydown', handleModalNavKeyPress); typeDropdown.addEventListener('keydown', handleModalNavKeyPress); }
if (listCheckbox) { listCheckbox.removeEventListener('keydown', handleModalNavKeyPress); listCheckbox.addEventListener('keydown', handleModalNavKeyPress); }
}
function handleModalNavKeyPress(event) { /* ... (no change, ensure Esc propagation stop for dropdowns) ... */
const { key, target, shiftKey } = event;
const modalElement = target.closest(MODAL_CONTENT_SELECTOR);
if (!modalElement) return;
const nameInput = modalElement.querySelector(NAME_INPUT_SELECTOR_IN_MODAL);
const typeDropdown = modalElement.querySelector(TYPE_DROPDOWN_SELECTOR_IN_MODAL);
const listCheckbox = modalElement.querySelector(CHECKBOX_SELECTOR_IN_MODAL);
if (!(nameInput && typeDropdown && listCheckbox)) {
return;
}
if (key === 'Tab') {
if (target === nameInput && !shiftKey) {
logDebug("Nav Tab from Name Input"); event.preventDefault();
if (typeDropdown) {
typeDropdown.focus(); logDebug("Nav Focused Type Dropdown.");
if (!isDropdownOpen(typeDropdown)) {
logDebug("Nav Attempting to open dropdown..."); standardClick(typeDropdown);
const captionContainer = typeDropdown.querySelector(DROPDOWN_CAPTION_SELECTOR);
if (captionContainer) { standardClick(captionContainer); dispatchMouseEvents(captionContainer); logDebug("Nav Dropdown open sequence complete on caption.");
} else { logError("Nav Caption container not found. Fallback: dispatch to main trigger."); dispatchMouseEvents(typeDropdown); }
}
} else if (listCheckbox) { listCheckbox.focus(); }
} else if (target === typeDropdown && !shiftKey) {
logDebug("Nav Tab from Type Dropdown"); event.preventDefault();
if (isDropdownOpen(typeDropdown)) {
logDebug("Nav Attempting to close dropdown...");
const captionContainer = typeDropdown.querySelector(DROPDOWN_CAPTION_SELECTOR);
if (captionContainer) dispatchMouseEvents(captionContainer); else dispatchMouseEvents(typeDropdown);
logDebug("Nav Dropdown close attempt complete.");
}
if (listCheckbox) { listCheckbox.focus(); logDebug("Nav Focused List Checkbox."); }
} else if (target === listCheckbox && !shiftKey) { logDebug("Nav Tab from list checkbox, allowing default.");
} else if (target === listCheckbox && shiftKey) {
logDebug("Nav Shift+Tab from list checkbox."); event.preventDefault();
if (typeDropdown) typeDropdown.focus(); else if (nameInput) nameInput.focus();
} else if (target === typeDropdown && shiftKey) {
logDebug("Nav Shift+Tab from type dropdown."); event.preventDefault();
if (isDropdownOpen(typeDropdown)) {
const captionContainer = typeDropdown.querySelector(DROPDOWN_CAPTION_SELECTOR);
if (captionContainer) dispatchMouseEvents(captionContainer); else dispatchMouseEvents(typeDropdown);
}
if (nameInput) nameInput.focus();
}
}
else if (key === ' ' && target === listCheckbox) {
logDebug("Nav Space on List Checkbox."); event.preventDefault(); standardClick(target); logDebug("Nav Clicked Checkbox via Space.");
}
else if (key === 'Escape' && target.closest(TYPE_DROPDOWN_SELECTOR_IN_MODAL)) {
if (typeDropdown && isDropdownOpen(typeDropdown)) {
logDebug("Nav Escape on/in open dropdown: attempting to close.");
const captionContainer = typeDropdown.querySelector(DROPDOWN_CAPTION_SELECTOR);
if (captionContainer) dispatchMouseEvents(captionContainer); else dispatchMouseEvents(typeDropdown);
typeDropdown.focus();
event.preventDefault();
event.stopPropagation(); // Crucial
logDebug("Nav Closed dropdown via Escape, refocused trigger. Propagation stopped.");
}
}
}
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
// Modal processing (no change from previous versions)
if (mutation.type === 'childList' || (mutation.type === 'attributes' && (mutation.attributeName === 'style' || mutation.attributeName === 'class'))) {
const addedNodesForModals = mutation.type === 'childList' ? Array.from(mutation.addedNodes) : (mutation.target.nodeType === 1 ? [mutation.target] : []);
for (const node of addedNodesForModals) { /* ... modal processing as before ... */
if (node.nodeType !== Node.ELEMENT_NODE) continue;
const modals = [];
if (node.matches && node.matches(MODAL_CONTENT_SELECTOR)) modals.push(node);
else if (node.querySelectorAll) modals.push(...node.querySelectorAll(MODAL_CONTENT_SELECTOR));
for (const modal of modals) {
const isGenericFieldModal = modal.querySelector(NAME_INPUT_SELECTOR_IN_MODAL) && modal.querySelector(TYPE_DROPDOWN_SELECTOR_IN_MODAL);
if (isGenericFieldModal) {
if (modal.offsetParent !== null && getComputedStyle(modal).display !== 'none' && getComputedStyle(modal).visibility !== 'hidden') {
if (!modal.dataset.tampermonkeyFieldModalProcessed) {
if(DEBUG_MODE) { /* ... */ }
modal.dataset.tampermonkeyFieldModalProcessed = 'true';
handleGenericFieldAttributeModal(modal);
}
} else {
if (modal.dataset.tampermonkeyFieldModalProcessed) {
if(DEBUG_MODE) { /* ... */ }
delete modal.dataset.tampermonkeyFieldModalProcessed;
modal.querySelector(NAME_INPUT_SELECTOR_IN_MODAL)?.removeEventListener('keydown', handleModalNavKeyPress);
modal.querySelector(TYPE_DROPDOWN_SELECTOR_IN_MODAL)?.removeEventListener('keydown', handleModalNavKeyPress);
modal.querySelector(CHECKBOX_SELECTOR_IN_MODAL)?.removeEventListener('keydown', handleModalNavKeyPress);
}
}
}
}
}
}
// Create New Button Styling
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches && node.matches(CREATE_NEW_BUTTON_SELECTOR)) {
styleCreateNewButton(node);
}
node.querySelectorAll(CREATE_NEW_BUTTON_SELECTOR).forEach(styleCreateNewButton);
}
});
} else if (mutation.type === 'attributes' && (mutation.attributeName === 'style' || mutation.attributeName === 'class')) {
if (mutation.target.nodeType === Node.ELEMENT_NODE && mutation.target.matches(CREATE_NEW_BUTTON_SELECTOR)) {
// Re-evaluate styling if style/class changes might affect visibility or if it wasn't hinted yet
if (mutation.target.offsetParent !== null && getComputedStyle(mutation.target).display !== 'none') {
// Only call if not already hinted, or if you want to re-evaluate context (though current logic prevents re-innerHTML)
if (mutation.target.dataset.keyHintAdded !== 'true') {
styleCreateNewButton(mutation.target);
}
}
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
function initialButtonScan() {
logDebug("Performing initial scan for '.btn.add-new-type' buttons to style.");
document.querySelectorAll(CREATE_NEW_BUTTON_SELECTOR).forEach(btn => {
if (btn.offsetParent !== null && getComputedStyle(btn).display !== 'none') {
styleCreateNewButton(btn);
}
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialButtonScan);
} else {
initialButtonScan();
}
logInfo(`Bubble.io enhancements script loaded. (v${GM_info.script.version}) - DEBUG_MODE is ${DEBUG_MODE ? 'ON' : 'OFF'}. Canvas padding is ${isCanvasPaddingEnabled ? 'ON' : 'OFF'}.`);
})();