From 3d9ef65f467a3ff4d2480bd90da63e20f7e01b3a Mon Sep 17 00:00:00 2001 From: Marc Berger <107938318+codechefmarc@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:24:21 -0800 Subject: [PATCH] SHS-5905: Rework social media footer (#1662) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(SHS-5905): Recreate social media block as a custom block plugin * feat(SHS-5905): Add composer.lock * feat(SHS-5905): Enable multivalue form element module * feat(SHS-5905): Add update hook for permissions change * fix(SHS-5905): Fix error for default config and add cache context for user * fix(SHS-5905): Contextual menu still not rendering on Tugboat for non-admins * fix(SHS-5905): Fix attached library for contextual menu * fix(SHS-5909): Strict checking of the array seemed to fix the contextual links on local * fix(SHS-5905): Remove strict checking * fix(SHS-5905): Remove old permissions before adding social media block permissions * fix(SHS-5905): Linting fixes * chore(SHS-5905): WIP TEST ONLY - see if removing the code block in the profile works on tugboat * fix(SHS-5905): Fix contextual menu for non-admins * feat(SHS-5905): Remove permission update hook and add dependency on one in profile * feat(shs-5906): icon logic, templates and styles for social media footer block (#1673) * chore(SHS-5905): Temp remove code to test other contextual menus * feat(SHS-5905): Add custom contextual link * fix(SHS-5905): Get contextual links working (finally) for Social Media block * fix(shs-5905): fixes in social media block * fix(shs-5905): replace multivalue_form_element with custom version in social media block * fix(shs-5905): remove multivalue_form_element module * fix(shs-5905): update social media block links help text * fix(shs-5905): social media footer block fixes --------- Co-authored-by: Andrés Díaz Soto --- config/default/user.role.site_manager.yml | 1 + .../config/schema/hs_blocks.schema.yml | 23 ++ .../humsci/hs_blocks/hs_blocks.install | 7 + .../hs_blocks/hs_blocks.links.contextual.yml | 5 + .../modules/humsci/hs_blocks/hs_blocks.module | 28 ++ .../hs_blocks/hs_blocks.permissions.yml | 3 + .../src/Plugin/Block/SocialMediaBlock.php | 383 ++++++++++++++++++ .../templates/block--social-media.html.twig | 12 + .../su_humsci_profile.profile | 6 +- .../humsci/humsci_basic/src/scss/_main.scss | 1 + .../_block-social-media-footer.scss | 70 ++++ ...ck--hs-blocks-social-media-block.html.twig | 47 +++ 12 files changed, 584 insertions(+), 2 deletions(-) create mode 100644 docroot/modules/humsci/hs_blocks/config/schema/hs_blocks.schema.yml create mode 100644 docroot/modules/humsci/hs_blocks/hs_blocks.permissions.yml create mode 100644 docroot/modules/humsci/hs_blocks/src/Plugin/Block/SocialMediaBlock.php create mode 100644 docroot/modules/humsci/hs_blocks/templates/block--social-media.html.twig create mode 100644 docroot/themes/humsci/humsci_basic/src/scss/components/_block-social-media-footer.scss create mode 100644 docroot/themes/humsci/humsci_basic/templates/block/block--hs-blocks-social-media-block.html.twig diff --git a/config/default/user.role.site_manager.yml b/config/default/user.role.site_manager.yml index 44441aba9..813bbd564 100644 --- a/config/default/user.role.site_manager.yml +++ b/config/default/user.role.site_manager.yml @@ -174,6 +174,7 @@ permissions: - 'edit own hs_research content' - 'edit own image media' - 'edit own video media' + - 'edit social media block' - 'edit terms in hs_course_component' - 'edit terms in hs_course_tags' - 'edit terms in hs_event_audience' diff --git a/docroot/modules/humsci/hs_blocks/config/schema/hs_blocks.schema.yml b/docroot/modules/humsci/hs_blocks/config/schema/hs_blocks.schema.yml new file mode 100644 index 000000000..9d65e85d4 --- /dev/null +++ b/docroot/modules/humsci/hs_blocks/config/schema/hs_blocks.schema.yml @@ -0,0 +1,23 @@ +block.settings.hs_blocks_social_media_block: + type: block_settings + label: 'Social Media Block Configuration' + mapping: + icon_size: + type: string + label: 'Icon Size' + layout: + type: string + label: String + links: + type: sequence + label: Links + sequence: + link_url: + type: uri + label: URL + link_title: + type: string + label: Title + _weight: + type: integer + label: Weight diff --git a/docroot/modules/humsci/hs_blocks/hs_blocks.install b/docroot/modules/humsci/hs_blocks/hs_blocks.install index 96fa837d9..f12884b55 100644 --- a/docroot/modules/humsci/hs_blocks/hs_blocks.install +++ b/docroot/modules/humsci/hs_blocks/hs_blocks.install @@ -78,3 +78,10 @@ function _hs_blocks_fix_sections(array $sections) { } return $was_changed; } + +/** + * Update user permissions for new social media block. + */ +function hs_blocks_update_10201() { + user_role_grant_permissions('site_manager', ['edit social media block']); +} diff --git a/docroot/modules/humsci/hs_blocks/hs_blocks.links.contextual.yml b/docroot/modules/humsci/hs_blocks/hs_blocks.links.contextual.yml index 154ae9d2d..f79fc6c13 100644 --- a/docroot/modules/humsci/hs_blocks/hs_blocks.links.contextual.yml +++ b/docroot/modules/humsci/hs_blocks/hs_blocks.links.contextual.yml @@ -48,3 +48,8 @@ hs_blocks.block_move_down: class: ['use-ajax'] # data-dialog-type: dialog # data-dialog-renderer: off_canvas + +hs_blocks.social_media_block: + title: 'Edit Social Media block' + route_name: 'entity.block.edit_form' + group: 'social_media_block' diff --git a/docroot/modules/humsci/hs_blocks/hs_blocks.module b/docroot/modules/humsci/hs_blocks/hs_blocks.module index ada2b35a8..0f180fba8 100644 --- a/docroot/modules/humsci/hs_blocks/hs_blocks.module +++ b/docroot/modules/humsci/hs_blocks/hs_blocks.module @@ -5,9 +5,12 @@ * Contains hs_blocks.module. */ +use Drupal\block\Entity\Block; use Drupal\Component\Utility\Html; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Session\AccountInterface; /** * Implements hook_help(). @@ -34,6 +37,14 @@ function hs_blocks_theme($existing, $type, $theme, $path) { 'template' => 'block--hs-login', 'variables' => ['preface' => NULL, 'link' => NULL, 'postface' => NULL], ], + 'hs_blocks_social_media' => [ + 'template' => 'block--social-media', + 'variables' => [ + 'icon_size' => NULL, + 'layout' => NULL, + 'links' => [], + ], + ], ]; } @@ -128,3 +139,20 @@ function hs_blocks_preprocess_block__views_exposed_filter_block(&$variables) { $variables['content']['#id'] .= '-' . $build_id; } } + +/** + * Implements hook_block_access(). + */ +function hs_blocks_block_access(Block $block, $operation, AccountInterface $account) { + // Allows roles with "Edit social media block" to edit the custom block. + if ( + $block->getPluginId() === 'hs_blocks_social_media_block' && + $operation === 'update' && + $account->hasPermission('edit social media block') + ) { + return AccessResult::allowed(); + } + + // No change, return neutral result. + return AccessResult::neutral(); +} diff --git a/docroot/modules/humsci/hs_blocks/hs_blocks.permissions.yml b/docroot/modules/humsci/hs_blocks/hs_blocks.permissions.yml new file mode 100644 index 000000000..b30de7d64 --- /dev/null +++ b/docroot/modules/humsci/hs_blocks/hs_blocks.permissions.yml @@ -0,0 +1,3 @@ +edit social media block: + title: 'Edit social media block' + description: 'Allows users to configure the social media block' diff --git a/docroot/modules/humsci/hs_blocks/src/Plugin/Block/SocialMediaBlock.php b/docroot/modules/humsci/hs_blocks/src/Plugin/Block/SocialMediaBlock.php new file mode 100644 index 000000000..074a8837c --- /dev/null +++ b/docroot/modules/humsci/hs_blocks/src/Plugin/Block/SocialMediaBlock.php @@ -0,0 +1,383 @@ +currentUser = $current_user; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('current_user') + ); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration(): array { + return [ + 'icon_size' => 'small', + 'layout' => 'grid', + 'links' => [], + ]; + } + + /** + * {@inheritdoc} + */ + public function blockForm($form, FormStateInterface $form_state): array { + // If the form is being rendered for the first time, need to set the links + // in the form_state, because we'll used them to generate the form elements. + if (is_null($form_state->get('links'))) { + $links = $this->configuration['links']; + // Add an empty item at the bottom, to make it easier for users to add + // new links. + $weight = !empty($links) ? $links[count($links) - 1]['_weight'] + 1 : 0; + $links[] = [ + 'link_title' => '', + 'link_url' => '', + '_weight' => $weight, + ]; + $form_state->set('links', $links); + } + + $form['icon_size'] = [ + '#type' => 'select', + '#title' => $this->t('Icon Size'), + '#options' => [ + 'small' => $this->t('Small (32px)'), + 'large' => $this->t('Large (48px)'), + ], + '#default_value' => $this->configuration['icon_size'], + '#required' => TRUE, + ]; + + $form['layout'] = [ + '#type' => 'select', + '#title' => $this->t('Layout'), + '#options' => [ + 'grid' => $this->t('Grid (no visible label)'), + 'vertical_list' => $this->t('Vertical List (with visible label)'), + ], + '#default_value' => $this->configuration['layout'], + '#required' => TRUE, + ]; + + $form['links'] = [ + '#type' => 'container', + '#field_name' => 'links', + '#title' => $this->t('Links'), + '#input' => TRUE, + '#theme' => 'field_multiple_value_form', + '#cardinality_multiple' => TRUE, + '#cardinality' => -1, + '#description' => $this->t( + '

Supported social platforms will show their icon, otherwise a generic icon will be shown.

See which social platforms are currently supported.

', + ['@user_guide_url' => 'https://hsweb.slite.page/p/NeJL89GqNsiOY-/Social-Media-Footer-block'] + ), + '#add_more_label' => $this->t('Add another item'), + '#element_validate' => [ + [get_class($this), 'validateLinks'], + ], + '#attributes' => [ + 'id' => 'links-wrapper', + ], + ]; + + foreach ($form_state->get('links') as $key => $link) { + $form['links'][$key] = [ + '#type' => 'container', + 'link_url' => [ + '#type' => 'url', + '#title' => $this->t('URL'), + '#description' => $this->t('Social Media Profile URL.'), + '#default_value' => $link['link_url'], + ], + 'link_title' => [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#description' => $this->t('If empty, the social platform name will be used for supported platforms, otherwise the domain name will be used.'), + '#default_value' => $link['link_title'], + ], + '_weight' => [ + '#type' => 'weight', + '#title' => $this->t('Weight'), + '#title_display' => 'invisible', + '#delta' => count($this->configuration['links']), + '#default_value' => $link['_weight'], + ], + ]; + } + + $form['links']['add_more'] = [ + '#type' => 'submit', + '#name' => 'links_add_more', + '#value' => $form['links']['#add_more_label'], + '#submit' => [[get_class($this), 'addMoreSubmit']], + '#ajax' => [ + 'callback' => [get_class($this), 'addMoreAjax'], + 'wrapper' => 'links-wrapper', + 'effect' => 'fade', + ], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function blockSubmit($form, FormStateInterface $form_state): void { + $this->configuration['icon_size'] = $form_state->getValue('icon_size'); + $this->configuration['layout'] = $form_state->getValue('layout'); + + // Only save links if they're not empty. + $links = array_filter($form_state->getValue('links'), function ($link) { + return !empty($link['link_url']); + }); + $this->configuration['links'] = $links; + + // This sets the placed block ID to be used for a custom contextual link. + $this->configuration['placed_block_id'] = $form['id']['#default_value']; + } + + /** + * {@inheritdoc} + */ + public function build(): array { + $links = array_map([$this, 'linkWithIcon'], $this->configuration['links']); + $placed_block_id = $this->configuration['placed_block_id']; + + $build = [ + '#theme' => 'hs_blocks_social_media', + '#icon_size' => $this->configuration['icon_size'], + '#layout' => $this->configuration['layout'], + '#links' => $links, + '#contextual_links' => [ + 'social_media_block' => [ + 'route_parameters' => ['block' => $placed_block_id], + ], + ], + ]; + + return $build; + } + + /** + * Check that links have a URL. + * + * @param array $element + * The element to check. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + */ + public static function validateLinks(array $element, FormStateInterface $form_state) { + $links = $form_state->getValue($element['#parents']); + foreach ($links as $key => $link) { + if (!is_array($link)) { + continue; + } + // Test if there is a title but no URL. + if (!empty($link['link_title']) && empty($link['link_url'])) { + $form_state->setErrorByName( + implode('][', array_merge($element['#parents'], [$key, 'link_url'])), + t('The URL must be provided if a label is set.') + ); + } + } + } + + /** + * Submit handler for the "Add another item" button in the "Links" element. + * + * @param array $form + * The form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + */ + public static function addMoreSubmit(array $form, FormStateInterface $form_state): void { + $links = $form_state->get('links'); + // Add new empty element at the bottom + // (weight greater than the last current element). + $weight = !empty($links) ? $links[count($links) - 1]['_weight'] + 1 : 0; + $links[] = [ + 'link_url' => '', + 'link_title' => '', + '_weight' => $weight, + ]; + $form_state->set('links', $links); + $form_state->setRebuild(); + } + + /** + * Ajax Callback for the "Add another item" button in the "Links" element. + * + * @param array $form + * The form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * The form element to replace. + */ + public static function addMoreAjax(array $form, FormStateInterface $form_state): array { + return $form['settings']['links']; + } + + /** + * Returns a list of social media providers with their url, title and icon. + * + * @return array + * The list of social media providers. + */ + protected function getProviders(): array { + return [ + [ + 'domains' => ['facebook.com', 'fb.com', 'fb.me'], + 'icon_classes' => 'fa-brands fa-square-facebook', + 'title' => 'Facebook', + ], + [ + 'domains' => ['twitter.com', 'x.com'], + 'icon_classes' => 'fa-brands fa-square-x-twitter', + 'title' => 'X', + ], + [ + 'domains' => ['linkedin.com', 'lnkd.in'], + 'icon_classes' => 'fa-brands fa-linkedin', + 'title' => 'Linkedin', + ], + [ + 'domains' => ['instagram.com', 'instagr.am'], + 'icon_classes' => 'fa-brands fa-square-instagram', + 'title' => 'Instagram', + ], + [ + 'domains' => ['youtube.com', 'youtu.be'], + 'icon_classes' => 'fa-brands fa-square-youtube', + 'title' => 'Youtube', + ], + [ + 'domains' => ['vimeo.com'], + 'icon_classes' => 'fa-brands fa-vimeo', + 'title' => 'Vimeo', + ], + [ + 'domains' => ['snapchat.com'], + 'icon_classes' => 'fa-brands fa-square-snapchat', + 'title' => 'Snapchat', + ], + [ + 'domains' => ['soundcloud.com'], + 'icon_classes' => 'fa-brands fa-soundcloud', + 'title' => 'Soundcloud', + ], + [ + 'domains' => ['spotify.com'], + 'icon_classes' => 'fa-brands fa-spotify', + 'title' => 'Spotify', + ], + [ + 'domains' => ['apple.com'], + 'icon_classes' => 'fa-brands fa-apple', + 'title' => 'Apple', + ], + [ + 'domains' => ['telegram.me', 't.me'], + 'icon_classes' => 'fa-brands fa-telegram', + 'title' => 'Telegram', + ], + [ + 'domains' => ['mailto:'], + 'icon_classes' => 'fa-solid fa-square-envelope', + 'title' => 'Email', + ], + [ + 'domains' => ['pinterest.com', 'pin.it'], + 'icon_classes' => 'fa-brands fa-square-pinterest', + 'title' => 'Pinterest', + ], + [ + 'domains' => ['tiktok.com'], + 'icon_classes' => 'fa-brands fa-tiktok', + 'title' => 'Tiktok', + ], + ]; + } + + /** + * Adds the social media icon and title information to a link. + * + * @param array $link + * The link item. + * + * @return array + * The updated link item with the icon and social provider title. + */ + protected function linkWithIcon(array $link): array { + $url = $link['link_url']; + + $icon_classes = 'fa-solid fa-globe'; + $link_title = $link['link_title'] ?: ''; + + foreach ($this->getProviders() as $provider) { + foreach ($provider['domains'] as $domain) { + if (strpos($url, $domain) !== FALSE) { + $icon_classes = $provider['icon_classes']; + $link_title = $link_title ?: $provider['title']; + break 2; + } + } + } + + // Use the domain as the link title if the provider is not listed above. + if (!$link_title && preg_match('/https?:\/\/(.+?)\//', $url, $matches)) { + $link_title = $matches[1]; + } + + return [ + 'link_url' => $url, + 'link_title' => $link_title, + 'icon_classes' => $icon_classes, + ]; + } + +} diff --git a/docroot/modules/humsci/hs_blocks/templates/block--social-media.html.twig b/docroot/modules/humsci/hs_blocks/templates/block--social-media.html.twig new file mode 100644 index 000000000..c77bf484b --- /dev/null +++ b/docroot/modules/humsci/hs_blocks/templates/block--social-media.html.twig @@ -0,0 +1,12 @@ +{% if links %} + + {% for link in links %} +
  • + +
  • + {% endfor %} + +{% endif %} diff --git a/docroot/profiles/humsci/su_humsci_profile/su_humsci_profile.profile b/docroot/profiles/humsci/su_humsci_profile/su_humsci_profile.profile index 4e1445a66..6b3154205 100644 --- a/docroot/profiles/humsci/su_humsci_profile/su_humsci_profile.profile +++ b/docroot/profiles/humsci/su_humsci_profile/su_humsci_profile.profile @@ -327,6 +327,7 @@ function su_humsci_profile_preprocess_table(&$variables) { * Implements hook_contextual_links_alter(). */ function su_humsci_profile_contextual_links_alter(array &$links, $group, array $route_parameters) { + $current_user = \Drupal::service('current_user'); if ($group == 'paragraph') { // Paragraphs edit module clone link does not function correctly. Remove it // from available links. Also remove delete to avoid unwanted delete. @@ -343,9 +344,10 @@ function su_humsci_profile_contextual_links_alter(array &$links, $group, array $ $link['title'] .= " {$entity_types[$group]}"; } } + if ( - !in_array($group, ['media', 'block_content']) && - !\Drupal::currentUser()->hasPermission('view all contextual links') + !in_array($group, ['media', 'block_content', 'social_media_block']) && + !$current_user->hasPermission('view all contextual links') ) { $links = []; } diff --git a/docroot/themes/humsci/humsci_basic/src/scss/_main.scss b/docroot/themes/humsci/humsci_basic/src/scss/_main.scss index 8a81e60d9..22caa423c 100644 --- a/docroot/themes/humsci/humsci_basic/src/scss/_main.scss +++ b/docroot/themes/humsci/humsci_basic/src/scss/_main.scss @@ -195,6 +195,7 @@ $hb-root-font-size: 10px !default; 'components/editoria11y', // customizations for editoria11y component. 'components/add-to-cal', 'components/main-content-anchor-target', + 'components/block-social-media-footer', // ===================================================================== // 7. Admin diff --git a/docroot/themes/humsci/humsci_basic/src/scss/components/_block-social-media-footer.scss b/docroot/themes/humsci/humsci_basic/src/scss/components/_block-social-media-footer.scss new file mode 100644 index 000000000..61962f81d --- /dev/null +++ b/docroot/themes/humsci/humsci_basic/src/scss/components/_block-social-media-footer.scss @@ -0,0 +1,70 @@ +.block-social-media-footer { + ul { + @include hb-list-empty-styles; + } + + li { + display: flex; + } + + .social-media-link { + text-decoration: none; + display: flex; + align-items: center; + flex-grow: 1; + } + + .social-media-link__icon { + width: 1em; + text-align: center; + + // Override for Soundcloud icon (too wide). + &.fa-soundcloud { + font-size: 0.73em; + width: 1.37em; + } + } +} + +// Grid Layout. +.block-social-media-footer--layout-grid { + ul { + display: grid; + gap: hb-calculate-rems(16px); + grid-template-columns: repeat(4, 1fr); + } + + .social-media-link__label { + @include visually-hidden; + } +} + +// Vertical List Layout. +.block-social-media-footer--layout-vertical-list { + li { + margin-bottom: hb-calculate-rems(16px); + } + + .social-media-link { + gap: hb-calculate-rems(16px); + } + + .social-media-link__label { + font-size: hb-calculate-rems(16px); + text-decoration: underline; + } +} + +// Small icons. +.block-social-media-footer--icons-small { + .social-media-link { + font-size: hb-calculate-rems(32px); + } +} + +// Large icons. +.block-social-media-footer--icons-large { + .social-media-link { + font-size: hb-calculate-rems(48px); + } +} diff --git a/docroot/themes/humsci/humsci_basic/templates/block/block--hs-blocks-social-media-block.html.twig b/docroot/themes/humsci/humsci_basic/templates/block/block--hs-blocks-social-media-block.html.twig new file mode 100644 index 000000000..962f2d1ce --- /dev/null +++ b/docroot/themes/humsci/humsci_basic/templates/block/block--hs-blocks-social-media-block.html.twig @@ -0,0 +1,47 @@ +{# +/** + * @file + * Default theme implementation to display a block. + * + * Available variables: + * - plugin_id: The ID of the block implementation. + * - label: The configured label of the block if visible. + * - configuration: A list of the block's configuration values. + * - label: The configured label for the block. + * - label_display: The display settings for the label. + * - provider: The module or other provider that provided this block plugin. + * - Block plugin specific settings will also be stored here. + * - in_preview: Whether the plugin is being rendered in preview mode. + * - content: The content of this block. + * - attributes: array of HTML attributes populated by modules, intended to + * be added to the main container tag of this template. + * - id: A valid HTML ID and guaranteed unique. + * - title_attributes: Same as attributes, except applied to the main title + * tag that appears in the template. + * - title_prefix: Additional output populated by modules, intended to be + * displayed in front of the main title tag that appears in the template. + * - title_suffix: Additional output populated by modules, intended to be + * displayed after the main title tag that appears in the template. + * + * @see template_preprocess_block() + * + * @ingroup themeable + */ +#} +{% set base_class = 'block-social-media-footer' %} +{% set classes = [ + base_class, + base_class ~ '--layout-' ~ content['#layout']|clean_class, + base_class ~ '--icons-' ~ content['#icon_size']|clean_class, +] %} + + + {{ title_prefix }} + {% if label %} + {{ label }} + {% endif %} + {{ title_suffix }} + {% block content %} + {{ content }} + {% endblock %} +