<?php
/* vim: set expandtab tabstop=4 shiftwidth=4: */
// +----------------------------------------------------------------------+
// | PHP Version 5                                                        |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997, 1998, 1999, 2000, 2001 The PHP Group             |
// +----------------------------------------------------------------------+
// | This source file is subject to version 2.0 of the PHP license,       |
// | that is bundled with this package in the file LICENSE, and is        |
// | available at through the world-wide-web at                           |
// | http://www.php.net/license/2_02.txt.                                 |
// | If you did not receive a copy of the PHP license and are unable to   |
// | obtain it through the world-wide-web, please send a note to          |
// | license@php.net so we can mail you a copy immediately.               |
// +----------------------------------------------------------------------+
// | Author: Alan Knowles <alan@akbkhome.com>                             |
// | Based on HTML_Common by: Adam Daniel <adaniel1@eesus.jnj.com>        |
// +----------------------------------------------------------------------+
//
// $Id$

/**
 * Lightweight HTML Element builder and render
 *
 * This differs from HTML_Common in the following ways:
 *
 * $element->attributes is Public
 * $element->override if set to anything other than false, renders the value rather than 
 *   the defined element
 *
 * $element->children is a recursvable child array which is rendered by toHTML
 * $element->toHtml() is implemented
 * $element->toHtmlNoClose() renders  only the first tag and children (designed for <form
 * No support for tab offsets, comments ...
 *
 * Full support for Select, and common Form elements using
 * setValue()
 * setOptions()
 * 
 * overlay support with SetFrom - base + inherited..
 *
 * attributes array values:
 *  key="value" // standard key="value" in output
 *  key = true // outputs just key.
 *
 * children can be 
 *  another HTML_Element
 *  or string (raw text)
 *
 *
 * @author      Adam Daniel <adaniel1@eesus.jnj.com>
 * @version     1.6
 * @since       PHP 4.0.3pl1
 * @abstract
 */
class HTML_Template_Flexy_Element {

    

    /**
     * Tag that this Element represents.
     * @var  array
     * @access   public
     */
    var $tag =  '';
    /**
     * Associative array of table attributes
     * Note Special values:
     *   true == only display the key 
     *   false == remove
     *
     * @var  array
     * @access   public
     */
    var $attributes = array();

    /**
     * Sequence array of children
     * children that are strings are assumed to be text 
     * @var  array
     * @access   public
     */
    var $children = array();
    
    /**
     * override the tag.
     * if this is set to anything other than false, it will be output 
     * rather than the tags+children
     * @var  array
     * @access   public
     */
    var $override = false;
    /**
     * prefix the tag.
     * this is output by toHtml as a prefix to the tag (can be used for require tags)
     * @var  array
     * @access   private
     */
    var $prefix = '';
    /**
     * suffix the tag.
     * this is output by toHtml as a suffix to the tag (can be used for error messages)
     * @var  array
     * @access   private
     */
    var $suffix = '';
    
    /**
     * a value for delayed merging into live objects
     * if you set this on an element, it is merged by setValue, at merge time.
     * @var  array
     * @access   public
     */
    var $value = null;
    /**
     * If an input element has a label element associated to it
     * *and* the 'useElementLabels' option is true, then you can
     * optionally set the text of this label. This permits
     * to set custom strings for doing translations.
     * @var string | null
     * @access   public
     */
    var $label = null;
    /**
     * Class constructor
     * @param    mixed   $attributes     Associative array of table tag attributes 
     *                                   or HTML attributes name="value" pairs
     * @access   public
     */
    public function __construct($tag = '', $attributes = null)
    {
        
        $this->tag = strtolower($tag);
        if (false !== strpos($tag, ':')) {
            $bits = explode(':',$this->tag);
            $this->tag = $bits[0] . ':'.strtolower($bits[1]);
        }
        
        $this->setAttributes($attributes);
    } // end constructor

      
    /**
     * Returns an HTML formatted attribute string
     * @param    array   $attributes
     * @return   string
     * @access   private
     */
    function attributesToHTML()
    {
        $strAttr = '';
        $xhtmlclose = '';
        $charset = $this->getCharset();
        foreach ($this->attributes as $key => $value) {
        
            // you shouldn't do this, but It shouldnt barf when you do..
            if (is_array($value) || is_object($value)) {
                continue;
            }
            
            if ($key == 'flexy:xhtml') {
                continue;
            }
            if ($value === false) {
                continue;
            }
            if ($value === true) {
                // this is not xhtml compatible..
                if ($key == '/') {
                    $xhtmlclose = ' /';
                    continue;
                }
                if (isset($this->attributes['flexy:xhtml'])) {
                    $strAttr .= " {$key}=\"{$key}\"";
                } else {
                    $strAttr .= ' ' . $key;
                }
            } else {
                // dont replace & with &amp;
                if ($this->tag == 'textbox') {  // XUL linefeed fix.
                    $value = str_replace("\n", '&#13;', htmlspecialchars($value, ENT_COMPAT, $charset));
                } else {
                    $value = str_replace('&amp;nbsp;', '&nbsp;', htmlspecialchars($value, ENT_COMPAT ,$charset));
                }
                $strAttr .= ' ' . $key . '="' . $value  . '"';
            }
            
        }
        $strAttr .= $xhtmlclose;
        return $strAttr;
    } // end func _getAttrString

    /**
     * Static Method to get key/value array from attributes.
     * Returns a valid atrributes array from either a string or array
     * @param    mixed   $attributes     Either a typical HTML attribute string or an associative array
     * @access   private
     */
    function parseAttributes($attributes)
    {
        if (is_array($attributes)) {
            $ret = array();
            foreach ($attributes as $key => $value) {
                if (is_int($key)) {
                    $ret[strtolower($value)] = true;
                } else {
                    $ret[strtolower($key)]   = $value;
                }
            }
            return $ret;

        } elseif (is_string($attributes)) {
            $preg = "/(([A-Za-z_:]|[^\\x00-\\x7F])([A-Za-z0-9_:.-]|[^\\x00-\\x7F])*)" .
                "([ \\n\\t\\r]+)?(=([ \\n\\t\\r]+)?(\"[^\"]*\"|'[^']*'|[^ \\n\\t\\r]*))?/";
            if (preg_match_all($preg, $attributes, $regs)) {
                for ($counter=0; $counter<count($regs[1]); $counter++) {
                    $name  = $regs[1][$counter];
                    $check = $regs[0][$counter];
                    $value = $regs[7][$counter];
                    if (trim($name) == trim($check)) {
                        $arrAttr[strtolower(trim($name))] = strtolower(trim($name));
                    } else {
                        if (substr($value, 0, 1) == "\"" || substr($value, 0, 1) == "'") {
                            $value = substr($value, 1, -1);
                        }
                        $arrAttr[strtolower(trim($name))] = trim($value);
                    }
                }
                return $arrAttr;
            }
        }
    } // end func _parseAttributes

     
     
       
    /**
     * Utility function to set values from common tag types.
     * @param    HTML_Element   $from  override settings from another element.
     * @access   public
     */
     
    function setValue($value) {
        // store the value in all situations
        $this->value = $value;
        $tag = strtolower($this->tag);
        if (strpos($tag,':') !==  false) {
            $bits = explode(':',$tag);
            $tag = $bits[1];
        }
        switch ($tag) {
            case 'input':
                switch (isset($this->attributes['type']) ? strtolower($this->attributes['type']) : '') {
                    case 'checkbox':
                        if (isset($this->attributes['checked'])) {
                            unset($this->attributes['checked']);
                        }
                        // if value is nto set, it doesnt make any difference what you set ?
                        if (!isset($this->attributes['value'])) {
                            return;
                        }
                        //print_r($this); echo "SET TO "; serialize($value);
                        if (isset($this->attributes['name']) && (substr($this->attributes['name'],-2) == '[]')) {
                            if (is_array($value) && 
                                in_array((string) $this->attributes['value'],$value)
                                ) {
                                $this->attributes['checked'] =  true;
                            }
                            // removed - see bug 15279 - not sure if there is any knock on effects from this.
                            ///return;
                        }
                        if ($this->attributes['value'] == $value) {
                            $this->attributes['checked'] =  true;
                        }
                        
                        
                        return;
                    case 'radio':
                        if (isset($this->attributes['checked'])) {
                            unset($this->attributes['checked']);
                        }
                        // if we dont have values associated yet, store it..
                        if (!isset($this->attributes['value'])) {
                            $this->value = $value;
                            return;
                        }
                        if ($this->attributes['value'] == $value) {
                            $this->attributes['checked'] =  true;
                        }
                        return;
                    
                    default:
                        // no other input accepts array as a value.
                        if (is_array($value)) {
                            return;
                        }
                    
                        $this->attributes['value'] = $value;
                        return;
                }
                
            case 'select':
                
                if (!is_array($value)) {
                    $value = array($value);
                }
                
                // its setting the default value..
                
                foreach($this->children as $i=>$child) {
                    
                    if (is_string($child)) {
                        continue;
                    }
                    if ($child->tag == 'optgroup') {
                        foreach($this->children[$i]->children as $ii=>$child) {
                        
                            // does the value exist and match..
                            if (isset($child->attributes['value']) 
                                && in_array((string) $child->attributes['value'], $value)) 
                            {
                                $this->children[$i]->children[$ii]->attributes['selected'] = 
                                    isset($this->attributes['flexy:xhtml']) ? 'selected' : true;
                                continue;
                            }
                            if (isset($child->attributes['value']) && 
                                isset($this->children[$i]->children[$ii]->attributes['selected'])) 
                            {
                                unset($this->children[$i]->children[$ii]->attributes['selected']);
                                continue;
                            }
                            // value doesnt exst..
                          
                            if (isset($this->children[$i]->children[$ii]->attributes['selected'])) {
                                unset($this->children[$i]->children[$ii]->attributes['selected']);
                                continue;
                            }
                        }
                        continue;
                    }
                    
                    // standard option value...
                    //echo "testing {$child->attributes['value']} against ". print_r($value,true)."\n";
                    // does the value exist and match..
                     
                    if (isset($child->attributes['value']) 
                        && in_array((string) $child->attributes['value'], $value)) 
                    {
                        
                      
                        $this->children[$i]->attributes['selected'] = 
                            isset($this->attributes['flexy:xhtml']) ? 'selected' : true;;
                        continue;
                    }
                    // no value attribute try and use the contents.
                    if (!isset($child->attributes['value'])
                        && is_string($child->children[0])
                        && in_array((string) $child->children[0], $value))
                    {
                        
                        $this->children[$i]->attributes['selected'] =
                            isset($this->attributes['flexy:xhtml']) ? 'selected' : true;
                        continue;
                    }
                     
                    if (isset($child->attributes['value']) && 
                        isset($this->children[$i]->attributes['selected'])) 
                    {
                        //echo "clearing selected\n";
                        unset($this->children[$i]->attributes['selected']);
                        continue;
                    }
                    // value doesnt exst..
                    
                    if (isset($this->children[$i]->attributes['selected'])) {
                        //echo "clearing selected\n";
                        unset($this->children[$i]->attributes['selected']);
                        continue;
                    }
                    
                    
                }
                return;
            case 'textarea':
            case 'label':
                $charset = $this->getCharset();
                $this->children = array(htmlspecialchars($value, ENT_COMPAT, $charset));
                return;
            case '':  // dummy objects.
                $this->value = $value;
                return;
                
            // XUL elements
            case 'menulist':
            case 'textbox':
            case 'checkbox':
                require_once 'HTML/Template/Flexy/Element/Xul.php';
                HTML_Template_Flexy_Element_Xul::setValue($this,$value);
                return ;
                
            default:
                if (is_array($value)) {
                    return;
                }
                $this->value = $value;
        }
            
        
    
    
    }
    /**
     * Utility function equivilant to HTML_Select - loadArray ** 
     * but using 
     * key=>value maps 
     * <option value="key">Value</option>
     * Key=key (eg. both the same) maps to
     * <option>key</option>
     * and label = array(key=>value) maps to 
     * <optgroup label="label"> <option value="key">value</option></optgroup>
     * 
     * $element->setOptions(array('a'=>'xxx','b'=>'yyy'));
     * or
     * $element->setOptions(array('a','b','c','d'),true);
     *
     *
     *.
     * @param    HTML_Element   $from  override settings from another element.
     * @param    HTML_Element   $noValue  ignore the key part of the array
     * @access   public
     */
     
    function setOptions($array,$noValue=false) 
    {
        if (!is_array($array)) {
            $this->children = array();
            return;
        }

        $charset = $this->getCharset();
        $tag = strtolower($this->tag);
        $namespace = '';
        if (false !== strpos($this->tag, ':')) {
            
            $bits = explode(':',$this->tag);
            $namespace = $bits[0] . ':';
            $tag = strtolower($bits[1]);
            
        }
        // if we have specified a xultag!!?
        if (strlen($tag) && ($tag != 'select')) {
                require_once 'HTML/Template/Flexy/Element/Xul.php';
                return HTML_Template_Flexy_Element_Xul::setOptions($this,$array,$noValue);
        }
        
        foreach($array as $k=>$v) {
            if (is_array($v)) {     // optgroup
                $child = new HTML_Template_Flexy_Element($namespace . 'optgroup',array('label'=>$k));
                foreach($v as $kk=>$vv) {
                    $atts=array();
                    if (($kk !== $vv) && !$noValue) {
                        $atts = array('value'=>$kk);
                    } else {
                        $atts = array('value'=>$vv);
                    }
                    $add = new HTML_Template_Flexy_Element($namespace . 'option',$atts);
                    $add->children = array(htmlspecialchars($vv, ENT_COMPAT, $charset));
                    $child->children[] = $add;
                }
                $this->children[] = $child;
                continue;
            } 
            $atts=array();
            if (($k !== $v) && !$noValue) {
                $atts = array('value'=>$k);
            } else {
                $atts = array('value'=>$v);
            }
            $add = new HTML_Template_Flexy_Element($namespace . 'option',$atts);
            $add->children = array(htmlspecialchars($v, ENT_COMPAT, $charset));
            $this->children[] = $add;
        }
       
    }
    
    
    
    /**
     *  Returns THIS select element's options as an associative array
     *  Validates that $this element is "select"
     * @return array $options
     * @access public
    */
    function getOptions()
    {

        $tag = strtolower($this->tag);
        $namespace = '';
        if (false !== strpos($this->tag, ':')) {
            $bits = explode(':',$this->tag);
            $namespace = $bits[0] . ':';
            $tag = strtolower($bits[1]);
        }

        // this is not a select element
        if (strlen($tag) && ($tag != 'select'))  {
            return false;
        }

        // creates an associative array that can be used by setOptions()
        // null does work for no value ( a "Please Choose" option, for example)
        foreach ($this->children as $child) {
            if (is_object($child)) {
                $child->attributes['value'] = isset($child->attributes['value']) ? $child->attributes['value'] : '';
                $children[$child->attributes['value']] = $child->children[0];
            }
        }
        return $children;
    }

     /**
     *  Removes all of this element's options
     *  Validates that $this element is "select"
     * @return bool result
     * @access public
    */
    function clearOptions($children = array())
    {
        $tag = strtolower($this->tag);
        $namespace = '';
        if (false !== strpos($this->tag, ':')) {
            $bits = explode(':',$this->tag);
            $namespace = $bits[0] . ':';
            $tag = strtolower($bits[1]);
        }

        // this is not a select element
        if (strlen($tag) && ($tag != 'select')) {
            return false;
        }

        // clear this select's options
        $this->children = array(null);
        $this->values = array(null);

        // If called with an array of new options go ahead and set them
        $this->setOptions($children);

        return true;
    }
    
    /**
     * Sets the HTML attributes
     * @param    mixed   $attributes     Either a typical HTML attribute string or an associative array
     * @access   public
     */
     
    function setAttributes($attributes)
    {
        $attrs= $this->parseAttributes($attributes);
        if (!is_array($attrs)) {
            return false;
        }
        foreach ($attrs as $key => $value) {
            $this->attributes[$key] = $value;
        }
    } // end func updateAttributes

    /**
     * Removes an attributes
     * 
     * @param     string    $attr   Attribute name
     * @since     1.4
     * @access    public
     * @return    void
     * @throws
     */
    function removeAttributes($attrs)
    {
        if (is_string($attrs)) {
            $attrs = array($attrs);
        }
        foreach ($attrs as $attr) { 
            if (isset($this->attributes[strtolower($attr)])) {
                 $this->attributes[strtolower($attr)] = false;
            } 
        }
    } //end func removeAttribute

      
    /**
     * Output HTML and children
     *
     * @access    public
     * @param     object    $overlay = merge data from object.
     * @return    string
     * @abstract
     */
    public function toHtml($overlay = false)
    {
         
        //echo "BEFORE<PRE>";print_R($this);
        $ret = $this;
        if ($overlay !== false) {
            $ret = $this->merge($overlay);
        }
        
        if ($ret->override !== false) {
            return $ret->override;
        }
        $prefix = $ret->prefix;
        if (is_object($prefix)) {
            $prefix = $prefix->toHtml();
        }
        $suffix = $ret->suffix;
        if (is_object($suffix)) {
            $suffix = $suffix->toHtml();
        }
        //echo "AFTER<PRE>";print_R($ret);
      
        $tag = $this->tag;
        if (strpos($tag,':') !==  false) {
            $bits = explode(':',$tag);
            $tag = $bits[1];
        }
        // tags that never should have closers  
        $close = "</{$ret->tag}>";
         
        if (in_array(strtoupper($tag),array("INPUT","IMG", "LINK", "META", "HR", "BR"))) {
            $close = '';
            if (isset($ret->attributes['flexy:xhtml'])) {
                $this->attributes['/'] = true;
            }

        }
        if (isset($this->attributes['/'])) {
            $close = '';
        }

        $close .= $suffix ;
       
        return "{$prefix}<{$ret->tag}" . $ret->attributesToHTML() . '>' .  $ret->childrenToHTML() . $close;
        
         
    } // end func toHtml
    
    
    /**
     * Output Open Tag and any children and not Child tag (designed for use with <form + hidden elements>
     *
     * @access    public
     * @param     object    $overlay = merge data from object.
     * @return    string
     * @abstract
     */
    public function toHtmlnoClose($overlay = false)
    {
        $ret = $this;
        if ($ret->override !== false) {
            return $ret->override;
        }
        if ($overlay !== false) {
            $ret = $this->merge($overlay);
        }

        return "<{$ret->tag}".$ret->attributesToHTML() . '>' . $ret->childrenToHTML();

    } // end func toHtml
    
    
    /**
     * Output HTML and children
     *
     * @access    public
     * @return    string
     * @abstract
     */
    function childrenToHtml()
    {
        $ret = '';
        foreach($this->children as $child) {
            if (!is_object($child)) {
                $ret .= $child;
                continue;
            }
            
            $ret .= $child->toHtml();
        }
        return $ret;
    } // end func toHtml
    
     
    /**
     * merge this element with another
     * originally was in  main engine
     *
     * @param    HTML_Template_Flexy_Element   $new (with data to replace/merge)
     * @return   HTML_Template_Flexy_Element   the combined/merged data.
     * @static
     * @access   public
     */
    public function merge($new)
    {
        // Clone objects is possible to avoid creating references between elements
        $original = clone $this;
        // no new - return original
        if (!$new) {
            return $original;
        }

        // If the properties of $original differ from those of $new and 
        // they are set on $new, set them to $new's. Otherwise leave them 
        // as they are.

        if ($new->tag && ($new->tag != $original->tag)) {
            $original->tag = $new->tag;
        }
        
        if ($new->override !== false) {
            $original->override = $new->override;
        }
        
        if (count($new->children)) {
            //echo "<PRE> COPY CHILDREN"; print_r($from->children);
            $original->children = $new->children;
        }
        
        if (is_array($new->attributes)) {
        
            foreach ($new->attributes as $key => $value) {
                $original->attributes[$key] = $value;
            }
        }
        // originals never have prefixes or suffixes..
        $original->prefix = $new->prefix;
        $original->suffix = $new->suffix;  

        if ($new->value !== null) {
            $original->setValue($new->value);
        } 
        if ($new->label !== null) {
            $original->label = $new->label;
        }
        return $original;
        
    }  

    /**
     * Get character set
     *
     * @param void
     *
     * @return string
     */
    protected function getCharset()
    {
        if (isset($GLOBALS['_HTML_TEMPLATE_FLEXY']['currentOptions'])) {
            $options = $GLOBALS['_HTML_TEMPLATE_FLEXY']['currentOptions'];
            if (is_array($options) && isset($options['charset'])) {
                return $options['charset'];
            }
        }

        $options = PEAR::getStaticProperty('HTML_Template_Flexy', 'options');
        if (is_array($options) && isset($options['charset'])) {
            return $options['charset'];
        }

        return 'UTF-8';
    }
}
