1: <?php
2:
3: 4: 5: 6: 7: 8: 9:
10:
11: namespace Kdyby\BootstrapFormRenderer;
12:
13: use Nette;
14: use Nette\Forms\Controls;
15: use Nette\Iterators\Filter;
16: use Nette\Latte\Macros\FormMacros;
17: use Nette\Templating\FileTemplate;
18: use Nette\Utils\Html;
19:
20:
21:
22: 23: 24: 25: 26: 27: 28: 29: 30: 31:
32: class BootstrapRenderer extends Nette\Object implements Nette\Forms\IFormRenderer
33: {
34:
35: 36: 37: 38:
39: public $errorsAtInputs = TRUE;
40:
41: 42: 43:
44: public $priorGroups = array();
45:
46: 47: 48:
49: private $form;
50:
51: 52: 53:
54: private $template;
55:
56:
57:
58: 59: 60:
61: public function __construct(FileTemplate $template = NULL)
62: {
63: $this->template = $template;
64: }
65:
66:
67:
68: 69: 70: 71: 72: 73: 74: 75:
76: public function render(Nette\Forms\Form $form, $mode = NULL, $args = NULL)
77: {
78: if ($this->template === NULL) {
79: if ($presenter = $form->lookup('Nette\Application\UI\Presenter', FALSE)) {
80:
81: $this->template = clone $presenter->getTemplate();
82:
83: } else {
84: $this->template = new FileTemplate();
85: $this->template->registerFilter(new Nette\Latte\Engine());
86: }
87: }
88:
89: if ($this->form !== $form) {
90: $this->form = $form;
91:
92:
93: if ($translator = $this->form->getTranslator()) {
94: $this->template->setTranslator($translator);
95: }
96:
97:
98: foreach ($this->form->getControls() as $control) {
99: $this->prepareControl($control);
100: }
101:
102: $formEl = $form->getElementPrototype();
103: if (!($classes = self::getClasses($formEl)) || stripos($classes, 'form-') === FALSE) {
104: $formEl->addClass('form-horizontal');
105: }
106:
107: } elseif ($mode === 'begin') {
108: foreach ($this->form->getControls() as $control) {
109:
110: $control->setOption('rendered', FALSE);
111: }
112: }
113:
114: $this->template->setFile(__DIR__ . '/@form.latte');
115: $this->template->setParameters(
116: array_fill_keys(array('control', '_control', 'presenter', '_presenter'), NULL) +
117: array('_form' => $this->form, 'form' => $this->form, 'renderer' => $this)
118: );
119:
120: if ($mode === NULL) {
121: if ($args) {
122: $this->form->getElementPrototype()->addAttributes($args);
123: }
124: $this->template->render();
125:
126: } elseif ($mode === 'begin') {
127: FormMacros::renderFormBegin($this->form, (array)$args);
128:
129: } elseif ($mode === 'end') {
130: FormMacros::renderFormEnd($this->form);
131:
132: } else {
133:
134: $attrs = array('input' => array(), 'label' => array());
135: foreach ((array) $args as $key => $val) {
136: if (stripos($key, 'input-') === 0) {
137: $attrs['input'][substr($key, 6)] = $val;
138:
139: } elseif (stripos($key, 'label-') === 0) {
140: $attrs['label'][substr($key, 6)] = $val;
141: }
142: }
143:
144: $this->template->setFile(__DIR__ . '/@parts.latte');
145: $this->template->mode = $mode;
146: $this->template->attrs = (array) $attrs;
147: $this->template->render();
148: }
149: }
150:
151:
152:
153: 154: 155:
156: private function prepareControl(Controls\BaseControl $control)
157: {
158: $translator = $this->form->getTranslator();
159: $control->setOption('rendered', FALSE);
160:
161: if ($control->isRequired()) {
162: $control->getLabelPrototype()->addClass('required');
163: $control->setOption('required', TRUE);
164: }
165:
166: $el = $control->getControlPrototype();
167: if ($placeholder = $control->getOption('placeholder')) {
168: if (!$placeholder instanceof Html && $translator) {
169: $placeholder = $translator->translate($placeholder);
170: }
171: $el->placeholder($placeholder);
172: }
173:
174: if ($control->controlPrototype->type === 'email'
175: && $control->getOption('input-prepend') === NULL
176: ) {
177: $control->setOption('input-prepend', '@');
178: }
179:
180: if ($control instanceof Nette\Forms\ISubmitterControl) {
181: $el->addClass('btn');
182:
183: } else {
184: $label = $control->labelPrototype;
185: if ($control instanceof Controls\Checkbox) {
186: $label->addClass('checkbox');
187:
188: } elseif (!$control instanceof Controls\RadioList) {
189: $label->addClass('control-label');
190: }
191:
192: $control->setOption('pairContainer', $pair = Html::el('div'));
193: $pair->id = $control->htmlId . '-pair';
194: $pair->addClass('control-group');
195: if ($control->getOption('required', FALSE)) {
196: $pair->addClass('required');
197: }
198: if ($control->errors) {
199: $pair->addClass('error');
200: }
201:
202: if ($prepend = $control->getOption('input-prepend')) {
203: $prepend = Html::el('span', array('class' => 'add-on'))
204: ->{$prepend instanceof Html ? 'add' : 'setText'}($prepend);
205: $control->setOption('input-prepend', $prepend);
206: }
207:
208: if ($append = $control->getOption('input-append')) {
209: $append = Html::el('span', array('class' => 'add-on'))
210: ->{$append instanceof Html ? 'add' : 'setText'}($append);
211: $control->setOption('input-append', $append);
212: }
213: }
214: }
215:
216:
217:
218: 219: 220:
221: public function findErrors()
222: {
223: $formErrors = $this->form->getErrors();
224:
225: if (!$formErrors) {
226: return array();
227: }
228:
229: $form = $this->form;
230: $translate = function ($errors) use ($form) {
231: if ($translator = $form->getTranslator()) {
232: foreach ($errors as $key => $val) {
233: $errors[$key] = $translator->translate($val);
234: }
235: }
236:
237: return $errors;
238: };
239:
240: if (!$this->errorsAtInputs) {
241: return $translate($formErrors);
242: }
243:
244: return $translate($this->form->getErrors());
245: }
246:
247:
248:
249: 250: 251: 252:
253: public function findGroups()
254: {
255: $formGroups = $visitedGroups = array();
256: foreach ($this->priorGroups as $i => $group) {
257: if (!$group instanceof Nette\Forms\ControlGroup) {
258: if (!$group = $this->form->getGroup($group)) {
259: $groupName = (string)$this->priorGroups[$i];
260: throw new \RuntimeException("Form has no group $groupName.");
261: }
262: }
263:
264: $visitedGroups[] = $group;
265: if ($group = $this->processGroup($group)) {
266: $formGroups[] = $group;
267: }
268: }
269:
270: foreach ($this->form->groups as $group) {
271: if (!in_array($group, $visitedGroups, TRUE) && ($group = $this->processGroup($group))) {
272: $formGroups[] = $group;
273: }
274: }
275:
276: return $formGroups;
277: }
278:
279:
280:
281: 282: 283: 284: 285:
286: public function findControls(Nette\Forms\Container $container = NULL, $buttons = NULL)
287: {
288: $container = $container ? : $this->form;
289: return new Filter($container->getControls(), function ($control) use ($buttons) {
290: $control = $control instanceof Filter ? $control->current() : $control;
291: $isButton = $control instanceof Controls\Button || $control instanceof Nette\Forms\ISubmitterControl;
292: return !$control->getOption('rendered')
293: && !$control instanceof Controls\HiddenField
294: && (($buttons === TRUE && $isButton) || ($buttons === FALSE && !$isButton) || $buttons === NULL);
295: });
296: }
297:
298:
299:
300: 301: 302: 303: 304:
305: public function processGroup(Nette\Forms\ControlGroup $group)
306: {
307: if (!$group->getOption('visual') || !$group->getControls()) {
308: return NULL;
309: }
310:
311: $groupLabel = $group->getOption('label');
312: $groupDescription = $group->getOption('description');
313:
314:
315: if ($translator = $this->form->getTranslator()) {
316: if (!$groupLabel instanceof Html) {
317: $groupLabel = $translator->translate($groupLabel);
318: }
319: if (!$groupDescription instanceof Html) {
320: $groupDescription = $translator->translate($groupDescription);
321: }
322: }
323:
324: $controls = array_filter($group->getControls(), function (Controls\BaseControl $control) {
325: return !$control->getOption('rendered')
326: && !$control instanceof Controls\HiddenField;
327: });
328:
329: if (!$controls) {
330: return NULL;
331: }
332:
333: $groupAttrs = $group->getOption('container', Html::el())->setName(NULL);
334:
335: $groupAttrs->attrs += array_diff_key($group->getOptions(), array_fill_keys(array(
336: 'container', 'label', 'description', 'visual'
337: ), NULL));
338:
339:
340: return (object)(array(
341: 'controls' => $controls,
342: 'label' => $groupLabel,
343: 'description' => $groupDescription,
344: 'attrs' => $groupAttrs,
345: ) + $group->getOptions());
346: }
347:
348:
349:
350: 351: 352: 353: 354:
355: public static function getControlName(Controls\BaseControl $control)
356: {
357: return $control->lookupPath('Nette\Forms\Form');
358: }
359:
360:
361:
362: 363: 364: 365: 366:
367: public static function getControlDescription(Controls\BaseControl $control)
368: {
369: if (!$desc = $control->getOption('description')) {
370: return Html::el();
371: }
372:
373:
374: if (!$desc instanceof Html && ($translator = $control->form->getTranslator())) {
375: $desc = $translator->translate($desc);
376: }
377:
378:
379: return Html::el('p', array('class' => 'help-block'))
380: ->{$desc instanceof Html ? 'add' : 'setText'}($desc);
381: }
382:
383:
384:
385: 386: 387: 388: 389:
390: public function getControlError(Controls\BaseControl $control)
391: {
392: if (!($errors = $control->getErrors()) || !$this->errorsAtInputs) {
393: return Html::el();
394: }
395: $error = reset($errors);
396:
397:
398: if (!$error instanceof Html && ($translator = $control->form->getTranslator())) {
399: $error = $translator->translate($error);
400: }
401:
402:
403: return Html::el('p', array('class' => 'help-inline'))
404: ->{$error instanceof Html ? 'add' : 'setText'}($error);
405: }
406:
407:
408:
409: 410: 411: 412: 413:
414: public static function getControlTemplate(Controls\BaseControl $control)
415: {
416: return $control->getOption('template');
417: }
418:
419:
420:
421: 422: 423: 424: 425:
426: public static function isButton(Nette\Forms\IControl $control)
427: {
428: return $control instanceof Controls\Button;
429: }
430:
431:
432:
433: 434: 435: 436: 437:
438: public static function isSubmitButton(Nette\Forms\IControl $control = NULL)
439: {
440: return $control instanceof Nette\Forms\ISubmitterControl;
441: }
442:
443:
444:
445: 446: 447: 448: 449:
450: public static function isCheckbox(Nette\Forms\IControl $control)
451: {
452: return $control instanceof Controls\Checkbox;
453: }
454:
455:
456:
457: 458: 459: 460: 461:
462: public static function isRadioList(Nette\Forms\IControl $control)
463: {
464: return $control instanceof Controls\RadioList;
465: }
466:
467:
468:
469: 470: 471: 472: 473:
474: public static function isCheckboxList(Nette\Forms\IControl $control)
475: {
476: foreach (array('Nette\Forms\Controls\\', 'Kdyby\Forms\Controls\\', '',) as $ns) {
477: if (class_exists($class = $ns . 'CheckboxList', FALSE) && $control instanceof $class) {
478: return TRUE;
479: }
480: }
481:
482: return FALSE;
483: }
484:
485:
486:
487: 488: 489: 490: 491:
492: public static function getRadioListItems(Controls\RadioList $control)
493: {
494: $items = array();
495: foreach ($control->items as $key => $value) {
496: $el = $control->getControlPart($key);
497: if ($el->getName() === 'input') {
498: $items[$key] = $radio = (object) array(
499: 'input' => $el,
500: 'label' => $cap = $control->getLabelPart($key),
501: 'caption' => $cap->getText(),
502: );
503:
504: } else {
505: $items[$key] = $radio = (object) array(
506: 'input' => $el[0],
507: 'label' => $el[1],
508: 'caption' => $el[1]->getText(),
509: );
510: }
511:
512: $radio->label->addClass('radio');
513: $radio->html = clone $radio->label;
514: $radio->html->insert(0, $radio->input);
515: }
516:
517: return $items;
518: }
519:
520:
521:
522: 523: 524: 525: 526: 527:
528: public static function getCheckboxListItems(Controls\BaseControl $control)
529: {
530: $items = array();
531: foreach ($control->items as $key => $value) {
532: $el = $control->getControl($key);
533: $el[1]->addClass('checkbox')->addClass('inline');
534:
535: $items[$key] = $check = (object)array(
536: 'input' => $el[0],
537: 'label' => $el[1],
538: 'caption' => $el[1]->getText(),
539: );
540:
541: $check->html = clone $check->label;
542: $check->html->insert(0, $check->input);
543: }
544:
545: return $items;
546: }
547:
548:
549:
550: 551: 552: 553:
554: public static function getLabelBody(Controls\BaseControl $control)
555: {
556: $label = $control->getLabel();
557: return $label;
558: }
559:
560:
561:
562: 563: 564: 565: 566:
567: public static function controlHasClass(Controls\BaseControl $control, $class)
568: {
569: $classes = explode(' ', self::getClasses($control->controlPrototype));
570: return in_array($class, $classes, TRUE);
571: }
572:
573:
574:
575: 576: 577: 578: 579:
580: public static function mergeAttrs(Html $_this = NULL, array $attrs)
581: {
582: if ($_this === NULL) {
583: return Html::el();
584: }
585:
586: $_this->attrs = array_merge_recursive($_this->attrs, $attrs);
587: return $_this;
588: }
589:
590:
591:
592: 593: 594: 595:
596: private static function getClasses(Html $el)
597: {
598: if (is_array($el->class)) {
599: $classes = array_filter(array_merge(array_keys($el->class), $el->class), 'is_string');
600: return implode(' ', $classes);
601: }
602: return $el->class;
603: }
604:
605: }
606: