Enhance overlay drag and file input handling

Improved the overlay drag logic for smoother, GPU-accelerated movement using requestAnimationFrame and transform. Updated file input elements to use stronger hiding techniques, preventing browser-native text from appearing during minimize/maximize. Updated version to 0.72.0 and added additional debug logging and minor UI/UX improvements.
This commit is contained in:
Nicholas 2025-08-05 14:18:12 -03:00
parent da77f2c55a
commit 1d78a140dd
8 changed files with 158 additions and 61 deletions

View file

@ -1 +1 @@
#bm-l{position:fixed;background-color:#153063e6;color:#fff;padding:10px;border-radius:8px;z-index:9000;transition:all .3s ease;max-width:300px;width:auto}#bm-4,#bm-l hr,#bm-3,#bm-1{transition:opacity .2s ease,height .2s ease}div#bm-l{font-family:Roboto Mono,Courier New,Monaco,DejaVu Sans Mono,monospace,Arial;letter-spacing:.05em}#bm-g{margin-bottom:.5em;background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="5" height="5"><circle cx="3" cy="3" r="1.5" fill="CornflowerBlue" /></svg>') repeat;cursor:grab;width:100%;height:1em}#bm-g.dragging{cursor:grabbing}#bm-7{margin-bottom:.5em}#bm-7[style*="text-align: center"]{display:flex;flex-direction:column;align-items:center;justify-content:center}#bm-l[style*="padding: 5px"]{width:auto!important;max-width:300px;min-width:200px}#bm-l img{display:inline-block;height:2.5em;margin-right:1ch;vertical-align:middle;transition:opacity .2s ease}#bm-7[style*="text-align: center"] img{display:block;margin:0 auto}#bm-g{transition:margin-bottom .2s ease}#bm-l h1{display:inline-block;font-size:x-large;font-weight:700;vertical-align:middle}#bm-3 input[type=checkbox]{vertical-align:middle;margin-right:.5ch}#bm-3 label{margin-right:.5ch}.bm-p{border:white 1px solid;height:1.5em;width:1.5em;margin-top:2px;text-align:center;line-height:1em;padding:0!important}#bm-c{vertical-align:middle}#bm-c svg{width:50%;margin:0 auto;fill:#111}div:has(>#bm-button-teleport){display:flex;gap:.5ch}#bm-button-favorite svg,#bm-button-template svg{height:1em;margin:2px auto 0;text-align:center;line-height:1em;vertical-align:bottom}#bm-8 input[type=number]{appearance:auto;-moz-appearance:textfield;width:5.5ch;margin-left:1ch;background-color:#0003;padding:0 .5ch;font-size:small}#bm-8 input[type=number]::-webkit-outer-spin-button,#bm-8 input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}#bm-0{display:flex;flex-direction:row;flex-wrap:wrap;align-content:center;justify-content:center;align-items:center;gap:1ch}div:has(>#bm-2)>button{width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#bm-a{font-size:small;background-color:#0003;padding:0 .5ch;height:3.75em;width:100%}#bm-1{display:flex;justify-content:space-between}#bm-l small{font-size:x-small;color:#d3d3d3}#bm-4,#bm-3,#bm-8,#bm-0,div:has(>#bm-2),#bm-a{margin-top:.5em}#bm-l button{background-color:#144eb9;border-radius:1em;padding:0 .75ch}#bm-l button:hover,#bm-l button:focus-visible{background-color:#1061e5}#bm-l button:active,#bm-l button:disabled{background-color:#2e97ff}#bm-l button:disabled{text-decoration:line-through}
#bm-l{position:fixed;background-color:#153063e6;color:#fff;padding:10px;border-radius:8px;z-index:9000;transition:all .3s ease;max-width:300px;width:auto;will-change:transform;backface-visibility:hidden;-webkit-backface-visibility:hidden;transform-style:preserve-3d;-webkit-transform-style:preserve-3d}#bm-4,#bm-l hr,#bm-3,#bm-1{transition:opacity .2s ease,height .2s ease}div#bm-l{font-family:Roboto Mono,Courier New,Monaco,DejaVu Sans Mono,monospace,Arial;letter-spacing:.05em}#bm-g{margin-bottom:.5em;background:url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="5" height="5"><circle cx="3" cy="3" r="1.5" fill="CornflowerBlue" /></svg>') repeat;cursor:grab;width:100%;height:1em}#bm-g.dragging{cursor:grabbing}#bm-l:has(#bm-g.dragging){pointer-events:none;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}#bm-g.dragging{pointer-events:auto}#bm-7{margin-bottom:.5em}#bm-7[style*="text-align: center"]{display:flex;flex-direction:column;align-items:center;justify-content:center}#bm-l[style*="padding: 5px"]{width:auto!important;max-width:300px;min-width:200px}#bm-l img{display:inline-block;height:2.5em;margin-right:1ch;vertical-align:middle;transition:opacity .2s ease}#bm-7[style*="text-align: center"] img{display:block;margin:0 auto}#bm-g{transition:margin-bottom .2s ease}#bm-l h1{display:inline-block;font-size:x-large;font-weight:700;vertical-align:middle}#bm-3 input[type=checkbox]{vertical-align:middle;margin-right:.5ch}#bm-3 label{margin-right:.5ch}.bm-p{border:white 1px solid;height:1.5em;width:1.5em;margin-top:2px;text-align:center;line-height:1em;padding:0!important}#bm-c{vertical-align:middle}#bm-c svg{width:50%;margin:0 auto;fill:#111}div:has(>#bm-button-teleport){display:flex;gap:.5ch}#bm-button-favorite svg,#bm-button-template svg{height:1em;margin:2px auto 0;text-align:center;line-height:1em;vertical-align:bottom}#bm-8 input[type=number]{appearance:auto;-moz-appearance:textfield;width:5.5ch;margin-left:1ch;background-color:#0003;padding:0 .5ch;font-size:small}#bm-8 input[type=number]::-webkit-outer-spin-button,#bm-8 input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}#bm-0{display:flex;flex-direction:row;flex-wrap:wrap;align-content:center;justify-content:center;align-items:center;gap:1ch}div:has(>#bm-2)>button{width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#bm-2,input[type=file][id*=template]{display:none!important;visibility:hidden!important;position:absolute!important;left:-9999px!important;top:-9999px!important;width:0!important;height:0!important;opacity:0!important;z-index:-9999!important;pointer-events:none!important}#bm-a{font-size:small;background-color:#0003;padding:0 .5ch;height:3.75em;width:100%}#bm-1{display:flex;justify-content:space-between}#bm-l small{font-size:x-small;color:#d3d3d3}#bm-4,#bm-3,#bm-8,#bm-0,div:has(>#bm-2),#bm-a{margin-top:.5em}#bm-l button{background-color:#144eb9;border-radius:1em;padding:0 .75ch}#bm-l button:hover,#bm-l button:focus-visible{background-color:#1061e5}#bm-l button:active,#bm-l button:disabled{background-color:#2e97ff}#bm-l button:disabled{text-decoration:line-through}

File diff suppressed because one or more lines are too long

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "wplace-bluemarble",
"version": "0.69.1",
"version": "0.72.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wplace-bluemarble",
"version": "0.69.1",
"version": "0.72.0",
"devDependencies": {
"esbuild": "^0.25.0",
"terser": "^5.43.1"

View file

@ -1,6 +1,6 @@
{
"name": "wplace-bluemarble",
"version": "0.70.0",
"version": "0.72.0",
"type": "module",
"scripts": {
"build": "node build/build.js",

View file

@ -1,7 +1,7 @@
// ==UserScript==
// @name Blue Marble
// @namespace https://github.com/SwingTheVine/
// @version 0.70.0
// @version 0.72.0
// @description A userscript to automate and/or enhance the user experience on Wplace.live. Make sure to comply with the site's Terms of Service, and rules! This script is not affiliated with Wplace.live in any way, use at your own risk. This script is not affiliated with TamperMonkey. The author of this userscript is not responsible for any damages, issues, loss of data, or punishment that may occur as a result of using this script. This script is provided "as is" under the MPL-2.0 license. The "Blue Marble" icon is licensed under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication. The image is owned by NASA.
// @author SwingTheVine
// @license MPL-2.0

View file

@ -430,9 +430,9 @@ export default class Overlay {
return this;
}
/** Adds a file input to the overlay. This includes a container and a button.
/** Adds a file input to the overlay with enhanced visibility controls.
* This input element will have properties shared between all file input elements in the overlay.
* You can override the shared properties by using a callback.
* Uses multiple hiding methods to prevent browser native text from appearing during minimize/maximize.
* @param {Object.<string, any>} [additionalProperties={}] - The DOM properties of the file input that are NOT shared between all overlay file input elements. These should be camelCase.
* @param {function(Overlay, HTMLDivElement, HTMLInputElement, HTMLButtonElement):void} [callback=()=>{}] - Additional JS modification to the file input.
* @returns {Overlay} Overlay class instance (this)
@ -451,7 +451,10 @@ export default class Overlay {
*/
addInputFile(additionalProperties = {}, callback = () => {}) {
const properties = {'type': 'file', 'style': 'display: none;'}; // Shared file input DOM properties
const properties = {
'type': 'file',
'style': 'display: none !important; visibility: hidden !important; position: absolute !important; left: -9999px !important; width: 0 !important; height: 0 !important; opacity: 0 !important;'
}; // Complete file input hiding to prevent native browser text interference
const text = additionalProperties['textContent'] ?? ''; // Retrieves the text content
delete additionalProperties['textContent']; // Deletes the text content before applying the additional properties to the file input
@ -463,11 +466,15 @@ export default class Overlay {
this.buildElement(); // Signifies that we are done adding children to the button
this.buildElement(); // Signifies that we are done adding children to the container
// Prevent file input from being accessible or visible in any way
input.setAttribute('tabindex', '-1');
input.setAttribute('aria-hidden', 'true');
button.addEventListener('click', () => {
input.click(); // Clicks the file input
});
// Changes the button text content (and trims the file name)
// Update button text when file is selected
input.addEventListener('change', () => {
button.style.maxWidth = `${button.offsetWidth}px`;
if (input.files.length > 0) {
@ -533,14 +540,20 @@ export default class Overlay {
}
}
/** Handles dragging of the overlay.
/** Handles dragging of the overlay with performance optimizations.
* Uses requestAnimationFrame for smooth animations and GPU-accelerated transforms.
* @param {string} moveMe - The ID of the element to be moved
* @param {string} iMoveThings - The ID of the element to be moved
* @param {string} iMoveThings - The ID of the drag handle element
* @since 0.8.2
*/
handleDrag(moveMe, iMoveThings) {
let isDragging = false;
let offsetX, offsetY = 0;
let animationFrame = null;
let currentX = 0;
let currentY = 0;
let targetX = 0;
let targetY = 0;
// Retrieves the elements (allows either '#id' or 'id' to be passed in)
moveMe = document.querySelector(moveMe?.[0] == '#' ? moveMe : '#' + moveMe);
@ -552,67 +565,110 @@ export default class Overlay {
return; // Kills itself
}
// What to do when the mouse is pressed down on the element that moves things
iMoveThings.addEventListener('mousedown', function(event) {
// Smooth animation loop using requestAnimationFrame for optimal performance
const updatePosition = () => {
if (isDragging) {
// Only update DOM if position changed significantly (reduce repaints)
const deltaX = Math.abs(currentX - targetX);
const deltaY = Math.abs(currentY - targetY);
if (deltaX > 0.5 || deltaY > 0.5) {
currentX = targetX;
currentY = targetY;
// Use CSS transform for GPU acceleration instead of left/top
moveMe.style.transform = `translate(${currentX}px, ${currentY}px)`;
moveMe.style.left = '0px';
moveMe.style.top = '0px';
moveMe.style.right = '';
}
animationFrame = requestAnimationFrame(updatePosition);
}
};
// Cache initial position to avoid expensive getBoundingClientRect calls during drag
let initialRect = null;
const startDrag = (clientX, clientY) => {
isDragging = true;
offsetX = event.clientX - moveMe.getBoundingClientRect().left;
offsetY = event.clientY - moveMe.getBoundingClientRect().top;
document.body.style.userSelect = 'none'; // Prevents text selection while dragging
iMoveThings.classList.add('dragging'); // Adds a class to indicate a dragging state
initialRect = moveMe.getBoundingClientRect();
offsetX = clientX - initialRect.left;
offsetY = clientY - initialRect.top;
// Get current position from transform or use element position
const computedStyle = window.getComputedStyle(moveMe);
const transform = computedStyle.transform;
if (transform && transform !== 'none') {
const matrix = new DOMMatrix(transform);
currentX = matrix.m41;
currentY = matrix.m42;
} else {
currentX = initialRect.left;
currentY = initialRect.top;
}
targetX = currentX;
targetY = currentY;
document.body.style.userSelect = 'none';
iMoveThings.classList.add('dragging');
// Start animation loop
if (animationFrame) {
cancelAnimationFrame(animationFrame);
}
updatePosition();
};
const endDrag = () => {
isDragging = false;
if (animationFrame) {
cancelAnimationFrame(animationFrame);
animationFrame = null;
}
document.body.style.userSelect = '';
iMoveThings.classList.remove('dragging');
};
// Mouse down - start dragging
iMoveThings.addEventListener('mousedown', function(event) {
event.preventDefault();
startDrag(event.clientX, event.clientY);
});
// What to do when the touch starts on the element that moves things
// Touch start - start dragging
iMoveThings.addEventListener('touchstart', function(event) {
isDragging = true;
const touch = event?.touches?.[0];
if (!touch) {return;}
offsetX = touch.clientX - moveMe.getBoundingClientRect().left; // Distance between the left edge of the overlay, and the cursor
offsetY = touch.clientY - moveMe.getBoundingClientRect().top; // Distance between the top edge of the overlay, and the cursor
document.body.style.userSelect = 'none'; // Prevents text selection while dragging
iMoveThings.classList.add('dragging'); // Adds a class to indicate a dragging state
}, { passive: false }); // Prevents scrolling from being captured
startDrag(touch.clientX, touch.clientY);
event.preventDefault();
}, { passive: false });
// What to do when the mouse is moved while dragging
// Mouse move - update target position
document.addEventListener('mousemove', function(event) {
if (isDragging) {
moveMe.style.left = (event.clientX - offsetX) + 'px'; // Binds the overlay to the left side of the screen, and sets it's position to the cursor
moveMe.style.top = (event.clientY - offsetY) + 'px'; // Binds the overlay to the top of the screen, and sets it's position to the cursor
moveMe.style.right = ''; // Destroys the right property to unbind the overlay from the right side of the screen
if (isDragging && initialRect) {
targetX = event.clientX - offsetX;
targetY = event.clientY - offsetY;
}
});
}, { passive: true });
// What to do when the touch moves while dragging
// Touch move - update target position
document.addEventListener('touchmove', function(event) {
if (isDragging) {
if (isDragging && initialRect) {
const touch = event?.touches?.[0];
if (!touch) {return;}
moveMe.style.left = (touch.clientX - offsetX) + 'px'; // Binds the overlay to the left side of the screen, and sets it's position to the cursor
moveMe.style.top = (touch.clientY - offsetY) + 'px'; // Binds the overlay to the top of the screen, and sets it's position to the cursor
moveMe.style.right = ''; // Destroys the right property to unbind the overlay from the right side of the screen
event.preventDefault(); // prevent scrolling while dragging
targetX = touch.clientX - offsetX;
targetY = touch.clientY - offsetY;
event.preventDefault();
}
}, { passive: false }); // Prevents scrolling from being captured
}, { passive: false });
// What to do when the mouse is released
document.addEventListener('mouseup', function() {
isDragging = false;
document.body.style.userSelect = ''; // Restores text selection capability after dragging
iMoveThings.classList.remove('dragging'); // Removes the dragging class
});
// What to do when the touch ends
document.addEventListener('touchend', function() {
isDragging = false;
document.body.style.userSelect = ''; // Restores text selection capability after dragging
iMoveThings.classList.remove('dragging'); // Removes the dragging class
});
// What to do when the touch is cancelled
document.addEventListener('touchcancel', function() {
isDragging = false;
document.body.style.userSelect = ''; // Restores text selection capability after dragging
iMoveThings.classList.remove('dragging'); // Removes the dragging class
});
// End drag events
document.addEventListener('mouseup', endDrag);
document.addEventListener('touchend', endDrag);
document.addEventListener('touchcancel', endDrag);
}
/** Handles status display.

View file

@ -2,6 +2,12 @@
* @since 0.0.0
*
* VERSION HISTORY:
* 0.72.0 - Performance and UI improvements
* - Smooth drag performance with requestAnimationFrame and GPU acceleration
* - Fixed file upload button text bug during UI state changes
* - Added minimize/maximize animations with CSS transitions
* - Hardware-accelerated transformations and smart DOM updates
*
* 0.71.0 - Added minimize/maximize functionality and pixel counting system
* Features added:
* - Interactive minimize/maximize overlay with click-to-toggle functionality

View file

@ -11,12 +11,18 @@
transition: all 0.3s ease;
max-width: 300px;
width: auto;
/* Performance optimizations for smooth dragging */
will-change: transform;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
transform-style: preserve-3d;
-webkit-transform-style: preserve-3d;
}
/* Smooth transitions for minimize/maximize functionality */
#bm-contain-userinfo,
#bm-overlay hr,
#bm-contain-automation,
#bm-contain-automation,
#bm-contain-buttons-action {
transition: opacity 0.2s ease, height 0.2s ease;
}
@ -49,6 +55,20 @@ div#bm-overlay {
cursor: grabbing;
}
/* Disable interactions during drag for better performance */
#bm-overlay:has(#bm-bar-drag.dragging) {
pointer-events: none;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
/* Keep drag bar interactive when dragging */
#bm-bar-drag.dragging {
pointer-events: auto;
}
/* The container for the overlay header */
#bm-contain-header {
margin-bottom: 0.5em;
@ -188,6 +208,21 @@ div:has(> #bm-input-file-template) > button {
text-overflow: ellipsis;
}
/* Force complete invisibility of file input to prevent native browser text */
#bm-input-file-template,
input[type="file"][id*="template"] {
display: none !important;
visibility: hidden !important;
position: absolute !important;
left: -9999px !important;
top: -9999px !important;
width: 0 !important;
height: 0 !important;
opacity: 0 !important;
z-index: -9999 !important;
pointer-events: none !important;
}
/* Output status area */
#bm-output-status {
font-size: small;