PageRenderTime 56ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/View/Helper/TwbFormHelper.php

https://github.com/bbcrew/Twb
PHP | 785 lines | 549 code | 122 blank | 114 comment | 101 complexity | 64873d991936945036c5f72c66d6ad18 MD5 | raw file
  1. <?php
  2. /**
  3. * FormHelper Customization Class
  4. * Twitter Bootstrap - UI Plugin
  5. *
  6. * @author mpeg
  7. */
  8. App::import( 'View/Helper', 'FormHelper' );
  9. class TwbFormHelper extends FormHelper {
  10. public $type = '';
  11. /**
  12. * Form Opening
  13. * customizes defaults for various scenario of forms
  14. */
  15. public function create($model = null, $options = array()) {
  16. $options = BB::setDefaults($options, array(
  17. 'validate' => false,
  18. 'layout' => '',
  19. 'class' => '',
  20. 'sticky' => true,
  21. 'ajax' => true,
  22. 'inputDefaults' => array(),
  23. ), array(
  24. 'boolean' => 'validate',
  25. 'else' => 'type'
  26. ));
  27. // sticky form data-option
  28. if ($options['sticky'] === true) {
  29. $options['data-twb-sticky'] = 'on';
  30. } unset($options['sticky']);
  31. // ajax form data-option
  32. if ($options['ajax'] === true) {
  33. $options['data-twb-ajax'] = 'on';
  34. } unset($options['ajax']);
  35. // apply "novalidate" options to prevent HTML5 validation
  36. if ($options['validate'] === false) {
  37. $options['novalidate'] = true;
  38. } unset($options['validate']);
  39. switch ($options['layout']) {
  40. case 'inline':
  41. $this->layout = 'inline';
  42. $options['class'] = trim('form-inline ' . $options['class']);
  43. $options['inputDefaults'] = array(
  44. 'div' => false,
  45. 'label' => false,
  46. 'error' => false
  47. );
  48. break;
  49. case 'horizontal':
  50. $this->layout = 'horizontal';
  51. $options['class'] = trim('form-horizontal ' . $options['class']);
  52. $options['inputDefaults'] = BB::extend(array(
  53. 'div' => array(
  54. 'class' => 'control-group'
  55. ),
  56. 'label' => array(
  57. 'class' => 'control-label'
  58. ),
  59. 'error' => array(
  60. 'attributes' => array(
  61. 'wrap' => 'div',
  62. 'class' => 'help-inline'
  63. )
  64. ),
  65. 'between' => '<div class="controls">',
  66. 'after' => '</div>',
  67. 'format' => array('before', 'label', 'between', 'input', 'error', 'after' )
  68. ), $options['inputDefaults']);
  69. break;
  70. case 'standard':
  71. default:
  72. $this->layout = 'standard';
  73. $options['class'] = trim('form-standard ' . $options['class']);
  74. $options['inputDefaults'] = BB::extend(array(
  75. 'div' => array(
  76. 'class' => 'control-group'
  77. ),
  78. 'error' => array(
  79. 'attributes' => array(
  80. 'wrap' => 'div',
  81. 'class' => 'help-inline'
  82. )
  83. ),
  84. 'between' => '<div class="controls">',
  85. 'after' => '</div>',
  86. 'format' => array('before', 'label', 'between', 'input', 'after', 'error' )
  87. ), $options['inputDefaults']);
  88. break;
  89. }
  90. return parent::create($model, BB::clear($options, array(
  91. 'layout'
  92. )));
  93. }
  94. /**
  95. * Close Form
  96. * handles multiple action buttons.
  97. *
  98. * $options = 'save,exit,cancel'
  99. * $options = array('save', 'exit')
  100. * $options = array('save' => 'custom label')
  101. * $options = array(
  102. * 'action' => 'save, exit'
  103. * )
  104. *
  105. *
  106. */
  107. public function end($options = null) {
  108. $actions = array();
  109. // fetch options and translates into options and actions array
  110. if (func_num_args() == 1) {
  111. if (is_string($options)) {
  112. $actions = BB::extend($actions, explode(',', $options));
  113. $options = array();
  114. } elseif (BB::isVector($options) || (BB::isAssoc($options) && !isset($options['actions']))) {
  115. $actions = $options;
  116. $options = array();
  117. }
  118. $options = BB::setDefaults($options, array(
  119. 'actions' => array()
  120. ));
  121. if (is_string($options['actions'])) {
  122. $options['actions'] = explode(',', $options['actions']);
  123. }
  124. $actions = BB::extend($actions, $options['actions']);
  125. unset($options['actions']);
  126. } else {
  127. $actions = func_get_args();
  128. }
  129. // render actions
  130. $ractions = '';
  131. if ($actions === false) $actions = array();
  132. foreach ($actions as $actionName => $action) {
  133. // action button from string!
  134. if (is_string($action)) {
  135. $ActionName = trim($action);
  136. $actionName = strtolower($ActionName);
  137. switch ($actionName) {
  138. case 'save':
  139. $action = array(
  140. 'value' => __('save'),
  141. 'name' => 'form-action-save',
  142. );
  143. break;
  144. case 'saveexit':
  145. $action = array(
  146. 'value' => __('save and exit'),
  147. 'name' => 'form-action-save-and-exit',
  148. );
  149. break;
  150. case 'exit':
  151. $action = array(
  152. 'value' => __('save and exit'),
  153. 'name' => 'form-action-save-and-exit',
  154. );
  155. break;
  156. case 'cancel':
  157. $action = array(
  158. 'value' => __($actionName),
  159. 'name' => 'form-action-cancel'
  160. );
  161. if (empty($options['group'])) {
  162. $action['$++class'] = ' btn-link';
  163. }
  164. break;
  165. case 'reset':
  166. $action = array(
  167. 'value' => __($actionName),
  168. 'name' => 'form-action-reset',
  169. 'type' => 'reset'
  170. );
  171. if (empty($options['group'])) {
  172. $action['$++class'] = ' btn-link';
  173. }
  174. break;
  175. default:
  176. $action = array(
  177. 'value' => __($ActionName),
  178. 'name' => $actionName
  179. );
  180. }
  181. // apply danger
  182. if (substr($ActionName, 0, 2) == '!!') {
  183. $action['$++class'] = ' btn-danger';
  184. if (substr($action['value'], 0, 2) == '!!') {
  185. $action['value'] = substr($action['value'], 2);
  186. }
  187. // apply warning
  188. } elseif (substr($ActionName, 0, 1) == '!') {
  189. $action['$++class'] = ' btn-warning';
  190. if (substr($action['value'], 0, 1) == '!') {
  191. $action['value'] = substr($action['value'], 1);
  192. }
  193. // apply info
  194. } elseif (substr($ActionName, 0, 1) == '?') {
  195. $action['$++class'] = ' btn-info';
  196. if (substr($action['value'], 0, 1) == '?') {
  197. $action['value'] = substr($action['value'], 1);
  198. }
  199. // apply success
  200. } elseif (substr($ActionName, 0, 1) == '+') {
  201. $action['$++class'] = ' btn-success';
  202. if (substr($action['value'], 0, 1) == '+') {
  203. $action['value'] = substr($action['value'], 1);
  204. }
  205. // apply primary
  206. } elseif (substr($ActionName, 0, 1) === strtoupper(substr($actionName, 0, 1))) {
  207. $action['$++class'] = ' btn-primary';
  208. }
  209. // action's defaults
  210. $action = BB::extend(array(
  211. 'type' => 'submit',
  212. 'class' => 'btn',
  213. 'div' => false
  214. ), $action);
  215. } else {
  216. $action = BB::extend(array(
  217. 'type' => 'submit',
  218. 'value' => $actionName,
  219. 'name' => $actionName,
  220. 'class' => 'btn',
  221. 'div' => false
  222. ), BB::set($action, 'value'));
  223. }
  224. // append blank char to separate buttons if no group is appliedy
  225. $ractions.= $this->submit(
  226. $action['value'],
  227. BB::clear(
  228. $action,
  229. array(
  230. 'value',
  231. 'tag'
  232. ),
  233. false
  234. )
  235. ) . ' ';
  236. }
  237. // apply defaults
  238. $options = BB::extend(array(
  239. 'group' => '',
  240. 'class' => '',
  241. 'before' => '',
  242. 'between' => '',
  243. 'after' => ''
  244. ), $options);
  245. // apply btn group if required
  246. // "group" options should be "true" or string with class to apply
  247. // to actions wrapper (useful for vertical groups)
  248. if (count($actions) > 1 && $options['group']) {
  249. if (!is_string($options['group'])) {
  250. $options['group'] = 'btn-group';
  251. }
  252. $ractions = $this->Html->tag(array(
  253. 'class' => $options['group'],
  254. 'content' => $ractions
  255. ));
  256. }
  257. // extract mishellaneous code injection vars
  258. $before = $options['before'];
  259. $between = $options['between'];
  260. $after = $options['after'];
  261. // clean up options
  262. $options = BB::clear($options, array(
  263. 'group',
  264. 'before',
  265. 'between',
  266. 'after'
  267. ), false);
  268. // setup base actions class
  269. $options['class'] = trim('form-actions ' . $options['class']);
  270. // apply wrapper
  271. $wrapper = $this->Html->tag(BB::extend(array(
  272. 'content' => array(
  273. $between,
  274. $ractions,
  275. $after
  276. )
  277. ), $options));
  278. return $before . $wrapper . parent::end(null);
  279. }
  280. /**
  281. * Implement generic form input field following form's style.
  282. */
  283. public function input($name = '', $options = array()) {
  284. $options = BB::set($options, 'label');
  285. // checkbox
  286. if (!empty($options['type']) && $options['type'] == 'checkbox') {
  287. return $this->checkbox($name, $options);
  288. }
  289. // radio
  290. if (!empty($options['type']) && $options['type'] == 'radio') {
  291. $options = BB::extend(array('options' => array()), $options);
  292. return $this->radio($name, $options['options'], BB::clear($options, 'options'));
  293. }
  294. // upload
  295. if (!empty($options['type']) && $options['type'] == 'upload') {
  296. return $this->upload($name, $options);
  297. }
  298. // accept "typeahead" data suggestions
  299. if (isset($options['typeahead'])) {
  300. $options['data-provide'] = 'typeahead';
  301. $options['data-items'] = count($options['typeahead']);
  302. $options['data-source'] = 'typeahead';
  303. $typeaheads = '[';
  304. foreach($options['typeahead'] as $tmp) $typeaheads.= '"' . $tmp . '",';
  305. $typeaheads = substr($typeaheads, 0, strlen($typeaheads)-1) . ']';
  306. unset($options['typeahead']);
  307. $append = '<a href="#" onclick="return false;" class="btn typeahead-show" type="button"><i class="icon-chevron-down"></i></a>';
  308. if (empty($options['append'])) {
  309. $options['append'] = array();
  310. }
  311. if (is_array($options['append'])) {
  312. $options['append'][] = $append;
  313. } else {
  314. $options['append'].= $append;
  315. }
  316. }
  317. // "autofocus" option
  318. if (isset($options['autofocus'])) {
  319. $options['data-twb-autofocus'] = 'on';
  320. unset($options['autofocus']);
  321. }
  322. // data-autosize special options
  323. if (isset($options['autosize'])) {
  324. $options['data-twb-autosize'] = 'on';
  325. unset($options['autosize']);
  326. }
  327. $options = $this->_labelOptions($options);
  328. $options = $this->_helperOptions($options);
  329. // apply form's type based variations
  330. switch ($this->layout) {
  331. case 'inline':
  332. if (empty($options['placeholder'])) {
  333. $options['placeholder'] = $options['label']['text'];
  334. }
  335. $options['label'] = false;
  336. break;
  337. }
  338. // fill with default options
  339. $options = BB::extend($this->_inputDefaults, $options);
  340. $wrap = array(
  341. 'class' => '',
  342. 'content' => array('$$__SPLIT__$$')
  343. );
  344. // "prepend" items to the field
  345. if (isset($options['prepend'])) {
  346. $wrap['class'].= 'input-prepend ';
  347. if (!is_array($options['prepend'])) $options['prepend'] = array($options['prepend']);
  348. $wrap['content'] = BB::extend($options['prepend'], $wrap['content']);
  349. unset($options['prepend']);
  350. }
  351. // "append" items to the field
  352. if (isset($options['append'])) {
  353. $wrap['class'].= 'input-append';
  354. if (!is_array($options['append'])) $options['append'] = array($options['append']);
  355. $wrap['content'] = BB::extend($wrap['content'], $options['append']);
  356. unset($options['append']);
  357. }
  358. if (count($wrap['content']) > 1) {
  359. $wrap = explode('$$__SPLIT__$$', $this->Html->tag($wrap));
  360. $options['between'].= $wrap[0];
  361. $options['after'] = $wrap[1] . $options['after'];
  362. }
  363. // render the field
  364. $field = parent::input($name, $options);
  365. // fix "typeahead" data source values
  366. if (isset($typeaheads)) {
  367. $field = str_replace('data-source="typeahead"', 'data-source=\''.$typeaheads.'\'', $field);
  368. }
  369. return $field;
  370. }
  371. /**
  372. * Custom Checkbox
  373. */
  374. public function checkbox($fieldName, $options = array()) {
  375. // apply label defaults
  376. $options = $this->_labelOptions($options);
  377. // apply defaults
  378. $options = BB::extend(array(
  379. 'type' => 'checkbox',
  380. 'value' => 1,
  381. 'options' => array(),
  382. 'inline' => false
  383. ), $options);
  384. // input wrapper options:
  385. $_options = BB::extend(BB::clear($options, array(
  386. 'value',
  387. 'options',
  388. 'inline',
  389. 'helper',
  390. ), false), array(
  391. 'type' => 'text'
  392. ));
  393. // style fix
  394. // standard's form single checkbox display itself inline without
  395. // control's label!
  396. if ($this->layout === 'standard' && empty($options['options'])) {
  397. $options['options'] = array($fieldName => array(
  398. 'text' => !empty($options['label']['text'])?$options['label']['text']:$fieldName,
  399. 'helper' => !empty($options['helper'])?$options['helper']:''
  400. ));
  401. $_options['label'] = false;
  402. }
  403. // multiple checkboxes
  404. $items = '';
  405. if (!empty($options['options'])) {
  406. foreach ($options['options'] as $itemName=>$config) {
  407. // apply defaults for label text to display near field
  408. $config = BB::setDefaults($config, array(
  409. 'text' => ''
  410. ), 'text');
  411. // export checkbox text
  412. $itemText = $config['text'];
  413. unset($config['text']);
  414. if (empty($itemText)) {
  415. $itemText = $itemName;
  416. }
  417. if (is_numeric($itemName)) {
  418. $itemName = $itemText;
  419. }
  420. // extends item's config with wide control configuration
  421. // setup single checkout value
  422. $config = BB::extend(BB::clear($options, array('options', 'inline')), $config);
  423. $config = $this->_helperOptions($config);
  424. // find control's ID to apply "label for" attribute
  425. $itemControl = parent::checkbox($itemName, $config);
  426. $itemControlId = '';
  427. $matches = '';
  428. preg_match_all("|id=\"(.*)\"|U", $itemControl, $matches);
  429. if (!empty($matches[1][1])) $itemControlId = $matches[1][1];
  430. // create checkbox control with label
  431. $item = $this->Html->tag(array(
  432. 'tag' => 'label',
  433. 'for' => $itemControlId,
  434. 'class' => 'checkbox' . ($options['inline']?' inline':''),
  435. 'data-toggle-checkbox' => 'on',
  436. 'content' => array(
  437. $itemControl,
  438. $itemText
  439. )
  440. ));
  441. $items.= $item;
  442. }
  443. // single checkbox
  444. } else {
  445. $options = $this->_helperOptions($options);
  446. $items = parent::checkbox($fieldName, $options);
  447. }
  448. // generate input wrapper code and fill with checkbox core
  449. $field = $this->input($fieldName, $_options);
  450. preg_match_all("|<input(.*)/>|U", $field, $matches);
  451. return str_replace( $matches[0][0], $items, $field );
  452. }
  453. /**
  454. * Custom Radio
  455. */
  456. public function radio($fieldName, $options = array(), $attributes = array()) {
  457. // apply custom label as second string param (shortcut)
  458. if (is_string($options)) {
  459. $attributes = $options;
  460. $options = array();
  461. }
  462. // internal naming conventions normalization
  463. // method declaration must be compatible to parent class (PHP 5.4)
  464. $values = $options;
  465. $options = $attributes;
  466. // apply label defaults
  467. // "true" options = inline
  468. if ($options === true) $options = array('inline' => true);
  469. $options = $this->_labelOptions($options);
  470. // apply defaults
  471. $options = BB::extend(array(
  472. 'type' => 'radio',
  473. 'inline' => false
  474. ), $options);
  475. // input wrapper options:
  476. $_options = BB::extend(BB::clear($options, array(
  477. 'inline',
  478. 'helper',
  479. ), false), array(
  480. 'type' => 'text'
  481. ));
  482. // default value for single radio button
  483. if (empty($values)) {
  484. $values = array($fieldName => $fieldName);
  485. }
  486. // build CakePHP compliant value array from rich Twb definition
  487. // compatible form.
  488. $_values = array();
  489. foreach ($values as $key=>$val) {
  490. if (is_array($val)) {
  491. $val = BB::extend(array(
  492. 'text' => $key
  493. ), $val);
  494. $values[$key] = $val;
  495. } else {
  496. $val = array('text' => $val);
  497. $values[$key] = $val;
  498. }
  499. if (is_numeric($key)) {
  500. $_values[$val['text']] = $val['text'];
  501. } else {
  502. $_values[$key] = $val['text'];
  503. }
  504. }
  505. // build original radio fields list
  506. $radio = $radio = parent::radio($fieldName, $_values, array(
  507. 'legend' => false,
  508. 'separator' => '--**--'
  509. ));
  510. // modify markup to fit Twb requirements
  511. $items = '';
  512. $keys = array_keys($values);
  513. foreach (explode('--**--', $radio) as $i=>$radio) {
  514. preg_match_all("|<label(.*)>|U", $radio, $matches);
  515. $radio = str_replace( $matches[0][0], '', $radio );
  516. $radio = $matches[0][0] . $radio;
  517. // compose helper attributes
  518. $helper_attrs = '';
  519. if (!empty($values[$keys[$i]]['helper'])) {
  520. foreach ($this->_helperOptions(array('type' => 'radio', 'helper' => $values[$keys[$i]]['helper'])) as $attrName=>$attrVal) {
  521. $helper_attrs.= $attrName.'="'.$attrVal.'" ';
  522. }
  523. }
  524. // apply class and inline attribute
  525. $radio = str_replace('<label', '<label class="radio' . ($options['inline']===true?' inline':'') . '" ', $radio);
  526. $radio = str_replace('<input ', '<input ' . $helper_attrs, $radio);
  527. $items.= $radio;
  528. }
  529. // generate input wrapper code and fill with checkbox core
  530. $field = $this->input($fieldName, $_options);
  531. preg_match_all("|<input(.*)/>|U", $field, $matches);
  532. return str_replace( $matches[0][0], $items, $field );
  533. }
  534. /**
  535. * display an upload control box to handle upload through BbAttachment
  536. * and display a preview rapresentation for uploaded file
  537. */
  538. public function upload($fieldName, $options = array()) {
  539. if (strpos($fieldName, '.') !== false) {
  540. list($_model, $_field) = explode('.', $fieldName);
  541. } else {
  542. $_model = $this->defaultModel;
  543. $_field = $fieldName;
  544. }
  545. // Handle "Model.{n}.fieldName" fields
  546. if (is_numeric($_field)) {
  547. list($_model, $_idx, $_field) = explode('.', $fieldName);
  548. }
  549. $options = BB::extend(array(
  550. 'previewImage' => '',
  551. 'previewPath' => ___BbAttachmentDefaultUploadDir__,
  552. 'previewSize' => null,
  553. 'previewOptions'=> array()
  554. ), $options, array(
  555. 'type' => 'file',
  556. 'div' => array(
  557. 'data-twb-upload' => 'on'
  558. )
  559. ));
  560. // extract image preview options
  561. $previewImage = $options['previewImage'];
  562. $previewPath = $options['previewPath'];
  563. $previewSize = $options['previewSize'];
  564. $previewOptions = $options['previewOptions'];
  565. BB::clear($options, array(
  566. 'previewImage',
  567. 'previewPath',
  568. 'previewSize',
  569. 'previewOptions'
  570. ), false);
  571. // compose preview image with request data set
  572. if (empty($previewImage) || !file_exists($previewImage)) {
  573. $uploadModel = $_model . Inflector::camelize($_field);
  574. // compose upload field name handling "{n}.fieldName" form
  575. $uploadField = '.full_path';
  576. if (isset($_idx)) $uploadField = '.' . $_idx . $uploadField;
  577. $previewImage = $this->Html->fileIcon($this->value(false, $uploadModel.$uploadField), $previewSize, $previewOptions);
  578. }
  579. // insert preview image inside template, it force to overrides
  580. // standard field format
  581. if (isset($options['between'])) {
  582. $options['$__between'] = $options['between'] . '<div class="twb-upload-preview">' . $previewImage . '</div>';
  583. } else {
  584. $options['$__between'] = '<div class="controls"><div class="twb-upload-preview">' . $previewImage . '</div>';
  585. }
  586. if (isset($options['after'])) {
  587. $options['$__after'] = $options['after'] . '';
  588. } else {
  589. $options['$__after'] = '</div>';
  590. }
  591. return $this->input($fieldName, BB::clear($options, array('between', 'after'), false));
  592. }
  593. protected function _labelOptions($options) {
  594. // string options converted to label
  595. $options = BB::setDefaults($options, array(
  596. 'label' => array()
  597. ), 'label');
  598. // string label converted to array configuration
  599. $options['label'] = BB::setDefaults($options['label'], null, 'text');
  600. return $options;
  601. }
  602. /**
  603. * Inject popover helper attributes into configuration
  604. */
  605. protected function _helperOptions($options) {
  606. if (array_key_exists('helper', $options)) {
  607. $trigger = 'focus';
  608. if (!empty($options['type']) && in_array($options['type'], array('checkbox', 'radio', 'select'))) {
  609. $trigger.= ' hover';
  610. }
  611. $position = 'right';
  612. if (!empty($options['type']) && in_array($options['type'], array('checkbox', 'radio', 'select'))) {
  613. $position = $this->layout == 'horizontal' ? 'top' : 'right';
  614. }
  615. // helper defaults
  616. $helper = BB::setDefaults($options['helper'], array(
  617. 'title' => '',
  618. 'content' => '',
  619. 'position' => $position,
  620. 'trigger' => $trigger,
  621. 'html' => true
  622. ), 'content');
  623. // mobile targetized options
  624. if ($this->request->is('mobile')) {
  625. if (isset($helper['mobile']) && $helper['mobile'] == false) {
  626. return BB::clear($options, 'helper', false);
  627. }
  628. if (isset($helper['mobile-position'])) {
  629. $helper['position'] = $helper['mobile-position'];
  630. }
  631. if (isset($helper['mobile-title'])) {
  632. $helper['title'] = $helper['mobile-title'];
  633. }
  634. if (isset($helper['mobile-content'])) {
  635. $helper['content'] = $helper['mobile-content'];
  636. }
  637. }
  638. // fetch title from content's text
  639. if (empty($helper['title']) && strpos($helper['content'], '>>') !== false) {
  640. $tmp = explode('>>', $helper['content']);
  641. $helper['title'] = rtrim(array_shift($tmp));
  642. $helper['content'] = ltrim(implode('>>', $tmp));
  643. }
  644. // fill popover data-attributes
  645. $options['data-helper'] = 'on';
  646. if (!empty($helper['title'])) $options['data-original-title'] = $helper['title'];
  647. if (!empty($helper['content'])) $options['data-content'] = $helper['content'];
  648. if (!empty($helper['position'])) $options['data-placement'] = $helper['position'];
  649. if (!empty($helper['trigger'])) $options['data-trigger'] = $helper['trigger'];
  650. if (!empty($helper['html'])) $options['data-html'] = $helper['html'];
  651. }
  652. return BB::clear($options, 'helper', false);
  653. }
  654. public function originalInput($fieldName, $options = array()) {
  655. return parent::input($fieldName, $options);
  656. }
  657. public function originalCheckbox($fieldName, $options = array()) {
  658. return parent::checkbox($fieldName, $options);
  659. }
  660. public function originalRadio($fieldName, $options = array(), $attributes = array()) {
  661. return parent::radio($fieldName, $options, $attributes);
  662. }
  663. }