Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merged repeat_element branch

git-svn-id: http://svn.php.net/repository/pear/packages/HTML_QuickForm2/trunk@325158 c90b9560-bf6c-de11-be94-00142212c4b1
  • Loading branch information...
commit b255061f2693935c5b998b9361b97520055cd40d 2 parents ac2756e + 073ef34
@sad-spirit sad-spirit authored
Showing with 1,422 additions and 522 deletions.
  1. +5 −4 HTML/QuickForm2/Container.php
  2. +5 −3 HTML/QuickForm2/Container/Group.php
  3. +694 −0 HTML/QuickForm2/Container/Repeat.php
  4. +16 −13 HTML/QuickForm2/Element/Hierselect.php
  5. +1 −1  HTML/QuickForm2/Element/Input.php
  6. +4 −0 HTML/QuickForm2/Element/InputCheckbox.php
  7. +53 −13 HTML/QuickForm2/JavascriptBuilder.php
  8. +5 −3 HTML/QuickForm2/Renderer/Default.php
  9. +4 −0 data/quickform.css
  10. +17 −4 js/build.xml
  11. +299 −0 js/src/repeat.js
  12. +2 −0  tests/QuickForm2/Container/AllTests.php
  13. +0 −5 tests/QuickForm2/Container/FieldsetTest.php
  14. +0 −10 tests/QuickForm2/Container/GroupTest.php
  15. +206 −0 tests/QuickForm2/Container/RepeatTest.php
  16. +0 −16 tests/QuickForm2/ContainerOverloadTest.php
  17. +0 −20 tests/QuickForm2/ContainerTest.php
  18. +0 −9 tests/QuickForm2/Controller/Action/BackTest.php
  19. +0 −9 tests/QuickForm2/Controller/Action/DirectTest.php
  20. +0 −9 tests/QuickForm2/Controller/Action/DisplayTest.php
  21. +0 −9 tests/QuickForm2/Controller/Action/JumpTest.php
  22. +0 −9 tests/QuickForm2/Controller/Action/NextTest.php
  23. +0 −9 tests/QuickForm2/Controller/Action/SubmitTest.php
  24. +0 −6 tests/QuickForm2/Controller/PageTest.php
  25. +0 −9 tests/QuickForm2/ControllerTest.php
  26. +0 −5 tests/QuickForm2/DataSource/ArrayTest.php
  27. +0 −5 tests/QuickForm2/DataSource/SuperGlobalTest.php
  28. +2 −0  tests/QuickForm2/Element/AllTests.php
  29. +0 −10 tests/QuickForm2/Element/ButtonTest.php
  30. +0 −3  tests/QuickForm2/Element/DateTest.php
  31. +73 −0 tests/QuickForm2/Element/HierselectTest.php
  32. +0 −5 tests/QuickForm2/Element/InputButtonTest.php
  33. +0 −15 tests/QuickForm2/Element/InputCheckableTest.php
  34. +24 −10 tests/QuickForm2/Element/InputCheckboxTest.php
  35. +0 −13 tests/QuickForm2/Element/InputFileTest.php
  36. +0 −5 tests/QuickForm2/Element/InputHiddenTest.php
  37. +0 −10 tests/QuickForm2/Element/InputImageTest.php
  38. +0 −5 tests/QuickForm2/Element/InputPasswordTest.php
  39. +0 −5 tests/QuickForm2/Element/InputResetTest.php
  40. +0 −10 tests/QuickForm2/Element/InputSubmitTest.php
  41. +8 −5 tests/QuickForm2/Element/InputTest.php
  42. +0 −5 tests/QuickForm2/Element/SelectTest.php
  43. +0 −10 tests/QuickForm2/Element/StaticTest.php
  44. +0 −5 tests/QuickForm2/Element/TextareaTest.php
  45. +0 −10 tests/QuickForm2/ElementTest.php
  46. +2 −10 tests/QuickForm2/FactoryTest.php
  47. +0 −4 tests/QuickForm2/FilterTest.php
  48. +0 −17 tests/QuickForm2/JavascriptBuilderTest.php
  49. +0 −10 tests/QuickForm2/NodeTest.php
  50. +0 −10 tests/QuickForm2/Renderer/ArrayTest.php
  51. +0 −10 tests/QuickForm2/Renderer/CallbackTest.php
  52. +0 −10 tests/QuickForm2/Renderer/DefaultTest.php
  53. +0 −4 tests/QuickForm2/Renderer/StubTest.php
  54. +0 −10 tests/QuickForm2/RendererTest.php
  55. +0 −10 tests/QuickForm2/Rule/CallbackTest.php
  56. +0 −10 tests/QuickForm2/Rule/CompareTest.php
  57. +0 −6 tests/QuickForm2/Rule/EachTest.php
  58. +0 −7 tests/QuickForm2/Rule/EmailTest.php
  59. +0 −10 tests/QuickForm2/Rule/EmptyTest.php
  60. +0 −10 tests/QuickForm2/Rule/LengthTest.php
  61. +0 −10 tests/QuickForm2/Rule/MaxFileSizeTest.php
  62. +0 −10 tests/QuickForm2/Rule/MimeTypeTest.php
  63. +0 −8 tests/QuickForm2/Rule/NonemptyTest.php
  64. +0 −11 tests/QuickForm2/Rule/NotCallbackTest.php
  65. +0 −10 tests/QuickForm2/Rule/NotRegexTest.php
  66. +0 −10 tests/QuickForm2/Rule/RegexTest.php
  67. +0 −10 tests/QuickForm2/Rule/RequiredTest.php
  68. +0 −12 tests/QuickForm2/RuleTest.php
  69. +0 −6 tests/QuickForm2Test.php
  70. +2 −0  tests/TestHelper.php
View
9 HTML/QuickForm2/Container.php
@@ -290,11 +290,10 @@ public function addElement(
/**
* Removes the element from this container
*
- * If the reference object is not given, the element will be appended.
- *
* @param HTML_QuickForm2_Node $element Element to remove
*
* @return HTML_QuickForm2_Node Removed object
+ * @throws HTML_QuickForm2_NotFoundException
*/
public function removeChild(HTML_QuickForm2_Node $element)
{
@@ -510,9 +509,11 @@ public function render(HTML_QuickForm2_Renderer $renderer)
public function __toString()
{
- require_once 'HTML/QuickForm2/Renderer.php';
+ HTML_QuickForm2_Loader::loadClass('HTML_QuickForm2_Renderer');
- return $this->render(HTML_QuickForm2_Renderer::factory('default'))->__toString();
+ $renderer = $this->render(HTML_QuickForm2_Renderer::factory('default'));
+ return $renderer->__toString()
+ . $renderer->getJavascriptBuilder()->getSetupCode(null, true);
}
/**
View
8 HTML/QuickForm2/Container/Group.php
@@ -349,12 +349,14 @@ public function render(HTML_QuickForm2_Renderer $renderer)
public function __toString()
{
- require_once 'HTML/QuickForm2/Renderer.php';
+ HTML_QuickForm2_Loader::loadClass('HTML_QuickForm2_Renderer');
- return $this->render(
+ $renderer = $this->render(
HTML_QuickForm2_Renderer::factory('default')
->setTemplateForId($this->getId(), '{content}')
- )->__toString();
+ );
+ return $renderer->__toString()
+ . $renderer->getJavascriptBuilder()->getSetupCode(null, true);
}
}
?>
View
694 HTML/QuickForm2/Container/Repeat.php
@@ -0,0 +1,694 @@
+<?php
+/**
+ * Handles a Container that can be repeated multiple times in the form
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2006-2012, Alexey Borzov <avb@php.net>,
+ * Bertrand Mansion <golgote@mamasam.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category HTML
+ * @package HTML_QuickForm2
+ * @author Alexey Borzov <avb@php.net>
+ * @author Bertrand Mansion <golgote@mamasam.com>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version SVN: $Id$
+ * @link http://pear.php.net/package/HTML_QuickForm2
+ */
+
+/** Base class for all HTML_QuickForm2 containers */
+require_once 'HTML/QuickForm2/Container.php';
+/** Javascript aggregator and builder class */
+require_once 'HTML/QuickForm2/JavascriptBuilder.php';
+
+/**
+ * Javascript builder used when rendering a repeat prototype
+ *
+ * Instead of returning form setup code and client-side rules as normal
+ * Javascript code, it returns them as Javascript string literals. These are
+ * expected to be eval()'d when adding a new repeat item.
+ *
+ * This class is not intended for normal use.
+ *
+ * @category HTML
+ * @package HTML_QuickForm2
+ * @author Alexey Borzov <avb@php.net>
+ * @author Bertrand Mansion <golgote@mamasam.com>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version Release: @package_version@
+ * @link http://pear.php.net/package/HTML_QuickForm2
+ */
+class HTML_QuickForm2_Container_Repeat_JavascriptBuilder
+ extends HTML_QuickForm2_JavascriptBuilder
+{
+ /**
+ * Fake "current form" ID
+ * @var string
+ */
+ protected $formId = 'repeat';
+
+ /**
+ * Empty list of javascript libraries, base one(s) are in original builder
+ * @var array
+ */
+ protected $libraries = array();
+
+
+ /**
+ * Returns rules and element setup code as Javascript string literals
+ *
+ * @return array array('rules', 'setup code')
+ */
+ public function getFormJavascriptAsStrings()
+ {
+ return array(
+ self::encode(
+ empty($this->rules['repeat'])
+ ? '' : "[\n" . implode(",\n", $this->rules['repeat']) . "\n]"
+ ),
+ self::encode(
+ empty($this->scripts['repeat'])
+ ? '' : implode("\n", $this->scripts['repeat'])
+ )
+ );
+ }
+
+ /**
+ * Passes Javascript libraries added by repeat prototype
+ *
+ * @param HTML_QuickForm2_JavascriptBuilder $recipient original Javascript builder
+ */
+ public function passLibraries(HTML_QuickForm2_JavascriptBuilder $recipient)
+ {
+ foreach ($this->libraries as $name => $library) {
+ $recipient->addLibrary(
+ $name, $library['file'], $library['webPath'], $library['absPath']
+ );
+ }
+ }
+}
+
+
+
+/**
+ * Handles a Container that can be repeated multiple times in the form
+ *
+ * This element accepts a Container (a Fieldset, a Group, but not another
+ * Repeat) serving as a "prototype" and repeats it several times. Repeated
+ * items can be dynamically added / removed via Javascript, with the benefit
+ * that server-side part automatically knows about these changes and that
+ * server-side and client-side validation can be easily leveraged.
+ *
+ * Example:
+ * <code>
+ * $group = new HTML_QuickForm2_Container_Group()
+ * $repeat = $form->addRepeat('related');
+ * ->setPrototype($group);
+ * // repeat indexes will be automatically appended to elements in prototype
+ * $group->addHidden('related_id');
+ * $group->addText('related_title');
+ * // this is identical to $group->addCheckbox('related_active');
+ * $repeat->addCheckbox('related_active');
+ *
+ * // value of this field will be used to find the indexes of repeated items
+ * $repeat->setIndexField('related_id');
+ * </code>
+ *
+ * @category HTML
+ * @package HTML_QuickForm2
+ * @author Alexey Borzov <avb@php.net>
+ * @author Bertrand Mansion <golgote@mamasam.com>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version Release: @package_version@
+ * @link http://pear.php.net/package/HTML_QuickForm2
+ */
+class HTML_QuickForm2_Container_Repeat extends HTML_QuickForm2_Container
+{
+ /**
+ * Key to replace by actual item index in elements' names / ids / values
+ */
+ const INDEX_KEY = ':idx:';
+
+ /**
+ * Regular expression used to check for valid indexes
+ */
+ const INDEX_REGEXP = '/^[a-zA-Z0-9_]+$/';
+
+ /**
+ * Field used to search for available indexes
+ * @var string
+ */
+ protected $indexField = null;
+
+ /**
+ * Available indexes
+ * @var array
+ */
+ protected $itemIndexes = array();
+
+ /**
+ * Errors for (repeated) child elements set during validate() call
+ * @var array
+ */
+ protected $childErrors = array();
+
+ /**
+ * Whether getDataSources() should return Container's data sources
+ *
+ * This is done to prevent useless updateValue() activity in child
+ * elements when their values are not going to be needed.
+ *
+ * @var bool
+ */
+ protected $passDataSources = false;
+
+ /**
+ * Returns the element's type
+ *
+ * @return string
+ */
+ public function getType()
+ {
+ return 'repeat';
+ }
+
+ /**
+ * Sets the element's value (not implemented)
+ *
+ * @param mixed $value element's value
+ *
+ * @throws HTML_QuickForm2_Exception
+ */
+ public function setValue($value)
+ {
+ throw new HTML_QuickForm2_Exception('Not implemented');
+ }
+
+ /**
+ * Sets the Container that will be used as a prototype for repeating
+ *
+ * @param HTML_QuickForm2_Container $prototype prototype container
+ *
+ * @return HTML_QuickForm2_Container_Repeat
+ */
+ public function setPrototype(HTML_QuickForm2_Container $prototype)
+ {
+ if (!empty($this->elements[0])) {
+ parent::removeChild($this->elements[0]);
+ $this->elements = array();
+ }
+ parent::appendChild($prototype);
+ return $this;
+ }
+
+ /**
+ * Returns the prototype Container
+ *
+ * @return HTML_QuickForm2_Container prototype
+ * @throws HTML_QuickForm2_NotFoundException if prototype was not set
+ */
+ protected function getPrototype()
+ {
+ if (empty($this->elements[0])) {
+ throw new HTML_QuickForm2_NotFoundException(
+ "Repeat element needs a prototype, use setPrototype()"
+ );
+ }
+ return $this->elements[0];
+ }
+
+ /**
+ * Appends an element to the prototype container
+ *
+ * Elements are kept in the prototype rather than directly in repeat
+ *
+ * @param HTML_QuickForm2_Node $element Element to add
+ *
+ * @return HTML_QuickForm2_Node Added element
+ * @throws HTML_QuickForm2_InvalidArgumentException
+ */
+ public function appendChild(HTML_QuickForm2_Node $element)
+ {
+ return $this->getPrototype()->appendChild($element);
+ }
+
+ /**
+ * Removes the element from the prototype container
+ *
+ * Elements are kept in the prototype rather than directly in repeat
+ *
+ * @param HTML_QuickForm2_Node $element Element to remove
+ *
+ * @return HTML_QuickForm2_Node Removed object
+ * @throws HTML_QuickForm2_NotFoundException
+ */
+ public function removeChild(HTML_QuickForm2_Node $element)
+ {
+ return $this->getPrototype()->removeChild($element);
+ }
+
+
+ /**
+ * Inserts an element to the prototype container
+ *
+ * Elements are kept in the prototype rather than directly in repeat
+ *
+ * @param HTML_QuickForm2_Node $element Element to insert
+ * @param HTML_QuickForm2_Node $reference Reference to insert before
+ *
+ * @return HTML_QuickForm2_Node Inserted element
+ */
+ public function insertBefore(
+ HTML_QuickForm2_Node $element, HTML_QuickForm2_Node $reference = null
+ ) {
+ return $this->getPrototype()->insertBefore($element, $reference);
+ }
+
+
+ /**
+ * Returns the data sources for this element
+ *
+ * @return array
+ * @see $passDataSources
+ */
+ protected function getDataSources()
+ {
+ if (!$this->passDataSources) {
+ return array();
+ } else {
+ return parent::getDataSources();
+ }
+ }
+
+ /**
+ * Sets a field to check for available indexes
+ *
+ * Form data sources will be searched for this field's value, indexes present
+ * in the array will be used for repeated elements. Use the field that will be
+ * always present in submit data: checkboxes, multiple selects and fields that
+ * may be disabled are bad choices
+ *
+ * @param string $field field name
+ */
+ public function setIndexField($field)
+ {
+ $this->indexField = $field;
+ $this->updateValue();
+ }
+
+ /**
+ * Tries to guess a field name to use for getting indexes of repeated items
+ *
+ * @return bool Whether we were able to guess something
+ * @see setIndexField()
+ */
+ private function _guessIndexField()
+ {
+ $this->appendIndexTemplates();
+ $this->passDataSources = false;
+ /* @var $child HTML_QuickForm2_Node */
+ foreach ($this->getRecursiveIterator(RecursiveIteratorIterator::LEAVES_ONLY) as $child) {
+ $name = $child->getName();
+ if (false === ($pos = strpos($name, '[' . self::INDEX_KEY . ']'))
+ || $child->getAttribute('disabled')
+ ) {
+ continue;
+ }
+ // The list is somewhat future-proof for HTML5 input elements
+ if ($child instanceof HTML_QuickForm2_Element_Input
+ && !($child instanceof HTML_QuickForm2_Element_InputButton
+ || $child instanceof HTML_QuickForm2_Element_InputCheckable
+ || $child instanceof HTML_QuickForm2_Element_InputFile
+ || $child instanceof HTML_QuickForm2_Element_InputImage
+ || $child instanceof HTML_QuickForm2_Element_InputReset
+ || $child instanceof HTML_QuickForm2_Element_InputSubmit)
+ || ($child instanceof HTML_QuickForm2_Element_Select
+ && !$child->getAttribute('multiple'))
+ || $child instanceof HTML_QuickForm2_Element_Textarea
+ ) {
+ $this->indexField = substr($name, 0, $pos);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the indexes for repeated items
+ *
+ * @return array
+ */
+ public function getIndexes()
+ {
+ if (null === $this->indexField && $this->_guessIndexField()) {
+ $this->updateValue();
+ }
+ return $this->itemIndexes;
+ }
+
+ /**
+ * Sets the indexes for repeated items
+ *
+ * As is the case with elements' values, the indexes will be updated
+ * from data sources, so use this after all possible updates were done.
+ *
+ * @param array $indexes
+ */
+ public function setIndexes(array $indexes)
+ {
+ $hash = array();
+ foreach ($indexes as $index) {
+ if (preg_match(self::INDEX_REGEXP, $index)) {
+ $hash[$index] = true;
+ }
+ }
+ $this->itemIndexes = array_keys($hash);
+ }
+
+ /**
+ * Called when the element needs to update its value from form's data sources
+ *
+ * Behaves similar to Element::updateValue(), the field's value is used to
+ * deduce indexes taken by repeat items.
+ *
+ * @see setIndexField()
+ */
+ protected function updateValue()
+ {
+ // check that we are not added to another Repeat
+ // done here instead of in setContainer() for reasons outlined in InputFile
+ $container = $this->getContainer();
+ while (!empty($container)) {
+ if ($container instanceof self) {
+ throw new HTML_QuickForm2_Exception(
+ "Repeat element cannot be added to another Repeat element"
+ );
+ }
+ $container = $container->getContainer();
+ }
+
+ if (null === $this->indexField && !$this->_guessIndexField()) {
+ return;
+ }
+ /* @var HTML_QuickForm2_DataSource $ds */
+ foreach (parent::getDataSources() as $ds) {
+ if (null !== ($value = $ds->getValue($this->indexField))) {
+ $this->setIndexes(array_keys($value));
+ return;
+ }
+ }
+ }
+
+ /**
+ * Appends the template to elements' names and ids that will be later replaced by index
+ *
+ * Default behaviour is to append '[:idx:]' to element names and '_:idx:' to
+ * element ids. If the string ':idx:' is already present in the attribute,
+ * then it will not be changed.
+ *
+ * Checkboxes and radios may contain ':idx:' in their 'value' attribute,
+ * in this case their 'name' attribute is left alone. Names of groups are
+ * also not touched.
+ */
+ protected function appendIndexTemplates()
+ {
+ $this->passDataSources = true;
+ /* @var HTML_QuickForm2_Node $child */
+ foreach ($this->getRecursiveIterator() as $child) {
+ $id = $child->getId();
+ if (false === strpos($id, self::INDEX_KEY)) {
+ $child->setId($id . '_' . self::INDEX_KEY);
+ }
+ $name = $child->getName();
+ // checkboxes and radios can have index inside "value" attribute instead,
+ // group names should not be touched
+ if (strlen($name) && false === strpos($name, self::INDEX_KEY)
+ && (!$child instanceof HTML_QuickForm2_Container || !$child->prependsName())
+ && (!$child instanceof HTML_QuickForm2_Element_InputCheckable
+ || false === strpos($child->getAttribute('value'), self::INDEX_KEY))
+ ) {
+ $child->setName($name . '[' . self::INDEX_KEY . ']');
+ }
+ }
+ }
+
+ /**
+ * Backs up child attributes
+ *
+ * @param bool $backupId whether to backup id attribute
+ * @param bool $backupError whether to backup error message
+ *
+ * @return array backup array
+ */
+ protected function backupChildAttributes($backupId = false, $backupError = false)
+ {
+ $this->appendIndexTemplates();
+ $backup = array();
+ $key = 0;
+ /* @var HTML_QuickForm2_Node $child */
+ foreach ($this->getRecursiveIterator() as $child) {
+ $backup[$key++] = array(
+ 'name' => $child->getName(),
+ ) + (
+ $child instanceof HTML_QuickForm2_Element_InputCheckable
+ ? array('valueAttr' => $child->getAttribute('value')) : array()
+ ) + (
+ $child instanceof HTML_QuickForm2_Container
+ ? array() : array('value' => $child->getValue())
+ ) + (
+ $backupId ? array('id' => $child->getId()) : array()
+ ) + (
+ $backupError ? array('error' => $child->getError()) : array()
+ );
+ }
+ return $backup;
+ }
+
+ /**
+ * Restores child attributes from backup array
+ *
+ * @param array $backup backup array
+ *
+ * @see backupChildAttributes()
+ */
+ protected function restoreChildAttributes(array $backup)
+ {
+ $key = 0;
+ /* @var HTML_QuickForm2_Node $child */
+ foreach ($this->getRecursiveIterator() as $child) {
+ if (array_key_exists('value', $backup[$key])) {
+ $child->setValue($backup[$key]['value']);
+ }
+ if (false !== strpos($backup[$key]['name'], self::INDEX_KEY)) {
+ $child->setName($backup[$key]['name']);
+ }
+ if ($child instanceof HTML_QuickForm2_Element_InputCheckable
+ && false !== strpos($backup[$key]['valueAttr'], self::INDEX_KEY)
+ ) {
+ $child->setAttribute('value', $backup[$key]['valueAttr']);
+ }
+ if (array_key_exists('id', $backup[$key])) {
+ $child->setId($backup[$key]['id']);
+ }
+ if (array_key_exists('error', $backup[$key])) {
+ $child->setError($backup[$key]['error']);
+ }
+ $key++;
+ }
+ $this->passDataSources = false;
+ }
+
+ /**
+ * Replaces a template in elements' attributes by a numeric index
+ *
+ * @param int $index numeric index
+ * @param array $backup backup array, contains attributes with templates
+ *
+ * @see backupChildAttributes()
+ */
+ protected function replaceIndexTemplates($index, array $backup)
+ {
+ $this->passDataSources = true;
+ $key = 0;
+ /* @var HTML_QuickForm2_Node $child */
+ foreach ($this->getRecursiveIterator() as $child) {
+ if (false !== strpos($backup[$key]['name'], self::INDEX_KEY)) {
+ $child->setName(str_replace(self::INDEX_KEY, $index, $backup[$key]['name']));
+ }
+ if ($child instanceof HTML_QuickForm2_Element_InputCheckable
+ && false !== strpos($backup[$key]['valueAttr'], self::INDEX_KEY)
+ ) {
+ $child->setAttribute(
+ 'value', str_replace(self::INDEX_KEY, $index, $backup[$key]['valueAttr'])
+ );
+ }
+ if (array_key_exists('id', $backup[$key])) {
+ $child->setId(str_replace(self::INDEX_KEY, $index, $backup[$key]['id']));
+ }
+ if (array_key_exists('error', $backup[$key])) {
+ $child->setError();
+ }
+ $key++;
+ }
+ }
+
+ /**
+ * Returns the array containing child elements' values
+ *
+ * Iterates over all available repeat indexes to get values
+ *
+ * @param bool $filtered Whether child elements should apply filters on values
+ *
+ * @return array|null
+ */
+ protected function getChildValues($filtered = false)
+ {
+ $backup = $this->backupChildAttributes();
+ $values = array();
+ foreach ($this->getIndexes() as $index) {
+ $this->replaceIndexTemplates($index, $backup);
+ $values = self::arrayMerge(
+ $values, parent::getChildValues($filtered)
+ );
+ }
+ $this->restoreChildAttributes($backup);
+ return empty($values) ? null : $values;
+ }
+
+ /**
+ * Performs the server-side validation
+ *
+ * Iterates over all available repeat indexes and calls validate() on
+ * prototype container.
+ *
+ * @return boolean Whether the repeat and all repeated items are valid
+ */
+ protected function validate()
+ {
+ $backup = $this->backupChildAttributes(false, true);
+ $valid = true;
+ $this->childErrors = array();
+ foreach ($this->getIndexes() as $index) {
+ $this->replaceIndexTemplates($index, $backup);
+ $valid = $this->getPrototype()->validate() && $valid;
+ /* @var HTML_QuickForm2_Node $child */
+ foreach ($this->getRecursiveIterator() as $child) {
+ if (strlen($error = $child->getError())) {
+ $this->childErrors[$child->getId()] = $error;
+ }
+ }
+ }
+ $this->restoreChildAttributes($backup);
+ foreach ($this->rules as $rule) {
+ if (strlen($this->error)) {
+ break;
+ }
+ if ($rule[1] & HTML_QuickForm2_Rule::SERVER) {
+ $rule[0]->validate();
+ }
+ }
+ return !strlen($this->error) && $valid;
+ }
+
+ /**
+ * Generates Javascript code to initialize repeat behaviour
+ *
+ * @param HTML_QuickForm2_Container_Repeat_JavascriptBuilder $evalBuilder
+ * Javascript builder returning JS string literals
+ *
+ * @return string javascript
+ */
+ private function _generateInitScript(
+ HTML_QuickForm2_Container_Repeat_JavascriptBuilder $evalBuilder
+ ) {
+ $myId = HTML_QuickForm2_JavascriptBuilder::encode($this->getId());
+ $protoId = HTML_QuickForm2_JavascriptBuilder::encode($this->getPrototype()->getId());
+ $triggers = HTML_QuickForm2_JavascriptBuilder::encode(
+ $this->getJavascriptTriggers()
+ );
+ list ($rules, $scripts) = $evalBuilder->getFormJavascriptAsStrings();
+
+ return "new qf.Repeat(document.getElementById({$myId}), {$protoId}, {$triggers},\n"
+ . $rules . ",\n" . $scripts . "\n);";
+ }
+
+ /**
+ * Renders the container using the given renderer
+ *
+ * Container will be output N + 1 times, where N are visible items and 1 is
+ * the hidden prototype used by Javascript code to create new items.
+ *
+ * @param HTML_QuickForm2_Renderer $renderer renderer to use
+ *
+ * @return HTML_QuickForm2_Renderer
+ */
+ public function render(HTML_QuickForm2_Renderer $renderer)
+ {
+ $backup = $this->backupChildAttributes(true, true);
+ $hiddens = $renderer->getOption('group_hiddens');
+ $jsBuilder = $renderer->getJavascriptBuilder();
+ $evalBuilder = new HTML_QuickForm2_Container_Repeat_JavascriptBuilder();
+
+ $renderer->setJavascriptBuilder($evalBuilder)
+ ->setOption('group_hiddens', false)
+ ->startContainer($this);
+
+ // first, render a (hidden) prototype
+ $this->getPrototype()->addClass('repeatItem repeatPrototype');
+ $this->getPrototype()->render($renderer);
+ $this->getPrototype()->removeClass('repeatPrototype');
+
+ // restore original JS builder
+ $evalBuilder->passLibraries($jsBuilder);
+ $renderer->setJavascriptBuilder($jsBuilder);
+
+ // next, render all available rows
+ foreach ($this->getIndexes() as $index) {
+ $this->replaceIndexTemplates($index, $backup);
+ /* @var HTML_QuickForm2_Node $child */
+ foreach ($this->getRecursiveIterator() as $child) {
+ if (isset($this->childErrors[$id = $child->getId()])) {
+ $child->setError($this->childErrors[$id]);
+ }
+ }
+ $this->getPrototype()->render($renderer);
+ }
+ $this->restoreChildAttributes($backup);
+
+ $jsBuilder->addLibrary('repeat', 'quickform-repeat.js');
+ $jsBuilder->addElementJavascript($this->_generateInitScript($evalBuilder));
+ $this->renderClientRules($jsBuilder);
+
+ $renderer->finishContainer($this);
+ $renderer->setOption('group_hiddens', $hiddens);
+ return $renderer;
+ }
+}
+?>
View
29 HTML/QuickForm2/Element/Hierselect.php
@@ -278,6 +278,22 @@ public function setValue($value)
return $this;
}
+ /**
+ * Sets the element's name
+ *
+ * Need to override group's implementation due to overridden updateValue()
+ *
+ * @param string $name
+ *
+ * @return HTML_QuickForm2_Element_Hierselect
+ */
+ public function setName($name)
+ {
+ parent::setName($name);
+ $this->updateValue();
+ return $this;
+ }
+
/**
* Called when the element needs to update its value from form's data sources
*
@@ -367,19 +383,6 @@ private function _generateInitScript()
. (empty($this->jsCallback)? '': ", {$this->jsCallback}") . ');';
}
- public function __toString()
- {
- require_once 'HTML/QuickForm2/Renderer.php';
-
- $cr = HTML_Common2::getOption('linebreak');
- return $this->render(
- HTML_QuickForm2_Renderer::factory('default')
- ->setTemplateForId($this->getId(), '{content}')
- )->__toString()
- . "<script type=\"text/javascript\">{$cr}//<![CDATA[{$cr}"
- . $this->_generateInitScript() . "{$cr}//]]>{$cr}</script>";
- }
-
/**
* Renders the hierselect using the given renderer
*
View
2  HTML/QuickForm2/Element/Input.php
@@ -84,7 +84,7 @@ public function getType()
public function setValue($value)
{
- $this->setAttribute('value', $value);
+ $this->setAttribute('value', (string)$value);
return $this;
}
View
4 HTML/QuickForm2/Element/InputCheckbox.php
@@ -96,6 +96,10 @@ protected function updateValue()
return;
}
}
+ // if *some* data sources were searched and we did not find a value -> uncheck the box
+ if (!empty($ds)) {
+ $this->removeAttribute('checked');
+ }
}
}
?>
View
66 HTML/QuickForm2/JavascriptBuilder.php
@@ -181,11 +181,7 @@ public function getLibraries($inline = false, $addScriptTags = true)
: $path . $library['file'];
}
}
- if ($inline && '' != $ret && $addScriptTags) {
- $ret = "<script type=\"text/javascript\">\n//<![CDATA[\n"
- . $ret . "\n//]]>\n</script>";
- }
- return $ret;
+ return ($inline && $addScriptTags) ? $this->wrapScript($ret) : $ret;
}
@@ -239,26 +235,70 @@ public function addElementJavascript($script)
*/
public function getFormJavascript($formId = null, $addScriptTags = true)
{
+ $js = $this->getValidator($formId, false);
+ $js .= ('' == $js ? '' : "\n") . $this->getSetupCode($formId, false);
+ return $addScriptTags ? $this->wrapScript($js) : $js;
+ }
+
+
+ /**
+ * Returns setup code for form elements
+ *
+ * @param string $formId form ID, if empty returns code for all forms
+ * @param bool $addScriptTags whether to enclose code in <script> tags
+ *
+ * @return string
+ */
+ public function getSetupCode($formId = null, $addScriptTags = false)
+ {
$js = '';
- foreach ($this->rules as $id => $rules) {
- if ((null === $formId || $id == $formId) && !empty($rules)) {
- $js .= ('' == $js? '': "\n") . "new qf.Validator(document.getElementById('{$id}'), [\n"
- . implode(",\n", $rules) . "\n]);";
- }
- }
foreach ($this->scripts as $id => $scripts) {
if ((null === $formId || $id == $formId) && !empty($scripts)) {
$js .= ('' == $js? '': "\n") . implode("\n", $scripts);
}
}
- if ('' != $js && $addScriptTags) {
+ return $addScriptTags ? $this->wrapScript($js) : $js;
+ }
+
+
+ /**
+ * Returns client-side validation code
+ *
+ * @param string $formId form ID, if empty returns code for all forms
+ * @param bool $addScriptTags whether to enclose code in <script> tags
+ *
+ * @return string
+ */
+ public function getValidator($formId = null, $addScriptTags = false)
+ {
+ $js = '';
+ foreach ($this->rules as $id => $rules) {
+ if ((null === $formId || $id == $formId) && !empty($rules)) {
+ $js .= ('' == $js ? '' : "\n")
+ . "new qf.Validator(document.getElementById('{$id}'), [\n"
+ . implode(",\n", $rules) . "\n]);";
+ }
+ }
+ return $addScriptTags ? $this->wrapScript($js) : $js;
+ }
+
+ /**
+ * Wraps the given Javascript code in <script> tags
+ *
+ * @param string $js Javascript code
+ *
+ * @return string code wrapped in <script></script> tags,
+ * empty string if $js is empty
+ */
+ protected function wrapScript($js)
+ {
+ if ('' != $js) {
$js = "<script type=\"text/javascript\">\n//<![CDATA[\n"
. $js . "\n//]]>\n</script>";
}
return $js;
}
-
/**
* Encodes a value for use as Javascript literal
*
View
8 HTML/QuickForm2/Renderer/Default.php
@@ -112,7 +112,8 @@ class HTML_QuickForm2_Renderer_Default extends HTML_QuickForm2_Renderer
'suffix' => '</li></ul><qf:message><p>{message}</p></qf:message></div>'
),
'html_quickform2_element' => '<div class="row"><p class="label"><qf:required><span class="required">*</span></qf:required><qf:label><label for="{id}">{label}</label></qf:label></p><div class="element<qf:error> error</qf:error>"><qf:error><span class="error">{error}<br /></span></qf:error>{element}</div></div>',
- 'html_quickform2_container_group' => '<div class="row"><p class="label"><qf:required><span class="required">*</span></qf:required><qf:label><label>{label}</label></qf:label></p><div class="element group<qf:error> error</qf:error>" id="{id}"><qf:error><span class="error">{error}<br /></span></qf:error>{content}</div></div>'
+ 'html_quickform2_container_group' => '<div class="row {class}"><p class="label"><qf:required><span class="required">*</span></qf:required><qf:label><label>{label}</label></qf:label></p><div class="element group<qf:error> error</qf:error>" id="{id}"><qf:error><span class="error">{error}<br /></span></qf:error>{content}</div></div>',
+ 'html_quickform2_container_repeat' => '<div class="row repeat" id="{id}"><qf:label><p>{label}</p></qf:label>{content}<br /><a href="#" class="repeatAdd">Add...</a></div>'
);
/**
@@ -364,8 +365,9 @@ public function startGroup(HTML_QuickForm2_Node $group)
public function finishGroup(HTML_QuickForm2_Node $group)
{
$gTpl = str_replace(
- array('{attributes}', '{id}'),
- array($group->getAttributes(true), array_pop($this->groupId)),
+ array('{attributes}', '{id}', '{class}'),
+ array($group->getAttributes(true), array_pop($this->groupId),
+ $group->getAttribute('class')),
$this->prepareTemplate($this->findTemplate($group, '{content}'), $group)
);
View
4 data/quickform.css
@@ -78,3 +78,7 @@
border: 1px solid red;
padding: 0.5em;
}
+
+.quickform .repeatPrototype {
+ display: none;
+}
View
21 js/build.xml
@@ -8,9 +8,11 @@
<property name="js.concat.base" value="build/quickform.js" />
<property name="js.concat.hs" value="build/quickform-hierselect.js" />
+ <property name="js.concat.repeat" value="build/quickform-repeat.js" />
<property name="js.filelist.base" value="build/filelist.txt" />
<property name="js.minified.base" value="build/quickform.min.js" />
<property name="js.minified.hs" value="build/quickform-hierselect.min.js" />
+ <property name="js.minified.repeat" value="build/quickform-repeat.min.js" />
<property name="release.concat" value="../data/js/" />
<property name="release.minified" value="../data/js/min/" />
@@ -35,7 +37,8 @@
<replaceregexp>
<regexp pattern="/\*\s*\$Id[^$]*\$\s*\*/\s+" replace="" />
</replaceregexp>
- </filterchain>
+ </filterchain>
+ <filelist dir="src/" files="${js.src.basesecond}" />
</append>
<copy file="src/hierselect.js" tofile="${js.concat.hs}">
<filterchain>
@@ -44,11 +47,18 @@
</replaceregexp>
</filterchain>
</copy>
+ <copy file="src/repeat.js" tofile="${js.concat.repeat}">
+ <filterchain>
+ <replaceregexp>
+ <regexp pattern="@preserve\s+" replace="" />
+ </replaceregexp>
+ </filterchain>
+ </copy>
</target>
<target name="appendjsfile">
- <target name="appendjsfile">
- <echo append="true" file="${js.filelist.base}" message=" --js src/${srcname}" />
+ <echo append="true" file="${js.filelist.base}" message=" --js src/${srcname}" />
+ </target>
<target name="minify" depends="init" description="Build minified javascript files using closure compiler">
<available file="${path.closure}" property="has_closure" />
<fail unless="has_closure" message="Closure compiler is not available" />
@@ -56,6 +66,7 @@
<foreach list="${js.src.basesecond}" param="srcname" target="appendjsfile" />
<exec logoutput="true" command="${closure_command} --js_output_file ${js.minified.base} --flagfile ${js.filelist.base}" />
<exec logoutput="true" command="${closure_command} --js_output_file ${js.minified.hs} --js src/hierselect.js" />
+ <exec logoutput="true" command="${closure_command} --js_output_file ${js.minified.repeat} --js src/repeat.js" />
</target>
<target name="docs" depends="init" description="Build API documentation using JSDoc toolkit">
@@ -73,7 +84,7 @@
<target name="clean" depends="clean-js, clean-docs" description="Removes all generated files">
</target>
-
+ <target name="build-all" depends="clean, concat, minify, docs" description="Builds everything" />
<target name="release" depends="clean, concat, minify" description="Builds JS files and copies them to data">
<copy todir="${release.concat}">
@@ -81,6 +92,7 @@
<fileset dir=".">
<include name="${js.concat.base}" />
<include name="${js.concat.hs}" />
+ <include name="${js.concat.repeat}" />
</fileset>
</copy>
<copy todir="${release.minified}">
@@ -88,6 +100,7 @@
<fileset dir=".">
<include name="${js.minified.base}" />
<include name="${js.minified.hs}" />
+ <include name="${js.minified.repeat}" />
</fileset>
</copy>
</target>
View
299 js/src/repeat.js
@@ -0,0 +1,299 @@
+/**
+ * @preserve HTML_QuickForm2: support functions for repeat elements
+ * Package version @package_version@
+ * http://pear.php.net/package/HTML_QuickForm2
+ *
+ * Copyright 2006-2012, Alexey Borzov, Bertrand Mansion
+ * Licensed under new BSD license
+ * http://opensource.org/licenses/bsd-license.php
+ */
+
+/* $Id$ */
+
+/**
+ * Sets repeat properties and attaches handlers for adding and removing items
+ *
+ * @param {HTMLElement} container
+ * @param {String} itemId
+ * @param {String[]} triggers
+ * @param {String} rulesTpl
+ * @param {String} scriptsTpl
+ * @constructor
+ */
+qf.Repeat = function(container, itemId, triggers, rulesTpl, scriptsTpl)
+{
+ container.repeat = this;
+
+ /**
+ * Form containing the repeat element
+ * @type {HTMLFormElement}
+ */
+ this.form = null;
+
+ /**
+ * Prototype item which will be cloned in add()
+ * @type {HTMLElement}
+ */
+ this.repeatPrototype = null;
+
+ /**
+ * HTML element containing all repeated items
+ * @type {HTMLElement}
+ */
+ this.container = container;
+
+ /**
+ * Id of repeated items, used to deduce index
+ * @type {String}
+ */
+ this.itemId = itemId;
+
+ /**
+ * String containing validation code template, will be eval()'d
+ * @type {String}
+ */
+ this.rulesTpl = rulesTpl;
+
+ /**
+ * String containing elements setup code template, will be eval()'d
+ * @type {String}
+ */
+ this.scriptsTpl = scriptsTpl;
+
+ /**
+ * Templates for element's id attributes, used to remove rules on removing repeated item
+ * @type {String[]}
+ */
+ this.triggers = triggers;
+
+ // find all elements with class repeatAdd inside container...
+ var adders = this.getElementsByClass('repeatAdd', container);
+ for (var i = 0, element; element = adders[i]; i++) {
+ qf.events.addListener(element, 'click', qf.Repeat.addHandler);
+ }
+ // find all elements with class repeatRemove inside container...
+ var removers = this.getElementsByClass('repeatRemove', container);
+ for (i = 0; element = removers[i]; i++) {
+ qf.events.addListener(element, 'click', qf.Repeat.removeHandler);
+ }
+};
+
+/**
+ * Event handler for "add item" onclick events, added automatically on elements with class 'repeatAdd'
+ *
+ * @param {Event} event
+ */
+qf.Repeat.addHandler = function(event)
+{
+ event = qf.events.fixEvent(event);
+
+ var parent = event.target;
+ while (parent && !qf.classes.has(parent, 'repeat')) {
+ parent = parent.parentNode;
+ }
+ if (parent && parent.repeat) {
+ parent.repeat.add();
+ }
+ event.preventDefault();
+};
+
+/**
+ * Event handler for "remove item" onclick events, added automatically on elements with class 'repeatRemove'
+ *
+ * @param {Event} event
+ */
+qf.Repeat.removeHandler = function(event)
+{
+ event = qf.events.fixEvent(event);
+
+ var parent = event.target,
+ item;
+ while (parent && !qf.classes.has(parent, 'repeat')) {
+ if (qf.classes.has(parent, 'repeatItem')) {
+ item = parent;
+ }
+ parent = parent.parentNode;
+ }
+ if (parent && parent.repeat) {
+ parent.repeat.remove(item);
+ }
+ event.preventDefault();
+};
+
+qf.Repeat.prototype = {
+ /**
+ * Finds elements by CSS class name
+ *
+ * Wraps around native getElementsByClassName() if available, uses a custom
+ * implementation if not.
+ *
+ * @function
+ * @param {String} className
+ * @param {Node} node
+ * @returns {Node[]}
+ */
+ getElementsByClass: (function() {
+ if (document.getElementsByClassName) {
+ return function(className, node) {
+ return node.getElementsByClassName(className);
+ };
+ } else {
+ return function(className, node) {
+ var list = node.getElementsByTagName('*'),
+ result = [];
+
+ for (var i = 0, child; child = list[i]; i++) {
+ if (qf.classes.has(child, className)) {
+ result.push(child);
+ }
+ }
+ return result;
+ };
+ }
+ })(),
+ /**
+ * Finds a numeric index for a given repeat item
+ *
+ * @param {Node} item
+ * @returns {Number}
+ */
+ findIndex: function(item)
+ {
+ var itemRegexp = new RegExp('^' + this.itemId.replace(':idx:', '([a-zA-Z0-9_]+?)') + '$'),
+ m;
+
+ if (item.id && (m = itemRegexp.exec(item.id))) {
+ // item has the needed id itself (fieldset case)
+ return m[1];
+ } else {
+ // search for item with a needed id (group case)
+ var elements = item.getElementsByTagName('*');
+ for (var i = 0, element; element = elements[i]; i++) {
+ if (element.id && (m = itemRegexp.exec(element.id))) {
+ return m[1];
+ }
+ }
+ }
+ },
+ /**
+ * Finds a form containing repeat element
+ *
+ * @returns {HTMLFormElement}
+ */
+ findForm: function()
+ {
+ var parent = this.container;
+ while (parent && 'form' !== parent.nodeName.toLowerCase()) {
+ parent = parent.parentNode;
+ }
+ return parent;
+ },
+ /**
+ * Generates a new index for item being added to the repeat
+ *
+ * @returns {String}
+ */
+ generateIndex: function()
+ {
+ var index;
+
+ do {
+ // 10000 will be enough for everybody!
+ index = 'add' + Math.round(Math.random() * 10000);
+ } while (document.getElementById(this.itemId.replace(':idx:', index)));
+ return index;
+ },
+ /**
+ * Adds a new repeated item to the repeat element
+ */
+ add: function()
+ {
+ if (!this.repeatPrototype) {
+ this.repeatPrototype = this.getElementsByClass('repeatPrototype', this.container)[0];
+ }
+
+ var items = this.getElementsByClass('repeatItem', this.container),
+ lastItem = items[items.length - 1],
+ clone = this.repeatPrototype.cloneNode(true),
+ index = this.generateIndex();
+
+ qf.classes.remove(clone, 'repeatPrototype');
+ if (clone.id) {
+ clone.id = clone.id.replace(':idx:', index);
+ }
+ // maybe get rid of this and mangle innerHTML instead?
+ var elements = clone.getElementsByTagName('*');
+ for (var i = 0, element; element = elements[i]; i++) {
+ if (element.id) {
+ element.id = element.id.replace(':idx:', index);
+ }
+ if (element.name) {
+ element.name = element.name.replace(':idx:', index);
+ }
+ if (element.type && ('checkbox' == element.type || 'radio' == element.type)) {
+ element.value = element.value.replace(':idx:', index);
+ }
+ if (element.htmlFor) {
+ element.htmlFor = element.htmlFor.replace(':idx:', index);
+ }
+ // inline script found, eval() 'em
+ if ('script' == element.nodeName.toLowerCase()) {
+ eval(element.innerHTML.replace(/:idx:/g, index));
+ }
+ if (qf.classes.has(element, 'repeatAdd')) {
+ qf.events.addListener(element, 'click', qf.Repeat.addHandler);
+ }
+ if (qf.classes.has(element, 'repeatRemove')) {
+ qf.events.addListener(element, 'click', qf.Repeat.removeHandler);
+ }
+ }
+
+ lastItem.parentNode.insertBefore(clone, lastItem.nextSibling);
+
+ if (this.scriptsTpl) {
+ eval(this.scriptsTpl.replace(/:idx:/g, index));
+ }
+ if (this.rulesTpl) {
+ if (!this.form) {
+ this.form = this.findForm();
+ }
+ if (this.form.validator) {
+ var rules = eval(this.rulesTpl.replace(/:idx:/g, index)),
+ rule;
+ for (i = 0; rule = rules[i]; i++) {
+ this.form.validator.rules.push(rule);
+ }
+ }
+ }
+ },
+ /**
+ * Removes an item from repeat element
+ *
+ * @param {Node} item
+ */
+ remove: function(item)
+ {
+ if (this.rulesTpl) {
+ if (!this.form) {
+ this.form = this.findForm();
+ }
+ if (this.form.validator) {
+ var check = new qf.Map(),
+ index = this.findIndex(item),
+ rules = this.form.validator.rules,
+ trigger, rule, i;
+ for (i = 0; trigger = this.triggers[i]; i++) {
+ check.set(trigger.replace(':idx:', index), true);
+ }
+ for (i = rules.length - 1; rule = rules[i]; i--) {
+ // repeated IDs are unlikely to appear in rule.triggers
+ // without appearing in rule.owner, so we check only owner
+ if (check.hasKey(rule.owner)) {
+ rules.splice(i, 1);
+ }
+ }
+ }
+ }
+ item.parentNode.removeChild(item);
+ }
+};
View
2  tests/QuickForm2/Container/AllTests.php
@@ -49,6 +49,7 @@
require_once dirname(__FILE__) . '/FieldsetTest.php';
require_once dirname(__FILE__) . '/GroupTest.php';
+require_once dirname(__FILE__) . '/RepeatTest.php';
class QuickForm2_Container_AllTests
{
@@ -66,6 +67,7 @@ public static function suite()
$suite->addTestSuite('HTML_QuickForm2_Element_FieldsetTest');
$suite->addTestSuite('HTML_QuickForm2_Element_GroupTest');
+ $suite->addTestSuite('HTML_QuickForm2_Container_RepeatTest');
return $suite;
}
View
5 tests/QuickForm2/Container/FieldsetTest.php
@@ -46,11 +46,6 @@
require_once dirname(dirname(dirname(__FILE__))) . '/TestHelper.php';
/**
- * Class for <fieldset> elements
- */
-require_once 'HTML/QuickForm2/Container/Fieldset.php';
-
-/**
* Unit test for HTML_QuickForm2_Element_Fieldset class
*/
class HTML_QuickForm2_Element_FieldsetTest extends PHPUnit_Framework_TestCase
View
10 tests/QuickForm2/Container/GroupTest.php
@@ -46,16 +46,6 @@
require_once dirname(dirname(dirname(__FILE__))) . '/TestHelper.php';
/**
- * Class for <group> elements
- */
-require_once 'HTML/QuickForm2/Container/Group.php';
-
-/**
- * Class representing a HTML form
- */
-require_once 'HTML/QuickForm2.php';
-
-/**
* Unit test for HTML_QuickForm2_Element_Group class
*/
class HTML_QuickForm2_Element_GroupTest extends PHPUnit_Framework_TestCase
View
206 tests/QuickForm2/Container/RepeatTest.php
@@ -0,0 +1,206 @@
+<?php
+/**
+ * Unit tests for HTML_QuickForm2 package
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2006-2012, Alexey Borzov <avb@php.net>,
+ * Bertrand Mansion <golgote@mamasam.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category HTML
+ * @package HTML_QuickForm2
+ * @author Alexey Borzov <avb@php.net>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version SVN: $Id$
+ * @link http://pear.php.net/package/HTML_QuickForm2
+ */
+
+/** Sets up includes */
+require_once dirname(dirname(dirname(__FILE__))) . '/TestHelper.php';
+
+/**
+ * Unit test for HTML_QuickForm2_Container_Repeat class
+ */
+class HTML_QuickForm2_Container_RepeatTest extends PHPUnit_Framework_TestCase
+{
+ public function testCannotAddRepeatToRepeat()
+ {
+ $repeatOne = new HTML_QuickForm2_Container_Repeat();
+ $repeatTwo = new HTML_QuickForm2_Container_Repeat();
+
+ try {
+ $repeatOne->setPrototype($repeatTwo);
+ $this->fail('Expected HTML_QuickForm2_Exception was not thrown');
+ } catch (HTML_QuickForm2_Exception $e) {}
+
+ $fieldset = new HTML_QuickForm2_Container_Fieldset();
+ $repeatOne->setPrototype($fieldset);
+
+ try {
+ $fieldset->appendChild($repeatTwo);
+ $this->fail('Expected HTML_QuickForm2_Exception was not thrown');
+ } catch (HTML_QuickForm2_Exception $e) {}
+ }
+
+ public function testPrototypeRequiredForDOMAndOutput()
+ {
+ $repeat = new HTML_QuickForm2_Container_Repeat();
+ $text = new HTML_QuickForm2_Element_InputText('aTextBox');
+
+ try {
+ $repeat->appendChild($text);
+ $this->fail('Expected HTML_QuickForm2_NotFoundException not found');
+ } catch (HTML_QuickForm2_NotFoundException $e) {}
+
+ try {
+ $repeat->insertBefore($text);
+ $this->fail('Expected HTML_QuickForm2_NotFoundException not found');
+ } catch (HTML_QuickForm2_NotFoundException $e) {}
+
+ try {
+ $repeat->render(HTML_QuickForm2_Renderer::factory('default'));
+ $this->fail('Expected HTML_QuickForm2_NotFoundException not found');
+ } catch (HTML_QuickForm2_NotFoundException $e) {}
+ }
+
+ public function testElementsAreAddedToPrototype()
+ {
+ $repeat = new HTML_QuickForm2_Container_Repeat();
+ $fieldset = new HTML_QuickForm2_Container_Fieldset();
+ $textOne = new HTML_QuickForm2_Element_InputText('firstText');
+ $textTwo = new HTML_QuickForm2_Element_InputText('secondText');
+
+ $repeat->setPrototype($fieldset);
+ $repeat->appendChild($textOne);
+ $this->assertSame($textOne->getContainer(), $fieldset);
+
+ $repeat->insertBefore($textTwo, $textOne);
+ $this->assertSame($textTwo->getContainer(), $fieldset);
+
+ $repeat->removeChild($textOne);
+ $this->assertNull($textOne->getContainer());
+ }
+
+ public function testSetIndexesExplicitly()
+ {
+ $repeat = new HTML_QuickForm2_Container_Repeat();
+ $this->assertEquals(array(), $repeat->getIndexes());
+
+ $repeat->setIndexes(array('foo', 'bar', 'baz', 'qu\'ux', 'baz', 25));
+ $this->assertEquals(array('foo', 'bar', 'baz', 25), $repeat->getIndexes());
+ }
+
+ public function testSetIndexFieldExplicitly()
+ {
+ $form = new HTML_QuickForm2('testIndexField');
+ $form->addDataSource(new HTML_QuickForm2_DataSource_Array(array(
+ 'blah' => array(
+ 'blergh' => 'a',
+ 'blurgh' => 'b',
+ 'ba-a-a-ah' => 'c',
+ 42 => 'd'
+ ),
+ 'argh' => array(
+ 'a' => 'e',
+ 'b\'c' => 'f',
+ 'd' => 'g'
+ )
+ )));
+
+ $repeat = new HTML_QuickForm2_Container_Repeat();
+ $repeat->setIndexField('blah');
+ $repeat->setIndexes(array('foo', 'bar'));
+ $form->appendChild($repeat);
+ $this->assertEquals(array('blergh', 'blurgh', 42), $repeat->getIndexes());
+
+ $repeat->setIndexField('argh');
+ $this->assertEquals(array('a', 'd'), $repeat->getIndexes());
+ }
+
+ public function testGuessIndexField()
+ {
+ $form = new HTML_QuickForm2('guessIndexField');
+ $form->addDataSource(new HTML_QuickForm2_DataSource_Array(array(
+ 'blah' => array('foo' => 1),
+ 'bzz' => array('bar' => array('a', 'b')),
+ 'aaargh' => array('foo' => ''),
+ 'blergh' => array('foo' => '', 'bar' => 'bar value')
+ )));
+
+ $repeat = new HTML_QuickForm2_Container_Repeat();
+ $form->appendChild($repeat);
+
+ $this->assertEquals(array(), $repeat->getIndexes());
+
+ $fieldset = new HTML_QuickForm2_Container_Fieldset();
+ $repeat->setPrototype($fieldset);
+ $this->assertEquals(array(), $repeat->getIndexes());
+
+ $fieldset->addCheckbox('blah');
+ $this->assertEquals(array(), $repeat->getIndexes());
+
+ $fieldset->addSelect('bzz', array('multiple'));
+ $this->assertEquals(array(), $repeat->getIndexes());
+
+ $fieldset->addText('aaargh', array('disabled'));
+ $this->assertEquals(array(), $repeat->getIndexes());
+
+ $fieldset->addText('blergh');
+ $this->assertEquals(array('foo', 'bar'), $repeat->getIndexes());
+ }
+
+ public function testGetValue()
+ {
+ $values = array(
+ 'foo' => array('a' => 'a value', 'b' => 'b value', 'c' => 'c value'),
+ 'bar' => array(
+ 'baz' => array('a' => 'aa', 'b' => 'bb', 'c' => 'cc')
+ )
+ );
+
+ $form = new HTML_QuickForm2('repeatValue');
+ $repeat = new HTML_QuickForm2_Container_Repeat();
+ $form->addDataSource(new HTML_QuickForm2_DataSource_Array($values));
+ $form->appendChild($repeat);
+
+ $fieldset = new HTML_QuickForm2_Container_Fieldset();
+ $repeat->setPrototype($fieldset);
+
+ $fieldset->addText('foo');
+ $fieldset->addText('bar[baz]');
+
+ $this->assertEquals($values, $repeat->getValue());
+
+ $repeat->setIndexes(array('a', 'c'));
+ unset($values['foo']['b'], $values['bar']['baz']['b']);
+ $this->assertEquals($values, $repeat->getValue());
+ }
+}
+?>
View
16 tests/QuickForm2/ContainerOverloadTest.php
@@ -47,22 +47,6 @@
require_once dirname(dirname(__FILE__)) . '/TestHelper.php';
/**
- * Container class
- */
-require_once 'HTML/QuickForm2/Container.php';
-
-/**
- * Base class for "scalar" elements
- */
-require_once 'HTML/QuickForm2/Element.php';
-
-/**
- * Base class for "checkbox" elements, used in tests
- */
-require_once 'HTML/QuickForm2/Element/InputCheckbox.php';
-
-
-/**
* Unit test for HTML_QuickForm2_Container overloaded methods
*/
class HTML_QuickForm2_ContainerOverloadTest extends PHPUnit_Framework_TestCase
View
20 tests/QuickForm2/ContainerTest.php
@@ -46,26 +46,6 @@
require_once dirname(dirname(__FILE__)) . '/TestHelper.php';
/**
- * Container class
- */
-require_once 'HTML/QuickForm2/Container.php';
-
-/**
- * Base class for "scalar" elements
- */
-require_once 'HTML/QuickForm2/Element.php';
-
-/**
- * Base class for HTML_QuickForm2 rules
- */
-require_once 'HTML/QuickForm2/Rule.php';
-
-/**
- * Base class for HTML_QuickForm2 renderers
- */
-require_once 'HTML/QuickForm2/Renderer.php';
-
-/**
* A non-abstract subclass of Element
*
* Element class is still abstract, we should "implement" the remaining methods.
View
9 tests/QuickForm2/Controller/Action/BackTest.php
@@ -45,15 +45,6 @@
/** Sets up includes */
require_once dirname(dirname(dirname(dirname(__FILE__)))) . '/TestHelper.php';
-/** Class implementing the Page Controller pattern for multipage forms */
-require_once 'HTML/QuickForm2/Controller.php';
-
-/** Class representing a HTML form */
-require_once 'HTML/QuickForm2.php';
-
-/** Action handler for a 'back' button of wizard-type multipage form */
-require_once 'HTML/QuickForm2/Controller/Action/Back.php';
-
/**
* Unit test for HTML_QuickForm2_Controller_Action_Back class
*/
View
9 tests/QuickForm2/Controller/Action/DirectTest.php
@@ -45,15 +45,6 @@
/** Sets up includes */
require_once dirname(dirname(dirname(dirname(__FILE__)))) . '/TestHelper.php';
-/** Class implementing the Page Controller pattern for multipage forms */
-require_once 'HTML/QuickForm2/Controller.php';
-
-/** Class representing a HTML form */
-require_once 'HTML/QuickForm2.php';
-
-/** Action handler for going to a specific page of a multipage form */
-require_once 'HTML/QuickForm2/Controller/Action/Direct.php';
-
/**
* Unit test for HTML_QuickForm2_Controller_Action_Direct class
*/
View
9 tests/QuickForm2/Controller/Action/DisplayTest.php
@@ -45,15 +45,6 @@
/** Sets up includes */
require_once dirname(dirname(dirname(dirname(__FILE__)))) . '/TestHelper.php';
-/** Class implementing the Page Controller pattern for multipage forms */
-require_once 'HTML/QuickForm2/Controller.php';
-
-/** Class representing a HTML form */
-require_once 'HTML/QuickForm2.php';
-
-/** Action handler for outputting the form */
-require_once 'HTML/QuickForm2/Controller/Action/Display.php';
-
/**
* Unit test for HTML_QuickForm2_Controller_Action_Display class
*/
View
9 tests/QuickForm2/Controller/Action/JumpTest.php
@@ -45,15 +45,6 @@
/** Sets up includes */
require_once dirname(dirname(dirname(dirname(__FILE__)))) . '/TestHelper.php';
-/** Class implementing the Page Controller pattern for multipage forms */
-require_once 'HTML/QuickForm2/Controller.php';
-
-/** Class representing a HTML form */
-require_once 'HTML/QuickForm2.php';
-
-/** This handler performs an HTTP redirect to a specific page */
-require_once 'HTML/QuickForm2/Controller/Action/Jump.php';
-
/**
* Unit test for HTML_QuickForm2_Controller_Action_Jump class
*/
View
9 tests/QuickForm2/Controller/Action/NextTest.php
@@ -45,15 +45,6 @@
/** Sets up includes */
require_once dirname(dirname(dirname(dirname(__FILE__)))) . '/TestHelper.php';
-/** Class implementing the Page Controller pattern for multipage forms */
-require_once 'HTML/QuickForm2/Controller.php';
-
-/** Class representing a HTML form */
-require_once 'HTML/QuickForm2.php';
-
-/** Action handler for a 'next' button of wizard-type multipage form */
-require_once 'HTML/QuickForm2/Controller/Action/Next.php';
-
/**
* Unit test for HTML_QuickForm2_Controller_Action_Next class
*/
View
9 tests/QuickForm2/Controller/Action/SubmitTest.php
@@ -45,15 +45,6 @@
/** Sets up includes */
require_once dirname(dirname(dirname(dirname(__FILE__)))) . '/TestHelper.php';
-/** Class implementing the Page Controller pattern for multipage forms */
-require_once 'HTML/QuickForm2/Controller.php';
-
-/** Class representing a HTML form */
-require_once 'HTML/QuickForm2.php';
-
-/** Action handler for a 'submit' button */
-require_once 'HTML/QuickForm2/Controller/Action/Submit.php';
-
/**
* Unit test for HTML_QuickForm2_Controller_Action_Submit class
*/
View
6 tests/QuickForm2/Controller/PageTest.php
@@ -45,12 +45,6 @@
/** Sets up includes */
require_once dirname(dirname(dirname(__FILE__))) . '/TestHelper.php';
-/** Class implementing the Page Controller pattern for multipage forms */
-require_once 'HTML/QuickForm2/Controller.php';
-
-/** Class representing a HTML form */
-require_once 'HTML/QuickForm2.php';
-
/**
* Unit test for HTML_QuickForm2_Controller_Page class
*/
View
9 tests/QuickForm2/ControllerTest.php
@@ -45,15 +45,6 @@
/** Sets up includes */
require_once dirname(dirname(__FILE__)) . '/TestHelper.php';
-/** Class implementing the Page Controller pattern for multipage forms */
-require_once 'HTML/QuickForm2/Controller.php';
-
-/** Interface for Controller action handlers */
-require_once 'HTML/QuickForm2/Controller/Action.php';
-
-/** Class representing a HTML form */
-require_once 'HTML/QuickForm2.php';
-
/**
* Unit test for HTML_QuickForm2_Controller class
*/
View
5 tests/QuickForm2/DataSource/ArrayTest.php
@@ -46,11 +46,6 @@
require_once dirname(dirname(dirname(__FILE__))) . '/TestHelper.php';
/**
- * Array-based data source for HTML_QuickForm2 objects
- */
-require_once 'HTML/QuickForm2/DataSource/Array.php';
-
-/**
* Unit test for array-based data source
*/
class HTML_QuickForm2_DataSource_ArrayTest extends PHPUnit_Framework_TestCase
View
5 tests/QuickForm2/DataSource/SuperGlobalTest.php
@@ -46,11 +46,6 @@
require_once dirname(dirname(dirname(__FILE__))) . '/TestHelper.php';
/**
- * Data source for HTML_QuickForm2 objects based on superglobal arrays
- */
-require_once 'HTML/QuickForm2/DataSource/SuperGlobal.php';
-
-/**
* Unit test for superglobal-based data source
*/
class HTML_QuickForm2_DataSource_SuperGlobalTest extends PHPUnit_Framework_TestCase
View
2  tests/QuickForm2/Element/AllTests.php
@@ -62,6 +62,7 @@
require_once dirname(__FILE__) . '/InputFileTest.php';
require_once dirname(__FILE__) . '/StaticTest.php';
require_once dirname(__FILE__) . '/DateTest.php';
+require_once dirname(__FILE__) . '/HierselectTest.php';
class QuickForm2_Element_AllTests
{
@@ -92,6 +93,7 @@ public static function suite()
$suite->addTestSuite('HTML_QuickForm2_Element_InputFileTest');
$suite->addTestSuite('HTML_QuickForm2_Element_StaticTest');
$suite->addTestSuite('HTML_QuickForm2_Element_DateTest');
+ $suite->addTestSuite('HTML_QuickForm2_Element_HierselectTest');
return $suite;
}
View
10 tests/QuickForm2/Element/ButtonTest.php
@@ -46,16 +46,6 @@
require_once dirname(dirname(dirname(__FILE__))) . '/TestHelper.php';
/**
- * Class for <button> elements
- */
-require_once 'HTML/QuickForm2/Element/Button.php';
-
-/**
- * Class representing a HTML form
- */
-require_once 'HTML/QuickForm2.php';
-
-/**
* Unit test for HTML_QuickForm2_Element_Button class
*/
class HTML_QuickForm2_Element_ButtonTest extends PHPUnit_Framework_TestCase
View
3  tests/QuickForm2/Element/DateTest.php
@@ -45,9 +45,6 @@
/** Sets up includes */
require_once dirname(dirname(dirname(__FILE__))) . '/TestHelper.php';
-/** Date element */
-require_once 'HTML/QuickForm2/Element/Date.php';
-
class HTML_QuickForm2_Element_DateTest extends PHPUnit_Framework_TestCase
{
/**
View
73 tests/QuickForm2/Element/HierselectTest.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * Unit tests for HTML_QuickForm2 package
+ *
+ * PHP version 5
+ *
+ * LICENSE:
+ *
+ * Copyright (c) 2006-2012, Alexey Borzov <avb@php.net>,
+ * Bertrand Mansion <golgote@mamasam.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * The names of the authors may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @category HTML
+ * @package HTML_QuickForm2
+ * @author Alexey Borzov <avb@php.net>
+ * @license http://opensource.org/licenses/bsd-license.php New BSD License
+ * @version SVN: $Id$
+ * @link http://pear.php.net/package/HTML_QuickForm2
+ */
+
+/** Sets up includes */
+require_once dirname(dirname(dirname(__FILE__))) . '/TestHelper.php';
+
+/**
+ * Unit test for HTML_QuickForm2_Element_Hierselect class
+ */
+class HTML_QuickForm2_Element_HierselectTest extends PHPUnit_Framework_TestCase
+{
+ public function testUpdateValueOnNameChange()
+ {
+ $primary = array(1 => 'one', 2 => 'two');
+ $secondary = array(
+ 1 => array(11 => 'one-one', 12 => 'one-two'),
+ 2 => array(21 => 'two-one', 22 => 'two-two')
+ );
+
+ $form = new HTML_QuickForm2('testHierselectForm');
+ $form->addDataSource(new HTML_QuickForm2_DataSource_Array(array(
+ 'foo' => array(1, 12),
+ 'bar' => array(2, 21)
+ )));
+ $hs = $form->addHierselect('foo')->loadOptions(array($primary, $secondary));
+ $this->assertEquals(array(1, 12), $hs->getValue());
+
+ $hs->setName('bar');
+ $this->assertEquals(array(2, 21), $hs->getValue());
+ }
+}
+?>
View
5 tests/QuickForm2/Element/InputButtonTest.php
@@ -46,11 +46,6 @@
require_once dirname(dirname(dirname(__FILE__))) . '/TestHelper.php';
/**
- * Class for <input type="button" /> elements
- */
-require_once 'HTML/QuickForm2/Element/InputButton.php';
-
-/**
* Unit test for HTML_QuickForm2_Element_InputButton class
*/
class HTML_QuickForm2_Element_InputButtonTest extends PHPUnit_Framework_TestCase
View
15 tests/QuickForm2/Element/InputCheckableTest.php
@@ -46,21 +46,6 @@
require_once dirname(dirname(dirname(__FILE__))) . '/TestHelper.php';
/**
- * Base class for radios and checkboxes
- */
-require_once 'HTML/QuickForm2/Element/InputCheckable.php';
-
-/**
- * Array-based data source for HTML_QuickForm2