hierarchical_select.module 93 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367
  1. <?php
  2. /**
  3. * @file
  4. * This module defines the "hierarchical_select" form element, which is a
  5. * greatly enhanced way for letting the user select items in a hierarchy.
  6. */
  7. // Make sure that the devel module is installed when you enable developer mode!
  8. define('HS_DEVELOPER_MODE', 0);
  9. //----------------------------------------------------------------------------
  10. // Drupal core hooks.
  11. /**
  12. * Implements hook_help().
  13. */
  14. function hierarchical_select_help($path, $arg) {
  15. switch ($path) {
  16. // Displaying help text on help page.
  17. case 'admin/help#hierarchical_select':
  18. return t("Hierarchical Select has the ability to save the entire lineage of a selection or only the 'deepest' selection. You can configure it to force the user to make a selection as deep as possible in the tree, or allow the user to select an item anywhere in the tree. Levels can be labeled, you can configure limit the number of items that can be selected, configure a title for the dropbox, choose a site-wide animation delay, and so on. You can even create new items and levels through Hierarchical Select!");
  19. }
  20. }
  21. /**
  22. * Implements hook_menu().
  23. */
  24. function hierarchical_select_menu() {
  25. $items['hierarchical_select_ajax'] = array(
  26. 'page callback' => 'hierarchical_select_ajax',
  27. 'delivery callback' => 'ajax_deliver',
  28. 'access arguments' => array('access content'),
  29. 'theme callback' => 'ajax_base_page_theme',
  30. 'type' => MENU_CALLBACK,
  31. );
  32. $items['admin/config/content/hierarchical_select'] = array(
  33. 'title' => 'Hierarchical Select',
  34. 'description' => 'Configure site-wide settings for the Hierarchical Select form element.',
  35. 'access arguments' => array('administer site configuration'),
  36. 'page callback' => 'drupal_get_form',
  37. 'page arguments' => array('hierarchical_select_admin_settings'),
  38. 'type' => MENU_NORMAL_ITEM,
  39. 'file' => 'hierarchical_select.admin.inc',
  40. );
  41. $items['admin/config/content/hierarchical_select/settings'] = array(
  42. 'title' => 'Site-wide settings',
  43. 'access arguments' => array('administer site configuration'),
  44. 'weight' => -10,
  45. 'type' => MENU_DEFAULT_LOCAL_TASK,
  46. 'file' => 'hierarchical_select.admin.inc',
  47. );
  48. $items['admin/config/content/hierarchical_select/configs'] = array(
  49. 'title' => 'Configurations',
  50. 'description' => 'All available Hierarchical Select configurations.',
  51. 'access arguments' => array('administer site configuration'),
  52. 'page callback' => 'hierarchical_select_admin_configs',
  53. 'type' => MENU_LOCAL_TASK,
  54. 'file' => 'hierarchical_select.admin.inc',
  55. );
  56. $items['admin/config/content/hierarchical_select/implementations'] = array(
  57. 'title' => 'Implementations',
  58. 'description' => 'Features of each Hierarchical Select implementation.',
  59. 'access arguments' => array('administer site configuration'),
  60. 'page callback' => 'hierarchical_select_admin_implementations',
  61. 'type' => MENU_LOCAL_TASK,
  62. 'file' => 'hierarchical_select.admin.inc',
  63. );
  64. $items['admin/config/content/hierarchical_select/export/%hierarchical_select_config_id'] = array(
  65. 'title' => 'Export',
  66. 'access arguments' => array('administer site configuration'),
  67. 'page callback' => 'drupal_get_form',
  68. 'page arguments' => array('hierarchical_select_admin_export', 5),
  69. 'type' => MENU_LOCAL_TASK,
  70. 'file' => 'hierarchical_select.admin.inc',
  71. );
  72. $items['admin/config/content/hierarchical_select/import/%hierarchical_select_config_id'] = array(
  73. 'title' => 'Import',
  74. 'access arguments' => array('administer site configuration'),
  75. 'page callback' => 'drupal_get_form',
  76. 'page arguments' => array('hierarchical_select_admin_import', 5),
  77. 'type' => MENU_LOCAL_TASK,
  78. 'file' => 'hierarchical_select.admin.inc',
  79. );
  80. return $items;
  81. }
  82. /**
  83. * Implements hook_element_info().
  84. */
  85. function hierarchical_select_element_info() {
  86. $types['hierarchical_select'] = array(
  87. '#input' => TRUE,
  88. '#process' => array('form_hierarchical_select_process'),
  89. '#theme' => array('hierarchical_select'),
  90. '#theme_wrappers' => array('form_element'),
  91. '#config' => array(
  92. 'module' => 'some_module',
  93. 'params' => array(),
  94. 'save_lineage' => 0,
  95. 'enforce_deepest' => 0,
  96. 'resizable' => 1,
  97. 'level_labels' => array(
  98. 'status' => 0,
  99. 'labels' => array(),
  100. ),
  101. 'dropbox' => array(
  102. 'status' => 0,
  103. 'title' => t('All selections'),
  104. 'limit' => 0,
  105. 'reset_hs' => 1,
  106. 'sort' => 1,
  107. ),
  108. 'editability' => array(
  109. 'status' => 0,
  110. 'item_types' => array(),
  111. 'allowed_levels' => array(),
  112. 'allow_new_levels' => 0,
  113. 'max_levels' => 3,
  114. ),
  115. 'entity_count' => array(
  116. 'enabled' => 0,
  117. 'require_entity' => 0,
  118. 'settings' => array(
  119. 'count_children' => 0,
  120. 'entity_types' => array(),
  121. ),
  122. ),
  123. 'animation_delay' => variable_get('hierarchical_select_animation_delay', 400),
  124. 'special_items' => array(),
  125. 'render_flat_select' => 0,
  126. ),
  127. '#default_value' => -1,
  128. );
  129. $types['hierarchical_select_item_separator'] = array(
  130. '#theme' => 'hierarchical_select_item_separator',
  131. );
  132. return $types;
  133. }
  134. /**
  135. * Implements hook_requirements().
  136. */
  137. function hierarchical_select_requirements($phase) {
  138. $requirements = array();
  139. if ($phase == 'runtime') {
  140. // Check if all hook_update_n() hooks have been executed.
  141. require_once DRUPAL_ROOT . '/' . 'includes/install.inc';
  142. drupal_load_updates();
  143. $updates = drupal_get_schema_versions('hierarchical_select');
  144. $current = drupal_get_installed_schema_version('hierarchical_select');
  145. $up_to_date = (end($updates) == $current);
  146. $hierarchical_select_weight = db_query("SELECT weight FROM {system} WHERE type = :type AND name = :name", array(':type' => 'module', ':name' => 'hierarchical_select'))->fetchField();
  147. $core_overriding_modules = array('hs_book', 'hs_menu', 'hs_taxonomy');
  148. $path_errors = array();
  149. foreach ($core_overriding_modules as $module) {
  150. $filename = db_query("SELECT filename FROM {system} WHERE type = :type AND name = :name", array(':type' => 'module', ':name' => $module))->fetchField();
  151. if (strpos($filename, 'modules/') === 0) {
  152. $module_info = drupal_parse_info_file(dirname($filename) . "/$module.info");
  153. $path_errors[] = t('!module', array('!module' => $module_info['name']));
  154. }
  155. }
  156. if ($up_to_date && !count($path_errors)) {
  157. $value = t('All updates installed. HS API implementation modules correctly installed.');
  158. $description = '';
  159. $severity = REQUIREMENT_OK;
  160. }
  161. elseif ($path_errors) {
  162. $value = t('Modules incorrectly installed!');
  163. $description = t(
  164. "The following modules implement Hierarchical Select module for Drupal
  165. core modules, but are installed in the wrong location. They're
  166. installed in core's <code>modules</code> directory, but should be
  167. installed in either the <code>sites/all/modules</code> directory or a
  168. <code>sites/yoursite.com/modules</code> directory"
  169. ) . ':' . theme('item_list', array('items' => $path_errors));
  170. $severity = REQUIREMENT_ERROR;
  171. }
  172. else {
  173. $value = t('Not all updates installed!');
  174. $description = t('Please run update.php to install the latest updates!
  175. You have installed update !installed_update, but the latest update is
  176. !latest_update!',
  177. array(
  178. '!installed_update' => $current,
  179. '!latest_update' => end($updates),
  180. )
  181. );
  182. $severity = REQUIREMENT_ERROR;
  183. }
  184. $requirements['hierarchical_select'] = array(
  185. 'title' => t('Hierarchical Select'),
  186. 'value' => $value,
  187. 'description' => $description,
  188. 'severity' => $severity,
  189. );
  190. }
  191. return $requirements;
  192. }
  193. /**
  194. * Implements hook_theme().
  195. */
  196. function hierarchical_select_theme() {
  197. return array(
  198. 'hierarchical_select_form_element' => array(
  199. 'file' => 'includes/theme.inc',
  200. 'variables' => array('element' => NULL, 'value' => NULL),
  201. ),
  202. 'hierarchical_select' => array(
  203. 'file' => 'includes/theme.inc',
  204. 'render element' => 'element',
  205. ),
  206. 'hierarchical_select_selects_container' => array(
  207. 'file' => 'includes/theme.inc',
  208. 'render element' => 'element',
  209. ),
  210. 'hierarchical_select_select' => array(
  211. 'file' => 'includes/theme.inc',
  212. 'render element' => 'element',
  213. ),
  214. 'hierarchical_select_item_separator' => array(
  215. 'file' => 'includes/theme.inc',
  216. 'render element' => 'element',
  217. ),
  218. 'hierarchical_select_special_option' => array(
  219. 'file' => 'includes/theme.inc',
  220. 'variables' => array('option' => NULL),
  221. ),
  222. 'hierarchical_select_dropbox_table' => array(
  223. 'file' => 'includes/theme.inc',
  224. 'render element' => 'element',
  225. ),
  226. 'hierarchical_select_common_config_form_level_labels' => array(
  227. 'file' => 'includes/theme.inc',
  228. 'render element' => 'form',
  229. ),
  230. 'hierarchical_select_common_config_form_editability' => array(
  231. 'file' => 'includes/theme.inc',
  232. 'render element' => 'form',
  233. ),
  234. 'hierarchical_select_selection_as_lineages' => array(
  235. 'file' => 'includes/theme.inc',
  236. 'variables' => array(
  237. 'selection' => NULL,
  238. 'config' => NULL,
  239. ),
  240. ),
  241. );
  242. }
  243. /**
  244. * Implements hook_features_api().
  245. */
  246. function hierarchical_select_features_api() {
  247. return array(
  248. 'hierarchical_select' => array(
  249. 'name' => t('Hierarchical select configs'),
  250. 'feature_source' => TRUE,
  251. 'default_hook' => 'hierarchical_select_default_configs',
  252. 'default_file' => FEATURES_DEFAULTS_INCLUDED,
  253. 'file' => drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.features.inc',
  254. ),
  255. );
  256. }
  257. /**
  258. * Implements hook_select_menu_site_status_alter().
  259. *
  260. * This will run straight after the bootstrap/hook_init(), and override the
  261. * interface language there determined with the interface language from the
  262. * previous request on the HS AJAX callback. We want the language to remain
  263. * the same between requests so we can determine the "triggering element"
  264. * correctly. If the button value changed because of a language change (as
  265. * can happen with the admin_language module), the whole form would submit.
  266. */
  267. function hierarchical_select_menu_site_status_alter(&$menu_site_status, $path) {
  268. global $language;
  269. // Make sure we are on the AJAX callback.
  270. if (0 === strpos($_GET['q'], 'hierarchical_select_ajax') && !empty($_POST['hs_current_language'])) {
  271. $languages = language_list();
  272. if (isset($languages[$_POST['hs_current_language']])) {
  273. // Override the language set during bootstrap with the language from the
  274. // previous request.
  275. $language = $languages[$_POST['hs_current_language']];
  276. }
  277. }
  278. }
  279. //----------------------------------------------------------------------------
  280. // Menu system callbacks.
  281. /**
  282. * Wildcard loader for Hierarchical Select config ID's.
  283. */
  284. function hierarchical_select_config_id_load($config_id) {
  285. $config = variable_get('hs_config_' . $config_id, FALSE);
  286. return ($config !== FALSE) ? $config['config_id'] : FALSE;
  287. }
  288. //----------------------------------------------------------------------------
  289. // Forms API callbacks.
  290. /**
  291. * Ajax callback to render the select form elements.
  292. *
  293. * @see file_ajax_upload(), upon which this is strongly inspired.
  294. * @see ajax_form_callback()
  295. */
  296. function hierarchical_select_ajax() {
  297. $form_parents = func_get_args();
  298. list($form, $form_state, $form_id, $form_build_id, $commands) = ajax_get_form();
  299. // Process user input. $form and $form_state are modified in the process.
  300. drupal_process_form($form['#form_id'], $form, $form_state);
  301. $element = drupal_array_get_nested_value($form, $form_parents);
  302. // Render the output.
  303. $output = theme('status_messages') . drupal_render($element);
  304. // Send AJAX command to update the Hierarchical Select.
  305. $commands[] = array(
  306. 'command' => 'hierarchicalSelectUpdate',
  307. 'output' => $output,
  308. );
  309. $new_settings = _hs_new_setting_ajax(FALSE);
  310. foreach ($new_settings as $new_setting) {
  311. $commands[] = array(
  312. 'command' => 'hierarchicalSelectSettingsUpdate',
  313. 'hsid' => $new_setting['hsid'],
  314. 'settings' => $new_setting['settings'],
  315. );
  316. }
  317. $context = array(
  318. 'form' => $form,
  319. 'form_state' => $form_state,
  320. 'element' => $element,
  321. );
  322. drupal_alter('hierarchical_select_ajax_commands', $commands, $context);
  323. return array('#type' => 'ajax', '#commands' => $commands);
  324. }
  325. function _hs_process_determine_hsid($element, &$form_state) {
  326. // Determine the HSID to use: either the existing one that is received, or
  327. // generate a new one based on the last HSID used (which is
  328. // stored in form state storage).
  329. if (!isset($element['#value']) || !is_array($element['#value']) || !array_key_exists('hsid', $element['#value'])) {
  330. $hsid = uniqid();
  331. }
  332. else {
  333. $hsid = check_plain($element['#value']['hsid']);
  334. }
  335. return $hsid;
  336. }
  337. // Get the config and convert the 'special_items' setting to a more easily
  338. // accessible format.
  339. function _hs_process_shortcut_special_items($config) {
  340. $special_items = array();
  341. if (isset($config['special_items'])) {
  342. $special_items['exclusive'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_exclusive'));
  343. $special_items['none'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_none'));
  344. }
  345. return $special_items;
  346. }
  347. function _hs_process_attach_css_js($element, $hsid, &$form_state, $complete_form) {
  348. global $language;
  349. // Set up Javascript and add settings specifically for the current
  350. // hierarchical select.
  351. $element['#attached']['library'][] = array('system', 'ui');
  352. $element['#attached']['library'][] = array('system', 'drupal.ajax');
  353. $element['#attached']['library'][] = array('system', 'jquery.form');
  354. $element['#attached']['library'][] = array('system', 'effects');
  355. $element['#attached']['library'][] = array('system', 'effects.drop');
  356. $element['#attached']['css'][] = drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.css';
  357. $element['#attached']['js'][] = drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.js';
  358. if (variable_get('hierarchical_select_js_cache_system', 0) == 1) {
  359. $element['#attached']['js'][] = drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select_cache.js';
  360. }
  361. if (!isset($form_state['storage']['hs']['js_settings_sent'])) {
  362. $form_state['storage']['hs']['js_settings_sent'] = array();
  363. }
  364. // Form was submitted; this is a newly loaded page, thus ensure that all JS
  365. // settings are resent.
  366. if ($form_state['process_input'] === TRUE) {
  367. $form_state['storage']['hs']['js_settings_sent'] = array();
  368. }
  369. if (!isset($form_state['storage']['hs']['js_settings_sent'][$hsid]) || (isset($form_state['storage']['hs']['js_settings_sent'][$hsid]) && (isset($form_state['triggering_element']) && $form_state['triggering_element']['#type'] == 'submit'))) {
  370. $config = _hierarchical_select_inherit_default_config($element['#config']);
  371. $settings = array(
  372. 'HierarchicalSelect' => array(
  373. // Save language in settings so we can use the same language during the AJAX callback.
  374. 'hs_current_language' => $language->language,
  375. 'settings' => array(
  376. "hs-$hsid" => array(
  377. 'animationDelay' => ($config['animation_delay'] == 0) ? (int) variable_get('hierarchical_select_animation_delay', 400) : $config['animation_delay'],
  378. 'cacheId' => $config['module'] . '_' . md5(serialize($config['params'])),
  379. 'renderFlatSelect' => (isset($config['render_flat_select'])) ? (int) $config['render_flat_select'] : 0,
  380. 'createNewItems' => (isset($config['editability']['status'])) ? (int) $config['editability']['status'] : 0,
  381. 'createNewLevels' => (isset($config['editability']['allow_new_levels'])) ? (int) $config['editability']['allow_new_levels'] : 0,
  382. 'resizable' => (isset($config['resizable'])) ? (int) $config['resizable'] : 0,
  383. 'ajax_url' => url('hierarchical_select_ajax/' . implode('/', $element['#array_parents'])),
  384. ),
  385. ),
  386. )
  387. );
  388. if (!isset($_POST['hsid'])) {
  389. $element['#attached']['js'][] = array(
  390. 'type' => 'setting',
  391. 'data' => $settings,
  392. );
  393. }
  394. else {
  395. $element['#attached']['_hs_new_setting_ajax'][] = array($hsid, $settings['HierarchicalSelect']['settings']["hs-$hsid"]);
  396. }
  397. $form_state['storage']['hs']['js_settings_sent'][$hsid] = TRUE;
  398. }
  399. return $element;
  400. }
  401. function _hs_new_setting_ajax($hsid = FALSE, $settings = NULL) {
  402. static $hs_settings = array();
  403. if ($hsid !== FALSE) {
  404. $hs_settings[] = array('hsid' => $hsid, 'settings' => $settings);
  405. }
  406. return $hs_settings;
  407. }
  408. // Basic config validation and diagnostics.
  409. function _hs_process_developer_mode_log_diagnostics(&$element) {
  410. if (HS_DEVELOPER_MODE) {
  411. $config = $element['#config'];
  412. $diagnostics = array();
  413. if (!isset($config['module']) || empty($config['module'])) {
  414. $diagnostics[] = t("'module is not set!");
  415. }
  416. elseif (!module_exists($config['module'])) {
  417. $diagnostics[] = t('the module that should be used (module) is not installed!', array('%module' => $config['module']));
  418. }
  419. else {
  420. $required_params = module_invoke($config['module'], 'hierarchical_select_params');
  421. $missing_params = array_diff($required_params, array_keys($config['params']));
  422. if (!empty($missing_params)) {
  423. $diagnostics[] = t("'params' is missing values for: ") . implode(', ', $missing_params) . '.';
  424. }
  425. }
  426. $config_id = (isset($config['config_id']) && is_string($config['config_id'])) ? $config['config_id'] : 'none';
  427. if (empty($diagnostics)) {
  428. _hierarchical_select_log("Config diagnostics (config id: $config_id): no problems found!");
  429. }
  430. else {
  431. $diagnostics_string = print_r($diagnostics, TRUE);
  432. $message = "Config diagnostics (config id: $config_id): $diagnostics_string";
  433. _hierarchical_select_log($message);
  434. $title = $element['#title'];
  435. $element = array();
  436. $element['#type'] = 'item';
  437. $element['#title'] = $title;
  438. $element['#markup'] = '<p><span style="color:red;">Fix the indicated errors in the #config property first!</span><br />' . nl2br($message) . '</p>';
  439. return FALSE;
  440. }
  441. }
  442. return TRUE;
  443. }
  444. function _hs_process_developer_mode_log_selections($config, $hs_selection, $db_selection) {
  445. if (HS_DEVELOPER_MODE) {
  446. _hierarchical_select_log("Calculated hierarchical select selection:");
  447. _hierarchical_select_log($hs_selection);
  448. if ($config['dropbox']['status']) {
  449. _hierarchical_select_log("Calculated dropbox selection:");
  450. _hierarchical_select_log($db_selection);
  451. }
  452. }
  453. }
  454. function _hs_process_developer_mode_log_hierarchy_and_dropbox($config, $hierarchy, $dropbox) {
  455. if (HS_DEVELOPER_MODE) {
  456. _hierarchical_select_log('Generated hierarchy in ' . $hierarchy->build_time['total'] . ' ms:');
  457. _hierarchical_select_log($hierarchy);
  458. if ($config['dropbox']['status']) {
  459. _hierarchical_select_log('Generated dropbox in ' . $dropbox->build_time . ' ms: ');
  460. _hierarchical_select_log($dropbox);
  461. }
  462. }
  463. }
  464. function _hs_process_developer_mode_send_log_js($element, $hsid) {
  465. if (HS_DEVELOPER_MODE) {
  466. $log = _hierarchical_select_log(NULL, TRUE);
  467. $settings = array(
  468. 'HierarchicalSelect' => array(
  469. 'initialLog' => array(
  470. "hs-$hsid" => $log,
  471. ),
  472. ),
  473. );
  474. $element['#attached']['js'][] = array(
  475. 'type' => 'setting',
  476. 'data' => $settings,
  477. );
  478. }
  479. return $element;
  480. }
  481. function _hs_process_exclusive_lineages($element, $hs_selection, $db_selection) {
  482. $config = $element['#config'];
  483. $special_items = _hs_process_shortcut_special_items($config);
  484. // If:
  485. // - the special_items setting has been configured
  486. // - at least one special item has the 'exclusive' property
  487. // - the dropbox is enabled
  488. // then do the necessary processing to make exclusive lineages possible.
  489. if (!empty($special_items) && count($special_items['exclusive']) && $config['dropbox']['status']) {
  490. // When the form is first loaded, $db_selection will contain the selection
  491. // that we should check, but in updates, $hs_selection will.
  492. $selection = (!empty($hs_selection)) ? $hs_selection : $db_selection;
  493. // If the current selection of the hierarchical select matches one of the
  494. // configured exclusive items, then disable the dropbox (to ensure an
  495. // exclusive selection).
  496. $exclusive_item = array_intersect($selection, $special_items['exclusive']);
  497. if (count($exclusive_item)) {
  498. // By also updating the configuration stored in $element, we ensure that
  499. // the validation step, which extracts the configuration again, also gets
  500. // the updated config.
  501. $element['#config']['dropbox']['status'] = 0;
  502. // Set the hierarchical select to the exclusive item and make the
  503. // dropbox empty.
  504. $hs_selection = array(0 => reset($exclusive_item));
  505. $db_selection = array();
  506. }
  507. }
  508. return array($element, $hs_selection, $db_selection);
  509. }
  510. function _hs_process_render_create_new_item($element, $hierarchy) {
  511. $creating_new_item = FALSE;
  512. // This container and the "Create" / "Cancel" buttons must always be part of
  513. // the form, even when HS is not in create mode, in order for AJAX submit
  514. // callbacks on the "Create" and "Cancel" buttons to be processed correctly.
  515. //
  516. // Basically, FAPI looks through each of the buttons in the form to determine
  517. // which one was clicked. If it can't find the responsible button, it
  518. // assumes it was the first button in the form. This is problematic when the
  519. // user clicks on the "Create" or "Cancel" buttons because we only want them
  520. // to show up when HS is in create mode. To fix this, we always render the
  521. // buttons as part of the form, then disable access to them in an
  522. // "#after_build" callback.
  523. //
  524. // This might not be necessary if we used D7's native AJAX callback function,
  525. // ajax_form_callback().
  526. $element['hierarchical_select']['create_new_item'] = array(
  527. '#prefix' => '<div class="create-new-item">',
  528. '#suffix' => '</div>',
  529. '#after_build' => array('hierarchical_select_create_new_item_after_build'),
  530. );
  531. // @todo Port to use built-in D7 AJAX callback?
  532. $element['hierarchical_select']['create_new_item']['create'] = array(
  533. '#type' => 'submit',
  534. '#value' => t('Create'),
  535. '#attributes' => array('class' => array('create-new-item-create')),
  536. '#limit_validation_errors' => array($element['#parents']),
  537. '#validate' => array(),
  538. '#submit' => array('hierarchical_select_ajax_update_submit'),
  539. );
  540. $element['hierarchical_select']['create_new_item']['cancel'] = array(
  541. '#type' => 'submit',
  542. '#value' => t('Cancel'),
  543. '#attributes' => array('class' => array('create-new-item-cancel')),
  544. '#limit_validation_errors' => array($element['#parents']),
  545. '#validate' => array(),
  546. '#submit' => array('hierarchical_select_ajax_update_submit'),
  547. );
  548. if (isset($element['#value']['hierarchical_select']['selects'])) {
  549. foreach ($element['#value']['hierarchical_select']['selects'] as $depth => $value) {
  550. if ($value == 'create_new_item' && _hierarchical_select_create_new_item_is_allowed($element['#config'], $depth)) {
  551. $creating_new_item = TRUE;
  552. // We want to override the select in which the "create_new_item"
  553. // option was selected and hide all selects after that, if they exist.
  554. // If depth == 0, then that means all selects should be hidden.
  555. if ($depth == 0) {
  556. unset($element['hierarchical_select']['selects']);
  557. }
  558. else {
  559. for ($i = $depth; $i < count($hierarchy->lineage); $i++) {
  560. unset($element['hierarchical_select']['selects'][$i]);
  561. }
  562. }
  563. $item_type_depth = ($value == 'create_new_item') ? $depth : $depth + 1;
  564. $item_type = (count($element['#config']['editability']['item_types']) == $item_type_depth)
  565. ? t($element['#config']['editability']['item_types'][$item_type_depth])
  566. : t('item');
  567. $element['hierarchical_select']['create_new_item']['input'] = array(
  568. '#type' => 'textfield',
  569. '#size' => 20,
  570. '#maxlength' => 255,
  571. '#default_value' => t('new @item', array('@item' => $item_type)),
  572. '#attributes' => array(
  573. 'title' => t('new @item', array('@item' => $item_type)),
  574. 'class' => array('create-new-item-input'),
  575. ),
  576. // Prevent the textfield from being wrapped in a div. This
  577. // simplifies the CSS and JS code.
  578. '#theme_wrappers' => array(),
  579. // Place the textfield above the "Create" / "Cancel" buttons.
  580. '#weight' => -1,
  581. );
  582. }
  583. }
  584. }
  585. $element['hierarchical_select']['create_new_item']['#creating_new_item'] = $creating_new_item;
  586. return array($element, $creating_new_item);
  587. }
  588. /**
  589. * Render API callback: Controls access to the create_new_item form.
  590. *
  591. * Only allows access to the create_new_item form if creating a new item.
  592. *
  593. * This function is assigned as an #after_build callback in
  594. * _hs_process_render_create_new_item().
  595. */
  596. function hierarchical_select_create_new_item_after_build(array $element) {
  597. $element['#access'] = $element['#creating_new_item'];
  598. return $element;
  599. }
  600. function _hs_process_render_dropbox($element, $hsid, $creating_new_item, $dropbox, $form_state) {
  601. $config = $element['#config'];
  602. if ($config['dropbox']['status']) {
  603. if (!$creating_new_item) {
  604. // Append an "Add" button to the selects.
  605. $element['hierarchical_select']['dropbox_add'] = array(
  606. '#type' => 'submit',
  607. '#value' => t('Add'),
  608. '#attributes' => array('class' => array('add-to-dropbox')),
  609. '#limit_validation_errors' => array($element['#parents']),
  610. '#validate' => array(),
  611. '#submit' => array('hierarchical_select_ajax_update_submit'),
  612. );
  613. }
  614. if ($config['dropbox']['limit'] > 0) { // Zero as dropbox limit means no limit.
  615. if (count($dropbox->lineages) >= $config['dropbox']['limit']) {
  616. $element['dropbox_limit_warning'] = array(
  617. '#markup' => t("You've reached the maximum number of items you can select."),
  618. '#prefix' => '<p class="hierarchical-select-dropbox-limit-warning">',
  619. '#suffix' => '</p>',
  620. );
  621. // Disable all child form elements of $element['hierarchical_select].
  622. // _hierarchical_select_mark_as_disabled($element['hierarchical_select']);
  623. // TODO: make the above work again. Currently, we're just disabling
  624. // the "Add" button. #disabled can't be used for the same reasons as
  625. // described in _hierarchical_select_mark_as_disabled().
  626. $element['hierarchical_select']['dropbox_add']['#attributes']['disabled'] = TRUE;
  627. }
  628. }
  629. // Store the currently selected lineages of the dropbox in the form state's
  630. // storage section.
  631. if (isset($dropbox->lineages_selections)) {
  632. $form_state['storage']['hs'][$hsid]['dropbox_lineages_selections'] = $dropbox->lineages_selections;
  633. }
  634. // Add the dropbox-as-a-table that will be visible to the user.
  635. $element['dropbox']['visible'] = _hs_process_render_db_table($hsid, $dropbox);
  636. }
  637. return array($element, $form_state);
  638. }
  639. function _hs_process_render_nojs($element, $config) {
  640. // This button and accompanying help text will be hidden when Javascript is
  641. // enabled.
  642. $element['nojs'] = array(
  643. '#prefix' => '<div class="nojs">',
  644. '#suffix' => '</div>',
  645. );
  646. $element['nojs']['update_button'] = array(
  647. '#type' => 'submit',
  648. '#value' => t('Update'),
  649. '#attributes' => array('class' => array('update-button')),
  650. '#limit_validation_errors' => array($element['#parents']),
  651. '#validate' => array(),
  652. '#submit' => array('hierarchical_select_ajax_update_submit'),
  653. '#ajax' => array(
  654. 'callback' => 'menu_link_weight_parent_ajax_callback',
  655. 'wrapper' => 'menu-link-weight-wrapper',
  656. ),
  657. );
  658. $element['nojs']['update_button_help_text'] = array(
  659. '#markup' => _hierarchical_select_nojs_helptext($config['dropbox']['status']),
  660. '#prefix' => '<div class="help-text">',
  661. '#suffix' => '</div>',
  662. );
  663. return $element;
  664. }
  665. /**
  666. * Hierarchical select form element type #process callback.
  667. */
  668. function form_hierarchical_select_process($element, &$form_state, $complete_form) {
  669. if (arg(0) != 'hierarchical_select_ajax') {
  670. // Get unique identifier using parents of the field.
  671. $cid = isset($element['#parents']) ? implode("-", $element['#parents']) : implode("-", $element['#field_parents']);
  672. // Verify if hsid is present.
  673. $elhsid = drupal_array_get_nested_value($element, array('#value', 'hsid'));
  674. if (!isset($elhsid)) {
  675. // Retrieve previous element from form_state.
  676. $cached = drupal_array_get_nested_value($form_state, array('storage', 'hs', 'hs_fields', $cid));
  677. }
  678. if (empty($cached)) {
  679. $docache = TRUE;
  680. }
  681. else {
  682. // Switch current element with the "cached".
  683. return $cached;
  684. }
  685. }
  686. // Determine the HSID.
  687. $hsid = _hs_process_determine_hsid($element, $form_state);
  688. // Config.
  689. $config = $element['#config'];
  690. // Attach CSS/JS files and JS settings.
  691. $element = _hs_process_attach_css_js($element, $hsid, $form_state, $complete_form);
  692. // Developer mode diagnostics, return immediately in case of a config error.
  693. if (!_hs_process_developer_mode_log_diagnostics($element)) {
  694. return $element;
  695. }
  696. // Calculate the selections in both the hierarchical select and the dropbox,
  697. // we need these before we can render anything.
  698. $hs_selection = $db_selection = array();
  699. list($hs_selection, $db_selection) = _hierarchical_select_process_calculate_selections($element, $hsid, $form_state);
  700. // Developer mode logging: log selections.
  701. _hs_process_developer_mode_log_selections($config, $hs_selection, $db_selection);
  702. // Dynamically disable the dropbox when an exclusive item has been selected.
  703. // When this happens, the configuration is dynamically altered. Hence, we
  704. // need to update $config.
  705. list($element, $hs_selection, $db_selection) = _hs_process_exclusive_lineages($element, $hs_selection, $db_selection);
  706. $config = $element['#config'];
  707. // Generate the $hierarchy and $dropbox objects using the selections that
  708. // were just calculated.
  709. $dropbox = (!$config['dropbox']['status']) ? FALSE : _hierarchical_select_dropbox_generate($config, $db_selection);
  710. $hierarchy = _hierarchical_select_hierarchy_generate($config, $hs_selection, $element['#required'], $dropbox);
  711. // Developer mode logging: log $hierarchy and $dropbox objects.
  712. _hs_process_developer_mode_log_hierarchy_and_dropbox($config, $hierarchy, $dropbox);
  713. // Finally, calculate the return value of this hierarchical_select form
  714. // element. This will be set in _hierarchical_select_validate(). (If we'd
  715. // set it now, it would be overridden again.)
  716. $element['#return_value'] = _hierarchical_select_process_calculate_return_value($hierarchy, ($config['dropbox']['status']) ? $dropbox : FALSE, $config['module'], $config['params'], $config['save_lineage']);
  717. if (!is_array($element['#return_value'])) {
  718. $element['#return_value'] = array($element['#return_value']);
  719. }
  720. // Add a validate callback, which will:
  721. // - validate that the dropbox limit was not exceeded.
  722. // - set the return value of this form element.
  723. // Also make sure it is the *first* validate callback.
  724. $element['#element_validate'] = (isset($element['#element_validate'])) ? $element['#element_validate'] : array();
  725. $element['#element_validate'] = array_merge(array('_hierarchical_select_validate'), $element['#element_validate']);
  726. // Ensure the form is cached, for AJAX to work.
  727. $form_state['cache'] = TRUE;
  728. //
  729. // Rendering.
  730. //
  731. // Ensure that #tree is enabled!
  732. $element['#tree'] = TRUE;
  733. // Store the HSID in a hidden form element; when an AJAX callback comes in,
  734. // we'll know which HS was updated.
  735. $element['hsid'] = array('#type' => 'hidden', '#value' => $hsid);
  736. // If render_flat_select is enabled, render a flat select.
  737. if ($config['render_flat_select']) {
  738. $element['flat_select'] = _hs_process_render_flat_select($hierarchy, $dropbox, $config);
  739. // See https://www.drupal.org/node/994820
  740. if (empty($element['flat_select']['#options'])) {
  741. unset($element['flat_select']);
  742. }
  743. }
  744. // Render the hierarchical select.
  745. $element['hierarchical_select'] = array(
  746. '#theme' => 'hierarchical_select_selects_container',
  747. );
  748. $size = isset($element['#size']) ? $element['#size'] : 0;
  749. $element['hierarchical_select']['selects'] = _hs_process_render_hs_selects($hsid, $hierarchy, $size);
  750. // When the special "create_new_item" value is passed in a level, replace
  751. // that level with an inline modal form to create a new item, and hide all
  752. // subsequent selects.
  753. list($element, $creating_new_item) = _hs_process_render_create_new_item($element, $hierarchy);
  754. // Render the dropbox, if enabled.
  755. // Automatically hides the "Add" button when creating a new item.
  756. // Automatically disables HS' selects when reaching the dropbox limit.
  757. // Stores the currently selected lineages of the dropbox in storage.
  758. list($element, $form_state) = _hs_process_render_dropbox($element, $hsid, $creating_new_item, $dropbox, $form_state);
  759. // Render the HTML that allows for graceful degradation.
  760. $element = _hs_process_render_nojs($element, $config);
  761. // Ensure the render order is correct.
  762. $element['hierarchical_select']['#weight'] = 0;
  763. $element['dropbox_limit_warning']['#weight'] = 1;
  764. $element['dropbox']['#weight'] = 2;
  765. $element['nojs']['#weight'] = 3;
  766. // If the form item is marked as disabled, disable all child form items as
  767. // well.
  768. if (isset($element['#disabled']) && $element['#disabled']) {
  769. _hierarchical_select_mark_as_disabled($element);
  770. }
  771. // This prevents values from in $form_state['input'] to be used instead of
  772. // the generated default values (#default_value).
  773. // For example: $element['hierarchical_select']['selects']['0']['#default_value']
  774. // is set to 'label_0' after an "Add" operation. When $form_state['input']
  775. // is NOT erased, the corresponding value in $form_state['input'] will be
  776. // used instead of the default value that was set. This would result in
  777. // undesired behavior.
  778. // This, however, must not be called on node preview, becuase in that case
  779. // the node will be rebuilt and we need the values inside $form_state['input']
  780. // to recreate the edited form properly.
  781. // @TODO: If the form is rebuilt by some other action than a node preview, we
  782. // might lose data again, we should see if there's any way to prevent this from
  783. // happening by setting this value after the form has been flagged to be rebuilt,
  784. // but as far as I checked, there's not.
  785. // Another option might be to rework the need of this function to prevent
  786. // the undesired behaviors of not having it with some other logic that would
  787. // work as well if the form is rebuilt.
  788. if (empty($docache)) {
  789. if (!isset($form_state['triggering_element']) || ($form_state['triggering_element']['#value'] != t('Preview') && $form_state['triggering_element']['#value'] != t('View changes'))) {
  790. if (isset($form_state['input']) && is_array($form_state['input'])) {
  791. drupal_array_set_nested_value($form_state['input'], $element['#array_parents'], array());
  792. }
  793. }
  794. }
  795. else {
  796. // Store new element in cache.
  797. $form_state['storage']['hs']['hs_fields'][$cid] = $element;
  798. }
  799. // Send the collected developer mode logs (by using #attached JS).
  800. $element = _hs_process_developer_mode_send_log_js($element, $hsid);
  801. return $element;
  802. }
  803. /**
  804. * Submit callback; only sets no_redirect to TRUE (which already)
  805. */
  806. function hierarchical_select_ajax_update_submit($form, &$form_state) {
  807. $form_state['no_redirect'] = TRUE;
  808. }
  809. /**
  810. * Hierarchical select form element #element_validate callback.
  811. */
  812. function _hierarchical_select_validate(&$element, &$form_state) {
  813. // If the dropbox is enabled and a dropbox limit is configured, check if
  814. // this limit is not exceeded.
  815. $hsid = $element['hsid']['#value'];
  816. $config = _hierarchical_select_inherit_default_config($element['#config']);
  817. if ($config['dropbox']['status']) {
  818. if ($config['dropbox']['limit'] > 0) { // Zero as dropbox limit means no limit.
  819. // TRICKY: #element_validate is not called upon the initial rendering
  820. // (i.e. it is assumed that the default value is valid). However,
  821. // Hierarchical Select's config can influence the validity (i.e. how
  822. // many selections may be added to the dropbox). This means it's
  823. // possible the user has actually selected too many items without being
  824. // notified of this.
  825. $lineage_count = count($form_state['storage']['hs'][$hsid]['dropbox_lineages_selections']);
  826. if ($lineage_count > $config['dropbox']['limit']) {
  827. // TRICKY: this should propagate the error down to the children, but
  828. // this doesn't seem to happen, since for example the selects of the
  829. // hierarchical select don't get the error class set. Further
  830. // investigation needed.
  831. form_error(
  832. $element,
  833. t("You've selected %lineage-count items, but you're only allowed to select %dropbox-limit items.",
  834. array(
  835. '%lineage-count' => $lineage_count,
  836. '%dropbox-limit' => $config['dropbox']['limit'],
  837. )
  838. )
  839. );
  840. _hierarchical_select_form_set_error_class($element);
  841. }
  842. }
  843. }
  844. // Set the proper return value. I.e. instead of returning all the values
  845. // that are used for making the hierarchical_select form element type work,
  846. // we pass a flat array of item ids. e.g. for the taxonomy module, this will
  847. // be an array of term ids. If a single item is selected, this will not be
  848. // an array.
  849. // If the form item is disabled, set the default value as the return value,
  850. // because otherwise nothing would be returned (disabled form items are not
  851. // submitted, as described in the HTML standard).
  852. if (isset($element['#disabled']) && $element['#disabled']) {
  853. $element['#return_value'] = $element['#default_value'];
  854. }
  855. $element['#value'] = $element['#return_value'];
  856. form_set_value($element, $element['#value'], $form_state);
  857. // We have to check again for errors. This line is taken litterally from
  858. // form.inc, so it works in an identical way.
  859. if ($element['#required'] &&
  860. (!isset($form_state['submit_handlers'][0]) || $form_state['submit_handlers'][0] !== 'hierarchical_select_ajax_update_submit') &&
  861. (!count($element['#value']) || (is_string($element['#value']) && strlen(trim($element['#value'])) == 0))) {
  862. form_error($element, t('!name field is required.', array('!name' => $element['#title'])));
  863. _hierarchical_select_form_set_error_class($element);
  864. }
  865. }
  866. //----------------------------------------------------------------------------
  867. // Forms API #process callback:
  868. // Calculation of hierarchical select and dropbox selection.
  869. /**
  870. * Get the current (flat) selection of the hierarchical select.
  871. *
  872. * This selection is updatable by the user, because the values are retrieved
  873. * from the selects in $element['hierarchical_select']['selects'].
  874. *
  875. * @param array $element
  876. * A hierarchical_select form element.
  877. * @return array
  878. * An array (bag) containing the ids of the selected items in the
  879. * hierarchical select.
  880. */
  881. function _hierarchical_select_process_get_hs_selection($element) {
  882. $hs_selection = array();
  883. $config = _hierarchical_select_inherit_default_config($element['#config']);
  884. if (!empty($element['#value']['hierarchical_select']['selects'])) {
  885. if ($config['save_lineage']) {
  886. foreach ($element['#value']['hierarchical_select']['selects'] as $key => $value) {
  887. $hs_selection[] = $value;
  888. }
  889. }
  890. else {
  891. foreach ($element['#value']['hierarchical_select']['selects'] as $key => $value) {
  892. $hs_selection[] = $value;
  893. }
  894. $hs_selection = _hierarchical_select_hierarchy_validate($hs_selection, $config['module'], $config['params']);
  895. // Get the last valid value. (Only the deepest item gets saved). Make
  896. // sure $hs_selection is an array at all times.
  897. $hs_selection = ($hs_selection != -1) ? array(end($hs_selection)) : array();
  898. }
  899. }
  900. return $hs_selection;
  901. }
  902. /**
  903. * Get the current (flat) selection of the dropbox.
  904. *
  905. * This selection is not updatable by the user, because the values are
  906. * retrieved from the hidden values in
  907. * $element['dropbox']['hidden']['lineages_selections']. This selection can
  908. * only be updated by the server, i.e. when the user clicks the "Add" button.
  909. * But this selection can still be reduced in size if the user has marked
  910. * dropbox entries (lineages) for removal.
  911. *
  912. * @param $element
  913. * A hierarchical_select form element.
  914. * @param $form_state
  915. * The $form_state array. We need to look at
  916. * $form_state['storage']['hs'][$hsid]['dropbox_lineages_selections']
  917. * to know what to remove.
  918. * @return
  919. * An array (bag) containing the ids of the selected items in the
  920. * dropbox.
  921. */
  922. function _hierarchical_select_process_get_db_selection($element, $hsid, &$form_state) {
  923. $db_selection = array();
  924. if (!empty($form_state['storage']['hs'][$hsid]['dropbox_lineages_selections'])) {
  925. // Check which lineages have been marked for removal by the user.
  926. $remove_from_db_selection = array();
  927. if (isset($element['#value']['dropbox']['visible']['lineages'])) {
  928. foreach ($element['#value']['dropbox']['visible']['lineages'] as $x => $remove_value) {
  929. if ($remove_value['remove'] === '1') {
  930. // $x is of the form "lineage-<number>". Extract the number.
  931. $remove_from_db_selection[] = substr($x, 8);
  932. // By removing the input (POST) reference to the remove checkbox,
  933. // we make sure that on a form rebuild the same remove checkbox,
  934. // which is accessed by index, is not set, preventing a double removal.
  935. // @see https://www.drupal.org/node/1566878#comment-9226261
  936. $elm = &$form_state['input'];
  937. foreach ($element['#parents'] as $parent) {
  938. $elm = &$elm[$parent];
  939. }
  940. unset($elm['dropbox']['visible']['lineages'][$x]['remove']);
  941. }
  942. }
  943. }
  944. // Add all selections to the dropbox selection, except for the ones that
  945. // are scheduled for removal.
  946. foreach ($form_state['storage']['hs'][$hsid]['dropbox_lineages_selections'] as $x => $selection) {
  947. if (!in_array($x, $remove_from_db_selection)) {
  948. $db_selection = array_merge($db_selection, $selection);
  949. }
  950. }
  951. // Ensure that the last item of each selection that was scheduled for
  952. // removal is completely absent from the dropbox selection.
  953. // In case of a tree with multiple parents, the same item can exist in
  954. // different entries, and thus it would stay in the selection. When the
  955. // server then reconstructs all lineages, the lineage we're removing, will
  956. // also be reconstructed: it will seem as if the removing didn't work!
  957. // This will not break removing dropbox entries for hierarchies without
  958. // multiple parents, since items at the deepest level are always unique to
  959. // that specific lineage.
  960. // Easier explanation at http://drupal.org/node/221210#comment-733715.
  961. foreach ($remove_from_db_selection as $key => $x) {
  962. $item = end($form_state['storage']['hs'][$hsid]['dropbox_lineages_selections'][$x]);
  963. $position = array_search($item, $db_selection);
  964. if ($position) {
  965. unset($db_selection[$position]);
  966. }
  967. }
  968. $db_selection = array_unique($db_selection);
  969. }
  970. return $db_selection;
  971. }
  972. /**
  973. * Calculates the flat selections of both the hierarchical select and the
  974. * dropbox.
  975. *
  976. * @param $element
  977. * A hierarchical_select form element.
  978. * @param $form_state
  979. * The $form_state array. We need to look at $form_state['input']['op'], to
  980. * know which operation has occurred.
  981. * @return
  982. * An array of the following structure:
  983. * array(
  984. * $hierarchical_select_selection = array(), // Flat list of selected ids.
  985. * $dropbox_selection = array(),
  986. * )
  987. * with both of the subarrays flat lists of selected ids. The
  988. * _hierarchical_select_hierarchy_generate() and
  989. * _hierarchical_select_dropbox_generate() functions should be applied on
  990. * these respective subarrays.
  991. *
  992. * @see _hierarchical_select_hierarchy_generate()
  993. * @see _hierarchical_select_dropbox_generate()
  994. */
  995. function _hierarchical_select_process_calculate_selections(&$element, $hsid, &$form_state) {
  996. $hs_selection = array(); // hierarchical select selection
  997. $db_selection = array(); // dropbox selection
  998. $config = _hierarchical_select_inherit_default_config($element['#config']);
  999. $dropbox = (bool) $config['dropbox']['status'];
  1000. // When:
  1001. // - no input data was provided (through POST nor GET)
  1002. // - or #value is set directly and not by a Hierarchical Select POST (and
  1003. // therefor set either manually or by another module),
  1004. // then use the value of #default_value, or when available, of #value.
  1005. if (empty($form_state['input']) || (!isset($element['#value']['hierarchical_select']) && !isset($element['#value']['dropbox']))) {
  1006. $value = (!empty($element['#value'])) ? $element['#value'] : $element['#default_value'];
  1007. $value = (is_array($value)) ? $value : array($value);
  1008. if ($dropbox) {
  1009. $db_selection = $value;
  1010. }
  1011. else {
  1012. $hs_selection = $value;
  1013. }
  1014. }
  1015. else {
  1016. $op = (isset($form_state['input']['op']) && isset($form_state['input']['hsid']) && $form_state['input']['hsid'] == $hsid) ? $form_state['input']['op'] : NULL;
  1017. if ($dropbox && $op == t('Add')) {
  1018. $hs_selection = _hierarchical_select_process_get_hs_selection($element);
  1019. $db_selection = _hierarchical_select_process_get_db_selection($element, $hsid, $form_state);
  1020. // Add $hs_selection to $db_selection.
  1021. $db_selection = array_unique(array_merge($db_selection, $hs_selection));
  1022. // Only reset $hs_selection if the user has configured it that way.
  1023. if ((bool) $config['dropbox']['reset_hs']) {
  1024. $hs_selection = array();
  1025. }
  1026. }
  1027. else if ($op == t('Create')) {
  1028. // This code handles both the creation of a new item in an existing
  1029. // level and the creation of an item that also creates a new level.
  1030. $label = trim($element['#value']['hierarchical_select']['create_new_item']['input']);
  1031. $selects = isset($element['#value']['hierarchical_select']['selects']) ? $element['#value']['hierarchical_select']['selects'] : array();
  1032. $depth = count($selects);
  1033. $parent = ($depth > 0) ? end($selects) : 0;
  1034. // Disallow items with empty labels; allow the user again to create a
  1035. // (proper) new item.
  1036. if (empty($label)) {
  1037. $element['#value']['hierarchical_select']['selects'][count($selects)] = 'create_new_item';
  1038. }
  1039. // Ensure that this new item will not violate the max_levels and
  1040. // allowed_levels settings.
  1041. else if (
  1042. (count(module_invoke($config['module'], 'hierarchical_select_children', $parent, $config['params']))
  1043. || $config['editability']['max_levels'] == 0
  1044. || $depth < $config['editability']['max_levels']
  1045. )
  1046. &&
  1047. (_hierarchical_select_create_new_item_is_allowed($config, $depth))
  1048. ) {
  1049. // Create the new item in the hierarchy and retrieve its value.
  1050. $value = module_invoke($config['module'], 'hierarchical_select_create_item', check_plain($label), $parent, $config['params']);
  1051. // Ensure the newly created item will be selected after rendering.
  1052. if ($value) {
  1053. // Pretend there was a select where the "create new item" section
  1054. // was, and assign it the value of the item that was just created.
  1055. $element['#value']['hierarchical_select']['selects'][count($selects)] = $value;
  1056. }
  1057. }
  1058. $hs_selection = _hierarchical_select_process_get_hs_selection($element);
  1059. if ($dropbox) {
  1060. $db_selection = _hierarchical_select_process_get_db_selection($element, $hsid, $form_state);
  1061. }
  1062. }
  1063. else {
  1064. // This handles the cases of:
  1065. // - $op == t('Update')
  1066. // - $op == t('Cancel') (used when creating a new item or a new level)
  1067. // - any other submit button, e.g. the "Preview" button
  1068. $hs_selection = _hierarchical_select_process_get_hs_selection($element);
  1069. if ($dropbox) {
  1070. $db_selection = _hierarchical_select_process_get_db_selection($element, $hsid, $form_state);
  1071. }
  1072. }
  1073. }
  1074. // Prevent doubles in either array.
  1075. $hs_selection = array_unique($hs_selection, SORT_REGULAR);
  1076. $db_selection = array_unique($db_selection, SORT_REGULAR);
  1077. return array($hs_selection, $db_selection);
  1078. }
  1079. //----------------------------------------------------------------------------
  1080. // Forms API #process callback:
  1081. // Rendering (generation of FAPI code) of hierarchical select and dropbox.
  1082. /**
  1083. * Render the selects in the hierarchical select.
  1084. *
  1085. * @param $hsid
  1086. * A hierarchical select id.
  1087. * @param $hierarchy
  1088. * A hierarchy object.
  1089. * @param $size
  1090. * The $size to render each select with.
  1091. * @return
  1092. * A structured array for use in the Forms API.
  1093. */
  1094. function _hs_process_render_hs_selects($hsid, $hierarchy, $size) {
  1095. $form['#tree'] = TRUE;
  1096. $form['#prefix'] = '<div class="selects">';
  1097. $form['#suffix'] = '</div>';
  1098. foreach ($hierarchy->lineage as $depth => $selected_item) {
  1099. $form[$depth] = array(
  1100. '#type' => 'select',
  1101. '#options' => $hierarchy->levels[$depth],
  1102. '#default_value' => $selected_item,
  1103. '#size' => $size,
  1104. // Prevent the select from being wrapped in a div. This simplifies the
  1105. // CSS and JS code.
  1106. '#theme_wrappers' => array(),
  1107. // This alternative to theme_select ets a special class on the level
  1108. // label option, if any, to make level label styles possible.
  1109. '#theme' => 'hierarchical_select_select',
  1110. // Add child information. When a child has no children, its
  1111. // corresponding "option" element will be marked as such.
  1112. '#childinfo' => (isset($hierarchy->childinfo[$depth])) ? $hierarchy->childinfo[$depth] : NULL,
  1113. // Drupal 7's Forms API insists on validating "select" form elements,
  1114. // despite the fact that this form element is merely part of a larger
  1115. // whole, with its own #element_validate callback. This disables that
  1116. // validation.
  1117. '#validated' => TRUE,
  1118. );
  1119. }
  1120. return $form;
  1121. }
  1122. /**
  1123. * Render the visible part of the dropbox.
  1124. *
  1125. * @param $hsid
  1126. * A hierarchical select id.
  1127. * @param $dropbox
  1128. * A dropbox object.
  1129. * @return
  1130. * A structured array for use in the Forms API.
  1131. */
  1132. function _hs_process_render_db_table($hsid, $dropbox) {
  1133. $element['#tree'] = TRUE;
  1134. $element['#theme'] = 'hierarchical_select_dropbox_table';
  1135. // This information is necessary for the #theme callback.
  1136. $element['title'] = array('#type' => 'value', '#value' => t($dropbox->title));
  1137. $element['separator'] = array('#type' => 'value', '#value' => '›');
  1138. $element['is_empty'] = array('#type' => 'value', '#value' => empty($dropbox->lineages));
  1139. if (!empty($dropbox->lineages)) {
  1140. foreach ($dropbox->lineages as $x => $lineage) {
  1141. // Store position information for the lineage. This will be used in the
  1142. // #theme callback.
  1143. $element['lineages']["lineage-$x"] = array(
  1144. '#zebra' => (($x + 1) % 2 == 0) ? 'even' : 'odd',
  1145. '#first' => ($x == 0) ? 'first' : '',
  1146. '#last' => ($x == count($dropbox->lineages) - 1) ? 'last' : '',
  1147. );
  1148. // Create a 'markup' element for each item in the lineage.
  1149. foreach ($lineage as $depth => $item) {
  1150. // The item is selected when save_lineage is enabled (i.e. each item
  1151. // will be selected), or when the item is the last item in the current
  1152. // lineage.
  1153. $is_selected = $dropbox->save_lineage || ($depth == count($lineage) - 1);
  1154. $element['lineages']["lineage-$x"][$depth] = array(
  1155. '#markup' => $item['label'],
  1156. '#prefix' => '<span class="dropbox-item' . (($is_selected) ? ' dropbox-selected-item' : '') . '">',
  1157. '#suffix' => '</span>',
  1158. );
  1159. }
  1160. // Finally, create a "Remove" checkbox for the lineage.
  1161. $element['lineages']["lineage-$x"]['remove'] = array(
  1162. '#type' => 'checkbox',
  1163. '#title' => t('Remove'),
  1164. );
  1165. }
  1166. }
  1167. return $element;
  1168. }
  1169. /**
  1170. * Render a flat select version of a hierarchical_select form element. This is
  1171. * necessary for backwards compatibility (together with some Javascript code)
  1172. * in case of GET forms.
  1173. *
  1174. * @param $hierarchy
  1175. * A hierarchy object.
  1176. * @param $dropbox
  1177. * A dropbox object.
  1178. * @param $config
  1179. * A config array with at least the following settings:
  1180. * - module
  1181. * - params
  1182. * - dropbox
  1183. * - status
  1184. * @return
  1185. * A structured array for use in the Forms API.
  1186. */
  1187. function _hs_process_render_flat_select($hierarchy, $dropbox, $config) {
  1188. $selection = array();
  1189. if ($config['dropbox']['status']) {
  1190. foreach ($dropbox->lineages_selections as $lineage_selection) {
  1191. $selection = array_merge($selection, $lineage_selection);
  1192. }
  1193. }
  1194. else {
  1195. $selection = $hierarchy->lineage;
  1196. }
  1197. $options = array();
  1198. foreach ($selection as $value) {
  1199. $is_valid = module_invoke($config['module'], 'hierarchical_select_valid_item', $value, $config['params']);
  1200. if ($is_valid) {
  1201. $options[$value] = $value;
  1202. }
  1203. }
  1204. $element = array(
  1205. '#type' => 'select',
  1206. '#multiple' => ($config['save_lineage'] || $config['dropbox']['status']),
  1207. '#options' => $options,
  1208. '#value' => array_keys($options),
  1209. // Use a #theme callback to prevent the select from being wrapped in a
  1210. // div. This simplifies the CSS and JS code.
  1211. '#theme' => 'hierarchical_select_select',
  1212. '#attributes' => array('class' => array('flat-select')),
  1213. );
  1214. return $element;
  1215. }
  1216. /**
  1217. * Calculate the return value of a hierarchical_select form element, based on
  1218. * the $hierarchy and $dropbox objects. We have to set a return value, because
  1219. * the values set and used by this form element ($element['#value]) are not
  1220. * easily usable in the Forms API; we want to return a flat list of item ids.
  1221. *
  1222. * @param $hierarchy
  1223. * A hierarchy object.
  1224. * @param $dropbox
  1225. * Optional. A dropbox object.
  1226. * @param $module
  1227. * The module that should be used for HS hooks.
  1228. * @param $params
  1229. * Optional. An array of parameters, which may be necessary for some
  1230. * implementations.
  1231. * @param $save_lineage
  1232. * Whether the save_lineage setting is enabled or not.
  1233. * @return
  1234. * A single item id or a flat array of item ids.
  1235. */
  1236. function _hierarchical_select_process_calculate_return_value($hierarchy, $dropbox = FALSE, $module, $params, $save_lineage) {
  1237. if (!$dropbox) {
  1238. $return_value = _hierarchical_select_hierarchy_validate($hierarchy->lineage, $module, $params);
  1239. // If the save_lineage setting is disabled, keep only the deepest item.
  1240. if (!$save_lineage) {
  1241. $return_value = (is_array($return_value)) ? end($return_value) : NULL;
  1242. }
  1243. // Prevent a return value of -1. -1 is used for HS' internal system and
  1244. // means "nothing selected", but to Drupal it *will* seam like a valid
  1245. // value. Therefore, we set it to NULL.
  1246. $return_value = ($return_value != -1) ? $return_value : NULL;
  1247. }
  1248. else {
  1249. $return_value = array();
  1250. foreach ($dropbox->lineages_selections as $x => $selection) {
  1251. if (!$save_lineage) {
  1252. // An entry in the dropbox when the save_lineage setting is disabled
  1253. // is only the deepest item of the generated lineage.
  1254. $return_value[] = end($selection);
  1255. }
  1256. else {
  1257. // An entry in the dropbox when the save_lineage setting is enabled is
  1258. // the entire generated lineage, if it's valid (i.e. if the user has
  1259. // not tampered with it).
  1260. $lineage = _hierarchical_select_hierarchy_validate($selection, $module, $params);
  1261. $return_value = array_merge($return_value, $lineage);
  1262. }
  1263. }
  1264. $return_value = array_unique($return_value);
  1265. }
  1266. return $return_value;
  1267. }
  1268. //----------------------------------------------------------------------------
  1269. // Private functions.
  1270. /**
  1271. * Inherit the default config from Hierarchical Selects' hook_elements().
  1272. *
  1273. * @param $config
  1274. * A config array with at least the following settings:
  1275. * - module
  1276. * - params
  1277. * @return
  1278. * An updated config array.
  1279. */
  1280. function _hierarchical_select_inherit_default_config($config, $defaults_override = array()) {
  1281. // Set defaults for unconfigured settings. Get the defaults from our
  1282. // hook_elements() implementation. Default properties from this hook are
  1283. // applied automatically, but properties inside properties, such as is the
  1284. // case for Hierarchical Select's #config property, aren't applied.
  1285. $type = hierarchical_select_element_info();
  1286. $defaults = $type['hierarchical_select']['#config'];
  1287. // Don't inherit the module and params settings.
  1288. unset($defaults['module']);
  1289. unset($defaults['params']);
  1290. // Allow the defaults to be overridden.
  1291. $defaults = array_smart_merge($defaults, $defaults_override);
  1292. // Apply the defaults to the config.
  1293. $config = array_smart_merge($defaults, $config);
  1294. return $config;
  1295. }
  1296. /**
  1297. * Convert a hierarchy object into an array of arrays that can be used for
  1298. * caching an entire hierarchy in a client-side database.
  1299. *
  1300. * @param $hierarchy
  1301. * A hierarchy object.
  1302. * @return
  1303. * An array of arrays.
  1304. */
  1305. function _hierarchical_select_json_convert_hierarchy_to_cache($hierarchy) {
  1306. // Convert the hierarchy object to an array of values like these:
  1307. // array('value' => $term_id, 'label => $term_name, 'parent' => $term_id)
  1308. $cache = array();
  1309. foreach ($hierarchy->levels as $depth => $items) {
  1310. $weight = 0;
  1311. foreach ($items as $value => $label) {
  1312. $weight++;
  1313. $cache[] = array(
  1314. 'value' => $value,
  1315. 'label' => $label,
  1316. 'parent' => ($depth == 0) ? 0 : $hierarchy->lineage[$depth - 1],
  1317. 'weight' => $weight,
  1318. );
  1319. }
  1320. }
  1321. // The last item in the lineage never has any children.
  1322. $value = end($hierarchy->lineage);
  1323. $cache[] = array(
  1324. 'value' => $value . '-has-no-children', // Construct a pseudo-value (will never be actually used).
  1325. 'label' => '',
  1326. 'parent' => $value,
  1327. 'weight' => 0,
  1328. );
  1329. return $cache;
  1330. }
  1331. /**
  1332. * Helper function that marks every element in the given element as disabled.
  1333. *
  1334. * @param &$element
  1335. * The element of which we want to mark all elements as disabled.
  1336. * @return
  1337. * A structured array for use in the Forms API.
  1338. */
  1339. function _hierarchical_select_mark_as_disabled(&$element) {
  1340. // Setting $element['#disabled'] = TRUE resulted in undesired side-effects:
  1341. // when the dropbox limit would be reached after pressing the "Add" button,
  1342. // then the *entire form* would be submitted. Using #attributes instead does
  1343. // not trigger this behavior.
  1344. // Based on documentation of @see _form_builder_handle_input_element():
  1345. // "If a form wants to start a control off with one of these attributes
  1346. // for UI purposes only, but still allow input to be processed if it's
  1347. // sumitted, it can set the desired attribute in #attributes directly
  1348. // rather than using #disabled."
  1349. // #disabled prevents #value from containing values for disabled elements,
  1350. // but using #attributes circumvents this. Most likely, Form API thinks that
  1351. // because HS' selects are disabled, that the whole of HS is disabled (which
  1352. // is of course a wrong assumption). Hence it thinks the 'op' that is being
  1353. // passed ('Add') is wrong and is forcefully being set through JS (which is
  1354. // also a wrong assumption). Hence it reverts to the main form's default
  1355. // submit handler.
  1356. $element['#attributes']['disabled'] = TRUE;
  1357. // Recurse through all children:
  1358. foreach (element_children($element) as $key) {
  1359. if (isset($element[$key]) && $element[$key]) {
  1360. _hierarchical_select_mark_as_disabled($element[$key]);
  1361. }
  1362. }
  1363. }
  1364. /**
  1365. * Helper function to determine whether a given depth (i.e. the depth of a
  1366. * level) is allowed by the allowed_levels setting.
  1367. *
  1368. * @param $config
  1369. * A config array with at least the following settings:
  1370. * - editability
  1371. * - allowed_levels
  1372. * @param $depth
  1373. * A depth, starting from 0.
  1374. * @return
  1375. * 0 or 1 if it allowed_levels is set for the given depth, 1 otherwise.
  1376. */
  1377. function _hierarchical_select_create_new_item_is_allowed($config, $depth) {
  1378. return (isset($config['editability']['allowed_levels'][$depth])) ? $config['editability']['allowed_levels'][$depth] : 1;
  1379. }
  1380. /**
  1381. * Helper function that generates the help text is that is displayed to the
  1382. * user when Javascript is disabled.
  1383. *
  1384. * @param $dropbox_is_enabled
  1385. * Indicates if the dropbox is enabled or not, the help text will be
  1386. * adjusted depending on this value.
  1387. * @return
  1388. * The generated help text (in HTML).
  1389. */
  1390. function _hierarchical_select_nojs_helptext($dropbox_is_enabled) {
  1391. $output = '<noscript>';
  1392. // The options that will be used in the unordered list.
  1393. $items = array(
  1394. t('<span class="highlight">enable Javascript</span> in your browser and then refresh this page, for a much enhanced experience.'),
  1395. t('<span class="highlight">click the <em>Update</em> button</span> every time you want to update the selection'),
  1396. );
  1397. $items[1] .= (!$dropbox_is_enabled) ? '.' : t(", or when you've checked some checkboxes for entries in the dropbox you'd like to remove.");
  1398. $output .= '<span class="warning">';
  1399. $output .= t("You don't have Javascript enabled.");
  1400. $output .= '</span> ';
  1401. $output .= '<span class="ask-to-hover">';
  1402. $output .= t('Hover for more information!');
  1403. $output .= '</span> ';
  1404. $output .= t("But don't worry: you can still use this web site! You have two options:");
  1405. $output .= theme('item_list', array('items' => $items, 'title' => NULL, 'type' => 'ul', 'attributes' => array('class' => array('solutions'))));
  1406. $output .= '</noscript>';
  1407. return $output;
  1408. }
  1409. /**
  1410. * Set the 'error' class on the appropriate part of Hierarchical Select,
  1411. * depending on its configuration.
  1412. *
  1413. * @param $element
  1414. * A Hierarchical Select form item.
  1415. */
  1416. function _hierarchical_select_form_set_error_class(&$element) {
  1417. $config = _hierarchical_select_inherit_default_config($element['#config']);
  1418. if ($config['dropbox']['status']) {
  1419. form_error($element['dropbox']['visible']);
  1420. }
  1421. else {
  1422. for ($i = 0; $i < count(element_children($element['hierarchical_select']['selects'])); $i++) {
  1423. form_error($element['hierarchical_select']['selects'][$i]);
  1424. }
  1425. }
  1426. }
  1427. /**
  1428. * Append messages to Hierarchical Select's log. Used when in developer mode.
  1429. *
  1430. * @param $item
  1431. * Either a message (string) or an array.
  1432. * @param $reset
  1433. * Reset the stored log.
  1434. * @return
  1435. * Only when the log is being reset, the stored log is returned.
  1436. */
  1437. function _hierarchical_select_log($item, $reset = FALSE) {
  1438. static $log;
  1439. if ($reset) {
  1440. $copy_of_log = $log;
  1441. $log = array();
  1442. return $copy_of_log;
  1443. }
  1444. $log[] = $item;
  1445. }
  1446. //----------------------------------------------------------------------------
  1447. // Hierarchy object generation functions.
  1448. /**
  1449. * Generate the hierarchy object.
  1450. *
  1451. * @param $config
  1452. * A config array with at least the following settings:
  1453. * - module
  1454. * - params
  1455. * - enforce_deepest
  1456. * - save_lineage
  1457. * - level_labels
  1458. * - status
  1459. * - labels
  1460. * - editability
  1461. * - status
  1462. * - allow_new_levels
  1463. * - max_levels
  1464. * @param $selection
  1465. * The selection based on which a HS should be rendered.
  1466. * @param $required
  1467. * Whether the form element is required or not. (#required in Forms API)
  1468. * @param $dropbox
  1469. * A dropbox object, or FALSE.
  1470. * @return
  1471. * A hierarchy object.
  1472. */
  1473. function _hierarchical_select_hierarchy_generate($config, $selection, $required, $dropbox = FALSE) {
  1474. $hierarchy = new stdClass();
  1475. // Convert the 'special_items' setting to a more easily accessible format.
  1476. if (isset($config['special_items'])) {
  1477. $special_items['exclusive'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_exclusive'));
  1478. $special_items['none'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_none'));
  1479. }
  1480. //
  1481. // Build the lineage.
  1482. //
  1483. $start_lineage = microtime();
  1484. // If save_linage is enabled, reconstruct the lineage. This is necessary
  1485. // because e.g. the taxonomy module stores the terms by order of weight and
  1486. // lexicography, rather than by hierarchy.
  1487. if ($config['save_lineage'] && is_array($selection) && count($selection) >= 2) {
  1488. // Ensure the item in the root level is the first item in the selection.
  1489. $root_level = array_keys(module_invoke($config['module'], 'hierarchical_select_root_level', $config['params']));
  1490. for ($i = 0; $i < count($selection); $i++) {
  1491. if (in_array($selection[$i], $root_level)) {
  1492. if ($i != 0) { // Don't swap if it's already the first item.
  1493. list($selection[0], $selection[$i]) = array($selection[$i], $selection[0]);
  1494. }
  1495. break;
  1496. }
  1497. }
  1498. // Reconstruct all sublevels.
  1499. for ($i = 0; $i < count($selection); $i++) {
  1500. $children = array_keys(module_invoke($config['module'], 'hierarchical_select_children', $selection[$i], $config['params']));
  1501. // Ensure the next item in the selection is a child of the current item.
  1502. for ($j = $i + 1; $j < count($selection); $j++) {
  1503. if (in_array($selection[$j], $children)) {
  1504. list($selection[$j], $selection[$i + 1]) = array($selection[$i + 1], $selection[$j]);
  1505. }
  1506. }
  1507. }
  1508. }
  1509. // Validate the hierarchy.
  1510. $selection = _hierarchical_select_hierarchy_validate($selection, $config['module'], $config['params']);
  1511. // When nothing is currently selected, set the root level to:
  1512. // - "<none>" (or its equivalent special item) when:
  1513. // - enforce_deepest is enabled *and* level labels are enabled *and*
  1514. // no root level label is set (1), or
  1515. // - the dropbox is enabled *and* at least one selection has been added
  1516. // to the dropbox (2)
  1517. // - "label_0" (the root level label) in all other cases.
  1518. if ($selection == -1) {
  1519. $root_level = module_invoke($config['module'], 'hierarchical_select_root_level', $config['params']);
  1520. $first_case = $config['enforce_deepest'] && $config['level_labels']['status'] && !isset($config['level_labels']['labels'][0]);
  1521. $second_case = $dropbox && count($dropbox->lineages) > 0;
  1522. // If
  1523. // - the special_items setting has been configured, and
  1524. // - one special item has the 'none' property
  1525. // then we'll use the special item instead of the normal "<none>" option.
  1526. $none_option = (isset($special_items) && count($special_items['none'])) ? $special_items['none'][0] : 'none';
  1527. // Set "<none>" option (or its equivalent special item), or "label_0".
  1528. $hierarchy->lineage[0] = ($first_case || $second_case) ? $none_option : 'label_0';
  1529. }
  1530. else {
  1531. // If save_lineage setting is enabled, then the selection *is* a lineage.
  1532. // If it's disabled, we have to generate one ourselves based on the
  1533. // (deepest) selected item.
  1534. if ($config['save_lineage']) {
  1535. // When the form element is optional, the "<none>" setting can be
  1536. // selected, thus only the first level will be displayed. As a result,
  1537. // we won't receive an array as the selection, but only a single item.
  1538. // We convert this into an array.
  1539. $hierarchy->lineage = (is_array($selection)) ? $selection : array(0 => $selection);
  1540. }
  1541. else {
  1542. $selection = (is_array($selection)) ? $selection[0] : $selection;
  1543. if (module_invoke($config['module'], 'hierarchical_select_valid_item', $selection, $config['params'])) {
  1544. $hierarchy->lineage = module_invoke($config['module'], 'hierarchical_select_lineage', $selection, $config['params']);
  1545. }
  1546. else {
  1547. // If the selected item is invalid, then start with an empty lineage.
  1548. $hierarchy->lineage = array();
  1549. }
  1550. }
  1551. }
  1552. // If enforce_deepest is enabled, ensure that the lineage goes as deep as
  1553. // possible: append values of items that will be selected by default.
  1554. if ($config['enforce_deepest'] && !in_array($hierarchy->lineage[0], array('none', 'label_0'))) {
  1555. $hierarchy->lineage = _hierarchical_select_hierarchy_enforce_deepest($hierarchy->lineage, $config['module'], $config['params']);
  1556. }
  1557. $end_lineage = microtime();
  1558. //
  1559. // Build the levels.
  1560. //
  1561. $start_levels = microtime();
  1562. // Start building the levels, initialize with the root level.
  1563. $hierarchy->levels[0] = module_invoke($config['module'], 'hierarchical_select_root_level', $config['params']);
  1564. $hierarchy->levels[0] = _hierarchical_select_apply_entity_settings($hierarchy->levels[0], $config);
  1565. // Prepend a "<create new item>" option to the root level when:
  1566. // - the editability setting is enabled, and
  1567. // - the hook is implemented (this is an optional hook), and
  1568. // - the logged in user has permission to edit terms in this vocabulary, and
  1569. // - the allowed_levels setting allows to create new items at this level.
  1570. if (!empty($config['editability']['status'])
  1571. && module_hook($config['module'], 'hierarchical_select_create_item')
  1572. && ($config['module'] == 'hs_taxonomy' && (user_access('administer taxonomy') || user_access('edit terms in ' . $config['params']['vid'])))
  1573. && _hierarchical_select_create_new_item_is_allowed($config, 0)
  1574. ) {
  1575. $item_type = (isset($config['editability']['item_types']) && count($config['editability']['item_types']) > 0)
  1576. ? t($config['editability']['item_types'][0])
  1577. : t('item');
  1578. $option = theme('hierarchical_select_special_option', array('option' => t('create new !item_type', array('!item_type' => $item_type))));
  1579. $hierarchy->levels[0] = array('create_new_item' => $option) + $hierarchy->levels[0];
  1580. }
  1581. // Prepend a "<none>" option to the root level when:
  1582. // - the form element is optional (1), or
  1583. // - enforce_deepest is enabled (2), or
  1584. // - the dropbox is enabled *and* at least one selection has been added to
  1585. // the dropbox (3)
  1586. // except when:
  1587. // - level labels are enabled
  1588. // - the special_items setting has been configured, and
  1589. // - one special item has the 'none' property
  1590. $first_case = !$required;
  1591. $second_case = $config['enforce_deepest'];
  1592. $third_case = $dropbox && count($dropbox->lineages) > 0;
  1593. if (($first_case || $second_case || $third_case) && (!$config['level_labels']['status'] && isset($special_items) && !count($special_items['none']))) {
  1594. $option = theme('hierarchical_select_special_option', array('option' => t('none')));
  1595. $hierarchy->levels[0] = array('none' => $option) + $hierarchy->levels[0];
  1596. }
  1597. // Calculate the lineage's depth (starting from 0).
  1598. $max_depth = count($hierarchy->lineage) - 1;
  1599. // Build all sublevels, based on the lineage.
  1600. for ($depth = 1; $depth <= $max_depth; $depth++) {
  1601. $hierarchy->levels[$depth] = module_invoke($config['module'], 'hierarchical_select_children', $hierarchy->lineage[$depth - 1], $config['params']);
  1602. $hierarchy->levels[$depth] = _hierarchical_select_apply_entity_settings($hierarchy->levels[$depth], $config);
  1603. }
  1604. if ($config['enforce_deepest']) {
  1605. // Prepend a "<create new item>" option to each level below the root level
  1606. // when:
  1607. // - the editability setting is enabled, and
  1608. // - the hook is implemented (this is an optional hook), and
  1609. // - the allowed_levels setting allows to create new items at this level.
  1610. if (!empty($config['editability']['status'])
  1611. && ($config['module'] == 'hs_taxonomy' && (user_access('administer taxonomy') || user_access('edit terms in ' . $config['params']['vid'])))
  1612. && module_hook($config['module'], 'hierarchical_select_create_item')) {
  1613. for ($depth = 1; $depth <= $max_depth; $depth++) {
  1614. $item_type = (count($config['editability']['item_types']) >= $depth)
  1615. ? t($config['editability']['item_types'][$depth])
  1616. : t('item');
  1617. $option = theme('hierarchical_select_special_option', array('option' => t('create new !item_type', array('!item_type' => $item_type))));
  1618. if (_hierarchical_select_create_new_item_is_allowed($config, $depth)) {
  1619. $hierarchy->levels[$depth] = array('create_new_item' => $option) + $hierarchy->levels[$depth];
  1620. }
  1621. }
  1622. }
  1623. // If level labels are enabled and the root label is set, prepend it.
  1624. if ($config['level_labels']['status'] && isset($config['level_labels']['labels'][0])) {
  1625. $hierarchy->levels[0] = array('label_0' => t($config['level_labels']['labels'][0])) + $hierarchy->levels[0];
  1626. }
  1627. }
  1628. else if (!$config['enforce_deepest']) {
  1629. // Prepend special options to every level.
  1630. for ($depth = 0; $depth <= $max_depth; $depth++) {
  1631. // Prepend a "<create new item>" option to the current level when:
  1632. // - this is not the root level (the root level already has this), and
  1633. // - the editability setting is enabled, and
  1634. // - the hook is implemented (this is an optional hook), and
  1635. // - the logged in user has permission to edit terms in this vocabulary, and
  1636. // - the allowed_levels setting allows to create new items at this level.
  1637. if ($depth > 0
  1638. && !empty($config['editability']['status'])
  1639. && module_hook($config['module'], 'hierarchical_select_create_item')
  1640. && ($config['module'] == 'hs_taxonomy' && (user_access('administer taxonomy') || user_access('edit terms in ' . $config['params']['vid'])))
  1641. && _hierarchical_select_create_new_item_is_allowed($config, $depth)
  1642. ) {
  1643. $item_type = (count($config['editability']['item_types']) == $depth)
  1644. ? t($config['editability']['item_types'][$depth])
  1645. : t('item');
  1646. $option = theme('hierarchical_select_special_option', array('option' => t('create new !item_type', array('!item_type' => $item_type))));
  1647. $hierarchy->levels[$depth] = array('create_new_item' => $option) + $hierarchy->levels[$depth];
  1648. }
  1649. // Level label: set an empty level label if they've been disabled.
  1650. $label = ($config['level_labels']['status'] && isset($config['level_labels']['labels'][$depth])) ? t($config['level_labels']['labels'][$depth]) : '';
  1651. $hierarchy->levels[$depth] = array('label_' . $depth => $label) + $hierarchy->levels[$depth];
  1652. }
  1653. // If the root level label is empty and the none option is present, remove
  1654. // the root level label because it's conceptually identical.
  1655. if ($hierarchy->levels[0]['label_0'] == '' && isset($hierarchy->levels[0]['none'])) {
  1656. unset($hierarchy->levels[0]['label_0']);
  1657. // Update the selected lineage when necessary to prevent an item that
  1658. // doesn't exist from being "selected" internally.
  1659. if ($hierarchy->lineage[0] == 'label_0') {
  1660. $hierarchy->lineage[0] = 'none';
  1661. }
  1662. }
  1663. // Add one more level if appropriate.
  1664. $parent = $hierarchy->lineage[$max_depth];
  1665. if (module_invoke($config['module'], 'hierarchical_select_valid_item', $parent, $config['params'])) {
  1666. $children = module_invoke($config['module'], 'hierarchical_select_children', $parent, $config['params']);
  1667. if (count($children)) {
  1668. // We're good, let's add one level!
  1669. $depth = $max_depth + 1;
  1670. $hierarchy->levels[$depth] = array();
  1671. // Prepend a "<create new item>" option to the current level when:
  1672. // - the editability setting is enabled, and
  1673. // - the hook is implemented (this is an optional hook), and
  1674. // - the logged in user has permission to edit terms in this vocabulary, and
  1675. // - the allowed_levels setting allows to create new items at this level.
  1676. if (!empty($config['editability']['status'])
  1677. && module_hook($config['module'], 'hierarchical_select_create_item')
  1678. && ($config['module'] == 'hs_taxonomy' && (user_access('administer taxonomy') || user_access('edit terms in ' . $config['params']['vid'])))
  1679. && _hierarchical_select_create_new_item_is_allowed($config, $depth)
  1680. ) {
  1681. $item_type = (count($config['editability']['item_types']) >= $depth)
  1682. ? t($config['editability']['item_types'][$depth])
  1683. : t('item');
  1684. $option = theme('hierarchical_select_special_option', array('option' => t('create new !item_type', array('!item_type' => $item_type))));
  1685. $hierarchy->levels[$depth] = array('create_new_item' => $option);
  1686. }
  1687. // Level label: set an empty level label if they've been disabled.
  1688. $hierarchy->lineage[$depth] = 'label_' . $depth;
  1689. $label = ($config['level_labels']['status']) ? t($config['level_labels']['labels'][$depth]) : '';
  1690. $hierarchy->levels[$depth] = array('label_' . $depth => $label) + $hierarchy->levels[$depth] + $children;
  1691. $hierarchy->levels[$depth] = _hierarchical_select_apply_entity_settings($hierarchy->levels[$depth], $config);
  1692. }
  1693. }
  1694. }
  1695. // Add an extra level with only a level label and a "<create new item>"
  1696. // option, if:
  1697. // - the editability setting is enabled
  1698. // - the allow_new_levels setting is enabled
  1699. // - an additional level is permitted by the max_levels setting
  1700. // - the logged in user has permission to edit terms in this vocabulary
  1701. // - the deepest item of the lineage is a valid item
  1702. // NOTE: this uses an optional hook, so we also check if it's implemented.
  1703. if (!empty($config['editability']['status'])
  1704. && !empty($config['editability']['allow_new_levels'])
  1705. && ($config['editability']['max_levels'] == 0 || count($hierarchy->lineage) < $config['editability']['max_levels'])
  1706. && module_invoke($config['module'], 'hierarchical_select_valid_item', end($hierarchy->lineage), $config['params'])
  1707. && ($config['module'] == 'hs_taxonomy' && (user_access('administer taxonomy') || user_access('edit terms in ' . $config['params']['vid'])))
  1708. && module_hook($config['module'], 'hierarchical_select_create_item')
  1709. ) {
  1710. $depth = $max_depth + 1;
  1711. // Level label: set an empty level label if they've been disabled.
  1712. $hierarchy->lineage[$depth] = 'label_' . $depth;
  1713. $label = ($config['level_labels']['status']) ? t($config['level_labels']['labels'][$depth]) : '';
  1714. // Item type.
  1715. $item_type = (count($config['editability']['item_types']) >= $depth)
  1716. ? t($config['editability']['item_types'][$depth])
  1717. : t('item');
  1718. // The new level with only a level label and a "<create new item>" option.
  1719. $option = theme('hierarchical_select_special_option', array('option' => t('create new !item_type', array('!item_type' => $item_type))));
  1720. $hierarchy->levels[$depth] = array(
  1721. 'label_' . $depth => $label,
  1722. 'create_new_item' => $option,
  1723. );
  1724. }
  1725. // Calculate the time it took to generate the levels.
  1726. $end_levels = microtime();
  1727. // Add child information.
  1728. $start_childinfo = microtime();
  1729. $hierarchy = _hierarchical_select_hierarchy_add_childinfo($hierarchy, $config);
  1730. $end_childinfo = microtime();
  1731. // Calculate the time it took to build the hierarchy object.
  1732. $hierarchy->build_time['total'] = ($end_childinfo - $start_lineage) * 1000;
  1733. $hierarchy->build_time['lineage'] = ($end_lineage - $start_lineage) * 1000;
  1734. $hierarchy->build_time['levels'] = ($end_levels - $start_levels) * 1000;
  1735. $hierarchy->build_time['childinfo'] = ($end_childinfo - $start_childinfo) * 1000;
  1736. return $hierarchy;
  1737. }
  1738. /**
  1739. * Given a level, apply the entity_count and require_entity settings.
  1740. *
  1741. * @param $level
  1742. * A level in the hierarchy.
  1743. * @param $config
  1744. * A config array with at least the following settings:
  1745. * - module
  1746. * - params
  1747. * - entity_count
  1748. * - require_entity
  1749. * @return
  1750. * The updated level
  1751. */
  1752. function _hierarchical_select_apply_entity_settings($level, $config) {
  1753. if (isset($config['special_items'])) {
  1754. $special_items['exclusive'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_exclusive'));
  1755. $special_items['none'] = array_keys(array_filter($config['special_items'], '_hierarchical_select_special_item_none'));
  1756. }
  1757. // Only do something when the entity_count or the require_entity (or both)
  1758. // settings are enabled.
  1759. // NOTE: this uses the optional "hierarchical_select_entity_count" hook, so
  1760. // we also check if it's implemented.
  1761. if (isset($config['entity_count']['enabled']) && ($config['entity_count']['enabled'] || $config['entity_count']['require_entity']) && module_hook($config['module'], 'hierarchical_select_entity_count')) {
  1762. foreach ($level as $item => $label) {
  1763. // We don't want to alter internal or special items.
  1764. if (!preg_match('/(none|label_\d+|create_new_item)/', $item)
  1765. && !in_array($item, $special_items['exclusive'])
  1766. && !in_array($item, $special_items['none'])
  1767. ) {
  1768. // Add our entity count settings to the parameters.
  1769. $config['params'] += array(
  1770. 'entity_count' => array(
  1771. 'settings' => array(
  1772. 'count_children' => $config['entity_count']['settings']['count_children'],
  1773. 'entity_types' => $config['entity_count']['settings']['entity_types'],
  1774. ),
  1775. ),
  1776. );
  1777. $entity_count = module_invoke($config['module'], 'hierarchical_select_entity_count', $item, $config['params']);
  1778. // When the require_entity setting is enabled and the entity count is
  1779. // zero, then remove the item from the level.
  1780. // When the item is not removed from the level due to the above and
  1781. // the entity_count setting is enabled, update the label of the item
  1782. // to include the entity count.
  1783. if ($config['entity_count']['require_entity'] && $entity_count == 0) {
  1784. unset($level[$item]);
  1785. }
  1786. elseif ($config['entity_count']['enabled']) {
  1787. $level[$item] = "$label ($entity_count)";
  1788. }
  1789. }
  1790. }
  1791. }
  1792. return $level;
  1793. }
  1794. /**
  1795. * Extends a hierarchy object with child information: for each item in the
  1796. * hierarchy, the child count will be retrieved and stored in the hierarchy
  1797. * object, in the "childinfo" property. Items are grouped per level.
  1798. *
  1799. * @param $hierarchy
  1800. * A hierarchy object with the "levels" property set.
  1801. * @param $config
  1802. * A config array with at least the following settings:
  1803. * - module
  1804. * - params
  1805. * @return
  1806. * An updated hierarchy object with the "childinfo" property set.
  1807. */
  1808. function _hierarchical_select_hierarchy_add_childinfo($hierarchy, $config) {
  1809. foreach ($hierarchy->levels as $depth => $level) {
  1810. foreach (array_keys($level) as $item) {
  1811. if (!preg_match('/(none|label_\d+|create_new_item)/', $item)) {
  1812. $hierarchy->childinfo[$depth][$item] = count(module_invoke($config['module'], 'hierarchical_select_children', $item, $config['params']));
  1813. }
  1814. }
  1815. }
  1816. return $hierarchy;
  1817. }
  1818. /**
  1819. * Reset the selection if no valid item was selected. The first item in the
  1820. * array corresponds to the first selected term. As soon as an invalid item
  1821. * is encountered, the lineage from that level to the deeper levels should be
  1822. * unset. This is so to ignore selection of a level label.
  1823. *
  1824. * @param $selection
  1825. * Either a single item id or an array of item ids.
  1826. * @param $module
  1827. * The module that should be used for HS hooks.
  1828. * @param $params
  1829. * The module that should be passed to HS hooks.
  1830. * @return
  1831. * The updated selection.
  1832. */
  1833. function _hierarchical_select_hierarchy_validate($selection, $module, $params) {
  1834. $valid = TRUE;
  1835. $selection_levels = count($selection);
  1836. for ($i = 0; $i < $selection_levels; $i++) {
  1837. // As soon as one invalid item has been found, we'll stop validating; all
  1838. // subsequently selected items will be removed from the selection.
  1839. if ($valid) {
  1840. $valid = module_invoke($module, 'hierarchical_select_valid_item', $selection[$i], $params);
  1841. if ($i > 0) {
  1842. $parent = $selection[$i - 1];
  1843. $child = $selection[$i];
  1844. $children = array_keys(module_invoke($module, 'hierarchical_select_children', $parent, $params));
  1845. $valid = $valid && in_array($child, $children);
  1846. }
  1847. }
  1848. if (!$valid) {
  1849. unset($selection[$i]);
  1850. }
  1851. }
  1852. if (empty($selection)) {
  1853. $selection = -1;
  1854. }
  1855. if (is_array($selection)) {
  1856. // This is needed because we may have unset some values and we don't want
  1857. // any gaps in the indexes (ie. the indexes would be 0,1,3 if we did
  1858. // "$selection[] = X" after unsetting #2).
  1859. $selection = array_values($selection);
  1860. }
  1861. return $selection;
  1862. }
  1863. /**
  1864. * Helper function to update the lineage of the hierarchy to ensure that the
  1865. * user selects an item in the deepest level of the hierarchy.
  1866. *
  1867. * @param $lineage
  1868. * The lineage up to the deepest selection the user has made so far.
  1869. * @param $module
  1870. * The module that should be used for HS hooks.
  1871. * @param $params
  1872. * The params that should be passed to HS hooks.
  1873. * @return
  1874. * The updated lineage.
  1875. */
  1876. function _hierarchical_select_hierarchy_enforce_deepest($lineage, $module, $params) {
  1877. // Use the deepest item as the first parent. Then apply this algorithm:
  1878. // 1) get the parent's children, stop if no children
  1879. // 2) choose the first child as the option that is selected by default, by
  1880. // adding it to the lineage of the hierarchy
  1881. // 3) make this child the parent, go to step 1.
  1882. $parent = end($lineage); // The last item in the lineage is the deepest one.
  1883. $children = module_invoke($module, 'hierarchical_select_children', $parent, $params);
  1884. while (count($children)) {
  1885. $keys = array_keys($children);
  1886. $first_child = $keys[0];
  1887. $lineage[] = $first_child;
  1888. $parent = $first_child;
  1889. $children = module_invoke($module, 'hierarchical_select_children', $parent, $params);
  1890. }
  1891. return $lineage;
  1892. }
  1893. //----------------------------------------------------------------------------
  1894. // Dropbox object generation functions.
  1895. /**
  1896. * Generate the dropbox object.
  1897. *
  1898. * @param $config
  1899. * A config array with at least the following settings:
  1900. * - module
  1901. * - save_lineage
  1902. * - params
  1903. * - dropbox
  1904. * - title
  1905. * @param $selection
  1906. * The selection based on which a dropbox should be generated.
  1907. * @return
  1908. * A dropbox object.
  1909. */
  1910. function _hierarchical_select_dropbox_generate($config, $selection) {
  1911. $dropbox = new stdClass();
  1912. $start = microtime();
  1913. $dropbox->title = (!empty($config['dropbox']['title'])) ? filter_xss_admin($config['dropbox']['title']) : t('All selections');
  1914. $dropbox->lineages = array();
  1915. $dropbox->lineages_selections = array();
  1916. // Clean selection.
  1917. foreach ($selection as $key => $item) {
  1918. if (!module_invoke($config['module'], 'hierarchical_select_valid_item', $item, $config['params'])) {
  1919. unset($selection[$key]);
  1920. }
  1921. }
  1922. if (!empty($selection)) {
  1923. // Store the "save lineage" setting, needed in the rendering layer.
  1924. $dropbox->save_lineage = $config['save_lineage'];
  1925. if ($config['save_lineage']) {
  1926. $dropbox->lineages = _hierarchical_select_dropbox_reconstruct_lineages_save_lineage_enabled($config['module'], $selection, $config['params']);
  1927. }
  1928. else {
  1929. // Retrieve the lineage of each item.
  1930. foreach ($selection as $item) {
  1931. $dropbox->lineages[] = module_invoke($config['module'], 'hierarchical_select_lineage', $item, $config['params']);
  1932. }
  1933. // We will also need the labels of each item in the rendering layer.
  1934. foreach ($dropbox->lineages as $id => $lineage) {
  1935. foreach ($lineage as $level => $item) {
  1936. $dropbox->lineages[$id][$level] = array('value' => $item, 'label' => module_invoke($config['module'], 'hierarchical_select_item_get_label', $item, $config['params']));
  1937. }
  1938. }
  1939. }
  1940. // Sanitize the labels.
  1941. foreach ($dropbox->lineages as $id => $lineage) {
  1942. foreach ($lineage as $level => $item) {
  1943. $dropbox->lineages[$id][$level]['label'] = check_plain($dropbox->lineages[$id][$level]['label']);
  1944. }
  1945. }
  1946. if (!isset($config['dropbox']['sort']) || $config['dropbox']['sort']){
  1947. usort($dropbox->lineages, '_hierarchical_select_dropbox_sort');
  1948. }
  1949. // Now store each lineage's selection too. This is needed on the client side
  1950. // to enable the remove button to let the server know which selected items
  1951. // should be removed.
  1952. foreach ($dropbox->lineages as $id => $lineage) {
  1953. if ($config['save_lineage']) {
  1954. // Store the entire lineage.
  1955. $dropbox->lineages_selections[$id] = array_map('_hierarchical_select_dropbox_lineage_item_get_value', $lineage);
  1956. }
  1957. else {
  1958. // Store only the last (aka the deepest) value of the lineage.
  1959. $dropbox->lineages_selections[$id][0] = $lineage[count($lineage) - 1]['value'];
  1960. }
  1961. }
  1962. }
  1963. // Calculate the time it took to build the dropbox object.
  1964. $dropbox->build_time = (microtime() - $start) * 1000;
  1965. return $dropbox;
  1966. }
  1967. /**
  1968. * Helper function to reconstruct the lineages given a set of selected items
  1969. * and the fact that the "save lineage" setting is enabled.
  1970. *
  1971. * Note that it's impossible to predict how many lineages if we know the
  1972. * number of selected items, exactly because the "save lineage" setting is
  1973. * enabled.
  1974. *
  1975. * Worst case time complexity is O(n^3), optimizations are still possible.
  1976. *
  1977. * @param $module
  1978. * The module that should be used for HS hooks.
  1979. * @param $selection
  1980. * The selection based on which a dropbox should be generated.
  1981. * @param $params
  1982. * Optional. An array of parameters, which may be necessary for some
  1983. * implementations.
  1984. * @return
  1985. * An array of dropbox lineages.
  1986. */
  1987. function _hierarchical_select_dropbox_reconstruct_lineages_save_lineage_enabled($module, $selection, $params) {
  1988. // We have to reconstruct all lineages from the given set of selected items.
  1989. // That means: we have to reconstruct every possible combination!
  1990. $lineages = array();
  1991. $root_level = module_invoke($module, 'hierarchical_select_root_level', $params);
  1992. foreach ($selection as $key => $item) {
  1993. // Create new lineage if the item can be found in the root level.
  1994. if (array_key_exists($item, $root_level)) {
  1995. $lineages[][0] = array('value' => $item, 'label' => $root_level[$item]);
  1996. unset($selection[$key]);
  1997. }
  1998. }
  1999. // Keep on trying as long as at least one lineage has been extended.
  2000. $at_least_one = TRUE;
  2001. for ($level = 0; $at_least_one; $level++) {
  2002. $at_least_one = FALSE;
  2003. $num = count($lineages);
  2004. // Try to extend every lineage. Make sure we don't iterate over
  2005. // possibly new lineages.
  2006. for ($id = 0; $id < $num; $id++) {
  2007. // Only try to extend a lineage if it has an item at the current level.
  2008. if (!isset($lineages[$id][$level])) {
  2009. continue;
  2010. }
  2011. $children = module_invoke($module, 'hierarchical_select_children', $lineages[$id][$level]['value'], $params);
  2012. $child_added_to_lineage = FALSE;
  2013. foreach (array_keys($children) as $child) {
  2014. if (in_array($child, $selection)) {
  2015. if (!$child_added_to_lineage) {
  2016. // Add the child to the lineage.
  2017. $lineages[$id][$level + 1] = array('value' => $child, 'label' => $children[$child]);
  2018. $child_added_to_lineage = TRUE;
  2019. $at_least_one = TRUE;
  2020. }
  2021. else {
  2022. // Create new lineage based on current one and add the child.
  2023. $lineage = $lineages[$id];
  2024. $lineage[$level + 1] = array('value' => $child, 'label' => $children[$child]);
  2025. // Add the new lineage to the set of lineages
  2026. $lineages[] = $lineage;
  2027. }
  2028. }
  2029. }
  2030. }
  2031. }
  2032. return $lineages;
  2033. }
  2034. /**
  2035. * Dropbox lineages sorting callback.
  2036. *
  2037. * @param $lineage_a
  2038. * The first lineage.
  2039. * @param $lineage_b
  2040. * The second lineage.
  2041. * @return
  2042. * An integer that determines which of the two lineages comes first.
  2043. */
  2044. function _hierarchical_select_dropbox_sort($lineage_a, $lineage_b) {
  2045. $string_a = implode('', array_map('_hierarchical_select_dropbox_lineage_item_get_label', $lineage_a));
  2046. $string_b = implode('', array_map('_hierarchical_select_dropbox_lineage_item_get_label', $lineage_b));
  2047. return strcmp($string_a, $string_b);
  2048. }
  2049. /**
  2050. * Helper function needed for the array_map() call in the dropbox sorting
  2051. * callback.
  2052. *
  2053. * @param $item
  2054. * An item in a dropbox lineage.
  2055. * @return
  2056. * The value associated with the "label" key of the item.
  2057. */
  2058. function _hierarchical_select_dropbox_lineage_item_get_label($item) {
  2059. return t($item['label']);
  2060. }
  2061. /**
  2062. * Helper function needed for the array_map() call in the dropbox lineages
  2063. * selections creation.
  2064. *
  2065. * @param $item
  2066. * An item in a dropbox lineage.
  2067. * @return
  2068. * The value associated with the "value" key of the item.
  2069. */
  2070. function _hierarchical_select_dropbox_lineage_item_get_value($item) {
  2071. return $item['value'];
  2072. }
  2073. /**
  2074. * Smarter version of array_merge_recursive: overwrites scalar values.
  2075. *
  2076. * From: http://www.php.net/manual/en/function.array-merge-recursive.php#82976.
  2077. */
  2078. if (!function_exists('array_smart_merge')) {
  2079. function array_smart_merge($array, $override) {
  2080. if (is_array($array) && is_array($override)) {
  2081. foreach ($override as $k => $v) {
  2082. if (isset($array[$k]) && is_array($v) && is_array($array[$k])) {
  2083. $array[$k] = array_smart_merge($array[$k], $v);
  2084. }
  2085. else {
  2086. $array[$k] = $v;
  2087. }
  2088. }
  2089. }
  2090. return $array;
  2091. }
  2092. }
  2093. /**
  2094. * Helper function needed for the array_filter() call to filter the items
  2095. * marked with the 'exclusive' property
  2096. *
  2097. * @param $item
  2098. * An item in the 'special_items' setting.
  2099. * @return
  2100. * TRUE if it's marked with the 'exclusive' property, FALSE otherwise.
  2101. */
  2102. function _hierarchical_select_special_item_exclusive($item) {
  2103. return in_array('exclusive', $item);
  2104. }
  2105. /**
  2106. * Helper function needed for the array_filter() call to filter the items
  2107. * marked with the 'none' property
  2108. *
  2109. * @param $item
  2110. * An item in the 'special_items' setting.
  2111. * @return
  2112. * TRUE if it's marked with the 'none' property, FALSE otherwise.
  2113. */
  2114. function _hierarchical_select_special_item_none($item) {
  2115. return in_array('none', $item);
  2116. }