diff --git a/core/includes/theme.inc b/core/includes/theme.inc index dfee854..d3d8553 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1538,9 +1538,15 @@ function template_preprocess_field(&$variables, $hook) { // Creating variables for the template. $variables['entity_type'] = $element['#entity_type']; - $variables['field_name'] = $element['#field_name']; - $variables['field_type'] = $element['#field_type']; + $variables['field_name'] = strtr($element['#field_name'], '_', '-'); + $variables['field_type'] = strtr($element['#field_type'], '_', '-'); + $variables['field_label'] = strtr($element['#label_display'], '_', '-'); $variables['label_display'] = $element['#label_display']; + // Are there multiple field items. + $variables['multiple'] = FALSE; + if (isset($element['#items']) && is_callable($element['#items']->getFieldDefinition(), 'isMultiple')) { + $variables['multiple'] = $element['#items']->getFieldDefinition()->isMultiple(); + } $variables['label_hidden'] = ($element['#label_display'] == 'hidden'); // Always set the field label - allow themes to decide whether to display it. @@ -1552,6 +1558,18 @@ function template_preprocess_field(&$variables, $hook) { if (!isset($default_attributes)) { $default_attributes = new Attribute; } + // Merge the attributes when its a multiple fields with hidden label + if ($element['#label_display'] == 'hidden' && $variables['multiple']) { + $variables['attributes'] = NestedArray::mergeDeep($variables['attributes'], $variables['content_attributes']); + } + // Merge the attributes when its a single field with a label + if ($element['#label_display'] != 'hidden' && !$variables['multiple']) { + $variables['content_attributes'] = NestedArray::mergeDeep($variables['content_attributes'], (array) $element['#items'][0]->_attributes); + } + // Merge the attributes when its a single field with hidden label + if ($element['#label_display'] == 'hidden' && !$variables['multiple']) { + $variables['attributes'] = NestedArray::mergeDeep($variables['attributes'], $variables['content_attributes'], (array) $element['#items'][0]->_attributes); + } // We want other preprocess functions and the theme implementation to have // fast access to the field item render arrays. The item render array keys diff --git a/core/includes/theme.inc.orig b/core/includes/theme.inc.orig new file mode 100644 index 0000000..dfee854 --- /dev/null +++ b/core/includes/theme.inc.orig @@ -0,0 +1,1863 @@ +get(); + } + else { + return $theme_registry->getRuntime(); + } +} + +/** + * Forces the system to rebuild the theme registry. + * + * This function should be called when modules are added to the system, or when + * a dynamic system needs to add more theme hooks. + */ +function drupal_theme_rebuild() { + \Drupal::service('theme.registry')->reset(); +} + +/** + * Allows themes and/or theme engines to discover overridden theme functions. + * + * @param $cache + * The existing cache of theme hooks to test against. + * @param $prefixes + * An array of prefixes to test, in reverse order of importance. + * + * @return $implementations + * The functions found, suitable for returning from hook_theme; + */ +function drupal_find_theme_functions($cache, $prefixes) { + $implementations = array(); + $functions = get_defined_functions(); + + foreach ($cache as $hook => $info) { + foreach ($prefixes as $prefix) { + // Find theme functions that implement possible "suggestion" variants of + // registered theme hooks and add those as new registered theme hooks. + // The 'pattern' key defines a common prefix that all suggestions must + // start with. The default is the name of the hook followed by '__'. An + // 'base hook' key is added to each entry made for a found suggestion, + // so that common functionality can be implemented for all suggestions of + // the same base hook. To keep things simple, deep hierarchy of + // suggestions is not supported: each suggestion's 'base hook' key + // refers to a base hook, not to another suggestion, and all suggestions + // are found using the base hook's pattern, not a pattern from an + // intermediary suggestion. + $pattern = isset($info['pattern']) ? $info['pattern'] : ($hook . '__'); + if (!isset($info['base hook']) && !empty($pattern)) { + $matches = preg_grep('/^' . $prefix . '_' . $pattern . '/', $functions['user']); + if ($matches) { + foreach ($matches as $match) { + $new_hook = substr($match, strlen($prefix) + 1); + $arg_name = isset($info['variables']) ? 'variables' : 'render element'; + $implementations[$new_hook] = array( + 'function' => $match, + $arg_name => $info[$arg_name], + 'base hook' => $hook, + ); + } + } + } + // Find theme functions that implement registered theme hooks and include + // that in what is returned so that the registry knows that the theme has + // this implementation. + if (function_exists($prefix . '_' . $hook)) { + $implementations[$hook] = array( + 'function' => $prefix . '_' . $hook, + ); + } + } + } + + return $implementations; +} + +/** + * Allows themes and/or theme engines to easily discover overridden templates. + * + * @param $cache + * The existing cache of theme hooks to test against. + * @param $extension + * The extension that these templates will have. + * @param $path + * The path to search. + */ +function drupal_find_theme_templates($cache, $extension, $path) { + $implementations = array(); + + // Collect paths to all sub-themes grouped by base themes. These will be + // used for filtering. This allows base themes to have sub-themes in its + // folder hierarchy without affecting the base themes template discovery. + $theme_paths = array(); + foreach (\Drupal::service('theme_handler')->listInfo() as $theme_info) { + if (!empty($theme_info->base_theme)) { + $theme_paths[$theme_info->base_theme][$theme_info->getName()] = $theme_info->getPath(); + } + } + foreach ($theme_paths as $basetheme => $subthemes) { + foreach ($subthemes as $subtheme => $subtheme_path) { + if (isset($theme_paths[$subtheme])) { + $theme_paths[$basetheme] = array_merge($theme_paths[$basetheme], $theme_paths[$subtheme]); + } + } + } + $theme = \Drupal::theme()->getActiveTheme()->getName(); + $subtheme_paths = isset($theme_paths[$theme]) ? $theme_paths[$theme] : array(); + + // Escape the periods in the extension. + $regex = '/' . str_replace('.', '\.', $extension) . '$/'; + // Get a listing of all template files in the path to search. + $files = file_scan_directory($path, $regex, array('key' => 'filename')); + + // Find templates that implement registered theme hooks and include that in + // what is returned so that the registry knows that the theme has this + // implementation. + foreach ($files as $template => $file) { + // Ignore sub-theme templates for the current theme. + if (strpos($file->uri, str_replace($subtheme_paths, '', $file->uri)) !== 0) { + continue; + } + // Remove the extension from the filename. + $template = str_replace($extension, '', $template); + // Transform - in filenames to _ to match function naming scheme + // for the purposes of searching. + $hook = strtr($template, '-', '_'); + if (isset($cache[$hook])) { + $implementations[$hook] = array( + 'template' => $template, + 'path' => dirname($file->uri), + ); + } + + // Match templates based on the 'template' filename. + foreach ($cache as $hook => $info) { + if (isset($info['template'])) { + $template_candidates = array($info['template'], str_replace($info['theme path'] . '/templates/', '', $info['template'])); + if (in_array($template, $template_candidates)) { + $implementations[$hook] = array( + 'template' => $template, + 'path' => dirname($file->uri), + ); + } + } + } + } + + // Find templates that implement possible "suggestion" variants of registered + // theme hooks and add those as new registered theme hooks. See + // drupal_find_theme_functions() for more information about suggestions and + // the use of 'pattern' and 'base hook'. + $patterns = array_keys($files); + foreach ($cache as $hook => $info) { + $pattern = isset($info['pattern']) ? $info['pattern'] : ($hook . '__'); + if (!isset($info['base hook']) && !empty($pattern)) { + // Transform _ in pattern to - to match file naming scheme + // for the purposes of searching. + $pattern = strtr($pattern, '_', '-'); + + $matches = preg_grep('/^' . $pattern . '/', $patterns); + if ($matches) { + foreach ($matches as $match) { + $file = $match; + // Remove the extension from the filename. + $file = str_replace($extension, '', $file); + // Put the underscores back in for the hook name and register this + // pattern. + $arg_name = isset($info['variables']) ? 'variables' : 'render element'; + $implementations[strtr($file, '-', '_')] = array( + 'template' => $file, + 'path' => dirname($files[$match]->uri), + $arg_name => $info[$arg_name], + 'base hook' => $hook, + ); + } + } + } + } + return $implementations; +} + +/** + * Retrieves a setting for the current theme or for a given theme. + * + * The final setting is obtained from the last value found in the following + * sources: + * - the saved values from the global theme settings form + * - the saved values from the theme's settings form + * To only retrieve the default global theme setting, an empty string should be + * given for $theme. + * + * @param $setting_name + * The name of the setting to be retrieved. + * @param $theme + * The name of a given theme; defaults to the current theme. + * + * @return + * The value of the requested setting, NULL if the setting does not exist. + */ +function theme_get_setting($setting_name, $theme = NULL) { + /** @var \Drupal\Core\Theme\ThemeSettings[] $cache */ + $cache = &drupal_static(__FUNCTION__, array()); + + // If no key is given, use the current theme if we can determine it. + if (!isset($theme)) { + $theme = \Drupal::theme()->getActiveTheme()->getName(); + } + + if (empty($cache[$theme])) { + // Create a theme settings object. + $cache[$theme] = new ThemeSettings($theme); + // Get the global settings from configuration. + $cache[$theme]->setData(\Drupal::config('system.theme.global')->get()); + + // Get the values for the theme-specific settings from the .info.yml files + // of the theme and all its base themes. + $themes = \Drupal::service('theme_handler')->listInfo(); + if (isset($themes[$theme])) { + $theme_object = $themes[$theme]; + + // Retrieve configured theme-specific settings, if any. + try { + if ($theme_settings = \Drupal::config($theme . '.settings')->get()) { + $cache[$theme]->merge($theme_settings); + } + } + catch (StorageException $e) { + } + + // If the theme does not support a particular feature, override the global + // setting and set the value to NULL. + if (!empty($theme_object->info['features'])) { + foreach (_system_default_theme_features() as $feature) { + if (!in_array($feature, $theme_object->info['features'])) { + $cache[$theme]->set('features.' . $feature, NULL); + } + } + } + + // Generate the path to the logo image. + if ($cache[$theme]->get('features.logo')) { + $logo_path = $cache[$theme]->get('logo.path'); + if ($cache[$theme]->get('logo.use_default')) { + $cache[$theme]->set('logo.url', file_create_url($theme_object->getPath() . '/logo.svg')); + } + elseif ($logo_path) { + $cache[$theme]->set('logo.url', file_create_url($logo_path)); + } + } + + // Generate the path to the favicon. + if ($cache[$theme]->get('features.favicon')) { + $favicon_path = $cache[$theme]->get('favicon.path'); + if ($cache[$theme]->get('favicon.use_default')) { + if (file_exists($favicon = $theme_object->getPath() . '/favicon.ico')) { + $cache[$theme]->set('favicon.url', file_create_url($favicon)); + } + else { + $cache[$theme]->set('favicon.url', file_create_url('core/misc/favicon.ico')); + } + } + elseif ($favicon_path) { + $cache[$theme]->set('favicon.url', file_create_url($favicon_path)); + } + else { + $cache[$theme]->set('features.favicon', FALSE); + } + } + } + } + + return $cache[$theme]->get($setting_name); +} + +/** + * Converts theme settings to configuration. + * + * @see system_theme_settings_submit() + * + * @param array $theme_settings + * An array of theme settings from system setting form or a Drupal 7 variable. + * @param Config $config + * The configuration object to update. + * + * @return + * The Config object with updated data. + */ +function theme_settings_convert_to_config(array $theme_settings, Config $config) { + foreach ($theme_settings as $key => $value) { + if ($key == 'default_logo') { + $config->set('logo.use_default', $value); + } + else if ($key == 'logo_path') { + $config->set('logo.path', $value); + } + else if ($key == 'default_favicon') { + $config->set('favicon.use_default', $value); + } + else if ($key == 'favicon_path') { + $config->set('favicon.path', $value); + } + else if ($key == 'favicon_mimetype') { + $config->set('favicon.mimetype', $value); + } + else if (substr($key, 0, 7) == 'toggle_') { + $config->set('features.' . Unicode::substr($key, 7), $value); + } + else if (!in_array($key, array('theme', 'logo_upload'))) { + $config->set($key, $value); + } + } + return $config; +} + +/** + * Prepares variables for time templates. + * + * Default template: time.html.twig. + * + * @param array $variables + * An associative array possibly containing: + * - attributes['timestamp']: + * - timestamp: + * - text: + */ +function template_preprocess_time(&$variables) { + // Format the 'datetime' attribute based on the timestamp. + // @see http://www.w3.org/TR/html5-author/the-time-element.html#attr-time-datetime + if (!isset($variables['attributes']['datetime']) && isset($variables['timestamp'])) { + $variables['attributes']['datetime'] = format_date($variables['timestamp'], 'html_datetime', '', 'UTC'); + } + + // If no text was provided, try to auto-generate it. + if (!isset($variables['text'])) { + // Format and use a human-readable version of the timestamp, if any. + if (isset($variables['timestamp'])) { + $variables['text'] = format_date($variables['timestamp']); + $variables['html'] = FALSE; + } + // Otherwise, use the literal datetime attribute. + elseif (isset($variables['attributes']['datetime'])) { + $variables['text'] = $variables['attributes']['datetime']; + $variables['html'] = FALSE; + } + } +} + +/** + * Prepares variables for datetime form element templates. + * + * The datetime form element serves as a wrapper around the date element type, + * which creates a date and a time component for a date. + * + * Default template: datetime-form.html.twig. + * + * @param array $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #title, #value, #options, #description, #required, + * #attributes. + * + * @see form_process_datetime() + */ +function template_preprocess_datetime_form(&$variables) { + $element = $variables['element']; + + $variables['attributes'] = array(); + if (isset($element['#id'])) { + $variables['attributes']['id'] = $element['#id']; + } + if (!empty($element['#attributes']['class'])) { + $variables['attributes']['class'] = (array) $element['#attributes']['class']; + } + + $variables['content'] = $element; +} + +/** + * Prepares variables for datetime form wrapper templates. + * + * Default template: datetime-wrapper.html.twig. + * + * @param array $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #title, #children, #required, #attributes. + */ +function template_preprocess_datetime_wrapper(&$variables) { + $element = $variables['element']; + + if (!empty($element['#title'])) { + $variables['title'] = $element['#title']; + } + + if (!empty($element['#description'])) { + $variables['description'] = $element['#description']; + } + + $variables['required'] = FALSE; + // For required datetime fields a 'form-required' class is appended to the + // label attributes. + if (!empty($element['#required'])) { + $variables['required'] = TRUE; + } + $variables['content'] = $element['#children']; +} + +/** + * Prepares variables for links templates. + * + * Default template: links.html.twig. + * + * @param array $variables + * An associative array containing: + * - links: An associative array of links to be themed. The key for each link + * is used as its CSS class. Each link should be itself an array, with the + * following elements: + * - title: The link text. + * - url: (optional) The url object to link to. If omitted, no a tag is + * printed out. + * - attributes: (optional) Attributes for the anchor, or for the + * tag used in its place if no 'href' is supplied. If element 'class' is + * included, it must be an array of one or more class names. + * If the 'href' element is supplied, the entire link array is passed to + * l() as its $options parameter. + * - attributes: A keyed array of attributes for the UL containing the + * list of links. + * - set_active_class: (optional) Whether each link should compare the + * route_name + route_parameters or href (path), language and query options + * to the current URL, to determine whether the link is "active". If so, an + * "active" class will be applied to the list item containing the link, as + * well as the link itself. It is important to use this sparingly since it + * is usually unnecessary and requires extra processing. + * For anonymous users, the "active" class will be calculated on the server, + * because most sites serve each anonymous user the same cached page anyway. + * For authenticated users, the "active" class will be calculated on the + * client (through JavaScript), only data- attributes are added to list + * items and contained links, to prevent breaking the render cache. The + * JavaScript is added in system_page_attachments(). + * - heading: (optional) A heading to precede the links. May be an + * associative array or a string. If it's an array, it can have the + * following elements: + * - text: The heading text. + * - level: The heading level (e.g. 'h2', 'h3'). + * - attributes: (optional) An array of the CSS attributes for the heading. + * When using a string it will be used as the text of the heading and the + * level will default to 'h2'. Headings should be used on navigation menus + * and any list of links that consistently appears on multiple pages. To + * make the heading invisible use the 'visually-hidden' CSS class. Do not + * use 'display:none', which removes it from screen readers and assistive + * technology. Headings allow screen reader and keyboard only users to + * navigate to or skip the links. See + * http://juicystudio.com/article/screen-readers-display-none.php and + * http://www.w3.org/TR/WCAG-TECHS/H42.html for more information. + * + * Unfortunately links templates duplicate the "active" class handling of l() + * and LinkGenerator::generate() because it needs to be able to set the "active" + * class not on the links themselves ("a" tags), but on the list items ("li" + * tags) that contain the links. This is necessary for CSS to be able to style + * list items differently when the link is active, since CSS does not yet allow + * one to style list items only if it contains a certain element with a certain + * class. I.e. we cannot yet convert this jQuery selector to a CSS selector: + * jQuery('li:has("a.active")') + * + * @see \Drupal\Core\Utility\LinkGenerator + * @see \Drupal\Core\Utility\LinkGenerator::generate() + * @see system_page_attachments() + */ +function template_preprocess_links(&$variables) { + $links = $variables['links']; + $heading = &$variables['heading']; + + if (!empty($links)) { + // Prepend the heading to the list, if any. + if (!empty($heading)) { + // Convert a string heading into an array, using a H2 tag by default. + if (is_string($heading)) { + $heading = array('text' => $heading); + } + // Merge in default array properties into $heading. + $heading += array( + 'level' => 'h2', + 'attributes' => array(), + ); + // Convert the attributes array into an Attribute object. + $heading['attributes'] = new Attribute($heading['attributes']); + $heading['text'] = SafeMarkup::checkPlain($heading['text']); + } + + $variables['links'] = array(); + foreach ($links as $key => $link) { + $item = array(); + $link += array( + 'ajax' => NULL, + 'url' => NULL, + ); + + $li_attributes = array(); + $keys = ['title', 'url']; + $link_element = array( + '#type' => 'link', + '#title' => $link['title'], + '#options' => array_diff_key($link, array_combine($keys, $keys)), + '#url' => $link['url'], + '#ajax' => $link['ajax'], + ); + + // Handle links and ensure that the active class is added on the LIs, but + // only if the 'set_active_class' option is not empty. + if (isset($link['url'])) { + if (!empty($variables['set_active_class'])) { + + // Also enable set_active_class for the contained link. + $link_element['#options']['set_active_class'] = TRUE; + + if (!empty($link['language'])) { + $li_attributes['hreflang'] = $link['language']->getId(); + } + + // Add a "data-drupal-link-query" attribute to let the + // drupal.active-link library know the query in a standardized manner. + if (!empty($link['query'])) { + $query = $link['query']; + ksort($query); + $li_attributes['data-drupal-link-query'] = Json::encode($query); + } + + /** @var \Drupal\Core\Url $url */ + $url = $link['url']; + if ($url->isRouted()) { + // Add a "data-drupal-link-system-path" attribute to let the + // drupal.active-link library know the path in a standardized manner. + $system_path = $url->getInternalPath(); + // @todo System path is deprecated - use the route name and parameters. + // Special case for the front page. + $li_attributes['data-drupal-link-system-path'] = $system_path == '' ? '' : $system_path; + } + } + + $item['link'] = $link_element; + } + + // Handle title-only text items. + $item['text'] = $link['title']; + if (isset($link['attributes'])) { + $item['text_attributes'] = new Attribute($link['attributes']); + } + + // Handle list item attributes. + $item['attributes'] = new Attribute($li_attributes); + + // Add the item to the list of links. + $variables['links'][$key] = $item; + } + } +} + +/** + * Prepares variables for image templates. + * + * Default template: image.html.twig. + * + * @param array $variables + * An associative array containing: + * - uri: Either the path of the image file (relative to base_path()) or a + * full URL. + * - width: The width of the image (if known). + * - height: The height of the image (if known). + * - alt: The alternative text for text-based browsers. HTML 4 and XHTML 1.0 + * always require an alt attribute. The HTML 5 draft allows the alt + * attribute to be omitted in some cases. Therefore, this variable defaults + * to an empty string, but can be set to NULL for the attribute to be + * omitted. Usually, neither omission nor an empty string satisfies + * accessibility requirements, so it is strongly encouraged for code + * calling _theme('image') to pass a meaningful value for this variable. + * - http://www.w3.org/TR/REC-html40/struct/objects.html#h-13.8 + * - http://www.w3.org/TR/xhtml1/dtds.html + * - http://dev.w3.org/html5/spec/Overview.html#alt + * - title: The title text is displayed when the image is hovered in some + * popular browsers. + * - attributes: Associative array of attributes to be placed in the img tag. + * - srcset: Array of multiple URIs and sizes/multipliers. + * - sizes: The sizes attribute for viewport-based selection of images. + * - http://www.whatwg.org/specs/web-apps/current-work/multipage/embedded-content.html#introduction-3:viewport-based-selection-2 + */ +function template_preprocess_image(&$variables) { + if (!empty($variables['uri'])) { + $variables['attributes']['src'] = file_create_url($variables['uri']); + } + // Generate a srcset attribute conforming to the spec at + // http://www.w3.org/html/wg/drafts/html/master/embedded-content.html#attr-img-srcset + if (!empty($variables['srcset'])) { + $srcset = array(); + foreach ($variables['srcset'] as $src) { + // URI is mandatory. + $source = file_create_url($src['uri']); + if (isset($src['width']) && !empty($src['width'])) { + $source .= ' ' . $src['width']; + } + elseif (isset($src['multiplier']) && !empty($src['multiplier'])) { + $source .= ' ' . $src['multiplier']; + } + $srcset[] = $source; + } + $variables['attributes']['srcset'] = implode(', ', $srcset); + } + + foreach (array('width', 'height', 'alt', 'title', 'sizes') as $key) { + if (isset($variables[$key])) { + // If the property has already been defined in the attributes, + // do not override, including NULL. + if (array_key_exists($key, $variables['attributes'])) { + continue; + } + $variables['attributes'][$key] = $variables[$key]; + } + } +} + +/** + * Prepares variables for table templates. + * + * Default template: table.html.twig. + * + * @param array $variables + * An associative array containing: + * - header: An array containing the table headers. Each element of the array + * can be either a localized string or an associative array with the + * following keys: + * - data: The localized title of the table column. + * - field: The database field represented in the table column (required + * if user is to be able to sort on this column). + * - sort: A default sort order for this column ("asc" or "desc"). Only + * one column should be given a default sort order because table sorting + * only applies to one column at a time. + * - class: An array of values for the 'class' attribute. In particular, + * the least important columns that can be hidden on narrow and medium + * width screens should have a 'priority-low' class, referenced with the + * RESPONSIVE_PRIORITY_LOW constant. Columns that should be shown on + * medium+ wide screens should be marked up with a class of + * 'priority-medium', referenced by with the RESPONSIVE_PRIORITY_MEDIUM + * constant. Themes may hide columns with one of these two classes on + * narrow viewports to save horizontal space. + * - Any HTML attributes, such as "colspan", to apply to the column header + * cell. + * - rows: An array of table rows. Every row is an array of cells, or an + * associative array with the following keys: + * - data: An array of cells. + * - Any HTML attributes, such as "class", to apply to the table row. + * - no_striping: A Boolean indicating that the row should receive no + * 'even / odd' styling. Defaults to FALSE. + * Each cell can be either a string or an associative array with the + * following keys: + * - data: The string to display in the table cell. + * - header: Indicates this cell is a header. + * - Any HTML attributes, such as "colspan", to apply to the table cell. + * Here's an example for $rows: + * @code + * $rows = array( + * // Simple row + * array( + * 'Cell 1', 'Cell 2', 'Cell 3' + * ), + * // Row with attributes on the row and some of its cells. + * array( + * 'data' => array('Cell 1', array('data' => 'Cell 2', 'colspan' => 2)), 'class' => array('funky') + * ), + * ); + * @endcode + * - footer: An array of table rows which will be printed within a + * tag, in the same format as the rows element (see above). + * - attributes: An array of HTML attributes to apply to the table tag. + * - caption: A localized string to use for the tag. + * - colgroups: An array of column groups. Each element of the array can be + * either: + * - An array of columns, each of which is an associative array of HTML + * attributes applied to the COL element. + * - An array of attributes applied to the COLGROUP element, which must + * include a "data" attribute. To add attributes to COL elements, set the + * "data" attribute with an array of columns, each of which is an + * associative array of HTML attributes. + * Here's an example for $colgroup: + * @code + * $colgroup = array( + * // COLGROUP with one COL element. + * array( + * array( + * 'class' => array('funky'), // Attribute for the COL element. + * ), + * ), + * // Colgroup with attributes and inner COL elements. + * array( + * 'data' => array( + * array( + * 'class' => array('funky'), // Attribute for the COL element. + * ), + * ), + * 'class' => array('jazzy'), // Attribute for the COLGROUP element. + * ), + * ); + * @endcode + * These optional tags are used to group and set properties on columns + * within a table. For example, one may easily group three columns and + * apply same background style to all. + * - sticky: Use a "sticky" table header. + * - empty: The message to display in an extra row if table does not have any + * rows. + */ +function template_preprocess_table(&$variables) { + $is_sticky = !empty($variables['sticky']); + $is_responsive = !empty($variables['responsive']); + + // Format the table columns: + if (!empty($variables['colgroups'])) { + foreach ($variables['colgroups'] as &$colgroup) { + // Check if we're dealing with a simple or complex column + if (isset($colgroup['data'])) { + $cols = $colgroup['data']; + unset($colgroup['data']); + $colgroup_attributes = $colgroup; + } + else { + $cols = $colgroup; + $colgroup_attributes = array(); + } + $colgroup = array(); + $colgroup['attributes'] = new Attribute($colgroup_attributes); + $colgroup['cols'] = array(); + + // Build columns. + if (is_array($cols) && !empty($cols)) { + foreach ($cols as $col_key => $col) { + $colgroup['cols'][$col_key]['attributes'] = new Attribute($col); + } + } + } + } + + // Build an associative array of responsive classes keyed by column. + $responsive_classes = array(); + + // Format the table header: + $ts = array(); + $header_columns = 0; + if (!empty($variables['header'])) { + $ts = tablesort_init($variables['header']); + + // Use a separate index with responsive classes as headers + // may be associative. + $responsive_index = -1; + foreach ($variables['header'] as $col_key => $cell) { + // Increase the responsive index. + $responsive_index++; + + if (!is_array($cell)) { + $header_columns++; + $cell_content = $cell; + $cell_attributes = new Attribute(); + $is_header = TRUE; + } + else { + if (isset($cell['colspan'])) { + $header_columns += $cell['colspan']; + } + else { + $header_columns++; + } + $cell_content = ''; + if (isset($cell['data'])) { + $cell_content = $cell['data']; + unset($cell['data']); + } + // Flag the cell as a header or not and remove the flag. + $is_header = isset($cell['header']) ? $cell['header'] : TRUE; + unset($cell['header']); + + // Track responsive classes for each column as needed. Only the header + // cells for a column are marked up with the responsive classes by a + // module developer or themer. The responsive classes on the header cells + // must be transferred to the content cells. + if (!empty($cell['class']) && is_array($cell['class'])) { + if (in_array(RESPONSIVE_PRIORITY_MEDIUM, $cell['class'])) { + $responsive_classes[$responsive_index] = RESPONSIVE_PRIORITY_MEDIUM; + } + elseif (in_array(RESPONSIVE_PRIORITY_LOW, $cell['class'])) { + $responsive_classes[$responsive_index] = RESPONSIVE_PRIORITY_LOW; + } + } + + if (is_array($cell_content)) { + $cell_content = drupal_render($cell_content); + } + + tablesort_header($cell_content, $cell, $variables['header'], $ts); + + // tablesort_header() removes the 'sort' and 'field' keys. + $cell_attributes = new Attribute($cell); + } + $variables['header'][$col_key] = array(); + $variables['header'][$col_key]['tag'] = $is_header ? 'th' : 'td'; + $variables['header'][$col_key]['attributes'] = $cell_attributes; + $variables['header'][$col_key]['content'] = $cell_content; + } + } + $variables['header_columns'] = $header_columns; + + // Rows and footer have the same structure. + $sections = array('rows' , 'footer'); + foreach ($sections as $section) { + if (!empty($variables[$section])) { + foreach ($variables[$section] as $row_key => $row) { + $cells = $row; + $row_attributes = array(); + + // Check if we're dealing with a simple or complex row + if (isset($row['data'])) { + $cells = $row['data']; + $variables['no_striping'] = isset($row['no_striping']) ? $row['no_striping'] : FALSE; + + // Set the attributes array and exclude 'data' and 'no_striping'. + $row_attributes = $row; + unset($row_attributes['data']); + unset($row_attributes['no_striping']); + } + + // Build row. + $variables[$section][$row_key] = array(); + $variables[$section][$row_key]['attributes'] = new Attribute($row_attributes); + $variables[$section][$row_key]['cells'] = array(); + if (!empty($cells)) { + // Reset the responsive index. + $responsive_index = -1; + foreach ($cells as $col_key => $cell) { + // Increase the responsive index. + $responsive_index++; + + if (!is_array($cell)) { + $cell_content = $cell; + $cell_attributes = array(); + $is_header = FALSE; + } + else { + $cell_content = ''; + if (isset($cell['data'])) { + $cell_content = $cell['data']; + unset($cell['data']); + } + + // Flag the cell as a header or not and remove the flag. + $is_header = !empty($cell['header']); + unset($cell['header']); + + $cell_attributes = $cell; + + if (is_array($cell_content)) { + $cell_content = drupal_render($cell_content); + } + } + // Active table sort information. + if (isset($variables['header'][$col_key]['data']) && $variables['header'][$col_key]['data'] == $ts['name'] && !empty($variables['header'][$col_key]['field'])) { + $variables[$section][$row_key]['cells'][$col_key]['active_table_sort'] = TRUE; + } + // Copy RESPONSIVE_PRIORITY_LOW/RESPONSIVE_PRIORITY_MEDIUM + // class from header to cell as needed. + if (isset($responsive_classes[$responsive_index])) { + $cell_attributes['class'][] = $responsive_classes[$responsive_index]; + } + $variables[$section][$row_key]['cells'][$col_key]['tag'] = $is_header ? 'th' : 'td'; + $variables[$section][$row_key]['cells'][$col_key]['attributes'] = new Attribute($cell_attributes); + $variables[$section][$row_key]['cells'][$col_key]['content'] = $cell_content; + } + } + } + } + } + if (empty($variables['no_striping'])) { + $variables['attributes']['data-striping'] = 1; + } +} + +/** + * Prepares variables for tablesort indicator templates. + * + * Default template: tablesort-indicator.html.twig. + * + * @param array $variables + * An associative array containing: + * - style: Set to either 'asc' or 'desc'. This determines which icon to show. + */ +function template_preprocess_tablesort_indicator(&$variables) { + // Provide the image attributes for an ascending or descending image. + if ($variables['style'] == 'asc') { + $variables['arrow_asc'] = file_create_url('core/misc/arrow-asc.png'); + } + else { + $variables['arrow_desc'] = file_create_url('core/misc/arrow-desc.png'); + } +} + +/** + * Prepares variables for item list templates. + * + * Default template: item-list.html.twig. + * + * @param array $variables + * An associative array containing: + * - items: An array of items to be displayed in the list. Each item can be + * either a string or a render array. If #type, #theme, or #markup + * properties are not specified for child render arrays, they will be + * inherited from the parent list, allowing callers to specify larger + * nested lists without having to explicitly specify and repeat the + * render properties for all nested child lists. + * - title: A title to be prepended to the list. + * - list_type: The type of list to return (e.g. "ul", "ol"). + * + * @see http://drupal.org/node/1842756 + */ +function template_preprocess_item_list(&$variables) { + foreach ($variables['items'] as &$item) { + $attributes = array(); + // If the item value is an array, then it is a render array. + if (is_array($item)) { + // List items support attributes via the '#wrapper_attributes' property. + if (isset($item['#wrapper_attributes'])) { + $attributes = $item['#wrapper_attributes']; + } + // Determine whether there are any child elements in the item that are not + // fully-specified render arrays. If there are any, then the child + // elements present nested lists and we automatically inherit the render + // array properties of the current list to them. + foreach (Element::children($item) as $key) { + $child = &$item[$key]; + // If this child element does not specify how it can be rendered, then + // we need to inherit the render properties of the current list. + if (!isset($child['#type']) && !isset($child['#theme']) && !isset($child['#markup'])) { + // Since item-list.html.twig supports both strings and render arrays + // as items, the items of the nested list may have been specified as + // the child elements of the nested list, instead of #items. For + // convenience, we automatically move them into #items. + if (!isset($child['#items'])) { + // This is the same condition as in + // \Drupal\Core\Render\Element::children(), which cannot be used + // here, since it triggers an error on string values. + foreach ($child as $child_key => $child_value) { + if ($child_key[0] !== '#') { + $child['#items'][$child_key] = $child_value; + unset($child[$child_key]); + } + } + } + // Lastly, inherit the original theme variables of the current list. + $child['#theme'] = $variables['theme_hook_original']; + $child['#list_type'] = $variables['list_type']; + } + } + } + + // Set the item's value and attributes for the template. + $item = array( + 'value' => $item, + 'attributes' => new Attribute($attributes), + ); + } +} + +/** + * Prepares variables for feed icon templates. + * + * Default template: feed-icon.html.twig. + * + * @param array $variables + * An associative array containing: + * - url: An internal system path or a fully qualified external URL of the + * feed. + * - title: A descriptive title of the feed. + */ +function template_preprocess_feed_icon(&$variables) { + $text = t('Subscribe to !feed-title', array('!feed-title' => $variables['title'])); + $variables['icon'] = array( + '#theme' => 'image__feed_icon', + '#uri' => 'core/misc/feed.png', + '#width' => 16, + '#height' => 16, + '#alt' => $text, + ); + // Stripping tags because that's what l() used to do. + $variables['attributes']['title'] = strip_tags($text); +} + +/** + * Returns HTML for an indentation div; used for drag and drop tables. + * + * @param $variables + * An associative array containing: + * - size: Optional. The number of indentations to create. + * + * @ingroup themeable + */ +function theme_indentation($variables) { + $output = ''; + for ($n = 0; $n < $variables['size']; $n++) { + $output .= '
 
'; + } + return $output; +} + +/** + * Prepares variables for container templates. + * + * Default template: container.html.twig. + * + * @param array $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #id, #attributes, #children. + */ +function template_preprocess_container(&$variables) { + $variables['has_parent'] = FALSE; + $element = $variables['element']; + // Ensure #attributes is set. + $element += array('#attributes' => array()); + + // Special handling for form elements. + if (isset($element['#array_parents'])) { + // Assign an html ID. + if (!isset($element['#attributes']['id'])) { + $element['#attributes']['id'] = $element['#id']; + } + $variables['has_parent'] = TRUE; + } + + $variables['children'] = $element['#children']; + $variables['attributes'] = $element['#attributes']; +} + +/** + * Prepares variables for maintenance task list templates. + * + * Default template: maintenance-task-list.html.twig. + * + * @param array $variables + * An associative array containing: + * - items: An associative array of maintenance tasks. + * It's the caller's responsibility to ensure this array's items contain no + * dangerous HTML such as SCRIPT tags. + * - active: The key for the currently active maintenance task. + */ +function template_preprocess_maintenance_task_list(&$variables) { + $items = $variables['items']; + $active = $variables['active']; + + $done = isset($items[$active]) || $active == NULL; + foreach ($items as $k => $item) { + $variables['tasks'][$k]['item'] = $item; + $variables['tasks'][$k]['attributes'] = new Attribute(); + if ($active == $k) { + $variables['tasks'][$k]['attributes']->addClass('active'); + $variables['tasks'][$k]['status'] = t('active'); + $done = FALSE; + } + else { + if ($done) { + $variables['tasks'][$k]['attributes']->addClass('done'); + $variables['tasks'][$k]['status'] = t('done'); + } + } + } +} + +/** + * Adds a default set of helper variables for preprocessors and templates. + * + * This function is called for theme hooks implemented as templates only, not + * for theme hooks implemented as functions. This preprocess function is the + * first in the sequence of preprocessing functions that are called when + * preparing variables for a template. See _theme() for more details about the + * full sequence. + * + * @see _theme() + */ +function template_preprocess(&$variables, $hook, $info) { + // Tell all templates where they are located. + $variables['directory'] = \Drupal::theme()->getActiveTheme()->getPath(); + + // Merge in variables that don't depend on hook and don't change during a + // single page request. + // Use the advanced drupal_static() pattern, since this is called very often. + static $drupal_static_fast; + if (!isset($drupal_static_fast)) { + $drupal_static_fast['default_variables'] = &drupal_static(__FUNCTION__); + } + $default_variables = &$drupal_static_fast['default_variables']; + if (!isset($default_variables)) { + $default_variables = _template_preprocess_default_variables(); + } + $variables += $default_variables; + + // When theming a render element, merge its #attributes into + // $variables['attributes']. + if (isset($info['render element'])) { + $key = $info['render element']; + if (isset($variables[$key]['#attributes'])) { + $variables['attributes'] = NestedArray::mergeDeep($variables['attributes'], $variables[$key]['#attributes']); + } + } +} + +/** + * Returns hook-independent variables to template_preprocess(). + */ +function _template_preprocess_default_variables() { + // Variables that don't depend on a database connection. + $variables = array( + 'attributes' => array(), + 'title_attributes' => array(), + 'content_attributes' => array(), + 'title_prefix' => array(), + 'title_suffix' => array(), + 'db_is_active' => !defined('MAINTENANCE_MODE'), + 'is_admin' => FALSE, + 'logged_in' => FALSE, + ); + + // Give modules a chance to alter the default template variables. + \Drupal::moduleHandler()->alter('template_preprocess_default_variables', $variables); + + return $variables; +} + +/** + * Prepares variables for HTML document templates. + * + * Default template: html.html.twig. + * + * @param array $variables + * An associative array containing: + * - page: A render element representing the page. + */ +function template_preprocess_html(&$variables) { + $variables['page'] = $variables['html']['page']; + unset($variables['html']['page']); + $variables['page_top'] = NULL; + if (isset($variables['html']['page_top'])) { + $variables['page_top'] = $variables['html']['page_top']; + unset($variables['html']['page_top']); + } + $variables['page_bottom'] = NULL; + if (isset($variables['html']['page_bottom'])) { + $variables['page_bottom'] = $variables['html']['page_bottom']; + unset($variables['html']['page_bottom']); + } + + $variables['html_attributes'] = new Attribute(); + + // HTML element attributes. + $language_interface = \Drupal::languageManager()->getCurrentLanguage(); + $variables['html_attributes']['lang'] = $language_interface->getId(); + $variables['html_attributes']['dir'] = $language_interface->getDirection(); + + // Compile a list of classes that are going to be applied to the body element. + // This allows advanced theming based on context (home page, node of certain + // type, etc.). + if (isset($variables['db_is_active']) && !$variables['db_is_active']) { + $variables['attributes']['class'][] = 'db-offline'; + } + + // Add a variable for the root path. This can be used to create a class and + // theme the page depending on the current path (e.g. node, admin, user) as + // well as more specific data like path-frontpage. + $is_front_page = \Drupal::service('path.matcher')->isFrontPage(); + + if ($is_front_page) { + $variables['root_path'] = FALSE; + } + else { + $system_path = \Drupal::service('path.current')->getPath(); + $variables['root_path'] = explode('/', $system_path)[1]; + } + + $site_config = \Drupal::config('system.site'); + // Construct page title. + if (!empty($variables['page']['#title'])) { + $head_title = array( + 'title' => SafeMarkup::set(trim(strip_tags($variables['page']['#title']))), + 'name' => SafeMarkup::checkPlain($site_config->get('name')), + ); + } + // @todo Remove once views is not bypassing the view subscriber anymore. + // @see http://drupal.org/node/2068471 + elseif ($is_front_page) { + $head_title = array( + 'title' => t('Home'), + 'name' => SafeMarkup::checkPlain($site_config->get('name')), + ); + } + else { + $head_title = array('name' => SafeMarkup::checkPlain($site_config->get('name'))); + if ($site_config->get('slogan')) { + $head_title['slogan'] = strip_tags(Xss::filterAdmin($site_config->get('slogan'))); + } + } + + $variables['head_title_array'] = $head_title; + $output = ''; + $separator = ''; + foreach ($head_title as $item) { + $output .= $separator . SafeMarkup::escape($item); + $separator = ' | '; + } + $variables['head_title'] = SafeMarkup::set($output); + + // Collect all attachments. This must happen in the preprocess function for + // #type => html, to ensure that attachments added in #pre_render callbacks + // for #type => html are included. + $attached = $variables['html']['#attached']; + $attached = drupal_merge_attached($attached, $variables['page']['#attached']); + if (isset($variables['page_top'])) { + $attached = drupal_merge_attached($attached, $variables['page_top']['#attached']); + } + if (isset($variables['page_bottom'])) { + $attached = drupal_merge_attached($attached, $variables['page_bottom']['#attached']); + } + + // Render the attachments into HTML markup to be used directly in the template + // for #type => html: html.html.twig. + $all_attached = ['#attached' => $attached]; + $assets = AttachedAssets::createFromRenderArray($all_attached); + // Take Ajax page state into account, to allow for something like Turbolinks + // to be implemented without altering core. + // @see https://github.com/rails/turbolinks/ + $ajax_page_state = \Drupal::request()->request->get('ajax_page_state'); + $assets->setAlreadyLoadedLibraries(isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : []); + // Optimize CSS/JS if necessary, but only during normal site operation. + $optimize_css = !defined('MAINTENANCE_MODE') && \Drupal::config('system.performance')->get('css.preprocess'); + $optimize_js = !defined('MAINTENANCE_MODE') && \Drupal::config('system.performance')->get('js.preprocess'); + // Render the asset collections. + $asset_resolver = \Drupal::service('asset.resolver'); + $variables['styles'] = \Drupal::service('asset.css.collection_renderer')->render($asset_resolver->getCssAssets($assets, $optimize_css)); + list($js_assets_header, $js_assets_footer) = $asset_resolver->getJsAssets($assets, $optimize_js); + $js_collection_renderer = \Drupal::service('asset.js.collection_renderer'); + $variables['scripts'] = $js_collection_renderer->render($js_assets_header); + $variables['scripts_bottom'] = $js_collection_renderer->render($js_assets_footer); + + // Handle all non-asset attachments. + drupal_process_attached($all_attached); + $variables['head'] = drupal_get_html_head(FALSE); +} + +/** + * Prepares variables for the page template. + * + * Default template: page.html.twig. + * + * Most themes use their own copy of page.html.twig. The default is located + * inside "modules/system/page.html.twig". Look in there for the full list of + * variables. + */ +function template_preprocess_page(&$variables) { + $language_interface = \Drupal::languageManager()->getCurrentLanguage(); + $site_config = \Drupal::config('system.site'); + + // Move some variables to the top level for themer convenience and template cleanliness. + $variables['title'] = $variables['page']['#title']; + + foreach (system_region_list(\Drupal::theme()->getActiveTheme()->getName()) as $region_key => $region_name) { + if (!isset($variables['page'][$region_key])) { + $variables['page'][$region_key] = array(); + } + } + + $variables['base_path'] = base_path(); + $variables['front_page'] = \Drupal::url(''); + $variables['language'] = $language_interface; + $variables['logo'] = theme_get_setting('logo.url'); + $variables['site_name'] = (theme_get_setting('features.name') ? SafeMarkup::checkPlain($site_config->get('name')) : ''); + $variables['site_slogan'] = (theme_get_setting('features.slogan') ? Xss::filterAdmin($site_config->get('slogan')) : ''); + + // An exception might be thrown. + try { + $variables['is_front'] = \Drupal::service('path.matcher')->isFrontPage(); + } + catch (Exception $e) { + // If the database is not yet available, set default values for these + // variables. + $variables['is_front'] = FALSE; + $variables['db_is_active'] = FALSE; + } + if (!defined('MAINTENANCE_MODE')) { + $variables['action_links'] = menu_get_local_actions(); + $variables['tabs'] = menu_local_tabs(); + } + else { + $variables['action_links'] = array(); + $variables['tabs'] = array(); + } + + if ($node = \Drupal::routeMatch()->getParameter('node')) { + $variables['node'] = $node; + } +} + +/** + * Generate an array of suggestions from path arguments. + * + * This is typically called for adding to the suggestions in + * hook_theme_suggestions_HOOK_alter() or adding to 'attributes' class key + * variables from within preprocess functions, when wanting to base the + * additional suggestions or classes on the path of the current page. + * + * @param $args + * An array of path arguments. + * @param $base + * A string identifying the base 'thing' from which more specific suggestions + * are derived. For example, 'page' or 'html'. + * @param $delimiter + * The string used to delimit increasingly specific information. The default + * of '__' is appropriate for theme hook suggestions. '-' is appropriate for + * extra classes. + * + * @return + * An array of suggestions, suitable for adding to + * hook_theme_suggestions_HOOK_alter() or to $variables['attributes']['class'] + * if the suggestions represent extra CSS classes. + */ +function theme_get_suggestions($args, $base, $delimiter = '__') { + + // Build a list of suggested theme hooks in order of + // specificity. One suggestion is made for every element of the current path, + // though numeric elements are not carried to subsequent suggestions. For + // example, for $base='page', http://www.example.com/node/1/edit would result + // in the following suggestions: + // + // page__node + // page__node__% + // page__node__1 + // page__node__edit + + $suggestions = array(); + $prefix = $base; + foreach ($args as $arg) { + // Remove slashes or null per SA-CORE-2009-003 and change - (hyphen) to _ + // (underscore). + // + // When we discover templates in @see drupal_find_theme_templates, + // hyphens (-) are converted to underscores (_) before the theme hook + // is registered. We do this because the hyphens used for delimiters + // in hook suggestions cannot be used in the function names of the + // associated preprocess functions. Any page templates designed to be used + // on paths that contain a hyphen are also registered with these hyphens + // converted to underscores so here we must convert any hyphens in path + // arguments to underscores here before fetching theme hook suggestions + // to ensure the templates are appropriately recognized. + $arg = str_replace(array("/", "\\", "\0", '-'), array('', '', '', '_'), $arg); + // The percent acts as a wildcard for numeric arguments since + // asterisks are not valid filename characters on many filesystems. + if (is_numeric($arg)) { + $suggestions[] = $prefix . $delimiter . '%'; + } + $suggestions[] = $prefix . $delimiter . $arg; + if (!is_numeric($arg)) { + $prefix .= $delimiter . $arg; + } + } + if (\Drupal::service('path.matcher')->isFrontPage()) { + // Front templates should be based on root only, not prefixed arguments. + $suggestions[] = $base . $delimiter . 'front'; + } + + return $suggestions; +} + +/** + * Prepares variables for maintenance page templates. + * + * Default template: maintenance-page.html.twig. + * + * @param array $variables + * An associative array containing: + * - content - An array of page content. + * + * @see system_page_attachments() + */ +function template_preprocess_maintenance_page(&$variables) { + // @todo Rename the templates to page--maintenance + page--install. + template_preprocess_page($variables); + + // @see system_page_attachments() + $variables['#attached']['library'][] = 'core/normalize'; + $variables['#attached']['library'][] = 'system/maintenance'; +} + +/** + * Prepares variables for install page templates. + * + * Default template: install-page.html.twig. + * + * @param array $variables + * An associative array containing: + * - content - An array of page content. + * + * @see template_preprocess_maintenance_page() + */ +function template_preprocess_install_page(&$variables) { + template_preprocess_maintenance_page($variables); + + // Override the site name that is displayed on the page, since Drupal is + // still in the process of being installed. + $distribution_name = SafeMarkup::checkPlain(drupal_install_profile_distribution_name()); + $variables['site_name'] = $distribution_name; + $variables['head_title_array']['name'] = $distribution_name; + + $variables['head_title'] = implode(' | ', $variables['head_title_array']); +} + +/** + * Prepares variables for region templates. + * + * Default template: region.html.twig. + * + * Prepares the values passed to the theme_region function to be passed into a + * pluggable template engine. Uses the region name to generate a template file + * suggestions. + * + * @param array $variables + * An associative array containing: + * - elements: An associative array containing properties of the region. + */ +function template_preprocess_region(&$variables) { + // Create the $content variable that templates expect. + $variables['content'] = $variables['elements']['#children']; + $variables['region'] = $variables['elements']['#region']; +} + +/** + * Prepares variables for field templates. + * + * Default template: field.html.twig. + * + * @param array $variables + * An associative array containing: + * - element: A render element representing the field. + * - attributes: A string containing the attributes for the wrapping div. + * - title_attributes: A string containing the attributes for the title. + * - content_attributes: A string containing the attributes for the content's + * div. + */ +function template_preprocess_field(&$variables, $hook) { + $element = $variables['element']; + + // Creating variables for the template. + $variables['entity_type'] = $element['#entity_type']; + $variables['field_name'] = $element['#field_name']; + $variables['field_type'] = $element['#field_type']; + $variables['label_display'] = $element['#label_display']; + + $variables['label_hidden'] = ($element['#label_display'] == 'hidden'); + // Always set the field label - allow themes to decide whether to display it. + // In addition the label should be rendered but hidden to support screen + // readers. + $variables['label'] = SafeMarkup::checkPlain($element['#title']); + + static $default_attributes; + if (!isset($default_attributes)) { + $default_attributes = new Attribute; + } + + // We want other preprocess functions and the theme implementation to have + // fast access to the field item render arrays. The item render array keys + // (deltas) should always be numerically indexed starting from 0, and looping + // on those keys is faster than calling Element::children() or looping on all + // keys within $element, since that requires traversal of all element + // properties. + $variables['items'] = array(); + $delta = 0; + while (!empty($element[$delta])) { + $variables['items'][$delta]['content'] = $element[$delta]; + + // Modules (e.g., rdf.module) can add field item attributes (to + // $item->_attributes) within hook_entity_prepare_view(). Some field + // formatters move those attributes into some nested formatter-specific + // element in order have them rendered on the desired HTML element (e.g., on + // the element of a field item being rendered as a link). Other field + // formatters leave them within $element['#items'][$delta]['_attributes'] to + // be rendered on the item wrappers provided by field.html.twig. + $variables['items'][$delta]['attributes'] = !empty($element['#items'][$delta]->_attributes) ? new Attribute($element['#items'][$delta]->_attributes) : clone($default_attributes); + $delta++; + } +} + +/** + * Prepares variables for individual form element templates. + * + * Default template: field-multiple-value-form.html.twig. + * + * Combines multiple values into a table with drag-n-drop reordering. + * + * @param array $variables + * An associative array containing: + * - element: A render element representing the form element. + */ +function template_preprocess_field_multiple_value_form(&$variables) { + $element = $variables['element']; + $variables['multiple'] = $element['#cardinality_multiple']; + + if ($variables['multiple']) { + $table_id = Html::getUniqueId($element['#field_name'] . '_values'); + $order_class = $element['#field_name'] . '-delta-order'; + $header_attributes = new Attribute(array('class' => array('label'))); + if (!empty($element['#required'])) { + $header_attributes['class'][] = 'form-required'; + } + $header = array( + array( + 'data' => array( + '#prefix' => '', + 'title' => array( + '#markup' => t($element['#title']), + ), + '#suffix' => '', + ), + 'colspan' => 2, + 'class' => array('field-label'), + ), + t('Order', array(), array('context' => 'Sort order')), + ); + $rows = array(); + + // Sort items according to '_weight' (needed when the form comes back after + // preview or failed validation). + $items = array(); + $variables['button'] = array(); + foreach (Element::children($element) as $key) { + if ($key === 'add_more') { + $variables['button'] = &$element[$key]; + } + else { + $items[] = &$element[$key]; + } + } + usort($items, '_field_multiple_value_form_sort_helper'); + + // Add the items as table rows. + foreach ($items as $item) { + $item['_weight']['#attributes']['class'] = array($order_class); + + // Remove weight form element from item render array so it can be rendered + // in a separate table column. + $delta_element = $item['_weight']; + unset($item['_weight']); + + $cells = array( + array('data' => '', 'class' => array('field-multiple-drag')), + array('data' => $item), + array('data' => $delta_element, 'class' => array('delta-order')), + ); + $rows[] = array( + 'data' => $cells, + 'class' => array('draggable'), + ); + } + + $variables['table'] = array( + '#type' => 'table', + '#header' => $header, + '#rows' => $rows, + '#attributes' => array( + 'id' => $table_id, + 'class' => array('field-multiple-table'), + ), + '#tabledrag' => array( + array( + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => $order_class, + ), + ), + ); + + $variables['description'] = $element['#description']; + } + else { + $variables['elements'] = array(); + foreach (Element::children($element) as $key) { + $variables['elements'][] = $element[$key]; + } + } +} + +/** + * Prepares variables for breadcrumb templates. + * + * Default template: breadcrumb.html.twig. + * + * @param array $variables + * An associative array containing: + * - links: A list of \Drupal\Core\Link objects which should be rendered. + */ +function template_preprocess_breadcrumb(&$variables) { + $variables['breadcrumb'] = array(); + /** @var \Drupal\Core\Link $link */ + foreach ($variables['links'] as $key => $link) { + $variables['breadcrumb'][$key] = array('text' => $link->getText(), 'url' => $link->getUrl()->toString()); + } +} + +/** + * Callback for usort() within template_preprocess_field_multiple_value_form(). + * + * Sorts using ['_weight']['#value'] + */ +function _field_multiple_value_form_sort_helper($a, $b) { + $a_weight = (is_array($a) && isset($a['_weight']['#value']) ? $a['_weight']['#value'] : 0); + $b_weight = (is_array($b) && isset($b['_weight']['#value']) ? $b['_weight']['#value'] : 0); + return $a_weight - $b_weight; +} + +/** + * Provides theme registration for themes across .inc files. + */ +function drupal_common_theme() { + return array( + // From theme.inc. + 'html' => array( + 'render element' => 'html', + ), + 'page' => array( + 'render element' => 'page', + ), + 'region' => array( + 'render element' => 'elements', + ), + 'time' => array( + 'variables' => array('timestamp' => NULL, 'text' => NULL, 'attributes' => array(), 'html' => FALSE), + ), + 'datetime_form' => array( + 'render element' => 'element', + ), + 'datetime_wrapper' => array( + 'render element' => 'element', + ), + 'status_messages' => array( + 'variables' => ['status_headings' => [], 'message_list' => NULL], + ), + 'links' => array( + 'variables' => array('links' => array(), 'attributes' => array('class' => array('links')), 'heading' => array(), 'set_active_class' => FALSE), + ), + 'dropbutton_wrapper' => array( + 'variables' => array('children' => NULL), + ), + 'image' => array( + // HTML 4 and XHTML 1.0 always require an alt attribute. The HTML 5 draft + // allows the alt attribute to be omitted in some cases. Therefore, + // default the alt attribute to an empty string, but allow code calling + // _theme('image') to pass explicit NULL for it to be omitted. Usually, + // neither omission nor an empty string satisfies accessibility + // requirements, so it is strongly encouraged for code calling + // _theme('image') to pass a meaningful value for the alt variable. + // - http://www.w3.org/TR/REC-html40/struct/objects.html#h-13.8 + // - http://www.w3.org/TR/xhtml1/dtds.html + // - http://dev.w3.org/html5/spec/Overview.html#alt + // The title attribute is optional in all cases, so it is omitted by + // default. + 'variables' => array('uri' => NULL, 'width' => NULL, 'height' => NULL, 'alt' => '', 'title' => NULL, 'attributes' => array(), 'sizes' => NULL, 'srcset' => array(), 'style_name' => NULL), + ), + 'breadcrumb' => array( + 'variables' => array('links' => array()), + ), + 'table' => array( + 'variables' => array('header' => NULL, 'rows' => NULL, 'footer' => NULL, 'attributes' => array(), 'caption' => NULL, 'colgroups' => array(), 'sticky' => FALSE, 'responsive' => TRUE, 'empty' => ''), + ), + 'tablesort_indicator' => array( + 'variables' => array('style' => NULL), + ), + 'mark' => array( + 'variables' => array('status' => MARK_NEW), + ), + 'item_list' => array( + 'variables' => array('items' => array(), 'title' => '', 'list_type' => 'ul', 'attributes' => array(), 'empty' => NULL), + ), + 'feed_icon' => array( + 'variables' => array('url' => NULL, 'title' => NULL), + ), + 'progress_bar' => array( + 'variables' => array('label' => NULL, 'percent' => NULL, 'message' => NULL), + ), + 'indentation' => array( + 'variables' => array('size' => 1), + 'function' => 'theme_indentation', + ), + // From theme.maintenance.inc. + 'maintenance_page' => array( + 'render element' => 'page', + ), + 'install_page' => array( + 'render element' => 'page', + ), + 'maintenance_task_list' => array( + 'variables' => array('items' => NULL, 'active' => NULL, 'variant' => NULL), + ), + 'authorize_message' => array( + 'variables' => array('message' => NULL, 'success' => TRUE), + 'function' => 'theme_authorize_message', + 'path' => 'core/includes', + 'file' => 'theme.maintenance.inc', + ), + 'authorize_report' => array( + 'variables' => array('messages' => array()), + 'function' => 'theme_authorize_report', + 'path' => 'core/includes', + 'file' => 'theme.maintenance.inc', + ), + // From pager.inc. + 'pager' => array( + 'render element' => 'pager', + ), + // From menu.inc. + 'menu' => array( + 'variables' => array('items' => array(), 'attributes' => array()), + ), + 'menu_local_task' => array( + 'render element' => 'element', + ), + 'menu_local_action' => array( + 'render element' => 'element', + ), + 'menu_local_tasks' => array( + 'variables' => array('primary' => array(), 'secondary' => array()), + ), + // From form.inc. + 'input' => array( + 'render element' => 'element', + ), + 'select' => array( + 'render element' => 'element', + ), + 'fieldset' => array( + 'render element' => 'element', + ), + 'details' => array( + 'render element' => 'element', + ), + 'radios' => array( + 'render element' => 'element', + ), + 'checkboxes' => array( + 'render element' => 'element', + ), + 'form' => array( + 'render element' => 'element', + ), + 'textarea' => array( + 'render element' => 'element', + ), + 'form_element' => array( + 'render element' => 'element', + ), + 'form_element_label' => array( + 'render element' => 'element', + ), + 'vertical_tabs' => array( + 'render element' => 'element', + ), + 'container' => array( + 'render element' => 'element', + ), + // From field system. + 'field' => array( + 'render element' => 'element', + ), + 'field_multiple_value_form' => array( + 'render element' => 'element', + ), + ); +} diff --git a/core/lib/Drupal/Core/Field/WidgetBase.php b/core/lib/Drupal/Core/Field/WidgetBase.php index e333df3..fa5f1a3 100644 --- a/core/lib/Drupal/Core/Field/WidgetBase.php +++ b/core/lib/Drupal/Core/Field/WidgetBase.php @@ -127,9 +127,9 @@ public function form(FieldItemListInterface $items, array &$form, FormStateInter '#parents' => array_merge($parents, array($field_name . '_wrapper')), '#attributes' => array( 'class' => array( - 'field-type-' . Html::getClass($this->fieldDefinition->getType()), - 'field-name-' . Html::getClass($field_name), - 'field-widget-' . Html::getClass($this->getPluginId()), + 'field--type-' . Html::getClass($this->fieldDefinition->getType()), + 'field--name-' . Html::getClass($field_name), + 'field--widget-' . Html::getClass($this->getPluginId()), ), ), 'widget' => $elements, diff --git a/core/modules/image/src/Tests/ImageFieldDisplayTest.php b/core/modules/image/src/Tests/ImageFieldDisplayTest.php index c112437..f0f8612 100644 --- a/core/modules/image/src/Tests/ImageFieldDisplayTest.php +++ b/core/modules/image/src/Tests/ImageFieldDisplayTest.php @@ -332,7 +332,7 @@ function testImageFieldDefaultImage() { $this->drupalGet('node/' . $node->id()); // Verify that no image is displayed on the page by checking for the class // that would be used on the image field. - $this->assertNoPattern('
', 'No image displayed when no image is attached and no default image specified.'); + $this->assertNoPattern('
', 'No image displayed when no image is attached and no default image specified.'); $cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags'); $this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.'); diff --git a/core/modules/image/src/Tests/ImageFieldDisplayTest.php.orig b/core/modules/image/src/Tests/ImageFieldDisplayTest.php.orig new file mode 100644 index 0000000..c112437 --- /dev/null +++ b/core/modules/image/src/Tests/ImageFieldDisplayTest.php.orig @@ -0,0 +1,439 @@ +_testImageFieldFormatters('public'); + } + + /** + * Test image formatters on node display for private files. + */ + function testImageFieldFormattersPrivate() { + // Remove access content permission from anonymous users. + user_role_change_permissions(RoleInterface::ANONYMOUS_ID, array('access content' => FALSE)); + $this->_testImageFieldFormatters('private'); + } + + /** + * Test image formatters on node display. + */ + function _testImageFieldFormatters($scheme) { + $node_storage = $this->container->get('entity.manager')->getStorage('node'); + $field_name = strtolower($this->randomMachineName()); + $field_settings = array('alt_field_required' => 0); + $instance = $this->createImageField($field_name, 'article', array('uri_scheme' => $scheme), $field_settings); + + // Go to manage display page. + $this->drupalGet("admin/structure/types/manage/article/display"); + + // Test for existence of link to image styles configuration. + $this->drupalPostAjaxForm(NULL, array(), "{$field_name}_settings_edit"); + $this->assertLinkByHref(\Drupal::url('entity.image_style.collection'), 0, 'Link to image styles configuration is found'); + + // Remove 'administer image styles' permission from testing admin user. + $admin_user_roles = $this->adminUser->getRoles(TRUE); + user_role_change_permissions(reset($admin_user_roles), array('administer image styles' => FALSE)); + + // Go to manage display page again. + $this->drupalGet("admin/structure/types/manage/article/display"); + + // Test for absence of link to image styles configuration. + $this->drupalPostAjaxForm(NULL, array(), "{$field_name}_settings_edit"); + $this->assertNoLinkByHref(\Drupal::url('entity.image_style.collection'), 'Link to image styles configuration is absent when permissions are insufficient'); + + // Restore 'administer image styles' permission to testing admin user + user_role_change_permissions(reset($admin_user_roles), array('administer image styles' => TRUE)); + + // Create a new node with an image attached. + $test_image = current($this->drupalGetTestFiles('image')); + + // Ensure that preview works. + $this->previewNodeImage($test_image, $field_name, 'article'); + + // After previewing, make the alt field required. It cannot be required + // during preview because the form validation will fail. + $instance->settings['alt_field_required'] = 1; + $instance->save(); + + // Create alt text for the image. + $alt = $this->randomMachineName(); + + // Save node. + $nid = $this->uploadNodeImage($test_image, $field_name, 'article', $alt); + $node_storage->resetCache(array($nid)); + $node = $node_storage->load($nid); + + // Test that the default formatter is being used. + $image_uri = file_load($node->{$field_name}->target_id)->getFileUri(); + $image = array( + '#theme' => 'image', + '#uri' => $image_uri, + '#width' => 40, + '#height' => 20, + '#alt' => $alt, + ); + $default_output = str_replace("\n", NULL, drupal_render($image)); + $this->assertRaw($default_output, 'Default formatter displaying correctly on full node view.'); + + // Test the image linked to file formatter. + $display_options = array( + 'type' => 'image', + 'settings' => array('image_link' => 'file'), + ); + $display = entity_get_display('node', $node->getType(), 'default'); + $display->setComponent($field_name, $display_options) + ->save(); + + $image = array( + '#theme' => 'image', + '#uri' => $image_uri, + '#width' => 40, + '#height' => 20, + '#alt' => $alt, + ); + $default_output = '' . drupal_render($image) . ''; + $this->drupalGet('node/' . $nid); + $cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags'); + $this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.'); + $this->assertRaw($default_output, 'Image linked to file formatter displaying correctly on full node view.'); + // Verify that the image can be downloaded. + $this->assertEqual(file_get_contents($test_image->uri), $this->drupalGet(file_create_url($image_uri)), 'File was downloaded successfully.'); + if ($scheme == 'private') { + // Only verify HTTP headers when using private scheme and the headers are + // sent by Drupal. + $this->assertEqual($this->drupalGetHeader('Content-Type'), 'image/png', 'Content-Type header was sent.'); + $this->assertTrue(strstr($this->drupalGetHeader('Cache-Control'),'private') !== FALSE, 'Cache-Control header was sent.'); + + // Log out and try to access the file. + $this->drupalLogout(); + $this->drupalGet(file_create_url($image_uri)); + $this->assertResponse('403', 'Access denied to original image as anonymous user.'); + + // Log in again. + $this->drupalLogin($this->adminUser); + } + + // Test the image linked to content formatter. + $display_options['settings']['image_link'] = 'content'; + $display->setComponent($field_name, $display_options) + ->save(); + $image = array( + '#theme' => 'image', + '#uri' => $image_uri, + '#width' => 40, + '#height' => 20, + ); + $this->drupalGet('node/' . $nid); + $cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags'); + $this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.'); + $elements = $this->xpath( + '//a[@href=:path]/img[@src=:url and @alt=:alt and @width=:width and @height=:height]', + array( + ':path' => $node->url(), + ':url' => file_create_url($image['#uri']), + ':width' => $image['#width'], + ':height' => $image['#height'], + ':alt' => $alt, + ) + ); + $this->assertEqual(count($elements), 1, 'Image linked to content formatter displaying correctly on full node view.'); + + // Test the image style 'thumbnail' formatter. + $display_options['settings']['image_link'] = ''; + $display_options['settings']['image_style'] = 'thumbnail'; + $display->setComponent($field_name, $display_options) + ->save(); + + // Ensure the derivative image is generated so we do not have to deal with + // image style callback paths. + $this->drupalGet(ImageStyle::load('thumbnail')->buildUrl($image_uri)); + $image_style = array( + '#theme' => 'image_style', + '#uri' => $image_uri, + '#width' => 40, + '#height' => 20, + '#style_name' => 'thumbnail', + '#alt' => $alt, + ); + $default_output = drupal_render($image_style); + $this->drupalGet('node/' . $nid); + $cache_tags = explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags')); + $this->assertTrue(in_array('config:image.style.thumbnail', $cache_tags)); + $this->assertRaw($default_output, 'Image style thumbnail formatter displaying correctly on full node view.'); + + if ($scheme == 'private') { + // Log out and try to access the file. + $this->drupalLogout(); + $this->drupalGet(ImageStyle::load('thumbnail')->buildUrl($image_uri)); + $this->assertResponse('403', 'Access denied to image style thumbnail as anonymous user.'); + } + } + + /** + * Tests for image field settings. + */ + function testImageFieldSettings() { + $node_storage = $this->container->get('entity.manager')->getStorage('node'); + $test_image = current($this->drupalGetTestFiles('image')); + list(, $test_image_extension) = explode('.', $test_image->filename); + $field_name = strtolower($this->randomMachineName()); + $field_settings = array( + 'alt_field' => 1, + 'file_extensions' => $test_image_extension, + 'max_filesize' => '50 KB', + 'max_resolution' => '100x100', + 'min_resolution' => '10x10', + 'title_field' => 1, + ); + $widget_settings = array( + 'preview_image_style' => 'medium', + ); + $field = $this->createImageField($field_name, 'article', array(), $field_settings, $widget_settings); + + // Verify that the min/max resolution set on the field are properly + // extracted, and displayed, on the image field's configuration form. + $this->drupalGet('admin/structure/types/manage/article/fields/' . $field->id()); + $this->assertFieldByName('settings[max_resolution][x]', '100', 'Expected max resolution X value of 100.'); + $this->assertFieldByName('settings[max_resolution][y]', '100', 'Expected max resolution Y value of 100.'); + $this->assertFieldByName('settings[min_resolution][x]', '10', 'Expected min resolution X value of 10.'); + $this->assertFieldByName('settings[min_resolution][y]', '10', 'Expected min resolution Y value of 10.'); + + $this->drupalGet('node/add/article'); + $this->assertText(t('50 KB limit.'), 'Image widget max file size is displayed on article form.'); + $this->assertText(t('Allowed types: @extensions.', array('@extensions' => $test_image_extension)), 'Image widget allowed file types displayed on article form.'); + $this->assertText(t('Images must be larger than 10x10 pixels. Images larger than 100x100 pixels will be resized.'), 'Image widget allowed resolution displayed on article form.'); + + // We have to create the article first and then edit it because the alt + // and title fields do not display until the image has been attached. + + // Create alt text for the image. + $alt = $this->randomMachineName(); + + $nid = $this->uploadNodeImage($test_image, $field_name, 'article', $alt); + $this->drupalGet('node/' . $nid . '/edit'); + $this->assertFieldByName($field_name . '[0][alt]', '', 'Alt field displayed on article form.'); + $this->assertFieldByName($field_name . '[0][title]', '', 'Title field displayed on article form.'); + // Verify that the attached image is being previewed using the 'medium' + // style. + $node_storage->resetCache(array($nid)); + $node = $node_storage->load($nid); + $image_style = array( + '#theme' => 'image_style', + '#uri' => file_load($node->{$field_name}->target_id)->getFileUri(), + '#width' => 40, + '#height' => 20, + '#style_name' => 'medium', + ); + $default_output = drupal_render($image_style); + $this->assertRaw($default_output, "Preview image is displayed using 'medium' style."); + + // Add alt/title fields to the image and verify that they are displayed. + $image = array( + '#theme' => 'image', + '#uri' => file_load($node->{$field_name}->target_id)->getFileUri(), + '#alt' => $alt, + '#title' => $this->randomMachineName(), + '#width' => 40, + '#height' => 20, + ); + $edit = array( + $field_name . '[0][alt]' => $image['#alt'], + $field_name . '[0][title]' => $image['#title'], + ); + $this->drupalPostForm('node/' . $nid . '/edit', $edit, t('Save and keep published')); + $default_output = str_replace("\n", NULL, drupal_render($image)); + $this->assertRaw($default_output, 'Image displayed using user supplied alt and title attributes.'); + + // Verify that alt/title longer than allowed results in a validation error. + $test_size = 2000; + $edit = array( + $field_name . '[0][alt]' => $this->randomMachineName($test_size), + $field_name . '[0][title]' => $this->randomMachineName($test_size), + ); + $this->drupalPostForm('node/' . $nid . '/edit', $edit, t('Save and keep published')); + $schema = $field->getFieldStorageDefinition()->getSchema(); + $this->assertRaw(t('Alternative text cannot be longer than %max characters but is currently %length characters long.', array( + '%max' => $schema['columns']['alt']['length'], + '%length' => $test_size, + ))); + $this->assertRaw(t('Title cannot be longer than %max characters but is currently %length characters long.', array( + '%max' => $schema['columns']['title']['length'], + '%length' => $test_size, + ))); + + // Set cardinality to unlimited and add upload a second image. + // The image widget is extending on the file widget, but the image field + // type does not have the 'display_field' setting which is expected by + // the file widget. This resulted in notices before when cardinality is not + // 1, so we need to make sure the file widget prevents these notices by + // providing all settings, even if they are not used. + // @see FileWidget::formMultipleElements(). + $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.' . $field_name . '/storage', array('cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED), t('Save field settings')); + $edit = array( + 'files[' . $field_name . '_1][]' => drupal_realpath($test_image->uri), + ); + $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); + // Add the required alt text. + $this->drupalPostForm(NULL, [$field_name . '[1][alt]' => $alt], t('Save and keep published')); + $this->assertText(format_string('Article @title has been updated.', array('@title' => $node->getTitle()))); + + // Assert ImageWidget::process() calls FieldWidget::process(). + $this->drupalGet('node/' . $node->id() . '/edit'); + $edit = array( + 'files[' . $field_name . '_2][]' => drupal_realpath($test_image->uri), + ); + $this->drupalPostAjaxForm(NULL, $edit, $field_name . '_2_upload_button'); + $this->assertNoRaw(''); + $this->assertRaw(''); + } + + /** + * Test use of a default image with an image field. + */ + function testImageFieldDefaultImage() { + $node_storage = $this->container->get('entity.manager')->getStorage('node'); + // Create a new image field. + $field_name = strtolower($this->randomMachineName()); + $this->createImageField($field_name, 'article'); + + // Create a new node, with no images and verify that no images are + // displayed. + $node = $this->drupalCreateNode(array('type' => 'article')); + $this->drupalGet('node/' . $node->id()); + // Verify that no image is displayed on the page by checking for the class + // that would be used on the image field. + $this->assertNoPattern('
', 'No image displayed when no image is attached and no default image specified.'); + $cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags'); + $this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.'); + + // Add a default image to the public image field. + $images = $this->drupalGetTestFiles('image'); + $alt = $this->randomString(512); + $title = $this->randomString(1024); + $edit = array( + 'files[settings_default_image_uuid]' => drupal_realpath($images[0]->uri), + 'settings[default_image][alt]' => $alt, + 'settings[default_image][title]' => $title, + ); + $this->drupalPostForm("admin/structure/types/manage/article/fields/node.article.$field_name/storage", $edit, t('Save field settings')); + // Clear field definition cache so the new default image is detected. + \Drupal::entityManager()->clearCachedFieldDefinitions(); + $field_storage = FieldStorageConfig::loadByName('node', $field_name); + $default_image = $field_storage->getSetting('default_image'); + $file = \Drupal::entityManager()->loadEntityByUuid('file', $default_image['uuid']); + $this->assertTrue($file->isPermanent(), 'The default image status is permanent.'); + $image = array( + '#theme' => 'image', + '#uri' => $file->getFileUri(), + '#alt' => $alt, + '#title' => $title, + '#width' => 40, + '#height' => 20, + ); + $default_output = str_replace("\n", NULL, drupal_render($image)); + $this->drupalGet('node/' . $node->id()); + $cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags'); + $this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.'); + $this->assertRaw($default_output, 'Default image displayed when no user supplied image is present.'); + + // Create a node with an image attached and ensure that the default image + // is not displayed. + + // Create alt text for the image. + $alt = $this->randomMachineName(); + + $nid = $this->uploadNodeImage($images[1], $field_name, 'article', $alt); + $node_storage->resetCache(array($nid)); + $node = $node_storage->load($nid); + $image = array( + '#theme' => 'image', + '#uri' => file_load($node->{$field_name}->target_id)->getFileUri(), + '#width' => 40, + '#height' => 20, + '#alt' => $alt, + ); + $image_output = str_replace("\n", NULL, drupal_render($image)); + $this->drupalGet('node/' . $nid); + $cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags'); + $this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.'); + $this->assertNoRaw($default_output, 'Default image is not displayed when user supplied image is present.'); + $this->assertRaw($image_output, 'User supplied image is displayed.'); + + // Remove default image from the field and make sure it is no longer used. + $edit = array( + 'settings[default_image][uuid][fids]' => 0, + ); + $this->drupalPostForm("admin/structure/types/manage/article/fields/node.article.$field_name/storage", $edit, t('Save field settings')); + // Clear field definition cache so the new default image is detected. + \Drupal::entityManager()->clearCachedFieldDefinitions(); + $field_storage = FieldStorageConfig::loadByName('node', $field_name); + $default_image = $field_storage->getSetting('default_image'); + $this->assertFalse($default_image['uuid'], 'Default image removed from field.'); + // Create an image field that uses the private:// scheme and test that the + // default image works as expected. + $private_field_name = strtolower($this->randomMachineName()); + $this->createImageField($private_field_name, 'article', array('uri_scheme' => 'private')); + // Add a default image to the new field. + $edit = array( + 'files[settings_default_image_uuid]' => drupal_realpath($images[1]->uri), + 'settings[default_image][alt]' => $alt, + 'settings[default_image][title]' => $title, + ); + $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.' . $private_field_name . '/storage', $edit, t('Save field settings')); + // Clear field definition cache so the new default image is detected. + \Drupal::entityManager()->clearCachedFieldDefinitions(); + + $private_field_storage = FieldStorageConfig::loadByName('node', $private_field_name); + $default_image = $private_field_storage->getSetting('default_image'); + $file = \Drupal::entityManager()->loadEntityByUuid('file', $default_image['uuid']); + $this->assertEqual('private', file_uri_scheme($file->getFileUri()), 'Default image uses private:// scheme.'); + $this->assertTrue($file->isPermanent(), 'The default image status is permanent.'); + // Create a new node with no image attached and ensure that default private + // image is displayed. + $node = $this->drupalCreateNode(array('type' => 'article')); + $image = array( + '#theme' => 'image', + '#uri' => $file->getFileUri(), + '#alt' => $alt, + '#title' => $title, + '#width' => 40, + '#height' => 20, + ); + $default_output = str_replace("\n", NULL, drupal_render($image)); + $this->drupalGet('node/' . $node->id()); + $cache_tags_header = $this->drupalGetHeader('X-Drupal-Cache-Tags'); + $this->assertTrue(!preg_match('/ image_style\:/', $cache_tags_header), 'No image style cache tag found.'); + $this->assertRaw($default_output, 'Default private image displayed when no user supplied image is present.'); + } + +} diff --git a/core/modules/search/src/Tests/SearchExcerptTest.php b/core/modules/search/src/Tests/SearchExcerptTest.php index 6bea2cb..96d5489 100644 --- a/core/modules/search/src/Tests/SearchExcerptTest.php +++ b/core/modules/search/src/Tests/SearchExcerptTest.php @@ -69,7 +69,7 @@ function testSearchExcerpt() { // The node body that will produce this rendered $text is: // 123456789 HTMLTest +123456789+‘ +‘ +‘ +‘ +12345678    +‘ +‘ +‘ ‘ - $text = "

123456789 HTMLTest +123456789+‘ +‘ +‘ +‘ +12345678    +‘ +‘ +‘ ‘

\n
"; + $text = "

123456789 HTMLTest +123456789+‘ +‘ +‘ +‘ +12345678 +‘ +‘ +‘ ‘

\n
"; $result = search_excerpt('HTMLTest', $text); $this->assertFalse(empty($result), 'Rendered Multi-byte HTML encodings are not corrupted in search excerpts'); } diff --git a/core/modules/system/css/system.theme.css b/core/modules/system/css/system.theme.css index f7af1e4..7bc7147 100644 --- a/core/modules/system/css/system.theme.css +++ b/core/modules/system/css/system.theme.css @@ -559,19 +559,20 @@ ul.tabs { } /* Field display */ -.field .field-label { +.field__label { font-weight: bold; + vertical-align: top; } -.field-label-inline .field-label, -.field-label-inline .field-items { - float:left; /*LTR*/ - margin-right: 0.5em; /*LTR*/ +.field--label-inline .field__label, +.field--label-inline > .field__item, +.field--label-inline .field__items { + display: inline-block; + padding-right: 0.5em; } -[dir="rtl"] .field-label-inline .field-label, -[dir="rtl"] .field-label-inline .field-items { - float: right; - margin-left: 0.5em; - margin-right: 0; +[dir="rtl"] .field--label-inline .field__label, +[dir="rtl"] .field--label-inline .field__items { + padding-left: 0.5em; + padding-right: 0; } .field-label-inline .field-label::after { content: ':'; diff --git a/core/modules/system/css/system.theme.css.orig b/core/modules/system/css/system.theme.css.orig new file mode 100644 index 0000000..f7af1e4 --- /dev/null +++ b/core/modules/system/css/system.theme.css.orig @@ -0,0 +1,599 @@ +/** + * @file + * Basic styling for common markup. + */ + +/** + * Publishing status. + */ +.node--unpublished { + background-color: #fff4f4; +} + +/** + * Markup generated by tablesort-indicator.html.twig. + */ +th.active img { + display: inline; +} +td.active { + background-color: #ddd; +} + +/** + * Markup generated by item-list.html.twig. + */ +.item-list .title { + font-weight: bold; +} +.item-list ul { + margin: 0 0 0.75em 0; + padding: 0; +} +.item-list ul li { + margin: 0 0 0.25em 1.5em; /* LTR */ + padding: 0; +} +[dir="rtl"] .item-list ul li { + margin: 0 1.5em 0.25em 0; +} + +/** + * Markup generated by Form API. + */ +.form-item, +.form-actions { + margin-top: 1em; + margin-bottom: 1em; +} +tr.odd .form-item, +tr.even .form-item { + margin-top: 0; + margin-bottom: 0; +} +.form-composite > .fieldset-wrapper > .description, +.form-item .description { + font-size: 0.85em; +} +label.option { + display: inline; + font-weight: normal; +} +.form-composite > legend, +.label { + display:inline; + font-size: inherit; + font-weight: bold; + margin: 0; + padding: 0; +} +.form-checkboxes .form-item, +.form-radios .form-item { + margin-top: 0.4em; + margin-bottom: 0.4em; +} +.form-type-radio .description, +.form-type-checkbox .description { + margin-left: 2.4em; +} +.marker { + color: #e00; +} + +.form-required:after { + content: ''; + vertical-align: super; + display: inline-block; + /* Use a background image to prevent screen readers from announcing the text. */ + background-image: url(../../../misc/icons/ee0000/required.svg); + background-repeat: no-repeat; + background-size: 6px 6px; + width: 6px; + height: 6px; + margin: 0 0.3em; +} + +abbr.tabledrag-changed, +abbr.ajax-changed { + border-bottom: none; +} +.form-item input.error, +.form-item textarea.error, +.form-item select.error { + border: 2px solid red; +} +.button, +.image-button { + margin-left: 1em; + margin-right: 1em; +} +.button:first-child, +.image-button:first-child { + margin-left: 0; + margin-right: 0; +} + +/** + * Inline items. + */ +.container-inline label:after, +.container-inline .label:after { + content: ':'; +} +.form-type-radios .container-inline label:after { + content: ''; +} +.form-type-radios .container-inline .form-type-radio { + margin: 0 1em; +} +.container-inline .form-actions, +.container-inline.form-actions { + margin-top: 0; + margin-bottom: 0; +} + +/** + * Markup generated by #type 'more_link'. + */ +.more-link { + display: block; + text-align: right; /* LTR */ +} +[dir="rtl"] .more-link { + text-align: left; +} + +/** + * More help link style. + */ +.more-help-link { + text-align: right; /* LTR */ +} +[dir="rtl"] .more-help-link { + text-align: left; +} +.more-help-link a { + background: url(../../../misc/help.png) 0 50% no-repeat; /* LTR */ + padding: 1px 0 1px 20px; /* LTR */ +} +[dir="rtl"] .more-help-link a { + background-position: 100% 50%; + padding: 1px 20px 1px 0; +} + +/** + * Markup generated by pager.html.twig. + */ +.pager__items { + clear: both; + text-align: center; +} +.pager__item { + display: inline; + padding: 0.5em; +} +.pager__item.is-active { + font-weight: bold; +} + +/** + * Show buttons as links. + */ +button.link { + background: transparent; + border: 0; + cursor: pointer; + margin: 0; + padding: 0; + font-size: 1em; +} + +label button.link { + font-weight: bold; +} + +/** + * Collapsible details. + * + * @see collapse.js + * @thanks http://nicolasgallagher.com/css-background-image-hacks/ + */ +details { + border: 1px solid #ccc; + margin-top: 1em; + margin-bottom: 1em; +} +details > .details-wrapper { + padding: 0.5em 1.5em; +} +/* @todo Regression: The summary of uncollapsible details are no longer + vertically aligned with the .details-wrapper in browsers without native + details support. */ +summary { + cursor: pointer; + padding: 0.2em 0.5em; +} +.collapse-processed > summary { + padding-left: 0.5em; + padding-right: 0.5em; +} +.collapse-processed > summary:before { + background: url(../../../misc/menu-expanded.png) 0px 100% no-repeat; /* LTR */ + content: ""; + float: left; + height: 1em; + width: 1em; +} +[dir="rtl"] .collapse-processed > summary:before { + background-position: 100% 100%; + float: right; +} +.collapse-processed:not([open]) > summary:before { + background-position: 25% 35%; /* LTR */ + -ms-transform: rotate(-90deg); + -webkit-transform: rotate(-90deg); + transform: rotate(-90deg); +} +[dir="rtl"] .collapse-processed:not([open]) > summary:before { + background-position: 75% 35%; + -ms-transform: rotate(90deg); + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +/** + * TableDrag behavior. + * + * @see tabledrag.js + */ +tr.drag { + background-color: #fffff0; +} +tr.drag-previous { + background-color: #ffd; +} +body div.tabledrag-changed-warning { + margin-bottom: 0.5em; +} + +/** + * TableSelect behavior. + * + * @see tableselect.js + */ +tr.selected td { + background: #ffc; +} +td.checkbox, +th.checkbox { + text-align: center; +} + +/** + * Progress bar. + * + * @see progress.js + */ +.progress__track { + border-color: #b3b3b3; + border-radius: 10em; + background-color: #f2f1eb; + background-image: -webkit-linear-gradient(#e7e7df, #f0f0f0); + background-image: linear-gradient(#e7e7df, #f0f0f0); + box-shadow: inset 0 1px 3px hsla(0, 0%, 0%, 0.16); +} +.progress__bar { + border: 1px #07629a solid; + background: #057ec9; + background-image: + -webkit-linear-gradient( top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15) ), + -webkit-linear-gradient( left top, + #0094f0 0%, + #0094f0 25%, + #007ecc 25%, + #007ecc 50%, + #0094f0 50%, + #0094f0 75%, + #0094f0 100% ); + background-image: + -webkit-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15)), -webkit-linear-gradient(left top, #0094f0 0%, #0094f0 25%, #007ecc 25%, #007ecc 50%, #0094f0 50%, #0094f0 75%, #0094f0 100%); + background-image: + linear-gradient( to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15) ), + linear-gradient( to right bottom, + #0094f0 0%, + #0094f0 25%, + #007ecc 25%, + #007ecc 50%, + #0094f0 50%, + #0094f0 75%, + #0094f0 100% ); + background-size: 40px 40px; + margin-top: -1px; + margin-left: -1px; + padding: 0 1px; + height: 16px; + border-radius: 10em; + -webkit-animation: animate-stripes 3s linear infinite; + -moz-animation: animate-stripes 3s linear infinite; + -webkit-transition: width 0.5s ease-out; + transition: width 0.5s ease-out; +} + +/** + * Progress bar animations. + */ +@-webkit-keyframes animate-stripes { + 0% {background-position: 0 0, 0 0;} 100% {background-position: 0 0, -80px 0;} +} +@-ms-keyframes animate-stripes { + 0% {background-position: 0 0, 0 0;} 100% {background-position: 0 0, -80px 0;} +} +@keyframes animate-stripes { + 0% {background-position: 0 0, 0 0;} 100% {background-position: 0 0, -80px 0;} +} + +/** + * Markup generated by menu.html.twig. + */ +ul.menu { + list-style: none outside; + margin-left: 1em; /* LTR */ + padding: 0; + text-align: left; /* LTR */ +} +[dir="rtl"] ul.menu { + margin-left: 0; + margin-right: 1em; + text-align: right; +} +.menu-item--expanded { + list-style-image: url(../../../misc/menu-expanded.png); + list-style-type: circle; +} +.menu-item--collapsed { + list-style-image: url(../../../misc/menu-collapsed.png); /* LTR */ + list-style-type: disc; +} +[dir="rtl"] .menu-item--collapsed { + list-style-image: url(../../../misc/menu-collapsed-rtl.png); +} +.menu-item { + padding-top: 0.2em; + margin: 0; +} +ul.menu a.active { + color: #000; +} + +/** + * Markup generated by links.html.twig. + */ +ul.inline, +ul.links.inline { + display: inline; + padding-left: 0; +} +ul.inline li { + display: inline; + list-style-type: none; + padding: 0 0.5em; +} +ul.links a.active { + color: #000; +} + +/** + * Markup generated by breadcrumb.html.twig. + */ +.breadcrumb { + padding-bottom: 0.5em; +} +.breadcrumb ol { + margin: 0; + padding: 0; +} +[dir="rtl"] .breadcrumb ol { + /* This is required to win over specifity of [dir="rtl"] ol */ + margin-right: 0; +} +.breadcrumb li { + display: inline; + list-style-type: none; + margin: 0; + padding: 0; +} +/* IE8 does not support :not() and :last-child. */ +.breadcrumb li:before { + content: ' \BB '; +} +.breadcrumb li:first-child:before { + content: none; +} + +/** + * Markup generated by menu-local-tasks.html.twig. + */ +div.tabs { + margin: 1em 0; +} +ul.tabs { + list-style: none; + margin: 0 0 0.5em; + padding: 0; +} +.tabs > li { + display: inline-block; + margin-right: 0.3em; /* LTR */ +} +[dir="rtl"] .tabs > li { + margin-left: 0.3em; + margin-right: 0; +} +.tabs a { + display: block; + padding: 0.2em 1em; + text-decoration: none; +} +.tabs a.active { + background-color: #eee; +} +.tabs a:focus, +.tabs a:hover { + background-color: #f5f5f5; +} + +/** + * Styles for link buttons and action links. + */ +.action-links { + list-style: none; + padding: 0; + margin: 1em 0; +} +[dir="rtl"] .action-links { + /* This is required to win over specifity of [dir="rtl"] ul */ + margin-right: 0; +} +.action-links li { + display: inline-block; + margin: 0 0.3em; +} +.action-links li:first-child { + margin-left: 0; /* LTR */ +} +[dir="rtl"] .action-links li:first-child { + margin-left: 0.3em; + margin-right: 0; +} +.button-action { + display: inline-block; + line-height: 160%; + padding: 0.2em 0.5em 0.3em; + text-decoration: none; +} +.button-action:before { + content: '+'; + font-weight: 900; + margin-left: -0.1em; /* LTR */ + padding-right: 0.2em; /* LTR */ +} +[dir="rtl"] .button-action:before { + margin-left: 0; + margin-right: -0.1em; + padding-left: 0.2em; + padding-right: 0; +} + +/** + * Styles for system messages. + */ +.messages { + background: no-repeat 10px 17px; /* LTR */ + border: 1px solid; + border-width: 1px 1px 1px 0; /* LTR */ + border-radius: 2px; + padding: 15px 20px 15px 35px; /* LTR */ + word-wrap: break-word; + overflow-wrap: break-word; +} +[dir="rtl"] .messages { + border-width: 1px 0 1px 1px; + background-position: right 10px top 17px; + padding-left: 20px; + padding-right: 35px; + text-align: right; +} +.messages + .messages { + margin-top: 1.538em; +} +.messages__list { + list-style: none; + padding: 0; + margin: 0; +} +.messages__item + .messages__item { + margin-top: 0.769em; +} + +/* See .color-success in Seven's colors.css */ +.messages--status { + color: #325e1c; + background-color: #f3faef; + border-color: #c9e1bd #c9e1bd #c9e1bd transparent; /* LTR */ + background-image: url(../../../misc/icons/73b355/check.svg); + box-shadow: -8px 0 0 #77b259; /* LTR */ +} +[dir="rtl"] .messages--status { + border-color: #c9e1bd transparent #c9e1bd #c9e1bd; + box-shadow: 8px 0 0 #77b259; + margin-left: 0; +} + +/* See .color-warning in Seven's colors.css */ +.messages--warning { + background-color: #fdf8ed; + background-image: url(../../../misc/icons/e29700/warning.svg); + border-color: #f4daa6 #f4daa6 #f4daa6 transparent; /* LTR */ + color: #734c00; + box-shadow: -8px 0 0 #e09600; /* LTR */ +} +[dir="rtl"] .messages--warning { + border-color: #f4daa6 transparent #f4daa6 #f4daa6; + box-shadow: 8px 0 0 #e09600; +} + +/* See .color-error in Seven's colors.css */ +.messages--error { + background-color: #fcf4f2; + color: #a51b00; + background-image: url(../../../misc/icons/ea2800/error.svg); + border-color: #f9c9bf #f9c9bf #f9c9bf transparent; /* LTR */ + box-shadow: -8px 0 0 #e62600; /* LTR */ +} +[dir="rtl"] .messages--error { + border-color: #f9c9bf transparent #f9c9bf #f9c9bf; + box-shadow: 8px 0 0 #e62600; +} +.messages--error p.error { + color: #a51b00; +} + +/* Field display */ +.field .field-label { + font-weight: bold; +} +.field-label-inline .field-label, +.field-label-inline .field-items { + float:left; /*LTR*/ + margin-right: 0.5em; /*LTR*/ +} +[dir="rtl"] .field-label-inline .field-label, +[dir="rtl"] .field-label-inline .field-items { + float: right; + margin-left: 0.5em; + margin-right: 0; +} +.field-label-inline .field-label::after { + content: ':'; +} + +/* Form display */ +form .field-multiple-table { + margin: 0; +} +form .field-multiple-table .field-multiple-drag { + width: 30px; + padding-right: 0; /*LTR*/ +} +[dir="rtl"] form .field-multiple-table .field-multiple-drag { + padding-left: 0; +} +form .field-multiple-table .field-multiple-drag .tabledrag-handle { + padding-right: .5em; /*LTR*/ +} +[dir="rtl"] form .field-multiple-table .field-multiple-drag .tabledrag-handle { + padding-left: .5em; +} +form .field-add-more-submit { + margin: .5em 0 0; +} diff --git a/core/modules/system/templates/field.html.twig b/core/modules/system/templates/field.html.twig index 0745f84..f30f857 100644 --- a/core/modules/system/templates/field.html.twig +++ b/core/modules/system/templates/field.html.twig @@ -28,6 +28,9 @@ * - items: List of all the field items. Each item contains: * - attributes: List of HTML attributes for each item. * - content: The field item's content. + * - field_type: @todo: needs description + * - field_name: @todo: needs description + * - field_label: @todo: needs description * - entity_type: The entity type to which the field belongs. * - field_name: The name of the field. * - field_type: The type of the field. @@ -38,30 +41,54 @@ * @ingroup themeable */ #} -{% set field_name_class = field_name|clean_class %} -{% - set classes = [ - 'field', - 'field-' ~ entity_type|clean_class ~ '--' ~ field_name_class, - 'field-name-' ~ field_name_class, - 'field-type-' ~ field_type|clean_class, - 'field-label-' ~ label_display, - label_display == 'inline' ? 'clearfix', - ] -%} -{% - set title_classes = [ - 'field-label', - label_display == 'visually_hidden' ? 'visually-hidden', - ] -%} - - {% if not label_hidden %} - {{ label }}
- {% endif %} - +{% if multiple and not label_hidden %} + + {{ label }}:
+ {% for item in items %} - {{ item.content }}
+ {{ item.content }} {% endfor %} + - +{% elseif multiple and label_hidden %} + {% for item in items %} + {{ item.content }} + {% endfor %} +{% elseif not multiple and not label_hidden %} + +
{{ label }}:
+ {% for item in items %} +
{{ item.content }}
+ {% endfor %} + +{% elseif not multiple and label_hidden %} + + {% set field_name_class = field_name|clean_class %} + {% + set classes = [ + 'field', + 'field-' ~ entity_type|clean_class ~ '--' ~ field_name_class, + 'field-name-' ~ field_name_class, + 'field-type-' ~ field_type|clean_class, + 'field-label-' ~ label_display, + label_display == 'inline' ? 'clearfix', + ] + %} + {% + set title_classes = [ + 'field-label', + label_display == 'visually_hidden' ? 'visually-hidden', + ] + %} + + {% if not label_hidden %} + {{ label }} + {% endif %} + + {% for item in items %} + {{ item.content }} + {% endfor %} + + + +{% endif %} diff --git a/core/modules/system/templates/field.html.twig.orig b/core/modules/system/templates/field.html.twig.orig new file mode 100644 index 0000000..0745f84 --- /dev/null +++ b/core/modules/system/templates/field.html.twig.orig @@ -0,0 +1,67 @@ +{# +/** + * @file + * Default theme implementation for a field. + * + * To override output, copy the "field.html.twig" from the templates directory + * to your theme's directory and customize it, just like customizing other + * Drupal templates such as page.html.twig or node.html.twig. + * + * Instead of overriding the theming for all fields, you can also just override + * theming for a subset of fields using + * @link themeable Theme hook suggestions. @endlink For example, + * here are some theme hook suggestions that can be used for a field_foo field + * on an article node type: + * - field--node--field-foo--article.html.twig + * - field--node--field-foo.html.twig + * - field--node--article.html.twig + * - field--field-foo.html.twig + * - field--text-with-summary.html.twig + * - field.html.twig + * + * Available variables: + * - attributes: HTML attributes for the containing element. + * - label_hidden: Whether to show the field label or not. + * - title_attributes: HTML attributes for the title. + * - label: The label for the field. + * - content_attributes: HTML attributes for the content. + * - items: List of all the field items. Each item contains: + * - attributes: List of HTML attributes for each item. + * - content: The field item's content. + * - entity_type: The entity type to which the field belongs. + * - field_name: The name of the field. + * - field_type: The type of the field. + * - label_display: The display settings for the label. + * + * @see template_preprocess_field() + * + * @ingroup themeable + */ +#} +{% set field_name_class = field_name|clean_class %} +{% + set classes = [ + 'field', + 'field-' ~ entity_type|clean_class ~ '--' ~ field_name_class, + 'field-name-' ~ field_name_class, + 'field-type-' ~ field_type|clean_class, + 'field-label-' ~ label_display, + label_display == 'inline' ? 'clearfix', + ] +%} +{% + set title_classes = [ + 'field-label', + label_display == 'visually_hidden' ? 'visually-hidden', + ] +%} + + {% if not label_hidden %} + {{ label }} + {% endif %} + + {% for item in items %} + {{ item.content }} + {% endfor %} + +