alter.inc 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. <?php
  2. /**
  3. * @file
  4. * alter.inc
  5. *
  6. * Contains various implementations of hook_*_alter().
  7. */
  8. /**
  9. * Include #pre_render callbacks for elements.
  10. */
  11. bootstrap_include('bootstrap', 'includes/pre-render.inc');
  12. /**
  13. * Include #process callbacks for elements.
  14. */
  15. bootstrap_include('bootstrap', 'includes/process.inc');
  16. /**
  17. * Implements hook_css_alter().
  18. */
  19. function bootstrap_css_alter(&$css) {
  20. // Add CDN assets, if any.
  21. if (($provider = bootstrap_setting('cdn_provider')) && ($cdn_assets = bootstrap_get_cdn_assets('css', $provider))) {
  22. $cdn_weight = -2.99;
  23. foreach ($cdn_assets as $cdn_asset) {
  24. $cdn_weight += .01;
  25. $css[$cdn_asset] = array(
  26. 'data' => $cdn_asset,
  27. 'type' => 'external',
  28. 'every_page' => TRUE,
  29. 'media' => 'all',
  30. 'preprocess' => FALSE,
  31. 'group' => CSS_THEME,
  32. 'browsers' => array('IE' => TRUE, '!IE' => TRUE),
  33. 'weight' => $cdn_weight,
  34. );
  35. }
  36. // Add Drupal Bootstrap Styles.
  37. // @see https://github.com/unicorn-fail/drupal-bootstrap-styles
  38. $version = bootstrap_setting('cdn_' . $provider . '_version') ?: BOOTSTRAP_VERSION;
  39. $theme = bootstrap_setting('cdn_' . $provider . '_theme') ?: 'bootstrap';
  40. if ($url = _bootstrap_cdn_get_drupal_bootstrap_styles_url($version, $theme)) {
  41. $external = url_is_external($url);
  42. $css[$url] = array(
  43. 'data' => $url,
  44. 'type' => $external ? 'external' : 'file',
  45. 'every_page' => TRUE,
  46. 'media' => 'all',
  47. 'preprocess' => $external ? FALSE : TRUE,
  48. 'group' => CSS_THEME,
  49. 'browsers' => array('IE' => TRUE, '!IE' => TRUE),
  50. 'weight' => -1,
  51. );
  52. }
  53. }
  54. // Exclude specified CSS files from theme.
  55. if ($excludes = bootstrap_get_theme_info(NULL, 'exclude][css')) {
  56. $excludes = array_merge($excludes, str_replace('.css', '-rtl.css', $excludes));
  57. $css = array_diff_key($css, drupal_map_assoc($excludes));
  58. }
  59. }
  60. /**
  61. * Implements hook_element_info_alter().
  62. */
  63. function bootstrap_element_info_alter(&$info) {
  64. global $theme_key;
  65. $cid = "theme_registry:bootstrap:element_info";
  66. $cached = array();
  67. if (($cache = cache_get($cid)) && !empty($cache->data)) {
  68. $cached = $cache->data;
  69. }
  70. $themes = _bootstrap_get_base_themes($theme_key, TRUE);
  71. foreach ($themes as $theme) {
  72. if (!isset($cached[$theme])) {
  73. $cached[$theme] = array();
  74. foreach (array_keys($info) as $type) {
  75. $element = array();
  76. // Replace fieldset theme implementations with bootstrap_panel.
  77. if (!empty($info[$type]['#theme']) && $info[$type]['#theme'] === 'fieldset') {
  78. $element['#bootstrap_replace']['#theme'] = 'bootstrap_panel';
  79. }
  80. if (!empty($info[$type]['#theme_wrappers']) && array_search('fieldset', $info[$type]['#theme_wrappers']) !== FALSE) {
  81. $element['#bootstrap_replace']['#theme_wrappers']['fieldset'] = 'bootstrap_panel';
  82. }
  83. // Setup a default "icon" variable. This allows #icon to be passed
  84. // to every template and theme function.
  85. // @see https://www.drupal.org/node/2219965
  86. $element['#icon'] = NULL;
  87. $element['#icon_position'] = 'before';
  88. $properties = array(
  89. '#process' => array(
  90. 'form_process',
  91. 'form_process_' . $type,
  92. ),
  93. '#pre_render' => array(
  94. 'pre_render',
  95. 'pre_render_' . $type,
  96. ),
  97. );
  98. foreach ($properties as $property => $callbacks) {
  99. foreach ($callbacks as $callback) {
  100. $function = $theme . '_' . $callback;
  101. if (function_exists($function)) {
  102. // Replace direct core function correlation.
  103. if (!empty($info[$type][$property]) && array_search($callback, $info[$type][$property]) !== FALSE) {
  104. $element['#bootstrap_replace'][$property][$callback] = $function;
  105. }
  106. // Check for a "form_" prefix instead (for #pre_render).
  107. elseif (!empty($info[$type][$property]) && array_search('form_' . $callback, $info[$type][$property]) !== FALSE) {
  108. $element['#bootstrap_replace'][$property]['form_' . $callback] = $function;
  109. }
  110. // Otherwise, append the function.
  111. else {
  112. $element[$property][] = $function;
  113. }
  114. }
  115. }
  116. }
  117. $cached[$theme][$type] = $element;
  118. }
  119. // Cache the element information.
  120. cache_set($cid, $cached);
  121. }
  122. // Merge in each theme's cached element info.
  123. $info = _bootstrap_element_info_array_merge($info, $cached[$theme]);
  124. }
  125. }
  126. /**
  127. * Merges the cached element information into the runtime array.
  128. *
  129. * @param array $info
  130. * The element info array to merge data into.
  131. * @param array $cached
  132. * The cached element info data array to merge from.
  133. *
  134. * @return array
  135. * The altered element info array.
  136. */
  137. function _bootstrap_element_info_array_merge(array $info, array $cached) {
  138. foreach ($cached as $type => $element) {
  139. $replacement_data = isset($element['#bootstrap_replace']) ? $element['#bootstrap_replace'] : array();
  140. unset($element['#bootstrap_replace']);
  141. foreach ($element as $property => $data) {
  142. if (is_array($data)) {
  143. if (!isset($info[$type][$property])) {
  144. $info[$type][$property] = array();
  145. }
  146. // Append the values if not already in the array.
  147. foreach ($data as $key => $value) {
  148. if (!in_array($value, $info[$type][$property])) {
  149. $info[$type][$property][] = $value;
  150. }
  151. }
  152. }
  153. // Create the property, if not already set.
  154. elseif (!isset($info[$type][$property])) {
  155. $info[$type][$property] = $data;
  156. }
  157. }
  158. // Replace data, if necessary.
  159. foreach ($replacement_data as $property => $data) {
  160. if (is_array($data)) {
  161. foreach ($data as $needle => $replacement) {
  162. if (!empty($info[$type][$property]) && ($key = array_search($needle, $info[$type][$property])) !== FALSE) {
  163. $info[$type][$property][$key] = $replacement;
  164. }
  165. }
  166. }
  167. // Replace the property with the new data.
  168. else {
  169. $info[$type][$property] = $data;
  170. }
  171. }
  172. }
  173. // Return the altered element info array.
  174. return $info;
  175. }
  176. /**
  177. * Implements hook_field_widget_form_alter().
  178. */
  179. function bootstrap_field_widget_form_alter(&$element, &$form_state, $context) {
  180. $widget_type = isset($context['instance']['widget']['type']) ? $context['instance']['widget']['type'] : NULL;
  181. if ($widget_type === 'image_image') {
  182. foreach (element_children($element) as $child) {
  183. $element[$child]['#process'][] = '_bootstrap_image_field_widget_process';
  184. }
  185. }
  186. }
  187. /**
  188. * Implements above #process callback.
  189. */
  190. function _bootstrap_image_field_widget_process($element, &$form_state, $form) {
  191. // Core explicitly sets #theme_wrappers to an empty array for the upload
  192. // element (perhaps for styling reasons?). Thus, bootstrap_form_element() is
  193. // invoked, preventing any necessary logic from executing. To achieve the
  194. // same goal and keep backwards compatibility, reset the theme wrapper back
  195. // and indicating that the wrapper shouldn't be printed.
  196. $element['upload']['#theme_wrappers'][] = 'form_element__image_widget';
  197. $element['upload']['#form_element_wrapper'] = FALSE;
  198. // Unfortunately, core also doesn't set #access on the appropriate elements
  199. // until way too late (ironically, because of #ajax). Instead of calling
  200. // file_managed_file_pre_render(), just mimic the same #access logic, but
  201. // using #default_value instead of #value since the ajax request populates
  202. // #value.
  203. $value = empty($element['#default_value']['fid']);
  204. if (!$value) {
  205. $element['upload']['#access'] = FALSE;
  206. $element['upload_button']['#access'] = FALSE;
  207. }
  208. // If we don't already have a file, there is nothing to remove.
  209. else {
  210. $element['remove_button']['#access'] = FALSE;
  211. }
  212. // Make the upload file element an input group with a button.
  213. $element['upload']['#input_group_button'] = $value;
  214. return $element;
  215. }
  216. /**
  217. * Implements hook_form_alter().
  218. */
  219. function bootstrap_form_alter(array &$form, array &$form_state = array(), $form_id = NULL) {
  220. if ($form_id) {
  221. switch ($form_id) {
  222. case 'system_theme_settings':
  223. // Create vertical tabs for global settings (provided by core or other
  224. // contrib modules).
  225. if (!isset($form['global'])) {
  226. $form['global'] = array(
  227. '#type' => 'vertical_tabs',
  228. '#weight' => -9,
  229. );
  230. if (!empty($form_state['build_info']['args'][0])) {
  231. $form['global']['#prefix'] = '<h2><small>' . t('Override Global Settings') . '</small></h2>';
  232. }
  233. }
  234. // Iterate over all child elements and check to see if they should be
  235. // moved in the global vertical tabs.
  236. $global_children = element_children($form);
  237. foreach ($global_children as $child) {
  238. if (isset($form[$child]['#type']) && $form[$child]['#type'] === 'fieldset' && !isset($form[$child]['#group'])) {
  239. $form[$child]['#group'] = 'global';
  240. }
  241. }
  242. break;
  243. case 'search_form':
  244. // Add a clearfix class so the results don't overflow onto the form.
  245. $form['#attributes']['class'][] = 'clearfix';
  246. // Remove container-inline from the container classes.
  247. $form['basic']['#attributes']['class'] = array();
  248. // Hide the default button from display.
  249. $form['basic']['submit']['#attributes']['class'][] = 'element-invisible';
  250. // Implement a theme wrapper to add a submit button containing a search
  251. // icon directly after the input element.
  252. $form['basic']['keys']['#theme_wrappers'] = array('bootstrap_search_form_wrapper');
  253. $form['basic']['keys']['#title'] = '';
  254. $form['basic']['keys']['#attributes']['placeholder'] = t('Search');
  255. break;
  256. case 'search_block_form':
  257. $form['#attributes']['class'][] = 'form-search';
  258. $form['search_block_form']['#title'] = '';
  259. $form['search_block_form']['#attributes']['placeholder'] = t('Search');
  260. // Hide the default button from display and implement a theme wrapper
  261. // to add a submit button containing a search icon directly after the
  262. // input element.
  263. $form['actions']['submit']['#attributes']['class'][] = 'element-invisible';
  264. $form['search_block_form']['#theme_wrappers'] = array('bootstrap_search_form_wrapper');
  265. // Apply a clearfix so the results don't overflow onto the form.
  266. $form['#attributes']['class'][] = 'content-search';
  267. break;
  268. case 'image_style_form':
  269. $form['effects']['new']['new']['#input_group_button'] = TRUE;
  270. break;
  271. case 'path_admin_filter_form':
  272. $form['basic']['filter']['#input_group_button'] = TRUE;
  273. break;
  274. }
  275. }
  276. }
  277. /**
  278. * Implements hook_js_alter().
  279. */
  280. function bootstrap_js_alter(&$js) {
  281. // Exclude specified JavaScript files from theme.
  282. $excludes = bootstrap_get_theme_info(NULL, 'exclude][js');
  283. $theme_path = drupal_get_path('theme', 'bootstrap');
  284. // Add or replace JavaScript files when matching paths are detected.
  285. // Replacement files must begin with '_', like '_node.js'.
  286. $files = _bootstrap_file_scan_directory($theme_path . '/js', '/\.js$/');
  287. foreach ($files as $file) {
  288. $path = str_replace($theme_path . '/js/', '', $file->uri);
  289. // Detect if this is a replacement file.
  290. $replace = FALSE;
  291. if (preg_match('/^[_]/', $file->filename)) {
  292. $replace = TRUE;
  293. $path = dirname($path) . '/' . preg_replace('/^[_]/', '', $file->filename);
  294. }
  295. $matches = array();
  296. if (preg_match('/^modules\/([^\/]*)/', $path, $matches)) {
  297. if (!module_exists($matches[1])) {
  298. continue;
  299. }
  300. else {
  301. $path = str_replace('modules/' . $matches[1], drupal_get_path('module', $matches[1]), $path);
  302. }
  303. }
  304. // Path should always exist to either add or replace JavaScript file.
  305. if (!empty($js[$path])) {
  306. // Replace file.
  307. if ($replace) {
  308. $js[$file->uri] = $js[$path];
  309. $js[$file->uri]['data'] = $file->uri;
  310. unset($js[$path]);
  311. }
  312. // Add file.
  313. else {
  314. $js[$file->uri] = drupal_js_defaults($file->uri);
  315. $js[$file->uri]['group'] = JS_THEME;
  316. }
  317. }
  318. }
  319. // Ensure jQuery Once is always loaded.
  320. // @see https://www.drupal.org/node/2149561
  321. if (empty($js['misc/jquery.once.js'])) {
  322. $jquery_once = drupal_get_library('system', 'jquery.once');
  323. $js['misc/jquery.once.js'] = $jquery_once['js']['misc/jquery.once.js'];
  324. $js['misc/jquery.once.js'] += drupal_js_defaults('misc/jquery.once.js');
  325. }
  326. // Always add bootstrap.js last.
  327. $bootstrap = $theme_path . '/js/bootstrap.js';
  328. $js[$bootstrap] = drupal_js_defaults($bootstrap);
  329. $js[$bootstrap]['group'] = JS_THEME;
  330. $js[$bootstrap]['scope'] = 'footer';
  331. if (!empty($excludes)) {
  332. $js = array_diff_key($js, drupal_map_assoc($excludes));
  333. }
  334. // Add Bootstrap settings.
  335. $js['settings']['data'][]['bootstrap'] = array(
  336. 'anchorsFix' => bootstrap_setting('anchors_fix'),
  337. 'anchorsSmoothScrolling' => bootstrap_setting('anchors_smooth_scrolling'),
  338. 'formHasError' => (int) bootstrap_setting('forms_has_error_value_toggle'),
  339. 'popoverEnabled' => bootstrap_setting('popover_enabled'),
  340. 'popoverOptions' => array(
  341. 'animation' => (int) bootstrap_setting('popover_animation'),
  342. 'html' => (int) bootstrap_setting('popover_html'),
  343. 'placement' => bootstrap_setting('popover_placement'),
  344. 'selector' => bootstrap_setting('popover_selector'),
  345. 'trigger' => implode(' ', array_filter(array_values((array) bootstrap_setting('popover_trigger')))),
  346. 'triggerAutoclose' => (int) bootstrap_setting('popover_trigger_autoclose'),
  347. 'title' => bootstrap_setting('popover_title'),
  348. 'content' => bootstrap_setting('popover_content'),
  349. 'delay' => (int) bootstrap_setting('popover_delay'),
  350. 'container' => bootstrap_setting('popover_container'),
  351. ),
  352. 'tooltipEnabled' => bootstrap_setting('tooltip_enabled'),
  353. 'tooltipOptions' => array(
  354. 'animation' => (int) bootstrap_setting('tooltip_animation'),
  355. 'html' => (int) bootstrap_setting('tooltip_html'),
  356. 'placement' => bootstrap_setting('tooltip_placement'),
  357. 'selector' => bootstrap_setting('tooltip_selector'),
  358. 'trigger' => implode(' ', array_filter(array_values((array) bootstrap_setting('tooltip_trigger')))),
  359. 'delay' => (int) bootstrap_setting('tooltip_delay'),
  360. 'container' => bootstrap_setting('tooltip_container'),
  361. ),
  362. );
  363. // Add CDN assets, if any.
  364. if ($cdn_assets = bootstrap_get_cdn_assets('js')) {
  365. $cdn_weight = -99.99;
  366. foreach ($cdn_assets as $cdn_asset) {
  367. $cdn_weight += .01;
  368. $js[$cdn_asset] = drupal_js_defaults($cdn_asset);
  369. $js[$cdn_asset]['type'] = 'external';
  370. $js[$cdn_asset]['every_page'] = TRUE;
  371. $js[$cdn_asset]['weight'] = $cdn_weight;
  372. }
  373. }
  374. }
  375. /**
  376. * Implements hook_icon_bundle_list_alter().
  377. */
  378. function bootstrap_icon_bundle_list_alter(&$build, $bundle) {
  379. if (bootstrap_setting('tooltip_enabled')) {
  380. foreach ($build as &$icon) {
  381. $icon['#attributes']['data-toggle'] = 'tooltip';
  382. $icon['#attributes']['data-placement'] = 'bottom';
  383. }
  384. }
  385. }
  386. /**
  387. * Implements hook_menu_local_tasks_alter().
  388. */
  389. function bootstrap_menu_local_tasks_alter(&$data, &$router_item, $root_path) {
  390. if (!empty($data['actions']['output'])) {
  391. $items = array();
  392. foreach ($data['actions']['output'] as $item) {
  393. $items[] = array(
  394. 'data' => $item,
  395. );
  396. }
  397. $data['actions']['output'] = array(
  398. '#theme' => 'item_list__action_links',
  399. '#items' => $items,
  400. '#attributes' => array(
  401. 'class' => array('action-links'),
  402. ),
  403. );
  404. }
  405. }
  406. /**
  407. * Implements hook_js_callback_filter_xss_alter().
  408. */
  409. function bootstrap_js_callback_filter_xss_alter(array &$allowed_tags = array()) {
  410. $allowed_tags[] = 'button';
  411. }