diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..86b82ea8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.idea +composer.lock +docs +vendor +*.sublime-projectcompletions +*.sublime-project +*.sublime-workspace +*.sublime-classdb +examples/googlekeys.php +.php_cs.cache diff --git a/.htaccess b/.htaccess new file mode 100644 index 00000000..94faea53 --- /dev/null +++ b/.htaccess @@ -0,0 +1,4 @@ +php_value xdebug.var_display_max_depth 20 +php_value xdebug.var_display_max_children 256 +php_value xdebug.var_display_max_data 1024 + diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index d511905c..00000000 --- a/LICENSE.txt +++ /dev/null @@ -1,339 +0,0 @@ - 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/README.md b/README.md index 1cc9c812..1ba15b7d 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,51 @@ -# A Forms API Library for PHP +# A Forms API Library for PHP ## Important notices -The main repository for this code is github. If you have received the file +This project is a fork from : https://github.com/darrenmothersele/php-forms-api + +The main repository for this code is github. If you have received the file from another location please check this URL for the latest version: - https://github.com/darrenmothersele/php-forms-api + https://github.com/degami/php-forms-api For bug reports, feature requests, or support requests, please start a case on the GitHub case tracker: - https://github.com/darrenmothersele/php-forms-api/issues + https://github.com/degami/php-forms-api/issues This library is open source licenses using GPL. See LICENSE.txt for more info. -Requirements: PHP 5.2.5+ +Requirements: + + * PHP 7.2 + * jQuery UI + * jQuery-mask-plugin ~~( https://github.com/igorescobar/jQuery-Mask-Plugin component is not included as a dependency in composer.json as the packagist.org package info is broken, you need to download the file by yourself )~~ + * Google's reCaptcha lib in order to use recaptcha fields ( https://code.google.com/p/recaptcha/downloads/list?q=label:phplib-Latest ) + * jQuery nestable plugin in order to use nestable fields ( https://github.com/dbushell/Nestable component , you need to download the file by yourself ) ## Roadmap - * **Finish the Documentation!** - * Finish implementation of options such as _disabled_, _attributes_, - * Offer a wider range of examples - * Complete the javascript functionality for collapsible fieldsets and the - password strength meter for password fields. + * **Finish the Documentation!** - you can also use phpdoc to generate a nice one ( phpdoc -d src -t docs --template=clean ) + * ~~Finish implementation of options such as _disabled_, _attributes_,~~ + * ~~Offer a wider range of examples~~ + * ~~Complete the javascript functionality for collapsible fieldsets and the + password strength meter for password fields.~~ * Complete unit tests. - * AJAX support for field submission and file uploads. + * ~~AJAX support for field submission and file uploads.~~ * Provide a better default theme and a methodology for defining themes. - * Implement new field types such as button, dates - * Add support for masked fields, possibly using a jquery plugin. + * ~~Implement new field types such as button, dates~~ + * ~~Add support for masked fields, possibly using a jquery plugin.~~ + * ~~Multistep forms~~ + * ~~Form builder class (à la drupal_get_form) in order to inject form state during form definition~~ + * ~~Support for AJAX form updates ( eg. "add another" button, depending fields, event driven modification of the form elements )~~ + * ~~Own namespace~~ -## Introduction +## Introduction This Forms API for PHP takes all the hard work out of building, validating, and -processing HTML forms in your PHP applications. The Forms API handles all -common form elements and validation rules, and it's object-oriented design +processing HTML forms in your PHP applications. The Forms API handles all +common form elements and validation rules, and it's object-oriented design can be easily expanded with new fields and validation rules. ## Basic forms workflow @@ -44,41 +56,37 @@ The basic workflow of this forms API is as follows: * Add fields to form * Process form * Display form - -It may not be obvious at first why you process the form before you display the -form. This is done because, when using this library, usually the same page -(or controller in MVC) will handle the form submissions as well as displaying + +It may not be obvious at first why you process the form before you display the +form. This is done because, when using this library, usually the same page +(or controller in MVC) will handle the form submissions as well as displaying the form. This is useful because if the form fails validation you are in the -right place to display the form again with error messages and prompts to +right place to display the form again with error messages and prompts to correct mistakes and resubmit the form. Let's look at the steps of the form workflow in more detail: ### Create new form object -The form object is the main starting point for using this Forms API. When you +The form object is the main starting point for using this Forms API. When you create a form you can provide some options in an array to override the default options. By default the form submits back to itself, this is the most useful configuration because you can use the same object to validate and process the submitted form. -NB: You can use the default form ID of cs_form if you only have one form, but -if you have multiple forms then you must override this value. The form ID is -used to generate the HTML ID tag, so must be unique for valid HTML. It is also -used in generating the default name of the php function to use when the form is -submitted. - -NB: If you are using file uploads in your form then you must specify the -enctype. For more information see the [Form objects] section of the API -reference. +NB: You can use the default form ID of cs_form if you only have one form, but +if you have multiple forms then you must override this value. The form ID is +used to generate the HTML ID tag, so must be unique for valid HTML. It is also +used in generating the default name of the php function to use when the form is +submitted. ### Add fields to your form -You can add any number of fields to a form, but the only required field is a -submit button so that the user can submit the form. +You can add any number of fields to a form, but the only required field is a +submit button so that the user can submit the form. -You can nest fields inside fieldsets if you want to break up longer forms, or -hide advanced options that are not commonly used. Javascript (jQuery) is +You can nest fields inside fieldsets if you want to break up longer forms, or +hide advanced options that are not commonly used. Javascript (jQuery) is provided to support collapsible fieldsets. ### Processing forms @@ -88,27 +96,29 @@ This is what happens when you submit a form for processing: * Check the incoming request to see if form has been submitted. If this page request is the result of a submission then copy the values from the request into the form. + * Call any alter functions for this form_id. * Run any field processors defined to run before validation (preprocess). - * Validate the form. If the form was not submitted fail validation without + * Validate the form. If the form was not submitted fail validation without checking anything so the form displays for the first time. If this request was the result of submitting this form run all field validators. If any of - the validators fail set error conditions in the form. + the validators fail set error conditions in the form. Then the form's level + validate handler is called if it is found. * If the form was submitted and passed validation run any field processors - defined to run after validation (postprocess). Then check if the submit - handler is exists as a php function. - * If the submit handler is found it is called, passing in + defined to run after validation (postprocess). Then check if the submit + handler is exists as a php function. + * If the submit handler is found it is called, passing in the final form object (which can be used to extract the values). ### Display the form Displaying the form is actually the final step. If there is no form submission -found in the request then this is the first time it is displayed and it has -been populated with the default values. Alternatively the form may be being +found in the request then this is the first time it is displayed and it has +been populated with the default values. Alternatively the form may be being displayed as the result of a request that has failed validation. In this case extra information is displayed about the error conditions. -## A walkthrough of the contact form example +## A walkthrough of the contact form example You will find the source code for this example in this file: example/contact.php @@ -116,19 +126,14 @@ You will find the source code for this example in this file: example/contact.php ## Form API reference -To use this Form API in your projects you just need to include the main -form.php file as follows: - - require 'form.php'; +Inclusion: Autoload will do the magic :) -Be sure to correct the path to form.php if it is not in the same folder as -your script. The static assets (images, css and javascript) should by default -be in a folder called assets. You may need to set the BASE_PATH configuration -option if this changes. +Usage: Write the definition and submission functions, then get the form object +with form_builder::get_form() and render it. -### Form objects +### Form objects -This example array shows all valid options for form objects, and their +This example array shows all valid options for form objects, and their default values: $options = array( @@ -136,70 +141,452 @@ default values: 'action' => '', 'attributes' => array(), 'method' => 'post', - 'prefix' => FORMS_DEFAULT_PREFIX, // set in configuration - 'suffix' => FORMS_DEFAULT_SUFFIX, // set in configuration - 'submit' => FORM_ID .'_submit', + 'container_tag' => FORMS_DEFAULT_FORM_CONTAINER_TAG, //set in configuration + 'container_class' => FORMS_DEFAULT_FORM_CONTAINER_CLASS, //set in configuration + 'prefix' => '', + 'suffix' => '', + 'submit' => array(FORM_ID .'_submit'), + 'validate' => array(FORM_ID .'_validate'), + 'inline_errors' => FALSE, + 'ajax_submit_url' => '', ); -If you include a file upload field in your form then you must set the encoding -type to multipart data, by setting an attribute like this: - - $options['attributes']['enctype'] = 'multipart/form-data'; +fields get a form reference during render , so they can modify the form object +state (eg. add the enctype attribute, or js scripts) ### Field objects Here are the available fields and their options: +#### Common + + $options = array( + 'title' => '', + 'description' => '', + 'name' => '', + 'id' => '', + 'attributes' => array(), + 'default_value' => '', + 'disabled' => FALSE, + 'stop_on_first_error' => FALSE, + 'tooltip' => FALSE, + 'container_tag' => FORMS_DEFAULT_FIELD_CONTAINER_TAG, //set in configuration + 'container_class' => FORMS_DEFAULT_FIELD_CONTAINER_CLASS, //set in configuration + 'label_class' => FORMS_DEFAULT_FIELD_LABEL_CLASS, //set in configuration + 'container_inherits_classes' => FALSE, + 'required_position' => 'after', + 'prefix' => '', + 'suffix' => '', + 'size' => 60, + 'weight' => 0, + 'validate' => array(), + 'preprocess' => array(), + 'postprocess' => array(), + ); + #### Text fields + $options += array( + 'type' => 'textfield', + ); + +#### Autocomplete + + $options += array( + 'type' => 'autocomplete', + 'autocomplete_path' => '', + 'options' => array(), + 'min_length' => 3, + ); + +if options array is defined, it is used as source for the autocomplete widget, +otherwise autocomplete_path is used + +#### Spinners + + $options += array( + 'type' => 'spinner', + 'min' => NULL, + 'max' => NULL, + 'step' => 1, + ); + +#### Masked fields + + $options += array( + 'type' => 'maskedfield', + 'mask' => '', + ); + #### Password fields + $options += array( + 'type' => 'password', + 'with_confirm' => FALSE, + 'with_strength_check' => FALSE, + 'confirm_string' => 'Confirm password', + ); + #### Text areas + $options += array( + 'type' => 'textarea', + 'rows' => 5, + 'resizable' => FALSE, + ); + + $options += array( + 'type' => 'tinymce', + 'tinymce_options' => array(), + ); + #### Submit buttons + $options += array( + 'type' => 'submit', + 'js_button' => FALSE, + ); + +They are always valid. + +#### Reset buttons + + $options += array( + 'type' => 'reset', + ); + +They are always valid. + +#### Buttons + + $options += array( + 'type' => 'button', + 'label' => '', + 'js_button' => FALSE, + ); + +#### Image Buttons + + $options += array( + 'type' => 'image_button', + 'src' => '', + 'alt' => '', + 'js_button' => FALSE, + ); + +They are always valid. +The value after submit is an array containing fields x,y + #### Select lists + $options += array( + 'type' => 'select', + 'options' => array(), + 'multiple' => FALSE, + ); + +#### Select Menus + + $options += array( + 'type' => 'selectmenu', + ); + +#### Multi Select + + $options += array( + 'type' => 'multiselect', + ); + +#### Sliders + + $options += array( + 'type' => 'slider', + 'options' => array(), + ); + +#### Color Pickers + + $options += array( + 'type' => 'colorpicker', + ); + #### Radio buttons + $options += array( + 'type' => 'radios', + 'options' => array(), + ); + #### Checkboxes + $options += array( + 'type' => 'checkboxes', + 'options' => array(), + ); + +#### Checkbox + + $options += array( + 'type' => 'checkbox', + ); + +#### Switchboxes + + $options += array( + 'type' => 'switchbox', + 'yes_value' => 1, + 'yes_label' => 'Yes', + 'no_value' => 0, + 'no_label' => 'No', + ); + #### Hidden values + $options += array( + 'type' => 'hidden', + ); + +#### OTP (one time passwords) + + $options += array( + 'type' => 'otp', + 'otp_length' => 6, + 'otp_type' => 'numeric', // can be numeric, alpha, alpha_numeric + 'show_characters' => false + ); + #### Markup + $options += array( + 'type' => 'markup', + ); + +Markup values are not passed to the values() function + +#### Progressbar + + $options += array( + 'type' => 'progressbar', + 'show_label' => FALSE, + 'indeterminate' => FALSE, + ); + +#### Values + + $options += array( + 'type' => 'value', + ); + +Values are passed with the form on submit but are not shown during render. +They are always valid. + #### Files + $options += array( + 'type' => 'file', + 'destination' => '', + ); + +#### Dates + + $options += array( + 'type' => 'date', + 'start_year' => '', + 'end_year' => '', + 'granularity' => 'day', // one of: year, month or day + 'js_selects' => FALSE, + ); + $options['default_value'] = array( + 'year'=>'', + 'month'=>'', + 'day'=>'', + ); + +#### Date Pickers + + $options += array( + 'type' => 'datepicker', + 'date_format' => 'yy-mm-dd', + 'change_month' => FALSE, + 'change_year' => FALSE, + 'mindate' => '-10Y', + 'maxdate' => '+10Y', + 'yearrange' => '-10:+10', + 'disabled_dates' => array(), + ); + +#### Times + + $options += array( + 'type' => 'time', + 'granularity' => 'seconds', // one of: hours, minutes or seconds + 'js_selects' => FALSE, + ); + $options['default_value'] = array( + 'hours'=>'', + 'minutes'=>'', + 'seconds'=>'', + ); + +#### DateTimes + + $options += array( + 'type' => 'datetime', + 'start_year' => '', + 'end_year' => '', + 'granularity' => 'day - seconds', // one of: year, month, day, hours, minutes or seconds + 'js_selects' => FALSE, + ); + $options['default_value'] = array( + 'year'=>'', + 'month'=>'', + 'day'=>'', + 'hours'=>'', + 'minutes'=>'', + 'seconds'=>'', + ); + +#### Math captcha + + $options += array( + 'type' => 'math_captcha', + 'pre_filled' => FALSE, + ); + +#### Image captcha + + $options += array( + 'type' => 'image_captcha', + 'out_type' => 'png', + 'characters' => 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', + 'min_length' => 5, + 'max_length' => 8, + 'image_width' => 100, + 'image_height' => 50, + 'font_size' => 14, + 'pre_filled' => FALSE, + ); + +#### Recapthas + + $options += array( + 'type' => 'recapcha', + 'publickey' => '', + 'privatekey' => '', + ); + + be sure you have loaded recaptchalib.php + ( https://code.google.com/p/recaptcha/downloads/list?q=label:phplib-Latest ) + +#### Tag containers + + $options += array( + 'type' => 'tag_container', + 'tag' => 'div', + ); + +#### Table containers + + $options += array( + 'type' => 'table_container', + 'table_header' => array(), // an array of column headers + 'col_row_attributes' => array(), // a matrix of cells attributes ( each one can be a string or an array ) + ); + #### Field sets -### Validators reference + $options += array( + 'type' => 'fieldset', + ); -Required, Max Length, Min Length, Exact Length, Alpha, Alpha-numeric, -Alpha-numeric with dashes, Numeric, Integer, Field matching, Email, -File extension, File not exists, File max size, File type. +#### Tabs -### Processors reference + $options += array( + 'type' => 'tabs', + ); -For security reasons, all user submitted data should be filtered before use. -This Forms API library helps you by providing some useful filtering tools that -will process submitted input. You can use these in your submit functions as and -when you need them, or you can have them run automatically during forms -processing. You can specify processors to run on a field before or after the -field is passed for validation. +#### Accordions -Plain, Trim, LTrim, RTrim, XSS, XSS Weak, + $options += array( + 'type' => 'accordion', + ); +#### Sortables + $options += array( + 'type' => 'sortable', + 'handle_position' => 'left', + ); + $options += array( + 'type' => 'sortable_table', + 'handle_position' => 'left', + 'table_header' => array(), + ); +#### Nestables + $options += array( + 'type' => 'nestable', + ); + +#### Repeatables + $options += array( + 'type' => 'repeatable', + 'num_reps' => 1, + ); + +#### Geolocation (latitude, longitude) + $options += array( + 'type' => 'geolocation', + ); + $options['default_value'] = array( + 'latitude' => NULL, + 'longitude' => NULL, + ); + $options += array( + 'type' => 'gmaplocation', + 'zoom' => 8, + 'scrollwheel' => FALSE, + 'mapwidth' => '100%', + 'mapheight' => '500px', + 'markertitle' => NULL, + 'maptype' => 'google.maps.MapTypeId.ROADMAP', + 'with_geocode' => FALSE, + 'lat_lon_type' => 'hidden', + 'geocode_box' => NULL, + 'with_map' => TRUE, + ); + $options['default_value'] = array( + 'latitude' => NULL, + 'longitude' => NULL, + 'geocodebox' => NULL, + ); + +### Validators reference +Required, Max Length, Min Length, Exact Length, Regular Expression, Alpha, +Alpha-numeric, Alpha-numeric with dashes, Numeric, Integer, Field matching, +Email, File extension, File not exists, File max size, File type. +### Processors reference + +For security reasons, all user submitted data should be filtered before use. +This Forms API library helps you by providing some useful filtering tools that +will process submitted input. You can use these in your submit functions as and +when you need them, or you can have them run automatically during forms +processing. You can specify processors to run on a field before or after the +field is passed for validation. + +Plain, Trim, LTrim, RTrim, XSS, XSS Weak, Addslashes +### Form_builder +Will be the new way to get form objects. Like drupal_get_form, it will pass a +form_state argument to the form definition function. diff --git a/composer.json b/composer.json new file mode 100644 index 00000000..e6aaa67a --- /dev/null +++ b/composer.json @@ -0,0 +1,41 @@ +{ + "name": "degami/php-forms-api", + "description": "A simple to use Forms API for generating, validating and processing forms in PHP", + "type": "library", + "keywords": ["forms", "PHP", "API"], + "homepage": "https://github.com/degami/php-forms-api", + + "license": "MIT", + "authors": [ + { + "name": "degami@github.com", + "homepage": "https://github.com/degami/php-forms-api" + } + ], + "require": { + "php": ">=7.2", + "ext-json": "*", + "ext-gd": "*", + "degami/basics": "dev-master", + "components/jqueryui": "1.12.*", + "igorescobar/jquery-mask-plugin": "*" + }, + "autoload": { + "files": [ + "src/php_forms_api_defines.php", + "src/php_forms_api_bootstrap.php" + ], + "psr-4": { + "Degami\\PHPFormsApi\\": "src/classes", + "Degami\\PHPFormsApi\\Interfaces\\": "src/interfaces", + "Degami\\PHPFormsApi\\Traits\\": "src/classes/traits", + "Degami\\PHPFormsApi\\Abstracts\\Base\\": "src/classes/abstracts/base", + "Degami\\PHPFormsApi\\Abstracts\\Containers\\": "src/classes/abstracts/containers", + "Degami\\PHPFormsApi\\Abstracts\\Fields\\": "src/classes/abstracts/fields", + "Degami\\PHPFormsApi\\Accessories\\": "src/classes/accessories", + "Degami\\PHPFormsApi\\Containers\\": "src/classes/containers", + "Degami\\PHPFormsApi\\Fields\\": "src/classes/fields", + "Degami\\PHPFormsApi\\Exceptions\\": "src/classes/exceptions" + } + } +} diff --git a/examples/ajax_contact.php b/examples/ajax_contact.php new file mode 100644 index 00000000..f474a5b5 --- /dev/null +++ b/examples/ajax_contact.php @@ -0,0 +1,31 @@ + + + + + Example contact form + + + + +

Example Form

+
+ To list | + Go back +
+
+ render('html');?> + + +
+ + diff --git a/examples/ajax_url.php b/examples/ajax_url.php new file mode 100644 index 00000000..0c033f38 --- /dev/null +++ b/examples/ajax_url.php @@ -0,0 +1,28 @@ +process(); + +// Submit function to call when the form is submitted and passes validation. +// This is where you would send the email (using PHP mail function) +// as this is not a real example I'm just outputting the values for now. +function contactform_ajax_submit(&$form) +{ + $form_values = $form->getValues(); + return $form_values; + //var_dump($form->get_triggering_element()); + // Reset the form if you want it to display again. + // $form->reset(); +} + +if ($form->isSubmitted()) : + print json_encode(array( 'html' => '

Thanks for submitting the form.

'.print_r($form->getSubmitResults('contactform_ajax_submit'), true).'
', 'js' => '' , 'is_submitted' => true )); +else : + print $form->render(/* 'json' */); +endif; diff --git a/examples/batch.php b/examples/batch.php new file mode 100644 index 00000000..42e207c6 --- /dev/null +++ b/examples/batch.php @@ -0,0 +1,35 @@ +render(); +} else { + ?> + + + + Example contact form + + + + +

Batch Operation Form

+
+ To list | + Go back +
+
+ render('html');?> + + +
+ + + diff --git a/examples/bulk.php b/examples/bulk.php new file mode 100644 index 00000000..611c7a36 --- /dev/null +++ b/examples/bulk.php @@ -0,0 +1,59 @@ +getValues()->toArray(); +// $form->add_highlight('Bulk operation done!'); +// $form->reset(); + + return $form_values; +} + +// function my_contactform_form_alter($form){ +// $form->get_field('fieldset')->remove_field('message'); +// } + +$form = FAPI\FormBuilder::getForm('bulkform'); +?> + + + + Example contact form + + + + + + +

Example Form

+
+ To list | + Go back +
+
+
process(); ?>
+ isSubmitted()) : ?> + +

Thanks for submitting the form.

+
getSubmitResults());?>
+ + render(); ?> + + + +
+ + diff --git a/examples/components b/examples/components new file mode 120000 index 00000000..e07072e6 --- /dev/null +++ b/examples/components @@ -0,0 +1 @@ +../vendor/components \ No newline at end of file diff --git a/examples/contact.php b/examples/contact.php index d25cd566..23ec80b4 100644 --- a/examples/contact.php +++ b/examples/contact.php @@ -1,64 +1,88 @@ - 'contact')); -$form->add_field('name', array( - 'type' => 'textfield', - 'validate' => array('required'), - 'preprocess' => array('trim'), - 'title' => 'Your name', -)); -$form->add_field('email', array( - 'type' => 'textfield', - 'validate' => array('required', 'email'), - 'title' => 'Your email address', -)); -$form->add_field('message', array( - 'type' => 'textarea', - 'postprocess' => array('xss'), - 'title' => 'Your message', -)); -$form->add_field('submit', array( - 'type' => 'submit', -)); +define('PHP_FORMS_API_DEBUG', true); +//session_start(); +// function my_textfield_field_alter(&$options, &$name){ +// $name = 'my_'.$name; +// $options['attributes'] = ['style' => 'border: solid 1px #f00;']; +// } +// function my_textfield_field_render_output_alter(&$html){ +// $html = 'aaa'.$html; +// } +// function __($str){ +// return "__ $str __"; +// } // Submit function to call when the form is submitted and passes validation. -// This is where you would send the email (using PHP mail function) +// This is where you would send the email (using PHP mail function) // as this is not a real example I'm just outputting the values for now. -function contact_submit(&$form) { - $form_values = $form->values(); - print_r($form_values); - // Reset the form if you want it to display again. - // $form->reset(); -} +function my_very_own_contactform_submit(&$form, &$form_state) +{ + $form_values = $form->getValues(); + $out = []; + foreach ($form_values->fieldset as $key => $value) { + $out[$key] = $value; + } + return $out; + return implode( + ' - ', + [ + $form_values->fieldset->name, + $form_values->fieldset->email, + $form_values->fieldset->message, + ] + ); + return $form_values; + + $form->addHighlight('Message sent!'); + print_r($form_values); + //var_dump($form->get_triggering_element()); + // Reset the form if you want it to display again. + $form->reset(); +} +// function my_contactform_form_alter($form){ +// $form->get_field('fieldset')->remove_field('message'); +// } +// i just want another form_id for my form +$form = FAPI\FormBuilder::getForm('contactform', 'my_very_own_contactform'); ?> - - Example contact form - + + Example contact form + + + -Go back -
process(); ?>
-

Example Form

-is_submitted()): ?> - -

Thanks for submitting the form.

- - render(); ?> - -
+

Example Form

+
+ To list | + Go back +
+
+
process(); ?>
+ isSubmitted()) : ?> + +

Thanks for submitting the form.

+
getSubmitResults());?>
+
getValues());?>
+ + render(); ?> + + + +
diff --git a/examples/databags.php b/examples/databags.php new file mode 100644 index 00000000..8c0b6d00 --- /dev/null +++ b/examples/databags.php @@ -0,0 +1,53 @@ + + + + + Example session bag + + + + +

Example Form

+
+ To list | + Go back +
+
+
add(
+//    $val
+//);
+if (isset($_GET['add'])) {
+    $bag->counter = isset($bag->counter) ? $bag->counter+1 : 0;
+    $bag->more = ['datas' => 0, 'load2' => 2, 'load' => ['count' => null] ];
+    $bag['more']['datas']=5;
+    $bag['more']['load']['count']=22;
+}
+//var_dump($val);
+var_dump($bag->toArray());
+?>
+
+
+ + diff --git a/examples/dates.php b/examples/dates.php new file mode 100644 index 00000000..670fe9f7 --- /dev/null +++ b/examples/dates.php @@ -0,0 +1,49 @@ +getValues(); + return $form_values->toArray(); + //var_dump($form->get_triggering_element()); + // Reset the form if you want it to display again. + // $form->reset(); +} + +$form = FAPI\FormBuilder::getForm('datesform'); +?> + + + + Example dates form + + + + +

Dates Form

+
+ To list | + Go back +
+
+
process(); ?>
+ isSubmitted()) : ?> + +
getSubmitResults());?>
+

Thanks for submitting the form.

+ + render(); ?> + + + +
+ + diff --git a/examples/events.php b/examples/events.php new file mode 100644 index 00000000..26765496 --- /dev/null +++ b/examples/events.php @@ -0,0 +1,56 @@ +getValues(); + //var_dump($form->get_triggering_element()); + // Reset the form if you want it to display again. + // $form->reset(); + return $form_values->toArray(); +} + +$form = FAPI\FormBuilder::getForm('eventsform'); + +if (isset($_REQUEST['partial'])) { + print $form->render(); +} else { + ?> + + + + Example contact form + + + + + +

Events Form

+
+ To list | + Go back +
+
+
process(); ?>
+ isSubmitted()) : ?> + +
getSubmitResults());?>
+

Thanks for submitting the form.

+ + render(); ?> + + + +
+ + + allocatedSize); +if (defined('PHP_FORMS_API_DEBUG')) : + $has_session = FAPI\FormBuilder::sessionPresent(); + if ($has_session) : ?> +
+
Session Info
+
toArray());// print_r($form); ?>
+
+ +
function body
+
getDefinitionBody();?>
+ 'contact', + // )); + // + $form->setInlineErrors(true); //->set_on_dialog(TRUE); + + $form + ->addField('fieldset', array( + 'type' => 'tabs', + 'title' => 'Contact', + ))->addTab('Contact') + ->addField('name', array( + 'type' => 'textfield', + 'validate' => array('required'), + 'preprocess' => array('trim'), + 'title' => 'Your name', + )) + ->addField('email', array( + 'type' => 'textfield', + 'validate' => array('required', 'email'), + 'title' => 'Your email address', + )) + ->addField('message', array( + 'type' => 'tinymce', + 'postprocess' => array('xss'), + 'title' => 'Your message', + )) + ->addField('switcher', array( + 'type' => 'switchbox', + 'title' => 'Yes or No', + 'default_value' => 1, +// 'default_value' => 'a', +// 'yes_value' => 'a', 'yes_label' => 'A value', +// 'no_value' => 'b', 'no_label' => 'B value', + )) + ->addField('captcha', array( + 'type' => 'math_captcha', + 'title' => 'Check this out!', + 'pre_filled' => true, + )) + ->addField('submit', array( + 'type' => 'submit', + )); + + return $form; +} + + + +//############################################################################// +//############################################################################// +//############################################################################// + + + +// Generate a simple contact form + +function contactform_ajax(FAPI\Form $form, &$form_state) +{ + // $form = new FAPI\form(array( + // 'form_id' => __FUNCTION__, + // 'ajax_submit_url' => 'ajax_url.php', + // 'output_type' => 'json', + // )); + + $form + ->setFormId(__FUNCTION__) + ->setAjaxSubmitUrl('ajax_url.php') + ->setOutputType('json'); + + $form->addField('name', array( + 'type' => 'textfield', + 'validate' => array('required'), + 'preprocess' => array('trim'), + 'title' => 'Your name', + )); + $form->addField('email', array( + 'type' => 'textfield', + 'validate' => array('required', 'email'), + 'title' => 'Your email address', + )); + $form->addField('message', array( + 'type' => 'textarea', + 'postprocess' => array('xss'), + 'title' => 'Your message', + )); + $form->addField('submit', array( + 'type' => 'submit', + )); + $form->addField('message2', array( + 'type' => 'textarea', + 'postprocess' => array('xss'), + 'title' => 'Your message 2', + ), 1); + $form->addField('submit2', array( + 'type' => 'submit', + ), 1); + + return $form; +} + +//############################################################################// +//############################################################################// +//############################################################################// + + +function multistepform(FAPI\Form $form, &$form_state) +{ +/* $form = new FAPI\form(array( + 'form_id' => __FUNCTION__, + 'action' => 'multistep.php', + ));*/ + + $form->setAction('multistep.php'); + + // add to step 0 + $form + ->addField('login_info', array( + 'type'=>'fieldset' + ), 0) + ->addField('user[username]', array( + 'title' => 'Username', + 'type'=>'textfield', + 'validate' => array('required'), + 'preprocess' => array('trim'), + )) + ->addField('user[password]', array( + 'title' => 'Password', + 'type'=>'password', + 'validate' => array('required'), + 'preprocess' => array('trim'), + )) + ->addField('user[image]', array( + 'title' => 'Picture', + 'type'=>'file', + 'destination' => dirname(__FILE__), + )) + ->addField('image2', array( + 'title' => 'Picture', + 'type'=>'file', + 'destination' => dirname(__FILE__), + )) + // ->addField('recaptcha',array( + // 'title' => 'Recaptcha', + // 'type'=>'recaptcha', + // 'publickey' => RECAPTCHA_PUBLIC_KEY, + // 'privatekey' => RECAPTCHA_PRIVATE_KEY, + // )) + ->addField('submit', array( + 'type'=>'submit', + 'value' => 'Continue', + )); +/* + // add to step 1 + $form + ->addField('personal_info', array( + 'type'=>'fieldset' + ), 1) + ->addField('name', array( + 'title' => 'Name', + 'type'=>'textfield', + 'validate' => array('required'), + 'preprocess' => array('trim'), + )) + ->addField('surname', array( + 'title' => 'Surname', + 'type'=>'textfield', + 'validate' => array('required'), + 'preprocess' => array('trim'), + )) + ->addField('birthday', array( + 'title' => 'Birthday', + 'type'=>'date', + )) + ->addField('submit', array( + 'type'=>'submit', + 'value' => 'Save', + )); +*/ + return $form; +} + + + +//############################################################################// +//############################################################################// +//############################################################################// + + +function showallform(FAPI\Form $form, &$form_state) +{ + $form = new FAPI\Form(array( + 'form_id' => 'showall', + //'inline_errors' => TRUE, + // 'attributes'=>array('enctype'=>'multipart/form-data') + )); + + $object = new stdClass; + $object->val1='val1'; + + $form->addField('object', array( + 'type'=>'value', + 'value' => $object, + 'my_evil_option' => 'evil_value', + )); + + // var_dump( isset($form->get_field('object')->my_evil_option) ); // evil option is not contained + + $form->addField('fieldset', array( + 'type' => 'fieldset', + 'attributes'=>array( + //'style' => 'width: 500px;padding: 10px 10px 10px 5px;', + ), + 'collapsible' => true, + 'title' => 'my fieldset', + )); + + $form->getField('fieldset')->addField('name', array( + 'type' => 'textfield', + 'validate' => array('multiple_by[3]','ReQuired'), // will be reordered and normalized + 'preprocess' => array('trim'), + 'title' => 'Your name', + 'tooltip' => true, + 'attributes' => array( + 'style' => 'width: 100%', + ), + )); + $form->getField('fieldset')->addField('email', array( + 'type' => 'email', + 'title' => 'Your email address', + 'attributes' => array( + 'style' => 'width: 100%', + 'placeholder' => 'yourmail@yourdomain', + ), + )); + $form->getField('fieldset')->addField('password', array( + 'type' => 'password', + // 'validate' => array('required'), + 'title' => 'Your Password', + 'attributes' => array( + 'style' => 'width: 100%', + ), + 'with_confirm' => true, + 'with_strength_check' => true, + )); + $form->getField('fieldset')->addMarkup('Markup 1 before all', array( + 'weight' => -10, + )); + + $form->getField('fieldset')->addField('email', array( + 'type' => 'otp', + 'title' => 'Your OTP', + 'otp_length' => 6, + )); + + $form->addField('fieldset2', array( + 'type' => 'fieldset', + 'attributes'=>array( + // 'style' => 'width: 500px;padding: 10px 10px 10px 5px;', + ), + 'collapsible' => true, + 'collapsed' => false, + 'title' => 'my fieldset 2', + )) + ->addField('message', array( + 'type' => 'textarea', + 'postprocess' => array('xss'), + 'title' => 'Your message', + 'rows' => 10, + 'resizable' => true, + 'attributes' => array( + 'style' => 'width: 100%;height: 200px;', + 'placeholder' => 'Type your message', + 'style' => 'width: 100%', + ), + )) + ->addField('message2', array( + 'type' => 'tinymce', + 'title' => 'Your beautiful message', + 'rows' => 10, + )) + ->addField('masked', array( + 'title' => 'Phone', + 'type' => 'maskedfield', + 'mask'=>'0000-0000', + )); + + $accordion = new FAPI\Containers\Accordion(array( + 'collapsible' => true, + 'attributes'=>array( + // 'style' => 'width: 500px', + )), 'accordion'); + + $accordion->addAccordion('accordion1'); + $accordion->addAccordion('accordion2'); + + $accordion->addField('spinner', array( + 'type' => 'spinner', + 'title' => 'Select a value', + ), 0) + ->addField('range', array( + 'type' => 'range', + 'title' => 'Range a value', + ), 0) + ->addField('number', array( + 'type' => 'number', + 'title' => 'Number field', + ), 0) + ->addField('color', array( + 'type'=>'color', + 'title' => 'Color', + 'default_value' => '#be2a99', + )) + ->addField('colorpicker', array( + 'type' => 'colorpicker', + 'title' => 'Pick your color', + 'default_value' => '#88B2D1', + )); + + $accordion->addField('date', array( + 'type' => 'date', + 'title' => 'select date', + 'granularity' => 'day', + 'js_selects' => false, + ), 1); + + $accordion->addField('time', array( + 'type' => 'time', + 'title' => 'time', + 'granularity' => 'minutes', + 'default_value' => array('hours'=>10,'minutes'=>23), + 'js_selects' => false, + ), 1); + + $accordion->addField('datepicker', array( + 'type' => 'datepicker', + 'title' => 'date picker', + 'weight' => -10, + ), 1); + + $accordion->addField('datetime', array( + 'type' => 'datetime', + 'title' => 'date time', + 'js_selects' => true, + ), 1); + + + $form->addField($accordion->getName(), $accordion); + + + $form->addField('tabs', array( + 'type' => 'tabs', + 'attributes'=>array( + // 'style' => 'width: 500px', + ), + )) + ->addTab('tab1') //index 0 + ->addTab('tab2') //index 1 + ->addTab('tab3') //index 2 + ->addField('markup2', array( + 'type' => 'markup', + 'value' => 'markup bbb', + ), 0) //to tab 0 + ->addField('markup3', array( + 'type' => 'markup', + 'value' => 'markup ccc', + ), 1) //to tab 1 + ->addField('checkboxes', array( + 'type' => 'checkboxes', + 'options' => array(0=>'zero',1=>'one',2=>'two'), + 'default_value' => 1, + )) //to tab 0 + ->addField('radios', array( + 'type' => 'radios', + 'options' => array(0=>'zero',1=>'one',2=>'two'), + 'default_value' => 2, + ), 2) //to tab 2 + ->addField('reqtextfield', array( + 'title' => 'Required Textfield', + 'type' => 'textfield', + 'default_value' => '', + 'validate' => array('required'), + )) //to tab 0 + ->addField('file', array( + 'type' => 'file', + 'destination' => dirname(__FILE__), + // 'validate' => array('required'), + ), 1) //to tab 1 + ->addField('select', array( + 'type' => 'select', + 'title' => 'select a number - select', + 'options' => array('1'=>'one','2'=>'two','3'=>'three','four'=>array('5'=>'five','6'=>'six','7'=>'seven'),'8'=>'eight'), + 'attributes' => array( + 'placeholder' => 'select placeholder', + ), + 'validate' => array('required'), + ), 1) //to tab 1 + ->addField('selectmenu', array( + 'type' => 'selectmenu', + 'title' => 'select a number - selectmenu', + 'options' => array('1'=>'one','2'=>'two','3'=>'three','four'=>array('5'=>'five','6'=>'six','7'=>'seven'),'8'=>'eight'), + 'default_value' => '2', + ), 1) //to tab 1 + ->addField('slider', array( + 'type' => 'slider', + 'title' => 'select a number - slider', + 'options' => array('1'=>'one','2'=>'two','3'=>'three','four'=>array('5'=>'five','6'=>'six','7'=>'seven'),'8'=>'eight'), + 'default_value' => '2', + 'with_val' => true, + ), 1); //to tab 1 + + + $form->addField('hidden1', array( + 'type' => 'hidden', + 'default_value' => 'aaaa', + )); + + + $sortable = $form->addField('sortable', array( + 'type' => 'sortable', + )); + + for ($i=0; $i<5; $i++) { + $field = array( + 'title' => 'Textfield '.($i+1), + 'type' => 'textfield', + ); + $sortable->addField('sortable_field_'.$i, $field); + } + + $sortable_table = $form->addField('sortable_table', array( + 'type' => 'sortable_table', + 'table_header' => array( + 'Textfields', + ), + )); + for ($i=0; $i<5; $i++) { + $field = array( + 'title' => 'Textfield '.($i+1), + 'type' => 'textfield', + 'default_value' => 'value '.($i+1), + ); + $sortable_table->addField('sortable_field_'.$i, $field, $i); + } + + $nestable = $form->addField('container', array( + 'type' => 'tag_container', + 'weight' => 1000, + ))->addField('nestable', array( + 'type' => 'nestable', + 'prefix' => '

', + 'suffix' => '

', + )); + + for ($i = 0; $i < 5; $i++) { + $nestable->addField('nested_val_'.$i, array( + 'type' => 'textfield', + 'default_value' => 'nested '.$i, + ))->addChild()->addField('nested_child_val_'.$i, array( + 'type' => 'textfield', + 'default_value' => 'nestedchild '.$i, + )); + } + //echo '
';var_dump($nestable);echo '
'; + + $form->addField('progressbar', array( + 'title' => 'Progress', + 'type' => 'progressbar', + 'default_value' => '42', + 'show_label' => true, + )); + + $elemslist = array( + 'ActionScript', + 'AppleScript', + 'Asp', + 'BASIC', + 'C', + 'C++', + 'Clojure', + 'COBOL', + 'ColdFusion', + 'Erlang', + 'Fortran', + 'Groovy', + 'Haskell', + 'Java', + 'JavaScript', + 'Lisp', + 'Perl', + 'PHP', + 'Python', + 'Ruby', + 'Scala', + 'Scheme' + ); + + $form->addField('autocomplete', array( + 'type' => 'autocomplete', + 'title' => 'autocomplete', + 'options' => $elemslist, + )) + ->addField('datalist', array( + 'type' => 'datalist', + 'title' => 'datalist', + 'options' => $elemslist, + )) + ->addField('multiselect', array( + 'type' => 'multiselect', + 'title' => 'multiselect', + 'size' => 8, + 'options' => $elemslist, + 'default_value' => array(4,5,7), + )) + ->getField('container') + ->addField('checkbox', array( + 'type' => 'checkbox', + 'default_value' => 'checkbox', + 'title' => 'Check me', + 'validate' => array( array('validator'=>'required','error_message'=>'You must check the %t checkbox!' ) ), + )) + ->addField('actions', array( + 'type' => 'tag_container', + 'tag' => 'div', + )) + ->addField('submit', array( + 'type' => 'submit', + 'value' => 'Send', + )) + ->addField('submit2', array( + 'type' => 'submit', + 'value' => 'Send2', + 'js_button' => true, + )) + ->addField('button', array( + 'type' => 'button', + 'value' => 'Send3', + )) + ->addField('image', array( + 'type' => 'image_button', + 'src' => 'https://www.google.it/images/srpr/logo11w.png', + 'attributes' => array( + 'width' => '100', + ), + 'js_button' => true, + )) + ->addField('reset', array( + 'type' => 'reset', + 'value' => 'Reset', + 'js_button' => true, + )); + + return $form; +} + + +//############################################################################// +//############################################################################// +//############################################################################// + +function nestableform(FAPI\Form $form, &$form_state) +{ + $nestable = $form + ->addField('nestable', array( + 'type' => 'nestable', + 'maxDepth' => 100, + 'prefix' => '

', + 'suffix' => '

', + ))->addField('nested_val_0', array( + 'type' => 'textfield', + 'default_value' => 'nested 0', + )); + + $nestable2 = $form + ->addField('nestable2', array( + 'type' => 'nestable', + 'maxDepth' => 100, + 'prefix' => '

', + 'suffix' => '

', + ))->addField('nested2_val_0', array( + 'type' => 'textfield', + 'default_value' => 'nested2 0', + )); + + for ($i = 1; $i <= 5; $i++) { + $nestable->addChild()->addField('nested_val_'.$i, array( + 'type' => 'value', + 'default_value' => 'nested '.$i, + 'prefix' => 'nested '.$i, + ))->addChild()->addField('nested_child_val_'.$i, array( + 'type' => 'value', + 'default_value' => 'nestedchild '.$i, + 'prefix' => 'nestedchild '.$i, + 'suffix' => 'ciao', + )); + + $nestable2->addChild()->addField('nested2_val_'.$i, array( + 'type' => 'value', + 'default_value' => 'nested2 '.$i, + 'prefix' => 'nested2 '.$i, + ))->addChild()->addField('nested2_child_val_'.$i, array( + 'type' => 'value', + 'default_value' => 'nestedchild2 '.$i, + 'prefix' => 'nestedchild2 '.$i, + 'suffix' => 'ciao', + )); + } + + $form->addField('submit', array( + 'type' => 'submit', + 'value' => 'Send', + )); + return $form; +} + +//############################################################################// +//############################################################################// +//############################################################################// + + +function pluploadform(FAPI\Form $form, &$form_state) +{ + // $form = new FAPI\form(array('form_id' => 'plupload')); + $form->addField('files_upload', array( + 'type' => 'plupload', + 'title' => 'Upload Extra Files', + 'filters' => array( + 'max_file_size' => '10mb', + 'mime_types' => array( + array('title' => "Image files", 'extensions' => "jpg,jpeg,gif,png"), + array('title' => "PDF files", 'extensions' => "pdf"), + // array('title' => "Zip files", 'extensions' => "zip"), + ), + ), + 'url' => 'file_plupload.php', + 'swf_url' => 'http://www.plupload.com//plupload/js/Moxie.swf', + 'xap_url' => 'http://www.plupload.com//plupload/js/Moxie.xap', + )); + + + $form->addField('submit', array( + 'type' => 'submit', + )); + + return $form; +} + + +//############################################################################// +//############################################################################// +//############################################################################// + + +function datesform(FAPI\Form $form, &$form_state) +{ + //$form = new FAPI\form(array('form_id' => 'dates')); + + $fieldset = $form->addField('html', array( + 'type' => 'fieldset', + 'title' => 'as html fields', + )); + + $fieldset->addField('date', array( + 'type' => 'date', + 'title' => 'select date', + )); + + $fieldset->addField('time', array( + 'type' => 'time', + 'title' => 'time', + 'default_value' => '10:23', + )); + + $fieldset->addField('datetime', array( + 'type' => 'datetime', + 'title' => 'date time', + )); + + $fieldset->addField('datepicker', array( + 'type' => 'datepicker', + 'title' => 'date picker', + )); + + $fieldset = $form->addField('selects', array( + 'type' => 'fieldset', + 'title' => 'as selects', + )); + + $fieldset->addField('dateselect', array( + 'type' => 'dateselect', + 'title' => 'select date', + 'granularity' => 'day', + 'js_selects' => false, + )); + + $fieldset->addField('timeselect', array( + 'type' => 'timeselect', + 'title' => 'time', + 'granularity' => 'minutes', + 'default_value' => array('hours'=>10,'minutes'=>23), + 'js_selects' => false, + )); + + + $fieldset->addField('datetimeselect', array( + 'type' => 'datetimeselect', + 'title' => 'date time', + )); + + $form->addField('submit', array( + 'type' => 'submit', + )); + + return $form; +} + + +//############################################################################// +//############################################################################// +//############################################################################// + +function eventsform(FAPI\Form $form, &$form_state) +{ + // $form = new FAPI\form(array('form_id' => 'events')); + + $step = 0; + + $form->setAction($_SERVER['PHP_SELF']); + + $fields = $form->addField('fields', array( + 'type' => 'tag_container', + 'id' => 'fieldset-textfields-target', + )); + + $num_textfields = 0; //isset($form_state['input_values']['num_textfields']) ? (intval($form_state['input_values']['num_textfields']) + 1) : 1; + foreach ($form_state['input_values'] as $key => $value) { + if (preg_match("/^text_[0-9]+$/i", $key)) { + $num_textfields++; + } + } + + if ($num_textfields == 0 || $form->isPartial()) { + $num_textfields++; + } + + for ($i = 0; $i < $num_textfields; $i++) { + $inner = $fields->addField('inner_'.$i, array( + 'type' => 'fieldset', + 'id' => 'fieldset-textfields', + 'title' => 'textfields', + )); + $inner->addMarkup($i); + + $inner->addField('text_'.$i, array( + 'type' => 'textfield', + 'title' => 'text', + )); + + $inner + ->addField('rep_'.$i, array( + 'type' => 'repeatable', + 'title' => 'Emails', + 'default_value' => [ + ['name' => 'name1', 'email' => 'email1@email.com'], + ], + )) + ->addField('name', array( + 'type' => 'textfield', + 'validate' => array('required'), + 'preprocess' => array('trim'), + 'title' => 'Your name', + )) + ->addField('email', array( + 'type' => 'textfield', + 'validate' => array('required', 'email'), + 'title' => 'Your email address', + )); + } + + $fields->addField('num_textfields', array( + 'type' => 'hidden', + 'default_value' => $num_textfields, + 'value' => $num_textfields, + )); + + + if (FAPI\Form::isPartial()) { + $jsondata = json_decode($form_state['input_values']['jsondata']); + $callback = $jsondata->callback; + if (is_callable($callback)) { + //$target_elem = $callback( $form )->get_field('num_textfields'); + //$fieldset->add_js('console.log(JSON.parse(\''.json_encode( array( 'build_options' => preg_replace("/\\\"|\"|\n/","",serialize($target_elem->get_build_options())), 'id' => $target_elem->get_html_id(), 'value' => $target_elem->get_value()) ).'\'))'); + $fields->addJs("\$('input[name=\"{$jsondata->name}\"]').focus();"); + } + //$fieldset->add_js('alert($("#num_textfields").val())'); + //$fieldset->add_js('console.log($("#num_textfields").val())'); + } + + $form->addField('addmore', array( + 'type' => 'submit', + 'value' => 'Add more', + 'ajax_url' => $_SERVER['PHP_SELF'], + 'event' => array( + array( + 'event' => 'click', + 'callback' => 'events_form_callback', + 'target' => 'fieldset-textfields-target', + 'effect' => 'fade', + 'method' => 'replace', + ), + ), + )); + + $form->addField('submit', array( + 'type' => 'submit', + )); + +//var_dump($form->toArray()); + return $form; +} + +function events_form_callback(FAPI\Form $form) +{ + return $form->getField('fields'); +} + + + +//############################################################################// +//############################################################################// +//############################################################################// + +function batchoperationsform(FAPI\Form $form, &$form_state) +{ + $step = 0; + $form->setAction($_SERVER['PHP_SELF']); + + $form->addField('progressnum', array( + 'type' => 'value', + 'value' => (isset($form_state['input_form_definition']['fields'][$step]['progressnum']['value']) )? $form_state['input_form_definition']['fields'][$step]['progressnum']['value'] + 20 : 0, + )); + + $fieldset = $form->addField('fieldset', array( + 'type' => 'tag_container', + )); + + if (FAPI\Form::isPartial()) { + $jsondata = json_decode($form_state['input_values']['jsondata']); + $callback = $jsondata->callback; + if (isset($form_state['input_form_definition']['fields'][$step]['progressnum']['value']) && $form_state['input_form_definition']['fields'][$step]['progressnum']['value'] >= 100) { + $fieldset->addMarkup('finito!'); + } else { + if (is_callable($callback)) { + $fieldset->addJs("setTimeout(function(){ \$('#progress','#{$form->getId()}').trigger('click') },1000);"); + } + + $fieldset->addField('progress', array( + 'type' => 'progressbar', + 'default_value' => $form->getField('progressnum')->get_value(), + 'show_label' => true, + 'ajax_url' => $_SERVER['PHP_SELF'], + 'event' => array( + array( + 'event' => 'click', + 'callback' => 'batch_operations_form_callback', + 'target' => 'batchoperationsform', + 'effect' => '', + 'method' => 'replace', + ), + ), + )); + } + } + + // must be outside of the fieldset in order to be processed + $form->addField('file', array( + 'type' => 'file', + 'ajax_url' => $_SERVER['PHP_SELF'], + 'destination' => dirname(__FILE__), + 'event' => array( + array( + 'event' => 'change', + 'callback' => 'batch_operations_form_callback', + 'target' => 'batchoperationsform', + 'effect' => 'fade', + 'method' => 'replace', + ), + ), + )); + +/* $fieldset->addField('submit', array( + 'type' => 'submit', + )); +*/ + return $form; +} + +function batch_operations_form_callback(FAPI\Form $form) +{ + return $form->getField('fieldset'); +} + + +function _batch_get_progress($filename, $offset = 0, $limit = 20) +{ +} + + + + +//############################################################################// +//############################################################################// +//############################################################################// + +function locationsform(FAPI\Form $form, &$form_state) +{ +/* + google.maps.MapTypeId.HYBRID + google.maps.MapTypeId.ROADMAP + google.maps.MapTypeId.SATELLITE + google.maps.MapTypeId.TERRAIN +*/ + + $form->addField('location[test]', array( + 'title' => 'GeoLocation', + 'type' => 'geolocation', + )) + ->addMarkup('
') + ->addField('map', array( + 'title' => 'MapLocation', + 'type' => 'gmaplocation', + 'scrollwheel' => true, + 'zoom' => 15, + 'mapheight' => '400px', + 'default_value' => array( + 'latitude' => 45.434332, + 'longitude' => 12.338440, + ), + 'maptype' => 'google.maps.MapTypeId.TERRAIN', + 'with_current_location' => true, + )) + ->addMarkup('
') + ->addField('decode', array( + 'title' => 'GeoDecode', + 'type' => 'gmaplocation', + 'with_geocode' => true, + 'with_reverse' => true, + 'lat_lon_type' => 'textfield', + 'zoom' => 15, + 'default_value' => array( + 'latitude' => 51.48257659999999, + 'longitude' => -0.0076589, + ), + )) + ->addMarkup('
') + ->addField('decode_nomap', array( + 'title' => 'GeoDecode No Map', + 'type' => 'gmaplocation', + 'with_geocode' => true, + 'with_map' => false, + 'with_reverse' => true, + 'with_current_location' => true, + 'lat_lon_type' => 'textfield', + 'default_value' => array( + 'latitude' => 51.48257659999999, + 'longitude' => -0.0076589, + ), + )) + ->addMarkup('
') + ->addField('leafletmap', array( + 'title' => 'LeafletLocation', + 'type' => 'leafletlocation', + 'scrollwheel' => true, + 'zoom' => 15, + 'mapheight' => '400px', + 'default_value' => array( + 'latitude' => 45.434332, + 'longitude' => 12.338440, + ), + 'maptype' => 'mapbox.light', + 'accessToken' => MAPBOX_API_KEY, + 'lat_lon_type' => 'textfield', + )) + ->addField('submit', array( + 'prefix' => '

', + 'type' => 'submit', + )); + + return $form; +} + + + +//############################################################################// +//############################################################################// +//############################################################################// + + +function repeatableform(FAPI\Form $form, &$form_state) +{ + $form->setInlineErrors(true); //->set_on_dialog(TRUE); + + $form + ->addField('rep', array( + 'type' => 'repeatable', + 'title' => 'Emails', + 'default_value' => [ + ['name' => 'name1', 'email' => 'email1@email.domain'], + ['name' => 'name2', 'email' => 'email2@email.domain'], + ['name' => 'name3', 'email' => 'email3@email.domain'], + ], + )) + ->addField('name', array( + 'type' => 'textfield', + 'validate' => array('required'), + 'preprocess' => array('trim'), + 'title' => 'Your name', + )) + ->addField('email', array( + 'type' => 'textfield', + 'validate' => array('required', 'email'), + 'title' => 'Your email address', + )) + ; + + + $form + ->addMarkup('
') + ->addField('submit', array( + 'type' => 'submit', + )); + + return $form; +} + + + +//############################################################################// +//############################################################################// +//############################################################################// + + +function bulkform(FAPI\Form $form, &$form_state) +{ + $bulk = $form->addField('bulk', array( + 'type' => 'bulk_table', + )); + $bulk->setTableHeader(array( + 'text', + 'number' + )); + $bulk->addOperation('dump', 'dump', 'var_dump'); + $bulk->addOperation('print', 'print', 'printf'); + + for ($i = 0; $i < 4; $i++) { + $bulk->addRow()->addField('text_'.$i, array( + 'type' => 'textfield', + 'default_value' => 'textfield_'.$i, + ), $i) + ->addField('number_'.$i, array( + 'type' => 'number', + 'default_value' => ''.$i, + ), $i); + } + + $form->addField('submit', array( + 'type' => 'submit', + )); + + return $form; +} diff --git a/examples/header.php b/examples/header.php new file mode 100644 index 00000000..3a12f92f --- /dev/null +++ b/examples/header.php @@ -0,0 +1,235 @@ +clear(); +// session_destroy(); +// session_start(); +} +?> + + + + + + + + + + + + + + + + + diff --git a/examples/igorescobar b/examples/igorescobar new file mode 120000 index 00000000..c38fd34e --- /dev/null +++ b/examples/igorescobar @@ -0,0 +1 @@ +../vendor/igorescobar \ No newline at end of file diff --git a/examples/index.php b/examples/index.php new file mode 100644 index 00000000..29c23bb1 --- /dev/null +++ b/examples/index.php @@ -0,0 +1,89 @@ + + + + + + +

php-forms-api

+
    +'.$dirent.''; + } +}*/ +$ignored_files = array( + basename(__FILE__), + 'forms.php', + 'header.php', + 'footer.php', + 'file_plupload.php', + 'recaptchalib.php', + 'ajax_url.php', +); +foreach (glob('*.php') as $dirent) { + if ($dirent[0] == '.' || !preg_match("/.*?\.(html?|php)$/i", $dirent)) { + continue; + } + if (in_array($dirent, $ignored_files)) { + continue; + } + echo '
  • '.$dirent.'
  • '; +} + +?> +
+ + diff --git a/examples/locations.php b/examples/locations.php new file mode 100644 index 00000000..0f8a64d3 --- /dev/null +++ b/examples/locations.php @@ -0,0 +1,68 @@ +'); +} +if (!defined('MAPBOX_API_KEY')) { + define('MAPBOX_API_KEY', ''); +} + +// if sessions are enabled then the form uses a token for extra security against CSRF +require_once '../vendor/autoload.php'; +include_once "forms.php"; +use Degami\PHPFormsApi as FAPI; + +session_start(); + +// Submit function to call when the form is submitted and passes validation. +// This is where you would send the email (using PHP mail function) +// as this is not a real example I'm just outputting the values for now. +function locationsform_submit(&$form) +{ + $form_values = $form->getValues(); + return $form_values; + //var_dump($form->get_triggering_element()); + // Reset the form if you want it to display again. + // $form->reset(); +} + +$form = FAPI\FormBuilder::getForm('locationsform'); +?> + + + + Example locations form + + + + + + + + + + +

Locations Form

+
+ To list | + Go back +
+
+
process(); ?>
+ isSubmitted()) : ?> + +
getSubmitResults());?>
+

Thanks for submitting the form.

+ + + + + +
+ + diff --git a/examples/multistep.php b/examples/multistep.php new file mode 100644 index 00000000..f6f441aa --- /dev/null +++ b/examples/multistep.php @@ -0,0 +1,61 @@ +getValues()->toArray(); + // print_r($form); + // get submission triggering element + //var_dump($form->get_triggering_element()); + return $form_values; + // Reset the form if you want it to display again. + // $form->reset(); +} + +$form = FAPI\FormBuilder::getForm('multistepform'); +?> + + + + Example multistep form + + + + + + + + +

Example Multistep Form

+
+ To list | + Go back +
+
+
process(); ?>
+ isSubmitted()) : ?> + +

Thanks for submitting the form.

+
getSubmitResults());?>
+ + render(); ?> + + + +
+ + diff --git a/examples/nestables.php b/examples/nestables.php new file mode 100644 index 00000000..a5570097 --- /dev/null +++ b/examples/nestables.php @@ -0,0 +1,70 @@ + 0 && (strlen("".$string)%$length) == 0) ? true : '%t length must be multiple of '.$length; +} + + +function nestableform_submit(&$form) +{ + $form_values = $form->getValues(); + // print_r($form); + // get submission triggering element + + // var_dump($form->get_triggering_element()); + return $form_values; + // Reset the form if you want it to display again. + // $form->reset(); +} + +$form = FAPI\FormBuilder::getForm('nestableform'); + +?> + + + + Example Nestable form + + + + + + + + + + +

Example Nestable form

+
+ To list | + Go back +
+
+
process(); ?>
+ isSubmitted()) : ?> + +

Thanks for submitting the form.

+
getSubmitResults());?>
+ + render(); ?> + + + +
+ + diff --git a/examples/object_form.php b/examples/object_form.php new file mode 100644 index 00000000..e5be6dff --- /dev/null +++ b/examples/object_form.php @@ -0,0 +1,65 @@ +getValues()->toArray(); + $form->addHighlight('Object submitted.'); + return $form_values; +} + +class MyClass +{ + + public $id; + public $name; + public $surname; + public $birthday; + public $number; + + public function __construct($name, $surname, $birthday, $number) + { + $this->id = 1; + $this->name = $name; + $this->surname = $surname; + $this->birthday = $birthday; + $this->number = $number; + } +} + +$classObject = new MyClass('Mirko', 'De Grandis', new \DateTime('1980-01-12'), 1); +$form = FAPI\FormBuilder::objectForm($classObject); + +?> + + + + Example object form + + + + +

Example objectForm

+
+ To list | + Go back +
+
+
process(); ?>
+ isSubmitted()) : ?> + +

Thanks for submitting the form.

+
getSubmitResults());?>
+ + render(); ?> + + + +
+ + diff --git a/examples/plupload.php b/examples/plupload.php new file mode 100644 index 00000000..1afb0932 --- /dev/null +++ b/examples/plupload.php @@ -0,0 +1,55 @@ +getValues(); + if (is_array($form_values['files_upload']) && count($form_values['files_upload'])>0) { + print $value->temppath . " => ".getcwd() . DIRECTORY_SEPARATOR . $value->name."\n"; + rename($value->temppath, getcwd() . DIRECTORY_SEPARATOR . $value->name); + } +} + +$form = FAPI\FormBuilder::getForm('pluploadform'); + + +?> + + + + PLUpload Form + + + + + + + + + +

PLUpload Form

+
+ To list | + Go back +
+
+
process(); ?>
+ isSubmitted()) : ?> + +

Thanks for submitting the form.

+ + render(); ?> + + + +
+ + diff --git a/examples/recaptchalib.php b/examples/recaptchalib.php new file mode 100644 index 00000000..5a65288b --- /dev/null +++ b/examples/recaptchalib.php @@ -0,0 +1,285 @@ + $value) { + $req .= $key . '=' . urlencode(stripslashes($value)) . '&'; + } + + // Cut the last '&' + $req=substr($req, 0, strlen($req)-1); + return $req; +} + + + +/** + * Submits an HTTP POST to a reCAPTCHA server + * @param string $host + * @param string $path + * @param array $data + * @param int port + * @return array response + */ +function _recaptcha_http_post($host, $path, $data, $port = 80) +{ + + $req = _recaptcha_qsencode($data); + + $http_request = "POST $path HTTP/1.0\r\n"; + $http_request .= "Host: $host\r\n"; + $http_request .= "Content-Type: application/x-www-form-urlencoded;\r\n"; + $http_request .= "Content-Length: " . strlen($req) . "\r\n"; + $http_request .= "User-Agent: reCAPTCHA/PHP\r\n"; + $http_request .= "\r\n"; + $http_request .= $req; + + $response = ''; + if (false == ( $fs = @fsockopen($host, $port, $errno, $errstr, 10) )) { + die('Could not open socket'); + } + + fwrite($fs, $http_request); + + while (!feof($fs)) { + $response .= fgets($fs, 1160); // One TCP-IP packet + } + fclose($fs); + $response = explode("\r\n\r\n", $response, 2); + + return $response; +} + + + +/** + * Gets the challenge HTML (javascript and non-javascript version). + * This is called from the browser, and the resulting reCAPTCHA HTML widget + * is embedded within the HTML form it was called from. + * @param string $pubkey A public key for reCAPTCHA + * @param string $error The error given by reCAPTCHA (optional, default is null) + * @param boolean $use_ssl Should the request be made over ssl? (optional, default is false) + + * @return string - The HTML to be embedded in the user's form. + */ +function recaptcha_get_html($pubkey, $error = null, $use_ssl = false) +{ + if ($pubkey == null || $pubkey == '') { + die("To use reCAPTCHA you must get an API key from https://www.google.com/recaptcha/admin/create"); + } + + if ($use_ssl) { + $server = RECAPTCHA_API_SECURE_SERVER; + } else { + $server = RECAPTCHA_API_SERVER; + } + + $errorpart = ""; + if ($error) { + $errorpart = "&error=" . $error; + } + return ' + + '; +} + + + + +/** + * A ReCaptchaResponse is returned from recaptcha_check_answer() + */ +class ReCaptchaResponse +{ + var $is_valid; + var $error; +} + + +/** + * Calls an HTTP POST function to verify if the user's guess was correct + * @param string $privkey + * @param string $remoteip + * @param string $challenge + * @param string $response + * @param array $extra_params an array of extra variables to post to the server + * @return ReCaptchaResponse + */ +function recaptcha_check_answer($privkey, $remoteip, $challenge, $response, $extra_params = array()) +{ + if ($privkey == null || $privkey == '') { + die("To use reCAPTCHA you must get an API key from https://www.google.com/recaptcha/admin/create"); + } + + if ($remoteip == null || $remoteip == '') { + die("For security reasons, you must pass the remote ip to reCAPTCHA"); + } + + + + //discard spam submissions + if ($challenge == null || strlen($challenge) == 0 || $response == null || strlen($response) == 0) { + $recaptcha_response = new ReCaptchaResponse(); + $recaptcha_response->is_valid = false; + $recaptcha_response->error = 'incorrect-captcha-sol'; + return $recaptcha_response; + } + + $response = _recaptcha_http_post( + RECAPTCHA_VERIFY_SERVER, + "/recaptcha/api/verify", + array ( + 'privatekey' => $privkey, + 'remoteip' => $remoteip, + 'challenge' => $challenge, + 'response' => $response + ) + $extra_params + ); + + $answers = explode("\n", $response [1]); + $recaptcha_response = new ReCaptchaResponse(); + + if (trim($answers [0]) == 'true') { + $recaptcha_response->is_valid = true; + } else { + $recaptcha_response->is_valid = false; + $recaptcha_response->error = $answers [1]; + } + return $recaptcha_response; +} + +/** + * gets a URL where the user can sign up for reCAPTCHA. If your application + * has a configuration page where you enter a key, you should provide a link + * using this function. + * @param string $domain The domain where the page is hosted + * @param string $appname The name of your application + */ +function recaptcha_get_signup_url($domain = null, $appname = null) +{ + return "https://www.google.com/recaptcha/admin/create?" . _recaptcha_qsencode(array ('domains' => $domain, 'app' => $appname)); +} + +function _recaptcha_aes_pad($val) +{ + $block_size = 16; + $numpad = $block_size - (strlen($val) % $block_size); + return str_pad($val, strlen($val) + $numpad, chr($numpad)); +} + +/* Mailhide related code */ + +function _recaptcha_aes_encrypt($val, $ky) +{ + if (! function_exists("mcrypt_encrypt")) { + die("To use reCAPTCHA Mailhide, you need to have the mcrypt php module installed."); + } + $mode=MCRYPT_MODE_CBC; + $enc=MCRYPT_RIJNDAEL_128; + $val=_recaptcha_aes_pad($val); + return mcrypt_encrypt($enc, $ky, $val, $mode, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"); +} + + +function _recaptcha_mailhide_urlbase64($x) +{ + return strtr(base64_encode($x), '+/', '-_'); +} + +/* gets the reCAPTCHA Mailhide url for a given email, public key and private key */ +function recaptcha_mailhide_url($pubkey, $privkey, $email) +{ + if ($pubkey == '' || $pubkey == null || $privkey == "" || $privkey == null) { + die("To use reCAPTCHA Mailhide, you have to sign up for a public and private key, " . + "you can do so at http://www.google.com/recaptcha/mailhide/apikey"); + } + + + $ky = pack('H*', $privkey); + $cryptmail = _recaptcha_aes_encrypt($email, $ky); + + return "http://www.google.com/recaptcha/mailhide/d?k=" . $pubkey . "&c=" . _recaptcha_mailhide_urlbase64($cryptmail); +} + +/** + * gets the parts of the email to expose to the user. + * eg, given johndoe@example,com return ["john", "example.com"]. + * the email is then displayed as john...@example.com + */ +function _recaptcha_mailhide_email_parts($email) +{ + $arr = preg_split("/@/", $email); + + if (strlen($arr[0]) <= 4) { + $arr[0] = substr($arr[0], 0, 1); + } elseif (strlen($arr[0]) <= 6) { + $arr[0] = substr($arr[0], 0, 3); + } else { + $arr[0] = substr($arr[0], 0, 4); + } + return $arr; +} + +/** + * Gets html to display an email address given a public an private key. + * to get a key, go to: + * + * http://www.google.com/recaptcha/mailhide/apikey + */ +function recaptcha_mailhide_html($pubkey, $privkey, $email) +{ + $emailparts = _recaptcha_mailhide_email_parts($email); + $url = recaptcha_mailhide_url($pubkey, $privkey, $email); + + return htmlentities($emailparts[0]) . "...@" . htmlentities($emailparts [1]); +} diff --git a/examples/repeatable.php b/examples/repeatable.php new file mode 100644 index 00000000..139c5802 --- /dev/null +++ b/examples/repeatable.php @@ -0,0 +1,62 @@ +getValues()->toArray(); + return $form_values; +} + +// function my_contactform_form_alter($form){ +// $form->get_field('fieldset')->remove_field('message'); +// } + +$form = FAPI\FormBuilder::getForm('repeatableform'); +?> + + + + Example contact form + + + + + + + +

Example Form

+
+ To list | + Go back +
+
+
process(); ?>
+ isSubmitted()) : ?> + +

Thanks for submitting the form.

+
getSubmitResults());?>
+
getValues());?>
+ + render(); ?> + + + +
+ + diff --git a/examples/showall.php b/examples/showall.php new file mode 100644 index 00000000..410991f8 --- /dev/null +++ b/examples/showall.php @@ -0,0 +1,83 @@ + 0 && (strlen("".$string)%$length) == 0) ? true : '%t length must be multiple of '.$length; +} + + +function showall_submit(&$form) +{ + $form_values = serialize($form->getValues()); + // print_r($form); + // get submission triggering element + + // var_dump($form->get_triggering_element()); + return $form_values; + // Reset the form if you want it to display again. + // $form->reset(); +} + +function showall_validate(&$form) +{ + $form_values = $form->getValues(); + if ($form_values['fieldset']['name'] == 'aaa' && $form_values['tabs']['slider']==2) { + return "You shall not pass!!!"; + } + + return true; +} +$form = FAPI\FormBuilder::getForm('showallform'); + +?> + + + + Example Show them all form + + + + + + + + + + + + + + +

Example Show them all form

+
+ To list | + Go back +
+
+
process(); ?>
+ isSubmitted()) : ?> + +

Thanks for submitting the form.

+
getSubmitResults());?>
+ + render(); ?> + + + +
+ + diff --git a/src/classes/Form.php b/src/classes/Form.php new file mode 100644 index 00000000..79b26769 --- /dev/null +++ b/src/classes/Form.php @@ -0,0 +1,1579 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FORM #### + ######################################################### */ + +namespace Degami\PHPFormsApi; + +use Degami\Basics\DataBag; +use \Exception; +use Degami\Basics\Traits\ToolsTrait as BasicToolsTrait; +use Degami\PHPFormsApi\Traits\Tools; +use Degami\PHPFormsApi\Traits\Processors; +use Degami\PHPFormsApi\Traits\Validators; +use Degami\PHPFormsApi\Traits\Containers; +use Degami\PHPFormsApi\Abstracts\Base\Element; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\PHPFormsApi\Abstracts\Base\FieldsContainer; +use Degami\PHPFormsApi\Accessories\OrderedFunctions; +use Degami\PHPFormsApi\Accessories\FormValues; +use Degami\PHPFormsApi\Fields\Datetime; +use Degami\PHPFormsApi\Fields\Geolocation; +use Degami\PHPFormsApi\Fields\Checkbox; +use Degami\PHPFormsApi\Fields\Radios; +use Degami\PHPFormsApi\Fields\Select; +use Degami\PHPFormsApi\Abstracts\Fields\FieldMultivalues; +use Degami\PHPFormsApi\Accessories\SessionBag; +use Degami\PHPFormsApi\Fields\Hidden; + +/** + * The form object class + */ +class Form extends Element +{ + use BasicToolsTrait, Tools, Validators, Processors, Containers ; + + /** + * form id + * + * @var string + */ + protected $form_id = 'cs_form'; + + /** + * form definition function name + * + * @var string + */ + protected $definition_function = ''; + + + /** + * form token + * + * @var string + */ + protected $form_token = ''; + + /** + * form action + * + * @var string + */ + protected $action = ''; + + /** + * form method + * + * @var string + */ + protected $method = 'post'; + + /** + * "form is already processed" flag + * + * @var boolean + */ + protected $processed = false; + + /** + * "form is already validated" flag + * + * @var boolean + */ + protected $validated = false; + + /** + * "form is already submitted" flag + * + * @var boolean + */ + protected $submitted = false; + + /** + * "form is valid" flag + * + * @var null + */ + protected $valid = null; + + /** + * validate functions list + * + * @var array + */ + protected $validate = []; + + /** + * submit functions list + * + * @var array + */ + protected $submit = []; + + /** + * form output type (html/json) + * + * @var string + */ + protected $output_type = 'html'; + + /** + * show inline errors + * + * @var boolean + */ + protected $inline_errors = false; + + /** + * "form already pre-rendered" flag + * + * @var boolean + */ + protected $pre_rendered = false; + + /** + * "js was aleready generated" flag + * + * @var boolean + */ + protected $js_generated = false; + + /** + * ajax submit url + * + * @var string + */ + protected $ajax_submit_url = ''; + + /** + * print form on a dialog + * + * @var boolean + */ + protected $on_dialog = false; + + /** + * current step + * + * @var integer + */ + private $current_step = 0; + + /** + * array of submit functions results + * + * @var array + */ + private $submit_functions_results = []; + + /** + * "do not process form token" flag + * + * @var boolean + */ + private $no_token = false; + + /** + * Session Bag Object + * + * @var SessionBag + */ + private $session_bag = null; + + /** + * form state array + * + * @var array + */ + private $form_state = []; + + public $allocatedSize = 0; + + /** + * Class constructor + * + * @param array $options build options + */ + public function __construct($options = []) + { + parent::__construct(); + + $this->build_options = $options; + $this->session_bag = new SessionBag(); + $this->container_tag = FORMS_DEFAULT_FORM_CONTAINER_TAG; + $this->container_class = FORMS_DEFAULT_FORM_CONTAINER_CLASS; + + $this->setClassProperties($options); + + $has_submitter = false; + foreach ($this->submit as $s) { + if (!empty($s) && is_callable($s)) { + $has_submitter = true; + } + } + if (!$has_submitter) { + array_push($this->submit, "{$this->form_id}_submit"); + } + + // if (empty($this->submit) || !is_callable($this->submit)) { + // array_push($this->submit, "{$this->form_id}_submit"); + // } + + $has_validator = false; + foreach ($this->validate as $v) { + if (!empty($v) && is_callable($v)) { + $has_validator = true; + } + } + if (!$has_validator) { + array_push($this->validate, "{$this->form_id}_validate"); + } + + // if (empty($this->validate) || !is_callable($this->validate)) { + // array_push($this->validate, "{$this->form_id}_validate"); + // } + + if (!$this->validate instanceof OrderedFunctions) { + $this->validate = new OrderedFunctions($this->validate, 'validator'); + } + + if (!$this->submit instanceof OrderedFunctions) { + $this->submit = new OrderedFunctions($this->submit, 'submitter'); + } + + $has_session = FormBuilder::sessionPresent(); + if ($has_session) { + $this->form_token = sha1(mt_rand(0, 1000000)); + $this->getSessionBag()->ensurePath("/form_token"); + $this->getSessionBag()->form_token->{$this->form_token} = $_SERVER['REQUEST_TIME']; + } + } + + /** + * Get Session Bag + * + * @return SessionBag + */ + public function getSessionBag(): SessionBag + { + return $this->session_bag; + } + + /** + * Set form id + * + * @param string $form_id set the form id used for getting the submit function name + * @return self + */ + public function setFormId(string $form_id): Form + { + $this->form_id = $form_id; + return $this; + } + + /** + * Get the form id + * + * @return string form id + */ + public function getFormId(): string + { + return $this->form_id; + } + + + /** + * Set the form action attribute + * + * @param string $action the form action url + * @return self + */ + public function setAction(string $action): Form + { + $this->action = $action; + return $this; + } + + /** + * Get the form action url + * + * @return string the form action + */ + public function getAction(): string + { + return $this->action; + } + + /** + * Set the form method + * + * @param string $method form method + * @return self + */ + public function setMethod(string $method): Form + { + $this->method = strtolower(trim($method)); + return $this; + } + + /** + * Get the form method + * + * @return string form method + */ + public function getMethod(): string + { + return $this->method; + } + + /** + * Set the ajax submit url used for form submission + * + * @param string $ajax_submit_url ajax endpoint url + * @return self + */ + public function setAjaxSubmitUrl(string $ajax_submit_url): Form + { + $this->ajax_submit_url = $ajax_submit_url; + return $this; + } + + /** + * Get the ajax form submission url + * + * @return string the form ajax submission url + */ + public function getAjaxSubmitUrl(): string + { + return $this->ajax_submit_url; + } + + /** + * Set the form render output type + * + * @param string $output_type output type ( 'html' / 'json' ) + * @return Form + */ + public function setOutputType(string $output_type): Form + { + $this->output_type = $output_type; + return $this; + } + + /** + * Get the form render output type + * + * @return string form output type + */ + public function getOutputType(): string + { + return $this->output_type; + } + + + /** + * Set no_token flag + * + * @param boolean $no_token no token flag + * @return Form + */ + public function setNoToken(bool $no_token): Form + { + $this->no_token = $no_token; + return $this; + } + + /** + * Get no_token flag + * + * @return boolean no token flag + */ + public function getNoToken(): bool + { + return $this->no_token; + } + + + /** + * Set the form on_dialog preference + * + * @param string $on_dialog the form on_dialog preference + * @return Form + */ + public function setOnDialog(string $on_dialog): Form + { + $this->on_dialog = $on_dialog; + return $this; + } + + /** + * Get the form on_dialog preference + * + * @return string the form on_dialog preference + */ + public function getOnDialog() + { + return $this->on_dialog; + } + + /** + * Get the form token + * + * @return string the form token used in form validation and submission process + */ + public function getFormToken(): string + { + return $this->form_token; + } + + /** + * Return form elements (all the steps) values + * + * @return FormValues form values + */ + public function getValues(): FormValues + { + // Warning: some messy logic in calling process->submit->values + if (!$this->processed) { + $this->processValue(); + } + $output = []; + for ($step = 0; $step <= $this->getNumSteps(); $step++) { + foreach ($this->getFields($step) as $name => $field) { + if ($field->isAValue() == true) { + $output[$name] = $field->getValues(); + if (is_array($output[$name]) && empty($output[$name])) { + unset($output[$name]); + } + } + } + } + + return new FormValues($output); + } + + /** + * Return form elements (all the steps) values + * + * @return FormValues form values + */ + public function values(): FormValues + { + return $this->getValues(); + } + + /** + * Get current step elements values + * + * @return array step values + */ + private function getCurrentStepValues(): array + { + $output = []; + foreach ($this->getFields($this->current_step) as $name => $field) { + if ($field->isAValue() == true) { + $output[$name] = $field->getValues(); + if (is_array($output[$name]) && empty($output[$name])) { + unset($output[$name]); + } + } + } + return $output; + } + + /** + * resets the form + * + * @return self + */ + public function resetField(): Form + { + foreach ($this->getFields() as $name => $field) { + $field->resetField(); + if (strtolower($this->method) == 'post') { + unset($_POST[$name]); + } else { + unset($_GET[$name]); + } + unset($_REQUEST[$name]); + } + + if (strtolower($this->method) == 'post') { + unset($_POST['form_id']); + unset($_POST['form_token']); + } else { + unset($_GET['form_id']); + unset($_GET['form_token']); + } + unset($_REQUEST['form_id']); + unset($_REQUEST['form_token']); + + if (isset($this->getSessionBag()->{$this->form_id})) { + unset($this->getSessionBag()->{$this->form_id}); + } + + if (isset($this->getSessionBag()->form_definition[$this->form_id])) { + unset($this->getSessionBag()->form_definition[$this->form_id]); + } + + $this->processed = false; + $this->validated = false; + $this->submitted = false; + $this->js_generated = false; + $this->setErrors([]); + $this->valid = null; + $this->current_step = 0; + $this->submit_functions_results = []; + + return $this; + } + + /** + * resets the form + * + * @return self + */ + public function reset(): Form + { + return $this->resetField(); + } + + /** + * Check if form is submitted + * + * @return boolean form is submitted + */ + public function isSubmitted(): bool + { + return $this->submitted; + } + + + /** + * Check if form is processed + * + * @return boolean form is processed + */ + public function isProcessed(): bool + { + return $this->processed; + } + + + /** + * Get the form submit results optionally by submit function name + * + * @param string $submit_function submit function name + * @return mixed function(s) return value or function(s) data sent to stdout if not returning anything + */ + public function getSubmitResults($submit_function = '') + { + if (!$this->isSubmitted()) { + return false; + } + if (!empty($submit_function)) { + if (!in_array($submit_function, array_keys($this->submit_functions_results))) { + return false; + } + return $this->submit_functions_results[$submit_function]; + } + return $this->submit_functions_results; + } + + /** + * {@inheritdocs} + * + * @param array $request request array + */ + private function alterRequest(array &$request) + { + foreach ($this->getFields($this->current_step) as $field) { + $field->alterRequest($request); + } + } + + /** + * copies the request values into the right form element + * + * @param array|DataBag $request request array + * @param int $step step number + */ + private function injectValues($request, int $step) + { + foreach ($this->getFields($step) as $name => $field) { + if ($field instanceof FieldsContainer) { + $field->processValue($request); + } elseif (($requestValue = static::traverseArray($request, $field->getName())) != null) { + $field->processValue($requestValue); + } elseif ($field instanceof Checkbox || $field instanceof Radios) { + // no value on request[name] && field is a checkbox or radios group - process anyway with an empty value + $field->processValue(null); + } elseif ($field instanceof Select) { + if ($field->isMultiple()) { + $field->processValue([]); + } else { + $field->processValue(null); + } + } elseif ($field instanceof FieldMultivalues) { + // no value on request[name] && field is a multivalue (eg. checkboxes ?) + // process anyway with an empty value + $field->processValue([]); + } + } + } + + /** + * save current step request array in session + * + * @param array $request request array + */ + private function saveStepRequest(array $request) + { + $files = $this->getStepFieldsByTypeAndName('file', null, $this->current_step); + if (!empty($files)) { + foreach ($files as $filefield) { + $request[$filefield->getName()] = $filefield->getValues(); + $request[$filefield->getName()]['uploaded'] = $filefield->isUploaded(); + } + } + + $recaptchas = $this->getStepFieldsByTypeAndName('recaptcha', null, $this->current_step); + if (!empty($recaptchas)) { + foreach ($recaptchas as $recaptchafield) { + $request[$recaptchafield->getName()] = $recaptchafield->getValues(); + $request[$recaptchafield->getName()]['already_validated'] = $recaptchafield->isAlreadyValidated(); + } + } + + $has_session = FormBuilder::sessionPresent(); + if ($has_session) { + $this->getSessionBag()->ensurePath("/{$this->form_id}/steps"); + $this->getSessionBag()->{$this->form_id}->steps->add( + [ + $this->current_step => $request + ] + ); + } + } + + /** + * save form_state array into form object, for use into process function + * + * @param array &$form_state [description] + * @return self + */ + public function setFormState(&$form_state = []): Form + { + $this->form_state = $form_state; + return $this; + } + + /** + * To array override + * + * @return array array representation for the element properties + */ + public function toArray(): array + { + $values = parent::toArray(); + unset($values['form_state']); + return $values; + } + + /** + * starts the form processing, validating and submitting + * + * @param array $values the request values array + */ + public function processValue($values = []) + { + $has_session = FormBuilder::sessionPresent(); + if ($has_session) { + $this->getSessionBag()->ensurePath("/form_token"); + foreach ($this->getSessionBag()->form_token as $key => $time) { + if ($time < ($_SERVER['REQUEST_TIME'] - FORMS_SESSION_TIMEOUT)) { + unset($this->getSessionBag()->form_token[$key]); + } + } + } + + // let others alter the form + $defined_functions = get_defined_functions(); + foreach ($defined_functions['user'] as $function_name) { + if (preg_match("/.*?_{$this->form_id}_form_alter$/i", $function_name)) { + call_user_func_array($function_name, [ &$this ]); + } + } + + $request = null; + if (!$this->processed) { //&& !form::is_partial() + if (empty($values)) { + $request = (strtolower($this->method) == 'post') ? $_POST : $_GET; + } else { + $request = $values; + } + + // recursive url_decode request elements + array_walk_recursive($request, function (&$item, $key) { + if (is_scalar($item)) { + $item = urldecode($item); + } + }); + + //alter request if needed + $this->alterRequest($request); + + if (isset($request['form_id']) && $request['form_id'] == $this->form_id) { + if (isset($request['current_step'])) { + $this->current_step = $request['current_step']; + } + // insert values into fields + for ($step = 0; $step < $this->current_step; $step++) { + if ($has_session && isset($this->getSessionBag()->{$this->form_id}->steps['_value'.$step])) { + $this->injectValues($this->getSessionBag()->{$this->form_id}->steps['_value'.$step], $step); + } + } + + $this->injectValues($request, $this->current_step); + + if (!$this->isFinalStep()) { + $this->saveStepRequest($request); + } + + $this->processed = true; + } + } + + if ($this->processed == true) { + for ($step = 0; $step <= $this->current_step; $step++) { + foreach ($this->getFields($step) as $name => $field) { + $field->preProcess(); + } + } + if (!Form::isPartial() && !$this->submitted && $this->isValid() && $this->isFinalStep()) { + $this->submitted = true; + + if ($has_session && isset($this->getSessionBag()->{$this->form_id})) { + unset($this->getSessionBag()->{$this->form_id}); + } + + for ($step = 0; $step < $this->getNumSteps(); $step++) { + foreach ($this->getFields($step) as $name => $field) { + $field->postProcess(); + } + } + + foreach ($this->submit as $submit_function) { + if (is_callable($submit_function)) { + if (!is_array($this->submit_functions_results)) { + $this->submit_functions_results = []; + } + $submitresult = ''; + ob_start(); + $submitresult = call_user_func_array($submit_function, [ &$this, &$this->form_state, $request ]); + if ($submitresult == null) { + $submitresult = ob_get_contents(); + } + ob_end_clean(); + $deffunctionname = FormBuilder::getDefinitionFunctionName($submit_function); + $this->submit_functions_results[$deffunctionname] = $submitresult; + } + } + } + } + } + + /** + * starts the form processing, validating and submitting + * + * @param array $values the request values array + */ + public function process($values = []) + { + $this->processValue($values); + } + + /** + * Check if form is valid / NULL if form is on the first render + * + * @return boolean form is valid + */ + public function isValid(): ?bool + { + if ($this->validated) { + return $this->valid; + } + if (!isset($_REQUEST['form_id'])) { + return null; + } elseif ($_REQUEST['form_id'] == $this->form_id) { + $has_session = FormBuilder::sessionPresent(); + if ($this->valid == null) { + $this->valid = true; + } + if ($has_session && !$this->no_token) { + $this->valid = false; + $this->addError($this->getText('Form is invalid or has expired'), __FUNCTION__); + if (isset($_REQUEST['form_token']) + && isset($this->getSessionBag()->form_token->{$_REQUEST['form_token']}) + ) { + if ($this->getSessionBag()->form_token->{$_REQUEST['form_token']} >= + ($_SERVER['REQUEST_TIME'] - FORMS_SESSION_TIMEOUT) + ) { + $this->valid = true; + $this->setErrors([]); + if (!Form::isPartial()) { + unset($this->getSessionBag()->form_token->{$_REQUEST['form_token']}); + } + } + } + } + for ($step = 0; $step <= $this->current_step; $step++) { + foreach ($this->getFields($step) as $field) { + if (!$field->isValid()) { + $this->valid = false; + } + } + } + + if ($this->valid) { + foreach ($this->getFields($this->current_step) as $field) { + $field->afterValidate($this); + } + $this->current_step++; + } + + if ($this->isFinalStep()) { + foreach ($this->validate as $validate_function) { + if (is_callable($validate_function)) { + $error = call_user_func_array($validate_function, [ + &$this, + &$this->form_state, + (strtolower($this->method) == 'post') ? $_POST : $_GET + ]); + if ($error !== true) { + $this->valid = false; + $this->addError( + is_string($error) ? $this->getText($error) : $this->getText('Error. Form is not valid'), + FormBuilder::getCallablaStringName($validate_function) + ); + } + } + } + } + + if (!$this->valid) { + $this->current_step--; + } + if ($this->current_step < 0) { + $this->current_step = 0; + } + + $this->validated = true; + return $this->valid; + } + return null; + } + + /** + * Add field to form + * + * @param string $name field name + * @param mixed $field field to add, can be an array or a field subclass + * @param int $step step to add the field to + * @return mixed + * @throws Exceptions\FormException + */ + public function addField(string $name, $field, int $step = 0) + { + $field = $this->getFieldObj($name, $field); + $field->setParent($this); + + $this->setField($name, $field, $step); + $this->insert_field_order[] = $name; + + if (!method_exists($field, 'onAddReturn')) { + if ($this->isFieldContainer($field)) { + return $field; + } + return $this; + } + if ($field->onAddReturn() == 'this') { + return $field; + } + return $this; + } + + /** + * remove field from form + * + * @param string $name field name + * @param int $step field step + * @return self + */ + public function removeField(string $name, int $step = 0): Form + { + unset($this->fields[$step][$name]); + if (($key = array_search($name, $this->insert_field_order)) !== false) { + unset($this->insert_field_order[$key]); + } + return $this; + } + + /** + * Get the number of form steps + * + * @return int steps number + */ + private function getNumSteps(): int + { + return count($this->fields); + } + + /** + * Check if current is the final step + * + * @return boolean this is the final step + */ + private function isFinalStep(): bool + { + return ($this->getCurrentStep() >= $this->getNumSteps()); + } + + /** + * Check if this request is a "partial" ( used in elements ajax requests ) + * + * @return boolean [description] + */ + public static function isPartial(): bool + { + return (isset($_REQUEST['partial']) && $_REQUEST['partial'] == 'true'); + } + + /** + * Get the fields array by reference + * + * @param int $step step number + * @return array the array of elements for the step specified + */ + public function &getFields(int $step = 0): array + { + $notfound = []; + if (!isset($this->fields[$step])) { + return $notfound; + } + return $this->fields[$step]; + } + + /** + * Get the step fields by type and name + * + * @param array|string $field_types field types + * @param ?string $name field name + * @param int $step step number + * @return array the array of fields matching the search criteria + */ + private function getStepFieldsByTypeAndName($field_types, ?string $name = null, int $step = 0): array + { + if (!is_array($field_types)) { + $field_types = [$field_types]; + } + $out = []; + foreach ($this->getFields($step) as $field) { + if ($field instanceof FieldsContainer) { + if ($name != null) { + $out = array_merge($out, $field->getFieldsByTypeAndName($field_types, $name)); + } else { + $out = array_merge($out, $field->getFieldsByType($field_types)); + } + } else { + if ($name != null) { + if ($field instanceof Field && in_array($field->getType(), $field_types) + && $field->getName() == $name + ) { + $out[] = $field; + } + } elseif ($field instanceof Field && in_array($field->getType(), $field_types)) { + $out[] = $field; + } + } + } + return $out; + } + + /** + * Get the form fields by type (in all the steps) + * + * @param array $field_types field types + * @return array fields in the form + */ + public function getFieldsByType(array $field_types): array + { + if (!is_array($field_types)) { + $field_types = [$field_types]; + } + $out = []; + + for ($step=0; $step < $this->getNumSteps(); $step++) { + $out = array_merge($out, $this->getStepFieldsByTypeAndName($field_types, null, $step)); + } + return $out; + } + + /** + * Get the step fields by type and name (in all the steps) + * + * @param array $field_types field types + * @param string $name field name + * @return array fields in the form matching the search criteria + */ + public function getFieldsByTypeAndName(array $field_types, string $name): array + { + if (!is_array($field_types)) { + $field_types = [$field_types]; + } + $out = []; + + for ($step=0; $step < $this->getNumSteps(); $step++) { + $out = array_merge($out, $this->getStepFieldsByTypeAndName($field_types, $name, $step)); + } + return $out; + } + + /** + * Get field by name + * + * @param string $field_name field name + * @param int $step step number where to find the field + * + * @return Element subclass field object + */ + public function getField(string $field_name, int $step = 0): ?Element + { + if (isset($this->fields[$step][$field_name])) { + return $this->fields[$step][$field_name]; + } + + $found = array_filter($this->fields[$step], fn ($el) => $el->getName() == $field_name); + + if (!empty($found)) { + return current($found); + } + + return null; + } + + /** + * Set field + * + * @param string $field_name field name + * @param Element $field subclass field object + * @param integer $step step number where to put the field + * @return self + */ + public function setField(string $field_name, Element $field, $step = 0): Form + { + $field->setName($field_name); + $this->fields[$step][$field_name] = $field; + return $this; + } + + /** + * Get the submit element which submitted the form + * + * @return ?Element subclass the submitter + */ + public function getTriggeringElement(): ?Element + { + $fields = $this->getFieldsByType(['submit','button','image_button']); + foreach ($fields as $field) { + if ($field->getClicked() == true) { + return $field; + } + } + + if (Form::isPartial()) { + $triggering_id = $_REQUEST['triggering_element']; + return Element::searchFieldById($this, $triggering_id); + } + + return null; + } + + /** + * Get the form submit + * + * @return OrderedFunctions form submit function(s) + */ + public function getSubmit() + { + return $this->submit; + } + + /** + * Set form submit functions list + * + * @param array|OrderedFunctions $submit set the form submit functions list + * @return self + */ + public function setSubmit($submit): Form + { + if (!($submit instanceof OrderedFunctions)) { + $submit = new OrderedFunctions($submit, 'submitter'); + } + $this->submit = $submit; + return $this; + } + + + /** + * Get the form validate + * + * @return OrderedFunctions form validate function(s) + */ + public function getValidate() + { + return $this->validate; + } + + /** + * Set form validate functions list + * + * @param array|OrderedFunctions $validate set the form validate functions list + * @return self + */ + public function setValidate($validate): Form + { + if (!($validate instanceof OrderedFunctions)) { + $validate = new OrderedFunctions($validate, 'validator'); + } + $this->validate = $validate; + return $this; + } + + + /** + * Get the form id + * + * @return string the form id + */ + public function getId(): string + { + return $this->form_id; + } + + /** + * Get the current step number + * + * @return int current step + */ + public function getCurrentStep(): int + { + return $this->current_step; + } + + /** + * Get ajax url + * + * @return string ajax form submit url + */ + public function getAjaxUrl(): string + { + return $this->ajax_submit_url; + } + + /** + * renders form errors + * + * @return string errors as an html
  • list + */ + public function showErrors(): string + { + return $this->notifications->renderHTML('error'); + } + + /** + * renders form highlights + * + * @return string highlights as an html
  • list + */ + public function showHighlights(): string + { + return $this->notifications->renderHTML('highlight'); + } + + /** + * Sets inline error preference + * + * @param boolean $inline_errors error preference + * @return self + */ + public function setInlineErrors(bool $inline_errors): Form + { + $this->inline_errors = $inline_errors; + + return $this; + } + + /** + * Returns inline error preference + * + * @return boolean errors should be presented inline after every element + */ + public function getInlineErrors(): bool + { + return $this->inline_errors; + } + + /** + * Returns inline error preference + * + * @return boolean errors should be presented inline after every element + */ + public function errorsInline(): bool + { + return $this->getInlineErrors(); + } + + + /** + * {@inheritdocs}. + * using this hook form elements can modify the form element + */ + public function preRender() + { + if ($this->on_dialog == true) { + $this->addJs('$("#'.$this->getFormId().'").dialog()'); + } + + foreach ($this->getFields($this->current_step) as $name => $field) { + if (is_object($field) && method_exists($field, 'preRender')) { + $field->preRender($this); + } + } + $this->pre_rendered = true; + } + + /** + * renders the form + * + * @param ?string $override_output_type output type + * @return string the form html + */ + public function renderHTML(?string $override_output_type = null) + { + $output = ''; + $errors = ''; + $highlights = ''; + $fields_html = ''; + + // render needs the form to be processed + if (!$this->processed) { + $this->processValue(); + } + + if (!is_string($override_output_type)) { + $override_output_type = null; + } + $output_type = !empty($override_output_type) ? $override_output_type : $this->getOutputType(); + $output_type = trim(strtolower($output_type)); + if ($output_type == 'json' && empty($this->ajax_submit_url)) { + $output_type = 'html'; + } + + if (!Form::isPartial() && $this->isValid() === false) { + $errors = $this->showErrors(); + $this->setAttribute('class', trim($this->getAttribute('class').' with-errors')); + if (!$this->errorsInline()) { + foreach ($this->getFields($this->current_step) as $field) { + $errors .= $field->showErrors(); + } + } + if (trim($errors)!='') { + $errors = sprintf(FORMS_ERRORS_TEMPLATE, $errors); + } + } + + if ($this->hasHighlights()) { + $highlights = $this->showHighlights(); + if (trim($highlights)!='') { + $highlights = sprintf(FORMS_HIGHLIGHTS_TEMPLATE, $highlights); + } + } + + $insertorder = array_flip($this->insert_field_order); + $weights = $order = []; + foreach ($this->getFields($this->current_step) as $key => $elem) { + $weights[$key] = $elem->getWeight(); + $order[$key] = isset($insertorder[$key]) ? $insertorder[$key] : PHP_INT_MAX; + } + if (count($this->getFields($this->current_step)) > 0) { + array_multisort($weights, SORT_ASC, $order, SORT_ASC, $this->getFields($this->current_step)); + } + + // hidden fields are always first + usort($this->getFields($this->current_step), function($fieldA, $fieldB){ + if (is_object($fieldA) && is_a($fieldA, Hidden::class)) { + return -1; + } + if (is_object($fieldB) && is_a($fieldB, Hidden::class)) { + return 1; + } + return 0; + }); + + foreach ($this->getFields($this->current_step) as $name => $field) { + if (is_object($field) && method_exists($field, 'renderHTML')) { + $fields_html .= $field->renderHTML($this); + } + } + + $attributes = $this->getAttributes(['action','method','id']); + $js = $this->generateJs(); + + if (Form::isPartial()) { + // ajax request - form item event + + $jsondata = json_decode($_REQUEST['jsondata']); + $callback = unserialize(base64_decode($jsondata->callback)); + if (is_callable($callback)) { + /** + * @var Field $target_elem + */ + $target_elem = call_user_func_array($callback, [$this]); + + $html = $target_elem->renderHTML($this); + + if (count($target_elem->getCss()) > 0) { + $html .= '"; + } + + $js = ''; + if (count($target_elem->getJs()) > 0) { + $js = $this->encapsulateJs($target_elem->getJs()); + } + + return json_encode([ 'html' => $html, 'js' => $js ]); + } + + return false; + } + + if (!empty($this->ajax_submit_url) && $this->getOutputType() == 'json' && $output_type == 'html') { + // print initial js for ajax form + + $initial_js = ["var {$this->getId()}_attachFormBehaviours = function (){ + \$('#{$this->getId()}').submit(function(evt){ + evt.preventDefault(); + \$.post( \"{$this->getAjaxUrl()}\", \$('#{$this->getId()}').serialize(), function( data ) { + var response; + if(typeof data =='object') response = data; + else response = \$.parseJSON(data); + \$('#{$this->getId()}-formcontainer').html(''); + \$(response.html).appendTo( \$('#{$this->getId()}-formcontainer') ); + if( \$.trim(response.js) != '' ){ + eval( response.js ); + }; + {$this->getId()}_attachFormBehaviours(); + }); + return false; + }); + };", + "\$.getJSON('{$this->getAjaxUrl()}',function(response){ + \$(response.html).appendTo( \$('#{$this->getId()}-formcontainer') ); + if( \$.trim(response.js) != '' ){ + eval( response.js ); + }; + {$this->getId()}_attachFormBehaviours(); + });"]; + + $output = "\n". + "
    getId()}-formcontainer\">
    "; + } else { + $form_tag_html = $this->getElementPrefix(); + $form_tag_html .= $this->getPrefix(); + $form_tag_html .= $highlights; + $form_tag_html .= $errors; + $form_tag_html .= "
    action}\" id=\"{$this->form_id}\" "; + $form_tag_html .= "method=\"{$this->method}\"{$attributes}>\n"; + $form_tag_html .= $fields_html; + $form_tag_html .= "form_id}\" />\n"; + if (!$this->no_token) { + $form_tag_html .= "form_token}\" />\n"; + } + if ($this->getNumSteps() > 1) { + $form_tag_html .= "current_step}\" />\n"; + } + $form_tag_html .= "
    \n"; + $form_tag_html .= $this->getSuffix(); + $form_tag_html .= $this->getElementSuffix(); + + switch ($output_type) { + case 'json': + $output = ['html'=>'','js'=>'','is_submitted'=>$this->isSubmitted()]; + + $output['html'] = $form_tag_html; + + if (count($this->getCss())>0) { + $output['html'] .= ""; + } + + if (!empty($js)) { + $output['js'] = $js; + } + + $output = json_encode($output); + break; + + case 'html': + default: + $output = $form_tag_html; + + if (count($this->getCss())>0) { + $output .= ""; + } + + if (!empty($js)) { + $output .= "\n\n"; + } + break; + } + } + return $output; + } + + /** + * renders the form + * + * @param ?string $override_output_type output type + * @return string the form html + */ + public function render(?string $override_output_type = null) + { + return $this->renderHTML($override_output_type); + } + + /** + * generate the js string + * + * @return string the js into a jquery sandbox + */ + public function generateJs(): string + { + if (!$this->pre_rendered) { + $this->preRender(); + } // call all elements pre_render, so they can attach js to the form element; + + $js = array_filter(array_map('trim', $this->getJs())); + if (!empty($js) && !$this->js_generated) { + foreach ($js as &$js_string) { + if ($js_string[strlen($js_string)-1] == ';') { + $js_string = substr($js_string, 0, strlen($js_string)-1); + } + } + + $this->js_generated = true; + return $this->encapsulateJs($js); + } + return ""; + } + + + /** + * toString magic method + * + * @return string the form html + */ + public function __toString() + { + try { + return $this->renderHTML(); + } catch (Exception $e) { + return $e->getMessage()."\n".$e->getTraceAsString(); + } + } + + + /** + * on_add_return overload + * + * @return string 'this' + */ + protected function onAddReturn(): string + { + return 'this'; + } + + /** + * Set the form definition function name + * + * @param string $function_name form definition function name + * @return self + */ + public function setDefinitionFunction(string $function_name): Form + { + $this->definition_function = $function_name; + return $this; + } + + /** + * Get the form definition function body + * + * @return string|false form definition function body + */ + public function getDefinitionBody() + { + $body = false; + + try { + $definition_name = (!empty($this->definition_function) ? $this->definition_function : $this->getFormId()); + if (is_callable($definition_name)) { + if (function_exists($definition_name)) { + $func = new \ReflectionFunction($definition_name); + } else { + $func = new \ReflectionMethod($definition_name); + } + + if (is_object($func)) { + $filename = $func->getFileName(); + $start_line = $func->getStartLine() - 1; // it's actually - 1, + // otherwise you wont get the function() block + $end_line = $func->getEndLine(); + $length = $end_line - $start_line; + + $source = file($filename); + $body = implode("", array_slice($source, $start_line, $length)); + $body = str_replace('<', '<', $body); + $body = str_replace('>', '>', $body); + } + } + } catch (Exception $e) { + var_dump($e->getMessage()); + } + return $body; + } + + private function encapsulateJs($js_array, $jquery_var_name = 'jQuery'): string + { + if (!is_array($js_array)) { + $js_array = [$js_array]; + } + + return "(function(\$){\n". + "\t$(document).ready(function(){\n". + "\t\t".implode(";\n\t\t", $js_array).";\n". + "\t});\n". + "})({$jquery_var_name});"; + } +} diff --git a/src/classes/FormBuilder.php b/src/classes/FormBuilder.php new file mode 100644 index 00000000..93896cd6 --- /dev/null +++ b/src/classes/FormBuilder.php @@ -0,0 +1,396 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### ACCESSORIES #### + ######################################################### */ + +namespace Degami\PHPFormsApi; + +use Degami\PHPFormsApi\Accessories\SessionBag; +use Degami\PHPFormsApi\Exceptions\FormException; + +/** + * The form builder class + */ +class FormBuilder +{ + /** + * Check if session is present + * + * @return bool + */ + public static function sessionPresent(): bool + { + return (defined('PHP_VERSION_ID') && PHP_VERSION_ID > 54000) ? + (session_status() === PHP_SESSION_ACTIVE) : (trim(session_id()) != ''); + } + + /** + * Get Session Bag + * @param boolean $refresh force refresh + * + * @return SessionBag + */ + public static function getSessionBag($refresh = false): SessionBag + { + /** @var SessionBag */ + static $session_bag = null; + if (!$session_bag instanceof SessionBag || $refresh == true) { + $session_bag = new SessionBag(); + } + return $session_bag; + } + + /** + * Returns the form_id + * + * @param string|callable $function_name the function name + * @return string the form_id + */ + public static function getFormId($function_name): string + { + if (is_string($function_name)) { + return $function_name; + } + if (is_callable($function_name) && is_array($function_name)) { + return $function_name[1]; + } + return 'cs_form'; + } + + /** + * Returns callable function name string + * + * @param string|callable $function_name callable element + * @return string the function name + */ + public static function getDefinitionFunctionName($function_name): ?string + { + return static::getCallablaStringName($function_name); + } + + /** + * Returns callable function name string + * + * @param string|callable $function_name callable element + * @return string the function name + */ + public static function getCallablaStringName($function_name): ?string + { + if (is_string($function_name)) { + return $function_name; + } + if (is_callable($function_name) && is_array($function_name)) { + if (is_string($function_name[0])) { + return $function_name[0].'::'.$function_name[1]; + } + if (is_object($function_name[0])) { + return get_class($function_name[0]).'::'.$function_name[1]; + } + } + + return null; + } + + /** + * Returns a form object. + * This function calls the form definition function passing an + * initial empty form object and the form state + * + * @param string|callable $callable + * @param array &$form_state form state by reference + * @param array $form_options additional form constructor options + * + * @return Form a new form object + * @throws FormException + */ + public static function buildForm($callable, array &$form_state, $form_options = []): Form + { + $before = memory_get_usage(); + + $form_id = FormBuilder::getFormId($callable); + if (isset($form_options['form_id'])) { + $form_id = $form_options['form_id']; + } + $function_name = FormBuilder::getDefinitionFunctionName($callable); + + $form = new Form( + [ + 'form_id' => $form_id, + 'definition_function' => $function_name, + ] + $form_options + ); + + $form_state += FormBuilder::getRequestValues($form_id); + if (is_callable($callable)) { + $form_obj = call_user_func_array( + $callable, + array_merge( + [$form, &$form_state], + $form_state['build_info']['args'] + ) + ); + if (! $form_obj instanceof Form) { + throw new FormException( + "Function {$function_name} does not return a valid form object", + 1 + ); + } else { + $form_obj->setFormState($form_state); + } + + $form = $form_obj; + $form->setDefinitionFunction($function_name); + + if (self::sessionPresent() && defined('PHP_FORMS_API_DEBUG') && PHP_FORMS_API_DEBUG == true) { + self::getSessionBag()->ensurePath('/form_definition'); + self::getSessionBag()->form_definition[$form->getId()] = $form->toArray(); + } + } + + $after = memory_get_usage(); + $form->allocatedSize = ($after - $before); + + return $form; + } + + /** + * Get a new form object + * + * @param string|callable $callable form definition callable + * @param null $form_id form_id (optional) + * @return Form a new form object + * @throws FormException + */ + public static function getForm($callable, $form_id = null): Form + { + $form_state = []; + $args = func_get_args(); + // Remove $callable and $form_id from the arguments. + array_shift($args); + array_shift($args); + + $form_state['build_info']['args'] = $args; + + $form_options = []; + if (!is_null($form_id)) { + $form_options['form_id'] = $form_id; + } + + return FormBuilder::buildForm($callable, $form_state, $form_options); + } + + /** + * Returns rendered form's html string + * + * @param string|callable $callable form definition callable + * @param string $form_id form_id (optional) + * @return string form html + * @throws FormException + */ + public static function renderForm($callable, $form_id = null): string + { + $form = FormBuilder::getForm($callable, $form_id); + return $form->renderHTML(); + } + + /** + * Prepares the form_state array + * + * @param string $form_id the form_id + * @return array the form_state array + */ + public static function getRequestValues(string $form_id): array + { + $out = ['input_values' => [] , 'input_form_definition' => null]; + foreach (['_POST' => $_POST,'_GET' => $_GET,'_REQUEST' => $_REQUEST] as $key => $array) { + if (!empty($array['form_id']) && $array['form_id'] == $form_id) { + $out['input_values'] = $array; //array_merge($out, $array); + $out['input_values']['__values_container'] = $key; //array_merge($out, $array); + + if (isset($array['form_id']) && isset(self::getSessionBag()->form_definition[ $array['form_id'] ])) { + $out['input_form_definition'] = self::getSessionBag()->form_definition[ $array['form_id'] ]; + } + + break; + } + } + return $out; + } + + /** + * guess form field type by value + * + * @param mixed $value value to find field to + * @param string|null $element_name element name + * @return array form field info + */ + public static function guessFormType($value, ?string $element_name = null): array + { + $default_value = $value; + $vtype = gettype($default_value); + switch ($vtype) { + case 'object': + $vtype = get_class($default_value); + break; + } + + $type = null; + $validate = []; + switch (strtolower($vtype)) { + case 'string': + $type = 'textfield'; + break; + case 'integer': + $type = 'spinner'; + $validate = ['integer']; + break; + case 'float': + case 'double': + $type = 'textfield'; + $validate = ['numeric']; + break; + case 'boolean': + case 'bool': + $type = 'checkbox'; + break; + case 'datetime': + $type = 'datetime'; + /** @var \DateTime $default_value */ + $default_value = [ + 'year' => $default_value->format('Y'), + 'month' => $default_value->format('m'), + 'day' => $default_value->format('d'), + 'hours' => $default_value->format('H'), + 'minutes' => $default_value->format('i'), + 'seconds' => $default_value->format('s'), + ]; + break; + case 'date': + $type = 'date'; + + /** @var \DateTime $default_value */ + $default_value = [ + 'year' => $default_value->format('Y'), + 'month' => $default_value->format('m'), + 'day' => $default_value->format('d'), + 'hours' => $default_value->format('H'), + 'minutes' => $default_value->format('i'), + 'seconds' => $default_value->format('s'), + ]; + + break; + case 'array': + case 'object': + $type = 'textarea'; + $default_value = json_encode($default_value); + break; + } + + if ($type == null && ($default_value == null || is_scalar($default_value))) { + switch ($element_name) { + case 'id': + case 'surname': + case 'name': + $type = 'textfield'; + break; + case 'email': + $type = 'textfield'; + $validate = ['email']; + break; + case 'date': + case 'day': + case 'birth': + case 'birthdate': + case 'birthday': + $type = 'date'; + break; + case 'time': + $type = 'time'; + break; + default: + break; + } + } + + if ($type == null) { + $type = 'textfield'; + } + return [ 'type' => $type, 'validate' => $validate, 'default_value' => $default_value ]; + } + + /** + * Get a form to represent given object + * + * @param Form $form initial form object + * @param array &$form_state form state + * @param mixed $object object to represent + * @return Form form object + * @throws FormException + */ + public static function objFormDefinition(Form $form, array &$form_state, $object): Form + { + $form->setFormId(get_class($object)); + $fields = get_object_vars($object) + get_class_vars(get_class($object)); + + $fieldset = $form->addField( + get_class($object), + [ + 'type' => 'fieldset', + 'title' => get_class($object), + ] + ); + + foreach ($fields as $k => $v) { + list($type, $validate, $default_value) = array_values(FormBuilder::guessFormType($v, $k)); + $fieldset->addField( + $k, + [ + 'type' => $type, + 'title' => $k, + 'validate' => $validate, + 'default_value' => $default_value, + ] + ); + } + + $form + ->addField( + 'submit', + ['type' => 'submit'] + ); + + return $form; + } + + /** + * Returns a form object representing the object parameter + * + * @param object $object the object to map + * @return Form form object + * @throws FormException + */ + public static function objectForm(object $object): Form + { + $form_state = []; + $form_state['build_info']['args'] = [$object]; + + $form = FormBuilder::buildForm( + [__CLASS__, 'objFormDefinition'], + $form_state, + [ + 'submit' => [strtolower(get_class($object).'_submit')], + 'validate' => [strtolower(get_class($object).'_validate')], + ] + ); + return $form; + } +} diff --git a/src/classes/abstracts/base/Element.php b/src/classes/abstracts/base/Element.php new file mode 100644 index 00000000..d74d156e --- /dev/null +++ b/src/classes/abstracts/base/Element.php @@ -0,0 +1,643 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### BASE #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Abstracts\Base; + +use Degami\Basics\Traits\ToolsTrait as BasicToolsTrait; +use Degami\PHPFormsApi\Traits\Tools; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Accessories\NotificationsBag; +use Degami\Basics\Html\BaseElement; +use Degami\Basics\Html\TagElement; + +/** + * Base element class + * every form element classes inherits from this class + * + * @abstract + */ +abstract class Element extends BaseElement +{ + use BasicToolsTrait, Tools; + + /** + * Element name + * + * @var string + */ + protected $name; + + /** + * Element parent + * + * @var Element subclass + */ + protected $parent; + + /** + * Element weight + * + * @var integer + */ + protected $weight; + + /** + * Element container tag + * + * @var string + */ + protected $container_tag; + + /** + * Element container html class + * + * @var string + */ + protected $container_class; + + /** + * inner div attributes + * + * @var array + */ + protected $container_attributes = []; + + /** + * Element label class + * + * @var string + */ + protected $label_class; + + /** + * Element container inherits classes + * + * @var boolean + */ + protected $container_inherits_classes; + + /** + * Element errors array + * + * @var object + */ + protected $notifications; + + /** + * Element js array + * + * @var array + */ + protected $js; + + /** + * Element css array + * + * @var array + */ + protected $css; + + /** + * Element prefix + * + * @var string + */ + protected $prefix; + + /** + * Element suffix + * + * @var string + */ + protected $suffix; + + /** + * Element build options + * + * @var null + */ + protected $build_options; + + /** + * Element no translation flag. if true form::translate_string won't be applied + * + * @var FALSE + */ + protected $no_translation; + + /** + * Class constructor + * @param array $options constructor options + * @param string $name element name + */ + public function __construct($options = [], $name = null) + { + if ($options == null) { + $options = []; + } + + $this->name = $name; + $this->parent = null; + $this->weight = 0; + $this->container_tag = FORMS_DEFAULT_FIELD_CONTAINER_TAG; + $this->container_class = FORMS_DEFAULT_FIELD_CONTAINER_CLASS; + $this->label_class = FORMS_DEFAULT_FIELD_LABEL_CLASS; + $this->container_inherits_classes = false; + $this->notifications = new NotificationsBag([ 'error' => [], 'highlight'=> [] ]); + $this->js = []; + $this->css = []; + $this->prefix = ''; + $this->suffix = ''; + $this->build_options = $options; + $this->no_translation = false; + + $this->setClassProperties($options); + } + + /** + * Returns initially build options + * + * @return array build_options + */ + public function getBuildOptions(): ?array + { + return $this->build_options; + } + + /** + * Set name + * + * @param string $name element name + * + * @return Element + */ + public function setName(string $name): Element + { + $this->name = $name; + + return $this; + } + + /** + * Get name + * + * @return string element name + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * Set parent + * + * @param Element $parent element parent + * + * @return Element + */ + public function setParent(Element $parent): Element + { + $this->parent = $parent; + + return $this; + } + + /** + * Get parent + * + * @return Element element parent + */ + public function getParent(): ?Element + { + return $this->parent; + } + + /** + * Get weight + * + * @return int element weight + */ + public function getWeight(): int + { + return $this->weight; + } + + /** + * Add error + * + * @param string $error_string error string + * @param string $validate_function_name validation function name + * + * @return Element + */ + public function addError(string $error_string, string $validate_function_name): Element + { + $this->notifications['error'][$validate_function_name] = $error_string; + return $this; + } + + /** + * Get defined errors + * + * @return array errors + */ + public function getErrors(): array + { + return $this->notifications['error']->toArray(); + } + + /** + * Check if element has errors + * + * @return boolean there are errors + */ + public function hasErrors(): bool + { + return count($this->getErrors()) > 0; + } + + /** + * Set element errors + * + * @param array $errors errors array + * + * @return Element + */ + public function setErrors(array $errors): Element + { + $this->notifications['error'] = $errors; + + return $this; + } + + /** + * Add highlight + * + * @param string $highlight_string highlight string + * + * @return Element + */ + public function addHighlight(string $highlight_string): Element + { + $this->notifications['highlight'][] = $highlight_string; + + return $this; + } + + /** + * Get defined highlights + * + * @return array errors + */ + public function getHighlights(): array + { + return $this->notifications['highlight']->toArray(); + } + + /** + * Check if element has highlights + * + * @return boolean there are highlights + */ + public function hasHighlights(): bool + { + return count($this->getHighlights()) > 0; + } + + /** + * Set element highlights + * + * @param array $highlights highlights array + * + * @return Element + */ + public function setHighlights(array $highlights): Element + { + $this->notifications['highlight'] = $highlights; + + return $this; + } + + /** + * Add js to element + * + * @param string / array $js javascript to add + * @param boolean $as_is no "minification" + * + * @return Element + */ + public function addJs($js, $as_is = false): Element + { + if (getenv('DEBUG') == true) { + $as_is = true; + } + + if (!$as_is) { + if (is_array($js)) { + $js = array_filter(array_map(['minify_js', $this], $js)); + } elseif (is_string($js) && trim($js) != '') { + $js = $this->minifyJs($js); + } + } + + if (is_array($js)) { + $this->js = array_merge($js, $this->js); + } elseif (is_string($js) && trim($js) != '') { + $this->js[] = $js; + } + + return $this; + } + + /** + * minify js string + * + * @param string $js javascript minify + * @return string + */ + public function minifyJs($js): string + { + if (is_string($js) && trim($js) != '') { + $js = trim(preg_replace("/\s+/", " ", str_replace("\n", "", "". $js))); + } + + return $js; + } + + /** + * Get the element's js array + * + * @return array element's js array + */ + public function &getJs(): array + { + if ($this instanceof FieldsContainer || $this instanceof Form) { + $js = array_filter(array_map('trim', $this->js)); + $fields = $this->getFields(); + if ($this instanceof Form) { + $fields = $this->getFields($this->getCurrentStep()); + } + foreach ($fields as $field) { + /** @var Field $field */ + $js = array_merge($js, $field->getJs()); + } + return $js; + } + return $this->js; + } + + + /** + * Add css to element + * + * @param string / array $css css to add + * @return Element + */ + public function addCss($css): Element + { + if (is_array($css)) { + $css = array_filter(array_map('trim', $css)); + $this->css = array_merge($css, $this->css); + } elseif (is_string($css) && trim($css) != '') { + $this->css[] = trim($css); + } + + return $this; + } + + /** + * Get the element's css array + * + * @return array element's css array + */ + public function &getCss(): array + { + if ($this instanceof FieldsContainer || $this instanceof Form) { + $css = array_filter(array_map('trim', $this->css)); + foreach ($this->getFields() as $field) { + /** @var Field $field */ + $css = array_merge($css, $field->getCss()); + } + return $css; + } + return $this->css; + } + + /** + * Get element prefix + * + * @return string element prefix + */ + public function getPrefix(): string + { + return $this->prefix; + } + + /** + * Set element prefix + * + * @param string $prefix element prefix + * @return Element + */ + public function setPrefix(string $prefix): Element + { + $this->prefix = $prefix; + + return $this; + } + + /** + * Get element suffix + * + * @return string element suffix + */ + public function getSuffix(): string + { + return $this->suffix; + } + + /** + * Set element suffix + * + * @param string $suffix element suffix + * @return Element + */ + public function setSuffix(string $suffix): Element + { + $this->suffix = $suffix; + + return $this; + } + + /** + * Get element container_tag + * + * @return string element container_tag + */ + public function getContainerTag(): string + { + return $this->container_tag; + } + + /** + * Set element container_tag + * + * @param string $container_tag element container_tag + * + * @return Element + */ + public function setContainerTag(string $container_tag): Element + { + $this->container_tag = $container_tag; + + return $this; + } + + /** + * Get element container_class + * + * @return string element container_class + */ + public function getContainerClass(): string + { + return $this->container_class; + } + + /** + * Set element container_class + * + * @param string $container_class element container_class + * + * @return Element + */ + public function setContainerClass(string $container_class): Element + { + $this->container_class = $container_class; + + return $this; + } + + /** + * Get element html prefix + * + * @return string html for the element prefix + */ + public function getElementPrefix(): string + { + if (!empty($this->container_tag)) { + if (preg_match("/<\/?(.*?)\s.*?(class=\"(.*?)\")?.*?>/i", $this->container_tag, $matches)) { + // if a is contained try to get tag and class + $this->container_tag = $matches[1]; + $this->container_class = ( + !empty($this->container_class) ? $this->container_class : '' + ) . ( + !empty($matches[3]) ? ' '.$matches[3] : '' + ); + } + + $class = $this->container_class; + if ($this->container_inherits_classes + && isset($this->attributes['class']) + && !empty($this->attributes['class']) + ) { + $class .= ' '.$this->attributes['class'].'-container'; + } else { + if (method_exists($this, 'getType')) { + $class .= ' '.$this->getType().'-container'; + } + } + if ($this->hasErrors()) { + $class .= ' has-errors'; + } + $class = trim($class); + + $container_attributes = $this->container_attributes; + if (!isset($container_attributes['class'])) { + $container_attributes['class'] = ''; + } + $container_attributes['class'] .= ' ' . $class; + $container_attributes['class'] = trim($container_attributes['class']); + + + $container_id = $container_attributes['id'] ?? null; + unset($container_attributes['id']); + + $containerContructorArray = [ + 'tag' => $this->container_tag, + 'attributes' => $container_attributes, + ]; + if (!is_null($container_id)) { + $containerContructorArray['id'] = $container_id; + } + + $container = new TagElement($containerContructorArray); + + return preg_replace("/<\/".$this->container_tag.">$/", "", (string)$container); + +// $this->container_attributes['class'] = ($this->container_attributes['class'] ?? '') . $class; +// return "<{$this->container_tag} class=\"{$class}\">"; + } + return ''; + } + + /** + * Get element html suffix + * + * @return string html for the element suffix + */ + public function getElementSuffix(): string + { + if (!empty($this->container_tag)) { + return "container_tag}>"; + } + return ''; + } + + /** + * search field by field html id + * + * @param Element $container container to search into + * @param string $field_id field id + * @return Field|null Field object or null if not found + */ + protected static function searchFieldById(Element $container, string $field_id) + { + /** @var Field $container */ + if ($container instanceof FieldsContainer || $container instanceof Form) { + $fields = ($container instanceof Form) ? + $container->getFields($container->getCurrentStep()) : + $container->getFields(); + foreach ($fields as $key => $field) { + /** @var Field $field */ + if ($field->getHtmlId() == $field_id) { + return $field; + } elseif ($field instanceof FieldsContainer) { + $out = Element::searchFieldById($field, $field_id); + if ($out != null) { + return $out; + } + } + } + } elseif ($container->getHtmlId() == $field_id) { + // not a container + return $container; + } + return null; + } + + public function getData(): array + { + return get_object_vars($this); + } +} diff --git a/src/classes/abstracts/base/Field.php b/src/classes/abstracts/base/Field.php new file mode 100644 index 00000000..5de3f2c2 --- /dev/null +++ b/src/classes/abstracts/base/Field.php @@ -0,0 +1,756 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELD BASE #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Abstracts\Base; + +use Degami\PHPFormsApi\Interfaces\FieldInterface; +use Degami\PHPFormsApi\Traits\Tools; +use Degami\PHPFormsApi\Accessories\OrderedFunctions; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\FieldsContainer; +use Degami\PHPFormsApi\Fields\Checkbox; +use Degami\PHPFormsApi\Accessories\SessionBag; +use Degami\PHPFormsApi\Fields\Hidden; + +/** + * The field element class. + * + * @abstract + */ +abstract class Field extends Element implements FieldInterface +{ + use Tools; + + /** + * validate functions list + * + * @var array|OrderedFunctions + */ + protected $validate = []; + + /** + * preprocess functions list + * + * @var array|OrderedFunctions + */ + protected $preprocess = []; + + /** + * postprocess functions list + * + * @var array|OrderedFunctions + */ + protected $postprocess = []; + + /** + * Element js events list + * + * @var array|OrderedFunctions + */ + protected $event = []; + + /** + * Element size + * + * @var integer + */ + protected $size = 20; + + /** + * Element type + * + * @var string + */ + protected $type = ''; + + /** + * "stop on first validation error" flag + * + * @var boolean + */ + protected $stop_on_first_error = false; + + /** + * "show tooltip instead of label" flag + * + * @var boolean + */ + protected $tooltip = false; + + /** + * Element id + * + * @var null + */ + protected $id = null; + + /** + * Element title + * + * @var null + */ + protected $title = null; + + /** + * Element title prefix + * + * @var null + */ + protected $title_prefix = null; + + /** + * Element title suffix + * + * @var null + */ + protected $title_suffix = null; + + + /** + * Element description + * + * @var null + */ + protected $description = null; + + /** + * Element disabled + * + * @var boolean + */ + protected $disabled = false; + + /** + * Element default value + * + * @var null + */ + protected $default_value = null; + + /** + * Element value + * + * @var null + */ + protected $value = null; + + /** + * "element already pre-rendered" flag + * + * @var boolean + */ + protected $pre_rendered = false; + + /** + * "this is a required field" position + * + * @var string + */ + protected $required_position = 'after'; + + /** + * Element ajax url + * + * @var null + */ + protected $ajax_url = null; + + /** + * Session Bag Object + * + * @var SessionBag + */ + private $session_bag = null; + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct($options = [], ?string $name = null) + { + parent::__construct($options, $name); + + $this->session_bag = new SessionBag(); + + if (!isset($this->attributes['class'])) { + $this->setAttribute('class', trim(FORMS_FIELD_ADDITIONAL_CLASS.' '.$this->getElementClassName())); + } + + if (empty($this->type)) { + $this->type = substr(get_class($this), strrpos(get_class($this), '\\') + 1); + } + + if (!$this->validate instanceof OrderedFunctions) { + $this->validate = new OrderedFunctions($this->validate, 'validator', [ __CLASS__,'order_validators' ]); + } + + if (!$this->preprocess instanceof OrderedFunctions) { + $this->preprocess = new OrderedFunctions($this->preprocess, 'preprocessor'); + } + + if (!$this->postprocess instanceof OrderedFunctions) { + $this->postprocess = new OrderedFunctions($this->postprocess, 'postprocessor'); + } + + if (!$this->event instanceof OrderedFunctions) { + $this->event = new OrderedFunctions($this->event, 'event'); + } + + if (!$this->value) { + $this->value = $this->default_value; + } + } + + /** + * class "static" constructor + * + * @param array $options build options + * @param string $name field name + * @return Field + */ + public static function getInstance($options = [], $name = null): Field + { + // let others alter the field + static::executeAlter("/.*?_".static::getClassNameString()."_alter$/i", [&$options, &$name]); + return new static($options, $name); + } + + /** + * Get Session Bag + * + * @return SessionBag + */ + public function getSessionBag(): SessionBag + { + return $this->session_bag; + } + + /** + * Return field value + * + * @return mixed field value + */ + public function getValues() + { + return ($this->getValue() != null) ? $this->getValue() : $this->getDefaultValue(); + } + + /** + * Return field value + * + * @return mixed field value + */ + public function getValue() + { + return $this->value; + } + + /** + * Set field value + * + * @param mixed $value value to set + * @return Field + */ + public function setValue($value) + { + $this->value = $value; + + return $this; + } + + /** + * Get default value + * + * @return mixed default value + */ + public function getDefaultValue() + { + return $this->default_value; + } + + /** + * Set default value + * + * @param mixed $default_value default value + * @return Field + */ + public function setDefaultValue($default_value) + { + $this->default_value = $default_value; + + return $this; + } + + /** + * resets the field + * + * @return self + */ + public function resetField(): Field + { + $this->setValue($this->getDefaultValue()); + $this->pre_rendered = false; + $this->setErrors([]); + + return $this; + } + + /** + * Get field type + * + * @return string field type + */ + public function getType() + { + return $this->type; + } + + /** + * Get field validate + * + * @return OrderedFunctions field validate + */ + public function getValidate() + { + return $this->validate; + } + + /** + * Get field preprocess + * + * @return OrderedFunctions field preprocess + */ + public function getPreprocess() + { + return $this->preprocess; + } + + /** + * Get field postprocess + * + * @return OrderedFunctions field postprocess + */ + public function getPostprocess() + { + return $this->postprocess; + } + + /** + * Get field id + * + * @return string field id + */ + public function getId() + { + return $this->id; + } + + /** + * Set field id + * + * @param string $id field id + * @return Field + */ + public function setId($id) + { + $this->id = $id; + return $this; + } + + /** + * Get field html id + * + * @return string the html id attributes + */ + public function getHtmlId() + { + return preg_replace("/\[(.*?)\]/i", "_\\1", strtolower(!empty($this->id) ? $this->getId() : $this->getName())); + } + + /** + * Get css class name for field + * + * @return string css class name + */ + public function getElementClassName() + { + return strtolower(substr(get_class($this), strrpos(get_class($this), '\\') + 1)); + } + + /** + * Get field ajax url + * + * @return string field ajax url + */ + public function getAjaxUrl() + { + return $this->ajax_url; + } + + /** + * Process (set) the field value + * + * @param mixed $value value to set + */ + public function processValue($value) + { + $this->setValue($value); + } + + /** + * execute the preprocess ( or postprocess ) list of functions + * + * @param string $process_type which list to process + */ + public function preProcess($process_type = "preprocess") + { + foreach ($this->{$process_type} as $processor) { + $processor_func = "process".ucfirst($processor); + if (method_exists(get_class($this), $processor_func)) { + $this->value = call_user_func([$this, $processor_func], $this->value); + } elseif (method_exists(Form::class, $processor_func)) { + $this->value = call_user_func([Form::class,$processor_func], $this->value); + } elseif (function_exists("process_{$this->getType()}_{$processor}")) { + $processor_func = "process_{$this->getType()}_{$processor}"; + $this->value = $processor_func($this->value); + } + } + } + + /** + * postprocess field + */ + public function postProcess() + { + $this->preProcess("postprocess"); + } + + /** + * which element should return the add_field() function + * + * @return string one of 'parent' or 'this' + */ + public function onAddReturn() : string + { + return 'parent'; + } + + /** + * Check if field is valid using the validate functions list + * + * @return boolean valid state + */ + public function isValid() : bool + { + $this->setErrors([]); + + foreach ($this->validate as $validator) { + $matches = []; + if (is_array($validator)) { + $validator_func = $validator['validator']; + } else { + $validator_func = $validator; + } + preg_match('/^([A-Za-z0-9_]+)(\[(.+)\])?$/', $validator_func, $matches); + if (!isset($matches[1])) { + continue; + } + $validator_func = "validate".ucfirst($matches[1]); + $options = isset($matches[3]) ? $matches[3] : null; + if (function_exists($validator_func)) { + $error = $validator_func($this->value, $options); + } elseif (method_exists(get_class($this), $validator_func)) { + $error = call_user_func([get_class($this), $validator_func], $this->value, $options); + } elseif (method_exists(Form::class, $validator_func)) { + $error = call_user_func([Form::class, $validator_func], $this->value, $options); + } + if (isset($error) && $error !== true) { + $titlestr = (!empty($this->title)) ? $this->title : (!empty($this->name) ? $this->name : $this->id); + if (empty($error)) { + $error = '%t - Error.'; + } + $this->addError(str_replace('%t', $titlestr, $this->getText($error)), $validator_func); + if (is_array($validator) && !empty($validator['error_message'])) { + $this->addError( + str_replace('%t', $titlestr, $this->getText($validator['error_message'])), + $validator_func + ); + } + + if ($this->stop_on_first_error) { + return false; + } + } + } + + if ($this->hasErrors()) { + return false; + } + + return true; + } + + /** + * renders field errors + * + * @return string errors as a
  • list + */ + public function showErrors(): string + { + return $this->notifications->renderHTML('error'); + } + + /** + * Pre render. this function will be overloaded by subclasses where needed + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + $this->pre_rendered = true; + // should not return value, just change element/form state + } + + /** + * render the field + * + * @param Form $form form object + * + * @return string the field html + */ + public function renderHTML(Form $form): string + { + $id = $this->getHtmlId(); + $output = ''; + if (!($this instanceof Hidden)) { + $output .= $this->getElementPrefix(); + $output .= $this->getPrefix(); + } + + if (!($this instanceof FieldsContainer) && !($this instanceof Checkbox)) { + // containers do not need label. checkbox too, as the render function prints the label itself + $required = ($this->validate->hasValue('required')) ? '*' : ''; + $requiredafter = $requiredbefore = $required; + if ($this->required_position == 'before') { + $requiredafter = ''; + $requiredbefore = $requiredbefore.' '; + } else { + $requiredbefore = ''; + $requiredafter = ' '.$requiredafter; + } + + if (!empty($this->title)) { + if ($this->tooltip == false) { + $this->label_class .= " label-" . $this->getElementClassName(); + $this->label_class = trim($this->label_class); + $label_class = (!empty($this->label_class)) ? " class=\"{$this->label_class}\"" : ""; + $output .= "\n"; + } else { + if (!in_array('title', array_keys($this->attributes))) { + $this->attributes['title'] = strip_tags( + ($this->title_prefix ? $this->getText($this->title_prefix) . ' ' : '') . + $this->getText($this->title) . + ($this->title_suffix ? ' ' . $this->getText($this->title_suffix) : '') . + $required + ); + } + + $id = $this->getHtmlId(); + $form->addJs("\$('#{$id}','#{$form->getId()}').tooltip();"); + } + } + } + + if (!$this->pre_rendered) { + $this->preRender($form); + $this->pre_rendered = true; + } + + $output .= $this->renderField($form); + + if (!($this instanceof FieldsContainer)) { + if (!empty($this->description)) { + $output .= "
    ".$this->getText($this->description)."
    "; + } + } + if ($form->errorsInline() == true && $this->hasErrors()) { + $output.= '
    '.implode("
    ", $this->getErrors()).'
    '; + } + + if (!($this instanceof Hidden)) { + $output .= $this->getSuffix(); + $output .= $this->getElementSuffix(); + } + + if (count($this->event) > 0 && trim($this->getAjaxUrl()) != '') { + foreach ($this->event as $event) { + $eventjs = $this->generateEventJs($event, $form); + $this->addJs($eventjs); + } + } + + // let others alter the output + static::executeAlter("/.*?_".static::getClassNameString()."_render_output_alter$/i", [&$output]); + + // return html string + return $output; + } + + /** + * generate the necessary js to handle ajax field event property + * + * @param array $event event element + * @param Form $form form object + * + * @return string javascript code + */ + public function generateEventJs($event, Form $form) + { + $id = $this->getHtmlId(); + if (empty($event['event'])) { + return false; + } + $question_ampersand = '?'; + if (preg_match("/\?/i", $this->getAjaxUrl())) { + $question_ampersand = '&'; + } + $event['callback'] = base64_encode(serialize($event['callback'])); + $eventjs = "\$('#{$id}','#{$form->getId()}').on('{$event['event']}',function(evt){ + evt.preventDefault(); + var \$target = ". + ((isset($event['target']) && !empty($event['target'])) ? + "\$('#".$event['target']."')" : + "\$('#{$id}').parent()"). + "; + var jsondata = { + 'name':\$('#{$id}').attr('name'), + 'value':\$('#{$id}').val(), + 'callback':'{$event['callback']}' + }; + var postdata = new FormData(); + postdata.append('form_id', '{$form->getId()}'); + postdata.append('jsondata', JSON.stringify(jsondata)); + \$('#{$form->getId()} input,#{$form->getId()} select,#{$form->getId()} textarea') + .each(function(index, elem){ + var \$this = \$(this); + if( + \$this.prop('tagName').toLowerCase() == 'input' && + \$this.attr('type').toLowerCase() == 'file' + ){ + if ((\$this)[0].files.length > 1) { + for (var i=0; i < (\$this)[0].files.length; i++) { + postdata.append(\$this.attr('name')+'[]', (\$this)[0].files[i] ); + } + } else if ((\$this)[0].files.length == 1) { + postdata.append(\$this.attr('name'), (\$this)[0].files[0] ); + } + } else { + if (\$this.attr('multiple') != undefined && \$.isArray(\$this.val())) { + \$.each(\$this.val(), function(index, value) { + postdata.append(\$this.attr('name')+'[]', value); + }); + } else { + var addvalue = true; + if ( + \$this.prop('tagName').toLowerCase() == 'input' && + (\$this.attr('type').toLowerCase() == 'radio' || \$this.attr('type').toLowerCase() == 'checkbox') + ) { + if (\$this.attr('checked') != 'checked') { + addvalue = false; + } + } + if(addvalue) { + postdata.append(\$this.attr('name'), \$this.val()); + } + } + } + }); + var \$loading = \$('
    ') + .appendTo(\$target) + .css({'font-size':'0.5em'}) + .progressbar({value: false}); + \$.data(\$target[0],'loading', \$loading.attr('id')); + \$.ajax({ + type: \"POST\", + contentType: false, + processData: false, + url: \"{$this->getAjaxUrl()}{$question_ampersand}partial=true&triggering_element={$this->getHtmlId()}\", + data: postdata, + success: function( data ){ + var response; + if(typeof data =='object') { response = data; } + else { response = \$.parseJSON(data); } + ".( + (!empty($event['method']) && $event['method'] == 'replace') ? + "\$target.html('');": + "" + )." + ".( + (!empty($event['effect']) && $event['effect'] == 'fade') ? + "\$target.hide(); \$(response.html).appendTo(\$target); \$target.fadeIn('fast');": + "\$(response.html).appendTo(\$target);" + )." + if( \$.trim(response.js) != '' ){ eval( response.js ); }; + + var element_onsuccess = \$.data( \$('#{$id}','#{$form->getId()}')[0], 'element_onsuccess' ); + if( !!( + element_onsuccess && element_onsuccess.constructor && + element_onsuccess.call && element_onsuccess.apply + ) ){ + element_onsuccess(); + } + }, + error: function ( jqXHR, textStatus, errorThrown ){ + var element_onerror = \$.data( \$('#{$id}','#{$form->getId()}')[0], 'element_onerror' ); + if( !!(element_onerror && element_onerror.constructor && element_onerror.call && element_onerror.apply) ){ + element_onerror(); + } + + if(\$.trim(errorThrown) != '') alert(textStatus+': '+errorThrown); + }, + complete: function( jqXHR, textStatus ){ + var loading = \$.data(\$target[0],'loading'); + \$('#'+loading).remove(); + } + }); + return false; + });"; + return $eventjs; + } + + /** + * alter request hook + * + * @param array &$request request array + */ + public function alterRequest(array &$request) + { + // implementing this function fields can change the request array + } + /** + * after validate hook + * + * @param Form $form form object + */ + public function afterValidate(Form $form) + { + // here field can do things after the validation has passed + } +} diff --git a/src/classes/abstracts/base/FieldsContainer.php b/src/classes/abstracts/base/FieldsContainer.php new file mode 100644 index 00000000..f4cfc0c2 --- /dev/null +++ b/src/classes/abstracts/base/FieldsContainer.php @@ -0,0 +1,330 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELD CONTAINERS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Abstracts\Base; + +use Degami\PHPFormsApi\Exceptions\FormException; +use Degami\PHPFormsApi\Interfaces\FieldsContainerInterface; +use Degami\PHPFormsApi\Traits\Containers; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Fields\Checkbox; +use Degami\PHPFormsApi\Fields\Select; +use Degami\PHPFormsApi\Abstracts\Fields\FieldMultivalues; + +/** + * a field that contains other fields class + * + * @abstract + */ +abstract class FieldsContainer extends Field implements FieldsContainerInterface +{ + use Containers; + + /** + * Get the form fields by type + * + * @param mixed $field_types field types + * @return array fields in the element + */ + public function getFieldsByType($field_types): array + { + if (!is_array($field_types)) { + $field_types = [$field_types]; + } + $out = []; + + foreach ($this->getFields() as $field) { + if ($field instanceof FieldsContainer) { + $out = array_merge($out, $field->getFieldsByType($field_types)); + } else { + if ($field instanceof Field && in_array($field->getType(), $field_types)) { + $out[] = $field; + } + } + } + return $out; + } + + /** + * Get fields by type and name + * + * @param mixed $field_types field types + * @param string $name field name + * @return array fields in the element matching the search criteria + */ + public function getFieldsByTypeAndName($field_types, string $name): array + { + if (!is_array($field_types)) { + $field_types = [$field_types]; + } + $out = []; + + foreach ($this->getFields() as $field) { + if ($field instanceof FieldsContainer) { + $out = array_merge($out, $field->getFieldsByTypeAndName($field_types, $name)); + } else { + if ($field instanceof Field + && in_array($field->getType(), $field_types) + && $field->getName() == $name + ) { + $out[] = $field; + } + } + } + return $out; + } + + /** + * Get field by name + * + * @param string $field_name field name + * @return Element|null subclass field object + */ + public function getField(string $field_name): ?Element + { + return isset($this->fields[$field_name]) ? $this->fields[$field_name] : null; + } + + /** + * Set field + * + * @param string $field_name field name + * @param Element $field subclass field object + * + * @return FieldsContainer + */ + public function setField(string $field_name, $field): FieldsContainer + { + $field->setName($field_name); + $this->fields[$field_name] = $field; + return $this; + } + + /** + * Add field to form + * + * @param string $name field name + * @param mixed $field field to add, can be an array or a field subclass + * @return Element + * @throws FormException + */ + public function addField(string $name, $field): Element + { + /** @var Field $field */ + $field = $this->getFieldObj($name, $field); + $field->setParent($this); + + $this->setField($name, $field); + $this->insert_field_order[] = $name; + + if (!method_exists($field, 'onAddReturn')) { + if ($this->isFieldContainer($field)) { + return $field; + } + return $this; + } + if ($field->onAddReturn() == 'this') { + return $field; + } + return $this; + } + + /** + * remove field from form + * + * @param string $name field name + * @return FieldsContainer + */ + public function removeField(string $name): FieldsContainer + { + unset($this->fields[$name]); + if (($key = array_search($name, $this->insert_field_order)) !== false) { + unset($this->insert_field_order[$key]); + } + return $this; + } + + /** + * Return form elements values into this element + * + * @return mixed form values + */ + public function getValues() + { + $output = []; + foreach ($this->getFields() as $name => $field) { + /** @var Field $field */ + if ($field->isAValue() == true) { + $output[$name] = $field->getValues(); + if (is_array($output[$name]) && empty($output[$name])) { + unset($output[$name]); + } + } + } + return $output; + } + + /** + * {@inheritdoc} + * + * @param string $process_type preprocess type + */ + public function preProcess($process_type = "preprocess") + { + foreach ($this->getFields() as $field) { + /** @var Field $field */ + $field->preProcess($process_type); + } + } + + /** + * Process (set) the fields value + * + * @param mixed $values value to set + */ + public function processValue($values) + { + foreach ($this->getFields() as $name => $field) { + /** @var Field $field */ + if ($field instanceof FieldsContainer) { + $this->getField($name)->processValue($values); + } elseif (($requestValue = static::traverseArray($values, $field->getName())) != null) { + $this->getField($name)->processValue($requestValue); + } elseif ($field instanceof Checkbox) { + // no value on request[name] && field is a checkbox - process anyway with an empty value + $this->getField($name)->processValue(null); + } elseif ($field instanceof Select) { + if ($field->isMultiple()) { + $this->getField($name)->processValue([]); + } else { + $this->getField($name)->processValue(null); + } + } elseif ($field instanceof FieldMultivalues) { + // no value on request[name] && field is a multivalue + // (eg. checkboxes ?) - process anyway with an empty value + $this->getField($name)->processValue([]); + } + } + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + foreach ($this->getFields() as $name => $field) { + if (is_object($field) && method_exists($field, 'preRender')) { + $field->preRender($form); + } + } + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @return boolean TRUE if element is valid + */ + public function isValid(): bool + { + $valid = true; + foreach ($this->getFields() as $field) { + /** @var Field $field */ + if (!$field->isValid()) { + // not returning FALSE to let all the fields to be validated + $valid = false; + } + } + return $valid; + } + + /** + * renders form errors + * + * @return string errors as an html
  • list + */ + public function showErrors(): string + { + $output = ""; + foreach ($this->getFields() as $field) { + /** @var Field $field */ + $output .= $field->showErrors(); + } + return $output; + } + + /** + * resets the fields + */ + public function resetField(): Field + { + foreach ($this->getFields() as $field) { + /** @var Field $field */ + $field->resetField(); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue(): bool + { + return true; + } + + /** + * {@inheritdoc} + * + * @param array $request request array + */ + public function alterRequest(array &$request) + { + foreach ($this->getFields() as $field) { + /** @var Field $field */ + $field->alterRequest($request); + } + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function afterValidate(Form $form) + { + foreach ($this->getFields() as $field) { + /** @var Field $field */ + $field->afterValidate($form); + } + } + + /** + * on_add_return overload + * + * @return string 'this' + */ + public function onAddReturn(): string + { + return 'this'; + } +} diff --git a/src/classes/abstracts/containers/FieldsContainerMultiple.php b/src/classes/abstracts/containers/FieldsContainerMultiple.php new file mode 100644 index 00000000..660092eb --- /dev/null +++ b/src/classes/abstracts/containers/FieldsContainerMultiple.php @@ -0,0 +1,219 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELD CONTAINERS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Abstracts\Containers; + +use Degami\PHPFormsApi\Abstracts\Base\Element; +use Degami\PHPFormsApi\Exceptions\FormException; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\FieldsContainer; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\PHPFormsApi\Traits\Containers; + +/** + * a field container subdivided in groups + * + * @abstract + */ +abstract class FieldsContainerMultiple extends FieldsContainer +{ + use Containers; + + /** + * Element sub-elements + * + * @var array + */ + protected $partitions = []; + + /** + * Get element partitions + * + * @return array partitions + */ + public function &getPartitions(): array + { + return $this->partitions; + } + + /** + * Get number of defined partitions + * + * @return integer partitions number + */ + public function numPartitions(): int + { + return count($this->partitions); + } + + /** + * Add a new partition + * + * @param string $title partition title + * @return self + */ + public function addPartition($title): FieldsContainerMultiple + { + $this->partitions[] = ['title'=>$title,'fieldnames'=>[]]; + + return $this; + } + + /** + * Add field to element + * + * @param string $name field name + * @param mixed $field field to add, can be an array or a field subclass + * @return Element + * @throws FormException + */ + public function addField(string $name, $field): Element + { + $field = $this->getFieldObj($name, $field); + $field->setParent($this); + + $partitions_index = null; + if (func_num_args() == 3) { + $partitions_index = func_get_arg(2); + } + if (!is_numeric($partitions_index) || !array_key_exists($partitions_index, $this->partitions)) { + $partitions_index = $this->numPartitions() - 1; + } + + $this->fields[$name] = $field; + $this->insert_field_order[$partitions_index][] = $name; + if (!isset($this->partitions[$partitions_index])) { + $this->partitions[$partitions_index] = ['title'=>'','fieldnames'=>[]]; + } + $this->partitions[$partitions_index]['fieldnames'][] = $name; + + if (!method_exists($field, 'onAddReturn')) { + if ($this->isFieldContainer($field)) { + return $field; + } + return $this; + } + if ($field->onAddReturn() == 'this') { + return $field; + } + return $this; + } + + /** + * remove field from form + * + * @param string $name field name + * @return FieldsContainerMultiple + */ + public function removeField(string $name) : FieldsContainer + { + $partitions_index = func_get_arg(1); + if (!is_numeric($partitions_index)) { + $partitions_index = $this->getPartitionIndex($name); + } + + unset($this->fields[$name]); + if (isset($this->insert_field_order[$partitions_index]) + && ($key = array_search($name, $this->insert_field_order[$partitions_index])) !== false + ) { + unset($this->insert_field_order[$partitions_index][$key]); + } + if (isset($this->partitions[$partitions_index]['fieldnames']) + && ($key = array_search($name, $this->partitions[$partitions_index]['fieldnames'])) !== false + ) { + unset($this->partitions[$partitions_index]['fieldnames'][$key]); + } + return $this; + } + + /** + * Get partition fields array + * + * @param int $partitions_index partition index + * @return array partition fields array + */ + public function &getPartitionFields(int $partitions_index): array + { + $out = []; + $field_names = $this->partitions[$partitions_index]['fieldnames']; + foreach ($field_names as $name) { + $out[$name] = $this->getField($name); + } + return $out; + } + + /** + * Set partition fields array + * + * @param array $fields array of new fields to set for partition + * @param integer $partition_index partition index + * @return self + * @throws FormException + */ + public function setPartitionFields(array $fields, $partition_index = 0): FieldsContainerMultiple + { + $fields_names = $this->partitions[$partition_index]['fieldnames']; + foreach ($fields_names as $name) { + $this->removeField($name, $partition_index); + } + unset($this->partitions[$partition_index]['fieldnames']); + $this->partitions[$partition_index]['fieldnames'] = []; + foreach ($fields as $name => $field) { + if ($field instanceof Field) { + $name = $field->getName(); + } + $field = $this->getFieldObj($name, $field); + $this->addField($field->getName(), $field, $partition_index); + } + return $this; + } + + /** + * Check if partition has errors + * + * @param int $partitions_index partition index + * @param Form $form form object + * @return boolean partition has errors + */ + public function partitionHasErrors(int $partitions_index, Form $form): bool + { + if (!$form->isProcessed()) { + return false; + } + $out = false; + foreach ($this->getPartitionFields($partitions_index) as $name => $field) { + if ($out == true) { + continue; + } + $out |= !$field->isValid(); + } + return $out; + } + + /** + * Get partition index containint specified field name + * + * @param string $field_name field name + * @return integer partition index, -1 on failure + */ + public function getPartitionIndex($field_name): int + { + foreach ($this->partitions as $partitions_index => $partition) { + if (in_array($field_name, $partition['fieldnames'])) { + return $partitions_index; + } + } + return -1; + } +} diff --git a/src/classes/abstracts/containers/SortableContainer.php b/src/classes/abstracts/containers/SortableContainer.php new file mode 100644 index 00000000..6dcd0655 --- /dev/null +++ b/src/classes/abstracts/containers/SortableContainer.php @@ -0,0 +1,129 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELD CONTAINERS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Abstracts\Containers; + +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\FieldsContainer; + +/** + * an abstract sortable field container + * + * @abstract + */ +abstract class SortableContainer extends FieldsContainerMultiple +{ + + /** + * sort handle position (left/right) + * + * @var string + */ + protected $handle_position = 'left'; + + /** + * deltas array ( used for sorting ) + * + * @var array + */ + protected $deltas = []; + + /** + * Get handle position (left/right) + * + * @return string handle position + */ + public function getHandlePosition(): string + { + return $this->handle_position; + } + + /** + * Return form elements values into this element + * + * @return mixed form values + */ + public function getValues() + { + $output = []; + + $fields_with_delta = $this->getFieldsWithDelta(); + usort($fields_with_delta, [__CLASS__, 'orderbyDelta']); + + foreach ($fields_with_delta as $name => $info) { + $field = $info['field']; + /** @var Field $field */ + if ($field->isAValue() == true) { + $output[$name] = $field->getValues(); + if (is_array($output[$name]) && empty($output[$name])) { + unset($output[$name]); + } + } + } + return $output; + } + + /** + * Process (set) the fields value + * + * @param mixed $values value to set + */ + public function processValue($values) + { + foreach ($this->getFields() as $name => $field) { + /** @var Field $field */ + $partitionindex = $this->getPartitionIndex($field->getName()); + + if ($field instanceof FieldsContainer) { + $this->getField($name)->processValue($values); + } elseif (($requestValue = static::traverseArray($values, $field->getName())) != null) { + $this->getField($name)->processValue($requestValue); + } + + $this->deltas[$name] = isset($values[$this->getHtmlId().'-delta-'.$partitionindex]) ? + $values[$this->getHtmlId().'-delta-'.$partitionindex] : + 0; + } + } + + /** + * Get an array of fields with the relative delta (ordering) information + * + * @return array fields with delta + */ + private function getFieldsWithDelta(): array + { + $out = []; + foreach ($this->getFields() as $key => $field) { + $out[$key]=['field'=> $field,'delta'=>$this->deltas[$key]]; + } + return $out; + } + + /** + * order elements by delta property + * + * @param array $a first element + * @param array $b second element + * @return integer order + */ + private static function orderbyDelta($a, $b): int + { + if ($a['delta']==$b['delta']) { + return 0; + } + return ($a['delta']>$b['delta']) ? 1:-1; + } +} diff --git a/src/classes/abstracts/fields/Action.php b/src/classes/abstracts/fields/Action.php new file mode 100644 index 00000000..157ba73f --- /dev/null +++ b/src/classes/abstracts/fields/Action.php @@ -0,0 +1,71 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Abstracts\Fields; + +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; + +/** + * The "actionable" field element class (a button, a submit or a reset) + * + * @abstract + */ +abstract class Action extends Field +{ + + /** + * "use jqueryui button method on this element" flag + * + * @var boolean + */ + protected $js_button = false; + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + if ($this->js_button == true) { + $id = $this->getHtmlId(); + $this->addJs("\$('#{$id}','#{$form->getId()}').button();"); + } + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @return boolean this is not a value + */ + public function isAValue(): bool + { + return false; + } + + /** + * validate function + * + * @return boolean this field is always valid + */ + public function isValid(): bool + { + return true; + } +} diff --git a/src/classes/abstracts/fields/Captcha.php b/src/classes/abstracts/fields/Captcha.php new file mode 100644 index 00000000..95909047 --- /dev/null +++ b/src/classes/abstracts/fields/Captcha.php @@ -0,0 +1,82 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Abstracts\Fields; + +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\FormBuilder; +use Degami\PHPFormsApi\Abstracts\Base\Field; + +/** + * The captcha field class + */ +abstract class Captcha extends Field +{ + + /** + * "already validated" flag + * + * @var boolean + */ + protected $already_validated = false; + + /** + * {@inheritdoc} + * + * @param mixed $values value to set + */ + public function processValue($values) + { + parent::processValue($values); + if (isset($values['already_validated'])) { + $this->already_validated = $values['already_validated']; + } + } + + /** + * Check if element is already validated + * + * @return boolean TRUE if element has already been validated + */ + public function isAlreadyValidated(): bool + { + return $this->already_validated; + } + + /** + * {@inheritdoc} + * + * @return boolean this is not a value + */ + public function isAValue(): bool + { + return false; + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function afterValidate(Form $form) + { + $session_value =$this->getValues(); + $session_value['already_validated'] = $this->isAlreadyValidated(); + + if (FormBuilder::sessionPresent()) { + $this->getSessionBag()->ensurePath("/{$form->getId()}/steps/{$form->getCurrentStep()}"); + $this->getSessionBag()->{$form->getId()}->steps->{$form->getCurrentStep()}->{$this->getName()} = $session_value; + } + } +} diff --git a/src/classes/abstracts/fields/Clickable.php b/src/classes/abstracts/fields/Clickable.php new file mode 100644 index 00000000..c6b70d62 --- /dev/null +++ b/src/classes/abstracts/fields/Clickable.php @@ -0,0 +1,88 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Abstracts\Fields; + +use Degami\PHPFormsApi\Abstracts\Base\Field; + +/** + * The "clickable" field element (a button or a submit ) + * + * @abstract + */ +abstract class Clickable extends Action +{ + + /** + * "this element was clicked" flag + * + * @var boolean + */ + protected $clicked = false; + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct($options = [], ?string $name = null) + { + parent::__construct($options, $name); + if (isset($options['value'])) { + $this->value = $options['value']; + } + $this->clicked = false; + } + + /** + * Check if this button was clicked + * + * @return boolean if this element was clicked + */ + public function getClicked(): bool + { + return $this->clicked; + } + + /** + * {@inheritdoc} + * + * @param mixed $value value to set + */ + public function processValue($value) + { + parent::processValue($value); + $this->clicked = true; + } + + /** + * reset this element + */ + public function resetField() : Field + { + $this->clicked = false; + return parent::resetField(); + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue() : bool + { + return true; + } +} diff --git a/src/classes/abstracts/fields/ComposedField.php b/src/classes/abstracts/fields/ComposedField.php new file mode 100644 index 00000000..7e578b92 --- /dev/null +++ b/src/classes/abstracts/fields/ComposedField.php @@ -0,0 +1,141 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Abstracts\Fields; + +use Degami\Basics\Traits\ToolsTrait as BasicToolsTrait; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Containers\TagContainer; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\Basics\Html\TagElement; + +/** + * The composed field class + */ +abstract class ComposedField extends TagContainer +{ + use BasicToolsTrait; + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue() : bool + { + return true; + } + + /** + * on_add_return overload + * + * @return string 'parent' + */ + public function onAddReturn() : string + { + return 'parent'; + } + + + /** + * Return subfields name + * + * @param string $subfieldName + * @return string + */ + protected function getSubfieldName(string $subfieldName): string + { + return $this->getName() . (preg_match("/.*?\[.*?\]$/", $this->getName()) ? '['.$subfieldName.']' : '_'.$subfieldName); + } + + + /** + * Process subfield value + * + * @param array $values + * @param Field $subfield + * @param string $subfieldName + */ + protected function processSubfieldsValues(array $values, Field $subfield, string $subfieldName) + { + $subfield->processValue(static::traverseArray($values, $this->getSubfieldName($subfieldName))); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|TagElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + $this->tag = 'div'; + + $required = ($this->validate->hasValue('required')) ? '*' : ''; + $requiredafter = $requiredbefore = $required; + if ($this->required_position == 'before') { + $requiredafter = ''; + $requiredbefore = $requiredbefore.' '; + } else { + $requiredbefore = ''; + $requiredafter = ' '.$requiredafter; + } + + if (!empty($this->title) && $this->tooltip == true && !in_array('title', array_keys($this->attributes))) { + $this->attributes['title'] = strip_tags($this->getText($this->title).$required); + } + + $tag = new TagElement([ + 'tag' => $this->tag, + 'id' => $id, + 'attributes' => $this->attributes, + ]); + + if (!empty($this->title)) { + if ($this->tooltip == false) { + $this->label_class .= " label-" .$this->getElementClassName(); + $this->label_class = trim($this->label_class); + + $tag_label = new TagElement([ + 'tag' => 'label', + 'attributes' => [ + 'for' => $id, + 'class' => $this->label_class, + 'text' => $requiredbefore + ], + ]); + $tag_label->addChild($this->getText($this->title)); + $tag_label->addChild($requiredafter); + $tag->addChild($tag_label); + } else { + $id = $this->getHtmlId(); + $form->addJs("\$('#{$id}','#{$form->getId()}').tooltip();"); + } + } + + foreach (get_object_vars($this) as $name => &$property) { + if ($property instanceof Field) { + if ($name == 'parent') { + continue; + } + $tag->addChild($property->renderHTML($form)); + } + } + + return $tag; + } +} diff --git a/src/classes/abstracts/fields/FieldMultivalues.php b/src/classes/abstracts/fields/FieldMultivalues.php new file mode 100644 index 00000000..779b23af --- /dev/null +++ b/src/classes/abstracts/fields/FieldMultivalues.php @@ -0,0 +1,135 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Abstracts\Fields; + +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\PHPFormsApi\Fields\Option; +use Degami\PHPFormsApi\Fields\Optgroup; +use Degami\PHPFormsApi\Abstracts\Fields\Optionable; + +/** + * The multivalues field class (a select, a radios or a checkboxes group) + * + * @abstract + */ +abstract class FieldMultivalues extends Field +{ + /** + * options array + * + * @var array + */ + protected $options = []; + + /** + * Adds an option to options array + * + * @param mixed $option option + */ + public function addOption($option) + { + $this->options[] = $option; + } + + /** + * Get elements options array by reference + * + * @return array element options + */ + public function &getOptions(): array + { + return $this->options; + } + + /** + * Check if key is present into haystack + * + * @param mixed $needle element to find + * @param array $haystack where to find it + * @return boolean TRUE if element is found + */ + public static function hasKey($needle, array $haystack): bool + { + foreach ($haystack as $key => $value) { + if ($value instanceof Option) { + if ($value->getKey() == $needle) { + return true; + } + } elseif ($value instanceof Optgroup) { + if ($value->optionsHasKey($needle) == true) { + return true; + } + } elseif ($needle == $key) { + return true; + } elseif (FieldMultivalues::isForeacheable($value) && FieldMultivalues::hasKey($needle, $value) == true) { + return true; + } + } + return false; + } + + /** + * Check if key is present into element options + * + * @param mixed $needle element to find + * @return boolean TRUE if element is found + */ + public function optionsHasKey($needle): bool + { + return FieldMultivalues::hasKey($needle, $this->options); + } + + /** + * {@inheritdoc} + * + * @return boolean TRUE if element is valid + */ + public function isValid(): bool + { + $titlestr = (!empty($this->title)) ? $this->title : (!empty($this->name) ? $this->name : $this->id); + + if (!is_array($this->value) && !empty($this->value)) { + $check = $this->optionsHasKey($this->value); + $this->addError(str_replace("%t", $titlestr, $this->getText("%t: Invalid choice")), __FUNCTION__); + + if (!$check) { + return false; + } + } elseif (FieldMultivalues::isForeacheable($this->value)) { + $check = true; + foreach ($this->value as $key => $value) { + $check &= $this->optionsHasKey($value); + } + if (!$check) { + $this->addError(str_replace("%t", $titlestr, $this->getText("%t: Invalid choice")), __FUNCTION__); + + if ($this->stop_on_first_error) { + return false; + } + } + } + return parent::isValid(); + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue(): bool + { + return true; + } +} diff --git a/src/classes/abstracts/fields/Optionable.php b/src/classes/abstracts/fields/Optionable.php new file mode 100644 index 00000000..23ac40a6 --- /dev/null +++ b/src/classes/abstracts/fields/Optionable.php @@ -0,0 +1,67 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Abstracts\Fields; + +use Degami\PHPFormsApi\Abstracts\Base\Element; + +/** + * The optionable field class + */ +abstract class Optionable extends Element +{ + /** + * option label + * + * @var string + */ + protected $label; + + /** + * Get the element label + * + * @return mixed the element label + */ + public function getLabel(): string + { + return $this->label; + } + + /** + * Set the element label + * + * @param mixed $label element label + * @return self + */ + public function setLabel($label): Optionable + { + $this->label = $label; + + return $this; + } + + /** + * Class constructor + * + * @param string $label label + * @param array $options options array + */ + public function __construct(string $label, array $options) + { + parent::__construct(); + $this->setLabel($label); + + $this->setClassProperties($options); + } +} diff --git a/src/classes/accessories/FormValues.php b/src/classes/accessories/FormValues.php new file mode 100644 index 00000000..66813831 --- /dev/null +++ b/src/classes/accessories/FormValues.php @@ -0,0 +1,30 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### ACCESSORIES #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Accessories; + +use Degami\Basics\MultiLevelDataBag; + +/** + * A class to hold form fields submitted values + */ +class FormValues extends MultiLevelDataBag +{ + /** + * onChange hook + */ + public function onChange() + { + } +} diff --git a/src/classes/accessories/NotificationsBag.php b/src/classes/accessories/NotificationsBag.php new file mode 100644 index 00000000..ea6cd5b0 --- /dev/null +++ b/src/classes/accessories/NotificationsBag.php @@ -0,0 +1,66 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### ACCESSORIES #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Accessories; + +use Degami\Basics\MultiLevelDataBag; + +/** + * A class to hold notifications + */ + +class NotificationsBag extends MultiLevelDataBag +{ + /** + * render notifications + * + * @param string the notification group + * + * @return string the notification list html + */ + public function renderHTML($group = null): string + { + if ($group == null) { + $out = ""; + foreach ($this->keys() as $group) { + if (($this->{$group} instanceof NotificationsBag && $this->{$group}->count() > 0) || + (is_array($this->{$group}) && count($this->{$group}) > 0) + ) { + $out .= "
  • ". + implode("
  • ", $this->{$group}->toArray()). + "
  • "; + } else { + $out .= "
  • ".$this->{$group}."
  • "; + } + } + return $out; + } + if (!isset($this->{$group}) || $this->{$group}->count() == 0) { + return ''; + } + return "
  • ". + implode( + "
  • ", + $this->{$group}->toArray() + ). + "
  • "; + } + + /** + * onChange hook + */ + public function onChange() + { + } +} diff --git a/src/classes/accessories/OrderedFunctions.php b/src/classes/accessories/OrderedFunctions.php new file mode 100644 index 00000000..3fc624d1 --- /dev/null +++ b/src/classes/accessories/OrderedFunctions.php @@ -0,0 +1,155 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### ACCESSORIES #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Accessories; + +use Degami\Basics\DataBag; + +/** + * class for maintaining ordered list of functions + */ +class OrderedFunctions extends DataBag +{ + + /** @var null|string sort function name */ + private $sort_callback = null; + + /** @var string type */ + private $type; + + /** @var array|null */ + protected array $dataelement_data = []; + + /** + * Class constructor + * + * @param array $array initially contained elements + * @param string $type type of elements + * @param callable $sort_callback sort callback name + */ + public function __construct(array $array, string $type, $sort_callback = null) + { + parent::__construct($array); + $this->type = $type; + $this->sort_callback = $sort_callback; + $this->sort(); + } + + /** + * sort elements + */ + public function sort() + { + foreach ($this->dataelement_data as &$value) { + if (is_string($value)) { + $value = strtolower(trim($value)); + } elseif (is_array($value) && isset($value[$this->type])) { + $value[$this->type] = strtolower(trim($value[$this->type])); + } + } + + $this->dataelement_data = array_unique($this->dataelement_data, SORT_REGULAR); + + if (!empty($this->sort_callback) && is_callable($this->sort_callback)) { + usort($this->dataelement_data, $this->sort_callback); + } + } + + /** + * rewind pointer position + */ + public function rewind() : void + { + parent::rewind(); + $this->sort(); + } + + /** + * Check if element is present + * + * @param mixed $value value to search + * @return bool TRUE if $value was found + */ + public function hasValue($value): bool + { + return in_array($value, $this->getValues()); + } + + /** + * Check if key is in the array keys + * + * @param int $key key to search + * @return bool TRUE if key was found + */ + public function hasKey(int $key): bool + { + return in_array($key, array_keys($this->dataelement_data)); + } + + /** + * Return element values + * + * @return mixed element values + */ + public function getValues(): array + { + $out = []; + foreach ($this->dataelement_data as $key => $value) { + if (is_array($value) && isset($value[$this->type])) { + $out[] = $value[$this->type]; + } else { + $out[] = $value; + } + } + return $out; + } + + /** + * Adds a new element to array elements + * + * @param mixed $value element to add + * @return self + */ + public function addElement($value): OrderedFunctions + { + $this->dataelement_data[] = $value; + $this->sort(); + + return $this; + } + + /** + * removes an element from array elements + * + * @param mixed $value element to remove + * @return self + */ + public function removeElement($value): OrderedFunctions + { + $this->dataelement_data = array_diff($this->dataelement_data, [$value]); + $this->sort(); + + return $this; + } + + /** + * Element to array + * + * @return array element to array + */ + public function toArray(): array + { + return $this->dataelement_data; + } +} diff --git a/src/classes/accessories/SessionBag.php b/src/classes/accessories/SessionBag.php new file mode 100644 index 00000000..b75a2654 --- /dev/null +++ b/src/classes/accessories/SessionBag.php @@ -0,0 +1,79 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### ACCESSORIES #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Accessories; + +use Degami\Basics\MultiLevelDataBag; +use Degami\PHPFormsApi\FormBuilder; + +/** + * A class to hold session values + */ + +class SessionBag extends MultiLevelDataBag +{ + + /** + * Class constructor + * + * @param mixed $data data to add + * @param MultiLevelDataBag $parent parent node + */ + public function __construct($data = [], $parent = null) + { + if (!$parent && isset($_SESSION[self::getSessionIdentifier()])) { + $data = unserialize($_SESSION[self::getSessionIdentifier()]); + } + parent::__construct($data, $parent); + } + + /** + * stores data to session + */ + public function onChange() + { + $_SESSION[self::getSessionIdentifier()] = serialize($this->toArray()); + } + + /** + * Get session identified + * + * @return string + */ + public static function getSessionIdentifier(): ?string + { + static $session_identifier = null; + if (!$session_identifier) { + if (isset($_SESSION['sessionbag_identifier'])) { + return $_SESSION['sessionbag_identifier']; + } + $session_identifier = 'SESS_'.uniqid(); + $_SESSION['sessionbag_identifier'] = $session_identifier; + } + return $session_identifier; + } + + /** + * {@inheritdoc} + */ + public function clear() : self + { + parent::clear(); + if (FormBuilder::sessionPresent()) { + session_destroy(); + session_start(); + } + return $this; + } +} diff --git a/src/classes/containers/Accordion.php b/src/classes/containers/Accordion.php new file mode 100644 index 00000000..8eed6b70 --- /dev/null +++ b/src/classes/containers/Accordion.php @@ -0,0 +1,145 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELD CONTAINERS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Containers; + +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Containers\FieldsContainerMultiple; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Fields\Hidden; + +/** + * an accordion field container + */ +class Accordion extends FieldsContainerMultiple +{ + + /** @var string height style */ + protected $height_style = 'auto'; + + /** @var integer active tab */ + protected $active = '0'; + + /** @var boolean collapsible */ + protected $collapsible = false; + + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + $collapsible = ($this->collapsible) ? 'true':'false'; + $this->addJs( + "\$('#{$id}','#{$form->getId()}').accordion({ + heightStyle: \"{$this->height_style}\", + active: {$this->active}, + collapsible: {$collapsible} + });" + ); + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + $tag = new TagElement([ + 'tag' => 'div', + 'id' => $id, + 'attributes' => $this->attributes, + ]); + + foreach ($this->partitions as $accordionindex => $accordion) { + $insertorder = array_flip($this->insert_field_order[$accordionindex]); + $weights = []; + $order = []; + + $partition_fields = $this->getPartitionFields($accordionindex); + + foreach ($partition_fields as $key => $elem) { + /** @var Field $elem */ + $weights[$key] = $elem->getWeight(); + $order[$key] = isset($insertorder[$key]) ? $insertorder[$key] : PHP_INT_MAX; + } + if (count($partition_fields) > 0) { + array_multisort($weights, SORT_ASC, $order, SORT_ASC, $partition_fields); + } + $tag->addChild(new TagElement([ + 'tag' => 'h3', + 'text' => $this->getText($this->partitions[$accordionindex]['title']), + 'attributes' => [ + 'class' => 'tabel '.( + $this->partitionHasErrors($accordionindex, $form) ? + 'has-errors' : '' + ) + ], + ])); + $inner = new TagElement([ + 'tag' => 'div', + 'id' => $id.'-tab-inner-'.$accordionindex, + 'attributes' => [ + 'class' => 'tab-inner'.( + $this->partitionHasErrors($accordionindex, $form) ? + ' has-errors' : '' + ) + ], + ]); + + // hidden fields are always first + usort($partition_fields, function($fieldA, $fieldB){ + if (is_object($fieldA) && is_a($fieldA, Hidden::class)) { + return -1; + } + if (is_object($fieldB) && is_a($fieldB, Hidden::class)) { + return 1; + } + return 0; + }); + + foreach ($partition_fields as $name => $field) { + /** @var Field $field */ + $inner->addChild($field->renderHTML($form)); + } + $tag->addChild($inner); + } + + return $tag; + } + + /** + * Adds a new accordion + * + * @param string $title accordion title + * @return self + */ + public function addAccordion($title): Accordion + { + return $this->addPartition($title); + } +} diff --git a/src/classes/containers/BulkTable.php b/src/classes/containers/BulkTable.php new file mode 100644 index 00000000..9ee07a32 --- /dev/null +++ b/src/classes/containers/BulkTable.php @@ -0,0 +1,191 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELD CONTAINERS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Containers; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\PHPFormsApi\Exceptions\FormException; +use Degami\PHPFormsApi\Form; +use Degami\Basics\Html\TagElement; +use Degami\Basics\Html\TagList; + +/** + * a table field container + */ +class BulkTable extends TableContainer +{ + + /** @var array available operations list */ + protected $operations = []; + + /** + * Get defined operations + * + * @return array $operations array of callable + */ + public function &getOperations(): array + { + return $this->operations; + } + + /** + * Add operation to operations array + * + * @param string $key key + * @param string $label label + * @param mixed $operation operation + * @return self + */ + public function addOperation(string $key, string $label, $operation): BulkTable + { + $this->operations[$key] = ['key'=>$key,'label'=>$label,'op'=>$operation]; + + return $this; + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * @throws FormException + */ + public function preRender(Form $form) + { + if ($this->pre_rendered) { + return; + } + $id = $this->getHtmlId(); + $this->setTableHeader(array_merge([' '], $this->getTableHeader())); + for ($i = 0; $i < $this->numRows(); $i++) { + foreach ($this->getPartitionFields($i) as $key => $field) { + /** @var Field $field */ + $field->setName($this->getName()."[rows][$i][{$field->getName()}]"); + } + $this->addField( + $this->getName()."[rows][$i][row_enabled]", + [ + 'type' => 'checkbox', + 'value' => 0, + 'default_value' => 1, + 'attributes' => [ + 'class' => 'checkbox-row', + ], + 'weight' => -100, + ], + $i + ); + } + + $this->addJs( + "\$('.btn.selAll','#{$id}_actions').click(function(evt){evt.preventDefault(); + \$('.checkbox-row','#{$id}').each(function(index,elem){ $(elem)[0].checked = true; }); });" + ); + $this->addJs( + "\$('.btn.deselAll','#{$id}_actions').click(function(evt){evt.preventDefault(); + \$('.checkbox-row','#{$id}').each(function(index,elem){ $(elem)[0].checked = false; }); });" + ); + $this->addJs( + "\$('.btn.inverSel','#{$id}_actions').click(function(evt){evt.preventDefault(); + \$('.checkbox-row','#{$id}').each(function(index,elem){ \$(elem)[0].checked = !\$(elem)[0].checked; }); });" + ); + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * @return BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + $tag = new TagList(); + $prefix = new TagElement(['tag' => 'div']); + $select = new TagElement([ + 'tag' => 'select', + 'name' => $this->getName().'[op]', + ]); + $prefix->addChild($select); + + foreach ($this->getOperations() as $operation) { + $select->addChild(new TagElement([ + 'tag' => 'option', + 'value' => $operation['key'], + 'text' => $operation['label'], + ])); + } + + $tag->addChild($prefix); + + $tag->addChild(parent::renderField($form)); + + $suffix = new TagElement([ + 'tag' => 'div', + 'id' => $id.'_actions', + 'attributes' => ['class' => 'bulk_actions'], + ]); + + $suffix + ->addChild(new TagElement([ + 'tag' => 'a', + 'attributes' => ['href' => '#', 'class' => 'btn selAll'], + 'text' => $this->getText('Select all'), + ])) + ->addChild(' - ') + ->addChild(new TagElement([ + 'tag' => 'a', + 'attributes' => ['href' => '#', 'class' => 'btn deselAll'], + 'text' => $this->getText('Deselect all'), + ])) + ->addChild(' - ') + ->addChild(new TagElement([ + 'tag' => 'a', + 'attributes' => ['href' => '#', 'class' => 'btn inverSel'], + 'text' => $this->getText('Invert selection'), + ])); + + $tag->addChild($suffix); + + return $tag; + } + + /** + * {@inheritdocs} + * + * @param mixed $values value to set + * @return null + */ + public function processValue($values) + { + foreach ($values[$this->getName()]['rows'] as $k => $row) { + if (!isset($row['row_enabled']) || $row['row_enabled'] != 1) { + unset($values[$this->getName()]['rows'][$k]); + } else { + unset($values[$this->getName()]['rows'][$k]['row_enabled']); + } + } + + $operation_key = $values[$this->getName()]['op']; + $callable = $this->operations[ $operation_key ]['op']; + foreach ($values[$this->getName()]['rows'] as $args) { + call_user_func_array($callable, $args); + } + + parent::processValue($values); + } +} diff --git a/src/classes/containers/Fieldset.php b/src/classes/containers/Fieldset.php new file mode 100644 index 00000000..8e47eb4e --- /dev/null +++ b/src/classes/containers/Fieldset.php @@ -0,0 +1,162 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELD CONTAINERS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Containers; + +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\FieldsContainer; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Fields\Hidden; + +/** + * a fieldset field container + */ +class Fieldset extends FieldsContainer +{ + + /** + * collapsible flag + * + * @var boolean + */ + protected $collapsible = false; + + /** + * collapsed flag + * + * @var boolean + */ + protected $collapsed = false; + + /** + * inner div attributes + * + * @var array + */ + protected $inner_attributes = []; + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + static $js_collapsible_added = false; + if ($this->pre_rendered == true) { + return; + } + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + if ($this->collapsible) { + $this->attributes['class'] .= ' collapsible'; + if ($this->collapsed) { + $this->attributes['class'] .= ' collapsed'; + } else { + $this->attributes['class'] .= ' expanded'; + } + + if (!$js_collapsible_added) { + $this->addJs( + " + \$('fieldset.collapsible').find('legend:not(\".collapsible-attached\")').css({'cursor':'pointer'}).click(function(evt){ + evt.preventDefault(); + var \$this = \$(this); + \$this.parent().find('.fieldset-inner').toggle( 'blind', {}, 500, function(){ + if(\$this.parent().hasClass('expanded')){ + \$this.parent().removeClass('expanded').addClass('collapsed'); + }else{ + \$this.parent().removeClass('collapsed').addClass('expanded'); + } + }); + }).addClass('collapsible-attached'); + \$('fieldset.collapsible.collapsed .fieldset-inner').hide();" + ); + $js_collapsible_added = true; + } + } + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return TagElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + $insertorder = array_flip($this->insert_field_order); + $weights = []; + $order = []; + foreach ($this->getFields() as $key => $elem) { + /** @var Field $elem */ + $weights[$key] = $elem->getWeight(); + $order[$key] = $insertorder[$key] ?? PHP_INT_MAX; + } + if (count($this->getFields()) > 0) { + array_multisort($weights, SORT_ASC, $order, SORT_ASC, $this->getFields()); + } + + $tag = new TagElement([ + 'tag' => 'fieldset', + 'id' => $id, + 'attributes' => $this->attributes, + ]); + if (!empty($this->title)) { + $tag->addChild(new TagElement([ + 'tag' => 'legend', + 'text' => $this->getText($this->title), + ])); + } + + $inner_attributes = $this->inner_attributes; + if (!isset($inner_attributes['class'])) { + $inner_attributes['class'] = ''; + } + $inner_attributes['class'] .= ' fieldset-inner'; + $inner_attributes['class'] = trim($inner_attributes['class']); + + $inner = new TagElement([ + 'tag' => 'div', + 'attributes' => $inner_attributes, + ]); + + // hidden fields are always first + usort($this->getFields(), function($fieldA, $fieldB){ + if (is_object($fieldA) && is_a($fieldA, Hidden::class)) { + return -1; + } + if (is_object($fieldB) && is_a($fieldB, Hidden::class)) { + return 1; + } + return 0; + }); + + foreach ($this->getFields() as $name => $field) { + /** @var Field $field */ + $inner->addChild($field->renderHTML($form)); + } + + $tag->addChild($inner); + return $tag; + } +} diff --git a/src/classes/containers/Nestable.php b/src/classes/containers/Nestable.php new file mode 100644 index 00000000..ea6ebda2 --- /dev/null +++ b/src/classes/containers/Nestable.php @@ -0,0 +1,448 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELD CONTAINERS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Containers; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\FieldsContainer; +use Degami\PHPFormsApi\Traits\Containers; +use Degami\PHPFormsApi\Exceptions\FormException; + +/** + * a nestable field container + */ +class Nestable extends FieldsContainer +{ + use Containers; + + /** @var integer level*/ + public $level = 0; + + /** @var integer number of children */ + public $child_num = 0; + + /** @var string tag for list */ + public $tag = 'ol'; + + /** @var string css class for list */ + public $tag_class = 'dd-list'; + + /** @var array children */ + public $children = []; + + /** @var TagContainer panel */ + public $fields_panel = null; + + /** @var integer maximum depth */ + public $maxDepth = 5; + + /** @var integer group counter */ + public static $group_counter = 0; + + /** @var boolean css has been rendered flag */ + public static $css_rendered = false; + + /** @var integer group */ + public $group = 0; + + /** + * Nestable constructor. + * + * @param array $options + * @param null $name + * + * @throws FormException + */ + public function __construct($options = [], $name = null) + { + parent::__construct($options, $name); + $this->fields_panel = new TagContainer( + [ + 'type' => 'tag_container', + 'tag' => 'div', + 'container_class' => '', + 'container_tag' => '', + 'prefix' => '
    +
     
    +
    ', + 'suffix' => '
    +
    ', + 'attributes' => ['class' => 'level-'.$this->level], + ], + 'panel-'.$this->getLevel().'-'.$this->getName() + ); + + parent::addField($this->fields_panel->getName(), $this->fields_panel); + + $this->group = Nestable::$group_counter++; + } + + /** + * Get level + * + * @return int + */ + public function getLevel(): int + { + return $this->level; + } + + /** + * Add child + * + * @param ?string $tag + * @param ?string $tag_class + * + * @return mixed + * @throws FormException + */ + public function addChild($tag = null, $tag_class = null): Nestable + { + if ($tag == null) { + $tag = $this->tag; + } + if ($tag_class == null) { + $tag_class = $this->tag_class; + } + + $nextchild = new Nestable( + [ + 'type' => 'nestable', + 'level' => $this->level+1, + 'tag' => $tag, + 'container_class' => '', + 'container_tag' => '', + 'attributes' => ['class' => $tag_class], + 'child_num' => $this->numChildren(), + ], + $this->getName().'-leaf-'. $this->numChildren() + ); + + $this->children[] = $nextchild; + parent::addField($nextchild->getName(), $nextchild); + + return $this->children[$this->numChildren()-1]; + } + + /** + * Get children count + * + * @return int + */ + public function numChildren(): int + { + return count($this->getChildren()); + } + + /** + * Check if there are children + * + * @return bool + */ + public function hasChildren(): bool + { + return $this->numChildren() > 0; + } + + /** + * Get a child + * + * @param $num_child + * + * @return bool|mixed + */ + public function &getChild($num_child): bool + { + $out = isset($this->children[$num_child]) ? $this->children[$num_child] : false; + return $out; + } + + /** + * Get all children + * + * @return array + */ + public function &getChildren(): array + { + return $this->children; + } + + /** + * {@inheritdoc} + * + * @param string $name + * @param mixed $field + * + * @return $this|Field + * @throws FormException + */ + public function addField(string $name, $field) : Field + { + $field = $this->getFieldObj($name, $field); + if (!($field instanceof Nestable) && $this->isFieldContainer($field)) { + throw new FormException("Can't add a fields_container to a tree_container.", 1); + } + + $this->fields_panel->addField($name, $field); + return $this; + } + + /** + * remove field from form + * + * @param string $name field name + * @return self + */ + public function removeField(string $name) : FieldsContainer + { + $this->fields_panel->removeField($name); + return $this; + } + + /** + * {@inheritdoc} + * + * @param mixed $values + */ + public function processValue($values) + { + parent::processValue($values); + if (isset($values[$this->getName()])) { + $this->value = json_decode($values[$this->getName()], true); + } + } + + /** + * Get a panel + * + * @param string $nestable_id + * @return bool|TagContainer|null + */ + private function getPanelById(string $nestable_id) + { + if ($this->getHtmlId() == $nestable_id) { + return $this->fields_panel; + } + foreach ($this->getChildren() as $key => $child) { + $return = $child->getPanelById($nestable_id); + if ($return != false) { + return $return; + } + } + return false; + } + + /** + * create values array + * + * @param array $tree tree + * @param Nestable $nestable_field field + * @return array values array + */ + private static function createValuesArray(array $tree, Nestable $nestable_field): array + { + $out = []; + $panel = $nestable_field->getPanelById($tree['id']); + if ($panel instanceof FieldsContainer) { + $out['value'] = $panel->getValues(); + if (isset($tree['children'])) { + foreach ($tree['children'] as $child) { + $out['children'][] = Nestable::createValuesArray($child, $nestable_field); + } + } + } + return $out; + } + + /** + * {@inheritdoc} + * + * @return mixed + */ + public function getValues() + { + if ($this->value) { + $out = []; + foreach ($this->value as $tree) { + $out[] = Nestable::createValuesArray($tree, $this); + } + return $out; + } + return parent::getValues(); + } + + /** + * {@inheritdoc} + * + * @param Form $form + * + * @return string|BaseElement + */ + public function renderField(Form $form) + { + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + $this->attributes['class'] .= ' '.$this->tag_class; + $id = $this->getHtmlId(); + + $attributes = $this->getAttributes(); + $out = ""; + if ($this->getLevel() == 0) { + $out .= "
    <{$this->tag}{$attributes}>"; + } + $out .= '
  • '; + $out .= $this->fields_panel->renderHTML($form); + if ($this->hasChildren()) { + $out .= "<{$this->tag} {$attributes}>"; + $children = $this->getChildren(); + foreach ($children as $key => &$child) { + $out .= $child->renderHTML($form); + } + $out .= "tag}>"; + } + $out .= '
  • '; + + if ($this->getLevel() == 0) { + $out .= "tag}> +
    + "; + } + + return $out; + } + + /** + * {@inheritdoc} + * + * @param Form $form + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + if ($this->getLevel() == 0) { + $this->addJs( + "\$('#{$id}','#{$form->getId()}').data('output', \$('#{$id}-output')); + \$('#{$id}','#{$form->getId()}').nestable({group: {$this->group}, maxDepth: {$this->maxDepth} }) + .on('change', function(e){ + var list = e.length ? e : $(e.target), + output = list.data('output'); + if (window.JSON) { + output.val(window.JSON.stringify(list.nestable('serialize'))); + } else { + output.val('JSON browser support required for this.'); + } + }) + .trigger('change');" + ); + + if (!Nestable::$css_rendered) { + $this->addCss( + ' +.dd { position: relative; display: block; margin: 0; padding: 0; list-style: none; font-size: 13px; line-height: 20px; } + +.dd-list { display: block; position: relative; margin: 0; padding: 0; list-style: none; } +.dd-list .dd-list { padding-left: 30px; } +.dd-collapsed .dd-list { display: none; } +.dd-item,.dd-empty,.dd-placeholder { + display: block; position: relative; margin: 10px 0 0 0; padding: 2px 0 0 0; + min-height: 20px; font-size: 13px; line-height: 20px; +} + +.dd-handle { display: block; margin: 5px 0; padding: 5px 10px; color: #333; text-decoration: none; font-weight: bold; + border: 1px solid #ccc; + background: #fafafa; + background: -webkit-linear-gradient(top, #fafafa 0%, #eee 100%); + background: -moz-linear-gradient(top, #fafafa 0%, #eee 100%); + background: linear-gradient(top, #fafafa 0%, #eee 100%); + -webkit-border-radius: 3px; + border-radius: 3px; + box-sizing: border-box; -moz-box-sizing: border-box; +} +.dd-handle:hover { color: #2ea8e5; background: #fff; } + +.dd-item > button { display: block; position: relative; cursor: pointer; z-index: 20; float: left; width: 25px; + height: 20px; margin: 5px 0; padding: 0; text-indent: 100%; white-space: nowrap; overflow: hidden; + border: 0; background: transparent; font-size: 12px; line-height: 1; text-align: center; + font-weight: bold; } +.dd-item > button:before { content: \'+\'; display: block; position: absolute; width: 100%; text-align: center; + text-indent: 0; } +.dd-item > button[data-action="collapse"]:before { content: \'-\'; } + +.dd-placeholder, +.dd-empty { margin: 5px 0; padding: 0; min-height: 30px; background: #f2fbff; border: 1px dashed #b6bcbf; + box-sizing: border-box; -moz-box-sizing: border-box; display: block; } +.dd-empty { + border: 1px dashed #bbb; min-height: 100px; background-color: #e5e5e5; + background-image: -webkit-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff), + -webkit-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff); + background-image: -moz-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff), + -moz-linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff); + background-image: linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff), + linear-gradient(45deg, #fff 25%, transparent 25%, transparent 75%, #fff 75%, #fff); + background-size: 60px 60px; + background-position: 0 0, 30px 30px; +} + +.dd-dragel { position: absolute; pointer-events: none; z-index: 9999; } +.dd-dragel > .dd-panel > .dd-item .dd-handle { margin-top: 0; } +.dd-dragel .dd-handle { + -webkit-box-shadow: 2px 4px 6px 0 rgba(0,0,0,.1); + box-shadow: 2px 4px 6px 0 rgba(0,0,0,.1); +} + +.dd-panel{ position: relative; } +.dd-content { display: block; min-height: 30px; margin: 5px 0; padding: 5px 10px 5px 40px; color: #333; + text-decoration: none; font-weight: bold; border: 1px solid #ccc; + background: #fafafa; + background: -webkit-linear-gradient(top, #fafafa 0%, #eee 100%); + background: -moz-linear-gradient(top, #fafafa 0%, #eee 100%); + background: linear-gradient(top, #fafafa 0%, #eee 100%); + -webkit-border-radius: 3px; + border-radius: 3px; + box-sizing: border-box; -moz-box-sizing: border-box; +} +.dd-content:hover { color: #2ea8e5; background: #fff; } +.dd-dragel > .dd-item > .dd-panel > .dd-content { margin: 0; } +.dd-item > button { margin-left: 30px; } + +.dd-handle { position: absolute; margin: 0; left: 0; top: 0; cursor: pointer; width: 30px; text-indent: 100%; + white-space: nowrap; overflow: hidden; + border: 1px solid #aaa; + background: #ddd; + background: -webkit-linear-gradient(top, #ddd 0%, #bbb 100%); + background: -moz-linear-gradient(top, #ddd 0%, #bbb 100%); + background: linear-gradient(top, #ddd 0%, #bbb 100%); + border-top-right-radius: 0; + border-bottom-right-radius: 0; + height: 100%; +} +.dd-handle:before { content: \'≡\'; display: block; position: absolute; left: 0; top: 3px; width: 100%; + text-align: center; text-indent: 0; color: #fff; font-size: 20px; font-weight: normal; } +.dd-handle:hover { background: #ddd; } +' + ); + Nestable::$css_rendered = true; + } + } + + parent::preRender($form); + } +} diff --git a/src/classes/containers/Repeatable.php b/src/classes/containers/Repeatable.php new file mode 100644 index 00000000..8de7bb4a --- /dev/null +++ b/src/classes/containers/Repeatable.php @@ -0,0 +1,466 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELD CONTAINERS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Containers; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\PHPFormsApi\Abstracts\Base\FieldsContainer; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Containers\FieldsContainerMultiple; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Exceptions\FormException; +use Degami\PHPFormsApi\Fields\Hidden; + +/** + * a field container with a repeatable group of fields + */ +class Repeatable extends FieldsContainerMultiple +{ + /** @var integer number of initial repetitions */ + protected $num_reps = null; + + /** @var array fields to repeat */ + private $repeatable_fields = []; + + /** @var array field order */ + private $repeatable_insert_field_order = []; + + /** @var array default values */ + protected $default_value = []; + + /** + * {@inheritdocs} + * + * @param array $options options + * @param string|null $name field name + */ + public function __construct(array $options = [], $name = null) + { + parent::__construct($options, $name); + if (is_array($options['default_value'])) { + $this->default_value = $options['default_value']; + $this->num_reps = count($this->default_value); + } + } + + /** + * Override add_field + * + * @param string $name + * @param mixed $field + * @return $this|mixed + * @throws FormException + */ + public function addField(string $name, $field): Field + { + $field = $this->getFieldObj($name, $field); + + if ($this->isFieldContainer($field)) { + throw new FormException('Can\'t nest field_containers into repeteables'); + } + + $this->repeatable_fields[$name] = $field; + $this->repeatable_insert_field_order[] = $name; + + if (!method_exists($field, 'onAddReturn')) { + if ($this->isFieldContainer($field)) { + return $field; + } + return $this; + } + if ($field->onAddReturn() == 'this') { + return $field; + } + return $this; + } + + /** + * {@inheritdoc} + * + * @param $name + * @param integer $partitions_index unused + * @return self + */ + public function removeField(string $name): FieldsContainer + { + unset($this->repeatable_fields[$name]); + if (($key = array_search($name, $this->repeatable_insert_field_order)) !== false) { + unset($this->repeatable_insert_field_order[$key]); + } + return $this; + } + + /** + * {@inheritdoc} + * + * @param array $request + * @throws FormException + */ + public function alterRequest(array &$request) + { + $id = $this->getHtmlId(); + if (isset($request[ $id.'-numreps' ])) { + $this->num_reps = (int) $request[ $id.'-numreps' ]; + if ($this->num_reps < 0) { + $this->num_reps = 1; + } + } + for ($i = 0; $i < $this->num_reps; $i++) { + foreach ($this->repeatable_fields as $rfield) { + /** + * @var Field $field + */ + $field = clone $rfield; + $field + ->setId($this->getName().'_'.$i.'_'.$field->getName()) + ->setName($this->getName().'['.$i.']['.$field->getName().']'); + + if (isset($this->default_value[$i][$rfield->getName()])) { + $field->setValue($this->default_value[$i][$rfield->getName()]); + } + parent::addField($field->getName(), $field, $i); + } + } + parent::alterRequest($request); + } + + /** + * {@inheritdoc} + * + * @param mixed $values + */ + public function processValue($values) + { + foreach ($this->getFields() as $i => $field) { + /** + * @var Field $field + */ + $field->processValue(static::traverseArray($values, $field->getName())); + } + //parent::processValue($values); + } + + /** + * {@inheritdoc} + * + * @return mixed + */ + public function getValue() + { + $out = []; + foreach ($this->getFields() as $i => $field) { + /** + * @var Field $field + */ + if ($field->isAValue() == true) { + $key = str_replace($this->getName(), "", $field->getName()); + if (preg_match('/\[([0-9]+)\]\[(.*?)\]/i', $key, $matches)) { + $out[$matches[1]][$matches[2]] = $field->getValues(); + } else { + $out[$field->getName()] = $field->getValues(); + } + } + } + return $out; + } + + /** + * {@inheritdoc} + * + * @return mixed + */ + public function getValues() + { + return is_array($this->getValue()) ? $this->getValue() : [$this->getValue()]; + } + + /** + * {@inheritdocs} + */ + public function isValid() : bool + { + if ($this->num_reps == 0) { + return true; + } + return parent::isValid(); + } + + /** + * {@inheritdoc} + * + * @param Form $form + */ + public function preRender(Form $form) + { + static $selectorPluginAdded = false; + + if (!$this->pre_rendered) { + $id = $this->getHtmlId(); + + $repetatable_fields = "
    \n
    "; + $fake_form = new Form(); + foreach ($this->repeatable_fields as $rfield) { + /** + * @var Field $field + */ + $field = clone $rfield; + $field + ->setId($this->getHtmlId().'_{x}_'.$field->getName()) + ->setName($this->getName().'[{x}]['.$field->getName().']'); + $repetatable_fields .= $field->renderHTML($fake_form); + } + $repetatable_fields .= "×\n"; + $repetatable_fields .= "
    "; + $repetatable_fields = str_replace("\n", "", $repetatable_fields); + + $js = array_filter(array_map('trim', $fake_form->getJs())); + if (!empty($js)) { + foreach ($js as &$js_string) { + if ($js_string[strlen($js_string)-1] == ';') { + $js_string = substr($js_string, 0, strlen($js_string)-1); + } + } + } + if (!empty($js)) { + $js = "eval( ".implode(";", $js).".replace( new RegExp('\{x\}', 'g'), newrownum ) );\n"; + } else { + $js = ''; + } + + $this->addCss( + "#{$id} .repeatable-row{ + margin: 10px 0; + padding: 10px; + border: solid 1px #cecece; + position: relative; + }" + ); + $this->addCss( + "#{$id} .repeatable-row .remove-btn{ + position: absolute; + top: 5px; + right: 10px; + z-index: 10; + }" + ); + + $this->addJs( + "\$('#{$id}').delegate('.remove-btn','click',function(evt){ + evt.preventDefault(); + var \$parent = \$(this).closest('.repeatable-row'); + + \$afterSiblings = \$parent.parent().nextAll(); + \$parent.remove(); + + \$afterSiblings.each(function(key, element) { + var regexp = /".str_replace(["[","]","/"], ['\[','\]','\/'], $this->getName())."\[([0-9]+)\]/; + var \$inputs = \$(element).find('input, textarea, select').regex(regexp, $.fn.attr, ['name']); + \$inputs.each(function(i, inp) { + var nameMatches = \$(inp).attr('name').match(/^".str_replace(["[","]","/"], ['\[','\]','\/'], $this->getName())."\[([0-9]+)\](.*?)$/); + var newNumber = parseInt(nameMatches[1]) - 1; + var newName = '".$this->getName()."['+(newNumber)+']'+nameMatches[2]; + \$(inp).attr('name', newName) + }); + }); + + var \$target = $('.fields-target:eq(0)'); + var newrownum = \$target.find('.repeatable-row').length; + \$('input[name=\"{$id}-numreps\"]').val(newrownum); + });" + ); + $this->addJs( + "\$('.btnaddmore', '#{$id}').click(function(evt){ + evt.preventDefault(); + var \$target = \$('.fields-target:eq(0)', '#{$id}'); + var newrownum = \$target.find('.repeatable-row').length; + \$( '{$repetatable_fields}'.replace( new RegExp('\{x\}', 'g'), newrownum ) ).appendTo( \$target ); + \$('input[name=\"{$id}-numreps\"]').val(newrownum + 1); + {$js} + });" + ); + + + if (!$selectorPluginAdded) { + $this->addJs( + "\$.fn.regex = function(pattern, fn, fn_a){ + var fn = fn || $.fn.text; + return this.filter(function() { + return pattern.test(fn.apply($(this), fn_a)); + }); + };" + ); + $selectorPluginAdded = true; + } + } + + parent::preRender($form); + } + + + /** + * {@inheritdocs} + * + * @param Form $form form object + * @return string|BaseElement the field html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + $tag = new TagElement([ + 'tag' => 'div', + 'id' => $id, + 'attributes' => $this->attributes, + ]); + + $target = new TagElement([ + 'tag' => 'div', + 'attributes' => ['class' => 'fields-target'], + ]); + + $tag->addChild($target); + + foreach ($this->partitions as $partitionindex => $tab) { + $insertorder = array_flip($this->insert_field_order[$partitionindex]); + $weights = []; + $order = []; + + $partition_fields = $this->getPartitionFields($partitionindex); + + foreach ($partition_fields as $key => $elem) { + /** @var Field $elem */ + $weights[$key] = $elem->getWeight(); + $order[$key] = isset($insertorder[$key]) ? $insertorder[$key] : PHP_INT_MAX; + } + if (count($partition_fields) > 0) { + array_multisort($weights, SORT_ASC, $order, SORT_ASC, $partition_fields); + } + + $inner = new TagElement([ + 'tag' => 'div', + 'id' => $id.'-row-'.$partitionindex, + ]); + $target->addChild($inner); + + $repeatablerow = new TagElement([ + 'tag' => 'div', + 'attributes' => ['class' => 'repeatable-row'], + ]); + $inner->addChild($repeatablerow); + + // hidden fields are always first + usort($partition_fields, function($fieldA, $fieldB){ + if (is_object($fieldA) && is_a($fieldA, Hidden::class)) { + return -1; + } + if (is_object($fieldB) && is_a($fieldB, Hidden::class)) { + return 1; + } + return 0; + }); + + foreach ($partition_fields as $name => $field) { + /** @var Field $field */ + $repeatablerow->addChild($field->renderHTML($form)); + } + $repeatablerow->addChild( + "×\n" + ); + } + + $tag->addChild(new TagElement([ + 'tag' => 'input', + 'type' => 'hidden', + 'name' => $id.'-numreps', + 'value' => $this->num_reps, + ])); + $tag->addChild(new TagElement([ + 'tag' => 'button', + 'id' => $id.'-btn-addmore', + 'attributes' => ['class' => 'btn btnaddmore'], + 'text' => $this->getText('+'), + 'has_close' => true, + 'value_needed' => false, + ])); + + return $tag; + } + + + /** + * render the field + * + * @param Form $form form object + * + * @return string the field html + */ + public function renderHTML(Form $form) : string + { + $id = $this->getHtmlId(); + $output = $this->getElementPrefix(); + $output .= $this->getPrefix(); + + // this container needs a label + if (!empty($this->title)) { + if ($this->tooltip == false) { + $this->label_class .= " label-" . $this->getElementClassName(); + $this->label_class = trim($this->label_class); + $label_class = (!empty($this->label_class)) ? " class=\"{$this->label_class}\"" : ""; + $output .= "\n"; + } else { + if (!in_array('title', array_keys($this->attributes))) { + $this->attributes['title'] = strip_tags($this->getText($this->title)); + } + + $id = $this->getHtmlId(); + $form->addJs("\$('#{$id}','#{$form->getId()}').tooltip();"); + } + } + + + if (!$this->pre_rendered) { + $this->preRender($form); + $this->pre_rendered = true; + } + $output .= $this->renderField($form); + + if (!($this instanceof FieldsContainer)) { + if (!empty($this->description)) { + $output .= "
    {$this->description}
    "; + } + } + if ($form->errorsInline() == true && $this->hasErrors()) { + $output.= '
    '.implode("
    ", $this->getErrors()).'
    '; + } + + $output .= $this->getSuffix(); + $output .= $this->getElementSuffix(); + + if (count($this->event) > 0 && trim($this->getAjaxUrl()) != '') { + foreach ($this->event as $event) { + $eventjs = $this->generateEventJs($event, $form); + $this->addJs($eventjs); + } + } + + // let others alter the output + static::executeAlter("/.*?_".static::getClassNameString()."_render_output_alter$/i", [&$output]); + + // return html string + return $output; + } +} diff --git a/src/classes/containers/SeamlessContainer.php b/src/classes/containers/SeamlessContainer.php new file mode 100644 index 00000000..295988d7 --- /dev/null +++ b/src/classes/containers/SeamlessContainer.php @@ -0,0 +1,69 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELD CONTAINERS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Containers; + +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\FieldsContainer; +use Degami\PHPFormsApi\Fields\Hidden; + +/** + * an hidden field container + */ +class SeamlessContainer extends FieldsContainer +{ + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string the element html + */ + public function renderField(Form $form): string + { + $output = ""; + + $insertorder = array_flip($this->insert_field_order); + $weights = []; + $order = []; + foreach ($this->getFields() as $key => $elem) { + /** @var Field $elem */ + $weights[$key] = $elem->getWeight(); + $order[$key] = isset($insertorder[$key]) ? $insertorder[$key] : PHP_INT_MAX; + } + if (count($this->getFields()) > 0) { + array_multisort($weights, SORT_ASC, $order, SORT_ASC, $this->getFields()); + } + + // hidden fields are always first + usort($this->getFields(), function($fieldA, $fieldB){ + if (is_object($fieldA) && is_a($fieldA, Hidden::class)) { + return -1; + } + if (is_object($fieldB) && is_a($fieldB, Hidden::class)) { + return 1; + } + return 0; + }); + + foreach ($this->getFields() as $name => $field) { + /** @var Field $field */ + $output .= $field->renderHTML($form); + } + + return $output; + } +} diff --git a/src/classes/containers/Sortable.php b/src/classes/containers/Sortable.php new file mode 100644 index 00000000..e3727dc1 --- /dev/null +++ b/src/classes/containers/Sortable.php @@ -0,0 +1,175 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELD CONTAINERS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Containers; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\PHPFormsApi\Abstracts\Base\FieldsContainer; +use Degami\PHPFormsApi\Exceptions\FormException; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Containers\SortableContainer; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Fields\Hidden; + +/** + * a sortable field container + */ +class Sortable extends SortableContainer +{ + + /** + * {@inheritdoc} + * + * @param string $name field name + * @param mixed $field field to add, can be an array or a field subclass + * @throws FormException + */ + public function addField(string $name, $field) : Field + { + //force every field to have its own tab. + $this->deltas[$name] = count($this->getFields()); + return parent::addField($name, $field, $this->deltas[$name]); + } + + /** + * {@inheritdoc} + * + * @param string $name field name + */ + public function removeField(string $name) : FieldsContainer + { + parent::removeField($name, $this->deltas['name']); + unset($this->deltas[$name]); + return $this; + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + $this->addJs( + "\$('#{$id}','#{$form->getId()}').sortable({ + placeholder: \"ui-state-highlight\", + stop: function( event, ui ) { + \$(this).find('input[type=hidden][name*=\"sortable-delta-\"]').each(function(index,elem){ + \$(elem).val(index); + }); + } + });" + ); + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + $handle_position = trim(strtolower($this->getHandlePosition())); + + $tag = new TagElement([ + 'tag' => 'div', + 'id' => $id, + 'attributes' => $this->attributes, + ]); + + foreach ($this->partitions as $partitionindex => $tab) { + $insertorder = array_flip($this->insert_field_order[$partitionindex]); + $weights = []; + $order = []; + + $partition_fields = $this->getPartitionFields($partitionindex); + foreach ($partition_fields as $key => $elem) { + /** @var Field $elem */ + $weights[$key] = $elem->getWeight(); + $order[$key] = isset($insertorder[$key]) ? $insertorder[$key] : PHP_INT_MAX; + } + if (count($partition_fields) > 0) { + array_multisort($weights, SORT_ASC, $order, SORT_ASC, $partition_fields); + } + + $inner = new TagElement([ + 'tag' => 'div', + 'id' => $id.'-sortable-'.$partitionindex, + 'attributes' => ['class' => 'tab-inner ui-state-default'], + ]); + + $tag->addChild($inner); + + if ($handle_position != 'right') { + $inner->addChild(new TagElement([ + 'tag' => 'span', + 'attributes' => [ + 'class' => 'ui-icon ui-icon-arrowthick-2-n-s', + 'style' => 'display: inline-block;' + ], + ])); + } + + $inner_inline = new TagElement([ + 'tag' => 'div', + 'attributes' => ['style' => 'display: inline-block;'], + ]); + $inner->addChild($inner_inline); + + // hidden fields are always first + usort($partition_fields, function($fieldA, $fieldB){ + if (is_object($fieldA) && is_a($fieldA, Hidden::class)) { + return -1; + } + if (is_object($fieldB) && is_a($fieldB, Hidden::class)) { + return 1; + } + return 0; + }); + + foreach ($partition_fields as $name => $field) { + /** @var Field $field */ + $inner_inline->addChild($field->renderHTML($form)); + } + $inner_inline->addChild(new TagElement([ + 'tag' => 'input', + 'type' => 'hidden', + 'name' => $id.'-delta-'.$partitionindex, + 'value' => $partitionindex, + 'has_close' => false, + ])); + if ($handle_position == 'right') { + $inner_inline->addChild(new TagElement([ + 'tag' => 'span', + 'attributes' => [ + 'class' => 'ui-icon ui-icon-arrowthick-2-n-s', + 'style' => 'display: inline-block;float: right;' + ], + ])); + } + } + return $tag; + } +} diff --git a/src/classes/containers/SortableTable.php b/src/classes/containers/SortableTable.php new file mode 100644 index 00000000..68352fdc --- /dev/null +++ b/src/classes/containers/SortableTable.php @@ -0,0 +1,210 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELD CONTAINERS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Containers; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Containers\SortableContainer; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Fields\Hidden; + +/** + * a sortable table rows field container + */ +class SortableTable extends SortableContainer +{ + + /** + * table header + * + * @var array + */ + protected $table_header = []; + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + $this->addJs( + " + \$('#{$id} tbody','#{$form->getId()}').sortable({ + helper: function(e, ui) { + ui.children().each(function() { + \$(this).width($(this).width()); + }); + return ui; + }, + placeholder: \"ui-state-highlight\", + stop: function( event, ui ) { + \$(this).find('input[type=hidden][name*=\"sortable-delta-\"]').each(function(index,elem){ + \$(elem).val(index); + }); + } + });" + ); + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + $handle_position = trim(strtolower($this->getHandlePosition())); + + $tag = new TagElement([ + 'tag' => 'table', + 'id' => $id, + 'attributes' => $this->attributes, + ]); + + if (!empty($this->table_header)) { + if (!is_array($this->table_header)) { + $this->table_header = [$this->table_header]; + } + + $thead = new TagElement(['tag' => 'thead']); + $tag->addChild($thead); + + if ($handle_position != 'right') { + $thead->addChild(new TagElement([ + 'tag' => 'th', + 'text' => ' ', + ])); + } + foreach ($this->table_header as $th) { + $thead->addChild(new TagElement([ + 'tag' => 'th', + 'text' => $this->getText($th), + ])); + } + if ($handle_position == 'right') { + $thead->addChild(new TagElement([ + 'tag' => 'th', 'text' => ' ', + ])); + } + } + + $tbody = new TagElement(['tag' => 'tbody']); + $tag->addChild($tbody); + + foreach ($this->partitions as $trindex => $tr) { + $insertorder = array_flip($this->insert_field_order[$trindex]); + $weights = []; + $order = []; + + $partition_fields = $this->getPartitionFields($trindex); + + foreach ($partition_fields as $key => $elem) { + /** + * @var Field $elem + */ + $weights[$key] = $elem->getWeight(); + $order[$key] = isset($insertorder[$key]) ? $insertorder[$key] : PHP_INT_MAX; + } + if (count($partition_fields) > 0) { + array_multisort($weights, SORT_ASC, $order, SORT_ASC, $partition_fields); + } + + $trow = new TagElement([ + 'tag' => 'tr', + 'id' => $id.'-sortable-'.$trindex, + 'attributes' => [ 'class' => 'tab-inner ui-state-default'], + ]); + $tbody->addChild($trow); + + if ($handle_position != 'right') { + $td = new TagElement([ + 'tag' => 'td', + 'attributes' => [ 'width' => 16, 'style' => 'width: 16px;'], + 'children' => [ + new TagElement([ + 'tag' => 'span', + 'attributes' => [ + 'class' => 'ui-icon ui-icon-arrowthick-2-n-s', + 'style' => 'display: inline-block;' + ], + ]) + ], + ]); + $trow->addChild($td); + } + + // hidden fields are always first + usort($partition_fields, function($fieldA, $fieldB){ + if (is_object($fieldA) && is_a($fieldA, Hidden::class)) { + return -1; + } + if (is_object($fieldB) && is_a($fieldB, Hidden::class)) { + return 1; + } + return 0; + }); + + foreach ($partition_fields as $name => $field) { + /** + * @var Field $field + */ + $fieldhtml = $field->renderHTML($form); + if (trim($fieldhtml) != '') { + $trow->addChild(new TagElement([ + 'tag' => 'td', + 'children' => [ $fieldhtml ], + ])); + } + } + + $trow->addChild(new TagElement([ + 'tag' => 'input', + 'type' => 'hidden', + 'name' => $id.'-delta-'.$trindex, + 'value' => $trindex, + 'has_close' => false, + ])); + if ($handle_position == 'right') { + $td = new TagElement([ + 'tag' => 'td', + 'attributes' => [ 'width' => 16, 'style' => 'width: 16px;'], + 'children' => [ + new TagElement([ + 'tag' => 'span', + 'attributes' => [ + 'class' => 'ui-icon ui-icon-arrowthick-2-n-s', + 'style' => 'display: inline-block;' + ], + ]) + ], + ]); + $trow->addChild($td); + } + } + + return $tag; + } +} diff --git a/src/classes/containers/TableContainer.php b/src/classes/containers/TableContainer.php new file mode 100644 index 00000000..fe7c0eea --- /dev/null +++ b/src/classes/containers/TableContainer.php @@ -0,0 +1,263 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELD CONTAINERS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Containers; + +use Degami\PHPFormsApi\Exceptions\FormException; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Containers\FieldsContainerMultiple; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Fields\Hidden; + +/** + * a table field container + */ +class TableContainer extends FieldsContainerMultiple +{ + + /** + * table header + * + * @var array + */ + protected $table_header = []; + + /** + * attributes for TRs or TDs + * + * @var array + */ + protected $col_row_attributes = []; + + /** + * table header attributes + * + * @var array + */ + protected $thead_attributes = []; + + /** + * table body attributes + * + * @var array + */ + protected $tbody_attributes = []; + + + /** + * Set table header array + * + * @param array $table_header table header elements array + * @return self + */ + public function setTableHeader(array $table_header): TableContainer + { + $this->table_header = $table_header; + return $this; + } + + /** + * Get table header array + * + * @return array table header array + */ + public function getTableHeader(): array + { + return $this->table_header; + } + + /** + * Set rows / cols attributes array + * + * @param array $col_row_attributes attributes array + * @return self + */ + public function setColRowAttributes(array $col_row_attributes): TableContainer + { + $this->col_row_attributes = $col_row_attributes; + return $this; + } + + /** + * Get rows / cols attributes array + * + * @return array attributes array + */ + public function getColRowAttributes(): array + { + return $this->col_row_attributes; + } + + /** + * Add a new table row + * + * @return self + */ + public function addRow(): TableContainer + { + $this->addPartition('table_row_'.$this->numPartitions()); + return $this; + } + + /** + * Return number of table rows + * + * @return int number of table rows + */ + public function numRows(): int + { + return $this->numPartitions(); + } + + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string the element html + * @throws FormException + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + $table_matrix = []; + $rows = 0; + + foreach ($this->partitions as $trindex => $tr) { + $table_matrix[$rows] = []; + $cols = 0; + foreach ($this->getPartitionFields($trindex) as $name => $field) { + $table_matrix[$rows][$cols] = ''; + if (isset($this->col_row_attributes[$rows][$cols])) { + if (is_array($this->col_row_attributes[$rows][$cols])) { + $this->col_row_attributes[$rows][$cols] = $this->getAttributesString( + $this->col_row_attributes[$rows][$cols] + ); + } + $table_matrix[$rows][$cols] = $this->col_row_attributes[$rows][$cols]; + } + $cols++; + } + $rows++; + } + + $tag = new TagElement([ + 'tag' => 'table', + 'id' => $id, + 'attributes' => $this->attributes, + ]); + + if (!empty($this->table_header)) { + if (!is_array($this->table_header)) { + $this->table_header = [$this->table_header]; + } + + $thead = new TagElement([ + 'tag' => 'thead', + 'attributes' => $this->thead_attributes, + ]); + $tag->addChild($thead); + + foreach ($this->table_header as $th) { + if (is_array($th)) { + $thead->addChild(new TagElement([ + 'tag' => 'th', + 'text' => $this->getText($th['value']), + 'attributes' => $th['attributes'], + ])); + } else { + $thead->addChild(new TagElement([ + 'tag' => 'th', + 'text' => $this->getText($th), + ])); + } + } + } + + $tbody = new TagElement([ + 'tag' => 'tbody', + 'attributes' => $this->tbody_attributes, + ]); + $tag->addChild($tbody); + + $rows = 0; + foreach ($this->partitions as $trindex => $tr) { + $insertorder = array_flip($this->insert_field_order[$trindex]); + $weights = []; + $order = []; + foreach ($this->getPartitionFields($trindex) as $key => $elem) { + /** @var \Degami\PHPFormsApi\Abstracts\Base\Field $elem */ + $weights[$key] = $elem->getWeight(); + $order[$key] = isset($insertorder[$key]) ? $insertorder[$key] : PHP_INT_MAX; + } + if (count($this->getPartitionFields($trindex)) > 0) { + $partition_fields = $this->getPartitionFields($trindex); + array_multisort($weights, SORT_ASC, $order, SORT_ASC, $partition_fields); + $this->setPartitionFields($partition_fields, $trindex); + } + + $trow = new TagElement([ + 'tag' => 'tr', + 'id' => $id.'-row-'.$trindex, + ]); + $tbody->addChild($trow); + + // hidden fields are always first + usort($this->getPartitionFields($trindex), function($fieldA, $fieldB){ + if (is_object($fieldA) && is_a($fieldA, Hidden::class)) { + return -1; + } + if (is_object($fieldB) && is_a($fieldB, Hidden::class)) { + return 1; + } + return 0; + }); + + $cols = 0; + foreach ($this->getPartitionFields($trindex) as $name => $field) { + /** + * @var \Degami\PHPFormsApi\Abstracts\Base\Field $field + */ + $fieldhtml = $field->renderHTML($form); + if (trim($fieldhtml) != '') { + $td_attributes = ''; + if (!empty($table_matrix[$rows][$cols])) { + $td_attributes = $table_matrix[$rows][$cols]; + } + $trow->addChild("".$fieldhtml."\n"); + } + $cols++; + } + $rows++; + } + + return $tag; + } +} diff --git a/src/classes/containers/Tabs.php b/src/classes/containers/Tabs.php new file mode 100644 index 00000000..fa62a387 --- /dev/null +++ b/src/classes/containers/Tabs.php @@ -0,0 +1,127 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELD CONTAINERS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Containers; + +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Containers\FieldsContainerMultiple; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Fields\Hidden; + +/** + * a "tabbed" field container + */ +class Tabs extends FieldsContainerMultiple +{ + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + $this->addJs("\$('#{$id}','#{$form->getId()}').tabs();"); + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + $tag = new TagElement([ + 'tag' => 'div', + 'id' => $id, + 'attributes' => $this->attributes, + ]); + + $tab_links = new TagElement(['tag' => 'ul']); + $tag->addChild($tab_links); + + foreach ($this->partitions as $tabindex => $tab) { + $insertorder = array_flip($this->insert_field_order[$tabindex]); + $weights = []; + $order = []; + + $partition_fields = $this->getPartitionFields($tabindex); + + foreach ($partition_fields as $key => $elem) { + /** @var Field $elem */ + $weights[$key] = $elem->getWeight(); + $order[$key] = isset($insertorder[$key]) ? $insertorder[$key] : PHP_INT_MAX; + } + if (count($this->getPartitionFields($tabindex)) > 0) { + array_multisort($weights, SORT_ASC, $order, SORT_ASC, $partition_fields); + } + + $tab_links->addChild(new TagElement([ + 'tag' => 'li', + 'attributes' => ['class' => 'tabel '.($this->partitionHasErrors($tabindex, $form) ? 'has-errors' : '')], + 'text' => "". + $this->getText($this->partitions[$tabindex]['title'])."" + ])); + + $inner = new TagElement([ + 'tag' => 'div', + 'id' => $id.'-tab-inner-'.$tabindex, + 'attributes' => [ + 'class' => 'tab-inner'.($this->partitionHasErrors($tabindex, $form) ? ' has-errors' : '') + ], + ]); + + // hidden fields are always first + usort($partition_fields, function($fieldA, $fieldB){ + if (is_object($fieldA) && is_a($fieldA, Hidden::class)) { + return -1; + } + if (is_object($fieldB) && is_a($fieldB, Hidden::class)) { + return 1; + } + return 0; + }); + + foreach ($partition_fields as $name => $field) { + /** @var Field $field */ + $inner->addChild($field->renderHTML($form)); + } + $tag->addChild($inner); + } + + return $tag; + } + + /** + * Add a new tab + * + * @param string $title tab title + * @return self + */ + public function addTab(string $title): Tabs + { + return $this->addPartition($title); + } +} diff --git a/src/classes/containers/TagContainer.php b/src/classes/containers/TagContainer.php new file mode 100644 index 00000000..eaf710c9 --- /dev/null +++ b/src/classes/containers/TagContainer.php @@ -0,0 +1,98 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELD CONTAINERS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Containers; + +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\FieldsContainer; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Fields\Hidden; + +/** + * a field container that can specify container's html tag + */ +class TagContainer extends FieldsContainer +{ + /** + * container html tag + * + * @var string + */ + protected $tag = 'div'; + + /** + * Class constructor + * + * @param array $options build options + * @param string $name field name + */ + public function __construct($options = [], $name = null) + { + parent::__construct($options, $name); + + if ($this->attributes['class'] == 'tag_container') { // if set to the default + $this->attributes['class'] = $this->tag.'_container'; + } + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|TagElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + $tag = new TagElement([ + 'tag' => $this->tag, + 'id' => $id, + 'attributes' => $this->attributes, + 'has_close' => true, + 'value_needed' => false, + ]); + + $insertorder = array_flip($this->insert_field_order); + $weights = []; + $order = []; + foreach ($this->getFields() as $key => $elem) { + /** @var Field $elem */ + $weights[$key] = $elem->getWeight(); + $order[$key] = isset($insertorder[$key]) ? $insertorder[$key] : PHP_INT_MAX; + } + if (count($this->getFields()) > 0) { + array_multisort($weights, SORT_ASC, $order, SORT_ASC, $this->getFields()); + } + + // hidden fields are always first + usort($this->getFields(), function($fieldA, $fieldB){ + if (is_object($fieldA) && is_a($fieldA, Hidden::class)) { + return -1; + } + if (is_object($fieldB) && is_a($fieldB, Hidden::class)) { + return 1; + } + return 0; + }); + + foreach ($this->getFields() as $name => $field) { + /** @var Field $field */ + $tag->addChild($field->renderHTML($form)); + } + return $tag; + } +} diff --git a/src/classes/exceptions/FormException.php b/src/classes/exceptions/FormException.php new file mode 100644 index 00000000..c7a746fd --- /dev/null +++ b/src/classes/exceptions/FormException.php @@ -0,0 +1,21 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### EXCEPTIONS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Exceptions; + +use Degami\Basics\Exceptions\BasicException; + +class FormException extends BasicException +{ +} diff --git a/src/classes/fields/Autocomplete.php b/src/classes/fields/Autocomplete.php new file mode 100644 index 00000000..5e4f6c4c --- /dev/null +++ b/src/classes/fields/Autocomplete.php @@ -0,0 +1,95 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\PHPFormsApi\Form; + +/** + * The "autocomplete" text input field class + */ +class Autocomplete extends Textfield +{ + /** + * autocomplete path + * + * @var mixed + */ + protected $autocomplete_path = false; + + /** + * options for autocomplete (if autocomplete path was not provided) + * + * @var array + */ + protected $options = []; + + /** + * minimum string length for autocomplete + * + * @var integer + */ + protected $min_length = 3; + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + if (!isset($options['attributes']['class'])) { + $options['attributes']['class'] = ''; + } + $options['attributes']['class'] .= ' autocomplete'; + + parent::__construct($options, $name); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + + $this->addJs( + " + \$('#{$id}','#{$form->getId()}') + .bind( 'keydown', function( event ) { + if ( event.keyCode === $.ui.keyCode.TAB && \$( this ).autocomplete( 'instance' ).menu.active ) { + event.preventDefault(); + } + }) + .autocomplete({ + source: " . ((!empty($this->options)) ? + json_encode($this->options) : + "'{$this->autocomplete_path}'") . ", + minLength: {$this->min_length}, + focus: function() { + return false; + } + }); + " + ); + + parent::preRender($form); + } +} diff --git a/src/classes/fields/Button.php b/src/classes/fields/Button.php new file mode 100644 index 00000000..46ed6ca5 --- /dev/null +++ b/src/classes/fields/Button.php @@ -0,0 +1,72 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\PHPFormsApi\Form; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Abstracts\Fields\Clickable; + +/** + * The button field class + */ +class Button extends Clickable +{ + + /** + * Element label + * + * @var string + */ + protected $label; + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + parent::__construct($options, $name); + if (empty($this->label)) { + $this->label = $this->getValues(); + } + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + + return new TagElement([ + 'tag' => 'button', + 'id' => $id, + 'name' => $this->name, + 'value' => $this->getValues(), + 'text' => $this->getText($this->label), + 'attributes' => $this->attributes, + 'has_close' => true, + ]); + } +} diff --git a/src/classes/fields/Checkbox.php b/src/classes/fields/Checkbox.php new file mode 100644 index 00000000..72cf4292 --- /dev/null +++ b/src/classes/fields/Checkbox.php @@ -0,0 +1,105 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\Basics\Html\TagElement; + +/** + * The single checkbox input field class + */ +class Checkbox extends Field +{ + /** + * @var string where (after or before) to print text + */ + protected $text_position = 'after'; + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + parent::__construct($options, $name); + $this->value = null; + if (isset($options['value'])) { + $this->value = $options['value']; + } + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + + $this->label_class .= " label-" . $this->getElementClassName(); + $this->label_class = trim($this->label_class); + + if ($this->value == $this->default_value) { + $this->attributes['checked'] = 'checked'; + } + + $tag = new TagElement([ + 'tag' => 'label', + 'attributes' => ['for' => $id, 'class' => $this->label_class], + 'text' => (($this->text_position == 'before') ? $this->getText($this->title) : ''), + ]); + $tag->addChild(new TagElement([ + 'tag' => 'input', + 'type' => 'checkbox', + 'id' => $id, + 'name' => $this->name, + 'value' => $this->default_value, + 'attributes' => $this->attributes, + 'text' => (($this->text_position != 'before') ? $this->getText($this->title) : ''), + ])); + return $tag; + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue(): bool + { + return true; + } + + /** + * {@inheritdoc} + * + * @return mixed field value + */ + public function getValues() + { + return $this->getValue(); + } +} diff --git a/src/classes/fields/Checkboxes.php b/src/classes/fields/Checkboxes.php new file mode 100644 index 00000000..a4899c32 --- /dev/null +++ b/src/classes/fields/Checkboxes.php @@ -0,0 +1,100 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Abstracts\Fields\FieldMultivalues; + +/** + * The checkboxes group field class + */ +class Checkboxes extends FieldMultivalues +{ + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + if (!is_array($this->default_value)) { + $this->default_value = [ $this->default_value ]; + } + + $tag = new TagElement([ + 'tag' => 'div', 'attributes' => ['class' => 'options'], + ]); + + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + + foreach ($this->options as $key => $value) { + if ($value instanceof Checkbox) { + $value->setName("{$this->name}".(count($this->options)>1 ? "[]":"")); + $value->setId("{$this->name}-{$key}"); + $tag->addChild($value->renderHTML($form)); + } else { + if (is_array($value) && isset($value['attributes'])) { + $attributes = $value['attributes']; + } else { + $attributes = []; + } + if (is_array($value)) { + $value = $value['value']; + } + + $tag_label = new TagElement([ + 'tag' => 'label', + 'attributes' => ['for' => "{$id}-{$key}", 'class' => "label-checkbox"], + ]); + $tag_label->addChild(new TagElement([ + 'tag' => 'input', + 'type' => 'checkbox', + 'id' => "{$id}-{$key}", + 'name' => "{$this->name}".(count($this->options)>1 ? "[]" : ""), + 'value' => $key, + 'attributes' => array_merge( + $attributes, + ( + is_array($this->default_value) && + in_array($key, $this->default_value) + ) ? + ['checked' => 'checked'] : [] + ), + 'text' => $value, + ])); + $tag->addChild($tag_label); + } + } + + return $tag; + } + + /** + * {@inheritdoc} + * + * @return mixed field value + */ + public function getValues() + { + return $this->getValue(); + } +} diff --git a/src/classes/fields/Color.php b/src/classes/fields/Color.php new file mode 100644 index 00000000..9813cbf3 --- /dev/null +++ b/src/classes/fields/Color.php @@ -0,0 +1,95 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\Basics\Html\TagElement; + +/** + * The color input field class + */ +class Color extends Field +{ + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + parent::__construct($options, $name); + if (!empty($this->default_value) && !$this->isRGB($this->default_value)) { + $this->value = $this->default_value = '#000000'; + } + } + + /** + * Check if string is an RGB representation + * + * @param string $str string to check + * @return boolean true if string is RGB + */ + private function isRGB(string $str): bool + { + return preg_match("/^#?([a-f\d]{3}([a-f\d]{3})?)$/i", $str); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + if ($this->hasErrors()) { + $this->attributes['class'] .= ' has-errors'; + } + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + if (is_array($this->value)) { + $this->value = ''; + } + + return new TagElement([ + 'tag' => 'input', + 'type' => 'color', + 'id' => $id, + 'name' => $this->name, + 'value' => htmlspecialchars($this->getValues()), + 'attributes' => $this->attributes + ['size' => $this->size], + ]); + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue(): bool + { + return true; + } +} diff --git a/src/classes/fields/Colorpicker.php b/src/classes/fields/Colorpicker.php new file mode 100644 index 00000000..574d3a9c --- /dev/null +++ b/src/classes/fields/Colorpicker.php @@ -0,0 +1,201 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\Basics\Html\TagElement; + +/** + * The colorpicker input field class + */ +class Colorpicker extends Field +{ + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + parent::__construct($options, $name); + if (!empty($this->default_value) && !$this->isRGB($this->default_value)) { + $this->value = $this->default_value = '#000000'; + } + } + + /** + * Check if string is an RGB representation + * + * @param string $str string to check + * @return boolean true if string is RGB + */ + private function isRGB(string $str): bool + { + return preg_match("/^#?([a-f\d]{3}([a-f\d]{3})?)$/i", $str); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + if ($this->hasErrors()) { + $this->attributes['class'] .= ' has-errors'; + } + $this->attributes['class'] .= ' ui-state-disabled'; + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + if (is_array($this->value)) { + $this->value = ''; + } + + $tag = new TagElement([ + 'tag' => 'div', 'id' => $id, + ]); + + $tag->addChild("
    +
    +
    +
    +
    +
    "); + + $tag->addChild(new TagElement([ + 'tag' => 'input', + 'type' => 'text', + 'name' => $this->name, + 'value' => htmlspecialchars($this->getValues()), + 'attributes' => $this->attributes + ['size' => $this->size, 'onFocus' => "blur();" ], + ])); + + return $tag; + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + + $js_func_hexFromRGB = "(function(r, g, b){ + var hex = [ + r.toString( 16 ), + g.toString( 16 ), + b.toString( 16 ) + ]; + \$.each( hex, function( nr, val ) { + if ( val.length === 1 ) { + hex[ nr ] = \"0\" + val; + } + }); + return hex.join( \"\" ).toUpperCase(); + })"; + + $js_func_refreshSwatch = "function refreshSwatch() { + var red = \$( \"#{$id}-red\" ).slider( \"value\" ), + green = \$( \"#{$id}-green\" ).slider( \"value\" ), + blue = \$( \"#{$id}-blue\" ).slider( \"value\" ), + hex = $js_func_hexFromRGB( red, green, blue ); + \$( \"#{$id}-swatch\" ).css( \"background-color\", \"#\" + hex ); + \$( \"input[name='{$this->name}']\", \"#{$id}\" ).val(\"#\" + hex ); + }"; + + $this->addJs( + " + \$('#{$id}-red,#{$id}-green,#{$id}-blue','#{$form->getId()}').slider({ + orientation: \"horizontal\", + range: \"min\", + max: 255, + slide: {$js_func_refreshSwatch}, + change: {$js_func_refreshSwatch} + });" + ); + + $this->addCss( + " + #{$id} {padding-top: 20px;} + #{$id}-red, #{$id}-green, #{$id}-blue { + float: left; + clear: left; + width: 300px; + margin: 5px; + } + #{$id}-swatch { + width: 120px; + height: 100px; + margin-top: -15px; + margin-left: 350px; + background-image: none; + } + #{$id}-red .ui-slider-range { background: #ef2929; } + #{$id}-red .ui-slider-handle { border-color: #ef2929; } + #{$id}-green .ui-slider-range { background: #8ae234; } + #{$id}-green .ui-slider-handle { border-color: #8ae234; } + #{$id}-blue .ui-slider-range { background: #729fcf; } + #{$id}-blue .ui-slider-handle { border-color: #729fcf; } + #{$id} .clearfix{display: table;width:100%;clear: both;float: none;padding-bottom: 15px;} + " + ); + + if (!empty($this->value)) { + $this->addJs( + "(function(hex){ + var result = /^#?([a-f\d]{3}([a-f\d]{3})?)$/i.exec(hex); + if( result ){ + if(undefined == result[2]) { + result[1] = (result[1].split(\"\")).map(function(elem){ return elem.repeat(2); }).join(\"\"); + } + result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec('#'+result[1]); + + \$( \"#{$id}-red\" ).slider( \"value\", parseInt(result[1], 16) ); + \$( \"#{$id}-green\" ).slider( \"value\", parseInt(result[2], 16) ); + \$( \"#{$id}-blue\" ).slider( \"value\", parseInt(result[3], 16) ); + } + })( \$(\"input[name='{$this->name}']\", \"#{$id}\").val() )" + ); + $this->addCss("#{$id}-swatch { background-color: {$this->value};}"); + } + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue(): bool + { + return true; + } +} diff --git a/src/classes/fields/Datalist.php b/src/classes/fields/Datalist.php new file mode 100644 index 00000000..715b22de --- /dev/null +++ b/src/classes/fields/Datalist.php @@ -0,0 +1,114 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Abstracts\Fields\FieldMultivalues; +use Degami\Basics\Html\TagList; + +/** + * The "autocomplete" text input field class + */ +class Datalist extends FieldMultivalues +{ + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + if (isset($options['options'])) { + foreach ($options['options'] as $k => $o) { + if ($o instanceof Option) { + $o->setParent($this); + $this->options[] = $o; + } else { + $option = new Option($o, $o); + $option->setParent($this); + $this->options[] = $option; + } + } + unset($options['options']); + } + + if (isset($options['default_value'])) { + if (is_array($options['default_value'])) { + $options['default_value'] = reset($options['default_value']); + } + $options['default_value'] = "".$options['default_value']; + } + + parent::__construct($options, $name); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + if ($this->hasErrors()) { + $this->attributes['class'] .= ' has-errors'; + } + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + if (is_array($this->value)) { + $this->value = ''; + } + + $tag = new TagList(); + $tag->addChild(new TagElement([ + 'tag' => 'input', + 'type' => 'text', + 'id' => $id, + 'name' => $this->name, + 'value' => htmlspecialchars($this->getValues()), + 'attributes' => $this->attributes + ['size' => $this->size, 'list' => $this->name."-data"], + ])); + + $dlist = new TagElement([ + 'tag' => 'datalist', + 'type' => null, + 'id' => $this->name.'-data', + 'value_needed' => false, + 'has_close' => true, + ]); + foreach ($this->options as $key => $opt) { + /** @var Option $opt */ + $dlist->addChild(new TagElement([ + 'tag' => 'option', + 'type' => null, + 'value' => $opt->getKey(), + 'text' => $this->getText($opt->getLabel()), + 'has_close' => true, + ])); + } + $tag->addChild($dlist); + + return $tag; + } +} diff --git a/src/classes/fields/Date.php b/src/classes/fields/Date.php new file mode 100644 index 00000000..c91d4b71 --- /dev/null +++ b/src/classes/fields/Date.php @@ -0,0 +1,82 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\Basics\Html\TagElement; + +/** + * The date field class + */ +class Date extends Field +{ + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + $this->default_value = date('Y-m-d'); + parent::__construct($options, $name); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + if ($this->hasErrors()) { + $this->attributes['class'] .= ' has-errors'; + } + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + if (is_array($this->value)) { + $this->value = ''; + } + + return new TagElement([ + 'tag' => 'input', + 'type' => 'date', + 'id' => $id, + 'name' => $this->name, + 'value' => htmlspecialchars($this->getValues()), + 'attributes' => $this->attributes + ['size' => $this->size], + ]); + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue(): bool + { + return true; + } +} diff --git a/src/classes/fields/Datepicker.php b/src/classes/fields/Datepicker.php new file mode 100644 index 00000000..21d7f204 --- /dev/null +++ b/src/classes/fields/Datepicker.php @@ -0,0 +1,120 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\PHPFormsApi\Form; + +/** + * The datepicker text input field class + */ +class Datepicker extends Textfield +{ + /** + * date format + * + * @var string + */ + protected $date_format = 'yy-mm-dd'; + + /** + * change month flag + * + * @var boolean + */ + protected $change_month = false; + + /** + * change year flag + * + * @var boolean + */ + protected $change_year = false; + + /** + * min date + * + * @var string + */ + protected $mindate = '-10Y'; + + /** + * max date + * + * @var string + */ + protected $maxdate = '+10Y'; + + /** + * year range + * + * @var string + */ + protected $yearrange = '-10:+10'; + + /** + * disabled dates array + * + * @var array + */ + protected $disabled_dates = []; // an array of date strings compliant to $date_format + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + + $changeMonth = ($this->change_month) ? 'true' :'false'; + $changeYear = ($this->change_year == true) ? 'true' :'false'; + + $this->addJs( + ((count($this->disabled_dates)>0) ? + "var disabled_dates_array_{$form->getId()}_{$id} = ". + json_encode((array) $this->disabled_dates).";": + "" + ). + "\$('#{$id}','#{$form->getId()}').datepicker({ + dateFormat: '{$this->date_format}', + ".((count($this->disabled_dates)>0) ? "beforeShowDay: function(date){ + var string = $.datepicker.formatDate('{$this->date_format}', date); + return [ disabled_dates_array_{$form->getId()}_{$id}.indexOf(string) == -1 ]; + },": "")." + changeMonth: {$changeMonth}, + changeYear: {$changeYear}, + minDate: \"{$this->mindate}\", + maxDate: \"{$this->maxdate}\", + yearRange: \"{$this->yearrange}\" + });" + ); + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue() : bool + { + return true; + } +} diff --git a/src/classes/fields/Dateselect.php b/src/classes/fields/Dateselect.php new file mode 100644 index 00000000..ecf0c336 --- /dev/null +++ b/src/classes/fields/Dateselect.php @@ -0,0 +1,296 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\Basics\Html\TagElement; + +/** + * The date select group field class + */ +class Dateselect extends Field +{ + /** + * granularity (day / month / year) + * + * @var string + */ + protected $granularity = 'day'; + + /** + * start year + * + * @var integer + */ + protected $start_year; + + /** + * end year + * + * @var integer + */ + protected $end_year; + + /** + * "use js selects" flag + * + * @var boolean + */ + protected $js_selects = false; + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + $this->start_year = date('Y')-100; + $this->end_year = date('Y')+100; + $this->default_value = [ + 'year'=>date('Y'), + 'month'=>date('m'), + 'day'=>date('d'), + ]; + + parent::__construct($options, $name); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + if ($this->js_selects == true) { + $id = $this->getHtmlId(); + $this->addJs( + "\$('#{$id} select[name=\"{$this->name}[year]\"]','#{$form->getId()}') + .selectmenu({width: 'auto' });" + ); + if ($this->granularity != 'year') { + $this->addJs( + "\$('#{$id} select[name=\"{$this->name}[month]\"]','#{$form->getId()}') + .selectmenu({width: 'auto' });" + ); + if ($this->granularity != 'month') { + $this->addJs( + "\$('#{$id} select[name=\"{$this->name}[day]\"]','#{$form->getId()}') + .selectmenu({width: 'auto' });" + ); + } + } + } + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + if ($this->hasErrors()) { + $this->attributes['class'] .= ' has-errors'; + } + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + + $tag = new TagElement([ + 'tag' => 'div', + 'id' => $id, + 'attributes' => $this->attributes, + ]); + + if ($this->granularity!='year' && $this->granularity!='month') { + if (!(isset($this->attributes['day']) && is_array($this->attributes['day']))) { + $this->attributes['day'] = []; + } + if ($this->disabled == true) { + $this->attributes['day']['disabled']='disabled'; + } + $select_day = new TagElement([ + 'tag' => 'select', + 'name' => $this->name.'[day]', + 'attributes' => $this->attributes['day'], + ]); + for ($i=1; $i<=31; $i++) { + $select_day->addChild(new TagElement([ + 'tag' => 'option', + 'value' => $i, + 'attributes' => [] + (($i == $this->value['day']) ? ['selected' => 'selected'] : []), + 'text' => $i, + ])); + } + $tag->addChild($select_day); + } + if ($this->granularity!='year') { + if (!(isset($this->attributes['month']) && is_array($this->attributes['month']))) { + $this->attributes['month'] = []; + } + if ($this->disabled == true) { + $this->attributes['month']['disabled']='disabled'; + } + $select_month = new TagElement([ + 'tag' => 'select', + 'name' => $this->name.'[month]', + 'attributes' => $this->attributes['month'], + ]); + for ($i=1; $i<=12; $i++) { + $select_month->addChild(new TagElement([ + 'tag' => 'option', + 'value' => $i, + 'attributes' => [] + (($i == $this->value['month']) ? ['selected' => 'selected'] : []), + 'text' => $i, + ])); + } + $tag->addChild($select_month); + } + if (!(isset($this->attributes['year']) && is_array($this->attributes['year']))) { + $this->attributes['year'] = []; + } + if ($this->disabled == true) { + $this->attributes['year']['disabled']='disabled'; + } + $select_year = new TagElement([ + 'tag' => 'select', + 'name' => $this->name.'[year]', + 'attributes' => $this->attributes['year'], + ]); + for ($i=$this->start_year; $i<=$this->end_year; $i++) { + $select_year->addChild(new TagElement([ + 'tag' => 'option', + 'value' => $i, + 'attributes' => [] + (($i == $this->value['year']) ? ['selected' => 'selected'] : []), + 'text' => $i, + ])); + } + $tag->addChild($select_year); + return $tag; + } + + /** + * {@inheritdoc} + * + * @param mixed $value value to set + */ + public function processValue($value) + { + $this->value = [ + 'year' => $value['year'], + ]; + if ($this->granularity!='year') { + $this->value['month'] = $value['month']; + if ($this->granularity!='month') { + $this->value['day'] = $value['day']; + } + } + } + + /** + * {@inheritdoc} + * + * @return boolean TRUE if element is valid + */ + public function isValid(): bool + { + $year = $this->value['year']; + $month = isset($this->value['month']) ? $this->value['month'] : 1; + $day = isset($this->value['day']) ? $this->value['day'] : 1; + + if (!checkdate($month, $day, $year)) { + $titlestr = (!empty($this->title)) ? $this->title : !empty($this->name) ? $this->name : $this->id; + $this->addError(str_replace("%t", $titlestr, $this->getText("%t: Invalid date")), __FUNCTION__); + + if ($this->stop_on_first_error) { + return false; + } + } + return parent::isValid(); + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue(): bool + { + return true; + } + + /** + * Get start timestamp + * + * @return int start timestamp + */ + public function tsStart(): int + { + $year = $this->value['year']; + $month = isset($this->value['month']) ? $this->value['month'] : 1; + $day = isset($this->value['day']) ? $this->value['day'] : 1; + + return mktime(0, 0, 0, $month, $day, $year); + } + + /** + * Get end timestamp + * + * @return int end timestamp + */ + public function tsEnd(): int + { + $year = $this->value['year']; + $month = isset($this->value['month']) ? $this->value['month'] : 1; + $day = isset($this->value['day']) ? $this->value['day'] : 1; + + return mktime(23, 59, 59, $month, $day, $year); + } + + /** + * Get value as a date string + * + * @return string date value + */ + public function valueString(): string + { + $value = $this->getValues(); + $out = (($value['year'] < 10) ? '0':'').((int) $value['year']); + if ($this->granularity!='year') { + $out .= '-'.(($value['month'] < 10) ? '0':'').((int) $value['month']); + if ($this->granularity!='month') { + $out .= '-'.(($value['day'] < 10) ? '0':'').((int) $value['day']); + } + } + return $out; + } +} diff --git a/src/classes/fields/Datetime.php b/src/classes/fields/Datetime.php new file mode 100644 index 00000000..d77b020b --- /dev/null +++ b/src/classes/fields/Datetime.php @@ -0,0 +1,169 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Traits\ToolsTrait as BasicToolsTrait; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\PHPFormsApi\Abstracts\Base\FieldsContainer; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Fields\ComposedField; + +/** + * The datetime group field class + */ +class Datetime extends ComposedField +{ + use BasicToolsTrait; + + /** + * date sub element + * + * @var Date + */ + protected $date = null; + + /** + * time sub_element + * + * @var Time + */ + protected $time = null; + + /** + * "use js selects" flag + * + * @var boolean + */ + protected $js_selects = false; + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + parent::__construct($options, $name); + + unset($options['title']); + $options['container_tag'] = ''; + + $options['type'] = 'date'; + $this->date = new Date($options, $this->getSubfieldName('date')); + + $options['type'] = 'time'; + $this->time = new Time($options, $this->getSubfieldName('time')); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + $this->addCss("#{$id} div.date,#{$id} div.time{display: inline-block;margin-right: 5px;}"); + + $this->date->preRender($form); + $this->time->preRender($form); + + foreach ($this->date->getJs() as $date_js_line) { + if (!empty($date_js_line)) { + $this->addJs($date_js_line); + } + } + + foreach ($this->time->getJs() as $time_js_line) { + if (!empty($time_js_line)) { + $this->addJs($time_js_line); + } + } + + parent::preRender($form); + } + + /** + * {@inheritdoc} . it simply calls the sub elements preprocess + * + * @param string $process_type preprocess type + */ + public function preProcess($process_type = "preprocess") + { + $this->date->preProcess($process_type); + $this->time->preProcess($process_type); + } + + /** + * {@inheritdoc} . it simply calls the sub elements process + * + * @param mixed $values value to set + */ + public function processValue($values) + { + $this->processSubfieldsValues($values, $this->date, 'date'); + $this->processSubfieldsValues($values, $this->time, 'time'); + } + + /** + * {@inheritdoc} + * + * @return boolean TRUE if element is valid + */ + public function isValid() : bool + { + return $this->date->isValid() && $this->time->isValid(); + } + + /** + * renders form errors + * + * @return string errors as an html
  • list + */ + public function showErrors() : string + { + $out = trim($this->date->showErrors() . $this->time->showErrors()); + return ($out == '') ? '' : $out; + } + + /** + * resets the sub elements + */ + public function resetField(): Field + { + $this->date->resetField(); + $this->time->resetField(); + + return $this; + } + + /** + * Return field value + * + * @return mixed field value + */ + public function getValues() + { + return [ + 'date'=> $this->date->getValues(), + 'time'=> $this->time->getValues(), + 'datetime' => $this->date->getValues().' '.$this->time->getValues(), + ]; + } +} diff --git a/src/classes/fields/Datetimeselect.php b/src/classes/fields/Datetimeselect.php new file mode 100644 index 00000000..523daf66 --- /dev/null +++ b/src/classes/fields/Datetimeselect.php @@ -0,0 +1,165 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Fields\ComposedField; + +/** + * The datetime select group field class + */ +class Datetimeselect extends ComposedField +{ + /** + * date sub element + * + * @var Date + */ + protected $date = null; + + /** + * time sub_element + * + * @var Time + */ + protected $time = null; + + /** + * "use js selects" flag + * + * @var boolean + */ + protected $js_selects = false; + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + parent::__construct($options, $name); + + unset($options['title']); + $options['container_tag'] = ''; + + $options['type'] = 'dateselect'; + $this->date = new Dateselect($options, $this->getSubfieldName('date')); + + $options['type'] = 'timeselect'; + $this->time = new Timeselect($options, $this->getSubfieldName('time')); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + $this->addCss("#{$id} div.date,#{$id} div.time{display: inline-block;margin-right: 5px;}"); + + $this->date->preRender($form); + $this->time->preRender($form); + + foreach ($this->date->getJs() as $date_js_line) { + if (!empty($date_js_line)) { + $this->addJs($date_js_line); + } + } + + foreach ($this->time->getJs() as $time_js_line) { + if (!empty($time_js_line)) { + $this->addJs($time_js_line); + } + } + + parent::preRender($form); + } + + /** + * {@inheritdoc} . it simply calls the sub elements preprocess + * + * @param string $process_type preprocess type + */ + public function preProcess($process_type = "preprocess") + { + $this->date->preProcess($process_type); + $this->time->preProcess($process_type); + } + + /** + * {@inheritdoc} . it simply calls the sub elements process + * + * @param array $values value to set + */ + public function processValue($values) + { + $this->processSubfieldsValues($values, $this->date, 'date'); + $this->processSubfieldsValues($values, $this->time, 'time'); + } + + /** + * {@inheritdoc} + * + * @return boolean TRUE if element is valid + */ + public function isValid() : bool + { + return $this->date->isValid() && $this->time->isValid(); + } + + /** + * renders form errors + * + * @return string errors as an html
  • list + */ + public function showErrors() : string + { + $out = trim($this->date->showErrors() . $this->time->showErrors()); + return ($out == '') ? '' : $out; + } + + /** + * resets the sub elements + */ + public function resetField() : Field + { + $this->date->resetField(); + $this->time->resetField(); + + return $this; + } + + /** + * Return field value + * + * @return mixed field value + */ + public function getValues() + { + return [ + 'date'=> $this->date->getValues(), + 'time'=> $this->time->getValues(), + 'datetime' => $this->date->valueString().' '.$this->time->valueString(), + ]; + } +} diff --git a/src/classes/fields/Email.php b/src/classes/fields/Email.php new file mode 100644 index 00000000..e7aa4410 --- /dev/null +++ b/src/classes/fields/Email.php @@ -0,0 +1,81 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\Basics\Html\TagElement; + +/** + * The email input field class + */ +class Email extends Field +{ + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + parent::__construct($options, $name); + + // ensure is email validator is present + $this->getValidate()->addElement('email'); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + if ($this->hasErrors()) { + $this->attributes['class'] .= ' has-errors'; + } + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + + $tag = new TagElement([ + 'tag' => 'input', + 'type' => 'email', + 'id' => $id, + 'name' => $this->name, + 'value' => $this->getValues(), + 'attributes' => $this->attributes + ['size' => $this->size], + ]); + return $tag; + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue() : bool + { + return true; + } +} diff --git a/src/classes/fields/File.php b/src/classes/fields/File.php new file mode 100644 index 00000000..564c62a0 --- /dev/null +++ b/src/classes/fields/File.php @@ -0,0 +1,224 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\Basics\Html\TagElement; +use Degami\Basics\Html\TagList; + +/** + * The file input field class + */ +class File extends Field +{ + /** + * "file already uploaded" flag + * + * @var boolean + */ + protected $uploaded = false; + + /** + * file destination directory + * + * @var string + */ + protected $destination; + + /** + * rename destination file if it already exists + * + * @var boolean + */ + protected $rename_on_existing = false; + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + $form->setAttribute('enctype', 'multipart/form-data'); + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + if ($this->hasErrors()) { + $this->attributes['class'] .= ' has-errors'; + } + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + + $tag = new TagList(); + $tag->addChild(new TagElement([ + 'tag' => 'input', + 'type' => 'hidden', + 'name' => $this->name, + 'value' => $this->name, + ])); + + $tag->addChild(new TagElement([ + 'tag' => 'input', + 'type' => 'file', + 'id' => $id, + 'name' => $this->name, + 'value_needed' => false, + 'attributes' => $this->attributes + ['size' => $this->size], + ])); + + return $tag; + } + + /** + * {@inheritdoc} + * + * @param mixed $value value to set + */ + public function processValue($value) + { + if (($requestValue = static::traverseArray($this->convertFilesArray(), $this->getName())) != null) { + $this->value = [ + 'filepath' => (isset($value['filepath'])) ? + $value['filepath'] : + $this->destination .'/'. basename($requestValue['name']), + 'filename' => (isset($value['filename'])) ? + $value['filename'] : + basename($requestValue['name']), + 'filesize' => (isset($value['filesize'])) ? + $value['filesize'] : + $requestValue['size'], + 'mimetype' => (isset($value['mimetype'])) ? + $value['mimetype'] : + $requestValue['type'], + ]; + } + + if (isset($value['uploaded'])) { + $this->uploaded = $value['uploaded']; + } + if (($requestValue['size'] ?? 0) == 0) { + $this->uploaded = false; + } else { + $this->uploaded = true; + } + + if (!$this->uploaded) { + return; + } + + if ($this->isValid()) { + if ($this->rename_on_existing) { + $filepath = $this->value['filepath']; $counter = 0; + do { + if (!file_exists($filepath)) { + break; + } + + $path_parts = pathinfo($filepath); + $filepath = $path_parts['dirname'] . '/' . $path_parts['filename'] . '_' . (++$counter) . '.' . $path_parts['extension']; + } while (file_exists($filepath)); + + if ($filepath != $this->value['filepath']) { + $this->value['renamed'] = true; + } + + $this->value['filepath'] = $filepath; + $this->value['filename'] = basename($filepath); + } + + if (@move_uploaded_file($_FILES[$this->getName()]['tmp_name'], $this->value['filepath']) == true) { + $this->uploaded = true; + } + } + } + + protected function convertFilesArray(): array + { + $out = []; + foreach ($_FILES as $input_name => $input_value) { + foreach (['name','type','tmp_name','error','size'] as $prop) { + if (is_array($input_value[$prop])) { + foreach ($input_value[$prop] as $key => $value) { + $out[$input_name][$key][$prop] = $value ?? null; + } + } else { + $out[$input_name][$prop] = $input_value[$prop] ?? null; + } + } + } + return $out; + } + + /** + * Check if file was uploaded + * + * @return boolean TRUE if file was uploaded + */ + public function isUploaded(): bool + { + return $this->uploaded; + } + + /** + * "required" validation function + * + * @param mixed $value the element value + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateRequired($value = null) + { + if (!empty($value) + && (isset($value['filepath']) && !empty($value['filepath'])) + && (isset($value['filename']) && !empty($value['filename'])) + && (isset($value['mimetype']) && !empty($value['mimetype'])) + && (isset($value['filesize']) && $value['filesize']>=0) + ) { + return true; + } else { + return "%t is required"; + } + } + + /** + * validate function + * + * @return boolean this field is always valid + */ + public function isValid() : bool + { + if ($this->uploaded) { + return true; + } + return parent::isValid(); + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue() : bool + { + return true; + } +} diff --git a/src/classes/fields/Geolocation.php b/src/classes/fields/Geolocation.php new file mode 100644 index 00000000..ee22bfad --- /dev/null +++ b/src/classes/fields/Geolocation.php @@ -0,0 +1,157 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Fields\ComposedField; + +/** + * The geolocation field class + */ +class Geolocation extends ComposedField +{ + + /** + * latitude + * + * @var float + */ + protected $latitude; + + /** + * longitude + * + * @var float + */ + protected $longitude; + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + parent::__construct($options, $name); + + $defaults = isset($options['default_value']) ? $options['default_value'] : ['latitude' => 0, 'longitude' => 0]; + + unset($options['title']); + unset($options['prefix']); + unset($options['suffix']); + $options['container_tag'] = ''; + + if (!isset($options['size'])) { + $options['size'] = 5; + } + + $options['type'] = 'textfield'; + $options['suffix'] = $this->getText('latitude').' '; + $options['default_value'] = (is_array($defaults) && isset($defaults['latitude'])) ? $defaults['latitude'] : 0; + $this->latitude = new Textfield($options, $this->getSubfieldName('latitude')); + + $options['type'] = 'textfield'; + $options['suffix'] = $this->getText('longitude').' '; + $options['default_value'] = (is_array($defaults) && isset($defaults['longitude'])) ? $defaults['longitude'] : 0; + $this->longitude = new Textfield($options, $this->getSubfieldName('longitude')); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + parent::preRender($form); + + $this->latitude->preRender($form); + $this->longitude->preRender($form); + } + + /** + * {@inheritdoc} . it simply calls the sub elements preprocess + * + * @param string $process_type preprocess type + */ + public function preProcess($process_type = "preprocess") + { + $this->latitude->preProcess($process_type); + $this->longitude->preProcess($process_type); + } + + /** + * {@inheritdoc} . it simply calls the sub elements process + * + * @param mixed $values value to set + */ + public function processValue($values) + { + $this->processSubfieldsValues($values, $this->latitude, 'latitude'); + $this->processSubfieldsValues($values, $this->longitude, 'longitude'); + } + + /** + * {@inheritdoc} + * + * @return boolean TRUE if element is valid + */ + public function isValid() : bool + { + return $this->latitude->isValid() && $this->longitude->isValid(); + } + + + /** + * renders form errors + * + * @return string errors as an html
  • list + */ + public function showErrors() : string + { + $out = trim($this->latitude->showErrors() . $this->longitude->showErrors()); + return ($out == '') ? '' : $out; + } + + + /** + * resets the sub elements + */ + public function resetField() : Field + { + $this->latitude->resetField(); + $this->longitude->resetField(); + + return $this; + } + + /** + * Return field value + * + * @return mixed field value + */ + public function getValues() + { + return [ + 'latitude'=> $this->latitude->getValues(), + 'longitude'=> $this->longitude->getValues(), + ]; + } +} diff --git a/src/classes/fields/Gmaplocation.php b/src/classes/fields/Gmaplocation.php new file mode 100644 index 00000000..283e23bf --- /dev/null +++ b/src/classes/fields/Gmaplocation.php @@ -0,0 +1,511 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\Basics\Html\TagElement; + +/** + * The google maps geolocation field class + */ +class Gmaplocation extends Geolocation +{ + + /** + * "current location" button + * + * @var Button + */ + protected $current_location_btn; + + + /** + * zoom + * + * @var integer + */ + protected $zoom = 8; + + /** + * scrollwheel + * + * @var boolean + */ + protected $scrollwheel = false; + + /** + * map width + * + * @var string + */ + protected $mapwidth = '100%'; + + /** + * map height + * + * @var string + */ + protected $mapheight = '500px'; + + /** + * marker title + * + * @var null + */ + protected $markertitle = null; + + /** + * map type - one of: + * google.maps.MapTypeId.HYBRID, + * google.maps.MapTypeId.ROADMAP, + * google.maps.MapTypeId.SATELLITE, + * google.maps.MapTypeId.TERRAIN + * + * @var string + */ + protected $maptype = 'google.maps.MapTypeId.ROADMAP'; + + /** + * enable geocode box + * + * @var boolean + */ + protected $with_geocode = false; + + /** + * enable current location button + * + * @var boolean + */ + protected $with_current_location = false; + + /** + * input type where latitude and longitude are stored (hidden / textfield) + * + * @var string + */ + protected $lat_lon_type = 'hidden'; + + /** + * textfield subelement for geocode box + * + * @var null + */ + protected $geocode_box = null; + + /** + * textarea subelement for reverse geocoding informations + * + * @var null + */ + protected $reverse_geocode_box = null; + + /** + * "show map" flag + * + * @var boolean + */ + protected $with_map = true; + + /** + * enable reverse geocoding information box + * + * @var boolean + */ + protected $with_reverse = false; + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + parent::__construct($options, $name); + $defaults = isset($options['default_value']) ? $options['default_value'] : ['latitude' => 0, 'longitude' => 0]; + + unset($options['title']); + unset($options['prefix']); + unset($options['suffix']); + $options['container_tag'] = ''; + + $opt = $options; + $opt['type'] = 'hidden'; + $opt['attributes']['class'] = 'latitude'; + if ($this->lat_lon_type == 'textfield') { + $opt['type'] = 'textfield'; + } + $opt['default_value'] = (is_array($defaults) && isset($defaults['latitude'])) ? $defaults['latitude'] : 0; + if ($this->lat_lon_type == 'textfield') { + $opt['suffix'] = $this->getText('latitude').' '; + } + if ($this->lat_lon_type == 'textfield') { + $this->latitude = new Textfield($opt, $this->getSubfieldName('latitude')); + } else { + $this->latitude = new Hidden($opt, $this->getSubfieldName('latitude')); + } + + $opt = $options; + $opt['type'] = 'hidden'; + $opt['attributes']['class'] = 'longitude'; + if ($this->lat_lon_type == 'textfield') { + $opt['type'] = 'textfield'; + } + $opt['default_value'] = (is_array($defaults) && isset($defaults['longitude'])) ? $defaults['longitude'] : 0; + if ($this->lat_lon_type == 'textfield') { + $opt['suffix'] = $this->getText('longitude').' '; + } + if ($this->lat_lon_type == 'textfield') { + $this->longitude = new Textfield($opt, $this->getSubfieldName('longitude')); + } else { + $this->longitude = new Hidden($opt, $this->getSubfieldName('longitude')); + } + + if ($this->with_geocode == true) { + $opt = $options; + $opt['type'] = 'textfield'; + $opt['size'] = 50; + $opt['attributes']['class'] = 'geocode'; + $opt['default_value'] = (is_array($defaults) && + isset($defaults['geocodebox'])) ? + $defaults['geocodebox'] : + ''; + $this->geocode_box = new Textfield($opt, $this->getSubfieldName('geocodebox')); + } + + if ($this->with_reverse == true) { + $opt = $options; + $opt['type'] = 'textarea'; + $opt['attributes']['class'] = 'reverse'; + $opt['default_value'] = (is_array($defaults) && + isset($defaults['reverse_geocodebox'])) ? + $defaults['reverse_geocodebox'] : + ''; + $this->reverse_geocode_box = new Textarea($opt, $this->getSubfieldName('reverse_geocodebox')); + } + + if ($this->with_current_location == true) { + $opt = $options; + $opt['type'] = 'button'; + $opt['size'] = 50; + $opt['attributes']['class'] = 'current_location'; + $opt['default_value'] = $this->getText('Current Location'); + $this->current_location_btn = new Button($opt, $this->getSubfieldName('current_location_btn')); + } + } + + /** + * {@inheritdoc} . it simply calls the sub elements preprocess + * + * @param string $process_type preprocess type + */ + public function preProcess($process_type = "preprocess") + { + parent::preprocess($process_type); + if ($this->with_geocode == true) { + $this->geocode_box->preProcess($process_type); + } + if ($this->with_reverse == true) { + $this->reverse_geocode_box->preProcess($process_type); + } + } + + + /** + * {@inheritdoc} . it simply calls the sub elements process + * + * @param array $values value to set + */ + public function processValue($values) + { + parent::processValue($values); + + if ($this->with_geocode == true) { + $this->processSubfieldsValues($values, $this->geocode_box, 'geocodebox'); + } + if ($this->with_reverse == true) { + $this->processSubfieldsValues($values, $this->reverse_geocode_box, 'reverse_geocodebox'); + } + } + + /** + * Return field value + * + * @return mixed field value + */ + public function getValues() + { + $out = parent::getValues(); + if ($this->with_geocode == true) { + $out += [ 'geocodebox' => $this->geocode_box->getValues() ]; + } + if ($this->with_reverse == true) { + $out += [ 'reverse_geocodebox' => $this->reverse_geocode_box->getValues() ]; + } + return $out; + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + + if ($this->with_geocode == true) { + $update_map_func = ""; + if ($this->with_map == true) { + $update_map_func = " + var map = \$.data( \$('#{$id}-map')[0] , 'map_obj'); + var marker = \$.data( \$('#{$id}-map')[0] , 'marker_obj'); + marker.setPosition( new google.maps.LatLng( lat, lng ) ); + map.panTo( new google.maps.LatLng( lat, lng ) ); + "; + } + + $this->addJs( + " + var {$id}_api_endpoint = 'https://maps.googleapis.com/maps/api/geocode/json?address='; + \$('#{$id}_geocodebox').autocomplete({ + source: function (request, response) { + jQuery.get({$id}_api_endpoint+\$('#{$id}_geocodebox').val(), { + query: request.term + }, function (data) { + response($.map( data.results, function( item ) { + return { + label: item.formatted_address, + id: item.geometry.location.lat+'|'+item.geometry.location.lng + } + })); + }); + }, + minLength: 5, + select: function( event, ui ) { + var tmp = ui.item.id.split('|'); + var lat = tmp[0]; + var lng = tmp[1]; + + \$('input[name=\"".$this->latitude->getName()."\"]','#{$id}').val( lat ); + \$('input[name=\"".$this->longitude->getName()."\"]','#{$id}').val( lng ); + ".(($this->with_reverse == true) ? "\$('#{$id}').trigger('lat_lon_updated');":"")." + + {$update_map_func} + + } + }); + " + ); + } + + if ($this->with_map == true) { + $this->addCss("#{$form->getId()} #{$id}-map {width: {$this->mapwidth}; height: {$this->mapheight}; }"); + $this->addJs( + " + var {$id}_latlng = {lat: ".$this->latitude->getValues().", lng: ".$this->longitude->getValues()."}; + + var {$id}_map = new google.maps.Map(document.getElementById('{$id}-map'), { + center: {$id}_latlng, + mapTypeId: {$this->maptype}, + scrollwheel: ".($this->scrollwheel ? 'true' : 'false').", + zoom: {$this->zoom} + }); + var {$id}_marker = new google.maps.Marker({ + map: {$id}_map, + draggable: true, + animation: google.maps.Animation.DROP, + position: {$id}_latlng, + title: '".(($this->markertitle == null) ? + "lat: ".$this->latitude->getValues().", lng: ".$this->longitude->getValues() : + $this->markertitle)."' + }); + \$.data( \$('#{$id}-map')[0] , 'map_obj', {$id}_map); + \$.data( \$('#{$id}-map')[0] , 'marker_obj', {$id}_marker); + + google.maps.event.addListener({$id}_marker, 'dragend', function() { + var mapdiv = {$id}_marker.map.getDiv(); + \$('input[name=\"".$this->latitude->getName()."\"]','#'+\$(mapdiv).parent(). + attr('id')).val( {$id}_marker.getPosition().lat() ); + \$('input[name=\"".$this->longitude->getName()."\"]','#'+\$(mapdiv).parent(). + attr('id')).val( {$id}_marker.getPosition().lng() ); + ".(($this->with_reverse == true) ? "\$('#{$id}').trigger('lat_lon_updated');":"")." + });" + ); + + if ($this->lat_lon_type == 'textfield') { + $this->addJs( + "\$('input[name=\"".$this->latitude->getName()."\"],input[name=\"".$this->longitude->getName()."\"]','#{$id}') + .change(function(evt){ + var map = \$.data( \$('#{$id}-map')[0] , 'map_obj'); + var marker = \$.data( \$('#{$id}-map')[0] , 'marker_obj'); + var lat = \$('input[name=\"".$this->latitude->getName()."\"]','#{$id}').val(); + var lng = \$('input[name=\"".$this->longitude->getName()."\"]','#{$id}').val(); + marker.setPosition( new google.maps.LatLng( lat, lng ) ); + map.panTo( new google.maps.LatLng( lat, lng ) ); + });" + ); + } + } + + if ($this->with_reverse == true) { + $this->addJs( + "var {$id}_geocoder = new google.maps.Geocoder; + \$('#{$id}').bind('lat_lon_updated',function(evt){ + var latlng = { + lat: parseFloat( \$('input[name=\"".$this->latitude->getName()."\"]','#{$id}').val() ), + lng: parseFloat( \$('input[name=\"".$this->longitude->getName()."\"]','#{$id}').val() ) + }; + {$id}_geocoder.geocode({'location': latlng}, function(results, status) { + if (status === 'OK') { + \$('#{$id}_reverse_geocodebox').val( JSON.stringify(results) ); + } else { + \$('#{$id}_reverse_geocodebox').val('Geocoder failed due to: ' + status); + } + }); + });" + ); + + if ($this->lat_lon_type == 'textfield') { + $this->addJs( + "\$('input[name=\"".$this->latitude->getName()."\"],input[name=\"".$this->longitude->getName()."\"]','#{$id}') + .change(function(evt){ + \$('#{$id}').trigger('lat_lon_updated'); + });" + ); + } + } + + if ($this->with_current_location == true) { + $update_map_func = ""; + if ($this->with_map == true) { + $update_map_func = " + var map = \$.data( \$('#{$id}-map')[0] , 'map_obj'); + var marker = \$.data( \$('#{$id}-map')[0] , 'marker_obj'); + marker.setPosition( new google.maps.LatLng( lat, lng ) ); + map.panTo( new google.maps.LatLng( lat, lng ) ); + "; + } + $this->addJs( + "\$('button.current_location','#{$id}') + .click(function(evt){ + evt.preventDefault(); + var lat = \$('input[name=\"".$this->latitude->getName()."\"]','#{$id}').val(); + var lng = \$('input[name=\"".$this->longitude->getName()."\"]','#{$id}').val(); + + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(function(position) { + lat = position.coords.latitude; + lng = position.coords.longitude; + \$('input[name=\"".$this->latitude->getName()."\"]','#{$id}').val(lat); + \$('input[name=\"".$this->longitude->getName()."\"]','#{$id}').val(lng); + ".(($this->with_reverse == true) ? "\$('#{$id}').trigger('lat_lon_updated');":"")." + + {$update_map_func} + }, function() { + /*handleLocationError();*/ + }); + } + });" + ); + } + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + $this->tag = 'div'; + + $required = ($this->validate->hasValue('required')) ? '*' : ''; + $requiredafter = $requiredbefore = $required; + if ($this->required_position == 'before') { + $requiredafter = ''; + $requiredbefore = $requiredbefore.' '; + } else { + $requiredbefore = ''; + $requiredafter = ' '.$requiredafter; + } + + if (!empty($this->title) && $this->tooltip == true && !in_array('title', array_keys($this->attributes))) { + $this->attributes['title'] = strip_tags($this->getText($this->title).$required); + } + + $tag = new TagElement([ + 'tag' => $this->tag, + 'id' => $id, + 'attributes' => $this->attributes, + ]); + + if (!empty($this->title)) { + if ($this->tooltip == false) { + $this->label_class .= " label-" .$this->getElementClassName(); + $this->label_class = trim($this->label_class); + $tag_label = new TagElement([ + 'tag' => 'label', + 'attributes' => [ + 'for' => $id, + 'class' => $this->label_class, + 'text' => $requiredbefore + ], + ]); + $tag_label->addChild($this->getText($this->title)); + $tag_label->addChild($requiredafter); + $tag->addChild($tag_label); + } else { + $id = $this->getHtmlId(); + $form->addJs("\$('#{$id}','#{$form->getId()}').tooltip();"); + } + } + + if ($this->with_geocode == true) { + $tag->addChild($this->geocode_box->renderHTML($form)); + } + + if ($this->with_map == true) { + $tag->addChild(new TagElement([ + 'tag' => 'div', + 'id' => "{$id}-map", + 'attributes' => ['class' => 'gmap'], + ])); + } + + $tag->addChild($this->latitude->renderHTML($form)); + $tag->addChild($this->longitude->renderHTML($form)); + + if ($this->with_current_location == true) { + $tag->addChild($this->current_location_btn->renderHTML($form)); + } + + if ($this->with_reverse == true) { + $tag->addChild($this->reverse_geocode_box->renderHTML($form)); + } + + return $tag; + } +} diff --git a/src/classes/fields/Hidden.php b/src/classes/fields/Hidden.php new file mode 100644 index 00000000..83d49ff2 --- /dev/null +++ b/src/classes/fields/Hidden.php @@ -0,0 +1,70 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\Basics\Html\TagElement; + +/** + * The hidden input field class + */ +class Hidden extends Field +{ + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + $this->container_tag = ''; + $this->container_class = ''; + parent::__construct($options, $name); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + $tag = new TagElement([ + 'tag' => 'input', + 'type' => 'hidden', + 'id' => $id, + 'name' => $this->name, + 'value' => $this->getValues(), + 'attributes' => $this->attributes, + ]); + return $tag; + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue() : bool + { + return true; + } +} diff --git a/src/classes/fields/ImageButton.php b/src/classes/fields/ImageButton.php new file mode 100644 index 00000000..3bfa5851 --- /dev/null +++ b/src/classes/fields/ImageButton.php @@ -0,0 +1,105 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Abstracts\Fields\Clickable; + +/** + * The image submit input type field class + */ +class ImageButton extends Clickable +{ + /** + * image source + * + * @var string + */ + protected $src; + + /** + * image alternate + * + * @var string + */ + protected $alt; + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + $this->default_value = [ + 'x'=>-1, + 'y'=>-1, + ]; + + parent::__construct($options, $name); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + + $tag = new TagElement([ + 'tag' => 'input', + 'type' => 'image', + 'id' => $id, + 'name' => $this->name, + 'value_needed' => false, + 'attributes' => $this->attributes + ['src' => $this->src, 'alt' => $this->alt], + ]); + return $tag; + } + + /** + * {@inheritdoc} + * + * @param array $request request array + */ + public function alterRequest(array &$request) + { + foreach ($request as $key => $val) { + //IMAGE BUTTONS HANDLE + if (preg_match('/^(.*?)_(x|y)$/', $key, $matches) && $this->getName() == $matches[1]) { + //assume this is an input type="image" + if (isset($request[$matches[1].'_'.(($matches[2] == 'x')?'y':'x')])) { + $request[$matches[1]] = [ + 'x'=>$request[$matches[1].'_x'], + 'y'=>$request[$matches[1].'_y'], + ]; + + unset($request[$matches[1].'_x']); + unset($request[$matches[1].'_y']); + } + } + } + } +} diff --git a/src/classes/fields/ImageCaptcha.php b/src/classes/fields/ImageCaptcha.php new file mode 100644 index 00000000..7e582e6c --- /dev/null +++ b/src/classes/fields/ImageCaptcha.php @@ -0,0 +1,309 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Abstracts\Fields\Captcha; +use Degami\PHPFormsApi\FormBuilder; + +/** + * The image captcha field class + */ +class ImageCaptcha extends Captcha +{ + /** + * output image type + * + * @var string + */ + protected $out_type = 'png'; + + /** + * availables characters for code + * + * @var string + */ + protected $characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + /** + * minimum code length + * + * @var integer + */ + protected $min_length = 5; + + /** + * maximum code length + * + * @var integer + */ + protected $max_length = 8; + + /** + * image width + * + * @var integer + */ + protected $image_width = 100; + + /** + * image height + * + * @var integer + */ + protected $image_height = 50; + + /** + * text font size + * + * @var integer + */ + protected $font_size = 14; + + /** + * pre-fill code into textfield + * + * @var boolean + */ + protected $pre_filled = false; + + /** + * captcha code + * + * @var string + */ + private $code; + + /** + * Gets a random text + * + * @return string text + */ + private function getRandomText(): string + { + $this->code = ''; + $length = mt_rand($this->min_length, $this->max_length); + while (strlen($this->code) < $length) { + $this->code .= substr($this->characters, mt_rand() % (strlen($this->characters)), 1); + } + + if (FormBuilder::sessionPresent()) { + $this->getSessionBag()->ensurePath("/image_captcha_code"); + $this->getSessionBag()->image_captcha_code->{$this->getName()} = $this->code; + } + + return $this->code; + } + + /** + * Gets a random color + * + * @param resource $im image resource + * @return integer + */ + private function getRandomColor($im): int + { + // never white, never black + return imagecolorallocate($im, mt_rand(20, 185), mt_rand(20, 185), mt_rand(20, 185)); + } + + /** + * Adds noise to image + * + * @param resource $im image resource + */ + private function addNoise($im) + { + for ($i = 0; $i < $this->image_width; $i++) { + for ($j = 0; $j < $this->image_height; $j++) { + if ((mt_rand(0, 255) % mt_rand(7, 11) == 0) && (mt_rand(0, 1) == 1)) { + $color = $this->getRandomColor($im); + imagesetpixel($im, $i, $j, $color); + } + } + } + } + + /** + * Adds arcs to image + * + * @param resource $im image resource + */ + private function addArcs($im) + { + for ($i = 0; $i < 50; $i++) { + //imagefilledrectangle($im, $i + $i2, 5, $i + $i3, 70, $black); + imagesetthickness($im, rand(1, 2)); + imagearc( + $im, + mt_rand(1, 300), // x-coordinate of the center. + mt_rand(1, 300), // y-coordinate of the center. + mt_rand(1, 300), // The arc width. + mt_rand(1, 300), // The arc height. + mt_rand(1, 300), // The arc start angle, in degrees. + mt_rand(1, 300), // The arc end angle, in degrees. + $this->getRandomColor($im) // A color identifier. + ); + } + } + + /** + * Gets image as base64 string + * + * @return string image representation as base64 string + */ + private function getImageString(): string + { + $text = $this->getRandomText(); + + $im = imagecreate($this->image_width, $this->image_height); + + $white = imagecolorallocate($im, 255, 255, 255); + $grey = imagecolorallocate($im, 128, 128, 128); + + $font = dirname(dirname(dirname(__FILE__))).'/fonts/Lato-Regular.ttf'; + imagefilledrectangle($im, 0, 0, $this->image_width, $this->image_height, $white); + + $x = 5; + foreach (str_split($text) as $character) { + $angle = mt_rand(-10, 10); + $size = mt_rand($this->font_size - 4, $this->font_size + 4); + $y = $this->image_height - $size - 5; + $occ_space = (int)($size * 0.925); + + if (($x + $occ_space + 5) > $this->image_width) { + $new_width = $x + $occ_space + 5; + $new_img = imagecreate($new_width, $this->image_height); + imagepalettecopy($new_img, $im); + imagefilledrectangle($new_img, 0, 0, $new_width, $this->image_height, $white); + + imagecopy($new_img, $im, 0, 0, 0, 0, $this->image_width, $this->image_height); + $this->image_width = $new_width; + imagedestroy($im); + $im = $new_img; + } + + imagettftext($im, $size, $angle, $x+1, $y, $grey, $font, $character); + imagettftext($im, $size, $angle, $x, $y+1, $this->getRandomColor($im), $font, $character); + + $x += $occ_space; + } + + $this->addArcs($im); + $this->addNoise($im); + + ob_start(); + switch ($this->out_type) { + case 'jpg': + case 'jpeg': + $this->out_type = 'jpeg'; + imagejpeg($im); + break; + case 'gif': + imagegif($im); + break; + case 'png': + default: + $this->out_type = 'png'; + imagepng($im); + break; + } + $data = ob_get_contents(); + ob_end_clean(); + + imagedestroy($im); + + $base64 = 'data:image/' . $this->out_type . ';base64,' . base64_encode($data); + return $base64; + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); +// $attributes = $this->getAttributes(); + $imagestring = $this->getImageString(); + $codeval = $this->pre_filled == true ? $this->code : ''; + + $tag = new TagElement([ + 'tag' => 'div', + 'id' => $id, + 'attributes' => $this->attributes, + ]); + $tag->addChild(new TagElement([ + 'tag' => 'img', + 'attributes' => ['src' => $imagestring, 'border' => 0], + ])); + $tag->addChild(new TagElement([ + 'tag' => 'input', + 'type' => 'text', + 'name' => $this->name."[code]", + 'value' => $codeval, + ])); + + if (!FormBuilder::sessionPresent()) { + $tag->addChild(new TagElement([ + 'tag' => 'input', + 'type' => 'hidden', + 'name' => $this->name."[code_chk]", + 'attributes' => [ + 'class' => FORMS_FIELD_ADDITIONAL_CLASS.' hidden', + ], + 'value' => sha1($this->code . substr(md5(static::class), 0, 5)), + ])); + } + return $tag; + } + + /** + * {@inheritdoc} + * + * @return boolean TRUE if element is valid + */ + public function isValid() : bool + { + if ($this->already_validated == true) { + return true; + } + if (isset($this->value['already_validated']) && $this->value['already_validated'] == true) { + return true; + } + + if (!FormBuilder::sessionPresent()) { + if (isset($this->value['code']) && isset($this->value['code_chk']) + && sha1($this->value['code'].substr(md5(static::class), 0, 5)) == $this->value['code_chk'] + ) { + return true; + } + } else { + if (isset($this->value['code']) + && $this->value['code'] == $this->getSessionBag()->image_captcha_code->{$this->getName()} + ) { + return true; + } + } + + $this->addError($this->getText("Captcha response is not valid"), __FUNCTION__); + return false; + } +} diff --git a/src/classes/fields/Leafletlocation.php b/src/classes/fields/Leafletlocation.php new file mode 100644 index 00000000..49f9b7a1 --- /dev/null +++ b/src/classes/fields/Leafletlocation.php @@ -0,0 +1,289 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\Basics\Html\TagElement; + +/** + * The leaflet maps geolocation field class + */ +class Leafletlocation extends Geolocation +{ + + /** + * MapBox accessToken + * + * @see https://www.mapbox.com/about/maps/ + * @var string + */ + protected $accessToken = null; + + /** + * zoom + * + * @var integer + */ + protected $zoom = 8; + + /** + * map width + * + * @var string + */ + protected $mapwidth = '100%'; + + /** + * map height + * + * @var string + */ + protected $mapheight = '500px'; + + /** + * marker title + * + * @var null + */ + protected $markertitle = null; + + /** + * map type - one of: + * mapbox.streets + * mapbox.light + * mapbox.dark + * mapbox.satellite + * mapbox.streets-satellite + * mapbox.wheatpaste + * mapbox.streets-basic + * mapbox.comic + * mapbox.outdoors + * mapbox.run-bike-hike + * mapbox.pencil + * mapbox.pirates + * mapbox.emerald + * mapbox.high-contrast + * + * @var string + */ + protected $maptype = 'mapbox.streets'; + + /** + * input type where latitude and longitude are stored (hidden / textfield) + * + * @var string + */ + protected $lat_lon_type = 'hidden'; + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + parent::__construct($options, $name); + $defaults = isset($options['default_value']) ? $options['default_value'] : ['latitude' => 0, 'longitude' => 0]; + + unset($options['title']); + unset($options['prefix']); + unset($options['suffix']); + $options['container_tag'] = ''; + + $opt = $options; + $opt['type'] = 'hidden'; + $opt['attributes']['class'] = 'latitude'; + if ($this->lat_lon_type == 'textfield') { + $opt['type'] = 'textfield'; + } + $opt['default_value'] = (is_array($defaults) && isset($defaults['latitude'])) ? $defaults['latitude'] : 0; + if ($this->lat_lon_type == 'textfield') { + $opt['suffix'] = $this->getText('latitude').' '; + } + if ($this->lat_lon_type == 'textfield') { + $this->latitude = new Textfield($opt, $this->getSubfieldName('latitude')); + } else { + $this->latitude = new Hidden($opt, $this->getSubfieldName('latitude')); + } + + $opt = $options; + $opt['type'] = 'hidden'; + $opt['attributes']['class'] = 'longitude'; + if ($this->lat_lon_type == 'textfield') { + $opt['type'] = 'textfield'; + } + $opt['default_value'] = (is_array($defaults) && isset($defaults['longitude'])) ? $defaults['longitude'] : 0; + if ($this->lat_lon_type == 'textfield') { + $opt['suffix'] = $this->getText('longitude').' '; + } + if ($this->lat_lon_type == 'textfield') { + $this->longitude = new Textfield($opt, $this->getSubfieldName('longitude')); + } else { + $this->longitude = new Hidden($opt, $this->getSubfieldName('longitude')); + } + } + + + /** + * {@inheritdoc} . it simply calls the sub elements preprocess + * + * @param string $process_type preprocess type + */ + public function preProcess($process_type = "preprocess") + { + parent::preprocess($process_type); + } + + + /** + * Return field value + * + * @return mixed field value + */ + public function getValues() + { + return parent::getValues(); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + + $this->addCss("#{$form->getId()} #{$id}-map {width: {$this->mapwidth}; height: {$this->mapheight}; }"); + $this->addJs( + "var {$id}_latlng = { + lat: ".$this->latitude->getValues().", + lng: ".$this->longitude->getValues()." + }; + var {$id}_map = L.map('{$id}-map').setView([{$id}_latlng.lat,{$id}_latlng.lng],{$this->zoom}); + L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', { + attribution: + 'Map data © OpenStreetMap contributors,'+ + 'CC-BY-SA,'+ + ' Imagery © Mapbox', + maxZoom: 18, + id: '{$this->maptype}', + accessToken: '{$this->accessToken}' + }).addTo({$id}_map); + + var {$id}_marker = L.marker([{$id}_latlng.lat, {$id}_latlng.lng],{ + draggable: true + }).addTo({$id}_map); + + {$id}_marker.on('dragend', function(e){ + {$id}_map.panTo( {$id}_marker.getLatLng() ); + \$('input[name=\"{$id}_latitude\"]','#{$id}').val( {$id}_marker.getLatLng().lat ); + \$('input[name=\"{$id}_longitude\"]','#{$id}').val( {$id}_marker.getLatLng().lng ); + }); + + \$.data( \$('#{$id}-map')[0] , 'map_obj', {$id}_map); + \$.data( \$('#{$id}-map')[0] , 'marker_obj', {$id}_marker); + " + ); + + if ($this->lat_lon_type == 'textfield') { + $this->addJs( + "\$('input[name=\"{$id}_latitude\"],input[name=\"{$id}_longitude\"]','#{$id}') + .change(function(evt){ + var map = \$.data( \$('#{$id}-map')[0] , 'map_obj'); + var marker = \$.data( \$('#{$id}-map')[0] , 'marker_obj'); + var lat = \$('input[name=\"{$id}_latitude\"]','#{$id}').val(); + var lng = \$('input[name=\"{$id}_longitude\"]','#{$id}').val(); + + map.panTo(L.latLng(lat, lng)); + marker.setLatLng(L.latLng(lat, lng)); + });" + ); + } + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + $attributes = $this->getAttributes(); + + $this->tag = 'div'; + + $required = ($this->validate->hasValue('required')) ? '*' : ''; + $requiredafter = $requiredbefore = $required; + if ($this->required_position == 'before') { + $requiredafter = ''; + $requiredbefore = $requiredbefore.' '; + } else { + $requiredbefore = ''; + $requiredafter = ' '.$requiredafter; + } + + if (!empty($this->title) && $this->tooltip == true && !in_array('title', array_keys($this->attributes))) { + $this->attributes['title'] = strip_tags($this->getText($this->title).$required); + } + + $tag = new TagElement([ + 'tag' => $this->tag, + 'id' => $id, + 'attributes' => $this->attributes, + ]); + + if (!empty($this->title)) { + if ($this->tooltip == false) { + $this->label_class .= " label-" .$this->getElementClassName(); + $this->label_class = trim($this->label_class); + $tag_label = new TagElement([ + 'tag' => 'label', + 'attributes' => [ + 'for' => $id, + 'class' => $this->label_class, + 'text' => $requiredbefore + ], + ]); + $tag_label->addChild($this->getText($this->title)); + $tag_label->addChild($requiredafter); + $tag->addChild($tag_label); + } else { + $id = $this->getHtmlId(); + $form->addJs("\$('#{$id}','#{$form->getId()}').tooltip();"); + } + } + + $tag->addChild(new TagElement([ + 'tag' => 'div', + 'id' => "{$id}-map", + 'attributes' => ['class' => 'leafletmap'], + ])); + + $tag->addChild($this->latitude->renderHTML($form)); + $tag->addChild($this->longitude->renderHTML($form)); + + return $tag; + } +} diff --git a/src/classes/fields/Markup.php b/src/classes/fields/Markup.php new file mode 100644 index 00000000..6f1019b3 --- /dev/null +++ b/src/classes/fields/Markup.php @@ -0,0 +1,72 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; + +/** + * The markup field class. + * this is not a value + */ +class Markup extends Field +{ + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + parent::__construct($options, $name); + if (isset($options['value'])) { + $this->value = $options['value']; + } + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element value + */ + public function renderField(Form $form) + { + return $this->getValues(); + } + + /** + * validate function + * + * @return bool this field is always valid + */ + public function isValid() : bool + { + return true; + } + + /** + * {@inheritdoc} + * + * @return bool this is not a value + */ + public function isAValue() : bool + { + return false; + } +} diff --git a/src/classes/fields/Maskedfield.php b/src/classes/fields/Maskedfield.php new file mode 100644 index 00000000..90bc5ea8 --- /dev/null +++ b/src/classes/fields/Maskedfield.php @@ -0,0 +1,98 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\PHPFormsApi\Form; + +/** + * The "masked" text input field class + */ +class Maskedfield extends Textfield +{ + /** + * input mask string + * + * @var string + */ + protected $mask; + + /** + * jQuery Mask Plugin patterns + * + * @var array + */ + private $translation = [ + '0' => "\d", + '9' => "\d?", + '#' => "\d+", + 'A' => "[a-zA-Z0-9]", + 'S' => "[a-zA-Z]", + ]; + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options, ?string $name = null) + { + if (!isset($options['attributes']['class'])) { + $options['attributes']['class'] = ''; + } + $options['attributes']['class'].=' maskedfield'; + + parent::__construct($options, $name); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + $this->addJs("\$('#{$id}','#{$form->getId()}').mask('{$this->mask}');"); + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @return boolean this TRUE if this element conforms to mask + */ + public function isValid() : bool + { + $mask = $this->mask; + $mask = preg_replace("(\[|\]|\(|\))", "\\\1", $mask); + foreach ($this->translation as $search => $replace) { + $mask = str_replace($search, $replace, $mask); + } + $mask = '/^'.$mask.'$/'; + if (!preg_match($mask, $this->value)) { + $this->addError($this->getText("Value does not conform to mask"), __FUNCTION__); + + if ($this->stop_on_first_error) { + return false; + } + } + + return parent::isValid(); + } +} diff --git a/src/classes/fields/MathCaptcha.php b/src/classes/fields/MathCaptcha.php new file mode 100644 index 00000000..2fc204b1 --- /dev/null +++ b/src/classes/fields/MathCaptcha.php @@ -0,0 +1,192 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Abstracts\Fields\Captcha; +use Degami\PHPFormsApi\FormBuilder; + +/** + * The image captcha field class + */ +class MathCaptcha extends Captcha +{ + /** + * pre-fill code into textfield + * + * @var boolean + */ + protected $pre_filled = false; + + /** @var string challenge code */ + private $code; + + /** @var integer first operator */ + private $a; + + /** @var integer second operator */ + private $b; + + /** @var string operation */ + private $op; + + /** + * Get a math challenge code + * + * @return string challenge string + */ + private function getMathCode(): string + { + $this->code = ''; + $operators = ['+','-','*','/']; + $this->a = mt_rand(0, 50); + $this->op = $operators[ mt_rand(0, count($operators)-1) ]; + + $ret = null; + do { + $this->b = mt_rand(1, 10); + eval('$ret = '.$this->a.$this->op.$this->b.';'); + } while (!is_int($ret)); + + if (FormBuilder::sessionPresent()) { + $this->getSessionBag()->ensurePath("/math_captcha_code"); + $this->getSessionBag()->math_captcha_code->{$this->getName()} = $this->a.$this->op.$this->b; + } + + if (mt_rand(0, 1) == 0) { + $this->code .= ''.mt_rand(1, 10).$operators[ mt_rand(0, count($operators)-1) ].''; + } + $this->code .= $this->a; + if (mt_rand(0, 1) == 0) { + $this->code .= ''.$operators[ mt_rand(0, count($operators)-1) ].mt_rand(1, 10).''; + } + $this->code .= $this->op; + if (mt_rand(0, 1) == 0) { + $this->code .= ''.mt_rand(1, 10).$operators[ mt_rand(0, count($operators)-1) ].''; + } + $this->code .= $this->b; + if (mt_rand(0, 1) == 0) { + $this->code .= ''.$operators[ mt_rand(0, count($operators)-1) ].mt_rand(1, 10).''; + } + + return $this->code; + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + $this->addJs("\$('#{$id} .nohm','#{$form->getId()}').css({'display': 'none'});"); + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + $attributes = $this->getAttributes(); + $this->getMathCode(); + $codeval = ''; + if ($this->pre_filled == true) { + eval('$codeval = '.$this->a.$this->op.$this->b.';'); + } + + $tag = new TagElement([ + 'tag' => 'div', + 'id' => $id, + 'attributes' => $this->attributes, + 'text' => $this->code, + ]); + $tag->addChild(new TagElement([ + 'tag' => 'input', + 'type' => 'text', + 'name' => $this->name."[code]", + 'attributes' => [ + 'class' => FORMS_FIELD_ADDITIONAL_CLASS.' textfield', + ], + 'value' => $codeval, + ])); + + if (!FormBuilder::sessionPresent()) { + $tag->addChild(new TagElement([ + 'tag' => 'input', + 'type' => 'hidden', + 'name' => $this->name."[code_chk]", + 'attributes' => [ + 'class' => FORMS_FIELD_ADDITIONAL_CLASS.' hidden', + ], + 'value' => sha1(eval("return {$this->a}{$this->op}{$this->b};") . substr(md5(static::class), 0, 5)), + ])); + } + + // @todo. if (!FormBuilder::sessionPresent()) add an hidden input with encoded captcha code + + return $tag; + } + + /** + * {@inheritdoc} + * + * @return boolean TRUE if element is valid + */ + public function isValid() : bool + { + if ($this->already_validated == true) { + return true; + } + if (isset($this->value['already_validated']) && $this->value['already_validated'] == true) { + return true; + } + + if (!FormBuilder::sessionPresent()) { + if (isset($this->value['code']) && isset($this->value['code_chk']) && sha1($this->value['code'].substr(md5(static::class), 0, 5)) == $this->value['code_chk']) { + return true; + } + + $this->addError($this->getText("Captcha response is not valid"), __FUNCTION__); + return false; + } else { + if (!isset($this->getSessionBag()->math_captcha_code->{$this->getName()})) { + return true; + } + + $_sessval = null; + if (trim($this->getSessionBag()->math_captcha_code->{$this->getName()}) != '') { + eval('$_sessval = '.$this->getSessionBag()->math_captcha_code->{$this->getName()}.';'); + if (isset($this->value['code']) && $this->value['code'] == $_sessval) { + return true; + } + + $this->addError($this->getText("Captcha response is not valid"), __FUNCTION__); + return false; + } + } + } +} diff --git a/src/classes/fields/Multiselect.php b/src/classes/fields/Multiselect.php new file mode 100644 index 00000000..2ce43547 --- /dev/null +++ b/src/classes/fields/Multiselect.php @@ -0,0 +1,235 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\Basics\Html\TagElement; + +/** + * The "Multiselect select" field class + */ +class Multiselect extends Select +{ + /** @var array options on the left side */ + private $leftOptions; + + /** @var array options on the right side */ + private $rightOptions; + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options, ?string $name = null) + { + if (!is_array($options)) { + $options = []; + } + $options['multiple'] = true; + parent::__construct($options, $name); + + $this->leftOptions = $this->options; + $this->rightOptions = []; + + foreach ($this->getDefaultValue() as $value) { + foreach ($this->leftOptions as $k => $v) { + /** @var Option $v */ + if ($v->getKey() == $value) { + $this->rightOptions[] = clone $v; + unset($this->leftOptions[$k]); + } + } + } + + $this->setAttribute('style', 'width: 100%;'); + } + + + /** + * {@inheritdocs} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + $this->addJs( + "\$('#{$id}_move_right, #{$id}_move_left','#{$form->getId()}') + .click(function(evt){ + evt.preventDefault(); + var \$this = \$(this); + var \$from = \$('#{$id}_from','#{$form->getId()}'); + var \$to = \$('#{$id}_to','#{$form->getId()}'); + + if( /_move_right\$/i.test(\$this.attr('id')) ){ + \$from.find('option:selected').each(function(index,elem){ + var \$elem = \$(elem); \$elem.appendTo(\$to); + }); + } + if( /_move_left\$/i.test(\$this.attr('id')) ){ + \$to.find('option:selected').each(function(index,elem){ + var \$elem = \$(elem); \$elem.appendTo(\$from); + }); + } + });" + ); + + $this->addJs( + "\$('#{$form->getId()}').submit(function(evt){ + var \$to = \$('#{$id}_to','#{$form->getId()}'); + \$to.find('option').each(function(index,elem){elem.selected=true;}); + });" + ); + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param array $value value to set + */ + public function processValue($value = []) + { + parent::processValue($value); + + $this->leftOptions = $this->options; + $this->rightOptions = []; + + $values = $this->getValue(); + foreach (array_values($values) as $keyval) { + foreach ($this->leftOptions as $k => $v) { + /** @var Option $v */ + if ($v->getKey() == $keyval) { + $this->rightOptions[] = clone $v; + unset($this->leftOptions[$k]); + } + } + } + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + if ($this->hasErrors()) { + $this->attributes['class'] .= ' has-errors'; + } + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + + $field_name = "{$this->name}[]"; + + $table = new TagElement([ + 'tag' => 'table', + 'id' => $id.'-table', + 'attributes' => [ + 'boder' => 0, + 'colspan' => 0, + 'cellpadding' => 0, + ], + ]); + + $tr1 = new TagElement(['tag' => 'tr']); + $table->addChild($tr1); + + $td1 = new TagElement(['tag' => 'td', 'attributes' => ['style' => 'width: 45%']]); + $td2 = new TagElement(['tag' => 'td', 'attributes' => ['style' => 'width: 10%']]); + $td3 = new TagElement(['tag' => 'td', 'attributes' => ['style' => 'width: 45%']]); + + $tr1->addChild($td1); + $tr1->addChild($td2); + $tr1->addChild($td3); + + $select_left = new TagElement([ + 'tag' => 'select', + 'name' => $this->name.'_from', + 'id' => $id.'_from', + 'attributes' => $this->attributes + ['size' => $this->size, 'multiple' => 'multiple'], + ]); + + if (isset($this->attributes['placeholder']) && !empty($this->attributes['placeholder'])) { + $select_left->addChild(new TagElement([ + 'tag' => 'option', + 'attributes' => [ + 'disabled' => 'disabled', + ] + (isset($this->default_value) ? [] : ['selected' => 'selected']), + 'text' => $this->attributes['placeholder'], + ])); + } + foreach ($this->leftOptions as $key => $value) { + /** @var Option $value */ + $select_left->addChild($value->renderHTML($this)); + } + $td1->addChild($select_left); + + $buttons = new TagElement([ + 'tag' => 'div', 'attributes' => ['class' => 'buttons'], + ]); + $buttons->addChild(new TagElement([ + 'tag' => 'button', + 'id' => $this->name.'_move_right', + 'text' => '>>', + ])) + ->addChild(new TagElement(['tag' => 'br'])) + ->addChild(new TagElement(['tag' => 'br'])) + ->addChild(new TagElement([ + 'tag' => 'button', + 'id' => $this->name.'_move_left', + 'text' => '<<', + ])); + $td2->addChild($buttons); + + $select_right = new TagElement([ + 'tag' => 'select', + 'name' => $field_name, + 'id' => $id.'_to', + 'attributes' => $this->attributes + ['size' => $this->size, 'multiple' => 'multiple'], + ]); + + if (isset($this->attributes['placeholder']) && !empty($this->attributes['placeholder'])) { + $select_right->addChild(new TagElement([ + 'tag' => 'option', + 'attributes' => [ + 'disabled' => 'disabled', + ] + (isset($this->default_value) ? [] : ['selected' => 'selected']), + 'text' => $this->attributes['placeholder'], + ])); + } + foreach ($this->rightOptions as $key => $value) { + /** @var Option $value */ + $select_right->addChild($value->renderHTML($this)); + } + $td3->addChild($select_right); + + return $table; + } +} diff --git a/src/classes/fields/Number.php b/src/classes/fields/Number.php new file mode 100644 index 00000000..abeedb67 --- /dev/null +++ b/src/classes/fields/Number.php @@ -0,0 +1,112 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\Basics\Html\TagElement; + +/** + * The number input field class + */ +class Number extends Field +{ + /** + * minimum value + * + * @var null + */ + protected $min = null; + + /** + * maximum value + * + * @var null + */ + protected $max = null; + + /** + * step value + * + * @var integer + */ + protected $step = 1; + + /** + * Class constructor + * + * @param array $options build options + * @param string $name field name + */ + public function __construct($options = [], $name = null) + { + parent::__construct($options, $name); + + // ensure is numeric validator is present + $this->getValidate()->addElement('numeric'); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + if ($this->hasErrors()) { + $this->attributes['class'] .= ' has-errors'; + } + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + + $this->attributes['size'] = $this->size; + if (is_numeric($this->min) && is_numeric($this->max) && $this->max >= $this->min) { + $this->attributes += [ + 'size' => $this->size, + 'min' => $this->min, + 'max' => $this->max, + 'step' => $this->step + ]; + } + + $tag = new TagElement([ + 'tag' => 'input', + 'type' => 'number', + 'id' => $id, + 'name' => $this->name, + 'value' => $this->getValues(), + 'attributes' => $this->attributes, + ]); + return $tag; + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue() : bool + { + return true; + } +} diff --git a/src/classes/fields/Optgroup.php b/src/classes/fields/Optgroup.php new file mode 100644 index 00000000..51a1b045 --- /dev/null +++ b/src/classes/fields/Optgroup.php @@ -0,0 +1,104 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Abstracts\Base\Element; +use Degami\PHPFormsApi\Abstracts\Fields\Optionable; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Abstracts\Fields\FieldMultivalues; + +/** + * The optgroup element class + */ +class Optgroup extends Optionable +{ + /** + * options array + * + * @var array + */ + protected $options; + + /** + * Class constructor + * + * @param string $label label + * @param array $options options array + */ + public function __construct(string $label, array $options) + { + if (isset($options['options'])) { + foreach ($options['options'] as $key => $value) { + if ($value instanceof Option) { + $this->addOption($value); + $value->setParent($this); + } elseif (is_scalar($key) && is_scalar($value)) { + $this->addOption(new Option($key, $value)); + } + } + unset($options['options']); + } + parent::__construct($label, $options); + } + + /** + * Check if key is present into element options array + * + * @param mixed $needle element to find + * @return boolean TRUE if element is present + */ + public function optionsHasKey($needle): bool + { + return FieldMultivalues::hasKey($needle, $this->options); + } + + /** + * Add option + * + * @param Option $option option to add + * @return Optgroup + */ + public function addOption(Option $option): Optgroup + { + $option->setParent($this); + $this->options[] = $option; + + return $this; + } + + /** + * render the optgroup + * + * @param Select $form_field select field + * @return TagElement the optgroup html + */ + public function renderHTML(Select $form_field): TagElement + { + $this->no_translation = $form_field->no_translation; + $tag = new TagElement([ + 'tag' => 'optgroup', + 'type' => null, + 'id' => null, + 'attributes' => $this->attributes + [ 'label' => $this->label ], + 'value_needed' => false, + 'has_close' => true, + ]); + foreach ($this->options as $option) { + $tag->addChild($option->renderHTML($form_field)); + } + return $tag; + } +} diff --git a/src/classes/fields/Option.php b/src/classes/fields/Option.php new file mode 100644 index 00000000..76a44b2a --- /dev/null +++ b/src/classes/fields/Option.php @@ -0,0 +1,103 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\PHPFormsApi\Abstracts\Base\Element; +use Degami\PHPFormsApi\Abstracts\Fields\Optionable; +use Degami\Basics\Html\TagElement; + +/** + * The option element class + */ +class Option extends Optionable +{ + /** + * option key + * + * @var string + */ + protected $key; + + /** + * Class constructor + * + * @param string $key key + * @param string $label label + * @param array $options build options + */ + public function __construct(string $key, string $label, $options = []) + { + $this->setKey(trim($key)); + parent::__construct($label, $options); + } + + /** + * render the option + * + * @param Select $form_field select field + * + * @return TagElement the option html + */ + public function renderHTML(Select $form_field): TagElement + { + $this->no_translation = $form_field->no_translation; + $field_value = $form_field->getValue(); + + if (is_array($field_value) || $form_field->isMultiple() == true) { + if (!is_array($field_value)) { + $field_value = [$field_value]; + } + if (in_array($this->key, array_values($field_value), true)) { + $this->attributes['selected'] = 'selected'; + } + } else { + if ($this->key === $field_value) { + $this->attributes['selected'] = 'selected'; + } + } + $tag = new TagElement([ + 'tag' => 'option', + 'type' => null, + 'value' => $this->key, + 'text' => $this->getText($this->label), + 'attributes' => $this->attributes + ['class' => false], + 'has_close' => true, + ]); + return $tag; + } + + /** + * Get the element key + * + * @return mixed the element key + */ + public function getKey() + { + return $this->key; + } + + /** + * Set the element key + * + * @param mixed $key element key + * @return Option + */ + public function setKey($key) + { + $this->key = $key; + + return $this; + } +} diff --git a/src/classes/fields/Otp.php b/src/classes/fields/Otp.php new file mode 100644 index 00000000..35fcf74b --- /dev/null +++ b/src/classes/fields/Otp.php @@ -0,0 +1,203 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Containers\SeamlessContainer; + +/** + * The otp input field class + */ +class Otp extends Textfield +{ + // not extending hidden has we do not want to empty container tag and class + + public const TYPE_NUMERIC = 'numeric'; + public const TYPE_ALPHA = 'alpha'; + public const TYPE_ALPHA_NUMERIC = 'alpha_numeric'; + + protected $otp_length = 6; + + protected $otp_type = self::TYPE_NUMERIC; + + protected $show_characters = false; + + protected $show_hide = false; + + public function __construct($options = [], ?string $name = null) + { + parent::__construct($options, $name); + + // do some checks + if (isset($options['maxlength'])) { + $this->otp_length = $options['maxlength']; + } + if (isset($options['otp_length'])) { + $this->minlength = $this->maxlength = $this->otp_length; + } + if ($this->show_characters == true) { + $this->show_hide = false; + } + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + $this->addJs(" + $('input', '#{$id}_digits').on('change', function() { + var value = ''; + for (var i = 0; i < ".$this->otp_length."; i++) { + value += $('#{$id}_' + i).val(); + } + $('#{$id}').val(value); + }); + "); + + if ($this->show_hide) { + $this->addJs(" + $('.otp-show-hide', '#{$id}_digits').on('click', function(e) { + e.preventDefault(); + var inputs = $('#{$id}_digits input'); + if (inputs.attr('type') === 'password') { + inputs.attr('type', 'text'); + $(this).text('".$this->getText('Hide')."'); + } else { + inputs.attr('type', 'password'); + $(this).text('".$this->getText('Show')."'); + } + }); + "); + } + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + if ($this->hasErrors()) { + $this->attributes['class'] .= ' has-errors'; + } + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + if (is_array($this->value)) { + $this->value = ''; + } + + $container = new SeamlessContainer([ + 'tag' => 'div', + 'class' => 'otp-container', + ]); + + $container->addMarkup('
    '); + for ($i = 0; $i < $this->otp_length; $i++) { + + $onInput = ""; + // note only one validator is allowed on js side + $fieldValidators = array_map(fn ($validator) => (is_array($validator) ? $validator['validator'] : $validator), $this->getValidate()->toArray()); + $fieldValidators = array_filter($fieldValidators, fn ($validator) => in_array($validator, ['alpha_numeric', 'alpha', 'numeric', 'integer'])); + + if (count($fieldValidators) == 0) { + switch ($this->otp_type) { + case self::TYPE_ALPHA_NUMERIC: + $fieldValidators[] = 'alpha_numeric'; + break; + case self::TYPE_ALPHA: + $fieldValidators[] = 'alpha'; + break; + case self::TYPE_NUMERIC: + default: + $fieldValidators[] = 'numeric'; + break; + } + } + + if (count($fieldValidators) && in_array('alpha_numeric', $fieldValidators)) { + $onInput = "this.value = this.value.replace(/[^a-zA-Z0-9]/g, '');"; + } else if (count($fieldValidators) && in_array('alpha', $fieldValidators)) { + $onInput = "this.value = this.value.replace(/[^a-zA-Z]/g, '');"; + } else if (count($fieldValidators) && count(array_intersect($fieldValidators, ['numeric', 'integer']))) { + $onInput = "this.value = this.value.replace(/[^0-9]/g, '');"; + } + + $onInput .= "this.value = this.value.toUpperCase();"; + $onKeyUp = "if (event.key !== 'Tab' && this.value.length === 1) { document.getElementById('" . $id . '_' . ($i + 1) . "').focus(); }"; + if ($i == $this->otp_length - 1) { + $onKeyUp = "if (event.key !== 'Tab' && this.value.length === 1) { this.blur(); }"; + } + + $container->addMarkup(new TagElement([ + 'tag' => 'input', + 'type' => $this->show_characters ? 'text' : 'password', + 'id' => $id . '_' . $i, + 'name' => $this->name . '_chars' . '[' . $i . ']', + 'value' => '', + 'attributes' => [ + 'size' => 1, + 'maxlength' => 1, + 'minlength' => 1, +// 'class' => 'otp-input inline', + 'autocomplete' => 'off', + 'oninput' => $onInput, + 'onkeyup' => $onKeyUp, + 'style' => "width: auto !important; display: inline-flex; text-align: center;margin-right: 5px;", + ] + $this->attributes, + ])); + } + if ($this->show_hide) { + $container->addMarkup(new TagElement([ + 'tag' => 'a', + 'href' => '#', + 'attributes' => [ + 'class' => 'otp-show-hide', + 'style' => 'cursor: pointer; margin-left: 10px;', + ], + 'text' => $this->getText( $this->show_characters ? 'Hide' : 'Show' ), + ])); + } + $container->addMarkup('
    '); + + $container->addField( + $this->name, [ + 'type' => 'hidden', + 'default_value' => $this->getValues(), + ] + ); + + return $container->renderField($form); + } +} \ No newline at end of file diff --git a/src/classes/fields/Password.php b/src/classes/fields/Password.php new file mode 100644 index 00000000..49f76475 --- /dev/null +++ b/src/classes/fields/Password.php @@ -0,0 +1,188 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\Basics\Html\TagElement; +use Degami\Basics\Html\TagList; + +/** + * The password input field class + */ +class Password extends Field +{ + + /** + * "with confirmation" flag + * + * @var boolean + */ + protected $with_confirm = false; + + /** + * confirmation input label + * + * @var string + */ + protected $confirm_string = "Confirm password"; + + /** + * "include javascript strength check" flag + * + * @var boolean + */ + protected $with_strength_check = false; + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + if ($this->with_strength_check == true) { + $id = $this->getHtmlId(); + + $this->addJs( + " + \$('#{$id}','#{$form->getId()}').keyup(function() { + \$('#{$id}_result').html( + + (function(password){ + var strength = 0; + if (password.length < 6) { + \$('#{$id}_result').removeClass().addClass('password_strength_checker').addClass('short'); + return '".$this->getText('Too short')."'; + } + + if (password.length > 7) strength += 1; + if (password.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/)) strength += 1; + if (password.match(/([a-zA-Z])/) && password.match(/([0-9])/)) strength += 1; + if (password.match(/([!,%,&,@,#,$,^,*,?,_,~])/)) strength += 1; + if (password.match(/(.*[!,%,&,@,#,$,^,*,?,_,~].*[!,%,&,@,#,$,^,*,?,_,~])/)) strength += 1; + if (strength < 2 ){ + \$('#{$id}_result').removeClass().addClass('password_strength_checker').addClass('weak'); + return '".$this->getText('Weak')."'; + } else if (strength == 2 ) { + \$('#{$id}_result').removeClass().addClass('password_strength_checker').addClass('good'); + return '".$this->getText('Good')."'; + } else { + \$('#{$id}_result').removeClass().addClass('password_strength_checker').addClass('strong'); + return '".$this->getText('Strong')."'; + } + })(\$('#{$id}','#{$form->getId()}').val()) + + ); + });" + ); + + $this->addCss("#{$form->getId()} .password_strength_checker.short{color:#FF0000;}"); + $this->addCss("#{$form->getId()} .password_strength_checker.weak{color:#E66C2C;}"); + $this->addCss("#{$form->getId()} .password_strength_checker.good{color:#2D98F3;}"); + $this->addCss("#{$form->getId()} .password_strength_checker.strong{color:#006400;}"); + } + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + if ($this->hasErrors()) { + $this->attributes['class'] .= ' has-errors'; + } + if ($this->disabled == true) { + $this->attributes['disabled'] = 'disabled'; + } + + $tag = new TagList(); + $tag->addChild(new TagElement([ + 'tag' => 'input', + 'type' => 'password', + 'id' => $id, + 'name' => $this->name, + 'value' => "", + 'attributes' => $this->attributes + ['size' => $this->size], + ])); + + if ($this->with_confirm == true) { + $tag->addChild(new TagElement([ + 'tag' => 'label', + 'attributes' => ['for' => $id.'-confirm'], + 'text' => $this->getText($this->confirm_string), + ])); + $tag->addChild(new TagElement([ + 'tag' => 'input', + 'type' => 'password', + 'id' => $id.'-confirm', + 'name' => $this->name.'_confirm', + 'value' => "", + 'attributes' => $this->attributes + ['size' => $this->size], + ])); + } + if ($this->with_strength_check) { + $tag->addChild(new TagElement([ + 'tag' => 'span', + 'id' => $id.'_result', + 'attributes' => ['class' => 'password_strength_checker'], + ])); + } + return $tag; + } + + /** + * {@inheritdoc} + * + * @return boolean check if element is valid + */ + public function isValid() : bool + { + if ($this->with_confirm == true) { + if (!isset($_REQUEST["{$this->name}_confirm"]) || $_REQUEST["{$this->name}_confirm"] != $this->getValues()) { + $this->addError($this->getText("The passwords do not match"), __FUNCTION__); + + if ($this->stop_on_first_error) { + return false; + } + } + } + return parent::isValid(); + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue() : bool + { + return true; + } +} diff --git a/src/classes/fields/Plupload.php b/src/classes/fields/Plupload.php new file mode 100644 index 00000000..20e2ea65 --- /dev/null +++ b/src/classes/fields/Plupload.php @@ -0,0 +1,219 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; + +/** + * The pupload field class + */ +class Plupload extends Field +{ + + /** + * filters + * + * @var array + */ + protected $filters = []; + + /** + * upload.php url + * + * @var string + */ + protected $url = ''; // url upload.php + + /** + * Moxie.swf url + * + * @var string + */ + protected $swf_url = ''; // url Moxie.swf + + /** + * Moxie.xap url + * + * @var string + */ + protected $xap_url = ''; // url Moxie.xap + + /** + * {@inheritdoc} + * + * @param mixed $value value to set + */ + public function processValue($value) + { + $this->value = json_decode($value); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + $form_id = $form->getId(); + + $this->addJs( + " + var {$id}_files_remaining = 0; + $('#{$id}_uploader').pluploadQueue({ + runtimes : 'html5,flash,silverlight,html4', + chunk_size : '1mb', + unique_names : true, + + resize : {width : 320, height : 240, quality : 90}, + + url : '{$this->url}', + flash_swf_url : '{$this->swf_url}', + silverlight_xap_url : '{$this->xap_url}', + filters : ".json_encode($this->filters).", + + preinit : { + Init: function(up, info) { + }, + + UploadFile: function(up, file) { + } + }, + + init : { + FileUploaded: function(up, file, info) { + response = JSON.parse( info.response ); + + if(file.status == plupload.DONE && response.result == null){ + var value = \$.trim( \$('#{$id}_uploaded_json').val() ); + if(value != '') {value = JSON.parse( value );} + else value = []; + if(value == null) value = []; + var obj = {temppath: response.temppath, name: file.name}; + value.push( obj ); + + \$('#{$id}_uploaded_json').val( JSON.stringify(value) ); + } + }, + + FilesRemoved: function(up, files) { + plupload.each(files, function(file) { + {$id}_files_remaining--; + }); + if({$id}_files_remaining == 0){ + \$('#{$form_id} input[type=submit]').removeAttr('disabled'); + } + }, + + FilesAdded: function(up, files) { + \$('#{$form_id} input[type=submit]').attr('disabled','disabled'); + plupload.each(files, function(file) { + {$id}_files_remaining++; + }); + }, + + UploadComplete: function(up, file, info) { + \$('#{$form_id} input[type=submit]').removeAttr('disabled'); + {$id}_files_remaining = 0; + }, + + Error: function(up, args) { + log('[Error] ', args); + } + } + }); + + + function log() { + var str = ''; + + plupload.each(arguments, function(arg) { + var row = ''; + + if (typeof(arg) != 'string') { + plupload.each(arg, function(value, key) { + if (arg instanceof plupload.File) { + switch (value) { + case plupload.QUEUED: + value = 'QUEUED'; + break; + + case plupload.UPLOADING: + value = 'UPLOADING'; + break; + + case plupload.FAILED: + value = 'FAILED'; + break; + + case plupload.DONE: + value = 'DONE'; + break; + } + } + + if (typeof(value) != 'function') { + row += (row ? ', ' : '') + key + '=' + value; + } + }); + + str += row + ' '; + } else { + str += arg + ' '; + } + }); + + var \$log = \$('#{$id}_log'); + \$('
    '+str+'
    ').appendTo(\$log) + }" + ); + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + return "

    Your browser doesn't have Flash, Silverlight or HTML5 support.

    +
    + name}\" value=\"". + json_encode($this->getValues()). + "\" />"; + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue() : bool + { + return true; + } +} diff --git a/src/classes/fields/Progressbar.php b/src/classes/fields/Progressbar.php new file mode 100644 index 00000000..710ffaaa --- /dev/null +++ b/src/classes/fields/Progressbar.php @@ -0,0 +1,106 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\Basics\Html\TagElement; + +/** + * The progressbar field class + */ +class Progressbar extends Markup +{ + + /** + * "indeterminate progressbar" flag + * + * @var boolean + */ + protected $indeterminate = false; + + /** + * "show label" flag + * + * @var boolean + */ + protected $show_label = false; + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + if ($this->indeterminate == true || !is_numeric($this->getValues())) { + $this->addJs("\$('#{$id}','#{$form->getId()}').progressbar({ value: false });"); + } elseif ($this->show_label == true) { + $this->addJs( + " + \$('#{$id}','#{$form->getId()}').progressbar({ value: parseInt(" . $this->getValues() . ") }); + \$('#{$id} .progress-label','#{$form->getId()}').text('" . $this->getValues() . "%'); + " + ); + } else { + $this->addJs("\$('#{$id}','#{$form->getId()}').progressbar({ value: parseInt(" . $this->getValues() . ") });"); + } + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + if ($this->show_label == true) { + $this->addCss("#{$form->getId()} #{$id}.ui-progressbar {position: relative;}"); + $this->addCss("#{$form->getId()} #{$id} .progress-label {position: absolute;left: 50%;top: 4px;}"); + } + + $tag = new TagElement([ + 'tag' => 'div', + 'type' => null, + 'id' => $id, + 'text' => null, + 'attributes' => $this->attributes, + 'has_close' => true, + ]); + + if ($this->show_label == true) { + $tag->addChild(new TagElement([ + 'tag' => 'div', + 'type' => null, + 'id' => null, + 'text' => null, + 'attributes' => ['class' => 'progress-label'], + 'has_close' => true, + ])); + } + return $tag; + } +} diff --git a/src/classes/fields/Radios.php b/src/classes/fields/Radios.php new file mode 100644 index 00000000..f0bd19ab --- /dev/null +++ b/src/classes/fields/Radios.php @@ -0,0 +1,74 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Abstracts\Fields\FieldMultivalues; + +/** + * The radios group field class + */ +class Radios extends FieldMultivalues +{ + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + $tag = new TagElement([ + 'tag' => 'div', + 'attributes' => ['class' => 'options'], + ]); + + if ($this->disabled == true) { + $this->attributes['disabled'] = 'disabled'; + } + + foreach ($this->options as $key => $value) { + if (is_array($value) && isset($value['attributes'])) { + $attributes = $value['attributes']; + } else { + $attributes = []; + } + + if (is_array($value)) { + $value = $value['value']; + } + + $tag_label = new TagElement([ + 'tag' => 'label', + 'attributes' => ['for' => "{$id}-{$key}", 'class' => "label-radio"], + ]); + $tag_label->addChild(new TagElement([ + 'tag' => 'input', + 'type' => 'radio', + 'id' => "{$id}-{$key}", + 'name' => $this->name, + 'value' => $key, + 'attributes' => array_merge($attributes, ($this->getValues() == $key) ? ['checked' => 'checked'] : []), + 'text' => $value, + ])); + $tag->addChild($tag_label); + } + return $tag; + } +} diff --git a/src/classes/fields/Range.php b/src/classes/fields/Range.php new file mode 100644 index 00000000..14dd0071 --- /dev/null +++ b/src/classes/fields/Range.php @@ -0,0 +1,67 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\Basics\Html\TagElement; + +/** + * The range input field class + */ +class Range extends Number +{ + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + if ($this->hasErrors()) { + $this->attributes['class'] .= ' has-errors'; + } + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + + $this->attributes['size'] = $this->size; + if (is_numeric($this->min) && is_numeric($this->max) && $this->max >= $this->min) { + $this->attributes += [ + 'size' => $this->size, + 'min' => $this->min, + 'max' => $this->max, + 'step' => $this->step + ]; + } + + return new TagElement([ + 'tag' => 'input', + 'type' => 'range', + 'id' => $id, + 'name' => $this->name, + 'value' => $this->getValues(), + 'attributes' => $this->attributes, + ]); + } +} diff --git a/src/classes/fields/Recaptcha.php b/src/classes/fields/Recaptcha.php new file mode 100644 index 00000000..36d34bd1 --- /dev/null +++ b/src/classes/fields/Recaptcha.php @@ -0,0 +1,123 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Fields\Captcha; + +/** + * The recaptcha field class + */ +class Recaptcha extends Captcha +{ + + /** + * public key + * + * @var string + */ + protected $publickey = ''; + + /** + * private key + * + * @var string + */ + protected $privatekey = ''; + + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + if (!function_exists('recaptcha_get_html')) { + return ''; + } + return recaptcha_get_html($this->publickey); + } + + /** + * {@inheritdoc} + * + * @return boolean TRUE if element is valid + */ + public function isValid() : bool + { + if ($this->already_validated == true) { + return true; + } + if (isset($this->value['already_validated']) && $this->value['already_validated'] == true) { + return true; + } + if (!function_exists('recaptcha_check_answer')) { + $this->already_validated = true; + return true; + } + + if (!is_array($this->value)) { + $this->value = []; + } + + // if something is missing... + $this->value += [ + 'challenge_field' => '', + 'response_field' => '', + ]; + + $resp = recaptcha_check_answer( + $this->privatekey, + $_SERVER["REMOTE_ADDR"], + $this->value["challenge_field"], + $this->value["response_field"] + ); + if (!$resp->is_valid) { + $this->addError($this->getText("Recaptcha response is not valid"), __FUNCTION__); + } else { + $this->already_validated = true; + $this->value['already_validated'] = true; + } + + return $resp->is_valid; + } + + /** + * {@inheritdoc} + * + * @param array $request request array + */ + public function alterRequest(array &$request) + { + foreach ($request as $key => $val) { + //RECAPTCHA HANDLE + if (preg_match('/^recaptcha\_(challenge|response)\_field$/', $key, $matches)) { + $fieldname = $this->getName(); + if (!empty($request["recaptcha_challenge_field"])) { + $request[$fieldname]["challenge_field"] = $request["recaptcha_challenge_field"]; + unset($request["recaptcha_challenge_field"]); + } + if (!empty($request["recaptcha_response_field"])) { + $request[$fieldname]["response_field"] = $request["recaptcha_response_field"]; + unset($request["recaptcha_response_field"]); + } + } + } + } +} diff --git a/src/classes/fields/Reset.php b/src/classes/fields/Reset.php new file mode 100644 index 00000000..9d4558a8 --- /dev/null +++ b/src/classes/fields/Reset.php @@ -0,0 +1,68 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Abstracts\Fields\Action; + +/** + * The reset button field class + */ +class Reset extends Action +{ + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + parent::__construct($options, $name); + if (isset($options['value'])) { + $this->value = $options['value']; + } + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + if (empty($this->value)) { + $this->value = 'Reset'; + } + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + + return new TagElement([ + 'tag' => 'input', + 'type' => 'reset', + 'id' => $id, + 'name' => $this->name, + 'value' => $this->getText($this->getValues()), + 'attributes' => $this->attributes, + ]); + } +} diff --git a/src/classes/fields/Select.php b/src/classes/fields/Select.php new file mode 100644 index 00000000..2520ec79 --- /dev/null +++ b/src/classes/fields/Select.php @@ -0,0 +1,165 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Fields\FieldMultivalues; +use Degami\PHPFormsApi\Abstracts\Fields\Optionable; +use Degami\Basics\Html\TagElement; + +/** + * The select field class + */ +class Select extends FieldMultivalues +{ + + /** + * multiple attribute + * + * @var boolean + */ + protected $multiple = false; + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + if (isset($options['options'])) { + foreach ($options['options'] as $k => $o) { + if ($o instanceof Optionable) { + $o->setParent($this); + $this->addOption($o); + } elseif (is_array($o)) { + $option = new Optgroup($k, ['options' => $o]); + $option->setParent($this); + $this->addOption($option); + } else { + $option = new Option($k, $o); + $option->setParent($this); + $this->addOption($option); + } + } + unset($options['options']); + } + + if (isset($options['default_value'])) { + if (!$this->isMultiple() && !(isset($options['multiple']) && $options['multiple']==true)) { + if (is_array($options['default_value'])) { + $options['default_value'] = reset($options['default_value']); + } + $options['default_value'] = "".$options['default_value']; + } else { + if (!is_array($options['default_value'])) { + $options['default_value'] = [$options['default_value']]; + } + foreach ($options['default_value'] as $k => $v) { + $options['default_value'][$k] = "".$v; + } + } + } + + parent::__construct($options, $name); + } + + /** + * Return field multiple attribute + * + * @return boolean field is multiple + */ + public function isMultiple() + { + return $this->multiple; + } + + /** + * Set field multiple attribute + * + * @param boolean $multiple multiple attribute + * @return Select + */ + public function setMultiple($multiple = true) + { + $this->multiple = ($multiple == true); + return $this; + } + + /** + * Return field value + * + * @return mixed field value + */ + public function getValue() + { + return $this->value; + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + $output = ''; + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + if ($this->hasErrors()) { + $this->attributes['class'] .= ' has-errors'; + } + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + $attributes = $this->getAttributes(); + $field_name = ($this->multiple) ? "{$this->name}[]" : $this->name; + + $tag = new TagElement([ + 'tag' => 'select', + 'id' => $id, + 'name' => $field_name, +// 'value' => htmlspecialchars($this->value), + 'attributes' => $this->attributes + ( + ($this->multiple) ? ['multiple' => 'multiple','size' => $this->size] : [] + ), + ]); + + if (isset($this->attributes['placeholder']) && !empty($this->attributes['placeholder'])) { + $tag->addChild(new TagElement([ + 'tag' => 'option', + 'attributes' => [ + 'disabled' => 'disabled', + ] + (isset($this->default_value) ? [] : ['selected' => 'selected']), + 'text' => $this->attributes['placeholder'], + ])); + } + + foreach ($this->options as $key => $value) { + /** @var \Degami\PHPFormsApi\Fields\Option $value */ + $tag->addChild( + $value->renderHTML($this) + ); + } + + return $tag; + } +} diff --git a/src/classes/fields/Selectmenu.php b/src/classes/fields/Selectmenu.php new file mode 100644 index 00000000..5636c9a7 --- /dev/null +++ b/src/classes/fields/Selectmenu.php @@ -0,0 +1,40 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\PHPFormsApi\Form; + +/** + * The "selectmenu" select field class + */ +class Selectmenu extends Select +{ + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + $this->addJs("\$('#{$id}','#{$form->getId()}').selectmenu({width: 'auto' });"); + + parent::preRender($form); + } +} diff --git a/src/classes/fields/Slider.php b/src/classes/fields/Slider.php new file mode 100644 index 00000000..e60ecdd1 --- /dev/null +++ b/src/classes/fields/Slider.php @@ -0,0 +1,126 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; + +/** + * The "slider" select field class + */ +class Slider extends Select +{ + + /** + * show value on change + * + * @var boolean + */ + protected $with_val = false; + + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + // get the "default_value" index value + $values = call_user_func_array([__CLASS__, 'arrayGetValues'], [ $this->default_value, $this->options ]); + $oldkey_value = end($values); + + // flatten the options array ang get a numeric keyset + $options['options'] = call_user_func_array([__CLASS__, 'arrayFlatten'], [ $options['options'] ]); + + // search the new index + $this->value = $this->default_value = array_search($oldkey_value, $this->options); + + if (!isset($options['attributes']['class'])) { + $options['attributes']['class'] = ''; + } + $options['attributes']['class'].=' slider'; + + if (isset($options['multiple'])) { + $options['multiple'] = false; + } + + parent::__construct($options, $name); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + $add_js = ''; + if ($this->with_val == true) { + $add_js .= " + var text = \$( '#{$id}' )[ 0 ].options[ \$( '#{$id}' )[ 0 ].selectedIndex ].label; + \$('#{$id}-show_val','#{$form->getId()}').text( text );"; + } + $this->addJs( + " + \$('#{$id}-slider','#{$form->getId()}').slider({ + min: 1, + max: ".count($this->options).", + value: \$( '#{$id}' )[ 0 ].selectedIndex + 1, + slide: function( event, ui ) { + \$( '#{$id}' )[ 0 ].selectedIndex = ui.value - 1; + ".$add_js." + } + }); + \$( '#{$id}' ).change(function() { + \$('#{$id}-slider').slider('value', this.selectedIndex + 1 ); + }).hide();" + ); + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + $text = isset($this->default_value) && $this->optionsHasKey($this->default_value) ? + $this->options[ $this->default_value ]->getLabel() : + ''; + if (trim($text) == '' && count($this->options) > 0) { + /** @var Option $option */ + $option = reset($this->options); + $text = $option->getLabel(); + } + if (!preg_match("/
    <\/div>/i", $this->suffix)) { + $this->suffix = "
    " . + (($this->with_val == true) ? "
    {$text}
    " : '') . + $this->suffix; + } + return parent::renderField($form); + } +} diff --git a/src/classes/fields/Spinner.php b/src/classes/fields/Spinner.php new file mode 100644 index 00000000..8ffbc408 --- /dev/null +++ b/src/classes/fields/Spinner.php @@ -0,0 +1,45 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\PHPFormsApi\Form; + +/** + * The spinner number input field class + */ +class Spinner extends Number +{ + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + + $js_options = ''; + if (is_numeric($this->min) && is_numeric($this->max) && $this->max >= $this->min) { + $js_options = "{min: $this->min, max: $this->max, step: $this->step}"; + } + + $this->addJs("\$('#{$id}','#{$form->getId()}').attr('type','text').spinner({$js_options});"); + + parent::preRender($form); + } +} diff --git a/src/classes/fields/Submit.php b/src/classes/fields/Submit.php new file mode 100644 index 00000000..3f2a40eb --- /dev/null +++ b/src/classes/fields/Submit.php @@ -0,0 +1,53 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Abstracts\Fields\Clickable; + +/** + * The submit input type field class + */ +class Submit extends Clickable +{ + /** + * {@inheritdoc} + * + * @param Form $form form object + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + if (empty($this->value)) { + $this->value = 'Submit'; + } + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + + $tag = new TagElement([ + 'tag' => 'input', + 'type' => 'submit', + 'id' => $id, + 'name' => $this->name, + 'value' => $this->getText($this->getValues()), + 'attributes' => $this->attributes, + ]); + return $tag; + } +} diff --git a/src/classes/fields/Switchbox.php b/src/classes/fields/Switchbox.php new file mode 100644 index 00000000..0d9a6b01 --- /dev/null +++ b/src/classes/fields/Switchbox.php @@ -0,0 +1,157 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\Basics\Html\TagElement; + +/** + * The switch selection field class + */ +class Switchbox extends Radios +{ + + /** @var mixed "no" value */ + protected $no_value; + + /** @var string "no" label */ + protected $no_label; + + /** @var mixed "yes" value */ + protected $yes_value; + + /** @var string "yes" label */ + protected $yes_label; + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + $this->no_value = 0; + $this->no_label = $this->getText('No'); + $this->yes_value = 1; + $this->yes_label = $this->getText('Yes'); + + // labels and values can be overwritten + parent::__construct($options, $name); + + // "options" is overwritten + $this->options = [ + $this->no_value => $this->no_label, + $this->yes_value => $this->yes_label, + ]; + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + + + foreach ($this->options as $key => $value) { + $this->addJs( + "\$('#{$id}-{$key}','#{$form->getId()}') + .click(function(evt){ + \$(this).closest('label').addClass('ui-state-active'); + \$('#{$id} input[type=\"radio\"]').not(this).closest('label').removeClass('ui-state-active'); + });" + ); + } + + $this->addCss( + "#{$id} .label-switch{ + text-align: center; + display: inline-block; + width: 50%; + padding-top: 10px; + padding-bottom: 10px; + box-sizing: border-box; + }" + ); + $this->addJs( + "\$('#{$id}','#{$form->getId()}').find('input[type=\"radio\"]:checked') + .closest('label').addClass('ui-state-active');" + ); + //$this->add_css("#{$id} .label-switch input{ display: none; }"); + $this->addJs("\$('#{$id} input[type=\"radio\"]','#{$form->getId()}').hide();"); + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + $tag = new TagElement([ + 'tag' => 'div', + 'id' => $id, + 'attributes' => ['class' => 'options ui-widget-content ui-corner-all'], + ]); + + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + + foreach ($this->options as $key => $value) { + $attributes = $this->attributes; + if (is_array($value) && isset($value['attributes'])) { + $attributes = $value['attributes']; + } + if (is_array($value)) { + $value = $value['value']; + } + + $tag_label = new TagElement([ + 'tag' => 'label', + 'attributes' => [ + 'id' => "{$id}-{$key}-button", + 'for' => "{$id}-{$key}", + 'class' => "label-switch ui-widget ui-state-default" + ], + ]); + $tag_label->addChild(new TagElement([ + 'tag' => 'input', + 'type' => 'radio', + 'id' => "{$id}-{$key}", + 'name' => "{$this->name}", + 'value' => $key, + 'attributes' => array_merge( + $attributes, + (($this->getValues() == $key) ? ['checked' => 'checked'] : []) + ), + 'text' => $this->getText($value), + ])); + $tag->addChild($tag_label); + } + return $tag; + } +} diff --git a/src/classes/fields/Tel.php b/src/classes/fields/Tel.php new file mode 100644 index 00000000..742f345b --- /dev/null +++ b/src/classes/fields/Tel.php @@ -0,0 +1,69 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\Basics\Html\TagElement; + +/** + * The tel input field class + */ +class Tel extends Field +{ + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + if ($this->hasErrors()) { + $this->attributes['class'] .= ' has-errors'; + } + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + if (is_array($this->value)) { + $this->value = ''; + } + + return new TagElement([ + 'tag' => 'input', + 'type' => 'tel', + 'id' => $id, + 'name' => $this->name, + 'value' => htmlspecialchars($this->getValues()), + 'attributes' => $this->attributes + ['size' => $this->size], + ]); + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue() : bool + { + return true; + } +} diff --git a/src/classes/fields/Textarea.php b/src/classes/fields/Textarea.php new file mode 100644 index 00000000..0cc07651 --- /dev/null +++ b/src/classes/fields/Textarea.php @@ -0,0 +1,111 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\Basics\Html\TagElement; + +/** + * The textarea field class + */ +class Textarea extends Field +{ + /** + * Element maxlenght + * + * @var integer + */ + protected $maxlength = null; + + /** + * Element minlength + * + * @var integer + */ + protected $minlength = null; + + /** + * rows + * + * @var integer + */ + protected $rows = 5; + + /** + * resizable flag + * + * @var boolean + */ + protected $resizable = false; + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + if ($this->resizable == true) { + $this->addJs("\$('#{$id}','#{$form->getId()}').resizable({handles:\"se\"});"); + } + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * @return string the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + $errors = $this->getErrors(); + if (!empty($errors)) { + $this->attributes['class'] .= ' has-errors'; + } + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + + return new TagElement([ + 'tag' => 'textarea', + 'id' => $id, + 'name' => $this->name, + 'text' => $this->getValues(), + 'attributes' => $this->attributes + ['cols' => $this->size, 'rows' => $this->rows], + 'has_close' => true, + ]); + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue() : bool + { + return true; + } +} diff --git a/src/classes/fields/Textfield.php b/src/classes/fields/Textfield.php new file mode 100644 index 00000000..73d8e235 --- /dev/null +++ b/src/classes/fields/Textfield.php @@ -0,0 +1,84 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\Basics\Html\TagElement; + +/** + * The text input field class + */ +class Textfield extends Field +{ + /** + * Element maxlenght + * + * @var integer + */ + protected $maxlength = null; + + /** + * Element minlength + * + * @var integer + */ + protected $minlength = null; + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + if ($this->hasErrors()) { + $this->attributes['class'] .= ' has-errors'; + } + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + if (is_array($this->value)) { + $this->value = ''; + } + + return new TagElement([ + 'tag' => 'input', + 'type' => 'text', + 'id' => $id, + 'name' => $this->name, + 'value' => htmlspecialchars((string) $this->getValues()), + 'attributes' => $this->attributes + ['size' => $this->size], + ]); + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue() : bool + { + return true; + } +} diff --git a/src/classes/fields/Time.php b/src/classes/fields/Time.php new file mode 100644 index 00000000..9f4a5402 --- /dev/null +++ b/src/classes/fields/Time.php @@ -0,0 +1,82 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\Basics\Html\TagElement; + +/** + * The time field class + */ +class Time extends Field +{ + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + $this->default_value = '00:00'; + parent::__construct($options, $name); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + if ($this->hasErrors()) { + $this->attributes['class'] .= ' has-errors'; + } + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + if (is_array($this->value)) { + $this->value = ''; + } + + return new TagElement([ + 'tag' => 'input', + 'type' => 'time', + 'id' => $id, + 'name' => $this->name, + 'value' => htmlspecialchars($this->getValues()), + 'attributes' => $this->attributes + ['size' => $this->size], + ]); + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue() : bool + { + return true; + } +} diff --git a/src/classes/fields/Timeselect.php b/src/classes/fields/Timeselect.php new file mode 100644 index 00000000..0e995cbe --- /dev/null +++ b/src/classes/fields/Timeselect.php @@ -0,0 +1,265 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\Basics\Html\TagElement; + +/** + * The time select group field class + */ +class Timeselect extends Field +{ + + /** + * granularity (seconds / minutes / hours) + * + * @var string + */ + protected $granularity = 'seconds'; + + /** + * "use js selects" flag + * + * @var boolean + */ + protected $js_selects = false; + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + $this->default_value = [ + 'hours'=>0, + 'minutes'=>0, + 'seconds'=>0, + ]; + + parent::__construct($options, $name); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + if ($this->js_selects == true) { + $id = $this->getHtmlId(); + + $this->addJs( + "\$('#{$id} select[name=\"{$this->name}[hours]\"]','#{$form->getId()}') + .selectmenu({width: 'auto' });" + ); + if ($this->granularity != 'hours') { + $this->addJs( + "\$('#{$id} select[name=\"{$this->name}[minutes]\"]','#{$form->getId()}') + .selectmenu({width: 'auto' });" + ); + + if ($this->granularity != 'minutes') { + $this->addJs( + "\$('#{$id} select[name=\"{$this->name}[seconds]\"]','#{$form->getId()}') + .selectmenu({width: 'auto' });" + ); + } + } + } + + parent::preRender($form); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + if ($this->hasErrors()) { + $this->attributes['class'] .= ' has-errors'; + } + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + $attributes = $this->getAttributes(['type','name','id','size','hours','minutes','seconds']); + + $tag = new TagElement([ + 'tag' => 'div', + 'id' => $id, + 'attributes' => $this->attributes, + ]); + + if (!(isset($this->attributes['hours']) && is_array($this->attributes['hours']))) { + $this->attributes['hours'] = []; + } + if ($this->disabled == true) { + $this->attributes['hours']['disabled']='disabled'; + } + $select_hours = new TagElement([ + 'tag' => 'select', + 'name' => $this->name.'[hours]', + 'attributes' => $this->attributes['hours'], + ]); + for ($i=0; $i<=23; $i++) { + $select_hours->addChild(new TagElement([ + 'tag' => 'option', + 'value' => $i, + 'attributes' => [] + (($i == $this->value['hours']) ? ['selected' => 'selected'] : []), + 'text' => str_pad($i, 2, "0", STR_PAD_LEFT), + ])); + } + $tag->addChild($select_hours); + if ($this->granularity != 'hours') { + if (!(isset($this->attributes['minutes']) && is_array($this->attributes['minutes']))) { + $this->attributes['minutes'] = []; + } + if ($this->disabled == true) { + $this->attributes['minutes']['disabled']='disabled'; + } + $select_minutes = new TagElement([ + 'tag' => 'select', + 'name' => $this->name.'[minutes]', + 'attributes' => $this->attributes['minutes'], + ]); + for ($i=0; $i<=59; $i++) { + $select_minutes->addChild(new TagElement([ + 'tag' => 'option', + 'value' => $i, + 'attributes' => [] + (($i == $this->value['minutes']) ? ['selected' => 'selected'] : []), + 'text' => str_pad($i, 2, "0", STR_PAD_LEFT), + ])); + } + $tag->addChild($select_minutes); + if ($this->granularity != 'minutes') { + if (!(isset($this->attributes['seconds']) && is_array($this->attributes['seconds']))) { + $this->attributes['seconds'] = []; + } + if ($this->disabled == true) { + $this->attributes['seconds']['disabled']='disabled'; + } + $select_seconds = new TagElement([ + 'tag' => 'select', + 'name' => $this->name.'[seconds]', + 'attributes' => $this->attributes['seconds'], + ]); + for ($i=0; $i<=59; $i++) { + $select_seconds->addChild(new TagElement([ + 'tag' => 'option', + 'value' => $i, + 'attributes' => [] + (($i == $this->value['seconds']) ? ['selected' => 'selected'] : []), + 'text' => str_pad($i, 2, "0", STR_PAD_LEFT), + ])); + } + $tag->addChild($select_seconds); + } + } + return $tag; + } + + /** + * {@inheritdoc} + * + * @param mixed $value value to set + */ + public function processValue($value) + { + $this->value = [ + 'hours' => $value['hours'], + ]; + if ($this->granularity!='hours') { + $this->value['minutes'] = $value['minutes']; + if ($this->granularity!='minutes') { + $this->value['seconds'] = $value['seconds']; + } + } + } + + /** + * {@inheritdoc} + * + * @return boolean TRUE if element is valid + */ + public function isValid() : bool + { + $check = true; + $check &= ($this->value['hours']>=0 && $this->value['hours']<=23); + + if ($this->granularity != 'hours') { + $check &= ($this->value['minutes']>=0 && $this->value['minutes']<=59); + + if ($this->granularity != 'minutes') { + $check &= ($this->value['seconds']>=0 && $this->value['seconds']<=59); + } + } + + if (! $check) { + $titlestr = (!empty($this->title)) ? $this->title : !empty($this->name) ? $this->name : $this->id; + $this->addError(str_replace("%t", $titlestr, $this->getText("%t: Invalid time")), __FUNCTION__); + + if ($this->stop_on_first_error) { + return false; + } + } + return parent::isValid(); + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue() : bool + { + return true; + } + + /** + * Get value as a date string + * + * @return string date value + */ + public function valueString(): string + { + $value = $this->getValues(); + $out = (($value['hours'] < 10) ? '0':'').((int) $value['hours']); + + if ($this->granularity!='hours') { + $out .= ':'.(($value['minutes'] < 10) ? '0':'').((int) $value['minutes']); + if ($this->granularity!='minutes') { + $out .= ':'.(($value['seconds'] < 10) ? '0':'').((int) $value['seconds']); + } + } + + return $out; + } +} diff --git a/src/classes/fields/Tinymce.php b/src/classes/fields/Tinymce.php new file mode 100644 index 00000000..f1e15ba7 --- /dev/null +++ b/src/classes/fields/Tinymce.php @@ -0,0 +1,100 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\PHPFormsApi\Form; +use \stdClass; + +/** + * tinymce beautified textarea + */ +class Tinymce extends Textarea +{ + /** + * tinymce options + * + * @var array + */ + protected $tinymce_options = FORMS_DEFAULT_TINYMCE_OPTIONS; + + /** + * Get tinymce options array + * + * @return array tinymce options + */ + public function &getTinymceOptions(): array + { + return $this->tinymce_options; + } + + /** + * Set tinymce options array + * + * @param array $options array of valid tinymce options + * @return self + */ + public function setTinymceOptions(array $options): Tinymce + { + $options = array_filter($options, [$this, 'isValidTinymceOption']); + $this->tinymce_options = $options; + + return $this; + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + */ + public function preRender(Form $form) + { + if ($this->pre_rendered == true) { + return; + } + $id = $this->getHtmlId(); + $this->tinymce_options['selector'] = "#{$id}"; + $tinymce_options = new stdClass; + foreach ($this->tinymce_options as $key => $value) { + if (! $this->isValidTinymceOption($key)) { + continue; + } + $tinymce_options->{$key} = $value; + } + $this->addJs("tinymce.init(".json_encode($tinymce_options).");"); + $this->addJs(" + document.querySelector('form').addEventListener('submit', function() { + const editor = tinymce.get('$id'); + const content = editor.getBody().innerHTML; + const textarea = document.querySelector('textarea#$id'); + if (textarea) { + textarea.value = content; + } + }); + "); + parent::preRender($form); + } + + /** + * filters valid tinymce options + * + * @param string $propertyname property name + * @return boolean TRUE if is a valid tinymce option + */ + private function isValidTinymceOption(string $propertyname): bool + { + // could be used to filter elements + return true; + } +} diff --git a/src/classes/fields/Url.php b/src/classes/fields/Url.php new file mode 100644 index 00000000..5276d905 --- /dev/null +++ b/src/classes/fields/Url.php @@ -0,0 +1,84 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\Basics\Html\TagElement; + +/** + * The url input field class + */ +class Url extends Field +{ + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + parent::__construct($options, $name); + + // ensure is url validator is present + $this->getValidate()->addElement('url'); + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string the element html + */ + public function renderField(Form $form) + { + $id = $this->getHtmlId(); + + if (!isset($this->attributes['class'])) { + $this->attributes['class'] = ''; + } + if ($this->hasErrors()) { + $this->attributes['class'] .= ' has-errors'; + } + if ($this->disabled == true) { + $this->attributes['disabled']='disabled'; + } + if (is_array($this->value)) { + $this->value = ''; + } + + return new TagElement([ + 'tag' => 'input', + 'type' => 'url', + 'id' => $id, + 'name' => $this->name, + 'value' => htmlspecialchars($this->getValues()), + 'attributes' => $this->attributes + ['size' => $this->size], + ]); + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue() : bool + { + return true; + } +} diff --git a/src/classes/fields/Value.php b/src/classes/fields/Value.php new file mode 100644 index 00000000..6eeb13d7 --- /dev/null +++ b/src/classes/fields/Value.php @@ -0,0 +1,75 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELDS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Fields; + +use Degami\Basics\Html\BaseElement; +use Degami\PHPFormsApi\Form; +use Degami\PHPFormsApi\Abstracts\Base\Field; + +/** + * The value field class + * this field is not rendered as part of the form, but the value is passed on form submission + */ +class Value extends Field +{ + + /** + * Class constructor + * + * @param array $options build options + * @param ?string $name field name + */ + public function __construct(array $options = [], ?string $name = null) + { + $this->container_tag = ''; + $this->container_class = ''; + parent::__construct($options, $name); + if (isset($options['value'])) { + $this->value = $options['value']; + } + } + + /** + * {@inheritdoc} + * + * @param Form $form form object + * + * @return string|BaseElement an empty string + */ + public function renderField(Form $form) + { + return ''; + } + + /** + * validate function + * + * @return boolean this field is always valid + */ + public function isValid() : bool + { + return true; + } + + /** + * {@inheritdoc} + * + * @return boolean this is a value + */ + public function isAValue() : bool + { + return true; + } +} diff --git a/src/classes/traits/Containers.php b/src/classes/traits/Containers.php new file mode 100644 index 00000000..f22e9b36 --- /dev/null +++ b/src/classes/traits/Containers.php @@ -0,0 +1,141 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### TRAITS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Traits; + +use Degami\PHPFormsApi\Abstracts\Base\Element; +use Degami\PHPFormsApi\Abstracts\Base\Field; +use Degami\PHPFormsApi\Abstracts\Base\FieldsContainer; +use Degami\PHPFormsApi\Abstracts\Fields\ComposedField; +use Degami\PHPFormsApi\Exceptions\FormException; + +/** + * containers specific functions + */ +trait Containers +{ + + /** + * keeps fields insert order + * + * @var array + */ + protected $insert_field_order = []; + + /** + * Element fields + * + * @var array + */ + protected $fields = []; + + /** + * Get the fields array by reference + * + * @return array the array of field elements + */ + public function &getFields(): array + { + return $this->fields; + } + + /** + * Get parent namespace + * + * @return string parent namespace + */ + private function parentNameSpace(): string + { + $namespaceParts = explode('\\', __NAMESPACE__); + return implode("\\", array_slice($namespaceParts, 0, -1)); + } + + /** + * Returns a field object instance + * + * @param string $name field name + * @param mixed $field field to add, can be an array or a field subclass + * @return Field instance + * @throws FormException + */ + public function getFieldObj(string $name, $field): Field + { + if (is_array($field)) { + $parentNS = $this->parentNameSpace(); + $element_type = isset($field['type']) ? + $this->snakeCaseToPascalCase($field['type']) : + 'textfield'; + + $field_type = $parentNS . "\\Fields\\" . $element_type; + $container_type = $parentNS . "\\Containers\\" . $element_type; + $root_type = $parentNS . "\\" . $element_type; + + if (!class_exists($field_type) && !class_exists($container_type) && !class_exists($root_type)) { + throw new FormException( + "Error adding field. Class \"$field_type\", \"$container_type\", \"$root_type\" not found", + 1 + ); + } + + if (class_exists($field_type)) { + $type = $field_type; + } elseif (class_exists($container_type)) { + $type = $container_type; + } else { + $type = $root_type; + } + + if (is_subclass_of($type, 'Degami\PHPFormsApi\Abstracts\Base\Field')) { + /** @var Field $type */ + $field = $type::getInstance($field, $name); + } else { + $field = new $type($field, $name); + } + } elseif ($field instanceof Field) { + $field->setName($name); + } else { + throw new FormException("Error adding field. Array or field subclass expected, ".gettype($field)." given", 1); + } + + return $field; + } + + /** + * Check if field is a field container + * + * @param Field $field field instance + * @return boolean true if field is a field container + */ + public function isFieldContainer(Field $field): bool + { + return $field instanceof FieldsContainer && !($field instanceof ComposedField); + } + + /** + * add markup helper + * + * @param string $markup markup to add + * @param array $options + * @return Element + */ + public function addMarkup(string $markup, array $options = []): Element + { + static $lastMarkupIndex = 0; + return $this->addField('_markup_'.time().'_'.$lastMarkupIndex++, [ + 'type' => 'markup', + 'container_tag' => null, + 'value' => $markup, + ] + $options); + } +} diff --git a/src/classes/traits/Processors.php b/src/classes/traits/Processors.php new file mode 100644 index 00000000..9ab44b48 --- /dev/null +++ b/src/classes/traits/Processors.php @@ -0,0 +1,489 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### TRAITS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Traits; + +/** + * Processor functions + */ +trait Processors +{ + /** + * applies trim to text + * + * @param string $text text to trim + * @return string trimmed version of $text + */ + public static function processTrim(string $text): string + { + return trim($text); + } + + /** + * applies ltrim to text + * + * @param string $text text to ltrim + * @return string ltrimmed version of $text + */ + public static function processLtrim(string $text): string + { + return ltrim($text); + } + + /** + * applies rtrim to text + * + * @param string $text text to rtrim + * @return string rtrimmed version of $text + */ + public static function processRtrim(string $text): string + { + return rtrim($text); + } + + /** + * applies xss checks on string (weak version) + * + * @param string $string text to check + * @return string safe value + */ + public static function processXssWeak(string $string): string + { + return call_user_func_array( + [__CLASS__, 'processXss'], + [ + $string, + implode( + "|", + [ + 'a', + 'abbr', + 'acronym', + 'address', + 'b', + 'bdo', + 'big', + 'blockquote', + 'br', + 'caption', + 'cite', + 'code', + 'col', + 'colgroup', + 'dd', + 'del', + 'dfn', + 'div', + 'dl', + 'dt', + 'em', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'hr', + 'i', + 'img', + 'ins', + 'kbd', + 'li', + 'ol', + 'p', + 'pre', + 'q', + 'samp', + 'small', + 'span', + 'strong', + 'sub', + 'sup', + 'table', + 'tbody', + 'td', + 'tfoot', + 'th', + 'thead', + 'tr', + 'tt', + 'ul', + 'var', + ] + ) + ] + ); + } + + /** + * Check if $text's character encoding is utf8 + * + * @param string $text text to check + * @return boolean is utf8 + */ + private static function _validateUtf8(string $text): bool + { + if (strlen($text) == 0) { + return true; + } + return (preg_match('/^./us', $text) == 1); + } + + /** + * applies xss checks on string + * + * @param string $string text to check + * @param string $allowed_tags allowed tags + * @return string safe value + */ + public static function processXss(string $string, $allowed_tags = FORMS_XSS_ALLOWED_TAGS): string + { + // Only operate on valid UTF-8 strings. This is necessary to prevent cross + // site scripting issues on Internet Explorer 6. + if (!call_user_func_array([__CLASS__, '_validateUtf8'], [ $string ])) { + return ''; + } + // Store the input format + call_user_func_array([__CLASS__, '_filterXssSplit'], [ $allowed_tags, true ]); + // Remove NUL characters (ignored by some browsers) + $string = str_replace(chr(0), '', $string); + // Remove Netscape 4 JS entities + $string = preg_replace('%&\s*\{[^}]*(\}\s*;?|$)%', '', $string); + + // Defuse all HTML entities + $string = str_replace('&', '&', $string); + // Change back only well-formed entities in our whitelist + // Decimal numeric entities + $string = preg_replace('/&#([0-9]+;)/', '&#\1', $string); + // Hexadecimal numeric entities + $string = preg_replace('/&#[Xx]0*((?:[0-9A-Fa-f]{2})+;)/', '&#x\1', $string); + // Named entities + $string = preg_replace('/&([A-Za-z][A-Za-z0-9]*;)/', '&\1', $string); + + return preg_replace_callback( + '% + ( + <(?=[^a-zA-Z!/]) # a lone < + | # or + <[^>]*(>|$) # a string that starts with a <, up until the > or the end of the string + | # or + > # just a > + )%x', + [__CLASS__, '_filterXssSplit'], + $string + ); + } + + /** + * _filter_xss_split private method + * + * @param string $m string to split + * @param boolean $store store elements into static $allowed html + * @return string string + */ + private static function _filterXssSplit(string $m, $store = false): string + { + static $allowed_html; + + if ($store) { + $m = explode("|", $m); + $allowed_html = array_flip($m); + return ''; + } + + $string = $m[1]; + + if (substr($string, 0, 1) != '<') { + // We matched a lone ">" character + return '>'; + } elseif (strlen($string) == 1) { + // We matched a lone "<" character + return '<'; + } + + if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9]+)([^>]*)>?$%', $string, $matches)) { + // Seriously malformed + return ''; + } + + $slash = trim($matches[1]); + $elem = &$matches[2]; + $attrlist = &$matches[3]; + + if (!isset($allowed_html[strtolower($elem)])) { + // Disallowed HTML element + return ''; + } + + if ($slash != '') { + return ""; + } + + // Is there a closing XHTML slash at the end of the attributes? + // In PHP 5.1.0+ we could count the changes, currently we need a separate match + $xhtml_slash = preg_match('%\s?/\s*$%', $attrlist) ? ' /' : ''; + $attrlist = preg_replace('%(\s?)/\s*$%', '\1', $attrlist); + + // Clean up attributes + $attr2 = implode( + ' ', + call_user_func_array( + [__CLASS__, + '_filterXssAttributes' + ], + [$attrlist] + ) + ); + $attr2 = preg_replace('/[<>]/', '', $attr2); + $attr2 = strlen($attr2) ? ' ' . $attr2 : ''; + + return "<$elem$attr2$xhtml_slash>"; + } + + /** + * _filter_xss_attributes private method + * + * @param string $attr attributes string + * @return array filtered attributes array + */ + private static function _filterXssAttributes(string $attr): array + { + $attrarr = []; + $mode = 0; + $attrname = ''; + $skip = false; + + while (strlen($attr) != 0) { + // Was the last operation successful? + $working = 0; + + switch ($mode) { + case 0: + // Attribute name, href for instance. + if (preg_match('/^([-a-zA-Z]+)/', $attr, $match)) { + $attrname = strtolower($match[1]); + $skip = ($attrname == 'style' || substr($attrname, 0, 2) == 'on'); + $working = $mode = 1; + $attr = preg_replace('/^[-a-zA-Z]+/', '', $attr); + } + break; + + case 1: + // Equals sign or valueless ("selected"). + if (preg_match('/^\s*=\s*/', $attr)) { + $working = 1; + $mode = 2; + $attr = preg_replace('/^\s*=\s*/', '', $attr); + break; + } + + if (preg_match('/^\s+/', $attr)) { + $working = 1; + $mode = 0; + if (!$skip) { + $attrarr[] = $attrname; + } + $attr = preg_replace('/^\s+/', '', $attr); + } + break; + + case 2: + // Attribute value, a URL after href= for instance. + if (preg_match('/^"([^"]*)"(\s+|$)/', $attr, $match)) { + $thisval = call_user_func_array( + [__CLASS__, + '_filterXssBadProtocol' + ], + [ $match[1] ] + ); + + if (!$skip) { + $attrarr[] = "$attrname=\"$thisval\""; + } + $working = 1; + $mode = 0; + $attr = preg_replace('/^"[^"]*"(\s+|$)/', '', $attr); + break; + } + + if (preg_match("/^'([^']*)'(\s+|$)/", $attr, $match)) { + $thisval = call_user_func_array( + [__CLASS__, + '_filterXssBadProtocol' + ], + [ $match[1] ] + ); + + if (!$skip) { + $attrarr[] = "$attrname='$thisval'"; + } + $working = 1; + $mode = 0; + $attr = preg_replace("/^'[^']*'(\s+|$)/", '', $attr); + break; + } + + if (preg_match("%^([^\s\"']+)(\s+|$)%", $attr, $match)) { + $thisval = call_user_func_array( + [__CLASS__,'_filterXssBadProtocol'], + [ $match[1] ] + ); + + if (!$skip) { + $attrarr[] = "$attrname=\"$thisval\""; + } + $working = 1; + $mode = 0; + $attr = preg_replace("%^[^\s\"']+(\s+|$)%", '', $attr); + } + break; + } + + if ($working == 0) { + // Not well formed; remove and try again. + + /* + * + * '/ + * ^ + * ( + * "[^"]*("|$) # - a string that starts with a double quote, + * # up until the next double quote or the end of the string + * | # or + * \'[^\']*(\'|$)| # - a string that starts with a quote, + * # up until the next quote or the end of the string + * | # or + * \S # - a non-whitespace character + * )* # any number of the above three + * \s* # any number of whitespaces + * /x', + * */ + + $attr = preg_replace( + '/^("[^"]*("|$)|\'[^\']*(\'|$)||\S)*\s*/x', + '', + $attr + ); + $mode = 0; + } + } + + // The attribute list ends with a valueless attribute like "selected". + if ($mode == 1 && !$skip) { + $attrarr[] = $attrname; + } + return $attrarr; + } + + /** + * [_filter_xss_bad_protocol private method + * + * @param string $string string + * @param boolean $decode process entity decode on string + * @return string safe value + */ + private static function _filterXssBadProtocol(string $string, $decode = true): string + { + if ($decode) { + $string = call_user_func_array([__CLASS__, 'processEntityDecode'], [ $string ]); + } + + return call_user_func_array( + [__CLASS__, 'processPlain'], + [ + call_user_func_array([__CLASS__, '_stripDangerousProtocols'], [ $string ]) + ] + ); + } + + /** + * _strip_dangerous_protocols private method + * + * @param string $uri uri + * @return string safe value + */ + private static function _stripDangerousProtocols(string $uri): string + { + static $allowed_protocols; + + if (!isset($allowed_protocols)) { + $allowed_protocols = array_flip( + [ + 'ftp', + 'http', + 'https', + 'irc', + 'mailto', + 'news', + 'nntp', + 'rtsp', + 'sftp', + 'ssh', + 'tel', + 'telnet', + 'webcal' + ] + ); + } + + // Iteratively remove any invalid protocol found. + do { + $before = $uri; + $colonpos = strpos($uri, ':'); + if ($colonpos > 0) { + // We found a colon, possibly a protocol. Verify. + $protocol = substr($uri, 0, $colonpos); + // If a colon is preceded by a slash, question mark or hash, it cannot + // possibly be part of the URL scheme. This must be a relative URL, which + // inherits the (safe) protocol of the base document. + if (preg_match('![/?#]!', $protocol)) { + break; + } + // Check if this is a disallowed protocol. Per RFC2616, section 3.2.3 + // (URI Comparison) scheme comparison must be case-insensitive. + if (!isset($allowed_protocols[strtolower($protocol)])) { + $uri = substr($uri, $colonpos + 1); + } + } + } while ($before != $uri); + + return $uri; + } + + /** + * applies entity_decode to text + * + * @param string $text text to decode + * @return string decoded version of $text + */ + public static function processEntityDecode(string $text): string + { + return html_entity_decode($text, ENT_QUOTES, 'UTF-8'); + } + + /** + * applies addslashes to text + * + * @param string $text text to addslash + * @return string addslashed version of $text + */ + public static function processAddslashes(string $text): string + { + if (!get_magic_quotes_gpc() && !preg_match("/\\/i", $text)) { + return addslashes($text); + } else { + return $text; + } + } +} diff --git a/src/classes/traits/Tools.php b/src/classes/traits/Tools.php new file mode 100644 index 00000000..1898d7b5 --- /dev/null +++ b/src/classes/traits/Tools.php @@ -0,0 +1,193 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### TRAITS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Traits; + +use Degami\PHPFormsApi\Abstracts\Base\Element; + +/** + * tools functions + */ +trait Tools +{ + /** + * scan_array private method + * + * @param string $string string to search + * @param array $array array to check + * @return mixed|bool found element / FALSE on failure + */ + private static function scanArray(string $string, array $array) + { + list($key, $rest) = preg_split('/[[\]]/', $string, 2, PREG_SPLIT_NO_EMPTY); + if ($key && $rest) { + return call_user_func_array([__CLASS__, 'scanArray'], [$rest, $array[$key]]); + } elseif ($key && isset($array[$key])) { + return $array[$key]; + } + return false; + } + + /** + * applies array_flatten to array + * + * @param array $array array to flatten + * @return array monodimensional array + */ + public static function arrayFlatten(array $array): array + { + $return = []; + foreach ($array as $key => $value) { + if (is_array($value)) { + $return = array_merge( + $return, + call_user_func_array([__CLASS__,'arrayFlatten'], [$value]) + ); + } else { + $return[$key] = $value; + } + } + return $return; + } + + /** + * Get array values by key + * + * @param ?string $search_key key to search + * @param array $array where to search + * @return array the filtered array + */ + public static function arrayGetValues(?string $search_key, array $array): array + { + $return = []; + foreach ($array as $key => $value) { + if (is_array($value)) { + $return = array_merge( + $return, + call_user_func_array([__CLASS__,'arrayGetValues'], [ $search_key, $value ]) + ); + } elseif ($key == $search_key) { + $return[] = $value; + } + } + return $return; + } + + /** + * order elements by weight properties + * + * @param Element $a first element + * @param Element $b second element + * @return int position + */ + public static function orderByWeight(Element $a, Element $b): int + { + if ($a->getWeight() == $b->getWeight()) { + return 0; + } + return ($a->getWeight() < $b->getWeight()) ? -1 : 1; + } + + /** + * order validation functions + * + * @param array $a first element + * @param array $b second element + * @return int position + */ + public static function orderValidators(array $a, array $b): int + { + if (is_array($a) && isset($a['validator'])) { + $a = $a['validator']; + } + if (is_array($b) && isset($b['validator'])) { + $b = $b['validator']; + } + + if ($a == $b) { + return 0; + } + if ($a == 'required') { + return -1; + } + if ($b == 'required') { + return 1; + } + + return 0; + // return $a > $b ? 1 : -1; + } + + /** + * translate strings, using a function named "__()" if is defined. + * The function should take a string written in english as parameter and return the translated version + * + * @param string $string string to translate + * @return string the translated version + */ + public static function translateString(string $string): string + { + if (is_string($string) && function_exists('__')) { + return __($string); + } + return $string; + } + + /** + * Returns the translated version of the input text ( when available ) depending on current element configuration + * + * @param string $text input text + * @return string text to return (translated or not) + */ + protected function getText(string $text): string + { + if ($this->no_translation == true) { + return $text; + } + return call_user_func_array([__CLASS__, 'translateString'], [$text]); + } + + /** + * Get a string representing the called class + * + * @return string + */ + public static function getClassNameString(): string + { + $basename = basename(strtolower(str_replace("\\", "/", get_called_class()))); + $parentname = basename(dirname(strtolower(str_replace("\\", "/", get_called_class())))); + + return $basename.'_'.preg_replace("/s$/", "", $parentname); + } + + /** + * Check if a function name in the "user" space match the regexp + * and if found executes it passing the arguments + * + * @param string $regexp regular Expression to find function + * @param mixed $args arguments array + */ + public static function executeAlter(string $regexp, $args) + { + $defined_functions = get_defined_functions(); + if (!is_array($args)) { + $args = [ $args ]; + } + foreach ($defined_functions['user'] as $function_name) { + if (preg_match($regexp, $function_name)) { + call_user_func_array($function_name, $args); + } + } + } +} diff --git a/src/classes/traits/Validators.php b/src/classes/traits/Validators.php new file mode 100644 index 00000000..6ff4d328 --- /dev/null +++ b/src/classes/traits/Validators.php @@ -0,0 +1,360 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### TRAITS #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Traits; + +/** + * validations functions + */ +trait Validators +{ + + /** + * "required" validation function + * + * @param mixed $value the element value + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateRequired($value = null) + { + if (!empty($value) || (!is_array($value) && trim("".$value) != '')) { + return true; + } else { + return "%t is required"; + } + } + + /** + * "notZero" required validation function - useful for radios + * + * @param mixed $value the element value + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateNotzero($value = null) + { + if ((!empty($value) && (!is_array($value) && trim("".$value) != '0'))) { + return true; + } else { + return "%t is required"; + } + } + + /** + * "max_length" validation function + * + * @param mixed $value the element value + * @param mixed $options max length + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateMaxLength($value, $options) + { + if (strlen("".$value) > $options) { + return "Maximum length of %t is {$options}"; + } + return true; + } + + /** + * "min_length" validation function + * + * @param mixed $value the element value + * @param mixed $options min length + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateMinLength($value, $options) + { + if (strlen("".$value) < $options) { + return "%t must be longer than {$options}"; + } + return true; + } + + /** + * "exact_length" validation function + * + * @param mixed $value the element value + * @param mixed $options length + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateExactLength($value, $options) + { + if (strlen("".$value) != $options) { + return "%t must be {$options} characters long."; + } + return true; + } + + /** + * "regexp" validation function + * + * @param mixed $value the element value + * @param mixed $options regexp string + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateRegexp($value, $options) + { + if (!preg_match($options, "".$value)) { + return "%t must match the regular expression \"$options\"."; + } + return true; + } + + /** + * "alpha" validation function + * + * @param mixed $value the element value + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateAlpha($value) + { + if (!preg_match("/^([a-z])+$/i", "".$value)) { + return "%t must contain alphabetic characters."; + } + return true; + } + + /** + * "alpha_numeric" validation function + * + * @param mixed $value the element value + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateAlphaNumeric($value) + { + if (!preg_match("/^([a-z0-9])+$/i", "".$value)) { + return "%t must only contain alpha numeric characters."; + } + return true; + } + + /** + * "alpha_dash" validation function + * + * @param mixed $value the element value + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateAlphaDash($value) + { + if (!preg_match("/^([-a-z0-9_-])+$/i", "".$value)) { + return "%t must contain only alpha numeric characters, underscore, or dashes"; + } + return true; + } + + /** + * "numeric" validation function + * + * @param mixed $value the element value + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateNumeric($value) + { + if (!is_numeric($value)) { + return "%t must be numeric."; + } + return true; + } + + /** + * "integer" validation function + * + * @param mixed $value the element value + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateInteger($value) + { + if (!preg_match('/^[\-+]?[0-9]+$/', "".$value)) { + return "%t must be an integer."; + } + return true; + } + + /** + * "match" validation function + * + * @param mixed $value the element value + * @param mixed $options elements to find into _REQUEST array + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateMatch($value, $options) + { + $other = call_user_func_array([__CLASS__, 'scan_array'], [$options, $_REQUEST]); + if ($value != $other) { + return "The field %t is invalid."; + } + return true; + } + + /** + * "file_extension" validation function + * + * @param mixed $value the element value + * @param mixed $options file extension + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateFileExtension($value, $options) + { + if (!isset($value['filepath'])) { + return "%t - Error. value has no filepath attribute"; + } + $options = explode(',', $options); + $ext = substr(strrchr($value['filepath'], '.'), 1); + if (!in_array($ext, $options)) { + return "File upload %t is not of required type"; + } + return true; + } + + /** + * "file_not_exists" validation function + * + * @param mixed $value the element value + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateFileNotExists($value) + { + if (!isset($value['filepath'])) { + return "%t - Error. value has no filepath attribute"; + } + if (file_exists($value['filepath'])) { + return "The file %t has already been uploaded"; + } + return true; + } + + /** + * "max_file_size" validation function + * + * @param mixed $value the element value + * @param mixed $options max file size + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateMaxFileSize($value, $options) + { + if (!isset($value['filesize'])) { + return "%t - Error. value has no filesize attribute"; + } + if ($value['filesize'] > $options) { + $max_size = call_user_func_array([__CLASS__, 'format_bytes'], [$options]); + return "The file %t is too big. Maximum filesize is {$max_size}."; + } + return true; + } + + /** + * "is_date" validation function + * + * @param mixed $value the element value + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateIsDate($value) + { + if (!$value || ($value && ($date = date_create($value)) === false)) { + return "%t is not a valid date."; + } + return true; + } + + /** + * "email" validation function + * + * @param mixed $value the element value + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateEmail($value) + { + if (empty($value)) { + return false; + } + $check_dns = FORMS_VALIDATE_EMAIL_DNS; + $blocked_domains = explode('|', FORMS_VALIDATE_EMAIL_BLOCKED_DOMAINS); + $atIndex = strrpos($value, "@"); + if (is_bool($atIndex) && !$atIndex) { + return "%t is not a valid email. It must contain the @ symbol."; + } else { + $domain = substr($value, $atIndex+1); + $local = substr($value, 0, $atIndex); + $localLen = strlen($local); + $domainLen = strlen($domain); + if ($localLen < 1 || $localLen > 64) { + return "%t is not a valid email. Local part is wrong length."; + } elseif ($domainLen < 1 || $domainLen > 255) { + return "%t is not a valid email. Domain name is wrong length."; + } elseif ($local[0] == '.' || $local[$localLen-1] == '.') { + return "%t is not a valid email. Local part starts or ends with '.'"; + } elseif (preg_match('/\\.\\./', $local)) { + return "%t is not a valid email. Local part two consecutive dots."; + } elseif (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain)) { + return "%t is not a valid email. Invalid character in domain."; + } elseif (preg_match('/\\.\\./', $domain)) { + return "%t is not a valid email. Domain name has two consecutive dots."; + } elseif (!preg_match('/^(\\\\.|[A-Za-z0-9!#%&`_=\\/$\'*+?^{}|~.-])+$/', str_replace("\\\\", "", $local))) { + if (!preg_match('/^"(\\\\"|[^"])+"$/', str_replace("\\\\", "", $local))) { + return "%t is not a valid email. Invalid character in local part."; + } + } + if (in_array($domain, $blocked_domains)) { + return "%t is not a valid email. Domain name is in list of disallowed domains."; + } + if ($check_dns && !(checkdnsrr($domain, "MX") || checkdnsrr($domain, "A"))) { + return "%t is not a valid email. Domain name not found in DNS."; + } + } + return true; + } + + /** + * "is_RGB" validation function + * + * @param mixed $value the element value + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateRgb($value) + { + if (!$value || ($value && !preg_match("/^#?([a-f\d]{3}([a-f\d]{3})?)$/i", $value))) { + return "%t is not a valid RGB color string."; + } + return true; + } + + /** + * "is_URL" validation function + * + * @param mixed $value the element value + * @return mixed TRUE if valid or a string containing the error message + */ + public static function validateUrl($value) + { + $URL_FORMAT = + '/^(https?):\/\/'. // protocol + '(([a-z0-9$_\.\+!\*\'\(\),;\?&=-]|%[0-9a-f]{2})+'. // username + '(:([a-z0-9$_\.\+!\*\'\(\),;\?&=-]|%[0-9a-f]{2})+)?'. // password + '@)?(?#'. // auth requires @ + ')((([a-z0-9]\.|[a-z0-9][a-z0-9-]*[a-z0-9]\.)*'. // domain segments AND + '[a-z][a-z0-9-]*[a-z0-9]'. // top level domain OR + '|((\d|[1-9]\d|1\d{2}|2[0-4][0-9]|25[0-5])\.){3}'. + '(\d|[1-9]\d|1\d{2}|2[0-4][0-9]|25[0-5])'. // IP address + ')(:\d+)?'. // port + ')(((\/+([a-z0-9$_\.\+!\*\'\(\),;:@&=-]|%[0-9a-f]{2})*)*'. // path + '(\?([a-z0-9$_\.\+!\*\'\(\),;:@&=-]|%[0-9a-f]{2})*)'. // query string + '?)?)?'. // path and query string optional + '(#([a-z0-9$_\.\+!\*\'\(\),;:@&=-]|%[0-9a-f]{2})*)?'. // fragment + '$/i'; + + if (!$value || ($value && !preg_match($URL_FORMAT, $value))) { + return "%t is not a valid URL string."; + } + return true; + } +} diff --git a/src/fonts/Lato-Regular.ttf b/src/fonts/Lato-Regular.ttf new file mode 100644 index 00000000..adbfc467 Binary files /dev/null and b/src/fonts/Lato-Regular.ttf differ diff --git a/src/form.php b/src/form.php deleted file mode 100644 index 2c514672..00000000 --- a/src/form.php +++ /dev/null @@ -1,752 +0,0 @@ -'); -define('FORMS_DEFAULT_SUFFIX', '
    '); -define('FORMS_DEFAULT_FIELD_PREFIX', '
    '); -define('FORMS_DEFAULT_FIELD_SUFFIX', '
    '); -define('FORMS_VALIDATE_EMAIL_DNS', TRUE); -define('FORMS_VALIDATE_EMAIL_BLOCKED_DOMAINS', 'mailinator.com|guerrillamail.com'); -define('FORMS_BASE_PATH', ''); -define('FORMS_XSS_ALLOWED_TAGS', 'a|em|strong|cite|code|ul|ol|li|dl|dt|dd'); - -// Here are some prioity things I'm working on: -// TODO: Support edit forms by allowing an array of values to be specified, not just taken from _REQUEST - - -class cs_form { - - protected $form_id = 'cs_form'; - protected $form_token = ''; - protected $action = ''; - protected $attributes = array(); - protected $method = 'post'; - protected $prefix = FORMS_DEFAULT_PREFIX; - protected $suffix = FORMS_DEFAULT_SUFFIX; - protected $validate = array(); - protected $processed = FALSE; - protected $preprocessors = FALSE; - protected $validated = FALSE; - protected $submitted = FALSE; - protected $valid = TRUE; - protected $submit = ''; - protected $error = ''; - - protected $fields = array(); - - public function __construct($options = array()) { - foreach ($options as $name => $value) { - $this->$name = $value; - } - if (empty($this->submit)) { - $this->submit = "{$this->form_id}_submit"; - } - $sid = session_id(); - if (!empty($sid)) { - $this->form_token = sha1(mt_rand(0, 1000000)); - $_SESSION['form_token'][$this->form_token] = $_SERVER['REQUEST_TIME']; - } - } - - // Warning: some messy logic in calling process->submit->values - public function values() { - if (!$this->processed) { - $this->process(); - } - $output = array(); - foreach ($this->fields as $name => $field) { - $output[$name] = $field->values(); - } - return $output; - } - - public function reset() { - foreach ($this->fields as $name => $field) { - $field->reset(); - unset($_POST[$name]); - } - unset($_REQUEST['form_id']); - $this->processed = FALSE; - $this->validated = FALSE; - $this->submitted = FALSE; - } - - public function is_submitted() { - return $this->submitted; - } - - public function process() { - if (!$this->processed) { - $request = ($this->method == 'post') ? $_POST : $_GET; - if (isset($request['form_id']) && $request['form_id'] == $this->form_id) { - foreach ($request as $name => $value) { - if (isset($this->fields[$name])) { - $this->fields[$name]->process($value, $name); - } - } - } - $this->processed = TRUE; - } - if (!$this->preprocessors) { - foreach ($this->fields as $name => $field) { - $field->preprocess(); - } - } - if ((!$this->submitted) && $this->valid()) { - $this->submitted = TRUE; - $submit_function = $this->submit; - if (function_exists($submit_function)) { - foreach ($this->fields as $name => $field) { - $field->postprocess(); - } - $submit_function($this, ($this->method == 'post') ? $_POST : $_GET); - } - } - } - - public function valid() { - if ($this->validated) { - return $this->valid; - } - if (!isset($_REQUEST['form_id'])) { - $this->valid = FALSE; - } else if ($_REQUEST['form_id'] == $this->form_id) { - $sid = session_id(); - if (!empty($sid)) { - $this->valid = FALSE; - $this->error = 'Form is invalid or has expired'; - if (isset($_REQUEST['form_token']) && isset($_SESSION['form_token'][$_REQUEST['form_token']])) { - if ($_SESSION['form_token'][$_REQUEST['form_token']] >= $_SERVER['REQUEST_TIME'] - 7200) { - $this->valid = TRUE; - $this->error = ''; - unset($_SESSION['form_token'][$_REQUEST['form_token']]); - } - } - } - foreach ($this->fields as $field) { - if (!$field->valid()) { - $this->valid = FALSE; - } - } - } - $this->validated = TRUE; - return $this->valid; - } - - public function add_field($name, $field) { - if (!is_object($field)) { - $field_type = isset($field['type']) ? "cs_{$field['type']}" : 'cs_textfield'; - $field = new $field_type($field); - } - $this->fields[$name] = $field; - } - - public function show_errors() { - return empty($this->error) ? '' : "
  • {$this->error}
  • "; - } - - public function render() { - $output = $this->prefix; - if (!$this->valid()) { - $output .= "
      "; - $output .= $this->show_errors(); - foreach ($this->fields as $field) { - $output .= $field->show_errors(); - } - $output .= "
    "; - } - $attributes = ''; - foreach ($this->attributes as $key => $value) { - $attributes .= " {$key}=\"{$value}\""; - } - $output .= "
    action}\" method=\"{$this->method}\"{$attributes}>\n"; - foreach ($this->fields as $name => $field) { - $output .= $field->render($name); - } - $output .= "form_id}\" />\n"; - $output .= "form_token}\" />\n"; - $output .= "
    \n"; - return $output . $this->suffix; - } - - public static function validate_required($value = NULL) { - if (!empty($value)) { - return TRUE; - } else { - return "%t is required"; - } - } - - public static function validate_max_length($value, $options) { - if (strlen($value) > $options) { - return "Maximum length of %t is {$options}"; - } - return TRUE; - } - public static function validate_min_length($value, $options) { - if (strlen($value) < $options) { - return "%t must be longer than {$options}"; - } - return TRUE; - } - public static function validate_exact_length($value, $options) { - if (strlen($value) != $options) { - return "%t must be {$options} characters long."; - } - return TRUE; - } - public static function validate_alpha($value) { - if (!preg_match( "/^([a-z])+$/i", $value)) { - return "%t must contain alphabetic characters."; - } - return TRUE; - } - - protected function validate_alpha_numeric($value) { - if (!preg_match("/^([a-z0-9])+$/i", $value)) { - return "%t must only contain alpha numeric characters."; - } - return TRUE; - } - - protected function validate_alpha_dash($value) { - if (!preg_match("/^([-a-z0-9_-])+$/i", $value)) { - return "%t must contain only alpha numeric characters, underscore, or dashes"; - } - return TRUE; - } - - protected function validate_numeric($value) { - if (!is_numeric($value)) { - return "%t must be numeric."; - } - return TRUE; - } - - protected function validate_integer($value) { - if (!preg_match( '/^[\-+]?[0-9]+$/', $value)) { - return "%t must be an integer."; - } - return TRUE; - } - - public static function validate_match($value, $options) { - $other = cs_form::scan_array($options, $_REQUEST); - if ($value != $other) { - return "The field %t is invalid."; - } - return TRUE; - } - - public static function validate_file_extension($value, $options) { - $options = explode(',', $options); - $ext = substr(strrchr($value['filepath'], '.'), 1); - if (!in_array($ext, $options)) { - return "File upload %t is not of required type"; - } - return TRUE; - } - public static function validate_file_not_exists($value) { - if (file_exists($value['filepath'])) { - return "The file %t has already been uploaded"; - } - return TRUE; - } - public static function validate_max_file_size($value, $options) { - if ($value['filesize'] > $options) { - $max_size = cs_form::format_bytes($options); - return "The file %t is too big. Maximum filesize is {$max_size}."; - } - return TRUE; - } - - private static function format_bytes($size) { - $units = array(' B', ' KB', ' MB', ' GB', ' TB'); - for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024; - return round($size, 2).$units[$i]; - } - - public static function validate_email($email) { - if (empty($email)) return TRUE; - $check_dns = FORMS_VALIDATE_EMAIL_DNS; - $blocked_domains = explode('|', FORMS_VALIDATE_EMAIL_BLOCKED_DOMAINS); - $atIndex = strrpos($email, "@"); - if (is_bool($atIndex) && !$atIndex) { - return "%t is not a valid email. It must contain the @ symbol."; - } else { - $domain = substr($email, $atIndex+1); - $local = substr($email, 0, $atIndex); - $localLen = strlen($local); - $domainLen = strlen($domain); - if ($localLen < 1 || $localLen > 64) { - return "%t is not a valid email. Local part is wrong length."; - } else if ($domainLen < 1 || $domainLen > 255) { - return "%t is not a valid email. Domain name is wrong length."; - } else if ($local[0] == '.' || $local[$localLen-1] == '.') { - return "%t is not a valid email. Local part starts or ends with '.'"; - } else if (preg_match('/\\.\\./', $local)) { - return "%t is not a valid email. Local part two consecutive dots."; - } else if (!preg_match('/^[A-Za-z0-9\\-\\.]+$/', $domain)) { - return "%t is not a valid email. Invalid character in domain."; - } else if (preg_match('/\\.\\./', $domain)) { - return "%t is not a valid email. Domain name has two consecutive dots."; - } else if (!preg_match('/^(\\\\.|[A-Za-z0-9!#%&`_=\\/$\'*+?^{}|~.-])+$/', str_replace("\\\\","",$local))) { - if (!preg_match('/^"(\\\\"|[^"])+"$/', str_replace("\\\\","",$local))) { - return "%t is not a valid email. Invalid character in local part."; - } - } - if (in_array($domain, $blocked_domains)) { - return "%t is not a valid email. Domain name is in list of disallowed domains."; - } - if ($check_dns && !(checkdnsrr($domain,"MX") || checkdnsrr($domain,"A"))) { - return "%t is not a valid email. Domain name not found in DNS."; - } - } - return TRUE; - } - - public static function process_trim($text) { - return trim($text); - } - public static function process_ltrim($text) { - return ltrim($text); - } - public static function process_rtrim($text) { - return rtrim($text); - } - - private static function _validate_utf8($text) { - if (strlen($text) == 0) { - return TRUE; - } - return (preg_match('/^./us', $text) == 1); - } - - public static function process_xss_weak($string) { - return filter_xss($string, array('a|abbr|acronym|address|b|bdo|big|blockquote|br|caption|cite|code|col|colgroup|dd|del|dfn|div|dl|dt|em|h1|h2|h3|h4|h5|h6|hr|i|img|ins|kbd|li|ol|p|pre|q|samp|small|span|strong|sub|sup|table|tbody|td|tfoot|th|thead|tr|tt|ul|var')); - } - - public static function process_xss($string, $allowed_tags = FORMS_XSS_ALLOWED_TAGS) { - // Only operate on valid UTF-8 strings. This is necessary to prevent cross - // site scripting issues on Internet Explorer 6. - if (!cs_form::_validate_utf8($string)) { - return ''; - } - // Store the input format - cs_form::_filter_xss_split($allowed_tags, TRUE); - // Remove NUL characters (ignored by some browsers) - $string = str_replace(chr(0), '', $string); - // Remove Netscape 4 JS entities - $string = preg_replace('%&\s*\{[^}]*(\}\s*;?|$)%', '', $string); - - // Defuse all HTML entities - $string = str_replace('&', '&', $string); - // Change back only well-formed entities in our whitelist - // Decimal numeric entities - $string = preg_replace('/&#([0-9]+;)/', '&#\1', $string); - // Hexadecimal numeric entities - $string = preg_replace('/&#[Xx]0*((?:[0-9A-Fa-f]{2})+;)/', '&#x\1', $string); - // Named entities - $string = preg_replace('/&([A-Za-z][A-Za-z0-9]*;)/', '&\1', $string); - - return preg_replace_callback('% - ( - <(?=[^a-zA-Z!/]) # a lone < - | # or - <[^>]*(>|$) # a string that starts with a <, up until the > or the end of the string - | # or - > # just a > - )%x', 'cs_form::_filter_xss_split', $string); - } - - private static function _filter_xss_split($m, $store = FALSE) { - static $allowed_html; - - if ($store) { - $m = explode("|", $m); - $allowed_html = array_flip($m); - return; - } - - $string = $m[1]; - - if (substr($string, 0, 1) != '<') { - // We matched a lone ">" character - return '>'; - } - else if (strlen($string) == 1) { - // We matched a lone "<" character - return '<'; - } - - if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9]+)([^>]*)>?$%', $string, $matches)) { - // Seriously malformed - return ''; - } - - $slash = trim($matches[1]); - $elem = &$matches[2]; - $attrlist = &$matches[3]; - - if (!isset($allowed_html[strtolower($elem)])) { - // Disallowed HTML element - return ''; - } - - if ($slash != '') { - return ""; - } - - // Is there a closing XHTML slash at the end of the attributes? - // In PHP 5.1.0+ we could count the changes, currently we need a separate match - $xhtml_slash = preg_match('%\s?/\s*$%', $attrlist) ? ' /' : ''; - $attrlist = preg_replace('%(\s?)/\s*$%', '\1', $attrlist); - - // Clean up attributes - $attr2 = implode(' ', _filter_xss_attributes($attrlist)); - $attr2 = preg_replace('/[<>]/', '', $attr2); - $attr2 = strlen($attr2) ? ' ' . $attr2 : ''; - - return "<$elem$attr2$xhtml_slash>"; - } - - public static function process_plain($text) { - // if using PHP < 5.2.5 add extra check of strings for valid UTF-8 - return htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); - } - - public static function attributes($attributes) { - if (is_array($attributes)) { - $t = ''; - foreach ($attributes as $key => $value) { - $t .= " $key=" . '"' . cs_form::process_plain($value) . '"'; - } - return $t; - } - } - - private static function scan_array($string, $array) { - list($key, $rest) = preg_split('/[[\]]/', $string, 2, PREG_SPLIT_NO_EMPTY); - if ( $key && $rest ) { - return @cs_form::scan_array($rest, $array[$key]); - } elseif ( $key ) { - return $array[$key]; - } else { - return FALSE; - } - } - -} - -class cs_field { - - protected $title = ''; - protected $description = ''; - protected $attributes = array(); - protected $autocomplete_path = FALSE; - protected $ajax = FALSE; - protected $default_value; - protected $disabled = FALSE; - protected $validate = array(); - protected $preprocess = array(); - protected $postprocess = array(); - protected $prefix = FORMS_DEFAULT_FIELD_PREFIX; - protected $suffix = FORMS_DEFAULT_FIELD_SUFFIX; - protected $size = 60; - protected $weight = 0; - protected $value = ''; - protected $error = ''; - - public function __construct($options = array()) { - foreach ($options as $name => $value) { - $this->$name = $value; - } - $this->value = $this->default_value; - } - - public function values() { - return $this->value; - } - - public function reset() { - $this->value = $this->default_value; - } - - public function get_weight() { - return $this->weight; - } - - public function process($value) { - $this->value = $value; - } - - public function preprocess($process_type = "preprocess") { - foreach ($this->$process_type as $processor) { - $processor = "process_{$processor}"; - if (function_exists($processor)) { - $this->value = $processor($this->value); - } else { - $this->value = cs_form::$processor($this->value); - } - } - } - public function postprocess() { - $this->preprocess("postprocess"); - } - - public function valid() { - foreach ($this->validate as $validator) { - $matches = array(); - preg_match('/^([A-Za-z0-9_]+)(\[(.+)\])?$/', $validator, $matches); - $validator = "validate_{$matches[1]}"; - $options = isset($matches[3]) ? $matches[3] : NULL; - if (function_exists($validator)) { - $error = $validator($this->value, $options); - } else { - $error = cs_form::$validator($this->value, $options); - } - if ($error !== TRUE) { - $this->error = str_replace('%t', $this->title, $error); - return FALSE; - } - } - return TRUE; - } - - public function show_errors() { - return empty($this->error) ? '' : "
  • {$this->error}
  • "; - } - -} - -class cs_submit extends cs_field { - - public function render($name) { - if (empty($this->value)) { - $this->value = 'Submit'; - } - $output = $this->prefix; - $output .= "value}\" />\n"; - return $output . $this->suffix; - } -} - -class cs_textfield extends cs_field { - - public function render($name) { - $output = $this->prefix; - $this->attributes['class'] = 'textfield'; - if (!empty($this->error)) { - $this->attributes['class'] .= ' error'; - } - $required = (in_array('required', $this->validate)) ? ' *' : ''; - $output .= "\n"; - $output .= "value}\" size=\"{$this->size}\" class=\"{$this->attributes['class']}\" />\n"; - if (!empty($this->description)) { - $output .= "
    {$this->description}
    "; - } - return $output . $this->suffix; - } - -} - -class cs_textarea extends cs_field { - - protected $rows = 5; - - public function render($name) { - $output = $this->prefix; - $output .= "\n"; - $output .= ""; - if (!empty($this->description)) { - $output .= "
    {$this->description}
    "; - } - return $output . $this->suffix; - } -} - - -class cs_password extends cs_field { - public function render($name) { - $output = $this->prefix; - $output .= "\n"; - $output .= "size}\" />\n"; - return $output . $this->suffix; - } -} - -class cs_select extends cs_field { - - protected $multiple = FALSE; - - public function render($name) { - $output = $this->prefix; - $output .= "\n"; - $extra = ($this->multiple) ? ' multiple' : ''; - $field_name = ($this->multiple) ? "{$name}[]" : $name; - $output .= "\n"; - return $output . $this->suffix; - } -} - -class cs_radios extends cs_field { - public function render($name) { - $output = $this->prefix; - $output .= "\n"; - foreach ($this->options as $key => $value) { - $checked = ($this->value == $key) ? ' checked=\"checked\"' : ''; - $output .= "\n"; - } - return $output . $this->suffix; - } -} - -class cs_checkboxes extends cs_field { - public function render($name) { - $output = $this->prefix; - if (!empty($this->title)) { - $output .= "\n"; - } - foreach ($this->options as $key => $value) { - $checked = (is_array($this->default_value) && in_array($key, $this->default_value)) ? ' checked=\"checked\"' : ''; - $output .= "\n"; - } - return $output . $this->suffix; - } -} - -class cs_file extends cs_field { - protected $uploaded = FALSE; - - public function __construct($options = array()) { - parent::__construct($options); - if (!isset($options['size'])) { - $this->size = 30; - } - } - - public function render($name) { - $output = $this->prefix; - if (!empty($this->title)) { - $output .= "\n"; - } - $output .= ""; - $output .= "size}\" />"; - return $output . $this->suffix; - } - - public function process($value, $name) { - $this->value = array( - 'filepath' => $this->destination .'/'. basename($_FILES[$name]['name']), - 'filename' => basename($_FILES[$name]['name']), - 'filesize' => $_FILES[$name]['size'], - 'mimetype' => $_FILES[$name]['type'], - ); - if ($this->valid()) { - move_uploaded_file($_FILES[$name]['tmp_name'], $this->value['filepath']); - $this->uploaded = TRUE; - } - } - - public function valid() { - if ($this->uploaded) { - return TRUE; - } - return parent::valid(); - } -} - - -class cs_fieldset extends cs_field { - - protected $collapsible = FALSE; - protected $collapsed = FALSE; - - protected $fields = array(); - - public function add_field($name, $field) { - if (!is_object($field)) { - $field_type = isset($field['type']) ? "cs_{$field['type']}" : 'cs_textfield'; - $field = new $field_type($field); - } - $this->fields[$name] = $field; - } - - public function values() { - $output = array(); - foreach ($fields as $name => $field) { - $output[$name] = $field->values(); - } - return $output; - } - - public function render($parent_name) { - $output = $this->prefix; - $this->attributes['class'] = 'fieldset'; - if ($this->collapsible) { - $this->attributes['class'] .= ' collapsible'; - if ($this->collapsed) { - $this->attributes['class'] .= ' collapsed'; - } else { - $this->attributes['class'] .= ' expanded'; - } - } - $output .= "
    attributes['class']}\">\n{$this->title}\n
    \n"; - foreach ($this->fields as $name => $field) { - $output .= $field->render("{$parent_name}[{$name}]"); - } - return $output ."
    \n". $this->suffix; - } - - public function preprocess() { - foreach ($this->fields as $field) { - $field->preprocess(); - } - } - public function process($values) { - foreach ($values as $name => $value) { - $this->fields[$name]->process($value); - } - } - - public function valid() { - $valid = TRUE; - foreach ($this->fields as $field) { - if (!$field->valid()) { - $valid = FALSE; - } - } - return $valid; - } - public function show_errors() { - $output = ""; - foreach ($this->fields as $field) { - $output .= $field->show_errors(); - } - return $output; - } - - public function reset() { - foreach ($this->fields as $field) { - $field->reset(); - } - } -} - - diff --git a/src/interfaces/FieldInterface.php b/src/interfaces/FieldInterface.php new file mode 100644 index 00000000..4a42e348 --- /dev/null +++ b/src/interfaces/FieldInterface.php @@ -0,0 +1,78 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELD INTERFACE #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Interfaces; + +use Degami\Basics\Html\BaseElement; +use Degami\Basics\Html\TagElement; +use Degami\PHPFormsApi\Form; + +/** + * field interface + */ +interface FieldInterface +{ + + /** + * this function tells to the form if this element is a value that needs to be + * included into parent values() function call result + * + * @return boolean include_me + */ + public function isAValue() : bool; // tells if component value is passed on the parent values() function call + + /** + * Pre-render hook + * + * @param Form $form form object + */ + public function preRender(Form $form); + + /** + * The function that actually renders the html field + * + * @param Form $form form object + * + * @return string|BaseElement the field html + */ + public function renderField(Form $form); // renders html + + /** + * Process / set field value + * + * @param mixed $value value to set + */ + public function processValue($value); + + /** + * Check element validity + * + * @return boolean TRUE if element is valid + */ + public function isValid() : bool; + + /** + * Return form elements values into this element + * + * @return mixed form values + */ + public function getValues(); + + /** + * which element should return the add_field() function + * + * @return string one of 'parent' or 'this' + */ + public function onAddReturn() : string; +} diff --git a/src/interfaces/FieldsContainerInterface.php b/src/interfaces/FieldsContainerInterface.php new file mode 100644 index 00000000..a60aaf35 --- /dev/null +++ b/src/interfaces/FieldsContainerInterface.php @@ -0,0 +1,50 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ +/* ######################################################### + #### FIELD INTERFACE #### + ######################################################### */ + +namespace Degami\PHPFormsApi\Interfaces; + +use Degami\PHPFormsApi\Abstracts\Base\Element; +use Degami\PHPFormsApi\Abstracts\Base\FieldsContainer; +use Degami\PHPFormsApi\Form; + +/** + * fields container interface + */ +interface FieldsContainerInterface extends FieldInterface +{ + + /** + * Add field to form + * + * @param string $name field name + * @param mixed $field field to add, can be an array or a field subclass + * @return FieldsContainer + */ + public function addField(string $name, $field) : Element; + + /** + * remove field from form + * + * @param string $name field name + * @return FieldsContainer + */ + public function removeField(string $name) : FieldsContainer; + + /** + * on_add_return overload + * + * @return string 'this' + */ + public function onAddReturn(): string; +} diff --git a/src/php_forms_api_bootstrap.php b/src/php_forms_api_bootstrap.php new file mode 100644 index 00000000..aab41bd6 --- /dev/null +++ b/src/php_forms_api_bootstrap.php @@ -0,0 +1,15 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ + +if ((function_exists('session_status') && session_status() != PHP_SESSION_NONE) || trim(session_id()) != '') { + @ini_set('session.gc_maxlifetime', FORMS_SESSION_TIMEOUT); + @session_set_cookie_params(FORMS_SESSION_TIMEOUT); +} diff --git a/src/php_forms_api_defines.php b/src/php_forms_api_defines.php new file mode 100644 index 00000000..27d4fe37 --- /dev/null +++ b/src/php_forms_api_defines.php @@ -0,0 +1,96 @@ + + * @license MIT https://opensource.org/licenses/mit-license.php + * @link https://github.com/degami/php-forms-api + */ + +/* + * Turn on error reporting during development + */ +// error_reporting(E_ALL); +// ini_set('display_errors', TRUE); +// ini_set('display_startup_errors', TRUE); + +namespace Degami\PHPFormsApi; + +/* + * PHP Forms API library configuration + */ + +if (!defined('FORMS_DEFAULT_FORM_CONTAINER_TAG')) { + define('FORMS_DEFAULT_FORM_CONTAINER_TAG', 'div'); +} +if (!defined('FORMS_DEFAULT_FORM_CONTAINER_CLASS')) { + define('FORMS_DEFAULT_FORM_CONTAINER_CLASS', 'form-container'); +} +if (!defined('FORMS_DEFAULT_FIELD_CONTAINER_TAG')) { + define('FORMS_DEFAULT_FIELD_CONTAINER_TAG', 'div'); +} +if (!defined('FORMS_DEFAULT_FIELD_CONTAINER_CLASS')) { + define('FORMS_DEFAULT_FIELD_CONTAINER_CLASS', 'form-item'); +} +if (!defined('FORMS_DEFAULT_FIELD_LABEL_CLASS')) { + define('FORMS_DEFAULT_FIELD_LABEL_CLASS', ''); +} +if (!defined('FORMS_FIELD_ADDITIONAL_CLASS')) { + define('FORMS_FIELD_ADDITIONAL_CLASS', ''); +} +if (!defined('FORMS_VALIDATE_EMAIL_DNS')) { + define('FORMS_VALIDATE_EMAIL_DNS', true); +} +if (!defined('FORMS_VALIDATE_EMAIL_BLOCKED_DOMAINS')) { + define('FORMS_VALIDATE_EMAIL_BLOCKED_DOMAINS', 'mailinator.com|guerrillamail.com'); +} +if (!defined('FORMS_BASE_PATH')) { + define('FORMS_BASE_PATH', ''); +} +if (!defined('FORMS_XSS_ALLOWED_TAGS')) { + define('FORMS_XSS_ALLOWED_TAGS', 'a|em|strong|cite|code|ul|ol|li|dl|dt|dd'); +} +if (!defined('FORMS_SESSION_TIMEOUT')) { + define('FORMS_SESSION_TIMEOUT', 7200); +} +if (!defined('FORMS_ERRORS_ICON')) { + define( + 'FORMS_ERRORS_ICON', + '' + ); +} +if (!defined('FORMS_ERRORS_TEMPLATE')) { + define( + 'FORMS_ERRORS_TEMPLATE', + '
    ' . FORMS_ERRORS_ICON . '
      %s
    ' + ); +} +if (!defined('FORMS_HIGHLIGHTS_ICON')) { + define( + 'FORMS_HIGHLIGHTS_ICON', + '' + ); +} +if (!defined('FORMS_HIGHLIGHTS_TEMPLATE')) { + define( + 'FORMS_HIGHLIGHTS_TEMPLATE', + '
    ' . FORMS_HIGHLIGHTS_ICON . '
      %s
    ' + ); +} +if (!defined('FORMS_DEFAULT_TINYMCE_OPTIONS')) { + define( + 'FORMS_DEFAULT_TINYMCE_OPTIONS', + [ + 'menubar' => false, + 'plugins' => "code,link,lists,advlist,preview,searchreplace,media,table", + 'toolbar_mode' => "wrap", + 'toolbar' => "undo redo | styles | bold italic | alignleft aligncenter alignright alignjustify", + 'relative_urls' => false, + 'remove_script_host' => true, + 'document_base_url' => "", + 'content_style' => "body { font-family:Helvetica,Arial,sans-serif; font-size:16px }", + ] + ); +} \ No newline at end of file