From 60dde2a565e3df9e19b86008ab4c2a4f12c40e27 Mon Sep 17 00:00:00 2001 From: bcanini <canini.16@osu.edu> Date: Fri, 25 Jan 2019 11:23:10 -0500 Subject: [PATCH] new module - module filter --- composer.json | 1 + composer.lock | 60 ++- vendor/composer/installed.json | 57 +++ web/modules/module_filter/.gitignore | 1 + web/modules/module_filter/LICENSE.txt | 339 +++++++++++++ .../config/install/module_filter.settings.yml | 1 + .../module_filter/css/module_filter.css | 32 ++ .../css/module_filter.modules.css | 5 + .../css/module_filter.modules_tabs.css | 134 +++++ .../css/module_filter.update_status.css | 8 + web/modules/module_filter/js/jquery.winnow.js | 361 +++++++++++++ web/modules/module_filter/js/module_filter.js | 45 ++ .../module_filter/js/module_filter.modules.js | 174 +++++++ .../js/module_filter.modules_bare.js | 41 ++ .../js/module_filter.modules_tabs.js | 480 ++++++++++++++++++ .../js/module_filter.modules_uninstall.js | 56 ++ .../js/module_filter.permissions.js | 71 +++ .../js/module_filter.update_status.js | 101 ++++ .../module_filter/module_filter.info.yml | 13 + .../module_filter/module_filter.install | 22 + .../module_filter/module_filter.libraries.yml | 61 +++ .../module_filter.links.menu.yml | 6 + .../module_filter/module_filter.module | 202 ++++++++ .../module_filter.permissions.yml | 3 + .../module_filter/module_filter.routing.yml | 7 + .../module_filter/module_filter.services.yml | 5 + .../ModuleFilterUpdateController.php | 27 + .../src/Form/ModuleFilterSettingsForm.php | 62 +++ .../src/Form/ModuleFilterUpdateStatusForm.php | 78 +++ .../src/Routing/RouteSubscriber.php | 22 + .../system-modules-details.html.twig | 76 +++ 31 files changed, 2549 insertions(+), 2 deletions(-) create mode 100644 web/modules/module_filter/.gitignore create mode 100644 web/modules/module_filter/LICENSE.txt create mode 100644 web/modules/module_filter/config/install/module_filter.settings.yml create mode 100644 web/modules/module_filter/css/module_filter.css create mode 100644 web/modules/module_filter/css/module_filter.modules.css create mode 100644 web/modules/module_filter/css/module_filter.modules_tabs.css create mode 100644 web/modules/module_filter/css/module_filter.update_status.css create mode 100644 web/modules/module_filter/js/jquery.winnow.js create mode 100644 web/modules/module_filter/js/module_filter.js create mode 100644 web/modules/module_filter/js/module_filter.modules.js create mode 100644 web/modules/module_filter/js/module_filter.modules_bare.js create mode 100644 web/modules/module_filter/js/module_filter.modules_tabs.js create mode 100644 web/modules/module_filter/js/module_filter.modules_uninstall.js create mode 100644 web/modules/module_filter/js/module_filter.permissions.js create mode 100644 web/modules/module_filter/js/module_filter.update_status.js create mode 100644 web/modules/module_filter/module_filter.info.yml create mode 100644 web/modules/module_filter/module_filter.install create mode 100644 web/modules/module_filter/module_filter.libraries.yml create mode 100644 web/modules/module_filter/module_filter.links.menu.yml create mode 100644 web/modules/module_filter/module_filter.module create mode 100644 web/modules/module_filter/module_filter.permissions.yml create mode 100644 web/modules/module_filter/module_filter.routing.yml create mode 100644 web/modules/module_filter/module_filter.services.yml create mode 100644 web/modules/module_filter/src/Controller/ModuleFilterUpdateController.php create mode 100644 web/modules/module_filter/src/Form/ModuleFilterSettingsForm.php create mode 100644 web/modules/module_filter/src/Form/ModuleFilterUpdateStatusForm.php create mode 100644 web/modules/module_filter/src/Routing/RouteSubscriber.php create mode 100644 web/modules/module_filter/templates/system-modules-details.html.twig diff --git a/composer.json b/composer.json index 40dfe121f1..afcb2e1462 100644 --- a/composer.json +++ b/composer.json @@ -118,6 +118,7 @@ "drupal/metatag": "^1.7", "drupal/migrate_plus": "4.0", "drupal/migrate_tools": "4.0", + "drupal/module_filter": "^3.1", "drupal/paragraphs": "1.5", "drupal/pathauto": "1.0", "drupal/redis": "1.0", diff --git a/composer.lock b/composer.lock index bec7d5c7b6..3218f444dd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "c7f966be2bf443d90f7fc6bea9496524", + "content-hash": "7c8d97cc8cd3a2ebf93eacc429204dd6", "packages": [ { "name": "alchemy/zippy", @@ -4498,7 +4498,8 @@ } }, "patches_applied": { - "2809699": "https://www.drupal.org/files/issues/2018-10-26/menu_block-label_configuration-2809699-82.patch" + "2809699": "https://www.drupal.org/files/issues/2018-10-26/menu_block-label_configuration-2809699-82.patch", + "2811337": "https://www.drupal.org/files/issues/menu_block-2_level_menu_block_not_limited_to_active_parent-2811337-58.patch" } }, "notification-url": "https://packages.drupal.org/8/downloads", @@ -4797,6 +4798,61 @@ "irc": "irc://irc.freenode.org/drupal-migrate" } }, + { + "name": "drupal/module_filter", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://git.drupal.org/project/module_filter", + "reference": "8.x-3.1" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/module_filter-8.x-3.1.zip", + "reference": "8.x-3.1", + "shasum": "39d627ce60280ae54bcf9beae217b85cce1969e4" + }, + "require": { + "drupal/core": "~8.0" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-3.x": "3.x-dev" + }, + "drupal": { + "version": "8.x-3.1", + "datestamp": "1507650844", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "greenSkin", + "homepage": "https://www.drupal.org/user/173855" + }, + { + "name": "realityloop", + "homepage": "https://www.drupal.org/user/139189" + }, + { + "name": "shumushin", + "homepage": "https://www.drupal.org/user/22093" + } + ], + "description": "Filter the modules list.", + "homepage": "https://www.drupal.org/project/module_filter", + "support": { + "source": "http://cgit.drupalcode.org/module_filter" + } + }, { "name": "drupal/paragraphs", "version": "1.5.0", diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 1fb1be93a1..efec831bfe 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -4945,6 +4945,63 @@ "irc": "irc://irc.freenode.org/drupal-migrate" } }, + { + "name": "drupal/module_filter", + "version": "3.1.0", + "version_normalized": "3.1.0.0", + "source": { + "type": "git", + "url": "https://git.drupal.org/project/module_filter", + "reference": "8.x-3.1" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/module_filter-8.x-3.1.zip", + "reference": "8.x-3.1", + "shasum": "39d627ce60280ae54bcf9beae217b85cce1969e4" + }, + "require": { + "drupal/core": "~8.0" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-3.x": "3.x-dev" + }, + "drupal": { + "version": "8.x-3.1", + "datestamp": "1507650844", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "installation-source": "dist", + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "greenSkin", + "homepage": "https://www.drupal.org/user/173855" + }, + { + "name": "realityloop", + "homepage": "https://www.drupal.org/user/139189" + }, + { + "name": "shumushin", + "homepage": "https://www.drupal.org/user/22093" + } + ], + "description": "Filter the modules list.", + "homepage": "https://www.drupal.org/project/module_filter", + "support": { + "source": "http://cgit.drupalcode.org/module_filter" + } + }, { "name": "drupal/paragraphs", "version": "1.5.0", diff --git a/web/modules/module_filter/.gitignore b/web/modules/module_filter/.gitignore new file mode 100644 index 0000000000..485dee64bc --- /dev/null +++ b/web/modules/module_filter/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/web/modules/module_filter/LICENSE.txt b/web/modules/module_filter/LICENSE.txt new file mode 100644 index 0000000000..d159169d10 --- /dev/null +++ b/web/modules/module_filter/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/web/modules/module_filter/config/install/module_filter.settings.yml b/web/modules/module_filter/config/install/module_filter.settings.yml new file mode 100644 index 0000000000..a23be2632a --- /dev/null +++ b/web/modules/module_filter/config/install/module_filter.settings.yml @@ -0,0 +1 @@ +tabs: true diff --git a/web/modules/module_filter/css/module_filter.css b/web/modules/module_filter/css/module_filter.css new file mode 100644 index 0000000000..4e3dbc67dc --- /dev/null +++ b/web/modules/module_filter/css/module_filter.css @@ -0,0 +1,32 @@ +.winnow-input { + position: relative; + display: inline-block; +} +.winnow-input input.form-search { + padding-right: 2em; +} +.winnow-clear { + position: absolute; + top: 0; + bottom: 0; + right: 2px; + left: auto; + text-align: left; + text-indent: -9999px; +} +.winnow-clear:after { + position: absolute; + right: 0; + padding: 0 0.5em; + height: 100%; + content: '✕'; + color: #aaa; + font: 1em/2.2em arial, sans-serif; + text-decoration: none; + text-shadow: 0 1px 0 #fff; + text-indent: 0; + opacity: 0.7; +} +.winnow-clear:hover:after { + opacity: 1; +} diff --git a/web/modules/module_filter/css/module_filter.modules.css b/web/modules/module_filter/css/module_filter.modules.css new file mode 100644 index 0000000000..42f0b7330d --- /dev/null +++ b/web/modules/module_filter/css/module_filter.modules.css @@ -0,0 +1,5 @@ +#system-modules .table-filter .module-filter-status .form-item, +#system-modules-uninstall .table-filter .module-filter-status .form-item { + display: inline-block; + padding-right: 1em; +} diff --git a/web/modules/module_filter/css/module_filter.modules_tabs.css b/web/modules/module_filter/css/module_filter.modules_tabs.css new file mode 100644 index 0000000000..9b6a4eab88 --- /dev/null +++ b/web/modules/module_filter/css/module_filter.modules_tabs.css @@ -0,0 +1,134 @@ +.winnow-input { + display: block; +} +.modules-tabs { + position: relative; + overflow: hidden; + margin: 10px 0; + background: #E6E5E1; + border: 1px solid #bdbdbd; + border-radius: 4px; +} +.modules-tabs__menu { + float: left; /* LTR */ + width: 240px; + margin: 0 -100% -1px 0; /* LTR */ + padding: 0; + border-bottom: 1px solid #ccc; + line-height: 1; +} +[dir="rtl"] .modules-tabs__menu { + float: right; + margin: 0 0 -1px -100%; +} +.modules-tabs__menu-item { + background: #eee; + border-top: 1px solid #ccc; +} +.modules-tabs__menu-item:first-child { + border-top: 0 none; +} +.modules-tabs__menu-item.tab__new { + margin-bottom: 10px; + border-bottom: 1px solid #ccc; +} +.modules-tabs__menu-item.suggest { + background: #F9F9F9; +} +.modules-tabs__menu-item.disabled { + background: #ccc; + border-top-color: #bbb; + border-bottom-color: #bbb; +} +.modules-tabs__menu-item a { + display: block; + text-decoration: none; + padding: 10px; +} +.modules-tabs__menu-item a:hover, +.modules-tabs__menu-item a:focus { + background: #D5D5D5; + text-decoration: none; + outline: 0; +} +.modules-tabs__menu-item.is-selected a, +.modules-tabs__menu-item.is-selected a:hover, +.modules-tabs__menu-item.is-selected a:focus, +.modules-tabs__menu-item.is-selected a:active { + background-color: #FCFCFA; + margin-right: -1px; +} +.modules-tabs__menu-item.disabled a, +.modules-tabs__menu-item.disabled span{ + color: #999; +} +.modules-tabs__menu-item.disabled a { + pointer-events: none; + cursor: default; +} +.modules-tabs__menu-item .summary { + padding-top: 0.4em; + color: #666; + font-size: 0.846em; + line-height: 1em; +} +.modules-tabs__menu-item ul.enabling { + display: block; + color: green; + font-size: 0.75em; + font-weight: bold; +} +.modules-tabs__menu-item ul.enabling:before { + content: '+'; +} +.modules-tabs__menu-item span.result { + float: right; + margin-top: 2px; + color: #999; + font-size: 10px; +} +.modules-tabs__menu-item.disabled span.result { + display: none; +} +.modules-tabs__pane { + margin: 0 0 0 240px; /* LTR */ + padding: 10px 15px; + background: #FCFCFA; + border-left: 1px solid #A6A5A1; /* LTR */ +} +[dir="rtl"] .modules-tabs__pane { + margin: 0 240px 0 0; + border-left: none; + border-right: 1px solid #A6A5A1; +} +.modules-tabs__pane:after { + content: ''; + display: table; + clear: both; +} +.modules-tabs__pane input.table-filter-text { + width: 100%; +} +.modules-tabs__pane table { + table-layout: fixed; +} +.modules-tabs__pane table col.checkbox { + width: 8%; +} +.modules-tabs__pane table col.name { + width: 25%; +} +.modules-tabs__pane table col.version { + width: 10%; +} +.modules-tabs__pane table col.links { + width: 15%; +} +.modules-tabs__pane table tr.enabling { + background-color: #dfd; +} +.modules-tabs__pane table td details summary { + overflow: hidden; + text-overflow: ellipsis; +} + diff --git a/web/modules/module_filter/css/module_filter.update_status.css b/web/modules/module_filter/css/module_filter.update_status.css new file mode 100644 index 0000000000..29042d887a --- /dev/null +++ b/web/modules/module_filter/css/module_filter.update_status.css @@ -0,0 +1,8 @@ +#module-filter-update-status-form .table-filter { + float: right; + text-align: right; +} +#module-filter-update-status-form .table-filter .module-filter-status .form-item { + display: inline-block; + padding-left: 1em; +} diff --git a/web/modules/module_filter/js/jquery.winnow.js b/web/modules/module_filter/js/jquery.winnow.js new file mode 100644 index 0000000000..32123fdd2d --- /dev/null +++ b/web/modules/module_filter/js/jquery.winnow.js @@ -0,0 +1,361 @@ +/** + * jQuery plugin + * Filter out elements based on user input. + */ + +(function ($) { + + var now = Date.now || function() { + return new Date().getTime(); + }; + + function debounce(func, wait, immediate) { + var timeout; + var args; + var context; + var timestamp; + var result; + + var later = function() { + var last = now() - timestamp; + + if (last < wait && last >= 0) { + timeout = setTimeout(later, wait - last); + } + else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + if (!timeout) { + args = context = null; + } + } + } + }; + + return function() { + context = this; + args = arguments; + timestamp = now(); + var callNow = immediate && !timeout; + if (!timeout) { + timeout = setTimeout(later, wait); + } + if (callNow) { + result = func.apply(context, args); + args = context = null; + } + + return result; + } + } + + function explode(string) { + return string.match(/(\w*\:(\w+|"[^"]+")*)|\w+|"[^"]+"/g); + } + + function preventEnterKey(e) { + if (e.which === 13) { + e.preventDefault(); + e.stopPropagation(); + } + }; + + var Winnow = function(element, selector, options) { + var self = this; + + self.element = element; + self.selector = selector; + self.text = ''; + self.queries = []; + self.results = []; + self.state = {}; + + self.options = $.extend({ + delay: 500, + striping: false, + selector: '', + textSelector: null, + emptyMessage: '', + clearLabel: 'clear', + rules: [], + buildIndex: [], + additionalOperators: {} + }, $.fn.winnow.defaults, options); + if (self.options.wrapper === undefined) { + self.options.wrapper = $(self.selector).parent(); + } + + self.element.wrap('<div class="winnow-input"></div>'); + + // Add clear button. + self.clearButton = $('<a href="#" class="winnow-clear">' + self.options.clearLabel + '</a>'); + self.clearButton.css({ + 'display': 'inline-block', + 'margin-left': '0.75em' + }); + if (self.element.val() == '') { + self.clearButton.hide(); + } + self.clearButton.click(function(e) { + e.preventDefault(); + + self.clearFilter(); + }); + self.element.after(self.clearButton); + + self.element.on({ + keyup: debounce(function() { + var value = self.element.val(); + if (!value || explode(value).pop().slice(-1) !== ':') { + // Only filter if we aren't using the operator autocomplete. + self.filter(); + } + }, self.options.delay), + keydown: preventEnterKey + }); + self.element.on({ + keyup: function() { + // Show/hide the clear button. + if (self.element.val() != '') { + self.clearButton.show(); + } + else { + self.clearButton.hide(); + } + } + }); + + // Autocomplete operators. When last query is ":", return list of available + // operators with the exception of "text". + if (typeof self.element.autocomplete === 'function') { + var operators = Object.keys(self.getOperators()); + var source = []; + for (var i in operators) { + if (operators[i] != 'text') { + source.push({ + label: operators[i], + value: operators[i] + ':' + }); + } + } + + self.element.autocomplete({ + 'search': function(event) { + if (explode(event.target.value).pop() != ':') { + return false; + } + }, + 'source': function(request, response) { + return response(source); + }, + 'select': function(event, ui) { + var terms = explode(event.target.value); + // Remove the current input. + terms.pop(); + // Add the selected item. + terms.push(ui.item.value); + event.target.value = terms.join(' '); + // Return false to tell jQuery UI that we've filled in the value already. + return false; + }, + 'focus': function() { + return false; + } + }); + } + + self.element.data('winnow', self); + }; + + Winnow.prototype.setQueries = function(string) { + var self = this; + var strings = explode(string); + + self.text = string; + self.queries = []; + + for (var i in strings) { + if (strings.hasOwnProperty(i)) { + var query = { operator: 'text', string: strings[i] }; + var operators = self.getOperators(); + + if (query.string.indexOf(':') > 0) { + var parts = query.string.split(':', 2); + var operator = parts.shift(); + if (operators[operator] !== undefined) { + query.operator = operator; + query.string = parts.shift(); + } + } + + if (query.string.charAt(0) == '"') { + // Remove wrapping double quotes. + query.string = query.string.replace(/^"|"$/g, ''); + } + + query.string = query.string.toLowerCase(); + + self.queries.push(query); + } + } + }; + + Winnow.prototype.buildIndex = function() { + var self = this; + this.index = []; + + $(self.selector, self.wrapper).each(function(i) { + var text = (self.options.textSelector) ? $(self.options.textSelector, this).text() : $(this).text(); + var item = { + key: i, + element: $(this), + text: text.toLowerCase() + }; + + for (var j in self.options.buildIndex) { + item = $.extend(self.options.buildIndex[j].apply(this, [item]), item); + } + + $(this).data('winnowIndex', i); + self.index.push(item); + }); + + return self.trigger('finishIndexing', [ self ]); + }; + + Winnow.prototype.bind = function() { + var args = arguments; + args[0] = 'winnow:' + args[0]; + + return this.element.bind.apply(this.element, args); + }; + + Winnow.prototype.trigger = function(event) { + var args = arguments; + args[0] = 'winnow:' + args[0]; + + return this.element.trigger.apply(this.element, args); + }; + + Winnow.prototype.filter = function() { + var self = this; + + self.results = []; + self.setQueries(self.element.val()); + + if (self.index === undefined) { + self.buildIndex(); + } + + var start = self.trigger('start'); + + $.each(self.index, function(key, item) { + var $item = item.element; + var operatorMatch = true; + + if (self.text != '') { + operatorMatch = false; + for (var i in self.queries) { + var query = self.queries[i]; + var operators = self.getOperators(); + + if (operators[query.operator] !== undefined) { + result = operators[query.operator].apply($item, [query.string, item]); + if (!result) { + // Is not a text match so continue to next query string. + continue; + } + } + + operatorMatch = true; + break; + } + } + + if (operatorMatch && self.processRules(item) !== false) { + // Item is a match. + $item.show(); + self.results.push(item); + return true; + } + + // By reaching here, the $item is not a match so we hide it. + $item.hide(); + }); + + var finish = self.trigger('finish', [ self.results ]); + + if (self.options.striping) { + stripe(); + } + + if (self.options.emptyMessage) { + if (self.results.length > 0) { + self.options.wrapper.children('.winnow-no-results').remove(); + } + else if (!self.options.wrapper.children('.winnow-no-results').length) { + self.options.wrapper.append($('<p class="winnow-no-results"></p>').text(self.options.emptyMessage)); + } + } + }; + + Winnow.prototype.getOperators = function() { + return $.extend({}, { + text: function(string, item) { + if (item.text.indexOf(string) >= 0) { + return true; + } + } + }, this.options.additionalOperators); + }; + + Winnow.prototype.processRules = function(item) { + var self = this; + var $item = item.element; + var result = true; + + if (self.options.rules.length > 0) { + for (var i in self.options.rules) { + result = self.options.rules[i].apply($item, [item]); + if (result === false) { + break; + } + } + } + + return result; + }; + + Winnow.prototype.stripe = function() { + var flip = { even: 'odd', odd: 'even' }; + var stripe = 'odd'; + + $.each(this.index, function(key, item) { + if (!item.element.is(':visible')) { + item.element.removeClass('odd even').addClass(stripe); + stripe = flip[stripe]; + } + }); + }; + + Winnow.prototype.clearFilter = function() { + this.element.val(''); + this.filter(); + this.clearButton.hide(); + this.element.focus(); + }; + + $.fn.winnow = function(selector, options) { + var $input = this.not('.winnow-processed').addClass('winnow-processed'); + + $input.each(function() { + var winnow = new Winnow($input, selector, options); + }); + + return this; + }; + + $.fn.winnow.defaults = {}; + +})(jQuery); diff --git a/web/modules/module_filter/js/module_filter.js b/web/modules/module_filter/js/module_filter.js new file mode 100644 index 0000000000..25c436ea00 --- /dev/null +++ b/web/modules/module_filter/js/module_filter.js @@ -0,0 +1,45 @@ +(function($, Drupal) { + + 'use strict'; + + Drupal.ModuleFilter = Drupal.ModuleFilter || {}; + + Drupal.ModuleFilter.localStorage = { + getItem: function(key) { + if (typeof Storage !== 'undefined') { + return localStorage.getItem('moduleFilter.' + key); + } + + return null; + }, + getBoolean: function(key) { + var item = Drupal.ModuleFilter.localStorage.getItem(key); + + if (item != null) { + return (item == 'true'); + } + + return null; + }, + setItem: function(key, data) { + if (typeof Storage !== 'undefined') { + localStorage.setItem('moduleFilter.' + key, data) + } + }, + removeItem: function(key) { + if (typeof Storage !== 'undefined') { + localStorage.removeItem('moduleFilter.' + key); + } + } + }; + + /** + * Filter enhancements. + */ + Drupal.behaviors.moduleFilter = { + attach: function(context) { + + } + }; + +})(jQuery, Drupal); diff --git a/web/modules/module_filter/js/module_filter.modules.js b/web/modules/module_filter/js/module_filter.modules.js new file mode 100644 index 0000000000..0e06711492 --- /dev/null +++ b/web/modules/module_filter/js/module_filter.modules.js @@ -0,0 +1,174 @@ +/** + * @file + * Module filter behaviors. + */ + +(function($, Drupal) { + + 'use strict'; + + Drupal.ModuleFilter = Drupal.ModuleFilter || {}; + var ModuleFilter = Drupal.ModuleFilter; + + /** + * Filter enhancements. + */ + Drupal.behaviors.moduleFilterModules = { + attach: function(context, settings) { + var $input = $('input.table-filter-text', context).once('module-filter'); + if ($input.length) { + ModuleFilter.input = $input; + ModuleFilter.selector = 'tbody tr'; + ModuleFilter.wrapperId = ModuleFilter.input.attr('data-table'); + ModuleFilter.wrapper = $(ModuleFilter.wrapperId); + var $enabled = $('.table-filter [name="checkboxes[enabled]"]', ModuleFilter.wrapper); + var $disabled = $('.table-filter [name="checkboxes[disabled]"]', ModuleFilter.wrapper); + var $unavailable = $('.table-filter [name="checkboxes[unavailable]"]', ModuleFilter.wrapper); + + var showEnabled = ModuleFilter.localStorage.getBoolean('modules.enabled'); + if (showEnabled == null) { + showEnabled = $enabled.is(':checked'); + } + $enabled.prop('checked', showEnabled); + var showDisabled = ModuleFilter.localStorage.getBoolean('modules.disabled'); + if (showDisabled == null) { + showDisabled = $disabled.is(':checked'); + } + $disabled.prop('checked', showDisabled); + var showUnavailable = ModuleFilter.localStorage.getBoolean('modules.unavailable'); + if (showUnavailable == null) { + showUnavailable = $unavailable.is(':checked'); + } + $unavailable.prop('checked', showUnavailable); + + ModuleFilter.wrapper.children('details').wrapAll('<div class="modules-wrapper"></div>'); + ModuleFilter.modulesWrapper = $('.modules-wrapper', ModuleFilter.wrapper); + + ModuleFilter.input.winnow(ModuleFilter.wrapperId + ' ' + ModuleFilter.selector, { + textSelector: 'td.module .module-name', + emptyMessage: Drupal.t('No results'), + clearLabel: Drupal.t('clear'), + wrapper: ModuleFilter.modulesWrapper, + buildIndex: [ + function(item) { + var $checkbox = $('td.checkbox :checkbox', item.element); + if ($checkbox.length > 0) { + item.status = $checkbox.is(':checked'); + item.disabled = $checkbox.is(':disabled'); + } + else { + item.status = false; + item.disabled = true; + } + return item; + } + ], + additionalOperators: { + description: function(string, item) { + if (item.description == undefined) { + // Soft cache. + item.description = $('.module-description', item.element).text().toLowerCase(); + } + + if (item.description.indexOf(string) >= 0) { + return true; + } + }, + requiredBy: function(string, item) { + if (item.requiredBy === undefined) { + // Soft cache. + item.requiredBy = []; + $('.admin-requirements.required-by li', item.element).each(function() { + var moduleName = $(this) + .text() + .toLowerCase() + .replace(/\([a-z]*\)/g, ''); + item.requiredBy.push($.trim(moduleName)); + }); + } + + if (item.requiredBy.length) { + for (var i in item.requiredBy) { + if (item.requiredBy[i].indexOf(string) >= 0) { + return true; + } + } + } + }, + requires: function(string, item) { + if (item.requires === undefined) { + // Soft cache. + item.requires = []; + $('.admin-requirements.requires li', item.element).each(function() { + var moduleName = $(this) + .text() + .toLowerCase() + .replace(/\([a-z]*\)/g, ''); + item.requires.push($.trim(moduleName)); + }); + } + + if (item.requires.length) { + for (var i in item.requires) { + if (item.requires[i].indexOf(string) >= 0) { + return true; + } + } + } + } + }, + rules: [ + function(item) { + if (showEnabled) { + if (item.status === true && item.disabled === true) { + return true; + } + } + if (showDisabled) { + if (item.status === false && item.disabled === false) { + return true; + } + } + if (showUnavailable) { + if (item.status === false && item.disabled === true) { + return true; + } + } + + return false; + } + ] + }).focus(); + ModuleFilter.winnow = ModuleFilter.input.data('winnow'); + + var $details = ModuleFilter.modulesWrapper.children('details'); + ModuleFilter.input.bind('winnow:finish', function() { + Drupal.announce( + Drupal.formatPlural( + ModuleFilter.modulesWrapper.find(ModuleFilter.selector + ':visible').length, + '1 module is available in the modified list.', + '@count modules are available in the modified list.' + ) + ); + }); + + $enabled.change(function(e, el) { + showEnabled = $(this).is(':checked'); + ModuleFilter.localStorage.setItem('modules.enabled', showEnabled); + ModuleFilter.winnow.filter(); + }); + $disabled.change(function() { + showDisabled = $disabled.is(':checked'); + ModuleFilter.localStorage.setItem('modules.disabled', showDisabled); + ModuleFilter.winnow.filter(); + }); + $unavailable.change(function() { + showUnavailable = $unavailable.is(':checked'); + ModuleFilter.localStorage.setItem('modules.unavailable', showUnavailable); + ModuleFilter.winnow.filter(); + }); + } + } + }; + +})(jQuery, Drupal); diff --git a/web/modules/module_filter/js/module_filter.modules_bare.js b/web/modules/module_filter/js/module_filter.modules_bare.js new file mode 100644 index 0000000000..fbaf9ce124 --- /dev/null +++ b/web/modules/module_filter/js/module_filter.modules_bare.js @@ -0,0 +1,41 @@ +(function($, Drupal) { + + 'use strict'; + + Drupal.ModuleFilter = Drupal.ModuleFilter || {}; + var ModuleFilter = Drupal.ModuleFilter; + + /** + * Filter enhancements. + */ + Drupal.behaviors.moduleFilterModulesBare = { + attach: function(context) { + if (ModuleFilter.input != undefined) { + var $details = ModuleFilter.modulesWrapper.children('details'); + + ModuleFilter.input.bind('winnow:start', function() { + // Note that we first open all <details> to be able to use ':visible'. + // Mark the <details> elements that were closed before filtering, so + // they can be reclosed when filtering is removed. + $details.show().not('[open]').attr('data-module_filter-state', 'forced-open'); + }); + ModuleFilter.input.bind('winnow:finish', function() { + // Hide the package <details> if they don't have any visible rows. + // Note that we first show() all <details> to be able to use ':visible'. + $details.attr('open', true).each(function(index, element) { + var $group = $(element); + var $visibleRows = $group.find('tbody tr:visible'); + $group.toggle($visibleRows.length > 0); + }); + + // Return <details> elements that had been closed before filtering + // to a closed state. + $details.filter('[data-module_filter-state="forced-open"]') + .removeAttr('data-module_filter-state') + .attr('open', false); + }); + } + } + }; + +})(jQuery, Drupal); diff --git a/web/modules/module_filter/js/module_filter.modules_tabs.js b/web/modules/module_filter/js/module_filter.modules_tabs.js new file mode 100644 index 0000000000..103e2cbcf7 --- /dev/null +++ b/web/modules/module_filter/js/module_filter.modules_tabs.js @@ -0,0 +1,480 @@ +(function($) { + + // 'use strict'; + + Drupal.ModuleFilter = Drupal.ModuleFilter || {}; + var ModuleFilter = Drupal.ModuleFilter; + + var Tabs = function(tabs, $pane) { + var $tabs = $('<ul class="modules-tabs__menu"></ul>'); + + // Add our three special tabs. + var all = new Tab(Drupal.t('All modules'), 'all'); + var recentModules = new Tab(Drupal.t('Recently enabled'), 'recent'); + var newModules = new Tab(Drupal.t('Newly available'), 'new'); + tabs = $.extend({ + "all": all, + "recent": recentModules, + "new": newModules + }, tabs); + + for (var i in tabs) { + $tabs.append(tabs[i].element); + } + + $pane.wrap('<div class="modules-tabs clearfix"></div>'); + $pane.before($tabs); + $pane.addClass('modules-tabs__pane'); + + this.tabs = tabs; + this.element = $tabs; + this.activeTab = null; + + // Handle "recent" and "new" tabs when they contain no items. + // Todo: move this somewhere else. + var $rows = $(ModuleFilter.selector, ModuleFilter.wrapper); + if (!$rows.filter('.recent').length) { + var recentModules = this.tabs['recent']; + if (recentModules) { + recentModules.element.addClass('disabled'); + recentModules.setSummary(Drupal.t('No modules installed or uninstalled within the last week.')); + recentModules.showSummary(); + } + } + if (!$rows.filter('.new').length) { + var newModules = this.tabs['new']; + if (newModules) { + newModules.element.addClass('disabled'); + newModules.setSummary(Drupal.t('No modules added within the last week.')); + newModules.showSummary(); + } + } + + // Add counts of how many modules are enabled out of the total for the tab. + // Todo: move this somewhere else. + var $elements = $(ModuleFilter.selector, ModuleFilter.wrapper); + var enabled; + var total; + for (var i in this.tabs) { + if (this.tabs[i].element.hasClass('disabled')) { + continue; + } + + switch (i) { + case 'all': + var $all = $elements.find('td.checkbox :checkbox'); + enabled = $all.filter(':checked').length; + total = $all.length; + break; + + case 'recent': + var $recent = $elements.filter('.recent').find('td.checkbox :checkbox'); + enabled = $recent.filter(':checked').length; + total = $recent.length; + break; + + case 'new': + var $new = $elements.filter('.new').find('td.checkbox :checkbox'); + enabled = $new.filter(':checked').length; + total = $new.length; + break; + + default: + var $package = $elements.filter('.package__' + i).find(' td.checkbox :checkbox'); + enabled = $package.filter(':checked').length; + total = $package.length; + break; + } + + if (total) { + var enabledCount = Drupal.t('@enabled of @total', { '@enabled': enabled, '@total': total }); + this.tabs[i].setSummary(enabledCount, 'enabledCount'); + } + } + }; + + Tabs.prototype.getActive = function() { + if (this.activeTab) { + return this.activeTab; + } + }; + + Tabs.prototype.setActive = function(tab) { + if (this.activeTab) { + this.activeTab.hideSummary(); + this.activeTab.element.removeClass('is-selected'); + } + + this.activeTab = tab; + this.activeTab.element.addClass('is-selected'); + this.activeTab.showSummary(); + + return this.activeTab; + }; + + Tabs.prototype.get = function(packageId) { + if (this.tabs[packageId]) { + return this.tabs[packageId]; + } + }; + + Tabs.prototype.resetResults = function() { + for (var i in this.tabs) { + this.tabs[i].resetResults(); + } + }; + + Tabs.prototype.showResults = function() { + var staticTabs = [ 'all', 'recent', 'new' ]; + + for (var i in this.tabs) { + var count = this.tabs[i].results.length; + + if (count > 0 || i == this.activeTab.packageId || staticTabs.indexOf(i) >= 0) { + this.tabs[i].showResults(); + this.tabs[i].element.show(); + } + else { + this.tabs[i].element.hide(); + } + } + }; + + Tabs.prototype.hideResults = function() { + for (var i in this.tabs) { + this.tabs[i].hideResults(); + this.tabs[i].element.show(); + } + }; + + var Tab = function(name, packageId) { + this.name = name; + this.packageId = packageId; + this.element = $('<li class="modules-tabs__menu-item tab__' + this.packageId + '"></li>'); + this.results = []; + this.link = $('<a href="#' + this.packageId + '"><strong>' + this.name + '</strong></a>'); + this.element.append(this.link); + this.link.append('<span class="result"></span>'); + this.summary = null; + }; + + Tab.prototype.select = function() { + ModuleFilter.tabs.setActive(this); + + if (ModuleFilter.winnow) { + ModuleFilter.winnow.filter(); + } + }; + + Tab.prototype.resetResults = function() { + this.results = []; + }; + + Tab.prototype.showResults = function() { + $('span.result', this.element).text(this.results.length); + }; + + Tab.prototype.hideResults = function() { + $('span.result', this.element).empty(); + }; + + Tab.prototype.setSummary = function(summary, key, persistent) { + if (!this.summary) { + this.summary = new Summary(); + this.link.append(this.summary.element); + } + + this.summary.set(summary, key, persistent); + }; + + Tab.prototype.showSummary = function() { + this.toggleSummary(true); + }; + + Tab.prototype.hideSummary = function() { + this.toggleSummary(false); + }; + + Tab.prototype.toggleSummary = function(display) { + if (this.summary) { + this.summary.toggle(Boolean(display)); + } + }; + + Tab.prototype.toggleEnabling = function(name) { + this.enabling = this.enabling || {}; + if (this.enabling[name] != undefined) { + delete this.enabling[name]; + } + else { + this.enabling[name] = name; + } + + var enabling = []; + for (var i in this.enabling) { + enabling.push(this.enabling[i]); + } + + $('ul.enabling', this.element).remove(); + if (enabling.length) { + enabling.sort(); + + var $list = $('<ul class="item-list__comma-list enabling"></ul>'); + $list.append('<li>' + enabling.join('</li><li>') + '</li>'); + this.setSummary($list, 'enabling', true); + } + else { + this.setSummary('', 'enabling'); + } + }; + + var Summary = function() { + this.element = $('<div class="summary"></div>'); + this.element.hide(); + this.items = {}; + }; + + Summary.prototype.show = function() { + this.toggle(true); + }; + + Summary.prototype.hide = function() { + this.toggle(false); + }; + + Summary.prototype.toggle = function(display) { + display = Boolean(display); + + this.element.children(':not(.persistent)').toggle(display); + + if (!display && !this.element.children('.persistent').length) { + this.element.hide(); + } + else { + this.element.show(); + } + }; + + Summary.prototype.set = function(summary, key, persistent) { + if (!key) { + key = 'default'; + } + + var empty = false; + if (typeof summary == 'string' || typeof summary == 'boolean') { + if (!summary) { + empty = true; + } + } + else if (typeof summary == 'object') { + // Assume the object is a jQuery object. + if (!summary.length) { + empty = true; + } + } + if (empty) { + if (this.items[key] != undefined) { + this.items[key].remove(); + delete this.items[key]; + } + + if (!Object.keys(this.items).length) { + // Hide the summary when there are no items. + this.hide(); + } + return; + } + + if (persistent == undefined) { + persistent = false; + } + + if (this.items[key] === undefined) { + var $element = $('<span></span>'); + this.element.append($element); + this.items[key] = $element; + } + + this.items[key].empty().append(summary); + this.items[key].toggleClass('persistent', persistent); + + if (persistent) { + // Make sure the summary element is visible. + this.show(); + } + }; + + Drupal.behaviors.moduleFilterModulesTabs = { + attach: function(context) { + if (ModuleFilter.input != undefined) { + var tabs = {}; + + function buildTable() { + // Build our unified table. + var $originalTable = $('table', ModuleFilter.wrapper).first(); + var $table = $('<table></table>'); + if ($originalTable.hasClass('responsive-enabled')) { + $table.addClass('responsive-enabled'); + } + var striping = $originalTable.attr('data-striping') + if (striping) { + $table.attr('data-striping', striping); + } + + // Because the table headers are visually hidden, we use col to set + // the column widths. + var $colgroup = $('<colgroup></colgroup>'); + $('thead th', $originalTable).each(function() { + $colgroup.append('<col class="' + $(this).attr('class') + '">'); + }); + $('col', $colgroup).removeClass('visually-hidden'); + $table.append($colgroup); + $table.append($('thead', $originalTable)); + $table.append('<tbody></tbody>'); + + ModuleFilter.modulesWrapper.children('details').each(function() { + var $details = $(this); + var packageName = $details.children('summary').text(); + var packageId = packageName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); + + if (tabs[packageId] == undefined) { + tabs[packageId] = new Tab(packageName, packageId); + } + + $('.details-wrapper tbody tr', $details).each(function() { + var $row = $(this); + $row.addClass('package__' + packageId); + $row.data('moduleFilter.packageId', packageId); + + $row.hover(function() { + tabs[packageId].element.addClass('suggest'); + }, function() { + tabs[packageId].element.removeClass('suggest'); + }); + + $('td.checkbox input', $row).change(function() { + $row.toggleClass('enabling', $(this).is(':checked')); + + var packageId = $row.data('moduleFilter.packageId'); + if (packageId && tabs[packageId]) { + tabs[packageId].toggleEnabling($('td.module label', $row).text()); + } + }); + + $('tbody', $table).append($row); + }); + }); + + // Remove package detail elements. + ModuleFilter.modulesWrapper.children('details').remove(); + + // Sort rows by module name. + var $rows = $('tbody tr', $table); + $rows.sort(function(a, b) { + var aname = $('td.module label', a).text(); + var bname = $('td.module label', b).text(); + + if (aname == bname) { + return 0; + } + + return aname > bname ? 1 : -1; + }); + $rows.detach().appendTo($('tbody', $table)); + + // Add the unified table. + ModuleFilter.modulesWrapper.append($table); + } + + function selectTabByHash() { + var hash = window.location.hash; + hash = hash.substring(hash.indexOf('#') + 1); + + var tab = ModuleFilter.tabs.get(hash); + if (tab) { + tab.select(); + } + else { + tab = ModuleFilter.tabs.get('all'); + if (tab) { + tab.select(); + } + } + + ModuleFilter.input.focus(); + } + + buildTable(); + ModuleFilter.tabs = new Tabs(tabs, ModuleFilter.wrapper); + + ModuleFilter.winnow.options.rules.push(function(item) { + var activeTab = ModuleFilter.tabs.getActive(); + + // Update tab results. The results are updated prior to hiding the + // items not visible in the active tab. + var allTab = ModuleFilter.tabs.get('all'); + allTab.results.push(item); + if (item.element.hasClass('recent')) { + var recentTab = ModuleFilter.tabs.get('recent'); + recentTab.results.push(item); + } + if (item.element.hasClass('new')) { + var newTab = ModuleFilter.tabs.get('new'); + newTab.results.push(item); + } + if (item.tab != undefined && item.tab) { + item.tab.results.push(item); + } + + // For tabs other than "all", evaluate whether the item should + // be shown. + if (activeTab && activeTab.packageId != 'all') { + switch (activeTab.packageId) { + case 'recent': + if (item.element.hasClass('recent')) { + return true; + } + break; + + case 'new': + if (item.element.hasClass('new')) { + return true; + } + break; + + default: + if (item.element.hasClass('package__' + activeTab.packageId)) { + return true; + } + break; + } + + return false; + } + }); + ModuleFilter.winnow.bind('finishIndexing', function(e, winnow) { + $.each(winnow.index, function(key, item) { + var packageId = item.element.data('moduleFilter.packageId'); + if (packageId) { + item.tab = ModuleFilter.tabs.get(packageId); + } + }); + }); + ModuleFilter.winnow.bind('start', function() { + ModuleFilter.tabs.resetResults(); + }); + ModuleFilter.winnow.bind('finish', function() { + if (ModuleFilter.input.val() != '') { + ModuleFilter.tabs.showResults(); + } + else { + ModuleFilter.tabs.hideResults(); + } + }); + + $(window).bind('hashchange.moduleFilter', selectTabByHash).triggerHandler('hashchange.moduleFilter'); + } + } + }; + +})(jQuery); diff --git a/web/modules/module_filter/js/module_filter.modules_uninstall.js b/web/modules/module_filter/js/module_filter.modules_uninstall.js new file mode 100644 index 0000000000..0c6e5a4276 --- /dev/null +++ b/web/modules/module_filter/js/module_filter.modules_uninstall.js @@ -0,0 +1,56 @@ +/** + * @file + * Module filter behaviors. + */ + +(function($, Drupal) { + + 'use strict'; + + /** + * Filter enhancements. + */ + Drupal.behaviors.moduleFilterModulesUninstall = { + attach: function(context, settings) { + var $input = $('input.table-filter-text', context).once('module-filter'); + if ($input.length) { + var wrapperId = $input.attr('data-table'); + var $wrapper = $(wrapperId); + var selector = 'tbody tr'; + + $wrapper.children('details').wrapAll('<div class="modules-uninstall-wrapper"></div>'); + var $modulesWrapper = $('.modules-uninstall-wrapper', $wrapper); + + $input.winnow(wrapperId + ' ' + selector, { + textSelector: 'td .module-name', + emptyMessage: Drupal.t('No results'), + clearLabel: Drupal.t('clear'), + wrapper: $modulesWrapper, + additionalOperators: { + description: function(string, item) { + if (item.description == undefined) { + // Soft cache. + item.description = $('.module-description', item.element).text().toLowerCase(); + } + + if (item.description.indexOf(string) >= 0) { + return true; + } + } + } + }).focus(); + + $input.bind('winnow:finish', function() { + Drupal.announce( + Drupal.formatPlural( + $modulesWrapper.find(selector + ':visible').length, + '1 module is available in the modified list.', + '@count modules are available in the modified list.' + ) + ); + }); + } + } + }; + +})(jQuery, Drupal); diff --git a/web/modules/module_filter/js/module_filter.permissions.js b/web/modules/module_filter/js/module_filter.permissions.js new file mode 100644 index 0000000000..2a2a9c149e --- /dev/null +++ b/web/modules/module_filter/js/module_filter.permissions.js @@ -0,0 +1,71 @@ +(function($) { + + 'use strict'; + + /** + * Filter enhancements. + */ + Drupal.behaviors.moduleFilterPermissions = { + attach: function(context) { + var $input = $('input.table-filter-text', context).once('module-filter'); + if ($input.length) { + var wrapperId = $input.attr('data-table'); + var selector = 'tbody tr'; + var lastModuleItem; + + // Move location of filter input to before the permissions table. + $(wrapperId).parent().prepend($input.closest('.table-filter')); + + $input.winnow(wrapperId + ' ' + selector, { + textSelector: 'td.module', + buildIndex: [ + function(item) { + item.isModule = item.text != ''; + + if (item.isModule) { + item.children = []; + lastModuleItem = item; + } + else { + item.parent = lastModuleItem; + lastModuleItem.children.push(item); + } + + return item; + } + ], + additionalOperators: { + perm: function(string, item) { + if (!item.isModule) { + if (item.permission == undefined) { + item.permission = $('.permission .title', item.element).text().toLowerCase(); + } + + if (item.permission.indexOf(string) >= 0) { + return true; + } + } + } + } + }); + + var winnow = $input.data('winnow'); + $input.bind('winnow:finish', function() { + if (winnow.results.length > 0) { + for (var i in winnow.results) { + if (winnow.results[i].isModule) { + for (var k in winnow.results[i].children) { + winnow.results[i].children[k].element.show(); + } + } + else { + winnow.results[i].parent.element.show(); + } + } + } + }); + } + } + }; + +})(jQuery); diff --git a/web/modules/module_filter/js/module_filter.update_status.js b/web/modules/module_filter/js/module_filter.update_status.js new file mode 100644 index 0000000000..6ef7802b7d --- /dev/null +++ b/web/modules/module_filter/js/module_filter.update_status.js @@ -0,0 +1,101 @@ +/** + * @file + * Module filter behaviors. + */ + +(function($, Drupal) { + + 'use strict'; + + Drupal.ModuleFilter = Drupal.ModuleFilter || {}; + + /** + * Filter enhancements. + */ + Drupal.behaviors.moduleFilterUpdateStatus = { + attach: function(context, settings) { + var $input = $('input.table-filter-text').once('module-filter'); + if ($input.length) { + var selector = 'tbody tr'; + var wrapperId = $input.attr('data-table'); + var $wrapper = $(wrapperId); + + var $show = $('.table-filter input[name="show"]', $wrapper); + var show = Drupal.ModuleFilter.localStorage.getItem('updateStatus.show') || 'all'; + + $input.winnow(wrapperId + ' ' + selector, { + textSelector: 'td .project-update__title a', + emptyMessage: Drupal.t('No results'), + clearLabel: Drupal.t('clear'), + wrapper: $wrapper, + buildIndex: [ + function(item) { + if (item.element.is('.color-success')) { + item.state = 'ok'; + } + else if (item.element.is('.color-warning')) { + item.state = 'warning'; + } + else if (item.element.is('.color-error')) { + item.state = 'error'; + } + + return item; + } + ], + rules: [ + function(item) { + switch (show) { + case 'all': + return true; + + case 'updates': + if (item.state == 'warning' || item.state == 'error') { + return true; + } + break; + + case 'ignore': + if (item.state == 'ignored') { + return true; + } + break; + } + + return false; + } + ] + }).focus(); + Drupal.ModuleFilter.winnow = $input.data('winnow'); + + var $titles = $('h3', $wrapper); + $input.bind('winnow:finish', function() { + $titles.each(function(index, element) { + var $title = $(element); + var $table = $title.next(); + if ($table.is('table')) { + var $visibleRows = $table.find(selector + ':visible'); + $title.toggle($visibleRows.length > 0); + } + }); + + Drupal.announce( + Drupal.formatPlural( + $wrapper.find(selector + ':visible').length, + '1 project is available in the modified list.', + '@count projects are available in the modified list.' + ) + ); + }); + + $show.change(function() { + show = $(this).val(); + Drupal.ModuleFilter.localStorage.setItem('updateStatus.show', show); + Drupal.ModuleFilter.winnow.filter(); + }); + $show.filter('[value="' + show + '"]').prop('checked', true).trigger('change'); + } + } + }; + +})(jQuery, Drupal); diff --git a/web/modules/module_filter/module_filter.info.yml b/web/modules/module_filter/module_filter.info.yml new file mode 100644 index 0000000000..ab442fa9cb --- /dev/null +++ b/web/modules/module_filter/module_filter.info.yml @@ -0,0 +1,13 @@ +name: Module filter +type: module +description: Filter the modules list. +package: Administration +# core: 8.x + +configure: module_filter.settings + +# Information added by Drupal.org packaging script on 2017-10-10 +version: '8.x-3.1' +core: '8.x' +project: 'module_filter' +datestamp: 1507650850 diff --git a/web/modules/module_filter/module_filter.install b/web/modules/module_filter/module_filter.install new file mode 100644 index 0000000000..3af5125d56 --- /dev/null +++ b/web/modules/module_filter/module_filter.install @@ -0,0 +1,22 @@ +<?php + +/** + * @file + * Install, update and uninstall functions for the module_filter module. + */ + +/** + * Implements hook_install(). + */ +function module_filter_install() { + $state = \Drupal::state(); + $state->set('module_filter.recent', ['module_filter' => REQUEST_TIME]); +} + +/** + * Implements hook_uninstall(). + */ +function module_filter_uninstall() { + $state = \Drupal::state(); + $state->delete('module_filter.recent'); +} diff --git a/web/modules/module_filter/module_filter.libraries.yml b/web/modules/module_filter/module_filter.libraries.yml new file mode 100644 index 0000000000..5a604c5fa1 --- /dev/null +++ b/web/modules/module_filter/module_filter.libraries.yml @@ -0,0 +1,61 @@ +winnow: + version: VERSION + js: + js/jquery.winnow.js: {} + dependencies: + - core/jquery + - core/jquery.ui.autocomplete +filter: + version: VERSION + js: + js/module_filter.js: {} + css: + theme: + css/module_filter.css: {} + dependencies: + - module_filter/winnow +modules: + version: VERSION + js: + js/module_filter.modules.js: {} + css: + theme: + css/module_filter.modules.css: {} + dependencies: + - module_filter/filter +modules.bare: + version: VERSION + js: + js/module_filter.modules_bare.js: {} + dependencies: + - module_filter/modules +modules.tabs: + version: VERSION + js: + js/module_filter.modules_tabs.js: {} + css: + theme: + css/module_filter.modules_tabs.css: {} + dependencies: + - module_filter/modules +modules.uninstall: + version: VERSION + js: + js/module_filter.modules_uninstall.js: {} + dependencies: + - module_filter/filter +update.status: + version: VERSION + js: + js/module_filter.update_status.js: {} + css: + theme: + css/module_filter.update_status.css: {} + dependencies: + - module_filter/filter +permissions: + version: VERSION + js: + js/module_filter.permissions.js: {} + dependencies: + - module_filter/filter diff --git a/web/modules/module_filter/module_filter.links.menu.yml b/web/modules/module_filter/module_filter.links.menu.yml new file mode 100644 index 0000000000..aaea139d4a --- /dev/null +++ b/web/modules/module_filter/module_filter.links.menu.yml @@ -0,0 +1,6 @@ +module_filter.config_admin_menu_item: + title: 'Module filter' + description: 'Settings for the Module Filter module.' + menu_name: admin + parent: system.admin_config_ui + route_name: module_filter.settings diff --git a/web/modules/module_filter/module_filter.module b/web/modules/module_filter/module_filter.module new file mode 100644 index 0000000000..18c009bb80 --- /dev/null +++ b/web/modules/module_filter/module_filter.module @@ -0,0 +1,202 @@ +<?php + +/** + * @file + * Provides a filtering mechanism to various admin pages. + */ + +use Drupal\Core\Render\Element; +use Drupal\Core\Form\FormStateInterface; + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function module_filter_form_system_modules_alter(&$form, FormStateInterface $form_state, $form_id) { + $config = \Drupal::config('module_filter.settings'); + + $key = array_search('system/drupal.system.modules', $form['#attached']['library']); + if ($key !== FALSE) { + unset($form['#attached']['library'][$key]); + } + $form['#attached']['library'][] = $config->get('tabs') ? 'module_filter/modules.tabs' : 'module_filter/modules.bare'; + unset($form['filters']['text']['#description']); + $form['filters']['text']['#placeholder'] = t('Filter by name'); + if (!empty($_GET['filter'])) { + $form['filters']['text']['#default_value'] = $_GET['filter']; + } + + $status_defaults = [ + ((isset($_GET['enabled'])) ? $_GET['enabled'] : 1) ? 'enabled' : '', + ((isset($_GET['disabled'])) ? $_GET['disabled'] : 1) ? 'disabled' : '', + ((isset($_GET['unavailable'])) ? $_GET['unavailable'] : 1) ? 'unavailable' : '', + ]; + $form['filters']['status'] = [ + '#type' => 'container', + '#attributes' => [ + 'class' => [ + 'module-filter-status', + ], + ], + 'checkboxes' => [ + '#type' => 'checkboxes', + '#default_value' => array_filter($status_defaults), + '#options' => [ + 'enabled' => t('Enabled'), + 'disabled' => t('Disabled'), + 'unavailable' => t('Unavailable'), + ], + ], + ]; + + $state = \Drupal::state(); + $recent = $state->get('module_filter.recent') ?: []; + + // Remove recent items older than a week. + $recent = array_filter($recent, function ($val) { + return !($val < REQUEST_TIME - 60 * 60 * 24 * 7); + }); + $state->set('module_filter.recent', $recent); + + if (!empty($recent)) { + foreach ($recent as $module => $time) { + foreach (Element::children($form['modules']) as $package) { + if (isset($form['modules'][$package][$module])) { + $form['modules'][$package][$module]['#attributes']['class'][] = 'recent'; + break; + } + } + } + } + + $modules = system_rebuild_module_data(); + + foreach ($modules as $name => $module) { + if ($name == 'module_filter') { + $ctime = filectime($module->getPathname()); + if (($ctime - strtotime('-1 week')) > 0) { + foreach (Element::children($form['modules']) as $package) { + if (isset($form['modules'][$package][$name])) { + $form['modules'][$package][$name]['#attributes']['class'][] = 'new'; + break; + } + } + } + } + } + + $form['#submit'][] = 'module_filter_system_modules_recent_enabled_submit'; + $form['#submit'][] = 'module_filter_system_modules_redirect_submit'; +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function module_filter_form_system_modules_confirm_form_alter(&$form, FormStateInterface $form_state, $form_id) { + $form['filters']['text'] = [ + '#type' => 'value', + '#value' => isset($_GET['filter']) ? $_GET['filter'] : '', + ]; + $form['#submit'][] = 'module_filter_system_modules_redirect_submit'; +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function module_filter_form_system_modules_uninstall_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { + $key = array_search('system/drupal.system.modules', $form['#attached']['library']); + if ($key !== FALSE) { + unset($form['#attached']['library'][$key]); + } + $form['#attached']['library'][] = 'module_filter/modules.uninstall'; + unset($form['filters']['text']['#description']); + $form['filters']['text']['#placeholder'] = t('Filter by name'); + if (!empty($_GET['filter'])) { + $form['filters']['text']['#default_value'] = $_GET['filter']; + } +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function module_filter_form_user_admin_permissions_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { + $form['filters'] = [ + '#type' => 'container', + '#attributes' => [ + 'class' => ['table-filter', 'js-show'], + ], + ]; + $form['filters']['text'] = [ + '#type' => 'search', + '#title' => t('Filter modules'), + '#title_display' => 'invisible', + '#size' => 30, + '#placeholder' => t('Filter by name'), + '#attributes' => [ + 'class' => ['table-filter-text'], + 'data-table' => '#permissions', + 'autocomplete' => 'off', + ], + '#weight' => -1000, + ]; + if (!empty($_GET['filter'])) { + $form['filters']['text']['#default_value'] = $_GET['filter']; + } + $form['#attached']['library'][] = 'module_filter/permissions'; +} + +/** + * Implements hook_theme_registry_alter(). + */ +function module_filter_theme_registry_alter(&$theme_registry) { + // We need to alter the system-modules-details template so we can add + // applicable requires and required-by classes. + $theme_registry['system_modules_details']['path'] = drupal_get_path('module', 'module_filter') . '/templates'; +} + +/** + * Implements hook_preprocess_HOOK(). + */ +function module_filter_preprocess_system_modules_details(&$variables) { +} + +/** + * Form submit callback to track recently enabled modules. + */ +function module_filter_system_modules_recent_enabled_submit($form, FormStateInterface $form_state) { + $state = \Drupal::state(); + $recent = $state->get('module_filter.recent') ?: []; + + // Drupal 8.3.0 simplified the module form structure which requires checking + // the version of Drupal and building the $modules array accordingly. + // @see https://www.drupal.org/node/2851653 + $modules = []; + if (version_compare(\DRUPAL::VERSION, '8.3.0', '<')) { + foreach ($form_state->getValue('modules') as $package) { + $modules += $package; + } + } + else { + $modules = $form_state->getValue('modules'); + } + + foreach (Element::children($form['modules']) as $package) { + foreach ($modules as $module => $details) { + if (isset($form['modules'][$package][$module]) && $form['modules'][$package][$module]['enable']['#default_value'] != $details['enable']) { + $recent[$module] = REQUEST_TIME; + } + } + } + + $state->set('module_filter.recent', $recent); +} + +/** + * Form submit callback for remembering the filter value. + */ +function module_filter_system_modules_redirect_submit($form, FormStateInterface $form_state) { + if ($text = $form_state->getValue('text')) { + /** @var \Drupal\Core\Url $redirect */ + $route_name = ($redirect = $form_state->getRedirect()) ? $redirect->getRouteName() : 'system.modules_list'; + $form_state->setRedirect($route_name, ['filter' => $text]); + } +} diff --git a/web/modules/module_filter/module_filter.permissions.yml b/web/modules/module_filter/module_filter.permissions.yml new file mode 100644 index 0000000000..fca0cb0082 --- /dev/null +++ b/web/modules/module_filter/module_filter.permissions.yml @@ -0,0 +1,3 @@ +administer module_filter: + title: 'Administer Module Filter' + description: 'Configure how Module Filter performs.' diff --git a/web/modules/module_filter/module_filter.routing.yml b/web/modules/module_filter/module_filter.routing.yml new file mode 100644 index 0000000000..524b2cb4f1 --- /dev/null +++ b/web/modules/module_filter/module_filter.routing.yml @@ -0,0 +1,7 @@ +module_filter.settings: + path: '/admin/config/user-interface/module-filter' + defaults: + _form: '\Drupal\module_filter\Form\ModuleFilterSettingsForm' + _title: 'Module filter settings' + requirements: + _permission: 'administer module_filter' diff --git a/web/modules/module_filter/module_filter.services.yml b/web/modules/module_filter/module_filter.services.yml new file mode 100644 index 0000000000..9bd8edffa4 --- /dev/null +++ b/web/modules/module_filter/module_filter.services.yml @@ -0,0 +1,5 @@ +services: + module_filter.route_subscriber: + class: Drupal\module_filter\Routing\RouteSubscriber + tags: + - { name: event_subscriber } diff --git a/web/modules/module_filter/src/Controller/ModuleFilterUpdateController.php b/web/modules/module_filter/src/Controller/ModuleFilterUpdateController.php new file mode 100644 index 0000000000..7abffa8b1c --- /dev/null +++ b/web/modules/module_filter/src/Controller/ModuleFilterUpdateController.php @@ -0,0 +1,27 @@ +<?php + +namespace Drupal\module_filter\Controller; + +use Drupal\update\Controller\UpdateController; + +/** + * A wrapper controller for injecting the filter into the update status page. + */ +class ModuleFilterUpdateController extends UpdateController { + + /** + * {@inheritdoc} + */ + public function updateStatus() { + $build = [ + '#type' => 'container', + '#attributes' => [ + 'id' => 'update-status', + ], + ]; + $build['module_filter'] = $this->formBuilder()->getForm('Drupal\module_filter\Form\ModuleFilterUpdateStatusForm'); + $build['update_report'] = parent::updateStatus(); + return $build; + } + +} diff --git a/web/modules/module_filter/src/Form/ModuleFilterSettingsForm.php b/web/modules/module_filter/src/Form/ModuleFilterSettingsForm.php new file mode 100644 index 0000000000..fb1ec4b652 --- /dev/null +++ b/web/modules/module_filter/src/Form/ModuleFilterSettingsForm.php @@ -0,0 +1,62 @@ +<?php + +namespace Drupal\module_filter\Form; + +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; + +/** + * Settings form for Module Filter. + */ +class ModuleFilterSettingsForm extends ConfigFormBase { + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'module_filter_settings_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $config = $this->config('module_filter.settings'); + $form = parent::buildForm($form, $form_state); + + $form['modules'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Extend'), + '#description' => $this->t('These are settings pertaining to the Extend pages of the site.'), + '#collapsible' => FALSE, + ]; + $form['modules']['tabs'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Enhance the Extend page with tabs'), + '#description' => $this->t('Provides many enhancements to the Extend page including the use of tabs for packages.'), + '#default_value' => $config->get('tabs'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $values = $form_state->getValues(); + $this->config('module_filter.settings') + ->set('tabs', $values['tabs']) + ->save(); + + parent::submitForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['module_filter.settings']; + } + +} diff --git a/web/modules/module_filter/src/Form/ModuleFilterUpdateStatusForm.php b/web/modules/module_filter/src/Form/ModuleFilterUpdateStatusForm.php new file mode 100644 index 0000000000..eebca5332b --- /dev/null +++ b/web/modules/module_filter/src/Form/ModuleFilterUpdateStatusForm.php @@ -0,0 +1,78 @@ +<?php + +namespace Drupal\module_filter\Form; + +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; + +/** + * A form for filtering the update status report page. + */ +class ModuleFilterUpdateStatusForm extends FormBase { + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'module_filter_update_status_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form['filters'] = [ + '#type' => 'container', + '#attributes' => [ + 'class' => ['table-filter', 'js-show'], + ], + ]; + + $form['filters']['text'] = [ + '#type' => 'search', + '#title' => $this->t('Filter projects'), + '#title_display' => 'invisible', + '#size' => 30, + '#placeholder' => $this->t('Filter by name'), + '#attributes' => [ + 'class' => ['table-filter-text'], + 'data-table' => '#update-status', + 'autocomplete' => 'off', + ], + '#attached' => [ + 'library' => [ + 'module_filter/update.status', + ], + ], + ]; + if (!empty($_GET['filter'])) { + $form['filters']['text']['#default_value'] = $_GET['filter']; + } + + $form['filters']['radios'] = [ + '#type' => 'container', + '#attributes' => [ + 'class' => [ + 'module-filter-status', + ], + ], + 'show' => [ + '#type' => 'radios', + '#default_value' => 'all', + '#options' => [ + 'all' => $this->t('All'), + 'updates' => $this->t('Update available'), + 'security' => $this->t('Security update'), + ], + ], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) {} + +} diff --git a/web/modules/module_filter/src/Routing/RouteSubscriber.php b/web/modules/module_filter/src/Routing/RouteSubscriber.php new file mode 100644 index 0000000000..91454769f4 --- /dev/null +++ b/web/modules/module_filter/src/Routing/RouteSubscriber.php @@ -0,0 +1,22 @@ +<?php + +namespace Drupal\module_filter\Routing; + +use Drupal\Core\Routing\RouteSubscriberBase; +use Symfony\Component\Routing\RouteCollection; + +/** + * Listens to the dynamic route events. + */ +class RouteSubscriber extends RouteSubscriberBase { + + /** + * {@inheritdoc} + */ + public function alterRoutes(RouteCollection $collection) { + if ($route = $collection->get('update.status')) { + $route->setDefault('_controller', 'Drupal\module_filter\Controller\ModuleFilterUpdateController::updateStatus'); + } + } + +} diff --git a/web/modules/module_filter/templates/system-modules-details.html.twig b/web/modules/module_filter/templates/system-modules-details.html.twig new file mode 100644 index 0000000000..144f8609e5 --- /dev/null +++ b/web/modules/module_filter/templates/system-modules-details.html.twig @@ -0,0 +1,76 @@ +{# +/** + * @file + * Default theme implementation for the modules listing page. + * + * Displays a list of all packages in a project. + * + * Available variables: + * - modules: Contains multiple module instances. Each module contains: + * - attributes: Attributes on the row. + * - checkbox: A checkbox for enabling the module. + * - name: The human-readable name of the module. + * - id: A unique identifier for interacting with the details element. + * - enable_id: A unique identifier for interacting with the checkbox element. + * - description: The description of the module. + * - machine_name: The module's machine name. + * - version: Information about the module version. + * - requires: A list of modules that this module requires. + * - required_by: A list of modules that require this module. + * - links: A list of administration links provided by the module. + * + * @see template_preprocess_system_modules_details() + * + * @ingroup themeable + */ +#} +<table class="responsive-enabled" data-striping="1"> + <thead> + <tr> + <th class="checkbox visually-hidden">{{ 'Installed'|t }}</th> + <th class="name visually-hidden">{{ 'Name'|t }}</th> + <th class="description visually-hidden priority-low">{{ 'Description'|t }}</th> + </tr> + </thead> + <tbody> + {% for module in modules %} + {% set zebra = cycle(['odd', 'even'], loop.index0) %} + <tr{{ module.attributes.addClass(zebra) }}> + <td class="checkbox"> + {{ module.checkbox }} + </td> + <td class="module"> + <label id="{{ module.id }}" for="{{ module.enable_id }}" class="module-name table-filter-text-source">{{ module.name }}</label> + </td> + <td class="description expand priority-low"> + <details class="js-form-wrapper form-wrapper" id="{{ module.enable_id }}-description"> + <summary aria-controls="{{ module.enable_id }}-description" role="button" aria-expanded="false"><span class="text module-description">{{ module.description }}</span></summary> + <div class="details-wrapper"> + <div class="details-description"> + <div class="requirements"> + <div class="admin-requirements">{{ 'Machine name: <span dir="ltr" class="table-filter-text-source">@machine-name</span>'|t({'@machine-name': module.machine_name }) }}</div> + {% if module.version %} + <div class="admin-requirements">{{ 'Version: @module-version'|t({'@module-version': module.version }) }}</div> + {% endif %} + {% if module.requires %} + <div class="admin-requirements requires">{{ 'Requires: @module-list'|t({'@module-list': module.requires }) }}</div> + {% endif %} + {% if module.required_by %} + <div class="admin-requirements required-by">{{ 'Required by: @module-list'|t({'@module-list': module.required_by }) }}</div> + {% endif %} + </div> + {% if module.links %} + <div class="links"> + {% for link_type in ['help', 'permissions', 'configure'] %} + {{ module.links[link_type] }} + {% endfor %} + </div> + {% endif %} + </div> + </div> + </details> + </td> + </tr> + {% endfor %} + </tbody> +</table> -- GitLab