Autodoc
  • Namespace
  • Class
  • Tree

Namespaces

  • BlueTihi
    • Context
  • Brickrouge
    • Element
      • Nodes
    • Renderer
    • Widget
  • ICanBoogie
    • ActiveRecord
    • AutoConfig
    • CLDR
    • Composer
    • Core
    • Event
    • Exception
    • HTTP
      • Dispatcher
      • Request
    • I18n
      • Translator
    • Mailer
    • Modules
      • Taxonomy
        • Support
      • Thumbnailer
        • Versions
    • Object
    • Operation
      • Dispatcher
    • Prototype
    • Routes
    • Routing
      • Dispatcher
    • Session
  • Icybee
    • ActiveRecord
      • Model
    • ConfigOperation
    • Document
    • EditBlock
    • Element
      • ActionbarContextual
      • ActionbarSearch
      • ActionbarToolbar
    • FormBlock
    • Installer
    • ManageBlock
    • Modules
      • Articles
      • Cache
        • Collection
        • ManageBlock
      • Comments
        • ManageBlock
      • Contents
        • ManageBlock
      • Dashboard
      • Editor
        • Collection
      • Files
        • File
        • ManageBlock
      • Forms
        • Form
        • ManageBlock
      • I18n
      • Images
        • ManageBlock
      • Members
      • Modules
        • ManageBlock
      • Nodes
        • ManageBlock
        • Module
      • Pages
        • BreadcrumbElement
        • LanguagesElement
        • ManageBlock
        • NavigationBranchElement
        • NavigationElement
        • Page
        • PageController
      • Registry
      • Search
      • Seo
      • Sites
        • ManageBlock
      • Taxonomy
        • Terms
          • ManageBlock
        • Vocabulary
          • ManageBlock
      • Users
        • ManageBlock
        • NonceLogin
        • Roles
      • Views
        • ActiveRecordProvider
        • Collection
        • View
    • Operation
      • ActiveRecord
      • Constructor
      • Module
      • Widget
    • Rendering
  • None
  • Patron
  • PHP

Classes

  • A
  • Actions
  • Alert
  • AlterCSSClassNamesEvent
  • AssetsCollector
  • Button
  • CSSCollector
  • Dataset
  • Date
  • DateRange
  • DateTime
  • Decorator
  • Document
  • DropdownMenu
  • Element
  • File
  • Form
  • Group
  • Helpers
  • HTMLString
  • Iterator
  • JSCollector
  • ListView
  • ListViewColumn
  • Modal
  • Pager
  • Popover
  • PopoverWidget
  • Ranger
  • RecursiveIterator
  • Salutation
  • Searchbox
  • Section
  • SplitButton
  • Text
  • Widget

Interfaces

  • CSSClassNames
  • DecoratorInterface
  • HTMLStringInterface
  • Validator

Traits

  • CSSClassNamesProperty

Exceptions

  • ElementIsEmpty

Functions

  • _array_flatten_callback
  • array_flatten
  • array_insert
  • check_session
  • dump
  • escape
  • escape_all
  • format
  • format_size
  • get_accessible_file
  • get_document
  • normalize
  • render_css_class
  • render_exception
  • retrieve_form
  • retrieve_form_errors
  • shorten
  • stable_sort
  • store_form
  • store_form_errors
  • strip
  • t
   1 <?php
   2 
   3 /*
   4  * This file is part of the Brickrouge package.
   5  *
   6  * (c) Olivier Laviale <olivier.laviale@gmail.com>
   7  *
   8  * For the full copyright and license information, please view the LICENSE
   9  * file that was distributed with this source code.
  10  */
  11 
  12 namespace Brickrouge;
  13 
  14 /**
  15  * An HTML element.
  16  *
  17  * The `Element` class can create any kind of HTML element. It supports class names, dataset,
  18  * children. It handles values and default values. It can decorate the HTML element with a label,
  19  * a legend and a description.
  20  *
  21  * This is the base class to all element types.
  22  *
  23  * @property string $class Assigns a class name or set of class names to an element. Any number of
  24  * elements may be assigned the same class name or names. Multiple class names must be separated
  25  * by white space characters.
  26  *
  27  * @property Dataset $dataset The dataset property provides a convenient mapping to the
  28  * data-* attributes on an element.
  29  *
  30  * @property string $id Assigns an identifier to an element. This identifier mush be unique in
  31  * a document.
  32  *
  33  * @see http://dev.w3.org/html5/spec/Overview.html#embedding-custom-non-visible-data-with-the-data-attributes
  34  */
  35 class Element extends \ICanBoogie\Object implements \ArrayAccess, \IteratorAggregate, HTMLStringInterface
  36 {
  37     #
  38     # special elements
  39     #
  40 
  41     /**
  42      * Custom type used to create checkbox elements.
  43      *
  44      * @var string
  45      */
  46     const TYPE_CHECKBOX = '#checkbox';
  47 
  48     /**
  49      * Custom type used to create checkbox group elements.
  50      *
  51      * @var string
  52      */
  53     const TYPE_CHECKBOX_GROUP = '#checkbox-group';
  54 
  55     /**
  56      * Custom type used to create radio elements.
  57      *
  58      * @var string
  59      */
  60     const TYPE_RADIO = '#radio';
  61 
  62     /**
  63      * Custom type used to create radio group elements.
  64      *
  65      * @var string
  66      */
  67     const TYPE_RADIO_GROUP = '#radio-group';
  68 
  69     #
  70     # special tags
  71     #
  72 
  73     /**
  74      * Used to define the children of an element.
  75      *
  76      * @var string
  77      */
  78     const CHILDREN = '#children';
  79 
  80     /**
  81      * Used to define the default value of an element.
  82      *
  83      * The default value is added to the dataset as 'default-value'.
  84      *
  85      * @var string
  86      */
  87     const DEFAULT_VALUE = '#default-value';
  88 
  89     /**
  90      * Used to define the description block of an element.
  91      *
  92      * @var string
  93      *
  94      * @see Element::decorate_with_description()
  95      */
  96     const DESCRIPTION = '#description';
  97 
  98     /**
  99      * Used to define the group of an element.
 100      *
 101      * @var string
 102      */
 103     const GROUP = '#group';
 104 
 105     /**
 106      * Used to define the groups that can be used by children elements.
 107      *
 108      * @var string
 109      */
 110     const GROUPS = '#groups';
 111 
 112     /**
 113      * Used to define the inline help of an element.
 114      *
 115      * @var string
 116      *
 117      * @see Element::decorate_with_inline_help()
 118      */
 119     const INLINE_HELP = '#inline-help';
 120 
 121     /**
 122      * Used to define the inner HTML of an element. If the value of the tag is null, the markup
 123      * will be self-closing.
 124      *
 125      * @var string
 126      */
 127     const INNER_HTML = '#inner-html';
 128 
 129     /**
 130      * Used to define the label of an element.
 131      *
 132      * @var string
 133      *
 134      * @see Element::decorate_with_label()
 135      */
 136     const LABEL = '#label';
 137 
 138     /**
 139      * Used to define the position of the label. Possible positions are "before", "after" and
 140      * "above". Defaults to "after".
 141      *
 142      * @var string
 143      */
 144     const LABEL_POSITION = '#label-position';
 145     const LABEL_MISSING = '#label-missing';
 146 
 147     /**
 148      * Used to define the legend of an element. If the legend is defined the element is wrapped
 149      * into a fieldset when it is rendered.
 150      *
 151      * @var string
 152      *
 153      * @see Element::decorate_with_legend()
 154      */
 155     const LEGEND = '#element-legend';
 156 
 157     /**
 158      * Used to define the required state of an element.
 159      *
 160      * @var string
 161      *
 162      * @see Form::validate()
 163      * @see http://dev.w3.org/html5/spec/Overview.html#the-required-attribute
 164      */
 165     const REQUIRED = 'required';
 166 
 167     /**
 168      * Used to define the options of the following element types: "select",
 169      * {@link TYPE_RADIO_GROUP} and {@link TYPE_CHECKBOX_GROUP}.
 170      *
 171      * @var string
 172      */
 173     const OPTIONS = '#options';
 174 
 175     /**
 176      * Used to define which options are disabled.
 177      *
 178      * @var string
 179      */
 180     const OPTIONS_DISABLED = '#options-disabled';
 181 
 182     /**
 183      * Used to define the state of the element: "success", "warning" or "error".
 184      *
 185      * @var string
 186      */
 187     const STATE = '#state';
 188 
 189     /**
 190      * Used to define the validator of an element. The validator is defined using an array made of
 191      * a callback and a possible userdata array.
 192      *
 193      * @var string
 194      */
 195     const VALIDATOR = '#validator';
 196     const VALIDATOR_OPTIONS = '#validator-options';
 197 
 198     /**
 199      * Use to define the weight of an element. This attribute can be used to reorder children when
 200      * a parent element is rendered.
 201      *
 202      * @var string
 203      *
 204      * @see Element::get_ordered_children()
 205      */
 206     const WEIGHT = '#weight';
 207 
 208     /**
 209      * The name of the Javascript constructor that should be used to construct the widget.
 210      *
 211      * @var string
 212      */
 213     const WIDGET_CONSTRUCTOR = '#widget-constructor';
 214 
 215     static private $inputs = array('button', 'form', 'input', 'option', 'select', 'textarea');
 216     static private $has_attribute_disabled = array('button', 'input', 'optgroup', 'option', 'select', 'textarea');
 217     static private $has_attribute_value = array('button', 'input', 'option');
 218     static private $has_attribute_required = array('input', 'select', 'textarea');
 219     static private $handled_assets = array();
 220 
 221     static protected function handle_assets()
 222     {
 223         $class = get_called_class();
 224 
 225         if (isset(self::$handled_assets[$class]))
 226         {
 227             return;
 228         }
 229 
 230         self::$handled_assets[$class] = true;
 231 
 232         static::add_assets(get_document());
 233     }
 234 
 235     /**
 236      * Adds assets to the document.
 237      *
 238      * @param Document $document
 239      */
 240     static protected function add_assets(Document $document)
 241     {
 242 
 243     }
 244 
 245     /**
 246      * Next available auto element id index.
 247      *
 248      * @var int
 249      */
 250     static protected $auto_element_id = 1;
 251 
 252     /**
 253      * Returns a unique element id string.
 254      *
 255      * @return string
 256      */
 257     static public function auto_element_id()
 258     {
 259         return 'autoid--' . self::$auto_element_id++;
 260     }
 261 
 262     /**
 263      * Type if the element, as provided during {@link __construct()}.
 264      *
 265      * @var string
 266      */
 267     public $type;
 268 
 269     /**
 270      * Tag name of the rendered HTML element.
 271      *
 272      * @var string
 273      */
 274     public $tag_name;
 275 
 276     /**
 277      * An array containing the children of the element.
 278      *
 279      * @var array
 280      */
 281     public $children = array();
 282 
 283     /**
 284      * Tags of the element, including HTML and special attributes.
 285      *
 286      * @var array[string]mixed
 287      */
 288     protected $tags = array();
 289 
 290     /**
 291      * Inner HTML of the element.
 292      *
 293      * @var string|null
 294      *
 295      * @see Element::render_inner_html()
 296      */
 297     protected $inner_html;
 298 
 299     /**
 300      * @param string $type Type of the element, it can be one of the custom types (`TYPE_*`) or
 301      * any HTML type.
 302      *
 303      * @param array $attributes HTML and custom attributes.
 304      */
 305     public function __construct($type, $attributes=array())
 306     {
 307         $this->type = $type;
 308 
 309         #
 310         # children first
 311         #
 312 
 313         if (!empty($attributes[self::CHILDREN]))
 314         {
 315             $this->children = array();
 316             $this->adopt($attributes[self::CHILDREN]);
 317 
 318             unset($attributes[self::CHILDREN]);
 319         }
 320 
 321         #
 322         # prepare special elements
 323         #
 324 
 325         switch ((string) $type)
 326         {
 327             case self::TYPE_CHECKBOX:
 328             case self::TYPE_RADIO:
 329             {
 330                 static $translate = array
 331                 (
 332                     self::TYPE_CHECKBOX => array('input', 'checkbox'),
 333                     self::TYPE_RADIO => array('input', 'radio'),
 334                 );
 335 
 336                 $this->tag_name = $translate[$type][0];
 337                 $attributes['type'] = $translate[$type][1];
 338             }
 339             break;
 340 
 341             case self::TYPE_CHECKBOX_GROUP:
 342             {
 343                 $this->tag_name = 'div';
 344             }
 345             break;
 346 
 347             case self::TYPE_RADIO_GROUP:
 348             {
 349                 $this->tag_name = 'div';
 350             }
 351             break;
 352 
 353             case 'textarea':
 354             {
 355                 $this->tag_name = 'textarea';
 356 
 357                 $attributes += array('rows' => 10, 'cols' => 76);
 358             }
 359             break;
 360 
 361             default:
 362             {
 363                 $this->tag_name = $type;
 364             }
 365             break;
 366         }
 367 
 368         foreach ($attributes as $attribute => $value)
 369         {
 370             $this[$attribute] = $value;
 371         }
 372 
 373         switch ((string) $this->type)
 374         {
 375             case self::TYPE_CHECKBOX_GROUP: $this->add_class('checkbox-group'); break;
 376             case self::TYPE_RADIO_GROUP: $this->add_class('radio-group'); break;
 377         }
 378     }
 379 
 380     /**
 381      * Checks is an attribute is set.
 382      *
 383      * @param string $attribute
 384      *
 385      * @return bool
 386      */
 387     public function offsetExists($attribute)
 388     {
 389         return isset($this->tags[$attribute]);
 390     }
 391 
 392     /**
 393      * Returns the value of an attribute.
 394      *
 395      * @param string $attribute
 396      * @param null $default The default value of the attribute.
 397      *
 398      * @return mixed|null The value of the attribute, or null if is not set.
 399      */
 400     public function offsetGet($attribute, $default=null)
 401     {
 402         return isset($this->tags[$attribute]) ? $this->tags[$attribute] : $default;
 403     }
 404 
 405     /**
 406      * Sets the value of an attribute.
 407      *
 408      * @param string $attribute The attribute.
 409      * @param mixed $value The value of the attribute.
 410      */
 411     public function offsetSet($attribute, $value)
 412     {
 413         switch ($attribute)
 414         {
 415             case self::CHILDREN:
 416             {
 417                 $this->children = array();
 418                 $this->adopt($value);
 419             }
 420             break;
 421 
 422             case self::INNER_HTML:
 423             {
 424                 $this->inner_html = $value;
 425             }
 426             break;
 427 
 428             case 'class':
 429             {
 430                 $this->class = $value;
 431             }
 432             break;
 433 
 434             case 'id':
 435             {
 436                 unset($this->id);
 437             }
 438             break;
 439         }
 440 
 441         $this->tags[$attribute] = $value;
 442     }
 443 
 444     /**
 445      * Removes an attribute.
 446      *
 447      * @param string $attribute The name of the attribute.
 448      */
 449     public function offsetUnset($attribute)
 450     {
 451         unset($this->tags[$attribute]);
 452     }
 453 
 454     /**
 455      * Returns an iterator.
 456      *
 457      * @return Iterator
 458      */
 459     public function getIterator()
 460     {
 461         return new Iterator($this);
 462     }
 463 
 464     private $_dataset;
 465 
 466     /**
 467      * Returns the {@link Dataset} of the element.
 468      *
 469      * @return Dataset
 470      */
 471     protected function get_dataset()
 472     {
 473         if (!$this->_dataset)
 474         {
 475             $this->_dataset = new Dataset($this);
 476         }
 477 
 478         return $this->_dataset;
 479     }
 480 
 481     /**
 482      * Sets the datset of the element.
 483      *
 484      * @param array|Dataset $properties
 485      *
 486      * @return Dataset
 487      */
 488     protected function set_dataset($dataset)
 489     {
 490         if (!($dataset instanceof Dataset))
 491         {
 492             $dataset = new Dataset($this, $dataset);
 493         }
 494 
 495         $this->_dataset = $dataset;
 496     }
 497 
 498     protected function get_attributes()
 499     {
 500         return $this->tags;
 501     }
 502 
 503     /**
 504      * Returns the element's id.
 505      *
 506      * If the element's id is empty, a unique id is generated and added to its tags.
 507      *
 508      * @return string
 509      */
 510     protected function lazy_get_id()
 511     {
 512         $id = $this['id'];
 513 
 514         if (!$id)
 515         {
 516             $name = $this['name'];
 517 
 518             if ($name)
 519             {
 520                 $id = 'autoid--' . normalize($name);
 521             }
 522             else
 523             {
 524                 $id = self::auto_element_id();
 525             }
 526 
 527             $this['id'] = $id;
 528         }
 529 
 530         return $id;
 531     }
 532 
 533     /**
 534      * Class names used to compose the value of the `class` attribute.
 535      *
 536      * @var array
 537      */
 538     protected $class_names = array();
 539 
 540     /**
 541      * Returns the value of the "class" attribute.
 542      *
 543      * @return string
 544      */
 545     protected function get_class()
 546     {
 547         $class_names = $this->alter_class_names($this->class_names);
 548 
 549         return $this->render_class($class_names);
 550     }
 551 
 552     /**
 553      * Sets the value of the "class" attribute.
 554      *
 555      * @param string $class
 556      */
 557     protected function set_class($class)
 558     {
 559         $names = explode(' ', trim($class));
 560         $names = array_map('trim', $names);
 561 
 562         $this->class_names = array_combine($names, array_fill(0, count($names), true));
 563     }
 564 
 565     /**
 566      * Adds a class name to the "class" attribute.
 567      *
 568      * @param $class
 569      *
 570      * @return Element
 571      */
 572     public function add_class($class)
 573     {
 574         $this->class_names[$class] = true;
 575 
 576         return $this;
 577     }
 578 
 579     /**
 580      * Removes a class name from the `class` attribute.
 581      *
 582      * @param $class
 583      *
 584      * @return Element
 585      */
 586     public function remove_class($class)
 587     {
 588         unset($this->class_names[$class]);
 589 
 590         return $this;
 591     }
 592 
 593     /**
 594      * Checks if a class name is defined in the `class` attribute.
 595      *
 596      * @param string $class_name
 597      *
 598      * @return boolean true if the element has the class name, false otherwise.
 599      */
 600     public function has_class($class_name)
 601     {
 602         return isset($this->class_names[$class_name]);
 603     }
 604 
 605     /**
 606      * Alters the class names.
 607      *
 608      * This method is invoked before the class names are rendered.
 609      *
 610      * @param array $class_names
 611      *
 612      * @return array
 613      */
 614     protected function alter_class_names(array $class_names)
 615     {
 616         return $class_names;
 617     }
 618 
 619     /**
 620      * Renders the `class` attribute value.
 621      *
 622      * @param array $class_names An array of class names. Each key/value pair describe a class
 623      * name. The key is the identifier of the class name, the value is its value. If the value is
 624      * empty then the class name is discarted. If the value is `true` the identifier of the class
 625      * name is used as value.
 626      *
 627      * @return string
 628      */
 629     protected function render_class(array $class_names)
 630     {
 631         $class = '';
 632         $class_names = array_filter($class_names);
 633 
 634         foreach ($class_names as $name => $value)
 635         {
 636             if ($value === true)
 637             {
 638                 $value = $name;
 639             }
 640 
 641             $class .= ' ' . $value;
 642         }
 643 
 644         return substr($class, 1);
 645     }
 646 
 647     /**
 648      * Add a child or children to the element.
 649      *
 650      * If the children are provided in an array, each key/value pair defines the name of a child
 651      * and the child itself. If the key is not numeric it is considered as the child's name and is
 652      * used to set its `name` attribute, unless the attribute is already defined.
 653      *
 654      * @param string|Element|array $child The child or children to add.
 655      * @param string|Element $other[optional] Other child.
 656      */
 657     public function adopt($child, $other=null)
 658     {
 659         if (func_num_args() > 1)
 660         {
 661             $child = func_get_args();
 662         }
 663 
 664         if (is_array($child))
 665         {
 666             $children = $child;
 667 
 668             foreach($children as $name => $child)
 669             {
 670                 if (is_numeric($name))
 671                 {
 672                     $this->children[] = $child;
 673                 }
 674                 else
 675                 {
 676                     if ($child instanceof self && $child['name'] === null)
 677                     {
 678                         $child['name'] = $name;
 679                     }
 680 
 681                     $this->children[$name] = $child;
 682                 }
 683             }
 684         }
 685         else
 686         {
 687             $this->children[] = $child;
 688         }
 689     }
 690 
 691     /**
 692      * Returns the children of the element ordered according to their weight.
 693      *
 694      * @return array[int]Element|string
 695      */
 696     public function get_ordered_children()
 697     {
 698         if (!$this->children)
 699         {
 700             return array();
 701         }
 702 
 703         $by_weight = array();
 704         $with_relative_positions = array();
 705 
 706         foreach ($this->children as $name => $child)
 707         {
 708             $weight = is_object($child) ? $child[self::WEIGHT] : 0;
 709 
 710             if (is_string($weight) && !is_numeric($weight)) // FIXME: is is_numeric() not enough ?
 711             {
 712                 $with_relative_positions[] = $child;
 713 
 714                 continue;
 715             }
 716 
 717             $by_weight[(int) $weight][$name] = $child;
 718         }
 719 
 720         if (count($by_weight) == 1 && !$with_relative_positions)
 721         {
 722             return $this->children;
 723         }
 724 
 725         ksort($by_weight);
 726 
 727         $rc = array();
 728 
 729         foreach ($by_weight as $children)
 730         {
 731             $rc += $children;
 732         }
 733 
 734         #
 735         # now we deal with the relative positions
 736         #
 737 
 738         if ($with_relative_positions)
 739         {
 740             foreach ($with_relative_positions as $child)
 741             {
 742                 list($target, $position) = explode(':', $child[self::WEIGHT]) + array(1 => 'after');
 743 
 744                 $rc = array_insert($rc, $target, $child, $child['name'], $position == 'after');
 745             }
 746         }
 747 
 748         return $rc;
 749     }
 750 
 751     /**
 752      * Returns the HTML representation of a child element.
 753      *
 754      * @param Element|string $child
 755      *
 756      * @return string
 757      */
 758     protected function render_child($child)
 759     {
 760         return (string) $child;
 761     }
 762 
 763     /**
 764      * Renders the children of the element into a HTML string.
 765      *
 766      * @param array $children
 767      *
 768      * @return string
 769      */
 770     protected function render_children(array $children)
 771     {
 772         $html = '';
 773 
 774         foreach ($children as $child)
 775         {
 776             $html .= $this->render_child($child);
 777         }
 778 
 779         return $html;
 780     }
 781 
 782     /**
 783      * Returns the HTML representation of the element's content.
 784      *
 785      * The children of the element are ordered before they are rendered using the
 786      * {@link render_children()} method.
 787      *
 788      * According to their types, the following methods can be invoked to render the inner HTML
 789      * of elements:
 790      *
 791      * - {@link render_inner_html_for_select}
 792      * - {@link render_inner_html_for_textarea}
 793      * - {@link render_inner_html_for_checkbox_group}
 794      * - {@link render_inner_html_for_radio_group}
 795      *
 796      * @return string|null The content of the element. The element is to be considered
 797      * _self-closing_ if `null` is returned.
 798      */
 799     protected function render_inner_html()
 800     {
 801         $html = null;
 802 
 803         switch ($this->type)
 804         {
 805             case 'select': $html = $this->render_inner_html_for_select(); break;
 806             case 'textarea': $html = $this->render_inner_html_for_textarea(); break;
 807             case self::TYPE_CHECKBOX_GROUP: $html = $this->render_inner_html_for_checkbox_group(); break;
 808             case self::TYPE_RADIO_GROUP: $html = $this->render_inner_html_for_radio_group(); break;
 809         }
 810 
 811         $children = $this->get_ordered_children();
 812 
 813         if ($children)
 814         {
 815             $html .= $this->render_children($children);
 816         }
 817         else if ($this->inner_html !== null)
 818         {
 819             $html = $this->inner_html;
 820         }
 821 
 822         return $html;
 823     }
 824 
 825     /**
 826      * Renders inner HTML of `SELECT` elements.
 827      *
 828      * @return string
 829      */
 830     protected function render_inner_html_for_select()
 831     {
 832         #
 833         # get the name and selected value for our children
 834         #
 835 
 836         $selected = $this['value'];
 837 
 838         if ($selected === null)
 839         {
 840             $selected = $this[self::DEFAULT_VALUE];
 841         }
 842 
 843         #
 844         # this is the 'template' child
 845         #
 846 
 847         $dummy_option = new Element('option');
 848 
 849         #
 850         # create the inner content of our element
 851         #
 852 
 853         $html = '';
 854 
 855         $options = $this[self::OPTIONS] ?: array();
 856         $disabled = $this[self::OPTIONS_DISABLED];
 857 
 858         foreach ($options as $value => $label)
 859         {
 860             if ($label instanceof self)
 861             {
 862                 $option = $label;
 863             }
 864             else
 865             {
 866                 $option = $dummy_option;
 867 
 868                 if ($label || is_numeric($label))
 869                 {
 870                     $label = escape($label);
 871                 }
 872                 else
 873                 {
 874                     $label = '&nbsp;';
 875                 }
 876 
 877                 $option->inner_html = $label;
 878             }
 879 
 880             #
 881             # value is casted to a string so that we can handle null value and compare '0' with 0
 882             #
 883 
 884             $option['value'] = $value;
 885             $option['selected'] = (string) $value === (string) $selected;
 886             $option['disabled'] = !empty($disabled[$value]);
 887 
 888             $html .= $option;
 889         }
 890 
 891         return $html;
 892     }
 893 
 894     /**
 895      * Renders the inner HTML of `TEXTAREA` elements.
 896      *
 897      * @return string
 898      */
 899     protected function render_inner_html_for_textarea()
 900     {
 901         $value = $this['value'];
 902 
 903         if ($value === null)
 904         {
 905             $value = $this[self::DEFAULT_VALUE];
 906         }
 907 
 908         return escape($value);
 909     }
 910 
 911     /**
 912      * Renders inner HTML of {@link TYPE_CHECKBOX_GROUP} custom elements.
 913      *
 914      * @return string
 915      */
 916     protected function render_inner_html_for_checkbox_group()
 917     {
 918         #
 919         # get the name and selected value for our children
 920         #
 921 
 922         $name = $this['name'];
 923         $selected = (array) $this['value'] ?: $this[self::DEFAULT_VALUE];
 924         $disabled = $this['disabled'] ?: false;
 925         $readonly = $this['readonly'] ?: false;
 926 
 927         #
 928         # this is the 'template' child
 929         #
 930 
 931         $child = new Element
 932         (
 933             'input', array
 934             (
 935                 'type' => 'checkbox',
 936                 'readonly' => $readonly
 937             )
 938         );
 939 
 940         #
 941         # create the inner content of our element
 942         #
 943 
 944         $html = '';
 945         $disableds = $this[self::OPTIONS_DISABLED];
 946 
 947         foreach ($this[self::OPTIONS] as $option_name => $label)
 948         {
 949             $child[self::LABEL] = $label;
 950             $child['name'] = $name . '[' . $option_name . ']';
 951             $child['checked'] = !empty($selected[$option_name]);
 952             $child['disabled'] = $disabled || !empty($disableds[$option_name]);
 953             $child['data-key'] = $option_name;
 954             $child['data-name'] = $name;
 955 
 956             $html .= $child;
 957         }
 958 
 959         return $html;
 960     }
 961 
 962     /**
 963      * Renders inner HTML of {@link TYPE_RADIO_GROUP} custom elements.
 964      *
 965      * @return string
 966      */
 967     protected function render_inner_html_for_radio_group()
 968     {
 969         #
 970         # get the name and selected value for our children
 971         #
 972 
 973         $name = $this['name'];
 974         $selected = $this['value'];
 975 
 976         if ($selected === null)
 977         {
 978             $selected = $this[self::DEFAULT_VALUE];
 979         }
 980 
 981         $disabled = $this['disabled'] ?: false;
 982         $readonly = $this['readonly'] ?: false;
 983 
 984         #
 985         # this is the 'template' child
 986         #
 987 
 988         $child = new Element
 989         (
 990             'input', array
 991             (
 992                 'type' => 'radio',
 993                 'name' => $name,
 994                 'readonly' => $readonly
 995             )
 996         );
 997 
 998         #
 999         # create the inner content of our element
1000         #
1001         # add our options as children
1002         #
1003 
1004         $html = '';
1005         $disableds = $this[self::OPTIONS_DISABLED];
1006 
1007         foreach ($this[self::OPTIONS] as $value => $label)
1008         {
1009             if ($label && !is_object($label) && $label{0} == '.')
1010             {
1011                 $label = t(substr($label, 1), array(), array('scope' => 'element.option'));
1012             }
1013 
1014             $child[self::LABEL] = $label;
1015             $child['value'] = $value;
1016             $child['checked'] = (string) $value === (string) $selected;
1017             $child['disabled'] = $disabled || !empty($disableds[$value]);
1018 
1019             $html .= $child;
1020         }
1021 
1022         return $html;
1023     }
1024 
1025     /**
1026      * Alters the provided attributes.
1027      *
1028      * - The `value`, `required`, `disabled` and `name` attributes are discarded if they are not
1029      * supported by the element type.
1030      *
1031      * - The `title` attribute is translated within the scope `element.title`.
1032      *
1033      * - The `checked` attribute of elements of type {@link TYPE_CHECKBOX} is set to `true` if
1034      * their {@link DEFAULT_VALUE} attribute is not empty and their `checked` attribute is not
1035      * defined (`null`).
1036      *
1037      * - The `value` attribute of `INPUT` and `BUTTON` elements is altered if the
1038      * {@link DEFAULT_VALUE} attribute is defined and the `value` attribute is not (`null`).
1039      *
1040      * @param array $attributes
1041      *
1042      * @return array The altered attributes.
1043      */
1044     protected function alter_attributes(array $attributes)
1045     {
1046         $tag_name = $this->tag_name;
1047 
1048         foreach ($attributes as $attribute => $value)
1049         {
1050             if ($attribute == 'value' && !in_array($tag_name, self::$has_attribute_value))
1051             {
1052                 unset($attributes[$attribute]);
1053 
1054                 continue;
1055             }
1056 
1057             if ($attribute == 'required' && !in_array($tag_name, self::$has_attribute_required))
1058             {
1059                 unset($attributes[$attribute]);
1060 
1061                 continue;
1062             }
1063 
1064             if ($attribute == 'disabled' && !in_array($tag_name, self::$has_attribute_disabled))
1065             {
1066                 unset($attributes[$attribute]);
1067 
1068                 continue;
1069             }
1070 
1071             if ($attribute == 'name' && !in_array($tag_name, self::$inputs))
1072             {
1073                 unset($attributes[$attribute]);
1074 
1075                 continue;
1076             }
1077 
1078             if ($attribute == 'title')
1079             {
1080                 $attributes[$attribute] = t($value, array(), array('scope' => 'element.title'));
1081             }
1082         }
1083 
1084         #
1085         # value/checked
1086         #
1087 
1088         if ($this->type == self::TYPE_CHECKBOX && $this['checked'] === null)
1089         {
1090             $attributes['checked'] = !!$this[self::DEFAULT_VALUE];
1091         }
1092         else if (($tag_name == 'input' || $tag_name == 'button') && $this['value'] === null)
1093         {
1094             $attributes['value'] = $this[self::DEFAULT_VALUE];
1095         }
1096 
1097         return $attributes;
1098     }
1099 
1100     /**
1101      * Renders attributes.
1102      *
1103      * Attributes with `false` or `null` values as well as custom attributes are discarted.
1104      * Attributes with the `true` value are translated to XHTML standard e.g. readonly="readonly".
1105      *
1106      * @param array $attributes
1107      *
1108      * @return string
1109      *
1110      * @throws \InvalidArgumentException if the value is an array or an object that doesn't
1111      * implement the `toString()` method.
1112      */
1113     protected function render_attributes(array $attributes)
1114     {
1115         $html = '';
1116 
1117         foreach ($attributes as $attribute => $value)
1118         {
1119             if ($value === false || $value === null || $attribute{0} == '#')
1120             {
1121                 continue;
1122             }
1123             else if ($value === true)
1124             {
1125                 $value = $attribute;
1126             }
1127             else if (is_array($value) || (is_object($value) && !method_exists($value, '__toString')))
1128             {
1129                 throw new \InvalidArgumentException(format('Invalid value for attribute %attribute: :value', array('attribute' => $attribute, 'value' => $value)));
1130             }
1131 
1132             $html .= ' ' . $attribute . '="' . (is_numeric($value) ? $value : escape($value)) . '"';
1133         }
1134 
1135         return $html;
1136     }
1137 
1138     /**
1139      * Alters the dataset.
1140      *
1141      * The method is invoked before the dataset is rendered.
1142      *
1143      * The method might add the `default-value` and `widget-constructor` keys.
1144      *
1145      * @param array $dataset
1146      *
1147      * @return array
1148      */
1149     protected function alter_dataset(array $dataset)
1150     {
1151         if ((in_array($this->tag_name, self::$has_attribute_value) || $this->tag_name == 'textarea')
1152         && $this['data-default-value'] === null)
1153         {
1154             $dataset['default-value'] = $this[self::DEFAULT_VALUE];
1155         }
1156 
1157         if (!isset($dataset['widget-constructor']))
1158         {
1159             $dataset['widget-constructor'] = $this[self::WIDGET_CONSTRUCTOR];
1160         }
1161 
1162         return $dataset;
1163     }
1164 
1165     /**
1166      * Renders dataset.
1167      *
1168      * The dataset is rendered as a series of "data-*" attributes. Values of type array are
1169      * encoded using the {@link json_encode()} function. Attributes with null values are discarted,
1170      * but unlike classic attributes boolean values are converted to integers.
1171      *
1172      * @param array $dataset
1173      *
1174      * @return string
1175      */
1176     protected function render_dataset(array $dataset)
1177     {
1178         $rc = '';
1179 
1180         foreach ($dataset as $name => $value)
1181         {
1182             if (is_array($value))
1183             {
1184                 $value = json_encode($value);
1185             }
1186 
1187             if ($value === null)
1188             {
1189                 continue;
1190             }
1191             else if ($value === false)
1192             {
1193                 $value = 0;
1194             }
1195 
1196             $rc .= ' data-' . $name . '="' . (is_numeric($value) ? $value : escape($value)) . '"';
1197         }
1198 
1199         return $rc;
1200     }
1201 
1202     /**
1203      * Returns the HTML representation of the element and its contents.
1204      *
1205      * The attributes are filtered before they are rendered. The attributes with a `false` or
1206      * `null` value are discarded as well as custom attributes, attributes that start with the has
1207      * sign "#". The dataset properties—attributes starting with "data-*"—are extracted to be
1208      * handled separately.
1209      *
1210      * The {@link alter_attributes()} method is invoked to alter the attributes and the
1211      * {@link render_attributes()} method is invoked to render them.
1212      *
1213      * The {@link alter_dataset()} method is invoked to alter the dataset and the
1214      * {@link render_dataset()} method is invoked to render them.
1215      *
1216      * If the inner HTML is null the element is self-closing.
1217      *
1218      * Note: The inner HTML is rendered before the outer HTML.
1219      *
1220      * @return string
1221      */
1222     protected function render_outer_html()
1223     {
1224         $inner = $this->render_inner_html();
1225         $attributes = array();
1226         $dataset = array();
1227 
1228         foreach ($this->tags as $attribute => $value)
1229         {
1230             if (strpos($attribute, 'data-') === 0)
1231             {
1232                 $dataset[substr($attribute, 5)] = $value;
1233             }
1234             else
1235             {
1236                 $attributes[$attribute] = $value;
1237             }
1238         }
1239 
1240         $class = $this->class;
1241 
1242         if ($class)
1243         {
1244             $attributes['class'] = $class;
1245         }
1246 
1247         $html = '<'
1248         . $this->tag_name
1249         . $this->render_attributes($this->alter_attributes($attributes))
1250         . $this->render_dataset($this->alter_dataset($dataset));
1251 
1252         #
1253         # if the inner HTML of the element is `null`, the element is self closing.
1254         #
1255 
1256         if ($inner === null)
1257         {
1258             $html .= ' />';
1259         }
1260         else
1261         {
1262             $html .= '>' . $inner . '</' . $this->tag_name . '>';
1263         }
1264 
1265         return $html;
1266     }
1267 
1268     /**
1269      * Decorates the specified HTML.
1270      *
1271      * The HTML can be decorated by following attributes:
1272      *
1273      * - A label defined by the {@link LABEL} special attribute. The {@link decorate_with_label()}
1274      * method is used to decorate the HTML with the label.
1275      *
1276      * - An inline help defined by the {@link INLINE_HELP} special attribute. The
1277      * {@link decorate_with_inline_help()} method is used to decorate the HTML with the inline
1278      * help.
1279      *
1280      * - A description (or help block) defined by the {@link DESCRIPTION} special attribute. The
1281      * {@link decorate_with_description()} method is used to decorate the HTML with the
1282      * description.
1283      *
1284      * - A legend defined by the {@link LEGEND} special attribute. The
1285      * {@link decorate_with_label()} method is used to decorate the HTML with the legend.
1286      *
1287      * @param string $html The HTML to decorate.
1288      *
1289      * @return string The decorated HTML.
1290      */
1291     protected function decorate($html)
1292     {
1293         #
1294         # add label
1295         #
1296 
1297         $label = $this[self::LABEL];
1298 
1299         if ($label || $label === '0')
1300         {
1301             $label = t($label, array(), array('scope' => 'element.label'));
1302             $html = $this->decorate_with_label($html, $label);
1303         }
1304 
1305         #
1306         # add inline help
1307         #
1308 
1309         $help = $this[self::INLINE_HELP];
1310 
1311         if ($help)
1312         {
1313             $help = t($help, array(), array('scope' => 'element.inline_help'));
1314             $html = $this->decorate_with_inline_help($html, $help);
1315         }
1316 
1317         #
1318         # add description
1319         #
1320 
1321         $description = $this[self::DESCRIPTION];
1322 
1323         if ($description)
1324         {
1325             $description = t($description, array(), array('scope' => 'element.description'));
1326             $html = $this->decorate_with_description($html, $description);
1327         }
1328 
1329         #
1330         # add legend
1331         #
1332 
1333         $legend = $this[self::LEGEND];
1334 
1335         if ($legend)
1336         {
1337             $legend = t($legend, array(), array('scope' => 'element.legend'));
1338             $html = $this->decorate_with_legend($html, $legend);
1339         }
1340 
1341         return $html;
1342     }
1343 
1344     /**
1345      * Decorates the specified HTML with specified label.
1346      *
1347      * The position of the label is defined using the {@link T_LABEL_POSITION} tag
1348      *
1349      * @param string $html
1350      * @param string $label The label as defined by the {@link T_LABEL} tag.
1351      *
1352      * @return string
1353      */
1354     protected function decorate_with_label($html, $label)
1355     {
1356         $class = 'element-label';
1357 
1358         if ($this[self::REQUIRED])
1359         {
1360             $class .= ' required';
1361         }
1362 
1363         switch ($this[self::LABEL_POSITION] ?: 'after')
1364         {
1365             case 'above': return <<<EOT
1366 <label class="$class above">$label</label>
1367 $html
1368 EOT;
1369 
1370             case 'below': return <<<EOT
1371 $html
1372 <label class="$class below">$label</label>
1373 EOT;
1374 
1375             case 'before': return <<<EOT
1376 <label class="$class wrapping before"><span class="label-text">$label</span> $html</label>
1377 EOT;
1378 
1379             case 'after':
1380             default: return <<<EOT
1381 <label class="$class wrapping after">$html <span class="label-text">$label</span></label>
1382 EOT;
1383         }
1384     }
1385 
1386     /**
1387      * Decorates the specified HTML with a fieldset and the specified legend.
1388      *
1389      * @param string $html
1390      * @param string $legend
1391      *
1392      * @return string
1393      */
1394     protected function decorate_with_legend($html, $legend)
1395     {
1396         return '<fieldset><legend>' . $legend . '</legend>' . $html . '</fieldset>';
1397     }
1398 
1399     /**
1400      * Decorates the specified HTML with an inline help element.
1401      *
1402      * @param string $html
1403      * @param string $help
1404      *
1405      * @return string
1406      */
1407     protected function decorate_with_inline_help($html, $help)
1408     {
1409         return $html . '<span class="help-inline">' . $help . '</span>';
1410     }
1411 
1412     /**
1413      * Decorates the specified HTML with the specified description.
1414      *
1415      * @param string $html
1416      * @param string $description
1417      *
1418      * @return string
1419      */
1420     protected function decorate_with_description($html, $description)
1421     {
1422         return $html . '<div class="element-description help-block">' . $description . '</div>';
1423     }
1424 
1425     /**
1426      * Renders the element into an HTML string.
1427      *
1428      * Before the element is rendered the method  {@link handle_assets()} is invoked.
1429      *
1430      * The inner HTML is rendered by the {@link render_inner_html()} method. The outer HTML is
1431      * rendered by the {@link render_outer_html()} method. Finaly, the HTML is decorated by
1432      * the {@link decorate()} method.
1433      *
1434      * If the {@link ElementIsEmpty} exception is caught during the rendering
1435      * an empty string is returned.
1436      *
1437      * @return string The HTML representation of the object
1438      */
1439     public function render()
1440     {
1441         if (get_class($this) != __CLASS__)
1442         {
1443             static::handle_assets();
1444         }
1445 
1446         try
1447         {
1448             $html = $this->render_outer_html();
1449 
1450             return $this->decorate($html);
1451         }
1452         catch (ElementIsEmpty $e)
1453         {
1454             return '';
1455         }
1456     }
1457 
1458     /**
1459      * Renders the element into an HTML string.
1460      *
1461      * The method {@link render()} is invoked to render the element.
1462      *
1463      * If an exception is thrown during the rendering it is rendered using the
1464      * {@link render_exception()} function and returned.
1465      *
1466      * @return string The HTML representation of the object
1467      */
1468     public function __toString()
1469     {
1470         try
1471         {
1472             return $this->render();
1473         }
1474         catch (\Exception $e)
1475         {
1476             return render_exception($e);
1477         }
1478     }
1479 
1480     /**
1481      * Validates the specified value.
1482      *
1483      * This function uses the validator defined using the {@link VALIDATOR} special attribute to
1484      * validate its value.
1485      *
1486      * @param $value
1487      * @param \ICanBoogie\Errors $errors
1488      *
1489      * @return boolean `true` if  the validation succeed, `false` otherwise.
1490      */
1491     public function validate($value, \ICanBoogie\Errors $errors)
1492     {
1493         $validator = $this[self::VALIDATOR];
1494         $options = $this[self::VALIDATOR_OPTIONS];
1495 
1496         if ($validator)
1497         {
1498             list($callback, $params) = $validator + array(1 => array());
1499 
1500             return call_user_func($callback, $errors, $this, $value, $params);
1501         }
1502 
1503         #
1504         # default validator
1505         #
1506 
1507         if (!$options)
1508         {
1509             return true;
1510         }
1511 
1512         switch ($this->type)
1513         {
1514             case self::TYPE_CHECKBOX_GROUP:
1515             {
1516                 if (isset($options['max-checked']))
1517                 {
1518                     $limit = $options['max-checked'];
1519 
1520                     if (count($value) > $limit)
1521                     {
1522                         $errors[$this->name] = t('Le nombre de choix possible pour le champ %name est limité à :limit', array
1523                         (
1524                             'name' => Form::select_element_label($this),
1525                             'limit' => $limit
1526                         ));
1527 
1528                         return false;
1529                     }
1530                 }
1531             }
1532             break;
1533         }
1534 
1535         return true;
1536     }
1537 }
1538 
1539 /**
1540  * Exception thrown when one wants to cancel the whole rendering of an empty element. The
1541  * {@link Element} class takes care of this special case and instead of rendering the exception
1542  * only returns an empty string as the result of its {@link Element::render()} method.
1543  */
1544 class ElementIsEmpty extends \Exception
1545 {
1546     public function __construct($message="The element is empty.", $code=200, \Exception $previous=null)
1547     {
1548         parent::__construct($message, $code, $previous);
1549     }
1550 }
Autodoc API documentation generated by ApiGen 2.8.0