diff --git a/metatag.module b/metatag.module index 394867e..1fcd862 100644 --- a/metatag.module +++ b/metatag.module @@ -238,6 +238,38 @@ function metatag_get_route_entity() { } /** + * Returns the display for the view from the current route. + * + * @return \Drupal\views\Plugin\views\display\DisplayPluginBase|null + * The Display entity for the current view + */ +function metatag_get_route_view() { + // Get the route name + $route_match = \Drupal::routeMatch(); + $route_name = $route_match->getRouteName(); + + // Check if the route name is generated by the view + if(strpos($route_name, 'view.') !== FALSE) { + // Get basic info about the view + $params = $route_match->getParameters(); + $view_id = $params->get('view_id'); + $display_id = $params->get('display_id'); + // If view_id or display_id are not set something is wrong. + if(!$view_id || !$display_id) { + return NULL; + } + // Get the view display object for current view and return it. + $view = \Drupal\views\Views::getView($view_id); + $view->setDisplay($display_id); + $display = $view->getDisplay(); + + return $display; + } + + return NULL; +} + +/** * Implements template_preprocess_html(). */ function metatag_preprocess_html(&$variables) { @@ -297,6 +329,28 @@ function metatag_get_tags_from_route() { } } + // Check if the Views integration is enabled and find settings for that + // @TODO: Might be a good idea to allow modules to hook into this process. + if (\Drupal::moduleHandler()->moduleExists('metatag_views')) { + // Get the view display from route + $display = metatag_get_route_view(); + if($display) { + // And get the list of extenders for this display. + $extenders = $display->getExtenders(); + if (!isset($extenders['metatag_display_extender'])) { + // If the id of our plugin is not in the list something is wrong. + return; + } + // Retrieve the settings of our plugins using method from metatag views module + $tags = $extenders['metatag_display_extender']->getMetatags(); + // Account for our future lanuage handling + $tags = !empty($tags) ? $tags[\Drupal\Core\Language\LanguageInterface::LANGCODE_NOT_SPECIFIED] : array(); + $metatags = array_merge($metatags, $tags); + // Pass the view as the entity. + $entity = $display->view; + } + } + return $metatag_manager->generateElements($metatags, $entity); } diff --git a/metatag.services.yml b/metatag.services.yml index ab98d77..c55377f 100644 --- a/metatag.services.yml +++ b/metatag.services.yml @@ -13,4 +13,4 @@ services: metatag.manager: class: Drupal\metatag\MetatagManager - arguments: ['@plugin.manager.metatag.group', '@plugin.manager.metatag.tag', '@metatag.token', '@logger.factory'] + arguments: ['@plugin.manager.metatag.group', '@plugin.manager.metatag.tag', '@metatag.token', '@logger.factory', '@entity_type.manager'] 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..a176aae --- /dev/null +++ b/metatag_views/config/schema/metatag_views.views.schema.yml @@ -0,0 +1,2 @@ +views.display_extender.metatag_display_extender: + type: views_display_extender \ No newline at end of file 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..b5c34a5 --- /dev/null +++ b/metatag_views/metatag_views.links.action.yml @@ -0,0 +1,6 @@ +metatag_views.metatags.add: + route_name: 'metatag_views.metatags.add' + title: 'Add views meta tags' + appears_on: + - metatag_views.metatags.list + diff --git a/metatag_views/metatag_views.links.task.yml b/metatag_views/metatag_views.links.task.yml new file mode 100644 index 0000000..7261c9a --- /dev/null +++ b/metatag_views/metatag_views.links.task.yml @@ -0,0 +1,9 @@ +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 \ No newline at end of file diff --git a/metatag_views/metatag_views.routing.yml b/metatag_views/metatag_views.routing.yml new file mode 100644 index 0000000..b7a9d3b --- /dev/null +++ b/metatag_views/metatag_views.routing.yml @@ -0,0 +1,59 @@ +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.translate: + 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' + options: + _admin_route: TRUE + +metatag_views.metatags.translate_langcode: + 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' + 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 + +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 diff --git a/metatag_views/src/Controller/MetatagViewsController.php b/metatag_views/src/Controller/MetatagViewsController.php new file mode 100644 index 0000000..349a7ab --- /dev/null +++ b/metatag_views/src/Controller/MetatagViewsController.php @@ -0,0 +1,200 @@ +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') + ); + } + + /** + * Main controller function. Generates the renderable array for views + * metatags UI. + * + * @return array + */ + public function listViews() { + // Get metatags for all of the views / displays that have them set. + $metataged_views = $this->metatagManager->getMetatagedViews(); + $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. + foreach($metataged_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', $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->get('label'); + foreach(array_keys($displays) as $display_id) { + $labels[$view_id][$display_id] = $displays[$display_id]['display_title']; + } + } + + $this->viewLabels = $labels; + } +} \ No newline at end of file diff --git a/metatag_views/src/Controller/MetatagViewsTranslationController.php b/metatag_views/src/Controller/MetatagViewsTranslationController.php new file mode 100644 index 0000000..c7b40e2 --- /dev/null +++ b/metatag_views/src/Controller/MetatagViewsTranslationController.php @@ -0,0 +1,143 @@ +viewStorage = $viewStorage; + $this->metatagManager = $metatagManager; + $this->languageManager = $languageManager; + $this->viewsManager = $entity_manager->getStorage('view'); + } + + /** + * {@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'), + $container->get('entity_type.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'); + + $languages = $this->languageManager->getLanguages(); + + $original_langcode = $this->languageManager->getDefaultLanguage()->getId(); + $operations_access = TRUE; + + if (!isset($languages[$original_langcode])) { + // If the language is not configured on the site, create a dummy language + // object for this listing only to ensure the user gets useful info. + $language_name = $this->languageManager->getLanguageName($original_langcode); + $languages[$original_langcode] = new Language(array('id' => $original_langcode, 'name' => $language_name)); + } + + $page['languages'] = array( + '#type' => 'table', + '#header' => array($this->t('Language'), $this->t('Operations')), + ); + + $metatags = $this->metatagManager->tagsFromViewDisplay($this->viewsManager->load($view_id), $display_id); + + foreach ($languages as $language) { + $langcode = $language->getId(); + + // 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)', array('@language' => $language->getName())) . ''; + + // Build list of operations. + $operations = array(); + $operations['edit'] = array( + 'title' => $this->t('Edit'), + 'url' => Url::fromRoute('metatag_views.metatags.edit', ['view_id' => $view_id, 'display_id' => $display_id]), + ); + } else { + $language_name = $language->getName(); + + $operations = array(); + // If no translation exists for this language, link to add one. + if (!array_key_exists($langcode, $metatags)) { + $operations['add'] = array( + 'title' => $this->t('Add'), + 'url' => Url::fromRoute('metatag_views.metatags.translate_langcode', ['view_id' => $view_id, 'display_id' => $display_id, 'langcode' => $langcode]), + ); + } + else { + // Otherwise, link to edit the existing translation. + $operations['edit'] = array( + 'title' => $this->t('Edit'), + 'url' => Url::fromRoute('metatag_views.metatags.translate_langcode', ['view_id' => $view_id, 'display_id' => $display_id, 'langcode' => $langcode]), + ); + + //TODO: operations delete. + } + } + + $page['languages'][$langcode]['language'] = array( + '#markup' => $language_name, + ); + + $page['languages'][$langcode]['operations'] = array( + '#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; + } +} \ No newline at end of file diff --git a/metatag_views/src/Form/MetatagViewsAddForm.php b/metatag_views/src/Form/MetatagViewsAddForm.php new file mode 100644 index 0000000..fc25bea --- /dev/null +++ b/metatag_views/src/Form/MetatagViewsAddForm.php @@ -0,0 +1,158 @@ +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_add_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + // Get views list as options. + $in_use = $this->metatagManager->getMetatagedViews(); + $opts = Views::getViewsAsOptions(FALSE, 'enabled', NULL, TRUE, TRUE); + // Get only the views that do not have the metatags set yet. + $filtered = $this->filterViewList($in_use, $opts); + + // Build/inject the Metatag form. Use defaults. + $metatags = $this->metatagManager->getDefaultMetatags(); + + $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' => 'select', + '#options' => $filtered, + '#title' => t('Choose a view'), + '#weight' => -100, + ]; + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Submit'), + ); + + return $form; + } + + /** + * Filters the options array to unset the views that already have metatag + * display extender configured. + * + * @param $used + * Views to exclude + * @param $all + * Views to exclude from + * @return mixed + * An associative array of views and display ids. + */ + protected function filterViewList($used, $all) { + foreach ($used as $view_id => $displays) { + foreach(array_keys($displays) as $display_id) { + $id = $view_id . ':' . $display_id; + if(isset($all[$view_id][$id])) { + unset($all[$view_id][$id]); + } + } + if(empty($all[$view_id])) { + unset($all[$view_id]); + } + } + return $all; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Get the values + $metatags = $form_state->getValues(); + $view_option = $form_state->getValue('view'); + // Remove the unnecessary entry from values. + unset($metatags['view']); + + list($view_id, $display_id) = explode(':', $view_option); + + /** @var ViewEntityInterface $view */ + $view = $this->viewsManager->load($view_id); + + // Clear the unneeded elements. + $metatags = $this->clearMetatagViewsDisallowedValues($metatags); + + // Save the display extender options and the view itself. + $this->setMetatagDisplayExtenderValues($view, $display_id, $metatags); + + // Redirect back to the views list. + $form_state->setRedirect($this->redirectRoute); + + $display = $view->getDisplay($display_id); + + $params = [ + '@view' => $view->get('label'), + '@display' => $display['display_title'] + ]; + + drupal_set_message($this->t('Metatags for @view : @display have been added.', $params)); + } +} diff --git a/metatag_views/src/Form/MetatagViewsEditForm.php b/metatag_views/src/Form/MetatagViewsEditForm.php new file mode 100644 index 0000000..fde9d8d --- /dev/null +++ b/metatag_views/src/Form/MetatagViewsEditForm.php @@ -0,0 +1,178 @@ +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'); + + $metatags = []; + + /** @var ViewEntityInterface $view */ + $this->view = $this->viewsManager->load($view_id); + // Array representing the display settings. + $this->display = $this->view->getDisplay($display_id); + + // Get metatags from the view entity. + $metatags = $this->metatagManager->tagsFromViewDisplay($this->view, $display_id); + + $form['metatags'] = $this->metatagManager->form( $metatags, $form, ['view'] ); + $form['metatags']['#title'] = t('Metatags'); + $form['metatags']['#type'] = 'fieldset'; + + $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 validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Get the values. + $metatags = $form_state->getValues(); + + // Clear the unneeded elements. + $metatags = $this->clearMetatagViewsDisallowedValues($metatags); + + // Save the display extender options and the view itself. + $this->setMetatagDisplayExtenderValues($this->view, $this->display['id'], $metatags); + + // Redirect back to the views list. + $form_state->setRedirect($this->redirectRoute); + + $params = [ + '@view' => $this->view->get('label'), + '@display' => $this->display['display_title'] + ]; + + drupal_set_message($this->t('Metatags for @view : @display have been updated.', $params)); + } + +} diff --git a/metatag_views/src/Form/MetatagViewsOptionsSubmitTrait.php b/metatag_views/src/Form/MetatagViewsOptionsSubmitTrait.php new file mode 100644 index 0000000..d8da865 --- /dev/null +++ b/metatag_views/src/Form/MetatagViewsOptionsSubmitTrait.php @@ -0,0 +1,37 @@ +get('display'); + // Set proper values for the display extender. + $element = is_null($values) ? [] : [ 'metatags' => $values ]; + + $element['metatags'] = array_merge($displays[$display_id]['display_options']['display_extenders']['metatag_display_extender']['metatags'], $element['metatags']); + + $displays[$display_id]['display_options']['display_extenders']['metatag_display_extender'] = $element; + + // Set the displays back to the view + $view->set('display', $displays); + // Save the views. + $view->save(); + } +} \ No newline at end of file diff --git a/metatag_views/src/Form/MetatagViewsRevertForm.php b/metatag_views/src/Form/MetatagViewsRevertForm.php new file mode 100644 index 0000000..5f1ba6b --- /dev/null +++ b/metatag_views/src/Form/MetatagViewsRevertForm.php @@ -0,0 +1,115 @@ +viewsManager = $entity_manager->getStorage('view'); + } + + 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->display['display_title'] + ]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('metatag_views.metatags.list'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->t('This reverts the override metatags for a selected view. This action cannot be undone.'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Revert'); + } + + /** + * {@inheritdoc} + */ + public function getCancelText() { + return $this->t('Forget it'); + } + + /** + * {@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) { + /** @var ViewEntityInterface view */ + $this->view = $this->viewsManager->load($view_id); + /** @var array display */ + $this->display = $this->view->getDisplay($display_id); + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Call set function in a reset mode (explicitly set the third argument to NULL). + $this->setMetatagDisplayExtenderValues($this->view, $this->display['id'], NULL); + + // Redirect back to the views list. + $form_state->setRedirect($this->redirectRoute); + + drupal_set_message($this->t('Reverted metatags for @view_name : @display_name', + [ '@view_name' => $this->view->label(), + '@display_name' => $this->display['display_title'] + ])); + } + +} diff --git a/metatag_views/src/Form/MetatagViewsTranslationForm.php b/metatag_views/src/Form/MetatagViewsTranslationForm.php new file mode 100644 index 0000000..9558641 --- /dev/null +++ b/metatag_views/src/Form/MetatagViewsTranslationForm.php @@ -0,0 +1,200 @@ +metatagManager = $metatag_manager; + $this->viewsManager = $entity_manager->getStorage('view'); + $this->tokenService = $token; + $this->tagPluginManager = $tagPluginManager; + } + + 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') + ); + } + + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'metatag_views_translate_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'); + $langcode = \Drupal::request()->get('langcode'); + + /** @var ViewEntityInterface $view */ + $this->view = $this->viewsManager->load($view_id); + // Array representing the display settings. + $this->display = $this->view->getDisplay($display_id); + + // Get metatags from the view entity. + $metatags = $this->metatagManager->tagsFromViewDisplay($this->view, $display_id); + + $metatags = array_filter($metatags, function($value) { + if(is_array($value)) { + return count(array_filter($value)) > 0; + } + else { + return $value !== ''; + } + }); + $form['#tree'] = TRUE; + $form['#attached']['library'][] = 'config_translation/drupal.config_translation.admin'; + + $form['metatags'] = $this->form($metatags, $form, ['view'], $langcode ); + $form['metatags']['#title'] = t('Metatags'); + $form['metatags']['#type'] = 'fieldset'; + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Submit'), + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function form(array $values, array $element, array $token_types = [], $langcode) { + $translation = $values[$langcode]; + $values = $this->clearMetatagViewsDisallowedValues($values); + + // Add the outer fieldset. + $element += [ + '#type' => 'details', + ]; + + $element += $this->tokenService->tokenBrowser($token_types); + + foreach ($values as $tag_id => $value) { + $tag = $this->tagPluginManager->createInstance($tag_id); + + $tag->setValue($translation[$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 validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + //Get langcode for object + $langcode = \Drupal::request()->get('langcode'); + + //Params for redirect + $view_id = \Drupal::request()->get('view_id'); + $display_id = \Drupal::request()->get('display_id'); + + // Get the values of metatags. + $metatags_values = $form_state->getValue('metatags'); + + $metatags = []; + + //Array of metatags translations. + foreach($metatags_values as $key => $value) { + $metatags[$key] = $value['translation']; + } + + // Save the display extender options and the view itself. + $this->setMetatagDisplayExtenderValues($this->view, $this->display['id'], [$langcode => $metatags]); + + // Redirect back to the views list. + $form_state->setRedirect('metatag_views.metatags.translate', ['view_id' => $view_id, 'display_id' => $display_id]); + + $params = [ + '@view' => $this->view->get('label'), + '@display' => $this->display['display_title'] + ]; + + drupal_set_message($this->t('Metatags for @view : @display have been updated.', $params)); + } + +} diff --git a/metatag_views/src/MetatagViewsValuesCleanerTrait.php b/metatag_views/src/MetatagViewsValuesCleanerTrait.php new file mode 100644 index 0000000..e4eb1af --- /dev/null +++ b/metatag_views/src/MetatagViewsValuesCleanerTrait.php @@ -0,0 +1,37 @@ +metatagManager->sortedTags(); + + // Return only common elements. + $metatags = array_intersect_key($metatags, $tags); + + return $metatags; + } + + public function clearMetatagViewsDisallowedTranslationValues($metatags, $langcode) { + // Get all legal tags. + $tags = $this->metatagManager->sortedTags(); + + // Return only common elements. + $metatags = array_intersect_key($metatags[$langcode], $tags); + + return $metatags; + } +} \ No newline at end of file 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..4308966 --- /dev/null +++ b/metatag_views/src/Plugin/views/display_extender/MetatagDisplayExtender.php @@ -0,0 +1,136 @@ +get('section') == 'metatags') { + $form['#title'] .= t('The meta tags for this display'); + $metatags = $this->getMetatags(); + $metatags = !empty($metatags) ? $metatags[LanguageInterface::LANGCODE_NOT_SPECIFIED] : array(); + + // 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[LanguageInterface::LANGCODE_NOT_SPECIFIED]); + + } + + /** + * Get the Metatag configuration for this display. + * + * @return array + * The meta tag values, keys by language (default LanguageInterface::LANGCODE_NOT_SPECIFIED). + */ + public function getMetatags($language = NULL) { + $metatags = $this->options['metatags']; + + $language = is_null($language) ? LanguageInterface::LANGCODE_NOT_SPECIFIED : $language; + // Leave some possibility for future versions to support translation. + if (empty($metatags)) { + $metatags = array($language => array()); + } + if (!isset($metatags[$language])) { + $metatags = array($language => $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/src/MetatagManager.php b/src/MetatagManager.php index 9123713..8844ed0 100644 --- a/src/MetatagManager.php +++ b/src/MetatagManager.php @@ -4,9 +4,12 @@ namespace Drupal\metatag; use Drupal\Component\Render\PlainTextOutput; use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Logger\LoggerChannelFactoryInterface; use Drupal\field\Entity\FieldConfig; +use Drupal\views\ViewEntityInterface; +use Drupal\views\ViewExecutable; /** * Class MetatagManager. @@ -17,6 +20,8 @@ class MetatagManager implements MetatagManagerInterface { protected $groupPluginManager; protected $tagPluginManager; + protected $defaultMetatagsManager; + protected $viewStorage; protected $tokenService; @@ -34,13 +39,17 @@ class MetatagManager implements MetatagManagerInterface { * @param MetatagTagPluginManager $tagPluginManager * @param MetatagToken $token * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $channelFactory + * @param EntityTypeManagerInterface $entityTypeManager */ public function __construct(MetatagGroupPluginManager $groupPluginManager, MetatagTagPluginManager $tagPluginManager, MetatagToken $token, - LoggerChannelFactoryInterface $channelFactory) { + LoggerChannelFactoryInterface $channelFactory, + EntityTypeManagerInterface $entityTypeManager) { $this->groupPluginManager = $groupPluginManager; $this->tagPluginManager = $tagPluginManager; + $this->defaultMetatagsManager = $entityTypeManager->getStorage('metatag_defaults'); + $this->viewStorage = $entityTypeManager->getStorage('view'); $this->tokenService = $token; $this->logger = $channelFactory->get('metatag'); } @@ -63,6 +72,53 @@ class MetatagManager implements MetatagManagerInterface { } /** + * {@inheritdoc} + */ + public function tagsFromViewDisplay(ViewEntityInterface $view, $display_id = NULL) { + $tags = []; + + if(is_null($display_id)) { + $displays = $view->get('display'); + foreach(array_keys($displays) as $display_id) { + $metatags = $this->tagsFromViewDisplay($view, $display_id); + if(!empty($metatags)) { + $tags[$display_id] = $metatags; + } + } + return $tags; + } + + $display = $view->getDisplay($display_id); + $tags = isset($display['display_options']['display_extenders']['metatag_display_extender']['metatags']) ? $display['display_options']['display_extenders']['metatag_display_extender']['metatags'] : NULL; + + return $tags; + } + + /** + * This returns metatag values for each view id and display id. + * + * @return array - associative array of metatags keyed by display id + */ + public function getMetatagedViews() { + /** @var ViewEntityInterface[] $views */ + $views = $this->viewStorage->loadByProperties(['status' => 1]); + + $metataged_views = []; + + // Iterate over views and get metatags for each of them creating array + // where the values are not empty. + foreach($views as $view_id => $view) { + $tags = $this->tagsFromViewDisplay($view); + if(empty($tags)) { + continue; + } + $metataged_views[$view_id] = $tags; + } + + return $metataged_views; + } + + /** * Gets the group plugin definitions. * * @return array @@ -207,6 +263,8 @@ class MetatagManager implements MetatagManagerInterface { return $element; } + + /** * Returns a list of the metatag fields on an entity. */ @@ -254,6 +312,77 @@ class MetatagManager implements MetatagManagerInterface { return $tags; } + 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 - ex. views. Maybe utilize 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->defaultMetatagsManager->load('global'); + } + + public function getSpecialMetatags() { + if (\Drupal::service('path.matcher')->isFrontPage()) { + return $this->defaultMetatagsManager->load('front'); + } + elseif (\Drupal::service('current_route_match')->getRouteName() == 'system.403') { + return $this->defaultMetatagsManager->load('403'); + } + elseif (\Drupal::service('current_route_match')->getRouteName() == 'system.404') { + return $this->defaultMetatagsManager->load('404'); + } + + return; + } + + + public function getEntityDefaultMetatags(ContentEntityInterface $entity) { + $entity_metatags = $this->defaultMetatagsManager->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->defaultMetatagsManager->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. @@ -266,9 +395,28 @@ class MetatagManager implements MetatagManagerInterface { * 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; + } + + public function generateRawElements($tags, $entity = NULL) { + $metatag_tags = $this->tagPluginManager->getDefinitions(); + + $rawTags = []; + // Each element of the $values array is a tag with the tag plugin name // as the key. foreach ($tags as $tag_name => $value) { @@ -280,7 +428,15 @@ class MetatagManager implements MetatagManagerInterface { // Render any tokens in the value. $token_replacements = []; if ($entity) { - $token_replacements = [$entity->getEntityTypeId() => $entity]; + // Check if our entity is a view and react appropriately. + // @TODO: Probably this needs a better way of discovering the context + // @TODO: This should be an EntityConfigInterface actually... + if($entity instanceof ViewExecutable) { + $token_replacements = ['view' => $entity]; + } + else { + $token_replacements = [$entity->getEntityTypeId() => $entity]; + } } // Set the value as sometimes the data needs massaging, such as when @@ -289,6 +445,7 @@ class MetatagManager implements MetatagManagerInterface { // @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]); } @@ -302,16 +459,14 @@ class MetatagManager implements MetatagManagerInterface { // Have the tag generate the output based on the value we gave it. $output = $tag->output(); - if (!empty($output)) { - $elements['#attached']['html_head'][] = [ - $output, - $tag_name - ]; + if(!empty($output)) { + $rawTags[$tag_name] = $output; } + } } - return $elements; + return $rawTags; } /** diff --git a/src/MetatagManagerInterface.php b/src/MetatagManagerInterface.php index 03aaa2f..ea429f5 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. @@ -23,6 +24,20 @@ interface MetatagManagerInterface { public function tagsFromEntity(ContentEntityInterface $entity); /** + * Extracts all tags of a given view. + * + * @param \Drupal\views\ViewEntityInterface $view + * The view to extract metatags from. + * @param string|null $display_id + * The id of the display to extract metatags from. + * + * @return array + * Array of metatags for a given display id of a view. Array is keyed by + * display_id when null is passed. + */ + public function tagsFromViewDisplay(ViewEntityInterface $view, $display_id = NULL); + + /** * Returns an array of group plugin information sorted by weight. * * @return array