would add a // landmark per bar, so a page with both bars grows two. $heading = ''; if ('inline' === $placement && !empty($config['show_heading']) && '' !== trim((string) ($config['heading'] ?? ''))) { $heading = sprintf( '%s', esc_html($config['heading']) ); } $group_label = 'floating' === $placement ? __('Share this post', 'addons-for-divi') : ('' !== $heading ? $config['heading'] : __('Share this post', 'addons-for-divi')); return sprintf( '
' . '%4$s' . '%5$s' . '
%6$s
' . '
', esc_attr(self::wrapper_class($placement, $settings)), esc_attr($label_id), self::data_attributes($placement, $settings), esc_html($group_label), $heading, $buttons ); } /** * Wrapper class list. * * Every interpolated fragment is an enum value that Share_My_Post_Settings * has already constrained, so none of them can introduce a new class. * * @param string $placement 'inline' or 'floating'. * @param array $settings Full settings tree. * * @return string */ public static function wrapper_class($placement, array $settings) { $appearance = $settings['appearance'] ?? array(); $classes = array( 'dtq-share', 'dtq-share--' . $placement, 'dtq-share--display-' . ($appearance['display'] ?? 'icon'), 'dtq-share--color-' . ($appearance['color_mode'] ?? 'brand'), 'dtq-share--shape-' . ($appearance['shape'] ?? 'rounded'), 'dtq-share--size-' . ($appearance['size'] ?? 'medium'), ); if ('floating' === $placement) { $classes[] = 'dtq-share--side-' . ($settings['floating']['side'] ?? 'left'); } return implode(' ', $classes); } /** * Wrapper data attributes. * * The mobile flags are attributes rather than classes because the generated * media query keys off them — that keeps the generated CSS to a fixed size * regardless of how many bars are on the page. * * @param string $placement 'inline' or 'floating'. * @param array $settings Full settings tree. * * @return string */ private static function data_attributes($placement, array $settings) { $mobile = $settings['mobile'] ?? array(); $visible = 'floating' === $placement ? !empty($mobile['show_floating']) : !empty($mobile['show_inline']); $attrs = sprintf(' data-mobile="%s"', $visible ? 'show' : 'hide'); if ('floating' === $placement) { $offset = (int) ($settings['floating']['offset'] ?? 50); $attrs .= sprintf(' style="--dtq-share-offset:%d%%"', max(0, min(100, $offset))); } return $attrs; } /** * Render one share button. * * @param string $slug Network slug (already catalog-checked by the caller). * @param array $settings Full settings tree. * @param array $context Share context. * * @return string */ public static function button($slug, array $settings, array $context) { $network = Share_My_Post_Networks::get($slug); if (null === $network) { return ''; } $icons = Share_My_Post_Networks::icons(); $icon = $icons[$slug]['svg'] ?? ''; /* translators: %s: social network name, e.g. Facebook. */ $accessible_name = sprintf(__('Share on %s', 'addons-for-divi'), $network['label']); // Special-cased because "Share on Copy Link" / "Share on Print" is not // English. These three act on this page rather than posting to a service. if ('copy' === $network['action']) { $accessible_name = __('Copy link to this post', 'addons-for-divi'); } elseif ('print' === $network['action']) { $accessible_name = __('Print this post', 'addons-for-divi'); } elseif ('native' === $network['action']) { $accessible_name = __('Share this post', 'addons-for-divi'); } // The accessible name comes from exactly one place. The icon and the // visible label are both hidden from assistive tech — otherwise "both" // mode announces "Share on Facebook Facebook", and "label" mode // announces a bare "Facebook", which fails WCAG 2.4.4 in a row of eight // identically-shaped links. $inner = sprintf( '%1$s' . '' . '', esc_html($accessible_name), $icon, // Generated at build time from Simple Icons; not author input. esc_html($network['label']) ); $class = 'dtq-share__btn dtq-share__btn--' . $slug; // copy / print / native have no URL to fall back to, so they are real // buttons. CSS hides them until frontend.js confirms the API exists — // a visible control that does nothing is worse than an absent one. if ('' !== $network['action']) { return sprintf( '', esc_attr($class), esc_attr($network['action']), esc_url($context['url'] ?? ''), $inner ); } $href = Share_My_Post_Url::build($slug, $context); if ('' === $href) { return ''; } $popup = Share_My_Post_Url::is_popup($slug) && !empty($settings['advanced']['open_in_popup']); // Scheme links (mailto:, sms:, viber:) must not get target="_blank": // the OS handler takes over and the new tab is left blank and orphaned. $external = Share_My_Post_Url::is_popup($slug); return sprintf( '%5$s', esc_attr($class), // The protocol list is what lets viber:// through. Pro's module skips // esc_url() entirely to work around this, losing escaping everywhere. esc_url($href, array_merge(wp_allowed_protocols(), Share_My_Post_Networks::EXTRA_PROTOCOLS)), $external ? ' target="_blank" rel="noopener nofollow"' : '', $popup ? ' data-dtq-share-popup="1"' : '', $inner ); } /** * Inline CSS for the parts that depend on settings. * * Three pieces, all through wp_add_inline_style() rather than a generated * stylesheet on disk: custom colours, brand colours for the enabled networks * only, and the media query. * * @param array $settings Full settings tree. * * @return string */ public static function inline_css(array $settings) { $css = ''; $appearance = $settings['appearance'] ?? array(); if ('custom' === ($appearance['color_mode'] ?? 'brand')) { $css .= sprintf( '.dtq-share--color-custom{--dtq-share-bg:%s;--dtq-share-fg:%s;}', dtq_css_color($appearance['custom_bg'] ?? '', '#2b6cb0'), dtq_css_color($appearance['custom_fg'] ?? '', '#ffffff') ); } else { $css .= self::brand_css($settings); } $css .= self::breakpoint_css($settings); /** * Filters the inline CSS emitted for the share bars. * * @param string $css Generated CSS. * @param array $settings Full settings tree. */ return apply_filters('divitorque_share_my_post_inline_css', $css, $settings); } /** * Brand colour rules for the networks actually on the page. * * Real CSS rather than inline style="" attributes: a page with both bars * would otherwise repeat every colour once per button per bar, and inline * styles need 'unsafe-inline' in a CSP style-src. * * @param array $settings Full settings tree. * * @return string */ private static function brand_css(array $settings) { $slugs = array_unique( array_merge( Share_My_Post_Networks::filter($settings['inline']['networks'] ?? array()), Share_My_Post_Networks::filter($settings['floating']['networks'] ?? array()) ) ); $icons = Share_My_Post_Networks::icons(); $css = ''; foreach ($slugs as $slug) { if (!isset($icons[$slug])) { continue; } // hex/fg are generated at build time, never author input — but they // still go through dtq_css_color, because "generated" is an argument // about today's code rather than a guarantee about the value. $css .= sprintf( '.dtq-share--color-brand .dtq-share__btn--%1$s{--dtq-share-bg:%2$s;--dtq-share-fg:%3$s;}', $slug, // Catalog membership already guarantees [a-z] only. dtq_css_color($icons[$slug]['hex'], '#2b6cb0'), dtq_css_color($icons[$slug]['fg'], '#ffffff') ); } return $css; } /** * The mobile media query. * * Generated rather than static because a custom property cannot appear in a * media condition — `@media (max-width: var(--x))` is invalid and the whole * block is dropped. Only the condition varies; the rules inside key off the * data-mobile attribute, so this stays a fixed ~200 bytes however many bars * are on the page. * * @param array $settings Full settings tree. * * @return string */ private static function breakpoint_css(array $settings) { $breakpoint = (int) ($settings['mobile']['breakpoint'] ?? 768); $breakpoint = max( Share_My_Post_Settings::BREAKPOINT_MIN, min(Share_My_Post_Settings::BREAKPOINT_MAX, $breakpoint) ); return sprintf( '@media (max-width:%dpx){.dtq-share[data-mobile="hide"]{display:none;}}', $breakpoint ); } }