Extension: inject Send-to-Tablet next to the arrival reminder banner too
New injection point on bookings_view pages — NewBook's own 'This Booking
is due to Arrive today. Do you want to go to Check-In?' banner
(.newbook_message.plain), confirmed from a real sample 2026-09-01. Shares
the row-injection's button/panel logic rather than duplicating it;
makeSendButton/togglePanel now take a variant ('block' for the list row's
table cell, 'inline' for this banner's flowing text context). Booking ID
extraction differs per shape: the list row has a checkbox value, this
banner's Check In button only carries it inside its onclick attribute's
URL, no separate structured attribute.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
82f926d736
commit
fbd1443e35
2 changed files with 87 additions and 31 deletions
|
|
@ -1,29 +1,34 @@
|
||||||
// Injects a "Send to Tablet" button into each row of NewBook's "due for
|
// Injects a "Send to Tablet" button next to two different shapes of
|
||||||
// arrival" list (bookings_lists_checkin), right under the page's own
|
// NewBook's own Check In button, on whichever pages carry them (see
|
||||||
// Check In button. Only runs on that one page (see manifest.json's
|
// manifest.json's content_scripts.matches) — the popup covers single-booking
|
||||||
// content_scripts.matches) — the popup covers single-booking pages
|
// pages independently of this, this is purely about inline injection:
|
||||||
// (bookings_view/edit/checkin) already.
|
// 1. Each row of the "due for arrival" list (bookings_lists_checkin).
|
||||||
|
// 2. The "This Booking is due to Arrive today. Do you want to go to
|
||||||
|
// Check-In?" reminder banner NewBook shows inline (confirmed 2026-09-01
|
||||||
|
// from a real sample — a `.newbook_message.plain` block, most likely on
|
||||||
|
// bookings_view/edit but the exact page isn't nailed down, hence the
|
||||||
|
// slightly broader matches list below rather than one exact page).
|
||||||
//
|
//
|
||||||
// Row markup, booking-ID extraction, and the "generic_button_1"-is-not-
|
// Both shapes share the same "generic_button_1"-is-not-actually-unique
|
||||||
// actually-unique gotcha are all confirmed from a real sample row the user
|
// gotcha (NewBook reuses that id on every instance of its own Check In
|
||||||
// provided 2026-09-01 (see project_checkin memory) — not guessed. NewBook's
|
// button, confirmed 2026-09-01) — this script never relies on ids from
|
||||||
// own Check In button uses a non-unique id (`generic_button_1` repeated
|
// NewBook's own markup for either shape, only class/attribute selectors
|
||||||
// across every row), so this script never relies on ids from NewBook's own
|
// scoped to each button's own container. Booking-ID extraction differs
|
||||||
// markup — only class/attribute selectors scoped per row, and its own
|
// between the two shapes though: the list row carries it in a checkbox's
|
||||||
// injected elements get ids built from the booking ID
|
// `value` (see extractBookingIdFromRow), the banner button only carries it
|
||||||
// (`tcp-btn-<bookingId>`) to stay collision-free and support idempotent
|
// inside its `onclick` attribute's URL (see extractBookingIdFromOnclick) —
|
||||||
// re-injection checks.
|
// there's no separate structured attribute for it there.
|
||||||
//
|
//
|
||||||
// MutationObserver pattern (watching for AJAX-refreshed rows, not just
|
// MutationObserver pattern (watching for AJAX-refreshed content, not just
|
||||||
// initial page load) is the same approach chrome-newbook-assistant already
|
// initial page load) is the same approach chrome-newbook-assistant already
|
||||||
// had to use for this same NewBook UI (~/laptop-archive) — this system
|
// had to use for this same NewBook UI (~/laptop-archive) — this system
|
||||||
// clearly refreshes table content via AJAX (see the `via_ajax` tooltip
|
// clearly refreshes content via AJAX (see the `via_ajax` tooltip classes in
|
||||||
// classes in the sample row) without a full page reload, so a one-time scan
|
// the row sample) without a full page reload, so a one-time scan on
|
||||||
// on document_idle alone would miss rows added/replaced afterward.
|
// document_idle alone would miss content added/replaced afterward.
|
||||||
|
|
||||||
const PROCESSED_ATTR = 'data-tcp-processed';
|
const PROCESSED_ATTR = 'data-tcp-processed';
|
||||||
|
|
||||||
function extractBookingId(row) {
|
function extractBookingIdFromRow(row) {
|
||||||
const checkbox = row.querySelector('input.booking_id_checkbox');
|
const checkbox = row.querySelector('input.booking_id_checkbox');
|
||||||
if (checkbox?.value) return checkbox.value;
|
if (checkbox?.value) return checkbox.value;
|
||||||
// Fallback: class name carries it too (`booking_37117`) — confirmed from
|
// Fallback: class name carries it too (`booking_37117`) — confirmed from
|
||||||
|
|
@ -33,6 +38,15 @@ function extractBookingId(row) {
|
||||||
return match ? match[1] : null;
|
return match ? match[1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The reminder banner's Check In button has no separate booking-ID
|
||||||
|
// attribute at all — only its onclick handler carries it, embedded in the
|
||||||
|
// NewBook URL it navigates to: load_url("https://.../bookings_checkin/37129")
|
||||||
|
function extractBookingIdFromOnclick(button) {
|
||||||
|
const onclick = button.getAttribute('onclick') || '';
|
||||||
|
const match = onclick.match(/bookings_checkin\/(\d+)/);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
function extractGuestName(row) {
|
function extractGuestName(row) {
|
||||||
// The guest/company link's own text node — tooltip content is a sibling
|
// The guest/company link's own text node — tooltip content is a sibling
|
||||||
// element (easytooltip_content), not part of the visible text, so a plain
|
// element (easytooltip_content), not part of the visible text, so a plain
|
||||||
|
|
@ -53,13 +67,21 @@ function findCheckInCell(row) {
|
||||||
return btn ? btn.closest('td') : null;
|
return btn ? btn.closest('td') : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeSendButton(bookingId, guestName) {
|
// variant 'block' = full-width, stacked below the anchor (table cell — the
|
||||||
|
// list row context, where width is already constrained by the column).
|
||||||
|
// variant 'inline' = sized to its own content, sitting next to the anchor
|
||||||
|
// (the reminder banner context, which flows inline with other text/buttons
|
||||||
|
// rather than living in a narrow fixed-width cell).
|
||||||
|
function makeSendButton(bookingId, guestName, variant = 'block') {
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.id = `tcp-btn-${bookingId}`;
|
btn.id = `tcp-btn-${bookingId}-${variant}`;
|
||||||
btn.type = 'button';
|
btn.type = 'button';
|
||||||
btn.textContent = 'Send to Tablet';
|
btn.textContent = 'Send to Tablet';
|
||||||
|
const layout = variant === 'block'
|
||||||
|
? 'display: block; margin-top: 6px; width: 100%;'
|
||||||
|
: 'display: inline-block; margin-left: 8px;';
|
||||||
btn.style.cssText = `
|
btn.style.cssText = `
|
||||||
display: block; margin-top: 6px; width: 100%;
|
${layout}
|
||||||
padding: 4px 8px; font-size: 12px; font-weight: 600;
|
padding: 4px 8px; font-size: 12px; font-weight: 600;
|
||||||
background: #be123c; color: #fff; border: none; border-radius: 6px;
|
background: #be123c; color: #fff; border: none; border-radius: 6px;
|
||||||
cursor: pointer; font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
cursor: pointer; font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
|
@ -67,20 +89,26 @@ function makeSendButton(bookingId, guestName) {
|
||||||
btn.addEventListener('click', e => {
|
btn.addEventListener('click', e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
togglePanel(btn, bookingId, guestName);
|
togglePanel(btn, bookingId, guestName, variant);
|
||||||
});
|
});
|
||||||
return btn;
|
return btn;
|
||||||
}
|
}
|
||||||
|
|
||||||
let openPanel = null;
|
let openPanel = null;
|
||||||
|
|
||||||
async function togglePanel(anchorBtn, bookingId, guestName) {
|
async function togglePanel(anchorBtn, bookingId, guestName, variant = 'block') {
|
||||||
if (openPanel) { openPanel.remove(); openPanel = null; }
|
if (openPanel) { openPanel.remove(); openPanel = null; }
|
||||||
|
|
||||||
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 width = variant === 'block' ? 'width: 100%;' : 'min-width: 200px; max-width: 280px;';
|
||||||
panel.style.cssText = `
|
panel.style.cssText = `
|
||||||
margin-top: 6px; padding: 8px; background: #f4f5f7; border: 1px solid #e4e8ee;
|
display: block; margin-top: 6px; ${width} padding: 8px; background: #f4f5f7;
|
||||||
border-radius: 8px; font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
border: 1px solid #e4e8ee; border-radius: 8px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
font-size: 12px; color: #1e293b;
|
font-size: 12px; color: #1e293b;
|
||||||
`;
|
`;
|
||||||
panel.textContent = 'Loading tablets…';
|
panel.textContent = 'Loading tablets…';
|
||||||
|
|
@ -135,26 +163,51 @@ function escapeHtml(s) {
|
||||||
|
|
||||||
function processRow(row) {
|
function processRow(row) {
|
||||||
if (row.hasAttribute(PROCESSED_ATTR)) return;
|
if (row.hasAttribute(PROCESSED_ATTR)) return;
|
||||||
const bookingId = extractBookingId(row);
|
const bookingId = extractBookingIdFromRow(row);
|
||||||
const cell = findCheckInCell(row);
|
const cell = findCheckInCell(row);
|
||||||
if (!bookingId || !cell) return; // not a real booking row, or no Check In button in it
|
if (!bookingId || !cell) return; // not a real booking row, or no Check In button in it
|
||||||
|
|
||||||
row.setAttribute(PROCESSED_ATTR, '1');
|
row.setAttribute(PROCESSED_ATTR, '1');
|
||||||
cell.appendChild(makeSendButton(bookingId, extractGuestName(row)));
|
cell.appendChild(makeSendButton(bookingId, extractGuestName(row), 'block'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "due to Arrive today" reminder banner on bookings_view pages
|
||||||
|
// (confirmed 2026-09-01 — see the class comment at the top of this file).
|
||||||
|
// Scoped specifically to a Check In button sitting inside a
|
||||||
|
// .newbook_message.plain block, not just any button with that aria-label
|
||||||
|
// anywhere on the page, so this can't accidentally match something
|
||||||
|
// unrelated that happens to reuse the same label.
|
||||||
|
function processCheckinBanner(button) {
|
||||||
|
if (button.hasAttribute(PROCESSED_ATTR)) return;
|
||||||
|
const bookingId = extractBookingIdFromOnclick(button);
|
||||||
|
if (!bookingId) return;
|
||||||
|
|
||||||
|
button.setAttribute(PROCESSED_ATTR, '1');
|
||||||
|
button.insertAdjacentElement('afterend', makeSendButton(bookingId, '', 'inline'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function scanForRows(root = document) {
|
function scanForRows(root = document) {
|
||||||
root.querySelectorAll('tr.checkbox_row').forEach(processRow);
|
root.querySelectorAll('tr.checkbox_row').forEach(processRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
scanForRows();
|
function scanForBanners(root = document) {
|
||||||
|
root.querySelectorAll('.newbook_message.plain .note_text button[aria-label="Check In"]')
|
||||||
|
.forEach(processCheckinBanner);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanAll(root = document) {
|
||||||
|
scanForRows(root);
|
||||||
|
scanForBanners(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
scanAll();
|
||||||
|
|
||||||
const observer = new MutationObserver(mutations => {
|
const observer = new MutationObserver(mutations => {
|
||||||
for (const m of mutations) {
|
for (const m of mutations) {
|
||||||
for (const node of m.addedNodes) {
|
for (const node of m.addedNodes) {
|
||||||
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
||||||
if (node.matches?.('tr.checkbox_row')) processRow(node);
|
if (node.matches?.('tr.checkbox_row')) processRow(node);
|
||||||
else scanForRows(node);
|
else scanAll(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,10 @@
|
||||||
|
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": ["https://appeu.newbook.cloud/bookings_lists_checkin*"],
|
"matches": [
|
||||||
|
"https://appeu.newbook.cloud/bookings_lists_checkin*",
|
||||||
|
"https://appeu.newbook.cloud/bookings_view/*"
|
||||||
|
],
|
||||||
"js": ["content-script.js"],
|
"js": ["content-script.js"],
|
||||||
"run_at": "document_idle"
|
"run_at": "document_idle"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue