diff --git a/metatag.api.php b/metatag.api.php index ee487e5..058a1c9 100644 --- a/metatag.api.php +++ b/metatag.api.php @@ -24,3 +24,18 @@ function hook_metatag_route_entity(\Drupal\Core\Routing\RouteMatchInterface $rou } } } + +/** + * Alter the metatags for pages that are not of content entities. + * + * @param array $metatags + * The special metatags to be added to the page. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity used for token replacements. + */ +function hook_metatags_alter(array &$metatags, \Drupal\Core\Entity\EntityInterface $entity) { + // Exclude metatags on frontpage. + if (\Drupal::service('path.matcher')->isFrontPage()) { + $metatags = NULL; + } +} diff --git a/metatag.module b/metatag.module index d16138c..7e6b56e 100644 --- a/metatag.module +++ b/metatag.module @@ -9,6 +9,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; @@ -103,7 +104,7 @@ function metatag_form_field_config_edit_form_alter(&$form, FormStateInterface $f */ function metatag_page_attachments(array &$attachments) { if (!metatag_is_current_route_supported()) { - return; + return NULL; } $metatag_attachments = &drupal_static('metatag_attachments'); @@ -112,7 +113,7 @@ function metatag_page_attachments(array &$attachments) { // Load the meta tags from the route. $metatag_attachments = metatag_get_tags_from_route(); if (!$metatag_attachments) { - return; + return NULL; } // If any Metatag items were found, append them. @@ -290,7 +291,7 @@ function metatag_get_route_entity() { */ function metatag_preprocess_html(&$variables) { if (!metatag_is_current_route_supported()) { - return; + return NULL; } $attachments = drupal_static('metatag_attachments'); @@ -299,7 +300,7 @@ function metatag_preprocess_html(&$variables) { } if (!$attachments) { - return; + return NULL; } // Load the page title. @@ -334,7 +335,7 @@ function metatag_get_tags_from_route() { // First, get defaults. $metatags = metatag_get_default_tags(); if (!$metatags) { - return; + return NULL; } // Then, set tag overrides for this particular entity. @@ -345,6 +346,12 @@ function metatag_get_tags_from_route() { } } + // Allow modules to override tags or the entity used for token replacements. + $context = [ + 'entity' => $entity, + ]; + \Drupal::service('module_handler')->alter('metatags', $metatags, $context); + return $metatag_manager->generateElements($metatags, $entity); } @@ -360,19 +367,11 @@ function metatag_get_default_tags() { // First we load global defaults. $metatags = $global_metatag_manager->load('global'); if (!$metatags) { - return; + return NULL; } // Check if this is a special page. - if (\Drupal::service('path.matcher')->isFrontPage()) { - $special_metatags = $global_metatag_manager->load('front'); - } - elseif (\Drupal::service('current_route_match')->getRouteName() == 'system.403') { - $special_metatags = $global_metatag_manager->load('403'); - } - elseif (\Drupal::service('current_route_match')->getRouteName() == 'system.404') { - $special_metatags = $global_metatag_manager->load('404'); - } + $special_metatags = \Drupal::service('metatag.manager')->getSpecialMetatags(); if (isset($special_metatags)) { $metatags->overwriteTags($special_metatags->get('tags')); } diff --git a/metatag_views/config/schema/metatag_views.views.schema.yml b/metatag_views/config/schema/metatag_views.views.schema.yml new file mode 100644 index 0000000..b98db8c --- /dev/null +++ b/metatag_views/config/schema/metatag_views.views.schema.yml @@ -0,0 +1,8 @@ +views.display_extender.metatag_display_extender: + type: views_display_extender + mapping: + metatags: + type: sequence + label: 'Metatags' + sequence: + type: metatag.metatag_tag.[%key] diff --git a/metatag_views/metatag_views.info.yml b/metatag_views/metatag_views.info.yml new file mode 100644 index 0000000..398ff2a --- /dev/null +++ b/metatag_views/metatag_views.info.yml @@ -0,0 +1,8 @@ +name: 'Metatag: Views' +type: module +description: Provides views integration for metatags. +core: 8.x +package: SEO +dependencies: + - metatag + - views diff --git a/metatag_views/metatag_views.install b/metatag_views/metatag_views.install new file mode 100644 index 0000000..5d57f86 --- /dev/null +++ b/metatag_views/metatag_views.install @@ -0,0 +1,29 @@ +getEditable('views.settings'); + $display_extenders = $config->get('display_extenders') ?: array(); + $display_extenders[] = 'metatag_display_extender'; + $config->set('display_extenders', $display_extenders); + $config->save(); +} + +/** + * Implements hook_uninstall(). + */ +function metatag_views_uninstall() { + // Disable metatag_display_extender plugin. + $config = \Drupal::service('config.factory')->getEditable('views.settings'); + $display_extenders = $config->get('display_extenders') ?: array(); + + $key = array_search('metatag_display_extender', $display_extenders); + if ($key !== FALSE) { + unset($display_extenders[$key]); + $config->set('display_extenders', $display_extenders); + $config->save(); + } +} \ No newline at end of file diff --git a/metatag_views/metatag_views.links.action.yml b/metatag_views/metatag_views.links.action.yml new file mode 100644 index 0000000..6c2d4c3 --- /dev/null +++ b/metatag_views/metatag_views.links.action.yml @@ -0,0 +1,5 @@ +metatag_views.metatags.add: + route_name: 'metatag_views.metatags.add' + title: 'Add views meta tags' + appears_on: + - metatag_views.metatags.list \ No newline at end of file diff --git a/metatag_views/metatag_views.links.task.yml b/metatag_views/metatag_views.links.task.yml new file mode 100644 index 0000000..4cb1030 --- /dev/null +++ b/metatag_views/metatag_views.links.task.yml @@ -0,0 +1,18 @@ +metatag_views.defaults: + title: 'Defaults' + route_name: entity.metatag_defaults.collection + base_route: entity.metatag_defaults.collection + weight: 0 +metatag_views.tab_views: + title: 'Views' + route_name: metatag_views.metatags.list + base_route: entity.metatag_defaults.collection + +metatag_views.metatags.edit: + title: 'Edit' + route_name: metatag_views.metatags.edit + base_route: metatag_views.metatags.edit +metatag_views.metatags.translate_overview: + title: 'Translate' + route_name: metatag_views.metatags.translate_overview + base_route: metatag_views.metatags.edit diff --git a/metatag_views/metatag_views.module b/metatag_views/metatag_views.module new file mode 100644 index 0000000..4a09b61 --- /dev/null +++ b/metatag_views/metatag_views.module @@ -0,0 +1,75 @@ +getExecutable(); + } + elseif (is_string($view)) { + $view = Views::getView($view); + } + if (!$view instanceof ViewExecutable) { + return; + } + $view->setDisplay($display_id); + + // And get the list of extenders for this display. + $extenders = $view->getDisplay()->getExtenders(); + if (!isset($extenders['metatag_display_extender'])) { + // If the id of the plugin is not in the list then something is wrong. + return; + } + + // Retrieve the metatag settings from the extender. + return $extenders['metatag_display_extender']->getMetatags(); +} + +/** + * Implements hook_metatags_alter(). + */ +function metatag_views_metatags_alter(&$metatags, $context) { + if (!$context['entity'] instanceof ViewEntityInterface) { + return; + } + + $view = $context['entity']->getExecutable(); + // If display_id is not available, will default to Master display. + $display_id = \Drupal::routeMatch()->getParameter('display_id'); + if ($tags = metatag_get_view_tags($view, $display_id)) { + // Apply view overrides. + $metatags = array_merge($metatags, $tags); + } +} + +/** + * Implements hook_metatag_route_entity(). + */ +function metatag_views_metatag_route_entity(RouteMatchInterface $route_match) { + if ($view_id = $route_match->getParameter('view_id')) { + $entity = \Drupal::entityTypeManager()->getStorage('view')->load($view_id); + return $entity; + } +} diff --git a/metatag_views/metatag_views.routing.yml b/metatag_views/metatag_views.routing.yml new file mode 100644 index 0000000..268cb48 --- /dev/null +++ b/metatag_views/metatag_views.routing.yml @@ -0,0 +1,62 @@ +metatag_views.metatags.list: + path: 'admin/config/search/metatag/views' + defaults: + _controller: '\Drupal\metatag_views\Controller\MetatagViewsController::listViews' + _title: 'Views metatags' + requirements: + _permission: 'administer meta tags' + options: + _admin_route: TRUE + +metatag_views.metatags.edit: + path: 'admin/config/search/metatag/views/{view_id}/{display_id}/edit' + defaults: + _form: '\Drupal\metatag_views\Form\MetatagViewsEditForm' + _title: 'Edit metatags for a view' + requirements: + _permission: 'administer meta tags' + options: + _admin_route: TRUE + +metatag_views.metatags.add: + path: 'admin/config/search/metatag/views/add' + defaults: + _form: '\Drupal\metatag_views\Form\MetatagViewsAddForm' + _title: 'Add metatags for a view' + requirements: + _permission: 'administer meta tags' + options: + _admin_route: TRUE + +metatag_views.metatags.revert: + path: 'admin/config/search/metatag/views/{view_id}/{display_id}/revert' + defaults: + _form: '\Drupal\metatag_views\Form\MetatagViewsRevertForm' + _title: 'Revert metatags for a view' + requirements: + _permission: 'administer meta tags' + options: + _admin_route: TRUE + +# Provide easy access to translate views metatags. +metatag_views.metatags.translate_overview: + path: 'admin/config/search/metatag/views/{view_id}/{display_id}/translate' + defaults: + _controller: '\Drupal\metatag_views\Controller\MetatagViewsTranslationController::itemPage' + _title: 'Translate metatags for a view' + requirements: + _permission: 'administer meta tags' + _module_dependencies: 'config_translation' + options: + _admin_route: TRUE + +metatag_views.metatags.translate: + path: 'admin/config/search/metatag/views/{view_id}/{display_id}/translate/{langcode}' + defaults: + _form: '\Drupal\metatag_views\Form\MetatagViewsTranslationForm' + _title: 'Translate metatags for a view' + requirements: + _permission: 'administer meta tags' + _module_dependencies: 'config_translation' + options: + _admin_route: TRUE diff --git a/metatag_views/src/Controller/MetatagViewsController.php b/metatag_views/src/Controller/MetatagViewsController.php new file mode 100644 index 0000000..aa052ef --- /dev/null +++ b/metatag_views/src/Controller/MetatagViewsController.php @@ -0,0 +1,219 @@ +viewStorage = $viewStorage; + $this->metatagManager = $metatagManager; + + // Generate the labels for views and displays. + $this->labels = $this->getViewsAndDisplaysLabels(); + } + + /** + * @inheritDoc + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager')->getStorage('view'), + $container->get('metatag.manager') + ); + } + + /** + * Get metatags for all of the views / displays that have them set. + * + * @return array + * List of tags grouped by view and display. + */ + public static function getTaggedViews() { + $tagged_views = []; + foreach (Views::getEnabledViews() as $view_id => $view) { + $displays = $view->get('display'); + foreach (array_keys($displays) as $display_id) { + if ($tags = metatag_get_view_tags($view_id, $display_id)) { + $tagged_views[$view_id][$display_id] = $tags; + } + } + } + return $tagged_views; + } + + /** + * Main controller function. Generates the renderable array for views + * metatags UI. + * + * @return array + */ + public function listViews() { + $elements = []; + + $elements['header'] = [ + '#markup' => '

' . t("To view a list of displays with meta tags set up, click on a view name. To view a summary of meta tags configuration for a particular display, click on the display name. If you need to set metatags for a specific view, choose Add views meta tags. Reverting the meta tags removes the specific configuration and falls back to defaults.") . '

', + ]; + + // Iterate over the values and build the whole UI. + // 1. Top level is a collapsible fieldset with a view name (details) + // 2. Inside each fieldset we have 2 columns -> Display and Operations. + // Display contains point 3. + // Operations contain edit and revert + // 3. In each display there is a table that has 2 columns: tag name and tag + // value. + $tagged_views = $this->getTaggedViews(); + foreach ($tagged_views as $view_id => $displays) { + $elements[$view_id] = [ + '#type' => 'details', + '#title' => $this->t($this->viewLabels[$view_id]['#label']), + 'details' => $this->buildViewDetails($view_id, $displays), + ]; + } + + return $elements; + } + + /** + * Builds the second "level" of the UI - table with display fieldset and operations. + * + * @param $view_id + * @param $displays + * @return array + */ + protected function buildViewDetails($view_id, $displays) { + $element = [ + '#type' => 'table', + '#collapsible' => TRUE, + '#header' => [$this->t('Display'), $this->t('Operations')], + ]; + + foreach ($displays as $display_id => $metatags) { + $metatags = array_filter($metatags); + + $element[$display_id]['details'] = [ + '#type' => 'details', + '#title' => $this->viewLabels[$view_id][$display_id], + ]; + + $params = ['view_id' => $view_id, 'display_id' => $display_id]; + + // Generate the operations. + $element[$display_id]['ops'] = [ + '#type' => 'operations', + '#links' => [ + 'edit' => [ + 'title' => t('Edit'), + 'url' => Url::fromRoute('metatag_views.metatags.edit', $params), + ], + 'translate' => [ + 'title' => t('Translate'), + 'url' => Url::fromRoute('metatag_views.metatags.translate_overview', $params), + ], + 'revert' => [ + 'title' => t('Revert'), + 'url' => Url::fromRoute('metatag_views.metatags.revert', $params), + ], + ], + ]; + + // Build the rows for each of the metatag types. + $element[$display_id]['details']['table'] = $this->buildDisplayDetailsTable($metatags); + } + + return $element; + } + + /** + * Build the table with metatags values summary. + * + * @param $tags + * @return array + */ + protected function buildDisplayDetailsTable($tags) { + $element = [ + '#type' => 'table', + ]; + + $i = 0; + foreach ($tags as $tag_name => $tag_value) { + // This is for the case where we have a subarray. + $tag_value = $this->prepareTagValue($tag_value); + if (!$tag_value) { + continue; + } + + $element[$i]['tag_name'] = [ + '#type' => 'markup', + '#markup' => $tag_name, + ]; + + $element[$i]['tag_value'] = [ + '#type' => 'markup', + '#markup' => $tag_value, + ]; + $i++; + } + + return $element; + } + + /** + * Massage the tag value. Returns an imploded string for metatags that + * are nested (ex. robots). + * + * @param $value + * @return string + */ + protected function prepareTagValue($value) { + if (is_array($value)) { + $value = implode(', ', array_filter($value)); + } + + return $value; + } + + /** + * Gets label values for the views and their displays. + */ + protected function getViewsAndDisplaysLabels() { + /** @var ViewEntityInterface[] $views */ + $views = $this->viewStorage->loadByProperties(['status' => 1]); + + $labels = []; + + foreach ($views as $view_id => $view) { + $displays = $view->get('display'); + $labels[$view_id]['#label'] = $view->label(); + foreach (array_keys($displays) as $display_id) { + $labels[$view_id][$display_id] = $displays[$display_id]['display_title']; + } + } + + $this->viewLabels = $labels; + } +} diff --git a/metatag_views/src/Controller/MetatagViewsTranslationController.php b/metatag_views/src/Controller/MetatagViewsTranslationController.php new file mode 100644 index 0000000..bf2ce5a --- /dev/null +++ b/metatag_views/src/Controller/MetatagViewsTranslationController.php @@ -0,0 +1,148 @@ +viewStorage = $viewStorage; + $this->metatagManager = $metatagManager; + $this->languageManager = $languageManager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager')->getStorage('view'), + $container->get('metatag.manager'), + $container->get('language_manager') + ); + } + + /** + * Language translations overview page for a views. + * + * @return array + * Page render array. + */ + public function itemPage() { + $view_id = \Drupal::request()->get('view_id'); + $display_id = \Drupal::request()->get('display_id'); + + $view = $this->viewStorage->load($view_id); + $original_langcode = $view->language()->getId(); + + $config_name = $view->getConfigDependencyName(); + $config_path = 'display.' . $display_id . '.display_options.display_extenders.metatag_display_extender.metatags'; + + $access_manager = \Drupal::service('access_manager'); + $configuration = \Drupal::service('config.factory')->get($config_name); + $config_source = $configuration->getOriginal($config_path, FALSE); + + $page['languages'] = [ + '#type' => 'table', + '#header' => [$this->t('Language'), $this->t('Operations')], + ]; + + $languages = $this->languageManager->getLanguages(); + foreach ($languages as $language) { + $langcode = $language->getId(); + $language_name = $language->getName(); + $operations = []; + + // Prepare the language name and the operations depending on whether this + // is the original language or not. + if ($langcode == $original_langcode) { + $language_name = '' . $this->t('@language (original)', [ + '@language' => $language_name, + ]) . ''; + + // Default language can only be edited, no add/delete. + $operations['edit'] = [ + 'title' => $this->t('Edit'), + 'url' => Url::fromRoute('metatag_views.metatags.edit', [ + 'view_id' => $view_id, + 'display_id' => $display_id, + ]), + ]; + } + else { + // Get the metatag translation for this language. + $config_translation = $this->languageManager + ->getLanguageConfigOverride($langcode, $config_name) + ->get($config_path); + + // If no translation exists for this language, link to add one. + if (!$config_translation || $config_translation == $config_source) { + $operations['add'] = [ + 'title' => $this->t('Add'), + 'url' => Url::fromRoute('metatag_views.metatags.translate', [ + 'view_id' => $view_id, + 'display_id' => $display_id, + 'langcode' => $langcode, + ]), + ]; + } + else { + // Otherwise, link to edit the existing translation. + $operations['edit'] = [ + 'title' => $this->t('Edit'), + 'url' => Url::fromRoute('metatag_views.metatags.translate', [ + 'view_id' => $view_id, + 'display_id' => $display_id, + 'langcode' => $langcode, + ]), + ]; + // @todo: operations delete. + } + } + + $page['languages'][$langcode]['language'] = [ + '#markup' => $language_name, + ]; + + $page['languages'][$langcode]['operations'] = [ + '#type' => 'operations', + '#links' => $operations, + // Even if the mapper contains multiple language codes, the source + // configuration can still be edited. + // '#access' => ($langcode == $original_langcode) || $operations_access, + ]; + } + + return $page; + } +} diff --git a/metatag_views/src/Form/MetatagViewsAddForm.php b/metatag_views/src/Form/MetatagViewsAddForm.php new file mode 100644 index 0000000..7e859aa --- /dev/null +++ b/metatag_views/src/Form/MetatagViewsAddForm.php @@ -0,0 +1,49 @@ + $displays) { + foreach (array_keys($displays) as $display_id) { + unset($views[$view_id][$view_id . ':' . $display_id]); + } + } + $views = array_filter($views); + + // Need to create that AFTER the $form['metatags'] as the whole form + // is passed to the $metatagManager->form() which causes duplicated field. + $form['view']['#type'] = 'select'; + $form['view']['#options'] = $views; + $form['view']['#empty_option'] = $this->t('- Select a view -'); + + return $form; + } + +} diff --git a/metatag_views/src/Form/MetatagViewsEditForm.php b/metatag_views/src/Form/MetatagViewsEditForm.php new file mode 100644 index 0000000..4398c23 --- /dev/null +++ b/metatag_views/src/Form/MetatagViewsEditForm.php @@ -0,0 +1,188 @@ +metatagManager = $metatag_manager; + $this->viewsManager = $entity_manager->getStorage('view'); + } + + public static function create(ContainerInterface $container) { + return new static( + $container->get('metatag.manager'), + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'metatag_views_edit_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + // Get the parameters from request. + $view_id = \Drupal::request()->get('view_id'); + $display_id = \Drupal::request()->get('display_id'); + + // Get metatags from the view entity. + $metatags = []; + if ($view_id && $display_id) { + $metatags = metatag_get_view_tags($view_id, $display_id); + } + + $form['metatags'] = $this->metatagManager->form($metatags, $form, ['view']); + $form['metatags']['#title'] = t('Metatags'); + $form['metatags']['#type'] = 'fieldset'; + + // Need to create that AFTER the $form['metatags'] as the whole form + // is passed to the $metatagManager->form() which causes duplicated field. + $form['view'] = [ + '#type' => 'value', + '#title' => $this->t('View'), + '#weight' => -100, + '#default_value' => $view_id . ':' . $display_id, + '#required' => TRUE, + ]; + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Submit'), + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function form(array $values, array $element, array $token_types = [], array $included_groups = NULL, array $included_tags = NULL) { + + // Add the outer fieldset. + $element += [ + '#type' => 'details', + ]; + + $element += $this->tokenService->tokenBrowser($token_types); + + $groups_and_tags = $this->sortedGroupsWithTags(); + + $first = TRUE; + foreach ($groups_and_tags as $group_id => $group) { + // Only act on groups that have tags and are in the list of included + // groups (unless that list is null). + if (isset($group['tags']) && (is_null($included_groups) || in_array($group_id, $included_groups))) { + // Create the fieldset. + $element[$group_id]['#type'] = 'details'; + $element[$group_id]['#title'] = $group['label']; + $element[$group_id]['#description'] = $group['description']; + $element[$group_id]['#open'] = $first; + $first = FALSE; + + foreach ($group['tags'] as $tag_id => $tag) { + // Only act on tags in the included tags list, unless that is null. + if (is_null($included_tags) || in_array($tag_id, $included_tags)) { + // Make an instance of the tag. + $tag = $this->tagPluginManager->createInstance($tag_id); + + // Set the value to the stored value, if any. + $tag_value = isset($values[$tag_id]) ? $values[$tag_id] : NULL; + $tag->setValue($tag_value); + + // Create the bit of form for this tag. + $element[$group_id][$tag_id] = $tag->form($element); + } + } + } + } + + return $element; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Get the submitted form values. + $view_name = $form_state->getValue('view'); + list($view_id, $display_id) = explode(':', $view_name); + + $metatags = $form_state->getValues(); + unset($metatags['view']); + $metatags = $this->clearMetatagViewsDisallowedValues($metatags); + + /** @var ViewEntityInterface $view */ + $view = $this->viewsManager->load($view_id); + + // Store the metatags on the view. + $config_name = $view->getConfigDependencyName(); + $config_path = 'display.' . $display_id . '.display_options.display_extenders.metatag_display_extender.metatags'; + + // Set configuration values based on form submission. + // This always edits the original language. + $configuration = $this->configFactory()->getEditable($config_name); + if (empty($this->removeEmptyTags($metatags))) { + $configuration->clear($config_path); + } + else { + $configuration->set($config_path, $metatags); + } + $configuration->save(); + + // Redirect back to the views list. + $form_state->setRedirect('metatag_views.metatags.list'); + + drupal_set_message($this->t('Metatags for @view : @display have been saved.', [ + '@view' => $view->label(), + '@display' => $view->getDisplay($display_id)['display_title'], + ])); + } + +} diff --git a/metatag_views/src/Form/MetatagViewsRevertForm.php b/metatag_views/src/Form/MetatagViewsRevertForm.php new file mode 100644 index 0000000..6f7af55 --- /dev/null +++ b/metatag_views/src/Form/MetatagViewsRevertForm.php @@ -0,0 +1,136 @@ +viewsManager = $entity_manager->getStorage('view'); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'metatag_views_revert_form'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->t('Do you want to revert metatags for @view_name : @display_name?', [ + '@view_name' => $this->view->label(), + '@display_name' => $this->view->getDisplay($this->display_id)['display_title'], + ]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('metatag_views.metatags.list'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->t('You are about to revert the custom metatags for the %display_name display on the %view_name view. This action cannot be undone.', [ + '%view_name' => $this->view->label(), + '%display_name' => $this->view->getDisplay($this->display_id)['display_title'], + ]); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Revert'); + } + + /** + * {@inheritdoc} + */ + public function getCancelText() { + return $this->t('Cancel'); + } + + /** + * {@inheritdoc} + * + * @param int $id + * (optional) The ID of the item to be deleted. + */ + public function buildForm(array $form, FormStateInterface $form_state, $view_id = NULL, $display_id = NUL) { + $this->view = $this->viewsManager->load($view_id); + $this->display_id = $display_id; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Removed metatags from the view. + $config_name = $this->view->getConfigDependencyName(); + $config_path = 'display.' . $this->display_id . '.display_options.display_extenders.metatag_display_extender.metatags'; + + $configuration = $this->configFactory()->getEditable($config_name) + ->clear($config_path) + ->save(); + + // Redirect back to the views list. + $form_state->setRedirect('metatag_views.metatags.list'); + + drupal_set_message($this->t('Reverted metatags for @view_name : @display_name', [ + '@view_name' => $this->view->label(), + '@display_name' => $this->view->getDisplay($this->display_id)['display_title'], + ])); + } + +} diff --git a/metatag_views/src/Form/MetatagViewsTranslationForm.php b/metatag_views/src/Form/MetatagViewsTranslationForm.php new file mode 100644 index 0000000..bdc8868 --- /dev/null +++ b/metatag_views/src/Form/MetatagViewsTranslationForm.php @@ -0,0 +1,248 @@ +metatagManager = $metatag_manager; + $this->viewsManager = $entity_manager->getStorage('view'); + $this->tokenService = $token; + $this->tagPluginManager = $tagPluginManager; + $this->languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('metatag.manager'), + $container->get('entity_type.manager'), + $container->get('metatag.token'), + $container->get('plugin.manager.metatag.tag'), + $container->get('language_manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'metatag_views_translate_form'; + } + + /** + * Gets the translated values while storing a copy of the original values. + */ + protected function prepareValues() { + $config_name = $this->view->getConfigDependencyName(); + $config_path = 'display.' . $this->display_id . '.display_options.display_extenders.metatag_display_extender.metatags'; + + $configuration = \Drupal::service('config.factory')->get($config_name); + $this->baseData = $configuration->getOriginal($config_path, FALSE); + + // Set the translation target language on the configuration factory. + $original_language = $this->languageManager->getConfigOverrideLanguage(); + $this->languageManager->setConfigOverrideLanguage($this->language); + + // Read in translated values. + $configuration = \Drupal::service('config.factory')->get($config_name); + $translated_values = $configuration->get($config_path); + + // Set the configuration language back. + $this->languageManager->setConfigOverrideLanguage($original_language); + + return $translated_values; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + // Get the parameters from request. + $this->view_id = \Drupal::request()->get('view_id'); + $this->display_id = \Drupal::request()->get('display_id'); + $langcode = \Drupal::request()->get('langcode'); + + $this->view = $this->viewsManager->load($this->view_id); + $this->language = $this->languageManager->getLanguage($langcode); + $this->sourceLanguage = $this->view->language(); + + // Get metatags from the view entity. + $form['#tree'] = TRUE; + $form['#attached']['library'][] = 'config_translation/drupal.config_translation.admin'; + + $form['#title'] = $this->t('Edit @language translation for %view: %display metatags', [ + '%view' => $this->view->label(), + '%display' => $this->view->getDisplay($this->display_id)['display_title'], + '@language' => $this->language->getName(), + ]); + + $form['metatags'] = $this->form($form, $this->prepareValues()); + $form['metatags']['#title'] = t('Metatags'); + $form['metatags']['#type'] = 'fieldset'; + + $form['submit'] = [ + '#type' => 'submit', + '#value' => t('Submit'), + ]; + + return $form; + } + + /** + * Add the translation form element for metatags available in the source. + */ + public function form(array $element, array $translated_values) { + $translated_values = $this->clearMetatagViewsDisallowedValues($translated_values); + // Only offer form elements for tags present in the source language. + $source_values = $this->removeEmptyTags($this->baseData); + + // Add the outer fieldset. + $element += [ + '#type' => 'details', + ]; + $element += $this->tokenService->tokenBrowser(['view']); + + foreach ($source_values as $tag_id => $value) { + $tag = $this->tagPluginManager->createInstance($tag_id); + $tag->setValue($translated_values[$tag_id]); + + $form_element = $tag->form($element); + $element[$tag_id] = [ + '#theme' => 'config_translation_manage_form_element', + 'source' => [ + '#type' => 'item', + '#title' => $form_element['#title'], + '#markup' => $value, + ], + 'translation' => $form_element, + ]; + } + + return $element; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Get the values of metatags. + $values = $form_state->getValue('metatags'); + $translated_values = array_combine(array_keys($values), array_column($values, 'translation')); + + $config_name = $this->view->getConfigDependencyName(); + $config_path = 'display.' . $this->display_id . '.display_options.display_extenders.metatag_display_extender.metatags'; + + // Set configuration values based on form submission and source values. + $base_config = $this->configFactory()->getEditable($config_name); + $config_translation = $this->languageManager->getLanguageConfigOverride($this->language->getId(), $config_name); + + // Save the configuration values, if they are different from the source + // values in the base configuration. Otherwise remove the override. + $source_values = $this->removeEmptyTags($base_config->get($config_path)); + if ($source_values !== $translated_values) { + $config_translation->set($config_path, $translated_values); + } + else { + $config_translation->clear($config_path); + } + + // If no overrides, delete language specific configuration file. + $saved_config = $config_translation->get(); + if (empty($saved_config)) { + $config_translation->delete(); + } + else { + $config_translation->save(); + } + + // Redirect back to the views list. + $form_state->setRedirect('metatag_views.metatags.translate_overview', [ + 'view_id' => $this->view_id, + 'display_id' => $this->display_id, + ]); + + drupal_set_message($this->t('Successfully updated @language translation.', [ + '@language' => $this->language->getName(), + ])); + } + +} diff --git a/metatag_views/src/MetatagViewsValuesCleanerTrait.php b/metatag_views/src/MetatagViewsValuesCleanerTrait.php new file mode 100644 index 0000000..4a1176a --- /dev/null +++ b/metatag_views/src/MetatagViewsValuesCleanerTrait.php @@ -0,0 +1,40 @@ +metatagManager->sortedTags(); + + // Return only common elements. + $metatags = array_intersect_key($metatags, $tags); + + return $metatags; + } + + public function removeEmptyTags($metatags) { + $metatags = array_filter($metatags, function($value) { + if (is_array($value)) { + return count(array_filter($value)) > 0; + } + else { + return $value !== ''; + } + }); + return $metatags; + } + +} diff --git a/metatag_views/src/Plugin/views/display_extender/MetatagDisplayExtender.php b/metatag_views/src/Plugin/views/display_extender/MetatagDisplayExtender.php new file mode 100644 index 0000000..deb6e5b --- /dev/null +++ b/metatag_views/src/Plugin/views/display_extender/MetatagDisplayExtender.php @@ -0,0 +1,130 @@ +get('section') == 'metatags') { + $form['#title'] .= t('The meta tags for this display'); + $metatags = $this->getMetatags(); + + // Build/inject the Metatag form. + $this->metatagManager = \Drupal::service('metatag.manager'); + $form['metatags'] = $this->metatagManager->form($metatags, $form, ['view']); + } + } + + /** + * Validate the options form. + */ + public function validateOptionsForm(&$form, FormStateInterface $form_state) { + } + + /** + * Handle any special handling on the validate form. + */ + public function submitOptionsForm(&$form, FormStateInterface $form_state) { + if ($form_state->get('section') == 'metatags') { + $metatags = $form_state->getValues(); + // Remove the unnecessary elements from values array. + $metatags = $this->clearMetatagViewsDisallowedValues($metatags); + $this->options['metatags'] = $metatags; + } + } + + /** + * Set up any variables on the view prior to execution. + */ + public function preExecute() { + } + + /** + * Inject anything into the query that the display_extender handler needs. + */ + public function query() { + } + + /** + * Provide the default summary for options in the views UI. + * + * This output is returned as an array. + */ + public function optionsSummary(&$categories, &$options) { + $categories['metatags'] = array( + 'title' => t('Meta tags'), + 'column' => 'second', + ); + $options['metatags'] = array( + 'category' => 'metatags', + 'title' => t('Meta tags'), + 'value' => $this->hasMetatags() ? t('Overridden') : t('Using defaults'), + ); + } + + /** + * Static member function to list which sections are defaultable + * and what items each section contains. + */ + public function defaultableSections(&$sections, $section = NULL) { + } + + /** + * Identify whether or not the current display has custom meta tags defined. + */ + protected function hasMetatags() { + $metatags = $this->getMetatags(); + return !empty($metatags); + + } + + /** + * Get the Metatag configuration for this display. + * + * @return array + * The meta tag values. + */ + public function getMetatags() { + $metatags = array(); + + if (!empty($this->options['metatags'])) { + $metatags = $this->options['metatags']; + } + + return $metatags; + } + + public function setMetatags($metatags) { + $this->options['metatags'] = $metatags; + } +} diff --git a/metatag_views/templates/config_translation_manage_form_element.html.twig b/metatag_views/templates/config_translation_manage_form_element.html.twig new file mode 100644 index 0000000..4c1459b --- /dev/null +++ b/metatag_views/templates/config_translation_manage_form_element.html.twig @@ -0,0 +1,23 @@ +{# +/** + * @file + * Default theme implementation for a form element in config_translation. + * + * Available variables: + * - element: Array that represents the element shown in the form. + * - source: The source of the translation. + * - translation: The translation for the target language. + * + * @see template_preprocess() + * + * @ingroup themeable + */ +#} +
+
+ {{ element.source }} +
+
+ {{ element.translation }} +
+
diff --git a/metatag_views/tests/src/Functional/MetatagViewsBasicsTest.php b/metatag_views/tests/src/Functional/MetatagViewsBasicsTest.php new file mode 100644 index 0000000..9e2e700 --- /dev/null +++ b/metatag_views/tests/src/Functional/MetatagViewsBasicsTest.php @@ -0,0 +1,69 @@ +install([$theme]); + \Drupal::service('theme_handler')->setDefault($theme); + + // Place the local actions block in the theme so that we can assert the + // presence of local actions and such. + $this->drupalPlaceBlock('local_actions_block', [ + 'region' => 'content', + 'theme' => $theme, + ]); + } + + /** + * Confirm the site isn't broken. + */ + public function testSiteStillWorks() { + // Load the front page. + $this->drupalGet(''); + $this->assertResponse(200); + + // With nothing else configured the front page just has a login form. + $this->assertText('Enter your Drupal username.'); + } + +} diff --git a/src/MetatagManager.php b/src/MetatagManager.php index 7990e8b..170f33d 100644 --- a/src/MetatagManager.php +++ b/src/MetatagManager.php @@ -9,6 +9,7 @@ use Drupal\Core\Logger\LoggerChannelFactoryInterface; use Drupal\field\Entity\FieldConfig; use Drupal\metatag\Entity\MetatagDefaults; +use Drupal\views\ViewEntityInterface; /** * Class MetatagManager. @@ -271,8 +272,8 @@ protected function getFields(ContentEntityInterface $entity) { /** * Returns a list of the metatags with values from a field. * - * @param $entity - * @param $field_name + * @param ContentEntityInterface $entity + * @param string $field_name */ protected function getFieldTags(ContentEntityInterface $entity, $field_name) { $tags = []; @@ -288,6 +289,93 @@ protected function getFieldTags(ContentEntityInterface $entity, $field_name) { } /** + * + * + * @param ContentEntityInterface $entity + */ + public function getDefaultMetatags(ContentEntityInterface $entity = NULL) { + // Get general global metatags + $metatags = $this->getGlobalMetatags(); + // If that is empty something went wrong. + if (!$metatags) { + return; + } + + // Check if this is a special page. + $special_metatags = $this->getSpecialMetatags(); + + // Merge with all globals defaults. + if ($special_metatags) { + $metatags->set('tags', array_merge($metatags->get('tags'), $special_metatags->get('tags'))); + } + + // Next check if there is this page is an entity that has meta tags. + // @TODO: Think about using other defaults, e.g. views. Maybe use plugins? + else { + if (is_null($entity)) { + $entity = metatag_get_route_entity(); + } + + if (!empty($entity)) { + // Get default metatags for a given entity. + $entity_defaults = $this->getEntityDefaultMetatags($entity); + if ($entity_defaults != NULL) { + $metatags->set('tags', array_merge($metatags->get('tags'), $entity_defaults)); + } + } + } + + return $metatags->get('tags'); + } + + /** + * + */ + public function getGlobalMetatags() { + return $this->metatagDefaults->load('global'); + } + + /** + * + */ + public function getSpecialMetatags() { + $metatags = NULL; + + if (\Drupal::service('path.matcher')->isFrontPage()) { + $metatags = $this->metatagDefaults->load('front'); + } + elseif (\Drupal::service('current_route_match')->getRouteName() == 'system.403') { + $metatags = $this->metatagDefaults->load('403'); + } + elseif (\Drupal::service('current_route_match')->getRouteName() == 'system.404') { + $metatags = $this->metatagDefaults->load('404'); + } + + return $metatags; + } + + /** + * + */ + public function getEntityDefaultMetatags(ContentEntityInterface $entity) { + $entity_metatags = $this->metatagDefaults->load($entity->getEntityTypeId()); + $metatags = []; + if ($entity_metatags != NULL) { + // Merge with global defaults. + $metatags = array_merge($metatags, $entity_metatags->get('tags')); + } + + // Finally, check if we should apply bundle overrides. + $bundle_metatags = $this->metatagDefaults->load($entity->getEntityTypeId() . '__' . $entity->bundle()); + if ($bundle_metatags != NULL) { + // Merge with existing defaults. + $metatags = array_merge($metatags, $bundle_metatags->get('tags')); + } + + return $metatags; + } + + /** * Generate the elements that go in the attached array in * hook_page_attachments. * @@ -300,8 +388,36 @@ protected function getFieldTags(ContentEntityInterface $entity, $field_name) { * Render array with tag elements. */ public function generateElements($tags, $entity = NULL) { - $metatag_tags = $this->tagPluginManager->getDefinitions(); $elements = []; + $tags = $this->generateRawElements($tags, $entity); + + foreach ($tags as $name => $tag) { + if (!empty($tag)) { + $elements['#attached']['html_head'][] = [ + $tag, + $name, + ]; + } + } + + return $elements; + } + + /** + * Generate the actual meta tag values. + * + * @param array $tags + * The array of tags as plugin_id => value. + * @param object $entity + * Optional entity object to use for token replacements. + * + * @return array + * Render array with tag elements. + */ + public function generateRawElements($tags, $entity = NULL) { + $rawTags = []; + + $metatag_tags = $this->tagPluginManager->getDefinitions(); // Order the elements by weight first, as some systems like Facebook care. uksort($tags, function ($tag_name_a, $tag_name_b) use ($metatag_tags) { @@ -322,7 +438,15 @@ public function generateElements($tags, $entity = NULL) { // Render any tokens in the value. $token_replacements = []; if ($entity) { - $token_replacements = [$entity->getEntityTypeId() => $entity]; + // @TODO: This needs a better way of discovering the context. + if ($entity instanceof ViewEntityInterface) { + // Views tokens require the ViewExecutable, not the config entity. + // @todo Can we move this into metatag_views somehow? + $token_replacements = ['view' => $entity->getExecutable()]; + } + else { + $token_replacements = [$entity->getEntityTypeId() => $entity]; + } } // Set the value as sometimes the data needs massaging, such as when @@ -331,6 +455,7 @@ public function generateElements($tags, $entity = NULL) { // @see @Robots::setValue(). $tag->setValue($value); $langcode = \Drupal::languageManager()->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); + if ($tag->type() === 'image') { $processed_value = $this->tokenService->replace($tag->value(), $token_replacements, ['langcode' => $langcode]); } @@ -345,15 +470,12 @@ public function generateElements($tags, $entity = NULL) { $output = $tag->output(); if (!empty($output)) { - $elements['#attached']['html_head'][] = [ - $output, - $tag_name - ]; + $rawTags[$tag_name] = $output; } } } - return $elements; + return $rawTags; } /** diff --git a/src/MetatagManagerInterface.php b/src/MetatagManagerInterface.php index 5a5af86..4be259e 100644 --- a/src/MetatagManagerInterface.php +++ b/src/MetatagManagerInterface.php @@ -3,6 +3,7 @@ namespace Drupal\metatag; use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\views\ViewEntityInterface; /** * Class MetatagManager.