04/26 Enhanced extension functions
This commit is contained in:
+1
-1
@@ -283,5 +283,5 @@ marimo/\_static/
|
||||
marimo/\_lsp/
|
||||
**marimo**/
|
||||
README.md
|
||||
CLAUDE.md
|
||||
# CLAUDE.md
|
||||
.claude/
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -931,4 +931,45 @@ body {
|
||||
.totp-btn:hover {
|
||||
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;
|
||||
}
|
||||
@@ -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);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user