xref: /dokuwiki/inc/Form/Form.php (revision 935d40e901f8e34a100d64426355e459419860f4)
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     * Returns a reference to the element at a position.
83     * A position out-of-bounds will return either the
84     * first (underflow) or last (overflow) element.
85     *
86     * @param int $pos
87     * @return Element
88     */
89    public function getElementAt($pos) {
90        if($pos < 0) $pos = count($this->elements) + $pos;
91        if($pos < 0) $pos = 0;
92        if($pos >= count($this->elements)) $pos = count($this->elements) - 1;
93        return $this->elements[$pos];
94    }
95
96    /**
97     * Gets the position of the first of a type of element
98     *
99     * @param string $type Element type to look for.
100     * @param int $offset search from this position onward
101     * @return false|int position of element if found, otherwise false
102     */
103    public function findPositionByType($type, $offset = 0) {
104        $len = $this->elementCount();
105        for($pos = $offset; $pos < $len; $pos++) {
106            if($this->elements[$pos]->getType() == $type) {
107                return $pos;
108            }
109        }
110        return false;
111    }
112
113    /**
114     * Gets the position of the first element matching the attribute
115     *
116     * @param string $name Name of the attribute
117     * @param string $value Value the attribute should have
118     * @param int $offset search from this position onward
119     * @return false|int position of element if found, otherwise false
120     */
121    public function findPositionByAttribute($name, $value, $offset = 0) {
122        $len = $this->elementCount();
123        for($pos = $offset; $pos < $len; $pos++) {
124            if($this->elements[$pos]->attr($name) == $value) {
125                return $pos;
126            }
127        }
128        return false;
129    }
130
131    #endregion
132
133    #region Element positioning functions
134
135    /**
136     * Adds or inserts an element to the form
137     *
138     * @param Element $element
139     * @param int $pos 0-based position in the form, -1 for at the end
140     * @return Element
141     */
142    public function addElement(Element $element, $pos = -1) {
143        if(is_a($element, '\dokuwiki\Form\Form')) throw new \InvalidArgumentException('You can\'t add a form to a form');
144        if($pos < 0) {
145            $this->elements[] = $element;
146        } else {
147            array_splice($this->elements, $pos, 0, array($element));
148        }
149        return $element;
150    }
151
152    /**
153     * Replaces an existing element with a new one
154     *
155     * @param Element $element the new element
156     * @param int $pos 0-based position of the element to replace
157     */
158    public function replaceElement(Element $element, $pos) {
159        if(is_a($element, '\dokuwiki\Form\Form')) throw new \InvalidArgumentException('You can\'t add a form to a form');
160        array_splice($this->elements, $pos, 1, array($element));
161    }
162
163    /**
164     * Remove an element from the form completely
165     *
166     * @param int $pos 0-based position of the element to remove
167     */
168    public function removeElement($pos) {
169        array_splice($this->elements, $pos, 1);
170    }
171
172    #endregion
173
174    #region Element adding functions
175
176    /**
177     * Adds a text input field
178     *
179     * @param string $name
180     * @param string $label
181     * @param int $pos
182     * @return InputElement
183     */
184    public function addTextInput($name, $label = '', $pos = -1) {
185        return $this->addElement(new InputElement('text', $name, $label), $pos);
186    }
187
188    /**
189     * Adds a password input field
190     *
191     * @param string $name
192     * @param string $label
193     * @param int $pos
194     * @return InputElement
195     */
196    public function addPasswordInput($name, $label = '', $pos = -1) {
197        return $this->addElement(new InputElement('password', $name, $label), $pos);
198    }
199
200    /**
201     * Adds a radio button field
202     *
203     * @param string $name
204     * @param string $label
205     * @param int $pos
206     * @return CheckableElement
207     */
208    public function addRadioButton($name, $label = '', $pos = -1) {
209        return $this->addElement(new CheckableElement('radio', $name, $label), $pos);
210    }
211
212    /**
213     * Adds a checkbox field
214     *
215     * @param string $name
216     * @param string $label
217     * @param int $pos
218     * @return CheckableElement
219     */
220    public function addCheckbox($name, $label = '', $pos = -1) {
221        return $this->addElement(new CheckableElement('checkbox', $name, $label), $pos);
222    }
223
224    /**
225     * Adds a dropdown field
226     *
227     * @param string $name
228     * @param array $options
229     * @param string $label
230     * @param int $pos
231     * @return DropdownElement
232     */
233    public function addDropdown($name, $options, $label = '', $pos = -1) {
234        return $this->addElement(new DropdownElement($name, $options, $label), $pos);
235    }
236
237    /**
238     * Adds a textarea field
239     *
240     * @param string $name
241     * @param string $label
242     * @param int $pos
243     * @return TextareaElement
244     */
245    public function addTextarea($name, $label = '', $pos = -1) {
246        return $this->addElement(new TextareaElement($name, $label), $pos);
247    }
248
249    /**
250     * Adds a simple button, escapes the content for you
251     *
252     * @param string $name
253     * @param string $content
254     * @param int $pos
255     * @return Element
256     */
257    public function addButton($name, $content, $pos = -1) {
258        return $this->addElement(new ButtonElement($name, hsc($content)), $pos);
259    }
260
261    /**
262     * Adds a simple button, allows HTML for content
263     *
264     * @param string $name
265     * @param string $html
266     * @param int $pos
267     * @return Element
268     */
269    public function addButtonHTML($name, $html, $pos = -1) {
270        return $this->addElement(new ButtonElement($name, $html), $pos);
271    }
272
273    /**
274     * Adds a label referencing another input element, escapes the label for you
275     *
276     * @param string $label
277     * @param string $for
278     * @param int $pos
279     * @return Element
280     */
281    public function addLabel($label, $for='', $pos = -1) {
282        return $this->addLabelHTML(hsc($label), $for, $pos);
283    }
284
285    /**
286     * Adds a label referencing another input element, allows HTML for content
287     *
288     * @param string $content
289     * @param string|Element $for
290     * @param int $pos
291     * @return Element
292     */
293    public function addLabelHTML($content, $for='', $pos = -1) {
294        $element = new LabelElement(hsc($content));
295
296        if(is_a($for, '\dokuwiki\Form\Element')) {
297            /** @var Element $for */
298            $for = $for->id();
299        }
300        $for = (string) $for;
301        if($for !== '') {
302            $element->attr('for', $for);
303        }
304
305        return $this->addElement($element, $pos);
306    }
307
308    /**
309     * Add fixed HTML to the form
310     *
311     * @param string $html
312     * @param int $pos
313     * @return HTMLElement
314     */
315    public function addHTML($html, $pos = -1) {
316        return $this->addElement(new HTMLElement($html), $pos);
317    }
318
319    /**
320     * Add a closed HTML tag to the form
321     *
322     * @param string $tag
323     * @param int $pos
324     * @return TagElement
325     */
326    public function addTag($tag, $pos = -1) {
327        return $this->addElement(new TagElement($tag), $pos);
328    }
329
330    /**
331     * Add an open HTML tag to the form
332     *
333     * Be sure to close it again!
334     *
335     * @param string $tag
336     * @param int $pos
337     * @return TagOpenElement
338     */
339    public function addTagOpen($tag, $pos = -1) {
340        return $this->addElement(new TagOpenElement($tag), $pos);
341    }
342
343    /**
344     * Add a closing HTML tag to the form
345     *
346     * Be sure it had been opened before
347     *
348     * @param string $tag
349     * @param int $pos
350     * @return TagCloseElement
351     */
352    public function addTagClose($tag, $pos = -1) {
353        return $this->addElement(new TagCloseElement($tag), $pos);
354    }
355
356    /**
357     * Open a Fieldset
358     *
359     * @param string $legend
360     * @param int $pos
361     * @return FieldsetOpenElement
362     */
363    public function addFieldsetOpen($legend = '', $pos = -1) {
364        return $this->addElement(new FieldsetOpenElement($legend), $pos);
365    }
366
367    /**
368     * Close a fieldset
369     *
370     * @param int $pos
371     * @return TagCloseElement
372     */
373    public function addFieldsetClose($pos = -1) {
374        return $this->addElement(new FieldsetCloseElement(), $pos);
375    }
376
377    #endregion
378
379    /**
380     * Adjust the elements so that fieldset open and closes are matching
381     */
382    protected function balanceFieldsets() {
383        $lastclose = 0;
384        $isopen = false;
385        $len = count($this->elements);
386
387        for($pos = 0; $pos < $len; $pos++) {
388            $type = $this->elements[$pos]->getType();
389            if($type == 'fieldsetopen') {
390                if($isopen) {
391                    //close previous fieldset
392                    $this->addFieldsetClose($pos);
393                    $lastclose = $pos + 1;
394                    $pos++;
395                    $len++;
396                }
397                $isopen = true;
398            } else if($type == 'fieldsetclose') {
399                if(!$isopen) {
400                    // make sure there was a fieldsetopen
401                    // either right after the last close or at the begining
402                    $this->addFieldsetOpen('', $lastclose);
403                    $len++;
404                    $pos++;
405                }
406                $lastclose = $pos;
407                $isopen = false;
408            }
409        }
410
411        // close open fieldset at the end
412        if($isopen) {
413            $this->addFieldsetClose();
414        }
415    }
416
417    /**
418     * The HTML representation of the whole form
419     *
420     * @return string
421     */
422    public function toHTML() {
423        $this->balanceFieldsets();
424
425        $html = '<form ' . buildAttributes($this->attrs()) . '>';
426
427        foreach($this->hidden as $name => $value) {
428            $html .= '<input type="hidden" name="' . $name . '" value="' . formText($value) . '" />';
429        }
430
431        foreach($this->elements as $element) {
432            $html .= $element->toHTML();
433        }
434
435        $html .= '</form>';
436
437        return $html;
438    }
439}
440