From 37c9cb8c0d589aba7550939142425af00b7761a1 Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Wed, 19 Apr 2023 18:39:03 +0800 Subject: [PATCH 01/20] Support loading SVG from files --- webpack.config.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index e4355cf..8715f16 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -10,12 +10,13 @@ module.exports = { devtool: 'cheap-module-source-map', entry: { popup: './src/popup/index.js', - background: './src/background/index.js' + background: './src/background/index.js', + content: './src/content/index.js', }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].bundle.js', - chunkFilename: '[name].[contenthash:5].chunk.js' + chunkFilename: '[name].[contenthash:5].chunk.js', }, optimization: { chunkIds: 'named', @@ -41,6 +42,10 @@ module.exports = { { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] + }, + { + test: /\.svg/, + type: 'asset/inline', } ] }, From dc80df27f0e9e9f42d6903aab66f17a08f0b4245 Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Wed, 19 Apr 2023 18:39:10 +0800 Subject: [PATCH 02/20] Start adding content script --- src/content/add_icon.svg | 1 + src/content/create_shadow_dom.js | 21 +++++++++ src/content/index.js | 73 ++++++++++++++++++++++++++++++++ src/manifest.json | 4 ++ 4 files changed, 99 insertions(+) create mode 100644 src/content/add_icon.svg create mode 100644 src/content/create_shadow_dom.js create mode 100644 src/content/index.js diff --git a/src/content/add_icon.svg b/src/content/add_icon.svg new file mode 100644 index 0000000..b8fb459 --- /dev/null +++ b/src/content/add_icon.svg @@ -0,0 +1 @@ + diff --git a/src/content/create_shadow_dom.js b/src/content/create_shadow_dom.js new file mode 100644 index 0000000..627fb72 --- /dev/null +++ b/src/content/create_shadow_dom.js @@ -0,0 +1,21 @@ +export function createShadowDom() { + const shadowElement = document.createElement('div'); + + shadowElement.style.background = "none"; + shadowElement.style.border = "none"; + shadowElement.style.outline = "none"; + shadowElement.style.boxShadow = "none"; + shadowElement.style.margin = "0"; + shadowElement.style.padding = "0"; + shadowElement.style.position = "absolute"; + shadowElement.style.top = "0"; + shadowElement.style.left = "0"; + shadowElement.style.transform = "none"; + shadowElement.style.zIndex = "2147483647"; + shadowElement.style.width = "100%"; + shadowElement.style.height = "0"; + + document.body.appendChild(shadowElement); + + return shadowElement.attachShadow({ mode: "open" }); +} diff --git a/src/content/index.js b/src/content/index.js new file mode 100644 index 0000000..bb52290 --- /dev/null +++ b/src/content/index.js @@ -0,0 +1,73 @@ +import { createShadowDom } from './create_shadow_dom'; +import addIcon from './add_icon.svg'; + +const ADD_REMOVE_BUTTON_ID = "powerlet-add-remove-button" + +const shadow = createShadowDom(); +const scripts = document.querySelectorAll('[href*="javascript:"]'); + +const style = document.createElement('style'); + +style.innerHTML = ` + #${ADD_REMOVE_BUTTON_ID} { + color: white; + background-image: linear-gradient(-30deg, #D86299 0%, #A449D6 100%); + border-radius: 13.5px; + border: none; + cursor: pointer; + display: flex; + align-items: center; + padding: 5px 10px; + padding-right: 14px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + font-size: 15px; + font-weight: 500; + position: absolute; + top: 10px; + left: 10px; + gap: 5px; + } +`; + +const button = document.createElement("button"); +const icon = document.createElement('img'); + +icon.src = addIcon; + +button.id = ADD_REMOVE_BUTTON_ID; +button.textContent = "Add Bookmarklet"; + +button.prepend(icon); +shadow.appendChild(style); +shadow.appendChild(button); + +// Array.from(scripts).forEach((script) => { +// const href = script.getAttribute('href'); + +// script.addEventListener('mouseenter', () => { +// const existingAddRemoveButton = document.getElementById(ADD_REMOVE_BUTTON_ID); + +// if (existingAddRemoveButton) { +// document.body.removeChild(existingAddRemoveButton); +// } + +// const rect = script.getBoundingClientRect(); +// const button = document.createElement("button"); + +// button.id = ADD_REMOVE_BUTTON_ID; +// button.style.border = "none"; +// button.style.background = "#D86299" +// button.style.borderRadius = "8px"; +// button.style.fontFamily = "sans-serif"; +// button.style.padding = "2px 4px"; +// button.style.margin = "0"; +// button.style.color = "white"; +// button.style.left = `${rect.x + rect.width}px`; +// button.style.top = `${rect.y - rect.height}px`; +// button.style.position = "fixed"; +// button.textContent = "+ Add to Powerlet" + +// shadow.appendChild(button); +// }); +// }); + diff --git a/src/manifest.json b/src/manifest.json index 355afe4..bf1ca4d 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -9,6 +9,10 @@ "scripts": ["background.bundle.js"], "persistent": false }, + "content_scripts": [{ + "js": ["content.bundle.js"], + "matches": ["http://*/*", "https://*/*"] + }], "browser_action": { "default_popup": "popup.html", "default_icon": { From f1600dddc5c47bc4184603c07a9d266b9493089d Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Wed, 19 Apr 2023 22:25:44 +0800 Subject: [PATCH 03/20] Add button styles --- ...dow_dom.js => create_shadow_dom_inside.js} | 10 +- src/content/index.js | 114 +++++++++--------- 2 files changed, 63 insertions(+), 61 deletions(-) rename src/content/{create_shadow_dom.js => create_shadow_dom_inside.js} (68%) diff --git a/src/content/create_shadow_dom.js b/src/content/create_shadow_dom_inside.js similarity index 68% rename from src/content/create_shadow_dom.js rename to src/content/create_shadow_dom_inside.js index 627fb72..d9077c0 100644 --- a/src/content/create_shadow_dom.js +++ b/src/content/create_shadow_dom_inside.js @@ -1,4 +1,4 @@ -export function createShadowDom() { +export function createShadowDomInside(target) { const shadowElement = document.createElement('div'); shadowElement.style.background = "none"; @@ -7,15 +7,17 @@ export function createShadowDom() { shadowElement.style.boxShadow = "none"; shadowElement.style.margin = "0"; shadowElement.style.padding = "0"; - shadowElement.style.position = "absolute"; shadowElement.style.top = "0"; shadowElement.style.left = "0"; shadowElement.style.transform = "none"; shadowElement.style.zIndex = "2147483647"; - shadowElement.style.width = "100%"; + shadowElement.style.width = "0"; shadowElement.style.height = "0"; + shadowElement.style.minWidth = "0"; + shadowElement.style.minHeight = "0"; + shadowElement.style.display = "inline-block"; - document.body.appendChild(shadowElement); + target.appendChild(shadowElement); return shadowElement.attachShadow({ mode: "open" }); } diff --git a/src/content/index.js b/src/content/index.js index bb52290..541887e 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -1,73 +1,73 @@ -import { createShadowDom } from './create_shadow_dom'; +import { createShadowDomInside } from './create_shadow_dom_inside'; import addIcon from './add_icon.svg'; -const ADD_REMOVE_BUTTON_ID = "powerlet-add-remove-button" +const BUTTON_ID = "powerlet-add-remove-button" +const LABEL_ID = "powerlet-add-remove-button__label" -const shadow = createShadowDom(); -const scripts = document.querySelectorAll('[href*="javascript:"]'); +const scripts = document.body.querySelectorAll('[href*="javascript:"]'); -const style = document.createElement('style'); +Array.from(scripts).forEach((script) => { + const shadow = createShadowDomInside(script); + const style = document.createElement('style'); -style.innerHTML = ` - #${ADD_REMOVE_BUTTON_ID} { - color: white; - background-image: linear-gradient(-30deg, #D86299 0%, #A449D6 100%); - border-radius: 13.5px; - border: none; - cursor: pointer; - display: flex; - align-items: center; - padding: 5px 10px; - padding-right: 14px; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - font-size: 15px; - font-weight: 500; - position: absolute; - top: 10px; - left: 10px; - gap: 5px; - } -`; + const button = document.createElement('button'); + const icon = document.createElement('img'); + const label = document.createElement('span'); -const button = document.createElement("button"); -const icon = document.createElement('img'); + icon.src = addIcon; -icon.src = addIcon; + button.id = BUTTON_ID; + button.style.position = "relative"; -button.id = ADD_REMOVE_BUTTON_ID; -button.textContent = "Add Bookmarklet"; + label.id = LABEL_ID; + label.textContent = "Add Bookmarklet"; -button.prepend(icon); -shadow.appendChild(style); -shadow.appendChild(button); + style.innerHTML = ` + #${BUTTON_ID} { + color: white; + background-image: linear-gradient(-30deg, #D86299 0%, #A449D6 100%); + border-radius: 13.5px; + border: none; + cursor: pointer; + display: flex; + align-items: center; + padding: 5px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + font-size: 15px; + font-weight: 500; + position: absolute; + width: 27px; + height: 27px; + transform: translateY(-50%); + transition: all; + gap: 5px; + } -// Array.from(scripts).forEach((script) => { -// const href = script.getAttribute('href'); + #${BUTTON_ID} img { + flex-shrink: 0; + } -// script.addEventListener('mouseenter', () => { -// const existingAddRemoveButton = document.getElementById(ADD_REMOVE_BUTTON_ID); + #${BUTTON_ID}:hover { + width: min-content; + } -// if (existingAddRemoveButton) { -// document.body.removeChild(existingAddRemoveButton); -// } + #${LABEL_ID} { + margin-right: 3px; + white-space: nowrap; + opacity: 0; + visibility: hidden; + } -// const rect = script.getBoundingClientRect(); -// const button = document.createElement("button"); + #${BUTTON_ID}:hover #${LABEL_ID} { + opacity: 1; + visibility: visible; + } + `; -// button.id = ADD_REMOVE_BUTTON_ID; -// button.style.border = "none"; -// button.style.background = "#D86299" -// button.style.borderRadius = "8px"; -// button.style.fontFamily = "sans-serif"; -// button.style.padding = "2px 4px"; -// button.style.margin = "0"; -// button.style.color = "white"; -// button.style.left = `${rect.x + rect.width}px`; -// button.style.top = `${rect.y - rect.height}px`; -// button.style.position = "fixed"; -// button.textContent = "+ Add to Powerlet" + button.append(icon); + button.append(label); -// shadow.appendChild(button); -// }); -// }); + shadow.appendChild(style); + shadow.appendChild(button); +}); From af8c894b49e229ec3706c0a88a3856e95d550811 Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Wed, 19 Apr 2023 23:36:00 +0800 Subject: [PATCH 04/20] Add adding functionality --- src/background/index.js | 7 +++ src/content/add_icon.svg | 4 +- src/content/create_shadow_dom_inside.js | 1 + src/content/index.js | 79 +++++++++++++++++-------- 4 files changed, 66 insertions(+), 25 deletions(-) diff --git a/src/background/index.js b/src/background/index.js index 6142148..3dfaae9 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -4,3 +4,10 @@ const renderIcon = createIconRenderer(); setInterval(renderIcon, 1000); renderIcon(); + +chrome.runtime.onMessage.addListener((message) => { + chrome.bookmarks.create({ + title: message.payload.title, + url: message.payload.url + }); +}); diff --git a/src/content/add_icon.svg b/src/content/add_icon.svg index b8fb459..b493266 100644 --- a/src/content/add_icon.svg +++ b/src/content/add_icon.svg @@ -1 +1,3 @@ - + + + diff --git a/src/content/create_shadow_dom_inside.js b/src/content/create_shadow_dom_inside.js index d9077c0..8d56183 100644 --- a/src/content/create_shadow_dom_inside.js +++ b/src/content/create_shadow_dom_inside.js @@ -15,6 +15,7 @@ export function createShadowDomInside(target) { shadowElement.style.height = "0"; shadowElement.style.minWidth = "0"; shadowElement.style.minHeight = "0"; + shadowElement.style.position = "relative"; shadowElement.style.display = "inline-block"; target.appendChild(shadowElement); diff --git a/src/content/index.js b/src/content/index.js index 541887e..6d79e19 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -1,29 +1,54 @@ import { createShadowDomInside } from './create_shadow_dom_inside'; + import addIcon from './add_icon.svg'; -const BUTTON_ID = "powerlet-add-remove-button" -const LABEL_ID = "powerlet-add-remove-button__label" +const POWERLET_BUTTON = "powerlet-button" +const MANAGE_BUTTON = "manage-button" +const LABEL_ID = "add-remove-button__label" const scripts = document.body.querySelectorAll('[href*="javascript:"]'); Array.from(scripts).forEach((script) => { + const title = script.textContent; const shadow = createShadowDomInside(script); const style = document.createElement('style'); + const powerletButton = document.createElement('button'); + const powerletIcon = document.createElement('img'); const button = document.createElement('button'); const icon = document.createElement('img'); const label = document.createElement('span'); - icon.src = addIcon; + powerletButton.classList.add(POWERLET_BUTTON); - button.id = BUTTON_ID; - button.style.position = "relative"; + button.classList.add(MANAGE_BUTTON); - label.id = LABEL_ID; + label.classList.add(LABEL_ID); label.textContent = "Add Bookmarklet"; + powerletIcon.src = addIcon; + icon.src = addIcon; + style.innerHTML = ` - #${BUTTON_ID} { + .${POWERLET_BUTTON} { + background-image: linear-gradient(-30deg, #D86299 0%, #A449D6 100%); + border-radius: 100px; + border: none; + cursor: pointer; + display: flex; + width: 19px; + height: 19px; + align-items: center; + justify-content: center; + position: absolute; + transform: translateY(-100%); + } + + .${POWERLET_BUTTON}:hover ~ .${MANAGE_BUTTON} { + visibility: visible; + } + + .${MANAGE_BUTTON} { color: white; background-image: linear-gradient(-30deg, #D86299 0%, #A449D6 100%); border-radius: 13.5px; @@ -36,38 +61,44 @@ Array.from(scripts).forEach((script) => { font-size: 15px; font-weight: 500; position: absolute; - width: 27px; - height: 27px; - transform: translateY(-50%); - transition: all; + transform: translateY(-100%); + top: 0; + left: 0; gap: 5px; + visibility: hidden; } - #${BUTTON_ID} img { - flex-shrink: 0; + .${MANAGE_BUTTON}:hover { + visibility: visible; } - #${BUTTON_ID}:hover { - width: min-content; + .${MANAGE_BUTTON} img { + flex-shrink: 0; } - #${LABEL_ID} { - margin-right: 3px; + .${LABEL_ID} { + margin-right: 4px; white-space: nowrap; - opacity: 0; - visibility: hidden; - } - - #${BUTTON_ID}:hover #${LABEL_ID} { - opacity: 1; - visibility: visible; } `; + button.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + + chrome.runtime.sendMessage({ action: "create_bookmark", payload: { + title, + url: script.getAttribute('href'), + }}); + }); + + powerletButton.append(powerletIcon); + button.append(icon); button.append(label); shadow.appendChild(style); + shadow.appendChild(powerletButton); shadow.appendChild(button); }); From f36e856e6a08c4c8e7fd46018bdff3c185263074 Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Wed, 19 Apr 2023 23:36:07 +0800 Subject: [PATCH 05/20] Format --- src/content/create_shadow_dom_inside.js | 34 ++++++++++++------------- src/content/index.js | 20 ++++++++------- src/manifest.json | 10 +++++--- webpack.config.js | 6 ++--- 4 files changed, 37 insertions(+), 33 deletions(-) diff --git a/src/content/create_shadow_dom_inside.js b/src/content/create_shadow_dom_inside.js index 8d56183..e4b09dd 100644 --- a/src/content/create_shadow_dom_inside.js +++ b/src/content/create_shadow_dom_inside.js @@ -1,24 +1,24 @@ export function createShadowDomInside(target) { const shadowElement = document.createElement('div'); - shadowElement.style.background = "none"; - shadowElement.style.border = "none"; - shadowElement.style.outline = "none"; - shadowElement.style.boxShadow = "none"; - shadowElement.style.margin = "0"; - shadowElement.style.padding = "0"; - shadowElement.style.top = "0"; - shadowElement.style.left = "0"; - shadowElement.style.transform = "none"; - shadowElement.style.zIndex = "2147483647"; - shadowElement.style.width = "0"; - shadowElement.style.height = "0"; - shadowElement.style.minWidth = "0"; - shadowElement.style.minHeight = "0"; - shadowElement.style.position = "relative"; - shadowElement.style.display = "inline-block"; + shadowElement.style.background = 'none'; + shadowElement.style.border = 'none'; + shadowElement.style.outline = 'none'; + shadowElement.style.boxShadow = 'none'; + shadowElement.style.margin = '0'; + shadowElement.style.padding = '0'; + shadowElement.style.top = '0'; + shadowElement.style.left = '0'; + shadowElement.style.transform = 'none'; + shadowElement.style.zIndex = '2147483647'; + shadowElement.style.width = '0'; + shadowElement.style.height = '0'; + shadowElement.style.minWidth = '0'; + shadowElement.style.minHeight = '0'; + shadowElement.style.position = 'relative'; + shadowElement.style.display = 'inline-block'; target.appendChild(shadowElement); - return shadowElement.attachShadow({ mode: "open" }); + return shadowElement.attachShadow({ mode: 'open' }); } diff --git a/src/content/index.js b/src/content/index.js index 6d79e19..3d4a9a3 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -2,9 +2,9 @@ import { createShadowDomInside } from './create_shadow_dom_inside'; import addIcon from './add_icon.svg'; -const POWERLET_BUTTON = "powerlet-button" -const MANAGE_BUTTON = "manage-button" -const LABEL_ID = "add-remove-button__label" +const POWERLET_BUTTON = 'powerlet-button'; +const MANAGE_BUTTON = 'manage-button'; +const LABEL_ID = 'add-remove-button__label'; const scripts = document.body.querySelectorAll('[href*="javascript:"]'); @@ -24,7 +24,7 @@ Array.from(scripts).forEach((script) => { button.classList.add(MANAGE_BUTTON); label.classList.add(LABEL_ID); - label.textContent = "Add Bookmarklet"; + label.textContent = 'Add Bookmarklet'; powerletIcon.src = addIcon; icon.src = addIcon; @@ -86,10 +86,13 @@ Array.from(scripts).forEach((script) => { event.preventDefault(); event.stopPropagation(); - chrome.runtime.sendMessage({ action: "create_bookmark", payload: { - title, - url: script.getAttribute('href'), - }}); + chrome.runtime.sendMessage({ + action: 'create_bookmark', + payload: { + title, + url: script.getAttribute('href') + } + }); }); powerletButton.append(powerletIcon); @@ -101,4 +104,3 @@ Array.from(scripts).forEach((script) => { shadow.appendChild(powerletButton); shadow.appendChild(button); }); - diff --git a/src/manifest.json b/src/manifest.json index bf1ca4d..e08a132 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -9,10 +9,12 @@ "scripts": ["background.bundle.js"], "persistent": false }, - "content_scripts": [{ - "js": ["content.bundle.js"], - "matches": ["http://*/*", "https://*/*"] - }], + "content_scripts": [ + { + "js": ["content.bundle.js"], + "matches": ["http://*/*", "https://*/*"] + } + ], "browser_action": { "default_popup": "popup.html", "default_icon": { diff --git a/webpack.config.js b/webpack.config.js index 8715f16..fb236da 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -11,12 +11,12 @@ module.exports = { entry: { popup: './src/popup/index.js', background: './src/background/index.js', - content: './src/content/index.js', + content: './src/content/index.js' }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].bundle.js', - chunkFilename: '[name].[contenthash:5].chunk.js', + chunkFilename: '[name].[contenthash:5].chunk.js' }, optimization: { chunkIds: 'named', @@ -45,7 +45,7 @@ module.exports = { }, { test: /\.svg/, - type: 'asset/inline', + type: 'asset/inline' } ] }, From 3e881f9a5903436826469082632cfe5ef7fc81fb Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Thu, 20 Apr 2023 16:59:49 +0800 Subject: [PATCH 06/20] Add content script to example page --- src/content/check_icon.svg | 1 + src/content/index.js | 83 ++++++++++++++++++++------------------ src/pages/examples.html | 2 +- src/pages/examples.js | 2 + webpack.config.js | 3 +- 5 files changed, 50 insertions(+), 41 deletions(-) create mode 100644 src/content/check_icon.svg diff --git a/src/content/check_icon.svg b/src/content/check_icon.svg new file mode 100644 index 0000000..fddb0af --- /dev/null +++ b/src/content/check_icon.svg @@ -0,0 +1 @@ + diff --git a/src/content/index.js b/src/content/index.js index 3d4a9a3..4b09892 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -1,10 +1,11 @@ import { createShadowDomInside } from './create_shadow_dom_inside'; import addIcon from './add_icon.svg'; +import checkIcon from './check_icon.svg'; -const POWERLET_BUTTON = 'powerlet-button'; -const MANAGE_BUTTON = 'manage-button'; -const LABEL_ID = 'add-remove-button__label'; +const CONTAINER_CLASS_NAME = 'powerlet-button'; +const MANAGE_BUTTON_CLASS_NAME = 'manage-button'; +const LABEL_CLASS_NAME = 'add-remove-button__label'; const scripts = document.body.querySelectorAll('[href*="javascript:"]'); @@ -13,72 +14,74 @@ Array.from(scripts).forEach((script) => { const shadow = createShadowDomInside(script); const style = document.createElement('style'); - const powerletButton = document.createElement('button'); - const powerletIcon = document.createElement('img'); + const container = document.createElement('div'); const button = document.createElement('button'); const icon = document.createElement('img'); const label = document.createElement('span'); - powerletButton.classList.add(POWERLET_BUTTON); + container.classList.add(CONTAINER_CLASS_NAME); + button.classList.add(MANAGE_BUTTON_CLASS_NAME); - button.classList.add(MANAGE_BUTTON); - - label.classList.add(LABEL_ID); + label.classList.add(LABEL_CLASS_NAME); label.textContent = 'Add Bookmarklet'; - powerletIcon.src = addIcon; icon.src = addIcon; style.innerHTML = ` - .${POWERLET_BUTTON} { - background-image: linear-gradient(-30deg, #D86299 0%, #A449D6 100%); - border-radius: 100px; - border: none; - cursor: pointer; - display: flex; - width: 19px; - height: 19px; - align-items: center; - justify-content: center; + .${CONTAINER_CLASS_NAME} { + width: 28px; + height: 28px; position: absolute; - transform: translateY(-100%); + transform: translate(-30%, -100%); + top: 50%; + left: 50%; } - .${POWERLET_BUTTON}:hover ~ .${MANAGE_BUTTON} { - visibility: visible; + .${CONTAINER_CLASS_NAME}:hover .${MANAGE_BUTTON_CLASS_NAME} { + padding: 5px; + width: auto; + height: auto; + transform: translate(-12.5px, -50%); } - .${MANAGE_BUTTON} { + .${CONTAINER_CLASS_NAME}:hover .${LABEL_CLASS_NAME} { + display: block; + } + + .${MANAGE_BUTTON_CLASS_NAME} { color: white; background-image: linear-gradient(-30deg, #D86299 0%, #A449D6 100%); - border-radius: 13.5px; + border-radius: 100px; border: none; cursor: pointer; display: flex; align-items: center; - padding: 5px; + justify-content: center; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; font-size: 15px; font-weight: 500; position: absolute; - transform: translateY(-100%); - top: 0; - left: 0; + top: 50%; + left: 50%; + transform: translate(-9px, -50%); gap: 5px; - visibility: hidden; + + width: 19px; + height: 19px; } - .${MANAGE_BUTTON}:hover { - visibility: visible; + .${MANAGE_BUTTON_CLASS_NAME}--added { + background: green; } - .${MANAGE_BUTTON} img { + .${MANAGE_BUTTON_CLASS_NAME} img { flex-shrink: 0; } - .${LABEL_ID} { + .${LABEL_CLASS_NAME} { margin-right: 4px; white-space: nowrap; + display: none; } `; @@ -89,18 +92,20 @@ Array.from(scripts).forEach((script) => { chrome.runtime.sendMessage({ action: 'create_bookmark', payload: { - title, + title: title.trim(), url: script.getAttribute('href') } }); - }); - powerletButton.append(powerletIcon); + label.textContent = 'Added!'; + button.classList.add(`${MANAGE_BUTTON_CLASS_NAME}--added`); + icon.src = checkIcon; + }); button.append(icon); button.append(label); + container.append(button); shadow.appendChild(style); - shadow.appendChild(powerletButton); - shadow.appendChild(button); + shadow.appendChild(container); }); diff --git a/src/pages/examples.html b/src/pages/examples.html index 2d5c533..92a0c6a 100644 --- a/src/pages/examples.html +++ b/src/pages/examples.html @@ -103,6 +103,6 @@

- + diff --git a/src/pages/examples.js b/src/pages/examples.js index dd5352e..990e0d3 100644 --- a/src/pages/examples.js +++ b/src/pages/examples.js @@ -1,3 +1,5 @@ +import '../content/index.js'; + const translationElements = document.querySelectorAll('[data-translation-key]'); Array.from(translationElements).forEach((element) => { diff --git a/webpack.config.js b/webpack.config.js index fb236da..9b1d511 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -11,7 +11,8 @@ module.exports = { entry: { popup: './src/popup/index.js', background: './src/background/index.js', - content: './src/content/index.js' + content: './src/content/index.js', + examples: './src/pages/examples.js' }, output: { path: path.resolve(__dirname, 'dist'), From 17604541c09713296ddeb22a705ead89476261c4 Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Thu, 20 Apr 2023 17:11:37 +0800 Subject: [PATCH 07/20] Add add animation --- src/content/index.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/content/index.js b/src/content/index.js index 4b09892..17245e0 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -72,6 +72,7 @@ Array.from(scripts).forEach((script) => { .${MANAGE_BUTTON_CLASS_NAME}--added { background: green; + animation: added 600ms ease; } .${MANAGE_BUTTON_CLASS_NAME} img { @@ -83,6 +84,16 @@ Array.from(scripts).forEach((script) => { white-space: nowrap; display: none; } + + @keyframes added { + from { + box-shadow: 0 0 0 rgba(0, 255, 0, 1); + } + + to { + box-shadow: 0 0 50px rgba(0, 255, 0, 0); + } + } `; button.addEventListener('click', (event) => { From 76b4f123ba1730c0e52e2813a17939283a7cf66b Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Thu, 20 Apr 2023 17:16:53 +0800 Subject: [PATCH 08/20] Add error check --- src/content/index.js | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/content/index.js b/src/content/index.js index 17245e0..121fd48 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -100,13 +100,20 @@ Array.from(scripts).forEach((script) => { event.preventDefault(); event.stopPropagation(); - chrome.runtime.sendMessage({ - action: 'create_bookmark', - payload: { - title: title.trim(), - url: script.getAttribute('href') - } - }); + try { + chrome.runtime.sendMessage({ + action: 'create_bookmark', + payload: { + title: title.trim(), + url: script.getAttribute('href') + } + }); + } catch (error) { + alert( + 'Powerlet: Something went wrong when adding bookmarklet. Please reload the page and try again.' + ); + return; + } label.textContent = 'Added!'; button.classList.add(`${MANAGE_BUTTON_CLASS_NAME}--added`); From 52e026428e884f9b6a08b70a400236b3f917442c Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Thu, 20 Apr 2023 17:34:12 +0800 Subject: [PATCH 09/20] Check for common or untitled scripts --- src/content/create_shadow_dom_inside.js | 1 - src/content/index.js | 12 +++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/content/create_shadow_dom_inside.js b/src/content/create_shadow_dom_inside.js index e4b09dd..b49e66f 100644 --- a/src/content/create_shadow_dom_inside.js +++ b/src/content/create_shadow_dom_inside.js @@ -10,7 +10,6 @@ export function createShadowDomInside(target) { shadowElement.style.top = '0'; shadowElement.style.left = '0'; shadowElement.style.transform = 'none'; - shadowElement.style.zIndex = '2147483647'; shadowElement.style.width = '0'; shadowElement.style.height = '0'; shadowElement.style.minWidth = '0'; diff --git a/src/content/index.js b/src/content/index.js index 121fd48..ff8ae43 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -11,6 +11,16 @@ const scripts = document.body.querySelectorAll('[href*="javascript:"]'); Array.from(scripts).forEach((script) => { const title = script.textContent; + const href = script.getAttribute('href'); + + if (href.trim().replaceAll(/\s/g, '') === 'javascript:void(0)') { + return; + } + + if (title.trim() === '') { + return; + } + const shadow = createShadowDomInside(script); const style = document.createElement('style'); @@ -105,7 +115,7 @@ Array.from(scripts).forEach((script) => { action: 'create_bookmark', payload: { title: title.trim(), - url: script.getAttribute('href') + url: href } }); } catch (error) { From fe6a8186c830f7c345a2a2d5dd35e2a9cb9d4773 Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Thu, 20 Apr 2023 18:23:11 +0800 Subject: [PATCH 10/20] Add add button to description --- src/_locales/en/messages.json | 2 +- src/content/index.js | 5 ++++- src/pages/examples.html | 21 ++++++++++++++++----- src/pages/examples.js | 7 ++++++- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 0a598aa..095e6c0 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -32,7 +32,7 @@ "description": "Title used on the example scripts page" }, "getting_started_subtitle": { - "message": "To add a script, drag it to your Bookmarks Bar.", + "message": "To add a script, drag it to your Bookmarks Bar or press the %r button", "description": "Subtitle used to explain how to add bookmarklets to the bookmarkbar" }, "edit_script_title": { diff --git a/src/content/index.js b/src/content/index.js index ff8ae43..d543cee 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -75,7 +75,6 @@ Array.from(scripts).forEach((script) => { left: 50%; transform: translate(-9px, -50%); gap: 5px; - width: 19px; height: 19px; } @@ -87,12 +86,16 @@ Array.from(scripts).forEach((script) => { .${MANAGE_BUTTON_CLASS_NAME} img { flex-shrink: 0; + user-select: none; + pointer-events: none; } .${LABEL_CLASS_NAME} { margin-right: 4px; white-space: nowrap; display: none; + user-select: none; + pointer-events: none; } @keyframes added { diff --git a/src/pages/examples.html b/src/pages/examples.html index 92a0c6a..58bfaf6 100644 --- a/src/pages/examples.html +++ b/src/pages/examples.html @@ -36,16 +36,27 @@ display: inline-block; font-size: 16px; font-weight: 500; - padding: 12px 24px; + padding: 7px; text-decoration: none; - margin-bottom: 20px; - margin-right: 10px; + margin-right: 20px; + margin-bottom: 22px; } .bookmarklet:hover { color: #2A7CEA; } + .add-button { + background-image: linear-gradient(-30deg, #D86299 0%, #A449D6 100%); + border-radius: 100px; + display: inline-flex; + align-items: center; + justify-content: center; + width: 19px; + height: 19px; + margin: 0 3px; + } + @media(prefers-color-scheme: dark) { body { background: #202124; @@ -66,8 +77,8 @@

Getting Started

-

- To add a script, drag it to your Bookmarks Bar. +

+ To add a script, drag it to your Bookmarks Bar!

diff --git a/src/pages/examples.js b/src/pages/examples.js index 990e0d3..0b21862 100644 --- a/src/pages/examples.js +++ b/src/pages/examples.js @@ -4,9 +4,14 @@ const translationElements = document.querySelectorAll('[data-translation-key]'); Array.from(translationElements).forEach((element) => { const key = element.getAttribute('data-translation-key'); + const replacement = element.getAttribute('data-translation-replacement'); const translation = chrome.i18n.getMessage(key); if (translation) { - element.textContent = translation; + if (replacement) { + element.innerHTML = translation.replace('%r', replacement); + } else { + element.textContent = translation; + } } }); From 2c60c3e36e67b5d94e0d844c81e3cef21ed7341b Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Thu, 20 Apr 2023 19:01:28 +0800 Subject: [PATCH 11/20] Darken drop shadow animation --- src/content/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/content/index.js b/src/content/index.js index d543cee..2b5744d 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -100,11 +100,11 @@ Array.from(scripts).forEach((script) => { @keyframes added { from { - box-shadow: 0 0 0 rgba(0, 255, 0, 1); + box-shadow: 0 0 0 rgba(0, 100, 0, 1); } to { - box-shadow: 0 0 50px rgba(0, 255, 0, 0); + box-shadow: 0 0 50px rgba(0, 100, 0, 0); } } `; From 76c31f3b60bf95fa93d66084b9c677d4b61f307b Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Fri, 21 Apr 2023 01:10:53 +0800 Subject: [PATCH 12/20] Add more checks for false positives --- src/content/create_shadow_dom_inside.js | 2 +- src/content/index.js | 33 ++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/content/create_shadow_dom_inside.js b/src/content/create_shadow_dom_inside.js index b49e66f..8d264ee 100644 --- a/src/content/create_shadow_dom_inside.js +++ b/src/content/create_shadow_dom_inside.js @@ -15,7 +15,7 @@ export function createShadowDomInside(target) { shadowElement.style.minWidth = '0'; shadowElement.style.minHeight = '0'; shadowElement.style.position = 'relative'; - shadowElement.style.display = 'inline-block'; + shadowElement.style.display = 'inline'; target.appendChild(shadowElement); diff --git a/src/content/index.js b/src/content/index.js index 2b5744d..a86c7d6 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -12,15 +12,42 @@ const scripts = document.body.querySelectorAll('[href*="javascript:"]'); Array.from(scripts).forEach((script) => { const title = script.textContent; const href = script.getAttribute('href'); + const cleanHref = href.trim().replaceAll(' ', '').replaceAll(';', ''); + const classList = Array.from(script.classList); - if (href.trim().replaceAll(/\s/g, '') === 'javascript:void(0)') { + // A lot of random buttons on websites include `void` code to prevent the + // default behaviour of links. These should not be considered bookmarklets. + if (cleanHref === 'javascript:void(0)' || cleanHref === 'javascript:') { return; } + // Empty links are not considered bookmarklets, such as icon buttons. if (title.trim() === '') { return; } + // A single character, like "x", is something used for buttons. Example on + // this cookie banner: https://www.universityofgalway.ie/t4training/bookmarklets.html + if (title.length === 1) { + return; + } + + // To avoid links that are buttons with non-bookmarklet scripts, check the + // class name for any reference for "button" but without "bookmarklet". + // Currently this basic heuristic is to avoid highlighting the "Add to Card" + // and "Add to Wishlist" buttons on the Steam store page. But also keep + // it working with things like: https://www.addtoany.com/services/pinboard_button + // which have a button class name but also reference bookmarklet. + for (const className of classList) { + const includesButtonWord = + className.includes('btn') || className.includes('button'); + const includesBookmarkletName = className.includes('bookmarklet'); + + if (includesButtonWord && !includesBookmarkletName) { + return; + } + } + const shadow = createShadowDomInside(script); const style = document.createElement('style'); @@ -90,6 +117,10 @@ Array.from(scripts).forEach((script) => { pointer-events: none; } + .${MANAGE_BUTTON_CLASS_NAME}--added img { + transform: translateY(1px); + } + .${LABEL_CLASS_NAME} { margin-right: 4px; white-space: nowrap; From ddd9b32cbf0bd7a0955e298b4656c298d07a15c3 Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Fri, 21 Apr 2023 23:02:12 +0800 Subject: [PATCH 13/20] Move logic into function and emit console error when there is an error --- src/content/index.js | 87 +++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/src/content/index.js b/src/content/index.js index a86c7d6..efa0efc 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -9,46 +9,8 @@ const LABEL_CLASS_NAME = 'add-remove-button__label'; const scripts = document.body.querySelectorAll('[href*="javascript:"]'); -Array.from(scripts).forEach((script) => { - const title = script.textContent; - const href = script.getAttribute('href'); - const cleanHref = href.trim().replaceAll(' ', '').replaceAll(';', ''); - const classList = Array.from(script.classList); - - // A lot of random buttons on websites include `void` code to prevent the - // default behaviour of links. These should not be considered bookmarklets. - if (cleanHref === 'javascript:void(0)' || cleanHref === 'javascript:') { - return; - } - - // Empty links are not considered bookmarklets, such as icon buttons. - if (title.trim() === '') { - return; - } - - // A single character, like "x", is something used for buttons. Example on - // this cookie banner: https://www.universityofgalway.ie/t4training/bookmarklets.html - if (title.length === 1) { - return; - } - - // To avoid links that are buttons with non-bookmarklet scripts, check the - // class name for any reference for "button" but without "bookmarklet". - // Currently this basic heuristic is to avoid highlighting the "Add to Card" - // and "Add to Wishlist" buttons on the Steam store page. But also keep - // it working with things like: https://www.addtoany.com/services/pinboard_button - // which have a button class name but also reference bookmarklet. - for (const className of classList) { - const includesButtonWord = - className.includes('btn') || className.includes('button'); - const includesBookmarkletName = className.includes('bookmarklet'); - - if (includesButtonWord && !includesBookmarkletName) { - return; - } - } - - const shadow = createShadowDomInside(script); +function showButtonInside(target, title, code) { + const shadow = createShadowDomInside(target); const style = document.createElement('style'); const container = document.createElement('div'); @@ -149,13 +111,14 @@ Array.from(scripts).forEach((script) => { action: 'create_bookmark', payload: { title: title.trim(), - url: href + url: code.trim() } }); } catch (error) { alert( 'Powerlet: Something went wrong when adding bookmarklet. Please reload the page and try again.' ); + console.error(error); return; } @@ -170,4 +133,46 @@ Array.from(scripts).forEach((script) => { shadow.appendChild(style); shadow.appendChild(container); +} + +Array.from(scripts).forEach((script) => { + const title = script.textContent; + const href = script.getAttribute('href'); + const cleanHref = href.trim().replaceAll(' ', '').replaceAll(';', ''); + const classList = Array.from(script.classList); + + // A lot of random buttons on websites include `void` code to prevent the + // default behaviour of links. These should not be considered bookmarklets. + if (cleanHref === 'javascript:void(0)' || cleanHref === 'javascript:') { + return; + } + + // Empty links are not considered bookmarklets, such as icon buttons. + if (title.trim() === '') { + return; + } + + // A single character, like "x", is something used for buttons. Example on + // this cookie banner: https://www.universityofgalway.ie/t4training/bookmarklets.html + if (title.length === 1) { + return; + } + + // To avoid links that are buttons with non-bookmarklet scripts, check the + // class name for any reference for "button" but without "bookmarklet". + // Currently this basic heuristic is to avoid highlighting the "Add to Card" + // and "Add to Wishlist" buttons on the Steam store page. But also keep + // it working with things like: https://www.addtoany.com/services/pinboard_button + // which have a button class name but also reference bookmarklet. + for (const className of classList) { + const includesButtonWord = + className.includes('btn') || className.includes('button'); + const includesBookmarkletName = className.includes('bookmarklet'); + + if (includesButtonWord && !includesBookmarkletName) { + return; + } + } + + showButtonInside(script, title, href); }); From 28ce6bb995199127aeb82ebae90c9b8569fef0a1 Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Fri, 21 Apr 2023 23:06:12 +0800 Subject: [PATCH 14/20] Move function into own file --- src/content/index.js | 195 ++++------------------ src/content/show_button_inside_element.js | 134 +++++++++++++++ 2 files changed, 167 insertions(+), 162 deletions(-) create mode 100644 src/content/show_button_inside_element.js diff --git a/src/content/index.js b/src/content/index.js index efa0efc..4026788 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -1,178 +1,49 @@ -import { createShadowDomInside } from './create_shadow_dom_inside'; +import { showButtonInsideElement } from './show_button_inside_element'; -import addIcon from './add_icon.svg'; -import checkIcon from './check_icon.svg'; +function main() { + const scripts = document.body.querySelectorAll('[href*="javascript:"]'); -const CONTAINER_CLASS_NAME = 'powerlet-button'; -const MANAGE_BUTTON_CLASS_NAME = 'manage-button'; -const LABEL_CLASS_NAME = 'add-remove-button__label'; + Array.from(scripts).forEach((script) => { + const title = script.textContent; + const href = script.getAttribute('href'); + const cleanHref = href.trim().replaceAll(' ', '').replaceAll(';', ''); + const classList = Array.from(script.classList); -const scripts = document.body.querySelectorAll('[href*="javascript:"]'); - -function showButtonInside(target, title, code) { - const shadow = createShadowDomInside(target); - const style = document.createElement('style'); - - const container = document.createElement('div'); - const button = document.createElement('button'); - const icon = document.createElement('img'); - const label = document.createElement('span'); - - container.classList.add(CONTAINER_CLASS_NAME); - button.classList.add(MANAGE_BUTTON_CLASS_NAME); - - label.classList.add(LABEL_CLASS_NAME); - label.textContent = 'Add Bookmarklet'; - - icon.src = addIcon; - - style.innerHTML = ` - .${CONTAINER_CLASS_NAME} { - width: 28px; - height: 28px; - position: absolute; - transform: translate(-30%, -100%); - top: 50%; - left: 50%; - } - - .${CONTAINER_CLASS_NAME}:hover .${MANAGE_BUTTON_CLASS_NAME} { - padding: 5px; - width: auto; - height: auto; - transform: translate(-12.5px, -50%); - } - - .${CONTAINER_CLASS_NAME}:hover .${LABEL_CLASS_NAME} { - display: block; - } - - .${MANAGE_BUTTON_CLASS_NAME} { - color: white; - background-image: linear-gradient(-30deg, #D86299 0%, #A449D6 100%); - border-radius: 100px; - border: none; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - font-size: 15px; - font-weight: 500; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-9px, -50%); - gap: 5px; - width: 19px; - height: 19px; - } - - .${MANAGE_BUTTON_CLASS_NAME}--added { - background: green; - animation: added 600ms ease; - } - - .${MANAGE_BUTTON_CLASS_NAME} img { - flex-shrink: 0; - user-select: none; - pointer-events: none; + // A lot of random buttons on websites include `void` code to prevent the + // default behaviour of links. These should not be considered bookmarklets. + if (cleanHref === 'javascript:void(0)' || cleanHref === 'javascript:') { + return; } - .${MANAGE_BUTTON_CLASS_NAME}--added img { - transform: translateY(1px); + // Empty links are not considered bookmarklets, such as icon buttons. + if (title.trim() === '') { + return; } - .${LABEL_CLASS_NAME} { - margin-right: 4px; - white-space: nowrap; - display: none; - user-select: none; - pointer-events: none; + // A single character, like "x", is something used for buttons. Example on + // this cookie banner: https://www.universityofgalway.ie/t4training/bookmarklets.html + if (title.length === 1) { + return; } - @keyframes added { - from { - box-shadow: 0 0 0 rgba(0, 100, 0, 1); - } + // To avoid links that are buttons with non-bookmarklet scripts, check the + // class name for any reference for "button" but without "bookmarklet". + // Currently this basic heuristic is to avoid highlighting the "Add to Card" + // and "Add to Wishlist" buttons on the Steam store page. But also keep + // it working with things like: https://www.addtoany.com/services/pinboard_button + // which have a button class name but also reference bookmarklet. + for (const className of classList) { + const includesButtonWord = + className.includes('btn') || className.includes('button'); + const includesBookmarkletName = className.includes('bookmarklet'); - to { - box-shadow: 0 0 50px rgba(0, 100, 0, 0); + if (includesButtonWord && !includesBookmarkletName) { + return; } } - `; - button.addEventListener('click', (event) => { - event.preventDefault(); - event.stopPropagation(); - - try { - chrome.runtime.sendMessage({ - action: 'create_bookmark', - payload: { - title: title.trim(), - url: code.trim() - } - }); - } catch (error) { - alert( - 'Powerlet: Something went wrong when adding bookmarklet. Please reload the page and try again.' - ); - console.error(error); - return; - } - - label.textContent = 'Added!'; - button.classList.add(`${MANAGE_BUTTON_CLASS_NAME}--added`); - icon.src = checkIcon; + showButtonInsideElement(script, title, href); }); - - button.append(icon); - button.append(label); - container.append(button); - - shadow.appendChild(style); - shadow.appendChild(container); } -Array.from(scripts).forEach((script) => { - const title = script.textContent; - const href = script.getAttribute('href'); - const cleanHref = href.trim().replaceAll(' ', '').replaceAll(';', ''); - const classList = Array.from(script.classList); - - // A lot of random buttons on websites include `void` code to prevent the - // default behaviour of links. These should not be considered bookmarklets. - if (cleanHref === 'javascript:void(0)' || cleanHref === 'javascript:') { - return; - } - - // Empty links are not considered bookmarklets, such as icon buttons. - if (title.trim() === '') { - return; - } - - // A single character, like "x", is something used for buttons. Example on - // this cookie banner: https://www.universityofgalway.ie/t4training/bookmarklets.html - if (title.length === 1) { - return; - } - - // To avoid links that are buttons with non-bookmarklet scripts, check the - // class name for any reference for "button" but without "bookmarklet". - // Currently this basic heuristic is to avoid highlighting the "Add to Card" - // and "Add to Wishlist" buttons on the Steam store page. But also keep - // it working with things like: https://www.addtoany.com/services/pinboard_button - // which have a button class name but also reference bookmarklet. - for (const className of classList) { - const includesButtonWord = - className.includes('btn') || className.includes('button'); - const includesBookmarkletName = className.includes('bookmarklet'); - - if (includesButtonWord && !includesBookmarkletName) { - return; - } - } - - showButtonInside(script, title, href); -}); +main(); diff --git a/src/content/show_button_inside_element.js b/src/content/show_button_inside_element.js new file mode 100644 index 0000000..8d283b4 --- /dev/null +++ b/src/content/show_button_inside_element.js @@ -0,0 +1,134 @@ +import { createShadowDomInside } from './create_shadow_dom_inside'; + +import addIcon from './add_icon.svg'; +import checkIcon from './check_icon.svg'; + +const CONTAINER_CLASS_NAME = 'powerlet-button'; +const MANAGE_BUTTON_CLASS_NAME = 'manage-button'; +const LABEL_CLASS_NAME = 'add-remove-button__label'; + +export function showButtonInsideElement(target, title, code) { + const shadow = createShadowDomInside(target); + const style = document.createElement('style'); + + const container = document.createElement('div'); + const button = document.createElement('button'); + const icon = document.createElement('img'); + const label = document.createElement('span'); + + container.classList.add(CONTAINER_CLASS_NAME); + button.classList.add(MANAGE_BUTTON_CLASS_NAME); + + label.classList.add(LABEL_CLASS_NAME); + label.textContent = 'Add Bookmarklet'; + + icon.src = addIcon; + + style.innerHTML = ` + .${CONTAINER_CLASS_NAME} { + width: 28px; + height: 28px; + position: absolute; + transform: translate(-30%, -100%); + top: 50%; + left: 50%; + } + + .${CONTAINER_CLASS_NAME}:hover .${MANAGE_BUTTON_CLASS_NAME} { + padding: 5px; + width: auto; + height: auto; + transform: translate(-12.5px, -50%); + } + + .${CONTAINER_CLASS_NAME}:hover .${LABEL_CLASS_NAME} { + display: block; + } + + .${MANAGE_BUTTON_CLASS_NAME} { + color: white; + background-image: linear-gradient(-30deg, #D86299 0%, #A449D6 100%); + border-radius: 100px; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + font-size: 15px; + font-weight: 500; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-9px, -50%); + gap: 5px; + width: 19px; + height: 19px; + } + + .${MANAGE_BUTTON_CLASS_NAME}--added { + background: green; + animation: added 600ms ease; + } + + .${MANAGE_BUTTON_CLASS_NAME} img { + flex-shrink: 0; + user-select: none; + pointer-events: none; + } + + .${MANAGE_BUTTON_CLASS_NAME}--added img { + transform: translateY(1px); + } + + .${LABEL_CLASS_NAME} { + margin-right: 4px; + white-space: nowrap; + display: none; + user-select: none; + pointer-events: none; + } + + @keyframes added { + from { + box-shadow: 0 0 0 rgba(0, 100, 0, 1); + } + + to { + box-shadow: 0 0 50px rgba(0, 100, 0, 0); + } + } + `; + + button.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + + try { + chrome.runtime.sendMessage({ + action: 'create_bookmark', + payload: { + title: title.trim(), + url: code.trim() + } + }); + } catch (error) { + alert( + 'Powerlet: Something went wrong when adding bookmarklet. Please reload the page and try again.' + ); + console.error(error); + return; + } + + label.textContent = 'Added!'; + button.classList.add(`${MANAGE_BUTTON_CLASS_NAME}--added`); + icon.src = checkIcon; + }); + + button.append(icon); + button.append(label); + container.append(button); + + shadow.appendChild(style); + shadow.appendChild(container); +} From 3c70d06a4ff895c6ccab3b2fd1c12c45e778acec Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Fri, 21 Apr 2023 23:08:11 +0800 Subject: [PATCH 15/20] Reorganise CSS a bit --- src/content/show_button_inside_element.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/content/show_button_inside_element.js b/src/content/show_button_inside_element.js index 8d283b4..9d469e8 100644 --- a/src/content/show_button_inside_element.js +++ b/src/content/show_button_inside_element.js @@ -35,9 +35,9 @@ export function showButtonInsideElement(target, title, code) { } .${CONTAINER_CLASS_NAME}:hover .${MANAGE_BUTTON_CLASS_NAME} { - padding: 5px; width: auto; height: auto; + padding: 5px; transform: translate(-12.5px, -50%); } @@ -54,16 +54,16 @@ export function showButtonInsideElement(target, title, code) { display: flex; align-items: center; justify-content: center; + gap: 5px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; font-size: 15px; font-weight: 500; position: absolute; top: 50%; left: 50%; - transform: translate(-9px, -50%); - gap: 5px; width: 19px; height: 19px; + transform: translate(-9px, -50%); } .${MANAGE_BUTTON_CLASS_NAME}--added { From 0652d4bcaccd801b59cc4ca30be0e18d1a79540b Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Fri, 21 Apr 2023 23:24:53 +0800 Subject: [PATCH 16/20] Make button appear on top when mouse over --- src/content/show_button_inside_element.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/content/show_button_inside_element.js b/src/content/show_button_inside_element.js index 9d469e8..4e69c8d 100644 --- a/src/content/show_button_inside_element.js +++ b/src/content/show_button_inside_element.js @@ -100,6 +100,14 @@ export function showButtonInsideElement(target, title, code) { } `; + button.addEventListener('mouseenter', () => { + container.style.zIndex = '2147483647'; + }); + + button.addEventListener('mouseleave', () => { + container.style.zIndex = 'auto'; + }); + button.addEventListener('click', (event) => { event.preventDefault(); event.stopPropagation(); From 6e86fa99b1605eea28ab0dfb620f4d208b85499c Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Fri, 21 Apr 2023 23:27:36 +0800 Subject: [PATCH 17/20] Disable add button once already added --- src/content/show_button_inside_element.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/content/show_button_inside_element.js b/src/content/show_button_inside_element.js index 4e69c8d..72a37ed 100644 --- a/src/content/show_button_inside_element.js +++ b/src/content/show_button_inside_element.js @@ -112,6 +112,10 @@ export function showButtonInsideElement(target, title, code) { event.preventDefault(); event.stopPropagation(); + if (button.classList.contains(`${MANAGE_BUTTON_CLASS_NAME}--added`)) { + return; + } + try { chrome.runtime.sendMessage({ action: 'create_bookmark', From 22502590bfadbb872efedf3155fa97384fddd3ed Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Fri, 21 Apr 2023 23:32:04 +0800 Subject: [PATCH 18/20] Use action const for message passing --- src/background/index.js | 17 +++++++++++------ src/content/show_button_inside_element.js | 4 +++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/background/index.js b/src/background/index.js index 3dfaae9..9ce5568 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -1,3 +1,4 @@ +import { CREATE_BOOKMARK } from '../content/show_button_inside_element'; import createIconRenderer from './icon_renderer'; const renderIcon = createIconRenderer(); @@ -5,9 +6,13 @@ const renderIcon = createIconRenderer(); setInterval(renderIcon, 1000); renderIcon(); -chrome.runtime.onMessage.addListener((message) => { - chrome.bookmarks.create({ - title: message.payload.title, - url: message.payload.url - }); -}); +chrome.runtime.onMessage.addListener( + (message = { action: null, payload: null }) => { + if (message.action === CREATE_BOOKMARK) { + chrome.bookmarks.create({ + title: message.payload.title, + url: message.payload.url + }); + } + } +); diff --git a/src/content/show_button_inside_element.js b/src/content/show_button_inside_element.js index 72a37ed..d026018 100644 --- a/src/content/show_button_inside_element.js +++ b/src/content/show_button_inside_element.js @@ -7,6 +7,8 @@ const CONTAINER_CLASS_NAME = 'powerlet-button'; const MANAGE_BUTTON_CLASS_NAME = 'manage-button'; const LABEL_CLASS_NAME = 'add-remove-button__label'; +export const CREATE_BOOKMARK = 'CREATE_BOOKMARK'; + export function showButtonInsideElement(target, title, code) { const shadow = createShadowDomInside(target); const style = document.createElement('style'); @@ -118,7 +120,7 @@ export function showButtonInsideElement(target, title, code) { try { chrome.runtime.sendMessage({ - action: 'create_bookmark', + action: CREATE_BOOKMARK, payload: { title: title.trim(), url: code.trim() From 338716a0a1decd07220b0c565dd1b9c247e31752 Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Thu, 18 May 2023 20:54:36 +0100 Subject: [PATCH 19/20] Check for single functions to see if they are custom or builtin window methods --- src/content/default_window_methods.js | 50 +++++++++++++++++++++++++++ src/content/index.js | 28 ++++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 src/content/default_window_methods.js diff --git a/src/content/default_window_methods.js b/src/content/default_window_methods.js new file mode 100644 index 0000000..89eaba5 --- /dev/null +++ b/src/content/default_window_methods.js @@ -0,0 +1,50 @@ +export const DEFAULT_WINDOW_METHODS = [ + 'alert', + 'atob', + 'blur', + 'btoa', + 'cancelAnimationFrame', + 'cancelIdleCallback', + 'captureEvents', + 'clearInterval', + 'clearTimeout', + 'close', + 'confirm', + 'createImageBitmap', + 'fetch', + 'find', + 'focus', + 'getComputedStyle', + 'getSelection', + 'matchMedia', + 'moveBy', + 'moveTo', + 'open', + 'postMessage', + 'print', + 'prompt', + 'queueMicrotask', + 'releaseEvents', + 'reportError', + 'requestAnimationFrame', + 'requestIdleCallback', + 'resizeBy', + 'resizeTo', + 'scroll', + 'scrollBy', + 'scrollTo', + 'setInterval', + 'setTimeout', + 'stop', + 'structuredClone', + 'webkitCancelAnimationFrame', + 'webkitRequestAnimationFrame', + 'getScreenDetails', + 'queryLocalFonts', + 'showDirectoryPicker', + 'showOpenFilePicker', + 'showSaveFilePicker', + 'openDatabase', + 'webkitRequestFileSystem', + 'webkitResolveLocalFileSystemURL' +]; diff --git a/src/content/index.js b/src/content/index.js index 4026788..be077e2 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -1,5 +1,8 @@ +import { DEFAULT_WINDOW_METHODS } from './default_window_methods'; import { showButtonInsideElement } from './show_button_inside_element'; +const FUNCTION_REGEX = /^([a-zA-Z0-9].*)\(.*?\)$/g; + function main() { const scripts = document.body.querySelectorAll('[href*="javascript:"]'); @@ -7,11 +10,16 @@ function main() { const title = script.textContent; const href = script.getAttribute('href'); const cleanHref = href.trim().replaceAll(' ', '').replaceAll(';', ''); + const cleanHrefWithoutPrefix = cleanHref.replace(/^javascript:/, ''); const classList = Array.from(script.classList); // A lot of random buttons on websites include `void` code to prevent the // default behaviour of links. These should not be considered bookmarklets. - if (cleanHref === 'javascript:void(0)' || cleanHref === 'javascript:') { + if ( + cleanHref === 'javascript:' || + cleanHref === 'javascript:void(0)' || + cleanHref === 'javascript:void0' + ) { return; } @@ -26,6 +34,24 @@ function main() { return; } + // A regex object is stateful. Not resetting this will cause the regex test + // to toggle between `true` and `false` on each iteration of the loop, which + // is bananas! + FUNCTION_REGEX.lastIndex = 0; + + const match = FUNCTION_REGEX.exec(cleanHrefWithoutPrefix); + + if (match) { + const potentialFunctionName = match[1].replace(/^window\./g, ''); + const isDefaultWindowMethod = DEFAULT_WINDOW_METHODS.includes( + potentialFunctionName + ); + + if (!isDefaultWindowMethod) { + return; + } + } + // To avoid links that are buttons with non-bookmarklet scripts, check the // class name for any reference for "button" but without "bookmarklet". // Currently this basic heuristic is to avoid highlighting the "Add to Card" From 426c1db3f8f6a4d7b546a81e500a5ec7062e552e Mon Sep 17 00:00:00 2001 From: Anthony Cossins <1451668+anthonyec@users.noreply.github.com> Date: Thu, 18 May 2023 21:02:42 +0100 Subject: [PATCH 20/20] Remove class heuristic --- src/content/index.js | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/content/index.js b/src/content/index.js index be077e2..96e3547 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -39,6 +39,8 @@ function main() { // is bananas! FUNCTION_REGEX.lastIndex = 0; + // Look for `href` values that are single function calls. If it's not a + // default window method, then it's probably not a bookmarklet. const match = FUNCTION_REGEX.exec(cleanHrefWithoutPrefix); if (match) { @@ -52,22 +54,6 @@ function main() { } } - // To avoid links that are buttons with non-bookmarklet scripts, check the - // class name for any reference for "button" but without "bookmarklet". - // Currently this basic heuristic is to avoid highlighting the "Add to Card" - // and "Add to Wishlist" buttons on the Steam store page. But also keep - // it working with things like: https://www.addtoany.com/services/pinboard_button - // which have a button class name but also reference bookmarklet. - for (const className of classList) { - const includesButtonWord = - className.includes('btn') || className.includes('button'); - const includesBookmarkletName = className.includes('bookmarklet'); - - if (includesButtonWord && !includesBookmarkletName) { - return; - } - } - showButtonInsideElement(script, title, href); }); }