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