/** * 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; }