menu_link.field.inc 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  1. <?php
  2. /**
  3. * @file
  4. * Defines a menu link field type.
  5. */
  6. /**
  7. * Implements hook_field_info().
  8. *
  9. * Field settings:
  10. * - link_path_field: Boolean whether or not the link path field is available
  11. * to users. If set to FALSE enity_uri() is used to determine the link path.
  12. * Instance settings:
  13. * - menu_options: The menus available to place links in for this field.
  14. */
  15. function menu_link_field_info() {
  16. return array(
  17. 'menu_link' => array(
  18. 'label' => t('Menu link'),
  19. 'description' => t('This field stores a reference to a menu link in the database.'),
  20. 'settings' => array(
  21. 'link_path_field' => FALSE,
  22. ),
  23. 'instance_settings' => array(
  24. 'menu_options' => array('main-menu'),
  25. ),
  26. 'default_widget' => 'menu_link_default',
  27. 'default_formatter' => 'menu_link_link',
  28. ),
  29. );
  30. }
  31. /**
  32. * Implements hook_field_settings_form().
  33. */
  34. function menu_link_field_settings_form($field, $instance, $has_data) {
  35. $settings = $field['settings'];
  36. $form = array();
  37. /* @todo Allow menu link fields to be used as a reference field? http://drupal.org/node/1028344
  38. $form['link_path_field'] = array(
  39. '#type' => 'checkbox',
  40. '#title' => t('Enable <em>Path</em> field'),
  41. '#default_value' => $settings['link_path_field'],
  42. '#description' => t('Allow users to set the path of menu links. When not checked the uri of the entity being edited is used.'),
  43. );*/
  44. return $form;
  45. }
  46. /**
  47. * Implements hook_field_instance_settings_form().
  48. */
  49. function menu_link_field_instance_settings_form($field, $instance) {
  50. $settings = $instance['settings'];
  51. $form['menu_options'] = array(
  52. '#type' => 'checkboxes',
  53. '#title' => t('Available menus'),
  54. '#default_value' => $settings['menu_options'],
  55. '#options' => menu_get_menus(),
  56. '#description' => t('The menus available to place links in for this field.'),
  57. '#required' => TRUE,
  58. '#weight' => -5,
  59. );
  60. return $form;
  61. }
  62. /**
  63. * Implements hook_field_load().
  64. */
  65. function menu_link_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
  66. foreach ($entities as $id => $entity) {
  67. foreach ($items[$id] as $delta => &$item) {
  68. $item['options'] = !empty($item['options']) ? unserialize($item['options']) : array();
  69. $item['external'] = (!empty($item['link_path']) && $item['link_path'] != '<front>' && url_is_external($item['link_path'])) ? 1 : 0;
  70. }
  71. }
  72. }
  73. /**
  74. * Implements hook_field_validate().
  75. *
  76. * Possible error codes:
  77. * - 'menu_link_duplicate': A menu link can only be referenced once per entity
  78. * per field.
  79. * - 'menu_link_invalid_parent': Selected parent menu link does not exist or may
  80. * not be selected.
  81. * - 'menu_link_no_entity_uri': No link path is provided while entity being
  82. * edited has no URI of its own.
  83. */
  84. function menu_link_field_validate($entity_type, $entity, $field, $instance, $langcode, &$items, &$errors) {
  85. if (!empty($entity)) {
  86. list($id, , ) = entity_extract_ids($entity_type, $entity);
  87. }
  88. // Build an array of existing menu link IDs so they can be loaded with
  89. // menu_link_load_multiple();
  90. $mlids = array();
  91. foreach ($items as $delta => &$item) {
  92. if (menu_link_field_is_empty($item, $field)) {
  93. continue;
  94. }
  95. if (!empty($item['mlid']) && (int)$item['mlid'] > 0) {
  96. $mlids[] = (int)$item['mlid'];
  97. }
  98. if (!empty($item['plid']) && (int)$item['plid'] > 0) {
  99. $mlids[] = (int)$item['plid'];
  100. }
  101. }
  102. $menu_links = !empty($mlids) ? menu_link_load_multiple($mlids) : array();
  103. $mlids = array();
  104. foreach ($items as $delta => &$item) {
  105. if (menu_link_field_is_empty($item, $field)) {
  106. continue;
  107. }
  108. if (empty($item['menu_name']) || !in_array($item['menu_name'], $instance['settings']['menu_options'])) {
  109. $errors[$field['field_name']][$langcode][$delta][] = array(
  110. 'error' => 'menu_link_invalid_parent',
  111. 'message' => t('%name: Invalid menu name.', array('%name' => t($instance['label']))),
  112. );
  113. }
  114. if (!empty($item['plid'])) {
  115. if (!isset($menu_links[$item['plid']]) || !in_array($menu_links[$item['plid']]['menu_name'], $instance['settings']['menu_options'])) {
  116. $errors[$field['field_name']][$langcode][$delta][] = array(
  117. 'error' => 'menu_link_invalid_parent',
  118. 'message' => t('%name: Invalid parent menu link.', array('%name' => t($instance['label']))),
  119. );
  120. }
  121. }
  122. if (!empty($item['mlid'])) {
  123. if (isset($mlids[$item['mlid']])) {
  124. $errors[$field['field_name']][$langcode][$delta][] = array(
  125. 'error' => 'menu_link_duplicate',
  126. 'message' => t('%name: A menu link can only be referenced once per entity per field.', array('%name' => t($instance['label']))),
  127. );
  128. }
  129. else {
  130. $mlids[$item['mlid']] = $item['mlid'];
  131. }
  132. }
  133. if (!empty($field['settings']['link_path_field']) && empty($item['link_path']) && !empty($id)) {
  134. $uri = entity_uri($entity_type, $entity);
  135. if ($uri === NULL) {
  136. $errors[$field['field_name']][$langcode][$delta][] = array(
  137. 'error' => 'menu_link_no_entity_uri',
  138. 'message' => t('%name: No link path is provided while entity being edited has no URI of its own.', array('%name' => t($instance['label']))),
  139. );
  140. }
  141. }
  142. }
  143. }
  144. /**
  145. * Implements hook_field_presave().
  146. *
  147. * @see menu_link_menu_link_update()
  148. */
  149. function menu_link_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
  150. foreach ($items as $delta => &$item) {
  151. if (empty($item['plid'])) {
  152. $item['plid'] = 0;
  153. }
  154. $item += array(
  155. 'options' => array(),
  156. 'hidden' => 0,
  157. 'expanded' => 0,
  158. 'weight' => 0,
  159. );
  160. // Add a key to indicate we are saving menu links from within a field. This
  161. // key is not stored in the database and will only be available during the
  162. // current request.
  163. // @see menu_link_menu_link_update()
  164. $item['menu_link_field_save'] = $field['field_name'];
  165. // TODO This could override the module column!
  166. $item['module'] = 'menu_link';
  167. }
  168. }
  169. /**
  170. * Implements hook_field_insert().
  171. */
  172. function menu_link_field_insert($entity_type, $entity, $field, $instance, $langcode, &$items) {
  173. $path = _menu_link_path($entity_type, $entity, $langcode);
  174. foreach ($items as $delta => &$item) {
  175. if (empty($field['settings']['link_path_field']) || empty($item['link_path'])) {
  176. $item['link_path'] = $path;
  177. }
  178. if (!menu_link_save($item)) {
  179. drupal_set_message(t('There was an error saving the menu link.'), 'error');
  180. }
  181. $item['options'] = serialize($item['options']);
  182. }
  183. }
  184. /**
  185. * Implements hook_field_update().
  186. */
  187. function menu_link_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) {
  188. list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
  189. // Load the field items as they are stored in the database before update.
  190. $original = entity_create_stub_entity($entity_type, array($id, $vid, $bundle));
  191. field_attach_load($entity_type, array($id => $original), FIELD_LOAD_CURRENT, $options = array('field_id' => $field['id']));
  192. // Initially asume that all links are being deleted; later on in this function,
  193. // links that are kept are removed from this array.
  194. $delete_links = array();
  195. if (!empty($original->{$instance['field_name']}[$langcode])) {
  196. foreach($original->{$instance['field_name']}[$langcode] as $item) {
  197. $delete_links[$item['mlid']] = $item['mlid'];
  198. }
  199. }
  200. $path = _menu_link_path($entity_type, $entity, $langcode);
  201. foreach ($items as $delta => &$item) {
  202. if (empty($field['settings']['link_path_field']) || empty($item['link_path'])) {
  203. $item['link_path'] = $path;
  204. }
  205. if (!menu_link_save($item)) {
  206. drupal_set_message(t('There was an error saving the menu link.'), 'error');
  207. // TODO what to do?
  208. }
  209. $item['options'] = serialize($item['options']);
  210. // Don't remove menu links that are being kept.
  211. unset($delete_links[$item['mlid']]);
  212. }
  213. // Delete any menu links that are no longer used.
  214. if (!empty($delete_links)) {
  215. menu_link_delete_multiple($delete_links);
  216. }
  217. }
  218. /**
  219. * Helper function to build a menu link path based on an entity.
  220. */
  221. function _menu_link_path($entity_type, $entity, $langcode) {
  222. $uri = entity_uri($entity_type, $entity);
  223. if (url_is_external($uri['path'])) {
  224. $path = url($uri['path'], $uri['options']);
  225. }
  226. else {
  227. $path = drupal_get_normal_path($uri['path'], $langcode);
  228. if (!empty($uri['options']['query'])) {
  229. $path .= (strpos($path, '?') !== FALSE ? '&' : '?') . drupal_http_build_query($uri['options']['query']);
  230. }
  231. if (!empty($uri['options']['fragment'])) {
  232. $path .= '#' . $uri['options']['fragment'];
  233. }
  234. }
  235. return $path;
  236. }
  237. /**
  238. * Implements hook_field_delete().
  239. */
  240. function menu_link_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) {
  241. $mlids = array();
  242. foreach ($items as $delta => $item) {
  243. $mlids[] = $item['mlid'];
  244. }
  245. if (!empty($mlids)) {
  246. // Only delete menu links that are (still) owned by the Menu link module.
  247. $mlids = db_select('menu_links')
  248. ->fields('menu_links', array('mlid'))
  249. ->condition('module', 'menu_link')
  250. ->condition('mlid', $mlids)
  251. ->execute()
  252. ->fetchCol();
  253. if (!empty($mlids)) {
  254. menu_link_delete_multiple($mlids);
  255. }
  256. }
  257. }
  258. /**
  259. * Implements hook_field_is_empty().
  260. */
  261. function menu_link_field_is_empty($item, $field) {
  262. if (!empty($item['delete']) || !trim($item['link_title']) || empty($item['menu_name']) || (empty($item['plid']) && $item['plid'] != '0')) {
  263. return TRUE;
  264. }
  265. return FALSE;
  266. }
  267. /**
  268. * Implements hook_field_formatter_info().
  269. */
  270. function menu_link_field_formatter_info() {
  271. return array(
  272. 'menu_link_link' => array(
  273. 'label' => t('Link'),
  274. 'field types' => array('menu_link'),
  275. ),
  276. );
  277. }
  278. /**
  279. * Implements hook_field_formatter_prepare_view().
  280. */
  281. function menu_link_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
  282. $mlids = array();
  283. // Collect every possible menu link attached to any of the fieldable entities.
  284. foreach ($entities as $id => $entity) {
  285. foreach ($items[$id] as $delta => $item) {
  286. // Prevent doing things twice.
  287. if (empty($item['title'])) {
  288. // Force the array key to prevent duplicates.
  289. $mlids[$item['mlid']] = $item['mlid'];
  290. }
  291. }
  292. }
  293. if (!empty($mlids)) {
  294. $menu_links = menu_link_load_multiple($mlids);
  295. // Iterate through the fieldable entities again to attach the loaded menu
  296. // link data.
  297. foreach ($entities as $id => $entity) {
  298. $rekey = FALSE;
  299. foreach ($items[$id] as $delta => $item) {
  300. // Check whether the menu link field instance value could be loaded.
  301. if (isset($menu_links[$item['mlid']])) {
  302. // Replace the instance value with the menu link data.
  303. $items[$id][$delta] = $menu_links[$item['mlid']];
  304. }
  305. // Otherwise, unset the instance value, since the menu link does not exist.
  306. else {
  307. unset($items[$id][$delta]);
  308. $rekey = TRUE;
  309. }
  310. }
  311. if ($rekey) {
  312. // Rekey the items array.
  313. $items[$id] = array_values($items[$id]);
  314. }
  315. }
  316. }
  317. }
  318. /**
  319. * Implements hook_field_formatter_view().
  320. */
  321. function menu_link_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  322. $element = array();
  323. // If the default formatter is set to "hidden" but the field is being
  324. // displayed using this formatter, some module don't properly invoke
  325. // hook_field_formatter_prepare_view() which is essential for this formatter.
  326. // Let's make sure it has been invoked.
  327. // TODO remove if resolved.
  328. if ($instance['display']['default']['type'] == 'hidden') {
  329. list($id, , ) = entity_extract_ids($entity_type, $entity);
  330. $items = array($id => &$items);
  331. menu_link_field_formatter_prepare_view(
  332. $entity_type,
  333. array($id => $entity),
  334. $field,
  335. array($id => $instance),
  336. $langcode,
  337. $items,
  338. array($id => $display)
  339. );
  340. $items = $items[$id];
  341. }
  342. switch ($display['type']) {
  343. case 'menu_link_link':
  344. foreach ($items as $delta => $item) {
  345. $element[$delta] = array(
  346. '#type' => 'link',
  347. '#title' => $item['title'],
  348. '#href' => $item['href'],
  349. '#options' => $item['localized_options'],
  350. );
  351. }
  352. break;
  353. }
  354. return $element;
  355. }
  356. /**
  357. * Implements hook_field_widget_info().
  358. */
  359. function menu_link_field_widget_info() {
  360. return array(
  361. 'menu_link_default' => array(
  362. 'label' => t('Select list'),
  363. 'field types' => array('menu_link'),
  364. 'settings' => array(
  365. 'description_field' => TRUE,
  366. 'expanded_field' => FALSE
  367. ),
  368. ),
  369. );
  370. }
  371. /**
  372. * Implements hook_field_widget_settings_form().
  373. */
  374. function menu_link_field_widget_settings_form($field, $instance) {
  375. $widget = $instance['widget'];
  376. $settings = $widget['settings'];
  377. $form['description_field'] = array(
  378. '#type' => 'checkbox',
  379. '#title' => t('Enable <em>Description</em> field'),
  380. '#default_value' => $settings['description_field'],
  381. '#description' => t('The description field is used as a tooltip when the mouse hovers over the menu link.'),
  382. '#weight' => 5,
  383. );
  384. $form['expanded_field'] = array(
  385. '#type' => 'checkbox',
  386. '#title' => t('Enable <em>Expanded</em> field'),
  387. '#default_value' => $settings['expanded_field'],
  388. '#description' => t('The expanded field is a checkbox which, if selected and the menu link has children, makes the menu link always appear expanded.'),
  389. '#weight' => 5,
  390. );
  391. return $form;
  392. }
  393. /**
  394. * Implements hook_field_widget_form().
  395. */
  396. function menu_link_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  397. $module_path = drupal_get_path('module', 'menu_link');
  398. $element += array(
  399. '#input' => TRUE,
  400. '#type' => ($field['cardinality'] == 1) ? 'fieldset' : 'container',
  401. '#element_validate' => array('menu_link_field_widget_validate'),
  402. '#attached' => array(
  403. 'js' => array($module_path . '/menu_link.field.js'),
  404. 'css' => array($module_path . '/menu_link.field.css'),
  405. ),
  406. '#attributes' => array(
  407. 'class' => array('menu-link-item', 'menu-link-auto-title', 'menu-link-auto-fieldset-summary'),
  408. )
  409. );
  410. // Populate the element with the link data.
  411. foreach (array('mlid', 'link_path', 'options', 'hidden', 'expanded') as $key) {
  412. if (isset($items[$delta][$key])) {
  413. $element[$key] = array('#type' => 'value', '#value' => $items[$delta][$key]);
  414. }
  415. }
  416. $menus = menu_get_menus();
  417. $available_menus = array_combine($instance['settings']['menu_options'], $instance['settings']['menu_options']);
  418. $options = _menu_get_options($menus, $available_menus, array('mlid' => 0));
  419. $element['parent'] = array(
  420. '#type' => 'select',
  421. '#title' => t('Parent menu item'),
  422. '#options' => $options,
  423. '#empty_value' => '_none',
  424. '#attributes' => array(
  425. 'class' => array('menu-link-item-parent'),
  426. 'title' => t('Choose the menu item to be the parent for this link.'),
  427. ),
  428. '#required' => !empty($element['#required']),
  429. );
  430. if (isset($items[$delta]['menu_name'], $items[$delta]['plid'])) {
  431. $element['parent']['#default_value'] = $items[$delta]['menu_name'] . ':' . $items[$delta]['plid'];
  432. }
  433. $element['weight'] = array(
  434. '#type' => 'weight',
  435. '#title' => t('Weight'),
  436. '#delta' => 50,
  437. '#default_value' => isset($items[$delta]['weight']) ? $items[$delta]['weight'] : 0,
  438. '#attributes' => array(
  439. 'class' => array('menu-link-item-weight'),
  440. 'title' => t('Menu links with smaller weights are displayed before links with larger weights.'),
  441. ),
  442. );
  443. $element['link_title'] = array(
  444. '#type' => 'textfield',
  445. '#title' => t('Title'),
  446. '#default_value' => isset($items[$delta]['link_title']) ? $items[$delta]['link_title'] : '',
  447. '#size' => 50,
  448. '#maxlength' => 255,
  449. '#attributes' => array(
  450. 'class' => array('menu-link-item-title'),
  451. ),
  452. '#description' => t('The text to be used for this link in the menu.'),
  453. '#required' => !empty($element['#required']),
  454. );
  455. if (!empty($field['settings']['link_path_field'])) {
  456. $element['link_path'] = array(
  457. '#type' => 'textfield',
  458. '#title' => t('Path'),
  459. '#default_value' => isset($items[$delta]['link_path']) ? $items[$delta]['link_path'] : '',
  460. '#size' => 50,
  461. '#maxlength' => 255,
  462. '#attributes' => array(
  463. 'class' => array('menu-link-item-path'),
  464. ),
  465. '#description' => t('The path for this menu link.'),
  466. );
  467. }
  468. if (!empty($instance['widget']['settings']['fragment_field'])) {
  469. $element['fragment'] = array(
  470. '#type' => 'textfield',
  471. '#title' => t('URL Fragment'),
  472. '#field_prefix' => '#',
  473. '#default_value' => isset($items[$delta]['options']['attributes']['fragment']) ? $items[$delta]['options']['attributes']['fragment'] : '',
  474. '#size' => 10,
  475. '#maxlength' => 255,
  476. '#attributes' => array(
  477. 'class' => array('menu-link-item-fragment'),
  478. ),
  479. );
  480. }
  481. if (!empty($instance['widget']['settings']['expanded_field'])) {
  482. $element['expanded'] = array(
  483. '#type' => 'checkbox',
  484. '#title' => t('Expanded'),
  485. '#title_display' => 'before',
  486. '#default_value' => isset($items[$delta]['expanded']) ? $items[$delta]['expanded'] : 0,
  487. '#attributes' => array(
  488. 'class' => array('menu-link-item-expanded'),
  489. 'title' => t('If selected and this menu link has children, the menu will always appear expanded.'),
  490. ),
  491. '#weight' => 5,
  492. );
  493. }
  494. if (!empty($instance['widget']['settings']['description_field'])) {
  495. $element['description'] = array(
  496. '#type' => 'textarea',
  497. '#title' => t('Description'),
  498. '#default_value' => isset($items[$delta]['options']['attributes']['title']) ? $items[$delta]['options']['attributes']['title'] : '',
  499. '#rows' => 1,
  500. '#description' => t('Shown when hovering over the menu link.'),
  501. '#attributes' => array('class' => array('menu-link-item-description')),
  502. '#weight' => 10,
  503. );
  504. }
  505. return $element;
  506. }
  507. /**
  508. * Form element validate handler for menu link field widget.
  509. */
  510. function menu_link_field_widget_validate($element, &$form_state) {
  511. $item = drupal_array_get_nested_value($form_state['values'], $element['#parents']);
  512. if (!empty($item['parent']) && $item['parent'] != '_none') {
  513. list($item['menu_name'], $item['plid']) = explode(':', $item['parent']);
  514. }
  515. $item['link_title'] = trim($item['link_title']);
  516. if (!empty($item['description']) && trim($item['description'])) {
  517. $item['options']['attributes']['title'] = trim($item['description']);
  518. }
  519. else {
  520. // If the description field was left empty, remove the title attribute
  521. // from the menu link.
  522. unset($item['options']['attributes']['title']);
  523. }
  524. if (!empty($item['fragment']) && trim($item['fragment'])) {
  525. $item['options']['fragment'] = trim($item['fragment']);
  526. }
  527. else {
  528. unset($item['options']['fragment']);
  529. }
  530. form_set_value($element, $item, $form_state);
  531. }
  532. /**
  533. * Implements hook_field_widget_error().
  534. */
  535. function menu_link_field_widget_error($element, $error, $form, &$form_state) {
  536. switch ($error['error']) {
  537. case 'menu_link_invalid_parent':
  538. $error_element = $element['parent'];
  539. break;
  540. default:
  541. $error_element = $element['link_title'];
  542. break;
  543. }
  544. form_error($error_element, $error['message']);
  545. }