/**
  * 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(Materialize::getTheme()->getAncestry())) . ')';
     $regex .= $prefix ? '_' . $prefix : '';
     $regex .= $suffix ? '_|_' . $suffix . '$' : '';
     return preg_replace("/{$regex}/", '', $string);
 }
 /**
  * {@inheritdoc}
  */
 public function __construct(array $configuration, $plugin_id, $plugin_definition)
 {
     if (!isset($configuration['theme'])) {
         $configuration['theme'] = Materialize::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\materialize\Theme|FALSE
  *   The currently selected theme object or FALSE if not a Materialize theme.
  */
 public static function getTheme(array &$form, FormStateInterface $form_state)
 {
     $build_info = $form_state->getBuildInfo();
     $theme = isset($build_info['args'][0]) ? Materialize::getTheme($build_info['args'][0]) : FALSE;
     // Do not continue if the theme is not Materialize specific.
     if (!$theme || !$theme->subthemeOf('materialize')) {
         unset($form['#submit'][0]);
         unset($form['#validate'][0]);
     }
     return $theme;
 }
 /**
  * {@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 (Materialize::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) {
                 Materialize::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) {
                 Materialize::addCallback($element['#pre_render'], [$definition['class'], 'preRender'], $definition['replace'], $definition['action']);
             }
         }
     }
 }
 /**
  * {@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',
     //      // Add a specific class for each and every mime type.
     //      'file--mime-' . strtr($mime_type, ['/' => '-', '.' => '-']),
     //      // Add a more general class for groups of well known mime types.
     //      'file--' . $generic_mime_type,
     //    ]);
     // Set the icon for the mime type.
     $icon = Materialize::material_icons_font($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);
 }
 /**
  * {@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' => Materialize::material_icons_font('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;
     }
 }
 /**
  * Processes elements with AJAX properties.
  *
  * @param \Drupal\materialize\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 = Materialize::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
                 // "materialize_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.
                 Materialize::addCallback($info['preprocess functions'], 'materialize_preprocess', $definition['replace'], $definition['action']);
                 $info['preprocess functions'] = array_unique($info['preprocess functions']);
                 $info['materialize preprocess'] = $plugin_id;
             }
         });
         if ($incomplete) {
             $cache[$plugin_id]['incomplete preprocess functions'] = TRUE;
         }
     }
     // Allow core to post process.
     $this->postProcessExtension($cache, $this->theme);
 }
 /**
  * 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 (Materialize::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();
     }
     // 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);
         }
     }
 }
 /**
  * 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] = Materialize::getTheme($name, $this->themeHandler);
     }
     $ancestry[$this->getName()] = $this;
     return $reverse ? array_reverse($ancestry) : $ancestry;
 }
 /**
  * Converts an element description into a tooltip based on certain criteria.
  *
  * @param array|\Drupal\materialize\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 = Materialize::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;
 }