Extension: icon-state feedback + overlay picker instead of inline panels
- Injected buttons now show state via the tablet icon itself — spinner while sending, check on success, warning triangle on busy/offline/error — with detail in a hover tooltip, rather than a persistent inline text block - Replaced the in-page dropdown panel (block-inserted for row/banner buttons, fixed-position floating for menu items) with a single centred modal overlay used by all three injection shapes — simpler than maintaining per-variant positioning logic, and looks the same everywhere - Picker overlay now opens automatically when the default tablet is busy (409) or configured but not actually connected (delivered: false), not just when no default is set at all — mirrored in popup.js's own fallback Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
4aeca15281
commit
d7fbdce04d
2 changed files with 214 additions and 136 deletions
|
|
@ -95,7 +95,7 @@ function makeSendButton(bookingId, guestName, variant = 'block') {
|
||||||
btn.id = `tcp-btn-${bookingId}-${variant}`;
|
btn.id = `tcp-btn-${bookingId}-${variant}`;
|
||||||
btn.type = 'button';
|
btn.type = 'button';
|
||||||
btn.className = variant === 'block' ? 'button most_width' : 'button';
|
btn.className = variant === 'block' ? 'button most_width' : 'button';
|
||||||
btn.innerHTML = '<i class="far fa-tablet-alt fa-fw" aria-hidden="true"></i> <span>Tablet Push</span>';
|
btn.innerHTML = '<i class="tcp-icon far fa-tablet-alt fa-fw" aria-hidden="true"></i> <span>Tablet Push</span>';
|
||||||
btn.style.cssText = `
|
btn.style.cssText = `
|
||||||
${variant === 'block' ? 'margin-top: 6px;' : 'margin-right: 8px;'}
|
${variant === 'block' ? 'margin-top: 6px;' : 'margin-right: 8px;'}
|
||||||
background: #be123c; border-color: #be123c; color: #fff;
|
background: #be123c; border-color: #be123c; color: #fff;
|
||||||
|
|
@ -103,121 +103,193 @@ function makeSendButton(bookingId, guestName, variant = 'block') {
|
||||||
btn.addEventListener('click', e => {
|
btn.addEventListener('click', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleSendClick(btn, bookingId, guestName, variant);
|
handleSendClick(btn, bookingId, guestName);
|
||||||
});
|
});
|
||||||
return btn;
|
return btn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.textContent = s;
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visual feedback is split two ways: a per-button icon swap for the simple
|
||||||
|
// pass/fail cases (no page real estate taken, self-explanatory at a glance),
|
||||||
|
// and the picker overlay below for anything that actually needs a human
|
||||||
|
// decision (no default configured, default busy, default offline). Detail
|
||||||
|
// text lives in the title attribute (hover tooltip) rather than a persistent
|
||||||
|
// inline block — keeps every variant (table-cell row, banner, nav-bar menu
|
||||||
|
// item) visually identical instead of needing per-container layout math.
|
||||||
|
const STATE_ICON = {
|
||||||
|
idle: 'fa-tablet-alt',
|
||||||
|
loading: 'fa-spinner fa-spin',
|
||||||
|
success: 'fa-check',
|
||||||
|
warning: 'fa-exclamation-triangle',
|
||||||
|
error: 'fa-exclamation-triangle',
|
||||||
|
};
|
||||||
|
const STATE_BG = {
|
||||||
|
idle: '#be123c',
|
||||||
|
loading: '#be123c',
|
||||||
|
success: '#166534',
|
||||||
|
warning: '#b45309',
|
||||||
|
error: '#dc2626',
|
||||||
|
};
|
||||||
|
const STATE_ICON_COLOR = {
|
||||||
|
success: '#86efac',
|
||||||
|
warning: '#fde68a',
|
||||||
|
error: '#fecaca',
|
||||||
|
};
|
||||||
|
|
||||||
|
// <button> variants (row/banner) get their whole background tinted; the
|
||||||
|
// context-menu variant is an <a> with no background box of its own — tinting
|
||||||
|
// that would look out of place in NewBook's own nav bar, so only its icon
|
||||||
|
// recolours.
|
||||||
|
function setButtonState(anchorBtn, state, title = '') {
|
||||||
|
const icon = anchorBtn.querySelector('.tcp-icon');
|
||||||
|
if (icon) {
|
||||||
|
icon.className = `tcp-icon far ${STATE_ICON[state] || STATE_ICON.idle} fa-fw`;
|
||||||
|
icon.style.color = STATE_ICON_COLOR[state] || '';
|
||||||
|
}
|
||||||
|
if (anchorBtn.tagName === 'BUTTON') {
|
||||||
|
const bg = STATE_BG[state] || STATE_BG.idle;
|
||||||
|
anchorBtn.style.background = bg;
|
||||||
|
anchorBtn.style.borderColor = bg;
|
||||||
|
}
|
||||||
|
if (title) anchorBtn.title = title; else anchorBtn.removeAttribute('title');
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleRevert(anchorBtn) {
|
||||||
|
setTimeout(() => {
|
||||||
|
// Button may already be gone (NewBook rebuilt its container) — touching
|
||||||
|
// a detached node is harmless, but skip the state reset if so, nothing
|
||||||
|
// to show it on anymore.
|
||||||
|
if (document.body.contains(anchorBtn)) setButtonState(anchorBtn, 'idle');
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
// Injected buttons skip asking which tablet entirely when a default is
|
// Injected buttons skip asking which tablet entirely when a default is
|
||||||
// configured for this machine (see options.js) — push straight to it. Only
|
// configured for this machine (see options.js) — push straight to it. Only
|
||||||
// falls back to the full device-picker panel if that specific push comes
|
// opens the picker overlay if that specific push comes back busy (409), the
|
||||||
// back busy (409), or if no default is configured at all — see
|
// default is configured but not actually connected (delivered: false — see
|
||||||
// project_checkin memory's extension notes, this was an explicit UX request
|
// device-stream.js), or no default is configured at all. The popup is
|
||||||
// ("injected buttons don't need to ask which tablet, just use/send to
|
// deliberately different — it always shows the picker, just pre-selected to
|
||||||
// default unless busy"). The popup is deliberately different — it always
|
// the default (see popup.js) — since opening the popup is already a more
|
||||||
// shows the picker, just pre-selected to the default (see popup.js) — since
|
// deliberate action than a one-click inline button.
|
||||||
// opening the popup is already a more deliberate action than a one-click
|
async function handleSendClick(anchorBtn, bookingId, guestName) {
|
||||||
// inline button.
|
if (anchorBtn.dataset.tcpBusy === '1') return; // already mid-send, ignore a double-click
|
||||||
async function handleSendClick(anchorBtn, bookingId, guestName, variant) {
|
|
||||||
const { defaultDeviceId } = await chrome.storage.local.get('defaultDeviceId');
|
|
||||||
|
|
||||||
if (defaultDeviceId) {
|
const { defaultDeviceId } = await chrome.storage.local.get('defaultDeviceId');
|
||||||
const status = showInlineStatus(anchorBtn, variant, 'Sending…');
|
if (!defaultDeviceId) {
|
||||||
let result;
|
showPickerOverlay(anchorBtn, bookingId, guestName);
|
||||||
try {
|
|
||||||
result = await chrome.runtime.sendMessage({ action: 'tcpPush', bookingId, deviceId: defaultDeviceId });
|
|
||||||
} catch {
|
|
||||||
status.textContent = 'Extension was reloaded — refresh this NewBook page and try again.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (result?.ok) {
|
|
||||||
if (result.delivered === false) {
|
|
||||||
status.textContent = 'Sent, but that tablet isn’t connected right now — it’ll pick this up if it reconnects.';
|
|
||||||
status.style.color = '#b45309';
|
|
||||||
} else {
|
|
||||||
status.textContent = 'Sent — the tablet should navigate now.';
|
|
||||||
status.style.color = '#166534';
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (result?.status === 409) {
|
|
||||||
status.remove();
|
|
||||||
// fall through to the picker below, with a note explaining why it's
|
|
||||||
// asking despite a default being configured
|
|
||||||
await togglePanel(anchorBtn, bookingId, guestName, variant, 'Default tablet is busy — choose another:');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
status.textContent = `Failed: ${result?.error || 'unknown error'}`;
|
|
||||||
status.style.color = '#dc2626';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await togglePanel(anchorBtn, bookingId, guestName, variant);
|
anchorBtn.dataset.tcpBusy = '1';
|
||||||
}
|
setButtonState(anchorBtn, 'loading');
|
||||||
|
|
||||||
// The context-menu buttons (variant 'menu') live inside a horizontal nav
|
let result;
|
||||||
// bar <li> — inserting a block-level status/panel 'afterend' there (like the
|
try {
|
||||||
// row/banner variants do) would stretch that menu item's row height instead
|
result = await chrome.runtime.sendMessage({ action: 'tcpPush', bookingId, deviceId: defaultDeviceId });
|
||||||
// of floating over the page. Anchoring to the viewport via the button's own
|
} catch {
|
||||||
// bounding rect sidesteps needing the menu bar to be a positioned ancestor.
|
delete anchorBtn.dataset.tcpBusy;
|
||||||
function positionFloating(el, anchorBtn) {
|
setButtonState(anchorBtn, 'error', 'Extension was reloaded — refresh this NewBook page and try again.');
|
||||||
const rect = anchorBtn.getBoundingClientRect();
|
scheduleRevert(anchorBtn);
|
||||||
el.style.position = 'fixed';
|
return;
|
||||||
el.style.top = `${rect.bottom + 4}px`;
|
|
||||||
el.style.left = `${rect.left}px`;
|
|
||||||
el.style.zIndex = '99999';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Small text-only status line for the default-tablet fast path — no
|
|
||||||
// dropdown, nothing to pick, just feedback on what happened.
|
|
||||||
function showInlineStatus(anchorBtn, variant, text) {
|
|
||||||
if (openPanel) { openPanel.remove(); openPanel = null; }
|
|
||||||
const el = document.createElement('div');
|
|
||||||
const width = variant === 'block' ? 'width: 100%;' : '';
|
|
||||||
const floating = variant === 'menu';
|
|
||||||
el.style.cssText = `
|
|
||||||
display: block; ${floating ? '' : 'margin-top: 6px;'} ${width} font-size: 12px; color: #64748b;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
|
||||||
${floating ? 'background: #fff; padding: 6px 10px; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.15);' : ''}
|
|
||||||
`;
|
|
||||||
el.textContent = text;
|
|
||||||
if (floating) {
|
|
||||||
document.body.appendChild(el);
|
|
||||||
positionFloating(el, anchorBtn);
|
|
||||||
} else {
|
|
||||||
anchorBtn.insertAdjacentElement('afterend', el);
|
|
||||||
}
|
}
|
||||||
openPanel = el;
|
delete anchorBtn.dataset.tcpBusy;
|
||||||
return el;
|
|
||||||
|
if (result?.ok && result.delivered !== false) {
|
||||||
|
setButtonState(anchorBtn, 'success', 'Sent — the tablet should navigate now.');
|
||||||
|
scheduleRevert(anchorBtn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result?.ok) {
|
||||||
|
// delivered === false — session created server-side but nothing's
|
||||||
|
// actually listening on that device right now.
|
||||||
|
setButtonState(anchorBtn, 'warning', 'That tablet isn’t connected right now.');
|
||||||
|
showPickerOverlay(anchorBtn, bookingId, guestName, 'Default tablet isn’t connected — choose another:');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result?.status === 409) {
|
||||||
|
setButtonState(anchorBtn, 'warning', result.error || 'That tablet is busy.');
|
||||||
|
showPickerOverlay(anchorBtn, bookingId, guestName, 'Default tablet is busy — choose another:');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setButtonState(anchorBtn, 'error', `Failed: ${result?.error || 'unknown error'}`);
|
||||||
|
scheduleRevert(anchorBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
let openPanel = null;
|
// Centred modal overlay — replaces the old in-page dropdown panel entirely
|
||||||
|
// (both its block-flow insertion for row/banner buttons and the fixed-
|
||||||
|
// position floating variant the nav-bar menu items needed). A real overlay
|
||||||
|
// doesn't care where its anchor lives on the page, so one implementation now
|
||||||
|
// covers all three injection shapes.
|
||||||
|
let overlayBackdrop = null;
|
||||||
|
|
||||||
async function togglePanel(anchorBtn, bookingId, guestName, variant = 'block', note = '') {
|
function closeOverlay() {
|
||||||
if (openPanel) { openPanel.remove(); openPanel = null; }
|
if (!overlayBackdrop) return;
|
||||||
|
overlayBackdrop.remove();
|
||||||
|
overlayBackdrop = null;
|
||||||
|
document.removeEventListener('keydown', handleOverlayKeydown);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOverlayKeydown(e) {
|
||||||
|
if (e.key === 'Escape') closeOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOverlay() {
|
||||||
|
closeOverlay();
|
||||||
|
const backdrop = document.createElement('div');
|
||||||
|
backdrop.style.cssText = `
|
||||||
|
position: fixed; inset: 0; background: rgba(15, 23, 42, 0.45);
|
||||||
|
z-index: 999998; display: flex; align-items: center; justify-content: center;
|
||||||
|
`;
|
||||||
|
backdrop.addEventListener('mousedown', e => { if (e.target === backdrop) closeOverlay(); });
|
||||||
|
|
||||||
const panel = document.createElement('div');
|
const panel = document.createElement('div');
|
||||||
// Always its own block below the anchor regardless of variant — an inline
|
|
||||||
// anchor (the banner button) has no full-width block context to size
|
|
||||||
// against naturally, so give it an explicit min-width instead of relying
|
|
||||||
// on parent width like the row-cell variant can.
|
|
||||||
const floating = variant === 'menu';
|
|
||||||
const width = variant === 'block' ? 'width: 100%;' : 'min-width: 200px; max-width: 280px;';
|
|
||||||
panel.style.cssText = `
|
panel.style.cssText = `
|
||||||
display: block; ${floating ? '' : 'margin-top: 6px;'} ${width} padding: 8px; background: #f4f5f7;
|
position: relative; background: #fff; border-radius: 12px; padding: 20px;
|
||||||
border: 1px solid #e4e8ee; border-radius: 8px;
|
min-width: 260px; max-width: 320px; box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
||||||
${floating ? 'box-shadow: 0 2px 10px rgba(0,0,0,0.2);' : ''}
|
font-family: -apple-system, BlinkMacSystemFont, sans-serif; font-size: 13px; color: #1e293b;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
|
||||||
font-size: 12px; color: #1e293b;
|
|
||||||
`;
|
`;
|
||||||
panel.textContent = note ? `${note} loading…` : 'Loading tablets…';
|
|
||||||
if (floating) {
|
const closeBtn = document.createElement('button');
|
||||||
document.body.appendChild(panel);
|
closeBtn.type = 'button';
|
||||||
positionFloating(panel, anchorBtn);
|
closeBtn.textContent = '×';
|
||||||
} else {
|
closeBtn.setAttribute('aria-label', 'Close');
|
||||||
anchorBtn.insertAdjacentElement('afterend', panel);
|
closeBtn.style.cssText = `
|
||||||
|
position: absolute; top: 6px; right: 10px; border: none; background: none;
|
||||||
|
font-size: 20px; line-height: 1; cursor: pointer; color: #94a3b8; padding: 4px;
|
||||||
|
`;
|
||||||
|
closeBtn.addEventListener('click', closeOverlay);
|
||||||
|
panel.appendChild(closeBtn);
|
||||||
|
|
||||||
|
backdrop.appendChild(panel);
|
||||||
|
document.body.appendChild(backdrop);
|
||||||
|
overlayBackdrop = backdrop;
|
||||||
|
document.addEventListener('keydown', handleOverlayKeydown);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showPickerOverlay(anchorBtn, bookingId, guestName, note = '') {
|
||||||
|
const panel = buildOverlay();
|
||||||
|
|
||||||
|
const title = document.createElement('div');
|
||||||
|
title.style.cssText = 'font-weight: 700; margin: 0 20px 10px 0; color: #1a1a2e;';
|
||||||
|
title.textContent = guestName ? `Send ${guestName} to a tablet` : 'Send to a tablet';
|
||||||
|
panel.appendChild(title);
|
||||||
|
|
||||||
|
if (note) {
|
||||||
|
const noteEl = document.createElement('div');
|
||||||
|
noteEl.textContent = note;
|
||||||
|
noteEl.style.cssText = 'margin-bottom: 10px; color: #b45309; font-weight: 600;';
|
||||||
|
panel.appendChild(noteEl);
|
||||||
}
|
}
|
||||||
openPanel = panel;
|
|
||||||
|
const body = document.createElement('div');
|
||||||
|
body.textContent = 'Loading tablets…';
|
||||||
|
panel.appendChild(body);
|
||||||
|
|
||||||
// A stale content script (injected before the extension was last
|
// A stale content script (injected before the extension was last
|
||||||
// reloaded) throws "Extension context invalidated" the moment it touches
|
// reloaded) throws "Extension context invalidated" the moment it touches
|
||||||
|
|
@ -230,82 +302,87 @@ async function togglePanel(anchorBtn, bookingId, guestName, variant = 'block', n
|
||||||
try {
|
try {
|
||||||
res = await chrome.runtime.sendMessage({ action: 'tcpListDevices' });
|
res = await chrome.runtime.sendMessage({ action: 'tcpListDevices' });
|
||||||
} catch {
|
} catch {
|
||||||
panel.textContent = 'Extension was reloaded — refresh this NewBook page and try again.';
|
body.textContent = 'Extension was reloaded — refresh this NewBook page and try again.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (overlayBackdrop !== panel.parentElement) return; // closed while loading
|
||||||
if (!res?.ok) {
|
if (!res?.ok) {
|
||||||
panel.textContent = `Could not load tablets: ${res?.error || 'not configured yet — see extension Settings'}`;
|
body.textContent = `Could not load tablets: ${res?.error || 'not configured yet — see extension Settings'}`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!res.devices.length) {
|
if (!res.devices.length) {
|
||||||
panel.textContent = 'No tablets registered yet.';
|
body.textContent = 'No tablets registered yet.';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const select = document.createElement('select');
|
const select = document.createElement('select');
|
||||||
select.style.cssText = 'width: 100%; margin-bottom: 6px; padding: 4px;';
|
select.style.cssText = 'width: 100%; margin-bottom: 10px; padding: 6px; border-radius: 6px; border: 1px solid #e4e8ee;';
|
||||||
select.innerHTML = res.devices.map(d =>
|
select.innerHTML = res.devices.map(d =>
|
||||||
`<option value="${d.device_id}" ${d.status === 'busy' ? 'disabled' : ''}>${escapeHtml(d.name)}${d.status === 'busy' ? ' (busy)' : ''}</option>`
|
`<option value="${d.device_id}" ${d.status === 'busy' ? 'disabled' : ''}>${escapeHtml(d.name)}${d.status === 'busy' ? ' (busy)' : ''}</option>`
|
||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
const sendBtn = document.createElement('button');
|
const sendBtn = document.createElement('button');
|
||||||
|
sendBtn.type = 'button';
|
||||||
sendBtn.textContent = `Send${guestName ? ` — ${guestName}` : ''}`;
|
sendBtn.textContent = `Send${guestName ? ` — ${guestName}` : ''}`;
|
||||||
sendBtn.style.cssText = `
|
sendBtn.style.cssText = `
|
||||||
width: 100%; padding: 4px 8px; background: #c9a84c; color: #1a1a2e;
|
width: 100%; padding: 8px; background: #c9a84c; color: #1a1a2e;
|
||||||
border: none; border-radius: 6px; font-weight: 600; cursor: pointer;
|
border: none; border-radius: 6px; font-weight: 700; cursor: pointer;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const statusEl = document.createElement('div');
|
||||||
|
statusEl.style.cssText = 'margin-top: 8px; font-size: 12px;';
|
||||||
|
|
||||||
sendBtn.addEventListener('click', async () => {
|
sendBtn.addEventListener('click', async () => {
|
||||||
sendBtn.disabled = true;
|
sendBtn.disabled = true;
|
||||||
sendBtn.textContent = 'Sending…';
|
sendBtn.textContent = 'Sending…';
|
||||||
const result = await chrome.runtime.sendMessage({
|
setButtonState(anchorBtn, 'loading');
|
||||||
action: 'tcpPush', bookingId, deviceId: select.value,
|
|
||||||
});
|
const result = await chrome.runtime.sendMessage({ action: 'tcpPush', bookingId, deviceId: select.value });
|
||||||
if (result?.ok) {
|
|
||||||
panel.textContent = result.delivered === false
|
sendBtn.disabled = false;
|
||||||
? 'Sent, but that tablet isn’t connected right now — it’ll pick this up if it reconnects.'
|
sendBtn.textContent = `Send${guestName ? ` — ${guestName}` : ''}`;
|
||||||
: 'Sent — the tablet should navigate now.';
|
|
||||||
panel.style.color = result.delivered === false ? '#b45309' : '#1e293b';
|
if (result?.ok && result.delivered !== false) {
|
||||||
|
statusEl.textContent = 'Sent — the tablet should navigate now.';
|
||||||
|
statusEl.style.color = '#166534';
|
||||||
|
setButtonState(anchorBtn, 'success', 'Sent — the tablet should navigate now.');
|
||||||
|
scheduleRevert(anchorBtn);
|
||||||
|
setTimeout(closeOverlay, 1000);
|
||||||
|
} else if (result?.ok) {
|
||||||
|
statusEl.textContent = 'Sent, but that tablet isn’t connected right now.';
|
||||||
|
statusEl.style.color = '#b45309';
|
||||||
|
setButtonState(anchorBtn, 'warning', 'That tablet isn’t connected right now.');
|
||||||
} else {
|
} else {
|
||||||
panel.textContent = `Failed: ${result?.error || 'unknown error'}`;
|
statusEl.textContent = `Failed: ${result?.error || 'unknown error'}`;
|
||||||
|
statusEl.style.color = '#dc2626';
|
||||||
|
setButtonState(anchorBtn, 'error', `Failed: ${result?.error || 'unknown error'}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
panel.innerHTML = '';
|
body.innerHTML = '';
|
||||||
if (note) {
|
body.appendChild(select);
|
||||||
const noteEl = document.createElement('div');
|
body.appendChild(sendBtn);
|
||||||
noteEl.textContent = note;
|
body.appendChild(statusEl);
|
||||||
noteEl.style.cssText = 'margin-bottom: 6px; color: #b45309; font-weight: 600;';
|
|
||||||
panel.appendChild(noteEl);
|
|
||||||
}
|
|
||||||
panel.appendChild(select);
|
|
||||||
panel.appendChild(sendBtn);
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(s) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = s;
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context-menu entries use a completely different markup shape than the
|
// Context-menu entries use a completely different markup shape than the
|
||||||
// .button-based row/banner injections above — <li><a><div class="page_icon">
|
// .button-based row/banner injections above — <li><a><div class="page_icon">
|
||||||
// <i/></div><span/></a></li> — so this builds that shape directly rather
|
// <i/></div><span/></a></li> — so this builds that shape directly rather
|
||||||
// than reusing makeSendButton. Click handling (default-tablet fast path,
|
// than reusing makeSendButton. Click handling (default-tablet fast path,
|
||||||
// picker fallback) is identical, via the 'menu' variant plumbed through
|
// picker overlay fallback) is identical via the shared handleSendClick.
|
||||||
// handleSendClick/showInlineStatus/togglePanel above.
|
|
||||||
function makeMenuLink(bookingId) {
|
function makeMenuLink(bookingId) {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = 'context tcp-menu-item';
|
li.className = 'context tcp-menu-item';
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = '#';
|
a.href = '#';
|
||||||
a.innerHTML = `
|
a.innerHTML = `
|
||||||
<div class="page_icon"><i class="far fa-tablet-alt fa-fw" alt="" style="margin-right: 0; margin-left: 0px; vertical-align: middle; font-size: 24px;" aria-hidden="true"></i></div>
|
<div class="page_icon"><i class="tcp-icon far fa-tablet-alt fa-fw" alt="" style="margin-right: 0; margin-left: 0px; vertical-align: middle; font-size: 24px;" aria-hidden="true"></i></div>
|
||||||
<span>Tablet Push</span>
|
<span>Tablet Push</span>
|
||||||
`;
|
`;
|
||||||
a.addEventListener('click', e => {
|
a.addEventListener('click', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleSendClick(a, bookingId, '', 'menu');
|
handleSendClick(a, bookingId, '');
|
||||||
});
|
});
|
||||||
li.appendChild(a);
|
li.appendChild(a);
|
||||||
return li;
|
return li;
|
||||||
|
|
|
||||||
|
|
@ -126,12 +126,13 @@ function renderBooking(el, bookingId, devices, defaultDeviceId, baseUrl, apiKey)
|
||||||
const msgEl = document.getElementById('msg');
|
const msgEl = document.getElementById('msg');
|
||||||
if (!msgEl) return; // "Send to a different tablet" was clicked before this resolved
|
if (!msgEl) return; // "Send to a different tablet" was clicked before this resolved
|
||||||
|
|
||||||
if (result.ok) {
|
if (result.ok && result.delivered !== false) {
|
||||||
if (result.delivered === false) {
|
setMsg(msgEl, 'Sent — the tablet should navigate now.', 'ok');
|
||||||
setMsg(msgEl, 'Sent, but that tablet isn’t connected right now — it’ll pick this up if it reconnects.', 'warn');
|
} else if (result.ok) {
|
||||||
} else {
|
// Session created server-side but nothing's actually listening on
|
||||||
setMsg(msgEl, 'Sent — the tablet should navigate now.', 'ok');
|
// that device right now — same "needs a human decision" case as
|
||||||
}
|
// busy, so offer the picker rather than a dead-end message.
|
||||||
|
renderPicker(el, bookingId, devices, defaultDeviceId, baseUrl, apiKey, 'Default tablet isn’t connected — choose another:');
|
||||||
} else if (result.status === 409) {
|
} else if (result.status === 409) {
|
||||||
// Default went busy in the moment between loading the popup and
|
// Default went busy in the moment between loading the popup and
|
||||||
// sending — fall back to the manual picker rather than leaving the
|
// sending — fall back to the manual picker rather than leaving the
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue