/** * Extracts the hook name from a function name. * * @param string $string * The function name to extract the hook name from. * @param string $suffix * A suffix hook ending (like "alter") to also remove. * @param string $prefix * A prefix hook beginning (like "form") to also remove. * * @return string * The extracted hook name. */ public static function extractHook($string, $suffix = NULL, $prefix = NULL) { $regex = '^(' . implode('|', array_keys(Bootstrap::getTheme()->getAncestry())) . ')'; $regex .= $prefix ? '_' . $prefix : ''; $regex .= $suffix ? '_|_' . $suffix . '$' : ''; return preg_replace("/{$regex}/", '', $string); }
/** * {@inheritdoc} */ public function preprocessElement(Element $element, Variables $variables) { $link = $element->getProperty('link'); $link += ['localized_options' => []]; $link['localized_options']['set_active_class'] = TRUE; $icon = Bootstrap::glyphiconFromString($link['title']); $options = isset($link['localized_options']) ? $link['localized_options'] : []; if (isset($link['url'])) { // Turn link into a mini-button and colorize based on title. $class = Bootstrap::cssClassFromString($link['title'], 'default'); if (!isset($options['attributes']['class'])) { $options['attributes']['class'] = []; } $string = is_string($options['attributes']['class']); if ($string) { $options['attributes']['class'] = explode(' ', $options['attributes']['class']); } $options['attributes']['class'][] = 'btn'; $options['attributes']['class'][] = 'btn-xs'; $options['attributes']['class'][] = 'btn-' . $class; if ($string) { $options['attributes']['class'] = implode(' ', $options['attributes']['class']); } $variables['link'] = ['#type' => 'link', '#title' => SafeMarkup::format(\Drupal::service('renderer')->render($icon) . '@text', ['@text' => $link['title']]), '#options' => $options, '#url' => $link['url']]; } else { $variables['link'] = ['#type' => 'link', '#title' => $link['title'], '#options' => $options, '#url' => $link['url']]; } }
/** * {@inheritdoc} */ public static function preRenderElement(Element $element) { // Injects the icon into the title (the only way this is possible). if ($icon =& $element->getProperty('icon')) { $title = $element->getProperty('title'); // Handle #icon_only. if ($element->getProperty('icon_only')) { if ($attribute_title = $element->getAttribute('title', '')) { $title .= ' - ' . $attribute_title; } $element->setAttribute('title', $title)->addClass('icon-only')->setProperty('title', $icon); if (Bootstrap::getTheme()->getSetting('tooltip_enabled')) { $element->setAttribute('data-toggle', 'tooltip'); } return; } // Handle #icon_position. $position = $element->getProperty('icon_position', 'before'); // Render #icon and trim it (so it doesn't add underlines in whitespace). $rendered_icon = trim(Element::create($icon)->render()); // Default position is before. $markup = "{$rendered_icon}@title"; if ($position === 'after') { $markup = "@title{$rendered_icon}"; } // Replace the title and set an icon position class. $element->setProperty('title', new FormattableMarkup($markup, ['@title' => $title]))->addClass("icon-{$position}"); } }
/** * {@inheritdoc} */ public function preprocessElement(Variables $variables, $hook, array $info) { $variables->element->map(['id', 'name', 'value', 'type']); // Autocomplete. if ($route = $variables->element->getProperty('autocomplete_route_name')) { $variables['autocomplete'] = TRUE; // Use an icon for autocomplete "throbber". $icon = Bootstrap::glyphicon('refresh', ['#type' => 'html_tag', '#tag' => 'span', '#attributes' => ['class' => ['ajax-progress', 'ajax-progress-throbber', 'invisible']], 'throbber' => ['#type' => 'html_tag', '#tag' => 'span', '#attributes' => ['class' => ['throbber']]]]); $variables->element->setProperty('input_group', TRUE); $variables->element->setProperty('field_suffix', $icon); } // Create variables for #input_group and #input_group_button flags. $variables['input_group'] = $variables->element->getProperty('input_group') || $variables->element->getProperty('input_group_button'); if ($variables['input_group']) { $input_group_attributes = ['class' => ['input-group-' . ($variables->element->getProperty('input_group_button') ? 'btn' : 'addon')]]; if ($prefix = $variables->element->getProperty('field_prefix')) { $variables->element->setProperty('field_prefix', ['#type' => 'html_tag', '#tag' => 'span', '#attributes' => $input_group_attributes, '#value' => Element::create($prefix)->render(), '#weight' => -1]); } if ($suffix = $variables->element->getProperty('field_suffix')) { $variables->element->setProperty('field_suffix', ['#type' => 'html_tag', '#tag' => 'span', '#attributes' => $input_group_attributes, '#value' => Element::create($suffix)->render(), '#weight' => 1]); } } // Map the element properties. $variables->map(['attributes' => 'attributes', 'icon' => 'icon', 'field_prefix' => 'prefix', 'field_suffix' => 'suffix', 'type' => 'type']); // Ensure attributes are proper objects. $this->preprocessAttributes($variables, $hook, $info); }
/** * {@inheritdoc} */ protected function preprocessVariables(Variables $variables, $hook, array $info) { // Retrieve the ID, generating one if needed. $id = $variables->getAttribute('id', Html::getUniqueId($variables->offsetGet('id', 'bootstrap-carousel'))); unset($variables['id']); // Build slides. foreach ($variables->slides as $key => &$slide) { if (!isset($slide['attributes'])) { $slide['attributes'] = []; } $slide['attributes'] = new Attribute($slide['attributes']); } // Build controls. if ($variables->controls) { $left_icon = Bootstrap::glyphicon('chevron-left'); $right_icon = Bootstrap::glyphicon('chevron-right'); $url = Url::fromUserInput("#{$id}"); $variables->controls = ['left' => ['#type' => 'link', '#title' => new FormattableMarkup(Element::create($left_icon)->render() . '<span class="sr-only">@text</span>', ['@text' => t('Previous')]), '#url' => $url, '#attributes' => ['class' => ['left', 'carousel-control'], 'role' => 'button', 'data-slide' => 'prev']], 'right' => ['#type' => 'link', '#title' => new FormattableMarkup(Element::create($right_icon)->render() . '<span class="sr-only">@text</span>', ['@text' => t('Next')]), '#url' => $url, '#attributes' => ['class' => ['right', 'carousel-control'], 'role' => 'button', 'data-slide' => 'next']]]; } // Build indicators. if ($variables->indicators) { $variables->indicators = ['#theme' => 'item_list__bootstrap_carousel_indicators', '#list_type' => 'ol', '#items' => array_keys($variables->slides), '#target' => "#{$id}", '#start_index' => $variables->start_index]; } // Ensure all attributes are proper objects. $this->preprocessAttributes($variables, $hook, $info); }
/** * {@inheritdoc} */ public function __construct(array $configuration, $plugin_id, $plugin_definition) { if (!isset($configuration['theme'])) { $configuration['theme'] = Bootstrap::getTheme(); } $this->theme = $configuration['theme']; parent::__construct($configuration, $plugin_id, $plugin_definition); }
/** * Retrieves the currently selected theme on the settings form. * * @param array $form * Nested array of form elements that comprise the form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. * * @return \Drupal\bootstrap\Theme|FALSE * The currently selected theme object or FALSE if not a Bootstrap theme. */ public static function getTheme(array &$form, FormStateInterface $form_state) { $build_info = $form_state->getBuildInfo(); $theme = isset($build_info['args'][0]) ? Bootstrap::getTheme($build_info['args'][0]) : FALSE; // Do not continue if the theme is not Bootstrap specific. if (!$theme || !$theme->subthemeOf('bootstrap')) { unset($form['#submit'][0]); unset($form['#validate'][0]); } return $theme; }
/** * {@inheritdoc} */ public function __construct(array $configuration, $plugin_id, $plugin_definition) { // This is technically a plugin constructor, but because we wish to use the // protected methods of the Registry class, we must extend from it. Thus, // to properly construct the extended Registry object, we must pass the // arguments it would normally get from the service container to "fake" it. if (!isset($configuration['theme'])) { $configuration['theme'] = Bootstrap::getTheme(); } $this->currentTheme = $configuration['theme']; parent::__construct(\Drupal::service('app.root'), \Drupal::service('cache.default'), \Drupal::service('lock'), \Drupal::service('module_handler'), \Drupal::service('theme_handler'), \Drupal::service('theme.initialization'), $this->currentTheme->getName()); $this->setThemeManager(\Drupal::service('theme.manager')); $this->init(); }
/** * {@inheritdoc} */ public static function processElement(Element $element, FormStateInterface $form_state, array &$complete_form) { if (isset($element->format)) { $element->format->addClass('form-inline'); // Guidelines (removed). $element->format->guidelines->setProperty('access', FALSE); // Format (select). $element->format->format->addClass('input-sm')->setProperty('title_display', 'invisible')->setProperty('weight', -10); // Help (link). $element->format->help->about->setAttribute('title', t('Opens in new window'))->setProperty('icon', Bootstrap::glyphicon('question-sign')); if (Bootstrap::getTheme()->getSetting('tooltip_enabled')) { $element->format->help->about->setAttribute('data-toggle', 'tooltip'); } } }
/** * {@inheritdoc} */ public function alter(&$types, &$context1 = NULL, &$context2 = NULL) { // Sort the types for easier debugging. ksort($types, SORT_NATURAL); $process_manager = new ProcessManager($this->theme); $pre_render_manager = new PrerenderManager($this->theme); foreach (array_keys($types) as $type) { $element =& $types[$type]; // Ensure elements that have a base type with the #input set match. if (isset($element['#base_type']) && isset($types[$element['#base_type']]) && isset($types[$element['#base_type']]['#input'])) { $element['#input'] = $types[$element['#base_type']]['#input']; } // Core does not actually use the "description_display" property on the // "details" or "fieldset" element types because the positioning of the // description is never used in core templates. However, the form builder // automatically applies the value of "after", thus making it impossible // to detect a valid value later in the rendering process. It looks better // for the "details" and "fieldset" element types to display as "before". // @see \Drupal\Core\Form\FormBuilder::doBuildForm() if ($type === 'details' || $type === 'fieldset') { $element['#description_display'] = 'before'; $element['#panel_type'] = 'default'; } // Add extra variables to all elements. foreach (Bootstrap::extraVariables() as $key => $value) { if (!isset($variables["#{$key}"])) { $variables["#{$key}"] = $value; } } // Only continue if the type isn't "form" (as it messes up AJAX). if ($type !== 'form') { $regex = "/^{$type}/"; // Add necessary #process callbacks. $element['#process'][] = [get_class($process_manager), 'process']; $definitions = $process_manager->getDefinitionsLike($regex); foreach ($definitions as $definition) { Bootstrap::addCallback($element['#process'], [$definition['class'], 'process'], $definition['replace'], $definition['action']); } // Add necessary #pre_render callbacks. $element['#pre_render'][] = [get_class($pre_render_manager), 'preRender']; foreach ($pre_render_manager->getDefinitionsLike($regex) as $definition) { Bootstrap::addCallback($element['#pre_render'], [$definition['class'], 'preRender'], $definition['replace'], $definition['action']); } } } }
/** * Global #process callback for form elements. * * @param array $element * The element render array. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. * @param array $complete_form * The complete form structure. * * @return array * The altered element array. * * @see \Drupal\bootstrap\Plugin\Alter\ElementInfo::alter */ public static function process(array $element, FormStateInterface $form_state, array &$complete_form) { if (!empty($element['#bootstrap_ignore_process'])) { return $element; } static $theme; if (!isset($theme)) { $theme = Bootstrap::getTheme(); } $e = Element::create($element, $form_state); // Add "form-inline" class. if ($e->hasClass('container-inline')) { $e->replaceClass('container-inline', 'form-inline'); } if ($e->isType(['color', 'date', 'number', 'password', 'password_confirm', 'range', 'tel', 'weight'])) { $e->addClass('form-inline', 'wrapper_attributes'); } // Add "form-group" class, don't replace "form-wrapper" as that is needed // by some JavaScript for certain functionality to work. if ($e->hasClass('form-wrapper')) { $e->addClass('form-group'); } // Check for errors and set the "has_error" property flag. $errors = $e->getError(); $e->setProperty('errors', $errors); if (isset($errors) || $e->getProperty('required') && $theme->getSetting('forms_required_has_error')) { $e->setProperty('has_error', TRUE); } // Automatically inject the nearest button found after this element if // #input_group_button exists. if ($e->getProperty('input_group_button')) { // Obtain the parent array to limit search. $array_parents = $e->getProperty('array_parents', []); // Remove the current element from the array. array_pop($array_parents); // If element is nested, return the referenced parent from the form. // Otherwise return the complete form. $parent = Element::create($array_parents ? NestedArray::getValue($complete_form, $array_parents) : $complete_form, $form_state); // Find the closest button. if ($button = self::findButton($parent)) { $e->setProperty('field_suffix', $button->setIcon()->getArray()); $button->setProperty('access', FALSE); } } return $element; }
/** * {@inheritdoc} */ public function preprocessVariables(Variables $variables, $hook, array $info) { if (!empty($variables['description'])) { $variables['description'] = FieldFilteredMarkup::create($variables['description']); } $descriptions = []; $cardinality = $variables['cardinality']; if (isset($cardinality)) { if ($cardinality == -1) { $descriptions[] = t('Unlimited number of files can be uploaded to this field.'); } else { $descriptions[] = \Drupal::translation()->formatPlural($cardinality, 'One file only.', 'Maximum @count files.'); } } $upload_validators = $variables['upload_validators']; if (isset($upload_validators['file_validate_size'])) { $descriptions[] = t('@size limit.', ['@size' => format_size($upload_validators['file_validate_size'][0])]); } if (isset($upload_validators['file_validate_extensions'])) { $extensions = new FormattableMarkup('<code>@extensions</code>', ['@extensions' => implode(', ', explode(' ', $upload_validators['file_validate_extensions'][0]))]); $descriptions[] = t('Allowed types: @extensions.', ['@extensions' => $extensions]); } if (isset($upload_validators['file_validate_image_resolution'])) { $max = $upload_validators['file_validate_image_resolution'][0]; $min = $upload_validators['file_validate_image_resolution'][1]; if ($min && $max && $min == $max) { $descriptions[] = t('Images must be exactly <strong>@size</strong> pixels.', ['@size' => $max]); } elseif ($min && $max) { $descriptions[] = t('Images must be larger than <strong>@min</strong> pixels. Images larger than <strong>@max</strong> pixels will be resized.', ['@min' => $min, '@max' => $max]); } elseif ($min) { $descriptions[] = t('Images must be larger than <strong>@min</strong> pixels.', ['@min' => $min]); } elseif ($max) { $descriptions[] = t('Images larger than <strong>@max</strong> pixels will be resized.', ['@max' => $max]); } } $variables['descriptions'] = $descriptions; if ($descriptions) { $build = array(); $id = Html::getUniqueId('upload-instructions'); $build['toggle'] = ['#type' => 'link', '#title' => t('Upload requirements'), '#url' => Url::fromUserInput("#{$id}"), '#icon' => Bootstrap::glyphicon('question-sign'), '#attributes' => ['class' => ['icon-before'], 'data-toggle' => 'popover', 'data-html' => 'true', 'data-placement' => 'bottom', 'data-title' => t('Upload requirements')]]; $build['requirements'] = ['#type' => 'container', '#theme_wrappers' => ['container__file_upload_help'], '#attributes' => ['id' => $id, 'class' => ['hidden', 'help-block'], 'aria-hidden' => 'true']]; $build['requirements']['descriptions'] = ['#theme' => 'item_list__file_upload_help', '#items' => $descriptions]; $variables['popover'] = $build; } }
/** * {@inheritdoc} */ public function alterForm(array &$form, FormStateInterface $form_state, $form_id = NULL) { parent::alterForm($form, $form_state, $form_id); $update_manager = new UpdateManager($this->theme); $pending = $update_manager->getPendingUpdates(); if ($pending) { $form['update'] = ['#type' => 'details', '#title' => \Drupal::translation()->formatPlural(count($pending), 'Pending Update', 'Pending Updates'), '#panel_type' => 'primary', '#weight' => -20]; $rows = []; foreach ($pending as $version => $update) { $row = []; $row[] = $version; $row[] = new FormattableMarkup('<strong>@title</strong><p class="help-block">@description</p>', ['@title' => $update->getTitle(), '@description' => $update->getDescription()]); $rows[] = ['class' => [$update->getLevel() ?: 'default'], 'data' => $row]; } $form['update']['table'] = ['#type' => 'table', '#header' => [t('Update'), t('Description')], '#rows' => $rows]; $form['update']['update'] = ['#type' => 'submit', '#value' => t('Update @theme', ['@theme' => $this->theme->getTitle()]), '#icon' => Bootstrap::glyphicon('open'), '#attributes' => ['class' => ['btn-primary']], '#submit' => [[get_class($this), 'updateTheme']]]; } }
/** * {@inheritdoc} */ public function preprocessVariables(Variables $variables) { $region = $variables['elements']['#region']; $variables['region'] = $region; $variables['content'] = $variables['elements']['#children']; // Help region. if ($region === 'help' && !empty($variables['content'])) { $variables['content'] = ['icon' => Bootstrap::glyphicon('question-sign'), 'content' => ['#markup' => $variables['content']]]; $variables->addClass(['alert', 'alert-info', 'messages', 'info']); } // Support for "well" classes in regions. static $region_wells; if (!isset($region_wells)) { $region_wells = $this->theme->getSetting('region_wells'); } if (!empty($region_wells[$region])) { $variables->addClass($region_wells[$region]); } }
/** * {@inheritdoc} */ public function alterForm(array &$form, FormStateInterface $form_state, $form_id = NULL) { $e = Element::create($form, $form_state); $e->addClass(['form-inline', 'bg-info', 'text-center', 'clearfix']); // Backlink. $options = $e->backlink->getProperty('options', []); $e->backlink->addClass(isset($options['attributes']['class']) ? $options['attributes']['class'] : []); $e->backlink->addClass(['btn', 'btn-info', 'pull-left']); $e->backlink->setButtonSize(); $e->backlink->setIcon(Bootstrap::glyphicon('chevron-left')); // Ensure the UUID is set. if ($uuid = $e->uuid->getProperty('value')) { $options['query'] = ['uuid' => $uuid]; } // Override the options attributes. $options['attributes'] = $e->backlink->getAttributes()->getArrayCopy(); $e->backlink->setProperty('options', $options); // View mode. $e->view_mode->addClass('pull-right', $e::WRAPPER); }
/** * {@inheritdoc} */ public function preprocessVariables(Variables $variables, $hook, array $info) { $options = []; $file = $variables['file'] instanceof File ? $variables['file'] : File::load($variables['file']->fid); $url = file_create_url($file->getFileUri()); $file_size = $file->getSize(); $mime_type = $file->getMimeType(); // Set options as per anchor format described at // http://microformats.org/wiki/file-format-examples $options['attributes']['type'] = "{$mime_type}; length={$file_size}"; // Use the description as the link text if available. if (empty($variables['description'])) { $link_text = $file->getFilename(); } else { $link_text = $variables['description']; $options['attributes']['title'] = $file->getFilename(); } // Retrieve the generic mime type from core (mislabeled as "icon_class"). $generic_mime_type = file_icon_class($mime_type); // Map the generic mime types to an icon and state. $mime_map = ['application-x-executable' => ['label' => t('binary file'), 'icon' => 'console'], 'audio' => ['label' => t('audio file'), 'icon' => 'headphones'], 'image' => ['label' => t('image'), 'icon' => 'picture'], 'package-x-generic' => ['label' => t('archive'), 'icon' => 'compressed'], 'text' => ['label' => t('document'), 'icon' => 'file'], 'video' => ['label' => t('video'), 'icon' => 'film']]; // Retrieve the mime map array. $mime = isset($mime_map[$generic_mime_type]) ? $mime_map[$generic_mime_type] : ['label' => t('file'), 'icon' => 'file', 'state' => 'primary']; // Classes to add to the file field for icons. $variables->addClass(['file', 'file--mime-' . strtr($mime_type, ['/' => '-', '.' => '-']), 'file--' . $generic_mime_type]); // Set the icon for the mime type. $icon = Bootstrap::glyphicon($mime['icon']); $variables->icon = Element::create($icon)->addClass('text-primary')->getArray(); $options['attributes']['title'] = t('Open @mime in new window', ['@mime' => $mime['label']]); if ($this->theme->getSetting('tooltip_enabled')) { $options['attributes']['data-toggle'] = 'tooltip'; $options['attributes']['data-placement'] = 'bottom'; } $variables['link'] = Link::fromTextAndUrl($link_text, Url::fromUri($url, $options)); // Add the file size as a variable. $variables->file_size = format_size($file_size); // Preprocess attributes. $this->preprocessAttributes($variables, $hook, $info); }
/** * Batch 'finished' callback * * @param bool $success * A boolean indicating whether the batch has completed successfully. * @param array $results * The value(s) set in $context['results'] in * \Drupal\bootstrap\Plugin\Setting\Update::batchProcess(). * @param $operations * If $success is FALSE, contains the operations that remained unprocessed. */ public static function batchFinished($success, $results, $operations) { /** @type \Drupal\bootstrap\Theme $theme */ // Reconstruct the theme object this update is being applied to. $theme = Bootstrap::getTheme($results['theme_name']); // Save the current state of the installed schemas. $theme->setSetting('schemas', $results['schemas']); // Show successful updates. if (!empty($results['success'])) { $build = ['#theme' => 'item_list__theme_update', '#items' => $results['success'], '#context' => ['type' => 'success']]; drupal_set_message(new FormattableMarkup('@message' . \Drupal::service('renderer')->render($build), ['@message' => t('Successfully completed the following theme updates:')])); } // Show failed errors. if (!empty($results['errors'])) { $build = ['#theme' => 'item_list__theme_update', '#items' => $results['errors'], '#context' => ['type' => 'errors']]; drupal_set_message(new FormattableMarkup('@message' . \Drupal::service('renderer')->render($build), ['@message' => t('The following theme updates could not be completed:')]), 'error'); } }
/** * AJAX callback for reloading CDN provider elements. * * @param array $form * Nested array of form elements that comprise the form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. */ public static function ajaxCallback(array $form, FormStateInterface $form_state) { return $form['advanced']['cdn'][$form_state->getValue('cdn_provider', Bootstrap::getTheme()->getSetting('cdn_provider'))]; }
/** * Processes elements with AJAX properties. * * @param \Drupal\bootstrap\Utility\Element $element * The element object. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. * @param array $complete_form * The complete form structure. */ public static function processAjax(Element $element, FormStateInterface $form_state, array &$complete_form) { $ajax = $element->getProperty('ajax'); // Show throbber AJAX requests in an input button group. if (!isset($ajax['progress']['type']) || $ajax['progress']['type'] === 'throbber') { // Use an icon for autocomplete "throbber". $icon = Bootstrap::glyphicon('refresh'); $element->appendProperty('field_suffix', Element::create($icon)->addClass(['ajax-progress', 'ajax-progress-throbber'])); $element->setProperty('input_group', TRUE); } }
/** * {@inheritdoc} */ public function alter(&$cache, &$context1 = NULL, &$context2 = NULL) { // Sort the registry alphabetically (for easier debugging). ksort($cache); // Ensure paths to templates are set properly. This allows templates to // be moved around in a theme without having to constantly ensuring that // the theme's hook_theme() definitions have the correct static "path" set. foreach ($this->currentTheme->getAncestry() as $ancestor) { $current_theme = $ancestor->getName() === $this->currentTheme->getName(); $theme_path = $ancestor->getPath(); foreach ($ancestor->fileScan('/\\.html\\.twig$/', 'templates') as $file) { $hook = str_replace('-', '_', str_replace('.html.twig', '', $file->filename)); $path = dirname($file->uri); $incomplete = !isset($cache[$hook]) || strrpos($hook, '__'); if (!isset($cache[$hook])) { $cache[$hook] = []; } $cache[$hook]['path'] = $path; $cache[$hook]['type'] = $current_theme ? 'theme' : 'base_theme'; $cache[$hook]['theme path'] = $theme_path; if ($incomplete) { $cache[$hook]['incomplete preprocess functions'] = TRUE; } } } // Discover all the theme's preprocess plugins. $preprocess_manager = new PreprocessManager($this->currentTheme); $plugins = $preprocess_manager->getDefinitions(); ksort($plugins, SORT_NATURAL); // Iterate over the preprocess plugins. foreach ($plugins as $plugin_id => $definition) { $incomplete = !isset($cache[$plugin_id]) || strrpos($plugin_id, '__'); if (!isset($cache[$plugin_id])) { $cache[$plugin_id] = []; } array_walk($cache, function (&$info, $hook) use($plugin_id, $definition) { if ($hook === $plugin_id || strpos($hook, $plugin_id . '__') === 0) { if (!isset($info['preprocess functions'])) { $info['preprocess functions'] = []; } // Due to a limitation in \Drupal\Core\Theme\ThemeManager::render, // callbacks must be functions and not classes. We always specify // "bootstrap_preprocess" here and then assign the plugin ID to a // separate property that we can later intercept and properly invoke. // @todo Revisit if/when preprocess callbacks can be any callable. Bootstrap::addCallback($info['preprocess functions'], 'bootstrap_preprocess', $definition['replace'], $definition['action']); $info['preprocess functions'] = array_unique($info['preprocess functions']); $info['bootstrap preprocess'] = $plugin_id; } }); if ($incomplete) { $cache[$plugin_id]['incomplete preprocess functions'] = TRUE; } } // Allow core to post process. $this->postProcessExtension($cache, $this->theme); }
/** * Retrieves a setting for the current theme or for a given theme. * * @param string $name * The name of the setting to be retrieved. * @param string $theme * The name of a given theme; defaults to the currently active theme. * @param string $prefix * The prefix used on the $name of the setting, this will be appended with * "_" automatically if set. * * @return mixed * The value of the requested setting, NULL if the setting does not exist. * * @deprecated Will be removed in a future release. * * @code * // Before ("button_colorize" and "my_subtheme_custom_option"). * $colorize = bootstrap_setting('button_colorize', 'my_subtheme'); * $custom_option = bootstrap_setting('custom_option', 'my_subtheme', 'my_subtheme'); * * // After ("button_colorize" and "my_subtheme_custom_option"). * use Drupal\bootstrap\Bootstrap; * $my_subtheme = Bootstrap::getTheme('my_subtheme'); * $my_subtheme->getSetting('button_colorize'); * $my_subtheme->getSetting('my_subtheme_custom_option'); * @endcode * * @see \Drupal\bootstrap\Theme::getSetting() * @see \Drupal\bootstrap\Bootstrap::getTheme() */ function bootstrap_setting($name, $theme = NULL, $prefix = 'bootstrap') { Bootstrap::deprecated(); $theme = Bootstrap::getTheme($theme); $prefix = $prefix !== 'bootstrap' && !empty($prefix) ? $prefix . '_' : ''; return $theme->getSetting($prefix . $name); }
/** * Retrieves the full base/sub-theme ancestry of a theme. * * @param bool $reverse * Whether or not to return the array of themes in reverse order, where the * active theme is the first entry. * * @return \Drupal\bootstrap\Theme[] * An associative array of \Drupal\bootstrap objects (theme), keyed * by machine name. */ public function getAncestry($reverse = FALSE) { $ancestry = $this->themeHandler->getBaseThemes($this->themes, $this->getName()); foreach (array_keys($ancestry) as $name) { $ancestry[$name] = Bootstrap::getTheme($name, $this->themeHandler); } $ancestry[$this->getName()] = $this; return $reverse ? array_reverse($ancestry) : $ancestry; }
/** * {@inheritdoc} */ public function getTheme() { return Bootstrap::getTheme($this->pluginDefinition['provider']); }
/** * Converts an element description into a tooltip based on certain criteria. * * @param array|\Drupal\bootstrap\Utility\Element|NULL $target_element * The target element render array the tooltip is to be attached to, passed * by reference or an existing Element object. If not set, it will default * this Element instance. * @param bool $input_only * Toggle determining whether or not to only convert input elements. * @param int $length * The length of characters to determine if description is "simple". * * @return $this */ public function smartDescription(&$target_element = NULL, $input_only = TRUE, $length = NULL) { static $theme; if (!isset($theme)) { $theme = Bootstrap::getTheme(); } // Determine if tooltips are enabled. static $enabled; if (!isset($enabled)) { $enabled = $theme->getSetting('tooltip_enabled') && $theme->getSetting('forms_smart_descriptions'); } // Immediately return if tooltip descriptions are not enabled. if (!$enabled) { return $this; } // Allow a different element to attach the tooltip. /** @var Element $target */ if (is_object($target_element) && $target_element instanceof self) { $target = $target_element; } elseif (isset($target_element) && is_array($target_element)) { $target = new self($target_element, $this->formState); } else { $target = $this; } // Retrieve the length limit for smart descriptions. if (!isset($length)) { // Disable length checking by setting it to FALSE if empty. $length = (int) $theme->getSetting('forms_smart_descriptions_limit') ?: FALSE; } // Retrieve the allowed tags for smart descriptions. This is primarily used // for display purposes only (i.e. non-UI/UX related elements that wouldn't // require a user to "click", like a link). Disable length checking by // setting it to FALSE if empty. static $allowed_tags; if (!isset($allowed_tags)) { $allowed_tags = array_filter(array_unique(array_map('trim', explode(',', $theme->getSetting('forms_smart_descriptions_allowed_tags') . '')))) ?: FALSE; } // Return if element or target shouldn't have "simple" tooltip descriptions. $html = FALSE; if ($input_only && !$target->hasProperty('input') || !$this->getProperty('smart_description', TRUE) || !$target->getProperty('smart_description', TRUE) || !$this->hasProperty('description') || $target->hasAttribute('data-toggle') || !Unicode::isSimple($this->getProperty('description'), $length, $allowed_tags, $html)) { return $this; } // Default attributes type. $type = DrupalAttributes::ATTRIBUTES; // Use #label_attributes for 'checkbox' and 'radio' elements. if ($this->isType(['checkbox', 'radio'])) { $type = DrupalAttributes::LABEL; } elseif ($this->isType(['checkboxes', 'radios'])) { $type = DrupalAttributes::WRAPPER; } // Retrieve the proper attributes array. $attributes = $target->getAttributes($type); // Set the tooltip attributes. $attributes['title'] = $allowed_tags !== FALSE ? Xss::filter((string) $this->getProperty('description'), $allowed_tags) : $this->getProperty('description'); $attributes['data-toggle'] = 'tooltip'; if ($html || $allowed_tags === FALSE) { $attributes['data-html'] = 'true'; } // Remove the element description so it isn't (re-)rendered later. $this->unsetProperty('description'); return $this; }
/** * Preprocess theme hook variables. * * @param array $variables * The variables array, passed by reference. * @param string $hook * The name of the theme hook. * @param array $info * The theme hook info. */ public static function preprocess(array &$variables, $hook, array $info) { static $theme; if (!isset($theme)) { $theme = self::getTheme(); } static $preprocess_manager; if (!isset($preprocess_manager)) { $preprocess_manager = new PreprocessManager($theme); } // Ensure that any default theme hook variables exist. Due to how theme // hook suggestion alters work, the variables provided are from the // original theme hook, not the suggestion. if (isset($info['variables'])) { $variables = NestedArray::mergeDeepArray([$info['variables'], $variables], TRUE); } // Add extra variables to all theme hooks. foreach (Bootstrap::extraVariables() as $key => $value) { if (!isset($variables[$key])) { $variables[$key] = $value; } } // Add active theme context. // @see https://www.drupal.org/node/2630870 if (!isset($variables['theme'])) { $variables['theme'] = $theme->getInfo(); $variables['theme']['name'] = $theme->getName(); $variables['theme']['path'] = $theme->getPath(); $variables['theme']['title'] = $theme->getTitle(); $variables['theme']['settings'] = $theme->settings()->get(); $variables['theme']['has_glyphicons'] = $theme->hasGlyphicons(); $variables['theme']['query_string'] = \Drupal::getContainer()->get('state')->get('system.css_js_query_string') ?: '0'; } // Invoke necessary preprocess plugin. if (isset($info['bootstrap preprocess'])) { if ($preprocess_manager->hasDefinition($info['bootstrap preprocess'])) { $class = $preprocess_manager->createInstance($info['bootstrap preprocess'], ['theme' => $theme]); /** @var \Drupal\bootstrap\Plugin\Preprocess\PreprocessInterface $class */ $class->preprocess($variables, $hook, $info); } } }