1 <?php
2 /**
3 * Represents a field in a form.
4 *
5 * A FieldSet contains a number of FormField objects which make up the whole of a form.
6 * In addition to single fields, FormField objects can be "composite", for example, the {@link TabSet}
7 * field. Composite fields let us define complex forms without having to resort to custom HTML.
8 *
9 * <b>Subclassing</b>
10 *
11 * Define a {@link dataValue()} method that returns a value suitable for inserting into a single database field.
12 * For example, you might tidy up the format of a date or currency field.
13 * Define {@link saveInto()} to totally customise saving.
14 * For example, data might be saved to the filesystem instead of the data record,
15 * or saved to a component of the data record instead of the data record itself.
16 *
17 * @package forms
18 * @subpackage core
19 */
20 class FormField extends RequestHandler {
21
22 // Глобальный флаг использования html5 полей на сайте
23 protected static $use_html5 = false;
24
25 // разрешение использования html5 для текущего поля
26 protected $fieldHTML5 = true;
27
28 protected $form;
29 protected $name, $title, $value ,$message, $messageType, $extraClass;
30 protected $extraAttributes = array();
31 protected $html5Attributes = array();
32
33 // autocomplete parametr, can use without html5
34 protected $autocomplete = false;
35
36 /**
37 * @var $description string Adds a "title"-attribute to the markup.
38 * @todo Implement in all subclasses
39 */
40 protected $description;
41
42 /**
43 * @var $extraClasses array Extra CSS-classes for the formfield-container
44 */
45 protected $extraClasses;
46
47 public $dontEscape;
48
49 /**
50 * @var $rightTitle string Used in SmallFieldHolder() to force a right-aligned label.
51 */
52 protected $rightTitle;
53
54 /**
55 * @var $leftTitle string Used in SmallFieldHolder() to force a left-aligned label with correct spacing.
56 * Please use $title for FormFields rendered with FieldHolder().
57 */
58 protected $leftTitle;
59
60 /**
61 * Set the "tabindex" HTML attribute on the field.
62 *
63 * @var int
64 */
65 protected $tabIndex;
66
67 /**
68 * Stores a reference to the FieldSet that contains this object.
69 * @var FieldSet
70 */
71 protected $containerFieldSet;
72
73 /**
74 * @var $readonly boolean
75 */
76 protected $readonly = false;
77
78 /**
79 * @var $disabled boolean
80 */
81 protected $disabled = false;
82
83 /**
84 * @var Custom Validation Message for the Field
85 */
86 protected $customValidationMessage = "";
87
88 static function allow_html5($val = true) {
89 self::$use_html5 = $val;
90 }
91
92 static function use_html5() {
93 return self::$use_html5;
94 }
95
96 function allowHTML5($val) {
97 $this->fieldHTML5 = $val;
98 }
99
100 function useHTML5() {
101 return (self::use_html5() && $this->fieldHTML5);
102 }
103
104 // возможные значения атрибута autocomplete поля (взято тут https://wiki.whatwg.org/wiki/Autocomplete_Types#4.10.7.3.1.1_Specifying_field_data_type_hints)
105 static $possible_autocomplete_values = array('name', 'given-name', 'family-name', 'street-address', 'city', 'region', 'postal-code', 'country-name', 'email', 'tel');
106
107 /**
108 * Create a new field.
109 * @param name The internal field name, passed to forms.
110 * @param title The field label.
111 * @param value The value of the field.
112 * @param form Reference to the container form
113 * @param maxLength The Maximum length of the attribute
114 */
115 function __construct($name, $title = null, $value = null, $form = null, $rightTitle = null) {
116 $this->name = $name;
117 $this->title = ($title === null) ? $name : $title;
118
119 if($value !== NULL) $this->setValue($value);
120 if($form) $this->setForm($form);
121
122 parent::__construct();
123 }
124
125 // выставляем атрибут autocomplete - возможно нужна проверка на подходящие значения
126 function setHTML5Attribute($title, $value) {
127 $this->html5Attributes[$title] = $value;
128 }
129
130 function setAutocomplete($value) {
131 if (array_search($value, self::$possible_autocomplete_values) !== false) {
132 $this->autocomplete = $value;
133 return true;
134 }
135 return false;
136 }
137
138 /**
139 * Return a Link to this field
140 */
141 function Link($action = null) {
142 return Controller::join_links($this->form->FormAction(), 'field/' . $this->name, $action);
143 }
144
145 /**
146 * Returns the HTML ID of the field - used in the template by label tags.
147 * The ID is generated as FormName_FieldName. All Field functions should ensure
148 * that this ID is included in the field.
149 */
150 function id() {
151 $name = ereg_replace('(^-)|(-$)','',ereg_replace('[^A-Za-z0-9_-]+','-',$this->Name()));
152 if($this->form) return $this->form->FormName() . '_' . $name;
153 else return $name;
154 }
155
156 /**
157 * Returns the field name - used by templates.
158 *
159 * @return string
160 */
161 function Name() {
162 return $this->name;
163 }
164
165 function attrName() {
166 return $this->name;
167 }
168
169 /**
170 * Returns the field message, used by form validation.
171 * Use {@link setError()} to set this property.
172 *
173 * @return string
174 */
175 function Message() {
176 return $this->message;
177 }
178
179 /**
180 * Returns the field message type, used by form validation.
181 * Arbitrary value which is mostly used for CSS classes
182 * in the rendered HTML, e.g. "required".
183 * Use {@link setError()} to set this property.
184 *
185 * @return string
186 */
187 function MessageType() {
188 return $this->messageType;
189 }
190
191 /**
192 * Returns the field value - used by templates.
193 */
194 function Value() {
195 return $this->value;
196 }
197
198 /**
199 * Method to save this form field into the given data object.
200 * By default, makes use of $this->dataValue()
201 */
202 function saveInto(DataObjectInterface $record) {
203 if($this->name) {
204 $record->setCastedField($this->name, $this->dataValue());
205 }
206 }
207
208 /**
209 * Returns the field value suitable for insertion into the data object
210 */
211 function dataValue() {
212 return $this->value;
213 }
214
215 /**
216 * Returns the field label - used by templates.
217 */
218 function Title() {
219 return $this->title;
220 }
221
222 function setTitle($val) {
223 $this->title = $val;
224 }
225
226 function RightTitle() {
227 return $this->rightTitle;
228 }
229
230 function setRightTitle($val) {
231 $this->rightTitle = $val;
232 }
233
234 function LeftTitle() {
235 return $this->leftTitle;
236 }
237
238 function setLeftTitle($val) {
239 $this->leftTitle = $val;
240 }
241
242 /**
243 * Set tabindex HTML attribute
244 * (defaults to none).
245 *
246 * @param int $index
247 */
248 public function setTabIndex($index) {
249 $this->tabIndex = $index;
250 }
251
252 /**
253 * Get tabindex (if previously set)
254 *
255 * @return int
256 */
257 public function getTabIndex() {
258 return $this->tabIndex;
259 }
260
261 /**
262 * Get tabindex HTML string
263 *
264 * @param int $increment Increase current tabindex by this value
265 * @return string
266 */
267 protected function getTabIndexHTML($increment = 0) {
268 $tabIndex = (int)$this->getTabIndex() + (int)$increment;
269 return (is_numeric($tabIndex)) ? ' tabindex = "' . $tabIndex . '"' : '';
270 }
271
272 /**
273 * Compiles all CSS-classes. Optionally includes a "nolabel"-class
274 * if no title was set on the formfield.
275 * Uses {@link Message()} and {@link MessageType()} to add validatoin
276 * error classes which can be used to style the contained tags.
277 *
278 * @return String CSS-classnames
279 */
280 function extraClass() {
281 $output = "";
282 if(is_array($this->extraClasses)) {
283 $output = " " . implode($this->extraClasses, " ");
284 }
285
286 // Allow customization of label and field tag positioning
287 if(!$this->Title()) $output .= " nolabel";
288
289 // Allow custom styling of any element in the container based
290 // on validation errors, e.g. red borders on input tags.
291 // CSS-Class needs to be different from the one rendered
292 // through {@link FieldHolder()}
293 if($this->Message()) $output .= " holder-" . $this->MessageType();
294
295 return $output;
296 }
297
298 /**
299 * Add a CSS-class to the formfield-container.
300 *
301 * @param $class String
302 */
303 function addExtraClass($class) {
304 $this->extraClasses[$class] = $class;
305 }
306
307 /**
308 * Remove a CSS-class from the formfield-container.
309 *
310 * @param $class String
311 */
312 function removeExtraClass($class) {
313 if(isset($this->extraClasses) && array_key_exists($class, $this->extraClasses)) unset($this->extraClasses[$class]);
314 }
315
316 /**
317 * Add an attribute
318 *
319 * @param $name String
320 * @param $value String
321 */
322 function addExtraAttribute($name, $value) {
323 $this->extraAttributes[$name] = $value;
324 }
325
326 /**
327 * Remove an attribute
328 *
329 * @param $name String
330 */
331 function removeExtraAttribute($name) {
332 if(isset($this->extraAttributes) && array_key_exists($name, $this->extraAttributes)) unset($this->extraAttributes[$name]);
333 }
334
335 /**
336 * Returns a version of a title suitable for insertion into an HTML attribute
337 */
338 function attrTitle() {
339 return Convert::raw2att($this->title);
340 }
341 /**
342 * Returns a version of a title suitable for insertion into an HTML attribute
343 */
344 function attrValue() {
345 return Convert::raw2att($this->value);
346 }
347
348 /**
349 * Set the field value.
350 * Returns $this.
351 */
352 function setValue($value) {
353 $this->value = $value; return $this;
354 }
355
356 /**
357 * Set the field name
358 */
359 function setName($name) {
360 $this->name = $name;
361 }
362
363 /**
364 * Set the container form.
365 * This is called whenever you create a new form and put fields inside it, so that you don't
366 * have to worry about linking the two.
367 */
368 function setForm($form) {
369 $this->form = $form;
370 }
371
372 /**
373 * Get the currently used form.
374 *
375 * @return Form
376 */
377 function getForm() {
378 return $this->form;
379 }
380
381 /**
382 * Return TRUE if security token protection is enabled on the parent {@link Form}.
383 *
384 * @return bool
385 */
386 public function securityTokenEnabled() {
387 return $this->getForm() && $this->getForm()->securityTokenEnabled();
388 }
389
390 /**
391 * Sets the error message to be displayed on the form field
392 * Set by php validation of the form
393 */
394 function setError($message,$messageType){
395 $this->message = $message;
396 $this->messageType = $messageType;
397 }
398
399 /**
400 * Set the custom error message to show instead of the default
401 * format of Please Fill In XXX. Different from setError() as
402 * that appends it to the standard error messaging
403 *
404 * @param String Message for the error
405 */
406 public function setCustomValidationMessage($msg) {
407 $this->customValidationMessage = $msg;
408 }
409
410 /**
411 * Get the custom error message for this form field. If a custom
412 * message has not been defined then just return blank. The default
413 * error is defined on {@link Validator}.
414 *
415 * @todo Should the default error message be stored here instead
416 * @return String
417 */
418 public function getCustomValidationMessage() {
419 return $this->customValidationMessage;
420 }
421
422 /**
423 * Returns the form field - used by templates.
424 * Although FieldHolder is generally what is inserted into templates, all of the field holder
425 * templates make use of $Field. It's expected that FieldHolder will give you the "complete"
426 * representation of the field on the form, whereas Field will give you the core editing widget,
427 * such as an input tag.
428 *
429 * Our base FormField class just returns a span containing the value. This should be overridden!
430 */
431 function Field() {
432 if($this->value) $value = $this->dontEscape ? ($this->reserveNL ? Convert::raw2xml($this->value) : $this->value) : Convert::raw2xml($this->value);
433 else $value = '<i>(' . _t('FormField.NONE', 'none') . ')</i>';
434
435 $attributes = array(
436 'id' => $this->id(),
437 'class' => 'readonly ' . $this->class . ($this->extraClass() ? $this->extraClass() : '')
438 );
439
440 $hiddenAttributes = array(
441 'type' => 'hidden',
442 'name' => $this->name,
443 'value' => $this->value,
444 'tabindex' => $this->getTabIndex()
445 );
446
447 $containerSpan = $this->createTag('span', $attributes, $value);
448 $hiddenInput = $this->createTag('input', $hiddenAttributes);
449
450 return $containerSpan . "\n" . $hiddenInput;
451 }
452 /**
453 * Returns a "Field Holder" for this field - used by templates.
454 * Forms are constructed from by concatenating a number of these field holders. The default
455 * field holder is a label and form field inside a paragraph tag.
456 *
457 * Composite fields can override FieldHolder to create whatever visual effects you like. It's
458 * a good idea to put the actual HTML for field holders into templates. The default field holder
459 * is the DefaultFieldHolder template. This lets you override the HTML for specific sites, if it's
460 * necessary.
461 *
462 * @todo Add "validationError" if needed.
463 */
464 function FieldHolder() {
465 if ($this->Required()) $this->addExtraClass('requiredField');
466
467 $Title = $this->XML_val('Title');
468 $Message = $this->XML_val('Message');
469 $MessageType = $this->XML_val('MessageType');
470 $RightTitle = $this->XML_val('RightTitle');
471 $Type = $this->XML_val('Type');
472 $extraClass = $this->XML_val('extraClass');
473 $Name = $this->XML_val('Name');
474 $Field = $this->XML_val('Field');
475
476 // Only of the the following titles should apply
477 $titleBlock = (!empty($Title)) ? "<label class=\"left\" for=\"{$this->id()}\">$Title</label>" : "";
478 $rightTitleBlock = (!empty($RightTitle)) ? "<label class=\"right\" for=\"{$this->id()}\">$RightTitle</label>" : "";
479
480 // $MessageType is also used in {@link extraClass()} with a "holder-" prefix
481 $messageBlock = (!empty($Message)) ? "<span class=\"message $MessageType\">$Message</span>" : "";
482
483 return <<<HTML
484 <div id="$Name" class="field $Type $extraClass">$titleBlock<div class="middleColumn">$Field</div>$rightTitleBlock$messageBlock</div>
485 HTML;
486 }
487
488 /**
489 * Returns a restricted field holder used within things like FieldGroups.
490 */
491 function SmallFieldHolder() {
492 if ($this->Required()) $this->addExtraClass('requiredField');
493 $extraClass = $this->XML_val('extraClass');
494
495 $result = '';
496 // set label
497 if($title = $this->RightTitle()){
498 $result .= "<label class=\"right $extraClass\" for=\"" . $this->id() . "\">{$title}</label>\n";
499 } elseif($title = $this->LeftTitle()) {
500 $result .= "<label class=\"left $extraClass\" for=\"" . $this->id() . "\">{$title}</label>\n";
501 } elseif($title = $this->Title()) {
502 $result .= "<label class=\"$extraClass\" for=\"" . $this->id() . "\">{$title}</label>\n";
503 }
504
505 $result .= $this->Field();
506
507 return $result;
508 }
509
510
511 /**
512 * Returns true if this field is a composite field.
513 * To create composite field types, you should subclass {@link CompositeField}.
514 */
515 function isComposite() { return false; }
516
517 /**
518 * Returns true if this field has its own data.
519 * Some fields, such as titles and composite fields, don't actually have any data. It doesn't
520 * make sense for data-focused methods to look at them. By overloading hasData() to return false,
521 * you can prevent any data-focused methods from looking at it.
522 *
523 * @see FieldSet::collateDataFields()
524 */
525 function hasData() { return true; }
526
527 /**
528 * @return boolean
529 */
530 function isReadonly() {
531 return $this->readonly;
532 }
533
534 /**
535 * Sets readonly-flag on form-field. Please use performReadonlyTransformation()
536 * to actually transform this instance.
537 * @param $bool boolean Setting "false" has no effect on the field-state.
538 */
539 function setReadonly($bool) {
540 $this->readonly = $bool;
541 }
542
543 /**
544 * @return boolean
545 */
546 function isDisabled() {
547 return $this->disabled;
548 }
549
550 /**
551 * Sets disabed-flag on form-field. Please use performDisabledTransformation()
552 * to actually transform this instance.
553 * @param $bool boolean Setting "false" has no effect on the field-state.
554 */
555 function setDisabled($bool) {
556 $this->disabled = $bool;
557 }
558
559 /**
560 * Returns a readonly version of this field
561 */
562 function performReadonlyTransformation() {
563 $field = new ReadonlyField($this->name, $this->title, $this->value);
564 $field->addExtraClass($this->extraClass());
565 $field->setForm($this->form);
566 return $field;
567 }
568
569 /**
570 * Return a disabled version of this field
571 */
572 function performDisabledTransformation() {
573 $clone = clone $this;
574 $disabledClassName = $clone->class . '_Disabled';
575 if( ClassInfo::exists( $disabledClassName ) )
576 return new $disabledClassName( $this->name, $this->title, $this->value );
577 elseif($clone->hasMethod('setDisabled')){
578 $clone->setDisabled(true);
579 return $clone;
580 }else{
581 return $this->performReadonlyTransformation();
582 }
583 }
584
585 function transform(FormTransformation $trans) {
586 return $trans->transform($this);
587 }
588
589 function hasClass($class){
590 $patten = '/'.strtolower($class).'/i';
591 $subject = strtolower($this->class." ".$this->extraClass());
592 return preg_match($patten, $subject);
593 }
594
595 /**
596 * Returns the field type - used by templates.
597 * The field type is the class name with the word Field dropped off the end, all lowercase.
598 * It's handy for assigning HTML classes.
599 */
600 function Type() {return strtolower(ereg_replace('Field$','',$this->class)); }
601
602 /**
603 * Construct and return HTML tag.
604 *
605 * @todo Transform to static helper method.
606 */
607 function createTag($tag, $attributes, $content = null, $useHTML5 = true) {
608 if ($this->Required()) {
609 $this->setHTML5Attribute('required', 'required');
610 }
611 $preparedAttributes = '';
612 $attributes = array_merge($attributes, $this->extraAttributes);
613 if ($useHTML5 && $this->useHTML5()) {
614 $attributes = array_merge($attributes, $this->html5Attributes);
615 }
616 if ($this->autocomplete) {
617 $attributes = array_merge($attributes, array('autocomplete' => $this->autocomplete));
618 }
619 foreach($attributes as $k => $v) {
620 // Note: as indicated by the $k == value item here; the decisions over what to include in the attributes can sometimes get finicky
621 if(!empty($v) || $v === '0' || $k == 'value') $preparedAttributes .= " $k=\"" . Convert::raw2att($v) . "\"";
622 }
623
624 if($content || $tag != 'input') return "<$tag$preparedAttributes>$content</$tag>";
625 else return "<$tag$preparedAttributes />";
626 }
627
628 /**
629 * javascript handler Functions for each field type by default
630 * formfield doesnt have a validation function
631 *
632 * @todo shouldn't this be an abstract method?
633 */
634 function jsValidation() {
635 }
636
637 /**
638 * Validation Functions for each field type by default
639 * formfield doesnt have a validation function
640 *
641 * @todo shouldn't this be an abstract method?
642 */
643 function validate() {
644 return true;
645 }
646
647 /**
648 * Describe this field, provide help text for it.
649 * The function returns this so it can be used like this:
650 * $action = FormAction::create('submit', 'Submit')->describe("Send your changes to be approved")
651 */
652 function describe($description) {
653 $this->description = $description;
654 return $this;
655 }
656
657 function debug() {
658 return "$this->class ($this->name: $this->title : <font style='color:red;'>$this->message</font>) = $this->value";
659 }
660
661 /**
662 * This function is used by the template processor. If you refer to a field as a $ variable, it
663 * will return the $Field value.
664 */
665 function forTemplate() {
666 return $this->Field();
667 }
668
669 /**
670 * @uses Validator->fieldIsRequired()
671 * @return boolean
672 */
673 function Required() {
674 if($this->form && ($validator = $this->form->Validator)) {
675 return $validator->fieldIsRequired($this->name);
676 }
677 }
678
679 /**
680 * Takes a fieldname and converts camelcase to spaced
681 * words. Also resolves combined fieldnames with dot syntax
682 * to spaced words.
683 *
684 * Examples:
685 * - 'TotalAmount' will return 'Total Amount'
686 * - 'Organisation.ZipCode' will return 'Organisation Zip Code'
687 *
688 * @param string $fieldName
689 * @return string
690 */
691 public function name_to_label($fieldName) {
692 if(strpos($fieldName, '.') !== false) {
693 $parts = explode('.', $fieldName);
694 $label = $parts[count($parts)-2] . ' ' . $parts[count($parts)-1];
695 } else {
696 $label = $fieldName;
697 }
698 $label = preg_replace("/([a-z]+)([A-Z])/","$1 $2", $label);
699
700 return $label;
701 }
702
703 /**
704 * Set the fieldset that contains this field.
705 *
706 * @param FieldSet $containerFieldSet
707 */
708 function setContainerFieldSet($containerFieldSet) {
709 $this->containerFieldSet = $containerFieldSet;
710 }
711
712 function rootFieldSet() {
713 if(is_object($this->containerFieldSet)) return $this->containerFieldSet->rootFieldSet();
714 else user_error("rootFieldSet() called on $this->class object without a containerFieldSet", E_USER_ERROR);
715 }
716
717 }
718 ?>