Skip to content

Commit d3d001c

Browse files
New Mobile-Aware Positioning: Centers the tooltip on mobile.
- New Mobile-Aware Positioning: Centers the tooltip on mobile and * clamps it to the screen edges for full visibility.
1 parent 01711da commit d3d001c

1 file changed

Lines changed: 59 additions & 165 deletions

File tree

src/toaster.js

Lines changed: 59 additions & 165 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,17 @@
11
/**
2-
* Universal Toaster (v2.0 - Stability Fixes)
3-
* ------------------------------------------------------------------
4-
* Fixed: Tooltips sticking when scrolling, clicking, or changing tabs.
5-
* ------------------------------------------------------------------
6-
*/
7-
8-
/**
9-
* Universal Toaster (v3.0 - Rich Text & Styles)
2+
* Universal Toaster (v3.1 - Mobile Viewport Fix)
103
* ------------------------------------------------------------------
4+
* Fixes: Tooltips getting cut off on small mobile screens.
115
* Features:
12-
* - HTML Safe (Prevents XSS while allowing custom styling)
13-
* - Custom Shortcodes: Color, Bg, Font, Weight
14-
* - Dynamic Google Fonts Loader
15-
* - Character Truncation Logic
6+
* - New Mobile-Aware Positioning: Centers the tooltip on mobile and
7+
* clamps it to the screen edges for full visibility.
168
* ------------------------------------------------------------------
179
*/
1810

1911
(function () {
12+
// --- 1. SETUP (No changes here) ---
2013
const userConfig = window.UniversalToasterConfig || {};
2114

22-
// --- 1. SETUP STYLES ---
2315
const settings = {
2416
bg: userConfig.backgroundColor || null,
2517
text: userConfig.textColor || null,
@@ -36,27 +28,22 @@
3628
z-index: 2147483647;
3729
pointer-events: none;
3830
opacity: 0;
39-
transition: opacity 0.1s ease-out;
31+
transition: opacity 0.1s ease-out, transform 0.1s ease-out; /* Added transform for smoother mobile positioning */
4032
visibility: hidden;
41-
42-
/* Visuals */
4333
border-radius: ${settings.radius};
4434
font-size: ${settings.size};
4535
font-family: ${settings.font};
4636
padding: ${settings.padding};
4737
box-shadow: ${settings.shadow};
4838
border: 1px solid rgba(255,255,255,0.1);
49-
50-
/* Layout */
51-
white-space: pre-wrap; /* Changed to allow flexibility with formatting */
52-
max-width: 90vw;
39+
white-space: pre-wrap;
40+
max-width: 95vw;
5341
line-height: 1.4;
5442
}
5543
.universal-toaster-popup.visible {
5644
opacity: 1;
5745
visibility: visible;
5846
}
59-
/* Inner spans for custom styling */
6047
.universal-toaster-popup span {
6148
display: inline-block;
6249
}
@@ -70,178 +57,85 @@
7057
tooltip.className = 'universal-toaster-popup';
7158
document.body.appendChild(tooltip);
7259

73-
// --- 2. HELPERS & PARSERS ---
74-
60+
// --- 2. HELPERS & PARSERS (No changes here) ---
7561
let activeElement = null;
76-
const loadedFonts = new Set(); // Track loaded fonts to avoid duplicates
77-
78-
// A. Sanitize HTML (Security First)
79-
function escapeHtml(text) {
80-
if (!text) return "";
81-
return text
82-
.replace(/&/g, "&")
83-
.replace(/</g, "&lt;")
84-
.replace(/>/g, "&gt;")
85-
.replace(/"/g, "&quot;")
86-
.replace(/'/g, "&#039;");
87-
}
88-
89-
// B. Google Fonts Loader
90-
function loadGoogleFont(fontName) {
91-
if (!fontName || loadedFonts.has(fontName)) return;
92-
93-
const link = document.createElement('link');
94-
link.href = `https://fonts.googleapis.com/css?family=${fontName.replace(/\s+/g, '+')}&display=swap`;
95-
link.rel = 'stylesheet';
96-
document.head.appendChild(link);
97-
98-
loadedFonts.add(fontName);
99-
}
100-
101-
// C. The Magic Parser (Converts &cmd=val; into HTML)
102-
function parseCustomSyntax(rawText) {
103-
// 1. First, Escape HTML to prevent script injection
104-
// note: We temporarily unescape ampersands used in OUR commands afterward
105-
let text = escapeHtml(rawText);
106-
107-
// 2. Decode our specific command ampersands so regex works
108-
// (Because escapeHtml turns '&cl=' into '&amp;cl=')
109-
text = text.replace(/&amp;(fz|cl|bgcl|fw|fn|chr)=/g, "&$1=");
110-
text = text.replace(/&amp;(fz|cl|bgcl|fw|fn|chr);/g, "&$1;");
111-
112-
// 3. Process Character Limits (&chr=10; text &chr;)
113-
text = text.replace(/&chr=(\d+);(.*?)&chr;/g, (match, limit, content) => {
114-
if (content.length > parseInt(limit)) {
115-
return content.substring(0, parseInt(limit)) + '...';
116-
}
117-
return content;
118-
});
119-
120-
// 4. Process Google Fonts (&fn=Roboto; text &fn;)
121-
text = text.replace(/&fn=(.*?);/g, (match, fontName) => {
122-
loadGoogleFont(fontName);
123-
return `<span style="font-family:'${fontName}', sans-serif">`;
124-
});
125-
text = text.replace(/&fn;/g, '</span>');
126-
127-
// 5. Process Colors & Weights
128-
// Replaces &tag=value; with <span style="...">
129-
const replacers = [
130-
{ tag: 'fz', css: 'font-size' },
131-
{ tag: 'cl', css: 'color' },
132-
{ tag: 'bgcl', css: 'background-color' },
133-
{ tag: 'fw', css: 'font-weight' }
134-
];
135-
136-
replacers.forEach(item => {
137-
// Replace Opener: &cl=red; -> <span style="color:red">
138-
const openerRegex = new RegExp(`&${item.tag}=(.*?);`, 'g');
139-
text = text.replace(openerRegex, `<span style="${item.css}:$1">`);
140-
141-
// Replace Closer: &cl; -> </span>
142-
const closerRegex = new RegExp(`&${item.tag};`, 'g');
143-
text = text.replace(closerRegex, '</span>');
144-
});
145-
146-
return text;
147-
}
148-
149-
// D. Auto Contrast Theme
150-
function applyTheme() {
151-
// User overrides
152-
if (settings.bg && settings.text) {
153-
tooltip.style.backgroundColor = settings.bg;
154-
tooltip.style.color = settings.text;
155-
return;
156-
}
62+
const loadedFonts = new Set();
15763

158-
// Auto-detect
159-
let computedStyle = window.getComputedStyle(document.body);
160-
let bgColor = computedStyle.backgroundColor;
161-
if (bgColor === 'rgba(0, 0, 0, 0)' || bgColor === 'transparent') bgColor = 'rgb(255, 255, 255)';
162-
163-
const rgb = bgColor.match(/\d+/g);
164-
let isLightPage = true;
165-
if (rgb) {
166-
const brightness = Math.round(((parseInt(rgb[0]) * 299) + (parseInt(rgb[1]) * 587) + (parseInt(rgb[2]) * 114)) / 1000);
167-
if (brightness < 125) isLightPage = false;
168-
}
169-
170-
if (isLightPage) {
171-
tooltip.style.backgroundColor = '#222222';
172-
tooltip.style.color = '#ffffff';
173-
} else {
174-
tooltip.style.backgroundColor = '#ffffff';
175-
tooltip.style.color = '#000000';
176-
}
177-
}
64+
function escapeHtml(text) { if (!text) return ""; return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;"); }
65+
function loadGoogleFont(fontName) { if (!fontName || loadedFonts.has(fontName)) return; const link = document.createElement('link'); link.href = `https://fonts.googleapis.com/css?family=${fontName.replace(/\s+/g, '+')}&display=swap`; link.rel = 'stylesheet'; document.head.appendChild(link); loadedFonts.add(fontName); }
66+
function parseCustomSyntax(rawText) { let text = escapeHtml(rawText); text = text.replace(/&amp;(cl|bgcl|fw|fn|chr)=/g, "&$1=").replace(/&amp;(cl|bgcl|fw|fn|chr);/g, "&$1;"); text = text.replace(/&chr=(\d+);(.*?)&chr;/g, (m, l, c) => (c.length > parseInt(l) ? c.substring(0, parseInt(l)) + '...' : c)); text = text.replace(/&fn=(.*?);/g, (m, f) => { loadGoogleFont(f); return `<span style="font-family:'${f}', sans-serif">`; }); text = text.replace(/&fn;/g, '</span>');[{ tag: 'cl', css: 'color' }, { tag: 'bgcl', css: 'background-color' }, { tag: 'fw', css: 'font-weight' }].forEach(i => { text = text.replace(new RegExp(`&${i.tag}=(.*?);`, 'g'), `<span style="${i.css}:$1">`).replace(new RegExp(`&${i.tag};`, 'g'), '</span>'); }); return text; }
67+
function applyTheme() { if (settings.bg && settings.text) { tooltip.style.backgroundColor = settings.bg; tooltip.style.color = settings.text; return; } let s = window.getComputedStyle(document.body); let c = s.backgroundColor; if (c === 'rgba(0, 0, 0, 0)' || c === 'transparent') c = 'rgb(255, 255, 255)'; const r = c.match(/\d+/g); let i = true; if (r) { const b = Math.round(((parseInt(r[0]) * 299) + (parseInt(r[1]) * 587) + (parseInt(r[2]) * 114)) / 1000); if (b < 125) i = false; } if (i) { tooltip.style.backgroundColor = '#222222'; tooltip.style.color = '#ffffff'; } else { tooltip.style.backgroundColor = '#ffffff'; tooltip.style.color = '#000000'; } }
17868

17969
// --- 3. EVENT LISTENERS ---
180-
181-
const hideTooltip = () => {
182-
if (activeElement) {
183-
tooltip.classList.remove('visible');
184-
activeElement = null;
185-
}
186-
};
70+
const hideTooltip = () => { if (activeElement) { tooltip.classList.remove('visible'); activeElement = null; } };
18771

18872
document.addEventListener('mouseover', (e) => {
18973
const target = e.target.closest('[title], [data-toaster-title]');
190-
19174
if (target) {
192-
if (target.hasAttribute('title')) {
193-
const raw = target.getAttribute('title');
194-
if (raw && raw.trim()) {
195-
target.setAttribute('data-toaster-title', raw);
196-
target.removeAttribute('title');
197-
}
198-
}
199-
75+
if (target.hasAttribute('title')) { const raw = target.getAttribute('title'); if (raw && raw.trim()) { target.setAttribute('data-toaster-title', raw); target.removeAttribute('title'); } }
20076
const rawText = target.getAttribute('data-toaster-title');
201-
if (rawText) {
202-
activeElement = target;
203-
204-
// PARSE THE TEXT HERE
205-
tooltip.innerHTML = parseCustomSyntax(rawText);
206-
207-
applyTheme();
208-
tooltip.classList.add('visible');
209-
}
77+
if (rawText) { activeElement = target; tooltip.innerHTML = parseCustomSyntax(rawText); applyTheme(); tooltip.classList.add('visible'); }
21078
}
21179
});
21280

81+
// ###############################################################
82+
// ### POSITIONING LOGIC - UPDATED FOR MOBILE AWARENESS ###
83+
// ###############################################################
21384
document.addEventListener('mousemove', (e) => {
214-
if (!activeElement || !activeElement.isConnected) {
215-
hideTooltip();
216-
return;
217-
}
85+
if (!activeElement || !activeElement.isConnected) { hideTooltip(); return; }
21886
if (!tooltip.classList.contains('visible')) return;
21987

22088
const rect = tooltip.getBoundingClientRect();
22189
const winW = window.innerWidth;
22290
const winH = window.innerHeight;
223-
const offset = 15;
91+
const offset = 15; // Vertical offset on mobile, cursor offset on desktop
92+
const mobileBreakpoint = 768; // The screen width to switch to mobile logic
93+
const mobileEdgePadding = 10; // Space from the screen edge on mobile
94+
95+
let x, y;
22496

225-
let x = e.clientX + offset;
226-
let y = e.clientY + offset;
97+
// --- Vertical Positioning (same for both) ---
98+
// Position below the cursor/finger, but flip above if it overflows the bottom
99+
y = e.clientY + offset;
100+
if (y + rect.height > winH) {
101+
y = e.clientY - rect.height - offset;
102+
}
227103

228-
if (x + rect.width > winW) x = e.clientX - rect.width - offset;
229-
if (y + rect.height > winH) y = e.clientY - rect.height - offset;
104+
// --- Horizontal Positioning (DIFFERENT for mobile vs desktop) ---
105+
if (winW <= mobileBreakpoint) {
106+
// MOBILE LOGIC: Center it, then clamp it to screen edges.
107+
x = e.clientX - (rect.width / 2); // Center under the finger
230108

231-
tooltip.style.left = `${x}px`;
232-
tooltip.style.top = `${y}px`;
109+
// Clamp to left edge
110+
if (x < mobileEdgePadding) {
111+
x = mobileEdgePadding;
112+
}
113+
// Clamp to right edge
114+
if (x + rect.width > winW - mobileEdgePadding) {
115+
x = winW - rect.width - mobileEdgePadding;
116+
}
117+
118+
} else {
119+
// DESKTOP LOGIC: Position to the right of cursor, but flip left if it overflows.
120+
x = e.clientX + offset;
121+
if (x + rect.width > winW) {
122+
x = e.clientX - rect.width - offset;
123+
}
124+
}
125+
126+
tooltip.style.transform = `translate(${x}px, ${y}px)`; // Use transform for better performance
127+
// Clear old left/top styles if they exist
128+
tooltip.style.left = '0';
129+
tooltip.style.top = '0';
233130
});
234131

235132
document.addEventListener('mouseout', (e) => {
236133
const target = e.target.closest('[data-toaster-title]');
237-
if (target && target === activeElement) {
238-
hideTooltip();
239-
}
134+
if (target && target === activeElement) { hideTooltip(); }
240135
});
241136

242-
// Safety triggers
137+
// Safety Triggers (no changes)
243138
window.addEventListener('mousedown', hideTooltip);
244139
window.addEventListener('scroll', hideTooltip, true);
245140
window.addEventListener('blur', hideTooltip);
246-
247141
})();

0 commit comments

Comments
 (0)