5 |
6 |
7 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Dark Pattern Detection Project
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/chrome/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "__MSG_extName__",
4 | "description": "__MSG_extDescription__",
5 | "version": "1.2.1",
6 | "icons": {
7 | "16": "images/icon-16.png",
8 | "32": "images/icon-32.png",
9 | "48": "images/icon-48.png",
10 | "128": "images/icon-128.png",
11 | "512": "images/icon-512.png"
12 | },
13 | "content_scripts": [
14 | {
15 | "js": [
16 | "scripts/content.js"
17 | ],
18 | "css": [
19 | "stylesheets/style.css"
20 | ],
21 | "matches": [
22 | "http://*/*",
23 | "https://*/*"
24 | ],
25 | "run_at": "document_idle"
26 | }
27 | ],
28 | "permissions": [
29 | "activeTab",
30 | "tabs",
31 | "storage"
32 | ],
33 | "action": {
34 | "default_popup": "popup/popup.html"
35 | },
36 | "web_accessible_resources": [
37 | {
38 | "resources": [
39 | "scripts/constants.js"
40 | ],
41 | "matches": [
42 | "http://*/*",
43 | "https://*/*"
44 | ]
45 | }
46 | ],
47 | "default_locale": "de",
48 | "background": {
49 | "service_worker": "background.js"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/firefox/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "__MSG_extName__",
4 | "description": "__MSG_extDescription__",
5 | "version": "1.2.1",
6 | "icons": {
7 | "16": "images/icon-16.png",
8 | "32": "images/icon-32.png",
9 | "48": "images/icon-48.png",
10 | "128": "images/icon-128.png",
11 | "512": "images/icon-512.png"
12 | },
13 | "content_scripts": [
14 | {
15 | "js": [
16 | "scripts/content.js"
17 | ],
18 | "css": [
19 | "stylesheets/style.css"
20 | ],
21 | "matches": [
22 | "http://*/*",
23 | "https://*/*"
24 | ],
25 | "run_at": "document_idle"
26 | }
27 | ],
28 | "permissions": [
29 | "activeTab",
30 | "tabs",
31 | "storage"
32 | ],
33 | "action": {
34 | "default_popup": "popup/popup.html"
35 | },
36 | "web_accessible_resources": [
37 | {
38 | "resources": [
39 | "scripts/constants.js"
40 | ],
41 | "matches": [
42 | "http://*/*",
43 | "https://*/*"
44 | ]
45 | }
46 | ],
47 | "default_locale": "de",
48 | "background": {
49 | "scripts": [
50 | "background.js"
51 | ]
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/chrome/scripts/lit/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2017 Google LLC. All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | 1. Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 |
11 | 2. Redistributions in binary form must reproduce the above copyright notice,
12 | this list of conditions and the following disclaimer in the documentation
13 | and/or other materials provided with the distribution.
14 |
15 | 3. Neither the name of the copyright holder nor the names of its
16 | contributors may be used to endorse or promote products derived from
17 | this software without specific prior written permission.
18 |
19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 |
--------------------------------------------------------------------------------
/create_firefox_from_chrome.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Set variable for the action to be performed.
4 | # 1 - Copy the files from the "chrome" folder to the "firefox" folder (default).
5 | # 2 - Remove the copied files from the "firefox" folder.
6 | # 3 - Display the manual.
7 | action=1
8 |
9 | # Boolean switch to indicate whether at least one unexpected argument was given.
10 | unexpected_argument=0
11 |
12 | # Iterate through all arguments (including flags).
13 | for arg in "$@"; do
14 | if [ "$arg" = "-c" ] || [ "$arg" = "--clean" ]; then
15 | action=2
16 | elif [ "$arg" = "-h" ] || [ "$arg" = "--help" ]; then
17 | action=3
18 | else
19 | # Output an error message to stderr directly for each unexpected parameter.
20 | echo >&2 "Unexpected argument: $arg"
21 | # Set the switch to 1, i.e. to true.
22 | unexpected_argument=1
23 | fi
24 | done
25 |
26 | # Output a help message if at least one unexpected argument was given.
27 | if [ "$unexpected_argument" -eq 1 ]; then
28 | echo "Use \"-h\" or \"--help\" to display the manual."
29 | exit 1
30 | fi
31 |
32 | # Get the relative path to the directory of the script.
33 | script_dir=$(dirname "$0")
34 |
35 | # Empty the "firefox" folder before copying the files or if a flag for cleaning has been set.
36 | if [ "$action" -eq 1 ] || [ "$action" -eq 2 ]; then
37 | # Set a variable for the path of the Firefox extension manifest file.
38 | manifest_file="$script_dir/firefox/manifest.json"
39 | # Create a copy of the manifest file in memory.
40 | manifest=$(cat "$manifest_file")
41 | # Delete the "firefox" folder.
42 | rm -rf "$script_dir/firefox/"
43 | # Create a new "firefox" folder.
44 | mkdir "$script_dir/firefox/"
45 | # Restore the manifest file using the copy in memory.
46 | printf "%s\n" "$manifest" >"$manifest_file"
47 | fi
48 |
49 | # Perform the requested action.
50 | if [ "$action" -eq 1 ]; then
51 | # Copy all files from the "chrome" folder to the "firefox" folder without overwriting existing files.
52 | cp -r -n "$script_dir/chrome/." "$script_dir/firefox/."
53 | echo "Files have been copied to the \"firefox\" folder."
54 | elif [ "$action" -eq 2 ]; then
55 | echo "Files have been removed from the \"firefox\" folder."
56 | exit 0
57 | elif [ "$action" -eq 3 ]; then
58 | echo "This script copies the missing files for the Firefox version of the extension from the \"chrome\" folder to the \"firefox\" folder."
59 | echo "Available flags:"
60 | echo "\t -c, --clean\t Remove the copied files. The \"manifest.json\" file will not be deleted."
61 | echo "\t -h, --help\t Display this manual."
62 | else
63 | exit 1
64 | fi
65 |
66 | exit 0
67 |
--------------------------------------------------------------------------------
/chrome/popup/styles.js:
--------------------------------------------------------------------------------
1 | // Import the required components from the Lit Library
2 | import { css, unsafeCSS } from '../scripts/lit/lit-core.min.js';
3 |
4 | /**
5 | * The object to access the API functions of the browser.
6 | * @constant
7 | * @type {{runtime: object, tabs: object, i18n: object}} BrowserAPI
8 | */
9 | const brw = chrome;
10 |
11 | export const sharedStyles = css`
12 | div {
13 | margin: 20px auto;
14 | }
15 |
16 | a:link,
17 | a:visited {
18 | color: inherit;
19 | }
20 |
21 | h2 {
22 | margin: 0.5em 0;
23 | }
24 |
25 | * {
26 | font-family: "Trebuchet MS", "Arial", sans-serif;
27 | }
28 | `;
29 |
30 | export const patternLinkStyles = css`
31 | a {
32 | text-decoration: none;
33 | cursor: pointer;
34 | }
35 |
36 | a:hover {
37 | text-decoration: underline;
38 | }
39 | `;
40 |
41 | export const actionButtonStyles = css`
42 | div span {
43 | color: #217284;
44 | font-weight: bold;
45 | cursor: pointer;
46 | text-decoration: none;
47 | }
48 |
49 | div span:hover {
50 | text-decoration: underline;
51 | }
52 |
53 | @media (prefers-color-scheme: dark) {
54 | div {
55 | color: #33bfde;
56 | }
57 | }
58 | `;
59 |
60 | export const patternsListStyles = css`
61 | ul {
62 | list-style-type: none;
63 | padding: 0;
64 | }
65 |
66 | li {
67 | margin: 10px 0;
68 | }
69 | `;
70 |
71 | // On/Off Flipswitch from https://proto.io/freebies/onoff/
72 | export const onOffSwitchStyles = css`
73 | div {
74 | position: relative;
75 | width: 90px;
76 | -webkit-user-select: none;
77 | -moz-user-select: none;
78 | -ms-user-select: none;
79 | }
80 |
81 | input {
82 | position: absolute;
83 | opacity: 0;
84 | pointer-events: none;
85 | }
86 |
87 | label {
88 | display: block;
89 | overflow: hidden;
90 | border: 2px solid #000000;
91 | border-radius: 50px;
92 | }
93 |
94 | input:enabled+label {
95 | cursor: pointer;
96 | }
97 |
98 | input:disabled+label {
99 | cursor: not-allowed;
100 | }
101 |
102 | .onoffswitch-inner {
103 | display: block;
104 | width: 200%;
105 | margin-left: -100%;
106 | }
107 |
108 | .onoffswitch-inner:before,
109 | .onoffswitch-inner:after {
110 | display: block;
111 | float: left;
112 | width: 50%;
113 | height: 30px;
114 | padding: 0;
115 | line-height: 30px;
116 | font-size: 14px;
117 | color: white;
118 | font-family: Trebuchet, Arial, sans-serif;
119 | font-weight: bold;
120 | box-sizing: border-box;
121 | }
122 |
123 | .onoffswitch-inner:before {
124 | content: "${unsafeCSS(brw.i18n.getMessage("buttonOnState"))}";
125 | padding-left: 10px;
126 | background-color: #34A7C1;
127 | color: #FFFFFF;
128 | }
129 |
130 | .onoffswitch-inner:after {
131 | content: "${unsafeCSS(brw.i18n.getMessage("buttonOffState"))}";
132 | padding-right: 10px;
133 | background-color: #EEEEEE;
134 | color: #999999;
135 | text-align: right;
136 | }
137 |
138 | .onoffswitch-switch {
139 | display: block;
140 | width: 18px;
141 | margin: 6px;
142 | background: #FFFFFF;
143 | position: absolute;
144 | top: 0;
145 | bottom: 0;
146 | right: 56px;
147 | border: 2px solid #000000;
148 | border-radius: 50px;
149 | }
150 |
151 | input:checked+label .onoffswitch-inner {
152 | margin-left: 0;
153 | }
154 |
155 | input:checked+label .onoffswitch-switch {
156 | right: 0px;
157 | }
158 |
159 | @media (prefers-color-scheme: dark) {
160 | label {
161 | border: 2px solid #FFFFFF;
162 | }
163 |
164 | .onoffswitch-inner:before {
165 | background-color: #33bfde;
166 | color: #000000;
167 | }
168 | }
169 | `;
170 |
--------------------------------------------------------------------------------
/create_firefox_from_chrome.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | rem Set variable for the action to be performed.
4 | rem 1 - Copy the files from the "chrome" folder to the "firefox" folder (default).
5 | rem 2 - Remove the copied files from the "firefox" folder.
6 | rem 3 - Display the manual.
7 | set action=1
8 |
9 | rem Boolean switch to indicate whether at least one unexpected argument was given.
10 | set unexpected_argument=0
11 |
12 | rem Iterate through all arguments (including flags).
13 | :args_start
14 | rem Check if the argument is one of the specified flags.
15 | if "%~1"=="-c" goto flag_clean
16 | if "%~1"=="/c" goto flag_clean
17 | if "%~1"=="--clean" goto flag_clean
18 | if "%~1"=="/clean" goto flag_clean
19 |
20 | if "%~1"=="-h" goto flag_help
21 | if "%~1"=="/h" goto flag_help
22 | if "%~1"=="--help" goto flag_help
23 | if "%~1"=="/help" goto flag_help
24 | if "%~1"=="-?" goto flag_help
25 | if "%~1"=="/?" goto flag_help
26 |
27 | if "%~1"=="" goto args_end
28 |
29 | rem Set the switch for unexpected arguments to 1, i.e. true, in case of an unexpected argument.
30 | set unexpected_argument=1
31 | rem Output an error message to stderr directly for each unexpected parameter.
32 | echo Unexpected argument: %~1 1>&2
33 | rem Continue with the next argument.
34 | goto next_argument
35 |
36 | :flag_clean
37 | rem Set the `action` variable to 2 if a flag for the cleanup operation was given.
38 | set action=2
39 | rem Continue with the next argument.
40 | goto next_argument
41 |
42 | :flag_help
43 | rem Set the `action` variable to 3 if a flag for the help operation was given.
44 | set action=3
45 | rem Continue with the next argument.
46 | goto next_argument
47 |
48 | :next_argument
49 | rem Look at the next argument.
50 | shift /1
51 | rem Check the next argument if it is not empty.
52 | if not "%~1" == "" goto args_start
53 | :args_end
54 |
55 | rem Check if not one unexpected argument was given. If true, perform the requested action.
56 | if %unexpected_argument% equ 0 goto perform_action
57 |
58 | rem Output a help message if at least one unexpected argument was given.
59 | echo Use "/h" or "/help" to display the manual.
60 | rem Exit the script with an error code.
61 | goto exit_error
62 |
63 | :perform_action
64 | rem Get the absolute path to the directory of the script.
65 | set script_dir=%~dp0
66 |
67 | rem # Perform the requested action.
68 | if %action% equ 1 goto action_clean_start
69 | if %action% equ 2 goto action_clean_start
70 | if %action% equ 3 goto action_help
71 |
72 | rem If the `action` variable somehow has an unexpected value, exit the script with an error code.
73 | goto exit_error
74 |
75 | :action_clean_start
76 | rem Set a variable for the path of the Firefox extension manifest file.
77 | set manifest_file=%script_dir%firefox\manifest.json
78 | rem Set a variable for the path of the temporary copy of the Firefox extension manifest file.
79 | set manifest_backup=%script_dir%firefox_manifest.json.bkp
80 | rem Create the temporary copy of the Firefox extension manifest file in the directory of the script.
81 | copy %manifest_file% %manifest_backup% >nul
82 | rem Delete the "firefox" folder.
83 | rmdir %script_dir%firefox\ /s /q
84 | rem Create a new "firefox" folder.
85 | md %script_dir%firefox\
86 |
87 | rem Unless the default action was requested, the file copying is to be skipped.
88 | if %action% neq 1 goto action_clean_end
89 | rem Copy all files from the "chrome" folder to the "firefox" folder.
90 | xcopy /s /e /y %script_dir%chrome\* %script_dir%firefox\* >nul
91 |
92 | :action_clean_end
93 | rem Restore the manifest file by copying the temporary copy.
94 | copy %manifest_backup% %manifest_file% >nul
95 | rem Remove the temporary copy of the Firefox extension manifest file.
96 | del %manifest_backup%
97 |
98 | rem When the files have been copied, the corresponding message should be output.
99 | if %action% equ 1 goto action_copy
100 | rem If only the folder was cleaned, the corresponding message should be output.
101 | echo Files have been removed from the "firefox" folder.
102 | rem Exit the script successfully.
103 | goto exit_success
104 |
105 | :action_copy
106 | echo Files have been copied to the "firefox" folder.
107 | rem Exit the script successfully.
108 | goto exit_success
109 |
110 | :action_help
111 | echo This script copies the missing files for the Firefox version of the extension from the "chrome" folder to the "firefox" folder.
112 | echo Available flags:
113 | echo. /c, /clean Remove the copied files. The "manifest.json" file will not be deleted.
114 | echo. /?, /h, /help Display this manual.
115 | rem Exit the script successfully.
116 | goto exit_success
117 |
118 | :exit_error
119 | rem Wait for a user input so that the user sees the error message.
120 | pause
121 | exit 1
122 |
123 | :exit_success
124 | exit 0
125 |
--------------------------------------------------------------------------------
/chrome/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extName": {
3 | "message": "Pattern Highlighter",
4 | "description": "The name of the extension."
5 | },
6 | "extDescription": {
7 | "message": "Detects and highlights different Dark Patterns on websites.",
8 | "description": "The description of the extension."
9 | },
10 | "errorInvalidConfig": {
11 | "message": "The internal pattern configuration of the extension is invalid. The pattern highlighter will not start.",
12 | "description": "The error message in case of an invalid internal pattern configuration."
13 | },
14 | "buttonOnState": {
15 | "message": "ON",
16 | "description": "The message when the switch is turned on in the popup."
17 | },
18 | "buttonOffState": {
19 | "message": "OFF",
20 | "description": "The message when the switch is turned off in the popup."
21 | },
22 | "buttonReloadPageForChange": {
23 | "message": "Refresh page to apply changes",
24 | "description": "Text for the button to reload the page after a changed activation state."
25 | },
26 | "buttonRedoPatternCheck": {
27 | "message": "Repeat pattern highlighting",
28 | "description": "Text for the button to repeat the pattern highlighting on the page."
29 | },
30 | "headingShowPattern": {
31 | "message": "Show patterns",
32 | "description": "Heading in the popup for the buttons to show individual pattern elements."
33 | },
34 | "headingFoundPatterns": {
35 | "message": "Patterns found",
36 | "description": "Heading in the popup for the list of found patterns."
37 | },
38 | "headingSupportedPatterns": {
39 | "message": "Supported pattern types",
40 | "description": "Heading in the popup for the list of supported patterns."
41 | },
42 | "textMoreInformation": {
43 | "message": "More information about Dark Patterns",
44 | "description": "Text in the footer preceding the link to the dapde.de website."
45 | },
46 | "infoExtensionStarted": {
47 | "message": "Pattern Highlighter started.",
48 | "description": "Message in the console when the pattern highlighter was started on a page."
49 | },
50 | "infoExtensionDisabled": {
51 | "message": "Pattern Highlighter is disabled for this tab.",
52 | "description": "Message in the console when the pattern highlighter is disabled in a tab."
53 | },
54 | "infoNumberPatternsFound": {
55 | "message": "$COUNT$ pattern(s) detected.",
56 | "description": "Message in the console about how many patterns were detected.",
57 | "placeholders": {
58 | "count": {
59 | "content": "$1",
60 | "example": "10"
61 | }
62 | }
63 | },
64 | "patternCountdown_name": {
65 | "message": "Countdown",
66 | "description": "Name of the countdown pattern."
67 | },
68 | "patternCountdown_infoUrl": {
69 | "message": "https://dapde.de/en/dark-patterns-en/types-and-examples-en/druck2-en/",
70 | "description": "URL to the explanation of the countdown pattern on the dapde.de website."
71 | },
72 | "patternCountdown_info": {
73 | "message": "Countdown patterns induce (truthfully or falsely) the impression that a product or service is only available for a certain period of time. This is illustrated through a running clock or a lapsing bar. You can watch as the desired good slips away.",
74 | "description": "Brief explanation of the countdown Pattern."
75 | },
76 | "patternScarcity_name": {
77 | "message": "Scarcity",
78 | "description": "Name of the scarcity pattern."
79 | },
80 | "patternScarcity_infoUrl": {
81 | "message": "https://dapde.de/en/dark-patterns-en/types-and-examples-en/druck2-en/",
82 | "description": "URL to the explanation of the scarcity pattern on the dapde.de website."
83 | },
84 | "patternScarcity_info": {
85 | "message": "The Scarcity Pattern induces (truthfully or falsely) the impression that goods or services are only available in limited numbers. Scarcity Patterns are also used in versions where the alleged scarcity is simply invented or where it is not made clear whether the limited availability relates to the product as a whole or only to the contingent of the portal visited.",
86 | "description": "Brief explanation of the scarcity Pattern."
87 | },
88 | "patternSocialProof_name": {
89 | "message": "Social Proof",
90 | "description": "Name of the social proof pattern."
91 | },
92 | "patternSocialProof_infoUrl": {
93 | "message": "https://dapde.de/en/dark-patterns-en/types-and-examples-en/druck2-en/",
94 | "description": "URL to the explanation of the social proof pattern on the dapde.de website."
95 | },
96 | "patternSocialProof_info": {
97 | "message": "Social Proof is another Dark Pattern of this category. Positive product reviews or activity reports from other users are displayed directly. Often, these reviews or reports are simply made up. But authentic reviews or reports also influence the purchase decision through smart selection and placement.",
98 | "description": "Brief explanation of the social proof Pattern."
99 | },
100 | "patternForcedContinuity_name": {
101 | "message": "Forced Continuity",
102 | "description": "Name of the forced continuity pattern."
103 | },
104 | "patternForcedContinuity_infoUrl": {
105 | "message": "https://dapde.de/en/dark-patterns-en/types-and-examples-en/operativer-zwang2-en/",
106 | "description": "URL to the explanation of the forced continuity pattern on the dapde.de website."
107 | },
108 | "patternForcedContinuity_info": {
109 | "message": "The Forced Continuity pattern automatically renews free or low-cost trial subscriptions - but for a fee or at a higher price. The design trick is that the order form visually suggests that there is no charge and conceals the (automatic) follow-up costs.",
110 | "description": "Brief explanation of the forced continuity Pattern."
111 | },
112 | "showPatternState": {
113 | "message": "$CURRENT$ of $TOTAL$",
114 | "description": "Display the number of the current pattern in the popup for the 'Show patterns' function.",
115 | "placeholders": {
116 | "current": {
117 | "content": "$1",
118 | "example": "1"
119 | },
120 | "total": {
121 | "content": "$2",
122 | "example": "4"
123 | }
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/chrome/_locales/de/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extName": {
3 | "message": "Pattern Highlighter",
4 | "description": "Der Name der Erweiterung."
5 | },
6 | "extDescription": {
7 | "message": "Erkennt und markiert verschiedene \"Dark Patterns\" auf Webseiten.",
8 | "description": "The description of the extension."
9 | },
10 | "errorInvalidConfig": {
11 | "message": "Die interne Pattern-Konfiguration der Erweiterung ist ungültig. Der Pattern Highlighter wird nicht gestartet.",
12 | "description": "Die Fehlermeldung im Falle einer ungültigen internen Pattern-Konfiguration."
13 | },
14 | "buttonOnState": {
15 | "message": "AN",
16 | "description": "Die Anzeige beim angeschalteten Schalter im Popup."
17 | },
18 | "buttonOffState": {
19 | "message": "AUS",
20 | "description": "Die Anzeige beim ausgeschaltenen Schalter im Popup."
21 | },
22 | "buttonReloadPageForChange": {
23 | "message": "Seite neu laden zur Änderung",
24 | "description": "Text für den Button um die Seite neu zu laden, nach geändertem Aktivierungsstatus."
25 | },
26 | "buttonRedoPatternCheck": {
27 | "message": "Pattern Highlighting wiederholen",
28 | "description": "Text für den Button um das Pattern Highlighting auf der Seite zu wiederholen."
29 | },
30 | "headingShowPattern": {
31 | "message": "Patterns anzeigen",
32 | "description": "Überschrift im Popup für die Buttons um einzelne Pattern-Elemente anzuzeigen."
33 | },
34 | "headingFoundPatterns": {
35 | "message": "Gefundene Patterns",
36 | "description": "Überschrift im Popup für die Liste der gefundenen Pattern."
37 | },
38 | "headingSupportedPatterns": {
39 | "message": "Unterstützte Pattern-Arten",
40 | "description": "Überschrift im Popup für die Liste der unterstützten Pattern."
41 | },
42 | "textMoreInformation": {
43 | "message": "Mehr Informationen zu Dark Pattern",
44 | "description": "Text im Footer vor der Verlinkung zur dapde.de Webseite."
45 | },
46 | "infoExtensionStarted": {
47 | "message": "Pattern Highlighter gestartet.",
48 | "description": "Nachricht in der Konsole, wenn der Pattern Highlighter auf einer Seite gestartet wurde."
49 | },
50 | "infoExtensionDisabled": {
51 | "message": "Der Pattern Highlighter ist für diesen Tab deaktiviert.",
52 | "description": "Nachricht in der Konsole, wenn der Pattern Highlighter in einem Tab deaktiviert ist."
53 | },
54 | "infoNumberPatternsFound": {
55 | "message": "$COUNT$ Pattern(s) wurden erkannt.",
56 | "description": "Nachricht in der Konsole, wie viele Pattern erkannt wurden.",
57 | "placeholders": {
58 | "count": {
59 | "content": "$1",
60 | "example": "10"
61 | }
62 | }
63 | },
64 | "patternCountdown_name": {
65 | "message": "Countdown",
66 | "description": "Name des Countdown Pattern."
67 | },
68 | "patternCountdown_infoUrl": {
69 | "message": "https://dapde.de/de/dark-patterns/arten-und-beispiele/druck2/",
70 | "description": "URL zur Erklärung des Countdown Pattern auf der dapde.de Webseite."
71 | },
72 | "patternCountdown_info": {
73 | "message": "Countdown-Dark Patterns erwecken (wahrheitsgemäß oder fälschlicherweise) den Eindruck, eine Ware bzw. Dienstleistung ist nur noch für eine bestimmte Zeit verfügbar. Grafisch wird dies durch eine ablaufende Uhr oder Leiste verdeutlicht. Man kann zusehen, wie einem das gewünschte Gut davongleitet.",
74 | "description": "Kurze Erklärung des Countdown Pattern."
75 | },
76 | "patternScarcity_name": {
77 | "message": "Scarcity",
78 | "description": "Name des Scarcity Pattern."
79 | },
80 | "patternScarcity_infoUrl": {
81 | "message": "https://dapde.de/de/dark-patterns/arten-und-beispiele/druck2/",
82 | "description": "URL zur Erklärung des Scarcity Pattern auf der dapde.de Webseite."
83 | },
84 | "patternScarcity_info": {
85 | "message": "Das Scarcity-Pattern erweckt (wahrheitsgemäß oder fälschlicherweise) den Eindruck, eine Ware oder Dienstleistung ist nur noch in knapper Zahl verfügbar. Scarcity Patterns gibt es auch in Varianten, bei denen die angebliche Knappheit schlicht erfunden ist. Oder es wird zumindest nicht deutlich gemacht, ob sich die begrenzte Verfügbarkeit auf das Produkt insgesamt oder nur auf das Kontingent des besuchten Portals bezieht.",
86 | "description": "Kurze Erklärung des Scarcity Pattern."
87 | },
88 | "patternSocialProof_name": {
89 | "message": "Social Proof",
90 | "description": "Name des Social Proof Pattern."
91 | },
92 | "patternSocialProof_infoUrl": {
93 | "message": "https://dapde.de/de/dark-patterns/arten-und-beispiele/druck2/",
94 | "description": "URL zur Erklärung des Social Proof Pattern auf der dapde.de Webseite."
95 | },
96 | "patternSocialProof_info": {
97 | "message": "Das Social Proof-Dark Pattern setzt auf soziale Bewährtheit. Positive Produktbewertungen oder Aktivitätsmeldungen anderer Nutzer:innen werden direkt eingeblendet. Häufig sind diese Bewertungen oder Meldungen schlicht erfunden. Aber auch authentische Bewertungen oder Meldungen beeinflussen durch geschicktes Selektieren und Platzieren die Kaufentscheidung.",
98 | "description": "Kurze Erklärung des Social Proof Pattern."
99 | },
100 | "patternForcedContinuity_name": {
101 | "message": "Forced Continuity",
102 | "description": "Name des Forced Continuity Pattern."
103 | },
104 | "patternForcedContinuity_infoUrl": {
105 | "message": "https://dapde.de/de/dark-patterns/arten-und-beispiele/operativer-zwang2/",
106 | "description": "URL zur Erklärung des Forced Continuity Pattern auf der dapde.de Webseite."
107 | },
108 | "patternForcedContinuity_info": {
109 | "message": "Das Forced Continuity-Dark Pattern verlängert kostenlose oder günstig Probeabonnements automatisch – allerdings kostenpflichtig bzw. zu einem höheren Preis. Der Designtrick liegt darin, dass das Bestellformular auch optisch Kostenlosigkeit suggeriert und die (automatischen) Folgekosten verschleiert.",
110 | "description": "Kurze Erklärung des Forced Continuity Pattern."
111 | },
112 | "showPatternState": {
113 | "message": "$CURRENT$ von $TOTAL$",
114 | "description": "Anzeige der Nummer des aktuellen Patterns im Popup für die Funktion 'Pattern ansehen'.",
115 | "placeholders": {
116 | "current": {
117 | "content": "$1",
118 | "example": "1"
119 | },
120 | "total": {
121 | "content": "$2",
122 | "example": "4"
123 | }
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/chrome/background.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The object to access the API functions of the browser.
3 | * @constant
4 | * @type {{runtime: object, tabs: object, action: object}} BrowserAPI
5 | */
6 | const brw = chrome;
7 |
8 | /**
9 | * The prefix for the keys in the session storage under which the activation state of the tabs is stored.
10 | * @constant
11 | * @type {string}
12 | */
13 | const activationPrefix = "activation_";
14 |
15 | /**
16 | * The object to access the browser storage API.
17 | * If no session storage is supported, use local storage (Firefox).
18 | * @constant
19 | * @type {object}
20 | */
21 | const storage = brw.storage.session ? brw.storage.session : brw.storage.local;
22 |
23 | /**
24 | * Retrieves the activation state for a tab from the session storage and returns it.
25 | * @param {number} tabId The ID of the tab of which the activation state should be retrieved.
26 | * @returns {Promise} `true` if the extension is activated,
27 | * `false` if it is deactivated or `undefined` if the activation has not yet been set (new tab).
28 | */
29 | async function getActivation(tabId){
30 | // Load the activation state from the session storage.
31 | // Compose the key from the `activationPrefix` and the `tabId`.
32 | // Since only one key in the storage is requested, the object contains exactly one or no value.
33 | // Therefore, the first value of the object is accessed directly.
34 | // If no value exists, the access returns `undefined`.
35 | return Object.values(await storage.get(`${activationPrefix}${tabId}`))[0];
36 | }
37 |
38 | /**
39 | * Sets the active state for a tab in the session storage.
40 | * @param {number} tabId The ID of the tab of which the activation state should be set.
41 | * @param {boolean} activation `true` if the extension should be activated,
42 | * `false` if it should be deactivated.
43 | */
44 | async function setActivation(tabId, activation){
45 | // Set the activation state in the session storage.
46 | // Compose the key from the `activationPrefix` and the `tabId`.
47 | return await storage.set({[`${activationPrefix}${tabId}`]: activation});
48 | }
49 |
50 | /**
51 | * Removes the activation state for a tab from the session storage.
52 | * Used when a tab is closed.
53 | * @param {number} tabId The ID of the tab of which the activation state should be removed.
54 | */
55 | async function removeActivation(tabId){
56 | // Remove the activation state from the session storage.
57 | // Compose the key from the `activationPrefix` and the `tabId`.
58 | return await storage.remove(`${activationPrefix}${tabId}`);
59 | }
60 |
61 | /**
62 | * Retrieves the activation state for a tab from the session storage and returns it.
63 | * Sets the activation state to `true` (activated) if it is not already set (new tab).
64 | * @param {number} tabId The ID of the tab of which the activation state should be retrieved.
65 | * @returns {Promise} `true` if the extension is activated, `false` if it is deactivated.
66 | */
67 | async function getActivationOrSetDefault(tabId){
68 | // Load the activation state from the session storage.
69 | let activation = await getActivation(tabId);
70 |
71 | // If there is no activation state saved for the tab yet, set it to active.
72 | if (activation === undefined){
73 | // Set the variable to `true` so that this will be returned later.
74 | activation = true;
75 | // Set the activation state in the session storage to `true` (active).
76 | await setActivation(tabId, activation);
77 | };
78 |
79 | // Return the activation state for the tab.
80 | return activation;
81 | }
82 |
83 | // Add event listeners for messages from other scripts of the extension.
84 | // The defined callback function is executed when a message is received from the content or popup script.
85 | brw.runtime.onMessage.addListener(
86 | function (message, sender, sendResponse) {
87 | if ("countVisible" in message) {
88 | // If the count of visible detected patterns is included in the message,
89 | // then the count on the icon in the browser bar should be updated.
90 |
91 | // Check if the extension should actually be active for the tab.
92 | // The case where this message is received from a tab that is not activated is unexpected.
93 | // To be on the safe side, it is checked anyway.
94 | getActivation(sender.tab.id).then((activation) => {
95 | if (activation === true){
96 | // Update the number of patterns detected on the icon
97 | // for the tab from which the message was received.
98 | displayPatternCount(message.countVisible, sender.tab.id);
99 | }
100 | // Send a simple reply with confirmation of successful execution.
101 | sendResponse({ success: true });
102 | });
103 |
104 | } else if ("enableExtension" in message && "tabId" in message) {
105 | // If the message contains the key `enableExtension` and a tab ID,
106 | // the activation state of the extension should be set for the respective tab.
107 |
108 | // Set the activation state of the extension for the tab.
109 | setActivation(message.tabId, message.enableExtension).then(() => {
110 | // If the extension should be disabled for the tab,
111 | // no number should be displayed on the icon anymore.
112 | if (message.enableExtension === false) {
113 | // Update the pattern count on the icon to an empty string.
114 | displayPatternCount("", message.tabId);
115 | }
116 | // Send a simple reply with confirmation of successful execution.
117 | sendResponse({ success: true });
118 | });
119 |
120 | } else if ("action" in message && message.action == "getActivationState") {
121 | // If the message contains the `action` key with the value `getActivationState`,
122 | // the activation state of the corresponding tab should be sent as a response.
123 |
124 | // Declare the variable for the tab ID.
125 | let tabId;
126 | if ("tabId" in message) {
127 | // If the Tab ID is included in the message, use it.
128 | // This is the case if the message was sent from the popup.
129 | tabId = message.tabId;
130 | } else {
131 | // If the tab ID is not included in the message, extract it from the `sender` object.
132 | // This is the case if the message was sent from the content script in a tab.
133 | tabId = sender.tab.id;
134 | }
135 |
136 | getActivationOrSetDefault(tabId).then((activation) => {
137 | // Respond with the activation state of the tab.
138 | sendResponse({ isEnabled: activation });
139 | });
140 |
141 | } else {
142 | // Send a simple reply with the message on failed processing of the message,
143 | // if a message without expected content was received.
144 | sendResponse({ success: false });
145 | }
146 |
147 | // In order for the sender to wait for a response,
148 | // `true` must be returned in order to use `sendResponse()` asynchronously.
149 | // See https://developer.chrome.com/docs/extensions/mv3/messaging/#simple.
150 | return true;
151 | }
152 | );
153 |
154 | // Add an event handler that handles tab ID changes.
155 | // It is not really clear when and how often this event occurs.
156 | // The documentation states the following (https://developer.chrome.com/docs/extensions/reference/tabs/#event-onReplaced):
157 | // "Fired when a tab is replaced with another tab due to prerendering or instant.".
158 | brw.tabs.onReplaced.addListener(async function (addedTabId, removedTabId) {
159 | // Save the activation state of the old tab ID for the new tab ID.
160 | await setActivation(addedTabId, await getActivation(removedTabId));
161 | // Delete the activation state of the old tab ID.
162 | await removeActivation(removedTabId);
163 | });
164 |
165 | // Add an event handler that handles the closing of tabs.
166 | // When a tab is closed, the activation state should be reset, i.e. deleted.
167 | brw.tabs.onRemoved.addListener(async function (tabId, removeInfo) {
168 | // Delete the activation state of the closed tab ID.
169 | await removeActivation(tabId);
170 | });
171 |
172 | /**
173 | * A dictionary in which the paths to the extension's icon in different resolutions are specified.
174 | * Is read directly from the manifest file.
175 | * @type {Object.}
176 | */
177 | let icons_default = brw.runtime.getManifest().icons;
178 | /**
179 | * A dictionary in which the paths to the extension's grayed out icon, which is used when the extension
180 | * is disabled, are specified in different resolutions.
181 | * @type {Object.}
182 | */
183 | let icons_disabled = {};
184 |
185 | // Generate the paths to the gray version of the icon using the paths of the default icon.
186 | for (let resolution in icons_default) {
187 | icons_disabled[resolution] = `${icons_default[resolution].slice(0, -4)}_grey.png`;
188 | }
189 |
190 | // Add an event handler that processes updates from tabs.
191 | // With this event can be used to capture changes to the URL.
192 | // This allows to detect whether the extension should be enabled in a tab or not.
193 | brw.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
194 | // If the tab contains a web page loaded with HTTP(S), the default icon should be displayed.
195 | // Otherwise the content script is not enabled and therefore the extension is disabled.
196 | // In this case, a gray version of the icon should be displayed.
197 | if (tab.url.toLowerCase().startsWith("http://") || tab.url.toLowerCase().startsWith("https://")) {
198 | // Set the default icon for the tab.
199 | brw.action.setIcon({
200 | path: icons_default,
201 | tabId: tabId
202 | });
203 | } else {
204 | // Set the gray icon for the tab.
205 | brw.action.setIcon({
206 | path: icons_disabled,
207 | tabId: tabId
208 | });
209 | }
210 | });
211 |
212 | /**
213 | * Displays the number of detected pattern elements as a number on the extension's icon in the browser bar.
214 | * If the number is 0, the background of the number is set to green, otherwise it is red.
215 | * @param {(number|"")} count The amount of detected pattern elements.
216 | * @param {number} tabId The ID of the tab in which the count should be displayed on the icon.
217 | */
218 | function displayPatternCount(count, tabId) {
219 | // Set the text on the icon (badge) of the specified tab to the count passed.
220 | brw.action.setBadgeText({
221 | tabId: tabId,
222 | text: "" + count
223 | });
224 |
225 | // Set the background color of the icon text to red as default [r, g, b, alpha].
226 | let bgColor = [255, 0, 0, 255];
227 | // If no patterns were detected, change the background color to green.
228 | if (count == 0) {
229 | // // Set the background color of the icon text to green.
230 | bgColor = [0, 255, 0, 255];
231 | }
232 | // Set the background color for the icon text of the specified tab.
233 | brw.action.setBadgeBackgroundColor({
234 | tabId: tabId,
235 | color: bgColor
236 | });
237 | }
238 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Dapde Pattern Highlighter Browser Extension
4 | This tool, developed by the informatics part of the [dapde-project](https://dapde.de/), is intended to help consumers to navigate the internet in a way similar to an ad blocker. However, the highlighter differs from ad blockers in one crucial aspect: it does not block individual dark patterns on websites but highlights them so that consumers become aware of the influences affecting them. In addition, the tool informs about the type of pattern.
5 |
6 | ## Contents
7 | - [Features](#features)
8 | - [Video and Screenshots](#video-and-screenshots)
9 | - [How it works](#how-it-works)
10 | - [Browser Compatibility](#browser-compatibility)
11 | - [Installation](#installation)
12 | - [Libraries Used](#libraries-used)
13 | - [License](#license)
14 | - [About Dapde](#about-dapde)
15 |
16 | ## Features
17 | - Automatic detection of dark patterns on web pages
18 | - Highlighting of suspicious elements with minimal impact on page appearance
19 | - Popup window providing information on detected dark patterns, including their category and an explanation
20 | - No blocking of web page content
21 | - Extension icon displaying number of detected dark patterns
22 | - Function to individually highlight each detected dark pattern
23 | - Supporting multiple languages (currently English and German available)
24 |
25 | ## Video and Screenshots
26 | ### Teaser Video
27 | Click on the image or [here](https://dapde.de/en/news-en/dapde-dark-pattern-highlighter-en/) to watch the teaser video for the Pattern Highlighter.
28 | [](https://dapde.de/en/news-en/dapde-dark-pattern-highlighter-en/)
29 |
30 | ### Screenshots
31 | |  |  |
32 | | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: |
33 | | *Example of a web site with two highlighted dark patterns. The black border highlights a countdown and a scarcity pattern element (from left to right). \*Some web site details were manually removed from the screenshot.* | *The popup window of the extension. The popup window can be used to disable and enable the highlighting. Additionally, information about the detected patterns is displayed and each one can be highlighted separately.* |
34 |
35 | ## How it works
36 | The Pattern Highlighter works entirely locally in the browser and does not connect to any servers. When visiting a web page, the extension injects a small script that creates an internal temporary copy of the entire web page i.e. its HTML DOM. After a short pause (about 1.5 seconds) a second copy is created. Subsequently, all elements of these copies are examined individually and in combination with child elements using the implemented pattern detection methods. The pattern detection methods decide whether an element is a specific dark pattern or not. The reason for creating two copies with a time gap is to detect changes on the web page. This makes it possible to detect certain patterns such as countdowns.
37 |
38 | Mainly responsible for the results of the pattern detection are the mentioned detection functions. These are centrally defined in the `patternConfig` object together with information about the associated patterns in [`constants.js`](chrome/scripts/constants.js). This `patternConfig` object can be extended arbitrarily by additional patterns and functions, according to the requirements that are commented in [`constants.js`](chrome/scripts/constants.js).
39 |
40 | Currently, one detection function each is implemented for the four following patterns.
41 | - [Countdown](https://dapde.de/en/dark-patterns-en/types-and-examples-en/druck2-en/)
42 | - [Scarcity](https://dapde.de/en/dark-patterns-en/types-and-examples-en/druck2-en/)
43 | - [Social Proof](https://dapde.de/en/dark-patterns-en/types-and-examples-en/druck2-en/)
44 | - [Forced Continuity](https://dapde.de/en/dark-patterns-en/types-and-examples-en/operativer-zwang2-en/)
45 |
46 | Right now, all of the four detection functions are optimized for German and English websites and cannot be applied to websites in other languages.
47 |
48 | ## Browser Compatibility
49 | | Browser | Is compatible? | Tested versions |
50 | |----------------- |:--------------: |------------------------------------------------------------------------------- |
51 | | Google Chrome | ✅ |
113.0.5672.92 (Mac/arm64)
113.0.5672.93 (Win/x64)
|
52 | | Microsoft Edge | ✅ |
113.0.1774.35 (Mac/arm64)
113.0.1774.35 (Win/x64)
|
53 | | Safari | ✅ |
16.4 (18615.1.26.110.1) (Mac/arm64)
|
54 | | Mozilla Firefox | ✅ |
113.0 (Mac/arm64)
112.0.2 (Win/x64)
|
55 | | Opera | ✅ |
98.0.4759.39 (Mac/arm64)
98.0.4759.39 (Win/x64)
|
56 |
57 | ### Google Chrome, Microsoft Edge and Opera
58 | The Pattern Highlighter uses an [API](https://developer.chrome.com/docs/extensions/reference/) that is specified by Google and primarily supported by the Google Chrome browser. However, many other browsers also support this Chrome API. Since Microsoft Edge and Opera, just like Google Chrome, are built on the [Chromium](https://en.wikipedia.org/wiki/Chromium_(web_browser)) code base, the [API support](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Browser_support_for_JavaScript_APIs) of the three browsers is almost completely identical. Consequently, the extension will behave the same way in these browsers. This is also to be expected for other browsers that are based on Chromium.
59 |
60 | ### Firefox
61 | Firefox also supports the Chrome API with [some differences and limitations](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Chrome_incompatibilities). For the Pattern Highlighter it is only relevant that Firefox [does not support](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json#browser_compatibility) the `background.service_worker` key in the [manifest file](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json) of extensions to define scripts that run in the background. Instead, it supports the `background.scripts` key, which is not supported by Chrome. Therefore, the Firefox version of the extension requires a custom manifest file. The other files are the same as in the Chrome version of the extension. The section on [installation in Firefox](#firefox) explains how to create the Firefox version.
62 |
63 | ### Safari
64 | Safari also supports the Chrome API functions required by the Pattern Highlighter. Thus, the Pattern Highlighter is functionally fully compatible with Safari. However, Safari uses its own format for extensions, which differs from the other browsers. Therefore, the code of the Pattern Highlighter must first be converted to a Safari extension. This can conveniently be done automatically and is described in the section on [installation in Safari](#safari-1).
65 |
66 | Visual differences from the versions of the other browsers:
67 | - *The number of detected patterns on the icon are always displayed with a red background, even if `0` patterns were detected.* Safari does [not support](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/action/setBadgeBackgroundColor) functions to change the background color of the text or these functions have [no effect](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/action/setBadgeBackgroundColor). By default, the text in Safari has a red background.
68 | - *The icon of the extension is monochrome in the address bar.* If the extension is not active in the current tab, the icon is colored gray. If the extension has access to the web page in the current tab and is therefore active, the icon is colored in the accent color of the system (blue by default). This has nothing to do with the extension's internal activation status. If the user deactivates the extension in a single tab via the popup, the icon remains colored in the accent color.
69 |
70 | ## Installation
71 | To install the extension, the repository or the `chrome` folder must be downloaded. Since the extension is not loaded from the stores of the browser providers, it must be installed in the developer mode of the browsers. For this, the individual steps for the different tested browsers are listed below.
72 |
73 | ### Chrome
74 | 1. Go to the Extensions page by entering `chrome://extensions` in a new tab.
75 | - Alternatively, click on the Extensions menu puzzle button and select **Manage Extensions** at the bottom of the menu.
76 | - Or, click the Chrome menu, hover over **More Tools**, then select **Extensions**.
77 | 2. Enable Developer Mode by clicking the toggle switch next to **Developer mode**.
78 | 3. Click the **Load unpacked** button and select the `chrome` directory.
79 | 4. (Optional): Click the Extensions menu puzzle button in the address bar and then click the **Pin** button next to the *Pattern Highlighter* to keep its icon permanently displayed.
80 |
81 | ### Edge
82 | 1. Go to the Extensions page by entering `edge://extensions` in a new tab.
83 | - Alternatively, click the **Settings and more (...)** button, select **Extensions** and click **Manage extensions** on the opened popup.
84 | 2. Enable Developer Mode by clicking the toggle switch next to **Developer mode**.
85 | 3. Click the **Load unpacked** button and select the `chrome` directory.
86 | 4. (Optional): Click the Extensions menu puzzle button in the address bar and then click the **Show in Toolbar** button (eye icon) next to the *Pattern Highlighter* to keep its icon permanently displayed.
87 |
88 | ### Safari
89 | In order to install the extension in Safari, it must first be converted into the compatible format. This requires an Xcode installation. To convert the Pattern Highlighter to a Safari extension, follow these steps.
90 |
91 | 1. Open a terminal window and navigate to the path of the repository (one directory level above the `chrome` folder).
92 | 2. Execute the following command: `xcrun safari-web-extension-converter --macos-only --project-location safari chrome`. ([More information about the command.](https://developer.apple.com/documentation/safariservices/safari_web_extensions/converting_a_web_extension_for_safari#3586260))
93 | 3. The command should have created a new folder named `safari` containing the Xcode project for extension. Also, a Xcode window should have opened. In Xcode, click the Run button, or choose **Product** > **Run** to build and run your app.
94 | 4. Now the developer menu in Safari has to be activated and unsigned extensions have to be allowed. After that, the extension can be activated in the browser. The necessary steps are explained [here](https://developer.apple.com/documentation/safariservices/safari_web_extensions/running_your_safari_web_extension#3744467).
95 |
96 | ### Firefox
97 | #### Building the Firefox version
98 | To build the firefox version, the complete repository must be downloaded.
99 |
100 | ##### Windows
101 | 1. Execute the `create_firefox_from_chrome.bat` batch script in the root directory of the repository, by double-clicking the file.
102 | - Alternatively, open `cmd.exe`, navigate to the root directory of the repository and execute the batch script from the command line.
103 | 2. The `firefox` folder should now contain not only the `manifest.json` file but also all other files and folders from the `chrome` folder.
104 |
105 | ##### Mac
106 | 1. Open a new terminal window, navigate to the root directory of the repository and execute the `create_firefox_from_chrome.sh` shell script.
107 | - To do so, execute the following command: `sh create_firefox_from_chrome.sh`.
108 | 2. The `firefox` folder should now contain not only the `manifest.json` file but also all other files and folders from the `chrome` folder.
109 |
110 | #### Installation
111 | 1. Go to the Firefox Debugging page by entering `about:debugging` in a new tab.
112 | 2. Click the **This Firefox** button on the left side, then click the **Load Temporary Add-on...** button and select the `manifest.json` file in the `firefox` directory.
113 | 3. Go to the Extensions page by entering `about:addons` in a new tab.
114 | 4. Click the **Extensions** button on the left side and then click on the *Pattern Highlighter*.
115 | 5. Open the **Permissions** tab and click the toggle switch to the right of `Access your data for all websites` to give the Pattern Highlighter permissions to scan for patterns on all websites.
116 | 6. (Optional): Click the Extensions menu puzzle button in the address bar, right-click on the *Pattern Highlighter* and then click **Pin to Toolbar** to keep its icon permanently displayed.
117 |
118 | Since the extension can currently only be installed via the method for developers, in Firefox it only remains installed until the browser is restarted.
119 |
120 | ### Opera
121 | 1. Go to the Extensions page by entering `opera://extensions` in a new tab (or use the Cmd/Ctrl + Shift + E shortcut).
122 | 2. Enable Developer Mode by clicking the toggle switch next to **Developer mode**.
123 | 3. Click the **Load unpacked** button and select the `chrome` directory.
124 | 4. (Optional): Click the Extensions menu cube button in the address bar and then click the **Pin** button next to the *Pattern Highlighter* to keep its icon permanently displayed.
125 |
126 | ## Libraries Used
127 | - [Lit 2.7.2](https://lit.dev/) ([BSD-3-Clause](chrome/scripts/lit/LICENSE))
128 |
129 | ## License
130 | [MIT](LICENSE)
131 |
132 | ## About Dapde
133 | The Dark Pattern Detection Project (Dapde) examines the manipulation of consumers in a digital environment through "dark patterns".
134 |
135 | ### Dark Patterns
136 | Dark patterns are design patterns that lead users to act in a certain way that is contrary to their interests, exploiting design power unilaterally in the interests of their creator.
137 |
138 | ### The Project
139 | Dapde is a joint project between the Institute of Computer Science at Heidelberg University and the German Research Institute for Public Administration in Speyer (FÖV). The Informatics Section tackles the challenge of recognizing dark patterns in online interactions with the aim of warning users of dangers early on. The Law Section develops legal answers to the challenges of steering consumers through dark patterns.
140 |
141 | ### Dapde Website
142 | More information about our project and dark patterns can be found on our [website](https://dapde.de/).
143 |
--------------------------------------------------------------------------------
/chrome/scripts/lit/lit-core.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @license
3 | * Copyright 2019 Google LLC
4 | * SPDX-License-Identifier: BSD-3-Clause
5 | */
6 | const t=window,i=t.ShadowRoot&&(void 0===t.ShadyCSS||t.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,s=Symbol(),e=new WeakMap;class o{constructor(t,i,e){if(this._$cssResult$=!0,e!==s)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=i}get styleSheet(){let t=this.i;const s=this.t;if(i&&void 0===t){const i=void 0!==s&&1===s.length;i&&(t=e.get(s)),void 0===t&&((this.i=t=new CSSStyleSheet).replaceSync(this.cssText),i&&e.set(s,t))}return t}toString(){return this.cssText}}const n=t=>new o("string"==typeof t?t:t+"",void 0,s),r=(t,...i)=>{const e=1===t.length?t[0]:i.reduce(((i,s,e)=>i+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(s)+t[e+1]),t[0]);return new o(e,t,s)},h=(s,e)=>{i?s.adoptedStyleSheets=e.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):e.forEach((i=>{const e=document.createElement("style"),o=t.litNonce;void 0!==o&&e.setAttribute("nonce",o),e.textContent=i.cssText,s.appendChild(e)}))},l=i?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let i="";for(const s of t.cssRules)i+=s.cssText;return n(i)})(t):t
7 | /**
8 | * @license
9 | * Copyright 2017 Google LLC
10 | * SPDX-License-Identifier: BSD-3-Clause
11 | */;var a;const u=window,c=u.trustedTypes,d=c?c.emptyScript:"",v=u.reactiveElementPolyfillSupport,p={toAttribute(t,i){switch(i){case Boolean:t=t?d:null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,i){let s=t;switch(i){case Boolean:s=null!==t;break;case Number:s=null===t?null:Number(t);break;case Object:case Array:try{s=JSON.parse(t)}catch(t){s=null}}return s}},f=(t,i)=>i!==t&&(i==i||t==t),m={attribute:!0,type:String,converter:p,reflect:!1,hasChanged:f},y="finalized";class _ extends HTMLElement{constructor(){super(),this.o=new Map,this.isUpdatePending=!1,this.hasUpdated=!1,this.l=null,this.u()}static addInitializer(t){var i;this.finalize(),(null!==(i=this.v)&&void 0!==i?i:this.v=[]).push(t)}static get observedAttributes(){this.finalize();const t=[];return this.elementProperties.forEach(((i,s)=>{const e=this.p(s,i);void 0!==e&&(this.m.set(e,s),t.push(e))})),t}static createProperty(t,i=m){if(i.state&&(i.attribute=!1),this.finalize(),this.elementProperties.set(t,i),!i.noAccessor&&!this.prototype.hasOwnProperty(t)){const s="symbol"==typeof t?Symbol():"__"+t,e=this.getPropertyDescriptor(t,s,i);void 0!==e&&Object.defineProperty(this.prototype,t,e)}}static getPropertyDescriptor(t,i,s){return{get(){return this[i]},set(e){const o=this[t];this[i]=e,this.requestUpdate(t,o,s)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||m}static finalize(){if(this.hasOwnProperty(y))return!1;this[y]=!0;const t=Object.getPrototypeOf(this);if(t.finalize(),void 0!==t.v&&(this.v=[...t.v]),this.elementProperties=new Map(t.elementProperties),this.m=new Map,this.hasOwnProperty("properties")){const t=this.properties,i=[...Object.getOwnPropertyNames(t),...Object.getOwnPropertySymbols(t)];for(const s of i)this.createProperty(s,t[s])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(t){const i=[];if(Array.isArray(t)){const s=new Set(t.flat(1/0).reverse());for(const t of s)i.unshift(l(t))}else void 0!==t&&i.push(l(t));return i}static p(t,i){const s=i.attribute;return!1===s?void 0:"string"==typeof s?s:"string"==typeof t?t.toLowerCase():void 0}u(){var t;this._=new Promise((t=>this.enableUpdating=t)),this._$AL=new Map,this.g(),this.requestUpdate(),null===(t=this.constructor.v)||void 0===t||t.forEach((t=>t(this)))}addController(t){var i,s;(null!==(i=this.S)&&void 0!==i?i:this.S=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(s=t.hostConnected)||void 0===s||s.call(t))}removeController(t){var i;null===(i=this.S)||void 0===i||i.splice(this.S.indexOf(t)>>>0,1)}g(){this.constructor.elementProperties.forEach(((t,i)=>{this.hasOwnProperty(i)&&(this.o.set(i,this[i]),delete this[i])}))}createRenderRoot(){var t;const i=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return h(i,this.constructor.elementStyles),i}connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this.S)||void 0===t||t.forEach((t=>{var i;return null===(i=t.hostConnected)||void 0===i?void 0:i.call(t)}))}enableUpdating(t){}disconnectedCallback(){var t;null===(t=this.S)||void 0===t||t.forEach((t=>{var i;return null===(i=t.hostDisconnected)||void 0===i?void 0:i.call(t)}))}attributeChangedCallback(t,i,s){this._$AK(t,s)}$(t,i,s=m){var e;const o=this.constructor.p(t,s);if(void 0!==o&&!0===s.reflect){const n=(void 0!==(null===(e=s.converter)||void 0===e?void 0:e.toAttribute)?s.converter:p).toAttribute(i,s.type);this.l=t,null==n?this.removeAttribute(o):this.setAttribute(o,n),this.l=null}}_$AK(t,i){var s;const e=this.constructor,o=e.m.get(t);if(void 0!==o&&this.l!==o){const t=e.getPropertyOptions(o),n="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==(null===(s=t.converter)||void 0===s?void 0:s.fromAttribute)?t.converter:p;this.l=o,this[o]=n.fromAttribute(i,t.type),this.l=null}}requestUpdate(t,i,s){let e=!0;void 0!==t&&(((s=s||this.constructor.getPropertyOptions(t)).hasChanged||f)(this[t],i)?(this._$AL.has(t)||this._$AL.set(t,i),!0===s.reflect&&this.l!==t&&(void 0===this.C&&(this.C=new Map),this.C.set(t,s))):e=!1),!this.isUpdatePending&&e&&(this._=this.T())}async T(){this.isUpdatePending=!0;try{await this._}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,this.o&&(this.o.forEach(((t,i)=>this[i]=t)),this.o=void 0);let i=!1;const s=this._$AL;try{i=this.shouldUpdate(s),i?(this.willUpdate(s),null===(t=this.S)||void 0===t||t.forEach((t=>{var i;return null===(i=t.hostUpdate)||void 0===i?void 0:i.call(t)})),this.update(s)):this.P()}catch(t){throw i=!1,this.P(),t}i&&this._$AE(s)}willUpdate(t){}_$AE(t){var i;null===(i=this.S)||void 0===i||i.forEach((t=>{var i;return null===(i=t.hostUpdated)||void 0===i?void 0:i.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}P(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._}shouldUpdate(t){return!0}update(t){void 0!==this.C&&(this.C.forEach(((t,i)=>this.$(i,this[i],t))),this.C=void 0),this.P()}updated(t){}firstUpdated(t){}}
12 | /**
13 | * @license
14 | * Copyright 2017 Google LLC
15 | * SPDX-License-Identifier: BSD-3-Clause
16 | */
17 | var b;_[y]=!0,_.elementProperties=new Map,_.elementStyles=[],_.shadowRootOptions={mode:"open"},null==v||v({ReactiveElement:_}),(null!==(a=u.reactiveElementVersions)&&void 0!==a?a:u.reactiveElementVersions=[]).push("1.6.1");const g=window,w=g.trustedTypes,S=w?w.createPolicy("lit-html",{createHTML:t=>t}):void 0,$="$lit$",C=`lit$${(Math.random()+"").slice(9)}$`,T="?"+C,P=`<${T}>`,x=document,A=()=>x.createComment(""),k=t=>null===t||"object"!=typeof t&&"function"!=typeof t,E=Array.isArray,M=t=>E(t)||"function"==typeof(null==t?void 0:t[Symbol.iterator]),U="[ \t\n\f\r]",N=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,R=/-->/g,O=/>/g,V=RegExp(`>|${U}(?:([^\\s"'>=/]+)(${U}*=${U}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),j=/'/g,z=/"/g,L=/^(?:script|style|textarea|title)$/i,I=t=>(i,...s)=>({_$litType$:t,strings:i,values:s}),H=I(1),B=I(2),D=Symbol.for("lit-noChange"),q=Symbol.for("lit-nothing"),J=new WeakMap,W=x.createTreeWalker(x,129,null,!1),Z=(t,i)=>{const s=t.length-1,e=[];let o,n=2===i?"":"");if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return[void 0!==S?S.createHTML(h):h,e]};class F{constructor({strings:t,_$litType$:i},s){let e;this.parts=[];let o=0,n=0;const r=t.length-1,h=this.parts,[l,a]=Z(t,i);if(this.el=F.createElement(l,s),W.currentNode=this.el.content,2===i){const t=this.el.content,i=t.firstChild;i.remove(),t.append(...i.childNodes)}for(;null!==(e=W.nextNode())&&h.length0){e.textContent=w?w.emptyScript:"";for(let s=0;s2||""!==s[0]||""!==s[1]?(this._$AH=Array(s.length-1).fill(new String),this.strings=s):this._$AH=q}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(t,i=this,s,e){const o=this.strings;let n=!1;if(void 0===o)t=G(this,t,i,0),n=!k(t)||t!==this._$AH&&t!==D,n&&(this._$AH=t);else{const e=t;let r,h;for(t=o[0],r=0;r{var e,o;const n=null!==(e=null==s?void 0:s.renderBefore)&&void 0!==e?e:i;let r=n._$litPart$;if(void 0===r){const t=null!==(o=null==s?void 0:s.renderBefore)&&void 0!==o?o:null;n._$litPart$=r=new Q(i.insertBefore(A(),t),t,void 0,null!=s?s:{})}return r._$AI(t),r};
18 | /**
19 | * @license
20 | * Copyright 2017 Google LLC
21 | * SPDX-License-Identifier: BSD-3-Clause
22 | */var ht,lt;const at=_;class ut extends _{constructor(){super(...arguments),this.renderOptions={host:this},this.st=void 0}createRenderRoot(){var t,i;const s=super.createRenderRoot();return null!==(t=(i=this.renderOptions).renderBefore)&&void 0!==t||(i.renderBefore=s.firstChild),s}update(t){const i=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(t),this.st=rt(i,this.renderRoot,this.renderOptions)}connectedCallback(){var t;super.connectedCallback(),null===(t=this.st)||void 0===t||t.setConnected(!0)}disconnectedCallback(){var t;super.disconnectedCallback(),null===(t=this.st)||void 0===t||t.setConnected(!1)}render(){return D}}ut.finalized=!0,ut._$litElement$=!0,null===(ht=globalThis.litElementHydrateSupport)||void 0===ht||ht.call(globalThis,{LitElement:ut});const ct=globalThis.litElementPolyfillSupport;null==ct||ct({LitElement:ut});const dt={_$AK:(t,i,s)=>{t._$AK(i,s)},_$AL:t=>t._$AL};(null!==(lt=globalThis.litElementVersions)&&void 0!==lt?lt:globalThis.litElementVersions=[]).push("3.3.1");
23 | /**
24 | * @license
25 | * Copyright 2022 Google LLC
26 | * SPDX-License-Identifier: BSD-3-Clause
27 | */
28 | const vt=!1;export{o as CSSResult,ut as LitElement,_ as ReactiveElement,at as UpdatingElement,dt as _$LE,ot as _$LH,h as adoptStyles,r as css,p as defaultConverter,l as getCompatibleStyle,H as html,vt as isServer,D as noChange,f as notEqual,q as nothing,rt as render,i as supportsAdoptingStyleSheets,B as svg,n as unsafeCSS};
29 | //# sourceMappingURL=lit-core.min.js.map
30 |
--------------------------------------------------------------------------------
/chrome/scripts/content.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The object to access the API functions of the browser.
3 | * @constant
4 | * @type {{runtime: object, i18n: object}} BrowserAPI
5 | */
6 | const brw = chrome;
7 |
8 | /**
9 | * This variable will be dynamically populated with the constants from the other module.
10 | * Since the import must be dynamic, the variable cannot be declared as a constant.
11 | * @type {object} A module namespace object
12 | */
13 | let constants;
14 |
15 | // Initialize the extension.
16 | initPatternHighlighter();
17 |
18 | /**
19 | * Initialize the extension in the current tab.
20 | * @returns {Promise}
21 | */
22 | async function initPatternHighlighter(){
23 | // Ask the background script if the extension should be activated in this tab.
24 | /**
25 | * The object that contains the activation state of the extension in the current tab.
26 | * @constant
27 | * @type {{isEnabled: boolean}} ResponseMessage
28 | */
29 | const activationState = await brw.runtime.sendMessage({ action: "getActivationState" });
30 |
31 | // Initialize the extension in the tab if it should be activated.
32 | if (activationState.isEnabled === true) {
33 |
34 | // Dynamically import the constants from the module.
35 | constants = await import(await brw.runtime.getURL("scripts/constants.js"));
36 |
37 | // Check if the pattern configuration is valid.
38 | if (!constants.patternConfigIsValid) {
39 | // If the configuration is not valid, issue an error message,
40 | // do not start pattern highlighting, and exit.
41 | console.error(brw.i18n.getMessage("errorInvalidConfig"));
42 | return;
43 | }
44 |
45 | // Print a message that the pattern highlighter has started.
46 | console.log(brw.i18n.getMessage("infoExtensionStarted"));
47 |
48 | // Run the initial pattern check and highlighting.
49 | await patternHighlighting();
50 |
51 | // Listen for messages from the popup.
52 | brw.runtime.onMessage.addListener(
53 | function (message, sender, sendResponse) {
54 | // Check which action is requested by the popup.
55 | if (message.action === "getPatternCount") {
56 | // Compute the pattern statistics/counts and send the result as response.
57 | sendResponse(getPatternsResults());
58 | } else if (message.action === "redoPatternHighlighting") {
59 | // Run the pattern checking and highlighting again,
60 | // send in response that the action has been started.
61 | patternHighlighting();
62 | sendResponse({ started: true });
63 | } else if ("showElement" in message) {
64 | // Highlight/show a single pattern element that was selected in the popup.
65 | showElement(message.showElement);
66 | sendResponse({ success: true });
67 | }
68 | }
69 | );
70 | } else {
71 | // Print a message that the pattern highlighter is disabled.
72 | console.log(brw.i18n.getMessage("infoExtensionDisabled"))
73 | }
74 | }
75 |
76 | /**
77 | * An observer that performs the pattern checking and highlighting after an observed change.
78 | * @constant
79 | * @type {MutationObserver}
80 | */
81 | const observer = new MutationObserver(async function () {
82 | await patternHighlighting(true);
83 | });
84 |
85 | /**
86 | * The function to identify for patterns on the page. The function uses the detection methods defined in the `patternConfig`.
87 | * Some HTML tags are ignored (see `tagBlacklist`).
88 | * If an element is identified as a pattern, two classes are added to it.
89 | * This will automatically highlight the element using predefined CSS styles.
90 | * @param {boolean} [waitForChanges=false] A flag to specify whether to wait briefly before executing the function.
91 | */
92 | async function patternHighlighting(waitForChanges = false) {
93 | // Check if the pattern detection is already in progress.
94 | if (this.lock === true) {
95 | // If the pattern detection is already in progress, exit the function.
96 | // The result will follow shortly and will be sent automatically to the other parts of the extension.
97 | return;
98 | }
99 | // Lock the function so that it cannot be executed more than once at the same time.
100 | this.lock = true;
101 |
102 | // Stop monitoring changes on the page with the observer during the pattern identification process.
103 | observer.disconnect();
104 |
105 | // Wait 2000 milliseconds for subsequent changes after the observer has detected a change.
106 | if (waitForChanges === true) {
107 | await new Promise(resolve => { setTimeout(resolve, 2000) });
108 | }
109 |
110 | // Add pattern highlighter IDs to every element on the page.
111 | addPhidForEveryElement(document.body);
112 |
113 | // Create a copy of the DOM that can be modified afterwards.
114 | let domCopyA = document.body.cloneNode(true);
115 | // Remove unwanted elements from the DOM copy (e.g. audio, video and script elements).
116 | removeBlacklistNodes(domCopyA);
117 |
118 | // Wait about 1.5 seconds for changes to elements to occur.
119 | // An example of an expected change is a countdown that counts down every second.
120 | await new Promise(resolve => { setTimeout(resolve, 1536) });
121 |
122 | // Add pattern highlighter IDs to every element on the page.
123 | addPhidForEveryElement(document.body);
124 |
125 | // Create a second copy of the DOM. This copy will reflect changes, if there were any.
126 | let domCopyB = document.body.cloneNode(true);
127 | // Remove unwanted elements from the second DOM copy.
128 | removeBlacklistNodes(domCopyB);
129 |
130 | // Reset all found patterns on the page before updating them afterwards.
131 | resetDetectedPatterns();
132 |
133 | // Identify patterns within the DOM copies. As reference for the current state of the web page `domCopyB` is used.
134 | // `domCopyA` is used as the previous state of the page to detect changes.
135 | // If elements are identified as patterns, respective classes are added to them.
136 | findPatternDeep(domCopyB, domCopyA);
137 |
138 | // Destroy both DOM copies so that they can be removed from memory.
139 | domCopyA.replaceChildren();
140 | domCopyA = null;
141 | domCopyB.replaceChildren();
142 | domCopyB = null;
143 |
144 | // Send the information about the detected patterns to the other extension scripts.
145 | sendResults();
146 |
147 | // Watch the entire page for changes in the DOM. All nodes, their attributes and contents are observed.
148 | // Elements that will be ignored later are also observed.
149 | // Due to the configuration that contents, i.e. characters, are also observed, it can lead to a situation
150 | // where the pattern highlighting function is executed at a fixed interval if the page is constantly changing.
151 | // For this it is enough that there is a dynamic countdown or an active video player with time information on the page.
152 | // Even changes in the background that are not visible can trigger the callback function of the observer.
153 | // However, the advantage over a fixed interval is that there are also pages where no changes take place.
154 | // In this case, no unnecessary operations are performed there.
155 | observer.observe(document.body, {
156 | subtree: true,
157 | childList: true,
158 | attributes: true,
159 | characterData: true,
160 | });
161 |
162 | // Finally, unlock the function so that it can be executed again.
163 | this.lock = false;
164 | }
165 |
166 | /**
167 | * Adds a pattern highlighter ID as a custom HTML attribute to each element of a DOM tree.
168 | * This ID is unique and makes it possible to find elements even after page changes.
169 | * If an element already has an ID, it will be kept and no new one will be added.
170 | * @param {Node} dom The DOM tree to whose elements a unique pattern highlighter ID will be added.
171 | */
172 | function addPhidForEveryElement(dom) {
173 | // Create a counter as a static local variable that is initialized once and then reused.
174 | this.counter = this.counter || 0;
175 | // Iterate over all the individual DOM nodes.
176 | for (const node of dom.querySelectorAll("*")) {
177 | // Add a pattern highlighter ID as a custom attribute if there is none already.
178 | if (!node.dataset.phid) {
179 | node.dataset.phid = this.counter;
180 | // Increment the ID counter.
181 | this.counter += 1;
182 | }
183 | }
184 | }
185 |
186 | /**
187 | * Searches the specified DOM tree for an element with the specified pattern highlighter ID.
188 | * @param {Node} dom The DOM tree in which to search for the element.
189 | * @param {number} id The ID of the element to search for.
190 | * @returns {(Element|null)} The element with the searched ID or `null` if no element with the ID was found.
191 | */
192 | function getElementByPhid(dom, id) {
193 | // Return the element on the page with the pattern highlighter ID of `id`.
194 | return dom.querySelector(`[data-phid="` + id + `"]`)
195 | }
196 |
197 | /**
198 | * Removes all elements on the `tagBlacklist` from the specified DOM tree.
199 | * @param {Node} dom The DOM tree from which the elements will be removed.
200 | */
201 | function removeBlacklistNodes(dom) {
202 | // Iterate over all elements on the page with a tag from the `tagBlacklist`.
203 | for (const elem of dom.querySelectorAll(constants.tagBlacklist.join(","))) {
204 | // Remove the element from the DOM.
205 | elem.remove();
206 | }
207 | }
208 |
209 | /**
210 | * Checks a DOM node for patterns. This is done using the detection functions defined in the `patternConfig`.
211 | * @param {Node} node The DOM node to be inspected for patterns.
212 | * @param {Node} [nodeOld] The previous state of the DOM node to be checked for patterns, if present.
213 | * @returns {(string|null)} The class name of the pattern type, if one was detected, otherwise `null`.
214 | */
215 | function findPatterInNode(node, nodeOld) {
216 | // Iterate over all patterns in the `patternConfig`.
217 | for (const pattern of constants.patternConfig.patterns) {
218 | // Iterate over all detection functions for the pattern. Usually is only a single one.
219 | for (const func of pattern.detectionFunctions) {
220 | // Pass the two parameters to the detection function and check if the pattern is detected.
221 | if (func(node, nodeOld)) {
222 | // If the detection function returns `true`, the respective pattern was detected.
223 | // The class name of the pattern is returned and the function terminates.
224 | return pattern.className;
225 | }
226 | }
227 | }
228 | return null;
229 | }
230 |
231 | /**
232 | * Recursively finds patterns within a DOM tree or node.
233 | * The recognition functions from the `patternConfig` are used.
234 | * If elements are identified as patterns, respective classes are added to them.
235 | * @param {Node} node A DOM node or a complete DOM tree in which to search for patterns.
236 | * @param {Node} domOld The complete previous state of the DOM tree of the page.
237 | */
238 | function findPatternDeep(node, domOld) {
239 | // Iterate over all child nodes of the provided DOM node.
240 | for (const child of node.children) {
241 | // Execute the function recursively on each child node.
242 | findPatternDeep(child, domOld);
243 | }
244 |
245 | // Extract the previous state of the node from the old DOM. Is `null` if the node did not exist yet.
246 | let nodeOld = getElementByPhid(domOld, node.dataset.phid);
247 | // Check if the node represents one of the patterns.
248 | let foundPattern = findPatterInNode(node, nodeOld);
249 |
250 | // If a pattern is detected, add appropriate classes to the element
251 | // and remove it from the DOM for the further pattern search.
252 | if (foundPattern) {
253 | // Find the element in the original DOM.
254 | let elem = getElementByPhid(document, node.dataset.phid);
255 | // Check if the element still exists.
256 | if (elem) {
257 | // Add a general class for patterns to the element
258 | // and a class for the specific pattern the element represents.
259 | elem.classList.add(
260 | constants.patternDetectedClassName,
261 | constants.extensionClassPrefix + foundPattern
262 | );
263 | }
264 | // Remove the previous state of the node, if it exists.
265 | if (nodeOld) {
266 | nodeOld.remove();
267 | }
268 | // Remove the current state of the node.
269 | node.remove();
270 | }
271 | }
272 |
273 | /**
274 | * Removes the classes that are assigned to found patterns from all pattern elements.
275 | */
276 | function resetDetectedPatterns() {
277 | // Regular expression to find all classes belonging to the extension.
278 | let regx = new RegExp("\\b" + constants.extensionClassPrefix + "[^ ]*[ ]?\\b", "g");
279 | // Iterate over all detected pattern elements.
280 | document.querySelectorAll("." + constants.patternDetectedClassName).forEach(
281 | function (node) {
282 | // Remove all classes belonging to the extension.
283 | node.className = node.className.replace(regx, "");
284 | }
285 | );
286 | }
287 |
288 | /**
289 | * Checks whether an element is visible based on its DOM node.
290 | * @param {Node} elem DOM node that is checked for visibility.
291 | * @returns {boolean} `true` if the element is visible, `false` otherwise.
292 | */
293 | function elementIsVisible(elem) {
294 | // Get the 'actual' style of the element after applying active stylesheets.
295 | const computedStyle = getComputedStyle(elem);
296 | // Check if the element has explicit CSS styles which hide it or make it invisible.
297 | if (computedStyle.visibility == "hidden" || computedStyle.display == "none" || computedStyle.opacity == "0") {
298 | // Return `false` if the element is not visible.
299 | return false;
300 | }
301 | // According to the CSS Object Model (CSSOM),
302 | // all of these three values should return `0`
303 | // if the element has no layout box and is therefore not visible.
304 | // Edge cases (false positives) cannot be ruled out, but should be rare.
305 | return !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length);
306 | };
307 |
308 | /**
309 | * Creates an object with the counts of detected patterns and
310 | * the pattern highlighter IDs of the corresponding elements on the page.
311 | * @returns {object} The object with the information and counts about the detected patterns.
312 | */
313 | function getPatternsResults() {
314 | // Initialize the result object with all required keys.
315 | let results = {
316 | // An array with the pattern highlighter IDs of the detected elements for each pattern.
317 | // The elements are divided into two arrays according to the property visible or hidden.
318 | // Each object in the `patterns` array contains the `name` key with the name of the pattern.
319 | "patterns": [],
320 | // The total count of detected elements that represent patterns and are visible on the page.
321 | "countVisible": 0,
322 | // The total count of detected elements that represent patterns.
323 | "count": 0,
324 | }
325 | // Iterate over all patterns in the `patternConfig`.
326 | for (const pattern of constants.patternConfig.patterns) {
327 | // Array to collect all visible elements to the pattern.
328 | let elementsVisible = [];
329 | // Array to collect all hidden elements to the pattern.
330 | let elementsHidden = [];
331 |
332 | // Iterate over all elements that represent the current pattern.
333 | for (const elem of document.getElementsByClassName(constants.extensionClassPrefix + pattern.className)) {
334 | // Depending on whether the element is visible or hidden,
335 | // add its pattern highlighter ID to the appropriate array.
336 | if (elementIsVisible(elem)) {
337 | elementsVisible.push(elem.dataset.phid);
338 | } else {
339 | elementsHidden.push(elem.dataset.phid);
340 | }
341 | }
342 |
343 | // Add the name of the pattern and the two arrays with the elements as an object to the result object.
344 | results.patterns.push({
345 | name: pattern.name,
346 | elementsVisible: elementsVisible,
347 | elementsHidden: elementsHidden,
348 | });
349 |
350 | // Add the number of visible detected elements of the pattern
351 | // to the total number of visible detected elements.
352 | results.countVisible += elementsVisible.length;
353 | // Add the count of detected elements of the pattern to the total count of detected elements.
354 | results.count += elementsVisible.length + elementsHidden.length;
355 | }
356 | // Return the complete result object.
357 | return results;
358 | }
359 |
360 | /**
361 | * Send the information and counts about the detected patterns to the other extension scripts.
362 | */
363 | function sendResults() {
364 | // Create the result object with all information and counts.
365 | let results = getPatternsResults();
366 |
367 | // Send the object to all other extension scripts. Do nothing in the event of a reply.
368 | brw.runtime.sendMessage(
369 | results,
370 | function (response) { }
371 | );
372 |
373 | // Print out the number of visible pattern elements.
374 | console.log(brw.i18n.getMessage("infoNumberPatternsFound", [results.countVisible.toString()]));
375 | }
376 |
377 | /**
378 | * @typedef {object} Position
379 | * @property {number} left - The offset from the left
380 | * @property {number} top - The offset from the top
381 | */
382 | /**
383 | * Compute the absolute offset of an element on the page using its DOM node.
384 | * @param {Node} elem DOM node from which the absolute position is determined.
385 | * @returns {Position}
386 | */
387 | function getAbsoluteOffsetFromBody(elem) {
388 | // Get a DOMRect object with the element's position relative to the viewport.
389 | const rect = elem.getBoundingClientRect();
390 | // Return the distance of the element to the left and top edge of the page in pixels.
391 | return {
392 | left: rect.left + window.scrollX,
393 | top: rect.top + window.scrollY
394 | };
395 | }
396 |
397 | /**
398 | * Shows an element on the page by automatically scrolling so that the element is vertically centered in the viewport.
399 | * Additionally, a catchy shadow is added for a few seconds, whose appearance is predefined by corresponding CSS styles.
400 | * @param {number} phid The pattern highlighter ID of the element that will be shown.
401 | */
402 | function showElement(phid) {
403 | // Remove all old shadow elements.
404 | for (const element of document.getElementsByClassName(constants.currentPatternClassName)) {
405 | element.remove();
406 | }
407 |
408 | // Get the element to be shown by its ID.
409 | let elem = getElementByPhid(document, phid);
410 |
411 | // Check if the element with the `phid` exists or if no element with the ID was found.
412 | if (elem == null) {
413 | // If the element does not exist, exit the function to prevent errors.
414 | // Since all components of the extension are constantly updated and receive the new IDs,
415 | // this case is not really to be expected.
416 | return;
417 | }
418 |
419 | // Scroll to the element so that it is displayed in the center of the viewport.
420 | elem.scrollIntoView({
421 | behavior: "smooth",
422 | block: "center",
423 | inline: "center"
424 | });
425 |
426 | // Create an element that will be used as a shadow for the pattern element.
427 | let highlightShadowElem = document.createElement("div");
428 |
429 | // Align it on the page so that it is in the same place on the page
430 | // with the same size as the pattern element that is shown.
431 | highlightShadowElem.style.position = "absolute";
432 | highlightShadowElem.style.height = elem.offsetHeight + "px";
433 | highlightShadowElem.style.width = elem.offsetWidth + "px";
434 | let elemXY = getAbsoluteOffsetFromBody(elem);
435 | highlightShadowElem.style.top = elemXY.top + "px";
436 | highlightShadowElem.style.left = elemXY.left + "px";
437 |
438 | // Add a class for which there are predefined styles to represent the shadow.
439 | highlightShadowElem.classList.add(constants.currentPatternClassName);
440 |
441 | // Add the shadow element to the DOM.
442 | document.body.appendChild(highlightShadowElem);
443 | }
444 |
--------------------------------------------------------------------------------
/chrome/scripts/constants.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The object to access the API functions of the browser.
3 | * @constant
4 | * @type {{runtime: object, i18n: object}} BrowserAPI
5 | */
6 | const brw = chrome;
7 |
8 | /**
9 | * Configuration of the pattern detection functions.
10 | * The following attributes must be specified for each pattern.
11 | * - `name`: The name of the pattern that will be displayed on the UI.
12 | * - `className`: A valid CSS class name for the pattern (used only internally and not displayed).
13 | * - `detectionFunctions`: An array of functions `f(node, nodeOld)` to detect the pattern.
14 | * Parameters of the functions are the HTML node to be examined in current and previous state (in this order).
15 | * The functions must return `true` if the pattern was detected and `false` if not.
16 | * - `infoUrl`: The URL to the explanation of the pattern on the `dapde.de` website.
17 | * - `info`: A brief explanation of the pattern.
18 | * - `languages`: An array of ISO 639-1 codes of the languages supported by the detection functions..
19 | * @constant
20 | * @type {{
21 | * patterns: Array.<{
22 | * name: string,
23 | * className: string,
24 | * detectionFunctions: Array.,
25 | * infoUrl: string,
26 | * info: string,
27 | * languages: Array.
28 | * }>
29 | * }}
30 | */
31 | export const patternConfig = {
32 | patterns: [
33 | {
34 | /**
35 | * Countdown Pattern.
36 | * Countdown patterns induce (truthfully or falsely) the impression that a product or service is only available for a certain period of time.
37 | * This is illustrated through a running clock or a lapsing bar.
38 | * You can watch as the desired good slips away.
39 | */
40 | name: brw.i18n.getMessage("patternCountdown_name"),
41 | className: "countdown",
42 | detectionFunctions: [
43 | function (node, nodeOld) {
44 | // Countdowns should only be identified as such if they are actively running and not static.
45 | // Therefore, it is necessary to check first if there is an old state of the element and if the text in it has changed.
46 | if (nodeOld && node.innerText != nodeOld.innerText) {
47 | /**
48 | * Regular expression for a usual countdown with or without words.
49 | * @constant
50 | */
51 | const reg = /(?:\d{1,2}\s*:\s*){1,3}\d{1,2}|(?:\d{1,2}\s*(?:days?|hours?|minutes?|seconds?|tage?|stunden?|minuten?|sekunden?|[a-zA-Z]{1,3}\.?)(?:\s*und)?\s*){2,4}/gi;
52 |
53 | /**
54 | * Regular expression for strings that match the regular expression for countdowns
55 | * but are not countdowns because there are too many numbers.
56 | * A maximum of 4 numbers for days, hours, minutes and seconds is expected.
57 | * @constant
58 | */
59 | const regBad = /(?:\d{1,2}\s*:\s*){4,}\d{1,2}|(?:\d{1,2}\s*(?:days?|hours?|minutes?|seconds?|tage?|stunden?|minuten?|sekunden?|[a-zA-Z]{1,3}\.?)(?:\s*und)?\s*){5,}/gi;
60 |
61 | // If matches for "wrong" countdowns are found with the second regular expression,
62 | // remove these parts from the string.
63 | // Then search for matches for real countdowns in the remaining string.
64 | // Do this for the old and current state of the text.
65 | let matchesOld = nodeOld.innerText.replace(regBad, "").match(reg);
66 | let matchesNew = node.innerText.replace(regBad, "").match(reg);
67 |
68 | // If no matches were found in one of the two states of the texts or
69 | // if the number of matches in the two states does not match,
70 | // the element is not classified as a countdown.
71 | if (matchesNew == null || matchesOld == null ||
72 | (matchesNew != null && matchesOld != null
73 | && matchesNew.length != matchesOld.length)) {
74 | return false;
75 | }
76 |
77 | // Since it was ensured at the point that there are the same number of matches
78 | // in both states of the text, it is initially assumed that the matches with the same index
79 | // in both states are the same countdown.
80 | for (let i = 0; i < matchesNew.length; i++) {
81 | // Extract all contiguous numbers from the strings.
82 | // Example: `"23:59:58"` -> `["23", "59", "58"]`.
83 | let numbersNew = matchesNew[i].match(/\d+/gi);
84 | let numbersOld = matchesOld[i].match(/\d+/gi);
85 |
86 | // If the number of each number does not match,
87 | // then the pair of countdowns does not match.
88 | if (numbersNew.length != numbersOld.length) {
89 | // Ignore this pair and examine at the next one.
90 | continue;
91 | }
92 |
93 | // Iterate through all pairs of numbers in the strings.
94 | for (let x = 0; x < numbersNew.length; x++) {
95 | // Since countdowns should be detected that are running down,
96 | // the numbers from left to right become smaller over time.
97 | // When the numbers are iterated from left to right,
98 | // at least one number in the current state of the text
99 | // should be smaller than in the old state.
100 | // If a number in the current state is larger before a number
101 | // is smaller than in the previous state, it does not seem to be an elapsing countdown.
102 | // Examples: current state - previous state -> result
103 | // 23,30,40 - 23,30,39 -> is a countdown
104 | // 23,30,00 - 23,29,59 -> is a countdown
105 | // 23,30,40 - 23,31,20 -> is not a countdown
106 | // 23,30,40 - 23,30,41 -> is not a countdown
107 | // 23,30,40 - 23,30,40 -> is not a countdown
108 | if (parseInt(numbersNew[x]) > parseInt(numbersOld[x])) {
109 | // If the number in the current state is larger,
110 | // break out of the loop and examine the next pair, if present.
111 | // This case occurs only if the second if-clause did not occur and `true` was returned.
112 | break;
113 | }
114 | if (parseInt(numbersNew[x]) < parseInt(numbersOld[x])) {
115 | // Return `true` if a number has decreased.
116 | return true;
117 | }
118 | }
119 | }
120 | }
121 | // Return `false` if no countdown was detected by the previous steps.
122 | return false;
123 | }
124 | ],
125 | infoUrl: brw.i18n.getMessage("patternCountdown_infoUrl"),
126 | info: brw.i18n.getMessage("patternCountdown_info"),
127 | languages: [
128 | "en",
129 | "de"
130 | ]
131 | },
132 | {
133 | /**
134 | * Scarcity Pattern.
135 | * The Scarcity Pattern induces (truthfully or falsely) the impression that goods or services are only available in limited numbers.
136 | * The pattern suggests: Buy quickly, otherwise the beautiful product will be gone!
137 | * Scarcity Patterns are also used in versions where the alleged scarcity is simply invented or
138 | * where it is not made clear whether the limited availability relates to the product as a whole or only to the contingent of the portal visited.
139 | */
140 | name: brw.i18n.getMessage("patternScarcity_name"),
141 | className: "scarcity",
142 | detectionFunctions: [
143 | function (node, nodeOld) {
144 | // Return true if a match is found in the current text of the element,
145 | // using a regular expression for the scarcity pattern with English words.
146 | // The regular expression checks whether a number is followed by one of several keywords
147 | // or alternatively if the word group 'last/final article/item' is present.
148 | // The previous state of the element is not used.
149 | // Example: "10 pieces available"
150 | // "99% claimed"
151 | return /\d+\s*(?:\%|pieces?|pcs\.?|pc\.?|ct\.?|items?)?\s*(?:available|sold|claimed|redeemed)|(?:last|final)\s*(?:article|item)/i.test(node.innerText);
152 | },
153 | function (node, nodeOld) {
154 | // Return true if a match is found in the current text of the element,
155 | // using a regular expression for the scarcity pattern with German words.
156 | // The regular expression checks whether a number is followed by one of several keywords
157 | // or alternatively if the word group 'last article' (`letzter\s*Artikel`) is present.
158 | // The previous state of the element is not used.
159 | // Example: "10 Stück verfügbar"
160 | // "99% eingelöst"
161 | return /\d+\s*(?:\%|stücke?|stk\.?)?\s*(?:verfügbar|verkauft|eingelöst)|letzter\s*Artikel/i.test(node.innerText);
162 | }
163 | ],
164 | infoUrl: brw.i18n.getMessage("patternScarcity_infoUrl"),
165 | info: brw.i18n.getMessage("patternScarcity_info"),
166 | languages: [
167 | "en",
168 | "de"
169 | ]
170 | },
171 | {
172 | /**
173 | * Social Proof Pattern.
174 | * Social Proof is another Dark Pattern of this category.
175 | * Positive product reviews or activity reports from other users are displayed directly.
176 | * Often, these reviews or reports are simply made up.
177 | * But authentic reviews or reports also influence the purchase decision through smart selection and placement.
178 | */
179 | name: brw.i18n.getMessage("patternSocialProof_name"),
180 | className: "social-proof",
181 | detectionFunctions: [
182 | function (node, nodeOld) {
183 | // Return true if a match is found in the current text of the element,
184 | // using a regular expression for the social proof pattern with English words.
185 | // The regular expression checks whether a number is followed by a combination of different keywords.
186 | // The previous state of the element is not used.
187 | // Example: "5 other customers also bought this article"
188 | // "6 buyers have rated the following products [with 5 stars]"
189 | return /\d+\s*(?:other)?\s*(?:customers?|clients?|buyers?|users?|shoppers?|purchasers?|people)\s*(?:have\s+)?\s*(?:(?:also\s*)?(?:bought|purchased|ordered)|(?:rated|reviewed))\s*(?:this|the\s*following)\s*(?:product|article|item)s?/i.test(node.innerText);
190 | },
191 | function (node, nodeOld) {
192 | // Return true if a match is found in the current text of the element,
193 | // using a regular expression for the social proof pattern with German words.
194 | // The regular expression checks whether a number is followed by a combination of different keywords.
195 | // The previous state of the element is not used.
196 | // Example: "5 andere Kunden kauften auch diesen Artikel"
197 | // "6 Käufer*innen haben folgende Produkte [mit 5 Sternen bewertet]"
198 | return /\d+\s*(?:andere)?\s*(?:Kunden?|Käufer|Besteller|Nutzer|Leute|Person(?:en)?)(?:(?:\s*\/\s*)?[_\-\*]?innen)?\s*(?:(?:kauften|bestellten|haben)\s*(?:auch|ebenfalls)?|(?:bewerteten|rezensierten))\s*(?:diese[ns]?|(?:den|die|das)?\s*folgenden?)\s*(?:Produkte?|Artikel)/i.test(node.innerText);
199 | }
200 | ],
201 | infoUrl: brw.i18n.getMessage("patternSocialProof_infoUrl"),
202 | info: brw.i18n.getMessage("patternSocialProof_info"),
203 | languages: [
204 | "en",
205 | "de"
206 | ]
207 | },
208 | {
209 | /**
210 | * Forced Continuity Pattern (adapted to German web pages).
211 | * The Forced Continuity pattern automatically renews free or low-cost trial subscriptions - but for a fee or at a higher price.
212 | * The design trick is that the order form visually suggests that there is no charge and conceals the (automatic) follow-up costs.
213 | */
214 | name: brw.i18n.getMessage("patternForcedContinuity_name"),
215 | className: "forced-continuity",
216 | detectionFunctions: [
217 | function (node, nodeOld) {
218 | // Return true if a match is found in the current text of the element,
219 | // using multiple regular expressions for the forced proof continuity with English words.
220 | // The regular expressions check if one of three combinations of a price specification
221 | // in Euro, Dollar or Pound and the specification of a month is present.
222 | // The previous state of the element is not used.
223 | if (/(?:(?:€|EUR|GBP|£|\$|USD)\s*\d+(?:\.\d{2})?|\d+(?:\.\d{2})?\s*(?:euros?|€|EUR|GBP|£|pounds?(?:\s*sterling)?|\$|USD|dollars?))\s*(?:(?:(?:per|\/|a)\s*month)|(?:p|\/)m)\s*(?:after|from\s*(?:month|day)\s*\d+)/i.test(node.innerText)) {
224 | // Example: "$10.99/month after"
225 | // "11 GBP a month from month 4"
226 | return true;
227 | }
228 | if (/(?:(?:€|EUR|GBP|£|\$|USD)\s*\d+(?:\.\d{2})?|\d+(?:\.\d{2})?\s*(?:euros?|€|EUR|GBP|£|pounds?(?:\s*sterling)?|\$|USD|dollars?))\s*(?:after\s*(?:the)?\s*\d+(?:th|nd|rd|th)?\s*(?:months?|days?)|from\s*(?:month|day)\s*\d+)/i.test(node.innerText)) {
229 | // Example: "$10.99 after 12 months"
230 | // "11 GBP from month 4"
231 | return true;
232 | }
233 | if (/(?:after\s*that|then|afterwards|subsequently)\s*(?:(?:€|EUR|GBP|£|\$|USD)\s*\d+(?:\.\d{2})?|\d+(?:\.\d{2})?\s*(?:euros?|€|EUR|GBP|£|pounds?(?:\s*sterling)?|\$|USD|dollars?))\s*(?:(?:(?:per|\/|a)\s*month)|(?:p|\/)m)/i.test(node.innerText)) {
234 | // Example: "after that $23.99 per month"
235 | // "then GBP 10pm"
236 | return true;
237 | }
238 | if (/after\s*(?:the)?\s*\d+(?:th|nd|rd|th)?\s*months?\s*(?:only|just)?\s*(?:(?:€|EUR|GBP|£|\$|USD)\s*\d+(?:\.\d{2})?|\d+(?:\.\d{2})?\s*(?:euros?|€|EUR|GBP|£|pounds?(?:\s*sterling)?|\$|USD|dollars?))/i.test(node.innerText)) {
239 | // Example: "after the 24th months only €23.99"
240 | // "after 6 months $10"
241 | return true;
242 | }
243 | // Return `false` if no regular expression matches.
244 | return false;
245 | },
246 | function (node, nodeOld) {
247 | // Return true if a match is found in the current text of the element,
248 | // using multiple regular expressions for the forced proof continuity with German words.
249 | // The regular expressions check if one of three combinations of a price specification
250 | // in Euro and the specification of a month is present.
251 | // The previous state of the element is not used.
252 | if (/\d+(?:,\d{2})?\s*(?:Euro|€)\s*(?:(?:pro|im|\/)\s*Monat)?\s*(?:ab\s*(?:dem)?\s*\d+\.\s*Monat|nach\s*\d+\s*(?:Monaten|Tagen)|nach\s*(?:einem|1)\s*Monat)/i.test(node.innerText)) {
253 | // Example: "10,99 Euro pro Monat ab dem 12. Monat"
254 | // "11€ nach 30 Tagen"
255 | return true;
256 | }
257 | if (/(?:anschließend|danach)\s*\d+(?:,\d{2})?\s*(?:Euro|€)\s*(?:pro|im|\/)\s*Monat/i.test(node.innerText)) {
258 | // Example: "anschließend 23,99€ pro Monat"
259 | // "danach 10 Euro/Monat"
260 | return true;
261 | }
262 | if (/\d+(?:,\d{2})?\s*(?:Euro|€)\s*(?:pro|im|\/)\s*Monat\s*(?:anschließend|danach)/i.test(node.innerText)) {
263 | // Example: "23,99€ pro Monat anschließend"
264 | // "10 Euro/Monat danach"
265 | return true;
266 | }
267 | if (/ab(?:\s*dem)?\s*\d+\.\s*Monat(?:\s*nur)?\s*\d+(?:,\d{2})?\s*(?:Euro|€)/i.test(node.innerText)) {
268 | // Example: "ab dem 24. Monat nur 23,99 Euro"
269 | // "ab 6. Monat 9,99€"
270 | return true;
271 | }
272 | // Return `false` if no regular expression matches.
273 | return false;
274 | }
275 | ],
276 | infoUrl: brw.i18n.getMessage("patternForcedContinuity_infoUrl"),
277 | info: brw.i18n.getMessage("patternForcedContinuity_info"),
278 | languages: [
279 | "en",
280 | "de"
281 | ]
282 | }
283 | ]
284 | }
285 |
286 | /**
287 | * Checks if the `patternConfig` is valid.
288 | * @returns {boolean} `true` if the `patternConfig` is valid, `false` otherwise.
289 | */
290 | function validatePatternConfig() {
291 | // Create an array with the names of the configured patterns.
292 | let names = patternConfig.patterns.map(p => p.name);
293 | // Check if there are duplicate names.
294 | if ((new Set(names)).size !== names.length) {
295 | // If there are duplicate names, the configuration is invalid.
296 | return false;
297 | }
298 | // Check every single configured pattern for validity.
299 | for (let pattern of patternConfig.patterns) {
300 | // Ensure that the name is a non-empty string.
301 | if (!pattern.name || typeof pattern.name !== "string") {
302 | return false;
303 | }
304 | // Ensure that the class name is a non-empty string.
305 | if (!pattern.className || typeof pattern.className !== "string") {
306 | return false;
307 | }
308 | // Ensure that the detection functions are a non-empty array.
309 | if (!Array.isArray(pattern.detectionFunctions) || pattern.detectionFunctions.length <= 0) {
310 | return false;
311 | }
312 | // Check every single configured detection function for validity.
313 | for (let detectionFunc of pattern.detectionFunctions) {
314 | // Ensure that the detection function is a function with two arguments.
315 | if (typeof detectionFunc !== "function" || detectionFunc.length !== 2) {
316 | return false;
317 | }
318 | }
319 | // Ensure that the info URL is a non-empty string.
320 | if (!pattern.infoUrl || typeof pattern.infoUrl !== "string") {
321 | return false;
322 | }
323 | // Ensure that the info/explanation is a non-empty string.
324 | if (!pattern.info || typeof pattern.info !== "string") {
325 | return false;
326 | }
327 | // Ensure that the languages are a non-empty array.
328 | if (!Array.isArray(pattern.languages) || pattern.languages.length <= 0) {
329 | return false;
330 | }
331 | // Check every single language for being a non-empty string.
332 | for (let language of pattern.languages) {
333 | // Ensure that the language is a non-empty string.
334 | if (!language || typeof language !== "string") {
335 | return false;
336 | }
337 | }
338 | }
339 | // If all checks have been passed successfully, the configuration is valid and `true` is returned.
340 | return true;
341 | }
342 |
343 | /**
344 | * @type {boolean} `true` if the `patternConfig` is valid, `false` otherwise.
345 | */
346 | export const patternConfigIsValid = validatePatternConfig();
347 |
348 | /**
349 | * Prefix for all CSS classes that are added to elements on websites by the extension.
350 | * @constant
351 | */
352 | export const extensionClassPrefix = "__ph__";
353 |
354 | /**
355 | * The class that is added to elements detected as patterns.
356 | * Elements with this class get a black border from the CSS styles.
357 | * @constant
358 | */
359 | export const patternDetectedClassName = extensionClassPrefix + "pattern-detected";
360 |
361 | /**
362 | * A class for the elements created as shadows for pattern elements
363 | * for displaying individual elements using the popup.
364 | */
365 | export const currentPatternClassName = extensionClassPrefix + "current-pattern";
366 |
367 | /**
368 | * A list of HTML tags that should be ignored during pattern detection.
369 | * The elements with these tags are removed from the DOM copy.
370 | */
371 | export const tagBlacklist = ["script", "style", "noscript", "audio", "video"];
372 |
--------------------------------------------------------------------------------
/chrome/popup/popup.js:
--------------------------------------------------------------------------------
1 | // Import the constants from the module.
2 | import * as constants from "../scripts/constants.js";
3 |
4 | // Import the required components from the Lit Library
5 | import { LitElement, html, css } from '../scripts/lit/lit-core.min.js';
6 |
7 | // Import component styles
8 | import { onOffSwitchStyles, sharedStyles, actionButtonStyles, patternsListStyles, patternLinkStyles } from "./styles.js";
9 |
10 | /**
11 | * The object to access the API functions of the browser.
12 | * @constant
13 | * @type {{runtime: object, tabs: object, i18n: object}} BrowserAPI
14 | */
15 | const brw = chrome;
16 |
17 | /**
18 | * An enum-like object that defines numbers for activation states of the extension.
19 | * @constant
20 | * @type {Object.}
21 | */
22 | const activationState = Object.freeze({
23 | On: 1,
24 | Off: 0,
25 | PermanentlyOff: -1,
26 | });
27 |
28 | // Add an event handler that processes incoming messages.
29 | // Expected messages to the popup are the results of the pattern detection from the content script.
30 | brw.runtime.onMessage.addListener(
31 | function (message, sender, sendResponse) {
32 | // Pass the message to the corresponding method of the `ExtensionPopup` component.
33 | document.querySelector("extension-popup").handleMessage(message, sender, sendResponse);
34 | }
35 | );
36 |
37 | /**
38 | * A function to get information about the currently opened tab.
39 | * The current tab is always the tab in which the popup was opened.
40 | * When the tab is changed, the popup is also closed automatically.
41 | * @returns {Promise.<{url: string, id: number, windowId: number}>}
42 | */
43 | async function getCurrentTab() {
44 | return (await brw.tabs.query({ active: true, currentWindow: true }))[0];
45 | }
46 |
47 | /**
48 | * Lit component for the entire popup.
49 | * Uses all other Lit components defined below.
50 | * @extends LitElement
51 | */
52 | export class ExtensionPopup extends LitElement {
53 | // From the Lit documentation (https://lit.dev/docs/components/properties/):
54 | // "Reactive properties are properties that can trigger the reactive update cycle when changed,
55 | // re-rendering the component, and optionally be read or written to attributes.".
56 | static properties = {
57 | // Variable for the internal activation state of the extension for the current tab.
58 | // Can be set by the on/off switch.
59 | activation: { type: Number },
60 | // Variable for the initial activation state of the extension for the current tab.
61 | // Will only be changed after a new activation state is sent to the background script and the page is refreshed.
62 | initActivation: { type: Number },
63 | // Variable for the results of the pattern detection from the content script.
64 | results: { type: Object }
65 | };
66 |
67 | constructor() {
68 | super();
69 | // Check if the pattern configuration is valid.
70 | if (!constants.patternConfigIsValid) {
71 | // If the configuration is not valid, the content script does not start the pattern highlighting
72 | // and the extension is permanently disabled. Therefore set the status to permanently off.
73 | this.activation = activationState.PermanentlyOff;
74 | } else {
75 | // Otherwise, set the activation state to off by default.
76 | // This will be overwritten with the true status later.
77 | this.activation = activationState.Off;
78 | }
79 | this.initActivation = this.activation;
80 | // Set the results initially to an empty dictionary. The true results will be loaded later.
81 | this.results = {};
82 | }
83 |
84 | /**
85 | * Function to process incoming messages.
86 | * @param {object} message The received message.
87 | * @param {MessageSender} sender Data about the sender of the message.
88 | * @param {function} sendResponse Function to send a reply.
89 | */
90 | async handleMessage(message, sender, sendResponse) {
91 | // Check if the message contains results from the pattern detection.
92 | if ("countVisible" in message) {
93 | // Check if the message was sent by the active tab from the current window.
94 | if (sender.tab.active && (await getCurrentTab()).windowId === sender.tab.windowId) {
95 | // Set the `results` property of the popup to the data from the message.
96 | this.results = message;
97 | }
98 | }
99 | }
100 |
101 | /**
102 | * From the Lit documentation (https://lit.dev/docs/components/lifecycle/):
103 | * "Called after the element's DOM has been updated the first time, immediately before `updated()` is called.".
104 | * This function is used to load and set the activation state and results.
105 | * Since asynchronous methods are used for this, this is not done in the constructor.
106 | */
107 | async firstUpdated() {
108 | // Check if the activation state has already been set as permanently disabled
109 | // due to an invalid configuration.
110 | if (this.activation === activationState.PermanentlyOff) {
111 | // If yes, then exit the function.
112 | return;
113 | }
114 | // Load data about the current tab.
115 | let currentTab = await getCurrentTab();
116 | // Load the activation state and results, if the tab contains a web page loaded with HTTP(S),
117 | // which means that the extension's content script will be injected.
118 | if (currentTab.url.toLowerCase().startsWith("http://") || currentTab.url.toLowerCase().startsWith("https://")) {
119 | // Load the activation state.
120 | let currentTabActivation = await brw.runtime.sendMessage({ "action": "getActivationState", "tabId": currentTab.id });
121 | // Only do more, if the extension is activated.
122 | if (currentTabActivation.isEnabled) {
123 | // Set the activation state to on.
124 | this.activation = activationState.On;
125 |
126 | // In case the popup was opened before the web page was fully loaded in the tab,
127 | // the request of the results fails because the content script cannot respond yet.
128 | // Therefore, the request is repeated until it is successful.
129 | while (true) {
130 | try {
131 | // Load the results of the pattern detection.
132 | this.results = await brw.tabs.sendMessage(currentTab.id, { action: "getPatternCount" });
133 | // Break out of the infinite loop if the request did not throw an error.
134 | break;
135 | } catch (error) {
136 | // Wait for 250 milliseconds.
137 | await new Promise(resolve => { setTimeout(resolve, 250) });
138 | }
139 | }
140 | }
141 | } else {
142 | // If the extension's content script is not injected, set the activation state to permanently off,
143 | // because in this case the extension cannot be activated.
144 | this.activation = activationState.PermanentlyOff;
145 | }
146 | // Set the initial activation state to the state just loaded at initialization.
147 | this.initActivation = this.activation;
148 | }
149 |
150 | /**
151 | * Render the HTML of the component.
152 | * @returns {html} HTML of the component
153 | */
154 | render() {
155 | return html`
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | `;
165 | }
166 | }
167 | // Define a custom element for the component so that it can be used in the HTML DOM.
168 | customElements.define("extension-popup", ExtensionPopup);
169 |
170 | /**
171 | * Lit component for the header/title of the popup.
172 | * @extends LitElement
173 | */
174 | export class PopupHeader extends LitElement {
175 | // CSS styles for the HTML elements in the component.
176 | static styles = [
177 | sharedStyles,
178 | css`
179 | h3 {
180 | color: red;
181 | }
182 | `
183 | ];
184 |
185 | /**
186 | * Render the HTML of the component.
187 | * @returns {html} HTML of the component
188 | */
189 | render() {
190 | return html`
191 |
` : html``}
194 | `;
195 | }
196 | }
197 | // Define a custom element for the component so that it can be used in the HTML DOM.
198 | customElements.define("popup-header", PopupHeader);
199 |
200 | /**
201 | * Lit component for the on/off switch of the popup.
202 | * @extends LitElement
203 | */
204 | export class OnOffSwitch extends LitElement {
205 | // Reactive properties
206 | static properties = {
207 | // Variable for the activation state of the component.
208 | activation: { type: Number },
209 | // Variable for the reference to the parent component.
210 | app: { type: Object }
211 | };
212 |
213 | // CSS styles for the HTML elements in the component.
214 | static styles = [
215 | sharedStyles,
216 | onOffSwitchStyles
217 | ];
218 |
219 | /**
220 | * Function that handles a change of the on/off switch value.
221 | * @param {Event} event
222 | */
223 | async changeActivation(event) {
224 | if (this.activation !== activationState.PermanentlyOff) {
225 | if (this.activation === activationState.Off) {
226 | this.activation = activationState.On;
227 | } else {
228 | this.activation = activationState.Off;
229 | }
230 | this.app.activation = this.activation;
231 | }
232 | }
233 |
234 | /**
235 | * Render the HTML of the component.
236 | * @returns {html} HTML of the component
237 | */
238 | render() {
239 | return html`
240 |
241 |
245 |
249 |
250 | `;
251 | }
252 | }
253 | // Define a custom element for the component so that it can be used in the HTML DOM.
254 | customElements.define("on-off-switch", OnOffSwitch);
255 |
256 | /**
257 | * Lit component for the refresh button of the popup.
258 | * @extends LitElement
259 | */
260 | export class RefreshButton extends LitElement {
261 | // Reactive properties
262 | static properties = {
263 | // Variable that specifies whether the component should be hidden.
264 | hide: { type: Boolean },
265 | // Variable for the reference to the parent component.
266 | app: { type: Object }
267 | };
268 |
269 | // CSS styles for the HTML elements in the component.
270 | static styles = [
271 | sharedStyles,
272 | actionButtonStyles
273 | ];
274 |
275 | /**
276 | * Function to set the activation state for the current tab and to reload it.
277 | */
278 | async refreshTab() {
279 | // Set the activation state for the current tab.
280 | await brw.runtime.sendMessage({ "enableExtension": this.app.activation === activationState.On, "tabId": (await getCurrentTab()).id });
281 | // Reload the current tab.
282 | await brw.tabs.reload();
283 | // Set the initial activation state of the popup to the new activation state.
284 | this.app.initActivation = this.app.activation;
285 | }
286 |
287 | /**
288 | * Render the HTML of the component.
289 | * @returns {html} HTML of the component
290 | */
291 | render() {
292 | // Return an empty string if the component should be hidden.
293 | if (this.hide) {
294 | return html``;
295 | }
296 | return html`
297 |
300 | `;
301 | }
302 | }
303 | // Define a custom element for the component so that it can be used in the HTML DOM.
304 | customElements.define("refresh-button", RefreshButton);
305 |
306 | /**
307 | * Lit component for the redo button of the popup.
308 | * @extends LitElement
309 | */
310 | export class RedoButton extends LitElement {
311 | // Reactive properties
312 | static properties = {
313 | // Variable for the activation state of the component.
314 | activation: { type: Number }
315 | };
316 |
317 | // CSS styles for the HTML elements in the component.
318 | static styles = [
319 | sharedStyles,
320 | actionButtonStyles
321 | ];
322 |
323 | /**
324 | * Function to trigger the pattern detection in the tab again.
325 | * @param {Event} event
326 | */
327 | async redoPatternCheck(event) {
328 | await brw.tabs.sendMessage((await getCurrentTab()).id, { action: "redoPatternHighlighting" });
329 | }
330 |
331 | /**
332 | * Render the HTML of the component.
333 | * @returns {html} HTML of the component
334 | */
335 | render() {
336 | // Return an empty string if the component is not activated.
337 | if (this.activation !== activationState.On) {
338 | return html``;
339 | }
340 | return html`
341 |
344 | `;
345 | }
346 | }
347 | // Define a custom element for the component so that it can be used in the HTML DOM.
348 | customElements.define("redo-button", RedoButton);
349 |
350 | /**
351 | * Lit component for the list of the detected patterns in the popup.
352 | * @extends LitElement
353 | */
354 | export class FoundPatternsList extends LitElement {
355 | // Reactive properties
356 | static properties = {
357 | // Variable for the activation state of the component.
358 | activation: { type: Number },
359 | // Variable for the results of the pattern detection from the content script.
360 | results: { type: Object }
361 | };
362 |
363 | // CSS styles for the HTML elements in the component.
364 | static styles = [
365 | sharedStyles,
366 | patternsListStyles,
367 | patternLinkStyles
368 | ];
369 |
370 | /**
371 | * Render the HTML of the component.
372 | * @returns {html} HTML of the component
373 | */
374 | render() {
375 | // Return an empty string if the component is not activated.
376 | if (this.activation !== activationState.On) {
377 | return html``;
378 | }
379 | return html`
380 |
396 | `;
397 | }
398 | }
399 | // Define a custom element for the component so that it can be used in the HTML DOM.
400 | customElements.define("found-patterns-list", FoundPatternsList);
401 |
402 | /**
403 | * Lit component for the buttons used to show individual found patterns on the web page.
404 | * @extends LitElement
405 | */
406 | export class ShowPatternButtons extends LitElement {
407 | // Reactive properties
408 | static properties = {
409 | // Variable for the activation state of the component.
410 | activation: { type: Number },
411 | // Variable for the results of the pattern detection from the content script.
412 | results: { type: Object },
413 | // Variable for the pattern highlighter ID of the currently selected pattern.
414 | _currentPatternId: { type: Number, state: true },
415 | // Variable for the list of visible detected patterns.
416 | _visiblePatterns: { type: Array, state: true }
417 | };
418 |
419 | // CSS styles for the HTML elements in the component.
420 | static styles = [
421 | sharedStyles,
422 | patternLinkStyles,
423 | css`
424 | .button {
425 | font-size: large;
426 | cursor: pointer;
427 | user-select: none;
428 | }
429 |
430 | span {
431 | display: inline-block;
432 | text-align: center;
433 | }
434 |
435 | span:not(.button) {
436 | width: 110px;
437 | margin: 0 15px;
438 | }
439 | `
440 | ];
441 |
442 | /**
443 | * Function to extract only the visible elements from the `results` dictionary.
444 | */
445 | extractVisiblePatterns() {
446 | // Initialize the variable with an empty array.
447 | this._visiblePatterns = [];
448 | // Check if patterns were detected.
449 | if (this.results.patterns) {
450 | // Iterate through all detected patterns.
451 | for (const pattern of this.results.patterns) {
452 | // Check if there are visible elements for this pattern.
453 | if (pattern.elementsVisible.length > 0) {
454 | // Iterate through all visible elements.
455 | for (const elem of pattern.elementsVisible) {
456 | // Add the id of the element and the name of the pattern to the visible patterns.
457 | this._visiblePatterns.push({ "phid": elem, "patternName": pattern.name });
458 | }
459 | }
460 | }
461 | }
462 | }
463 |
464 | /**
465 | * Get the index of an element in the `_visiblePatterns` array by its pattern highlighter ID.
466 | * @param {number} phid The pattern highlighter ID.
467 | * @returns {number|-1} The index of the element with the `phid` in the `_visiblePatterns` array
468 | * or `-1` if the element is not in the array.
469 | */
470 | getIndexOfPatternId(phid) {
471 | // Create an array of IDs from the `_visiblePatterns` and get the index of the passed `phid`.
472 | return this._visiblePatterns.map(pattern => pattern.phid).indexOf(phid);
473 | }
474 |
475 | /**
476 | * Show the next or previous pattern element on the website.
477 | * @param {number} step `x` for the next pattern or `-x` for the previous pattern.
478 | */
479 | async showPattern(step) {
480 | /**
481 | * Index of the pattern element to be shown.
482 | */
483 | let idx;
484 | // If there was no pattern shown before, the index must be set to the first or last element of the array.
485 | if (!this._currentPatternId) {
486 | if (step > 0) {
487 | // If one of the next elements should be shown,
488 | // set the index to `0`.
489 | idx = 0;
490 | } else {
491 | // If one of the previous elements should be shown,
492 | // set the index to the last element of the array.
493 | idx = this._visiblePatterns.length - 1;
494 | }
495 | } else {
496 | // If an element has already been shown, use its index as a starting point.
497 | idx = this.getIndexOfPatternId(this._currentPatternId);
498 | if (idx === -1) {
499 | // If the element is no longer present in the array, set the index to `0`.
500 | idx = 0;
501 | } else {
502 | // Add the passed `step` parameter to the index.
503 | idx += step;
504 | }
505 | }
506 | if (idx >= this._visiblePatterns.length) {
507 | // If the new index is greater/equal than the number of elements in the array,
508 | // set it to `0`.
509 | idx = 0;
510 | } else if (idx < 0) {
511 | // If the new index is smaller than 0,
512 | // set it to the index of the last element of the array.
513 | idx = this._visiblePatterns.length - 1;
514 | }
515 | // Set the ID of the currently shown element to the ID of the element at the new index.
516 | this._currentPatternId = this._visiblePatterns[idx].phid;
517 | // Send a message to the content script to show the element with the ID of `_currentPatternId`.
518 | await brw.tabs.sendMessage((await getCurrentTab()).id, { "showElement": this._currentPatternId });
519 | }
520 |
521 | /**
522 | * Function to show the next pattern element.
523 | * Used as a click event handler.
524 | * @param {Event} event
525 | */
526 | async showNextPattern(event) {
527 | await this.showPattern(1);
528 | }
529 |
530 | /**
531 | * Function to show the previous pattern element.
532 | * Used as a click event handler.
533 | * @param {Event} event
534 | */
535 | async showPreviousPattern(event) {
536 | await this.showPattern(-1);
537 | }
538 |
539 | /**
540 | * Function to generate the HTML text for the currently shown pattern.
541 | * @returns {html} HTML of the text for the currently shown pattern element
542 | */
543 | getCurrentPatternText() {
544 | // Only generate a text when a pattern element is shown.
545 | if (this._currentPatternId) {
546 | // Get the index of the current pattern element in the array.
547 | let idx = this.getIndexOfPatternId(this._currentPatternId);
548 | // Only generate a text when the element is still present in the array.
549 | if (idx !== -1) {
550 | // Get information about the pattern type from the configuration constant of the extension.
551 | let currentPatternInfo = constants.patternConfig.patterns.find(p => p.name === this._visiblePatterns[idx].patternName);
552 | // Generate the HTML text.
553 | return html`
554 |
`;
557 | }
558 | }
559 | return html``;
560 | }
561 |
562 | /**
563 | * Function to generate the HTML of the number of the currently shown pattern element
564 | * @returns {html} HTML of the number (`index + 1`) of the currently shown pattern element
565 | */
566 | getCurrentPatternNumber() {
567 | // Only generate a text when a pattern element is shown.
568 | if (this._currentPatternId) {
569 | // Get the index of the current pattern element in the array.
570 | let idx = this.getIndexOfPatternId(this._currentPatternId);
571 | // Only generate a text when the element is still present in the array.
572 | if (idx !== -1) {
573 | // Generate the HTML text with the number (`index + 1`).
574 | return `${idx + 1}`;
575 | }
576 | }
577 | return "-";
578 | }
579 |
580 | /**
581 | * From the Lit documentation (https://lit.dev/docs/components/lifecycle/):
582 | * "Called before `update()` to compute values needed during the update.".
583 | * Used here to react to changes in the `results` before the component is rendered.
584 | * @param {Map} changedProperties
585 | */
586 | willUpdate(changedProperties) {
587 | // Extract the visible elements from the `results`, if the `results` have changed.
588 | if (changedProperties.has("results")) {
589 | this.extractVisiblePatterns();
590 | }
591 | }
592 |
593 | /**
594 | * Render the HTML of the component.
595 | * @returns {html} HTML of the component
596 | */
597 | render() {
598 | // Return an empty string if the component is not activated or if no patterns were detected.
599 | if (this.activation !== activationState.On || this.results.countVisible === 0) {
600 | return html``;
601 | }
602 |
603 | return html`
604 |
611 | `;
612 | }
613 | }
614 | // Define a custom element for the component so that it can be used in the HTML DOM.
615 | customElements.define("show-pattern-button", ShowPatternButtons);
616 |
617 | /**
618 | * Lit component for the list of all supported patterns in the popup.
619 | * @extends LitElement
620 | */
621 | export class SupportedPatternsList extends LitElement {
622 | // CSS styles for the HTML elements in the component.
623 | static styles = [
624 | sharedStyles,
625 | patternsListStyles,
626 | patternLinkStyles,
627 | css`
628 | div {
629 | margin: 2.5em 0 1em;
630 | }
631 | `
632 | ];
633 |
634 | /**
635 | * Render the HTML of the component.
636 | * @returns {html} HTML of the component
637 | */
638 | render() {
639 | return html`
640 |
653 | `;
654 | }
655 | }
656 | // Define a custom element for the component so that it can be used in the HTML DOM.
657 | customElements.define("supported-patterns-list", SupportedPatternsList);
658 |
659 | /**
660 | * Lit component for footer of the popup.
661 | * @extends LitElement
662 | */
663 | export class PopupFooter extends LitElement {
664 | // CSS styles for the HTML elements in the component.
665 | static styles = [
666 | sharedStyles,
667 | css`
668 | div {
669 | margin-top: 2em;
670 | }
671 | `
672 | ];
673 |
674 | /**
675 | * Render the HTML of the component.
676 | * @returns {html} HTML of the component
677 | */
678 | render() {
679 | return html`
680 |
683 | `;
684 | }
685 | }
686 | // Define a custom element for the component so that it can be used in the HTML DOM.
687 | customElements.define("popup-footer", PopupFooter);
688 |
--------------------------------------------------------------------------------