diff --git a/composer.json b/composer.json index ef885b046a..9076880c35 100644 --- a/composer.json +++ b/composer.json @@ -131,6 +131,7 @@ "drupal/metatag": "^2.0", "drupal/migrate_plus": "6.0.2", "drupal/migrate_source_csv": "^3.4", + "drupal/multivalue_form_element": "^1.0@beta", "drupal/mysql56": "^1.1", "drupal/nobots": "^1.0", "drupal/node_revision_delete": "^1.0@RC", diff --git a/composer.lock b/composer.lock index 70671d5775..9f19aa342e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "33f18b98bdcbfadf4387d304597a471e", + "content-hash": "7fbae20a95dc324b69d2caaf22c29fdc", "packages": [ { "name": "acquia/blt", @@ -8888,6 +8888,60 @@ "slack": "#migrate" } }, + { + "name": "drupal/multivalue_form_element", + "version": "1.0.0-beta6", + "source": { + "type": "git", + "url": "https://git.drupalcode.org/project/multivalue_form_element.git", + "reference": "1.0.0-beta6" + }, + "dist": { + "type": "zip", + "url": "https://ftp.drupal.org/files/projects/multivalue_form_element-1.0.0-beta6.zip", + "reference": "1.0.0-beta6", + "shasum": "653214124be45f11bb76c76871355a30651dd6fe" + }, + "require": { + "drupal/core": "^9.3 || ^10", + "php": ">=7.4" + }, + "type": "drupal-module", + "extra": { + "drupal": { + "version": "1.0.0-beta6", + "datestamp": "1678886197", + "security-coverage": { + "status": "not-covered", + "message": "Project has not opted into security advisory coverage!" + } + } + }, + "notification-url": "https://packages.drupal.org/8/downloads", + "license": [ + "EUPL-1.2" + ], + "authors": [ + { + "name": "hernani", + "homepage": "https://www.drupal.org/user/448086" + }, + { + "name": "ieguskiza", + "homepage": "https://www.drupal.org/user/2652665" + }, + { + "name": "sardara", + "homepage": "https://www.drupal.org/user/2353864" + } + ], + "description": "Provides a form element that wraps existing form elements, making them multi-value.", + "homepage": "https://www.drupal.org/project/multivalue_form_element", + "support": { + "source": "https://git.drupalcode.org/project/multivalue_form_element", + "issues": "https://www.drupal.org/project/issues/multivalue_form_element" + } + }, { "name": "drupal/mysql56", "version": "1.7.0", @@ -26050,6 +26104,7 @@ "drupal/masquerade": 10, "drupal/maxlength": 5, "drupal/menu_position": 10, + "drupal/multivalue_form_element": 10, "drupal/node_revision_delete": 5, "drupal/selective_better_exposed_filters": 10, "drupal/shortcut_menu": 10, @@ -26069,5 +26124,5 @@ "php": ">=8.2" }, "platform-dev": [], - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/config/default/core.extension.yml b/config/default/core.extension.yml index 378b24598a..04e8f87953 100644 --- a/config/default/core.extension.yml +++ b/config/default/core.extension.yml @@ -176,6 +176,7 @@ module: migrate_plus: 0 migrate_source_csv: 0 migrate_tools: 0 + multivalue_form_element: 0 mysql: 0 nobots: 0 node: 0 diff --git a/config/default/user.role.site_manager.yml b/config/default/user.role.site_manager.yml index 44441aba95..9802468224 100644 --- a/config/default/user.role.site_manager.yml +++ b/config/default/user.role.site_manager.yml @@ -7,7 +7,6 @@ dependencies: - filter.format.basic_html - filter.format.minimal_html - filter.format.minimal_html_with_styles - - media.type.embeddable - media.type.file - media.type.image - media.type.video @@ -43,6 +42,7 @@ dependencies: - file - filter - help + - hs_blocks - hs_entities - hs_page_reports - media @@ -76,9 +76,7 @@ permissions: - 'administer main menu items' - 'administer nodes' - 'administer users' - - 'assign author role' - 'assign contributor role' - - 'assign intranet_viewer role' - 'assign site_manager role' - 'create embeddable media' - 'create file media' @@ -174,6 +172,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' @@ -187,7 +186,7 @@ permissions: - 'edit terms in hs_person_staff_type' - 'edit terms in hs_person_student_type' - 'edit terms in hs_publication_type' - - 'import hs_capx migration' + - 'import hs_courses migration' - 'import hs_events_importer migration' - 'manipulate entityqueues' - 'mark as hidden in editoria11y' @@ -196,7 +195,6 @@ permissions: - 'publish any content' - 'publish editable content' - 'rabbit hole bypass node' - - 'rabbit hole bypass taxonomy_term' - 'revert hs_basic_page revisions' - 'revert hs_course revisions' - 'revert hs_event revisions' diff --git a/docroot/modules/humsci/hs_blocks/hs_blocks.install b/docroot/modules/humsci/hs_blocks/hs_blocks.install index 96fa837d9d..fd35bf05ef 100644 --- a/docroot/modules/humsci/hs_blocks/hs_blocks.install +++ b/docroot/modules/humsci/hs_blocks/hs_blocks.install @@ -78,3 +78,21 @@ function _hs_blocks_fix_sections(array $sections) { } return $was_changed; } + +/** + * Implements hook_update_dependencies(). + */ +function hs_blocks_update_dependencies() { + // Permissions for field_paragraph_style need to be removed first. + $dependencies['hs_blocks'][10201] = [ + 'su_humsci_profile' => 9714, + ]; + return $dependencies; +} + +/** + * 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.module b/docroot/modules/humsci/hs_blocks/hs_blocks.module index ada2b35a82..7a2c8b537d 100644 --- a/docroot/modules/humsci/hs_blocks/hs_blocks.module +++ b/docroot/modules/humsci/hs_blocks/hs_blocks.module @@ -6,8 +6,11 @@ */ use Drupal\Component\Utility\Html; +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Session\AccountInterface; /** * Implements hook_help(). @@ -34,6 +37,10 @@ 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 +135,19 @@ function hs_blocks_preprocess_block__views_exposed_filter_block(&$variables) { $variables['content']['#id'] .= '-' . $build_id; } } + +/** + * Implements hook_entity_access(). + */ +function hs_blocks_entity_access(EntityInterface $entity, $operation, AccountInterface $account) { + // Allows roles with "Edit social media block" to edit the custom block. + /** @var Drupal\block\Entity\Block $entity */ + if ($entity->getEntityTypeId() === 'block' && $entity->getPluginId() === 'hs_blocks_social_media_block') { + if ($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 0000000000..b30de7d644 --- /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 0000000000..4887aa9a78 --- /dev/null +++ b/docroot/modules/humsci/hs_blocks/src/Plugin/Block/SocialMediaBlock.php @@ -0,0 +1,262 @@ + 'small', + 'layout' => 'grid', + 'links' => [], + ]; + } + + /** + * {@inheritdoc} + */ + public function blockForm($form, FormStateInterface $form_state): array { + $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' => 'multivalue', + '#title' => $this->t('Links'), + '#description' => $this->t('Popular social platforms will show their icon, otherwise a generic icon will be shown.'), + '#cardinality' => MultiValue::CARDINALITY_UNLIMITED, + '#default_value' => ($this->configuration['links']) ? $this->configuration['links'] : [], + '#element_validate' => [ + [get_class($this), 'validateLinks'], + ], + 'link_url' => [ + '#type' => 'url', + '#title' => $this->t('URL'), + '#description' => $this->t('Social Media Profile URL.'), + ], + 'link_title' => [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#description' => $this->t('If empty the domain name will be used.'), + ], + ]; + + 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 have data. + $links = $form_state->getValue('links'); + $filtered_links = []; + foreach ($links as $link) { + if (!empty($link['link_url'] || !empty($link['link_title']))) { + $filtered_links[] = $link; + } + } + + $this->configuration['links'] = $filtered_links; + } + + /** + * {@inheritdoc} + */ + public function build(): array { + $links = array_map([$this, 'linkWithIcon'], $this->configuration['links']); + $build = [ + '#theme' => 'hs_blocks_social_media', + '#icon_size' => $this->configuration['icon_size'], + '#layout' => $this->configuration['layout'], + '#links' => $links, + '#cache' => [ + 'tags' => array_merge($this->getCacheTags(), ['block_view']), + 'contexts' => ['user', 'user.permissions'], + ], + ]; + + $build['#contextual_links']['hs_blocks.social_media_block'] = [ + 'route_parameters' => ['block' => $this->getDerivativeId()], + ]; + + $build['#attached']['library'][] = 'contextual/drupal.contextual-links'; + + 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)) { + // 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') + ); + } + } + } + } + + /** + * Adds the social media icon and name information to a link. + * + * @param array $link + * The link item. + * + * @return array + * The updated link item with the icon and social provider name. + */ + protected function linkWithIcon(array $link) { + $url = $link['link_url']; + $providers = [ + [ + 'domains' => ['facebook.com', 'fb.com', 'fb.me'], + 'icon_classes' => 'fa-brands fa-square-facebook', + 'name' => 'Facebook', + ], + [ + 'domains' => ['twitter.com', 'x.com'], + 'icon_classes' => 'fa-brands fa-square-x-twitter', + 'name' => 'Twitter', + ], + [ + 'domains' => ['linkedin.com', 'lnkd.in'], + 'icon_classes' => 'fa-brands fa-linkedin', + 'name' => 'Linkedin', + ], + [ + 'domains' => ['instagram.com', 'instagr.am'], + 'icon_classes' => 'fa-brands fa-square-instagram', + 'name' => 'Instagram', + ], + [ + 'domains' => ['youtube.com', 'youtu.be'], + 'icon_classes' => 'fa-brands fa-square-youtube', + 'name' => 'Youtube', + ], + [ + 'domains' => ['vimeo.com'], + 'icon_classes' => 'fa-brands fa-vimeo', + 'name' => 'Vimeo', + ], + [ + 'domains' => ['snapchat.com'], + 'icon_classes' => 'fa-brands fa-square-snapchat', + 'name' => 'Snapchat', + ], + [ + 'domains' => ['soundcloud.com'], + 'icon_classes' => 'fa-brands fa-soundcloud', + 'name' => 'Soundcloud', + ], + [ + 'domains' => ['spotify.com'], + 'icon_classes' => 'fa-brands fa-spotify', + 'name' => 'Spotify', + ], + [ + 'domains' => ['apple.com'], + 'icon_classes' => 'fa-brands fa-apple', + 'name' => 'Apple', + ], + [ + 'domains' => ['telegram.me', 't.me'], + 'icon_classes' => 'fa-brands fa-telegram', + 'name' => 'Telegram', + ], + [ + 'domains' => ['mailto:'], + 'icon_classes' => 'fa-solid fa-square-envelope', + 'name' => 'Email', + ], + [ + 'domains' => ['pinterest.com', 'pin.it'], + 'icon_classes' => 'fa-brands fa-square-pinterest', + 'name' => 'Pinterest', + ], + [ + 'domains' => ['tiktok.com'], + 'icon_classes' => 'fa-brands fa-tiktok', + 'name' => 'Tiktok', + ], + ]; + + $icon_classes = ''; + $icon_name = ''; + + foreach ($providers as $provider) { + foreach ($provider['domains'] as $domain) { + if (strpos($url, $domain) !== FALSE) { + $icon_classes = $provider['icon_classes']; + $icon_name = $provider['name']; + break 2; + } + } + } + + if (!$icon_classes) { + $icon_classes = 'fa-solid fa-globe'; + // Use the domain as the name if the provider is not listed above. + if (preg_match('/https?:\/\/(.+?)\//', $url, $matches)) { + $icon_name = $matches[1]; + } + } + + return [ + 'link_url' => $url, + 'link_title' => $link['link_title'] ?: $icon_name, + '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 0000000000..bc4aea9645 --- /dev/null +++ b/docroot/modules/humsci/hs_blocks/templates/block--social-media.html.twig @@ -0,0 +1,12 @@ +{% if links %} + +{% 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 4fc0b40601..b7cc811ae9 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,11 @@ 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', 'hs_blocks.social_media_block']) && + !$current_user->hasPermission('view all contextual links') && + !$current_user->hasPermission('edit social media block') ) { $links = []; } diff --git a/docroot/themes/humsci/humsci_basic/src/scss/_main.scss b/docroot/themes/humsci/humsci_basic/src/scss/_main.scss index 8a81e60d91..22caa423cc 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 0000000000..61962f81d7 --- /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 0000000000..962f2d1ce2 --- /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 %} +