1<?php 2namespace dokuwiki\Form; 3 4/** 5 * Class Form 6 * 7 * Represents the whole Form. This is what you work on, and add Elements to 8 * 9 * @package dokuwiki\Form 10 */ 11class Form extends Element { 12 13 /** 14 * @var array name value pairs for hidden values 15 */ 16 protected $hidden = array(); 17 18 /** 19 * @var Element[] the elements of the form 20 */ 21 protected $elements = array(); 22 23 /** 24 * Creates a new, empty form with some default attributes 25 * 26 * @param array $attributes 27 */ 28 public function __construct($attributes = array()) { 29 global $ID; 30 31 parent::__construct('form', $attributes); 32 33 // use the current URL as default action 34 if(!$this->attr('action')) { 35 $get = $_GET; 36 if(isset($get['id'])) unset($get['id']); 37 $self = wl($ID, $get, false, '&'); //attributes are escaped later 38 $this->attr('action', $self); 39 } 40 41 // post is default 42 if(!$this->attr('method')) { 43 $this->attr('method', 'post'); 44 } 45 46 // we like UTF-8 47 if(!$this->attr('accept-charset')) { 48 $this->attr('accept-charset', 'utf-8'); 49 } 50 51 // add the security token by default 52 $this->setHiddenField('sectok', getSecurityToken()); 53 54 // identify this as a new form based form in HTML 55 $this->addClass('doku_form'); 56 } 57 58 /** 59 * Sets a hidden field 60 * 61 * @param string $name 62 * @param string $value 63 * @return $this 64 */ 65 public function setHiddenField($name, $value) { 66 $this->hidden[$name] = $value; 67 return $this; 68 } 69 70 #region element query function 71 72 /** 73 * Returns the numbers of elements in the form 74 * 75 * @return int 76 */ 77 public function elementCount() { 78 return count($this->elements); 79 } 80 81 /** 82 * Get the position of the element in the form or false if it is not in the form 83 * 84 * Warning: This function may return Boolean FALSE, but may also return a non-Boolean value which evaluates to FALSE. Please read the section on Booleans for more information. Use the === operator for testing the return value of this function. 85 * 86 * @param Element $element 87 * 88 * @return false|int 89 */ 90 public function getElementPosition(Element $element) 91 { 92 return array_search($element, $this->elements, true); 93 } 94 95 /** 96 * Returns a reference to the element at a position. 97 * A position out-of-bounds will return either the 98 * first (underflow) or last (overflow) element. 99 * 100 * @param int $pos 101 * @return Element 102 */ 103 public function getElementAt($pos) { 104 if($pos < 0) $pos = count($this->elements) + $pos; 105 if($pos < 0) $pos = 0; 106 if($pos >= count($this->elements)) $pos = count($this->elements) - 1; 107 return $this->elements[$pos]; 108 } 109 110 /** 111 * Gets the position of the first of a type of element 112 * 113 * @param string $type Element type to look for. 114 * @param int $offset search from this position onward 115 * @return false|int position of element if found, otherwise false 116 */ 117 public function findPositionByType($type, $offset = 0) { 118 $len = $this->elementCount(); 119 for($pos = $offset; $pos < $len; $pos++) { 120 if($this->elements[$pos]->getType() == $type) { 121 return $pos; 122 } 123 } 124 return false; 125 } 126 127 /** 128 * Gets the position of the first element matching the attribute 129 * 130 * @param string $name Name of the attribute 131 * @param string $value Value the attribute should have 132 * @param int $offset search from this position onward 133 * @return false|int position of element if found, otherwise false 134 */ 135 public function findPositionByAttribute($name, $value, $offset = 0) { 136 $len = $this->elementCount(); 137 for($pos = $offset; $pos < $len; $pos++) { 138 if($this->elements[$pos]->attr($name) == $value) { 139 return $pos; 140 } 141 } 142 return false; 143 } 144 145 #endregion 146 147 #region Element positioning functions 148 149 /** 150 * Adds or inserts an element to the form 151 * 152 * @param Element $element 153 * @param int $pos 0-based position in the form, -1 for at the end 154 * @return Element 155 */ 156 public function addElement(Element $element, $pos = -1) { 157 if(is_a($element, '\dokuwiki\Form\Form')) throw new \InvalidArgumentException('You can\'t add a form to a form'); 158 if($pos < 0) { 159 $this->elements[] = $element; 160 } else { 161 array_splice($this->elements, $pos, 0, array($element)); 162 } 163 return $element; 164 } 165 166 /** 167 * Replaces an existing element with a new one 168 * 169 * @param Element $element the new element 170 * @param int $pos 0-based position of the element to replace 171 */ 172 public function replaceElement(Element $element, $pos) { 173 if(is_a($element, '\dokuwiki\Form\Form')) throw new \InvalidArgumentException('You can\'t add a form to a form'); 174 array_splice($this->elements, $pos, 1, array($element)); 175 } 176 177 /** 178 * Remove an element from the form completely 179 * 180 * @param int $pos 0-based position of the element to remove 181 */ 182 public function removeElement($pos) { 183 array_splice($this->elements, $pos, 1); 184 } 185 186 #endregion 187 188 #region Element adding functions 189 190 /** 191 * Adds a text input field 192 * 193 * @param string $name 194 * @param string $label 195 * @param int $pos 196 * @return InputElement 197 */ 198 public function addTextInput($name, $label = '', $pos = -1) { 199 return $this->addElement(new InputElement('text', $name, $label), $pos); 200 } 201 202 /** 203 * Adds a password input field 204 * 205 * @param string $name 206 * @param string $label 207 * @param int $pos 208 * @return InputElement 209 */ 210 public function addPasswordInput($name, $label = '', $pos = -1) { 211 return $this->addElement(new InputElement('password', $name, $label), $pos); 212 } 213 214 /** 215 * Adds a radio button field 216 * 217 * @param string $name 218 * @param string $label 219 * @param int $pos 220 * @return CheckableElement 221 */ 222 public function addRadioButton($name, $label = '', $pos = -1) { 223 return $this->addElement(new CheckableElement('radio', $name, $label), $pos); 224 } 225 226 /** 227 * Adds a checkbox field 228 * 229 * @param string $name 230 * @param string $label 231 * @param int $pos 232 * @return CheckableElement 233 */ 234 public function addCheckbox($name, $label = '', $pos = -1) { 235 return $this->addElement(new CheckableElement('checkbox', $name, $label), $pos); 236 } 237 238 /** 239 * Adds a dropdown field 240 * 241 * @param string $name 242 * @param array $options 243 * @param string $label 244 * @param int $pos 245 * @return DropdownElement 246 */ 247 public function addDropdown($name, $options, $label = '', $pos = -1) { 248 return $this->addElement(new DropdownElement($name, $options, $label), $pos); 249 } 250 251 /** 252 * Adds a textarea field 253 * 254 * @param string $name 255 * @param string $label 256 * @param int $pos 257 * @return TextareaElement 258 */ 259 public function addTextarea($name, $label = '', $pos = -1) { 260 return $this->addElement(new TextareaElement($name, $label), $pos); 261 } 262 263 /** 264 * Adds a simple button, escapes the content for you 265 * 266 * @param string $name 267 * @param string $content 268 * @param int $pos 269 * @return Element 270 */ 271 public function addButton($name, $content, $pos = -1) { 272 return $this->addElement(new ButtonElement($name, hsc($content)), $pos); 273 } 274 275 /** 276 * Adds a simple button, allows HTML for content 277 * 278 * @param string $name 279 * @param string $html 280 * @param int $pos 281 * @return Element 282 */ 283 public function addButtonHTML($name, $html, $pos = -1) { 284 return $this->addElement(new ButtonElement($name, $html), $pos); 285 } 286 287 /** 288 * Adds a label referencing another input element, escapes the label for you 289 * 290 * @param string $label 291 * @param string $for 292 * @param int $pos 293 * @return Element 294 */ 295 public function addLabel($label, $for='', $pos = -1) { 296 return $this->addLabelHTML(hsc($label), $for, $pos); 297 } 298 299 /** 300 * Adds a label referencing another input element, allows HTML for content 301 * 302 * @param string $content 303 * @param string|Element $for 304 * @param int $pos 305 * @return Element 306 */ 307 public function addLabelHTML($content, $for='', $pos = -1) { 308 $element = new LabelElement(hsc($content)); 309 310 if(is_a($for, '\dokuwiki\Form\Element')) { 311 /** @var Element $for */ 312 $for = $for->id(); 313 } 314 $for = (string) $for; 315 if($for !== '') { 316 $element->attr('for', $for); 317 } 318 319 return $this->addElement($element, $pos); 320 } 321 322 /** 323 * Add fixed HTML to the form 324 * 325 * @param string $html 326 * @param int $pos 327 * @return HTMLElement 328 */ 329 public function addHTML($html, $pos = -1) { 330 return $this->addElement(new HTMLElement($html), $pos); 331 } 332 333 /** 334 * Add a closed HTML tag to the form 335 * 336 * @param string $tag 337 * @param int $pos 338 * @return TagElement 339 */ 340 public function addTag($tag, $pos = -1) { 341 return $this->addElement(new TagElement($tag), $pos); 342 } 343 344 /** 345 * Add an open HTML tag to the form 346 * 347 * Be sure to close it again! 348 * 349 * @param string $tag 350 * @param int $pos 351 * @return TagOpenElement 352 */ 353 public function addTagOpen($tag, $pos = -1) { 354 return $this->addElement(new TagOpenElement($tag), $pos); 355 } 356 357 /** 358 * Add a closing HTML tag to the form 359 * 360 * Be sure it had been opened before 361 * 362 * @param string $tag 363 * @param int $pos 364 * @return TagCloseElement 365 */ 366 public function addTagClose($tag, $pos = -1) { 367 return $this->addElement(new TagCloseElement($tag), $pos); 368 } 369 370 /** 371 * Open a Fieldset 372 * 373 * @param string $legend 374 * @param int $pos 375 * @return FieldsetOpenElement 376 */ 377 public function addFieldsetOpen($legend = '', $pos = -1) { 378 return $this->addElement(new FieldsetOpenElement($legend), $pos); 379 } 380 381 /** 382 * Close a fieldset 383 * 384 * @param int $pos 385 * @return TagCloseElement 386 */ 387 public function addFieldsetClose($pos = -1) { 388 return $this->addElement(new FieldsetCloseElement(), $pos); 389 } 390 391 #endregion 392 393 /** 394 * Adjust the elements so that fieldset open and closes are matching 395 */ 396 protected function balanceFieldsets() { 397 $lastclose = 0; 398 $isopen = false; 399 $len = count($this->elements); 400 401 for($pos = 0; $pos < $len; $pos++) { 402 $type = $this->elements[$pos]->getType(); 403 if($type == 'fieldsetopen') { 404 if($isopen) { 405 //close previous fieldset 406 $this->addFieldsetClose($pos); 407 $lastclose = $pos + 1; 408 $pos++; 409 $len++; 410 } 411 $isopen = true; 412 } else if($type == 'fieldsetclose') { 413 if(!$isopen) { 414 // make sure there was a fieldsetopen 415 // either right after the last close or at the begining 416 $this->addFieldsetOpen('', $lastclose); 417 $len++; 418 $pos++; 419 } 420 $lastclose = $pos; 421 $isopen = false; 422 } 423 } 424 425 // close open fieldset at the end 426 if($isopen) { 427 $this->addFieldsetClose(); 428 } 429 } 430 431 /** 432 * The HTML representation of the whole form 433 * 434 * @return string 435 */ 436 public function toHTML() { 437 $this->balanceFieldsets(); 438 439 $html = '<form ' . buildAttributes($this->attrs()) . '>'; 440 441 foreach($this->hidden as $name => $value) { 442 $html .= '<input type="hidden" name="' . $name . '" value="' . formText($value) . '" />'; 443 } 444 445 foreach($this->elements as $element) { 446 $html .= $element->toHTML(); 447 } 448 449 $html .= '</form>'; 450 451 return $html; 452 } 453} 454