[ 'claro/drupal.nav-tabs', ], ]; } elseif (!empty($variables['secondary'])) { $variables['secondary']['#attached'] = [ 'library' => [ 'claro/drupal.nav-tabs', ], ]; } foreach (Element::children($variables['primary']) as $key) { $variables['primary'][$key]['#level'] = 'primary'; } foreach (Element::children($variables['secondary']) as $key) { $variables['secondary'][$key]['#level'] = 'secondary'; } } /** * Implements hook_preprocess_HOOK() for menu-local-task templates. */ function claro_preprocess_menu_local_task(&$variables) { $variables['link']['#options']['attributes']['class'][] = 'tabs__link'; $variables['link']['#options']['attributes']['class'][] = 'js-tabs-link'; // Ensure is-active class is set when the tab is active. The generic active // link handler applies stricter comparison rules than what is necessary for // tabs. if (isset($variables['is_active']) && $variables['is_active'] === TRUE) { $variables['link']['#options']['attributes']['class'][] = 'is-active'; } if (isset($variables['element']['#level'])) { $variables['level'] = $variables['element']['#level']; } } /** * Implements hook_preprocess_HOOK() for menu-local-task Views UI templates. */ function claro_preprocess_menu_local_task__views_ui(&$variables) { // Remove 'tabs__link' without adding a new class because it couldn't be used // reliably. // @see https://www.drupal.org/node/3051605 $link_class_index = array_search('tabs__link', $variables['link']['#options']['attributes']['class']); if ($link_class_index !== FALSE) { unset($variables['link']['#options']['attributes']['class'][$link_class_index]); } } /** * Implements template_preprocess_HOOK() for node_add_list. * * Makes node_add_list variables compatible with entity_add_list. */ function claro_preprocess_node_add_list(&$variables) { if (!empty($variables['content'])) { /** @var \Drupal\node\NodeTypeInterface $type */ foreach ($variables['content'] as $type) { $label = $type->label(); $description = $type->getDescription(); $type_id = $type->id(); $add_url = Url::fromRoute('node.add', ['node_type' => $type_id]); $variables['bundles'][$type_id] = [ 'label' => $label, 'add_link' => Link::fromTextAndUrl($label, $add_url), 'description' => [], ]; if (!empty($description)) { $variables['bundles'][$type_id]['description'] = [ '#markup' => $description, ]; } } $variables['attributes']['class'][] = 'node-type-list'; } } /** * Implements template_preprocess_HOOK() for block_content_add_list. * * Makes block_content_add_list variables compatible with entity_add_list. */ function claro_preprocess_block_content_add_list(&$variables) { if (!empty($variables['content'])) { $query = \Drupal::request()->query->all(); /** @var \Drupal\block_content\BlockContentTypeInterface $type */ foreach ($variables['content'] as $type) { $label = $type->label(); $description = $type->getDescription(); $type_id = $type->id(); $add_url = Url::fromRoute('block_content.add_form', [ 'block_content_type' => $type_id, ], [ 'query' => $query, ]); $variables['bundles'][$type_id] = [ 'label' => $label, 'add_link' => Link::fromTextAndUrl($label, $add_url), 'description' => [], ]; if (!empty($description)) { $variables['bundles'][$type_id]['description'] = [ '#markup' => $description, ]; } } } } /** * Implements template_preprocess_HOOK() for entity_add_list. */ function claro_preprocess_entity_add_list(&$variables) { // Remove description if empty. foreach ($variables['bundles'] as $type_id => $values) { if (isset($values['description']['#markup']) && empty($values['description']['#markup'])) { $variables['bundles'][$type_id]['description'] = []; } } } /** * Implements hook_preprocess_block() for block content. * * Disables contextual links for all blocks except for layout builder blocks. */ function claro_preprocess_block(&$variables) { if (isset($variables['title_suffix']['contextual_links']) && !isset($variables['elements']['#contextual_links']['layout_builder_block'])) { unset($variables['title_suffix']['contextual_links']); unset($variables['elements']['#contextual_links']); $variables['attributes']['class'] = array_diff($variables['attributes']['class'], ['contextual-region']); } } /** * Implements template_preprocess_HOOK() for admin_block. */ function claro_preprocess_admin_block(&$variables) { if (!empty($variables['block']['content'])) { $variables['block']['content']['#attributes']['class'][] = 'admin-list--panel'; } } /** * Implements template_preprocess_HOOK() for admin_block. */ function claro_preprocess_admin_block_content(&$variables) { foreach ($variables['content'] as &$item) { $link_attributes = $item['url']->getOption('attributes') ?: []; $link_attributes['class'][] = 'admin-item__link'; $item['url']->setOption('attributes', $link_attributes); $item['link'] = Link::fromTextAndUrl($item['title'], $item['url']); if (empty($item['description']) || empty($item['description']['#markup'])) { unset($item['description']); } } } /** * Implements hook_preprocess_HOOK() for menu-local-action templates. */ function claro_preprocess_menu_local_action(array &$variables) { $variables['link']['#options']['attributes']['class'][] = 'button--primary'; $variables['attributes']['class'][] = 'local-actions__item'; $legacy_class_key = array_search('button-action', $variables['link']['#options']['attributes']['class']); if ($legacy_class_key !== FALSE) { $variables['link']['#options']['attributes']['class'][$legacy_class_key] = 'button--action'; } } /** * Implements hook_element_info_alter(). */ function claro_element_info_alter(&$type) { // Add a pre-render function that handles the sidebar of the node form. // @todo Refactor when https://www.drupal.org/node/3056089 is in. if (isset($type['container'])) { $container_pre_renders = !empty($type['container']['#pre_render']) ? $type['container']['#pre_render'] : []; array_unshift($container_pre_renders, [ClaroPreRender::class, 'container']); $type['container']['#pre_render'] = $container_pre_renders; } // @todo Refactor when https://www.drupal.org/node/3016343 is fixed. if (isset($type['text_format'])) { $type['text_format']['#pre_render'][] = [ClaroPreRender::class, 'textFormat']; } // Add a pre-render function that handles dropbutton variants. if (isset($type['dropbutton'])) { $type['dropbutton']['#pre_render'][] = [ClaroPreRender::class, 'dropButton']; } if (isset($type['vertical_tabs'])) { $type['vertical_tabs']['#pre_render'][] = [ClaroPreRender::class, 'verticalTabs']; } // Add a pre-render to managed_file. if (isset($type['managed_file'])) { $type['managed_file']['#pre_render'][] = [ClaroPreRender::class, 'managedFile']; } // Add a pre-render to status_messages to alter the placeholder markup. if (isset($type['status_messages'])) { $type['status_messages']['#pre_render'][] = [ClaroPreRender::class, 'messagePlaceholder']; } } /** * Implements template_preprocess_filter_guidelines(). */ function claro_preprocess_filter_guidelines(&$variables) { // Fix filter guidelines selector issue of 'filter/drupal.filter'. // @todo Remove when https://www.drupal.org/node/2881212 is fixed. $variables['attributes']['class'][] = 'filter-guidelines-item'; $variables['attributes']['class'][] = 'filter-guidelines-' . $variables['format']->id(); } /** * Implements template_preprocess_text_format_wrapper(). * * @todo Remove when https://www.drupal.org/node/3016343 is fixed. */ function claro_preprocess_text_format_wrapper(&$variables) { $description_attributes = []; if (!empty($variables['attributes']['id'])) { $description_attributes['id'] = $variables['attributes']['aria-describedby'] = $variables['attributes']['id']; unset($variables['attributes']['id']); } $variables['description_attributes'] = new Attribute($description_attributes); } /** * Implements hook_theme_registry_alter(). */ function claro_theme_registry_alter(&$theme_registry) { if (!empty($theme_registry['admin_block_content'])) { $theme_registry['admin_block_content']['variables']['attributes'] = []; } // @todo Remove when https://www.drupal.org/node/3016346 is fixed. if (!empty($theme_registry['text_format_wrapper'])) { $theme_registry['text_format_wrapper']['variables']['disabled'] = FALSE; } } /** * Implements hook_preprocess_install_page(). */ function claro_preprocess_install_page(&$variables) { // Claro has custom styling for the install page. $variables['#attached']['library'][] = 'claro/install-page'; } /** * Implements hook_preprocess_maintenance_page(). */ function claro_preprocess_maintenance_page(&$variables) { // Claro has custom styling for the maintenance page. $variables['#attached']['library'][] = 'claro/maintenance-page'; } /** * Implements hook_preprocess_HOOK() for details. * * @todo Revisit when https://www.drupal.org/node/3056089 is in. */ function claro_preprocess_details(&$variables) { $element = $variables['element']; if (!empty($element['#accordion_item'])) { // Details should appear as an accordion item. $variables['accordion_item'] = TRUE; } if (!empty($element['#accordion'])) { // Details should appear as a standalone accordion. $variables['accordion'] = TRUE; } if (!empty($element['#theme']) && $element['#theme'] === 'file_widget_multiple') { // Mark the details required if needed. If the file widget allows uploading // multiple files, the required state is checked by checking the state of // the first child. $variables['required'] = isset($element[0]['#required']) ? $element[0]['#required'] : !empty($element['#required']); // If the error is the same as the one in the multiple field widget element, // we have to avoid displaying it twice. Seven and Stark have this issue // as well. // @todo revisit when https://www.drupal.org/node/3084906 is fixed. if (isset($element['#errors']) && isset($variables['errors']) && $element['#errors'] === $variables['errors']) { unset($variables['errors']); } } $variables['disabled'] = !empty($element['#disabled']); } /** * Implements hook_form_alter(). */ function claro_form_alter(array &$form, FormStateInterface $form_state, $form_id) { $build_info = $form_state->getBuildInfo(); $form_object = $form_state->getFormObject(); // Make entity forms delete link use the action-link component. if (isset($form['actions']['delete']['#type']) && $form['actions']['delete']['#type'] === 'link' && !empty($build_info['callback_object']) && $build_info['callback_object'] instanceof EntityForm) { $form['actions']['delete'] = _claro_convert_link_to_action_link($form['actions']['delete'], 'trash', 'default', 'danger'); } if ($form_object instanceof ViewsForm && strpos($form_object->getBaseFormId(), 'views_form_media_library') === 0) { if (isset($form['header'])) { $form['header']['#attributes']['class'][] = 'media-library-views-form__header'; $form['header']['media_bulk_form']['#attributes']['class'][] = 'media-library-views-form__bulk_form'; } $form['actions']['submit']['#attributes']['class'] = ['media-library-select']; } } /** * Implements hook_preprocess_HOOK() for links. */ function claro_preprocess_links(&$variables) { foreach ($variables['links'] as $links_item) { if (!empty($links_item['link']) && !empty($links_item['link']['#url']) && $links_item['link']['#url'] instanceof Url) { if ($links_item['link']['#url']->isRouted()) { switch ($links_item['link']['#url']->getRouteName()) { case 'system.theme_settings_theme': $links_item['link'] = _claro_convert_link_to_action_link($links_item['link'], 'cog', 'small'); break; case 'system.theme_uninstall': $links_item['link'] = _claro_convert_link_to_action_link($links_item['link'], 'ex', 'small'); break; case 'system.theme_set_default': case 'system.theme_install': $links_item['link'] = _claro_convert_link_to_action_link($links_item['link'], 'checkmark', 'small'); break; } } } } } /** * Converts a link render element to an action link. * * This helper merges every attributes from $link['#attributes'], from * $link['#options']['attributes'] and from the Url object's. * * @param array $link * Link renderable array. * @param string|null $icon_name * The name of the needed icon. When specified, a CSS class will be added with * the following pattern: 'action-link--icon-[icon_name]'. If the needed icon * is not implemented in CSS, no icon will be added. * Currently available icons are: * - checkmark, * - cog, * - ex, * - plus, * - trash. * @param string $size * Name of the small action link variant. Defaults to 'default'. * Supported sizes are: * - default, * - small, * - extrasmall. * @param string $variant * Variant of the action link. Supported variants are 'default' and 'danger'. * Defaults to 'default'. * * @return array * The link renderable converted to action link. */ function _claro_convert_link_to_action_link(array $link, $icon_name = NULL, $size = 'default', $variant = 'default') { // Early opt-out if we cannot do anything. if (empty($link['#type']) || $link['#type'] !== 'link' || empty($link['#url'])) { return $link; } // \Drupal\Core\Render\Element\Link::preRenderLink adds $link['#attributes'] // to $link[#options]['attributes'] if it is not empty, but it does not merges // the 'class' subkey deeply. // Because of this, when $link[#options]['attributes']['class'] is set, the // classes defined in $link['#attributes']['class'] are ignored. // // To keep this behavior we repeat this for action-link, which means that this // conversion happens a bit earlier. We unset $link['#attributes'] to prevent // Link::preRenderLink() doing the same, because for action-links, that would // be needless. $link += ['#options' => []]; if (isset($link['#attributes'])) { $link['#options'] += [ 'attributes' => [], ]; $link['#options']['attributes'] += $link['#attributes']; unset($link['#attributes']); } $link['#options'] += ['attributes' => []]; $link['#options']['attributes'] += ['class' => []]; // Determine the needed (type) variant. $variants_supported = ['default', 'danger']; $variant = is_string($variant) && in_array($variant, $variants_supported) ? $variant : reset($variants_supported); // Remove button, button modifier CSS classes and other unwanted ones. $link['#options']['attributes']['class'] = array_diff($link['#options']['attributes']['class'], [ 'button', 'button--action', 'button--primary', 'button--danger', 'button--small', 'button--extrasmall', 'link', ]); // Adding the needed CSS classes. $link['#options']['attributes']['class'][] = 'action-link'; // Add the variant-modifier CSS class only if the variant is not the default. if ($variant !== reset($variants_supported)) { $link['#options']['attributes']['class'][] = Html::getClass("action-link--$variant"); } // Add the icon modifier CSS class. if (!empty($icon_name)) { $link['#options']['attributes']['class'][] = Html::getClass("action-link--icon-$icon_name"); } if ($size && in_array($size, ['small', 'extrasmall'])) { $link['#options']['attributes']['class'][] = Html::getClass("action-link--$size"); } // If the provided $link is an item of the 'links' theme function, then only // the attributes of the Url object are processed during rendering. $url_attributes = $link['#url']->getOption('attributes') ?: []; $url_attributes = NestedArray::mergeDeep($url_attributes, $link['#options']['attributes']); $link['#url']->setOption('attributes', $url_attributes); return $link; } /** * Implements hook_form_BASE_FORM_ID_alter() for \Drupal\node\NodeForm. * * Changes vertical tabs to container. */ function claro_form_node_form_alter(&$form, FormStateInterface $form_state) { $form['#theme'] = ['node_edit_form']; $form['#attached']['library'][] = 'claro/node-form'; $form['advanced']['#type'] = 'container'; $form['advanced']['#accordion'] = TRUE; $form['meta']['#type'] = 'container'; $form['meta']['#access'] = TRUE; $form['revision_information']['#type'] = 'container'; $form['revision_information']['#group'] = 'meta'; $form['revision_information']['#attributes']['class'][] = 'entity-meta__revision'; } /** * Implements hook_form_BASE_FORM_ID_alter() for \Drupal\media\MediaForm. */ function claro_form_media_form_alter(&$form, FormStateInterface $form_state) { // Only attach CSS from core if this form comes from Media core, and not from // the contrib Media Entity 1.x branch. if (\Drupal::moduleHandler()->moduleExists('media') && $form_state->getFormObject() instanceof MediaForm) { // @todo Revisit after https://www.drupal.org/node/2892304 is in. It // introduces a footer region to these forms which will allow for us to // display a top border over the published checkbox by defining a // media-edit-form.html.twig template the same way node does. $form['#attached']['library'][] = 'claro/media-form'; } } /** * Implements hook_views_ui_display_top_alter(). */ function claro_views_ui_display_top_alter(&$element) { // @todo remove this after https://www.drupal.org/node/3051605 has been // solved. $element['tabs']['#prefix'] = preg_replace('/(class="(.+\s)?)tabs(\s.+"|")/', '$1views-tabs$3', $element['tabs']['#prefix']); $element['tabs']['#prefix'] = preg_replace('/(class="(.+\s)?)secondary(\s.+"|")/', '$1views-tabs--secondary$3', $element['tabs']['#prefix']); foreach (Element::children($element['tabs']) as $tab) { $element['tabs'][$tab]['#theme'] = 'menu_local_task__views_ui'; } // Change top extra actions to use the small dropbutton variant. // @todo Revisit after https://www.drupal.org/node/3057581 is added. if (!empty($element['extra_actions'])) { $element['extra_actions']['#dropbutton_type'] = 'small'; } } /** * Implements hook_views_ui_display_tab_alter(). */ function claro_views_ui_display_tab_alter(&$element) { // We process the dropbutton-like element on views edit form's // display settings top section. // // That element should be a regular Dropbutton. // // After that the reported issue is fixed and the element is rendered with // the Dropbutton type, we just have to set it's '#dropbutton_type' to // 'extrasmall'. // // @todo: revisit after https://www.drupal.org/node/3057577 is fixed. $dummy_dropbutton = &$element['details']['top']['actions']; if ($dummy_dropbutton) { $child_keys = Element::children($dummy_dropbutton); $prefix_regex = '/(<.*class\s*= *["\']?)([^"\']*)(.*)/i'; $child_count = 0; foreach ($child_keys as $key) { if (in_array($key, ['prefix', 'suffix'])) { continue; } $nested_child_keys = Element::children($dummy_dropbutton[$key], TRUE); if (!empty($nested_child_keys)) { foreach ($nested_child_keys as $nested_key) { $child_count++; $prefix = $dummy_dropbutton[$key][$nested_key]['#prefix']; $dummy_dropbutton[$key][$nested_key]['#prefix'] = preg_replace($prefix_regex, '$1$2 dropbutton__item dropbutton__item--extrasmall$3', $prefix); } } else { $child_count++; $prefix = $dummy_dropbutton[$key]['#prefix']; $dummy_dropbutton[$key]['#prefix'] = preg_replace($prefix_regex, '$1$2 dropbutton__item dropbutton__item--extrasmall$3', $prefix); } } if (!empty($dummy_dropbutton['prefix']) && !empty($dummy_dropbutton['prefix']['#markup'])) { $classes = 'dropbutton--extrasmall '; $classes .= ($child_count > 1) ? 'dropbutton--multiple' : 'dropbutton--single'; $prefix = $dummy_dropbutton['prefix']['#markup']; $dummy_dropbutton['prefix']['#markup'] = preg_replace($prefix_regex, '$1$2 ' . $classes . '$3', $prefix); } } } /** * Implements hook_preprocess_HOOK for views_exposed_form. */ function claro_preprocess_views_exposed_form(&$variables) { $form = &$variables['form']; // Add BEM classes for items in the form. // Sorted keys. $child_keys = Element::children($form, TRUE); $last_key = NULL; $child_before_actions_key = NULL; foreach ($child_keys as $child_key) { if (!empty($form[$child_key]['#type'])) { if ($form[$child_key]['#type'] === 'actions') { // We need the key of the element that precedes the actions element. $child_before_actions_key = $last_key; $form[$child_key]['#attributes']['class'][] = 'views-exposed-form__item'; $form[$child_key]['#attributes']['class'][] = 'views-exposed-form__item--actions'; } if (!in_array($form[$child_key]['#type'], ['hidden', 'actions'])) { $form[$child_key]['#wrapper_attributes']['class'][] = 'views-exposed-form__item'; $last_key = $child_key; } } } if ($child_before_actions_key) { // Add a modifier class to the item that precedes the form actions. $form[$child_before_actions_key]['#wrapper_attributes']['class'][] = 'views-exposed-form__item--preceding-actions'; } } /** * Implements hook_form_FORM_ID_alter() for views_exposed_form. */ function claro_form_views_exposed_form_alter(&$form, FormStateInterface $form_state) { $view = $form_state->getStorage()['view']; $view_title = $view->getTitle(); // Add a label so screenreaders can identify the purpose of the exposed form // without having to scan content that appears further down the page. $form['#attributes']['aria-label'] = t('Filter the contents of the %view_title view', ['%view_title' => $view_title]); } /** * Implements hook_preprocess_form_element(). */ function claro_preprocess_form_element(&$variables) { if (!empty($variables['element']['#errors'])) { $variables['label']['#attributes']['class'][] = 'has-error'; } if ($variables['disabled']) { $variables['label']['#attributes']['class'][] = 'is-disabled'; if (!empty($variables['description']['attributes'])) { $variables['description']['attributes']->addClass('is-disabled'); } } } /** * Implements template_preprocess_HOOK() for input. */ function claro_preprocess_input(&$variables) { if ( !empty($variables['element']['#title_display']) && $variables['element']['#title_display'] === 'attribute' && !empty((string) $variables['element']['#title']) ) { $variables['attributes']['title'] = (string) $variables['element']['#title']; } $type_api = $variables['element']['#type']; $type_html = $variables['attributes']['type']; $text_types_html = [ 'text', 'email', 'tel', 'number', 'search', 'password', 'date', 'time', 'file', 'color', 'datetime-local', 'url', 'month', 'week', ]; if (in_array($type_html, $text_types_html)) { $variables['attributes']['class'][] = 'form-element'; $variables['attributes']['class'][] = Html::getClass('form-element--type-' . $type_html); $variables['attributes']['class'][] = Html::getClass('form-element--api-' . $type_api); if (!empty($variables['element']['#autocomplete_route_name'])) { $variables['autocomplete_message'] = t('Loading…'); } } if (in_array($type_html, ['checkbox', 'radio'])) { $variables['attributes']['class'][] = 'form-boolean'; $variables['attributes']['class'][] = Html::getClass('form-boolean--type-' . $type_html); } } /** * Implements template_preprocess_HOOK() for textarea. */ function claro_preprocess_textarea(&$variables) { $variables['attributes']['class'][] = 'form-element'; $variables['attributes']['class'][] = 'form-element--type-textarea'; $variables['attributes']['class'][] = 'form-element--api-textarea'; } /** * Implements template_preprocess_HOOK() for select. */ function claro_preprocess_select(&$variables) { if (!empty($variables['element']['#title_display']) && $variables['element']['#title_display'] === 'attribute' && !empty((string) $variables['element']['#title'])) { $variables['attributes']['title'] = (string) $variables['element']['#title']; } $variables['attributes']['class'][] = 'form-element'; $variables['attributes']['class'][] = $variables['element']['#multiple'] ? 'form-element--type-select-multiple' : 'form-element--type-select'; if (in_array('block-region-select', $variables['attributes']['class'])) { $variables['attributes']['class'][] = 'form-element--extrasmall'; } } /** * Implements template_preprocess_HOOK() for datetime_wrapper. */ function claro_preprocess_datetime_wrapper(&$variables) { if (!empty($variables['element']['#errors'])) { $variables['title_attributes']['class'][] = 'has-error'; } if (!empty($variables['element']['#disabled'])) { $variables['title_attributes']['class'][] = 'is-disabled'; if (!empty($variables['description_attributes'])) { $variables['description_attributes']->addClass('is-disabled'); } } } /** * Implements template_preprocess_HOOK() for fieldset. */ function claro_preprocess_fieldset(&$variables) { $element = $variables['element']; $composite_types = ['checkboxes', 'radios']; if (!empty($element['#type']) && in_array($element['#type'], $composite_types) && !empty($variables['element']['#children_errors'])) { $variables['legend_span']['attributes']->addClass('has-error'); } if (!empty($element['#disabled'])) { $variables['legend_span']['attributes']->addClass('is-disabled'); if (!empty($variables['description']) && !empty($variables['description']['attributes'])) { $variables['description']['attributes']->addClass('is-disabled'); } } // Remove 'container-inline' class from the main attributes and add a flag // instead. // @todo remove this after https://www.drupal.org/node/3059593 has been // resolved. if (!empty($variables['attributes']['class'])) { $container_inline_key = array_search('container-inline', $variables['attributes']['class']); if ($container_inline_key !== FALSE) { unset($variables['attributes']['class'][$container_inline_key]); $variables['inline_items'] = TRUE; } } } /** * Implements hook_preprocess_HOOK() for field_multiple_value_form. */ function claro_preprocess_field_multiple_value_form(&$variables) { // Make disabled available for the template. $variables['disabled'] = !empty($variables['element']['#disabled']); if ($variables['multiple']) { // Add an additional CSS class for the field label table cell. // This repeats the logic of template_preprocess_field_multiple_value_form() // without using '#prefix' and '#suffix' for the wrapper element. // // If the field is multiple, we don't have to check the existence of the // table header cell. // // @see template_preprocess_field_multiple_value_form(). $header_attributes = ['class' => ['form-item__label', 'form-item__label--multiple-value-form']]; if (!empty($variables['element']['#required'])) { $header_attributes['class'][] = 'js-form-required'; $header_attributes['class'][] = 'form-required'; } // Using array_key_first() for addressing the first header cell would be // more elegant here, but we can rely on the related theme.inc preprocess. $variables['table']['#header'][0]['data'] = [ '#type' => 'html_tag', '#tag' => 'h4', '#value' => $variables['element']['#title'], '#attributes' => $header_attributes, ]; if ($variables['disabled']) { $variables['table']['#attributes']['class'][] = 'tabledrag-disabled'; $variables['table']['#attributes']['class'][] = 'js-tabledrag-disabled'; // We will add the 'is-disabled' CSS class to the disabled table header // cells. $header_attributes['class'][] = 'is-disabled'; foreach ($variables['table']['#header'] as &$cell) { if (is_array($cell) && isset($cell['data'])) { $cell = $cell + ['class' => []]; $cell['class'][] = 'is-disabled'; } else { // We have to modify the structure of this header cell. $cell = [ 'data' => $cell, 'class' => ['is-disabled'], ]; } } } // Make add-more button smaller. if (!empty($variables['button'])) { $variables['button']['#attributes']['class'][] = 'button--small'; } } } /** * Implements hook_preprocess_HOOK() for form_element__password_confirm. */ function claro_preprocess_form_element__password_confirm(&$variables) { // Add CSS classes needed for theming the password confirm widget. $variables['attributes']['class'][] = 'password-confirm'; $variables['attributes']['class'][] = 'is-initial'; $variables['attributes']['class'][] = 'is-password-empty'; $variables['attributes']['class'][] = 'is-confirm-empty'; } /** * Implements hook_preprocess_HOOK() for form_element__password. */ function claro_preprocess_form_element__password(&$variables) { if (!empty($variables['element']['#array_parents']) && in_array('pass1', $variables['element']['#array_parents'])) { // This is the main password form element. $variables['attributes']['class'][] = 'password-confirm__password'; } if (!empty($variables['element']['#array_parents']) && in_array('pass2', $variables['element']['#array_parents'])) { // This is the password confirm form element. $variables['attributes']['class'][] = 'password-confirm__confirm'; } } /** * Implements template_preprocess_HOOK() for filter_tips. */ function claro_preprocess_filter_tips(&$variables) { $variables['#attached']['library'][] = 'filter/drupal.filter'; } /** * Implements template_preprocess_HOOK() for table. */ function claro_preprocess_table(&$variables) { // Adding table sort indicator CSS class for inactive sort link. // @todo Revisit after https://www.drupal.org/node/3025726 or // https://www.drupal.org/node/1973418 is in. if (!empty($variables['header'])) { foreach ($variables['header'] as &$header_cell) { // For 8.6.x and below. // @todo Remove this after 8.6.x is out of support. if ($header_cell['content'] instanceof GeneratedLink) { $dom_doc = Html::load($header_cell['content']->getGeneratedLink()); $anchors = $dom_doc->getElementsByTagName('a'); if (!empty($anchors)) { foreach ($anchors as $anchor) { $anchor_href = $anchor->getAttribute('href'); $parsed_url = UrlHelper::parse($anchor_href); $query = !empty($parsed_url['query']) ? $parsed_url['query'] : []; if (isset($query['order']) && isset($query['sort'])) { $header_cell['attributes']->addClass('sortable-heading'); } } } } // For 8.7.x and above. if ($header_cell['content'] instanceof Link) { $query = $header_cell['content']->getUrl()->getOption('query') ?: []; if (isset($query['order']) && isset($query['sort'])) { $header_cell['attributes']->addClass('sortable-heading'); } } } } // Mark the whole table and the first cells if rows are draggable. if (!empty($variables['rows'])) { $draggable_row_found = FALSE; foreach ($variables['rows'] as &$row) { /** @var \Drupal\Core\Template\Attribute $row['attributes'] */ if (!empty($row['attributes']) && $row['attributes']->hasClass('draggable')) { if (!$draggable_row_found) { $variables['attributes']['class'][] = 'draggable-table'; $draggable_row_found = TRUE; } reset($row['cells']); $first_cell_key = key($row['cells']); // The 'attributes' key is always here and it is an // \Drupal\Core\Template\Attribute. // @see template_preprocess_table(); $row['cells'][$first_cell_key]['attributes']->addClass('tabledrag-cell'); // Check that the first cell is empty or not. if (empty($row['cells'][$first_cell_key]) || empty($row['cells'][$first_cell_key]['content'])) { $row['cells'][$first_cell_key]['attributes']->addClass('tabledrag-cell--only-drag'); } } } } } /** * Implements template_preprocess_HOOK() for field_ui_table. */ function claro_preprocess_field_ui_table(&$variables) { claro_preprocess_table($variables); } /** * Implements template_preprocess_HOOK() for views_view_table. * * @todo Revisit after https://www.drupal.org/node/3025726 or * https://www.drupal.org/node/1973418 is in. */ function claro_preprocess_views_view_table(&$variables) { if (!empty($variables['header'])) { foreach ($variables['header'] as &$header_cell) { if (!empty($header_cell['url'])) { $parsed_url = UrlHelper::parse($header_cell['url']); $query = !empty($parsed_url['query']) ? $parsed_url['query'] : []; if (isset($query['order']) && isset($query['sort'])) { $header_cell['attributes']->addClass('sortable-heading'); } } } } } /** * Implements hook_preprocess_HOOK() for links__dropbutton__operations. * * Operations always use the extra small dropbutton variant. */ function claro_preprocess_links__dropbutton__operations(&$variables) { $item_classes = ['dropbutton__item', 'dropbutton__item--extrasmall']; $variables['attributes']['class'][] = 'dropbutton--extrasmall'; foreach ($variables['links'] as &$link_data) { $link_data['attributes']->addClass($item_classes); } } /** * Implements hook_preprocess_HOOK() for links__dropbutton. */ function claro_preprocess_links__dropbutton(&$variables) { // Add the right CSS class for the dropbutton list that helps reducing FOUC. if (!empty($variables['links'])) { $variables['attributes']['class'][] = count($variables['links']) > 1 ? 'dropbutton--multiple' : 'dropbutton--single'; } $item_classes = ['dropbutton__item']; // Check that the dropbutton has a supported variant class. // @todo Revisit after https://www.drupal.org/node/3057581 is added. if (!empty($variables['attributes']['class'])) { if (array_search('dropbutton--small', $variables['attributes']['class'])) { $item_classes[] = 'dropbutton__item--small'; } elseif (array_search('dropbutton--extrasmall', $variables['attributes']['class'])) { $item_classes[] = 'dropbutton__item--extrasmall'; } } foreach ($variables['links'] as &$link_data) { $link_data['attributes']->addClass($item_classes); } } /** * Implements hook_preprocess_HOOK() for views_ui_display_tab_bucket. */ function claro_preprocess_views_ui_display_tab_bucket(&$variables) { // Instead of re-styling Views UI dropbuttons with module-specific CSS styles, // change dropbutton variants to the extra small version. // @todo Revisit after https://www.drupal.org/node/3057581 is added. if (!empty($variables['actions']) && $variables['actions']['#type'] === 'dropbutton') { $variables['actions']['#dropbutton_type'] = 'extrasmall'; } } /** * Implements hook_preprocess_HOOK() for status_messages. */ function claro_preprocess_status_messages(&$variables) { $variables['title_ids'] = []; foreach ($variables['message_list'] as $message_type => $messages) { $variables['title_ids'][$message_type] = Html::getUniqueId("message-$message_type-title"); } } /** * Implements hook_preprocess_HOOK() for system_themes_page. */ function claro_preprocess_system_themes_page(&$variables) { if (!empty($variables['theme_groups'])) { foreach ($variables['theme_groups'] as &$theme_group) { if (!empty($theme_group['themes'])) { foreach ($theme_group['themes'] as &$theme_card) { /** * @todo Remove dependency on attributes after * https://www.drupal.org/project/drupal/issues/2511548 has been * resolved. */ if (isset($theme_card['screenshot']['#attributes']) && $theme_card['screenshot']['#attributes'] instanceof Attribute && $theme_card['screenshot']['#attributes']->hasClass('no-screenshot')) { unset($theme_card['screenshot']); } $theme_card['title_id'] = Html::getUniqueId($theme_card['name'] . '-label'); $description_is_empty = empty((string) $theme_card['description']); // Set description_id only if the description is not empty. if (!$description_is_empty) { $theme_card['description_id'] = Html::getUniqueId($theme_card['name'] . '-description'); } if (!empty($theme_card['operations']) && !empty($theme_card['operations']['#theme']) && $theme_card['operations']['#theme'] === 'links') { $theme_card['operations']['#theme'] = 'links__action_links'; } } } } } } /** * Implements hook_preprocess_HOOK() for links__action_links. */ function claro_preprocess_links__action_links(&$variables) { $variables['attributes']['class'][] = 'action-links'; foreach ($variables['links'] as $delta => $link_item) { $variables['links'][$delta]['attributes']->addClass('action-links__item'); } } /** * Implements hook_preprocess_HOOK() for file_managed_file. */ function claro_preprocess_file_managed_file(&$variables) { // Produce the same renderable element structure as image widget has. $child_keys = Element::children($variables['element']); foreach ($child_keys as $child_key) { $variables['data'][$child_key] = $variables['element'][$child_key]; } _claro_preprocess_file_and_image_widget($variables); } /** * Implements hook_preprocess_HOOK() for file_widget_multiple. */ function claro_preprocess_file_widget_multiple(&$variables) { $has_upload = FALSE; if (isset($variables['table']['#type']) && $variables['table']['#type'] === 'table') { // Add a variant class for the table. $variables['table']['#attributes']['class'][] = 'table-file-multiple-widget'; // Mark table disabled if the field widget is disabled. if (isset($variables['element']['#disabled']) && $variables['element']['#disabled']) { $variables['table']['#attributes']['class'][] = 'tabledrag-disabled'; $variables['table']['#attributes']['class'][] = 'js-tabledrag-disabled'; // We will add the 'is-disabled' CSS class to the disabled table header // cells. foreach ($variables['table']['#header'] as &$cell) { if (is_array($cell) && isset($cell['data'])) { $cell = $cell + ['class' => []]; $cell['class'][] = 'is-disabled'; } else { // We have to modify the structure of this header cell. $cell = [ 'data' => $cell, 'class' => ['is-disabled'], ]; } } } // Mark operations column cells with a CSS class. if (isset($variables['table']['#rows']) && is_array($variables['table']['#rows'])) { foreach ($variables['table']['#rows'] as $row_key => $row) { if (isset($row['data']) && is_array($row['data'])) { $last_cell = end($row['data']); $last_cell_key = key($row['data']); if (is_array($last_cell['data'])) { foreach ($last_cell['data'] as $last_cell_item) { if (isset($last_cell_item['#attributes']['class']) && is_array($last_cell_item['#attributes']['class']) && in_array('remove-button', $last_cell_item['#attributes']['class'])) { $variables['table']['#rows'][$row_key]['data'][$last_cell_key] += ['class' => []]; $variables['table']['#rows'][$row_key]['data'][$last_cell_key]['class'][] = 'file-operations-cell'; break 1; } } } } } } // Add a CSS class to the table if an upload widget is present. // This is required for removing the border of the last table row. if (!empty($variables['element'])) { $element_keys = Element::children($variables['element']); foreach ($element_keys as $delta) { if (!isset($variables['element'][$delta]['upload']['#access']) || $variables['element'][$delta]['upload']['#access'] !== FALSE) { $has_upload = TRUE; break 1; } } } $variables['table']['#attributes']['class'][] = $has_upload ? 'table-file-multiple-widget--has-upload' : 'table-file-multiple-widget--no-upload'; } $table_is_not_empty = isset($variables['table']['#rows']) && !empty($variables['table']['#rows']); $table_is_accessible = !isset($variables['table']['#access']) || (isset($variables['table']['#access']) && $variables['table']['#access'] !== FALSE); $variables['has_table'] = $table_is_not_empty && $table_is_accessible; } /** * Implements hook_preprocess_HOOK() for image_widget. */ function claro_preprocess_image_widget(&$variables) { // Stable adds the file size as #suffix for image file_link renderable array. // We have to remove that because we will render it in our file_link template // for every kind of files, and not just for images. if (!empty($variables['element']['fids']['#value'])) { $file = reset($variables['element']['#files']); unset($variables['data']['file_' . $file->id()]['filename']['#suffix']); } _claro_preprocess_file_and_image_widget($variables); } /** * Helper pre-process callback for file_managed_file and image_widget. * * @param array $variables * The renderable array of image and file widgets, with 'element' and 'data' * keys. */ function _claro_preprocess_file_and_image_widget(array &$variables) { $element = $variables['element']; $main_item_keys = [ 'upload', 'upload_button', 'remove_button', ]; // Calculate helper values for the template. $upload_is_accessible = !isset($element['upload']['#access']) || $element['upload']['#access'] !== FALSE; $is_multiple = !empty($element['#cardinality']) && $element['#cardinality'] !== 1; $has_value = isset($element['#value']['fids']) && !empty($element['#value']['fids']); // File widget properties. $display_can_be_displayed = !empty($element['#display_field']); // Display is rendered in a separate table cell for multiple value widgets. $display_is_visible = $display_can_be_displayed && !$is_multiple && isset($element['display']['#type']) && $element['display']['#type'] !== 'hidden'; $description_can_be_displayed = !empty($element['#description_field']); $description_is_visible = $description_can_be_displayed && isset($element['description']); // Image widget properties. $alt_can_be_displayed = !empty($element['#alt_field']); $alt_is_visible = $alt_can_be_displayed && (!isset($element['alt']['#access']) || $element['alt']['#access'] !== FALSE); $title_can_be_displayed = !empty($element['#title_field']); $title_is_visible = $title_can_be_displayed && (!isset($element['title']['#access']) || $element['title']['#access'] !== FALSE); $variables['multiple'] = $is_multiple; $variables['upload'] = $upload_is_accessible; $variables['has_value'] = $has_value; $variables['has_meta'] = $alt_is_visible || $title_is_visible || $display_is_visible || $description_is_visible; $variables['display'] = $display_is_visible; // Render file upload input and upload button (or file name and remove button, // if the field is not empty) in an emphasized div. foreach ($variables['data'] as $key => $item) { $item_is_filename = isset($item['filename']['#file']) && $item['filename']['#file'] instanceof FileInterface; // Move filename to main items. if ($item_is_filename) { $variables['main_items']['filename'] = $item; unset($variables['data'][$key]); continue; } // Move buttons, upload input and hidden items to main items. if (in_array($key, $main_item_keys)) { $variables['main_items'][$key] = $item; unset($variables['data'][$key]); } } } /** * Implements hook_preprocess_views_view_fields(). * * This targets each rendered media item in the grid display of the media * library's modal dialog. */ function claro_preprocess_views_view_fields__media_library(array &$variables) { // Add classes to media rendered entity field so it can be targeted for // styling. Adding this class in a template is very difficult to do. if (isset($variables['fields']['rendered_entity']->wrapper_attributes)) { $variables['fields']['rendered_entity']->wrapper_attributes->addClass('media-library-item__click-to-select-trigger'); } } /** * Implements hook_form_BASE_FORM_ID_alter(). */ function claro_form_media_library_add_form_alter(array &$form, FormStateInterface $form_state) { $form['#attributes']['class'][] = 'media-library-add-form'; $form['#attached']['library'][] = 'claro/media_library.theme'; // If there are unsaved media items, apply styling classes to various parts // of the form. if (isset($form['media'])) { $form['#attributes']['class'][] = 'media-library-add-form--with-input'; // Put a wrapper around the informational message above the unsaved media // items. $form['description']['#template'] = '
{{ text }}
'; } else { $form['#attributes']['class'][] = 'media-library-add-form--without-input'; } } /** * Implements hook_form_FORM_ID_alter(). */ function claro_form_media_library_add_form_upload_alter(array &$form, FormStateInterface $form_state) { $form['#attributes']['class'][] = 'media-library-add-form--upload'; if (isset($form['container'])) { $form['container']['#attributes']['class'][] = 'media-library-add-form__input-wrapper'; } } /** * Implements hook_form_FORM_ID_alter(). */ function claro_form_media_library_add_form_oembed_alter(array &$form, FormStateInterface $form_state) { $form['#attributes']['class'][] = 'media-library-add-form--oembed'; // If no media items have been added yet, add a couple of styling classes // to the initial URL form. if (isset($form['container'])) { $form['container']['#attributes']['class'][] = 'media-library-add-form__input-wrapper'; $form['container']['url']['#attributes']['class'][] = 'media-library-add-form-oembed-url'; $form['container']['submit']['#attributes']['class'][] = 'media-library-add-form-oembed-submit'; } } /** * Implements hook_preprocess_item_list__media_library_add_form_media_list(). * * This targets each new, unsaved media item added to the media library, before * they are saved. */ function claro_preprocess_item_list__media_library_add_form_media_list(array &$variables) { foreach ($variables['items'] as &$item) { $item['value']['preview']['#attributes']['class'][] = 'media-library-add-form__preview'; $item['value']['fields']['#attributes']['class'][] = 'media-library-add-form__fields'; $item['value']['remove_button']['#attributes']['class'][] = 'media-library-add-form__remove-button'; // #source_field_name is set by AddFormBase::buildEntityFormElement() // to help themes and form_alter hooks identify the source field. $fields = &$item['value']['fields']; $source_field_name = $fields['#source_field_name']; if (isset($fields[$source_field_name])) { $fields[$source_field_name]['#attributes']['class'][] = 'media-library-add-form__source-field'; } } } /** * Implements hook_preprocess_media_library_item__widget(). * * This targets each media item selected in an entity reference field. */ function claro_preprocess_media_library_item__widget(array &$variables) { $variables['content']['remove_button']['#attributes']['class'][] = 'media-library-item__remove'; } /** * Implements hook_preprocess_media_library_item__small(). * * This targets each pre-selected media item selected when adding new media in * the modal media library dialog. */ function claro_preprocess_media_library_item__small(array &$variables) { $variables['content']['select']['#attributes']['class'][] = 'media-library-item__click-to-select-checkbox'; } /** * @todo Remove this when https://www.drupal.org/project/drupal/issues/2999549 * lands. * * @see \Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget::formElement() */ function claro_preprocess_fieldset__media_library_widget(array &$variables) { if (isset($variables['prefix']['weight_toggle'])) { $variables['prefix']['weight_toggle']['#attributes']['class'][] = 'media-library-widget__toggle-weight'; } if (isset($variables['suffix']['open_button'])) { $variables['suffix']['open_button']['#attributes']['class'][] = 'media-library-open-button'; } } /** * Implements hook_views_pre_render(). */ function claro_views_pre_render(ViewExecutable $view) { $add_classes = function (&$option, array $classes_to_add) { $classes = preg_split('/\s+/', $option); $classes = array_filter($classes); $classes = array_merge($classes, $classes_to_add); $option = implode(' ', array_unique($classes)); }; if ($view->id() === 'media_library') { if ($view->display_handler->options['defaults']['css_class']) { $add_classes($view->displayHandlers->get('default')->options['css_class'], ['media-library-view']); } else { $add_classes($view->display_handler->options['css_class'], ['media-library-view']); } if ($view->current_display === 'page') { if (array_key_exists('media_bulk_form', $view->field)) { $add_classes($view->field['media_bulk_form']->options['element_class'], ['media-library-item__click-to-select-checkbox']); } if (array_key_exists('rendered_entity', $view->field)) { $add_classes($view->field['rendered_entity']->options['element_class'], ['media-library-item__content']); } if (array_key_exists('edit_media', $view->field)) { $add_classes($view->field['edit_media']->options['alter']['link_class'], ['media-library-item__edit']); } if (array_key_exists('delete_media', $view->field)) { $add_classes($view->field['delete_media']->options['alter']['link_class'], ['media-library-item__remove']); } } elseif (strpos($view->current_display, 'widget') === 0) { if (array_key_exists('rendered_entity', $view->field)) { $add_classes($view->field['rendered_entity']->options['element_class'], ['media-library-item__content']); } if (array_key_exists('media_library_select_form', $view->field)) { $add_classes($view->field['media_library_select_form']->options['element_wrapper_class'], ['media-library-item__click-to-select-checkbox']); } if ($view->display_handler->options['defaults']['css_class']) { $add_classes($view->displayHandlers->get('default')->options['css_class'], ['media-library-view--widget']); } else { $add_classes($view->display_handler->options['css_class'], ['media-library-view--widget']); } } } }