diff --git a/profiles/markaspot/modules/mark_a_spot/modules/markaspot_logic/markaspot_logic.module b/profiles/markaspot/modules/mark_a_spot/modules/markaspot_logic/markaspot_logic.module index c9bfb7d..8293910 100755 --- a/profiles/markaspot/modules/mark_a_spot/modules/markaspot_logic/markaspot_logic.module +++ b/profiles/markaspot/modules/mark_a_spot/modules/markaspot_logic/markaspot_logic.module @@ -18,6 +18,7 @@ function markaspot_logic_form_report_node_form_alter(&$form, &$form_state, $form $form['field_e_mail'][LANGUAGE_NONE][0]['value']['#default_value'] = $user->mail; } $form['#validate'][] = 'markaspot_logic_validate'; + $form['revision_information']['#access'] = FALSE; } /** diff --git a/profiles/markaspot/themes/ntxuva/js/ntxuva.js b/profiles/markaspot/themes/ntxuva/js/ntxuva.js index 3caf067..a888ea5 100644 --- a/profiles/markaspot/themes/ntxuva/js/ntxuva.js +++ b/profiles/markaspot/themes/ntxuva/js/ntxuva.js @@ -834,6 +834,10 @@ var lang_pt = { $('.node-report-form #edit-submit').html(Drupal.t('Next')); + if (window.location.pathname.substr(window.location.pathname.length - 4) == 'edit') { + $('.node-report-form #edit-submit').html(Drupal.t('Save')); + } + var currentHash = document.location.hash.replace(/^#/, ''); if (currentHash) { $('.nav-tabs a[href=#' + currentHash + ']').tab('show'); @@ -852,7 +856,7 @@ var lang_pt = { } ); - if (linkHash.indexOf('4--') > -1 || linkHash.indexOf('---foto') > -1)  { + if (linkHash.indexOf('4--') > -1 || linkHash.indexOf('---foto') > -1 || window.location.pathname.substr(window.location.pathname.length - 4) == 'edit')  { $('.node-report-form #edit-submit').html(Drupal.t('Save')); } else { @@ -865,8 +869,8 @@ var lang_pt = { currentHash = document.location.hash.replace(/^#/, ''); e.preventDefault(); - if (!currentHash || currentHash.indexOf('1--') > -1 || currentHash.indexOf('---local') > -1) { - $('a:contains(2.)').tab('show'); + if ((!currentHash || currentHash.indexOf('1--') > -1 || currentHash.indexOf('---local') > -1) && !(window.location.pathname.substr(window.location.pathname.length - 4) == 'edit')) { + $('a:contains(2.)').tab('show'); var hash = $('a:contains(2.)').attr('href'); // animate $('html, body').animate({ @@ -887,7 +891,7 @@ var lang_pt = { }); document.getElementById("edit-field-geo-und-0-address-field").disabled = false; } - else if (currentHash.indexOf('3--') > -1 || currentHash.indexOf('---contacto') > -1) { + else if (currentHash.indexOf('3--') > -1 || currentHash.indexOf('---contacto') > -1) { $('a:contains(4.)').tab('show'); var hash = $('a:contains(4.)').attr('href'); @@ -901,7 +905,7 @@ var lang_pt = { $('#edit-submit').html(Drupal.t('Save')); } - else if (currentHash.indexOf('4--') > -1 || currentHash.indexOf('---foto') > -1) { + else if (currentHash.indexOf('4--') > -1 || currentHash.indexOf('---foto') > -1 || window.location.pathname.substr(window.location.pathname.length - 4) == 'edit' ) { $('form').unbind('submit').submit(); } }); diff --git a/profiles/markaspot/themes/ntxuva/template.php b/profiles/markaspot/themes/ntxuva/template.php index ff12d19..143e611 100755 --- a/profiles/markaspot/themes/ntxuva/template.php +++ b/profiles/markaspot/themes/ntxuva/template.php @@ -32,7 +32,6 @@ function ntxuva_preprocess_html(&$variables) { ); drupal_add_html_head($ie_render_engine, 'meta_ie_render_engine'); - } diff --git a/profiles/markaspot/themes/ntxuva/templates/node--report--edit.tpl.php b/profiles/markaspot/themes/ntxuva/templates/node--report--edit.tpl.php new file mode 100755 index 0000000..36bc969 --- /dev/null +++ b/profiles/markaspot/themes/ntxuva/templates/node--report--edit.tpl.php @@ -0,0 +1,85 @@ + +
> + + field_category[LANGUAGE_NONE][0]['taxonomy_term']['tid']); + $cat_tid = $node->field_category[LANGUAGE_NONE][0]['tid']; + $status_tid = $node->field_status[LANGUAGE_NONE][0]['tid']; + $category = taxonomy_term_load($cat_tid); + $status = taxonomy_term_load($status_tid); + ?> +
+
+
+ + + > + + + + + +
+ name?> name ?> + field_address_id[LANGUAGE_NONE][0]['value'])): ?> + Ponto: + field_address_id[LANGUAGE_NONE][0]['value']); + $json_a = json_decode($string, true); + if ($json_a) { + print $json_a[0]['location_name']; + } + ?> + + + +
+
+ + + Morada: + + +
+
+ +
+
+ + + + + +
+
+ + + + +
+
+ + + +
+
+
diff --git a/profiles/markaspot/themes/ntxuva/templates/page--inqueritos.tpl.old.php b/profiles/markaspot/themes/ntxuva/templates/page--inqueritos.tpl.old.php deleted file mode 100644 index ff1007e..0000000 --- a/profiles/markaspot/themes/ntxuva/templates/page--inqueritos.tpl.old.php +++ /dev/null @@ -1,116 +0,0 @@ - - - - -
-
-
- -

Inquéritos

-
-
-
- -
-
-
- - -
-
- - -
-
- - -
-
-
-
- -
-
-
-
-
-
-
- - - - - - - - - - - - - - - -
Bairro/RotaMensagemSimNãoData
-
-
-
-
- \ No newline at end of file diff --git a/profiles/markaspot/themes/ntxuva/templates/page--report--edit.tpl.php b/profiles/markaspot/themes/ntxuva/templates/page--report--edit.tpl.php new file mode 100755 index 0000000..36bc969 --- /dev/null +++ b/profiles/markaspot/themes/ntxuva/templates/page--report--edit.tpl.php @@ -0,0 +1,85 @@ + +
> + + field_category[LANGUAGE_NONE][0]['taxonomy_term']['tid']); + $cat_tid = $node->field_category[LANGUAGE_NONE][0]['tid']; + $status_tid = $node->field_status[LANGUAGE_NONE][0]['tid']; + $category = taxonomy_term_load($cat_tid); + $status = taxonomy_term_load($status_tid); + ?> +
+
+
+ + + > + + + + + +
+ name?> name ?> + field_address_id[LANGUAGE_NONE][0]['value'])): ?> + Ponto: + field_address_id[LANGUAGE_NONE][0]['value']); + $json_a = json_decode($string, true); + if ($json_a) { + print $json_a[0]['location_name']; + } + ?> + + + +
+
+ + + Morada: + + +
+
+ +
+
+ + + + + +
+
+ + + + +
+
+ + + +
+
+
diff --git a/sites/all/modules/better_exposed_filters/LICENSE.txt b/sites/all/modules/better_exposed_filters/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/sites/all/modules/better_exposed_filters/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/sites/all/modules/better_exposed_filters/README.txt b/sites/all/modules/better_exposed_filters/README.txt new file mode 100644 index 0000000..0fd4c13 --- /dev/null +++ b/sites/all/modules/better_exposed_filters/README.txt @@ -0,0 +1,41 @@ + +-- SUMMARY -- + +The Better Exposed Filters module replaces the Views' default single- +or multi-select boxes with radio buttons or checkboxes, respectively. + +Views Filters are a powerful tool to limit the results of a given view. +When you expose a filter, you allow the user to interact with the view +making it easy to build a customized advanced search. For example, +exposing a taxonomy filter lets your site visitor search for articles +with specific tags. Better Exposed Filters gives you greater control +over the rendering of exposed filters. + +For a full description of the module, visit the project page: + https://drupal.org/project/better_exposed_filters + +To submit bug reports and feature suggestions, or to track changes: + https://drupal.org/project/issues/better_exposed_filters + +For more information on Views filters, see the Advanced Help documentation +that comes with Views or visit the online version: + https://api.drupal.org/api/views/7 + + +-- REQUIREMENTS -- + +This module requires the Views module: + https://drupal.org/project/views + + +-- DOCUMENTATION -- + +See: + https://drupal.org/node/766974 + + +-- CONTACT -- + +The maintainer for this project is Mike Keran, known on drupal.org as mikeker +(https://drupal.org/user/192273). He can be contacted for work on this module or +other custom projects. diff --git a/sites/all/modules/better_exposed_filters/better_exposed_filters.api.php b/sites/all/modules/better_exposed_filters/better_exposed_filters.api.php new file mode 100644 index 0000000..9148519 --- /dev/null +++ b/sites/all/modules/better_exposed_filters/better_exposed_filters.api.php @@ -0,0 +1,35 @@ +display as $display) { + if (!empty($display->display_options['exposed_form']['type']) && 'better_exposed_filters' == $display->display_options['exposed_form']['type']) { + $warnings[] = t('The %display_title display in the %view_name view. (Update this display)', + array( + '%display_title' => $display->display_title, + '%view_name' => $view->human_name, + '@link' => url('admin/structure/views/view/' . $view->name . '/edit/' . $display->id), + ) + ); + } + } + } + + if (!empty($warnings)) { + $message = t('The following Views displays are using the Better Exposed Filters plugin, which is no longer enabled. It is recommended that you update these displays (links open in a new window) before removing the code associated with this module. Not doing so may cause unexpected results.'); + $message .= '
  • ' . join('
  • ', $warnings) . '
'; + drupal_set_message($message, 'warning'); + } +} + +/* + * Implementations of hook_update_N. + * + * Comments above each function appear in the update database message. + */ +/** + * Rebuild the theme registry to discover new theme_bef_checkbox() function. + */ +function better_exposed_filters_update_7000() { + drupal_theme_rebuild(); + return t('Theme registry has been rebuilt.'); +} diff --git a/sites/all/modules/better_exposed_filters/better_exposed_filters.js b/sites/all/modules/better_exposed_filters/better_exposed_filters.js new file mode 100644 index 0000000..40da193 --- /dev/null +++ b/sites/all/modules/better_exposed_filters/better_exposed_filters.js @@ -0,0 +1,448 @@ +/** + * @file better_exposed_filters.js + * + * Provides some client-side functionality for the Better Exposed Filters module + */ +(function ($) { + Drupal.behaviors.betterExposedFilters = { + attach: function(context) { + // Add highlight class to checked checkboxes for better theming + $('.bef-tree input[type=checkbox], .bef-checkboxes input[type=checkbox]') + // Highlight newly selected checkboxes + .change(function() { + _bef_highlight(this, context); + }) + .filter(':checked').closest('.form-item', context).addClass('highlight') + ; + } + }; + + Drupal.behaviors.betterExposedFiltersSelectAllNone = { + attach: function(context) { + + /* + * Add Select all/none links to specified checkboxes + */ + var selected = $('.form-checkboxes.bef-select-all-none:not(.bef-processed)'); + if (selected.length) { + var selAll = Drupal.t('Select All'); + var selNone = Drupal.t('Select None'); + + // Set up a prototype link and event handlers + var link = $(''+ selAll +'') + link.click(function(event) { + // Don't actually follow the link... + event.preventDefault(); + event.stopPropagation(); + + if (selAll == $(this).text()) { + // Select all the checkboxes + $(this) + .html(selNone) + .siblings('.bef-checkboxes, .bef-tree') + .find('.form-item input:checkbox').each(function() { + $(this).attr('checked', true); + _bef_highlight(this, context); + }) + .end() + + // attr() doesn't trigger a change event, so we do it ourselves. But just on + // one checkbox otherwise we have many spinning cursors + .find('input[type=checkbox]:first').change() + ; + } + else { + // Unselect all the checkboxes + $(this) + .html(selAll) + .siblings('.bef-checkboxes, .bef-tree') + .find('.form-item input:checkbox').each(function() { + $(this).attr('checked', false); + _bef_highlight(this, context); + }) + .end() + + // attr() doesn't trigger a change event, so we do it ourselves. But just on + // one checkbox otherwise we have many spinning cursors + .find('input[type=checkbox]:first').change() + ; + } + }); + + // Add link to the page for each set of checkboxes. + selected + .addClass('bef-processed') + .each(function(index) { + // Clone the link prototype and insert into the DOM + var newLink = link.clone(true); + + newLink.insertBefore($('.bef-checkboxes, .bef-tree', this)); + + // If all checkboxes are already checked by default then switch to Select None + if ($('input:checkbox:checked', this).length == $('input:checkbox', this).length) { + newLink.click(); + } + }) + ; + } + + // Check for and initialize datepickers + var befSettings = Drupal.settings.better_exposed_filters; + if (befSettings && befSettings.datepicker && befSettings.datepicker_options && $.fn.datepicker) { + var opt = []; + $.each(befSettings.datepicker_options, function(key, val) { + if (key && val) { + opt[key] = JSON.parse(val); + } + }); + $('.bef-datepicker').datepicker(opt); + } + + } // attach: function() { + }; // Drupal.behaviors.better_exposed_filters = { + + Drupal.behaviors.betterExposedFiltersAllNoneNested = { + attach:function (context, settings) { + $('.form-checkboxes.bef-select-all-none-nested li').has('ul').once('bef-all-none-nested', function () { + $(this) + // To respect term depth, check/uncheck child term checkboxes. + .find('input.form-checkboxes:first') + .click(function() { + var checkedParent = $(this).attr('checked'); + if (!checkedParent) { + // Uncheck all children if parent is unchecked. + $(this).parents('li:first').find('ul input.form-checkboxes').removeAttr('checked'); + } + else { + // Check all children if parent is checked. + $(this).parents('li:first').find('ul input.form-checkboxes').attr('checked', $(this).attr('checked')); + } + }) + .end() + // When a child term is checked or unchecked, set the parent term's + // status. + .find('ul input.form-checkboxes') + .click(function() { + var checked = $(this).attr('checked'); + + // Determine the number of unchecked sibling checkboxes. + var ct = $(this).parents('ul:first').find('input.form-checkboxes:not(:checked)').size(); + + // If the child term is unchecked, uncheck the parent. + if (!checked) { + // Uncheck parent if any of the childres is unchecked. + $(this).parents('li:first').parents('li:first').find('input.form-checkboxes:first').removeAttr('checked'); + } + + // If all sibling terms are checked, check the parent. + if (!ct) { + // Check the parent if all the children are checked. + $(this).parents('li:first').parents('li:first').find('input.form-checkboxes:first').attr('checked', checked); + } + }); + }); + } + }; + + Drupal.behaviors.better_exposed_filters_slider = { + attach: function(context, settings) { + var befSettings = settings.better_exposed_filters; + if (befSettings && befSettings.slider && befSettings.slider_options) { + $.each(befSettings.slider_options, function(i, sliderOptions) { + var containing_parent = "#" + sliderOptions.viewId + " #edit-" + sliderOptions.id + "-wrapper .views-widget"; + var $filter = $(containing_parent); + + // If the filter is placed in a secondary fieldset, we may not have + // the usual wrapper element. + if (!$filter.length) { + containing_parent = "#" + sliderOptions.viewId + " .bef-slider-wrapper"; + $filter = $(containing_parent); + } + + // Only make one slider per filter. + $filter.once('slider-filter', function() { + var $input = $(this).find('input[type=text]'); + + // This is a "between" or "not between" filter with two values. + if ($input.length == 2) { + var $min = $input.parent().find('input#edit-' + sliderOptions.id + '-min'), + $max = $input.parent().find('input#edit-' + sliderOptions.id + '-max'), + default_min, + default_max; + + if (!$min.length || !$max.length) { + return; + } + + // Get the default values. + // We use slider min & max if there are no defaults. + default_min = parseFloat(($min.val() == '') ? sliderOptions.min : $min.val(), 10); + default_max = parseFloat(($max.val() == '') ? sliderOptions.max : $max.val(), 10); + // Set the element value in case we are using the slider min & max. + $min.val(default_min); + $max.val(default_max); + + $min.parents(containing_parent).after( + $('
').slider({ + range: true, + min: parseFloat(sliderOptions.min, 10), + max: parseFloat(sliderOptions.max, 10), + step: parseFloat(sliderOptions.step, 10), + animate: sliderOptions.animate ? sliderOptions.animate : false, + orientation: sliderOptions.orientation, + values: [default_min, default_max], + // Update the textfields as the sliders are moved + slide: function (event, ui) { + $min.val(ui.values[0]); + $max.val(ui.values[1]); + }, + // This fires when the value is set programmatically or the + // stop event fires. + // This takes care of the case that a user enters a value + // into the text field that is not a valid step of the slider. + // In that case the slider will go to the nearest step and + // this change event will update the text area. + change: function (event, ui) { + $min.val(ui.values[0]); + $max.val(ui.values[1]); + }, + // Attach stop listeners. + stop: function(event, ui) { + // Click the auto submit button. + $(this).parents('form').find('.ctools-auto-submit-click').click(); + } + }) + ); + + // Update the slider when the fields are updated. + $min.blur(function() { + befUpdateSlider($(this), 0, sliderOptions); + }); + $max.blur(function() { + befUpdateSlider($(this), 1, sliderOptions); + }); + } + // This is single value filter. + else if ($input.length == 1) { + if ($input.attr('id') != 'edit-' + sliderOptions.id) { + return; + } + + // Get the default value. We use slider min if there is no default. + var default_value = parseFloat(($input.val() == '') ? sliderOptions.min : $input.val(), 10); + // Set the element value in case we are using the slider min. + $input.val(default_value); + + $input.parents(containing_parent).after( + $('
').slider({ + min: parseFloat(sliderOptions.min, 10), + max: parseFloat(sliderOptions.max, 10), + step: parseFloat(sliderOptions.step, 10), + animate: sliderOptions.animate ? sliderOptions.animate : false, + orientation: sliderOptions.orientation, + value: default_value, + // Update the textfields as the sliders are moved. + slide: function (event, ui) { + $input.val(ui.value); + }, + // This fires when the value is set programmatically or the + // stop event fires. + // This takes care of the case that a user enters a value + // into the text field that is not a valid step of the slider. + // In that case the slider will go to the nearest step and + // this change event will update the text area. + change: function (event, ui) { + $input.val(ui.value); + }, + // Attach stop listeners. + stop: function(event, ui) { + // Click the auto submit button. + $(this).parents('form').find('.ctools-auto-submit-click').click(); + } + }) + ); + + // Update the slider when the field is updated. + $input.blur(function() { + befUpdateSlider($(this), null, sliderOptions); + }); + } + else { + return; + } + }) + }); + } + } + }; + + // This is only needed to provide ajax functionality + Drupal.behaviors.better_exposed_filters_select_as_links = { + attach: function(context, settings) { + + $('.bef-select-as-links', context).once(function() { + var $element = $(this); + + // Check if ajax submission is enabled. If it's not enabled then we + // don't need to attach our custom submission handling, because the + // links are already properly built. + + // First check if any ajax views are contained in the current page. + if (typeof settings.views == 'undefined' || typeof settings.views.ajaxViews == 'undefined') { + return; + } + + // Now check that the view for which the current filter block is used, + // is part of the configured ajax views. + var $uses_ajax = false; + $.each(settings.views.ajaxViews, function(i, item) { + var $view_name = item.view_name.replace(/_/g, '-'); + var $view_display_id = item.view_display_id.replace(/_/g, '-'); + var $id = 'views-exposed-form-' + $view_name + '-' + $view_display_id; + var $form_id = $element.parents('form').attr('id'); + if ($form_id == $id) { + $uses_ajax = true; + return; + } + }); + + // If no ajax is used for form submission, we quit here. + if (!$uses_ajax) { + return; + } + + // Attach selection toggle and form submit on click to each link. + $(this).find('a').click(function(event) { + var $wrapper = $(this).parents('.bef-select-as-links'); + var $options = $wrapper.find('select option'); + // We have to prevent the page load triggered by the links. + event.preventDefault(); + event.stopPropagation(); + // Un select old select value. + $wrapper.find('select option').removeAttr('selected'); + + // Set the corresponding option inside the select element as selected. + var link_text = $(this).text(); + $selected = $options.filter(function() { + return $(this).text() == link_text; + }); + $selected.attr('selected', 'selected'); + $wrapper.find('.bef-new-value').val($selected.val()); + $wrapper.find('a').removeClass('active'); + $(this).addClass('active'); + // Submit the form. + $wrapper.parents('form').find('.views-submit-button *[type=submit]').click(); + }); + }); + } + }; + + Drupal.behaviors.betterExposedFiltersRequiredFilter = { + attach: function(context, settings) { + // Required checkboxes should re-check all inputs if a user un-checks + // them all. + $('.bef-select-as-checkboxes', context).once('bef-required-filter').ajaxComplete(function (e, xhr, s) { + var $element = $(this); + + if (typeof settings.views == 'undefined' || typeof settings.views.ajaxViews == 'undefined') { + return; + } + + // Now check that the view for which the current filter block is used, + // is part of the configured ajax views. + var $view_name; + var $view_display_id; + var $uses_ajax = false; + $.each(settings.views.ajaxViews, function(i, item) { + $view_name = item.view_name; + $view_display_id = item.view_display_id; + var $id = 'views-exposed-form-' + $view_name.replace(/_/g, '-') + '-' + $view_display_id.replace(/_/g, '-'); + var $form_id = $element.parents('form').attr('id'); + if ($form_id == $id) { + $uses_ajax = true; + return false; + } + }); + + //Check if we have any filters at all because of Views Selective Filter + if($('input', this).length > 0) { + var $filter_name = $('input', this).attr('name').slice(0, -2); + if (Drupal.settings.better_exposed_filters.views[$view_name].displays[$view_display_id].filters[$filter_name].required && $('input:checked', this).length == 0) { + $('input', this).prop('checked', true); + } + } + }); + } + } + + /* + * Helper functions + */ + + /** + * Adds/Removes the highlight class from the form-item div as appropriate + */ + function _bef_highlight(elem, context) { + $elem = $(elem, context); + $elem.attr('checked') + ? $elem.closest('.form-item', context).addClass('highlight') + : $elem.closest('.form-item', context).removeClass('highlight'); + } + + /** + * Update a slider when a related input element is changed. + * + * We don't need to check whether the new value is valid based on slider min, + * max, and step because the slider will do that automatically and then we + * update the textfield on the slider's change event. + * + * We still have to make sure that the min & max values of a range slider + * don't pass each other though, however once this jQuery UI bug is fixed we + * won't have to. - http://bugs.jqueryui.com/ticket/3762 + * + * @param $el + * A jQuery object of the updated element. + * @param valIndex + * The index of the value for a range slider or null for a non-range slider. + * @param sliderOptions + * The options for the current slider. + */ + function befUpdateSlider($el, valIndex, sliderOptions) { + var val = parseFloat($el.val(), 10), + currentMin = $el.parents('div.views-widget').next('.bef-slider').slider('values', 0), + currentMax = $el.parents('div.views-widget').next('.bef-slider').slider('values', 1); + // If we have a range slider. + if (valIndex != null) { + // Make sure the min is not more than the current max value. + if (valIndex == 0 && val > currentMax) { + val = currentMax; + } + // Make sure the max is not more than the current max value. + if (valIndex == 1 && val < currentMin) { + val = currentMin; + } + // If the number is invalid, go back to the last value. + if (isNaN(val)) { + val = $el.parents('div.views-widget').next('.bef-slider').slider('values', valIndex); + } + } + else { + // If the number is invalid, go back to the last value. + if (isNaN(val)) { + val = $el.parents('div.views-widget').next('.bef-slider').slider('value'); + } + } + // Make sure we are a number again. + val = parseFloat(val, 10); + // Set the slider to the new value. + // The slider's change event will then update the textfield again so that + // they both have the same value. + if (valIndex != null) { + $el.parents('div.views-widget').next('.bef-slider').slider('values', valIndex, val); + } + else { + $el.parents('div.views-widget').next('.bef-slider').slider('value', val); + } + } + +}) (jQuery); diff --git a/sites/all/modules/better_exposed_filters/better_exposed_filters.module b/sites/all/modules/better_exposed_filters/better_exposed_filters.module new file mode 100644 index 0000000..7d93748 --- /dev/null +++ b/sites/all/modules/better_exposed_filters/better_exposed_filters.module @@ -0,0 +1,206 @@ + array( + 'function' => 'theme_select_as_checkboxes', + 'render element' => 'element', + 'file' => 'better_exposed_filters.theme', + ), + 'select_as_checkboxes_fieldset' => array( + 'function' => 'theme_select_as_checkboxes_fieldset', + 'render element' => 'element', + 'file' => 'better_exposed_filters.theme', + ), + 'select_as_radios' => array( + 'function' => 'theme_select_as_radios', + 'render element' => 'element', + 'file' => 'better_exposed_filters.theme', + ), + 'select_as_radios_fieldset' => array( + 'function' => 'theme_select_as_radios_fieldset', + 'render element' => 'element', + 'file' => 'better_exposed_filters.theme', + ), + 'select_as_hidden' => array( + 'function' => 'theme_select_as_hidden', + 'render element' => 'element', + 'file' => 'better_exposed_filters.theme', + ), + 'select_as_tree' => array( + 'function' => 'theme_select_as_tree', + 'render element' => 'element', + 'file' => 'better_exposed_filters.theme', + ), + 'select_as_links' => array( + 'function' => 'theme_select_as_links', + 'render element' => 'element', + 'file' => 'better_exposed_filters.theme', + ), + 'secondary_exposed_elements' => array( + 'function' => 'theme_secondary_exposed_elements', + 'render element' => 'element', + 'file' => 'better_exposed_filters.theme', + ), + 'bef_checkbox' => array( + 'function' => 'theme_bef_checkbox', + 'render element' => 'element', + 'file' => 'better_exposed_filters.theme', + ), + ); +} + +/* + * Views3 support + * + * Views3 adds the concept of exposed forms to the mix. In addition, elements + * injected into a Views dialog is no longer saved along with the Views form + * information (see the unpack_options() and option_definition() methods of the + * views_object object). + */ + +/** + * Implements hook_views_api(). + */ +function better_exposed_filters_views_api() { + return array( + 'api' => 3.0, + ); +} + +/** + * Unpacks sort_by and sort_order from the sort_bef_combine element. + */ +function bef_sort_combine_submit($form, &$form_state) { + // Same default as better_exposed_filters_exposed_form_plugin::options_form. + $combine_param = empty($form_state['#combine_param']) ? $form_state['complete form']['#info']['sort-sort_bef_combine']['value'] : $form_state['#combine_param']; + if (empty($form_state['values'][$combine_param])) { + $form_state['values']['sort_by'] = $form_state['values']['sort_order'] = ''; + } + else { + list($form_state['values']['sort_by'], $form_state['values']['sort_order']) = explode(' ', $form_state['values'][$combine_param]); + } + + // And pass this along to Views. + views_exposed_form_submit($form, $form_state); +} + +/** + * Form element validation handler for BEF jQuery slider required fields. + */ +function better_exposed_filters_element_validate_slider_required($element, &$form_state) { + $value = $element['#value']; + // If a jQuery slider format has been selected make sure the min & max value + // fields are not empty. + if ($value == '' && _better_exposed_filters_slider_selected($element, $form_state)) { + form_error($element, t('!name field is required.', array('!name' => $element['#title']))); + } +} + +/** + * Form element validation handler for BEF jQuery slider animate setting. + */ +function better_exposed_filters_element_validate_slider_animate($element, &$form_state) { + $value = $element['#value']; + if ($value !== '' && _better_exposed_filters_slider_selected($element, $form_state) && + ((!is_numeric($value) || intval($value) != $value || $value <= 0) && + !in_array($value, array('slow', 'normal', 'fast')))) { + form_error($element, t('%name must be "slow", "normal", "fast" or the number of milliseconds to run the animation (e.g. 1000).', array('%name' => $element['#title']))); + } +} + +/** + * Form element validation handler for BEF jQuery slider min and max settings. + * + * The max value must be greater than the min value. + */ +function better_exposed_filters_element_validate_slider_min_max($element, &$form_state) { + $value = $element['#value']; + $slider_min = $form_state['values']['exposed_form_options']['bef'][$element['#bef_filter_id']]['slider_options']['bef_slider_min']; + $slider_max = $form_state['values']['exposed_form_options']['bef'][$element['#bef_filter_id']]['slider_options']['bef_slider_max']; + + if ($value !== '' && _better_exposed_filters_slider_selected($element, $form_state)) { + // Must not have more than 11 decimal places. + if (_better_exposed_filters_get_num_decimal_places($value) > 11) { + form_error($element, t('%name must not have more than 11 decimal places.', array('%name' => $element['#title']))); + } + // The slider min must be less than the slider max. + if (is_numeric($slider_min) && is_numeric($slider_max) && ($slider_max <= $slider_min)) { + form_error($element, t('The Range minimum value must be less than the Range maximum value.')); + } + } +} + +/** + * Form element validation handler for BEF jQuery slider step setting. + * + * The full specified value range of the slider (range maximum - range minimum) + * should be evenly divisible by the step. + */ +function better_exposed_filters_element_validate_slider_step($element, &$form_state) { + $value = $element['#value']; + + if ($value !== '' && _better_exposed_filters_slider_selected($element, $form_state)) { + $slider_min = $form_state['values']['exposed_form_options']['bef'][$element['#bef_filter_id']]['slider_options']['bef_slider_min']; + $slider_max = $form_state['values']['exposed_form_options']['bef'][$element['#bef_filter_id']]['slider_options']['bef_slider_max']; + + // Must be positive. + if ($value < 0) { + form_error($element, t('%name must be a positive number.', array('%name' => $element['#title']))); + } + // Must not have more than 5 decimal places. + if (_better_exposed_filters_get_num_decimal_places($value) > 5) { + form_error($element, t('%name must not have more than 5 decimal places.', array('%name' => $element['#title']))); + } + // The slider range must be evenly divisible by the step. + // We check like this because of the issues PHP has with inaccurate floats, + // where 2 might actually be 1.9999999999. + // Because of this we can't reliably use fmod(). + if (is_numeric($slider_min) && is_numeric($slider_max) && !ctype_digit((string) abs(($slider_max - $slider_min) / $value))) { + form_error($element, t('The range of the slider (Range maximum - Range minimum) should be evenly divisible by the step.')); + } + } +} + +/** + * Return whether or not the slider has been selected for the given filter. + */ +function _better_exposed_filters_slider_selected($element, &$form_state) { + return (isset($element['#bef_filter_id']) && + isset($form_state['values']['exposed_form_options']['bef'][$element['#bef_filter_id']]['bef_format']) && + $form_state['values']['exposed_form_options']['bef'][$element['#bef_filter_id']]['bef_format'] == 'bef_slider'); +} + +/** + * Return the number of decimal places of the given number. + */ +function _better_exposed_filters_get_num_decimal_places($number) { + $str = (string) $number; + return strlen(substr(strrchr($str, '.'), 1)); +} + +/** + * Implements hook_preprocess_views_view(). + */ +function better_exposed_filters_preprocess_views_view(&$variables) { + $filters = array(); + foreach ($variables['view']->filter as $filter) { + if ($filter->options['exposed']) { + $identifier = !empty($filter->options['is_grouped']) ? $filter->options['group_info']['identifier'] : $filter->options['expose']['identifier']; + $filters[$identifier] = array( + 'required' => $filter->options['expose']['required'] ? TRUE : FALSE, + ); + } + } + + $bef_js['views'][$variables['view']->name]['displays'][$variables['view']->current_display]['filters'] = $filters; + drupal_add_js(array('better_exposed_filters' => $bef_js), 'setting'); +} diff --git a/sites/all/modules/better_exposed_filters/better_exposed_filters.theme b/sites/all/modules/better_exposed_filters/better_exposed_filters.theme new file mode 100644 index 0000000..98e7cd8 --- /dev/null +++ b/sites/all/modules/better_exposed_filters/better_exposed_filters.theme @@ -0,0 +1,763 @@ + '', + '#bef_description' => '', + '#bef_operator' => array(), + ), + $vars['element'] + ); + + $fieldset = array( + '#title' => $element['#bef_title'], + '#description' => $element['#bef_description'], + '#attributes' => array( + 'class' => array( + 'bef-select-as-checkboxes-fieldset', + 'collapsible', + ), + ), + ); + if (empty($element['#value'])) { + // Using the FAPI #collapsible and #collapsed attribute doesn't work here + // TODO: not sure why... + $fieldset['#attributes']['class'][] = 'collapsed'; + } + + // We rendered the description as part of the fieldset element, don't render + // it again along with the checkboxes. + unset($element['#bef_description']); + + $children = ''; + if (!empty($element['#bef_operator'])) { + // Put an exposed operator inside the fieldset. + $children = drupal_render($element['#bef_operator']); + } + + // Render the checkboxes. + $children .= theme('select_as_checkboxes', array('element' => $element)); + + $fieldset['#children'] = $children; + return theme('fieldset', array('element' => $fieldset)); +} + +/** + * Themes a select element as a set of checkboxes. + * + * @see http://api.drupal.org/api/function/theme_select/7 + * + * @param array $vars + * An array of arrays, the 'element' item holds the properties of the element. + * + * @return string + * HTML representing the form element. + */ +function theme_select_as_checkboxes($vars) { + $element = $vars['element']; + if (!empty($element['#bef_nested'])) { + if (empty($element['#attributes']['class'])) { + $element['#attributes']['class'] = array(); + } + $element['#attributes']['class'][] = 'form-checkboxes'; + return theme('select_as_tree', array('element' => $element)); + } + + // The selected keys from #options. + $selected_options = empty($element['#value']) ? (empty($element['#default_value']) ? array() : $element['#default_value']) : $element['#value']; + if (!is_array($selected_options)) { + $selected_options = array($selected_options); + } + + // Grab exposed filter description. We'll put it under the label where it + // makes more sense. + $description = ''; + if (!empty($element['#bef_description'])) { + $description = '
' . $element['#bef_description'] . '
'; + } + + $output = '
'; + foreach ($element['#options'] as $option => $elem) { + if ('All' === $option) { + // TODO: 'All' text is customizable in Views. + // No need for an 'All' option -- either unchecking or checking all the + // checkboxes is equivalent. + continue; + } + + // Check for Taxonomy-based filters. + if (is_object($elem)) { + $slice = array_slice($elem->option, 0, 1, TRUE); + list($option, $elem) = each($slice); + } + + // Check for optgroups. Put subelements in the $element_set array and add + // a group heading. Otherwise, just add the element to the set. + $element_set = array(); + $is_optgroup = FALSE; + if (is_array($elem)) { + $output .= '
'; + $output .= '
' . $option . '
'; + $output .= '
'; + $element_set = $elem; + $is_optgroup = TRUE; + } + else { + $element_set[$option] = $elem; + } + + foreach ($element_set as $key => $value) { + $output .= theme('bef_checkbox', array('element' => $element, 'value' => $key, 'label' => $value, 'selected' => array_search($key, $selected_options) !== FALSE)); + } + + if ($is_optgroup) { + // Close group and item
s. + $output .= '
'; + } + + } + $output .= '
'; + + // Fake theme_checkboxes() which we can't call because it calls + // theme_form_element() for each option. + $attributes['class'] = array('form-checkboxes', 'bef-select-as-checkboxes'); + if (!empty($element['#bef_select_all_none'])) { + $attributes['class'][] = 'bef-select-all-none'; + } + if (!empty($element['#bef_select_all_none_nested'])) { + $attributes['class'][] = 'bef-select-all-none-nested'; + } + if (!empty($element['#attributes']['class'])) { + $attributes['class'] = array_merge($element['#attributes']['class'], $attributes['class']); + } + + return '$description$output
"; +} + +/** + * Themes a select element as a series of hidden fields. + * + * @see http://api.drupal.org/api/function/theme_select/7 + * + * @param array $vars + * An array of arrays, the 'element' item holds the properties of the element. + * + * @return string + * HTML representing the form element. + */ +function theme_select_as_hidden($vars) { + $element = $vars['element']; + $output = ''; + $selected_options = empty($element['#value']) ? $element['#default_value'] : $element['#value']; + $properties = array( + 'title' => isset($element['#title']) ? $element['#title'] : '', + 'description' => isset($element['#bef_description']) ? $element['#bef_description'] : '', + 'required' => FALSE, + ); + + foreach ($element['#options'] as $option => $elem) { + // Check for Taxonomy-based filters. + if (is_object($elem)) { + $slice = array_slice($elem->option, 0, 1, TRUE); + list($option, $elem) = each($slice); + } + + // Check for optgroups. Put subelements in the $element_set array and add a + // group heading. Otherwise, just add the element to the set. + $element_set = array(); + if (is_array($elem)) { + $element_set = $elem; + } + else { + $element_set[$option] = $elem; + } + + foreach ($element_set as $key => $value) { + // Only render fields for selected values -- no selected values renders + // zero fields. + if (array_search($key, $selected_options) !== FALSE) { + // Custom ID for each hidden field based on the 's original ID. + $id = drupal_html_id($element['#id'] . '-' . $key); + $elem = array( + '#id' => $id, + '#markup' => '', + '#type' => 'bef-link', + '#name' => $id, + ); + + $link_options = array(); + // Add "active" class to the currently active filter link. + if (in_array((string) $key, $selected_options)) { + $link_options['attributes'] = array('class' => array('active')); + } + $url = bef_replace_query_string_arg($name, $key, $multiple, FALSE, $path); + $elem['#children'] = l($value, $url, $link_options); + $element_output = theme('form_element', array('element' => $elem)); + + if (!empty($element['#settings']['combine_param']) && $element['#name'] == $element['#settings']['combine_param'] && !empty($element['#settings']['toggle_links'])) { + $sort_pair = explode(' ', $key); + if (count($sort_pair) == 2) { + // Highlight the link if it is the selected sort_by (can be either + // asc or desc, it doesn't matter). + if (strpos($selected_options[0], $sort_pair[0]) === 0) { + $element_output = str_replace('form-item', 'form-item selected', $element_output); + } + } + } + $output .= $element_output; + + } + } + + $properties = array( + '#description' => isset($element['#bef_description']) ? $element['#bef_description'] : '', + '#children' => $output, + ); + + $output = ''; + + return $output; +} + +/** + * Themes some exposed form elements in a collapsible fieldset. + * + * @param array $vars + * An array of arrays, the 'element' item holds the properties of the element. + * + * @return string + * HTML to render the form element. + */ +function theme_secondary_exposed_elements($vars) { + $element = $vars['element']; + + // Render child elements in the order they would appear as exposed filters. + // First collect the elements that have a specified position and order them + // based on that position. Then render those without a position. + $children = array(); + $unordered = array(); + foreach (element_children($element) as $id) { + if (isset($element[$id]['#bef_position'])) { + $children[$element[$id]['#bef_position']] = $element[$id]; + } + else { + $unordered[] = $element[$id]; + } + } + ksort($children, SORT_NUMERIC); + $children = array_merge($children, $unordered); + + $output = '
'; + foreach ($children as $child) { + $output .= drupal_render($child); + } + $output .= '
'; + + return $output; +} + +/* + * + * Helper functions + * + */ + +/** + * Build a BEF checkbox. + * + * @see http://api.drupal.org/api/function/theme_checkbox/7 + * + * @param array $element + * Original 's original ID. + $properties = array( + '#required' => FALSE, + '#id' => $id, + '#type' => 'bef-checkbox', + '#name' => $id, + '#description' => isset($element['#bef_term_descriptions'][$value]) ? $element['#bef_term_descriptions'][$value] : + '', + ); + + // Prevent the select-all-none class from cascading to all checkboxes. + if (!empty($element['#attributes']['class']) + && FALSE !== ($key = array_search('bef-select-all-none', $element['#attributes']['class']))) { + unset($element['#attributes']['class'][$key]); + } + + // Unset the name attribute as we are setting it manually. + unset($element['#attributes']['name']); + + // Unset the multiple attribute as it doesn't apply for checkboxes. + unset ($element['#attributes']['multiple']); + + $checkbox = ''; + $properties['#children'] = "$checkbox "; + $output = theme('form_element', array('element' => $properties)); + return $output; +} + +/** + * Replaces/adds a given query string argument to the current URL. + * + * @param string $key + * Query string key (argument). + * @param string $value + * Query string value. + * @param bool $multiple + * (optional) TRUE if this key/value pair allows multiple values. + * @param bool $remove + * (optional) TRUE if this key/value should be a link to remove/unset the + * filter. + * @param string $path + * (optional) Use this specify the View results page when the exposed form + * is displayed as a block and may be a different URL from the results. + * Defaults to the current path if unspecified. + * + * @return string + * URL. + */ +function bef_replace_query_string_arg($key, $value, $multiple = FALSE, $remove = FALSE, $path = '') { + if (!$path) { + $path = implode('/', arg()); + } + + // Prevents us from having to check for each index from parse_url that we may + // use. + $urllist = array('path' => '', 'fragment' => '', 'query' => ''); + $urllist = array_merge($urllist, parse_url(request_uri())); + $fragment = urldecode($urllist['fragment']); + $query = array(); + parse_str($urllist['query'], $query); + if (isset($query[$key]) && is_array($query[$key])) { + // Multiple values allowed for this existing key. + if ($remove && ($key_remove = array_search($value, $query[$key])) !== FALSE) { + unset($query[$key][$key_remove]); + } + else { + $query[$key][] = $value; + } + } + else { + // Create a new key. + if ($multiple && !$remove) { + $query[$key] = array($value); + } + elseif (!$remove) { + $query[$key] = $value; + } + } + // Unset page arg so we don't land on an empty page off the end of the newly + // filtered listing. + unset($query['page']); + return url(ltrim($path, '/'), array( + 'query' => $query, + 'fragment' => $fragment, + 'absolute' => TRUE, + )); +} diff --git a/sites/all/modules/better_exposed_filters/better_exposed_filters.views.inc b/sites/all/modules/better_exposed_filters/better_exposed_filters.views.inc new file mode 100644 index 0000000..d91d2f5 --- /dev/null +++ b/sites/all/modules/better_exposed_filters/better_exposed_filters.views.inc @@ -0,0 +1,31 @@ + array( + 'better_exposed_filters' => array( + 'title' => t('Better Exposed Filters'), + 'help' => t('Allow use of checkboxes/radio buttons for exposed filters'), + 'handler' => 'better_exposed_filters_exposed_form_plugin', + 'uses row plugin' => FALSE, + 'uses fields' => TRUE, + 'uses options' => TRUE, + 'help topic' => 'exposed-form-basic', + 'type' => 'normal', + 'parent' => 'basic', + ), + ), + ); +} diff --git a/sites/all/modules/better_exposed_filters/better_exposed_filters_exposed_form_plugin.inc b/sites/all/modules/better_exposed_filters/better_exposed_filters_exposed_form_plugin.inc new file mode 100644 index 0000000..ca3d9c6 --- /dev/null +++ b/sites/all/modules/better_exposed_filters/better_exposed_filters_exposed_form_plugin.inc @@ -0,0 +1,2156 @@ +view = &$view; + $this->display = &$display; + + $this->localization_keys = $this->unpack_translatable_keys(); + + $this->unpack_options($this->options, $options); + //$this->unpack_options($this->options, $options, NULL, FALSE); + } + + function summary_title() { + return t('BEF Settings'); + } + + function option_definition() { + $options = parent::option_definition(); + + // Add Better Exposed Filters options to those saved by Views. + $options['bef'] = array( + 'default' => array(), + 'translatable' => TRUE, + 'unpack_translatable' => 'unpack_translatable_options', + ); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $bef_options = array(); + + // Get current settings and default values for new filters. + $existing = $this->_bef_get_settings(); + + /* + * Add general options for exposed form items. + */ + $bef_options['general']['input_required'] = array( + '#type' => 'checkbox', + '#default_value' => $existing['general']['input_required'], + '#title' => t('Require input before results are shown'), + '#description' => t("Emulates the built in Input Required exposed filter handler") + ); + + $bef_options['general']['text_input_required'] = array( + '#type' => 'container', + // Indent dependent options. + '#prefix' => '
', + '#suffix' => '
', + '#states' => array( + // Hide this field when the input_required checkbox is disabled. + 'invisible' => array( + ':input[name="exposed_form_options[bef][general][input_required]"]' => array('checked' => FALSE), + ), + ), + ); + + $bef_options['general']['text_input_required']['text_input_required'] = array( + '#type' => 'text_format', + '#title' => t('Text on demand'), + '#description' => t('Text to display instead of results until the user selects and applies an exposed filter.'), + '#default_value' => $existing['general']['text_input_required']['text_input_required']['value'], + '#format' => $existing['general']['text_input_required']['text_input_required']['format'], + '#wysiwyg' => FALSE, + ); + + $bef_options['general']['allow_secondary'] = array( + '#type' => 'checkbox', + '#title' => t('Enable secondary exposed form options'), + '#default_value' => $existing['general']['allow_secondary'], + '#description' => t('Allows you to specify some exposed form elements as being secondary options and places those elements in a collapsible fieldset. Use this option to place some exposed filters in an "Advanced Search" area of the form, for example.'), + ); + $bef_options['general']['secondary_label'] = array( + '#type' => 'textfield', + '#default_value' => $existing['general']['secondary_label'], + '#title' => t('Secondary options label'), + '#description' => t( + 'The name of the fieldset to hold secondary options. This cannot be left blank or there will be no way to show/hide these options.' + ), + // Indent dependent options. + '#prefix' => '
', + '#suffix' => '
', + '#states' => array( + 'required' => array( + ':input[name="exposed_form_options[bef][general][allow_secondary]"]' => array('checked' => TRUE), + ), + 'visible' => array( + ':input[name="exposed_form_options[bef][general][allow_secondary]"]' => array('checked' => TRUE), + ), + ), + ); + $bef_options['general']['secondary_collapse_override'] = array( + '#type' => 'select', + '#title' => t('Override secondary options fieldset'), + '#default_value' => $existing['general']['secondary_collapse_override'], + '#description' => t( + 'This setting overrides the secondary options fieldset collapsed value. By default the fieldset renders opened if a value within is selected and closed otherwise.' + ), + '#options' => array( + 0 => t('Default'), + 1 => t('Always open'), + 2 => t('Always closed'), + ), + // Indent dependent options. + '#prefix' => '
', + '#suffix' => '
', + '#states' => array( + 'visible' => array( + ':input[name="exposed_form_options[bef][general][allow_secondary]"]' => array('checked' => TRUE), + ), + ), + ); + + /* + * Add options for exposed sorts. + */ + $exposed = FALSE; + foreach ($this->display->handler->get_handlers('sort') as $label => $sort) { + if ($sort->options['exposed']) { + $exposed = TRUE; + break; + } + } + if ($exposed) { + $bef_options['sort']['bef_format'] = array( + '#type' => 'select', + '#title' => t('Display exposed sort options as'), + '#default_value' => $existing['sort']['bef_format'], + '#options' => array( + 'default' => t('Default select list'), + 'bef' => t('Radio Buttons'), + 'bef_links' => t('Links'), + 'bef_toggle_links' => t('Toggle Links'), + ), + '#description' => t('Select a format for the exposed sort options. Note: the "toggle links" option will only work correctly if "Combine sort order with sort by" is checked in the "Advanced Sort Options" section.'), + ); + $bef_options['sort']['advanced'] = array( + '#type' => 'fieldset', + '#title' => t('Advanced sort options'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $bef_options['sort']['advanced']['collapsible'] = array( + '#type' => 'checkbox', + '#title' => t('Make sort options collapsible'), + '#default_value' => $existing['sort']['advanced']['collapsible'], + '#description' => t( + 'Puts the sort options in a collapsible fieldset' + ), + ); + $bef_options['sort']['advanced']['collapsible_label'] = array( + '#type' => 'textfield', + '#title' => t('Collapsible fieldset title'), + '#default_value' => empty($existing['sort']['advanced']['collapsible_label']) ? t('Sort options') : $existing['sort']['advanced']['collapsible_label'], + '#description' => t('This cannot be left blank or there will be no way to show/hide sort options.'), + // Indent dependent options. + '#prefix' => '
', + '#suffix' => '
', + '#states' => array( + 'visible' => array( + 'input[name="exposed_form_options[bef][sort][advanced][collapsible]"]' => array('checked' => TRUE) + ), + ), + ); + $bef_options['sort']['advanced']['combine'] = array( + '#type' => 'checkbox', + '#title' => t('Combine sort order with sort by'), + '#default_value' => $existing['sort']['advanced']['combine'], + '#description' => t('Combines the sort by options and order (ascending or decending) into a single list. Use this to display "Option1 Desc", "Option1 Asc", "Option2 Desc", "Option2 Asc" in a single form element. "Expose sort order" must be checked to enable this option.'), + '#states' => array( + 'disabled' => array( + 'input[name="exposed_form_options[expose_sort_order]"]' => array('checked' => FALSE) + ), + ), + ); + $bef_options['sort']['advanced']['combine_param'] = array( + '#type' => 'textfield', + '#title' => t('Enter a query parameter to use for combined sorts'), + '#default_value' => $existing['sort']['advanced']['combine_param'], + '#description' => t('This will be the $_GET parameter used in query strings. Useful for preventing collisions between exposed filters when there are multiple instances of BEF on the page. Use only UTF-8 letters, numbers, and the dash (-), underscore (_), asterisk (*), and period(.) characters.'), + '#default_value' => empty($existing['sort']['advanced']['combine_param']) ? 'sort_bef_combine' : $existing['sort']['advanced']['combine_param'], + '#required' => TRUE, + // Indent dependent options -- closing is in combine_rewrite. + '#prefix' => '
', + '#states' => array( + 'visible' => array( + 'input[name="exposed_form_options[bef][sort][advanced][combine]"]' => array('checked' => TRUE), + 'input[name="exposed_form_options[expose_sort_order]"]' => array('checked' => TRUE), + ), + ), + ); + $bef_options['sort']['advanced']['combine_rewrite'] = array( + '#type' => 'textarea', + '#title' => t('Rewrite the text displayed'), + '#default_value' => $existing['sort']['advanced']['combine_rewrite'], + '#description' => t('Use this field to rewrite the text displayed for combined sort options and sort order. Use the format of current_text|replacement_text, one replacement per line. For example:
+Post date Asc|Oldest first
+Post date Desc|Newest first
+Title Asc|A -> Z
+Title Desc|Z -> A
Leave the replacement text blank to remove an option altogether.'), + '#suffix' => '
', + '#states' => array( + 'visible' => array( + 'input[name="exposed_form_options[bef][sort][advanced][combine]"]' => array('checked' => TRUE), + 'input[name="exposed_form_options[expose_sort_order]"]' => array('checked' => TRUE), + ), + ), + ); + + $bef_options['sort']['advanced']['reset'] = array( + '#type' => 'checkbox', + '#title' => t('Include a "Reset sort" option'), + '#default_value' => $existing['sort']['advanced']['reset'], + '#description' => t('Adds a "Reset sort" option which will use Views\' default sort order.'), + ); + $bef_options['sort']['advanced']['reset_label'] = array( + '#type' => 'textfield', + '#title' => t('"Reset sort" label'), + '#default_value' => $existing['sort']['advanced']['reset_label'], + '#description' => t('This cannot be left blank if the above option is checked'), + // Indent dependent options. + '#prefix' => '
', + '#suffix' => '
', + '#states' => array( + 'required' => array( + 'input[name="exposed_form_options[bef][sort][advanced][reset]"]' => array('checked' => TRUE), + ), + 'visible' => array( + 'input[name="exposed_form_options[bef][sort][advanced][reset]"]' => array('checked' => TRUE), + ), + ), + ); + + $bef_options['sort']['advanced']['is_secondary'] = array( + '#type' => 'checkbox', + '#title' => t('This is a secondary option'), + '#default_value' => $existing['sort']['advanced']['is_secondary'], + '#states' => array( + 'visible' => array( + ':input[name="exposed_form_options[bef][general][allow_secondary]"]' => array('checked' => TRUE), + ), + ), + '#description' => t('Places this element in the secondary options portion of the exposed form.'), + ); + + $bef_options['sort']['advanced']['autosubmit'] = array( + '#type' => 'checkbox', + '#title' => t('Autosubmit'), + '#default_value' => $existing['sort']['advanced']['autosubmit'], + '#states' => array( + 'disabled' => array( + ':input[name="exposed_form_options[autosubmit]"]' => array('checked' => TRUE), + ), + ), + '#description' => t('Automatically submit the form once a sort element is changed. This option is not available if you enabled autosubmit option for the whole form.'), + ); + } + + /* + * Add options for exposed pager. + */ + $pager_exposed = FALSE; + $current_display = $this->display->handler->view->display[$this->display->handler->view->current_display]; + $default_display = $this->display->handler->view->display['default']; + if (!empty($current_display->handler->options['defaults']["pager"])) { + $pager_exposed = isset($default_display->handler->options['pager']) && !empty($default_display->handler->options['pager']['options']['expose']['items_per_page']); + } + else { + $pager_exposed = isset($current_display->handler->options['pager']) && !empty($current_display->handler->options['pager']['options']['expose']['items_per_page']); + } + if ($pager_exposed) { + $bef_options['pager']['bef_format'] = array( + '#type' => 'select', + '#title' => t('Display exposed pager options as'), + '#default_value' => $existing['pager']['bef_format'], + '#options' => array( + 'default' => t('Default select list'), + 'bef' => t('Radio Buttons'), + 'bef_links' => t('Links'), + ), + '#description' => t('Select a format for the exposed pager options.'), + ); + $bef_options['pager']['is_secondary'] = array( + '#type' => 'checkbox', + '#title' => t('This is a secondary option'), + '#default_value' => $existing['pager']['is_secondary'], + '#states' => array( + 'visible' => array( + ':input[name="exposed_form_options[bef][general][allow_secondary]"]' => array('checked' => TRUE), + ), + ), + '#description' => t('Places this element in the secondary options portion of the exposed form.'), + ); + } + + // Only add the description text once -- it was getting a little long to be + // added to each filter. + $bef_filter_intro = FALSE; + + // Go through each filter and add BEF options. + foreach ($this->display->handler->get_handlers('filter') as $label => $filter) { + if (!$filter->options['exposed']) { + continue; + } + + // If we're adding BEF filter options, add an intro to explain what's + // going on. + if (!$bef_filter_intro) { + $link = l(t('BEF settings documentation'), 'http://drupal.org/node/1701012'); + $bef_options['bef_intro'] = array( + '#markup' => '

' + . t('Exposed Filter Settings') + . '

' + . t('This section lets you select additional options for exposed filters. Some options are only available in certain situations. If you do not see the options you expect, please see the !link page for more details.', + array('!link' => $link)) + . '

', + ); + $bef_filter_intro = TRUE; + } + + // These filter operators get our standard options: radio or checkboxes. + $bef_standard = FALSE; + + // Allows a filter to be rendered as a hidden element. Useful when + // building multi-step filter "wizards," for example. + $bef_hidden = FALSE; + + // Renders the filter as a link. + // @TODO: expand to include toggle links for all bef_links filters, not + // just exposed sorts. + $bef_links = FALSE; + + // These filters get a single on/off checkbox option for boolean + // operators. + $bef_single = FALSE; + + // Used for taxonomy filters with hierarchy. + $bef_nested = FALSE; + + // Allows the term description to be added to rendered output. + $bef_term_description = FALSE; + + // Used for date-based filters. + $bef_datepicker = FALSE; + + // Used for numeric, non-date filters. + $bef_slider = FALSE; + + // Check various filter types and determine what options are available. + if ($filter instanceof views_handler_filter_string || $filter instanceof views_handler_filter_in_operator) { + if (in_array($filter->operator, array('in', 'or', 'and', 'not'))) { + $bef_standard = TRUE; + $bef_links = TRUE; + $bef_hidden = TRUE; + } + if (in_array($filter->operator, array('empty', 'not empty'))) { + $bef_standard = TRUE; + $bef_links = TRUE; + $bef_hidden = TRUE; + if (!$filter->options['expose']['multiple']) { + $bef_single = TRUE; + } + } + } + + if ($filter instanceof views_handler_filter_boolean_operator) { + $bef_standard = TRUE; + $bef_links = TRUE; + $bef_hidden = TRUE; + if (!$filter->options['expose']['multiple']) { + $bef_single = TRUE; + } + } + + // Grouped filters will have a limited number of filter options available + // so we can offer basic BEF options. + if ($filter->options['is_grouped']) { + $bef_standard = TRUE; + $bef_links = TRUE; + $bef_hidden = TRUE; + } + + if ($filter instanceof views_handler_filter_term_node_tid) { + // Autocomplete and dropdown taxonomy filter are both instances of + // view_handler_filter_term_node_tid, but we can't show BEF options for + // the autocomplete widget. + if ('textfield' == $filter->options['type']) { + $bef_standard = FALSE; + $bef_links = FALSE; + $bef_hidden = FALSE; + } + elseif (!empty($filter->options['hierarchy'])) { + $bef_nested = TRUE; + $bef_term_description = TRUE; + } + } + + if ($filter instanceof views_handler_filter_date || !empty($filter->date_handler)) { + $bef_datepicker = TRUE; + $bef_hidden = TRUE; + } + + // The date filter handler extends the numeric filter handler so we have + // to exclude it specifically. + if ($filter instanceof views_handler_filter_numeric && !($filter instanceof views_handler_filter_date)) { + $bef_slider = TRUE; + $bef_hidden = TRUE; + } + + // Search API extends the more general views_handler_filter rather than + // operator-specific classes such as views_handler_filter_in_operator. + // Handle those options here. + if ($filter instanceof SearchApiViewsHandlerFilterOptions) { + $bef_standard = TRUE; + $bef_links = TRUE; + $bef_hidden = TRUE; + } + elseif ($filter instanceof SearchApiViewsHandlerFilterDate) { + $bef_datepicker = TRUE; + if ($filter->options['is_grouped']) { + $bef_standard = TRUE; + $bef_links = TRUE; + $bef_hidden = TRUE; + } + } + // Search API numeric filters support + elseif ($filter instanceof SearchApiViewsHandlerFilter && !($filter instanceof SearchApiViewsHandlerFilterFulltext)) { + $bef_slider = TRUE; + } + elseif ($filter instanceof SearchApiViewsHandlerFilterBoolean) { + $bef_single = TRUE; + } + + // All filters can use the default filter exposed by Views. + $display_options = array('default' => t('Default select list')); + + if ($bef_standard) { + // Main BEF option: radios/checkboxes. + $display_options['bef'] = t('Checkboxes/Radio Buttons'); + } + + if ($bef_nested) { + $display_options['bef_ul'] = t('Nested Checkboxes/Radio Buttons'); + } + + if ($bef_single) { + $display_options['bef_single'] = t('Single on/off checkbox'); + } + + if ($bef_datepicker) { + $display_options['bef_datepicker'] = t('jQuery UI Datepicker'); + } + + if ($bef_slider) { + $display_options['bef_slider'] = t('jQuery UI slider'); + } + + if ($bef_links) { + $display_options['bef_links'] = t('Links'); + } + + if ($bef_hidden) { + $display_options['bef_hidden'] = t('Hidden'); + } + + // Alter the list of available display options for this filter. + drupal_alter('better_exposed_filters_display_options', $display_options, $filter); + + $filter_key = $filter->options['is_grouped'] ? 'group_info' : 'expose'; + $identifier = '"' . $filter->options[$filter_key]['identifier'] . '"'; + if (!empty($filter->options[$filter_key]['label'])) { + $identifier .= t(' (Filter label: "@fl")', array('@fl' => $filter->options[$filter_key]['label'])); + } + $bef_options[$label]['bef_format'] = array( + '#type' => 'select', + '#title' => t('Display @identifier exposed filter as', array('@identifier' => $identifier)), + '#default_value' => $existing[$label]['bef_format'], + '#options' => $display_options, + ); + + if ($bef_slider) { + // Fieldset for jQuery slider options. + $bef_options[$label]['slider_options'] = array( + '#type' => 'fieldset', + '#title' => t('Slider options for @identifier', array('@identifier' => $identifier)), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + '#states' => array( + 'visible' => array( + ':input[name="exposed_form_options[bef][' . $label . '][bef_format]"]' => array('value' => 'bef_slider'), + ), + ), + ); + + $bef_options[$label]['slider_options']['bef_slider_min'] = array( + '#type' => 'textfield', + '#title' => t('Range minimum'), + '#default_value' => $existing[$label]['slider_options']['bef_slider_min'], + '#bef_filter_id' => $label, + '#states' => array( + 'required' => array( + ':input[name="exposed_form_options[bef][' . $label . '][bef_format]"]' => array('value' => 'bef_slider'), + ), + ), + '#description' => t('The minimum allowed value for the jQuery range slider. It can be positive, negative, or zero and have up to 11 decimal places.'), + '#element_validate' => array('element_validate_number', 'better_exposed_filters_element_validate_slider_required', 'better_exposed_filters_element_validate_slider_min_max'), + ); + $bef_options[$label]['slider_options']['bef_slider_max'] = array( + '#type' => 'textfield', + '#title' => t('Range maximum'), + '#default_value' => $existing[$label]['slider_options']['bef_slider_max'], + '#bef_filter_id' => $label, + '#states' => array( + 'required' => array( + ':input[name="exposed_form_options[bef][' . $label . '][bef_format]"]' => array('value' => 'bef_slider'), + ), + ), + '#description' => t('The maximum allowed value for the jQuery range slider. It can be positive, negative, or zero and have up to 11 decimal places.'), + '#element_validate' => array('element_validate_number', 'better_exposed_filters_element_validate_slider_required', 'better_exposed_filters_element_validate_slider_min_max'), + ); + $bef_options[$label]['slider_options']['bef_slider_step'] = array( + '#type' => 'textfield', + '#title' => t('Step'), + '#default_value' => empty($existing[$label]['slider_options']['bef_slider_step']) ? 1 : $existing[$label]['slider_options']['bef_slider_step'], + '#bef_filter_id' => $label, + '#states' => array( + 'required' => array( + ':input[name="exposed_form_options[bef][' . $label . '][bef_format]"]' => array('value' => 'bef_slider'), + ), + ), + '#description' => t('Determines the size or amount of each interval or step the slider takes between the min and max.') . '
' . + t('The full specified value range of the slider (Range maximum - Range minimum) must be evenly divisible by the step.') . '
' . + t('The step must be a positive number of up to 5 decimal places.'), + '#element_validate' => array('element_validate_number', 'better_exposed_filters_element_validate_slider_required', 'better_exposed_filters_element_validate_slider_step'), + ); + $bef_options[$label]['slider_options']['bef_slider_animate'] = array( + '#type' => 'textfield', + '#title' => t('Animate'), + '#default_value' => $existing[$label]['slider_options']['bef_slider_animate'], + '#bef_filter_id' => $label, + '#description' => t('Whether to slide handle smoothly when user click outside handle on the bar. Allowed values are "slow", "normal", "fast" or the number of milliseconds to run the animation (e.g. 1000). If left blank, there will be no animation, the slider will just jump to the new value instantly.'), + '#element_validate' => array('better_exposed_filters_element_validate_slider_animate'), + ); + $bef_options[$label]['slider_options']['bef_slider_orientation'] = array( + '#type' => 'select', + '#title' => t('Orientation'), + '#options' => array( + 'horizontal' => t('Horizontal'), + 'vertical' => t('Vertical'), + ), + '#default_value' => $existing[$label]['slider_options']['bef_slider_orientation'], + '#bef_filter_id' => $label, + '#states' => array( + 'required' => array( + ':input[name="exposed_form_options[bef][' . $label . '][bef_format]"]' => array('value' => 'bef_slider'), + ), + ), + '#description' => t('The orientation of the jQuery range slider.'), + ); + } + + // Fieldset to keep the UI from getting out of hand. + $bef_options[$label]['more_options'] = array( + '#type' => 'fieldset', + '#title' => t('More options for @identifier', array('@identifier' => $identifier)), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + + // Select all checkbox. + if ($bef_standard) { + $bef_options[$label]['more_options']['bef_select_all_none'] = array( + '#type' => 'checkbox', + '#title' => t('Add select all/none links'), + '#default_value' => $existing[$label]['more_options']['bef_select_all_none'], + '#disabled' => !$filter->options['expose']['multiple'], + '#description' => t( + 'Add a "Select All/None" link when rendering the exposed filter using + checkboxes. If this option is disabled, edit the filter and check the + "Allow multiple selections".' + ), + ); + + if ($bef_nested) { + $bef_options[$label]['more_options']['bef_select_all_none_nested'] = array( + '#type' => 'checkbox', + '#title' => t('Add nested all/none selection'), + '#default_value' => $existing[$label]['more_options']['bef_select_all_none_nested'], + '#disabled' => !$filter->options['expose']['multiple'] || !$filter->options['hierarchy'], + '#description' => t( + 'When a parent checkbox is checked, check all its children. If this option + is disabled, edit the filter and check "Allow multiple selections" and + edit the filter settings and check "Show hierarchy in dropdown".' + ), + ); + } + + if ($bef_term_description) { + $bef_options[$label]['more_options']['bef_term_description'] = array( + '#type' => 'checkbox', + '#title' => t('Include the term description'), + '#default_value' => $existing[$label]['more_options']['bef_term_description'], + '#description' => t('For taxonomy term filters, includes the term description for each filter option.'), + '#states' => array( + 'visible' => array( + ':input[name="exposed_form_options[bef][' . $label . '][bef_format]"]' => array( + array('value' => 'bef'), + array('value' => 'bef_ul'), + ), + ), + ), + ); + } + + // Put filter in collapsible fieldset option. + // TODO: expand to all exposed filters. + $bef_options[$label]['more_options']['bef_collapsible'] = array( + '#type' => 'checkbox', + '#title' => t('Make this filter collapsible'), + '#default_value' => $existing[$label]['more_options']['bef_collapsible'], + '#description' => t( + 'Puts this filter in a collapsible fieldset' + ), + ); + } + + // Individual element autosubmit option. + $bef_options[$label]['more_options']['autosubmit'] = array( + '#type' => 'checkbox', + '#title' => t('Autosubmit'), + '#default_value' => $existing[$label]['more_options']['autosubmit'], + '#states' => array( + 'disabled' => array( + ':input[name="exposed_form_options[autosubmit]"]' => array('checked' => TRUE), + ), + ), + '#description' => t('Automatically submit the form once a filter element is changed. This option is not available if you enabled autosubmit option for the whole form.'), + ); + + // Allow any filter to be moved into the secondary options fieldset. + $bef_options[$label]['more_options']['is_secondary'] = array( + '#type' => 'checkbox', + '#title' => t('This is a secondary option'), + '#default_value' => $existing[$label]['more_options']['is_secondary'], + '#states' => array( + 'visible' => array( + ':input[name="exposed_form_options[bef][general][allow_secondary]"]' => array('checked' => TRUE), + ), + ), + '#description' => t('Places this element in the secondary options portion of the exposed form.'), + ); + + // Allow "Any" label to be overridden. + $bef_options[$label]['more_options']['any_label'] = array( + '#type' => 'textfield', + '#title' => t('Override "Any" option label'), + '#default_value' => $existing[$label]['more_options']['any_label'], + '#description' => t('Leave blank to use Views\' default value.'), + ); + + // Build a description option form element -- available to all exposed + // filters. + $bef_options[$label]['more_options']['bef_filter_description'] = array( + '#type' => 'textarea', + '#title' => t('Description'), + '#default_value' => $existing[$label]['more_options']['bef_filter_description'], + '#description' => t('Adds descriptive text to the exposed filter. This is usually rendered in smaller print under the label or the options. You may use tokens as specified below and in the "Global replacements values" section at the bottom of this page.'), + ); + + // Add token support to the description field. + $bef_options[$label]['more_options']['tokens'] = array( + '#title' => t('Replacement patterns'), + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + + if (!module_exists('token')) { + $bef_options[$label]['more_options']['tokens']['no_tokens'] = array( + '#markup' => '

' + . t('Enable the !token module to use replacement values.', array('!token' => l(t('Token'), 'http://drupal.org/project/token'))) + . '

', + ); + } + else { + // Collect a list of token types that make sense for this filter. + $available = array('global_types'); + if (!empty($filter->options['vocabulary'])) { + $available[] = 'vocabulary'; + } + /* @TODO: Other token types? */ + + $filter_specific = array_diff($available, array('global_types')); + if (empty($filter_specific)) { + $bef_options[$label]['more_options']['tokens']['nothing_specific'] = array( + '#markup' => '

' . t('There are no filter-specific tokens for this filter. See the "Global replacement tokens" at the bottom of this dialog for additional token replacement options.') . '

', + ); + } + else { + $bef_options[$label]['more_options']['tokens']['list'] = array( + //'#title' => t('Filter-specific tokens'), + '#theme' => 'token_tree', + '#token_types' => $filter_specific, + '#global_types' => FALSE, + ); + } + + $bef_options[$label]['more_options']['tokens']['available'] = array( + // Save us from parsing available tokens again. + '#type' => 'value', + '#value' => $available, + ); + } + + // Allow rewriting of filter options for any filter. + $bef_options[$label]['more_options']['rewrite'] = array( + '#title' => t('Rewrite filter options'), + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $bef_options[$label]['more_options']['rewrite']['filter_rewrite_values'] = array( + '#type' => 'textarea', + '#title' => t('Rewrite the text displayed'), + '#default_value' => $existing[$label]['more_options']['rewrite']['filter_rewrite_values'], + '#description' => t(' + Use this field to rewrite the filter options displayed. Use the format + of current_text|replacement_text, one replacement per line. For + example:

+Current|Replacement
+On|Yes
+Off|No
+
Leave the replacement text blank to remove an option altogether. If using hierarchical taxonomy filters, do not including leading hyphens in the current value. + '), + ); + + // Add an option to override defaults for the jQuery UI Datepicker. + if ($bef_datepicker) { + $bef_options[$label]['more_options']['datepicker_options'] = array( + '#type' => 'textarea', + '#title' => t('jQuery UI Datepicker option'), + '#default_value' => $existing[$label]['more_options']['datepicker_options'], + '#description' => t('Use this field to override the default options for the Datepicker widget. Options should be in the form of option_name: option_value, one setting per line. For example, to set the days of the week and date format to French:
+dayNamesMin: [ "Di", "Lu", "Ma", "Me", "Je", "Ve", "Sa" ]
+dateFormat: "dd-mm-yy"
+
More information about datepicker options can be found on the jQuery UI website. This field is translatable so you can specify different options for different languages. See the BEF localization documentation for more details.', + array( + '!link' => 'http://api.jqueryui.com/datepicker/', + '!docs_link' => 'https://www.drupal.org/node/2460797', + ) + ), + ); + } + } + /* Ends: foreach ($filters as $filter) { */ + + // Add global token replacements, if available. + if (module_exists('token')) { + $bef_options['global_replacement_tokens'] = array( + '#title' => t('Global replacement patterns'), + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $bef_options['global_replacement_tokens']['list'] = array( + '#theme' => 'token_tree', + '#token_types' => array('global_types'), + ); + } + + // Add BEF form elements to the exposed form options form. + $form['bef'] = $bef_options; + } + + /** + * Tweak the exposed filter form to show Better Exposed Filter options. + * + * @param array $form + * Exposed form array + * @param array $form_state + * Current state of form variables + */ + function exposed_form_alter(&$form, &$form_state) { + parent::exposed_form_alter($form, $form_state); + + // If we have no visible elements, we don't show the Apply button. + $show_apply = FALSE; + + // Collect BEF's Javascript settings, add to Drupal.settings at the end. + // Historical note: We used to only add BEF's Javascript when absolutely + // needed. Eventually, much of that functionality worked its way into the + // normal usage of BEF so that we now turn those Jvaascript behaviors on + // by default. (See https://drupal.org/node/1807114). + $bef_add_js = TRUE; + $bef_js = array( + 'datepicker' => FALSE, + 'slider' => FALSE, + 'settings' => array(), + 'autosubmit' => FALSE, + ); + + // Some widgets will require additional CSS. + $bef_add_css = FALSE; + + // Grab BEF settings. + $settings = $this->_bef_get_settings(); + + // Allow modules/themes to alter BEF settings before they are passed to the + // exposed form widgets. + $context['view'] = $this->view; + $context['display'] = $this->display; + drupal_alter('better_exposed_filters_settings', $settings, $context); + + // Adds view's arguments (if any) to the path to be used use for #bef_path. + $view_path = $this->view->args ? implode('/', array_merge(array($this->view->get_path()), $this->view->args)) : $this->view->get_path(); + + // Some elements may be placed in a secondary fieldset (eg: "Advanced + // search options"). Place this after the exposed filters and before the + // rest of the items in the exposed form. + if ($allow_secondary = $settings['general']['allow_secondary']) { + // If one of the secondary widgets has exposed input, do not collapse the + // secondary fieldset. Or is the "always open" or "always closed" option + // is selected, use that instead. + $secondary_collapse = TRUE; + if ($settings['general']['secondary_collapse_override']) { + $secondary_collapse = $settings['general']['secondary_collapse_override'] == 2; + } + else { + $exposed_input = $this->view->get_exposed_input(); + foreach ($this->display->handler->get_handlers('filter') as $label => $filter) { + if (!$filter->options['exposed']) { + continue; + } + if (!empty($exposed_input[$filter->options['expose']['identifier']]) && $settings[$label]['more_options']['is_secondary']) { + $secondary_collapse = FALSE; + break; + } + } + } + + $secondary = array( + '#type' => 'fieldset', + '#title' => $settings['general']['secondary_label'], + '#collapsible' => TRUE, + '#collapsed' => $secondary_collapse, + '#theme' => 'secondary_exposed_elements', + ); + } + + /* + * Handle exposed sort elements. + */ + if (isset($settings['sort']) && !empty($form['sort_by']) && !empty($form['sort_order'])) { + $show_apply = TRUE; + + // If selected, collect all sort-related form elements and put them + // in a collapsible fieldset. + $collapse = $settings['sort']['advanced']['collapsible'] + && !empty($settings['sort']['advanced']['collapsible_label']); + $sort_elems = array(); + + // Check for combined sort_by and sort_order. + if ($settings['sort']['advanced']['combine']) { + $form_state['#combine_param'] = $settings['sort']['advanced']['combine_param']; + // Combine sort_by and sort_order into a single element. + $form[$settings['sort']['advanced']['combine_param']] = array( + '#type' => 'radios', + // Already sanitized by Views. + '#title' => $form['sort_by']['#title'], + ); + $options = array(); + // If using the bef_toggle_links format, determine which links should + // not be shown. + $hidden_options = array(); + + // Add reset sort option at the top of the list. + if ($settings['sort']['advanced']['reset']) { + $options[' '] = t($settings['sort']['advanced']['reset_label']); + } + else { + $form[$settings['sort']['advanced']['combine_param']]['#default_value'] = ''; + } + + $selected = ''; + $used_sort_keys = array(); + foreach ($form['sort_by']['#options'] as $by_key => $by_val) { + foreach ($form['sort_order']['#options'] as $order_key => $order_val) { + // Use a space to separate the two keys, we'll unpack them in our + // submit handler. + $options["$by_key $order_key"] = "$by_val $order_val"; + + if ($form['sort_order']['#default_value'] == $order_key && empty($selected)) { + // Respect default sort order set in Views. The default sort field + // will be the first one if there are multiple sort criteria. + $selected = "$by_key $order_key"; + } + if ($settings['sort']['bef_format'] == 'bef_toggle_links') { + if (isset($used_sort_keys[$by_key]) + || (!empty($form_state['input'][$settings['sort']['advanced']['combine_param']]) && $form_state['input'][$settings['sort']['advanced']['combine_param']] == "$by_key $order_key") + || (empty($form_state['input'][$settings['sort']['advanced']['combine_param']]) && $selected == "$by_key $order_key") + ) { + $hidden_options["$by_key $order_key"] = "$by_val $order_val"; + } + else { + $used_sort_keys[$by_key] = $order_key; + } + } + } + } + + // Rewrite the option values if any were specified. + if (!empty($settings['sort']['advanced']['combine_rewrite'])) { + $lines = explode("\n", trim($settings['sort']['advanced']['combine_rewrite'])); + $rewrite = array(); + foreach ($lines as $line) { + list($search, $replace) = explode('|', $line); + if (isset($search)) { + $rewrite[$search] = $replace; + } + } + foreach ($options as $index => $option) { + if (isset($rewrite[$option])) { + if ('' == $rewrite[$option]) { + unset($options[$index]); + if ($selected == $index) { + // Avoid "Illegal choice" errors. + $selected = NULL; + } + } + else { + $options[$index] = $rewrite[$option]; + } + } + } + } + + $form[$settings['sort']['advanced']['combine_param']] = array( + '#type' => 'radios', + '#options' => $options, + '#hidden_options' => $hidden_options, + '#settings' => array( + 'toggle_links' => ($settings['sort']['bef_format'] == 'bef_toggle_links'), + 'combine_param' => $settings['sort']['advanced']['combine_param'], + ), + '#default_value' => $selected, + // Already sanitized by Views. + '#title' => $form['sort_by']['#title'], + ); + + // Handle display-specific details. + switch ($settings['sort']['bef_format']) { + case 'bef': + $form[$settings['sort']['advanced']['combine_param']]['#prefix'] = '
'; + $form[$settings['sort']['advanced']['combine_param']]['#suffix'] = '
'; + break; + + case 'bef_links': + case 'bef_toggle_links': + $bef_add_js = TRUE; + $form[$settings['sort']['advanced']['combine_param']]['#theme'] = 'select_as_links'; + + // Exposed form displayed as blocks can appear on pages other than + // the view results appear on. This can cause problems with + // select_as_links options as they will use the wrong path. We + // provide a hint for theme functions to correct this. + if (!empty($this->display->display_options['exposed_block'])) { + $form[$settings['sort']['advanced']['combine_param']]['#bef_path'] = $view_path; + } + break; + + case 'default': + $form[$settings['sort']['advanced']['combine_param']]['#type'] = 'select'; + break; + } + + // Add our submit routine to process. + $form['#submit'][] = 'bef_sort_combine_submit'; + + // Pretend we're another exposed form widget. + $form['#info']['sort-sort_bef_combine'] = array( + 'value' => $settings['sort']['advanced']['combine_param'], + ); + + // Remove the existing sort_by and sort_order elements. + unset($form['sort_by']); + unset($form['sort_order']); + + if ($collapse) { + $sort_elems[] = $settings['sort']['advanced']['combine_param']; + } + } + /* if ($settings['sort']['advanced']['combine']) { } */ + else { + // Leave sort_by and sort_order as separate elements. + if ('bef' == $settings['sort']['bef_format']) { + $form['sort_by']['#type'] = 'radios'; + if (empty($form['sort_by']['#process'])) { + $form['sort_by']['#process'] = array(); + } + array_unshift($form['sort_by']['#process'], 'form_process_radios'); + $form['sort_by']['#prefix'] = '
'; + $form['sort_by']['#suffix'] = '
'; + + $form['sort_order']['#type'] = 'radios'; + if (empty($form['sort_order']['#process'])) { + $form['sort_order']['#process'] = array(); + } + array_unshift($form['sort_order']['#process'], 'form_process_radios'); + $form['sort_order']['#prefix'] = '
'; + $form['sort_order']['#suffix'] = '
'; + } + elseif ('bef_links' == $settings['sort']['bef_format']) { + $form['sort_by']['#theme'] = 'select_as_links'; + $form['sort_order']['#theme'] = 'select_as_links'; + + // Exposed form displayed as blocks can appear on pages other than the + // view results appear on. This can cause problems with + // select_as_links options as they will use the wrong path. We provide + // a hint for theme functions to correct this. + if (!empty($this->display->display_options['exposed_block'])) { + $form['sort_by']['#bef_path'] = $form['sort_order']['#bef_path'] = $view_path; + } + } + + if ($collapse) { + $sort_elems[] = 'sort_by'; + $sort_elems[] = 'sort_order'; + } + + // Add reset sort option if selected. + if ($settings['sort']['advanced']['reset']) { + array_unshift($form['sort_by']['#options'], $settings['sort']['advanced']['reset_label']); + } + } + /* Ends: if ($settings['sort']['advanced']['combine']) { ... } else { */ + + if ($collapse) { + $form['bef_sort_options'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => $settings['sort']['advanced']['collapsible_label'], + ); + foreach ($sort_elems as $elem) { + $form['bef_sort_options'][$elem] = $form[$elem]; + unset($form[$elem]); + } + } + + // Check if this is a secondary form element. + if ($allow_secondary && $settings['sort']['advanced']['is_secondary']) { + foreach (array($settings['sort']['advanced']['combine_param'], 'sort_by', 'sort_order') as $elem) { + if (!empty($form[$elem])) { + $secondary[$elem] = $form[$elem]; + unset($form[$elem]); + } + } + } + } + elseif (isset($settings['sort']) && !empty($form['sort_by'])) { + if ('bef_links' == $settings['sort']['bef_format']) { + $bef_add_js = TRUE; + $form['sort_by']['#theme'] = 'select_as_links'; + + // Exposed form displayed as blocks can appear on pages other than + // the view results appear on. This can cause problems with + // select_as_links options as they will use the wrong path. We + // provide a hint for theme functions to correct this. + if (!empty($this->display->display_options['exposed_block'])) { + $form['sort_by']['#bef_path'] = $view_path; + } + } + } + + // Apply autosubmit sort values. + if (empty($this->options['autosubmit']) && !empty($settings['sort']['advanced']['autosubmit'])) { + $bef_js['autosubmit'] = TRUE; + foreach (array($settings['sort']['advanced']['combine_param'], 'sort_by', 'sort_order') as $elem) { + if (!empty($form[$elem])) { + $form[$elem] = array_merge_recursive($form[$elem], array('#attributes' => array('class' => array('ctools-auto-submit')))); + } + } + } + /* Ends: if (isset($settings['sort'])) { */ + + /* + * Handle exposed pager elements. + */ + if (isset($settings['pager'])) { + switch ($settings['pager']['bef_format']) { + case 'bef': + $show_apply = TRUE; + $form['items_per_page']['#type'] = 'radios'; + if (empty($form['items_per_page']['#process'])) { + $form['items_per_page']['#process'] = array(); + } + array_unshift($form['items_per_page']['#process'], 'form_process_radios'); + $form['items_per_page']['#prefix'] = '
'; + $form['items_per_page']['#suffix'] = '
'; + break; + + case 'bef_links': + if (count($form['items_per_page']['#options']) > 1) { + $bef_add_js = TRUE; + $form['items_per_page']['#theme'] = 'select_as_links'; + $form['items_per_page']['#items_per_page'] = max($form['items_per_page']['#default_value'], key($form['items_per_page']['#options'])); + + // Exposed form displayed as blocks can appear on pages other than + // the view results appear on. This can cause problems with + // select_as_links options as they will use the wrong path. We + // provide a hint for theme functions to correct this. + if (!empty($this->display->display_options['exposed_block'])) { + $form['items_per_page']['#bef_path'] = $view_path; + } + } + break; + } + + // Check if this is a secondary form element. + if ($allow_secondary && $settings['pager']['is_secondary']) { + foreach (array('items_per_page', 'offset') as $elem) { + if (!empty($form[$elem])) { + $secondary[$elem] = $form[$elem]; + unset($form[$elem]); + } + } + } + } + + // Shorthand for all filters in this view. + $filters = $form_state['view']->display_handler->handlers['filter']; + + // Go through each saved option looking for Better Exposed Filter settings. + foreach ($settings as $label => $options) { + // Sanity check: Ensure this filter is an exposed filter. + if (empty($filters[$label]) || !$filters[$label]->options['exposed']) { + continue; + } + + // Form element is designated by the element ID which is user- + // configurable. + $filter_key = 'filter-' . (!empty($filters[$label]->options['is_grouped']) ? $filters[$label]->options['group_info']['identifier'] : $label); + $filter_id = $form['#info'][$filter_key]['value']; + + // Token replacement on BEF Description fields. + if (!empty($options['more_options']['bef_filter_description'])) { + // Collect replacement data. + $data = array(); + $available = $options['more_options']['tokens']['available']; + if (in_array('vocabulary', $available) && isset($filters[$label]->definition['vocabulary'])) { + $data['vocabulary'] = taxonomy_vocabulary_machine_name_load($filters[$label]->definition['vocabulary']); + } + /* Others? */ + + // Replace tokens. + $options['more_options']['bef_filter_description'] = token_replace( + $options['more_options']['bef_filter_description'], $data + ); + $form[$filter_id]['#bef_description'] = $options['more_options']['bef_filter_description']; + } + + // Handle filter value rewrites. + if (!empty($options['more_options']['rewrite']['filter_rewrite_values'])) { + $lines = explode("\n", trim($options['more_options']['rewrite']['filter_rewrite_values'])); + $rewrite = array(); + foreach ($lines as $line) { + list($search, $replace) = explode('|', $line); + if (isset($search)) { + $rewrite[$search] = $replace; + } + } + + foreach ($form[$filter_id]['#options'] as $index => $option) { + $is_object = FALSE; + if (is_object($option)) { + // Taxonomy filters use objects instead of text. + $is_object = TRUE; + $option = reset($option->option); + + // Hierarchical filters prepend hyphens to indicate depth. We need + // to remove them for comparison, but keep them after replacement to + // ensure nested options display correctly. + $option = ltrim($option, '-'); + } + + if (isset($rewrite[$option])) { + if ('' == $rewrite[$option]) { + unset($form[$filter_id]['#options'][$index]); + } + else { + if ($is_object) { + // dsm($form[$filter_id]['#options'][$index]->option, "$filter_id at $index"); + // Taxonomy term filters are stored as objects. Use str_replace + // to ensure that keep hyphens for hierarchical filters. + list($tid, $original) = each($form[$filter_id]['#options'][$index]->option); + $form[$filter_id]['#options'][$index]->option[$tid] = str_replace($option, $rewrite[$option], $original); + } + else { + $form[$filter_id]['#options'][$index] = $rewrite[$option]; + } + } + } + } + } + + // @TODO: Is this conditional needed anymore after the existing settings + // array default values were added? + if (!isset($options['bef_format'])) { + $options['bef_format'] = ''; + } + + // These BEF options require a set of given options to work (namely, + // $form[$filter_id]['#options'] needs to set). But it is possible to + // adjust settings elsewhere in the view that removes these options from + // the form (eg: changing a taxonomy term filter from dropdown to + // autocomplete). Check for that here and revert to Views' default filter + // in those cases. + $requires_options = array('bef', 'bef_ul', 'bef_links'); + if (in_array($options['bef_format'], $requires_options) && !array_key_exists('#options', $form[$filter_id])) { + $options['bef_format'] = 'default'; + } + + switch ($options['bef_format']) { + case 'bef_datepicker': + $show_apply = TRUE; + $bef_add_js = TRUE; + $bef_js['datepicker'] = TRUE; + $bef_js['datepicker_options'] = array(); + + if (( + // Single Date API-based input element. + isset($form[$filter_id]['value']['#type']) + && 'date_text' == $form[$filter_id]['value']['#type'] + ) + // Double Date-API-based input elements such as "in-between". + || (isset($form[$filter_id]['min']) && isset($form[$filter_id]['max']) + && 'date_text' == $form[$filter_id]['min']['#type'] + && 'date_text' == $form[$filter_id]['max']['#type'] + )) { + /* + * Convert Date API formatting to jQuery formatDate formatting. + * + * @TODO: To be honest, I'm not sure this is needed. Can you set a + * Date API field to accept anything other than Y-m-d? Well, better + * safe than sorry... + * + * @see http://us3.php.net/manual/en/function.date.php + * @see http://docs.jquery.com/UI/Datepicker/formatDate + * + * Array format: PHP date format => jQuery formatDate format + * (comments are for the PHP format, lines that are commented out do + * not have a jQuery formatDate equivalent, but maybe someday they + * will...) + */ + $convert = array( + /* Day */ + + // Day of the month, 2 digits with leading zeros 01 to 31. + 'd' => 'dd', + // A textual representation of a day, three letters Mon through + // Sun. + 'D' => 'D', + // Day of the month without leading zeros 1 to 31. + 'j' => 'd', + // (lowercase 'L') A full textual representation of the day of the + // week Sunday through Saturday. + 'l' => 'DD', + // ISO-8601 numeric representation of the day of the week (added + // in PHP 5.1.0) 1 (for Monday) through 7 (for Sunday). + // 'N' => ' ', + // English ordinal suffix for the day of the month, 2 characters + // st, nd, rd or th. Works well with j. + // 'S' => ' ', + // Numeric representation of the day of the week 0 (for Sunday) + // through 6 (for Saturday). + // 'w' => ' ', + // The day of the year (starting from 0) 0 through 365. + 'z' => 'o', + + /* Week */ + // ISO-8601 week number of year, weeks starting on Monday (added + // in PHP 4.1.0) Example: 42 (the 42nd week in the year). + // 'W' => ' ', + // + /* Month */ + // A full textual representation of a month, such as January or + // March January through December. + 'F' => 'MM', + // Numeric representation of a month, with leading zeros 01 + // through 12. + 'm' => 'mm', + // A short textual representation of a month, three letters Jan + // through Dec. + 'M' => 'M', + // Numeric representation of a month, without leading zeros 1 + // through 12. + 'n' => 'm', + // Number of days in the given month 28 through 31. + // 't' => ' ', + // + /* Year */ + // Whether it's a leap year 1 if it is a leap year, 0 otherwise. + // 'L' => ' ', + // ISO-8601 year number. This has the same value as Y, except that + // if the ISO week number (W) belongs to the previous or next + // year, that year is used instead. (added in PHP 5.1.0). + // Examples: 1999 or 2003. + // 'o' => ' ', + // A full numeric representation of a year, 4 digits Examples: + // 1999 or 2003. + 'Y' => 'yy', + // A two digit representation of a year Examples: 99 or 03. + 'y' => 'y', + + /* Time */ + // Lowercase Ante meridiem and Post meridiem am or pm. + // 'a' => ' ', + // Uppercase Ante meridiem and Post meridiem AM or PM. + // 'A' => ' ', + // Swatch Internet time 000 through 999. + // 'B' => ' ', + // 12-hour format of an hour without leading zeros 1 through 12. + // 'g' => ' ', + // 24-hour format of an hour without leading zeros 0 through 23. + // 'G' => ' ', + // 12-hour format of an hour with leading zeros 01 through 12. + // 'h' => ' ', + // 24-hour format of an hour with leading zeros 00 through 23. + // 'H' => ' ', + // Minutes with leading zeros 00 to 59. + // 'i' => ' ', + // Seconds, with leading zeros 00 through 59. + // 's' => ' ', + // Microseconds (added in PHP 5.2.2) Example: 654321. + // 'u' => ' ', + ); + + $format = ''; + if (isset($form[$filter_id]['value'])) { + $format = $form[$filter_id]['value']['#date_format']; + $form[$filter_id]['value']['#attributes']['class'][] = 'bef-datepicker'; + + // This element renders via Drupal's FormAPI so we can use + // #description instead of passing the description along to BEF's + // theme functions. + if (!empty($form[$filter_id]['#bef_description'])) { + $form[$filter_id]['#description'] = $form[$filter_id]['#bef_description']; + } + } + else { + // Both min and max share the same format. + $format = $form[$filter_id]['min']['#date_format']; + $form[$filter_id]['min']['#attributes']['class'][] = 'bef-datepicker'; + $form[$filter_id]['max']['#attributes']['class'][] = 'bef-datepicker'; + + // Description goes with the second field for in-between filters. + if (!empty($form[$filter_id]['#bef_description'])) { + $form[$filter_id]['max']['#description'] = $form[$filter_id]['#bef_description']; + } + } + $bef_js['datepicker_options']['dateFormat'] = json_encode(str_replace(array_keys($convert), array_values($convert), $format)); + } + else { + $bef_js['datepicker_options']['dateFormat'] = ''; + /* + * Standard Drupal date field. Depending on the settings, the field + * can be at $form[$filter_id] (single field) or + * $form[$filter_id][subfield] for two-value date fields or filters + * with exposed operators. + */ + $fields = array('min', 'max', 'value'); + if (count(array_intersect($fields, array_keys($form[$filter_id])))) { + $final = ''; + foreach ($fields as $field) { + if (isset($form[$filter_id][$field])) { + $form[$filter_id][$field]['#attributes']['class'][] = 'bef-datepicker'; + $final = $field; + } + } + // Description goes with the second field for in-between filters. + if (!empty($form[$filter_id]['#bef_description'])) { + $form[$filter_id][$final]['#description'] = $form[$filter_id]['#bef_description']; + } + } + else { + $form[$filter_id]['#attributes']['class'][] = 'bef-datepicker'; + + // This element renders via Drupal's FormAPI so we can use + // #description instead of passing the description along to BEF's + // theme functions. + if (!empty($form[$filter_id]['#bef_description'])) { + $form[$filter_id]['#description'] = $form[$filter_id]['#bef_description']; + } + } + } + + if (!empty($options['more_options']['datepicker_options'])) { + foreach (explode("\n", $options['more_options']['datepicker_options']) as $setting) { + list ($key, $val) = explode(':', trim($setting), 2); + // No need to json_encode() this value like we do other datepicker + // options as it should already be entered in JSON format in the + // UI. While having an admin enter JSON in a text field is not + // ideal, it is how the jQueryUI widget documentation shows it in + // their examples. + $bef_js['datepicker_options'][$key] = $val; + } + } + break; + + case 'bef_slider': + $show_apply = TRUE; + $bef_add_js = TRUE; + $bef_add_css = TRUE; + $bef_js['slider'] = TRUE; + + // Add js options for the slider for this filter. + $bef_js['slider_options'][$filter_id] = array( + 'min' => $options['slider_options']['bef_slider_min'], + 'max' => $options['slider_options']['bef_slider_max'], + 'step' => $options['slider_options']['bef_slider_step'], + 'animate' => $options['slider_options']['bef_slider_animate'], + 'orientation' => $options['slider_options']['bef_slider_orientation'], + 'id' => drupal_html_id($filter_id), + 'viewId' => $form['#id'], + ); + + // We need a wrapping element that covers all elements in the + // slider -- not just the text fields, but descriptions as well. When + // placed in the secondary fieldset, we lose what we usually get from + // the FormAPI. + if ($options['more_options']['is_secondary']) { + $form[$filter_id]['#prefix'] = '
'; + $form[$filter_id]['#suffix'] = '
'; + } + + // This element renders via Drupal's FormAPI so we can use + // #description instead of passing the description along to BEF's + // theme functions. + if (!empty($form[$filter_id]['#bef_description'])) { + $children = element_children($form[$filter_id]); + if (count($children) > 1) { + // Put the description on the last child or this element. + $form[$filter_id][end($children)]['#description'] = $form[$filter_id]['#bef_description']; + } + else { + $form[$filter_id]['#description'] = $form[$filter_id]['#bef_description']; + } + } + break; + + case 'bef_links': + $bef_add_js = TRUE; + $form[$filter_id]['#theme'] = 'select_as_links'; + + // Exposed form displayed as blocks can appear on pages other than + // the view results appear on. This can cause problems with + // select_as_links options as they will use the wrong path. We provide + // a hint for theme functions to correct this. + if (!empty($this->display->display_options['exposed_block'])) { + $form[$filter_id]['#bef_path'] = $view_path; + } + break; + + case 'bef_single': + $show_apply = TRUE; + + // Use filter label as checkbox label. + $form[$filter_id]['#title'] = $filters[$label]->options['expose']['label']; + $form[$filter_id]['#description'] = $options['more_options']['bef_filter_description']; + $form[$filter_id]['#return_value'] = 1; + $form[$filter_id]['#type'] = 'checkbox'; + // Views populates missing values in $form_state['input'] with the + // defaults and a checkbox does not appear in $_GET (or $_POST) so it + // will appear to be missing when a user submits a form. Because of + // this, instead of unchecking the checkbox value will revert to the + // default. More, the default value for select values is reused which + // results in the checkbox always checked. So we need to add a form + // element to see whether the form is submitted or not and then we + // need to look at $_GET directly to see whether the checkbox is + // there. For security reasons, we must not copy the $_GET value. + + // First, let's figure out a short name for the signal element and + // then add it. + if (empty($signal)) { + for ($signal = 'a'; isset($form[$signal]); $signal++); + // This is all the signal element needs. + $form[$signal]['#type'] = 'hidden'; + } + $checked = isset($form_state['input'][$signal]) ? isset($_GET[$filter_id]) : $form[$filter_id]['#default_value']; + // Now we know whether the checkbox is checked or not, set #value + // accordingly. + $form[$filter_id]['#value'] = $checked ? $form[$filter_id]['#return_value'] : 0; + // Handoff to the theme layer. + $form[$filter_id]['#theme'] = 'checkbox'; + break; + + case 'bef_ul': + $show_apply = TRUE; + + $form[$filter_id]['#bef_nested'] = TRUE; + /* Intentionally falling through to case 'bef'. */ + + case 'bef': + $show_apply = TRUE; + + if (empty($form[$filter_id]['#multiple'])) { + // Single-select -- display as radio buttons. + $form[$filter_id]['#type'] = 'radios'; + if (empty($form[$filter_id]['#process'])) { + $form[$filter_id]['#process'] = array(); + } + array_unshift($form[$filter_id]['#process'], 'form_process_radios'); + + // Add description + if (!empty($form[$filter_id]['#bef_description'])) { + $form[$filter_id]['#description'] = $form[$filter_id]['#bef_description']; + } + + // Clean up objects from the options array (happens for taxonomy- + // based filters). + $opts = $form[$filter_id]['#options']; + $form[$filter_id]['#options'] = array(); + foreach ($opts as $index => $opt) { + if (is_object($opt)) { + reset($opt->option); + list($key, $val) = each($opt->option); + $form[$filter_id]['#options'][$key] = $val; + } + else { + $form[$filter_id]['#options'][$index] = $opt; + } + } + + if (isset($form[$filter_id]['#options']['All'])) { + // @TODO: The terms 'All' and 'Any' are customizable in Views. + if ($filters[$label]->options['expose']['multiple']) { + // Some third-party filter handlers still add the "Any" option + // even if this is not an optional filter. Zap it here if they + // do. + unset($form[$filter_id]['#options']['All']); + } + else { + // Otherwise, make sure the "Any" text is clean. + $form[$filter_id]['#options']['All'] = check_plain($form[$filter_id]['#options']['All']); + } + } + + // Render as radio buttons or radio buttons in a collapsible + // fieldset. + if (!empty($options['more_options']['bef_collapsible'])) { + // Pass the description and title along in a way such that it + // doesn't get rendered as part of the exposed form widget. We'll + // render them as part of the fieldset. + if (isset($form['#info'][$filter_key]['label'])) { + $form[$filter_id]['#bef_title'] = $form['#info'][$filter_key]['label']; + unset($form['#info'][$filter_key]['label']); + } + if (!empty($options['more_options']['bef_filter_description'])) { + $form[$filter_id]['#bef_description'] = $options['more_options']['bef_filter_description']; + if (isset($form[$filter_id]['#description'])) { + unset($form[$filter_id]['#description']); + } + } + + // If the operator is exposed as well, put it inside the fieldset. + if ($filters[$label]->options['expose']['use_operator']) { + $operator_id = $filters[$label]->options['expose']['operator_id']; + $form[$filter_id]['#bef_operator'] = $form[$operator_id]; + unset ($form[$operator_id]); + } + + // Add collapse/expand Javascript and BEF CSS to prevent collapsed + // fieldset from disappearing. + if (empty($form[$filter_id]['#attached']['js'])) { + $form[$filter_id]['#attached']['js'] = array(); + } + $form[$filter_id]['#attached']['js'][] = 'misc/form.js'; + $form[$filter_id]['#attached']['js'][] = 'misc/collapse.js'; + + if (empty($form[$filter_id]['#attached']['css'])) { + $form[$filter_id]['#attached']['css'] = array(); + } + $form[$filter_id]['#attached']['css'][] = drupal_get_path('module', 'better_exposed_filters') . '/better_exposed_filters.css'; + + // Take care of adding the fieldset in the theme layer. + $form[$filter_id]['#theme'] = 'select_as_radios_fieldset'; + } + /* if (!empty($options['more_options']['bef_collapsible'])) { */ + else { + // Render select element as radio buttons. + $form[$filter_id]['#attributes']['class'][] = 'bef-select-as-radios'; + $form[$filter_id]['#theme'] = 'select_as_radios'; + } + } + /* if (empty($form[$filter_id]['#multiple'])) { */ + else { + // Render as checkboxes or checkboxes enclosed in a collapsible + // fieldset. + if (!empty($options['more_options']['bef_collapsible'])) { + // Pass the description and title along in a way such that it + // doesn't get rendered as part of the exposed form widget. We'll + // render them as part of the fieldset. + if (isset($form['#info'][$filter_key]['label'])) { + $form[$filter_id]['#bef_title'] = $form['#info'][$filter_key]['label']; + unset($form['#info'][$filter_key]['label']); + } + if (!empty($options['more_options']['bef_filter_description'])) { + $form[$filter_id]['#bef_description'] = $options['more_options']['bef_filter_description']; + if (isset($form[$filter_id]['#description'])) { + unset($form[$filter_id]['#description']); + } + } + + // If the operator is exposed as well, put it inside the fieldset. + if ($filters[$label]->options['expose']['use_operator']) { + $operator_id = $filters[$label]->options['expose']['operator_id']; + $form[$filter_id]['#bef_operator'] = $form[$operator_id]; + unset ($form[$operator_id]); + } + + // Add collapse/expand Javascript and BEF CSS to prevent collapsed + // fieldset from disappearing. + if (empty($form[$filter_id]['#attached']['js'])) { + $form[$filter_id]['#attached']['js'] = array(); + } + $form[$filter_id]['#attached']['js'][] = 'misc/form.js'; + $form[$filter_id]['#attached']['js'][] = 'misc/collapse.js'; + + if (empty($form[$filter_id]['#attached']['css'])) { + $form[$filter_id]['#attached']['css'] = array(); + } + $form[$filter_id]['#attached']['css'][] = drupal_get_path('module', 'better_exposed_filters') . '/better_exposed_filters.css'; + + // Take care of adding the fieldset in the theme layer. + $form[$filter_id]['#theme'] = 'select_as_checkboxes_fieldset'; + } + else { + $form[$filter_id]['#theme'] = 'select_as_checkboxes'; + } + + if ($options['more_options']['bef_select_all_none'] || $options['more_options']['bef_select_all_none_nested']) { + $bef_add_js = TRUE; + + if ($options['more_options']['bef_select_all_none']) { + $form[$filter_id]['#bef_select_all_none'] = TRUE; + } + if ($options['more_options']['bef_select_all_none_nested']) { + $form[$filter_id]['#bef_select_all_none_nested'] = TRUE; + } + } + } + /* Ends: if (empty($form[$filter_id]['#multiple'])) { ... } else { */ + + // Add term descriptions, if appropriate. Pass along to theme + // functions for rendering. + if ($options['more_options']['bef_term_description'] && $filters[$filter_id] instanceof views_handler_filter_term_node_tid) { + $tids = array(); + foreach ($form[$filter_id]['#options'] as $tid => $option) { + if (is_object($option)) { + reset($option->option); + list ($tid, ) = each($option->option); + } + $tids[] = $tid; + } + $terms = taxonomy_term_load_multiple($tids); + foreach ($terms as $tid => $term) { + $form[$filter_id]['#bef_term_descriptions'][$tid] = $term->description; + } + } + break; + + case 'bef_hidden': + // Hide the label. + $form['#info'][$filter_key]['label'] = ''; + if (empty($form[$filter_id]['#multiple'])) { + $form[$filter_id]['#type'] = 'hidden'; + } + else { + $form[$filter_id]['#theme'] = 'select_as_hidden'; + } + break; + + default: + // Handles functionality for exposed filters that are not rendered + // using BEF. + $show_apply = TRUE; + + // Add a description to the exposed filter. + if (!empty($options['more_options']['bef_filter_description'])) { + $children = element_children($form[$filter_id]); + if (count($children) > 1) { + // A filter may have multiple children if it's a in-between + // filter. In this case, put the description on the last item. + $form[$filter_id][end($children)]['#description'] = t($options['more_options']['bef_filter_description']); + } + else { + $form[$filter_id]['#description'] = t($options['more_options']['bef_filter_description']); + } + } + break; + } + /* Ends switch ($options['bef_format']) */ + + // Apply autosubmit filter values. + if (empty($this->options['autosubmit']) && !empty($options['more_options']['autosubmit'])) { + $bef_js['autosubmit'] = TRUE; + + // Check type form element. + if (isset($form[$filter_id]['value'])) { + $form[$filter_id]['value'] = array_merge_recursive($form[$filter_id]['value'], array('#attributes' => array('class' => array('ctools-auto-submit')))); + } + else { + $form[$filter_id] = array_merge_recursive($form[$filter_id], array('#attributes' => array('class' => array('ctools-auto-submit')))); + } + } + + // Override "Any" label, if applicable. + if (!empty($options['more_options']['any_label']) && !empty($form[$filter_id]['#options']['All'])) { + $form[$filter_id]['#options']['All'] = $options['more_options']['any_label']; + } + + // Check if this is a secondary form element. + if ($allow_secondary && $settings[$label]['more_options']['is_secondary']) { + $identifier = $form['#info'][$filter_key]['value']; + if (!empty($form[$identifier])) { + // Move from the main form to the secondary options fieldset. + $children = element_children($form[$identifier]); + $secondary[$identifier] = $form[$identifier]; + unset($form[$identifier]); + + if (1 < count($children)) { + // Some elements can have multiple children, for example min/max + // fields on an in-between filter. In those cases Add the label + // to the first element. + $secondary[$identifier][$children[0]]['#title'] = isset($form['#info'][$filter_key]['label']) ? $form['#info'][$filter_key]['label'] : ''; + unset($form['#info'][$filter_key]); + } + else { + $secondary[$identifier]['#title'] = isset($form['#info'][$filter_key]['label']) ? $form['#info'][$filter_key]['label'] : ''; + unset($form['#info'][$filter_key]); + } + + // Ensure secondary options respect ordering of filters. Pass this + // along to the theme function for rendering. Multiply existing + // position by 2 so that we have room to stick the exposed operator + // before the filter. + if (isset($filters[$identifier]->position)) { + $secondary[$identifier]['#bef_position'] = $filters[$identifier]->position * 2; + } + + // Move exposed operators with exposed filters + if (!empty($filters[$label]->options['expose']['use_operator'])) { + $op_id = $filters[$label]->options['expose']['operator_id']; + $secondary[$op_id] = $form[$op_id]; + unset($form[$op_id]); + + // Make sure operators appear just before the filter they are + // associated with. + if (isset($filters[$identifier]->position)) { + $secondary[$op_id]['#bef_position'] = ($filters[$identifier]->position * 2) - 1; + } + } + } + } + } + + // If our form has no visible filters, hide the submit button. + if (!$show_apply) { + if (isset($form['submit'])) { + $form['submit']['#attributes']['class'][] = 'element-hidden'; + } + if (isset($form['reset'])) { + $form['reset']['#attributes']['class'][] = 'element-hidden'; + } + } + + // Add Javascript as needed. + if ($bef_add_js) { + // Add jQuery UI library code as needed. + if ($bef_js['datepicker']) { + drupal_add_library('system', 'ui.datepicker'); + } + if ($bef_js['slider']) { + drupal_add_library('system', 'ui.slider'); + } + if ($bef_js['autosubmit']) { + $form['submit']['#attributes']['class'][] = 'ctools-use-ajax'; + $form['submit']['#attributes']['class'][] = 'ctools-auto-submit-click'; + $form['#attached']['js'][] = drupal_get_path('module', 'ctools') . '/js/auto-submit.js'; + } + + drupal_add_js(array('better_exposed_filters' => $bef_js), 'setting'); + drupal_add_js(drupal_get_path('module', 'better_exposed_filters') . '/better_exposed_filters.js'); + } + if ($bef_add_css) { + drupal_add_css(drupal_get_path('module', 'better_exposed_filters') . '/better_exposed_filters.css'); + } + + // Check for secondary elements. + if ($allow_secondary && !empty($secondary)) { + // Add secondary elements after regular exposed filter elements. + $remaining = array_splice($form, count($form['#info']) + 1); + $form['secondary'] = $secondary; + $form = array_merge($form, $remaining); + $form['#info']['filter-secondary']['value'] = 'secondary'; + } + } + + /** + * Fills in missing settings with default values. + * + * Similar to array_merge_recursive, but later numeric keys overwrites earlier + * values. Use this to set defaults for missing values in a multi-dimensional + * array. Eg: + * + * $existing = $this->_bef_set_defaults($defaults, $existing); + * + * @return array + * The resulting settings array + */ + function _bef_set_defaults() { + $count = func_num_args(); + if (!$count) { + return; + } + elseif (1 == $count) { + return (func_get_arg(0)); + } + + // First array is the default values. + $params = func_get_args(); + $return = array_shift($params); + + // Merge the rest of the arrays onto the default array. + foreach ($params as $array) { + foreach ($array as $key => $value) { + // Numeric keyed values are added (unless already there). + if (is_numeric($key) && !in_array($value, $return)) { + if (is_array($value)) { + $return[] = $this->_bef_set_defaults($return[$key], $value); + } + else { + $return[] = $value; + } + } + // String keyed values are replaced. + else { + if (isset($return[$key]) && is_array($value) && is_array($return[$key])) { + $return[$key] = $this->_bef_set_defaults($return[$key], $value); + } + else { + $return[$key] = $value; + } + } + } + } + return $return; + } + + /** + * Updates legacy settings to their current location. + * + * @param array $settings + * Array of BEF settings. + */ + function _bef_update_legacy_settings($settings) { + // There has got to be a better way... But for now, this works. + if (isset($settings['sort']['collapsible'])) { + $settings['sort']['advanced']['collapsible'] = $settings['sort']['collapsible']; + unset($settings['sort']['collapsible']); + } + if (isset($settings['sort']['collapsible_label'])) { + $settings['sort']['advanced']['collapsible_label'] = $settings['sort']['collapsible_label']; + unset($settings['sort']['collapsible_label']); + } + if (isset($settings['sort']['combine'])) { + $settings['sort']['advanced']['combine'] = $settings['sort']['combine']; + unset($settings['sort']['combine']); + } + if (isset($settings['sort']['reset'])) { + $settings['sort']['advanced']['reset'] = $settings['sort']['reset']; + unset($settings['sort']['reset']); + } + if (isset($settings['sort']['reset_label'])) { + $settings['sort']['advanced']['reset_label'] = $settings['sort']['reset_label']; + unset($settings['sort']['reset_label']); + } + + // Input required was originally stored in Views' options, not in BEF's + // settings. + if (isset($this->options['input_required'])) { + $settings['general']['input_required'] = $this->options['input_required']; + } + if (isset($this->options['input_required']['text_input_required'])) { + $settings['general']['input_required']['text_input_required']['text_input_required'] = $this->options['text_input_required']; + } + if (isset($this->options['input_required']['text_input_required_format'])) { + $settings['general']['input_required']['text_input_required']['text_input_required_format'] = $this->options['text_input_required_format']; + } + + return $settings; + } + + /** + * Returns an array of default or current existing values for BEF settings. + * + * This helps us as we add new options and prevents a lot of + * @code + * if (isset($settings['new_settings'])) { ... } + * @endcode + * as there will be a default value at all positions in the settings array. + * Also updates legacy settings to their new locations via + * _bef_update_legacy_settings(). + * + * @return array + * Multi-dimensional settings array. + */ + function _bef_get_settings() { + // General, sort, pagers, etc. + $defaults = array( + 'general' => array( + 'input_required' => FALSE, + 'text_input_required' => array( + 'text_input_required' => array( + 'value' => t('Select any filter and click on Apply to see results'), + 'format' => filter_default_format(), + ), + ), + 'allow_secondary' => FALSE, + 'secondary_label' => t('Advanced options'), + 'secondary_collapse_override' => 0, + ), + 'sort' => array( + 'bef_format' => 'default', + 'advanced' => array( + 'collapsible' => FALSE, + 'collapsible_label' => '', + 'combine' => FALSE, + 'combine_param' => 'sort_bef_combine', + 'combine_rewrite' => '', + 'reset' => FALSE, + 'reset_label' => '', + 'is_secondary' => FALSE, + 'autosubmit' => FALSE, + ), + ), + 'pager' => array( + 'bef_format' => 'default', + 'is_secondary' => FALSE, + ), + ); + + // Update legacy settings in the exposed form settings form. This + // keep us from losing settings when an option is put into an + // 'advanced options' fieldset. + $current = $this->_bef_update_legacy_settings($this->options['bef']); + + // Collect existing values or use defaults. + $settings = $this->_bef_set_defaults($defaults, $current); + + // Filter default values. + $filter_defaults = array( + 'bef_format' => 'default', + 'more_options' => array( + 'bef_select_all_none' => FALSE, + 'bef_select_all_none_nested' => FALSE, + 'bef_collapsible' => FALSE, + 'is_secondary' => FALSE, + 'bef_filter_description' => '', + 'any_label' => '', + 'bef_term_description' => FALSE, + 'autosubmit' => FALSE, + 'tokens' => array( + 'list' => array(), + 'available' => array(), + ), + 'rewrite' => array( + 'filter_rewrite_values' => '', + ), + 'datepicker_options' => '', + ), + 'slider_options' => array( + 'bef_slider_min' => 0, + 'bef_slider_max' => 99999, + 'bef_slider_step' => 1, + 'bef_slider_animate' => '', + 'bef_slider_orientation' => 'horizontal', + ), + ); + + // Go through each exposed filter and collect settings. + foreach ($this->display->handler->get_handlers('filter') as $label => $filter) { + if (!$filter->options['exposed']) { + continue; + } + + // Get existing values or use defaults. + $filter_current = isset($this->options['bef'][$label]) ? $this->options['bef'][$label] : array(); + $settings[$label] = $this->_bef_set_defaults($filter_defaults, $filter_current); + } + return $settings; + } + + /** + * Utility function to determine if any filters have been applied. + * Borrowed from views_plugin_exposed_form_input_required + */ + function exposed_filter_applied() { + static $cache = NULL; + if (!isset($cache)) { + $view = $this->view; + if (is_array($view->filter) && count($view->filter)) { + foreach ($view->filter as $filter_id => $filter) { + if ($filter->is_exposed()) { + $identifier = $filter->options['expose']['identifier']; + if (isset($view->exposed_input[$identifier])) { + if (!empty($view->exposed_input[$identifier])) { + $cache = TRUE; + return $cache; + } + } + } + } + } + $cache = FALSE; + } + + return $cache; + } + + /** + * Pre render callback to append the 'no values found' text if input required + * options is enabled. + */ + function pre_render($values) { + $bef_settings = $this->_bef_get_settings(); + if (!$this->exposed_filter_applied() && !empty($bef_settings['general']['input_required'])) { + $options = array( + 'id' => 'area', + 'table' => 'views', + 'field' => 'area', + 'label' => '', + 'relationship' => 'none', + 'group_type' => 'group', + 'content' => $bef_settings['general']['text_input_required']['text_input_required']['value'], + 'format' => $bef_settings['general']['text_input_required']['text_input_required']['format'], + 'empty' => TRUE, + ); + $handler = views_get_handler('views', 'area', 'area'); + $handler->init($this->view, $options); + $this->display->handler->handlers['empty'] = array( + 'area' => $handler, + ); + $this->display->handler->set_option('empty', array('text' => $options)); + } + } + + /** + * Query callback, intervenes if no filters are applied and input is required. + */ + function query() { + $bef_settings = $this->_bef_get_settings(); + if (!$this->exposed_filter_applied() && !empty($bef_settings['general']['input_required'])) { + // We return with no query; this will force the empty text. + $this->view->built = TRUE; + $this->view->executed = TRUE; + $this->view->result = array(); + } + else { + parent::query(); + } + } + + function unpack_translatable_options(&$translatable, $storage, $option, $definition, $parents) { + foreach ($this->unpack_translatable_keys() as $key) { + $value = drupal_array_get_nested_value($this->options, $key); + if (!empty($value)) { + $translatable[] = array( + 'value' => $value, + 'keys' => $key, + 'format' => NULL, + ); + } + } + } + + function unpack_translatable_keys() { + // Default options. + $keys = array( + // @TODO: Do we need to give this a better key so it makes more sense in + // the localization UI? + 'value' => array('bef', 'general', 'text_input_required', 'text_input_required', 'value'), + + 'general_secondary_label' => array('bef', 'general', 'secondary_label'), + 'sort_collapsible_label' => array('bef', 'sort', 'advanced', 'collapsible_label'), + 'sort_combine_rewrite' => array('bef', 'sort', 'advanced', 'combine_rewrite'), + 'sort_reset_label' => array('bef', 'sort', 'advanced', 'reset_label'), + ); + + // Exposed filter options. + foreach ($this->display->handler->get_handlers('filter') as $label => $filter) { + if (!$filter->options['exposed']) { + continue; + } + $keys[$label . '_filter_description'] = array('bef', $label, 'more_options', 'bef_filter_description'); + $keys[$label . '_any_label'] = array('bef', $label, 'more_options', 'any_label'); + $keys[$label . '_rewrite_values'] = array('bef', $label, 'more_options', 'rewrite', 'filter_rewrite_values'); + $keys[$label . '_datepicker_options'] = array('bef', $label, 'more_options', 'datepicker_options'); + } + + return $keys; + } + + function unpack_options(&$storage, $options, $definition = NULL, $all = TRUE, $check = TRUE, $localization_keys = array()) { + parent::unpack_options($storage, $options, $definition, $all, $check, $localization_keys); + + // Override the values. + foreach ($this->localization_keys as $key => $parents) { + $parent = end($parents); + $value = isset($options[$parent]) ? $options[$parent] : NULL; + + // Don't localize strings during editing. When editing, we need to work with + // the original data, not the translated version. + if (empty($this->view->editing) && !empty($value)) { + if (!empty($this->view) && $this->view->is_translatable()) { + // Allow other modules to make changes to the string before it's + // sent for translation. + // The $keys array is built from the bef form array structure. + $format = NULL; + $translation_data = array( + 'value' => $value, + 'format' => $format, + 'keys' => $parents, + ); + $storage[$parent] = $this->view->localization_plugin->translate($translation_data); + } + // Otherwise, this is a code-based string, so we can use t(). + else { + $storage[$parent] = t($value); + } + } + else if (!empty($value)) { + $storage[$parent] = $value; + } + } + } +} diff --git a/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.features.field_base.inc b/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.features.field_base.inc new file mode 100644 index 0000000..234588f --- /dev/null +++ b/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.features.field_base.inc @@ -0,0 +1,184 @@ + 1, + 'cardinality' => 1, + 'deleted' => 0, + 'entity_types' => array(), + 'field_name' => 'field_bef_test_body', + 'indexes' => array( + 'format' => array( + 0 => 'format', + ), + ), + 'locked' => 0, + 'module' => 'text', + 'settings' => array(), + 'translatable' => 0, + 'type' => 'text_with_summary', + ); + + // Exported field_base: 'field_bef_test_date' + $field_bases['field_bef_test_date'] = array( + 'active' => 1, + 'cardinality' => 1, + 'deleted' => 0, + 'entity_types' => array(), + 'field_name' => 'field_bef_test_date', + 'indexes' => array(), + 'locked' => 0, + 'module' => 'date', + 'settings' => array( + 'cache_count' => 4, + 'cache_enabled' => 0, + 'granularity' => array( + 'day' => 'day', + 'hour' => 0, + 'minute' => 0, + 'month' => 'month', + 'second' => 0, + 'year' => 'year', + ), + 'timezone_db' => '', + 'todate' => '', + 'tz_handling' => 'none', + ), + 'translatable' => 0, + 'type' => 'datetime', + ); + + // Exported field_base: 'field_bef_test_integer' + $field_bases['field_bef_test_integer'] = array( + 'active' => 1, + 'cardinality' => 1, + 'deleted' => 0, + 'entity_types' => array(), + 'field_name' => 'field_bef_test_integer', + 'indexes' => array(), + 'locked' => 0, + 'module' => 'number', + 'settings' => array(), + 'translatable' => 0, + 'type' => 'number_integer', + ); + + // Exported field_base: 'field_bef_test_letters' + $field_bases['field_bef_test_letters'] = array( + 'active' => 1, + 'cardinality' => -1, + 'deleted' => 0, + 'entity_types' => array(), + 'field_name' => 'field_bef_test_letters', + 'indexes' => array( + 'value' => array( + 0 => 'value', + ), + ), + 'locked' => 0, + 'module' => 'list', + 'settings' => array( + 'allowed_values' => array( + 'a' => 'Aardvark', + 'b' => 'Bumble-Bee', + 'c' => ' Caterpillar', + 'd' => 'Devil, Tasmanian', + 'e' => 'Elephant', + 'f' => 'Frigatebird', + 'g' => 'Giraffe', + 'h' => 'Horse', + ), + 'allowed_values_function' => '', + ), + 'translatable' => 0, + 'type' => 'list_text', + ); + + // Exported field_base: 'field_bef_test_location' + $field_bases['field_bef_test_location'] = array( + 'active' => 1, + 'cardinality' => 1, + 'deleted' => 0, + 'entity_types' => array(), + 'field_name' => 'field_bef_test_location', + 'indexes' => array( + 'tid' => array( + 0 => 'tid', + ), + ), + 'locked' => 0, + 'module' => 'taxonomy', + 'settings' => array( + 'allowed_values' => array( + 0 => array( + 'vocabulary' => 'taxonomy-bef_test-location', + 'parent' => 0, + ), + ), + ), + 'translatable' => 0, + 'type' => 'taxonomy_term_reference', + ); + + // Exported field_base: 'field_bef_test_numbers' + $field_bases['field_bef_test_numbers'] = array( + 'active' => 1, + 'cardinality' => 1, + 'deleted' => 0, + 'entity_types' => array(), + 'field_name' => 'field_bef_test_numbers', + 'indexes' => array( + 'value' => array( + 0 => 'value', + ), + ), + 'locked' => 0, + 'module' => 'list', + 'settings' => array( + 'allowed_values' => array( + 1 => 'One', + 2 => 'Two', + 3 => 'Three', + 4 => 'Four', + 5 => 'Five', + 6 => 'Six', + 7 => 'Seven', + 8 => 'Eight', + 9 => 'Nine', + 10 => 'Ten', + ), + 'allowed_values_function' => '', + ), + 'translatable' => 0, + 'type' => 'list_integer', + ); + + // Exported field_base: 'field_bef_test_price' + $field_bases['field_bef_test_price'] = array( + 'active' => 1, + 'cardinality' => 1, + 'deleted' => 0, + 'entity_types' => array(), + 'field_name' => 'field_bef_test_price', + 'indexes' => array(), + 'locked' => 0, + 'module' => 'number', + 'settings' => array( + 'decimal_separator' => '.', + ), + 'translatable' => 0, + 'type' => 'number_float', + ); + + return $field_bases; +} diff --git a/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.features.field_instance.inc b/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.features.field_instance.inc new file mode 100644 index 0000000..72b897b --- /dev/null +++ b/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.features.field_instance.inc @@ -0,0 +1,322 @@ + 'bef_test', + 'default_value' => NULL, + 'deleted' => 0, + 'description' => '', + 'display' => array( + 'default' => array( + 'label' => 'above', + 'module' => 'text', + 'settings' => array(), + 'type' => 'text_default', + 'weight' => 12, + ), + 'teaser' => array( + 'label' => 'above', + 'settings' => array(), + 'type' => 'hidden', + 'weight' => 0, + ), + ), + 'entity_type' => 'node', + 'field_name' => 'field_bef_test_body', + 'label' => 'Body', + 'required' => 0, + 'settings' => array( + 'display_summary' => 0, + 'text_processing' => 1, + 'user_register_form' => FALSE, + ), + 'widget' => array( + 'active' => 1, + 'module' => 'text', + 'settings' => array( + 'rows' => 20, + 'summary_rows' => 5, + ), + 'type' => 'text_textarea_with_summary', + 'weight' => 7, + ), + ); + + // Exported field_instance: 'node-bef_test-field_bef_test_date' + $field_instances['node-bef_test-field_bef_test_date'] = array( + 'bundle' => 'bef_test', + 'deleted' => 0, + 'description' => '', + 'display' => array( + 'default' => array( + 'label' => 'above', + 'module' => 'date', + 'settings' => array( + 'format_type' => 'long', + 'fromto' => 'both', + 'multiple_from' => '', + 'multiple_number' => '', + 'multiple_to' => '', + ), + 'type' => 'date_default', + 'weight' => 11, + ), + 'teaser' => array( + 'label' => 'above', + 'settings' => array(), + 'type' => 'hidden', + 'weight' => 0, + ), + ), + 'entity_type' => 'node', + 'field_name' => 'field_bef_test_date', + 'label' => 'Date', + 'required' => 0, + 'settings' => array( + 'default_value' => 'now', + 'default_value2' => 'same', + 'default_value_code' => '', + 'default_value_code2' => '', + 'user_register_form' => FALSE, + ), + 'widget' => array( + 'active' => 1, + 'module' => 'date', + 'settings' => array( + 'increment' => 15, + 'input_format' => 'm/d/Y - H:i:s', + 'input_format_custom' => '', + 'label_position' => 'above', + 'text_parts' => array(), + 'year_range' => '-3:+3', + ), + 'type' => 'date_text', + 'weight' => 6, + ), + ); + + // Exported field_instance: 'node-bef_test-field_bef_test_integer' + $field_instances['node-bef_test-field_bef_test_integer'] = array( + 'bundle' => 'bef_test', + 'default_value' => NULL, + 'deleted' => 0, + 'description' => '', + 'display' => array( + 'default' => array( + 'label' => 'above', + 'module' => 'number', + 'settings' => array( + 'decimal_separator' => '.', + 'prefix_suffix' => TRUE, + 'scale' => 0, + 'thousand_separator' => ' ', + ), + 'type' => 'number_integer', + 'weight' => 10, + ), + 'teaser' => array( + 'label' => 'above', + 'settings' => array(), + 'type' => 'hidden', + 'weight' => 0, + ), + ), + 'entity_type' => 'node', + 'field_name' => 'field_bef_test_integer', + 'label' => 'Integer', + 'required' => 0, + 'settings' => array( + 'max' => 5000, + 'min' => 100, + 'prefix' => '', + 'suffix' => '', + 'user_register_form' => FALSE, + ), + 'widget' => array( + 'active' => 0, + 'module' => 'number', + 'settings' => array(), + 'type' => 'number', + 'weight' => 4, + ), + ); + + // Exported field_instance: 'node-bef_test-field_bef_test_letters' + $field_instances['node-bef_test-field_bef_test_letters'] = array( + 'bundle' => 'bef_test', + 'default_value' => NULL, + 'deleted' => 0, + 'description' => '', + 'display' => array( + 'default' => array( + 'label' => 'above', + 'module' => 'list', + 'settings' => array(), + 'type' => 'list_default', + 'weight' => 7, + ), + 'teaser' => array( + 'label' => 'above', + 'settings' => array(), + 'type' => 'hidden', + 'weight' => 0, + ), + ), + 'entity_type' => 'node', + 'field_name' => 'field_bef_test_letters', + 'label' => 'Letters', + 'required' => 0, + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'widget' => array( + 'active' => 1, + 'module' => 'options', + 'settings' => array(), + 'type' => 'options_buttons', + 'weight' => 1, + ), + ); + + // Exported field_instance: 'node-bef_test-field_bef_test_location' + $field_instances['node-bef_test-field_bef_test_location'] = array( + 'bundle' => 'bef_test', + 'default_value' => NULL, + 'deleted' => 0, + 'description' => '', + 'display' => array( + 'default' => array( + 'label' => 'above', + 'module' => 'taxonomy', + 'settings' => array(), + 'type' => 'taxonomy_term_reference_link', + 'weight' => 9, + ), + 'teaser' => array( + 'label' => 'above', + 'settings' => array(), + 'type' => 'hidden', + 'weight' => 0, + ), + ), + 'entity_type' => 'node', + 'field_name' => 'field_bef_test_location', + 'label' => 'Location', + 'required' => 0, + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'widget' => array( + 'active' => 1, + 'module' => 'options', + 'settings' => array(), + 'type' => 'options_buttons', + 'weight' => 3, + ), + ); + + // Exported field_instance: 'node-bef_test-field_bef_test_numbers' + $field_instances['node-bef_test-field_bef_test_numbers'] = array( + 'bundle' => 'bef_test', + 'default_value' => NULL, + 'deleted' => 0, + 'description' => '', + 'display' => array( + 'default' => array( + 'label' => 'above', + 'module' => 'list', + 'settings' => array(), + 'type' => 'list_default', + 'weight' => 8, + ), + 'teaser' => array( + 'label' => 'above', + 'settings' => array(), + 'type' => 'hidden', + 'weight' => 0, + ), + ), + 'entity_type' => 'node', + 'field_name' => 'field_bef_test_numbers', + 'label' => 'Numbers', + 'required' => 0, + 'settings' => array( + 'user_register_form' => FALSE, + ), + 'widget' => array( + 'active' => 1, + 'module' => 'options', + 'settings' => array(), + 'type' => 'options_select', + 'weight' => 2, + ), + ); + + // Exported field_instance: 'node-bef_test-field_bef_test_price' + $field_instances['node-bef_test-field_bef_test_price'] = array( + 'bundle' => 'bef_test', + 'default_value' => NULL, + 'deleted' => 0, + 'description' => '', + 'display' => array( + 'default' => array( + 'label' => 'above', + 'module' => 'number', + 'settings' => array( + 'decimal_separator' => '.', + 'prefix_suffix' => TRUE, + 'scale' => 2, + 'thousand_separator' => ' ', + ), + 'type' => 'number_decimal', + 'weight' => 13, + ), + 'teaser' => array( + 'label' => 'above', + 'settings' => array(), + 'type' => 'hidden', + 'weight' => 0, + ), + ), + 'entity_type' => 'node', + 'field_name' => 'field_bef_test_price', + 'label' => 'Price', + 'required' => 0, + 'settings' => array( + 'max' => '', + 'min' => '', + 'prefix' => '$', + 'suffix' => '', + 'user_register_form' => FALSE, + ), + 'widget' => array( + 'active' => 0, + 'module' => 'number', + 'settings' => array(), + 'type' => 'number', + 'weight' => 5, + ), + ); + + // Translatables + // Included for use with string extractors like potx. + t('Body'); + t('Date'); + t('Integer'); + t('Letters'); + t('Location'); + t('Numbers'); + t('Price'); + + return $field_instances; +} diff --git a/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.features.inc b/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.features.inc new file mode 100644 index 0000000..ff5d275 --- /dev/null +++ b/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.features.inc @@ -0,0 +1,30 @@ + "3.0"); +} + +/** + * Implements hook_node_info(). + */ +function bef_test_content_node_info() { + $items = array( + 'bef_test' => array( + 'name' => t('bef_test'), + 'base' => 'node_content', + 'description' => '', + 'has_title' => '1', + 'title_label' => t('Title'), + 'help' => '', + ), + ); + drupal_alter('node_info', $items); + return $items; +} diff --git a/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.features.taxonomy.inc b/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.features.taxonomy.inc new file mode 100644 index 0000000..bbbe7ef --- /dev/null +++ b/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.features.taxonomy.inc @@ -0,0 +1,36 @@ + array( + 'name' => 'Location', + 'machine_name' => 'taxonomy-bef_test-location', + 'description' => '', + 'hierarchy' => 1, + 'module' => 'taxonomy', + 'weight' => 0, + 'rdf_mapping' => array( + 'rdftype' => array( + 0 => 'skos:ConceptScheme', + ), + 'name' => array( + 'predicates' => array( + 0 => 'dc:title', + ), + ), + 'description' => array( + 'predicates' => array( + 0 => 'rdfs:comment', + ), + ), + ), + ), + ); +} diff --git a/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.info b/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.info new file mode 100644 index 0000000..6305b8d --- /dev/null +++ b/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.info @@ -0,0 +1,49 @@ +name = BEF Test Content +description = Content and Views for testing Better Exposed Filters. +core = 7.x +package = BEF Tests +dependencies[] = better_exposed_filters +dependencies[] = date +dependencies[] = features +dependencies[] = list +dependencies[] = number +dependencies[] = taxonomy +dependencies[] = text +dependencies[] = views +dependencies[] = views_content +features[ctools][] = views:views_default:3.0 +features[features_api][] = api:2 +features[field_base][] = field_bef_test_body +features[field_base][] = field_bef_test_date +features[field_base][] = field_bef_test_integer +features[field_base][] = field_bef_test_letters +features[field_base][] = field_bef_test_location +features[field_base][] = field_bef_test_numbers +features[field_base][] = field_bef_test_price +features[field_instance][] = node-bef_test-field_bef_test_body +features[field_instance][] = node-bef_test-field_bef_test_date +features[field_instance][] = node-bef_test-field_bef_test_integer +features[field_instance][] = node-bef_test-field_bef_test_letters +features[field_instance][] = node-bef_test-field_bef_test_location +features[field_instance][] = node-bef_test-field_bef_test_numbers +features[field_instance][] = node-bef_test-field_bef_test_price +features[node][] = bef_test +features[taxonomy][] = taxonomy-bef_test-location +features[views_view][] = bef_test_view +features_exclude[dependencies][ctools] = ctools +features_exclude[dependencies][options] = options +features_exclude[field][node-bef_test-body] = node-bef_test-body +features_exclude[field][node-bef_test-field_letters] = node-bef_test-field_letters +features_exclude[field][node-bef_test-field_numbers] = node-bef_test-field_numbers +features_exclude[field][node-bef_test-field_location] = node-bef_test-field_location +features_exclude[field][node-bef_test-field_integer] = node-bef_test-field_integer +features_exclude[field][node-bef_test-field_price] = node-bef_test-field_price +features_exclude[field][node-bef_test-field_date] = node-bef_test-field_date +hidden = 1 + +; Information added by Drupal.org packaging script on 2017-01-13 +version = "7.x-3.3" +core = "7.x" +project = "better_exposed_filters" +datestamp = "1484287088" + diff --git a/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.install b/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.install new file mode 100644 index 0000000..ed3b317 --- /dev/null +++ b/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.install @@ -0,0 +1,81 @@ + array( + 'California' => array( + 'San Francisco', + 'San Diego', + 'Santa Barbara', + ), + 'Oregon' => array( + 'Portland', + 'Eugene', + ), + 'Washington' => array( + 'Seattle', + 'Spokane', + 'Walla Walla', + ), + ), + 'Canada' => array( + 'British Columbia' => array( + 'Vancouver', + 'Victoria', + 'Whistler', + ), + 'Alberta' => array( + 'Calgary', + 'Edmonton', + 'Lake Louise', + ), + ), + 'Mexico' => array(), + ); + foreach ($locations as $country => $states) { + $country_tid = _bef_test_content_add_term($country); + if ($country_tid && !empty($states)) { + foreach ($states as $state => $cities) { + $state_tid = _bef_test_content_add_term($state, $country_tid); + if ($state_tid && !empty($cities)) { + foreach ($cities as $city) { + _bef_test_content_add_term($city, $state_tid); + } + } + } + } + } +} + +/** + * Adds a new term to the bef_test-location vocabulary. If a TID is specified + * in $parent, the new term is added as a child of that term. + * + * @param string $name + * The name of the new term. + * @param int $parent + * The (optional) TID of the parent term. + * + * @return int + * TID of the newly created term or 0 on an error. + */ +function _bef_test_content_add_term($name, $parent = 0) { + $term = new stdClass(); + // Features manages to create a vocab machine name that includes illegal + // characters (taxonomy-bef_test-location -- the hyphen is not allowed). So + // we use the VID of the vocab instead. + $term->vid = 2; + $term->parent = $parent; + $term->name = $name; + if (taxonomy_term_save($term) == SAVED_NEW) { + return $term->tid; + } + return 0; +} diff --git a/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.module b/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.module new file mode 100644 index 0000000..785a248 --- /dev/null +++ b/sites/all/modules/better_exposed_filters/tests/bef_test_content/bef_test_content.module @@ -0,0 +1,7 @@ +name = 'bef_test_view'; + $view->description = ''; + $view->tag = 'default'; + $view->base_table = 'node'; + $view->human_name = 'BEF Test View'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['title'] = 'BEF Test View'; + $handler->display->display_options['use_more_always'] = FALSE; + $handler->display->display_options['access']['type'] = 'perm'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['exposed_form']['type'] = 'better_exposed_filters'; + $handler->display->display_options['exposed_form']['options']['bef'] = array( + 'general' => array( + 'allow_secondary' => 0, + 'secondary_label' => 'Advanced options', + 'secondary_collapse_override' => 0, + ), + 'sort' => array( + 'bef_format' => 'bef_toggle_links', + 'advanced' => array( + 'collapsible' => 0, + 'collapsible_label' => 'Sort options', + 'combine' => 1, + 'combine_rewrite' => '', + 'reset' => 0, + 'reset_label' => '', + 'is_secondary' => 0, + ), + ), + 'pager' => array( + 'bef_format' => 'bef_links', + 'is_secondary' => 0, + ), + 'field_bef_test_letters_value' => array( + 'bef_format' => 'default', + 'more_options' => array( + 'bef_select_all_none' => FALSE, + 'bef_collapsible' => 0, + 'is_secondary' => 0, + 'any_label' => 'Any Letter', + 'bef_filter_description' => '', + 'tokens' => array( + 'available' => array( + 0 => 'global_types', + ), + ), + 'rewrite' => array( + 'filter_rewrite_values' => '', + ), + ), + ), + 'field_bef_test_numbers_value' => array( + 'bef_format' => 'default', + 'more_options' => array( + 'bef_select_all_none' => FALSE, + 'bef_collapsible' => 0, + 'is_secondary' => 0, + 'any_label' => 'Any Number', + 'bef_filter_description' => '', + 'tokens' => array( + 'available' => array( + 0 => 'global_types', + ), + ), + 'rewrite' => array( + 'filter_rewrite_values' => '', + ), + ), + ), + 'type' => array( + 'bef_format' => 'default', + 'more_options' => array( + 'bef_select_all_none' => FALSE, + 'bef_collapsible' => 0, + 'is_secondary' => 0, + 'any_label' => 'Any Type', + 'bef_filter_description' => '', + 'tokens' => array( + 'available' => array( + 0 => 'global_types', + ), + ), + 'rewrite' => array( + 'filter_rewrite_values' => '', + ), + ), + ), + 'field_bef_test_location_tid' => array( + 'bef_format' => 'default', + 'more_options' => array( + 'bef_select_all_none' => FALSE, + 'bef_select_all_none_nested' => FALSE, + 'bef_collapsible' => 0, + 'is_secondary' => 0, + 'any_label' => 'Anywhere', + 'bef_filter_description' => '', + 'tokens' => array( + 'available' => array( + 0 => 'global_types', + ), + ), + 'rewrite' => array( + 'filter_rewrite_values' => '', + ), + ), + ), + ); + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = '10'; + $handler->display->display_options['pager']['options']['offset'] = '0'; + $handler->display->display_options['pager']['options']['id'] = '0'; + $handler->display->display_options['pager']['options']['quantity'] = '9'; + $handler->display->display_options['pager']['options']['expose']['items_per_page'] = TRUE; + $handler->display->display_options['pager']['options']['expose']['items_per_page_label'] = 'Yo! Show me'; + $handler->display->display_options['pager']['options']['expose']['items_per_page_options_all'] = TRUE; + $handler->display->display_options['pager']['options']['expose']['items_per_page_options_all_label'] = 'All dem things!'; + $handler->display->display_options['style_plugin'] = 'table'; + $handler->display->display_options['style_options']['columns'] = array( + 'title' => 'title', + 'field_letters' => 'field_letters', + 'field_numbers' => 'field_numbers', + ); + $handler->display->display_options['style_options']['default'] = '-1'; + $handler->display->display_options['style_options']['info'] = array( + 'title' => array( + 'sortable' => 0, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'field_letters' => array( + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'field_numbers' => array( + 'sortable' => 0, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + ); + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['label'] = ''; + $handler->display->display_options['fields']['title']['alter']['word_boundary'] = FALSE; + $handler->display->display_options['fields']['title']['alter']['ellipsis'] = FALSE; + /* Field: Content: Post date */ + $handler->display->display_options['fields']['created']['id'] = 'created'; + $handler->display->display_options['fields']['created']['table'] = 'node'; + $handler->display->display_options['fields']['created']['field'] = 'created'; + $handler->display->display_options['fields']['created']['date_format'] = 'short'; + /* Field: Content: Integer */ + $handler->display->display_options['fields']['field_bef_test_integer']['id'] = 'field_bef_test_integer'; + $handler->display->display_options['fields']['field_bef_test_integer']['table'] = 'field_data_field_bef_test_integer'; + $handler->display->display_options['fields']['field_bef_test_integer']['field'] = 'field_bef_test_integer'; + $handler->display->display_options['fields']['field_bef_test_integer']['settings'] = array( + 'thousand_separator' => ' ', + 'prefix_suffix' => 1, + ); + /* Field: Content: Letters */ + $handler->display->display_options['fields']['field_bef_test_letters']['id'] = 'field_bef_test_letters'; + $handler->display->display_options['fields']['field_bef_test_letters']['table'] = 'field_data_field_bef_test_letters'; + $handler->display->display_options['fields']['field_bef_test_letters']['field'] = 'field_bef_test_letters'; + $handler->display->display_options['fields']['field_bef_test_letters']['delta_offset'] = '0'; + /* Field: Content: Numbers */ + $handler->display->display_options['fields']['field_bef_test_numbers']['id'] = 'field_bef_test_numbers'; + $handler->display->display_options['fields']['field_bef_test_numbers']['table'] = 'field_data_field_bef_test_numbers'; + $handler->display->display_options['fields']['field_bef_test_numbers']['field'] = 'field_bef_test_numbers'; + /* Sort criterion: Content: Post date */ + $handler->display->display_options['sorts']['created']['id'] = 'created'; + $handler->display->display_options['sorts']['created']['table'] = 'node'; + $handler->display->display_options['sorts']['created']['field'] = 'created'; + $handler->display->display_options['sorts']['created']['order'] = 'DESC'; + $handler->display->display_options['sorts']['created']['exposed'] = TRUE; + $handler->display->display_options['sorts']['created']['expose']['label'] = 'Post date'; + /* Filter criterion: Content: Letters (field_bef_test_letters) */ + $handler->display->display_options['filters']['field_bef_test_letters_value']['id'] = 'field_bef_test_letters_value'; + $handler->display->display_options['filters']['field_bef_test_letters_value']['table'] = 'field_data_field_bef_test_letters'; + $handler->display->display_options['filters']['field_bef_test_letters_value']['field'] = 'field_bef_test_letters_value'; + $handler->display->display_options['filters']['field_bef_test_letters_value']['exposed'] = TRUE; + $handler->display->display_options['filters']['field_bef_test_letters_value']['expose']['operator_id'] = 'field_bef_test_letters_value_op'; + $handler->display->display_options['filters']['field_bef_test_letters_value']['expose']['label'] = 'Letters'; + $handler->display->display_options['filters']['field_bef_test_letters_value']['expose']['operator'] = 'field_bef_test_letters_value_op'; + $handler->display->display_options['filters']['field_bef_test_letters_value']['expose']['identifier'] = 'field_bef_test_letters_value'; + $handler->display->display_options['filters']['field_bef_test_letters_value']['expose']['remember_roles'] = array( + 2 => '2', + 1 => 0, + 3 => 0, + ); + /* Filter criterion: Content: Numbers (field_bef_test_numbers) */ + $handler->display->display_options['filters']['field_bef_test_numbers_value']['id'] = 'field_bef_test_numbers_value'; + $handler->display->display_options['filters']['field_bef_test_numbers_value']['table'] = 'field_data_field_bef_test_numbers'; + $handler->display->display_options['filters']['field_bef_test_numbers_value']['field'] = 'field_bef_test_numbers_value'; + $handler->display->display_options['filters']['field_bef_test_numbers_value']['exposed'] = TRUE; + $handler->display->display_options['filters']['field_bef_test_numbers_value']['expose']['operator_id'] = 'field_bef_test_numbers_value_op'; + $handler->display->display_options['filters']['field_bef_test_numbers_value']['expose']['label'] = 'Numbers'; + $handler->display->display_options['filters']['field_bef_test_numbers_value']['expose']['operator'] = 'field_bef_test_numbers_value_op'; + $handler->display->display_options['filters']['field_bef_test_numbers_value']['expose']['identifier'] = 'field_bef_test_numbers_value'; + $handler->display->display_options['filters']['field_bef_test_numbers_value']['expose']['remember_roles'] = array( + 2 => '2', + 1 => 0, + 3 => 0, + ); + /* Filter criterion: Content: Type */ + $handler->display->display_options['filters']['type']['id'] = 'type'; + $handler->display->display_options['filters']['type']['table'] = 'node'; + $handler->display->display_options['filters']['type']['field'] = 'type'; + $handler->display->display_options['filters']['type']['exposed'] = TRUE; + $handler->display->display_options['filters']['type']['expose']['operator_id'] = 'type_op'; + $handler->display->display_options['filters']['type']['expose']['label'] = 'Type'; + $handler->display->display_options['filters']['type']['expose']['operator'] = 'type_op'; + $handler->display->display_options['filters']['type']['expose']['identifier'] = 'type'; + $handler->display->display_options['filters']['type']['expose']['remember_roles'] = array( + 2 => '2', + 1 => 0, + 3 => 0, + ); + /* Filter criterion: Content: Location (field_bef_test_location) */ + $handler->display->display_options['filters']['field_bef_test_location_tid']['id'] = 'field_bef_test_location_tid'; + $handler->display->display_options['filters']['field_bef_test_location_tid']['table'] = 'field_data_field_bef_test_location'; + $handler->display->display_options['filters']['field_bef_test_location_tid']['field'] = 'field_bef_test_location_tid'; + $handler->display->display_options['filters']['field_bef_test_location_tid']['exposed'] = TRUE; + $handler->display->display_options['filters']['field_bef_test_location_tid']['expose']['operator_id'] = 'field_bef_test_location_tid_op'; + $handler->display->display_options['filters']['field_bef_test_location_tid']['expose']['label'] = 'Location'; + $handler->display->display_options['filters']['field_bef_test_location_tid']['expose']['operator'] = 'field_bef_test_location_tid_op'; + $handler->display->display_options['filters']['field_bef_test_location_tid']['expose']['identifier'] = 'field_bef_test_location_tid'; + $handler->display->display_options['filters']['field_bef_test_location_tid']['expose']['remember_roles'] = array( + 2 => '2', + 1 => 0, + 3 => 0, + ); + $handler->display->display_options['filters']['field_bef_test_location_tid']['type'] = 'select'; + $handler->display->display_options['filters']['field_bef_test_location_tid']['vocabulary'] = 'taxonomy-bef_test-location'; + $handler->display->display_options['filters']['field_bef_test_location_tid']['hierarchy'] = 1; + + /* Display: Page */ + $handler = $view->new_display('page', 'Page', 'page'); + $handler->display->display_options['path'] = 'test'; + + /* Display: Content pane */ + $handler = $view->new_display('panel_pane', 'Content pane', 'panel_pane_1'); + $handler->display->display_options['exposed_block'] = TRUE; + $export['bef_test_view'] = $view; + + return $export; +} diff --git a/sites/all/modules/better_exposed_filters/tests/better_exposed_filters.test b/sites/all/modules/better_exposed_filters/tests/better_exposed_filters.test new file mode 100644 index 0000000..9784b8c --- /dev/null +++ b/sites/all/modules/better_exposed_filters/tests/better_exposed_filters.test @@ -0,0 +1,935 @@ + 'BEF Options tests', + 'description' => 'Checks that BEF options appear when should.', + 'group' => 'Better Exposed Filters', + ); + } + + /** + * Verify BEF options display for binary and unlimited option fields + */ + public function test_befOptionsExist() { + // Add filters to the default display + $this->addFilter('node.title'); + $this->addFilter('node.status'); + $this->addFilter('node.type'); + $this->drupalGet($this->getBefSettingsUrl()); + + $settings = array( + 'status_1' => 'Published', + 'type' => 'Type', + 'title' => 'Title' + ); + + foreach ($settings as $id => $label) { + $translation = array( + '!id' => $id, + '!label' => $label, + ); + $this->assertRaw( + htmlentities(t('Display "!id" (Filter label: "!label") exposed filter as', $translation)), + t('BEF options exist for exposed "!id" filter', $translation) + ); + $this->assertRaw( + htmlentities(t('More options for "!id" (Filter label: "!label")', $translation)), + t('BEF "More options" fieldset exists for exposed "!id" filter', $translation) + ); + $this->assertField( + 'edit-exposed-form-options-bef-' . str_replace('_', '-', $id) . '-bef-format', + t('BEF options dropdown exists for "!id"', $translation) + ); + } + } + + /** + * Verify BEF options are saved and redisplayed properly + */ + public function test_befOptionsSave() { + // Add filters to the default display + $this->addFilter('node.status'); + $this->addFilter('node.type'); + $this->saveView(); + + // Ensure basic BEF settings are preserved on save. + $settings = array( + 'status_1' => 'Published', + 'type' => 'Type', + ); + foreach ($settings as $id => $label) { + $edit = array( + "exposed_form_options[bef][$id][bef_format]" => 'bef', + "exposed_form_options[bef][$id][more_options][bef_filter_description]" => $this->randomName(16), + "exposed_form_options[bef][$id][more_options][bef_collapsible]" => TRUE, + ); + $this->setBefSettings($edit); + $this->saveView(); + $this->drupalGet($this->getBefSettingsUrl()); + foreach ($edit as $name => $expected) { + $this->assertFieldByName($name, $expected); + } + } + + // Ensure View-based settings are preserved on save. + $edit = array( + 'exposed_form_options[bef][general][input_required]' => 1, + 'exposed_form_options[bef][general][allow_secondary]' => 1, + ); + $this->setBefSettings($edit); + $this->saveView(); + $this->drupalGet($this->getBefSettingsUrl()); + foreach ($edit as $name => $expected) { + $this->assertFieldByName($name, $expected); + } + + // This filter cannot use BEF settings. Ensure only generic settings are + // available for open text filters. + $this->addFilter('node.title'); + $this->saveView(); + $this->drupalGet($this->getBefSettingsUrl()); + $this->assertNoFieldByName('exposed_form_options[bef][title][bef_format]', 'bef'); + $edit = array( + "exposed_form_options[bef][$id][more_options][bef_filter_description]" => $this->randomName(16), + "exposed_form_options[bef][$id][more_options][bef_collapsible]" => TRUE, + ); + $this->setBefSettings($edit); + $this->saveView(); + $this->drupalGet($this->getBefSettingsUrl()); + foreach ($edit as $name => $expected) { + $this->assertFieldByName($name, $expected); + } + } +} + +class BEF_TestRadios extends BEF_TestBase { + + /** + * Describes these tests to the testing framework. + */ + public static function getInfo() { + return array( + 'name' => 'BEF radio button tests', + 'description' => 'Verifies rendering filters as radio buttons.', + 'group' => 'Better Exposed Filters', + ); + } + + /** + * Verify BEF radio buttons are rendered correctly on a page view. + */ + public function test_befPageDisplayRadios() { + // Create a page display to validate rendering. + $this->createDisplay('Page', array( + 'path' => array('path' => 'bef_test_page'), + )); + + // Verify radio button rendering with the node type filter. + $this->addFilter('node.type'); + $this->setBefSettings(array( + 'exposed_form_options[bef][type][bef_format]' => 'bef', + )); + $this->saveView(); + + $this->drupalGet('bef_test_page'); + $this->assertText(t('Type'), 'Exposed "Type" filter title renders correctly.'); + + // Exposed filter options render as radio buttons. + $this->assertFieldByXpath('//input[@name="type" and @type="radio" and @id="edit-type-all"]'); + $this->assertFieldByXpath('//input[@name="type" and @type="radio" and @id="edit-type-article"]'); + $this->assertFieldByXpath('//input[@name="type" and @type="radio" and @id="edit-type-page"]'); + $this->assertFieldByXpath('//input[@name="type" and @type="radio" and @id="edit-type-bef-test"]'); + + // Now try it in a with the collapsible option. + $this->setBefSettings(array( + 'exposed_form_options[bef][type][more_options][bef_collapsible]' => TRUE, + )); + $this->saveView(); + $this->drupalGet('bef_test_page'); + + // Exposed filter is shown as radio buttons enclosed within a fieldset. + $this->assertFieldByXpath('//fieldset//input[@name="type" and @type="radio" and @id="edit-type-all"]'); + $this->assertFieldByXpath('//fieldset//input[@name="type" and @type="radio" and @id="edit-type-article"]'); + $this->assertFieldByXpath('//fieldset//input[@name="type" and @type="radio" and @id="edit-type-page"]'); + $this->assertFieldByXpath('//fieldset//input[@name="type" and @type="radio" and @id="edit-type-bef-test"]'); + + // Filter label is used as the fieldset legend + $this->assertFieldByXpath('//fieldset//legend[* = "Type"]'); + } +} + +class BEF_TestCheckboxes extends BEF_TestBase { + + /** + * Describes these tests to the testing framework. + */ + public static function getInfo() { + return array( + 'name' => 'BEF checkbox tests', + 'description' => 'Verifies rendering filter options as checkboxes.', + 'group' => 'Better Exposed Filters', + ); + } + + /** + * Verify that checkboxes are rendered correctly on a page display + */ + public function test_befPageDisplayCheckboxes() { + // Create a page display to validate rendering. + $this->createDisplay('Page', array( + 'path' => array('path' => 'bef_test_page'), + )); + + // Verify checkbox rendering with the node type filter. + $this->addFilter('node.type', array( + 'options[expose][multiple]' => TRUE, + )); + $this->setBefSettings(array( + 'exposed_form_options[bef][type][bef_format]' => 'bef', + )); + $this->saveView(); + + $this->drupalGet('bef_test_page'); + $this->assertText(t('Type'), 'Exposed "Type" filter title renders correctly.'); + + // Exposed filter option render as checkboxes. + $this->assertFieldByXpath('//input[@name="type[]" and @type="checkbox" and @id="edit-type-article"]'); + $this->assertFieldByXpath('//input[@name="type[]" and @type="checkbox" and @id="edit-type-page"]'); + $this->assertFieldByXpath('//input[@name="type[]" and @type="checkbox" and @id="edit-type-bef-test"]'); + + // Verify classes added for select all/none functionality. + $this->assertNoFieldByXpath('//div[contains(@class, "bef-select-all-none")]'); + $this->setBefSettings(array( + 'exposed_form_options[bef][type][more_options][bef_select_all_none]' => TRUE, + )); + $this->saveView(); + $this->drupalGet('bef_test_page'); + $this->assertFieldByXpath('//div[contains(@class, "bef-select-all-none")]'); + + // Now try it in a with the collapsible option. + $this->setBefSettings(array( + 'exposed_form_options[bef][type][more_options][bef_collapsible]' => TRUE, + )); + $this->saveView(); + $this->drupalGet('bef_test_page'); + + // Exposed filter is shown as checkboxes enclosed within a fieldset. + $this->assertFieldByXpath('//fieldset//input[@name="type[]" and @type="checkbox" and @id="edit-type-article"]'); + $this->assertFieldByXpath('//fieldset//input[@name="type[]" and @type="checkbox" and @id="edit-type-page"]'); + $this->assertFieldByXpath('//fieldset//input[@name="type[]" and @type="checkbox" and @id="edit-type-bef-test"]'); + + // Filter label is used as the fieldset legend + $this->assertFieldByXpath('//fieldset//legend[* = "Type"]'); + + // Test the select all/none option in fieldsets. + $this->assertFieldByXpath('//fieldset//div[contains(@class, "bef-select-all-none")]'); + } +} + +class BEF_TestHidden extends BEF_TestBase { + + /** + * Describes these tests to the testing framework. + */ + public static function getInfo() { + return array( + 'name' => 'BEF hidden tests', + 'description' => 'Verifies rendering filter options as hidden elements.', + 'group' => 'Better Exposed Filters', + ); + } + + /** + * Verify hidden exposed filters are rendered correctly on a page display + * + * @TODO: check the functionality of a multi-pass filter using hidden BEF + * settings. + */ + public function test_befPageDisplayHidden() { + // Create a page display to validate rendering. + $this->createDisplay('Page', array( + 'path' => array('path' => 'bef_test_page'), + )); + + // Verify multi-selection hidden elements with node->type. + $this->addFilter('node.type', array( + 'options[expose][multiple]' => TRUE, + )); + + // Verify single-selection hidden elements with node->status. + $this->addFilter('node.status'); + + $this->setBefSettings(array( + 'exposed_form_options[bef][type][bef_format]' => 'bef_hidden', + 'exposed_form_options[bef][status_1][bef_format]' => 'bef_hidden', + )); + $this->saveView(); + + $this->drupalGet('bef_test_page'); + $this->assertNoText(t('Type'), 'Exposed "Type" filter title does not show for hidden elements.'); + $this->assertNoText(t('Published'), 'Exposed "Published" filter title does not show for hidden elements.'); + + // Exposed filter option render as checkboxes. + // @todo: Need to fix these. + // $this->assertFieldByXpath('//input[@name="type[]" and @type="hidden"]'); + // $this->assertFieldByXpath('//input[@name="status_1" and @type="hidden"]'); + } +} + +class BEF_TestTaxonomyFilters extends BEF_TestBase { + + /** + * Describes these tests to the testing framework. + */ + public static function getInfo() { + return array( + 'name' => 'BEF taxonomy filter tests', + 'description' => 'Verifies rendering of taxonomy filters.', + 'group' => 'Better Exposed Filters', + ); + } + + /** + * Verify taxonomy-based exposed filters display correctly as both radio + * buttons and checkboxes + */ + public function test_befTaxonomyFilters() { + // Create a page display to validate rendering. + $this->createDisplay('Page', array( + 'path' => array('path' => 'bef_test_page'), + )); + + // Not sure why I can't get the Features-based functionality to work without + // pulling up the modules page, but it works... + $this->drupalGet('admin/modules'); + + // Add three terms with two children each to the vocab defined in + // bef_test_content. + $vocab = taxonomy_vocabulary_machine_name_load('taxonomy-bef_test-location'); + $parents = array(); + foreach (array('parent1', 'parent2', 'parent3') as $name) { + $term = new stdClass(); + $term->vid = $vocab->vid; + $term->name = $name; + taxonomy_term_save($term); + $parents[$term->tid] = $term->name; + } + foreach (array('child1', 'child2') as $name) { + foreach ($parents as $pid => $pname) { + $term = new stdClass(); + $term->vid = $vocab->vid; + $term->name = "$pname $name"; + $term->parent = $pid; + taxonomy_term_save($term); + } + } + + // Add a heirarchical taxonomy filter from the BEF test feature. + $this->addFilter( + 'field_data_field_bef_test_location.field_bef_test_location_tid', + array(), + array('options[type]' => 'select', 'options[hierarchy]' => TRUE) + ); + $this->saveView(); + $this->drupalGet($this->getBefSettingsUrl()); + + // Verify settings available to a heirarchical taxonomy filter. + $this->assertFieldByXpath('//select[@name="exposed_form_options[bef][field_bef_test_location_tid][bef_format]"]'); + $this->assertFieldByXpath('//select[@name="exposed_form_options[bef][field_bef_test_location_tid][bef_format]"]/option[@value="default"]'); + $this->assertFieldByXpath('//select[@name="exposed_form_options[bef][field_bef_test_location_tid][bef_format]"]/option[@value="bef"]'); + $this->assertFieldByXpath('//select[@name="exposed_form_options[bef][field_bef_test_location_tid][bef_format]"]/option[@value="bef_ul"]'); + $this->assertFieldByXpath('//select[@name="exposed_form_options[bef][field_bef_test_location_tid][bef_format]"]/option[@value="bef_links"]'); + $this->assertFieldByXpath('//select[@name="exposed_form_options[bef][field_bef_test_location_tid][bef_format]"]/option[@value="bef_hidden"]'); + + $this->setBefSettings(array( + 'exposed_form_options[bef][field_bef_test_location_tid][bef_format]' => 'bef_ul', + )); + $this->saveView(); + + // Verify taxonomy filter as radio buttons + $this->drupalGet('bef_test_page'); + $this->assertText(t('Location (field_bef_test_location)'), 'Verify exposed filter label'); + $this->assertFieldByXpath('//input[@name="field_bef_test_location_tid" and @type="radio"]', NULL, 'Verify exposed filter is shown as radio buttons'); + $this->assertFieldByXpath('//ul[@class="bef-tree"]/li//label[@for="edit-field-bef-test-location-tid-all"]', NULL, 'Verify "- Any -" is in the top level of heirarchy.'); + $this->assertFieldByXpath('//ul[@class="bef-tree"]/li//label[@for="edit-field-bef-test-location-tid-1"]', NULL, 'Verify "parent1" is in the top level of heirarchy.'); + $this->assertFieldByXpath('//ul[@class="bef-tree"]/li/ul/li//label[@for="edit-field-bef-test-location-tid-4"]', NULL, 'Verify "parent1 child1" is in the second level of heirarchy.'); + } +} + +class BEF_TestSort extends BEF_TestBase { + + /** + * Describes these tests to the testing framework. + */ + public static function getInfo() { + return array( + 'name' => 'BEF sort tests', + 'description' => 'Verifies rendering exposed sort options.', + 'group' => 'Better Exposed Filters', + ); + } + + /** + * Verify exposed sorts, including the combined sort/operator option and + * option rewriting, work with BEF. + */ + public function test_befExposedSort() { + // Create a page display to validate rendering. + $this->createDisplay('Page', array( + 'path' => array('path' => 'bef_test_page'), + )); + + // Add an exposed sort + $this->addSort('node.title'); + + // Double-check that expose sort order is checked. + $this->setBefSettings(array( + 'exposed_form_options[expose_sort_order]' => TRUE, + )); + $this->saveView(); + + // Verify basic options + $this->drupalGet($this->getBefSettingsUrl()); + $this->assertFieldByXpath('//select[@name="exposed_form_options[bef][sort][bef_format]"]/option', 'Default select list'); + $this->assertFieldByXpath('//select[@name="exposed_form_options[bef][sort][bef_format]"]/option', 'Radio Buttons'); + $this->assertFieldByXpath('//select[@name="exposed_form_options[bef][sort][bef_format]"]/option', 'Links'); + $this->assertFieldByXpath('//select[@name="exposed_form_options[bef][sort][bef_format]"]/option', 'Toggle Links'); + + // Verify combined sort order and sort by. + $this->setBefSettings(array( + 'exposed_form_options[bef][sort][advanced][combine]' => TRUE, + )); + $this->saveView(); + $this->drupalGet('bef_test_page'); + $this->assertFieldByXpath('//select[@name="sort_bef_combine"]/option', 'title DESC'); + $this->assertFieldByXpath('//select[@name="sort_bef_combine"]/option', 'title ASC'); + + // And rewrite the combined sort options. + $this->setBefSettings(array( + 'exposed_form_options[bef][sort][advanced][combine]' => TRUE, + 'exposed_form_options[bef][sort][advanced][combine_rewrite]' => "Title Desc|Going down\nTitle Asc|Going up", + )); + $this->saveView(); + $this->drupalGet('bef_test_page'); + $this->assertFieldByXpath('//select[@name="sort_bef_combine"]/option', 'Going up'); + $this->assertFieldByXpath('//select[@name="sort_bef_combine"]/option', 'Going down'); + + // @todo: add tests for correct rendering of Links and Toggle links options. + } +} + +class BEF_TestSliders extends BEF_TestBase { + + /** + * Describes these tests to the testing framework. + */ + public static function getInfo() { + return array( + 'name' => 'BEF slider tests', + 'description' => 'Verifies rendering filters jQueryUI sliders.', + 'group' => 'Better Exposed Filters', + ); + } + + /** + * Verify the correct classes are placed on exposed form elements and + * correct JS files are loaded to display jQuery UI sliders. + */ + public function test_befSliders() { + // Create a page display to validate rendering. + $this->createDisplay('Page', array( + 'path' => array('path' => 'bef_test_page'), + )); + + // Not sure why I can't get the Features-based functionality to work without + // pulling up the modules page, but it works... + $this->drupalGet('admin/modules'); + + // Verify slider rendering using the Numbers field from the BEF test content + // type. + $this->addFilter('field_data_field_bef_test_integer.field_bef_test_integer_value'); + $this->addFilter('field_data_field_bef_test_price.field_bef_test_price_value', array( + 'options[operator]' => 'between', + )); + $this->saveView(); + + // Verify the slider option appears for integer fields + $this->drupalGet($this->getBefSettingsUrl()); + $this->assertFieldByXpath('//select[@name="exposed_form_options[bef][field_bef_test_integer_value][bef_format]"]/option', 'Default select list'); + $this->assertFieldByXpath('//select[@name="exposed_form_options[bef][field_bef_test_integer_value][bef_format]"]/option', 'jQuery UI slider'); + $this->assertFieldByXpath('//select[@name="exposed_form_options[bef][field_bef_test_price_value][bef_format]"]/option', 'Default select list'); + $this->assertFieldByXpath('//select[@name="exposed_form_options[bef][field_bef_test_price_value][bef_format]"]/option', 'jQuery UI slider'); + + // Set both fields to use sliders using the default slider options. Integer + // should render as a single slider, Price should render as two because of + // the "between" operator. But we can't test the creation of the sliders as + // Simpletest ignores JavaScript, so we just verify the settings and + // necessary files are there. + $this->setBefSettings(array( + // Just use the default values for this one. + 'exposed_form_options[bef][field_bef_test_integer_value][bef_format]' => 'bef_slider', + // More complicated option for this one. + 'exposed_form_options[bef][field_bef_test_price_value][bef_format]' => 'bef_slider', + 'exposed_form_options[bef][field_bef_test_price_value][slider_options][bef_slider_min]' => 100, + 'exposed_form_options[bef][field_bef_test_price_value][slider_options][bef_slider_max]' => 5000, + 'exposed_form_options[bef][field_bef_test_price_value][slider_options][bef_slider_step]' => 250, + 'exposed_form_options[bef][field_bef_test_price_value][slider_options][bef_slider_animate]' => 'normal', + 'exposed_form_options[bef][field_bef_test_price_value][slider_options][bef_slider_orientation]' => 'vertical', + ), t('The range of the slider (Range maximum - Range minimum) should be evenly divisible by the step.')); + $this->drupalPost(NULL, array('exposed_form_options[bef][field_bef_test_price_value][slider_options][bef_slider_step]' => 100), 'Apply'); + $this->saveView(); + + $this->drupalGet('bef_test_page'); + $settings = $this->drupalGetSettings(); + $settings = $settings['better_exposed_filters']; + $this->assertTrue($settings['slider'], 'BEF slider setting is TRUE'); + + // Check slider options are passed from the settings page to JavaScript. + // Note that we skip the + $this->assertEqual($settings['slider_options']['field_bef_test_integer_value'], array( + 'min' => '0', + 'max' => '99999', + 'step' => '1', + 'animate' => '', + 'orientation' => 'horizontal', + 'id' => 'field-bef-test-integer-value', + 'viewId' => 'views-exposed-form-' . $this->view['machine_name'] . '-page-1', + )); + $this->assertEqual($settings['slider_options']['field_bef_test_price_value'], array( + 'min' => '100', + 'max' => '5000', + 'step' => '100', + 'animate' => 'normal', + 'orientation' => 'vertical', + 'id' => 'field-bef-test-price-value', + 'viewId' => 'views-exposed-form-' . $this->view['machine_name'] . '-page-1', + )); + } +} + +class BEF_TestDatepicker extends BEF_TestBase { + + /** + * Describes these tests to the testing framework. + */ + public static function getInfo() { + return array( + 'name' => 'BEF datepicker tests', + 'description' => 'Verifies rendering filter options as a jQueryUI Datepicker.', + 'group' => 'Better Exposed Filters', + ); + } + + /** + * Verifies Datepicker options using Drupal core date fields. + */ + public function test_befDatepicker() { + // Create a page display to validate rendering. + $this->createDisplay('Page', array( + 'path' => array('path' => 'bef_test_page'), + )); + + // Verify Datepicker rendering using the Numbers field from the BEF test + // content type. + $this->addFilter('node.created'); + $this->saveView(); + + // Verify the datepicker option appears for core date fields. + $this->drupalGet($this->getBefSettingsUrl()); + $this->assertFieldByXpath('//select[@name="exposed_form_options[bef][created][bef_format]"]/option', 'Default select list'); + $this->assertFieldByXpath('//select[@name="exposed_form_options[bef][created][bef_format]"]/option', 'jQuery UI Datepicker'); + $this->setBefSettings(array( + 'exposed_form_options[bef][created][bef_format]' => 'bef_datepicker', + )); + $this->saveView(); + + $this->drupalGet('bef_test_page'); + $settings = $this->drupalGetSettings(); + $settings = $settings['better_exposed_filters']; + $this->assertTrue($settings['datepicker'], 'BEF datepicker setting is TRUE'); + $this->assertFieldByXpath('//div[@id="edit-created-wrapper"]//input[contains(@class,"bef-datepicker")]'); + } + + /** + * Verifies Datepicker options using a custom (Date module) date field. + */ + public function test_befDatepickerCustom() { + // Create a page display to validate rendering. + $this->createDisplay('Page', array( + 'path' => array('path' => 'bef_test_page'), + )); + + // Not sure why I can't get the Features-based functionality to work without + // pulling up the modules page, but it works... + $this->drupalGet('admin/modules'); + + // Verify datepicker rendering using the Numbers field from the BEF test + // content type. + $this->addFilter('field_data_field_bef_test_date.field_bef_test_date_value', array(), array( + 'options[form_type]' => 'date_text', + )); + $this->saveView(); + + // Verify the datepicker option appears for custom date fields. + $this->drupalGet($this->getBefSettingsUrl()); + $this->assertFieldByXpath('//select[@name="exposed_form_options[bef][field_bef_test_date_value][bef_format]"]/option', 'Default select list'); + $this->assertFieldByXpath('//select[@name="exposed_form_options[bef][field_bef_test_date_value][bef_format]"]/option', 'jQuery UI Datepicker'); + $this->setBefSettings(array( + 'exposed_form_options[bef][field_bef_test_date_value][bef_format]' => 'bef_datepicker', + )); + $this->saveView(); + + $this->drupalGet('bef_test_page'); + $settings = $this->drupalGetSettings(); + $settings = $settings['better_exposed_filters']; + $this->assertTrue($settings['datepicker'], 'BEF datepicker setting is TRUE'); + $this->assertTrue(!empty($settings['datepicker_options']['dateFormat']), 'BEF datepicker setting is TRUE'); + $this->assertFieldByXpath('//div[@id="edit-field-bef-test-date-value-value"]//input[contains(@class,"bef-datepicker")]'); + } +} + +class BEF_TestLinks extends BEF_TestBase { + + /** + * Describes these tests to the testing framework. + */ + public static function getInfo() { + return array( + 'name' => 'BEF links tests', + 'description' => 'Verifies rendering filter options as toggle links.', + 'group' => 'Better Exposed Filters', + ); + } + + /** + * Verify filters rendered as links. + */ + public function test_befFilterLinks() { + // Create a page display to validate rendering. + $this->createDisplay('Page', array( + 'path' => array('path' => 'bef_test_page'), + )); + + // Verify link rendering with the node type filter. + $this->addFilter('node.type', array( + 'options[expose][multiple]' => TRUE, + )); + $this->addFilter('node.status'); + $this->setBefSettings(array( + 'exposed_form_options[bef][type][bef_format]' => 'bef_links', + 'exposed_form_options[bef][status_1][bef_format]' => 'bef_links', + )); + $this->saveView(); + $this->drupalGet('bef_test_page'); + + // Verify the type filter, which is multi-select. + $this->assertFieldByXpath('//div[@id="edit-type-wrapper"]//div[@id="edit-type-article"]/a[contains(@href, "?type[0]=article")]', NULL, 'Correct link for "article" filter'); + $this->assertFieldByXpath('//div[@id="edit-type-wrapper"]//div[@id="edit-type-page"]/a[contains(@href, "?type[0]=page")]', NULL, 'Correct link for "page" filter'); + $this->assertFieldByXpath('//div[@id="edit-type-wrapper"]//div[@id="edit-type-bef-test"]/a[contains(@href, "?type[0]=bef_test")]', NULL, 'Correct link for "bef test" filter'); + + // None of the type filter options should be marked as "active." + $this->assertNoFieldByXpath('//div[@id="edit-type-wrapper"]//a[contains(@class, "Active")]', NULL, 'No "type" link options are marked as "active"'); + + // Verify the status filter, which is single select. + $this->assertFieldByXpath('//div[@id="edit-status-1-wrapper"]//div[@id="edit-status-1-1"]/a[contains(@href, "?status_1=1")]', NULL, 'Correct link for "status: yes" filter'); + $this->assertFieldByXpath('//div[@id="edit-status-1-wrapper"]//div[@id="edit-status-1-0"]/a[contains(@href, "?status_1=0")]', NULL, 'Correct link for "status: no" filter'); + + // None of the status filter options should be marked as "active." + $this->assertNoFieldByXpath('//div[@id="edit-status-1-wrapper"]//a[contains(@class, "Active")]', NULL, 'No "status" link options are marked as "active"'); + + // Click the published/yes link and verify the filter links update. + $this->clickLink('Yes'); + + // Verify the type filter, which is multi-select. + $this->assertFieldByXpath('//div[@id="edit-type-wrapper"]//div[@id="edit-type-article"]/a[contains(@href, "?status_1=1&type[0]=article")]', NULL, 'Correct link for "article" filter'); + $this->assertFieldByXpath('//div[@id="edit-type-wrapper"]//div[@id="edit-type-page"]/a[contains(@href, "?status_1=1&type[0]=page")]', NULL, 'Correct link for "page" filter'); + $this->assertFieldByXpath('//div[@id="edit-type-wrapper"]//div[@id="edit-type-bef-test"]/a[contains(@href, "?status_1=1&type[0]=bef_test")]', NULL, 'Correct link for "bef test" filter'); + + // None of the type filter options should be marked as "active." + $this->assertNoFieldByXpath('//div[@id="edit-type-wrapper"]//a[contains(@class, "Active")]', NULL, 'No "type" link options are marked as "active"'); + + // Verify the status filter, which is single select. + $this->assertFieldByXpath('//div[@id="edit-status-1-wrapper"]//div[@id="edit-status-1-1"]/a[contains(@href, "?status_1=1")]', NULL, 'Correct link for "status: yes" filter'); + $this->assertFieldByXpath('//div[@id="edit-status-1-wrapper"]//div[@id="edit-status-1-0"]/a[contains(@href, "?status_1=0")]', NULL, 'Correct link for "status: no" filter'); + + // Just the "yes" option should be marked as "active." + $this->assertFieldByXpath('//div[@id="edit-status-1-wrapper"]//div[@id="edit-status-1-1"]/a[contains(@class, "active")]', NULL, 'Correct "active" setting for the "status: yes" filter'); + $this->assertNoFieldByXpath('//div[@id="edit-status-1-wrapper"]//div[@id="edit-status-1-0"]/a[contains(@class, "active")]', NULL, 'Correct "active" setting for the "status: no" filter'); + + // Verify correct links and "active" settings for non-required, boolean + // filters. See https://www.drupal.org/node/2631804. + $this->editFilter('status_1', array( + 'options[expose][required]' => FALSE, + )); + $this->saveView(); + $this->drupalGet('bef_test_page'); + + // No default filters so nothing should be marked as active. + // @TODO: Views is giving us an $element['#value'] = FALSE which leads to + // the "No" option being marked as active. There is no reason that #value + // should be set, as far as I can tell. Because I'm not sure if this is a + // Views bug or assumption, I'm leaving this test out for now. + //$this->assertNoFieldByXpath('//div[@id="edit-status-1-wrapper"]//a[contains(@class, "active")]', NULL, 'Nothing marked as "active"'); + + // Clicking "no" should mark that option as active. + $this->clickLink('No'); + $this->assertNoFieldByXpath('//div[@id="edit-status-1-wrapper"]//div[@id="edit-status-1-all"]/a[contains(@class, "active")]', NULL, 'Correct "active" setting for the "status: any" filter'); + $this->assertNoFieldByXpath('//div[@id="edit-status-1-wrapper"]//div[@id="edit-status-1-1"]/a[contains(@class, "active")]', NULL, 'Correct "active" setting for the "status: yes" filter'); + $this->assertFieldByXpath('//div[@id="edit-status-1-wrapper"]//div[@id="edit-status-1-0"]/a[contains(@class, "active")]', NULL, 'Correct "active" setting for the "status: no" filter'); + + // Clicking "any" should mark that option as active. + $this->clickLink('- Any -'); + $this->assertFieldByXpath('//div[@id="edit-status-1-wrapper"]//div[@id="edit-status-1-all"]/a[contains(@class, "active")]', NULL, 'Correct "active" setting for the "status: any" filter'); + $this->assertNoFieldByXpath('//div[@id="edit-status-1-wrapper"]//div[@id="edit-status-1-1"]/a[contains(@class, "active")]', NULL, 'Correct "active" setting for the "status: yes" filter'); + $this->assertNoFieldByXpath('//div[@id="edit-status-1-wrapper"]//div[@id="edit-status-1-0"]/a[contains(@class, "active")]', NULL, 'Correct "active" setting for the "status: no" filter'); + + // Verify correct "active" settings for filters with default values. + $this->editFilter('status_1', array( + 'options[expose][required]' => FALSE, + 'options[value]' => 0, + )); + $this->saveView(); + $this->drupalGet('bef_test_page'); + $this->assertNoFieldByXpath('//div[@id="edit-status-1-wrapper"]//div[@id="edit-status-1-all"]/a[contains(@class, "active")]', NULL, 'Correct "active" setting for the "status: any" filter'); + $this->assertNoFieldByXpath('//div[@id="edit-status-1-wrapper"]//div[@id="edit-status-1-1"]/a[contains(@class, "active")]', NULL, 'Correct "active" setting for the "status: yes" filter'); + $this->assertFieldByXpath('//div[@id="edit-status-1-wrapper"]//div[@id="edit-status-1-0"]/a[contains(@class, "active")]', NULL, 'Correct "active" setting for the "status: no" filter'); + } + +} + +class BEF_TestSecondaryFilters extends BEF_TestBase { + + /** + * Describes these tests to the testing framework. + */ + public static function getInfo() { + return array( + 'name' => 'BEF secondary filter tests', + 'description' => 'Verifies rendering filter options within the secondary filter fieldset.', + 'group' => 'Better Exposed Filters', + ); + } + + /** + * Basic coverage for filters rendered in secondary options fieldsets. See + * + * @todo near the end of this test. + */ + public function test_befSecondaryFilters() { + // Create a page display to validate rendering. + $this->createDisplay('Page', array( + 'path' => array('path' => 'bef_test_page'), + )); + + // Add the node.type filter as a multi-select filter. + $this->addFilter('node.type', array( + 'options[expose][multiple]' => TRUE, + )); + $this->setBefSettings(array( + 'exposed_form_options[bef][general][allow_secondary]' => 1, + 'exposed_form_options[bef][general][secondary_label]' => 'Custom Label', + 'exposed_form_options[bef][general][secondary_collapse_override]' => 0, + 'exposed_form_options[bef][type][more_options][is_secondary]' => 1, + )); + $this->saveView(); + $this->drupalGet('bef_test_page'); + + // Verify a collapsed fieldset exists with the correct label. + $this->assertFieldByXpath('//div[@id="edit-secondary-wrapper"]//fieldset[contains(@class, "collapsible") and contains(@class, "collapsed")]', NULL, 'Collapsible fieldset for secondary options starts collapsed/'); + $this->assertFieldByXpath('//div[@id="edit-secondary-wrapper"]//fieldset/legend/span', 'Custom Label', 'Collapsible fieldset has the correct custom label'); + $this->assertFieldByXpath('//div[@id="edit-secondary-wrapper"]//fieldset//select[@name="type[]"]', NULL, 'The node.type filter is within the secondary fieldset'); + + // Select an option and make sure the fieldset is expanded when rendered. + $this->drupalGet('bef_test_page', array('query' => array('type' => 'page'))); + $this->assertFieldByXpath('//div[@id="edit-secondary-wrapper"]//fieldset[contains(@class, "collapsible") and not(contains(@class, "collapsed"))]', NULL, 'Collapsible fieldset starts open'); + + // Verify force-open and force-closed fieldset options. + $this->setBefSettings(array( + // Always render opened. + 'exposed_form_options[bef][general][secondary_collapse_override]' => 1, + )); + $this->saveView(); + $this->drupalGet('bef_test_page'); + $this->assertFieldByXpath('//div[@id="edit-secondary-wrapper"]//fieldset[contains(@class, "collapsible") and not(contains(@class, "collapsed"))]', NULL, 'Collapsible fieldset starts open'); + $this->setBefSettings(array( + // Always render closed. + 'exposed_form_options[bef][general][secondary_collapse_override]' => 2, + )); + $this->saveView(); + $this->drupalGet('bef_test_page', array('query' => array('type' => 'page'))); + $this->assertFieldByXpath('//div[@id="edit-secondary-wrapper"]//fieldset[contains(@class, "collapsible") and contains(@class, "collapsed")]', NULL, 'Collapsible fieldset starts closed'); + + // https://drupal.org/node/2189321 + // Verify fieldset is collapsed/expanded when a custom filter ID is set. + // @TODO: Consider refactoring so we can leverage all existing tests to use + // custom filter ID's... + $this->editFilter('node.type', array( + 'options[expose][identifier]' => 'custom_id', + )); + $this->setBefSettings(array( + // Use default open/closed rendering. + 'exposed_form_options[bef][general][secondary_collapse_override]' => 0, + )); + $this->saveView(); + $this->drupalGet('bef_test_page'); + + // Verify a collapsed fieldset exists with the correct label. + $this->assertFieldByXpath('//div[@id="edit-secondary-wrapper"]//fieldset[contains(@class, "collapsible") and contains(@class, "collapsed")]', NULL, 'Collapsible fieldset for secondary options, starts collapsed.'); + $this->assertFieldByXpath('//div[@id="edit-secondary-wrapper"]//fieldset//select[@name="custom_id[]"]', NULL, 'The node.type filter is within the secondary fieldset'); + + // Select an option and make sure the fieldset is expanded when rendered. + $this->drupalGet('bef_test_page', array('query' => array('custom_id' => 'page'))); + $this->assertFieldByXpath('//div[@id="edit-secondary-wrapper"]//fieldset[contains(@class, "collapsible") and not(contains(@class, "collapsed"))]', NULL, 'Collapsible fieldset starts open'); + + // Add an in-between filter and verify it renders correctly. + $this->addFilter('field_data_field_bef_test_integer.field_bef_test_integer_value', array( + 'options[operator]' => 'between', + )); + $this->setBefSettings(array( + 'exposed_form_options[bef][general][allow_secondary]' => 1, + 'exposed_form_options[bef][general][secondary_label]' => 'Custom Label', + 'exposed_form_options[bef][general][secondary_collapse_override]' => 0, + 'exposed_form_options[bef][field_bef_test_integer_value][more_options][is_secondary]' => 1, + )); + $this->saveView(); + $this->drupalGet('bef_test_page'); + + // Verify labels for the min and max fields. + // Note: Both labels have a trailing space which is added by Views... I + // assume to provide some visual whitespace between the label and widget if + // they are arranged horizontally. I imagine that'll change at some point as + // it's a terrible way to add some padding to an element! + $this->assertFieldByXpath('//label[@for="edit-field-bef-test-integer-value-min"]', 'Integer (field_bef_test_integer) ', 'Label appears for in-between filters in secondary fieldsets.'); + $this->assertFieldByXpath('//label[@for="edit-field-bef-test-integer-value-max"]', 'And ', 'In-between filter has correct label between min and max inputs'); + $this->assertFieldByXpath('//label[@for="edit-custom-id"]', 'Type ', 'Single input filter has correct label'); + } +} + +class BEF_TestRewrite extends BEF_TestBase { + + /** + * Describes these tests to the testing framework. + */ + public static function getInfo() { + return array( + 'name' => 'BEF rewrite tests', + 'description' => 'Verifies rewriting filter and sort options.', + 'group' => 'Better Exposed Filters', + ); + } + + /** + * Verify rewriting works for filter options + */ + public function test_befFilterRewrite() { + // Create a page display to validate rendering. + $this->createDisplay('Page', array( + 'path' => array('path' => 'bef_test_page'), + )); + + // Add a node type filter set to allow multiple selections. + $this->addFilter('node.type', array( + 'options[expose][multiple]' => TRUE, + )); + + // Add a node status filter set to allow a single selection. + $this->addFilter('node.status'); + + // Add a few terms to the Tags vocab and a Tags filter to the view. + // Verify values that PHP equates to empty. + $term1 = new stdClass(); + $term1->name = "0"; + $term1->vid = 1; + taxonomy_term_save($term1); + + // Verify values with accents. + $term2 = new stdClass(); + $term2->name = "Tèstiñg Térm"; + $term2->vid = 1; + taxonomy_term_save($term2); + $this->addFilter('field_data_field_tags.field_tags_tid', array(), array( + // Choose the dropdown option for term filter, without hierarchy shown. + 'options[type]' => 'select', + 'options[hierarchy]' => '0', + )); + + // Add some exposed sort options. + $this->addSort('node.created'); + $this->addSort('node.title'); + + // Set rewrite for single and multiple selection filters. + $this->setBefSettings(array( + 'exposed_form_options[bef][status_1][more_options][rewrite][filter_rewrite_values]' => + "Yes|Hell yeah!\nNo|Booo...", + 'exposed_form_options[bef][type][more_options][rewrite][filter_rewrite_values]' => + "bef_test|This is a test\nArticle|", + 'exposed_form_options[bef][field_tags_tid][more_options][rewrite][filter_rewrite_values]' => + $term1->name . "|The first one\n" . $term2->name . '|' . $term2->name . ' rewritten', + 'exposed_form_options[bef][sort][advanced][combine]' => '1', + 'exposed_form_options[bef][sort][advanced][combine_rewrite]' => + "Post date Asc|Oldest first\nPost date Desc|Newest first\nTitle Asc|A -> Z\nTitle Desc|", + )); + $this->saveView(); + + $this->drupalGet('bef_test_page'); + $this->assertFieldByXpath('//select[@id="edit-type"]/option', 'This is a test', 'Rerwrite of the filter option "bef_test" in "Type" filter'); + $this->assertNoFieldByXpath('//select[@id="edit-type"]/option', 'Article', 'Rerwrite of the filter option "article" to null in "Type" filter.'); + $this->assertFieldByXpath('//select[@id="edit-status-1"]/option', 'Hell yeah!', 'Rerwrite of the filter option "Yes" in "Published" filter.'); + $this->assertFieldByXpath('//select[@id="edit-status-1"]/option', 'Booo...', 'Rerwrite of the filter option "No" in "Published" filter.'); + $this->assertFieldByXpath('//select[@id="edit-field-tags-tid"]/option', 'The first one', 'Rerwrite of the filter value "0" in "Tags (field_tags)" filter.'); + $this->assertFieldByXpath('//select[@id="edit-field-tags-tid"]/option', $term2->name . ' rewritten', 'Rerwrite of a filter option with accents in "Tags (field_tags)" filter.'); + $this->assertFieldByXpath('//select[@id="edit-sort-bef-combine"]/option', 'Oldest first', 'Rerwrite of an exposed combined sort option.'); + $this->assertFieldByXpath('//select[@id="edit-sort-bef-combine"]/option', 'Newest first', 'Rerwrite of an exposed combined sort option.'); + $this->assertFieldByXpath('//select[@id="edit-sort-bef-combine"]/option', 'A -> Z', 'Rerwrite of an exposed combined sort option.'); + $this->assertNoFieldByXpath('//select[@id="edit-sort-bef-combine"]/option', 'Title Desc', 'Removal of an exposed combined sort option through rewriting it to NULL.'); + } +} + +class BEF_TestMisc extends BEF_TestBase { + + /** + * Describes these tests to the testing framework. + */ + public static function getInfo() { + return array( + 'name' => 'BEF miscellaneous tests', + 'description' => 'Verifies misc BEF functional.', + 'group' => 'Better Exposed Filters', + ); + } + + /** + * Verifies the "Require input..." option. + */ + public function test_befRequireInput() { + // Create a page display to validate rendering. + $this->createDisplay('Page', array( + 'path' => array('path' => 'bef_test_page'), + )); + + // Add the node.type filter as a multi-select filter. + $this->addFilter('node.type', array( + 'options[expose][multiple]' => TRUE, + )); + $this->setBefSettings(array( + 'exposed_form_options[bef][general][input_required]' => 1, + 'exposed_form_options[bef][general][allow_secondary]' => 1, + )); + $this->saveView(); + $this->drupalGet('bef_test_page'); + + // Verify the "Text on demand" is shown. + $this->assertText(t('Select any filter and click on Apply to see results'), '"Empty" text should show on initial display.'); + } +} diff --git a/sites/all/modules/better_exposed_filters/tests/better_exposed_filters_TestBase.php b/sites/all/modules/better_exposed_filters/tests/better_exposed_filters_TestBase.php new file mode 100644 index 0000000..e7082ed --- /dev/null +++ b/sites/all/modules/better_exposed_filters/tests/better_exposed_filters_TestBase.php @@ -0,0 +1,275 @@ + 'BEF Basic functionality tests', + 'description' => 'Basic tests for Better Exposed Filters.', + 'group' => 'Better Exposed Filters', + ); + } + + public function setUp() { + // For benchmarking. + $this->start = time(); + + // Enable any modules required for the test. + parent::setUp( + 'better_exposed_filters', + 'date', + 'date_views', + 'list', + 'number', + 'taxonomy', + 'text', + 'views', + 'views_ui' + ); + + // One of these days I'll figure out why Features is breaking all my tests. + module_enable(array('bef_test_content')); + + // User with edit views perms + $this->admin_user = $this->drupalCreateUser(); + $role = user_role_load_by_name('administrator'); + $this->assertTrue(!empty($role->rid), 'Found the "administrator" role.'); + user_save($this->admin_user, array('roles' => array($role->rid => $role->rid))); + $this->drupalLogin($this->admin_user); + + // Build a basic view for use in tests. + $this->createView(); + + // $this->createDisplay('Page', array('path' => array('path' => 'bef_test_page'))); + + // Add field to default display + // $this->addField('node.title'); + + // Turn of Better Exposed Filters + $this->setBefExposedForm(); + } + + public function tearDown() { + debug('This test run took ' . (time() - $this->start) . ' seconds.'); + unset($this->view); + parent::tearDown(); + } + + /******************************************************************************* + * Helper functions + ******************************************************************************/ + + /** + * Returns the URL for the BEF exposed form settings page. + */ + protected function getBefSettingsUrl() { + return 'admin/structure/views/nojs/display/' . $this->view['machine_name'] . '/default/exposed_form_options'; + } + + protected function createView($name = '') { + if (!empty($this->view)) { + debug('WARNING: createView called after view has already been created.'); + return; + } + + if (empty($name)) { + $name = $this->randomName(8); + } + $this->view['name'] = $name; + $this->view['machine_name'] = strtolower($name); + + $edit = array( + 'human_name' => $this->view['name'], + 'name' => $this->view['machine_name'], + // Default is to create a page display. + 'page[create]' => FALSE, + ); + $this->drupalPost('admin/structure/views/add', $edit, 'Save & exit'); + + // URL to edit this view. + $this->view['edit_url'] = 'admin/structure/views/view/' . $this->view['machine_name'] . '/edit'; + } + + /** + * Creates a display of $type. Currently supports: + * 'Page' + * + * @todo: support more types... + */ + protected function createDisplay($type = 'Page', $settings = NULL) { + if (!isset($this->view['displays'])) { + $this->view['displays'] = array(); + } + + // Add a display of $type to the view + $this->drupalPost($this->view['edit_url'], array(), "Add $type"); + + // Grab the name of the newly created display and store some info about it. + $url = $this->getUrl(); + $display_name = substr($url, strrpos($url, '/') + 1); + $this->view['displays'][$display_name] = array( + 'machine_name' => $display_name, + 'edit_url' => 'admin/structure/views/view/' . $this->view['machine_name'] . '/edit/' . $display_name, + 'settings_base_url' => 'admin/structure/views/nojs/display/' . $this->view['machine_name'] . '/' . $display_name, + ); + + // Settings should be in the form of 'path' => array_of_form_settings. Eg: + // to set the title for a new display as an override: + // 'title' => array( + // 'title' => 'This is an override title', + // 'override[dropdown]' => display_machine_name_goes_here, + // ) + // + // If you navigate to + // admin/structure/views/nojs/display///title + // you will see the form in question. + foreach ($settings as $path => $values) { + $this->drupalPost($this->view['displays'][$display_name]['settings_base_url'] . "/$path", $values, 'Apply'); + } + $this->saveView(); + } + + /** + * Adds a filter to a view display. + * + * $field: string in the form of node.status or + * field_data_field_example.field_example_value + * $settings: (array) Settings on the "Configure filter criterion" dialog. + * NOTE: called after the "Expose filter" button is pressed if $exposed + * is TRUE so you can set things like "Allow multiple items" or grouped + * filter options. + * $additional: (array) settings for any additional configuration forms such + * as taxonomy term settings. + * $display: machine name of the display to add this filter to. NOTE: + * Currently only allows filters on the master display, no overrides. + * @todo: fix that, if needed. + * $exposed: (bool) (optional, default: TRUE) Is this an exposed filter? + * + * Note: This routine expects the caller to save the view, as needed. + */ + protected function addFilter($field, $settings = array(), $additional = array(), $exposed = TRUE, $display = 'default') { + $edit = array( + "name[$field]" => TRUE, + ); + $url = 'admin/structure/views/nojs/add-item/' . $this->view['machine_name'] . "/$display/filter"; + $this->drupalPost($url, $edit, 'Add and configure filter criteria'); + + if (!empty($additional)) { + // Handle filter-specific options screen. + $this->drupalPost(NULL, $additional, 'Apply'); + } + + if ($exposed) { + $this->drupalPost(NULL, array(), 'Expose filter'); + } + $this->drupalPost(NULL, $settings, 'Apply'); + } + + /** + * Edits an existing filter in the current view. See addFilter for param + * definitions. + */ + protected function editFilter($field, $settings, $additional = array(), $display = 'default') { + if (FALSE !== ($pos = strpos($field, '.'))) { + $field = substr($field, $pos + 1); + } + $url = 'admin/structure/views/nojs/config-item/' . $this->view['machine_name'] . "/$display/filter/$field"; + $this->drupalPost($url, $settings, 'Apply'); + + if (!empty($additional)) { + // Handle filter-specific options screen. + $this->drupalPost(NULL, $additional, 'Apply'); + } + } + + /** + * Adds a sort to a view display. See addFilter for parameter options. + * + * Note: This routine expects the caller to save the view, as needed. + */ + protected function addSort($field, $settings = array(), $additional = array(), $exposed = TRUE, $display = 'default') { + $edit = array( + "name[$field]" => TRUE, + ); + $url = 'admin/structure/views/nojs/add-item/' . $this->view['machine_name'] . "/$display/sort"; + $this->drupalPost($url, $edit, 'Add and configure sort criteria'); + + if (!empty($additional)) { + // Handle filter-specific options screen. + $this->drupalPost(NULL, $additional, 'Apply'); + } + + if ($exposed) { + $this->drupalPost(NULL, array(), 'Expose sort'); + } + $this->drupalPost(NULL, $settings, 'Apply'); + } + + /** + * Adds a field to a view display. See addFilter for parameter options. + * + * Note: This routine expects the caller to save the view, as needed. + */ + protected function addField($field, $settings = array(), $display = 'default') { + $edit = array( + "name[$field]" => TRUE, + ); + $url = 'admin/structure/views/nojs/add-item/' . $this->view['machine_name'] . "/$display/field"; + $this->drupalPost($url, $edit, 'Add and configure fields'); + $this->drupalPost(NULL, $settings, 'Apply'); + } + + /** + * Ensures that BEF is selected as the exposed form option + * + * Note: This routine expects the caller to save the view, as needed. + */ + protected function setBefExposedForm($display = 'default') { + $edit = array( + "exposed_form[type]" => 'better_exposed_filters', + ); + $url = 'admin/structure/views/nojs/display/' . $this->view['machine_name'] . "/$display/exposed_form"; + $this->drupalPost($url, $edit, 'Apply'); + + // BEF settings is covered under setBefSettings() so we just accept the + // default values and move on. + $this->drupalPost(NULL, array(), 'Apply'); + } + + /** + * Sets various BEF exposed form settings. If $error is specified it also + * asserts that the error text apepars when trying to apply $settings. + * + * Note: This routine expects the caller to save the view, as needed. + */ + protected function setBefSettings($settings, $error = '') { + $this->drupalPost($this->getBefSettingsUrl(), $settings, 'Apply'); + if (!empty($error)) { + $this->assertText($error); + } + } + + /** + * Saves the view + */ + protected function saveView() { + $this->drupalPost($this->view['edit_url'], array(), 'Save'); + } +} diff --git a/sites/all/modules/ddf/LICENSE.txt b/sites/all/modules/ddf/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/sites/all/modules/ddf/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/sites/all/modules/ddf/README.txt b/sites/all/modules/ddf/README.txt new file mode 100644 index 0000000..ed8d5fe --- /dev/null +++ b/sites/all/modules/ddf/README.txt @@ -0,0 +1,7 @@ +Dynamic dependent fields +================================================== + +The documentation is available online https://www.drupal.org/node/2532790 +Ask your questions and report bugs through the issue tracker https://www.drupal.org/project/issues/ddf + +Enjoy the module! diff --git a/sites/all/modules/ddf/ddf.info b/sites/all/modules/ddf/ddf.info new file mode 100644 index 0000000..84509e1 --- /dev/null +++ b/sites/all/modules/ddf/ddf.info @@ -0,0 +1,12 @@ +name = Dynamic dependent fields +description = Makes field value or options depend on other field values when editing entity. +core = 7.x +package = Fields +dependencies[] = entity + +; Information added by Drupal.org packaging script on 2015-08-13 +version = "7.x-1.8" +core = "7.x" +project = "ddf" +datestamp = "1439462340" + diff --git a/sites/all/modules/ddf/ddf.install b/sites/all/modules/ddf/ddf.install new file mode 100644 index 0000000..957d4d4 --- /dev/null +++ b/sites/all/modules/ddf/ddf.install @@ -0,0 +1,44 @@ + 'Dynamic field dependencies.', + 'fields' => array( + 'field_name' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'description' => 'The name of controlling field.', + ), + 'entity_type' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + ), + 'bundle' => array( + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + ), + 'dependent_field_name' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => TRUE, + 'description' => 'The name of controlled field.', + ), + 'data' => array( + 'type' => 'text', + 'not null' => FALSE, + 'description' => 'Serialized data containing dependency properties.', + ), + ), + 'primary key' => array('field_name', 'entity_type', 'bundle', 'dependent_field_name'), + ); + + return $schema; +} diff --git a/sites/all/modules/ddf/ddf.js b/sites/all/modules/ddf/ddf.js new file mode 100644 index 0000000..4e86b66 --- /dev/null +++ b/sites/all/modules/ddf/ddf.js @@ -0,0 +1,129 @@ +(function ($) { + +Drupal.behaviors.ddf = { + attach: function (context, settings) { + if (!settings.ddf) { + return; + } + + $.each(settings.ddf, function(key, form_settings) { + var id = form_settings['form_id']; + var form = $('input[value="' + id + '"]', context).closest('form').not('.ddf-processed').addClass('ddf-processed'); + if (form.length === 1) { + if (form_settings['dependent'] && form_settings['fields']) { + ddfForm(form, id, form_settings['dependent'], form_settings['fields'], form_settings); + } + } + }); + + function ddfForm(form, form_id, dependent, fields, form_settings) { + var controlling_elements = {}; + var dependent_selectors = {}; + + $.each(dependent, function(field_name) { + var element = form.find('[name="' + dependent[field_name] + '"],[name="' + dependent[field_name] + '[]"]'); + dependent_selectors[field_name] = '#' + form.attr('id') + ' [name="' + element.attr('name') + '"]'; + if ((element.length === 1) && (element.attr('id'))) { + dependent_selectors[field_name] = '#' + element.attr('id'); + } + }); + + $.each(fields, function(field_name, name_attr) { + var elements = form.find('[name="' + name_attr + '"],[name^="' + name_attr + '["]').not('.ddf-field-processed').addClass('ddf-field-processed'); + if (elements.length > 0) { + controlling_elements[field_name] = elements; + elements.each(function() { + var element = $(this); + var url = settings['basePath'] + settings['pathPrefix'] + 'ddf/update/' + field_name + '/' + + ddfGetScalar(form_settings['entity_type']) + '/' + + ddfGetScalar(form_settings['bundle']) + '/' + + ddfGetScalar(form_settings['entity_id']); + + element.attr('autocomplete', 'off'); + var ajax = new Drupal.ajax(false, element, {event: 'change', url: url}); + ajax.beforeSerialize = ddfBeforeSerialize; + }); + } + }); + + function ddfBeforeSerialize(element, options) { + options.type = 'GET'; + options.data = {}; + options.data['form_build_id'] = form_id; + + $.each(controlling_elements, function(field_name, controlling_element) { + var value = $(controlling_element).fieldValue(); + options.data[field_name] = (value.length > 0) ? value.join('+') : 'NULL'; + }); + + $.each(dependent_selectors, function(field_name, selector) {options.data['dep:' + field_name] = selector;}); + } + + function ddfGetScalar(value) { + return ($.isArray(value)) ? value[0] : value; + } + } + } +}; + +/** + * Command to insert new content into the DOM without wrapping in extra DIV element. + */ +Drupal.ajax.prototype.commands.ddf_insertnowrap = function (ajax, response, status) { + // Get information from the response. If it is not there, default to + // our presets. + var wrapper = response.selector ? $(response.selector) : $(ajax.wrapper); + var method = response.method || ajax.method; + var effect = ajax.getEffect(response); + + // We don't know what response.data contains: it might be a string of text + // without HTML, so don't rely on jQuery correctly interpreting + // $(response.data) as new HTML rather than a CSS selector. Also, if + // response.data contains top-level text nodes, they get lost with either + // $(response.data) or $('
').replaceWith(response.data). + var new_content_wrapped = $('
').html(response.data); + var new_content = new_content_wrapped.contents(); + var settings = {}; + + // If removing content from the wrapper, detach behaviors first. + switch (method) { + case 'html': + case 'replaceWith': + case 'replaceAll': + case 'empty': + case 'remove': + settings = response.settings || ajax.settings || Drupal.settings; + Drupal.detachBehaviors(wrapper, settings); + break; + } + + // Add the new content to the page. + wrapper[method](new_content); + + // Immediately hide the new content if we're using any effects. + if (effect.showEffect !== 'show') { + new_content.hide(); + } + + // Determine which effect to use and what content will receive the + // effect, then show the new content. + if ($('.ajax-new-content', new_content).length > 0) { + $('.ajax-new-content', new_content).hide(); + new_content.show(); + $('.ajax-new-content', new_content)[effect.showEffect](effect.showSpeed); + } + else if (effect.showEffect !== 'show') { + new_content[effect.showEffect](effect.showSpeed); + } + + // Attach all JavaScript behaviors to the new content, if it was successfully + // added to the page, this if statement allows #ajax['wrapper'] to be + // optional. + if (new_content.parents('html').length > 0) { + // Apply any settings from the returned JSON if available. + settings = response.settings || ajax.settings || Drupal.settings; + Drupal.attachBehaviors(wrapper, settings); + } +}; + +})(jQuery); diff --git a/sites/all/modules/ddf/ddf.module b/sites/all/modules/ddf/ddf.module new file mode 100644 index 0000000..5080aa1 --- /dev/null +++ b/sites/all/modules/ddf/ddf.module @@ -0,0 +1,445 @@ + 'Update dependent widget', + 'page callback' => 'ddf_update_callback', + 'page arguments' => array(2, 3, 4, 5), + 'access callback' => 'ddf_update_access_callback', + 'access arguments' => array(2, 3, 4, 5), + 'type' => MENU_CALLBACK, + 'delivery callback' => 'ajax_deliver', + ); + + return $items; +} + +/** + * Ajax callback returning ajax commands to update dependent widgets. + */ +function ddf_update_callback($controlling_field_name, $entity_type, $bundle, $entity_id) { + $result = array('#type' => 'ajax', '#commands' => array()); + $dependent_fields = db_select('ddf', 'd') + ->fields('d', array('dependent_field_name', 'data')) + ->condition('field_name', $controlling_field_name) + ->condition('entity_type', $entity_type) + ->condition('bundle', $bundle) + ->execute() + ->fetchAllKeyed(); + if (empty($dependent_fields)) { + return $result; + } + if ($entity_id == 'NULL') { + $entity_id = NULL; + } + $parameters = drupal_get_query_parameters($_GET); + $entity = NULL; + if (!is_null($entity_id)) { + $entity = entity_load_single($entity_type, $entity_id); + } + if (empty($entity)) { + $entity = NULL; + } + + // Replace '_none' => NULL for select widgets. + foreach ($parameters as $field_name => $value) { + if ($value === '_none') { + $instance = field_info_instance($entity_type, $field_name, $bundle); + if (($instance) && ($instance['widget']['type'] == 'options_select')) { + $parameters[$field_name] = NULL; + } + } + } + + foreach ($dependent_fields as $dependent_field_name => $settings) { + if (!empty($settings)) { + $settings = unserialize($settings); + } + $dependent_field = field_info_field($dependent_field_name); + if (is_null($dependent_field)) { + continue; + } + $selector = ''; + if (isset($parameters['dep:' . $dependent_field_name])) { + $selector = $parameters['dep:' . $dependent_field_name]; + } + $commands = module_invoke_all('ddf_update_widget', $dependent_field, $parameters, $selector, $entity, $settings, $controlling_field_name, $entity_type, $bundle); + if (empty($commands)) { + continue; + } + $result['#commands'] = array_merge_recursive($result['#commands'], $commands); + } + + return $result; +} + +/** + * Access callback returning TRUE if the user can edit current entity. + */ +function ddf_update_access_callback($controlling_field_name, $entity_type, $bundle, $entity_id) { + $entity = NULL; + + if ($entity_id === 'NULL') { + $entity_id = NULL; + } + else { + $entity = entity_load_single($entity_type, $entity_id); + if (empty($entity)) { + return FALSE; + } + } + + $dependencies = ddf_load_dependencies($entity_type, $bundle); + if (empty($dependencies)) { + return FALSE; + } + + $dependency_exists = FALSE; + foreach ($dependencies as $dependency) { + if ($dependency[0] == $controlling_field_name) { + $dependency_exists = TRUE; + break; + } + } + if (!$dependency_exists) { + return FALSE; + } + + $field = field_info_field($controlling_field_name); + $instance = field_info_instance($entity_type, $controlling_field_name, $bundle); + if (!$field || !$instance || !field_access('edit', $field, $entity_type)) { + return FALSE; + } + + switch ($entity_type) { + case 'node': + return empty($entity_id) ? node_access('create', $bundle) : node_access('update', $entity); + + case 'taxonomy_term': + return empty($entity_id) ? user_access('administer taxonomy') : taxonomy_term_edit_access($entity); + + case 'user': + return empty($entity_id) ? (user_access('administer users') || user_register_access()) : (user_access('administer users') || ($entity_id == $GLOBALS['user']->uid)); + + case 'comment': + return empty($entity_id) ? (user_access('administer comments') || user_access('post comments')) : comment_access('edit', $entity); + } + + // Enable access for unknown entity types. + return TRUE; +} + +/** + * Implements hook_field_delete_field(). + */ +function ddf_field_delete_field($field) { + db_delete('ddf') + ->condition('field_name', $field['field_name']) + ->execute(); + + db_delete('ddf') + ->condition('dependent_field_name', $field['field_name']) + ->execute(); +} + +/** + * Implements hook_field_delete_instance(). + */ +function ddf_field_delete_instance($instance) { + db_delete('ddf') + ->condition('field_name', $instance['field_name']) + ->condition('entity_type', $instance['entity_type']) + ->condition('bundle', $instance['bundle']) + ->execute(); + + db_delete('ddf') + ->condition('dependent_field_name', $instance['field_name']) + ->condition('entity_type', $instance['entity_type']) + ->condition('bundle', $instance['bundle']) + ->execute(); +} + +function ddf_add_dependency($controlling_field_name, $dependent_field_name, $entity_type, $bundle, $settings = NULL) { + $fields = array( + 'field_name' => $controlling_field_name, + 'entity_type' => $entity_type, + 'bundle' => $bundle, + 'dependent_field_name' => $dependent_field_name, + ); + if (!is_null($settings)) { + $fields['data'] = serialize($settings); + } + db_insert('ddf')->fields($fields)->execute(); +} + +function ddf_remove_dependency($dependent_field_name, $entity_type, $bundle) { + db_delete('ddf') + ->condition('dependent_field_name', $dependent_field_name) + ->condition('entity_type', $entity_type) + ->condition('bundle', $bundle) + ->execute(); +} + +/** + * Loads dependencies from the database. + */ +function ddf_load_dependencies($entity_type, $bundle) { + // Use the advanced drupal_static() pattern. + static $dependencies = NULL; + if (!isset($dependencies)) { + $dependencies = &drupal_static(__FUNCTION__, array()); + } + + if (!isset($dependencies[$entity_type][$bundle])) { + $dependencies[$entity_type][$bundle] = array(); + + $result = db_select('ddf', 'd') + ->fields('d', array('field_name', 'dependent_field_name', 'data')) + ->condition('entity_type', $entity_type) + ->condition('bundle', $bundle) + ->execute(); + + foreach ($result as $dependency) { + if (strlen($dependency->data) > 0) { + $dependency->data = unserialize($dependency->data); + } + $dependencies[$entity_type][$bundle][] = array($dependency->field_name, $dependency->dependent_field_name, $dependency->data); + } + } + + return $dependencies[$entity_type][$bundle]; +} + +/** + * Implements hook_element_info_alter(). + * Adds an #after_build function to all form elements. + */ +function ddf_element_info_alter(&$types) { + foreach ($types as $type => $info) { + $types[$type]['#after_build'][] = 'ddf_element_after_build'; + } +} + +function ddf_element_after_build($element, &$form_state) { + // Ensure that the element is a field. + if (isset($element['#field_name'])) { + $field = &$element; + } + elseif (isset($element['#language'], $element[$element['#language']], $element[$element['#language']]['#field_name'])) { + // Some fields are wrapped in containers before processing. + $field = &$element[$element['#language']]; + } + else { + return $element; + } + + // Do not process hidden fields. + if ((isset($field['#access'])) && ($field['#access'] == FALSE)) { + return $element; + } + + $form = &$form_state['complete form']; + + // Avoid processing fields in fields_ui administration pages. + if (drupal_substr($form['#form_id'], 0, 9) == 'field_ui_') { + return $element; + } + + $entity_type = NULL; + $bundle = NULL; + $entity = NULL; + $entity_id = NULL; + if (isset($field['#entity_type'], $field['#bundle'])) { + $entity_type = $field['#entity_type']; + $bundle = $field['#bundle']; + if (isset($field['#entity'])) { + $entity = $field['#entity']; + } + } + elseif (isset($form['#entity_type'], $form['#bundle'])) { + $entity_type = $form['#entity_type']; + $bundle = $form['#bundle']; + if (isset($form['#entity'])) { + $entity = $form['#entity']; + } + } + else { + return $element; + } + + $dependencies = ddf_load_dependencies($entity_type, $bundle); + if (empty($dependencies)) { + return $element; + } + + if (!empty($entity)) { + list($entity_id,,) = entity_extract_ids($entity_type, $entity); + } + if (empty($entity_id)) { + $entity_id = 'NULL'; + } + + foreach ($dependencies as $dependency) { + // Process dependent field. + if ($dependency[1] == $field['#field_name']) { + if (isset($field['#name'])) { + $settings = array( + 'ddf' => array( + $form['#build_id'] . ':' . $entity_type . ':' . $bundle => array( + 'form_id' => $form['#build_id'], + 'dependent' => array($field['#field_name'] => $field['#name']), + ), + ), + ); + drupal_add_js($settings, 'setting'); + } + } + + // Process controlling field. + if ($dependency[0] == $field['#field_name']) { + if (isset($field['#name'])) { + $settings = array( + 'ddf' => array( + $form['#build_id'] . ':' . $entity_type . ':' . $bundle => array( + 'form_id' => $form['#build_id'], + 'entity_type' => $entity_type, + 'bundle' => $bundle, + 'entity_id' => $entity_id, + 'fields' => array($field['#field_name'] => $field['#name']), + ), + ), + ); + drupal_add_js($settings, 'setting'); + drupal_add_library('system', 'drupal.ajax'); + drupal_add_library('system', 'jquery.form'); + drupal_add_js(drupal_get_path('module', 'ddf') . '/ddf.js'); + } + } + } + + return $element; +} + +/** + * Implements hook_field_widget_WIDGET_TYPE_form_alter(). + */ +function ddf_field_widget_options_select_form_alter(&$element, &$form_state, $context) { + // Fix dependent fields with selects which loose multiple property for empty lists, see options_field_widget_form() function in options.module file. + $dependencies = ddf_load_dependencies($context['instance']['entity_type'], $context['instance']['bundle']); + if (empty($dependencies)) { + return; + } + $field = $context['field']; + foreach ($dependencies as $dependency) { + if ($dependency[1] == $field['field_name']) { + if ((is_array($dependency[2])) && (isset($dependency[2]['type'])) && ($dependency[2]['type'] != 'options')) { + // Not options-type dependency, no special processing is required. + continue; + } + $ddf_processor = array('ddf_selector_element_process'); + if (empty($element['#process'])) { + $info = element_info($element['#type']); + if (!empty($info['#process'])) { + $element['#process'] = array_merge($info['#process'], $ddf_processor); + } + } + else { + $element['#process'] = $ddf_processor; + } + $element['#value_callback'] = 'ddf_selector_element_value'; + if ((isset($element['#multiple'])) && (!$element['#multiple']) && ($field['cardinality'] != 1)) { + $element['#multiple'] = TRUE; + } + } + } +} + +function ddf_selector_element_process($element, &$form_state, $form) { + // Do not process hidden fields. + if ((isset($field['#access'])) && ($field['#access'] == FALSE)) { + return $element; + } + + if ($form_state['process_input']) { + $entity_type = $element['#entity_type']; + $bundle = $element['#bundle']; + $entity = $element['#entity']; + $properties = $element['#properties']; + $field_name = $element['#field_name']; + $dependencies = ddf_load_dependencies($entity_type, $bundle); + if (empty($dependencies)) { + return $element; + } + $field = field_info_field($field_name); + $instance = field_info_instance($entity_type, $field_name, $bundle); + $form_state_copy = $form_state; + $form_copy = $form; + + foreach ($dependencies as $dependency) { + if ($dependency[1] == $field_name) { + $controlling_field_name = $dependency[0]; + $controlling_field = field_info_field($controlling_field_name); + $columns = ($controlling_field['type'] === 'entityreference') ? array('target_id') : array_keys($controlling_field['columns']); + $available_languages = field_available_languages($entity_type, $controlling_field); + $languages = _field_language_suggestion($available_languages, NULL, $controlling_field_name); + foreach ($languages as $langcode) { + $path = array_merge($form_copy['#parents'], array($controlling_field['field_name'], $langcode)); + $key_exists = NULL; + $items = drupal_array_get_nested_value($form_state_copy['values'], $path, $key_exists); + if ($key_exists) { + if (!is_array($items)) { + foreach ($columns as $column) { + $entity->{$controlling_field_name}[$langcode] = array(0 => array($column => $items)); + } + } + elseif ($items !== array()) { + if (array_key_exists($columns[0], $items)) { + $entity->{$controlling_field_name}[$langcode] = array(0 => $items); + } + else { + $entity->{$controlling_field_name}[$langcode] = $items; + } + } + } + } + } + } + $options = _options_get_options($field, $instance, $properties, $entity_type, $entity); + $element_copy = $element; + $element_copy['#options'] = $options; + $default_items = field_get_default_value($entity_type, $entity, $field, $instance); + $context = array( + 'form' => $form_copy, + 'field' => $field, + 'instance' => $instance, + 'langcode' => LANGUAGE_NONE, + 'items' => $default_items, + 'delta' => 0, + ); + drupal_alter(array('field_widget_form', 'field_widget_' . $instance['widget']['type'] . '_form'), $element_copy, $form_state_copy, $context); + $element['#options'] = $element_copy['#options']; + } + + return $element; +} + +/** + * Replacement for form_type_select_value() functions for selectors. + */ +function ddf_selector_element_value(&$element, $input, $form_state) { + $element['#after_build'][] = 'ddf_remove_validation'; + return form_type_select_value($element, $input); +} + +/** + * Disables field validation selectors. + */ +function ddf_remove_validation(&$element, &$form_state) { + unset($element['#needs_validation']); + return $element; +} diff --git a/sites/all/modules/ddf/modules/ddf_entityreference/ddf_entityreference.info b/sites/all/modules/ddf/modules/ddf_entityreference/ddf_entityreference.info new file mode 100644 index 0000000..de82ac9 --- /dev/null +++ b/sites/all/modules/ddf/modules/ddf_entityreference/ddf_entityreference.info @@ -0,0 +1,15 @@ +name = DDF Entity Reference +description = Supports dynamic dependency for entity reference fields. +core = 7.x +package = Fields +dependencies[] = ddf +dependencies[] = entityreference +dependencies[] = views +files[] = plugins/selection/ddf.inc + +; Information added by Drupal.org packaging script on 2015-08-13 +version = "7.x-1.8" +core = "7.x" +project = "ddf" +datestamp = "1439462340" + diff --git a/sites/all/modules/ddf/modules/ddf_entityreference/ddf_entityreference.module b/sites/all/modules/ddf/modules/ddf_entityreference/ddf_entityreference.module new file mode 100644 index 0000000..eb65bf5 --- /dev/null +++ b/sites/all/modules/ddf/modules/ddf_entityreference/ddf_entityreference.module @@ -0,0 +1,119 @@ + $entity_type, '#entity' => $entity, '#required' => $instance['required']); + $element = options_field_widget_form($form, $form_state, $dependent_field, $instance, LANGUAGE_NONE, array(), 0, $element); + + $default_items = field_get_default_value($entity_type, $entity, $field, $instance, LANGUAGE_NONE); + $context = array( + 'form' => $form, + 'field' => $field, + 'instance' => $instance, + 'langcode' => LANGUAGE_NONE, + 'items' => $default_items, + 'delta' => 0, + ); + drupal_alter(array('field_widget_form', 'field_widget_' . $instance['widget']['type'] . '_form'), $element, $form_state, $context); + + $result = ''; + if (isset($element['#options'])) { + if (!empty($element['#properties'])) { + $position = NULL; + if ((isset($element['#properties']['empty_option'])) && ($element['#properties']['empty_option'] == 'option_select')) { + if (count($element['#options']) == 2) { + $position = 1; + } + } + elseif (!isset($element['#properties']['empty_option'])) { + if (count($element['#options']) == 1) { + $position = 0; + } + } + if (!is_null($position)) { + $keys = array_keys($element['#options']); + if (isset($keys[$position])) { + $element['#value'] = $keys[$position]; + } + } + } + $result = form_select_options($element); + } + + $commands = array(); + $command = ajax_command_html($selector, $result); + $command['command'] = 'ddf_insertnowrap'; + $commands[] = $command; + + // If chosen is applied, it can't be updated by attachBehavior(). + $commands[] = ajax_command_invoke($selector, 'trigger', array('liszt:updated')); + $commands[] = ajax_command_invoke($selector, 'trigger', array('chosen:updated')); + + // Options are changed, so run 'change' handlers. + $commands[] = ajax_command_invoke($selector, 'trigger', array('change')); + + return $commands; +} + +/** + * Implements hook_field_update_field(). + */ +function ddf_entityreference_field_update_field($field, $prior_field, $has_data) { + if ($field['type'] != 'entityreference') { + return; + } + // We need 'bundles' element of the field structure. + $field = field_info_field($field['field_name']); + + // Remove old dependencies. + foreach ($field['bundles'] as $entity_type => $bundles) { + foreach ($bundles as $bundle) { + ddf_remove_dependency($field['field_name'], $entity_type, $bundle); + } + } + // Add new dependencies. + if (isset($field['settings']['handler_settings']['view']['args'])) { + $controlling_fields = array(); + foreach ($field['settings']['handler_settings']['view']['args'] as $arg) { + $matches = array(); + if (preg_match('/^\{([^{}]+)\}$/', $arg, $matches)) { + $field_name = $matches[1]; + if (!empty($controlling_fields[$field_name])) { + // Do not try to re-add the same dependency. + continue; + } + $controlling_fields[$field_name] = TRUE; + foreach ($field['bundles'] as $entity_type => $bundles) { + foreach ($bundles as $bundle) { + if (field_info_instance($entity_type, $field_name, $bundle)) { + ddf_add_dependency($field_name, $field['field_name'], $entity_type, $bundle, array('type' => 'options')); + } + } + } + } + } + } +} diff --git a/sites/all/modules/ddf/modules/ddf_entityreference/plugins/selection/EntityReference_SelectionHandler_DDF.class.php b/sites/all/modules/ddf/modules/ddf_entityreference/plugins/selection/EntityReference_SelectionHandler_DDF.class.php new file mode 100644 index 0000000..bfa3411 --- /dev/null +++ b/sites/all/modules/ddf/modules/ddf_entityreference/plugins/selection/EntityReference_SelectionHandler_DDF.class.php @@ -0,0 +1,218 @@ + $value) { + self::$controlling_field_values[$entity_type][$entity_id][$key] = $value; + } + } + + /** + * Implements EntityReferenceHandler::getInstance(). + */ + public static function getInstance($field, $instance = NULL, $entity_type = NULL, $entity = NULL) { + return new EntityReference_SelectionHandler_DDF($field, $instance, $entity_type, $entity); + } + + protected function __construct($field, $instance, $entity_type, $entity) { + $this->field = $field; + $this->instance = $instance; + $this->entity_type = $entity_type; + $this->entity = $entity; + // Get the entity token type of the entity type. + $entity_info = entity_get_info($entity_type); + $this->entity_type_token = isset($entity_info['token type']) ? $entity_info['token type'] : $entity_type; + } + + /** + * Implements EntityReferenceHandler::settingsForm(). + */ + public static function settingsForm($field, $instance) { + $form = parent::settingsForm($field, $instance); + + if (isset($form['view']['args'])) { + $master_fields = array(); + $other_instances = field_info_instances($instance['entity_type'], $instance['bundle']); + foreach ($other_instances as $other_instance) { + if ($other_instance['field_name'] == $instance['field_name']) { + continue; + } + if (($other_instance['widget']['type'] != 'options_select') && ($other_instance['widget']['type'] != 'options_buttons')) { + continue; + } + $other_field = field_info_field($other_instance['field_name']); + if ($other_field['cardinality'] != 1) { + continue; + } + if (($other_field['type'] === 'entityreference') || (count($other_field['columns']) === 1)) { + $master_fields[$other_instance['field_name']] = htmlspecialchars($other_instance['label']); + } + } + if (!empty($master_fields)) { + $dynamic_token_list = ''; + foreach ($master_fields as $master_field_name => $master_field_label) { + $dynamic_token_list .= '{' . $master_field_name . '} - ' . $master_field_label . '
'; + } + $form['view']['dynamic_help'] = array( + '#type' => 'item', + '#title' => t('Dynamic arguments'), + '#description' => + t('The list of entities that can be referenced can depend on the current values of other fields. When the user changes these fields, the list is rebuilt.'), + '#markup' => t('The following dynamic tokens can be used as view arguments:') . '
' . $dynamic_token_list, + ); + } + } + + return $form; + } + + /** + * Implements EntityReferenceHandler::getReferencableEntities(). + */ + public function getReferencableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) { + $display_name = $this->field['settings']['handler_settings']['view']['display_name']; + $args = $this->handleArgs($this->field['settings']['handler_settings']['view']['args']); + $args = $this->handleDynamicArgs($args); + $result = array(); + if ($this->initializeView($match, $match_operator, $limit)) { + // Get the results. + $result = $this->view->execute_display($display_name, (!array_filter($args) ? array() : $args)); + } + + $return = array(); + if ($result) { + $target_type = $this->field['settings']['target_type']; + $entities = entity_load($target_type, array_keys($result)); + foreach ($entities as $entity) { + list($id,, $bundle) = entity_extract_ids($target_type, $entity); + $return[$bundle][$id] = $result[$id]; + } + } + return $return; + } + + public function validateReferencableEntities(array $ids) { + $display_name = $this->field['settings']['handler_settings']['view']['display_name']; + $args = $this->handleArgs($this->field['settings']['handler_settings']['view']['args']); + $args = $this->handleDynamicArgs($args); + $result = array(); + if ($this->initializeView(NULL, 'CONTAINS', 0, $ids)) { + // Get the results. + $entities = $this->view->execute_display($display_name, $args); + if (is_null($entities)) { + $entities = array(); + } + $result = array_keys($entities); + } + return $result; + } + + /** + * Handles arguments for views. + * + * Replaces tokens using token_replace(). + * + * @param array $args + * Usually $this->field['settings']['handler_settings']['view']['args']. + * + * @return array + * The arguments to be send to the View. + */ + protected function handleArgs($args) { + // Parameters for token_replace(). + $data = array(); + $options = array('clear' => TRUE); + + if ($this->entity) { + $data = array($this->entity_type_token => $this->entity); + } + // Replace tokens for each argument. + foreach ($args as $key => $arg) { + $args[$key] = token_replace($arg, $data, $options); + } + return $args; + } + + protected function handleDynamicArgs($args) { + $dynamic_args = array(); + foreach ($args as $key => $arg) { + $matches = array(); + if (preg_match('/^\{([^{}]+)\}$/', $arg, $matches)) { + $dynamic_args[$key] = $matches[1]; + } + } + if (empty($dynamic_args)) { + return $args; + } + + $entity_id = 0; + if (!is_null($this->entity)) { + list($entity_id,,) = entity_extract_ids($this->entity_type, $this->entity); + if (empty($entity_id)) { + $entity_id = 0; + } + } + + foreach ($dynamic_args as $key => $field_name) { + $field = field_info_field($field_name); + if (!$field) { + $args[$key] = ''; + continue; + } + // Workaround for possible Entity reference weirdness with field columns. + $columns = ($field['type'] === 'entityreference') ? array('target_id') : array_keys($field['columns']); + + if (count($columns) != 1) { + $args[$key] = ''; + continue; + } + + $column = $columns[0]; + if ((isset(self::$controlling_field_values[$this->entity_type][$entity_id])) + && (array_key_exists($field_name, self::$controlling_field_values[$this->entity_type][$entity_id]))) { + $args[$key] = self::$controlling_field_values[$this->entity_type][$entity_id][$field_name]; + } + elseif (isset($this->entity->{$field_name})) { + foreach ($this->entity->{$field_name} as $values) { + foreach ($values as $value) { + if (!is_array($value)) { + $args[$key] = $value; + } + elseif (array_key_exists($column, $value)) { + $args[$key] = $value[$column]; + } + } + } + } + else { + $args[$key] = ''; + $instance = field_info_instance($this->instance['entity_type'], $field_name, $this->instance['bundle']); + $default_values = field_get_default_value($this->instance['entity_type'], $this->entity, $field, $instance); + if (!empty($default_values)) { + foreach ($default_values as $value) { + if (array_key_exists($column, $value)) { + $args[$key] = $value[$column]; + } + } + } + } + } + return $args; + } + +} diff --git a/sites/all/modules/ddf/modules/ddf_entityreference/plugins/selection/ddf.inc b/sites/all/modules/ddf/modules/ddf_entityreference/plugins/selection/ddf.inc new file mode 100644 index 0000000..f646c77 --- /dev/null +++ b/sites/all/modules/ddf/modules/ddf_entityreference/plugins/selection/ddf.inc @@ -0,0 +1,9 @@ + t('Views: Filter by an entity reference view with dynamic arguments'), + 'class' => 'EntityReference_SelectionHandler_DDF', + 'weight' => 100, + ); +} diff --git a/sites/all/modules/hierarchical_select/API.txt b/sites/all/modules/hierarchical_select/API.txt new file mode 100644 index 0000000..aa7cfaa --- /dev/null +++ b/sites/all/modules/hierarchical_select/API.txt @@ -0,0 +1,693 @@ + +Terminology +----------- +- item: an item in the hierarchy. A hierarchy can also be seen as a tree. In + that case, an item can be either a parent or a child. However, if + "multiple parents" are supported (i.e. a child can have multiple + parents), then it's actually not a tree but a directed acyclic graph + (see http://en.wikipedia.org/wiki/Directed_acyclic_graph), in which + each case technically is a "node". + An example: in the case of taxonomy, this is the term id (tid). +- label: the label associated with an item in the hierarchy. You may now it + as "title" or something else similar. + An example: in the case of taxonomy, this is the actual term. +- item type: a per-level, human-readable name that describes what kind of + items that level contains. +- entity: an item is often associated with an entity. E.g. a term is usually + associated with a node. +- form element: a form element allows the developer to assign a new value to + a #type property in a form item. Examples of form elements + supported by Drupal core are: select, checkboxes, textfield. +- form item: an instance of a form element, with various other properties + defined, such as #title, #default_value and #description. These + are used to define a form in Drupal. +- Hierarchical Select: this is the name of the module. +- hierarchical_select: this is the internal name of the Hierarchical Select + form element. +- hierarchical select: (note the difference in case) this is the part of the + widget with the multiple selects. +- dropbox: this is the part of the widget where the selections are stored when + multiple selections are allowed. + + +Form API usage +-------------- +You have to make sure your form item is using the "hierarchical_select" form +element type: + + $form['select_some_term'] = array( + '#type' => 'hierarchical_select', + '#title' => t('Select the tag you wish to use.'), + '#size' => 1, + '#config' => array( + 'module' => 'hs_taxonomy', + 'params' => array( + 'vid' => $vid, + ), + 'save_lineage' => 0, + 'enforce_deepest' => 0, + 'resizable' => 1, + 'level_labels' => array( + 'status' => 0, + 'labels' => array( + 0 => t('Main category'), + 1 => t('Subcategory'), + 2 => t('Third level category'), + ), + ), + 'dropbox' => array( + 'status' => 0, + 'title' => t('All selections'), + 'limit' => 0, + 'reset_hs' => 1, + 'sort' => 1, + ), + 'editability' => array( + 'status' => 0, + 'item_types' => array(), + 'allowed_levels' => array( + 0 => 0, + 1 => 0, + 2 => 1, + ), + 'allow_new_levels' => 0, + 'max_levels' => 3, + ), + 'entity_count' => array( + 'enabled' => 0, + 'require_entity' => 0, + 'settings' => array( + 'count_children' => 0, + 'entity_types' => array(), + ), + ), + // These settings cannot be configured through the UI: they can only be + // overridden through code. + 'animation_delay' => 400, + 'special_items' => array(), + 'render_flat_select' => 0, + ), + '#default_value' => '83', + ); + +Now, let's explain what we see here: +1) We've set the #type property to "hierarchical_select" instead of "select". +2) The #size property is inherited by the selects of the hierarchical select. + You can use it to change a vertical size of the select (i.e. change how many + items are displayed in the select, similar to a form select multiple). +3) There's a new property: #config. This must be an +array. These are the items it can contain: + - module (required) + This will be passed through in the AJAX requests, to let Hierarchical + Select know which module's hooks should be used. + + - params (optional, may be necessary for some implementations) + An array of parameters that will also be passed through in every AJAX + request. + e.g. In the case of taxonomy, this is the vocabulary id (vid). In case of + content_taxonomy, there's three parameters: vid, tid and depth (tid allows + one to define a new root, depth allows one to limit the depth of the + displayed hierarchy). + + - save_lineage (optional, defaults to 0) + Triggers the lineage saving functionality. If enabled, the selection can + consist of multiple values. + + - enforce_deepest (optional, defaults to 0) + Triggers the enforcing of a selection in the deepest level. If enabled, the + selection will always be a single value. + + - resizable (optional, defaults to 1) + Makes the hierarchical select resizable. + + - level_labels['status'] (optional, defaults to 0) + Whether level labels should be enabled or not. When save_lineage is + enabled, this will result in *empty* level labels. + + - level_labels['labels'] (optional) + An array of labels, one per level. The label for the first level should be + the value of key 0. + When enforce_deepest is set to: + - 0, then you can provide n level labels, with n the number of levels + - 1, then you can provide only one level label. + + - dropbox['status'] (optional, defaults to 0) + Whether the dropbox is enabled or not (the dropbox allows the user to make + multiple selections). + + - dropbox['title'] (optional, defaults to "All selections:") + The title of the dropbox. The dropbox is the area where all selections are + displayed when the dropbox is enabled. + + - dropbox['limit'] (optional, defaults to 0, which means "no limit") + Limit the number of selection that can be added to the dropbox. So this + allows you the restrict the number of items that can be selected when + the dropbox has been enabled. + + - dropbox['reset_hs'] (optional, defaults to 1, which means "do reset") + Determines what will happen to the hierarchical select when the user has + added a selection to the dropbox. + + - dropbox['sort'] (optional, defaults to 1, which means "do sort") + Determines whether the items in the dropbox will be automatically sorted. + + - editability['status] (optional, defaults to 0) + Allow the user to create new items in the hierarchy. + + - editability['item_types'] (optional, defaults to the empty array) + Only meaningful when editable is set to TRUE. + Set the item type for each level. E.g.: "country" for the first level, + "region" for the second and "city" for the third. When the user then wants + to create a new item, the default label for the new item will be of the + form "new ", e.g. "new region". + + - editability['allowed_levels'] (optional, defaults to 1 for each level) + Only meaningful when editable is set to TRUE. + Specify in which levels the user is allowed to create new items. In the + example, the user is only allowed to create new items in the third level. + When a setting for a level is ommitted, it defaults to 1 (i.e. allowed for + that level). This means you only have to specify in which levels the user + is not allowed to create new items. + This only applies to *existing* levels: it does not affect the + allow_new_levels setting (the next setting). + + - editability['allow_new_levels'] (optional, defaults to 0) + Only meaningful when editable is set to TRUE. + Allow the user to create new levels, i.e. when a certain item does not yet + have children, the user can create a first child for it (thus thereby + creating a new level). + + - editability['max_levels'] (optional, defaults to 3) + Only meaningful when editable_settings['allow_new_levels'] is set to TRUE. + Limits the maximum number of levels. Don't set this too high or you'll end + up with very deep hierarchies. This only affects how deep new levels can be + created, it will not affect the existing hierarchy. + + - entity_count['enabled'] (optional, defaults to 0) + Enables the display of entity counts, between parentheses, for each item in + the hierarchy. + + - entity_count['require_entity'] (optional, defaults to 0) + Whether an item should only be displayed if it has at least one associated + entity. + + - entity_count['settings']['count_children'] (optional, defaults to 0) + Whether the entity count should also count associated children of the entity. + + - entity_count['settings']['entity_types'] (optional, defaults to array()) + Which types of entities should be counted. This is a list of checkboxes that + allow the user to select entity types by bundles. + + - animation_delay (optional, defaults to 400) + The delay of each animation (the drop in left and right animations), in ms. + + - special_items (optional, defaults to the empty array) + Through this setting, you can mark each item with special properties it + possesses. There currently are two special properties: 'exclusive' and + 'none'. + Note: you should include these items in the hierarchy as if it were a + normal item and then you can mark them as special through this property. + * 'exclusive': Sometimes it's desirable to have exclusive lineages. When + such an option is selected, the user should not be able to + select anything else. This also means that nothing else in + the dropbox can be selected: if the dropbox contains + anything, it will be reset. + Can be applied to multiple items. + e.g. an 'entire_tree' item: + 'special_items' => array( + 'entire_tree' => array('exclusive'), + ) + * 'none': Sometimes you want to replace the default '' option by + something else. This replacement should of course also exist in + the root level. + Can be applied to only one item. + e.g. an 'any' item (used in hs_taxonomy_views): + 'special_items' => array( + 'any' => array('none', 'exclusive'), + ) + And a final example for a better overview: + 'special_items' => array( + 'entire_tree' => array('exclusive'), + 'any' => array('none', 'exclusive'), + ) + + - render_flat_select (optional, defaults to 0) + Because the hierarchical_select form element consists of multiple form + items, it doesn't work well in GET forms. By enabling this setting, a flat + select will also be rendered, that contains only the selected lineages. + Combine that with Drupal.HierarchicalSelect.prepareGETSubmit in the JS code + (or, alternatively, the 'prepare-GET-submit' event that can be triggered, + see the JavaScript events section for details) and you have a work-around + (which, admittedly, only works when JS is enabled). + +3) We *don't* specify a list of options: Hierarchical Select automatically +generates the options for us, thanks to the 'module' and 'params' settings. + + +Concepts +-------- +- Item Unicity: each item in the hierarchy must be *unique*. It doesn't have + to be numerical, it can also be a string. + If your hierarchy does not have unique items by nature or by + design (your items may be unique per level instead), that's + not a problem. You can simply prepend the item's ancestors to + get a unique item. + e.g. you have an item "foobar" at the first, second and third + levels. By prepending the ancestors using the dash as the + separator, you'd get an item "foobar-foobar-foobar" at the + third level. + Also see the "Reserved item values" section. +- #options: it's gone, because it was the inherent cause for scalability + problems: if a hierarchy consists of 10,000 or even 100,000 items, + this results in huge HTML being generated. Huge HTML means more + processing power necessary, and more bandwidth necessary. So where + does Hierarchical Select get its "options"? It uses the hooks that + every implementation has to implement to only get what it needs. +- The General Concept: you should think of Hierarchical Select as an abstract + widget that can represent *any* hierarchy. To be able + to display any hierarchy, you obviously need some + universal way to "browse" a hierarchy. + If you are familiar with C++ or Java iterators, this + should come natural: the hooks you have to implement + is what allows Hierarchical Select to iterate over your + hierarchy. Then the heart of the iterator would be the + root_level() and children() hooks. params() allows you + to define which information is necessary before you can + determine *which* hierarchy or which *part* of the + hierarchy is being browsed. lineage() must return the + lineage, i.e. the item itself and all its ancestors, + this allows a hierarchy to be generated from just one + (selected) item. + + +Reserved item values +-------------------- +- Ensure that your items don't have a "none", "all", "create_new_item" nor + "label_\d+" values (the latter means "label_" followed by one or more + digits). Your values should also not contain a pipe ("|"), since pipes are + used to separate the selection of values that are sent back to the server + in the callbacks. +- Valid 'empty' selections (i.e. if you want to set the #default_value + property of your form item), are -1 and the empty array. The empty string is + also considered valid, because Drupal core's Taxonomy module uses this as + the empty selection. + + +Developer mode +-------------- +When you are writing your implementation of the Hierarchical Select API, you +will often wonder what Hierarchical Select is doing internally with the data +you're feeding it. That's why there's a developer mode: it will show you this +data, even the data generated in AJAX callbacks. It'll also show you the time +it took to generate the lineage, to fill up the levels and to calculate the +child info, to track down badly performing code. +Also, when you're just creating a new HS config and it doesn't quite work +right, it can be helpful to enable the developer mode. It will perform some +basic diagnostics that might help you track down the cause. +To use this, you must have a browser with console.log() support. Install +Firebug Lite (http://getfirebug.com/lite.html) if your browser does not +suport this. Next, go to Hierarchical Select's .module file and set the define +for the HS_DEVELOPER_MODE constant to TRUE. +When you now open Firebug (Firefox) or the Web Inspector (Safari), you'll see +the debug output. New output is added after each callback to the server. + + +Hierarchical Select implementations: gotcha's +--------------------------------------------- +- "warning: Missing argument 1 for drupal_retrieve_form() …" + This implies that your implementation's module weight is heavier than + hierarchical_select.module. In that case, Hierarchical Select will not be + able to detect hierarchical_select form items, preventing it from applying + some magic, and AJAX updates won't work. + + +Hierarchical Select compatibility: gotcha's +------------------------------------------- +- "Invalid response from server" + This typically means that some functions could not be found when + Hierarchical Select does an AJAX callback to the server, which in turn means + that some code (some PHP file) has not been included, while it should have + been. Instead of using module_load_include() or even require_once, you + should use form_load_include(). This function is new in Drupal 7 and will + ensure that all required PHP files are included automatically. + + +Hierarchical Select API Tutorial +-------------------------------- +Written by Stephen Barker of Digital Frontiers Media +(http://drupal.org/user/106070) and reviewed by Wim Leers: + http://drupal.org/node/532724 + + +Hierarchical Select Small Hierarchy +----------------------------------- +Hierarchical Select includes a Hierarchical Select API implementation that +allows one to use a hardcoded hierarchy. When it becomes to slow, you should +move the hierarchy into the database and write a proper implementation. +Below you can find an example of how to use the hs_smallhierarchy module. Just +change the $hierarchy array to suit your needs and off you go! Look at the +code of hs_smallhierarchy.module for full details, but this code example +should get you started. + + $hierarchy = array( + 'win' => array( + 'label' => 'Windows', + 'children' => array( + 'xp' => array('label' => 'XP'), + 'vista' => array( + 'label' => 'Vista', + 'children' => array( + 'x86' => array('label' => '32-bits'), + 'x64' => array('label' => '64-bits'), + ), + ), + ), + ), + ); + + $form['select_some_term'] = array( + '#type' => 'hierarchical_select', + '#title' => t('Select the tag you wish to use.'), + '#size' => 1, + '#config' => array( + 'module' => 'hs_smallhierarchy', + 'params' => array( + 'hierarchy' => $hierarchy, + 'id' => 'my-hierarchy-about-windows', + 'separator' => '|', + ), + 'save_lineage' => 0, + 'enforce_deepest' => 0, + 'resizable' => 1, + 'level_labels' => array( + 'status' => 0, + 'labels' => array( + 0 => t('Main category'), + 1 => t('Subcategory'), + 2 => t('Third level category'), + ), + ), + 'dropbox' => array( + 'status' => 0, + 'title' => t('All selections'), + 'limit' => 0, + 'reset_hs' => 1, + 'sort' => 1, + ), + 'editability' => array( + 'status' => 0, + 'item_types' => array(), + 'allowed_levels' => array( + 0 => 0, + 1 => 0, + 2 => 1, + ), + 'allow_new_levels' => 0, + 'max_levels' => 3, + ), + 'entity_count' => array( + 'enabled' => 0, + 'require_entity' => 0, + 'settings' => array( + 'count_children' => 0, + 'entity_types' => array(), + ), + ), + // These settings cannot be configured through the UI: they can only be + // overridden through code. + 'animation_delay' => 400, + 'exclusive_lineages' => array(), + 'render_flat_select' => 0, + ), + '#description' => 'Put your description here', + '#default_value' => 'win|xp|x86', + ); + + +Hooks +----- +1) hook_hierarchical_select_params(); + Returns an array with the names of all parameters that are necessary for + this implementation to work. + +2) hook_hierarchical_select_root_level($params, $dropbox = FALSE); + Returns the root level of the hierarchy: an array of (item, label) pairs. + The $dropbox parameter can is optional and can even ommitted, as it's only + necessary if you need the dropbox to influence your hierarchy. + +3) hook_hierarchical_select_children($parent, $params, $dropbox = FALSE); + Gets the children of $parent ($parent is an item in the hierarchy) and + returns them: an array of (item, label) pairs, or the empty array if the + given $parent has no children. + The $dropbox parameter can is optional and can even ommitted, as it's only + necessary if you need the dropbox to influence your hierarchy. + +4) hook_hierarchical_select_lineage($item, $params); + Calculates the lineage of $item (array of items, with $item the last) and + returns it. Necessary when the "enforce_deepest" option is enabled. + +5) hook_hierarchical_select_valid_item($item, $params); + Validates an item, returns TRUE if valid, FALSE if invalid. + +6) hook_hierarchical_select_item_get_label($item, $params); + Given a valid item, returns the label. Is only used for rendering the + selections in the dropbox. + +7) hook_hierarchical_select_create_item($label, $parent, $params); + Given a parent item and the label of a new item, create a new item as a + child of the parent item. When $parent == 0, this means a new item is being + created at the root level. + Optional hook. When this hook is not implemented, this functionality will + never be used, even when you configure it that way in code. + +8) hook_hierarchical_select_entity_count($item, $params); + Given a item, get the number of entities (most of the time the entity type + is 'node') that are related to the given item. Used for the entity_count + and require_entity settings. + Optional hook. When this hook is not implemented, this functionality will + never be used, even when you configure it that way (i.e. when you enable + the entity_count and require_entity settings). + +9) hook_hierarchical_select_implementation_info(); + Return metadata about this implementation. + This information is used to generate the implementations overview at + admin/settings/hierarchical_select/implementations. The expected format is: + + array( + 'hierarchy type' => t('Taxonomy'), + 'entity type' => t('Node'), + 'entity' => t('Story'), + 'context type' => t('Node form'), + 'context' => '', + ); + + another example: + + array( + 'hierarchy type' => t('Taxonomy'), + 'entity type' => t('Node'), + 'entity' => '', + 'context type' => t('Views exposed filter'), + 'context' => t('some view'), + ); + +10) hook_hierarchical_select_config_info(); + Return metadata about each available user-editable configuration for this + implementation. + Optional hook. This information is used to generate the configurations + overview at admin/settings/hierarchical_select/configs. The expected + format is: + + $config_info[$config_id] = array( + 'config_id' => $config_id, + 'hierarchy type' => t('Taxonomy'), + 'hierarchy' => t($vocabulary->name), + 'entity type' => t('Node'), + 'entity' => implode(', ', array_map('t', $entities)), + 'edit link' => "admin/content/taxonomy/edit/vocabulary/$vid", + ); + + +Standardized configuration form +------------------------------- +Hierarchical Select 3 comes with a standardized configuration form: +hierarchical_select_common_config_form(). This function accepts a lot of +parameters, which allows you to use names typical to your module's hierarchy +(e.g. 'leaf' instead of 'term' and 'tree' instead of 'vocabulary'). A submit +handler is also provided, of course. +An example: + + // I'm not configuring all parameters here. For an example of that, see one + // of the included modules. + $form['foobar_hierarchical_select_config'] = hierarchical_select_common_config_form($module, $params, $config_id, $defaults, $strings, $max_hierarchy_depth, $preview_is_required); + + // Add the the submit handler for the Hierarchical Select config form. + $parents = array('foobar_hierarchical_select_config'); + $form['#submit'][] = 'hierarchical_select_common_config_form_submit'; + $form['#hs_common_config_form_parents'] = $parents; + + +Configuration management +------------------------ +It's now possible to export Hierarchical Select configurations, and there is a +function to set the configuration of a certain Hierarchical Select. Combine +the two and you can manage your Hierarchical Select configurations in code! +An example: + + // The exported configuration. + $config = array( … ); + $config_id = $config['config_id]; + + // Apply the configuration. + require_once(drupal_get_path('module', 'hierarchical_select') .'/includes/common.inc'); + hierarchical_select_common_config_set($config_id, $config); + + +JavaScript events +----------------- +The Hierarchical Select module's JavaScript code triggers several events, to +allow for advanced interactions. + +You can find all hierarchical_select form items using this selector: + + $('.hierarchical-select-wrapper'); + +You can find a *specific* hierarchical_select form item using this selector: + + $('#hierarchical-select-x-wrapper'); + +where x is a number, or more accurately: a hsid (hierarchical select id). +Retrieving all hsids in the current document can be done like this: + + for (var hsid in Drupal.settings.HierarchicalSelect.settings) { + // … + } + +Alternatively, you can use one of the transliterated class names. A wrapper +for Hierarchical Select looks like this: +
+ … +
+Hence, you could also use selectors such as these, to achieve the same effect, +but with more robust code: + $('.hierarchical-select-wrapper-for-config-taxonomy-1:first') + .trigger('enforce-update'); + $('.hierarchical-select-wrapper-for-name-edit-taxonomy-1:first') + .trigger('enforce-update'); + +The following events are triggered: + - change-hierarchical-select + - update-hierarchical-select + - create-new-item + - cancel-new-item + - add-to-dropbox (check https://www.drupal.org/node/1277068) + - remove-from-dropbox + - enforced-update + - prepared-GET-submit +All events are triggered *after* the animations have completed. + +However, it's often useful to do something *before* an event (especially +because all of the above events perform an AJAX request to the server). So, +the equivalent "before" events exist as well: + - before-update-hierarchical-select + - before-create-new-item + - before-cancel-new-item + - before-add-to-dropbox + - before-remove-from-dropbox + - before-enforced-update +There is one exception: when the cache is enabled, the "before update +hierarchical select" event will not be triggered. This makes sense, because +updates from the cache are instantaneous. + +An example of binding a function to the 'create-new-item' event of the second +(hsid == 1) hierarchical_select form item on the page: + + $('#hierarchical-select-1-wrapper') + .bind('create-new-item', function() { + // … + }); + +And finally, you can trigger a special event to enforce an update (this can be +useful when you have changed a hierarchy through another form item, or for +live previews, or …). You can then also pass additional information that will +be POSTed. You can even disable normal updates, to manage that completely +yourself via enforced updates. This allows you to write a Hierarchical Select +implementation that gets some of its information ($params) from another form +item! +Suppose you'd like to enforce an update of the first (hsid == 0) +hierarchical_select form item on the page: + + $('#hierarchical-select-0-wrapper') + .trigger('enforce-update'); + +Now let's move on to a more advanced example, in which we will disable normal +updates and let another form item (here a select) provide a part of the +information that will be used to render the Hierarchical Select. Effectively, +this other form item will *influence* the hierarchy that will be presented by +Hierarchical Select! + + $(document).ready(function() { + Drupal.settings.specialfilter = {}; + + // .specialfilter-first: a select form item + // .specialfilter-second: a hierarchical_select form item + + update = function() { + var selection = Drupal.settings.specialfilter.currentSelection; + + // Send an extra parameter via POST: dynamicParameter. This is the stored + // selection. + $('.specialfilter-second') + .trigger('enforce-update', + [ + { name : 'dynamicParameter', value : selection } + ] + ); + }; + + attachHSBindings = function() { + // When a user navigates the hierarchical_select form item, we still want to + // POST the the extra dynamicParameter, or otherwise we will no longer have + // a hierarchy in the hierarchical_select form item that really depends on + // the select. + $('.specialfilter-second .hierarchical-select > select') + .change(function() { update(); }); + + $('.specialfilter-second') + .unbind('enforced-update').bind('enforced-update', function() { return attachHSBindings(); }); + }; + + // Initialize after 25 ms, because otherwise the event binding of HS will + // not yet be ready, and hence this won't have any effect + setTimeout(function() { + // Get the initial selection (before the user has changed anything). + Drupal.settings.specialfilter.currentSelection = $('.specialfilter-first').attr('value'); + + // When the select form item changes, we want to *store* that selection, and + // update the hierarchical_select form item. + $('.specialfilter-first') + .change(function() { + // Store the current selection. + Drupal.settings.specialfilter.currentSelection = $(this).attr('value'); + + update(); + }); + + $('.specialfilter-second') + .trigger('disable-updates'); + + attachHSBindings(); + }, 25); + }); + +The 'enforced-update' (notice the past tense!) event is triggered upon +completion. +An even more rarely used special event can be triggered to prepare the +hierarchical_select form element for a get submit: the 'prepare GET submit' +event. To use this event, the 'render_flat_select' setting should be enabled +in the config. diff --git a/sites/all/modules/hierarchical_select/LICENSE.txt b/sites/all/modules/hierarchical_select/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/sites/all/modules/hierarchical_select/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/sites/all/modules/hierarchical_select/README.txt b/sites/all/modules/hierarchical_select/README.txt new file mode 100644 index 0000000..39719dd --- /dev/null +++ b/sites/all/modules/hierarchical_select/README.txt @@ -0,0 +1,200 @@ + +Description +----------- +This module defines the "hierarchical_select" form element, which is a greatly +enhanced way for letting the user select items in a hierarchy. + +Hierarchical Select has the ability to save the entire lineage of a selection +or only the "deepest" selection. You can configure it to force the user to +make a selection as deep as possible in the tree, or allow the user to select +an item anywhere in the tree. Levels can be labeled, you can configure limit +the number of items that can be selected, configure a title for the dropbox, +choose a site-wide animation delay, and so on. You can even create new items +and levels through Hierarchical Select! + + +Integrates with +--------------- +* Taxonomy (Drupal core) + + +Installation +------------ +1) Place this module directory in your "modules" folder (this will usually be +"sites/all/modules/"). Don't install your module in Drupal core's "modules" +folder, since that will cause problems and is bad practice in general. If +"sites/all/modules" doesn't exist yet, just create it. + +2) Enable the Hierarchical Select and Hierarchical Select Taxonomy modules. + +3) If you want to use it for one or more of your vocabularies, go to +admin/structure/types and click the "manage fields" link for a content type on +which you're using a Term reference field. Click the "edit" link for this Term +reference field and then go to the "widget type" tab in the upper right corner. +There, you can choose the "Hierarchical Select" widget type, and when you do, +the entire Hierarchical Select configuration UI will appear: here you'll find +a whole range of Hierarchical Select settings. All settings are explained +there as well! + + +Troubleshooting +--------------- +If you ever have problems, make sure to go through these steps: + +1) Go to admin/reports/status (i.e. the Status Report). Ensure that the status + of the Hierarchical Select module is ok. + +2) Ensure that the page isn't being served from your browser's cache. Use + CTRL+R in Windows/Linux browsers, CMD+R in Mac OS X browsers to enforce the + browser to reload everything, preventing it from using its cache. + +3) When you're getting a JS alert with the following message: "Received an + invalid response from the server.", ensure that the page (of which this + form is a part) is *not* being cached. + +4) When Hierarchical Select seems to be misbehaving in a certain use case in + which terms with multiple parents are being used, make sure to enable the + "Save term lineage" setting. + Note: you may have to repeat this for every configuration in which the + vocabulary with terms that have multiple parents are being used. E.g. if + such a vocabulary is called "A", then go to + admin/config/content/hierarchical_select/configs + and edit all configuration that have "A" in the "Hierarchy" column. + +In case of problems, don't forget to try a hard refresh in your browser! + + +Limitations +----------- +- Creating new items in the hierarchy in a multiple parents hierarchy (more + scientifically: a directed acyclic graph) is *not* supported. +- Not the entire scalability problem can be solved by installing this set of + modules; read the maximum scalability section for details. +- The child indicators only work in Firefox. This *cannot* be supported in + Safari or IE. See http://drupal.org/node/180691#comment-1044691. +- The special [save-lineage-termpath] token only works with content_taxonomy + fields as long as you have the "Save option" set to either "Tag" or "Both". +- In hierarchies where items can have multiple parent items and where you have + enabled Hierarchical Select's "save lineage" setting, it is impossible to + remember individual hierarchies, unless the underlying module supports it. + So far, no module supports this. Hierarchical Select is just a form element, + not a system for storing hierarchies. + For example, if you have created a multiple parent vocabulary through the + Taxonomy module, and you have terms like this: + A -> C + A -> D + B -> C + B -> D + If you then save any two lineages in which all four terms exist, all four + lineages will be rendered by Hierarchical Select, because only the four + terms are stored and thus there is no way to recover the originally selected + two lineages. +- You can NOT expect the Hierarchical Select Taxonomy module to automagically + fix all existing nodes when you enable or disable the "save lineage" setting + and neither can you expect it to keep working properly when you reorganize + the term hierarchy. There's nothing I can do about this. Hierarchical Select + is merely a form element, it can't be held responsible for features that + Drupal core lacks or supports poorly. + See the following issues: + * http://drupal.org/node/1023762#comment-4054386 + * http://drupal.org/node/976394#comment-4054456 + + +Rendering hierarchy lineages when viewing content +------------------------------------------------- +Hierarchical Select is obviously only used for input. Hence it is only used on +the create/edit forms of content. +Combine that with the fact that Hierarchical Select is the only module capable +of restoring the lineage of saved items (e.g. Taxonomy terms). None of the +Drupal core modules is capable of storing the lineage, but Hierarchical Select +can reconstruct it relatively efficiently. However, this lineage is only +visible when creating/editing content, not when viewing it. +To allow you to display the lineages of stored items, I have provided a +theming function that you can call from within e.g. your node.tpl.php file: +the theme_hierarchical_select_selection_as_lineages($selection, $config) +function. + +Sample usage (using Taxonomy and Hierarchical Select Taxonomy): + +
taxonomy, $config); ?>
+ + +This will automatically render all lineages for vocabulary 2 (meaning that if +you want to render the lineages of multiple vocabularies, you'll have to clone +this piece of code once for every vocabulary). It will also automatically get +the current Hierarchical Select configuration for that vocabulary. + +Alternatively, you could provide the $config array yourself. Only three keys +are required: 1) module, 2) params, 3) save_lineage. For example: + +
taxonomy, $config); ?>
+ + +If you don't like how the lineage is displayed, simply override the +theme_hierarchical_select_selection_as_lineages() function from within your +theme, create e.g. garland_hierarchical_select_selection_as_lineages(). + +It's also worth mentioning that the 'hs_taxonomy_tree' tag was added to the +queries that build the term tree. As a result now you can easily change/filter +the elements that are selected by the module (see hs_taxonomy.module for more +info). + + +Setting a fixed size +-------------------- +When you don't want users to be able to resize a hierarchical select +themselves, you can set a fixed size in advance yourself +Setting #size to >1 does *not* generate #multiple = TRUE selects! And the +opposite is also true. #multiple sets the "multiple" HTML attribute. This +enables the user to select multiple options of a select. #size just controls +the "size" HTML attribute. This increases the vertical size of selects, +thereby showing more options. +See http://www.w3.org/TR/html401/interact/forms.html#adef-size-SELECT. + + +Sponsors +-------- +* Initial development: + Paul Ektov of http://autobin.ru. +* Abstraction, to let other modules than taxonomy hook in: + Etienne Leers of http://creditcalc.biz. +* Support for saving the term lineage: + Paul Ektov of http://autobin.ru. +* Multiple select support: + Marmaladesoul, http://marmaladesoul.com. +* Taxonomy Subscriptions support: + Mr Bidster Inc. +* Ability to create new items/levels: + The Worx Company, http://www.worxco.com. +* Ability to only show items that are associated with at least one entity: + Merge, http://merge.nl. +* Views 2 support: + Merge, http://merge.nl. +* Initial Drupal 7 port + folow-up fixes: + PingV, http://pingv.com. +* Port of "save lineage" functionality to Drupal 7: + Bancard Data Service + + +Author +------ +Wim Leers + +* website: http://wimleers.com/ +* contact: http://wimleers.com/contact + +The author can be contacted for paid development on this module. This can vary +from new features to Hierarchical Select itself, to new implementations (i.e. +support for new kinds of hierarchies). diff --git a/sites/all/modules/hierarchical_select/TODO.txt b/sites/all/modules/hierarchical_select/TODO.txt new file mode 100644 index 0000000..0e55ab5 --- /dev/null +++ b/sites/all/modules/hierarchical_select/TODO.txt @@ -0,0 +1,44 @@ +HS core: +✓ port: initial port +✓ fix: JS code cleanup (remove hardcoded hacks) +✓ fix: title + description (i.e. something's off with the theme wrapper) +✓ fix: #value_callback may be necessary? (see file.module) OR: ensure #return_value works +✓ fix: #element_validate callback: _hierarchical_select_validate() — verify this still works +✓ port: support multiple HS on the same page +✓ port: admin UI +✓ port: "dropbox" support +✓ upgrade path: delete cache_hierarchical_select +✓ upgrade path: documentation +✓ port: "create new item" support — see http://drupal.org/node/1087620 +✓ port: status report +- port: render_flat_select support +- port: client-side caching (use _hierarchical_select_json_convert_hierarchy_to_cache()) +- feature: live preview of HS on the common config form +- refactor: use the proper #value_callback -> #process callback -> #after_build callback pipeline as described in the documentation for form_builder() in form.inc + +Taxonomy: +✓ port: admin UI +✓ port: "dropbox" support +✓ port: "save lineage" support (i.e. support multiple parents, automatic warning shown through hs_taxonomy_hierarchical_select_root_level()) +✓ port: field formatters (from content_taxonomy) +✓ port: taxonomy term (create/edit) form should be altered to include HS +✓ upgrade path: migrate settings (no migration necessary) +✓ upgrade path: documentation (no migration, no docs) +✓ port: "create new item" support — see http://drupal.org/node/1087620 +- port: "entity_count" support — see http://drupal.org/node/1068462 +- refactor: use the vocabulary machine name internally instead of the vid +- port: token support — see http://drupal.org/node/1248908 +- port: forum support +- refactor: optimize HS API implementation: take advantage of improvements in Taxonomy + +HS Taxonomy Views: +- everything — see http://drupal.org/node/1170192 + +Menu: +✓ everything + +Flat List: +✓ everything + +Small Hierarchy: +✓ everything diff --git a/sites/all/modules/hierarchical_select/UPGRADE.txt b/sites/all/modules/hierarchical_select/UPGRADE.txt new file mode 100644 index 0000000..f3473ee --- /dev/null +++ b/sites/all/modules/hierarchical_select/UPGRADE.txt @@ -0,0 +1,20 @@ +# Upgrading (from Drupal 6 to 7) + +1. **BE WARE THAT NOT ALL FUNCTIONALITY HAS BEEN PORTED!** + + Make sure that you know if the part of Hierarchical Select's functionality + that you want to use has been ported. Otherwise, you may be in for a + frustrating upgrade experience. + + See the included TODO.txt file for details. In a nutshell: + + - Taxonomy support is almost complete, only "create new item", "entity count" and token support are missing + - Forum support has **not** yet been ported (but relies on Taxonomy, so this is trivial) + - Taxonomy Views support has **not** yet been ported + - Menu support has **not** yet been ported + +2. Upgrade this module just like any other: delete the old module, copy the + files of the new module and run update.php. + For details, see . + +3. That's it! :) diff --git a/sites/all/modules/hierarchical_select/hierarchical_select-rtl.css b/sites/all/modules/hierarchical_select/hierarchical_select-rtl.css new file mode 100644 index 0000000..6bc6c2f --- /dev/null +++ b/sites/all/modules/hierarchical_select/hierarchical_select-rtl.css @@ -0,0 +1,57 @@ + + +/* The hierarchical select. */ +.hierarchical-select-wrapper .hierarchical-select .selects { + float: right; /* If a block is floated, it won't consume as much width as + available, only just enough. This allows the grippie to + perfectly scale with the with consumed by the selects. */ +} + +.hierarchical-select-wrapper .hierarchical-select .selects .grippie { + clear: right; /* clear: left; */ + height: 9px; + overflow: hidden; + background: #eee url(images/grippie.png) no-repeat center 2px; + border: 1px solid #ddd; + border-top-width: 0; + cursor: s-resize; + margin-left: 0.5em; /* margin-right: 0.5em; */ /* Give the grippie the same margin as each select. */ + min-width: 70px; /* Hack for IE, makes the grip usable, but not yet the same as in other browsers. */ +} + +.hierarchical-select-wrapper .hierarchical-select select, +.hierarchical-select-wrapper .hierarchical-select .add-to-dropbox, +.hierarchical-select-wrapper .hierarchical-select .create-new-item { + margin-left: .5em; + margin-right: 0; /* Reset ltr style */ + float: right; +} + + +/* The pseudo-modal window for creating a new item or new level. */ +.hierarchical-select-wrapper .hierarchical-select .create-new-item-create, +.hierarchical-select-wrapper .hierarchical-select .create-new-item-cancel { + float: left; + margin-right: .4em; + margin-left: 0; /* Reset ltr style */ +} + +.hierarchical-select-wrapper .hierarchical-select .create-new-item-input { + float: right; + clear: left; +} + + +/* Child level indicator. */ +.hierarchical-select-wrapper .hierarchical-select option.has-children { + background: url(images/arrow-rtl.png) no-repeat left center; + padding-left: 20px; + padding-right: 0; +} + + +/* Dropbox limit warning.*/ +p.hierarchical-select-dropbox-limit-warning { + padding-right: .5em; + padding-left: 0; /* Reset ltr style */ +} diff --git a/sites/all/modules/hierarchical_select/hierarchical_select.admin.inc b/sites/all/modules/hierarchical_select/hierarchical_select.admin.inc new file mode 100644 index 0000000..d52ee9f --- /dev/null +++ b/sites/all/modules/hierarchical_select/hierarchical_select.admin.inc @@ -0,0 +1,328 @@ + t('All settings below will be used as site-wide defaults.'), + '#prefix' => '
', + '#suffix' => '
', + ); + $form['hierarchical_select_animation_delay'] = array( + '#type' => 'textfield', + '#title' => t('Animation delay'), + '#description' => t( + 'The delay that will be used for the "drop in/out" effect when a + hierarchical select is being updated (in milliseconds).' + ), + '#size' => 5, + '#maxlength' => 5, + '#default_value' => variable_get('hierarchical_select_animation_delay', 400), + ); + $form['hierarchical_select_level_labels_style'] = array( + '#type' => 'select', + '#title' => t('Level labels style'), + '#description' => t( + 'The style that will be used for level labels. This is not supported by + all browsers! If you want a consistent interface, choose to use no + style.' + ), + '#options' => array( + 'none' => t('No style'), + 'bold' => t('Bold'), + 'inversed' => t('Inversed'), + 'underlined' => t('Underlined'), + ), + '#default_value' => variable_get('hierarchical_select_level_labels_style', 'none'), + ); + // TODO: port the HS client-side cache system to Drupal 7. + /* + $form['hierarchical_select_js_cache_system'] = array( + '#type' => 'radios', + '#title' => t('Cache in a HTML 5 client-side database'), + '#description' => t( + 'This feature only works in browsers that support the + HTML 5 client-side database storage specification + .
+ After enabling this, you will notice (in supporting browsers) that + refreshing the hierarchical select will not require a request to the + server when a part is being requested that has been requested before.', + array('!spec-url' => url('http://www.whatwg.org/specs/web-apps/current-work/multipage/section-sql.html')) + ), + '#options' => array( + 0 => t('Disabled'), + 1 => t('Enabled'), + ), + '#default_value' => variable_get('hierarchical_select_js_cache_system', 0), + ); + */ + + return system_settings_form($form); +} + +/** + * Menu callback; a table that lists all Hierarchical Select configs. + */ +function hierarchical_select_admin_configs() { + $header = array(t('Hierarchy type'), t('Hierarchy'), t('Entity type'), t('Bundle'), t('Context type'), t('Context'), t('Actions')); + + // Retrieve all information items + $info_items = array(); + foreach (module_implements('hierarchical_select_config_info') as $module) { + $info_items = array_merge_recursive($info_items, module_invoke($module, 'hierarchical_select_config_info')); + } + + // Process the retrieved information into rows. + $rows = array(); + foreach ($info_items as $id => $item) { + $config_id = $item['config_id']; + + $rows[$id] = array( + $item['hierarchy type'], + $item['hierarchy'], + $item['entity type'], + $item['bundle'], + $item['context type'], + $item['context'], + theme('links', array('links' => array( + array( + 'title' => t('Edit'), + 'href' => $item['edit link'], + 'fragment' => "hierarchical-select-config-form-$config_id", + ), + array( + 'title' => t('Export'), + 'href' => "admin/config/content/hierarchical_select/export/$config_id", + ), + array( + 'title' => t('Import'), + 'href' => "admin/config/content/hierarchical_select/import/$config_id", + ), + ))), + ); + } + + return theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array(), 'caption' => t('Overview of all Hierarchical Select configurations.'))); +} + +/** + * Menu callback; a table that lists all Hierarchical Select implementations + * and the features they support. + */ +function hierarchical_select_admin_implementations() { + $output = ''; + $header = array(t('Implementation (module)'), t('Hierarchy type'), t('Entity type'), t('Create new items'), t('Entity count')); + + // Retrieve all information items + $rows = array(); + foreach (module_implements('hierarchical_select_root_level') as $module) { + $filename = db_query("SELECT filename FROM {system} WHERE type = :type AND name = :name", array(':type' => 'module', ':name' => $module))->fetchField(); + $module_info = drupal_parse_info_file(dirname($filename) . "/$module.info"); + // Try to extract the hierarchy type from the optional hook_hierarchical_select_config_info(). + $hierarchy_type = $entity_type = t('unknown'); + if (module_hook($module, 'hierarchical_select_implementation_info')) { + $implementation = module_invoke($module, 'hierarchical_select_implementation_info'); + $hierarchy_type = $implementation['hierarchy type']; + $entity_type = $implementation['entity type']; + } + + $rows[] = array( + $module_info['name'], + $hierarchy_type, + $entity_type, + (module_hook($module, 'hierarchical_select_create_item')) ? t('Yes') : t('No'), + (module_hook($module, 'hierarchical_select_entity_count')) ? t('Yes') : t('No'), + ); + } + + $output .= '

'; + $output .= t(' + The table below allows you to find out which Hierarchical Select + features are supported by the implementations of the Hierarchical + Select API.
+ It is not a reflection of some settings. + '); + $output .= '

'; + + $output .= theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array(), 'caption' => t('Overview of all installed Hierarchical Select implementations.'))); + + return $output; +} + +/** + * Form definition; config export form. + */ +function hierarchical_select_admin_export($form, &$form_state, $config_id) { + require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc'; + + $config = hierarchical_select_common_config_get($config_id); + $code = _hierarchical_select_create_export_code($config); + + drupal_add_css(drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.css'); + drupal_add_js('$(document).ready(function() { $(".hierarchical-select-code").focus(); });', array('type' => 'inline', 'scope' => JS_DEFAULT)); + + $lines = substr_count($code, "\n") + 1; + $form['config'] = array( + '#type' => 'textarea', + '#title' => t('Hierarchical Select configuration %config_id', array('%config_id' => $config_id)), + '#default_value' => $code, + '#rows' => $lines, + '#attributes' => array('class' => array('hierarchical-select-config-code')), + ); + + return $form; +} + +/** + * Form definition; config import form. + */ +function hierarchical_select_admin_import($form, &$form_state, $config_id) { + require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc'; + + drupal_add_css(drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.css'); + drupal_add_js('$(document).ready(function() { $(".hierarchical-select-code").focus(); });', array('type' => 'inline', 'scope' => JS_DEFAULT)); + + $form['config'] = array( + '#type' => 'textarea', + '#title' => t('Import Hierarchical Select configuration code'), + '#cols' => 60, + '#rows' => 15, + '#description' => t('Copy and paste the results of an exported + Hierarchical Select configuration here.
This will override the + current Hierarchical Select configuration for %config_id.', + array('%config_id' => $config_id) + ), + '#attributes' => array('class' => array('hierarchical-select-config-code')), + ); + $form['interpreted_config'] = array('#type' => 'value', '#value' => NULL); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t("Import"), + ); + $form_state['#redirect'] = NULL; + return $form; +} + +/** + * Validate callback; config import form. + */ +function hierarchical_select_admin_import_validate($form, &$form_state) { + ob_start(); + eval($form_state['values']['config']); + ob_end_clean(); + + form_set_value($form['interpreted_config'], serialize($config), $form_state); + + if (empty($form_state['values']['config'])) { + form_error($form['config'], t('You did not enter anything.')); + } + elseif ($config == NULL) { + form_error($form['config'], t('There is a syntax error in the Hierarchical Select configuration you entered.')); + } + elseif (!isset($config['config_id']) || empty($config['config_id'])) { + form_error($form['config'], t('Unable to import this configuration, because no Hierarchical Select config id is set.')); + } +} + +/** + * Submit callback; config import form. + */ +function hierarchical_select_admin_import_submit($form, &$form_state) { + $config = unserialize($form_state['values']['interpreted_config']); + $config_id = $config['config_id']; + hierarchical_select_common_config_set($config_id, $config); + drupal_set_message(t('Hierarchical Select configuration for %config_id imported!', array('%config_id' => $config_id))); +} + + +//---------------------------------------------------------------------------- +// Private functions. + +/** + * Given a config array, create the export code for it. + * + * @param array $config + * A Hierarchical Select config array, as described in API.txt + * @return string + * The code as it would appear in an editor. + */ +function _hierarchical_select_create_export_code($config) { + $output = ''; + + $output .= "\$config = array(\n"; + $output .= " 'config_id' => '" . $config['config_id'] . "',\n"; + $output .= " 'save_lineage' => " . $config['save_lineage'] . ",\n"; + $output .= " 'enforce_deepest' => " . $config['enforce_deepest'] . ",\n"; + $output .= " 'resizable' => " . $config['resizable'] . ",\n"; + $output .= " 'level_labels' => array(\n"; + $output .= " 'status' => " . $config['level_labels']['status'] . ",\n"; + $output .= " 'labels' => array(\n"; + if (isset($config['level_labels']['labels'])) { + foreach ($config['level_labels']['labels'] as $depth => $label) { + $label = str_replace("'", "\'", $label); + $output .= " $depth => '$label',\n"; + } + } + $output .= " ),\n"; + $output .= " ),\n"; + $output .= " 'dropbox' => array(\n"; + $output .= " 'status' => " . $config['dropbox']['status'] . ",\n"; + $output .= " 'title' => '" . str_replace("'", "\'", $config['dropbox']['title']) . "',\n"; + $output .= " 'limit' => " . $config['dropbox']['limit'] . ",\n"; + $output .= " 'reset_hs' => " . $config['dropbox']['reset_hs'] . ",\n"; + $output .= " 'sort' => " . $config['dropbox']['sort'] . ",\n"; + $output .= " ),\n"; + $output .= " 'editability' => array(\n"; + $output .= " 'status' => " . $config['editability']['status'] . ",\n"; + $output .= " 'item_types' => array(\n"; + if (isset($config['editability']['item_types'])) { + foreach ($config['editability']['item_types'] as $depth => $item_type) { + $item_type = str_replace("'", "\'", $item_type); + $output .= " $depth => '$item_type',\n"; + } + } + $output .= " ),\n"; + $output .= " 'allowed_levels' => array(\n"; + if (isset($config['editability']['allowed_levels'])) { + foreach ($config['editability']['allowed_levels'] as $depth => $allowed_level) { + $output .= " $depth => $allowed_level,\n"; + } + } + $output .= " ),\n"; + $output .= " 'allow_new_levels' => " . $config['editability']['allow_new_levels'] . ",\n"; + $output .= " 'max_levels' => " . $config['editability']['max_levels'] . ",\n"; + $output .= " ),\n"; + $output .= " 'entity_count' => array(\n"; + $output .= " 'enabled' => " . $config['entity_count']['enabled'] . ",\n"; + $output .= " 'require_entity' => " . $config['entity_count']['require_entity'] . ",\n"; + $output .= " 'settings' => array(\n"; + $output .= " 'count_children' => " . $config['entity_count']['settings']['count_children'] . ",\n"; + if (isset($config['entity_count']['settings']['entity_types'])) { + $output .= " 'entity_types' => array(\n"; + foreach ($config['entity_count']['settings']['entity_types'] as $entity_type => $count) { + $output .= " '$entity_type' => array(\n"; + foreach ($count as $delta => $bundles) { + $output .= " '$delta' => array(\n"; + foreach ($bundles as $delta => $bundle) { + $output .= " '$delta' => '$bundle',\n"; + } + $output .= " ),\n"; + } + $output .= " ),\n"; + } + } + $output .= " ),\n"; + $output .= " ),\n"; + $output .= " ),\n"; + $output .= ");\n"; + + return $output; +} diff --git a/sites/all/modules/hierarchical_select/hierarchical_select.css b/sites/all/modules/hierarchical_select/hierarchical_select.css new file mode 100644 index 0000000..5f20a75 --- /dev/null +++ b/sites/all/modules/hierarchical_select/hierarchical_select.css @@ -0,0 +1,226 @@ + + +/* The hierarchical select. */ +.hierarchical-select-wrapper .hierarchical-select .selects { + float: left; /* If a block is floated, it won't consume as much width as + available, only just enough. This allows the grippie to + perfectly scale with the with consumed by the selects. */ +} + +.hierarchical-select-wrapper .hierarchical-select .selects .grippie { + clear: left; + height: 9px; + overflow: hidden; + background: #eee url(images/grippie.png) no-repeat center 2px; + border: 1px solid #ddd; + border-top-width: 0; + cursor: s-resize; + margin-right: 0.5em; /* Give the grippie the same margin as each select. */ + min-width: 50px; /* Hack for IE, makes the grip usable, but not yet the same as in other browsers. */ +} + +.hierarchical-select-wrapper .hierarchical-select select, +.hierarchical-select-wrapper .hierarchical-select .add-to-dropbox, +.hierarchical-select-wrapper .hierarchical-select .create-new-item { + margin: 0; + margin-right: .5em; + margin-bottom: 3px; + float: left; +} + + +/* The flat select (only used in GET forms). */ +.hierarchical-select-wrapper .flat-select { + display: none; +} + + +/* The pseudo-modal window for creating a new item or new level. */ +.hierarchical-select-wrapper .hierarchical-select .create-new-item { + padding: .7em; + border: 2px outset gray; +} + +.hierarchical-select-wrapper .hierarchical-select .create-new-item { + width: 11em; +} + +.hierarchical-select-wrapper .hierarchical-select .create-new-item-create, +.hierarchical-select-wrapper .hierarchical-select .create-new-item-cancel { + float: right; + margin: 0; + margin-left: .4em; +} + +.hierarchical-select-wrapper .hierarchical-select .create-new-item-input { + width: 10.5em; + margin: 0; + margin-bottom: 1em; + float: left; + clear: right; +} + + +/* Level labels styles. */ +.hierarchical-select-level-labels-style-bold .hierarchical-select select option.level-label { + font-weight: bold; +} + +.hierarchical-select-level-labels-style-inversed .hierarchical-select select option.level-label { + background-color: #000000; + color: #FFFFFF; +} + +.hierarchical-select-level-labels-style-underlined .hierarchical-select select option.level-label { + text-decoration: underline; +} + + +/* Child level indicator. */ +.hierarchical-select-wrapper .hierarchical-select option.has-children { + background: url(images/arrow.png) no-repeat right center; + padding-right: 20px; +} + + +/* Dropbox limit warning.*/ +p.hierarchical-select-dropbox-limit-warning { + padding: 0; + color: #F7A54F; + font-size: 110%; + padding-left: .5em; +} + + +/* The dropbox table. */ +.hierarchical-select-wrapper .dropbox-title { + font-size: 115%; + color: #898989; + margin-bottom: 0.2em; +} + +.hierarchical-select-wrapper .dropbox { + display: inline-block; + margin: .5em 0; +} + +.hierarchical-select-wrapper .dropbox table { + margin: 0; + width: auto; + max-width: 100%; + min-width: 20em; + color: gray; + font-size: 90%; + border: 1px solid gray; +} + +tr.dropbox-entry { + line-height: 1.3em; + padding: .3em .6em; +} + +tr.dropbox-entry.even { + background-color: transparent; + border-bottom: 1px solid #CCCCCC; +} + +tr.dropbox-entry.odd { + background-color: #EDF5FA; + border-bottom: 1px solid #CCCCCC; +} + +tr.dropbox-entry.first { + border-top: 1px solid gray; +} + +tr.dropbox-entry.last { + border-bottom: 1px solid gray; +} + +.dropbox-selected-item { + font-weight: bold; +} + +.hierarchical-select-item-separator { + padding-left: .5em; + padding-right: .5em; +} + +td.dropbox-remove *, +td.dropbox-remove a:link, +td.dropbox-remove a:visited { + color: #F7A54F; + text-decoration: none; +} + +td.dropbox-remove a:hover { + text-decoration: underline; +} + +tr.dropbox-is-empty { + padding: .5em 1em; +} + + +/* The "Update" button and help text (used when Javascript is disabled). */ +.hierarchical-select-wrapper .nojs .update-button { + margin: 0 0 1em; +} + +.hierarchical-select-wrapper .nojs .help-text { + font-size: 90%; + color: transparent; + display: block; + border: 1px dotted black; + overflow: hidden; + width: 34em; + height: 1.2em; + padding: .6em; + line-height: normal; +} + +.hierarchical-select-wrapper .nojs .help-text:hover { + height: auto; + width: auto; + min-width: 25em; + max-width: 45em; + color: gray; +} + +.hierarchical-select-wrapper .nojs .help-text .ask-to-hover { + color: gray; + font-style: italic; +} + +.hierarchical-select-wrapper .nojs .help-text:hover .ask-to-hover { + display: none; +} + +.hierarchical-select-wrapper .nojs .help-text .highlight { + text-decoration: underline; +} + +.hierarchical-select-wrapper .nojs .help-text .warning { + color: red; +} + +.hierarchical-select-wrapper .nojs .help-text .solutions { + margin: 0; + padding: 0; +} + + +/* The 'waiting' class is set dynamically, during a callback to the server. */ +.hierarchical-select-wrapper.waiting { + opacity: 0.5; + + /* IE doesn't support CSS 2 properly. */ + zoom: 1; + filter: alpha(opacity=50); +} + + +/* Use a monospace font for the import/export config code text areas. */ +.hierarchical-select-config-code { + font-family: 'Monaco', 'Lucida Console', 'Consolas', monospace; +} diff --git a/sites/all/modules/hierarchical_select/hierarchical_select.features.inc b/sites/all/modules/hierarchical_select/hierarchical_select.features.inc new file mode 100644 index 0000000..301e187 --- /dev/null +++ b/sites/all/modules/hierarchical_select/hierarchical_select.features.inc @@ -0,0 +1,99 @@ + $config) { + $dependencies[$config_id] = $module; + } + } + + // Add features and dependencies. + foreach ($data as $config_id) { + $export['features']['hierarchical_select'][$config_id] = $config_id; + if (isset($dependencies[$config_id])) { + $module = $dependencies[$config_id]; + $export['dependencies'][$module] = $module; + } + } + + return array(); +} + +/** + * Implements hook_features_export_options(). + */ +function hierarchical_select_features_export_options() { + // Retrieve all information items. + $info_items = array(); + foreach (module_implements('hierarchical_select_config_info') as $module) { + $info_items = array_merge_recursive($info_items, module_invoke($module, 'hierarchical_select_config_info')); + } + + // Process the retrieved information into options. + $options = array(); + foreach ($info_items as $id => $item) { + $config_id = $item['config_id']; + $options[$config_id] = $item['hierarchy type'] . ': ' . $item['hierarchy'] . ' - ' . $item['context type'] . (!empty($item['context']) ? ': ' . $item['context'] : ''); + } + + return $options; +} + +/** + * Implements hook_features_export_render(). + */ +function hierarchical_select_features_export_render($module, $data) { + module_load_include('inc', 'hierarchical_select', 'includes/common'); + module_load_include('inc', 'hierarchical_select', 'hierarchical_select.admin'); + + $code = array(); + $code[] = '$configs = array();'; + foreach ($data as $config_id) { + $config = hierarchical_select_common_config_get($config_id); + $config['config_id'] = $config_id; + + $code[] = _hierarchical_select_create_export_code($config); + $code[] = "\$configs['{$config_id}'] = \$config;"; + } + $code[] = "return \$configs;"; + $code = implode("\n", $code); + + return array('hierarchical_select_default_configs' => $code); +} + +/** + * Implements hook_features_revert(). + */ +function hierarchical_select_features_revert($module) { + hierarchical_select_features_rebuild($module); +} + +/** + * Implements hook_features_rebuild(). + */ +function hierarchical_select_features_rebuild($module) { + module_load_include('inc', 'hierarchical_select', 'includes/common'); + $configs = features_get_default('hierarchical_select', $module); + if (!empty($configs)) { + // Apply the configuration. + require_once(drupal_get_path('module', 'hierarchical_select') .'/includes/common.inc'); + + foreach ($configs as $config_id => $config) { + hierarchical_select_common_config_set($config_id, $config); + } + } +} diff --git a/sites/all/modules/hierarchical_select/hierarchical_select.info b/sites/all/modules/hierarchical_select/hierarchical_select.info new file mode 100644 index 0000000..c796df3 --- /dev/null +++ b/sites/all/modules/hierarchical_select/hierarchical_select.info @@ -0,0 +1,14 @@ +name = Hierarchical Select +description = Simplifies the selection of one or multiple items in a hierarchical tree. +package = Form Elements + +core = 7.x +configure = admin/config/content/hierarchical_select +files[] = tests/internals.test + +; Information added by Drupal.org packaging script on 2016-06-03 +version = "7.x-3.0-beta7" +core = "7.x" +project = "hierarchical_select" +datestamp = "1464981541" + diff --git a/sites/all/modules/hierarchical_select/hierarchical_select.install b/sites/all/modules/hierarchical_select/hierarchical_select.install new file mode 100644 index 0000000..825c2a0 --- /dev/null +++ b/sites/all/modules/hierarchical_select/hierarchical_select.install @@ -0,0 +1,84 @@ +condition('name', 'hs_config_%', 'LIKE') + ->execute(); + + db_delete('variable') + ->condition('name', 'hierarchical_select_%', 'LIKE') + ->execute(); +} + + +//---------------------------------------------------------------------------- +// Updates. + +/** + * Update Hierarchical Select to Drupal 7. Basically remove a lot of cruft. + */ +function hierarchical_select_update_7001() { + // Drop Hierarchical Select's cache table, which is now obsolete. + db_drop_table('cache_hierarchical_select'); + + // Undo Hierarchical Select module weight changes, because they're no longer + // necessary. + db_update('system') + ->fields(array( + 'weight' => 0, + )) + ->condition('name', 'hierarchical_select') + ->execute(); +} + +/** + * Update Hierarchical Select config to support improved "entity count". + */ +function hierarchical_select_update_7002() { + module_load_include('inc', 'hierarchical_select', 'includes/common'); + // Retrieve all information items. + $info_items = array(); + foreach (module_implements('hierarchical_select_config_info') as $module) { + $info_items = array_merge_recursive($info_items, module_invoke($module, 'hierarchical_select_config_info')); + } + foreach ($info_items as $info_item) { + // Load config. + $config = hierarchical_select_common_config_get($info_item['config_id']); + + // Move old settings to new location. + $config['entity_count'] = array( + 'enabled' => $config['entity_count'], + 'require_entity' => $config['require_entity'], + ); + + // Remove old setting. + unset($config['require_entity']); + + // Add entity types settings. + $entity_info = entity_get_info(); + foreach ($entity_info as $entity => $entity_info) { + if (!empty($entity_info['bundles']) && $entity_info['fieldable'] === TRUE) { + foreach ($entity_info['bundles'] as $bundle => $bundle_info) { + if ($entity == 'node') { + $config['entity_count']['settings']['entity_types'][$entity]['count_' . $entity][$bundle] = $bundle; + } + else { + $config['entity_count']['settings']['entity_types'][$entity]['count_' . $entity][$bundle] = 0; + } + } + } + } + + // Save new config. + hierarchical_select_common_config_set($info_item['config_id'], $config); + } +} diff --git a/sites/all/modules/hierarchical_select/hierarchical_select.js b/sites/all/modules/hierarchical_select/hierarchical_select.js new file mode 100644 index 0000000..df98f65 --- /dev/null +++ b/sites/all/modules/hierarchical_select/hierarchical_select.js @@ -0,0 +1,702 @@ + +(function($) { + +Drupal.behaviors.HierarchicalSelect = { + attach: function (context) { + $('.hierarchical-select-wrapper:not(.hierarchical-select-wrapper-processed)', context) + .addClass('hierarchical-select-wrapper-processed').each(function() { + var hsid = $(this).attr('id').replace(/^hierarchical-select-(.+)-wrapper$/, "$1"); + Drupal.HierarchicalSelect.initialize(hsid); + }); + } +}; + +Drupal.HierarchicalSelect = {}; + +Drupal.HierarchicalSelect.state = []; + +Drupal.HierarchicalSelect.context = function() { + return $("form .hierarchical-select-wrapper"); +}; + +Drupal.HierarchicalSelect.initialize = function(hsid) { + // Prevent JS errors when Hierarchical Select is loaded dynamically. + if (undefined == Drupal.settings.HierarchicalSelect || undefined == Drupal.settings.HierarchicalSelect.settings["hs-" + hsid]) { + return false; + } + + // If you set Drupal.settings.HierarchicalSelect.pretendNoJS to *anything*, + // and as such, Hierarchical Select won't initialize its Javascript! It + // will seem as if your browser had Javascript disabled. + if (undefined != Drupal.settings.HierarchicalSelect.pretendNoJS) { + return false; + } + + var form = $('#hierarchical-select-'+ hsid +'-wrapper').parents('form'); + + // Pressing the 'enter' key on a form that contains an HS widget, depending + // on which browser, usually causes the first submit button to be pressed + // (likely an HS button). This results in unpredictable behaviour. There is + // no way to determine the 'real' submit button, so disable the enter key. + form.find('input').keypress(function(event) { + if (event.keyCode == 13) { + event.preventDefault(); + return false; + } + }); + + // Turn off Firefox' autocomplete feature. This causes Hierarchical Select + // form items to be disabled after a hard refresh. + // See http://drupal.org/node/453048 and + // http://www.ryancramer.com/journal/entries/radio_buttons_firefox/ + if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) { + form.attr('autocomplete', 'off'); + } + + // Enable *all* submit buttons in this form, as well as all input-related + // elements of the current hierarchical select, in case we reloaded while + // they were disabled. + form.add('#hierarchical-select-' + hsid +'-wrapper .hierarchical-select .selects select') + .add('#hierarchical-select-' + hsid +'-wrapper .hierarchical-select input') + .attr('disabled', false); + + if (this.cache != null) { + this.cache.initialize(); + } + + Drupal.settings.HierarchicalSelect.settings["hs-" + hsid]['updatesEnabled'] = true; + if (undefined == Drupal.HierarchicalSelect.state["hs-" + hsid]) { + Drupal.HierarchicalSelect.state["hs-" + hsid] = {}; + } + + this.transform(hsid); + if (Drupal.settings.HierarchicalSelect.settings["hs-" + hsid].resizable) { + this.resizable(hsid); + } + Drupal.HierarchicalSelect.attachBindings(hsid); + + if (this.cache != null && this.cache.status()) { + this.cache.load(hsid); + } + + Drupal.HierarchicalSelect.log(hsid); +}; + +Drupal.HierarchicalSelect.log = function(hsid, messages) { + // Only perform logging if logging is enabled. + if (Drupal.settings.HierarchicalSelect.initialLog == undefined || Drupal.settings.HierarchicalSelect.initialLog["hs-" + hsid] == undefined) { + return; + } + else { + Drupal.HierarchicalSelect.state["hs-" + hsid].log = []; + } + + // Store the log messages. The first call to this function may not contain a + // message: the initial log included in the initial HTML rendering should be + // used instead.. + if (Drupal.HierarchicalSelect.state["hs-" + hsid].log.length == 0) { + Drupal.HierarchicalSelect.state["hs-" + hsid].log.push(Drupal.settings.HierarchicalSelect.initialLog["hs-" + hsid]); + } + else { + Drupal.HierarchicalSelect.state["hs-" + hsid].log.push(messages); + } + + // Print the log messages. + console.log("HIERARCHICAL SELECT " + hsid); + var logIndex = Drupal.HierarchicalSelect.state["hs-" + hsid].log.length - 1; + for (var i = 0; i < Drupal.HierarchicalSelect.state["hs-" + hsid].log[logIndex].length; i++) { + console.log(Drupal.HierarchicalSelect.state["hs-" + hsid].log[logIndex][i]); + } + console.log(' '); +}; + +Drupal.HierarchicalSelect.transform = function(hsid) { + var removeString = $('#hierarchical-select-'+ hsid +'-wrapper .dropbox .dropbox-remove:first', Drupal.HierarchicalSelect.context).text(); + + $('#hierarchical-select-'+ hsid +'-wrapper', Drupal.HierarchicalSelect.context) + // Remove the .nojs div. + .find('.nojs').hide().end() + // Find all .dropbox-remove cells in the dropbox table. + .find('.dropbox .dropbox-remove') + // Hide the children of these table cells. We're not removing them because + // we want to continue to use the "Remove" checkboxes. + .find('*').css('display', 'none').end() // We can't use .hide() because of collapse.js: http://drupal.org/node/351458#comment-1258303. + // Put a "Remove" link there instead. + .append(''+ removeString +''); +}; + +Drupal.HierarchicalSelect.resizable = function(hsid) { + var $selectsWrapper = $('#hierarchical-select-' + hsid + '-wrapper .hierarchical-select .selects', Drupal.HierarchicalSelect.context); + + // No select wrapper present: the user is creating a new item. + if ($selectsWrapper.length == 0) { + return; + } + + // Append the drag handle ("grippie"). + $selectsWrapper.append($('
')); + + // jQuery object that contains all selects in the hierarchical select, to + // speed up DOM manipulation during dragging. + var $selects = $selectsWrapper.find('select'); + + var defaultPadding = parseInt($selects.slice(0, 1).css('padding-top').replace(/^(\d+)px$/, "$1")) + parseInt($selects.slice(0, 1).css('padding-bottom').replace(/^(\d+)px$/, "$1")); + var defaultHeight = Drupal.HierarchicalSelect.state["hs-" + hsid].defaultHeight = $selects.slice(0, 1).height() + defaultPadding; + var defaultSize = Drupal.HierarchicalSelect.state["hs-" + hsid].defaultSize = $selects.slice(0, 1).attr('size'); + defaultSize = (defaultSize == 0) ? 1 : defaultSize; + var margin = Drupal.HierarchicalSelect.state["hs-" + hsid].margin = parseInt($selects.slice(0, 1).css('margin-bottom').replace(/^(\d+)px$/, "$1")); + + // Bind the drag event. + $('.grippie', $selectsWrapper) + .mousedown(startDrag) + .dblclick(function() { + if (Drupal.HierarchicalSelect.state["hs-" + hsid].resizedHeight == undefined) { + Drupal.HierarchicalSelect.state["hs-" + hsid].resizedHeight = defaultHeight; + } + var resizedHeight = Drupal.HierarchicalSelect.state["hs-" + hsid].resizedHeight = (Drupal.HierarchicalSelect.state["hs-" + hsid].resizedHeight > defaultHeight + 2) ? defaultHeight : 4.6 / defaultSize * defaultHeight; + Drupal.HierarchicalSelect.resize($selects, defaultHeight, resizedHeight, defaultSize, margin); + }); + + function startDrag(e) { + staticOffset = $selects.slice(0, 1).height() - e.pageY; + $selects.css('opacity', 0.25); + $(document).mousemove(performDrag).mouseup(endDrag); + return false; + } + + function performDrag(e) { + var resizedHeight = staticOffset + e.pageY; + Drupal.HierarchicalSelect.resize($selects, defaultHeight, resizedHeight, defaultSize, margin); + return false; + } + + function endDrag(e) { + var height = $selects.slice(0, 1).height(); + + $(document).unbind("mousemove", performDrag).unbind("mouseup", endDrag); + $selects.css('opacity', 1); + if (height != Drupal.HierarchicalSelect.state["hs-" + hsid].resizedHeight) { + Drupal.HierarchicalSelect.state["hs-" + hsid].resizedHeight = (height > defaultHeight) ? height : defaultHeight; + } + } +}; + +Drupal.HierarchicalSelect.resize = function($selects, defaultHeight, resizedHeight, defaultSize, margin) { + if (resizedHeight == undefined) { + resizedHeight = defaultHeight; + } + + $selects + .attr('size', (resizedHeight > defaultHeight) ? 2 : defaultSize) + .height(Math.max(defaultHeight + margin, resizedHeight)); // Without the margin component, the height() method would allow the select to be sized to low: defaultHeight - margin. +}; + +Drupal.HierarchicalSelect.disableForm = function(hsid) { + // Disable *all* submit buttons in this form, as well as all input-related + // elements of the current hierarchical select. + $('form:has(#hierarchical-select-' + hsid +'-wrapper) :submit') + .add('#hierarchical-select-' + hsid +'-wrapper .hierarchical-select .selects select') + .add('#hierarchical-select-' + hsid +'-wrapper .hierarchical-select :input') + .attr('disabled', true); + + // Add the 'waiting' class. Default style: make everything transparent. + $('#hierarchical-select-' + hsid +'-wrapper').addClass('waiting'); + + // Indicate that the user has to wait. + $('body').css('cursor', 'wait'); +}; + +Drupal.HierarchicalSelect.enableForm = function(hsid) { + // This method undoes everything the disableForm() method did. + + $e = $('form:has(#hierarchical-select-' + hsid +'-wrapper) :submit') + .add('#hierarchical-select-' + hsid +'-wrapper .hierarchical-select :input:not(:submit)'); + + // Don't enable the selects again if they've been disabled because the + // dropbox limit was exceeded. + dropboxLimitExceeded = $('#hierarchical-select-' + hsid +'-wrapper .hierarchical-select-dropbox-limit-warning').length > 0; + if (!dropboxLimitExceeded) { + $e = $e.add($('#hierarchical-select-' + hsid +'-wrapper .hierarchical-select .selects select')); + } + $e.removeAttr("disabled"); + + // Don't enable the 'Add' button again if it's been disabled because the + // dropbox limit was exceeded. + if (dropboxLimitExceeded) { + $('#hierarchical-select-' + hsid +'-wrapper .hierarchical-select :submit') + .attr('disabled', true); + } + + $('#hierarchical-select-' + hsid +'-wrapper').removeClass('waiting'); + + $('body').css('cursor', 'auto'); +}; + +Drupal.HierarchicalSelect.throwError = function(hsid, message) { + // Show the error to the user. + alert(message); + + // Log the error. + Drupal.HierarchicalSelect.log(hsid, [ message ]); + + // Re-enable the form to allow the user to retry, but reset the selection to + // the level label if possible, otherwise the "" option if possible. + var $select = $('#hierarchical-select-' + hsid +'-wrapper .hierarchical-select .selects select:first'); + var levelLabelOption = $('option[value^=label_]', $select).val(); + if (levelLabelOption !== undefined) { + $select.val(levelLabelOption); + } + else { + var noneOption = $('option[value=none]', $select).val(); + if (noneOption !== undefined) { + $select.val(noneOption); + } + } + Drupal.HierarchicalSelect.enableForm(hsid); +}; + +Drupal.HierarchicalSelect.prepareGETSubmit = function(hsid) { + // Remove the name attributes of all form elements that end up in GET, + // except for the "flat select" form element. + $('#hierarchical-select-'+ hsid +'-wrapper', Drupal.HierarchicalSelect.context) + .find('input, select') + .not('.flat-select') + .removeAttr('name'); + + // Update the name attribute of the "flat select" form element + var $flatSelect = $('#hierarchical-select-'+ hsid +'-wrapper .flat-select', Drupal.HierarchicalSelect.context); + var newName = $flatSelect.attr('name').replace(/^([a-zA-Z0-9_\-]*)(?:\[flat_select\]){1}(\[\])?$/, "$1$2"); + $flatSelect.attr('name', newName); + + Drupal.HierarchicalSelect.triggerEvents(hsid, 'prepared-GET-submit', {}); +}; + +Drupal.HierarchicalSelect.attachBindings = function(hsid) { + var updateOpString = $('#hierarchical-select-'+ hsid +'-wrapper .update-button').val(); + var addOpString = $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .add-to-dropbox', Drupal.HierarchicalSelect.context).val(); + var createNewItemOpString = $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .create-new-item-create', Drupal.HierarchicalSelect.context).val(); + var cancelNewItemOpString = $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .create-new-item-cancel', Drupal.HierarchicalSelect.context).val(); + + var data = {}; + data.hsid = hsid; + + $('#hierarchical-select-'+ hsid +'-wrapper', this.context) + // "disable-updates" event + .unbind('disable-updates').bind('disable-updates', data, function(e) { + Drupal.settings.HierarchicalSelect.settings["hs-" + e.data.hsid]['updatesEnabled'] = false; + }) + + // "enforce-update" event + .unbind('enforce-update').bind('enforce-update', data, function(e, extraPost) { + Drupal.HierarchicalSelect.update(e.data.hsid, 'enforced-update', { opString: updateOpString, extraPost: extraPost }); + }) + + // "prepare-GET-submit" event + .unbind('prepare-GET-submit').bind('prepare-GET-submit', data, function(e) { + Drupal.HierarchicalSelect.prepareGETSubmit(e.data.hsid); + }) + + // "update-hierarchical-select" event + .find('.hierarchical-select .selects select').unbind().change(function(_hsid) { + return function() { + if (Drupal.settings.HierarchicalSelect.settings["hs-" + _hsid]['updatesEnabled']) { + Drupal.HierarchicalSelect.update(_hsid, 'update-hierarchical-select', { opString: updateOpString, select_id : $(this).attr('id') }); + } + }; + }(hsid)).end() + + // "create-new-item" event + .find('.hierarchical-select .create-new-item .create-new-item-create').unbind().click(function(_hsid) { + return function() { + Drupal.HierarchicalSelect.update(_hsid, 'create-new-item', { opString : createNewItemOpString }); + return false; // Prevent the browser from POSTing the page. + }; + }(hsid)).end() + + // "cancel-new-item" event" + .find('.hierarchical-select .create-new-item .create-new-item-cancel').unbind().click(function(_hsid) { + return function() { + Drupal.HierarchicalSelect.update(_hsid, 'cancel-new-item', { opString : cancelNewItemOpString }); + return false; // Prevent the browser from POSTing the page (in case of the "Cancel" button). + }; + }(hsid)).end() + + // "add-to-dropbox" event + .find('.hierarchical-select .add-to-dropbox').unbind().click(function(_hsid) { + return function() { + Drupal.HierarchicalSelect.update(_hsid, 'add-to-dropbox', { opString : addOpString }); + return false; // Prevent the browser from POSTing the page. + }; + }(hsid)).end() + + // "remove-from-dropbox" event + // (anchors in the .dropbox-remove cells in the .dropbox table) + .find('.dropbox .dropbox-remove a').unbind().click(function(_hsid) { + return function() { + var isDisabled = $('#hierarchical-select-'+ hsid +'-wrapper', Drupal.HierarchicalSelect.context).attr('disabled'); + + // If the hierarchical select is disabled, then ignore this click. + if (isDisabled) { + return false; + } + + // Check the (hidden, because JS is enabled) checkbox that marks this + // dropbox entry for removal. + $(this).parent().find('input[type=checkbox]').attr('checked', true); + Drupal.HierarchicalSelect.update(_hsid, 'remove-from-dropbox', { opString: updateOpString }); + return false; // Prevent the browser from POSTing the page. + }; + }(hsid)); +}; + +Drupal.HierarchicalSelect.preUpdateAnimations = function(hsid, updateType, lastUnchanged, callback) { + switch (updateType) { + case 'update-hierarchical-select': + // Drop out the selects of the levels deeper than the select of the + // level that just changed. + var animationDelay = Drupal.settings.HierarchicalSelect.settings["hs-" + hsid]['animationDelay']; + var $animatedSelects = $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .selects select', Drupal.HierarchicalSelect.context).slice(lastUnchanged); + if ($animatedSelects.size() > 0) { + $animatedSelects.hide(); + for (var i = 0; i < $animatedSelects.size(); i++) { + if (i < $animatedSelects.size() - 1) { + $animatedSelects.slice(i, i + 1).hide("drop", { direction: "left" }, animationDelay); + } + else { + $animatedSelects.slice(i, i + 1).hide("drop", { direction: "left" }, animationDelay, callback); + } + } + } + else if (callback) { + callback(); + } + break; + default: + if (callback) { + callback(); + } + break; + } +}; + +Drupal.HierarchicalSelect.postUpdateAnimations = function(hsid, updateType, lastUnchanged, callback) { + if (Drupal.settings.HierarchicalSelect.settings["hs-" + hsid].resizable) { + // Restore the resize. + Drupal.HierarchicalSelect.resize( + $('#hierarchical-select-' + hsid + '-wrapper .hierarchical-select .selects select', Drupal.HierarchicalSelect.context), + Drupal.HierarchicalSelect.state["hs-" + hsid].defaultHeight, + Drupal.HierarchicalSelect.state["hs-" + hsid].resizedHeight, + Drupal.HierarchicalSelect.state["hs-" + hsid].defaultSize, + Drupal.HierarchicalSelect.state["hs-" + hsid].margin + ); + } + + switch (updateType) { + case 'update-hierarchical-select': + var $createNewItemInput = $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .create-new-item-input', Drupal.HierarchicalSelect.context); + // Hide the loaded selects after the one that was just changed, then + // drop them in. + var animationDelay = Drupal.settings.HierarchicalSelect.settings["hs-" + hsid]['animationDelay']; + var $animatedSelects = $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .selects select', Drupal.HierarchicalSelect.context).slice(lastUnchanged); + if ($animatedSelects.size() > 0) { + $animatedSelects.hide(); + for (var i = 0; i < $animatedSelects.size(); i++) { + if (i < $animatedSelects.size() - 1) { + $animatedSelects.slice(i, i + 1).show("drop", { direction: "left" }, animationDelay); + } + else { + $animatedSelects.slice(i, i + 1).show("drop", { direction: "left" }, animationDelay, callback); + } + } + } + else if (callback) { + callback(); + } + if ($createNewItemInput.size() == 0) { + // Give focus to the level below the one that has changed, if it + // exists. + setTimeout( + function() { + $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .selects select', Drupal.HierarchicalSelect.context) + .slice(lastUnchanged, lastUnchanged + 1) + .focus(); + }, + animationDelay + 100 + ); + } + else { + // Give focus to the input field of the "create new item/level" + // section, if it exists, and also select the existing text. + $createNewItemInput.focus(); + $createNewItemInput[0].select(); + } + break; + + case 'create-new-item': + // Make sure that other Hierarchical Selects that represent the same + // hierarchy are also updated, to make sure that they have the newly + // created item! + var cacheId = Drupal.settings.HierarchicalSelect.settings["hs-" + hsid].cacheId; + for (var otherHsid in Drupal.settings.HierarchicalSelect.settings) { + if (Drupal.settings.HierarchicalSelect.settings[otherHsid].cacheId == cacheId) { + $('#hierarchical-select-'+ otherHsid +'-wrapper') + .trigger('enforce-update'); + } + } + // TRICKY: NO BREAK HERE! + + case 'cancel-new-item': + // After an item/level has been created/cancelled, reset focus to the + // beginning of the hierarchical select. + $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .selects select', Drupal.HierarchicalSelect.context) + .slice(0, 1) + .focus(); + + if (callback) { + callback(); + } + break; + + default: + if (callback) { + callback(); + } + break; + } +}; + +Drupal.HierarchicalSelect.triggerEvents = function(hsid, updateType, settings) { + $('#hierarchical-select-'+ hsid +'-wrapper', Drupal.HierarchicalSelect.context) + .trigger(updateType, [ hsid, settings ]); +}; + +Drupal.HierarchicalSelect.update = function(hsid, updateType, settings) { + var post = $('form:has(#hierarchical-select-' + hsid +'-wrapper)', Drupal.HierarchicalSelect.context).formToArray(); + var hs_current_language = Drupal.settings.HierarchicalSelect.hs_current_language; + + // Pass the hierarchical_select id via POST. + post.push({ name : 'hsid', value : hsid }); + // Send the current language so we can use the same language during the AJAX callback. + post.push({ name : 'hs_current_language', value : hs_current_language}); + // Emulate the AJAX data sent normally so that we get the same theme. + post.push({ name : 'ajax_page_state[theme]', value : Drupal.settings.ajaxPageState.theme }); + post.push({ name : 'ajax_page_state[theme_token]', value : Drupal.settings.ajaxPageState.theme_token }); + + // If a cache system is installed, let the server know if it's running + // properly. If it is running properly, the server will send back additional + // information to maintain a lazily-loaded cache. + if (Drupal.HierarchicalSelect.cache != null) { + post.push({ name : 'client_supports_caching', value : Drupal.HierarchicalSelect.cache.status() }); + } + + // updateType is one of: + // - 'none' (default) + // - 'update-hierarchical-select' + // - 'enforced-update' + // - 'create-new-item' + // - 'cancel-new-item' + // - 'add-to-dropbox' + // - 'remove-from-dropbox' + switch (updateType) { + case 'update-hierarchical-select': + var value = $('#'+ settings.select_id).val(); + var lastUnchanged = parseInt(settings.select_id.replace(/^.*-hierarchical-select-selects-(\d+)/, "$1")) + 1; + var optionClass = $('#'+ settings.select_id).find('option[value="'+ value +'"]').attr('class'); + + // Don't do anything (also no callback to the server!) when the selected + // item is: + // - the '' option and the renderFlatSelect setting is disabled, or + // - a level label, or + // - an option of class 'has-no-children', and + // (the renderFlatSelect setting is disabled or the dropbox is enabled) + // and + // (the createNewLevels setting is disabled). + if ((value == 'none' && Drupal.settings.HierarchicalSelect.settings["hs-" + hsid]['renderFlatSelect'] == false) + || value.match(/^label_\d+$/) + || (optionClass == 'has-no-children' + && + ( + (Drupal.settings.HierarchicalSelect.settings["hs-" + hsid]['renderFlatSelect'] == false + || $('#hierarchical-select-'+ hsid +'-wrapper .dropbox').length > 0 + ) + && + Drupal.settings.HierarchicalSelect.settings["hs-" + hsid]['createNewLevels'] == false + ) + ) + ) + { + Drupal.HierarchicalSelect.preUpdateAnimations(hsid, updateType, lastUnchanged, function() { + // Remove the sublevels. + $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .selects select', Drupal.HierarchicalSelect.context) + .slice(lastUnchanged) + .remove(); + + // The selection of this hierarchical select has changed! + Drupal.HierarchicalSelect.triggerEvents(hsid, 'change-hierarchical-select', settings); + }); + return; + } + post.push({ name : 'op', value : settings.opString }); + break; + + case 'enforced-update': + post.push({ name : 'op', value : settings.opString }); + post = post.concat(settings.extraPost); + break; + + case 'create-new-item': + case 'cancel-new-item': + case 'add-to-dropbox': + case 'remove-from-dropbox': + post.push({ name : 'op', value : settings.opString }); + break; + + default: + break; + } + + // Construct the URL the request should be made to. + var url = Drupal.settings.HierarchicalSelect.settings["hs-" + hsid].ajax_url; + + // Construct the object that contains the options for a callback to the + // server. If a client-side cache is found however, it's possible that this + // won't be used. + var ajaxOptions = $.extend({}, Drupal.ajax.prototype, { + url: url, + type: 'POST', + dataType: 'json', + data: post, + effect: 'fade', + wrapper: '#hierarchical-select-' + hsid + '-wrapper', + beforeSend: function() { + Drupal.HierarchicalSelect.triggerEvents(hsid, 'before-' + updateType, settings); + Drupal.HierarchicalSelect.disableForm(hsid); + }, + error: function (XMLHttpRequest, textStatus, errorThrown) { + // When invalid HTML is received in Safari, jQuery calls this function. + Drupal.HierarchicalSelect.throwError(hsid, Drupal.t('Received an invalid response from the server.')); + }, + success: function(response, status) { + // An invalid response may be returned by the server, in case of a PHP + // error. Detect this and let the user know. + if (response === null || response.length == 0) { + Drupal.HierarchicalSelect.throwError(hsid, Drupal.t('Received an invalid response from the server.')); + return; + } + + // Execute all AJAX commands in the response. But pass an additional + // hsid parameter, which is then only used by the commands written + // for Hierarchical Select. + + // This is another hack because of the non-Drupal ajax implementation + // of this module, one of the response that can come from a drupal + // ajax command is insert, which expects a Drupal.ajax object as the first + // arguments and assumes that certain functions/settings are available. + // Because we are calling a Drupal.ajax.command but providing the regular + // jQuery ajax object itself, we are allowing Drupal.ajax.prototype.commands + // to misserably fail. + // + // This hack attempts to fix one issue with an insert command, + // @see https://www.drupal.org/node/2393695, allowing it to work properly + // Other hacks might be necessary for other ajax commands if they are added + // by external modules. + this.effect = 'none'; + this.getEffect = Drupal.ajax.prototype.getEffect; + + for (var i in response) { + if (response[i]['command'] && Drupal.ajax.prototype.commands[response[i]['command']]) { + Drupal.ajax.prototype.commands[response[i]['command']](this, response[i], status, hsid); + } + } + + // Attach behaviors. This is just after the HTML has been updated, so + // it's as soon as we can. + Drupal.attachBehaviors($('#hierarchical-select-' + hsid + '-wrapper').parents('div.form-type-hierarchical-select')[0]); + + // Transform the hierarchical select and/or dropbox to the JS variant, + // make it resizable again and re-enable the disabled form items. + Drupal.HierarchicalSelect.enableForm(hsid); + + Drupal.HierarchicalSelect.postUpdateAnimations(hsid, updateType, lastUnchanged, function() { + // Update the client-side cache when: + // - information for in the cache is provided in the response, and + // - the cache system is available, and + // - the cache system is running. + if (response.cache != null && Drupal.HierarchicalSelect.cache != null && Drupal.HierarchicalSelect.cache.status()) { + Drupal.HierarchicalSelect.cache.sync(hsid, response.cache); + } + + if (response.log != undefined) { + Drupal.HierarchicalSelect.log(hsid, response.log); + } + + Drupal.HierarchicalSelect.triggerEvents(hsid, updateType, settings); + + if (updateType == 'update-hierarchical-select') { + // The selection of this hierarchical select has changed! + Drupal.HierarchicalSelect.triggerEvents(hsid, 'change-hierarchical-select', settings); + } + }); + } + }); + + // Use the client-side cache to update the hierarchical select when: + // - the hierarchical select is being updated (i.e. no add/remove), and + // - the renderFlatSelect setting is disabled, and + // - the createNewItems setting is disabled, and + // - the cache system is available, and + // - the cache system is running. + // Otherwise, perform a normal dynamic form submit. + if (updateType == 'update-hierarchical-select' + && Drupal.settings.HierarchicalSelect.settings["hs-" + hsid]['renderFlatSelect'] == false + && Drupal.settings.HierarchicalSelect.settings["hs-" + hsid]['createNewItems'] == false + && Drupal.HierarchicalSelect.cache != null + && Drupal.HierarchicalSelect.cache.status()) + { + Drupal.HierarchicalSelect.cache.updateHierarchicalSelect(hsid, value, settings, lastUnchanged, ajaxOptions); + } + else { + Drupal.HierarchicalSelect.preUpdateAnimations(hsid, updateType, lastUnchanged, function() { + // Adding current theme to prevent conflicts, @see ajax.js + // @TODO, try converting to use Drupal.ajax instead. + + // Prevent duplicate HTML ids in the returned markup. + // @see drupal_html_id() + var ids = []; + $('[id]').each(function () { + ids.push(this.id); + }); + + ajaxOptions.data.push({ name : 'ajax_html_ids[]', value : ids }); + + ajaxOptions.data.push({ name : 'ajax_page_state[theme]', value : Drupal.settings.ajaxPageState.theme }); + ajaxOptions.data.push({ name : 'ajax_page_state[theme_token]', value : Drupal.settings.ajaxPageState.theme_token }); + for (var key in Drupal.settings.ajaxPageState.css) { + ajaxOptions.data.push({ name : 'ajax_page_state[css][' + key + ']', value : 1}); + } + for (var key in Drupal.settings.ajaxPageState.js) { + ajaxOptions.data.push({ name : 'ajax_page_state[js][' + key + ']', value : 1}); + } + + // Make it work with jquery update + if (Drupal.settings.ajaxPageState.jquery_version) { + ajaxOptions.data.push({ name : 'ajax_page_state[jquery_version]', value : Drupal.settings.ajaxPageState.jquery_version }); + } + + $.ajax(ajaxOptions); + }); + } +}; + +Drupal.ajax.prototype.commands.hierarchicalSelectUpdate = function(ajax, response, status, hsid) { + // Replace the old HTML with the (relevant part of) retrieved HTML. + $('#hierarchical-select-'+ hsid +'-wrapper', Drupal.HierarchicalSelect.context) + .parent('.form-item') + .replaceWith($(response.output)); +}; + +Drupal.ajax.prototype.commands.hierarchicalSelectSettingsUpdate = function(ajax, response, status, hsid) { + Drupal.settings.HierarchicalSelect.settings["hs-" + response.hsid] = response.settings; +}; + +})(jQuery); diff --git a/sites/all/modules/hierarchical_select/hierarchical_select.module b/sites/all/modules/hierarchical_select/hierarchical_select.module new file mode 100644 index 0000000..aa074cf --- /dev/null +++ b/sites/all/modules/hierarchical_select/hierarchical_select.module @@ -0,0 +1,2367 @@ + 'hierarchical_select_ajax', + 'delivery callback' => 'ajax_deliver', + 'access arguments' => array('access content'), + 'theme callback' => 'ajax_base_page_theme', + 'type' => MENU_CALLBACK, + ); + + $items['admin/config/content/hierarchical_select'] = array( + 'title' => 'Hierarchical Select', + 'description' => 'Configure site-wide settings for the Hierarchical Select form element.', + 'access arguments' => array('administer site configuration'), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('hierarchical_select_admin_settings'), + 'type' => MENU_NORMAL_ITEM, + 'file' => 'hierarchical_select.admin.inc', + ); + $items['admin/config/content/hierarchical_select/settings'] = array( + 'title' => 'Site-wide settings', + 'access arguments' => array('administer site configuration'), + 'weight' => -10, + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'file' => 'hierarchical_select.admin.inc', + ); + $items['admin/config/content/hierarchical_select/configs'] = array( + 'title' => 'Configurations', + 'description' => 'All available Hierarchical Select configurations.', + 'access arguments' => array('administer site configuration'), + 'page callback' => 'hierarchical_select_admin_configs', + 'type' => MENU_LOCAL_TASK, + 'file' => 'hierarchical_select.admin.inc', + ); + $items['admin/config/content/hierarchical_select/implementations'] = array( + 'title' => 'Implementations', + 'description' => 'Features of each Hierarchical Select implementation.', + 'access arguments' => array('administer site configuration'), + 'page callback' => 'hierarchical_select_admin_implementations', + 'type' => MENU_LOCAL_TASK, + 'file' => 'hierarchical_select.admin.inc', + ); + $items['admin/config/content/hierarchical_select/export/%hierarchical_select_config_id'] = array( + 'title' => 'Export', + 'access arguments' => array('administer site configuration'), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('hierarchical_select_admin_export', 5), + 'type' => MENU_LOCAL_TASK, + 'file' => 'hierarchical_select.admin.inc', + ); + $items['admin/config/content/hierarchical_select/import/%hierarchical_select_config_id'] = array( + 'title' => 'Import', + 'access arguments' => array('administer site configuration'), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('hierarchical_select_admin_import', 5), + 'type' => MENU_LOCAL_TASK, + 'file' => 'hierarchical_select.admin.inc', + ); + + return $items; +} + +/** + * Implements hook_element_info(). + */ +function hierarchical_select_element_info() { + $types['hierarchical_select'] = array( + '#input' => TRUE, + '#process' => array('form_hierarchical_select_process'), + '#theme' => array('hierarchical_select'), + '#theme_wrappers' => array('form_element'), + '#config' => array( + 'module' => 'some_module', + 'params' => array(), + 'save_lineage' => 0, + 'enforce_deepest' => 0, + 'resizable' => 1, + 'level_labels' => array( + 'status' => 0, + 'labels' => array(), + ), + 'dropbox' => array( + 'status' => 0, + 'title' => t('All selections'), + 'limit' => 0, + 'reset_hs' => 1, + 'sort' => 1, + ), + 'editability' => array( + 'status' => 0, + 'item_types' => array(), + 'allowed_levels' => array(), + 'allow_new_levels' => 0, + 'max_levels' => 3, + ), + 'entity_count' => array( + 'enabled' => 0, + 'require_entity' => 0, + 'settings' => array( + 'count_children' => 0, + 'entity_types' => array(), + ), + ), + 'animation_delay' => variable_get('hierarchical_select_animation_delay', 400), + 'special_items' => array(), + 'render_flat_select' => 0, + ), + '#default_value' => -1, + ); + $types['hierarchical_select_item_separator'] = array( + '#theme' => 'hierarchical_select_item_separator', + ); + + return $types; +} + +/** + * Implements hook_requirements(). + */ +function hierarchical_select_requirements($phase) { + $requirements = array(); + + if ($phase == 'runtime') { + // Check if all hook_update_n() hooks have been executed. + require_once DRUPAL_ROOT . '/' . 'includes/install.inc'; + drupal_load_updates(); + $updates = drupal_get_schema_versions('hierarchical_select'); + $current = drupal_get_installed_schema_version('hierarchical_select'); + + $up_to_date = (end($updates) == $current); + + $hierarchical_select_weight = db_query("SELECT weight FROM {system} WHERE type = :type AND name = :name", array(':type' => 'module', ':name' => 'hierarchical_select'))->fetchField(); + $core_overriding_modules = array('hs_book', 'hs_menu', 'hs_taxonomy'); + $path_errors = array(); + foreach ($core_overriding_modules as $module) { + $filename = db_query("SELECT filename FROM {system} WHERE type = :type AND name = :name", array(':type' => 'module', ':name' => $module))->fetchField(); + if (strpos($filename, 'modules/') === 0) { + $module_info = drupal_parse_info_file(dirname($filename) . "/$module.info"); + $path_errors[] = t('!module', array('!module' => $module_info['name'])); + } + } + + if ($up_to_date && !count($path_errors)) { + $value = t('All updates installed. HS API implementation modules correctly installed.'); + $description = ''; + $severity = REQUIREMENT_OK; + } + elseif ($path_errors) { + $value = t('Modules incorrectly installed!'); + $description = t( + "The following modules implement Hierarchical Select module for Drupal + core modules, but are installed in the wrong location. They're + installed in core's modules directory, but should be + installed in either the sites/all/modules directory or a + sites/yoursite.com/modules directory" + ) . ':' . theme('item_list', array('items' => $path_errors)); + $severity = REQUIREMENT_ERROR; + } + else { + $value = t('Not all updates installed!'); + $description = t('Please run update.php to install the latest updates! + You have installed update !installed_update, but the latest update is + !latest_update!', + array( + '!installed_update' => $current, + '!latest_update' => end($updates), + ) + ); + $severity = REQUIREMENT_ERROR; + } + + $requirements['hierarchical_select'] = array( + 'title' => t('Hierarchical Select'), + 'value' => $value, + 'description' => $description, + 'severity' => $severity, + ); + } + + return $requirements; +} + +/** + * Implements hook_theme(). + */ +function hierarchical_select_theme() { + return array( + 'hierarchical_select_form_element' => array( + 'file' => 'includes/theme.inc', + 'variables' => array('element' => NULL, 'value' => NULL), + ), + 'hierarchical_select' => array( + 'file' => 'includes/theme.inc', + 'render element' => 'element', + ), + 'hierarchical_select_selects_container' => array( + 'file' => 'includes/theme.inc', + 'render element' => 'element', + ), + 'hierarchical_select_select' => array( + 'file' => 'includes/theme.inc', + 'render element' => 'element', + ), + 'hierarchical_select_item_separator' => array( + 'file' => 'includes/theme.inc', + 'render element' => 'element', + ), + 'hierarchical_select_special_option' => array( + 'file' => 'includes/theme.inc', + 'variables' => array('option' => NULL), + ), + 'hierarchical_select_dropbox_table' => array( + 'file' => 'includes/theme.inc', + 'render element' => 'element', + ), + 'hierarchical_select_common_config_form_level_labels' => array( + 'file' => 'includes/theme.inc', + 'render element' => 'form', + ), + 'hierarchical_select_common_config_form_editability' => array( + 'file' => 'includes/theme.inc', + 'render element' => 'form', + ), + 'hierarchical_select_selection_as_lineages' => array( + 'file' => 'includes/theme.inc', + 'variables' => array( + 'selection' => NULL, + 'config' => NULL, + ), + ), + ); +} + +/** + * Implements hook_features_api(). + */ +function hierarchical_select_features_api() { + return array( + 'hierarchical_select' => array( + 'name' => t('Hierarchical select configs'), + 'feature_source' => TRUE, + 'default_hook' => 'hierarchical_select_default_configs', + 'default_file' => FEATURES_DEFAULTS_INCLUDED, + 'file' => drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.features.inc', + ), + ); +} + +/** + * Implements hook_select_menu_site_status_alter(). + * + * This will run straight after the bootstrap/hook_init(), and override the + * interface language there determined with the interface language from the + * previous request on the HS AJAX callback. We want the language to remain + * the same between requests so we can determine the "triggering element" + * correctly. If the button value changed because of a language change (as + * can happen with the admin_language module), the whole form would submit. + */ +function hierarchical_select_menu_site_status_alter(&$menu_site_status, $path) { + global $language; + // Make sure we are on the AJAX callback. + if (0 === strpos($_GET['q'], 'hierarchical_select_ajax') && !empty($_POST['hs_current_language'])) { + $languages = language_list(); + if (isset($languages[$_POST['hs_current_language']])) { + // Override the language set during bootstrap with the language from the + // previous request. + $language = $languages[$_POST['hs_current_language']]; + } + } +} + + +//---------------------------------------------------------------------------- +// Menu system callbacks. + +/** + * Wildcard loader for Hierarchical Select config ID's. + */ +function hierarchical_select_config_id_load($config_id) { + $config = variable_get('hs_config_' . $config_id, FALSE); + return ($config !== FALSE) ? $config['config_id'] : FALSE; +} + + +//---------------------------------------------------------------------------- +// Forms API callbacks. + +/** + * Ajax callback to render the select form elements. + * + * @see file_ajax_upload(), upon which this is strongly inspired. + * @see ajax_form_callback() + */ +function hierarchical_select_ajax() { + $form_parents = func_get_args(); + list($form, $form_state, $form_id, $form_build_id, $commands) = ajax_get_form(); + + // Process user input. $form and $form_state are modified in the process. + drupal_process_form($form['#form_id'], $form, $form_state); + $element = drupal_array_get_nested_value($form, $form_parents); + + // Render the output. + $output = theme('status_messages') . drupal_render($element); + + // Send AJAX command to update the Hierarchical Select. + $commands[] = array( + 'command' => 'hierarchicalSelectUpdate', + 'output' => $output, + ); + + $new_settings = _hs_new_setting_ajax(FALSE); + foreach ($new_settings as $new_setting) { + $commands[] = array( + 'command' => 'hierarchicalSelectSettingsUpdate', + 'hsid' => $new_setting['hsid'], + 'settings' => $new_setting['settings'], + ); + } + + $context = array( + 'form' => $form, + 'form_state' => $form_state, + 'element' => $element, + ); + drupal_alter('hierarchical_select_ajax_commands', $commands, $context); + return array('#type' => 'ajax', '#commands' => $commands); +} + +function _hs_process_determine_hsid($element, &$form_state) { + // Determine the HSID to use: either the existing one that is received, or + // generate a new one based on the last HSID used (which is + // stored in form state storage). + if (!isset($element['#value']) || !is_array($element['#value']) || !array_key_exists('hsid', $element['#value'])) { + $hsid = uniqid(); + } + else { + $hsid = check_plain($element['#value']['hsid']); + } + + return $hsid; +} + +// Get the config and convert the 'special_items' setting to a more easily +// accessible format. +function _hs_process_shortcut_special_items($config) { + $special_items = array(); + if (isset($config['special_items'])) { + $special_items['exclusive'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_exclusive')); + $special_items['none'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_none')); + } + return $special_items; +} + +function _hs_process_attach_css_js($element, $hsid, &$form_state, $complete_form) { + global $language; + // Set up Javascript and add settings specifically for the current + // hierarchical select. + $element['#attached']['library'][] = array('system', 'ui'); + $element['#attached']['library'][] = array('system', 'drupal.ajax'); + $element['#attached']['library'][] = array('system', 'jquery.form'); + $element['#attached']['library'][] = array('system', 'effects'); + $element['#attached']['library'][] = array('system', 'effects.drop'); + $element['#attached']['css'][] = drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.css'; + $element['#attached']['js'][] = drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.js'; + if (variable_get('hierarchical_select_js_cache_system', 0) == 1) { + $element['#attached']['js'][] = drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select_cache.js'; + } + + if (!isset($form_state['storage']['hs']['js_settings_sent'])) { + $form_state['storage']['hs']['js_settings_sent'] = array(); + } + + // Form was submitted; this is a newly loaded page, thus ensure that all JS + // settings are resent. + if ($form_state['process_input'] === TRUE) { + $form_state['storage']['hs']['js_settings_sent'] = array(); + } + + if (!isset($form_state['storage']['hs']['js_settings_sent'][$hsid]) || (isset($form_state['storage']['hs']['js_settings_sent'][$hsid]) && (isset($form_state['triggering_element']) && $form_state['triggering_element']['#type'] == 'submit'))) { + $config = _hierarchical_select_inherit_default_config($element['#config']); + $settings = array( + 'HierarchicalSelect' => array( + // Save language in settings so we can use the same language during the AJAX callback. + 'hs_current_language' => $language->language, + 'settings' => array( + "hs-$hsid" => array( + 'animationDelay' => ($config['animation_delay'] == 0) ? (int) variable_get('hierarchical_select_animation_delay', 400) : $config['animation_delay'], + 'cacheId' => $config['module'] . '_' . md5(serialize($config['params'])), + 'renderFlatSelect' => (isset($config['render_flat_select'])) ? (int) $config['render_flat_select'] : 0, + 'createNewItems' => (isset($config['editability']['status'])) ? (int) $config['editability']['status'] : 0, + 'createNewLevels' => (isset($config['editability']['allow_new_levels'])) ? (int) $config['editability']['allow_new_levels'] : 0, + 'resizable' => (isset($config['resizable'])) ? (int) $config['resizable'] : 0, + 'ajax_url' => url('hierarchical_select_ajax/' . implode('/', $element['#array_parents'])), + ), + ), + ) + ); + + if (!isset($_POST['hsid'])) { + $element['#attached']['js'][] = array( + 'type' => 'setting', + 'data' => $settings, + ); + } + else { + $element['#attached']['_hs_new_setting_ajax'][] = array($hsid, $settings['HierarchicalSelect']['settings']["hs-$hsid"]); + } + + $form_state['storage']['hs']['js_settings_sent'][$hsid] = TRUE; + } + + return $element; +} + +function _hs_new_setting_ajax($hsid = FALSE, $settings = NULL) { + static $hs_settings = array(); + + if ($hsid !== FALSE) { + $hs_settings[] = array('hsid' => $hsid, 'settings' => $settings); + } + + return $hs_settings; +} + +// Basic config validation and diagnostics. +function _hs_process_developer_mode_log_diagnostics(&$element) { + if (HS_DEVELOPER_MODE) { + $config = $element['#config']; + $diagnostics = array(); + if (!isset($config['module']) || empty($config['module'])) { + $diagnostics[] = t("'module is not set!"); + } + elseif (!module_exists($config['module'])) { + $diagnostics[] = t('the module that should be used (module) is not installed!', array('%module' => $config['module'])); + } + else { + $required_params = module_invoke($config['module'], 'hierarchical_select_params'); + $missing_params = array_diff($required_params, array_keys($config['params'])); + if (!empty($missing_params)) { + $diagnostics[] = t("'params' is missing values for: ") . implode(', ', $missing_params) . '.'; + } + } + $config_id = (isset($config['config_id']) && is_string($config['config_id'])) ? $config['config_id'] : 'none'; + if (empty($diagnostics)) { + _hierarchical_select_log("Config diagnostics (config id: $config_id): no problems found!"); + } + else { + $diagnostics_string = print_r($diagnostics, TRUE); + $message = "Config diagnostics (config id: $config_id): $diagnostics_string"; + _hierarchical_select_log($message); + $title = $element['#title']; + $element = array(); + $element['#type'] = 'item'; + $element['#title'] = $title; + $element['#markup'] = '

Fix the indicated errors in the #config property first!
' . nl2br($message) . '

'; + return FALSE; + } + } + return TRUE; +} + +function _hs_process_developer_mode_log_selections($config, $hs_selection, $db_selection) { + if (HS_DEVELOPER_MODE) { + _hierarchical_select_log("Calculated hierarchical select selection:"); + _hierarchical_select_log($hs_selection); + + if ($config['dropbox']['status']) { + _hierarchical_select_log("Calculated dropbox selection:"); + _hierarchical_select_log($db_selection); + } + } +} + +function _hs_process_developer_mode_log_hierarchy_and_dropbox($config, $hierarchy, $dropbox) { + if (HS_DEVELOPER_MODE) { + _hierarchical_select_log('Generated hierarchy in ' . $hierarchy->build_time['total'] . ' ms:'); + _hierarchical_select_log($hierarchy); + + if ($config['dropbox']['status']) { + _hierarchical_select_log('Generated dropbox in ' . $dropbox->build_time . ' ms: '); + _hierarchical_select_log($dropbox); + } + } +} + +function _hs_process_developer_mode_send_log_js($element, $hsid) { + if (HS_DEVELOPER_MODE) { + $log = _hierarchical_select_log(NULL, TRUE); + $settings = array( + 'HierarchicalSelect' => array( + 'initialLog' => array( + "hs-$hsid" => $log, + ), + ), + ); + $element['#attached']['js'][] = array( + 'type' => 'setting', + 'data' => $settings, + ); + } + + return $element; +} + +function _hs_process_exclusive_lineages($element, $hs_selection, $db_selection) { + $config = $element['#config']; + $special_items = _hs_process_shortcut_special_items($config); + + // If: + // - the special_items setting has been configured + // - at least one special item has the 'exclusive' property + // - the dropbox is enabled + // then do the necessary processing to make exclusive lineages possible. + if (!empty($special_items) && count($special_items['exclusive']) && $config['dropbox']['status']) { + // When the form is first loaded, $db_selection will contain the selection + // that we should check, but in updates, $hs_selection will. + $selection = (!empty($hs_selection)) ? $hs_selection : $db_selection; + + // If the current selection of the hierarchical select matches one of the + // configured exclusive items, then disable the dropbox (to ensure an + // exclusive selection). + $exclusive_item = array_intersect($selection, $special_items['exclusive']); + if (count($exclusive_item)) { + // By also updating the configuration stored in $element, we ensure that + // the validation step, which extracts the configuration again, also gets + // the updated config. + $element['#config']['dropbox']['status'] = 0; + + // Set the hierarchical select to the exclusive item and make the + // dropbox empty. + $hs_selection = array(0 => reset($exclusive_item)); + $db_selection = array(); + } + } + + return array($element, $hs_selection, $db_selection); +} + +function _hs_process_render_create_new_item($element, $hierarchy) { + $creating_new_item = FALSE; + + // This container and the "Create" / "Cancel" buttons must always be part of + // the form, even when HS is not in create mode, in order for AJAX submit + // callbacks on the "Create" and "Cancel" buttons to be processed correctly. + // + // Basically, FAPI looks through each of the buttons in the form to determine + // which one was clicked. If it can't find the responsible button, it + // assumes it was the first button in the form. This is problematic when the + // user clicks on the "Create" or "Cancel" buttons because we only want them + // to show up when HS is in create mode. To fix this, we always render the + // buttons as part of the form, then disable access to them in an + // "#after_build" callback. + // + // This might not be necessary if we used D7's native AJAX callback function, + // ajax_form_callback(). + $element['hierarchical_select']['create_new_item'] = array( + '#prefix' => '
', + '#suffix' => '
', + '#after_build' => array('hierarchical_select_create_new_item_after_build'), + ); + + // @todo Port to use built-in D7 AJAX callback? + $element['hierarchical_select']['create_new_item']['create'] = array( + '#type' => 'submit', + '#value' => t('Create'), + '#attributes' => array('class' => array('create-new-item-create')), + '#limit_validation_errors' => array($element['#parents']), + '#validate' => array(), + '#submit' => array('hierarchical_select_ajax_update_submit'), + ); + + $element['hierarchical_select']['create_new_item']['cancel'] = array( + '#type' => 'submit', + '#value' => t('Cancel'), + '#attributes' => array('class' => array('create-new-item-cancel')), + '#limit_validation_errors' => array($element['#parents']), + '#validate' => array(), + '#submit' => array('hierarchical_select_ajax_update_submit'), + ); + + if (isset($element['#value']['hierarchical_select']['selects'])) { + foreach ($element['#value']['hierarchical_select']['selects'] as $depth => $value) { + if ($value == 'create_new_item' && _hierarchical_select_create_new_item_is_allowed($element['#config'], $depth)) { + $creating_new_item = TRUE; + + // We want to override the select in which the "create_new_item" + // option was selected and hide all selects after that, if they exist. + // If depth == 0, then that means all selects should be hidden. + if ($depth == 0) { + unset($element['hierarchical_select']['selects']); + } + else { + for ($i = $depth; $i < count($hierarchy->lineage); $i++) { + unset($element['hierarchical_select']['selects'][$i]); + } + } + + $item_type_depth = ($value == 'create_new_item') ? $depth : $depth + 1; + $item_type = (count($element['#config']['editability']['item_types']) == $item_type_depth) + ? t($element['#config']['editability']['item_types'][$item_type_depth]) + : t('item'); + + $element['hierarchical_select']['create_new_item']['input'] = array( + '#type' => 'textfield', + '#size' => 20, + '#maxlength' => 255, + '#default_value' => t('new @item', array('@item' => $item_type)), + '#attributes' => array( + 'title' => t('new @item', array('@item' => $item_type)), + 'class' => array('create-new-item-input'), + ), + // Prevent the textfield from being wrapped in a div. This + // simplifies the CSS and JS code. + '#theme_wrappers' => array(), + // Place the textfield above the "Create" / "Cancel" buttons. + '#weight' => -1, + ); + } + } + } + $element['hierarchical_select']['create_new_item']['#creating_new_item'] = $creating_new_item; + + return array($element, $creating_new_item); +} + +/** + * Render API callback: Controls access to the create_new_item form. + * + * Only allows access to the create_new_item form if creating a new item. + * + * This function is assigned as an #after_build callback in + * _hs_process_render_create_new_item(). + */ +function hierarchical_select_create_new_item_after_build(array $element) { + $element['#access'] = $element['#creating_new_item']; + + return $element; +} + +function _hs_process_render_dropbox($element, $hsid, $creating_new_item, $dropbox, $form_state) { + $config = $element['#config']; + + if ($config['dropbox']['status']) { + if (!$creating_new_item) { + // Append an "Add" button to the selects. + $element['hierarchical_select']['dropbox_add'] = array( + '#type' => 'submit', + '#value' => t('Add'), + '#attributes' => array('class' => array('add-to-dropbox')), + '#limit_validation_errors' => array($element['#parents']), + '#validate' => array(), + '#submit' => array('hierarchical_select_ajax_update_submit'), + ); + } + + if ($config['dropbox']['limit'] > 0) { // Zero as dropbox limit means no limit. + if (count($dropbox->lineages) >= $config['dropbox']['limit']) { + $element['dropbox_limit_warning'] = array( + '#markup' => t("You've reached the maximum number of items you can select."), + '#prefix' => '

', + '#suffix' => '

', + ); + + // Disable all child form elements of $element['hierarchical_select]. + // _hierarchical_select_mark_as_disabled($element['hierarchical_select']); + + // TODO: make the above work again. Currently, we're just disabling + // the "Add" button. #disabled can't be used for the same reasons as + // described in _hierarchical_select_mark_as_disabled(). + $element['hierarchical_select']['dropbox_add']['#attributes']['disabled'] = TRUE; + } + } + + // Store the currently selected lineages of the dropbox in the form state's + // storage section. + if (isset($dropbox->lineages_selections)) { + $form_state['storage']['hs'][$hsid]['dropbox_lineages_selections'] = $dropbox->lineages_selections; + } + + // Add the dropbox-as-a-table that will be visible to the user. + $element['dropbox']['visible'] = _hs_process_render_db_table($hsid, $dropbox); + } + + return array($element, $form_state); +} + +function _hs_process_render_nojs($element, $config) { + // This button and accompanying help text will be hidden when Javascript is + // enabled. + $element['nojs'] = array( + '#prefix' => '
', + '#suffix' => '
', + ); + $element['nojs']['update_button'] = array( + '#type' => 'submit', + '#value' => t('Update'), + '#attributes' => array('class' => array('update-button')), + '#limit_validation_errors' => array($element['#parents']), + '#validate' => array(), + '#submit' => array('hierarchical_select_ajax_update_submit'), + '#ajax' => array( + 'callback' => 'menu_link_weight_parent_ajax_callback', + 'wrapper' => 'menu-link-weight-wrapper', + ), + ); + $element['nojs']['update_button_help_text'] = array( + '#markup' => _hierarchical_select_nojs_helptext($config['dropbox']['status']), + '#prefix' => '
', + '#suffix' => '
', + ); + + return $element; +} + +/** + * Hierarchical select form element type #process callback. + */ +function form_hierarchical_select_process($element, &$form_state, $complete_form) { + if (arg(0) != 'hierarchical_select_ajax') { + // Get unique identifier using parents of the field. + $cid = isset($element['#parents']) ? implode("-", $element['#parents']) : implode("-", $element['#field_parents']); + + // Verify if hsid is present. + $elhsid = drupal_array_get_nested_value($element, array('#value', 'hsid')); + + if (!isset($elhsid)) { + // Retrieve previous element from form_state. + $cached = drupal_array_get_nested_value($form_state, array('storage', 'hs', 'hs_fields', $cid)); + } + if (empty($cached)) { + $docache = TRUE; + } + else { + // Switch current element with the "cached". + return $cached; + } + } + + // Determine the HSID. + $hsid = _hs_process_determine_hsid($element, $form_state); + + // Config. + $config = $element['#config']; + + // Attach CSS/JS files and JS settings. + $element = _hs_process_attach_css_js($element, $hsid, $form_state, $complete_form); + + // Developer mode diagnostics, return immediately in case of a config error. + if (!_hs_process_developer_mode_log_diagnostics($element)) { + return $element; + } + + // Calculate the selections in both the hierarchical select and the dropbox, + // we need these before we can render anything. + $hs_selection = $db_selection = array(); + list($hs_selection, $db_selection) = _hierarchical_select_process_calculate_selections($element, $hsid, $form_state); + + // Developer mode logging: log selections. + _hs_process_developer_mode_log_selections($config, $hs_selection, $db_selection); + + // Dynamically disable the dropbox when an exclusive item has been selected. + // When this happens, the configuration is dynamically altered. Hence, we + // need to update $config. + list($element, $hs_selection, $db_selection) = _hs_process_exclusive_lineages($element, $hs_selection, $db_selection); + $config = $element['#config']; + + // Generate the $hierarchy and $dropbox objects using the selections that + // were just calculated. + $dropbox = (!$config['dropbox']['status']) ? FALSE : _hierarchical_select_dropbox_generate($config, $db_selection); + $hierarchy = _hierarchical_select_hierarchy_generate($config, $hs_selection, $element['#required'], $dropbox); + + // Developer mode logging: log $hierarchy and $dropbox objects. + _hs_process_developer_mode_log_hierarchy_and_dropbox($config, $hierarchy, $dropbox); + + // Finally, calculate the return value of this hierarchical_select form + // element. This will be set in _hierarchical_select_validate(). (If we'd + // set it now, it would be overridden again.) + $element['#return_value'] = _hierarchical_select_process_calculate_return_value($hierarchy, ($config['dropbox']['status']) ? $dropbox : FALSE, $config['module'], $config['params'], $config['save_lineage']); + if (!is_array($element['#return_value'])) { + $element['#return_value'] = array($element['#return_value']); + } + + // Add a validate callback, which will: + // - validate that the dropbox limit was not exceeded. + // - set the return value of this form element. + // Also make sure it is the *first* validate callback. + $element['#element_validate'] = (isset($element['#element_validate'])) ? $element['#element_validate'] : array(); + $element['#element_validate'] = array_merge(array('_hierarchical_select_validate'), $element['#element_validate']); + + // Ensure the form is cached, for AJAX to work. + $form_state['cache'] = TRUE; + + // + // Rendering. + // + + // Ensure that #tree is enabled! + $element['#tree'] = TRUE; + + // Store the HSID in a hidden form element; when an AJAX callback comes in, + // we'll know which HS was updated. + $element['hsid'] = array('#type' => 'hidden', '#value' => $hsid); + + + // If render_flat_select is enabled, render a flat select. + if ($config['render_flat_select']) { + $element['flat_select'] = _hs_process_render_flat_select($hierarchy, $dropbox, $config); + // See https://www.drupal.org/node/994820 + if (empty($element['flat_select']['#options'])) { + unset($element['flat_select']); + } + } + + // Render the hierarchical select. + $element['hierarchical_select'] = array( + '#theme' => 'hierarchical_select_selects_container', + ); + $size = isset($element['#size']) ? $element['#size'] : 0; + $element['hierarchical_select']['selects'] = _hs_process_render_hs_selects($hsid, $hierarchy, $size); + + // When the special "create_new_item" value is passed in a level, replace + // that level with an inline modal form to create a new item, and hide all + // subsequent selects. + list($element, $creating_new_item) = _hs_process_render_create_new_item($element, $hierarchy); + + // Render the dropbox, if enabled. + // Automatically hides the "Add" button when creating a new item. + // Automatically disables HS' selects when reaching the dropbox limit. + // Stores the currently selected lineages of the dropbox in storage. + list($element, $form_state) = _hs_process_render_dropbox($element, $hsid, $creating_new_item, $dropbox, $form_state); + + // Render the HTML that allows for graceful degradation. + $element = _hs_process_render_nojs($element, $config); + + // Ensure the render order is correct. + $element['hierarchical_select']['#weight'] = 0; + $element['dropbox_limit_warning']['#weight'] = 1; + $element['dropbox']['#weight'] = 2; + $element['nojs']['#weight'] = 3; + + // If the form item is marked as disabled, disable all child form items as + // well. + if (isset($element['#disabled']) && $element['#disabled']) { + _hierarchical_select_mark_as_disabled($element); + } + + // This prevents values from in $form_state['input'] to be used instead of + // the generated default values (#default_value). + // For example: $element['hierarchical_select']['selects']['0']['#default_value'] + // is set to 'label_0' after an "Add" operation. When $form_state['input'] + // is NOT erased, the corresponding value in $form_state['input'] will be + // used instead of the default value that was set. This would result in + // undesired behavior. + // This, however, must not be called on node preview, becuase in that case + // the node will be rebuilt and we need the values inside $form_state['input'] + // to recreate the edited form properly. + // @TODO: If the form is rebuilt by some other action than a node preview, we + // might lose data again, we should see if there's any way to prevent this from + // happening by setting this value after the form has been flagged to be rebuilt, + // but as far as I checked, there's not. + // Another option might be to rework the need of this function to prevent + // the undesired behaviors of not having it with some other logic that would + // work as well if the form is rebuilt. + if (empty($docache)) { + if (!isset($form_state['triggering_element']) || ($form_state['triggering_element']['#value'] != t('Preview') && $form_state['triggering_element']['#value'] != t('View changes'))) { + if (isset($form_state['input']) && is_array($form_state['input'])) { + drupal_array_set_nested_value($form_state['input'], $element['#array_parents'], array()); + } + } + } + else { + // Store new element in cache. + $form_state['storage']['hs']['hs_fields'][$cid] = $element; + } + + // Send the collected developer mode logs (by using #attached JS). + $element = _hs_process_developer_mode_send_log_js($element, $hsid); + + return $element; +} + +/** + * Submit callback; only sets no_redirect to TRUE (which already) + */ +function hierarchical_select_ajax_update_submit($form, &$form_state) { + $form_state['no_redirect'] = TRUE; +} + + +/** + * Hierarchical select form element #element_validate callback. + */ +function _hierarchical_select_validate(&$element, &$form_state) { + // If the dropbox is enabled and a dropbox limit is configured, check if + // this limit is not exceeded. + $hsid = $element['hsid']['#value']; + $config = _hierarchical_select_inherit_default_config($element['#config']); + if ($config['dropbox']['status']) { + if ($config['dropbox']['limit'] > 0) { // Zero as dropbox limit means no limit. + // TRICKY: #element_validate is not called upon the initial rendering + // (i.e. it is assumed that the default value is valid). However, + // Hierarchical Select's config can influence the validity (i.e. how + // many selections may be added to the dropbox). This means it's + // possible the user has actually selected too many items without being + // notified of this. + $lineage_count = count($form_state['storage']['hs'][$hsid]['dropbox_lineages_selections']); + if ($lineage_count > $config['dropbox']['limit']) { + // TRICKY: this should propagate the error down to the children, but + // this doesn't seem to happen, since for example the selects of the + // hierarchical select don't get the error class set. Further + // investigation needed. + form_error( + $element, + t("You've selected %lineage-count items, but you're only allowed to select %dropbox-limit items.", + array( + '%lineage-count' => $lineage_count, + '%dropbox-limit' => $config['dropbox']['limit'], + ) + ) + ); + _hierarchical_select_form_set_error_class($element); + } + } + } + + // Set the proper return value. I.e. instead of returning all the values + // that are used for making the hierarchical_select form element type work, + // we pass a flat array of item ids. e.g. for the taxonomy module, this will + // be an array of term ids. If a single item is selected, this will not be + // an array. + // If the form item is disabled, set the default value as the return value, + // because otherwise nothing would be returned (disabled form items are not + // submitted, as described in the HTML standard). + if (isset($element['#disabled']) && $element['#disabled']) { + $element['#return_value'] = $element['#default_value']; + } + + $element['#value'] = $element['#return_value']; + form_set_value($element, $element['#value'], $form_state); + + // We have to check again for errors. This line is taken litterally from + // form.inc, so it works in an identical way. + if ($element['#required'] && + (!isset($form_state['submit_handlers'][0]) || $form_state['submit_handlers'][0] !== 'hierarchical_select_ajax_update_submit') && + (!count($element['#value']) || (is_string($element['#value']) && strlen(trim($element['#value'])) == 0))) { + form_error($element, t('!name field is required.', array('!name' => $element['#title']))); + _hierarchical_select_form_set_error_class($element); + } +} + + +//---------------------------------------------------------------------------- +// Forms API #process callback: +// Calculation of hierarchical select and dropbox selection. + +/** + * Get the current (flat) selection of the hierarchical select. + * + * This selection is updatable by the user, because the values are retrieved + * from the selects in $element['hierarchical_select']['selects']. + * + * @param array $element + * A hierarchical_select form element. + * @return array + * An array (bag) containing the ids of the selected items in the + * hierarchical select. + */ +function _hierarchical_select_process_get_hs_selection($element) { + $hs_selection = array(); + $config = _hierarchical_select_inherit_default_config($element['#config']); + + if (!empty($element['#value']['hierarchical_select']['selects'])) { + if ($config['save_lineage']) { + foreach ($element['#value']['hierarchical_select']['selects'] as $key => $value) { + $hs_selection[] = $value; + } + } + else { + foreach ($element['#value']['hierarchical_select']['selects'] as $key => $value) { + $hs_selection[] = $value; + } + $hs_selection = _hierarchical_select_hierarchy_validate($hs_selection, $config['module'], $config['params']); + + // Get the last valid value. (Only the deepest item gets saved). Make + // sure $hs_selection is an array at all times. + $hs_selection = ($hs_selection != -1) ? array(end($hs_selection)) : array(); + } + } + + return $hs_selection; +} + +/** + * Get the current (flat) selection of the dropbox. + * + * This selection is not updatable by the user, because the values are + * retrieved from the hidden values in + * $element['dropbox']['hidden']['lineages_selections']. This selection can + * only be updated by the server, i.e. when the user clicks the "Add" button. + * But this selection can still be reduced in size if the user has marked + * dropbox entries (lineages) for removal. + * + * @param $element + * A hierarchical_select form element. + * @param $form_state + * The $form_state array. We need to look at + * $form_state['storage']['hs'][$hsid]['dropbox_lineages_selections'] + * to know what to remove. + * @return + * An array (bag) containing the ids of the selected items in the + * dropbox. + */ +function _hierarchical_select_process_get_db_selection($element, $hsid, &$form_state) { + $db_selection = array(); + + if (!empty($form_state['storage']['hs'][$hsid]['dropbox_lineages_selections'])) { + // Check which lineages have been marked for removal by the user. + $remove_from_db_selection = array(); + if (isset($element['#value']['dropbox']['visible']['lineages'])) { + foreach ($element['#value']['dropbox']['visible']['lineages'] as $x => $remove_value) { + if ($remove_value['remove'] === '1') { + // $x is of the form "lineage-". Extract the number. + $remove_from_db_selection[] = substr($x, 8); + // By removing the input (POST) reference to the remove checkbox, + // we make sure that on a form rebuild the same remove checkbox, + // which is accessed by index, is not set, preventing a double removal. + // @see https://www.drupal.org/node/1566878#comment-9226261 + $elm = &$form_state['input']; + foreach ($element['#parents'] as $parent) { + $elm = &$elm[$parent]; + } + unset($elm['dropbox']['visible']['lineages'][$x]['remove']); + } + } + } + + // Add all selections to the dropbox selection, except for the ones that + // are scheduled for removal. + foreach ($form_state['storage']['hs'][$hsid]['dropbox_lineages_selections'] as $x => $selection) { + if (!in_array($x, $remove_from_db_selection)) { + $db_selection = array_merge($db_selection, $selection); + } + } + + // Ensure that the last item of each selection that was scheduled for + // removal is completely absent from the dropbox selection. + // In case of a tree with multiple parents, the same item can exist in + // different entries, and thus it would stay in the selection. When the + // server then reconstructs all lineages, the lineage we're removing, will + // also be reconstructed: it will seem as if the removing didn't work! + // This will not break removing dropbox entries for hierarchies without + // multiple parents, since items at the deepest level are always unique to + // that specific lineage. + // Easier explanation at http://drupal.org/node/221210#comment-733715. + foreach ($remove_from_db_selection as $key => $x) { + $item = end($form_state['storage']['hs'][$hsid]['dropbox_lineages_selections'][$x]); + $position = array_search($item, $db_selection); + if ($position) { + unset($db_selection[$position]); + } + } + $db_selection = array_unique($db_selection); + } + + return $db_selection; +} + +/** + * Calculates the flat selections of both the hierarchical select and the + * dropbox. + * + * @param $element + * A hierarchical_select form element. + * @param $form_state + * The $form_state array. We need to look at $form_state['input']['op'], to + * know which operation has occurred. + * @return + * An array of the following structure: + * array( + * $hierarchical_select_selection = array(), // Flat list of selected ids. + * $dropbox_selection = array(), + * ) + * with both of the subarrays flat lists of selected ids. The + * _hierarchical_select_hierarchy_generate() and + * _hierarchical_select_dropbox_generate() functions should be applied on + * these respective subarrays. + * + * @see _hierarchical_select_hierarchy_generate() + * @see _hierarchical_select_dropbox_generate() + */ +function _hierarchical_select_process_calculate_selections(&$element, $hsid, &$form_state) { + $hs_selection = array(); // hierarchical select selection + $db_selection = array(); // dropbox selection + + $config = _hierarchical_select_inherit_default_config($element['#config']); + $dropbox = (bool) $config['dropbox']['status']; + + // When: + // - no input data was provided (through POST nor GET) + // - or #value is set directly and not by a Hierarchical Select POST (and + // therefor set either manually or by another module), + // then use the value of #default_value, or when available, of #value. + if (empty($form_state['input']) || (!isset($element['#value']['hierarchical_select']) && !isset($element['#value']['dropbox']))) { + $value = (!empty($element['#value'])) ? $element['#value'] : $element['#default_value']; + $value = (is_array($value)) ? $value : array($value); + if ($dropbox) { + $db_selection = $value; + } + else { + $hs_selection = $value; + } + } + else { + $op = (isset($form_state['input']['op']) && isset($form_state['input']['hsid']) && $form_state['input']['hsid'] == $hsid) ? $form_state['input']['op'] : NULL; + if ($dropbox && $op == t('Add')) { + $hs_selection = _hierarchical_select_process_get_hs_selection($element); + $db_selection = _hierarchical_select_process_get_db_selection($element, $hsid, $form_state); + + // Add $hs_selection to $db_selection. + $db_selection = array_unique(array_merge($db_selection, $hs_selection)); + + // Only reset $hs_selection if the user has configured it that way. + if ((bool) $config['dropbox']['reset_hs']) { + $hs_selection = array(); + } + } + else if ($op == t('Create')) { + // This code handles both the creation of a new item in an existing + // level and the creation of an item that also creates a new level. + $label = trim($element['#value']['hierarchical_select']['create_new_item']['input']); + $selects = isset($element['#value']['hierarchical_select']['selects']) ? $element['#value']['hierarchical_select']['selects'] : array(); + $depth = count($selects); + $parent = ($depth > 0) ? end($selects) : 0; + + // Disallow items with empty labels; allow the user again to create a + // (proper) new item. + if (empty($label)) { + $element['#value']['hierarchical_select']['selects'][count($selects)] = 'create_new_item'; + } + // Ensure that this new item will not violate the max_levels and + // allowed_levels settings. + else if ( + (count(module_invoke($config['module'], 'hierarchical_select_children', $parent, $config['params'])) + || $config['editability']['max_levels'] == 0 + || $depth < $config['editability']['max_levels'] + ) + && + (_hierarchical_select_create_new_item_is_allowed($config, $depth)) + ) { + // Create the new item in the hierarchy and retrieve its value. + $value = module_invoke($config['module'], 'hierarchical_select_create_item', check_plain($label), $parent, $config['params']); + + // Ensure the newly created item will be selected after rendering. + if ($value) { + // Pretend there was a select where the "create new item" section + // was, and assign it the value of the item that was just created. + $element['#value']['hierarchical_select']['selects'][count($selects)] = $value; + } + } + + $hs_selection = _hierarchical_select_process_get_hs_selection($element); + if ($dropbox) { + $db_selection = _hierarchical_select_process_get_db_selection($element, $hsid, $form_state); + } + } + else { + // This handles the cases of: + // - $op == t('Update') + // - $op == t('Cancel') (used when creating a new item or a new level) + // - any other submit button, e.g. the "Preview" button + $hs_selection = _hierarchical_select_process_get_hs_selection($element); + if ($dropbox) { + $db_selection = _hierarchical_select_process_get_db_selection($element, $hsid, $form_state); + } + } + } + + // Prevent doubles in either array. + $hs_selection = array_unique($hs_selection, SORT_REGULAR); + $db_selection = array_unique($db_selection, SORT_REGULAR); + + return array($hs_selection, $db_selection); +} + + +//---------------------------------------------------------------------------- +// Forms API #process callback: +// Rendering (generation of FAPI code) of hierarchical select and dropbox. + +/** + * Render the selects in the hierarchical select. + * + * @param $hsid + * A hierarchical select id. + * @param $hierarchy + * A hierarchy object. + * @param $size + * The $size to render each select with. + * @return + * A structured array for use in the Forms API. + */ +function _hs_process_render_hs_selects($hsid, $hierarchy, $size) { + $form['#tree'] = TRUE; + $form['#prefix'] = '
'; + $form['#suffix'] = '
'; + + foreach ($hierarchy->lineage as $depth => $selected_item) { + $form[$depth] = array( + '#type' => 'select', + '#options' => $hierarchy->levels[$depth], + '#default_value' => $selected_item, + '#size' => $size, + // Prevent the select from being wrapped in a div. This simplifies the + // CSS and JS code. + '#theme_wrappers' => array(), + // This alternative to theme_select ets a special class on the level + // label option, if any, to make level label styles possible. + '#theme' => 'hierarchical_select_select', + // Add child information. When a child has no children, its + // corresponding "option" element will be marked as such. + '#childinfo' => (isset($hierarchy->childinfo[$depth])) ? $hierarchy->childinfo[$depth] : NULL, + // Drupal 7's Forms API insists on validating "select" form elements, + // despite the fact that this form element is merely part of a larger + // whole, with its own #element_validate callback. This disables that + // validation. + '#validated' => TRUE, + ); + } + return $form; +} + +/** + * Render the visible part of the dropbox. + * + * @param $hsid + * A hierarchical select id. + * @param $dropbox + * A dropbox object. + * @return + * A structured array for use in the Forms API. + */ +function _hs_process_render_db_table($hsid, $dropbox) { + $element['#tree'] = TRUE; + $element['#theme'] = 'hierarchical_select_dropbox_table'; + + + // This information is necessary for the #theme callback. + $element['title'] = array('#type' => 'value', '#value' => t($dropbox->title)); + $element['separator'] = array('#type' => 'value', '#value' => '›'); + $element['is_empty'] = array('#type' => 'value', '#value' => empty($dropbox->lineages)); + + + if (!empty($dropbox->lineages)) { + foreach ($dropbox->lineages as $x => $lineage) { + + // Store position information for the lineage. This will be used in the + // #theme callback. + $element['lineages']["lineage-$x"] = array( + '#zebra' => (($x + 1) % 2 == 0) ? 'even' : 'odd', + '#first' => ($x == 0) ? 'first' : '', + '#last' => ($x == count($dropbox->lineages) - 1) ? 'last' : '', + ); + + // Create a 'markup' element for each item in the lineage. + foreach ($lineage as $depth => $item) { + // The item is selected when save_lineage is enabled (i.e. each item + // will be selected), or when the item is the last item in the current + // lineage. + $is_selected = $dropbox->save_lineage || ($depth == count($lineage) - 1); + + $element['lineages']["lineage-$x"][$depth] = array( + '#markup' => $item['label'], + '#prefix' => '', + '#suffix' => '', + ); + } + + // Finally, create a "Remove" checkbox for the lineage. + $element['lineages']["lineage-$x"]['remove'] = array( + '#type' => 'checkbox', + '#title' => t('Remove'), + ); + } + } + + return $element; +} + +/** + * Render a flat select version of a hierarchical_select form element. This is + * necessary for backwards compatibility (together with some Javascript code) + * in case of GET forms. + * + * @param $hierarchy + * A hierarchy object. + * @param $dropbox + * A dropbox object. + * @param $config + * A config array with at least the following settings: + * - module + * - params + * - dropbox + * - status + * @return + * A structured array for use in the Forms API. + */ +function _hs_process_render_flat_select($hierarchy, $dropbox, $config) { + $selection = array(); + if ($config['dropbox']['status']) { + foreach ($dropbox->lineages_selections as $lineage_selection) { + $selection = array_merge($selection, $lineage_selection); + } + } + else { + $selection = $hierarchy->lineage; + } + + $options = array(); + foreach ($selection as $value) { + $is_valid = module_invoke($config['module'], 'hierarchical_select_valid_item', $value, $config['params']); + if ($is_valid) { + $options[$value] = $value; + } + } + + $element = array( + '#type' => 'select', + '#multiple' => ($config['save_lineage'] || $config['dropbox']['status']), + '#options' => $options, + '#value' => array_keys($options), + // Use a #theme callback to prevent the select from being wrapped in a + // div. This simplifies the CSS and JS code. + '#theme' => 'hierarchical_select_select', + '#attributes' => array('class' => array('flat-select')), + ); + + return $element; +} + +/** + * Calculate the return value of a hierarchical_select form element, based on + * the $hierarchy and $dropbox objects. We have to set a return value, because + * the values set and used by this form element ($element['#value]) are not + * easily usable in the Forms API; we want to return a flat list of item ids. + * + * @param $hierarchy + * A hierarchy object. + * @param $dropbox + * Optional. A dropbox object. + * @param $module + * The module that should be used for HS hooks. + * @param $params + * Optional. An array of parameters, which may be necessary for some + * implementations. + * @param $save_lineage + * Whether the save_lineage setting is enabled or not. + * @return + * A single item id or a flat array of item ids. + */ +function _hierarchical_select_process_calculate_return_value($hierarchy, $dropbox = FALSE, $module, $params, $save_lineage) { + if (!$dropbox) { + $return_value = _hierarchical_select_hierarchy_validate($hierarchy->lineage, $module, $params); + // If the save_lineage setting is disabled, keep only the deepest item. + if (!$save_lineage) { + $return_value = (is_array($return_value)) ? end($return_value) : NULL; + } + + // Prevent a return value of -1. -1 is used for HS' internal system and + // means "nothing selected", but to Drupal it *will* seam like a valid + // value. Therefore, we set it to NULL. + $return_value = ($return_value != -1) ? $return_value : NULL; + } + else { + $return_value = array(); + foreach ($dropbox->lineages_selections as $x => $selection) { + if (!$save_lineage) { + // An entry in the dropbox when the save_lineage setting is disabled + // is only the deepest item of the generated lineage. + $return_value[] = end($selection); + } + else { + // An entry in the dropbox when the save_lineage setting is enabled is + // the entire generated lineage, if it's valid (i.e. if the user has + // not tampered with it). + $lineage = _hierarchical_select_hierarchy_validate($selection, $module, $params); + $return_value = array_merge($return_value, $lineage); + } + } + $return_value = array_unique($return_value); + } + + return $return_value; +} + + +//---------------------------------------------------------------------------- +// Private functions. + +/** + * Inherit the default config from Hierarchical Selects' hook_elements(). + * + * @param $config + * A config array with at least the following settings: + * - module + * - params + * @return + * An updated config array. + */ +function _hierarchical_select_inherit_default_config($config, $defaults_override = array()) { + // Set defaults for unconfigured settings. Get the defaults from our + // hook_elements() implementation. Default properties from this hook are + // applied automatically, but properties inside properties, such as is the + // case for Hierarchical Select's #config property, aren't applied. + $type = hierarchical_select_element_info(); + $defaults = $type['hierarchical_select']['#config']; + // Don't inherit the module and params settings. + unset($defaults['module']); + unset($defaults['params']); + + // Allow the defaults to be overridden. + $defaults = array_smart_merge($defaults, $defaults_override); + + // Apply the defaults to the config. + $config = array_smart_merge($defaults, $config); + + return $config; +} + +/** + * Convert a hierarchy object into an array of arrays that can be used for + * caching an entire hierarchy in a client-side database. + * + * @param $hierarchy + * A hierarchy object. + * @return + * An array of arrays. + */ +function _hierarchical_select_json_convert_hierarchy_to_cache($hierarchy) { + // Convert the hierarchy object to an array of values like these: + // array('value' => $term_id, 'label => $term_name, 'parent' => $term_id) + $cache = array(); + foreach ($hierarchy->levels as $depth => $items) { + $weight = 0; + foreach ($items as $value => $label) { + $weight++; + $cache[] = array( + 'value' => $value, + 'label' => $label, + 'parent' => ($depth == 0) ? 0 : $hierarchy->lineage[$depth - 1], + 'weight' => $weight, + ); + } + } + + // The last item in the lineage never has any children. + $value = end($hierarchy->lineage); + $cache[] = array( + 'value' => $value . '-has-no-children', // Construct a pseudo-value (will never be actually used). + 'label' => '', + 'parent' => $value, + 'weight' => 0, + ); + + return $cache; +} + +/** + * Helper function that marks every element in the given element as disabled. + * + * @param &$element + * The element of which we want to mark all elements as disabled. + * @return + * A structured array for use in the Forms API. + */ +function _hierarchical_select_mark_as_disabled(&$element) { + // Setting $element['#disabled'] = TRUE resulted in undesired side-effects: + // when the dropbox limit would be reached after pressing the "Add" button, + // then the *entire form* would be submitted. Using #attributes instead does + // not trigger this behavior. + // Based on documentation of @see _form_builder_handle_input_element(): + // "If a form wants to start a control off with one of these attributes + // for UI purposes only, but still allow input to be processed if it's + // sumitted, it can set the desired attribute in #attributes directly + // rather than using #disabled." + // #disabled prevents #value from containing values for disabled elements, + // but using #attributes circumvents this. Most likely, Form API thinks that + // because HS' selects are disabled, that the whole of HS is disabled (which + // is of course a wrong assumption). Hence it thinks the 'op' that is being + // passed ('Add') is wrong and is forcefully being set through JS (which is + // also a wrong assumption). Hence it reverts to the main form's default + // submit handler. + $element['#attributes']['disabled'] = TRUE; + + // Recurse through all children: + foreach (element_children($element) as $key) { + if (isset($element[$key]) && $element[$key]) { + _hierarchical_select_mark_as_disabled($element[$key]); + } + } +} + +/** + * Helper function to determine whether a given depth (i.e. the depth of a + * level) is allowed by the allowed_levels setting. + * + * @param $config + * A config array with at least the following settings: + * - editability + * - allowed_levels + * @param $depth + * A depth, starting from 0. + * @return + * 0 or 1 if it allowed_levels is set for the given depth, 1 otherwise. + */ +function _hierarchical_select_create_new_item_is_allowed($config, $depth) { + return (isset($config['editability']['allowed_levels'][$depth])) ? $config['editability']['allowed_levels'][$depth] : 1; +} + +/** + * Helper function that generates the help text is that is displayed to the + * user when Javascript is disabled. + * + * @param $dropbox_is_enabled + * Indicates if the dropbox is enabled or not, the help text will be + * adjusted depending on this value. + * @return + * The generated help text (in HTML). + */ +function _hierarchical_select_nojs_helptext($dropbox_is_enabled) { + $output = ''; + + return $output; +} + +/** + * Set the 'error' class on the appropriate part of Hierarchical Select, + * depending on its configuration. + * + * @param $element + * A Hierarchical Select form item. + */ +function _hierarchical_select_form_set_error_class(&$element) { + $config = _hierarchical_select_inherit_default_config($element['#config']); + + if ($config['dropbox']['status']) { + form_error($element['dropbox']['visible']); + } + else { + for ($i = 0; $i < count(element_children($element['hierarchical_select']['selects'])); $i++) { + form_error($element['hierarchical_select']['selects'][$i]); + } + } +} + +/** + * Append messages to Hierarchical Select's log. Used when in developer mode. + * + * @param $item + * Either a message (string) or an array. + * @param $reset + * Reset the stored log. + * @return + * Only when the log is being reset, the stored log is returned. + */ +function _hierarchical_select_log($item, $reset = FALSE) { + static $log; + + if ($reset) { + $copy_of_log = $log; + $log = array(); + return $copy_of_log; + } + + $log[] = $item; +} + + +//---------------------------------------------------------------------------- +// Hierarchy object generation functions. + +/** + * Generate the hierarchy object. + * + * @param $config + * A config array with at least the following settings: + * - module + * - params + * - enforce_deepest + * - save_lineage + * - level_labels + * - status + * - labels + * - editability + * - status + * - allow_new_levels + * - max_levels + * @param $selection + * The selection based on which a HS should be rendered. + * @param $required + * Whether the form element is required or not. (#required in Forms API) + * @param $dropbox + * A dropbox object, or FALSE. + * @return + * A hierarchy object. + */ +function _hierarchical_select_hierarchy_generate($config, $selection, $required, $dropbox = FALSE) { + $hierarchy = new stdClass(); + + // Convert the 'special_items' setting to a more easily accessible format. + if (isset($config['special_items'])) { + $special_items['exclusive'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_exclusive')); + $special_items['none'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_none')); + } + + + // + // Build the lineage. + // + + $start_lineage = microtime(); + + // If save_linage is enabled, reconstruct the lineage. This is necessary + // because e.g. the taxonomy module stores the terms by order of weight and + // lexicography, rather than by hierarchy. + if ($config['save_lineage'] && is_array($selection) && count($selection) >= 2) { + // Ensure the item in the root level is the first item in the selection. + $root_level = array_keys(module_invoke($config['module'], 'hierarchical_select_root_level', $config['params'])); + + for ($i = 0; $i < count($selection); $i++) { + if (in_array($selection[$i], $root_level)) { + if ($i != 0) { // Don't swap if it's already the first item. + list($selection[0], $selection[$i]) = array($selection[$i], $selection[0]); + } + break; + } + } + // Reconstruct all sublevels. + for ($i = 0; $i < count($selection); $i++) { + $children = array_keys(module_invoke($config['module'], 'hierarchical_select_children', $selection[$i], $config['params'])); + + // Ensure the next item in the selection is a child of the current item. + for ($j = $i + 1; $j < count($selection); $j++) { + if (in_array($selection[$j], $children)) { + list($selection[$j], $selection[$i + 1]) = array($selection[$i + 1], $selection[$j]); + } + } + } + } + + // Validate the hierarchy. + $selection = _hierarchical_select_hierarchy_validate($selection, $config['module'], $config['params']); + + // When nothing is currently selected, set the root level to: + // - "" (or its equivalent special item) when: + // - enforce_deepest is enabled *and* level labels are enabled *and* + // no root level label is set (1), or + // - the dropbox is enabled *and* at least one selection has been added + // to the dropbox (2) + // - "label_0" (the root level label) in all other cases. + if ($selection == -1) { + $root_level = module_invoke($config['module'], 'hierarchical_select_root_level', $config['params']); + $first_case = $config['enforce_deepest'] && $config['level_labels']['status'] && !isset($config['level_labels']['labels'][0]); + $second_case = $dropbox && count($dropbox->lineages) > 0; + + // If + // - the special_items setting has been configured, and + // - one special item has the 'none' property + // then we'll use the special item instead of the normal "" option. + $none_option = (isset($special_items) && count($special_items['none'])) ? $special_items['none'][0] : 'none'; + + // Set "" option (or its equivalent special item), or "label_0". + $hierarchy->lineage[0] = ($first_case || $second_case) ? $none_option : 'label_0'; + } + else { + // If save_lineage setting is enabled, then the selection *is* a lineage. + // If it's disabled, we have to generate one ourselves based on the + // (deepest) selected item. + if ($config['save_lineage']) { + // When the form element is optional, the "" setting can be + // selected, thus only the first level will be displayed. As a result, + // we won't receive an array as the selection, but only a single item. + // We convert this into an array. + $hierarchy->lineage = (is_array($selection)) ? $selection : array(0 => $selection); + } + else { + $selection = (is_array($selection)) ? $selection[0] : $selection; + if (module_invoke($config['module'], 'hierarchical_select_valid_item', $selection, $config['params'])) { + $hierarchy->lineage = module_invoke($config['module'], 'hierarchical_select_lineage', $selection, $config['params']); + } + else { + // If the selected item is invalid, then start with an empty lineage. + $hierarchy->lineage = array(); + } + } + } + + // If enforce_deepest is enabled, ensure that the lineage goes as deep as + // possible: append values of items that will be selected by default. + if ($config['enforce_deepest'] && !in_array($hierarchy->lineage[0], array('none', 'label_0'))) { + $hierarchy->lineage = _hierarchical_select_hierarchy_enforce_deepest($hierarchy->lineage, $config['module'], $config['params']); + } + + $end_lineage = microtime(); + + + // + // Build the levels. + // + + $start_levels = microtime(); + + // Start building the levels, initialize with the root level. + $hierarchy->levels[0] = module_invoke($config['module'], 'hierarchical_select_root_level', $config['params']); + $hierarchy->levels[0] = _hierarchical_select_apply_entity_settings($hierarchy->levels[0], $config); + + // Prepend a "" option to the root level when: + // - the editability setting is enabled, and + // - the hook is implemented (this is an optional hook), and + // - the logged in user has permission to edit terms in this vocabulary, and + // - the allowed_levels setting allows to create new items at this level. + if (!empty($config['editability']['status']) + && module_hook($config['module'], 'hierarchical_select_create_item') + && ($config['module'] == 'hs_taxonomy' && (user_access('administer taxonomy') || user_access('edit terms in ' . $config['params']['vid']))) + && _hierarchical_select_create_new_item_is_allowed($config, 0) + ) { + $item_type = (count($config['editability']['item_types']) > 0) + ? t($config['editability']['item_types'][0]) + : t('item'); + $option = theme('hierarchical_select_special_option', array('option' => t('create new !item_type', array('!item_type' => $item_type)))); + $hierarchy->levels[0] = array('create_new_item' => $option) + $hierarchy->levels[0]; + } + + // Prepend a "" option to the root level when: + // - the form element is optional (1), or + // - enforce_deepest is enabled (2), or + // - the dropbox is enabled *and* at least one selection has been added to + // the dropbox (3) + // except when: + // - level labels are enabled + // - the special_items setting has been configured, and + // - one special item has the 'none' property + $first_case = !$required; + $second_case = $config['enforce_deepest']; + $third_case = $dropbox && count($dropbox->lineages) > 0; + if (($first_case || $second_case || $third_case) && (!$config['level_labels']['status'] && isset($special_items) && !count($special_items['none']))) { + $option = theme('hierarchical_select_special_option', array('option' => t('none'))); + $hierarchy->levels[0] = array('none' => $option) + $hierarchy->levels[0]; + } + + // Calculate the lineage's depth (starting from 0). + $max_depth = count($hierarchy->lineage) - 1; + + // Build all sublevels, based on the lineage. + for ($depth = 1; $depth <= $max_depth; $depth++) { + $hierarchy->levels[$depth] = module_invoke($config['module'], 'hierarchical_select_children', $hierarchy->lineage[$depth - 1], $config['params']); + $hierarchy->levels[$depth] = _hierarchical_select_apply_entity_settings($hierarchy->levels[$depth], $config); + } + + if ($config['enforce_deepest']) { + // Prepend a "" option to each level below the root level + // when: + // - the editability setting is enabled, and + // - the hook is implemented (this is an optional hook), and + // - the allowed_levels setting allows to create new items at this level. + if (!empty($config['editability']['status']) + && ($config['module'] == 'hs_taxonomy' && (user_access('administer taxonomy') || user_access('edit terms in ' . $config['params']['vid']))) + && module_hook($config['module'], 'hierarchical_select_create_item')) { + for ($depth = 1; $depth <= $max_depth; $depth++) { + $item_type = (count($config['editability']['item_types']) >= $depth) + ? t($config['editability']['item_types'][$depth]) + : t('item'); + $option = theme('hierarchical_select_special_option', array('option' => t('create new !item_type', array('!item_type' => $item_type)))); + if (_hierarchical_select_create_new_item_is_allowed($config, $depth)) { + $hierarchy->levels[$depth] = array('create_new_item' => $option) + $hierarchy->levels[$depth]; + } + } + } + + // If level labels are enabled and the root label is set, prepend it. + if ($config['level_labels']['status'] && isset($config['level_labels']['labels'][0])) { + $hierarchy->levels[0] = array('label_0' => t($config['level_labels']['labels'][0])) + $hierarchy->levels[0]; + } + } + else if (!$config['enforce_deepest']) { + // Prepend special options to every level. + for ($depth = 0; $depth <= $max_depth; $depth++) { + // Prepend a "" option to the current level when: + // - this is not the root level (the root level already has this), and + // - the editability setting is enabled, and + // - the hook is implemented (this is an optional hook), and + // - the logged in user has permission to edit terms in this vocabulary, and + // - the allowed_levels setting allows to create new items at this level. + if ($depth > 0 + && !empty($config['editability']['status']) + && module_hook($config['module'], 'hierarchical_select_create_item') + && ($config['module'] == 'hs_taxonomy' && (user_access('administer taxonomy') || user_access('edit terms in ' . $config['params']['vid']))) + && _hierarchical_select_create_new_item_is_allowed($config, $depth) + ) { + $item_type = (count($config['editability']['item_types']) == $depth) + ? t($config['editability']['item_types'][$depth]) + : t('item'); + $option = theme('hierarchical_select_special_option', array('option' => t('create new !item_type', array('!item_type' => $item_type)))); + $hierarchy->levels[$depth] = array('create_new_item' => $option) + $hierarchy->levels[$depth]; + } + // Level label: set an empty level label if they've been disabled. + $label = ($config['level_labels']['status'] && isset($config['level_labels']['labels'][$depth])) ? t($config['level_labels']['labels'][$depth]) : ''; + $hierarchy->levels[$depth] = array('label_' . $depth => $label) + $hierarchy->levels[$depth]; + } + + // If the root level label is empty and the none option is present, remove + // the root level label because it's conceptually identical. + if ($hierarchy->levels[0]['label_0'] == '' && isset($hierarchy->levels[0]['none'])) { + unset($hierarchy->levels[0]['label_0']); + // Update the selected lineage when necessary to prevent an item that + // doesn't exist from being "selected" internally. + if ($hierarchy->lineage[0] == 'label_0') { + $hierarchy->lineage[0] = 'none'; + } + } + + // Add one more level if appropriate. + $parent = $hierarchy->lineage[$max_depth]; + if (module_invoke($config['module'], 'hierarchical_select_valid_item', $parent, $config['params'])) { + $children = module_invoke($config['module'], 'hierarchical_select_children', $parent, $config['params']); + if (count($children)) { + // We're good, let's add one level! + $depth = $max_depth + 1; + + $hierarchy->levels[$depth] = array(); + + // Prepend a "" option to the current level when: + // - the editability setting is enabled, and + // - the hook is implemented (this is an optional hook), and + // - the logged in user has permission to edit terms in this vocabulary, and + // - the allowed_levels setting allows to create new items at this level. + if (!empty($config['editability']['status']) + && module_hook($config['module'], 'hierarchical_select_create_item') + && ($config['module'] == 'hs_taxonomy' && (user_access('administer taxonomy') || user_access('edit terms in ' . $config['params']['vid']))) + && _hierarchical_select_create_new_item_is_allowed($config, $depth) + ) { + $item_type = (count($config['editability']['item_types']) >= $depth) + ? t($config['editability']['item_types'][$depth]) + : t('item'); + $option = theme('hierarchical_select_special_option', array('option' => t('create new !item_type', array('!item_type' => $item_type)))); + $hierarchy->levels[$depth] = array('create_new_item' => $option); + } + + // Level label: set an empty level label if they've been disabled. + $hierarchy->lineage[$depth] = 'label_' . $depth; + $label = ($config['level_labels']['status']) ? t($config['level_labels']['labels'][$depth]) : ''; + $hierarchy->levels[$depth] = array('label_' . $depth => $label) + $hierarchy->levels[$depth] + $children; + + $hierarchy->levels[$depth] = _hierarchical_select_apply_entity_settings($hierarchy->levels[$depth], $config); + } + } + } + + // Add an extra level with only a level label and a "" + // option, if: + // - the editability setting is enabled + // - the allow_new_levels setting is enabled + // - an additional level is permitted by the max_levels setting + // - the logged in user has permission to edit terms in this vocabulary + // - the deepest item of the lineage is a valid item + // NOTE: this uses an optional hook, so we also check if it's implemented. + if (!empty($config['editability']['status']) + && !empty($config['editability']['allow_new_levels']) + && ($config['editability']['max_levels'] == 0 || count($hierarchy->lineage) < $config['editability']['max_levels']) + && module_invoke($config['module'], 'hierarchical_select_valid_item', end($hierarchy->lineage), $config['params']) + && ($config['module'] == 'hs_taxonomy' && (user_access('administer taxonomy') || user_access('edit terms in ' . $config['params']['vid']))) + && module_hook($config['module'], 'hierarchical_select_create_item') + ) { + $depth = $max_depth + 1; + + // Level label: set an empty level label if they've been disabled. + $hierarchy->lineage[$depth] = 'label_' . $depth; + $label = ($config['level_labels']['status']) ? t($config['level_labels']['labels'][$depth]) : ''; + + // Item type. + $item_type = (count($config['editability']['item_types']) >= $depth) + ? t($config['editability']['item_types'][$depth]) + : t('item'); + + // The new level with only a level label and a "" option. + $option = theme('hierarchical_select_special_option', array('option' => t('create new !item_type', array('!item_type' => $item_type)))); + $hierarchy->levels[$depth] = array( + 'label_' . $depth => $label, + 'create_new_item' => $option, + ); + } + + // Calculate the time it took to generate the levels. + $end_levels = microtime(); + + // Add child information. + $start_childinfo = microtime(); + $hierarchy = _hierarchical_select_hierarchy_add_childinfo($hierarchy, $config); + $end_childinfo = microtime(); + + // Calculate the time it took to build the hierarchy object. + $hierarchy->build_time['total'] = ($end_childinfo - $start_lineage) * 1000; + $hierarchy->build_time['lineage'] = ($end_lineage - $start_lineage) * 1000; + $hierarchy->build_time['levels'] = ($end_levels - $start_levels) * 1000; + $hierarchy->build_time['childinfo'] = ($end_childinfo - $start_childinfo) * 1000; + + return $hierarchy; +} + +/** + * Given a level, apply the entity_count and require_entity settings. + * + * @param $level + * A level in the hierarchy. + * @param $config + * A config array with at least the following settings: + * - module + * - params + * - entity_count + * - require_entity + * @return + * The updated level + */ +function _hierarchical_select_apply_entity_settings($level, $config) { + if (isset($config['special_items'])) { + $special_items['exclusive'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_exclusive')); + $special_items['none'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_none')); + } + + // Only do something when the entity_count or the require_entity (or both) + // settings are enabled. + // NOTE: this uses the optional "hierarchical_select_entity_count" hook, so + // we also check if it's implemented. + if (isset($config['entity_count']['enabled']) && ($config['entity_count']['enabled'] || $config['entity_count']['require_entity']) && module_hook($config['module'], 'hierarchical_select_entity_count')) { + foreach ($level as $item => $label) { + // We don't want to alter internal or special items. + if (!preg_match('/(none|label_\d+|create_new_item)/', $item) + && !in_array($item, $special_items['exclusive']) + && !in_array($item, $special_items['none']) + ) { + // Add our entity count settings to the parameters. + $config['params'] += array( + 'entity_count' => array( + 'settings' => array( + 'count_children' => $config['entity_count']['settings']['count_children'], + 'entity_types' => $config['entity_count']['settings']['entity_types'], + ), + ), + ); + $entity_count = module_invoke($config['module'], 'hierarchical_select_entity_count', $item, $config['params']); + + // When the require_entity setting is enabled and the entity count is + // zero, then remove the item from the level. + // When the item is not removed from the level due to the above and + // the entity_count setting is enabled, update the label of the item + // to include the entity count. + if ($config['entity_count']['require_entity'] && $entity_count == 0) { + unset($level[$item]); + } + elseif ($config['entity_count']['enabled']) { + $level[$item] = "$label ($entity_count)"; + } + } + } + } + + return $level; +} + +/** + * Extends a hierarchy object with child information: for each item in the + * hierarchy, the child count will be retrieved and stored in the hierarchy + * object, in the "childinfo" property. Items are grouped per level. + * + * @param $hierarchy + * A hierarchy object with the "levels" property set. + * @param $config + * A config array with at least the following settings: + * - module + * - params + * @return + * An updated hierarchy object with the "childinfo" property set. + */ +function _hierarchical_select_hierarchy_add_childinfo($hierarchy, $config) { + foreach ($hierarchy->levels as $depth => $level) { + foreach (array_keys($level) as $item) { + if (!preg_match('/(none|label_\d+|create_new_item)/', $item)) { + $hierarchy->childinfo[$depth][$item] = count(module_invoke($config['module'], 'hierarchical_select_children', $item, $config['params'])); + } + } + } + + return $hierarchy; +} + +/** + * Reset the selection if no valid item was selected. The first item in the + * array corresponds to the first selected term. As soon as an invalid item + * is encountered, the lineage from that level to the deeper levels should be + * unset. This is so to ignore selection of a level label. + * + * @param $selection + * Either a single item id or an array of item ids. + * @param $module + * The module that should be used for HS hooks. + * @param $params + * The module that should be passed to HS hooks. + * @return + * The updated selection. + */ +function _hierarchical_select_hierarchy_validate($selection, $module, $params) { + $valid = TRUE; + $selection_levels = count($selection); + for ($i = 0; $i < $selection_levels; $i++) { + // As soon as one invalid item has been found, we'll stop validating; all + // subsequently selected items will be removed from the selection. + if ($valid) { + $valid = module_invoke($module, 'hierarchical_select_valid_item', $selection[$i], $params); + if ($i > 0) { + $parent = $selection[$i - 1]; + $child = $selection[$i]; + $children = array_keys(module_invoke($module, 'hierarchical_select_children', $parent, $params)); + $valid = $valid && in_array($child, $children); + } + } + if (!$valid) { + unset($selection[$i]); + } + } + + if (empty($selection)) { + $selection = -1; + } + if (is_array($selection)) { + // This is needed because we may have unset some values and we don't want + // any gaps in the indexes (ie. the indexes would be 0,1,3 if we did + // "$selection[] = X" after unsetting #2). + $selection = array_values($selection); + } + + return $selection; +} + +/** + * Helper function to update the lineage of the hierarchy to ensure that the + * user selects an item in the deepest level of the hierarchy. + * + * @param $lineage + * The lineage up to the deepest selection the user has made so far. + * @param $module + * The module that should be used for HS hooks. + * @param $params + * The params that should be passed to HS hooks. + * @return + * The updated lineage. + */ +function _hierarchical_select_hierarchy_enforce_deepest($lineage, $module, $params) { + // Use the deepest item as the first parent. Then apply this algorithm: + // 1) get the parent's children, stop if no children + // 2) choose the first child as the option that is selected by default, by + // adding it to the lineage of the hierarchy + // 3) make this child the parent, go to step 1. + $parent = end($lineage); // The last item in the lineage is the deepest one. + $children = module_invoke($module, 'hierarchical_select_children', $parent, $params); + while (count($children)) { + $keys = array_keys($children); + $first_child = $keys[0]; + $lineage[] = $first_child; + $parent = $first_child; + $children = module_invoke($module, 'hierarchical_select_children', $parent, $params); + } + + return $lineage; +} + + +//---------------------------------------------------------------------------- +// Dropbox object generation functions. + +/** + * Generate the dropbox object. + * + * @param $config + * A config array with at least the following settings: + * - module + * - save_lineage + * - params + * - dropbox + * - title + * @param $selection + * The selection based on which a dropbox should be generated. + * @return + * A dropbox object. + */ +function _hierarchical_select_dropbox_generate($config, $selection) { + $dropbox = new stdClass(); + $start = microtime(); + + $dropbox->title = (!empty($config['dropbox']['title'])) ? filter_xss_admin($config['dropbox']['title']) : t('All selections'); + $dropbox->lineages = array(); + $dropbox->lineages_selections = array(); + + // Clean selection. + foreach ($selection as $key => $item) { + if (!module_invoke($config['module'], 'hierarchical_select_valid_item', $item, $config['params'])) { + unset($selection[$key]); + } + } + + if (!empty($selection)) { + // Store the "save lineage" setting, needed in the rendering layer. + $dropbox->save_lineage = $config['save_lineage']; + if ($config['save_lineage']) { + $dropbox->lineages = _hierarchical_select_dropbox_reconstruct_lineages_save_lineage_enabled($config['module'], $selection, $config['params']); + } + else { + // Retrieve the lineage of each item. + foreach ($selection as $item) { + $dropbox->lineages[] = module_invoke($config['module'], 'hierarchical_select_lineage', $item, $config['params']); + } + + // We will also need the labels of each item in the rendering layer. + foreach ($dropbox->lineages as $id => $lineage) { + foreach ($lineage as $level => $item) { + $dropbox->lineages[$id][$level] = array('value' => $item, 'label' => module_invoke($config['module'], 'hierarchical_select_item_get_label', $item, $config['params'])); + } + } + } + + // Sanitize the labels. + foreach ($dropbox->lineages as $id => $lineage) { + foreach ($lineage as $level => $item) { + $dropbox->lineages[$id][$level]['label'] = check_plain($dropbox->lineages[$id][$level]['label']); + } + } + + if (!isset($config['dropbox']['sort']) || $config['dropbox']['sort']){ + usort($dropbox->lineages, '_hierarchical_select_dropbox_sort'); + } + + // Now store each lineage's selection too. This is needed on the client side + // to enable the remove button to let the server know which selected items + // should be removed. + foreach ($dropbox->lineages as $id => $lineage) { + if ($config['save_lineage']) { + // Store the entire lineage. + $dropbox->lineages_selections[$id] = array_map('_hierarchical_select_dropbox_lineage_item_get_value', $lineage); + } + else { + // Store only the last (aka the deepest) value of the lineage. + $dropbox->lineages_selections[$id][0] = $lineage[count($lineage) - 1]['value']; + } + } + } + + // Calculate the time it took to build the dropbox object. + $dropbox->build_time = (microtime() - $start) * 1000; + + return $dropbox; +} + +/** + * Helper function to reconstruct the lineages given a set of selected items + * and the fact that the "save lineage" setting is enabled. + * + * Note that it's impossible to predict how many lineages if we know the + * number of selected items, exactly because the "save lineage" setting is + * enabled. + * + * Worst case time complexity is O(n^3), optimizations are still possible. + * + * @param $module + * The module that should be used for HS hooks. + * @param $selection + * The selection based on which a dropbox should be generated. + * @param $params + * Optional. An array of parameters, which may be necessary for some + * implementations. + * @return + * An array of dropbox lineages. + */ +function _hierarchical_select_dropbox_reconstruct_lineages_save_lineage_enabled($module, $selection, $params) { + // We have to reconstruct all lineages from the given set of selected items. + // That means: we have to reconstruct every possible combination! + $lineages = array(); + $root_level = module_invoke($module, 'hierarchical_select_root_level', $params); + + foreach ($selection as $key => $item) { + // Create new lineage if the item can be found in the root level. + if (array_key_exists($item, $root_level)) { + $lineages[][0] = array('value' => $item, 'label' => $root_level[$item]); + unset($selection[$key]); + } + } + + // Keep on trying as long as at least one lineage has been extended. + $at_least_one = TRUE; + for ($level = 0; $at_least_one; $level++) { + $at_least_one = FALSE; + $num = count($lineages); + + // Try to extend every lineage. Make sure we don't iterate over + // possibly new lineages. + for ($id = 0; $id < $num; $id++) { + // Only try to extend a lineage if it has an item at the current level. + if (!isset($lineages[$id][$level])) { + continue; + } + $children = module_invoke($module, 'hierarchical_select_children', $lineages[$id][$level]['value'], $params); + + $child_added_to_lineage = FALSE; + foreach (array_keys($children) as $child) { + if (in_array($child, $selection)) { + if (!$child_added_to_lineage) { + // Add the child to the lineage. + $lineages[$id][$level + 1] = array('value' => $child, 'label' => $children[$child]); + $child_added_to_lineage = TRUE; + $at_least_one = TRUE; + } + else { + // Create new lineage based on current one and add the child. + $lineage = $lineages[$id]; + $lineage[$level + 1] = array('value' => $child, 'label' => $children[$child]); + + // Add the new lineage to the set of lineages + $lineages[] = $lineage; + } + } + } + } + } + + return $lineages; +} + +/** + * Dropbox lineages sorting callback. + * + * @param $lineage_a + * The first lineage. + * @param $lineage_b + * The second lineage. + * @return + * An integer that determines which of the two lineages comes first. + */ +function _hierarchical_select_dropbox_sort($lineage_a, $lineage_b) { + $string_a = implode('', array_map('_hierarchical_select_dropbox_lineage_item_get_label', $lineage_a)); + $string_b = implode('', array_map('_hierarchical_select_dropbox_lineage_item_get_label', $lineage_b)); + return strcmp($string_a, $string_b); +} + +/** + * Helper function needed for the array_map() call in the dropbox sorting + * callback. + * + * @param $item + * An item in a dropbox lineage. + * @return + * The value associated with the "label" key of the item. + */ +function _hierarchical_select_dropbox_lineage_item_get_label($item) { + return t($item['label']); +} + +/** + * Helper function needed for the array_map() call in the dropbox lineages + * selections creation. + * + * @param $item + * An item in a dropbox lineage. + * @return + * The value associated with the "value" key of the item. + */ +function _hierarchical_select_dropbox_lineage_item_get_value($item) { + return $item['value']; +} + +/** + * Smarter version of array_merge_recursive: overwrites scalar values. + * + * From: http://www.php.net/manual/en/function.array-merge-recursive.php#82976. + */ +if (!function_exists('array_smart_merge')) { + function array_smart_merge($array, $override) { + if (is_array($array) && is_array($override)) { + foreach ($override as $k => $v) { + if (isset($array[$k]) && is_array($v) && is_array($array[$k])) { + $array[$k] = array_smart_merge($array[$k], $v); + } + else { + $array[$k] = $v; + } + } + } + return $array; + } +} + +/** + * Helper function needed for the array_filter() call to filter the items + * marked with the 'exclusive' property + * + * @param $item + * An item in the 'special_items' setting. + * @return + * TRUE if it's marked with the 'exclusive' property, FALSE otherwise. + */ +function _hierarchical_select_special_item_exclusive($item) { + return in_array('exclusive', $item); +} + +/** + * Helper function needed for the array_filter() call to filter the items + * marked with the 'none' property + * + * @param $item + * An item in the 'special_items' setting. + * @return + * TRUE if it's marked with the 'none' property, FALSE otherwise. + */ +function _hierarchical_select_special_item_none($item) { + return in_array('none', $item); +} diff --git a/sites/all/modules/hierarchical_select/hierarchical_select_cache.js b/sites/all/modules/hierarchical_select/hierarchical_select_cache.js new file mode 100644 index 0000000..dbac146 --- /dev/null +++ b/sites/all/modules/hierarchical_select/hierarchical_select_cache.js @@ -0,0 +1,229 @@ + +/** + * @file + * Cache system for Hierarchical Select. + * This cache system takes advantage of the HTML 5 client-side database + * storage specification to reduce the number of queries to the server. A lazy + * loading strategy is used. + */ + + +/** + * Note: this cache system can be replaced by another one, as long as you + * provide the following methods: + * - initialize() + * - status() + * - load() + * - sync() + * - updateHierarchicalSelect() + * + * TODO: better documentation + */ + +(function ($) { + +Drupal.HierarchicalSelect.cache = {}; + +Drupal.HierarchicalSelect.cache.initialize = function() { + try { + if (window.openDatabase) { + this.db = openDatabase("Hierarchical Select", "3.x", "Hierarchical Select cache", 200000); + + this.db + // Create the housekeeping table if it doesn't exist yet. + .transaction(function(tx) { + tx.executeSql("SELECT COUNT(*) FROM hierarchical_select", [], null, function(tx, error) { + tx.executeSql("CREATE TABLE hierarchical_select (table_name TEXT UNIQUE, expires REAL)", []); + console.log("Created housekeeping table."); + }); + }) + // Empty tables that have expired, based on the information in the + // housekeeping table. + .transaction(function(tx) { + tx.executeSql("SELECT table_name FROM hierarchical_select WHERE expires < ?", [ new Date().getTime() ], function(tx, resultSet) { + for (var i = 0; i < resultSet.rows.length; i++) { + var row = resultSet.rows.item(i); + var newExpiresTimestamp = new Date().getTime() + 86400; + + tx.executeSql("DELETE * FROM " + row.table_name); + tx.executeSql("UPDATE hierarchical_select SET expires = ? WHERE table_name = ?", [ newExpiresTimestamp, row.table_name ]); + + console.log("Table "+ row.table_name +" was expired: emptied it. Will expire again in "+ (newExpiresTimestamp - new Date().getTime()) / 3600 +" hours."); + } + }); + }); + } + else { + this.db = false; + } + } + catch(err) { } +}; + +Drupal.HierarchicalSelect.cache.status = function() { + return Drupal.HierarchicalSelect.cache.db !== false; +}; + +Drupal.HierarchicalSelect.cache.table = function(hsid) { + return Drupal.settings.HierarchicalSelect.settings[hsid].cacheId; +}; + +Drupal.HierarchicalSelect.cache.load = function(hsid) { + // If necessary, create the cache table for the given Hierarchical Select. + Drupal.HierarchicalSelect.cache.db.transaction(function(tx) { + var table = Drupal.HierarchicalSelect.cache.table(hsid); + + tx.executeSql("SELECT value FROM "+ table, [], function(tx, resultSet) { + console.log("" + resultSet.rows.length + " cached items in the " + table + " table."); + }, function(tx, error) { + var expiresTimestamp = new Date().getTime() + 86400; + + tx.executeSql("CREATE TABLE "+ table +" (parent REAL, value REAL UNIQUE, label REAL, weight REAL)"); + tx.executeSql("INSERT INTO hierarchical_select (table_name, expires) VALUES (?, ?)", [ table, expiresTimestamp ]); + + console.log("Created table "+ table +", will expire in "+ (expiresTimestamp - new Date().getTime()) / 3600 +" hours."); + }); + }); +}; + +Drupal.HierarchicalSelect.cache.insertOnDuplicateKeyUpdate = function(table, row) { +// console.log("storing: value: "+ row.value +", label: "+ row.label +", parent: "+ row.parent +", weight: "+ row.weight); + Drupal.HierarchicalSelect.cache.db.transaction(function(tx) { + tx.executeSql("INSERT INTO "+ table +" (parent, value, label, weight) VALUES (?, ?, ?, ?)", [ row.parent, row.value, row.label, row.weight ], null, function(tx, error) { +// console.log("UPDATING value: "+ row.value +", label: "+ row.label +", parent: "+ row.parent +", weight: "+ row.weight); + tx.executeSql("UPDATE "+ table +" SET parent = ?, label = ?, weight = ? WHERE value = ?", [ row.parent, row.label, row.weight, row.value ], null, function(tx, error) { +// console.log("sql error: " + error.message); + }); + }); + }); +}; + +Drupal.HierarchicalSelect.cache.sync = function(hsid, info) { + var table = Drupal.HierarchicalSelect.cache.table(hsid); + for (var id in info) { + var closure = function(_info, id) { + Drupal.HierarchicalSelect.cache.insertOnDuplicateKeyUpdate(table, _info[id]); + } (info, id); + } +}; + +Drupal.HierarchicalSelect.cache.hasChildren = function(hsid, value, successCallback, failCallback) { + var table = Drupal.HierarchicalSelect.cache.table(hsid); + Drupal.HierarchicalSelect.cache.db.transaction(function(tx) { + tx.executeSql("SELECT * FROM "+ table +" WHERE parent = ?", [ value ], function(tx, resultSet) { + if (resultSet.rows.length > 0) { + successCallback(); + } + else { + failCallback(); + } + }); + }); +}; + +Drupal.HierarchicalSelect.cache.getSubLevels = function(hsid, value, callback, previousSubLevels) { + var table = Drupal.HierarchicalSelect.cache.table(hsid); + + var subLevels = new Array(); + if (previousSubLevels != undefined) { + subLevels = previousSubLevels; + } + + Drupal.HierarchicalSelect.cache.db.transaction(function(tx) { + tx.executeSql("SELECT value, label FROM "+ table +" WHERE parent = ? ORDER BY weight", [ value ], function(tx, resultSet) { + var numChildren = resultSet.rows.length; + + // If there's only one child, check if it has the dummy "-has-no-children" value. + if (numChildren == 1) { + var valueOfFirstRow = String(resultSet.rows.item(0).value); + var isDummy = valueOfFirstRow.match(/^.*-has-no-children$/); + } + + // Only pass the children if there are any (and not a fake one either). + if (numChildren && !isDummy) { + var level = new Array(); + for (var i = 0; i < resultSet.rows.length; i++) { + var row = resultSet.rows.item(i); + level[i] = { 'value' : row.value, 'label' : row.label }; + console.log("child of "+ value +": ("+ row.value +", "+ row.label +")"); + } + + subLevels.push(level); + + Drupal.HierarchicalSelect.cache.getSubLevels(hsid, level[0].value, callback, subLevels); + } + else { + if (subLevels.length > 0) { + callback(subLevels); + } + else { + callback(false); + } + } + }); + }); +}; + +Drupal.HierarchicalSelect.cache.createAndUpdateSelects = function(hsid, subLevels, lastUnchanged) { + // Remove all levels below the level in which a value was selected, if they + // exist. + // Note: the root level can never change because of this! + $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .selects select').slice(lastUnchanged).remove(); + + // Create the new sublevels, by cloning the root level and then modifying + // that clone. + var $rootSelect = $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .selects select:first'); + for (var depth in subLevels) { + var optionElements = $.map(subLevels[depth], function(item) { return ''; }); + + var level = parseInt(lastUnchanged) + parseInt(depth); + + $('#hierarchical-select-'+ hsid +'-wrapper .hierarchical-select .selects select:last').after( + $rootSelect.clone() + // Update the name attribute. + .attr('name', $rootSelect.attr('name').replace(/(.*)\d+\]$/, "$1"+ level +"]")) + // Update the id attribute. + .attr('id', $rootSelect.attr('id').replace(/(.*-hierarchical-select-selects-)\d+/, "$1"+ level)) + // Remove the existing options and set the new ones. + .empty().append(optionElements.join('')) + ); + } +}; + +Drupal.HierarchicalSelect.cache.updateHierarchicalSelect = function(hsid, value, settings, lastUnchanged, ajaxOptions) { + // If the selected value has children + Drupal.HierarchicalSelect.cache.hasChildren(hsid, value, function() { + console.log("Cache hit."); + Drupal.HierarchicalSelect.cache.getSubLevels(hsid, value, function(subLevels) { + Drupal.HierarchicalSelect.preUpdateAnimations(hsid, 'update-hierarchical-select', lastUnchanged, function() { + if (subLevels !== false) { + Drupal.HierarchicalSelect.cache.createAndUpdateSelects(hsid, subLevels, lastUnchanged); + } + else { + // Nothing must happen: the user selected a value that doesn't + // have any subLevels. + $('#hierarchical-select-' + hsid + '-wrapper .hierarchical-select .selects select').slice(lastUnchanged).remove(); + } + + Drupal.HierarchicalSelect.postUpdateAnimations(hsid, 'update-hierarchical-select', lastUnchanged, function() { + // Reattach the bindings. + Drupal.HierarchicalSelect.attachBindings(hsid); + + Drupal.HierarchicalSelect.triggerEvents(hsid, 'update-hierarchical-select', settings); + + // The selection of this hierarchical select has changed! + Drupal.HierarchicalSelect.triggerEvents(hsid, 'change-hierarchical-select', settings); + }); + }); + }); + }, function() { + // This item was not yet requested before, so we still have to perform + // the dynamic form submit. + console.log("Cache miss. Querying the server."); + Drupal.HierarchicalSelect.preUpdateAnimations(hsid, 'update-hierarchical-select', lastUnchanged, function() { + $.ajax(ajaxOptions); + }); + }); +}; + +})(jQuery); diff --git a/sites/all/modules/hierarchical_select/hierarchical_select_formtoarray.js b/sites/all/modules/hierarchical_select/hierarchical_select_formtoarray.js new file mode 100644 index 0000000..62af3ec --- /dev/null +++ b/sites/all/modules/hierarchical_select/hierarchical_select_formtoarray.js @@ -0,0 +1,95 @@ + +/** + * @file + * Contains the formToArray method and the method it depends on. Taken from + * jQuery Form Plugin 2.12. (http://www.malsup.com/jquery/form/) + */ + +(function ($) { + +/** + * formToArray() gathers form element data into an array of objects that can + * be passed to any of the following ajax functions: $.get, $.post, or load. + * Each object in the array has both a 'name' and 'value' property. An example of + * an array for a simple login form might be: + * + * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ] + * + * It is this array that is passed to pre-submit callback functions provided to the + * ajaxSubmit() and ajaxForm() methods. + */ +$.fn.formToArray = function(semantic) { + var a = []; + if (this.length == 0) return a; + + var form = this[0]; + var els = semantic ? form.getElementsByTagName('*') : form.elements; + if (!els) return a; + for(var i=0, max=els.length; i < max; i++) { + var el = els[i]; + var n = el.name; + if (!n) continue; + + if (semantic && form.clk && el.type == "image") { + // handle image inputs on the fly when semantic == true + if(!el.disabled && form.clk == el) + a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y}); + continue; + } + + var v = $.fieldValue(el, true); + if (v && v.constructor == Array) { + for(var j=0, jmax=v.length; j < jmax; j++) + a.push({name: n, value: v[j]}); + } + else if (v !== null && typeof v != 'undefined') + a.push({name: n, value: v}); + } + + if (!semantic && form.clk) { + // input type=='image' are not found in elements array! handle them here + var inputs = form.getElementsByTagName("input"); + for(var i=0, max=inputs.length; i < max; i++) { + var input = inputs[i]; + var n = input.name; + if(n && !input.disabled && input.type == "image" && form.clk == input) + a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y}); + } + } + return a; +}; + +/** + * Returns the value of the field element. + */ +$.fieldValue = function(el, successful) { + var n = el.name, t = el.type, tag = el.tagName.toLowerCase(); + if (typeof successful == 'undefined') successful = true; + + if (successful && (!n || el.disabled || t == 'reset' || t == 'button' || + (t == 'checkbox' || t == 'radio') && !el.checked || + (t == 'submit' || t == 'image') && el.form && el.form.clk != el || + tag == 'select' && el.selectedIndex == -1)) + return null; + + if (tag == 'select') { + var index = el.selectedIndex; + if (index < 0) return null; + var a = [], ops = el.options; + var one = (t == 'select-one'); + var max = (one ? index+1 : ops.length); + for(var i=(one ? index : 0); i < max; i++) { + var op = ops[i]; + if (op.selected) { + // extra pain for IE... + var v = $.browser.msie && !(op.attributes['value'].specified) ? op.text : op.value; + if (one) return v; + a.push(v); + } + } + return a; + } + return el.value; +}; + +})(jQuery); diff --git a/sites/all/modules/hierarchical_select/images/arrow-rtl.png b/sites/all/modules/hierarchical_select/images/arrow-rtl.png new file mode 100644 index 0000000..2908c38 Binary files /dev/null and b/sites/all/modules/hierarchical_select/images/arrow-rtl.png differ diff --git a/sites/all/modules/hierarchical_select/images/arrow.png b/sites/all/modules/hierarchical_select/images/arrow.png new file mode 100644 index 0000000..cb901e2 Binary files /dev/null and b/sites/all/modules/hierarchical_select/images/arrow.png differ diff --git a/sites/all/modules/hierarchical_select/images/grippie.png b/sites/all/modules/hierarchical_select/images/grippie.png new file mode 100644 index 0000000..6524d41 Binary files /dev/null and b/sites/all/modules/hierarchical_select/images/grippie.png differ diff --git a/sites/all/modules/hierarchical_select/includes/common.inc b/sites/all/modules/hierarchical_select/includes/common.inc new file mode 100644 index 0000000..151873f --- /dev/null +++ b/sites/all/modules/hierarchical_select/includes/common.inc @@ -0,0 +1,441 @@ + $strings['item'], + '!items' => $strings['items'], + '!entity' => $strings['entity'], + '!entities' => $strings['entities'], + '!hierarchy' => $strings['hierarchy'], + '!hierarchies' => $strings['hierarchies'] + ); + + $form = array( + '#tree' => TRUE, + '#type' => 'fieldset', + '#title' => t('Hierarchical Select configuration'), + '#attributes' => array( + 'class' => array('hierarchical-select-config-form'), + 'id' => 'hierarchical-select-config-form-' . $config_id, + ), + '#attached' => array( + 'css' => array( + drupal_get_path('module', 'hierarchical_select') . '/includes/common_config_form.css' + ), + 'js' => array( + array( + 'type' => 'file', + 'data' => drupal_get_path('module', 'hierarchical_select') . '/includes/common_config_form.js', + ), + array( + 'type' => 'setting', + 'data' => array('HierarchicalSelect' => array('configForm' => array($config_id))), + ), + ), + ) + ); + + $form['config_id'] = array('#type' => 'value', '#value' => $config_id); + + // TODO: really make this a *live* preview, i.e. refresh the preview on each + // change in the form. This cannot be done easily in Drupal 5 or 6, so let's + // do so in Drupal 7. See cfg.livePreview in common_config_form.js. + $form['live_preview'] = array( + '#type' => 'fieldset', + '#title' => t('Preview'), + '#description' => t('This is what the Hierarchical Select will look like with your current configuration.'), + '#collapsible' => FALSE, + '#attributes' => array('class' => array('live-preview')), + ); + $form['live_preview']['example'] = array( + '#type' => 'hierarchical_select', + '#required' => $preview_is_required, + '#title' => t('Preview'), + '#description' => t('The description.'), + // Skip al validation for this form element: the data collected through it + // is always discarded, it's merely here for illustrative purposes. + '#validated' => TRUE, + ); + hierarchical_select_common_config_apply($form['live_preview']['example'], $config_id, array_merge($defaults_override, array('module' => $module, 'params' => $params))); + + $form['save_lineage'] = array( + '#type' => 'radios', + '#title' => t('Save lineage'), + '#options' => array( + 1 => t('Save !item lineage', $args), + 0 => t('Save only the deepest !item', $args), + ), + '#default_value' => (isset($config['save_lineage'])) ? $config['save_lineage'] : NULL, + '#description' => t( + 'Saving the !item lineage means saving the the !item itself and all + its ancestors.', + $args + ), + ); + + $form['enforce_deepest'] = array( + '#type' => 'radios', + '#title' => t('Level choice'), + '#options' => array( + 1 => t('Force the user to choose a !item from a deepest level', $args), + 0 => t('Allow the user to choose a !item from any level', $args), + ), + '#default_value' => (isset($config['enforce_deepest'])) ? $config['enforce_deepest'] : NULL, + '#description' => t( + 'This setting determines from which level in the !hierarchy tree a + user can select a !item.', + $args + ), + '#attributes' => array('class' => array('enforce-deepest')), + ); + + $form['resizable'] = array( + '#type' => 'radios', + '#title' => t('Resizable'), + '#description' => t( + "When enabled, a handle appears below the Hierarchical Select to allow + the user to dynamically resize it. Double clicking will toggle between + the smallest and a sane 'big size'." + ), + '#options' => array( + 0 => t('Disabled'), + 1 => t('Enabled'), + ), + '#default_value' => (isset($config['resizable'])) ? $config['resizable'] : NULL, + '#attributes' => array('class' => array('resizable')), + ); + + $form['level_labels'] = array( + '#tree' => TRUE, + '#type' => 'fieldset', + '#title' => t('Level labels'), + '#description' => t( + 'When the user is allowed to choose a !item from any level in the + Level choice setting, you can enter a label for each + level.
+ However, when the user is only allowed to choose a !item from the + deepest level, then you can only enter a label for the root + level.', + $args + ), + '#collapsible' => TRUE, + ); + $form['level_labels']['status'] = array( + '#type' => 'checkbox', + '#title' => t('Enable level labels'), + '#default_value' => (isset($config['level_labels']['status'])) ? $config['level_labels']['status'] : NULL, + '#attributes' => array('class' => array('level-labels-status')), + ); + for ($depth = 0; $depth <= $max_hierarchy_depth; $depth++) { + $form['level_labels']['labels'][$depth] = array( + '#type' => 'textfield', + '#size' => 20, + '#maxlength' => 255, + '#default_value' => (isset($config['level_labels']['labels'][$depth])) ? $config['level_labels']['labels'][$depth] : NULL, + '#attributes' => array('class' => array('level-label')), + ); + } + $form['level_labels']['#theme'] = 'hierarchical_select_common_config_form_level_labels'; + $form['level_labels']['#strings'] = $strings; + + $form['dropbox'] = array( + '#type' => 'fieldset', + '#title' => t('Dropbox settings'), + '#description' => t('The dropbox allows the user to make multiple selections.'), + '#collapsible' => TRUE, + ); + $form['dropbox']['status'] = array( + '#type' => 'checkbox', + '#title' => t('Enable the dropbox'), + '#default_value' => (isset($config['dropbox']['status'])) ? $config['dropbox']['status'] : NULL, + '#attributes' => array('class' => array('dropbox-status')), + ); + $form['dropbox']['title'] = array( + '#type' => 'textfield', + '#title' => t('Title'), + '#description' => t('The title you enter here appears above the dropbox.'), + '#size' => 20, + '#maxlength' => 255, + '#default_value' => (isset($config['dropbox']['title'])) ? $config['dropbox']['title'] : NULL, + '#attributes' => array('class' => array('dropbox-title')), + ); + $form['dropbox']['limit'] = array( + '#type' => 'textfield', + '#title' => t('Limit the number of selections'), + '#description' => t( + 'Limits the number of selections that can be added to the dropbox. + 0 means no limit. +
+ Note: the "Save !item lineage" option has no effect on this, even if + a lineage consists of 3 !items, this will count as only one selection + in the dropbox.', + $args + ), + '#size' => 5, + '#maxlength' => 5, + '#default_value' => (isset($config['dropbox']['limit'])) ? $config['dropbox']['limit'] : NULL, + '#attributes' => array('class' => array('dropbox-limit')), + ); + $form['dropbox']['reset_hs'] = array( + '#type' => 'radios', + '#title' => t('Reset selection of hierarchical select'), + '#description' => t( + 'This setting determines what will happen to the hierarchical select + when the user has added a selection to the dropbox.' + ), + '#options' => array( + 0 => t('Disabled'), + 1 => t('Enabled'), + ), + '#default_value' => (isset($config['dropbox']['reset_hs'])) ? $config['dropbox']['reset_hs'] : NULL, + '#attributes' => array('class' => array('dropbox-reset-hs')), + ); + $form['dropbox']['sort'] = array( + '#type' => 'checkbox', + '#title' => t('Sort dropbox items'), + '#description' => t('Automatically sort items added to the dropbox. If unchecked new items will be added to the end of the dropbox list.'), + '#default_value' => (isset($config['dropbox']['sort'])) ? $config['dropbox']['sort'] : 1, + '#attributes' => array('class' => array('dropbox-sort')), + ); + if (module_hook($module, 'hierarchical_select_create_item')) { + $form['editability'] = array( + '#type' => 'fieldset', + '#title' => t('Editability settings'), + '#description' => t( + 'You can allow the user to add new !items to this + !hierarchy through Hierarchical Select.', + $args + ), + '#collapsible' => TRUE, + ); + $form['editability']['status'] = array( + '#type' => 'checkbox', + '#title' => t('Allow creation of new !items', $args), + '#options' => array( + 0 => t('Disabled'), + 1 => t('Enabled'), + ), + '#default_value' => (isset($config['editability']['status'])) ? $config['editability']['status'] : NULL, + '#attributes' => array('class' => array('editability-status')), + ); + for ($depth = 0; $depth <= $max_hierarchy_depth; $depth++) { + $form['editability']['item_types'][$depth] = array( + '#type' => 'textfield', + '#size' => 20, + '#maxlength' => 255, + '#default_value' => (isset($config['editability']['item_types'][$depth])) ? $config['editability']['item_types'][$depth] : NULL, + '#attributes' => array('class' => array('editability-item-type')), + ); + } + for ($depth = 0; $depth <= $max_hierarchy_depth; $depth++) { + $form['editability']['allowed_levels'][$depth] = array( + '#type' => 'checkbox', + '#default_value' => (isset($config['editability']['allowed_levels'][$depth])) ? $config['editability']['allowed_levels'][$depth] : 1, + ); + } + $form['editability']['allow_new_levels'] = array( + '#type' => 'checkbox', + '#title' => t('Allow creation of new levels'), + '#default_value' => $config['editability']['allow_new_levels'], + '#description' => t( + 'Allow the user to create child !items for !items that do not yet have + children.', + $args + ), + '#attributes' => array('class' => array('editability-allow-new-levels')), + ); + $form['editability']['max_levels'] = array( + '#type' => 'select', + '#title' => t('Maximum number of levels allowed'), + '#options' => array( + 0 => t('0 (no limit)'), 1, 2, 3, 4, 5, 6, 7, 8, 9 + ), + '#default_value' => (isset($config['editability']['max_levels'])) ? $config['editability']['max_levels'] : NULL, + '#description' => t( + 'When the user is allowed to create new levels, this option prevents + the user from creating extremely deep !hierarchies.', + $args + ), + '#attributes' => array('class' => array('editability-max-levels')), + ); + + $form['editability']['#theme'] = 'hierarchical_select_common_config_form_editability'; + $form['editability']['#strings'] = $strings; + } + + if (module_hook($module, 'hierarchical_select_entity_count')) { + $form['entity_count'] = array( + '#type' => 'fieldset', + '#title' => t('Entity Count'), + '#collapsible' => TRUE, + ); + + $form['entity_count']['enabled'] = array( + '#type' => 'checkbox', + '#title' => t('Display number of entities'), + '#description' => t('Display the number of entities associated with the !item. Do not forget to check which entities should be counted.', $args), + '#default_value' => isset($config['entity_count']['enabled']) ? $config['entity_count']['enabled'] : FALSE, + '#weight' => -1, + '#attributes' => array('class' => array('entity-count-enabled')), + ); + + $form['entity_count']['require_entity'] = array( + '#type' => 'checkbox', + '#title' => t('Require associated entity'), + '#description' => t('If checked only !items that have at least one entity associated with them will be displayed.', $args), + '#default_value' => (isset($config['entity_count']['require_entity'])) ? $config['entity_count']['require_entity'] : FALSE, + ); + + $form['entity_count']['settings'] = array( + '#type' => 'fieldset', + '#title' => t('Entity count settings'), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + '#weight' => -1, + '#attributes' => array('class' => array('entity-count-settings')), + ); + + $form['entity_count']['settings']['count_children'] = array( + '#type' => 'checkbox', + '#title' => t('Also count children of !item.', $args), + '#description' => t('If checked this will result in a larger number because the children will be counted also.'), + '#default_value' => isset($config['entity_count']['settings']['count_children']) ? $config['entity_count']['settings']['count_children'] : FALSE, + ); + + $form['entity_count']['settings']['entity_types'] = array( + '#type' => 'item', + '#title' => t('Select entities that should be counted.'), + '#description' => t('Select entity type or one of it\'s bundles that should be counted'), + ); + + $entity_info = entity_get_info(); + foreach ($entity_info as $entity => $entity_info) { + if (!empty($entity_info['bundles']) && $entity_info['fieldable'] === TRUE) { + $options = array(); + $default_values = array(); + + $form['entity_count']['settings']['entity_types'][$entity] = array( + '#type' => 'fieldset', + '#title' => check_plain($entity_info['label']), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + + foreach ($entity_info['bundles'] as $bundle => $bundle_info) { + $options[$bundle] = check_plain($bundle_info['label']); + $default_values[$entity][$bundle] = isset($config['entity_count']['settings']['entity_types'][$entity]['count_' . $entity][$bundle]) ? $config['entity_count']['settings']['entity_types'][$entity]['count_' . $entity][$bundle] : 0; + } + + $form['entity_count']['settings']['entity_types'][$entity]['count_' . $entity] = array( + '#type' => 'checkboxes', + '#options' => $options, + '#default_value' => $default_values[$entity], + ); + } + } + } + + return $form; +} + +/** + * Submit callback for the hierarchical_select_common_config_form form. + */ +function hierarchical_select_common_config_form_submit($form, &$form_state) { + $config = _hierarchical_select_get_form_item_by_parents($form_state['values'], $form['#hs_common_config_form_parents']); + + // Don't include the value of the live preview in the config. + unset($config['live_preview']); + + hierarchical_select_common_config_set($config['config_id'], $config); +} + +/** + * Get the form element of a form that has a certain lineage of parents. + * + * @param $form + * A structured array for use in the Forms API. + * @param $parents + * An array of parent form element names. + * @return + * The form element that has the specified lineage of parents. + */ +function _hierarchical_select_get_form_item_by_parents($form, $parents) { + if (count($parents)) { + $parent = array_shift($parents); + return _hierarchical_select_get_form_item_by_parents($form[$parent], $parents); + } + else { + return $form; + } +} diff --git a/sites/all/modules/hierarchical_select/includes/common_config_form.css b/sites/all/modules/hierarchical_select/includes/common_config_form.css new file mode 100644 index 0000000..d72c277 --- /dev/null +++ b/sites/all/modules/hierarchical_select/includes/common_config_form.css @@ -0,0 +1,11 @@ + +.hierarchical-select-config-form .live-preview { + margin-left: auto; + margin-right: auto; + width: 25em; + float: right; +} + +.hierarchical-select-config-form fieldset { + clear: right; +} diff --git a/sites/all/modules/hierarchical_select/includes/common_config_form.js b/sites/all/modules/hierarchical_select/includes/common_config_form.js new file mode 100644 index 0000000..36233a6 --- /dev/null +++ b/sites/all/modules/hierarchical_select/includes/common_config_form.js @@ -0,0 +1,148 @@ + +Drupal.HierarchicalSelectConfigForm = {}; + +(function ($, cfg) { + +cfg.context = function(configId) { + if (configId === undefined) { + return $('.hierarchical-select-config-form > *').not('.live-preview'); + } + else { + return $('#hierarchical-select-config-form-'+ configId + ' > *').not('.live-preview'); + } +}; + +cfg.levelLabels = function(configId) { + var $status = $('.level-labels-status', cfg.context(configId)); + var $enforceDeepest = $('.enforce-deepest input', cfg.context(configId)); + + var showHide = function(speed) { + $affected = $('.level-labels-settings', cfg.context(configId)); + if (!$status.is(':checked')) { + $affected.hide(speed); + } + else { + // For showing/hiding rows, I'm relying on setting the style + // "display: none" and removing it again. jQuery's show()/hide() leave + // "display: block" behind and are thereby messing up the table layout. + if ($enforceDeepest.slice(1, 2).is(':checked')) { + $affected.find('tr').removeAttr('style'); + } + else { + // We need to take special measures if sticky headers are enabled, so + // handle the show/hide separately when it's enabled. + if ($affected.find('table.sticky-header').length == 0) { + $affected.find('tr').slice(0, 2).removeAttr('style'); // Show header tr and root level tr. + $affected.find('tr').slice(2).attr('style', 'display: none'); // Hide all other tr's. + } + else { + $affected.find('table').show(speed); // Show both tables (the one with the sticky headers and the one with the actual content). + $affected.find('table').slice(1).find('tr').slice(2).attr('style', 'display: none'); // Show all tr's after the header tr and root level tr of the 2nd table (the one with the actual content). + } + } + + // If $status was unchecked previously, the entire div would have been + // hidden! + if ($affected.css('display') == 'none') { + $affected.show(speed); + } + } + }; + + $status.click(function() { showHide(200); }); + $enforceDeepest.click(function() { showHide(200); }); + showHide(0); +}; + +cfg.dropbox = function(configId) { + var $status = $('.dropbox-status', cfg.context(configId)); + + var showHide = function(speed) { + var $affected = $('.dropbox-title, .dropbox-limit, .dropbox-reset-hs', cfg.context(configId)).parent(); + if ($status.is(':checked')) { + $affected.show(speed); + } + else { + $affected.hide(speed); + } + }; + + $status.click(function() { showHide(200); }); + showHide(0); +}; + +cfg.editability = function(configId) { + var $status = $('.editability-status', cfg.context(configId)); + var $allowNewLevels = $('.editability-allow-new-levels', cfg.context(configId)); + + var showHide = function(speed) { + var $affected = $('.editability-per-level-settings, .form-item:has(.editability-allow-new-levels)', cfg.context(configId)); + var $maxLevels = $('.form-item:has(.editability-max-levels)', cfg.context(configId)); + if ($status.is(':checked')) { + if ($allowNewLevels.is(':checked')) { + $affected.add($maxLevels).show(speed); + } + else { + $affected.show(speed); + } + } + else { + $affected.add($maxLevels).hide(speed); + } + }; + + var showHideMaxLevels = function(speed) { + $affected = $('.editability-max-levels', cfg.context(configId)).parent(); + if ($allowNewLevels.is(':checked')) { + $affected.show(speed); + } + else { + $affected.hide(speed); + } + }; + + $status.click(function() { showHide(200); }); + $allowNewLevels.click(function() { showHideMaxLevels(200); }); + showHideMaxLevels(0); + showHide(0); +}; + +cfg.entityCount = function(configId) { + var $status = $('.entity-count-enabled', cfg.context(configId)); + + var showHide = function(speed) { + var $affected = $('.entity-count-settings', cfg.context(configId)); + if ($status.is(':checked')) { + $affected.show(speed); + } + else { + $affected.hide(speed); + } + }; + + $status.click(function() { showHide(200); }); + showHide(0); +}; + +cfg.livePreview = function(configId) { + // React on changes to any input, except the ones in the live preview. + $updateLivePreview = $('input', cfg.context(configId)) + .filter(':not(.create-new-item-input):not(.create-new-item-create):not(.create-new-item-cancel)') + .change(function() { + // TODO: Do an AJAX submit of the entire form. + }); +}; + +$(document).ready(function() { + for (var id in Drupal.settings.HierarchicalSelect.configForm) { + var configId = Drupal.settings.HierarchicalSelect.configForm.id; + + cfg.levelLabels(configId); + cfg.dropbox(configId); + cfg.editability(configId); + cfg.entityCount(configId); + //cfg.livePreview(configId); + } +}); + +})(jQuery, Drupal.HierarchicalSelectConfigForm); diff --git a/sites/all/modules/hierarchical_select/includes/theme.inc b/sites/all/modules/hierarchical_select/includes/theme.inc new file mode 100644 index 0000000..7c37667 --- /dev/null +++ b/sites/all/modules/hierarchical_select/includes/theme.inc @@ -0,0 +1,413 @@ +\n"; + $required = !empty($element['#required']) ? '*' : ''; + + if (!empty($element['#title'])) { + $title = $element['#title']; + if (!empty($element['#id'])) { + $output .= ' \n"; + } + else { + $output .= ' \n"; + } + } + + $output .= " $value\n"; + + if (!empty($element['#description'])) { + $output .= '
' . $element['#description'] . "
\n"; + } + + $output .= "\n"; + + return $output; +} + +/** + * Format a hierarchical select. + * + * @param array $variables + * An associative array containing the properties of the element. + * @return string + * A themed HTML string representing the form element. + */ +function theme_hierarchical_select($variables) { + $element = $variables['element']; + $output = ''; + + // Update $element['#attributes']['class']. + if (!isset($element['#attributes']['class'])) { + $element['#attributes']['class'] = array(); + } + $hsid = $element['hsid']['#value']; + $level_labels_style = variable_get('hierarchical_select_level_labels_style', 'none'); + $classes = array( + 'hierarchical-select-wrapper', + "hierarchical-select-level-labels-style-$level_labels_style", + // Classes that make it possible to override the styling of specific + // instances of Hierarchical Select, based on either the ID of the form + // element or the config that it uses. + 'hierarchical-select-wrapper-for-name-' . $element['#id'], + (isset($element['#config']['config_id'])) ? 'hierarchical-select-wrapper-for-config-' . $element['#config']['config_id'] : NULL, + ); + $element['#attributes']['class'] = array_merge($element['#attributes']['class'], $classes); + $element['#attributes']['id'] = "hierarchical-select-$hsid-wrapper"; + $element['#id'] = "hierarchical-select-$hsid-wrapper"; // This ensures the label's for attribute is correct. + + return '
' . drupal_render_children($element) . '
'; +} + +/** + * Format the container for all selects in the hierarchical select. + * + * @param array $variables + * An associative array containing the properties of the element. + * @return string + * A themed HTML string representing the form element. + */ +function theme_hierarchical_select_selects_container($variables) { + $element = $variables['element']; + $output = ''; + $output .= '
'; + $output .= drupal_render_children($element); + $output .= '
'; + return $output; +} + +/** + * Format a select in the .hierarchial-select div: prevent it from being + * wrapped in a div. This simplifies the CSS and JS code. + * + * @param array $variables + * An associative array containing the properties of the element. + * @return string + * A themed HTML string representing the form element. + */ +function theme_hierarchical_select_select($variables) { + $element = $variables['element']; + element_set_attributes($element, array('id', 'name', 'size')); + _form_set_class($element, array('form-select')); + + return '' . _hierarchical_select_options($element) . ''; +} + +/** + * Format an item separator (for use in a lineage). + */ +function theme_hierarchical_select_item_separator($variables) { + $output = ''; + $output .= ''; + $output .= '›'; + $output .= ''; + return $output; +} + +/** + * Format a special option in a Hierarchical Select select. For example the + * "none" option or the "create new item" option. This theme function allows + * you to change how a special option is indicated textually. + * + * @param array $variables + * A special option. + * @return string + * A textually indicated special option. + */ +function theme_hierarchical_select_special_option($variables) { + $option = $variables['option']; + return '<' . $option . '>'; +} + +/** + * Forms API theming callback for the dropbox. Renders the dropbox as a table. + * + * @param array $variables + * An element for which the #theme property was set to this function. + * @return string + * A themed HTML string. + */ +function theme_hierarchical_select_dropbox_table($variables) { + $element = $variables['element']; + $output = ''; + + $class = 'dropbox'; + if (form_get_error($element) === '') { + $class .= ' error'; + } + + $title = $element['title']['#value']; + $separator = $element['separator']['#value']; + $is_empty = $element['is_empty']['#value']; + + $separator_html = '' . $separator . ''; + + $output .= '
'; + $output .= ''; + $output .= ''; + $output .= ''; + + if (!$is_empty) { + // Each lineage in the dropbox corresponds to an entry in the dropbox table. + $lineage_count = count(element_children($element['lineages'])); + for ($x = 0; $x < $lineage_count; $x++) { + $db_entry = $element['lineages']["lineage-$x"]; + $zebra = $db_entry['#zebra']; + $first = $db_entry['#first']; + $last = $db_entry['#last']; + // The deepest level is the number of child levels minus one. This "one" + // is the element for the "Remove" checkbox. + $deepest_level = count(element_children($db_entry)) - 1; + + $output .= ''; + $output .= ''; + $output .= ''; + $output .= ''; + } + } + else { + $output .= ''; + } + + $output .= ''; + $output .= '
' . $title . '
'; + // Each item in a lineage is separated by the separator string. + for ($depth = 0; $depth < $deepest_level; $depth++) { + $output .= drupal_render($db_entry[$depth]); + + if ($depth < $deepest_level - 1) { + $output .= $separator_html; + } + } + $output .= '' . drupal_render($db_entry['remove']) . '
'; + $output .= t('Nothing has been selected.'); + $output .= '
'; + $output .= '
'; + + return $output; +} + +/** + * Themeing function to render the level_labels settings as a table. + */ +// TODO: rename $form to $element for consistency (and update hook_theme() after that), make the comment consistent. +/** + * @todo Please document this function. + * @see http://drupal.org/node/1354 + */ +function theme_hierarchical_select_common_config_form_level_labels($variables) { + $form = $variables['form']; + // Recover the stored strings. + $strings = $form['#strings']; + + $output = ''; + $header = array(t('Level'), t('Label')); + $rows = array(); + + $output .= drupal_render($form['status']); + + $output .= '
'; + if (isset($form['labels']) && count(element_children($form['labels']))) { + foreach (element_children($form['labels']) as $depth) { + $row = array(); + $row[]['data'] = ($depth == 0) ? t('Root level') : t('Sublevel !depth', array('!depth' => $depth)); + $row[]['data'] = drupal_render($form['labels'][$depth]); + $rows[] = $row; + } + + // Render the table. + $output .= theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('style' => 'width: auto;'))); + } + else { + // No levels exist yet in the hierarchy! + $output .= '

'; + $output .= t('There are no levels yet in this !hierarchy!', array('!hierarchy' => $strings['hierarchy'])); + $output .= '

'; + } + $output .= '
'; + + // Render the remaining form items. + $output .= drupal_render_children($form); + + return $output; +} + +/** + * Themeing function to render the per-level editability settings as a table, + * (these are the item_types and allowed_levels settings). + */ +// TODO: rename $form to $element for consistency (and update hook_theme() after that), make the comment consistent. +/** + * @todo Please document this function. + * @see http://drupal.org/node/1354 + */ +function theme_hierarchical_select_common_config_form_editability($variables) { + $form = $variables['form']; + // Recover the stored strings. + $strings = $form['#strings']; + + $output = ''; + $header = array(t('Level'), t('Allow'), t('!item_type', array('!item_type' => drupal_ucfirst($strings['item_type'])))); + $rows = array(); + + $output .= drupal_render($form['status']); + + $output .= '
'; + if (isset($form['item_types']) && count(element_children($form['item_types']))) { + foreach (element_children($form['item_types']) as $depth) { + $row = array(); + $row[]['data'] = ($depth == 0) ? t('Root level') : t('Sublevel !depth', array('!depth' => $depth)); + $row[]['data'] = drupal_render($form['allowed_levels'][$depth]); + $row[]['data'] = drupal_render($form['item_types'][$depth]); + $rows[] = $row; + } + + // Render the table and description. + $output .= theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('style' => 'width: auto;'), 'caption' => '' . t('Per-level settings for creating new !items.', array('!items' => $strings['items'])))); + $output .= '
'; + $output .= t( + 'The %item_type you enter for each level is what will be used in + each level to replace a "<create new item>" option with a + "<create new %item_type>" option, which is often more + intuitive.', + array( + '%item_type' => $strings['item_type'], + ) + ); + $output .= '
'; + } + else { + // No levels exist yet in the hierarchy! + $output .= '

'; + $output .= t('There are no levels yet in this !hierarchy!', array('!hierarchy' => $strings['hierarchy'])); + $output .= '

'; + } + $output .= '
'; + + // Render the remaining form items. + $output .= drupal_render_children($form); + + return $output; +} + +/** + * Themeing function to render a selection (of items) according to a given + * Hierarchical Select configuration as one or more lineages. + * + * @param $selection + * A selection of items of a hierarchy. + * @param $config + * A config array with at least the following settings: + * - module + * - save_lineage + * - params + */ +function theme_hierarchical_select_selection_as_lineages($variables) { + $selection = $variables['selection']; + $config = $variables['config']; + $output = ''; + + $selection = (!is_array($selection)) ? array($selection) : $selection; + + // Generate a dropbox out of the selection. This will automatically + // calculate all lineages for us. + $selection = array_keys($selection); + $dropbox = _hierarchical_select_dropbox_generate($config, $selection); + + // Actual formatting. + foreach ($dropbox->lineages as $id => $lineage) { + if ($id > 0) { + $output .= '
'; + } + + $items = array(); + foreach ($lineage as $level => $item) { + $items[] = $item['label']; + } + $output .= implode('', $items); + } + + // Add the CSS. + drupal_add_css(drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.css'); + + return $output; +} + +/** + * @} End of "ingroup themeable". + */ + + +//---------------------------------------------------------------------------- +// Private functions. + +/** + * This is an altered clone of form_select_options(). The reason: I need to be + * able to set a class on an option element if it contains a level label, to + * allow for level label styles. + * TODO: rename to _hierarchical_select_select_options(). + */ +function _hierarchical_select_options($element) { + if (!isset($choices)) { + $choices = $element['#options']; + } + // array_key_exists() accommodates the rare event where $element['#value'] is NULL. + // isset() fails in this situation. + $value_valid = isset($element['#value']) || array_key_exists('#value', $element); + $value_is_array = isset($element['#value']) && is_array($element['#value']); + $options = ''; + foreach ($choices as $key => $choice) { + $key = (string) $key; + if ($value_valid && (!$value_is_array && (string) $element['#value'] === $key || ($value_is_array && in_array($key, $element['#value'])))) { + $selected = ' selected="selected"'; + } + else { + $selected = ''; + } + + // If an option DOES NOT have child info, then it's a special option: + // - label_\d+ (level label) + // - none ("") + // - create_new_item ("") + // Only when it's a level label, we have to add a class to this option. + if (!isset($element['#childinfo'][$key])) { + $class = (preg_match('/label_\d+/', $key)) ? ' level-label' : ''; + } + else { + $class = ($element['#childinfo'][$key] == 0) ? 'has-no-children' : 'has-children'; + } + + $options .= ''; + } + return $options; +} diff --git a/sites/all/modules/hierarchical_select/includes/views.js b/sites/all/modules/hierarchical_select/includes/views.js new file mode 100644 index 0000000..d5b5e3e --- /dev/null +++ b/sites/all/modules/hierarchical_select/includes/views.js @@ -0,0 +1,29 @@ + + +/** + * @file + * Make Hierarchical Select work in Views' exposed filters form. + * + * Views' exposed filters form is a GET form, but since Hierarchical Select + * really is a combination of various form items, this will result in a very + * ugly and unnecessarily long GET URL, which also breaks the exposed filters. + * This piece of JavaScript is a necessity to make it work again, but it will + * of course only work when JavaScript is enabled! + */ + + +if (Drupal.jsEnabled) { + $(document).ready(function(){ + $('.view-filters form').submit(function() { + // Remove the Hierarchical Select form build id and the form id, to + // prevent them from ending up in the GET URL. + $('#edit-hs-form-build-id').remove(); + + // Prepare the hierarchical select form elements that are used as + // exposed filters for a GET submit. + $('.view-filters form') + .find('.hierarchical-select-wrapper') + .trigger('prepare-GET-submit'); + }); + }); +} diff --git a/sites/all/modules/hierarchical_select/modules/hs_flatlist.info b/sites/all/modules/hierarchical_select/modules/hs_flatlist.info new file mode 100644 index 0000000..35e2f07 --- /dev/null +++ b/sites/all/modules/hierarchical_select/modules/hs_flatlist.info @@ -0,0 +1,12 @@ +name = Hierarchical Select Flat List +description = Allows Hierarchical Select's dropbox to be used for selecting multiple items in a flat list of options. +dependencies[] = hierarchical_select +package = Form Elements +core = 7.x + +; Information added by Drupal.org packaging script on 2016-06-03 +version = "7.x-3.0-beta7" +core = "7.x" +project = "hierarchical_select" +datestamp = "1464981541" + diff --git a/sites/all/modules/hierarchical_select/modules/hs_flatlist.module b/sites/all/modules/hierarchical_select/modules/hs_flatlist.module new file mode 100644 index 0000000..d2fa4de --- /dev/null +++ b/sites/all/modules/hierarchical_select/modules/hs_flatlist.module @@ -0,0 +1,66 @@ + t('None: flat list'), + 'entity type' => t('N/A'), + ); +} diff --git a/sites/all/modules/hierarchical_select/modules/hs_menu.info b/sites/all/modules/hierarchical_select/modules/hs_menu.info new file mode 100644 index 0000000..9de8489 --- /dev/null +++ b/sites/all/modules/hierarchical_select/modules/hs_menu.info @@ -0,0 +1,13 @@ +name = Hierarchical Select Menu +description = Use Hierarchical Select for menu parent selection. +dependencies[] = hierarchical_select +dependencies[] = menu +package = Form Elements +core = 7.x + +; Information added by Drupal.org packaging script on 2016-06-03 +version = "7.x-3.0-beta7" +core = "7.x" +project = "hierarchical_select" +datestamp = "1464981541" + diff --git a/sites/all/modules/hierarchical_select/modules/hs_menu.install b/sites/all/modules/hierarchical_select/modules/hs_menu.install new file mode 100644 index 0000000..3b4deb5 --- /dev/null +++ b/sites/all/modules/hierarchical_select/modules/hs_menu.install @@ -0,0 +1,27 @@ +fields(array('weight' => 1)) + ->condition('name', 'hs_menu') + ->execute(); +} + +/** + * Implementats hook_uninstall(). + */ +function hs_menu_uninstall() { + db_delete('variable') + ->condition('name', 'hs_menu_%', 'LIKE') + ->execute(); +} diff --git a/sites/all/modules/hierarchical_select/modules/hs_menu.module b/sites/all/modules/hierarchical_select/modules/hs_menu.module new file mode 100644 index 0000000..6cee6e8 --- /dev/null +++ b/sites/all/modules/hierarchical_select/modules/hs_menu.module @@ -0,0 +1,326 @@ + 'Menu', + 'description' => 'Hierarchical Select configuration for Menu', + 'access arguments' => array('administer site configuration'), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('hs_menu_admin_settings'), + 'type' => MENU_LOCAL_TASK, + ); + return $items; +} + +/** + * Implements hook_form_FORMID_alter(). + * + * Alter the node form's menu form. + */ +function hs_menu_form_node_form_alter(&$form, &$form_state) { + $active_types = array_filter(variable_get('hs_menu_content_types', array())); + $active = empty($active_types) || in_array($form_state['node']->type, $active_types); + if ($active && isset($form['menu']['link']['parent']) && isset($form['menu']['#access']) && $form['menu']['#access']) { + unset($form['menu']['link']['parent']['#options']); + $form['menu']['link']['parent']['#type'] = 'hierarchical_select'; + // Get menu name, needed to exclude current node. + $menu_name = explode(':', $form['menu']['link']['parent']['#default_value']); + _hs_menu_apply_config($form['menu']['link']['parent'], array( + 0 => $menu_name[0], + 1 => $form['menu']['link']['mlid']['#value'], + 'type' => $form['type']['#value'], + )); + + // Set custom submit callback. + array_unshift($form['#submit'], 'hs_menu_node_form_submit'); + // Change the loaded default value into an array so we can populate the + // Hierarchical Select element. + $form['menu']['link']['parent']['#default_value'] = array($form['menu']['link']['parent']['#default_value']); + } +} + +/** + * Implements hook_form_BASE_FORMID_alter(). + * + * Alter the widget type form; dynamically add the Hierarchical Select + * Configuration form when it is needed. + */ +function hs_menu_form_menu_edit_item_alter(&$form, &$form_state) { + unset($form['parent']['#options']); + $original_item = $form['original_item']['#value']; + $form['parent']['#type'] = 'hierarchical_select'; + _hs_menu_apply_config($form['parent'], array('exclude' => array( + $original_item['menu_name'], + $original_item['mlid'], + ))); + + // Set custom submit callback. + array_unshift($form['#submit'], 'hs_menu_menu_edit_item_form_submit'); +} + + +//---------------------------------------------------------------------------- +// Form API callbacks. + +/** + * Submit callback; menu edit item form. + */ +function hs_menu_menu_edit_item_form_submit(&$form, &$form_state) { + // Don't return an array, but a single item. + $form_state['values']['parent'] = $form_state['values']['parent'][0]; +} + +/** + * Submit callback; node edit form. + */ +function hs_menu_node_form_submit(&$form, &$form_state) { + // Don't return an array, but a single item. + $form_state['values']['menu']['parent'] = $form_state['values']['menu']['parent'][0]; +} + +//---------------------------------------------------------------------------- +// Menu callbacks. + +/** + * Form definition; admin settings. + */ +function hs_menu_admin_settings() { + $form['hs_menu_resizable'] = array( + '#type' => 'radios', + '#title' => t('Resizable'), + '#description' => t( + "When enabled, a handle appears below the Hierarchical Select to allow + the user to dynamically resize it. Double clicking will toggle between + the smallest and a sane 'big size'." + ), + '#options' => array( + 0 => t('Disabled'), + 1 => t('Enabled'), + ), + '#default_value' => variable_get('hs_menu_resizable', 1), + ); + + $form['hs_menu_content_types'] = array( + '#type' => 'checkboxes', + '#title' => t('Content types'), + '#description' => t("Select the content types to use Hierarchical Select Menu on. If no content types are selected, then it will apply to all content types."), + '#options' => node_type_get_names(), + '#default_value' => variable_get('hs_menu_content_types', array()), + ); + + return system_settings_form($form); +} + + +//---------------------------------------------------------------------------- +// Hierarchical Select hooks. + +/** + * Implements hook_hierarchical_select_params(). + */ +function hs_menu_hierarchical_select_params() { + $params = array( + 'exclude', // The menu_name and mlid (in an array) of a menu link that should be excluded from the hierarchy. + ); + return $params; +} + +/** + * Implements hook_hierarchical_select_root_level(). + */ +function hs_menu_hierarchical_select_root_level($params) { + $menus = array(); + + $result = db_query("SELECT menu_name, title FROM {menu_custom} ORDER BY title"); + // If the type is set, respect the core menu options setting. + if (isset($params['type'])) { + $type_menus = variable_get('menu_options_' . $params['type'], array('main-menu' => 'main-menu')); + while ($menu = $result->fetchObject()) { + if (in_array($menu->menu_name, $type_menus)) { + $menus[$menu->menu_name . ':0'] = $menu->title; + } + } + } + // Fall back to the legacy approach, show all menu's. + else { + while ($menu = $result->fetchObject()) { + $menus[$menu->menu_name . ':0'] = $menu->title; + } + } + + return $menus; +} + +/** + * Implements hook_hierarchical_select_children(). + */ +function hs_menu_hierarchical_select_children($parent, $params) { + $children = array(); + list($menu_name, $plid) = explode(':', $parent); + $tree = menu_tree_all_data($menu_name, NULL); + return _hs_menu_children($tree, $menu_name, $plid, $params['exclude']); +} + +/** + * Implements hook_hierarchical_select_lineage(). + */ +function hs_menu_hierarchical_select_lineage($item, $params) { + $lineage = array($item); + + list($menu_name, $mlid) = explode(':', $item); + + // If the initial mlid is zero, then this is the root level, so we don't + // have to get the lineage. + if ($mlid > 0) { + // Prepend each parent mlid (i.e. plid) to the lineage. + do { + $plid = db_query("SELECT plid FROM {menu_links} WHERE mlid = :mlid", array(':mlid' => $mlid))->fetchField(); + array_unshift($lineage, "$menu_name:$plid"); + if ($mlid == $plid) { + // Somehow we have an infinite loop situation. Bail out of the loop. + break; + } + $mlid = $plid; + } while ($plid > 0); + } + + return $lineage; +} + +/** + * Implements hook_hierarchical_select_valid_item(). + */ +function hs_menu_hierarchical_select_valid_item($item, $params) { + $parts = explode(':', $item); + + $valid = TRUE; + + // Validate menu name. + $valid = (array_key_exists($parts[0], menu_get_menus())); + + // Validate hierarchy of mlids. + for ($i = 1; $valid && $i < count($parts); $i++) { + $valid = $valid && is_numeric($parts[$i]); + } + + // Ensure that this isn't the excluded menu link. + $valid = $valid && $item != $params['exclude'][0] . $params['exclude'][1]; + + return $valid; +} + +/** + * Implements hook_hierarchical_select_item_get_label(). + */ +function hs_menu_hierarchical_select_item_get_label($item, $params) { + static $labels = array(); + + $parts = explode(':', $item); + if (count($parts) == 1) { // Get the menu name. + $menu_name = $parts[0]; + $labels[$item] = db_query("SELECT title FROM {menu_custom} WHERE menu_name = :menu_name", array(':menu_name' => $menu_name))->fetchField(); + } + else { // Get the menu link title. + $mlid = end($parts); + $menu_link = menu_link_load($mlid); + $labels[$item] = $menu_link['title']; + } + + return $labels[$item]; +} + +/** + * Implements hook_hierarchical_select_implementation_info(). + */ +function hs_menu_hierarchical_select_implementation_info() { + return array( + 'hierarchy type' => t('Menu'), + 'entity type' => t('N/A'), + ); +} + + +//---------------------------------------------------------------------------- +// Private functions. + +/** + * Recursive helper function for hs_menu_hierarchical_select_children(). + */ +function _hs_menu_children($tree, $menu_name, $plid = 0, $exclude = FALSE) { + $children = array(); + + foreach ($tree as $data) { + if ($data['link']['plid'] == $plid && $data['link']['hidden'] >= 0) { + if ($exclude && $data['link']['menu_name'] === $exclude[0] && $data['link']['mlid'] == $exclude[1]) { + continue; + } + + $title = truncate_utf8($data['link']['title'], 30, TRUE, FALSE); + if ($data['link']['hidden']) { + $title .= ' (' . t('disabled') . ')'; + } + $children[$menu_name . ':' . $data['link']['mlid']] = $title; + if ($data['below']) { + $children += _hs_menu_children($data['below'], $menu_name, $plid, $exclude); + } + } + elseif ($data['below']) { + $children += _hs_menu_children($data['below'], $menu_name, $plid, $exclude); + } + } + + return $children; +} + +/** + * Helper function to apply the HS config to a form item. + */ +function _hs_menu_apply_config(&$form, $params) { + // The following is to ensure via javascript self is not listed. + if (!empty($params['exclude'])) { + $params['exclude'] = $params['exclude'][0] .':'. $params['exclude'][1]; + drupal_add_js('jQuery(document).ready(function () { + jQuery("[value*=' . $params['exclude'] . ']").hide(); + });', 'inline'); + } + $form['#config'] = array( + 'module' => 'hs_menu', + 'params' => array( + 'exclude' => isset($params['exclude']) ? $params['exclude'] : NULL, + 'type' => isset($params['type']) ? $params['type'] : NULL, + ), + 'save_lineage' => 0, + 'enforce_deepest' => 0, + 'resizable' => variable_get('hs_menu_resizable', 1), + 'level_labels' => array( + 'status' => 0, + ), + 'dropbox' => array( + 'status' => 0, + ), + 'editability' => array( + 'status' => 0, + ), + 'entity_count' => array( + 'enabled' => 0, + 'require_entity' => 0, + 'settings' => array( + 'count_children' => 0, + 'entity_types' => array(), + ), + ), + 'render_flat_select' => 0, + ); +} diff --git a/sites/all/modules/hierarchical_select/modules/hs_smallhierarchy.info b/sites/all/modules/hierarchical_select/modules/hs_smallhierarchy.info new file mode 100644 index 0000000..7865b04 --- /dev/null +++ b/sites/all/modules/hierarchical_select/modules/hs_smallhierarchy.info @@ -0,0 +1,12 @@ +name = Hierarchical Select Small Hierarchy +description = Allows Hierarchical Select to be used for a hardcoded hierarchy. When it becomes to slow, you should move the hierarchy into the database and write a proper implementation. +dependencies[] = hierarchical_select +package = Form Elements +core = 7.x + +; Information added by Drupal.org packaging script on 2016-06-03 +version = "7.x-3.0-beta7" +core = "7.x" +project = "hierarchical_select" +datestamp = "1464981541" + diff --git a/sites/all/modules/hierarchical_select/modules/hs_smallhierarchy.module b/sites/all/modules/hierarchical_select/modules/hs_smallhierarchy.module new file mode 100644 index 0000000..be4c7eb --- /dev/null +++ b/sites/all/modules/hierarchical_select/modules/hs_smallhierarchy.module @@ -0,0 +1,214 @@ + t('Custom'), + 'entity type' => t('N/A'), + ); +} + + +//---------------------------------------------------------------------------- +// Private functions. + +/** + * Automatically transform a given hierarchy with this format: + * array( + * 'win' => array( + * 'label' => 'Windows', + * 'children' => array( + * 'xp' => array('label' => 'XP'), + * 'vista' => array( + * 'label' => 'Vista', + * 'children' => array( + * 'x86' => array('label' => '32-bits'), + * 'x64' => array('label' => '64-bits'), + * ), + * ), + * ), + * ), + * ) + * + * to one with this format: + * array( + * 'root' => array( + * 'children' => array( + * 'xp', + * 'vista', + * ), + * ), + * 'win' => array( + * 'label' => 'Windows', + * 'children' => array( + * 'win|xp', + * 'win|vista', + * ), + * ), + * 'win|xp' => array( + * 'label' => 'XP', + * ), + * 'win|vista' => array( + * 'label' => 'Vista', + * 'children' => array( + * 'win|vista|x86', + * 'win|vista|x64', + * ), + * ), + * 'win|vista|x86' => array( + * 'label' => '32-bits', + * ), + * 'win|vista|x64' => array( + * 'label' => '64-bits', + * ), + * ) + * + * This new format: + * - ensures unique identifiers for each item + * - makes it very easy to find the parent of a given item. + * - makes it very easy to find the label and children of a given item. + * + * @params $hierarchy + * The hierarchy. + * @params $id + * A unique identifier for the hierarchy, for caching purposes. + * @params $separator + * The separator to use. + */ +function _hs_smallhierarchy_transform($hierarchy, $id, $separator = '|') { + // Make sure each hierarchy is only transformed once. + if (!isset($hs_hierarchy[$id])) { + $hs_hierarchy[$id] = array(); + + // Build the root level. + foreach ($hierarchy as $item => $children) { + $hs_hierarchy[$id]['root']['children'][] = $item; + $hs_hierarchy[$id][$item]['label'] = $children['label']; + + // Build the subsequent levels. + if (isset($children['children'])) { + _hs_smallhierarchy_transform_recurse($item, $hs_hierarchy[$id], $children['children'], $separator); + } + } + } + + return $hs_hierarchy[$id]; +} + +/** + * Helper function for _hs_smallhierarchy_transform(). + * + * @params $parent + * The parent item of the current level. + * @params $hs_hierarchy + * The HS hierarchy. + * @params $relative_hierarchy + * The hierarchy relative to the current level. + * @params $separator + * The separator to use. + */ +function _hs_smallhierarchy_transform_recurse($parent, &$hs_hierarchy, $relative_hierarchy, $separator = '|') { + foreach ($relative_hierarchy as $item => $children) { + $generated_item = $parent . $separator . $item; + $hs_hierarchy[$parent]['children'][] = $generated_item; + $hs_hierarchy[$generated_item]['label'] = $children['label']; + + // Build the subsequent levels. + if (isset($children['children'])) { + _hs_smallhierarchy_transform_recurse($generated_item, $hs_hierarchy, $children['children'], $separator); + } + } +} diff --git a/sites/all/modules/hierarchical_select/modules/hs_taxonomy.info b/sites/all/modules/hierarchical_select/modules/hs_taxonomy.info new file mode 100644 index 0000000..16811ea --- /dev/null +++ b/sites/all/modules/hierarchical_select/modules/hs_taxonomy.info @@ -0,0 +1,14 @@ +name = Hierarchical Select Taxonomy +description = Use Hierarchical Select for Taxonomy. +dependencies[] = hierarchical_select +dependencies[] = taxonomy +package = Form Elements + +core = 7.x + +; Information added by Drupal.org packaging script on 2016-06-03 +version = "7.x-3.0-beta7" +core = "7.x" +project = "hierarchical_select" +datestamp = "1464981541" + diff --git a/sites/all/modules/hierarchical_select/modules/hs_taxonomy.install b/sites/all/modules/hierarchical_select/modules/hs_taxonomy.install new file mode 100644 index 0000000..72712c3 --- /dev/null +++ b/sites/all/modules/hierarchical_select/modules/hs_taxonomy.install @@ -0,0 +1,123 @@ +condition('name', 'taxonomy_hierarchical_select_%', 'LIKE') + ->execute(); + variable_del('taxonomy_override_selector'); +} + +/** + * Implementation of hook_enable(). + */ +function hs_taxonomy_enable() { + variable_set('taxonomy_override_selector', TRUE); + drupal_set_message(t("Drupal core's taxonomy selects are now overridden on the + Taxonomy Term form. They've been replaced by + Hierarchical Selects for better scalability.
+ You can configure it to + be used on node forms too!", + array( + '!configure-url' => url('admin/config/content/hierarchical_select/configs'), + ))); +} + +/** + * Implementation of hook_disable(). + */ +function hs_taxonomy_disable() { + variable_set('taxonomy_override_selector', FALSE); + drupal_set_message(t("Drupal core's taxonomy selects are now restored. + Please remember that they're not scalable!."), + 'warning'); +} + + +//---------------------------------------------------------------------------- +// Schema updates. + +/** + * Upgrade path from Drupal 6 to Drupal 7 version of Hierarchical Select: + * - delete the taxonomy_override_selector variable if it exists. + */ +function hs_taxonomy_update_7300() { + variable_del('taxonomy_override_selector'); +} + +/** + * Apparently, taxonomy_override_selector still exists in *one* location in + * Drupal 7 core: on the form_taxonomy_form_term form (where you can create or + * edit a Taxonomy term). + */ +function hs_taxonomy_update_7301() { + variable_set('taxonomy_override_selector', TRUE); +} + +/** + * Convert Taxonomy vocabulary config IDs to use machine name instead of serial + * vocabulary ID. + */ +function hs_taxonomy_update_7302() { + require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc'; + + $vocabularies = taxonomy_vocabulary_get_names(); + + foreach ($vocabularies as $machine_name => $vocabulary) { + $old_config_id = "taxonomy-{$vocabulary->vid}"; + $new_config_id = "taxonomy-{$machine_name}"; + + $old_config = variable_get('hs_config_' . $old_config_id, NULL); + + if (!empty($old_config)) { + hierarchical_select_common_config_set($new_config_id, $old_config); + hierarchical_select_common_config_del($old_config_id); + } + } +} + +/** + * Convert Taxonomy vocabulary config IDs to use field name. + */ +function hs_taxonomy_update_7303() { + require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc'; + + foreach (field_info_instances() as $entity_type => $bundles) { + foreach ($bundles as $bundle => $field_list) { + foreach ($field_list as $field_name => $instance) { + if ($instance['widget']['type'] == 'taxonomy_hs') { + $field_info = field_info_field($field_name); + $allowed_value = $field_info['settings']['allowed_values'][0]; + $vocabulary_name = $allowed_value['vocabulary']; + $old_config_id = "taxonomy-{$vocabulary_name}"; + $new_config_id = "taxonomy-{$field_name}"; + $old_config = hierarchical_select_common_config_get($old_config_id); + if (!empty($old_config)) { + hierarchical_select_common_config_set($new_config_id, $old_config); + } + } + } + } + } +} + +/** + * Cleanup old-named configs. + */ +function hs_taxonomy_update_7304() { + require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc'; + + $vocabularies = taxonomy_get_vocabularies(); + foreach ($vocabularies as $vid => $vocabulary) { + $old_config_id = "taxonomy-{$vocabulary->machine_name}"; + hierarchical_select_common_config_del($old_config_id); + } +} diff --git a/sites/all/modules/hierarchical_select/modules/hs_taxonomy.module b/sites/all/modules/hierarchical_select/modules/hs_taxonomy.module new file mode 100644 index 0000000..7da8106 --- /dev/null +++ b/sites/all/modules/hierarchical_select/modules/hs_taxonomy.module @@ -0,0 +1,1273 @@ + array( + 'variables' => array('lineage' => array()), + ), + ); +} + +/** + * Implements hook_form_FORMID_alter(). + * + * Alter the Hierarchical Select admin settings form to add a checkbox to + * disable Hierarchical Select for taxonomy term edit forms. + */ +function hs_taxonomy_form_hierarchical_select_admin_settings_alter(&$form, &$form_state) { + $form['taxonomy_override_selector'] = array( + '#type' => 'checkbox', + '#title' => t('Enable for taxonomy term edit forms'), + '#description' => t( + 'If this is checked then the "Relations > Parent terms" field on taxonomy term edit pages will use hierarchical select.' + ), + '#default_value' => variable_get('taxonomy_override_selector', FALSE), + ); +} + +/** + * Implements hook_form_FORMID_alter(). + * + * Alter the widget type form; dynamically add the Hierarchical Select + * Configuration form when it is needed. + */ +function hs_taxonomy_form_field_ui_widget_type_form_alter(&$form, &$form_state) { + form_load_include($form_state, 'inc', 'hierarchical_select', 'includes/common'); + + // Alter the widget type select: configure #ajax so that we can respond to + // changes in its value: whenever it is set to "taxonomy_hs", we add the HS + // config UI. + $form['basic']['widget_type']['#ajax'] = array( + 'event' => 'change', + 'callback' => 'hs_taxonomy_field_ui_widget_settings_ajax', + 'wrapper' => 'hs-config-replace', + 'method' => 'replace' + ); + + $current_widget_type = (isset($form_state['input']['widget_type'])) ? $form_state['input']['widget_type'] : $form_state['build_info']['args'][0]['widget']['type']; + if ($current_widget_type == 'taxonomy_hs') { + $field = field_info_field($form['#field_name']); + if (!empty($field['settings']['allowed_values'][0]['vocabulary'])) { + $vocabulary = taxonomy_vocabulary_machine_name_load($field['settings']['allowed_values'][0]['vocabulary']); + } + + $vid = isset($vocabulary->vid) ? $vocabulary->vid : NULL; + $save_lineage = isset($vocabulary->hierarchy) ? (int) ($vocabulary->hierarchy == 2) : 0; + $instance = field_info_instance($form['#entity_type'], $form['#field_name'], $form['#bundle']); + + // Add the Hierarchical Select config form. + $module = 'hs_taxonomy'; + $params = array( + 'vid' => $vid, + 'exclude_tid' => NULL, + 'root_term' => NULL, + ); + $config_id = hs_taxonomy_get_config_id($form['#field_name']); + $defaults = array( + // Enable the save_lineage setting by default if the multiple parents + // vocabulary option is enabled. + 'save_lineage' => $save_lineage, + 'editability' => array( + 'max_levels' => _hs_taxonomy_hierarchical_select_get_depth($vid), + ), + ); + $strings = array( + 'hierarchy' => t('taxonomy_vocabulary'), + 'hierarchies' => t('vocabularies'), + 'item' => t('term'), + 'items' => t('terms'), + 'item_type' => t('term type'), + 'entity' => t('node'), + 'entities' => t('nodes'), + ); + $max_hierarchy_depth = _hs_taxonomy_hierarchical_select_get_depth($vid); + $preview_is_required = ($instance['required'] == 1); + $form['hs'] = hierarchical_select_common_config_form($module, $params, $config_id, $defaults, $strings, $max_hierarchy_depth, $preview_is_required); + + if (!module_exists('taxonomy_entity_index')) { + // Only allow person to select nodes. + $form['hs']['entity_count']['settings']['entity_types'] = array('node' => $form['hs']['entity_count']['settings']['entity_types']['node']); + $form['hs']['entity_count']['settings']['#collapsed'] = FALSE; + $form['hs']['entity_count']['settings']['entity_types']['node']['#collapsed'] = FALSE; + $form['hs']['entity_count']['#description'] = '

' . t('You can extend this functionality to other entities if you install !taxonomy_entity_index.', array('!taxonomy_entity_index' => l('Taxonomy entity index', 'https://www.drupal.org/project/taxonomy_entity_index'))) . '

'; + } + + // Make the config form AJAX-updateable. + $form['hs'] += array( + '#prefix' => '
', + '#suffix' => '
', + ); + + // Add the submit handler for the Hierarchical Select config form. Make + // sure it is executed first. + $form['#hs_common_config_form_parents'] = array('hs'); + array_unshift($form['#submit'], 'hierarchical_select_common_config_form_submit'); + + // Add a submit handler for HS Taxonomy that will update the field + // settings when necessary. + // @see hs_taxonomy_field_settings_submit() for details. + $form['#submit'][] = 'hs_taxonomy_field_settings_submit'; + } + else { + $form['hs'] = array( + '#prefix' => '
', + '#suffix' => '
', + ); + } +} + +/** + * Submit callback; updates the field settings (i.e. sets the cardinality of + * the field to unlimited) whenever either the dropbox or "save lineage" is + * enabled. + */ +function hs_taxonomy_field_settings_submit(&$form, &$form_state) { + $field = field_info_field($form['#field_name']); + $config = hierarchical_select_common_config_get(hs_taxonomy_get_config_id($form['#field_name'])); + + if ($config['dropbox']['status'] || $config['save_lineage']) { + $field = field_info_field($form['#field_name']); + $field['cardinality'] = -1; // -1 = unlimited + field_update_field($field); + + drupal_set_message(t("Updated this field's cardinality to unlimited.")); + } +} + +/** + * Implements hook_form_FORMID_alter(). + * + * Alter the field settings form; dynamically disable the "cardinality" (or + * "Number of values" in the UI) setting on the form when either the dropbox + * or "save lineage" is enabled. + */ +function hs_taxonomy_form_field_ui_field_edit_form_alter(&$form, &$form_state) { + if (isset($form['#field']['type']) && $form['#field']['type'] === 'taxonomy_term_reference' && $form['#instance']['widget']['type'] == 'taxonomy_hs') { + require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc'; + + $config = hierarchical_select_common_config_get(hs_taxonomy_get_config_id($form['#field']['field_name'])); + + if ($config['dropbox']['status'] || $config['save_lineage']) { + $form['field']['cardinality']['#disabled'] = TRUE; + $form['field']['cardinality']['#description'] .= ' ' . t('This setting is now managed by the Hierarchical Select configuration.') . ''; + } + } +} + +function hs_taxonomy_form_taxonomy_form_term_alter(&$form, &$form_state) { + // Don't alter the form when taxonomy_override_selector is not TRUE (or 1). + if (!variable_get('taxonomy_override_selector', FALSE)) { + return; + } + + // Don't alter the form when it's in confirmation mode. + if (isset($form_state['confirm_delete']) || isset($form_state['confirm_parents'])) { + return; + } + + // Build an appropriate config. + $vocabulary = $form['#vocabulary']; + $vid = $vocabulary->vid; + module_load_include('inc', 'hierarchical_select', 'includes/common'); + $config = array( + 'module' => 'hs_taxonomy', + 'params' => array( + 'vid' => $vid, + 'exclude_tid' => isset($form['#term']['tid']) ? $form['#term']['tid'] : NULL, + 'root_term' => TRUE, + ), + 'enforce_deepest' => 0, + 'save_lineage' => 0, + 'level_labels' => array('status' => FALSE, 'labels' => array()), + 'dropbox' => array( + 'status' => variable_get('hs_taxonomy_enable_dropbox_on_term_form', 0), + 'limit' => 0, + ), + 'editability' => array( + 'status' => 0, + ), + 'entity_count' => array( + 'enable' => 0, + 'require_entity' => 0, + 'settings' => array( + 'count_children' => 0, + 'entity_types' => array(), + ), + ), + 'render_flat_select' => 0, + ); + + // Use Hierarchical Select for selecting the parent term(s). + $parent_tid = array_keys(taxonomy_get_parents($form['#term']['tid'])); + $parent = !empty($parent_tid) ? $parent_tid : array(0); + $form['relations']['parent'] = array( + '#type' => 'hierarchical_select', + '#title' => t('Parents'), + '#required' => TRUE, + '#default_value' => $parent, + '#config' => $config, + ); + $form['relations']['parent']['#config']['dropbox']['title'] = t('All parent terms'); +} + +/** + * Implements hook_field_delete(). + * + * This enables us to delete HS configs when fields are deleted. + */ +function hs_taxonomy_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) { + require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc'; + hierarchical_select_common_config_del(hs_taxonomy_get_config_id($field['field_name'])); +} + +//---------------------------------------------------------------------------- +// FAPI callbacks. + +/** + * AJAX callback; field UI widget settings form. + */ +function hs_taxonomy_field_ui_widget_settings_ajax($form, &$form_state) { + return $form['hs']; +} + + +//---------------------------------------------------------------------------- +// Field API widget hooks. + +/** + * Implements hook_field_widget_info(). + */ +function hs_taxonomy_field_widget_info() { + return array( + 'taxonomy_hs' => array( + 'label' => t('Hierarchical Select'), + 'field types' => array('taxonomy_term_reference'), + 'settings' => array(), // All set in hs_taxonomy_field_widget_form(). + 'behaviors' => array( + // TODO: figure out how to map the "dropbox" behavior to Field API's + // "multiple values" system. + 'multiple values' => FIELD_BEHAVIOR_CUSTOM, + ), + ), + ); +} + +/** + * Implements hook_field_widget_form(). + */ +function hs_taxonomy_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { + require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc'; + + if (!empty($field['settings']['allowed_values'][0]['vocabulary'])) { + $vocabulary = taxonomy_vocabulary_machine_name_load($field['settings']['allowed_values'][0]['vocabulary']); + } + + // Build an array of existing term IDs. + $tids = array(); + foreach ($items as $delta => $item) { + if (!empty($item['tid']) && $item['tid'] != 'autocreate') { + $tids[] = $item['tid']; + } + } + + $element += array( + '#type' => 'hierarchical_select', + '#config' => array( + 'module' => 'hs_taxonomy', + 'params' => array( + 'vid' => isset($vocabulary->vid) ? (int) $vocabulary->vid : NULL, + 'exclude_tid' => NULL, + 'root_term' => isset($field['settings']['allowed_values'][0]['parent']) ? (int) $field['settings']['allowed_values'][0]['parent'] : NULL, + ), + ), + '#default_value' => $tids, + ); + + hierarchical_select_common_config_apply($element, hs_taxonomy_get_config_id($field['field_name'])); + + // Append another #process callback that transforms #return_value to the + // format that Field API/Taxonomy Field expects. + // However, HS' default #process callback has not yet been set, since this + // typically happens automatically during FAPI processing. To ensure the + // order is right, we already set HS' own #process callback here explicitly. + $element_info = element_info('hierarchical_select'); + $element['#process'] = array_merge($element_info['#process'], array('hs_taxonomy_widget_process')); + + return $element; +} + +/** + * Implements hook_field_widget_settings_form(). + */ +function hs_taxonomy_field_widget_settings_form($field, $instance) { + // This poorly integrates with the Field UI. Hence we alter the + // field_ui_widget_type_form, to provide a more appropriate integration. + // @see hs_taxonomy_form_field_ui_widget_type_form_alter. + $form = array(); + return $form; +} + +/** + * Implements hook_field_widget_error(). + */ +function hs_taxonomy_field_widget_error($element, $error, $form, &$form_state) { + form_error($element, $error['message']); +} + +/** + * #process callback that runs after HS' #process callback, to transform + * #return_value to the format that Field API/Taxonomy Field expects. + */ +function hs_taxonomy_widget_process($element, &$form_state, $complete_form) { + $tids = $element['#return_value']; + + // If #return_value is array(NULL), then nothing was selected! + if (count($tids) == 1 && $tids[0] === NULL) { + $element['#return_value'] = array(); + return $element; + } + + $items = array(); + foreach ($tids as $tid) { + $items[] = array('tid' => $tid); + } + + $element['#return_value'] = $items; + + return $element; +} + + +//---------------------------------------------------------------------------- +// Field API formatter hooks. + +/** + * Implements hook_field_formatter_info(). + */ +function hs_taxonomy_field_formatter_info() { + return array( + 'hs_taxonomy_term_reference_hierarchical_text' => array( + 'label' => t('Hierarchical text'), + 'field types' => array('taxonomy_term_reference'), + ), + 'hs_taxonomy_term_reference_hierarchical_links' => array( + 'label' => t('Hierarchical links'), + 'field types' => array('taxonomy_term_reference'), + ), + 'hs_taxonomy_term_reference_hierarchical_links_last_text' => array( + 'label' => t('Hierarchical links last text'), + 'field types' => array('taxonomy_term_reference'), + ), + 'hs_taxonomy_term_reference_hierarchical_text_last_link' => array( + 'label' => t('Hierarchical text last link'), + 'field types' => array('taxonomy_term_reference'), + ), + 'hs_taxonomy_term_reference_last_link_only' => array( + 'label' => t('Last link only'), + 'field types' => array('taxonomy_term_reference'), + ), + 'hs_taxonomy_term_reference_last_text_only' => array( + 'label' => t('Last text only'), + 'field types' => array('taxonomy_term_reference'), + ), + ); +} + +/** + * Implements hook_field_formatter_prepare_view(). + */ +function hs_taxonomy_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) { + // Extract required field information. + $vocabulary = taxonomy_vocabulary_machine_name_load($field['settings']['allowed_values'][0]['vocabulary']); + $vid = $vocabulary->vid; + + // Get the config for this field. + module_load_include('inc', 'hierarchical_select', 'includes/common'); + $config_id = hs_taxonomy_get_config_id($field['field_name']); + $config = hierarchical_select_common_config_get($config_id); + $config += array( + 'module' => 'hs_taxonomy', + 'params' => array( + 'vid' => $vid, + ), + ); + + // Collect every possible term attached to any of the fieldable entities. + // Copied from taxonomy_field_formatter_prepare_view(). + foreach ($entities as $id => $entity) { + $selection = array(); + + foreach ($items[$id] as $delta => $item) { + // Force the array key to prevent duplicates. + if ($item['tid'] != 'autocreate') { + $selection[$item['tid']] = $item['tid']; + } + } + + // Generate a dropbox out of the selection. This will automatically + // calculate all lineages for us. + $dropbox = _hierarchical_select_dropbox_generate($config, $selection); + + // Store additional information in each item that's required for + // Hierarchical Select's custom formatters that are compatible with the + // save_lineage functionality. + if (!empty($dropbox->lineages)) { + foreach (array_keys($dropbox->lineages) as $lineage) { + foreach ($dropbox->lineages[$lineage] as $level => $details) { + $tid = $details['value']; + + // Look up where this term (tid) is stored in the items array. + $key = array_search($tid, $selection); + + // Store the additional information. One term can occur in multiple + // lineages: when Taxonomy's "multiple parents" functionality is + // being used. + $items[$id][$key]['hs_lineages'][] = array( + 'lineage' => $lineage, + 'level' => $level, + 'label' => $details['label'], + 'tid' => $tid, + ); + } + } + } + } +} + +/** + * Implements hook_field_formatter_view(). + */ +function hs_taxonomy_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) { + // Extract required field information. + $vocabulary = taxonomy_vocabulary_machine_name_load($field['settings']['allowed_values'][0]['vocabulary']); + + // Extract the lineage information from the items (this was added by + // hs_taxonomy_field_formatter_prepare_view()). + $lineages = array(); + foreach ($items as $delta => $item) { + if (!empty($item['hs_lineages'])) { + $metadata = $item['hs_lineages']; + + for ($i = 0; $i < count($metadata); $i++) { + $term = new StdClass(); + $term->tid = $metadata[$i]['tid']; + $term->vid = $vocabulary->vid; + $term->vocabulary_machine_name = $vocabulary->machine_name; + $term->name = $metadata[$i]['label']; + + $lineages[$metadata[$i]['lineage']][$metadata[$i]['level']] = $term; + } + } + } + + // Actual formatting. + $element = array(); + switch ($display['type']) { + case 'hs_taxonomy_term_reference_hierarchical_text': + for ($l = 0; $l < count($lineages); $l++) { + $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage'; + for ($level = 0; $level < count($lineages[$l]); $level++) { + $term = $lineages[$l][$level]; + $element[$l]['#lineage'][$level] = array( + '#markup' => $term->name, + ); + } + } + break; + + case 'hs_taxonomy_term_reference_hierarchical_links': + for ($l = 0; $l < count($lineages); $l++) { + $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage'; + for ($level = 0; $level < count($lineages[$l]); $level++) { + $term = $lineages[$l][$level]; + $uri = entity_uri('taxonomy_term', $term); + $uri['options']['html'] = TRUE; + $element[$l]['#lineage'][$level] = array( + '#type' => 'link', + '#title' => $term->name, + '#href' => $uri['path'], + '#options' => $uri['options'], + ); + } + } + break; + + case 'hs_taxonomy_term_reference_hierarchical_links_last_text': + for ($l = 0; $l < count($lineages); $l++) { + $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage'; + for ($level = 0; $level < count($lineages[$l]) - 1; $level++) { + $term = $lineages[$l][$level]; + $uri = entity_uri('taxonomy_term', $term); + $element[$l]['#lineage'][$level] = array( + '#type' => 'link', + '#title' => $term->name, + '#href' => $uri['path'], + '#options' => $uri['options'], + ); + } + if (count($lineages[$l]) > 0) { + $level = count($lineages[$l]) - 1; + $term = $lineages[$l][$level]; + $element[$l]['#lineage'][$level] = array( + '#markup' => $term->name, + ); + } + } + break; + + case 'hs_taxonomy_term_reference_hierarchical_text_last_link': + for ($l = 0; $l < count($lineages); $l++) { + $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage'; + for ($level = 0; $level < count($lineages[$l]) - 1; $level++) { + $term = $lineages[$l][$level]; + $element[$l]['#lineage'][$level] = array( + '#markup' => $term->name, + ); + } + if (count($lineages[$l]) > 0) { + $level = count($lineages[$l]) - 1; + $term = $lineages[$l][$level]; + $uri = entity_uri('taxonomy_term', $term); + $element[$l]['#lineage'][$level] = array( + '#type' => 'link', + '#title' => $term->name, + '#href' => $uri['path'], + '#options' => $uri['options'], + ); + } + } + break; + + case 'hs_taxonomy_term_reference_last_text_only': + for ($l = 0; $l < count($lineages); $l++) { + $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage'; + if (count($lineages[$l]) > 0) { + $level = count($lineages[$l]) - 1; + $term = $lineages[$l][$level]; + $element[$l]['#lineage'][0] = array( + '#markup' => $term->name, + ); + } + } + break; + + case 'hs_taxonomy_term_reference_last_link_only': + for ($l = 0; $l < count($lineages); $l++) { + $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage'; + if (count($lineages[$l]) > 0) { + $level = count($lineages[$l]) - 1; + $term = $lineages[$l][$level]; + $uri = entity_uri('taxonomy_term', $term); + $element[$l]['#lineage'][0] = array( + '#type' => 'link', + '#title' => $term->name, + '#href' => $uri['path'], + '#options' => $uri['options'], + ); + } + } + break; + } + + if (!empty($element)) { + $element['#attached']['css'][] = drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.css'; + } + + return $element; +} + + +//---------------------------------------------------------------------------- +// Hierarchical Select hooks. + +/** + * Implementation of hook_hierarchical_select_params(). + */ +function hs_taxonomy_hierarchical_select_params() { + $params = array( + 'vid', + 'exclude_tid', // Allows a term to be excluded (necessary for the taxonomy_form_term form). + 'root_term', // Displays a fake "" term in the root level (necessary for the taxonomy_form-term form). + 'entity_count_for_node_type', // Restrict the entity count to a specific node type. + ); + return $params; +} + +/** + * Implementation of hook_hierarchical_select_root_level(). + */ +function hs_taxonomy_hierarchical_select_root_level($params) { + if (!isset($params['vid'])) { + return array(); + } + // TODO: support multiple parents, i.e. support "save lineage". + $vocabulary = taxonomy_vocabulary_load($params['vid']); + $terms = _hs_taxonomy_hierarchical_select_get_tree($params['vid'], 0, -1, 1); + + // If the root_term parameter is enabled, then prepend a fake "" term. + if (isset($params['root_term']) && $params['root_term'] === TRUE) { + $root_term = new StdClass(); + $root_term->tid = 0; + $root_term->name = '<' . t('root') . '>'; + $terms = array_merge(array($root_term), $terms); + } + + // Unset the term that's being excluded, if it is among the terms. + if (isset($params['exclude_tid'])) { + foreach ($terms as $key => $term) { + if ($term->tid == $params['exclude_tid']) { + unset($terms[$key]); + } + } + } + + // If the Term Permissions module is installed, honor its settings. + if (function_exists('term_permissions_allowed')) { + global $user; + foreach ($terms as $key => $term) { + if (!term_permissions_allowed($term->tid, $user) ) { + unset($terms[$key]); + } + } + } + + return _hs_taxonomy_hierarchical_select_terms_to_options($terms); +} + +/** + * Implementation of hook_hierarchical_select_children(). + */ +function hs_taxonomy_hierarchical_select_children($parent, $params) { + if (isset($params['root_term']) && $params['root_term'] && $parent == 0) { + return array(); + } + + $terms = taxonomy_get_children($parent, $params['vid']); + + // Unset the term that's being excluded, if it is among the children. + if (isset($params['exclude_tid'])) { + unset($terms[$params['exclude_tid']]); + } + + // If the Term Permissions module is installed, honor its settings. + if (function_exists('term_permissions_allowed')) { + global $user; + foreach ($terms as $key => $term) { + if (!term_permissions_allowed($term->tid, $user) ) { + unset($terms[$key]); + } + } + } + + return _hs_taxonomy_hierarchical_select_terms_to_options($terms); +} + +/** + * Implementation of hook_hierarchical_select_lineage(). + */ +function hs_taxonomy_hierarchical_select_lineage($item, $params) { + $lineage = array(); + + if (isset($params['root_term']) && $params['root_term'] && $item == 0) { + return array(0); + } + + $terms = array_reverse(hs_taxonomy_get_parents_all($item)); + foreach ($terms as $term) { + $lineage[] = $term->tid; + } + return $lineage; +} + +/** + * Alternative version of taxonomy_get_parents_all(): instead of using all + * parents of a term (i.e. when multiple parents are being used), only the + * first is kept. + */ +function hs_taxonomy_get_parents_all($tid) { + $parents = array(); + if ($term = taxonomy_term_load($tid)) { + $parents[] = $term; + $n = 0; + while ($parent = taxonomy_get_parents($parents[$n]->tid)) { + $parents = array_merge($parents, array(reset($parent))); + $n++; + } + } + return $parents; +} + +/** + * Implementation of hook_hierarchical_select_valid_item(). + */ +function hs_taxonomy_hierarchical_select_valid_item($item, $params) { + if (isset($params['root_term']) && $params['root_term'] && $item == 0) { + return TRUE; + } + + if (!is_numeric($item) || $item < 1 || (isset($params['exclude_tid']) && $item == $params['exclude_tid'])) { + return FALSE; + } + + $term = taxonomy_term_load($item); + if (!$term) { + return FALSE; + } + + // If the Term Permissions module is installed, honor its settings. + if (function_exists('term_permissions_allowed')) { + global $user; + if (!term_permissions_allowed($term->tid, $user)) { + return FALSE; + } + } + + return ($term->vid == $params['vid']); +} + +/** + * Implementation of hook_hierarchical_select_item_get_label(). + */ +function hs_taxonomy_hierarchical_select_item_get_label($item, $params) { + static $labels = array(); + + if (!isset($labels[$item])) { + if ($item === 0 && isset($params['root_term']) && $params['root_term'] === TRUE) { + $term = new StdClass(); + $term->name = '<' . t('root') . '>'; + } + else { + $term = taxonomy_term_load($item); + // Try to translate the label if the i18n module is available. + if (module_exists('i18n_taxonomy')) { + $term->name = i18n_taxonomy_term_name($term); + } + } + $labels[$item] = $term->name; + } + + return $labels[$item]; +} + +/** + * Implementation of hook_hierarchical_select_create_item(). + */ +function hs_taxonomy_hierarchical_select_create_item($label, $parent, $params) { + $term = new StdClass(); + $term->vid = $params['vid']; + $term->name = html_entity_decode($label, ENT_QUOTES); + $term->description = ''; + $term->parent = $parent; + + $status = taxonomy_term_save($term); + + if ($status !== FALSE) { + // Reset the cached tree. + _hs_taxonomy_hierarchical_select_get_tree($params['vid'], 0, -1, 1, TRUE); + + // Retrieve the tid. + $children = _hs_taxonomy_hierarchical_select_get_tree($params['vid'], $parent, 1); + foreach ($children as $term) { + if ($term->name == $label) { + return $term->tid; + } + } + } + else { + return FALSE; + } +} + +/** + * Implementation of hook_hierarchical_select_entity_count(). + */ +function hs_taxonomy_hierarchical_select_entity_count($item, $params) { + + $num_entities = 0; + $selected_bundles = $params['entity_count']['settings']['entity_types']; + $count_children = $params['entity_count']['settings']['count_children']; + + // Maybe this needs some more caching and value-updates on entity_save()/ + // _update()/delete(). + if (empty($num_entities)) { + $index_table = 'taxonomy_index'; + if (module_exists('taxonomy_entity_index')) { + $index_table = 'taxonomy_entity_index'; + } + + // Count entities associated to this term. + $query = db_select($index_table, 'ti'); + $query->fields('ti'); + $query->condition('ti.tid', $item); + if (module_exists('taxonomy_entity_index')) { + _hs_taxonomy_add_entity_bundles_condition_to_query($query, $selected_bundles); + } + $result = $query->execute(); + + $num_entities = $result->rowCount(); + + if ($count_children) { + $tids = array(); + $tree = taxonomy_get_tree($params['vid'], $item); + foreach ($tree as $child_term) { + $tids[] = $child_term->tid; + } + if (count($tids)) { + // Count entities associated to child terms. + $query = db_select($index_table, 'ti'); + $query->fields('ti'); + $query->condition('ti.tid', $tids, 'IN'); + if (module_exists('taxonomy_entity_index')) { + _hs_taxonomy_add_entity_bundles_condition_to_query($query, $selected_bundles); + } + $result = $query->execute(); + + $num_entities += $result->rowCount(); + } + } + } + + return $num_entities; +} + +/** + * Implementation of hook_hierarchical_select_implementation_info(). + */ +function hs_taxonomy_hierarchical_select_implementation_info() { + return array( + 'hierarchy type' => t('Taxonomy'), + 'entity type' => t('Node'), + ); +} + +/** + * Implementation of hook_hierarchical_select_config_info(). + */ +function hs_taxonomy_hierarchical_select_config_info() { + static $config_info; + + if (!isset($config_info)) { + $config_info = array(); + $fields = field_info_fields(); + foreach ($fields as $field_name => $field) { + foreach ($field['bundles'] as $entity_type => $bundles) { + foreach ($bundles as $bundle) { + $instance = field_info_instance($entity_type, $field_name, $bundle); + if ($instance['widget']['type'] == 'taxonomy_hs') { + $bundles_info = field_info_bundles($entity_type); + $entity_info = entity_get_info($entity_type); + $machine_name = $field['settings']['allowed_values'][0]['vocabulary']; + $vocabulary = taxonomy_vocabulary_machine_name_load($machine_name); + $config_id = hs_taxonomy_get_config_id($field_name); + $config_info[$config_id] = array( + 'config_id' => $config_id, + 'hierarchy type' => t('Taxonomy'), + 'hierarchy' => t('Vocabulary') . ': ' . l(t($vocabulary->name), "admin/structure/taxonomy/$vocabulary->machine_name") . '
' . t('Field label') . ': ' . $instance['label'] . '
' . t('Field machine name') . ': ' . $field_name . '
', + 'entity type' => $entity_info['label'], + 'bundle' => l(t($bundles_info[$bundle]['label']), "admin/structure/types/manage/$bundle"), + 'context type' => '', + 'context' => '', + 'edit link' => isset($bundles_info[$bundle]['admin']['real path']) ? $bundles_info[$bundle]['admin']['real path'] . "/fields/$field_name/widget-type" : $bundles_info[$bundle]['admin']['path'] . "/fields/$field_name/widget-type", + ); + + } + } + } + } + } + + return $config_info; +} + + +//---------------------------------------------------------------------------- +// Token hooks. + +/** + * Implementation of hook_token_values(). + */ +/* +// TODO: port this to D7. +function hs_taxonomy_token_values($type, $object = NULL, $options = array()) { + static $hs_vids; + static $all_vids; + + $separator = variable_get('hs_taxonomy_separator', variable_get('pathauto_separator', '-')); + + $values = array(); + switch ($type) { + case 'node': + $node = $object; + + // Default values. + $values['save-lineage-termpath'] = $values['save-lineage-termpath-raw'] = ''; + + // If $node->taxonomy doesn't exist, these tokens cannot be created! + if (!is_object($node) || !isset($node->taxonomy) || !is_array($node->taxonomy)) { + return $values; + } + + // Find out which vocabularies are using Hierarchical Select. + if (!isset($hs_vids)) { + $hs_vids = array(); + // TODO Please convert this statement to the D7 database API syntax. + $result = db_query("SELECT SUBSTRING(name, 30, 3) AS vid FROM {variable} WHERE name LIKE 'taxonomy_hierarchical_select_%' AND value LIKE 'i:1\;';"); + while ($o = db_fetch_object($result)) { + $hs_vids[] = $o->vid; + } + } + + // Get a list of all existent vids, so we can generate an empty token + // when a token is requested for a vocabulary that's not associated with + // the current content type. + if (!isset($all_vids)) { + $all_vids = array(); + $result = db_query("SELECT vid FROM {taxonomy_vocabulary}"); + while ($row = db_fetch_object($result)) { + $all_vids[] = $row->vid; + } + } + + // Generate the per-vid "save-lineage-termpath" tokens. + foreach ($all_vids as $vid) { + $terms = array(); + if (in_array($vid, $hs_vids) && isset($node->taxonomy[$vid])) { + $selection = $node->taxonomy[$vid]; + $terms = _hs_taxonomy_token_termpath_for_vid($selection, $vid); + } + + $terms_raw = $terms; + $terms = array_map('check_plain', $terms); + $values["save-lineage-termpath:$vid"] = !empty($options['pathauto']) ? $terms : implode($separator, $terms); + $values["save-lineage-termpath-raw:$vid"] = !empty($options['pathauto']) ? $terms_raw : implode($separator, $terms_raw); + } + + // We use the terms of the first vocabulary that uses Hierarchical + // Select for the default "save-lineage-termpath" tokens. + $vids = array_intersect(array_keys($node->taxonomy), $hs_vids); + if (!empty($vids)) { + $vid = $vids[0]; + $values['save-lineage-termpath'] = $values["save-lineage-termpath:$vid"]; + $values['save-lineage-termpath-raw'] = $values["save-lineage-termpath-raw:$vid"]; + } + break; + } + + return $values; +} +*/ + +/** + * Implementation of hook_token_list(). + */ +/* +// TODO: port this to D7. +function hs_taxonomy_token_list($type = 'all') { + if ($type == 'node' || $type == 'all') { + $tokens['node']['save-lineage-termpath'] = t('Only use when you have enabled the "save lineage" setting of Hierarchical Select. Will show the term\'s parent terms separated by /.'); + $tokens['node']['save-lineage-termpath-raw'] = t('As [save-linage-termpath]. WARNING - raw user input.'); + + $tokens['node']['save-lineage-termpath:vid'] = t('Only has output when terms are present for the vocabulary with the specified vid. Only use when you have enabled the "save lineage" setting of Hierarchical Select. Will show the term\'s parent terms separated by /.'); + $tokens['node']['save-lineage-termpath-raw:vid'] = t('Only has output when terms are present for the vocabulary with the specified vid. As [save-linage-termpath]. WARNING - raw user input.'); + + return $tokens; + } +} +*/ + +/** + * Helper function for hs_taxonomy_token_values(). + */ +function _hs_taxonomy_token_termpath_for_vid($selection, $vid) { + $terms = array(); + $selection = (is_array($selection)) ? $selection : array($selection); + + // Generate the part we'll need of the Hierarchical Select configuration. + $config = array( + 'module' => 'hs_taxonomy', + 'save_lineage' => 1, + 'params' => array( + 'vid' => $vid, + 'exclude_tid' => NULL, + 'root_term' => NULL, + ), + ); + + // Validate all items in the selection, if any. + if (!empty($selection)) { + foreach ($selection as $key => $item) { + $valid = module_invoke($config['module'], 'hierarchical_select_valid_item', $selection[$key], $config['params']); + if (!$valid) { + unset($selection[$key]); + } + } + } + + // Generate a dropbox out of the selection. This will automatically + // calculate all lineages for us. + // If the selection is empty, then the tokens will be as well. + if (!empty($selection)) { + $dropbox = _hierarchical_select_dropbox_generate($config, $selection); + + // If no lineages could be generated, these tokens cannot be created! + if (empty($dropbox->lineages)) { + return $terms; + } + + // We pick the first lineage. + $lineage = $dropbox->lineages[0]; + + // Finally, we build the tokens. + foreach ($lineage as $item) { + $terms[] = $item['label']; + } + } + + return $terms; +} + + +//---------------------------------------------------------------------------- +// Theme functions. + +/** + * Format a lineage for one of HS Taxonomy's custom Term reference formatters. + */ +function theme_hs_taxonomy_formatter_lineage($variables) { + $output = ''; + $lineage = $variables['lineage']; + $separator = theme('hierarchical_select_item_separator'); + + // Render each item within a lineage. + $items = array(); + foreach ($lineage as $level => $item ) { + $line = ''; + $line .= drupal_render($item); + $line .= ''; + $items[] = $line; + } + $output .= implode($separator, $items); + + return $output; +} + + +//---------------------------------------------------------------------------- +// Private functions. + +/** + * Drupal core's taxonomy_get_tree() doesn't allow us to reset the cached + * trees, which obviously causes problems when you create new items between + * two calls to it. + */ +function _hs_taxonomy_hierarchical_select_get_tree($vid, $parent = 0, $depth = -1, $max_depth = NULL, $reset = FALSE) { + static $children, $parents, $terms; + + if ($reset) { + $children = $parents = $terms = array(); + } + + $tree = array(); + + if (!is_array($parent)) { + $parent = array($parent); + } + + $max_depth = (is_null($max_depth)) ? 99999999 : $max_depth; + $depth++; + + // We cache trees, so it's not CPU-intensive to call get_tree() on a term + // and its children, too. + if ($max_depth <= $depth) { + return $tree; + } + // Prepare queue for the "IN ( .. )" part of query. + $queue = array(); + foreach ($parent as $single_parent) { + // Queue branch for processing if it's not cached yet. + if (!isset($children[$vid][$single_parent])) { + $queue[] = $single_parent; + // Use an empty array to distinguish between a stub (without children) + // term and a branch that is not loaded yet. + $children[$vid][$single_parent] = array(); + } + } + if (!empty($queue)) { + + $query = db_select('taxonomy_term_data', 't'); + $query->join('taxonomy_term_hierarchy', 'h', 'h.tid = t.tid'); + $result = $query + ->addTag('translatable') + ->addTag('term_access') + ->addTag('hs_taxonomy_tree') + ->fields('t') + ->fields('h', array('parent')) + ->condition('t.vid', $vid) + ->condition('parent', array_merge(array($vid), $queue), 'IN') + ->orderBy('t.weight') + ->orderBy('t.name') + ->execute(); + + foreach ($result as $term) { + $children[$vid][$term->parent][] = $term->tid; + $parents[$vid][$term->tid][] = $term->parent; + $terms[$vid][$term->tid] = $term; + } + } + + // Provide support for Title module. If Title module is enabled and this + // vocabulary uses translated term names we want output those terms with their + // translated version. Therefore a full taxonomy term entity load is required, + // similar to taxonomy_get_tree(). + if (!empty($terms) && module_exists('title')) { + $vocabulary = taxonomy_vocabulary_load($vid); + if (title_field_replacement_enabled('taxonomy_term', $vocabulary->machine_name, 'name')) { + $term_entities = taxonomy_term_load_multiple(array_keys($terms[$vid])); + } + } + + $next_parent = array(); + foreach ($parent as $single_parent) { + foreach ($children[$vid][$single_parent] as $child) { + $term = isset($term_entities[$child]) ? $term_entities[$child] : $terms[$vid][$child]; + $term = clone $term; + $term->depth = $depth; + // The "parent" attribute is not useful, as it would show one parent only. + unset($term->parent); + $term->parents = $parents[$vid][$child]; + $tree[] = $term; + // Need more steps ? + if ($max_depth > $depth + 1) { + // Queue children for the next step down the tree. Do not process + // children which we already know as stub ones. + if (!isset($children[$vid][$child]) || !empty($children[$vid][$child])) { + $next_parent[] = $child; + } + } + } + } + if (!empty($next_parent)) { + // Process multiple children together i.e. next level. + $tree = array_merge($tree, _hs_taxonomy_hierarchical_select_get_tree($vid, $next_parent, $depth, $max_depth)); + } + + return isset($tree) ? $tree : array(); +} + +/** + * Returns the configuration ID that would be used for the specified field. + * + * @param string $field_name + * The field machine name. + * + * @return string + * The config id for the provided field. + */ +function hs_taxonomy_get_config_id($field_name) { + return "taxonomy-{$field_name}"; +} + +/** + * Drupal core's taxonomy_term_count_nodes() is buggy. See + * http://drupal.org/node/144969#comment-843000. + */ +function hs_taxonomy_term_count_nodes($tid, $type = 0) { + static $count; + + $tids = array($tid); + if ($term = taxonomy_term_load($tid)) { + $tree = _hs_taxonomy_hierarchical_select_get_tree($term->vid, $tid); + foreach ($tree as $descendant) { + $tids[] = $descendant->tid; + } + } + + if (!isset($count[$type][$tid])) { + $query = db_select('taxonomy_term_node','t'); + $query->join('node', 'n', 't.nid = n.nid'); + $query->addExpression('COUNT(DISTINCT(n.nid))', 'count') + ->condition('n.status', 1) + ->condition('t,tid', $tids); + + if (!is_numeric($type)) { + $query->condition('n.type', $type); + } + + $query->addTag('hs_taxonomy_term_count_nodes'); + $query->addTag('term_access'); + $result = $query->execute(); + + $count[$type][$tid] = $result->fetchField(); + } + return $count[$type][$tid]; +} + +/** + * Transform an array of terms into an associative array of options, for use + * in a select form item. + * + * @param $terms + * An array of term objects. + * @return + * An associative array of options, keys are tids, values are term names. + */ +function _hs_taxonomy_hierarchical_select_terms_to_options($terms) { + $options = array(); + $use_i18n = module_exists('i18n_taxonomy'); + foreach ($terms as $key => $term) { + // Use the translated term when available! + $options[$term->tid] = $use_i18n && isset($term->vid) ? i18n_taxonomy_term_name($term) : $term->name; + } + return $options; +} + +/** + * Get the depth of a vocabulary's tree. + * + * @param $vid + * A vocabulary id. + * @return + * The depth of the vocabulary's tree. + */ +function _hs_taxonomy_hierarchical_select_get_depth($vid) { + $depth = -99999; + $tree = _hs_taxonomy_hierarchical_select_get_tree($vid); + foreach ($tree as $term) { + if ($term->depth > $depth) { + $depth = $term->depth; + } + } + return $depth; +} + +/** + * Helper function to add entity_type and bundles to count query in form of a + * and/or combination. + * + * @param object $query + * A db_select query object. + * @param array $selected_bundles + * An associative array of entities that contain bundles. + */ +function _hs_taxonomy_add_entity_bundles_condition_to_query(&$query, $selected_bundles) { + $db_or = db_or(); + foreach ($selected_bundles as $entity_type => $bundles) { + $db_and = db_and(); + $db_and->condition('ti.entity_type', $entity_type); + $db_and->condition('ti.bundle', array_shift($bundles) , 'IN'); + $db_or->condition($db_and); + } + $query->condition($db_or); +} diff --git a/sites/all/modules/hierarchical_select/modules/hser/README.txt b/sites/all/modules/hierarchical_select/modules/hser/README.txt new file mode 100644 index 0000000..4eb1737 --- /dev/null +++ b/sites/all/modules/hierarchical_select/modules/hser/README.txt @@ -0,0 +1,8 @@ +This module allows you to use hierarchical_select (version 7.x-3.x) as a widget +for a taxonomy-based entityreference field. + +To use it, create an entityreference field, with the Hierarchical Select widget, +select "Taxonomy term" as the target type, and select one vocabulary (you must +choose exactly one) as the target bundle. + +Credit: John Morahan, iO1 and iVillage. diff --git a/sites/all/modules/hierarchical_select/modules/hser/hser.info b/sites/all/modules/hierarchical_select/modules/hser/hser.info new file mode 100644 index 0000000..c3c87d1 --- /dev/null +++ b/sites/all/modules/hierarchical_select/modules/hser/hser.info @@ -0,0 +1,18 @@ +name = Hierarchical Select Entity Reference +description = Use the hierarchical select widget for entity reference fields, using taxonomy to provide hierarchy if appropriate, otherwise flat. +package = Form Elements +core = 7.x +dependencies[] = hierarchical_select +dependencies[] = hs_taxonomy +dependencies[] = entityreference +dependencies[] = entity +dependencies[] = ctools +dependencies[] = options +dependencies[] = field + +; Information added by Drupal.org packaging script on 2016-06-03 +version = "7.x-3.0-beta7" +core = "7.x" +project = "hierarchical_select" +datestamp = "1464981541" + diff --git a/sites/all/modules/hierarchical_select/modules/hser/hser.module b/sites/all/modules/hierarchical_select/modules/hser/hser.module new file mode 100644 index 0000000..eaaf47d --- /dev/null +++ b/sites/all/modules/hierarchical_select/modules/hser/hser.module @@ -0,0 +1,132 @@ + array( + 'label' => t('Hierarchical Select'), + 'field types' => array('entityreference'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_CUSTOM, + ), + 'settings' => array( + 'editable' => FALSE, + ), + ), + ); +} + +/** + * Implements hook_field_widget_settings_form(). + */ +function hser_field_widget_settings_form($field, $instance) { + $widget = $instance['widget']; + $settings = $widget['settings'] + field_info_widget_settings($widget['type']); + + $form = array(); + + if ($widget['type'] == 'hser_hierarchy') { + $form['editable'] = array( + '#type' => 'checkbox', + '#title' => t('Editable'), + '#default_value' => $settings['editable'], + '#description' => t('Select this to allow users to use the hierarchical select widget to create new terms in the selected vocabulary.'), + ); + } + + return $form; +} + + +/** + * Implements hook_field_widget_form(). + */ +function hser_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { + if ($field['settings']['target_type'] == 'taxonomy_term') { + $vocabularies = $field['settings']['handler_settings']['target_bundles']; + if ((count($vocabularies) == 1) && ($vocabulary = taxonomy_vocabulary_machine_name_load(reset($vocabularies)))) { + $default_value = array(); + foreach ($items as $item) { + $default_value[] = $item['target_id']; + } + $element += array( + '#type' => 'hierarchical_select', + '#size' => 1, + '#default_value' => $default_value, + '#element_validate' => array('_hser_element_validate'), + '#required' => $instance['required'], + '#config' => array( + 'module' => 'hs_taxonomy', + 'params' => array( + 'vid' => $vocabulary->vid, + ), + 'save_lineage' => FALSE, + 'enforce_deepest' => FALSE, + 'resizable' => FALSE, + 'level_labels' => array('status' => FALSE), + 'dropbox' => array( + 'status' => ($field['cardinality'] != 1), + 'limit' => $field['cardinality'], + ), + 'editability' => array( + 'status' => $instance['widget']['settings']['editable'], + 'allow_new_levels' => TRUE, + 'max_levels' => 0, + ), + 'entity_count' => array( + 'enabled' => 0, + 'require_entity' => 0, + 'settings' => array( + 'count_children' => 0, + 'entity_types' => array(), + ), + ), + 'render_flat_select' => FALSE, + ), + ); + return $element; + } + } + // If we reach this point, we decided that hierarchical_select would not be + // appropriate for some reason (not taxonomy, no vocabulary selected, etc). + // So instead we fall back to a normal select widget. + $instance['widget']['type'] = 'options_select'; + return options_field_widget_form($form, $form_state, $field, $instance, $langcode, $items, $delta, $element); +} + +/** + * Element validation callback for field widget hierarchical select element. + */ +function _hser_element_validate($element, &$form_state, $form) { + $value = array(); + foreach ($element['#value'] as $delta => $target_id) { + $value[$delta]['target_id'] = $target_id; + } + form_set_value($element, $value, $form_state); + + if ($element['#required'] && (!isset($form_state['submit_handlers'][0]) || $form_state['submit_handlers'][0] !== 'hierarchical_select_ajax_update_submit')) { + if (!count($element['#value']) || (is_string($element['#value']) && strlen(trim($element['#value'])) == 0) || (array_key_exists(0, $element['#value'])) && !$element['#value'][0]) { + form_error($element, t('!name field is required.', array('!name' => $element['#title']))); + _hierarchical_select_form_set_error_class($element); + } + } +} + +/** + * Implements hook_node_validate(). + * + * Temporary workaround for https://drupal.org/node/1293166 - remove when that + * bug is fixed. + */ +function hser_node_validate($node, $form, &$form_state) { + if (arg(0) == 'hierarchical_select_ajax') { + form_set_error(''); + } +} diff --git a/sites/all/modules/hierarchical_select/tests/internals.test b/sites/all/modules/hierarchical_select/tests/internals.test new file mode 100644 index 0000000..e734124 --- /dev/null +++ b/sites/all/modules/hierarchical_select/tests/internals.test @@ -0,0 +1,433 @@ + array( + 'label' => LABEL_EURO, + 'children' => array( + EURO_BE => array( + 'label' => LABEL_EURO_BE, + 'children' => array( + EURO_BE_BRU => array( + 'label' => LABEL_EURO_BE_BRU, + ), + EURO_BE_HAS => array( + 'label' => LABEL_EURO_BE_HAS, + ), + ), + ), + EURO_FR => array( + 'label' => LABEL_EURO_FR, + ), + ), + ), + ASIA => array( + 'label' => LABEL_ASIA, + 'children' => array( + ASIA_CH => array( + 'label' => LABEL_ASIA_CH, + ), + ASIA_JP => array( + 'label' => LABEL_ASIA_JP, + 'children' => array( + ASIA_JP_TOK => array( + 'label' => LABEL_ASIA_JP_TOK, + ), + ), + ), + ), + ), + ); + + + /** + * Implementation of getInfo(). + */ + public function getInfo() { + return array( + 'name' => 'Internals', + 'description' => 'Checks whether all internals are working: the + building of the hierarchy and dropbox objects.', + 'group' => 'Hierarchical Select', + ); + } + + /** + * Implementation of setUp(). + */ + public function setUp() { + parent::setUp('hierarchical_select', 'hs_smallhierarchy'); + } + + // In this test, all settings are disabled. + public function testAllSettingsOff() { + // Generate form item. + $form_item = array( + '#required' => FALSE, + '#config' => array( + 'module' => 'hs_smallhierarchy', + 'params' => array( + 'hierarchy' => $this->small_hierarchy, + 'id' => 'driverpack_platforms', + 'separator' => '|', + ), + 'save_lineage' => 0, + 'enforce_deepest' => 0, + 'resizable' => 1, + 'level_labels' => array( + 'status' => 0, + ), + 'dropbox' => array( + 'status' => 0, + 'limit' => 0, + 'reset_hs' => 1, + ), + 'editability' => array( + 'status' => 0, + 'item_types' => array(), + 'allowed_levels' => array(), + 'allow_new_levels' => 0, + 'max_levels' => 3, + ), + 'entity_count' => array( + 'enabled' => 0, + 'require_entity' => 0, + 'settings' => array( + 'count_children' => 0, + 'entity_types' => array(), + ), + ), + 'animation_delay' => 400, + 'exclusive_lineages' => array(), + 'render_flat_select' => 0, + ), + ); + + // No selection + list($hierarchy, $dropbox) = $this->generate($form_item, array(), array()); + $reference = new stdClass(); + $reference->lineage = array( + 0 => 'none', + ); + $reference->levels = array( + 0 => array( + 'none' => '', + LINEAGE_EURO => LABEL_EURO, + LINEAGE_ASIA => LABEL_ASIA, + ), + ); + $reference->childinfo = array( + 0 => array( + LINEAGE_EURO => 2, + LINEAGE_ASIA => 2, + ), + ); + $this->assertHierarchy($hierarchy, $reference); + + // Europe + list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_EURO), array()); + $reference->lineage = array( + 0 => LINEAGE_EURO, + 1 => 'label_1', + ); + $reference->levels[1] = array( + 'label_1' => '', + LINEAGE_EURO_BE => LABEL_EURO_BE, + LINEAGE_EURO_FR => LABEL_EURO_FR, + ); + $reference->childinfo[1] = array( + LINEAGE_EURO_BE => 2, + LINEAGE_EURO_FR => 0, + ); + $this->assertHierarchy($hierarchy, $reference); + + // Europe > France + list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_EURO_FR), array()); + $reference->lineage = array( + 0 => LINEAGE_EURO, + 1 => LINEAGE_EURO_FR, + ); + $this->assertHierarchy($hierarchy, $reference); + + // Europe > Belgium + list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_EURO_BE), array()); + $reference->lineage = array( + 0 => LINEAGE_EURO, + 1 => LINEAGE_EURO_BE, + 2 => 'label_2', + ); + $reference->levels[1] = array( + 'label_1' => '', + LINEAGE_EURO_BE => LABEL_EURO_BE, + LINEAGE_EURO_FR => LABEL_EURO_FR, + ); + $reference->levels[2] = array( + 'label_2' => '', + LINEAGE_EURO_BE_BRU => LABEL_EURO_BE_BRU, + LINEAGE_EURO_BE_HAS => LABEL_EURO_BE_HAS, + ); + $reference->childinfo[1] = array( + LINEAGE_EURO_BE => 2, + LINEAGE_EURO_FR => 0, + ); + $reference->childinfo[2] = array( + LINEAGE_EURO_BE_BRU => 0, + LINEAGE_EURO_BE_HAS => 0, + ); + $this->assertHierarchy($hierarchy, $reference); + + // Asia + list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_ASIA), array()); + $reference->lineage = array( + 0 => LINEAGE_ASIA, + 1 => 'label_1', + ); + $reference->levels[1] = array( + 'label_1' => '', + LINEAGE_ASIA_CH => LABEL_ASIA_CH, + LINEAGE_ASIA_JP => LABEL_ASIA_JP, + ); + unset($reference->levels[2]); + $reference->childinfo[1] = array( + LINEAGE_ASIA_CH => 0, + LINEAGE_ASIA_JP => 1, + ); + unset($reference->childinfo[2]); + $this->assertHierarchy($hierarchy, $reference); + + // Asia > Japan > Tokyo + list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_ASIA_JP_TOK), array()); + $reference->lineage = array( + 0 => LINEAGE_ASIA, + 1 => LINEAGE_ASIA_JP, + 2 => LINEAGE_ASIA_JP_TOK, + ); + $reference->levels[2] = array( + 'label_2' => '', + LINEAGE_ASIA_JP_TOK => LABEL_ASIA_JP_TOK, + ); + $reference->childinfo[2] = array( + LINEAGE_ASIA_JP_TOK => 0, + ); + $this->assertHierarchy($hierarchy, $reference); + } + + // In this test, only enforce_deepest enabled. + public function testEnforceDeepest() { + // Generate form item. + $form_item = array( + '#required' => FALSE, + '#config' => array( + 'module' => 'hs_smallhierarchy', + 'params' => array( + 'hierarchy' => $this->small_hierarchy, + 'id' => 'driverpack_platforms', + 'separator' => '|', + ), + 'save_lineage' => 0, + 'enforce_deepest' => 1, + 'resizable' => 1, + 'level_labels' => array( + 'status' => 0, + ), + 'dropbox' => array( + 'status' => 0, + 'limit' => 0, + 'reset_hs' => 1, + ), + 'editability' => array( + 'status' => 0, + 'item_types' => array(), + 'allowed_levels' => array(), + 'allow_new_levels' => 0, + 'max_levels' => 3, + ), + 'entity_count' => array( + 'enabled' => 0, + 'require_entity' => 0, + 'settings' => array( + 'count_children' => 0, + 'entity_types' => array(), + ), + ), + 'animation_delay' => 400, + 'exclusive_lineages' => array(), + 'render_flat_select' => 0, + ), + ); + + // No selection + list($hierarchy, $dropbox) = $this->generate($form_item, array(), array()); + $reference = new stdClass(); + $reference->lineage = array( + 0 => 'label_0', + ); + $reference->levels = array( + 0 => array( + 'none' => '', + LINEAGE_EURO => LABEL_EURO, + LINEAGE_ASIA => LABEL_ASIA, + ), + ); + $reference->childinfo = array( + 0 => array( + LINEAGE_EURO => 2, + LINEAGE_ASIA => 2, + ), + ); + $this->assertHierarchy($hierarchy, $reference); + + // Europe + list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_EURO), array()); + $reference->lineage = array( + 0 => LINEAGE_EURO, + 1 => LINEAGE_EURO_BE, + 2 => LINEAGE_EURO_BE_BRU, + ); + $reference->levels[1] = array( + LINEAGE_EURO_BE => LABEL_EURO_BE, + LINEAGE_EURO_FR => LABEL_EURO_FR, + ); + $reference->levels[2] = array( + LINEAGE_EURO_BE_BRU => LABEL_EURO_BE_BRU, + LINEAGE_EURO_BE_HAS => LABEL_EURO_BE_HAS, + ); + $reference->childinfo[1] = array( + LINEAGE_EURO_BE => 2, + LINEAGE_EURO_FR => 0, + ); + $reference->childinfo[2] = array( + LINEAGE_EURO_BE_BRU => 0, + LINEAGE_EURO_BE_HAS => 0, + ); + $this->assertHierarchy($hierarchy, $reference); + + // Europe > France + list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_EURO_FR), array()); + $reference->lineage = array( + 0 => LINEAGE_EURO, + 1 => LINEAGE_EURO_FR, + ); + unset($reference->levels[2]); + unset($reference->childinfo[2]); + $this->assertHierarchy($hierarchy, $reference); + + // Europe > Belgium + list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_EURO_BE), array()); + $reference->lineage = array( + 0 => LINEAGE_EURO, + 1 => LINEAGE_EURO_BE, + 2 => LINEAGE_EURO_BE_BRU, + ); + $reference->levels[1] = array( + LINEAGE_EURO_BE => LABEL_EURO_BE, + LINEAGE_EURO_FR => LABEL_EURO_FR, + ); + $reference->levels[2] = array( + LINEAGE_EURO_BE_BRU => LABEL_EURO_BE_BRU, + LINEAGE_EURO_BE_HAS => LABEL_EURO_BE_HAS, + ); + $reference->childinfo[1] = array( + LINEAGE_EURO_BE => 2, + LINEAGE_EURO_FR => 0, + ); + $reference->childinfo[2] = array( + LINEAGE_EURO_BE_BRU => 0, + LINEAGE_EURO_BE_HAS => 0, + ); + $this->assertHierarchy($hierarchy, $reference); + + // Asia + list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_ASIA), array()); + $reference->lineage = array( + 0 => LINEAGE_ASIA, + 1 => LINEAGE_ASIA_CH, + ); + $reference->levels[1] = array( + LINEAGE_ASIA_CH => LABEL_ASIA_CH, + LINEAGE_ASIA_JP => LABEL_ASIA_JP, + ); + unset($reference->levels[2]); + $reference->childinfo[1] = array( + LINEAGE_ASIA_CH => 0, + LINEAGE_ASIA_JP => 1, + ); + unset($reference->childinfo[2]); + $this->assertHierarchy($hierarchy, $reference); + + // Asia > Japan > Tokyo + list($hierarchy, $dropbox) = $this->generate($form_item, array(LINEAGE_ASIA_JP_TOK), array()); + $reference->lineage = array( + 0 => LINEAGE_ASIA, + 1 => LINEAGE_ASIA_JP, + 2 => LINEAGE_ASIA_JP_TOK, + ); + $reference->levels[2] = array( + LINEAGE_ASIA_JP_TOK => LABEL_ASIA_JP_TOK, + ); + $reference->childinfo[2] = array( + LINEAGE_ASIA_JP_TOK => 0, + ); + $this->assertHierarchy($hierarchy, $reference); + } + + + //-------------------------------------------------------------------------- + // Private methods. + + private function generate($element, $hs_selection, $db_selection, $op = 'Update') { + $config = $element['#config']; + + // Generate the $hierarchy and $dropbox objects using the selections that + // were just calculated. + $dropbox = (!$config['dropbox']['status']) ? FALSE : _hierarchical_select_dropbox_generate($config, $db_selection); + $hierarchy = _hierarchical_select_hierarchy_generate($config, $hs_selection, $element['#required'], $dropbox); + + return array($hierarchy, $dropbox); + } + + private function assertHierarchy($hierarchy, $reference) { + $this->assertIdentical($hierarchy->lineage, $reference->lineage, 'Hierarchy lineage is correct.'); + $this->assertIdentical($hierarchy->levels, $reference->levels, 'Hierarchy levels is correct.'); + $this->assertIdentical($hierarchy->childinfo, $reference->childinfo, 'Hierarchy child info is correct.'); + } +} diff --git a/sites/all/modules/revisioning/LICENSE.txt b/sites/all/modules/revisioning/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/sites/all/modules/revisioning/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/sites/all/modules/revisioning/README.txt b/sites/all/modules/revisioning/README.txt new file mode 100644 index 0000000..d6bc6e3 --- /dev/null +++ b/sites/all/modules/revisioning/README.txt @@ -0,0 +1,39 @@ + +DESCRIPTION +=========== +This module channels unpublished content as well as edits to current content +into a queue for review by a moderator/publisher prior to becoming "live", i.e. +visible to the public. + +We took our inspiration from the Revision Moderation module by Angie Byron, +but found that a patch could not implement the deviating functionality our +customers required, which would change the current behaviour of the RM module +and surprise existing users. + +The Revisioning module now comes with two user interface models to chose from: +"traditional" and "updated". + +Traditional: The traditional UI model is enabled by default. With the standard +UI, the emphasis is on minimizing the number of controls, so each page is free +of buttons and links that are not immediately relevant. + +Updated: The updated UI model can be selected by enabling the "revisioning_ux" +submodule. In the updated UI, the emphasis is on maintaining consistent +navigation controls. This allows revision moderators to flip between the tabs +on the revisions operations page without ending up on a page that no longer +contains all of the tabs formerly available. + +There are no database schema differences between the two UI models; you may +enable revisioning_ux, try it out, and later go back to the traditional model +without any difficulty. + + +INSTALLATION & CONFIGURATION +============================ +Step-by-step instructions can be found in the tutorials listed on the +Revisioning project page, http://drupal.org/project/revisioning. + +AUTHORS +======= +Main module: Rik de Boer, Melbourne, Australia. +Revisioning UX: Greg Anderson, USA diff --git a/sites/all/modules/revisioning/THEMING.txt b/sites/all/modules/revisioning/THEMING.txt new file mode 100644 index 0000000..0f077a5 --- /dev/null +++ b/sites/all/modules/revisioning/THEMING.txt @@ -0,0 +1,19 @@ + +If you wish to override the way the revisions table is constructed, +implement in your own module the function below. + +If you decided to do so, don't forget to register your theme_hook() via +_theme() in a manner similar to what is done in revisioning_theme(), +file revisioning_theme.inc. + +/* + * Custom alternative to the way Revisioning module themes its revisions table. + * @param $header + * @param $rows + * @return themed HTML, see for instance /includes/theme.inc/theme_table() and + * diff.module/theme_diff_table() + * + * @ingroup themeable + */ + function theme_table_revisions($header, $rows) { + } diff --git a/sites/all/modules/revisioning/css/revisioning-rtl.css b/sites/all/modules/revisioning/css/revisioning-rtl.css new file mode 100644 index 0000000..da59e4b --- /dev/null +++ b/sites/all/modules/revisioning/css/revisioning-rtl.css @@ -0,0 +1 @@ +/* Empty. Add your own. */ diff --git a/sites/all/modules/revisioning/css/revisioning.css b/sites/all/modules/revisioning/css/revisioning.css new file mode 100644 index 0000000..92aed62 --- /dev/null +++ b/sites/all/modules/revisioning/css/revisioning.css @@ -0,0 +1,25 @@ +.table-revisions th.form-submit, +.table-revisions td .form-type-checkbox { + text-align: center; +} + +.revision-pending td { + background-color: #fcc; /* pale pink */ +} + +.revision-current td { + background-color: #faa; /* red-pink when current revision is unpublished */ +} + +.revision-current.published td { + background-color: #ffc; /* pale yellow, same as node/node.css */ +} + +div.block-revisioning ul { + padding: 0 0 0 1em; +} + +div.block-revisioning ul li { +/*list-style-type: square;*/ + margin: 0; +} diff --git a/sites/all/modules/revisioning/js/revisioning-radios.js b/sites/all/modules/revisioning/js/revisioning-radios.js new file mode 100644 index 0000000..fa8a5d8 --- /dev/null +++ b/sites/all/modules/revisioning/js/revisioning-radios.js @@ -0,0 +1,55 @@ +(function ($) { + + Drupal.behaviors.suppressPublishedForModeratedContent = { + attach: function (context) { + + var publishedDiv = $('.form-item-status', context); + var firstSummaryTab = $('.vertical-tabs span.summary:first', context) + var lastSummaryTab = $('.vertical-tabs span.summary:last', context); + var textNewRevision = Drupal.t('New revision'); + var textNoRevision = Drupal.t('No revision'); + var textPublished = Drupal.t('Published'); + var textNotPublished = Drupal.t('Not published'); + var checked = $('.form-item-revision-operation .form-radio:checked').val(); + + if (checked == 0) { + // REVISIONING_NO_REVISION. + firstSummaryTab.text(firstSummaryTab.text().replace(textNewRevision, textNoRevision)); + } + else if (checked == 2) { + // REVISIONING_NEW_REVISION_WITH_MODERATION. + // Hide the "Published" check-box, as it does not apply in this mode + // because the new revision will always be unpublished. + publishedDiv.hide(); + lastSummaryTab.text(lastSummaryTab.text().replace(textPublished, textNotPublished)); + } + + $('.form-radio').click(function() { + checked = $('.form-item-revision-operation .form-radio:checked').val(); + + if (checked == 2) { + // REVISIONING_NEW_REVISION_WITH_MODERATION. + publishedDiv.hide(); + lastSummaryTab.text(lastSummaryTab.text().replace(textPublished, textNotPublished)); + } + else { + // REVISIONING_NO_REVISION or REVISIONING_NEW_REVISION_NO_MODERATION. + publishedDiv.show(); + if (checked == 0) { + // REVISIONING_NO_REVISION. + firstSummaryTab.text(firstSummaryTab.text().replace(textNewRevision, textNoRevision)); + } + if ($('.form-item-status input').is(':checked')) { + updatedText = lastSummaryTab.text().replace(textNotPublished, textPublished); + } + else { + updatedText = lastSummaryTab.text().replace(textPublished, textNotPublished); + } + lastSummaryTab.text(updatedText); + } + + }); + } + }; + +})(jQuery); diff --git a/sites/all/modules/revisioning/revisioning.admin.inc b/sites/all/modules/revisioning/revisioning.admin.inc new file mode 100644 index 0000000..a4f7aea --- /dev/null +++ b/sites/all/modules/revisioning/revisioning.admin.inc @@ -0,0 +1,124 @@ + 'fieldset', + '#title' => t('Configure which revision is loaded when clicking on a view or edit link'), + '#description' => t('This section allows you to configure which revision is loaded when clicking on links for viewing or editing moderated content, that is content for which the check-box New revision in draft, pending moderation is ticked on the %link page. If not ticked for this content type, current and latest revisions will be one and the same, so that the options behave identically.', + array('%link' => 'admin/structure/types/manage/')), + ); + $form['revisioning_view_and_edit_links']['revisioning_view_callback'] = array( + '#type' => 'radios', + '#title' => t('Links to view content default to'), + '#options' => array( + REVISIONING_LOAD_CURRENT => t('displaying the current revision'), + REVISIONING_LOAD_LATEST => t('displaying the latest revision (reverts to current revision, if the user is not permitted to view revisions of the content type in question)')), + '#default_value' => variable_get('revisioning_view_callback', REVISIONING_LOAD_CURRENT), + '#description' => t('The first option represents default core behaviour.'), + ); + $form['revisioning_view_and_edit_links']['revisioning_edit_callback'] = array( + '#type' => 'radios', + '#title' => t('Links to edit content default to'), + '#options' => array( + REVISIONING_LOAD_CURRENT => t('editing the current revision'), + REVISIONING_LOAD_LATEST => t('editing the latest revision (reverts to current revision, if the user is not permitted to view revisions of the content type in question)')), + '#default_value' => variable_get('revisioning_edit_callback', REVISIONING_LOAD_CURRENT), + '#description' => t('The first option represents default core behaviour.'), + ); + + $form['revisioning_publication_config'] = array( + '#type' => 'fieldset', + '#title' => t('Publication options'), + ); + $form['revisioning_publication_config']['revisioning_require_update_to_publish'] = array( + '#type' => 'checkbox', + '#title' => t('Require update permission in order to publish or unpublish content.'), + '#default_value' => variable_get('revisioning_require_update_to_publish', TRUE), + '#description' => t('If you have enabled one or modules dealing with content access, then you would normally tick this box.'), + ); + $form['revisioning_publication_config']['revisioning_publication_shortcuts'] = array( + '#type' => 'checkbox', + '#title' => t('Show publication shortcut controls for all users permitted to publish nodes.'), + '#default_value' => variable_get('revisioning_publication_shortcuts', FALSE), + '#description' => t('Radio buttons are added immediately under the "Revisioning" pane in the content edit form for any user permitted to publish. If not selected, then only users with "administer nodes" will see these controls.'), + ); + $form['revisioning_publication_config']['revisioning_no_moderation_by_default'] = array( + '#type' => 'checkbox', + '#title' => t('Publish content by default.'), + '#default_value' => variable_get('revisioning_no_moderation_by_default', FALSE), + '#description' => t('When publication shortcut controls are shown, publish content by default. If not, the default will be to moderate.'), + ); + + if (module_exists('taxonomy')) { + $form['revisioning_display_options'] = array( + '#type' => 'fieldset', + '#title' => t('Display options'), + ); + $form['revisioning_display_options']['revisioning_show_taxonomy_terms'] = array( + '#type' => 'checkbox', + '#title' => t('Where applicable add Tags and Terms columns to the content revisions summary (Revisions tab).'), + '#default_value' => variable_get('revisioning_show_taxonomy_terms', TRUE), + '#description' => t('These columns will only appear if some tags or terms have been assigned to the content shown.'), + ); + $form['revisioning_display_options']['revisioning_in_views_show_unpublished_content_terms'] = array( + '#type' => 'checkbox', + '#title' => t('In feeds and Views, e.g. /content-summary, show Tags and Terms associated with unpublished content, subject to permissions'), + '#default_value' => variable_get('revisioning_in_views_show_unpublished_content_terms', TRUE), + '#description' => t('Core behaviour is to suppress the display of tags and terms on all unpublished content even if the user has the permission.'), + ); + } + + $access_modules = module_implements('node_access'); + $grants_modules = module_implements('node_grants'); + $modules = array_merge($access_modules, $grants_modules); + + $form['revisioning_additional_info'] = array( + '#type' => 'fieldset', + '#title' => t('Additional info'), + '#description' => t('The following content access modules are enabled and may affect whether users can view, edit, delete or publish content:
%modules.', + array('%modules' => empty($modules) ? t('none') : implode(', ', $modules))), + ); + + $form['actions']['#type'] = 'actions'; + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save configuration'), + ); + $form['#submit'][] = 'revisioning_admin_configure_form_submit'; + + $form['#theme'] = 'system_settings_form'; + + return $form; +} + + +/** + * Execute the revisioning_admin_configure_form. + */ +function revisioning_admin_configure_form_submit($form, &$form_state) { + // Exclude unnecessary form elements. + form_state_values_clean($form_state); + + foreach ($form_state['values'] as $key => $new_value) { + $old_value = variable_get($key); + if ($old_value != $new_value) { + variable_set($key, $new_value); + if ($key == 'revisioning_in_views_show_unpublished_content_terms') { + foreach (node_load_multiple(FALSE) as $node) { + revisioning_update_taxonomy_index($node, $new_value); + } + } + } + } + + drupal_set_message(t('The revisioning configuration options have been saved.')); +} diff --git a/sites/all/modules/revisioning/revisioning.api.php b/sites/all/modules/revisioning/revisioning.api.php new file mode 100644 index 0000000..dfe6c34 --- /dev/null +++ b/sites/all/modules/revisioning/revisioning.api.php @@ -0,0 +1,76 @@ +fields(array('weight' => 6)) + ->condition('name', 'revisioning') + ->execute(); + + // Panels override fix, see http://drupal.org/node/519924. + variable_set('page_manager_override_anyway', TRUE); + + // If requested, not yet published and unpublished nodes need to reveal their + // taxonomy terms in Views and in feeds (subject to permissions). + if (module_exists('taxonomy') && variable_get('revisioning_in_views_show_unpublished_content_terms', TRUE)) { + foreach (node_load_multiple(FALSE) as $node) { + revisioning_update_taxonomy_index($node, TRUE); + } + } +} + +/** + * Implements hook_uninstall(). + */ +function revisioning_uninstall() { + // Delete all revisioning_* variables at once. + db_query("DELETE FROM {variable} WHERE name LIKE 'revisioning_%%'"); + // See above. + variable_del('page_manager_override_anyway'); + foreach (node_type_get_types() as $type) { + // Maybe revisioning_auto_publish_ and new_revisions_ + // should be used in array, like 'revision_moderation' below? + variable_del('new_revisions_' . $type->type); + // Remove 'revision_moderation' from all node_options_ variables. + $variable_name = 'node_options_' . $type->type; + if ($node_options = variable_get($variable_name, NULL)) { + $node_options = array_diff($node_options, array('revision_moderation')); + variable_set($variable_name, $node_options); + } + } + // Make sure that unpublished nodes do not reveal their taxonomy terms, once + // Revisioning is uninstalled. + if (module_exists('taxonomy')) { + require_once 'revisioning.taxonomy.inc'; + + foreach (node_load_multiple(FALSE) as $node) { + // Modify node objects to be consistent with Revisioning being + // uninstalled, before updating the {taxonomy_index} table accordingly. + unset($node->revision_moderation); + revisioning_update_taxonomy_index($node, FALSE); + } + } +} + +/** + * Adjust module weight. + * + * As of core 7.12 the new hooks taxonomy_node_insert() and + * taxonomy_node_update() must be executed before the associated revisioning + * hooks. + */ +function revisioning_update_7104() { + db_update('system') + ->fields(array('weight' => 1)) + ->condition('name', 'revisioning') + ->execute(); +} + +/** + * To avoid problems when Revisioning is installed with Diff and Workbench too. + * See [#2142429]. + */ +function revisioning_update_7107() { + db_update('system') + ->fields(array('weight' => '6')) + ->condition('name', 'revisioning') + ->execute(); +} diff --git a/sites/all/modules/revisioning/revisioning.module b/sites/all/modules/revisioning/revisioning.module new file mode 100644 index 0000000..326357d --- /dev/null +++ b/sites/all/modules/revisioning/revisioning.module @@ -0,0 +1,1229 @@ +Revisioning project page', + array('@revisioning' => url('http://drupal.org/project/revisioning'))); + break; + + case 'node/%/revisions': + $s = t('To edit, publish or delete one of the revisions below, click on its saved date.'); + break; + + case 'admin/structure/trigger/revisioning': + $s = t("Below you can assign actions to run when certain publication-related events happen. For example, you could send an e-mail to an author when their pending content is pubished."); + break; + + case 'accessible-content/i-created/pending': + $s = t('Showing all pending content you created and still have at least view access to.'); + break; + + case 'accessible-content/i-last-modified/pending': + $s = t('Showing all pending content you last modified and still have at least view access to.'); + break; + + case 'accessible-content/i-can-edit/pending': + $s = t('Showing all pending content you can edit.'); + break; + + case 'accessible-content/i-can-view/pending': + $s = t('Showing all pending content you have at least view access to.'); + break; + } + return empty($s) ? '' : $s . '
'; +} + +/** + * Implements hook_permission(). + * + * Revisioning permissions. Note that permissions to view, revert and delete + * revisions already exist in node.module. + */ +function revisioning_permission() { + + $edit_description = t('Also requires edit permission from either the Node or other content access module(s).'); + + $moderated_content_types = implode(', ', revisioning_moderated_content_types(FALSE)); + $publish_description = empty($moderated_content_types) + ? t('Please select one or more content types for moderation by ticking the New revision in draft, pending moderation Publishing option at Structure >> Content types >> edit.') + : t('Applies to content types that are subject to moderation, i.e.: %moderated_content_types.', + array('%moderated_content_types' => $moderated_content_types)); + + if (!empty($moderated_content_types) && variable_get('revisioning_require_update_to_publish', TRUE)) { + $publish_description .= '
' . $edit_description; + } + + $permissions = array( + 'view revision status messages' => array( + 'title' => t('View revision status messages'), + 'description' => '', + ), + 'edit revisions' => array( + 'title' => t('Edit content revisions'), + 'description' => $edit_description, + ), + 'publish revisions' => array( + 'title' => t("Publish content revisions (of anyone's content)"), + 'description' => $publish_description, + ), + 'unpublish current revision' => array( + 'title' => t("Unpublish current revision (of anyone's content)"), + 'description' => $publish_description, + ), + ); + // Add per node-type view permissions in same way as edit permissions of node + // module, but only for moderated content-types. + foreach (node_type_get_types() as $type) { + $machine_name = check_plain($type->type); + if (revisioning_content_is_moderated($machine_name)) { + $permissions['view revisions of own ' . $machine_name . ' content'] = array( + 'title' => t('%type-name: View revisions of own content', array('%type-name' => $type->name))); + $permissions['view revisions of any ' . $machine_name . ' content'] = array( + 'title' => t("%type-name: View revisions of anyone's content", array('%type-name' => $type->name))); + $permissions['publish revisions of own ' . $machine_name . ' content'] = array( + 'title' => t('%type-name: Publish revisions of own content', array('%type-name' => $type->name))); + $permissions['publish revisions of any ' . $machine_name . ' content'] = array( + 'title' => t("%type-name: Publish revisions of anyone's content", array('%type-name' => $type->name))); + } + } + return $permissions; +} + +/** + * Implements hook_menu(). + * + * Define new menu items. + * Existing menu items are modified through hook_menu_alter(). + */ +function revisioning_menu() { + $items = array(); + + // Start with the Revisioning config menu item, put under Content Authoring. + $items['admin/config/content/revisioning'] = array( + 'title' => 'Revisioning', + 'description' => 'Configure how content view and edit links behave. Customise revision summary listing.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('revisioning_admin_configure'), + 'access arguments' => array('administer site configuration'), + 'file' => 'revisioning.admin.inc', + ); + + // Plain link, not a tab, to allow users to unpublish a node. + $items['node/%node/unpublish-current'] = array( + // 'title' => t(Unpublish current revision'), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('revisioning_unpublish_confirm', 1), + 'access callback' => '_revisioning_access_node_revision', + 'access arguments' => array('unpublish current revision', 1), + 'type' => MENU_CALLBACK, + ); + + // Revision tab local subtasks (i.e. secondary tabs), up to 8 of them: + // list, view, edit, publish, unpublish, revert, delete and compare. + // All revision operations 'node/%node/revisions/%vid/' are defined as + // local subtasks (subtabs) secondary to the primary 'node/%node/revisions' + // local task (primary tab). + // + // Subtab to the Revisions primary tab to allow going back to the revisions + // list without clicking the primary tab for a second time, which also works. + $items['node/%node/revisions/list'] = array( + 'title' => 'List all revisions', + 'access callback' => '_revisioning_access_node_revision', + 'access arguments' => array('view revision list', 1), + 'type' => MENU_LOCAL_TASK, + 'weight' => -20, + ); + + $items['node/%node/revisions/delete-archived'] = array( + 'title' => 'Delete archived revisions', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('revisioning_delete_archived_confirm', 1), + 'access callback' => '_revisioning_access_node_revision', + 'access arguments' => array('delete archived revisions', 1), + 'type' => MENU_CALLBACK, + ); + + // View revision local subtask. + // Note the use of %vid as opposed to %. This allows us to manipulate the + // second argument in the path through vid_to_arg(). + $items['node/%node/revisions/%vid/view'] = array( + 'title' => 'View', + 'load arguments' => array(3), + 'page callback' => '_revisioning_view_revision', + 'page arguments' => array(1), + 'access callback' => '_revisioning_access_node_revision', + 'access arguments' => array('view revisions', 1), + 'type' => MENU_LOCAL_TASK, + 'weight' => -10, + // 'tab_parent' => 'node/%/revisions', + ); + // Edit revision local subtask. + $items['node/%node/revisions/%vid/edit'] = array( + 'title' => 'Edit', + 'load arguments' => array(3), + 'page callback' => '_revisioning_edit_revision', + 'page arguments' => array(1), + 'access callback' => '_revisioning_access_node_revision', + 'access arguments' => array('edit revisions', 1), + 'file' => 'node.pages.inc', + 'file path' => drupal_get_path('module', 'node'), + 'type' => MENU_LOCAL_TASK, + 'weight' => -7, + // 'tab_parent' => 'node/%/revisions', + ); + // Publish revision local subtask. + // As the menu is content type unaware, a further check on + // node->revision_moderation must be made to determine whether it is + // appropriate to show this tab. + // This is done in _revisioning_access_node_revision. + $items['node/%node/revisions/%vid/publish'] = array( + 'title' => 'Publish', + 'load arguments' => array(3), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('revisioning_publish_confirm', 1), + 'access callback' => '_revisioning_access_node_revision', + 'access arguments' => array('publish revisions', 1), + 'type' => MENU_LOCAL_TASK, + 'weight' => -4, + ); + // Unpublish node local subtask. + // As the menu is content type unaware, a further check on + // node->revision_moderation must be made to determine whether it is + // appropriate to show this tab. + // This is done in _revisioning_access_node_revision. + $items['node/%node/revisions/%vid/unpublish'] = array( + 'title' => 'Unpublish', + 'load arguments' => array(3), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('revisioning_unpublish_confirm', 1), + 'access callback' => '_revisioning_access_node_revision', + 'access arguments' => array('unpublish current revision', 1), + 'type' => MENU_LOCAL_TASK, + 'weight' => -3, + ); + // Revert to revision local subtask. + $items['node/%node/revisions/%vid/revert'] = array( + 'title' => 'Revert to this', + 'load arguments' => array(3), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('node_revision_revert_confirm', 1), + 'access callback' => '_revisioning_access_node_revision', + 'access arguments' => array('revert revisions', 1), + 'file' => 'node.pages.inc', + 'file path' => drupal_get_path('module', 'node'), + 'type' => MENU_LOCAL_TASK, + 'weight' => -2, + ); + // Delete revision local subtask. + $items['node/%node/revisions/%vid/delete'] = array( + 'title' => 'Delete', + 'load arguments' => array(3), + 'page callback' => 'drupal_get_form', + 'page arguments' => array('node_revision_delete_confirm', 1), + 'access callback' => '_revisioning_access_node_revision', + 'access arguments' => array('delete revisions', 1), + 'file' => 'node.pages.inc', + 'file path' => drupal_get_path('module', 'node'), + 'type' => MENU_LOCAL_TASK, + 'weight' => 10, + ); + + // If Diff module is enabled, provide a "Compare to current" local subtask. + if (module_exists('diff')) { + $items['node/%node/revisions/%vid/compare'] = array( + 'title' => 'Compare to current', + 'load arguments' => array(3), + 'page callback' => '_revisioning_compare_to_current_revision', + 'page arguments' => array(1), + 'access callback' => '_revisioning_access_node_revision', + 'access arguments' => array('compare to current', 1), + 'type' => MENU_LOCAL_TASK, + 'weight' => 0, + // 'tab_parent' => 'node/%/revisions', + ); + } + return $items; +} + +/** + * Implements hook_menu_alter(). + * + * Modify menu items defined in other modules (in particular the Node module). + */ +function revisioning_menu_alter(&$items) { + + // Change to access callbacks for existing node paths so that we properly + // control revision-related operation. + // Some also have their page callbacks altered, e.g to load the latest + // rather than the current revision of a node. + // Can't change node load function to, say nid_load(), as we'll run into + // trouble elsewhere, e.g. menu_get_object(), due to the fact that the + // prefix, e.g. '%nid', is meant to be a type name, i.e. '%node'. + // + // Alter the 3 primary node page tabs: View tab, Edit tab, Revisions tab ... + $items['node/%node']['access callback'] = '_revisioning_view_edit_access_callback'; + $items['node/%node']['access arguments'] = array('view', 1); + $items['node/%node']['page callback'] = '_revisioning_view'; + $items['node/%node']['page arguments'] = array(1); + // This is the MENU_DEFAULT_LOCAL_TASK, so inherits the above. + $items['node/%node/view']['title callback'] = '_revisioning_title_for_tab'; + $items['node/%node/view']['title arguments'] = array(1, 'view'); + $items['node/%node/view']['weight'] = -10; + + $items['node/%node/edit']['access callback'] = '_revisioning_view_edit_access_callback'; + $items['node/%node/edit']['access arguments'] = array('edit', 1); + $items['node/%node/edit']['page callback'] = '_revisioning_edit'; + $items['node/%node/edit']['page arguments'] = array(1); + $items['node/%node/edit']['title callback'] = '_revisioning_title_for_tab'; + $items['node/%node/edit']['title arguments'] = array(1, 'edit'); + + // 'Revisions' tab remains, but points to new page callback, allowing users + // to pick the revision to view, edit, publish, revert, unpublish, delete. + // Need to override _node_revision_access() call back as it disallows access + // to the 'Revisions' tab when there's only one revision, which will prevent + // users from getting to the publish/unpublish links. + $items['node/%node/revisions']['access callback'] = '_revisioning_access_node_revision'; + $items['node/%node/revisions']['access arguments'] = array('view revision list', 1); + $items['node/%node/revisions']['page callback'] = 'revisioning_node_overview'; + $items['node/%node/revisions']['page arguments'] = array(1); + $items['node/%node/revisions']['title callback'] = '_revisioning_title_for_tab'; + $items['node/%node/revisions']['title arguments'] = array(1, 'revisions'); + + // Remove the node.module links as we defined our own versions, using %vid + unset($items['node/%node/revisions/%/view']); + unset($items['node/%node/revisions/%/revert']); + unset($items['node/%node/revisions/%/delete']); + + if (module_exists('diff')) { + // If Diff module is enabled, make sure it uses correct access callback. + $items['node/%node/revisions/view']['access callback'] = '_revisioning_access_node_revision'; + $items['node/%node/revisions/view']['access arguments'] = array('view revisions', 1); + } +} + +/** + * Perform path manipulations for menu items containing the %vid wildcard. + * + * For example the ones from revisioning_menu(). + * @see http://drupal.org/node/500864 + */ +function vid_to_arg($arg, &$map, $index) { + if (empty($arg)) { + // For e.g. node/%/revisions. + // Suppresses subtabs of Revisions tab where %vid is omitted. + $map = array(); + } + return $arg; +} + +/** + * Implements hook_node_load(). + * + * The same load op may occur multiple times during the same HTTP request, so + * hooray for caching! + * + * hook_node_load is called when viewing a single node + * node_load() -> node_load_multiple() -> + * DrupalDefaultEntityController->attachLoad() + * + * hook_node_load is also called on the /content summary page: + * node_admin_nodes() -> node_load_multiple() -> + * DrupalDefaultEntityController->attachLoad() + * + * We do nothing in this 2nd case. + */ +function revisioning_node_load($nodes, $types) { + // The 'taxonomy/term/%' menu callback taxonomy_term_page() selects nodes + // based on presence of their nids in the {taxonomy_index} table, which is + // mainly based on publication status. Revisioning also updates the table for + // unpublished content so that in Views we can see the terms belonging to + // published as well as unpublished content. As a result we must re-apply + // access control when taxonomy feeds are displayed. + // See also revisioning_update_taxonomy_index(). + // + $double_check_access = (strpos($_GET['q'], 'taxonomy/term') === 0) && + variable_get('revisioning_in_views_show_unpublished_content_terms', TRUE); + + // At this point status, comment, promote and sticky have been set on all of + // the $nodes according to the {node_revision} table (not the {node} table), + // using {node.vid} as the foreign key into {node_revision}. + $nodes_to_be_fixed = array(); + foreach ($nodes as $nid => $node) { + if ($double_check_access && !node_access('view', $node)) { + // At this point we cannot remove the node object from $nodes, + // but we can set a flag to be checked in a later hook. + $node->dont_display = TRUE; + } + else { + revisioning_set_node_revision_info($node); + if (!empty($node->revision_moderation) && !empty($node->is_current)) { + // Hack! + // Because of core issue [#1120272/#542290], if the current revision is + // loaded, $node fields may in fact be those belonging to LATEST + // revision. + // So reload with FIELD_LOAD_REVISION. We can rely on $node->vid, that + // attribute is set correctly. + // Make sure to unset the already loaded fields or we end up with 2 + // copies of each field, e.g. 2 bodies, 2 tags, 2 image attachments etc. + list($nid, $vid, $bundle) = entity_extract_ids('node', $node); + $instances = _field_invoke_get_instances('node', $bundle, array('deleted' => FALSE)); + foreach ($instances as $instance) { + $field_name = $instance['field_name']; + unset($node->{$field_name}); + } + $nodes_to_be_fixed[$nid] = $node; + } + } + } + if (!empty($nodes_to_be_fixed)) { + field_attach_load_revision('node', $nodes_to_be_fixed); + foreach ($nodes_to_be_fixed as $nid => $node) { + $nodes[$nid] = $node; + } + } +} + +/** + * Implements hook_entity_prepare_view(). + * + * First of the dont_display hooks. + */ +function revisioning_entity_prepare_view($entities, $entity_type, $langcode) { + if ($entity_type == 'node') { + foreach ($entities as $node) { + if (!empty($node->dont_display)) { + $node->title = FALSE; + // This == COMMENT_NODE_HIDDEN if Comment module enabled. + $node->comment = 0; + $node->link = FALSE; + unset($node->body); + unset($node->rss_elements); + } + else { + // Display, however suppress comment form when revision is not current. + if (isset($node->comment) && !empty($node->revision_moderation) && empty($node->is_current)) { + // Prevent comment_node_view() from adding the comment form. + // This == COMMENT_NODE_HIDDEN; + $node->comment = 0; + } + } + } + } +} + +/** + * Implements hook_field_attach_view_alter(). + */ +function revisioning_field_attach_view_alter(&$output, $context) { + if ($context['entity_type'] == 'node' && !empty($context['entity']->dont_display)) { + $output = array(); + } +} + +/** + * Implements hook_node_view(). + */ +function revisioning_node_view($node, $view_mode, $langcode) { + if (!empty($node->dont_display)) { + // Suppress "Read more". + $node->content = array(); + } +} + +/** + * Output a status message, provided teh user has the required permission. + * + * @param string $message + * The status message to be displayed. + */ +function revisioning_set_status_message($message) { + if (user_access('view revision status messages')) { + drupal_set_message(filter_xss($message), 'status'); + } +} + +/** + * Implements hook_node_prepare(). + * + * Called when presenting edit form. + */ +function revisioning_node_prepare($node) { + if (!empty($node->nid)) { + $count = _revisioning_get_number_of_revisions_newer_than($node->vid, $node->nid); + if ($count == 1) { + drupal_set_message(t('Please note there is one revision more recent than the one you are about to edit.'), 'warning'); + } + elseif ($count > 1) { + drupal_set_message(t('Please note there are @count revisions more recent than the one you are about to edit.', + array('@count' => $count)), 'warning'); + } + } +} + +/** + * Implements hook_node_presave(). + * + * Called when saving, be it an edit or when creating a node. + * + * Note that the following may be set programmatically on the $node object + * before calling node_save($node): + * + * o $node->revision_operation, one of: + * + * REVISIONING_NO_REVISION + * ($node->revision == $node->revision_moderation == FALSE) + * + * REVISIONING_NEW_REVISION_NO_MODERATION + * ($node->revision == TRUE, $node->revision_moderation == FALSE) + * + * REVISIONING_NEW_REVISION_WITH_MODERATION + * ($node->revision == $node->revision_moderation == TRUE) + * + * o $node->revision_condition (applies only to NEW_REVISION_WITH_MODERATION): + * + * REVISIONING_NEW_REVISION_EVERY_SAVE + * REVISIONING_NEW_REVISION_WHEN_NOT_PENDING + */ +function revisioning_node_presave($node) { + revisioning_set_node_revision_info($node); + + if (isset($node->revision_operation)) { + $node->revision = ($node->revision_operation > REVISIONING_NO_REVISION); + $node->revision_moderation = ($node->revision_operation == REVISIONING_NEW_REVISION_WITH_MODERATION); + } + + if (!empty($node->revision_moderation) && revisioning_user_may_auto_publish($node)) { + revisioning_set_status_message(t('Auto-publishing this revision.')); + // Follow the default saving process making this revision current and + // published, as opposed to pending. + unset($node->revision_moderation); + // This is not required for correct operation, as a revision becomes + // pending based on vid > current_revision_id. But it looks less confusing, + // when the "Published" box is in sync with the moderation radio buttons. + $node->status = NODE_PUBLISHED; + $node->auto_publish = TRUE; + } + + if (!isset($node->nid)) { + // New node, if moderated without Auto-publish, ignore the default Publish + // tickbox. + if (isset($node->revision_moderation) && $node->revision_moderation == TRUE) { + $node->status = NODE_NOT_PUBLISHED; + } + // Set these for Rules, see [#1627400] + $node->current_status = $node->status; + $node->current_title = $node->title; + $node->current_promote = $node->promote; + $node->current_sticky = $node->sticky; + $node->current_comment = isset($node->comment) ? $node->comment : 0; + return; + } + + if (!empty($node->revision_moderation) /* || !empty($auto_publish) */) { + // May want to do this for auto_publish too, to provide $node->current... to + // other modules, as a courtesy. + if (!isset($node->revision_condition) && !empty($node->revision) && !empty($node->is_pending) + && variable_get('new_revisions_' . $node->type, REVISIONING_NEW_REVISION_WHEN_NOT_PENDING) == REVISIONING_NEW_REVISION_WHEN_NOT_PENDING) { + revisioning_set_status_message(t('Updating existing draft, not creating new revision as this one is still pending.')); + // To tell revisioning_node_update(). + $node->revision_condition = REVISIONING_NEW_REVISION_WHEN_NOT_PENDING; + } + if (isset($node->revision_condition)) { + // Tell node_save() whether a new revision should be created. + $node->revision = ($node->revision_condition == REVISIONING_NEW_REVISION_EVERY_SAVE); + } + + $result = db_query("SELECT status, title, comment, promote, sticky FROM {node_revision} WHERE vid = :vid", + array(':vid' => $node->current_revision_id)); + $current_revision = $result->fetchObject(); + // Copy from {node_revision} the field values replicated on {node} before + // handing back to node_save(). This is a side-effect of core D7's somewhat + // "sick" table denormalisation. + // If the Scheduler module is used take the status from there. See + // revisioning_scheduler_api(). + $node->current_status = isset($node->scheduled_status) ? $node->scheduled_status : $current_revision->status; + $node->current_title = $current_revision->title; + $node->current_promote = $current_revision->promote; + $node->current_sticky = $current_revision->sticky; + $node->current_comment = isset($current_revision->comment) ? $current_revision->comment : 0; + } +} + +/** + * Implements hook_node_update(). + * + * Note: $node->revision_moderation and $node->revision_condition may be set + * programmatically prior to calling node_save(). + * See also: revisioning_node_pre_save(). + */ +function revisioning_node_update($node) { + + revisioning_update_taxonomy_index($node, variable_get('revisioning_in_views_show_unpublished_content_terms', TRUE)); + + // Check whether the node has a current_status property and update the + // node->status to allow node_save()'s call to node_access_acquire_grants() + // to create the correct node_access entry for the current_status. + if (isset($node->current_status)) { + $node->status = $node->current_status; + } + + if (!empty($node->revision_moderation) && (isset($node->revision_condition) || !empty($node->revision))) { + // Enter when revision moderation is on and revision_condition=0,1 + // Have to do this due to D7's "sick" denormalisation of node revision data. + // Resetting the fields duplicated from new {node_revision} back to their + // originial values to match the current revision as opposed to the latest + // revision. The latter is done by node_save() just before it calls this + // function. + // By resetting {node.vid} {node.vid} < {node_revision.vid}, which makes + // the newly created revision a pending revision in Revisioning's books. + // Note: cannot use $node->old_vid as set by node_save(), as this refers to + // the revision edited, which may not be the current, which is what we are + // after here. + db_update('node') + ->fields(array( + 'vid' => $node->current_revision_id, + 'status' => $node->current_status, + 'title' => $node->current_title, + // In case Comment module enabled. + 'comment' => $node->current_comment, + 'promote' => $node->current_promote, + 'sticky' => $node->current_sticky)) + ->condition('nid', $node->nid) + ->execute(); + } + // Generate a 'post update' event in Rules. + module_invoke_all('revisionapi', 'post update', $node); + + // Add new revision usage records to files to prevent them being deleted. + $fields = field_info_instances('node', $node->type); + foreach ($fields as $field_name => $value) { + $field_info = field_info_field($field_name); + if ($field_info['type'] == 'file' || $field_info['type'] == 'image') { + // See #1996412. + $file_fields[$field_name] = $value; + } + } + // Create file revision entries for files created using older versions. + // $old_node = isset($node->original) ? $node->original : NULL; + // [#2276657] + $old_node = isset($node->original) && !empty($node->original) ? $node->original : NULL; + if (isset($old_node) && !empty($file_fields)) { + foreach ($file_fields as $file_field) { + if ($old_files = field_get_items('node', $old_node, $file_field['field_name'], $old_node->language)) { + foreach ($old_files as $old_single_file) { + if (!empty($old_single_file)) { + $old_file = (object) $old_single_file; + file_usage_add($old_file, 'revisioning', 'revision', $old_node->vid); + } + } + } + } + } + if (!empty($file_fields)) { + foreach ($file_fields as $file_field) { + if ($files = field_get_items('node', $node, $file_field['field_name'], $node->language)) { + foreach ($files as $single_file) { + $file = (object) $single_file; + file_usage_add($file, 'revisioning', 'revision', $node->vid); + } + } + } + } +} + +/** + * Implements hook_node_insert(). + * + * New node. + */ +function revisioning_node_insert($node) { + + revisioning_update_taxonomy_index($node, variable_get('revisioning_in_views_show_unpublished_content_terms', TRUE)); + + if (!empty($node->revision_moderation)) { + revisioning_set_status_message($node->status ? t('Initial revision created and published.') : t('Initial draft created, pending publication.')); + } + + // Add revision usage records to files to prevent them being deleted. + $fields = field_info_instances('node', $node->type); + foreach ($fields as $field_name => $value) { + $field_info = field_info_field($field_name); + if ($field_info['type'] == 'file') { + $file_fields[$field_name] = $value; + } + } + if (!empty($file_fields)) { + foreach ($file_fields as $file_field) { + if ($files = field_get_items('node', $node, $file_field['field_name'], $node->language)) { + foreach ($files as $single_file) { + $file = (object) $single_file; + file_usage_add($file, 'revisioning', 'revision', $node->vid); + } + } + } + } +} + +/** + * Implements hook_node_delete(). + */ +function revisioning_node_delete($node) { + if ($revisions = node_revision_list($node)) { + $vids = array_keys($revisions); + db_delete('file_usage')->condition('module', 'revisioning')->condition('id', $vids, 'IN')->execute(); + } +} + +/** + * Implements hook_node_access_records_alter(). + * + * If the node is not the current node this function clears the grants array and + * rebuilds it using the current node. + */ +function revisioning_node_access_records_alter(&$grants, $node) { + + if (!revisioning_revision_is_current($node)) { + $current_node = node_load($node->nid, NULL, TRUE); + $grants = array(); + foreach (module_implements('node_access_records') as $module) { + $function = $module . '_node_access_records'; + if (function_exists($function)) { + $result = call_user_func_array($function, array($current_node)); + if (isset($result)) { + if (is_array($result)) { + $grants = array_merge_recursive($grants, $result); + } + else { + $grants[] = $result; + } + } + } + } + } +} + +/** + * Implements hook_views_api(). + */ +function revisioning_views_api() { + return array( + 'api' => views_api_version(), + 'path' => drupal_get_path('module', 'revisioning') . '/views', + ); +} + +/** + * Implements hook_user_node_access(). + */ +function revisioning_user_node_access($revision_op, $node, $account = NULL) { + if (!isset($account)) { + $account = $GLOBALS['user']; + } + + $type = check_plain($node->type); + + switch ($revision_op) { + case 'view current': + break; + + case 'compare to current': + case 'view revisions': + case 'view revision list': + if (user_access('view revisions', $account)) { + break; + } + if (user_access('view revisions of any ' . $type . ' content', $account)) { + break; + } + if (($node->uid == $account->uid) && user_access('view revisions of own ' . $type . ' content', $account)) { + break; + } + return FALSE; + + case 'edit current': + return 'update'; + + case 'edit revisions': + case 'revert revisions': + return user_access($revision_op, $account) ? 'update' : FALSE; + + case 'publish revisions': + $node_op = variable_get('revisioning_require_update_to_publish', TRUE) ? 'update' : 'view'; + if (user_access('publish revisions', $account)) { + return $node_op; + } + if (user_access('publish revisions of any ' . $type . ' content', $account)) { + return $node_op; + } + if (($node->uid == $account->uid) && user_access('publish revisions of own ' . $type . ' content', $account)) { + return $node_op; + } + return FALSE; + + case 'unpublish current revision': + $node_op = variable_get('revisioning_require_update_to_publish', TRUE) ? 'update' : 'view'; + return user_access('unpublish current revision', $account) ? $node_op : FALSE; + + case 'delete revisions': + case 'delete archived revisions': + if (!user_access('delete revisions', $account)) { + return FALSE; + } + case 'delete node': + return 'delete'; + + default: + drupal_set_message(t("Unknown Revisioning operation '%revision_op'. Treating as 'view'.", + array('%revision_op' => $revision_op)), 'warning', FALSE); + } + return 'view'; +} + +/** + * Implements hook_scheduler_api(). + */ +function revisioning_scheduler_api($node, $action) { + if ($action == 'pre_publish') { + $node->scheduled_status = NODE_PUBLISHED; + } + elseif ($action == 'pre_unpublish') { + $node->scheduled_status = NODE_NOT_PUBLISHED; + } +} + +/** + * Test whether the supplied revision operation is appropriate for the node. + * + * This is irrespective of user permissions, e.g. even for an administrator it + * doesn't make sense to publish a node that is already published or to + * "revert" to the current revision. + * + * @param string $revision_op + * For instance 'publish revisions', 'delete revisions' + * @param object $node + * The node object + * + * @return bool + * TRUE if the operation is appropriate for this node at this point + */ +function _revisioning_operation_appropriate($revision_op, $node) { + + switch ($revision_op) { + + case 'compare to current': + // Can't compare against itself. + case 'delete revisions': + // If the revision is the current one, suppress the delete operation + // @TODO ...unless it's the only revision, in which case delete the + // entire node; however this requires a different URL. + return !$node->is_current; + + case 'delete archived revisions': + break; + + case 'view revision list': + // For i.e. node revisions summary. + if (empty($node->revision_moderation) && isset($node->num_revisions) && $node->num_revisions == 1) { + // Suppress Revisions tab when when there's only 1 revision. This is + // consistent with core. + // However, when content is moderated (i.e. "New revision in draft, + // pending moderation" is ticked) we want to be able to get to the + // 'Unpublish current' link on this page and the 'Publish' tab on + // the next. + return FALSE; + } + break; + + case 'edit revisions': + if (empty($node->revision_moderation) /* && !$node->is_current*/) { + return FALSE; + } + break; + + case 'publish revisions': + // If the node isn't meant to be moderated, + // or the revision is not either pending or current but not published, + // then disallow publication. + if (empty($node->revision_moderation) || + !($node->is_pending || ($node->is_current && !$node->status))) { + return FALSE; + } + break; + + case 'unpublish current revision': + // If the node isn't meant to be moderated or it is unpublished already + // or we're not looking at the current revision, then unpublish is not an + // option. + if (empty($node->revision_moderation) || !$node->status || !$node->is_current) { + return FALSE; + } + break; + + case 'revert revisions': + // If this revision is pending or current, suppress the reversion. + if ($node->is_pending || $node->is_current) { + return FALSE; + } + break; + } + return TRUE; +} + +/** + * Determine whether the supplied revision operation is permitted on the node. + * + * This requires getting through three levels of defence: + * o Is the operation appropriate for this node at this time, e.g. a node must + * not be published if it already is or if it isn't under moderation control + * o Does the user have permission for the requested REVISION operation? + * o Does the user have the NODE access rights (view/update/delete) for this + * operation? + * + * @param string $revision_op + * For instance 'publish revisions', 'delete revisions' + * @param object $node + * The node object + * + * @return bool + * TRUE if the user has access + */ +function _revisioning_access_node_revision($revision_op, $node) { + + if (!_revisioning_operation_appropriate($revision_op, $node)) { + return FALSE; + } + // Check the revision-aspect of the operation. + $node_op = revisioning_user_node_access($revision_op, $node); + // ... then check with core to assess node permissions + // node_access will invoke hook_node_access(), i.e. revisioning_node_access(). + $access = $node_op && node_access($node_op, $node); + + // Let other modules override the outcome, if there are any. + // If any module denies access that is the final result, otherwise allow. + $overrides = module_invoke_all('revisioning_access_node_revision', $revision_op, $node); + return empty($overrides) ? $access : !in_array(NODE_ACCESS_DENY, $overrides, TRUE); +} + +/** + * Implements hook_node_access(). + * + * This gets invoked from node.module/node_access() after it has checked the + * standard node permissions using node_node_access() and just before it checks + * the node_access grants table. + * We basically return "don't care" except for one 'view' case, which replicates + * the node.module. "Don't care" in this case would result in "access denied". + */ +function revisioning_node_access($node, $node_op, $account) { + // Taken from node.module/node_access(): + // If no modules implement hook_node_grants(), the default behaviour is to + // allow all users to view published nodes, so reflect that here, + // augmented for the 'view revisions' family of permissions, which apply to + // both published and unpublished nodes. + if ($node_op == 'view' && !module_implements('node_grants')) { + if ($node->status == NODE_PUBLISHED || (!empty($node->revision_moderation) && revisioning_user_node_access('view revisions', $node, $account))) { + return NODE_ACCESS_ALLOW; + } + } + // [#1492246] + // Deny access to unpublished, moderated content by anonymous users. + if (empty($node->status) && !empty($node->revision_moderation) && empty($account->uid)) { + return NODE_ACCESS_DENY; + } + return NODE_ACCESS_IGNORE; +} + +/** + * Access callback function. + * + * Access callback for 'node/%', 'node/%/view' and 'node/%/edit' links that + * may appear anywhere on the site. + * At the time that this function is called the CURRENT revision will already + * have been loaded by the system. However depending on the value of the + * 'revisioning_view_callback' and 'revisioning_edit_callback' variables (as + * set on the admin/config/content/revisioning page), this may not be the + * desired revision. + * If these variables state that the LATEST revision should be loaded, we need + * to check at this point whether the user has permission to view this revision. + * + * The 'View current' and/or 'Edit current' tabs are suppressed when the current + * revision is already displayed via one of the Revisions subtabs. + * The 'View latest' and/or 'Edit latest' tabs are suppressed when the latest + * revision is already displayed via one of the Revisions subtabs. + * + * @param string $op + * must be one of 'view' or 'edit' + * @param object $node + * the node object + * + * @return bool + * FALSE if access to the desired revision is denied + */ +function _revisioning_view_edit_access_callback($op, $node) { + + $load_op = _revisioning_load_op($node, $op); + + $vid = arg(3); + if (!empty($node->revision_moderation) && is_numeric($vid)) { + // The View, Edit primary tabs are requested indirectly, in the context of + // the secondary tabs under Revisions, e.g. node/%/revisions/% + if ($load_op == REVISIONING_LOAD_CURRENT && $vid == $node->current_revision_id) { + // Suppress 'View current' and 'Edit current' tabs when viewing current. + return FALSE; + } + if ($load_op == REVISIONING_LOAD_LATEST && $vid == revisioning_get_latest_revision_id($node->nid)) { + // Suppress 'View latest' and 'Edit latest' tabs when viewing latest. + return FALSE; + } + } + if ($load_op == REVISIONING_LOAD_LATEST) { + // _revisioning_load_op has already checked permission to view latest. + return TRUE; + } + $revision_op = ($op == 'view') ? 'view current' : 'edit current'; + return _revisioning_access_node_revision($revision_op, $node); +} + +/** + * Load a revision. + * + * Assuming that the node passed in is the current revision (core default), + * this function determines whether the lastest revision should be loaded + * instead, in which case it returns REVISIONING_LOAD_LATEST. + * + * @param object $node + * only nodes of content types subject to moderation are + * processed by this function + * @param string $op + * either 'edit' or 'view' + * @param bool $check_access + * whether revision access permissions should be checked; if the user has no + * permission to load the latest revisions, then the function returns + * REVISIONING_LOAD_CURRENT + * + * @return int + * REVISIONING_LOAD_LATEST or REVISIONING_LOAD_CURRENT + */ +function _revisioning_load_op($node, $op, $check_access = TRUE) { + if (!empty($node->revision_moderation)) { + $view_mode = (int) variable_get('revisioning_view_callback', REVISIONING_LOAD_CURRENT); + $edit_mode = (int) variable_get('revisioning_edit_callback', REVISIONING_LOAD_CURRENT); + $load_op = ($op == 'edit') ? $edit_mode : $view_mode; + if ($load_op == REVISIONING_LOAD_LATEST) { + // Site is configured to load latest revision, but we'll only do this if + // the latest isn't loaded already and user has the permission to do so. + $latest_vid = revisioning_get_latest_revision_id($node->nid); + if ($latest_vid != $node->current_revision_id) { + if (!$check_access) { + return REVISIONING_LOAD_LATEST; + } + $original_vid = $node->vid; + $node->vid = $latest_vid; + $node->is_current = revisioning_revision_is_current($node); + $revision_op = ($op == 'view') ? 'view revisions' : 'edit revisions'; + $access = _revisioning_access_node_revision($revision_op, $node); + // Restore $node (even though called by value), to remain consistent. + $node->vid = $original_vid; + $node->is_current = revisioning_revision_is_current($node); + if ($access) { + return REVISIONING_LOAD_LATEST; + } + } + } + } + return REVISIONING_LOAD_CURRENT; +} + +/** + * Display node overview. + * + * Display all revisions of the supplied node in a themed table with links for + * the permitted operations above it. + * + * @return array + * render array as returned by drupal_get_form() + */ +function revisioning_node_overview($node) { + return _revisioning_theme_revisions_summary($node); +} + +/** + * Menu callback for the primary View tab. + * + * This is the same callback as used in core, except that in core current and + * latest revisions are always the same. + */ +function _revisioning_view($node) { + if (_revisioning_load_op($node, 'view') == REVISIONING_LOAD_LATEST) { + $vid_to_load = revisioning_get_latest_revision_id($node->nid); + $node = node_load($node->nid, $vid_to_load); + } + // This is the callback used by node.module for node/%node & node/%node/view + return node_page_view($node); +} + +/** + * Callback for the primary Edit tab. + * + * This is the same callback as used in core, except that in core current and + * latest revisions are always the same. + */ +function _revisioning_edit($node) { + if (_revisioning_load_op($node, 'edit') == REVISIONING_LOAD_LATEST) { + $vid_to_load = revisioning_get_latest_revision_id($node->nid); + $node = node_load($node->nid, $vid_to_load); + } + _revisioning_set_custom_theme_if_necessary(); + // This is the callback used by node.module for node/%node/edit + return node_page_edit($node); +} + +/** + * Callback to view a particular revision. + */ +function _revisioning_view_revision($node) { + if (isset($node->nid)) { + /* For Panels: @todo test this thoroughly. See [#1567880] + $router_item = menu_get_item('node/' . $node->nid); + if (!empty($router_item['file'])) { + $path = $_SERVER['DOCUMENT_ROOT'] . base_path(); + require_once $path . $router_item['file']; + } + // Call whatever function is assigned to the main node path but pass the + // current node as an argument. This approach allows for the reuse of Panel + // definition acting on node/%node. + if (isset($router_item['page_callback'])) { + return $router_item['page_callback']($node); + }*/ + } + // This is the callback used by node.module for node/%node/revisions/%/view + return node_show($node, TRUE); +} + +/** + * Callback to edit a particular revision. + * + * Note that there is no equivalent of this in core and we should not allow + * editing of a non-current revision, if $node->revision_moderation is not set. + * This is the job of the access callback _revisioning_access_node_revision(). + */ +function _revisioning_edit_revision($node) { + _revisioning_set_custom_theme_if_necessary(); + return node_page_edit($node); +} + +/** + * Callback for the primary View, Edit and Revisions tabs titles. + * + * @param object $node + * the node object + * @param string $tab + * 'view', 'edit' or 'revisions' + * + * @return string + * translatable title string + */ +function _revisioning_title_for_tab($node, $tab) { + if ($tab == 'revisions') { + return is_numeric(arg(3)) ? t('Revision operations') : t('Revisions'); + } + /* + if (empty($node->revision_moderation) || $node->num_revisions <= 1) { + return ($tab == 'edit' ? t('Edit') : t('View')); + } + if (_revisioning_load_op($node, $tab) == REVISIONING_LOAD_LATEST) { + return ($tab == 'edit' ? t('Edit latest') : t('View latest')); + } + return ($tab == 'edit' ? t('Edit current') : t('View current')); + */ + if (!empty($node->revision_moderation) && $node->num_revisions > 1) { + if (_revisioning_load_op($node, $tab) == REVISIONING_LOAD_LATEST) { + return ($tab == 'edit' ? t('Edit latest') : t('View latest')); + } + if (_revisioning_access_node_revision('view revisions', $node)) { + return ($tab == 'edit' ? t('Edit current') : t('View current')); + } + } + return ($tab == 'edit' ? t('Edit') : t('View')); +} + +/** + * Set custom theme. + */ +function _revisioning_set_custom_theme_if_necessary() { + // Use the admin theme if the user specified this at Appearance >> Settings. + // Note: first tick 'View the administration theme' at People >> Permissions. + if (variable_get('node_admin_theme', FALSE)) { + global $theme, $custom_theme; + $custom_theme = variable_get('admin_theme', $theme); + } +} + +if (module_exists('diff')) { + /** + * Compare two revisions. + * + * Use diff's diff_diffs_show() function to compare specific revision to the + * current one. + */ + function _revisioning_compare_to_current_revision($node) { + // For diff_diffs_show(). + module_load_include('inc', 'diff', 'diff.pages'); + // Make sure that latest of the two revisions is on the right. + if ($node->current_revision_id < $node->vid) { + return diff_diffs_show($node, $node->current_revision_id, $node->vid); + } + return diff_diffs_show($node, $node->vid, $node->current_revision_id); + } +} + +/** + * Implements hook_page_manager_override(). + * + * See http://drupal.org/node/1509674#comment-6702798 + */ +function revisioning_page_manager_override($task_name) { + switch ($task_name) { + case 'node_view': + return '_revisioning_view'; + } +} diff --git a/sites/all/modules/revisioning/revisioning.pages.inc b/sites/all/modules/revisioning/revisioning.pages.inc new file mode 100644 index 0000000..cc92630 --- /dev/null +++ b/sites/all/modules/revisioning/revisioning.pages.inc @@ -0,0 +1,467 @@ +revision_moderation) ? $node->revision_moderation : revisioning_content_is_moderated($form['type']['#value'], $node); + + // Alter the Create/Edit content form, if subject to moderation. + if ($is_moderated_content) { + // "Create new revision" must be set when the node is to be moderated. + $node->revision = TRUE; + // Next line is not essential, just ensures form options are + // consistent with the edited content being subject to moderation. + $form['revision_information']['revision']['#default_value'] = TRUE; + // For moderated content "Published" box will be treated as unticked, + // See revisioning_node_presave(). + } + + // Only add this radio selector if user has the appropriate permissions. + // 'administer nodes' is required by default, but if configured + // appropriately, then any user premitted to publish this node will get the + // shortcut controls. + $add_radio_selector = variable_get('revisioning_publication_shortcuts', FALSE) ? + revisioning_user_node_access('publish revisions', $node) : + user_access('administer nodes'); + if ($add_radio_selector) { + // Expand and move this vertical tab to top, so that it's in user's face. + if (isset($form['menu'])) { + $form['menu']['#collapsed'] = TRUE; + } + $form['revision_information']['#collapsed'] = FALSE; + $form['revision_information']['#weight'] = -3; + + $options = array(); + if (isset($node->nid)) { + $options[REVISIONING_NO_REVISION] = t('Modify current revision, no moderation'); + } + $options[REVISIONING_NEW_REVISION_NO_MODERATION] = t('Create new revision, no moderation'); + $options[REVISIONING_NEW_REVISION_WITH_MODERATION] = t('Create new revision and moderate'); + + // This radio selection will appear in hook_node_presave as + // $node->revision_operation + $form['revision_information']['revision_operation'] = array( + '#title' => t('Revision creation and moderation options'), + '#description' => t('Moderation means that the new revision is not publicly visible until approved by someone with the appropriate permissions.'), + '#type' => 'radios', + '#options' => $options, + '#default_value' => isset($node->nid) + ? (int) $node->revision + (int) $is_moderated_content + : ($is_moderated_content ? REVISIONING_NEW_REVISION_WITH_MODERATION : REVISIONING_NEW_REVISION_NO_MODERATION), + ); + unset($form['revision_information']['revision']); + + // Add javascript to show/hide the "Published" checkbox if the user + // presses one of the first two radio buttons. Also updates summary tabs. + $js_file = drupal_get_path('module', 'revisioning') . '/js/revisioning-radios.js'; + // After node.js. + drupal_add_js($js_file, array('weight' => 1)); + if (variable_get('revisioning_no_moderation_by_default', FALSE)) { + $form['revision_information']['revision_operation']['#default_value'] = REVISIONING_NEW_REVISION_NO_MODERATION; + } + } + else { + // For non-admin don't show radios, just set default, hidden on form. + // Note that $form['revision_information']['revision'] is already set. + $form['revision_moderation'] = array( + '#type' => 'value', + '#value' => $is_moderated_content, + ); + } + + // In addition to node_form_submit() append our own handler to the list, so + // that we can redirect to the pending, as opposed to current, revision. + $form['actions']['submit']['#submit'][] = '_revisioning_form_submit'; + + if (isset($form['actions']['delete']) && isset($form['actions']['delete']['#type'])) { + $nid = $form['#node']->nid; + if (revisioning_get_number_of_revisions($nid) > 1) { + // Special treatment for Delete button when there are >= 2 revisions. + if ($form['#node']->vid == revisioning_get_current_node_revision_id($nid)) { + // Make it obvious to user that a 'Delete' is in fact 'Delete all'. + $form['actions']['delete']['#value'] = t('Delete (all revisions)'); + } + elseif (user_access('delete revisions')) { + // Change the meaning of the 'Delete' button when editing a revision + // to be the deletion of the viewed revision, rather than the node + // node. + $form['actions']['delete']['#value'] = t('Delete this revision'); + $form['actions']['delete']['#submit'][] = '_revisioning_delete_submit'; + } + } + } + } +} + +/** + * Implements hook_form_FORM_ID_form_alter(). + * + * On the content type edit form, add the "New revision in draft, pending + * moderation" tick-box and a couple of radio-boxes to select the new revision + * and auto-publish policies. + */ +function revisioning_form_node_type_form_alter(&$form, &$form_state) { + $form['workflow']['#collapsed'] = FALSE; + $form['workflow']['node_options']['#options']['revision_moderation'] = t('New revision in draft, pending moderation (requires "Create new revision")'); + + $form['workflow']['revisioning'] = array( + '#type' => 'fieldset', + '#title' => t('New revision in draft'), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + ); + $content_type = $form['#node_type']->type; + $form['workflow']['revisioning']['new_revisions'] = array( + '#title' => t('Create new revision:'), + '#type' => 'radios', + '#options' => array( + REVISIONING_NEW_REVISION_WHEN_NOT_PENDING => t('Only when saving %type content that is not already in draft/pending moderation', array('%type' => $content_type)), + REVISIONING_NEW_REVISION_EVERY_SAVE => t('Every time %type content is updated, even when saving content in draft/pending moderation', array('%type' => $content_type))), + '#default_value' => (int) variable_get('new_revisions_' . $content_type, REVISIONING_NEW_REVISION_WHEN_NOT_PENDING), + '#description' => t('Use less disk space and avoid cluttering your revisions list. With the first option ticked, modifications are saved to the same copy (i.e. no additional revisions are created) until the content is published.'), + ); + $form['workflow']['revisioning']['revisioning_auto_publish'] = array( + '#title' => t('Auto-publish drafts of type %type (for moderators)', array('%type' => $content_type)), + '#type' => 'checkbox', + '#default_value' => (int) variable_get('revisioning_auto_publish_' . $content_type, FALSE), + '#description' => t('If this box is ticked and the user has one of the "Publish content revisions" permissions, then any draft of type %type is published immediately upon saving, without further review or the option to schedule a publication date.', array('%type' => $content_type)), + ); +} + +/** + * Return a confirmation page for publishing a revision. + */ +function revisioning_publish_confirm($form, &$form_state, $node) { + $form['node_id'] = array('#type' => 'value', '#value' => $node->nid); + $form['title'] = array('#type' => 'value', '#value' => $node->title); + $form['revision'] = array('#type' => 'value', '#value' => $node->vid); + $form['type'] = array('#type' => 'value', '#value' => $node->type); + return confirm_form($form, + t('Are you sure you want to publish this revision of %title?', array('%title' => $node->title)), + 'node/' . $node->nid . '/revisions', + t('Publishing this revision will make it visible to the public.'), + t('Publish'), t('Cancel')); +} + +/** + * Submission handler for the publish_confirm form. + */ +function revisioning_publish_confirm_submit($form, &$form_state) { + $nid = $form_state['values']['node_id']; + $vid = $form_state['values']['revision']; + $node = node_load($nid, $vid); + _revisioning_publish_revision($node); + revisioning_set_status_message(t('Revision has been published.')); + // Redirect to the same page as unpublish and revert. + $form_state['redirect'] = "node/$nid/revisions"; +} + +/** + * Return a confirmation page for unpublishing the node. + */ +function revisioning_unpublish_confirm($form, &$form_state, $node) { + $form['node_id'] = array('#type' => 'value', '#value' => $node->nid); + $form['title'] = array('#type' => 'value', '#value' => $node->title); + $form['type'] = array('#type' => 'value', '#value' => $node->type); + return confirm_form($form, + t('Are you sure you want to unpublish %title?', array('%title' => $node->title)), + "node/$node->nid/revisions", + t('Unpublishing will remove this content from public view.'), + t('Unpublish'), t('Cancel')); +} + +/** + * Submission handler for the unpublish_confirm form. + */ +function revisioning_unpublish_confirm_submit($form, &$form_state) { + $nid = $form_state['values']['node_id']; + _revisioning_unpublish_revision($nid); + $title = $form_state['values']['title']; + revisioning_set_status_message(t('%title is no longer publicly visible.', array('%title' => $title))); + // Redirect to the same page as publish and revert. + $form_state['redirect'] = "node/$nid/revisions"; +} + +/** + * Return a confirmation page for deleting archived revisione. + */ +function revisioning_delete_archived_confirm($form, &$form_state, $node) { + $node->num_archived = revisioning_get_number_of_archived_revisions($node); + $form['node'] = array('#type' => 'value', '#value' => $node); + $t = format_plural($node->num_archived, + 'Are you sure you want to delete the archived revision of %title?', + 'Are you sure you want to delete all @count archived revisions of %title?', + array('%title' => $node->title) + ); + return confirm_form($form, $t, 'node/' . $node->nid . '/revisions', + t('This action cannot be undone.'), + t('Delete archived'), t('Cancel')); +} + +/** + * Submission handler for the delete_archived_confirm form. + */ +function revisioning_delete_archived_confirm_submit($form, &$form_state) { + $node = $form_state['values']['node']; + revisioning_delete_archived_revisions($node); + $t = format_plural($node->num_archived, 'One archived revision deleted.', '@count archived revisions deleted.'); + revisioning_set_status_message($t); + $form_state['redirect'] = ($node->num_revisions - $node->num_archived > 1) ? 'node/' . $node->nid . '/revisions' : 'node/' . $node->nid; +} + +/** + * Implements hook_form_FORM_ID_alter(). + * + * @see node.pages.inc/node_revision_revert_confirm() + */ +function revisioning_form_node_revision_revert_confirm_alter(&$form, &$form_state) { + $node = $form['#node_revision']; + if (_revisioning_get_number_of_pending_revisions($node->nid) > 0) { + drupal_set_message(t('There is a pending revision. Are you sure you want to revert to an archived revision?'), 'warning'); + } + array_unshift($form['#submit'], 'revisioning_revert_confirm_pre_submit'); + $form['#submit'][] = 'revisioning_revert_confirm_post_submit'; +} + +/** + * Implements hook_form_FORM_ID_alter(). + * + * We only add "pre" submit handler, because "post delete" event is already + * available via hook_nodeapi(). + * + * @see node_revision_delete_confirm() + */ +function revisioning_form_node_revision_delete_confirm_alter(&$form, &$form_state) { + array_unshift($form['#submit'], 'revisioning_revision_delete_confirm_pre_submit'); +} + +/** + * Submission "pre" handler for the node_revision_delete_confirm form. + * + * Runs BEFORE the existing delete function in node.pages.inc + */ +function revisioning_revision_delete_confirm_pre_submit($form, &$form_state) { + $node = $form['#node_revision']; + module_invoke_all('revisionapi', 'pre delete', $node); +} + +/** + * Submission "pre" handler the revert_confirm form. + * + * Runs BEFORE the existing revert function in node.pages.inc + */ +function revisioning_revert_confirm_pre_submit($form, &$form_state) { + $node = $form['#node_revision']; + $return = module_invoke_all('revisionapi', 'pre revert', $node); + if (in_array(FALSE, $return)) { + drupal_goto('node/' . $node->nid . '/revisions/' . $node->vid . '/view'); + } +} + +/** + * Submission "post" handler for the revert_confirm form. + * + * Runs AFTER the existing revert function in node.pages.inc + * + * Note: + * It would be nice if publish and revert were symmetrical operations and that + * node_revision_revert_confirm_submit didn't save a physical copy of the + * revision (under a new vid), as this has the side-effect of making all + * "pending" revisions "archived". This is because the definition of "pending" + * is: "node_vid > current_vid". + * It would be better if "pending" relied on a separate flag rather than a field + * such as vid or timestamp that changes every time a piece of code executes a + * node_save(). + */ +function revisioning_revert_confirm_post_submit($form, &$form_state) { + $node = $form['#node_revision']; + // _revisioning_publish_node($node->nid); [#611988] + module_invoke_all('revisionapi', 'post revert', $node); +} + + +/** + * Return as a themed table a list of nodes that have pending revisions. + * + * Also checks access rights of the logged-in user. + * + * @param string $access + * Operation, one of 'view', 'update' or 'delete'. + * @param int $user_filter + * One of NO_FILTER, I_CREATED or I_LAST_MODIFIED. + * + * @return string + * themed HTML + */ +function _revisioning_show_pending_nodes($access = 'view', $user_filter = NO_FILTER) { + $is_moderated = user_access('administer nodes') ? NO_FILTER : TRUE; + $content_summary = module_grants_monitor_accessible_content_summary($access, NO_FILTER, $user_filter, $is_moderated, TRUE); + if (user_access('view revision status messages') && strpos($content_summary, 'No content') === FALSE && !user_access('administer nodes')) { + _revisioning_set_info_message(); + } + return $content_summary; +} + +/** + * Set info message. + */ +function _revisioning_set_info_message() { + if (user_access('publish revisions')) { + $moderated_types = array(); + foreach (node_type_get_types() as $type) { + if (revisioning_content_is_moderated($type->type) && + (user_access('view revisions') || user_access('view revisions of any ' . $type->type . ' content'))) { + $moderated_types[] = $type->name; + } + } + if (count($moderated_types) > 0) { + drupal_set_message(t('You have permission to publish content revisions of type(s): %moderated_types.', + array('%moderated_types' => implode(', ', $moderated_types)))); + } + } +} + +/** + * Handler for the 'Save' button on the edit form. + * + * When saving a new revision we shouldn't redirect to "View current", as + * that's not the one we've saved. + */ +function _revisioning_form_submit($form, &$form_state) { + // Don't redirect when creating new node, when not moderated or user doesn't + // have access to the revision. + if (isset($form_state['node']->nid) && !empty($form_state['node']->revision_moderation) && _revisioning_access_node_revision('view revisions', $form_state['node'])) { + $form_state['redirect'] = 'node/' . $form_state['node']->nid . '/revisions/' . $form_state['node']->vid . '/view'; + } +} + +/** + * Handler for the 'Delete this revision' button on the edit form. + * + * Redirect to node/%/revisions/%/delete as opposed to node/%/delete + */ +function _revisioning_delete_submit(&$form, &$form_state) { + $form_state['redirect'][0] = 'node/' . $form['#node']->nid . '/revisions/' . $form['#node']->vid . '/delete'; +} + + +/** + * Implements hook_block_info(). + * + * A block that may be placed on selected pages, alerting the moderator when + * new content has been submitted for review. Shows titles of pending revisions + * as a series of links. Clicking a link takes the moderator straight to the + * revision in question. + */ +function revisioning_block_info() { + $block['pending']['info'] = t('Pending revisions'); + $block['pending']['cache'] = DRUPAL_NO_CACHE; + // Towards top of whatever region is chosen. + $block['pending']['weight'] = -10; + // Block is implemented by this module. + $block['pending']['custom'] = FALSE; + return $block; +} + +/** + * Implements hook_block_configure(). + */ +function revisioning_block_configure($delta = 'pending') { + $form['revisioning_block_num_pending'] = array( + '#type' => 'textfield', + '#title' => t('Maximum number of pending revisions displayed'), + '#default_value' => variable_get('revisioning_block_num_pending', 5), + '#description' => t('Note: the title of this block mentions the total number of revisions pending, which may be greater than the number of revisions displayed.'), + ); + $form['revisioning_block_order'] = array( + '#type' => 'radios', + '#title' => t('Order in which pending revisions are displayed'), + '#options' => array( + REVISIONING_REVISIONS_BLOCK_OLDEST_AT_TOP => t('Oldest at top'), + REVISIONING_REVISIONS_BLOCK_NEWEST_AT_TOP => t('Newest at top')), + '#default_value' => variable_get('revisioning_block_order', REVISIONING_REVISIONS_BLOCK_NEWEST_AT_TOP), + '#description' => t('Note: order is based on revision timestamps.'), + ); + $form['revisioning_content_summary_page'] = array( + '#type' => 'textfield', + '#title' => t('Page to go to when the block title is clicked'), + '#default_value' => variable_get('revisioning_content_summary_page', ''), + '#description' => t('When left blank this will default to either %view_content, if the Views module is enabled, or %admin_content, subject to permissions.
For any of this to work the above Block title field must be left blank.', array( + '%view_content' => 'content-summary', + '%admin_content' => 'admin/content')), + ); + return $form; +} + +/** + * Implements hook_block_save(). + */ +function revisioning_block_save($delta = '', $edit = array()) { + variable_set('revisioning_block_num_pending', (int) $edit['revisioning_block_num_pending']); + variable_set('revisioning_block_order', (int) $edit['revisioning_block_order']); + variable_set('revisioning_content_summary_page', $edit['revisioning_content_summary_page']); +} + +/** + * Implements hook_block_view(). + */ +function revisioning_block_view($delta = '') { + $order = variable_get('revisioning_block_order', REVISIONING_REVISIONS_BLOCK_NEWEST_AT_TOP) == REVISIONING_REVISIONS_BLOCK_NEWEST_AT_TOP ? 'DESC' : 'ASC'; + $revisions = revisioning_get_revisions('view revision list', NO_FILTER, NO_FILTER, NO_FILTER, TRUE, TRUE, 100, 'timestamp ' . $order); + if (!empty($revisions)) { + return _revisioning_block_pending_revisions_content($revisions); + } +} + +/** + * Define content for pending revisions block. + * + * @param array $revisions + * array of revision objects + * + * @return array + * the block array + */ +function _revisioning_block_pending_revisions_content($revisions) { + $num_revisions = count($revisions); + $max_num_shown = variable_get('revisioning_block_num_pending', 5); + $links = array(); + foreach (array_slice($revisions, 0, $max_num_shown) as $revision) { + // If they exist, should we show multiple pending revisions on same node? + $links[] = l($revision->title, "node/$revision->nid/revisions/$revision->vid/view"); + } + // Also loads /css/revisioning-rtl.css + drupal_add_css(drupal_get_path('module', 'revisioning') . '/css/revisioning.css'); + $title_link = trim(variable_get('revisioning_content_summary_page', '')); + $link_options = array(); + if (empty($title_link)) { + if (module_exists('views')) { + $title_link = 'content-summary'; + $link_options['query'] = array('revision_moderation' => 1, 'state' => 2); + } + elseif (user_access('access content overview')) { + $title_link = 'admin/content'; + } + } + $title = t('!num_revisions pending', array( + '!num_revisions' => format_plural($num_revisions, '1 revision', '@count revisions'), + )); + $block = array(); + $block['subject'] = empty($title_link) ? $title : l($title, $title_link, $link_options); + $block['content'] = theme('item_list', array('items' => $links, 'title' => '')); + return $block; +} diff --git a/sites/all/modules/revisioning/revisioning.rules.inc b/sites/all/modules/revisioning/revisioning.rules.inc new file mode 100644 index 0000000..fd6aa1f --- /dev/null +++ b/sites/all/modules/revisioning/revisioning.rules.inc @@ -0,0 +1,297 @@ + t('Revisioning'), + 'variables' => _revisioning_rules_event_variables(), + ); + $events = array( + 'revisioning_post_update' => $default + array( + 'label' => t('Content revision has been updated'), + ), + 'revisioning_pre_publish' => $default + array( + 'label' => t('Content revision is going to be published'), + ), + 'revisioning_post_publish' => $default + array( + 'label' => t('Pending revision has been published'), + ), + 'revisioning_pre_revert' => $default + array( + 'label' => t('Content is going to be reverted to revision'), + ), + 'revisioning_post_revert' => $default + array( + 'label' => t('Content has been reverted to revision'), + ), + 'revisioning_post_unpublish' => $default + array( + 'label' => t('Content has been unpublished'), + ), + 'revisioning_pre_delete' => $default + array( + 'label' => t('Content revision is going to be deleted'), + ), + ); + return $events; +} + +/** + * Implements hook_rules_event_info_alter(). + * + * Add revision variable to all events with node. [#2232451] + */ +function revisioning_rules_event_info_alter(&$events) { + foreach ($events as $key => $event) { + if (isset($event['variables'])) { + foreach ($event['variables'] as $name => $variable) { + if ($variable['type'] == 'node') { + $extra = _revisioning_rules_event_variables(); + unset($extra['node']); + $events[$key]['variables'] += $extra; + } + } + } + } +} + +/** + * Returns event variables suitable for use with a node revision event. + * + * (aka Data Selectors) + */ +function _revisioning_rules_event_variables() { + $vars = array( + 'user' => array( + 'type' => 'user', + 'label' => t('acting user'), + 'description' => t('The acting user.'), + 'handler' => 'revisioning_events_argument_global_user', + ), + 'node' => array( + 'type' => 'node', + 'label' => t('target node.'), + ), + 'revision' => array( + // Type revisioning_revision' ? + 'type' => 'node', + 'label' => t('current revision of target content.'), + 'description' => t('The current content revision'), + 'handler' => 'revisioning_events_argument_current_revision', + ), + ); + return $vars; +} + +/** + * Get global user argument. + */ +function revisioning_events_argument_global_user($arguments, $name, $info) { + global $user; + return user_load($user->uid); +} + +/** + * Evaluate revision argument. + */ +function revisioning_events_argument_current_revision($arguments, $name, $info) { + if (empty($arguments['node'])) { + drupal_set_message(t('Revisioning: could not evaluate rule condition -- node variable missing.'), 'warning'); + return FALSE; + } + $node = $arguments['node']; + // Use revisioning_get_current_node_revision_id($node->nid); ? + $current_vid = $node->current_revision_id; + if ($node->vid != $current_vid) { + $current = node_load($node->nid, $current_vid); + return $current; + } + return $node; +} + +/** + * Target revision author argument handler. + */ +function revisioning_events_argument_target_revision_author($arguments, $name, $info) { + global $user; + $node = $arguments['node']; + return user_load($node->revision_uid); +} + +function revisioning_events_argument_node_author($arguments, $name, $info) { + $node = $arguments['node']; + return user_load($node->uid); +} + +function revisioning_events_argument_current_revision_author($arguments, $name, $info) { + $node = $arguments['node']; + $current_vid = $node->current_revision_id; + if ($node->vid != $current_vid) { + $current = node_load($node->nid, $current_vid); + return user_load($current->revision_uid); + } + return user_load($node->revision_uid); +} + +/*************************** Rules Conditions ********************************/ + +/** + * Implements hook_rules_condition_info(). + */ +function revisioning_rules_condition_info() { + $defaults = array( + 'group' => t('Revisioning'), + 'parameter' => array( + 'node' => array('type' => 'node', 'label' => t('Content')), + ), + ); + $items['revisioning_node_has_pending'] = $defaults + array( + 'label' => t('Content has pending revision'), + 'help' => t('Evaluates to TRUE, if the content has one or more pending revisions.'), + 'module' => 'revisioning', + ); + $items['revisioning_condition_revision_is'] = $defaults + array( + 'label' => t('Content revision state'), + 'help' => t('Evaluates to TRUE, if the revision is in one of the selected states.'), + 'module' => 'revisioning', + 'base' => 'revisioning_condition_revision_is', + ); + $items['revisioning_condition_revision_is']['parameter']['revision_type'] = array( + 'type' => 'list', + 'label' => t('Is one of'), + 'options list' => 'revisioning_revision_states_all', + 'description' => t('The content state(s) to check for.'), + ); + return $items; +} + +/** + * Wrapper function to always return all revisioning states. + */ +function revisioning_revision_states_all() { + return revisioning_revision_states(); +} + +/** + * Condition: check for pending revisions of the node. + */ +function revisioning_node_has_pending($node, $settings) { + revisioning_set_node_revision_info($node); + return ($node->revision_moderation && $node->num_revisions == 1 && !$node->status) || (_revisioning_get_number_of_pending_revisions($node->nid) > 0); +} + +/** + * Condition: check revision state. + */ +function revisioning_condition_revision_is($node, $settings) { + revisioning_set_node_revision_info($node); + // Should we return FALSE here if $node->revision_moderation is not set? + $type = _revisioning_revision_is($node); + return in_array($type, $settings); +} + +/** + * Label callback for "revisioning_revision_is" condition. + */ +function revisioning_condition_revision_is_label($settings, $argument_labels) { + $names = array_intersect_key(revisioning_revision_states(), $settings['revision_type']); + return t('Revision status of @node is: @type', $argument_labels + array('@type' => implode(t(' or '), $names))); +} + +/** + * Label callback for "revisioning_node_has_pending" condition. + */ +function revisioning_node_has_pending_label($settings, $argument_labels) { + return t('Content "@node" has pending revision(s)', $argument_labels); +} + +/*************************** Rules Actions ************************************/ + +/** + * Implements hook_rules_action_info(). + */ +function revisioning_rules_action_info() { + $default = array( + 'group' => t('Revisioning'), + ); + return array( + 'revisioning_rules_action_publish_latest' => $default + array( + 'label' => t('Publish the most recent pending revision'), + 'parameter' => array( + 'node' => array('type' => 'node', 'label' => t('content')), + ), + ), + 'revisioning_rules_action_load_current' => $default + array( + 'label' => t('Load current revision of content'), + 'parameter' => array( + 'node' => array('type' => 'node', 'label' => t('content')), + ), + 'new variables' => array( + 'loaded_current_revision' => array( + 'type' => 'node', + 'label' => t('Loaded current revision of content'), + 'save' => FALSE, + 'label callback' => 'revisioning_rules_loaded_current_label', + ), + ), + ), + ); +} + +/** + * Label callback for "revisioning_rules_action_load_current" action. + * @obsolete ? + */ +function revisioning_rules_action_load_current_label($settings, $argument_labels) { + return t('Load current revision of "@node"', $argument_labels); +} + +/** + * Label callback for "loaded_current_revision" variable. + * @obsolete ? + */ +function revisioning_rules_loaded_current_label($settings, $argument_labels) { + return t('Loaded current revision of "@node"', $argument_labels); +} + +/** + * Action: load current revision of provided node. + */ +function revisioning_rules_action_load_current($node) { + // Or revisioning_get_current_node_revision_id($node->nid); ? + $current_vid = $node->current_revision_id; + if ($node->vid != $current_vid) { + $current = node_load($node->nid, $current_vid); + return array('loaded_current_revision' => $current); + } + return array('loaded_current_revision' => $node); +} + +/** + * Action: publish most recent pending revision. + */ +function revisioning_rules_action_publish_latest($node) { + _revisioning_publish_latest_revision($node); +} + +/** + * Implements hook_entity_property_info_alter(). + */ +function revisioning_entity_property_info_alter(&$info) { + // $info['node']['properties']['author']['getter callback'] = + // 'revisioning_user_get_properties'; +} + +/** + * @} + */ diff --git a/sites/all/modules/revisioning/revisioning.taxonomy.inc b/sites/all/modules/revisioning/revisioning.taxonomy.inc new file mode 100644 index 0000000..27b163b --- /dev/null +++ b/sites/all/modules/revisioning/revisioning.taxonomy.inc @@ -0,0 +1,75 @@ +condition('nid', $node->nid)->execute(); + // Use of taxonomy_delete_node_index($node); requires core 7.12 or later + // Then add terms if node is published OR terms are requested explicitly. + if ($node->status || $show_unpublished_content_terms) { + $vid = empty($node->revision_moderation) || empty($node->current_revision_id) ? $node->vid : $node->current_revision_id; + // Find all the terms attached to this node revision. + $tids = revisioning_get_tids($vid); + if (!empty($tids)) { + // Core, via taxonomy_node_update(), only does this when node is + // published, but then we can't see the terms of unpublished content + // in Views! + $query = db_insert('taxonomy_index')->fields( + array('nid', 'tid', 'sticky', 'created') + ); + foreach ($tids as $tid) { + $query->values(array( + 'nid' => $node->nid, + 'tid' => $tid, + // See [#1417658]. + 'sticky' => empty($node->sticky) ? 0 : 1, + 'created' => $node->created, + )); + } + $query->execute(); + } + } + } +} + +/** + * Return array of all taxonomy term ids belonging to the supplied revision. + * + * @param int $vid + * the revision id + */ +function revisioning_get_tids($vid) { + $tids = array(); + $conditions = array('type' => 'taxonomy_term_reference'); + $fields = field_read_fields($conditions); + foreach ($fields as $field => $data) { + $sql = "SELECT {$field}_tid AS tid FROM {field_revision_$field} WHERE revision_id = :vid AND entity_type = 'node' AND deleted = 0 ORDER BY tid"; + $result = db_query($sql, array(':vid' => $vid)); + foreach ($result as $term) { + $tids[$term->tid] = $term->tid; + } + } + return $tids; +} diff --git a/sites/all/modules/revisioning/revisioning_api.inc b/sites/all/modules/revisioning/revisioning_api.inc new file mode 100644 index 0000000..9ea147b --- /dev/null +++ b/sites/all/modules/revisioning/revisioning_api.inc @@ -0,0 +1,965 @@ + current_vid) of ANY node + * OR single revision of UNPUBLISHED node + * Current, published: + * - revision with (vid == current_vid) of PUBLISHED node + * Archived: + * - all other revisions, i.e. + * revision with (vid < current_vid) of ANY node + * OR revision with (vid == current_vid) of UNPUBLISHED node + * + * Note: these will change when Revisioning is going to store revision states + * independently from vid number (e.g. in different table). + */ + +/** + * Set node revision info. + * + * We use this in revisioning_node_load() to set up some useful node properties + * that may be read later, whether it be in this module or another, thus + * removing the need for multiple calls in various places to retrieve the same + * info. + */ +function revisioning_set_node_revision_info(&$node) { + if (!isset($node->num_revisions) && isset($node->nid)) { + // We need this info for updated content even if it is not moderated. + // Luckily this info is easily retrieved. + $node->num_revisions = revisioning_get_number_of_revisions($node->nid); + $node->current_revision_id = revisioning_get_current_node_revision_id($node->nid); + $node->is_current = revisioning_revision_is_current($node); + $node->is_pending = _revisioning_node_is_pending($node); + } + // The revision_moderation flag may be overridden on the node edit form by + // users with the "administer nodes" permission. By implication, the 'Publish' + // link needs to be available to those users, for any content with a pending + // revision, as the publish check box on the edit form applies to the current + // rather than the pending revision(s). + if (!isset($node->revision_moderation)) { + $node->revision_moderation = revisioning_content_is_moderated($node->type, $node); + } + // $node->uid and $node->revision_uid were already set in node_load() + // $node->revision is set as part of 'prepare'-op, see node_object_prepare() +} + +/** + * Get the number of revisions belonging to a node. + * + * @param int $nid + * id of the node + * + * @return int + * A count representing the number of revisions associated with the node + */ +function revisioning_get_number_of_revisions($nid) { + $result = db_query("SELECT COUNT(vid) FROM {node_revision} WHERE nid = :nid", array(':nid' => $nid)); + return $result->fetchField(); +} + +/** + * Get the number of archived revisions belonging to a node. + * + * @param object $node + * the node object + * + * @return int + * A count representing the number of archived revisions for the node + * Returns zero if there is only one (i.e. current) revision. + */ +function revisioning_get_number_of_archived_revisions($node) { + $result = db_query("SELECT COUNT(vid) FROM {node_revision} WHERE nid = :nid AND vid < :vid", array( + ':nid' => $node->nid, + ':vid' => $node->current_revision_id, + )); + return $result->fetchField(); +} + +/** + * Delete all revisions with a vid less than the current. + * + * Use node_revision_delete from node.module to ensure that we cleanup not + * only node revisions but also all attached field revisions as well. + */ +function revisioning_delete_archived_revisions($node) { + $revisions = db_select('node_revision', 'n') + ->fields('n', array('vid')) + ->condition('n.nid', $node->nid) + ->condition('n.vid', $node->current_revision_id, '<') + ->execute(); + + foreach ($revisions as $rev) { + node_revision_delete($rev->vid); + } +} + +/** + * Get the id of the current revision that the supplied node is pointing to. + * + * Used in cases where the node object wasn't fully loaded or was loaded + * with a different revision. + * + * @param int $nid + * The id of the node whose current revision id is to be returned. + * + * @return int + * A single number being the current revision id (vid). + */ +function revisioning_get_current_node_revision_id($nid) { + $result = db_query("SELECT vid FROM {node} WHERE nid = :nid", array(':nid' => $nid)); + return $result->fetchField(); +} + +/** + * Get the id of the user who last edited the supplied node. + * + * ie. the author of the latest revision. + * + * This is irrespective of whether this latest revision is pending or not, + * unless TRUE is specified for the second argument, in which case the uid + * of the creator of the current revision (published or not) is returned. + * + * @param int $nid + * The id of the node whose most recent editor id is to be returned. + * @param bool $current + * Whether the uid of the current or very latest revision should be returned. + * + * @return int + * A single number being the user id (uid). + */ +function revisioning_get_last_editor($nid, $current = FALSE) { + $sql = ($current) ? "SELECT vid FROM {node} WHERE nid = :nid" : "SELECT MAX(vid) FROM {node_revision} WHERE nid = :nid"; + $vid = db_query($sql, array(':nid' => $nid))->fetchField(); + $result = db_query("SELECT uid FROM {node_revision} WHERE vid = :vid", array(':vid' => $vid)); + return $result->fetchField(); +} + +/** + * Return whether the currenly loaded revision is the current one. + * + * @param object $node + * The node object + * + * @return bool + * TRUE if the currently loaded node revision is the current revision + */ +function revisioning_revision_is_current($node) { + return isset($node->vid) && isset($node->current_revision_id) && $node->vid == $node->current_revision_id; +} + +/** + * Return whether the supplied content type is subject to moderation. + * + * @param string $content_type + * i.e. machine name, ie. $node->type + * @param object $node + * (optional) argument to implement "moderation opt-in" for nodes that are not + * moderated by default (i.e by their content type). + * + * @return bool + * TRUE, if the supplied type has the "New revision in draft, pending + * moderation" box ticked on the Structure >> Content types >> edit page OR + * when a pending revision exists. The latter is to support the feature of + * "moderate at any time" available to users with the "administer nodes" + * permission. + */ +function revisioning_content_is_moderated($content_type, $node = NULL) { + $content_type_is_moderated = !empty($content_type) && in_array('revision_moderation', variable_get('node_options_' . $content_type, array())); + if (!$content_type_is_moderated && isset($node->nid) && isset($node->current_revision_id)) { + $latest_vid = revisioning_get_latest_revision_id($node->nid); + return $latest_vid > $node->current_revision_id; + } + return $content_type_is_moderated; +} + +/** + * Return whether the user has permission to auto-publish the supplied node. + * + * Auto-publish applies only for content types for which this feature has been + * activated on the content type form and only when "New revision in draft, + * pending moderation" is ticked also. + * + * @param object $node + * The node object + */ +function revisioning_user_may_auto_publish($node) { + return variable_get('revisioning_auto_publish_' . $node->type, FALSE) && revisioning_user_node_access('publish revisions', $node); +} + +/** + * Return a single or all possible revision state names. + * + * @param int $state + * (optional) state id, as defined in revisioning_api.inc + * + * @return array|string + * if $state is provided, state name. Otherwise, an array keyed by state id. + */ +function revisioning_revision_states($state = NULL) { + $states = array( + REVISION_ARCHIVED => t('Archived'), + REVISION_CURRENT => t('Current, published'), + REVISION_PENDING => t('Pending'), + ); + return $state === NULL ? $states : $states[$state]; +} + +/** + * Check for a pending revision. + * + * Return TRUE when either of the following is true: + * o the supplied node has at least one revision more recent than the current + * o the node is not yet published and consists of a single revision + * + * Relies on vid, current_revision_id and num_revisions set on the node object, + * see function revisioning_set_node_revision_info() + * + * @param object $node + * The node object + * + * @return bool + * TRUE, if node is pending according to the above definition + */ +function _revisioning_node_is_pending($node) { + return isset($node->vid) && isset($node->current_revision_id) && + ($node->vid > $node->current_revision_id || (!$node->status && $node->num_revisions == 1)); +} + +/** + * Implements hook_revisionapi(). + * + * Act on various revision events. + * + * "Pre" operations can be useful to get values before they are lost or changed, + * for example, to save a backup of revision before it's deleted. + * Also, for "pre" operations vetoing mechanics could be implemented, so it + * would be possible to veto an operation via hook_revisionapi(). For example, + * when the hook is returning FALSE, operation will be vetoed. + */ +function revisioning_revisionapi($op, $node) { + switch ($op) { + + case 'post update': + if (module_exists('rules')) { + rules_invoke_event('revisioning_post_update', $node); + } + break; + + case 'pre publish': + if (module_exists('rules')) { + rules_invoke_event('revisioning_pre_publish', $node); + } + break; + + case 'post publish': + // Called from _revisioning_publish_revision. + // Invoke hook_revision_publish() triggers, passing the node as argument. + module_invoke_all('revision_publish', $node); + + if (module_exists('rules')) { + rules_invoke_event('revisioning_post_publish', $node); + } + break; + + case 'post unpublish': + // Called from _revisioning_unpublish_revision(). + // Invoke hook_revision_unpublish triggers passing the node as an arg. + module_invoke_all('revision_unpublish', $node); + + if (module_exists('rules')) { + rules_invoke_event('revisioning_post_unpublish', $node); + } + break; + + case 'pre revert': + if (module_exists('rules')) { + rules_invoke_event('revisioning_pre_revert', $node); + } + break; + + case 'post revert': + // Called from revisioning_revert_confirm_post_submit(). + // Invoke hook_revision_revert() triggers passing the node as an arg. + module_invoke_all('revision_revert', $node); + + if (module_exists('rules')) { + rules_invoke_event('revisioning_post_revert', $node); + } + break; + + case 'pre delete': + if (module_exists('rules')) { + rules_invoke_event('revisioning_pre_delete', $node); + } + break; + + case 'post delete': + break; + } +} + +/** + * Get the id of the latest revision belonging to a node. + * + * @param int $nid + * id of the node + * + * @return int + * ID of the latest revision. + */ +function revisioning_get_latest_revision_id($nid) { + $result = db_query("SELECT MAX(vid) FROM {node_revision} WHERE nid = :nid", array(':nid' => $nid)); + return $result->fetchField(); +} + +/** + * Revert node to selected revision without changing its publication status. + * + * @param object|int $node + * Target $node object (loaded with target revision) or nid of target node + * @param int $vid + * (optional) vid of revision to revert to, if provided $node must not be an + * object. + */ +function _revisioning_revertpublish_revision(&$node, $vid = NULL) { + $node_revision = is_object($node) ? $node : node_load($node, $vid); + $return = module_invoke_all('revisionapi', 'pre revert', $node_revision); + if (in_array(FALSE, $return)) { + drupal_goto('node/' . $node_revision->nid . '/revisions/' . $node_revision->vid . '/view'); + } + _revisioning_revert_revision($node_revision); + module_invoke_all('revisionapi', 'post revert', $node_revision); +} + +/** + * Revert node to selected revision without publishing it. + * + * This is same as node_revision_revert_confirm_submit() in node_pages.inc, + * except it doesn't put any messages on screen. + * + * @param object|int $node + * Target $node object (loaded with target revision) or nid of target node + * @param int $vid + * (optional) vid of revision to revert to, if provided $node is not an + * object. + */ +function _revisioning_revert_revision(&$node, $vid = NULL) { + $node_revision = is_object($node) ? $node : node_load($node, $vid); + $node_revision->revision = 1; + $node_revision->log = t('Copy of the revision from %date.', array('%date' => format_date($node_revision->revision_timestamp))); + if (module_exists('taxonomy')) { + $node_revision->taxonomy = array_keys($node_revision->taxonomy); + } + node_save($node_revision); + watchdog('content', '@type: reverted %title revision %revision.', array( + '@type' => $node_revision->type, + '%title' => $node_revision->title, + '%revision' => $node_revision->vid, + )); +} + +/** + * Unpublish node, without calling node_save(). + * + * @param object|int $nid_or_node + * Target $node object or nid of target node + * @param bool $clear_cache + * Whether to clear the cache afterwards or not. Clearing the cache on every + * node during bulk operations can be time-consuming. + */ +function _revisioning_unpublish_node($nid_or_node, $clear_cache = TRUE) { + $node = is_object($nid_or_node) ? $nid_or_node : node_load($nid_or_node); + + db_update('node') + ->fields(array( + 'changed' => time(), + 'status' => NODE_NOT_PUBLISHED)) + ->condition('nid', $node->nid) + ->execute(); + + db_update('node_revision') + ->fields(array('status' => NODE_NOT_PUBLISHED)) + ->condition('vid', $node->vid) + ->execute(); + + $node->status = NODE_NOT_PUBLISHED; + + // Make sure the alias, if present, is not changed when unpublishing. + if (!isset($node->path['pathauto'])) { + $node->path = array( + // So that pathauto_node_update() does nothing. + 'alias' => '', + // So that pathauto_node_update() does nothing. + 'pathauto' => FALSE, + ); + } + elseif (!isset($node->path['alias'])) { + // [#1328180], [#1576552] + $node->path['alias'] = ''; + } + $node->original = clone $node; + $node->original->status = NODE_PUBLISHED; + + module_invoke_all('node_update', $node); + module_invoke_all('entity_update', $node, 'node'); + + // Update node_access table. + node_access_acquire_grants($node); + + if ($clear_cache) { + cache_clear_all(); + } +} + +/** + * Delete selected revision of node, provided it's not current. + * + * This is same as node_revision_delete_confirm_submit() in node_pages.inc, + * except it doesn't put any messages on the screen. This way it becomes + * reusable (eg. in actions). + * + * @param object $node + * Target $node object (loaded with target revision) or nid of target node + * @param int $vid + * (optional) vid of revision to delete, if provided $node is not object. + * + * @TODO: Insert check to prevent deletion of current revision of node. + */ +function _revisioning_delete_revision(&$node, $vid = NULL) { + $node_revision = is_object($node) ? $node : node_load($node, $vid); + module_invoke_all('revisionapi', 'pre delete', $node_revision); + db_delete('node_revision') + ->condition('vid', $node_revision->vid) + ->execute(); + watchdog('content', '@type: deleted %title revision %revision.', array( + '@type' => $node_revision->type, + '%title' => $node_revision->title, + '%revision' => $node_revision->vid, + )); + module_invoke_all('revisionapi', 'post delete', $node_revision); +} + +/** + * Unpublish revision (i.e. the node). + * + * Note that no check is made as to whether the initiating user has permission + * to unpublish this node. + * + * @param object $node + * Target $node object or nid of target node + */ +function _revisioning_unpublish_revision(&$node) { + $node_revision = is_object($node) ? $node : node_load($node); + module_invoke_all('revisionapi', 'pre unpublish', $node_revision); + _revisioning_unpublish_node($node_revision->nid); + watchdog('content', 'Unpublished @type %title', array('@type' => $node_revision->type, '%title' => $node_revision->title), WATCHDOG_NOTICE, l(t('view'), "node/$node_revision->nid")); + module_invoke_all('revisionapi', 'post unpublish', $node_revision); +} + +/** + * Make the supplied revision of the node current and publish it. + * + * It is the caller's responsibility to provide a proper revision. + * Note that no check is made as to whether the initiating user has permission + * to publish this revision. + * + * @param int $node_revision + * Target $node object (loaded with target revision) + * @param bool $clear_cache + * Whether to clear the cache afterwards or not. Clearing the cache on every + * node during bulk operations can be time-consuming. + */ +function _revisioning_publish_revision(&$node_revision, $clear_cache = TRUE) { + + $return = module_invoke_all('revisionapi', 'pre publish', $node_revision); + if (in_array(FALSE, $return)) { + drupal_goto('node/' . $node_revision->nid . '/revisions/' . $node_revision->vid . '/view'); + } + // Update {node} and {node_revision} tables setting status and other flags. + db_update('node') + ->fields(array( + 'vid' => $node_revision->vid, + 'title' => $node_revision->title, + 'changed' => time(), + 'status' => NODE_PUBLISHED, + 'comment' => $node_revision->comment, + 'promote' => $node_revision->promote, + 'sticky' => $node_revision->sticky)) + ->condition('nid', $node_revision->nid) + ->execute(); + + db_update('node_revision') + ->fields(array('status' => NODE_PUBLISHED)) + ->condition('vid', $node_revision->vid) + ->execute(); + + if (empty($node_revision->is_current)) { + // Need to set up $node_revision correctly before calling + // revisioning_update_taxonomy_index(), via revisioning_node_update(). + $node_revision->current_revision_id = $node_revision->vid; + } + $node_revision->status = $node_revision->current_status = NODE_PUBLISHED; + + // Make sure the alias, if present, is not changed when publishing. + if (!isset($node_revision->path['pathauto'])) { + $node_revision->path = array( + // So that path_node_update() does nothing. + 'alias' => '', + // So that pathauto_node_update() does nothing. + 'pathauto' => FALSE, + ); + } + elseif (!isset($node_revision->path['alias'])) { + // [#1328180], [#1576552] + $node_revision->path['alias'] = ''; + } + // Make sure the menu, if present, is not changed when publishing [#1698024] + if (!isset($node_revision->menu)) { + $node_revision->menu = array( + 'enabled' => '', + 'mlid' => '', + 'link_title' => '', + 'link_path' => '', + 'description' => '', + ); + } + elseif (!isset($node_revision->menu)) { + $node_revision->menu = ''; + } + $node_revision->original = clone $node_revision; + $node_revision->original->status = NODE_NOT_PUBLISHED; + + module_invoke_all('node_update', $node_revision); + module_invoke_all('entity_update', $node_revision, 'node'); + + // Update node_access table only for existing nodes. When the node is newly + // created via the node/add page, node_access_acquire_grants() is called by + // node_save() anyway. See [#1243018]. + if (empty($node_revision->is_new)) { + node_access_acquire_grants($node_revision); + } + + if ($clear_cache) { + cache_clear_all(); + } + + watchdog('content', 'Published rev #%revision of @type %title', + array( + '@type' => $node_revision->type, + '%title' => $node_revision->title, + '%revision' => $node_revision->vid, + ), + WATCHDOG_NOTICE, + l(t('view'), "node/$node_revision->nid/revisions/$node_revision->vid/view") + ); + module_invoke_all('revisionapi', 'post publish', $node_revision); +} + +/** + * Publish latest revision. + * + * Find the most recent pending revision, make it current, unless it already is + * and publish it and its node. + * + * Note that no check is made as to whether the initiating user has permission + * to publish this node. + * + * Note that this is a post-save operation that should not be called in + * response to hook_node_presave(), as invoked from node_save(). + * + * @param object $node + * The node object whose latest pending revision is to be published + * + * @return bool + * TRUE if operation was successful, FALSE if there is no pending revision to + * publish + */ +function _revisioning_publish_latest_revision(&$node) { + // Get the latest pending revision. + $pending_revisions = _revisioning_get_pending_revisions($node->nid); + $latest_pending = array_shift($pending_revisions); + if ($latest_pending) { + $node_revision = node_load($node->nid, $latest_pending->vid); + _revisioning_publish_revision($node_revision); + return TRUE; + } + // If there is no pending revision, take the current revision, provided it is + // NOT published. + if (!$node->status) { + if (!isset($node->is_current)) { + $node->current_revision_id = revisioning_get_current_node_revision_id($node->nid); + $node->is_current = revisioning_revision_is_current($node); + } + if ($node->is_current) { + _revisioning_publish_revision($node); + return TRUE; + } + } + return FALSE; +} + +/** + * Return a count of the number of revisions newer than the supplied vid. + * + * @param int $vid + * The reference vid. + * @param int $nid + * The id of the node. + * + * @return int + * count of the number of revisions newer than the supplied vid + */ +function _revisioning_get_number_of_revisions_newer_than($vid, $nid) { + $result = db_query("SELECT COUNT(vid) FROM {node_revision} WHERE nid = :nid AND vid > :vid", + array( + ':vid' => $vid, + ':nid' => $nid, + ) + ); + return $result->fetchField(); +} + +/** + * Return a count of the number of revisions newer than the current revision. + * + * @param int $nid + * The id of the node. + * + * @return int + * count of the number of revisions newer than the current revision + */ +function _revisioning_get_number_of_pending_revisions($nid) { + $result = db_query("SELECT COUNT(r.vid) FROM {node} n INNER JOIN {node_revision} r ON n.nid = r.nid WHERE (r.vid > n.vid AND n.nid = :nid)", array( + ':nid' => $nid) + ); + return $result->fetchField(); +} + +/** + * Retrieve a list of revisions with a vid greater than the current. + * + * @param int $nid + * The node id to retrieve. + * + * @return array + * An array of revisions (latest first), each containing vid, title and + * content type. + */ +function _revisioning_get_pending_revisions($nid) { + $sql = "SELECT r.vid, r.title, n.type FROM {node} n INNER JOIN {node_revision} r ON n.nid = r.nid WHERE (r.vid > n.vid AND n.nid = :nid) ORDER BY r.vid DESC"; + $result = db_query($sql, array( + ':nid' => $nid) + ); + $revisions = array(); + foreach ($result as $revision) { + $revisions[$revision->vid] = $revision; + } + return $revisions; +} + +/** + * Return revision type of the supplied node. + * + * @param object $node + * Node object to check + * + * @return int + * Revision type + */ +function _revisioning_revision_is(&$node) { + if ($node->is_pending) { + return REVISION_PENDING; + } + return ($node->is_current && $node->status == NODE_PUBLISHED) ? REVISION_CURRENT : REVISION_ARCHIVED; +} + +/** + * Return a string with details about the node that is about to be displayed. + * + * @param object $node + * The node that is about to be viewed + * + * @return string + * A translatable message containing details about the node + */ +function _revisioning_node_info_msg($node) { + // Get username for the revision, not the creator of the node. + $revision_author = user_load($node->revision_uid); + $placeholder_data = array( + '@content_type' => $node->type, + '%title' => $node->title, + '!author' => theme('username', array('account' => $revision_author)), + '@date' => format_date($node->revision_timestamp, 'short'), + ); + $revision_type = _revisioning_revision_is($node); + switch ($revision_type) { + + case REVISION_PENDING: + return t('Displaying pending revision of @content_type %title, last modified by !author on @date', $placeholder_data); + + case REVISION_CURRENT: + return t('Displaying current, published revision of @content_type %title, last modified by !author on @date', $placeholder_data); + + case REVISION_ARCHIVED: + return t('Displaying archived revision of @content_type %title, last modified by !author on @date', $placeholder_data); + } +} + +/** + * Return TRUE only if the user account has ALL of the supplied permissions. + * + * @param array $permissions + * An array of permissions (strings) + * @param object $account + * (optional) The user account object. Defaults to current user if omitted. + * + * @return bool + * Whether the user has access to all permissions + */ +function revisioning_user_access_all($permissions, $account = NULL) { + foreach ($permissions as $permission) { + if (!user_access($permission, $account)) { + return FALSE; + } + } + return TRUE; +} + +/** + * Return an array of names of content types that are subject to moderation. + * + * @return array + * array of strings, may be empty + */ +function revisioning_moderated_content_types($machine_name = TRUE) { + $moderated_content_types = array(); + foreach (node_type_get_types() as $type) { + $content_type = check_plain($type->type); + if (revisioning_content_is_moderated($content_type)) { + $moderated_content_types[] = ($machine_name ? $content_type : $type->name); + } + } + return $moderated_content_types; +} + +define('NO_FILTER', '-1'); + +/** + * Get list of revisions accessible to the logged-in user via the operation. + * + * @param string $op + * Revision operation, eg 'view revision list' (as used by Pending Revisions + * block) + * @param int $is_published + * (optional) 1 to return only published content + * 0 to return only content that isn't published + * -1 (default) no filter, return content regardles of publication status + * @param int $creator_uid + * (optional) Only return content created by the user with the supplied id. + * Defaults to -1, which means don't care who the creator is. + * @param int $modifier_uid + * (optional) Only return content last modified by user with the supplied id. + * Defaults to -1, which means don't care who last modifed the node. + * @param bool|int $is_moderated + * (optional) TRUE to return only content of types subject to moderation + * FALSE to return only content that isn't subject to moderation + * -1 (default) no filter, return content regardles of moderation flag + * @param bool $is_pending + * (optional) bool indicating whether only nodes pending publication should be + * returned; a pending node is defined as a node that has a revision newer + * than the current OR a node with a single revision that is not published. + * @param int $max + * (optional) Maximum number of nodes to be returned, defaults to 1000 + * @param string $order_by_override + * (optional) "ORDER BY ..." clause to be added, defaults to "timestamp DESC". + * + * @return array + * An array of revision objects each containing nid, content type, published + * flag, creator-id, title+vid+modifier-id+timestamp of the current revision, + * plus tags and taxonomy terms. + * + * @todo + * This code may need to be reviewed if used for purposes other than the + * Pending Revisions block. + */ +function revisioning_get_revisions($op, $is_published = -1, $creator_uid = -1, $modifier_uid = -1, + $is_moderated = -1, $is_pending = FALSE, $max = 1000, $order_by_override = NULL) { + $sql_select = 'SELECT n.nid, r.vid, n.uid AS creator_uid, r.uid, n.type, n.status, r.title, r.timestamp'; + // Join on current revision (vid) except when looking for pending revisions. + $sql_from = ' FROM {node} n INNER JOIN {node_revision} r ' . ($is_pending ? 'ON n.nid=r.nid' : 'ON n.vid=r.vid'); + $sql_where = ($is_published < 0) ? '' : " WHERE n.status=$is_published"; + if ($creator_uid >= 0) { + $sql_where = empty($sql_where) ? " WHERE n.uid=$creator_uid" : $sql_where . " AND n.uid=$creator_uid"; + } + if ($modifier_uid >= 0) { + $sql_where = empty($sql_where) ? " WHERE r.uid=$modifier_uid" : $sql_where . " AND r.uid=$modifier_uid"; + } + if ($is_pending) { + $sql_where = empty($sql_where) ? ' WHERE' : $sql_where . ' AND'; + $sql_where .= ' (r.vid>n.vid OR (n.status=0 AND (SELECT COUNT(vid) FROM {node_revision} WHERE nid=n.nid)=1))'; + } + $sql_order = " ORDER BY " . (empty($order_by_override) ? _revisioning_extract_order_clause_from_URI() : $order_by_override); + $include_taxonomy_terms = module_exists('taxonomy') && + variable_get('revisioning_show_taxonomy_terms', TRUE) && (count(taxonomy_get_vocabularies()) > 0); + if ($include_taxonomy_terms) { + $conditions = array('type' => 'taxonomy_term_reference'); + $fields = field_read_fields($conditions); + foreach ($fields as $field => $data) { + $sql_select .= ", ttd_$field.name AS " . ($field == 'field_tags' ? 'tags' : 'term'); + $sql_from .= " LEFT JOIN {field_revision_$field} r_$field ON r.vid = r_$field.revision_id LEFT JOIN {taxonomy_term_data} ttd_$field ON r_$field.{$field}_tid=ttd_$field.tid"; + } + } + $sql = $sql_select . $sql_from . $sql_where . $sql_order; + $node_query_result = db_query_range($sql, 0, $max); + $revisions = array(); + foreach ($node_query_result as $revision) { + // Need to set revision_moderation for revisioning_node_access() to work + // properly. + $revision->revision_moderation = revisioning_content_is_moderated($revision->type/*, $revision*/); + + $filter = ($is_moderated < 0) || ($is_moderated == $revision->revision_moderation); + + if ($filter && _revisioning_access_node_revision($op, $revision)) { + if (empty($revisions[$revision->vid])) { + $revisions[$revision->vid] = $revision; + } + // If a revision has more than one taxonomy term, these will be returned + // by the query as seperate objects differing only in their terms. + elseif ($include_taxonomy_terms) { + $existing_revision = $revisions[$revision->vid]; + if (!empty($revision->term)) { + if (strpos($existing_revision->term, $revision->term) === FALSE) { + // Bit of a quick & dirty -- goes wrong if a term is substr of + // another. + $existing_revision->term .= ", $revision->term"; + } + } + if (!empty($revision->tags)) { + if (strpos($existing_revision->tags, $revision->tags) === FALSE) { + $existing_revision->tags .= ", $revision->tags"; + } + } + } + } + } + return $revisions; +} + +/** + * Retrieve a list of all revisions belonging to the supplied node. + * + * Includes archive, current, and pending revisions. + * + * @param int $nid + * The node id to retrieve. + * @param bool $include_taxonomy_terms + * Whether to also retrieve the taxonomy terms for each revision + * + * @return array + * An array of revision objects, each with published flag, log message, vid, + * title, timestamp and name of user that created the revision + */ +function _revisioning_get_all_revisions_for_node($nid, $include_taxonomy_terms = FALSE) { + $node = node_load($nid); + + $sql_select = 'SELECT r.vid, r.status, r.title, r.log, r.uid, r.timestamp, u.name'; + $sql_from = ' FROM {node_revision} r INNER JOIN {users} u ON r.uid=u.uid'; + $sql_where = ' WHERE r.nid = :nid ORDER BY r.vid DESC'; + if ($include_taxonomy_terms) { + $conditions = array('type' => 'taxonomy_term_reference'); + $fields = field_read_fields($conditions); + foreach ($fields as $field => $data) { + $field_instance = field_read_instance('node', $field, $node->type); + if ($field_instance) { + $sql_select .= ", ttd_$field.name AS " . ($field == 'field_tags' ? 'tags' : 'term'); + $sql_from .= " LEFT JOIN {field_revision_$field} r_$field ON r.vid = r_$field.revision_id LEFT JOIN {taxonomy_term_data} ttd_$field ON r_$field.{$field}_tid=ttd_$field.tid"; + } + } + } + $sql = $sql_select . $sql_from . $sql_where; + $result = db_query($sql, array(':nid' => $nid)); + $revisions = array(); + foreach ($result as $revision) { + if (empty($revisions[$revision->vid])) { + $revision->current = $node->vid; + $revisions[$revision->vid] = $revision; + } + // If a revision has more than one tag or taxonomy term, these will be + // returned by the query as seperate objects differing only in their terms. + elseif ($include_taxonomy_terms) { + $existing_revision = $revisions[$revision->vid]; + if (!empty($revision->term)) { + if (strpos($existing_revision->term, $revision->term) === FALSE) { + // Bit of a quick & dirty -- goes wrong if a term is substr of + // another. + $existing_revision->term .= ", $revision->term"; + } + } + if (!empty($revision->tags)) { + if (strpos($existing_revision->tags, $revision->tags) === FALSE) { + $existing_revision->tags .= ", $revision->tags"; + } + } + } + } + return $revisions; +} + +/** + * Extract order clause. + * + * Extract from the incoming URI (as in the table column header href) + * the sort field and order for use in an SQL 'ORDER BY' clause. + * + * @return string + * db table field name and sort direction as a string + */ +function _revisioning_extract_order_clause_from_URI() { + // We shouldn't have to do this, as tablesort.inc/tablesort_header(), called + // from theme_table() is meant to look after it, but it's got a bug [#480382]. + // Note: this function is secure, as we're only allowing recognised values, + // all unknown values, result in a descending sort by 'timestamp'. + switch ($order_by = drupal_strtolower($_REQUEST['order'])) { + case 'creator': + $order_by = 'n.uid'; + break; + + case 'by': + $order_by = 'r.uid'; + break; + + case 'published?': + $order_by = 'status'; + break; + + case 'workflow state': + $order_by = 'state'; + break; + + // List names that are fine the way they are here: + case 'title': + case 'type': + case 'term': + break; + + default: + $order_by = 'timestamp'; + break; + } + $direction = (drupal_strtolower($_REQUEST['sort']) == 'asc') ? 'ASC' : 'DESC'; + return "$order_by $direction"; +} diff --git a/sites/all/modules/revisioning/revisioning_scheduler/README.txt b/sites/all/modules/revisioning/revisioning_scheduler/README.txt new file mode 100644 index 0000000..7bf936f --- /dev/null +++ b/sites/all/modules/revisioning/revisioning_scheduler/README.txt @@ -0,0 +1,33 @@ + +DESCRIPTION +=========== +The Revisioning Scheduler is an optional add-on for the Revisioning module. +It allows users to schedule a publication date for a new or existing content +revision (in the latter case we speak of reversion rather than publication). + +The publication date and time may be set when clicking the Publish or Revert +links or on the content edit form prior to pressing Save. + +The publication date text field on the content edit form appears only when: +o the user has publish or "administer content" permission for this piece of + content +o "Auto-publish drafts of type ..." is NOT ticked on the content type edit form + (or it is ticked, but the user does not have permission to publish) +o a) the node is currently not published yet (Published box NOT ticked), or + b) if it is published, "Create new revision and moderate" IS ticked + under "Publishing options". + +INSTALLATION & CONFIGURATION +============================ +Enable like any other module. Then have a look at the self-explanatory +configuration options at Configurartion >> Revisioning Scheduler. + +CHANGELOG +========= +http://drupalcode.org/project/revisioning.git/shortlog/refs/heads/7.x-1.x + +AUTHORS +======= +Original version by Adam Bramley, adam at catalyst dot net dot nz +Edit form extension by Zvi Epner (zepner on http://drupal.org) +Further developed by Rik de Boer, rik at flink dot com dot au diff --git a/sites/all/modules/revisioning/revisioning_scheduler/revision-schedule.js b/sites/all/modules/revisioning/revisioning_scheduler/revision-schedule.js new file mode 100644 index 0000000..7166894 --- /dev/null +++ b/sites/all/modules/revisioning/revisioning_scheduler/revision-schedule.js @@ -0,0 +1,40 @@ +(function ($) { + + /* This reflect the logic of revisioning_scheduler_node_presave(). + * Scheduling a publication date is only appropriate if: + * o the node is NOT published + * o the node is already published, but the new revision to be created goes in + * moderation, i.e will not be published yet + */ + Drupal.behaviors.showPublicationDateTimeField = { + attach: function (context) { + var publishedBox = $('.form-item-status input'); + var newRevisionInModerationRadio = $('#edit-revision-operation-2'); + var publicationDate = $('.form-item-publication-date'); + + // Page-load: hide the publication date textbox, if necessary + if ((publishedBox.is(':checked') && !newRevisionInModerationRadio.is(':checked'))) { + publicationDate.hide(); + } + + // Define handlers for clicks on the Published box and moderation radio buttons. + publishedBox.click(function() { + if (!publishedBox.is(':checked') || newRevisionInModerationRadio.is(':checked')) { + publicationDate.show(); + } + else { + publicationDate.hide(); + } + }); + $('#edit-revision-operation input').click(function() { + if (!publishedBox.is(':checked') || newRevisionInModerationRadio.is(':checked')) { + publicationDate.show(); + } + else { + publicationDate.hide(); + } + }); + } + }; + +})(jQuery); diff --git a/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.info b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.info new file mode 100644 index 0000000..49bcbe1 --- /dev/null +++ b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.info @@ -0,0 +1,12 @@ +name = Revisioning Scheduler +description = "Allows revisions to be published or reverted at specified times." +dependencies[] = revisioning +configure = admin/config/content/revisioning_scheduler +core = "7.x" + +; Information added by Drupal.org packaging script on 2014-06-26 +version = "7.x-1.9" +core = "7.x" +project = "revisioning" +datestamp = "1403741628" + diff --git a/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.install b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.install new file mode 100644 index 0000000..5a67a4d --- /dev/null +++ b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.install @@ -0,0 +1,62 @@ + 'The base table for revisions that are scheduled to be published', + 'fields' => array( + 'revision_vid' => array( + 'description' => 'The identifier for this revision', + 'type' => 'int', + 'not null' => TRUE), + 'revision_nid' => array( + 'description' => 'The primary identifier for this node', + 'type' => 'int', + 'not null' => TRUE), + 'revision_uid' => array( + 'description' => 'The user who submitted this revision', + 'type' => 'int', + 'not null' => TRUE), + 'revision_date' => array( + 'description' => 'The date when this revision will be published', + 'type' => 'int', + 'not null' => TRUE), + ), + 'primary key' => array('revision_vid'), + ); + + return $schema; +} + +/** + * Implements hook_install(). + */ +function revisioning_scheduler_install() { + db_update('system') + ->fields(array('weight' => 1)) + ->condition('name', 'revisioning_scheduler') + ->execute(); +} + +/** + * Weight of revisioning_scheduler should be same as revisioning. + */ +function revisioning_scheduler_update_7014() { + db_update('system') + ->fields(array('weight' => 1)) + ->condition('name', 'revisioning_scheduler') + ->execute(); +} + +/** + * Delete scheduled publication of revisions of already deleted nodes. + */ +function revisioning_scheduler_update_7015() { + db_query('DELETE FROM {revisioning_scheduler} WHERE revision_nid NOT IN (SELECT nid FROM {node})'); +} diff --git a/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.module b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.module new file mode 100644 index 0000000..e9b09ac --- /dev/null +++ b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.module @@ -0,0 +1,432 @@ + + * zepner (drupal.org) + * RdeBoer (drupal.org) + */ + +define('REVISIONING_SCHEDULER_DEFAULT_DATE_FORMAT', 'd-m-Y H:i'); +define('REVISIONING_SCHEDULER_SLACK', 120); + +/** + * Implements hook_menu(). + */ +function revisioning_scheduler_menu() { + $items = array(); + // Put the administrative settings under Content on the Configuration page. + $items['admin/config/content/revisioning_scheduler'] = array( + 'title' => 'Revisioning Scheduler', + 'description' => 'Set the format for entering publication dates', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('revisioning_scheduler_admin_configure'), + 'access arguments' => array('administer site configuration'), + ); + return $items; +} + +/** + * Menu callback for admin settings. + */ +function revisioning_scheduler_admin_configure() { + $date_format = variable_get('revisioning_scheduler_date_format'); + $default_date_format = REVISIONING_SCHEDULER_DEFAULT_DATE_FORMAT; + if (empty($date_format)) { + $date_format = $default_date_format; + } + $help_text = t('Date and time must be separated by a space. See this manual page for available symbols and their meaning.', array( + '!php_manual_page' => 'http://php.net/manual/en/function.date.php')); + $t_args = array( + '%date_format' => $default_date_format, + '%date' => date($date_format), + ); + $form['revisioning_scheduler_date_format'] = array( + '#type' => 'textfield', + '#size' => 25, + '#title' => t('Format used for entering publication dates'), + '#default_value' => $date_format, + '#description' => $help_text . '
' . ($date_format == $default_date_format + ? t('The default input format %date_format is used.
Time now in this format: %date.', $t_args) + : t('Time now in above format: %date
If left blank the date input format defaults to %date_format', $t_args) + ), + ); + $form['revisioning_scheduler_on_edit_form'] = array( + '#type' => 'checkbox', + '#title' => t('Allow publication dates to be scheduled on the content edit form'), + '#default_value' => variable_get('revisioning_scheduler_on_edit_form', TRUE), + '#description' => t('In addition publication dates may be scheduled when you press the Publish or Revert links.'), + ); + return system_settings_form($form); +} + +/** + * Implements hook_form_BASEFORMID_alter(). + * + * This function adds a publication date & time field to the Edit form. + * It also loads a small javascript file, which controls visibility of the field + * in response to clicks on the revision moderation radio-buttons. + */ +function revisioning_scheduler_form_node_form_alter(&$form, &$form_state, $form_id) { + + if (!empty($form['#node_edit_form']) && variable_get('revisioning_scheduler_on_edit_form', TRUE)) { + + $node = $form_state['node']; + if (!user_access('administer nodes')) { + // If the node is already published then scheduling a publication date is + // inappropriate, unless the new revision goes into moderation. + // The exception are users with 'administer nodes' permission, as they + // can change the Published checkbox and moderation radio buttons, so we + // have to deal with that client-side, see file revision-schedule.js + if ($node->status == NODE_PUBLISHED) { + return; + } + if (empty($node->revision_moderation) && !(empty($node->nid) && revisioning_content_is_moderated($node->type, $node))) { + // No moderation specified and not a new node of a type subject to + // moderation. + return; + } + if (!revisioning_user_node_access('publish revisions', $node)) { + return; + } + } + // Don't offer the form if auto-publish is enabled for this node and user. + if (revisioning_user_may_auto_publish($node)) { + return; + } + + $date_format = variable_get('revisioning_scheduler_date_format'); + if (empty($date_format)) { + $date_format = REVISIONING_SCHEDULER_DEFAULT_DATE_FORMAT; + } + $description1 = t('Please use this format: %format, e.g %datetime. If you enter "now" this content will be published immediately.', + array( + '%format' => $date_format, + '%datetime' => format_date(time(), 'custom', $date_format), + ) + ); + $description2 = t('If you do not wish to schedule publication, leave the field blank.'); + + if (isset($node->vid)) { + $result = db_query('SELECT * FROM {revisioning_scheduler} WHERE revision_vid = :vid', + array(':vid' => $node->vid) + ); + $revision = $result->fetchAssoc(); + } + $scheduled_datetime = empty($revision) ? '' : format_date($revision['revision_date'], 'custom', $date_format); + + $form['revision_information']['publication_date'] = array( + '#type' => 'textfield', + '#size' => 25, + '#maxlength' => 25, + '#title' => t('Optionally schedule a date and time for publication'), + '#description' => $description1 . ' ' . $description2, + '#default_value' => $scheduled_datetime, + '#weight' => 10, + '#attributes' => array('class' => array('publication-date')), + ); + $form['#attached']['js'][] = drupal_get_path('module', 'revisioning_scheduler') . '/revision-schedule.js'; + } +} + +/** + * Implements hook_form_alter(). + * + * Adds date and time fields to the publication and reverting forms. + * Also shows the entered date and time on the revisions summary. + */ +function revisioning_scheduler_form_alter(&$form, $form_state, $form_id) { + + switch ($form_id) { + + case 'revisioning_publish_confirm': + case 'node_revision_revert_confirm': + $vid = arg(3); + $result = db_query('SELECT * FROM {revisioning_scheduler} WHERE revision_vid = :vid', + array(':vid' => $vid) + ); + $revision = $result->fetchAssoc(); + if (!empty($revision)) { + drupal_set_message(t('This revision was already scheduled by !username for publication on %date. You may override this date and time.', + array( + '%date' => format_date($revision['revision_date']), + '!username' => theme('username', array('account' => user_load($revision['revision_uid']))), + )), + 'warning', FALSE); + } + $date_format = variable_get('revisioning_scheduler_date_format'); + if (empty($date_format)) { + $date_format = REVISIONING_SCHEDULER_DEFAULT_DATE_FORMAT; + } + $date_and_time = explode(' ', date($date_format)); + $form['revisioning_scheduler_date'] = array( + '#title' => $form_id == 'node_revision_revert_confirm' ? t('Date for reversion') : t('Date for publication'), + '#type' => 'textfield', + '#description' => t('Enter the date you want this revision to be published.'), + '#maxlength' => 10, + '#size' => 10, + '#default_value' => $date_and_time[0], + '#weight' => -1, + ); + $form['revisioning_scheduler_time'] = array( + '#title' => $form_id == 'node_revision_revert_confirm' ? t('Time for reversion') : t('Time for publication'), + '#type' => 'textfield', + '#maxlength' => 5, + '#size' => 5, + '#default_value' => $date_and_time[1], + '#description' => t('Enter the time you want this revision to be published. Use the 24 hour clock.'), + '#weight' => 0, + ); + break; + + case 'revisioning_revisions_summary': + $result = db_query('SELECT * FROM {revisioning_scheduler} WHERE revision_nid = :nid', + array(':nid' => arg(1)) + ); + foreach ($result as $revision) { + if ($revision->revision_date > time()) { + $form['info'][$revision->revision_vid]['#markup'] .= '
' . t('Scheduled for publication on %date.', + array('%date' => format_date($revision->revision_date, 'long'))); + } + else { + $form['info'][$revision->revision_vid]['#markup'] .= '
' . t('Scheduled for publication at next cron run.'); + } + } + break; + } +} + +/** + * Implements hook_validate(). + */ +function revisioning_publish_confirm_validate($node, &$form) { + $date = check_plain($_POST['revisioning_scheduler_date']); + $date_format = variable_get('revisioning_scheduler_date_format'); + if (empty($date_format)) { + $date_format = REVISIONING_SCHEDULER_DEFAULT_DATE_FORMAT; + } + $date_only_format = drupal_substr($date_format, 0, strpos($date_format, ' ')); + if (strtotime($date) < strtotime(date($date_only_format))) { + form_set_error('revisioning_scheduler_date', t('The publication date you set is in the past.')); + } + else { + $time = check_plain($_POST['revisioning_scheduler_time']); + $scheduled_time = strtotime($date . $time); + // Add 90 seconds of slack to give user a chance to publish instantly by + // leaving time as is. + if ($scheduled_time < time() - REVISIONING_SCHEDULER_SLACK) { + form_set_error('revisioning_scheduler_time', t('The publication time you set is in the past.')); + } + } +} + +/** + * Implements hook_revisionapi(). + * + * @see revisioning/revisioning_api.inc + */ +function revisioning_scheduler_revisionapi($op, $node) { + switch ($op) { + case 'pre publish': + case 'post revert': + if (empty($_POST['revisioning_scheduler_date'])) { + break; + } + $date = check_plain($_POST['revisioning_scheduler_date']); + $time = check_plain($_POST['revisioning_scheduler_time']); + $result = _revisioning_scheduler_schedule_publication($date, $time, $node); + if (isset($result)) { + // This will abort the current publication operation. + return FALSE; + } + break; + + // The revision is being deleted. If it is scheduled for publishing, i.e. + // vid exists in {revisioning_scheduler} table, remove the scheduler entry. + case 'pre delete': + _revisioning_scheduler_unschedule($node->vid); + break; + } +} + +/** + * Implements hook_node_presave(). + * + * Called when saving, be it an edit or when creating a node. + * + * Picks up the value for the scheduled publication date (if entered) and + * decides whether the node should be published immediately or scheduled for a + * later date. + */ +function revisioning_scheduler_node_presave($node) { + if (!isset($node->nid)) { + // This may happen when importing files using Feeds module. + return; + } + if (empty($node->revision_moderation) || !empty($node->auto_publish)) { + _revisioning_scheduler_unschedule_all_revisions($node->nid); + } + elseif (!empty($node->publication_date)) { + $datetime = explode(' ', trim($node->publication_date)); + $date = $datetime[0]; + $time = isset($datetime[1]) ? $datetime[1] : '00:00'; + $node->publication_date = "$date $time"; + $scheduled_time = strtotime($node->publication_date); + if ($date == 'now' || ($scheduled_time > time() - REVISIONING_SCHEDULER_SLACK && $scheduled_time <= time())) { + // Publish immediately without scheduling. + // Follow the default saving process making this revision current and + // published, as opposed to pending. + unset($node->revision_moderation); + $node->status = NODE_PUBLISHED; + _revisioning_scheduler_unschedule_all_revisions($node->nid); + } + else { + // Schedule publication date. + return; + } + } + // Publication date does not apply in this situation. + unset($node->publication_date); +} + +/** + * Implements hook_node_insert(). + * + * Called when a new node has just been created. + */ +function revisioning_scheduler_node_insert($node) { + revisioning_scheduler_node_update($node); +} + +/** + * Implements hook_node_update(). + * + * This hook was chosen to invoke the scheduler because at this point vid has + * the new value. + */ +function revisioning_scheduler_node_update($node) { + if (empty($node->publication_date)) { + _revisioning_scheduler_unschedule($node->vid); + } + else { + $datetime = explode(' ', $node->publication_date); + _revisioning_scheduler_schedule_publication($datetime[0], $datetime[1], $node); + } +} + +/** + * Implements hook_node_delete(). + */ +function revisioning_scheduler_node_delete($node) { + // Delete scheduled publication of revisions of the deleted node. + _revisioning_scheduler_unschedule_all_revisions($node->nid); +} + +/** + * Implements hook_cron(). + * + * If there are any revisions with times that have passed, then publish them + * and delete them from the database. + */ +function revisioning_scheduler_cron() { + module_load_include('inc', 'revisioning', 'revisioning_api'); + $result = db_query('SELECT * FROM {revisioning_scheduler} WHERE revision_date <= :date', + array(':date' => time()) + ); + foreach ($result as $revision) { + if ($node_revision = node_load($revision->revision_nid, $revision->revision_vid)) { + _revisioning_publish_revision($node_revision); + } + _revisioning_scheduler_unschedule_all_revisions($revision->revision_nid); + } +} + +/** + * Schedule the supplied node for publication at the supplied date & time. + * + * @param string $date + * Publication date as a string, e.g. '25-12-2012' + * @param string $time + * Publication time as a string, e.g. '23:59' + * @param object $node + * The node object + * + * @return null|bool + * TRUE: revision successfully scheduled + * empty: revision not scheduled, should be published immediately + * FALSE: error, date & time in the past or database error + */ +function _revisioning_scheduler_schedule_publication($date, $time, $node) { + $date = trim($date); + $time = trim($time); + if (empty($time)) { + $time = '00:00'; + } + $scheduled_time = strtotime($date . $time); + if ($scheduled_time > time() - REVISIONING_SCHEDULER_SLACK) { + if ($scheduled_time <= time()) { + // Schedule immediately. + return; + } + + _revisioning_scheduler_unschedule_all_revisions($node->nid); + + global $user; + $data = array( + 'revision_nid' => $node->nid, + 'revision_vid' => $node->vid, + 'revision_uid' => $user->uid, + 'revision_date' => $scheduled_time, + ); + if (drupal_write_record('revisioning_scheduler', $data)) { + if ($scheduled_time > time()) { + drupal_set_message(t('Revision scheduled for publication at %time on %date.', array('%time' => $time, '%date' => $date))); + } + else { + // Should never get here. + drupal_set_message(t('Revision will be published at next cron run.')); + } + return TRUE; + } + } + drupal_set_message(t('Publication could not be scheduled at this date & time: %date %time.', array('%date' => $date, '%time' => $time)), 'error'); + return FALSE; +} + +/** + * Delete all scheduled publication dates for this node, if any. + * + * @param int $nid + * the unique node id + */ +function _revisioning_scheduler_unschedule_all_revisions($nid) { + return db_delete('revisioning_scheduler') + ->condition('revision_nid', $nid) + ->execute(); +} + +/** + * Check if there is a scheduled publication date for this revision. + * + * If so delete that date. + * + * @param int $vid + * the unique revision id + */ +function _revisioning_scheduler_unschedule($vid) { + return db_delete('revisioning_scheduler') + ->condition('revision_vid', $vid) + ->execute(); +} + +/** + * Register View API information. + */ +function revisioning_scheduler_views_api() { + return array( + 'api' => views_api_version(), + 'path' => drupal_get_path('module', 'revisioning_scheduler'), + ); +} diff --git a/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.views.inc b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.views.inc new file mode 100644 index 0000000..f958bd1 --- /dev/null +++ b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.views.inc @@ -0,0 +1,126 @@ + 'revision_vid', + 'title' => t('Content revisions scheduled'), + 'help' => t('A list of all revisions currently scheduled for publication.'), + 'defaults' => array( + 'field' => 'revision_date', + ), + ); + + // For other base tables (node_revision, users), explain how we join. + $data['revisioning_scheduler']['table']['join'] = array( + // Directly links to node_revision table. See also relationship details at + // $data['revisioning_scheduler']['revision_vid']. + 'node_revision' => array( + 'left_field' => 'vid', + 'field' => 'revision_vid', + ), + 'node' => array( + 'left_field' => 'nid', + 'field' => 'revision_nid', + ), + // Directly links to the users table. See also relationship details at + // $data['revisioning_scheduler']['revision_uid']. + 'users' => array( + 'left_field' => 'uid', + 'field' => 'revision_uid', + ), + ); + + $data['revisioning_scheduler']['revision_nid'] = array( + 'title' => t('Nid'), + 'help' => t('Node ID.'), + 'field' => array( + 'views_handler_field', + 'click sortable' => FALSE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_integer', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'relationship' => array( + 'base' => 'node', + 'field' => 'revision_nid', + 'handler' => 'views_handler_relationship', + 'label' => t('Content revision (NID)'), + ), + ); + + $data['revisioning_scheduler']['revision_date'] = array( + 'title' => t('Scheduled publication date'), + 'help' => t('Date & time on this content revision will be published.'), + 'field' => array( + 'handler' => 'views_handler_field_date', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_date', + ), + 'sort' => array( + 'handler' => 'views_handler_sort_date', + ), + ); + + $data['revisioning_scheduler']['revision_vid'] = array( + 'title' => t('Vid'), + 'help' => t('Revision ID.'), + 'field' => array( + 'views_handler_field', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_integer', + ), + 'relationship' => array( + 'base' => 'node_revision', + 'field' => 'revision_vid', + 'handler' => 'views_handler_relationship', + 'label' => t('Content revision (VID)'), + ), + ); + + $data['revisioning_scheduler']['revision_uid'] = array( + 'title' => t('Scheduled by'), + 'help' => t('User ID.'), + 'field' => array( + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'views_handler_filter_integer', + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'relationship' => array( + 'base' => 'users', + 'field' => 'revision_uid', + 'handler' => 'views_handler_relationship', + 'label' => t('User'), + ), + ); + + return $data; +} diff --git a/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.views_default.inc b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.views_default.inc new file mode 100644 index 0000000..95a1ae8 --- /dev/null +++ b/sites/all/modules/revisioning/revisioning_scheduler/revisioning_scheduler.views_default.inc @@ -0,0 +1,222 @@ +name = 'scheduled_publications'; + $view->description = 'A summary table of content revisions currently scheduled for publication.'; + $view->tag = 'default'; + $view->base_table = 'revisioning_scheduler'; + $view->human_name = 'Scheduled publications'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['title'] = 'Scheduled publications'; + $handler->display->display_options['access']['type'] = 'perm'; + $handler->display->display_options['access']['perm'] = 'publish revisions'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['query']['options']['query_comment'] = FALSE; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = '10'; + $handler->display->display_options['style_plugin'] = 'table'; + $handler->display->display_options['style_options']['group_rendered'] = 0; + $handler->display->display_options['style_options']['columns'] = array( + 'title' => 'title', + 'revision_vid' => 'revision_vid', + 'last_editor' => 'last_editor', + 'revision_date' => 'revision_date', + 'name' => 'name', + ); + $handler->display->display_options['style_options']['default'] = 'revision_date'; + $handler->display->display_options['style_options']['info'] = array( + 'title' => array( + 'sortable' => 1, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'revision_vid' => array( + 'sortable' => 1, + 'default_sort_order' => 'asc', + 'align' => 'views-align-right', + 'separator' => '', + 'empty_column' => 0, + ), + 'last_editor' => array( + 'sortable' => 1, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'revision_date' => array( + 'sortable' => 1, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'name' => array( + 'sortable' => 1, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + ); + $handler->display->display_options['style_options']['override'] = 1; + $handler->display->display_options['style_options']['sticky'] = 0; + $handler->display->display_options['style_options']['empty_table'] = 0; + /* Relationship: Scheduled content: Scheduled by */ + $handler->display->display_options['relationships']['revision_uid']['id'] = 'revision_uid'; + $handler->display->display_options['relationships']['revision_uid']['table'] = 'revisioning_scheduler'; + $handler->display->display_options['relationships']['revision_uid']['field'] = 'revision_uid'; + $handler->display->display_options['relationships']['revision_uid']['required'] = 1; + /* Relationship: Scheduled content: Vid */ + $handler->display->display_options['relationships']['revision_vid']['id'] = 'revision_vid'; + $handler->display->display_options['relationships']['revision_vid']['table'] = 'revisioning_scheduler'; + $handler->display->display_options['relationships']['revision_vid']['field'] = 'revision_vid'; + $handler->display->display_options['relationships']['revision_vid']['required'] = 1; + /* Field: Content revision: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node_revision'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['relationship'] = 'revision_vid'; + $handler->display->display_options['fields']['title']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['title']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['title']['alter']['absolute'] = 0; + $handler->display->display_options['fields']['title']['alter']['external'] = 0; + $handler->display->display_options['fields']['title']['alter']['replace_spaces'] = 0; + $handler->display->display_options['fields']['title']['alter']['trim_whitespace'] = 0; + $handler->display->display_options['fields']['title']['alter']['nl2br'] = 0; + $handler->display->display_options['fields']['title']['alter']['word_boundary'] = 1; + $handler->display->display_options['fields']['title']['alter']['ellipsis'] = 1; + $handler->display->display_options['fields']['title']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['title']['alter']['trim'] = 0; + $handler->display->display_options['fields']['title']['alter']['html'] = 0; + $handler->display->display_options['fields']['title']['element_label_colon'] = 1; + $handler->display->display_options['fields']['title']['element_default_classes'] = 1; + $handler->display->display_options['fields']['title']['hide_empty'] = 0; + $handler->display->display_options['fields']['title']['empty_zero'] = 0; + $handler->display->display_options['fields']['title']['hide_alter_empty'] = 1; + $handler->display->display_options['fields']['title']['link_to_node'] = 0; + $handler->display->display_options['fields']['title']['link_to_node_revision'] = 1; + /* Field: Scheduled content: Vid */ + $handler->display->display_options['fields']['revision_vid']['id'] = 'revision_vid'; + $handler->display->display_options['fields']['revision_vid']['table'] = 'revisioning_scheduler'; + $handler->display->display_options['fields']['revision_vid']['field'] = 'revision_vid'; + $handler->display->display_options['fields']['revision_vid']['label'] = 'Rev. ID'; + $handler->display->display_options['fields']['revision_vid']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['revision_vid']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['revision_vid']['alter']['absolute'] = 0; + $handler->display->display_options['fields']['revision_vid']['alter']['external'] = 0; + $handler->display->display_options['fields']['revision_vid']['alter']['replace_spaces'] = 0; + $handler->display->display_options['fields']['revision_vid']['alter']['trim_whitespace'] = 0; + $handler->display->display_options['fields']['revision_vid']['alter']['nl2br'] = 0; + $handler->display->display_options['fields']['revision_vid']['alter']['word_boundary'] = 1; + $handler->display->display_options['fields']['revision_vid']['alter']['ellipsis'] = 1; + $handler->display->display_options['fields']['revision_vid']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['revision_vid']['alter']['trim'] = 0; + $handler->display->display_options['fields']['revision_vid']['alter']['html'] = 0; + $handler->display->display_options['fields']['revision_vid']['element_label_colon'] = 1; + $handler->display->display_options['fields']['revision_vid']['element_default_classes'] = 1; + $handler->display->display_options['fields']['revision_vid']['hide_empty'] = 0; + $handler->display->display_options['fields']['revision_vid']['empty_zero'] = 0; + $handler->display->display_options['fields']['revision_vid']['hide_alter_empty'] = 1; + /* Field: Content: Last edited by */ + $handler->display->display_options['fields']['last_editor']['id'] = 'last_editor'; + $handler->display->display_options['fields']['last_editor']['table'] = 'node'; + $handler->display->display_options['fields']['last_editor']['field'] = 'last_editor'; + $handler->display->display_options['fields']['last_editor']['relationship'] = 'revision_vid'; + $handler->display->display_options['fields']['last_editor']['label'] = 'Edited by'; + $handler->display->display_options['fields']['last_editor']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['last_editor']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['last_editor']['alter']['path'] = 'display->display_options['fields']['last_editor']['alter']['absolute'] = 0; + $handler->display->display_options['fields']['last_editor']['alter']['external'] = 0; + $handler->display->display_options['fields']['last_editor']['alter']['replace_spaces'] = 0; + $handler->display->display_options['fields']['last_editor']['alter']['trim_whitespace'] = 0; + $handler->display->display_options['fields']['last_editor']['alter']['nl2br'] = 0; + $handler->display->display_options['fields']['last_editor']['alter']['word_boundary'] = 1; + $handler->display->display_options['fields']['last_editor']['alter']['ellipsis'] = 1; + $handler->display->display_options['fields']['last_editor']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['last_editor']['alter']['trim'] = 0; + $handler->display->display_options['fields']['last_editor']['alter']['html'] = 0; + $handler->display->display_options['fields']['last_editor']['element_label_colon'] = 1; + $handler->display->display_options['fields']['last_editor']['element_default_classes'] = 1; + $handler->display->display_options['fields']['last_editor']['hide_empty'] = 0; + $handler->display->display_options['fields']['last_editor']['empty_zero'] = 0; + $handler->display->display_options['fields']['last_editor']['hide_alter_empty'] = 1; + /* Field: Scheduled content: Scheduled publication date */ + $handler->display->display_options['fields']['revision_date']['id'] = 'revision_date'; + $handler->display->display_options['fields']['revision_date']['table'] = 'revisioning_scheduler'; + $handler->display->display_options['fields']['revision_date']['field'] = 'revision_date'; + $handler->display->display_options['fields']['revision_date']['label'] = 'Scheduled for publication'; + $handler->display->display_options['fields']['revision_date']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['revision_date']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['revision_date']['alter']['absolute'] = 0; + $handler->display->display_options['fields']['revision_date']['alter']['external'] = 0; + $handler->display->display_options['fields']['revision_date']['alter']['replace_spaces'] = 0; + $handler->display->display_options['fields']['revision_date']['alter']['trim_whitespace'] = 0; + $handler->display->display_options['fields']['revision_date']['alter']['nl2br'] = 0; + $handler->display->display_options['fields']['revision_date']['alter']['word_boundary'] = 1; + $handler->display->display_options['fields']['revision_date']['alter']['ellipsis'] = 1; + $handler->display->display_options['fields']['revision_date']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['revision_date']['alter']['trim'] = 0; + $handler->display->display_options['fields']['revision_date']['alter']['html'] = 0; + $handler->display->display_options['fields']['revision_date']['element_label_colon'] = 1; + $handler->display->display_options['fields']['revision_date']['element_default_classes'] = 1; + $handler->display->display_options['fields']['revision_date']['hide_empty'] = 0; + $handler->display->display_options['fields']['revision_date']['empty_zero'] = 0; + $handler->display->display_options['fields']['revision_date']['hide_alter_empty'] = 1; + $handler->display->display_options['fields']['revision_date']['date_format'] = 'long'; + /* Field: User: Name */ + $handler->display->display_options['fields']['name']['id'] = 'name'; + $handler->display->display_options['fields']['name']['table'] = 'users'; + $handler->display->display_options['fields']['name']['field'] = 'name'; + $handler->display->display_options['fields']['name']['relationship'] = 'revision_uid'; + $handler->display->display_options['fields']['name']['label'] = 'Scheduled by'; + $handler->display->display_options['fields']['name']['alter']['alter_text'] = 0; + $handler->display->display_options['fields']['name']['alter']['make_link'] = 0; + $handler->display->display_options['fields']['name']['alter']['absolute'] = 0; + $handler->display->display_options['fields']['name']['alter']['external'] = 0; + $handler->display->display_options['fields']['name']['alter']['replace_spaces'] = 0; + $handler->display->display_options['fields']['name']['alter']['trim_whitespace'] = 0; + $handler->display->display_options['fields']['name']['alter']['nl2br'] = 0; + $handler->display->display_options['fields']['name']['alter']['word_boundary'] = 1; + $handler->display->display_options['fields']['name']['alter']['ellipsis'] = 1; + $handler->display->display_options['fields']['name']['alter']['strip_tags'] = 0; + $handler->display->display_options['fields']['name']['alter']['trim'] = 0; + $handler->display->display_options['fields']['name']['alter']['html'] = 0; + $handler->display->display_options['fields']['name']['element_label_colon'] = 1; + $handler->display->display_options['fields']['name']['element_default_classes'] = 1; + $handler->display->display_options['fields']['name']['hide_empty'] = 0; + $handler->display->display_options['fields']['name']['empty_zero'] = 0; + $handler->display->display_options['fields']['name']['hide_alter_empty'] = 1; + $handler->display->display_options['fields']['name']['link_to_user'] = 1; + $handler->display->display_options['fields']['name']['overwrite_anonymous'] = 0; + $handler->display->display_options['fields']['name']['format_username'] = 1; + + /* Display: Page */ + $handler = $view->new_display('page', 'Page', 'page'); + $handler->display->display_options['path'] = 'scheduled-publications'; + $handler->display->display_options['menu']['type'] = 'normal'; + $handler->display->display_options['menu']['title'] = 'Scheduled publications'; + + $views[$view->name] = $view; + return $views; +} diff --git a/sites/all/modules/revisioning/revisioning_theme.inc b/sites/all/modules/revisioning/revisioning_theme.inc new file mode 100644 index 0000000..fd01eb7 --- /dev/null +++ b/sites/all/modules/revisioning/revisioning_theme.inc @@ -0,0 +1,362 @@ + 'form', + ); + $theme['revisions_pending_block'] = array( + // Uses revisions-pending-block.tpl.php. + 'template' => 'revisions-pending-block', + ); + $theme['revisions_summary'] = array( + // Uses revisions-summary.tpl.php. + 'template' => 'revisions-summary', + ); + return $theme; +} + +/** + * Return revisions summary table data. + * + * If the Diff modules is enabled, the + * object returned includes a column of checkboxes allowing the user to select + * two revisions for side-by-side comparison. + * + * @param array $form + * typically an empty array as passed in by drupal_get_form() + * @param array $form_state + * the form state + * @param int $extra + * parameters as passed into drupal_get_form(), see _theme_revisions_summary() + * + * @return array + * updated form containing all data to be themed + */ +function revisioning_revisions_summary($form, &$form_state, $extra) { + + // #type=>'value' form field values will not appear in the HTML. Used here + // to pass the node id to theme_revisioning_revisions_summary(). + // Or: $form_state['build_info']['args'][0]; ? + $nid = $extra; + $form['nid'] = array('#type' => 'value', '#value' => $nid); + + $show_taxonomy_terms = module_exists('taxonomy') && + variable_get('revisioning_show_taxonomy_terms', TRUE) && (count(taxonomy_get_vocabularies()) > 0); + + $revisions = _revisioning_get_all_revisions_for_node($nid, $show_taxonomy_terms); + + $revision_ids = array(); + $published = FALSE; + foreach ($revisions as $revision) { + $vid = $revision->vid; + if ($vid == $revision->current) { + $title = $revision->title; + $published = $revision->status; + } + // No text next to check boxes (see below). + $revision_ids[$vid] = ''; + $base_url = "node/$nid/revisions/$vid"; + + // First column: saved date + author. + $first_cell = t('Saved !date by !username', + array( + '!date' => l(format_date($revision->timestamp, 'short'), "$base_url/view"), + '!username' => theme('username', array('account' => $revision)))) + . (empty($revision->log) ? '' : '

' . filter_xss($revision->log) . '

'); + $form['info'][$vid] = array( + // Was: 'item', see [#1884696]. + '#type' => 'markup', + '#markup' => $first_cell, + ); + + // Third & fourth columns: term (2nd column is handled below). + if (!empty($revision->tags)) { + $form['tags'][$vid] = array( + '#type' => 'item', + '#markup' => $revision->tags, + ); + $has_tags = TRUE; + } + if (!empty($revision->term)) { + $form['term'][$vid] = array( + '#type' => 'item', + '#markup' => $revision->term, + ); + $has_terms = TRUE; + } + $form['status'][$vid] = array( + '#type' => 'value', + '#value' => $revision->status, + ); + } + // Close foreach ($revisions as $revision). + if (empty($has_tags)) { + unset($form['tags']); + } + if (empty($has_terms)) { + unset($form['term']); + } + revisioning_set_status_message(format_plural(count($revisions), + '%title is @publication_status. It has only one revision', + '%title is @publication_status. It has @count revisions.', + array( + '%title' => $title, + '@publication_status' => ($published ? t('published') : t('NOT published')), + ) + )); + if (count($revisions) >= 2 && module_exists('diff')) { + // Second column: check-boxes to select two revisions to compare + // The default selection is the top two check-boxes + $id1 = key($revision_ids); + next($revision_ids); + $id2 = key($revision_ids); + $form['tickbox'] = array( + '#type' => 'checkboxes', + '#options' => $revision_ids, + '#default_value' => array($id1, $id2), + '#required' => TRUE, + ); + // Submit button. + $form['submit'] = array('#value' => t('Compare'), '#type' => 'submit'); + } + return $form; +} + +/** + * Validation for input form to select two revisions. + * + * @param array $form + * The form + * @param array $form_state + * the form state + */ +function revisioning_revisions_summary_validate($form, &$form_state) { + // Strip out all unchecked boxes. + $form_state['values']['tickbox'] = array_filter($form_state['values']['tickbox']); + $count = count($form_state['values']['tickbox']); + if ($count != 2) { + form_set_error('tickbox', t('Please select 2 revisions rather than @count', array('@count' => $count))); + } +} + +/** + * Submit two selected revisions to Diff module. + * + * @param array $form + * The form + * @param array $form_state + * the form state + */ +function revisioning_revisions_summary_submit($form, &$form_state) { + $selected_vids = $form_state['values']['tickbox']; + $vid1 = key($selected_vids); + next($selected_vids); + $vid2 = key($selected_vids); + // Clear existing msgs. + drupal_get_messages(); + revisioning_set_status_message(t('Comparing revision #!revision2 against revision #!revision1', + array('!revision2' => $vid2, '!revision1' => $vid1))); + $nid = $form_state['values']['nid']; + $form_state['redirect'] = "node/$nid/revisions/view/$vid2/$vid1"; +} + +/** + * Theme revision summary table. + * + * Theme the supplied form as a table, then prepend submenu links via + * revisions-summary.tpl.php + * + * Uses the following subthemes: + * o 'table_revisions', falling back to theme.inc/theme_table() if not defined + * o 'placeholder' (to display current revision status) + * o 'username' + * Uses the following style-classes (see revisioning.css) + * o 'table-revisions' + * o 'revision-current' and 'published' + * o 'revision-pending' + * + * @param array $variables + * The variables array + * + * @return string + * The rendered html + */ +function theme_revisioning_revisions_summary($variables) { + + $form = $variables['form']; + if (!isset($form['nid'])) { + drupal_set_message(t('theme_revisioning_revisions_summary(): form does not contain nid - aborting.'), 'error'); + return; + } + + // Need node info, fortunately node_load() employs a cache so is efficient. + $node = node_load($form['nid']['#value']); + + drupal_add_css(drupal_get_path('module', 'revisioning') . '/css/revisioning.css'); + + // Set up the table rows. + $rows = array(); + + $revision_ids = element_children($form['info']); + $show_diff = count($revision_ids) >= 2 && module_exists('diff'); + + // Set up the table header. + $header = array(t('Revision')); + if ($show_diff) { + $header[] = array('data' => drupal_render($form['submit']), 'class' => 'form-submit'); + } + if (isset($form['tags'])) { + $header[] = t('Tags'); + } + if (isset($form['term'])) { + $header[] = t('Terms'); + } + $header[] = t('Status'); + + $is_initial_unpublished_draft = !$node->status && (count($revision_ids) == 1); + + // From the $form columns create table $rows. + foreach ($revision_ids as $vid) { + $row = array(); + // Column #1: Revision info. + $row[] = drupal_render($form['info'][$vid]); + + // Column #2: Compare checkbox. + if ($show_diff) { + $row[] = array('data' => drupal_render($form['tickbox'][$vid])); + } + + // Columns #3 & #4: Tags & Terms. + if (isset($form['tags'])) { + $row[] = filter_xss(drupal_render($form['tags'][$vid])); + } + if (isset($form['term'])) { + $row[] = filter_xss(drupal_render($form['term'][$vid])); + } + + // Column #4: Publication status. + $is_current = ($vid == $node->vid); + $is_pending = ($vid > $node->vid) || $is_initial_unpublished_draft; + if ($is_pending) { + $status = array('data' => t('in draft/pending publication')); + } + else { + $status = array('data' => $is_current && $node->status ? drupal_placeholder(t('current revision (published)')) : t('archived')); + } + $row[] = $status; + + // Apply CSS class. + $row_style = $is_current ? array('revision-current') : ($is_pending ? array('revision-pending') : array()); + if ($is_current && $node->status == NODE_PUBLISHED) { + $row_style[] = 'published'; + } + $rows[] = array('data' => $row, 'class' => $row_style); + } + // Render $header and $rows as a table. + $table_variables = array( + 'header' => $header, + 'rows' => $rows, + 'attributes' => array('class' => array('table-revisions')), + ); + $content = theme(array('table_revisions', 'table'), $table_variables); + + // Prepend submenu links. + $submenu_links = _revisioning_generate_node_links_according_to_permissions($node); + + // Combine submenu links and rendered table using a template (.tpl.php) file. + $template_variables = array( + 'submenu_links' => $submenu_links, + 'content' => $content, + ); + $output = theme(array('revisions_summary'), $template_variables); + + return $output . drupal_render_children($form); +} + +/* + * Implement (in your own module) the function below if you want to override + * the way in which the Revisions table is constructed. + * If you do, don't forget to register this theme_hook() via _theme() + * in a manner similar to revisioning_theme() in this file. + * + * @param $header + * @param $rows + * @return themed HTML, see for instance /includes/theme.inc/theme_table() and + * diff.module/theme_diff_table() + * + * @ingroup themeable + * + function theme_table_revisions($header, $rows) { + } +*/ + +/** + * Theme the revisions summary of the supplied node. + * + * @param object $node + * Node whose revisions to display + * + * @return array + * Render array + * + * @ingroup themeable + */ +function _revisioning_theme_revisions_summary($node) { + drupal_set_title(t('Revisions for %title', array('%title' => $node->title)), PASS_THROUGH); + return drupal_get_form('revisioning_revisions_summary', $node->nid); +} + + +/** + * Get link operations. + * + * Return an array of hyperlinks representing the operations the logged-in user + * is allowed to perform on the supplied node. + * + * @param object $node + * The node obkect + * @param int $link_type + * The type of link, e.g. MENU_IS_LOCAL_TASK, may affect the rendering via + * theme('menu_item_link'), if overridden (eg zen_theme_menu_item_link()). + * + * @return array + * array of themed hyperlinks + */ +function _revisioning_generate_node_links_according_to_permissions($node, $link_type = 0) { + $nid = $node->nid; + $themed_links = array(); + if (!empty($node->revision_moderation)) { + if (_revisioning_access_node_revision('publish revisions', $node)) { + $themed_links[] = l(t('Publish'), "node/$nid/revisions/$node->vid/publish"); + } + elseif (_revisioning_access_node_revision('unpublish current revision', $node)) { + $themed_links[] = l(t('Unpublish current revision'), "node/$nid/unpublish-current"); + } + } + if (_revisioning_access_node_revision('delete archived revisions', $node)) { + $num_archived = revisioning_get_number_of_archived_revisions($node); + if ($num_archived > 0) { + $themed_links[] = l(t('Delete archived'), "node/$nid/revisions/delete-archived"); + } + } + if (_revisioning_access_node_revision('delete node', $node)) { + // Avoiding format_plural see [#557050]. + $text = (empty($node->revision_moderation) || $node->num_revisions == 1) ? t('Delete') : t('Delete all'); + $themed_links[] = l($text, "node/$nid/delete"); + } + return $themed_links; +} diff --git a/sites/all/modules/revisioning/revisioning_tokens.inc b/sites/all/modules/revisioning/revisioning_tokens.inc new file mode 100644 index 0000000..c413349 --- /dev/null +++ b/sites/all/modules/revisioning/revisioning_tokens.inc @@ -0,0 +1,110 @@ + t("Revisions"), + 'description' => t('Tokens related to revisions of individual content items, or "node revisions".'), + 'needs-data' => 'node', + ); + // Then specify the tokens. + $info['tokens']['revision']['revision-author'] = array( + 'name' => t("Revision author"), + 'description' => t("The author (or editor) of the revision"), + 'type' => 'user', + ); + $info['tokens']['revision']['revision-body'] = array( + 'name' => t("Revision body"), + 'description' => t("The main body text of the revision"), + ); + $info['tokens']['revision']['revision-created'] = array( + 'name' => t("Revision timestamp"), + 'description' => t("The date and time the revision was created."), + 'type' => 'date', + ); + $info['tokens']['revision']['revision-title'] = array( + 'name' => t("Revision title"), + 'description' => t("The title of the revision"), + ); + $info['tokens']['revision']['revision-vid'] = array( + 'name' => t("Revision ID"), + 'description' => t("The unique ID of the revision"), + ); + return $info; +} + +/** + * Implements hook_tokens(). + */ +function revisioning_tokens($type, $tokens, array $data = array(), array $options = array()) { + $replacements = array(); + + if (!empty($data['revision'])) { + $revision = $data['revision']; + } + elseif (!empty($data['entity']) && $data['entity_type'] == 'node') { + // When Token module is enabled. + $revision = $data['entity']; + } + elseif (!empty($data['node'])) { + $revision = $data['node']; + } + else { + return $replacements; + } + + $sanitize = !empty($options['sanitize']); + + foreach ($tokens as $name => $original) { + switch ($name) { + + case 'revision-title': + $title = $revision->title; + $replacements[$original] = $sanitize ? check_plain($title) : $title; + break; + + case 'revision-body': + $text = reset($revision->body); + $text = $text[0]['value']; + $replacements[$original] = $sanitize ? check_plain($text) : $text; + break; + + case 'revision-vid': + $replacements[$original] = $revision->vid; + break; + + // Default values for the chained tokens handled below. + case 'revision-author': + $author = user_load($revision->revision_uid); + $name = format_username($author); + $replacements[$original] = $sanitize ? check_plain($name) : $name; + break; + + case 'revision-created': + $langcode = empty($options['language']->language) ? LANGUAGE_NONE : $options['language']->language; + $replacements[$original] = format_date($revision->revision_timestamp, 'medium', '', NULL, $langcode); + break; + } + } + // Chained tokens for revision author and revision timestamp. + // These fan out into sub-token fields, e.g revision-author:mail etc. + if ($author_tokens = token_find_with_prefix($tokens, 'revision-author')) { + $author = user_load($revision->revision_uid); + $replacements += token_generate('user', $author_tokens, array('user' => $author), $options); + } + if ($created_tokens = token_find_with_prefix($tokens, 'revision-created')) { + $replacements += token_generate('date', $created_tokens, array('date' => $revision->revision_timestamp), $options); + } + return $replacements; +} diff --git a/sites/all/modules/revisioning/revisioning_triggers_actions.inc b/sites/all/modules/revisioning/revisioning_triggers_actions.inc new file mode 100644 index 0000000..abe1422 --- /dev/null +++ b/sites/all/modules/revisioning/revisioning_triggers_actions.inc @@ -0,0 +1,222 @@ + array( + // Or 'node'. + 'revision_publish' => array( + 'label' => t('When publishing a pending revision'), + ), + 'revision_unpublish' => array( + 'label' => t('When unpublishing the current revision'), + ), + 'revision_revert' => array( + 'label' => t('When reverting to an archived revision'), + ), + ), + ); + return $trigger_info; +} + +/** + * Implements hook_revision_publish(). + * + * Called from revisioning_revisionapi(). + */ +function revisioning_revision_publish($revision) { + revisioning_action_revision_trigger($revision, 'revision_publish'); +} + +/** + * Implements hook_revision_unpublish(). + * + * Called from revisioning_revisionapi(). + */ +function revisioning_revision_unpublish($revision) { + revisioning_action_revision_trigger($revision, 'revision_unpublish'); +} + +/** + * Implements hook_revision_revert(). + * + * Called from revisioning_revisionapi(). + */ +function revisioning_revision_revert($revision) { + revisioning_action_revision_trigger($revision, 'revision_revert'); +} + +/** + * Execute all actions associated with the supplied trigger. + * + * @param object $revision + * the node object as passed in from revisioning_revisionapi(); + * if omitted this function will try to load the node object based on the URL + * + * @param string $hook + * trigger name, as passed in from revisioning_revision_hook() above, ie. one + * of 'revision_publish', 'revision_unpublish' or 'revision_revert'. + */ +function revisioning_action_revision_trigger($revision, $hook) { + if (!module_exists('trigger')) { + return; + } + $aids = trigger_get_assigned_actions($hook); + if (empty($aids)) { + // No actions defined for this trigger. + return; + } + // Prepare a context to pass to all the actions to be invoked. + // Include everything we can think of (important for token replacement). + // See token_tokens() + global $user; + $context = array( + 'group' => 'revisioning', + 'hook' => $hook, + 'comment' => NULL, + 'file' => NULL, + 'menu-link' => NULL, + 'node' => $revision, + 'node_type' => node_type_get_type($revision), + 'revision' => $revision, + 'path' => NULL, + // Use taxonomy_term_load() ? + 'term' => NULL, + 'user' => $user, + 'vocabulary' => NULL, + ); + // Loop through all actions attached to this trigger and load up the + // appropriate argument (eg node or user object) before invoking each action. + foreach ($aids as $aid => $info) { + $type = $info['type']; + $objects[$type] = NULL; + if (!isset($revision) && ($type == 'node' || $type == 'user')) { + drupal_set_message(t('Trigger %hook: no argument supplied to pass to @type action %aid', array( + '%hook' => $hook, '@type' => $type, '%aid' => $info['label'])), 'warning'); + } + watchdog('revisioning', '%hook trigger is actioning %label', + array('%hook' => $hook, '%label' => $info['label'])); + + if (!isset($objects[$type])) { + switch ($type) { + case 'node': + $objects[$type] = $revision; + break; + + case 'user': + $objects[$type] = user_load($revision->uid); + break; + + case 'comment': + // Not sure how. + break; + + default: + } + } + actions_do($aid, $objects[$type], $context); + } +} + +/* ---------------------------------- Actions ------------------------------ */ + +/** + * Implements hook_action_info(). + * + * Defines actions available in the Revisioning module. + */ +function revisioning_action_info() { + $action_info = array( + 'revisioning_delete_archived_action' => array( + 'type' => 'node', + 'label' => t('Delete archived revisions'), + 'configurable' => FALSE, + 'triggers' => array('any'), + ), + 'revisioning_publish_latest_revision_action' => array( + 'type' => 'node', + 'label' => t('Publish the most recent pending revision'), + // For 'configurable' => TRUE, then we must define a form function with + // the same name as the action function with '_form' appended. + 'configurable' => FALSE, + // 'behavior' => array('changes_property'), + // Unlike 'node_publish_action', this is NOT a 'node_presave' action: + 'triggers' => array('any'), + ), + ); + return $action_info; +} + +/** + * As declared in revisioning_action_info(). + * + * Called from actions.inc/actions_do() + * + * @param object $entity + * in our case the node in question + * @param array $context + * an array with $context['hook'] telling us which trigger + * instigated this call, eg 'node_update' as specified in the 'triggers' + * array, in hook_action_info() + */ +function revisioning_delete_archived_action(&$entity, $context = array()) { + $node = $entity; + if (empty($node->revision_moderation)) { + // return; + } + $num_archived = revisioning_get_number_of_archived_revisions($node); + if ($num_archived > 0) { + $type = node_type_get_type($node->type); + watchdog('revisioning', + 'Executing deleting archived revisions action for @type %title', array('@type' => $type->name, '%title' => $node->title), + WATCHDOG_NOTICE, l(t('view'), "node/$node->nid")); + if (revisioning_delete_archived_revisions($node)) { + revisioning_set_status_message(format_plural($num_archived, '@type %title: one archived revision deleted.', '@type %title: @count archived revisions deleted.', + array('@type' => $type->name, '%title' => $node->title))); + } + } +} + +/** + * As declared in revisioning_action_info(). + * + * Called from actions.inc/actions_do(). + * + * @param object $entity + * in our case the node in question + * @param array $context + * an array with $context['hook'] telling us which trigger + * instigated this call, eg 'node_presave' as specified in the 'triggers' + * array, in hook_action_info() + */ +function revisioning_publish_latest_revision_action(&$entity, $context = array()) { + $node = $entity; + $type = node_type_get_type($node->type); + watchdog('revisioning', + 'Executing publish_latest_revision action for @type %title', array('@type' => $type->name, '%title' => $node->title), + WATCHDOG_NOTICE, l(t('view'), "node/$node->nid")); + if (_revisioning_publish_latest_revision($node)) { + revisioning_set_status_message(t('Revision has been published.')); + } + else { + drupal_set_message(t('"%title" has no pending revision to be published.', array('%title' => $node->title)), 'warning'); + } +} diff --git a/sites/all/modules/revisioning/revisioning_ux/images/arrow-right.png b/sites/all/modules/revisioning/revisioning_ux/images/arrow-right.png new file mode 100644 index 0000000..6f2f482 Binary files /dev/null and b/sites/all/modules/revisioning/revisioning_ux/images/arrow-right.png differ diff --git a/sites/all/modules/revisioning/revisioning_ux/revisioning_ux.css b/sites/all/modules/revisioning/revisioning_ux/revisioning_ux.css new file mode 100644 index 0000000..a5cfd8e --- /dev/null +++ b/sites/all/modules/revisioning/revisioning_ux/revisioning_ux.css @@ -0,0 +1,15 @@ +.table-revisions .form-type-item { + padding-left: 24px; +} + +.revisions-active { + margin-left: -24px; + padding-left: 24px; + background: url('images/arrow-right.png') no-repeat; +} + +.revisioning-ux-buttons form { + float: left; + margin-right: 16px; + margin-bottom: 8px; +} diff --git a/sites/all/modules/revisioning/revisioning_ux/revisioning_ux.info b/sites/all/modules/revisioning/revisioning_ux/revisioning_ux.info new file mode 100644 index 0000000..4ba23cd --- /dev/null +++ b/sites/all/modules/revisioning/revisioning_ux/revisioning_ux.info @@ -0,0 +1,12 @@ +name = Revisioning UX +description = Revisioning user experience improvements for more consistent tab behavior +core = 7.x +configure = admin/config/content/revisioning +dependencies[] = revisioning + +; Information added by Drupal.org packaging script on 2014-06-26 +version = "7.x-1.9" +core = "7.x" +project = "revisioning" +datestamp = "1403741628" + diff --git a/sites/all/modules/revisioning/revisioning_ux/revisioning_ux.module b/sites/all/modules/revisioning/revisioning_ux/revisioning_ux.module new file mode 100644 index 0000000..ad37cab --- /dev/null +++ b/sites/all/modules/revisioning/revisioning_ux/revisioning_ux.module @@ -0,0 +1,266 @@ +' . $form['info'][$vid]['#markup'] . ''; + drupal_add_css(drupal_get_path('module', 'revisioning_ux') . '/revisioning_ux.css'); + } + } +} + +/** + * Implements hook_menu(). + */ +function revisioning_ux_menu() { + $items = array(); + + $items['node/%node/revisions/%vid/list'] = array( + 'title' => 'List all revisions', + 'access callback' => '_revisioning_access_node_revision', + 'access arguments' => array('view revision list', 1), + 'type' => MENU_LOCAL_TASK, + 'weight' => -20, + ); + + return $items; +} + +/** + * Implements hook_page_alter(). + */ +function revisioning_ux_page_alter(&$page) { + if (isset($page['content']['system_main']['#form_id']) && ($page['content']['system_main']['#form_id'] == 'revisioning_revisions_summary')) { + $node = node_load($page['content']['system_main']['nid']['#value']); + $page['content']['buttons']['#prefix'] = '
'; + $page['content']['buttons']['#suffix'] = '
'; + + if (!empty($node->revision_moderation)) { + if (_revisioning_access_node_revision('publish revisions', $node)) { + $page['content']['buttons']['publish'] = drupal_get_form('revisioning_ux_publish', $node); + } + elseif (_revisioning_access_node_revision('unpublish current revision', $node)) { + $page['content']['buttons']['unpublish'] = drupal_get_form('revisioning_ux_unpublish', $node); + } + } + if (_revisioning_access_node_revision('delete archived revisions', $node)) { + $num_archived = revisioning_get_number_of_archived_revisions($node); + if ($num_archived > 0) { + $page['content']['buttons']['delete-archived'] = drupal_get_form('revisioning_ux_delete_archived', $node); + } + } + if (_revisioning_access_node_revision('delete node', $node)) { + $page['content']['buttons']['delete-all'] = drupal_get_form('revisioning_ux_delete_all', $node); + } + } +} + +/** + * Implements hook_menu_alter(). + * + * Use a consistent title, 'Revision operations' for the + * revisions tab. Insure that the sub-tabs shown on + * the various Revisioning Operations pages remain consistent. + */ +function revisioning_ux_menu_alter(&$items) { + // Put back the standard access callback (remove revisioning callback that + // hides 'View' and 'Edit'). + $items['node/%node']['access callback'] = 'node_access'; + $items['node/%node/revisions']['page arguments'] = array( + 1, $items['node/%node/revisions']['page callback']); + $items['node/%node/revisions']['page callback'] = '_revisioning_ux_revisioning_redirect'; + $items['node/%node/revisions']['title callback'] = '_revisioning_ux_revisioning_title_for_tab'; + unset($items['node/%node/revisions/list']); + $items['node/%node/revisions/%vid/view']['title callback'] = '_revisioning_ux_revisioning_title_for_edit_and_view'; + $items['node/%node/revisions/%vid/view']['title arguments'] = array( + 1, 3, 'view'); + $items['node/%node/revisions/%vid/edit']['title callback'] = '_revisioning_ux_revisioning_title_for_edit_and_view'; + $items['node/%node/revisions/%vid/edit']['title arguments'] = array( + 1, 3, 'edit'); +} + +/** + * Implements hook_theme(). + * + * Load revisions-summary-reset.tpl.php instead of revsions-summary.tpl.php. + */ +function revisioning_ux_theme() { + $theme = array(); + $theme['revisions_summary'] = array( + // Uses revisions-summary-reset.tpl.php. + 'template' => 'revisions-summary-reset', + ); + return $theme; +} + +/** + * Redirect to revisions. + */ +function _revisioning_ux_revisioning_redirect($node, $was) { + $item = menu_get_item(); + if ($item['path'] == 'node/%/revisions') { + drupal_goto('node/' . $node->nid . '/revisions/' . $node->current_revision_id . '/list'); + } + return revisioning_node_overview($node); +} + +/** + * Get title for tab. + */ +function _revisioning_ux_revisioning_title_for_tab($node, $tab) { + return t('Revision operations'); +} + +/** + * Get title for edit and view. + */ +function _revisioning_ux_revisioning_title_for_edit_and_view($node, $vid, $tab) { + $latest_vid = revisioning_get_latest_revision_id($node->nid); + $title = ($tab == 'edit') ? t('Edit') : t('View'); + if ($vid == $node->current_revision_id) { + $title = ($tab == 'edit') ? t('Edit Current') : t('View Current'); + } + elseif ($vid == $latest_vid) { + $title = ($tab == 'edit') ? t('Edit Latest') : t('View Latest'); + } + return $title; +} + +/** + * Form builder: Publish. + */ +function revisioning_ux_publish($form, $form_state, $node) { + $form = array(); + $form['node'] = array( + '#type' => 'value', + '#value' => $node, + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Publish'), + '#submit' => array('revisioning_ux_publish_submit'), + ); + return $form; +} + +/** + * Form submit function for publish button. + */ +function revisioning_ux_publish_submit(&$form, &$form_state) { + if (isset($form_state['values']['node'])) { + $node = $form_state['values']['node']; + $form_state['redirect'] = "node/{$node->nid}/publish"; + } +} + +/** + * Form builder: Unpublish. + */ +function revisioning_ux_unpublish($form, $form_state, $node) { + $form = array(); + $form['node'] = array( + '#type' => 'value', + '#value' => $node, + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Unpublish current revision'), + '#submit' => array('revisioning_ux_unpublish_submit'), + ); + return $form; +} + +/** + * Form submit function for unpublish button. + */ +function revisioning_ux_unpublish_submit(&$form, &$form_state) { + if (isset($form_state['values']['node'])) { + $node = $form_state['values']['node']; + $form_state['redirect'] = "node/{$node->nid}/unpublish-current"; + } +} + +/** + * Form builder: Delete archived. + */ +function revisioning_ux_delete_archived($form, $form_state, $node) { + $form = array(); + $form['node'] = array( + '#type' => 'value', + '#value' => $node, + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Delete archived'), + '#submit' => array('revisioning_ux_delete_archived_submit'), + ); + return $form; +} + +/** + * Form submit function for delete archived button. + */ +function revisioning_ux_delete_archived_submit(&$form, &$form_state) { + if (isset($form_state['values']['node'])) { + $node = $form_state['values']['node']; + $form_state['redirect'] = "node/{$node->nid}/revisions/delete-archived"; + } +} + +/** + * Form builder: Delete all. + */ +function revisioning_ux_delete_all($form, $form_state, $node) { + $text = (empty($node->revision_moderation) || $node->num_revisions == 1) ? t('Delete') : t('Delete all'); + $form = array(); + $form['node'] = array( + '#type' => 'value', + '#value' => $node, + ); + $form['submit'] = array( + '#type' => 'submit', + '#value' => $text, + '#submit' => array('revisioning_ux_delete_all_submit'), + ); + return $form; +} + +/** + * Form submit function for delete all button. + */ +function revisioning_ux_delete_all_submit(&$form, &$form_state) { + if (isset($form_state['values']['node'])) { + $node = $form_state['values']['node']; + $form_state['redirect'] = "node/{$node->nid}/delete"; + } +} diff --git a/sites/all/modules/revisioning/revisioning_ux/revisions-summary-reset.tpl.php b/sites/all/modules/revisioning/revisioning_ux/revisions-summary-reset.tpl.php new file mode 100644 index 0000000..18d8084 --- /dev/null +++ b/sites/all/modules/revisioning/revisioning_ux/revisions-summary-reset.tpl.php @@ -0,0 +1,15 @@ +-tags + * - $content: summary of node revisions (as a table) + */ +?> +-tags + * - $content: summary of node revisions (as a table) + */ +?> + + +
+ + \ No newline at end of file diff --git a/sites/all/modules/revisioning/views/revisioning.views.inc b/sites/all/modules/revisioning/views/revisioning.views.inc new file mode 100644 index 0000000..9903ba7 --- /dev/null +++ b/sites/all/modules/revisioning/views/revisioning.views.inc @@ -0,0 +1,92 @@ + t('Last edited by'), + 'help' => t('The name of the user who last edited this content.'), + 'field' => array( + 'handler' => 'revisioning_handler_field_node_last_editor', + 'click sortable' => TRUE, + ), + ); + + $data['node']['state'] = array( + 'title' => t('State'), + 'help' => t('One of: pending, current, archived.'), + 'field' => array( + 'handler' => 'revisioning_handler_field_node_state', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'revisioning_handler_filter_node_state', + ), + // Cannot support sort as 'state' cannot be expressed as ORDER BY clause. + ); + + $data['node']['number_of_revisions'] = array( + 'title' => t('Revisions'), + 'help' => t('Number of revisions.'), + 'field' => array( + 'handler' => 'revisioning_handler_field_node_number_of_revisions', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'revisioning_handler_filter_node_number_of_revisions', + ), + // Cannot support sort as 'number of revisions' cannot be expressed as + // ORDER BY clause. + ); + + $data['node']['revision_moderation'] = array( + 'title' => t('Revision moderation'), + 'help' => t('Whether this content is subject to revision moderation.'), + 'field' => array( + 'handler' => 'revisioning_handler_field_node_revision_moderation', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'revisioning_handler_filter_node_revision_moderation', + ), + // Cannot support sort as this flag cannot be expressed as ORDER BY clause. + ); + + $data['node_revision']['title']['help'] = t('The title of the content revision.'); + + $data['node_revision']['state'] = array( + 'title' => t('State'), + 'help' => t('One of: pending, current, archived.'), + 'field' => array( + 'handler' => 'revisioning_handler_field_revision_state', + 'click sortable' => TRUE, + ), + 'filter' => array( + 'handler' => 'revisioning_handler_filter_revision_state', + ), + // Cannot support sort as 'state' cannot be expressed as ORDER BY clause. + ); + $data['node_revision']['latest'] = array( + 'title' => t('Latest'), + 'help' => t('Include only the most recent revisions.'), + 'filter' => array( + 'handler' => 'revisioning_handler_filter_revision_latest', + ), + ); + $data['node_revision']['latest_published'] = array( + 'title' => t('Published'), + 'help' => t('Include only currently published revisions.'), + 'filter' => array( + 'handler' => 'revisioning_handler_filter_revision_latest_published', + ), + ); +} diff --git a/sites/all/modules/revisioning/views/revisioning.views_default.inc b/sites/all/modules/revisioning/views/revisioning.views_default.inc new file mode 100644 index 0000000..7947565 --- /dev/null +++ b/sites/all/modules/revisioning/views/revisioning.views_default.inc @@ -0,0 +1,368 @@ +name = 'revisioning_content_summary'; + $view->description = 'Summary of site content with revision counts and other details, filtered by state.'; + $view->tag = 'Revisioning'; + $view->base_table = 'node'; + $view->human_name = 'Content summary'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Defaults */ + $handler = $view->new_display('default', 'Defaults', 'default'); + $handler->display->display_options['title'] = 'Summary of site content'; + $handler->display->display_options['use_more_always'] = FALSE; + $handler->display->display_options['access']['type'] = 'role'; + $handler->display->display_options['access']['role'] = array( + 2 => 2, + ); + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = 20; + $handler->display->display_options['style_plugin'] = 'table'; + $handler->display->display_options['style_options']['columns'] = array( + 'title' => 'title', + 'type' => 'type', + 'changed' => 'changed', + 'name' => 'name', + 'number_of_revisions' => 'number_of_revisions', + 'state' => 'state', + 'view_node' => 'view_node', + 'edit_node' => 'edit_node', + 'timestamp' => 'timestamp', + ); + $handler->display->display_options['style_options']['default'] = '-1'; + $handler->display->display_options['style_options']['info'] = array( + 'title' => array( + 'sortable' => 1, + 'separator' => '', + ), + 'type' => array( + 'sortable' => 1, + 'separator' => '', + ), + 'changed' => array( + 'sortable' => 1, + 'separator' => '', + ), + 'name' => array( + 'sortable' => 1, + 'separator' => '', + ), + 'number_of_revisions' => array( + 'sortable' => 0, + 'separator' => '', + ), + 'state' => array( + 'sortable' => 0, + 'separator' => '', + ), + 'view_node' => array( + 'separator' => '', + ), + 'edit_node' => array( + 'separator' => '', + ), + 'timestamp' => array( + 'separator' => '', + ), + ); + /* Header: Global: Text area */ + $handler->display->display_options['header']['text']['id'] = 'area'; + $handler->display->display_options['header']['text']['table'] = 'views'; + $handler->display->display_options['header']['text']['field'] = 'area'; + $handler->display->display_options['header']['text']['content'] = 'Summary of site content'; + $handler->display->display_options['header']['text']['format'] = 'filtered_html'; + /* No results behavior: Global: Text area */ + $handler->display->display_options['empty']['text']['id'] = 'area'; + $handler->display->display_options['empty']['text']['table'] = 'views'; + $handler->display->display_options['empty']['text']['field'] = 'area'; + $handler->display->display_options['empty']['text']['content'] = 'No content matches your query.'; + $handler->display->display_options['empty']['text']['format'] = 'filtered_html'; + /* Relationship: Content: Author */ + $handler->display->display_options['relationships']['uid']['id'] = 'uid'; + $handler->display->display_options['relationships']['uid']['table'] = 'node'; + $handler->display->display_options['relationships']['uid']['field'] = 'uid'; + /* Field: Content: Nid */ + $handler->display->display_options['fields']['nid']['id'] = 'nid'; + $handler->display->display_options['fields']['nid']['table'] = 'node'; + $handler->display->display_options['fields']['nid']['field'] = 'nid'; + $handler->display->display_options['fields']['nid']['exclude'] = TRUE; + /* Field: Content: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['alter']['make_link'] = TRUE; + $handler->display->display_options['fields']['title']['alter']['path'] = 'node/[nid]/view'; + $handler->display->display_options['fields']['title']['link_to_node'] = FALSE; + /* Field: Content: Type */ + $handler->display->display_options['fields']['type']['id'] = 'type'; + $handler->display->display_options['fields']['type']['table'] = 'node'; + $handler->display->display_options['fields']['type']['field'] = 'type'; + /* Field: Content: Updated date */ + $handler->display->display_options['fields']['changed']['id'] = 'changed'; + $handler->display->display_options['fields']['changed']['table'] = 'node'; + $handler->display->display_options['fields']['changed']['field'] = 'changed'; + $handler->display->display_options['fields']['changed']['label'] = 'Last updated'; + /* Field: User: Name */ + $handler->display->display_options['fields']['name']['id'] = 'name'; + $handler->display->display_options['fields']['name']['table'] = 'users'; + $handler->display->display_options['fields']['name']['field'] = 'name'; + $handler->display->display_options['fields']['name']['relationship'] = 'uid'; + $handler->display->display_options['fields']['name']['label'] = 'Author'; + /* Field: Content: Revision moderation */ + $handler->display->display_options['fields']['revision_moderation']['id'] = 'revision_moderation'; + $handler->display->display_options['fields']['revision_moderation']['table'] = 'node'; + $handler->display->display_options['fields']['revision_moderation']['field'] = 'revision_moderation'; + $handler->display->display_options['fields']['revision_moderation']['label'] = 'Mod?'; + $handler->display->display_options['fields']['revision_moderation']['alter']['alter_text'] = TRUE; + $handler->display->display_options['fields']['revision_moderation']['alter']['make_link'] = TRUE; + $handler->display->display_options['fields']['revision_moderation']['alter']['path'] = 'node/[nid]/revisions'; + $handler->display->display_options['fields']['revision_moderation']['empty'] = 'No'; + $handler->display->display_options['fields']['revision_moderation']['hide_empty'] = TRUE; + /* Field: Content: Revisions */ + $handler->display->display_options['fields']['number_of_revisions']['id'] = 'number_of_revisions'; + $handler->display->display_options['fields']['number_of_revisions']['table'] = 'node'; + $handler->display->display_options['fields']['number_of_revisions']['field'] = 'number_of_revisions'; + $handler->display->display_options['fields']['number_of_revisions']['label'] = 'Rev.'; + $handler->display->display_options['fields']['number_of_revisions']['alter']['alt'] = 'View revision summary'; + /* Field: Content: State */ + $handler->display->display_options['fields']['state']['id'] = 'state'; + $handler->display->display_options['fields']['state']['table'] = 'node'; + $handler->display->display_options['fields']['state']['field'] = 'state'; + /* Field: Content: Edit link */ + $handler->display->display_options['fields']['edit_node']['id'] = 'edit_node'; + $handler->display->display_options['fields']['edit_node']['table'] = 'views_entity_node'; + $handler->display->display_options['fields']['edit_node']['field'] = 'edit_node'; + $handler->display->display_options['fields']['edit_node']['label'] = ''; + /* Field: Content: Has new content */ + $handler->display->display_options['fields']['timestamp']['id'] = 'timestamp'; + $handler->display->display_options['fields']['timestamp']['table'] = 'history'; + $handler->display->display_options['fields']['timestamp']['field'] = 'timestamp'; + $handler->display->display_options['fields']['timestamp']['label'] = ''; + /* Sort criterion: Content: Updated date */ + $handler->display->display_options['sorts']['changed']['id'] = 'changed'; + $handler->display->display_options['sorts']['changed']['table'] = 'node'; + $handler->display->display_options['sorts']['changed']['field'] = 'changed'; + $handler->display->display_options['sorts']['changed']['order'] = 'DESC'; + /* Filter criterion: Content: Revision moderation */ + $handler->display->display_options['filters']['revision_moderation']['id'] = 'revision_moderation'; + $handler->display->display_options['filters']['revision_moderation']['table'] = 'node'; + $handler->display->display_options['filters']['revision_moderation']['field'] = 'revision_moderation'; + $handler->display->display_options['filters']['revision_moderation']['exposed'] = TRUE; + $handler->display->display_options['filters']['revision_moderation']['expose']['operator_id'] = 'revision_moderation_op'; + $handler->display->display_options['filters']['revision_moderation']['expose']['label'] = 'Moderated?'; + $handler->display->display_options['filters']['revision_moderation']['expose']['operator'] = 'revision_moderation_op'; + $handler->display->display_options['filters']['revision_moderation']['expose']['identifier'] = 'revision_moderation'; + $handler->display->display_options['filters']['revision_moderation']['expose']['remember'] = TRUE; + /* Filter criterion: Content: State */ + $handler->display->display_options['filters']['state']['id'] = 'state'; + $handler->display->display_options['filters']['state']['table'] = 'node'; + $handler->display->display_options['filters']['state']['field'] = 'state'; + $handler->display->display_options['filters']['state']['exposed'] = TRUE; + $handler->display->display_options['filters']['state']['expose']['operator_id'] = 'state_op'; + $handler->display->display_options['filters']['state']['expose']['label'] = 'State'; + $handler->display->display_options['filters']['state']['expose']['operator'] = 'state_op'; + $handler->display->display_options['filters']['state']['expose']['identifier'] = 'state'; + + /* Display: Page */ + $handler = $view->new_display('page', 'Page', 'page_1'); + $handler->display->display_options['path'] = 'content-summary'; + $handler->display->display_options['menu']['type'] = 'normal'; + $handler->display->display_options['menu']['title'] = 'Content summary'; + $handler->display->display_options['menu']['description'] = 'Summary of site content'; + $handler->display->display_options['menu']['weight'] = '-1'; + $translatables['revisioning_content_summary'] = array( + t('Defaults'), + t('Summary of site content'), + t('more'), + t('Apply'), + t('Reset'), + t('Sort by'), + t('Asc'), + t('Desc'), + t('Items per page'), + t('- All -'), + t('Offset'), + t('« first'), + t('‹ previous'), + t('next ›'), + t('last »'), + t('No content matches your query.'), + t('author'), + t('Nid'), + t('Title'), + t('Type'), + t('Last updated'), + t('Author'), + t('Mod?'), + t('No'), + t('Rev.'), + t('View revision summary'), + t('State'), + t('Moderated?'), + t('Page'), + ); + + $views[$view->name] = $view; + + $view = new view(); + $view->name = 'revisioning_content_revisions_summary'; + $view->description = 'A list of individual content revisions, filtered by state.'; + $view->tag = 'Revisioning'; + $view->base_table = 'node_revision'; + $view->human_name = 'Content summary (all revisions)'; + $view->core = 7; + $view->api_version = '3.0'; + $view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + + /* Display: Master */ + $handler = $view->new_display('default', 'Master', 'default'); + $handler->display->display_options['title'] = 'Content revisions summary'; + $handler->display->display_options['use_more_always'] = FALSE; + $handler->display->display_options['access']['type'] = 'perm'; + $handler->display->display_options['access']['perm'] = 'view revisions'; + $handler->display->display_options['cache']['type'] = 'none'; + $handler->display->display_options['query']['type'] = 'views_query'; + $handler->display->display_options['query']['options']['distinct'] = TRUE; + $handler->display->display_options['query']['options']['pure_distinct'] = TRUE; + $handler->display->display_options['exposed_form']['type'] = 'basic'; + $handler->display->display_options['pager']['type'] = 'full'; + $handler->display->display_options['pager']['options']['items_per_page'] = '20'; + $handler->display->display_options['style_plugin'] = 'table'; + $handler->display->display_options['style_options']['columns'] = array( + 'nid' => 'nid', + 'vid' => 'vid', + 'title' => 'title', + 'timestamp' => 'timestamp', + 'state' => 'state', + ); + $handler->display->display_options['style_options']['default'] = 'timestamp'; + $handler->display->display_options['style_options']['info'] = array( + 'nid' => array( + 'sortable' => 1, + 'default_sort_order' => 'desc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'vid' => array( + 'sortable' => 1, + 'default_sort_order' => 'desc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'title' => array( + 'sortable' => 1, + 'default_sort_order' => 'asc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'timestamp' => array( + 'sortable' => 1, + 'default_sort_order' => 'desc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + 'state' => array( + 'sortable' => 0, + 'default_sort_order' => 'desc', + 'align' => '', + 'separator' => '', + 'empty_column' => 0, + ), + ); + /* Field: Content revision: Nid */ + $handler->display->display_options['fields']['nid']['id'] = 'nid'; + $handler->display->display_options['fields']['nid']['table'] = 'node_revision'; + $handler->display->display_options['fields']['nid']['field'] = 'nid'; + /* Field: Content revision: Vid */ + $handler->display->display_options['fields']['vid']['id'] = 'vid'; + $handler->display->display_options['fields']['vid']['table'] = 'node_revision'; + $handler->display->display_options['fields']['vid']['field'] = 'vid'; + /* Field: Content revision: Title */ + $handler->display->display_options['fields']['title']['id'] = 'title'; + $handler->display->display_options['fields']['title']['table'] = 'node_revision'; + $handler->display->display_options['fields']['title']['field'] = 'title'; + $handler->display->display_options['fields']['title']['alter']['word_boundary'] = FALSE; + $handler->display->display_options['fields']['title']['alter']['ellipsis'] = FALSE; + $handler->display->display_options['fields']['title']['link_to_node_revision'] = TRUE; + /* Field: Content revision: Updated date */ + $handler->display->display_options['fields']['timestamp']['id'] = 'timestamp'; + $handler->display->display_options['fields']['timestamp']['table'] = 'node_revision'; + $handler->display->display_options['fields']['timestamp']['field'] = 'timestamp'; + $handler->display->display_options['fields']['timestamp']['alter']['word_boundary'] = FALSE; + $handler->display->display_options['fields']['timestamp']['alter']['ellipsis'] = FALSE; + /* Field: Content revision: State */ + $handler->display->display_options['fields']['state']['id'] = 'state'; + $handler->display->display_options['fields']['state']['table'] = 'node_revision'; + $handler->display->display_options['fields']['state']['field'] = 'state'; + /* Filter criterion: Content revision: State */ + $handler->display->display_options['filters']['state']['id'] = 'state'; + $handler->display->display_options['filters']['state']['table'] = 'node_revision'; + $handler->display->display_options['filters']['state']['field'] = 'state'; + $handler->display->display_options['filters']['state']['value'] = array( + 0 => '0', + 1 => '1', + 2 => '2', + ); + $handler->display->display_options['filters']['state']['exposed'] = TRUE; + $handler->display->display_options['filters']['state']['expose']['operator_id'] = 'state_op'; + $handler->display->display_options['filters']['state']['expose']['label'] = 'State'; + $handler->display->display_options['filters']['state']['expose']['operator'] = 'state_op'; + $handler->display->display_options['filters']['state']['expose']['identifier'] = 'state'; + $handler->display->display_options['filters']['state']['expose']['remember_roles'] = array( + 2 => '2', + 1 => 0, + 3 => 0, + 4 => 0, + 5 => 0, + ); + + /* Display: Page */ + $handler = $view->new_display('page', 'Page', 'page'); + $handler->display->display_options['path'] = 'content-revisions-summary'; + $translatables['revisioning_content_revisions_summary'] = array( + t('Master'), + t('Content revisions summary'), + t('more'), + t('Apply'), + t('Reset'), + t('Sort by'), + t('Asc'), + t('Desc'), + t('Items per page'), + t('- All -'), + t('Offset'), + t('« first'), + t('‹ previous'), + t('next ›'), + t('last »'), + t('Nid'), + t('Vid'), + t('Title'), + t('Updated date'), + t('State'), + t('Page'), + ); + $views[$view->name] = $view; + + return $views; +} diff --git a/sites/all/modules/revisioning/views/revisioning_handler_field_node_last_editor.inc b/sites/all/modules/revisioning/views/revisioning_handler_field_node_last_editor.inc new file mode 100644 index 0000000..9d07571 --- /dev/null +++ b/sites/all/modules/revisioning/views/revisioning_handler_field_node_last_editor.inc @@ -0,0 +1,84 @@ +additional_fields['nid'] = 'nid'; + } + + /** + * Check for access. + */ + public function access() { + return user_access('access content'); + } + + /** + * Help build the query. + */ + public function query() { + // Not calling parent::query() as it will treat 'last_editor' as a real db + // field. + $this->ensure_my_table(); + $this->add_additional_fields(); + } + + /** + * Render data. + */ + public function render($values) { + $nid = $values->{$this->aliases['nid']}; + $uid = revisioning_get_last_editor($nid, FALSE); + $account = user_load($uid); + return $account->name; + } + /* + * Consider this performance improvement from [#1852680]. + * Not tested. + * + function pre_render($values) { + // Collect Nodes + $nids = array(); + foreach ($values as $row) { + $nids[] = $row->{$this->aliases['nid']}; + } + + // If the view has no results, there is no last editor to determine. + if (empty($nids)) { + return; + } + + // Get newest revisions for nodes. + $sql = "SELECT MAX(vid) AS vid, nid FROM {node_revision} WHERE nid IN (:nids) GROUP BY nid"; + $result = db_query($sql, array(':nids' => $nids)); + $vids = array(); + while($vid = $result->fetchAssoc()) { + $vids[] = $vid['vid']; + } + + if (empty($vids)) { + return; + } + + // Get author name oft the revisions. + $result = db_query("SELECT node_revision.nid, users.name FROM {node_revision} JOIN {users} ON node_revision.uid=users.uid WHERE node_revision.vid IN (:vids)", array(':vids' => $vids)); + while($revision = $result->fetchAssoc()) { + $this->uids[$revision['nid']] = $revision['name']; + } + } + + function render($values) { + $nid = $values->{$this->aliases['nid']}; + return isset($this->uids[$nid])? $this->uids[$nid] : ''; + } + */ +} diff --git a/sites/all/modules/revisioning/views/revisioning_handler_field_node_number_of_revisions.inc b/sites/all/modules/revisioning/views/revisioning_handler_field_node_number_of_revisions.inc new file mode 100644 index 0000000..ee63048 --- /dev/null +++ b/sites/all/modules/revisioning/views/revisioning_handler_field_node_number_of_revisions.inc @@ -0,0 +1,42 @@ +additional_fields['nid'] = 'nid'; + } + + /** + * Check access. + */ + public function access() { + return user_access('access content'); + } + + /** + * Help build the query. + */ + public function query() { + // Not calling parent::query() as it will treat 'number of revisions' as + // a real db field. + $this->ensure_my_table(); + $this->add_additional_fields(); + } + + /** + * Render data. + */ + public function render($values) { + $nid = $values->{$this->aliases['nid']}; + return revisioning_get_number_of_revisions($nid); + } +} diff --git a/sites/all/modules/revisioning/views/revisioning_handler_field_node_revision_moderation.inc b/sites/all/modules/revisioning/views/revisioning_handler_field_node_revision_moderation.inc new file mode 100644 index 0000000..386414a --- /dev/null +++ b/sites/all/modules/revisioning/views/revisioning_handler_field_node_revision_moderation.inc @@ -0,0 +1,44 @@ +additional_fields['type'] = 'type'; + } + + /** + * Check for access. + */ + public function access() { + return user_access('access content'); + } + + /** + * Help build the query. + */ + public function query() { + // Not calling parent::query() as it will treat 'number of revisions' as + // a real db field. + $this->ensure_my_table(); + $this->add_additional_fields(); + } + + /** + * Render the data. + */ + public function render($values) { + $content_type = $values->{$this->aliases['type']}; + $moderated = revisioning_content_is_moderated($content_type); + // Not returning 'No' to allow special tricks with empty field. + return $moderated ? t('Yes') : ''; + } +} diff --git a/sites/all/modules/revisioning/views/revisioning_handler_field_node_state.inc b/sites/all/modules/revisioning/views/revisioning_handler_field_node_state.inc new file mode 100644 index 0000000..3ceea97 --- /dev/null +++ b/sites/all/modules/revisioning/views/revisioning_handler_field_node_state.inc @@ -0,0 +1,57 @@ +additional_fields['nid'] = 'nid'; + $this->additional_fields['vid'] = 'vid'; + $this->additional_fields['published'] = array('field' => 'status'); + } + + /** + * Check for access. + */ + public function access() { + return user_access('access content'); + } + + /** + * Help build the query. + */ + public function query() { + // Not calling parent::query() as it will treat 'state' as a real db field. + $this->ensure_my_table(); + $this->add_additional_fields(); + } + + /** + * Implement the rendering of the state value. + * + * Note that $values contains: + * o nid + * o node_vid (current revision id) + * o node_status (published flag) + */ + public function render($values) { + $published = $values->{$this->aliases['published']}; + $nid = $values->{$this->aliases['nid']}; + $current_vid = $values->{$this->aliases['vid']}; + $is_initial_unpublished_draft = !$published && (revisioning_get_number_of_revisions($nid) == 1); + $latest_vid = revisioning_get_latest_revision_id($nid); + $is_pending = ($latest_vid > $current_vid) || $is_initial_unpublished_draft; + $description = $is_pending + ? (!$published && !$is_initial_unpublished_draft ? t('Archived with revision pending') : t('Revision pending')) + : ($published && ($latest_vid == $current_vid) ? t('Current, published') : t('Archived')); + return $description; + } +} diff --git a/sites/all/modules/revisioning/views/revisioning_handler_field_revision_state.inc b/sites/all/modules/revisioning/views/revisioning_handler_field_revision_state.inc new file mode 100644 index 0000000..0708303 --- /dev/null +++ b/sites/all/modules/revisioning/views/revisioning_handler_field_revision_state.inc @@ -0,0 +1,78 @@ +additional_fields['nid'] = 'nid'; + $this->additional_fields['vid'] = 'vid'; + } + + /** + * Check for access. + */ + public function access() { + return user_access('view revisions'); + } + + /** + * Modify the query before it's executed. + */ + public function pre_query() { + $this->_node_based = $this->view->base_table == 'node' || (!empty($this->options['relationship']) && $this->options['relationship'] !== 'none'); + + if ($this->_node_based) { + $this->additional_fields['published'] = array('table' => 'node', 'field' => 'status'); + $this->additional_fields['current_vid'] = array('table' => 'node', 'field' => 'vid'); + } + } + + /** + * Help build the query. + */ + public function query() { + // Not calling parent::query() as it will treat 'state' as a real db field. + $this->ensure_my_table(); + $this->add_additional_fields(); + + if (empty($this->_node_based)) { + $join = new views_join(); + $join->construct('node', $this->table_alias, 'nid', 'nid'); + $table_alias = $this->query->ensure_table('node', $this->table_alias, $join); + $this->aliases['published'] = $this->query->add_field($table_alias, 'status'); + $this->aliases['current_vid'] = $this->query->add_field($table_alias, 'vid'); + } + } + + /** + * Render the data. + * + * Note that $values contains: + * o node_revisions_nid (parent node id) + * o node_revisions_vid (revision id) + * o node_status (published flag) + * o node_vid (current revision id) + */ + public function render($values) { + $nid = $values->{$this->aliases['nid']}; + $vid = $values->{$this->aliases['vid']}; + $published = $values->{$this->aliases['published']}; + $current_vid = $values->{$this->aliases['current_vid']}; + $is_initial_unpublished_draft = !$published && (revisioning_get_number_of_revisions($nid) == 1); + $is_pending = ($vid > $current_vid) || $is_initial_unpublished_draft; + $state_code = $is_pending ? REVISION_PENDING : ($vid == $current_vid ? REVISION_CURRENT : REVISION_ARCHIVED); + $description = revisioning_revision_states($state_code); + return $description; + } +} diff --git a/sites/all/modules/revisioning/views/revisioning_handler_filter_node_number_of_revisions.inc b/sites/all/modules/revisioning/views/revisioning_handler_filter_node_number_of_revisions.inc new file mode 100644 index 0000000..a8709d4 --- /dev/null +++ b/sites/all/modules/revisioning/views/revisioning_handler_filter_node_number_of_revisions.inc @@ -0,0 +1,35 @@ +value)) { + return; + } + + $info = $this->operators(); + $operator_symbol = drupal_strtoupper($info[$this->operator]['short']); + if (!empty($operator_symbol)) { + $node_table = $this->ensure_my_table(); + $revisions_table = $this->query->ensure_table('node_revision'); + $pseudo_field = "(SELECT COUNT(vid) FROM {$revisions_table} WHERE nid=$node_table.nid)"; + // Can't use add_where() as it sanitises (destroys) the $where_expression + // Have to use add_where_expression() and copy what add_where() does. + if ($operator_symbol == 'BETWEEN' || $operator_symbol == 'NOT BETWEEN') { + $where_expression = "$pseudo_field $operator_symbol '" . $this->value['min'] . "' AND '" . $this->value['max'] . "'"; + } + else { + $where_expression = "$pseudo_field $operator_symbol " . $this->value['value']; + } + $this->query->add_where_expression($this->options['group'], $where_expression); + } + } +} diff --git a/sites/all/modules/revisioning/views/revisioning_handler_filter_node_revision_moderation.inc b/sites/all/modules/revisioning/views/revisioning_handler_filter_node_revision_moderation.inc new file mode 100644 index 0000000..876b12c --- /dev/null +++ b/sites/all/modules/revisioning/views/revisioning_handler_filter_node_revision_moderation.inc @@ -0,0 +1,52 @@ +value) || (isset($this->value[0]) && isset($this->value[1]))) { + // Don't filter if none or both options are set. + return; + } + $moderated_content_types = array(); + foreach (revisioning_moderated_content_types() as $moderated_content_type) { + $moderated_content_types[] = "'$moderated_content_type'"; + } + $this->ensure_my_table(); + if (empty($moderated_content_types)) { + if (reset($this->value) == REVISIONING_MODERATED) { + // None of the content types are moderated, so return nothing. + $this->query->add_where($this->options['group'], '1 = 0'); + } + } + else { + $moderated = (reset($this->value) == REVISIONING_MODERATED); + if ($this->operator == 'not in') { + $moderated = !$moderated; + } + $where_operator = $moderated ? 'IN' : 'NOT IN'; + $where_expression = $this->table_alias . '.type ' . $where_operator . ' (' . implode(',', $moderated_content_types) . ')'; + $this->query->add_where_expression($this->options['group'], $where_expression); + } + } + + /** + * Get the value options. + */ + public function get_value_options() { + $this->value_title = t('Moderated'); + $this->value_options = array( + REVISIONING_MODERATED => t('Moderated'), + REVISIONING_NOT_MODERATED => t('Not moderated')); + } +} diff --git a/sites/all/modules/revisioning/views/revisioning_handler_filter_node_state.inc b/sites/all/modules/revisioning/views/revisioning_handler_filter_node_state.inc new file mode 100644 index 0000000..2d8a679 --- /dev/null +++ b/sites/all/modules/revisioning/views/revisioning_handler_filter_node_state.inc @@ -0,0 +1,61 @@ +value)) { + return; + } + $node_table = $this->ensure_my_table(); + $revisions_table = '{' . $this->query->ensure_table('node_revision') . '}'; + $subclauses = array(); + foreach ($this->value as $state_code) { + switch ($state_code) { + + case REVISION_ARCHIVED: + // A node is considered archived when it's unpublished and has 2 or + // more revisions. An unpublished node with 1 revision is considered + // pending. + $subclauses[] = "($node_table.status=0 AND (SELECT COUNT(vid) FROM $revisions_table WHERE nid=$node_table.nid)>1)"; + break; + + case REVISION_CURRENT: + // A node is considered up-to-date when it's published and its + // current revision is the latest revision (highest vid). + $subclauses[] = "($node_table.status=1 AND $node_table.vid=(SELECT MAX(vid) FROM $revisions_table WHERE nid=$node_table.nid))"; + break; + + case REVISION_PENDING: + // A node is pending when it's published with a current revision + // that's not the latest or when the node has a single, yet to be + // published revision. + $subclauses[] = "(($node_table.status=1 AND $node_table.vid<(SELECT MAX(vid) FROM $revisions_table WHERE nid=$node_table.nid)) OR ($node_table.status=0 AND (SELECT COUNT(vid) FROM $revisions_table WHERE nid=$node_table.nid)=1))"; + break; + } + } + if (!empty($subclauses)) { + $where_expression = implode(' OR ', $subclauses); + if ($this->operator == 'not in') { + $where_expression = "not ($where_expression)"; + } + $this->query->add_where_expression($this->options['group'], $where_expression); + } + } + + /** + * Get value options. + */ + public function get_value_options() { + $this->value_title = t('Revision state'); + $this->value_options = revisioning_revision_states(); + } +} diff --git a/sites/all/modules/revisioning/views/revisioning_handler_filter_revision_latest.inc b/sites/all/modules/revisioning/views/revisioning_handler_filter_revision_latest.inc new file mode 100644 index 0000000..bb9a373 --- /dev/null +++ b/sites/all/modules/revisioning/views/revisioning_handler_filter_revision_latest.inc @@ -0,0 +1,73 @@ +value_value = t('Revisions'); + } + + /** + * Check for access. + */ + public function access() { + return user_access('access content'); + } + + /** + * Is this filter exposible. + */ + public function can_expose() { + return TRUE; + } + + /** + * Exposed form elements. + */ + public function expose_form(&$form, &$form_state) { + $form['expose']['required'] = array( + '#type' => 'value', + '#value' => FALSE, + ); + } + + /** + * Get the value options. + */ + public function get_value_options() { + $this->value_options = array(1 => t('Latest only')); + } + + /** + * Override the query, in particular the WHERE clause. + */ + public function query() { + $revisions_table = $this->ensure_my_table(); + $node_table = $this->query->ensure_table('node'); + // If we are using a relationship we need to try again because the alias + // is not going to be node! + if (!$node_table) { + $node_table = $this->query->ensure_table('node_' . $revisions_table); + } + if (!$node_table) { + // Final desperate guess ... + $node_table = 'node'; + } + + // The subquery selects the maximum revision ID (vid) for a given node ID + // (nid). This is guaranteed to be the latest revision, because vids are + // assigned chronologically. + $max_vid_subquery = "SELECT MAX(vid) FROM $revisions_table WHERE $revisions_table.nid = $node_table.nid"; + + $where_clause = "$revisions_table.vid = ($max_vid_subquery)"; + $this->query->add_where_expression($this->options['group'], $where_clause); + } +} diff --git a/sites/all/modules/revisioning/views/revisioning_handler_filter_revision_latest_published.inc b/sites/all/modules/revisioning/views/revisioning_handler_filter_revision_latest_published.inc new file mode 100644 index 0000000..231da2c --- /dev/null +++ b/sites/all/modules/revisioning/views/revisioning_handler_filter_revision_latest_published.inc @@ -0,0 +1,60 @@ +value_value = t('Revisions'); + } + + /** + * Check for access. + */ + public function access() { + return user_access('access content'); + } + + /** + * Make filter exposible. + */ + public function can_expose() { + return TRUE; + } + + /** + * Exposed form element required. + */ + public function expose_form(&$form, &$form_state) { + $form['expose']['required'] = array( + '#type' => 'value', + '#value' => FALSE, + ); + } + + /** + * Get all value options. + */ + public function get_value_options() { + $this->value_options = array(1 => t('Published only')); + } + + /** + * Override the query, in particular the WHERE clause. + */ + public function query() { + $revisions_table = $this->ensure_my_table(); + $node_table = $this->query->ensure_table('node'); + + $where_clause = "$node_table.vid = $revisions_table.vid AND $node_table.status=1"; + + $this->query->add_where_expression($this->options['group'], $where_clause); + } +} diff --git a/sites/all/modules/revisioning/views/revisioning_handler_filter_revision_state.inc b/sites/all/modules/revisioning/views/revisioning_handler_filter_revision_state.inc new file mode 100644 index 0000000..a38e920 --- /dev/null +++ b/sites/all/modules/revisioning/views/revisioning_handler_filter_revision_state.inc @@ -0,0 +1,74 @@ +value)) { + return; + } + $revisions_table = $this->ensure_my_table(); + $node_table = $this->query->ensure_table('node', $this->relationship); + if (!$node_table) { + $node_table = $this->query->ensure_table('node'); + // If we are using a relationship we need to try again because the alias + // is not going to be node! + if (!$node_table) { + $node_table = $this->query->ensure_table('node_' . $revisions_table); + } + if (!$node_table) { + // Final desperate guess ... + $node_table = 'node'; + } + } + + $subclauses = array(); + foreach ($this->value as $state_code) { + switch ($state_code) { + + case REVISION_ARCHIVED: + $subclauses[] = "($revisions_table.vid<$node_table.vid)"; + break; + + case REVISION_CURRENT: + $subclauses[] = "($revisions_table.vid=$node_table.vid AND $node_table.status=1)"; + break; + + case REVISION_PENDING: + $subclauses[] = "($revisions_table.vid>$node_table.vid OR ($node_table.status=0 AND (SELECT COUNT(vid) FROM {" . $revisions_table . "} WHERE nid=$node_table.nid)=1))"; + // Only add this join if there is not already a 'Revision NID of the + // content revision' contextual filter relationship. + if (empty($this->query->relationships[$this->relationship])) { + // Make sure UNIQUE is set! + $this->query->table_queue[$revisions_table]['join'] = new views_join(); + $this->query->table_queue[$revisions_table]['join']->construct($node_table, $revisions_table, 'nid', 'nid'); + } + break; + } + } + if (!empty($subclauses)) { + $where_expression = implode(' OR ', $subclauses); + if ($this->operator == 'not in') { + $where_expression = '!' . $where_expression; + } + $this->query->add_where_expression($this->options['group'], $where_expression); + } + } + + /** + * Get value options. + */ + public function get_value_options() { + $this->value_title = t('Revision state'); + $this->value_options = revisioning_revision_states(); + } +} diff --git a/sites/all/modules/shs/LICENSE.txt b/sites/all/modules/shs/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/sites/all/modules/shs/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/sites/all/modules/shs/README.txt b/sites/all/modules/shs/README.txt new file mode 100644 index 0000000..4fa800a --- /dev/null +++ b/sites/all/modules/shs/README.txt @@ -0,0 +1,86 @@ + +-- SUMMARY -- + +The Simple hierarchical select module displays selected taxonomy fields as +hierarchical selects on node creation/edit forms and as exposed filter in views. + + +-- REQUIREMENTS -- + +Taxonomy module (Drupal core) needs to be enabled. + + +-- INSTALLATION -- + +* Install as usual, see http://drupal.org/documentation/install/modules-themes/modules-7 + for further information. + + +-- CONFIGURATION -- + +* Create a new field (type "Term reference") and select + "Simple hierarchical select" as widget type. + +* Field settings + - "Display number of nodes" + Displays the number of nodes associated to a term next to the term name in + the dropdown. + WARNING: on sites with a lot of terms and nodes this can be a great + performance killer (even if the data is cached). + - "Allow creating new terms" + Terms may be created directly from within the dropdowns (user needs to have + permission to create terms in the vocabulary). + - "Allow creating new levels" + If selected users with permission to create terms in the vocabulary will be + able to create a new term as child of the currently selected term. + - "Force selection of deepest level" + Force users to select terms from the deepest level. + +* Views (exposed filter) + - add a new filter using the field set-up as "Simple hierarchical select" or + use "Content: Has taxonomy terms (with depth; Simple hierarchical select)" + as a new filter + - use "Simple hierarchical select" as selection type + - select "Expose this filter to visitors, to allow them to change it" + - enjoy :) + + +-- INTEGRATION WITH OTHER MODULES -- + +* Chosen (http://drupal.org/project/chosen) + - If you have installed the module "Chosen" (>= 7.x-2.x) all elements of + "Simple hierarchical select" are modified, so the user can search for items + within the list. See http://drupal.org/project/chosen for more information. + - If you have configured "Chosen" to apply always but do not want to apply it + to the dropdowns created by "Simple hierarchical select" you may use the + following CSS selector in the "Chosen" configuration: + select:visible:not(.shs-select) + - Apart from that you can select whether to use chosen on a per-field base by + setting the option "Output this field with Chosen" in the field + configuration. + - "let chosen decide" + The field is modified by Chosen if it matches the Chosen configuration. + - "always" + The field is always modified by Chosen. + - "never" + The field is not modified by Chosen even if it matches the Chosen + configuration. + +* High-performance JavaScript callback handler (http://drupal.org/project/js) + - If you have lots of terms and a huge hierarchy you could increase the + performance of "Simple hierarchical select" by installing "JS". It routes + all javascript callbacks needed by "Simple hierarchical select" through a + custom handler to avoid loading of all Drupal functions and speed up + loading. + + +-- CONTACT -- + +Current maintainers: +* Stefan Borchert (stborchert) - http://drupal.org/user/36942 + +This project has been sponsored by: +* undpaul + Drupal experts providing professional Drupal development services. + Visit http://www.undpaul.de for more information. + diff --git a/sites/all/modules/shs/includes/handlers/shs_handler_filter_term_node_tid.inc b/sites/all/modules/shs/includes/handlers/shs_handler_filter_term_node_tid.inc new file mode 100644 index 0000000..a10eb22 --- /dev/null +++ b/sites/all/modules/shs/includes/handlers/shs_handler_filter_term_node_tid.inc @@ -0,0 +1,175 @@ + 'shs'); + $options['use_chosen'] = array('default' => 'never'); + + return $options; + } + + function extra_options_form(&$form, &$form_state) { + parent::extra_options_form($form, $form_state); + + $form['type']['#options'] += array( + 'shs' => t('Simple hierarchical select'), + ); + + if (module_exists('chosen')) { + $form['use_chosen'] = array( + '#type' => 'select', + '#title' => t('Output this field with !chosen', array('!chosen' => l(t('Chosen'), 'http://drupal.org/project/chosen'))), + '#description' => t('Select in which cases the element will use the !chosen module for the term selection of each level.', array('!chosen' => l(t('Chosen'), 'http://drupal.org/project/chosen'))), + '#default_value' => empty($this->options['use_chosen']) ? 'never' : $this->options['use_chosen'], + '#dependency' => array('radio:options[type]' => array('shs')), + '#options' => array( + 'chosen' => t('let chosen decide'), + 'always' => t('always'), + 'never' => t('never'), + ), + ); + } + } + + function value_form(&$form, &$form_state) { + parent::value_form($form, $form_state); + + $multiple = $this->options['expose']['multiple']; + $vocabulary = taxonomy_vocabulary_machine_name_load($this->options['vocabulary']); + if (empty($vocabulary) && $this->options['limit']) { + $form['markup'] = array( + '#markup' => '
' . t('An invalid vocabulary is selected. Please change it in the options.') . '
', + ); + return; + } + + if ($this->options['type'] == 'shs' && $this->options['exposed']) { + $identifier = $this->options['expose']['identifier']; + + // Get parents of selected term. + $default_value = empty($this->value) ? 0 : $this->value; + + if (!empty($form_state['input'][$identifier])) { + $default_value = $form_state['input'][$identifier]; + if (!is_array($default_value) && $multiple) { + $default_value = explode('+', $default_value); + foreach ($default_value as $delta => $value) { + $default_value[$delta] = explode(',', $value); + } + } + } + + $parents = array(); + if (!empty($default_value) && $default_value != 'All') { + if (is_array($default_value) && $default_value[0] != 'All') { + foreach ($default_value as $delta => $level) { + foreach ($level as $tid) { + $parents[$delta]['tid'] = $level; + } + } + } + elseif (is_string($default_value)) { + $term_parents = taxonomy_get_parents_all($default_value); + // Remove selected term from list. + array_shift($term_parents); + foreach (array_reverse($term_parents) as $term) { + $parents[] = array('tid' => $term->tid); + } + // Add current term (name not needed). + $parents[] = array('tid' => $default_value); + } + } + + // Add fake item for next level. + $parents[] = array('tid' => 0); + + $element_settings = array( + 'create_new_terms' => FALSE, + 'create_new_levels' => FALSE, + 'required' => FALSE, + ); + if (module_exists('chosen') && !empty($this->options['use_chosen'])) { + $element_settings['use_chosen'] = $this->options['use_chosen']; + } + if (!empty($this->options['exposed']) && !empty($this->options['expose']['required'])) { + $element_settings['required'] = TRUE; + } + + // Generate a random hash to avoid merging of settings by drupal_add_js. + // This is necessary until http://drupal.org/node/208611 lands for D7. + $js_hash = _shs_create_hash(); + // Create settings needed for our js magic. + $settings_js = array( + 'shs' => array( + $identifier => array( + $js_hash => array( + 'vid' => $vocabulary->vid, + 'settings' => $element_settings, + 'default_value' => $default_value, + 'parents' => $parents, + 'multiple' => $multiple, + ), + ), + ), + ); + // Add settings. + drupal_add_js($settings_js, 'setting'); + // Add behavior. + drupal_add_js(drupal_get_path('module', 'shs') . '/js/shs.js'); + + if (is_array($default_value) && $multiple && $default_value[0] != 'All') { + foreach ($default_value as $delta => $level) { + if ($delta == 0) { + $default_value_str = implode(',', $level); + } + else { + $default_value_str .= '+' . implode(',', $level); + } + } + } + + // Rewrite default select to textfield. + $form['value'] = array( + '#type' => 'textfield', + '#default_value' => empty($default_value_str) ? 'All' : $default_value_str, + '#attributes' => array( + 'class' => array('element-invisible', 'shs-enabled'), + ), + ); + + if (!empty($form_state['exposed']) && isset($identifier) && empty($form_state['input'][$identifier]) || $multiple && isset($form_state['input'][$identifier][0])) { + $form_state['input'][$identifier] = empty($default_value_str) ? 'All' : $default_value_str; + } + } + + if (empty($form_state['exposed'])) { + // Retain the helper option. + $this->helper->options_form($form, $form_state); + } + } + + function admin_summary() { + // Set up $this->value_options for the parent summary. + $this->value_options = array(); + + if ($this->value == 'All') { + $this->value = NULL; + } + + return parent::admin_summary(); + } + +} diff --git a/sites/all/modules/shs/includes/handlers/shs_handler_filter_term_node_tid_depth.inc b/sites/all/modules/shs/includes/handlers/shs_handler_filter_term_node_tid_depth.inc new file mode 100644 index 0000000..4d41a86 --- /dev/null +++ b/sites/all/modules/shs/includes/handlers/shs_handler_filter_term_node_tid_depth.inc @@ -0,0 +1,115 @@ + t('Is one of'), + ); + } + + function option_definition() { + $options = parent::option_definition(); + + $options['depth'] = array('default' => 0); + + return $options; + } + + function extra_options_form(&$form, &$form_state) { + parent::extra_options_form($form, $form_state); + + $form['depth'] = array( + '#type' => 'weight', + '#title' => t('Depth'), + '#default_value' => $this->options['depth'], + '#description' => t('The depth will match nodes tagged with terms in the hierarchy. For example, if you have the term "fruit" and a child term "apple", with a depth of 1 (or higher) then filtering for the term "fruit" will get nodes that are tagged with "apple" as well as "fruit". If negative, the reverse is true; searching for "apple" will also pick up nodes tagged with "fruit" if depth is -1 (or lower).'), + ); + } + + function query() { + // Check if multiple select allowed. + if ($this->options['expose']['multiple']) { + // If no filter values are present, then do nothing. + if ($this->value == 'All') { + return; + } + else { + $this->value = explode('+', $this->value[0]); + $this->value = explode(',', end($this->value)); + $operator = 'IN'; + } + } + else { + // If no filter values are present, then do nothing. + if (count($this->value) == 0) { + return; + } + elseif (count($this->value) == 1) { + // $this->value is an array with a single element so convert it. + if (is_array($this->value)) { + $this->value = current($this->value); + } + $operator = '='; + } + else { + $operator = 'IN'; + } + } + + // The normal use of ensure_my_table() here breaks Views. + // So instead we trick the filter into using the alias of the base table. + // See http://drupal.org/node/271833 + // If a relationship is set, we must use the alias it provides. + if (!empty($this->relationship)) { + $this->table_alias = $this->relationship; + } + // If no relationship, then use the alias of the base table. + elseif (isset($this->query->table_queue[$this->query->base_table]['alias'])) { + $this->table_alias = $this->query->table_queue[$this->query->base_table]['alias']; + } + // This should never happen, but if it does, we fail quietly. + else { + return; + } + + // Now build the subqueries. + $subquery = db_select('taxonomy_index', 'tn'); + $subquery->addField('tn', 'nid'); + $where = db_or()->condition('tn.tid', $this->value, $operator); + $last = "tn"; + + if ($this->options['depth'] > 0) { + $subquery->leftJoin('taxonomy_term_hierarchy', 'th', "th.tid = tn.tid"); + $last = "th"; + foreach (range(1, abs($this->options['depth'])) as $count) { + $subquery->leftJoin('taxonomy_term_hierarchy', "th$count", "$last.parent = th$count.tid"); + $where->condition("th$count.tid", $this->value, $operator); + $last = "th$count"; + } + } + elseif ($this->options['depth'] < 0) { + foreach (range(1, abs($this->options['depth'])) as $count) { + $subquery->leftJoin('taxonomy_term_hierarchy', "th$count", "$last.tid = th$count.parent"); + $where->condition("th$count.tid", $this->value, $operator); + $last = "th$count"; + } + } + + $subquery->condition($where); + $this->query->add_where($this->options['group'], "$this->table_alias.$this->real_field", $subquery, 'IN'); + } +} diff --git a/sites/all/modules/shs/js/shs.js b/sites/all/modules/shs/js/shs.js new file mode 100644 index 0000000..28cea53 --- /dev/null +++ b/sites/all/modules/shs/js/shs.js @@ -0,0 +1,459 @@ +/** + * @file + * Custom behaviors for Simple hierarchical select. + */ + +(function ($) { + + /** + * Creates the widget for Simple hierarchical select. + */ + Drupal.behaviors.shsWidgetCreate = { + + // Default function to attach the behavior. + attach: function (context, settings) { + var self = this; + $('input.shs-enabled') + .not('.shs-processed') + .once('shs') + .addClass('element-invisible') + .each(function() { + var $field = $(this); + var fieldName = $(this).attr('name'); + + if (fieldName in settings.shs) { + var fieldSettings = {}; + // Since we store the field settings within an associative array with + // random strings as keys (reason: http://drupal.org/node/208611) we + // need to get the last setting for this field. + $.each(settings.shs[fieldName], function(hash, setting) { + fieldSettings = setting; + }); + var level = 0; + var parent_id = 0; + // Update class of wrapper element. + $field.parent('.form-item').not('.shs-wrapper-processed').once('shs-wrapper'); + // Create elements for each parent of the current value. + $.each(fieldSettings.parents, function(index, parent) { + level++; + // Create select element. + $select = shsElementCreate($field.attr('id'), fieldSettings, level); + if ($field.hasClass('error')) { + // Add error-class if there was an error with the original field. + $select.addClass('error'); + } + $select.appendTo($field.parent()); + // Retrieve data for this level. + getTermChildren($select, fieldSettings, parent_id, parent.tid, $field.attr('id')); + // Use current term id as parent id for the next level. + if (fieldSettings.multiple) { + parent_id = parent['tid']; + } + else { + parent_id = parent.tid; + } + }); + var addNextLevel = false; + if ((level > 1 || parent_id) && (fieldSettings.settings.create_new_terms && fieldSettings.settings.create_new_levels)) { + // Add next level in hierarchy if new levels may be created. + addNextLevel = true; + } + if (fieldSettings.default_value && (fieldSettings.default_value.tid == parent_id)) { + addNextLevel = true; + } + if (addNextLevel) { + // Try to add one additional level. + $select = shsElementCreate($field.attr('id'), fieldSettings, level); + $select.appendTo($field.parent()); + // Retrieve data for this level. + getTermChildren($select, fieldSettings, parent_id, 0, $field.attr('id')); + } + } + }); + } + } + + /** + * Load direct children of a selected term. + * + * @param $element + * Element to fill with terms. + * @param settings + * Field settings. + * @param parent_value + * Value which has been selected in the parent element (== "selected term"). + * @param default_value + * Value to use as default. + * @param base_id + * ID of original field which is rewritten as "taxonomy_shs". + */ + getTermChildren = function($element, settings, parent_value, default_value, base_id) { + + // Check if parent_value is number and convert it. + if (!$.isArray(parent_value) && typeof parent_value != "object") { + parent_value = [parent_value]; + } + + // Check if default_value is object and convert it. + if (!$.isArray(default_value) && typeof default_value == "object") { + var arr = new Array; + $.each(default_value, function(delta, value){ + arr.push(value); + }); + default_value = arr; + } + + $.ajax({ + url: Drupal.settings.basePath + 'js/shs/json', + type: 'POST', + dataType: 'json', + cache: true, + data: { + callback: 'shs_json_term_get_children', + arguments: { + vid: settings.vid, + parent: parent_value, + settings: settings.settings + } + }, + success: function(data) { + if (data.success == true) { + if ($element.prop) { + var options = $element.prop('options'); + } + else { + var options = $element.attr('options'); + } + + if (data.data.length == 0 && !(settings.settings.create_new_terms && (settings.settings.create_new_levels || (parent_value + default_value == 0)))) { + // Remove element. + $element.remove(); + return; + } + + // Remove all existing options. + $('option', $element).remove(); + // Add empty option (if field is not required and not multiple + // or this is not the first level and not multiple). + if (!settings.settings.required || (settings.settings.required && parent_value != 0 && !settings.multiple)) { + options[options.length] = new Option(Drupal.t('- None -'), 0); + } + + if (settings.settings.create_new_terms) { + // Add option to add new item. + options[options.length] = new Option(Drupal.t('', {}, {context: 'shs'}), '_add_new_'); + } + + // Add retrieved list of options. + $.each(data.data, function(key, term) { + options[options.length] = new Option(term.label, term.tid); + }); + // Set default value. + $element.val(default_value); + + // Try to convert the element to a "Chosen" element. + if (!elementConvertToChosen($element, settings)) { + // Display original dropdown element. + $element.fadeIn(); + $element.css('display','inline-block'); + } + + // If there is no data, the field is required and the user is allowed + // to add new terms, trigger click on "Add new". + if (data.data.length == 0 && settings.settings.required && settings.settings.create_new_terms && (settings.settings.create_new_levels || (parent_value + default_value == 0))) { + updateElements($element, base_id, settings, 1); + } + } + }, + error: function(xhr, status, error) { + } + }); + } + + /** + * Add a new term to database. + * + * @param $triggering_element + * Element to add the new term to. + * @param $container + * Container for "Add new" elements. + * @param term + * The new term object. + * @param base_id + * ID of original field which is rewritten as "taxonomy_shs". + * @param level + * Current level in hierarchy. + */ + termAddNew = function($triggering_element, $container, term, base_id, level) { + $.ajax({ + url: Drupal.settings.basePath + 'js/shs/json', + type: 'POST', + dataType: 'json', + cache: true, + data: { + callback: 'shs_json_term_add', + arguments: { + vid: term.vid, + parent: term.parent, + name: term.name + } + }, + success: function(data) { + if (data.success == true && data.data.tid) { + if ($triggering_element.prop) { + var options = $triggering_element.prop('options'); + } + else { + var options = $triggering_element.attr('options'); + } + + // Add new option with data from created term. + options[options.length] = new Option(data.data.name, data.data.tid); + // Set new default value. + $triggering_element.val(data.data.tid); + // Set value of original field. + updateFieldValue($triggering_element, base_id, level); + } + }, + error: function(xhr, status, error) { + // Reset value of triggering element. + $triggering_element.val(0); + }, + complete: function(xhr, status) { + // Remove container. + $container.remove(); + // Display triggering element. + $triggering_element.fadeIn(); + $triggering_element.css('display','inline-block'); + } + }); + } + + /** + * Update the changed element. + * + * @param $triggering_element + * Element which has been changed. + * @param base_id + * ID of original field which is rewritten as "taxonomy_shs". + * @param settings + * Field settings. + * @param level + * Current level in hierarchy. + */ + updateElements = function($triggering_element, base_id, settings, level) { + // Remove all following elements. + $triggering_element.nextAll('select').each(function() { + if (Drupal.settings.chosen) { + // Remove element created by chosen. + var elem_id = $(this).attr('id'); + $('#' + elem_id.replace(/-/g, '_') + '_chzn').remove(); + } + // Remove element. + $(this).remove(); + }); + //$triggering_element.nextAll('.chzn-container').remove(); + $triggering_element.nextAll('.shs-term-add-new-wrapper').remove(); + // Create next level (if the value is != 0). + if ($triggering_element.val() == '_add_new_') { + // Hide element. + $triggering_element.hide(); + // Create new container with textfield and buttons ("cancel", "save"). + $container = $('
') + .addClass('shs-term-add-new-wrapper') + .addClass('clearfix'); + // Append container to parent. + $container.appendTo($triggering_element.parent()); + + // Add textfield for term name. + $nameField = $('') + .attr('maxlength', 255) + .attr('size', 10) + .addClass('shs-term-name') + .addClass('form-text'); + $nameField.appendTo($container); + + // Add buttons. + $buttons = $('
') + .addClass('buttons'); + $buttons.appendTo($container); + $cancel = $('') + .attr('href', '#') + .html(Drupal.t('Cancel')) + .bind('click', function(event) { + event.preventDefault(); + // Remove container. + $container.remove(); + // Reset value of triggering element. + $triggering_element.val(0); + // Display triggering element. + $triggering_element.fadeIn(); + $triggering_element.css('display','inline-block'); + }); + $cancel.appendTo($buttons); + if (level == 1 && settings.settings.required && $('option', $triggering_element).length == 1) { + // Hide cancel button since the term selection is empty (apart from + // "Add new term") and the field is required. + $cancel.hide(); + } + + $save = $('') + .attr('href', '#') + .html(Drupal.t('Save')) + .bind('click', function(event) { + event.preventDefault(); + // Get the new term name. + var termName = $(this).parents('.shs-term-add-new-wrapper').find('input.shs-term-name').val(); + // Create a term object. + var term = { + vid: settings.vid, + parent: $triggering_element.prev('select').val() || 0, + name: termName + }; + if (termName.length > 0) { + termAddNew($triggering_element, $container, term, base_id, level); + } + else { + // Remove container. + $container.remove(); + // Reset value of triggering element. + $triggering_element.val(0); + // Display triggering element. + $triggering_element.fadeIn(); + $triggering_element.css('display','inline-block');; + } + }); + $save.appendTo($buttons); + } + else if ($triggering_element.val() != 0) { + level++; + $element_new = shsElementCreate(base_id, settings, level); + $element_new.appendTo($triggering_element.parent()); + // Retrieve list of items for the new element. + getTermChildren($element_new, settings, $triggering_element.val(), 0, base_id); + } + + // Set value of original field. + updateFieldValue($triggering_element, base_id, level, settings.multiple); + } + + /** + * Create a new element. + */ + shsElementCreate = function(base_id, settings, level) { + // Create element and initially hide it. + if (settings.multiple) { + $element = $('') + .attr('id', base_id + '-select-' + level) + .addClass('shs-select') + // Add core class to apply default styles to the element. + .addClass('form-select') + .addClass('shs-select-level-' + level) + .bind('change', function() { + updateElements($(this), base_id, settings, level); + }) + .hide(); + } + // Return the new element. + return $element; + } + + /** + * Update value of original (hidden) field. + * + * @param $triggering_element + * Element which has been changed. + * @param base_id + * ID of original field which is rewritten as "taxonomy_shs". + * @param level + * Current level in hierarchy. + */ + updateFieldValue = function($triggering_element, base_id, level, multiple) { + // Reset value of original field. + $field_orig = $('#' + base_id); + $field_orig.val(0); + // Set original field value. + if ($triggering_element.val() == 0 || $triggering_element.val() == '_add_new_') { + if (level > 1) { + // Use value from parent level. + $field_orig.val($triggering_element.prev('select').val()); + } + } + else { + var new_val = $triggering_element.val(); + if (level > 1 && multiple) { + var new_value = ''; + for (i = 0; i < level - 1; i++) { + var prev_value = $('.shs-select:eq(' + i + ')').val(); + if (i == 0) { + new_value = prev_value; + } + else { + new_value = new_value + '+' + prev_value; + } + } + new_val = new_value; + } + // Use value from current field. + if ($.isArray(new_val)) { + $field_orig.val(new_val.join(',')); + } + else { + $field_orig.val(new_val); + } + } + } + + /** + * Convert a dropdown to a "Chosen" element. + * + * @see http://drupal.org/project/chosen + */ + elementConvertToChosen = function($element, settings) { + if (Drupal.settings.chosen) { + var minWidth = Drupal.settings.chosen.minimum_width; + // Define options for chosen. + var options = {}; + options.search_contains = Drupal.settings.chosen.search_contains; + options.placeholder_text_multiple = Drupal.settings.chosen.placeholder_text_multiple; + options.placeholder_text_single = Drupal.settings.chosen.placeholder_text_single; + options.no_results_text = Drupal.settings.chosen.no_results_text; + + // Get element selector from settings (and remove "visible" option since + // our select element is hidden by default). + var selector = Drupal.settings.chosen.selector.replace(/:visible/, ''); + + if ((settings.settings.use_chosen == 'always') || ((settings.settings.use_chosen == 'chosen') && ($element.is(selector) && $element.find('option').size() >= Drupal.settings.chosen.minimum))) { + $element.css({ + width : ($element.width() < minWidth) ? minWidth : $element.width() + }).chosen(options); + return true; + } + } + return false; + } + +})(jQuery); diff --git a/sites/all/modules/shs/shs.info b/sites/all/modules/shs/shs.info new file mode 100644 index 0000000..b22f8de --- /dev/null +++ b/sites/all/modules/shs/shs.info @@ -0,0 +1,13 @@ +name = Simple hierarchical select +core = 7.x +description = Creates a simple hierarchical select widget for taxonomy fields. +dependencies[] = taxonomy +files[] = includes/handlers/shs_handler_filter_term_node_tid.inc +files[] = includes/handlers/shs_handler_filter_term_node_tid_depth.inc + +; Information added by drupal.org packaging script on 2013-03-17 +version = "7.x-1.6" +core = "7.x" +project = "shs" +datestamp = "1363529732" + diff --git a/sites/all/modules/shs/shs.module b/sites/all/modules/shs/shs.module new file mode 100644 index 0000000..b9f73bc --- /dev/null +++ b/sites/all/modules/shs/shs.module @@ -0,0 +1,832 @@ + 'JSON callback', + 'description' => 'JSON callbacks for Simple hierarchical select', + 'page callback' => 'shs_json', + 'access callback' => 'user_access', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + + return $items; +} + +/** + * Implements hook_js(). + */ +function shs_js() { + return array( + 'json' => array( + 'callback' => 'shs_json', + 'access callback' => 'user_access', + 'access arguments' => array('access content'), + 'dependencies' => array('taxonomy'), + ), + ); +} + +/** + * Menu callback to get data in JSON format. + */ +function shs_json() { + $result = array( + 'success' => FALSE, + 'data' => array(), + ); + if (isset($_POST['callback'])) { + // Get name of function we need to call to get the data. + $_callback = check_plain($_POST['callback']); + // Is this a valid callback? + $valid_callbacks = shs_json_callbacks(); + if (isset($valid_callbacks[$_callback]) && !empty($valid_callbacks[$_callback]['callback']) && function_exists($valid_callbacks[$_callback]['callback'])) { + // Get arguments and validate them. + $post_args = (isset($_POST['arguments']) && is_array($_POST['arguments'])) ? $_POST['arguments'] : array(); + $arguments = _shs_json_callback_get_arguments($valid_callbacks[$_callback], $post_args); + if (($callback_result = call_user_func_array($valid_callbacks[$_callback]['callback'], $arguments)) !== FALSE) { + $result['success'] = TRUE; + $result['data'] = $callback_result; + } + } + } + // Return result as JSON string. + drupal_json_output($result); +} + +/** + * Get a list of supported JSON callbacks. + * + * @return + * List of valid callbacks with the following structure: + * - [name of callback] + * - 'callback': function to call + * - 'arguments' + * - [name of argument]: [validation function] (FALSE for no validation) + */ +function shs_json_callbacks() { + $callbacks = array( + 'shs_json_term_get_children' => array( + 'callback' => 'shs_json_term_get_children', + 'arguments' => array( + 'vid' => 'is_numeric', + 'parent' => 'is_array', + 'settings' => 'is_array', + ), + ), + 'shs_json_term_add' => array( + 'callback' => 'shs_json_term_add', + 'arguments' => array( + 'vid' => 'is_numeric', + 'parent' => 'is_numeric', + 'name' => 'is_string', + ), + ), + ); + // Let other modules add some more callbacks and alter the existing. + drupal_alter('shs_json_callbacks', $callbacks); + return $callbacks; +} + +/** + * Helper function to get the (validated) arguments for a JSON callback. + * + * @param $callback + * Callback definition from campus_events_json_callbacks(). + * @param $arguments + * Unfiltered arguments posted with $.ajax(). + * + * @return + * List of (validated) arguments for this callback. Any arguments not defined + * for this callback will be removed. + */ +function _shs_json_callback_get_arguments($callback, $arguments) { + $result = array(); + // Get arguments from callback definition. + $callback_arguments = $callback['arguments']; + foreach ($arguments as $key => $value) { + if (isset($callback_arguments[$key])) { + $argument_valid = TRUE; + if ((($validation_function = $callback_arguments[$key]) !== FALSE) && function_exists($validation_function)) { + // Validate argument. + $argument_valid = $validation_function($value); + } + if ($argument_valid) { + // Add argument and its value to the result list. + $result[$key] = $value; + } + } + } + return $result; +} + +/** + * Implements hook_views_data_alter(). + */ +function shs_views_data_alter(&$data) { + // Get a list of all field instances with widget type "taxonomy_shs". + $instances = _shs_get_instances('node'); + foreach ($instances as $field_instances) { + foreach ($field_instances as $field_name => $instance) { + // Replace filter handler for this field. + if (!empty($data["field_data_{$field_name}"]["{$field_name}_tid"]['filter']['handler'])) { + $data["field_data_{$field_name}"]["{$field_name}_tid"]['filter']['handler'] = 'shs_handler_filter_term_node_tid'; + } + } + } + + // Add filter handler for term ID with depth. + $data['node']['shs_term_node_tid_depth'] = array( + 'help' => t('Display content if it has the selected taxonomy terms, or children of the selected terms. Due to additional complexity, this has fewer options than the versions without depth. Optionally the filter will use a simple hierarchical select for the selection of terms.'), + 'real field' => 'nid', + 'filter' => array( + 'title' => t('Has taxonomy terms (with depth; %type)', array('%type' => 'Simple hierarchical select')), + 'handler' => 'shs_handler_filter_term_node_tid_depth', + ), + ); +} + +/** + * Implements hook_field_widget_info(). + */ +function shs_field_widget_info() { + return array( + 'taxonomy_shs' => array( + 'label' => t('Simple hierarchical select'), + 'field types' => array('taxonomy_term_reference'), + 'settings' => array( + 'shs' => array( + 'node_count' => FALSE, + 'create_new_terms' => FALSE, + 'create_new_levels' => FALSE, + 'force_deepest' => FALSE, + ), + ), + ), + ); +} + +/** + * Implements hook__field_widget_settings_form(). + */ +function shs_field_widget_settings_form($field, $instance) { + $widget = $instance['widget']; + $settings = $widget['settings']; + + $form = array(); + + $form['shs'] = array( + '#type' => 'fieldset', + '#title' => 'Simple hierarchical select settings', + '#collapsible' => TRUE, + '#collapsed' => FALSE, + '#tree' => TRUE, + ); + $form['shs']['node_count'] = array( + '#type' => 'checkbox', + '#title' => t('Display number of nodes'), + '#description' => t('Display the number of nodes associated with the term.'), + '#default_value' => empty($settings['shs']['node_count']) ? FALSE : $settings['shs']['node_count'], + ); + $form['shs']['create_new_terms'] = array( + '#type' => 'checkbox', + '#title' => t('Allow creating new terms'), + '#description' => t('If checked the user will be able to create new terms (permission to edit terms in this vocabulary must be set).'), + '#default_value' => empty($settings['shs']['create_new_terms']) ? FALSE : $settings['shs']['create_new_terms'], + ); + $form['shs']['create_new_levels'] = array( + '#type' => 'checkbox', + '#title' => t('Allow creating new levels'), + '#description' => t('If checked the user will be able to create new children for items which do not have any children yet (permission to edit terms in this vocabulary must be set).'), + '#default_value' => empty($settings['shs']['create_new_levels']) ? FALSE : $settings['shs']['create_new_levels'], + '#states' => array( + 'visible' => array( + ':input[name="instance[widget][settings][shs][create_new_terms]"]' => array('checked' => TRUE), + ), + ), + ); + $form['shs']['force_deepest'] = array( + '#type' => 'checkbox', + '#title' => t('Force selection of deepest level'), + '#description' => t('If checked the user will be forced to select terms from the deepest level.'), + '#default_value' => empty($settings['shs']['force_deepest']) ? FALSE : $settings['shs']['force_deepest'], + ); + + // "Chosen" integration. + if (module_exists('chosen')) { + $form['shs']['use_chosen'] = array( + '#type' => 'select', + '#title' => t('Output this field with !chosen', array('!chosen' => l(t('Chosen'), 'http://drupal.org/project/chosen'))), + '#description' => t('Select in which cases the element will use the !chosen module for the term selection of each level.', array('!chosen' => l(t('Chosen'), 'http://drupal.org/project/chosen'))), + '#default_value' => empty($settings['shs']['use_chosen']) ? 'chosen' : $settings['shs']['use_chosen'], + '#options' => array( + 'chosen' => t('let chosen decide'), + 'always' => t('always'), + 'never' => t('never'), + ), + ); + } + + return $form; +} + +/** + * Implements hook_field_widget_error(). + */ +function shs_field_widget_error($element, $error, $form, &$form_state) { + form_error($element, $error['message']); +} + +/** + * Implements hook_field_widget_form(). + */ +function shs_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { + // Get value. + $element_value = NULL; + if (!empty($items[$delta]['tid'])) { + // Use saved value from database or cache. + $element_value = $items[$delta]['tid']; + } + elseif (!empty($form_state['values'][$element['#field_name']][$element['#language']][$delta]['tid'])) { + // Use value from form_state (for example for fields with cardinality = -1). + $element_value = $form_state['values'][$element['#field_name']][$element['#language']][$delta]['tid']; + } + + // Get vocabulary. + $allowed_values = reset($field['settings']['allowed_values']); + if (empty($allowed_values['vocabulary']) || ($vocabulary = taxonomy_vocabulary_machine_name_load($allowed_values['vocabulary'])) === FALSE) { + // No vocabulary selected yet or vocabulary not found. + return array(); + } + + // Check if term exists (may be deleted). + if ($element_value && (($term = taxonomy_term_load($element_value)) === FALSE)) { + $element_value = 0; + } + + if (!user_access('edit terms in ' . $vocabulary->vid)) { + // Update setting based on permission. + $instance['widget']['settings']['shs']['create_new_terms'] = FALSE; + } + $instance['widget']['settings']['shs']['required'] = $element['#required']; + + // Element is required and there is no initial value. + if (empty($element_value) && $element['#required']) { + // Load list of options. + $options = shs_term_get_children($vocabulary->vid, 0); + if (count($options)) { + // Set element value to first available option. + $option_keys = array_keys($options); + $element_value = reset($option_keys); + } + } + + // Create element. + $element += array( + '#type' => 'textfield', + '#default_value' => empty($element_value) ? NULL : $element_value, + '#attributes' => array( + 'class' => array('shs-enabled'), + ), + // Prevent errors with drupal_strlen(). + '#maxlength' => NULL, + '#element_validate' => array('shs_field_widget_validate'), + '#after_build' => array('shs_field_widget_afterbuild'), + '#shs_settings' => $instance['widget']['settings']['shs'], + '#shs_vocabulary' => $vocabulary, + ); + + return array('tid' => $element); +} + +/** + * Afterbuild callback for widgets of type "taxonomy_shs". + */ +function shs_field_widget_afterbuild($element, &$form_state) { + $js_added = &drupal_static(__FUNCTION__ . '_js_added', array()); + // Generate a random hash to avoid merging of settings by drupal_add_js. + // This is necessary until http://drupal.org/node/208611 lands for D7. + $js_hash = &drupal_static(__FUNCTION__ . '_js_hash'); + + if (empty($js_hash)) { + $js_hash = _shs_create_hash(); + } + + $parents = array(); + // Get value from element. + if (!empty($form_state['values'][$element['#field_name']][$element['#language']][$element['#delta']]['tid'])) { + // Use value from form_state (for example for fields with cardinality = -1). + $element['#default_value'] = $form_state['values'][$element['#field_name']][$element['#language']][$element['#delta']]; + } + + // Add main Javascript behavior and style only once. + if (count($js_added) == 0) { + // Add behavior. + drupal_add_js(drupal_get_path('module', 'shs') . '/js/shs.js'); + // Add styles. + drupal_add_css(drupal_get_path('module', 'shs') . '/theme/shs.form.css'); + } + + // Create Javascript settings for the element only if it hasn't been added + // before. + if (empty($js_added[$element['#name']][$js_hash])) { + $element_value = $element['#default_value']['tid']; + + if (empty($element_value)) { + // Add fake parent for new items. + $parents[] = array('tid' => 0); + } + else { + $term_parents = taxonomy_get_parents_all($element_value); + foreach ($term_parents as $term) { + // Create term lineage. + $parents[] = array('tid' => $term->tid); + } + } + + // Create settings needed for our js magic. + $settings_js = array( + 'shs' => array( + "{$element['#name']}" => array( + $js_hash => array( + 'vid' => $element['#shs_vocabulary']->vid, + 'settings' => $element['#shs_settings'], + 'default_value' => $element['#default_value'], + 'parents' => array_reverse($parents), + ), + ), + ), + ); + + // Add settings. + drupal_add_js($settings_js, 'setting'); + + if (empty($js_added[$element['#name']])) { + $js_added[$element['#name']] = array(); + } + $js_added[$element['#name']][$js_hash] = TRUE; + } + + return $element; +} + +/** + * Validation handler for widgets of type "taxonomy_shs". + */ +function shs_field_widget_validate($element, &$form_state, $form) { + $field_name = $element['#field_name']; + $field_language = $element['#language']; + + if (empty($form_state['field'][$field_name][$field_language]['instance']['widget'])) { + return; + } + $field = $form_state['field'][$field_name][$field_language]; + $instance = $field['instance']; + $settings = empty($instance['widget']['settings']['shs']) ? array() : $instance['widget']['settings']['shs']; + + // Do we want to force the user to select terms from the deepest level? + $force_deepest_level = empty($settings['force_deepest']) ? FALSE : $settings['force_deepest']; + $value = empty($element['#value']) ? 0 : $element['#value']; + if ($force_deepest_level && $value) { + // Get vocabulary. + $allowed_values = reset($field['field']['settings']['allowed_values']); + if (empty($allowed_values['vocabulary']) || ($vocabulary = taxonomy_vocabulary_machine_name_load($allowed_values['vocabulary'])) === FALSE) { + // No vocabulary selected yet or vocabulary not found. + form_error($element, t('Vocabulary %machine_name is configured as source for field %field_name but could not be found.', array('%machine_name' => $allowed_values['vocabulary'], '%field_name' => $field_name))); + } + // Does the selected term has any children? + $children = shs_term_get_children($vocabulary->vid, $value); + if (count($children)) { + form_error($element, t('You need to select a term from the deepest level.')); + } + } +} + +/** + * Implements hook_field_formatter_info(). + */ +function shs_field_formatter_info() { + return array( + 'shs_default' => array( + 'label' => t('Simple hierarchy'), + 'field types' => array('taxonomy_term_reference'), + 'settings' => array( + 'linked' => FALSE, + ), + ), + ); +} + +/** + * Implements hook_field_formatter_settings_form(). + */ +function shs_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) { + $display = $instance['display'][$view_mode]; + $settings = $display['settings']; + + $element = array(); + + if ($display['type'] == 'shs_default') { + $element['linked'] = array( + '#title' => t('Link to term page'), + '#type' => 'checkbox', + '#default_value' => $settings['linked'], + ); + } + + return $element; +} + +/** + * Implements hook_field_formatter_settings_summary(). + */ +function shs_field_formatter_settings_summary($field, $instance, $view_mode) { + $display = $instance['display'][$view_mode]; + $settings = $display['settings']; + + $summary = ''; + + if ($display['type'] == 'shs_default') { + $summary = t('Linked to term page: !linked', array('!linked' => $settings['linked'] ? t('Yes') : t('No'))); + } + + return $summary; +} + +/** + * Implements hook_field_formatter_prepare_view(). + */ +function shs_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) { + foreach ($entities as $entity_id => $entity) { + foreach ($items[$entity_id] as $delta => $item) { + $items[$entity_id][$delta]['parents'] = array(); + // Load list of parent terms. + $parents = taxonomy_get_parents_all($item['tid']); + // Remove current term from list. + array_shift($parents); + foreach (array_reverse($parents) as $parent) { + $items[$entity_id][$delta]['parents'][$parent->tid] = $parent; + } + // Load term. + $items[$entity_id][$delta]['term'] = taxonomy_term_load($item['tid']); + } + } +} + +/** + * Implements hook_field_formatter_view(). + */ +function shs_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) { + $elements = array(); + $settings = $display['settings']; + + switch ($display['type']) { + case 'shs_default': + foreach ($items as $delta => $item) { + if (empty($item['tid'])) { + continue; + } + $list_items = array(); + // Add parent term names. + foreach ($item['parents'] as $parent) { + $list_items[] = array( + 'data' => $settings['linked'] ? l($parent->name, "taxonomy/term/{$parent->tid}") : $parent->name, + 'class' => array('shs-parent'), + ); + }; + // Add name of selected term. + $list_items[] = array( + 'data' => $settings['linked'] ? l($item['term']->name, "taxonomy/term/{$item['term']->tid}") : $item['term']->name, + 'class' => array('shs-term-selected'), + ); + $elements[$delta] = array( + '#items' => $list_items, + '#theme' => 'item_list', + '#attributes' => array( + 'class' => 'shs-hierarchy', + ), + ); + } + + // Add basic style. + $elements['#attached']['css'][] = drupal_get_path('module', 'shs') . '/theme/shs.formatter.css'; + break; + } + + return $elements; +} + +/** + * Function to get the list of children of a term. + * + * The structure is stored in the database cache as well as in drupal_static(). + * Cache has the following structure: + * + * [$parent] => array( + * [0] => array(tid1, tid2, tid3), // !$node_count. + * [1] => array('tid1 (x)', 'tid2 (x)', 'tid3 (x)'), // $node_count. + * ), + * + * + * @param $vid + * ID of vocabulary the term is associated to. + * @param $parent + * ID of parent term. + * @param $settings + * Additional settings (for example "display node count"). + * @param $reset + * If TRUE, rebuild the cache for the given $vid and $parent. + * + * @return + * List of child terms keyed by term id. + */ +function shs_term_get_children($vid, $parent = 0, $settings = array(), $reset = FALSE) { + $terms = &drupal_static(__FUNCTION__, array()); + $node_count = !empty($settings['node_count']) && variable_get('taxonomy_maintain_index_table', TRUE); + + if ($reset || ($vid && empty($terms[$vid][$parent][$node_count]))) { + // Initialize list. + $terms[$vid][$parent] = array( + 0 => array(), + 1 => array(), + ); + $cache_key = "shs:{$vid}"; + // Get cached values. + $cache = cache_get($cache_key); + if ($reset || !$cache || ($cache->expire && time() > $cache->expire) || empty($cache->data[$parent][$node_count])) { + // Cache is empty or data has become outdated or the parent is not cached. + if ($cache) { + // Cache exists and is not yet expired but $parent is missing. + $terms[$vid] = $cache->data; + } + if ($reset) { + $terms[$vid][$parent] = array( + 0 => array(), + 1 => array(), + ); + } + // Get term children (only first level). + $tree = taxonomy_get_tree($vid, $parent, 1); + foreach ($tree as $term) { + $terms[$vid][$parent][0][$term->tid] = $term->name; + if ($node_count) { + // Count nodes associated to this term (and its children). + $num_nodes = _shs_term_get_node_count($term, TRUE); + // Update term label. + $terms[$vid][$parent][1][$term->tid] = t('!term_name (!node_count)', array('!term_name' => $term->name, '!node_count' => $num_nodes)); + } + } + // Set cached data. + cache_set($cache_key, $terms[$vid], 'cache', CACHE_PERMANENT); + } + else { + // Use cached data. + $terms[$vid] = $cache->data; + } + } + // Allow other module to modify the list of terms. + $alter_options = array( + 'vid' => $vid, + 'parent' => $parent, + 'settings' => $settings, + ); + drupal_alter('shs_term_get_children', $terms, $alter_options); + + return empty($terms[$vid][$parent][$node_count]) ? array() : $terms[$vid][$parent][$node_count]; +} + +/** + * JSON callback to get the list of children of a term. + * + * @param $vid + * ID of vocabulary the term is associated to. + * @param $parent + * ID of parent term. + * @param $settings + * Additional settings (for example "display node count"). + * + * @return + * Associative list of child terms. + * + * @see shs_term_get_children() + */ +function shs_json_term_get_children($vid, $parent = array(), $settings = array()) { + $scope = $result = array(); + foreach ($parent as $tid) { + $scope[] = shs_term_get_children($vid, $tid, $settings); + } + + // Rewrite result set to preserve original sort of terms through JSON request. + foreach ($scope as $terms) { + foreach ($terms as $tid => $label) { + $result[] = array( + 'tid' => $tid, + 'label' => $label, + ); + } + } + + return $result; +} +/** + * Adds a term with ajax. + * + * @param $vid + * ID of vocabulary to create the term in. + * @param $parent + * ID of parent term (0 for top level). + * @param $term_name + * Name of new term. + * + * @return + * Array with tid and name or FALSE on error. + */ +function shs_json_term_add($vid, $parent, $term_name) { + if (!user_access('edit terms in ' . $vid)) { + // Sorry, but this user may not add a term to this vocabulary. + return FALSE; + } + + $term = (object) array( + 'vid' => $vid, + 'parent' => $parent, + 'name' => check_plain(filter_xss($term_name)), + ); + // Save term. + $status = taxonomy_term_save($term); + + // Return term object or FALSE (in case of errors). + return ($status == SAVED_NEW) ? array('tid' => $term->tid, 'name' => $term->name) : FALSE; +} + +/** + * Implements hook_hook_taxonomy_term_insert(). + */ +function shs_taxonomy_term_insert($term) { + // Update vocabulary cache for the terms parents. + foreach ($term->parent as $parent) { + shs_term_get_children($term->vid, $parent, array('node_count' => TRUE), TRUE); + } +} + +/** + * Implements hook_hook_taxonomy_term_update(). + */ +function shs_taxonomy_term_update($term) { + // Update vocabulary cache for the terms parents. + foreach ($term->parent as $parent) { + shs_term_get_children($term->vid, $parent, array('node_count' => TRUE), TRUE); + } +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function shs_form_taxonomy_overview_terms_alter(&$form, &$form_state, $form_id) { + $form['#submit'][] = 'shs_form_taxonomy_overview_terms_submit'; +} + +/** + * Implements hook_hook_taxonomy_term_delete(). + */ +function shs_form_taxonomy_overview_terms_submit(&$form, &$form_state) { + // Update vocabulary cache for the terms parents. + shs_term_get_children($form_state['complete form']['#vocabulary']->vid, 0, array('node_count' => TRUE), TRUE); +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function shs_form_taxonomy_form_term_alter(&$form, &$form_state, $form_id) { + if (isset($form_state['confirm_delete']) && isset($form_state['values']['vid'])) { + // Add custom submit handler to update cache. + array_unshift($form['#submit'], 'shs_form_taxonomy_form_term_submit'); + } +} + +/** + * Implements hook_hook_taxonomy_term_delete(). + */ +function shs_form_taxonomy_form_term_submit(&$form, &$form_state) { + // Update vocabulary cache for the terms parents. + $parents = db_select('taxonomy_term_hierarchy', 'tth') + ->fields('tth', array('parent')) + ->condition('tid', $form_state['term']->tid) + ->execute() + ->fetchAll(); + if ($parents) { + // Update vocabulary cache for the terms parents. + foreach ($parents as $parent) { + shs_term_get_children($form_state['term']->vid, $parent->parent, array('node_count' => TRUE), TRUE); + } + } +} + +/** + * Helper function to get all instances of widgets with type "taxonomy_shs". + * + * @param $entity_type + * Name of entity type. + * @param $bundle + * Name of bundle (optional). + * + * @return + * List of instances keyed by field name. + */ +function _shs_get_instances($entity_type, $bundle = NULL) { + $instances = array(); + $field_instances = field_info_instances($entity_type, $bundle); + // Get all field instances with widget type "shs_taxonomy". + if (empty($bundle)) { + foreach ($field_instances as $bundle_name => $bundle_instances) { + foreach ($bundle_instances as $instance) { + if ($instance['widget']['type'] == 'taxonomy_shs') { + $instances[$bundle_name][$instance['field_name']] = $instance; + } + } + } + } + else { + foreach ($field_instances as $instance) { + if ($instance['widget']['type'] == 'taxonomy_shs') { + $instances[$instance['field_name']] = $instance; + } + } + } + return $instances; +} + +/** + * Helper function to count number of nodes associated to a term. + * + * @param $term + * The term object. + * @param $count_children + * If set to TRUE, nodes in child terms are counted also. + * + * @return + * Number of nodes within the term. + */ +function _shs_term_get_node_count($term, $count_children = FALSE) { + $num_nodes = &drupal_static(__FUNCTION__, array()); + + // Maybe this needs some more caching and value-updates on node_save()/ + // _update()/delete(). + if (empty($num_nodes["{$term->tid}:{$count_children}"])) { + // Count nodes associated to this term. + $num_nodes["{$term->tid}:{$count_children}"] = db_select('taxonomy_index', 'ti') + ->fields('ti') + ->condition('tid', $term->tid) + ->execute() + ->rowCount(); + + if ($count_children) { + $tids = array(); + $tree = taxonomy_get_tree($term->vid, $term->tid); + foreach ($tree as $child_term) { + $tids[] = $child_term->tid; + } + if (count($tids)) { + $num_nodes["{$term->tid}:{$count_children}"] += db_select('taxonomy_index', 'ti') + ->fields('ti') + ->condition('tid', $tids, 'IN') + ->execute() + ->rowCount(); + } + } + } + + return isset($num_nodes["{$term->tid}:{$count_children}"]) ? $num_nodes["{$term->tid}:{$count_children}"] : 0; +} + +/** + * Helper function to create a pseudo hash needed for javascript settings. + * + * @param $length + * Lenght of string to return. + * + * @return + * Random string. + * + * @see DrupalTestCase::randomName() + */ +function _shs_create_hash($length = 8) { + $values = array_merge(range(65, 90), range(97, 122), range(48, 57)); + $max = count($values) - 1; + $hash = chr(mt_rand(97, 122)); + for ($i = 1; $i < $length; $i++) { + $hash .= chr($values[mt_rand(0, $max)]); + } + return $hash; +} diff --git a/sites/all/modules/shs/theme/shs.form.css b/sites/all/modules/shs/theme/shs.form.css new file mode 100644 index 0000000..658515f --- /dev/null +++ b/sites/all/modules/shs/theme/shs.form.css @@ -0,0 +1,28 @@ +/** +* @file +* Styles for shs module on forms. +*/ + +.shs-wrapper-processed select { + margin-top: 4px; + vertical-align: top; +} +.shs-term-add-new-wrapper { + display: inline-block; + margin: 0 0 0 10px; + vertical-align: bottom; +} +.shs-term-add-new-wrapper .buttons { + font-size: 12px; + text-align: right; +} +.shs-term-add-new-wrapper a { + display: inline; + font-size: 12px; + margin: 0 2px; + padding: 1px 3px; +} +.shs-term-add-new-wrapper input { + font-size: 90%; + padding: 1px 4px; +} diff --git a/sites/all/modules/shs/theme/shs.formatter.css b/sites/all/modules/shs/theme/shs.formatter.css new file mode 100644 index 0000000..0512415 --- /dev/null +++ b/sites/all/modules/shs/theme/shs.formatter.css @@ -0,0 +1,24 @@ +/** +* @file +* Styles for formatted output of shs module. +*/ + +.field-type-taxonomy-term-reference ul.shs-hierarchy { + list-style: none; + margin: 0; + padding: 0; +} +.field-type-taxonomy-term-reference .links ul.shs-hierarchy li { + padding: 0 5px 0 0; +} +.field-type-taxonomy-term-reference .links ul.shs-hierarchy li.last { + padding: 0; +} +.shs-hierarchy li:after { + content: '>'; + margin: 0 0 0 5px; +} +.shs-hierarchy li.last:after { + content: ''; + margin: 0; +}