04/26 Enhanced extension functions

This commit is contained in:
2026-04-26 09:44:54 -04:00
parent 747a9124c2
commit 194601e461
5 changed files with 1470 additions and 13 deletions
+1 -1
View File
@@ -283,5 +283,5 @@ marimo/\_static/
marimo/\_lsp/
**marimo**/
README.md
CLAUDE.md
# CLAUDE.md
.claude/
+1328
View File
File diff suppressed because it is too large Load Diff
+38 -4
View File
@@ -45,6 +45,21 @@
return true;
}
/**
* Returns a debounced version of `fn` that waits `ms` milliseconds after
* the last call before firing. Used to avoid re-rendering the dropdown on
* every keystroke.
*/
function _debounce(fn, ms) {
var timer;
return function () {
var args = arguments;
var ctx = this;
clearTimeout(timer);
timer = setTimeout(function () { fn.apply(ctx, args); }, ms);
};
}
function visiblePasswordFields() {
return Array.from(document.querySelectorAll('input[type="password"]'))
.filter(el => isVisible(el) && !el.disabled);
@@ -621,10 +636,12 @@
showDropdown(field, pwField, field.value, 'credentials');
}, { signal: abortSignal });
// Re-filter as user types.
field.addEventListener('input', function () {
// Re-filter as user types — debounced to avoid rebuilding the dropdown
// on every single keystroke (noticeable on large vaults or slow machines).
var _debouncedShow = _debounce(function () {
showDropdown(field, pwField, field.value, 'credentials');
}, { signal: abortSignal });
}, 150);
field.addEventListener('input', _debouncedShow, { signal: abortSignal });
// Dim button when field loses focus and no dropdown is open.
field.addEventListener('blur', function () {
@@ -901,7 +918,24 @@
}
});
_formObserver = new MutationObserver(function () {
_formObserver = new MutationObserver(function (mutations) {
// Ignore mutations caused by the extension's own injected elements
// (dropdown, icon buttons, save banner) to prevent re-decoration loops
// on SPAs that react to every DOM change.
var ownMutation = mutations.every(function (m) {
return Array.from(m.addedNodes).concat(Array.from(m.removedNodes)).every(function (node) {
if (!node || node.nodeType !== 1) return true;
var cls = (node.className || '');
var id = (node.id || '');
return cls.indexOf('__pk') !== -1 ||
id.indexOf('__pk') !== -1 ||
node.querySelector && (
node.querySelector('.' + PK_BTN_CLASS) ||
node.querySelector('#' + PK_DROPDOWN_ID)
);
});
});
if (ownMutation) return;
_hasNotifiedForm = false;
notifyFormDetected();
decorateFields();
+41
View File
@@ -932,3 +932,44 @@ body {
background: #e0f2fe;
color: #0369a1;
}
/* ── Three-dot flyout menu ───────────────────────────────────────────────── */
.pk-flyout {
position: absolute;
top: calc(100% + 4px);
right: 0;
z-index: 9999;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,0.13);
min-width: 150px;
padding: 4px 0;
display: flex;
flex-direction: column;
}
.pk-flyout-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: none;
border: none;
cursor: pointer;
font-size: 12px;
color: #374151;
text-align: left;
width: 100%;
transition: background 0.1s;
}
.pk-flyout-item:hover {
background: #f3f4f6;
color: #111827;
}
.pk-flyout-item svg {
flex-shrink: 0;
color: #6b7280;
}
+62 -8
View File
@@ -480,6 +480,7 @@ function renderList() {
// SVG icons for action buttons
const svgCopy = `<svg viewBox="0 0 24 24" fill="none"><rect x="9" y="9" width="11" height="11" rx="2" stroke="currentColor" stroke-width="1.7"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke="currentColor" stroke-width="1.7"/></svg>`;
const svgUser = `<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="1.7"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>`;
const svgDots = `<svg viewBox="0 0 24 24" fill="currentColor"><circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/></svg>`;
const svgFill = `<svg viewBox="0 0 24 24" fill="none"><path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M17 3l4 4-9 9H8v-4l9-9z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></svg>`;
const svgTotp = `<svg viewBox="0 0 24 24" fill="none" width="16" height="16"><rect x="5" y="2" width="14" height="20" rx="2" stroke="currentColor" stroke-width="1.7"/><circle cx="12" cy="17" r="1" fill="currentColor"/><path d="M9 7h6M9 11h4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>`;
@@ -503,10 +504,11 @@ function renderList() {
${hasTotp ? `<div class="item-totp-row"><span class="totp-code-inline" id="totp-${item.id}">······</span><span class="totp-timer-inline" id="totp-t-${item.id}"></span></div>` : ''}
</div>
<div class="item-actions">
${item.plain?.username ? `<button class="btn-item-action" data-copy-user="${item.id}" title="Copy username">${svgUser}</button>` : ''}
${canCopy ? `<button class="btn-item-action" data-copy-pass="${item.id}" title="Copy password">${svgCopy}</button>` : ''}
${hasTotp ? `<button class="btn-item-action totp-btn" data-copy-totp="${item.id}" title="Copy 2FA code">${svgTotp}</button>` : ''}
${canFill ? `<button class="btn-item-action" data-autofill="${item.id}" title="Autofill">${svgFill}</button>` : ''}
<button class="btn-item-action" data-menu="${item.id}" title="More">${svgDots}</button>
<button class="btn-item-action" data-menu="${item.id}" title="More options">${svgDots}</button>
</div>
</div>`;
}).join('');
@@ -578,17 +580,69 @@ function renderList() {
})
);
// Three-dot menu: copy username
// Copy username (dedicated button)
listEl.querySelectorAll('[data-copy-user]').forEach(btn =>
btn.addEventListener('click', e => {
e.stopPropagation();
const item = _items.find(i => i.id === parseInt(btn.dataset.copyUser));
if (item?.plain?.username) {
navigator.clipboard.writeText(item.plain.username);
btn.title = 'Copied!';
setTimeout(() => { btn.title = 'Copy username'; }, 1500);
}
})
);
// Three-dot menu: proper flyout with contextual actions
listEl.querySelectorAll('[data-menu]').forEach(btn =>
btn.addEventListener('click', e => {
e.stopPropagation();
// Close any already-open flyout first.
document.querySelectorAll('.pk-flyout').forEach(el => el.remove());
const item = _items.find(i => i.id === parseInt(btn.dataset.menu));
if (!item?.plain) return;
if (item.plain.username) {
navigator.clipboard.writeText(item.plain.username);
btn.title = 'Username copied!';
setTimeout(() => { btn.title = 'More'; }, 1500);
}
if (!item) return;
const flyout = document.createElement('div');
flyout.className = 'pk-flyout';
const menuItems = [
item.plain?.url ? {
label: 'Open URL',
icon: '<svg viewBox="0 0 24 24" fill="none" width="14" height="14"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M15 3h6v6M10 14L21 3" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>',
action: () => { chrome.tabs.create({ url: item.plain.url }); flyout.remove(); },
} : null,
item.plain?.username ? {
label: 'Copy username',
icon: '<svg viewBox="0 0 24 24" fill="none" width="14" height="14"><circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="1.7"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>',
action: () => { navigator.clipboard.writeText(item.plain.username); flyout.remove(); },
} : null,
item.plain?.password ? {
label: 'Copy password',
icon: '<svg viewBox="0 0 24 24" fill="none" width="14" height="14"><rect x="3" y="10" width="18" height="11" rx="2" stroke="currentColor" stroke-width="1.7"/><path d="M8 10V7a4 4 0 018 0v3" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>',
action: () => { navigator.clipboard.writeText(item.plain.password); flyout.remove(); },
} : null,
].filter(Boolean);
menuItems.forEach(mi => {
const row = document.createElement('button');
row.className = 'pk-flyout-item';
row.innerHTML = mi.icon + '<span>' + escHtml(mi.label) + '</span>';
row.addEventListener('click', e => { e.stopPropagation(); mi.action(); });
flyout.appendChild(row);
});
// Position flyout below the button, right-aligned.
btn.style.position = 'relative';
btn.appendChild(flyout);
// Close on any outside click.
setTimeout(() => {
document.addEventListener('click', function handler() {
flyout.remove();
document.removeEventListener('click', handler);
});
}, 0);
})
);
}