├── .github ├── CODEOWNERS ├── labeler.yml └── workflows │ ├── github-projects.yml │ ├── label.yml │ ├── release-please.yml │ ├── semantic-pr.yml │ └── top-issues.yml ├── CHANGELOG.md ├── LICENSE ├── OpenFoodFactsPower.user.js └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence, 3 | # review when someone opens a pull request. 4 | # For more on how to customize the CODEOWNERS file - https://help.github.com/en/articles/about-code-owners 5 | 6 | * @openfoodfacts/openfoodfacts-server 7 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # Add labels to any any pull request with changes to the specified paths 2 | 3 | github: 4 | - .github/**/* 5 | -------------------------------------------------------------------------------- /.github/workflows/github-projects.yml: -------------------------------------------------------------------------------- 1 | name: Add issues to the PUS project 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | add-to-projects: 10 | name: Ventilate issues to the right GitHub projects 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/add-to-project@main 14 | with: 15 | project-url: https://github.com/orgs/openfoodfacts/projects/79 # Add issue to the PUS project 16 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 17 | - uses: actions/add-to-project@main 18 | with: 19 | project-url: https://github.com/orgs/openfoodfacts/projects/36 # Add issue to the open pet food facts project 20 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 21 | labeled: 🐾 Open Pet Food Facts 22 | label-operator: OR 23 | - uses: actions/add-to-project@main 24 | with: 25 | project-url: https://github.com/orgs/openfoodfacts/projects/11 # Add issue to the open products facts project 26 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 27 | labeled: 📸 Open Products Facts 28 | label-operator: OR 29 | - uses: actions/add-to-project@main 30 | with: 31 | project-url: https://github.com/orgs/openfoodfacts/projects/37 # Add issue to the open beauty facts project 32 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 33 | labeled: 🧴 Open Beauty Facts 34 | label-operator: OR 35 | - uses: actions/add-to-project@main 36 | with: 37 | project-url: https://github.com/orgs/openfoodfacts/projects/4 # Add issue to the packaging project 38 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 39 | labeled: 📦 Packaging 40 | label-operator: OR 41 | - uses: actions/add-to-project@main 42 | with: 43 | project-url: https://github.com/orgs/openfoodfacts/projects/25 # Add issue to the documentation project 44 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 45 | labeled: 📚 Documentation 46 | label-operator: OR 47 | - uses: actions/add-to-project@main 48 | with: 49 | project-url: https://github.com/orgs/openfoodfacts/projects/5 # Add issue to the folksonomy project 50 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 51 | labeled: 🏷️ Folksonomy Project 52 | label-operator: OR 53 | - uses: actions/add-to-project@main 54 | with: 55 | project-url: https://github.com/orgs/openfoodfacts/projects/44 # Add issue to the data quality project 56 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 57 | labeled: 🧽 Data quality 58 | label-operator: OR 59 | - uses: actions/add-to-project@main 60 | with: 61 | project-url: https://github.com/orgs/openfoodfacts/projects/82 # Add issue to the search project 62 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 63 | labeled: 🔎 Search 64 | label-operator: OR 65 | - uses: actions/add-to-project@main 66 | with: 67 | project-url: https://github.com/orgs/openfoodfacts/projects/41 # Add issue to the producer platform project 68 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 69 | labeled: 🏭 Producers Platform 70 | label-operator: OR 71 | - uses: actions/add-to-project@main 72 | with: 73 | project-url: https://github.com/orgs/openfoodfacts/projects/92 # Add issue to the Nutri-Score project 74 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 75 | labeled: 🚦 Nutri-Score 76 | label-operator: OR 77 | - uses: actions/add-to-project@main 78 | with: 79 | project-url: https://github.com/orgs/openfoodfacts/projects/132 # Add issue to the Top upvoted issues board 80 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 81 | labeled: ⭐ top issue, 👍 Top 10 Issue! 82 | label-operator: OR 83 | - uses: actions/add-to-project@main 84 | with: 85 | project-url: https://github.com/orgs/openfoodfacts/projects/57 # Add issue to the Most impactful issues board 86 | github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} 87 | labeled: 🎯 P0, 🎯 P1 88 | label-operator: OR 89 | -------------------------------------------------------------------------------- /.github/workflows/label.yml: -------------------------------------------------------------------------------- 1 | # This workflow will triage pull requests and apply a label based on the 2 | # paths that are modified in the pull request. 3 | # 4 | # To use this workflow, you will need to set up a .github/labeler.yml 5 | # file with configuration. For more information, see: 6 | # https://github.com/actions/labeler 7 | 8 | name: Labeler 9 | on: [pull_request] 10 | 11 | jobs: 12 | label: 13 | 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | pull-requests: write 18 | 19 | steps: 20 | - uses: actions/labeler@v4 21 | with: 22 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 23 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: Run release-please 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release-please: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: GoogleCloudPlatform/release-please-action@v3 11 | with: 12 | token: ${{ secrets.GITHUB_TOKEN }} 13 | release-type: simple 14 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pr.yml: -------------------------------------------------------------------------------- 1 | name: "Semantic PRs" 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | name: Validate PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v4 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/top-issues.yml: -------------------------------------------------------------------------------- 1 | name: Top issues action. 2 | #on: 3 | # schedule: 4 | # - cron: '0 0 */1 * *' 5 | on: 6 | issues: 7 | types: [opened, transferred] 8 | 9 | jobs: 10 | ShowAndLabelTopIssues: 11 | name: Display and label top issues. 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Run top issues action 15 | uses: rickstaa/top-issues-action@v1 16 | env: 17 | github_token: ${{ secrets.GITHUB_TOKEN }} 18 | with: 19 | label: true 20 | dashboard: true 21 | dashboard_title: 👍 Top Issues Dashboard 22 | dashboard_show_total_reactions: true 23 | top_issues: true 24 | top_bugs: true 25 | top_features: true 26 | top_pull_requests: true 27 | top_list_size: 20 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.0 (2024-09-26) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * 90 ([2783a69](https://github.com/openfoodfacts/power-user-script/commit/2783a696df69cd88c0c76593587e24cb0d0e3a2f)) 9 | * 92 ([b9b2c0a](https://github.com/openfoodfacts/power-user-script/commit/b9b2c0a9c70302e47ed5932f2905d57ed6eda50a)) 10 | * fix [#64](https://github.com/openfoodfacts/power-user-script/issues/64) ([1e76663](https://github.com/openfoodfacts/power-user-script/commit/1e76663f92f3ec55285ede4cd90a6010ef2f1f2e)) 11 | * fix script ([808bb42](https://github.com/openfoodfacts/power-user-script/commit/808bb42c916ccbf776aaf2dcf793e93469e7f81b)) 12 | * Hunger games button outside list barcodes ([33e0831](https://github.com/openfoodfacts/power-user-script/commit/33e0831b4fb9a894683d34b0d75ad3894f407c56)) 13 | * make links display again ([6538459](https://github.com/openfoodfacts/power-user-script/commit/6538459eab77292e65fb5a2f0a4254a9cfe1b186)) 14 | * quick-fix-sidebar ([1108171](https://github.com/openfoodfacts/power-user-script/commit/1108171f37208f270943c1890ff1fb62c80b3588)) 15 | * remove edit link in listByRow mode ([3081c2e](https://github.com/openfoodfacts/power-user-script/commit/3081c2ed5b8154da35ca0e5d076f3d6a3424c28a)) 16 | 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /OpenFoodFactsPower.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Open Food Facts power user script 3 | // @description Helps power users in their day to day work. Key "?" shows help. This extension is a kind of sandbox to experiment features that could be added to Open Food Facts website. 4 | // @namespace openfoodfacts.org 5 | // @version 2024-12-20T11:15 6 | // @include https://*.openfoodfacts.org/* 7 | // @include https://*.openproductsfacts.org/* 8 | // @include https://*.openbeautyfacts.org/* 9 | // @include https://*.openpetfoodfacts.org/* 10 | // @include https://*.pro.openfoodfacts.org/* 11 | // @include https://*.openfoodfacts.net/* 12 | // @include https://*.openfoodfacts.dev/* 13 | // @include http://*.productopener.localhost/* 14 | // @include http://*.openfoodfacts.localhost/* 15 | // @include http://*.openfoodfacts.localhost:8080/* 16 | // @include http://*.openpetfoodfacts.localhost:*/* 17 | // @include http://*.openproductsfacts.localhost:*/* 18 | // @include http://*.openbeautyfacts.localhost:*/* 19 | // @exclude https://analytics.openfoodfacts.org/* 20 | // @exclude https://api.folksonomy.openfoodfacts.org/* 21 | // @exclude https://*.wiki.openfoodfacts.org/* 22 | // @exclude https://wiki.openfoodfacts.org/* 23 | // @exclude https://support.openfoodfacts.org/* 24 | // @exclude https://translate.openfoodfacts.org/* 25 | // @exclude https://donate.openfoodfacts.org/* 26 | // @exclude https://hunger.openfoodfacts.org/* 27 | // @exclude https://monitoring.openfoodfacts.org/* 28 | // @exclude https://forum.openfoodfacts.org/* 29 | // @exclude https://*blog.openfoodfacts.org/* 30 | // @exclude https://*connect.openfoodfacts.org/* 31 | // @exclude https://*connect-test.openfoodfacts.org/* 32 | // @exclude https://contents.openfoodfacts.org/* 33 | // @exclude https://mirabelle.openfoodfacts.org/* 34 | // @exclude https://prices.openfoodfacts.org/* 35 | // @exclude https://search.openfoodfacts.org/* 36 | // 37 | // @icon http://world.openfoodfacts.org/favicon.ico 38 | // @updateURL https://github.com/openfoodfacts/power-user-script/raw/master/OpenFoodFactsPower.user.js 39 | // @grant GM_getResourceText 40 | // @require http://code.jquery.com/jquery-latest.min.js 41 | // @require http://code.jquery.com/ui/1.12.1/jquery-ui.min.js 42 | // @require https://cdn.jsdelivr.net/npm/jsbarcode@latest/dist/JsBarcode.all.min.js 43 | // @author charles@openfoodfacts.org 44 | // ==/UserScript== 45 | /* eslint-env jquery */ 46 | 47 | // Product Opener (Open Food Facts web app) uses: 48 | // * jQuery 2.1.4: view-source:https://static.openfoodfacts.org/js/dist/jquery.js 49 | // http://code.jquery.com/jquery-2.1.4.min.js 50 | // * jQuery-UI 1.12.1: view-source:https://static.openfoodfacts.org/js/dist/jquery-ui.js 51 | // http://code.jquery.com/ui/1.12.1/jquery-ui.min.js 52 | // * Tagify 3.x: view-source:https://static.openfoodfacts.org/js/dist/tagify.min.js 53 | // https://github.com/yairEO/tagify 54 | // * Foundation 5 CSS Framework: https://sudheerdev.github.io/Foundation5CheatSheet/ 55 | // https://get.foundation/sites/docs-v5/ 56 | // See also: https://github.com/openfoodfacts/openfoodfacts-server/pull/2987 57 | 58 | (function() { 59 | 'use strict'; 60 | 61 | const log_to_console = true; // true if you want to log activity 62 | var version_user; 63 | var version_date; 64 | var proPlatform = false; // TODO: to be included in isPageType() 65 | const pageType = isPageType(); // test page type 66 | const corsProxyURL = ""; 67 | log("2024-12-20T11:15 - mode: " + pageType); 68 | 69 | // Disable extension if the page is an API result; https://world.openfoodfacts.org/api/v0/product/3222471092705.json 70 | if (pageType === "api") { 71 | // TODO: allow keyboard shortcut to get back to product view? 72 | var _code = window.location.href.match(/\/product\/(.*)\.json$/)[1]; 73 | var viewURL = document.location.protocol + "//" + document.location.host + "/product/" + _code; 74 | log('press v to get back to product view: ' + viewURL); 75 | $(document).on('keydown', function(event) { 76 | if (event.key === 'v') { 77 | window.open(viewURL, "_blank"); // open a new window 78 | return; 79 | } 80 | }); 81 | return; 82 | } 83 | 84 | // Setup options 85 | var zoomOption = false; // "true" allows zooming images with mouse wheel, while "false" disallow it 86 | var listByRowsOption = false; // "true" automatically lists products by rows, while "false" not 87 | 88 | //Hidden form for ingredients analysis used both in list mode and single products. 89 | //Ingredients analysis takes its input from 'ingredients_text' for single products or from textarea with the id=i[product_id] when in a list 90 | //but the language pages have the text in 'ingredients_text_xx' 91 | //so we have to copy the text (in Copytext) before submitting the form 92 | var analyse_form = document.createElement("form"); 93 | analyse_form.setAttribute("method", "get"); 94 | analyse_form.setAttribute("enctype", "multipart/form-data"); 95 | var txt = document.createElement('textarea'); 96 | txt.setAttribute('id', 'ingredients_text'); 97 | txt.setAttribute('name', 'ingredients_text'); 98 | txt.setAttribute('style', 'display:none;'); 99 | var sub = document.createElement('input'); 100 | sub.setAttribute('type', 'hidden'); 101 | sub.setAttribute('name', 'action'); 102 | sub.setAttribute('value', 'process'); 103 | analyse_form.appendChild(txt); 104 | analyse_form.appendChild(sub); 105 | document.body.appendChild(analyse_form); 106 | 107 | // Open Food Facts power user 108 | // * Main code by Charles Nepote (@CharlesNepote) 109 | // * Barcode code by @harragastudios 110 | 111 | // Firefox: add it via Greasemonkey or Tampermonkey extension: https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/ 112 | // Chrome (not tested): add it with Tampermonkey: https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo 113 | 114 | // Main features 115 | // * DESIGN (custom CSS with small improvements) 116 | // * barcode highlighted with a sweet color 117 | // * better distinguished sections 118 | // * fields highlighted, current field highlighted 119 | // * less margins for some elements 120 | // * Smaller fixed validation bar 121 | // * UI 122 | // * help screen called with button [?] or keyboard shortcut (?) or (h) 123 | // * zoom every images with mouse wheel; see http://www.jacklmoore.com/zoom/ 124 | // * show/hide barcode; keyboard shortcut (shift+B) 125 | // * see https://github.com/openfoodfacts/openfoodfacts-server/issues/1728 126 | // * Edit mode: 127 | // * show hide help comments for each field (see help screen) 128 | // * Firefox: Nutrition facts picture takes all the place available 129 | // * Add "History" anchor in the nav bar 130 | // * Ingredient lists: external link for each ingredient (appear when hovering rows) 131 | // * keyboard shortcut to API product page (a) 132 | // * keyboard shortcut to get back to view mode (v) 133 | // * keyboard shortcut to enter edit mode: (e) in the current window, (E) in a new window 134 | // * see Add "Edit" keyboard shortcut for logged users: https://github.com/openfoodfacts/openfoodfacts-server/issues/1852 135 | // * keyboard shortcuts to help modify data without a mouse: P(roduct), Q(uality), B(rands), C(ategories), L(abels), I(ngredients), (e)N(ergy), F(ibers) 136 | // * Quick links in the sidebar: page translation, category translation, Recent Changes, Hunger Game, categorization opportunities... 137 | // * dedicated to list screens (facets, search results...): 138 | // * "n" keyboard shortcut to reload the list without cache (&nocache=1 parameter), if it's not already the case 139 | // * [alpha] keyboard shortcut to list products as a table containing ingredients and options to edit or delete ingredients 140 | // (shift+L) ["L" for "list"] 141 | // The LanguageTool Firefox extension is recommanded because it detects automatically the language of each field. 142 | // https://addons.mozilla.org/en-US/firefox/addon/languagetool/ 143 | // * Inline edit of ingredients in list mode 144 | // * Option to set ingredient textareas to fixed width font, to make it easier to see bad OCR, 145 | // such as when it confuses "m" and "rn" (e.g. corn), lowercase l/L and uppercase i/I, etc. 146 | // 147 | // * FEATURES 148 | // * [beta] transfer data from a language to another (use *very* carefully); keyboard shortcut (shift+T) 149 | // * [beta] easily delete ingredients, by entering the list by rows mode (shift+L) 150 | // * [alpha] allow flagging products for later review (shift+S) 151 | // * https://github.com/openfoodfacts/openfoodfacts-server/issues/1408 152 | // * Ask charles@openfoodfacts.org 153 | // * launch Google OCR if "Edit ingredients" is clicked in view mode 154 | // * "[Products without brand that might be from this brand]" link, following product code 155 | // * Links beside barcode number: Google and DuckDuckGo link for product barcode + Open Beauty Facts + Open Pet Food Facts + pro.openfoodfacts.dev 156 | // * Product view: button to open an ingredient analysis popup 157 | // * help screen: add "Similarly named products without a category" link 158 | // * help screen: add "Product code search on Google" link 159 | // * help screen: add links to Google/Yandex Reverse Image search (thanks Tacite for suggestion) 160 | // * Edit mode: 161 | // * Check serving size field 162 | // * Add the ⇅ icon allowing to reverse kJ and kcals 163 | // * Colorize icon ⇅ when kJ/kcal values are not coherent (ratio is displayed inside ⇅ tooltip) 164 | // * Add fiew informations on the confirmation page: 165 | // * Products issues: 166 | // * To be completed (from "states_tags") 167 | // * Quality errors tags (green message if none) 168 | // * Quality warings tags (green message if none) 169 | // * and a link to product edit 170 | // * Going further 171 | // * "XX products without brand that might be from this brand" link 172 | // * Add a field to filter Recent Changes results (filter as you type) 173 | 174 | // * DEPLOYMENT 175 | // * Tampermonkey suggests to update the extension when one click to updateURL: 176 | // https://gist.github.com/CharlesNepote/f6c675dce53830757854141c7ba769fc/raw/OpenFoodFactsPowerUser.user.js 177 | 178 | 179 | // TODO 180 | // * FEATURES 181 | // * identify problematic fields based on quality feedbacks; https://world.openfoodfacts.org/api/v0/product/7502271153193.json 182 | // * see "data_quality_errors_tags" array 183 | // * On the fly quality checks in the product edit form (javascript): https://github.com/openfoodfacts/openfoodfacts-server/issues/1905 184 | // * Add automatic detection of nutriments, see: https://robotoff.openfoodfacts.org/api/v1/predict/nutrient?ocr_url=https://static.openfoodfacts.org/images/products/841/037/511/0228/nutrition_pt.12.json 185 | // * Easily delete ingredients when too buggy 186 | // * Add few informations on the confirmation page: 187 | // * Nutri-Score and NOVA if just calculated? 188 | // * unknown ingredients 189 | // * Product of a brand from a particular country, that are not present in this country (see @teolemon) 190 | // * Keyboard shortcut to get back to view mode (v) => target=_self + prevent leaving page if changes are not saved 191 | // * Mass edit (?) -- see https://github.com/roiKosmic/OFFMassUpdate/blob/master/js/content_script.js 192 | // * Mass edit with regexp (with preview) 193 | // * Mass deletion of a tag? 194 | // * Mini Hunger Game (dedicated to categories?) 195 | // * Revert from an old version 196 | // * UI & DESIGN 197 | // * Picture dates 198 | // => in the list: change background color depending on the year? 199 | // => in the product page: highlight in red when date is old? 200 | // * Highlight products with old pictures (?) 201 | // * Add a fixed menu button as in mass-updater 202 | // * Highlight empty fields? 203 | // * Select high resolution images on demand 204 | // * Show special prompt when the nutrition photo has changed, but not the nutrition data itself: https://github.com/openfoodfacts/openfoodfacts-server/issues/1910 205 | // * Show a special prompt when the ingredient list photo has changed, but not the ingredient list itself: https://github.com/openfoodfacts/openfoodfacts-server/issues/1909 206 | // * BUGS 207 | // * deal with products without official barcodes: https://fr.openfoodfacts.org/produit/2000050217197/mondose-exquisite-belgian-chocolates 208 | // * wheelzoom transform image links to: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaH.................. 209 | // * Some access keys dont seem to work, due to javascript library 210 | // * See Support hitting the TAB key only once to quickly move to the next text field and then make entering text possible: 211 | // https://github.com/openfoodfacts/openfoodfacts-server/issues/1245 212 | // * focus on .tagsinput fields is not highlighted 213 | 214 | 215 | // css 216 | // See https://stackoverflow.com/questions/4376431/javascript-heredoc 217 | var css = ` 218 | /* 219 | * OFF web app already load jquery-ui.css but it doesn't work properly with "dialog" function. 220 | * We add the CSS this way so that the embedded, relatively linked images load correctly. 221 | * (Use //ajax... so that https or http is selected as appropriate to avoid "mixed content".) 222 | */ 223 | 224 | .ui-dialog { 225 | position: absolute; 226 | top: 0; 227 | left: 0; 228 | padding: .2em; 229 | outline: 0; 230 | } 231 | .ui-dialog .ui-dialog-titlebar { 232 | padding: .4em 1em; 233 | position: relative; 234 | } 235 | .ui-dialog .ui-dialog-title { 236 | float: left; 237 | margin: .1em 0; 238 | white-space: nowrap; 239 | width: 90%; 240 | overflow: hidden; 241 | text-overflow: ellipsis; 242 | } 243 | .ui-dialog .ui-dialog-titlebar-close { 244 | position: absolute; 245 | right: .3em; 246 | top: 50%; 247 | width: 20px; 248 | margin: -10px 0 0 0; 249 | padding: 1px; 250 | height: 20px; 251 | } 252 | .ui-dialog .ui-dialog-content { 253 | position: relative; 254 | border: 0; 255 | padding: .5em 1em; 256 | background: none; 257 | overflow: auto; 258 | } 259 | .ui-dialog .ui-dialog-buttonpane { 260 | text-align: left; 261 | border-width: 1px 0 0 0; 262 | background-image: none; 263 | margin-top: .5em; 264 | padding: .3em 1em .5em .4em; 265 | } 266 | .ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { 267 | float: right; 268 | } 269 | .ui-dialog .ui-dialog-buttonpane button { 270 | margin: .5em .4em .5em 0; 271 | cursor: pointer; 272 | } 273 | 274 | .ui-dialog .ui-resizable-n { 275 | height: 2px; 276 | top: 0; 277 | } 278 | .ui-dialog .ui-resizable-e { 279 | width: 2px; 280 | right: 0; 281 | } 282 | .ui-dialog .ui-resizable-s { 283 | height: 2px; 284 | bottom: 0; 285 | } 286 | .ui-dialog .ui-resizable-w { 287 | width: 2px; 288 | left: 0; 289 | } 290 | .ui-dialog .ui-resizable-se, 291 | .ui-dialog .ui-resizable-sw, 292 | .ui-dialog .ui-resizable-ne, 293 | .ui-dialog .ui-resizable-nw { 294 | width: 7px; 295 | height: 7px; 296 | } 297 | .ui-dialog .ui-resizable-se { 298 | right: 0; 299 | bottom: 0; 300 | } 301 | .ui-dialog .ui-resizable-sw { 302 | left: 0; 303 | bottom: 0; 304 | } 305 | .ui-dialog .ui-resizable-ne { 306 | right: 0; 307 | top: 0; 308 | } 309 | .ui-dialog .ui-resizable-nw { 310 | left: 0; 311 | top: 0; 312 | } 313 | .ui-draggable .ui-dialog-titlebar { 314 | cursor: move; 315 | } 316 | /** End of jquery-ui requirements **/ 317 | 318 | 319 | 320 | /* .row { width: 80% !important; margin: 0 0 !important; } */ 321 | 322 | /* Special color for barcode */ 323 | span[property="food:code"] { color: Olive; } 324 | 325 | /* Enhancements to better distinguish sections: Product information, Ingredients and Nutriments facts */ 326 | #main_column > div > h2 { margin-top: 1.6rem !important; 327 | margin-bottom: 0.2rem !important; 328 | border-bottom: 1px solid lightgrey; } 329 | 330 | /* Special background color for all input fieds */ 331 | textarea, .tagify, input[type=text] { background-color: LightYellow !important; } 332 | input.nutriment_value { background-color: LightYellow; } 333 | textarea:focus, .tagify__input:focus, .tagify:focus, input[type=text]:focus, input.nutriment_value:focus { background-color: lightblue !important; } 334 | 335 | /* Small enhancements */ 336 | p { margin-bottom: 0.6rem; } 337 | input[type=text] { margin: 1px 0; } /* reduce vertical space between fields and notes */ 338 | .note, .example { margin: 1px 0; } 339 | label { margin-top: 10px; } 340 | .data_table { margin-top: 7px; } 341 | td { line-height: 1rem; } 342 | input[type="checkbox"], input[type="radio"] { margin: 0; } 343 | .data_table td, .data_table th { padding: .1rem .1rem .1rem .4rem; } 344 | /*.data_table label { display: table-cell; }*/ 345 | 346 | #image_box_front { margin-bottom: 1rem !important; } 347 | 348 | .unselectbuttondiv_front_fr { 349 | text-align: center !important; 350 | } 351 | 352 | .unselectbutton_front_fr { 353 | margin:0 0 0 0 !important; 354 | } 355 | 356 | /* Buttons Rotate left - Rotate right: 0.25rem vs 1.25 */ 357 | .cropbox > div > a { margin: 0 0 0.25rem; } 358 | 359 | /* checkbox: Normalize colors and Photo on white background: try to remove the background */ 360 | .cropbox > label { margin-top: 3px; } 361 | .cropbox > input { margin: 0 0 0.5rem 0; } 362 | 363 | /* Reset margins of nutriments form */ 364 | input.nutriment_value { margin: 0 0 0 0; } 365 | 366 | input.show_comparison { 367 | margin: 0 0 0.2rem 0 !important; 368 | } 369 | 370 | 371 | /* --------------- Let panels use less space ----------------------- */ 372 | /* On the legacy website, in the "changes saved" page, the second panel is not seen without scrolling. */ 373 | .card-section { padding-top: 12px; padding-bottom: 10px; } 374 | .panel_card { margin-bottom: 0.5rem !important } 375 | .panel_title_card { margin-top: 0px; } 376 | .panel_content_card { margin-top: 0px; } 377 | .panel_title, .panel_content { padding-top: 0px !important; padding-bottom: 0.2rem !important; } 378 | 379 | 380 | /* ---------------- Power User Script UI --------------------------- */ 381 | /* ------------------ Help box ------------------------------------- */ 382 | .pus_menu { 383 | font-size: 0.9rem; 384 | } 385 | 386 | /* checkboxes in popup */ 387 | .pus_menu label { 388 | margin-top: 0; 389 | } 390 | .pus_menu input[type=checkbox] { 391 | margin-bottom: 0; 392 | } 393 | 394 | .ui-widget-content a { 395 | color: #00f; 396 | } 397 | 398 | /* ------------------ Fixed menu buttons --------------------------- */ 399 | #pwe_help { 400 | position:fixed; 401 | left:0%; 402 | top:3rem; 403 | padding:0 0.7rem 0 0.7rem; 404 | font-size:1.5rem; 405 | background-color:red; 406 | border-radius: 0 10px 10px 0; 407 | z-index: 200; 408 | } 409 | 410 | #ing_analysis { 411 | position:fixed; 412 | left:0%; 413 | top:5rem; 414 | padding:0 0.7rem 0 0.7rem; 415 | font-size:1.1rem; 416 | width: 7rem; 417 | background-color:red; 418 | border-radius: 0 10px 10px 0; 419 | z-index: 200; 420 | } 421 | #pwe_hide_text_fields { 422 | position:fixed; 423 | left:0%; 424 | top:8rem; 425 | padding:0 0.7rem 0 0.7rem; 426 | font-size:1.1rem; 427 | background-color:red; 428 | border-radius: 0 10px 10px 0; 429 | z-index: 200; 430 | } 431 | 432 | /* --------------- Hunger games logo search button --------------- */ 433 | .list_hunger_games_logo_search { 434 | position: absolute; 435 | top: 0; 436 | right: 2.5em; 437 | padding: 0 0.5em; 438 | border-radius: 0.3em; 439 | } 440 | 441 | .list_hunger_games_logo_search:hover, .list_rotate_image_90:hover, .list_rotate_image_180:hover, .list_rotate_image_270:hover { 442 | background-color: #aaf; 443 | } 444 | 445 | /* --------------- Rotate list product buttons --------------- */ 446 | .list_rotate_image_90 { 447 | position: absolute; 448 | top: 0; 449 | right: 5em; 450 | padding: 0 0.5em; 451 | border-radius: 0.3em; 452 | } 453 | 454 | .list_rotate_image_180 { 455 | position: absolute; 456 | top: 0; 457 | right: 7.5em; 458 | padding: 0 0.5em; 459 | border-radius: 0.3em; 460 | } 461 | 462 | .list_rotate_image_270 { 463 | position: absolute; 464 | top: 0; 465 | right: 10em; 466 | padding: 0 0.5em; 467 | border-radius: 0.3em; 468 | } 469 | 470 | /* ---------------- /Power User Script UI -------------------------- */ 471 | 472 | 473 | /* ---------------- Height of input fields ------------------------- */ 474 | .tagify__input { margin: 4 px; } /* instead of 5px */ 475 | 476 | 477 | /* ---------------- Nutrition facts ------------------------- */ 478 | 479 | label[for="serving_size"] { 480 | float: left; 481 | margin-right: 10px; 482 | } 483 | 484 | #serving_size { 485 | width: 30%; 486 | } 487 | 488 | input.nutriment_value { height: 1.9rem !important; } 489 | select.nutriment_unit { 490 | height: 1.9rem !important; 491 | padding: .1rem .3rem !important; 492 | } 493 | 494 | #nutriment_fruits-vegetables-nuts-estimate_tr :first-child { max-inline-size: 25em; } 495 | 496 | 497 | /* ---- Edit mode: Nutrition image as tall as Nutrition facts table ---- */ 498 | /* Works with Firefox, Chrome at least */ 499 | #nutrition_image_copy { 500 | width: -moz-available; 501 | height: 92%; 502 | } 503 | 504 | #nutrition_image_copy > img { 505 | /* Vertical image: https://world.openfoodfacts.org/cgi/product.pl?type=edit&code=8002063211913 */ 506 | /* Horizontal image: https://world.openfoodfacts.org/cgi/product.pl?type=edit&code=0490711801117 */ 507 | height: 100%;/**/ 508 | width: 100%;/**/ 509 | /* https://hacks.mozilla.org/2015/02/exploring-object-fit/ */ 510 | object-fit:contain; 511 | object-position: left; 512 | } 513 | /* ---- /Edit mode: Nutrition image as tall as Nutrition facts table ---- */ 514 | 515 | 516 | /* ------------------- Smaller fixed validation bar ---------------------- */ 517 | .bottom-validation { padding-top: .3rem; } 518 | .bottom-validation > div > div { height: 2rem; } 519 | 520 | 521 | /* ----------------- Varia ------------------------- */ 522 | .productLink::before { 523 | content: " — "; 524 | } 525 | 526 | .hidden { 527 | display: none; 528 | } 529 | 530 | .ingredient_td:hover .hidden { 531 | display: inline; 532 | } 533 | 534 | /* ingredients box alternative font */ 535 | textarea.monospace { 536 | font-family: Consolas, Lucida Console, monospace; 537 | } 538 | 539 | .ul[id^='products_'].search_results a.with_barcode { margin-top: 0; padding-top: 0; } 540 | 541 | `; 542 | 543 | // apply custom CSS 544 | var s = document.createElement('style'); 545 | s.type = 'text/css'; 546 | s.innerHTML = css; 547 | document.documentElement.appendChild(s); 548 | 549 | 550 | 551 | // *** 552 | // * Image zoom 553 | // * 554 | // Test image zoom with mouse wheel 555 | // Don't forget to add: // @require https://cdn.jsdelivr.net/npm/wheelzoom 556 | if(zoomOption) { wheelzoom(document.querySelectorAll('img')); } // doesn't work in edit mode 557 | 558 | // Test image zoom with jquery-zoom 559 | // Don't forget to add: // @require https://cdn.jsdelivr.net/npm/jquery-zoom 560 | // $('img').zoom({ on:'grab' }); // add zoom // doesn't work 561 | // $('img').trigger('zoom.destroy'); // remove zoom 562 | 563 | 564 | 565 | // *** 566 | // * Every modes, except "list" 567 | // * 568 | // Build variables 569 | if(pageType !== "list") { 570 | log("This is not a list."); 571 | var code; 572 | code = getURLParam("code")||$('span[property="food:code"]').html(); 573 | 574 | if (code === undefined) { 575 | // product view needs more effort to get the product code. 576 | // Using e.g. 577 | // as it doesn't contain the code if the given code is not a valid entry. 578 | var code2 = $('link[rel="canonical"]').attr("href").match('product/\([0-9]+\)'); 579 | if (code2 && code2[1]) { 580 | code = code2[1]; 581 | //log("code2: "+ code2); 582 | } 583 | } 584 | 585 | // Horrible hack to prevent issue introduced by https://github.com/openfoodfacts/openfoodfacts-server/pull/8223 586 | if (pageType === "saved-product page") { 587 | //log(document.getElementById('changes_saved').getElementsByClassName("warning")[0].href.match(/code=(.*)/)[1]); 588 | code = document.getElementById('changes_saved').getElementsByClassName("warning")[0].href.match(/code=(.*)/)[1]; 589 | } 590 | 591 | log("code: "+ code); 592 | // build API product link; example: https://world.openfoodfacts.org/api/v0/product/737628064502.json 593 | var apiProductURL = "/api/v0/product/" + code + ".json"; 594 | log("API: " + apiProductURL); 595 | // build edit url 596 | var editURL = document.location.protocol + "//" + document.location.host + "/cgi/product.pl?type=edit&code=" + code; 597 | } 598 | 599 | 600 | 601 | // *** 602 | // * Every mode, except "api" 603 | // * 604 | // Add quick links in the sidebar: page translation, category translation, Recent Changes... 605 | if (pageType !== "api") { 606 | var pageLanguage = $("html").attr('lang'); // Get page language 607 | log("Page language: " + pageLanguage); 608 | if(pageLanguage === "en") { // Delete page language if "en" because we can't make the difference bewteen "en-GB" and "en-US" 609 | pageLanguage = ""; 610 | } 611 | 612 | // Non contextual links 613 | // TODO: no more displayed since OFF redesign in 2022-10; put it elsewhere 614 | $("#match").before( 615 | ` 616 |
` 633 | ); 634 | 635 | // Hunger Game contextual link 636 | // TODO: display a number of opportunities. 637 | /*var hungerGameDeepLink = 638 | ($("div[itemtype='https://schema.org/Brand']").length) ? "questions?type=brand&value_tag=" + normalizeTagName($("h1[itemprop='name']").text()) 639 | : (/label\/(.*)$/.test(document.URL) === true) ? "questions?type=label&value_tag=en:" + normalizeTagName(RegExp.$1) 640 | : (($("div[itemtype='https://schema.org/Thing']").length) ? "questions?type=category&value_tag=en:" + normalizeTagName($("h1[itemprop='name']").text()) 641 | : ""); 642 | $("h1[itemprop='name']").append( 643 | (hungerGameDeepLink ? 644 | ' ' + 645 | 'Hunger Game' : "") 646 | );*/ 647 | } 648 | 649 | 650 | // Add external link to ingredient so it opens in a new window 651 | if (pageType === "ingredients"){ 652 | $('#tagstable').find('tr').each(function(){ 653 | var tds = $(this).find('td'); 654 | var urlToIngredient; 655 | $(this).children().addClass("ingredient_td"); 656 | if(tds.length != 0) { 657 | urlToIngredient = tds.children().attr("href"); // /category/gouda/ingredient/dairy 658 | } 659 | $(this).find('td').children().after(' '); 660 | }); 661 | } 662 | 663 | 664 | // *** 665 | // * Every mode, except "api", "list", "search-form" 666 | // * 667 | if (pageType === "edit" || 668 | pageType === "product view"|| 669 | pageType === "saved-product page") { 670 | 671 | // Add product public link if we are on the pro platform 672 | if(proPlatform) { 673 | var publicURL = document.URL.replace(/\.pro\./gi, "."); 674 | log("publicURL: "+publicURL); 675 | $(".sidebar p:first").after('

> Product public URL

'); 676 | } 677 | 678 | // Add informations right after the barcode 679 | if ($("#barcode_paragraph") && code !== undefined) { 680 | 681 | // Icon for toggling graphical barcode 682 | $("#barcode_paragraph").append(' 📲'); 683 | $("#toggleBarcodeLink").on("click", function(){ 684 | toggleSingleBarcode(code); 685 | }); 686 | 687 | // Find products from the same brand 688 | var sameBrandProducts = code.replace(/[0-9][0-9][0-9][0-9]$/gi, "xxxx"); 689 | var sameBrandProductsURL = document.location.protocol + 690 | "//" + document.location.host + 691 | '/state/brands-to-be-completed/code/' + 692 | sameBrandProducts; 693 | $("#barcode_paragraph") 694 | .append(' ['+ 697 | 'Non-branded ϵ same brand?]'); 698 | // Google Link 699 | var googleLink = 'https://www.google.com/search?q=' + code; 700 | $("#barcode_paragraph") 701 | .append(' [G]'); 703 | // DuckDuckGo Link 704 | var duckLink = 'https://duckduckgo.com/?q=' + code; 705 | $("#barcode_paragraph") 706 | .append(' [DDG]'); 708 | // Link to Open Beauty Facts 709 | var obfLink = 'https://world.openbeautyfacts.org/product/' + code; 710 | productExists(corsProxyURL+obfLink,"#obfLinkStatus","",""); 711 | $("#barcode_paragraph") 712 | .append(' [obf.org] ()'); 714 | // Link to Open Pet Food Facts 715 | var opffLink = 'https://world.openpetfoodfacts.org/product/' + code; 716 | productExists(corsProxyURL+opffLink,"#opffLinkStatus","",""); 717 | $("#barcode_paragraph") 718 | .append(' [opff.org] ()'); 720 | // Link to .pro.openfoodfacts.dev 721 | //var proDevLink = 'https://off:off@world.pro.openfoodfacts.dev/product/' + code; 722 | var proDevLink = 'https://world.pro.openfoodfacts.dev/product/' + code; 723 | productExists(corsProxyURL+proDevLink,"#proDevLinkStatus","off","off"); 724 | $("#barcode_paragraph") 725 | .append(' [.pro.off.dev] ()'); 727 | 728 | // https://fr.openfoodfacts.org/etat/marques-a-completer/code/506036745xxxx&json=1 729 | var sameBrandProductsJSON = sameBrandProductsURL + "&json=1"; 730 | log("Get JSON from: " + sameBrandProductsJSON); 731 | $.getJSON(sameBrandProductsJSON, function(data) { 732 | var nbOfSameBrandProducts = data.count; 733 | log("nbOfSameBrandProducts: " + nbOfSameBrandProducts); 734 | if($("#going-further")) $("#going-further").append('
  • ' + nbOfSameBrandProducts + 737 | ' products without brand that might be from this brand' + 738 | '
  • '); 739 | if($("#barcode_paragraph")) $("#sameBrandProductLink").html( 740 | '['+ 743 | nbOfSameBrandProducts + ' non-branded ϵ same brand]'); 744 | }); 745 | 746 | } 747 | 748 | // Compute Google and Yandex reverse image search 749 | var gReverseImageURL = "https://images.google.com/searchbyimage?image_url="; 750 | var yReverseImageURL = "https://yandex.com/images/search?source=collections&url="; 751 | var frontImgURL = $('meta[name="twitter:image"]').attr("content"); 752 | var ingredientsImgURL = ($('#image_box_ingredients a img').attr('srcset') ? $('#image_box_ingredients a img').attr('srcset').match(/(.*) (.*)/)[1] : ""); 753 | var nutritionImgURL = ($('#image_box_nutrition a img').attr('srcset') ? $('#image_box_nutrition a img').attr('srcset').match(/(.*) (.*)/)[1] : ""); 754 | 755 | // Help box based on page type: api|saved-product page|edit|list|search form|product view 756 | var help = "
      " + 757 | "
    • (?) or (h): this present help
    • " + 758 | "" + 759 | ((pageType === "edit") ? 760 | '
    • ' + 761 | '
    • ': 762 | "") + 763 | ((pageType === "edit" || pageType === "list") ? 764 | '
    • ': 765 | "") + 766 | ((pageType === "product view" || pageType === "edit") ? 767 | "
    • (Shift+b): show/hide barcode
    • " + 768 | "
    • (Alt+shift+key): direct access to (P)roduct name, (Q)uality, (B)rands, (C)ategories, (L)abels, (I)ngredients, e(N)ergy, (F)ibers
    • " + 769 | "
      ": 770 | "") + 771 | ((pageType === "product view" || pageType === "api") ? 772 | "
    • (e): edit current product in current window
    • " + 773 | "
    • (E): edit product in a new window
    • ": 774 | "") + 775 | ((pageType === "product view" || pageType === "edit") ? 776 | "
    • (a): API product page (json)
    • ": 777 | "") + 778 | "
    • Product code search on Google
    • " + 779 | "
    • Google Reverse Image search"+ 780 | (pageType !== "product view" ? " (view mode only)
    • " : 781 | ": " + 782 | (frontImgURL ? "front" : "")+ 783 | (ingredientsImgURL ? ", ingredients" : "") + 784 | (nutritionImgURL ? ", nutrition" : "")) + 785 | "" + 786 | "
    • Yandex Reverse Image search"+ 787 | (pageType !== "product view" ? " (view mode only)
    • " : 788 | ": " + 789 | (frontImgURL ? "front" : "")+ 790 | (ingredientsImgURL ? ", ingredients" : "") + 791 | (nutritionImgURL ? ", nutrition" : "")) + 792 | "" + 793 | "
    • (shift+T): transfer a product from a language to another, in edition mode only (use very carefully)
    • " + 794 | "
    • (shift+S): flag product for later review (ask charles@openfoodfacts.org for log access)
    • " + 795 | "
      " + 796 | (pageType === "product view" ? 797 | "
    • " + sameBrandProducts + " products without a brand
    • " + 798 | "
    • Similarly named products without a category
    • ": 799 | "
    • " + sameBrandProducts + " products without a brand
    • " + 800 | "
    • Similarly named products without a category
    • ") + 801 | "
    "; 802 | 803 | // Help icon fixed 804 | $('body').append(''); 805 | //$('#select_country_li').insertAfter('
  • ?
  • '); // issue: menu desappear when scrolling 806 | 807 | // User help dialog 808 | $("#pwe_help").click(function(){ 809 | togglePowerUserInfo(help); 810 | toggleHelpers(); 811 | toggleIngredientsMonospace(); 812 | toggleDFMode(); 813 | }); 814 | 815 | if (pageType === "edit"){ 816 | 817 | //Ingredients analysis check - opens in new window 818 | $('body').append(''); 819 | $("#ing_analysis").click(function(){ 820 | //log("analyse"); 821 | Copydata(); 822 | submitToPopup(analyse_form); 823 | }); 824 | 825 | $('body').append(''); 826 | $("#pwe_hide_text_fields").click(function(){ 827 | toggleHideTextFieldsPopUp(); 828 | }); 829 | 830 | loadHideTextFieldsFromStorage(); 831 | } 832 | 833 | 834 | if (pageType === "edit" || pageType === "product view") { 835 | var history = document.getElementById("history"); 836 | if (history !== null) { 837 | // add search field after "Changes history" 838 | const historyInput = document.createElement("input"); 839 | history.after(historyInput); 840 | var initalList = true; 841 | // search term when the user fill a value 842 | historyInput.addEventListener('input', function (input) { 843 | const value = input.target.value; 844 | let list = document.querySelector('#history_list').querySelectorAll('li'); 845 | // if search term is less than 2 characters, reset style and return 846 | if (value.length < 2) { 847 | if (initalList === false) list.forEach((x) => { x.style.color = '' }); 848 | initalList = true; 849 | return; 850 | } 851 | initalList = false; 852 | let re = new RegExp(value, 'i'); 853 | // highlight line in blue or grey weither it contains the searched term or not 854 | list.forEach((x) => { x.style.color = (re.test(x.textContent)) ? 'blue' : 'grey' }); 855 | }); 856 | } 857 | } 858 | 859 | // Keyboard actions 860 | $(document).on('keydown', function(event) { 861 | log(event); 862 | // If the key is not pressed inside a input field (ex. search product field) 863 | if ( 864 | !$(event.target).is(':input') 865 | && !$(event.target).is('span.tagify__input') 866 | && !$(event.target).is('span.tagify__tag-text') 867 | ) { 868 | // (Shift + B): toggle show/hide barcode 869 | if (event.key === 'B') { 870 | toggleSingleBarcode(code); 871 | return; 872 | } 873 | // (a): api page in a new window 874 | if ((pageType === "product view" || pageType == "edit") && event.key === 'a') { 875 | window.open(apiProductURL, "_blank"); // open in an other window 876 | return; 877 | } 878 | // (e): edit current product in current window 879 | if ((pageType === "product view" || pageType === "saved-product page") && event.key === 'e') { 880 | window.open(editURL, "_self"); // edit in current window 881 | return; 882 | } 883 | // (E): edit current product in a new window 884 | if (pageType === "product view" && event.key === 'E') { 885 | window.open(editURL); // open a new window 886 | return; 887 | } 888 | // (v): if in "edit" mode, switch to view mode 889 | if (pageType !== "product view" && event.key === 'v') { 890 | var viewURL = document.location.protocol + "//" + document.location.host + "/product/" + code; 891 | window.open(viewURL, "_blank"); // open a new window 892 | return; 893 | } 894 | // (I): ingredients 895 | if (pageType === "edit" && event.key === 'i') { 896 | toggleIngredientsMode(); 897 | return; 898 | } 899 | // (?): open help box 900 | if (event.key === '?' || event.key === 'h') { 901 | togglePowerUserInfo(help); 902 | toggleHelpers(); 903 | toggleIngredientsMonospace(); 904 | toggleDFMode(); 905 | return; 906 | } 907 | // (S): Flag a product 908 | // See "Add a flag button/API to put up a product for review when you're in a hurry": https://github.com/openfoodfacts/openfoodfacts-server/issues/1408 909 | if (event.key === 'S') { 910 | flagThisRevision(); 911 | return; 912 | } 913 | // (T): transfer a product from a language to another 914 | if (event.key === 'T') { 915 | if (pageType !== "edit") { 916 | showPowerUserInfo('

    Transfer only work in "edit" mode.

    '); 917 | return; 918 | } 919 | // products to test: https://es-en.openfoodfacts.org/language/en:1/language/french 920 | // https://europe-west1-openfoodfacts-1148.cloudfunctions.net/openfoodfacts-language-change?ol=fr&fl=es&code=7622210829580 921 | // TODO: use detectLanguages() function 922 | var array_langs = $("#sorted_langs").val().split(","); 923 | var options_langs; 924 | var transferServiceURL = "https://europe-west1-openfoodfacts-1148.cloudfunctions.net/openfoodfacts-language-change"; 925 | $.each(array_langs,function(i){ 926 | options_langs += ''; 927 | }); 928 | log("options_langs: "+options_langs); 929 | var transfer = "
    " + 930 | '
    ' + 931 | "" + 932 | "" + 935 | "" + 936 | "" + 937 | "" + 938 | " Transfer\">" + 939 | "
    " + 940 | '
    ' + 941 | "
    "; 942 | showPowerUserInfo(transfer); // open a new window 943 | $("#transfer_submit").click(function(){ 944 | var url = transferServiceURL + 945 | "?ol=" + $("#transfer_ol").val() + 946 | "&fl=" + $("#transfer_fl").val() + 947 | "&code=" + code; 948 | log("transfert url: "+url); 949 | $.ajax({url: url, success: function(result){ 950 | $("#transfer_result").html(result); 951 | }}); 952 | $("#transfer_result").html("

    Page is going to reload in 5s...

    "); 953 | setTimeout(function() { 954 | location.reload(); // reload the page 955 | }, 8000); 956 | }); 957 | return; 958 | } 959 | } 960 | 961 | }); 962 | } 963 | 964 | 965 | 966 | 967 | // *** 968 | // * View mode 969 | // * 970 | // Test if we are in a product view. 971 | if (pageType === "product view") { 972 | 973 | // Showing it directly on the product page, for emerging categories. 974 | // https://world.openfoodfacts.org/cgi/search.pl?action=process&sort_by=unique_scans_n&page_size=20&action=display&tagtype_0=states&tag_contains_0=contains&tag_0=categories%20to%20be%20completed&search_terms=lasagne 975 | var productName = $('h1[property="food:name"]').html().match(/(.*?)(( - .*)|$)/)[1]; // h1[property="food:name"] => Cerneaux noix de pécan - Vahiné - 50 g ℮ 976 | log("productName: " + productName); 977 | var SearchUncategorizedProductsOpportunitiesDeepLink = encodeURI(productName); 978 | $("#hungerGameLink").after( 979 | ((SearchUncategorizedProductsOpportunitiesDeepLink) ? '

    '+ 980 | '> ' + 983 | 'Categorization opportunities' + 984 | '

    ' : "")); 985 | 986 | 987 | // For each different brand, if any, add a deep link to Hunger Game 988 | // TODO: make this a parameter which can be saved from a session to another; something like: 989 | // readParameter(isLinkToHungerGameForEachBrand) 990 | let isLinkToHungerGameForEachBrand = true; 991 | if(isLinkToHungerGameForEachBrand) { 992 | $('[itemprop="brand"]').each(function() { 993 | const brand = normalizeTagName($(this).text()); 994 | $(this).after(' [Hunger Game]'); 995 | }); 996 | } 997 | 998 | 999 | // If ingredients are already entered, show results of the OCR 1000 | if($("#editingredients")[0]) { 1001 | // Looking for ingredients language 1002 | var regex1 = new RegExp(/\((..)\)/); 1003 | var ingredientsButton = $("#editingredients").html(); 1004 | //log($("#editingredients").html()); 1005 | var lc = regex1.exec(ingredientsButton)[1]; 1006 | log("Ingredients language: "+lc); 1007 | 1008 | // Show results of the OCR 1009 | $('body').on('DOMNodeInserted', '#ingredients_list', function(e) { 1010 | $(e.target).before( "

    OCR results (not saved):

    " ); 1011 | $(e.target).before( "" ); 1012 | getIngredientsFromGCV(code,lc); 1013 | $(e.target).before( "

    Text to be saved:

    " ); 1014 | }); 1015 | } 1016 | 1017 | } 1018 | 1019 | 1020 | 1021 | // *** 1022 | // * Edit mode 1023 | // * 1024 | // Accesskeys ; see https://stackoverflow.com/questions/5061353/how-to-create-a-keyboard-shortcut-for-an-input-button 1025 | // "P" could be for "Product characteristic" section (view mode:

    Product characteristics

    =>

    Product characteristics

    (not very useful) ; edit mode: Product characteristics => add the id) 1026 | // "P" could also be for the "product name" field (edit mode: id="product_name_fr" when fr) 1027 | // "Q" for "quantity" 1028 | // "B" for "brands" 1029 | // "C" for "categories" (very important field) 1030 | // "L" for "labels" 1031 | // "I" could be for "Ingredients" section (view mode:

    Ingredients

    =>

    Ingredients

    ; edit mode: Ingredients => add the id) 1032 | // "I" could also be for the "Ingredients" field (edit mode: id="ingredients_text_fr" when fr) 1033 | // "N" could be for "Nutrition facts" section (view mode:

    Nutrition facts

    =>

    Nutrition facts

    ; edit mode: Nutrition facts => add the id) 1034 | // "N" could also be for the "Energy" field in edit mode (id="nutriment_energy") 1035 | // "F" for "Dietary fiber" (often not completed for historical reasons) 1036 | if (pageType === "edit") { 1037 | $("#product_name_fr").attr("accesskey","P"); 1038 | $("#quantity").attr("accesskey","Q"); 1039 | $("#brands_tagsinput").attr("accesskey","B"); 1040 | $("#categories_tagsinput").attr("accesskey","C"); 1041 | $("#labels_tagsinput").attr("accesskey","L"); 1042 | $("#ingredients_text_fr").attr("accesskey","I"); 1043 | $("#nutriment_energy").attr("accesskey","N"); 1044 | $("#nutriment_fiber").attr("accesskey","F"); 1045 | 1046 | // Toggle helpers based on previous selection if any 1047 | toggleHelpers(); 1048 | toggleIngredientsMonospace(); 1049 | toggleDFMode(); 1050 | 1051 | // Add "History" anchor in the nav bar 1052 | let newElement = document.createElement("li"); 1053 | newElement.innerHTML += `History`; 1054 | newElement.className = "item-list"; 1055 | document.querySelector('#navbar ul').append(newElement); 1056 | 1057 | // TODO: add ingredients picture aside ingredients text area 1058 | var ingredientsImage = $("#display_ingredients_es img"); 1059 | log("ingredientsImage: "+ ingredientsImage); 1060 | $("#ingredients_text_es").after(ingredientsImage); 1061 | $("#ingredients_text_es").css({ 1062 | "width": "50%", 1063 | "float": "left", 1064 | }); 1065 | // //$("#display_ingredients_es img").clone().after("#ingredients_text_es"); 1066 | 1067 | 1068 | // Check serving size field 1069 | checkServingSize(document.getElementById("serving_size").value); 1070 | document.getElementById("serving_size").addEventListener("input", function() { 1071 | checkServingSize(this.value); 1072 | }); 1073 | 1074 | // Check fibers' field 1075 | checkFiber($("#nutriment_fiber").attr("value")); 1076 | $("#nutriment_fiber").on("input", function() { 1077 | checkFiber($(this).val()); 1078 | }); 1079 | 1080 | // Check energy (kJ and kcal) now and for any change 1081 | checkKJ(); 1082 | $("#nutriment_energy-kj").on("input", function() { 1083 | $("#nutriment_energy-kj").attr("value", $(this).val()); 1084 | checkKJ(); 1085 | }); 1086 | $("#nutriment_energy-kcal").on("input", function() { 1087 | $("#nutriment_energy-kcal").attr("value", $(this).val()); 1088 | checkKJ(); 1089 | }); 1090 | 1091 | // Compute and display energy in realtime, based on fat, carbs, fibers, proteins, polyols and alcohol 1092 | const energySpan = ''; 1093 | document.querySelector('[for="nutriment_energy-kj"]').insertAdjacentHTML('afterend', energySpan); 1094 | computeEnergy(); 1095 | const ids = ["nutriment_fat", "nutriment_carbohydrates", "nutriment_proteins", "nutriment_polyols", "nutriment_fiber", "nutriment_alcohol"]; 1096 | for (const id of ids) { 1097 | const element = document.getElementById(id); 1098 | element && element.addEventListener("input", computeEnergy); 1099 | } 1100 | } 1101 | 1102 | 1103 | 1104 | // *** 1105 | // * Saved product page 1106 | // * 1107 | var nbOfSameBrandProducts; 1108 | 1109 | if(pageType === "saved-product page") { 1110 | $("#main_column").append( 1111 | '
    ' + 1112 | '
    ' + 1113 | '
    ' + 1114 | '

    Power User Script

    ' + 1115 | '

    Product issues:

    ' + 1116 | '
      ' + 1117 | '
    ' + 1118 | '
    Going further:
    ' + 1119 | '
      ' + 1120 | '
    ' + 1121 | '
    ' + 1122 | '
    ' + 1123 | '
    ' 1124 | ); 1125 | isNbOfSimilarNamedProductsWithoutACategory(); 1126 | addQualityTags(); 1127 | addStateTags(); 1128 | } 1129 | 1130 | 1131 | 1132 | // *** 1133 | // * Recent Changes page 1134 | // * 1135 | if (pageType === "recent changes") { 1136 | $("#main_column h1").after(''); 1137 | $("#filter").on("keyup", function() { 1138 | var value = $(this).val().toLowerCase(); 1139 | $("#main_column li").filter(function() { 1140 | $(this).toggle($(this).text().toLowerCase().indexOf(value) > -1); 1141 | }); 1142 | value == "" ? $("#main_column details").show() : $("#main_column details").hide(); 1143 | }); 1144 | } 1145 | 1146 | 1147 | 1148 | // *** 1149 | // * "list" mode (when a page contains a list of products (home page, facets, search results...) 1150 | // * 1151 | if (pageType === "list") { 1152 | // CSS injected by listByRows() when switching to list edit mode. 1153 | var css_4_list =` 1154 | /* */ 1155 | #main_column { height:auto !important; } /* Because main_column has an inline style with "height: 1220px" */ 1156 | ul#products_match_all { /*display: table; /**/ border-collapse: collapse; /*float:none;/**/ } 1157 | ul.search_results { display: block; } 1158 | ul#products_match_all li { position: static; display: table-row; width: auto; text-align: left; border: 1px solid black; float:none; } 1159 | 1160 | ul#products_match_all > li > a, 1161 | ul#products_match_all > li > a > div, 1162 | ul#products_match_all > li > a > span, 1163 | .ingr, 1164 | .p_actions { display: table-cell; } 1165 | 1166 | ul#products_match_all > li > a { border: 1px solid black; } 1167 | .ingr, .p_actions { border: 0px solid black;/**/ } 1168 | .ingr { border-right: 0px; } .p_actions {border-left: 0px; } 1169 | 1170 | ul#products_match_all > li > a { display: table-cell; width: 30%; vertical-align: middle; height: 6rem !important; } 1171 | ul#products_match_all > li > a > div { display: table-cell; max-width: 35% !important; } /* */ 1172 | .list_product_name { height: auto; } 1173 | ul#products_match_all > li > a > span { display: table-cell; width: 70%; vertical-align: middle; padding-left: 1rem;} /* */ 1174 | 1175 | .wrap_ingr { position: relative; line-height: 1rem !important; } 1176 | .ingr { display: table-cell; /*width: 800px;/**/ height:8rem; margin: 0; vertical-align: middle; padding: 0 0.6rem 0 0.6rem;} 1177 | .p_actions { display: table-cell; vertical-align: middle; padding: 0.5rem; line-height: 2rem !important; width: 4rem !important; } 1178 | .ingr, .p_actions > button { font-size: 0.9rem; vertical-align: middle; } 1179 | .save_needs_clicking { background-color: #ff952b; } 1180 | .p_actions > button { margin: 0 0 0 0; padding: 0.3rem 0.1rem 0.3rem 0.1rem; width: 6rem; } 1181 | .ingr_del { background-color: #ff2c2c; } 1182 | ._lang { position: absolute; top:3rem; right:16px; font-size:3rem; opacity:0.4; } 1183 | 1184 | #timed_alert, div.timed_alert { position:fixed; top:0; right:0; font-size: 8rem } 1185 | #timed_alert.failed, div.timed_alert.failed { color: red; } 1186 | 1187 | `; 1188 | 1189 | // Help box based on page type: list 1190 | var listhelp = `
      1191 |
    • (?) or (h): this present help
    • 1192 |
      1193 |
    • 1194 |
    • 1195 |
      1196 |
    • (Shift+L): List edit mode
    • 1197 |
    • (Shift+b): Show/hide barcodes
    • 1198 |
    • (n): reload the page without cache (add &nocache=1)
    • 1199 |
    `; 1200 | 1201 | // Help icon fixed 1202 | $('body').append(''); 1203 | 1204 | // User help dialog 1205 | $("#pwe_help").click(function(){ 1206 | togglePowerUserInfo(listhelp); 1207 | toggleIngredientsMonospace(); 1208 | toggleAlwaysShowBarcodes(); 1209 | }); 1210 | 1211 | // detect product codes and add them as attributes 1212 | addCodesToProductList(); 1213 | showListButtons(); 1214 | loadAlwaysShowBarcodesFromStorage(); 1215 | 1216 | // Show an easier to read number of products 1217 | /* 1218 | var xxxProducts = $(".button-group li div").text(); log(xxxProducts); // 1009326 products 1219 | var nbOfProducts = parseInt(xxxProducts.match(/(\d+)/g)[0]); //log(nbOfProducts); // 1009326 1220 | nbOfProducts = nbOfProducts.toLocaleString(); //log(nbOfProducts); // 1 009 326 1221 | $(".button-group li div").text(xxxProducts.replace(/(\d+)(.*)/, nbOfProducts+"$2")); // 1 009 326 products /**/ 1222 | 1223 | 1224 | var listByRowsMode = false; // We are not yet in "list by rows" mode 1225 | // Keyboard actions 1226 | if (listByRowsOption === true) { listByRows(); } 1227 | $(document).on('keydown', function(event) { 1228 | // If the key is not pressed inside a input field (ex. search product field) 1229 | if (!$(event.target).is(':input')) { 1230 | // (Shift + L) 1231 | if (event.key === 'L' && listByRowsMode === false) { 1232 | listByRows(); 1233 | return; 1234 | } 1235 | 1236 | // (n): reload and add &nocache=1 if not already the case 1237 | if (event.key === 'n') { 1238 | let nocache = ((/\&nocache=1/.test(window.location)) ? "" : "&nocache=1"); 1239 | window.open(window.location + nocache, "_self"); // reload in current window 1240 | return; 1241 | } 1242 | 1243 | // (?): open help box 1244 | if (event.key === '?' || event.key === 'h') { 1245 | togglePowerUserInfo(listhelp); 1246 | toggleIngredientsMonospace(); 1247 | return; 1248 | } 1249 | 1250 | // (Shift + B) - show/hide barcodes 1251 | if (event.key === 'B') { 1252 | toggleListBarcodes(); 1253 | return; 1254 | } 1255 | } 1256 | 1257 | }); 1258 | 1259 | } // if list mode 1260 | 1261 | var langcodes_with_different_countrycodes = [ "af", "am", "ar", "bn", "cs", "da", "dv", "dz", "el", "et", "fa", "hy", "ja", "ka", "kl", "km", "ko", "lo", "ms", "my", "na", "nb", "ne", "ps", "si", "sl", "sq", "sr", "sv", "ta", "tk", "uk", "ur", "vi", "zh" ]; 1262 | 1263 | //Copy data from the list textarea to the ingredients_text in the hidden form so it can be passed to the analyser 1264 | //As the list can contain different languages we take the language from the textarea 1265 | function CopyListData(_code, lang){ 1266 | log("Lang:" + lang); 1267 | var cd = $("#i" + _code).val(); 1268 | log("Language Text:"+cd); 1269 | var country = lang; 1270 | 1271 | // handle languages where the language code and country code differ. 1272 | if (langcodes_with_different_countrycodes.includes(lang)) { 1273 | country = "world-" + lang; 1274 | } 1275 | 1276 | //Here we have to manipulate the language for regional languages 1277 | if(lang === 'ca'){ country = 'es-ca'; } //Catalan 1278 | if(lang === 'en'){ 1279 | country = 'world'; //English 1280 | } 1281 | 1282 | //As target language can be different from the page language we have to create the full URL 1283 | var URL = "//" + country + ".openfoodfacts.org/cgi/test_ingredients_analysis.pl"; 1284 | log("CopyListData() analyse url="+URL); 1285 | analyse_form.action = URL; 1286 | //analyse_form.setAttribute("action", URL); 1287 | $("#ingredients_text").val(cd); 1288 | } 1289 | 1290 | //Copy data from the language specific ingredients_text to the ingredients_text in the hidden form so it can be passed to the analyser 1291 | //This is for single product page, list is handled differently 1292 | function Copydata(){ 1293 | var lang = $('ul#tabs_ingredients_image > li.active').attr("data-language"); 1294 | var pageLanguage = $("html").attr('lang'); // Get page language 1295 | var country = lang; 1296 | 1297 | //log("Lang:" + lang); 1298 | var cd = $("#ingredients_text_"+lang).val(); 1299 | //log("Language Text:"+cd); 1300 | 1301 | // handle languages where the language code and country code differ. 1302 | if (langcodes_with_different_countrycodes.includes(lang)) { 1303 | country = "world-" + lang; 1304 | } 1305 | 1306 | //Here we have to manipulate the language for regional languages 1307 | if(lang === 'ca'){ country = 'es-ca'; } //Catalan 1308 | if(lang === 'en'){ 1309 | if(pageLanguage === 'en'){ 1310 | country = 'uk';//English 1311 | } 1312 | else 1313 | { 1314 | country = pageLanguage + '-en'; //English from source language page 1315 | } 1316 | } 1317 | 1318 | //As target language can be different from the page language we have to create the full URL 1319 | var URL = "//" + country + ".openfoodfacts.org/cgi/test_ingredients_analysis.pl"; 1320 | //analyse_form.setAttribute("action", "/cgi/test_ingredients_analysis.pl"); 1321 | log("Copydata() analyse url="+URL); 1322 | analyse_form.setAttribute("action", URL); 1323 | $("#ingredients_text").val(cd); 1324 | } 1325 | 1326 | 1327 | 1328 | function submitToPopup(f) { 1329 | log("submitToPopup"); 1330 | var w = window.open('', 'form-target', 'width=800','height=800'); 1331 | f.target = 'form-target'; 1332 | f.submit(); 1333 | } 1334 | 1335 | 1336 | 1337 | /*** 1338 | * listByRows 1339 | * 1340 | * @param : none 1341 | * @return : none 1342 | */ 1343 | function listByRows() { 1344 | log("listByRows() > List by rows -------------"); 1345 | listByRowsMode = true; 1346 | log("listByRows() > listByRowsMode: " + listByRowsMode); 1347 | var s = document.createElement('style'); 1348 | s.type = 'text/css'; 1349 | s.innerHTML = css_4_list; 1350 | document.documentElement.appendChild(s); 1351 | 1352 | var urlList = document.URL; 1353 | var prods = getJSONList(urlList); 1354 | //log(prods); 1355 | 1356 | $(".off").hide(); 1357 | $(".app").hide(); 1358 | $(".project").hide(); 1359 | $(".community").hide(); 1360 | } 1361 | 1362 | 1363 | 1364 | /*** 1365 | * getJSONList 1366 | * 1367 | * @param : var, url of the list; example: https://world.openfoodfacts.org/cgi/search.pl?search_terms=banania&search_simple=1 1368 | * @return : object, JSON list of products 1369 | */ 1370 | function getJSONList(urlList) { 1371 | // Test URLs: 1372 | // https://world.dev.openfoodfacts.org/quality/ingredients-100-percent-unknown 1373 | // https://fr.openfoodfacts.org/quality/ingredients-100-percent-unknown/quality/ingredients-ingredient-tag-length-greater-than-50/200 ( 1374 | var ingr = ""; 1375 | $.getJSON( urlList + "&json=1&page_size=100", function(data) { 1376 | log("getJSONList(urlList) > Data from products' page: " + urlList); 1377 | log(data); 1378 | 1379 | var data_by_code = {}; 1380 | for (let aproduct of data.products) { 1381 | //log(aproduct); 1382 | data_by_code[aproduct.code] = aproduct; 1383 | } 1384 | //log(data_by_code); 1385 | 1386 | var local_code, editIngUrl; 1387 | $( "ul#products_match_all > li" ).each(function( index ) { 1388 | //log( index + ": " + $( this ).text() ); 1389 | //$( this ).find(">:first-child").append(''+data["products"][index]["ingredients_text"]+''); 1390 | //local_code = data.products[index].code; 1391 | local_code = $(this).attr('data-code'); 1392 | if (data_by_code[local_code] === undefined) { 1393 | return; 1394 | } 1395 | 1396 | //log("local_code: " + local_code ); 1397 | var _lang = data_by_code[local_code].lang; 1398 | editIngUrl = document.location.protocol + "//" + document.location.host + 1399 | '/cgi/product.pl?type=edit&code=' + local_code + '#tabs_ingredients_image'; 1400 | // Add ingredients form 1401 | // Note: we added lang="xx" to let browsers spellcheck contents of each form depending 1402 | // on the language. But it seems complicated, see: 1403 | // TODO: https://stackoverflow.com/questions/41252737/over-ride-chrome-browser-spell-check-language-using-jquery-or-javascript 1404 | // https://bugs.chromium.org/p/chromium/issues/detail?id=389498 (It's a "won't fix" in Chrome) 1405 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1073827#c33 1406 | // about:config in Firefox 1407 | $("html").removeAttr("lang"); 1408 | if (data_by_code[local_code].ingredients_text == null) { 1409 | data_by_code[local_code].ingredients_text = ''; 1410 | } 1411 | $( this ).append('
    '+ 1412 | ''+ 1415 | ''+ _lang +''+ 1416 | '
    ' 1417 | ); 1418 | $( this ).append('
    '+ 1419 | ''+ 1423 | ''+ 1427 | "'+ 1431 | 1432 | "'+ 1436 | 1437 | "'+ 1441 | 1442 | "'+ 1446 | 1447 | "'+ 1451 | 1452 | '
    '); 1453 | 1454 | $("#i"+local_code).attr('lang', _lang); 1455 | // Edit ingredient field inline 1456 | //$("#i"+local_code).dblclick(function() { 1457 | // log("dblclick on: "+$(this).attr("id")); 1458 | //}); 1459 | 1460 | $("#i"+local_code).on("change", function() { 1461 | var _code = $(this).attr("id").replace('i','p_actions_sav_'); 1462 | $("#"+_code).addClass("save_needs_clicking"); 1463 | }); 1464 | 1465 | //Ingredients analysis check - opens in new window 1466 | $("#p_actions_analysis_"+local_code).click(function(){ 1467 | //log("analyse"); 1468 | var _code = $(this).attr("value"); 1469 | 1470 | CopyListData(_code, _lang); 1471 | submitToPopup(analyse_form); 1472 | }); 1473 | 1474 | //Move product to OBF 1475 | $("#p_actions_obf_"+local_code).click(function(){ 1476 | moveProductToSite( $(this).attr("value"), 'beauty' ); 1477 | }); 1478 | 1479 | //Move product to OPF 1480 | $("#p_actions_opf_"+local_code).click(function(){ 1481 | moveProductToSite( $(this).attr("value"), 'product' ); 1482 | }); 1483 | 1484 | //Move product to OPFF 1485 | $("#p_actions_opff_"+local_code).click(function(){ 1486 | moveProductToSite( $(this).attr("value"), 'petfood' ); 1487 | }); 1488 | 1489 | // Save ingredients 1490 | $("#p_actions_sav_"+local_code).click(function(){ 1491 | //saveProductField(productCode, field); 1492 | var _code = $(this).attr("value"); 1493 | var _url = document.location.protocol + "//" + encodeURIComponent(document.location.host) + 1494 | "/cgi/product_jqm2.pl?code=" + encodeURIComponent(_code) + 1495 | "&ingredients_text_" + encodeURIComponent(_lang) + 1496 | "=" + encodeURIComponent($("#i" + _code).val()); 1497 | log("getJSONList(urlList) > "+_url); 1498 | $("body").append('
    Saving
    '); 1499 | var _d = $.getJSON(_url, function() { 1500 | log("getJSONList(urlList) > Save product ingredients"); 1501 | }) 1502 | .done(function(jqm2) { 1503 | log(jqm2.status_verbose); 1504 | log(jqm2); 1505 | $("#p_actions_sav_"+_code).removeClass("save_needs_clicking"); 1506 | $("#timed_alert_save_" + _code).html('Saved!'); 1507 | $("#timed_alert_save_" + _code).fadeOut(3000, function () { $(this).remove(); }); 1508 | }) 1509 | .fail(function() { 1510 | log("getJSONList(urlList) > fail"); 1511 | $("#timed_alert_save_" + _code).html('Failed!'); 1512 | $("#timed_alert_save_" + _code).addClass('failed'); 1513 | $("#timed_alert_save_" + _code).fadeOut(3000, function () { $(this).remove(); }); 1514 | }); 1515 | }); 1516 | 1517 | // Delete ingredients field: https://world.openfoodfacts.net/cgi/product_jqm2.pl?code=0048151623426&ingredients_text= 1518 | $("#p_actions_del_"+local_code).click(function(){ 1519 | //deleteProductField(productCode, field); 1520 | var _code = $(this).attr("value"); 1521 | var _url = document.location.protocol + "//" + document.location.host + "/cgi/product_jqm2.pl?code=" + _code + "&ingredients_text="; 1522 | log("getJSONList(urlList) > "+_url); 1523 | var _d = $.getJSON(_url, function() { 1524 | log("getJSONList(urlList) > Delete product ingredients"); 1525 | }) 1526 | .done(function(jqm2) { 1527 | log(jqm2.status_verbose); 1528 | log(jqm2); 1529 | $("#i"+_code).empty(); 1530 | }) 1531 | .fail(function() { 1532 | log("getJSONList(urlList) > fail"); 1533 | }); 1534 | }); 1535 | }); 1536 | toggleIngredientsMonospace(); 1537 | return data; 1538 | }); 1539 | } 1540 | 1541 | 1542 | // Show pop-up 1543 | function showPowerUserInfo(message) { 1544 | log("showPowerUserInfo(message) > "+$("#power-user-help")); 1545 | // Inspiration: http://christianelagace.com 1546 | // If not already exists, create div for popup 1547 | if($("#power-user-help").length === 0) { 1548 | $('body').append('
    '); 1549 | $("#power-user-help").dialog({autoOpen: false}); 1550 | } 1551 | 1552 | $("#power-user-help").html(message); 1553 | 1554 | // transforme la division en popup 1555 | let popup = $("#power-user-help").dialog({ 1556 | autoOpen: true, 1557 | width: 400, 1558 | dialogClass: 'dialogstyleperso', 1559 | }); 1560 | // add style if necessarry 1561 | //$("#power-user-help").prev().addClass('ui-state-information'); 1562 | return popup; 1563 | } 1564 | 1565 | // Toggle popup 1566 | function togglePowerUserInfo(message) { 1567 | if ($("#power-user-help").dialog( "isOpen" ) === true) { 1568 | $("#power-user-help").dialog( "close" ); 1569 | return false; 1570 | } else { 1571 | return showPowerUserInfo(message); 1572 | } 1573 | } 1574 | 1575 | // Hide Text Fields 1576 | function toggleHideTextFieldsPopUp() { 1577 | if($("#power-user-hide-fields-popup").dialog("isOpen") === true){ 1578 | $("#power-user-hide-fields-popup").dialog("close"); 1579 | }else{ 1580 | return showPowerUserHideTextFieldsPopUp(); 1581 | } 1582 | } 1583 | 1584 | function showPowerUserHideTextFieldsPopUp(){ 1585 | if($("#power-user-hide-fields-popup").length === 0){ 1586 | $('body').append('
    '); 1587 | $("#power-user-hide-fields-popup").dialog({autoOpen: false}); 1588 | } 1589 | 1590 | var popUpContent = getPowerUserHideFieldsContent(); 1591 | 1592 | $("#power-user-hide-fields-popup").html(popUpContent); 1593 | 1594 | getHideFieldsCheckboxesFromStorage(); 1595 | 1596 | let popup = $("#power-user-hide-fields-popup").dialog({ 1597 | autoOpen: true, 1598 | width: 400, 1599 | dialogClass: 'dialogstyleperso', 1600 | }); 1601 | } 1602 | 1603 | function getPowerUserHideFieldsContent(){ 1604 | return `
      1605 |
    • `+ createInputWithCheckbox('Hide misc card','pus-hide-misc-card') + ` 1606 |
      • `+ createInputWithCheckbox('Barcode not correct','pus-hide-barcode-not-correct') + `
      1607 |
      • `+ createInputWithCheckbox('Product taken off the market','pus-hide-product-taken-off') + `
      1608 |
      • `+ createInputWithCheckbox('Withdrawal date','pus-hide-withdrawal-date') + `
      1609 |
      • `+ createInputWithCheckbox('Alert boxes','pus-hide-alert-boxes') + `
      1610 |
    • 1611 |
      1612 |
    • `+ createInputWithCheckbox('Hide product picture card','pus-hide-product-picture') + `
    • 1613 |
      1614 |
    • `+ createInputWithCheckbox('Hide product characteristics card','pus-hide-product-char') + ` 1615 |
      • `+ createInputWithCheckbox('Product name','pus-hide-product-name') + `
      1616 |
      • `+ createInputWithCheckbox('Common name','pus-hide-common-name') + `
      1617 |
      • `+ createInputWithCheckbox('Quantity','pus-hide-quantity') + `
      1618 |
      • `+ createInputWithCheckbox('Brands','pus-hide-brands') + `
      1619 |
      • `+ createInputWithCheckbox('Categories','pus-hide-categories') + `
      1620 |
      • `+ createInputWithCheckbox('Labels, certifications, awards','pus-hide-labels') + `
      1621 |
      • `+ createInputWithCheckbox('Manufacturing or processing places','pus-hide-manufactoring') + `
      1622 |
      • `+ createInputWithCheckbox('Traceability code','pus-hide-traceability') + `
      1623 |
      • `+ createInputWithCheckbox('Link to the product page...','pus-hide-link-to-product') + `
      1624 |
      • `+ createInputWithCheckbox('Best before date','pus-hide-best-before') + `
      1625 |
      • `+ createInputWithCheckbox('City, state and country ','pus-hide-city-state') + `
      1626 |
      • `+ createInputWithCheckbox('Stores','pus-hide-stores') + `
      1627 |
      • `+ createInputWithCheckbox('Countries where sold','pus-hide-countries-sold') + `
      1628 |
    • 1629 |
      1630 |
    • `+ createInputWithCheckbox('Hide ingredients card','pus-hide-ingredients') + ` 1631 |
      • `+ createInputWithCheckbox('Origin of the product ','pus-hide-origin-product') + `
      1632 |
      • `+ createInputWithCheckbox('Substances or products...','pus-hide-substances') + `
      1633 |
      • `+ createInputWithCheckbox('Traces','pus-hide-traces') + `
      1634 |
      • `+ createInputWithCheckbox('Origin of ingredients','pus-hide-origin-ingredients') + `
      1635 | 1636 |
    • 1637 |
      1638 |
    • `+ createInputWithCheckbox('Hide nutrition card','pus-hide-nutrition') + `
    • 1639 |
      1640 |
    • `+ createInputWithCheckbox('Hide packaging card','pus-hide-packaging') + `
    • 1641 |
    `; 1642 | } 1643 | 1644 | //generetes an input and also manages, stores, retrieves the checked state. 1645 | function createInputWithCheckbox(labelValue, inputId){ 1646 | let checkbox = ''; 1647 | return checkbox 1648 | } 1649 | 1650 | function getHideFieldsCheckboxesFromStorage(){ 1651 | getHideFieldCheckboxFromStorage('pus-hide-misc-card',['#misc']); 1652 | getHideFieldCheckboxFromStorage('pus-hide-barcode-not-correct',['#label_new_code','#new_code']); 1653 | getHideFieldCheckboxFromStorage('pus-hide-product-taken-off',['#obsolete','label[for="obsolete"]']); 1654 | getHideFieldCheckboxFromStorage('pus-hide-withdrawal-date',['#obsolete_since_date','label[for="obsolete_since_date"]']); 1655 | getHideFieldCheckboxFromStorage('pus-hide-alert-boxes',['#warning_3rd_party_content','#licence_accept']); 1656 | getHideFieldCheckboxFromStorage('pus-hide-product-picture',['#product_image']); 1657 | getHideFieldCheckboxFromStorage('pus-hide-product-char',['#product_characteristics']); 1658 | getHideFieldCheckboxFromStorage('pus-hide-product-name',['[id^="product_name_"]','label[for^="product_name_"]']); 1659 | getHideFieldCheckboxFromStorage('pus-hide-common-name',['[id^="generic_name_"]','label[for^="generic_name_"]']); 1660 | getHideFieldCheckboxFromStorage('pus-hide-quantity',['#quantity','label[for="quantity"]']); 1661 | getHideFieldCheckboxFromStorage('pus-hide-brands',['label[for="brands"]','label[for="brands"]'],true); 1662 | getHideFieldCheckboxFromStorage('pus-hide-categories',['label[for="categories"]','label[for="categories"]'],true); 1663 | getHideFieldCheckboxFromStorage('pus-hide-labels',['label[for="labels"]','label[for="labels"]'],true); 1664 | getHideFieldCheckboxFromStorage('pus-hide-manufactoring',['label[for="manufacturing_places"]','label[for="manufacturing_places"]'],true); 1665 | getHideFieldCheckboxFromStorage('pus-hide-traceability',['label[for="emb_codes"]','label[for="emb_codes"]'],true); 1666 | getHideFieldCheckboxFromStorage('pus-hide-link-to-product',['#link','label[for="link"]']); 1667 | getHideFieldCheckboxFromStorage('pus-hide-best-before',['#expiration_date','label[for="expiration_date"]']); 1668 | getHideFieldCheckboxFromStorage('pus-hide-city-state',['label[for="purchase_places"]','label[for="purchase_places"]'],true); 1669 | getHideFieldCheckboxFromStorage('pus-hide-stores',['label[for="stores"]','label[for="stores"]'],true); 1670 | getHideFieldCheckboxFromStorage('pus-hide-countries-sold',['label[for="countries"]','label[for="countries"]'],true); 1671 | getHideFieldCheckboxFromStorage('pus-hide-ingredients',['#ingredients']); 1672 | getHideFieldCheckboxFromStorage('pus-hide-origin-product',['[id^="origin_"]','label[for^="origin_"]']); 1673 | getHideFieldCheckboxFromStorage('pus-hide-substances',['label[for="allergens"]','label[for="allergens"]'],true); 1674 | getHideFieldCheckboxFromStorage('pus-hide-traces',['label[for="traces"]','label[for="traces"]'],true); 1675 | getHideFieldCheckboxFromStorage('pus-hide-origin-ingredients',['label[for="origins"]','label[for="origins"]'],true); 1676 | getHideFieldCheckboxFromStorage('pus-hide-nutrition',['#nutrition']); 1677 | getHideFieldCheckboxFromStorage('pus-hide-packaging',['#packaging_section']); 1678 | } 1679 | 1680 | function getHideFieldCheckboxFromStorage(checkboxId,hideFieldsIds,hasTags = false){ 1681 | if(getLocalStorage(checkboxId) === "checked"){ 1682 | $('#'+checkboxId).prop("checked", true); 1683 | } 1684 | 1685 | $('#'+checkboxId).change(function() { 1686 | if(this.checked){ 1687 | localStorage.setItem(checkboxId, "checked"); 1688 | }else{ 1689 | localStorage.setItem(checkboxId, "unchecked"); 1690 | } 1691 | toggleHideField(hideFieldsIds,hasTags); 1692 | }); 1693 | } 1694 | 1695 | function toggleHideField(hideFieldsIds,hasTags = false){ 1696 | //$("label[for='brands']").next().hide(); 1697 | $.each(hideFieldsIds,function(index,element){ 1698 | var elemen = element; 1699 | if(hasTags && index===1){ elemen = $(elemen).next();} 1700 | if($(elemen).hasClass('pus-hide-content')){ 1701 | $(elemen).removeClass('pus-hide-content'); 1702 | $(elemen).show(); 1703 | }else{ 1704 | $(elemen).addClass('pus-hide-content'); 1705 | $(elemen).hide(); 1706 | } 1707 | }); 1708 | } 1709 | 1710 | function loadHideTextFieldsFromStorage(){ 1711 | $( window ).on( "load", function() { 1712 | loadHideTextFieldFromStorage('pus-hide-misc-card',['#misc']); 1713 | loadHideTextFieldFromStorage('pus-hide-barcode-not-correct',['#label_new_code','#new_code']); 1714 | loadHideTextFieldFromStorage('pus-hide-product-taken-off',['#obsolete','label[for="obsolete"]']); 1715 | loadHideTextFieldFromStorage('pus-hide-withdrawal-date',['#obsolete_since_date','label[for="obsolete_since_date"]']); 1716 | loadHideTextFieldFromStorage('pus-hide-alert-boxes',['#warning_3rd_party_content','#licence_accept']); 1717 | loadHideTextFieldFromStorage('pus-hide-product-picture',['#product_image']); 1718 | loadHideTextFieldFromStorage('pus-hide-product-char',['#product_characteristics']); 1719 | loadHideTextFieldFromStorage('pus-hide-product-name',['[id^="product_name_"]','label[for^="product_name_"]']); 1720 | loadHideTextFieldFromStorage('pus-hide-common-name',['[id^="generic_name_"]','label[for^="generic_name_"]']); 1721 | loadHideTextFieldFromStorage('pus-hide-quantity',['#quantity','label[for="quantity"]']); 1722 | loadHideTextFieldFromStorage('pus-hide-brands',['label[for="brands"]','label[for="brands"]'],true); 1723 | loadHideTextFieldFromStorage('pus-hide-categories',['label[for="categories"]','label[for="categories"]'],true); 1724 | loadHideTextFieldFromStorage('pus-hide-labels',['label[for="labels"]','label[for="labels"]'],true); 1725 | loadHideTextFieldFromStorage('pus-hide-manufactoring',['label[for="manufacturing_places"]','label[for="manufacturing_places"]'],true); 1726 | loadHideTextFieldFromStorage('pus-hide-traceability',['label[for="emb_codes"]','label[for="emb_codes"]'],true); 1727 | loadHideTextFieldFromStorage('pus-hide-link-to-product',['#link','label[for="link"]']); 1728 | loadHideTextFieldFromStorage('pus-hide-best-before',['#expiration_date','label[for="expiration_date"]']); 1729 | loadHideTextFieldFromStorage('pus-hide-city-state',['label[for="purchase_places"]','label[for="purchase_places"]'],true); 1730 | loadHideTextFieldFromStorage('pus-hide-stores',['label[for="stores"]','label[for="stores"]'],true); 1731 | loadHideTextFieldFromStorage('pus-hide-countries-sold',['label[for="countries"]','label[for="countries"]'],true); 1732 | loadHideTextFieldFromStorage('pus-hide-ingredients',['#ingredients']); 1733 | loadHideTextFieldFromStorage('pus-hide-origin-product',['[id^="origin_"]','label[for^="origin_"]']); 1734 | loadHideTextFieldFromStorage('pus-hide-substances',['label[for="allergens"]','label[for="allergens"]'],true); 1735 | loadHideTextFieldFromStorage('pus-hide-traces',['label[for="traces"]','label[for="traces"]'],true); 1736 | loadHideTextFieldFromStorage('pus-hide-origin-ingredients',['label[for="origins"]','label[for="origins"]'],true); 1737 | loadHideTextFieldFromStorage('pus-hide-nutrition',['#nutrition']); 1738 | loadHideTextFieldFromStorage('pus-hide-packaging',['#packaging_section']); 1739 | }); 1740 | 1741 | } 1742 | function loadHideTextFieldFromStorage(checkboxId, hideFieldsIds, hasTags = false){ 1743 | if(getLocalStorage(checkboxId) === "checked"){ 1744 | toggleHideField(hideFieldsIds,hasTags); 1745 | } 1746 | } 1747 | // END OF Hide Text Fields 1748 | 1749 | 1750 | function toggleIngredientsMode() { 1751 | // 1752 | log("Ingredients mode"); 1753 | $('.example, .note').hide(); 1754 | //$("div").attr("style", "padding-top: 0.1rem!important; padding-bottom: 0.1rem!important; margin-top: 0.1rem!important; margin-bottom: 0.1rem!important"); 1755 | $("#main_column, #main_column label, #main_column input").attr("style", "padding-top: 0.1rem!important; padding-bottom: 0.1rem!important; margin-top: 0.1rem!important; margin-bottom: 0.1rem!important"); 1756 | $(".upload_image_div").attr("style", "display:none !important"); 1757 | $("#top-bar").hide(); 1758 | $(".medium-4").hide(); 1759 | $(".sidebar").hide(); 1760 | $("#main_column > div > div > div, #donate_banner, h1, #barcode_paragraph").hide(); 1761 | $("#label_new_code, #new_code, #obsolete, label[for='obsolete'], #obsolete_since_date, label[for='obsolete_since_date']").hide(); 1762 | $("#warning_3rd_party_content, #licence_accept").hide(); 1763 | $("#manage_images_accordion").hide(); 1764 | 1765 | $(".img_input, .upload_image_div, #imgupload_front_fr").hide(); 1766 | 1767 | $("#generic_name_fr, label[for='generic_name_fr']").hide(); 1768 | 1769 | // From "Product caracteristics until Ingredient 1770 | $("#quantity, label[for='quantity']").hide(); 1771 | $("div.fieldset:nth-child(16) > tags:nth-child(8), #packaging, label[for='packaging']").hide(); 1772 | $("div.fieldset:nth-child(16) > tags:nth-child(13), #brands, label[for='brands']").hide(); 1773 | $("div.fieldset:nth-child(16) > tags.tagify:nth-child(17), #categories, label[for='categories']").hide(); 1774 | $("div.fieldset:nth-child(16) > tags.tagify:nth-child(22), #labels, label[for='labels']").hide(); 1775 | $("div.fieldset:nth-child(16) > tags.tagify:nth-child(27), #manufacturing_places, label[for='manufacturing_places']").hide(); 1776 | $("div.fieldset:nth-child(16) > tags.tagify:nth-child(31), #emb_codes, label[for='emb_codes']").hide(); 1777 | $("#link, label[for='link']").hide(); 1778 | $("#expiration_date, label[for='expiration_date']").hide(); 1779 | $("div.fieldset:nth-child(16) > tags.tagify:nth-child(41), #purchase_places, label[for='purchase_places']").hide(); 1780 | $("div.fieldset:nth-child(16) > tags.tagify:nth-child(45), #stores, label[for='stores']").hide(); 1781 | $("div.fieldset:nth-child(16) > tags.tagify:nth-child(49), #countries, label[for='countries']").hide(); 1782 | $("div.fieldset:nth-child(16) > tags.tagify:nth-child(53), #environment_impact_level, label[for='environment_impact_level']").hide(); 1783 | 1784 | $("#check").hide(); 1785 | 1786 | // History and footer 1787 | $("#history, #history_list").hide(); 1788 | $("footer").hide(); 1789 | 1790 | } 1791 | 1792 | 1793 | 1794 | /** 1795 | * Hide/show example text below editing fields, 1796 | * and store the setting from the popup checkbox in local storage. 1797 | */ 1798 | function toggleDFMode() { 1799 | 1800 | // read setting from local storage 1801 | log("toggleDFMode() > DFMode: " + getLocalStorage("pus-dist-free")); 1802 | if(getLocalStorage("pus-dist-free") === "checked") { 1803 | $('#pus-dist-free').removeAttr('checked'); // set checkbox state 1804 | $('#offNav').hide(); 1805 | $('#prodNav').css("margin-top", "0px"); 1806 | } 1807 | 1808 | // hide/unhide field helpers on toggling the checkbox 1809 | $('#pus-dist-free').change(function() { 1810 | if(this.checked) { 1811 | localStorage.setItem('pus-dist-free', "checked"); 1812 | log("toggleDFMode() > DFMode on"); 1813 | $('#offNav').hide(); 1814 | $('#prodNav').css("margin-top", "0px"); 1815 | } 1816 | else { 1817 | localStorage.setItem('pus-dist-free', "unchecked"); 1818 | log("toggleDFMode() > DFMode off"); 1819 | $('#offNav').show(); 1820 | $('#prodNav').css("margin-top", "82px"); 1821 | } 1822 | }); 1823 | } 1824 | 1825 | 1826 | 1827 | /** 1828 | * Hide/show example text below editing fields, 1829 | * and store the setting from the popup checkbox in local storage. 1830 | */ 1831 | function toggleHelpers() { 1832 | 1833 | // read setting from local storage 1834 | log("toggleHelpers() > Helpers: " + getLocalStorage("pus-helpers")); 1835 | if(getLocalStorage("pus-helpers") === "unchecked") { 1836 | $('#pus-helpers').removeAttr('checked'); // set checkbox state 1837 | $('.note').hide(); 1838 | $('.example').hide(); 1839 | } 1840 | 1841 | // hide/unhide field helpers on toggling the checkbox 1842 | $('#pus-helpers').change(function() { 1843 | if(this.checked) { 1844 | localStorage.setItem('pus-helpers', "checked"); 1845 | log("toggleHelpers() > Show helpers"); 1846 | $('.note').show(); 1847 | $('.example').show(); 1848 | } 1849 | else { 1850 | localStorage.setItem('pus-helpers', "unchecked"); 1851 | log("toggleHelpers() > Hide helpers"); 1852 | $('.note').hide(); 1853 | $('.example').hide(); 1854 | } 1855 | }); 1856 | } 1857 | 1858 | 1859 | /** 1860 | * Optionally set the ingredients box font to monospace, 1861 | * to more easily see OCR errors like "com" vs "corn", uppercase "I" vs lowercase "l", etc. 1862 | * and store the setting from the popup checkbox in local storage. 1863 | */ 1864 | function toggleIngredientsMonospace() { 1865 | 1866 | // read setting from local storage 1867 | log("toggleIngredientsMonospace() > Monospace: " + getLocalStorage("pus-ingredients-font")); 1868 | if(getLocalStorage("pus-ingredients-font") === "monospace") { 1869 | $('#pus-ingredients-font').prop("checked", true); // set checkbox state 1870 | $("textarea[id^='ingredients_text_']").addClass("monospace"); // edit view 1871 | $("div.wrap_ingr > textarea.ingr").addClass("monospace"); // list view 1872 | } 1873 | 1874 | // change the textarea font on toggling the checkbox 1875 | $('#pus-ingredients-font').change(function() { 1876 | if(this.checked) { 1877 | localStorage.setItem('pus-ingredients-font', "monospace"); 1878 | log("toggleIngredientsMonospace() > monospace font"); 1879 | $("textarea[id^='ingredients_text_']").addClass("monospace"); // edit view 1880 | $("div.wrap_ingr > textarea.ingr").addClass("monospace"); // list view 1881 | } 1882 | else { 1883 | localStorage.setItem('pus-ingredients-font', "default"); 1884 | log("toggleIngredientsMonospace() > default font"); 1885 | $("textarea[id^='ingredients_text_']").removeClass("monospace"); // edit view 1886 | $("div.wrap_ingr > textarea.ingr").removeClass("monospace"); // list view 1887 | } 1888 | 1889 | }); 1890 | } 1891 | 1892 | function toggleAlwaysShowBarcodes(){ 1893 | if(getLocalStorage("pus-always-show-barcode") === "always"){ 1894 | $('#pus-always-show-barcode').prop("checked", true); 1895 | } 1896 | 1897 | $('#pus-always-show-barcode').change(function() { 1898 | if(this.checked){ 1899 | localStorage.setItem('pus-always-show-barcode', "always"); 1900 | }else{ 1901 | localStorage.setItem('pus-always-show-barcode', "never"); 1902 | } 1903 | toggleListBarcodes(); 1904 | }); 1905 | } 1906 | 1907 | function loadAlwaysShowBarcodesFromStorage(){ 1908 | $( window ).on( "load", function() { 1909 | if(getLocalStorage("pus-always-show-barcode") === "always"){ 1910 | toggleListBarcodes(); 1911 | } 1912 | }); 1913 | } 1914 | 1915 | 1916 | /** 1917 | * Show/hide a graphical barcode on the product view 1918 | */ 1919 | function showSingleBarcode(code) { 1920 | if ($("#barcode_draw").length) { return; } 1921 | 1922 | $('').insertAfter('#barcode_paragraph'); 1923 | 1924 | let barcode_format = 'CODE128'; 1925 | switch (code.length) { 1926 | case 13: 1927 | barcode_format = 'EAN13'; 1928 | break; 1929 | case 12: 1930 | barcode_format = 'UPC'; 1931 | break; 1932 | case 8: 1933 | barcode_format = 'EAN8'; 1934 | break; 1935 | } 1936 | 1937 | JsBarcode("#barcode_draw", code, { 1938 | format: barcode_format, 1939 | lineColor: "black", 1940 | width: 3, 1941 | height: 60, 1942 | displayValue: true, 1943 | }); 1944 | } 1945 | 1946 | function hideSingleBarcode(code) { 1947 | $("#barcode_draw").remove(); 1948 | } 1949 | 1950 | function toggleSingleBarcode(code) { 1951 | if ($("#barcode_draw").length) { 1952 | hideSingleBarcode(code); 1953 | } else { 1954 | showSingleBarcode(code); 1955 | } 1956 | } 1957 | 1958 | 1959 | /** 1960 | * Show/hide graphical barcodes on the list view 1961 | */ 1962 | function toggleListBarcodes() { 1963 | if ($("svg.list_barcode").length) { 1964 | hideListBarcodes(); 1965 | } else { 1966 | showListBarcodes(); 1967 | } 1968 | } 1969 | 1970 | function showListBarcodes() { 1971 | 1972 | $("ul[id^='products_'].search_results li[data-code]").each(function(index, element) { 1973 | let code = $(this).attr('data-code'); 1974 | if ($("#barcode_draw_" + code).length) { return; } 1975 | 1976 | $('').insertBefore( $('a.list_product_a', this) ); 1977 | 1978 | let barcode_format = 'CODE128'; 1979 | 1980 | switch (code.length) { 1981 | case 13: 1982 | barcode_format = 'EAN13'; 1983 | break; 1984 | case 12: 1985 | barcode_format = 'UPC'; 1986 | break; 1987 | case 8: 1988 | barcode_format = 'EAN8'; 1989 | break; 1990 | } 1991 | 1992 | try { 1993 | JsBarcode("#barcode_draw_" + code, code, { 1994 | format: barcode_format, 1995 | flat: true, 1996 | fontSize: 10, 1997 | lineColor: "black", 1998 | width: 1, 1999 | height: 40, 2000 | displayValue: true, 2001 | }); 2002 | }catch(error){ 2003 | console.error(error); 2004 | } 2005 | 2006 | 2007 | $('a.list_product_a', this).addClass('with_barcode'); 2008 | }); 2009 | } 2010 | 2011 | function hideListBarcodes() { 2012 | $("svg.list_barcode").remove(); 2013 | $('ul[id^="products_"].search_results .with_barcode').removeClass('with_barcode'); 2014 | } 2015 | 2016 | //shows HungerGames logo, rotate buttons 2017 | function showListButtons(){ 2018 | let languageCode = getSubdomainLanguageCode(); 2019 | 2020 | $("ul[id^='products_'].search_results li[data-code]").each(function(index, element) { 2021 | let barcode = $(this).attr('data-code'); 2022 | $(this).append('image_search'); 2023 | $(this).append('redo'); 2024 | $(this).append('rotate_right'); 2025 | $(this).append('redo'); 2026 | 2027 | var image_reference = $(".list_product_img", $(this)); 2028 | $(".list_rotate_image_270",$(this)).on("click", function(){ 2029 | getFrontImagesToRotate(270,barcode,languageCode); 2030 | image_reference.css('transform', 'rotate(270deg)'); 2031 | }); 2032 | 2033 | $(".list_rotate_image_180",$(this)).on("click", function(){ 2034 | getFrontImagesToRotate(180,barcode,languageCode); 2035 | image_reference.css('transform', 'rotate(180deg)'); 2036 | }); 2037 | 2038 | $(".list_rotate_image_90",$(this)).on("click", function(){ 2039 | getFrontImagesToRotate(90,barcode,languageCode); 2040 | image_reference.css('transform', 'rotate(90deg)'); 2041 | }); 2042 | }); 2043 | } 2044 | 2045 | //if 'ru-en'->ru while $("html").attr('lang'); returns en 2046 | function getSubdomainLanguageCode(){ 2047 | var subdomain = window.location.href.split('.')[0].split('//')[1]; 2048 | if(subdomain === 'world'){ return 'en';} 2049 | if(subdomain.length === 2){ return subdomain;} 2050 | 2051 | return subdomain.split('-')[0]; 2052 | } 2053 | 2054 | /*gets all the front_lc images available and then compares it to the subdomain. 2055 | For example if you are on ru.openfoodfacts and a product only has front_en then that picture will be rotated 2056 | instead of creating a new rotated front_ru */ 2057 | function getFrontImagesToRotate(angle,barcode,languageCode){ 2058 | var _productUrl = "/api/v2/product/" + barcode + ".json?fields=images"; 2059 | $.getJSON(_productUrl,function(productData){ 2060 | let productImages = productData.product.images; 2061 | var frontImages = []; 2062 | if(productImages){ 2063 | $.each(productImages,function(key,value){ 2064 | let startsWithFront = key.toString().startsWith('front'); 2065 | if(startsWithFront){ 2066 | frontImages.push(key); 2067 | } 2068 | }); 2069 | if(frontImages.length>0){ 2070 | let includesLanguageCode = frontImages.includes("front_"+languageCode); 2071 | var front_lc = frontImages[0]; 2072 | 2073 | if(includesLanguageCode){ 2074 | front_lc = "front_"+languageCode; 2075 | } 2076 | 2077 | let image_id = productImages[front_lc].imgid; 2078 | //let angle = productImages[front_lc].angle; 2079 | rotateImage(angle,barcode,front_lc,image_id); 2080 | } 2081 | 2082 | } 2083 | }); 2084 | } 2085 | 2086 | function rotateImage(angle,barcode,front_lc,image_id){ 2087 | var _url = "/cgi/product_image_crop.pl?code=" + barcode + "&id="+front_lc+"&imgid="+image_id+"&angle="+angle; 2088 | $.getJSON(_url, function(data) { 2089 | log("rotate status:" +data.status); 2090 | }); 2091 | } 2092 | 2093 | /** 2094 | * The product list view has no easy way to get the barcode for each entry, 2095 | * so detect them from the link, and add an attribute to the LI tag recording the barcode for later use. 2096 | */ 2097 | function addCodesToProductList() { 2098 | //log("in addCodesToProductList()"); 2099 | $("ul[id^='products_'].search_results li").each(function() { 2100 | //log(this); 2101 | let product_url = $("a.list_product_a", this).attr('href'); // find URL within "this" 2102 | let product_code = product_url.match(/\/([0-9]+)(\/|$)/); // find a number surrounded by slashes 2103 | if (product_code && product_code[1]) { 2104 | $(this).attr('data-code', product_code[1]); 2105 | } 2106 | }); 2107 | } 2108 | 2109 | 2110 | /** 2111 | * Get an array of barcodes for the current list view. 2112 | * Read barcodes out of data attributes added by addCodesToProductList() 2113 | * @return {Array} 2114 | */ 2115 | /* 2116 | function getCodesFromProductList() { 2117 | let product_codes = new Array(); 2118 | $("ul.products li").each(function() { 2119 | product_codes.push($(this).attr('data-code')); 2120 | }); 2121 | return product_codes; 2122 | } 2123 | */ 2124 | 2125 | 2126 | // *** 2127 | // * Flag this version 2128 | // * 2129 | function flagThisRevision() { 2130 | // Extract contributor of the current version from /contributor/jaeulitt => jaeulitt 2131 | $('.rev_contributor').attr('href') != undefined ? 2132 | version_user = $('.rev_contributor').attr('href').match(/contributor\/(.*)/)[1]: 2133 | version_user = ""; 2134 | // Extract revision number from URL: 2135 | // https://us.openfoodfacts.org/product/0744473477111/coconut-milk-non-dairy-frozen-dessert-vanilla-bean-so-delicious-dairy-free?rev=8 2136 | var rev = getURLParam("rev"); 2137 | if (rev !== null) { 2138 | version_date = $("#rev_summary > p > time > time").attr("datetime"); 2139 | log("version_date: "); log(version_date); 2140 | flagRevision(rev); 2141 | } 2142 | else { 2143 | var _url = "/api/v0/product/" + code + ".json"; 2144 | $.getJSON(_url, function(data) { 2145 | rev = data.product.rev; 2146 | log("rev: "); log(rev); 2147 | version_user = data.product.last_editor; 2148 | log("version_user: "); log(version_user); 2149 | var last_modified_t = new Date(data.product.last_modified_t*1000); 2150 | version_date = last_modified_t.toISOString(); 2151 | log("version_date: "); log(version_date); 2152 | flagRevision(rev); 2153 | }); 2154 | } 2155 | } 2156 | 2157 | 2158 | // *** 2159 | // * Flag revision 2160 | // * 2161 | function flagRevision(rev) { 2162 | // Get connected user id 2163 | var user_name = getConnectedUserID(); 2164 | 2165 | // Submit data to a Google Spreadsheet, see: 2166 | // * https://gist.github.com/mhawksey/1276293 2167 | // * https://mashe.hawksey.info/2014/07/google-sheets-as-a-database-insert-with-apps-script-using-postget-methods-with-ajax-example/ 2168 | // * https://medium.com/@dmccoy/how-to-submit-an-html-form-to-google-sheets-without-google-forms-b833952cc175 2169 | // https://script.google.com/macros/s/AKfycbwi9tIOPc7zh2NggDuq8geTSZqdZ470unBWUi4KV4AwYzCTNO8/exec?code=123&issue=fhkshf 2170 | // Debug CORS: https://www.test-cors.org/ 2171 | // CORS proxies: 2172 | // * https://crossorigin.me/ => GET only // 2020-04-10: site down? 2173 | // * https://cors.io? => sometimes down (3 days after first tries); can be installed on Heroku 2174 | // * https://cors-anywhere.herokuapp.com/ => ok 2175 | var googleScriptURL = corsProxyURL+"https://script.google.com/macros/s/AKfycbwi9tIOPc7zh2NggDuq8geTSZqdZ470unBWUi4KV4AwYzCTNO8/exec"; 2176 | var flagWindow = 2177 | '
    ' + 2178 | '
    ' + 2179 | '' + 2180 | '' + 2198 | '' + 2199 | '' + 2200 | //'' + 2201 | //'' + 2202 | '' + 2203 | '' + 2204 | '' + 2205 | '' + 2206 | '' + 2207 | '' + 2208 | '' + 2209 | '
    ' + 2210 | '
    ' + 2211 | '
    '; 2212 | showPowerUserInfo(flagWindow); // open a new window 2213 | 2214 | const form = document.forms['flag_form']; 2215 | log(form); 2216 | form.addEventListener('submit', e => { 2217 | log("Submited rev "+rev); 2218 | e.preventDefault(); // Do not submit the form 2219 | fetch(googleScriptURL, { 2220 | method: 'POST', 2221 | mode: 'cors', 2222 | body: new FormData(form) 2223 | }) 2224 | .then(function(response) { 2225 | log('Success!', response); 2226 | var spreadsheetURL = 'https://docs.google.com/spreadsheets/d/1DE85Or0QiYwIXcG4vSVZyFSLMKvmJqOXM5ooJzxZr6Y/'; 2227 | $("#flag_result").append('

    ' + 2228 | '✅ Version ' + 2229 | '' + 2230 | 'flagged.

    '); 2231 | return;}) 2232 | .catch(error => console.error('Error!', error.message)); 2233 | }); 2234 | } 2235 | 2236 | 2237 | 2238 | 2239 | // https://fr.openfoodfacts.org/etat/marques-a-completer/code/506036745xxxx&json=1 2240 | function getNumberOfProductsWithSimilardCodeAndWithoutBrand(codeToCheck) { 2241 | // 2242 | 2243 | } 2244 | 2245 | 2246 | 2247 | /** 2248 | * Display the quality tags in "product issues" section (#issues id) 2249 | * Examples: 2250 | * * Quality error tags: No quality errors 2251 | * * Quality warnings tags: en:ecoscore-origins-of-ingredients-origins-are-100-percent-unknown ◼ en:ecoscore-packaging-packaging-data-missing 2252 | * 2253 | * See also: https://github.com/openfoodfacts/openfoodfacts-server/issues/7718 2254 | * 2255 | * @returns none 2256 | */ 2257 | function addQualityTags() { 2258 | // TODO: use fetch instead of $.getJSON (faster + no dependencies) 2259 | $.getJSON(apiProductURL, function(data) { 2260 | var qualityErrorsTagsArray = data.product.data_quality_errors_tags; 2261 | log("addQualityTags() > qualityErrorsTagsArray: "); 2262 | log(qualityErrorsTagsArray); 2263 | var list = (qualityErrorsTagsArray.length === 0 ? 2264 | ('No quality errors') : 2265 | //('' + qualityErrorsTagsArray.join(' ◼ ') + '') 2266 | ('') 2267 | ); 2268 | $("#issues").append('
  • Quality error tags: ' + list + '
  • '); 2269 | 2270 | var qualityWarningsTagsArray = data.product.data_quality_warnings_tags; 2271 | log("addQualityTags() > qualityWarningsTagsArray: "); 2272 | log(qualityWarningsTagsArray); 2273 | //var list = '
    • ' + arr.join('
    • ') + '
    '; 2274 | list = (qualityWarningsTagsArray.length === 0 ? 2275 | ('No quality warnings') : 2276 | ('' + qualityWarningsTagsArray.join(' ◼ ') + '')); 2277 | $("#issues").append('
  • Quality warnings tags: ' + list + '
  • '); 2278 | }); 2279 | } 2280 | 2281 | 2282 | /** 2283 | * Display the "to be completed" state tags in "product issues" section (#issues id) 2284 | * Examples: "To be completed (from "State tags"): ingredients ◼ characteristics ◼ categories ◼ packaging ◼ quantity ◼ photos to be validated 2285 | * 2286 | * See also: https://github.com/openfoodfacts/openfoodfacts-server/issues/7718 2287 | * 2288 | * @returns none 2289 | */ 2290 | function addStateTags() { // TODO: merge with addQualityTags function? 2291 | // TODO: use fetch instead of $.getJSON (faster + no dependencies) 2292 | $.getJSON(apiProductURL, function(data) { 2293 | var stateTagsArray = data.product.states_tags; 2294 | log("addStateTags() > stateTagsArray: "); 2295 | log(stateTagsArray); 2296 | //var list = '
    • ' + arr.join('
    • ') + '
    '; 2297 | var filteredStateTagsArray = keepMatching(stateTagsArray, /(.*)to-be(.*)/); 2298 | var finalArray = replaceInsideArray(filteredStateTagsArray, /en\:/, ''); 2299 | finalArray = replaceInsideArray(finalArray, /to-be-completed/, ''); 2300 | finalArray = replaceInsideArray(finalArray, /\-/g, ' '); 2301 | log(finalArray); 2302 | var list = stateTagsArray.join(' ◼ '); 2303 | $("#issues").append('
  • To be completed (from "State tags"): ' + list + 2304 | ' ' + 2305 | '
  • '); 2306 | }); 2307 | } 2308 | 2309 | 2310 | /** 2311 | * Display the number and the link to similar named products without a category 2312 | * 2313 | * @returns none 2314 | */ 2315 | function isNbOfSimilarNamedProductsWithoutACategory() { 2316 | // Get URL 2317 | var url = getSimilarlyNamedProductsWithoutCategorySearchURL(); 2318 | log("isNbOfSimilarNamedProductsWithoutACategory() > url: " + url); 2319 | $.getJSON(url + "&json=1", function(data) { 2320 | var nbOfSimilarNamedProductsWithoutACategory = data.count; 2321 | log("isNbOfSimilarNamedProductsWithoutACategory() > nbOfSimilarNamedProductsWithoutACategory: " + nbOfSimilarNamedProductsWithoutACategory); 2322 | $("#going-further").append('
  • ' + nbOfSimilarNamedProductsWithoutACategory + 2325 | ' products with a similar name but without a category' + 2326 | '
  • '); 2327 | }); 2328 | } 2329 | 2330 | 2331 | /** 2332 | * Build search URL that finds products with a similar name, without category; example: 2333 | * https://world.openfoodfacts.org/cgi/search.pl?search_terms=beef%20jerky&tagtype_0=states&tag_contains_0=contains&tag_0=categories%20to%20be%20completed&sort_by=unique_scans_n 2334 | * 2335 | * @returns {String} - Returns an URL 2336 | */ 2337 | function getSimilarlyNamedProductsWithoutCategorySearchURL() { 2338 | var productName, similarProductsSearchURL; 2339 | if (pageType !== "product view") { // script fail if productName below is undefined 2340 | return; 2341 | } 2342 | // The productName below sometimes is undefined; TODO: get it with API? https://world.openfoodfacts.org/api/v0/product/3222475464430.json&fields=product_name 2343 | productName = $('h1[property="food:name"]').html().match(/(.*?)(( - .*)|$)/)[1]; 2344 | similarProductsSearchURL = encodeURI( 2345 | document.location.protocol + "//" + document.location.host + 2346 | "/cgi/search.pl?search_terms=" + productName + 2347 | "&tagtype_0=states&tag_contains_0=contains&tag_0=categories to be completed&sort_by=unique_scans_n"); 2348 | log("getSimilarlyNamedProductsWithoutCategorySearchURL() > productName: "+productName); 2349 | log("getSimilarlyNamedProductsWithoutCategorySearchURL() > similarProductsSearchURL: "+similarProductsSearchURL); 2350 | return similarProductsSearchURL; 2351 | } 2352 | 2353 | 2354 | /** 2355 | * Read a given URL parameter 2356 | * https://stackoverflow.com/questions/19491336/get-url-parameter-jquery-or-how-to-get-query-string-values-in-js 2357 | * 2358 | * @param {String} name - paramater name; ex. "code" in http://example.org/index?code=839370889 2359 | * @returns {String} Return either null if param doesn't exist, either content of the param 2360 | */ 2361 | function getURLParam(name) { 2362 | var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href); 2363 | if (results === null) { 2364 | return null; 2365 | } 2366 | return decodeURI(results[1]) || 0; 2367 | } 2368 | 2369 | 2370 | /** 2371 | * isPageType: Detects which kind of page has been loaded 2372 | * See also https://github.com/openfoodfacts/openfoodfacts-server/pull/4533/files 2373 | * 2374 | * @returns {String} - Type of page: api|saved-product page|edit|list|search form|product view|error page 2375 | */ 2376 | function isPageType() { 2377 | // Detect API page. Example: https://world.openfoodfacts.org/api/v0/product/3599741003380.json 2378 | var regex_api = RegExp('api/v0/'); 2379 | if(regex_api.test(document.URL) === true) return "api"; 2380 | 2381 | // Detect producers platform 2382 | var regex_pro = RegExp('\.pro\.open'); 2383 | if(regex_pro.test(document.URL) === true) proPlatform = true; 2384 | 2385 | // Detect "edit" mode. 2386 | var regex = RegExp('product\\.pl'); 2387 | if(regex.test(document.URL) === true) { 2388 | if ($("body").hasClass("error_page")) return "error page"; // perhaps a more specific test for product-not-found? 2389 | if (!$("#sorted_langs").length) return "saved-product page"; // Detect "Changes saved." page 2390 | else return "edit"; 2391 | } 2392 | 2393 | // Detect other error pages 2394 | if ($("body").hasClass("error_page")) return "error page"; 2395 | 2396 | // Detect page containing a list of products (home page, search results...) 2397 | if ($("body").hasClass("products_page") || $("body").hasClass("list_of_products_page")) return "list"; 2398 | 2399 | // Detect search form 2400 | var regex_search = RegExp('cgi/search.pl$'); 2401 | if(regex_search.test(document.URL) === true) return "search form"; 2402 | 2403 | // Detect recentchanges 2404 | if ($("body").hasClass("recent_changes_page")) return "recent changes"; 2405 | 2406 | //Detect if in the list of ingredients 2407 | regex_search = RegExp('ingredients'); 2408 | if(regex_search.test(document.URL) === true) return "ingredients"; 2409 | 2410 | // Finally, it's a product view 2411 | if ($("body").hasClass("product_page")) return "product view"; 2412 | } 2413 | 2414 | 2415 | 2416 | /** 2417 | * detectLanguages: detects which kind of page has been loaded 2418 | * 2419 | * @returns {Array} - array of all languages available for a product; ex. ["de","fr","en"] 2420 | */ 2421 | function detectLanguages() { 2422 | log("detectLanguages() > detectLanguages: "); 2423 | var array = $("#sorted_langs").val().split(","); 2424 | log(array); 2425 | return array; 2426 | } 2427 | 2428 | 2429 | /** 2430 | * getIngredientsFromGCV: Get ingredients via Google Cloud Vision 2431 | * 2432 | * @param {String} code - product code; ex. 7613035748699 2433 | * @param {String} lc - language; ex. "fr" 2434 | */ 2435 | function getIngredientsFromGCV(code,lc) { 2436 | // https://world.openfoodfacts.org/cgi/ingredients.pl?code=7613035748699&id=ingredients_fr&process_image=1&ocr_engine=google_cloud_vision 2437 | var ingredientsURL = document.location.protocol + "//" + document.location.host + 2438 | "/cgi/ingredients.pl?code=" + code + 2439 | "&id=ingredients_" + lc + "&process_image=1&ocr_engine=google_cloud_vision"; 2440 | log("getIngredientsFromGCV(code,lc) > ingredientsURL: "+ingredientsURL); 2441 | $.getJSON(ingredientsURL, function(json) { 2442 | $("#ingredientFromGCV").append(json.ingredients_text_from_image); 2443 | }); 2444 | } 2445 | 2446 | 2447 | /** 2448 | * keepMatching: keep only matching strings of an array 2449 | * @example finalArray = keepMatching(["tomatoes","eggs"], /eggs/); 2450 | * // => ["eggs"] 2451 | * 2452 | * @param {Array} originalArray - array to check 2453 | * @param {String} regex - regex pattern 2454 | * @returns {Array} - new array 2455 | */ 2456 | function keepMatching(originalArray, regex) { 2457 | var j = 0; 2458 | while (j < originalArray.length) { 2459 | if (regex.test(originalArray[j]) === false) { 2460 | originalArray.splice(j, 1); // delete value at position j 2461 | } else { 2462 | j++; 2463 | } 2464 | } 2465 | return originalArray; 2466 | } 2467 | 2468 | 2469 | /** 2470 | * replaceInsideArray: replace some content by another in each string of an array 2471 | * @example finalArray = replaceInsideArray(["en:tomatoes","en:eggs"], /en:/, ''); 2472 | * // => ["tomatoes","eggs"] 2473 | * 2474 | * @param {Array} originalArray - array to check 2475 | * @param {String} regex - regex pattern 2476 | * @param {string} target - target content 2477 | * @returns {Array} - new array 2478 | */ 2479 | function replaceInsideArray(originalArray, regex, target) { 2480 | var j = 0; 2481 | while (j < originalArray.length) { 2482 | originalArray[j] = originalArray[j].replace(regex, target); 2483 | if (originalArray[j] === "") { 2484 | originalArray.splice(j, 1); // delete value at position j 2485 | } else { 2486 | j++; 2487 | } 2488 | } 2489 | return originalArray; 2490 | } 2491 | 2492 | 2493 | /** 2494 | * getLocalStorage 2495 | * 2496 | * @param {String} key - key to check 2497 | * @returns {String} 2498 | */ 2499 | function getLocalStorage(key) { 2500 | var val = localStorage.getItem(key); 2501 | return val ? val:""; 2502 | } 2503 | 2504 | 2505 | /** 2506 | * getConnectedUserID: returns user id of the current connected user 2507 | * 2508 | * @param none 2509 | * @returns {String} user id; Example: "charlesnepote" 2510 | */ 2511 | function getConnectedUserID() { 2512 | // Extract connected user_id by reading charlesnepote 2513 | var user_name = $("#user_id").text(); 2514 | log("getConnectedUserID() > user_name: "); log(user_name); 2515 | return user_name; 2516 | } 2517 | 2518 | 2519 | 2520 | /** 2521 | * normalizeTagName: returns a normalized version of a tag 2522 | * 2523 | * @param {string} tagName: tag to normalize; eg. "Cereal bars", "Marque Repère", "Trader Joe's" 2524 | * @returns {String} normalized tagName; eg. "cereal-bars", "marque-repere", "trader-joe-s" 2525 | */ 2526 | function normalizeTagName(tagName) { 2527 | tagName = tagName.toLowerCase(); 2528 | tagName = tagName.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); // Accentued chars 2529 | tagName = tagName.replace(/[ '"&]/mg, "-"); // "Kellog's" => kellog-s 2530 | tagName = tagName.replace(/-+/mg, "-"); // "Elle & Vire" => "elle-vire" 2531 | log("normalizeTagName() - tagName: " + tagName); 2532 | return tagName; 2533 | } 2534 | 2535 | 2536 | /** 2537 | * productExists: check link status: 200 means the product exists, 404 means it doesn't 2538 | * 2539 | * @param {string} urlToCheck: url to check; example: "https://world.openbeautyfacts.org/product/8008343200233" 2540 | * @param {string} id: HTML id where to publish the result; example: "obfLinkStatus" 2541 | * @param {string} userName: userName if the web server need an authentication 2542 | * @param {string} passWord: password if the web server need an authentication 2543 | * @returns none 2544 | */ 2545 | function productExists(urlToCheck,id,userName,passWord){ 2546 | //log("productExists( "+urlToCheck+" )"); 2547 | $.ajax({ 2548 | url: urlToCheck, 2549 | type: "GET", 2550 | //xhrFields: { withCredentials: true }, 2551 | // username: userName, 2552 | // password: passWord, 2553 | headers: { // send auth headers, needed for .dev platform 2554 | "Authorization": "Basic " + btoa(userName + ":" + passWord) 2555 | }, 2556 | success: function(data, textStatus, xhr) { 2557 | log("productExists( "+urlToCheck+" ) > success - xhr.status: " + xhr.status); 2558 | if(xhr.status) $(id).text(xhr.status); 2559 | }, 2560 | statusCode: { 2561 | 404: function(xhr, textStatus) { 2562 | log( "productExists( "+urlToCheck+" ) > 404 > " + xhr.status); 2563 | if(xhr.status) $(id).text(xhr.status); 2564 | }, 2565 | 200: function(xhr, textStatus) { 2566 | log( "productExists( "+urlToCheck+" ) > 200 > " + xhr.status); 2567 | if(xhr.status) $(id).text(xhr.status); 2568 | } 2569 | } 2570 | }) 2571 | .always(function (xhr, textStatus) { 2572 | log("productExists( "+urlToCheck+" ) > always - xhr.status: " + xhr.status); 2573 | //log("productExists( "+urlToCheck+" ) > always - getAllResponseHeaders(): " + xhr.getAllResponseHeaders()); 2574 | if(xhr.status) $(id).text(xhr.status); 2575 | }); 2576 | } 2577 | 2578 | 2579 | /** 2580 | * Move products between sites 2581 | */ 2582 | function moveProductToSite(_code, newSite) { 2583 | if (/^(beauty|food|product|petfood)$/.test(newSite) !== true) { 2584 | log("moveProductToSite() > invalid site: " + newSite); 2585 | return false; 2586 | } 2587 | 2588 | if (!_code) { 2589 | log("moveProductToSite() > missing barcode"); 2590 | return false; 2591 | } 2592 | 2593 | var _url = encodeURI(document.location.protocol + "//" + document.location.host + 2594 | "/cgi/product_jqm.pl?type=edit&code=" + _code + "&product_type=" + newSite); 2595 | log("api call-> "+_url); 2596 | $("body").append('
    Moving
    '); 2597 | var _d = $.getJSON(_url, function() { 2598 | log("getJSONList(urlList) > Move to " + newSite ); 2599 | }) 2600 | .done(function(jqm2) { 2601 | log(jqm2.status_verbose); 2602 | log(jqm2); 2603 | if (jqm2.status == 1 || jqm2.status_verbose == 'not modified') { 2604 | $("#timed_alert_move_" + _code).html('Moved!'); 2605 | } else { 2606 | $("#timed_alert_move_" + _code).html('Failed!'); 2607 | $("#timed_alert_move_" + _code).addClass('failed'); 2608 | } 2609 | $("#timed_alert_move_" + _code).fadeOut(3000, function () { $(this).remove(); }); 2610 | }) 2611 | .fail(function() { 2612 | log("getJSONList(urlList) > fail"); 2613 | $("#timed_alert_move_" + _code).html('Failed!'); 2614 | $("#timed_alert_move_" + _code).addClass('failed'); 2615 | $("#timed_alert_move_" + _code).fadeOut(3000, function () { $(this).remove(); }); 2616 | }); 2617 | } 2618 | 2619 | 2620 | /* ***************************************************************************************** 2621 | * Edition context. Below are functions which are useful in edition mode. 2622 | */ 2623 | 2624 | /*** 2625 | * computeEnergy: 2626 | * The energy of a given product is computed based on INCO european regulation (see Annex XIV). 2627 | * The formula is: ((carb - polyols)*17) + (polyols * 10) + (proteins * 17) + (fat * 37) + (fiber * 8) + (alcohol * 29) 2628 | * or: ((carb - polyols)*4) + (polyols * 2.4) + (proteins * 4) + (fat * 9) + (fiber * 2) + (alcohol * 7) 2629 | * 2630 | * @param {string} servingSize: value of the servingSize 2631 | * @returns: none 2632 | */ 2633 | function computeEnergy() { 2634 | //log("computeEnergy"); 2635 | let fat = readAndNormalizeNutrient("nutriment_fat"); 2636 | let carb = readAndNormalizeNutrient("nutriment_carbohydrates"); 2637 | let proteins = readAndNormalizeNutrient("nutriment_proteins"); 2638 | let polyols = readAndNormalizeNutrient("nutriment_polyols"); 2639 | let fiber = readAndNormalizeNutrient("nutriment_fiber"); 2640 | let alcohol = readAndNormalizeNutrient("nutriment_alcohol"); 2641 | let computed_kj = ((carb - polyols)*17) + (polyols * 10) + (proteins * 17) + (fat * 37) + (fiber * 8) + (alcohol * 29); 2642 | computed_kj = computed_kj % 1 !== 0 ? computed_kj.toFixed(2) : computed_kj.toString(); 2643 | // Display computed KJ near "Energy (kJ)*" label 2644 | document.getElementById("computed_kj").innerText = "["+computed_kj+"]"; 2645 | } 2646 | 2647 | 2648 | 2649 | /*** 2650 | * readAndNormalizeNutrient: read nutrient from its field and remove "<" or "-" 2651 | * or return 0 to allow its use in computations. 2652 | * 2653 | * @param {string} nutrient: id of the nutrient 2654 | * @returns {int} value of the nutrient or 0 2655 | */ 2656 | function readAndNormalizeNutrient(nutrient) { 2657 | let nutrientValue = (document.getElementById(nutrient) || {}).value || "0"; 2658 | nutrientValue = (nutrientValue == "-") ? "0" : nutrientValue; 2659 | return parseFloat(nutrientValue.trim().replace("<", "")); 2660 | } 2661 | 2662 | 2663 | 2664 | /*** 2665 | * checkServingSize: check if serving size contains a right value; otherwise, highlight the field 2666 | * and add a interrogation mark associated with a tooltip. 2667 | * 2668 | * @param {string} servingSize: value of the servingSize 2669 | * @returns: none 2670 | */ 2671 | function checkServingSize(servingSize) { 2672 | log("checkServingSize - serving size val: " + servingSize); 2673 | const servingSizeElement = document.getElementById("serving_size"); 2674 | if (servingSize.length == 0) { 2675 | servingSizeElement.style.setProperty("background-color", "LightYellow", "important"); 2676 | document.getElementById("serving_size_help") != null ? document.getElementById("serving_size_help").remove() : false; 2677 | return; 2678 | } 2679 | const regex = /\d+(\.\d+)? ?(kg|g|dg|cg|mg|mcg|l|dl|cl|ml|fl|oz)(\W+.*)?$/gi; // Examples matching: 60 g, 12 oz, 20cl, 2 fl oz 2680 | if (regex.test(servingSize) === false) { 2681 | log("checkServingSize - serving size is not correct!"); 2682 | servingSizeElement.style.setProperty("background-color", "orange", "important"); 2683 | servingSizeElement.style.setProperty("display", "inline"); 2684 | if(document.getElementById("serving_size_help") == null) { 2685 | const servingSizeHelp = ' (?)
    '; 2686 | servingSizeElement.insertAdjacentHTML("afterend", servingSizeHelp); 2687 | document.getElementById("serving_size_help").style.setProperty("cursor", "pointer"); 2688 | } 2689 | } 2690 | else { 2691 | servingSizeElement.style.setProperty("background-color", "LightYellow", "important"); 2692 | document.getElementById("serving_size_help") != null ? document.getElementById("serving_size_help").remove() : false; 2693 | } 2694 | } 2695 | 2696 | 2697 | 2698 | /*** 2699 | * checkFiber 2700 | * 2701 | * @param {string} f: value of fiber 2702 | * @returns: none 2703 | */ 2704 | function checkFiber(f) { 2705 | log("fiber val: " + f); 2706 | if (f.length == 0) { 2707 | log("fiber empty!"); 2708 | $("#nutriment_fiber").css({"background-color": "orange"}); 2709 | if($('#fiber_help').length == 0) { 2710 | $("#nutriment_fiber").after(' (?) '); 2711 | $('#fiber_help').css('cursor', 'pointer'); 2712 | } 2713 | } 2714 | else { 2715 | $("#nutriment_fiber").css({"background-color": "LightYellow"}); 2716 | if($('#fiber_help').length != 0) { 2717 | $("#fiber_help").remove(); 2718 | } 2719 | } 2720 | } 2721 | 2722 | 2723 | 2724 | /*** 2725 | * reverseKJKcal 2726 | * 2727 | * @returns none 2728 | */ 2729 | function reverseKJKcal() { 2730 | log("reverseKJKcal()"); 2731 | // Read the values 2732 | let joules = document.getElementById('nutriment_energy-kj').value; 2733 | let calories = document.getElementById('nutriment_energy-kcal').value; 2734 | // Change the values 2735 | document.getElementById("nutriment_energy-kj").value = calories; 2736 | document.getElementById("nutriment_energy-kcal").value = joules; 2737 | // After change, check if kJ and Kcal are coherent 2738 | checkKJ(); 2739 | } 2740 | 2741 | 2742 | 2743 | /*** 2744 | * checkKJ 2745 | * 2746 | * @returns none 2747 | */ 2748 | function checkKJ() { 2749 | // If not already displayed, add the small icon to allow changing kJ to Kcal: ⇅ 2750 | if(!document.getElementById('kjtokcal')) { 2751 | const reverseIcon = ''; 2752 | document.getElementById("nutriment_energy-kj").insertAdjacentHTML("afterend", reverseIcon); 2753 | document.getElementById('kjtokcal').style.setProperty("cursor", "pointer"); 2754 | document.getElementById("kjtokcal").addEventListener("click", function() { 2755 | //document.getElementById('kjtokcal').click(function(){ 2756 | reverseKJKcal(); 2757 | }); 2758 | } 2759 | const j = document.getElementById('nutriment_energy-kj').value; 2760 | const c = document.getElementById('nutriment_energy-kcal').value; 2761 | log("checkKJ() - kJ: " + j + ", kcal: " + c); 2762 | // If either KJ or Kcal does not exist: compute the missing value 2763 | if (j == "" && c != "") { 2764 | const cj = (c * 4.2).toFixed(); 2765 | document.getElementById('kjtokcal').title = "Reverse the kj/kcal values -- kcal: " + c + "; computed kJ: ~" + cj; 2766 | } 2767 | if (c == "" && j != "") { 2768 | const cc = (j / 4.2).toFixed(); 2769 | document.getElementById('kjtokcal').title = "Reverse the kj/kcal values -- kJ: " + j + "; computed kcal: ~" + cc; 2770 | } 2771 | if (c != "" && j != "") { 2772 | const ratio = (j / c).toFixed(1); 2773 | document.getElementById("kjtokcal").title = "Reverse the kj/kcal values -- ratio Kj/kcal (should be 4.2): " + ratio; 2774 | (ratio >= 4.6 || ratio <= 4.0) ? 2775 | document.getElementById('kjtokcal').style.color = "red" : document.getElementById('kjtokcal').style.color = "black"; 2776 | } 2777 | // CAREFUL: all of this might be false if values are per serving!!!! 2778 | /* 2779 | if (parseInt(j) < parseInt(c)) { 2780 | log("kj < kcal: " + j + " < " + c); 2781 | $("#nutriment_energy-kj").css({"background-color": "orange"}); 2782 | $("#nutriment_energy-kcal").css({"background-color": "orange"}); 2783 | /*if($('#kjtokcal').length == 0) { 2784 | $("#nutriment_energy-kj").after(''); 2785 | } 2786 | $('#kjtokcal').css('cursor', 'pointer'); 2787 | $("#kjtokcal").click(function(){ 2788 | $("#nutriment_energy-kj").attr("value", c); 2789 | $("#nutriment_energy-kcal").attr("value", j); 2790 | kj = $("#nutriment_energy-kj").attr("value"); 2791 | kcal = $("#nutriment_energy-kcal").attr("value"); 2792 | checkKJ(kj, kcal); 2793 | });/* 2794 | } 2795 | log(typeof $("#nutriment_energy-kcal").attr("value") + ", " + typeof $("#nutriment_energy-kj").attr("value")); 2796 | if ($("#nutriment_energy-kcal").attr("value") === "" || (parseInt(j) > parseInt(c) && $("#nutriment_energy-kcal").attr("value") < 910)) { 2797 | log("ok"); 2798 | $("#nutriment_energy-kcal").css({"background-color": "LightYellow"}); 2799 | } 2800 | if ($("#nutriment_energy-kj").attr("value") === "" || (parseInt(j) > parseInt(c) && $("#nutriment_energy-kj").attr("value") < 3800)) { 2801 | $("#nutriment_energy-kj").css({"background-color": "LightYellow"}); 2802 | }/**/ 2803 | } 2804 | 2805 | 2806 | 2807 | /* ***************************************************************************************** 2808 | * Functions which are useful in any context 2809 | */ 2810 | 2811 | /*** 2812 | * Log things 2813 | * @param {string} id: HTML id where to publish the result; example: "obfLinkStatus" 2814 | * @returns none 2815 | */ 2816 | function log(thing) { 2817 | // Log data if "log" variable set to true, 2818 | // and add "[PUS]" to allow filtering in the browser console 2819 | if (log_to_console) { 2820 | console.log("[PUS]", thing); 2821 | } 2822 | } 2823 | 2824 | })(); 2825 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # power-user-script 2 | User script for your browser, to empower [Open Food Facts](https://world.openfoodfacts.org/) contribution. Power User Script is a kind of laboratory, to explore new features before they can get into Open Food Facts. 3 | 4 | Some features: 5 | * keyboard shortcuts to different pages: product edition (e), product JSON (a), view mode (v), etc. 6 | * styling improvements via CSS: barcode highlighted, fields highlighted, etc. 7 | * show/hide barcode 8 | * show/hide helpers 9 | * sidebar quick links: page translation, category translation, Recent Changes, Hunger Game... 10 | * inline edit of ingredients 11 | * information enhancements beside barcode number: links to Google search, Open Beauty Facts, etc. 12 | * information enhancements in the confirmation page: product issues, going further, etc. 13 | * recent changes filter (filter as you type) 14 | * etc. See complete list in the "changelog" or directly in JS code 15 | 16 | # Install 17 | To run userscripts it's best to have a script manager installed. Userscript managers are available as browser extensions: 18 | 19 | * Greasemonkey – works with Firefox - https://addons.mozilla.org/en-US/firefox/addon/greasemonkey/ 20 | * Tampermonkey – works with Chrome, Safari, Firefox and other browsers - http://tampermonkey.net/ 21 | 22 | Choose an appropriate manager and install it according to the requirements of your browser. 23 | 24 | Once your script manager is installed you can go to https://github.com/openfoodfacts/power-user-script/blob/master/OpenFoodFactsPower.user.js 25 | 26 | Just click on the Raw button and your script manager will ask you if you want to install the script. 27 | 28 | # Changelog 29 | ### 2024-12-20T11:15 30 | * Fix moving products to OPF, OBF, OPFF 31 | ### 2024-08-27T19:25 32 | * Exclude search.openfoodfacts.org 33 | ### 2024-06-19T14:18 34 | * Exclude prices.openfoodfacts.org 35 | * Fix kJ/kcal ratio to 4.2 instead of 4.4 36 | ### 2024-02-02T20:39 37 | * Product list: rotate images 38 | ### 2024-01-03T13:20 39 | * Search as you type to filter changes. Useful for many cases: 40 | * find **add**itions or **change**s 41 | * monitor changes for specific fields 42 | * monitor changes from specific users 43 | ### 2024-01-03T11:20 44 | * Products' list: add button to each product to open Hunger Game 45 | ### 2023-12-22T10:15 46 | * Add option to display barcodes by default 47 | ### 2023-09-15T22:59 48 | * Edit mode: compute and display energy in real-time 49 | ### 2023-09-13T23:00 50 | * Let panels use less space 51 | ### 2023-08-24T18:56 52 | * Add "History" anchor in the nav bar 53 | * Check serving size field 54 | * Colorize icon ⇅ when kJ/kcal values are not coherent (ratio is displayed inside ⇅ tooltip) 55 | * Small UI improvements (smaller fixed validation bar) 56 | * Better organization of the code; more code in vanilla JS (faster and less dependant from libraries) 57 | ### 2023-05-04T23:39 58 | * Button ⇅ to reverse kj/kcal 59 | * "No quality errors" is back 60 | ### 2023-03-20T12:38 61 | * Hack to prevent product opener regression in saved-product page 62 | ### 2022-12-22T15:30 63 | * Distraction free mode: Open Food Facts top bar is hidden. Option to check or uncheck (by default) in the settings. 64 | * The nutrition facts form has many improvements: now fits in one screen on a HD display at 100%. 65 | ### 2022-12-19T08:20 66 | * fix nutrition image size on Chrome and Safari 67 | ### 2022-12-08T08:20 68 | * Delete hunger games links which are now in Product Opener. 69 | * CSS tweaks: 70 | * nutrition facts table more condensed 71 | * products issues are well displayed 72 | ### 2022-10-19T15:11 73 | * Categories, brands and labels' facets: added Hunger Game deep link as a button right after the title 74 | * Edition: 75 | * Quick and dirty hack to control and manage kj/kcal inversion 76 | * Control fiber field 77 | * Remove "edit" button feature in list mode (added in Product Opener) 78 | * Fix some regressions due to Open Food Facts redesign of 2022-10 79 | ### 2021-12-06T13:55 80 | * Add a deep link to Hunger Game for brand on product's page 81 | ### 2021-10-20T16:31 82 | * Add "n" keyboard shortcut in list mode to reload the list without cache (&nocache=1 parameter) 83 | ### 2021-03-30T02:36 84 | * Add ->OPetFF button in list mode (to move products to Open Pet Food Facts), and improve error handling 85 | ### 2021-03-26T10:48 86 | * Add ->OPF button in list mode (to move products to Open Products Facts) 87 | * Recent changes link displays 100# instead of 900# 88 | ### 2021-03-25T18:18 89 | * Exclude https://analytics.openfoodfacts.org/ 90 | ### 2021-03-21T20:44 91 | * Fix product list view tweaks to work with new layout 92 | ### 2020-12-12T16:32 93 | * Exclude https://support.openffodfacts.org 94 | ### 2020-10-29T18:00 95 | * Add graphical barcodes to list view (shift-B) (Issue #26) 96 | ### 2020-10-17T08:30 97 | * Add option to set ingredient textareas to fixed width font, to make it easier to see bad OCR, such as when it confuses "m" and "rn" (e.g. corn), lowercase l/L and uppercase i/I, letter O with number 0, etc. 98 | ### 2020-10-15T08:35 99 | * minor fixes and code tidying 100 | ### 2020-10-14T11:53 101 | * fixes from @svensven (thanks!) 102 | * Categorization opportunities link 103 | * Add DuckDuckGo link for product barcode (near the barcode) 104 | ### 2020-06-26T16:33 105 | * Deep link to Hunger Game when the page is related to a category, label or brand 106 | * exclude wiki pages from script 107 | ### 2020-05-04T10:39 108 | * Modify link to hunger game 109 | * Nutrition facts picture takes all the place available: should work for every modern browser (CSS3) 110 | * very small update, the "a" key now opens the json page in a new window (instead of Alt+Shift+A) 111 | ### 2020-04-17T14:33 112 | * Confirmation page: quality errors and quality warnings displayed in red (or green when it's all right) 113 | * Google Link for product barcode (near the barcode) 114 | * Link to Open Pet Food Facts (near the barcode) 115 | * Firefox: Nutrition facts picture takes all the place available 116 | ### 2020-04-11T17:26 117 | * Add openbeautyfacts.org link and its status code (200 = the product exists; 404 it doesn't) 118 | * Add .pro.openffodfacts.org link (status code isn't working) 119 | * Add "ingredient mode" to simplify ingredients management => ("i" key in "edit" mode) 120 | * Developper: console.log messages more clear 121 | ### 2020-03-24TT11:12 122 | * Add a field to filter Recent Changes results (filter as you type) 123 | ### 2020-01-09T16:54 124 | * Add version date to flag feature 125 | ### 2019-12-16T17:27 126 | * Add Hunger Game link 127 | * Various fixes 128 | ### 2019-12-09T18:34 129 | * Change "?" menu position 130 | ### 2019-12-04T15:15 131 | * Edit mode: show/hide field help comments 132 | ### 2019-11-22T08:33 133 | * flagging improvement 134 | * allow flagging on page which is not a revision 135 | * add product_improvement 136 | * reorganize menu and add separators 137 | ### 2019-11-19T11:40 138 | * Detect pro platform + add product public URL 139 | ### 2019-11-18T16:54 140 | * Add quick links in the sidebar 141 | * Refactor help box 142 | ### 2019-11-04T09:33 143 | * change @updateURL to https://github.com/openfoodfacts/power-user-script/raw/master/OpenFoodFactsPower.user.js 144 | * comment code made for easier to read number of products because of https://github.com/openfoodfacts/openfoodfacts-server/issues/2474 145 | ### 2019-10-23T13:42 146 | * number of products easier to read (with separators depending on your locale); see: https://github.com/openfoodfacts/openfoodfacts-server/issues/2474 147 | ### 2019-09-12T16:45 148 | * initial publication on this current Github repo 149 | --------------------------------------------------------------------------------