diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 643d21d913..3c52e967be 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -88,6 +88,9 @@ "generatePasswordCopied": { "message": "Generate Password (copied)" }, + "copyElementIdentifier": { + "message": "Copy Custom Field Name" + }, "noMatchingLogins": { "message": "No matching logins." }, diff --git a/src/background/contextMenus.background.ts b/src/background/contextMenus.background.ts index 19ee4421c2..f690d28320 100644 --- a/src/background/contextMenus.background.ts +++ b/src/background/contextMenus.background.ts @@ -29,6 +29,8 @@ export default class ContextMenusBackground { this.contextMenus.onClicked.addListener(async (info: any, tab: any) => { if (info.menuItemId === 'generate-password') { await this.generatePasswordToClipboard(); + } else if (info.menuItemId === 'copy-identifier') { + await this.getClickedElement(); } else if (info.parentMenuItemId === 'autofill' || info.parentMenuItemId === 'copy-username' || info.parentMenuItemId === 'copy-password' || @@ -45,6 +47,15 @@ export default class ContextMenusBackground { this.passwordGenerationService.addHistory(password); } + private async getClickedElement() { + const tab = await BrowserApi.getTabFromCurrentWindow(); + if (tab == null) { + return; + } + + BrowserApi.tabSendMessageData(tab, 'getClickedElement'); + } + private async cipherAction(info: any) { const id = info.menuItemId.split('_')[1]; if (id === 'noop') { diff --git a/src/background/main.background.ts b/src/background/main.background.ts index 2e7fa19fac..37a86412ee 100644 --- a/src/background/main.background.ts +++ b/src/background/main.background.ts @@ -512,6 +512,14 @@ export default class MainBackground { title: this.i18nService.t('generatePasswordCopied'), }); + await this.contextMenusCreate({ + type: 'normal', + id: 'copy-identifier', + parentId: 'root', + contexts: ['all'], + title: this.i18nService.t('copyElementIdentifier'), + }); + this.buildingContextMenu = false; } diff --git a/src/background/runtime.background.ts b/src/background/runtime.background.ts index 4c47d837af..a55026c757 100644 --- a/src/background/runtime.background.ts +++ b/src/background/runtime.background.ts @@ -195,6 +195,8 @@ export default class RuntimeBackground { type: 'info', }); break; + case 'getClickedElementResponse': + this.platformUtilsService.copyToClipboard(msg.identifier, { window: window }); default: break; } diff --git a/src/content/contextMenuHandler.ts b/src/content/contextMenuHandler.ts new file mode 100644 index 0000000000..6aae8ca168 --- /dev/null +++ b/src/content/contextMenuHandler.ts @@ -0,0 +1,45 @@ +const inputTags = ['input', 'textarea', 'select']; +const attributes = ['id', 'name', 'label-aria', 'placeholder']; +let clickedEl: HTMLElement = null; + +// Find the best attribute to be used as the Name for an element in a custom field. +function getClickedElementIdentifier() { + if (clickedEl == null) { + return 'Unable to identify clicked element.' + } + + if (!inputTags.includes(clickedEl.nodeName.toLowerCase())) { + return 'Invalid element type.'; + } + + for (const attr of attributes) { + const attributeValue = clickedEl.getAttribute(attr); + const selector = '[' + attr + '="' + attributeValue + '"]'; + if (!isNullOrEmpty(attributeValue) && document.querySelectorAll(selector)?.length === 1) { + return attributeValue; + } + } + return 'No unique identifier found.'; +} + +function isNullOrEmpty(s: string) { + return s == null || s === ''; +} + +// We only have access to the element that's been clicked when the context menu is first opened. +// Remember it for use later. +document.addEventListener('contextmenu', event => { + clickedEl = event.target as HTMLElement; +}); + +// Runs when the 'Copy Custom Field Name' context menu item is actually clicked. +chrome.runtime.onMessage.addListener(event => { + if (event.command === 'getClickedElement') { + const identifier = getClickedElementIdentifier(); + chrome.runtime.sendMessage({ + command: 'getClickedElementResponse', + sender: 'contextMenuHandler', + identifier: identifier, + }); + } +}); diff --git a/src/manifest.json b/src/manifest.json index 74146f72dc..cb79e00627 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -20,7 +20,8 @@ "js": [ "content/autofill.js", "content/autofiller.js", - "content/notificationBar.js" + "content/notificationBar.js", + "content/contextMenuHandler.js" ], "matches": [ "http://*/*", diff --git a/webpack.config.js b/webpack.config.js index 6b5ca45301..08918eed55 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -130,6 +130,7 @@ const config = { 'content/autofill': './src/content/autofill.js', 'content/autofiller': './src/content/autofiller.ts', 'content/notificationBar': './src/content/notificationBar.ts', + 'content/contextMenuHandler': './src/content/contextMenuHandler.ts', 'content/shortcuts': './src/content/shortcuts.ts', 'content/message_handler': './src/content/message_handler.ts', 'notification/bar': './src/notification/bar.js',