diff --git a/composer.json b/composer.json index 7f288ad73..234d84edc 100644 --- a/composer.json +++ b/composer.json @@ -60,6 +60,9 @@ "drupal/markdown": "^1.2", "drupal/metatag": "^1.3", "drupal/name": "^1.0-beta1", + "drupal/page_manager": "^4.0-beta2", + "drupal/panelizer": "^4.0", + "drupal/panels": "^4.2", "drupal/pathauto": "^1.0", "drupal/profile": "^1.0-rc1", "drupal/redirect": "^1.0-beta1", @@ -114,6 +117,12 @@ "docroot/profiles/contrib/{$name}": ["type:drupal-profile"], "docroot/themes/contrib/{$name}": ["type:drupal-theme"], "drush/contrib/{$name}": ["type:drupal-drush"] + }, + "patches": { + "drupal/page_manager": { + "#2876880 - page_variant entity type does not exist when installing or enabling": "https://www.drupal.org/files/issues/2876880-page-varient-cache-2.patch", + "#2868216 - Page variants cannot be selected": "https://www.drupal.org/files/issues/page_manager-page_variants_selection-2868216-7.patch" + } } } } diff --git a/composer.lock b/composer.lock index 81baf0a15..3419ed46d 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": "f52a84c37c743119c514124fe5128f25", + "content-hash": "1089db53e2a25ad475423af87b57ce65", "packages": [ { "name": "alchemy/zippy", @@ -2558,6 +2558,79 @@ "issues": "https://www.drupal.org/project/issues/ctools" } }, + { + "name": "drupal/ctools_block", + "version": "3.0.0", + "require": { + "drupal/core": "~8.0", + "drupal/ctools": "self.version" + }, + "type": "metapackage", + "extra": { + "branch-alias": { + "dev-3.x": "3.x-dev" + }, + "drupal": { + "version": "8.x-3.0", + "datestamp": "1493401742", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "EclipseGc", + "homepage": "https://www.drupal.org/user/61203" + }, + { + "name": "damiankloip", + "homepage": "https://www.drupal.org/user/1037976" + }, + { + "name": "dawehner", + "homepage": "https://www.drupal.org/user/99340" + }, + { + "name": "esmerel", + "homepage": "https://www.drupal.org/user/164022" + }, + { + "name": "japerry", + "homepage": "https://www.drupal.org/user/45640" + }, + { + "name": "merlinofchaos", + "homepage": "https://www.drupal.org/user/26979" + }, + { + "name": "neclimdul", + "homepage": "https://www.drupal.org/user/48673" + }, + { + "name": "sdboyer", + "homepage": "https://www.drupal.org/user/146719" + }, + { + "name": "sun", + "homepage": "https://www.drupal.org/user/54136" + }, + { + "name": "tim.plunkett", + "homepage": "https://www.drupal.org/user/241634" + } + ], + "description": "Provides improvements to blocks that will one day be added to Drupal core.", + "homepage": "https://www.drupal.org/project/ctools", + "support": { + "source": "http://cgit.drupalcode.org/ctools" + } + }, { "name": "drupal/diff", "version": "1.0.0-rc1", @@ -3307,6 +3380,304 @@ "source": "http://cgit.drupalcode.org/name" } }, + { + "name": "drupal/page_manager", + "version": "4.0.0-beta2", + "source": { + "type": "git", + "url": "https://git.drupal.org/project/page_manager", + "reference": "8.x-4.0-beta2" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/page_manager-8.x-4.0-beta2.zip", + "reference": "8.x-4.0-beta2", + "shasum": "29a4dda0f068b5df5971eb8319c675cd8e5c78b3" + }, + "require": { + "drupal/core": "^8.0.5", + "drupal/ctools": "~3" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-4.x": "4.x-dev" + }, + "drupal": { + "version": "8.x-4.0-beta2", + "datestamp": "1493410443", + "security-coverage": { + "status": "not-covered", + "message": "Project has not opted into security advisory coverage!" + } + }, + "patches_applied": { + "#2876880 - page_variant entity type does not exist when installing or enabling": "https://www.drupal.org/files/issues/2876880-page-varient-cache-2.patch", + "#2868216 - Page variants cannot be selected": "https://www.drupal.org/files/issues/page_manager-page_variants_selection-2868216-7.patch" + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "Tim Plunkett", + "homepage": "https://www.drupal.org/u/tim.plunkett", + "role": "Maintainer" + }, + { + "name": "dsnopek", + "homepage": "https://www.drupal.org/user/266527" + }, + { + "name": "japerry", + "homepage": "https://www.drupal.org/user/45640" + }, + { + "name": "tim.plunkett", + "homepage": "https://www.drupal.org/user/241634" + } + ], + "description": "Provides a way to place blocks on a custom page.", + "homepage": "https://www.drupal.org/project/page_manager", + "support": { + "source": "https://git.drupal.org/project/page_manager.git", + "issues": "https://www.drupal.org/project/issues/page_manager", + "irc": "irc://irc.freenode.org/drupal-contribute" + } + }, + { + "name": "drupal/panelizer", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://git.drupal.org/project/panelizer", + "reference": "8.x-4.0" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/panelizer-8.x-4.0.zip", + "reference": "8.x-4.0", + "shasum": "8913d1b782d3f0e48ac957ce1f6d3bc611d524c1" + }, + "require": { + "drupal/core": "*", + "drupal/ctools": ">=3.0.0-beta1", + "drupal/ctools_block": "*", + "drupal/panels": ">=4.0.0-alpha1", + "drupal/panels_ipe": ">=4.0.0-alpha1" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-4.x": "4.x-dev" + }, + "drupal": { + "version": "8.x-4.0", + "datestamp": "1493427125", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "Damien McKenna", + "homepage": "https://www.drupal.org/u/damienmckenna" + }, + { + "name": "Kris Vanderwater", + "homepage": "https://www.drupal.org/u/eclipsegc" + }, + { + "name": "David Snopek", + "homepage": "https://www.drupal.org/u/dsnopek" + }, + { + "name": "Jakob Perry", + "homepage": "https://www.drupal.org/u/japerry" + }, + { + "name": "Earl Miles", + "homepage": "https://www.drupal.org/u/merlinofchaos" + }, + { + "name": "See other contributors", + "homepage": "https://www.drupal.org/node/1072922/committers" + }, + { + "name": "tim.plunkett", + "homepage": "https://www.drupal.org/user/241634" + } + ], + "description": "Allow any entity view mode to be rendered using a Panels display.", + "homepage": "https://www.drupal.org/project/panelizer", + "support": { + "source": "http://git.drupal.org/project/panelizer.git", + "issues": "https://www.drupal.org/project/issues/panelizer", + "irc": "irc://irc.freenode.org/drupal-scotch" + } + }, + { + "name": "drupal/panels", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://git.drupal.org/project/panels", + "reference": "8.x-4.2" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/panels-8.x-4.2.zip", + "reference": "8.x-4.2", + "shasum": "6991377531eafaec09f9c7a31e091592e455137b" + }, + "require": { + "drupal/core": "^8.3", + "drupal/ctools": ">=3.0.0" + }, + "require-dev": { + "drupal/page_manager": "*" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-4.x": "4.x-dev" + }, + "drupal": { + "version": "8.x-4.2", + "datestamp": "1500497642", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "Jakob Perry", + "homepage": "https://www.drupal.org/u/japerry" + }, + { + "name": "Samuel Mortenson", + "homepage": "https://www.drupal.org/u/samuel.mortenson" + }, + { + "name": "See other contributors", + "homepage": "https://www.drupal.org/node/74958/committers" + }, + { + "name": "japerry", + "homepage": "https://www.drupal.org/user/45640" + }, + { + "name": "merlinofchaos", + "homepage": "https://www.drupal.org/user/26979" + }, + { + "name": "neclimdul", + "homepage": "https://www.drupal.org/user/48673" + }, + { + "name": "samuel.mortenson", + "homepage": "https://www.drupal.org/user/2582268" + }, + { + "name": "sdboyer", + "homepage": "https://www.drupal.org/user/146719" + }, + { + "name": "tim.plunkett", + "homepage": "https://www.drupal.org/user/241634" + } + ], + "description": "Core Panels display functions; provides no external UI, at least one other Panels module should be enabled.", + "homepage": "https://www.drupal.org/project/panels", + "support": { + "source": "http://git.drupal.org/project/panels.git", + "issues": "https://www.drupal.org/project/issues/panels", + "irc": "irc://irc.freenode.org/drupal-scotch" + } + }, + { + "name": "drupal/panels_ipe", + "version": "4.2.0", + "require": { + "drupal/core": "*", + "drupal/panels": "self.version" + }, + "type": "metapackage", + "extra": { + "branch-alias": { + "dev-4.x": "4.x-dev" + }, + "drupal": { + "version": "8.x-4.2", + "datestamp": "1500497642", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "EclipseGc", + "homepage": "https://www.drupal.org/user/61203" + }, + { + "name": "Letharion", + "homepage": "https://www.drupal.org/user/373603" + }, + { + "name": "esmerel", + "homepage": "https://www.drupal.org/user/164022" + }, + { + "name": "japerry", + "homepage": "https://www.drupal.org/user/45640" + }, + { + "name": "merlinofchaos", + "homepage": "https://www.drupal.org/user/26979" + }, + { + "name": "neclimdul", + "homepage": "https://www.drupal.org/user/48673" + }, + { + "name": "samuel.mortenson", + "homepage": "https://www.drupal.org/user/2582268" + }, + { + "name": "sdboyer", + "homepage": "https://www.drupal.org/user/146719" + }, + { + "name": "tim.plunkett", + "homepage": "https://www.drupal.org/user/241634" + } + ], + "description": "Panels In-place editor.", + "homepage": "https://www.drupal.org/project/panels", + "support": { + "source": "http://cgit.drupalcode.org/panels" + } + }, { "name": "drupal/pathauto", "version": "1.0.0", diff --git a/docroot/modules/contrib/page_manager/LICENSE.txt b/docroot/modules/contrib/page_manager/LICENSE.txt new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/docroot/modules/contrib/page_manager/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. + + + Copyright (C) + + 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. + + , 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/docroot/modules/contrib/page_manager/PATCHES.txt b/docroot/modules/contrib/page_manager/PATCHES.txt new file mode 100644 index 000000000..495e66e1c --- /dev/null +++ b/docroot/modules/contrib/page_manager/PATCHES.txt @@ -0,0 +1,11 @@ +This file was automatically generated by Composer Patches (https://github.com/cweagans/composer-patches) +Patches applied to this directory: + +#2876880 - page_variant entity type does not exist when installing or enabling +Source: https://www.drupal.org/files/issues/2876880-page-varient-cache-2.patch + + +#2868216 - Page variants cannot be selected +Source: https://www.drupal.org/files/issues/page_manager-page_variants_selection-2868216-7.patch + + diff --git a/docroot/modules/contrib/page_manager/composer.json b/docroot/modules/contrib/page_manager/composer.json new file mode 100644 index 000000000..65710c203 --- /dev/null +++ b/docroot/modules/contrib/page_manager/composer.json @@ -0,0 +1,23 @@ +{ + "name": "drupal/page_manager", + "description": "Provides a way to place blocks on a custom page.", + "type": "drupal-module", + "homepage": "https://www.drupal.org/project/page_manager", + "authors": [ + { + "name": "Tim Plunkett", + "homepage": "https://www.drupal.org/u/tim.plunkett", + "role": "Maintainer" + } + ], + "support": { + "issues": "https://www.drupal.org/project/issues/page_manager", + "irc": "irc://irc.freenode.org/drupal-contribute", + "source": "https://git.drupal.org/project/page_manager.git" + }, + "license": "GPL-2.0+", + "minimum-stability": "dev", + "require": { + "drupal/ctools": "~3" + } +} diff --git a/docroot/modules/contrib/page_manager/config/install/page_manager.page.node_view.yml b/docroot/modules/contrib/page_manager/config/install/page_manager.page.node_view.yml new file mode 100644 index 000000000..49111c81b --- /dev/null +++ b/docroot/modules/contrib/page_manager/config/install/page_manager.page.node_view.yml @@ -0,0 +1,15 @@ +langcode: en +status: true +dependencies: { } +id: node_view +label: 'Node view' +description: 'When enabled, this overrides the default Drupal behavior for displaying nodes at /node/{node}. If you add variants, you may use selection criteria such as node type or language or user access to provide different views of nodes. If no variant is selected, the default Drupal node view will be used. This page only affects nodes viewed as pages, it will not affect nodes viewed in lists or at other locations.' +use_admin_theme: false +path: '/node/{node}' +access_logic: and +access_conditions: { } +parameters: + node: + machine_name: node + type: 'entity:node' + label: Node diff --git a/docroot/modules/contrib/page_manager/config/schema/page_manager.schema.yml b/docroot/modules/contrib/page_manager/config/schema/page_manager.schema.yml new file mode 100644 index 000000000..1fe346439 --- /dev/null +++ b/docroot/modules/contrib/page_manager/config/schema/page_manager.schema.yml @@ -0,0 +1,141 @@ +page_manager.page.*: + type: config_entity + label: 'Page' + mapping: + id: + type: string + label: 'Machine-readable name' + label: + type: label + label: 'Label' + description: + type: text + label: 'Description' + use_admin_theme: + type: boolean + label: 'Whether the page is displayed using the admin theme or not' + path: + type: string + label: 'Page path' + access_logic: + type: string + label: 'Access logic' + access_conditions: + type: sequence + label: 'Access Conditions' + sequence: + - type: condition.plugin.[id] + label: 'Access Condition' + parameters: + type: sequence + label: Parameter context list + sequence: + - type: mapping + label: Parameter context + mapping: + machine_name: + type: string + label: 'Machine-readable name' + label: + type: label + label: 'Label of the context' + type: + type: string + label: 'Context type' + +page_manager.page_variant.*: + type: config_entity + label: 'Page variant configuration' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + weight: + type: integer + label: 'Weight' + uuid: + type: string + label: 'UUID' + variant: + type: string + label: 'Display variant' + variant_settings: + type: display_variant.plugin.[%parent.variant] + label: 'Variant settings' + page: + type: string + label: 'Parent page' + selection_criteria: + type: sequence + label: 'Selection criteria' + sequence: + - type: condition.plugin.[id] + label: 'Selection condition' + selection_logic: + type: string + label: 'Selection logic' + static_context: + type: sequence + label: Static context list + sequence: + - type: ctools.context + label: 'Static context' + +page_manager.block_plugin.*: + type: block.settings.[id] + mapping: + region: + type: string + label: 'Region' + weight: + type: integer + label: 'Weight' + uuid: + type: string + label: 'UUID' + context_mapping: + type: sequence + label: 'Context assignments' + sequence: + - type: string + +# @todo Move to core in https://www.drupal.org/node/2838130. +display_variant.plugin.*: + type: display_variant.plugin + label: 'Variant settings' +condition.plugin.*: + type: condition.plugin + label: 'Condition settings' + +display_variant.plugin.block_display: + type: display_variant.plugin + label: 'Block variant plugin' + mapping: + selection_logic: + type: string + label: 'Selection logic' + selection_conditions: + type: sequence + label: 'Selection Conditions' + sequence: + - type: condition.plugin.[id] + label: 'Selection Condition' + blocks: + type: sequence + label: 'Blocks' + sequence: + - type: page_manager.block_plugin.[id] + page_title: + type: label + label: 'Page title' + +display_variant.plugin.http_status_code: + type: display_variant.plugin + label: 'HTTP status code variant plugin' + mapping: + status_code: + type: integer + label: 'Status code' diff --git a/docroot/modules/contrib/page_manager/page_manager.info.yml b/docroot/modules/contrib/page_manager/page_manager.info.yml new file mode 100644 index 000000000..89e62568a --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager.info.yml @@ -0,0 +1,15 @@ +type: module +name: Page Manager +description: 'Provides a way to place blocks on a custom page.' +package: Layout +# core: 8.x +dependencies: + - system (>=8.0.5) + - block + - ctools + +# Information added by Drupal.org packaging script on 2017-04-28 +version: '8.x-4.0-beta2' +core: '8.x' +project: 'page_manager' +datestamp: 1493410446 diff --git a/docroot/modules/contrib/page_manager/page_manager.install b/docroot/modules/contrib/page_manager/page_manager.install new file mode 100644 index 000000000..6180cf93a --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager.install @@ -0,0 +1,57 @@ += 8.0.5. + if (!version_compare(\Drupal::VERSION, '8.0.5', '>=')) { + $requirements['page_manager_core_version'] = [ + 'title' => t('Page manager Drupal core version'), + 'value' => \Drupal::VERSION, + 'description' => t('Page manager requires at least Drupal core 8.0.5.'), + 'severity' => REQUIREMENT_ERROR, + ]; + } + + return $requirements; +} + +/** + * Install the Page Manager UI for existing sites. + */ +function page_manager_update_8001() { + \Drupal::service('module_installer')->install(['page_manager_ui']); +} + +/** + * Rename layout machine names in config entities to match layout discovery's default layouts. + */ +function page_manager_update_8002() { + $names = \Drupal::configFactory()->listAll('page_manager.page_variant'); + foreach ($names as $name) { + $config = \Drupal::configFactory()->getEditable($name); + if ($config->get('variant') === 'panels_variant') { + module_load_install('panels'); + + if (!function_exists('panels_convert_plugin_ids_to_layout_discovery')) { + throw new \Exception('Panels helper function does not exist, the latest Panels 4.x-dev snapshot is required to run this update.'); + } + + $layout_id = $config->get('variant_settings.layout'); + if ($new_layout_id = panels_convert_plugin_ids_to_layout_discovery($layout_id)) { + $config->set('variant_settings.layout', $new_layout_id); + $config->save(); + } + } + } +} diff --git a/docroot/modules/contrib/page_manager/page_manager.permissions.yml b/docroot/modules/contrib/page_manager/page_manager.permissions.yml new file mode 100644 index 000000000..f0c488ce6 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager.permissions.yml @@ -0,0 +1 @@ +administer pages: 'Administer pages' diff --git a/docroot/modules/contrib/page_manager/page_manager.services.yml b/docroot/modules/contrib/page_manager/page_manager.services.yml new file mode 100644 index 000000000..33bce445d --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager.services.yml @@ -0,0 +1,40 @@ +services: + page_manager.current_user_context: + class: Drupal\page_manager\EventSubscriber\CurrentUserContext + arguments: ['@current_user', '@entity_type.manager'] + tags: + - { name: 'event_subscriber' } + page_manager.route_param_context: + class: Drupal\page_manager\EventSubscriber\RouteParamContext + arguments: ['@router.route_provider', '@request_stack'] + tags: + - { name: 'event_subscriber' } + page_manager.language_interface_context: + class: Drupal\page_manager\EventSubscriber\LanguageInterfaceContext + arguments: ['@context.repository'] + tags: + - { name: 'event_subscriber' } + page_manager.context_mapper: + class: Drupal\page_manager\ContextMapper + arguments: ['@entity.repository'] + page_manager.page_manager_routes: + class: Drupal\page_manager\Routing\PageManagerRoutes + arguments: ['@entity_type.manager', '@cache_tags.invalidator'] + tags: + - { name: 'event_subscriber' } + page_manager.variant_route_filter: + class: Drupal\page_manager\Routing\VariantRouteFilter + arguments: ['@entity_type.manager', '@path.current', '@request_stack'] + tags: + # Run as late as possible to allow all other filters to run first. + - { name: non_lazy_route_filter, priority: -1024 } + - { name: service_collector, tag: non_lazy_route_enhancer, call: addRouteEnhancer } + page_manager.route_name_response_subscriber: + class: Drupal\page_manager\EventSubscriber\RouteNameResponseSubscriber + tags: + - { name: event_subscriber } + arguments: ['@current_route_match'] + page_manager.page_access_check: + class: Drupal\page_manager\Entity\PageAccessCheck + tags: + - { name: access_check, applies_to: _page_access } diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/css/page-manager-ui-page-variants.css b/docroot/modules/contrib/page_manager/page_manager_ui/css/page-manager-ui-page-variants.css new file mode 100644 index 000000000..caee1c550 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/css/page-manager-ui-page-variants.css @@ -0,0 +1,28 @@ +/* Add Style Improvements for Page Variant admin page. */ + +.page-manager-wizard-tree label { + font-weight: normal; +} +.page-manager-wizard-tree .page__section__2 { + display: none; +} +.page-manager-wizard-tree .page__section__2.active { + display: inherit; +} +.page-manager-wizard-tree .page__section_item__1 > .page__section__label { + color: #0074bd; + text-decoration: none; +} +.page-manager-wizard-tree .page__section_item__1 > .page__section__label:hover { + cursor: pointer; + text-decoration: underline; +} +.page-manager-wizard-tree .page__section_item__1 > .page__section__label.active { + font-weight: bold; +} +.page-manager-wizard-tree .page__section_item__1 > .page__section__label.current_variant:before { + content: '\00bb'; + font-weight: bold; + color: black; + font-size: 18px; +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/css/page-manager-ui.admin.css b/docroot/modules/contrib/page_manager/page_manager_ui/css/page-manager-ui.admin.css new file mode 100644 index 000000000..e82c3770e --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/css/page-manager-ui.admin.css @@ -0,0 +1,113 @@ +/** + * @file + * Styles for Page Manager admin. + */ + +/* Narrow screens */ + +.page-manager-wizard-tree, +.page-manager-wizard-form { + box-sizing: border-box; +} + +/** + * Wizard actions across the top. + */ +.page-manager-wizard-actions { + text-align: right; /* LTR */ +} +.page-manager-wizard-actions ul.inline, +.page-manager-wizard-actions ul.inline li { + display: inline-block; + margin: 0; +} +.page-manager-wizard-actions ul.inline { + border-top: 1px solid black; + border-left: 1px solid black; +} +.page-manager-wizard-actions ul.inline li { + border-right: 1px solid black; + padding: .5em; +} + +/** + * The tree of wizard steps. + */ +.page-manager-wizard-tree ul { + margin: 0; + padding: 0; + list-style: none; +} +.page-manager-wizard-tree ul > li > ul { + margin-left: 1em; +} +.page-manager-wizard-tree > ul { + border: 1px solid black; + padding-bottom: .5em; + margin-bottom: 20px; +} +.page-manager-wizard-tree li { + border-bottom: 1px solid black; + padding: .5em; + padding-right: 0; +} +.page-manager-wizard-tree li:last-child { + border-bottom: 0; + padding-bottom: 0; +} + +/** + * The wizard form. + */ +.page-manager-wizard-form { + border: 1px solid black; + padding: 1em; + margin-bottom: 20px; +} + +/* Wide screens */ +@media + screen and (min-width: 780px), + (orientation: landscape) and (min-device-height: 780px) { + + /** + * Overall layout. + */ + .page-manager-wizard-tree { + float: left; /* LTR */ + width: 20%; + } + .page-manager-wizard-form { + float: left; /* LTR */ + width: 80%; + } + .page-manager-wizard-form-actions { + margin-left: 20%; /* LTR */ + } + + /** + * Make the borders look nice. + */ + .page-manager-wizard-tree > ul { + border-right: 0; /* LTR */ + } + .page-manager-wizard-form { + min-height: 700px; + } + + /** + * Right-to-left support. + */ + [dir="rtl"] .page-manager-wizard-tree, + [dir="rtl"] .page-manager-wizard-form { + float: right; + } + [dir="rtl"] .page-manager-wizard-form-actions { + margin-left: 0; + margin-right: 20%; + } + [dir="rtl"] .page-manager-wizard-tree > ul { + border-right: 1px solid black; + border-left: 0; + } +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/js/page-manager-ui-page-variants.js b/docroot/modules/contrib/page_manager/page_manager_ui/js/page-manager-ui-page-variants.js new file mode 100644 index 000000000..b5b8896d8 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/js/page-manager-ui-page-variants.js @@ -0,0 +1,27 @@ +/** + * @file + * Add open/close behavior to Page Manager page variants admin page. + */ +(function ($, Drupal) { + Drupal.behaviors.pageManagerVariantsAdmin = { + attach: function (context, settings) { + // When the page loads find the Current Variant and activate its label and content. + $('.current_variant').parents('.page__section__2').addClass('active'); + $('.current_variant').parents('.page__section__2').siblings('.page__section__label').addClass('active current_variant'); + + // When the label is clicked show the variant settings. + $('.page__section_item__1 > .page__section__label').once().click(function() { + if ($(this).hasClass('active')) { + $(this).removeClass('active'); + $(this).siblings('.page__section__2').removeClass('active'); + } + else { + $('.page__section_item__1 > label').removeClass('active'); + $('.page__section__2').removeClass('active'); + $(this).addClass('active'); + $(this).siblings('.page__section__2').addClass('active'); + } + }); + } + }; +})(window.jQuery, window.Drupal); diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.info.yml b/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.info.yml new file mode 100644 index 000000000..43166adbf --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.info.yml @@ -0,0 +1,13 @@ +type: module +name: Page Manager UI +description: 'Provides a simple UI for Page Manager.' +package: Layout +# core: 8.x +dependencies: + - page_manager + +# Information added by Drupal.org packaging script on 2017-04-28 +version: '8.x-4.0-beta2' +core: '8.x' +project: 'page_manager' +datestamp: 1493410446 diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.libraries.yml b/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.libraries.yml new file mode 100644 index 000000000..e71784564 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.libraries.yml @@ -0,0 +1,15 @@ +admin: + version: VERSION + css: + layout: + css/page-manager-ui.admin.css: {} +page_variants: + version: VERSION + css: + layout: + css/page-manager-ui-page-variants.css: {} + js: + js/page-manager-ui-page-variants.js: {} + dependencies: + - core/jquery + - core/jquery.once diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.links.action.yml b/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.links.action.yml new file mode 100644 index 000000000..8e9eec568 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.links.action.yml @@ -0,0 +1,5 @@ +entity.page.add_form: + route_name: entity.page.add_form + title: 'Add page' + appears_on: + - 'entity.page.collection' diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.links.menu.yml b/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.links.menu.yml new file mode 100644 index 000000000..38706716b --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.links.menu.yml @@ -0,0 +1,5 @@ +entity.page.collection: + title: 'Pages' + parent: system.admin_structure + description: 'Manage pages.' + route_name: entity.page.collection diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.links.task.yml b/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.links.task.yml new file mode 100644 index 000000000..ab49bb141 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.links.task.yml @@ -0,0 +1,4 @@ +entity.page.collection: + route_name: entity.page.collection + base_route: entity.page.collection + title: 'Pages' diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.module b/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.module new file mode 100644 index 000000000..4c0b6502c --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.module @@ -0,0 +1,137 @@ +setListBuilderClass(PageListBuilder::class) + ->setFormClass('delete', PageDeleteForm::class) + ->setLinkTemplate('collection', '/admin/structure/page_manager') + ->setLinkTemplate('add-form', '/admin/structure/page_manager/add') + ->setLinkTemplate('edit-form', '/admin/structure/page_manager/manage/{machine_name}/{step}') + ->setLinkTemplate('delete-form', '/admin/structure/page_manager/manage/{page}/delete') + ->setLinkTemplate('enable', '/admin/structure/page_manager/manage/{page}/enable') + ->setLinkTemplate('disable', '/admin/structure/page_manager/manage/{page}/disable') + ->setHandlerClass('wizard', [ + 'add' => PageAddWizard::class, + 'edit' => PageEditWizard::class, + ]); + + $entity_types['page_variant'] + // The edit-form template is required by config_translation. + ->setLinkTemplate('edit-form', '/admin/structure/page_manager/manage/{machine_name}/{step}') + ->setHandlerClass('wizard', [ + 'add_variant' => PageVariantAddWizard::class, + ]); +} + +/** + * Implements hook_entity_type_alter(). + */ +function page_manager_ui_entity_type_alter(array &$entity_types) { + // Change the URL for page config translation overview to outside the wizard. + if ($entity_types['page']->hasLinkTemplate('config-translation-overview')) { + $entity_types['page']->setLinkTemplate('config-translation-overview', str_replace('manage/{machine_name}/{step}', '{page}', $entity_types['page']->getLinkTemplate('config-translation-overview'))); + } + + // Change the URL for page variant config translation overview to outside the + // wizard. + if ($entity_types['page_variant']->hasLinkTemplate('config-translation-overview')) { + $entity_types['page_variant']->setLinkTemplate('config-translation-overview', str_replace('manage/{machine_name}/{step}', '{page}/{page_variant}', $entity_types['page_variant']->getLinkTemplate('config-translation-overview'))); + } +} + +/** + * Implements hook_theme(). + */ +function page_manager_ui_theme() { + return [ + 'page_manager_wizard_form' => [ + 'render element' => 'form', + ], + 'page_manager_wizard_tree' => [ + 'variables' => [ + 'wizard' => NULL, + 'cached_values' => [], + 'tree' => [], + 'divider' => ' ยป ', + 'step' => NULL, + ], + ], + ]; +} + +/** + * Preprocess function for page-manager-wizard-tree.html.twig. + */ +function template_preprocess_page_manager_wizard_tree(&$variables) { + /** @var $wizard \Drupal\ctools\Wizard\FormWizardInterface|\Drupal\ctools\Wizard\EntityFormWizardInterface */ + $wizard = $variables['wizard']; + $cached_values = $variables['cached_values']; + $tree = $variables['tree']; + $variables['step'] = $wizard->getStep($cached_values); + + foreach ($wizard->getOperations($cached_values) as $step => $operation) { + $parameters = $wizard->getNextParameters($cached_values); + // Override step to be the step we want. + $parameters['step'] = $step; + + // Fill in parents if there are breadcrumbs. + $parent =& $tree; + if (isset($operation['breadcrumbs'])) { + foreach ($operation['breadcrumbs'] as $breadcrumb) { + $breadcrumb_string = (string) $breadcrumb; + if (!isset($parent[$breadcrumb_string])) { + $parent[$breadcrumb_string] = [ + 'title' => $breadcrumb, + 'children' => [], + ]; + } + $parent =& $parent[$breadcrumb_string]['children']; + } + } + + $parent[$step] = [ + 'title' => !empty($operation['title']) ? $operation['title'] : '', + 'url' => new \Drupal\Core\Url($wizard->getRouteName(), $parameters), + 'step' => $step, + ]; + } + + $variables['tree'] = $tree; +} + +/** + * Implements hook_config_translation_info_alter(). + */ +function page_manager_ui_config_translation_info_alter(&$info) { + // Alter page and page variant config translation classes. + $info['page']['class'] = PageConfigMapper::class; + $info['page_variant']['class'] = PageVariantConfigMapper::class; + $info['page_variant']['base_route_name'] = 'entity.page.edit_form'; +} + +/** + * Implements hook_local_tasks_alter(). + */ +function page_manager_ui_local_tasks_alter(&$local_tasks) { + // Remove local tasks for page and page variant config translation overview + // routes. + unset($local_tasks['config_translation.local_tasks:entity.page.config_translation_overview']); + unset($local_tasks['config_translation.local_tasks:entity.page_variant.config_translation_overview']); +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.routing.yml b/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.routing.yml new file mode 100644 index 000000000..fe03acddc --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/page_manager_ui.routing.yml @@ -0,0 +1,303 @@ +#### Pages + +entity.page.collection: + path: '/admin/structure/page_manager' + defaults: + _entity_list: 'page' + _title: 'Pages' + requirements: + _permission: 'administer pages' + +entity.page.add_form: + path: '/admin/structure/page_manager/add' + defaults: + _entity_wizard: 'page.add' + _title: 'Add new page' + tempstore_id: page_manager.page + requirements: + _entity_create_access: page + +entity.page.add_step_form: + path: '/admin/structure/page_manager/add/{machine_name}/{step}' + defaults: + _entity_wizard: 'page.add' + _title: 'Add new page' + tempstore_id: page_manager.page + requirements: + _entity_create_access: page + +entity.page.edit_form: + path: '/admin/structure/page_manager/manage/{machine_name}/{step}' + defaults: + _entity_wizard: 'page.edit' + _title_callback: '\Drupal\page_manager_ui\Controller\PageManagerController::editPageTitle' + tempstore_id: page_manager.page + page: '{machine_name}' + options: + parameters: + page: + type: tempstore:page + requirements: + _permission: 'administer pages' + +entity.page.delete_form: + path: '/admin/structure/page_manager/manage/{page}/delete' + defaults: + _entity_form: 'page.delete' + _title: 'Delete page' + requirements: + _permission: 'administer pages' + +entity.page.enable: + path: '/admin/structure/page_manager/manage/{page}/enable' + defaults: + _controller: '\Drupal\page_manager_ui\Controller\PageManagerController::performPageOperation' + op: 'enable' + requirements: + _permission: 'administer pages' + +entity.page.disable: + path: '/admin/structure/page_manager/manage/{page}/disable' + defaults: + _controller: '\Drupal\page_manager_ui\Controller\PageManagerController::performPageOperation' + op: 'disable' + requirements: + _permission: 'administer pages' + +entity.page.reorder_variants_form: + path: '/admin/structure/page_manager/manage/{machine_name}/reorder_variants' + defaults: + _title: 'Reorder variants' + _form: '\Drupal\page_manager_ui\Form\PageReorderVariantsForm' + requirements: + _permission: 'administer pages' + +#### Access Conditions + +entity.page.condition.add: + path: '/admin/structure/page_manager/manage/{machine_name}/access/add/{condition}' + defaults: + _form: '\Drupal\page_manager_ui\Form\AccessConfigure' + _title: 'Add access condition' + tempstore_id: page_manager.page + requirements: + _permission: 'administer pages' + +entity.page.condition.edit: + path: '/admin/structure/page_manager/manage/{machine_name}/access/edit/{condition}' + defaults: + _form: '\Drupal\page_manager_ui\Form\AccessConfigure' + _title: 'Edit access condition' + tempstore_id: page_manager.page + requirements: + _permission: 'administer pages' + +entity.page.condition.delete: + path: '/admin/structure/page_manager/manage/{machine_name}/access/delete/{id}' + defaults: + _form: '\Drupal\page_manager_ui\Form\AccessDelete' + _title: 'Delete access condition' + tempstore_id: page_manager.page + requirements: + _permission: 'administer pages' + +#### Parameters + +page_manager.parameter.edit: + path: '/admin/structure/page_manager/manage/{machine_name}/parameter/edit/{name}' + defaults: + _form: '\Drupal\page_manager_ui\Form\ParameterEditForm' + _title_callback: '\Drupal\page_manager_ui\Controller\PageManagerController::editParameterTitle' + tempstore_id: page_manager.page + page: '{machine_name}' + options: + parameters: + page: + type: tempstore:page + requirements: + _entity_access: page.update + +#### Variants + +page_manager.variant_select: + path: '/admin/structure/page_manager/manage/{machine_name}/add' + defaults: + _controller: '\Drupal\page_manager_ui\Controller\PageManagerController::selectVariant' + _title: 'Select variant' + requirements: + _permission: 'administer pages' + +entity.page_variant.add_form: + path: '/admin/structure/page_manager/manage/{page}/add_variant' + defaults: + _entity_wizard: 'page_variant.add_variant' + _title: 'Add page variant' + tempstore_id: page_manager.page_variant + requirements: + _permission: 'administer pages' + +entity.page_variant.add_step_form: + path: '/admin/structure/page_manager/manage/{page}/add_variant/{machine_name}/{step}' + defaults: + _entity_wizard: 'page_variant.add_variant' + _title: 'Add page variant' + tempstore_id: page_manager.page_variant + requirements: + _permission: 'administer pages' + +entity.page_variant.add_step_form.condition.add: + path: '/admin/structure/page_manager/manage/{page}/add_variant/{machine_name}/selection/add/{condition}' + defaults: + _form: '\Drupal\page_manager_ui\Form\AddVariantSelectionConfigure' + _title: 'Add new selection condition' + tempstore_id: page_manager.page_variant + requirements: + _permission: 'administer pages' + +entity.page_variant.add_step_form.condition.edit: + path: '/admin/structure/page_manager/manage/{page}/add_variant/{machine_name}/selection/edit/{condition}' + defaults: + _form: '\Drupal\page_manager_ui\Form\AddVariantSelectionConfigure' + _title: 'Add new selection condition' + tempstore_id: page_manager.page_variant + requirements: + _permission: 'administer pages' + +entity.page_variant.add_step_form.condition.delete: + path: '/admin/structure/page_manager/manage/{page}/add_variant/{machine_name}/selection/delete/{id}' + defaults: + _form: '\Drupal\page_manager_ui\Form\AddVariantSelectionDelete' + tempstore_id: page_manager.page_variant + _title: 'Delete selection condition' + requirements: + _permission: 'administer pages' + +entity.page_variant.add_step_form.context.add: + path: '/admin/structure/page_manager/manage/{page}/add_variant/{machine_name}/contexts/add/{context_id}' + defaults: + _form: '\Drupal\page_manager_ui\Form\AddVariantStaticContextConfigure' + _title: 'Add custom context' + tempstore_id: page_manager.page_variant + requirements: + _permission: 'administer pages' + +entity.page_variant.add_step_form.context.edit: + path: '/admin/structure/page_manager/manage/{page}/add_variant/{machine_name}/contexts/edit/{context_id}' + defaults: + _form: '\Drupal\page_manager_ui\Form\AddVariantStaticContextConfigure' + _title: 'Edit context' + tempstore_id: page_manager.page_variant + requirements: + _permission: 'administer pages' + +entity.page_variant.add_step_form.context.delete: + path: '/admin/structure/page_manager/manage/{page}/add_variant/{machine_name}/context/delete/{context_id}' + defaults: + _form: '\Drupal\page_manager_ui\Form\AddVariantStaticContextDeleteForm' + _title: 'Delete static context' + tempstore_id: page_manager.page_variant + requirements: + _permission: 'administer pages' + +entity.page_variant.delete_form: + path: '/admin/structure/page_manager/manage/{machine_name}/variant/{variant_machine_name}/delete' + defaults: + _form: '\Drupal\page_manager_ui\Form\PageVariantDeleteForm' + _title: 'Delete page variant' + tempstore_id: page_manager.page + requirements: + _permission: 'administer pages' + +page_manager.block_display_select_block: + path: '/admin/structure/page_manager/block_display/{block_display}/select' + defaults: + _controller: '\Drupal\page_manager_ui\Controller\PageManagerController::selectBlock' + _title: 'Select block' + tempstore_id: 'page_manager.block_display' + requirements: + _ctools_access: 'block_display' + +page_manager.block_display_add_block: + path: '/admin/structure/page_manager/block_display/{block_display}/add/{block_id}' + defaults: + _form: '\Drupal\page_manager_ui\Form\VariantPluginAddBlockForm' + _title: 'Add block' + tempstore_id: 'page_manager.block_display' + requirements: + _ctools_access: 'block_display' + +page_manager.block_display_edit_block: + path: '/admin/structure/page_manager/block_display/{block_display}/edit/{block_id}' + defaults: + _form: '\Drupal\page_manager_ui\Form\VariantPluginEditBlockForm' + _title: 'Edit block' + tempstore_id: 'page_manager.block_display' + requirements: + _ctools_access: 'block_display' + +page_manager.block_display_delete_block: + path: '/admin/structure/page_manager/block_display/{block_display}/delete/{block_id}' + defaults: + _form: '\Drupal\page_manager_ui\Form\VariantPluginDeleteBlockForm' + _title: 'Delete block' + tempstore_id: 'page_manager.block_display' + requirements: + _ctools_access: 'block_display' + +#### Static Contexts + +entity.page_variant.context.add: + path: '/admin/structure/page_manager/manage/{machine_name}/variant/{variant_machine_name}/contexts/add/{context_id}' + defaults: + _form: '\Drupal\page_manager_ui\Form\StaticContextConfigure' + _title: 'Add custom context' + tempstore_id: page_manager.page + requirements: + _permission: 'administer pages' + +entity.page_variant.context.edit: + path: '/admin/structure/page_manager/manage/{machine_name}/variant/{variant_machine_name}/contexts/edit/{context_id}' + defaults: + _form: '\Drupal\page_manager_ui\Form\StaticContextConfigure' + _title: 'Edit context' + tempstore_id: page_manager.page + requirements: + _permission: 'administer pages' + +entity.page_variant.context.delete: + path: '/admin/structure/page_manager/manage/{machine_name}/variant/{variant_machine_name}/context/delete/{context_id}' + defaults: + _form: '\Drupal\page_manager_ui\Form\StaticContextDeleteForm' + _title: 'Delete static context' + tempstore_id: page_manager.page + requirements: + _permission: 'administer pages' + +#### Selection Conditions + +entity.page_variant.condition.add: + path: '/admin/structure/page_manager/manage/{machine_name}/variant/{variant_machine_name}/selection/add/{condition}' + defaults: + _form: '\Drupal\page_manager_ui\Form\SelectionConfigure' + tempstore_id: page_manager.page + _title: 'Add new selection condition' + requirements: + _permission: 'administer pages' + +entity.page_variant.condition.edit: + path: '/admin/structure/page_manager/manage/{machine_name}/variant/{variant_machine_name}/selection/edit/{condition}' + defaults: + _form: '\Drupal\page_manager_ui\Form\SelectionConfigure' + tempstore_id: page_manager.page + _title: 'Edit selection condition' + requirements: + _permission: 'administer pages' + +entity.page_variant.condition.delete: + path: '/admin/structure/page_manager/manage/{machine_name}/variant/{variant_machine_name}/selection/delete/{id}' + defaults: + _form: '\Drupal\page_manager_ui\Form\SelectionDelete' + tempstore_id: page_manager.page + _title: 'Delete selection condition' + requirements: + _permission: 'administer pages' diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Access/PageManagerPluginAccess.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Access/PageManagerPluginAccess.php new file mode 100644 index 000000000..455857a54 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Access/PageManagerPluginAccess.php @@ -0,0 +1,19 @@ +hasPermission('administer pages') ? AccessResult::allowed() : AccessResult::forbidden(); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/ConfigTranslation/PageConfigMapper.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/ConfigTranslation/PageConfigMapper.php new file mode 100644 index 000000000..916d53e73 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/ConfigTranslation/PageConfigMapper.php @@ -0,0 +1,40 @@ +getPath(); + $path = str_replace('manage/{machine_name}/{step}', '{page}', $path); + $route->setPath($path); + } + + /** + * {@inheritdoc} + */ + public function getBaseRouteParameters() { + $parameters = parent::getBaseRouteParameters(); + $parameters['step'] = 'general'; + $parameters['machine_name'] = $parameters['page']; + return $parameters; + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/ConfigTranslation/PageVariantConfigMapper.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/ConfigTranslation/PageVariantConfigMapper.php new file mode 100644 index 000000000..da9947e58 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/ConfigTranslation/PageVariantConfigMapper.php @@ -0,0 +1,76 @@ +getPath(); + $path = str_replace('manage/{machine_name}/{step}', '{page}/{page_variant}', $path); + $route->setPath($path); + } + + /** + * {@inheritdoc} + */ + public function getBaseRouteParameters() { + $parameters = parent::getBaseRouteParameters(); + $parameters['page'] = $this->entity->get('page'); + $parameters['machine_name'] = $parameters['page']; + $parameters['step'] = 'page_variant__' . $parameters['page_variant'] . '__general'; + return $parameters; + } + + /** + * {@inheritdoc} + */ + public function getAddRouteName() { + return $this->alterRouteName(parent::getAddRouteName()); + } + + /** + * {@inheritdoc} + */ + public function getEditRouteName() { + return $this->alterRouteName(parent::getEditRouteName()); + } + + /** + * {@inheritdoc} + */ + public function getDeleteRouteName() { + return $this->alterRouteName(parent::getDeleteRouteName()); + } + + /** + * Alter the route name to be unique from page entity route names. + * + * @param string $name + * Route name for the mapper. + * + * @return string + * Altered route name for the mapper. + */ + protected function alterRouteName($name) { + return str_replace('page', 'page_variant', $name); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Controller/PageManagerController.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Controller/PageManagerController.php new file mode 100644 index 000000000..ebb08a741 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Controller/PageManagerController.php @@ -0,0 +1,384 @@ +blockManager = $block_manager; + $this->conditionManager = $condition_manager; + $this->variantManager = $variant_manager; + $this->contextHandler = $context_handler; + $this->tempstore = $tempstore; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.block'), + $container->get('plugin.manager.condition'), + $container->get('plugin.manager.display_variant'), + $container->get('context.handler'), + $container->get('user.shared_tempstore') + ); + } + + /** + * Route title callback. + * + * @param string $machine_name + * The page's machine_name. + * @param string $tempstore_id + * The temporary store identifier. + * + * @return string + * The title for the page edit form. + */ + public function editPageTitle($machine_name, $tempstore_id) { + $cached_values = $this->tempstore->get($tempstore_id)->get($machine_name); + /** @var \Drupal\page_manager\PageInterface $page */ + $page = $cached_values['page']; + return $this->t('Edit %label page', ['%label' => $page->label()]); + } + + /** + * Route title callback. + * + * @param \Drupal\page_manager\PageVariantInterface $page_variant + * The page variant entity. + * + * @return string + * The title for the page variant edit form. + */ + public function editPageVariantTitle(PageVariantInterface $page_variant) { + return $this->t('Edit %label variant', ['%label' => $page_variant->label()]); + } + + /** + * Route title callback. + * + * @param \Drupal\page_manager\PageInterface $page + * The page entity. + * @param string $condition_id + * The access condition ID. + * + * @return string + * The title for the access condition edit form. + */ + public function editAccessConditionTitle(PageInterface $page, $condition_id) { + $access_condition = $page->getAccessCondition($condition_id); + return $this->t('Edit %label access condition', ['%label' => $access_condition->getPluginDefinition()['label']]); + } + + /** + * Route title callback. + * + * @param \Drupal\page_manager\PageVariantInterface $page_variant + * The page variant entity. + * @param string $condition_id + * The selection condition ID. + * + * @return string + * The title for the selection condition edit form. + */ + public function editSelectionConditionTitle(PageVariantInterface $page_variant, $condition_id) { + $selection_condition = $page_variant->getSelectionCondition($condition_id); + return $this->t('Edit %label selection condition', ['%label' => $selection_condition->getPluginDefinition()['label']]); + } + + /** + * Route title callback. + * + * @param \Drupal\page_manager\PageInterface $page + * The page entity. + * @param string $name + * The parameter context name. + * + * @return string + * The title for the parameter edit form. + */ + public function editParameterTitle(PageInterface $page, $name) { + return $this->t('Edit @label parameter', ['@label' => $page->getParameter($name)['label']]); + } + + /** + * Enables or disables a Page. + * + * @param \Drupal\page_manager\PageInterface $page + * The page entity. + * @param string $op + * The operation to perform, usually 'enable' or 'disable'. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * A redirect back to the pages list page. + */ + public function performPageOperation(PageInterface $page, $op) { + $page->$op()->save(); + + if ($op == 'enable') { + drupal_set_message($this->t('The %label page has been enabled.', ['%label' => $page->label()])); + } + elseif ($op == 'disable') { + drupal_set_message($this->t('The %label page has been disabled.', ['%label' => $page->label()])); + } + + return $this->redirect('entity.page.collection'); + } + + /** + * Presents a list of variants to add to the page entity. + * + * @param \Drupal\page_manager\PageInterface $page + * The page entity. + * + * @return array + * The variant selection page. + */ + public function selectVariant(PageInterface $page) { + $build = [ + '#theme' => 'links', + '#links' => [], + ]; + foreach ($this->variantManager->getDefinitions() as $variant_plugin_id => $variant_plugin) { + // The following two variants are provided by Drupal Core. They are not + // configurable and therefore not compatible with Page Manager but have + // similar and confusing labels. Skip them so that they are not shown in + // the UI. + if (in_array($variant_plugin_id, ['simple_page', 'block_page'])) { + continue; + } + + $build['#links'][$variant_plugin_id] = [ + 'title' => $variant_plugin['admin_label'], + 'url' => Url::fromRoute('entity.page_variant.add_form', [ + 'page' => $page->id(), + 'variant_plugin_id' => $variant_plugin_id, + ]), + 'attributes' => $this->getAjaxAttributes(), + ]; + } + return $build; + } + + /** + * Presents a list of access conditions to add to the page entity. + * + * @param \Drupal\page_manager\PageInterface $page + * The page entity. + * + * @return array + * The access condition selection page. + */ + public function selectAccessCondition(PageInterface $page) { + $build = [ + '#theme' => 'links', + '#links' => [], + ]; + $available_plugins = $this->conditionManager->getDefinitionsForContexts($page->getContexts()); + foreach ($available_plugins as $access_id => $access_condition) { + $build['#links'][$access_id] = [ + 'title' => $access_condition['label'], + 'url' => Url::fromRoute('page_manager.access_condition_add', [ + 'page' => $page->id(), + 'condition_id' => $access_id, + ]), + 'attributes' => $this->getAjaxAttributes(), + ]; + } + return $build; + } + + /** + * Presents a list of selection conditions to add to the page entity. + * + * @param \Drupal\page_manager\PageVariantInterface $page_variant + * The page variant entity. + * + * @return array + * The selection condition selection page. + */ + public function selectSelectionCondition(PageVariantInterface $page_variant) { + $build = [ + '#theme' => 'links', + '#links' => [], + ]; + $available_plugins = $this->conditionManager->getDefinitionsForContexts($page_variant->getContexts()); + foreach ($available_plugins as $selection_id => $selection_condition) { + $build['#links'][$selection_id] = [ + 'title' => $selection_condition['label'], + 'url' => Url::fromRoute('page_manager.selection_condition_add', [ + 'page' => $page_variant->get('page'), + 'page_variant' => $page_variant->id(), + 'condition_id' => $selection_id, + ]), + 'attributes' => $this->getAjaxAttributes(), + ]; + } + return $build; + } + + /** + * Presents a list of blocks to add to the variant. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request. + * @param string $block_display + * The identifier of the block display variant. + * @param string $tempstore_id + * The identifier of the temporary store. + * + * @return array + * The block selection page. + */ + public function selectBlock(Request $request, $block_display, $tempstore_id) { + $cached_values = $this->tempstore->get($tempstore_id)->get($block_display); + /** @var \Drupal\page_manager\Plugin\DisplayVariant\PageBlockDisplayVariant $variant_plugin */ + $variant_plugin = $cached_values['plugin']; + + // Rehydrate the contexts on this end. + $contexts = []; + /** + * @var string $context_name + * @var \Drupal\Core\Plugin\Context\ContextDefinitionInterface $context_definition + */ + foreach ($cached_values['contexts'] as $context_name => $context_definition) { + $contexts[$context_name] = new Context($context_definition); + } + $variant_plugin->setContexts($contexts); + + // Add a section containing the available blocks to be added to the variant. + $build = [ + '#type' => 'container', + '#attached' => [ + 'library' => [ + 'core/drupal.ajax', + ], + ], + ]; + $available_plugins = $this->blockManager->getDefinitionsForContexts($variant_plugin->getContexts()); + // Order by category, and then by admin label. + $available_plugins = $this->blockManager->getSortedDefinitions($available_plugins); + foreach ($available_plugins as $plugin_id => $plugin_definition) { + // Make a section for each region. + $category = $plugin_definition['category']; + $category_key = 'category-' . $category; + if (!isset($build[$category_key])) { + $build[$category_key] = [ + '#type' => 'fieldgroup', + '#title' => $category, + 'content' => [ + '#theme' => 'links', + ], + ]; + } + // Add a link for each available block within each region. + $build[$category_key]['content']['#links'][$plugin_id] = [ + 'title' => $plugin_definition['admin_label'], + 'url' => Url::fromRoute('page_manager.block_display_add_block', [ + 'block_display' => $block_display, + 'block_id' => $plugin_id, + 'region' => $request->query->get('region'), + 'destination' => $request->query->get('destination'), + ]), + 'attributes' => $this->getAjaxAttributes(), + ]; + } + return $build; + } + + /** + * Build the page variant entity add form. + * + * @param \Drupal\page_manager\PageInterface $page + * The page this page variant belongs to. + * @param string $variant_plugin_id + * The variant plugin ID. + * + * @return array + * The page variant entity add form. + */ + public function addPageVariantEntityForm(PageInterface $page, $variant_plugin_id) { + // Create a page variant entity. + $entity = $this->entityTypeManager()->getStorage('page_variant')->create([ + 'page' => $page->id(), + 'variant' => $variant_plugin_id, + ]); + + return $this->entityFormBuilder()->getForm($entity, 'add'); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Entity/PageListBuilder.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Entity/PageListBuilder.php new file mode 100644 index 000000000..8c3ec33e8 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Entity/PageListBuilder.php @@ -0,0 +1,88 @@ +t('Label'); + $header['id'] = $this->t('Machine name'); + $header['path'] = $this->t('Path'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + /** @var \Drupal\page_manager\PageInterface $entity */ + $row['label'] = $entity->label(); + $row['id'] = $entity->id(); + $row['path'] = $this->getPath($entity); + + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + public function getDefaultOperations(EntityInterface $entity) { + $operations = parent::getDefaultOperations($entity); + $operations['edit']['url'] = new Url('entity.page.edit_form', ['machine_name' => $entity->id(), 'step' => 'general']); + + return $operations; + } + + /** + * Gets the displayable path of a page entity. + * + * @param \Drupal\page_manager\PageInterface $entity + * The page entity. + * + * @return array|string + * The value of the path. + */ + protected function getPath(PageInterface $entity) { + // If the page is enabled and not dynamic, show the path as a link, + // otherwise as plain text. + $path = $entity->getPath(); + if ($entity->status() && strpos($path, '%') === FALSE) { + return [ + 'data' => [ + '#type' => 'link', + '#url' => Url::fromUserInput(rtrim($path, '/')), + '#title' => $path, + ], + ]; + } + else { + return $path; + } + } + + /** + * {@inheritdoc} + */ + public function render() { + $build = parent::render(); + $build['table']['#empty'] = $this->t('There are currently no pages. Add a new page.', [':url' => Url::fromRoute('entity.page.add_form')->toString()]); + return $build; + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AccessConfigure.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AccessConfigure.php new file mode 100644 index 000000000..0b9f3d22c --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AccessConfigure.php @@ -0,0 +1,58 @@ +isNew() ? 'entity.page.add_step_form' : 'entity.page.edit_form'; + return [$route_name, [ + 'machine_name' => $this->machine_name, + 'step' => 'access', + ]]; + } + + /** + * {@inheritdoc} + */ + protected function getConditions($cached_values) { + /** @var $page \Drupal\page_manager\PageInterface */ + $page = $cached_values['page']; + return $page->get('access_conditions'); + } + + /** + * {@inheritdoc} + */ + protected function setConditions($cached_values, $conditions) { + /** @var $page \Drupal\page_manager\PageInterface */ + $page = $cached_values['page']; + $page->set('access_conditions', $conditions); + $cached_values['page'] = $page; + return $cached_values; + } + + /** + * {@inheritdoc} + */ + protected function getContexts($cached_values) { + /** @var $page \Drupal\page_manager\PageInterface */ + $page = $cached_values['page']; + return $page->getContexts(); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AccessDelete.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AccessDelete.php new file mode 100644 index 000000000..7961bba1b --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AccessDelete.php @@ -0,0 +1,57 @@ +isNew() ? 'entity.page.add_step_form' : 'entity.page.edit_form'; + return [$route_name, [ + 'machine_name' => $this->machine_name, + 'step' => 'access', + ]]; + } + + /** + * {@inheritdoc} + */ + protected function getConditions($cached_values) { + /** @var $page \Drupal\page_manager\PageInterface */ + $page = $cached_values['page']; + return $page->get('access_conditions'); + } + + /** + * {@inheritdoc} + */ + protected function setConditions($cached_values, $conditions) { + /** @var $page \Drupal\page_manager\PageInterface */ + $page = $cached_values['page']; + $page->set('access_conditions', $conditions); + return $cached_values; + } + + /** + * {@inheritdoc} + */ + protected function getContexts($cached_values) { + /** @var $page \Drupal\page_manager\PageInterface */ + $page = $cached_values['page']; + return $page->getContexts(); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AddVariantContextsForm.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AddVariantContextsForm.php new file mode 100644 index 000000000..2e1af4b4e --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AddVariantContextsForm.php @@ -0,0 +1,129 @@ +getTemporaryValue('wizard'); + /** @var $page_variant \Drupal\page_manager\Entity\PageVariant */ + $page_variant = $cached_values['page_variant']; + $context = $form_state->getValue('context'); + $content = $this->formBuilder->getForm($this->getContextClass(), $context, $this->getTempstoreId(), $this->machine_name, $page_variant->id()); + $content['#attached']['library'][] = 'core/drupal.dialog.ajax'; + list(, $route_parameters) = $this->getContextOperationsRouteInfo($cached_values, $this->machine_name, $context); + $content['submit']['#attached']['drupalSettings']['ajax'][$content['submit']['#id']]['url'] = $this->url($this->getContextAddRoute($cached_values), $route_parameters, ['query' => [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]]); + $response = new AjaxResponse(); + $response->addCommand(new OpenModalDialogCommand($this->t('Add new context'), $content, array('width' => '700'))); + return $response; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'page_manager_variant_context_form'; + } + + /** + * {@inheritdoc} + */ + protected function getContextClass($cached_values) { + return AddVariantStaticContextConfigure::class; + } + + /** + * {@inheritdoc} + */ + protected function getRelationshipClass($cached_values) { + //return AddVariantRelationshipConfigure::class; + } + + /** + * {@inheritdoc} + */ + protected function getContextAddRoute($cached_values) { + return 'entity.page_variant.add_step_form.context.add'; + } + + /** + * {@inheritdoc} + */ + protected function getRelationshipAddRoute($cached_values) { + return 'entity.page_variant.add_step_form.context.add'; + } + + /** + * {@inheritdoc} + */ + protected function getTempstoreId() { + return 'page_manager.page_variant'; + } + + /** + * {@inheritdoc} + */ + protected function getContexts($cached_values) { + /** @var $page_variant \Drupal\page_manager\Entity\PageVariant */ + $page_variant = $cached_values['page_variant']; + return $page_variant->getContexts(); + } + + /** + * {@inheritdoc} + */ + protected function getContextOperationsRouteInfo($cached_values, $machine_name, $row) { + /** @var \Drupal\page_manager\PageVariantInterface $page_variant */ + $page_variant = $cached_values['page_variant']; + return ['entity.page_variant.add_step_form.context', [ + 'page' => $page_variant->getPage()->id(), + 'machine_name' => $machine_name, + 'context_id' => $row + ]]; + } + + /** + * {@inheritdoc} + */ + protected function getRelationshipOperationsRouteInfo($cached_values, $machine_name, $row) { + /** @var \Drupal\page_manager\PageVariantInterface $page_variant */ + $page_variant = $cached_values['page_variant']; + return ['entity.page_variant.add_step_form.context', [ + 'page' => $page_variant->getPage()->id(), + 'machine_name' => $machine_name, + 'context_id' => $row + ]]; + } + + protected function isEditableContext($cached_values, $row) { + /** @var \Drupal\page_manager\PageVariantInterface $page_variant */ + $page_variant = $cached_values['page_variant']; + $page = $page_variant->getPage(); + return empty($page->getContexts()[$row]) && !empty($page_variant->getContexts()[$row]); + } + + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AddVariantSelectionConfigure.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AddVariantSelectionConfigure.php new file mode 100644 index 000000000..854af46aa --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AddVariantSelectionConfigure.php @@ -0,0 +1,64 @@ +getPageVariant($cached_values); + return ['entity.page_variant.add_step_form', [ + 'page' => $page_variant->getPage()->id(), + 'machine_name' => $this->machine_name, + 'step' => 'selection', + ]]; + } + + /** + * {@inheritdoc} + */ + protected function getConditions($cached_values) { + $page_variant = $this->getPageVariant($cached_values); + return $page_variant->get('selection_criteria'); + } + + /** + * {@inheritdoc} + */ + protected function setConditions($cached_values, $conditions) { + $page_variant = $this->getPageVariant($cached_values); + $page_variant->set('selection_criteria', $conditions); + return $cached_values; + } + + /** + * {@inheritdoc} + */ + protected function getContexts($cached_values) { + $page_variant = $this->getPageVariant($cached_values); + return $page_variant->getContexts(); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AddVariantSelectionDelete.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AddVariantSelectionDelete.php new file mode 100644 index 000000000..3ab11c649 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AddVariantSelectionDelete.php @@ -0,0 +1,64 @@ +getPageVariant($cached_values); + return ['entity.page_variant.add_step_form', [ + 'page' => $page_variant->getPage()->id(), + 'machine_name' => $this->machine_name, + 'step' => 'selection', + ]]; + } + + /** + * {@inheritdoc} + */ + protected function getConditions($cached_values) { + $page_variant = $this->getPageVariant($cached_values); + return $page_variant->get('selection_criteria'); + } + + /** + * {@inheritdoc} + */ + protected function setConditions($cached_values, $conditions) { + $page_variant = $this->getPageVariant($cached_values); + $page_variant->set('selection_criteria', $conditions); + return $cached_values; + } + + /** + * {@inheritdoc} + */ + protected function getContexts($cached_values) { + $page_variant = $this->getPageVariant($cached_values); + return $page_variant->getContexts(); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AddVariantSelectionForm.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AddVariantSelectionForm.php new file mode 100644 index 000000000..f80bea39c --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AddVariantSelectionForm.php @@ -0,0 +1,105 @@ + $page_variant->getPage()->id(), + 'machine_name' => $machine_name, + 'condition' => $row + ]]; + } + + /** + * {@inheritdoc} + */ + protected function getConditions($cached_values) { + /** @var $page \Drupal\page_manager\Entity\PageVariant */ + $page_variant = $cached_values['page_variant']; + return $page_variant->get('selection_criteria'); + } + + /** + * {@inheritdoc} + */ + protected function getContexts($cached_values) { + /** @var $page_variant \Drupal\page_manager\Entity\PageVariant */ + $page_variant = $cached_values['page_variant']; + return $page_variant->getContexts(); + } + + /** + * {@inheritdoc} + */ + protected function getAddRoute($cached_values) { + return 'entity.page_variant.add_step_form.condition.add'; + } + + /** + * {@inheritdoc} + */ + public function add(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + $page_variant = $cached_values['page_variant']; + $condition = $form_state->getValue('conditions'); + $content = \Drupal::formBuilder()->getForm($this->getConditionClass(), $condition, $this->getTempstoreId(), $this->machine_name, $page_variant->id()); + $content['#attached']['library'][] = 'core/drupal.dialog.ajax'; + list(, $route_parameters) = $this->getOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('conditions')); + $content['submit']['#attached']['drupalSettings']['ajax'][$content['submit']['#id']]['url'] = $this->url($this->getAddRoute($cached_values), $route_parameters, ['query' => [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]]); + $response = new AjaxResponse(); + $response->addCommand(new OpenModalDialogCommand($this->t('Configure Required Context'), $content, array('width' => '700'))); + return $response; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + if ($triggering_element['#value']->getUntranslatedString() != 'Add Condition') { + return; + } + parent::submitForm($form, $form_state); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AddVariantStaticContextConfigure.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AddVariantStaticContextConfigure.php new file mode 100644 index 000000000..6f33c0f52 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AddVariantStaticContextConfigure.php @@ -0,0 +1,81 @@ +getPageVariant($cached_values); + return ['entity.page_variant.add_step_form', [ + 'page' => $page_variant->getPage()->id(), + 'machine_name' => $this->machine_name, + 'step' => 'contexts', + ]]; + } + + /** + * {@inheritdoc} + */ + protected function getContexts($cached_values) { + return $this->getPageVariant($cached_values)->getContexts(); + } + + /** + * {@inheritdoc} + */ + protected function addContext($cached_values, $context_id, ContextInterface $context) { + /** @var $page_variant \Drupal\page_manager\PageVariantInterface */ + $page_variant = $this->getPageVariant($cached_values); + $context_config = [ + 'label' => $context->getContextDefinition()->getLabel(), + 'type' => $context->getContextDefinition()->getDataType(), + 'description' => $context->getContextDefinition()->getDescription(), + 'value' => strpos($context->getContextDefinition()->getDataType(), 'entity:') === 0 ? $context->getContextValue()->uuid() : $context->getContextValue(), + ]; + $page_variant->setStaticContext($context_id, $context_config); + $cached_values['page_variant'] = $page_variant; + return $cached_values; + } + + /** + * {@inheritdoc} + */ + public function contextExists($value, $element, $form_state) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + protected function disableMachineName($cached_values, $machine_name) { + if ($machine_name) { + return !empty($this->getContexts($cached_values)[$machine_name]); + } + return FALSE; + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AddVariantStaticContextDeleteForm.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AddVariantStaticContextDeleteForm.php new file mode 100644 index 000000000..5adb99dfb --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/AddVariantStaticContextDeleteForm.php @@ -0,0 +1,72 @@ +getTempstore(); + /** @var $page \Drupal\page_manager\PageInterface */ + $page_variant = $this->getPageVariant($cached_values); + return $this->t('Are you sure you want to delete the static context %label?', ['%label' => $page_variant->getStaticContext($this->context_id)['label']]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + $cached_values = $this->getTempstore(); + $page_variant = $this->getPageVariant($cached_values); + return new Url('entity.page_variant.add_step_form', [ + 'page' => $page_variant->getPage()->id(), + 'machine_name' => $this->machine_name, + 'step' => 'contexts', + ]); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $this->getTempstore(); + /** @var $page \Drupal\page_manager\PageInterface */ + $page_variant = $this->getPageVariant($cached_values); + drupal_set_message($this->t('The static context %label has been removed.', ['%label' => $page_variant->getStaticContext($this->context_id)['label']])); + $page_variant->removeStaticContext($this->context_id); + $this->setTempstore($cached_values); + parent::submitForm($form, $form_state); + } + + /** + * Get the page variant. + * + * @param array $cached_values + * The cached values from the wizard. + * + * @return \Drupal\page_manager\PageVariantInterface + */ + protected function getPageVariant($cached_values) { + return $cached_values['page_variant']; + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageAccessForm.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageAccessForm.php new file mode 100644 index 000000000..0f95a6bfb --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageAccessForm.php @@ -0,0 +1,82 @@ + $machine_name, 'condition' => $row]]; + } + + /** + * {@inheritdoc} + */ + protected function getConditions($cached_values) { + /** @var $page \Drupal\page_manager\Entity\Page */ + $page = $cached_values['page']; + return $page->get('access_conditions'); + } + + /** + * {@inheritdoc} + */ + protected function getContexts($cached_values) { + /** @var $page \Drupal\page_manager\Entity\Page */ + $page = $cached_values['page']; + return $page->getContexts(); + } + + /** + * The route to which condition 'add' actions should submit. + * + * @return string + */ + protected function getAddRoute($cached_values) { + return 'entity.page.condition.add'; + } + + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + if ($triggering_element['#value']->getUntranslatedString() == 'Update') { + return; + } + parent::submitForm($form, $form_state); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageDeleteForm.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageDeleteForm.php new file mode 100644 index 000000000..d190cfc1c --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageDeleteForm.php @@ -0,0 +1,49 @@ +t('Are you sure you want to delete the page %name?', ['%name' => $this->entity->label()]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.page.collection'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->entity->delete(); + drupal_set_message($this->t('The page %name has been removed.', ['%name' => $this->entity->label()])); + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageGeneralForm.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageGeneralForm.php new file mode 100644 index 000000000..2cf26b13f --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageGeneralForm.php @@ -0,0 +1,189 @@ +variantManager = $variant_manager; + $this->entityQuery = $entity_query; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.display_variant'), + $container->get('entity.query') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'page_manager_general_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var $page \Drupal\page_manager\Entity\Page */ + $page = $cached_values['page']; + $form['description'] = [ + '#type' => 'textarea', + '#title' => $this->t('Administrative description'), + '#default_value' => $page->getDescription(), + ]; + $form['path'] = [ + '#type' => 'textfield', + '#title' => $this->t('Path'), + '#maxlength' => 255, + '#default_value' => $page->getPath(), + '#required' => TRUE, + '#element_validate' => [[$this, 'validatePath']], + ]; + $form['use_admin_theme'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Use admin theme'), + '#default_value' => $page->usesAdminTheme(), + ]; + + if ($page->isNew()) { + $variant_plugin_options = []; + foreach ($this->variantManager->getDefinitions() as $plugin_id => $definition) { + // The following two variants are provided by Drupal Core. They are not + // configurable and therefore not compatible with Page Manager but have + // similar and confusing labels. Skip them so that they are not shown in + // the UI. + if (in_array($plugin_id, ['simple_page', 'block_page'])) { + continue; + } + + $variant_plugin_options[$plugin_id] = $definition['admin_label']; + } + $form['variant_plugin_id'] = [ + '#title' => $this->t('Variant type'), + '#type' => 'select', + '#options' => $variant_plugin_options, + '#default_value' => !empty($cached_values['variant_plugin_id']) ? $cached_values['variant_plugin_id'] : '', + ]; + $form['wizard_options'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Optional features'), + '#description' => $this->t('Check any optional features you need to be presented with forms for configuring them. If you do not check them here you will still be able to utilize these features once the new page is created. If you are not sure, leave these unchecked.'), + '#options' => [ + 'access' => $this->t('Page access'), + 'contexts' => $this->t('Variant contexts'), + 'selection' => $this->t('Variant selection criteria'), + ], + '#default_value' => !empty($cached_values['wizard_options']) ? $cached_values['wizard_options'] : [], + ]; + } + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var $page \Drupal\page_manager\Entity\Page */ + $page = $cached_values['page']; + $page->set('description', $form_state->getValue('description')); + $page->set('path', $form_state->getValue('path')); + $page->set('use_admin_theme', $form_state->getValue('use_admin_theme')); + + if ($page->isNew()) { + $page->set('id', $form_state->getValue('id')); + $page->set('label', $form_state->getValue('label')); + if (empty($cached_values['variant_plugin_id'])) { + $variant_plugin_id = $cached_values['variant_plugin_id'] = $form_state->getValue('variant_plugin_id'); + /* @var \Drupal\page_manager\PageVariantInterface $page_variant */ + $page_variant = \Drupal::entityManager() + ->getStorage('page_variant') + ->create([ + 'variant' => $form_state->getValue('variant_plugin_id'), + 'page' => $page->id(), + 'id' => "{$page->id()}-{$variant_plugin_id}-0", + 'label' => $form['variant_plugin_id']['#options'][$variant_plugin_id], + ]); + $page_variant->setPageEntity($page); + $page->addVariant($page_variant); + $cached_values['page_variant'] = $page_variant; + } + if ($cached_values['variant_plugin_id'] != $form_state->getValue('variant_plugin_id') && !empty($cached_values['page_variant'])) { + $page_variant = $cached_values['page_variant']; + /** @var $page_variant \Drupal\page_manager\Entity\PageVariant */ + $page_variant->set('variant', $form_state->getValue('variant_plugin_id')); + $page_variant->set('variant_settings', []); + $cached_values['variant_plugin_id'] = $form_state->getValue('variant_plugin_id'); + } + + $cached_values['wizard_options'] = $form_state->getValue('wizard_options'); + $form_state->setTemporaryValue('wizard', $cached_values); + } + } + + /** + * {@inheritdoc} + */ + public function validatePath(&$element, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var $page \Drupal\page_manager\Entity\Page */ + $page = $cached_values['page']; + + // Ensure the path has a leading slash. + $value = '/' . trim($element['#value'], '/'); + $form_state->setValueForElement($element, $value); + + // Ensure each path is unique. + $path_query = $this->entityQuery->get('page') + ->condition('path', $value); + if (!$page->isNew()) { + $path_query->condition('id', $page->id(), '<>'); + } + $path = $path_query->execute(); + if ($path) { + $form_state->setErrorByName('path', $this->t('The page path must be unique.')); + } + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageParametersForm.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageParametersForm.php new file mode 100644 index 000000000..e1a9a8171 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageParametersForm.php @@ -0,0 +1,126 @@ +getTemporaryValue('wizard'); + $this->machine_name = $cached_values['id']; + $form['items'] = [ + '#type' => 'markup', + '#prefix' => '
', + '#suffix' => '
', + '#theme' => 'table', + '#header' => [ + $this->t('Machine name'), + $this->t('Label'), + $this->t('Type'), + $this->t('Operations'), + ], + '#rows' => $this->renderRows($cached_values), + '#empty' => $this->t('There are no parameters defined for this page.') + ]; + return $form; + } + + protected function renderRows($cached_values) { + $rows = []; + /** @var $page \Drupal\page_manager\Entity\Page */ + $page = $cached_values['page']; + /** + * @var string $parameter + */ + foreach ($page->getParameterNames() as $parameter_name) { + $parameter = $page->getParameter($parameter_name); + $row = []; + $row['machine_name'] = $parameter['machine_name']; + if ($label = $parameter['label']) { + $row['label'] = $label; + } + else { + $row['type']['colspan'] = 2; + } + $row['type']['data'] = $parameter['type'] ?: $this->t('No context assigned'); + + list($route_partial, $route_parameters) = $this->getOperationsRouteInfo($cached_values, $cached_values['id'], $parameter_name); + $build = [ + '#type' => 'operations', + '#links' => $this->getOperations($route_partial, $route_parameters), + ]; + $row['operations']['data'] = $build; + $rows[$parameter_name] = $row; + } + + return $rows; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + } + + protected function getOperations($route_name_base, array $route_parameters = array()) { + $operations['edit'] = array( + 'title' => t('Edit'), + 'url' => new Url($route_name_base . '.edit', $route_parameters), + 'weight' => 10, + 'attributes' => array( + 'class' => ['use-ajax'], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => Json::encode([ + 'width' => 700, + ]), + ), + ); + return $operations; + } + + /** + * Returns the tempstore id to use. + * + * @return string + */ + protected function getTempstoreId() { + return 'page_manager.page'; + } + + protected function getOperationsRouteInfo($cached_values, $machine_name, $row) { + return ['page_manager.parameter', ['machine_name' => $machine_name, 'name' => $row]]; + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageReorderVariantsForm.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageReorderVariantsForm.php new file mode 100644 index 000000000..047e7e181 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageReorderVariantsForm.php @@ -0,0 +1,148 @@ +tempstore = $tempstore; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('user.shared_tempstore') + ); + } + + /** + * Get the tempstore id. + * + * @return string + */ + protected function getTempstoreId() { + return 'page_manager.page'; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'page_manager_reorder_variants_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $machine_name = '') { + $cached_values = $this->tempstore->get($this->getTempstoreId())->get($machine_name); + $form_state->setTemporaryValue('wizard', $cached_values); + /** @var $page \Drupal\page_manager\Entity\Page */ + $page = $cached_values['page']; + + $form['variants'] = [ + '#type' => 'table', + '#header' => [ + $this->t('Label'), + $this->t('Plugin'), + $this->t('Weight'), + ], + '#empty' => $this->t('There are no variants.'), + '#tabledrag' => [[ + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'variant-weight', + ]], + ]; + + $variants = $page->getVariants(); + // Variants can be resorted, but the getVariants() method is still cached + // so manually invoke the sorting for this UI. + @uasort($variants, [$page, 'variantSortHelper']); + if (!empty($cached_values['deleted_variants'])) { + foreach ($cached_values['deleted_variants'] as $page_variant) { + unset($variants[$page_variant->id()]); + } + } + /** @var \Drupal\page_manager\PageVariantInterface $page_variant */ + foreach ($variants as $page_variant) { + $row = [ + '#attributes' => [ + 'class' => ['draggable'], + ], + ]; + $row['label']['#markup'] = $page_variant->label(); + $row['id']['#markup'] = $page_variant->getVariantPlugin()->adminLabel(); + $row['weight'] = [ + '#type' => 'weight', + '#default_value' => $page_variant->getWeight(), + '#title' => $this->t('Weight for @page_variant variant', ['@page_variant' => $page_variant->label()]), + '#title_display' => 'invisible', + '#attributes' => [ + 'class' => ['variant-weight'], + ], + ]; + $form['variants'][$page_variant->id()] = $row; + } + + $form['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Update'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var \Drupal\page_manager\Entity\Page $page */ + $page = $cached_values['page']; + + foreach ($form_state->getValue('variants') as $id => $values) { + if ($page_variant = $page->getVariant($id)) { + $page_variant->setWeight($values['weight']); + } + } + + $form_state->setRedirect('entity.page.edit_form', [ + 'machine_name' => $page->id(), + 'step' => 'general', + ]); + + $this->tempstore->get($this->getTempstoreId())->set($page->id(), $cached_values); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageVariantAddForm.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageVariantAddForm.php new file mode 100644 index 000000000..847c7fbf8 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageVariantAddForm.php @@ -0,0 +1,168 @@ +variantManager = $variant_manager; + $this->tempstore = $tempstore; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.display_variant'), + $container->get('user.shared_tempstore') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'page_manager_add_variant_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $machine_name = '') { + $cached_values = $form_state->getTemporaryValue('wizard'); + // The name label for variants is not required and can be changed later. + $form['name']['label']['#required'] = FALSE; + $form['name']['label']['#disabled'] = FALSE; + + $variant_plugin_options = []; + foreach ($this->variantManager->getDefinitions() as $plugin_id => $definition) { + // The following two variants are provided by Drupal Core. They are not + // configurable and therefore not compatible with Page Manager but have + // similar and confusing labels. Skip them so that they are not shown in + // the UI. + if (in_array($plugin_id, ['simple_page', 'block_page'])) { + continue; + } + + $variant_plugin_options[$plugin_id] = $definition['admin_label']; + } + $form['variant_plugin_id'] = [ + '#title' => $this->t('Type'), + '#type' => 'select', + '#options' => $variant_plugin_options, + '#default_value' => !empty($cached_values['variant_plugin_id']) ? $cached_values['variant_plugin_id'] : '', + ]; + $form['wizard_options'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Optional features'), + '#description' => $this->t('Check any optional features you need to be presented with forms for configuring them. If you do not check them here you will still be able to utilize these features once the new variant is created.'), + '#options' => [ + 'selection' => $this->t('Selection criteria'), + 'contexts' => $this->t('Contexts'), + ], + '#default_value' => !empty($cached_values['wizard_options']) ? $cached_values['wizard_options'] : [], + ]; + + return $form; + } + + /** + * Check if a variant id is taken. + * + * @param \Drupal\page_manager\PageInterface $page + * The page entity. + * @param string $variant_id + * The page variant id to check. + * + * @return bool + * TRUE if the ID is available; FALSE otherwise. + */ + protected function variantExists(PageInterface $page, $variant_id) { + return isset($page->getVariants()[$variant_id]) || PageVariant::load($variant_id); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + // If the label is not present or is an empty string. + if (!$form_state->hasValue('label') || !$form_state->getValue('label')) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var $page_variant \Drupal\page_manager\Entity\PageVariant */ + $page_variant = $cached_values['page_variant']; + $plugin = $page_variant->getVariantPlugin(); + /** @var \Drupal\Core\StringTranslation\TranslatableMarkup $admin_label */ + $admin_label = $plugin->getPluginDefinition()['admin_label']; + $form_state->setValue('label', (string) $admin_label); + } + // Currently the parent does nothing, but that could change. + parent::validateForm($form, $form_state); + } + + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var $page \Drupal\page_manager\Entity\Page */ + $page = $cached_values['page']; + $variant_plugin_id = $cached_values['variant_plugin_id'] = $form_state->getValue('variant_plugin_id'); + + /** @var $page_variant \Drupal\page_manager\Entity\PageVariant */ + $page_variant = $cached_values['page_variant']; + $page_variant->setVariantPluginId($variant_plugin_id); + $page_variant->set('label', $form_state->getValue('label')); + $page_variant->set('page', $page->id()); + + // Loop over variant ids until one is available. + $variant_id_base = "{$page->id()}-{$variant_plugin_id}"; + $key = 0; + while ($this->variantExists($page, "{$variant_id_base}-{$key}")) { + $key++; + } + + $cached_values['id'] = "{$variant_id_base}-{$key}"; + $page_variant->set('id', $cached_values['id']); + $cached_values['wizard_options'] = $form_state->getValue('wizard_options'); + $form_state->setTemporaryValue('wizard', $cached_values); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageVariantConfigureForm.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageVariantConfigureForm.php new file mode 100644 index 000000000..a15cb1539 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageVariantConfigureForm.php @@ -0,0 +1,94 @@ +getTemporaryValue('wizard'); + /** @var \Drupal\page_manager\PageInterface $page */ + $page = $cached_values['page']; + /** @var \Drupal\page_manager\PageVariantInterface $page_variant */ + $page_variant = $cached_values['page_variant']; + + $form['page_variant_label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#required' => TRUE, + '#size' => 32, + '#maxlength' => 255, + '#default_value' => $page_variant->label(), + ]; + + $variant_plugin = $page_variant->getVariantPlugin(); + $form['variant_settings'] = $variant_plugin->buildConfigurationForm([], (new FormState())->setValues($form_state->getValue('variant_settings', []))); + $form['variant_settings']['#tree'] = TRUE; + + if (!$page->isNew()) { + $form['delete'] = [ + '#type' => 'link', + '#title' => $this->t('Delete this variant'), + '#attributes' => [ + 'class' => ['button', 'use-ajax'], + 'data-dialog-type' => 'modal', + ], + '#url' => new Url('entity.page_variant.delete_form', [ + 'machine_name' => $page->id(), + 'variant_machine_name' => $page_variant->id(), + ]), + ]; + } + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var $page_variant \Drupal\page_manager\Entity\PageVariant */ + $page_variant = $cached_values['page_variant']; + + $variant_plugin = $page_variant->getVariantPlugin(); + $variant_plugin->validateConfigurationForm($form['variant_settings'], (new FormState())->setValues($form_state->getValue('variant_settings', []))); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var $page \Drupal\page_manager\Entity\Page */ + $page = $cached_values['page']; + /** @var $page_variant \Drupal\page_manager\Entity\PageVariant */ + $page_variant = $cached_values['page_variant']; + $variant_plugin = $page_variant->getVariantPlugin(); + $variant_plugin->submitConfigurationForm($form['variant_settings'], (new FormState())->setValues($form_state->getValue('variant_settings', []))); + $configuration = $variant_plugin->getConfiguration(); + $page_variant->set('variant_settings', $configuration); + $page_variant->set('label', $form_state->getValue('page_variant_label')); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageVariantContextsForm.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageVariantContextsForm.php new file mode 100644 index 000000000..d48c1380b --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageVariantContextsForm.php @@ -0,0 +1,130 @@ +getTemporaryValue('wizard'); + /** @var $page_variant \Drupal\page_manager\Entity\PageVariant */ + $page_variant = $cached_values['page_variant']; + $context = $form_state->getValue('context'); + $content = $this->formBuilder->getForm($this->getContextClass(), $context, $this->getTempstoreId(), $this->machine_name, $page_variant->id()); + $content['#attached']['library'][] = 'core/drupal.dialog.ajax'; + list(, $route_parameters) = $this->getContextOperationsRouteInfo($cached_values, $this->machine_name, $context); + $content['submit']['#attached']['drupalSettings']['ajax'][$content['submit']['#id']]['url'] = $this->url($this->getContextAddRoute($cached_values), $route_parameters, ['query' => [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]]); + $response = new AjaxResponse(); + $response->addCommand(new OpenModalDialogCommand($this->t('Add new context'), $content, array('width' => '700'))); + return $response; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'page_manager_variant_context_form'; + } + + /** + * {@inheritdoc} + */ + protected function getContextClass($cached_values) { + return StaticContextConfigure::class; + } + + /** + * {@inheritdoc} + */ + protected function getRelationshipClass($cached_values) { + //return VariantRelationshipConfigure::class; + } + + /** + * {@inheritdoc} + */ + protected function getContextAddRoute($cached_values) { + return 'entity.page_variant.context.add'; + } + + /** + * {@inheritdoc} + */ + protected function getRelationshipAddRoute($cached_values) { + return 'entity.page_variant.context.add'; + } + + /** + * {@inheritdoc} + */ + protected function getTempstoreId() { + return 'page_manager.page'; + } + + /** + * {@inheritdoc} + */ + protected function getContexts($cached_values) { + /** @var $page_variant \Drupal\page_manager\Entity\PageVariant */ + $page_variant = $cached_values['page_variant']; + return $page_variant->getContexts(); + } + + /** + * {@inheritdoc} + */ + protected function getContextOperationsRouteInfo($cached_values, $machine_name, $row) { + /** @var \Drupal\page_manager\PageVariantInterface $page_variant */ + $page_variant = $cached_values['page_variant']; + return ['entity.page_variant.context', [ + 'machine_name' => $machine_name, + 'variant_machine_name' => $page_variant->id(), + 'context_id' => $row + ]]; + } + + /** + * {@inheritdoc} + */ + protected function getRelationshipOperationsRouteInfo($cached_values, $machine_name, $row) { + /** @var \Drupal\page_manager\PageVariantInterface $page_variant */ + $page_variant = $cached_values['page_variant']; + return ['entity.page_variant.relationship', [ + 'machine_name' => $machine_name, + 'variant_machine_name' => $page_variant->id(), + 'relationship' => $row + ]]; + } + + protected function isEditableContext($cached_values, $row) { + /** @var \Drupal\page_manager\PageInterface $page */ + $page = $cached_values['page']; + /** @var \Drupal\page_manager\PageVariantInterface $page_variant */ + $page_variant = $cached_values['page_variant']; + return empty($page->getContexts()[$row]) && !empty($page_variant->getContexts()[$row]); + } + + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageVariantDeleteForm.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageVariantDeleteForm.php new file mode 100644 index 000000000..1b242e243 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageVariantDeleteForm.php @@ -0,0 +1,111 @@ +tempstore = $tempstore; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('user.shared_tempstore') + ); + } + + /** + * Get the tempstore id. + * + * @return string + */ + protected function getTempstoreId() { + return 'page_manager.page'; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'page_manager_variant_delete_form'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->t('Are you sure you want to delete this variant?'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + $machine_name = $this->getRouteMatch()->getParameter('machine_name'); + return new Url('entity.page.edit_form', [ + 'machine_name' => $machine_name, + 'step' => 'general', + ]); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $machine_name = $this->getRouteMatch()->getParameter('machine_name'); + $variant_machine_name = $this->getRouteMatch()->getParameter('variant_machine_name'); + $cached_values = $this->tempstore->get($this->getTempstoreId())->get($machine_name); + /** @var \Drupal\page_manager\PageInterface $page */ + $page = $cached_values['page']; + $page_variant = $page->getVariant($variant_machine_name); + + // Add to a list to remove for real later. + $cached_values['deleted_variants'][$variant_machine_name] = $page_variant; + + drupal_set_message($this->t('The variant %label has been removed.', [ + '%label' => $page_variant->label(), + ])); + + $form_state->setRedirectUrl($this->getCancelUrl()); + + $this->tempstore->get($this->getTempstoreId())->set($page->id(), $cached_values); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageVariantSelectionForm.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageVariantSelectionForm.php new file mode 100644 index 000000000..a344988c7 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/PageVariantSelectionForm.php @@ -0,0 +1,104 @@ + $machine_name, + 'variant_machine_name' => $page_variant->id(), + 'condition' => $row + ]]; + } + + /** + * {@inheritdoc} + */ + protected function getConditions($cached_values) { + /** @var $page \Drupal\page_manager\Entity\PageVariant */ + $page_variant = $cached_values['page_variant']; + return $page_variant->get('selection_criteria'); + } + + /** + * {@inheritdoc} + */ + protected function getContexts($cached_values) { + /** @var $page_variant \Drupal\page_manager\Entity\PageVariant */ + $page_variant = $cached_values['page_variant']; + return $page_variant->getContexts(); + } + + /** + * {@inheritdoc} + */ + protected function getAddRoute($cached_values) { + return 'entity.page_variant.condition.add'; + } + + /** + * {@inheritdoc} + */ + public function add(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + $page_variant = $cached_values['page_variant']; + $condition = $form_state->getValue('conditions'); + $content = \Drupal::formBuilder()->getForm($this->getConditionClass(), $condition, $this->getTempstoreId(), $this->machine_name, $page_variant->id()); + $content['#attached']['library'][] = 'core/drupal.dialog.ajax'; + list(, $route_parameters) = $this->getOperationsRouteInfo($cached_values, $this->machine_name, $form_state->getValue('conditions')); + $content['submit']['#attached']['drupalSettings']['ajax'][$content['submit']['#id']]['url'] = $this->url($this->getAddRoute($cached_values), $route_parameters, ['query' => [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]]); + $response = new AjaxResponse(); + $response->addCommand(new OpenModalDialogCommand($this->t('Configure Required Context'), $content, array('width' => '700'))); + return $response; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + if ($triggering_element['#value']->getUntranslatedString() != 'Add Condition') { + return; + } + parent::submitForm($form, $form_state); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/ParameterEditForm.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/ParameterEditForm.php new file mode 100644 index 000000000..fa9cbec8a --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/ParameterEditForm.php @@ -0,0 +1,233 @@ +entityTypeRepository = $entity_type_repository; + $this->typedDataManager = $typed_data_manager; + $this->tempstore = $tempstore; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.repository'), + $container->get('typed_data_manager'), + $container->get('user.shared_tempstore') + ); + } + + protected function getTempstore() { + return $this->tempstore->get($this->tempstore_id)->get($this->machine_name); + } + + protected function setTempstore($cached_values) { + $this->tempstore->get($this->tempstore_id)->set($this->machine_name, $cached_values); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'page_manager_parameter_edit_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $name = '', $tempstore_id = NULL, $machine_name = NULL) { + $this->tempstore_id = $tempstore_id; + $this->machine_name = $machine_name; + $cached_values = $this->getTempstore(); + $page = $cached_values['page']; + $parameter = $page->getParameter($name); + + $form['machine_name'] = [ + '#type' => 'value', + '#value' => $name, + ]; + + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#default_value' => $parameter['label'] ?: ucfirst($parameter['machine_name']), + '#states' => [ + 'invisible' => [ + ':input[name="type"]' => ['value' => static::NO_CONTEXT_KEY], + ], + ], + ]; + + $form['type'] = [ + '#type' => 'select', + '#title' => $this->t('Type'), + '#required' => TRUE, + '#options' => $this->buildParameterTypeOptions(), + '#default_value' => $parameter['type'], + ]; + + $form['actions'] = ['#type' => 'actions']; + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Update parameter'), + '#button_type' => 'primary', + ]; + + return $form; + } + + /** + * Builds an array of options for the parameter type. + * + * @return array[] + * A multidimensional array. The top level is keyed by group ('Content', + * 'Configuration', 'Typed Data'). Those values are an array of type labels, + * keyed by the machine name. + */ + protected function buildParameterTypeOptions() { + $options = [static::NO_CONTEXT_KEY => $this->t('No context selected')]; + + // Make a grouped, sorted list of entity type options. Key the inner array + // to use the typed data format of 'entity:$entity_type_id'. + foreach ($this->entityTypeRepository->getEntityTypeLabels(TRUE) as $group_label => $grouped_options) { + foreach ($grouped_options as $key => $label) { + $options[$group_label]['entity:' . $key] = $label; + } + } + + $primitives_label = (string) $this->t('Primitives'); + foreach ($this->typedDataManager->getDefinitions() as $key => $definition) { + if (is_subclass_of($definition['class'], PrimitiveInterface::class)) { + $options[$primitives_label][$key] = $definition['label']; + } + } + asort($options[$primitives_label], SORT_NATURAL); + + return $options; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cache_values = $this->getTempstore(); + /** @var \Drupal\page_manager\PageInterface $page */ + $page = $cache_values['page']; + $name = $form_state->getValue('machine_name'); + $type = $form_state->getValue('type'); + if ($type === static::NO_CONTEXT_KEY) { + $page->removeParameter($name); + $label = NULL; + } + else { + $label = $form_state->getValue('label'); + $page->setParameter($name, $type, $label); + } + + $this->setTempstore($cache_values); + drupal_set_message($this->t('The %label parameter has been updated.', ['%label' => $label ?: $name])); + list($route_name, $route_parameters) = $this->getParentRouteInfo($cache_values); + $form_state->setRedirect($route_name, $route_parameters); + } + + /** + * Returns the parent route to redirect after form submission. + * + * @return array + * Array containing the route name and its parameters. + */ + protected function getParentRouteInfo($cached_values) { + /** @var $page \Drupal\page_manager\PageInterface */ + $page = $cached_values['page']; + + if ($page->isNew()) { + return ['entity.page.add_step_form', [ + 'machine_name' => $this->machine_name, + 'step' => 'parameters', + ]]; + } + else { + return ['entity.page.edit_form', [ + 'machine_name' => $this->machine_name, + 'step' => 'parameters', + ]]; + } + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/SelectionConfigure.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/SelectionConfigure.php new file mode 100644 index 000000000..23b933d39 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/SelectionConfigure.php @@ -0,0 +1,95 @@ +getVariant($this->variantMachineName); + } + + /** + * {@inheritdoc} + */ + protected function getParentRouteInfo($cached_values) { + /** @var $page \Drupal\page_manager\PageInterface */ + $page = $cached_values['page']; + + if ($page->isNew()) { + return ['entity.page.add_step_form', [ + 'machine_name' => $this->machine_name, + 'step' => 'selection', + ]]; + } + else { + $page_variant = $this->getPageVariant($cached_values); + return ['entity.page.edit_form', [ + 'machine_name' => $this->machine_name, + 'step' => 'page_variant__' . $page_variant->id() . '__selection', + ]]; + } + } + + /** + * @inheritDoc + */ + public function buildForm(array $form, FormStateInterface $form_state, $condition = NULL, $tempstore_id = NULL, $machine_name = NULL, $variant_machine_name = NULL) { + $this->variantMachineName = $variant_machine_name; + return parent::buildForm($form, $form_state, $condition, $tempstore_id, $machine_name); + } + + /** + * {@inheritdoc} + */ + protected function getConditions($cached_values) { + $page_variant = $this->getPageVariant($cached_values); + return $page_variant->get('selection_criteria'); + } + + /** + * {@inheritdoc} + */ + protected function setConditions($cached_values, $conditions) { + $page_variant = $this->getPageVariant($cached_values); + $page_variant->set('selection_criteria', $conditions); + return $cached_values; + } + + /** + * {@inheritdoc} + */ + protected function getContexts($cached_values) { + /** @var $page \Drupal\page_manager\Entity\PageVariant */ + $page_variant = $this->getPageVariant($cached_values); + return $page_variant->getContexts(); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/SelectionDelete.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/SelectionDelete.php new file mode 100644 index 000000000..9c03a504a --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/SelectionDelete.php @@ -0,0 +1,95 @@ +getVariant($this->variantMachineName); + } + + /** + * {@inheritdoc} + */ + protected function getParentRouteInfo($cached_values) { + /** @var $page \Drupal\page_manager\PageInterface */ + $page = $cached_values['page']; + + if ($page->isNew()) { + return ['entity.page.add_step_form', [ + 'machine_name' => $this->machine_name, + 'step' => 'selection', + ]]; + } + else { + $page_variant = $this->getPageVariant($cached_values); + return ['entity.page.edit_form', [ + 'machine_name' => $this->machine_name, + 'step' => 'page_variant__' . $page_variant->id() . '__selection', + ]]; + } + } + + /** + * @inheritDoc + */ + public function buildForm(array $form, FormStateInterface $form_state, $id = NULL, $tempstore_id = NULL, $machine_name = NULL, $variant_machine_name = NULL) { + $this->variantMachineName = $variant_machine_name; + return parent::buildForm($form, $form_state, $id, $tempstore_id, $machine_name); + } + + + /** + * {@inheritdoc} + */ + protected function getConditions($cached_values) { + $page_variant = $this->getPageVariant($cached_values); + return $page_variant->get('selection_criteria'); + } + + /** + * {@inheritdoc} + */ + protected function setConditions($cached_values, $conditions) { + $page_variant = $this->getPageVariant($cached_values); + $page_variant->set('selection_criteria', $conditions); + return $cached_values; + } + + /** + * {@inheritdoc} + */ + protected function getContexts($cached_values) { + $page_variant = $this->getPageVariant($cached_values); + return $page_variant->getContexts(); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/StaticContextConfigure.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/StaticContextConfigure.php new file mode 100644 index 000000000..bfc8edfff --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/StaticContextConfigure.php @@ -0,0 +1,119 @@ +getVariant($this->variantMachineName); + } + + /** + * {@inheritdoc} + */ + protected function getParentRouteInfo($cached_values) { + /** @var $page \Drupal\page_manager\PageInterface */ + $page = $cached_values['page']; + + if ($page->isNew()) { + return ['entity.page.add_step_form', [ + 'machine_name' => $this->machine_name, + 'step' => 'contexts', + ]]; + } + else { + $page_variant = $this->getPageVariant($cached_values); + return ['entity.page.edit_form', [ + 'machine_name' => $this->machine_name, + 'step' => 'page_variant__' . $page_variant->id() . '__contexts', + ]]; + } + } + + /** + * {@inheritdoc} + * + * Overridden to set the variantMachineName. + */ + public function buildForm(array $form, FormStateInterface $form_state, $context_id = NULL, $tempstore_id = NULL, $machine_name = NULL, $variant_machine_name = NULL) { + $this->variantMachineName = $variant_machine_name; + return parent::buildForm($form, $form_state, $context_id, $tempstore_id, $machine_name); + } + + /** + * {@inheritdoc} + */ + protected function getContexts($cached_values) { + /** @var $page_variant \Drupal\page_manager\PageVariantInterface */ + $page_variant = !empty($cached_values['page_variant']) ? $cached_values['page_variant'] : NULL; + if (is_null($page_variant)) { + $page_variant = $this->getPageVariant($cached_values); + } + return $page_variant->getContexts(); + } + + /** + * {@inheritdoc} + */ + protected function addContext($cached_values, $context_id, ContextInterface $context) { + /** @var $page_variant \Drupal\page_manager\PageVariantInterface */ + $page_variant = $this->getPageVariant($cached_values); + $context_config = [ + 'label' => $context->getContextDefinition()->getLabel(), + 'type' => $context->getContextDefinition()->getDataType(), + 'description' => $context->getContextDefinition()->getDescription(), + 'value' => strpos($context->getContextDefinition()->getDataType(), 'entity:') === 0 ? $context->getContextValue()->uuid() : $context->getContextValue(), + ]; + $page_variant->setStaticContext($context_id, $context_config); + $cached_values['page_variant'] = $page_variant; + return $cached_values; + } + + /** + * {@inheritdoc} + */ + public function contextExists($value, $element, $form_state) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + protected function disableMachineName($cached_values, $machine_name) { + if ($machine_name) { + return !empty($this->getContexts($cached_values)[$machine_name]); + } + return FALSE; + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/StaticContextDeleteForm.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/StaticContextDeleteForm.php new file mode 100644 index 000000000..0206a3170 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/StaticContextDeleteForm.php @@ -0,0 +1,105 @@ +getTempstore(); + /** @var $page \Drupal\page_manager\PageInterface */ + $page_variant = $this->getPageVariant($cached_values); + return $this->t('Are you sure you want to delete the static context %label?', ['%label' => $page_variant->getStaticContext($this->context_id)['label']]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + $cached_values = $this->getTempstore(); + /** @var $page \Drupal\page_manager\PageInterface */ + $page = $cached_values['page']; + + if ($page->isNew()) { + return new Url('entity.page.add_step_form', [ + 'machine_name' => $this->machine_name, + 'step' => 'contexts', + ]); + } + else { + $page_variant = $this->getPageVariant($cached_values); + return new Url('entity.page.edit_form', [ + 'machine_name' => $this->machine_name, + 'step' => 'page_variant__' . $page_variant->id() . '__contexts', + ]); + } + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $tempstore_id = NULL, $machine_name = NULL, $context_id = NULL, $variant_machine_name = NULL) { + $this->variantMachineName = $variant_machine_name; + return parent::buildForm($form, $form_state, $tempstore_id, $machine_name, $context_id); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $this->getTempstore(); + /** @var $page \Drupal\page_manager\PageInterface */ + $page_variant = $this->getPageVariant($cached_values); + drupal_set_message($this->t('The static context %label has been removed.', ['%label' => $page_variant->getStaticContext($this->context_id)['label']])); + $page_variant->removeStaticContext($this->context_id); + $this->setTempstore($cached_values); + parent::submitForm($form, $form_state); + } + + /** + * Get the page variant. + * + * @param array $cached_values + * The cached values from the wizard. + * + * @return \Drupal\page_manager\PageVariantInterface + */ + protected function getPageVariant($cached_values) { + if (isset($cached_values['page_variant'])) { + return $cached_values['page_variant']; + } + + /** @var $page \Drupal\page_manager\PageInterface */ + $page = $cached_values['page']; + return $page->getVariant($this->variantMachineName); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/VariantPluginAddBlockForm.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/VariantPluginAddBlockForm.php new file mode 100644 index 000000000..a372a1282 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/VariantPluginAddBlockForm.php @@ -0,0 +1,82 @@ +blockManager = $block_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('user.shared_tempstore'), + $container->get('plugin.manager.block') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'page_manager_variant_add_block_form'; + } + + /** + * {@inheritdoc} + */ + protected function prepareBlock($plugin_id) { + $block = $this->blockManager->createInstance($plugin_id); + $block_id = $this->getVariantPlugin()->addBlock($block->getConfiguration()); + return $this->getVariantPlugin()->getBlock($block_id); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, Request $request = NULL, $block_display = NULL, $block_id = NULL) { + $form = parent::buildForm($form, $form_state, $block_display, $block_id); + $form['region']['#default_value'] = $request->query->get('region'); + return $form; + } + + /** + * {@inheritdoc} + */ + protected function submitText() { + return $this->t('Add block'); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/VariantPluginConfigureBlockFormBase.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/VariantPluginConfigureBlockFormBase.php new file mode 100644 index 000000000..59de6c687 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/VariantPluginConfigureBlockFormBase.php @@ -0,0 +1,199 @@ +tempstore = $tempstore; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('user.shared_tempstore') + ); + } + + /** + * Get the tempstore id. + * + * @return string + */ + protected function getTempstoreId() { + return 'page_manager.block_display'; + } + + /** + * Get the tempstore. + * + * @return \Drupal\user\SharedTempStore + */ + protected function getTempstore() { + return $this->tempstore->get($this->getTempstoreId()); + } + + /** + * Prepares the block plugin based on the block ID. + * + * @param string $block_id + * Either a block ID, or the plugin ID used to create a new block. + * + * @return \Drupal\Core\Block\BlockPluginInterface + * The block plugin. + */ + abstract protected function prepareBlock($block_id); + + /** + * Returns the text to use for the submit button. + * + * @return string + * The submit button text. + */ + abstract protected function submitText(); + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $block_display = NULL, $block_id = NULL) { + $cached_values = $this->tempstore->get('page_manager.block_display')->get($block_display); + /** @var \Drupal\page_manager\Plugin\DisplayVariant\PageBlockDisplayVariant $variant_plugin */ + $this->variantPlugin = $cached_values['plugin']; + + // Rehydrate the contexts on this end. + $contexts = []; + /** + * @var string $context_name + * @var \Drupal\Core\Plugin\Context\ContextDefinitionInterface $context_definition + */ + foreach ($cached_values['contexts'] as $context_name => $context_definition) { + $contexts[$context_name] = new Context($context_definition); + } + $this->variantPlugin->setContexts($contexts); + + $this->block = $this->prepareBlock($block_id); + $form_state->set('variant_id', $this->getVariantPlugin()->id()); + $form_state->set('block_id', $this->block->getConfiguration()['uuid']); + + $form['#tree'] = TRUE; + $form['settings'] = $this->block->buildConfigurationForm([], $form_state); + $form['settings']['id'] = [ + '#type' => 'value', + '#value' => $this->block->getPluginId(), + ]; + $form['region'] = [ + '#title' => $this->t('Region'), + '#type' => 'select', + '#options' => $this->getVariantPlugin()->getRegionNames(), + '#default_value' => $this->getVariantPlugin()->getRegionAssignment($this->block->getConfiguration()['uuid']), + '#required' => TRUE, + ]; + + if ($this->block instanceof ContextAwarePluginInterface) { + $form['context_mapping'] = $this->addContextAssignmentElement($this->block, $this->getVariantPlugin()->getContexts()); + } + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->submitText(), + '#button_type' => 'primary', + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + // The page might have been serialized, resulting in a new variant + // collection. Refresh the block object. + $this->block = $this->getVariantPlugin()->getBlock($form_state->get('block_id')); + + $settings = (new FormState())->setValues($form_state->getValue('settings')); + // Call the plugin validate handler. + $this->block->validateConfigurationForm($form, $settings); + // Update the original form values. + $form_state->setValue('settings', $settings->getValues()); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $settings = (new FormState())->setValues($form_state->getValue('settings')); + + // Call the plugin submit handler. + $this->block->submitConfigurationForm($form, $settings); + // Update the original form values. + $form_state->setValue('settings', $settings->getValues()); + + if ($this->block instanceof ContextAwarePluginInterface) { + $this->block->setContextMapping($form_state->getValue('context_mapping', [])); + } + + $this->getVariantPlugin()->updateBlock($this->block->getConfiguration()['uuid'], ['region' => $form_state->getValue('region')]); + + $cached_values = $this->getTempstore()->get($form_state->get('variant_id')); + $cached_values['plugin'] = $this->getVariantPlugin(); + $this->getTempstore()->set($form_state->get('variant_id'), $cached_values); + } + + /** + * Gets the variant plugin for this page variant entity. + * + * @return \Drupal\ctools\Plugin\BlockVariantInterface + */ + protected function getVariantPlugin() { + return $this->variantPlugin; + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/VariantPluginContentForm.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/VariantPluginContentForm.php new file mode 100644 index 000000000..3d67314e0 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/VariantPluginContentForm.php @@ -0,0 +1,257 @@ +tempstore = $tempstore; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('user.shared_tempstore') + ); + } + + /** + * Get the tempstore ID. + * + * @return string + */ + protected function getTempstoreId() { + return 'page_manager.block_display'; + } + + /** + * Get the tempstore. + * + * @return \Drupal\user\SharedTempStore + */ + protected function getTempstore() { + return $this->tempstore->get($this->getTempstoreId()); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'page_manager_block_page_content'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var \Drupal\page_manager\Plugin\DisplayVariant\PageBlockDisplayVariant $variant_plugin */ + $variant_plugin = $cached_values['plugin']; + + // Store the block display plugin so we can get it in our dialogs. + if (!empty($this->getTempstore()->get($variant_plugin->id())['plugin'])) { + $variant_plugin->setConfiguration($this->getTempstore()->get($variant_plugin->id())['plugin']->getConfiguration()); + $form_state->setTemporaryValue('wizard', $cached_values); + } + $context_definitions = []; + foreach ($variant_plugin->getContexts() as $context_name => $context) { + $context_definitions[$context_name] = $context->getContextDefinition(); + } + $this->getTempstore()->set($variant_plugin->id(), [ + 'plugin' => $variant_plugin, + 'access' => $cached_values['access'], + 'contexts' => $context_definitions, + ]); + + // Set up the attributes used by a modal to prevent duplication later. + $attributes = $this->getAjaxAttributes(); + $add_button_attributes = $this->getAjaxButtonAttributes(); + + if ($block_assignments = $variant_plugin->getRegionAssignments()) { + // Build a table of all blocks used by this variant. + $form['add'] = [ + '#type' => 'link', + '#title' => $this->t('Add new block'), + '#url' => Url::fromRoute('page_manager.block_display_select_block', [ + 'block_display' => $variant_plugin->id(), + 'destination' => $this->getRequest()->getRequestUri(), + ]), + '#attributes' => $add_button_attributes, + '#attached' => [ + 'library' => [ + 'core/drupal.ajax', + ], + ], + ]; + $form['blocks'] = [ + '#type' => 'table', + '#header' => [ + $this->t('Label'), + $this->t('Plugin ID'), + $this->t('Region'), + $this->t('Weight'), + $this->t('Operations'), + ], + '#empty' => $this->t('There are no regions for blocks.'), + ]; + // Loop through the blocks per region. + foreach ($block_assignments as $region => $blocks) { + // Add a section for each region and allow blocks to be dragged between + // them. + $form['blocks']['#tabledrag'][] = [ + 'action' => 'match', + 'relationship' => 'sibling', + 'group' => 'block-region-select', + 'subgroup' => 'block-region-' . $region, + 'hidden' => FALSE, + ]; + $form['blocks']['#tabledrag'][] = [ + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'block-weight', + 'subgroup' => 'block-weight-' . $region, + ]; + $form['blocks'][$region] = [ + '#attributes' => [ + 'class' => ['region-title', 'region-title-' . $region], + 'no_striping' => TRUE, + ], + ]; + $form['blocks'][$region]['title'] = [ + '#markup' => $variant_plugin->getRegionName($region), + '#wrapper_attributes' => [ + 'colspan' => 5, + ], + ]; + $form['blocks'][$region . '-message'] = [ + '#attributes' => [ + 'class' => [ + 'region-message', + 'region-' . $region . '-message', + empty($blocks) ? 'region-empty' : 'region-populated', + ], + ], + ]; + $form['blocks'][$region . '-message']['message'] = [ + '#markup' => '' . $this->t('No blocks in this region') . '', + '#wrapper_attributes' => [ + 'colspan' => 5, + ], + ]; + + /** @var \Drupal\Core\Block\BlockPluginInterface[] $blocks */ + foreach ($blocks as $block_id => $block) { + $row = [ + '#attributes' => [ + 'class' => ['draggable'], + ], + ]; + $row['label']['#markup'] = $block->label(); + $row['id']['#markup'] = $block->getPluginId(); + // Allow the region to be changed for each block. + $row['region'] = [ + '#title' => $this->t('Region'), + '#title_display' => 'invisible', + '#type' => 'select', + '#options' => $variant_plugin->getRegionNames(), + '#default_value' => $variant_plugin->getRegionAssignment($block_id), + '#attributes' => [ + 'class' => ['block-region-select', 'block-region-' . $region], + ], + ]; + // Allow the weight to be changed for each block. + $configuration = $block->getConfiguration(); + $row['weight'] = [ + '#type' => 'weight', + '#default_value' => isset($configuration['weight']) ? $configuration['weight'] : 0, + '#title' => $this->t('Weight for @block block', ['@block' => $block->label()]), + '#title_display' => 'invisible', + '#attributes' => [ + 'class' => ['block-weight', 'block-weight-' . $region], + ], + ]; + // Add the operation links. + $operations = []; + $operations['edit'] = [ + 'title' => $this->t('Edit'), + 'url' => Url::fromRoute('page_manager.block_display_edit_block', [ + 'block_display' => $variant_plugin->id(), + 'block_id' => $block_id, + 'destination' => $this->getRequest()->getRequestUri(), + ]), + 'attributes' => $attributes, + ]; + $operations['delete'] = [ + 'title' => $this->t('Delete'), + 'url' => Url::fromRoute('page_manager.block_display_delete_block', [ + 'block_display' => $variant_plugin->id(), + 'block_id' => $block_id, + 'destination' => $this->getRequest()->getRequestUri(), + ]), + 'attributes' => $attributes, + ]; + + $row['operations'] = [ + '#type' => 'operations', + '#links' => $operations, + ]; + $form['blocks'][$block_id] = $row; + } + } + } + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var \Drupal\page_manager\Plugin\DisplayVariant\PageBlockDisplayVariant $variant_plugin */ + $variant_plugin = $cached_values['plugin']; + + // If the blocks were rearranged, update their values. + if (!$form_state->isValueEmpty('blocks')) { + foreach ($form_state->getValue('blocks') as $block_id => $block_values) { + $variant_plugin->updateBlock($block_id, $block_values); + } + } + + // Remove from the tempstore so we refresh from the database the next time + // we come here. + $this->getTempstore()->delete($variant_plugin->id()); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/VariantPluginDeleteBlockForm.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/VariantPluginDeleteBlockForm.php new file mode 100644 index 000000000..b45107438 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/VariantPluginDeleteBlockForm.php @@ -0,0 +1,101 @@ +get($this->getTempstoreId()); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'page_manager_variant_delete_block_form'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->t('Are you sure you want to delete the block %label?', ['%label' => $this->block->label()]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return \Drupal::request()->attributes->get('destination'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $block_display = NULL, $block_id = NULL) { + $this->plugin = $this->getTempstore()->get($block_display)['plugin']; + $this->block = $this->plugin->getBlock($block_id); + $form['block_display'] = [ + '#type' => 'value', + '#value' => $block_display + ]; + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->plugin->removeBlock($this->block->getConfiguration()['uuid']); + $cached_values = $this->getTempstore()->get($form_state->getValue('block_display')); + $cached_values['plugin'] = $this->plugin; + $this->getTempstore()->set($form_state->getValue('block_display'), $cached_values); + drupal_set_message($this->t('The block %label has been removed.', ['%label' => $this->block->label()])); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/VariantPluginEditBlockForm.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/VariantPluginEditBlockForm.php new file mode 100644 index 000000000..2cd999fb4 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Form/VariantPluginEditBlockForm.php @@ -0,0 +1,36 @@ +getVariantPlugin()->getBlock($block_id); + } + + /** + * {@inheritdoc} + */ + protected function submitText() { + return $this->t('Update block'); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Tests/AddVariantSelectionTest.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Tests/AddVariantSelectionTest.php new file mode 100644 index 000000000..cda3cfa49 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Tests/AddVariantSelectionTest.php @@ -0,0 +1,156 @@ +drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + $this->drupalCreateContentType(['type' => 'page', 'name' => 'Page']); + $this->drupalLogin($this->drupalCreateUser(['administer pages', 'create article content'])); + + $this->drupalPlaceBlock('page_title_block'); + } + + /** + * Tests configuration of the selection criteria wizard step. + */ + public function testSelectionCriteria() { + // Create a node, and check its page. + $node = $this->drupalCreateNode(['type' => 'article']); + $node2 = $this->drupalCreateNode(['type' => 'article']); + $this->drupalGet('node/' . $node->id()); + $this->assertResponse(200); + $this->assertText($node->label()); + $this->assertTitle($node->label() . ' | Drupal'); + + // Create a new page entity. + $edit_page = [ + 'label' => 'Selection criteria', + 'id' => 'selection_criteria', + 'path' => 'selection-criteria', + 'variant_plugin_id' => 'block_display' + ]; + $this->drupalPostForm('admin/structure/page_manager/add', $edit_page, 'Next'); + $this->drupalPostForm(NULL, [], 'Next'); + $this->drupalPostForm(NULL, [], 'Finish'); + $this->clickLink('Add variant'); + $edit = [ + 'label' => 'Variant two', + 'variant_plugin_id' => 'block_display', + 'wizard_options[contexts]' => TRUE, + 'wizard_options[selection]' => TRUE, + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + // Add a static context for each node to the page variant. + $contexts = array( + array( + 'title' => 'Static Node', + 'machine_name' => 'static_node', + 'description' => 'Static node 1', + 'node' => $node, + ), + array( + 'title' => 'Static Node 2', + 'machine_name' => 'static_node_2', + 'description' => 'Static node 2', + 'node' => $node2, + ), + ); + foreach ($contexts as $context) { + $edit = [ + 'context' => 'entity:node', + ]; + $this->drupalPostForm(NULL, $edit, 'Add new context'); + $edit = [ + 'label' => $context['title'], + 'machine_name' => $context['machine_name'], + 'description' => $context['description'], + 'context_value' => $context['node']->getTitle() . ' (' . $context['node']->id() . ')', + ]; + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText($context['title']); + } + $this->drupalPostForm(NULL, [], 'Next'); + + // Configure selection criteria. + $edit = [ + 'conditions' => 'entity_bundle:node', + ]; + $this->drupalPostForm(NULL, $edit, 'Add Condition'); + + $edit = [ + 'bundles[article]' => TRUE, + 'bundles[page]' => TRUE, + 'context_mapping[node]' => 'static_node_2', + ]; + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText('Content type is article or page'); + $this->clickLink('Edit'); + $edit = [ + 'bundles[article]' => TRUE, + 'context_mapping[node]' => 'static_node_2', + ]; + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText('Content type is article'); + $this->clickLink('Delete'); + $this->drupalPostForm(NULL, [], 'Delete'); + $this->assertNoText('Content type is article'); + $this->drupalPostForm(NULL, [], 'Next'); + + // Configure the new variant. + $variant_edit = [ + 'variant_settings[page_title]' => 'Variant two criteria test', + ]; + $this->drupalPostForm(NULL, $variant_edit, 'Next'); + + // Add a block that renders the node from the first static context. + $this->clickLink('Add new block'); + $this->clickLink('Entity view (Content)'); + $edit = [ + 'settings[label]' => 'Static node view', + 'settings[label_display]' => 1, + 'settings[view_mode]' => 'default', + 'region' => 'top', + ]; + $this->drupalPostForm(NULL, $edit, 'Add block'); + $this->assertText($edit['settings[label]']); + + // Add a block that renders the node from the second static context. + $this->clickLink('Add new block'); + $this->clickLink('Entity view (Content)'); + $edit = [ + 'settings[label]' => 'Static node 2 view', + 'settings[label_display]' => 1, + 'settings[view_mode]' => 'default', + 'region' => 'bottom', + 'context_mapping[entity]' => $contexts[1]['machine_name'], + ]; + $this->drupalPostForm(NULL, $edit, 'Add block'); + $this->assertText($edit['settings[label]']); + $this->drupalPostForm(NULL, [], 'Finish'); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Tests/PageManagerAdminTest.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Tests/PageManagerAdminTest.php new file mode 100644 index 000000000..715df72fe --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Tests/PageManagerAdminTest.php @@ -0,0 +1,712 @@ +drupalPlaceBlock('local_tasks_block'); + $this->drupalPlaceBlock('local_actions_block'); + $this->drupalPlaceBlock('system_branding_block'); + $this->drupalPlaceBlock('page_title_block'); + + \Drupal::service('theme_handler')->install(['bartik', 'classy']); + $this->config('system.theme')->set('admin', 'classy')->save(); + + $this->drupalLogin($this->drupalCreateUser(['administer pages', 'access administration pages', 'view the administration theme'])); + + // Remove the default node_view page to start with a clean UI. + Page::load('node_view')->delete(); + } + + /** + * Tests the Page Manager admin UI. + */ + public function testAdmin() { + $this->doTestAddPage(); + $this->doTestAccessConditions(); + $this->doTestSelectionCriteria(); + $this->doTestSelectionCriteriaWithAjax(); + $this->doTestDisablePage(); + $this->doTestAddVariant(); + $this->doTestAddBlock(); + $this->doTestSecondPage(); + $this->doTestEditBlock(); + $this->doTestEditVariant(); + $this->doTestReorderVariants(); + $this->doTestAddPageWithDuplicatePath(); + $this->doTestAdminPath(); + $this->doTestRemoveVariant(); + $this->doTestRemoveBlock(); + $this->doTestAddBlockWithAjax(); + $this->doTestEditBlock(); + $this->doTestExistingPathWithoutParameters(); + $this->doTestUpdateSubmit(); + $this->doTestDeletePage(); + } + + /** + * Tests adding a page. + */ + protected function doTestAddPage() { + $this->drupalGet('admin/structure'); + $this->clickLink('Pages'); + $this->assertText('Add a new page.'); + + // Add a new page without a label. + $this->clickLink('Add page'); + $edit = [ + 'id' => 'foo', + 'path' => 'admin/foo', + 'variant_plugin_id' => 'http_status_code', + 'use_admin_theme' => TRUE, + 'description' => 'This is our first test page.', + // Go through all available steps (we skip them all in doTestSecondPage()) + 'wizard_options[access]' => TRUE, + 'wizard_options[selection]' => TRUE, + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + $this->assertText('Administrative title field is required.'); + + // Add a new page with a label. + $edit += ['label' => 'Foo']; + $this->drupalPostForm(NULL, $edit, 'Next'); + + // Test the 'Page access' step. + $this->assertTitle('Page access | Drupal'); + $access_path = 'admin/structure/page_manager/add/foo/access'; + $this->assertUrl($access_path . '?js=nojs'); + $this->doTestAccessConditions($access_path, FALSE); + $this->drupalPostForm(NULL, [], 'Next'); + + // Test the 'Selection criteria' step. + $this->assertTitle('Selection criteria | Drupal'); + $selection_path = 'admin/structure/page_manager/add/foo/selection'; + $this->assertUrl($selection_path . '?js=nojs'); + $this->doTestSelectionCriteria($selection_path, FALSE); + $this->drupalPostForm(NULL, [], 'Next'); + + // Configure the variant. + $edit = [ + 'page_variant_label' => 'Status Code', + 'variant_settings[status_code]' => 200, + ]; + $this->drupalPostForm(NULL, $edit, 'Finish'); + $this->assertRaw(new FormattableMarkup('Saved the %label Page.', ['%label' => 'Foo'])); + // We've gone from the add wizard to the edit wizard. + $this->drupalGet('admin/structure/page_manager/manage/foo/general'); + + $this->drupalGet('admin/foo'); + $this->assertResponse(200); + $this->assertTitle('Foo | Drupal'); + + // Change the status code to 403. + $this->drupalGet('admin/structure/page_manager/manage/foo/page_variant__foo-http_status_code-0__general'); + $edit = [ + 'variant_settings[status_code]' => 403, + ]; + $this->drupalPostForm(NULL, $edit, 'Update'); + + // Set the weight of the 'Status Code' variant to 10. + $this->drupalGet('admin/structure/page_manager/manage/foo/reorder_variants'); + $edit = [ + 'variants[foo-http_status_code-0][weight]' => 10, + ]; + $this->drupalPostForm(NULL, $edit, 'Update'); + $this->drupalPostForm(NULL, [], 'Update and save'); + } + + /** + * Tests access conditions step on both add and edit wizard. + * + * @param string $path + * The path this step is supposed to be at. + * @param bool|TRUE $redirect + * Whether or not to redirect to the path. + */ + protected function doTestAccessConditions($path = 'admin/structure/page_manager/manage/foo/access', $redirect = TRUE) { + if ($this->getUrl() !== $path && $redirect) { + $this->drupalGet($path); + } + + $this->assertRaw('No required conditions have been configured.'); + + // Configure a new condition. + $edit = [ + 'conditions' => 'user_role', + ]; + $this->drupalPostForm(NULL, $edit, 'Add Condition'); + $this->assertTitle('Add access condition | Drupal'); + $edit = [ + 'roles[authenticated]' => TRUE, + ]; + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertRaw('The user is a member of Authenticated user'); + // Make sure we're still on the same wizard. + $this->assertUrl($path); + + // Edit the condition. + $this->clickLink('Edit'); + $this->assertTitle('Edit access condition | Drupal'); + $edit = [ + 'roles[anonymous]' => TRUE, + ]; + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertRaw('The user is a member of Anonymous user, Authenticated user'); + $this->assertUrl($path); + + // Delete the condition. + $this->clickLink('Delete'); + $this->assertTitle('Are you sure you want to delete the user_role condition? | Drupal'); + $this->drupalPostForm(NULL, [], 'Delete'); + $this->assertRaw('No required conditions have been configured.'); + $this->assertUrl($path); + } + + /** + * Tests selection criteria step on both add and edit wizard. + * + * @param string $path + * The path this step is supposed to be at. + * @param bool|TRUE $redirect + * Whether or not to redirect to the path. + */ + protected function doTestSelectionCriteria($path = 'admin/structure/page_manager/manage/foo/page_variant__foo-http_status_code-0__selection', $redirect = TRUE) { + if ($this->getUrl() !== $path && $redirect) { + $this->drupalGet($path); + } + $this->assertRaw('No required conditions have been configured.'); + + // Configure a new condition. + $edit = [ + 'conditions' => 'user_role', + ]; + $this->drupalPostForm(NULL, $edit, 'Add Condition'); + $this->assertTitle('Add new selection condition | Drupal'); + $edit = [ + 'roles[authenticated]' => TRUE, + ]; + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertRaw('The user is a member of Authenticated user'); + // Make sure we're still on the add wizard (not the edit wizard). + $this->assertUrl($path); + + // Edit the condition. + $this->clickLink('Edit'); + $this->assertTitle('Edit selection condition | Drupal'); + $edit = [ + 'roles[anonymous]' => TRUE, + ]; + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertRaw('The user is a member of Anonymous user, Authenticated user'); + $this->assertUrl($path); + + // Delete the condition. + $this->clickLink('Delete'); + $this->assertTitle('Are you sure you want to delete the user_role condition? | Drupal'); + $this->drupalPostForm(NULL, [], 'Delete'); + $this->assertRaw('No required conditions have been configured.'); + $this->assertUrl($path); + } + + /** + * Tests the AJAX form for Selection Criteria. + */ + protected function doTestSelectionCriteriaWithAjax() { + $this->drupalGet('admin/structure/page_manager/manage/foo/page_variant__foo-http_status_code-0__selection'); + $edit = [ + 'conditions' => 'user_role', + ]; + $response = $this->drupalPostAjaxForm(NULL, $edit, ['add' => 'Add Condition']); + $this->assertEqual($response[2]['dialogOptions']['title'], 'Configure Required Context'); + } + + /** + * Tests disabling a page. + */ + protected function doTestDisablePage() { + $this->drupalGet('admin/foo'); + $this->assertResponse(403); + + $this->drupalGet('admin/structure/page_manager'); + $this->clickLink('Disable'); + $this->drupalGet('admin/foo'); + // The page should not be found if the page is enabled. + $this->assertResponse(404); + + $this->drupalGet('admin/structure/page_manager'); + $this->clickLink('Enable'); + $this->drupalGet('admin/foo'); + // Re-enabling the page should make this path available. + $this->assertResponse(403); + } + + /** + * Tests adding a variant. + */ + protected function doTestAddVariant() { + $this->drupalGet('admin/structure/page_manager/manage/foo/general'); + + // Add a new variant. + $this->clickLink('Add variant'); + $edit = [ + 'variant_plugin_id' => 'block_display', + 'label' => 'First', + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + + // Set the page title. + $edit = [ + 'variant_settings[page_title]' => 'Example title', + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + + // Finish variant wizard without adding blocks. + $this->drupalPostForm(NULL, [], 'Finish'); + + // Save page to apply variant changes. + $this->drupalPostForm(NULL, [], 'Update and save'); + + // Test that the variant is still used but empty. + $this->drupalGet('admin/foo'); + $this->assertResponse(200); + // Tests that the content region has no content at all. + $elements = $this->xpath('//div[@class=:region]', [':region' => 'region region-content']); + $this->assertIdentical(0, $elements[0]->count()); + } + + /** + * Tests adding a block to a variant. + */ + protected function doTestAddBlock() { + $this->drupalGet('admin/structure/page_manager/manage/foo/page_variant__foo-block_display-0__content'); + // Add a block to the variant. + $this->clickLink('Add new block'); + + // Assert that the broken/missing block is not visible. + $this->assertNoText('Broken/Missing'); + + $this->clickLink('User account menu'); + $edit = [ + 'region' => 'top', + ]; + $this->drupalPostForm(NULL, $edit, 'Add block'); + $this->drupalPostForm(NULL, [], 'Update and save'); + + // Test that the block is displayed. + $this->drupalGet('admin/foo'); + $this->assertResponse(200); + $elements = $this->xpath('//div[@class="block-region-top"]/nav/ul[@class="menu"]/li/a'); + $this->assertTitle('Example title | Drupal'); + $expected = ['My account', 'Log out']; + $links = []; + foreach ($elements as $element) { + $links[] = (string) $element; + } + $this->assertEqual($expected, $links); + // Check the block label. + $this->assertRaw('User account menu'); + } + + /** + * Creates a second page with another block display. + */ + protected function doTestSecondPage() { + $this->drupalGet('admin/structure/page_manager'); + + // Add a new page. + $this->clickLink('Add page'); + $edit = [ + 'id' => 'second', + 'label' => 'Second', + 'path' => 'second', + 'variant_plugin_id' => 'block_display', + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + + // Configure the variant. + $edit = [ + 'page_variant_label' => 'Second variant', + 'variant_settings[page_title]' => 'Second title', + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + + // We're now on the content step, but we don't need to add any blocks. + $this->drupalPostForm(NULL, [], 'Finish'); + $this->assertRaw(new FormattableMarkup('Saved the %label Page.', ['%label' => 'Second'])); + + // Visit both pages, make sure that they do not interfere with each other. + $this->drupalGet('admin/foo'); + $this->assertTitle('Example title | Drupal'); + $this->drupalGet('second'); + $this->assertTitle('Second title | Drupal'); + } + + /** + * Tests editing a block. + */ + protected function doTestEditBlock() { + $this->drupalGet('admin/structure/page_manager/manage/foo/page_variant__foo-block_display-0__general'); + $edit = [ + 'variant_settings[page_title]' => 'Updated block label', + 'page_variant_label' => 'Updated block label', + ]; + $this->drupalPostForm(NULL, $edit, 'Update and save'); + // Test that the block is displayed. + $this->drupalGet('admin/foo'); + $this->assertResponse(200); + // Check the block label. + $this->assertRaw($edit['variant_settings[page_title]']); + } + + /** + * Tests editing a variant. + */ + protected function doTestEditVariant() { + if (!$block = $this->findBlockByLabel('foo-block_display-0', 'User account menu')) { + $this->fail('Block not found'); + return; + } + + $block_config = $block->getConfiguration(); + $this->drupalGet('admin/structure/page_manager/manage/foo/page_variant__foo-block_display-0__content'); + + $this->assertOptionSelected('edit-blocks-' . $block_config['uuid'] . '-region', 'top'); + $this->assertOptionSelected('edit-blocks-' . $block_config['uuid'] . '-weight', 0); + + $form_name = 'blocks[' . $block_config['uuid'] . ']'; + $edit = [ + $form_name . '[region]' => 'bottom', + $form_name . '[weight]' => -10, + ]; + $this->drupalPostForm(NULL, $edit, 'Update'); + $this->assertOptionSelected('edit-blocks-' . $block_config['uuid'] . '-region', 'bottom'); + $this->assertOptionSelected('edit-blocks-' . $block_config['uuid'] . '-weight', -10); + $this->drupalPostForm(NULL, [], 'Update and save'); + } + + /** + * Tests reordering variants. + */ + protected function doTestReorderVariants() { + $this->drupalGet('admin/foo'); + $this->assertResponse(200); + $elements = $this->xpath('//div[@class="block-region-bottom"]/nav/ul[@class="menu"]/li/a'); + $expected = ['My account', 'Log out']; + $links = []; + foreach ($elements as $element) { + $links[] = (string) $element; + } + $this->assertEqual($expected, $links); + + $this->drupalGet('admin/structure/page_manager/manage/foo/general'); + $this->clickLink('Reorder variants'); + + $edit = [ + 'variants[foo-http_status_code-0][weight]' => -10, + ]; + $this->drupalPostForm(NULL, $edit, 'Update'); + $this->drupalPostForm(NULL, [], 'Update and save'); + $this->drupalGet('admin/foo'); + $this->assertResponse(403); + } + + /** + * Tests adding a page with a duplicate path. + */ + protected function doTestAddPageWithDuplicatePath() { + // Try to add a second page with the same path. + $edit = [ + 'label' => 'Bar', + 'id' => 'bar', + 'path' => 'admin/foo', + ]; + $this->drupalPostForm('admin/structure/page_manager/add', $edit, 'Next'); + $this->assertText('The page path must be unique.'); + $this->drupalGet('admin/structure/page_manager'); + $this->assertNoText('Bar'); + } + + /** + * Tests changing the admin theme of a page. + */ + protected function doTestAdminPath() { + $this->config('system.theme')->set('default', 'bartik')->save(); + $this->drupalGet('admin/foo'); + $this->assertTheme('classy'); + + $this->drupalGet('admin/structure/page_manager/manage/foo/general'); + $edit = [ + 'use_admin_theme' => FALSE, + ]; + $this->drupalPostForm(NULL, $edit, 'Update and save'); + $this->drupalGet('admin/foo'); + $this->assertTheme('bartik'); + + // Reset theme. + $this->config('system.theme')->set('default', 'classy')->save(); + } + + /** + * Tests removing a variant. + */ + protected function doTestRemoveVariant() { + $this->drupalGet('admin/structure/page_manager/manage/foo/page_variant__foo-http_status_code-0__general'); + $this->clickLink('Delete this variant'); + $this->assertRaw('Are you sure you want to delete this variant?'); + $this->drupalPostForm(NULL, [], 'Delete'); + $this->assertRaw(new FormattableMarkup('The variant %label has been removed.', ['%label' => 'Status Code'])); + $this->drupalPostForm(NULL, [], 'Update and save'); + } + + /** + * Tests removing a block. + */ + protected function doTestRemoveBlock() { + // Assert that the block is displayed. + $this->drupalGet('admin/foo'); + $this->assertResponse(200); + $elements = $this->xpath('//div[@class="block-region-bottom"]/nav/ul[@class="menu"]/li/a'); + $expected = ['My account', 'Log out']; + $links = []; + foreach ($elements as $element) { + $links[] = (string) $element; + } + $this->assertEqual($expected, $links); + + $this->drupalGet('admin/structure/page_manager/manage/foo/page_variant__foo-block_display-0__content'); + $this->clickLink('Delete'); + $this->assertRaw(new FormattableMarkup('Are you sure you want to delete the block %label?', ['%label' => 'User account menu'])); + $this->drupalPostForm(NULL, [], 'Delete'); + $this->assertRaw(new FormattableMarkup('The block %label has been removed.', ['%label' => 'User account menu'])); + $this->drupalPostForm(NULL, [], 'Update and save'); + + // Assert that the block is now gone. + $this->drupalGet('admin/foo'); + $this->assertResponse(200); + $elements = $this->xpath('//div[@class="block-region-bottom"]/nav/ul[@class="menu"]/li/a'); + $this->assertTrue(empty($elements)); + } + + /** + * Tests adding a block with #ajax to a variant. + */ + protected function doTestAddBlockWithAjax() { + $this->drupalGet('admin/structure/page_manager/manage/foo/page_variant__foo-block_display-0__content'); + // Add a block to the variant. + $this->clickLink('Add new block'); + $this->clickLink('Page Manager Test Block'); + $edit = [ + 'region' => 'top', + ]; + $this->drupalPostForm(NULL, $edit, 'Add block'); + $this->drupalPostForm(NULL, [], 'Update and save'); + + // Test that the block is displayed. + $this->drupalGet('admin/foo'); + $this->assertResponse(200); + $this->assertText(t('Example output')); + // Check the block label. + $this->assertRaw('Page Manager Test Block'); + } + + /** + * Tests adding a page with an existing path with no route parameters. + */ + protected function doTestExistingPathWithoutParameters() { + // Test an existing path. + $this->drupalGet('admin'); + $this->assertResponse(200); + + $this->drupalGet('admin/structure/page_manager'); + // Add a new page with existing path 'admin'. + $this->clickLink('Add page'); + $edit = [ + 'label' => 'existing', + 'id' => 'existing', + 'path' => 'admin', + 'variant_plugin_id' => 'http_status_code', + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + + // Configure the variant. + $edit = [ + 'page_variant_label' => 'Status Code', + 'variant_settings[status_code]' => 404, + ]; + $this->drupalPostForm(NULL, $edit, 'Finish'); + + // Ensure the existing path leads to the new page. + $this->drupalGet('admin'); + $this->assertResponse(404); + } + + /** + * Tests the Update button on Variant forms. + */ + protected function doTestUpdateSubmit() { + // Add a block variant. + $this->drupalGet('admin/structure/page_manager/manage/foo/general'); + + // Add a new variant. + $this->clickLink('Add variant'); + $edit = [ + 'variant_plugin_id' => 'block_display', + 'label' => 'First', + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + + // Set the page title. + $edit = [ + 'variant_settings[page_title]' => 'Example title', + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + + // Finish variant wizard without adding blocks. + $this->drupalPostForm(NULL, [], 'Finish'); + + // Update the description and click on Update. + $edit = [ + 'page_variant_label' => 'First updated', + 'variant_settings[page_title]' => 'Example title updated', + ]; + $this->drupalPostForm('admin/structure/page_manager/manage/foo/page_variant__foo-block_display-0__general', $edit, 'Update'); + $this->assertFieldByName('page_variant_label', 'First updated'); + $this->assertFieldByName('variant_settings[page_title]', 'Example title updated'); + + // Click on Update at Contexts. Nothing should happen. + $this->drupalPostForm('admin/structure/page_manager/manage/foo/page_variant__foo-block_display-0__contexts', [], 'Update'); + $this->assertUrl('admin/structure/page_manager/manage/foo/page_variant__foo-block_display-0__contexts'); + + // Click on Update at Selection criteria. Nothing should happen. + $this->drupalPostForm('admin/structure/page_manager/manage/foo/page_variant__foo-block_display-0__selection', [], 'Update'); + $this->assertUrl('admin/structure/page_manager/manage/foo/page_variant__foo-block_display-0__selection'); + + // Click on Update at Content. Nothing should happen. + $this->drupalPostForm('admin/structure/page_manager/manage/foo/page_variant__foo-block_display-0__content', [], 'Update'); + $this->assertUrl('admin/structure/page_manager/manage/foo/page_variant__foo-block_display-0__content'); + } + + /** + * Tests deleting a page. + */ + protected function doTestDeletePage() { + $this->drupalGet('admin/structure/page_manager'); + $this->clickLink('Delete'); + $this->drupalPostForm(NULL, [], 'Delete'); + $this->assertRaw(new FormattableMarkup('The page %name has been removed.', ['%name' => 'existing'])); + $this->drupalGet('admin'); + // The overridden page is back to its default. + $this->assertResponse(200); + + $this->drupalGet('admin/structure/page_manager'); + $this->clickLink('Delete'); + $this->drupalPostForm(NULL, [], 'Delete'); + $this->assertRaw(new FormattableMarkup('The page %name has been removed.', ['%name' => 'Foo'])); + $this->drupalGet('admin/foo'); + // The custom page is no longer found. + $this->assertResponse(404); + } + + /** + * Tests that default arguments are not removed from existing routes. + */ + public function testExistingRoutes() { + // Test that the page without placeholder is accessible. + $this->drupalGet('admin/structure/page_manager/add'); + $edit = [ + 'label' => 'Placeholder test 2', + 'id' => 'placeholder2', + 'path' => '/page-manager-test', + 'variant_plugin_id' => 'http_status_code', + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + + $edit = [ + 'variant_settings[status_code]' => 418, + ]; + $this->drupalPostForm(NULL, $edit, 'Finish'); + $this->drupalGet('page-manager-test'); + $this->assertResponse(418); + + // Test that the page test is accessible. + $page_string = 'test-page'; + $this->drupalGet('page-manager-test/' . $page_string); + $this->assertResponse(200); + + // Without a single variant, it will fall through to the original. + $this->drupalGet('admin/structure/page_manager/manage/placeholder2/page_variant__placeholder2-http_status_code-0__general'); + $this->clickLink('Delete this variant'); + $this->drupalPostForm(NULL, [], 'Delete'); + $this->drupalPostForm(NULL, [], 'Update and save'); + $this->drupalGet('page-manager-test'); + $this->assertResponse(200); + } + + /** + * Asserts that a theme was used for the page. + * + * @param string $theme_name + * The theme name. + */ + protected function assertTheme($theme_name) { + $url = Url::fromUri('base:core/themes/' . $theme_name . '/logo.svg')->toString(); + $elements = $this->xpath('//img[contains(@src, :url)]', [':url' => $url]); + $this->assertEqual(count($elements), 1, new FormattableMarkup('Page is rendered in @theme', ['@theme' => $theme_name])); + } + + /** + * Finds a block based on its variant and block label. + * + * @param string $page_variant_id + * The ID of the page variant entity. + * @param string $block_label + * The label of the block. + * + * @return \Drupal\Core\Block\BlockPluginInterface|null + * Either a block plugin, or NULL. + */ + protected function findBlockByLabel($page_variant_id, $block_label) { + /** @var \Drupal\page_manager\Entity\PageVariant $page_variant */ + if ($page_variant = PageVariant::load($page_variant_id)) { + /** @var \Drupal\ctools\Plugin\BlockVariantInterface $variant_plugin */ + $variant_plugin = $page_variant->getVariantPlugin(); + foreach ($variant_plugin->getRegionAssignments() as $blocks) { + /** @var \Drupal\Core\Block\BlockPluginInterface[] $blocks */ + foreach ($blocks as $block) { + if ($block->label() == $block_label) { + return $block; + } + } + } + } + return NULL; + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Tests/PageParametersTest.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Tests/PageParametersTest.php new file mode 100644 index 000000000..558f0be75 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Tests/PageParametersTest.php @@ -0,0 +1,105 @@ +drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + + $this->drupalPlaceBlock('local_tasks_block'); + $this->drupalPlaceBlock('local_actions_block'); + $this->drupalPlaceBlock('system_branding_block'); + $this->drupalPlaceBlock('page_title_block'); + + $this->drupalLogin($this->drupalCreateUser([ + 'administer pages', + 'access administration pages', + 'view the administration theme', + 'create article content', + ])); + } + + /** + * Tests page parameters when adding a page and when editing it. + */ + public function testParameters() { + $node = $this->drupalCreateNode(['type' => 'article']); + + // Create a page. + $this->drupalGet('admin/structure'); + $this->clickLink('Pages'); + $this->clickLink('Add page'); + $edit = [ + 'id' => 'foo', + 'label' => 'Foo', + 'path' => 'admin/foo/{node}', + 'variant_plugin_id' => 'block_display', + 'use_admin_theme' => TRUE, + 'description' => 'Sample test page.', + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + + // Test the 'Parameters' step. + $this->assertTitle('Page parameters | Drupal'); + $access_path = 'admin/structure/page_manager/add/foo/parameters'; + $this->assertUrl($access_path . '?js=nojs'); + $this->assertNoText('There are no parameters defined for this page.'); + + // Edit the node parameter. + $this->clickLink('Edit'); + $this->assertTitle('Edit parameter | Drupal'); + $edit = [ + 'type' => 'entity:node', + ]; + $this->drupalPostForm(NULL, $edit, 'Update parameter'); + $this->assertText('The node parameter has been updated.'); + + // Skip the variant General configuration step. + $this->drupalPostForm(NULL, [], 'Next'); + + // Add the Node block to the top region. + $this->drupalPostForm(NULL, [], 'Next'); + $this->clickLink('Add new block'); + $this->clickLink('Entity view (Content)'); + $edit = [ + 'region' => 'top', + ]; + $this->drupalPostForm(NULL, $edit, 'Add block'); + + // Finish the wizard. + $this->drupalPostForm(NULL, [], 'Finish'); + $this->assertRaw(new FormattableMarkup('Saved the %label Page.', ['%label' => 'Foo'])); + + // Check that the node's title is visible at the page. + $this->drupalGet('admin/foo/' . $node->id()); + $this->assertResponse(200); + $this->assertText($node->getTitle()); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Tests/StaticContextTest.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Tests/StaticContextTest.php new file mode 100644 index 000000000..ca5d9b238 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Tests/StaticContextTest.php @@ -0,0 +1,240 @@ +drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + $this->drupalLogin($this->drupalCreateUser(['administer pages', 'create article content'])); + + $this->drupalPlaceBlock('page_title_block'); + } + + /** + * Tests that a node bundle condition controls the node view page. + */ + public function testStaticContext() { + // Create a node, and check its page. + $node = $this->drupalCreateNode(['type' => 'article']); + $node2 = $this->drupalCreateNode(['type' => 'article']); + $this->drupalGet('node/' . $node->id()); + $this->assertResponse(200); + $this->assertText($node->label()); + $this->assertTitle($node->label() . ' | Drupal'); + + // Create a new page entity. + $edit_page = [ + 'label' => 'Static node context', + 'id' => 'static_node_context', + 'path' => 'static-context', + 'variant_plugin_id' => 'block_display', + 'wizard_options[contexts]' => TRUE, + ]; + $this->drupalPostForm('admin/structure/page_manager/add', $edit_page, 'Next'); + + // Add a static context for each node to the page variant. + $contexts = array( + array( + 'title' => 'Static Node', + 'machine_name' => 'static_node', + 'description' => 'Static node 1', + 'node' => $node, + ), + array( + 'title' => 'Static Node 2', + 'machine_name' => 'static_node_2', + 'description' => 'Static node 2', + 'node' => $node2, + ), + ); + foreach ($contexts as $context) { + $edit = [ + 'context' => 'entity:node', + ]; + $this->drupalPostForm(NULL, $edit, 'Add new context'); + $edit = [ + 'label' => $context['title'], + 'machine_name' => $context['machine_name'], + 'description' => $context['description'], + 'context_value' => $context['node']->getTitle() . ' (' . $context['node']->id() . ')', + ]; + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText($context['title']); + } + $this->drupalPostForm(NULL, [], 'Next'); + + // Add a new variant. + $variant_edit = [ + 'variant_settings[page_title]' => 'Static context test page', + ]; + $this->drupalPostForm(NULL, $variant_edit, 'Next'); + + // Add a block that renders the node from the first static context. + $this->clickLink('Add new block'); + $this->clickLink('Entity view (Content)'); + $edit = [ + 'settings[label]' => 'Static node view', + 'settings[label_display]' => 1, + 'settings[view_mode]' => 'default', + 'region' => 'top', + ]; + $this->drupalPostForm(NULL, $edit, 'Add block'); + $this->assertText($edit['settings[label]']); + + // Add a block that renders the node from the second static context. + $this->clickLink('Add new block'); + $this->clickLink('Entity view (Content)'); + $edit = [ + 'settings[label]' => 'Static node 2 view', + 'settings[label_display]' => 1, + 'settings[view_mode]' => 'default', + 'region' => 'bottom', + 'context_mapping[entity]' => $contexts[1]['machine_name'], + ]; + $this->drupalPostForm(NULL, $edit, 'Add block'); + $this->assertText($edit['settings[label]']); + $this->drupalPostForm(NULL, [], 'Finish'); + + // Open the page and verify that the node from the static context is there. + $this->drupalGet($edit_page['path']); + $this->assertText($node->label()); + $this->assertText($node->get('body')->getValue()[0]['value']); + $this->assertText($node2->label()); + $this->assertText($node2->get('body')->getValue()[0]['value']); + + // Change the second static context to the first node. + $this->drupalGet('admin/structure/page_manager/manage/static_node_context/page_variant__static_node_context-block_display-0__contexts'); + $this->clickLink('Edit', 1); + $edit = [ + 'label' => 'Static Node 2 edited', + 'context_value' => $node->getTitle(), + ]; + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText("Static Node 2 edited"); + $this->drupalPostForm(NULL, [], 'Update and save'); + + // Open the page and verify that the node from the static context is there. + $this->drupalGet($edit_page['path']); + $this->assertText($node->label()); + $this->assertText($node->get('body')->getValue()[0]['value']); + // Also make sure the second node is NOT there. + $this->assertNoText($node2->label()); + $this->assertNoText($node2->get('body')->getValue()[0]['value']); + + // Change the first static context to the second node. + $this->drupalGet('admin/structure/page_manager/manage/static_node_context/page_variant__static_node_context-block_display-0__contexts'); + $this->clickLink('Edit'); + $edit = array( + 'label' => 'Static Node edited', + 'context_value' => $node2->getTitle(), + ); + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText("Static Node 2 edited"); + + // Remove the second static context view block from the variant. + $this->drupalGet('admin/structure/page_manager/manage/static_node_context/page_variant__static_node_context-block_display-0__content'); + $this->clickLink('Delete', 1); + $this->drupalPostForm(NULL, [], t('Delete')); + $this->drupalPostForm(NULL, [], 'Update and save'); + + // Make sure only the second static context's node is rendered on the page. + $this->drupalGet($edit_page['path']); + $this->assertNoText($node->label()); + $this->assertNoText($node->get('body')->getValue()[0]['value']); + $this->assertText($node2->label()); + $this->assertText($node2->get('body')->getValue()[0]['value']); + + // Delete a static context and verify that it was deleted. + $this->drupalGet('admin/structure/page_manager/manage/static_node_context/page_variant__static_node_context-block_display-0__contexts'); + $this->clickLink('Delete'); + $this->drupalPostForm(NULL, [], t('Delete')); + $this->assertText("The static context Static Node edited has been removed."); + // Reload the page to clear the message + $this->drupalGet($this->getUrl()); + $this->assertNoText($edit['label']); + + // Test contexts in a new variant. + $this->drupalGet('admin/structure/page_manager/manage/static_node_context/general'); + $this->clickLink('Add variant'); + $edit = [ + 'label' => 'Variant two', + 'variant_plugin_id' => 'block_display', + 'wizard_options[contexts]' => TRUE, + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + foreach ($contexts as $context) { + $edit = [ + 'context' => 'entity:node', + ]; + $this->drupalPostForm(NULL, $edit, 'Add new context'); + $edit = [ + 'label' => $context['title'], + 'machine_name' => $context['machine_name'], + 'description' => $context['description'], + 'context_value' => $context['node']->getTitle() . ' (' . $context['node']->id() . ')', + ]; + $this->drupalPostForm(NULL, $edit, 'Save'); + $this->assertText($context['title']); + } + $this->drupalPostForm(NULL, [], 'Next'); + + // Configure the new variant. + $variant_edit = [ + 'variant_settings[page_title]' => 'Variant two static context test', + ]; + $this->drupalPostForm(NULL, $variant_edit, 'Next'); + + // Add a block that renders the node from the first static context. + $this->clickLink('Add new block'); + $this->clickLink('Entity view (Content)'); + $edit = [ + 'settings[label]' => 'Static node view', + 'settings[label_display]' => 1, + 'settings[view_mode]' => 'default', + 'region' => 'top', + ]; + $this->drupalPostForm(NULL, $edit, 'Add block'); + $this->assertText($edit['settings[label]']); + + // Add a block that renders the node from the second static context. + $this->clickLink('Add new block'); + $this->clickLink('Entity view (Content)'); + $edit = [ + 'settings[label]' => 'Static node 2 view', + 'settings[label_display]' => 1, + 'settings[view_mode]' => 'default', + 'region' => 'bottom', + 'context_mapping[entity]' => $contexts[1]['machine_name'], + ]; + $this->drupalPostForm(NULL, $edit, 'Add block'); + $this->assertText($edit['settings[label]']); + $this->drupalPostForm(NULL, [], 'Finish'); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Wizard/PageAddWizard.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Wizard/PageAddWizard.php new file mode 100644 index 000000000..8bf2213c4 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Wizard/PageAddWizard.php @@ -0,0 +1,78 @@ + $this->t('Contexts'), + 'form' => PageVariantContextsForm::class, + ]; + $operations['selection'] = [ + 'title' => $this->t('Selection criteria'), + 'form' => PageVariantSelectionForm::class, + ]; + $operations['display_variant'] = [ + 'title' => $this->t('Configure variant'), + 'form' => PageVariantConfigureForm::class, + ]; + + // Hide the Parameters step if there aren't any path parameters. + if (isset($cached_values['page']) && !$cached_values['page']->getParameterNames()) { + unset($operations['parameters']); + } + + // Hide any optional steps that aren't selected. + $optional_steps = ['access', 'contexts', 'selection']; + foreach ($optional_steps as $step_name) { + if (empty($cached_values['wizard_options'][$step_name])) { + unset($operations[$step_name]); + } + } + + // Add any wizard operations from the plugin itself. + if (!empty($cached_values['page_variant'])) { + /** @var \Drupal\page_manager\PageVariantInterface $page_variant */ + $page_variant = $cached_values['page_variant']; + $variant_plugin = $page_variant->getVariantPlugin(); + if ($variant_plugin instanceof PluginWizardInterface) { + if ($variant_plugin instanceof ContextAwareVariantInterface) { + $variant_plugin->setContexts($page_variant->getContexts()); + } + $cached_values['plugin'] = $variant_plugin; + foreach ($variant_plugin->getWizardOperations($cached_values) as $name => $operation) { + $operation['values']['plugin'] = $variant_plugin; + $operation['submit'][] = '::submitVariantStep'; + $operations[$name] = $operation; + } + } + } + + return $operations; + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Wizard/PageEditWizard.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Wizard/PageEditWizard.php new file mode 100644 index 000000000..e9a413bdc --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Wizard/PageEditWizard.php @@ -0,0 +1,314 @@ +getVariants(); + if (!empty($cached_values['deleted_variants'])) { + foreach (array_keys($cached_values['deleted_variants']) as $page_variant_id) { + // @todo There's a bug that adds non-variants to the deleted_variants + // key in the cached_values. This has something to do with adding a + // block_page variant to a page in tempstore that's already had a + // variant previously deleted and then reordering the blocks in a + // region. It's pretty weird, and as we rebuild that UI, I suspect it + // will go away, but the keys aren't manipulated, so we use them + // instead of the entity. + unset($variants[$page_variant_id]); + } + } + // Suppress errors because of https://bugs.php.net/bug.php?id=50688. + @uasort($variants, '\Drupal\page_manager\Entity\PageVariant::sort'); + + foreach ($variants as $page_variant) { + $page_variant->setPageEntity($page); + foreach ($this->getVariantOperations($page_variant, $cached_values) as $name => $operation) { + $operation['values']['page_variant'] = $page_variant; + $operation['breadcrumbs'] = [ + $this->t('Variants'), + $page_variant->label() ?: $this->t('Variant'), + ]; + $operations['page_variant__' . $page_variant->id() . '__' . $name] = $operation; + } + } + } + + return $operations; + } + + /** + * Get operations for the variant. + * + * @param \Drupal\page_manager\PageVariantInterface $page_variant + * The page variant entity. + * @param mixed $cached_values + * The cached values. + * + * @returns array + */ + protected function getVariantOperations(PageVariantInterface $page_variant, $cached_values) { + $operations = []; + $operations['general'] = [ + 'title' => $this->t('General'), + 'form' => PageVariantConfigureForm::class, + ]; + $operations['contexts'] = [ + 'title' => $this->t('Contexts'), + 'form' => PageVariantContextsForm::class, + ]; + $operations['selection'] = [ + 'title' => $this->t('Selection criteria'), + 'form' => PageVariantSelectionForm::class, + ]; + + // Add any wizard operations from the plugin itself. + $variant_plugin = $page_variant->getVariantPlugin(); + if ($variant_plugin instanceof PluginWizardInterface) { + if ($variant_plugin instanceof ContextAwareVariantInterface) { + $variant_plugin->setContexts($page_variant->getContexts()); + } + $cached_values['plugin'] = $variant_plugin; + foreach ($variant_plugin->getWizardOperations($cached_values) as $name => $operation) { + $operation['values']['plugin'] = $variant_plugin; + $operation['submit'][] = '::submitVariantStep'; + $operations[$name] = $operation; + } + } + + return $operations; + } + + /** + * Get action links for the page. + * + * @return array + * An array of associative arrays with the following keys: + * - title: The link text + * - url: A URL object + */ + protected function getPageActionLinks(PageInterface $page) { + $links = []; + + $links[] = [ + 'title' => $this->t('Delete page'), + 'url' => new Url('entity.page.delete_form', [ + 'page' => $this->getMachineName(), + ]), + ]; + + $links[] = [ + 'title' => $this->t('Add variant'), + 'url' => new Url('entity.page_variant.add_form', [ + 'page' => $this->getMachineName(), + ]), + ]; + + $links[] = [ + 'title' => $this->t('Reorder variants'), + 'url' => new Url('entity.page.reorder_variants_form', [ + 'machine_name' => $this->getMachineName(), + ]), + ]; + + return $links; + } + + /** + * {@inheritdoc} + */ + protected function customizeForm(array $form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var $page \Drupal\page_manager\Entity\Page */ + $page = $cached_values['page']; + + // The page actions. + $form['wizard_actions'] = [ + '#theme' => 'links', + '#links' => [], + '#attributes' => [ + 'class' => ['inline'], + ] + ]; + foreach ($this->getPageActionLinks($page) as $action) { + $form['wizard_actions']['#links'][] = $action + [ + 'attributes' => [ + 'class' => 'use-ajax', + 'data-dialog-type' => 'modal', + 'data-dialog-options' => Json::encode([ + 'width' => 700, + ]), + ], + ]; + } + + // The tree of wizard steps. + $form['wizard_tree'] = [ + '#theme' => ['page_manager_wizard_tree'], + '#wizard' => $this, + '#cached_values' => $form_state->getTemporaryValue('wizard'), + ]; + + $form['#theme'] = 'page_manager_wizard_form'; + $form['#attached']['library'][] = 'page_manager_ui/admin'; + return $form; + } + + /** + * {@inheritdoc} + */ + protected function actions(FormInterface $form_object, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + $operation = $this->getOperation($cached_values); + + $actions = []; + + $actions['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Update'), + '#validate' => [ + '::populateCachedValues', + [$form_object, 'validateForm'], + ], + '#submit' => [ + [$form_object, 'submitForm'], + ], + ]; + + $actions['update_and_save'] = [ + '#type' => 'submit', + '#value' => $this->t('Update and save'), + '#button_type' => 'primary', + '#validate' => [ + '::populateCachedValues', + [$form_object, 'validateForm'], + ], + '#submit' => [ + [$form_object, 'submitForm'], + ], + ]; + + $actions['finish'] = [ + '#type' => 'submit', + '#value' => $this->t('Save'), + '#validate' => [ + '::populateCachedValues', + [$form_object, 'validateForm'], + ], + '#submit' => [ + [$form_object, 'submitForm'], + ], + ]; + + $actions['cancel'] = [ + '#type' => 'submit', + '#value' => $this->t('Cancel'), + '#submit' => [ + '::clearTempstore' + ], + ]; + + // Add any submit or validate functions for the step and the global ones. + foreach (['submit', 'update_and_save', 'finish'] as $button) { + if (isset($operation['validate'])) { + $actions[$button]['#validate'] = array_merge($actions[$button]['#validate'], $operation['validate']); + } + $actions[$button]['#validate'][] = '::validateForm'; + if (isset($operation['submit'])) { + $actions[$button]['#submit'] = array_merge($actions[$button]['#submit'], $operation['submit']); + } + $actions[$button]['#submit'][] = '::submitForm'; + } + $actions['update_and_save']['#submit'][] = '::finish'; + $actions['finish']['#submit'][] = '::finish'; + + if ($form_state->get('ajax')) { + $cached_values = $form_state->getTemporaryValue('wizard'); + $ajax_parameters = $this->getNextParameters($cached_values); + $ajax_parameters['step'] = $this->getStep($cached_values); + $actions['submit']['#ajax'] = [ + 'callback' => '::ajaxSubmit', + 'url' => Url::fromRoute($this->getRouteName(), $ajax_parameters), + 'options' => ['query' => \Drupal::request()->query->all() + [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], + ]; + $actions['update_and_save']['#ajax'] = [ + 'callback' => '::ajaxFinish', + 'url' => Url::fromRoute($this->getRouteName(), $ajax_parameters), + 'options' => ['query' => \Drupal::request()->query->all() + [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], + ]; + $actions['finish']['#ajax'] = [ + 'callback' => '::ajaxFinish', + 'url' => Url::fromRoute($this->getRouteName(), $ajax_parameters), + 'options' => ['query' => \Drupal::request()->query->all() + [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]], + ]; + } + + return $actions; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + // Normally, the wizard only saves the data when the 'Next' button is + // clicked, but we want to save the data always when editing. + $this->getTempstore()->set($this->getMachineName(), $cached_values); + } + + /** + * @inheritDoc + */ + public function finish(array &$form, FormStateInterface $form_state) { + // Delete any of the variants marked for deletion. + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var \Drupal\page_manager\Entity\Page $page */ + $page = $cached_values['page']; + if (!empty($cached_values['deleted_variants'])) { + foreach (array_keys($cached_values['deleted_variants']) as $page_variant_id) { + $page->removeVariant($page_variant_id); + } + } + + parent::finish($form, $form_state); + } + + /** + * Clears the temporary store. + * + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + */ + public function clearTempstore(array &$form, FormStateInterface $form_state) { + $this->getTempstore()->delete($this->getMachineName()); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Wizard/PageVariantAddWizard.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Wizard/PageVariantAddWizard.php new file mode 100644 index 000000000..9ebf82b78 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Wizard/PageVariantAddWizard.php @@ -0,0 +1,198 @@ +t('Page Variant'); + } + + /** + * {@inheritdoc} + */ + public function getMachineLabel() { + return $this->t('Label'); + } + + /** + * {@inheritdoc} + */ + public function getRouteName() { + return 'entity.page_variant.add_step_form'; + } + + /** + * {@inheritdoc} + */ + public function initValues() { + $cached_values = parent::initValues(); + $cached_values['access'] = new PageManagerPluginAccess(); + return $cached_values; + } + + /** + * {@inheritdoc} + */ + public function getOperations($cached_values) { + $operations = []; + $operations['type'] = [ + 'title' => $this->t('Page variant type'), + 'form' => PageVariantAddForm::class, + ]; + $operations['contexts'] = [ + 'title' => $this->t('Contexts'), + 'form' => AddVariantContextsForm::class, + ]; + $operations['selection'] = [ + 'title' => $this->t('Selection criteria'), + 'form' => AddVariantSelectionForm::class, + ]; + $operations['configure'] = [ + 'title' => $this->t('Configure variant'), + 'form' => PageVariantConfigureForm::class, + ]; + + // Hide any optional steps that aren't selected. + $optional_steps = ['selection', 'contexts']; + foreach ($optional_steps as $step_name) { + if (isset($cached_values['wizard_options']) && empty($cached_values['wizard_options'][$step_name])) { + unset($operations[$step_name]); + } + } + + // Add any wizard operations from the plugin itself. + if (!empty($cached_values['page_variant']) && !empty($cached_values['variant_plugin_id'])) { + /** @var \Drupal\page_manager\PageVariantInterface $page_variant */ + $page_variant = $cached_values['page_variant']; + $variant_plugin = $page_variant->getVariantPlugin(); + if ($variant_plugin instanceof PluginWizardInterface) { + if ($variant_plugin instanceof ContextAwareVariantInterface) { + $variant_plugin->setContexts($page_variant->getContexts()); + } + $cached_values['plugin'] = $variant_plugin; + foreach ($variant_plugin->getWizardOperations($cached_values) as $name => $operation) { + $operation['values']['plugin'] = $variant_plugin; + $operations[$name] = $operation; + } + } + } + + return $operations; + } + + /** + * {@inheritdoc} + */ + protected function customizeForm(array $form, FormStateInterface $form_state) { + $form = parent::customizeForm($form, $form_state); + + // We set the variant id as part of form submission. + if ($this->step == 'type' && isset($form['name']['id'])) { + unset($form['name']['id']); + } + + return $form; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $page = NULL) { + $form = parent::buildForm($form, $form_state); // TODO: Change the autogenerated stub + + // Get the page tempstore so we can modify the unsaved page. + if (!isset($cached_values['page']) || !$cached_values['page']->id()) { + $cached_values = $form_state->getTemporaryValue('wizard'); + $page_tempstore = $this->tempstore->get('page_manager.page')->get($page); + $cached_values['page'] = $page_tempstore['page']; + $form_state->setTemporaryValue('wizard', $cached_values); + } + + // Hide form elements that are not useful during the add wizard. + if ($this->step == 'configure') { + $form['page_variant_label']['#type'] = 'value'; + unset($form['delete']); + } + + return $form; + } + + /** + * {@inheritdoc} + */ + public function getNextParameters($cached_values) { + $parameters = parent::getNextParameters($cached_values); + + // Add the page to the url parameters. + $parameters['page'] = $cached_values['page']->id(); + return $parameters; + } + + /** + * {@inheritdoc} + */ + public function getPreviousParameters($cached_values) { + $parameters = parent::getPreviousParameters($cached_values); + + // Add the page to the url parameters. + $parameters['page'] = $cached_values['page']->id(); + return $parameters; + } + + /** + * {@inheritdoc} + */ + public function finish(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + + // Add the variant to the parent page tempstore. + $page_tempstore = $this->tempstore->get('page_manager.page')->get($cached_values['page']->id()); + $page_tempstore['page']->addVariant($cached_values['page_variant']); + $this->tempstore->get('page_manager.page')->set($cached_values['page']->id(), $page_tempstore); + + $variant_plugin = $cached_values['page_variant']->getVariantPlugin(); + drupal_set_message($this->t('The %label @entity_type has been added to the page, but has not been saved. Please save the page to store changes.', array( + '%label' => $cached_values['page_variant']->label(), + '@entity_type' => $variant_plugin->adminLabel(), + ))); + + $form_state->setRedirectUrl(new Url('entity.page.edit_form', [ + 'machine_name' => $cached_values['page']->id(), + 'step' => 'general', + ])); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Wizard/PageWizardBase.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Wizard/PageWizardBase.php new file mode 100644 index 000000000..e6529f24e --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Wizard/PageWizardBase.php @@ -0,0 +1,117 @@ +t('Page Manager'); + } + + /** + * {@inheritdoc} + */ + public function getMachineLabel() { + return $this->t('Administrative title'); + } + + /** + * {@inheritdoc} + */ + public function getOperations($cached_values) { + $operations = []; + $operations['general'] = [ + 'title' => $this->t('Page information'), + 'form' => PageGeneralForm::class, + ]; + /** @var $page \Drupal\page_manager\Entity\Page */ + $page = $cached_values['page']; + + if ($page) { + $matches = []; + preg_match_all('|\{\w+\}|', $page->getPath(), $matches); + if (array_filter($matches)) { + $operations['parameters'] = [ + 'title' => $this->t('Page parameters'), + 'form' => PageParametersForm::class, + ]; + } + } + $operations['access'] = [ + 'title' => $this->t('Page access'), + 'form' => PageAccessForm::class, + ]; + + return $operations; + } + + /** + * Submission callback for the variant plugin steps. + */ + public function submitVariantStep(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var \Drupal\page_manager\PageVariantInterface $page_variant */ + $page_variant = $cached_values['page_variant']; + /** @var \Drupal\Core\Display\VariantInterface $plugin */ + $plugin = $cached_values['plugin']; + + // Make sure the variant plugin on the page variant gets the configuration + // from the 'plugin' which should have been setup by the variant's steps. + if (!empty($plugin) && !empty($page_variant)) { + $page_variant->getVariantPlugin()->setConfiguration($plugin->getConfiguration()); + } + } + + public function finish(array &$form, FormStateInterface $form_state) { + parent::finish($form, $form_state); + + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var \Drupal\page_manager\Entity\Page $page */ + $page = $cached_values['page']; + foreach($page->getVariants() as $variant) { + $variant->save(); + } + + $form_state->setRedirectUrl(new Url('entity.page.edit_form', [ + 'machine_name' => $this->machine_name, + 'step' => $this->step + ])); + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/src/Wizard/RouteParameters.php b/docroot/modules/contrib/page_manager/page_manager_ui/src/Wizard/RouteParameters.php new file mode 100644 index 000000000..26996dc6e --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/src/Wizard/RouteParameters.php @@ -0,0 +1,86 @@ + [ + 'title' => $this->t('Assign Parameter Context'), + 'form' => ParameterAssignContextForm::class, + ], + 'settings' => [ + 'title' => $this->t('Parameter Settings'), + 'form' => ParameterSettingsForm::class, + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function getRouteName() { + return 'page_manager.route.parameters.configure'; + } + + /** + * Override to get the parameter from the URL and make it available to steps. + */ + public function buildForm(array $form, FormStateInterface $form_state, $parameter = NULL) { + $this->parameter = $parameter; + return parent::buildForm($form, $form_state); + } + + public function getNextParameters($cached_values) { + $parameters = parent::getNextParameters($cached_values); + $parameters['parameter'] = $this->parameter; + return $parameters; + } + + public function getPreviousParameters($cached_values) { + $parameters = parent::getPreviousParameters($cached_values); + $parameters['parameter'] = $this->parameter; + return $parameters; + } + + /** + * Save the values to the tempstore. + */ + public function finish(array &$form, FormStateInterface $form_state) { + $this->getTempstore()->set($this->getMachineName(), $form_state->getTemporaryValue('wizard')); + } + + public function ajaxFinish(array $form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + $response = new AjaxResponse(); + $response->addCommand(new RedirectCommand($this->url('entity.page.edit_form', ['machine_name' => $cached_values['id'], 'step' => 'parameters']))); + $response->addCommand(new CloseModalDialogCommand()); + return $response; + } + +} diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/templates/page-manager-wizard-form.html.twig b/docroot/modules/contrib/page_manager/page_manager_ui/templates/page-manager-wizard-form.html.twig new file mode 100644 index 000000000..82b45d99d --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/templates/page-manager-wizard-form.html.twig @@ -0,0 +1,31 @@ +{# +/** + * @file + * Default theme implementation for a 'form' element. + * + * Available variables + * - attributes: A list of HTML attributes for the wrapper element. + * - children: The child elements of the form. + * + * @see template_preprocess_form() + * + * @ingroup themeable + */ +#} +
+
+ {{ form.wizard_actions }} +
+
+
+ {{ form.wizard_tree }} +
+
+ {{ form|without('wizard_actions', 'wizard_tree', 'actions') }} +
+
+ +
+ {{ form.actions }} +
+
diff --git a/docroot/modules/contrib/page_manager/page_manager_ui/templates/page-manager-wizard-tree.html.twig b/docroot/modules/contrib/page_manager/page_manager_ui/templates/page-manager-wizard-tree.html.twig new file mode 100644 index 000000000..b3527c396 --- /dev/null +++ b/docroot/modules/contrib/page_manager/page_manager_ui/templates/page-manager-wizard-tree.html.twig @@ -0,0 +1,58 @@ +{# +/** + * @file + * Default theme implementation to display wizard tree. + * + * Available variables: + * - step: The current step name. + * - tree: A nested list of menu items. Each menu item contains: + * - title: The menu link title. + * - url: The menu link url, instance of \Drupal\Core\Url + * - children: The menu item child items. + * - step: The name of the step. + * + * @ingroup themeable + */ +#} + +{{ attach_library('page_manager_ui/page_variants') }} + +{% import _self as page_manager %} + +{# + We call a macro which calls itself to render the full tree. + @see http://twig.sensiolabs.org/doc/tags/macro.html +#} +{{ page_manager.wizard_tree(tree, step, 0) }} + +{% macro wizard_tree(items, step, menu_level) %} + {% import _self as page_manager %} + {% if items %} + + {% endif %} +{% endmacro %} diff --git a/docroot/modules/contrib/page_manager/src/Context/EntityLazyLoadContext.php b/docroot/modules/contrib/page_manager/src/Context/EntityLazyLoadContext.php new file mode 100644 index 000000000..1c9f722e4 --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Context/EntityLazyLoadContext.php @@ -0,0 +1,71 @@ +entityRepository = $entity_repository; + $this->uuid = $uuid; + } + + /** + * {@inheritdoc} + */ + public function getContextValue() { + if (!$this->contextData) { + $entity_type_id = substr($this->contextDefinition->getDataType(), 7); + $this->setContextValue($this->entityRepository->loadEntityByUuid($entity_type_id, $this->uuid)); + } + return parent::getContextValue(); + } + + /** + * {@inheritdoc} + */ + public function hasContextValue() { + // Ensure that the entity is loaded before checking if it exists. + if (!$this->contextData) { + $this->getContextValue(); + } + return parent::hasContextValue(); + } + +} diff --git a/docroot/modules/contrib/page_manager/src/ContextMapper.php b/docroot/modules/contrib/page_manager/src/ContextMapper.php new file mode 100644 index 000000000..101bf2fd4 --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/ContextMapper.php @@ -0,0 +1,55 @@ +entityRepository = $entity_repository; + } + + /** + * {@inheritdoc} + */ + public function getContextValues(array $context_configurations) { + $contexts = []; + foreach ($context_configurations as $name => $context_configuration) { + $context_definition = new ContextDefinition($context_configuration['type'], $context_configuration['label']); + if (strpos($context_configuration['type'], 'entity:') === 0) { + $context = new EntityLazyLoadContext($context_definition, $this->entityRepository, $context_configuration['value']); + } + else { + $context = new Context($context_definition, $context_configuration['value']); + } + $contexts[$name] = $context; + } + return $contexts; + } + +} diff --git a/docroot/modules/contrib/page_manager/src/ContextMapperInterface.php b/docroot/modules/contrib/page_manager/src/ContextMapperInterface.php new file mode 100644 index 000000000..88dd3815b --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/ContextMapperInterface.php @@ -0,0 +1,26 @@ +description; + } + + /** + * {@inheritdoc} + */ + public function getPath() { + return $this->path; + } + + /** + * {@inheritdoc} + */ + public function usesAdminTheme() { + return isset($this->use_admin_theme) ? $this->use_admin_theme : strpos($this->getPath(), '/admin/') === 0; + } + + /** + * {@inheritdoc} + */ + public function postSave(EntityStorageInterface $storage, $update = TRUE) { + parent::postSave($storage, $update); + static::routeBuilder()->setRebuildNeeded(); + } + + /** + * {@inheritdoc} + */ + public static function postDelete(EntityStorageInterface $storage, array $entities) { + parent::postDelete($storage, $entities); + static::routeBuilder()->setRebuildNeeded(); + } + + /** + * Wraps the route builder. + * + * @return \Drupal\Core\Routing\RouteBuilderInterface + * An object for state storage. + */ + protected static function routeBuilder() { + return \Drupal::service('router.builder'); + } + + /** + * Wraps the entity storage for page variants. + * + * @return \Drupal\Core\Entity\EntityStorageInterface + */ + protected function variantStorage() { + return \Drupal::service('entity_type.manager')->getStorage('page_variant'); + } + + /** + * {@inheritdoc} + */ + public function getPluginCollections() { + return [ + 'access_conditions' => $this->getAccessConditions(), + ]; + } + + /** + * {@inheritdoc} + */ + public function getAccessConditions() { + if (!$this->accessConditionCollection) { + $this->accessConditionCollection = new ConditionPluginCollection(\Drupal::service('plugin.manager.condition'), $this->get('access_conditions')); + } + return $this->accessConditionCollection; + } + + /** + * {@inheritdoc} + */ + public function addAccessCondition(array $configuration) { + $configuration['uuid'] = $this->uuidGenerator()->generate(); + $this->getAccessConditions()->addInstanceId($configuration['uuid'], $configuration); + return $configuration['uuid']; + } + + /** + * {@inheritdoc} + */ + public function getAccessCondition($condition_id) { + return $this->getAccessConditions()->get($condition_id); + } + + /** + * {@inheritdoc} + */ + public function removeAccessCondition($condition_id) { + $this->getAccessConditions()->removeInstanceId($condition_id); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getAccessLogic() { + return $this->access_logic; + } + + /** + * {@inheritdoc} + */ + public function getParameters() { + $names = $this->getParameterNames(); + if ($names) { + return array_intersect_key($this->parameters, array_flip($names)); + } + return []; + } + + /** + * {@inheritdoc} + */ + public function getParameter($name) { + if ($this->hasParameter($name)) { + return $this->parameters[$name]; + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function hasParameter($name) { + return isset($this->parameters[$name]); + } + + /** + * {@inheritdoc} + */ + public function setParameter($name, $type, $label = '') { + $this->parameters[$name] = [ + 'machine_name' => $name, + 'type' => $type, + 'label' => $label, + ]; + // Reset contexts when a parameter is added or changed. + $this->contexts = []; + // Reset the contexts of every variant. + foreach ($this->getVariants() as $page_variant) { + $page_variant->resetCollectedContexts(); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function removeParameter($name) { + unset($this->parameters[$name]); + // Reset contexts when a parameter is removed. + $this->contexts = []; + // Reset the contexts of every variant. + foreach ($this->getVariants() as $page_variant) { + $page_variant->resetCollectedContexts(); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function getParameterNames() { + if (preg_match_all('|\{(\w+)\}|', $this->getPath(), $matches)) { + return $matches[1]; + } + return []; + } + + /** + * {@inheritdoc} + */ + public function preSave(EntityStorageInterface $storage) { + parent::preSave($storage); + + $this->filterParameters(); + } + + /** + * Filters the parameters to remove any without a valid type. + * + * @return $this + */ + protected function filterParameters() { + $names = $this->getParameterNames(); + foreach ($this->get('parameters') as $name => $parameter) { + // Remove parameters without any type, or which are no longer valid. + if (empty($parameter['type']) || !in_array($name, $names)) { + $this->removeParameter($name); + } + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function addContext($name, ContextInterface $value) { + $this->contexts[$name] = $value; + } + + /** + * {@inheritdoc} + */ + public function getContexts() { + // @todo add the other global contexts here as they are added + // @todo maybe come up with a non-hardcoded way of doing this? + $global_contexts = [ + 'current_user' + ]; + if (!$this->contexts) { + $this->eventDispatcher()->dispatch(PageManagerEvents::PAGE_CONTEXT, new PageManagerContextEvent($this)); + foreach ($this->getParameters() as $machine_name => $configuration) { + // Parameters can be updated in the UI, so unless it's a global context + // we'll need to rely on the current settings in the tempstore instead + // of the ones cached in the router. + if (!isset($global_contexts[$machine_name])) { + // First time through, parameters will not be defined by the route. + if (!isset($this->contexts[$machine_name])) { + $cacheability = new CacheableMetadata(); + $cacheability->setCacheContexts(['route']); + + $context_definition = new ContextDefinition($configuration['type'], $configuration['label']); + $context = new Context($context_definition); + $context->addCacheableDependency($cacheability); + $this->contexts[$machine_name] = $context; + } + else { + $this->contexts[$machine_name]->getContextDefinition()->setDataType($configuration['type']); + if (!empty($configuration['label'])) { + $this->contexts[$machine_name]->getContextDefinition()->setLabel($configuration['label']); + } + } + } + } + } + return $this->contexts; + } + + /** + * {@inheritdoc} + */ + public function addVariant(PageVariantInterface $variant) { + // If variants hasn't been initialized, we initialize it before adding the + // new variant. + if ($this->variants === NULL) { + $this->getVariants(); + } + $this->variants[$variant->id()] = $variant; + $this->sortVariants(); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getVariant($variant_id) { + $variants = $this->getVariants(); + if (!isset($variants[$variant_id])) { + throw new \UnexpectedValueException('The requested variant does not exist or is not associated with this page'); + } + return $variants[$variant_id]; + } + + /** + * {@inheritdoc} + */ + public function removeVariant($variant_id) { + $this->getVariant($variant_id)->delete(); + unset($this->variants[$variant_id]); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getVariants() { + if (!isset($this->variants)) { + $this->variants = []; + /** @var \Drupal\page_manager\PageVariantInterface $variant */ + foreach ($this->variantStorage()->loadByProperties(['page' => $this->id()]) as $variant) { + $this->variants[$variant->id()] = $variant; + } + $this->sortVariants(); + } + return $this->variants; + } + + /** + * Sort variants. + */ + protected function sortVariants() { + if (isset($this->variants)) { + // Suppress errors because of https://bugs.php.net/bug.php?id=50688. + @uasort($this->variants, [$this, 'variantSortHelper']); + } + } + + /** + * {@inheritdoc} + */ + public function variantSortHelper($a, $b) { + $a_weight = $a->getWeight(); + $b_weight = $b->getWeight(); + if ($a_weight == $b_weight) { + return 0; + } + + return ($a_weight < $b_weight) ? -1 : 1; + } + + /** + * Wraps the event dispatcher. + * + * @return \Symfony\Component\EventDispatcher\EventDispatcherInterface + * The event dispatcher. + */ + protected function eventDispatcher() { + return \Drupal::service('event_dispatcher'); + } + + /** + * {@inheritdoc} + */ + public function __sleep() { + $vars = parent::__sleep(); + + // Gathered contexts objects should not be serialized. + if (($key = array_search('contexts', $vars)) !== FALSE) { + unset($vars[$key]); + } + + return $vars; + } + + /** + * {@inheritdoc} + * + * @todo: Remove this as part of https://www.drupal.org/node/2696683. + */ + protected function urlRouteParameters($rel) { + if ($rel == 'edit-form') { + $uri_route_parameters = []; + $uri_route_parameters['machine_name'] = $this->id(); + $uri_route_parameters['step'] = 'general'; + return $uri_route_parameters; + } + + return parent::urlRouteParameters($rel); + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Entity/PageAccess.php b/docroot/modules/contrib/page_manager/src/Entity/PageAccess.php new file mode 100644 index 000000000..faac6451f --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Entity/PageAccess.php @@ -0,0 +1,89 @@ +contextHandler = $context_handler; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('context.handler') + ); + } + + /** + * Wraps the context handler. + * + * @return \Drupal\Core\Plugin\Context\ContextHandlerInterface + */ + protected function contextHandler() { + return $this->contextHandler; + } + + /** + * {@inheritdoc} + */ + protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { + /** @var \Drupal\page_manager\PageInterface $entity */ + if ($operation == 'view') { + if (!$entity->status()) { + return AccessResult::forbidden()->addCacheableDependency($entity); + } + + $contexts = $entity->getContexts(); + $conditions = $entity->getAccessConditions(); + foreach ($conditions as $condition) { + if ($condition instanceof ContextAwarePluginInterface) { + $this->contextHandler()->applyContextMapping($condition, $contexts); + } + } + return AccessResult::allowedIf($this->resolveConditions($conditions, $entity->getAccessLogic())); + } + return parent::checkAccess($entity, $operation, $account); + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Entity/PageAccessCheck.php b/docroot/modules/contrib/page_manager/src/Entity/PageAccessCheck.php new file mode 100644 index 000000000..7d5d6431f --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Entity/PageAccessCheck.php @@ -0,0 +1,37 @@ +getRequirements(); + + // Replace it with our entity access value and run the parent access check. + $route->setRequirement('_entity_access', $route->getRequirement('_page_access')); + $access = parent::access($route, $route_match, $account); + + // Restore the original requirements. + $route->setRequirements($original_requirements); + + return $access; + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Entity/PageVariant.php b/docroot/modules/contrib/page_manager/src/Entity/PageVariant.php new file mode 100644 index 000000000..58ad7f83e --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Entity/PageVariant.php @@ -0,0 +1,473 @@ +getCacheTagsToInvalidate()); + } + + /** + * {@inheritdoc} + */ + protected static function invalidateTagsOnDelete(EntityTypeInterface $entity_type, array $entities) { + parent::invalidateTagsOnDelete($entity_type, $entities); + + // The parent doesn't invalidate the entity cache tags on delete because the + // config system will invalidate them, but since we're using the parent + // page's cache tags, we need to invalidate them special. + $tags = []; + foreach ($entities as $entity) { + $tags = Cache::mergeTags($tags, $entity->getCacheTagsToInvalidate()); + } + Cache::invalidateTags($tags); + } + + /** + * {@inheritdoc} + */ + public function getCacheTagsToInvalidate() { + // We use the same cache tags as the parent page. + return $this->getPage()->getCacheTagsToInvalidate(); + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + parent::calculateDependencies(); + + $this->addDependency('config', $this->getPage()->getConfigDependencyName()); + + foreach ($this->getSelectionConditions() as $instance) { + $this->calculatePluginDependencies($instance); + } + + return $this->getDependencies(); + } + + /** + * {@inheritdoc} + */ + public function getPluginCollections() { + return [ + 'selection_criteria' => $this->getSelectionConditions(), + 'variant_settings' => $this->getVariantPluginCollection(), + ]; + } + + /** + * Get the plugin collection that holds the single variant plugin instance. + * + * @return \Drupal\Core\Plugin\DefaultSingleLazyPluginCollection + * The plugin collection that holds the single variant plugin instance. + */ + protected function getVariantPluginCollection() { + if (!$this->variantPluginCollection) { + if (empty($this->variant_settings['uuid'])) { + $this->variant_settings['uuid'] = $this->uuidGenerator()->generate(); + } + $this->variantPluginCollection = new DefaultSingleLazyPluginCollection(\Drupal::service('plugin.manager.display_variant'), $this->variant, $this->variant_settings); + } + return $this->variantPluginCollection; + } + + /** + * {@inheritdoc} + */ + public function getVariantPlugin() { + return $this->getVariantPluginCollection()->get($this->variant); + } + + /** + * {@inheritdoc} + */ + public function getVariantPluginId() { + return $this->variant; + } + + /** + * {@inheritdoc} + */ + public function setVariantPluginId($variant) { + $this->variant = $variant; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getPage() { + if (!$this->pageEntity) { + if (!$this->page) { + throw new \UnexpectedValueException('The page variant has no associated page'); + } + $this->pageEntity = $this->getPageStorage()->load($this->page); + if (!$this->pageEntity) { + throw new \UnexpectedValueException(sprintf('The page %s could not be loaded', $this->page)); + } + } + + return $this->pageEntity; + } + + /** + * {@inheritdoc} + */ + public function setPageEntity(PageInterface $page) { + $this->pageEntity = $page; + $this->page = $page->id(); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getContexts() { + if (is_null($this->contexts)) { + $static_contexts = $this->getContextMapper()->getContextValues($this->getStaticContexts()); + $page_contexts = $this->getPage()->getContexts(); + $this->contexts = $page_contexts + $static_contexts; + } + return $this->contexts; + } + + /** + * {@inheritdoc} + */ + public function resetCollectedContexts() { + $this->contexts = NULL; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getWeight() { + return $this->weight; + } + + /** + * {@inheritdoc} + */ + public function setWeight($weight) { + $this->weight = $weight; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getSelectionLogic() { + return $this->get('selection_logic'); + } + + /** + * {@inheritdoc} + */ + protected function getSelectionConfiguration() { + return $this->get('selection_criteria'); + } + + /** + * {@inheritdoc} + */ + public function getSelectionConditions() { + if (!$this->selectionConditionCollection) { + $this->selectionConditionCollection = new ConditionPluginCollection($this->getConditionManager(), $this->getSelectionConfiguration()); + } + return $this->selectionConditionCollection; + } + + /** + * {@inheritdoc} + */ + public function addSelectionCondition(array $configuration) { + $configuration['uuid'] = $this->uuidGenerator()->generate(); + $this->getSelectionConditions()->addInstanceId($configuration['uuid'], $configuration); + return $configuration['uuid']; + } + + /** + * {@inheritdoc} + */ + public function getSelectionCondition($condition_id) { + return $this->getSelectionConditions()->get($condition_id); + } + + /** + * {@inheritdoc} + */ + public function removeSelectionCondition($condition_id) { + $this->getSelectionConditions()->removeInstanceId($condition_id); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getStaticContexts() { + return $this->static_context; + } + + /** + * {@inheritdoc} + */ + public function getStaticContext($name) { + if (isset($this->static_context[$name])) { + return $this->static_context[$name]; + } + return []; + } + + /** + * {@inheritdoc} + */ + public function setStaticContext($name, $configuration) { + $this->static_context[$name] = $configuration; + $this->resetCollectedContexts(); + return $this; + } + + /** + * {@inheritdoc} + */ + public function removeStaticContext($name) { + unset($this->static_context[$name]); + $this->resetCollectedContexts(); + return $this; + } + + /** + * {@inheritdoc} + */ + protected function urlRouteParameters($rel) { + $parameters = parent::urlRouteParameters($rel); + $parameters['page'] = $this->get('page'); + return $parameters; + } + + /** + * {@inheritdoc} + */ + public function postSave(EntityStorageInterface $storage, $update = TRUE) { + parent::postSave($storage, $update); + static::routeBuilder()->setRebuildNeeded(); + } + + /** + * {@inheritdoc} + */ + public static function postDelete(EntityStorageInterface $storage, array $entities) { + parent::postDelete($storage, $entities); + static::routeBuilder()->setRebuildNeeded(); + } + + /** + * Wraps the route builder. + * + * @return \Drupal\Core\Routing\RouteBuilderInterface + * An object for state storage. + */ + protected static function routeBuilder() { + return \Drupal::service('router.builder'); + } + + /** + * Wraps the condition plugin manager. + * + * @return \Drupal\Core\Condition\ConditionManager + */ + protected function getConditionManager() { + return \Drupal::service('plugin.manager.condition'); + } + + /** + * Wraps the context mapper. + * + * @return \Drupal\page_manager\ContextMapperInterface + */ + protected function getContextMapper() { + return \Drupal::service('page_manager.context_mapper'); + } + + /** + * Wraps the page entity storage. + * + * @return \Drupal\Core\Entity\EntityStorageInterface + */ + protected function getPageStorage() { + return \Drupal::entityTypeManager()->getStorage('page'); + } + + /** + * {@inheritdoc} + */ + public function __sleep() { + $vars = parent::__sleep(); + + // Gathered contexts objects should not be serialized. + if (($key = array_search('contexts', $vars)) !== FALSE) { + unset($vars[$key]); + } + + return $vars; + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Entity/PageVariantAccess.php b/docroot/modules/contrib/page_manager/src/Entity/PageVariantAccess.php new file mode 100644 index 000000000..3893b6df1 --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Entity/PageVariantAccess.php @@ -0,0 +1,85 @@ +contextHandler = $context_handler; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('context.handler') + ); + } + + /** + * Wraps the context handler. + * + * @return \Drupal\Core\Plugin\Context\ContextHandlerInterface + */ + protected function contextHandler() { + return $this->contextHandler; + } + + /** + * {@inheritdoc} + */ + protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { + /** @var \Drupal\page_manager\PageVariantInterface $entity */ + if ($operation === 'view') { + $contexts = $entity->getContexts(); + $conditions = $entity->getSelectionConditions(); + foreach ($conditions as $condition) { + if ($condition instanceof ContextAwarePluginInterface) { + $this->contextHandler()->applyContextMapping($condition, $contexts); + } + } + return AccessResult::allowedIf($this->resolveConditions($conditions, $entity->getSelectionLogic())); + } + return parent::checkAccess($entity, $operation, $account); + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Entity/PageVariantViewBuilder.php b/docroot/modules/contrib/page_manager/src/Entity/PageVariantViewBuilder.php new file mode 100644 index 000000000..c9b74d0ca --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Entity/PageVariantViewBuilder.php @@ -0,0 +1,83 @@ +getVariantPlugin(); + if ($variant_plugin instanceof ContextAwareVariantInterface) { + $variant_plugin->setContexts($entity->getContexts()); + } + if ($variant_plugin instanceof RefinableCacheableDependencyInterface) { + $variant_plugin->addCacheableDependency($entity); + } + return $variant_plugin->build(); + } + + /** + * {@inheritdoc} + */ + public function viewMultiple(array $entities = [], $view_mode = 'full', $langcode = NULL) { + $build = []; + foreach ($entities as $key => $entity) { + $build[$key] = $this->view($entity, $view_mode, $langcode); + } + return $build; + } + + /** + * {@inheritdoc} + */ + public function resetCache(array $entities = NULL) { + // Intentionally empty. + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + // Intentionally empty. + } + + /** + * {@inheritdoc} + */ + public function buildComponents(array &$build, array $entities, array $displays, $view_mode) { + throw new \LogicException(); + } + + /** + * {@inheritdoc} + */ + public function viewField(FieldItemListInterface $items, $display_options = array()) { + throw new \LogicException(); + } + + /** + * {@inheritdoc} + */ + public function viewFieldItem(FieldItemInterface $item, $display_options = array()) { + throw new \LogicException(); + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Event/PageManagerContextEvent.php b/docroot/modules/contrib/page_manager/src/Event/PageManagerContextEvent.php new file mode 100644 index 000000000..5d443a412 --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Event/PageManagerContextEvent.php @@ -0,0 +1,47 @@ +page = $page; + } + + /** + * Returns the page entity for this event. + * + * @return \Drupal\page_manager\PageInterface + * The page entity. + */ + public function getPage() { + return $this->page; + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Event/PageManagerEvents.php b/docroot/modules/contrib/page_manager/src/Event/PageManagerEvents.php new file mode 100644 index 000000000..aa69fb824 --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Event/PageManagerEvents.php @@ -0,0 +1,23 @@ +account = $account; + $this->userStorage = $entity_type_manager->getStorage('user'); + } + + /** + * Adds in the current user as a context. + * + * @param \Drupal\page_manager\Event\PageManagerContextEvent $event + * The page entity context event. + */ + public function onPageContext(PageManagerContextEvent $event) { + $id = $this->account->id(); + $current_user = $this->userStorage->load($id); + + $context = new Context(new ContextDefinition('entity:user', $this->t('Current user')), $current_user); + $cacheability = new CacheableMetadata(); + $cacheability->setCacheContexts(['user']); + $context->addCacheableDependency($cacheability); + $event->getPage()->addContext('current_user', $context); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[PageManagerEvents::PAGE_CONTEXT][] = 'onPageContext'; + return $events; + } + +} diff --git a/docroot/modules/contrib/page_manager/src/EventSubscriber/LanguageInterfaceContext.php b/docroot/modules/contrib/page_manager/src/EventSubscriber/LanguageInterfaceContext.php new file mode 100644 index 000000000..f2df21caa --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/EventSubscriber/LanguageInterfaceContext.php @@ -0,0 +1,53 @@ +contextRepository = $context_repository; + } + + /** + * Add the language_interface context onPageContext event. + * + * @param \Drupal\page_manager\Event\PageManagerContextEvent $event + * The page entity context event. + */ + public function onPageContext(PageManagerContextEvent $event) { + $contexts = $this->contextRepository->getRuntimeContexts(array('@language.current_language_context:language_interface')); + $context = reset($contexts); + $event->getPage()->addContext('language_interface', $context); + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[PageManagerEvents::PAGE_CONTEXT][] = 'onPageContext'; + + return $events; + } + +} diff --git a/docroot/modules/contrib/page_manager/src/EventSubscriber/RouteNameResponseSubscriber.php b/docroot/modules/contrib/page_manager/src/EventSubscriber/RouteNameResponseSubscriber.php new file mode 100644 index 000000000..bd2a3ca05 --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/EventSubscriber/RouteNameResponseSubscriber.php @@ -0,0 +1,65 @@ +routeMatch = $route_match->getMasterRouteMatch(); + } + + /** + * Adds the route name as a cache tag to all cacheable responses. + * + * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event + * The event to process. + */ + public function onResponse(FilterResponseEvent $event) { + $response = $event->getResponse(); + if ($response instanceof CacheableResponseInterface) { + $cacheability_metadata = $response->getCacheableMetadata(); + // If the route specifies a 'base route name', use that. Otherwise fall + // back to the route name. The 'base route name' is specified in + // \Drupal\page_manager\Routing\PageManagerRoutes. + $route_name = $this->routeMatch->getParameter('base_route_name') ?: $this->routeMatch->getRouteName(); + $cacheability_metadata->addCacheTags(['page_manager_route_name:' . $route_name]); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + // Run before dynamic_page_cache_subscriber:onResponse. + $events[KernelEvents::RESPONSE][] = ['onResponse', 101]; + return $events; + } + +} diff --git a/docroot/modules/contrib/page_manager/src/EventSubscriber/RouteParamContext.php b/docroot/modules/contrib/page_manager/src/EventSubscriber/RouteParamContext.php new file mode 100644 index 000000000..8680878d5 --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/EventSubscriber/RouteParamContext.php @@ -0,0 +1,100 @@ +routeProvider = $route_provider; + $this->requestStack = $request_stack; + } + + /** + * Adds in the current user as a context. + * + * @param \Drupal\page_manager\Event\PageManagerContextEvent $event + * The page entity context event. + */ + public function onPageContext(PageManagerContextEvent $event) { + $request = $this->requestStack->getCurrentRequest(); + $page = $event->getPage(); + $routes = $this->routeProvider->getRoutesByPattern($page->getPath())->all(); + $route = reset($routes); + + if ($route && $route_contexts = $route->getOption('parameters')) { + foreach ($route_contexts as $route_context_name => $route_context) { + // Skip this parameter. + if ($route_context_name == 'page_manager_page_variant' || $route_context_name == 'page_manager_page') { + continue; + } + + $parameter = $page->getParameter($route_context_name); + $context_name = !empty($parameter['label']) ? $parameter['label'] : $this->t('{@name} from route', ['@name' => $route_context_name]); + if ($request->attributes->has($route_context_name)) { + $value = $request->attributes->get($route_context_name); + } + else { + $value = NULL; + } + $cacheability = new CacheableMetadata(); + $cacheability->setCacheContexts(['route']); + + $context = new Context(new ContextDefinition($route_context['type'], $context_name, FALSE), $value); + $context->addCacheableDependency($cacheability); + + $page->addContext($route_context_name, $context); + } + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[PageManagerEvents::PAGE_CONTEXT][] = 'onPageContext'; + return $events; + } + +} diff --git a/docroot/modules/contrib/page_manager/src/PageInterface.php b/docroot/modules/contrib/page_manager/src/PageInterface.php new file mode 100644 index 000000000..ae134b94a --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/PageInterface.php @@ -0,0 +1,218 @@ + $options['404'], + '403' => $options['403'], + '500' => $options['500'], + ] + $options; + + // Add the HTTP status code, so it's easier for people to find it. + array_walk($options, function($title, $code) use (&$options) { + $options[$code] = $this->t('@code (@title)', ['@code' => $code, '@title' => $title]); + }); + + $form['status_code'] = [ + '#title' => $this->t('HTTP status code'), + '#type' => 'select', + '#default_value' => $this->configuration['status_code'], + '#options' => $options, + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + $configuration = parent::defaultConfiguration(); + $configuration['status_code'] = '404'; + return $configuration; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + $this->configuration['status_code'] = $form_state->getValue('status_code'); + } + + /** + * {@inheritdoc} + */ + public function build() { + $status_code = $this->configuration['status_code']; + if ($status_code == 200) { + return []; + } + else { + throw new HttpException($status_code); + } + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Plugin/DisplayVariant/PageBlockDisplayVariant.php b/docroot/modules/contrib/page_manager/src/Plugin/DisplayVariant/PageBlockDisplayVariant.php new file mode 100644 index 000000000..4b704d012 --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Plugin/DisplayVariant/PageBlockDisplayVariant.php @@ -0,0 +1,339 @@ +moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('context.handler'), + $container->get('current_user'), + $container->get('uuid'), + $container->get('token'), + $container->get('plugin.manager.block'), + $container->get('plugin.manager.condition'), + $container->get('module_handler') + ); + } + + /** + * {@inheritdoc} + */ + public function build() { + // Set default page cache keys that include the display. + $build['#cache']['keys'] = [ + 'page_manager_block_display', + $this->id(), + ]; + $build['#pre_render'][] = [$this, 'buildRegions']; + return $build; + } + + /** + * #pre_render callback for building the regions. + */ + public function buildRegions(array $build) { + $cacheability = CacheableMetadata::createFromRenderArray($build) + ->addCacheableDependency($this); + + $contexts = $this->getContexts(); + foreach ($this->getRegionAssignments() as $region => $blocks) { + if (!$blocks) { + continue; + } + + $region_name = Html::getClass("block-region-$region"); + $build[$region]['#prefix'] = '
'; + $build[$region]['#suffix'] = '
'; + + /** @var \Drupal\Core\Block\BlockPluginInterface[] $blocks */ + $weight = 0; + foreach ($blocks as $block_id => $block) { + if ($block instanceof ContextAwarePluginInterface) { + $this->contextHandler()->applyContextMapping($block, $contexts); + } + $access = $block->access($this->account, TRUE); + $cacheability->addCacheableDependency($access); + if (!$access->isAllowed()) { + continue; + } + + $block_build = [ + '#theme' => 'block', + '#attributes' => [], + '#weight' => $weight++, + '#configuration' => $block->getConfiguration(), + '#plugin_id' => $block->getPluginId(), + '#base_plugin_id' => $block->getBaseId(), + '#derivative_plugin_id' => $block->getDerivativeId(), + '#block_plugin' => $block, + '#pre_render' => [[$this, 'buildBlock']], + '#cache' => [ + 'keys' => ['page_manager_block_display', $this->id(), 'block', $block_id], + // Each block needs cache tags of the page and the block plugin, as + // only the page is a config entity that will trigger cache tag + // invalidations in case of block configuration changes. + 'tags' => Cache::mergeTags($this->getCacheTags(), $block->getCacheTags()), + 'contexts' => $block->getCacheContexts(), + 'max-age' => $block->getCacheMaxAge(), + ], + ]; + + // Merge the cacheability metadata of blocks into the page. This helps + // to avoid cache redirects if the blocks have more cache contexts than + // the page, which the page must respect as well. + $cacheability->addCacheableDependency($block); + + // If an alter hook wants to modify the block contents, it can append + // another #pre_render hook. + $this->moduleHandler->alter(['block_view', 'block_view_' . $block->getBaseId()], $block_build, $block); + $build[$region][$block_id] = $block_build; + } + } + + $build['#title'] = $this->renderPageTitle($this->configuration['page_title']); + + $cacheability->applyTo($build); + + return $build; + } + + /** + * #pre_render callback for building a block. + * + * Renders the content using the provided block plugin, if there is no + * content, aborts rendering, and makes sure the block won't be rendered. + */ + public function buildBlock($build) { + $content = $build['#block_plugin']->build(); + // Remove the block plugin from the render array. + unset($build['#block_plugin']); + if ($content !== NULL && !Element::isEmpty($content)) { + $build['content'] = $content; + } + else { + // Abort rendering: render as the empty string and ensure this block is + // render cached, so we can avoid the work of having to repeatedly + // determine whether the block is empty. E.g. modifying or adding entities + // could cause the block to no longer be empty. + $build = [ + '#markup' => '', + '#cache' => $build['#cache'], + ]; + } + // If $content is not empty, then it contains cacheability metadata, and + // we must merge it with the existing cacheability metadata. This allows + // blocks to be empty, yet still bubble cacheability metadata, to indicate + // why they are empty. + if (!empty($content)) { + CacheableMetadata::createFromRenderArray($build) + ->merge(CacheableMetadata::createFromRenderArray($content)) + ->applyTo($build); + } + return $build; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + // Don't call VariantBase::buildConfigurationForm() on purpose, because it + // adds a 'Label' field that we don't actually want to use - we store the + // label on the page variant entity. + //$form = parent::buildConfigurationForm($form, $form_state); + + // Allow to configure the page title, even when adding a new display. + // Default to the page label in that case. + $form['page_title'] = [ + '#type' => 'textfield', + '#title' => $this->t('Page title'), + '#description' => $this->t('Configure the page title that will be used for this display.'), + '#default_value' => $this->configuration['page_title'] ?: '', + ]; + + $form['uuid'] = [ + '#type' => 'value', + '#value' => $this->configuration['uuid'] ?: $this->uuidGenerator->generate(), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + + if ($form_state->hasValue('page_title')) { + $this->configuration['page_title'] = $form_state->getValue('page_title'); + } + if ($form_state->hasValue('uuid')) { + $this->configuration['uuid'] = $form_state->getValue('uuid'); + } + } + + /** + * {@inheritdoc} + */ + public function getWizardOperations($cached_values) { + return [ + 'content' => [ + 'title' => $this->t('Content'), + 'form' => VariantPluginContentForm::class, + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return parent::defaultConfiguration() + [ + 'page_title' => '', + ]; + } + + /** + * Renders the page title and replaces tokens. + * + * @param string $page_title + * The page title that should be rendered. + * + * @return string + * The page title after replacing any tokens. + */ + protected function renderPageTitle($page_title) { + $data = $this->getContextAsTokenData(); + // Token replace only escapes replacement values, ensure a consistent + // behavior by also escaping the input and then returning it as a Markup + // object to avoid double escaping. + // @todo: Simplify this when core provides an API for this in + // https://www.drupal.org/node/2580723. + $title = (string) $this->token->replace(new HtmlEscapedText($page_title), $data); + return Markup::create($title); + } + + /** + * Returns available context as token data. + * + * @return array + * An array with token data values keyed by token type. + */ + protected function getContextAsTokenData() { + $data = []; + foreach ($this->getContexts() as $context) { + // @todo Simplify this when token and typed data types are unified in + // https://drupal.org/node/2163027. + if (strpos($context->getContextDefinition()->getDataType(), 'entity:') === 0) { + $token_type = substr($context->getContextDefinition()->getDataType(), 7); + if ($token_type == 'taxonomy_term') { + $token_type = 'term'; + } + $data[$token_type] = $context->getContextValue(); + } + } + return $data; + } + + /** + * {@inheritdoc} + */ + public function getRegionNames() { + return [ + 'top' => 'Top', + 'bottom' => 'Bottom', + ]; + } + + /** + * {@inheritdoc} + */ + public function __sleep() { + $vars = parent::__sleep(); + + // Gathered contexts objects should not be serialized. + if (($key = array_search('contexts', $vars)) !== FALSE) { + unset($vars[$key]); + } + + return $vars; + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Routing/PageManagerRoutes.php b/docroot/modules/contrib/page_manager/src/Routing/PageManagerRoutes.php new file mode 100644 index 000000000..7def0d89e --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Routing/PageManagerRoutes.php @@ -0,0 +1,166 @@ +entityStorage = $entity_type_manager->getStorage('page'); + $this->cacheTagsInvalidator = $cache_tags_invalidator; + } + + /** + * {@inheritdoc} + */ + protected function alterRoutes(RouteCollection $collection) { + foreach ($this->entityStorage->loadMultiple() as $entity_id => $entity) { + /** @var \Drupal\page_manager\PageInterface $entity */ + + // If the page is disabled skip making a route for it. + if (!$entity->status() || !$entity->getVariants()) { + continue; + } + + $defaults = []; + $parameters = []; + $requirements = []; + + $route_name = "page_manager.page_view_$entity_id"; + if ($overridden_route_name = $this->findOverriddenRouteName($entity, $collection)) { + $base_route_name = $overridden_route_name; + + $collection_route = $collection->get($overridden_route_name); + + // Add the name of the overridden route for use during filtering. + $defaults['overridden_route_name'] = $overridden_route_name; + $path = $collection_route->getPath(); + $parameters = $collection_route->getOption('parameters') ?: []; + $requirements = $collection_route->getRequirements(); + } + else { + $base_route_name = $route_name; + $path = $entity->getPath(); + } + + // Add in configured parameters. + foreach ($entity->getParameters() as $parameter_name => $parameter) { + if (!empty($parameter['type'])) { + $parameters[$parameter_name]['type'] = $parameter['type']; + } + } + + // When adding multiple variants, the variant ID is added to the route + // name. In order to convey the base route name for this set of variants, + // add it as a parameter. + $defaults['base_route_name'] = $base_route_name; + + $defaults['_entity_view'] = 'page_manager_page_variant'; + $defaults['_title'] = $entity->label(); + $defaults['page_manager_page'] = $entity->id(); + $parameters['page_manager_page_variant']['type'] = 'entity:page_variant'; + $parameters['page_manager_page']['type'] = 'entity:page'; + $requirements['_page_access'] = 'page_manager_page.view'; + foreach ($entity->getVariants() as $variant_id => $variant) { + // Construct and add a new route. + $route = new Route( + $path, + $defaults + [ + 'page_manager_page_variant' => $variant_id, + 'page_manager_page_variant_weight' => $variant->getWeight(), + ], + $requirements, + [ + 'parameters' => $parameters, + '_admin_route' => $entity->usesAdminTheme(), + ] + ); + $collection->add($route_name . '_' . $variant_id, $route); + } + + // Invalidate any page with the same base route name. See + // \Drupal\page_manager\EventSubscriber\RouteNameResponseSubscriber. + $this->cacheTagsInvalidator->invalidateTags(["page_manager_route_name:$base_route_name"]); + } + } + + /** + * Finds the overridden route name. + * + * @param \Drupal\page_manager\PageInterface $entity + * The page entity. + * @param \Symfony\Component\Routing\RouteCollection $collection + * The route collection. + * + * @return string|null + * Either the route name if this is overriding an existing path, or NULL. + */ + protected function findOverriddenRouteName(PageInterface $entity, RouteCollection $collection) { + // Get the stored page path. + $path = $entity->getPath(); + + // Loop through all existing routes to see if this is overriding a route. + foreach ($collection->all() as $name => $collection_route) { + // Find all paths which match the path of the current display. + $route_path = $collection_route->getPath(); + $route_path_outline = RouteCompiler::getPatternOutline($route_path); + + // Match either the path or the outline, e.g., '/foo/{foo}' or '/foo/%'. + // The route must be a GET route and must not specify a format. + if (($path === $route_path || $path === $route_path_outline) && + (!$collection_route->getMethods() || in_array('GET', $collection_route->getMethods())) && + !$collection_route->hasRequirement('_format')) { + // Return the overridden route name. + return $name; + } + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + // Run after EntityRouteAlterSubscriber. + $events[RoutingEvents::ALTER][] = ['onAlterRoutes', -160]; + return $events; + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Routing/RouteAttributes.php b/docroot/modules/contrib/page_manager/src/Routing/RouteAttributes.php new file mode 100644 index 000000000..03c065494 --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Routing/RouteAttributes.php @@ -0,0 +1,52 @@ +compile()->getRegex(), $path, $matches); + + // See \Symfony\Component\Routing\Matcher\UrlMatcher::mergeDefaults(). + $attributes = $route->getDefaults(); + foreach ($matches as $key => $value) { + if (!is_int($key)) { + $attributes[$key] = $value; + } + } + + // See \Symfony\Cmf\Component\Routing\NestedMatcher\UrlMatcher::getAttributes(). + $attributes[RouteObjectInterface::ROUTE_OBJECT] = $route; + $attributes[RouteObjectInterface::ROUTE_NAME] = $name; + + return $attributes; + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Routing/RouteEnhancerCollectorTrait.php b/docroot/modules/contrib/page_manager/src/Routing/RouteEnhancerCollectorTrait.php new file mode 100644 index 000000000..0ca6c44d2 --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Routing/RouteEnhancerCollectorTrait.php @@ -0,0 +1,87 @@ +enhancers[$priority])) { + $this->enhancers[$priority] = array(); + } + + $this->enhancers[$priority][] = $enhancer; + $this->sortedEnhancers = array(); + + return $this; + } + + /** + * Sorts the enhancers and flattens them. + * + * @return \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface[] + * The enhancers ordered by priority. + */ + protected function getRouteEnhancers() { + if (empty($this->sortedEnhancers)) { + $this->sortedEnhancers = $this->sortRouteEnhancers(); + } + + return $this->sortedEnhancers; + } + + /** + * Sort enhancers by priority. + * + * The highest priority number is the highest priority (reverse sorting). + * + * @return \Symfony\Cmf\Component\Routing\Enhancer\RouteEnhancerInterface[] + * The sorted enhancers. + */ + protected function sortRouteEnhancers() { + $sortedEnhancers = array(); + krsort($this->enhancers); + + foreach ($this->enhancers as $enhancers) { + $sortedEnhancers = array_merge($sortedEnhancers, $enhancers); + } + + return $sortedEnhancers; + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Routing/VariantRouteFilter.php b/docroot/modules/contrib/page_manager/src/Routing/VariantRouteFilter.php new file mode 100644 index 000000000..f5f9619b9 --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Routing/VariantRouteFilter.php @@ -0,0 +1,249 @@ +pageVariantStorage = $entity_type_manager->getStorage('page_variant'); + } + catch (PluginNotFoundException $e) { + $entity_type_manager->clearCachedDefinitions(); + $this->pageVariantStorage = $entity_type_manager->getStorage('page_variant'); + } + $this->currentPath = $current_path; + $this->requestStack = $request_stack; + } + + /** + * {@inheritdoc} + * + * Ensures only one page manager route remains in the collection. + */ + public function filter(RouteCollection $collection, Request $request) { + $routes = $collection->all(); + // Only continue if at least one route has a page manager variant. + if (!array_filter($routes, function (Route $route) { + return $route->hasDefault('page_manager_page_variant'); + })) { + return $collection; + } + + // Sort routes by variant weight. + $routes = $this->sortRoutes($routes); + + $variant_route_name = $this->getVariantRouteName($routes, $request); + foreach ($routes as $name => $route) { + if (!$route->hasDefault('page_manager_page_variant')) { + continue; + } + + // If this page manager route isn't the one selected, remove it. + if ($variant_route_name !== $name) { + unset($routes[$name]); + } + // If the selected route is overriding another route, remove the + // overridden route. + elseif ($overridden_route_name = $route->getDefault('overridden_route_name')) { + unset($routes[$overridden_route_name]); + } + } + + // Create a new route collection by iterating over the sorted routes, using + // the overridden_route_name if available. + $result_collection = new RouteCollection(); + foreach ($routes as $name => $route) { + $overridden_route_name = $route->getDefault('overridden_route_name') ?: $name; + $result_collection->add($overridden_route_name, $route); + } + return $result_collection; + } + + /** + * Gets the route name of the first valid variant. + * + * @param \Symfony\Component\Routing\Route[] $routes + * An array of sorted routes. + * @param \Symfony\Component\HttpFoundation\Request $request + * A current request. + * + * @return string|null + * A route name, or NULL if none are found. + */ + protected function getVariantRouteName(array $routes, Request $request) { + // Store the unaltered request attributes. + $original_attributes = $request->attributes->all(); + foreach ($routes as $name => $route) { + if (!$page_variant_id = $route->getDefault('page_manager_page_variant')) { + continue; + } + + if ($attributes = $this->getRequestAttributes($route, $name, $request)) { + // Use the overridden route name if available. + $attributes[RouteObjectInterface::ROUTE_NAME] = $route->getDefault('overridden_route_name') ?: $name; + // Add the enhanced attributes to the request. + $request->attributes->add($attributes); + $this->requestStack->push($request); + + if ($this->checkPageVariantAccess($page_variant_id)) { + $this->requestStack->pop(); + return $name; + } + + // Restore the original request attributes, this must be done in the loop + // or the request attributes will not be calculated correctly for the + // next route. + $request->attributes->replace($original_attributes); + $this->requestStack->pop(); + } + } + } + + /** + * Sorts routes based on the variant weight. + * + * @param \Symfony\Component\Routing\Route[] $unsorted_routes + * An array of unsorted routes. + * + * @return \Symfony\Component\Routing\Route[] + * An array of sorted routes. + */ + protected function sortRoutes(array $unsorted_routes) { + // Create a mapping of route names to their weights. + $weights_by_key = array_map(function (Route $route) { + return $route->getDefault('page_manager_page_variant_weight') ?: 0; + }, $unsorted_routes); + + // Create an array holding the route names to be sorted. + $keys = array_keys($unsorted_routes); + + // Sort $keys first by the weights and then by the original order. + array_multisort($weights_by_key, array_keys($keys), $keys); + + // Return the routes using the sorted order of $keys. + return array_replace(array_combine($keys, $keys), $unsorted_routes); + } + + /** + * Checks access of a page variant. + * + * @param string $page_variant_id + * The page variant ID. + * + * @return bool + * TRUE if the route is valid, FALSE otherwise. + */ + protected function checkPageVariantAccess($page_variant_id) { + /** @var \Drupal\page_manager\PageVariantInterface $variant */ + $variant = $this->pageVariantStorage->load($page_variant_id); + + try { + $access = $variant && $variant->access('view'); + } + // Since access checks can throw a context exception, consider that as + // a disallowed variant. + catch (ContextException $e) { + $access = FALSE; + } + + return $access; + } + + /** + * Prepares the request attributes for use by the selection process. + * + * This is be done because route filters run before request attributes are + * populated. + * + * @param \Symfony\Component\Routing\Route $route + * The route. + * @param string $name + * The route name. + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request. + * + * @return array|false + * An array of request attributes or FALSE if any route enhancers fail. + */ + protected function getRequestAttributes(Route $route, $name, Request $request) { + // Extract the raw attributes from the current path. This performs the same + // functionality as \Drupal\Core\Routing\UrlMatcher::finalMatch(). + $path = $this->currentPath->getPath($request); + $raw_attributes = RouteAttributes::extractRawAttributes($route, $name, $path); + $attributes = $request->attributes->all(); + $attributes = NestedArray::mergeDeep($attributes, $raw_attributes); + + // Run the route enhancers on the raw attributes. This performs the same + // functionality as \Symfony\Cmf\Component\Routing\DynamicRouter::match(). + foreach ($this->getRouteEnhancers() as $enhancer) { + try { + $attributes = $enhancer->enhance($attributes, $request); + } + catch (\Exception $e) { + return FALSE; + } + } + + return $attributes; + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Tests/FrontPageTest.php b/docroot/modules/contrib/page_manager/src/Tests/FrontPageTest.php new file mode 100644 index 000000000..448dd08c3 --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Tests/FrontPageTest.php @@ -0,0 +1,76 @@ + 'My frontpage', + 'id' => 'myfront', + 'path' => '/myfront', + ]); + $page->save(); + /** @var \Drupal\page_manager\PageVariantInterface $page_variant */ + $page_variant = PageVariant::create([ + 'variant' => 'block_display', + 'id' => 'block_page', + 'label' => 'Block page', + 'page' => 'myfront', + ]); + $page_variant->save(); + + $this->config('system.site')->set('page.front', '/myfront')->save(); + + $block = Block::create([ + 'id' => $this->randomMachineName(), + 'plugin' => 'system_powered_by_block', + ]); + $block->save(); + $page_variant->getVariantPlugin()->setConfiguration([ + 'page_title' => '', + 'blocks' => [ + $block->uuid() => [ + 'region' => 'top', + 'weight' => 0, + 'id' => $block->id(), + 'uuid' => $block->uuid(), + 'context_mapping' => [], + ], + ], + ]); + + $this->verbose(var_export($page_variant->toArray(), TRUE)); + + $this->triggerRouterRebuild(); + + // The title should default to "Home" on the front page. + // @todo This gives 404 :( + $this->drupalGet(''); + $this->assertTitle('Home | Drupal'); + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Tests/PageConfigSchemaTest.php b/docroot/modules/contrib/page_manager/src/Tests/PageConfigSchemaTest.php new file mode 100644 index 000000000..9c4bafb8e --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Tests/PageConfigSchemaTest.php @@ -0,0 +1,107 @@ +installConfig(['page_manager']); + } + + /** + * Tests whether the page entity config schema is valid. + */ + public function testValidPageConfigSchema() { + $id = 'node_view'; + $label = 'Node view'; + $description = 'When enabled, this overrides the default Drupal behavior for displaying nodes at /node/{node}. If you add variants, you may use selection criteria such as node type or language or user access to provide different views of nodes. If no variant is selected, the default Drupal node view will be used. This page only affects nodes viewed as pages, it will not affect nodes viewed in lists or at other locations.'; + + /** @var \Drupal\page_manager\PageInterface $page */ + $page = Page::load($id); + + // Add an access condition. + $page->addAccessCondition([ + 'id' => 'node_type', + 'bundles' => [ + 'article' => 'article', + ], + 'negate' => TRUE, + 'context_mapping' => [ + 'node' => 'node', + ], + ]); + $page->save(); + + $page_variant_id = 'block_page'; + // Add a block variant. + $page_variant = PageVariant::create([ + 'variant' => 'block_display', + 'id' => $page_variant_id, + 'label' => 'Block page', + 'page' => $page->id(), + ]); + $page_variant->save(); + $page->addVariant($page_variant); + /** @var \Drupal\page_manager\Plugin\DisplayVariant\PageBlockDisplayVariant $variant_plugin */ + $variant_plugin = $page_variant->getVariantPlugin(); + + // Add a selection condition. + $page_variant->addSelectionCondition([ + 'id' => 'node_type', + 'bundles' => [ + 'page' => 'page', + ], + 'context_mapping' => [ + 'node' => 'node', + ], + ]); + + // Add a block. + $variant_plugin->addBlock([ + 'id' => 'entity_view:node', + 'label' => 'View the node', + 'provider' => 'page_manager', + 'label_display' => 'visible', + 'view_mode' => 'default', + ]); + $page_variant->save(); + + $page_config = \Drupal::config("page_manager.page.$id"); + $this->assertSame($page_config->get('id'), $id); + $this->assertSame($page_config->get('label'), $label); + $this->assertSame($page_config->get('description'), $description); + + $variant_config = \Drupal::config("page_manager.page_variant.$page_variant_id"); + $this->assertSame($variant_config->get('id'), $page_variant_id); + + $this->assertConfigSchema(\Drupal::service('config.typed'), $page_config->getName(), $page_config->get()); + $this->assertConfigSchema(\Drupal::service('config.typed'), $variant_config->getName(), $variant_config->get()); + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Tests/PageManagerConfigTranslationTest.php b/docroot/modules/contrib/page_manager/src/Tests/PageManagerConfigTranslationTest.php new file mode 100644 index 000000000..0627d389e --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Tests/PageManagerConfigTranslationTest.php @@ -0,0 +1,68 @@ +save(); + + $this->drupalLogin($this->drupalCreateUser(['administer site configuration', 'translate configuration'])); + + PageVariant::create([ + 'variant' => 'http_status_code', + 'label' => 'HTTP status code', + 'id' => 'http_status_code', + 'page' => 'node_view', + ])->save(); + } + + /** + * Tests config translation. + */ + public function testTranslation() { + $this->drupalGet('admin/config/regional/config-translation'); + $this->assertLinkByHref('admin/config/regional/config-translation/page'); + $this->assertLinkByHref('admin/config/regional/config-translation/page_variant'); + + $this->drupalGet('admin/config/regional/config-translation/page'); + $this->assertText('Node view'); + $this->clickLink('Translate'); + $this->clickLink('Add'); + $this->assertField('translation[config_names][page_manager.page.node_view][label]'); + + $this->drupalGet('admin/config/regional/config-translation/page_variant'); + $this->assertText('HTTP status code'); + $this->clickLink('Translate'); + $this->clickLink('Add'); + $this->assertField('translation[config_names][page_manager.page_variant.http_status_code][label]'); + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Tests/PageManagerTranslationIntegrationTest.php b/docroot/modules/contrib/page_manager/src/Tests/PageManagerTranslationIntegrationTest.php new file mode 100644 index 000000000..7ba06dcdf --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Tests/PageManagerTranslationIntegrationTest.php @@ -0,0 +1,83 @@ +drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + } + + /** + * {@inheritdoc} + */ + protected function getTranslatorPermissions() { + return array_merge(parent::getTranslatorPermissions(), ['administer pages', 'administer pages']); + } + + /** + * Tests that overriding the node page does not prevent translation. + */ + public function testNode() { + $this->drupalPlaceBlock('local_tasks_block'); + $this->drupalPlaceBlock('page_title_block'); + + $node = $this->drupalCreateNode(['type' => 'article']); + $this->drupalGet('node/' . $node->id()); + $this->assertResponse(200); + $this->assertText($node->label()); + $this->clickLink('Translate'); + $this->assertResponse(200); + + // Create a new variant. + $http_status_variant = PageVariant::create([ + 'variant' => 'http_status_code', + 'label' => 'HTTP status code', + 'id' => 'http_status_code', + 'page' => 'node_view', + ]); + $http_status_variant->getVariantPlugin()->setConfiguration(['status_code' => 200]); + $http_status_variant->save(); + $this->triggerRouterRebuild(); + + $this->drupalGet('node/' . $node->id()); + $this->assertResponse(200); + $this->clickLink('Translate'); + $this->assertResponse(200); + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Tests/PageNodeAccessTest.php b/docroot/modules/contrib/page_manager/src/Tests/PageNodeAccessTest.php new file mode 100644 index 000000000..b5dae3e47 --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Tests/PageNodeAccessTest.php @@ -0,0 +1,120 @@ +revokePermission('access content')->save(); + Role::load(RoleInterface::AUTHENTICATED_ID)->revokePermission('access content')->save(); + + $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + $this->drupalCreateContentType(['type' => 'page', 'name' => 'Page']); + $this->drupalPlaceBlock('page_title_block'); + $this->page = Page::load('node_view'); + } + + /** + * Tests that a user role condition controls the node view page. + */ + public function testUserRoleAccessCondition() { + $node1 = $this->drupalCreateNode(['type' => 'page']); + $node2 = $this->drupalCreateNode(['type' => 'article']); + + $this->drupalLogin($this->drupalCreateUser(['access content'])); + $this->drupalGet('node/' . $node1->id()); + $this->assertResponse(200); + $this->assertText($node1->label()); + $this->assertTitle($node1->label() . ' | Drupal'); + + // Add a variant and an access condition. + /** @var \Drupal\page_manager\Entity\PageVariant $page_variant */ + $page_variant = PageVariant::create([ + 'variant' => 'block_display', + 'id' => 'block_page', + 'label' => 'Block page', + 'page' => $this->page->id(), + ]); + $page_variant->getVariantPlugin()->setConfiguration(['page_title' => 'The overridden page']); + $page_variant->save(); + + $this->page->addAccessCondition([ + 'id' => 'user_role', + 'roles' => [ + RoleInterface::AUTHENTICATED_ID => RoleInterface::AUTHENTICATED_ID, + ], + 'context_mapping' => [ + 'user' => 'current_user', + ], + ]); + $this->page->addAccessCondition([ + 'id' => 'node_type', + 'bundles' => [ + 'page' => 'page', + ], + 'context_mapping' => [ + 'node' => 'node', + ], + ]); + $this->page->save(); + $this->triggerRouterRebuild(); + + $this->drupalLogout(); + $this->drupalGet('node/' . $node1->id()); + $this->assertResponse(403); + $this->assertNoText($node1->label()); + $this->assertTitle('Access denied | Drupal'); + + $this->drupalLogin($this->drupalCreateUser()); + $this->drupalGet('node/' . $node1->id()); + $this->assertResponse(403); + $this->assertNoText($node1->label()); + $this->assertTitle('Access denied | Drupal'); + + $this->drupalLogin($this->drupalCreateUser(['access content'])); + $this->drupalGet('node/' . $node1->id()); + $this->assertResponse(200); + $this->assertNoText($node1->label()); + $this->assertTitle('The overridden page | Drupal'); + + $this->drupalGet('node/' . $node2->id()); + $this->assertResponse(403); + $this->assertNoText($node2->label()); + $this->assertTitle('Access denied | Drupal'); + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Tests/PageNodeSelectionTest.php b/docroot/modules/contrib/page_manager/src/Tests/PageNodeSelectionTest.php new file mode 100644 index 000000000..a7979338b --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Tests/PageNodeSelectionTest.php @@ -0,0 +1,148 @@ +drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + $this->drupalCreateContentType(['type' => 'page', 'name' => 'Page']); + $this->drupalLogin($this->drupalCreateUser(['administer pages', 'create article content', 'create page content'])); + + $this->drupalPlaceBlock('page_title_block'); + } + + /** + * Tests that a node bundle condition controls the node view page. + */ + public function testAdmin() { + // Create two nodes, and view their pages. + $node1 = $this->drupalCreateNode(['type' => 'page']); + $node2 = $this->drupalCreateNode(['title' => 'First & ', 'type' => 'article']); + $node3 = $this->drupalCreateNode(['type' => 'article']); + $this->drupalGet('node/' . $node1->id()); + $this->assertResponse(200); + $this->assertText($node1->label()); + $this->assertTitle($node1->label() . ' | Drupal'); + $this->drupalGet('node/' . $node2->id()); + $this->assertResponse(200); + $this->assertCacheTag('page_manager_route_name:entity.node.canonical'); + $expected_title = '<em>First</em> & <Second>'; + $this->assertRaw($expected_title); + $this->assertTitle($expected_title . ' | Drupal'); + + // Create a new variant to always return 404, the node_view page exists by + // default. + $http_status_variant = PageVariant::create([ + 'variant' => 'http_status_code', + 'label' => 'HTTP status code', + 'id' => 'http_status_code', + 'page' => 'node_view', + ]); + $http_status_variant->getVariantPlugin()->setConfiguration(['status_code' => 404]); + $http_status_variant->save(); + $this->triggerRouterRebuild(); + + $this->drupalGet('node/' . $node1->id()); + $this->assertResponse(404); + $this->assertCacheTag('page_manager_route_name:entity.node.canonical'); + $this->assertNoText($node1->label()); + $this->drupalGet('node/' . $node2->id()); + $this->assertResponse(404); + $this->assertNoText($node2->label()); + + // Add a new variant. + /** @var \Drupal\page_manager\PageVariantInterface $block_page_variant */ + $block_page_variant = PageVariant::create([ + 'variant' => 'block_display', + 'id' => 'block_page_first', + 'label' => 'First', + 'page' => 'node_view', + ]); + $block_page_plugin = $block_page_variant->getVariantPlugin(); + $this->assertTrue(!empty($block_page_plugin->getConfiguration()['uuid'])); + $uuid = $block_page_plugin->getConfiguration()['uuid']; + $block_page_plugin->setConfiguration(['page_title' => '[node:title]']); + $second_uuid = $block_page_plugin->getConfiguration()['uuid']; + $this->assertEqual($uuid, $second_uuid); + /** @var \Drupal\page_manager\Plugin\DisplayVariant\PageBlockDisplayVariant $block_page_plugin */ + $block_page_plugin->addBlock([ + 'id' => 'entity_view:node', + 'label' => 'Entity view (Content)', + 'label_display' => FALSE, + 'view_mode' => 'default', + 'region' => 'top', + 'context_mapping' => [ + 'entity' => 'node', + ], + ]); + $block_page_variant->addSelectionCondition([ + 'id' => 'node_type', + 'bundles' => [ + 'article' => 'article', + ], + 'context_mapping' => [ + 'node' => 'node', + ], + ]); + $block_page_variant->setWeight(-10); + $block_page_variant->save(); + $this->triggerRouterRebuild(); + + // The page node will 404, but the article node will display the variant. + $this->drupalGet('node/' . $node1->id()); + $this->assertResponse(404); + $this->assertNoText($node1->label()); + + $this->drupalGet('node/' . $node2->id()); + $this->assertResponse(200); + $this->assertTitle($expected_title . ' | Drupal'); + $this->assertText($node2->body->value); + $this->assertRaw('

' . $expected_title . '

'); + + // Test cacheability metadata. + $this->drupalGet('node/' . $node3->id()); + $this->assertTitle($node3->label() . ' | Drupal'); + $this->assertText($node3->body->value); + $this->assertNoText($node2->label()); + + // Ensure that setting the same title directly in the block display results + // in the same output. + $block_page_plugin->setConfiguration(['page_title' => 'First & ']); + $block_page_variant->save(); + $this->drupalGet('node/' . $node2->id()); + $this->assertResponse(200); + $this->assertTitle($expected_title . ' | Drupal'); + $this->assertRaw('

' . $expected_title . '

'); + + // Ensure this doesn't affect the /node/add page. + $this->drupalGet('node/add'); + $this->assertResponse(200); + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Tests/PagePlaceholderTest.php b/docroot/modules/contrib/page_manager/src/Tests/PagePlaceholderTest.php new file mode 100644 index 000000000..a2ef4799b --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Tests/PagePlaceholderTest.php @@ -0,0 +1,77 @@ +drupalLogin($this->drupalCreateUser(['administer pages'])); + } + + /** + * Tests that a node bundle condition controls the node view page. + */ + public function testPagePlaceHolder() { + // Access the page callback and check whether string is printed. + $page_string = 'test-page'; + $this->drupalGet('page-manager-test/' . $page_string); + $this->assertResponse(200); + $this->assertCacheTag('page_manager_route_name:page_manager_test.page_view'); + $this->assertText('Hello World! Page ' . $page_string); + + // Create a new page entity with the same path as in the test module. + $page = Page::create([ + 'label' => 'Placeholder test', + 'id' => 'placeholder', + 'path' => '/page-manager-test/%', + ]); + $page->save(); + + // Create a new variant. + /* @var $http_status_variant \Drupal\page_manager\Entity\PageVariant */ + $http_status_variant = PageVariant::create([ + 'label' => 'HTTP status code', + 'id' => 'http_status_code', + 'page' => 'placeholder', + ]); + + // Test setting variant post create works. + $http_status_variant->setVariantPluginId('http_status_code'); + + $http_status_variant->getVariantPlugin()->setConfiguration(['status_code' => 200]); + $http_status_variant->save(); + $this->triggerRouterRebuild(); + + // Access the page callback again and check that now the text is not there. + $this->drupalGet('page-manager-test/' . $page_string); + $this->assertResponse(200); + $this->assertCacheTag('page_manager_route_name:page_manager_test.page_view'); + $this->assertNoText('Hello World! Page ' . $page_string); + } + +} diff --git a/docroot/modules/contrib/page_manager/src/Tests/PageTestHelperTrait.php b/docroot/modules/contrib/page_manager/src/Tests/PageTestHelperTrait.php new file mode 100644 index 000000000..aa5b33985 --- /dev/null +++ b/docroot/modules/contrib/page_manager/src/Tests/PageTestHelperTrait.php @@ -0,0 +1,26 @@ +container->get('router.builder')->rebuildIfNeeded(); + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/modules/page_manager_routing_test/page_manager_routing_test.info.yml b/docroot/modules/contrib/page_manager/tests/modules/page_manager_routing_test/page_manager_routing_test.info.yml new file mode 100644 index 000000000..53b2b80ea --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/modules/page_manager_routing_test/page_manager_routing_test.info.yml @@ -0,0 +1,10 @@ +type: module +name: Page Manager Routing Test +description: 'Required for Page Manager tests.' +# core: 8.x + +# Information added by Drupal.org packaging script on 2017-04-28 +version: '8.x-4.0-beta2' +core: '8.x' +project: 'page_manager' +datestamp: 1493410446 diff --git a/docroot/modules/contrib/page_manager/tests/modules/page_manager_routing_test/page_manager_routing_test.services.yml b/docroot/modules/contrib/page_manager/tests/modules/page_manager_routing_test/page_manager_routing_test.services.yml new file mode 100644 index 000000000..e09db3bd2 --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/modules/page_manager_routing_test/page_manager_routing_test.services.yml @@ -0,0 +1,6 @@ +services: + page_manager_routing_test.subscriber: + class: Drupal\page_manager_routing_test\Routing\RouteSubscriber + arguments: ['@entity.manager'] + tags: + - { name: event_subscriber } diff --git a/docroot/modules/contrib/page_manager/tests/modules/page_manager_routing_test/src/Plugin/Condition/EntityTestCondition.php b/docroot/modules/contrib/page_manager/tests/modules/page_manager_routing_test/src/Plugin/Condition/EntityTestCondition.php new file mode 100644 index 000000000..eb3770151 --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/modules/page_manager_routing_test/src/Plugin/Condition/EntityTestCondition.php @@ -0,0 +1,34 @@ +getContext('entity_test'); + } + + /** + * {@inheritdoc} + */ + public function summary() { + return ''; + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/modules/page_manager_routing_test/src/Routing/RouteSubscriber.php b/docroot/modules/contrib/page_manager/tests/modules/page_manager_routing_test/src/Routing/RouteSubscriber.php new file mode 100644 index 000000000..41ff7bc45 --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/modules/page_manager_routing_test/src/Routing/RouteSubscriber.php @@ -0,0 +1,54 @@ +getRouteCollection(); + $route = new Route('/entity_test/{entity_test}', [], ['_access' => 'TRUE']); + $route->setRequirement('_format', 'xml'); + $collection->add('entity.entity_test.canonical.xml', $route); + } + + /** + * Alters the existing route collection. + * + * @param \Drupal\Core\Routing\RouteBuildEvent $event + * The route build event. + */ + public function afterPageManagerRoutes(RouteBuildEvent $event) { + $collection = $event->getRouteCollection(); + if ($original_route = $collection->get('entity.entity_test.canonical')) { + $route = new Route($original_route->getPath(), $original_route->getDefaults(), $original_route->getRequirements(), $original_route->getOptions()); + $route->setRequirement('_format', 'json'); + $collection->add('entity.entity_test.canonical.json', $route); + } + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + // Run before PageManagerRoutes. + $events[RoutingEvents::ALTER][] = ['beforePageManagerRoutes', -155]; + // Run after PageManagerRoutes. + $events[RoutingEvents::ALTER][] = ['afterPageManagerRoutes', -165]; + return $events; + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/modules/page_manager_test/page_manager_test.info.yml b/docroot/modules/contrib/page_manager/tests/modules/page_manager_test/page_manager_test.info.yml new file mode 100644 index 000000000..bdd1a6a7c --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/modules/page_manager_test/page_manager_test.info.yml @@ -0,0 +1,10 @@ +type: module +name: Page Manager Test +description: 'Required for page manager simpletests only.' +# core: 8.x + +# Information added by Drupal.org packaging script on 2017-04-28 +version: '8.x-4.0-beta2' +core: '8.x' +project: 'page_manager' +datestamp: 1493410446 diff --git a/docroot/modules/contrib/page_manager/tests/modules/page_manager_test/page_manager_test.module b/docroot/modules/contrib/page_manager/tests/modules/page_manager_test/page_manager_test.module new file mode 100644 index 000000000..bcca4537c --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/modules/page_manager_test/page_manager_test.module @@ -0,0 +1,18 @@ + "Hello World! Page $page"]; + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/modules/page_manager_test/src/Plugin/Block/TestBlock.php b/docroot/modules/contrib/page_manager/tests/modules/page_manager_test/src/Plugin/Block/TestBlock.php new file mode 100644 index 000000000..a4ccf6d09 --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/modules/page_manager_test/src/Plugin/Block/TestBlock.php @@ -0,0 +1,51 @@ +t('Example output'); + return $build; + } + + /** + * {@inheritdoc} + */ + public function blockForm($form, FormStateInterface $form_state) { + $form['example'] = [ + '#type' => 'textfield', + '#title' => $this->t('Example'), + '#ajax' => [ + 'callback' => [$this, 'exampleAjaxCallback'], + ] + ]; + return $form; + } + + /** + * Example ajax callback. + */ + public function exampleAjaxCallback($form, FormStateInterface $form_state) { + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/src/Kernel/PageManagerRoutingTest.php b/docroot/modules/contrib/page_manager/tests/src/Kernel/PageManagerRoutingTest.php new file mode 100644 index 000000000..6c07bed12 --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/src/Kernel/PageManagerRoutingTest.php @@ -0,0 +1,154 @@ +container->get('current_user')->setAccount($this->createUser([], ['view test entity'])); + EntityTest::create()->save(); + + Page::create([ + 'id' => 'entity_test_view', + 'path' => '/entity_test/{entity_test}', + ])->save(); + PageVariant::create([ + 'id' => 'entity_test_view_variant', + 'variant' => 'simple_page', + 'page' => 'entity_test_view', + ])->save(); + + Page::create([ + 'id' => 'custom_entity_test_view', + 'path' => '/custom/entity_test/{entity_test}', + 'parameters' => [ + 'entity_test' => [ + 'type' => 'entity:entity_test', + ], + ], + ])->save(); + $variant = PageVariant::create([ + 'id' => 'custom_entity_test_view_variant', + 'variant' => 'simple_page', + 'page' => 'custom_entity_test_view', + ]); + $variant->addSelectionCondition([ + 'id' => 'page_manager_routing_test__entity_test', + ]); + $variant->getPluginCollections(); + $variant->save(); + + Page::create([ + 'id' => 'entity_test_edit', + 'path' => '/entity_test/manage/{entity_test}/edit', + ])->save(); + PageVariant::create([ + 'id' => 'entity_test_edit_variant', + 'variant' => 'simple_page', + 'page' => 'entity_test_edit', + // Add a selection condition that will never pass. + 'selection_criteria' => [ + 'request_path' => [ + 'id' => 'request_path', + 'pages' => 'invalid', + ], + ], + ])->save(); + + Page::create([ + 'id' => 'entity_test_delete', + 'path' => '/entity_test/delete/entity_test/{entity_test}', + // Add an access condition that will never pass. + 'access_conditions' => [ + 'request_path' => [ + 'id' => 'request_path', + 'pages' => 'invalid', + ], + ], + ])->save(); + PageVariant::create([ + 'id' => 'entity_test_delete_variant', + 'variant' => 'simple_page', + 'page' => 'entity_test_delete', + ])->save(); + } + + /** + * @covers \Drupal\page_manager\Routing\VariantRouteFilter + * + * @dataProvider providerTestRouteFilter + */ + public function testRouteFilter($path, $expected) { + $request = Request::create($path); + try { + $parameters = $this->container->get('router')->matchRequest($request); + } + catch (\Exception $e) { + $parameters = []; + } + + if ($expected) { + $this->assertArrayHasKey(RouteObjectInterface::ROUTE_NAME, $parameters); + $this->assertSame($expected, $parameters[RouteObjectInterface::ROUTE_NAME]); + } + else { + $this->assertEmpty($parameters); + } + } + + public function providerTestRouteFilter() { + $data = []; + $data['custom'] = [ + '/custom/entity_test/1', + 'page_manager.page_view_custom_entity_test_view_custom_entity_test_view_variant', + ]; + $data['no_format'] = [ + '/entity_test/1', + 'entity.entity_test.canonical', + ]; + $data['format_added_after'] = [ + '/entity_test/1?_format=json', + 'entity.entity_test.canonical.json', + ]; + $data['format_added_before'] = [ + '/entity_test/1?_format=xml', + 'entity.entity_test.canonical.xml', + ]; + $data['same_pattern_no_match'] = [ + '/entity_test/add', + 'entity.entity_test.add_form', + ]; + $data['failed_selection'] = [ + '/entity_test/manage/1/edit', + 'entity.entity_test.edit_form', + ]; + $data['access_denied'] = [ + '/entity_test/delete/entity_test/1', + NULL, + ]; + return $data; + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/src/Kernel/PageVariantPageEntityTest.php b/docroot/modules/contrib/page_manager/tests/src/Kernel/PageVariantPageEntityTest.php new file mode 100644 index 000000000..874cb7c2a --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/src/Kernel/PageVariantPageEntityTest.php @@ -0,0 +1,86 @@ + 'test_page']); + $page->save(); + + /* @var \Drupal\page_manager\PageVariantInterface $page_variant */ + $page_variant = PageVariant::create([ + 'id' => 'test_page_variant', + 'page' => 'test_page', + ]); + + // Get the page from the variant. + $page_first = $page_variant->getPage(); + $this->assertNotEmpty($page_first); + $page_second = $page_variant->getPage(); + $this->assertEquals(spl_object_hash($page_first), spl_object_hash($page_second)); + } + + /** + * Tests that a an unsaved page can be set against a page variant. + */ + public function testUnsavedPage() { + /* @var \Drupal\page_manager\PageInterface $page */ + $page = Page::create(['id' => 'test_page']); + + /* @var \Drupal\page_manager\PageVariantInterface $page_variant */ + $page_variant = PageVariant::create([ + 'id' => 'test_page_variant', + 'page' => 'test_page', + ]); + $page_variant->setPageEntity($page); + + // Get the page from the variant. + $page_result = $page_variant->getPage(); + $this->assertEquals($page, $page_result); + } + + /** + * Tests that a page gets cached on the page variant. + */ + public function testChangePageId() { + /* @var \Drupal\page_manager\PageVariantInterface $page_variant */ + $page_variant = PageVariant::create(['id' => 'test_page_variant']); + + // Check the page gets set correctly. + /* @var \Drupal\page_manager\PageInterface $page */ + $page1 = Page::create(['id' => 'test_page_1']); + $page_variant->setPageEntity($page1); + $this->assertEquals('test_page_1', $page_variant->get('page')); + + // Check the page gets changed correctly. + /* @var \Drupal\page_manager\PageInterface $page */ + $page2 = Page::create(['id' => 'test_page_2']); + $page_variant->setPageEntity($page2); + $this->assertEquals('test_page_2', $page_variant->get('page')); + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/src/Kernel/SerializationTest.php b/docroot/modules/contrib/page_manager/tests/src/Kernel/SerializationTest.php new file mode 100644 index 000000000..38bf2bf54 --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/src/Kernel/SerializationTest.php @@ -0,0 +1,251 @@ +installEntitySchema('user'); + } + + /** + * Assert that an object successfully serializes and unserializes. + * + * @param object $object + * The object to serialize. + * @param string $message + * + * @return object + * The unserialized object. + */ + protected function assertSerialization($object, $message = '') { + $unserialized = unserialize(serialize($object)); + $this->assertInstanceOf(get_class($object), $unserialized, $message); + return $unserialized; + } + + /** + * Create a basic page. + * + * @return \Drupal\page_manager\Entity\Page + */ + protected function createPage() { + return Page::create([ + 'id' => $this->randomMachineName(), + 'label' => $this->randomString(), + 'description' => $this->randomString(), + 'path' => 'admin/foo', + 'use_admin_theme' => TRUE, + ]); + } + + /** + * Create a basic page variant. + * + * @return \Drupal\page_manager\Entity\PageVariant + */ + protected function createPageVariant() { + return PageVariant::create([ + 'id' => $this->randomMachineName(), + 'label' => $this->randomString(), + 'weight' => 0, + 'variant' => 'block_display', + ]); + } + + /** + * Test serialization of a page. + * + * @covers \Drupal\page_manager\Entity\Page::__sleep + */ + public function testPage() { + $page = $this->createPage(); + + // Test that a very simple page successfully serializes. + /* @var \Drupal\page_manager\Entity\Page $unserialized */ + $unserialized = $this->assertSerialization($page); + $this->assertEquals($page->id(), $unserialized->id()); + $this->assertEquals($page->label(), $unserialized->label()); + $this->assertEquals($page->getDescription(), $unserialized->getDescription()); + $this->assertEquals($page->getPath(), $unserialized->getPath()); + $this->assertEquals($page->usesAdminTheme(), $unserialized->usesAdminTheme()); + + // Test adding parameters. + $page->set('path', 'admin/foo/{id}'); + $page->setParameter('id', 'integer', 'ID'); + $unserialized = $this->assertSerialization($page); + $this->assertEquals($page->getPath(), $unserialized->getPath()); + $this->assertEquals($page->getParameters(), $unserialized->getParameters()); + + // Test adding access conditions. + $condition = [ + 'id' => 'request_path', + 'pages' => '/admin/foo/*', + 'negate' => FALSE, + 'context_mapping' => [], + ]; + $page->addAccessCondition($condition); + $unserialized = $this->assertSerialization($page); + $this->assertNull($unserialized->get('accessConditionCollection')); + $this->assertEquals($page->getAccessConditions()->getConfiguration(), $unserialized->getAccessConditions()->getConfiguration()); + + // Test adding context. + $context = new Context(new ContextDefinition('integer', 'ID'), 1); + $page->addContext('id', $context); + $unserialized = $this->assertSerialization($page); + $this->assertEquals([], $unserialized->get('contexts')); + + // Test adding a very basic variant. + $page_variant = $this->createPageVariant(); + $page->addVariant($page_variant); + $unserialized = $this->assertSerialization($page); + $this->assertInstanceOf(PageVariant::class, $unserialized->getVariant($page_variant->id())); + $this->assertEquals($page_variant->id(), $unserialized->getVariant($page_variant->id())->id()); + } + + /** + * Test serialization of a variant. + * + * @covers \Drupal\page_manager\Entity\PageVariant::__sleep + */ + public function testPageVariant() { + $page_variant = $this->createPageVariant(); + + // Test that a very simple page variant successfully serializes. + /* @var \Drupal\page_manager\Entity\PageVariant $unserialized */ + $unserialized = $this->assertSerialization($page_variant); + $this->assertEquals($page_variant->id(), $unserialized->id()); + $this->assertEquals($page_variant->label(), $unserialized->label()); + $this->assertEquals($page_variant->getWeight(), $unserialized->getWeight()); + $this->assertEquals($page_variant->getVariantPluginId(), $unserialized->getVariantPluginId()); + + // Test setting the page. + $page = $this->createPage(); + $page_variant->setPageEntity($page); + $unserialized = $this->assertSerialization($page_variant); + $this->assertInstanceOf(Page::class, $unserialized->getPage()); + $this->assertEquals($page->id(), $unserialized->getPage()->id()); + + // Test adding static context. + $page_variant->setStaticContext('test', [ + 'label' => 'Test', + 'type' => 'integer', + 'value' => 1, + ]); + $unserialized = $this->assertSerialization($page_variant); + $this->assertEquals($page_variant->getStaticContexts(), $unserialized->getStaticContexts()); + + // Add context to the page directly to avoid the + // \Drupal\page_manager\Event\PageManagerEvents::PAGE_CONTEXT event which + // relies on the router. + $context = new Context(new ContextDefinition('integer', 'ID'), 1); + $page->addContext('id', $context); + + // Test initializing context. + $page_variant->getContexts(); + $unserialized = $this->assertSerialization($page_variant); + $this->assertNull($unserialized->get('contexts')); + + // Test adding selection criteria. + $condition = [ + 'id' => 'request_path', + 'pages' => '/admin/foo/*', + 'negate' => FALSE, + 'context_mapping' => [], + ]; + $page_variant->addSelectionCondition($condition); + $unserialized = $this->assertSerialization($page_variant); + $this->assertNull($unserialized->get('selectionConditionCollection')); + $this->assertEquals($page_variant->getSelectionConditions()->getConfiguration(), $unserialized->getSelectionConditions()->getConfiguration()); + + // Initialize the variant plugin. + $page_variant->getVariantPlugin(); + $unserialized = $this->assertSerialization($page_variant); + $this->assertNull($unserialized->get('variantPluginCollection')); + + // Test adding variant settings. + $page_variant->getVariantPlugin()->setConfiguration([ + 'page_title' => $this->randomString(), + 'blocks' => [], + ]); + $unserialized = $this->assertSerialization($page_variant); + $this->assertEquals($page_variant->getVariantPlugin()->getConfiguration(), $unserialized->getVariantPlugin()->getConfiguration()); + } + + /** + * Test serialization of a block_display variant plugin. + */ + public function testPageBlockVariantPlugin() { + $configuration = [ + 'page_title' => 'Test variant', + ]; + /* @var \Drupal\page_manager\Plugin\DisplayVariant\PageBlockDisplayVariant $variant_plugin */ + $variant_plugin = $this->container + ->get('plugin.manager.display_variant') + ->createInstance('block_display', $configuration); + $this->assertInstanceOf(PageBlockDisplayVariant::class, $variant_plugin); + + // Test that a very simple variant successfully serializes. + /* @var \Drupal\page_manager\Plugin\DisplayVariant\PageBlockDisplayVariant $unserialized */ + $unserialized = $this->assertSerialization($variant_plugin); + $this->assertEquals($variant_plugin->getConfiguration(), $unserialized->getConfiguration()); + + // Set some context. + $context = new Context(new ContextDefinition('integer', 'ID'), 1); + $variant_plugin->setContexts(['id' => $context]); + $unserialized = $this->assertSerialization($variant_plugin); + $this->assertEquals([], $unserialized->getContexts()); + } + + /** + * Test serialization of a block_display variant plugin. + */ + public function testHttpStatusCodeVariantPlugin() { + $configuration = [ + 'status_code' => '404', + ]; + /* @var \Drupal\page_manager\Plugin\DisplayVariant\HttpStatusCodeDisplayVariant $variant_plugin */ + $variant_plugin = $this->container + ->get('plugin.manager.display_variant') + ->createInstance('http_status_code', $configuration); + $this->assertInstanceOf(HttpStatusCodeDisplayVariant::class, $variant_plugin); + + // Test that a very simple variant successfully serializes. + /* @var \Drupal\page_manager\Plugin\DisplayVariant\PageBlockDisplayVariant $unserialized */ + $unserialized = $this->assertSerialization($variant_plugin); + $this->assertEquals($variant_plugin->getConfiguration(), $unserialized->getConfiguration()); + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/src/Unit/ContextMapperTest.php b/docroot/modules/contrib/page_manager/tests/src/Unit/ContextMapperTest.php new file mode 100644 index 000000000..fd8067c7a --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/src/Unit/ContextMapperTest.php @@ -0,0 +1,107 @@ +typedDataManager = $this->prophesize(TypedDataManager::class); + $this->entityRepository = $this->prophesize(EntityRepositoryInterface::class); + $this->staticContext = new ContextMapper($this->entityRepository->reveal()); + + $container = new ContainerBuilder(); + $container->set('typed_data_manager', $this->typedDataManager->reveal()); + \Drupal::setContainer($container); + } + + /** + * @covers ::getContextValues + */ + public function testGetContextValues() { + $input = []; + $actual = $this->staticContext->getContextValues($input); + $this->assertEquals([], $actual); + } + + /** + * @covers ::getContextValues + */ + public function testGetContextValuesContext() { + $data_definition = DataDefinition::createFromDataType('integer'); + $typed_data = IntegerData::createInstance($data_definition); + $this->typedDataManager->createDataDefinition('integer')->willReturn($data_definition); + $this->typedDataManager->getDefaultConstraints($data_definition)->willReturn([]); + $this->typedDataManager->create($data_definition, 5)->willReturn($typed_data); + + $input = [ + 'foo' => [ + 'label' => 'Foo', + 'type' => 'integer', + 'value' => 5, + ], + ]; + $expected = new Context(new ContextDefinition('integer', 'Foo'), 5); + $actual = $this->staticContext->getContextValues($input)['foo']; + $this->assertEquals($expected, $actual); + } + + /** + * @covers ::getContextValues + */ + public function testGetContextValuesEntityContext() { + $input = [ + 'foo' => [ + 'label' => 'Foo', + 'type' => 'entity:node', + 'value' => 'the_node_uuid', + ], + ]; + $expected = new EntityLazyLoadContext(new ContextDefinition('entity:node', 'Foo'), $this->entityRepository->reveal(), 'the_node_uuid'); + $actual = $this->staticContext->getContextValues($input)['foo']; + $this->assertEquals($expected, $actual); + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/src/Unit/CurrentUserContextTest.php b/docroot/modules/contrib/page_manager/tests/src/Unit/CurrentUserContextTest.php new file mode 100644 index 000000000..ec7ab04f9 --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/src/Unit/CurrentUserContextTest.php @@ -0,0 +1,62 @@ +prophesize(AccountInterface::class); + $account->id()->willReturn(1); + $user = $this->prophesize(UserInterface::class); + + $data_definition = new DataDefinition(['type' => 'entity:user']); + + $this->typedDataManager->create($data_definition, $user) + ->willReturn(EntityAdapter::createFromEntity($user->reveal())); + + $this->typedDataManager->getDefaultConstraints($data_definition) + ->willReturn([]); + + $this->typedDataManager->createDataDefinition('entity:user') + ->will(function () use ($data_definition) { + return $data_definition; + }); + + $this->page->addContext('current_user', Argument::type(Context::class))->shouldBeCalled(); + + $user_storage = $this->prophesize(EntityStorageInterface::class); + $user_storage->load(1)->willReturn($user->reveal()); + + $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); + $entity_type_manager->getStorage('user')->willReturn($user_storage->reveal()); + + $route_param_context = new CurrentUserContext($account->reveal(), $entity_type_manager->reveal()); + $route_param_context->onPageContext($this->event); + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/src/Unit/LanguageInterfaceContextTest.php b/docroot/modules/contrib/page_manager/tests/src/Unit/LanguageInterfaceContextTest.php new file mode 100644 index 000000000..8f534a57c --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/src/Unit/LanguageInterfaceContextTest.php @@ -0,0 +1,56 @@ +getMockBuilder('\Drupal\Core\Language\LanguageManagerInterface') + ->disableOriginalConstructor() + ->getMock(); + + $context = new Context(new ContextDefinition('language', 'current_language_context'), $language_manager->getCurrentLanguage(LanguageInterface::TYPE_INTERFACE)); + + $this->contextRepository = $this->getMockBuilder('\Drupal\Core\Plugin\Context\ContextRepositoryInterface') + ->disableOriginalConstructor() + ->getMock(); + $this->contextRepository->expects($this->once()) + ->method('getRunTimeContexts') + ->willReturn(array('@language.current_language_context:language_interface' => $context)); + } + + /** + * @covers ::onPageContext + */ + public function testOnPageContext() { + $this->page->addContext('language_interface', Argument::type(Context::class))->shouldBeCalled(); + $language_interface_context = new LanguageInterfaceContext($this->contextRepository); + $language_interface_context->onPageContext($this->event); + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/src/Unit/PageAccessTest.php b/docroot/modules/contrib/page_manager/tests/src/Unit/PageAccessTest.php new file mode 100644 index 000000000..f3ce5edb5 --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/src/Unit/PageAccessTest.php @@ -0,0 +1,154 @@ +contextHandler = $this->prophesize(ContextHandlerInterface::class); + $this->entityType = $this->prophesize(EntityTypeInterface::class); + + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + $module_handler->invokeAll(Argument::cetera())->willReturn([]); + + $this->pageAccess = new PageAccess($this->entityType->reveal(), $this->contextHandler->reveal()); + $this->pageAccess->setModuleHandler($module_handler->reveal()); + + $this->cacheContextsManager = $this->prophesize(CacheContextsManager::class); + $container = new ContainerBuilder(); + $container->set('cache_contexts_manager', $this->cacheContextsManager->reveal()); + \Drupal::setContainer($container); + } + + /** + * @covers ::checkAccess + */ + public function testAccessView() { + $page = $this->prophesize(PageInterface::class); + + $page->getContexts()->willReturn([]); + $page->getAccessConditions()->willReturn([]); + $page->getAccessLogic()->willReturn('and'); + $page->status()->willReturn(TRUE); + $page->language()->willReturn($this->prophesize(LanguageInterface::class)->reveal()); + + $page->uuid()->shouldBeCalled(); + $page->getEntityTypeId()->shouldBeCalled(); + + $account = $this->prophesize(AccountInterface::class); + + $this->assertTrue($this->pageAccess->access($page->reveal(), 'view', $account->reveal())); + } + + /** + * @covers ::checkAccess + */ + public function testAccessViewDisabled() { + $page = $this->prophesize(PageInterface::class); + $page->status()->willReturn(FALSE); + $page->getCacheTags()->willReturn(['page:1']); + $page->getCacheContexts()->willReturn([]); + $page->getCacheMaxAge()->willReturn(0); + $page->language()->willReturn($this->prophesize(LanguageInterface::class)->reveal()); + + $page->uuid()->shouldBeCalled(); + $page->getEntityTypeId()->shouldBeCalled(); + + $account = $this->prophesize(AccountInterface::class); + + $this->assertFalse($this->pageAccess->access($page->reveal(), 'view', $account->reveal())); + } + + /** + * @covers ::checkAccess + * + * @dataProvider providerTestAccessDelete + */ + public function testAccessDelete($is_new, $expected) { + $this->entityType->getAdminPermission()->willReturn('test permission'); + + $page = $this->prophesize(PageInterface::class); + $page->isNew()->willReturn($is_new); + $page->language()->willReturn($this->prophesize(LanguageInterface::class)->reveal()); + + $page->uuid()->shouldBeCalled(); + $page->getEntityTypeId()->shouldBeCalled(); + + // Ensure that the cache tag is added for the temporary conditions. + if ($is_new) { + $page->getCacheTags()->willReturn(['page:1']); + $page->getCacheContexts()->willReturn([]); + $page->getCacheMaxAge()->willReturn(0); + } + else { + $this->cacheContextsManager->assertValidTokens(['user.permissions'])->willReturn(TRUE); + } + + $account = $this->prophesize(AccountInterface::class); + $account->hasPermission('test permission')->willReturn(TRUE); + $account->id()->shouldBeCalled(); + + $this->assertSame($expected, $this->pageAccess->access($page->reveal(), 'delete', $account->reveal())); + } + + /** + * Provides data for testAccessDelete(). + */ + public function providerTestAccessDelete() { + $data = []; + $data[] = [TRUE, FALSE]; + $data[] = [FALSE, TRUE]; + return $data; + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/src/Unit/PageBlockDisplayVariantTest.php b/docroot/modules/contrib/page_manager/tests/src/Unit/PageBlockDisplayVariantTest.php new file mode 100644 index 000000000..2a140eace --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/src/Unit/PageBlockDisplayVariantTest.php @@ -0,0 +1,236 @@ +prophesize(AccountInterface::class); + $block1 = $this->prophesize(BlockPluginInterface::class); + $block1->access($account)->willReturn(TRUE); + + // Building a block with empty content. + $block1->build()->willReturn(['#cache' => [ 'tags' => [ 0 => 'tag_to_be_merged']]]); + + $context_handler = $this->prophesize(ContextHandlerInterface::class); + $uuid_generator = $this->prophesize(UuidInterface::class); + $token = $this->prophesize(Token::class); + $block_manager = $this->prophesize(BlockManager::class); + $condition_manager = $this->prophesize(ConditionManager::class); + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + + $variant_plugin = new PageBlockDisplayVariant([], '', [], $context_handler->reveal(), $account->reveal(), $uuid_generator->reveal(), $token->reveal(), $block_manager->reveal(), $condition_manager->reveal(), $module_handler->reveal()); + + // Empty block. + $expected_build = [ + '#markup' => '', + '#cache' => [ + 'tags' => [ + 'block_plugin:block_plugin_id', + 'page:page_id', + 'tag_to_be_merged', + ], + 'contexts' => [], + 'max-age' => -1, + ], + ]; + + $build = [ + '#block_plugin' => $block1->reveal(), + '#cache' => [ + 'tags' => [ + 'page:page_id', + 'block_plugin:block_plugin_id', + ], + ], + ]; + + $build = $variant_plugin->buildBlock($build); + + // Assert that cacheability metadata is merged. + $this->assertSame($expected_build, $build); + } + + /** + * Tests the build() method when blocks can be cached. + * + * @covers ::build + * @covers ::buildRegions + * @covers ::buildBlock + */ + public function testBuild() { + $container = new ContainerBuilder(); + $cache_contexts = $this->prophesize(CacheContextsManager::class); + $container->set('cache_contexts_manager', $cache_contexts->reveal()); + \Drupal::setContainer($container); + + $account = $this->prophesize(AccountInterface::class); + + // Define one block that allows access, access varies by permissions. + $cache_contexts->assertValidTokens(['user.permissions'])->willReturn(TRUE); + $block1 = $this->prophesize(BlockPluginInterface::class); + $block1->access($account, TRUE)->willReturn(AccessResult::allowed()->cachePerPermissions()); + $block1->getConfiguration()->willReturn(['label' => 'Block label']); + $block1->getPluginId()->willReturn('block_plugin_id'); + $block1->getBaseId()->willReturn('block_base_plugin_id'); + $block1->getDerivativeId()->willReturn('block_derivative_plugin_id'); + $block1->getCacheTags()->willReturn(['block_plugin1:block_plugin_id']); + $block1->getCacheMaxAge()->willReturn(3600); + $block1->getCacheContexts()->willReturn(['url']); + + // Define another block that doesn't allow access, varies by user. + $cache_contexts->assertValidTokens(['user'])->willReturn(TRUE); + $block2 = $this->prophesize()->willImplement(ContextAwarePluginInterface::class)->willImplement(BlockPluginInterface::class); + $block2->access($account, TRUE)->willReturn(AccessResult::forbidden()->cachePerUser()); + $block2->getConfiguration()->willReturn([]); + $block2->getPluginId()->willReturn('block_plugin_id'); + $block2->getBaseId()->willReturn('block_base_plugin_id'); + $block2->getDerivativeId()->willReturn('block_derivative_plugin_id'); + // The block is not shown, so cacheability metadata is not collected. + $block2->getCacheContexts()->shouldNotBeCalled(); + $block2->getCacheMaxAge()->shouldNotBeCalled(); + $block2->getCacheTags()->shouldNotBeCalled(); + $blocks = [ + 'top' => [ + 'block1' => $block1->reveal(), + 'block2' => $block2->reveal(), + ], + ]; + $block_collection = $this->getMockBuilder(BlockPluginCollection::class) + ->disableOriginalConstructor() + ->getMock(); + $block_collection->expects($this->once()) + ->method('getAllByRegion') + ->willReturn($blocks); + + $context_handler = $this->prophesize(ContextHandlerInterface::class); + $context_handler->applyContextMapping($block2->reveal(), [])->shouldBeCalledTimes(1); + + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + $module_handler->alter(); + $uuid_generator = $this->prophesize(UuidInterface::class); + $page_title = 'Page title'; + $token = $this->getMockBuilder(Token::class) + ->disableOriginalConstructor() + ->getMock(); + $block_manager = $this->prophesize(BlockManager::class); + $condition_manager = $this->prophesize(ConditionManager::class); + $variant_plugin = $this->getMockBuilder(PageBlockDisplayVariant::class) + ->setConstructorArgs([['page_title' => $page_title, 'uuid' => 'UUID'], 'test', [], $context_handler->reveal(), $account->reveal(), $uuid_generator->reveal(), $token, $block_manager->reveal(), $condition_manager->reveal(), $module_handler->reveal()]) + ->setMethods(['renderPageTitle']) + ->getMock(); + + $property = new \ReflectionProperty($variant_plugin, 'blockPluginCollection'); + $property->setAccessible(TRUE); + $property->setValue($variant_plugin, $block_collection); + + $page = $this->prophesize(PageInterface::class); + $page->id()->willReturn('page_id'); + + $variant_plugin->expects($this->once()) + ->method('renderPageTitle') + ->with($page_title) + ->willReturn($page_title); + + $expected_cache_block1 = [ + 'keys' => ['page_manager_block_display', 'UUID', 'block', 'block1'], + 'tags' => ['block_plugin1:block_plugin_id'], + 'contexts' => ['url'], + 'max-age' => 3600, + ]; + $cache_contexts->assertValidTokens(['user.permissions', 'url'])->willReturn(TRUE); + + // The page cacheability metadata contains the access cacheability metadata + // of accessible and non-accessible blocks. Additionally, the cacheability + // metadata of accessible blocks is merged to avoid cache redirects when + // possible. + $expected_cache_page = [ + 'keys' => ['page_manager_block_display', 'UUID'], + 'contexts' => ['url', 'user', 'user.permissions'], + 'tags' => ['block_plugin1:block_plugin_id'], + 'max-age' => 3600, + ]; + $cache_contexts->assertValidTokens(['url', 'user.permissions', 'user'])->willReturn(TRUE); + + // Build the variant and ensure that pre_render is set only for the first + // block. + $build = $variant_plugin->build(); + $build = $variant_plugin->buildRegions($build); + $this->assertSame([$variant_plugin, 'buildBlock'], $build['top']['block1']['#pre_render'][0]); + $this->assertTrue(empty($build['top']['block2'])); + $this->assertSame($expected_cache_block1, $build['top']['block1']['#cache']); + $this->assertSame($expected_cache_page, $build['#cache']); + + // Ensure that building the block returns the correct markup. + $block1->build()->willReturn([ + '#markup' => 'block1_build_value', + ]); + $block1_build = $variant_plugin->buildBlock($build['top']['block1']); + $this->assertSame(['#markup' => 'block1_build_value'], $block1_build['content']); + } + + /** + * Tests the submitConfigurationForm() method. + * + * @covers ::submitConfigurationForm + */ + public function testSubmitConfigurationForm() { + $account = $this->prophesize(AccountInterface::class); + $context_handler = $this->prophesize(ContextHandlerInterface::class); + $uuid_generator = $this->prophesize(UuidInterface::class); + $token = $this->prophesize(Token::class); + $block_manager = $this->prophesize(BlockManager::class); + $condition_manager = $this->prophesize(ConditionManager::class); + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + + $variant_plugin = new PageBlockDisplayVariant([], '', [], $context_handler->reveal(), $account->reveal(), $uuid_generator->reveal(), $token->reveal(), $block_manager->reveal(), $condition_manager->reveal(), $module_handler->reveal()); + + $values = ['page_title' => "Go hang a salami, I'm a lasagna hog!"]; + + $form = []; + $form_state = (new FormState())->setValues($values); + $variant_plugin->submitConfigurationForm($form, $form_state); + + $property = new \ReflectionProperty($variant_plugin, 'configuration'); + $property->setAccessible(TRUE); + $this->assertSame($values['page_title'], $property->getValue($variant_plugin)['page_title']); + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/src/Unit/PageContextTestBase.php b/docroot/modules/contrib/page_manager/tests/src/Unit/PageContextTestBase.php new file mode 100644 index 000000000..dbe74deb8 --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/src/Unit/PageContextTestBase.php @@ -0,0 +1,60 @@ +typedDataManager = $this->prophesize(TypedDataManager::class); + + $container = new ContainerBuilder(); + $container->set('string_translation', $this->getStringTranslationStub()); + $container->set('typed_data_manager', $this->typedDataManager->reveal()); + \Drupal::setContainer($container); + + $this->page = $this->prophesize(PageInterface::class); + + $this->event = new PageManagerContextEvent($this->page->reveal()); + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/src/Unit/PageManagerRoutesTest.php b/docroot/modules/contrib/page_manager/tests/src/Unit/PageManagerRoutesTest.php new file mode 100644 index 000000000..d0fff1491 --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/src/Unit/PageManagerRoutesTest.php @@ -0,0 +1,441 @@ +pageStorage = $this->prophesize(ConfigEntityStorageInterface::class); + + $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + $this->entityTypeManager->getStorage('page') + ->willReturn($this->pageStorage); + $this->cacheTagsInvalidator = $this->prophesize(CacheTagsInvalidatorInterface::class); + + $this->routeSubscriber = new PageManagerRoutes($this->entityTypeManager->reveal(), $this->cacheTagsInvalidator->reveal()); + } + + /** + * Tests adding routes for enabled and disabled pages. + * + * @covers ::alterRoutes + */ + public function testAlterRoutesWithStatus() { + // Set up a valid page. + /** @var \Drupal\page_manager\PageInterface|\Prophecy\Prophecy\ProphecyInterface $page1 */ + $page1 = $this->prophesize(PageInterface::class); + $page1->status() + ->willReturn(TRUE) + ->shouldBeCalled(); + $page1->getPath() + ->willReturn('/page1') + ->shouldBeCalled(); + $page1->id()->willReturn('page1'); + + $variant1 = $this->prophesize(PageVariantInterface::class); + $variant1->getWeight()->willReturn(0); + $page1->getVariants() + ->willReturn(['variant1' => $variant1->reveal()]); + $page1->label() + ->willReturn('Page label') + ->shouldBeCalled(); + $page1->usesAdminTheme() + ->willReturn(TRUE) + ->shouldBeCalled(); + $page1->getParameters() + ->willReturn([]) + ->shouldBeCalled(); + $pages['page1'] = $page1->reveal(); + + // Set up a disabled page. + /** @var \Drupal\page_manager\PageInterface|\Prophecy\Prophecy\ProphecyInterface $page2 */ + $page2 = $this->prophesize(PageInterface::class); + $page2->status() + ->willReturn(FALSE) + ->shouldBeCalled(); + $page2->getVariants() + ->willReturn(['variant2' => 'variant2']); + $page2->id()->willReturn('page1'); + $page2->getPath()->willReturn('/page2'); + $page2->getParameters() + ->willReturn([]); + $pages['page2'] = $page2->reveal(); + + $this->pageStorage->loadMultiple() + ->willReturn($pages) + ->shouldBeCalledTimes(1); + + $collection = new RouteCollection(); + $route_event = new RouteBuildEvent($collection); + $this->routeSubscriber->onAlterRoutes($route_event); + + // Only the valid page should be in the collection. + $this->assertSame(['page_manager.page_view_page1_variant1'], array_keys($collection->all())); + $route = $collection->get('page_manager.page_view_page1_variant1'); + $expected_defaults = [ + '_entity_view' => 'page_manager_page_variant', + '_title' => 'Page label', + 'page_manager_page_variant' => 'variant1', + 'page_manager_page' => 'page1', + 'page_manager_page_variant_weight' => 0, + 'base_route_name' => 'page_manager.page_view_page1', + ]; + $expected_requirements = [ + '_page_access' => 'page_manager_page.view', + ]; + $expected_options = [ + 'compiler_class' => 'Symfony\Component\Routing\RouteCompiler', + 'parameters' => [ + 'page_manager_page_variant' => [ + 'type' => 'entity:page_variant', + ], + 'page_manager_page' => [ + 'type' => 'entity:page', + ], + ], + '_admin_route' => TRUE, + ]; + $this->assertMatchingRoute($route, '/page1', $expected_defaults, $expected_requirements, $expected_options); + } + + /** + * Tests overriding an existing route. + * + * @covers ::alterRoutes + * @covers ::findOverriddenRouteName + * + * @dataProvider providerTestAlterRoutesOverrideExisting + */ + public function testAlterRoutesOverrideExisting($page_path, $existing_route_path, $requirements = []) { + $route_name = 'test_route'; + // Set up a page with the same path as an existing route. + /** @var \Drupal\page_manager\PageInterface|\Prophecy\Prophecy\ProphecyInterface $page */ + $page = $this->prophesize(PageInterface::class); + $page->status() + ->willReturn(TRUE) + ->shouldBeCalled(); + $page->getPath() + ->willReturn($page_path) + ->shouldBeCalled(); + $page->getParameters() + ->willReturn([]) + ->shouldBeCalled(); + $variant1 = $this->prophesize(PageVariantInterface::class); + $variant1->getWeight()->willReturn(0); + $page->getVariants() + ->willReturn(['variant1' => $variant1->reveal()]); + $page->id()->willReturn('page1'); + $page->label()->willReturn(NULL); + $page->usesAdminTheme()->willReturn(FALSE); + + $this->pageStorage->loadMultiple() + ->willReturn(['page1' => $page->reveal()]) + ->shouldBeCalledTimes(1); + + $this->cacheTagsInvalidator->invalidateTags(["page_manager_route_name:$route_name"])->shouldBeCalledTimes(1); + + $collection = new RouteCollection(); + $collection->add("$route_name.POST", new Route($existing_route_path, ['default_exists' => 'default_value'], $requirements, ['parameters' => ['foo' => ['type' => 'bar']]], '', [], ['POST'])); + $collection->add("$route_name.POST_with_format", new Route($existing_route_path, ['default_exists' => 'default_value'], $requirements + ['_format' => 'json'], ['parameters' => ['foo' => ['type' => 'bar']]], '', [], ['GET', 'POST'])); + $collection->add($route_name, new Route($existing_route_path, ['default_exists' => 'default_value'], $requirements, ['parameters' => ['foo' => ['type' => 'bar']]])); + $route_event = new RouteBuildEvent($collection); + $this->routeSubscriber->onAlterRoutes($route_event); + + // The existing route name is not overridden. + $this->assertSame([ + 'test_route.POST', + 'test_route.POST_with_format', + 'test_route', + 'page_manager.page_view_page1_variant1', + ], array_keys($collection->all())); + + $route = $collection->get('page_manager.page_view_page1_variant1'); + $expected_defaults = [ + '_entity_view' => 'page_manager_page_variant', + '_title' => NULL, + 'page_manager_page_variant' => 'variant1', + 'page_manager_page' => 'page1', + 'page_manager_page_variant_weight' => 0, + 'overridden_route_name' => 'test_route', + 'base_route_name' => 'test_route', + ]; + $expected_requirements = $requirements + ['_page_access' => 'page_manager_page.view']; + $expected_options = [ + 'compiler_class' => 'Symfony\Component\Routing\RouteCompiler', + 'parameters' => [ + 'foo' => ['type' => 'bar'], + 'page_manager_page_variant' => [ + 'type' => 'entity:page_variant', + ], + 'page_manager_page' => [ + 'type' => 'entity:page', + ], + ], + '_admin_route' => FALSE, + ]; + $this->assertMatchingRoute($route, $existing_route_path, $expected_defaults, $expected_requirements, $expected_options); + } + + public function providerTestAlterRoutesOverrideExisting() { + $data = []; + $data['no_slug'] = ['/test_route', '/test_route']; + $data['slug'] = ['/test_route/{test_route}', '/test_route/{test_route}']; + $data['placeholder'] = ['/test_route/%', '/test_route/{test_route}']; + $data['slug_with_default'] = ['/test_route/{default_exists}', '/test_route/{default_exists}']; + $data['placeholder_with_default'] = ['/test_route/%', '/test_route/{default_exists}']; + $data['with_requirement'] = ['/test_route/{foo}', '/test_route/{foo}', ['foo' => '\d+']]; + return $data; + } + + /** + * @covers ::alterRoutes + */ + public function testAlterRoutesMultipleVariantsDifferentRequirements() { + $variant1 = $this->prophesize(PageVariantInterface::class); + $variant2 = $this->prophesize(PageVariantInterface::class); + $variant1->getWeight()->willReturn(0); + + $page1 = $this->prophesize(PageInterface::class); + $page1->status()->willReturn(TRUE); + $page1->getVariants()->willReturn(['variant1' => $variant1->reveal()]); + $page1->getPath()->willReturn('/test_route1'); + $page1->getParameters()->willReturn([]); + $page1->id()->willReturn('page1'); + $page1->label()->willReturn('Page 1'); + $page1->usesAdminTheme()->willReturn(FALSE); + + $page2 = $this->prophesize(PageInterface::class); + $page2->status()->willReturn(TRUE); + $page2->getVariants()->willReturn(['variant2' => $variant2->reveal()]); + $page2->getPath()->willReturn('/test_route2'); + $page2->getParameters()->willReturn([]); + $page2->id()->willReturn('page2'); + $page2->label()->willReturn('Page 2'); + $page2->usesAdminTheme()->willReturn(FALSE); + + $this->pageStorage->loadMultiple()->willReturn(['page1' => $page1->reveal(), 'page2' => $page2->reveal()]); + + $collection = new RouteCollection(); + $collection->add('test_route', new Route('/test_route1', [], ['_access' => 'TRUE'], [])); + $route_event = new RouteBuildEvent($collection); + $this->routeSubscriber->onAlterRoutes($route_event); + + $this->assertSame([ + 'test_route', + 'page_manager.page_view_page1_variant1', + 'page_manager.page_view_page2_variant2', + ], array_keys($collection->all())); + $expected = [ + 'test_route' => [ + 'path' => '/test_route1', + 'defaults' => [ + ], + 'requirements' => [ + '_access' => 'TRUE', + ], + 'options' => [ + 'compiler_class' => 'Symfony\Component\Routing\RouteCompiler', + ], + ], + 'page_manager.page_view_page1_variant1' => [ + 'path' => '/test_route1', + 'defaults' => [ + '_entity_view' => 'page_manager_page_variant', + '_title' => 'Page 1', + 'page_manager_page_variant' => 'variant1', + 'page_manager_page' => 'page1', + 'page_manager_page_variant_weight' => 0, + 'overridden_route_name' => 'test_route', + 'base_route_name' => 'test_route', + ], + 'requirements' => [ + '_access' => 'TRUE', + '_page_access' => 'page_manager_page.view', + ], + 'options' => [ + 'compiler_class' => 'Symfony\Component\Routing\RouteCompiler', + 'parameters' => [ + 'page_manager_page_variant' => [ + 'type' => 'entity:page_variant', + ], + 'page_manager_page' => [ + 'type' => 'entity:page', + ], + ], + '_admin_route' => FALSE, + ], + ], + 'page_manager.page_view_page2_variant2' => [ + 'path' => '/test_route2', + 'defaults' => [ + '_entity_view' => 'page_manager_page_variant', + '_title' => 'Page 2', + 'page_manager_page_variant' => 'variant2', + 'page_manager_page' => 'page2', + 'page_manager_page_variant_weight' => 0, + 'base_route_name' => 'page_manager.page_view_page2', + ], + 'requirements' => [ + '_page_access' => 'page_manager_page.view', + ], + 'options' => [ + 'compiler_class' => 'Symfony\Component\Routing\RouteCompiler', + 'parameters' => [ + 'page_manager_page_variant' => [ + 'type' => 'entity:page_variant', + ], + 'page_manager_page' => [ + 'type' => 'entity:page', + ], + ], + '_admin_route' => FALSE, + ], + ], + ]; + foreach ($collection as $route_name => $route) { + $this->assertMatchingRoute($route, $expected[$route_name]['path'], $expected[$route_name]['defaults'], $expected[$route_name]['requirements'], $expected[$route_name]['options']); + } + } + + /** + * Tests overriding an existing route with configured parameters. + * + * @covers ::alterRoutes + * @covers ::findOverriddenRouteName + * + * @dataProvider providerTestAlterRoutesOverrideExisting + */ + public function testAlterRoutesOverrideExistingWithConfiguredParameters($page_path, $existing_route_path, $requirements = []) { + $route_name = 'test_route'; + // Set up a page with the same path as an existing route. + /** @var \Drupal\page_manager\PageInterface|\Prophecy\Prophecy\ProphecyInterface $page */ + $page = $this->prophesize(PageInterface::class); + $page->status()->willReturn(TRUE); + $page->getPath()->willReturn($page_path); + $page->id()->willReturn('page1'); + $page->label()->willReturn(NULL); + $page->usesAdminTheme()->willReturn(FALSE); + $page->getParameters()->willReturn(['foo' => ['machine_name' => 'foo', 'type' => 'integer', 'label' => 'Foo'], 'test_route' => ['machine_name' => 'test_route', 'type' => '', 'label' => '']]); + + $variant1 = $this->prophesize(PageVariantInterface::class); + $variant1->getWeight()->willReturn(0); + $page->getVariants()->willReturn(['variant1' => $variant1->reveal()]); + + $this->pageStorage->loadMultiple()->willReturn(['page1' => $page->reveal()]); + + $collection = new RouteCollection(); + $collection->add($route_name, new Route($existing_route_path, ['default_exists' => 'default_value'], $requirements, ['parameters' => ['foo' => ['bar' => 'bar']]])); + $route_event = new RouteBuildEvent($collection); + $this->routeSubscriber->onAlterRoutes($route_event); + + $expected_defaults = [ + '_entity_view' => 'page_manager_page_variant', + '_title' => NULL, + 'page_manager_page_variant' => 'variant1', + 'page_manager_page' => 'page1', + 'page_manager_page_variant_weight' => 0, + 'overridden_route_name' => $route_name, + 'base_route_name' => 'test_route', + ]; + $expected_requirements = $requirements + ['_page_access' => 'page_manager_page.view']; + $expected_options = [ + 'compiler_class' => 'Symfony\Component\Routing\RouteCompiler', + 'parameters' => [ + 'foo' => [ + 'bar' => 'bar', + 'type' => 'integer', + ], + 'page_manager_page_variant' => [ + 'type' => 'entity:page_variant', + ], + 'page_manager_page' => [ + 'type' => 'entity:page', + ], + ], + '_admin_route' => FALSE, + ]; + $this->assertSame([ + 'test_route', + 'page_manager.page_view_page1_variant1', + ], array_keys($collection->all())); + $this->assertMatchingRoute($collection->get('page_manager.page_view_page1_variant1'), $existing_route_path, $expected_defaults, $expected_requirements, $expected_options); + } + + /** + * Asserts that a route object has the expected properties. + * + * @param \Symfony\Component\Routing\Route $route + * The route to test. + * @param string $expected_path + * The expected path for the route. + * @param array $expected_defaults + * The expected defaults for the route. + * @param array $expected_requirements + * The expected requirements for the route. + * @param array $expected_options + * The expected options for the route. + */ + protected function assertMatchingRoute(Route $route, $expected_path, array $expected_defaults, array $expected_requirements, array $expected_options) { + $this->assertEquals($expected_path, $route->getPath()); + $this->assertEquals($expected_defaults, $route->getDefaults()); + $this->assertEquals($expected_requirements, $route->getRequirements()); + $this->assertEquals($expected_options, $route->getOptions()); + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/src/Unit/PageTest.php b/docroot/modules/contrib/page_manager/tests/src/Unit/PageTest.php new file mode 100644 index 000000000..53b40224c --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/src/Unit/PageTest.php @@ -0,0 +1,268 @@ +page = new Page(['id' => 'the_page'], 'page'); + } + + /** + * @covers ::getVariants + */ + public function testGetVariants() { + $variant1 = $this->prophesize(PageVariantInterface::class); + $variant1->id()->willReturn('variant1'); + $variant1->getWeight()->willReturn(0); + $variant2 = $this->prophesize(PageVariantInterface::class); + $variant2->id()->willReturn('variant2'); + $variant2->getWeight()->willReturn(-10); + + $entity_storage = $this->prophesize(EntityStorageInterface::class); + $entity_storage + ->loadByProperties(['page' => 'the_page']) + ->willReturn(['variant1' => $variant1->reveal(), 'variant2' => $variant2->reveal()]) + ->shouldBeCalledTimes(1); + + $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); + $entity_type_manager->getStorage('page_variant')->willReturn($entity_storage); + + $container = new ContainerBuilder(); + $container->set('entity_type.manager', $entity_type_manager->reveal()); + \Drupal::setContainer($container); + + $variants = $this->page->getVariants(); + $this->assertSame(['variant2' => $variant2->reveal(), 'variant1' => $variant1->reveal()], $variants); + $variants = $this->page->getVariants(); + $this->assertSame(['variant2' => $variant2->reveal(), 'variant1' => $variant1->reveal()], $variants); + } + + /** + * @covers ::addVariant + */ + public function testAddVariant() { + $variant1 = $this->prophesize(PageVariantInterface::class); + $variant1->id()->willReturn('variant1'); + $variant1->getWeight()->willReturn(0); + + $variant2 = $this->prophesize(PageVariantInterface::class); + $variant2->id()->willReturn('variant2'); + $variant2->getWeight()->willReturn(-10); + + $variant3 = $this->prophesize(PageVariantInterface::class); + $variant3->id()->willReturn('variant3'); + $variant3->getWeight()->willReturn(-5); + + $entity_storage = $this->prophesize(EntityStorageInterface::class); + $entity_storage + ->loadByProperties(['page' => 'the_page']) + ->willReturn([ + 'variant1' => $variant1->reveal(), + 'variant2' => $variant2->reveal(), + ]) + ->shouldBeCalledTimes(1); + + $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); + $entity_type_manager->getStorage('page_variant')->willReturn($entity_storage); + + $container = new ContainerBuilder(); + $container->set('entity_type.manager', $entity_type_manager->reveal()); + \Drupal::setContainer($container); + + // Check that Variant 1 and 2 are in the right order. + $variants = $this->page->getVariants(); + $this->assertSame([ + 'variant2' => $variant2->reveal(), + 'variant1' => $variant1->reveal(), + ], $variants); + + // Add Variant 3. + $this->page->addVariant($variant3->reveal()); + + // Check that Variant 1, 2 and 3 are in the right order. + $variants = $this->page->getVariants(); + $this->assertSame([ + 'variant2' => $variant2->reveal(), + 'variant3' => $variant3->reveal(), + 'variant1' => $variant1->reveal(), + ], $variants); + } + + /** + * @covers ::removeVariant + */ + public function testRemoveVariant() { + $variant1 = $this->prophesize(PageVariantInterface::class); + $variant1->id()->willReturn('variant1'); + $variant1->getWeight()->willReturn(0); + $variant1->delete()->shouldBeCalledTimes(1); + + $variant2 = $this->prophesize(PageVariantInterface::class); + $variant2->id()->willReturn('variant2'); + $variant2->getWeight()->willReturn(-10); + + $entity_storage = $this->prophesize(EntityStorageInterface::class); + $entity_storage + ->loadByProperties(['page' => 'the_page']) + ->willReturn([ + 'variant1' => $variant1->reveal(), + 'variant2' => $variant2->reveal(), + ]) + ->shouldBeCalledTimes(1); + + $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); + $entity_type_manager->getStorage('page_variant')->willReturn($entity_storage); + + $container = new ContainerBuilder(); + $container->set('entity_type.manager', $entity_type_manager->reveal()); + \Drupal::setContainer($container); + + // Check that Variant 1 and 2 are returned. + $variants = $this->page->getVariants(); + $this->assertSame([ + 'variant2' => $variant2->reveal(), + 'variant1' => $variant1->reveal(), + ], $variants); + + // Remove Variant 1. + $this->page->removeVariant($variant1->reveal()->id()); + + // Check that Variant 1 has been removed. + $variants = $this->page->getVariants(); + $this->assertSame([ + 'variant2' => $variant2->reveal(), + ], $variants); + } + + /** + * @covers ::addContext + */ + public function testAddContext() { + $context = new Context(new ContextDefinition('bar')); + $this->page->addContext('foo', $context); + $contexts = $this->page->getContexts(); + $this->assertSame(['foo' => $context], $contexts); + } + + /** + * @covers ::getContexts + */ + public function testGetContexts() { + $context = new Context(new ContextDefinition('bar')); + + $event_dispatcher = $this->prophesize(EventDispatcherInterface::class); + $event_dispatcher->dispatch(PageManagerEvents::PAGE_CONTEXT, Argument::type(PageManagerContextEvent::class)) + ->will(function ($args) use ($context) { + $args[1]->getPage()->addContext('foo', $context); + }); + + $container = new ContainerBuilder(); + $container->set('event_dispatcher', $event_dispatcher->reveal()); + \Drupal::setContainer($container); + + $contexts = $this->page->getContexts(); + $this->assertSame(['foo' => $context], $contexts); + } + + /** + * @covers ::filterParameters + */ + public function testFilterParameters() { + // Changing filters clears cached contexts on variants so we have to setup + // some variants for our page. + $variant1 = $this->prophesize(PageVariantInterface::class); + $variant1->id()->willReturn('variant1'); + $variant1->getWeight()->willReturn(0); + $variant1->resetCollectedContexts()->willReturn($variant1->reveal()); + $variant2 = $this->prophesize(PageVariantInterface::class); + $variant2->id()->willReturn('variant2'); + $variant2->getWeight()->willReturn(-10); + $variant2->resetCollectedContexts()->willReturn($variant2->reveal()); + + $entity_storage = $this->prophesize(EntityStorageInterface::class); + $entity_storage + ->loadByProperties(['page' => 'the_page']) + ->willReturn(['variant1' => $variant1->reveal(), 'variant2' => $variant2->reveal()]) + ->shouldBeCalledTimes(1); + + $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); + $entity_type_manager->getStorage('page_variant')->willReturn($entity_storage); + + $container = new ContainerBuilder(); + $container->set('entity_type.manager', $entity_type_manager->reveal()); + \Drupal::setContainer($container); + + $parameters = [ + 'foo' => [ + 'machine_name' => 'foo', + 'type' => 'integer', + 'label' => 'Foo', + ], + 'bar' => [ + 'machine_name' => 'bar', + 'type' => '', + 'label' => '', + ], + 'baz' => [ + 'machine_name' => 'baz', + 'type' => 'integer', + 'label' => 'Baz', + ] + ]; + $page = new Page(['id' => 'the_page', 'parameters' => $parameters, 'path' => 'test/{foo}/{bar}'], 'page'); + + $expected = $parameters; + unset($expected['baz']); + $this->assertEquals($expected, $page->getParameters()); + + $method = new \ReflectionMethod($page, 'filterParameters'); + $method->setAccessible(TRUE); + $method->invoke($page); + + $expected = [ + 'foo' => [ + 'machine_name' => 'foo', + 'type' => 'integer', + 'label' => 'Foo', + ], + ]; + $this->assertEquals($expected, $page->get('parameters')); + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/src/Unit/PageVariantTest.php b/docroot/modules/contrib/page_manager/tests/src/Unit/PageVariantTest.php new file mode 100644 index 000000000..1fdbfd233 --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/src/Unit/PageVariantTest.php @@ -0,0 +1,113 @@ +pageVariant = new PageVariant(['id' => 'the_page_variant', 'page' => 'the_page'], 'page_variant'); + $this->page = $this->prophesize(PageInterface::class); + + $entity_storage = $this->prophesize(EntityStorageInterface::class); + $entity_storage->load('the_page')->willReturn($this->page->reveal()); + + $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); + $entity_type_manager->getStorage('page')->willReturn($entity_storage); + + $this->contextMapper = $this->prophesize(ContextMapperInterface::class); + + $container = new ContainerBuilder(); + $container->set('entity_type.manager', $entity_type_manager->reveal()); + $container->set('page_manager.context_mapper', $this->contextMapper->reveal()); + \Drupal::setContainer($container); + } + + /** + * @covers ::getContexts + * @dataProvider providerTestGetContexts + */ + public function testGetContexts($static_contexts, $page_contexts, $expected) { + $this->contextMapper->getContextValues([])->willReturn($static_contexts)->shouldBeCalledTimes(1); + $this->page->getContexts()->willReturn($page_contexts)->shouldBeCalledTimes(1); + + $contexts = $this->pageVariant->getContexts(); + $this->assertSame($expected, $contexts); + $contexts = $this->pageVariant->getContexts(); + $this->assertSame($expected, $contexts); + } + + public function providerTestGetContexts() { + $data = []; + $data['empty'] = [ + [], + [], + [], + ]; + $data['additive'] = [ + ['static' => 'static'], + ['page' => 'page'], + ['page' => 'page', 'static' => 'static'], + ]; + $data['conflicting'] = [ + ['foo' => 'static'], + ['foo' => 'page'], + ['foo' => 'page'], + ]; + return $data; + } + + /** + * @covers ::getContexts + * @covers ::removeStaticContext + */ + public function testGetContextsAfterReset() { + $this->contextMapper->getContextValues([])->willReturn([])->shouldBeCalledTimes(2); + $this->page->getContexts()->willReturn([])->shouldBeCalledTimes(2); + + $expected = []; + $contexts = $this->pageVariant->getContexts(); + $this->assertSame($expected, $contexts); + $this->pageVariant->removeStaticContext('anything'); + $contexts = $this->pageVariant->getContexts(); + $this->assertSame($expected, $contexts); + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/src/Unit/RouteAttributesTest.php b/docroot/modules/contrib/page_manager/tests/src/Unit/RouteAttributesTest.php new file mode 100644 index 000000000..cb00a0b49 --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/src/Unit/RouteAttributesTest.php @@ -0,0 +1,40 @@ +assertEquals($expected, RouteAttributes::extractRawAttributes($route, $name, $path)); + } + + public function providerTestExtractRawAttributes() { + $data = []; + $data['no-parameters'] = [new Route('/prefix/a'), 'a_route', '/prefix', []]; + $data['no-matching-parameters'] = [new Route('/prefix/{x}'), 'a_route', '/different-prefix/b', []]; + $data['matching-parameters'] = [new Route('/prefix/{x}'), 'a_route', '/prefix/b', ['x' => 'b']]; + $data['with-defaults'] = [new Route('/prefix/{x}', ['foo' => 'bar']), 'a_route', '/different-prefix/b', ['foo' => 'bar']]; + return $data; + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/src/Unit/RouteNameResponseSubscriberTest.php b/docroot/modules/contrib/page_manager/tests/src/Unit/RouteNameResponseSubscriberTest.php new file mode 100644 index 000000000..ac2c78af3 --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/src/Unit/RouteNameResponseSubscriberTest.php @@ -0,0 +1,100 @@ +buildEvent($response); + + $route_name = 'the_route_name'; + $master_route_match = $this->prophesize(RouteMatchInterface::class); + $master_route_match->getParameter('base_route_name')->willReturn(NULL); + $master_route_match->getRouteName()->willReturn($route_name); + $current_route_match = $this->prophesize(StackedRouteMatchInterface::class); + $current_route_match->getMasterRouteMatch()->willReturn($master_route_match->reveal()); + + $subscriber = new RouteNameResponseSubscriber($current_route_match->reveal()); + $subscriber->onResponse($event); + + $expected = ["page_manager_route_name:$route_name"]; + $this->assertSame($expected, $response->getCacheableMetadata()->getCacheTags()); + } + + /** + * @covers ::onResponse + */ + public function testOnResponseUncacheable() { + $response = new Response(''); + $event = $this->buildEvent($response); + + $master_route_match = $this->prophesize(RouteMatchInterface::class); + $master_route_match->getParameter()->shouldNotBeCalled(); + $master_route_match->getRouteName()->shouldNotBeCalled(); + $current_route_match = $this->prophesize(StackedRouteMatchInterface::class); + $current_route_match->getMasterRouteMatch()->willReturn($master_route_match->reveal()); + + $subscriber = new RouteNameResponseSubscriber($current_route_match->reveal()); + $subscriber->onResponse($event); + } + + /** + * @covers ::onResponse + */ + public function testOnResponseCacheableWithBaseRouteName() { + $response = new CacheableResponse(''); + $event = $this->buildEvent($response); + + $route_name = 'the_route_name'; + $master_route_match = $this->prophesize(RouteMatchInterface::class); + $master_route_match->getParameter('base_route_name')->willReturn($route_name); + $master_route_match->getRouteName()->shouldNotBeCalled(); + $current_route_match = $this->prophesize(StackedRouteMatchInterface::class); + $current_route_match->getMasterRouteMatch()->willReturn($master_route_match->reveal()); + + $subscriber = new RouteNameResponseSubscriber($current_route_match->reveal()); + $subscriber->onResponse($event); + + $expected = ["page_manager_route_name:$route_name"]; + $this->assertSame($expected, $response->getCacheableMetadata()->getCacheTags()); + } + + /** + * Builds an event to wrap a response. + * + * @param \Symfony\Component\HttpFoundation\Response $response + * The response to be sent as the event payload. + * + * @return \Symfony\Component\HttpKernel\Event\FilterResponseEvent + * An event suitable for a KernelEvents::RESPONSE subscriber to process. + */ + protected function buildEvent(Response $response) { + $kernel = $this->prophesize(HttpKernelInterface::class); + $request = Request::create(''); + return new FilterResponseEvent($kernel->reveal(), $request, HttpKernelInterface::SUB_REQUEST, $response); + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/src/Unit/RouteParamContextTest.php b/docroot/modules/contrib/page_manager/tests/src/Unit/RouteParamContextTest.php new file mode 100644 index 000000000..f0ae72a69 --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/src/Unit/RouteParamContextTest.php @@ -0,0 +1,114 @@ +prophesize(RouteProviderInterface::class); + $route_provider->getRoutesByPattern('/test_route')->willReturn($collection); + + $request = new Request(); + $request_stack = new RequestStack(); + $request_stack->push($request); + + $data_definition = new DataDefinition(['type' => 'entity:user']); + + $typed_data = $this->prophesize(TypedDataInterface::class); + $this->typedDataManager->getDefaultConstraints($data_definition) + ->willReturn([]); + + $this->typedDataManager->create($data_definition, 'banana') + ->willReturn($typed_data->reveal()); + + $this->typedDataManager->createDataDefinition('bar') + ->will(function () use ($data_definition) { + return $data_definition; + }); + + $this->page->getPath()->willReturn('/test_route'); + $this->page->getParameter('foo')->willReturn(['machine_name' => 'foo', 'type' => 'integer', 'label' => 'Foo']); + $this->page->hasParameter('foo')->willReturn(TRUE); + $this->page->getParameter('bar')->willReturn(NULL); + $this->page->hasParameter('bar')->willReturn(FALSE); + $this->page->getParameter('baz')->willReturn(['machine_name' => 'baz', 'type' => 'integer', 'label' => '']); + $this->page->hasParameter('baz')->willReturn(TRUE); + $this->page->getParameter('page')->willReturn(['machine_name' => 'page', 'type' => 'entity:page', 'label' => '']); + $this->page->hasParameter('page')->willReturn(TRUE); + + $this->page->addContext('foo', Argument::that(function ($context) { + return $context instanceof Context && $context->getContextDefinition()->getLabel() == 'Foo'; + }))->shouldBeCalled(); + $this->page->addContext('baz', Argument::that(function ($context) { + return $context instanceof Context && $context->getContextDefinition()->getLabel() == '{baz} from route'; + }))->shouldBeCalled(); + $this->page->addContext('page', Argument::that(function ($context) { + return $context instanceof Context && $context->getContextDefinition()->getLabel() == '{page} from route'; + }))->shouldBeCalled(); + + $collection->add('test_route', new Route('/test_route', [], [], [ + 'parameters' => [ + 'foo' => ['type' => 'bar'], + 'baz' => ['type' => 'bop'], + 'page' => ['type' => 'entity:page'] + ], + ])); + + // Set up a request with one of the expected parameters as an attribute. + $request->attributes->add(['foo' => 'banana']); + + $route_param_context = new RouteParamContext($route_provider->reveal(), $request_stack); + $route_param_context->onPageContext($this->event); + } + + /** + * @covers ::onPageContext + */ + public function testOnPageContextEmpty() { + $collection = new RouteCollection(); + $route_provider = $this->prophesize(RouteProviderInterface::class); + $route_provider->getRoutesByPattern('/test_route')->willReturn($collection); + + $request = new Request(); + $request_stack = new RequestStack(); + $request_stack->push($request); + + $this->page->getPath()->willReturn('/test_route'); + + $this->page->addContext(Argument::cetera())->shouldNotBeCalled(); + + // Set up a request with one of the expected parameters as an attribute. + $request->attributes->add(['foo' => 'banana']); + + $route_param_context = new RouteParamContext($route_provider->reveal(), $request_stack); + $route_param_context->onPageContext($this->event); + } + +} diff --git a/docroot/modules/contrib/page_manager/tests/src/Unit/VariantRouteFilterTest.php b/docroot/modules/contrib/page_manager/tests/src/Unit/VariantRouteFilterTest.php new file mode 100644 index 000000000..1664fbb08 --- /dev/null +++ b/docroot/modules/contrib/page_manager/tests/src/Unit/VariantRouteFilterTest.php @@ -0,0 +1,452 @@ +pageVariantStorage = $this->prophesize(ConfigEntityStorageInterface::class); + + $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + $this->entityTypeManager->getStorage('page_variant') + ->willReturn($this->pageVariantStorage); + $this->currentPath = $this->prophesize(CurrentPathStack::class); + $this->requestStack = new RequestStack(); + + $this->routeFilter = new VariantRouteFilter($this->entityTypeManager->reveal(), $this->currentPath->reveal(), $this->requestStack); + } + + /** + * {@inheritdoc} + */ + protected function tearDown() { + // The request stack begins empty, ensure it is empty after filtering. + $this->assertNull($this->requestStack->getCurrentRequest()); + parent::tearDown(); + } + + /** + * @covers ::filter + */ + public function testFilterEmptyCollection() { + $route_collection = new RouteCollection(); + $request = new Request(); + + $this->currentPath->getPath($request)->shouldNotBeCalled(); + + $result = $this->routeFilter->filter($route_collection, $request); + $expected = []; + $this->assertSame($expected, $result->all()); + $this->assertSame([], $request->attributes->all()); + } + + /** + * @covers ::filter + * @covers ::getVariantRouteName + * @covers ::checkPageVariantAccess + */ + public function testFilterContextException() { + $route_collection = new RouteCollection(); + $request = new Request(); + + $route = new Route('/path/with/{slug}', ['page_manager_page_variant' => 'a_variant']); + $route_collection->add('a_route', $route); + + $page_variant = $this->prophesize(PageVariantInterface::class); + $page_variant->access('view')->willThrow(new ContextException()); + + $this->currentPath->getPath($request)->willReturn(''); + $this->pageVariantStorage->load('a_variant')->willReturn($page_variant->reveal()); + + $result = $this->routeFilter->filter($route_collection, $request); + $expected = []; + $this->assertSame($expected, $result->all()); + $this->assertSame([], $request->attributes->all()); + } + + /** + * @covers ::filter + * @covers ::getVariantRouteName + */ + public function testFilterNonMatchingRoute() { + $route_collection = new RouteCollection(); + $request = new Request(); + + $route = new Route('/path/with/{slug}'); + $route_collection->add('a_route', $route); + + $this->currentPath->getPath($request)->willReturn(''); + + $result = $this->routeFilter->filter($route_collection, $request); + $expected = ['a_route' => $route]; + $this->assertSame($expected, $result->all()); + $this->assertSame([], $request->attributes->all()); + } + + /** + * @covers ::filter + * @covers ::getVariantRouteName + * @covers ::checkPageVariantAccess + */ + public function testFilterDeniedAccess() { + $route_collection = new RouteCollection(); + $request = new Request(); + + $route = new Route('/path/with/{slug}', ['page_manager_page_variant' => 'a_variant']); + $route_collection->add('a_route', $route); + + $page_variant = $this->prophesize(PageVariantInterface::class); + $page_variant->access('view')->willReturn(FALSE); + + $this->currentPath->getPath($request)->willReturn(''); + $this->pageVariantStorage->load('a_variant')->willReturn($page_variant->reveal()); + + $result = $this->routeFilter->filter($route_collection, $request); + $expected = []; + $this->assertSame($expected, $result->all()); + $this->assertSame([], $request->attributes->all()); + } + + /** + * @covers ::filter + * @covers ::getVariantRouteName + * @covers ::checkPageVariantAccess + */ + public function testFilterAllowedAccess() { + $route_collection = new RouteCollection(); + $request = new Request(); + + $route = new Route('/path/with/{slug}', ['page_manager_page_variant' => 'a_variant']); + $route_collection->add('a_route', $route); + + $page_variant = $this->prophesize(PageVariantInterface::class); + $page_variant->access('view')->willReturn(TRUE); + + $this->currentPath->getPath($request)->willReturn(''); + $this->pageVariantStorage->load('a_variant')->willReturn($page_variant->reveal()); + + $result = $this->routeFilter->filter($route_collection, $request); + $expected = ['a_route' => $route]; + $this->assertSame($expected, $result->all()); + $expected_attributes = [ + 'page_manager_page_variant' => 'a_variant', + '_route_object' => $route, + '_route' => 'a_route', + ]; + $this->assertSame($expected_attributes, $request->attributes->all()); + } + + /** + * @covers ::filter + * @covers ::getVariantRouteName + */ + public function testFilterAllowedAccessTwoRoutes() { + $route_collection = new RouteCollection(); + $request = new Request(); + + $route1 = new Route('/path/with/{slug}', ['page_manager_page_variant' => 'variant_1', 'page_manager_page_variant_weight' => 0]); + $route2 = new Route('/path/with/{slug}', ['page_manager_page_variant' => 'variant_2', 'page_manager_page_variant_weight' => 2]); + // Add route2 first to ensure that the routes get sorted by weight. + $route_collection->add('route_2', $route2); + $route_collection->add('route_1', $route1); + + $page_variant = $this->prophesize(PageVariantInterface::class); + $page_variant->access('view')->willReturn(TRUE); + + $this->currentPath->getPath($request)->willReturn(''); + $this->pageVariantStorage->load('variant_1')->willReturn($page_variant->reveal()); + $this->pageVariantStorage->load('variant_2')->shouldNotBeCalled(); + + $result = $this->routeFilter->filter($route_collection, $request); + $expected = ['route_1' => $route1]; + $this->assertSame($expected, $result->all()); + $expected_attributes = [ + 'page_manager_page_variant' => 'variant_1', + 'page_manager_page_variant_weight' => 0, + '_route_object' => $route1, + '_route' => 'route_1', + ]; + $this->assertSame($expected_attributes, $request->attributes->all()); + } + + /** + * @covers ::filter + * @covers ::getVariantRouteName + */ + public function testFilterAllowedAccessSecondRoute() { + $route_collection = new RouteCollection(); + $request = new Request(); + + $route1 = new Route('/path/with/{slug}', ['page_manager_page_variant' => 'variant_1', 'page_manager_page_variant_weight' => 1]); + $defaults = [ + 'page_manager_page_variant' => 'variant_2', + 'page_manager_page_variant_weight' => 2, + 'overridden_route_name' => 'overridden_route_name_for_selected_route', + ]; + $route2 = new Route('/path/with/{slug}', $defaults); + // Add route2 first to ensure that the routes get sorted by weight. + $route_collection->add('route_2', $route2); + $route_collection->add('route_1', $route1); + + $page_variant1 = $this->prophesize(PageVariantInterface::class); + $page_variant1->access('view')->willReturn(FALSE); + $page_variant2 = $this->prophesize(PageVariantInterface::class); + $page_variant2->access('view')->willReturn(TRUE); + + $this->currentPath->getPath($request)->willReturn(''); + $this->pageVariantStorage->load('variant_1')->willReturn($page_variant1->reveal())->shouldBeCalled(); + $this->pageVariantStorage->load('variant_2')->willReturn($page_variant2->reveal())->shouldBeCalled(); + + $result = $this->routeFilter->filter($route_collection, $request); + $expected = ['overridden_route_name_for_selected_route' => $route2]; + $this->assertSame($expected, $result->all()); + $expected_attributes = $defaults + [ + '_route_object' => $route2, + '_route' => 'overridden_route_name_for_selected_route', + ]; + $this->assertSame($expected_attributes, $request->attributes->all()); + } + + /** + * @covers ::filter + * @covers ::getVariantRouteName + * @covers ::sortRoutes + * + * Tests when the first page_manager route is allowed, but other + * non-page_manager routes are also present. + */ + public function testFilterAllowedAccessFirstRoute() { + $route_collection = new RouteCollection(); + $request = new Request(); + + // The selected route specifies a different base route. + $defaults = [ + 'page_manager_page_variant' => 'variant1', + 'page_manager_page_variant_weight' => -2, + 'overridden_route_name' => 'route_1', + ]; + $route1 = new Route('/path/with/{slug}'); + $route2 = new Route('/path/with/{slug}', $defaults); + $route3 = new Route('/path/with/{slug}', ['page_manager_page_variant' => 'variant2', 'page_manager_page_variant_weight' => -1]); + $route4 = new Route('/path/with/{slug}'); + // Add routes in different order to test sorting. + $route_collection->add('route_3', $route3); + $route_collection->add('route_2', $route2); + $route_collection->add('route_1', $route1); + $route_collection->add('route_4', $route4); + + $page_variant1 = $this->prophesize(PageVariantInterface::class); + $page_variant1->access('view')->willReturn(TRUE); + $page_variant2 = $this->prophesize(PageVariantInterface::class); + $page_variant2->access('view')->willReturn(FALSE); + + $this->currentPath->getPath($request)->willReturn(''); + $this->pageVariantStorage->load('variant1')->willReturn($page_variant1->reveal())->shouldBeCalled(); + + $result = $this->routeFilter->filter($route_collection, $request); + $expected = ['route_1' => $route2, 'route_4' => $route4]; + $this->assertSame($expected, $result->all()); + $expected_attributes = $defaults + [ + '_route_object' => $route2, + '_route' => 'route_1', + ]; + $this->assertSame($expected_attributes, $request->attributes->all()); + } + + /** + * @covers ::filter + * @covers ::getVariantRouteName + * @covers ::getRequestAttributes + */ + public function testFilterRequestAttributes() { + $route_collection = new RouteCollection(); + $request = new Request([], [], ['foo' => 'bar', 'slug' => 2]); + + $route = new Route('/path/with/{slug}', ['page_manager_page_variant' => 'a_variant']); + $route_collection->add('a_route', $route); + + $page_variant = $this->prophesize(PageVariantInterface::class); + $page_variant->access('view')->willReturn(TRUE); + + $this->currentPath->getPath($request)->willReturn('/path/with/1'); + $this->pageVariantStorage->load('a_variant')->willReturn($page_variant->reveal()); + + $route_enhancer = $this->prophesize(RouteEnhancerInterface::class); + $this->routeFilter->addRouteEnhancer($route_enhancer->reveal()); + $result_enhance_attributes = $expected_enhance_attributes = [ + 'foo' => 'bar', + 'slug' => '1', + 'page_manager_page_variant' => 'a_variant', + '_route_object' => $route, + '_route' => 'a_route', + ]; + $result_enhance_attributes['slug'] = 'slug 1'; + $route_enhancer->enhance($expected_enhance_attributes, $request)->willReturn($result_enhance_attributes); + + $result = $this->routeFilter->filter($route_collection, $request); + $expected = ['a_route' => $route]; + $this->assertSame($expected, $result->all()); + $expected_attributes = [ + 'foo' => 'bar', + 'slug' => 'slug 1', + 'page_manager_page_variant' => 'a_variant', + '_route_object' => $route, + '_route' => 'a_route', + ]; + $this->assertSame($expected_attributes, $request->attributes->all()); + } + + /** + * @covers ::filter + * @covers ::getVariantRouteName + * @covers ::getRequestAttributes + */ + public function testFilterRequestAttributesException() { + $route_collection = new RouteCollection(); + $original_attributes = ['foo' => 'bar', 'slug' => 2]; + $request = new Request([], [], $original_attributes); + + $route = new Route('/path/with/{slug}', ['page_manager_page_variant' => 'a_variant']); + $route_collection->add('a_route', $route); + + $page_variant = $this->prophesize(PageVariantInterface::class); + $page_variant->access('view')->willReturn(TRUE); + + $this->currentPath->getPath($request)->willReturn('/path/with/1'); + $this->pageVariantStorage->load('a_variant')->willReturn($page_variant->reveal()); + + $route_enhancer = $this->prophesize(RouteEnhancerInterface::class); + $this->routeFilter->addRouteEnhancer($route_enhancer->reveal()); + $expected_enhance_attributes = [ + 'foo' => 'bar', + 'slug' => '1', + 'page_manager_page_variant' => 'a_variant', + '_route_object' => $route, + '_route' => 'a_route', + ]; + $route_enhancer->enhance($expected_enhance_attributes, $request)->willThrow(new \Exception('A route enhancer failed')); + + $result = $this->routeFilter->filter($route_collection, $request); + $this->assertEmpty($result->all()); + $this->assertSame($original_attributes, $request->attributes->all()); + } + + /** + * @covers ::filter + * @covers ::sortRoutes + */ + public function testFilterPreservingBaseRouteName() { + $route_collection = new RouteCollection(); + $request = new Request(); + + // Add routes in different order to also test order preserving. + $route1 = new Route('/path/with/{slug}', ['page_manager_page_variant' => 'variant1', 'page_manager_page_variant_weight' => -10, 'overridden_route_name' => 'preserved_route_name']); + $route2 = new Route('/path/with/{slug}', ['page_manager_page_variant' => 'variant2', 'page_manager_page_variant_weight' => -5]); + $route3 = new Route('/path/with/{slug}', []); + $route4 = new Route('/path/with/{slug}', []); + $route_collection->add('route_4', $route4); + $route_collection->add('route_3', $route3); + $route_collection->add('route_1', $route1); + $route_collection->add('route_2', $route2); + + $page_variant1 = $this->prophesize(PageVariantInterface::class); + $page_variant1->access('view')->willReturn(TRUE); + $page_variant2 = $this->prophesize(PageVariantInterface::class); + $page_variant2->access('view')->willReturn(FALSE); + + $this->currentPath->getPath($request)->willReturn(''); + $this->pageVariantStorage->load('variant1')->willReturn($page_variant1->reveal())->shouldBeCalled(); + $this->pageVariantStorage->load('variant2')->shouldNotBeCalled(); + + $result = $this->routeFilter->filter($route_collection, $request); + + $expected = ['preserved_route_name' => $route1, 'route_4' => $route4, 'route_3' => $route3]; + $this->assertSame($expected, $result->all()); + } + + /** + * @covers ::getRequestAttributes + */ + public function testGetRequestAttributes() { + $request = new Request(); + + $route = new Route('/path/with/{slug}'); + $route_name = 'a_route'; + + $this->currentPath->getPath($request)->willReturn('/path/with/1'); + + $expected_attributes = ['slug' => 1, '_route_object' => $route, '_route' => $route_name]; + $route_enhancer = $this->prophesize(RouteEnhancerInterface::class); + $route_enhancer->enhance($expected_attributes, $request)->willReturn(['slug' => 'slug 1']); + $this->routeFilter->addRouteEnhancer($route_enhancer->reveal()); + + $this->assertSame([], $request->attributes->all()); + + $method = new \ReflectionMethod($this->routeFilter, 'getRequestAttributes'); + $method->setAccessible(TRUE); + $attributes = $method->invoke($this->routeFilter, $route, $route_name, $request); + + $this->assertSame(['slug' => 'slug 1'], $attributes); + } + +} diff --git a/docroot/modules/contrib/panelizer/.codeclimate.yml b/docroot/modules/contrib/panelizer/.codeclimate.yml new file mode 100644 index 000000000..e8b0bd077 --- /dev/null +++ b/docroot/modules/contrib/panelizer/.codeclimate.yml @@ -0,0 +1,25 @@ +--- +engines: + csslint: + enabled: true + duplication: + enabled: true + config: + languages: + - javascript + - php + eslint: + enabled: true + fixme: + enabled: true + phpmd: + enabled: true +ratings: + paths: + - "**.css" + - "**.inc" + - "**.install" + - "**.js" + - "**.module" + - "**.php" + - "**.test" diff --git a/docroot/modules/contrib/panelizer/.csslintrc b/docroot/modules/contrib/panelizer/.csslintrc new file mode 100644 index 000000000..aacba956e --- /dev/null +++ b/docroot/modules/contrib/panelizer/.csslintrc @@ -0,0 +1,2 @@ +--exclude-exts=.min.css +--ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes diff --git a/docroot/modules/contrib/panelizer/.eslintignore b/docroot/modules/contrib/panelizer/.eslintignore new file mode 100644 index 000000000..96212a359 --- /dev/null +++ b/docroot/modules/contrib/panelizer/.eslintignore @@ -0,0 +1 @@ +**/*{.,-}min.js diff --git a/docroot/modules/contrib/panelizer/.eslintrc b/docroot/modules/contrib/panelizer/.eslintrc new file mode 100644 index 000000000..9faa37508 --- /dev/null +++ b/docroot/modules/contrib/panelizer/.eslintrc @@ -0,0 +1,213 @@ +ecmaFeatures: + modules: true + jsx: true + +env: + amd: true + browser: true + es6: true + jquery: true + node: true + +# http://eslint.org/docs/rules/ +rules: + # Possible Errors + comma-dangle: [2, never] + no-cond-assign: 2 + no-console: 0 + no-constant-condition: 2 + no-control-regex: 2 + no-debugger: 2 + no-dupe-args: 2 + no-dupe-keys: 2 + no-duplicate-case: 2 + no-empty: 2 + no-empty-character-class: 2 + no-ex-assign: 2 + no-extra-boolean-cast: 2 + no-extra-parens: 0 + no-extra-semi: 2 + no-func-assign: 2 + no-inner-declarations: [2, functions] + no-invalid-regexp: 2 + no-irregular-whitespace: 2 + no-negated-in-lhs: 2 + no-obj-calls: 2 + no-regex-spaces: 2 + no-sparse-arrays: 2 + no-unexpected-multiline: 2 + no-unreachable: 2 + use-isnan: 2 + valid-jsdoc: 0 + valid-typeof: 2 + + # Best Practices + accessor-pairs: 2 + block-scoped-var: 0 + complexity: [2, 6] + consistent-return: 0 + curly: 0 + default-case: 0 + dot-location: 0 + dot-notation: 0 + eqeqeq: 2 + guard-for-in: 2 + no-alert: 2 + no-caller: 2 + no-case-declarations: 2 + no-div-regex: 2 + no-else-return: 0 + no-empty-label: 2 + no-empty-pattern: 2 + no-eq-null: 2 + no-eval: 2 + no-extend-native: 2 + no-extra-bind: 2 + no-fallthrough: 2 + no-floating-decimal: 0 + no-implicit-coercion: 0 + no-implied-eval: 2 + no-invalid-this: 0 + no-iterator: 2 + no-labels: 0 + no-lone-blocks: 2 + no-loop-func: 2 + no-magic-number: 0 + no-multi-spaces: 0 + no-multi-str: 0 + no-native-reassign: 2 + no-new-func: 2 + no-new-wrappers: 2 + no-new: 2 + no-octal-escape: 2 + no-octal: 2 + no-proto: 2 + no-redeclare: 2 + no-return-assign: 2 + no-script-url: 2 + no-self-compare: 2 + no-sequences: 0 + no-throw-literal: 0 + no-unused-expressions: 2 + no-useless-call: 2 + no-useless-concat: 2 + no-void: 2 + no-warning-comments: 0 + no-with: 2 + radix: 2 + vars-on-top: 0 + wrap-iife: 2 + yoda: 0 + + # Strict + strict: 0 + + # Variables + init-declarations: 0 + no-catch-shadow: 2 + no-delete-var: 2 + no-label-var: 2 + no-shadow-restricted-names: 2 + no-shadow: 0 + no-undef-init: 2 + no-undef: 0 + no-undefined: 0 + no-unused-vars: 0 + no-use-before-define: 0 + + # Node.js and CommonJS + callback-return: 2 + global-require: 2 + handle-callback-err: 2 + no-mixed-requires: 0 + no-new-require: 0 + no-path-concat: 2 + no-process-exit: 2 + no-restricted-modules: 0 + no-sync: 0 + + # Stylistic Issues + array-bracket-spacing: 0 + block-spacing: 0 + brace-style: 0 + camelcase: 0 + comma-spacing: 0 + comma-style: 0 + computed-property-spacing: 0 + consistent-this: 0 + eol-last: 0 + func-names: 0 + func-style: 0 + id-length: 0 + id-match: 0 + indent: 0 + jsx-quotes: 0 + key-spacing: 0 + linebreak-style: 0 + lines-around-comment: 0 + max-depth: 0 + max-len: 0 + max-nested-callbacks: 0 + max-params: 0 + max-statements: [2, 30] + new-cap: 0 + new-parens: 0 + newline-after-var: 0 + no-array-constructor: 0 + no-bitwise: 0 + no-continue: 0 + no-inline-comments: 0 + no-lonely-if: 0 + no-mixed-spaces-and-tabs: 0 + no-multiple-empty-lines: 0 + no-negated-condition: 0 + no-nested-ternary: 0 + no-new-object: 0 + no-plusplus: 0 + no-restricted-syntax: 0 + no-spaced-func: 0 + no-ternary: 0 + no-trailing-spaces: 0 + no-underscore-dangle: 0 + no-unneeded-ternary: 0 + object-curly-spacing: 0 + one-var: 0 + operator-assignment: 0 + operator-linebreak: 0 + padded-blocks: 0 + quote-props: 0 + quotes: 0 + require-jsdoc: 0 + semi-spacing: 0 + semi: 0 + sort-vars: 0 + space-after-keywords: 0 + space-before-blocks: 0 + space-before-function-paren: 0 + space-before-keywords: 0 + space-in-parens: 0 + space-infix-ops: 0 + space-return-throw-case: 0 + space-unary-ops: 0 + spaced-comment: 0 + wrap-regex: 0 + + # ECMAScript 6 + arrow-body-style: 0 + arrow-parens: 0 + arrow-spacing: 0 + constructor-super: 0 + generator-star-spacing: 0 + no-arrow-condition: 0 + no-class-assign: 0 + no-const-assign: 0 + no-dupe-class-members: 0 + no-this-before-super: 0 + no-var: 0 + object-shorthand: 0 + prefer-arrow-callback: 0 + prefer-const: 0 + prefer-reflect: 0 + prefer-spread: 0 + prefer-template: 0 + require-yield: 0 diff --git a/docroot/modules/contrib/panelizer/CHANGELOG.txt b/docroot/modules/contrib/panelizer/CHANGELOG.txt new file mode 100644 index 000000000..29cd3a9a2 --- /dev/null +++ b/docroot/modules/contrib/panelizer/CHANGELOG.txt @@ -0,0 +1,99 @@ +Panelizer 8.x-4.x-dev, xxxx-xx-xx +--------------------------------- +#2867568 by japerry: Update hook to fix layout machine names. +#2866705 by japerry: Add handling for revision support when saving custom + panelizer layouts. +#2856471 by phenaproxima, samuel.mortenson, japerry: IPE on Panelizer nodes + should only allow for custom overrides. +#2867503 by DamienMcKenna, phenaproxima: Change tests to not use the "standard" + installation profile, move PanelizerAddDefaultLinkTest to functional tests. +#2699085 by tim.plunkett: Lock PanelsIPE when a user has made TempStore changes. +#2867819 by DamienMcKenna: Cloned NodeFunctionalTests to focus on translations. +By japerry: Make it clear to the site builder that they can select defaults by + adding the panelizer field. +By DamienMcKenna: Noting some translation todos. +#2870719 by DamienMcKenna: Moved all tests to the 'panelizer' test group. +By DamienMcKenna: Identify hook_post_update_NAME(). + + +Panelizer 8.x-4.0-beta2, 2017-04-04 +----------------------------------- +By DamienMcKenna: Tidied up the dependencies a little. +By DamienMcKenna: Trying to solve composer dependencies in Panels by + specifically requiring Panels IPE 4.x. +#2858488 by tim.plunkett: Moved PanelizerIntegrationTest from Panels to + Panelizer. +#2866903 by japerry, EclipseGc: Updated composer.json for compatibility with + core 8.3. + + +Panelizer 8.x-4.0-beta1, 2017-03-04 +----------------------------------- +#2856102 by japerry, DamienMcKenna, tim.plunkett, Mixologic: Updated to work + with Panels 8.x-4.x and Drupal core 8.3.x. + + +Panelizer 8.x-3.x-dev, xxxx-xx-xx +--------------------------------- +#2850246 by DamienMcKenna: Added a new permission to control who has access to + the 'reset to default' option, added some tests for IPE functionality. +#2852739 by DamienMcKenna: Added files for controlling the codeclimate testing + system. +#2854655 by DamienMcKenna: Added a custom composer.json file to clearly define + the dependencies. +By DamienMcKenna: Grouped theme functions together in panelizer.module. + + +Panelizer 8.x-3.0-beta1, 2017-02-04 +----------------------------------- +#2828840 by hctom, DamienMcKenna: Handle entity types that use + RevisionableInterface but are not revisionable. +#2664574 by artreaktor, phenaproxima, hctom, jiff, floretan: Add support for + Taxonomy Term entities. +#2835590 by DamienMcKenna: Hide the custom Panelizer field, it's no longer used. +#2800991 by garethhallnz: Allow other modules to change the view_mode being + rendered. +#2847351 by porchlight, rvillan, DamienMcKenna: Permission to 'Set as Default' + in the Panelizer IPE. +#2727629 by samuel.mortenson, Dane Powell, dsnopek: Panelized nodes sporadically + become locked. +#2664572 by hctom, balsama, smurrayatwork, DamienMcKenna: Support for user + entities. +By DamienMcKenna: Does not support comment entities. +#2835587 by DamienMcKenna: Minor UX tweaks, code improvements. + + +Panelizer 8.x-3.0-alpha3, 2016-12-15 +------------------------------------ +#2664648: Added unit tests for PanelizerDefaultPanelsStorage and + PanelizerFieldPanelsStorage. +Reverted #2664616. +#2688951 by swentel, hampercm, wiifm: Panels IPE javascript loaded, even when + you have no access to the in place editor. +#2760051 by Gravypower: Undefined index: #attached. +#2795375 by webflo: Theme suggestions are in the wrong order. +#2820562 by jiff: Node functional test fails due to non-existent permission. +#2716071 by Xano: PanelizerFieldPanelsStorage::access() calls entity access with + incorrect operations and excludes cacheability metadata. +#2693163 by samuel.mortenson: Quick Edit support for fields displayed using the + ctools_field block. +#2700597 by samuel.mortenson, mdooley: Explicitly set the Panels IPE URL root + when saving in Panelizer. +#2664682 by juampynr, EclipseGc, phenaproxima, balsama, japerry, Dane Powell, + samuel.mortenson, hctom: Implement admin UI for editing Panelizer defaults. +#2701349 by EclipseGc: Generate new UUIDs for displays when switching from + default to field storage. + + +Panelizer 8.x-3.0-alpha2, 2016-02-16 +------------------------------------ +- Add basic functional tests. +- Add permission for changing layout. +- "Undefined Index: label in Drupal\panelizer\Panelizer->getPermissions()." +- Contextual blocks not shown in the IPE. +- PanelizerFieldType::generateSampleValue() needs to specify a view mode. + + +Panelizer 8.x-3.0-alpha1, 2016-02-05 +------------------------------------ +Initial D8 port by dsnopek and the Panels contributors. diff --git a/docroot/modules/contrib/panelizer/LICENSE.txt b/docroot/modules/contrib/panelizer/LICENSE.txt new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/docroot/modules/contrib/panelizer/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. + + + Copyright (C) + + 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. + + , 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/docroot/modules/contrib/panelizer/README.txt b/docroot/modules/contrib/panelizer/README.txt new file mode 100644 index 000000000..d00f41d5c --- /dev/null +++ b/docroot/modules/contrib/panelizer/README.txt @@ -0,0 +1,40 @@ +Panelizer +--------- +The Panelizer module allows supported entities to be treated as Panels [1], +giving options for different default displays on a per bundle basis. For +example, this allows each node display to be customized individually. + + +Features +-------------------------------------------------------------------------------- +* Supports Drupal core's included entities - nodes, taxonomy terms and users. + +* Can be easily extended to support additional entities. + + +Requirements +-------------------------------------------------------------------------------- +CTools v8.x-3.x [2] +Panels v8.x-4.x + +This version is only designed to be compatible with Drupal 8.3.x and above, it +is not compatible with 8.2.x and the Layout Plugin module. + + +Credits / Contact +-------------------------------------------------------------------------------- +Drupal 8 port by David Snopek [3] and the Panels contributors. Originally +written by merlinofchaos [4]. Additional maintenance by Damien McKenna [5] + +The best way to contact the authors is to submit an issue, be it a support +request, a feature request or a bug report, in the project's issue queue: + https://www.drupal.org/project/issues/panelizer + + +References +-------------------------------------------------------------------------------- +1: https://www.drupal.org/project/panels +2: https://www.drupal.org/project/ctools +3: https://www.drupal.org/u/dsnopek +4: https://www.drupal.org/u/merlinofchaos +5: https://www.drupal.org/u/damienmckenna diff --git a/docroot/modules/contrib/panelizer/composer.json b/docroot/modules/contrib/panelizer/composer.json new file mode 100644 index 000000000..4ace76948 --- /dev/null +++ b/docroot/modules/contrib/panelizer/composer.json @@ -0,0 +1,44 @@ +{ + "name": "drupal/panelizer", + "description": "Allow any entity view mode to be rendered using a Panels display.", + "type": "drupal-module", + "homepage": "https://www.drupal.org/project/panelizer", + "authors": [ + { + "name": "Damien McKenna", + "homepage": "https://www.drupal.org/u/damienmckenna" + }, + { + "name": "Kris Vanderwater", + "homepage": "https://www.drupal.org/u/eclipsegc" + }, + { + "name": "David Snopek", + "homepage": "https://www.drupal.org/u/dsnopek" + }, + { + "name": "Jakob Perry", + "homepage": "https://www.drupal.org/u/japerry" + }, + { + "name": "Earl Miles", + "homepage": "https://www.drupal.org/u/merlinofchaos" + }, + { + "name": "See other contributors", + "homepage":"https://www.drupal.org/node/1072922/committers" + } + ], + "support": { + "issues": "https://www.drupal.org/project/issues/panelizer", + "irc": "irc://irc.freenode.org/drupal-scotch", + "source": "http://git.drupal.org/project/panelizer.git" + }, + "license": "GPL-2.0+", + "minimum-stability": "dev", + "require": { + "drupal/ctools": ">=3.0.0-beta1", + "drupal/panels": ">=4.0.0-alpha1", + "drupal/panels_ipe": ">=4.0.0-alpha1" + } +} diff --git a/docroot/modules/contrib/panelizer/config/schema/panelizer.schema.yml b/docroot/modules/contrib/panelizer/config/schema/panelizer.schema.yml new file mode 100644 index 000000000..8b873a96e --- /dev/null +++ b/docroot/modules/contrib/panelizer/config/schema/panelizer.schema.yml @@ -0,0 +1,33 @@ +# Config schema for Panelizer. +panelizer.default.display: + type: display_variant.plugin.panels_variant + label: 'Panelizer display default' + mapping: + static_context: + type: ctools.context + label: 'Contexts' + pattern: + type: string + label: 'Pattern plugin ID' + +core.entity_view_display.*.*.*.third_party.panelizer: + type: mapping + mapping: + enable: + type: boolean + description: 'Is Panelizer enabled?' + custom: + type: boolean + description: 'Are custom overrides allowed?' + allow: + type: boolean + description: 'Is choosing between available defaults during entity creation allowed?' + default: + type: string + description: 'Default display ID' + displays: + type: sequence + description: 'Default displays' + sequence: + type: panelizer.default.display + description: 'Default displays' diff --git a/docroot/modules/contrib/panelizer/css/panelizer.admin.css b/docroot/modules/contrib/panelizer/css/panelizer.admin.css new file mode 100644 index 000000000..1aa379464 --- /dev/null +++ b/docroot/modules/contrib/panelizer/css/panelizer.admin.css @@ -0,0 +1,113 @@ +/** + * @file + * Styles for Panelizer wizard admin. + */ + +/* Narrow screens */ + +.panelizer-wizard-tree, +.panelizer-wizard-form { + box-sizing: border-box; +} + +/** + * Wizard actions across the top. + */ +.panelizer-wizard-actions { + text-align: right; /* LTR */ +} +.panelizer-wizard-actions ul.inline, +.panelizer-wizard-actions ul.inline li { + display: inline-block; + margin: 0; +} +.panelizer-wizard-actions ul.inline { + border-top: 1px solid black; + border-left: 1px solid black; +} +.panelizer-wizard-actions ul.inline li { + border-right: 1px solid black; + padding: .5em; +} + +/** + * The tree of wizard steps. + */ +.panelizer-wizard-tree ul { + margin: 0; + padding: 0; + list-style: none; +} +.panelizer-wizard-tree ul > li > ul { + margin-left: 1em; +} +.panelizer-wizard-tree > ul { + border: 1px solid black; + padding-bottom: .5em; + margin-bottom: 20px; +} +.panelizer-wizard-tree li { + border-bottom: 1px solid black; + padding: .5em; + padding-right: 0; +} +.panelizer-wizard-tree li:last-child { + border-bottom: 0; + padding-bottom: 0; +} + +/** + * The wizard form. + */ +.panelizer-wizard-form { + border: 1px solid black; + padding: 1em; + margin-bottom: 20px; +} + +/* Wide screens */ +@media + screen and (min-width: 780px), + (orientation: landscape) and (min-device-height: 780px) { + + /** + * Overall layout. + */ + .panelizer-wizard-tree { + float: left; /* LTR */ + width: 20%; + } + .panelizer-wizard-form { + float: left; /* LTR */ + width: 80%; + } + .panelizer-wizard-form-actions { + margin-left: 20%; /* LTR */ + } + + /** + * Make the borders look nice. + */ + .panelizer-wizard-tree > ul { + border-right: 0; /* LTR */ + } + .panelizer-wizard-form { + min-height: 400px; + } + + /** + * Right-to-left support. + */ + [dir="rtl"] .panelizer-wizard-tree, + [dir="rtl"] .panelizer-wizard-form { + float: right; + } + [dir="rtl"] .panelizer-wizard-form-actions { + margin-left: 0; + margin-right: 20%; + } + [dir="rtl"] .panelizer-wizard-tree > ul { + border-right: 1px solid black; + border-left: 0; + } +} diff --git a/docroot/modules/contrib/panelizer/css/panels_ipe.css b/docroot/modules/contrib/panelizer/css/panels_ipe.css new file mode 100644 index 000000000..c5e26d2c9 --- /dev/null +++ b/docroot/modules/contrib/panelizer/css/panels_ipe.css @@ -0,0 +1,20 @@ +/** + * @file + * Stylesheet for Panelizer IPE customizations. + */ + +.panelizer-ipe-save-button { + display: inline-block; + margin: 10px; +} + +.ipe-icon.ipe-icon-revert:before { + /* @todo: Make a new icon for revert! */ + content: "\e90a"; +} + +/* Show the cancel title so it easier to tell the difference from revert. */ +[data-tab-id="cancel"] .ipe-tab-title { + display: inline; +} + diff --git a/docroot/modules/contrib/panelizer/js/panelizer-default-form.js b/docroot/modules/contrib/panelizer/js/panelizer-default-form.js new file mode 100644 index 000000000..76aafe84a --- /dev/null +++ b/docroot/modules/contrib/panelizer/js/panelizer-default-form.js @@ -0,0 +1,24 @@ +/** + * @file + * Javascript for the Panelizer defaults page. + */ +(function ($) { + Drupal.behaviors.panelizer_default_form = { + attach: function (context, settings) { + var $panelizer_checkbox = $(':input[name="panelizer[enable]"]'); + + function update_form() { + var $core_form = $('#field-display-overview-wrapper'); + if ($panelizer_checkbox.is(':checked')) { + $core_form.fadeOut(); + } + else { + $core_form.fadeIn(); + } + } + + $panelizer_checkbox.once('panelizer-default-form').click(update_form); + update_form(); + } + }; +})(jQuery); diff --git a/docroot/modules/contrib/panelizer/js/panels_ipe/panels_ipe.js b/docroot/modules/contrib/panelizer/js/panels_ipe/panels_ipe.js new file mode 100644 index 000000000..36af7740c --- /dev/null +++ b/docroot/modules/contrib/panelizer/js/panels_ipe/panels_ipe.js @@ -0,0 +1,72 @@ +/** + * @file + * Entry point for the Panelizer IPE customizations. + */ + +(function ($, _, Backbone, Drupal) { + + 'use strict'; + + Drupal.panelizer = Drupal.panelizer || {}; + + /** + * @namespace + */ + Drupal.panelizer.panels_ipe = {}; + + /** + * Make customizations to the Panels IPE for Panelizer. + */ + Backbone.on('PanelsIPEInitialized', function() { + // Disable the normal save event. + Drupal.panels_ipe.app_view.stopListening(Drupal.panels_ipe.app.get('saveTab'), 'change:active'); + + // Add a new revert tab model. + if (drupalSettings.panelizer.user_permission.revert) { + var revert_tab = new Drupal.panels_ipe.TabModel({title: 'Revert to default', id: 'revert'}); + Drupal.panels_ipe.app_view.tabsView.collection.add(revert_tab); + + // @todo: Put this into a proper view? + Drupal.panels_ipe.app_view.listenTo(revert_tab, 'change:active', function () { + var entity = drupalSettings.panelizer.entity; + if (revert_tab.get('active') && !revert_tab.get('loading')) { + if (confirm(Drupal.t('Are you sure you want to revert to default layout? All your layout changes will be lost for this node.'))) { + // Remove our changes and refresh the page. + revert_tab.set({loading: true}); + $.ajax({ + url: drupalSettings.path.baseUrl + 'admin/panelizer/panels_ipe/' + entity.entity_type_id + '/' + entity.entity_id + '/' + entity.view_mode + '/revert_to_default', + data: {}, + type: 'POST' + }).done(function (data) { + location.reload(); + }); + } + else { + revert_tab.set('active', false, {silent: true}); + } + } + }); + + // Hide the 'Revert to default' button if we're already on a default. + if (drupalSettings.panels_ipe.panels_display.storage_type == 'panelizer_default') { + revert_tab.set({hidden: true}); + } + } + + // Hide the 'Revert to default' button if the user does not have permission. + // if (!drupalSettings.panelizer.user_permission.revert) { + // revert_tab.set({hidden: true}); + // } + + // Add a new view for the save button to the TabsView. + var tabs = { + model: Drupal.panels_ipe.app_view.model, + tabsView: Drupal.panels_ipe.app_view.tabsView, + }; + if (typeof revert_tab !== 'undefined') { + tabs.revertTab = revert_tab; + } + Drupal.panels_ipe.app_view.tabsView.tabViews['save'] = new Drupal.panelizer.panels_ipe.SaveTabView(tabs); + }); + +}(jQuery, _, Backbone, Drupal)); diff --git a/docroot/modules/contrib/panelizer/js/panels_ipe/views/SaveTabView.js b/docroot/modules/contrib/panelizer/js/panels_ipe/views/SaveTabView.js new file mode 100644 index 000000000..8ddbab364 --- /dev/null +++ b/docroot/modules/contrib/panelizer/js/panels_ipe/views/SaveTabView.js @@ -0,0 +1,122 @@ +/** + * @file + * Contains Drupal.panelizer.panels_ipe.SaveTabView. + */ + +(function ($, _, Backbone, Drupal) { + 'use strict'; + + Drupal.panelizer.panels_ipe.SaveTabView = Backbone.View.extend(/** @lends Drupal.panelizer.panels_ipe.SaveTabView# */{ + + /** + * @type {function} + */ + template: function() { + return ''; + }, + + /** + * @type {Drupal.panels_ipe.AppModel} + */ + model: null, + + /** + * @type {Drupal.panels_ipe.TabsView} + */ + tabsView: null, + + /** + * @type {Drupal.panels_ipe.TabModel} + */ + revertTab: null, + + /** + * @type {object} + */ + events: { + }, + + /** + * @type {function} + */ + onClick: function () { + if (this.model.get('saveTab').get('active')) { + // Always save as a custom override. + this.saveCustom(); + } + }, + + /** + * @type {function} + */ + saveCustom: function () { + this._save('panelizer_field'); + }, + + /** + * @type {function} + */ + _save: function (storage_type) { + var self = this, + layout = this.model.get('layout'); + + // Give the backend enough information to save in the correct way. + layout.set('panelizer_save_as', storage_type); + layout.set('panelizer_entity', drupalSettings.panelizer.entity); + + if (this.model.get('saveTab').get('active')) { + // Save the Layout and disable the tab. + this.model.get('saveTab').set({loading: true, active: false}); + this.tabsView.render(); + layout.save().done(function () { + self.model.get('saveTab').set({loading: false}); + self.model.set('unsaved', false); + + // Change the storage type and id for the next save. + drupalSettings.panels_ipe.panels_display.storage_type = storage_type; + drupalSettings.panels_ipe.panels_display.storage_id = drupalSettings.panelizer.entity[storage_type + '_storage_id']; + Drupal.panels_ipe.setUrlRoot(drupalSettings); + + // Show/hide the revert to default tab. + self.revertTab.set({hidden: storage_type === 'panelizer_default'}); + self.tabsView.render(); + }); + } + }, + + /** + * @constructs + * + * @augments Backbone.View + * + * @param {Object} options + * An object containing the following keys: + * @param {Drupal.panels_ipe.AppModel} options.model + * The app state model. + * @param {Drupal.panels_ipe.TabsView} options.tabsView + * The app view. + * @param {Drupal.panels_ipe.TabModel} options.revertTab + * The revert tab. + */ + initialize: function (options) { + this.model = options.model; + this.tabsView = options.tabsView; + this.revertTab = options.revertTab; + + this.listenTo(this.model.get('saveTab'), 'change:active', this.onClick); + }, + + /** + * Renders the selection menu for picking Layouts. + * + * @return {Drupal.panelizer.panels_ipe.SaveTabView} + * Return this, for chaining. + */ + render: function () { + this.$el.html(this.template()); + return this; + } + + }); + +}(jQuery, _, Backbone, Drupal)); diff --git a/docroot/modules/contrib/panelizer/panelizer.api.php b/docroot/modules/contrib/panelizer/panelizer.api.php new file mode 100644 index 000000000..6d60a6eed --- /dev/null +++ b/docroot/modules/contrib/panelizer/panelizer.api.php @@ -0,0 +1,30 @@ +bundle() == 'page') { + $view_mode = 'my_custom_view_mode'; + } +} + +/** + * @} End of "addtogroup hooks". + */ \ No newline at end of file diff --git a/docroot/modules/contrib/panelizer/panelizer.info.yml b/docroot/modules/contrib/panelizer/panelizer.info.yml new file mode 100644 index 000000000..270406fe4 --- /dev/null +++ b/docroot/modules/contrib/panelizer/panelizer.info.yml @@ -0,0 +1,22 @@ +type: module +name: Panelizer +description: 'Allow any entity view mode to be rendered using a Panels display.' +package: Layout +# version: VERSION +# core: 8.x +dependencies: + # Core. + - layout_discovery + # @todo Break the dependency on Field UI. + - field_ui + # Contrib + - ctools:ctools_block + - panels:panels + # @todo Allow editing without the IPE. + - panels:panels_ipe + +# Information added by Drupal.org packaging script on 2017-04-28 +version: '8.x-4.0' +core: '8.x' +project: 'panelizer' +datestamp: 1493406088 diff --git a/docroot/modules/contrib/panelizer/panelizer.install b/docroot/modules/contrib/panelizer/panelizer.install new file mode 100644 index 000000000..ba3925d61 --- /dev/null +++ b/docroot/modules/contrib/panelizer/panelizer.install @@ -0,0 +1,33 @@ +listAll('core.entity_view_display.') as $entity_display_name) { + $entity_display = $config_factory->getEditable($entity_display_name); + if ($displays = $entity_display->get('third_party_settings.panelizer.displays')) { + foreach ($displays as $display_name => $display) { + if (isset($display['layout'])) { + $new_layout = panels_convert_plugin_ids_to_layout_discovery($display['layout']); + if ($new_layout) { + $displays[$display_name]['layout'] = $new_layout; + } + } + } + $entity_display + ->set('third_party_settings.panelizer.displays', $displays) + // Mark the resulting configuration as trusted data. This avoids issues + // with future schema changes. + ->save(TRUE); + } + } +} diff --git a/docroot/modules/contrib/panelizer/panelizer.libraries.yml b/docroot/modules/contrib/panelizer/panelizer.libraries.yml new file mode 100644 index 000000000..50b9c7622 --- /dev/null +++ b/docroot/modules/contrib/panelizer/panelizer.libraries.yml @@ -0,0 +1,24 @@ +panelizer_default_form: + version: VERSION + js: + js/panelizer-default-form.js: {} + dependencies: + - core/jquery + - core/jquery.once +panels_ipe: + version: VERSION + js: + # Core. + js/panels_ipe/panels_ipe.js: {} + # Views. + js/panels_ipe/views/SaveTabView.js: {} + css: + component: + css/panels_ipe.css: {} + dependencies: + - panels_ipe/panels_ipe +wizard_admin: + version: VERSION + css: + layout: + css/panelizer.admin.css: {} diff --git a/docroot/modules/contrib/panelizer/panelizer.links.action.yml b/docroot/modules/contrib/panelizer/panelizer.links.action.yml new file mode 100644 index 000000000..6ed51fe31 --- /dev/null +++ b/docroot/modules/contrib/panelizer/panelizer.links.action.yml @@ -0,0 +1,5 @@ +panelizer.default.add: + route_name: panelizer.wizard.add + title: 'Add a new Panelizer default display' + class: \Drupal\panelizer\Menu\AddDefaultLocalAction + deriver: \Drupal\panelizer\Plugin\AddDefaultLinkDeriver diff --git a/docroot/modules/contrib/panelizer/panelizer.module b/docroot/modules/contrib/panelizer/panelizer.module new file mode 100644 index 000000000..badc90dc0 --- /dev/null +++ b/docroot/modules/contrib/panelizer/panelizer.module @@ -0,0 +1,407 @@ + [ + 'render element' => 'element', + ], + 'panelizer_wizard_form' => [ + 'render element' => 'form', + ], + 'panelizer_wizard_tree' => [ + 'variables' => [ + 'wizard' => NULL, + 'cached_values' => [], + 'tree' => [], + 'divider' => ' ยป ', + 'step' => NULL, + ], + ], + ]; +} + +/** + * Implements hook_entity_type_alter(). + */ +function panelizer_entity_type_alter(array &$entity_types) { + /** @var \Drupal\panelizer\Plugin\PanelizerEntityManager $panelizer_manager */ + $panelizer_manager = \Drupal::service('plugin.manager.panelizer_entity'); + + // Replace the entity view builder on any entity where we have a Panelizer + // entity plugin and the entity itself has a view builder. + foreach ($panelizer_manager->getDefinitions() as $entity_type_id => $panelizer_info) { + if (isset($entity_types[$entity_type_id]) && $entity_types[$entity_type_id]->hasHandlerClass('view_builder')) { + $entity_types[$entity_type_id]->setHandlerClass('fallback_view_builder', $entity_types[$entity_type_id]->getHandlerClass('view_builder')); + $entity_types[$entity_type_id]->setHandlerClass('view_builder', '\Drupal\panelizer\PanelizerEntityViewBuilder'); + } + } +} + +/** + * Implements hook_panels_build_alter(). + */ +function panelizer_panels_build_alter(&$build, PanelsDisplayVariant $panels_display) { + $builder = $panels_display->getBuilder(); + $storage_type = $panels_display->getStorageType(); + + $is_panelizer = $builder->getPluginId() == 'ipe' && + in_array($storage_type, ['panelizer_default', 'panelizer_field']) && + isset($build['#attached']) && + isset($build['#attached']['library']) && + in_array('panels_ipe/panels_ipe', $build['#attached']['library']); + + // Add our Javascript customizations for the IPE. + if ($is_panelizer) { + $build['#attached']['library'][] = 'panelizer/panels_ipe'; + + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $panels_display->getContexts()['@panelizer.entity_context:entity']->getContextValue(); + $revision_id = ($entity instanceof RevisionableInterface && $entity->getEntityType()->isRevisionable()) && !$entity->isDefaultRevision() ? $entity->getRevisionId() : NULL; + list (,, $view_mode) = explode(':', $panels_display->getStorageId()); + + // Get the default storage id, if we're looking at a panelizer default or + // the panelizer field contains a revision. + if (sizeof(explode(':', $panels_display->getStorageId())) == 4) { + list(, , , $default) = explode(':', $panels_display->getStorageId()); + } + else { + $default = NULL; + } + if ($panels_display->getStorageType() == 'panelizer_field') { + $panelizer_default_storage_id = rtrim(implode(':', [$entity->getEntityTypeId(), $entity->bundle(), $view_mode, $default]), ':'); + } + else { + $panelizer_default_storage_id = $panels_display->getStorageId(); + } + + // Get the special, internal default storage id that includes the entity id, + // which will allow us to correctly set the contexts on the Panels display. + $panelizer_default_internal_storage_id = '*' . rtrim(implode(':', [$entity->getEntityTypeId(), $entity->id(), $view_mode, $default]), ':'); + + // Get the custom storage id (omitting revision id if this is the default + // revision). + $panelizer_field_storage_id_parts = [$entity->getEntityTypeId(), $entity->id(), $view_mode]; + if ($revision_id) { + $panelizer_field_storage_id_parts[] = $revision_id; + } + $panelizer_field_storage_id = implode(':', $panelizer_field_storage_id_parts); + + /** @var \Drupal\panelizer\PanelizerInterface $panelizer */ + $panelizer = \Drupal::service('panelizer'); + + $build['#attached']['drupalSettings']['panelizer']['entity'] = [ + 'entity_type_id' => $entity->getEntityTypeId(), + 'entity_id' => $entity->id(), + 'view_mode' => $view_mode, + 'revision_id' => $revision_id, + 'panelizer_default_storage_id' => $panelizer->hasDefaultPermission('change content', $entity->getEntityTypeId(), $entity->bundle(), $view_mode, $default) ? $panelizer_default_internal_storage_id : FALSE, + 'panelizer_field_storage_id' => $panelizer->hasEntityPermission('change content', $entity, $view_mode) ? $panelizer_field_storage_id : FALSE, + 'panelizer_default_real_storage_id' => $panelizer_default_storage_id, + ]; + + // Whether or not the current user has access to the "revert to default" + // action in the IPE; any user with the 'administer panelizer' will also + // have access. + $build['#attached']['drupalSettings']['panelizer']['user_permission']['revert'] = $panelizer->hasEntityPermission('revert to default', $entity, $view_mode); + + // Whether or not the current user has access to the "set as default" action + // in the IPE; any user with the 'administer panelizer' will also have + // access. + $user = \Drupal::currentUser(); + $build['#attached']['drupalSettings']['panelizer']['user_permission']['save_default'] = $user->hasPermission('set panelizer default') || $user->hasPermission('administer panelizer'); + + if ($panels_display->getStorageType() == 'panelizer_field') { + // If using panelizer_field, we change the storage id to match what we put + // together here because it'll have the revision id omitted in the right + // situation. + $build['#attached']['drupalSettings']['panels_ipe']['panels_display']['storage_id'] = $panelizer_field_storage_id; + } + else { + // If using panelizer_default, we need to switch to a the special, + // internal storage id. + $build['#attached']['drupalSettings']['panels_ipe']['panels_display']['storage_id'] = $panelizer_default_internal_storage_id; + } + } +} + +/** + * Implements hook_panels_ipe_panels_display_presave(). + */ +function panelizer_panels_ipe_panels_display_presave(PanelsDisplayVariant $panels_display, array $layout_model) { + if (empty($layout_model['panelizer_save_as'])) { + return; + } + + // See if the user requested changing the storage type. + $current_storage = $panels_display->getStorageType(); + $panelizer_save_as = $layout_model['panelizer_save_as']; + if ($current_storage !== $panelizer_save_as) { + $panelizer_entity = $layout_model['panelizer_entity']; + + // When actually saving, we want to use the real storage id for me the + // Panelizer default. + $panelizer_entity['panelizer_default_storage_id'] = $panelizer_entity['panelizer_default_real_storage_id']; + + // If we were custom and now we want to save to the default, we need to + // save specially to the Panelizer field so that we can tell it we're on + // a default. + if ($panelizer_save_as == 'panelizer_default') { + /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */ + $entity_type_manager = \Drupal::service("entity_type.manager"); + $storage = $entity_type_manager->getStorage($panelizer_entity['entity_type_id']); + $entity = $storage->load($panelizer_entity['entity_id']); + if ($entity instanceof FieldableEntityInterface) { + /** @var \Drupal\panelizer\PanelizerInterface $panelizer */ + $panelizer = \Drupal::service('panelizer'); + list (,,, $default_name) = explode(':', $panelizer_entity['panelizer_default_storage_id']); + $panelizer->setPanelsDisplay($entity, $panelizer_entity['view_mode'], $default_name); + } + } + + // We need to generate a new UUID if we're creating a custom display. + if ($current_storage == 'panelizer_default' && $panelizer_save_as == 'panelizer_field') { + $configuration = $panels_display->getConfiguration(); + $configuration['uuid'] = \Drupal::service('uuid')->generate(); + $panels_display->setConfiguration($configuration); + } + + // Set the new storage information. + $panels_display->setStorage($panelizer_save_as, $panelizer_entity[$panelizer_save_as . '_storage_id']); + } +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function panelizer_form_entity_view_display_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) { + /** @var \Drupal\Core\Entity\EntityFormInterface $form_object */ + $form_object = $form_state->getFormObject(); + /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */ + $display = $form_object->getEntity(); + + /** @var \Drupal\panelizer\Plugin\PanelizerEntityManager $panelizer_manager */ + $panelizer_manager = \Drupal::service('plugin.manager.panelizer_entity'); + /** @var \Drupal\panelizer\PanelizerInterface $panelizer */ + $panelizer = \Drupal::service('panelizer'); + + $entity_type_id = $display->getTargetEntityTypeId(); + $bundle = $display->getTargetBundle(); + $mode = $display->getMode(); + + if ($panelizer_manager->hasDefinition($entity_type_id)) { + $settings = $panelizer->getPanelizerSettings($entity_type_id, $bundle, $mode, $display); + + // Always put the field table below the Panelizer options. + $form['fields']['#weight'] = 10; + + $form['panelizer'] = [ + '#tree' => TRUE, + ]; + $form['panelizer']['enable'] = [ + '#type' => 'checkbox', + '#title' => t('Panelize this view mode'), + '#default_value' => isset($settings['enable']) ? $settings['enable'] : FALSE, + ]; + + $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id); + + $form['panelizer']['options'] = [ + '#type' => 'details', + '#open' => TRUE, + '#title' => t('Panelizer options'), + '#states' => [ + 'visible' => [ + '#edit-panelizer-enable' => ['checked' => TRUE], + ], + ], + '#parents' => ['panelizer'], + ]; + + $form['panelizer']['options']['custom'] = [ + '#type' => 'checkbox', + '#title' => t('Allow each @entity to have its display customized', [ + '@entity' => $entity_type->getSingularLabel(), + ]), + '#default_value' => isset($settings['custom']) ? $settings['custom'] : FALSE, + ]; + $form['panelizer']['options']['allow'] = [ + '#type' => 'checkbox', + '#title' => t('Allow users to select which display to use'), + '#description' => t('When multiple default displays are available for a view mode it can be useful to allow content creators to choose which display to use for an individual @entity.', [ + '@entity' => $entity_type->getSingularLabel(), + ]) . '
' . t('To enable the choice to be enabled by users, make sure the panelizer field is visible in the Manage form display'), + '#default_value' => isset($settings['allow']) ? $settings['allow'] : FALSE, + ]; + + // If this display mode is panelized, then show the available displays in a + // table. + if (!empty($settings['enable'])) { + $form['#cache']['tags'][] = "panelizer_default:$entity_type_id:$bundle:$mode"; + $form['panelizer']['displays'] = [ + '#type' => 'table', + '#caption' => t('Default displays available for this view mode'), + '#header' => [t('Name'), t('Use as default'), t('Operations')], + ]; + foreach ($display->getThirdPartySetting('panelizer', 'displays', []) as $machine_name => $panels_display) { + // Reset operations when in the foreach loop. + $operations = []; + $display_name = $machine_name; + $machine_name ="{$entity_type_id}__{$bundle}__{$mode}__$machine_name"; + $operations['edit'] = [ + 'title' => t('Edit'), + 'url' => Url::fromRoute('panelizer.wizard.edit', ['machine_name' => $machine_name]), + ]; + if ($settings['default'] != $display_name) { + $operations['set_default'] = [ + 'title' => t('Use as default'), + 'url' => Url::fromRoute('panelizer.default.select', ['machine_name' => $machine_name]), + ]; + $operations['delete'] = [ + 'title' => t('Delete'), + 'url' => Url::fromRoute('panelizer.default.delete', ['machine_name' => $machine_name]), + ]; + } + $form['panelizer']['displays'][$machine_name] = [ + 'label' => ['#markup' => $panels_display['label']], + 'default' => ['#markup' => $settings['default'] == $display_name ? '✓' : ''], + 'operations' => [ + 'data' => [ + '#type' => 'operations', + '#links' => $operations, + ] + ] + ]; + } + $form['fields']['#access'] = FALSE; + } + + $form['#attached']['library'][] = 'panelizer/panelizer_default_form'; + $form['actions']['submit']['#submit'][] = 'panelizer_form_entity_view_display_edit_submit'; + } +} + +function panelizer_form_entity_view_display_edit_submit(&$form, FormStateInterface $form_state) { + $rebuild = FALSE; + /** @var \Drupal\Core\Entity\EntityFormInterface $form_object */ + $form_object = $form_state->getFormObject(); + /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */ + $display = $form_object->getEntity(); + /** @var \Drupal\panelizer\Plugin\PanelizerEntityManager $panelizer_manager */ + $panelizer_manager = \Drupal::service('plugin.manager.panelizer_entity'); + /** @var \Drupal\panelizer\Panelizer $panelizer */ + $panelizer = \Drupal::service('panelizer'); + + if ($panelizer_manager->hasDefinition($display->getTargetEntityTypeId())) { + $settings = $panelizer->getPanelizerSettings($display->getTargetEntityTypeId(), $display->getTargetBundle(), $display->getMode(), $display); + if ($settings['enable'] != $form_state->getValue(['panelizer', 'enable'])) { + $rebuild = TRUE; + } + $settings['enable'] = $form_state->getValue(['panelizer', 'enable']); + $settings['custom'] = $form_state->getValue(['panelizer', 'custom']); + $settings['allow'] = $form_state->getValue(['panelizer', 'allow']); + $panelizer->setPanelizerSettings($display->getTargetEntityTypeId(), $display->getTargetBundle(), $display->getMode(), $settings, $display); + if ($rebuild) { + \Drupal::service('router.builder')->rebuild(); + /** @var \Drupal\Core\Menu\LocalActionManager $local_action_manager */ + $local_action_manager = \Drupal::service('plugin.manager.menu.local_action'); + $local_action_manager->clearCachedDefinitions(); + // Manually reinitialize these. + $local_action_manager->getDefinitions(); + \Drupal::service('cache.render')->invalidateAll(); + } + } +} + +/** + * Implements hook_form_FORM_ID_alter() for field_ui_field_storage_add_form(). + */ +function panelizer_form_field_ui_field_storage_add_form_alter(&$form, FormStateInterface $form_state, $form_id) { + // Hide the legacy Panelizer field. + // @todo Remove the Panelizer field entirely. + if (isset($form['add']['new_storage_type']['#options']['General']['panelizer'])) { + unset($form['add']['new_storage_type']['#options']['General']['panelizer']); + } +} + +/** + * Preprocess function for panelizer-wizard-tree.html.twig. + */ +function template_preprocess_panelizer_wizard_tree(&$variables) { + /** @var $wizard \Drupal\ctools\Wizard\FormWizardInterface|\Drupal\ctools\Wizard\EntityFormWizardInterface */ + $wizard = $variables['wizard']; + $cached_values = $variables['cached_values']; + $tree = $variables['tree']; + $variables['step'] = $wizard->getStep($cached_values); + + foreach ($wizard->getOperations($cached_values) as $step => $operation) { + $parameters = $wizard->getNextParameters($cached_values); + // Override step to be the step we want. + $parameters['step'] = $step; + + // Fill in parents if there are breadcrumbs. + $parent =& $tree; + if (isset($operation['breadcrumbs'])) { + foreach ($operation['breadcrumbs'] as $breadcrumb) { + $breadcrumb_string = (string) $breadcrumb; + if (!isset($parent[$breadcrumb_string])) { + $parent[$breadcrumb_string] = [ + 'title' => $breadcrumb, + 'children' => [], + ]; + } + $parent =& $parent[$breadcrumb_string]['children']; + } + } + + $parent[$step] = [ + 'title' => !empty($operation['title']) ? $operation['title'] : '', + 'url' => new Url($wizard->getRouteName(), $parameters), + 'step' => $step, + ]; + } + + $variables['tree'] = $tree; +} + +/** + * Preprocess function for panelizer-view-mode.html.twig + * + * Prepare variables for Panelizer view mode templates. + */ +function template_preprocess_panelizer_view_mode(&$variables) { + $element = $variables['element']; + + // Copy values into the variables. + /** @var \Drupal\panelizer\Plugin\PanelizerEntityInterface $panelizer_plugin */ + $panelizer_plugin = $variables['panelizer_plugin'] = $element['#panelizer_plugin']; + /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $panels_display */ + $panels_display = $variables['panels_display'] = $element['#panels_display']; + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $variables['entity'] = $element['#entity']; + $view_mode = $variables['view_mode'] = $element['#view_mode']; + $variables['content'] = $element['content']; + $variables['title'] = isset($element['#title']) ? $element['#title'] : ''; + + // Setup the defaults. + $variables['title_element'] = 'h2'; + $variables['entity_url'] = $entity->toUrl('canonical', [ + 'language' => $entity->language(), + ]); + + // Allow the Panelizer entity plugin to do additional preprocessing. + $panelizer_plugin->preprocessViewMode($variables, $entity, $panels_display, $view_mode); +} + diff --git a/docroot/modules/contrib/panelizer/panelizer.permissions.yml b/docroot/modules/contrib/panelizer/panelizer.permissions.yml new file mode 100644 index 000000000..1fa7b0b0d --- /dev/null +++ b/docroot/modules/contrib/panelizer/panelizer.permissions.yml @@ -0,0 +1,8 @@ +administer panelizer: + title: 'Administer Panelizer' + description: 'Fully administer Panelizer and all Panelizer settings.' +set panelizer default: + title: 'Set Panelizer Default' + description: 'Allow user to "Set as default" in the Panelizer IPE.' +permission_callbacks: + - panelizer:getPermissions diff --git a/docroot/modules/contrib/panelizer/panelizer.post_update.php b/docroot/modules/contrib/panelizer/panelizer.post_update.php new file mode 100644 index 000000000..ac122814f --- /dev/null +++ b/docroot/modules/contrib/panelizer/panelizer.post_update.php @@ -0,0 +1,83 @@ +getDefinitions() as $entity_type => $definition) { + if (db_table_exists($entity_type . '__panelizer')) { + $storage = \Drupal::entityTypeManager()->getStorage($entity_type); + $ids = $storage->getQuery() + ->condition('panelizer', serialize([]), '<>') + ->execute(); + + foreach ($storage->loadMultiple($ids) as $entity_id => $entity) { + $results[] = $entity; + } + } + } + + // Use the sandbox to store the information needed to track progression. + if (!isset($sandbox['current'])) { + // The count of entities visited so far. + $sandbox['current'] = 0; + // Total entities that must be visited. + $sandbox['max'] = count($results); + // A place to store messages during the run. + } + + // Process entities by groups of 20. + // When a group is processed, the batch update engine determines whether it + // should continue processing in the same request or provide progress + // feedback to the user and wait for the next request. + $limit = 5; + $result = array_slice($results, $sandbox['current'], $limit); + + foreach ($result as $entity) { + if ($entity->hasField('panelizer') && $entity->panelizer->first()) { + foreach ($entity->panelizer as $item) { + $panels_display_config = $item->get('panels_display')->getValue(); + + // If our field has custom panelizer display config data. + if (!empty($panels_display_config) && is_array($panels_display_config)) { + $layout_id = $panels_display_config['layout']; + if ($new_layout_id = panels_convert_plugin_ids_to_layout_discovery($layout_id)) { + $panels_display_config['layout'] = $new_layout_id; + $item->set('panels_display', $panels_display_config); + } + } + } + $entity->save(); + } + // Update our progress information. + $sandbox['current']++; + } + + $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['current'] / $sandbox['max']); + + if ($sandbox['#finished'] >= 1) { + return t('Panelized custom layouts have been updated.'); + } +} diff --git a/docroot/modules/contrib/panelizer/panelizer.routing.yml b/docroot/modules/contrib/panelizer/panelizer.routing.yml new file mode 100644 index 000000000..5baf178d5 --- /dev/null +++ b/docroot/modules/contrib/panelizer/panelizer.routing.yml @@ -0,0 +1,89 @@ +# Panels IPE +panelizer.panels_ipe.revert_to_default: + path: '/admin/panelizer/panels_ipe/{entity_type_id}/{entity}/{view_mode}/revert_to_default' + options: + parameters: + entity: + type: entity:{entity_type_id} + defaults: + _controller: '\Drupal\panelizer\Controller\PanelizerPanelsIPEController::revertToDefault' + requirements: + _method: 'POST' + _permission: 'access panels in-place editing' + _custom_access: '\Drupal\panelizer\Controller\PanelizerPanelsIPEController::accessRevertToDefault' + +# Wizard +panelizer.wizard.add: + path: '/admin/structure/panelizer/add/{entity_type_id}/{bundle}/{view_mode_name}' + defaults: + _wizard: '\Drupal\panelizer\Wizard\PanelizerAddWizard' + _title: 'Panelizer Wizard' + tempstore_id: 'panelizer.wizard' + requirements: + _panelizer_default_access: 'TRUE' + _permission: 'administer panelizer' + +panelizer.wizard.add.step: + path: '/admin/structure/panelizer/add/{machine_name}/{step}' + defaults: + _wizard: '\Drupal\panelizer\Wizard\PanelizerAddWizard' + _title: 'Panelizer Wizard' + tempstore_id: 'panelizer.wizard' + requirements: + _permission: 'administer panelizer' + +panelizer.wizard.edit: + path: '/admin/structure/panelizer/edit/{machine_name}/{step}' + defaults: + _wizard: '\Drupal\panelizer\Wizard\PanelizerEditWizard' + _title: 'Panelizer Wizard' + tempstore_id: 'panelizer.wizard' + step: 'general' + requirements: + _permission: 'administer panelizer' + +panelizer.default.delete: + path: '/admin/structure/panelizer/delete/{machine_name}' + defaults: + _form: '\Drupal\panelizer\Form\PanelizerDefaultDelete' + _title: 'Delete panelizer default' + requirements: + _panelizer_field_ui_view_mode_access: 'TRUE' + _custom_access: '\Drupal\panelizer\Access\PanelizerDefaultsDisplayAccess::isNotDefaultDisplay' + +panelizer.default.select: + path: '/admin/structure/panelizer/set_default/{machine_name}' + defaults: + _form: '\Drupal\panelizer\Form\PanelizerDefaultSelect' + _title: 'Set as default' + requirements: + _panelizer_field_ui_view_mode_access: 'TRUE' + _custom_access: '\Drupal\panelizer\Access\PanelizerDefaultsDisplayAccess::isNotDefaultDisplay' + +# Contexts +panelizer.wizard.step.context.add: + path: '/admin/panelizer/wizard/{machine_name}/contexts/add/{context_id}' + defaults: + _form: '\Drupal\panelizer\Form\PanelizerWizardContextConfigure' + _title: 'Add custom context' + tempstore_id: 'panelizer.wizard' + requirements: + _permission: 'administer panelizer' + +panelizer.wizard.step.context.edit: + path: '/admin/panelizer/wizard/{machine_name}/contexts/edit/{context_id}' + defaults: + _form: '\Drupal\panelizer\Form\PanelizerWizardContextConfigure' + _title: 'Edit context' + tempstore_id: 'panelizer.wizard' + requirements: + _permission: 'administer panelizer' + +panelizer.wizard.step.context.delete: + path: '/admin/panelizer/wizard/{machine_name}/context/delete/{context_id}' + defaults: + _form: '\Drupal\panelizer\Form\PanelizerWizardContextDeleteForm' + _title: 'Delete static context' + tempstore_id: 'panelizer.wizard' + requirements: + _permission: 'administer panelizer' diff --git a/docroot/modules/contrib/panelizer/panelizer.services.yml b/docroot/modules/contrib/panelizer/panelizer.services.yml new file mode 100644 index 000000000..d36b86ca5 --- /dev/null +++ b/docroot/modules/contrib/panelizer/panelizer.services.yml @@ -0,0 +1,17 @@ +services: + access_check.panelizer.view_mode: + class: Drupal\panelizer\Access\ViewModeAccessCheck + arguments: ['@access_check.field_ui.view_mode'] + tags: + - { name: access_check, applies_to: _panelizer_field_ui_view_mode_access } + plugin.manager.panelizer_entity: + class: Drupal\panelizer\Plugin\PanelizerEntityManager + parent: default_plugin_manager + panelizer: + class: Drupal\panelizer\Panelizer + arguments: ['@entity_type.manager', '@entity_type.bundle.info', '@entity_field.manager', '@plugin.manager.field.field_type', '@module_handler', '@current_user', '@plugin.manager.panelizer_entity', '@panels.display_manager', '@string_translation', '@ctools.context_mapper'] + panelizer.default.access: + class: Drupal\panelizer\Access\DefaultAccess + arguments: ['@panelizer'] + tags: + - { name: access_check, applies_to: _panelizer_default_access } diff --git a/docroot/modules/contrib/panelizer/panelizer_quickedit/README.txt b/docroot/modules/contrib/panelizer/panelizer_quickedit/README.txt new file mode 100644 index 000000000..284bb6837 --- /dev/null +++ b/docroot/modules/contrib/panelizer/panelizer_quickedit/README.txt @@ -0,0 +1,5 @@ +Panelizer Quick Edit +--------- +This module contains customizations which allow Panelized Entities to be edited +inline with Quick Edit. It is separate from the Panelizer project as not all +users need this functionality. diff --git a/docroot/modules/contrib/panelizer/panelizer_quickedit/panelizer_quickedit.info.yml b/docroot/modules/contrib/panelizer/panelizer_quickedit/panelizer_quickedit.info.yml new file mode 100644 index 000000000..4f1145a57 --- /dev/null +++ b/docroot/modules/contrib/panelizer/panelizer_quickedit/panelizer_quickedit.info.yml @@ -0,0 +1,15 @@ +type: module +name: Panelizer Quick Edit +description: 'Enables Quick Edit to function normally when using Panelizer.' +package: Layout +# version: VERSION +# core: 8.x +dependencies: + - panelizer + - quickedit + +# Information added by Drupal.org packaging script on 2017-04-28 +version: '8.x-4.0' +core: '8.x' +project: 'panelizer' +datestamp: 1493406088 diff --git a/docroot/modules/contrib/panelizer/panelizer_quickedit/panelizer_quickedit.module b/docroot/modules/contrib/panelizer/panelizer_quickedit/panelizer_quickedit.module new file mode 100644 index 000000000..44c19f071 --- /dev/null +++ b/docroot/modules/contrib/panelizer/panelizer_quickedit/panelizer_quickedit.module @@ -0,0 +1,59 @@ +getTranslationFromContext($entity, $langcode); + + // Grab the information required to re-render the entity_field block. + $temp = str_replace('panelizer-', '', $view_mode_id); + list($view_mode, $block_id) = explode('-block-id-', $temp); + + // Load the Panelizer display. + /** @var \Drupal\panelizer\PanelizerInterface $panelizer */ + $panelizer = \Drupal::service('panelizer'); + $display = $panelizer->getPanelsDisplay($entity, $view_mode); + + /** @var \Drupal\ctools_block\Plugin\Block\EntityField $plugin */ + $plugin = $display->getBlock($block_id); + + // Set the appropriate Entity context and build the plugin. + $plugin->setContextValue('entity', $entity); + $build = $plugin->build(); + + // Add our custom field view-mode in case the user wants to edit again. + $build['field']['#view_mode'] = $view_mode_id; + + return $build; +} + +/** + * Implements hook_entity_view_alter(). + */ +function panelizer_quickedit_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) { + // Explicitly add support for ctools_block by attaching a custom view-mode to + // every Block that's about to be rendered. + if (isset($build['#panels_display'])) { + // We only support known Display Builders. + $supported_plugins = ['ipe', 'standard']; + if (in_array($build['#panels_display']->getBuilder()->getPluginId(), $supported_plugins)) { + $region_names = Element::getVisibleChildren($build['content']); + foreach ($region_names as $region_name) { + $block_ids = Element::getVisibleChildren($build['content'][$region_name]); + foreach ($block_ids as $block_id) { + $block = &$build['content'][$region_name][$block_id]; + if (isset($block['#base_plugin_id']) && $block['#base_plugin_id'] === 'entity_field') { + $view_mode = 'panelizer-' . $build['#view_mode'] . '-block-id-' . $block_id; + $block['content']['field']['#view_mode'] = $view_mode; + } + } + } + } + } +} diff --git a/docroot/modules/contrib/panelizer/panelizer_quickedit/tests/src/FunctionalJavascript/PanelizerQuickEditTest.php b/docroot/modules/contrib/panelizer/panelizer_quickedit/tests/src/FunctionalJavascript/PanelizerQuickEditTest.php new file mode 100644 index 000000000..005668aad --- /dev/null +++ b/docroot/modules/contrib/panelizer/panelizer_quickedit/tests/src/FunctionalJavascript/PanelizerQuickEditTest.php @@ -0,0 +1,159 @@ +drupalCreateContentType(['type' => 'page', 'name' => 'Page']); + + // Add a plain text field for this content type. + FieldStorageConfig::create([ + 'field_name' => 'test_field', + 'entity_type' => 'node', + 'type' => 'string', + ])->save(); + + FieldConfig::create([ + 'field_name' => 'test_field', + 'label' => 'Test Field', + 'entity_type' => 'node', + 'bundle' => 'page', + 'required' => FALSE, + 'settings' => [], + 'description' => '', + ])->save(); + + /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $entity_form_display */ + $entity_form_display = \Drupal::entityTypeManager() + ->getStorage('entity_form_display') + ->load('node.page.default'); + $entity_form_display->setComponent('test_field')->save(); + + /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $entity_display */ + $entity_display = \Drupal::entityTypeManager() + ->getStorage('entity_view_display') + ->load('node.page.default'); + $entity_display->setComponent('test_field')->save(); + + // Create a privileged user. + $user = $this->drupalCreateUser([ + 'access contextual links', + 'access in-place editing', + 'access content', + 'administer node display', + 'administer panelizer', + 'create page content', + 'edit any page content', + ]); + $this->drupalLogin($user); + + // Enable Panelizer for Articles. + $this->drupalGet('admin/structure/types/manage/page/display'); + $this->assertResponse(200); + $edit = [ + 'panelizer[enable]' => TRUE, + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertResponse(200); + } + + /** + * Tests Quick Editing a Panelized Node. + */ + public function testPanelizerQuickEdit() { + /** @var \Drupal\panelizer\PanelizerInterface $panelizer */ + $panelizer = \Drupal::service('panelizer'); + $displays = $panelizer->getDefaultPanelsDisplays('node', 'page', 'default'); + $display = $displays['default']; + + // Find the "test_field" block. + $block_id = FALSE; + foreach ($display->getConfiguration()['blocks'] as $block) { + if ($block['id'] === 'entity_field:node:test_field') { + $block_id = $block['uuid']; + } + } + + // Make sure we found a valid UUID. + $this->assertNotFalse($block_id); + + // Create an Article. + $node = $this->drupalCreateNode([ + 'type' => 'page', + 'test_field' => [ + 'value' => 'Change me', + ], + ]); + + // Visit the new node. + $this->drupalGet('node/' . $node->id()); + $this->assertResponse(200); + + // This is the unique ID we append to normal Quick Edit field IDs. + $panelizer_id = 'panelizer-full-block-id-' . $block_id; + + // Assemble common CSS selectors. + $entity_selector = '[data-quickedit-entity-id="node/' . $node->id() . '"]'; + $field_selector = '[data-quickedit-field-id="node/' . $node->id() . '/test_field/' . $node->language()->getId() . '/' . $panelizer_id . '"]'; + + // Wait until Quick Edit loads. + $condition = "jQuery('" . $entity_selector . " .quickedit').length > 0"; + $this->assertJsCondition($condition, 10000); + + // Initiate Quick Editing. + $this->triggerClick($entity_selector . ' [data-contextual-id] > button'); + $this->click($entity_selector . ' [data-contextual-id] .quickedit > a'); + $this->triggerClick($field_selector); + $this->assertSession()->assertWaitOnAjaxRequest(); + + // Trigger an edit with Javascript (this is a "contenteditable" element). + $this->getSession()->executeScript("jQuery('" . $field_selector . "').text('Hello world').trigger('keyup');"); + + // To prevent 403s on save, we re-set our request (cookie) state. + $this->prepareRequest(); + + // Save the change. + $this->triggerClick('.quickedit-button.action-save'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + // Re-visit the node to make sure the edit worked. + $this->drupalGet('node/' . $node->id()); + $this->assertResponse(200); + $this->assertSession()->pageTextContains('Hello world'); + } + + /** + * Clicks the element with the given CSS selector using event triggers. + * + * @todo Remove when https://github.com/jcalderonzumba/gastonjs/issues/19 + * is fixed. Currently clicking anchors/buttons with nested elements is not + * possible. + * + * @param string $css_selector + * The CSS selector identifying the element to click. + */ + protected function triggerClick($css_selector) { + $this->getSession()->executeScript("jQuery('" . $css_selector . "')[0].click()"); + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Access/DefaultAccess.php b/docroot/modules/contrib/panelizer/src/Access/DefaultAccess.php new file mode 100644 index 000000000..45af31170 --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Access/DefaultAccess.php @@ -0,0 +1,48 @@ +panelizer = $panelizer; + } + + /** + * Determines access to a default Panelizer layout. + * + * @param string $entity_type_id + * The panelized entity type ID. + * @param string $bundle + * The panelized bundle ID. + * @param string $view_mode_name + * The panelized view mode ID. + * + * @return \Drupal\Core\Access\AccessResult + */ + public function access($entity_type_id, $bundle, $view_mode_name) { + $settings = $this->panelizer->getPanelizerSettings($entity_type_id, $bundle, $view_mode_name); + return $settings['enable'] ? AccessResult::allowed() : AccessResult::forbidden(); + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Access/PanelizerDefaultsDisplayAccess.php b/docroot/modules/contrib/panelizer/src/Access/PanelizerDefaultsDisplayAccess.php new file mode 100644 index 000000000..1e76a9bed --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Access/PanelizerDefaultsDisplayAccess.php @@ -0,0 +1,34 @@ +getPanelizerSettings($entity_type, $bundle, $view_mode); + if ($settings['default'] != $default) { + $access = AccessResult::allowed(); + } + else { + $access = AccessResult::forbidden(); + } + return $access->addCacheTags(["panelizer_default:$entity_type:$bundle:$view_mode", "panelizer_default:$entity_type:$bundle:$view_mode:$default"]); + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Access/PanelizerUIAccess.php b/docroot/modules/contrib/panelizer/src/Access/PanelizerUIAccess.php new file mode 100644 index 000000000..a58bdec07 --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Access/PanelizerUIAccess.php @@ -0,0 +1,18 @@ +hasPermission('administer panelizer') ? AccessResult::allowed() : AccessResult::forbidden(); + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Access/ViewModeAccessCheck.php b/docroot/modules/contrib/panelizer/src/Access/ViewModeAccessCheck.php new file mode 100644 index 000000000..bb2811098 --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Access/ViewModeAccessCheck.php @@ -0,0 +1,61 @@ +accessCheck = $access_check; + } + + /** + * Adapt the panelizer defaults access check to correspond to field ui. + * + * @param \Symfony\Component\Routing\Route $route + * The original route definition. + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The route matched. + * @param \Drupal\Core\Session\AccountInterface $account + * The current user's account. + * @param string $machine_name + * The machine name of the panelizer default. + * + * @return \Drupal\Core\Access\AccessResultInterface + * @throws \Exception + */ + public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account, $machine_name) { + $parts = explode('__', $machine_name); + if (count($parts) != 4) { + throw new \Exception('The provided machine_name is not well formed.'); + } + list($entity_type_id, $bundle, $view_mode) = $parts; + $defaults = [ + 'entity_type_id' => $entity_type_id, + ] + $route->getDefaults(); + $route->setDefaults($defaults); + $route->setRequirement('_field_ui_view_mode_access', 'administer ' . $entity_type_id . ' display'); + return $this->accessCheck->access($route, $route_match, $account, $view_mode, $bundle); + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Annotation/PanelizerEntity.php b/docroot/modules/contrib/panelizer/src/Annotation/PanelizerEntity.php new file mode 100644 index 000000000..145214389 --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Annotation/PanelizerEntity.php @@ -0,0 +1,18 @@ +panelizer = $panelizer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('panelizer') + ); + } + + /** + * Reverts an entity view mode to a particular named default. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * The entity. + * @param string $view_mode + * The view mode. + * + * @return \Symfony\Component\HttpFoundation\Response + * An empty response. + * + * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException + */ + public function revertToDefault(FieldableEntityInterface $entity, $view_mode) { + // Get the bundle specific default display as a fallback. + $settings = $this->panelizer->getPanelizerSettings($entity->getEntityTypeId(), $entity->bundle(), $view_mode); + $default = $settings['default']; + // Check the entity for a documented default to which we should revert. + if ($entity->hasField('panelizer') && $entity->panelizer->first()) { + foreach ($entity->panelizer as $item) { + if ($item->view_mode == $view_mode && !empty($item->default)) { + $default = $item->default; + break; + } + } + } + // If we somehow ended up not having a default, throw an exception. + if (empty($default)) { + throw new BadRequestHttpException("Default name to revert to must be passed!"); + } + $this->panelizer->setPanelsDisplay($entity, $view_mode, $default); + return new Response(); + } + + /** + * Custom access checker for reverting an entity view mode to a named default. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * The entity. + * @param string $view_mode + * The view mode. + * @param \Drupal\Core\Session\AccountInterface $account + * The user account. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. + */ + public function accessRevertToDefault(FieldableEntityInterface $entity, $view_mode, AccountInterface $account) { + return AccessResult::allowedIf($this->panelizer->hasEntityPermission('revert to default', $entity, $view_mode, $account)); + } + +} \ No newline at end of file diff --git a/docroot/modules/contrib/panelizer/src/Exception/PanelizerException.php b/docroot/modules/contrib/panelizer/src/Exception/PanelizerException.php new file mode 100644 index 000000000..3bd76d949 --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Exception/PanelizerException.php @@ -0,0 +1,9 @@ +entityTypeManager = $entity_type_manager; + $this->panelizer = $panelizer; + $this->panelsDisplayManager = $panels_display_manager; + $this->invalidator = $invalidator; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('panelizer'), + $container->get('panels.display_manager'), + $container->get('cache_tags.invalidator') + ); + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return 'Are you certain you want to delete this panelizer default?.'; + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + $bundle_entity_type = $this->entityTypeManager->getDefinition($this->entityTypeId)->getBundleEntityType(); + if ($this->viewMode == 'default') { + $route = "entity.entity_view_display.{$this->entityTypeId}.default"; + $arguments = [ + $bundle_entity_type => $this->bundle, + ]; + } + else { + $route = "entity.entity_view_display.{$this->entityTypeId}.view_mode"; + $arguments = [ + $bundle_entity_type => $this->bundle, + 'view_mode_name' => $this->viewMode, + ]; + } + return new Url($route, $arguments); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'panelizer_default_delete'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $machine_name = NULL) { + list ( + $this->entityTypeId, + $this->bundle, + $this->viewMode, + $this->displayId + ) = explode('__', $machine_name); + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $display = $this->panelizer->getEntityViewDisplay($this->entityTypeId, $this->bundle, $this->viewMode); + $displays = $this->panelizer->getDefaultPanelsDisplays($this->entityTypeId, $this->bundle, $this->viewMode, $display); + unset($displays[$this->displayId]); + foreach ($displays as $key => $value) { + $displays[$key] = $this->panelsDisplayManager->exportDisplay($value); + } + $display->setThirdPartySetting('panelizer', 'displays', $displays); + $display->save(); + $form_state->setRedirectUrl($this->getCancelUrl()); + $tag = "panelizer_default:{$this->entityTypeId}:{$this->bundle}:{$this->viewMode}:{$this->displayId}"; + $this->invalidator->invalidateTags([$tag]); + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Form/PanelizerDefaultSelect.php b/docroot/modules/contrib/panelizer/src/Form/PanelizerDefaultSelect.php new file mode 100644 index 000000000..e1c98f14b --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Form/PanelizerDefaultSelect.php @@ -0,0 +1,142 @@ +panelizer = $panelizer; + $this->invalidator = $invalidator; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('panelizer'), + $container->get('cache_tags.invalidator') + ); + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return 'Are you certain you want to set this panelizer default as the default for this bundle?.'; + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + $bundle_entity_type = \Drupal::entityTypeManager()->getDefinition($this->entityTypeId)->getBundleEntityType(); + if ($this->viewMode == 'default') { + $route = "entity.entity_view_display.{$this->entityTypeId}.default"; + $arguments = [ + $bundle_entity_type => $this->bundle, + ]; + } + else { + $route = "entity.entity_view_display.{$this->entityTypeId}.view_mode"; + $arguments = [ + $bundle_entity_type => $this->bundle, + 'view_mode_name' => $this->viewMode, + ]; + } + return new Url($route, $arguments); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'panelizer_default_delete'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $machine_name = NULL) { + list ( + $this->entityTypeId, + $this->bundle, + $this->viewMode, + $this->displayId + ) = explode('__', $machine_name); + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $display = $this->panelizer->getEntityViewDisplay($this->entityTypeId, $this->bundle, $this->viewMode); + $settings = $this->panelizer->getPanelizerSettings($this->entityTypeId, $this->bundle, $this->viewMode, $display); + $settings['default'] = $this->displayId; + $this->panelizer->setPanelizerSettings($this->entityTypeId, $this->bundle, $this->viewMode, $settings, $display); + $form_state->setRedirectUrl($this->getCancelUrl()); + $tag = "panelizer_default:{$this->entityTypeId}:{$this->bundle}:{$this->viewMode}"; + $this->invalidator->invalidateTags([$tag]); + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Form/PanelizerWizardContextConfigure.php b/docroot/modules/contrib/panelizer/src/Form/PanelizerWizardContextConfigure.php new file mode 100644 index 000000000..a7ab05049 --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Form/PanelizerWizardContextConfigure.php @@ -0,0 +1,92 @@ +contextMapper = $context_mapper; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('user.shared_tempstore'), + $container->get('ctools.context_mapper') + ); + } + + /** + * {@inheritdoc} + */ + protected function getParentRouteInfo($cached_values) { + return ['panelizer.wizard.add.step', [ + 'machine_name' => $cached_values['id'], + 'step' => 'contexts', + ]]; + } + + /** + * {@inheritdoc} + */ + protected function getContexts($cached_values) { + $static_contexts = isset($cached_values['contexts']) ? $cached_values['contexts'] : []; + $static_contexts = $this->contextMapper->getContextValues($static_contexts); + return $static_contexts; + } + + /** + * {@inheritdoc} + */ + protected function addContext($cached_values, $context_id, ContextInterface $context) { + $cached_values['contexts'][$context_id] = [ + 'label' => $context->getContextDefinition()->getLabel(), + 'type' => $context->getContextDefinition()->getDataType(), + 'description' => $context->getContextDefinition()->getDescription(), + 'value' => strpos($context->getContextDefinition()->getDataType(), 'entity:') === 0 ? $context->getContextValue()->uuid() : $context->getContextValue(), + ]; + return $cached_values; + } + + /** + * {@inheritdoc} + */ + public function contextExists($value, $element, $form_state) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + protected function disableMachineName($cached_values, $machine_name) { + return !empty($cached_values['contexts'][$machine_name]); + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Form/PanelizerWizardContextDeleteForm.php b/docroot/modules/contrib/panelizer/src/Form/PanelizerWizardContextDeleteForm.php new file mode 100644 index 000000000..e4175e0df --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Form/PanelizerWizardContextDeleteForm.php @@ -0,0 +1,54 @@ +getTempstore(); + $context = $cached_values['contexts'][$this->context_id]; + return $this->t('Are you sure you want to delete the context @label?', ['@label' => $context['label']]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + $cached_values = $this->getTempstore(); + + return new Url('panelizer.wizard.add.step', [ + 'machine_name' => $cached_values['id'], + 'step' => 'contexts', + ]); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $this->getTempstore(); + $context = $cached_values['contexts'][$this->context_id]; + drupal_set_message($this->t('The static context %label has been removed.', ['%label' => $context['label']])); + unset($cached_values['contexts'][$this->context_id]); + $this->setTempstore($cached_values); + parent::submitForm($form, $form_state); + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Form/PanelizerWizardContextForm.php b/docroot/modules/contrib/panelizer/src/Form/PanelizerWizardContextForm.php new file mode 100644 index 000000000..44b6da73d --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Form/PanelizerWizardContextForm.php @@ -0,0 +1,150 @@ +get('typed_data_manager'), + $container->get('form_builder'), + $container->get('user.shared_tempstore') + ); + } + + /** + * ManageContext constructor. + * + * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager + * The typed data manager. + * @param \Drupal\Core\Form\FormBuilderInterface $form_builder + * The form builder. + * @param \Drupal\user\SharedTempStoreFactory $tempstore_factory + * Shared user tempstore factory. + */ + public function __construct(TypedDataManagerInterface $typed_data_manager, FormBuilderInterface $form_builder, SharedTempStoreFactory $tempstore_factory) { + parent::__construct($typed_data_manager, $form_builder); + $this->tempstoreFactory = $tempstore_factory; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'panelizer_wizard_context_form'; + } + + /** + * {@inheritdoc} + */ + protected function getContextClass($cached_values) { + return PanelizerWizardContextConfigure::class; + } + + /** + * {@inheritdoc} + */ + protected function getRelationshipClass($cached_values) {} + + /** + * {@inheritdoc} + */ + protected function getContextAddRoute($cached_values) { + return 'panelizer.wizard.step.context.add'; + } + + /** + * {@inheritdoc} + */ + protected function getRelationshipAddRoute($cached_values) { + return 'panelizer.wizard.step.context.add'; + } + + /** + * {@inheritdoc} + */ + protected function getContexts($cached_values) { + return $cached_values['plugin']->getPattern()->getDefaultContexts($this->tempstoreFactory, $this->getTempstoreId(), $this->machine_name); + } + + /** + * {@inheritdoc} + */ + protected function getTempstoreId() { + return 'panelizer.wizard'; + } + + /** + * {@inheritdoc} + */ + protected function getContextOperationsRouteInfo($cached_values, $machine_name, $row) { + return ['panelizer.wizard.step.context', [ + 'machine_name' => $machine_name, + 'context_id' => $row, + ]]; + } + + /** + * {@inheritdoc} + */ + protected function getRelationshipOperationsRouteInfo($cached_values, $machine_name, $row) { + return ['panelizer.wizard.step.context', [ + 'machine_name' => $machine_name, + 'context_id' => $row, + ]]; + } + + /** + * {@inheritdoc} + */ + protected function isEditableContext($cached_values, $row) { + if (!isset($cached_values['contexts'][$row])) { + return FALSE; + } + $context = $cached_values['contexts'][$row]; + return !empty($context['value']); + } + + /** + * {@inheritdoc} + */ + public function addContext(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + $context = $form_state->getValue('context'); + $content = $this->formBuilder->getForm($this->getContextClass($cached_values), $context, $this->getTempstoreId(), $this->machine_name); + $content['#attached']['library'][] = 'core/drupal.dialog.ajax'; + list(, $route_parameters) = $this->getContextOperationsRouteInfo($cached_values, $this->machine_name, $context); + $content['submit']['#attached']['drupalSettings']['ajax'][$content['submit']['#id']]['url'] = $this->url($this->getContextAddRoute($cached_values), $route_parameters, ['query' => [FormBuilderInterface::AJAX_FORM_REQUEST => TRUE]]); + $response = new AjaxResponse(); + $response->addCommand(new OpenModalDialogCommand($this->t('Add new context'), $content, ['width' => '700'])); + return $response; + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Form/PanelizerWizardGeneralForm.php b/docroot/modules/contrib/panelizer/src/Form/PanelizerWizardGeneralForm.php new file mode 100644 index 000000000..dcb84a989 --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Form/PanelizerWizardGeneralForm.php @@ -0,0 +1,154 @@ +routeMatch = $route_match; + + if ($route_match->getRouteName() == 'panelizer.wizard.add') { + $this->entityTypeId = $route_match->getParameter('entity_type_id'); + $this->bundle = $route_match->getParameter('bundle'); + $this->viewMode = $route_match->getParameter('view_mode_name'); + } + $this->panelizer = $panelizer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('current_route_match'), + $container->get('panelizer') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'panelizer_wizard_general_form'; + } + + /** + * @param $machine_name + * @param $element + */ + public static function validateMachineName($machine_name, $element) { + // Attempt to load via the machine name and entity type. + if (isset($element['#machine_name']['prefix'])) { + $panelizer = \Drupal::service('panelizer'); + // Load the panels display variant. + $full_machine_name = $element['#machine_name']['prefix'] . '__' . $machine_name; + return $panelizer->getDefaultPanelsDisplayByMachineName($full_machine_name); + } + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $plugin */ + $plugin = $cached_values['plugin']; + + $form_state = new FormState(); + $form_state->setValues($form_state->getValue('variant_settings', [])); + $settings = $plugin->buildConfigurationForm([], $form_state); + + // If the entity view display supports custom Panelizer layouts, force use + // of the in-place editor. Right now, there is no other way to work with + // custom layouts. + if (isset($cached_values['id'])) { + list ($this->entityTypeId, $this->bundle, $this->viewMode) = explode('__', $cached_values['id']); + } + $panelizer_settings = $this->panelizer + ->getPanelizerSettings($this->entityTypeId, $this->bundle, $this->viewMode); + + if (!empty($panelizer_settings['custom'])) { + $settings['builder']['#default_value'] = 'ipe'; + $settings['builder']['#access'] = FALSE; + } + + $settings['#tree'] = TRUE; + $form['variant_settings'] = $settings; + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + if ($form_state->hasValue('id') && !isset($this->machine_name) && $form_state->has('machine_name_prefix')) { + $form_state->setValue('id', "{$form_state->get('machine_name_prefix')}__{$form_state->getValue('id')}"); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $plugin */ + $plugin = $cached_values['plugin']; + $plugin->submitConfigurationForm($form['variant_settings'], (new FormState())->setValues($form_state->getValue('variant_settings', []))); + $configuration = $plugin->getConfiguration(); + $cached_values['plugin']->setConfiguration($configuration); + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Menu/AddDefaultLocalAction.php b/docroot/modules/contrib/panelizer/src/Menu/AddDefaultLocalAction.php new file mode 100644 index 000000000..c3b78f3fc --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Menu/AddDefaultLocalAction.php @@ -0,0 +1,23 @@ +pluginDefinition['route_parameters']['entity_type_id'] = $route_match->getCurrentRouteMatch()->getParameter('entity_type_id'); + $this->pluginDefinition['route_parameters']['bundle'] = $route_match->getCurrentRouteMatch()->getParameter('bundle'); + $this->pluginDefinition['route_parameters']['view_mode_name'] = $route_match->getCurrentRouteMatch()->getParameter('view_mode_name'); + return parent::getRouteParameters($route_match); + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Panelizer.php b/docroot/modules/contrib/panelizer/src/Panelizer.php new file mode 100644 index 000000000..5169bb196 --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Panelizer.php @@ -0,0 +1,688 @@ +entityTypeManager = $entity_type_manager; + $this->entityTypeBundleInfo = $entity_type_bundle_info; + $this->entityFieldManager = $entity_field_manager; + $this->fieldTypeManager = $field_type_manager; + $this->moduleHandler = $module_handler; + $this->currentUser = $current_user; + $this->panelizerEntityManager = $panelizer_entity_manager; + $this->panelsManager = $panels_manager; + $this->stringTranslation = $string_translation; + $this->contextMapper = $context_mapper; + } + + /** + * Gets the Panelizer entity plugin. + * + * @param $entity_type_id + * The entity type id. + * + * @return \Drupal\panelizer\Plugin\PanelizerEntityInterface + */ + protected function getEntityPlugin($entity_type_id) { + return $this->panelizerEntityManager->createInstance($entity_type_id, []); + } + + /** + * Load a Panels Display via an ID (Machine Name). + * + * @return \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant|NULL + * The default Panels display with the given name if it exists; otherwise + * NULL. + */ + public function getDefaultPanelsDisplayByMachineName($full_machine_name) { + list($entity_type, $bundle, $view_mode, $machine_name) = explode('__', $full_machine_name); + /** @var \Drupal\panelizer\Panelizer $panelizer */ + // @todo this $display_id looks all wrong to me since it's the name and view_mode. + return $this->getDefaultPanelsDisplay($machine_name, $entity_type, $bundle, $view_mode); + } + + /** + * {@inheritdoc} + */ + public function getEntityViewDisplay($entity_type_id, $bundle, $view_mode) { + // Check the existence and status of: + // - the display for the view mode, + // - the 'default' display. + $candidate_ids = []; + if ($view_mode != 'default') { + $candidate_ids[] = $entity_type_id . '.' . $bundle . '.' . $view_mode; + } + $candidate_ids[] = $entity_type_id . '.' . $bundle . '.default'; + $results = \Drupal::entityQuery('entity_view_display') + ->condition('id', $candidate_ids) + ->condition('status', TRUE) + ->execute(); + + // Select the first valid candidate display, if any. + $load_id = FALSE; + foreach ($candidate_ids as $candidate_id) { + if (isset($results[$candidate_id])) { + $load_id = $candidate_id; + break; + } + } + + // Use the selected display if any, or create a fresh runtime object. + $storage = $this->entityTypeManager->getStorage('entity_view_display'); + if ($load_id) { + $display = $storage->load($load_id); + } + else { + $display = $storage->create([ + 'targetEntityType' => $entity_type_id, + 'bundle' => $bundle, + 'mode' => $view_mode, + 'status' => TRUE, + ]); + } + + // Let modules alter the display. + $display_context = [ + 'entity_type' => $entity_type_id, + 'bundle' => $bundle, + 'view_mode' => $view_mode, + ]; + $this->moduleHandler->alter('entity_view_display', $display, $display_context); + + return $display; + } + + /** + * {@inheritdoc} + */ + public function getPanelsDisplay(FieldableEntityInterface $entity, $view_mode, EntityViewDisplayInterface $display = NULL) { + $settings = $this->getPanelizerSettings($entity->getEntityTypeId(), $entity->bundle(), $view_mode, $display); + if (($settings['custom'] || $settings['allow']) && isset($entity->panelizer) && $entity->panelizer->first()) { + /** @var \Drupal\Core\Field\FieldItemInterface[] $values */ + $values = []; + foreach ($entity->panelizer as $item) { + $values[$item->view_mode] = $item; + } + if (isset($values[$view_mode])) { + $panelizer_item = $values[$view_mode]; + // Check for a customized display first and use that if present. + if (!empty($panelizer_item->panels_display)) { + // @todo: validate schema after https://www.drupal.org/node/2392057 is fixed. + return $this->panelsManager->importDisplay($panelizer_item->panels_display, FALSE); + } + // If not customized, use the specified default. + if (!empty($panelizer_item->default)) { + // If we're using this magic key use the settings default. + if ($panelizer_item->default == '__bundle_default__') { + $default = $settings['default']; + } + else { + $default = $panelizer_item->default; + // Ensure the default still exists and if not fallback sanely. + $displays = $this->getDefaultPanelsDisplays($entity->getEntityTypeId(), $entity->bundle(), $view_mode); + if (!isset($displays[$default])) { + $default = $settings['default']; + } + } + $panels_display = $this->getDefaultPanelsDisplay($default, $entity->getEntityTypeId(), $entity->bundle(), $view_mode, $display); + $this->setCacheTags($panels_display, $entity->getEntityTypeId(), $entity->bundle(), $view_mode, $display, $default, $settings); + return $panels_display; + } + } + } + // If the field has no input to give us, use the settings default. + $panels_display = $this->getDefaultPanelsDisplay($settings['default'], $entity->getEntityTypeId(), $entity->bundle(), $view_mode, $display); + $this->setCacheTags($panels_display, $entity->getEntityTypeId(), $entity->bundle(), $view_mode, $display, $settings['default'], $settings); + return $panels_display; + } + + /** + * Properly determine the cache tags for a display and set them. + * + * @param \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $panels_display + * The panels display variant. + * @param string $entity_type_id + * The entity type id. + * @param string $bundle + * The bundle. + * @param string $view_mode + * The view mode. + * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface|NULL $display + * If the caller already has the correct display, it can optionally be + * passed in here so the Panelizer service doesn't have to look it up; + * otherwise, this argument can be omitted. + * @param $default + * The name of the panels display we are about to render. + * @param array $settings + * The default panelizer settings for this EntityViewDisplay. + */ + protected function setCacheTags(PanelsDisplayVariant $panels_display, $entity_type_id, $bundle, $view_mode, EntityViewDisplayInterface $display = NULL, $default, array $settings) { + if (!$display) { + $display = $this->getEntityViewDisplay($entity_type_id, $bundle, $view_mode); + } + $display_mode = $display ? $display->getMode() : ''; + + if ($default == $settings['default']) { + $tags = ["{$panels_display->getStorageType()}:{$entity_type_id}:{$bundle}:{$display_mode}"]; + } + $tags[] = "{$panels_display->getStorageType()}:{$entity_type_id}:{$bundle}:{$display_mode}:$default"; + $panels_display->addCacheTags($tags); + } + + /** + * {@inheritdoc} + */ + public function setPanelsDisplay(FieldableEntityInterface $entity, $view_mode, $default, PanelsDisplayVariant $panels_display = NULL) { + $settings = $this->getPanelizerSettings($entity->getEntityTypeId(), $entity->bundle(), $view_mode); + if (($settings['custom'] || $settings['allow']) && isset($entity->panelizer)) { + $panelizer_item = NULL; + /** @var \Drupal\Core\Field\FieldItemInterface $item */ + foreach ($entity->panelizer as $item) { + if ($item->view_mode == $view_mode) { + $panelizer_item = $item; + break; + } + } + if (!$panelizer_item) { + $panelizer_item = $this->fieldTypeManager->createFieldItem($entity->panelizer, count($entity->panelizer)); + $panelizer_item->view_mode = $view_mode; + } + + // Note: We don't call $panels_display->setStorage() here because it will + // be set internally by PanelizerFieldType::postSave() which will know + // the real revision ID of the newly saved entity. + + $panelizer_item->panels_display = $panels_display ? $this->panelsManager->exportDisplay($panels_display) : []; + $panelizer_item->default = $default; + + // Create a new revision if possible. + if ($entity instanceof RevisionableInterface && $entity->getEntityType()->isRevisionable()) { + if ($entity->isDefaultRevision()) { + $entity->setNewRevision(TRUE); + } + } + + // Updates the changed time of the entity, if necessary. + if ($entity->getEntityType()->isSubclassOf(EntityChangedInterface::class)) { + $entity->setChangedTime(REQUEST_TIME); + } + + $entity->panelizer[$panelizer_item->getName()] = $panelizer_item; + + $entity->save(); + } + else { + throw new PanelizerException("Custom overrides not enabled on this entity, bundle and view mode"); + } + } + + /** + * {@inheritdoc} + */ + public function getDefaultPanelsDisplays($entity_type_id, $bundle, $view_mode, EntityViewDisplayInterface $display = NULL) { + if (!$display) { + $display = $this->getEntityViewDisplay($entity_type_id, $bundle, $view_mode); + } + + // Get a list of all the defaults. + $display_config = $display->getThirdPartySetting('panelizer', 'displays', []); + $display_names = array_keys($display_config); + if (empty($display_names)) { + $display_names = ['default']; + } + + // Get each one individually. + $panels_displays = []; + foreach ($display_names as $name) { + if ($panels_display = $this->getDefaultPanelsDisplay($name, $entity_type_id, $bundle, $view_mode, $display)) { + $panels_displays[$name] = $panels_display; + } + } + + return $panels_displays; + } + + /** + * {@inheritdoc} + */ + public function getDefaultPanelsDisplay($name, $entity_type_id, $bundle, $view_mode, EntityViewDisplayInterface $display = NULL) { + if (!$display) { + $display = $this->getEntityViewDisplay($entity_type_id, $bundle, $view_mode); + // If we still don't find a display, then we won't find a Panelizer + // default for sure. + if (!$display) { + return NULL; + } + } + + $config = $display->getThirdPartySetting('panelizer', 'displays', []); + if (!empty($config[$name])) { + // Set a default just in case. + $config[$name]['builder'] = empty($config[$name]['builder']) ? 'standard' : $config[$name]['builder']; + // @todo: validate schema after https://www.drupal.org/node/2392057 is fixed. + $panels_display = $this->panelsManager->importDisplay($config[$name], FALSE); + } + else { + return NULL; + } + + // @todo: Should be set when written, not here! + $storage_id_parts = [ + $entity_type_id, + $bundle, + $view_mode, + $name, + ]; + $panels_display->setStorage('panelizer_default', implode(':', $storage_id_parts)); + + return $panels_display; + } + + /** + * {@inheritdoc} + */ + public function setDefaultPanelsDisplay($name, $entity_type_id, $bundle, $view_mode, PanelsDisplayVariant $panels_display) { + $display = $this->getEntityViewDisplay($entity_type_id, $bundle, $view_mode); + if (!$display) { + throw new PanelizerException("Unable to find display for given entity type, bundle and view mode"); + } + + // Set this individual Panels display. + $panels_displays = $display->getThirdPartySetting('panelizer', 'displays', []); + $panels_displays[$name] = $this->panelsManager->exportDisplay($panels_display); + $display->setThirdPartySetting('panelizer', 'displays', $panels_displays); + + $display->save(); + } + + /** + * {@inheritdoc} + */ + public function getDisplayStaticContexts($name, $entity_type_id, $bundle, $view_mode, EntityViewDisplayInterface $display = NULL) { + if (!$display) { + $display = $this->getEntityViewDisplay($entity_type_id, $bundle, $view_mode); + // If we still don't find a display, then we won't find a Panelizer + // default for sure. + if (!$display) { + return NULL; + } + } + + $config = $display->getThirdPartySetting('panelizer', 'displays', []); + if (!empty($config[$name]) && !empty($config[$name]['static_context'])) { + return $this->contextMapper->getContextValues($config[$name]['static_context']); + } + return []; + } + + /** + * {@inheritdoc} + */ + public function setDisplayStaticContexts($name, $entity_type_id, $bundle, $view_mode, $contexts) { + $display = $this->getEntityViewDisplay($entity_type_id, $bundle, $view_mode); + if (!$display) { + throw new PanelizerException("Unable to find display for given entity type, bundle and view mode"); + } + + // Set this Panels display's static contexts. + $panels_displays = $display->getThirdPartySetting('panelizer', 'displays', []); + $panels_displays[$name]['static_context'] = $contexts; + $display->setThirdPartySetting('panelizer', 'displays', $panels_displays); + + $display->save(); + } + + /** + * {@inheritdoc} + */ + public function isPanelized($entity_type_id, $bundle, $view_mode, EntityViewDisplayInterface $display = NULL) { + if (!$this->getEntityPlugin($entity_type_id)) { + return FALSE; + } + + if (!$display) { + $display = $this->getEntityViewDisplay($entity_type_id, $bundle, $view_mode); + } + + return $display->getThirdPartySetting('panelizer', 'enable', FALSE); + } + + /** + * {@inheritdoc} + */ + public function getPanelizerSettings($entity_type_id, $bundle, $view_mode, EntityViewDisplayInterface $display = NULL) { + if (!$display) { + $display = $this->getEntityViewDisplay($entity_type_id, $bundle, $view_mode); + } + + $settings = [ + 'enable' => $this->isPanelized($entity_type_id, $bundle, $view_mode, $display), + 'custom' => $display->getThirdPartySetting('panelizer', 'custom', FALSE), + 'allow' => $display->getThirdPartySetting('panelizer', 'allow', FALSE), + 'default' => $display->getThirdPartySetting('panelizer', 'default', 'default'), + ]; + + // Make sure that the Panelizer field actually exists. + if ($settings['custom']) { + $fields = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle); + $settings['custom'] = isset($fields['panelizer']) && $fields['panelizer']->getType() == 'panelizer'; + } + + return $settings; + } + + /** + * {@inheritdoc} + */ + public function setPanelizerSettings($entity_type_id, $bundle, $view_mode, array $settings, EntityViewDisplayInterface $display = NULL) { + if (!$display) { + $display = $this->getEntityViewDisplay($entity_type_id, $bundle, $view_mode); + } + + $display->setThirdPartySetting('panelizer', 'enable', !empty($settings['enable'])); + $display->setThirdPartySetting('panelizer', 'custom', !empty($settings['enable']) && !empty($settings['custom'])); + $display->setThirdPartySetting('panelizer', 'allow', !empty($settings['enable']) && !empty($settings['allow'])); + $display->setThirdPartySetting('panelizer', 'default', $settings['default']); + + if (!empty($settings['enable'])) { + // Set the default display. + $displays = $display->getThirdPartySetting('panelizer', 'displays', []); + if (empty($displays['default'])) { + /** @var \Drupal\panelizer\Plugin\PanelizerEntityInterface $panelizer_entity_plugin */ + $panelizer_entity_plugin = $this->panelizerEntityManager->createInstance($display->getTargetEntityTypeId(), []); + $displays['default'] = $this->panelsManager->exportDisplay($panelizer_entity_plugin->getDefaultDisplay($display, $display->getTargetBundle(), $display->getMode())); + $settings['default'] = "{$display->getTargetEntityTypeId()}__{$display->getTargetBundle()}__{$view_mode}__default"; + $display->setThirdPartySetting('panelizer', 'displays', $displays); + } + + // Make sure the field exists. + if (($settings['custom'] || $settings['allow'])) { + $field_storage = $this->entityTypeManager->getStorage('field_storage_config')->load($entity_type_id . '.panelizer'); + if (!$field_storage) { + $field_storage = $this->entityTypeManager->getStorage('field_storage_config')->create([ + 'entity_type' => $entity_type_id, + 'field_name' => 'panelizer', + 'type' => 'panelizer', + 'cardinality' => -1, + 'settings' => [], + 'status' => TRUE, + ]); + $field_storage->save(); + } + + $field = $this->entityTypeManager->getStorage('field_config')->load($entity_type_id . '.' . $bundle . '.panelizer'); + if (!$field) { + $field = $this->entityTypeManager->getStorage('field_config')->create([ + 'field_storage' => $field_storage, + 'bundle' => $bundle, + 'label' => $this->t('Panelizer'), + 'settings' => [], + ]); + $field->save(); + } + } + } + + $display->save(); + } + + /** + * Get a list of all the Panelizer operations. + * + * @return array + * Associative array of the human-readable operation names, keyed by the + * path. + */ + protected function getOperations() { + return [ + 'content' => $this->t('Content'), + 'layout' => $this->t('Layout'), + 'revert' => $this->t('Revert to default'), + ]; + } + + /** + * {@inheritdoc} + */ + public function getPermissions() { + $permissions = []; + + // Only look at entity types that have a corresponding Panelizer plugin. + $entity_types = array_intersect_key( + $this->entityTypeManager->getDefinitions(), + $this->panelizerEntityManager->getDefinitions() + ); + + foreach ($entity_types as $entity_type_id => $entity_type) { + $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id); + foreach ($bundles as $bundle => $bundle_info) { + $permissions["administer panelizer $entity_type_id $bundle defaults"] = [ + 'title' => t('%entity_name %bundle_name: Administer Panelizer default panels, allowed content and settings.', [ + '%entity_name' => $entity_type->getLabel(), + '%bundle_name' => $bundle_info['label'], + ]), + 'description' => t('Users with this permission can fully administer panelizer for this entity bundle.'), + ]; + + foreach ($this->getOperations() as $path => $operation) { + $permissions["administer panelizer $entity_type_id $bundle $path"] = [ + 'title' => $this->t('%entity_name %bundle_name: Administer Panelizer @operation', [ + '%entity_name' => $entity_type->getLabel(), + '%bundle_name' => $bundle_info['label'], + '@operation' => $operation, + ]), + ]; + } + } + } + + ksort($permissions); + return $permissions; + } + + /** + * Check permission for an individual operation only. + * + * Doesn't check any of the baseline permissions that you need along with + * the operation permission. + * + * @param string $op + * The operation. + * @param string $entity_type_id + * The entity type id. + * @param string $bundle + * The bundle. + * @param \Drupal\Core\Session\AccountInterface $account + * The user account. + * + * @return bool + * TRUE if the user has permission; FALSE otherwise. + */ + protected function hasOperationPermission($op, $entity_type_id, $bundle, AccountInterface $account) { + switch ($op) { + case 'change content': + return $account->hasPermission("administer panelizer $entity_type_id $bundle content"); + + case 'change layout': + return $account->hasPermission("administer panelizer $entity_type_id $bundle layout"); + + case 'revert to default': + return $account->hasPermission("administer panelizer $entity_type_id $bundle revert"); + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function hasEntityPermission($op, EntityInterface $entity, $view_mode, AccountInterface $account = NULL) { + if (!$account) { + $account = $this->currentUser->getAccount(); + } + + // Must be able to edit the entity. + if (!$entity->access('update', $account)) { + return FALSE; + } + + // Must have overrides enabled. + $panelizer_settings = $this->getPanelizerSettings($entity->getEntityTypeId(), $entity->bundle(), $view_mode); + if (empty($panelizer_settings['custom'])) { + return FALSE; + } + + // Check admin permission. + if ($account->hasPermission('administer panelizer')) { + return TRUE; + } + + // @todo: check field access too! + + // if ($op == 'revert to default') { + // // We already have enough permissions at this point. + // return TRUE; + // } + + return $this->hasOperationPermission($op, $entity->getEntityTypeId(), $entity->bundle(), $account); + } + + /** + * {@inheritdoc} + */ + public function hasDefaultPermission($op, $entity_type_id, $bundle, $view_mode, $default, AccountInterface $account = NULL) { + if (!$this->isPanelized($entity_type_id, $bundle, $view_mode)) { + return FALSE; + } + + if (!$account) { + $account = $this->currentUser->getAccount(); + } + + // Check admin permissions. + if ($account->hasPermission('administer panelizer')) { + return TRUE; + } + if ($account->hasPermission("administer panelizer $entity_type_id $bundle defaults")) { + return TRUE; + } + + return $this->hasOperationPermission($op, $entity_type_id, $bundle, $account); + } + +} diff --git a/docroot/modules/contrib/panelizer/src/PanelizerEntityViewBuilder.php b/docroot/modules/contrib/panelizer/src/PanelizerEntityViewBuilder.php new file mode 100644 index 000000000..40c734fec --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/PanelizerEntityViewBuilder.php @@ -0,0 +1,412 @@ +entityTypeId = $entity_type->id(); + $this->entityType = $entity_type; + $this->entityTypeManager = $entity_type_manager; + $this->moduleHandler = $module_handler; + $this->panelizer = $panelizer; + $this->panelizerManager = $panelizer_manager; + $this->panelsManager = $panels_manager; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('entity_type.manager'), + $container->get('module_handler'), + $container->get('panelizer'), + $container->get('plugin.manager.panelizer_entity'), + $container->get('panels.display_manager') + ); + } + + /** + * Get the Panelizer entity plugin. + * + * @return \Drupal\panelizer\Plugin\PanelizerEntityInterface|FALSE + */ + protected function getPanelizerPlugin() { + if (!isset($this->panelizerPlugin)) { + if (!$this->panelizerManager->hasDefinition($this->entityTypeId)) { + $this->panelizerPlugin = FALSE; + } + else { + $this->panelizerPlugin = $this->panelizerManager->createInstance($this->entityTypeId, []); + } + } + + return $this->panelizerPlugin; + } + + /** + * Check if Panelizer should be used for building this display. + * + * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display + * The display we're building. + * + * @return bool + */ + protected function isPanelizerEnabled(EntityViewDisplayInterface $display) { + return $display->getThirdPartySetting('panelizer', 'enable', FALSE); + } + + /** + * Gets the original view builder for this entity. + * + * @return \Drupal\Core\Entity\EntityViewBuilderInterface + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + */ + protected function getFallbackViewBuilder() { + return $this->entityTypeManager->getHandler($this->entityTypeId, 'fallback_view_builder'); + } + + /** + * Get the Panels display out of an the entity view display + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display + * The display. + * @param $view_mode + * The view mode. + * + * @return \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant + * The Panels display. + */ + protected function getPanelsDisplay(EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) { + return $this->panelizer->getPanelsDisplay($entity, $view_mode, $display); + } + + /** + * Returns the display objects used to render a set of entities. + * + * Wraps EntityViewDisplay::collectRenderDisplays() so we can mock it in + * tests. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface[] $entities + * The entities being rendered. They should all be of the same entity type. + * @param string $view_mode + * The view mode being rendered. + * + * @return \Drupal\Core\Entity\Display\EntityViewDisplayInterface[] + * The display objects to use to render the entities, keyed by entity + * bundle. + * + * @see EntityViewDisplay::collectRenderDisplays() + */ + protected function collectRenderDisplays($entities, $view_mode) { + return EntityViewDisplay::collectRenderDisplays($entities, $view_mode); + } + + /** + * Returns the entity context. + * + * Wraps creating new Context objects to avoid typed data in tests. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + * + * @return \Drupal\Core\Plugin\Context\Context + * The context. + */ + protected function getEntityContext(EntityInterface $entity) { + return new AutomaticContext(new ContextDefinition('entity:' . $this->entityTypeId, NULL, TRUE), $entity); + } + + /* + * Methods from EntityViewBuilderInterface. + */ + + /** + * {@inheritdoc} + */ + public function buildComponents(array &$build, array $entities, array $displays, $view_mode) { + $fallback_view_builder = $this->getFallbackViewBuilder(); + + $panelized_entities = []; + $fallback_entities = []; + /** + * @var string $id + * @var \Drupal\Core\Entity\EntityInterface $entity + */ + foreach ($entities as $id => $entity) { + $display = $displays[$entity->bundle()]; + if ($this->isPanelizerEnabled($display)) { + $panelized_entities[$id] = $entity; + } + else { + $fallback_entities[$id] = $entity; + } + } + + // Handle all the fallback entities first! + if (!empty($fallback_entities)) { + $fallback_view_builder->buildComponents($build, $fallback_entities, $displays, $view_mode); + } + + // Handle the panelized entities. + if (!empty($panelized_entities)) { + $this->moduleHandler + ->invokeAll('entity_prepare_view', [ + $this->entityTypeId, + $panelized_entities, + $displays, + $view_mode + ]); + } + } + + /** + * {@inheritdoc} + */ + public function view(EntityInterface $entity, $view_mode = 'full', $langcode = NULL) { + // Trigger hook_panelizer_pre_view_builder_alter(). + $this->moduleHandler->alter('panelizer_pre_view_builder', $view_mode, $entity, $langcode); + + $displays = $this->collectRenderDisplays([$entity], $view_mode); + $display = $displays[$entity->bundle()]; + + if (!$this->isPanelizerEnabled($display)) { + return $this->getFallbackViewBuilder()->view($entity, $view_mode, $langcode); + } + + $build = $this->buildMultiplePanelized([$entity->id() => $entity], $displays, $view_mode, $langcode); + return $build[$entity->id()]; + } + + /** + * {@inheritdoc} + */ + public function viewMultiple(array $entities = [], $view_mode = 'full', $langcode = NULL) { + $displays = $this->collectRenderDisplays($entities, $view_mode); + + $panelized_entities = []; + $fallback_entities = []; + foreach ($entities as $id => $entity) { + $display = $displays[$entity->bundle()]; + if ($this->isPanelizerEnabled($display)) { + $panelized_entities[$id] = $entity; + } + else { + $fallback_entities[$id] = $entity; + } + } + + $result = []; + if (!empty($fallback_entities)) { + $result += $this->getFallbackViewBuilder()->viewMultiple($fallback_entities, $view_mode, $langcode); + } + if (!empty($panelized_entities)) { + $result += $this->buildMultiplePanelized($panelized_entities, $displays, $view_mode, $langcode); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function resetCache(array $entities = NULL) { + $this->getFallbackViewBuilder()->resetCache($entities); + } + + /** + * {@inheritdoc} + */ + public function viewField(FieldItemListInterface $items, $display_options = []) { + return $this->getFallbackViewBuilder()->viewfield($items, $display_options); + } + + /** + * {@inheritdoc} + */ + public function viewFieldItem(FieldItemInterface $item, $display = []) { + return $this->getFallbackViewBuilder()->viewFieldItem($item, $display); + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + return $this->getFallbackViewBuilder()->getCacheTags(); + } + + /* + * Methods for actually rendering the Panelized entities. + */ + + /** + * Build the render array for a list of panelized entities. + * + * @param \Drupal\Core\Entity\EntityInterface[] $entities + * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface[] $displays + * @param string $view_mode + * @param string|NULL $langcode + * + * @return array + */ + protected function buildMultiplePanelized(array $entities, array $displays, $view_mode, $langcode) { + $build = []; + + foreach ($entities as $id => $entity) { + $panels_display = $this->panelizer->getPanelsDisplay($entity, $view_mode, $displays[$entity->bundle()]); + $settings = $this->panelizer->getPanelizerSettings($entity->getEntityTypeId(), $entity->bundle(), $view_mode, $displays[$entity->bundle()]); + $panels_display->setContexts($this->panelizer->getDisplayStaticContexts($settings['default'], $entity->getEntityTypeId(), $entity->bundle(), $view_mode, $displays[$entity->bundle()])); + $build[$id] = $this->buildPanelized($entity, $panels_display, $view_mode, $langcode); + + // Allow modules to modify the render array. + $alter_types = [ + "{$this->entityTypeId}_view", + 'entity_view', + ]; + $this->moduleHandler->alter($alter_types, $build[$id], $entity, $displays[$entity->bundle()]); + } + + return $build; + } + + /** + * Build the render array for a single panelized entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * @param \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $panels_display + * @param string $view_mode + * @param string $langcode + * + * @return array + */ + protected function buildPanelized(EntityInterface $entity, PanelsDisplayVariant $panels_display, $view_mode, $langcode) { + $contexts = $panels_display->getContexts(); + $contexts['@panelizer.entity_context:entity'] = $this->getEntityContext($entity); + $panels_display->setContexts($contexts); + + $build = [ + '#theme' => [ + 'panelizer_view_mode__' . $this->entityTypeId . '__' . $entity->id(), + 'panelizer_view_mode__' . $this->entityTypeId . '__' . $entity->bundle(), + 'panelizer_view_mode__' . $this->entityTypeId, + 'panelizer_view_mode', + ], + '#panelizer_plugin' => $this->getPanelizerPlugin(), + '#panels_display' => $panels_display, + '#entity' => $entity, + '#view_mode' => $view_mode, + '#langcode' => $langcode, + 'content' => $panels_display->build(), + ]; + + if (isset($build['content']['#title'])) { + $build['#title'] = $build['content']['#title']; + } + + // @todo: I'm sure more is necessary to get the cache contexts right... + $entity_metadata = CacheableMetadata::createFromObject($entity); + CacheableMetadata::createFromObject($panels_display)->merge($entity_metadata)->applyTo($build); + + $this->getPanelizerPlugin()->alterBuild($build, $entity, $panels_display, $view_mode); + + return $build; + } + +} diff --git a/docroot/modules/contrib/panelizer/src/PanelizerInterface.php b/docroot/modules/contrib/panelizer/src/PanelizerInterface.php new file mode 100644 index 000000000..76a472cfe --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/PanelizerInterface.php @@ -0,0 +1,243 @@ +panelizerEntityManager = $panelizer_entity_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('plugin.manager.panelizer_entity') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + foreach ($this->panelizerEntityManager->getDefinitions() as $plugin_id => $definition) { + $this->derivatives["$plugin_id"] = $base_plugin_definition; + $this->derivatives["$plugin_id"]['appears_on'] = [ + "entity.entity_view_display.$plugin_id.default", + "entity.entity_view_display.$plugin_id.view_mode" + ]; + } + return parent::getDerivativeDefinitions($base_plugin_definition); + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Plugin/Field/FieldFormatter/PanelizerFormatter.php b/docroot/modules/contrib/panelizer/src/Plugin/Field/FieldFormatter/PanelizerFormatter.php new file mode 100644 index 000000000..7eb366660 --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Plugin/Field/FieldFormatter/PanelizerFormatter.php @@ -0,0 +1,61 @@ + $item) { + $elements[$delta] = [ + '#type' => 'item', + '#title' => $item->view_mode, + '#markup' => $this->viewValue($item), + ]; + } + + return $elements; + } + + /** + * Generate the output appropriate for one field item. + * + * @param \Drupal\Core\Field\FieldItemInterface $item + * One field item. + * + * @return string + * The textual output generated. + */ + protected function viewValue(FieldItemInterface $item) { + $description = ''; + if (!empty($item->default)) { + $description = $this->t('Using default called "@default"', ['@default' => $item->default]); + } + else { + $description = $this->t('Custom'); + } + return $description; + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Plugin/Field/FieldType/PanelizerFieldType.php b/docroot/modules/contrib/panelizer/src/Plugin/Field/FieldType/PanelizerFieldType.php new file mode 100644 index 000000000..8f56c1b87 --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Plugin/Field/FieldType/PanelizerFieldType.php @@ -0,0 +1,143 @@ +setLabel(new TranslatableMarkup('View mode')) + ->setSetting('case_sensitive', FALSE) + ->setRequired(TRUE); + $properties['default'] = DataDefinition::create('string') + ->setLabel(new TranslatableMarkup('Default name')) + ->setSetting('case_sensitive', FALSE) + ->setRequired(FALSE); + $properties['panels_display'] = MapDataDefinition::create('map') + ->setLabel(new TranslatableMarkup('Panels display')) + ->setRequired(FALSE); + + return $properties; + } + + /** + * @inheritDoc + */ + public static function mainPropertyName() { + return 'panels_display'; + } + + /** + * {@inheritdoc} + */ + public static function schema(FieldStorageDefinitionInterface $field_definition) { + $schema = [ + 'columns' => [ + 'view_mode' => [ + 'type' => 'varchar', + 'length' => '255', + 'binary' => FALSE, + ], + 'default' => [ + 'type' => 'varchar', + 'length' => '255', + 'binary' => FALSE, + ], + 'panels_display' => [ + 'type' => 'blob', + 'size' => 'normal', + 'serialize' => TRUE, + ], + ], + 'indexes' => [ + 'default' => ['default'], + ] + ]; + + return $schema; + } + + /** + * Returns the Panels display plugin manager. + * + * @return \Drupal\panels\PanelsDisplayManagerInterface + */ + protected static function getPanelsDisplayManager() { + return \Drupal::service('panels.display_manager'); + } + + /** + * {@inheritdoc} + */ + public static function generateSampleValue(FieldDefinitionInterface $field_definition) { + $panels_manager = static::getPanelsDisplayManager(); + $sample_display = $panels_manager->createDisplay(); + + $values['view_mode'] = 'default'; + $values['default'] = NULL; + $values['panels_display'] = $panels_manager->exportDisplay($sample_display); + return $values; + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + $panels_display = $this->get('panels_display')->getValue(); + $default = $this->get('default')->getValue(); + return empty($panels_display) && empty($default); + } + + /** + * {@inheritdoc} + */ + public function postSave($update) { + $panels_manager = $this->getPanelsDisplayManager(); + $panels_display_config = $this->get('panels_display')->getValue(); + + // If our field has custom panelizer display config data. + if (!empty($panels_display_config) && is_array($panels_display_config)) { + $panels_display = $panels_manager->importDisplay($panels_display_config, FALSE); + } + if (!empty($panels_display)) { + // Set the storage id to include the current revision id. + $entity = $this->getEntity(); + $storage_id_parts = [ + $entity->getEntityTypeId(), + $entity->id(), + $this->get('view_mode')->getValue() + ]; + if ($entity instanceof RevisionableInterface && $entity->getEntityType()->isRevisionable()) { + $storage_id_parts[] = $entity->getRevisionId(); + } + $panels_display->setStorage('panelizer_field', implode(':', $storage_id_parts)); + $this->set('panels_display', $panels_manager->exportDisplay($panels_display)); + + return TRUE; + } + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Plugin/Field/FieldWidget/PanelizerWidget.php b/docroot/modules/contrib/panelizer/src/Plugin/Field/FieldWidget/PanelizerWidget.php new file mode 100644 index 000000000..3d59b001d --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Plugin/Field/FieldWidget/PanelizerWidget.php @@ -0,0 +1,146 @@ +getEntity(); + $entity_type_id = $entity->getEntityTypeId(); + $entity_view_modes = $this->getEntityDisplayRepository()->getViewModes($entity_type_id); + + // Get the current values from the entity. + $values = []; + /** @var \Drupal\Core\Field\FieldItemInterface $item */ + foreach ($items as $item) { + $values[$item->view_mode] = [ + 'default' => $item->default, + 'panels_display' => $item->panels_display, + ]; + } + + // If any view modes are missing, then set the default. + $displays = []; + foreach ($entity_view_modes as $view_mode => $view_mode_info) { + $display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode); + $displays[$view_mode] = $display->getThirdPartySetting('panelizer', 'displays', []); + // If we don't have a value, or the default is __bundle_default__ and our + // panels_display is empty, set the default to __bundle_default__. + if (!isset($values[$view_mode]) || ($values[$view_mode]['default'] == '__bundle_default__' && empty($values[$view_mode]['panels_display']))) { + if ($display->getThirdPartySetting('panelizer', 'enable', FALSE)) { + $values[$view_mode] = [ + 'default' => '__bundle_default__', + 'panels_display' => [], + ]; + } + } + } + + // Add elements to the form for each view mode. + $delta = 0; + foreach ($values as $view_mode => $value) { + $element[$delta]['view_mode'] = [ + '#type' => 'value', + '#value' => $view_mode, + ]; + + $settings = $this->getPanelizer()->getPanelizerSettings($entity_type_id, $entity->bundle(), $view_mode); + if (!empty($settings['allow'])) { + // We default to this option when the user hasn't previous interacted + // with the field. + $options = [ + '__bundle_default__' => $this->t('Current default display'), + ]; + foreach ($displays[$view_mode] as $machine_name => $panels_display) { + $options[$machine_name] = $panels_display['label']; + } + $element[$delta]['default'] = [ + '#title' => $entity_view_modes[$view_mode]['label'], + '#type' => 'select', + '#options' => $options, + '#default_value' => $value['default'], + ]; + // If we have a value in panels_display, prevent the user from + // interacting with the widget for the view modes that are overridden. + if (!empty($value['panels_display'])) { + $element[$delta]['default']['#disabled'] = TRUE; + $element[$delta]['default']['#options'][$value['default']] = $this->t('Custom Override'); + } + } + else { + $element[$delta]['default'] = [ + '#type' => 'value', + '#value' => $value['default'], + ]; + } + + $element[$delta]['panels_display'] = [ + '#type' => 'value', + '#value' => $value['panels_display'], + ]; + + $delta++; + } + + return $element; + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Plugin/PanelizerEntity/PanelizerNode.php b/docroot/modules/contrib/panelizer/src/Plugin/PanelizerEntity/PanelizerNode.php new file mode 100644 index 000000000..aeec6f9d1 --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Plugin/PanelizerEntity/PanelizerNode.php @@ -0,0 +1,81 @@ +setPageTitle('[node:title]'); + + // Remove the 'title' block because it's covered already. + foreach ($panels_display->getRegionAssignments() as $region => $blocks) { + /** @var \Drupal\Core\Block\BlockPluginInterface[] $blocks */ + foreach ($blocks as $block_id => $block) { + if ($block->getPluginId() == 'entity_field:node:title') { + $panels_display->removeBlock($block_id); + } + } + } + + if ($display->getComponent('links')) { + // @todo: add block for node links. + } + + if ($display->getComponent('langcode')) { + // @todo: add block for node language. + } + + return $panels_display; + } + + /** + * {@inheritdoc} + */ + public function alterBuild(array &$build, EntityInterface $entity, PanelsDisplayVariant $panels_display, $view_mode) { + /** @var $entity \Drupal\node\Entity\Node */ + parent::alterBuild($build, $entity, $panels_display, $view_mode); + + if ($entity->id()) { + $build['#contextual_links']['node'] = [ + 'route_parameters' => ['node' => $entity->id()], + 'metadata' => ['changed' => $entity->getChangedTime()], + ]; + } + } + + /** + * {@inheritdoc} + */ + public function preprocessViewMode(array &$variables, EntityInterface $entity, PanelsDisplayVariant $panels_display, $view_mode) { + parent::preprocessViewMode($variables, $entity, $panels_display, $view_mode); + + /** @var \Drupal\node\NodeInterface $node */ + $node = $entity; + + // Add node specific CSS classes. + if ($node->isPromoted()) { + $variables['attributes']['class'][] = 'node--promoted'; + } + if ($node->isSticky()) { + $variables['attributes']['class'][] = 'node--sticky'; + } + if (!$node->isPublished()) { + $variables['attributes']['class'][] = 'node--unpublished'; + } + } + +} \ No newline at end of file diff --git a/docroot/modules/contrib/panelizer/src/Plugin/PanelizerEntity/PanelizerTerm.php b/docroot/modules/contrib/panelizer/src/Plugin/PanelizerEntity/PanelizerTerm.php new file mode 100644 index 000000000..a78a7d347 --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Plugin/PanelizerEntity/PanelizerTerm.php @@ -0,0 +1,52 @@ +setPageTitle('[term:name]'); + + // Remove the 'name' block because it's covered already. + foreach ($panels_display->getRegionAssignments() as $region => $blocks) { + /** @var \Drupal\Core\Block\BlockPluginInterface[] $blocks */ + foreach ($blocks as $block_id => $block) { + if ($block->getPluginId() == 'entity_field:taxonomy_term:name') { + $panels_display->removeBlock($block_id); + } + } + } + + return $panels_display; + } + + /** + * {@inheritdoc} + */ + public function alterBuild(array &$build, EntityInterface $entity, PanelsDisplayVariant $panels_display, $view_mode) { + /** @var $entity \Drupal\taxonomy\Entity\Term */ + parent::alterBuild($build, $entity, $panels_display, $view_mode); + + if ($entity->id()) { + $build['#contextual_links']['taxonomy_term'] = [ + 'route_parameters' => ['taxonomy_term' => $entity->id()], + 'metadata' => ['changed' => $entity->getChangedTime()], + ]; + } + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Plugin/PanelizerEntity/PanelizerUser.php b/docroot/modules/contrib/panelizer/src/Plugin/PanelizerEntity/PanelizerUser.php new file mode 100644 index 000000000..c2f84f213 --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Plugin/PanelizerEntity/PanelizerUser.php @@ -0,0 +1,76 @@ +setPageTitle('[user:name]'); + + // Remove the 'name' block because it's covered already. + foreach ($panels_display->getRegionAssignments() as $region => $blocks) { + /** @var \Drupal\Core\Block\BlockPluginInterface[] $blocks */ + foreach ($blocks as $block_id => $block) { + if ($block->getPluginId() == 'entity_field:user:name') { + $panels_display->removeBlock($block_id); + } + } + } + + if ($display->getComponent('member_for')) { + // @todo: add block for 'Member for'. + } + + return $panels_display; + } + + /** + * {@inheritdoc} + */ + public function alterBuild(array &$build, EntityInterface $entity, PanelsDisplayVariant $panels_display, $view_mode) { + /** @var $entity \Drupal\user\Entity\User */ + parent::alterBuild($build, $entity, $panels_display, $view_mode); + + if ($entity->id()) { + $build['#contextual_links']['user'] = [ + 'route_parameters' => ['user' => $entity->id()], + 'metadata' => ['changed' => $entity->getChangedTime()], + ]; + } + + // This function adds a default alt tag to the user_picture field to + // maintain accessibility. + if (user_picture_enabled() && !empty($build['content']['content'])) { + foreach (Element::children($build['content']['content']) as $key) { + if (isset($build['content']['content'][$key]['content']['field'])) { + foreach (Element::children($build['content']['content'][$key]['content']['field']) as $field_key) { + if ($build['content']['content'][$key]['content']['field']['#field_name'] == 'user_picture') { + if (empty($build['content']['content'][$key]['content']['field'][$field_key]['#item_attributes'])) { + $build['content']['content'][$key]['content']['field'][$field_key]['#item_attributes'] = [ + 'alt' => \Drupal::translation() + ->translate('Profile picture for user @username', ['@username' => $entity->getUsername()]) + ]; + } + } + } + } + } + } + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Plugin/PanelizerEntityBase.php b/docroot/modules/contrib/panelizer/src/Plugin/PanelizerEntityBase.php new file mode 100644 index 000000000..30a8b538f --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Plugin/PanelizerEntityBase.php @@ -0,0 +1,133 @@ +panelsManager = $panels_manager; + $this->entityFieldManager = $entity_field_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('panels.display_manager'), + $container->get('entity_field.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getDefaultDisplay(EntityViewDisplayInterface $display, $bundle, $view_mode) { + $panels_display = $this->panelsManager->createDisplay(); + + $panels_display->setConfiguration(['label' => $this->t('Default')] + $panels_display->getConfiguration()); + $panels_display->setLayout('layout_onecol'); + // @todo: For now we always use the IPE, but we should support not using the ipe. + $panels_display->setBuilder('ipe'); + $panels_display->setPattern('panelizer'); + + // Add all the visible fields to the Panel. + $entity_type_id = $this->getPluginId(); + /** + * @var string $field_name + * @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition + */ + foreach ($this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle) as $field_name => $field_definition) { + // Skip the Panelizer field. + if ($field_definition->getType() == 'panelizer') { + continue; + } + + if ($component = $display->getComponent($field_name)) { + $weight = $component['weight']; + unset($component['weight']); + + $panels_display->addBlock([ + 'id' => 'entity_field:' . $entity_type_id . ':' . $field_name, + 'label' => $field_definition->getLabel(), + 'provider' => 'ctools_block', + 'label_display' => '0', + 'formatter' => $component, + 'context_mapping' => [ + 'entity' => '@panelizer.entity_context:entity', + ], + 'region' => 'content', + 'weight' => $weight, + ]); + } + } + + return $panels_display; + } + + /** + * {@inheritdoc} + */ + public function alterBuild(array &$build, EntityInterface $entity, PanelsDisplayVariant $panels_display, $view_mode) { + // By default, do nothing! + } + + /** + * {@inheritdoc} + */ + public function preprocessViewMode(array &$variables, EntityInterface $entity, PanelsDisplayVariant $panels_display, $view_mode) { + $entity_type_id = $this->getPluginId(); + + // Add some default classes. + $variables['attributes']['class'][] = $entity_type_id; + $variables['attributes']['class'][] = $entity_type_id . '--type-' . $entity->bundle(); + $variables['attributes']['class'][] = $entity_type_id . '--view-mode-' . $view_mode; + $variables['attributes']['class'][] = 'clearfix'; + + // Don't render the title in the template + if ($view_mode == 'full') { + $variables['title'] = ''; + } + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Plugin/PanelizerEntityInterface.php b/docroot/modules/contrib/panelizer/src/Plugin/PanelizerEntityInterface.php new file mode 100644 index 000000000..189e2db3c --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Plugin/PanelizerEntityInterface.php @@ -0,0 +1,70 @@ +alterInfo('panelizer_entity_info'); + $this->setCacheBackend($cache_backend, 'panelizer_entity_plugins'); + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Plugin/PanelizerEntityManagerInterface.php b/docroot/modules/contrib/panelizer/src/Plugin/PanelizerEntityManagerInterface.php new file mode 100644 index 000000000..4b6d4716e --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Plugin/PanelizerEntityManagerInterface.php @@ -0,0 +1,13 @@ +t('Entity being panelized')); + $contexts['@panelizer.entity_context:entity'] = new AutomaticContext($entity_definition); + $user_definition = new ContextDefinition("entity:user", $this->t('Current user')); + $contexts['current_user'] = new Context($user_definition); + return $contexts + parent::getDefaultContexts($tempstore, $tempstore_id, $machine_name); + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Plugin/PanelsStorage/PanelizerDefaultPanelsStorage.php b/docroot/modules/contrib/panelizer/src/Plugin/PanelsStorage/PanelizerDefaultPanelsStorage.php new file mode 100644 index 000000000..738b98dd4 --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Plugin/PanelsStorage/PanelizerDefaultPanelsStorage.php @@ -0,0 +1,192 @@ +entityTypeManager = $entity_type_manager; + $this->panelizer = $panelizer; + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('panelizer') + ); + } + + /** + * Converts the storage id into its component parts. + * + * @param string $id + * The storage id. There are two formats that can potentially be used: + * - The first is the normal format that we actually store: + * "entity_type_id:bundle:view_mode:name" + * - The second is a special internal format we use in the IPE so we can + * correctly set context: + * "*entity_type_id:entity_id:view_mode:name" + * + * @return array + * An array with 4 or 5 items: + * - Entity type id: string + * - Bundle name: string + * - View mode: string + * - Default name: string + * - Entity: \Drupal\Core\Entity\EntityInterface|NULL + * + * @throws \Drupal\panelizer\Exception\PanelizerException + */ + protected function parseId($id) { + list ($entity_type_id, $part_two, $view_mode, $name) = explode(':', $id); + + if (strpos($entity_type_id, '*') === 0) { + $entity_type_id = substr($entity_type_id, 1); + $storage = $this->entityTypeManager->getStorage($entity_type_id); + if ($entity = $storage->load($part_two)) { + $bundle = $entity->bundle(); + } + else { + throw new PanelizerException("Unable to load $entity_type_id with id $part_two"); + } + } + else { + $entity = NULL; + $bundle = $part_two; + } + + return [$entity_type_id, $bundle, $view_mode, $name, $entity]; + } + + /** + * Returns the entity context. + * + * Wraps creating new Context objects to avoid typed data in tests. + * + * @param string $entity_type_id + * The entity type id. + * @param \Drupal\Core\Entity\EntityInterface|NULL $entity + * The entity. + * + * @return \Drupal\Core\Plugin\Context\Context[] + * The available contexts. + */ + protected function getEntityContext($entity_type_id, EntityInterface $entity = NULL) { + $contexts = []; + // Set a placeholder context so that the calling code knows that we need + // an entity context. If we have the value available, then we actually set + // the context value. + $contexts['@panelizer.entity_context:entity'] = new AutomaticContext(new ContextDefinition('entity:' . $entity_type_id, NULL, TRUE), $entity); + return $contexts; + } + + + + /** + * {@inheritdoc} + */ + public function load($id) { + try { + list ($entity_type_id, $bundle, $view_mode, $name, $entity) = $this->parseId($id); + if ($panels_display = $this->panelizer->getDefaultPanelsDisplay($name, $entity_type_id, $bundle, $view_mode)) { + $contexts = $this->getEntityContext($entity_type_id, $entity); + $contexts = $contexts + $this->panelizer->getDisplayStaticContexts($name, $entity_type_id, $bundle, $view_mode); + $panels_display->setContexts($contexts); + return $panels_display; + } + } + catch (PanelizerException $e) { + // Do nothing to fallback on returning NULL. + } + } + + /** + * {@inheritdoc} + */ + public function save(PanelsDisplayVariant $panels_display) { + $id = $panels_display->getStorageId(); + try { + list ($entity_type_id, $bundle, $view_mode, $name) = $this->parseId($id); + $this->panelizer->setDefaultPanelsDisplay($name, $entity_type_id, $bundle, $view_mode, $panels_display); + } + catch (PanelizerException $e) { + throw new \Exception("Couldn't find Panelizer default to store Panels display"); + } + } + + /** + * {@inheritdoc} + */ + public function access($id, $op, AccountInterface $account) { + try { + list ($entity_type_id, $bundle, $view_mode, $name) = $this->parseId($id); + } + catch (PanelizerException $e) { + return AccessResult::forbidden(); + } + + if ($panels_display = $this->panelizer->getDefaultPanelsDisplay($name, $entity_type_id, $bundle, $view_mode)) { + if ($op == 'change layout') { + if ($this->panelizer->hasDefaultPermission('change layout', $entity_type_id, $bundle, $view_mode, $name, $account)) { + return AccessResult::allowed(); + } + } + else if ($op == 'read' || $this->panelizer->hasDefaultPermission('change content', $entity_type_id, $bundle, $view_mode, $name, $account)) { + return AccessResult::allowed(); + } + } + + return AccessResult::forbidden(); + } + +} \ No newline at end of file diff --git a/docroot/modules/contrib/panelizer/src/Plugin/PanelsStorage/PanelizerFieldPanelsStorage.php b/docroot/modules/contrib/panelizer/src/Plugin/PanelsStorage/PanelizerFieldPanelsStorage.php new file mode 100644 index 000000000..ca946f80c --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Plugin/PanelsStorage/PanelizerFieldPanelsStorage.php @@ -0,0 +1,196 @@ +entityTypeManager = $entity_type_manager; + $this->panelizer = $panelizer; + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('panelizer') + ); + } + + /** + * Gets the underlying entity from storage. + * + * @param $id + * The storage service id. + * + * @return \Drupal\Core\Entity\EntityInterface|NULL + */ + protected function loadEntity($id) { + list ($entity_type, $id, , $revision_id) = array_pad(explode(':', $id), 4, NULL); + + $storage = $this->entityTypeManager->getStorage($entity_type); + if ($revision_id) { + $entity = $storage->loadRevision($revision_id); + } + else { + $entity = $storage->load($id); + } + + return $entity; + } + + /** + * Returns the entity context. + * + * Wraps creating new Context objects to avoid typed data in tests. + * + * @param string $entity_type_id + * The entity type id. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + * + * @return \Drupal\Core\Plugin\Context\Context + * The context. + */ + protected function getEntityContext($entity_type_id, EntityInterface $entity) { + return new AutomaticContext(new ContextDefinition('entity:' . $entity_type_id, NULL, TRUE), $entity); + } + + /** + * {@inheritdoc} + */ + public function load($id) { + if ($entity = $this->loadEntity($id)) { + list ($entity_type_id, , $view_mode) = explode(':', $id); + if ($panels_display = $this->panelizer->getPanelsDisplay($entity, $view_mode)) { + // Set the entity as a context on the Panels display. + $contexts = [ + '@panelizer.entity_context:entity' => $this->getEntityContext($entity_type_id, $entity), + ]; + $panels_display->setContexts($contexts); + return $panels_display; + } + } + } + + /** + * {@inheritdoc} + */ + public function save(PanelsDisplayVariant $panels_display) { + $id = $panels_display->getStorageId(); + if ($entity = $this->loadEntity($id)) { + list (,, $view_mode) = explode(':', $id); + // If we're dealing with an entity that has a documented default, we + // don't want to lose that information when we save our customizations. + // This enables us to revert to the correct default at a later date. + if ($entity instanceof FieldableEntityInterface) { + $default = NULL; + if ($entity->hasField('panelizer') && $entity->panelizer->first()) { + foreach ($entity->panelizer as $item) { + if ($item->view_mode == $view_mode) { + $default = $item->default; + break; + } + } + } + try { + $this->panelizer->setPanelsDisplay($entity, $view_mode, $default, $panels_display); + } + catch (PanelizerException $e) { + // Translate to expected exception type. + throw new \Exception($e->getMessage()); + } + } + } + else { + throw new \Exception("Couldn't find entity to store Panels display on"); + } + } + + /** + * {@inheritdoc} + */ + public function access($id, $op, AccountInterface $account) { + if ($entity = $this->loadEntity($id)) { + $access = AccessResult::neutral() + ->addCacheableDependency($account); + + // We do not support "create", as this method's interface dictates, + // because we work with existing entities here. + $entity_operations = [ + 'read' => 'view', + 'update' => 'update', + 'delete'=> 'delete', + 'change layout' => 'update', + ]; + // Do not add entity cacheability metadata to the forbidden result, + // because it depends on the Panels operation, and not on the entity. + $access->orIf(isset($entity_operations[$op]) ? $entity->access($entity_operations[$op], $account, TRUE) : AccessResult::forbidden()); + + if (!$access->isForbidden() && $entity instanceof FieldableEntityInterface) { + list (,, $view_mode) = explode(':', $id); + if ($op == 'change layout') { + if ($this->panelizer->hasEntityPermission('change layout', $entity, $view_mode, $account)) { + return $access->orIf(AccessResult::allowed()); + } + } + else if ($op == 'read' || $this->panelizer->hasEntityPermission('change content', $entity, $view_mode, $account)) { + return $access->orIf(AccessResult::allowed()); + } + } + } + + return AccessResult::forbidden(); + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Tests/Update/PanelizerLayoutIDUpdateTest.php b/docroot/modules/contrib/panelizer/src/Tests/Update/PanelizerLayoutIDUpdateTest.php new file mode 100644 index 000000000..96cddcd1b --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Tests/Update/PanelizerLayoutIDUpdateTest.php @@ -0,0 +1,41 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../tests/fixtures/update/drupal-8.panelizer.minimal.php.gz', + ]; + } + + /** + * Test updates. + */ + public function testUpdate() { + $this->runUpdates(); + + $this->drupalLogin($this->rootUser); + $this->drupalGet('admin/structure/types/manage/article/display'); + $this->clickLink('Edit', 1); + $this->assertResponse(200); + + $this->drupalGet('node/1'); + $this->assertResponse(200); + + $this->drupalGet('node/2'); + $this->assertResponse(200); + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Wizard/PanelizerAddWizard.php b/docroot/modules/contrib/panelizer/src/Wizard/PanelizerAddWizard.php new file mode 100644 index 000000000..85513e8ce --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Wizard/PanelizerAddWizard.php @@ -0,0 +1,72 @@ +set('machine_name_prefix', "{$entity_type_id}__{$bundle}__{$view_mode_name}"); + } + $form = parent::buildForm($form, $form_state); + $cached_values = $form_state->getTemporaryValue('wizard'); + $cached_values['id'] = $this->getMachineName(); + // Some variants like PanelsDisplayVariant need this. Set it to empty. + $cached_values['access'] = []; + $form_state->setTemporaryValue('wizard', $cached_values); + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $operations = array_map('strval', [ + $this->getNextOp(), + $this->t('Update'), + $this->t('Update and save'), + $this->t('Save'), + ]); + + if (in_array($form_state->getValue('op'), $operations)) { + $cached_values = $form_state->getTemporaryValue('wizard'); + if ($form_state->hasValue('label')) { + $config = $cached_values['plugin']->getConfiguration(); + $config['label'] = $form_state->getValue('label'); + $cached_values['plugin']->setConfiguration($config); + } + if ($form_state->hasValue('id')) { + $cached_values['id'] = $form_state->getValue('id'); + /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $plugin */ + $plugin = $cached_values['plugin']; + $plugin->setStorage($plugin->getStorageType(), $cached_values['id']); + } + } + parent::submitForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function finish(array &$form, FormStateInterface $form_state) { + parent::finish($form, $form_state); + $cached_values = $form_state->getTemporaryValue('wizard'); + $form_state->setRedirect('panelizer.wizard.edit', ['machine_name' => $cached_values['id'], 'step' => 'content']); + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Wizard/PanelizerEditWizard.php b/docroot/modules/contrib/panelizer/src/Wizard/PanelizerEditWizard.php new file mode 100644 index 000000000..fd68561b9 --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Wizard/PanelizerEditWizard.php @@ -0,0 +1,225 @@ +getMachineName(); + list($entity_type, $bundle, $view_mode, $display_id) = explode('__', $this->getMachineName()); + $panelizer = \Drupal::service('panelizer'); + // Load the panels display variant. + /** @var \Drupal\panelizer\Panelizer $panelizer */ + // @todo this $display_id looks all wrong to me since it's the name and view_mode. + $variant_plugin = $panelizer->getDefaultPanelsDisplay($display_id, $entity_type, $bundle, $view_mode); + $cached_values['plugin'] = $variant_plugin; + $cached_values['label'] = $cached_values['plugin']->getConfiguration()['label']; + + $display = $panelizer->getEntityViewDisplay($entity_type, $bundle, $view_mode); + $config = $display->getThirdPartySetting('panelizer', 'displays', []); + if (!empty($config[$display_id]['static_context'])) { + $cached_values['contexts'] = $config[$display_id]['static_context']; + } + return $cached_values; + } + + /** + * {@inheritdoc} + */ + protected function customizeForm(array $form, FormStateInterface $form_state) { + // The page actions. + $form['wizard_actions'] = [ + '#theme' => 'links', + '#links' => [], + '#attributes' => [ + 'class' => ['inline'], + ] + ]; + + // The tree of wizard steps. + $form['wizard_tree'] = [ + '#theme' => ['panelizer_wizard_tree'], + '#wizard' => $this, + '#cached_values' => $form_state->getTemporaryValue('wizard'), + ]; + + $form['#theme'] = 'panelizer_wizard_form'; + $form['#attached']['library'][] = 'panelizer/wizard_admin'; + $form = parent::customizeForm($form, $form_state); + return $form; + } + + /** + * {@inheritdoc} + */ + protected function actions(FormInterface $form_object, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + $operation = $this->getOperation($cached_values); + + $actions = []; + + $actions['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Update'), + '#validate' => [ + '::populateCachedValues', + [$form_object, 'validateForm'], + ], + '#submit' => [ + [$form_object, 'submitForm'], + ], + ]; + + $actions['update_and_save'] = [ + '#type' => 'submit', + '#value' => $this->t('Update and save'), + '#button_type' => 'primary', + '#validate' => [ + '::populateCachedValues', + [$form_object, 'validateForm'], + ], + '#submit' => [ + [$form_object, 'submitForm'], + ], + ]; + + $actions['finish'] = [ + '#type' => 'submit', + '#value' => $this->t('Save'), + '#validate' => [ + '::populateCachedValues', + [$form_object, 'validateForm'], + ], + '#submit' => [ + [$form_object, 'submitForm'], + ], + ]; + + $actions['cancel'] = [ + '#type' => 'submit', + '#value' => $this->t('Cancel'), + '#submit' => [ + '::clearTempstore' + ], + ]; + + // Add any submit or validate functions for the step and the global ones. + foreach (['submit', 'update_and_save', 'finish'] as $button) { + if (isset($operation['validate'])) { + $actions[$button]['#validate'] = array_merge($actions[$button]['#validate'], $operation['validate']); + } + $actions[$button]['#validate'][] = '::validateForm'; + if (isset($operation['submit'])) { + $actions[$button]['#submit'] = array_merge($actions[$button]['#submit'], $operation['submit']); + } + $actions[$button]['#submit'][] = '::submitForm'; + } + $actions['update_and_save']['#submit'][] = '::finish'; + $actions['finish']['#submit'][] = '::finish'; + + if ($form_state->get('ajax')) { + $cached_values = $form_state->getTemporaryValue('wizard'); + $ajax_parameters = $this->getNextParameters($cached_values); + $ajax_parameters['step'] = $this->getStep($cached_values); + $ajax_url = Url::fromRoute($this->getRouteName(), $ajax_parameters); + $ajax_options = [ + 'query' => $this->getRequest()->query->all() + [ + FormBuilderInterface::AJAX_FORM_REQUEST => TRUE, + ], + ]; + $actions['submit']['#ajax'] = [ + 'callback' => '::ajaxSubmit', + 'url' => $ajax_url, + 'options' => $ajax_options, + ]; + $actions['update_and_save']['#ajax'] = [ + 'callback' => '::ajaxFinish', + 'url' => $ajax_url, + 'options' => $ajax_options, + ]; + $actions['finish']['#ajax'] = [ + 'callback' => '::ajaxFinish', + 'url' => $ajax_url, + 'options' => $ajax_options, + ]; + } + + return $actions; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $operations = array_map('strval', [ + $this->getNextOp(), + $this->t('Update'), + $this->t('Update and save'), + $this->t('Save'), + ]); + + if (in_array($form_state->getValue('op'), $operations)) { + $cached_values = $form_state->getTemporaryValue('wizard'); + if ($form_state->hasValue('label')) { + $config = $cached_values['plugin']->getConfiguration(); + $config['label'] = $form_state->getValue('label'); + $cached_values['plugin']->setConfiguration($config); + } + if ($form_state->hasValue('id')) { + $cached_values['id'] = $form_state->getValue('id'); + } + if (is_null($this->machine_name) && !empty($cached_values['id'])) { + $this->machine_name = $cached_values['id']; + } + $this->getTempstore()->set($this->getMachineName(), $cached_values); + if (!$form_state->get('ajax')) { + $form_state->setRedirect($this->getRouteName(), $this->getNextParameters($cached_values)); + } + } + } + + /** + * Clears the temporary store. + * + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + */ + public function clearTempstore(array &$form, FormStateInterface $form_state) { + $this->getTempstore()->delete($this->getMachineName()); + list($entity_type_id, $bundle, $view_mode) = explode('__', $this->getMachineName()); + $bundle_entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id)->getBundleEntityType(); + if ($view_mode == 'default') { + $route = "entity.entity_view_display.{$entity_type_id}.default"; + $arguments = [ + $bundle_entity_type => $bundle, + ]; + } + else { + $route = "entity.entity_view_display.{$entity_type_id}.view_mode"; + $arguments = [ + $bundle_entity_type => $bundle, + 'view_mode_name' => $view_mode, + ]; + } + $form_state->setRedirect($route, $arguments); + } + +} diff --git a/docroot/modules/contrib/panelizer/src/Wizard/PanelizerWizardBase.php b/docroot/modules/contrib/panelizer/src/Wizard/PanelizerWizardBase.php new file mode 100644 index 000000000..ac446b4c8 --- /dev/null +++ b/docroot/modules/contrib/panelizer/src/Wizard/PanelizerWizardBase.php @@ -0,0 +1,168 @@ +getTemporaryValue('wizard'); + // Get the current form operation. + $operation = $this->getOperation($cached_values); + $operations = $this->getOperations($cached_values); + $default_operation = reset($operations); + + // Get the machine name. There are two ways we can get this data. + $storage = $form_state->getStorage(); + $prefix = isset($storage['machine_name_prefix']) ? $storage['machine_name_prefix'] : $form_state->getTemporaryValue('wizard')['id']; + + if ($operation['form'] == $default_operation['form']) { + // Create id and label form elements. + $form['name'] = [ + '#type' => 'fieldset', + '#attributes' => ['class' => ['fieldset-no-legend']], + '#title' => $this->getWizardLabel(), + ]; + $form['name']['label'] = [ + '#type' => 'textfield', + '#title' => $this->getMachineLabel(), + '#required' => TRUE, + '#size' => 32, + '#default_value' => !empty($cached_values['label']) ? $cached_values['label'] : '', + '#maxlength' => 255, + '#disabled' => !empty($cached_values['label']), + ]; + $form['name']['id'] = [ + '#type' => 'machine_name', + '#maxlength' => 128, + '#machine_name' => [ + 'source' => ['name', 'label'], + 'exists' => $this->exists(), + 'prefix' => $prefix, + ], + '#description' => $this->t('A unique machine-readable name for this display. It must only contain lowercase letters, numbers, and underscores.'), + '#default_value' => !empty($cached_values['id']) ? $cached_values['id'] : '', + '#disabled' => !empty($cached_values['id']), + ]; + } + return $form; + } + + /** + * {@inheritdoc} + */ + public function getWizardLabel() { + return $this->t('Wizard Information'); + } + + /** + * {@inheritdoc} + */ + public function getMachineLabel() { + return $this->t('Wizard name'); + } + + /** + * {@inheritdoc} + */ + public function exists() { + return '\Drupal\panelizer\Form\PanelizerWizardGeneralForm::validateMachineName'; + } + + /** + * {@inheritdoc} + */ + public function getOperations($cached_values) { + $operations = [ + 'general' => [ + 'form' => PanelizerWizardGeneralForm::class, + 'title' => $this->t('General settings'), + ], + 'contexts' => [ + 'form' => PanelizerWizardContextForm::class, + 'title' => $this->t('Contexts'), + ], + ]; + + // Add any wizard operations from the plugin itself. + foreach ($cached_values['plugin']->getWizardOperations($cached_values) as $name => $operation) { + $operations[$name] = $operation; + } + + // Change the class that manages the Content step. + if (isset($operations['content'])) { + //$operations['content']['form'] = PanelizerWizardContentForm::class; + } + + return $operations; + } + + public function initValues() { + $cached_values = parent::initValues(); + $cached_values['access'] = new PanelizerUIAccess(); + if (empty($cached_values['plugin'])) { + /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $plugin */ + $plugin = \Drupal::service('plugin.manager.display_variant')->createInstance('panels_variant'); + $plugin->setPattern('panelizer'); + $plugin->setBuilder('ipe'); + $plugin->setStorage('panelizer_default', 'TEMPORARY_STORAGE_ID'); + $cached_values['plugin'] = $plugin; + } + if (empty($cached_values['contexts'])) { + $cached_values['contexts'] = []; + } + return $cached_values; + } + + + /** + * {@inheritdoc} + */ + public function finish(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + + // Save the panels display mode and its custom settings as third party + // data of the display mode for this entity+bundle+display. + /** @var \Drupal\panelizer\Panelizer $panelizer */ + $panelizer = \Drupal::service('panelizer'); + /** @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface $invalidator */ + $invalidator = \Drupal::service('cache_tags.invalidator'); + list($entity_type, $bundle, $view_mode, $display_id) = explode('__', $cached_values['id']); + $panelizer->setDefaultPanelsDisplay($display_id, $entity_type, $bundle, $view_mode, $cached_values['plugin']); + $panelizer->setDisplayStaticContexts($display_id, $entity_type, $bundle, $view_mode, $cached_values['contexts']); + + parent::finish($form, $form_state); + $form_state->setRedirect('panelizer.wizard.edit', ['machine_name' => $cached_values['id']]); + $invalidator->invalidateTags(["panelizer_default:$entity_type:$bundle:$view_mode:$display_id"]); + } + + /** + * Wraps the context mapper. + * + * @return \Drupal\ctools\ContextMapperInterface + */ + protected function getContextMapper() { + return \Drupal::service('ctools.context_mapper'); + } + + /** + * {@inheritdoc} + */ + protected function getContexts($cached_values) { + return $this->getContextMapper()->getContextValues($cached_values['contexts']); + } + +} diff --git a/docroot/modules/contrib/panelizer/templates/panelizer-view-mode.html.twig b/docroot/modules/contrib/panelizer/templates/panelizer-view-mode.html.twig new file mode 100644 index 000000000..415b69190 --- /dev/null +++ b/docroot/modules/contrib/panelizer/templates/panelizer-view-mode.html.twig @@ -0,0 +1,42 @@ +{# +/** + * @file + * Template for a generic Panelizer view mode. + * + * Available variables: + * - entity: The entity with limited access to object properties and methods. + * - attributes: HTML attributes for the containing element. + * - content: All entity items. + * - entity_url: Direct URL of the current entity. + * - title: The title of the entity. + * - title_element: HTML element to use for the title (defaults to 'h2'). + * - title_attributes: Same as attributes, except applied to the main title + * tag that appears in the template. + * - title_prefix: Additional output populated by modules, intended to be + * displayed in front of the main title tag that appears in the template. + * - title_suffix: Additional output populated by modules, intended to be + * displayed after the main title tag that appears in the template. + * - view_mode: View mode; for example, "teaser" or "full". + * + * @see template_preprocess_panelizer_view_mode() + * + * @ingroup themeable + */ +#} + + {{ title_prefix }} + {% if title %} + <{{ title_element }}{{ title_attributes }}> + {% if entity_url %} + {{ title }} + {% else %} + {{ title }} + {% endif %} + + {% endif %} + {{ title_suffix }} + + + {{ content }} + + diff --git a/docroot/modules/contrib/panelizer/templates/panelizer-wizard-form.html.twig b/docroot/modules/contrib/panelizer/templates/panelizer-wizard-form.html.twig new file mode 100644 index 000000000..a6911624e --- /dev/null +++ b/docroot/modules/contrib/panelizer/templates/panelizer-wizard-form.html.twig @@ -0,0 +1,31 @@ +{# +/** + * @file + * Default theme implementation for a 'form' element. + * + * Available variables + * - attributes: A list of HTML attributes for the wrapper element. + * - children: The child elements of the form. + * + * @see template_preprocess_form() + * + * @ingroup themeable + */ +#} +
+
+ {{ form.wizard_actions }} +
+
+
+ {{ form.wizard_tree }} +
+
+ {{ form|without('wizard_actions', 'wizard_tree', 'actions') }} +
+
+ +
+ {{ form.actions }} +
+
diff --git a/docroot/modules/contrib/panelizer/templates/panelizer-wizard-tree.html.twig b/docroot/modules/contrib/panelizer/templates/panelizer-wizard-tree.html.twig new file mode 100644 index 000000000..7d48fcf7a --- /dev/null +++ b/docroot/modules/contrib/panelizer/templates/panelizer-wizard-tree.html.twig @@ -0,0 +1,47 @@ +{# +/** + * @file + * Default theme implementation to display wizard tree. + * + * Available variables: + * - step: The current step name. + * - tree: A nested list of menu items. Each menu item contains: + * - title: The menu link title. + * - url: The menu link url, instance of \Drupal\Core\Url + * - children: The menu item child items. + * - step: The name of the step. + * + * @ingroup themeable + */ +#} +{% import _self as panelizer %} + +{# + We call a macro which calls itself to render the full tree. + @see http://twig.sensiolabs.org/doc/tags/macro.html +#} +{{ panelizer.wizard_tree(tree, step, 0) }} + +{% macro wizard_tree(items, step, menu_level) %} + {% import _self as panelizer %} + {% if items %} +
    + {% for item in items %} +
  • + {% if item.url %} + {% if step is same as(item.step) %} + {{ link(item.title, item.url) }} + {% else %} + {{ link(item.title, item.url) }} + {% endif %} + {% else %} + {{ item.title }} + {% endif %} + {% if item.children %} + {{ panelizer.wizard_tree(item.children, step, menu_level + 1) }} + {% endif %} +
  • + {% endfor %} +
+ {% endif %} +{% endmacro %} diff --git a/docroot/modules/contrib/panelizer/tests/modules/panelizer_test/panelizer_test.info.yml b/docroot/modules/contrib/panelizer/tests/modules/panelizer_test/panelizer_test.info.yml new file mode 100644 index 000000000..93cbc930b --- /dev/null +++ b/docroot/modules/contrib/panelizer/tests/modules/panelizer_test/panelizer_test.info.yml @@ -0,0 +1,10 @@ +type: module +name: Panelizer Test +description: 'Required for Panelizer simpletests only.' +# core: 8.x + +# Information added by Drupal.org packaging script on 2017-04-28 +version: '8.x-4.0' +core: '8.x' +project: 'panelizer' +datestamp: 1493406088 diff --git a/docroot/modules/contrib/panelizer/tests/modules/panelizer_test/src/Plugin/Block/PanelizerTestBlock.php b/docroot/modules/contrib/panelizer/tests/modules/panelizer_test/src/Plugin/Block/PanelizerTestBlock.php new file mode 100644 index 000000000..212243fa7 --- /dev/null +++ b/docroot/modules/contrib/panelizer/tests/modules/panelizer_test/src/Plugin/Block/PanelizerTestBlock.php @@ -0,0 +1,26 @@ + 'Abracadabra', + ]; + } + +} \ No newline at end of file diff --git a/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerAddDefaultLinkTest.php b/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerAddDefaultLinkTest.php new file mode 100644 index 000000000..b8fb42571 --- /dev/null +++ b/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerAddDefaultLinkTest.php @@ -0,0 +1,67 @@ +drupalPlaceBlock('local_actions_block', [ + 'region' => 'content', + 'theme' => \Drupal::theme()->getActiveTheme()->getName(), + ]); + + $content_type = 'page'; + + // Log in the user. + $this->loginUser1(); + + // Create the content type. + $this->drupalCreateContentType(['type' => $content_type, 'name' => 'Page']); + + // Panelize the content type. + $this->panelize($content_type); + + // Confirm that the content type is now panelized. + $this->assertLink('Add a new Panelizer default display'); + + // Un-panelize the content type. + $this->unpanelize($content_type); + + // Confirm that the content type is no longer panelized. + $this->assertNoLink('Add a new Panelizer default display'); + } + +} diff --git a/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerDefaultsTest.php b/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerDefaultsTest.php new file mode 100644 index 000000000..421e1b107 --- /dev/null +++ b/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerDefaultsTest.php @@ -0,0 +1,96 @@ +install([$theme]); + \Drupal::service('theme_handler')->setDefault($theme); + + // Place the local actions block in the theme so that we can assert the + // presence of local actions and such. + $this->drupalPlaceBlock('local_actions_block', [ + 'region' => 'content', + 'theme' => $theme, + ]); + } + + public function test() { + $this->setupContentType(); + $this->loginUser1(); + + // Create an additional default layout so we can assert that it's available + // as an option when choosing the layout on the node form. + $default_id = $this->addPanelizerDefault(); + + $this->assertDefaultExists('page', 'default', $default_id); + + // The user should only be able to choose the layout if specifically allowed + // to (the panelizer[allow] checkbox in the view display configuration). By + // default, they aren't. + $this->drupalGet('node/add/page'); + $this->assertResponse(200); + $this->assertNoFieldByName('panelizer[0][default]'); + + // Enable layout selection and assert that all the expected fields show up. + $this->panelize('page', NULL, ['panelizer[allow]' => TRUE]); + $this->drupalGet('node/add/page'); + $this->assertResponse(200); + $view_modes = \Drupal::service('entity_display.repository')->getViewModes('node'); + $view_modes = array_filter($view_modes, function (array $view_mode) { + // View modes that are inheriting the default display (i.e., status is + // FALSE) will not show up unless they, too, are panelized. But in this + // test, we only panelized the default display. + return $view_mode['status'] == FALSE; + }); + for ($i = 0; $i < count($view_modes); $i++) { + $this->assertFieldByName("panelizer[{$i}][default]"); + $this->assertOption("edit-panelizer-{$i}-default", 'default'); + $this->assertOption("edit-panelizer-{$i}-default", $default_id); + } + + $this->deletePanelizerDefault('page', 'default', $default_id); + $this->assertDefaultNotExists('page', 'default', $default_id); + } + +} diff --git a/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerIpeTest.php b/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerIpeTest.php new file mode 100644 index 000000000..9b04138c7 --- /dev/null +++ b/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerIpeTest.php @@ -0,0 +1,378 @@ +rebuildAll(); + } + + /** + * The content type that will be tested against. + * + * @string + */ + protected $content_type = 'page'; + + /** + * Create a user with the required permissions. + * + * @param array $perms + * Any additiona permissions that need to be added. + * + * @return Drupal\user\Entity\User + * The user account that was created. + */ + protected function createAdminUser(array $perms = array()) { + $perms += [ + // From system. + 'access administration pages', + + // Content permissions. + 'access content', + 'administer content types', + 'administer nodes', + 'create page content', + 'edit any page content', + 'edit own page content', + + // From Field UI. + 'administer node display', + + // From Panels. + 'access panels in-place editing', + ]; + $this->verbose('
' . print_r($perms, TRUE) . '
'); + return $this->drupalCreateUser($perms); + } + + /** + * Test that the IPE functionality as user 1, which should cover all options. + */ + public function testAdminUser() { + $this->setupContentType($this->content_type); + + // Create a test node. + $node = $this->createTestNode(); + + // Log in as user 1. + $this->loginUser1(); + + // Load the test node. + $this->drupalGet('node/' . $node->id()); + $this->assertResponse(200); + + // Confirm the JSON Drupal settings are appropriate. + $drupalSettings = NULL; + $matches = []; + if (preg_match('@@', $this->getRawContent(), $matches)) { + $drupalSettings = Json::decode($matches[1]); + $this->verbose('
' . print_r($drupalSettings, TRUE) . '
'); + } + $this->assertNotNull($drupalSettings); + if (!empty($drupalSettings)) { + $this->assertTrue(isset($drupalSettings['panels_ipe'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['regions'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['layout'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['user_permission'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['panels_display'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['unsaved'])); + $this->assertTrue(isset($drupalSettings['panelizer'])); + $this->assertTrue(isset($drupalSettings['panelizer']['entity'])); + $this->assertTrue(isset($drupalSettings['panelizer']['entity']['entity_type_id'])); + $this->assertEqual($drupalSettings['panelizer']['entity']['entity_type_id'], 'node'); + $this->assertTrue(isset($drupalSettings['panelizer']['entity']['entity_id'])); + $this->assertEqual($drupalSettings['panelizer']['entity']['entity_id'], $node->id()); + $this->assertTrue(isset($drupalSettings['panelizer']['user_permission'])); + $this->assertTrue(isset($drupalSettings['panelizer']['user_permission']['revert'])); + $this->assertTrue(isset($drupalSettings['panelizer']['user_permission']['save_default'])); + } + } + + /** + * Confirm the 'administer panelizer' permission works. + */ + public function testAdministerPanelizerPermission() { + $this->setupContentType($this->content_type); + + // Create a test node. + $node = $this->createTestNode(); + + // Create a new user with the permissions being tested. + $perms = [ + 'administer panelizer', + ]; + $account = $this->createAdminUser($perms); + $this->drupalLogin($account); + + // Load the test node. + $this->drupalGet('node/' . $node->id()); + $this->assertResponse(200); + + // Confirm the appropriate DOM structures are present for the IPE. + $drupalSettings = NULL; + $matches = []; + if (preg_match('@@', $this->getRawContent(), $matches)) { + $drupalSettings = Json::decode($matches[1]); + $this->verbose('
' . print_r($drupalSettings, TRUE) . '
'); + } + $this->assertNotNull($drupalSettings); + if (!empty($drupalSettings)) { + $this->assertTrue(isset($drupalSettings['panels_ipe'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['regions'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['layout'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['user_permission'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['panels_display'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['unsaved'])); + $this->assertTrue(isset($drupalSettings['panelizer'])); + $this->assertTrue(isset($drupalSettings['panelizer']['entity'])); + $this->assertTrue(isset($drupalSettings['panelizer']['entity']['entity_type_id'])); + $this->assertEqual($drupalSettings['panelizer']['entity']['entity_type_id'], 'node'); + $this->assertTrue(isset($drupalSettings['panelizer']['entity']['entity_id'])); + $this->assertEqual($drupalSettings['panelizer']['entity']['entity_id'], $node->id()); + $this->assertTrue(isset($drupalSettings['panelizer']['user_permission'])); + $this->assertTrue(isset($drupalSettings['panelizer']['user_permission']['revert'])); + $this->assertTrue(isset($drupalSettings['panelizer']['user_permission']['save_default'])); + $this->assertTrue($drupalSettings['panelizer']['user_permission']['revert']); + $this->assertTrue($drupalSettings['panelizer']['user_permission']['save_default']); + } + } + + /** + * @todo Confirm the 'set panelizer default' permission works. + */ + // public function testSetDefault() { + // } + + /** + * @todo Confirm the 'administer panelizer $entity_type_id $bundle defaults' + * permission works. + */ + // public function testAdministerEntityDefaults() { + // } + + /** + * @todo Confirm the 'administer panelizer $entity_type_id $bundle content' + * permission works. + */ + public function testAdministerEntityContentPermission() { + $this->setupContentType($this->content_type); + + // Need the node for the tests below, so create it now. + $node = $this->createTestNode(); + + $perms = [ + 'administer panelizer node page content', + ]; + $drupalSettings = $this->setupPermissionTests($perms, $node); + $this->assertNotNull($drupalSettings); + + // @todo How to tell if the user can change the display or add new items vs + // other tasks? + if (!empty($drupalSettings)) { + $this->assertTrue(isset($drupalSettings['panels_ipe'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['regions'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['layout'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['user_permission'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['panels_display'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['unsaved'])); + $this->assertTrue(isset($drupalSettings['panelizer'])); + $this->assertTrue(isset($drupalSettings['panelizer']['entity'])); + $this->assertTrue(isset($drupalSettings['panelizer']['entity']['entity_type_id'])); + $this->assertEqual($drupalSettings['panelizer']['entity']['entity_type_id'], 'node'); + $this->assertTrue(isset($drupalSettings['panelizer']['entity']['entity_id'])); + $this->assertEqual($drupalSettings['panelizer']['entity']['entity_id'], $node->id()); + $this->assertTrue(isset($drupalSettings['panelizer']['user_permission'])); + $this->assertTrue(isset($drupalSettings['panelizer']['user_permission']['revert'])); + $this->assertTrue(isset($drupalSettings['panelizer']['user_permission']['save_default'])); + } + } + + /** + * @todo Confirm the 'administer panelizer $entity_type_id $bundle layout' + * permission works. + */ + public function testAdministerEntityLayoutPermission() { + $this->setupContentType($this->content_type); + + // Need the node for the tests below, so create it now. + $node = $this->createTestNode(); + + // Test with just the 'layout' permission + $perms = [ + 'administer panelizer node page layout', + ]; + $drupalSettings = $this->setupPermissionTests($perms, $node); + $this->assertNotNull($drupalSettings); + + if (!empty($drupalSettings)) { + $this->assertFalse(isset($drupalSettings['panels_ipe'])); + $this->assertFalse(isset($drupalSettings['panelizer'])); + } + + // Make sure the user is logged out before doing another pass. + $this->drupalLogout(); + + // Test with the 'revert' and the 'content' permission. + $perms = [ + // The permission to be tested. + 'administer panelizer node page layout', + // This permission has to be enabled for the 'revert' permission to work. + 'administer panelizer node page content', + ]; + $drupalSettings = $this->setupPermissionTests($perms, $node); + $this->assertNotNull($drupalSettings); + + // @todo How to tell if the user can change the layout vs other tasks? + if (!empty($drupalSettings)) { + $this->assertTrue(isset($drupalSettings['panels_ipe'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['regions'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['layout'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['user_permission'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['panels_display'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['unsaved'])); + $this->assertTrue(isset($drupalSettings['panelizer'])); + $this->assertTrue(isset($drupalSettings['panelizer']['entity'])); + $this->assertTrue(isset($drupalSettings['panelizer']['entity']['entity_type_id'])); + $this->assertEqual($drupalSettings['panelizer']['entity']['entity_type_id'], 'node'); + $this->assertTrue(isset($drupalSettings['panelizer']['entity']['entity_id'])); + $this->assertEqual($drupalSettings['panelizer']['entity']['entity_id'], $node->id()); + $this->assertTrue(isset($drupalSettings['panelizer']['user_permission'])); + $this->assertTrue(isset($drupalSettings['panelizer']['user_permission']['revert'])); + $this->assertTrue(isset($drupalSettings['panelizer']['user_permission']['save_default'])); + $this->assertFalse($drupalSettings['panelizer']['user_permission']['revert']); + $this->assertFalse($drupalSettings['panelizer']['user_permission']['save_default']); + } + } + + /** + * @todo Confirm the 'administer panelizer $entity_type_id $bundle revert' + * permission works. + */ + public function testAdministerEntityRevertPermission() { + $this->setupContentType($this->content_type); + + // Need the node for the tests below, so create it now. + $node = $this->createTestNode(); + + // Test with just the 'revert' permission + $perms = [ + 'administer panelizer node page revert', + ]; + $drupalSettings = $this->setupPermissionTests($perms, $node); + $this->assertNotNull($drupalSettings); + + if (!empty($drupalSettings)) { + $this->assertFalse(isset($drupalSettings['panels_ipe'])); + $this->assertFalse(isset($drupalSettings['panelizer'])); + } + + // Make sure the user is logged out before doing another pass. + $this->drupalLogout(); + + // Test with the 'revert' and the 'content' permission. + $perms = [ + // The permission to be tested. + 'administer panelizer node page revert', + // This permission has to be enabled for the 'revert' permission to work. + 'administer panelizer node page content', + ]; + $drupalSettings = $this->setupPermissionTests($perms, $node); + $this->assertNotNull($drupalSettings); + + if (!empty($drupalSettings)) { + $this->assertTrue(isset($drupalSettings['panels_ipe'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['regions'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['layout'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['user_permission'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['panels_display'])); + $this->assertTrue(isset($drupalSettings['panels_ipe']['unsaved'])); + $this->assertTrue(isset($drupalSettings['panelizer'])); + $this->assertTrue(isset($drupalSettings['panelizer']['entity'])); + $this->assertTrue(isset($drupalSettings['panelizer']['entity']['entity_type_id'])); + $this->assertEqual($drupalSettings['panelizer']['entity']['entity_type_id'], 'node'); + $this->assertTrue(isset($drupalSettings['panelizer']['entity']['entity_id'])); + $this->assertEqual($drupalSettings['panelizer']['entity']['entity_id'], $node->id()); + $this->assertTrue(isset($drupalSettings['panelizer']['user_permission'])); + $this->assertTrue(isset($drupalSettings['panelizer']['user_permission']['revert'])); + $this->assertTrue(isset($drupalSettings['panelizer']['user_permission']['save_default'])); + $this->assertTrue($drupalSettings['panelizer']['user_permission']['revert']); + $this->assertFalse($drupalSettings['panelizer']['user_permission']['save_default']); + } + } + + /** + * Do the necessary setup work for the individual permissions tests. + * + * @param array $perms + * Any additiona permissions that need to be added. + * @param obj $node + * The node to test against, if none provided one will be generated. + * + * @return array + * The full drupalSettings JSON structure in array format. + */ + protected function setupPermissionTests(array $perms, $node = NULL) { + // Create a new user with the permissions being tested. + $account = $this->createAdminUser($perms); + $this->drupalLogin($account); + + // Make sure there's a test node to work with. + if (empty($node)) { + $node = $this->createTestNode(); + } + + // Load the test node. + $this->drupalGet('node/' . $node->id()); + $this->assertResponse(200); + + // Extract the drupalSettings structure and return it. + $drupalSettings = NULL; + $matches = []; + if (preg_match('@@', $this->getRawContent(), $matches)) { + $drupalSettings = Json::decode($matches[1]); + $this->verbose('
' . print_r($drupalSettings, TRUE) . '
'); + } + return $drupalSettings; + } + +} diff --git a/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerNodeFunctionalTest.php b/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerNodeFunctionalTest.php new file mode 100644 index 000000000..fe57812ea --- /dev/null +++ b/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerNodeFunctionalTest.php @@ -0,0 +1,163 @@ +setupContentType(); + $this->loginUser1(); + $this->panelize('page', NULL, ['panelizer[custom]' => TRUE]); + } + + /** + * Tests the admin interface to set a default layout for a bundle. + */ + public function testWizardUI() { + // Enter the wizard. + $this->drupalGet('admin/structure/panelizer/edit/node__page__default__default'); + $this->assertResponse(200); + $this->assertText('Wizard Information'); + $this->assertField('edit-label'); + + // Contexts step. + $this->clickLink('Contexts'); + $this->assertText('@panelizer.entity_context:entity', 'The current entity context is present.'); + + // Layout selection step. + $this->clickLink('Layout'); + $this->assertSession()->buttonExists('edit-update-layout'); + + // Content step. Add the Node block to the top region. + $this->clickLink('Content'); + $this->clickLink('Add new block'); + $this->clickLink('Title'); + $edit = [ + 'region' => 'content', + ]; + $this->drupalPostForm(NULL, $edit, t('Add block')); + $this->assertResponse(200); + + // Finish the wizard. + $this->drupalPostForm(NULL, [], t('Update and save')); + $this->assertResponse(200); + // Confirm this returned to the main wizard page. + $this->assertText('Wizard Information'); + $this->assertField('edit-label'); + + // Return to the Manage Display page, which is where the Cancel button + // currently sends you. That's a UX WTF and should be fixed... + $this->drupalPostForm(NULL, [], t('Cancel')); + $this->assertResponse(200); + + // Confirm the page is back to the content type settings page. + $this->assertFieldChecked('edit-panelizer-custom'); + + // Now change and save the general setting. + $edit = [ + 'panelizer[custom]' => FALSE, + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertResponse(200); + $this->assertNoFieldChecked('edit-panelizer-custom'); + + // Add another block at the Content step and then save changes. + $this->drupalGet('admin/structure/panelizer/edit/node__page__default__default/content'); + $this->assertResponse(200); + $this->clickLink('Add new block'); + $this->clickLink('Body'); + $edit = [ + 'region' => 'content', + ]; + $this->drupalPostForm(NULL, $edit, t('Add block')); + $this->assertResponse(200); + $this->assertText('entity_field:node:body', 'The body block was added successfully.'); + $this->drupalPostForm(NULL, [], t('Save')); + $this->assertResponse(200); + $this->clickLink('Content'); + $this->assertText('entity_field:node:body', 'The body block was saved successfully.'); + + // Check that the Manage Display tab changed now that Panelizer is set up. + // Also, the field display table should be hidden. + $this->assertNoRaw('
'); + + // Disable Panelizer for the default display mode. This should bring back + // the field overview table at Manage Display and not display the link to + // edit the default Panelizer layout. + $this->unpanelize('page'); + $this->assertNoLinkByHref('admin/structure/panelizer/edit/node__page__default'); + $this->assertRaw('
'); + } + + /** + * Tests rendering a node with Panelizer default. + */ + public function testPanelizerDefault() { + /** @var \Drupal\panelizer\PanelizerInterface $panelizer */ + $panelizer = $this->container->get('panelizer'); + $displays = $panelizer->getDefaultPanelsDisplays('node', 'page', 'default'); + $display = $displays['default']; + $display->addBlock([ + 'id' => 'panelizer_test', + 'label' => 'Panelizer test', + 'provider' => 'block_content', + 'region' => 'content', + ]); + $panelizer->setDefaultPanelsDisplay('default', 'node', 'page', 'default', $display); + + // Create a node, and check that the IPE is visible on it. + $node = $this->drupalCreateNode(['type' => 'page']); + $out = $this->drupalGet('node/' . $node->id()); + $this->assertResponse(200); + $this->verbose($out); + $elements = $this->xpath('//*[@id="panels-ipe-content"]'); + if (is_array($elements)) { + $this->assertIdentical(count($elements), 1); + } + else { + $this->fail('Could not parse page content.'); + } + + // Check that the block we added is visible. + $this->assertText('Panelizer test'); + $this->assertText('Abracadabra'); + } + +} diff --git a/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerNodeTranslationsTest.php b/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerNodeTranslationsTest.php new file mode 100644 index 000000000..714f92e68 --- /dev/null +++ b/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerNodeTranslationsTest.php @@ -0,0 +1,200 @@ +loginUser1(); + } + + /** + * The entity type being tested. + * + * @var string + */ + protected $entityTypeId = 'node'; + + /** + * The bundle being tested. + * + * @var string + */ + protected $bundle = 'page'; + + /** + * Tests the admin interface to set a default layout for a bundle. + */ + public function _testWizardUI() { + $this->panelize($this->bundle, NULL, ['panelizer[custom]' => TRUE]); + + // Enter the wizard. + $this->drupalGet("admin/structure/panelizer/edit/{$this->entityTypeId}__{$this->bundle}__default__default"); + $this->assertResponse(200); + $this->assertText('Wizard Information'); + $this->assertField('edit-label'); + + // Contexts step. + $this->clickLink('Contexts'); + $this->assertText('@panelizer.entity_context:entity', 'The current entity context is present.'); + + // Layout selection step. + $this->clickLink('Layout'); + $this->assertSession()->buttonExists('edit-update-layout'); + + // Content step. Add the Node block to the top region. + // @todo The index will have to change if the install profile is changed. + $this->clickLink('Content', 1); + $this->clickLink('Add new block'); + $this->clickLink('Title'); + $edit = [ + 'region' => 'content', + ]; + $this->drupalPostForm(NULL, $edit, t('Add block')); + $this->assertResponse(200); + + // Finish the wizard. + $this->drupalPostForm(NULL, [], t('Update and save')); + $this->assertResponse(200); + // Confirm this returned to the main wizard page. + $this->assertText('Wizard Information'); + $this->assertField('edit-label'); + + // Return to the Manage Display page, which is where the Cancel button + // currently sends you. That's a UX WTF and should be fixed... + $this->drupalPostForm(NULL, [], t('Cancel')); + $this->assertResponse(200); + + // Confirm the page is back to the content type settings page. + $this->assertFieldChecked('edit-panelizer-custom'); + + // Now change and save the general setting. + $edit = [ + 'panelizer[custom]' => FALSE, + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertResponse(200); + $this->assertNoFieldChecked('edit-panelizer-custom'); + + // Add another block at the Content step and then save changes. + $this->drupalGet("admin/structure/panelizer/edit/{$this->entityTypeId}__{$this->bundle}__default__default/content"); + $this->assertResponse(200); + $this->clickLink('Add new block'); + $this->clickLink('Body'); + $edit = [ + 'region' => 'content', + ]; + $this->drupalPostForm(NULL, $edit, t('Add block')); + $this->assertResponse(200); + $this->assertText("entity_field:{$this->entityTypeId}:body", 'The body block was added successfully.'); + $this->drupalPostForm(NULL, [], t('Save')); + $this->assertResponse(200); + $this->clickLink('Content', 1); + $this->assertText("entity_field:{$this->entityTypeId}:body", 'The body block was saved successfully.'); + + // Check that the Manage Display tab changed now that Panelizer is set up. + // Also, the field display table should be hidden. + $this->assertNoRaw('
'); + + // Disable Panelizer for the default display mode. This should bring back + // the field overview table at Manage Display and not display the link to + // edit the default Panelizer layout. + $this->unpanelize($this->bundle); + $this->assertNoLinkByHref("admin/structure/panelizer/edit/{$this->entityTypeId}__{$this->bundle}__default"); + $this->assertRaw('
'); + } + + /** + * Tests rendering a node with Panelizer default. + */ + public function testPanelizerDefault() { + $this->panelize($this->bundle, NULL, ['panelizer[custom]' => TRUE]); + /** @var \Drupal\panelizer\PanelizerInterface $panelizer */ + $panelizer = $this->container->get('panelizer'); + $displays = $panelizer->getDefaultPanelsDisplays($this->entityTypeId, $this->bundle, 'default'); + $display = $displays['default']; + $display->addBlock([ + 'id' => 'panelizer_test', + 'label' => 'Panelizer test', + 'provider' => 'block_content', + 'region' => 'content', + ]); + $panelizer->setDefaultPanelsDisplay('default', $this->entityTypeId, $this->bundle, 'default', $display); + + // Create a node, and check that the IPE is visible on it. + $node = $this->drupalCreateNode([ + 'type' => $this->bundle, + 'langcode' => [ + [ + 'value' => 'en', + ], + ], + ]); + $out = $this->drupalGet('node/' . $node->id()); + $this->assertResponse(200); + $this->verbose($out); + $elements = $this->xpath('//*[@id="panels-ipe-content"]'); + if (is_array($elements)) { + $this->assertIdentical(count($elements), 1); + } + else { + $this->fail('Could not parse page content.'); + } + + // Check that the block we added is visible. + $this->assertText('Panelizer test'); + $this->assertText('Abracadabra'); + + // Load the translation page. + $this->clickLink('Translate'); + $this->assertText('English (Original language)'); + $this->assertText('Published'); + $this->assertText('Not translated'); + } + + // @todo Confirm that the different languages of a translated node are loaded properly when using a default display. + // @todo Decide what should happen if a node is translated and has a customized display. + // @todo Confirm loading a referenced block uses the block's correct language. +} diff --git a/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerTermFunctionalTest.php b/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerTermFunctionalTest.php new file mode 100644 index 000000000..3467f7abb --- /dev/null +++ b/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerTermFunctionalTest.php @@ -0,0 +1,122 @@ + 'tags', + 'name' => 'Tags', + ])->save(); + + $user = $this->drupalCreateUser([ + 'administer taxonomy', + 'administer taxonomy_term display', + 'edit terms in tags', + 'administer panelizer', + 'access panels in-place editing', + 'administer taxonomy_term fields', + ]); + $this->drupalLogin($user); + + $this->drupalGet('admin/structure/taxonomy/manage/tags/overview/display'); + $edit = [ + 'panelizer[enable]' => TRUE, + 'panelizer[custom]' => TRUE, + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertResponse(200); + $this->rebuildAll(); + } + + /** + * Tests rendering a taxonomy term with Panelizer default. + */ + public function testPanelizerDefault() { + /** @var \Drupal\panelizer\PanelizerInterface $panelizer */ + $panelizer = \Drupal::service('panelizer'); + $displays = $panelizer->getDefaultPanelsDisplays('taxonomy_term', 'tags', 'default'); + $display = $displays['default']; + $display->addBlock([ + 'id' => 'panelizer_test', + 'label' => 'Panelizer test', + 'provider' => 'block_content', + 'region' => 'content', + ]); + $panelizer->setDefaultPanelsDisplay('default', 'taxonomy_term', 'tags', 'default', $display); + + // Create a term, and check that the IPE is visible on it. + $term = $this->createTerm(); + + $out = $this->drupalGet('taxonomy/term/' . $term->id()); + $this->assertResponse(200); + $this->verbose($out); + $elements = $this->xpath('//*[@id="panels-ipe-content"]'); + if (is_array($elements)) { + $this->assertIdentical(count($elements), 1); + } + else { + $this->fail('Could not parse page content.'); + } + + // Check that the block we added is visible. + $this->assertText('Panelizer test'); + $this->assertText('Abracadabra'); + } + + /** + * Create a term. + * + * @return Term; + */ + protected function createTerm() { + $settings = [ + 'description' => [['value' => $this->randomMachineName(32)]], + 'name' => $this->randomMachineName(8), + 'vid' => 'tags', + 'uid' => \Drupal::currentUser()->id(), + ]; + $term = Term::create($settings); + $term->save(); + return $term; + } + +} diff --git a/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerTestTrait.php b/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerTestTrait.php new file mode 100644 index 000000000..d47b51f98 --- /dev/null +++ b/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerTestTrait.php @@ -0,0 +1,234 @@ +setPassword($password)->save(); + // Support old and new tests. + $account->passRaw = $password; + $account->pass_raw = $password; + $this->drupalLogin($account); + } + + /** + * Prep a content type for use with these tests. + * + * @param string $content_type + * The content type, i.e. the node bundle ID, to configure; defaults to + * 'page'. + */ + protected function setupContentType($content_type = 'page') { + // Log in as user 1. + $this->loginUser1(); + + // Create the content type. + $this->drupalCreateContentType(['type' => $content_type, 'name' => 'Page']); + + // Allow each node to have a customized display. + $this->panelize($content_type, NULL, ['panelizer[custom]' => TRUE]); + + // Logout so that a new user can log in. + $this->drupalLogout(); + } + + /** + * Create a test node. + * + * @param string $type + * The entity type to create, defaults to 'page'. + * + * @return object + * An example node. + */ + protected function createTestNode($type = 'page') { + // Create a test node. + return $this->drupalCreateNode([ + 'title' => t('Hello, world!'), + 'type' => $type, + ]); + } + + /** + * Panelizes a node type's default view display. + * + * @param string $content_type + * The content type, i.e. the node bundle ID, to configure; defaults to + * 'page'. + * @param string $display + * (optional) The view mode to work on. + * @param array $values + * (optional) Additional form values. + */ + protected function panelize($content_type = 'page', $display = NULL, array $values = []) { + $this->drupalGet("admin/structure/types"); + $this->assertResponse(200); + + $this->drupalGet("admin/structure/types/manage/{$content_type}"); + $this->assertResponse(200); + + $path = "admin/structure/types/manage/{$content_type}/display"; + if (!empty($display)) { + $path .= '/' . $display; + } + $this->drupalGet($path); + $this->assertResponse(200); + + $edit = [ + 'panelizer[enable]' => TRUE, + ] + $values; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertResponse(200); + + entity_get_form_display('node', $content_type, 'default') + ->setComponent('panelizer', [ + 'type' => 'panelizer', + ]) + ->save(); + } + + /** + * Unpanelizes a node type's default view display. + * + * Panelizer is disabled for the display, but its configuration is retained. + * + * @param string $content_type + * The content type, i.e. the node bundle ID, to configure; defaults to + * 'page'. + * @param string $display + * (optional) The view mode to work on. + * @param array $values + * (optional) Additional form values. + */ + protected function unpanelize($content_type = 'page', $display = NULL, array $values = []) { + $this->drupalGet("admin/structure/types/manage/{$content_type}/display/{$display}"); + $this->assertResponse(200); + + $edit = [ + 'panelizer[enable]' => FALSE, + ] + $values; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertResponse(200); + + entity_get_form_display('node', $content_type, 'default') + ->removeComponent('panelizer') + ->save(); + } + + /** + * + * + * @param string $content_type + * The content type, i.e. the node bundle ID, to configure; defaults to + * 'page'. + */ + protected function addPanelizerDefault($content_type = 'page', $display = 'default') { + $label = $this->getRandomGenerator()->word(16); + $id = strtolower($label); + $default_id = "node__{$content_type}__{$display}__{$id}"; + $options = [ + 'query' => [ + 'js' => 'nojs', + ], + ]; + + $this->drupalGet("admin/structure/types/manage/{$content_type}/display"); + $this->assertResponse(200); + $this->clickLink('Add a new Panelizer default display'); + + // Step 1: Enter the default's label and ID. + $edit = [ + 'id' => $id, + 'label' => $label, + ]; + $this->drupalPostForm(NULL, $edit, t('Next')); + $this->assertResponse(200); + + // Step 2: Define contexts. + $this->assertUrl("admin/structure/panelizer/add/{$default_id}/contexts", $options); + $this->drupalPostForm(NULL, [], t('Next')); + $this->assertResponse(200); + + // Step 3: Select layout. + $this->assertUrl("admin/structure/panelizer/add/{$default_id}/layout", $options); + $this->drupalPostForm(NULL, [], t('Next')); + $this->assertResponse(200); + + // Step 4: Select content. + $this->assertUrl("admin/structure/panelizer/add/{$default_id}/content", $options); + $this->drupalPostForm(NULL, [], t('Finish')); + $this->assertResponse(200); + + return $id; + } + + /** + * Deletes a Panelizer default. + * + * @param string $content_type + * The content type, i.e. the node bundle ID, to configure; defaults to + * 'page'. + * @param string $display + * (optional) The view mode to work on. + * @param string $id + * (optional) The default ID. + */ + protected function deletePanelizerDefault($content_type = 'page', $display = 'default', $id = 'default') { + $this->drupalGet("admin/structure/panelizer/delete/node__{$content_type}__{$display}__{$id}"); + $this->assertResponse(200); + $this->drupalPostForm(NULL, [], t('Confirm')); + $this->assertResponse(200); + } + + /** + * Asserts that a Panelizer default exists. + * + * @param string $content_type + * The content type, i.e. the node bundle ID, to configure; defaults to + * 'page'. + * @param string $display + * (optional) The view mode to work on. + * @param string $id + * (optional) The default ID. + */ + protected function assertDefaultExists($content_type = 'page', $display = 'default', $id = 'default') { + $settings = entity_get_display('node', $content_type, $display) + ->getThirdPartySettings('panelizer'); + + $display_exists = isset($settings['displays'][$id]); + $this->assertTrue($display_exists); + } + + /** + * Asserts that a Panelizer default does not exist. + * + * @param string $content_type + * The content type, i.e. the node bundle ID, to configure; defaults to + * 'page'. + * @param string $display + * (optional) The view mode to work on. + * @param string $id + * The default ID. + */ + protected function assertDefaultNotExists($content_type = 'page', $display = 'default', $id = 'default') { + $settings = entity_get_display('node', $content_type, $display) + ->getThirdPartySettings('panelizer'); + + $display_exists = isset($settings['displays'][$id]); + $this->assertFalse($display_exists); + } + +} diff --git a/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerUserFunctionalTest.php b/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerUserFunctionalTest.php new file mode 100644 index 000000000..73aa09297 --- /dev/null +++ b/docroot/modules/contrib/panelizer/tests/src/Functional/PanelizerUserFunctionalTest.php @@ -0,0 +1,111 @@ +drupalCreateUser([ + // Required for Panelizer. + 'administer panelizer', + 'access panels in-place editing', + // Allow managing user entities. + 'administer users', + // Allow managing user entity settings. + 'administer account settings', + // View access to user profiles. + 'access user profiles', + // Allow managing the user entity fields and display settings. + 'administer user display', + 'administer user fields', + ]); + $this->drupalLogin($user); + + // Enable Panelizer for this entity. + $this->drupalGet('admin/config/people/accounts/display'); + $this->assertResponse(200); + $edit = [ + 'panelizer[enable]' => TRUE, + 'panelizer[custom]' => TRUE, + ]; + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertResponse(200); + + // Reload all caches. + $this->rebuildAll(); + } + + /** + * Tests rendering a user with Panelizer default. + */ + public function testPanelizerDefault() { + /** @var \Drupal\panelizer\PanelizerInterface $panelizer */ + $panelizer = \Drupal::service('panelizer'); + $displays = $panelizer->getDefaultPanelsDisplays('user', 'user', 'default'); + $display = $displays['default']; + $display->addBlock([ + 'id' => 'panelizer_test', + 'label' => 'Panelizer test', + 'provider' => 'block_content', + 'region' => 'content', + ]); + $panelizer->setDefaultPanelsDisplay('default', 'user', 'user', 'default', $display); + + // Create a user, and check that the IPE is visible on it. + $account = $this->drupalCreateUser(); + + // Check the user entity page. + $out = $this->drupalGet('user/' . $account->id()); + $this->assertResponse(200); + $this->verbose($out); + + // Verify that + $elements = $this->xpath('//*[@id="panels-ipe-content"]'); + if (is_array($elements)) { + $this->assertIdentical(count($elements), 1); + } + else { + $this->fail('Could not parse page content.'); + } + + // Check that the block we added is visible. + $this->assertText('Panelizer test'); + $this->assertText('Abracadabra'); + } + +} diff --git a/docroot/modules/contrib/panelizer/tests/src/FunctionalJavascript/PanelizerIntegrationTest.php b/docroot/modules/contrib/panelizer/tests/src/FunctionalJavascript/PanelizerIntegrationTest.php new file mode 100644 index 000000000..28ce515e8 --- /dev/null +++ b/docroot/modules/contrib/panelizer/tests/src/FunctionalJavascript/PanelizerIntegrationTest.php @@ -0,0 +1,87 @@ +drupalCreateUser([ + 'access content', + 'access panels in-place editing', + 'administer blocks', + 'administer content types', + 'administer nodes', + 'administer node display', + 'administer panelizer', + ]); + $this->drupalLogin($admin_user); + + // Create the "Basic Page" content type. + $this->createContentType([ + 'type' => 'page', + 'name' => 'Basic Page', + ]); + + // Enable Panelizer for the "Basic Page" content type. + $this->drupalGet('admin/structure/types/manage/page/display'); + $this->submitForm(['panelizer[enable]' => 1], t('Save')); + + // Create a new Basic Page. + $this->drupalGet('node/add/page'); + $this->submitForm(['title[0][value]' => 'Test Node'], t('Save and publish')); + + $this->test_route = 'node/1'; + } + + /** + * Tests that the IPE editing session is specific to a user. + */ + public function testUserEditSession() { + $this->visitIPERoute(); + $this->assertSession()->elementExists('css', '.layout--onecol'); + + // Change the layout to lock the IPE. + $this->changeLayout('Columns: 2', 'layout_twocol'); + $this->assertSession()->elementExists('css', '.layout--twocol'); + $this->assertSession()->elementNotExists('css', '.layout--onecol'); + + // Create a second node. + $this->drupalGet('node/add/page'); + $this->submitForm(['title[0][value]' => 'Test Node 2'], t('Save and publish')); + $this->test_route = 'node/2'; + + // Ensure the second node does not use the session of the other node. + $this->visitIPERoute(); + $this->assertSession()->elementExists('css', '.layout--onecol'); + $this->assertSession()->elementNotExists('css', '.layout--twocol'); + } + +} diff --git a/docroot/modules/contrib/panelizer/tests/src/Unit/PanelizerDefaultPanelsStorageTest.php b/docroot/modules/contrib/panelizer/tests/src/Unit/PanelizerDefaultPanelsStorageTest.php new file mode 100644 index 000000000..6f14453fb --- /dev/null +++ b/docroot/modules/contrib/panelizer/tests/src/Unit/PanelizerDefaultPanelsStorageTest.php @@ -0,0 +1,271 @@ +storage = $this->prophesize(EntityStorageInterface::class); + + $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + $this->entityTypeManager->getStorage('entity_type_id')->willReturn($this->storage->reveal()); + + $this->panelizer = $this->prophesize(Panelizer::class); + + $this->panelsStorage = $this->getMockBuilder(PanelizerDefaultPanelsStorage::class) + ->setConstructorArgs([ + [], + '', + [], + $this->entityTypeManager->reveal(), + $this->panelizer->reveal(), + ]) + ->setMethods(['getEntityContext']) + ->getMock(); + } + + /** + * @covers ::load + */ + public function testLoadEmptyContext() { + $entity_context = $this->prophesize(AutomaticContext::class); + + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $panels_display->setContexts([ + '@panelizer.entity_context:entity' => $entity_context->reveal(), + ])->shouldBeCalled(); + + $this->panelizer->getDefaultPanelsDisplay('default', 'entity_type_id', 'bundle', 'view_mode', NULL) + ->willReturn($panels_display->reveal()); + + $this->panelizer + ->getDisplayStaticContexts('default', 'entity_type_id', 'bundle', 'view_mode') + ->willReturn([]); + + $this->panelsStorage->method('getEntityContext') + ->with($this->equalTo('entity_type_id'), $this->isNull()) + ->willReturn([ + '@panelizer.entity_context:entity' => $entity_context->reveal(), + ]); + + $this->assertSame($panels_display->reveal(), $this->panelsStorage->load('entity_type_id:bundle:view_mode:default')); + } + + /** + * @covers ::load + */ + public function testLoadWithContextValue() { + $entity_context = $this->prophesize(AutomaticContext::class); + + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $panels_display->setContexts([ + '@panelizer.entity_context:entity' => $entity_context->reveal(), + ])->shouldBeCalled(); + + $this->panelizer->getDefaultPanelsDisplay('default', 'entity_type_id', 'bundle', 'view_mode', NULL) + ->willReturn($panels_display->reveal()); + + $this->panelizer + ->getDisplayStaticContexts('default', 'entity_type_id', 'bundle', 'view_mode') + ->willReturn([]); + + $entity = $this->prophesize(EntityInterface::class); + $entity->bundle()->willReturn("bundle"); + $this->storage->load('123')->willReturn($entity->reveal())->shouldBeCalled(); + + $this->panelsStorage->method('getEntityContext') + ->with($this->equalTo('entity_type_id'), $entity->reveal()) + ->willReturn([ + '@panelizer.entity_context:entity' => $entity_context->reveal(), + ]); + + $this->assertSame($panels_display->reveal(), $this->panelsStorage->load('*entity_type_id:123:view_mode:default')); + } + + /** + * @covers ::load + */ + public function testLoadDoesntExist() { + $this->panelizer->getDefaultPanelsDisplay('default', 'entity_type_id', 'bundle', 'view_mode', NULL) + ->willReturn(NULL); + + $this->assertSame(NULL, $this->panelsStorage->load('entity_type_id:bundle:view_mode:default')); + } + + /** + * @covers ::load + */ + public function testLoadNoEntity() { + $this->storage->load('123')->willReturn(NULL)->shouldBeCalled(); + + $this->panelizer->getDefaultPanelsDisplay('default', 'entity_type_id', 'bundle', 'view_mode', NULL) + ->shouldNotBeCalled(); + + $this->assertSame(NULL, $this->panelsStorage->load('*entity_type_id:123:view_mode:default')); + } + + /** + * @covers ::save + */ + public function testSaveSuccessful() { + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $panels_display->getStorageId()->willReturn('entity_type_id:bundle:view_mode:default'); + + $this->panelizer->setDefaultPanelsDisplay('default', 'entity_type_id', 'bundle', 'view_mode', $panels_display->reveal()) + ->shouldBeCalled(); + + $this->panelsStorage->save($panels_display->reveal()); + } + + /** + * @covers ::save + * + * @expectedException \Exception + * @expectedExceptionMessage Couldn't find Panelizer default to store Panels display + */ + public function testSaveDoesntExist() { + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $panels_display->getStorageId()->willReturn('entity_type_id:bundle:view_mode:default'); + + $this->panelizer->setDefaultPanelsDisplay('default', 'entity_type_id', 'bundle', 'view_mode', $panels_display->reveal()) + ->willThrow(new PanelizerException()); + + $this->panelsStorage->save($panels_display->reveal()); + } + + /** + * @covers ::save + * + * @expectedException \Exception + * @expectedExceptionMessage Couldn't find Panelizer default to store Panels display + */ + public function testSaveNoEntity() { + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $panels_display->getStorageId()->willReturn('*entity_type_id:123:view_mode:default'); + + $this->storage->load('123')->willReturn(NULL)->shouldBeCalled(); + + $this->panelizer->setDefaultPanelsDisplay('default', 'entity_type_id', 'bundle', 'view_mode', $panels_display->reveal()) + ->shouldNotBeCalled(); + + $this->panelsStorage->save($panels_display->reveal()); + } + + /** + * @covers ::access + */ + public function testAccessRead() { + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $account = $this->prophesize(AccountInterface::class); + + $this->panelizer->getDefaultPanelsDisplay('default', 'entity_type_id', 'bundle', 'view_mode') + ->willReturn($panels_display->reveal()); + $this->panelizer->hasDefaultPermission()->shouldNotBeCalled(); + + $this->assertEquals(AccessResult::allowed(), $this->panelsStorage->access('entity_type_id:bundle:view_mode:default', 'read', $account->reveal())); + } + + /** + * @covers ::access + */ + public function testAccessNotFound() { + $account = $this->prophesize(AccountInterface::class); + + $this->panelizer->getDefaultPanelsDisplay('default', 'entity_type_id', 'bundle', 'view_mode') + ->willReturn(NULL); + $this->panelizer->hasDefaultPermission()->shouldNotBeCalled(); + + $this->assertEquals(AccessResult::forbidden(), $this->panelsStorage->access('entity_type_id:bundle:view_mode:default', 'read', $account->reveal())); + } + + /** + * @covers ::access + */ + public function testAccessNoEntity() { + $account = $this->prophesize(AccountInterface::class); + + $this->storage->load('123')->willReturn(NULL)->shouldBeCalled(); + + $this->panelizer->getDefaultPanelsDisplay('default', 'entity_type_id', 'bundle', 'view_mode') + ->shouldNotBeCalled(); + + $this->assertEquals(AccessResult::forbidden(), $this->panelsStorage->access('*entity_type_id:123:view_mode:default', 'read', $account->reveal())); + } + + /** + * @covers ::access + */ + public function testAccessChangeContent() { + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $account = $this->prophesize(AccountInterface::class); + + $this->panelizer->getDefaultPanelsDisplay('default', 'entity_type_id', 'bundle', 'view_mode') + ->willReturn($panels_display->reveal()); + $this->panelizer->hasDefaultPermission('change content', 'entity_type_id', 'bundle', 'view_mode', 'default', $account->reveal()) + ->willReturn(TRUE); + + $this->assertEquals(AccessResult::allowed(), $this->panelsStorage->access('entity_type_id:bundle:view_mode:default', 'update', $account->reveal())); + $this->assertEquals(AccessResult::allowed(), $this->panelsStorage->access('entity_type_id:bundle:view_mode:default', 'delete', $account->reveal())); + $this->assertEquals(AccessResult::allowed(), $this->panelsStorage->access('entity_type_id:bundle:view_mode:default', 'create', $account->reveal())); + } + + /** + * @covers ::access + */ + public function testAccessChangeLayout() { + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $account = $this->prophesize(AccountInterface::class); + + $this->panelizer->getDefaultPanelsDisplay('default', 'entity_type_id', 'bundle', 'view_mode') + ->willReturn($panels_display->reveal()); + $this->panelizer->hasDefaultPermission('change layout', 'entity_type_id', 'bundle', 'view_mode', 'default', $account->reveal()) + ->willReturn(TRUE); + + $this->assertEquals(AccessResult::allowed(), $this->panelsStorage->access('entity_type_id:bundle:view_mode:default', 'change layout', $account->reveal())); + } + +} diff --git a/docroot/modules/contrib/panelizer/tests/src/Unit/PanelizerEntityViewBuilderTest.php b/docroot/modules/contrib/panelizer/tests/src/Unit/PanelizerEntityViewBuilderTest.php new file mode 100644 index 000000000..b934da92a --- /dev/null +++ b/docroot/modules/contrib/panelizer/tests/src/Unit/PanelizerEntityViewBuilderTest.php @@ -0,0 +1,370 @@ +entityType = $this->prophesize(EntityTypeInterface::class); + $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class); + $this->panelizer = $this->prophesize(PanelizerInterface::class); + $this->panelizerManager = $this->prophesize(PanelizerEntityManagerInterface::class); + $this->panelsManager = $this->prophesize(PanelsDisplayManagerInterface::class); + + $this->entityType->id() + ->willReturn('entity_type_id'); + + $this->entityViewBuilder = $this->getMockBuilder(PanelizerEntityViewBuilder::class) + ->setConstructorArgs([ + $this->entityType->reveal(), + $this->entityTypeManager->reveal(), + $this->moduleHandler->reveal(), + $this->panelizer->reveal(), + $this->panelizerManager->reveal(), + $this->panelsManager->reveal() + ]) + ->setMethods(['getFallbackViewBuilder', 'getPanelizerPlugin', 'collectRenderDisplays', 'getEntityContext']) + ->getMock(); + + $this->fallbackViewBuilder = $this->prophesize(EntityViewBuilderInterface::class); + $this->panelizerPlugin = $this->prophesize(PanelizerEntityInterface::class); + + $this->entityViewBuilder->method('getFallbackViewBuilder') + ->willReturn($this->fallbackViewBuilder->reveal()); + $this->entityViewBuilder->method('getPanelizerPlugin') + ->willReturn($this->panelizerPlugin->reveal()); + } + + /** + * Tests buildComponents(). + * + * @covers ::buildComponents + */ + public function testBuildComponents() { + $build = ['random_value' => 123]; + + $entity1 = $this->prophesize(FieldableEntityInterface::class); + $entity1->bundle()->willReturn('abc'); + + $display1 = $this->prophesize(EntityViewDisplayInterface::class); + $display1->getThirdPartySetting('panelizer', 'enable', FALSE) + ->willReturn(TRUE); + + $entity2 = $this->prophesize(FieldableEntityInterface::class); + $entity2->bundle()->willReturn('xyz'); + + $display2 = $this->prophesize(EntityViewDisplayInterface::class); + $display2->getThirdPartySetting('panelizer', 'enable', FALSE) + ->willReturn(FALSE); + + $displays = [ + 'abc' => $display1->reveal(), + 'xyz' => $display2->reveal(), + ]; + + $this->fallbackViewBuilder->buildComponents($build, [234 => $entity2->reveal()], $displays, 'full') + ->shouldBeCalled(); + + $this->moduleHandler->invokeAll('entity_prepare_view', [ + 'entity_type_id', + [123 => $entity1->reveal()], + $displays, + 'full' + ])->shouldBeCalled(); + + $this->entityViewBuilder->buildComponents( + $build, + [ + 123 => $entity1->reveal(), + 234 => $entity2->reveal() + ], + $displays, + 'full' + ); + } + + /** + * Setups up the mock objects for testing view() and viewMultiple(). + * + * @return array + * An associative array with the following keys: + * - entities: Associative array of the mocked entity objects, keyed by the + * id. + * - expected: Associative array of the built render arrays keyed by the + * entity id. + */ + protected function setupView() { + $entity1 = $this->prophesize(FieldableEntityInterface::class); + $entity1->bundle()->willReturn('abc'); + $entity1->getEntityTypeId()->willReturn('entity_type_id'); + $entity1->id()->willReturn(123); + $entity1->getCacheContexts()->willReturn(['context']); + $entity1->getCacheTags()->willReturn(['tag']); + $entity1->getCacheMaxAge()->willReturn(123); + + $display1 = $this->prophesize(EntityViewDisplayInterface::class); + $display1->getThirdPartySetting('panelizer', 'enable', FALSE) + ->willReturn(TRUE); + + $entity2 = $this->prophesize(FieldableEntityInterface::class); + $entity2->bundle()->willReturn('xyz'); + $entity2->getEntityTypeId()->willReturn('entity_type_id'); + + $display2 = $this->prophesize(EntityViewDisplayInterface::class); + $display2->getThirdPartySetting('panelizer', 'enable', FALSE) + ->willReturn(FALSE); + + $this->entityViewBuilder + ->method('collectRenderDisplays') + ->willReturn([ + 'abc' => $display1->reveal(), + 'xyz' => $display2->reveal() + ]); + + $entity_context = $this->prophesize(AutomaticContext::class); + $this->entityViewBuilder->method('getEntityContext') + ->willReturn($entity_context->reveal()); + + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $other_context = $this->prophesize(AutomaticContext::class); + $panels_display->getContexts() + ->willReturn(['other' => $other_context->reveal()]); + $panels_display->setContexts([ + 'other' => $other_context->reveal(), + '@panelizer.entity_context:entity' => $entity_context->reveal(), + ])->shouldBeCalled(); + $panels_display->build()->willReturn(['#markup' => 'Panelized']); + + $this->panelizer + ->getPanelizerSettings('entity_type_id', 'abc', 'full', $display1->reveal()) + ->willReturn([ + 'default' => 'default', + ]); + + $this->panelizer + ->getDisplayStaticContexts('default', 'entity_type_id', 'abc', 'full', $display1->reveal()) + ->willReturn([ + 'other' => $other_context->reveal(), + '@panelizer.entity_context:entity' => $entity_context->reveal(), + ]); + + $this->panelizer->getPanelsDisplay($entity1->reveal(), 'full', $display1->reveal()) + ->willReturn($panels_display->reveal()); + + $panels_display->getCacheContexts()->willReturn([]); + $panels_display->getCacheTags()->willReturn([]); + $panels_display->getCacheMaxAge()->willReturn(-1); + + return [ + 'entities' => [ + 123 => $entity1->reveal(), + 234 => $entity2->reveal(), + ], + 'expected' => [ + 123 => [ + '#theme' => [ + 'panelizer_view_mode__entity_type_id__123', + 'panelizer_view_mode__entity_type_id__abc', + 'panelizer_view_mode__entity_type_id', + 'panelizer_view_mode', + ], + '#panelizer_plugin' => $this->panelizerPlugin->reveal(), + '#panels_display' => $panels_display->reveal(), + '#entity' => $entity1->reveal(), + '#view_mode' => 'full', + '#langcode' => 'pl', + 'content' => [ + '#markup' => 'Panelized', + ], + '#cache' => [ + 'tags' => ['tag'], + 'contexts' => ['context'], + 'max-age' => 123, + ], + ], + 234 => [ + '#markup' => 'Fallback', + ], + ] + ]; + } + + /** + * Tests view(). + * + * @covers ::view + */ + public function testView() { + $data = $this->setupView(); + $entities = $data['entities']; + $expected = $data['expected']; + + $this->fallbackViewBuilder->view($entities[234], 'full', 'pl') + ->willReturn(['#markup' => 'Fallback']); + + $this->assertEquals($expected[123], $this->entityViewBuilder->view($entities[123], 'full', 'pl')); + $this->assertEquals($expected[234], $this->entityViewBuilder->view($entities[234], 'full', 'pl')); + } + + /** + * Tests viewMultiple(). + * + * @covers ::viewMultiple + */ + public function testViewMultiple() { + $data = $this->setupView(); + $entities = $data['entities']; + $expected = $data['expected']; + + $this->fallbackViewBuilder->viewMultiple([234 => $entities[234]], 'full', 'pl') + ->willReturn([234 => ['#markup' => 'Fallback']]); + + $this->assertEquals($expected, $this->entityViewBuilder->viewMultiple($entities, 'full', 'pl')); + } + + /** + * Tests resetCache(). + * + * @covers ::resetCache + */ + public function testResetCache() { + $entities = [ + $this->prophesize(EntityInterface::class)->reveal(), + $this->prophesize(EntityInterface::class)->reveal(), + ]; + $this->fallbackViewBuilder->resetCache($entities)->shouldBeCalled(); + $this->entityViewBuilder->resetCache($entities); + } + + /** + * Tests viewField(). + * + * @covers ::viewField + */ + public function testViewField() { + $items = $this->prophesize(FieldItemListInterface::class)->reveal(); + $display_options = ['abc' => 123]; + $this->fallbackViewBuilder->viewField($items, $display_options) + ->willReturn(['#markup' => 'field']); + $this->assertEquals(['#markup' => 'field'], $this->entityViewBuilder->viewField($items, $display_options)); + } + + /** + * Tests viewFieldItem(). + * + * @covers ::viewFieldItem + */ + public function testViewFieldItem() { + $item = $this->prophesize(FieldItemInterface::class)->reveal(); + $display = ['abc' => 123]; + $this->fallbackViewBuilder->viewFieldItem($item, $display) + ->willReturn(['#markup' => 'item']); + $this->assertEquals(['#markup' => 'item'], $this->entityViewBuilder->viewFieldItem($item, $display)); + } + + /** + * Tests getCacheTags(). + * + * @covers ::getCacheTags + */ + public function testGetCacheTags() { + $this->fallbackViewBuilder->getCacheTags() + ->willReturn(['tag']); + $this->assertEquals(['tag'], $this->entityViewBuilder->getCacheTags()); + } + +} diff --git a/docroot/modules/contrib/panelizer/tests/src/Unit/PanelizerFieldPanelsStorageTest.php b/docroot/modules/contrib/panelizer/tests/src/Unit/PanelizerFieldPanelsStorageTest.php new file mode 100644 index 000000000..089ddc15c --- /dev/null +++ b/docroot/modules/contrib/panelizer/tests/src/Unit/PanelizerFieldPanelsStorageTest.php @@ -0,0 +1,277 @@ +storage = $this->prophesize(EntityStorageInterface::class); + + $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + $this->entityTypeManager->getStorage('entity_type_id')->willReturn($this->storage->reveal()); + + $this->panelizer = $this->prophesize(Panelizer::class); + + $this->panelsStorage = $this->getMockBuilder(PanelizerFieldPanelsStorage::class) + ->setConstructorArgs([ + [], + '', + [], + $this->entityTypeManager->reveal(), + $this->panelizer->reveal(), + ]) + ->setMethods(['getEntityContext']) + ->getMock(); + } + + /** + * @covers ::load + */ + public function testLoad() { + $entity_context = $this->prophesize(AutomaticContext::class); + + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $panels_display->setContexts([ + '@panelizer.entity_context:entity' => $entity_context->reveal(), + ])->shouldBeCalled(); + + $entity = $this->prophesize(FieldableEntityInterface::class); + + $this->panelizer->getPanelsDisplay($entity->reveal(), 'view_mode') + ->willReturn($panels_display->reveal()); + + $this->storage->load('123')->willReturn($entity->reveal())->shouldBeCalled(); + + $this->panelsStorage->method('getEntityContext') + ->with($this->equalTo('entity_type_id'), $entity->reveal()) + ->willReturn($entity_context->reveal()); + + $this->assertSame($panels_display->reveal(), $this->panelsStorage->load('entity_type_id:123:view_mode')); + } + + /** + * @covers ::load + */ + public function testLoadRevision() { + $entity_context = $this->prophesize(AutomaticContext::class); + + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $panels_display->setContexts([ + '@panelizer.entity_context:entity' => $entity_context->reveal(), + ])->shouldBeCalled(); + + $entity = $this->prophesize(FieldableEntityInterface::class); + + $this->panelizer->getPanelsDisplay($entity->reveal(), 'view_mode') + ->willReturn($panels_display->reveal()); + + $this->storage->loadRevision('456')->willReturn($entity->reveal())->shouldBeCalled(); + + $this->panelsStorage->method('getEntityContext') + ->with($this->equalTo('entity_type_id'), $entity->reveal()) + ->willReturn($entity_context->reveal()); + + $this->assertSame($panels_display->reveal(), $this->panelsStorage->load('entity_type_id:123:view_mode:456')); + } + + /** + * @covers ::load + */ + public function testLoadNoEntity() { + $this->storage->load('123')->willReturn(NULL)->shouldBeCalled(); + + $this->panelizer->getPanelsDisplay()->shouldNotBeCalled(); + + $this->assertSame(NULL, $this->panelsStorage->load('entity_type_id:123:view_mode')); + } + + /** + * @covers ::load + */ + public function testLoadNotFound() { + $entity = $this->prophesize(FieldableEntityInterface::class); + + $this->storage->load('123')->willReturn($entity->reveal()); + + $this->panelizer->getPanelsDisplay($entity->reveal(), 'view_mode') + ->willReturn(NULL); + + $this->assertSame(NULL, $this->panelsStorage->load('entity_type_id:123:view_mode')); + } + + /** + * @covers ::save + */ + public function testSaveSuccessful() { + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $panels_display->getStorageId()->willReturn('entity_type_id:123:view_mode'); + + $entity = $this->prophesize(FieldableEntityInterface::class); + + $this->panelizer->setPanelsDisplay($entity->reveal(), 'view_mode', NULL, $panels_display) + ->shouldBeCalled(); + + $this->storage->load('123')->willReturn($entity->reveal())->shouldBeCalled(); + + $this->panelsStorage->save($panels_display->reveal()); + } + + /** + * @covers ::save + * + * @expectedException \Exception + * @expectedExceptionMessage Couldn't find entity to store Panels display on + */ + public function testSaveNoEntity() { + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $panels_display->getStorageId()->willReturn('entity_type_id:123:view_mode'); + + $this->panelizer->setPanelsDisplay()->shouldNotBeCalled(); + + $this->storage->load('123')->willReturn(NULL)->shouldBeCalled(); + + $this->panelsStorage->save($panels_display->reveal()); + } + + /** + * @covers ::save + * + * @expectedException \Exception + * @expectedExceptionMessage Save failed + */ + public function testSaveFailed() { + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $panels_display->getStorageId()->willReturn('entity_type_id:123:view_mode'); + + $entity = $this->prophesize(FieldableEntityInterface::class); + + $this->panelizer->setPanelsDisplay($entity->reveal(), 'view_mode', NULL, $panels_display) + ->willThrow(new PanelizerException("Save failed")); + + $this->storage->load('123')->willReturn($entity->reveal())->shouldBeCalled(); + + $this->panelsStorage->save($panels_display->reveal()); + } + + /** + * @covers ::access + */ + public function testAccessRead() { + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $account = $this->prophesize(AccountInterface::class); + + $entity = $this->prophesize(FieldableEntityInterface::class); + $entity->access('view', $account->reveal(), TRUE) + ->willReturn(AccessResult::allowed()); + + $this->storage->load('123')->willReturn($entity->reveal()); + + $this->panelizer->getPanelsDisplay($entity->reveal(), 'view_mode') + ->willReturn($panels_display->reveal()); + $this->panelizer->hasEntityPermission()->shouldNotBeCalled(); + + $this->assertEquals(AccessResult::allowed(), $this->panelsStorage->access('entity_type_id:123:view_mode', 'read', $account->reveal())); + } + + /** + * @covers ::access + */ + public function testAccessNoEntity() { + $account = $this->prophesize(AccountInterface::class); + + $this->storage->load('123')->willReturn(NULL)->shouldBeCalled(); + + $this->panelizer->getPanelsDisplay()->shouldNotBeCalled(); + + $this->assertEquals(AccessResult::forbidden(), $this->panelsStorage->access('entity_type_id:123:view_mode', 'read', $account->reveal())); + } + + + /** + * @covers ::access + */ + public function testAccessChangeContent() { + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $account = $this->prophesize(AccountInterface::class); + + $entity = $this->prophesize(FieldableEntityInterface::class); + $entity->access('update', $account->reveal(), TRUE) + ->willReturn(AccessResult::allowed()); + + $this->storage->load('123')->willReturn($entity->reveal()); + + $this->panelizer->getPanelsDisplay($entity->reveal(), 'view_mode') + ->willReturn($panels_display->reveal()); + $this->panelizer->hasEntityPermission('change content', $entity->reveal(), 'view_mode', $account->reveal()) + ->willReturn(TRUE); + + $access = $this->panelsStorage->access('entity_type_id:123:view_mode', 'update', $account->reveal()); + $this->assertEquals(AccessResult::allowed(), $access); + } + + /** + * @covers ::access + */ + public function testAccessChangeLayout() { + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $account = $this->prophesize(AccountInterface::class); + + $entity = $this->prophesize(FieldableEntityInterface::class); + $entity->access('update', $account->reveal(), TRUE) + ->willReturn(AccessResult::allowed()); + + $this->storage->load('123')->willReturn($entity->reveal()); + + $this->panelizer->getPanelsDisplay($entity->reveal(), 'view_mode') + ->willReturn($panels_display->reveal()); + $this->panelizer->hasEntityPermission('change layout', $entity->reveal(), 'view_mode', $account->reveal()) + ->willReturn(TRUE); + + $this->assertEquals(AccessResult::allowed(), $this->panelsStorage->access('entity_type_id:123:view_mode', 'change layout', $account->reveal())); + } + +} diff --git a/docroot/modules/contrib/panels/LICENSE.txt b/docroot/modules/contrib/panels/LICENSE.txt new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/docroot/modules/contrib/panels/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. + + + Copyright (C) + + 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. + + , 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/docroot/modules/contrib/panels/composer.json b/docroot/modules/contrib/panels/composer.json new file mode 100644 index 000000000..7f145b8ab --- /dev/null +++ b/docroot/modules/contrib/panels/composer.json @@ -0,0 +1,31 @@ +{ + "name": "drupal/panels", + "description": "Core Panels display functions; provides no external UI, at least one other Panels module should be enabled.", + "type": "drupal-module", + "homepage": "https://www.drupal.org/project/panels", + "authors": [ + { + "name": "Jakob Perry", + "homepage": "https://www.drupal.org/u/japerry" + }, + { + "name": "Samuel Mortenson", + "homepage": "https://www.drupal.org/u/samuel.mortenson" + }, + { + "name": "See other contributors", + "homepage":"https://www.drupal.org/node/74958/committers" + } + + ], + "support": { + "issues": "https://www.drupal.org/project/issues/panels", + "irc": "irc://irc.freenode.org/drupal-scotch", + "source": "http://git.drupal.org/project/panels.git" + }, + "license": "GPL-2.0+", + "minimum-stability": "dev", + "require": { + "drupal/ctools": ">=3.0.0" + } +} diff --git a/docroot/modules/contrib/panels/config/schema/panels.schema.yml b/docroot/modules/contrib/panels/config/schema/panels.schema.yml new file mode 100644 index 000000000..8adb932b5 --- /dev/null +++ b/docroot/modules/contrib/panels/config/schema/panels.schema.yml @@ -0,0 +1,23 @@ +display_variant.plugin.panels_variant: + type: ctools.block_display_variant + label: 'Panels display variant' + mapping: + page_title: + type: label + label: 'Page title' + layout: + type: string + label: Layout + layout_settings: + type: layout_plugin.settings.[%parent.layout] + label: Layout settings + builder: + type: string + label: Builder + storage_type: + type: string + label: Storage type + storage_id: + type: string + label: Storage Id + diff --git a/docroot/modules/contrib/panels/layouts/layout_onecol/layout_onecol.png b/docroot/modules/contrib/panels/layouts/layout_onecol/layout_onecol.png new file mode 100644 index 000000000..176ed69e7 Binary files /dev/null and b/docroot/modules/contrib/panels/layouts/layout_onecol/layout_onecol.png differ diff --git a/docroot/modules/contrib/panels/layouts/layout_threecol_25_50_25/layout_threecol_25_50_25.png b/docroot/modules/contrib/panels/layouts/layout_threecol_25_50_25/layout_threecol_25_50_25.png new file mode 100644 index 000000000..14b4779a0 Binary files /dev/null and b/docroot/modules/contrib/panels/layouts/layout_threecol_25_50_25/layout_threecol_25_50_25.png differ diff --git a/docroot/modules/contrib/panels/layouts/layout_threecol_33_34_33/layout_threecol_33_34_33.png b/docroot/modules/contrib/panels/layouts/layout_threecol_33_34_33/layout_threecol_33_34_33.png new file mode 100644 index 000000000..ffd135136 Binary files /dev/null and b/docroot/modules/contrib/panels/layouts/layout_threecol_33_34_33/layout_threecol_33_34_33.png differ diff --git a/docroot/modules/contrib/panels/layouts/layout_twocol/layout_twocol.png b/docroot/modules/contrib/panels/layouts/layout_twocol/layout_twocol.png new file mode 100644 index 000000000..30ab8b67d Binary files /dev/null and b/docroot/modules/contrib/panels/layouts/layout_twocol/layout_twocol.png differ diff --git a/docroot/modules/contrib/panels/layouts/layout_twocol_bricks/layout_twocol_bricks.png b/docroot/modules/contrib/panels/layouts/layout_twocol_bricks/layout_twocol_bricks.png new file mode 100644 index 000000000..450395c80 Binary files /dev/null and b/docroot/modules/contrib/panels/layouts/layout_twocol_bricks/layout_twocol_bricks.png differ diff --git a/docroot/modules/contrib/panels/layouts/no-layout-preview.png b/docroot/modules/contrib/panels/layouts/no-layout-preview.png new file mode 100644 index 000000000..e78116b18 Binary files /dev/null and b/docroot/modules/contrib/panels/layouts/no-layout-preview.png differ diff --git a/docroot/modules/contrib/panels/panels.api.php b/docroot/modules/contrib/panels/panels.api.php new file mode 100644 index 000000000..edd67aa12 --- /dev/null +++ b/docroot/modules/contrib/panels/panels.api.php @@ -0,0 +1,22 @@ + '
Some extra markup
', + ]; +} diff --git a/docroot/modules/contrib/panels/panels.info.yml b/docroot/modules/contrib/panels/panels.info.yml new file mode 100644 index 000000000..f3f9fd15a --- /dev/null +++ b/docroot/modules/contrib/panels/panels.info.yml @@ -0,0 +1,19 @@ +# Initial Conversion to info.yml file for d8. +name: Panels +description: 'Core Panels display functions; provides no external UI, at least one other Panels module should be enabled.' +type: module +package: Panels +# core: 8.x +configure: panels.admin +dependencies: + - ctools + - layout_discovery + - drupal:system (>= 8.3.x) +test_dependencies: + - page_manager + +# Information added by Drupal.org packaging script on 2017-07-19 +version: '8.x-4.2' +core: '8.x' +project: 'panels' +datestamp: 1500497646 diff --git a/docroot/modules/contrib/panels/panels.install b/docroot/modules/contrib/panels/panels.install new file mode 100644 index 000000000..6a677047d --- /dev/null +++ b/docroot/modules/contrib/panels/panels.install @@ -0,0 +1,65 @@ += 8.3.0 + if (!version_compare(\Drupal::VERSION, '8.3', '>=')) { + $requirements['panels_core_version'] = [ + 'title' => t('Panels Drupal core version'), + 'value' => \Drupal::VERSION, + 'description' => t('Panels requires at least Drupal core 8.3.'), + 'severity' => REQUIREMENT_ERROR, + ]; + } + + return $requirements; +} + +/** + * Helper function for updating panels created layouts to layout discovery + */ +function panels_convert_plugin_ids_to_layout_discovery($layout_id) { + // For layouts we know about, return what the new ID should be. + // If we don't recognize the layout ID, return false to not update the config. + switch($layout_id) { + case 'onecol': + return 'layout_onecol'; + case 'threecol_25_50_25': + return 'layout_threecol_25_50_25'; + case 'threecol_25_50_25_stacked': + return 'layout_threecol_25_50_25'; + case 'threecol_33_34_33': + return 'layout_threecol_33_34_33'; + case 'threecol_33_34_33_stacked': + return 'layout_threecol_33_34_33'; + case 'twocol': + return 'layout_twocol'; + case 'twocol_bricks': + return 'layout_twocol_bricks'; + case 'twocol_stacked': + return 'layout_twocol'; + default: + return FALSE; + } +} + +/** + * Uninstalls Layout plugin, then enables Layout Discovery. + */ +function panels_update_8401() { + if (\Drupal::moduleHandler()->moduleExists('layout_plugin')) { + \Drupal::service('module_installer')->uninstall(['layout_plugin'], FALSE); + \Drupal::service('module_installer')->install(['layout_discovery'], FALSE); + } +} \ No newline at end of file diff --git a/docroot/modules/contrib/panels/panels.module b/docroot/modules/contrib/panels/panels.module new file mode 100644 index 000000000..31de5f55f --- /dev/null +++ b/docroot/modules/contrib/panels/panels.module @@ -0,0 +1,109 @@ +getVariantPluginId() == 'panels_variant') { + /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $panels_display */ + $panels_display = $page_variant->getVariantPlugin(); + // At this point, it very likely that $page_variant->id() is NULL. But + // setting the storage type to 'page_manager' is enough to trigger Panels + // to offer the IPE as an option. + $panels_display->setStorage('page_manager', $page_variant->id()); + } +} + +/** + * Implements hook_layout_alter(). + */ +function panels_layout_alter(&$definitions) { + $core_layouts = ['layout_onecol', 'layout_twocol', 'layout_twocol_bricks', 'layout_threecol_25_50_25', 'layout_threecol_33_34_33']; + foreach($definitions as $layout_name => $layout) { + // Verify that the layout definition is a LayoutDefinition + if (!($definitions[$layout_name] instanceof LayoutDefinition)) { + continue; + } + if (in_array($layout_name, $core_layouts) && empty($definitions[$layout_name]->getIconPath())) { + $definitions[$layout_name]->setIconPath(drupal_get_path('module', 'panels') . '/layouts/'.$layout_name.'/'.$layout_name.'.png'); + } + if (empty($definitions[$layout_name]->getIconPath())) { + $definitions[$layout_name]->setIconPath(drupal_get_path('module', 'panels') . '/layouts/no-layout-preview.png'); + } + } +} + + +/** + * Implements hook_ENTITY_TYPE_presave(). + * + * Now that we have the id for the page variant, we need to set the storage + * information again. + * + * @see panels_page_variant_create() + */ +function panels_page_variant_presave(PageVariantInterface $page_variant) { + if ($page_variant->getVariantPluginId() == 'panels_variant') { + /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $panels_display */ + $panels_display = $page_variant->getVariantPlugin(); + // Set the storage info now that we have the id. + $panels_display->setStorage('page_manager', $page_variant->id()); + // It's ConfigEntityBase::preSave() that copies configuration from plugin + // collections to the config entity, and unfortunately, that runs just + // before invoking hook_entity_presave(). So, we have to copy the + // configuration from $panels_display to $page_variant again manually for + // it to be saved. + $page_variant->set('variant_settings', $panels_display->getConfiguration()); + } +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function panels_form_page_manager_add_variant_form_alter(array &$form, FormStateInterface $form_state) { + $pos = array_search('::submitForm', $form['actions']['submit']['#submit']); + $handlers_top = array_slice($form['actions']['submit']['#submit'], 0, $pos); + $handlers_bottom = array_slice($form['actions']['submit']['#submit'], $pos); + $panels_handler = ['panels_form_page_manager_add_variant_form_submit']; + $handlers = array_merge($handlers_top, $panels_handler, $handlers_bottom); + $form['actions']['submit']['#submit'] = $handlers; +} + +/** + * Form submit handler to set page_manager storage for panels display variants. + */ +function panels_form_page_manager_add_variant_form_submit(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var $page_variant \Drupal\page_manager\Entity\PageVariant */ + $page_variant = $cached_values['page_variant']; + $plugin = $page_variant->getVariantPlugin(); + if ($plugin instanceof PanelsDisplayVariant) { + $plugin->setStorage('page_manager', $page_variant->id()); + } +} diff --git a/docroot/modules/contrib/panels/panels.permissions.yml b/docroot/modules/contrib/panels/panels.permissions.yml new file mode 100644 index 000000000..245d542f9 --- /dev/null +++ b/docroot/modules/contrib/panels/panels.permissions.yml @@ -0,0 +1,27 @@ +# @todo Almost all of these need to be moved into pipelines. +use panels dashboard: + title: 'Use Panels Dashboard' + description: 'Allows a user to access the Panels Dashboard.' +view pane admin links: + title: 'View administrative links on Panel panes' +# @todo should we really have a global perm for this, or should it be moved into a pipeline question? +administer pane access: + title: 'Configure access settings on Panel panes' + description: 'Access rules (often also called visibility rules) can be configured on a per-pane basis. This permission allows users to configure those settings.' +administer advanced pane settings: + title: 'Configure advanced settings on Panel panes' +administer panels layouts: + title: 'Administer Panels layouts' + description: 'Allows a user to administer exported Panels layout plugins & instances.' +administer panels styles: + title: 'Administer Panels styles' + description: 'Allows a user to administer the styles of Panel panes.' +use panels caching features: + title: 'Configure caching settings on Panels' + description: 'Allows a user to configure caching on Panels displays and panes.' +use panels locks: + title: 'Use panel locks' + description: 'Allows a user to lock and unlock panes in a panel display.' +use ipe with page manager: + title: 'Use the Panels In-Place Editor with Page Manager' + description: 'Allows users with access to the In-Place editor to administer page manager pages. This permission is only needed for users without "use page manager" access.' diff --git a/docroot/modules/contrib/panels/panels.routing.yml b/docroot/modules/contrib/panels/panels.routing.yml new file mode 100644 index 000000000..101a80864 --- /dev/null +++ b/docroot/modules/contrib/panels/panels.routing.yml @@ -0,0 +1,28 @@ +panels.select_block: + path: '/admin/structure/panels/{tempstore_id}/{machine_name}/select_block' + defaults: + _controller: '\Drupal\panels\Controller\Panels::selectBlock' + _title: 'Select block' + requirements: + _ctools_access: 'machine_name' +panels.add_block: + path: '/admin/structure/panels/{tempstore_id}/{machine_name}/add/{block_id}' + defaults: + _form: '\Drupal\panels\Form\PanelsAddBlockForm' + _title: 'Add block' + requirements: + _ctools_access: 'machine_name' +panels.edit_block: + path: '/admin/structure/panels/{tempstore_id}/{machine_name}/edit/{block_id}' + defaults: + _form: '\Drupal\panels\Form\PanelsEditBlockForm' + _title: 'Edit block' + requirements: + _ctools_access: 'machine_name' +panels.delete_block: + path: '/admin/structure/panels/{tempstore_id}/{machine_name}/delete/{block_id}' + defaults: + _form: '\Drupal\panels\Form\PanelsDeleteBlockForm' + _title: 'Delete block' + requirements: + _ctools_access: 'machine_name' diff --git a/docroot/modules/contrib/panels/panels.services.yml b/docroot/modules/contrib/panels/panels.services.yml new file mode 100644 index 000000000..63e4560fc --- /dev/null +++ b/docroot/modules/contrib/panels/panels.services.yml @@ -0,0 +1,18 @@ +services: + plugin.manager.panels.display_builder: + class: Drupal\panels\Plugin\DisplayBuilder\DisplayBuilderManager + arguments: ['@container.namespaces', '@cache.discovery', '@module_handler'] + plugin.manager.panels.pattern: + class: Drupal\panels\PanelsPatternManager + parent: default_plugin_manager + panels.display_manager: + class: Drupal\panels\PanelsDisplayManager + arguments: ['@plugin.manager.display_variant', '@config.typed'] + panels.storage_manager: + class: Drupal\panels\Storage\PanelsStorageManager + arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@current_user', '@event_dispatcher'] + panels.storage_access: + class: Drupal\panels\Storage\PanelsStorageAccess + arguments: ['@panels.storage_manager'] + tags: + - { name: access_check, applies_to: _panels_storage_access } diff --git a/docroot/modules/contrib/panels/panels_ipe/css/panels_ipe.css b/docroot/modules/contrib/panels/panels_ipe/css/panels_ipe.css new file mode 100644 index 000000000..606f81348 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/css/panels_ipe.css @@ -0,0 +1,780 @@ +/** + * @file + * Contains all CSS for the Panels In-Place Editor. + */ + +/* Define our icon font, which is generated from the SVGs in /images. */ +@font-face { + font-family: PanelsIPEIcon; + src: url(../fonts/ipeicons.woff); +} + +.ipe-icon { + display: inline-block; + vertical-align: middle; + font-family: PanelsIPEIcon; + font-size: 24px; +} + +.ipe-action-list .ipe-icon { + height: 24px; + margin-top: -10px; + display: block; +} + +.ipe-icon.ipe-icon-up:before { + content: "\e900"; +} + +.ipe-icon.ipe-icon-down:before { + content: "\e901"; +} + +.ipe-icon.ipe-icon-warning:before { + content: "\e902"; +} + +.ipe-icon.ipe-icon-change_layout:before { + content: "\e903"; +} + +.ipe-icon.ipe-icon-edit:before { + content: "\e904"; +} + +.ipe-icon.ipe-icon-manage_content:before { + content: "\e905"; +} + +.ipe-icon.ipe-icon-locked:before { + content: "\e90e"; +} + +.ipe-icon.ipe-icon-create_content:before { + content: "\e90b"; +} + +.ipe-icon.ipe-icon-save:before { + content: "\e906"; +} + +.ipe-icon.ipe-icon-loading:before { + display: inline-block; + content: "\e907"; + animation: ipe-spin 1s infinite linear; +} + +.ipe-icon.ipe-icon-remove { + /* Normalize font size as the "X" is quite large. */ + font-size: 20px; +} + +.ipe-icon.ipe-icon-remove:before { + content: "\e90c"; +} + +.ipe-icon.ipe-icon-configure:before { + content: "\e909"; +} + +.ipe-icon.ipe-icon-cancel:before { + content: "\e90a"; +} + +.ipe-icon.ipe-icon-search:before { + content: "\e90d"; +} + +.ipe-icon.ipe-icon-configure { + font-size: 20px; +} + +@keyframes ipe-spin { + from { + transform: rotate(360deg); + } + to { + transform: rotate(0deg); + } +} + +/* Fix the output of the AppView to the bottom of the screen. */ +#panels-ipe-tray { + position: fixed; + /* One below jQuery.ui dialogs. */ + z-index: 100; + width: 100%; + bottom: 0; + left: 0; + text-align: center; +} + +/* Force text to render as a sans-serif web-safe font. */ +#panels-ipe-tray, +#panels-ipe-tray a, +#panels-ipe-tray p { + font-family: Arial, Helvetica, sans-serif; +} + +#panels-ipe-tray p { + margin: 0; +} + +/* Remove focus outlines on known elements, if they are otherwise indicated. */ +.ipe-tabs-content:focus, +.ipe-tab a:focus, +.ipe-category:focus, +a.ipe-layout:focus, +.ipe-blockpicker-item a:focus, +.ipe-category-picker-bottom:focus { + outline: none; +} + +/* Add a box shadow to the IPE tabs. */ +.ipe-tabs-content { + box-shadow: 3px -1px 2px 0 rgba(0, 0, 0, 0.3333); +} + +/* Remove list styling from the output of the TabsView. */ +.ipe-tabs { + list-style: none; + margin: 0; + padding: 0; +} + +/* Display tabs inline and slightly on top of .ipe-tabs-content. */ +.ipe-tab { + overflow: hidden; + position: relative; + display: inline-block; + vertical-align: bottom; + margin-bottom: -1px; + background-color: white; + border-top: 1px solid darkgray; + box-shadow: 3px -1px 2px 0 rgba(0, 0, 0, 0.3333); +} + +html[dir="rtl"] .ipe-tab { + box-shadow: -3px -1px 2px 0 rgba(0, 0, 0, 0.3333); +} + +.ipe-tab:first-child { + border-left: 1px solid darkgray; + border-top-left-radius: 5px; + box-shadow: -2px -1px 2px rgba(0, 0, 0, 0.3333); + right: -5px; +} + +html[dir="rtl"] .ipe-tab:first-child { + border-left: 0 none; + border-top-left-radius: 0; + border-right: 1px solid darkgray; + border-top-right-radius: 5px; + box-shadow: 2px -1px 2px rgba(0, 0, 0, 0.3333); + right: auto; + left: -5px; +} + +.ipe-tab:last-child { + border-right: 1px solid darkgray; + border-top-right-radius: 5px; + box-shadow: 2px -1px 2px rgba(0, 0, 0, 0.3333); +} + +html[dir="rtl"] .ipe-tab:last-child { + border-right: 0 none; + border-top-right-radius: 0; + border-left: 1px solid darkgray; + border-top-left-radius: 5px; + box-shadow: -2px -1px 2px rgba(0, 0, 0, 0.3333); +} + +.ipe-tab a { + font-size: 13px; + text-transform: capitalize; + color: black; + padding: 10px 15px 12px 5px; + display: block; + vertical-align: top; + border: none; + cursor: pointer; + transition: .2s; + transition-property: color, border-color; + border-bottom: 1px solid darkgray; +} + +.ipe-tab.active a, +.ipe-tab a:hover { + padding-bottom: 10px; + color: rgb(67, 125, 33); + border: none; + border-bottom: 3px solid rgb(67, 125, 33); +} + +/* Indicate the cancel button. */ +[data-tab-id="cancel"] a:hover, +[data-tab-id="cancel"].active a { + color: #7d0000; + border-color: #7d0000; +} + +.ipe-tab a::selection { + background: none; +} + +/* Provide default styles and a minimum height for tab content. */ +.ipe-tab-content { + display: none; + min-height: 100px; + padding: 20px 5px 10px 5px; + background-color: white; + border-top: 1px solid darkgray; +} + +.ipe-tab-content.active { + display: block; +} + +/* Styles for the Layout selector. */ +.ipe-current-layout, +.ipe-all-layouts { + display: inline-block; +} + +.layout-wrapper { + display: flex; + justify-content: center; +} + +/* Remove
    list styling and make list scrollable. */ +.ipe-layouts { + vertical-align: top; + list-style: none; + margin: 0; + padding: 0; + white-space: nowrap; +} + +/* Show layouts as clickable things. */ +.ipe-layout { + cursor: pointer; + position: relative; + display: inline-block; + margin-right: 10px; + vertical-align: top; + border: 1px solid #d4d4d4; + min-width: 150px; + transition: .2s; +} + +.ipe-layout:hover { + background: #f1f1f1; +} + +/* Show the layout's label on hover. */ +.ipe-layout-label { + display: block; + color: black; + line-height: 14px; + font-size: 14px; + padding: 10px; + border-top: 1px solid #d4d4d4; + transition: .2s; +} + +/* Flag the current layout if a user is viewing it inside its category. */ +.ipe-current-layout-label { + position: absolute; + top: 0; + left: 0; + padding: 5px; + background: #437d21; + color: white; + border-bottom-right-radius: 5px; + line-height: 12px; + font-size: 12px; +} + +/* Make sure image icons aren't too wide. */ +.ipe-layout-image { + width: 75px; + margin: 10px 0 10px 0; +} + +/* Style the block/region headers. */ +div.ipe-actions { + display: flex; + justify-content: space-between; + min-height: 20px; + border-radius: 5px 5px 0 0; + background-color: rgb(222, 222, 222); + padding: 5px; + margin-top: 1px; + clear: both; +} + +div.ipe-actions-block { + background-color: rgb(243, 243, 243); +} + +.ipe-actions ul.ipe-action-list { + list-style: none; + margin: 0; + padding: 0; +} + +.ipe-actions h5, +.ipe-actions li { + font-family: Arial, Helvetica, sans-serif; + font-size: 12px; + text-transform: uppercase; + font-weight: bold; + margin: 0; +} + +.ipe-actions a { + color: black; + display: block; + text-transform: uppercase; + border: none; + cursor: pointer; + transition: .2s; +} + +.ipe-actions a:hover { + color: #bebebe; + border: none; +} + +.ipe-actions *::selection { + background: none; +} + +.ipe-action-list li { + display: inline-block; + vertical-align: middle; +} + +.ipe-action-list [data-action-id="move"] select { + font-size: 14px; + margin: 0; + background: transparent; + border: none; + text-transform: uppercase; +} + +.ipe-action-list [data-action-id="move"] option { + font-size: 14px; +} + +/* Fix contextual links hovering over action links. */ + +[data-block-id] [data-contextual-id].contextual { + /* Top is normally hard-set at 6px, this puts it below the action bar. */ + top: 35px; +} + +/* Indicate that blocks are draggable */ +[data-block-id].active { + cursor: move; + border: 1px dashed transparent; +} + +[data-block-id].active:hover { + border: 1px dashed #3c3c3c; +} + +/* Indicate an HTML request if a block is syncing. */ +[data-block-id].syncing:before { + float: left; + vertical-align: middle; + font-family: PanelsIPEIcon; + font-size: 24px; + content: "\e907"; + animation: ipe-spin 1s infinite linear; +} + +/* This is used for highlighting new content on screen. */ +.ipe-highlight { + animation: ipe-blink .4s ease-in-out 2; +} + +@keyframes ipe-blink { + from, + to { + box-shadow: 0 0 0 1px transparent; + } + 50% { + box-shadow: 0 0 0 2px rgba(88, 160, 44, 0.70); + } +} + +/* Drag/drop styles for blocks. */ +.ipe-droppable { + display: none; + width: 100%; + height: 30px; + margin: 5px 0 5px; + background-color: transparent; + border: 1px dashed #3c3c3c; + transition: .2s; +} + +.ipe-droppable.active { + display: block; +} + +.ipe-droppable.hover { + background-color: rgba(88, 160, 44, 0.70); + border-color: rgb(67, 125, 33); +} + +/* Style the BlockPicker. */ +.ipe-category-picker-bottom { + overflow-x: scroll; + white-space: nowrap; + margin: auto; + padding: 20px 20px 0 20px; + min-height: 70px; +} + +.ipe-category-picker-bottom.top-open { + border-top: 1px solid darkgray; +} + +.ipe-category-picker-top { + display: none; + overflow-y: scroll; +} + +.ipe-category-picker-top.active { + display: block; + padding: 10px 0 10px 0; + max-height: 100%; +} + +.ipe-category-picker-search { + border-bottom: 1px solid lightgray; + padding-bottom: 5px; +} + +.ipe-category-picker-search input { + display: inline-block; + width: inherit; + margin-left: 5px; +} + +.ipe-category-picker-search input[type="submit"] { + display: none; + background: white; + color: black; + border: 1px solid lightgray; + border-radius: 5px; + padding: 6px; + transition: .2s; +} + +#panels-ipe-tray .ipe-category { + margin-right: 5px; +} + +.ipe-category { + position: relative; + color: black; + display: inline-block; + padding: 10px; + text-transform: capitalize; + font-size: 15px; + border: 1px solid transparent; + border-radius: 5px; + transition: .2s; + cursor: pointer; +} + +.ipe-category h4, +.ipe-category h5 { + margin: 0; +} + +.ipe-category h4 { + font-size: 18px; +} + +.ipe-category p { + text-transform: none; + font-weight: normal; +} + +.ipe-category:hover, +.ipe-category.active { + border-color: rgb(67, 125, 33); + color: inherit; +} + +/* Indicate an active block category with an arrow. */ +.ipe-category.active:after { + bottom: 100%; + left: 50%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; +} + +.ipe-category.active:after { + border-color: transparent; + border-bottom-color: rgb(67, 125, 33); + border-width: 10px; + margin-left: -10px; +} + +/* Style the create button a bit differently than other categories. */ +.ipe-create-category { + border: 1px solid lightgray; + background: #fbfbfb; +} + +.ipe-create-category:hover { + background: white; +} + +.ipe-category-count { + color: white; + background: black; + border-radius: 50%; + margin-left: 5px; + font-size: 10px; + height: 20px; + width: 20px; + line-height: 20px; + display: inline-block; + vertical-align: middle; +} + +/* Display block picker elements in a flex grid. */ +.ipe-block-picker-list .ipe-category-picker-top.active { + display: inline-flex; + flex-flow: row wrap; + justify-content: center; +} + +.ipe-category-picker-top.active.form-displayed { + display: block; +} + +.ipe-blockpicker-item { + flex-basis: 240px; + width: 240px; + margin-left: 10px; + margin-top: 10px; + text-align: left; +} + +.ipe-blockpicker-item-info { + font-size: 12px; +} + +.ipe-blockpicker-item h5 { + position: relative; + bottom: 0; + margin: 0; + display: inline-block; + padding: 10px 20px 10px 10px; + font-size: 12px; + line-height: 1.5em; + font-weight: bold; + text-transform: capitalize; +} + +.ipe-blockpicker-item p { + font-size: 12px; +} + +.ipe-blockpicker-item a { + border: 1px solid #333; + background: #fbfbfb; + display: flex; + flex-direction: column; + position: relative; + height: 100%; + border-radius: 0; + padding: 0; + transition: .2s; + color: inherit; + cursor: pointer; +} + +.ipe-blockpicker-item a:after { + content: "+"; + display: inline-block; + position: absolute; + right: 10px; + top: 20%; + font-size: 16px; +} + +.ipe-blockpicker-item a:hover { + background: white; + border-color: rgb(67, 125, 33); + color: rgb(67, 125, 33); +} + +.ipe-block-content-type-info p { + padding: 0 10px 10px 10px; +} + +/* Theme the category view. */ + +.ipe-category-picker-top > h4 { + text-transform: uppercase; + display: block; + width: 100%; + border-bottom: 1px solid darkgray; + padding: 5px; + margin: 0 0 10px 0; + font-size: 18px; +} + +/* Theme the block plugin and layout forms. */ + +.ipe-block-form, +.ipe-layout-form { + text-align: left; + margin: 0 auto; + display: inline-block; +} + +.ipe-form label { + display: inline-block; + text-transform: uppercase; + margin-right: 5px; +} + +.ipe-form summary { + text-transform: uppercase; + background: transparent; +} + +.ipe-form .ipe-icon-loading { + text-align: center; + width: 100%; +} + +.ipe-form input[type="submit"] { + text-transform: uppercase; + margin: 0; + background: white; + color: black; + border-radius: 5px; + display: inline-block; + transition: .2s; +} + +.ipe-form input[type="submit"]:hover { + border-color: rgb(67, 125, 33); + color: black; + background: white; +} + +.ipe-form { + max-width: 650px; + padding: 5px; +} + +/* Vertical-tabs specific styling. */ +.ipe-form .vertical-tabs__menu { + margin: 0; +} + +.ipe-form .vertical-tabs__menu-item > a { + margin: inherit; + display: block !important; +} + +.ipe-form .vertical-tabs__menu-item-summary { + display: block !important; +} + +/* Styles required to do the card-flip affect. */ +/* Credit to https://davidwalsh.name/css-flip for the original CSS. */ +.ipe-block-form .flip-container { + perspective: 1000; +} +/* flip the pane when hovered */ +.ipe-block-form.flipped .flipper, +.ipe-block-form.flipped .flipper { + transform: rotateY(180deg); +} + +/* flip speed goes here */ +.ipe-block-form .flipper { + transition: 0.6s; + transform-style: preserve-3d; + position: relative; +} + +/* hide back of pane during swap */ +.ipe-block-form .front, +.ipe-block-form .back { + backface-visibility: hidden; + background: white; + overflow: hidden; +} + +/* front pane, placed above back */ +.ipe-block-form .front { + z-index: 2; + /* for firefox 31 */ + transform: rotateY(0deg); +} + +/* back, initially hidden pane */ +.ipe-block-form .back { + transform: rotateY(180deg); + position: absolute; + top: 0; + right: 0; +} + +/* Mobile specific styles. */ +@media screen and (max-width: 40em) { + + /* Only display the search "button" on mobile. */ + .ipe-category-picker-search input[type="submit"] { + display: inline-block; + } + + /* Hide tab titles on mobile. */ + .ipe-tab-title { + display: none; + } + + /* Increase tab padding on mobile. */ + .ipe-tab a { + padding: 10px 15px 12px 15px; + } + + /* Show block plugins in one column. */ + .ipe-blockpicker-item { + display: block; + width: inherit; + } + + /* Increase font size on block plugins. */ + .ipe-blockpicker-item h5 { + font-size: 16px; + } + + /* Put the actions below the block title. */ + div.ipe-actions { + display: block; + } + + /* Put actions to the right of the bar. */ + .ipe-actions ul.ipe-action-list { + text-align: right; + } + +} diff --git a/docroot/modules/contrib/panels/panels_ipe/fonts/README.txt b/docroot/modules/contrib/panels/panels_ipe/fonts/README.txt new file mode 100644 index 000000000..fe0481634 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/fonts/README.txt @@ -0,0 +1,9 @@ +Our icon font file (ipeicons.woff) is auto-generated from https://icomoon.io/. + +If you wish to add or change an icon in ipeicons.woof, visit +https://icomoon.io/app/#/select and import the selection.json file. You can +then import new SVGs to add to the font from your preferred source. + +If IcoMoon ever goes offline, we can move to using the full Google Material +icons font, which is larger but potentially easier to maintain than what we do +now. diff --git a/docroot/modules/contrib/panels/panels_ipe/fonts/ipeicons.woff b/docroot/modules/contrib/panels/panels_ipe/fonts/ipeicons.woff new file mode 100755 index 000000000..d817e2e60 Binary files /dev/null and b/docroot/modules/contrib/panels/panels_ipe/fonts/ipeicons.woff differ diff --git a/docroot/modules/contrib/panels/panels_ipe/fonts/selection.json b/docroot/modules/contrib/panels/panels_ipe/fonts/selection.json new file mode 100755 index 000000000..b235cdea5 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/fonts/selection.json @@ -0,0 +1,401 @@ +{ + "IcoMoonType": "selection", + "icons": [ + { + "icon": { + "paths": [ + "M661.333 597.333h-33.707l-11.947-11.52c41.813-48.64 66.987-111.787 66.987-180.48 0-153.173-124.16-277.333-277.333-277.333s-277.333 124.16-277.333 277.333 124.16 277.333 277.333 277.333c68.693 0 131.84-25.173 180.48-66.987l11.52 11.947v33.707l213.333 212.907 63.573-63.573-212.907-213.333zM405.333 597.333c-106.24 0-192-85.76-192-192s85.76-192 192-192 192 85.76 192 192-85.76 192-192 192z" + ], + "attrs": [], + "isMulticolor": false, + "grid": 0, + "tags": [ + "ic_search_black_24px (1)" + ] + }, + "attrs": [], + "properties": { + "order": 17, + "id": 13, + "prevSize": 32, + "code": 59661, + "name": "ic_search_black_24px1" + }, + "setIdx": 0, + "setId": 5, + "iconIdx": 0 + }, + { + "icon": { + "paths": [ + "M810.667 273.493l-60.16-60.16-238.507 238.507-238.507-238.507-60.16 60.16 238.507 238.507-238.507 238.507 60.16 60.16 238.507-238.507 238.507 238.507 60.16-60.16-238.507-238.507z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "ic_close_black_24px" + ], + "grid": 0 + }, + "attrs": [], + "properties": { + "order": 16, + "id": 0, + "prevSize": 32, + "code": 59660, + "name": "ic_close_black_24px" + }, + "setIdx": 0, + "setId": 5, + "iconIdx": 1 + }, + { + "icon": { + "paths": [ + "M597.333 85.333h-341.333c-46.933 0-84.907 38.4-84.907 85.333l-0.427 682.667c0 46.933 37.973 85.333 84.907 85.333h512.427c46.933 0 85.333-38.4 85.333-85.333v-512l-256-256zM682.667 682.667h-128v128h-85.333v-128h-128v-85.333h128v-128h85.333v128h128v85.333zM554.667 384v-234.667l234.667 234.667h-234.667z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "ic_note_add_black_24px" + ], + "grid": 0 + }, + "attrs": [], + "properties": { + "order": 14, + "id": 1, + "prevSize": 32, + "code": 59659, + "name": "ic_note_add_black_24px" + }, + "setIdx": 0, + "setId": 5, + "iconIdx": 2 + }, + { + "icon": { + "paths": [ + "M512 85.333c-235.947 0-426.667 190.72-426.667 426.667s190.72 426.667 426.667 426.667 426.667-190.72 426.667-426.667-190.72-426.667-426.667-426.667zM725.333 665.173l-60.16 60.16-153.173-153.173-153.173 153.173-60.16-60.16 153.173-153.173-153.173-153.173 60.16-60.16 153.173 153.173 153.173-153.173 60.16 60.16-153.173 153.173 153.173 153.173z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "ic_cancel_black_24px" + ], + "grid": 0 + }, + "attrs": [], + "properties": { + "order": 13, + "id": 2, + "prevSize": 32, + "code": 59658, + "name": "ic_cancel_black_24px" + }, + "setIdx": 0, + "setId": 5, + "iconIdx": 3 + }, + { + "icon": { + "paths": [ + "M829.013 553.813c1.707-13.653 2.987-27.307 2.987-41.813s-1.28-28.16-2.987-41.813l90.027-70.4c8.107-6.4 10.24-17.92 5.12-27.307l-85.333-147.627c-5.12-9.387-16.64-12.8-26.027-9.387l-106.24 42.667c-22.187-17.067-46.080-31.147-72.107-41.813l-16.213-113.067c-1.28-10.24-10.24-17.92-20.907-17.92h-170.667c-10.667 0-19.627 7.68-20.907 17.92l-16.213 113.067c-26.027 10.667-49.92 25.173-72.107 41.813l-106.24-42.667c-9.813-3.84-20.907 0-26.027 9.387l-85.333 147.627c-5.547 9.387-2.987 20.907 5.12 27.307l90.027 70.4c-1.707 13.653-2.987 27.733-2.987 41.813s1.28 28.16 2.987 41.813l-90.027 70.4c-8.107 6.4-10.24 17.92-5.12 27.307l85.333 147.627c5.12 9.387 16.64 12.8 26.027 9.387l106.24-42.667c22.187 17.067 46.080 31.147 72.107 41.813l16.213 113.067c1.28 10.24 10.24 17.92 20.907 17.92h170.667c10.667 0 19.627-7.68 20.907-17.92l16.213-113.067c26.027-10.667 49.92-25.173 72.107-41.813l106.24 42.667c9.813 3.84 20.907 0 26.027-9.387l85.333-147.627c5.12-9.387 2.987-20.907-5.12-27.307l-90.027-70.4zM512 661.333c-82.347 0-149.333-66.987-149.333-149.333s66.987-149.333 149.333-149.333 149.333 66.987 149.333 149.333-66.987 149.333-149.333 149.333z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "ic_settings_black_24px" + ], + "grid": 0 + }, + "attrs": [], + "properties": { + "order": 12, + "id": 3, + "prevSize": 32, + "code": 59657, + "name": "ic_settings_black_24px" + }, + "setIdx": 0, + "setId": 5, + "iconIdx": 4 + }, + { + "icon": { + "paths": [ + "M810.667 554.667h-597.333v-85.333h597.333v85.333z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "ic_remove_black_24px" + ], + "grid": 0 + }, + "attrs": [], + "properties": { + "order": 6, + "id": 4, + "prevSize": 32, + "code": 59656, + "name": "ic_remove_black_24px" + }, + "setIdx": 0, + "setId": 5, + "iconIdx": 5 + }, + { + "icon": { + "paths": [ + "M512 170.667v-128l-170.667 170.667 170.667 170.667v-128c141.227 0 256 114.773 256 256 0 43.093-10.667 84.053-29.867 119.467l62.293 62.293c33.28-52.48 52.907-114.773 52.907-181.76 0-188.587-152.747-341.333-341.333-341.333zM512 768c-141.227 0-256-114.773-256-256 0-43.093 10.667-84.053 29.867-119.467l-62.293-62.293c-33.28 52.48-52.907 114.773-52.907 181.76 0 188.587 152.747 341.333 341.333 341.333v128l170.667-170.667-170.667-170.667v128z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "ic_sync_black_24px" + ], + "grid": 0 + }, + "attrs": [], + "properties": { + "order": 10, + "id": 5, + "prevSize": 32, + "code": 59655, + "name": "ic_sync_black_24px" + }, + "setIdx": 0, + "setId": 5, + "iconIdx": 6 + }, + { + "icon": { + "paths": [ + "M512 341.333l-256 256 60.16 60.16 195.84-195.413 195.84 195.413 60.16-60.16z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "ic_expand_less_black_24px" + ], + "grid": 0 + }, + "attrs": [], + "properties": { + "order": 9, + "id": 6, + "prevSize": 32, + "code": 59648, + "name": "ic_expand_less_black_24px" + }, + "setIdx": 0, + "setId": 5, + "iconIdx": 7 + }, + { + "icon": { + "paths": [ + "M707.84 366.507l-195.84 195.413-195.84-195.413-60.16 60.16 256 256 256-256z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "ic_expand_more_black_24px" + ], + "grid": 0 + }, + "attrs": [], + "properties": { + "order": 6, + "id": 7, + "prevSize": 32, + "code": 59649, + "name": "ic_expand_more_black_24px" + }, + "setIdx": 0, + "setId": 5, + "iconIdx": 8 + }, + { + "icon": { + "paths": [ + "M42.667 896h938.667l-469.333-810.667-469.333 810.667zM554.667 768h-85.333v-85.333h85.333v85.333zM554.667 597.333h-85.333v-170.667h85.333v170.667z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "ic_warning_black_24px" + ], + "grid": 0 + }, + "attrs": [], + "properties": { + "order": 5, + "id": 8, + "prevSize": 32, + "code": 59650, + "name": "ic_warning_black_24px" + }, + "setIdx": 0, + "setId": 5, + "iconIdx": 9 + }, + { + "icon": { + "paths": [ + "M128 554.667h341.333v-426.667h-341.333v426.667zM128 896h341.333v-256h-341.333v256zM554.667 896h341.333v-426.667h-341.333v426.667zM554.667 128v256h341.333v-256h-341.333z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "tab_change_layout" + ], + "grid": 0 + }, + "attrs": [], + "properties": { + "order": 4, + "id": 9, + "prevSize": 32, + "code": 59651, + "name": "tab_change_layout" + }, + "setIdx": 0, + "setId": 5, + "iconIdx": 10 + }, + { + "icon": { + "paths": [ + "M128 736v160h160l471.893-471.893-160-160-471.893 471.893zM883.627 300.373c16.64-16.64 16.64-43.52 0-60.16l-99.84-99.84c-16.64-16.64-43.52-16.64-60.16 0l-78.080 78.080 160 160 78.080-78.080z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "tab_edit" + ], + "grid": 0 + }, + "attrs": [], + "properties": { + "order": 3, + "id": 10, + "prevSize": 32, + "code": 59652, + "name": "tab_edit" + }, + "setIdx": 0, + "setId": 5, + "iconIdx": 11 + }, + { + "icon": { + "paths": [ + "M682.667 42.667h-512c-46.933 0-85.333 38.4-85.333 85.333v597.333h85.333v-597.333h512v-85.333zM810.667 213.333h-469.333c-46.933 0-85.333 38.4-85.333 85.333v597.333c0 46.933 38.4 85.333 85.333 85.333h469.333c46.933 0 85.333-38.4 85.333-85.333v-597.333c0-46.933-38.4-85.333-85.333-85.333zM810.667 896h-469.333v-597.333h469.333v597.333z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "tab_manage_content" + ], + "grid": 0 + }, + "attrs": [], + "properties": { + "order": 4, + "id": 11, + "prevSize": 32, + "code": 59653, + "name": "tab_manage_content" + }, + "setIdx": 0, + "setId": 5, + "iconIdx": 12 + }, + { + "icon": { + "paths": [ + "M725.333 128h-512c-47.36 0-85.333 38.4-85.333 85.333v597.333c0 46.933 37.973 85.333 85.333 85.333h597.333c46.933 0 85.333-38.4 85.333-85.333v-512l-170.667-170.667zM512 810.667c-70.827 0-128-57.173-128-128s57.173-128 128-128 128 57.173 128 128-57.173 128-128 128zM640 384h-426.667v-170.667h426.667v170.667z" + ], + "attrs": [], + "isMulticolor": false, + "tags": [ + "tab_save" + ], + "grid": 0 + }, + "attrs": [], + "properties": { + "order": 1, + "id": 12, + "prevSize": 32, + "code": 59654, + "name": "tab_save" + }, + "setIdx": 0, + "setId": 5, + "iconIdx": 13 + }, + { + "icon": { + "paths": [ + "M800.128 451.776h-32v-133.44c0-140.48-113.728-254.336-254.272-254.336-140.608 0-254.464 113.856-254.464 254.336v133.44h-35.392c-17.6 0-32 14.4-32 32v382.080c0 17.6 14.4 32 32 32h576.128c17.6 0 32-14.4 32-32v-382.080c0-17.6-14.4-32-32-32zM387.392 318.336c0-69.632 56.64-126.336 126.464-126.336 69.632 0 126.272 56.64 126.272 126.336v133.44h-252.736v-133.44z" + ], + "attrs": [ + {} + ], + "isMulticolor": false, + "grid": 0, + "tags": [ + "lock" + ] + }, + "attrs": [ + {} + ], + "properties": { + "order": 12, + "id": 14, + "name": "lock", + "prevSize": 32, + "code": 59662 + }, + "setIdx": 0, + "setId": 5, + "iconIdx": 14 + } + ], + "height": 1024, + "metadata": { + "name": "icomoon" + }, + "preferences": { + "showGlyphs": true, + "showQuickUse": true, + "showQuickUse2": true, + "showSVGs": true, + "fontPref": { + "prefix": "icon-", + "metadata": { + "fontFamily": "icomoon" + }, + "metrics": { + "emSize": 1024, + "baseline": 6.25, + "whitespace": 50 + }, + "embed": false + }, + "imagePref": { + "prefix": "icon-", + "png": true, + "useClassSelector": true, + "color": 4473924, + "bgColor": 16777215, + "classSelector": ".icon" + }, + "historySize": 100, + "showCodes": true + } +} \ No newline at end of file diff --git a/docroot/modules/contrib/panels/panels_ipe/js/models/AppModel.js b/docroot/modules/contrib/panels/panels_ipe/js/models/AppModel.js new file mode 100644 index 000000000..23af097bb --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/js/models/AppModel.js @@ -0,0 +1,88 @@ +/** + * @file + * The primary Backbone model for Panels IPE. + * + * @see Drupal.panels_ipe.AppView + */ + +(function (Backbone, Drupal) { + + 'use strict'; + + /** + * @constructor + * + * @augments Backbone.Model + */ + Drupal.panels_ipe.AppModel = Backbone.Model.extend(/** @lends Drupal.panels_ipe.AppModel# */{ + + /** + * @type {object} + * + * @prop {bool} active + * @prop {Drupal.panels_ipe.TabModel} activeTab + * @prop {Drupal.panels_ipe.BlockModel} activeBlock + * @prop {Drupal.panels_ipe.RegionModel} activeRegion + */ + defaults: /** @lends Drupal.panels_ipe.AppModel# */{ + + /** + * Whether or not the editing part of the application is active. + * + * @type {bool} + */ + active: false, + + /** + * The current Layout. + * + * @type {Drupal.panels_ipe.LayoutModel} + */ + layout: null, + + /** + * A collection of all tabs on screen. + * + * @type {Drupal.panels_ipe.TabCollection} + */ + tabCollection: null, + + /** + * The "Locked" tab. + * + * @type {Drupal.panels_ipe.TabModel} + */ + lockedTab: null, + + /** + * The "Edit" tab. + * + * @type {Drupal.panels_ipe.TabModel} + */ + editTab: null, + + /** + * The "Save" tab. + * + * @type {Drupal.panels_ipe.TabModel} + */ + saveTab: null, + + /** + * The "Cancel" tab. + * + * @type {Drupal.panels_ipe.TabModel} + */ + cancelTab: null, + + /** + * Whether or not there are unsaved changes. + * + * @type {bool} + */ + unsaved: false + } + + }); + +}(Backbone, Drupal)); diff --git a/docroot/modules/contrib/panels/panels_ipe/js/models/BlockContentTypeModel.js b/docroot/modules/contrib/panels/panels_ipe/js/models/BlockContentTypeModel.js new file mode 100644 index 000000000..4cacdc8f7 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/js/models/BlockContentTypeModel.js @@ -0,0 +1,72 @@ +/** + * @file + * Base Backbone model for a Block Content Type. + */ + +(function (_, $, Backbone, Drupal) { + + 'use strict'; + + Drupal.panels_ipe.BlockContentTypeModel = Backbone.Model.extend(/** @lends Drupal.panels_ipe.BlockContentTypeModel# */{ + + /** + * @type {object} + */ + defaults: /** @lends Drupal.panels_ipe.BlockContentTypeModel# */{ + + /** + * The content type ID. + * + * @type {string} + */ + id: null, + + /** + * Whether or not entities of this type are revisionable by default. + * + * @type {bool} + */ + revision: null, + + /** + * The content type label. + * + * @type {string} + */ + label: null, + + /** + * The content type description. + * + * @type {string} + */ + description: null + } + + }); + + /** + * @constructor + * + * @augments Backbone.Collection + */ + Drupal.panels_ipe.BlockContentTypeCollection = Backbone.Collection.extend(/** @lends Drupal.panels_ipe.BlockContentTypeCollection# */{ + + /** + * @type {Drupal.panels_ipe.BlockContentTypeModel} + */ + model: Drupal.panels_ipe.BlockContentTypeModel, + + /** + * @type {function} + * + * @return {string} + * The URL required to sync this collection with the server. + */ + url: function () { + return Drupal.panels_ipe.urlRoot(drupalSettings) + '/block_content/types'; + } + + }); + +}(_, jQuery, Backbone, Drupal)); diff --git a/docroot/modules/contrib/panels/panels_ipe/js/models/BlockModel.js b/docroot/modules/contrib/panels/panels_ipe/js/models/BlockModel.js new file mode 100644 index 000000000..930f83c4d --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/js/models/BlockModel.js @@ -0,0 +1,139 @@ +/** + * @file + * Base Backbone model for a Block. + */ + +(function (_, $, Backbone, Drupal, drupalSettings) { + + 'use strict'; + + Drupal.panels_ipe.BlockModel = Backbone.Model.extend(/** @lends Drupal.panels_ipe.BlockModel# */{ + + /** + * @type {object} + */ + defaults: /** @lends Drupal.panels_ipe.BlockModel# */{ + + /** + * The block state. + * + * @type {bool} + */ + active: false, + + /** + * The ID of the block. + * + * @type {string} + */ + id: null, + + /** + * The unique ID of the block. + * + * @type {string} + */ + uuid: null, + + /** + * The label of the block. + * + * @type {string} + */ + label: null, + + /** + * The provider for the block (usually the module name). + * + * @type {string} + */ + provider: null, + + /** + * The ID of the plugin for this block. + * + * @type {string} + */ + plugin_id: null, + + /** + * The HTML content of the block. This is stored in the model as the + * IPE doesn't actually care what the block's content is, the functional + * elements of the model are the metadata. The BlockView renders this + * wrapped inside IPE elements. + * + * @type {string} + */ + html: null, + + /** + * Whether or not this block is currently in a syncing state. + * + * @type {bool} + */ + syncing: false + + }, + + /** + * @type {function} + * + * @return {string} + * A URL that can be used to refresh this Block model. Only fetch methods + * are supported currently. + */ + url: function () { + return Drupal.panels_ipe.urlRoot(drupalSettings) + '/block/' + this.get('uuid'); + } + + }); + + /** + * @constructor + * + * @augments Backbone.Collection + */ + Drupal.panels_ipe.BlockCollection = Backbone.Collection.extend(/** @lends Drupal.panels_ipe.BlockCollection# */{ + + /** + * @type {Drupal.panels_ipe.BlockModel} + */ + model: Drupal.panels_ipe.BlockModel, + + /** + * For Blocks, our identifier is the UUID, not the ID. + * + * @type {function} + * + * @param {Object} attrs + * The attributes of the current model in the collection. + * + * @return {string} + * The value of a BlockModel's UUID. + */ + modelId: function (attrs) { + return attrs.uuid; + }, + + /** + * Moves a Block up or down in this collection. + * + * @type {function} + * + * @param {Drupal.panels_ipe.BlockModel} block + * The BlockModel you want to move. + * @param {string} direction + * The string name of the direction (either "up" or "down"). + */ + shift: function (block, direction) { + var index = this.indexOf(block); + if ((direction === 'up' && index > 0) || (direction === 'down' && index < this.models.length)) { + this.remove(block, {silent: true}); + var new_index = direction === 'up' ? index - 1 : index + 1; + this.add(block, {at: new_index, silent: true}); + } + } + + }); + +}(_, jQuery, Backbone, Drupal, drupalSettings)); diff --git a/docroot/modules/contrib/panels/panels_ipe/js/models/BlockPluginModel.js b/docroot/modules/contrib/panels/panels_ipe/js/models/BlockPluginModel.js new file mode 100644 index 000000000..51f01bab6 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/js/models/BlockPluginModel.js @@ -0,0 +1,104 @@ +/** + * @file + * Base Backbone model for a Block Plugin. + */ + +(function (_, $, Backbone, Drupal) { + + 'use strict'; + + Drupal.panels_ipe.BlockPluginModel = Backbone.Model.extend(/** @lends Drupal.panels_ipe.BlockPluginModel# */{ + + /** + * @type {object} + */ + defaults: /** @lends Drupal.panels_ipe.BlockPluginModel# */{ + + /** + * The plugin ID. + * + * @type {string} + */ + plugin_id: null, + + /** + * The block's id (machine name). + * + * @type {string} + */ + id: null, + + /** + * The plugin label. + * + * @type {string} + */ + label: null, + + /** + * The category of the plugin. + * + * @type {string} + */ + category: null, + + /** + * The provider for the block (usually the module name). + * + * @type {string} + */ + provider: null + + }, + + idAttribute: 'plugin_id' + + }); + + /** + * @constructor + * + * @augments Backbone.Collection + */ + Drupal.panels_ipe.BlockPluginCollection = Backbone.Collection.extend(/** @lends Drupal.panels_ipe.BlockPluginCollection# */{ + + /** + * @type {Drupal.panels_ipe.BlockPluginModel} + */ + model: Drupal.panels_ipe.BlockPluginModel, + + /** + * Defines a sort parameter for the collection. + * + * @type {string} + */ + comparator: 'category', + + /** + * For Block Plugins, our identifier is the plugin id. + * + * @type {function} + * + * @param {Object} attrs + * The attributes of the current model in the collection. + * + * @return {string} + * A string representing a BlockPlugin's id. + */ + modelId: function (attrs) { + return attrs.plugin_id; + }, + + /** + * @type {function} + * + * @return {string} + * The URL required to sync this collection with the server. + */ + url: function () { + return Drupal.panels_ipe.urlRoot(drupalSettings) + '/block_plugins'; + } + + }); + +}(_, jQuery, Backbone, Drupal)); diff --git a/docroot/modules/contrib/panels/panels_ipe/js/models/LayoutModel.js b/docroot/modules/contrib/panels/panels_ipe/js/models/LayoutModel.js new file mode 100644 index 000000000..c23b64f43 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/js/models/LayoutModel.js @@ -0,0 +1,148 @@ +/** + * @file + * Base Backbone model for a Layout. + */ + +(function (_, $, Backbone, Drupal) { + + 'use strict'; + + Drupal.panels_ipe.LayoutModel = Backbone.Model.extend(/** @lends Drupal.panels_ipe.LayoutModel# */{ + + /** + * @type {object} + */ + defaults: /** @lends Drupal.panels_ipe.LayoutModel# */{ + + /** + * The layout machine name. + * + * @type {string} + */ + id: null, + + /** + * Whether or not this was the original layout for the variant. + * + * @type {bool} + */ + original: false, + + /** + * The layout label. + * + * @type {string} + */ + label: null, + + /** + * The layout icon. + * + * @type {string} + */ + icon: null, + + /** + * Whether or not this is the current layout. + * + * @type {bool} + */ + current: false, + + /** + * The wrapping HTML for this layout. Only used for initial rendering. + * + * @type {string} + */ + html: null, + + /** + * A collection of regions contained in this Layout. + * + * @type {Drupal.panels_ipe.RegionCollection} + */ + regionCollection: null, + + /** + * An array of Block UUIDs that we need to delete. + * + * @type {Array} + */ + deletedBlocks: [] + + + }, + + /** + * Overrides the isNew method to mark if this is the initial layout or not. + * + * @return {bool} + * A boolean which determines if this Block was on the page on load. + */ + isNew: function () { + return !this.get('original'); + }, + + /** + * Overrides the parse method to set our regionCollection dynamically. + * + * @param {Object} resp + * The decoded JSON response from the backend server. + * @param {Object} options + * Additional options passed to parse. + * + * @return {Object} + * An object representing a LayoutModel's attributes. + */ + parse: function (resp, options) { + // If possible, initialize our region collection. + if (typeof resp.regions != 'undefined') { + resp.regionCollection = new Drupal.panels_ipe.RegionCollection(); + for (var i in resp.regions) { + if (resp.regions.hasOwnProperty(i)) { + var region = new Drupal.panels_ipe.RegionModel(resp.regions[i]); + region.set({blockCollection: new Drupal.panels_ipe.BlockCollection()}); + resp.regionCollection.add(region); + } + } + } + return resp; + }, + + /** + * @type {function} + * + * @return {string} + * A URL that can be used to refresh this Layout's attributes. + */ + url: function () { + return Drupal.panels_ipe.urlRoot(drupalSettings) + '/layouts/' + this.get('id'); + } + + }); + + /** + * @constructor + * + * @augments Backbone.Collection + */ + Drupal.panels_ipe.LayoutCollection = Backbone.Collection.extend(/** @lends Drupal.panels_ipe.LayoutCollection# */{ + + /** + * @type {Drupal.panels_ipe.LayoutModel} + */ + model: Drupal.panels_ipe.LayoutModel, + + /** + * @type {function} + * + * @return {string} + * A URL that can be used to refresh this collection's child models. + */ + url: function () { + return Drupal.panels_ipe.urlRoot(drupalSettings) + '/layouts'; + } + + }); + +}(_, jQuery, Backbone, Drupal)); diff --git a/docroot/modules/contrib/panels/panels_ipe/js/models/RegionModel.js b/docroot/modules/contrib/panels/panels_ipe/js/models/RegionModel.js new file mode 100644 index 000000000..2d9bcff80 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/js/models/RegionModel.js @@ -0,0 +1,125 @@ +/** + * @file + * Base Backbone model for a Region. + * + * @todo Support sync operations to refresh a region, even if we don't have + * a use case for that yet. + */ + +(function (_, $, Backbone, Drupal) { + + 'use strict'; + + Drupal.panels_ipe.RegionModel = Backbone.Model.extend(/** @lends Drupal.panels_ipe.RegionModel# */{ + + /** + * @type {object} + */ + defaults: /** @lends Drupal.panels_ipe.RegionModel# */{ + + /** + * The machine name of the region. + * + * @type {string} + */ + name: null, + + /** + * The label of the region. + * + * @type {string} + */ + label: null, + + /** + * A BlockCollection for all blocks in this region. + * + * @type {Drupal.panels_ipe.BlockCollection} + * + * @see Drupal.panels_ipe.BlockCollection + */ + blockCollection: null + }, + + /** + * Checks if our BlockCollection contains a given Block UUID. + * + * @param {string} block_uuid + * The universally unique identifier of the block. + * + * @return {boolean} + * Whether the BlockCollection contains the block. + */ + hasBlock: function (block_uuid) { + return this.get('blockCollection').get(block_uuid) ? true : false; + }, + + /** + * Gets a Block from our BlockCollection based on its UUID. + * + * @param {string} block_uuid + * The universally unique identifier of the block. + * + * @return {Drupal.panels_ipe.BlockModel|undefined} + * The block if it is inside this region. + */ + getBlock: function (block_uuid) { + return this.get('blockCollection').get(block_uuid); + }, + + /** + * Removes a Block from our BlockCollection based on its UUID. + * + * @param {Drupal.panels_ipe.BlockModel|string} block + * The block or it's universally unique identifier. + * @param {object} options + * Block related configuration. + */ + removeBlock: function (block, options) { + this.get('blockCollection').remove(block, options); + }, + + /** + * Adds a new BlockModel to our BlockCollection. + * + * @param {Drupal.panels_ipe.BlockModel} block + * The block that needs to be added. + * @param {object} options + * Block related configuration. + */ + addBlock: function (block, options) { + this.get('blockCollection').add(block, options); + } + + }); + + /** + * @constructor + * + * @augments Backbone.Collection + */ + Drupal.panels_ipe.RegionCollection = Backbone.Collection.extend(/** @lends Drupal.panels_ipe.RegionCollection# */{ + + /** + * @type {Drupal.panels_ipe.RegionModel} + */ + model: Drupal.panels_ipe.RegionModel, + + /** + * For Regions, our identifier is the region name. + * + * @type {function} + * + * @param {Object} attrs + * The current RegionModel's attributes. + * + * @return {string} + * The current RegionModel's name attribute. + */ + modelId: function (attrs) { + return attrs.name; + } + + }); + +}(_, jQuery, Backbone, Drupal)); diff --git a/docroot/modules/contrib/panels/panels_ipe/js/models/TabModel.js b/docroot/modules/contrib/panels/panels_ipe/js/models/TabModel.js new file mode 100644 index 000000000..97f5d5466 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/js/models/TabModel.js @@ -0,0 +1,78 @@ +/** + * @file + * A . + * + * @see Drupal.panels_ipe.TabView + */ + +(function (Backbone, Drupal) { + + 'use strict'; + + /** + * @constructor + * + * @augments Backbone.Model + */ + Drupal.panels_ipe.TabModel = Backbone.Model.extend(/** @lends Drupal.panels_ipe.TabModel# */{ + + /** + * @type {object} + * + * @prop {bool} active + * @prop {string} title + */ + defaults: /** @lends Drupal.panels_ipe.TabModel# */{ + + /** + * The ID of the tab. + * + * @type {int} + */ + id: null, + + /** + * Whether or not the tab is active. + * + * @type {bool} + */ + active: false, + + /** + * Whether or not the tab is hidden. + * + * @type {bool} + */ + hidden: false, + + /** + * Whether or not the tab is loading. + * + * @type {bool} + */ + loading: false, + + /** + * The title of the tab. + * + * @type {string} + */ + title: null + } + + }); + + /** + * @constructor + * + * @augments Backbone.Collection + */ + Drupal.panels_ipe.TabCollection = Backbone.Collection.extend(/** @lends Drupal.panels_ipe.TabCollection# */{ + + /** + * @type {Drupal.panels_ipe.TabModel} + */ + model: Drupal.panels_ipe.TabModel + }); + +}(Backbone, Drupal)); diff --git a/docroot/modules/contrib/panels/panels_ipe/js/panels_ipe.js b/docroot/modules/contrib/panels/panels_ipe/js/panels_ipe.js new file mode 100644 index 000000000..fa758b61f --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/js/panels_ipe.js @@ -0,0 +1,239 @@ +/** + * @file + * Attaches behavior for the Panels IPE module. + * + */ + +(function ($, _, Backbone, Drupal, drupalSettings) { + + 'use strict'; + + /** + * Contains initial Backbone initialization for the IPE. + * + * @type {Drupal~behavior} + */ + Drupal.behaviors.panels_ipe = { + attach: function (context, settings) { + // Perform initial setup of our app. + $('body').once('panels-ipe-init').each(Drupal.panels_ipe.init, [settings]); + + // @todo Make every settings-related thing a generic event, or add a + // panels_ipe event command to Drupal.ajax. + + // We need to add/update a new BlockModel somewhere. Inform the App that + // this has occurred. + if (settings['panels_ipe']['updated_block']) { + var blockData = settings['panels_ipe']['updated_block']; + // Remove the setting. + delete settings['panels_ipe']['updated_block']; + // Create a BlockModel. + var block = new Drupal.panels_ipe.BlockModel(blockData); + // Trigger the event. + Drupal.panels_ipe.app.trigger('addBlockPlugin', block, blockData.region); + } + + // We need to add/update our Layout Inform the App that this has occurred. + if (settings['panels_ipe']['updated_layout']) { + var layoutData = settings['panels_ipe']['updated_layout']; + // Remove the setting. + delete settings['panels_ipe']['updated_layout']; + // Create a LayoutModel. + layoutData = Drupal.panels_ipe.LayoutModel.prototype.parse(layoutData); + var layout = new Drupal.panels_ipe.LayoutModel(layoutData); + // Trigger the event. + Drupal.panels_ipe.app.trigger('changeLayout', layout); + } + + // Toggle the preview - We need to do this with drupalSettings as the + // animation won't work if triggered by a form submit. It must occur after + // the form is rendered. + if (context.className === 'panels-ipe-block-plugin-form flip-container' + && settings['panels_ipe']['toggle_preview']) { + var $form = $('.ipe-block-form'); + + // Flip the form. + $form.toggleClass('flipped'); + + // Calculate and set new heights, if appropriate. + Drupal.panels_ipe.setFlipperHeight($form); + + // As images can load late on new content, recalculate the flipper + // height on image load. + $form.find('img').each(function () { + $(this).load(function () { + Drupal.panels_ipe.setFlipperHeight($form); + }); + }); + + delete settings['panels_ipe']['toggle_preview']; + } + + // A new Block Content entity has been created. Trigger an app-level event + // to switch tabs and open the placement form. + if (settings['panels_ipe']['new_block_content']) { + var newBlockData = settings['panels_ipe']['new_block_content']; + delete settings['panels_ipe']['new_block_content']; + Drupal.panels_ipe.app.trigger('addContentBlock', newBlockData); + } + + // A Block Content entity has been edited. + if (settings['panels_ipe']['edit_block_content']) { + var editBlockData = settings['panels_ipe']['edit_block_content']; + delete settings['panels_ipe']['edit_block_content']; + Drupal.panels_ipe.app.trigger('editContentBlockDone', editBlockData); + } + } + }; + + /** + * @namespace + */ + Drupal.panels_ipe = {}; + + /** + * Setups up our initial Collection and Views based on the current settings. + * + * @param {Object} settings + * The contextual drupalSettings. + */ + Drupal.panels_ipe.init = function (settings) { + settings = settings || drupalSettings; + // Set up our initial tabs. + var tab_collection = new Drupal.panels_ipe.TabCollection(); + + if (!settings.panels_ipe.locked) { + if (settings.panels_ipe.user_permission.change_layout) { + tab_collection.add(createTabModel(Drupal.t('Change Layout'), 'change_layout')); + } + tab_collection.add(createTabModel(Drupal.t('Manage Content'), 'manage_content')); + } + + // The edit/save/cancel tabs are special, and are tracked by our app. + var edit_tab = createTabModel(Drupal.t('Edit'), 'edit'); + var save_tab = createTabModel(Drupal.t('Save'), 'save'); + var cancel_tab = createTabModel(Drupal.t('Cancel'), 'cancel'); + var locked_tab = createTabModel(Drupal.t('Locked'), 'locked'); + tab_collection.add(edit_tab); + tab_collection.add(save_tab); + tab_collection.add(cancel_tab); + tab_collection.add(locked_tab); + + // Create a global(ish) AppModel. + Drupal.panels_ipe.app = new Drupal.panels_ipe.AppModel({ + tabCollection: tab_collection, + editTab: edit_tab, + saveTab: save_tab, + cancelTab: cancel_tab, + lockedTab: locked_tab, + locked: settings.panels_ipe.locked, + unsaved: settings.panels_ipe.unsaved + }); + + // Set up our initial tab views. + var tab_views = {}; + if (settings.panels_ipe.user_permission.change_layout) { + tab_views.change_layout = new Drupal.panels_ipe.LayoutPicker(); + } + tab_views.manage_content = new Drupal.panels_ipe.BlockPicker(); + + // Create an AppView instance. + Drupal.panels_ipe.app_view = new Drupal.panels_ipe.AppView({ + model: Drupal.panels_ipe.app, + el: '#panels-ipe-tray', + tabContentViews: tab_views + }); + + // Assemble the initial region and block collections. + // This logic is a little messy, as traditionally we would never initialize + // Backbone with existing HTML content. + var region_collection = new Drupal.panels_ipe.RegionCollection(); + for (var i in settings.panels_ipe.regions) { + if (settings.panels_ipe.regions.hasOwnProperty(i)) { + var region = new Drupal.panels_ipe.RegionModel(); + region.set(settings.panels_ipe.regions[i]); + + var block_collection = new Drupal.panels_ipe.BlockCollection(); + for (var j in settings.panels_ipe.regions[i].blocks) { + if (settings.panels_ipe.regions[i].blocks.hasOwnProperty(j)) { + // Add a new block model. + var block = new Drupal.panels_ipe.BlockModel(); + block.set(settings.panels_ipe.regions[i].blocks[j]); + block_collection.add(block); + } + } + + region.set({blockCollection: block_collection}); + + region_collection.add(region); + } + } + + // Create the Layout model/view. + var layout = new Drupal.panels_ipe.LayoutModel(settings.panels_ipe.layout); + layout.set({regionCollection: region_collection}); + var layout_view = new Drupal.panels_ipe.LayoutView({ + model: layout, + el: '#panels-ipe-content' + }); + + Drupal.panels_ipe.app.set({layout: layout}); + Drupal.panels_ipe.app_view.layoutView = layout_view; + + // Trigger a global Backbone event informing other Views that we're done + // initializing and ready to render. + Backbone.trigger('PanelsIPEInitialized'); + + // Render our AppView, without rendering the layout. + $('body').append(Drupal.panels_ipe.app_view.render(false).$el); + + // Set our initial URL root. + Drupal.panels_ipe.setUrlRoot(settings); + + function createTabModel(title, id) { + return new Drupal.panels_ipe.TabModel({title: title, id: id}); + } + }; + + Drupal.panels_ipe.setFlipperHeight = function ($form) { + // The preview could be larger than the form. + // Manually set the height to be sure that things fit. + var $new_side; + var $current_side; + if ($form.hasClass('flipped')) { + $new_side = $form.find('.flipper > .back'); + $current_side = $form.find('.flipper > .front'); + } + else { + $new_side = $form.find('.flipper > .front'); + $current_side = $form.find('.flipper > .back'); + } + + $current_side.animate({height: $new_side.outerHeight() + 10}, 600); + }; + + /** + * Returns the urlRoot for all callbacks. + * + * @param {Object} settings + * The contextual drupalSettings. + * + * @return {string} + * A base path for most other URL callbacks in this App. + */ + Drupal.panels_ipe.urlRoot = function (settings) { + return settings.panels_ipe.url_root; + }; + + /** + * Sets the urlRoot for all callbacks. + * + * @param {Object} settings + * The contextual drupalSettings. + */ + Drupal.panels_ipe.setUrlRoot = function (settings) { + var panels_display = settings.panels_ipe.panels_display; + settings.panels_ipe.url_root = settings.path.baseUrl + settings.path.pathPrefix + 'admin/panels_ipe/variant/' + panels_display.storage_type + '/' + panels_display.storage_id; + }; + +}(jQuery, _, Backbone, Drupal, drupalSettings)); diff --git a/docroot/modules/contrib/panels/panels_ipe/js/views/AppView.js b/docroot/modules/contrib/panels/panels_ipe/js/views/AppView.js new file mode 100644 index 000000000..b619bcffe --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/js/views/AppView.js @@ -0,0 +1,423 @@ +/** + * @file + * The primary Backbone view for Panels IPE. For now this only controls the + * bottom tray, but in the future could have a larger scope. + * + * see Drupal.panels_ipe.AppModel + */ + +(function ($, _, Backbone, Drupal, drupalSettings) { + + 'use strict'; + + Drupal.panels_ipe.AppView = Backbone.View.extend(/** @lends Drupal.panels_ipe.AppView# */{ + + /** + * @type {function} + */ + template: _.template('
    '), + + /** + * @type {function} + */ + template_content_block_edit: _.template( + '

    ' + Drupal.t('Edit existing "<%- label %>" content') + '

    ' + + '
    ' + ), + + /** + * @type {Drupal.panels_ipe.TabsView} + */ + tabsView: null, + + /** + * @type {Drupal.panels_ipe.LayoutView} + */ + layoutView: null, + + /** + * @type {Drupal.panels_ipe.AppModel} + */ + model: null, + + /** + * @constructs + * + * @augments Backbone.View + * + * @param {object} options + * An object with the following keys: + * @param {Drupal.panels_ipe.AppModel} options.model + * The application state model. + * @param {Object} options.tabContentViews + * An object mapping TabModel ids to arbitrary Backbone views. + */ + initialize: function (options) { + this.model = options.model; + + // Create a TabsView instance. + this.tabsView = new Drupal.panels_ipe.TabsView({ + collection: this.model.get('tabCollection'), + tabViews: options.tabContentViews + }); + + // Display the cancel and save tab based on whether or not we have unsaved changes. + this.model.get('cancelTab').set('hidden', !this.model.get('unsaved')); + this.model.get('saveTab').set('hidden', !this.model.get('unsaved')); + // Do not show the edit tab if the IPE is locked. + this.model.get('editTab').set('hidden', this.model.get('locked')); + this.model.get('lockedTab').set('hidden', !this.model.get('locked')); + + // Listen to important global events throughout the app. + this.listenTo(this.model, 'changeLayout', this.changeLayout); + this.listenTo(this.model, 'addBlockPlugin', this.addBlockPlugin); + this.listenTo(this.model, 'configureBlock', this.configureBlock); + this.listenTo(this.model, 'addContentBlock', this.addContentBlock); + this.listenTo(this.model, 'editContentBlock', this.editContentBlock); + this.listenTo(this.model, 'editContentBlockDone', this.editContentBlockDone); + + // Listen to tabs that don't have associated BackboneViews. + this.listenTo(this.model.get('editTab'), 'change:active', this.clickEditTab); + this.listenTo(this.model.get('saveTab'), 'change:active', this.clickSaveTab); + this.listenTo(this.model.get('cancelTab'), 'change:active', this.clickCancelTab); + this.listenTo(this.model.get('lockedTab'), 'change:active', this.clickLockedTab); + + // Change the look/feel of the App if we have unsaved changes. + this.listenTo(this.model, 'change:unsaved', this.unsavedChange); + }, + + /** + * Appends the IPE tray to the bottom of the screen. + * + * @param {bool} render_layout + * Whether or not the layout should be rendered. Useful for just calling + * render on UI elements and not content. + * + * @return {Drupal.panels_ipe.AppView} + * Returns this, for chaining. + */ + render: function (render_layout) { + render_layout = typeof render_layout !== 'undefined' ? render_layout : true; + + // Empty our list. + this.$el.html(this.template(this.model.toJSON())); + // Add our tab collection to the App. + this.tabsView.setElement(this.$('.ipe-tab-wrapper')).render(); + + // If we have unsaved changes, add a special class. + this.$el.toggleClass('unsaved', this.model.get('unsaved')); + + // Re-render our layout. + if (this.layoutView && render_layout) { + this.layoutView.render(); + } + return this; + }, + + /** + * Actives all regions and blocks for editing. + */ + openIPE: function () { + var active = this.model.get('active'); + if (active) { + return; + } + + // Set our active state correctly. + this.model.set({active: true}); + + // Set the layout's active state correctly. + this.model.get('layout').set({active: true}); + + this.$el.addClass('active'); + + // Add a top-level body class. + $('body').addClass('panels-ipe-active'); + }, + + /** + * Deactivate all regions and blocks for editing. + */ + closeIPE: function () { + var active = this.model.get('active'); + if (!active) { + return; + } + + // Set our active state correctly. + this.model.set({active: false}); + + // Set the layout's active state correctly. + this.model.get('layout').set({active: false}); + + this.$el.removeClass('active'); + + // Remove our top-level body class. + $('body').removeClass('panels-ipe-active'); + }, + + /** + * Event callback for when a new layout has been selected. + * + * @param {Drupal.panels_ipe.LayoutModel} layout + * The new layout model. + */ + changeLayout: function (layout) { + // Early render the tabs and layout - if changing the Layout was the first + // action on the page the Layout would have never been rendered. + this.render(); + + // Grab all the blocks from the current layout. + var regions = this.model.get('layout').get('regionCollection'); + var block_collection = new Drupal.panels_ipe.BlockCollection(); + + // @todo Our backend should inform us of region suggestions. + regions.each(function (region) { + // If a layout with the same name exists, copy our block collection. + var new_region = layout.get('regionCollection').get(region.get('name')); + if (new_region) { + new_region.set('blockCollection', region.get('blockCollection')); + } + // Otherwise add these blocks to our generic pool. + else { + block_collection.add(region.get('blockCollection').toJSON()); + } + }); + + // Get the first region in the layout. + var first_region = layout.get('regionCollection').at(0); + + // Merge our block collection with the existing block collection. + block_collection.each(function (block) { + first_region.get('blockCollection').add(block); + }); + + // Change the default layout in our AppModel. + this.model.set({layout: layout}); + + // Change the LayoutView's layout. + this.layoutView.changeLayout(layout); + + // Re-render the app. + this.render(); + + // Indicate that there are unsaved changes in the app. + this.model.set('unsaved', true); + + // Switch back to the edit tab. + this.tabsView.switchTab('edit'); + }, + + /** + * Sets the IPE active state based on the "Edit" TabModel. + */ + clickEditTab: function () { + var active = this.model.get('editTab').get('active'); + if (active) { + this.openIPE(); + } + else { + this.closeIPE(); + } + }, + + /** + * Cancels another user's temporary changes and refreshes the page. + */ + clickLockedTab: function () { + var locked_tab = this.model.get('lockedTab'); + + if (confirm(Drupal.t('This page is being edited by another user, and is locked from editing by others. Would you like to break this lock?'))) { + if (locked_tab.get('active') && !locked_tab.get('loading')) { + // Remove our changes and refresh the page. + locked_tab.set({loading: true}); + $.ajax(Drupal.panels_ipe.urlRoot(drupalSettings) + '/cancel') + .done(function () { + location.reload(); + }); + } + } + else { + locked_tab.set('active', false, {silent: true}); + } + }, + + /** + * Saves our layout to the server. + */ + clickSaveTab: function () { + if (this.model.get('saveTab').get('active')) { + // Save the Layout and disable the tab. + var self = this; + self.model.get('saveTab').set({loading: true}); + this.model.get('layout').save().done(function () { + self.model.get('saveTab').set({loading: false, active: false}); + self.model.set('unsaved', false); + self.tabsView.render(); + }); + } + }, + + /** + * Cancels our temporary changes and refreshes the page. + */ + clickCancelTab: function () { + var cancel_tab = this.model.get('cancelTab'); + + if (confirm(Drupal.t('Are you sure you want to cancel your changes?'))) { + if (cancel_tab.get('active') && !cancel_tab.get('loading')) { + // Remove our changes and refresh the page. + cancel_tab.set({loading: true}); + $.ajax(Drupal.panels_ipe.urlRoot(drupalSettings) + '/cancel') + .done(function (data) { + location.reload(); + }); + } + } + else { + cancel_tab.set('active', false, {silent: true}); + } + }, + + /** + * Adds a new BlockPlugin to the screen. + * + * @param {Drupal.panels_ipe.BlockModel} block + * The new BlockModel + * @param {string} region + * The region the block should be placed in. + */ + addBlockPlugin: function (block, region) { + this.layoutView.addBlock(block, region); + + // Indicate that there are unsaved changes in the app. + this.model.set('unsaved', true); + + // Switch back to the edit tab. + this.tabsView.switchTab('edit'); + }, + + /** + * Opens the Manage Content tray when configuring an existing Block. + * + * @param {Drupal.panels_ipe.BlockModel} block + * The Block that needs to have its form opened. + */ + configureBlock: function (block) { + var info = { + url: Drupal.panels_ipe.urlRoot(drupalSettings) + '/block_plugins/' + block.get('id') + '/block/' + block.get('uuid') + '/form', + model: block + }; + + this.loadBlockForm(info); + }, + + /** + * Opens the Manage Content tray after adding a new Block Content entity. + * + * @param {string} uuid + * The UUID of the newly added Content Block. + */ + addContentBlock: function (uuid) { + // Delete the current block plugin collection so that a new one is pulled in. + delete this.tabsView.tabViews['manage_content'].collection; + + // Auto-click the new block, which we know is in the "Custom" category. + // @todo When configurable categories are in, determine this from the + // passed-in settings. + this.tabsView.tabViews['manage_content'].autoClick = '[data-plugin-id="block_content:' + uuid + '"]'; + this.tabsView.tabViews['manage_content'].activeCategory = 'Custom'; + + this.tabsView.tabViews['manage_content'].render(); + }, + + /** + * Opens the Manage Content tray when editing an existing Content Block. + * + * @param {Drupal.panels_ipe.BlockModel} block + * The Block that needs to have its form opened. + */ + editContentBlock: function (block) { + var plugin_split = block.get('id').split(':'); + + var info = { + url: Drupal.panels_ipe.urlRoot(drupalSettings) + '/block_content/edit/block/' + plugin_split[1] + '/form', + model: block + }; + + this.loadBlockForm(info, this.template_content_block_edit); + }, + + /** + * React after a content block has been edited. + * + * @param {string} block_content_uuid + * The UUID of the Block Content entity that was edited. + */ + editContentBlockDone: function (block_content_uuid) { + // Find all on-screen Blocks that render this Content Block and refresh + // them from the server. + this.layoutView.model.get('regionCollection').each(function (region) { + var id = 'block_content:' + block_content_uuid; + var blocks = region.get('blockCollection').where({id: id}); + + for (var i in blocks) { + if (blocks.hasOwnProperty(i)) { + blocks[i].set('syncing', true); + blocks[i].fetch(); + } + } + + }); + + this.tabsView.switchTab('edit'); + }, + + /** + * Hides/shows certain elements if our unsaved state changes. + */ + unsavedChange: function () { + // Show/hide the cancel tab based on our saved status. + this.model.get('cancelTab').set('hidden', !this.model.get('unsaved')); + this.model.get('saveTab').set('hidden', !this.model.get('unsaved')); + + // Re-render ourselves, pass "false" as we don't need to re-render the + // layout, just the tabs. + this.render(false); + }, + + /** + * Helper function to switch tabs to Manage Content and load an arbitrary + * form. + * + * @param {object} info + * An object compatible with Drupal.panels_ipe.CategoryView.loadForm() + * @param {function} template + * An optional callback function for the form template. + */ + loadBlockForm: function (info, template) { + // We're going to open the manage content tab, which may take time to + // render. Load the Block edit form on render. + var manage_content = this.tabsView.tabViews['manage_content']; + manage_content.on('render', function () { + + if (template) { + manage_content.loadForm(info, template); + } + else { + manage_content.loadForm(info); + } + + // We only need this event to trigger once. + manage_content.off('render', null, this); + }, this); + + // Disable the active category to avoid confusion. + manage_content.activeCategory = null; + + this.tabsView.switchTab('manage_content'); + } + + }); + +}(jQuery, _, Backbone, Drupal, drupalSettings)); diff --git a/docroot/modules/contrib/panels/panels_ipe/js/views/BlockPicker.js b/docroot/modules/contrib/panels/panels_ipe/js/views/BlockPicker.js new file mode 100644 index 000000000..755b5d157 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/js/views/BlockPicker.js @@ -0,0 +1,274 @@ +/** + * @file + * Renders a list of existing Blocks for selection. + * + * see Drupal.panels_ipe.BlockPluginCollection + * + */ + +(function ($, _, Backbone, Drupal, drupalSettings) { + + 'use strict'; + + Drupal.panels_ipe.BlockPicker = Drupal.panels_ipe.CategoryView.extend(/** @lends Drupal.panels_ipe.BlockPicker# */{ + + /** + * A selector to automatically click on render. + * + * @type {string} + */ + autoClick: null, + + /** + * @type {Drupal.panels_ipe.BlockPluginCollection} + */ + collection: null, + + /** + * @type {Drupal.panels_ipe.BlockContentTypeCollection} + */ + contentCollection: null, + + /** + * @type {function} + */ + template_plugin: _.template( + '' + ), + + /** + * @type {function} + */ + template_content_type: _.template( + '' + ), + + /** + * @type {function} + */ + template_create_button: _.template( + '' + + ' ' + + ' <%- name %>' + + '' + ), + + /** + * @type {function} + */ + template_form: _.template( + '<% if (typeof(plugin_id) !== "undefined") { %>' + + '

    ' + Drupal.t('Configure <%- label %> block') + '

    ' + + '<% } else { %>' + + '

    ' + Drupal.t('Create new <%- label %> content') + '

    ' + + '<% } %>' + + '
    ' + ), + + /** + * @type {function} + */ + template_loading: _.template( + '' + ), + + /** + * @type {object} + */ + events: { + 'click .ipe-block-plugin [data-plugin-id]': 'displayForm', + 'click .ipe-block-type [data-block-type]': 'displayForm' + }, + + /** + * @constructs + * + * @augments Backbone.View + * + * @param {Object} options + * An object containing the following keys: + * @param {Drupal.panels_ipe.BlockPluginCollection} options.collection + * An optional initial collection. + */ + initialize: function (options) { + if (options && options.collection) { + this.collection = options.collection; + } + + this.on('tabActiveChange', this.tabActiveChange, this); + + // Extend our parent's events. + _.extend(this.events, Drupal.panels_ipe.CategoryView.prototype.events); + }, + + /** + * Renders the selection menu for picking Blocks. + * + * @return {Drupal.panels_ipe.BlockPicker} + * Return this, for chaining. + */ + render: function () { + var create_active = this.activeCategory === 'Create Content'; + + // Initialize our collections if they don't already exist. + if (!this.collection) { + this.fetchCollection('default'); + return this; + } + else if (create_active && !this.contentCollection) { + this.fetchCollection('content'); + return this; + } + + // Render our categories. + this.renderCategories(); + + // Add a unique class to our top region to scope CSS. + this.$el.addClass('ipe-block-picker-list'); + + // Prepend a custom button for creating content, if the user has access. + if (drupalSettings.panels_ipe.user_permission.create_content) { + this.$('.ipe-categories').prepend(this.template_create_button({ + name: 'Create Content', + active: create_active + })); + } + + // If the create content category is active, render items in our top + // region. + if (create_active) { + // Hide the search box. + this.$('.ipe-category-picker-search').hide(); + + this.contentCollection.each(function (block_content_type) { + var template_vars = block_content_type.toJSON(); + + // Reduce the length of the description if needed. + template_vars.trimmed_description = template_vars.description; + if (template_vars.trimmed_description.length > 30) { + template_vars.trimmed_description = template_vars.description.substring(0, 30) + '...'; + } + + this.$('.ipe-category-picker-top').append(this.template_content_type(template_vars)); + }, this); + } + + // Check if we need to automatically select one item. + if (this.autoClick) { + this.$(this.autoClick).click(); + this.autoClick = null; + } + + this.trigger('render'); + + // Focus on the current category. + this.$('.ipe-category.active').focus(); + + return this; + }, + + /** + * Callback for our CategoryView, which renders an individual item. + * + * @param {Drupal.panels_ipe.BlockPluginModel} block_plugin + * The Block plugin that needs rendering. + * + * @return {string} + * The rendered block plugin. + */ + template_item: function (block_plugin) { + var template_vars = block_plugin.toJSON(); + + // Not all blocks have labels, add a default if necessary. + if (!template_vars.label) { + template_vars.label = Drupal.t('No label'); + } + + // Reduce the length of the Block label if needed. + template_vars.trimmed_label = template_vars.label; + if (template_vars.trimmed_label.length > 30) { + template_vars.trimmed_label = template_vars.label.substring(0, 30) + '...'; + } + + return this.template_plugin(template_vars); + }, + + /** + * Informs the CategoryView of our form's callback URL. + * + * @param {Object} e + * The event object. + * + * @return {Object} + * An object containing the properties "url" and "model". + */ + getFormInfo: function (e) { + // Get the current plugin_id or type. + var plugin_id = $(e.currentTarget).data('plugin-id'); + var url = Drupal.panels_ipe.urlRoot(drupalSettings); + var model; + + // Generate a base URL for the form. + if (plugin_id) { + model = this.collection.get(plugin_id); + url += '/block_plugins/' + plugin_id + '/form'; + } + else { + var block_type = $(e.currentTarget).data('block-type'); + + model = this.contentCollection.get(block_type); + url += '/block_content/' + block_type + '/form'; + } + + return {url: url, model: model}; + }, + + /** + * Fetches a collection from the server and re-renders the View. + * + * @param {string} type + * The type of collection to fetch. + */ + fetchCollection: function (type) { + var collection; + var self = this; + + if (type === 'default') { + // Indicate an AJAX request. + this.$el.html(this.template_loading()); + + // Fetch a collection of block plugins from the server. + this.collection = new Drupal.panels_ipe.BlockPluginCollection(); + collection = this.collection; + } + else { + // Indicate an AJAX request. + this.$('.ipe-category-picker-top').html(this.template_loading()); + + // Fetch a collection of block content types from the server. + this.contentCollection = new Drupal.panels_ipe.BlockContentTypeCollection(); + collection = this.contentCollection; + } + + collection.fetch().done(function () { + // We have a collection now, re-render ourselves. + self.render(); + }); + } + + }); + +}(jQuery, _, Backbone, Drupal, drupalSettings)); diff --git a/docroot/modules/contrib/panels/panels_ipe/js/views/BlockView.js b/docroot/modules/contrib/panels/panels_ipe/js/views/BlockView.js new file mode 100644 index 000000000..368cf7279 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/js/views/BlockView.js @@ -0,0 +1,157 @@ +/** + * @file + * The primary Backbone view for a Block. + * + * see Drupal.panels_ipe.BlockModel + */ + +(function ($, _, Backbone, Drupal) { + + 'use strict'; + + Drupal.panels_ipe.BlockView = Backbone.View.extend(/** @lends Drupal.panels_ipe.BlockView# */{ + + /** + * @type {function} + */ + template_actions: _.template( + '
    ' + + '
    ' + Drupal.t('Block: <%- label %>') + '
    ' + + '
      ' + + '
    • ' + + ' ' + + '
    • ' + + '
    • ' + + ' ' + + '
    • ' + + '
    • ' + + ' ' + + '
    • ' + + '
    • ' + + ' ' + + '
    • ' + + '
    • ' + + ' ' + + '
    • ' + + '<% if (plugin_id === "block_content" && edit_access) { %>' + + '
    • ' + + ' ' + + '
    • ' + + '<% } %>' + + '
    ' + + '
    ' + ), + + /** + * @type {Drupal.panels_ipe.BlockModel} + */ + model: null, + + /** + * @constructs + * + * @augments Backbone.View + * + * @param {object} options + * An object with the following keys: + * @param {Drupal.panels_ipe.BlockModel} options.model + * The block state model. + * @param {string} options.el + * An optional selector if an existing element is already on screen. + */ + initialize: function (options) { + this.model = options.model; + // An element already exists and our HTML properly isn't set. + // This only occurs on initial page load for performance reasons. + if (options.el && !this.model.get('html')) { + this.model.set({html: this.$el.prop('outerHTML')}); + } + this.listenTo(this.model, 'sync', this.finishedSync); + this.listenTo(this.model, 'change:syncing', this.render); + }, + + /** + * Renders the wrapping elements and refreshes a block model. + * + * @return {Drupal.panels_ipe.BlockView} + * Return this, for chaining. + */ + render: function () { + // Replace our current HTML. + this.$el.replaceWith(this.model.get('html')); + this.setElement("[data-block-id='" + this.model.get('uuid') + "']"); + + // We modify our content if the IPE is active. + if (this.model.get('active')) { + // Prepend the ipe-actions header. + var template_vars = this.model.toJSON(); + template_vars['edit_access'] = drupalSettings.panels_ipe.user_permission.create_content; + this.$el.prepend(this.template_actions(template_vars)); + + // Add an active class. + this.$el.addClass('active'); + + // Make ourselves draggable. + this.$el.draggable({ + scroll: true, + scrollSpeed: 20, + // Maintain our original width when dragging. + helper: function (e) { + var original = $(e.target).hasClass('ui-draggable') ? $(e.target) : $(e.target).closest('.ui-draggable'); + return original.clone().css({ + width: original.width() + }); + }, + start: function (e, ui) { + $('.ipe-droppable').addClass('active'); + // Remove the droppable regions closest to this block. + $(e.target).next('.ipe-droppable').removeClass('active'); + $(e.target).prev('.ipe-droppable').removeClass('active'); + }, + stop: function (e, ui) { + $('.ipe-droppable').removeClass('active'); + }, + opacity: .5 + }); + } + + // Add a special class if we're currently syncing HTML from the server. + if (this.model.get('syncing')) { + this.$el.addClass('syncing'); + } + + return this; + }, + + /** + * Overrides the default remove function to make a copy of our current HTML + * into the Model for future rendering. This is required as modules like + * Quickedit modify Block HTML without our knowledge. + * + * @return {Drupal.panels_ipe.BlockView} + * Return this, for chaining. + */ + remove: function () { + // Remove known augmentations to HTML so that they do not persist. + this.$('.ipe-actions-block').remove(); + this.$el.removeClass('ipe-highlight active'); + + // Update our Block model HTML based on our current visual state. + this.model.set({html: this.$el.prop('outerHTML')}); + + // Call the normal Backbow.view.remove() routines. + this._removeElement(); + this.stopListening(); + return this; + }, + + /** + * Reacts to our model being synced from the server. + */ + finishedSync: function () { + this.model.set('syncing', false); + } + + }); + +}(jQuery, _, Backbone, Drupal)); diff --git a/docroot/modules/contrib/panels/panels_ipe/js/views/CategoryView.js b/docroot/modules/contrib/panels/panels_ipe/js/views/CategoryView.js new file mode 100644 index 000000000..2c4413bad --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/js/views/CategoryView.js @@ -0,0 +1,373 @@ +/** + * @file + * Sorts a collection into categories and renders them as tabs with content. + * + * see Drupal.panels_ipe.CategoryView + * + */ + +(function ($, _, Backbone, Drupal, drupalSettings) { + + 'use strict'; + + Drupal.panels_ipe.CategoryView = Backbone.View.extend(/** @lends Drupal.panels_ipe.CategoryView# */{ + + /** + * The name of the currently selected category. + * + * @type {string} + */ + activeCategory: null, + + /** + * @type {Backbone.Collection} + */ + collection: null, + + /** + * The attribute that we should search for. Defaults to "label". + * + * @type {string} + */ + searchAttribute: 'label', + + /** + * @type {function} + */ + template: _.template( + '' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + ), + + /** + * @type {function} + */ + template_category: _.template( + '' + + ' <%- name %>' + + ' <% if (count) { %>
    <%- count %>
    <% } %>' + + '
    ' + ), + + /** + * @type {function} + * + * A function to render an item, provided by whoever uses this View. + */ + template_item: null, + + /** + * @type {function} + * + * A function to display the form wrapper. + */ + template_form: null, + + /** + * @type {object} + */ + events: { + 'click [data-category]': 'toggleCategory', + 'keyup .ipe-category-picker-search input[type="text"]': 'searchCategories' + }, + + /** + * @constructs + * + * @augments Backbone.View + * + * @param {Object} options + * An object containing the following keys: + * @param {Backbone.Collection} options.collection + * An optional initial collection. + */ + initialize: function (options) { + if (options && options.collection) { + this.collection = options.collection; + } + + this.on('tabActiveChange', this.tabActiveChange, this); + }, + + /** + * Renders the selection menu for picking categories. + * + * @return {Drupal.panels_ipe.CategoryView} + * Return this, for chaining. + */ + renderCategories: function () { + // Empty ourselves. + var search_prompt; + if (this.activeCategory) { + search_prompt = Drupal.t('Search current category'); + } + else { + search_prompt = Drupal.t('Search all categories'); + } + this.$el.html(this.template({search_prompt: search_prompt})); + + // Get a list of categories from the collection. + var categories_count = {}; + this.collection.each(function (model) { + var category = model.get('category'); + if (!categories_count[category]) { + categories_count[category] = 0; + } + ++categories_count[category]; + }); + + // Render each category. + for (var i in categories_count) { + if (categories_count.hasOwnProperty(i)) { + this.$('.ipe-categories').append(this.template_category({ + name: i, + count: categories_count[i], + active: this.activeCategory === i + })); + } + } + + // Check if a category is selected. If so, render the top-tray. + if (this.activeCategory) { + var $top = this.$('.ipe-category-picker-top'); + $top.addClass('active'); + this.$('.ipe-category-picker-bottom').addClass('top-open'); + this.collection.each(function (model) { + if (model.get('category') === this.activeCategory) { + $top.append(this.template_item(model)); + } + }, this); + + // Add a top-level body class. + $('body').addClass('panels-ipe-category-picker-top-open'); + + // Focus on the active category. + this.$('.ipe-category.active').focus(); + } + else { + // Remove our top-level body class. + $('body').removeClass('panels-ipe-category-picker-top-open'); + + // Focus on the bottom region. + this.$('.ipe-category-picker-bottom').focus(); + } + + this.setTopMaxHeight(); + + return this; + }, + + /** + * Reacts to a category being clicked. + * + * @param {Object} e + * The event object. + */ + toggleCategory: function (e) { + var category = $(e.currentTarget).data('category'); + + var animation = false; + + // No category is open. + if (!this.activeCategory) { + this.activeCategory = category; + animation = 'slideDown'; + } + // The same category is clicked twice. + else if (this.activeCategory === category) { + this.activeCategory = null; + animation = 'slideUp'; + } + // Another category is already open. + else if (this.activeCategory) { + this.activeCategory = category; + } + + // Trigger a re-render, with animation if needed. + if (animation === 'slideUp') { + // Close the tab, then re-render. + var self = this; + this.$('.ipe-category-picker-top')[animation]('fast', function () { + self.render(); + }); + } + else if (animation === 'slideDown') { + // We need to render first as hypothetically nothing is open. + this.render(); + this.$('.ipe-category-picker-top').hide(); + this.$('.ipe-category-picker-top')[animation]('fast'); + } + else { + this.render(); + } + + // Focus on the first focusable element. + this.$('.ipe-category-picker-top :focusable:first').focus(); + }, + + /** + * Informs us of our form's callback URL. + * + * @param {Object} e + * The event object. + */ + getFormInfo: function (e) {}, + + /** + * Determines form info from the current click event and displays a form. + * + * @param {Object} e + * The event object. + */ + displayForm: function (e) { + var info = this.getFormInfo(e); + + // Indicate an AJAX request. + this.loadForm(info); + }, + + /** + * Reacts to the search field changing and displays category items based on + * our search. + * + * @param {Object} e + * The event object. + */ + searchCategories: function (e) { + // Grab the formatted search from out input field. + var search = $(e.currentTarget).val().trim().toLowerCase(); + + // If the search is empty, re-render the view. + if (search.length === 0) { + this.render(); + this.$('.ipe-category-picker-search input').focus(); + return; + } + + // Filter our collection based on the input. + var results = this.collection.filter(function (model) { + var attribute = model.get(this.searchAttribute); + return attribute.toLowerCase().indexOf(search) !== -1; + }, this); + + // Empty ourselves. + var $top = this.$('.ipe-category-picker-top'); + $top.empty(); + + // Render categories that matched the search. + if (results.length > 0) { + $top.addClass('active'); + this.$('.ipe-category-picker-bottom').addClass('top-open'); + + for (var i in results) { + // If a category is empty, search within that category. + if (this.activeCategory) { + if (results[i].get('category') === this.activeCategory) { + $top.append(this.template_item(results[i])); + } + } + else { + $top.append(this.template_item(results[i])); + } + } + + $('body').addClass('panels-ipe-category-picker-top-open'); + } + else { + $top.removeClass('active'); + $('body').removeClass('panels-ipe-category-picker-top-open'); + } + + this.setTopMaxHeight(); + }, + + /** + * Displays a configuration form in our top region. + * + * @param {Object} info + * An object containing the form URL the model for our form template. + * @param {function} template + * An optional callback function for the form template. + */ + loadForm: function (info, template) { + template = template || this.template_form; + var self = this; + + // Hide the search box. + this.$('.ipe-category-picker-search').fadeOut('fast'); + + this.$('.ipe-category-picker-top').fadeOut('fast', function () { + self.$('.ipe-category-picker-top').html(template(info.model.toJSON())); + self.$('.ipe-category-picker-top').fadeIn('fast'); + + // Setup the Drupal.Ajax instance. + var ajax = Drupal.ajax({ + url: info.url, + submit: {js: true} + }); + + // Remove our throbber on load. + ajax.options.complete = function () { + self.$('.ipe-category-picker-top .ipe-icon-loading').remove(); + + self.setTopMaxHeight(); + + // Remove the inline display style and add a unique class. + self.$('.ipe-category-picker-top').css('display', '').addClass('form-displayed'); + + self.$('.ipe-category-picker-top').hide().fadeIn(); + self.$('.ipe-category-picker-bottom').addClass('top-open'); + + // Focus on the first focusable element. + self.$('.ipe-category-picker-top :focusable:first').focus(); + }; + + // Make the Drupal AJAX request. + ajax.execute(); + }); + }, + + /** + * Responds to our associated tab being opened/closed. + * + * @param {bool} state + * Whether or not our associated tab is open. + */ + tabActiveChange: function (state) { + $('body').toggleClass('panels-ipe-category-picker-top-open', state); + }, + + /** + * Calculates and sets maximum height of our top area based on known + * floating and fixed elements. + */ + setTopMaxHeight: function () { + // Calculate the combined height of (known) floating elements. + var used_height = this.$('.ipe-category-picker-bottom').outerHeight() + + this.$('.ipe-category-picker-search').outerHeight() + + $('.ipe-tabs').outerHeight(); + + // Add optional toolbar support. + var toolbar = $('#toolbar-bar'); + if (toolbar.length > 0) { + used_height += $('#toolbar-item-administration-tray:visible').outerHeight() + + toolbar.outerHeight(); + } + + // The .ipe-category-picker-top padding is 30 pixels, plus five for margin. + var max_height = $(window).height() - used_height - 35; + + // Set the form's max height. + this.$('.ipe-category-picker-top').css('max-height', max_height); + } + + }); + +}(jQuery, _, Backbone, Drupal, drupalSettings)); diff --git a/docroot/modules/contrib/panels/panels_ipe/js/views/LayoutPicker.js b/docroot/modules/contrib/panels/panels_ipe/js/views/LayoutPicker.js new file mode 100644 index 000000000..f500dcb2a --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/js/views/LayoutPicker.js @@ -0,0 +1,156 @@ +/** + * @file + * Renders a collection of Layouts for selection. + * + * see Drupal.panels_ipe.LayoutCollection + */ + +(function ($, _, Backbone, Drupal) { + + 'use strict'; + + Drupal.panels_ipe.LayoutPicker = Drupal.panels_ipe.CategoryView.extend(/** @lends Drupal.panels_ipe.LayoutPicker# */{ + + /** + * @type {function} + */ + template_form: _.template( + '

    ' + Drupal.t('Configure <%- label %> layout') + '

    ' + + '
    ' + ), + + /** + * @type {function} + */ + template_layout: _.template( + '' + + ' <%- label %>' + + ' <%- label %>' + + '' + ), + + /** + * @type {function} + */ + template_loading: _.template( + '' + ), + + /** + * @type {Drupal.panels_ipe.LayoutCollection} + */ + collection: null, + + /** + * @type {object} + */ + events: { + 'click [data-layout-id]': 'displayForm' + }, + + /** + * @constructs + * + * @augments Backbone.View + * + * @param {Object} options + * An object containing the following keys: + * @param {Drupal.panels_ipe.LayoutCollection} options.collection + * An optional initial collection. + */ + initialize: function (options) { + if (options && options.collection) { + this.collection = options.collection; + } + // Extend our parent's events. + _.extend(this.events, Drupal.panels_ipe.CategoryView.prototype.events); + }, + + /** + * Renders the selection menu for picking Layouts. + * + * @return {Drupal.panels_ipe.LayoutPicker} + * Return this, for chaining. + */ + render: function () { + var current_layout = Drupal.panels_ipe.app.get('layout').get('id'); + // If we don't have layouts yet, pull some from the server. + if (!this.collection) { + // Indicate an AJAX request. + this.$el.html(this.template_loading()); + + // Fetch a list of layouts from the server. + this.collection = new Drupal.panels_ipe.LayoutCollection(); + var self = this; + this.collection.fetch().done(function () { + // We have a collection now, re-render ourselves. + self.render(); + }); + + return this; + } + + // Render our categories. + this.renderCategories(); + + // Flag the current layout. + var current_layout_text = '' + Drupal.t('Current') + ''; + this.$('[data-layout-id="' + current_layout + '"]').append(current_layout_text); + + // Prepend the current layout as its own category. + this.$('.ipe-categories').prepend(this.template_category({ + name: Drupal.t('Current Layout'), + count: null, + active: this.activeCategory === 'Current Layout' + })); + + // If we're viewing the current layout tab, show a custom item. + if (this.activeCategory && this.activeCategory === 'Current Layout') { + // Hide the search box. + this.$('.ipe-category-picker-search').hide(); + + this.collection.each(function (layout) { + if (Drupal.panels_ipe.app.get('layout').get('id') === layout.get('id')) { + this.$('.ipe-category-picker-top').append(this.template_item(layout)); + } + }, this); + } + + return this; + }, + + /** + * Callback for our CategoryView, which renders an individual item. + * + * @param {Drupal.panels_ipe.LayoutModel} layout + * The Layout that needs rendering. + * + * @return {string} + * The rendered block plugin. + */ + template_item: function (layout) { + return this.template_layout(layout.toJSON()); + }, + + /** + * Informs the CategoryView of our form's callback URL. + * + * @param {Object} e + * The event object. + * + * @return {Object} + * An object containing the properties "url" and "model". + */ + getFormInfo: function (e) { + // Get the current layout_id. + var layout_id = $(e.currentTarget).data('layout-id'); + + var layout = this.collection.get(layout_id); + var url = Drupal.panels_ipe.urlRoot(drupalSettings) + '/layouts/' + layout_id + '/form'; + + return {url: url, model: layout}; + } + + }); + +}(jQuery, _, Backbone, Drupal)); diff --git a/docroot/modules/contrib/panels/panels_ipe/js/views/LayoutView.js b/docroot/modules/contrib/panels/panels_ipe/js/views/LayoutView.js new file mode 100644 index 000000000..43f023182 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/js/views/LayoutView.js @@ -0,0 +1,542 @@ +/** + * @file + * The primary Backbone view for a Layout. + * + * see Drupal.panels_ipe.LayoutModel + */ + +(function ($, _, Backbone, Drupal) { + + 'use strict'; + + Drupal.panels_ipe.LayoutView = Backbone.View.extend(/** @lends Drupal.panels_ipe.LayoutView# */{ + + /** + * @type {function} + */ + template_region_actions: _.template( + '
    ' + + '
    ' + Drupal.t('Region: <%- name %>') + '
    ' + + '
      ' + + '
      ' + ), + + /** + * @type {function} + */ + template_region_option: _.template( + '' + ), + + /** + * @type {function} + */ + template_region_droppable: _.template( + '
      ' + ), + + /** + * @type {Drupal.panels_ipe.LayoutModel} + */ + model: null, + + /** + * @type {Array} + * An array of child Drupal.panels_ipe.BlockView objects. + */ + blockViews: [], + + /** + * @type {object} + */ + events: { + 'mousedown [data-action-id="move"] > select': 'showBlockRegionList', + 'blur [data-action-id="move"] > select': 'hideBlockRegionList', + 'change [data-action-id="move"] > select': 'newRegionSelected', + 'click [data-action-id="up"]': 'moveBlock', + 'click [data-action-id="down"]': 'moveBlock', + 'click [data-action-id="remove"]': 'removeBlock', + 'click [data-action-id="configure"]': 'configureBlock', + 'click [data-action-id="edit-content-block"]': 'editContentBlock', + 'drop .ipe-droppable': 'dropBlock' + }, + + /** + * @type {object} + */ + droppable_settings: { + tolerance: 'pointer', + hoverClass: 'hover', + accept: '[data-block-id]' + }, + + /** + * @constructs + * + * @augments Backbone.View + * + * @param {object} options + * An object with the following keys: + * @param {Drupal.panels_ipe.LayoutModel} options.model + * The layout state model. + */ + initialize: function (options) { + this.model = options.model; + // Initialize our html, this never changes. + if (this.model.get('html')) { + this.$el.html(this.model.get('html')); + } + + this.on('tabActiveChange', this.tabActiveChange, this); + this.listenTo(this.model, 'change:active', this.changeState); + }, + + /** + * Re-renders our blocks, we have no HTML to be re-rendered. + * + * @return {Drupal.panels_ipe.LayoutView} + * Returns this, for chaining. + */ + render: function () { + // Remove all existing BlockViews. + for (var i in this.blockViews) { + if (this.blockViews.hasOwnProperty(i)) { + this.blockViews[i].remove(); + } + } + this.blockViews = []; + + // Remove any active-state items that may remain rendered. + this.$('.ipe-actions').remove(); + this.$('.ipe-droppable').remove(); + + // Re-attach all BlockViews to appropriate regions. + this.model.get('regionCollection').each(function (region) { + var region_selector = '[data-region-name="' + region.get('name') + '"]'; + + // Add an initial droppable area to our region if this is the first render. + if (this.model.get('active')) { + this.$(region_selector).prepend($(this.template_region_droppable({ + region: region.get('name'), + index: 0 + })).droppable(this.droppable_settings)); + + // Prepend the action header for this region. + this.$(region_selector).prepend(this.template_region_actions(region.toJSON())); + } + + var i = 1; + region.get('blockCollection').each(function (block) { + var block_selector = '[data-block-id="' + block.get('uuid') + '"]'; + + // Attach an empty element for our View to attach itself to. + if (this.$(block_selector).length === 0) { + var empty_elem = $('
      '); + this.$(region_selector).append(empty_elem); + } + + // Attach a View to this empty element. + var block_view = new Drupal.panels_ipe.BlockView({ + model: block, + el: block_selector + }); + this.blockViews.push(block_view); + + // Render the new BlockView. + block_view.render(); + + // Prepend/append droppable regions if the Block is active. + if (this.model.get('active')) { + block_view.$el.after($(this.template_region_droppable({ + region: region.get('name'), + index: i + })).droppable(this.droppable_settings)); + } + + ++i; + }, this); + }, this); + + // Attach any Drupal behaviors. + Drupal.attachBehaviors(this.el); + + return this; + }, + + /** + * Prepends Regions and Blocks with action items. + * + * @param {Drupal.panels_ipe.LayoutModel} model + * The target LayoutModel. + * @param {bool} value + * The desired active state. + * @param {Object} options + * Unused options. + */ + changeState: function (model, value, options) { + // Sets the active state of child blocks when our state changes. + this.model.get('regionCollection').each(function (region) { + // BlockViews handle their own rendering, so just set the active value here. + region.get('blockCollection').each(function (block) { + block.set({active: value}); + }, this); + }, this); + + // Re-render ourselves. + this.render(); + }, + + /** + * Replaces the "Move" button with a select list of regions. + * + * @param {Object} e + * The event object. + */ + showBlockRegionList: function (e) { + // Get the BlockModel id (uuid). + var id = this.getEventBlockUuid(e); + + $(e.currentTarget).empty(); + + // Add other regions to select list. + this.model.get('regionCollection').each(function (region) { + var option = $(this.template_region_option(region.toJSON())); + // If this is the current region, place it first in the list. + if (region.getBlock(id)) { + option.attr('selected', 'selected'); + $(e.currentTarget).prepend(option); + } + else { + $(e.currentTarget).append(option); + } + }, this); + }, + + /** + * Hides the region selector. + * + * @param {Object} e + * The event object. + */ + hideBlockRegionList: function (e) { + $(e.currentTarget).html(''); + }, + + /** + * React to a new region being selected. + * + * @param {Object} e + * The event object. + */ + newRegionSelected: function (e) { + var block_uuid = this.getEventBlockUuid(e); + var new_region_name = $(e.currentTarget).children(':selected').data('region-option-name'); + + if (new_region_name) { + this.moveBlockToRegion(block_uuid, new_region_name); + this.hideBlockRegionList(e); + this.render(); + this.highlightBlock(block_uuid, true); + this.saveToTempStore(); + } + }, + + /** + * Get the block Uuid related to an event. + * + * @param {Object} e + * The event object. + * + * @return {String} + * The block Uuid + */ + getEventBlockUuid: function (e) { + return $(e.currentTarget).closest('[data-block-action-id]').data('block-action-id'); + }, + + /** + * Get the block Uuid related to an event. + * + * @param {Object} e + * The event object. + * + * @return {String} + * The block Uuid + */ + getEventBlockId: function (e) { + return $(e.currentTarget).closest('[data-block-edit-id]').data('block-edit-id'); + }, + + /** + * Moves an existing Block to a new region. + * + * @param {string} block_uuid + * The universally unique identifier of the block. + * @param {string} target_region_id + * The id of the target region. + */ + moveBlockToRegion: function (block_uuid, target_region_id) { + var target_region = this.model.get('regionCollection').get(target_region_id); + var original_region = this.getRegionContainingBlock(block_uuid); + target_region.addBlock(original_region.getBlock(block_uuid)); + original_region.removeBlock(block_uuid); + }, + + /** + * Determines what region a Block resides in. + * + * @param {string} block_uuid + * The universally unique identifier of the block. + * + * @return {Drupal.panels_ipe.RegionModel|undefined} + * The region containing the block if it was found. + */ + getRegionContainingBlock: function (block_uuid) { + var region_collection = this.model.get('regionCollection'); + for (var i = 0, l = region_collection.length; i < l; i++) { + var region = region_collection.at(i); + if (region.hasBlock(block_uuid)) { + return region; + } + } + }, + + /** + * Highlights a block by adding a css class and optionally scrolls to the + * block's location. + * + * @param {string} block_uuid + * The universally unique identifier of the block. + * @param {bool} scroll + * Whether or not the page should scroll to the block. Defaults to false. + */ + highlightBlock: function (block_uuid, scroll) { + scroll = scroll || false; + + var $block = this.$('[data-block-id="' + block_uuid + '"]'); + $block.addClass('ipe-highlight'); + + if (scroll) { + $('body').animate({scrollTop: $block.offset().top}, 600); + } + }, + + /** + * Marks the global AppModel as unsaved. + */ + markUnsaved: function () { + Drupal.panels_ipe.app.set('unsaved', true); + }, + + /** + * Changes the LayoutModel for this view. + * + * @param {Drupal.panels_ipe.LayoutModel} layout + * The new LayoutModel. + */ + changeLayout: function (layout) { + // Stop listening to the current model. + this.stopListening(this.model); + // Initialize with the new model. + this.initialize({model: layout}); + }, + + /** + * Saves the current state of the layout to the tempstore. + */ + saveToTempStore: function () { + var model = this.model; + var urlRoot = Drupal.panels_ipe.urlRoot(drupalSettings); + var options = {url: urlRoot + '/layouts/' + model.get('id') + '/tempstore'}; + + Backbone.sync('update', model, options); + + this.markUnsaved(); + }, + + /** + * Removes the block on the server via an AJAX call. + * + * @param {string} block_uuid + * The UUID/ID of a BlockModel. + */ + removeServerSideBlock: function (block_uuid) { + $.ajax({ + url: Drupal.panels_ipe.urlRoot(drupalSettings) + '/remove_block', + method: 'DELETE', + data: JSON.stringify(block_uuid), + contentType: 'application/json; charset=UTF-8' + }); + + this.markUnsaved(); + }, + + /** + * Moves a block up or down in its RegionModel's BlockCollection. + * + * @param {Object} e + * The event object. + */ + moveBlock: function (e) { + // Get the BlockModel id (uuid). + var id = this.getEventBlockUuid(e); + + // Get the direction the block is moving. + var dir = $(e.currentTarget).data('action-id'); + + // Grab the model for this region. + var region_name = $(e.currentTarget).closest('[data-region-name]').data('region-name'); + var region = this.model.get('regionCollection').get(region_name); + var block = region.getBlock(id); + + // Shift the Block. + region.get('blockCollection').shift(block, dir); + + this.render(); + this.highlightBlock(id); + + this.saveToTempStore(); + }, + + /** + * Removes a Block from its region. + * + * @param {Object} e + * The event object. + */ + removeBlock: function (e) { + // Get the BlockModel id (uuid). + var id = this.getEventBlockUuid(e); + + // Grab the model for this region. + var region_name = $(e.currentTarget).closest('[data-region-name]').data('region-name'); + var region = this.model.get('regionCollection').get(region_name); + + // Remove the block. + region.removeBlock(id); + + // Re-render ourselves. + this.render(); + + this.removeServerSideBlock(id); + }, + + /** + * Configures an existing (on screen) Block. + * + * @param {Object} e + * The event object. + */ + configureBlock: function (e) { + // Get the BlockModel id (uuid). + var id = this.getEventBlockUuid(e); + + // Grab the model for this region. + var region_name = $(e.currentTarget).closest('[data-region-name]').data('region-name'); + var region = this.model.get('regionCollection').get(region_name); + + // Send an App-level event so our BlockPicker View can display a Form. + Drupal.panels_ipe.app.trigger('configureBlock', region.getBlock(id)); + }, + + /** + * Edits an existing Content Block. + * + * @param {Object} e + * The event object. + */ + editContentBlock: function (e) { + // Get the BlockModel id (uuid). + var id = this.getEventBlockUuid(e); + + // Get the blockModel content id. + var plugin_id = this.getEventBlockId(e); + + // Split plugin id. + var plugin_split = plugin_id.split(':'); + + if (plugin_split[0] === 'block_content') { + // Grab the model for this region. + var region_name = $(e.currentTarget).closest('[data-region-name]').data('region-name'); + var region = this.model.get('regionCollection').get(region_name); + + // Send a App-level event so our BlockPicker View can respond and display a Form. + Drupal.panels_ipe.app.trigger('editContentBlock', region.getBlock(id)); + } + }, + + /** + * Reacts to a block being dropped on a droppable region. + * + * @param {Object} e + * The event object. + * @param {Object} ui + * The jQuery UI object. + */ + dropBlock: function (e, ui) { + // Get the BlockModel id (uuid) and old region name. + var id = ui.draggable.data('block-id'); + var old_region_name = ui.draggable.closest('[data-region-name]').data('region-name'); + + // Get the BlockModel and remove it from its last position. + var old_region = this.model.get('regionCollection').get(old_region_name); + var block = old_region.getBlock(id); + old_region.removeBlock(block, {silent: true}); + + // Get the new region name and index from the droppable. + var new_region_name = $(e.currentTarget).data('droppable-region-name'); + var index = $(e.currentTarget).data('droppable-index'); + + // Add the BlockModel to its new region/index. + var new_region = this.model.get('regionCollection').get(new_region_name); + new_region.addBlock(block, {at: index, silent: true}); + + // Re-render after the current execution cycle, to account for DOM editing + // that jQuery.ui is going to do on this run. Modules like Contextual do + // something similar to ensure rendering order is preserved. + var self = this; + window.setTimeout(function () { + self.render(); + self.highlightBlock(id); + }); + + this.saveToTempStore(); + }, + + /** + * Adds a new BlockModel to the layout, or updates an existing Block model. + * + * @param {Drupal.panels_ipe.BlockModel} block + * The new BlockModel + * @param {string} region_name + * The region name that the block should be placed in. + */ + addBlock: function (block, region_name) { + // First, check if the Block already exists and remove it if so. + var index = null; + this.model.get('regionCollection').each(function (region) { + var old_block = region.getBlock(block.get('uuid')); + if (old_block) { + index = region.get('blockCollection').indexOf(old_block); + region.removeBlock(old_block); + } + }); + + // Get the target region. + var region = this.model.get('regionCollection').get(region_name); + if (region) { + // Add the block, at its previous index if necessary. + var options = {}; + if (index !== null && index !== -1) { + options.at = index; + } + region.addBlock(block, options); + + this.render(); + this.highlightBlock(block.get('uuid'), true); + } + } + + }); + +}(jQuery, _, Backbone, Drupal)); diff --git a/docroot/modules/contrib/panels/panels_ipe/js/views/TabsView.js b/docroot/modules/contrib/panels/panels_ipe/js/views/TabsView.js new file mode 100644 index 000000000..1372abe28 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/js/views/TabsView.js @@ -0,0 +1,234 @@ +/** + * @file + * The primary Backbone view for a tab collection. + * + * see Drupal.panels_ipe.TabCollection + */ + +(function ($, _, Backbone, Drupal, drupalSettings) { + + 'use strict'; + + Drupal.panels_ipe.TabsView = Backbone.View.extend(/** @lends Drupal.panels_ipe.TabsView# */{ + + /** + * @type {function} + */ + template_tab: _.template( + '
    • ' + + ' ' + + ' ' + + ' <%- title %>' + + ' ' + + '
    • ' + ), + + /** + * @type {function} + */ + template_content: _.template('
      '), + + /** + * @type {object} + */ + events: { + 'click .ipe-tab > a': 'switchTab' + }, + + /** + * @type {Drupal.panels_ipe.TabCollection} + */ + collection: null, + + /** + * @type {Object} + * + * An object mapping tab IDs to Backbone views. + */ + tabViews: {}, + + /** + * @constructs + * + * @augments Backbone.TabsView + * + * @param {object} options + * An object with the following keys: + * @param {object} options.tabViews + * An object mapping tab IDs to Backbone views. + */ + initialize: function (options) { + this.tabViews = options.tabViews; + + // Bind our global key down handler to the document. + $(document).bind('keydown', $.proxy(this.keydownHandler, this)); + }, + + /** + * Renders our tab collection. + * + * @return {Drupal.panels_ipe.TabsView} + * Return this, for chaining. + */ + render: function () { + // Empty our list. + this.$el.empty(); + + // Setup the initial wrapping elements. + this.$el.append('
        '); + this.$el.append('
        '); + + // Remove any previously added body classes. + $('body').removeClass('panels-ipe-tabs-open'); + + // Append each of our tabs and their tab content view. + this.collection.each(function (tab) { + // Return early if this tab is hidden. + if (tab.get('hidden')) { + return; + } + + // Append the tab. + var id = tab.get('id'); + + this.$('.ipe-tabs').append(this.template_tab(tab.toJSON())); + + // Check to see if this tab has content. + if (tab.get('active') && this.tabViews[id]) { + // Add a top-level body class. + $('body').addClass('panels-ipe-tabs-open'); + + // Render the tab content. + this.$('.ipe-tabs-content').append(this.template_content(tab.toJSON())); + this.tabViews[id].setElement('[data-tab-content-id="' + id + '"]').render(); + } + }, this); + + // Focus on the current tab. + this.$('.ipe-tab.active a').focus(); + + return this; + }, + + /** + * Switches the current tab. + * + * @param {Object} e + * The event object. + */ + switchTab: function (e) { + var id; + if (typeof e === 'string') { + id = e; + } + else { + e.preventDefault(); + id = $(e.currentTarget).parent().data('tab-id'); + } + + // Disable all existing tabs. + var animation = null; + var already_open = false; + this.collection.each(function (tab) { + // If the tab is loading, do nothing. + if (tab.get('loading')) { + return; + } + + // Don't repeat comparisons, if possible. + var clicked = tab.get('id') === id; + var active = tab.get('active'); + + // If the user is clicking the same tab twice, close it. + if (clicked && active) { + tab.set('active', false); + animation = 'close'; + } + // If this is the first click, open the tab. + else if (clicked) { + tab.set('active', true); + // Only animate the tab if there is an associate Backbone View. + if (this.tabViews[id]) { + animation = 'open'; + } + } + // The tab wasn't clicked, make sure it's closed. + else { + // Mark that the View was already open. + if (active) { + already_open = true; + } + tab.set('active', false); + } + + // Inform the tab's view of the change. + if (this.tabViews[tab.get('id')]) { + this.tabViews[tab.get('id')].trigger('tabActiveChange', tab.get('active')); + } + }, this); + + // Trigger a re-render, with animation if needed. + if (animation === 'close') { + this.closeTabContent(); + } + else if (animation === 'open' && !already_open) { + this.openTabContent(); + } + else { + this.render(); + } + }, + + /** + * Handles keypress events, checking for contextual commands in IPE. + * + * @param {Object} e + * The event object. + */ + keydownHandler: function (e) { + if (e.keyCode === 27) { + // Get the currently focused element. + var $focused = $(':focus'); + + // If a tab is currently open and we are in focus, close the tab. + if (this.$el.has($focused).length) { + var active_tab = false; + this.collection.each(function (tab) { + if (tab.get('active')) { + active_tab = tab.get('id'); + } + }); + if (active_tab) { + this.switchTab(active_tab); + } + } + } + }, + + /** + * Closes any currently open tab. + */ + closeTabContent: function () { + // Close the tab, then re-render. + var self = this; + this.$('.ipe-tabs-content')['slideUp']('fast', function () { + self.render(); + }); + + // Remove our top-level body class. + $('body').removeClass('panels-ipe-tabs-open'); + }, + + /** + * Opens any currently closed tab. + */ + openTabContent: function () { + // We need to render first as hypothetically nothing is open. + this.render(); + this.$('.ipe-tabs-content').hide(); + this.$('.ipe-tabs-content')['slideDown']('fast'); + } + + }); + +}(jQuery, _, Backbone, Drupal, drupalSettings)); diff --git a/docroot/modules/contrib/panels/panels_ipe/panels_ipe.api.php b/docroot/modules/contrib/panels/panels_ipe/panels_ipe.api.php new file mode 100644 index 000000000..bda971baf --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/panels_ipe.api.php @@ -0,0 +1,43 @@ +getConfiguration(); + $panels_display->setStorage('custom_storage_key', $configuration['storage_id']); + } +} + +/** + * Modify the list of blocks available through the IPE interface. + * + * @param array $blocks + * The blocks that are currently available. + */ +function hook_panels_ipe_blocks_alter(array &$blocks = array()) { + // Only show blocks that were provided by the 'mymodule' module. + foreach ($blocks as $key => $block) { + if ($block['provider'] !== 'mymodule') { + unset($blocks[$key]); + } + } +} diff --git a/docroot/modules/contrib/panels/panels_ipe/panels_ipe.info.yml b/docroot/modules/contrib/panels/panels_ipe/panels_ipe.info.yml new file mode 100644 index 000000000..cc4740eaf --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/panels_ipe.info.yml @@ -0,0 +1,14 @@ +name: Panels IPE +type: module +description: Panels In-place editor. +# core: 8.x +package: Panels +dependencies: + - block_content + - panels + +# Information added by Drupal.org packaging script on 2017-07-19 +version: '8.x-4.2' +core: '8.x' +project: 'panels' +datestamp: 1500497646 diff --git a/docroot/modules/contrib/panels/panels_ipe/panels_ipe.libraries.yml b/docroot/modules/contrib/panels/panels_ipe/panels_ipe.libraries.yml new file mode 100644 index 000000000..5aad05848 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/panels_ipe.libraries.yml @@ -0,0 +1,36 @@ +panels_ipe: + version: VERSION + js: + # Core. + js/panels_ipe.js: {} + # Models. + js/models/AppModel.js: {} + js/models/BlockContentTypeModel.js: {} + js/models/BlockModel.js: {} + js/models/BlockPluginModel.js: {} + js/models/RegionModel.js: {} + js/models/TabModel.js: {} + js/models/LayoutModel.js: {} + # Views. + js/views/AppView.js: {} + js/views/BlockView.js: {} + js/views/CategoryView.js: {} + js/views/BlockPicker.js: {} + js/views/LayoutPicker.js: {} + js/views/LayoutView.js: {} + js/views/TabsView.js: {} + css: + component: + css/panels_ipe.css: {} + dependencies: + - core/jquery + - core/jquery.once + - core/jquery.ui + - core/jquery.ui.draggable + - core/jquery.ui.droppable + - core/underscore + - core/backbone + - core/drupal + - core/drupal.form + - core/drupal.ajax + - core/drupalSettings diff --git a/docroot/modules/contrib/panels/panels_ipe/panels_ipe.module b/docroot/modules/contrib/panels/panels_ipe/panels_ipe.module new file mode 100644 index 000000000..450a83387 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/panels_ipe.module @@ -0,0 +1,38 @@ +' . t('About') . ''; + $output .= '

        ' . t('Panels In-place editor.') . '

        '; + // @todo: Add useful help text for Panels In-place editor. + return $output; + + default: + } +} + +/** + * Implements hook_entity_type_build(). + * + * Adds a custom Form Class to Block Content entities, so we can add custom + * actions and potentially override normal Entity CRUD operations. + */ +function panels_ipe_entity_type_build(array &$entity_types) { + /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */ + if (isset($entity_types['block_content'])) { + $entity_types['block_content']->setFormClass('panels_ipe', 'Drupal\panels_ipe\Form\PanelsIPEBlockContentForm'); + } +} diff --git a/docroot/modules/contrib/panels/panels_ipe/panels_ipe.permissions.yml b/docroot/modules/contrib/panels/panels_ipe/panels_ipe.permissions.yml new file mode 100644 index 000000000..6350b3892 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/panels_ipe.permissions.yml @@ -0,0 +1,2 @@ +access panels in-place editing: + title: 'Access panels in-place editing' \ No newline at end of file diff --git a/docroot/modules/contrib/panels/panels_ipe/panels_ipe.routing.yml b/docroot/modules/contrib/panels/panels_ipe/panels_ipe.routing.yml new file mode 100644 index 000000000..06180bd99 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/panels_ipe.routing.yml @@ -0,0 +1,133 @@ +panels_ipe.cancel: + path: '/admin/panels_ipe/variant/{panels_storage_type}/{panels_storage_id}/cancel' + defaults: + _controller: '\Drupal\panels_ipe\Controller\PanelsIPEPageController::cancel' + requirements: + _panels_storage_access: update + _permission: 'access panels in-place editing' + +panels_ipe.block_plugins: + path: '/admin/panels_ipe/variant/{panels_storage_type}/{panels_storage_id}/block_plugins' + defaults: + _controller: '\Drupal\panels_ipe\Controller\PanelsIPEPageController::getBlockPlugins' + requirements: + _panels_storage_access: read + _permission: 'access panels in-place editing' + _method: 'GET' + methods: ['GET'] + +panels_ipe.block_plugin.form: + path: '/admin/panels_ipe/variant/{panels_storage_type}/{panels_storage_id}/block_plugins/{plugin_id}/form' + defaults: + _controller: '\Drupal\panels_ipe\Controller\PanelsIPEPageController::getBlockPluginForm' + requirements: + _panels_storage_access: read + _permission: 'access panels in-place editing' + options: + _admin_route: FALSE + +panels_ipe.block_plugin_existing.form: + path: '/admin/panels_ipe/variant/{panels_storage_type}/{panels_storage_id}/block_plugins/{plugin_id}/block/{block_uuid}/form' + defaults: + _controller: '\Drupal\panels_ipe\Controller\PanelsIPEPageController::getBlockPluginForm' + requirements: + _panels_storage_access: read + _permission: 'access panels in-place editing' + +panels_ipe.remove_block: + path: '/admin/panels_ipe/variant/{panels_storage_type}/{panels_storage_id}/remove_block' + defaults: + _controller: '\Drupal\panels_ipe\Controller\PanelsIPEPageController::handleRemoveBlockRequest' + requirements: + _panels_storage_access: update + _permission: 'access panels in-place editing' + _method: 'DELETE' + methods: ['DELETE'] + +panels_ipe.block_content_types: + path: '/admin/panels_ipe/variant/{panels_storage_type}/{panels_storage_id}/block_content/types' + defaults: + _controller: '\Drupal\panels_ipe\Controller\PanelsIPEPageController::getBlockContentTypes' + requirements: + _panels_storage_access: read + _permission: 'access panels in-place editing' + _method: 'GET' + methods: ['GET'] + +panels_ipe.block_content.form: + path: '/admin/panels_ipe/variant/{panels_storage_type}/{panels_storage_id}/block_content/{type}/form' + defaults: + _controller: '\Drupal\panels_ipe\Controller\PanelsIPEPageController::getBlockContentForm' + requirements: + _panels_storage_access: read + _permission: 'access panels in-place editing+administer blocks' + +panels_ipe.block_content_existing.form: + path: '/admin/panels_ipe/variant/{panels_storage_type}/{panels_storage_id}/block_content/{type}/block/{block_content_uuid}/form' + defaults: + _controller: '\Drupal\panels_ipe\Controller\PanelsIPEPageController::getBlockContentForm' + requirements: + _panels_storage_access: read + _permission: 'access panels in-place editing+administer blocks' + +panels_ipe.layouts: + path: '/admin/panels_ipe/variant/{panels_storage_type}/{panels_storage_id}/layouts' + defaults: + _controller: '\Drupal\panels_ipe\Controller\PanelsIPEPageController::getLayouts' + requirements: + _panels_storage_access: read + _permission: 'access panels in-place editing' + _method: 'GET' + methods: ['GET'] + +panels_ipe.layout.form: + path: '/admin/panels_ipe/variant/{panels_storage_type}/{panels_storage_id}/layouts/{layout_id}/form' + defaults: + _controller: '\Drupal\panels_ipe\Controller\PanelsIPEPageController::getLayoutForm' + requirements: + _panels_storage_access: 'change layout' + _permission: 'access panels in-place editing' + +panels_ipe.layout.update: + path: '/admin/panels_ipe/variant/{panels_storage_type}/{panels_storage_id}/layouts/{layout_id}' + defaults: + _controller: '\Drupal\panels_ipe\Controller\PanelsIPEPageController::handleUpdateLayoutRequest' + requirements: + _panels_storage_access: update + _permission: 'access panels in-place editing' + _method: 'PUT' + methods: ['PUT'] + +panels_ipe.layout.update_tempstore: + path: '/admin/panels_ipe/variant/{panels_storage_type}/{panels_storage_id}/layouts/{layout_id}/tempstore' + defaults: + _controller: '\Drupal\panels_ipe\Controller\PanelsIPEPageController::handleUpdateLayoutTempStorageRequest' + requirements: + _panels_storage_access: update + _permission: 'access panels in-place editing' + _method: 'PUT' + methods: ['PUT'] + +panels_ipe.layout.save: + path: '/admin/panels_ipe/variant/{panels_storage_type}/{panels_storage_id}/layouts/{layout_id}' + defaults: + _controller: '\Drupal\panels_ipe\Controller\PanelsIPEPageController::handleCreateLayoutRequest' + requirements: + _panels_storage_access: update + _permission: 'access panels in-place editing' + _method: 'POST' + methods: ['POST'] + +# @todo Add/consolidate routes for all Block CRUD operations. + +panels_ipe.block.read: + path: '/admin/panels_ipe/variant/{panels_storage_type}/{panels_storage_id}/block/{block_uuid}' + defaults: + _controller: '\Drupal\panels_ipe\Controller\PanelsIPEPageController::getBlock' + requirements: + _panels_storage_access: read + _permission: 'access panels in-place editing' + _method: 'GET' + methods: ['GET'] + options: + _admin_route: FALSE diff --git a/docroot/modules/contrib/panels/panels_ipe/panels_ipe.services.yml b/docroot/modules/contrib/panels/panels_ipe/panels_ipe.services.yml new file mode 100644 index 000000000..a10a616e7 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/panels_ipe.services.yml @@ -0,0 +1,4 @@ +services: + plugin.manager.ipe_access: + class: Drupal\panels_ipe\Plugin\IPEAccessManager + parent: default_plugin_manager diff --git a/docroot/modules/contrib/panels/panels_ipe/src/Annotation/IPEAccess.php b/docroot/modules/contrib/panels/panels_ipe/src/Annotation/IPEAccess.php new file mode 100644 index 000000000..9d693e712 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/src/Annotation/IPEAccess.php @@ -0,0 +1,33 @@ +blockManager = $block_manager; + $this->renderer = $renderer; + $this->layoutPluginManager = $layout_plugin_manager; + $this->panelsStorage = $panels_storage_manager; + $this->tempStore = $temp_store_factory->get('panels_ipe'); + $this->contextHandler = $context_handler; + $this->updateLayoutRequestHandler = new UpdateLayoutRequestHandler($this->moduleHandler(), $this->panelsStorage, $this->tempStore); + $this->removeBlockRequestHandler = new RemoveBlockRequestHandler($this->moduleHandler(), $this->panelsStorage, $this->tempStore); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.block'), + $container->get('renderer'), + $container->get('plugin.manager.core.layout'), + $container->get('panels.storage_manager'), + $container->get('user.shared_tempstore'), + $container->get('context.handler') + ); + } + + /** + * Takes the current Page Variant and returns a possibly modified Page Variant + * based on what's in TempStore for this user. + * + * @param string $panels_storage_type + * The Panels storage plugin which holds the Panels display. + * @param string $panels_storage_id + * The id within the Panels storage plugin for this Panels display. + * + * @return \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant|NULL + */ + protected function loadPanelsDisplay($panels_storage_type, $panels_storage_id) { + /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $panels_display */ + $panels_display = $this->panelsStorage->load($panels_storage_type, $panels_storage_id); + + // If a temporary configuration for this variant exists, use it. + if ($variant_config = $this->tempStore->get($panels_display->getTempStoreId())) { + $panels_display->setConfiguration($variant_config); + } + + return $panels_display; + } + + /** + * Removes any temporary changes to the variant. + * + * @param string $panels_storage_type + * The id of the storage plugin. + * @param string $panels_storage_id + * The id within the storage plugin for the requested Panels display. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + * + * @throws \Drupal\user\TempStoreException + */ + public function cancel($panels_storage_type, $panels_storage_id) { + $panels_display = $this->loadPanelsDisplay($panels_storage_type, $panels_storage_id); + + // If a temporary configuration for this variant exists, use it. + $temp_store_key = $panels_display->getTempStoreId(); + if ($variant_config = $this->tempStore->get($temp_store_key)) { + $this->tempStore->delete($temp_store_key); + } + + // Return an empty JSON response. + return new JsonResponse(); + } + + /** + * Gets a list of available Layouts as a data array. + * + * @param string $panels_storage_type + * The id of the storage plugin. + * @param string $panels_storage_id + * The id within the storage plugin for the requested Panels display. + * + * @return array + */ + public function getLayoutsData($panels_storage_type, $panels_storage_id) { + $panels_display = $this->loadPanelsDisplay($panels_storage_type, $panels_storage_id); + + // Get the current layout. + $current_layout_id = $panels_display->getLayout()->getPluginId(); + + // Get a list of all available layouts. + $layouts = $this->layoutPluginManager->getDefinitions(); + $base_path = base_path(); + $data = []; + foreach ($layouts as $id => $layout) { + $icon = $layout->getIconPath() ?: drupal_get_path('module', 'panels') . '/layouts/no-layout-preview.png'; + $data[] = [ + 'id' => $id, + 'label' => $layout->getLabel(), + 'icon' => $base_path . $icon, + 'current' => $id == $current_layout_id, + 'category' => $layout->getCategory(), + ]; + } + + return $data; + } + + /** + * Gets a list of available Layouts as JSON. + * + * @param string $panels_storage_type + * The id of the storage plugin. + * @param string $panels_storage_id + * The id within the storage plugin for the requested Panels display. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + */ + public function getLayouts($panels_storage_type, $panels_storage_id) { + // Get the layouts data. + $data = $this->getLayoutsData($panels_storage_type, $panels_storage_id); + + // Return a structured JSON response for our Backbone App. + return new JsonResponse($data); + } + + /** + * Gets a layout configuration form for the requested layout. + * + * @param string $panels_storage_type + * The id of the storage plugin. + * @param string $panels_storage_id + * The id within the storage plugin for the requested Panels display. + * @param string $layout_id + * The machine name of the requested layout. + * + * @return \Drupal\Core\Ajax\AjaxResponse + */ + public function getLayoutForm($panels_storage_type, $panels_storage_id, $layout_id) { + $panels_display = $this->loadPanelsDisplay($panels_storage_type, $panels_storage_id); + + // Build a Block Plugin configuration form. + $form = $this->formBuilder()->getForm('Drupal\panels_ipe\Form\PanelsIPELayoutForm', $layout_id, $panels_display); + + // Return the rendered form as a proper Drupal AJAX response. + $response = new AjaxResponse(); + $command = new AppendCommand('.ipe-layout-form', $form); + $response->addCommand($command); + return $response; + } + + /** + * Updates (PUT) an existing Layout in this Variant. + * + * @param string $panels_storage_type + * The id of the storage plugin. + * @param string $panels_storage_id + * The id within the storage plugin for the requested Panels display. + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + */ + public function handleUpdateLayoutRequest($panels_storage_type, $panels_storage_id, Request $request) { + $panels_display = $this->loadPanelsDisplay($panels_storage_type, $panels_storage_id); + $this->updateLayoutRequestHandler->handleRequest($panels_display, $request); + return $this->updateLayoutRequestHandler->getJsonResponse(); + } + + /** + * Stores changes to the temporary storage. + * + * @param string $panels_storage_type + * The id of the storage plugin. + * @param string $panels_storage_id + * The id within the storage plugin for the requested Panels display. + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + */ + public function handleUpdateLayoutTempStorageRequest($panels_storage_type, $panels_storage_id, Request $request) { + $panels_display = $this->loadPanelsDisplay($panels_storage_type, $panels_storage_id); + $this->updateLayoutRequestHandler->handleRequest($panels_display, $request, TRUE); + return $this->updateLayoutRequestHandler->getJsonResponse(); + } + + /** + * Creates (POST) a new Layout for this Variant. + * + * @param string $panels_storage_type + * The id of the storage plugin. + * @param string $panels_storage_id + * The id within the storage plugin for the requested Panels display. + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + */ + public function handleCreateLayoutRequest($panels_storage_type, $panels_storage_id, Request $request) { + // For now, creating and updating a layout is the same thing. + return $this->handleUpdateLayoutRequest($panels_storage_type, $panels_storage_id, $request); + } + + /** + * Removes a block from the layout. + * + * @param string $panels_storage_type + * The id of the storage plugin. + * @param string $panels_storage_id + * The id within the storage plugin for the requested Panels display. + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + */ + public function handleRemoveBlockRequest($panels_storage_type, $panels_storage_id, Request $request) { + $panels_display = $this->loadPanelsDisplay($panels_storage_type, $panels_storage_id); + $this->removeBlockRequestHandler->handleRequest($panels_display, $request, TRUE); + return $this->updateLayoutRequestHandler->getJsonResponse(); + } + + /** + * Gets a list of Block Plugins from the server as data. + * + * @param string $panels_storage_type + * The id of the storage plugin. + * @param string $panels_storage_id + * The id within the storage plugin for the requested Panels display. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + */ + public function getBlockPluginsData($panels_storage_type, $panels_storage_id) { + $panels_display = $this->loadPanelsDisplay($panels_storage_type, $panels_storage_id); + + // Get block plugin definitions from the server. + $definitions = $this->blockManager->getDefinitionsForContexts($panels_display->getContexts()); + + // Assemble our relevant data. + $blocks = []; + foreach ($definitions as $plugin_id => $definition) { + // Don't add broken Blocks. + if ($plugin_id == 'broken') { + continue; + } + $blocks[] = [ + 'plugin_id' => $plugin_id, + 'label' => $definition['admin_label'], + 'category' => $definition['category'], + 'id' => $definition['id'], + 'provider' => $definition['provider'], + ]; + } + + // Trigger hook_panels_ipe_blocks_alter(). Allows other modules to change + // the list of blocks that are visible. + \Drupal::moduleHandler()->alter('panels_ipe_blocks', $blocks); + // We need to re-index our return value, in case a hook unset a block. + $blocks = array_values($blocks); + + return $blocks; + } + + /** + * Gets a list of Block Plugins from the server as JSON. + * + * @param string $panels_storage_type + * The id of the storage plugin. + * @param string $panels_storage_id + * The id within the storage plugin for the requested Panels display. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + */ + public function getBlockPlugins($panels_storage_type, $panels_storage_id) { + // Get the block plugins data. + $blocks = $this->getBlockPluginsData($panels_storage_type, $panels_storage_id); + + // Return a structured JSON response for our Backbone App. + return new JsonResponse($blocks); + } + + /** + * Drupal AJAX compatible route for rendering a given Block Plugin's form. + * + * @param string $panels_storage_type + * The id of the storage plugin. + * @param string $panels_storage_id + * The id within the storage plugin for the requested Panels display. + * @param string $plugin_id + * The requested Block Plugin ID. + * @param string $block_uuid + * The Block UUID, if this is an existing Block. + * + * @return Response + */ + public function getBlockPluginForm($panels_storage_type, $panels_storage_id, $plugin_id, $block_uuid = NULL) { + $panels_display = $this->loadPanelsDisplay($panels_storage_type, $panels_storage_id); + + // Get the configuration in the block plugin definition. + $definitions = $this->blockManager->getDefinitionsForContexts($panels_display->getContexts()); + + // Check if the block plugin is defined. + if (!isset($definitions[$plugin_id])) { + throw new NotFoundHttpException(); + } + + // Build a Block Plugin configuration form. + $form = $this->formBuilder()->getForm('Drupal\panels_ipe\Form\PanelsIPEBlockPluginForm', $plugin_id, $panels_display, $block_uuid); + + // Return the rendered form as a proper Drupal AJAX response. + $response = new AjaxResponse(); + $command = new AppendCommand('.ipe-block-form', $form); + $response->addCommand($command); + return $response; + } + + /** + * Gets a list of Block Content Types from the server as data. + * + * @param string $panels_storage_type + * The id of the storage plugin. + * @param string $panels_storage_id + * The id within the storage plugin for the requested Panels display. + * + * @return array + */ + public function getBlockContentTypesData($panels_storage_type, $panels_storage_id) { + // Assemble our relevant data. + $types = $this->entityTypeManager() + ->getStorage('block_content_type') + ->loadMultiple(); + $data = []; + + /** @var \Drupal\block_content\BlockContentTypeInterface $definition */ + foreach ($types as $id => $definition) { + $data[] = [ + 'id' => $definition->id(), + 'revision' => $definition->shouldCreateNewRevision(), + 'label' => $definition->label(), + 'description' => $definition->getDescription(), + ]; + } + + return $data; + } + + /** + * Gets a list of Block Content Types from the server as JSON. + * + * @param string $panels_storage_type + * The id of the storage plugin. + * @param string $panels_storage_id + * The id within the storage plugin for the requested Panels display. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + */ + public function getBlockContentTypes($panels_storage_type, $panels_storage_id) { + // Get the block content types data. + $data = $this->getBlockContentTypesData($panels_storage_type, $panels_storage_id); + + // Return a structured JSON response for our Backbone App. + return new JsonResponse($data); + } + + /** + * Drupal AJAX compatible route for rendering a Block Content Type's form. + * + * @param string $panels_storage_type + * The id of the storage plugin. + * @param string $panels_storage_id + * The id within the storage plugin for the requested Panels display. + * @param string $type + * The requested Block Type. + * @param string $block_content_uuid + * The Block Content Entity UUID, if this is an existing Block. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + */ + public function getBlockContentForm($panels_storage_type, $panels_storage_id, $type, $block_content_uuid = NULL) { + $storage = $this->entityTypeManager()->getStorage('block_content'); + + // Create or load a new block of the given type. + if ($block_content_uuid) { + $block_list = $storage->loadByProperties(['uuid' => $block_content_uuid]); + $block = array_shift($block_list); + + $operation = 'update'; + } + else { + $block = $storage->create([ + 'type' => $type, + ]); + + $operation = 'create'; + } + + // Check Block Content entity access for the current operation. + if (!$block->access($operation)) { + throw new AccessDeniedHttpException(); + } + + // Grab our Block Content Entity form handler, and pass the Panels display + // variant to it in $form_state. + $form_state = [ + 'panels_display' => $this->loadPanelsDisplay($panels_storage_type, $panels_storage_id), + ]; + $form = $this->entityFormBuilder()->getForm($block, 'panels_ipe', $form_state); + + // Return the rendered form as a proper Drupal AJAX response. + $response = new AjaxResponse(); + $command = new AppendCommand('.ipe-block-form', $form); + $response->addCommand($command); + return $response; + } + + /** + * Gets a single Block from the current Panels Display as data. Uses TempStore. + * + * @param string $panels_storage_type + * The id of the storage plugin. + * @param string $panels_storage_id + * The id within the storage plugin for the requested Panels display. + * @param string $block_uuid + * The Block UUID. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + */ + public function getBlockModelData($panels_storage_type, $panels_storage_id, $block_uuid) { + $panels_display = $this->loadPanelsDisplay($panels_storage_type, $panels_storage_id); + + /** @var \Drupal\Core\Block\BlockBase $block_instance */ + $block_instance = $panels_display->getBlock($block_uuid); + $block_config = $block_instance->getConfiguration(); + + // Assemble data required for our App. + $build = $this->buildBlockInstance($block_instance, $panels_display); + + // Bubble Block attributes to fix bugs with the Quickedit and Contextual + // modules. + $this->bubbleBlockAttributes($build); + + // Add our data attribute for the Backbone app. + $build['#attributes']['data-block-id'] = $block_uuid; + + $plugin_definition = $block_instance->getPluginDefinition(); + + $block_model = [ + 'uuid' => $block_uuid, + 'label' => $block_instance->label(), + 'id' => $block_instance->getPluginId(), + 'region' => $block_config['region'], + 'provider' => $block_config['provider'], + 'plugin_id' => $plugin_definition['id'], + 'html' => $this->renderer->render($build), + ]; + + return $block_model; + } + + /** + * Gets a single Block from the current Panels Display as JSON. + * + * @param string $panels_storage_type + * The id of the storage plugin. + * @param string $panels_storage_id + * The id within the storage plugin for the requested Panels display. + * @param string $block_uuid + * The Block UUID. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + */ + public function getBlock($panels_storage_type, $panels_storage_id, $block_uuid) { + // Get the block model data. + $data = $this->getBlockModelData($panels_storage_type, $panels_storage_id, $block_uuid); + + // Return a structured JSON response for our Backbone App. + return new JsonResponse($data); + } + +} diff --git a/docroot/modules/contrib/panels/panels_ipe/src/Exception/EmptyRequestContentException.php b/docroot/modules/contrib/panels/panels_ipe/src/Exception/EmptyRequestContentException.php new file mode 100644 index 000000000..7b18cd104 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/src/Exception/EmptyRequestContentException.php @@ -0,0 +1,8 @@ +t('Create and Place'); + if (!$this->entity->isNew()) { + $button_value = $this->t('Update'); + } + + // Override normal BlockContentForm actions as we need to be AJAX + // compatible, and also need to communicate with our App. + $actions['submit'] = [ + '#type' => 'button', + '#value' => $button_value, + '#name' => 'panels_ipe_submit', + '#ajax' => [ + 'callback' => '::submitForm', + 'wrapper' => 'panels-ipe-block-type-form-wrapper', + 'method' => 'replace', + 'progress' => [ + 'type' => 'throbber', + 'message' => '', + ], + ], + ]; + + return $actions; + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + $form['is_new'] = [ + '#type' => 'value', + '#value' => $this->entity->isNew(), + ]; + + // Wrap our form so that our submit callback can re-render the form. + $form['#prefix'] = '
        '; + $form['#suffix'] = '
        '; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + + // Return early if there are any errors or if a button we're not aware of + // submitted the form. + if ($form_state->hasAnyErrors() || $triggering_element['#name'] !== 'panels_ipe_submit') { + return $form; + } + + // Submit the parent form and save. This mimics the normal behavior of the + // submit element in our parent form(s). + parent::submitForm($form, $form_state); + $this->save($form, $form_state); + + // Inform the App that we've created a new Block Content entity. + if ($form_state->getValue('is_new')) { + $form['#attached']['drupalSettings']['panels_ipe']['new_block_content'] = $this->entity->uuid(); + } + else { + $form['#attached']['drupalSettings']['panels_ipe']['edit_block_content'] = $this->entity->uuid(); + } + + return $form; + } + +} diff --git a/docroot/modules/contrib/panels/panels_ipe/src/Form/PanelsIPEBlockPluginForm.php b/docroot/modules/contrib/panels/panels_ipe/src/Form/PanelsIPEBlockPluginForm.php new file mode 100644 index 000000000..8d8a8b9bd --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/src/Form/PanelsIPEBlockPluginForm.php @@ -0,0 +1,397 @@ +blockManager = $block_manager; + $this->contextHandler = $context_handler; + $this->renderer = $renderer; + $this->tempStore = $temp_store_factory->get('panels_ipe'); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.block'), + $container->get('context.handler'), + $container->get('renderer'), + $container->get('user.shared_tempstore') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'panels_ipe_block_plugin_form'; + } + + /** + * Builds a form that constructs a unsaved instance of a Block for the IPE. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param string $plugin_id + * The requested Block Plugin ID. + * @param \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $panels_display + * The current PageVariant ID. + * @param string $uuid + * An optional Block UUID, if this is an existing Block. + * + * @return array + * The form structure. + */ + public function buildForm(array $form, FormStateInterface $form_state, $plugin_id = NULL, PanelsDisplayVariant $panels_display = NULL, $uuid = NULL) { + // We require these default arguments. + if (!$plugin_id || !$panels_display) { + return FALSE; + } + + // Save the panels display for later. + $this->panelsDisplay = $panels_display; + + // Grab the current layout's regions. + $regions = $panels_display->getRegionNames(); + + // If $uuid is present, a block should exist. + if ($uuid) { + /** @var \Drupal\Core\Block\BlockBase $block_instance */ + $block_instance = $panels_display->getBlock($uuid); + } + else { + // Create an instance of this Block plugin. + /** @var \Drupal\Core\Block\BlockBase $block_instance */ + $block_instance = $this->blockManager->createInstance($plugin_id); + } + + // Determine the current region. + $block_config = $block_instance->getConfiguration(); + if (isset($block_config['region']) && isset($regions[$block_config['region']])) { + $region = $block_config['region']; + } + else { + $region = reset($regions); + } + + // Some Block Plugins rely on the block_theme value to load theme settings. + // @see \Drupal\system\Plugin\Block\SystemBrandingBlock::blockForm(). + $form_state->set('block_theme', $this->config('system.theme')->get('default')); + + // Wrap the form so that our AJAX submit can replace its contents. + $form['#prefix'] = '
        '; + $form['#suffix'] = '
        '; + + // Add our various card wrappers. + $form['flipper'] = [ + '#type' => 'container', + '#attributes' => [ + 'class' => 'flipper', + ], + ]; + + $form['flipper']['front'] = [ + '#type' => 'container', + '#attributes' => [ + 'class' => 'front', + ], + ]; + + $form['flipper']['back'] = [ + '#type' => 'container', + '#attributes' => [ + 'class' => 'back', + ], + ]; + + $form['#attributes']['class'][] = 'flip-container'; + + // Get the base configuration form for this block. + $form['flipper']['front']['settings'] = $block_instance->buildConfigurationForm([], $form_state); + $form['flipper']['front']['settings']['context_mapping'] = $this->addContextAssignmentElement($block_instance, $this->panelsDisplay->getContexts()); + $form['flipper']['front']['settings']['#tree'] = TRUE; + + // Add the block ID, variant ID to the form as values. + $form['plugin_id'] = ['#type' => 'value', '#value' => $plugin_id]; + $form['variant_id'] = ['#type' => 'value', '#value' => $panels_display->id()]; + $form['uuid'] = ['#type' => 'value', '#value' => $uuid]; + + // Add a select list for region assignment. + $form['flipper']['front']['settings']['region'] = [ + '#title' => $this->t('Region'), + '#type' => 'select', + '#options' => $regions, + '#required' => TRUE, + '#default_value' => $region, + ]; + + // Add an add button, which is only used by our App. + $form['submit'] = [ + '#type' => 'button', + '#value' => $uuid ? $this->t('Update') : $this->t('Add'), + '#ajax' => [ + 'callback' => '::submitForm', + 'wrapper' => 'panels-ipe-block-plugin-form-wrapper', + 'method' => 'replace', + 'progress' => [ + 'type' => 'throbber', + 'message' => '', + ], + ], + ]; + + // Add a preview button. + $form['preview'] = [ + '#type' => 'button', + '#value' => $this->t('Toggle Preview'), + '#ajax' => [ + 'callback' => '::submitPreview', + 'wrapper' => 'panels-ipe-block-plugin-form-wrapper', + 'method' => 'replace', + 'progress' => [ + 'type' => 'throbber', + 'message' => '', + ], + ], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $block_instance = $this->getBlockInstance($form_state); + + // Validate the block configuration form. + $block_form_state = (new FormState())->setValues($form_state->getValue('settings')); + $block_instance->validateConfigurationForm($form, $block_form_state); + // Update the original form values. + $form_state->setValue('settings', $block_form_state->getValues()); + } + + /** + * Executes the block plugin's submit handlers. + * + * @param \Drupal\Core\Block\BlockPluginInterface $block_instance + * The block instance. + * @param array $form + * The full form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The full form state. + */ + protected function submitBlock(BlockPluginInterface $block_instance, array $form, FormStateInterface $form_state) { + $block_form_state = (new FormState())->setValues($form_state->getValue('settings')); + $block_instance->submitConfigurationForm($form['flipper']['front']['settings'], $block_form_state); + if ($block_instance instanceof ContextAwarePluginInterface) { + $block_instance->setContextMapping($block_form_state->getValue('context_mapping', [])); + } + // Update the original form values. + $form_state->setValue('settings', $block_form_state->getValues()); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Return early if there are any errors. + if ($form_state->hasAnyErrors()) { + return $form; + } + + // If a temporary configuration for this variant exists, use it. + $temp_store_key = $this->panelsDisplay->getTempStoreId(); + if ($variant_config = $this->tempStore->get($temp_store_key)) { + $this->panelsDisplay->setConfiguration($variant_config); + } + + $block_instance = $this->getBlockInstance($form_state); + + // Submit the block configuration form. + $this->submitBlock($block_instance, $form, $form_state); + + // Set the block region appropriately. + $block_config = $block_instance->getConfiguration(); + $block_config['region'] = $form_state->getValue(array('settings', 'region')); + + // Determine if we need to update or add this block. + if ($uuid = $form_state->getValue('uuid')) { + $this->panelsDisplay->updateBlock($uuid, $block_config); + } + else { + $uuid = $this->panelsDisplay->addBlock($block_config); + } + + // Set the tempstore value. + $this->tempStore->set($this->panelsDisplay->getTempStoreId(), $this->panelsDisplay->getConfiguration()); + + // Assemble data required for our App. + $build = $this->buildBlockInstance($block_instance, $this->panelsDisplay); + + // Bubble Block attributes to fix bugs with the Quickedit and Contextual + // modules. + $this->bubbleBlockAttributes($build); + + // Add our data attribute for the Backbone app. + $build['#attributes']['data-block-id'] = $uuid; + + $plugin_definition = $block_instance->getPluginDefinition(); + + $block_model = [ + 'uuid' => $uuid, + 'label' => $block_instance->label(), + 'id' => $block_instance->getPluginId(), + 'region' => $block_config['region'], + 'provider' => $block_config['provider'], + 'plugin_id' => $plugin_definition['id'], + 'html' => $this->renderer->render($build), + ]; + + $form['build'] = $build; + + // Add Block metadata and HTML as a drupalSetting. + $form['#attached']['drupalSettings']['panels_ipe']['updated_block'] = $block_model; + + return $form; + } + + /** + * Previews our current Block configuration. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array $form + * The form structure. + */ + public function submitPreview(array &$form, FormStateInterface $form_state) { + // Return early if there are any errors. + if ($form_state->hasAnyErrors()) { + return $form; + } + + // Get the Block instance. + $block_instance = $this->getBlockInstance($form_state); + + // Submit the block configuration form. + $this->submitBlock($block_instance, $form, $form_state); + + // Gather a render array for the block. + $build = $this->buildBlockInstance($block_instance, $this->panelsDisplay); + + // Replace any nested form tags from the render array. + $build['content']['#post_render'][] = function ($html, array $elements) { + $search = ['']; + $replace = ['']; + return str_replace($search, $replace, $html); + }; + + // Add the preview to the backside of the card and inform JS that we need to + // be flipped. + $form['flipper']['back']['preview'] = $build; + + // Add a cleafix element to the end of the preview. This prevents overlaps + // with nested float elements. + $build['clearfix'] = [ + '#markup' => '
        ', + ]; + + $form['#attached']['drupalSettings']['panels_ipe']['toggle_preview'] = TRUE; + + return $form; + } + + /** + * Loads or creates a Block Plugin instance suitable for rendering or testing. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return \Drupal\Core\Block\BlockPluginInterface + * The Block Plugin instance. + */ + protected function getBlockInstance(FormStateInterface $form_state) { + // If a UUID is provided, the Block should already exist. + if ($uuid = $form_state->getValue('uuid')) { + // If a temporary configuration for this variant exists, use it. + $temp_store_key = $this->panelsDisplay->getTempStoreId(); + if ($variant_config = $this->tempStore->get($temp_store_key)) { + $this->panelsDisplay->setConfiguration($variant_config); + } + + // Load the existing Block instance. + $block_instance = $this->panelsDisplay->getBlock($uuid); + } + else { + // Create an instance of this Block plugin. + /** @var \Drupal\Core\Block\BlockBase $block_instance */ + $block_instance = $this->blockManager->createInstance($form_state->getValue('plugin_id')); + } + + return $block_instance; + } + +} diff --git a/docroot/modules/contrib/panels/panels_ipe/src/Form/PanelsIPELayoutForm.php b/docroot/modules/contrib/panels/panels_ipe/src/Form/PanelsIPELayoutForm.php new file mode 100644 index 000000000..274816f13 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/src/Form/PanelsIPELayoutForm.php @@ -0,0 +1,244 @@ +layoutManager = $layout_manager; + $this->renderer = $renderer; + $this->tempStore = $temp_store_factory->get('panels_ipe'); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.core.layout'), + $container->get('renderer'), + $container->get('user.shared_tempstore') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'panels_ipe_layout_form'; + } + + /** + * Builds a form that configure an existing or new layout for the IPE. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param string $layout_id + * The requested Layout ID. + * @param \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $panels_display + * The current PageVariant ID. + * + * @return array + * The form structure. + */ + public function buildForm(array $form, FormStateInterface $form_state, $layout_id = NULL, PanelsDisplayVariant $panels_display = NULL) { + // We require these default arguments. + if (!$layout_id || !$panels_display) { + return FALSE; + } + + // Save the panels display for later. + $this->panelsDisplay = $panels_display; + + // Check if this is the current layout, and if not create an instance. + $layout = $this->panelsDisplay->getLayout(); + $current = $layout->getPluginId() == $layout_id; + if (!$current) { + // Create a new layout instance. + $layout = $this->layoutManager->createInstance($layout_id, []); + } + + // Save the layout for future use. + $this->layout = $layout; + + if ($layout instanceof PluginFormInterface) { + $form['settings'] = $layout->buildConfigurationForm([], $form_state); + } + $form['settings']['#tree'] = TRUE; + + // If the form is empty, inform the user or auto-submit if they are changing + // layouts. + if (empty(Element::getVisibleChildren($form['settings']))) { + if ($current) { + $form['settings'][] = [ + '#markup' => $this->t('
        This layout does not provide any configuration.
        '), + ]; + } + else { + $this->submitForm($form, $form_state); + } + } + + // Add an add button, which is only used by our App. + $form['submit'] = [ + '#type' => 'button', + '#value' => $current ? $this->t('Update') : $this->t('Change Layout'), + '#ajax' => [ + 'callback' => '::submitForm', + 'wrapper' => 'panels-ipe-layout-form-wrapper', + 'method' => 'replace', + 'progress' => [ + 'type' => 'throbber', + 'message' => '', + ], + ], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + if ($this->layout instanceof PluginFormInterface) { + $layout_form_state = (new FormState())->setValues($form_state->getValue('settings', [])); + $this->layout->validateConfigurationForm($form, $layout_form_state); + // Update the original form values. + $form_state->setValue('settings', $layout_form_state->getValues()); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Return early if there are any errors. + if ($form_state->hasAnyErrors()) { + return $form; + } + + $panels_display = $this->panelsDisplay; + + // Submit the layout form. + if ($this->layout instanceof PluginFormInterface) { + $layout_form_state = (new FormState())->setValues($form_state->getValue('settings', [])); + $this->layout->submitConfigurationForm($form, $layout_form_state); + } + $layout_config = $this->layout->getConfiguration(); + + // Shift our blocks to the first available region. The IPE can control + // re-assigning blocks in a smarter way. + $first_region = $this->layout->getPluginDefinition()->getDefaultRegion(); + + // For each block, set the region to match the new layout. + foreach ($panels_display->getRegionAssignments() as $region => $region_assignment) { + /** @var \Drupal\Core\Block\BlockPluginInterface $block */ + foreach ($region_assignment as $block_id => $block) { + $block_config = $block->getConfiguration(); + // If the new layout does not have a region with the same name, use the + // first available region. + if (!isset($region_definitions[$block_config['region']])) { + $block_config['region'] = $first_region; + $panels_display->updateBlock($block_id, $block_config); + } + } + } + + // Have our panels display use the new layout. + $this->panelsDisplay->setLayout($this->layout, $layout_config); + + // Update tempstore. + $this->tempStore->set($panels_display->getTempStoreId(), $panels_display->getConfiguration()); + + $region_data = []; + $region_content = []; + + // Compile region content and metadata. + $regions = $panels_display->getRegionAssignments(); + foreach ($regions as $id => $label) { + // Wrap the region with a class/data attribute that our app can use. + $region_name = Html::getClass("block-region-$id"); + $region_content[$id] = [ + '#prefix' => '
        ', + '#suffix' => '
        ', + ]; + + // Format region metadata. + $region_data[] = [ + 'name' => $id, + 'label' => $label, + ]; + } + + $build = $panels_display->getLayout()->build($region_content); + $form['build'] = $build; + + $data = [ + 'id' => $this->layout->getPluginId(), + 'label' => $this->layout->getPluginDefinition()->getLabel(), + 'current' => TRUE, + 'html' => $this->renderer->render($build), + 'regions' => $region_data, + ]; + + // Add Block metadata and HTML as a drupalSetting. + $form['#attached']['drupalSettings']['panels_ipe']['updated_layout'] = $data; + + return $form; + } + +} diff --git a/docroot/modules/contrib/panels/panels_ipe/src/Helpers/RemoveBlockRequestHandler.php b/docroot/modules/contrib/panels/panels_ipe/src/Helpers/RemoveBlockRequestHandler.php new file mode 100644 index 000000000..708f67386 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/src/Helpers/RemoveBlockRequestHandler.php @@ -0,0 +1,23 @@ +removeBlock($decoded_request); + + if ($save_to_temp_store) { + $this->savePanelsDisplayToTempStore($panels_display); + } + else { + $this->savePanelsDisplay($panels_display); + } + } + +} diff --git a/docroot/modules/contrib/panels/panels_ipe/src/Helpers/RequestHandlerBase.php b/docroot/modules/contrib/panels/panels_ipe/src/Helpers/RequestHandlerBase.php new file mode 100644 index 000000000..90031a04c --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/src/Helpers/RequestHandlerBase.php @@ -0,0 +1,150 @@ +moduleHandler = $module_handler; + $this->panelsStore = $panels_store; + $this->tempStore = $temp_store; + } + + /** + * @inheritdoc + */ + public function handleRequest(PanelsDisplayVariant $panels_display, Request $request, $save_to_temp_store = FALSE) { + $this->setResponse([]); + + try { + $this->handle($panels_display, self::decodeRequest($request), $save_to_temp_store); + } + catch (EmptyRequestContentException $e) { + $this->setResponse(['success' => FALSE], 400); + } + } + + /** + * Handles the decoded request by making some change to the Panels Display. + * + * @param \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $panels_display + * @param mixed $decoded_request + * @param bool $save_to_temp_store + * + * @throws \Drupal\panels_ipe\Exception\EmptyRequestContentException + */ + protected abstract function handle(PanelsDisplayVariant $panels_display, $decoded_request, $save_to_temp_store = FALSE); + + /** + * Attempts to decode the incoming request's content as JSON. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * + * @return mixed + * + * @throws \Drupal\panels_ipe\Exception\EmptyRequestContentException + */ + protected static function decodeRequest(Request $request) { + if (empty($request->getContent())) { + throw new EmptyRequestContentException(); + } + + return Json::decode($request->getContent()); + } + + /** + * Helper function for invoking hooks for all enabled modules. + * + * @param $hook + * @param array $arguments + */ + protected function invokeHook($hook, array $arguments) { + $this->moduleHandler->invokeAll($hook, $arguments); + } + + /** + * Deletes TempStore and saves the current Panels display. + * + * @param \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $panels_display + * The Panels display to be saved. + * + * @throws \Drupal\user\TempStoreException + * If there are any issues manipulating the entry in the temp store. + */ + protected function savePanelsDisplay(PanelsDisplayVariant $panels_display) { + $this->deletePanelsDisplayTempStore($panels_display); + $this->panelsStore->save($panels_display); + } + + /** + * Saves the given Panels Display to TempStore. + * + * @param \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $panels_display + * + * @throws \Drupal\user\TempStoreException + */ + protected function savePanelsDisplayToTempStore(PanelsDisplayVariant $panels_display) { + $this->tempStore->set($panels_display->getTempStoreId(), $panels_display->getConfiguration()); + } + + /** + * Deletes the given Panels Display from TempStore. + * + * @param \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $panels_display + * + * @throws \Drupal\user\TempStoreException + */ + protected function deletePanelsDisplayTempStore(PanelsDisplayVariant $panels_display) { + $this->tempStore->delete($panels_display->getTempStoreId()); + } + + /** + * Returns the current response data as a JSON Response. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + */ + public function getJsonResponse() { + return new JsonResponse($this->response, $this->responseStatusCode); + } + + /** + * Updates our response and response status code properties. + * + * @param array $response + * @param int $response_status_code + */ + protected function setResponse(array $response, $response_status_code = 200) { + $this->response = $response; + $this->responseStatusCode = $response_status_code; + } + +} diff --git a/docroot/modules/contrib/panels/panels_ipe/src/Helpers/RequestHandlerInterface.php b/docroot/modules/contrib/panels/panels_ipe/src/Helpers/RequestHandlerInterface.php new file mode 100644 index 000000000..da0474d00 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/src/Helpers/RequestHandlerInterface.php @@ -0,0 +1,24 @@ +updateLayout($panels_display, $decodedRequest, $save_to_temp_store); + } + + /** + * Changes the layout for the given Panels Display. + * + * @param \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $panels_display + * @param $layout_model + * @param bool $save_to_temp_store + */ + private function updateLayout(PanelsDisplayVariant $panels_display, $layout_model, $save_to_temp_store = FALSE) { + $panels_display = self::updatePanelsDisplay($panels_display, $layout_model); + + $this->invokeHook('panels_ipe_panels_display_presave', [ + $panels_display, + $layout_model, + ]); + + if ($save_to_temp_store) { + $this->savePanelsDisplayToTempStore($panels_display); + } + else { + $this->savePanelsDisplay($panels_display); + } + } + + /** + * Updates the current Panels display based on the changes done in our app. + * + * @param \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $panels_display + * The current Panels display. + * @param array $layout_model + * The decoded LayoutModel from our App. + * + * @return \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant + */ + private static function updatePanelsDisplay(PanelsDisplayVariant $panels_display, array $layout_model) { + // Set our weight and region based on the metadata in our Backbone app. + foreach ($layout_model['regionCollection'] as $region) { + $weight = 0; + foreach ($region['blockCollection'] as $block) { + /** @var \Drupal\Core\Block\BlockBase $block_instance */ + $block_instance = $panels_display->getBlock($block['uuid']); + + $block_instance->setConfigurationValue('region', $region['name']); + $block_instance->setConfigurationValue('weight', ++$weight); + + $panels_display->updateBlock($block['uuid'], $block_instance->getConfiguration()); + } + } + + return $panels_display; + } + +} diff --git a/docroot/modules/contrib/panels/panels_ipe/src/PanelsIPEBlockRendererTrait.php b/docroot/modules/contrib/panels/panels_ipe/src/PanelsIPEBlockRendererTrait.php new file mode 100644 index 000000000..6d5d36cad --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/src/PanelsIPEBlockRendererTrait.php @@ -0,0 +1,79 @@ +getConfiguration(); + + // Add context to the block. + if ($this->contextHandler && $block_instance instanceof ContextAwarePluginInterface) { + $this->contextHandler->applyContextMapping($block_instance, $panels_display->getContexts()); + } + + // Build the block content. + $content = $block_instance->build(); + + // Compile the render array. + $build = [ + '#theme' => 'block', + '#attributes' => [], + '#contextual_links' => [], + '#configuration' => $configuration, + '#plugin_id' => $block_instance->getPluginId(), + '#base_plugin_id' => $block_instance->getBaseId(), + '#derivative_plugin_id' => $block_instance->getDerivativeId(), + 'content' => $content, + ]; + + return $build; + } + + /** + * Bubble block attributes up if possible. This allows modules like + * Quickedit to function. + * + * @see \Drupal\block\BlockViewBuilder::preRender for reference. + * + * @param array $build + * The Block render array. + */ + protected function bubbleBlockAttributes(&$build) { + // Bubble block attributes up if possible. This allows modules like + // Quickedit to function. + // See \Drupal\block\BlockViewBuilder::preRender() for reference. + if ($build['content'] !== NULL && !Element::isEmpty($build['content'])) { + foreach (['#attributes', '#contextual_links'] as $property) { + if (isset($build['content'][$property])) { + $build[$property] += $build['content'][$property]; + unset($build['content'][$property]); + } + } + } + } + +} diff --git a/docroot/modules/contrib/panels/panels_ipe/src/Plugin/DisplayBuilder/InPlaceEditorDisplayBuilder.php b/docroot/modules/contrib/panels/panels_ipe/src/Plugin/DisplayBuilder/InPlaceEditorDisplayBuilder.php new file mode 100644 index 000000000..920e69224 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/src/Plugin/DisplayBuilder/InPlaceEditorDisplayBuilder.php @@ -0,0 +1,223 @@ +tempStore = $temp_store_factory->get('panels_ipe'); + $this->panelsStorage = $panels_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('context.handler'), + $container->get('current_user'), + $container->get('user.shared_tempstore'), + $container->get('panels.storage_manager') + ); + } + + /** + * Compiles settings needed for the IPE to function. + * + * @param array $regions + * The render array representing regions. + * @param \Drupal\Core\Layout\LayoutInterface $layout + * The current layout. + * @param \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $panels_display + * The Panels display we are editing. + * @param bool $unsaved + * Whether or not there are unsaved changes. + * + * @return array|bool + * An associative array representing the contents of drupalSettings, or + * FALSE if there was an error. + */ + protected function getDrupalSettings(array $regions, LayoutInterface $layout, PanelsDisplayVariant $panels_display, $unsaved, $locked) { + $settings = [ + 'regions' => [], + ]; + + // Add current block IDs to settings sorted by region. + foreach ($regions as $region => $blocks) { + $settings['regions'][$region] = [ + 'name' => $region, + 'label' => '', + 'blocks' => [], + ]; + + if (!$blocks) { + continue; + } + + /** @var \Drupal\Core\Block\BlockPluginInterface[] $blocks */ + foreach ($blocks as $block_uuid => $block) { + $configuration = $block->getConfiguration(); + $plugin_definition = $block->getPluginDefinition(); + $setting = [ + 'uuid' => $block_uuid, + 'label' => $block->label(), + 'id' => $block->getPluginId(), + 'provider' => $configuration['provider'], + 'plugin_id' => $plugin_definition['id'], + ]; + $settings['regions'][$region]['blocks'][$block_uuid] = $setting; + } + } + + $storage_type = $panels_display->getStorageType(); + $storage_id = $panels_display->getStorageId(); + + // Add the layout information. + $layout_definition = $layout->getPluginDefinition(); + $settings['layout'] = [ + 'id' => $layout->getPluginId(), + 'label' => $layout_definition->getLabel(), + 'original' => TRUE, + ]; + + // Add information about the current user's permissions. + $settings['user_permission'] = [ + 'change_layout' => $this->panelsStorage->access($storage_type, $storage_id, 'change layout', $this->account)->isAllowed(), + 'create_content' => $this->account->hasPermission('administer blocks'), + ]; + + // Add the display variant's config. + $settings['panels_display'] = [ + 'storage_type' => $storage_type, + 'storage_id' => $storage_id, + 'id' => $panels_display->id(), + ]; + + // Inform the App of our saved state. + $settings['unsaved'] = $unsaved; + $settings['locked'] = $locked; + + return $settings; + } + + /** + * {@inheritdoc} + */ + public function build(PanelsDisplayVariant $panels_display) { + // Check to see if the current user has permissions to use the IPE. + $has_permission = $this->account->hasPermission('access panels in-place editing') && $this->panelsStorage->access($panels_display->getStorageType(), $panels_display->getStorageId(), 'update', $this->account)->isAllowed(); + if ($has_permission) { + $has_permission = \Drupal::service('plugin.manager.ipe_access')->access($panels_display); + } + + // Attach the Panels In-place editor library based on permissions. + if ($has_permission) { + // This flag tracks whether or not there are unsaved changes. + $unsaved = FALSE; + $locked = FALSE; + + // If a temporary configuration for this variant exists, use it. + $temp_store_key = $panels_display->getTempStoreId(); + $lock_info = $this->tempStore->getMetadata($temp_store_key); + if ($lock_info) { + if ($lock_info->owner === $this->account->id()) { + $variant_config = $this->tempStore->get($temp_store_key); + unset($variant_config['id']); + $panels_display->setConfiguration($variant_config); + + // Indicate that the user is viewing un-saved changes. + $unsaved = TRUE; + } + else { + $locked = TRUE; + } + } + + $build = parent::build($panels_display); + + $regions = $panels_display->getRegionAssignments(); + $layout = $panels_display->getLayout(); + + foreach ($regions as $region => $blocks) { + // Wrap each region with a unique class and data attribute. + $region_name = Html::getClass("block-region-$region"); + $build[$region]['#prefix'] = '
        '; + $build[$region]['#suffix'] = '
        '; + + if ($blocks) { + foreach ($blocks as $block_id => $block) { + $build[$region][$block_id]['#attributes']['data-block-id'] = $block_id; + } + } + } + + // Attach the required settings and IPE. + $build['#attached']['library'][] = 'panels_ipe/panels_ipe'; + $build['#attached']['drupalSettings']['panels_ipe'] = $this->getDrupalSettings($regions, $layout, $panels_display, $unsaved, $locked); + + // Add our custom elements to the build. + $build['#prefix'] = '
        '; + + $build['#suffix'] = '
        '; + } + // Use a standard build if the user can't use IPE. + else { + $build = parent::build($panels_display); + } + + return $build; + } + +} diff --git a/docroot/modules/contrib/panels/panels_ipe/src/Plugin/IPEAccessBase.php b/docroot/modules/contrib/panels/panels_ipe/src/Plugin/IPEAccessBase.php new file mode 100644 index 000000000..789392762 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/src/Plugin/IPEAccessBase.php @@ -0,0 +1,8 @@ +alterInfo('panels_ipe_ipe_access_info'); + $this->setCacheBackend($cache_backend, 'panels_ipe_ipe_access_plugins'); + } + + /** + * {@inheritdoc} + */ + public function applies(PanelsDisplayVariant $display) { + $applies = []; + foreach ($this->getDefinitions() as $plugin_id => $definition) { + /** @var \Drupal\panels_ipe\Plugin\IPEAccessInterface $plugin */ + $plugin = $this->createInstance($plugin_id); + if ($plugin->applies($display)) { + $applies[$plugin_id] = $plugin; + } + } + return $applies; + } + + /** + * {@inheritdoc} + */ + public function access(PanelsDisplayVariant $display) { + foreach ($this->applies($display) as $plugin_id => $plugin) { + if (!$plugin->access($display)) { + return FALSE; + } + } + return TRUE; + } + +} diff --git a/docroot/modules/contrib/panels/panels_ipe/src/Plugin/IPEAccessManagerInterface.php b/docroot/modules/contrib/panels/panels_ipe/src/Plugin/IPEAccessManagerInterface.php new file mode 100644 index 000000000..9f385b197 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/src/Plugin/IPEAccessManagerInterface.php @@ -0,0 +1,25 @@ +user1 = $this->drupalCreateUser([ + 'access panels in-place editing', + 'administer blocks', + 'administer pages', + ]); + $this->user2 = $this->drupalCreateUser([ + 'access panels in-place editing', + 'administer blocks', + 'administer pages', + ]); + + $this->drupalLogin($this->user1); + + $this->test_route = 'test-page'; + } + + /** + * Tests that the IPE editing session is specific to a user. + */ + public function testUserEditSession() { + $this->visitIPERoute(); + $this->assertSession()->elementExists('css', '.layout--onecol'); + + // Change the layout to lock the IPE. + $this->changeLayout('Columns: 2', 'layout_twocol'); + $this->assertSession()->elementExists('css', '.layout--twocol'); + $this->assertSession()->elementNotExists('css', '.layout--onecol'); + $this->assertSession()->elementExists('css', '[data-tab-id="save"]'); + + // Ensure the second user does not see the session of the other user. + $this->drupalLogin($this->user2); + $this->visitIPERoute(); + $this->assertSession()->elementExists('css', '.layout--onecol'); + $this->assertSession()->elementNotExists('css', '.layout--twocol'); + // Ensure the IPE is locked. + $this->assertSession()->elementNotExists('css', '[data-tab-id="edit"]'); + $this->assertSession()->elementExists('css', '[data-tab-id="locked"]'); + + // Click the break lock button. + $this->breakLock(); + $this->assertSession()->waitForElementVisible('css', '[data-tab-id="edit"]'); + + // Log back in as the first user to find the edits gone. + $this->drupalLogin($this->user1); + $this->visitIPERoute(); + $this->assertSession()->elementExists('css', '[data-tab-id="edit"]'); + $this->assertSession()->elementNotExists('css', '[data-tab-id="save"]'); + $this->assertSession()->elementExists('css', '.layout--onecol'); + } + +} diff --git a/docroot/modules/contrib/panels/panels_ipe/tests/src/FunctionalJavascript/PanelsIPETestBase.php b/docroot/modules/contrib/panels/panels_ipe/tests/src/FunctionalJavascript/PanelsIPETestBase.php new file mode 100644 index 000000000..9a67f9b59 --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/tests/src/FunctionalJavascript/PanelsIPETestBase.php @@ -0,0 +1,63 @@ +visitIPERoute(). + */ + protected $window_size = [1024, 768]; + + /** + * Tests that the IPE is loaded on the current test route. + */ + public function testIPEIsLoaded() { + $this->visitIPERoute(); + + $this->assertIPELoaded(); + } + + /** + * Tests that adding a block with default configuration works. + */ + public function testIPEAddBlock() { + $this->visitIPERoute(); + + $this->addBlock('System', 'system_breadcrumb_block'); + } + + /** + * Tests that changing layout from one (default) to two columns works. + */ + public function testIPEChangeLayout() { + $this->visitIPERoute(); + + // Change the layout to two columns. + $this->changeLayout('Columns: 2', 'layout_twocol'); + $this->waitUntilVisible('.layout--twocol', 10000, 'Layout changed to two column.'); + } + + /** + * Visits the test route and sets an appropriate window size for IPE. + */ + protected function visitIPERoute() { + $this->drupalGet($this->test_route); + + // Set the window size to ensure that IPE elements are visible. + call_user_func_array([$this->getSession(), 'resizeWindow'], $this->window_size); + } + +} diff --git a/docroot/modules/contrib/panels/panels_ipe/tests/src/FunctionalJavascript/PanelsIPETestTrait.php b/docroot/modules/contrib/panels/panels_ipe/tests/src/FunctionalJavascript/PanelsIPETestTrait.php new file mode 100644 index 000000000..ea750768d --- /dev/null +++ b/docroot/modules/contrib/panels/panels_ipe/tests/src/FunctionalJavascript/PanelsIPETestTrait.php @@ -0,0 +1,281 @@ +waitUntilVisible('#panels-ipe-content'); + } + + /** + * Asserts that an on-screen Block contains the given content. + * + * @param string $block_id + * The unique ID of the Block. + * @param string $content + * The content to check. + * @param string $message + * (Optional) message to pass to the assertContains() call. + */ + protected function assertBlockContains($block_id, $content, $message = '') { + $selector = '[data-block-id="' . $block_id . '"]'; + $block_element = $this->getSession()->getPage()->find('css', $selector); + $this->assertContains($content, $block_element->getHtml(), $message); + } + + /** + * Enables the in-place editing mode of IPE. + */ + protected function enableEditing() { + // Click the "Edit" tab if it's not already active. + $selector = '[data-tab-id="edit"]:not(.active)'; + $inactive_tab = $this->getSession()->getPage()->find('css', $selector); + if ($inactive_tab) { + $this->clickAndWait($selector); + } + $this->assertSession()->elementExists('css', '[data-tab-id="edit"].active'); + } + + /** + * Disables the in-place editing mode of IPE. + */ + protected function disableEditing() { + // Click the "Edit" tab if it's already active. + $selector = '[data-tab-id="edit"].active'; + $active_tab = $this->getSession()->getPage()->find('css', $selector); + if ($active_tab) { + $this->clickAndWait($selector); + } + $this->assertSession()->elementNotExists('css', '[data-tab-id="edit"].active'); + } + + /** + * Breaks the lock of an IPE session. + */ + protected function breakLock() { + // Click the "Locked" tab. + $selector = '[data-tab-id="locked"]:not(.active)'; + $inactive_tab = $this->getSession()->getPage()->find('css', $selector); + if ($inactive_tab) { + $this->click($selector); + } + } + + /** + * Changes the IPE layout. + * + * This function assumes you're using Panels layouts and as a result expects + * the PanelsIPELayoutForm to auto-submit. + * + * @param string $category + * The name of the category, i.e. "One Column". + * @param string $layout_id + * The ID of the layout, i.e. "layout_onecol". + */ + protected function changeLayout($category, $layout_id) { + // Open the "Change Layout" tab. + $this->clickAndWait('[data-tab-id="change_layout"]'); + + // Wait for layouts to be pulled into our collection. + $this->waitUntilNotPresent('.ipe-icon-loading'); + + // Select the target category. + $this->clickAndWait('[data-category="' . $category . '"]'); + + // Select the target layout. + $this->clickAndWait('[data-layout-id="' . $layout_id . '"]'); + + // Wait for the form to load/submit. + $this->waitUntilNotPresent('.ipe-icon-loading'); + + // Wait for the edit tab to become active (happens automatically after + // form submit). + $this->waitUntilVisible('[data-tab-id="edit"].active'); + } + + /** + * Adds a Block (Plugin) to the page. + * + * @param string $category + * The name of the category, i.e. "Help". + * @param string $plugin_id + * The ID of the Block Plugin, i.e. "help_block". + * + * @return string + * The newly created Block ID. + */ + protected function addBlock($category, $plugin_id) { + // Get a list of current Block Plugins. + $old_blocks = $this->getOnScreenBlockIDs(); + + // Open the "Manage Content" tab and select the given Block Plugin. + $this->clickAndWait('[data-tab-id="manage_content"]'); + + $this->waitUntilNotPresent('.ipe-icon-loading'); + + $this->clickAndWait('[data-category="' . $category . '"]'); + // @todo Remove when https://github.com/jcalderonzumba/gastonjs/issues/19 + // is fixed. Currently clicking anchor tags with nested elements is not + // possible. + $this->getSession()->executeScript("jQuery('" . '[data-plugin-id="' . $plugin_id . '"]' . "')[0].click()"); + + // Wait for the Block form to finish loading/opening. + $this->waitUntilNotPresent('.ipe-icon-loading'); + $this->waitUntilVisible('.ipe-form form'); + + // Submit the form with default settings. + $this->saveBlockConfigurationForm(); + + // Find the newest Block Plugin. + $new_blocks = $this->getOnScreenBlockIDs(); + $diff_blocks = array_diff($new_blocks, $old_blocks); + $new_block = reset($diff_blocks); + + $this->assertNotFalse($new_block, 'New block was placed on screen.'); + + return $new_block; + } + + /** + * Opens the Block configuration form for a given on-screen block. + * + * @param string $block_id + * The unique ID of the Block you want to configure. + */ + protected function openBlockConfigurationForm($block_id) { + $base_selector = '[data-block-id="' . $block_id . '"]'; + $configure_selector = $base_selector . ' [data-action-id="configure"]'; + + // Enable the in place editor and click the configure button. + $this->enableEditing(); + $this->clickAndWait($configure_selector); + + // Wait for the Block form to finish opening. + $this->waitUntilNotPresent('.ipe-icon-loading'); + $this->waitForAjaxToFinish(); + } + + /** + * Sets a configuration value on the Block configuration form. + * + * @param string $name + * The string name of the form value, i.e. settings[label]. + * @param string|bool|array $value + * The value for the given form value. + */ + protected function setBlockConfigurationFormValue($name, $value) { + $selector_converter = new CssSelectorConverter(); + $xpath = $selector_converter->toXPath('.panels-ipe-block-plugin-form [name="' . $name . '"]'); + + // Set the value of the given form field. + $this->getSession()->getDriver()->setValue($xpath, $value); + } + + /** + * Saves the currently open Block configuration form. + */ + protected function saveBlockConfigurationForm() { + $submit_selector = '.panels-ipe-block-plugin-form [data-drupal-selector="edit-submit"]'; + $this->clickAndWait($submit_selector); + } + + /** + * Removes a Block from the page. + * + * @param string $block_id + * The unique ID of the Block you want to remove. + */ + protected function removeBlock($block_id) { + $base_selector = '[data-block-id="' . $block_id . '"]'; + $remove_selector = $base_selector . ' [data-action-id="remove"]'; + + // Enable the in place editor and click the remove (X) button. + $this->enableEditing(); + $this->clickAndWait($remove_selector); + + // Ensure that the Block is removed. + $message = 'Block does not exist after removal'; + $this->assertElementNotPresent($base_selector, $message); + } + + /** + * Grabs the current Block IDs from the page. + * + * This is required as Block IDs are randomly generated when blocks are + * placed, so you can't predict their IDs beforehand. + * + * @return array + * An array of Block IDs. + */ + protected function getOnScreenBlockIDs() { + $block_ids = []; + $session = $this->getSession(); + $blocks = $session->getPage()->findAll('css', '[data-block-id]'); + if (count($blocks)) { + /** @var \Behat\Mink\Element\NodeElement $block */ + foreach ($blocks as $block) { + $block_ids[] = $block->getAttribute('data-block-id'); + } + } + return $block_ids; + } + + /** + * Clicks an arbitrary element and waits for AJAX/animations to finish. + * + * @param string $selector + * The CSS selector. + */ + protected function clickAndWait($selector) { + $this->click($selector); + $this->waitForAjaxToFinish(); + } + + /** + * Waits and asserts that a given element is visible. + * + * @param string $selector + * The CSS selector. + * @param int $timeout + * (Optional) Timeout in milliseconds, defaults to 10000. + * @param string $message + * (Optional) Message to pass to assertJsCondition(). + */ + protected function waitUntilVisible($selector, $timeout = 10000, $message = '') { + $condition = "jQuery('" . $selector . ":visible').length > 0"; + $this->assertJsCondition($condition, $timeout, $message); + } + + /** + * Waits and asserts that a given element is not present. + * + * @param string $selector + * The CSS selector. + * @param int $timeout + * (Optional) Timeout in milliseconds, defaults to 10000. + * @param string $message + * (Optional) Message to pass to assertJsCondition(). + */ + protected function waitUntilNotPresent($selector, $timeout = 10000, $message = '') { + $condition = "jQuery('" . $selector . "').length === 0"; + $this->assertJsCondition($condition, $timeout, $message); + } + + /** + * Waits for jQuery to become ready and animations to complete. + */ + protected function waitForAjaxToFinish() { + $condition = "(0 === jQuery.active && 0 === jQuery(':animated').length)"; + $this->assertJsCondition($condition, 10000); + } + +} diff --git a/docroot/modules/contrib/panels/src/Annotation/DisplayBuilder.php b/docroot/modules/contrib/panels/src/Annotation/DisplayBuilder.php new file mode 100644 index 000000000..302563bda --- /dev/null +++ b/docroot/modules/contrib/panels/src/Annotation/DisplayBuilder.php @@ -0,0 +1,28 @@ +get($tempstore_id)->get($machine_name[0]); + // PageManager specific handling. If $machine_name[1] is set, it's the + // page variant ID. + if (isset($machine_name[1]) && !isset($cached_values['page_variant'])) { + /** @var \Drupal\page_manager\PageInterface $page */ + $page = $cached_values['page']; + $cached_values['page_variant'] = $page->getVariant($machine_name[1]); + } + if (!isset($cached_values['plugin']) && !empty($cached_values['page_variant'])) { + $cached_values['plugin'] = $cached_values['page_variant']->getVariantPlugin(); + } + return $cached_values; + } + +} diff --git a/docroot/modules/contrib/panels/src/Controller/Panels.php b/docroot/modules/contrib/panels/src/Controller/Panels.php new file mode 100644 index 000000000..7774042b7 --- /dev/null +++ b/docroot/modules/contrib/panels/src/Controller/Panels.php @@ -0,0 +1,152 @@ +blockManager = $block_manager; + $this->conditionManager = $condition_manager; + $this->variantManager = $variant_manager; + $this->contextHandler = $context_handler; + $this->tempstore = $tempstore; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.block'), + $container->get('plugin.manager.condition'), + $container->get('plugin.manager.display_variant'), + $container->get('context.handler'), + $container->get('user.shared_tempstore'), + $container->get('plugin.manager.panels.pattern') + ); + } + + /** + * Presents a list of blocks to add to the variant. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request. + * @param string $machine_name + * The identifier of the block display variant. + * @param string $tempstore_id + * The identifier of the temporary store. + * + * @return array + * The block selection page. + */ + public function selectBlock(Request $request, $machine_name, $tempstore_id) { + $cached_values = $this->getCachedValues($this->tempstore, $tempstore_id, $machine_name); + /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $variant_plugin */ + $variant_plugin = $cached_values['plugin']; + /** @var \Drupal\panels\Plugin\PanelsPattern\PanelsPatternInterface $pattern_plugin */ + $pattern_plugin = $variant_plugin->getPattern(); + + $contexts = $pattern_plugin->getDefaultContexts($this->tempstore, $tempstore_id, $machine_name); + $variant_plugin->setContexts($contexts); + + // Add a section containing the available blocks to be added to the variant. + $build = [ + '#type' => 'container', + '#attached' => [ + 'library' => [ + 'core/drupal.ajax', + ], + ], + ]; + $available_plugins = $this->blockManager->getDefinitionsForContexts($variant_plugin->getContexts()); + // Order by category, and then by admin label. + $available_plugins = $this->blockManager->getSortedDefinitions($available_plugins); + foreach ($available_plugins as $plugin_id => $plugin_definition) { + // Make a section for each region. + $category = $plugin_definition['category']; + $category_key = 'category-' . $category; + if (!isset($build[$category_key])) { + $build[$category_key] = [ + '#type' => 'fieldgroup', + '#title' => $category, + 'content' => [ + '#theme' => 'links', + ], + ]; + } + // Add a link for each available block within each region. + $build[$category_key]['content']['#links'][$plugin_id] = [ + 'title' => $plugin_definition['admin_label'], + 'url' => $pattern_plugin->getBlockAddUrl($tempstore_id, $machine_name, $plugin_id, $request->query->get('region'), $request->query->get('destination')), + 'attributes' => $this->getAjaxAttributes(), + ]; + } + return $build; + } + +} diff --git a/docroot/modules/contrib/panels/src/Form/LayoutChangeRegions.php b/docroot/modules/contrib/panels/src/Form/LayoutChangeRegions.php new file mode 100644 index 000000000..d14f42f05 --- /dev/null +++ b/docroot/modules/contrib/panels/src/Form/LayoutChangeRegions.php @@ -0,0 +1,222 @@ +get('plugin.manager.core.layout'), + $container->get('user.shared_tempstore') + ); + } + + /** + * LayoutChangeRegions constructor. + * + * @param \Drupal\Core\Layout\LayoutPluginManagerInterface $manager + * The layout plugin manager. + * @param \Drupal\user\SharedTempStoreFactory $tempstore + * The tempstore factory. + */ + public function __construct(LayoutPluginManagerInterface $manager, SharedTempStoreFactory $tempstore) { + $this->manager = $manager; + $this->tempstore = $tempstore; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'panels_layout_regions_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + + /* @var $variant_plugin \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant */ + $variant_plugin = $cached_values['plugin']; + + $form['#attached']['library'][] = 'block/drupal.block'; + + $form['old_layout'] = [ + '#title' => $this->t('Old Layout'), + '#type' => 'select', + '#options' => $this->manager->getLayoutOptions(), + '#default_value' => $cached_values['layout_change']['old_layout'], + '#disabled' => TRUE, + ]; + + $form['new_layout'] = [ + '#title' => $this->t('New Layout'), + '#type' => 'select', + '#options' => $this->manager->getLayoutOptions(), + '#default_value' => $cached_values['layout_change']['new_layout'], + '#disabled' => TRUE, + ]; + + $layout_settings = !empty($cached_values['layout_change']['layout_settings']) ? $cached_values['layout_change']['layout_settings'] : []; + $old_layout = $this->manager->createInstance($cached_values['layout_change']['old_layout'], []); + $new_layout = $this->manager->createInstance($cached_values['layout_change']['new_layout'], $layout_settings); + + if ($block_assignments = $variant_plugin->getRegionAssignments()) { + // Build a table of all blocks used by this variant. + + $form['blocks'] = [ + '#type' => 'table', + '#header' => [ + $this->t('Label'), + $this->t('ID'), + $this->t('Region'), + $this->t('Weight'), + ], + '#attributes' => array( + 'id' => 'blocks', + ), + '#empty' => $this->t('There are no regions for blocks.'), + ]; + + // Loop through the blocks per region. + $new_regions = $new_layout->getPluginDefinition()->getRegionLabels(); + $new_regions['__unassigned__'] = $this->t('Unassigned'); + + $regions = []; + foreach ($old_layout->getPluginDefinition()->getRegions() as $region => $region_definition) { + if (empty($block_assignments[$region])) { + continue; + } + $label = $region_definition['label']; + // Prevent region names clashing with new regions. + $region_id = 'old_'.$region; + $new_region = isset($new_regions[$region]) ? $region : '__unassigned__'; + $row['label']['#markup'] = $label; + $row['id']['#markup'] = $region; + // Allow the region to be changed for each block. + $row['region'] = [ + '#title' => $this->t('Region'), + '#title_display' => 'invisible', + '#type' => 'select', + '#options' => $new_regions, + '#default_value' => $new_region, + '#attributes' => [ + 'class' => ['block-region-select', 'block-region-' . $new_region], + ], + ]; + // Allow the weight to be changed for each region. + $row['weight'] = [ + '#type' => 'weight', + '#default_value' => 0, + '#title' => $this->t('Weight for @block block', ['@block' => $label]), + '#title_display' => 'invisible', + '#attributes' => [ + 'class' => ['block-weight', 'block-weight-' . $region], + ], + ]; + $form['blocks'][$region_id] = $row; + $regions[$new_region][] = $region_id; + } + + foreach ($new_regions as $region => $label) { + + // Add a section for each region and allow blocks to be dragged between + // them. + $form['blocks']['region-' . $region] = [ + '#attributes' => [ + 'class' => ['region-title', 'region-title-' . $region], + 'no_striping' => TRUE, + ], + ]; + $form['blocks']['region-' . $region]['title'] = [ + '#markup' => $label, + '#wrapper_attributes' => [ + 'colspan' => 4, + ], + ]; + $form['blocks']['region-' . $region . '-message'] = [ + '#attributes' => [ + 'class' => [ + 'region-message', + 'region-' . $region . '-message', + empty($regions[$region]) ? 'region-empty' : 'region-populated', + ], + ], + ]; + if (empty($regions[$region])) { + $form['blocks']['region-' . $region . '-message']['message'] = [ + '#markup' => '' . $this->t('No blocks in this region') . '', + '#wrapper_attributes' => [ + 'colspan' => 4, + ], + ]; + } + } + } + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $plugin */ + $plugin = $cached_values['plugin']; + $blocks = $plugin->getRegionAssignments(); + /** + * @var string $region + * @var \Drupal\Core\Block\BlockPluginInterface[] $block_group + */ + foreach ($blocks as $region => $block_group) { + foreach ($block_group as $uuid => $block) { + $new_region = $form_state->getValue(['blocks', 'old_' . $region, 'region']); + $block->setConfiguration(['region' => $new_region] + $block->getConfiguration()); + } + } + $layout_id = !empty($cached_values['layout_change']['new_layout']) ? $cached_values['layout_change']['new_layout'] : $plugin->getConfiguration()['layout']; + $layout_settings = !empty($cached_values['layout_change']['layout_settings']) ? $cached_values['layout_change']['layout_settings'] : []; + $plugin->setLayout($layout_id, $layout_settings); + unset($cached_values['layout_change']); + $form_state->setTemporaryValue('wizard', $cached_values); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + foreach ($form_state->getValue('blocks') as $old_region => $values) { + if ($values['region'] == '__unassigned__') { + $form_state->setErrorByName('blocks][' . $old_region, $this->t('You must assign your old regions to an available new region.')); + } + } + } + +} diff --git a/docroot/modules/contrib/panels/src/Form/LayoutChangeSettings.php b/docroot/modules/contrib/panels/src/Form/LayoutChangeSettings.php new file mode 100644 index 000000000..84d062596 --- /dev/null +++ b/docroot/modules/contrib/panels/src/Form/LayoutChangeSettings.php @@ -0,0 +1,208 @@ +get('plugin.manager.core.layout'), + $container->get('user.shared_tempstore') + ); + } + + /** + * LayoutChangeSettings constructor. + * + * @param \Drupal\Core\Layout\LayoutPluginManagerInterface $manager + * The layout plugin manager. + * @param \Drupal\user\SharedTempStoreFactory $tempstore + * The tempstore factory. + */ + public function __construct(LayoutPluginManagerInterface $manager, SharedTempStoreFactory $tempstore) { + $this->manager = $manager; + $this->tempstore = $tempstore; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'panels_layout_settings_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + + /* @var $variant_plugin \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant */ + $variant_plugin = $cached_values['plugin']; + + $form['old_layout'] = [ + '#title' => $this->t('Old Layout'), + '#type' => 'select', + '#options' => $this->manager->getLayoutOptions(), + '#default_value' => !empty($cached_values['layout_change']['old_layout']) ? $cached_values['layout_change']['old_layout'] : '', + '#disabled' => TRUE, + '#access' => !empty($cached_values['layout_change']), + ]; + + $form['new_layout'] = [ + '#title' => $this->t('New Layout'), + '#type' => 'select', + '#options' => $this->manager->getLayoutOptions(), + '#default_value' => !empty($cached_values['layout_change']['new_layout']) ? $cached_values['layout_change']['new_layout'] : '', + '#disabled' => TRUE, + '#access' => !empty($cached_values['layout_change']), + ]; + + // If a layout is already selected, show the layout settings. + $form['layout_settings_wrapper'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Layout settings'), + '#tree' => TRUE, + ]; + + $layout_settings = !empty($cached_values['layout_change']['layout_settings']) ? $cached_values['layout_change']['layout_settings'] : []; + if (!$layout_settings && $variant_plugin->getLayout() instanceof ConfigurablePluginInterface) { + $layout_settings = $variant_plugin->getLayout()->getConfiguration(); + } + $layout_id = !empty($cached_values['layout_change']['new_layout']) ? $cached_values['layout_change']['new_layout'] : $variant_plugin->getConfiguration()['layout']; + $layout = $this->manager->createInstance($layout_id, $layout_settings); + if ($layout instanceof PluginFormInterface) { + $form['layout_settings_wrapper']['layout_settings'] = $layout->buildConfigurationForm([], $form_state); + } + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var \Drupal\ctools\Wizard\EntityFormWizardInterface $wizard */ + $wizard = $form_state->getFormObject(); + $next_params = $wizard->getNextParameters($cached_values); + /* @var $plugin \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant */ + $plugin = $cached_values['plugin']; + $layout_id = !empty($cached_values['layout_change']['new_layout']) ? $cached_values['layout_change']['new_layout'] : $plugin->getConfiguration()['layout']; + /** @var \Drupal\Core\Layout\LayoutInterface $layout */ + $layout = $this->manager->createInstance($layout_id, []); + // If we're dealing with a form, submit it. + if ($layout instanceof PluginFormInterface) { + $sub_form_state = new FormState(); + $plugin_values = $form_state->getValue(['layout_settings_wrapper', 'layout_settings']); + // If form values came through the step's submission, handle them. + if ($plugin_values) { + $sub_form_state->setValues($plugin_values); + $layout->submitConfigurationForm($form, $sub_form_state); + // If this plugin is configurable, get that configuration and set it in + // cached values. + if ($layout instanceof ConfigurablePluginInterface) { + $cached_values = $this->setCachedValues($next_params['step'], $plugin, $layout, $cached_values, $layout->getConfiguration()); + } + } + // If no values came through, set the cached values layout config to + // empty array. + else { + $cached_values = $this->setCachedValues($next_params['step'], $plugin, $layout, $cached_values, []); + } + } + // If we're not dealing with a Layout plugin that implements + // PluginFormInterface, handle this unlikely situation. + else { + $cached_values = $this->setCachedValues($next_params['step'], $plugin, $layout, $cached_values, []); + } + $form_state->setTemporaryValue('wizard', $cached_values); + } + + /** + * Sets the appropriate cached values for the layout settings. + * + * Depending upon the next step, this form could be required to properly + * update the values of the PanelsDisplayVariant plugin in the cached values + * or it could just be adding the configuration to the cached values + * directly. This bit of logic is repeated a number of times in the form + * submission, and so abstracting it is typical DRY approach. + * + * @param string $next_step + * The next step of the wizard. + * @param \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $plugin + * The plugin to update. + * @param \Drupal\Core\Layout\LayoutInterface $layout + * The layout for which we are upating settings. + * @param array $cached_values + * The current cached values from the wizard. + * @param array $configuration + * The new configuration of the layout. + * + * @return mixed + * Returns the new cached values. + */ + protected function setCachedValues($next_step, PanelsDisplayVariant $plugin, LayoutInterface $layout, $cached_values, $configuration) { + // The step is modified by various wizards but will end in "regions" + if (substr($next_step, 0 -7) == 'regions') { + $cached_values['layout_change']['layout_settings'] = $configuration; + } + else { + $plugin->setLayout($layout, $configuration); + $cached_values['plugin'] = $plugin; + unset($cached_values['layout_change']); + } + return $cached_values; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /* @var $plugin \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant */ + $plugin = $cached_values['plugin']; + $layout_id = !empty($cached_values['layout_change']['new_layout']) ? $cached_values['layout_change']['new_layout'] : $plugin->getConfiguration()['layout']; + $layout = $this->manager->createInstance($layout_id, []); + if ($layout instanceof PluginFormInterface) { + $sub_form_state = new FormState(); + $plugin_values = $form_state->getValue(['layout_settings_wrapper', 'layout_settings']); + if ($plugin_values) { + $sub_form_state->setValues($plugin_values); + $layout->validateConfigurationForm($form, $sub_form_state); + } + } + } + +} diff --git a/docroot/modules/contrib/panels/src/Form/LayoutPluginSelector.php b/docroot/modules/contrib/panels/src/Form/LayoutPluginSelector.php new file mode 100644 index 000000000..cc1847bf3 --- /dev/null +++ b/docroot/modules/contrib/panels/src/Form/LayoutPluginSelector.php @@ -0,0 +1,138 @@ +get('plugin.manager.core.layout'), + $container->get('user.shared_tempstore') + ); + } + + /** + * LayoutPluginSelector constructor. + * + * @param \Drupal\Core\Layout\LayoutPluginManagerInterface $manager + * The layout plugin manager. + * @param \Drupal\user\SharedTempStoreFactory $tempstore + * The tempstore factory. + */ + public function __construct(LayoutPluginManagerInterface $manager, SharedTempStoreFactory $tempstore) { + $this->manager = $manager; + $this->tempstore = $tempstore; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'panels_layout_selection_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + + /* @var $variant_plugin \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant */ + $variant_plugin = $cached_values['plugin']; + $form['layout'] = [ + '#title' => $this->t('Layout'), + '#type' => 'select', + '#options' => $this->manager->getLayoutOptions(), + '#default_value' => $variant_plugin->getConfiguration()['layout'] ?: NULL, + ]; + + $wizard = $form_state->getFormObject(); + $form['update_layout'] = [ + '#type' => 'submit', + '#value' => $this->t('Change Layout'), + '#access' => !empty($variant_plugin->getConfiguration()['layout']), + '#validate' => [ + [$this, 'validateForm'], + ], + '#submit' => [ + [$this, 'submitForm'], + [$wizard, 'submitForm'], + ], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /* @var $variant_plugin \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant */ + $variant_plugin = $cached_values['plugin']; + // If we're changing the layout, the variant plugin must remain out of date + // until the layout is fully configured and regions are remapped. + if ($form_state->getValue('op') == $form['update_layout']['#value']) { + $cached_values['layout_change'] = [ + 'old_layout' => $variant_plugin->getConfiguration()['layout'], + 'new_layout' => $form_state->getValue('layout'), + ]; + /** @var \Drupal\ctools\Wizard\EntityFormWizardInterface $wizard */ + $wizard = $form_state->getFormObject(); + $next_op = $wizard->getNextOp(); + $form_state->setValue('op', $next_op); + } + // Creating a new layout. Take the selected layout value. + else { + $variant_plugin->setLayout($form_state->getValue('layout')); + } + + $cached_values['plugin'] = $variant_plugin; + + $form_state->setTemporaryValue('wizard', $cached_values); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + + /* @var $variant_plugin \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant */ + $variant_plugin = $cached_values['plugin']; + + if ((string)$form_state->getValue('op') == $this->t('Change Layout') && $variant_plugin->getConfiguration()['layout'] == $form_state->getValue('layout')) { + $form_state->setErrorByName('layout', $this->t('You must select a different layout if you wish to change layouts.')); + } + if ($form['update_layout']['#access'] && $variant_plugin->getConfiguration()['layout'] != $form_state->getValue('layout') && $form_state->getValue('op') != $form['update_layout']['#value']) { + $form_state->setErrorByName('layout', $this->t('To select a different layout, you must click "Change Layout".')); + } + } + +} diff --git a/docroot/modules/contrib/panels/src/Form/PanelsAddBlockForm.php b/docroot/modules/contrib/panels/src/Form/PanelsAddBlockForm.php new file mode 100644 index 000000000..c36f8849b --- /dev/null +++ b/docroot/modules/contrib/panels/src/Form/PanelsAddBlockForm.php @@ -0,0 +1,78 @@ +blockManager = $block_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('user.shared_tempstore'), + $container->get('plugin.manager.block') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'panels_add_block_form'; + } + + /** + * {@inheritdoc} + */ + protected function prepareBlock($plugin_id) { + $block = $this->blockManager->createInstance($plugin_id); + $block_id = $this->getVariantPlugin()->addBlock($block->getConfiguration()); + return $this->getVariantPlugin()->getBlock($block_id); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, Request $request = NULL, $tempstore_id = NULL, $machine_name = NULL, $block_id = NULL) { + $form = parent::buildForm($form, $form_state, $tempstore_id, $machine_name, $block_id); + $form['region']['#default_value'] = $request->query->get('region'); + return $form; + } + + /** + * {@inheritdoc} + */ + protected function submitText() { + return $this->t('Add block'); + } + +} diff --git a/docroot/modules/contrib/panels/src/Form/PanelsBlockConfigureFormBase.php b/docroot/modules/contrib/panels/src/Form/PanelsBlockConfigureFormBase.php new file mode 100644 index 000000000..675568796 --- /dev/null +++ b/docroot/modules/contrib/panels/src/Form/PanelsBlockConfigureFormBase.php @@ -0,0 +1,200 @@ +tempstore = $tempstore; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('user.shared_tempstore') + ); + } + + /** + * Get the tempstore id. + * + * @return string + */ + protected function getTempstoreId() { + return $this->tempstore_id; + } + + /** + * Get the tempstore. + * + * @return \Drupal\user\SharedTempStore + */ + protected function getTempstore() { + return $this->tempstore->get($this->getTempstoreId()); + } + + /** + * Prepares the block plugin based on the block ID. + * + * @param string $block_id + * Either a block ID, or the plugin ID used to create a new block. + * + * @return \Drupal\Core\Block\BlockPluginInterface + * The block plugin. + */ + abstract protected function prepareBlock($block_id); + + /** + * Returns the text to use for the submit button. + * + * @return string + * The submit button text. + */ + abstract protected function submitText(); + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $tempstore_id = NULL, $machine_name = NULL, $block_id = NULL) { + $this->tempstore_id = $tempstore_id; + $cached_values = $this->getCachedValues($this->tempstore, $tempstore_id, $machine_name); + $this->variantPlugin = $cached_values['plugin']; + + $contexts = $this->variantPlugin->getPattern()->getDefaultContexts($this->tempstore, $this->getTempstoreId(), $machine_name); + $this->variantPlugin->setContexts($contexts); + $form_state->setTemporaryValue('gathered_contexts', $contexts); + + $this->block = $this->prepareBlock($block_id); + $form_state->set('machine_name', $machine_name); + $form_state->set('block_id', $this->block->getConfiguration()['uuid']); + + // Some Block Plugins rely on the block_theme value to load theme settings. + // @see \Drupal\system\Plugin\Block\SystemBrandingBlock::blockForm(). + $form_state->set('block_theme', $this->config('system.theme')->get('default')); + + $form['#tree'] = TRUE; + $form['settings'] = $this->block->buildConfigurationForm([], $form_state); + $form['settings']['id'] = [ + '#type' => 'value', + '#value' => $this->block->getPluginId(), + ]; + $form['region'] = [ + '#title' => $this->t('Region'), + '#type' => 'select', + '#options' => $this->variantPlugin->getRegionNames(), + '#default_value' => $this->variantPlugin->getRegionAssignment($this->block->getConfiguration()['uuid']), + '#required' => TRUE, + ]; + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->submitText(), + '#button_type' => 'primary', + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + // The page might have been serialized, resulting in a new variant + // collection. Refresh the block object. + $this->block = $this->getVariantPlugin()->getBlock($form_state->get('block_id')); + + $settings = (new FormState())->setValues($form_state->getValue('settings')); + // Call the plugin validate handler. + $this->block->validateConfigurationForm($form, $settings); + // Update the original form values. + $form_state->setValue('settings', $settings->getValues()); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $settings = (new FormState())->setValues($form_state->getValue('settings')); + + // Call the plugin submit handler. + $this->block->submitConfigurationForm($form, $settings); + // Update the original form values. + $form_state->setValue('settings', $settings->getValues()); + + if ($this->block instanceof ContextAwarePluginInterface) { + $this->block->setContextMapping($settings->getValue('context_mapping', [])); + } + + $configuration = $this->block->getConfiguration(); + $configuration['region'] = $form_state->getValue('region'); + $this->getVariantPlugin()->updateBlock($this->block->getConfiguration()['uuid'], $configuration); + + $cached_values = $this->getCachedValues($this->tempstore, $this->tempstore_id, $form_state->get('machine_name')); + $cached_values['plugin'] = $this->getVariantPlugin(); + // PageManager specific handling. + if (isset($cached_values['page_variant'])) { + $cached_values['page_variant']->getVariantPlugin()->setConfiguration($cached_values['plugin']->getConfiguration()); + } + $this->getTempstore()->set($cached_values['id'], $cached_values); + } + + /** + * Gets the variant plugin for this page variant entity. + * + * @return \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant + */ + protected function getVariantPlugin() { + return $this->variantPlugin; + } + +} diff --git a/docroot/modules/contrib/panels/src/Form/PanelsContentForm.php b/docroot/modules/contrib/panels/src/Form/PanelsContentForm.php new file mode 100644 index 000000000..9ab78baf3 --- /dev/null +++ b/docroot/modules/contrib/panels/src/Form/PanelsContentForm.php @@ -0,0 +1,250 @@ +tempstore = $tempstore; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('user.shared_tempstore') + ); + } + + /** + * Get the tempstore ID. + * + * @return string + */ + protected function getTempstoreId() { + return $this->tempstore_id; + } + + /** + * Get the tempstore. + * + * @return \Drupal\user\SharedTempStore + */ + protected function getTempstore() { + return $this->tempstore->get($this->getTempstoreId()); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'panels_block_page_content'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form['#attached']['library'][] = 'block/drupal.block'; + $this->tempstore_id = $form_state->getFormObject()->getTempstoreId(); + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $variant_plugin */ + $variant_plugin = $cached_values['plugin']; + // Allow to configure the page title, even when adding a new display. + // Default to the page label in that case. + $form['page_title'] = [ + '#type' => 'textfield', + '#title' => $this->t('Page title'), + '#description' => $this->t('Configure the page title that will be used for this display.'), + '#default_value' => $variant_plugin->getConfiguration()['page_title'] ?: '', + ]; + $pattern_plugin = $variant_plugin->getPattern(); + $machine_name = $pattern_plugin->getMachineName($cached_values); + + // Set up the attributes used by a modal to prevent duplication later. + $attributes = $this->getAjaxAttributes(); + $add_button_attributes = $this->getAjaxButtonAttributes(); + + if ($block_assignments = $variant_plugin->getRegionAssignments()) { + // Build a table of all blocks used by this variant. + $form['add'] = [ + '#type' => 'link', + '#title' => $this->t('Add new block'), + '#url' => $pattern_plugin->getBlockListUrl($this->tempstore_id, $machine_name, NULL, $this->getRequest()->getRequestUri()), + '#attributes' => $add_button_attributes, + '#attached' => [ + 'library' => [ + 'core/drupal.ajax', + ], + ], + ]; + $form['blocks'] = [ + '#type' => 'table', + '#header' => [ + $this->t('Label'), + $this->t('Plugin ID'), + $this->t('Region'), + $this->t('Weight'), + $this->t('Operations'), + ], + '#attributes' => array( + 'id' => 'blocks', + ), + '#empty' => $this->t('There are no regions for blocks.'), + ]; + // Loop through the blocks per region. + foreach ($block_assignments as $region => $blocks) { + // Add a section for each region and allow blocks to be dragged between + // them. + $form['blocks']['#tabledrag'][] = [ + 'action' => 'match', + 'relationship' => 'sibling', + 'group' => 'block-region-select', + 'subgroup' => 'block-region-' . $region, + 'hidden' => FALSE, + ]; + $form['blocks']['#tabledrag'][] = [ + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'block-weight', + 'subgroup' => 'block-weight-' . $region, + ]; + $form['blocks']['region-' . $region] = [ + '#attributes' => [ + 'class' => ['region-title', 'region-title-' . $region], + 'no_striping' => TRUE, + ], + ]; + $form['blocks']['region-' . $region]['title'] = [ + '#markup' => $variant_plugin->getRegionName($region), + '#wrapper_attributes' => [ + 'colspan' => 5, + ], + ]; + $form['blocks']['region-' . $region . '-message'] = [ + '#attributes' => [ + 'class' => [ + 'region-message', + 'region-' . $region . '-message', + empty($blocks) ? 'region-empty' : 'region-populated', + ], + ], + ]; + $form['blocks']['region-' . $region . '-message']['message'] = [ + '#markup' => '' . $this->t('No blocks in this region') . '', + '#wrapper_attributes' => [ + 'colspan' => 5, + ], + ]; + + /** @var \Drupal\Core\Block\BlockPluginInterface[] $blocks */ + foreach ($blocks as $block_id => $block) { + $row = [ + '#attributes' => [ + 'class' => ['draggable'], + ], + ]; + $row['label']['#markup'] = $block->label(); + $row['id']['#markup'] = $block->getPluginId(); + // Allow the region to be changed for each block. + $row['region'] = [ + '#title' => $this->t('Region'), + '#title_display' => 'invisible', + '#type' => 'select', + '#options' => $variant_plugin->getRegionNames(), + '#default_value' => $variant_plugin->getRegionAssignment($block_id), + '#attributes' => [ + 'class' => ['block-region-select', 'block-region-' . $region], + ], + ]; + // Allow the weight to be changed for each block. + $configuration = $block->getConfiguration(); + $row['weight'] = [ + '#type' => 'weight', + '#default_value' => isset($configuration['weight']) ? $configuration['weight'] : 0, + '#title' => $this->t('Weight for @block block', ['@block' => $block->label()]), + '#title_display' => 'invisible', + '#attributes' => [ + 'class' => ['block-weight', 'block-weight-' . $region], + ], + ]; + // Add the operation links. + $operations = []; + $operations['edit'] = [ + 'title' => $this->t('Edit'), + 'url' => $pattern_plugin->getBlockEditUrl($this->tempstore_id, $machine_name, $block_id, $this->getRequest()->getRequestUri()), + 'attributes' => $attributes, + ]; + $operations['delete'] = [ + 'title' => $this->t('Delete'), + 'url' => $pattern_plugin->getBlockDeleteUrl($this->tempstore_id, $machine_name, $block_id, $this->getRequest()->getRequestUri()), + 'attributes' => $attributes, + ]; + + $row['operations'] = [ + '#type' => 'operations', + '#links' => $operations, + ]; + $form['blocks'][$block_id] = $row; + } + } + } + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $cached_values = $form_state->getTemporaryValue('wizard'); + /** @var \Drupal\page_manager\Plugin\DisplayVariant\PageBlockDisplayVariant $variant_plugin */ + $variant_plugin = $cached_values['plugin']; + + // If the blocks were rearranged, update their values. + if (!$form_state->isValueEmpty('blocks')) { + foreach ($form_state->getValue('blocks') as $block_id => $block_values) { + $variant_plugin->updateBlock($block_id, $block_values); + } + } + // Page Variant title handling. + if ($form_state->hasValue('page_title')) { + $configuration = $variant_plugin->getConfiguration(); + $configuration['page_title'] = $form_state->getValue('page_title'); + $variant_plugin->setConfiguration($configuration); + } + } + +} diff --git a/docroot/modules/contrib/panels/src/Form/PanelsDeleteBlockForm.php b/docroot/modules/contrib/panels/src/Form/PanelsDeleteBlockForm.php new file mode 100644 index 000000000..409900098 --- /dev/null +++ b/docroot/modules/contrib/panels/src/Form/PanelsDeleteBlockForm.php @@ -0,0 +1,130 @@ +tempstore = $tempstore; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('user.shared_tempstore') + ); + } + + /** + * Get the tempstore id. + * + * @return string + */ + protected function getTempstoreId() { + return $this->tempstore_id; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'panels_delete_block_form'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->t('Are you sure you want to delete the block %label?', ['%label' => $this->block->label()]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return $this->getRequest()->attributes->get('destination'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, $tempstore_id = NULL, $machine_name = NULL, $block_id = NULL) { + $this->tempstore_id = $tempstore_id; + $cached_values = $this->getCachedValues($this->tempstore, $tempstore_id, $machine_name); + $this->plugin = $cached_values['plugin']; + $this->block = $this->plugin->getBlock($block_id); + $form['block_display'] = [ + '#type' => 'value', + '#value' => $machine_name, + ]; + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->plugin->removeBlock($this->block->getConfiguration()['uuid']); + $cached_values = $this->getCachedValues($this->tempstore, $this->getTempstoreId(), $form_state->getValue('block_display')); + $cached_values['plugin'] = $this->plugin; + // PageManager specific handling. + if (isset($cached_values['page_variant'])) { + $cached_values['page_variant']->getVariantPlugin()->setConfiguration($cached_values['plugin']->getConfiguration()); + } + $this->tempstore->get($this->getTempstoreId())->set($cached_values['id'], $cached_values); + drupal_set_message($this->t('The block %label has been removed.', ['%label' => $this->block->label()])); + } + +} diff --git a/docroot/modules/contrib/panels/src/Form/PanelsEditBlockForm.php b/docroot/modules/contrib/panels/src/Form/PanelsEditBlockForm.php new file mode 100644 index 000000000..962dc88fe --- /dev/null +++ b/docroot/modules/contrib/panels/src/Form/PanelsEditBlockForm.php @@ -0,0 +1,31 @@ +getVariantPlugin()->getBlock($block_id); + } + + /** + * {@inheritdoc} + */ + protected function submitText() { + return $this->t('Update block'); + } + +} diff --git a/docroot/modules/contrib/panels/src/PanelsDisplayManager.php b/docroot/modules/contrib/panels/src/PanelsDisplayManager.php new file mode 100644 index 000000000..0fd189910 --- /dev/null +++ b/docroot/modules/contrib/panels/src/PanelsDisplayManager.php @@ -0,0 +1,97 @@ +variantManager = $variant_manager; + $this->typedConfigManager = $typed_config_manager; + } + + /** + * {@inheritdoc} + */ + public function createDisplay($layout = NULL, $builder = NULL) { + $display = $this->variantManager->createInstance('panels_variant', []); + + // Set the default builder and layout. + // @todo: load the defaults from config somewhere. + $display->setLayout($layout ?: 'layout_onecol'); + $display->setBuilder($builder ?: 'standard'); + + return $display; + } + + /** + * Validates the config against the schema. + * + * @param array $config + * The configuration data. + * + * @throws \Exception + * If the configuration doesn't validate. + */ + protected function validate(array $config) { + $this->configName = 'display_variant.plugin.panels_variant'; + $definition = $this->typedConfigManager->getDefinition($this->configName); + $data_definition = $this->typedConfigManager->buildDataDefinition($definition, $config); + $this->schema = $this->typedConfigManager->create($data_definition, $config); + $errors = array(); + foreach ($config as $key => $value) { + $errors = array_merge($errors, $this->checkValue($key, $value)); + } + if (!empty($errors)) { + $error_list = []; + foreach ($errors as $key => $error) { + $error_list[] = $key . ': ' . $error; + } + throw new \Exception("Config for Panels display doesn't validate: " . implode(', ', $error_list)); + } + } + + /** + * {@inheritdoc} + */ + public function importDisplay(array $config, $validate = TRUE) { + // Validate against the schema if requested. + if ($validate) { + $this->validate($config); + } + + return $this->variantManager->createInstance('panels_variant', $config); + } + + /** + * {@inheritdoc} + */ + public function exportDisplay(PanelsDisplayVariant $display) { + return $display->getConfiguration(); + } + +} diff --git a/docroot/modules/contrib/panels/src/PanelsDisplayManagerInterface.php b/docroot/modules/contrib/panels/src/PanelsDisplayManagerInterface.php new file mode 100644 index 000000000..ac5951e9f --- /dev/null +++ b/docroot/modules/contrib/panels/src/PanelsDisplayManagerInterface.php @@ -0,0 +1,52 @@ +alterInfo('panels_pattern_info'); + $this->setCacheBackend($cache_backend, 'panels_pattern_plugins'); + + parent::__construct('Plugin/PanelsPattern', $namespaces, $module_handler, 'Drupal\panels\Plugin\PanelsPattern\PanelsPatternInterface', '\Drupal\panels\Annotation\PanelsPattern'); + } + +} diff --git a/docroot/modules/contrib/panels/src/PanelsVariantEvent.php b/docroot/modules/contrib/panels/src/PanelsVariantEvent.php new file mode 100644 index 000000000..104f80345 --- /dev/null +++ b/docroot/modules/contrib/panels/src/PanelsVariantEvent.php @@ -0,0 +1,40 @@ +variant = $variant; + } + + /** + * Returns the Panels display variant that triggered the event. + * + * @return \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant + * The Panels display variant. + */ + public function getVariant() { + return $this->variant; + } + +} diff --git a/docroot/modules/contrib/panels/src/Plugin/DisplayBuilder/DisplayBuilderBase.php b/docroot/modules/contrib/panels/src/Plugin/DisplayBuilder/DisplayBuilderBase.php new file mode 100644 index 000000000..dba63731e --- /dev/null +++ b/docroot/modules/contrib/panels/src/Plugin/DisplayBuilder/DisplayBuilderBase.php @@ -0,0 +1,21 @@ +getRegionAssignments(); + return $regions; + } + +} diff --git a/docroot/modules/contrib/panels/src/Plugin/DisplayBuilder/DisplayBuilderInterface.php b/docroot/modules/contrib/panels/src/Plugin/DisplayBuilder/DisplayBuilderInterface.php new file mode 100644 index 000000000..43b6d1194 --- /dev/null +++ b/docroot/modules/contrib/panels/src/Plugin/DisplayBuilder/DisplayBuilderInterface.php @@ -0,0 +1,28 @@ +setCacheBackend($cache_backend, 'display_builder'); + $this->alterInfo('display_builder'); + } + + public function createInstance($plugin_id, array $configuration = array()) { + // Redirect the deprecated editor builder to use the standard builder. + if ($plugin_id == 'editor') { + return parent::createInstance('standard', $configuration); + } + + return parent::createInstance($plugin_id, $configuration); + } + +} diff --git a/docroot/modules/contrib/panels/src/Plugin/DisplayBuilder/DisplayBuilderManagerInterface.php b/docroot/modules/contrib/panels/src/Plugin/DisplayBuilder/DisplayBuilderManagerInterface.php new file mode 100644 index 000000000..ace9c239b --- /dev/null +++ b/docroot/modules/contrib/panels/src/Plugin/DisplayBuilder/DisplayBuilderManagerInterface.php @@ -0,0 +1,12 @@ +contextHandler = $context_handler; + $this->account = $account; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('context.handler'), + $container->get('current_user') + ); + } + + /** + * Build render arrays for each of the regions. + * + * @param array $regions + * The render array representing regions. + * @param array $contexts + * The array of context objects. + * + * @return array + * An associative array, keyed by region ID, containing the render arrays + * representing the content of each region. + */ + protected function buildRegions(array $regions, array $contexts) { + $build = []; + foreach ($regions as $region => $blocks) { + if (!$blocks) { + continue; + } + + $region_name = Html::getClass("block-region-$region"); + $build[$region]['#prefix'] = '
        '; + $build[$region]['#suffix'] = '
        '; + + /** @var \Drupal\Core\Block\BlockPluginInterface[] $blocks */ + $weight = 0; + foreach ($blocks as $block_id => $block) { + if ($block instanceof ContextAwarePluginInterface) { + $this->contextHandler->applyContextMapping($block, $contexts); + } + if ($block->access($this->account)) { + $block_render_array = [ + '#theme' => 'block', + '#attributes' => [], + '#contextual_links' => [], + '#weight' => $weight++, + '#configuration' => $block->getConfiguration(), + '#plugin_id' => $block->getPluginId(), + '#base_plugin_id' => $block->getBaseId(), + '#derivative_plugin_id' => $block->getDerivativeId(), + ]; + + // Build the block and bubble its attributes up if possible. This + // allows modules like Quickedit to function. + // See \Drupal\block\BlockViewBuilder::preRender() for reference. + $content = $block->build(); + if ($content !== NULL && !Element::isEmpty($content)) { + foreach (['#attributes', '#contextual_links'] as $property) { + if (isset($content[$property])) { + $block_render_array[$property] += $content[$property]; + unset($content[$property]); + } + } + } + + // If the block is empty, instead of trying to render the block + // correctly return just #cache, so that the render system knows the + // reasons (cache contexts & tags) why this block is empty. + if (Element::isEmpty($content)) { + $block_render_array = []; + $cacheable_metadata = CacheableMetadata::createFromObject($block_render_array); + $cacheable_metadata->applyTo($block_render_array); + if (isset($content['#cache'])) { + $block_render_array['#cache'] += $content['#cache']; + } + } + + $block_render_array['content'] = $content; + + $build[$region][$block_id] = $block_render_array; + } + } + } + return $build; + } + + /** + * {@inheritdoc} + */ + public function build(PanelsDisplayVariant $panels_display) { + $regions = $panels_display->getRegionAssignments(); + $contexts = $panels_display->getContexts(); + $layout = $panels_display->getLayout(); + + $regions = $this->buildRegions($regions, $contexts); + if ($layout) { + $regions = $layout->build($regions); + } + return $regions; + } + + /** + * {@inheritdoc} + */ + public function getWizardOperations($cached_values) { + $operations = []; + $operations['content'] = [ + 'title' => $this->t('Content'), + 'form' => PanelsContentForm::class, + ]; + return $operations; + } + +} diff --git a/docroot/modules/contrib/panels/src/Plugin/DisplayVariant/PanelsDisplayVariant.php b/docroot/modules/contrib/panels/src/Plugin/DisplayVariant/PanelsDisplayVariant.php new file mode 100644 index 000000000..08b5ffb28 --- /dev/null +++ b/docroot/modules/contrib/panels/src/Plugin/DisplayVariant/PanelsDisplayVariant.php @@ -0,0 +1,530 @@ +moduleHandler = $module_handler; + $this->builderManager = $builder_manager; + $this->layoutManager = $layout_manager; + + parent::__construct($configuration, $plugin_id, $plugin_definition, $context_handler, $account, $uuid_generator, $token, $block_manager, $condition_manager); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('context.handler'), + $container->get('current_user'), + $container->get('uuid'), + $container->get('token'), + $container->get('plugin.manager.block'), + $container->get('plugin.manager.condition'), + $container->get('module_handler'), + $container->get('plugin.manager.panels.display_builder'), + $container->get('plugin.manager.core.layout') + ); + } + + /** + * Returns the builder assigned to this display variant. + * + * @return \Drupal\panels\Plugin\DisplayBuilder\DisplayBuilderInterface + * A display builder plugin instance. + */ + public function getBuilder() { + if (!isset($this->builder)) { + if (empty($this->configuration['builder'])) { + $this->builder = $this->builderManager->createInstance('standard', []); + } + else { + $this->builder = $this->builderManager->createInstance($this->configuration['builder'], []); + } + } + return $this->builder; + } + + /** + * Assigns a builder to this display variant. + * + * @param string|\Drupal\panels\Plugin\DisplayBuilder\DisplayBuilderInterface $builder + * The builder object or plugin id. + * + * @return $this + * + * @throws \Exception + * If $build isn't a string or DisplayBuilderInterface object. + */ + public function setBuilder($builder) { + if ($builder instanceof DisplayBuilderInterface) { + $this->builder = $builder; + $this->configuration['builder'] = $builder->getPluginId(); + } + elseif (is_string($builder)) { + $this->builder = NULL; + $this->configuration['builder'] = $builder; + } + else { + throw new \Exception("Builder must be a string or DisplayBuilderInterface object"); + } + + return $this; + } + + /** + * Returns instance of the layout plugin used by this page variant. + * + * @return \Drupal\Core\Layout\LayoutInterface + * A layout plugin instance. + */ + public function getLayout() { + if (!isset($this->layout)) { + $this->layout = $this->layoutManager->createInstance($this->configuration['layout'], $this->configuration['layout_settings']); + } + return $this->layout; + } + + /** + * Assigns the layout plugin to this variant. + * + * @param string|\Drupal\Core\Layout\LayoutInterface $layout + * The layout plugin object or plugin id. + * @param array $layout_settings + * The layout configuration. + * + * @return $this + * + * @throws \Exception + * If $layout isn't a string or LayoutInterface object. + */ + public function setLayout($layout, array $layout_settings = []) { + if ($layout instanceof LayoutInterface) { + $this->layout = $layout; + $this->configuration['layout'] = $layout->getPluginId(); + $this->configuration['layout_settings'] = $layout_settings; + } + elseif (is_string($layout)) { + $this->layout = NULL; + $this->configuration['layout'] = $layout; + $this->configuration['layout_settings'] = $layout_settings; + } + else { + throw new \Exception("Layout must be a string or LayoutInterface object"); + } + + return $this; + } + + /** + * Gets the assigned PanelsPattern or falls back to the default pattern. + * + * @return \Drupal\panels\Plugin\PanelsPattern\PanelsPatternInterface + */ + public function getPattern() { + if (!isset($this->pattern)) { + if (empty($this->configuration['pattern'])) { + $this->pattern = \Drupal::service('plugin.manager.panels.pattern')->createInstance('default'); + } + else { + $this->pattern = \Drupal::service('plugin.manager.panels.pattern')->createInstance($this->configuration['pattern']); + } + } + return $this->pattern; + } + + /** + * Assign the pattern for panels content operations and default contexts. + * + * @param mixed string|\Drupal\panels\Plugin\PanelsPattern\PanelsPatternInterface $pattern + * + * @return $this + * + * @throws \Exception + * If $pattern isn't a string or PanelsPatternInterface object. + */ + public function setPattern($pattern) { + if ($pattern instanceof PanelsPatternInterface) { + $this->pattern = $pattern; + $this->configuration['pattern'] = $pattern->getPluginId(); + } + elseif (is_string($pattern)) { + $this->pattern = NULL; + $this->configuration['pattern'] = $pattern; + } + else { + throw new \Exception("Pattern must be a string or PanelsPatternInterface object"); + } + + return $this; + } + + /** + * Configures how this Panel is being stored. + * + * @param string $type + * The storage type used by the storage plugin. + * @param string $id + * The id within the storage plugin for this Panels display. + * + * @return $this + */ + public function setStorage($type, $id) { + $this->configuration['storage_type'] = $type; + $this->configuration['storage_id'] = $id; + return $this; + } + + /** + * Gets the id of the storage plugin which can save this. + * + * @return string|NULL + */ + public function getStorageType() { + return $this->configuration['storage_type'] ?: NULL; + } + + /** + * Gets id within the storage plugin for this Panels display. + * + * @return string|NULL + */ + public function getStorageId() { + return $this->configuration['storage_id'] ?: NULL; + } + + /** + * {@inheritdoc} + */ + public function getRegionNames() { + return $this->getLayout()->getPluginDefinition()->getRegionLabels(); + } + + /** + * Returns the configured page title. + * + * @return string + */ + public function getPageTitle() { + return $this->configuration['page_title']; + } + + /** + * Sets the page title. + * + * @param string $title + * The desired page title. + * + * @return $this + */ + public function setPageTitle($title) { + $this->configuration['page_title'] = $title; + return $this; + } + + /** + * {@inheritdoc} + */ + public function build() { + $build = $this->getBuilder()->build($this); + $build['#title'] = $this->renderPageTitle($this->configuration['page_title']); + + // Allow other module to alter the built panel. + $this->moduleHandler->alter('panels_build', $build, $this); + + return $build; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + // Don't call VariantBase::buildConfigurationForm() on purpose, because it + // adds a 'Label' field that we don't actually want to use - we store the + // label on the page variant entity. + // $form = parent::buildConfigurationForm($form, $form_state);. + $plugins = $this->builderManager->getDefinitions(); + $options = array(); + foreach ($plugins as $id => $plugin) { + $options[$id] = $plugin['label']; + } + // Only allow the IPE if the storage information is set. + if (!$this->getStorageType()) { + unset($options['ipe']); + } + $form['builder'] = [ + '#title' => $this->t('Builder'), + '#type' => 'select', + '#options' => $options, + '#default_value' => !empty($this->configuration['builder']) ? $this->configuration['builder'] : 'standard', + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::submitConfigurationForm($form, $form_state); + + if ($form_state->hasValue('builder')) { + $this->configuration['builder'] = $form_state->getValue('builder'); + } + $this->configuration['page_title'] = $form_state->getValue('page_title'); + } + + /** + * {@inheritdoc} + */ + public function access(AccountInterface $account = NULL) { + // If no blocks are configured for this variant, deny access. + if (empty($this->configuration['blocks'])) { + return FALSE; + } + + return parent::access($account); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return parent::defaultConfiguration() + [ + 'uuid' => $this->uuidGenerator()->generate(), + 'layout' => '', + 'layout_settings' => [], + 'page_title' => '', + 'storage_type' => '', + 'storage_id' => '', + ]; + } + + /** + * {@inheritdoc} + */ + public function getWizardOperations($cached_values) { + $operations = []; + $operations['layout'] = [ + 'title' => $this->t('Layout'), + 'form' => LayoutPluginSelector::class, + ]; + if (!empty($this->getConfiguration()['layout']) && $cached_values['plugin']->getLayout() instanceof PluginFormInterface) { + /** @var \Drupal\Core\Layout\LayoutInterface $layout */ + if (empty($cached_values['layout_change']['new_layout'])) { + $layout = $cached_values['plugin']->getLayout(); + $class = get_class($layout); + } + else { + $layout_definition = \Drupal::service('plugin.manager.core.layout')->getDefinition($cached_values['layout_change']['new_layout']); + $class = $layout_definition->getClass(); + } + // If the layout does not implement + // \Drupal\Core\Plugin\PluginFormInterface there's no reason to include + // the wizard step for displaying that UI. + if (is_subclass_of($class, PluginFormInterface::class)) { + $operations['settings'] = [ + 'title' => $this->t('Layout Settings'), + 'form' => LayoutChangeSettings::class, + ]; + } + } + if (!empty($cached_values['layout_change']['old_layout'])) { + $operations['regions'] = [ + 'title' => $this->t('Layout Regions'), + 'form' => LayoutChangeRegions::class, + ]; + } + /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $plugin */ + $plugin = $cached_values['plugin']; + $builder = $plugin->getBuilder(); + if ($builder instanceof PluginWizardInterface) { + $operations = array_merge($operations, $builder->getWizardOperations($cached_values)); + } + return $operations; + } + + /** + * {@inheritdoc} + */ + public function setConfiguration(array $configuration) { + if (empty($configuration['uuid'])) { + $configuration['uuid'] = $this->uuidGenerator()->generate(); + } + + // Make sure blocks are mapped to valid regions, and if not, map them to the + // first available region. This is a work-around the fact that we're not + // totally in control of the block placement UI from page_manager. + // @todo Replace after https://www.drupal.org/node/2550879 + if (!empty($configuration['layout']) && !empty($configuration['blocks'])) { + $layout_definition = $this->layoutManager->getDefinition($configuration['layout']); + $valid_regions = $layout_definition->getRegions(); + $first_region = $layout_definition->getDefaultRegion(); + foreach ($configuration['blocks'] as &$block) { + if (!isset($valid_regions[$block['region']])) { + $block['region'] = $first_region; + } + } + } + + return parent::setConfiguration($configuration); + } + + /** + * Renders the page title and replaces tokens. + * + * @param string $page_title + * The page title that should be rendered. + * + * @return string + * The page title after replacing any tokens. + */ + protected function renderPageTitle($page_title) { + $data = $this->getContextAsTokenData(); + // Token replace only escapes replacement values, ensure a consistent + // behavior by also escaping the input and then returning it as a Markup + // object to avoid double escaping. + // @todo: Simplify this when core provides an API for this in + // https://www.drupal.org/node/2580723. + $title = (string) $this->token->replace(new HtmlEscapedText($page_title), $data); + return Markup::create($title); + } + + /** + * Returns available context as token data. + * + * @return array + * An array with token data values keyed by token type. + */ + protected function getContextAsTokenData() { + $data = array(); + foreach ($this->getContexts() as $context) { + // @todo Simplify this when token and typed data types are unified in + // https://drupal.org/node/2163027. + if (strpos($context->getContextDefinition()->getDataType(), 'entity:') === 0) { + $token_type = substr($context->getContextDefinition()->getDataType(), 7); + if ($token_type == 'taxonomy_term') { + $token_type = 'term'; + } + $data[$token_type] = $context->getContextValue(); + } + } + return $data; + } + + /** + * Returns the ID to be used to store this in the tempstore. + * + * @return string + */ + public function getTempStoreId() { + $id = [$this->id()]; + foreach ($this->getContexts() as $context) { + if ($context instanceof AutomaticContext && $context->isAutomatic()) { + $id = array_merge($id, $context->getCacheTags()); + } + } + return implode(':', $id); + } + +} diff --git a/docroot/modules/contrib/panels/src/Plugin/PanelsPattern/DefaultPattern.php b/docroot/modules/contrib/panels/src/Plugin/PanelsPattern/DefaultPattern.php new file mode 100644 index 000000000..73f39c3c9 --- /dev/null +++ b/docroot/modules/contrib/panels/src/Plugin/PanelsPattern/DefaultPattern.php @@ -0,0 +1,126 @@ +get('ctools.context_mapper')); + } + + /** + * DefaultPattern constructor. + * + * @param array $configuration + * The plugin's configuration. + * @param string $plugin_id + * The plugin id. + * @param mixed $plugin_definition + * The plugin definition. + * @param \Drupal\ctools\ContextMapperInterface $context_mapper + * The context mapper. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, ContextMapperInterface $context_mapper) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->contextMapper = $context_mapper; + } + + /** + * {@inheritdoc} + */ + public function getMachineName($cached_values) { + // PageManager needs special handling, so lets see if we're dealing with a PM page. + if (isset($cached_values['page_variant'])) { + return implode('--', [$cached_values['id'], $cached_values['page_variant']->id()]); + } + return $cached_values['id']; + } + + /** + * {@inheritdoc} + */ + public function getDefaultContexts(SharedTempStoreFactory $tempstore, $tempstore_id, $machine_name) { + $cached_values = $this->getCachedValues($tempstore, $tempstore_id, $machine_name); + // PageManager specific context loading. + if (!empty($cached_values['page_variant'])) { + /** @var \Drupal\page_manager\PageVariantInterface $page_variant */ + $page_variant = $cached_values['page_variant']; + return $page_variant->getContexts(); + } + // General handling for contexts. + return !empty($cached_values['contexts']) ? $this->contextMapper->getContextValues($cached_values['contexts']) : []; + } + + /** + * {@inheritdoc} + */ + public function getBlockListUrl($tempstore_id, $machine_name, $region = NULL, $destination = NULL) { + return Url::fromRoute('panels.select_block', [ + 'tempstore_id' => $tempstore_id, + 'machine_name' => $machine_name, + 'region' => $region, + 'destination' => $destination, + ]); + } + + /** + * {@inheritdoc} + */ + public function getBlockAddUrl($tempstore_id, $machine_name, $block_id, $region = NULL, $destination = NULL) { + return Url::fromRoute('panels.add_block', [ + 'tempstore_id' => $tempstore_id, + 'machine_name' => $machine_name, + 'block_id' => $block_id, + 'region' => $region, + 'destination' => $destination, + ]); + } + + /** + * {@inheritdoc} + */ + public function getBlockEditUrl($tempstore_id, $machine_name, $block_id, $destination = NULL) { + return Url::fromRoute('panels.edit_block', [ + 'tempstore_id' => $tempstore_id, + 'machine_name' => $machine_name, + 'block_id' => $block_id, + 'destination' => $destination, + ]); + } + + /** + * {@inheritdoc} + */ + public function getBlockDeleteUrl($tempstore_id, $machine_name, $block_id, $destination = NULL) { + return Url::fromRoute('panels.delete_block', [ + 'tempstore_id' => $tempstore_id, + 'machine_name' => $machine_name, + 'block_id' => $block_id, + 'destination' => $destination, + ]); + } + +} diff --git a/docroot/modules/contrib/panels/src/Plugin/PanelsPattern/PanelsPatternInterface.php b/docroot/modules/contrib/panels/src/Plugin/PanelsPattern/PanelsPatternInterface.php new file mode 100644 index 000000000..36012af89 --- /dev/null +++ b/docroot/modules/contrib/panels/src/Plugin/PanelsPattern/PanelsPatternInterface.php @@ -0,0 +1,102 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager') + ); + } + + /** + * Load a page variant entity. + * + * @param string $id + * The page variant entity's id. + * + * @return \Drupal\page_manager\PageVariantInterface + */ + protected function loadPageVariant($id) { + return $this->entityTypeManager->getStorage('page_variant')->load($id); + } + + /** + * {@inheritdoc} + */ + public function save(PanelsDisplayVariant $panels_display) { + $id = $panels_display->getStorageId(); + if ($id && ($page_variant = $this->loadPageVariant($id))) { + $variant_plugin = $page_variant->getVariantPlugin(); + if (!($variant_plugin instanceof PanelsDisplayVariant)) { + throw new \Exception("Page variant doesn't use a Panels display variant"); + } + $variant_plugin->setConfiguration($panels_display->getConfiguration()); + $page_variant->save(); + } + else { + throw new \Exception("Couldn't find page variant to store Panels display"); + } + } + + /** + * {@inheritdoc} + */ + public function load($id) { + if ($page_variant = $this->loadPageVariant($id)) { + $panels_display = $page_variant->getVariantPlugin(); + + // If this page variant doesn't have a Panels display on it, then we treat + // it the same as if there was no such page variant. + if (!($panels_display instanceof PanelsDisplayVariant)) { + return NULL; + } + + // Pass down the contexts because the display has no other way to get them + // from the variant. + $panels_display->setContexts($page_variant->getContexts()); + + return $panels_display; + } + } + + /** + * {@inheritdoc} + */ + public function access($id, $op, AccountInterface $account) { + if ($op == 'change layout') { + $op = 'update'; + } + if ($page_variant = $this->loadPageVariant($id)) { + return $page_variant->access($op, $account, TRUE); + } + + return AccessResult::forbidden(); + } + +} diff --git a/docroot/modules/contrib/panels/src/Storage/PanelsStorageAccess.php b/docroot/modules/contrib/panels/src/Storage/PanelsStorageAccess.php new file mode 100644 index 000000000..bef9e22c1 --- /dev/null +++ b/docroot/modules/contrib/panels/src/Storage/PanelsStorageAccess.php @@ -0,0 +1,52 @@ +panelsStorage = $panels_storage; + } + + /** + * Checks if the user has access to underlying storage for a Panels display. + * + * @param \Symfony\Component\Routing\Route $route + * The route to check against. + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The parametrized route. + * @param \Drupal\Core\Session\AccountInterface $account + * The currently logged in account. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. + */ + public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account) { + $panels_storage_type = $route_match->getParameter('panels_storage_type'); + $panels_storage_id = $route_match->getParameter('panels_storage_id'); + $op = $route->getRequirement('_panels_storage_access'); + return $this->panelsStorage->access($panels_storage_type, $panels_storage_id, $op, $account); + } + +} diff --git a/docroot/modules/contrib/panels/src/Storage/PanelsStorageBase.php b/docroot/modules/contrib/panels/src/Storage/PanelsStorageBase.php new file mode 100644 index 000000000..bd43cdb7a --- /dev/null +++ b/docroot/modules/contrib/panels/src/Storage/PanelsStorageBase.php @@ -0,0 +1,8 @@ +getStorageId() must return + * the display's id as known to this storage plugin. + * + * @throws \Exception + * If the storage information isn't set, or there is no such Panels display. + */ + public function save(PanelsDisplayVariant $panels_display); + + /** + * Checks if the user has access to a Panels display. + * + * @param string $id + * The id for the Panels display within this storage plugin. + * @param string $op + * The operation to perform (create, read, update, delete, change layout). + * If the operation is 'change layout', implementing classes should + * implicitly check the 'update' permission as well. + * @param \Drupal\Core\Session\AccountInterface $account + * The user to check access for. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. If there is no such Panels display then deny access. + */ + public function access($id, $op, AccountInterface $account); + +} diff --git a/docroot/modules/contrib/panels/src/Storage/PanelsStorageManager.php b/docroot/modules/contrib/panels/src/Storage/PanelsStorageManager.php new file mode 100644 index 000000000..8ddaa7172 --- /dev/null +++ b/docroot/modules/contrib/panels/src/Storage/PanelsStorageManager.php @@ -0,0 +1,117 @@ +currentUser = $current_user; + $this->eventDispatcher = $event_dispatcher; + + $this->alterInfo('panels_storage_info'); + $this->setCacheBackend($cache_backend, 'panels_storage'); + } + + /** + * An associative array of Panels storages services keyed by storage type. + * + * @var \Drupal\panels\Storage\PanelsStorageInterface[] + */ + protected $storage = []; + + /** + * Gets a storage plugin. + * + * @param string $storage_type + * The storage type used by the storage plugin. + * + * @return \Drupal\panels\Storage\PanelsStorageInterface + * The Panels storage plugin with the given storage type. + * + * @throws \Drupal\Component\Plugin\Exception\PluginException + * If there is no Panels storage plugin with the given storage type. + */ + protected function getStorage($storage_type) { + if (!isset($this->storage[$storage_type])) { + $this->storage[$storage_type] = $this->createInstance($storage_type); + } + return $this->storage[$storage_type]; + } + + /** + * {@inheritdoc} + */ + public function load($storage_type, $id) { + $storage = $this->getStorage($storage_type); + return $storage->load($id); + } + + /** + * {@inheritdoc} + */ + public function save(PanelsDisplayVariant $panels_display) { + // Allow event subscribers to react to the variant being saved. + $event = new PanelsVariantEvent($panels_display); + + $this->eventDispatcher->dispatch(PanelsEvents::VARIANT_PRE_SAVE, $event); + $storage = $this->getStorage($panels_display->getStorageType()); + $storage->save($panels_display); + $this->eventDispatcher->dispatch(PanelsEvents::VARIANT_POST_SAVE, $event); + } + + /** + * {@inheritdoc} + */ + public function access($storage_type, $id, $op, AccountInterface $account = NULL) { + if ($account === NULL) { + $account = $this->currentUser->getAccount(); + } + return $this->getStorage($storage_type)->access($id, $op, $account); + } + +} diff --git a/docroot/modules/contrib/panels/src/Storage/PanelsStorageManagerInterface.php b/docroot/modules/contrib/panels/src/Storage/PanelsStorageManagerInterface.php new file mode 100644 index 000000000..5a65b7c85 --- /dev/null +++ b/docroot/modules/contrib/panels/src/Storage/PanelsStorageManagerInterface.php @@ -0,0 +1,71 @@ +getStorageType() and + * $panels_display->getStorageId() must return the storage type and id as + * known to the storage plugin. + * + * @throws \Exception + * If $panels->getStorageType() or $panels->getStorageId() aren't set, the + * storage plugin can't be found, or there is no Panels display found in + * the storage plugin with the given id. + */ + public function save(PanelsDisplayVariant $panels_display); + + /** + * Checks if the user has access to underlying storage for a Panels display. + * + * @param string $storage_type + * The storage type used by the storage plugin. + * @param string $id + * The id within the storage plugin for the requested Panels display. + * @param string $op + * The operation to perform (ie. create, read, update, delete). + * @param \Drupal\Core\Session\AccountInterface|NULL $account + * The user to check access for. If omitted, it'll check the curerntly + * logged in user. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. If there is no such Panels display then deny access. + */ + public function access($storage_type, $id, $op, AccountInterface $account = NULL); + +} diff --git a/docroot/modules/contrib/panels/src/Tests/PageManagerPanelsStorageIntegrationTest.php b/docroot/modules/contrib/panels/src/Tests/PageManagerPanelsStorageIntegrationTest.php new file mode 100644 index 000000000..d4014f3b5 --- /dev/null +++ b/docroot/modules/contrib/panels/src/Tests/PageManagerPanelsStorageIntegrationTest.php @@ -0,0 +1,77 @@ +drupalPlaceBlock('local_tasks_block'); + $this->drupalPlaceBlock('local_actions_block'); + $this->drupalPlaceBlock('system_branding_block'); + $this->drupalPlaceBlock('page_title_block'); + + \Drupal::service('theme_handler')->install(['bartik', 'classy']); + $this->config('system.theme')->set('admin', 'classy')->save(); + + $this->drupalLogin($this->drupalCreateUser(['administer pages', 'access administration pages', 'view the administration theme'])); + } + + /** + * Tests creating a Panels variant with the IPE. + */ + public function testPanelsIPE() { + // Create new page. + $this->drupalGet('admin/structure/page_manager/add'); + $edit = [ + 'id' => 'foo', + 'label' => 'foo', + 'path' => 'testing', + 'variant_plugin_id' => 'panels_variant', + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + + // Add a Panels variant which uses the IPE. + $edit = [ + // This option won't be present at all if our integration isn't working! + 'variant_settings[builder]' => 'ipe', + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + + // Choose a layout. + $edit = [ + 'layout' => 'layout_twocol', + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + + // Finish without adding any blocks. + $this->drupalPostForm(NULL, [], 'Finish'); + + /** @var \Drupal\page_manager\PageVariantInterface $page_variant */ + $page_variant = PageVariant::load('foo-panels_variant-0'); + /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $panels_display */ + $panels_display = $page_variant->getVariantPlugin(); + + // Make sure the storage type and id were set to the right value. + $this->assertEqual($panels_display->getStorageType(), 'page_manager'); + $this->assertEqual($panels_display->getStorageId(), 'foo-panels_variant-0'); + } + +} diff --git a/docroot/modules/contrib/panels/src/Tests/PanelsConfigSchemaTest.php b/docroot/modules/contrib/panels/src/Tests/PanelsConfigSchemaTest.php new file mode 100644 index 000000000..980e9aa78 --- /dev/null +++ b/docroot/modules/contrib/panels/src/Tests/PanelsConfigSchemaTest.php @@ -0,0 +1,57 @@ +panelsManager = \Drupal::service('panels.display_manager'); + } + + /** + * Tests whether the Panels display config schema is valid. + */ + public function testPanelsConfigSchema() { + $panels_display = $this->panelsManager->createDisplay(); + + // Add a block. + $panels_display->addBlock([ + 'id' => 'entity_view:node', + 'label' => 'View the node', + 'provider' => 'page_manager', + 'label_display' => 'visible', + 'view_mode' => 'default', + 'region' => 'content', + ]); + + $config = $this->panelsManager->exportDisplay($panels_display); + // This will throw an exception if it doesn't validate. + $new_panels_display = $this->panelsManager->importDisplay($config, TRUE); + + $this->assertEqual($panels_display->getConfiguration(), $new_panels_display->getConfiguration()); + } + +} diff --git a/docroot/modules/contrib/panels/src/Tests/PanelsTest.php b/docroot/modules/contrib/panels/src/Tests/PanelsTest.php new file mode 100644 index 000000000..603bd7303 --- /dev/null +++ b/docroot/modules/contrib/panels/src/Tests/PanelsTest.php @@ -0,0 +1,137 @@ +drupalPlaceBlock('local_tasks_block'); + $this->drupalPlaceBlock('local_actions_block'); + $this->drupalPlaceBlock('system_branding_block'); + $this->drupalPlaceBlock('page_title_block'); + + \Drupal::service('theme_handler')->install(['bartik', 'classy']); + $this->config('system.theme')->set('admin', 'classy')->save(); + + $this->drupalLogin($this->drupalCreateUser(['administer pages', 'access administration pages', 'view the administration theme'])); + } + + /** + * Tests adding a layout with settings. + */ + public function testLayoutSettings() { + // Create new page. + $this->drupalGet('admin/structure/page_manager/add'); + $edit = [ + 'id' => 'foo', + 'label' => 'foo', + 'path' => 'testing', + 'variant_plugin_id' => 'panels_variant', + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + + // Add variant with a layout that has settings. + $edit = [ + 'page_variant_label' => 'Default', + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + + // Choose a layout. + $edit = [ + 'layout' => 'layout_example_test', + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + + // Update the layout's settings. + $this->assertFieldByName('layout_settings_wrapper[layout_settings][setting_1]', 'Default'); + $edit = [ + 'layout_settings_wrapper[layout_settings][setting_1]' => 'Abracadabra', + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + + // Add a block. + $this->clickLink('Add new block'); + $this->clickLink('Powered by Drupal'); + $edit = [ + 'region' => 'top', + ]; + $this->drupalPostForm(NULL, $edit, 'Add block'); + + // Finish the page add wizard. + $this->drupalPostForm(NULL, [], 'Finish'); + + // View the page and make sure the setting is present. + $this->drupalGet('testing'); + $this->assertText('Blah:'); + $this->assertText('Abracadabra'); + $this->assertText('Powered by Drupal'); + } + + /** + * Tests that special characters are not escaped when using tokens in titles. + */ + public function testPageTitle() { + // Change the logged in user's name to include a special character. + $user = User::load($this->loggedInUser->id()); + $user->setUsername("My User's Name"); + $user->save(); + + // Create new page. + $this->drupalGet('admin/structure/page_manager/add'); + $edit = [ + 'id' => 'foo', + 'label' => 'foo', + 'path' => 'testing', + 'variant_plugin_id' => 'panels_variant', + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + + // Use default variant settings. + $edit = [ + 'page_variant_label' => 'Default', + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + + // Choose a simple layout. + $edit = [ + 'layout' => 'layout_onecol', + ]; + $this->drupalPostForm(NULL, $edit, 'Next'); + + // Set the title to a token value that includes an apostrophe. + $edit = [ + 'page_title' => '[user:name]', + ]; + $this->drupalPostForm(NULL, $edit, 'Finish'); + + // View the page and make sure the page title is valid. + $this->drupalGet('testing'); + // We expect "'" to be escaped only once, which is why we're doing a raw + // assertion here. + $this->assertRaw('

        My User's Name

        '); + } + +} diff --git a/docroot/modules/contrib/panels/tests/modules/panels_test/config/schema/layout_plugin_example.schema.yml b/docroot/modules/contrib/panels/tests/modules/panels_test/config/schema/layout_plugin_example.schema.yml new file mode 100644 index 000000000..91df27e72 --- /dev/null +++ b/docroot/modules/contrib/panels/tests/modules/panels_test/config/schema/layout_plugin_example.schema.yml @@ -0,0 +1,7 @@ +layout_plugin.settings.layout_example_test: + type: layout_plugin.settings + label: 'Settings for layout_example_test layout' + mapping: + setting_1: + type: string + label: 'First setting' diff --git a/docroot/modules/contrib/panels/tests/modules/panels_test/panels_test.info.yml b/docroot/modules/contrib/panels/tests/modules/panels_test/panels_test.info.yml new file mode 100644 index 000000000..fb799dd2d --- /dev/null +++ b/docroot/modules/contrib/panels/tests/modules/panels_test/panels_test.info.yml @@ -0,0 +1,14 @@ +type: module +name: Panels Test +description: 'Required for Panels simpletests only.' +# core: 8.x +dependencies: + - page_manager + - panels + - layout_discovery + +# Information added by Drupal.org packaging script on 2017-07-19 +version: '8.x-4.2' +core: '8.x' +project: 'panels' +datestamp: 1500497646 diff --git a/docroot/modules/contrib/panels/tests/modules/panels_test/src/Plugin/Layout/LayoutExampleTest.php b/docroot/modules/contrib/panels/tests/modules/panels_test/src/Plugin/Layout/LayoutExampleTest.php new file mode 100644 index 000000000..05d8c4eb3 --- /dev/null +++ b/docroot/modules/contrib/panels/tests/modules/panels_test/src/Plugin/Layout/LayoutExampleTest.php @@ -0,0 +1,71 @@ + 'Default', + ]; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $configuration = $this->getConfiguration(); + $form['setting_1'] = [ + '#type' => 'textfield', + '#title' => 'Blah', + '#default_value' => $configuration['setting_1'], + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + } + + /** + * @inheritDoc + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + $this->configuration['setting_1'] = $form_state->getValue('setting_1'); + } + +} diff --git a/docroot/modules/contrib/panels/tests/modules/panels_test/templates/layout-example-test.html.twig b/docroot/modules/contrib/panels/tests/modules/panels_test/templates/layout-example-test.html.twig new file mode 100644 index 000000000..3cabd71d9 --- /dev/null +++ b/docroot/modules/contrib/panels/tests/modules/panels_test/templates/layout-example-test.html.twig @@ -0,0 +1,18 @@ +{# +/** + * @file + * Template for layout_example_test layout. + */ +#} +
        +
        + Blah: + {{ settings.setting_1 }} +
        +
        + {{ content.top }} +
        +
        + {{ content.bottom }} +
        +
        diff --git a/docroot/modules/contrib/panels/tests/src/Kernel/PanelsStorageManagerTest.php b/docroot/modules/contrib/panels/tests/src/Kernel/PanelsStorageManagerTest.php new file mode 100644 index 000000000..339100d9b --- /dev/null +++ b/docroot/modules/contrib/panels/tests/src/Kernel/PanelsStorageManagerTest.php @@ -0,0 +1,66 @@ +container->get('event_dispatcher'); + + $event_dispatcher->addListener(PanelsEvents::VARIANT_PRE_SAVE, function (PanelsVariantEvent $event) { + $event->getVariant()->setPageTitle('Gentlefolk, BEHOLD!'); + }); + $event_dispatcher->addListener(PanelsEvents::VARIANT_POST_SAVE, function (PanelsVariantEvent $event) { + $event->getVariant()->setPageTitle('This will be discarded.'); + }); + + $page = Page::create([ + 'id' => $this->randomMachineName(), + 'label' => $this->randomMachineName(), + 'path' => '/' . $this->randomMachineName(), + ]); + $page->save(); + + $variant = PageVariant::create([ + 'id' => 'stunning', + 'label' => $this->randomMachineName(), + 'variant' => 'panels_variant', + 'variant_settings' => [ + 'page_title' => 'Pastafazoul', + 'storage_type' => 'page_manager', + 'storage_id' => $this->randomMachineName(), + 'layout' => 'layout_onecol', + 'layout_settings' => [], + ], + 'page' => $page->id(), + ]); + $variant->save(); + + $this->container->get('panels.storage_manager')->save($variant->getVariantPlugin()); + + // The page title set by the pre-save event handler should be persisted; + // the one set by the post-save handler should be discarded. + $page_title = PageVariant::load('stunning')->getVariantPlugin()->getPageTitle(); + $this->assertSame('Gentlefolk, BEHOLD!', $page_title); + } + +} diff --git a/docroot/modules/contrib/panels/tests/src/Unit/PanelsDisplayVariantTest.php b/docroot/modules/contrib/panels/tests/src/Unit/PanelsDisplayVariantTest.php new file mode 100644 index 000000000..d41ab27b3 --- /dev/null +++ b/docroot/modules/contrib/panels/tests/src/Unit/PanelsDisplayVariantTest.php @@ -0,0 +1,160 @@ +account = $this->prophesize(AccountInterface::class); + $this->contextHandler = $this->prophesize(ContextHandlerInterface::class); + $this->uuidGenerator = $this->prophesize(UuidInterface::class); + $this->token = $this->prophesize(Token::class); + $this->blockManager = $this->prophesize(BlockManager::class); + $this->conditionManager = $this->prophesize(ConditionManager::class); + $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class); + $this->builderManager = $this->prophesize(DisplayBuilderManagerInterface::class); + $this->layoutManager = $this->prophesize(LayoutPluginManagerInterface::class); + $this->layout = $this->prophesize(LayoutInterface::class); + + $this->layoutManager + ->createInstance(Argument::type('string'), Argument::type('array')) + ->willReturn($this->layout->reveal()); + + $this->variant = new PanelsDisplayVariant([], '', [], $this->contextHandler->reveal(), $this->account->reveal(), $this->uuidGenerator->reveal(), $this->token->reveal(), $this->blockManager->reveal(), $this->conditionManager->reveal(), $this->moduleHandler->reveal(), $this->builderManager->reveal(), $this->layoutManager->reveal()); + } + + /** + * @covers ::submitConfigurationForm + */ + public function testSubmitConfigurationForm() { + $values = ['page_title' => "Go hang a salami, I'm a lasagna hog!"]; + + $form = []; + $form_state = (new FormState())->setValues($values); + $this->variant->submitConfigurationForm($form, $form_state); + + $configuration = $this->variant->getConfiguration(); + $this->assertSame($values['page_title'], $configuration['page_title']); + } + + /** + * @covers ::getLayout + */ + public function testGetLayout() { + $this->assertSame($this->layout->reveal(), $this->variant->getLayout()); + } + + /** + * @covers ::getRegionNames + */ + public function testGetRegionNames() { + $region_names = ['foo' => 'Foo', 'bar' => 'Bar', 'baz' => 'Baz']; + $layout_definition = new LayoutDefinition([ + 'regions' => [ + 'foo' => [ + 'label' => 'Foo', + ], + 'bar' => [ + 'label' => 'Bar', + ], + 'baz' => [ + 'label' => 'Baz', + ], + ], + ]); + $this->layout->getPluginDefinition()->willReturn($layout_definition); + $this->assertSame($region_names, $this->variant->getRegionNames()); + } + + /** + * @covers ::access + */ + public function testAccessNoBlocksConfigured() { + $this->assertFalse($this->variant->access()); + } + + /** + * @covers ::defaultConfiguration + */ + public function testDefaultConfiguration() { + $defaults = $this->variant->defaultConfiguration(); + $this->assertSame('', $defaults['layout']); + $this->assertSame('', $defaults['page_title']); + } + +} diff --git a/docroot/modules/contrib/panels/tests/src/Unit/PanelsStorageTest.php b/docroot/modules/contrib/panels/tests/src/Unit/PanelsStorageTest.php new file mode 100644 index 000000000..645df9c2a --- /dev/null +++ b/docroot/modules/contrib/panels/tests/src/Unit/PanelsStorageTest.php @@ -0,0 +1,179 @@ +panelsDisplay = $this->prophesize(PanelsDisplayVariant::class); + + $this->pageVariant = $this->prophesize(PageVariantInterface::class); + $this->pageVariant->getVariantPlugin()->willReturn($this->panelsDisplay->reveal()); + + $this->pageVariantNotPanels = $this->prophesize(PageVariantInterface::class); + $this->pageVariantNotPanels->getContexts()->shouldNotBeCalled(); + + $non_panels_variant = $this->prophesize(HttpStatusCodeDisplayVariant::class); + $this->pageVariantNotPanels->getVariantPlugin()->willReturn($non_panels_variant->reveal()); + + $this->storage = $this->prophesize(EntityStorageInterface::class); + + $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); + $this->entityTypeManager->getStorage('page_variant')->willReturn($this->storage->reveal()); + } + + /** + * @covers ::load + */ + public function testLoad() { + // Make sure that the contexts are passed down (or not). + $this->pageVariant->getContexts()->willReturn([]); + $this->panelsDisplay->setContexts([])->shouldBeCalledTimes(1); + + $this->storage->load('id_exists')->willReturn($this->pageVariant->reveal()); + $this->storage->load('doesnt_exist')->willReturn(NULL); + $this->storage->load('not_a_panel')->willReturn($this->pageVariantNotPanels->reveal()); + + $panels_storage = new PageManagerPanelsStorage([], '', [], $this->entityTypeManager->reveal()); + + // Test the success condition. + $this->assertSame($this->panelsDisplay->reveal(), $panels_storage->load('id_exists')); + + // Should be NULL if it doesn't exist. + $this->assertNull($panels_storage->load('doesnt_exist')); + + // Should also be NULL if it's not a PanelsDisplayVariant. + $this->assertNull($panels_storage->load('not_a_panel')); + } + + /** + * @covers ::save + */ + public function testSaveSuccessful() { + $test_config = ['my_config' => '123']; + $this->panelsDisplay->setConfiguration($test_config)->shouldBeCalledTimes(1); + $this->pageVariant->save()->shouldBeCalledTimes(1); + + $this->storage->load('id_exists')->willReturn($this->pageVariant->reveal()); + + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $panels_display->getStorageId()->willReturn('id_exists'); + $panels_display->getConfiguration()->willReturn($test_config); + + $panels_storage = new PageManagerPanelsStorage([], '', [], $this->entityTypeManager->reveal()); + $panels_storage->save($panels_display->reveal()); + } + + /** + * @covers ::save + * + * @expectedException \Exception + * @expectedExceptionMessage Couldn't find page variant to store Panels display + */ + public function testSaveDoesntExist() { + $this->panelsDisplay->setConfiguration()->shouldNotBeCalled(); + $this->pageVariant->save()->shouldNotBeCalled(); + + $this->storage->load('doesnt_exist')->willReturn(NULL); + + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $panels_display->getStorageId()->willReturn('doesnt_exist'); + $panels_display->getConfiguration()->shouldNotBeCalled(); + + $panels_storage = new PageManagerPanelsStorage([], '', [], $this->entityTypeManager->reveal()); + $panels_storage->save($panels_display->reveal()); + } + + /** + * @covers ::save + * + * @expectedException \Exception + * @expectedExceptionMessage Page variant doesn't use a Panels display variant + */ + public function testSaveNotPanels() { + $this->storage->load('not_a_panel')->willReturn($this->pageVariantNotPanels->reveal()); + + $this->panelsDisplay->setConfiguration(Argument::cetera())->shouldNotBeCalled(); + $this->pageVariant->save()->shouldNotBeCalled(); + + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $panels_display->getStorageId()->willReturn('not_a_panel'); + $panels_display->getConfiguration()->shouldNotBeCalled(); + + $panels_storage = new PageManagerPanelsStorage([], '', [], $this->entityTypeManager->reveal()); + $panels_storage->save($panels_display->reveal()); + } + + /** + * @covers ::access + */ + public function testAccess() { + $this->storage->load('id_exists')->willReturn($this->pageVariant->reveal()); + $this->storage->load('doesnt_exist')->willReturn(NULL); + + $account = $this->prophesize(AccountInterface::class); + + $this->pageVariant->access('read', $account->reveal(), TRUE)->willReturn(AccessResult::allowed()); + + $panels_storage = new PageManagerPanelsStorage([], '', [], $this->entityTypeManager->reveal()); + + // Test the access condition. + $this->assertEquals(AccessResult::allowed(), $panels_storage->access('id_exists', 'read', $account->reveal())); + + // Should be forbidden if it doesn't exist. + $this->assertEquals(AccessResult::forbidden(), $panels_storage->access('doesnt_exist', 'read', $account->reveal())); + + // Test that 'change layout' becomes 'update'. + $this->pageVariant->access('update', $account->reveal(), TRUE)->willReturn(AccessResult::allowed()); + $this->assertEquals(AccessResult::allowed(), $panels_storage->access('id_exists', 'change layout', $account->reveal())); + } + +} diff --git a/docroot/modules/contrib/panels/tests/src/Unit/StandardDisplayBuilderTest.php b/docroot/modules/contrib/panels/tests/src/Unit/StandardDisplayBuilderTest.php new file mode 100644 index 000000000..572d80b5f --- /dev/null +++ b/docroot/modules/contrib/panels/tests/src/Unit/StandardDisplayBuilderTest.php @@ -0,0 +1,87 @@ +prophesize(ContextHandlerInterface::class) + ->reveal(); + $account = $this->prophesize(AccountInterface::class) + ->reveal(); + $this->builder = new StandardDisplayBuilder(array(), 'standard', array(), $context_handler, $account); + } + + /** + * @covers ::build + */ + public function testBuild() { + $regions = array(); + + $block = $this->prophesize(BlockPluginInterface::class); + $block->access(Argument::type(AccountInterface::class)) + ->willReturn(TRUE); + $block->getConfiguration()->willReturn([]); + $block->getPluginId()->willReturn('foo'); + $block->getBaseId()->willReturn('foo'); + $block->getDerivativeId()->willReturn('foo'); + $block->build()->willReturn(['#markup' => 'Foo!']); + $regions['content']['foo'] = $block->reveal(); + + $block = $this->prophesize(BlockPluginInterface::class); + $block->access(Argument::type(AccountInterface::class)) + ->willReturn(TRUE); + $block->getConfiguration()->willReturn([]); + $block->getPluginId()->willReturn('bar'); + $block->getBaseId()->willReturn('bar'); + $block->getDerivativeId()->willReturn('bar'); + $block->build()->willReturn(['#markup' => 'Bar...']); + $regions['sidebar']['bar'] = $block->reveal(); + + $block = $this->prophesize(BlockPluginInterface::class); + $block->access(Argument::type(AccountInterface::class)) + ->willReturn(FALSE); + $regions['sidebar']['baz'] = $block->reveal(); + + $regions['footer'] = array(); + + $panels_display = $this->prophesize(PanelsDisplayVariant::class); + $panels_display->getRegionAssignments()->willReturn($regions); + $panels_display->getContexts()->willReturn([]); + $panels_display->getLayout()->willReturn(NULL); + + $build = $this->builder->build($panels_display->reveal()); + // Ensure that regions get the proper prefix and suffix. + $this->assertEquals('
        ', $build['content']['#prefix']); + $this->assertEquals('
        ', $build['content']['#suffix']); + + // Ensure that blocks which allowed access showed up... + $this->assertEquals('Foo!', $build['content']['foo']['content']['#markup']); + $this->assertEquals('Bar...', $build['sidebar']['bar']['content']['#markup']); + // ...and that blocks which disallowed access did not. + $this->assertArrayNotHasKey('baz', $build['sidebar']); + // Ensure that empty regions don't show up in $build. + $this->assertArrayNotHasKey('footer', $build); + } + +} diff --git a/docroot/modules/contrib/panels/tests/src/Unit/panels_ipe/RemoveBlockRequestHandlerTest.php b/docroot/modules/contrib/panels/tests/src/Unit/panels_ipe/RemoveBlockRequestHandlerTest.php new file mode 100644 index 000000000..9eda8c7f1 --- /dev/null +++ b/docroot/modules/contrib/panels/tests/src/Unit/panels_ipe/RemoveBlockRequestHandlerTest.php @@ -0,0 +1,45 @@ +sut = new RemoveBlockRequestHandler($this->moduleHandler, $this->panelsStore, $this->tempStore); + } + + /** + * @test + */ + public function removeBlockRequestRemovesTheBlock() { + $this->panelsDisplay->expects($this->once())->method('removeBlock'); + $this->sut->handleRequest($this->panelsDisplay, $this->createRequest('someblock')); + $this->assertEquals(new JsonResponse([]), $this->sut->getJsonResponse()); + } + + /** + * @test + */ + public function panelsDisplayIsSavedAfterBlockRemoval() { + $this->panelsStore->expects($this->once())->method('save'); + $this->sut->handleRequest($this->panelsDisplay, $this->createRequest('someblock')); + } + + /** + * @test + */ + public function panelsDisplayIsSavedToTempstoreAfterBlockRemoval() { + $this->tempStore->expects($this->once())->method('set'); + $this->sut->handleRequest($this->panelsDisplay, $this->createRequest('someblock'), TRUE); + } + +} diff --git a/docroot/modules/contrib/panels/tests/src/Unit/panels_ipe/RequestHandlerTestBase.php b/docroot/modules/contrib/panels/tests/src/Unit/panels_ipe/RequestHandlerTestBase.php new file mode 100644 index 000000000..5f7de8c16 --- /dev/null +++ b/docroot/modules/contrib/panels/tests/src/Unit/panels_ipe/RequestHandlerTestBase.php @@ -0,0 +1,69 @@ +moduleHandler = $this->getMockForAbstractClass(ModuleHandlerInterface::class); + $this->panelsStore = $this->getMockForAbstractClass(PanelsStorageManagerInterface::class); + $this->tempStore = $this->getMockBuilder(SharedTempstore::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->panelsDisplay = $this->getMockBuilder(PanelsDisplayVariant::class) + ->disableOriginalConstructor() + ->getMock(); + } + + protected function createRequest($content = NULL) { + return new Request([], [], [], [], [], [], $content); + } + + /** + * @test + */ + public function emptyRequestResultsInFailedResponse() { + $this->sut->handleRequest($this->panelsDisplay, $this->createRequest()); + + $expected = new JsonResponse(['success' => FALSE], 400); + $this->assertEquals($expected, $this->sut->getJsonResponse()); + } + +} diff --git a/docroot/modules/contrib/panels/tests/src/Unit/panels_ipe/UpdateLayoutRequestHandlerTest.php b/docroot/modules/contrib/panels/tests/src/Unit/panels_ipe/UpdateLayoutRequestHandlerTest.php new file mode 100644 index 000000000..a068468f7 --- /dev/null +++ b/docroot/modules/contrib/panels/tests/src/Unit/panels_ipe/UpdateLayoutRequestHandlerTest.php @@ -0,0 +1,99 @@ +sut = new UpdateLayoutRequestHandler($this->moduleHandler, $this->panelsStore, $this->tempStore); + } + + private function getLayoutModel() { + return [ + 'regionCollection' => [ + [ + 'name' => 'some_region', + 'blockCollection' => [ + ['uuid' => 'someBlock'], + ['uuid' => 'someOtherBlock'], + ], + ], + ], + ]; + } + + private function setPanelsDisplayExpectations() { + $block = $this->getMockBuilder(BlockBase::class) + ->disableOriginalConstructor() + ->getMock(); + $block->expects($this->exactly(4))->method('setConfigurationValue'); + $block->expects($this->exactly(2)) + ->method('getConfiguration') + ->willReturn([]); + + $this->panelsDisplay->method('getBlock') + ->willReturn($block); + } + + /** + * @test + */ + public function successfulSaveOperationResultsInEmptyJsonResponse() { + $this->setPanelsDisplayExpectations(); + $this->sut->handleRequest($this->panelsDisplay, $this->createRequest(Json::encode($this->getLayoutModel()))); + $this->assertEquals(new JsonResponse([]), $this->sut->getJsonResponse()); + } + + /** + * @test + */ + public function successfulTempStoreSaveOperationResultsInEmptyJsonResponse() { + $this->setPanelsDisplayExpectations(); + $this->sut->handleRequest($this->panelsDisplay, $this->createRequest(Json::encode($this->getLayoutModel())), TRUE); + $this->assertEquals(new JsonResponse([]), $this->sut->getJsonResponse()); + } + + /** + * @test + */ + public function updatedLayoutGetsSaved() { + $this->setPanelsDisplayExpectations(); + $this->panelsStore->expects($this->once())->method('save'); + $this->tempStore->expects($this->once())->method('delete'); + $this->tempStore->expects($this->never())->method('set'); + + $this->sut->handleRequest($this->panelsDisplay, $this->createRequest(Json::encode($this->getLayoutModel()))); + } + + /** + * @test + */ + public function updatedLayoutGetsSavedToTempStore() { + $this->setPanelsDisplayExpectations(); + $this->panelsStore->expects($this->never())->method('save'); + $this->tempStore->expects($this->never())->method('delete'); + $this->tempStore->expects($this->once())->method('set'); + $this->sut->handleRequest($this->panelsDisplay, $this->createRequest(Json::encode($this->getLayoutModel())), TRUE); + } + + /** + * @test + */ + public function hookPreSaveGetsCalledBeforeSave() { + $this->setPanelsDisplayExpectations(); + $this->moduleHandler->expects($this->once())->method('invokeAll'); + $this->sut->handleRequest($this->panelsDisplay, $this->createRequest(Json::encode($this->getLayoutModel())), TRUE); + } + +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 40ba908c0..ff9ef7f1e 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -8762,5 +8762,384 @@ "source": "http://cgit.drupalcode.org/age_field_formatter", "issues": "https://www.drupal.org/project/issues/age_field_formatter" } + }, + { + "name": "drupal/panels", + "version": "4.2.0", + "version_normalized": "4.2.0.0", + "source": { + "type": "git", + "url": "https://git.drupal.org/project/panels", + "reference": "8.x-4.2" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/panels-8.x-4.2.zip", + "reference": "8.x-4.2", + "shasum": "6991377531eafaec09f9c7a31e091592e455137b" + }, + "require": { + "drupal/core": "^8.3", + "drupal/ctools": ">=3.0.0" + }, + "require-dev": { + "drupal/page_manager": "*" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-4.x": "4.x-dev" + }, + "drupal": { + "version": "8.x-4.2", + "datestamp": "1500497642", + "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+" + ], + "authors": [ + { + "name": "Jakob Perry", + "homepage": "https://www.drupal.org/u/japerry" + }, + { + "name": "Samuel Mortenson", + "homepage": "https://www.drupal.org/u/samuel.mortenson" + }, + { + "name": "See other contributors", + "homepage": "https://www.drupal.org/node/74958/committers" + }, + { + "name": "japerry", + "homepage": "https://www.drupal.org/user/45640" + }, + { + "name": "merlinofchaos", + "homepage": "https://www.drupal.org/user/26979" + }, + { + "name": "neclimdul", + "homepage": "https://www.drupal.org/user/48673" + }, + { + "name": "samuel.mortenson", + "homepage": "https://www.drupal.org/user/2582268" + }, + { + "name": "sdboyer", + "homepage": "https://www.drupal.org/user/146719" + }, + { + "name": "tim.plunkett", + "homepage": "https://www.drupal.org/user/241634" + } + ], + "description": "Core Panels display functions; provides no external UI, at least one other Panels module should be enabled.", + "homepage": "https://www.drupal.org/project/panels", + "support": { + "source": "http://git.drupal.org/project/panels.git", + "issues": "https://www.drupal.org/project/issues/panels", + "irc": "irc://irc.freenode.org/drupal-scotch" + } + }, + { + "name": "drupal/panels_ipe", + "version": "4.2.0", + "version_normalized": "4.2.0.0", + "require": { + "drupal/core": "*", + "drupal/panels": "self.version" + }, + "type": "metapackage", + "extra": { + "branch-alias": { + "dev-4.x": "4.x-dev" + }, + "drupal": { + "version": "8.x-4.2", + "datestamp": "1500497642", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "EclipseGc", + "homepage": "https://www.drupal.org/user/61203" + }, + { + "name": "Letharion", + "homepage": "https://www.drupal.org/user/373603" + }, + { + "name": "esmerel", + "homepage": "https://www.drupal.org/user/164022" + }, + { + "name": "japerry", + "homepage": "https://www.drupal.org/user/45640" + }, + { + "name": "merlinofchaos", + "homepage": "https://www.drupal.org/user/26979" + }, + { + "name": "neclimdul", + "homepage": "https://www.drupal.org/user/48673" + }, + { + "name": "samuel.mortenson", + "homepage": "https://www.drupal.org/user/2582268" + }, + { + "name": "sdboyer", + "homepage": "https://www.drupal.org/user/146719" + }, + { + "name": "tim.plunkett", + "homepage": "https://www.drupal.org/user/241634" + } + ], + "description": "Panels In-place editor.", + "homepage": "https://www.drupal.org/project/panels", + "support": { + "source": "http://cgit.drupalcode.org/panels" + } + }, + { + "name": "drupal/ctools_block", + "version": "3.0.0", + "version_normalized": "3.0.0.0", + "require": { + "drupal/core": "~8.0", + "drupal/ctools": "self.version" + }, + "type": "metapackage", + "extra": { + "branch-alias": { + "dev-3.x": "3.x-dev" + }, + "drupal": { + "version": "8.x-3.0", + "datestamp": "1493401742", + "security-coverage": { + "status": "covered", + "message": "Covered by Drupal's security advisory policy" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "EclipseGc", + "homepage": "https://www.drupal.org/user/61203" + }, + { + "name": "damiankloip", + "homepage": "https://www.drupal.org/user/1037976" + }, + { + "name": "dawehner", + "homepage": "https://www.drupal.org/user/99340" + }, + { + "name": "esmerel", + "homepage": "https://www.drupal.org/user/164022" + }, + { + "name": "japerry", + "homepage": "https://www.drupal.org/user/45640" + }, + { + "name": "merlinofchaos", + "homepage": "https://www.drupal.org/user/26979" + }, + { + "name": "neclimdul", + "homepage": "https://www.drupal.org/user/48673" + }, + { + "name": "sdboyer", + "homepage": "https://www.drupal.org/user/146719" + }, + { + "name": "sun", + "homepage": "https://www.drupal.org/user/54136" + }, + { + "name": "tim.plunkett", + "homepage": "https://www.drupal.org/user/241634" + } + ], + "description": "Provides improvements to blocks that will one day be added to Drupal core.", + "homepage": "https://www.drupal.org/project/ctools", + "support": { + "source": "http://cgit.drupalcode.org/ctools" + } + }, + { + "name": "drupal/panelizer", + "version": "4.0.0", + "version_normalized": "4.0.0.0", + "source": { + "type": "git", + "url": "https://git.drupal.org/project/panelizer", + "reference": "8.x-4.0" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/panelizer-8.x-4.0.zip", + "reference": "8.x-4.0", + "shasum": "8913d1b782d3f0e48ac957ce1f6d3bc611d524c1" + }, + "require": { + "drupal/core": "*", + "drupal/ctools": ">=3.0.0-beta1", + "drupal/ctools_block": "*", + "drupal/panels": ">=4.0.0-alpha1", + "drupal/panels_ipe": ">=4.0.0-alpha1" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-4.x": "4.x-dev" + }, + "drupal": { + "version": "8.x-4.0", + "datestamp": "1493427125", + "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+" + ], + "authors": [ + { + "name": "Damien McKenna", + "homepage": "https://www.drupal.org/u/damienmckenna" + }, + { + "name": "Kris Vanderwater", + "homepage": "https://www.drupal.org/u/eclipsegc" + }, + { + "name": "David Snopek", + "homepage": "https://www.drupal.org/u/dsnopek" + }, + { + "name": "Jakob Perry", + "homepage": "https://www.drupal.org/u/japerry" + }, + { + "name": "Earl Miles", + "homepage": "https://www.drupal.org/u/merlinofchaos" + }, + { + "name": "See other contributors", + "homepage": "https://www.drupal.org/node/1072922/committers" + }, + { + "name": "tim.plunkett", + "homepage": "https://www.drupal.org/user/241634" + } + ], + "description": "Allow any entity view mode to be rendered using a Panels display.", + "homepage": "https://www.drupal.org/project/panelizer", + "support": { + "source": "http://git.drupal.org/project/panelizer.git", + "issues": "https://www.drupal.org/project/issues/panelizer", + "irc": "irc://irc.freenode.org/drupal-scotch" + } + }, + { + "name": "drupal/page_manager", + "version": "4.0.0-beta2", + "version_normalized": "4.0.0.0-beta2", + "source": { + "type": "git", + "url": "https://git.drupal.org/project/page_manager", + "reference": "8.x-4.0-beta2" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/page_manager-8.x-4.0-beta2.zip", + "reference": "8.x-4.0-beta2", + "shasum": "29a4dda0f068b5df5971eb8319c675cd8e5c78b3" + }, + "require": { + "drupal/core": "^8.0.5", + "drupal/ctools": "~3" + }, + "type": "drupal-module", + "extra": { + "branch-alias": { + "dev-4.x": "4.x-dev" + }, + "drupal": { + "version": "8.x-4.0-beta2", + "datestamp": "1493410443", + "security-coverage": { + "status": "not-covered", + "message": "Project has not opted into security advisory coverage!" + } + }, + "patches_applied": { + "#2876880 - page_variant entity type does not exist when installing or enabling": "https://www.drupal.org/files/issues/2876880-page-varient-cache-2.patch", + "#2868216 - Page variants cannot be selected": "https://www.drupal.org/files/issues/page_manager-page_variants_selection-2868216-7.patch" + } + }, + "installation-source": "dist", + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "GPL-2.0+" + ], + "authors": [ + { + "name": "Tim Plunkett", + "homepage": "https://www.drupal.org/u/tim.plunkett", + "role": "Maintainer" + }, + { + "name": "dsnopek", + "homepage": "https://www.drupal.org/user/266527" + }, + { + "name": "japerry", + "homepage": "https://www.drupal.org/user/45640" + }, + { + "name": "tim.plunkett", + "homepage": "https://www.drupal.org/user/241634" + } + ], + "description": "Provides a way to place blocks on a custom page.", + "homepage": "https://www.drupal.org/project/page_manager", + "support": { + "source": "https://git.drupal.org/project/page_manager.git", + "issues": "https://www.drupal.org/project/issues/page_manager", + "irc": "irc://irc.freenode.org/drupal-contribute" + } } ]