Fix sell
This commit is contained in:
@@ -3,15 +3,33 @@ import type { SiteAdapter } from './adapter-interface';
|
|||||||
/** Matches "Buy", "Buy pixelcat", etc. — never a hardcoded token name. */
|
/** Matches "Buy", "Buy pixelcat", etc. — never a hardcoded token name. */
|
||||||
const NOOB_MODE_BUY_TEXT = /^Buy\s/i;
|
const NOOB_MODE_BUY_TEXT = /^Buy\s/i;
|
||||||
|
|
||||||
|
/** Axiom's "increase"/"decrease" Tailwind color tokens are its buy(green)/sell(red) convention. */
|
||||||
|
const INCREASE_MARKER_SELECTOR = '[class*="increase"]';
|
||||||
|
|
||||||
|
function looksLikeBuyPill(el: HTMLElement): boolean {
|
||||||
|
return el.matches(INCREASE_MARKER_SELECTOR) || el.querySelector(INCREASE_MARKER_SELECTOR) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
function findQuickBuyContainers(root: ParentNode): HTMLElement[] {
|
function findQuickBuyContainers(root: ParentNode): HTMLElement[] {
|
||||||
// The reliable anchor is `.buy-click-container` itself, not a fixed
|
// `.buy-click-container` is a *shared* wrapper around both the buy and
|
||||||
// ancestor depth from the inner pill — DOM depth here isn't stable.
|
// sell quick-amount pills as siblings — it is NOT buy-only. The buy pill
|
||||||
return Array.from(root.querySelectorAll<HTMLElement>('.buy-click-container'));
|
// is always its first child element; the sell pill is another sibling.
|
||||||
|
// Never treat the wrapper itself as the buy element/container.
|
||||||
|
const buyPills: HTMLElement[] = [];
|
||||||
|
for (const wrapper of root.querySelectorAll<HTMLElement>('.buy-click-container')) {
|
||||||
|
const firstChild = wrapper.firstElementChild;
|
||||||
|
if (firstChild instanceof HTMLElement && looksLikeBuyPill(firstChild)) {
|
||||||
|
buyPills.push(firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buyPills;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findNoobModeBuyButtons(root: ParentNode): HTMLElement[] {
|
function findNoobModeBuyButtons(root: ParentNode): HTMLElement[] {
|
||||||
|
// Buy and Sell noob-mode buttons are siblings too; the buy one is
|
||||||
|
// identified by its own text starting with "Buy", never by position.
|
||||||
return Array.from(root.querySelectorAll<HTMLElement>('button.bg-increase')).filter((button) =>
|
return Array.from(root.querySelectorAll<HTMLElement>('button.bg-increase')).filter((button) =>
|
||||||
NOOB_MODE_BUY_TEXT.test(button.textContent?.trim() ?? ''),
|
{console.log(button.textContent); return NOOB_MODE_BUY_TEXT.test(button.textContent?.trim() ?? '')},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,9 +49,12 @@ export const axiomAdapter: SiteAdapter = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getContainer(el) {
|
getContainer(el) {
|
||||||
// Quick Buy pills are already `.buy-click-container` (closest() matches
|
// findBuyElements() above already resolves to exactly the buy-side
|
||||||
// the element itself). Noob Mode buttons have no such ancestor, so the
|
// element in both cases — the quick-buy pill (first child of the shared
|
||||||
// button itself is the stable container to style.
|
// buy/sell wrapper) or the noob-mode button whose own text is "Buy …".
|
||||||
return el.closest<HTMLElement>('.buy-click-container') ?? el;
|
// Do NOT climb to any ancestor (e.g. via closest('.buy-click-container'))
|
||||||
|
// — that wrapper holds the sell pill too, so treating it as the
|
||||||
|
// container would blur/disable sell right along with buy.
|
||||||
|
return el;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,13 +3,20 @@ import type { Intervention } from './intervention-interface';
|
|||||||
interface LockedRecord {
|
interface LockedRecord {
|
||||||
originalStyleAttr: string | null;
|
originalStyleAttr: string | null;
|
||||||
overlay: HTMLElement;
|
overlay: HTMLElement;
|
||||||
onOverlayClick: (event: MouseEvent) => void;
|
onOverlayEvent: (event: Event) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tracked outside the DOM so remove() can restore each container exactly,
|
// Tracked outside the DOM so remove() can restore each container exactly,
|
||||||
// without relying on any marker class/attribute the site itself might touch.
|
// without relying on any marker class/attribute the site itself might touch.
|
||||||
const locked = new WeakMap<HTMLElement, LockedRecord>();
|
const locked = new WeakMap<HTMLElement, LockedRecord>();
|
||||||
|
|
||||||
|
// Fast trading UIs (Axiom included) often fire the actual buy on
|
||||||
|
// `pointerdown`/`mousedown` rather than waiting for `click`, to shave off
|
||||||
|
// the mouseup round-trip. Intercepting only `click` lets those through —
|
||||||
|
// so every stage of a click gesture is captured and swallowed here, and the
|
||||||
|
// reason toast is surfaced on `click` once the gesture completes.
|
||||||
|
const INTERCEPTED_EVENT_TYPES = ['pointerdown', 'mousedown', 'mouseup', 'click'] as const;
|
||||||
|
|
||||||
const LOCK_ICON_SVG =
|
const LOCK_ICON_SVG =
|
||||||
'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="10" width="16" height="10" rx="2"></rect><path d="M8 10V7a4 4 0 0 1 8 0v3"></path></svg>';
|
'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="10" width="16" height="10" rx="2"></rect><path d="M8 10V7a4 4 0 0 1 8 0v3"></path></svg>';
|
||||||
|
|
||||||
@@ -58,23 +65,29 @@ export const blurDisable: Intervention = {
|
|||||||
overlay.style.cssText = OVERLAY_STYLE;
|
overlay.style.cssText = OVERLAY_STYLE;
|
||||||
overlay.innerHTML = LOCK_ICON_SVG;
|
overlay.innerHTML = LOCK_ICON_SVG;
|
||||||
|
|
||||||
const onOverlayClick = (event: MouseEvent) => {
|
const onOverlayEvent = (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
|
if (event.type === 'click') {
|
||||||
onLockedInteraction(reason);
|
onLockedInteraction(reason);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
overlay.addEventListener('click', onOverlayClick, true);
|
for (const type of INTERCEPTED_EVENT_TYPES) {
|
||||||
|
overlay.addEventListener(type, onOverlayEvent, true);
|
||||||
|
}
|
||||||
|
|
||||||
container.appendChild(overlay);
|
container.appendChild(overlay);
|
||||||
locked.set(container, { originalStyleAttr, overlay, onOverlayClick });
|
locked.set(container, { originalStyleAttr, overlay, onOverlayEvent });
|
||||||
},
|
},
|
||||||
|
|
||||||
remove(container) {
|
remove(container) {
|
||||||
const record = locked.get(container);
|
const record = locked.get(container);
|
||||||
if (!record) return;
|
if (!record) return;
|
||||||
|
|
||||||
record.overlay.removeEventListener('click', record.onOverlayClick, true);
|
for (const type of INTERCEPTED_EVENT_TYPES) {
|
||||||
|
record.overlay.removeEventListener(type, record.onOverlayEvent, true);
|
||||||
|
}
|
||||||
record.overlay.remove();
|
record.overlay.remove();
|
||||||
|
|
||||||
if (record.originalStyleAttr === null) {
|
if (record.originalStyleAttr === null) {
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import type { NexaMessage } from '@/shared/messaging';
|
|||||||
|
|
||||||
const TOAST_ID = 'nexa-lock-toast';
|
const TOAST_ID = 'nexa-lock-toast';
|
||||||
const STYLE_ID = 'nexa-lock-toast-styles';
|
const STYLE_ID = 'nexa-lock-toast-styles';
|
||||||
|
const AUTO_HIDE_MS = 5000;
|
||||||
|
|
||||||
|
let hideTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
function ensureStyles(): void {
|
function ensureStyles(): void {
|
||||||
if (document.getElementById(STYLE_ID)) return;
|
if (document.getElementById(STYLE_ID)) return;
|
||||||
@@ -59,8 +62,9 @@ function ensureToast(): HTMLElement {
|
|||||||
<div class="nexa-toast-action">View details</div>
|
<div class="nexa-toast-action">View details</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
// Deliberately no close/dismiss control — unlocking is the only way this
|
// No manual close button — the toast only goes away on its own (auto-hide
|
||||||
// toast goes away. See hideLockToast(), called only on an actual unlock.
|
// timer below) or when the lock state actually changes; clicking it never
|
||||||
|
// unlocks anything.
|
||||||
el.querySelector('.nexa-toast-action')?.addEventListener('click', () => {
|
el.querySelector('.nexa-toast-action')?.addEventListener('click', () => {
|
||||||
const message: NexaMessage = { type: 'nexa:open-popup' };
|
const message: NexaMessage = { type: 'nexa:open-popup' };
|
||||||
browser.runtime.sendMessage(message).catch(() => undefined);
|
browser.runtime.sendMessage(message).catch(() => undefined);
|
||||||
@@ -76,8 +80,18 @@ export function showLockToast(reason: string | undefined): void {
|
|||||||
if (reasonEl) {
|
if (reasonEl) {
|
||||||
reasonEl.textContent = reason ?? 'Buy actions are temporarily locked.';
|
reasonEl.textContent = reason ?? 'Buy actions are temporarily locked.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hideTimer) clearTimeout(hideTimer);
|
||||||
|
hideTimer = setTimeout(() => {
|
||||||
|
hideTimer = undefined;
|
||||||
|
hideLockToast();
|
||||||
|
}, AUTO_HIDE_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hideLockToast(): void {
|
export function hideLockToast(): void {
|
||||||
|
if (hideTimer) {
|
||||||
|
clearTimeout(hideTimer);
|
||||||
|
hideTimer = undefined;
|
||||||
|
}
|
||||||
document.getElementById(TOAST_ID)?.remove();
|
document.getElementById(TOAST_ID)?.remove();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user