xref: /dokuwiki/inc/Form/Form.php (revision 2ce8affc66a5825b8eb9f401e287a2315987d62a)
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