paragraphs.field_widget.inc 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094
  1. <?php
  2. /**
  3. * @file
  4. * Holds functions for the paragraphs widgets.
  5. */
  6. /**
  7. * Implements hook_field_widget_info().
  8. */
  9. function paragraphs_field_widget_info() {
  10. return array(
  11. 'paragraphs_hidden' => array(
  12. 'label' => t('Hidden'),
  13. 'field types' => array('paragraphs'),
  14. 'behaviors' => array(
  15. 'multiple values' => FIELD_BEHAVIOR_CUSTOM,
  16. 'default value' => FIELD_BEHAVIOR_NONE,
  17. ),
  18. ),
  19. 'paragraphs_embed' => array(
  20. 'label' => t('Embedded'),
  21. 'field types' => array('paragraphs'),
  22. 'behaviors' => array(
  23. 'multiple values' => FIELD_BEHAVIOR_CUSTOM,
  24. 'default value' => FIELD_BEHAVIOR_NONE,
  25. ),
  26. ),
  27. );
  28. }
  29. /**
  30. * Implements hook_field_widget_form().
  31. */
  32. function paragraphs_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  33. switch ($instance['widget']['type']) {
  34. case 'paragraphs_hidden':
  35. return $element;
  36. break;
  37. case 'paragraphs_embed':
  38. return paragraphs_field_multiple_value_form($field, $instance, $langcode, $items, $form, $form_state, $delta, $element);
  39. break;
  40. }
  41. }
  42. /**
  43. * Special handling to create form elements for multiple values.
  44. *
  45. * Handles generic features for multiple fields:
  46. * - number of widgets
  47. * - AHAH-'add more' button
  48. * - drag-n-drop value reordering
  49. */
  50. function paragraphs_field_multiple_value_form($field, $instance, $langcode, $items, &$form, &$form_state, $delta, $original_element) {
  51. $field_name = $field['field_name'];
  52. $parents = $form['#parents'];
  53. $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
  54. $max = $field_state['items_count'] - 1;
  55. $title = check_plain($instance['label']);
  56. $description = field_filter_xss($instance['description']);
  57. $id_prefix = implode('-', array_merge($parents, array($field_name)));
  58. $wrapper_id = drupal_html_id($id_prefix . '-add-more-wrapper');
  59. $field_elements = array();
  60. $function = $instance['widget']['module'] . '_field_widget_form_build';
  61. $had_first = FALSE;
  62. $actual_item_count = 0;
  63. if (function_exists($function)) {
  64. for ($delta = 0; $delta <= $max; $delta++) {
  65. $multiple = TRUE;
  66. $element = array(
  67. '#entity_type' => $original_element['#entity_type'],
  68. '#entity' => $original_element['#entity'],
  69. '#bundle' => $original_element['#bundle'],
  70. '#field_name' => $field_name,
  71. '#language' => $langcode,
  72. '#field_parents' => $parents,
  73. '#columns' => array_keys($field['columns']),
  74. // For multiple fields, title and description are handled by the wrapping table.
  75. '#title' => $multiple ? '' : $title,
  76. '#description' => $multiple ? '' : $description,
  77. '#delta' => $delta,
  78. '#weight' => $delta,
  79. );
  80. if ($element = $function($form, $form_state, $field, $instance, $langcode, $items, $delta, $element)) {
  81. // Input field for the delta (drag-n-drop reordering).
  82. if ($multiple) {
  83. // We name the element '_weight' to avoid clashing with elements
  84. // defined by widget.
  85. $element['_weight'] = array(
  86. '#type' => 'weight',
  87. '#title' => t('Weight for row @number', array('@number' => $delta + 1)),
  88. '#title_display' => 'invisible',
  89. // Note: this 'delta' is the FAPI 'weight' element's property.
  90. '#delta' => $max,
  91. '#default_value' => isset($items[$delta]['_weight']) ? $items[$delta]['_weight'] : $delta,
  92. '#weight' => 100,
  93. );
  94. }
  95. // Because our deleted elements are still in the form, only the first showed element is required.
  96. if (!$had_first && (!isset($element['#access']) || $element['#access'])) {
  97. $had_first = TRUE;
  98. $element['#required'] = $instance['required'];
  99. }
  100. if (!isset($element['#access']) || $element['#access']) {
  101. $actual_item_count++;
  102. }
  103. // Allow modules to alter the field widget form element.
  104. $context = array(
  105. 'form' => $form,
  106. 'field' => $field,
  107. 'instance' => $instance,
  108. 'langcode' => $langcode,
  109. 'items' => $items,
  110. 'delta' => $delta,
  111. );
  112. drupal_alter(array('paragraphs_field_widget_form', 'paragraphs_field_widget_' . $instance['widget']['type'] . '_form'), $element, $form_state, $context);
  113. $field_elements[$delta] = $element;
  114. }
  115. }
  116. $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
  117. $field_state['real_items_count'] = $actual_item_count;
  118. field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state);
  119. $field_elements += array(
  120. '#theme' => 'paragraphs_field_multiple_value_form',
  121. '#field_name' => $field['field_name'],
  122. '#cardinality' => $field['cardinality'],
  123. '#title' => $title,
  124. '#required' => $instance['required'],
  125. '#description' => $description,
  126. '#prefix' => '<div id="' . $wrapper_id . '">',
  127. '#suffix' => '</div>',
  128. '#max_delta' => $max,
  129. '#instance' => $instance,
  130. );
  131. // Add 'add more' button, if not working with a programmed form.
  132. if (empty($form_state['programmed'])) {
  133. $available_bundles = paragraphs_bundle_load();
  134. $select_bundles = array();
  135. $select_bundles_weighted = array();
  136. // By default, consider that no bundle has been explicitly picked.
  137. $explicitly_enabled = FALSE;
  138. foreach ($instance['settings']['allowed_bundles'] as $allowed_bundle_key => $allowed_bundle_value) {
  139. if ($allowed_bundle_key === $allowed_bundle_value && isset($available_bundles[$allowed_bundle_key])) {
  140. $select_bundles[$available_bundles[$allowed_bundle_key]->bundle] = $available_bundles[$allowed_bundle_key]->name;
  141. // If an item has been explicitly selected, raise our flag.
  142. $explicitly_enabled = TRUE;
  143. }
  144. elseif (isset($available_bundles[$allowed_bundle_key]->bundle)) {
  145. $select_bundles_weighted[$available_bundles[$allowed_bundle_key]->bundle] = $available_bundles[$allowed_bundle_key]->name;
  146. }
  147. }
  148. // If no bundle has been explicitly selected, give access to all of them.
  149. if (!$explicitly_enabled) {
  150. $select_bundles = $select_bundles_weighted;
  151. foreach ($available_bundles as $bundle) {
  152. if (!isset($select_bundles[$bundle->bundle])) {
  153. $select_bundles[$bundle->bundle] = $bundle->name;
  154. }
  155. }
  156. }
  157. $removed_a_bundle = FALSE;
  158. $weight = 0;
  159. foreach ($select_bundles as $machine_name => $bundle) {
  160. $select_bundles[$machine_name] = array(
  161. 'name' => $bundle,
  162. 'weight' => $weight,
  163. );
  164. /* @var $entity_shell ParagraphsItemEntity */
  165. $entity_shell = entity_create('paragraphs_item', array('bundle' => $machine_name, 'field_name' => $field_name));
  166. $entity_shell->setHostEntity($original_element['#entity_type'], $original_element['#entity'], $langcode, FALSE);
  167. if (!entity_access('create', 'paragraphs_item', $entity_shell)) {
  168. unset($select_bundles[$machine_name]);
  169. $removed_a_bundle = TRUE;
  170. }
  171. elseif (isset($instance['settings']['bundle_weights'][$machine_name])) {
  172. $select_bundles[$machine_name]['weight'] = $instance['settings']['bundle_weights'][$machine_name];
  173. }
  174. $weight++;
  175. }
  176. if($removed_a_bundle && count($select_bundles) === 0) {
  177. $field_elements['add_more'] = array(
  178. '#type' => 'container',
  179. '#tree' => TRUE,
  180. );
  181. $field_elements['add_more']['add_more'] = array(
  182. '#type' => 'markup',
  183. '#markup' => '<em>' . t('You are not allowed to add any of the bundles.') . '</em>',
  184. );
  185. }
  186. elseif (count($select_bundles)) {
  187. uasort($select_bundles, 'drupal_sort_weight');
  188. $field = $field_state['field'];
  189. if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $field_state['real_items_count'] < $field['cardinality']) {
  190. if (!isset($instance['settings']['title'])) {
  191. $instance['settings']['title'] = PARAGRAPHS_DEFAULT_TITLE;
  192. }
  193. $field_elements['add_more'] = array(
  194. '#type' => 'container',
  195. '#tree' => TRUE,
  196. );
  197. $add_mode = (isset($instance['settings']['add_mode']) ? $instance['settings']['add_mode'] : PARAGRAPHS_DEFAULT_ADD_MODE);
  198. if ($add_mode == 'button') {
  199. foreach ($select_bundles as $machine_name => $bundle) {
  200. /* @var $entity_shell ParagraphsItemEntity */
  201. $entity_shell = entity_create('paragraphs_item', array('bundle' => $machine_name, 'field_name' => $field_name));
  202. $entity_shell->setHostEntity($original_element['#entity_type'], $original_element['#entity'], $langcode, FALSE);
  203. $field_elements['add_more']['add_more_bundle_' . $machine_name] = array(
  204. '#type' => 'submit',
  205. '#name' => strtr($id_prefix, '-', '_') . '_add_more_add_more_bundle_' . $machine_name,
  206. '#value' => t('Add !title', array('!title' => $bundle['name'])),
  207. '#access' => entity_access('create', 'paragraphs_item', $entity_shell),
  208. '#attributes' => array('class' => array('field-add-more-submit', 'paragraphs-add-more-submit')),
  209. '#limit_validation_errors' => array(),
  210. '#submit' => array('paragraphs_add_more_submit'),
  211. '#ajax' => array(
  212. 'callback' => 'paragraphs_add_more_js',
  213. 'wrapper' => $wrapper_id,
  214. 'effect' => 'fade',
  215. ),
  216. );
  217. }
  218. }
  219. else {
  220. uasort($select_bundles, 'drupal_sort_weight');
  221. $select_list = array();
  222. foreach ($select_bundles as $machine_name => $bundle) {
  223. $select_list[$machine_name] = $bundle['name'];
  224. }
  225. $field_elements['add_more']['type'] = array(
  226. '#type' => 'select',
  227. '#name' => strtr($id_prefix, '-', '_') . '_add_more_type',
  228. '#title' => t('!title type', array('!title' => t($instance['settings']['title']))),
  229. '#options' => $select_list,
  230. '#attributes' => array('class' => array('field-add-more-type')),
  231. '#limit_validation_errors' => array(array_merge($parents, array($field_name, $langcode))),
  232. );
  233. // Hide the bundle selection if only one bundle is allowed.
  234. if (count($select_list) == 1) {
  235. $field_elements['add_more']['type']['#type'] = 'hidden';
  236. $keys = array_keys($select_list);
  237. $field_elements['add_more']['type']['#value'] = $keys[0];
  238. }
  239. if (isset($form_state['input'][strtr($id_prefix, '-', '_') . '_add_more_type'])) {
  240. $field_elements['add_more']['type']['#default_value'] = $form_state['input'][strtr($id_prefix, '-', '_') . '_add_more_type'];
  241. }
  242. $text = 'Add new !title';
  243. if ($max >= 0) {
  244. $text = 'Add another !title';
  245. }
  246. $field_elements['add_more']['add_more'] = array(
  247. '#type' => 'submit',
  248. '#name' => strtr($id_prefix, '-', '_') . '_add_more_add_more',
  249. '#value' => t($text, array('!title' => t($instance['settings']['title']))),
  250. '#attributes' => array('class' => array('field-add-more-submit', 'paragraphs-add-more-submit')),
  251. '#limit_validation_errors' => array(),
  252. '#submit' => array('paragraphs_add_more_submit'),
  253. '#ajax' => array(
  254. 'callback' => 'paragraphs_add_more_js',
  255. 'wrapper' => $wrapper_id,
  256. 'effect' => 'fade',
  257. ),
  258. );
  259. }
  260. }
  261. }
  262. else {
  263. $field_elements['add_more']['add_more'] = array(
  264. '#type' => 'markup',
  265. '#markup' => '<em>' . t('No bundles available, edit field settings') . '</em>',
  266. );
  267. }
  268. }
  269. }
  270. if (module_exists('file')) {
  271. // file.js triggers uploads when the main Submit button is clicked.
  272. $field_elements['#attached']['js'] = array(
  273. drupal_get_path('module', 'file') . '/file.js',
  274. array('data' => drupal_get_path('module', 'paragraphs') . '/paragraphs.js', 'type' => 'file', 'weight' => 9999),
  275. );
  276. $form_state['has_file_element'] = TRUE;
  277. }
  278. return $field_elements;
  279. }
  280. /**
  281. * Widget form implementation for paragraphs.
  282. *
  283. * @param $form
  284. * @param $form_state
  285. * @param $field
  286. * @param $instance
  287. * @param $langcode
  288. * @param $items
  289. * @param $delta
  290. * @param $element
  291. *
  292. * @return array
  293. */
  294. function paragraphs_field_widget_form_build(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  295. static $recursion = 0;
  296. if (!isset($instance['settings']['title'])) {
  297. $instance['settings']['title'] = PARAGRAPHS_DEFAULT_TITLE;
  298. }
  299. if (!isset($instance['settings']['title_multiple'])) {
  300. $instance['settings']['title_multiple'] = PARAGRAPHS_DEFAULT_TITLE_MULTIPLE;
  301. }
  302. // If the paragraph item form contains another paragraph,
  303. // we might ran into a recursive loop. Prevent that.
  304. if ($recursion++ > PARAGRAPHS_RECURSION_LIMIT) {
  305. drupal_set_message(t('The paragraphs item form has not been embedded to avoid recursive loops.'), 'error');
  306. return $element;
  307. }
  308. $field_parents = $element['#field_parents'];
  309. $field_name = $element['#field_name'];
  310. $language = $element['#language'];
  311. $bundle = FALSE;
  312. $id_prefix = implode('-', array_merge($field_parents, array($field_name)));
  313. if (isset($form_state['input'][strtr($id_prefix, '-', '_') . '_add_more_type'])) {
  314. $bundle = $form_state['input'][strtr($id_prefix, '-', '_') . '_add_more_type'];
  315. }
  316. elseif (isset($form_state['input']['_triggering_element_name'])) {
  317. if (strpos($form_state['input']['_triggering_element_name'], strtr($id_prefix, '-', '_') . '_add_more_add_more_bundle_') === 0) {
  318. $bundle = substr($form_state['input']['_triggering_element_name'], drupal_strlen(strtr($id_prefix, '-', '_') . '_add_more_add_more_bundle_'));
  319. }
  320. }
  321. // Nest the paragraphs item entity form in a dedicated parent space,
  322. // by appending [field_name, langcode, delta] to the current parent space.
  323. // That way the form values of the paragraphs item are separated.
  324. $parents = array_merge($field_parents, array($field_name, $language, $delta));
  325. $element += array(
  326. '#element_validate' => array('paragraphs_field_widget_embed_validate'),
  327. '#parents' => $parents,
  328. );
  329. $field_state = field_form_get_state($field_parents, $field_name, $language, $form_state);
  330. $deleted_paragraph = FALSE;
  331. $confirmed_deleted_paragraph = FALSE;
  332. $is_new_paragraph = FALSE;
  333. $default_edit_mode = isset($instance['settings']['default_edit_mode']) ? $instance['settings']['default_edit_mode'] : PARAGRAPHS_DEFAULT_EDIT_MODE;
  334. $being_edited_paragraph = TRUE;
  335. if ($default_edit_mode === 'closed' || $default_edit_mode === 'preview') {
  336. $being_edited_paragraph = FALSE;
  337. }
  338. if (isset($field_state['entity'][$delta])) {
  339. if (isset($field_state['entity'][$delta]->removed) && $field_state['entity'][$delta]->removed) {
  340. $deleted_paragraph = TRUE;
  341. }
  342. if (isset($field_state['entity'][$delta]->confirmed_removed) && $field_state['entity'][$delta]->confirmed_removed) {
  343. $confirmed_deleted_paragraph = TRUE;
  344. }
  345. if ($being_edited_paragraph || (isset($field_state['entity'][$delta]->being_edited) && $field_state['entity'][$delta]->being_edited)) {
  346. $being_edited_paragraph = TRUE;
  347. }
  348. else {
  349. $being_edited_paragraph = FALSE;
  350. }
  351. /* @var $paragraph_item ParagraphsItemEntity */
  352. $paragraph_item = $field_state['entity'][$delta];
  353. $paragraph_item->setHostEntity($field_state['instance']['entity_type'], $element['#entity'], $langcode, FALSE);
  354. }
  355. else {
  356. if (isset($items[$delta])) {
  357. $paragraph_item = paragraphs_field_get_entity($items[$delta]);
  358. }
  359. // Show an empty collection if we have no existing one or it does not
  360. // load.
  361. if (empty($paragraph_item) && $bundle) {
  362. /* @var $paragraph_item ParagraphsItemEntity */
  363. $paragraph_item = entity_create('paragraphs_item', array('bundle' => $bundle, 'field_name' => $field_name));
  364. $paragraph_item->being_edited = TRUE;
  365. $being_edited_paragraph = TRUE;
  366. $is_new_paragraph = TRUE;
  367. }
  368. if (!empty($paragraph_item)) {
  369. /* @var $paragraph_item ParagraphsItemEntity */
  370. $paragraph_item->setHostEntity($element['#entity_type'], $element['#entity'], $langcode, FALSE);
  371. // Put our entity in the form state, so FAPI callbacks can access it.
  372. $field_state['entity'][$delta] = $paragraph_item;
  373. }
  374. }
  375. field_form_set_state($field_parents, $field_name, $language, $form_state, $field_state);
  376. if (!empty($paragraph_item)) {
  377. $bundle_info = paragraphs_bundle_load($paragraph_item->bundle);
  378. if ($bundle_info) {
  379. $element['paragraph_bundle_title'] = array(
  380. '#type' => 'container',
  381. '#weight' => -100,
  382. );
  383. $element['paragraph_bundle_title']['info'] = array(
  384. '#markup' => t('!title type: %bundle', array('!title' => t($instance['settings']['title']), '%bundle' => $bundle_info->name)),
  385. );
  386. }
  387. if (!$deleted_paragraph) {
  388. $element['actions'] = array(
  389. '#type' => 'actions',
  390. '#weight' => 9999,
  391. );
  392. field_attach_form('paragraphs_item', $paragraph_item, $element, $form_state, $language);
  393. if ($being_edited_paragraph) {
  394. if (!$is_new_paragraph && !entity_access('update', 'paragraphs_item', $paragraph_item)) {
  395. foreach (element_children($element) as $key) {
  396. if ($key != 'paragraph_bundle_title' && $key != 'actions' && $key != 'paragraph_bundle_preview' && $key != 'access_info') {
  397. $element[$key]['#access'] = FALSE;
  398. }
  399. }
  400. $element['access_info'] = array(
  401. '#type' => 'container',
  402. '#weight' => 9998,
  403. );
  404. $element['access_info']['info'] = array(
  405. '#type' => 'markup',
  406. '#markup' => '<em>' . t('You are not allowed to edit this !title item.', array('!title' => t($instance['settings']['title']))) . '</em>',
  407. );
  408. }
  409. else {
  410. if (empty($element['#required'])) {
  411. $element['#after_build'][] = 'paragraphs_field_widget_embed_delay_required_validation';
  412. }
  413. }
  414. if ($default_edit_mode != 'open') {
  415. $element['actions']['collapse_button'] = array(
  416. '#delta' => $delta,
  417. '#name' => implode('_', $parents) . '_collapse_button',
  418. '#type' => 'submit',
  419. '#value' => t('Collapse'),
  420. '#validate' => array(),
  421. '#submit' => array('paragraphs_collapse_submit'),
  422. '#limit_validation_errors' => array(),
  423. '#ajax' => array(
  424. 'path' => 'paragraphs/collapse/ajax',
  425. 'effect' => 'fade',
  426. ),
  427. '#access' => entity_access('update', 'paragraphs_item', $paragraph_item),
  428. '#weight' => 999,
  429. );
  430. }
  431. }
  432. else {
  433. if($default_edit_mode === 'preview' && entity_access('view', 'paragraphs_item', $paragraph_item)) {
  434. $element['paragraph_bundle_preview'] = array(
  435. '#type' => 'container',
  436. );
  437. $preview = $paragraph_item->view('paragraphs_editor_preview');
  438. $element['paragraph_bundle_preview']['preview'] = $preview;
  439. }
  440. foreach (element_children($element) as $key) {
  441. if ($key != 'paragraph_bundle_title' && $key != 'actions' && $key != 'paragraph_bundle_preview' && $key != 'access_info') {
  442. $element[$key]['#access'] = FALSE;
  443. }
  444. }
  445. $element['actions'] = array(
  446. '#type' => 'actions',
  447. '#weight' => 9999,
  448. );
  449. if (isset($field_state['entity'][$delta]->must_be_saved) && $field_state['entity'][$delta]->must_be_saved) {
  450. $element['actions']['must_be_saved'] = array(
  451. '#markup' => '<p><em>' . t('Warning: this content must be saved to reflect changes on this paragraph item.') . '</em></p>',
  452. '#weight' => 998,
  453. );
  454. }
  455. $element['actions']['edit_button'] = array(
  456. '#delta' => $delta,
  457. '#name' => implode('_', $parents) . '_edit_button',
  458. '#type' => 'submit',
  459. '#value' => t('Edit'),
  460. '#validate' => array(),
  461. '#submit' => array('paragraphs_edit_submit'),
  462. '#limit_validation_errors' => array(),
  463. '#ajax' => array(
  464. 'path' => 'paragraphs/edit/ajax',
  465. 'effect' => 'fade',
  466. ),
  467. '#access' => entity_access('update', 'paragraphs_item', $paragraph_item),
  468. '#weight' => 999,
  469. );
  470. }
  471. if (isset($paragraph_item)) {
  472. $element['actions']['remove_button'] = array(
  473. '#delta' => $delta,
  474. '#name' => implode('_', $parents) . '_remove_button',
  475. '#type' => 'submit',
  476. '#value' => t('Remove'),
  477. '#validate' => array(),
  478. '#submit' => array('paragraphs_remove_submit'),
  479. '#limit_validation_errors' => array(),
  480. '#ajax' => array(
  481. 'path' => 'paragraphs/remove/ajax',
  482. 'effect' => 'fade',
  483. ),
  484. '#access' => entity_access('delete', 'paragraphs_item', $paragraph_item),
  485. '#weight' => 1000,
  486. );
  487. }
  488. if (isset($element['actions']['edit_button']) && !$element['actions']['edit_button']['#access']
  489. && isset($element['actions']['remove_button']) && !$element['actions']['remove_button']['#access']) {
  490. $element['access_info'] = array(
  491. '#type' => 'container',
  492. '#weight' => 9998,
  493. );
  494. $element['access_info']['info'] = array(
  495. '#type' => 'markup',
  496. '#markup' => '<em>' . t('You are not allowed to edit or remove this !title item.', array('!title' => t($instance['settings']['title']))) . '</em>',
  497. );
  498. }
  499. elseif (isset($element['actions']['edit_button']) && !$element['actions']['edit_button']['#access']) {
  500. $element['access_info'] = array(
  501. '#type' => 'container',
  502. '#weight' => 9998,
  503. );
  504. $element['access_info']['info'] = array(
  505. '#type' => 'markup',
  506. '#markup' => '<em>' . t('You are not allowed to edit this !title item.', array('!title' => t($instance['settings']['title']))) . '</em>',
  507. );
  508. }
  509. elseif (isset($element['actions']['remove_button']) && !$element['actions']['remove_button']['#access']) {
  510. $element['access_info'] = array(
  511. '#type' => 'container',
  512. '#weight' => 9998,
  513. );
  514. $element['access_info']['info'] = array(
  515. '#type' => 'markup',
  516. '#markup' => '<em>' . t('You are not allowed to remove this !title item.', array('!title' => t($instance['settings']['title']))) . '</em>',
  517. );
  518. }
  519. }
  520. else {
  521. $element['actions'] = array(
  522. '#type' => 'actions',
  523. '#weight' => 9999,
  524. );
  525. $element['actions']['remove_button'] = array(
  526. '#markup' => '<p>' . t('This !title has been removed, press the button below to restore.', array('!title' => t($instance['settings']['title']))) . ' </p><p><em>' . t('Warning: this !title will actually be deleted when you press "!confirm" or "!save"!', array('!title' => $instance['settings']['title'], '!confirm' => t('Confirm Deletion'), '!save' => t('Save'))) . '</em></p>',
  527. );
  528. $element['actions']['restore_button'] = array(
  529. '#delta' => $delta,
  530. '#name' => implode('_', $parents) . '_restore_button',
  531. '#type' => 'submit',
  532. '#value' => t('Restore'),
  533. '#validate' => array(),
  534. '#submit' => array('paragraphs_restore_submit'),
  535. '#limit_validation_errors' => array(),
  536. '#ajax' => array(
  537. 'path' => 'paragraphs/restore/ajax',
  538. 'effect' => 'fade',
  539. ),
  540. '#weight' => 1000,
  541. );
  542. $element['actions']['confirm_delete_button'] = array(
  543. '#delta' => $delta,
  544. '#name' => implode('_', $parents) . '_deleteconfirm_button',
  545. '#type' => 'submit',
  546. '#value' => t('Confirm Deletion'),
  547. '#validate' => array(),
  548. '#submit' => array('paragraphs_deleteconfirm_submit'),
  549. '#limit_validation_errors' => array(),
  550. '#ajax' => array(
  551. 'path' => 'paragraphs/deleteconfirm/ajax',
  552. 'effect' => 'fade',
  553. ),
  554. '#weight' => 1001,
  555. );
  556. }
  557. }
  558. // Hide full item when we are confirmed delete.
  559. if ($confirmed_deleted_paragraph) {
  560. $element['#access'] = FALSE;
  561. }
  562. $recursion--;
  563. return $element;
  564. }
  565. /**
  566. * FAPI #after_build of an individual paragraph element to delay the validation of #required.
  567. */
  568. function paragraphs_field_widget_embed_delay_required_validation(&$element, &$form_state) {
  569. // If the process_input flag is set, the form and its input is going to be
  570. // validated. Prevent #required (sub)fields from throwing errors while
  571. // their non-#required paragraph item is empty.
  572. if ($form_state['process_input']) {
  573. _paragraphs_collect_required_elements($element, $element['#paragraphs_required_elements']);
  574. }
  575. return $element;
  576. }
  577. /**
  578. * Collects all embedded required fields.
  579. *
  580. * @param $element
  581. * @param $required_elements
  582. */
  583. function _paragraphs_collect_required_elements(&$element, &$required_elements) {
  584. // Recurse through all children.
  585. foreach (element_children($element) as $key) {
  586. if (isset($element[$key]) && $element[$key]) {
  587. _paragraphs_collect_required_elements($element[$key], $required_elements);
  588. }
  589. }
  590. if (!empty($element['#required'])) {
  591. $required_elements[] = &$element;
  592. $element += array('#pre_render' => array());
  593. array_unshift($element['#pre_render'], 'paragraphs_field_widget_render_required');
  594. }
  595. }
  596. /**
  597. * #pre_render callback that ensures the element is rendered as being required.
  598. */
  599. function paragraphs_field_widget_render_required($element) {
  600. $element['#required'] = TRUE;
  601. return $element;
  602. }
  603. /**
  604. * FAPI validation of an individual paragraph element.
  605. */
  606. function paragraphs_field_widget_embed_validate($element, &$form_state, $complete_form) {
  607. $instance = field_widget_instance($element, $form_state);
  608. $field = field_widget_field($element, $form_state);
  609. $field_parents = $element['#field_parents'];
  610. $field_name = $element['#field_name'];
  611. $language = $element['#language'];
  612. $field_state = field_form_get_state($field_parents, $field_name, $language, $form_state);
  613. if (isset($field_state['entity'][$element['#delta']])) {
  614. $paragraph_item = $field_state['entity'][$element['#delta']];
  615. // Now validate elements if the entity is not empty.
  616. if (((!isset($paragraph_item->removed) || !$paragraph_item->removed) && (!isset($paragraph_item->confirmed_removed) || !$paragraph_item->confirmed_removed))) {
  617. // Attach field API validation of the embedded form.
  618. field_attach_form_validate('paragraphs_item', $paragraph_item, $element, $form_state);
  619. if (!empty($element['#paragraphs_required_elements'])) {
  620. foreach ($element['#paragraphs_required_elements'] as &$elements) {
  621. // Copied from _form_validate().
  622. if (isset($elements['#needs_validation'])) {
  623. $is_empty_multiple = (!count($elements['#value']));
  624. $is_empty_string = (is_string($elements['#value']) && drupal_strlen(trim($elements['#value'])) == 0);
  625. $is_empty_value = ($elements['#value'] === 0);
  626. if ($is_empty_multiple || $is_empty_string || $is_empty_value) {
  627. if (isset($elements['#title'])) {
  628. $error_text = t('!name field is required.', array('!name' => $elements['#title']));
  629. form_error($elements, filter_xss_admin($error_text));
  630. }
  631. else {
  632. form_error($elements);
  633. }
  634. }
  635. }
  636. }
  637. }
  638. }
  639. // Only if the form is being submitted, finish the collection entity and
  640. // prepare it for saving.
  641. if ($form_state['submitted'] && !form_get_errors()) {
  642. field_attach_submit('paragraphs_item', $paragraph_item, $element, $form_state);
  643. // Load initial form values into $item, so any other form values below the
  644. // same parents are kept.
  645. $item = drupal_array_get_nested_value($form_state['values'], $element['#parents']);
  646. // Set the _weight if it is a multiple field.
  647. if (isset($element['_weight'])) {
  648. $item['_weight'] = $element['_weight']['#value'];
  649. }
  650. // Put the paragraph item in $item['entity'], so it is saved with
  651. // the host entity via hook_field_presave() / field API if it is not empty.
  652. // @see paragraph_field_presave()
  653. $item['entity'] = $paragraph_item;
  654. form_set_value($element, $item, $form_state);
  655. }
  656. }
  657. }
  658. /**
  659. * Submit function to add another paragraph.
  660. * @param $form
  661. * @param $form_state
  662. */
  663. function paragraphs_add_more_submit($form, &$form_state) {
  664. $button = $form_state['triggering_element'];
  665. // Go one level up in the form, to the widgets container.
  666. $element = drupal_array_get_nested_value($form, array_slice($button['#array_parents'], 0, -2));
  667. $field_name = $element['#field_name'];
  668. $langcode = $element['#language'];
  669. $parents = $element['#field_parents'];
  670. // Increment the items count.
  671. $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
  672. $field = $field_state['field'];
  673. if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $field_state['real_items_count'] < $field['cardinality']) {
  674. $field_state['items_count']++;
  675. }
  676. field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state);
  677. $form_state['rebuild'] = TRUE;
  678. }
  679. /**
  680. * Ajax callback in response to a new empty widget being added to the form.
  681. *
  682. * This returns the new page content to replace the page content made obsolete
  683. * by the form submission.
  684. *
  685. * @see field_add_more_submit()
  686. */
  687. function paragraphs_add_more_js($form, $form_state) {
  688. $button = $form_state['triggering_element'];
  689. // Go one level up in the form, to the widgets container.
  690. $element = drupal_array_get_nested_value($form, array_slice($button['#array_parents'], 0, -2));
  691. $field_name = $element['#field_name'];
  692. $langcode = $element['#language'];
  693. $parents = $element['#field_parents'];
  694. $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
  695. $field = $field_state['field'];
  696. // Add a DIV around the delta receiving the Ajax effect.
  697. $delta = $element['#max_delta'];
  698. $element[$delta]['#prefix'] = '<div class="ajax-new-content">' . (isset($element[$delta]['#prefix']) ? $element[$delta]['#prefix'] : '');
  699. $element[$delta]['#suffix'] = (isset($element[$delta]['#suffix']) ? $element[$delta]['#suffix'] : '') . '</div>';
  700. return $element;
  701. }
  702. /**
  703. * Submit callback to remove an item from the field UI multiple wrapper.
  704. *
  705. * When a remove button is submitted, we need to find the item that it
  706. * referenced and delete it. Since field UI has the deltas as a straight
  707. * unbroken array key, we have to renumber everything down. Since we do this
  708. * we *also* need to move all the deltas around in the $form_state['values']
  709. * and $form_state['input'] so that user changed values follow. This is a bit
  710. * of a complicated process.
  711. */
  712. function paragraphs_remove_submit($form, &$form_state) {
  713. $button = $form_state['triggering_element'];
  714. $delta = $button['#delta'];
  715. // Where in the form we'll find the parent element.
  716. $address = array_slice($button['#array_parents'], 0, -3);
  717. // Go one level up in the form, to the widgets container.
  718. $parent_element = drupal_array_get_nested_value($form, $address);
  719. $field_name = $parent_element['#field_name'];
  720. $langcode = $parent_element['#language'];
  721. $parents = $parent_element['#field_parents'];
  722. $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
  723. if (isset($field_state['entity'][$delta])) {
  724. $field_state['entity'][$delta]->removed = 1;
  725. }
  726. // Fix the weights. Field UI lets the weights be in a range of
  727. // (-1 * item_count) to (item_count). This means that when we remove one,
  728. // the range shrinks; weights outside of that range then get set to
  729. // the first item in the select by the browser, floating them to the top.
  730. // We use a brute force method because we lost weights on both ends
  731. // and if the user has moved things around, we have to cascade because
  732. // if I have items weight weights 3 and 4, and I change 4 to 3 but leave
  733. // the 3, the order of the two 3s now is undefined and may not match what
  734. // the user had selected.
  735. $input = drupal_array_get_nested_value($form_state['input'], $address);
  736. // Sort by weight,
  737. // but first remove garbage values to ensure proper '_weight' sorting
  738. unset($input['add_more']);
  739. uasort($input, '_field_sort_items_helper');
  740. // Reweight everything in the correct order.
  741. $weight = -1 * $field_state['items_count'] + 1;
  742. foreach ($input as $key => $item) {
  743. if ($item) {
  744. $input[$key]['_weight'] = $weight++;
  745. }
  746. }
  747. drupal_array_set_nested_value($form_state['input'], $address, $input);
  748. field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state);
  749. $form_state['rebuild'] = TRUE;
  750. }
  751. /**
  752. * Submit callback to editing an item from the field UI multiple wrapper.
  753. *
  754. * When a edited button is submitted, we need to find the item that it
  755. * referenced and delete it. Since field UI has the deltas as a straight
  756. * unbroken array key, we have to renumber everything down. Since we do this
  757. * we *also* need to move all the deltas around in the $form_state['values']
  758. * and $form_state['input'] so that user changed values follow. This is a bit
  759. * of a complicated process.
  760. */
  761. function paragraphs_edit_submit($form, &$form_state) {
  762. $button = $form_state['triggering_element'];
  763. $delta = $button['#delta'];
  764. // Where in the form we'll find the parent element.
  765. $address = array_slice($button['#array_parents'], 0, -3);
  766. // Go one level up in the form, to the widgets container.
  767. $parent_element = drupal_array_get_nested_value($form, $address);
  768. $field_name = $parent_element['#field_name'];
  769. $langcode = $parent_element['#language'];
  770. $parents = $parent_element['#field_parents'];
  771. $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
  772. if (isset($field_state['entity'][$delta])) {
  773. $field_state['entity'][$delta]->being_edited = 1;
  774. }
  775. // Fix the weights. Field UI lets the weights be in a range of
  776. // (-1 * item_count) to (item_count). This means that when we remove one,
  777. // the range shrinks; weights outside of that range then get set to
  778. // the first item in the select by the browser, floating them to the top.
  779. // We use a brute force method because we lost weights on both ends
  780. // and if the user has moved things around, we have to cascade because
  781. // if I have items weight weights 3 and 4, and I change 4 to 3 but leave
  782. // the 3, the order of the two 3s now is undefined and may not match what
  783. // the user had selected.
  784. $input = drupal_array_get_nested_value($form_state['input'], $address);
  785. // Sort by weight,
  786. // but first remove garbage values to ensure proper '_weight' sorting
  787. unset($input['add_more']);
  788. uasort($input, '_field_sort_items_helper');
  789. // Reweight everything in the correct order.
  790. $weight = -1 * $field_state['items_count'] + 1;
  791. foreach ($input as $key => $item) {
  792. if ($item) {
  793. $input[$key]['_weight'] = $weight++;
  794. }
  795. }
  796. drupal_array_set_nested_value($form_state['input'], $address, $input);
  797. field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state);
  798. $form_state['rebuild'] = TRUE;
  799. }
  800. /**
  801. * Submit callback to collapse an item from the field UI multiple wrapper.
  802. *
  803. * When a collapse button is submitted, we need to find the item that it
  804. * referenced and delete it. Since field UI has the deltas as a straight
  805. * unbroken array key, we have to renumber everything down. Since we do this
  806. * we *also* need to move all the deltas around in the $form_state['values']
  807. * and $form_state['input'] so that user changed values follow. This is a bit
  808. * of a complicated process.
  809. */
  810. function paragraphs_collapse_submit($form, &$form_state) {
  811. $button = $form_state['triggering_element'];
  812. $delta = $button['#delta'];
  813. // Where in the form we'll find the parent element.
  814. $address = array_slice($button['#array_parents'], 0, -3);
  815. // Go one level up in the form, to the widgets container.
  816. $parent_element = drupal_array_get_nested_value($form, $address);
  817. $field_name = $parent_element['#field_name'];
  818. $langcode = $parent_element['#language'];
  819. $parents = $parent_element['#field_parents'];
  820. $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
  821. if (isset($field_state['entity'][$delta])) {
  822. $field_state['entity'][$delta]->being_edited = 0;
  823. $field_state['entity'][$delta]->must_be_saved = 1;
  824. }
  825. // Fix the weights. Field UI lets the weights be in a range of
  826. // (-1 * item_count) to (item_count). This means that when we remove one,
  827. // the range shrinks; weights outside of that range then get set to
  828. // the first item in the select by the browser, floating them to the top.
  829. // We use a brute force method because we lost weights on both ends
  830. // and if the user has moved things around, we have to cascade because
  831. // if I have items weight weights 3 and 4, and I change 4 to 3 but leave
  832. // the 3, the order of the two 3s now is undefined and may not match what
  833. // the user had selected.
  834. $input = drupal_array_get_nested_value($form_state['input'], $address);
  835. // Sort by weight,
  836. // but first remove garbage values to ensure proper '_weight' sorting
  837. unset($input['add_more']);
  838. uasort($input, '_field_sort_items_helper');
  839. // Reweight everything in the correct order.
  840. $weight = -1 * $field_state['items_count'] + 1;
  841. foreach ($input as $key => $item) {
  842. if ($item) {
  843. $input[$key]['_weight'] = $weight++;
  844. }
  845. }
  846. drupal_array_set_nested_value($form_state['input'], $address, $input);
  847. field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state);
  848. $form_state['rebuild'] = TRUE;
  849. }
  850. /**
  851. * Submit callback to remove an item from the field UI multiple wrapper.
  852. *
  853. * When a remove button is submitted, we need to find the item that it
  854. * referenced and delete it. Since field UI has the deltas as a straight
  855. * unbroken array key, we have to renumber everything down. Since we do this
  856. * we *also* need to move all the deltas around in the $form_state['values']
  857. * and $form_state['input'] so that user changed values follow. This is a bit
  858. * of a complicated process.
  859. */
  860. function paragraphs_deleteconfirm_submit($form, &$form_state) {
  861. $button = $form_state['triggering_element'];
  862. $delta = $button['#delta'];
  863. // Where in the form we'll find the parent element.
  864. $address = array_slice($button['#array_parents'], 0, -3);
  865. // Go one level up in the form, to the widgets container.
  866. $parent_element = drupal_array_get_nested_value($form, $address);
  867. $field_name = $parent_element['#field_name'];
  868. $langcode = $parent_element['#language'];
  869. $parents = $parent_element['#field_parents'];
  870. $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
  871. if (isset($field_state['entity'][$delta])) {
  872. $field_state['entity'][$delta]->removed = 1;
  873. $field_state['entity'][$delta]->confirmed_removed = 1;
  874. }
  875. // Fix the weights. Field UI lets the weights be in a range of
  876. // (-1 * item_count) to (item_count). This means that when we remove one,
  877. // the range shrinks; weights outside of that range then get set to
  878. // the first item in the select by the browser, floating them to the top.
  879. // We use a brute force method because we lost weights on both ends
  880. // and if the user has moved things around, we have to cascade because
  881. // if I have items weight weights 3 and 4, and I change 4 to 3 but leave
  882. // the 3, the order of the two 3s now is undefined and may not match what
  883. // the user had selected.
  884. $input = drupal_array_get_nested_value($form_state['input'], $address);
  885. // Sort by weight,
  886. // but first remove garbage values to ensure proper '_weight' sorting
  887. unset($input['add_more']);
  888. uasort($input, '_field_sort_items_helper');
  889. // Reweight everything in the correct order.
  890. $weight = -1 * $field_state['items_count'] + 1;
  891. foreach ($input as $key => $item) {
  892. if ($item) {
  893. $input[$key]['_weight'] = $weight++;
  894. }
  895. }
  896. drupal_array_set_nested_value($form_state['input'], $address, $input);
  897. field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state);
  898. $form_state['rebuild'] = TRUE;
  899. }
  900. /**
  901. * Submit function to restore a paragraph that was deleted.
  902. * @param $form
  903. * @param $form_state
  904. */
  905. function paragraphs_restore_submit($form, &$form_state) {
  906. $button = $form_state['triggering_element'];
  907. $delta = $button['#delta'];
  908. // Where in the form we'll find the parent element.
  909. $address = array_slice($button['#array_parents'], 0, -3);
  910. // Go one level up in the form, to the widgets container.
  911. $parent_element = drupal_array_get_nested_value($form, $address);
  912. $field_name = $parent_element['#field_name'];
  913. $langcode = $parent_element['#language'];
  914. $parents = $parent_element['#field_parents'];
  915. $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
  916. if (isset($field_state['entity'][$delta])) {
  917. $field_state['entity'][$delta]->removed = 0;
  918. }
  919. // Fix the weights. Field UI lets the weights be in a range of
  920. // (-1 * item_count) to (item_count). This means that when we remove one,
  921. // the range shrinks; weights outside of that range then get set to
  922. // the first item in the select by the browser, floating them to the top.
  923. // We use a brute force method because we lost weights on both ends
  924. // and if the user has moved things around, we have to cascade because
  925. // if I have items weight weights 3 and 4, and I change 4 to 3 but leave
  926. // the 3, the order of the two 3s now is undefined and may not match what
  927. // the user had selected.
  928. $input = drupal_array_get_nested_value($form_state['input'], $address);
  929. // Sort by weight,
  930. // but first remove garbage values to ensure proper '_weight' sorting
  931. unset($input['add_more']);
  932. uasort($input, '_field_sort_items_helper');
  933. // Reweight everything in the correct order.
  934. $weight = -1 * $field_state['items_count'] + 1;
  935. foreach ($input as $key => $item) {
  936. if ($item) {
  937. $input[$key]['_weight'] = $weight++;
  938. }
  939. }
  940. drupal_array_set_nested_value($form_state['input'], $address, $input);
  941. field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state);
  942. $form_state['rebuild'] = TRUE;
  943. }