/system/library/PEAR/HTML/QuickForm2/Container/Repeat.php
PHP | 725 lines | 342 code | 46 blank | 337 comment | 51 complexity | f306f7b68476f84f96d93d55c5b60de4 MD5 | raw file
Possible License(s): BSD-2-Clause
- <?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: Repeat.php 325694 2012-05-15 07:46:13Z avb $
- * @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: 2.0.0
- * @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: 2.0.0
- * @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');
- }
- /**
- * Class constructor
- *
- * Repeat element can understand the following keys in $data parameter:
- * - 'prototype': a Container to be repeated. Passed to {@link setPrototype()}.
- *
- * @param string $name Element name
- * @param string|array $attributes Attributes (either a string or an array)
- * @param array $data Additional element data
- */
- public function __construct($name = null, $attributes = null, array $data = array())
- {
- if (!empty($data['prototype'])) {
- $this->setPrototype($data['prototype']);
- }
- unset($data['prototype']);
- parent::__construct($name, $attributes, $data);
- }
- /**
- * 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
- *
- * @return HTML_QuickForm2_Container_Repeat
- */
- public function setIndexField($field)
- {
- $this->indexField = $field;
- $this->updateValue();
- return $this;
- }
- /**
- * 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
- *
- * @return HTML_QuickForm2_Container_Repeat
- */
- 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);
- return $this;
- }
- /**
- * 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()
- * @throws HTML_QuickForm2_Exception
- */
- 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[spl_object_hash($child)][$index] = $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.elements.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[$hash = spl_object_hash($child)])
- && isset($this->childErrors[$hash][$index])
- ) {
- $child->setError($this->childErrors[$hash][$index]);
- }
- }
- $this->getPrototype()->render($renderer);
- }
- $this->restoreChildAttributes($backup);
- // only add javascript if not frozen
- if (!$this->toggleFrozen()) {
- $jsBuilder->addLibrary('repeat', 'quickform-repeat.js');
- $jsBuilder->addElementJavascript($this->_generateInitScript($evalBuilder));
- $this->renderClientRules($jsBuilder);
- }
- $renderer->finishContainer($this);
- $renderer->setOption('group_hiddens', $hiddens);
- return $renderer;
- }
- }
- ?>