1<?php
2
3
4namespace ComboStrap;
5
6
7use action_plugin_combo_metatwitter;
8use ComboStrap\TagAttribute\StyleAttribute;
9
10/**
11 *
12 * Brand button
13 *   * basic
14 *   * share
15 *   * follow
16 *
17 * @package ComboStrap
18 *
19 *
20 * Share link:
21 *   * [Link](https://github.com/mxstbr/sharingbuttons.io/blob/master/js/stores/AppStore.js#L242)
22 *   * https://github.com/ellisonleao/sharer.js/blob/main/sharer.js#L72
23 * Style:
24 *   * [Style](https://github.com/mxstbr/sharingbuttons.io/blob/master/js/stores/AppStore.js#L10)
25 *
26 * Popup:
27 * https://gist.github.com/josephabrahams/9d023596b884e80e37e5
28 * https://jonsuh.com/blog/social-share-links/
29 * https://stackoverflow.com/questions/11473345/how-to-pop-up-new-window-with-tweet-button
30 *
31 * Inspired by:
32 * http://sharingbuttons.io (Specifically thanks for the data)
33 */
34class BrandButton
35{
36    public const WIDGET_BUTTON_VALUE = "button";
37    public const WIDGET_LINK_VALUE = "link";
38    const WIDGETS = [self::WIDGET_BUTTON_VALUE, self::WIDGET_LINK_VALUE];
39    const ICON_SOLID_VALUE = "solid";
40    const ICON_SOLID_CIRCLE_VALUE = "solid-circle";
41    const ICON_OUTLINE_CIRCLE_VALUE = "outline-circle";
42    const ICON_OUTLINE_VALUE = "outline";
43    const ICON_TYPES = [self::ICON_SOLID_VALUE, self::ICON_SOLID_CIRCLE_VALUE, self::ICON_OUTLINE_VALUE, self::ICON_OUTLINE_CIRCLE_VALUE, self::ICON_NONE_VALUE];
44    const ICON_NONE_VALUE = "none";
45
46    const CANONICAL = "social";
47
48
49    /**
50     * @var string
51     */
52    private $widget = self::WIDGET_BUTTON_VALUE;
53    /**
54     * @var mixed|string
55     */
56    private $iconType = self::ICON_SOLID_VALUE;
57    /**
58     * The width of the icon
59     * @var int|null
60     */
61    private $width = null;
62    /**
63     * @var string
64     */
65    private $type;
66    const TYPE_BUTTON_SHARE = "share";
67    const TYPE_BUTTON_FOLLOW = "follow";
68    const TYPE_BUTTON_BRAND = "brand";
69    const TYPE_BUTTONS = [self::TYPE_BUTTON_SHARE, self::TYPE_BUTTON_FOLLOW, self::TYPE_BUTTON_BRAND];
70
71
72    /**
73     * @var string the follow handle
74     */
75    private $handle;
76
77
78    /**
79     * @var Brand
80     */
81    private $brand;
82    private $primaryColor;
83    private $title;
84    private $secondaryColor;
85
86
87    /**
88     * @throws ExceptionCompile
89     */
90    public function __construct(string $brandName, string $typeButton)
91    {
92
93        $this->brand = Brand::create($brandName);
94
95        $this->type = strtolower($typeButton);
96        if (!in_array($this->type, self::TYPE_BUTTONS)) {
97            throw new ExceptionCompile("The button type ($this->type} is unknown.");
98        }
99
100
101    }
102
103    /**
104     * Return all combination of widget type and icon type
105     * @return array
106     */
107    public static function getVariants(): array
108    {
109        $variants = [];
110        foreach (self::WIDGETS as $widget) {
111            foreach (self::ICON_TYPES as $typeIcon) {
112                if ($typeIcon === self::ICON_NONE_VALUE) {
113                    continue;
114                }
115                $variants[] = [BrandTag::ICON_ATTRIBUTE => $typeIcon, TagAttributes::TYPE_KEY => $widget];
116            }
117        }
118        return $variants;
119    }
120
121    /**
122     * @throws ExceptionCompile
123     */
124    public static function createBrandButton(string $brand): BrandButton
125    {
126        return new BrandButton($brand, self::TYPE_BUTTON_BRAND);
127    }
128
129
130    /**
131     * @throws ExceptionCompile
132     */
133    public function setWidget($widget): BrandButton
134    {
135        /**
136         * Widget validation
137         */
138        $this->widget = $widget;
139        $widget = trim(strtolower($widget));
140        if (!in_array($widget, self::WIDGETS)) {
141            throw new ExceptionCompile("The {$this->type} widget ($widget} is unknown. The possible widgets value are " . implode(",", self::WIDGETS));
142        }
143        return $this;
144    }
145
146    /**
147     * @throws ExceptionCompile
148     */
149    public function setIconType($iconType): BrandButton
150    {
151        /**
152         * Icon Validation
153         */
154        $this->iconType = $iconType;
155        $iconType = trim(strtolower($iconType));
156        if (!in_array($iconType, self::ICON_TYPES)) {
157            throw new ExceptionCompile("The icon type ($iconType) is unknown. The possible icons value are " . implode(",", self::ICON_TYPES));
158        }
159        return $this;
160    }
161
162    public function setWidth(?int $width): BrandButton
163    {
164        /**
165         * Width
166         */
167        if ($width === null) {
168            return $this;
169        }
170        $this->width = $width;
171        return $this;
172    }
173
174    /**
175     * @throws ExceptionCompile
176     */
177    public static function createShareButton(
178        string $brandName,
179        string $widget = self::WIDGET_BUTTON_VALUE,
180        string $icon = self::ICON_SOLID_VALUE,
181        ?int   $width = null): BrandButton
182    {
183        return (new BrandButton($brandName, self::TYPE_BUTTON_SHARE))
184            ->setWidget($widget)
185            ->setIconType($icon)
186            ->setWidth($width);
187    }
188
189    /**
190     * @throws ExceptionCompile
191     */
192    public static function createFollowButton(
193        string $brandName,
194        string $handle = null,
195        string $widget = self::WIDGET_BUTTON_VALUE,
196        string $icon = self::ICON_SOLID_VALUE,
197        ?int   $width = null): BrandButton
198    {
199        return (new BrandButton($brandName, self::TYPE_BUTTON_FOLLOW))
200            ->setHandle($handle)
201            ->setWidget($widget)
202            ->setIconType($icon)
203            ->setWidth($width);
204    }
205
206    /**
207     *
208     *
209     * Dictionary has been made with the data found here:
210     *   * https://github.com/ellisonleao/sharer.js/blob/main/sharer.js#L72
211     *   * and
212     * @throws ExceptionBadArgument
213     */
214    public function getBrandEndpointForPage(MarkupPath $requestedPage = null): ?string
215    {
216
217        /**
218         * Shared/Follow Url template
219         */
220        $urlTemplate = $this->brand->getWebUrlTemplate($this->type);
221        if ($urlTemplate === null) {
222            throw new ExceptionBadArgument("The brand ($this) does not support the $this->type button (The $this->type URL is unknown)");
223        }
224        switch ($this->type) {
225
226            case self::TYPE_BUTTON_SHARE:
227                if ($requestedPage === null) {
228                    throw new ExceptionBadArgument("The page requested should not be null for a share button when requesting the endpoint uri.");
229                }
230                $canonicalUrl = $this->getSharedUrlForPage($requestedPage);
231                $templateData["url"] = $canonicalUrl;
232                $templateData["title"] = $requestedPage->getTitleOrDefault();
233
234                try {
235                    $templateData["description"] = $requestedPage->getDescription();
236                } catch (ExceptionNotFound $e) {
237                    $templateData["description"] = "";
238                }
239
240                $templateData["text"] = $this->getTextForPage($requestedPage);
241
242                $via = null;
243                if ($this->brand->getName() == \action_plugin_combo_metatwitter::CANONICAL) {
244                    $via = substr(action_plugin_combo_metatwitter::COMBO_STRAP_TWITTER_HANDLE, 1);
245                }
246                if ($via !== null && $via !== "") {
247                    $templateData["via"] = $via;
248                }
249                foreach ($templateData as $key => $value) {
250                    $templateData[$key] = urlencode($value);
251                }
252
253                return Template::create($urlTemplate)->setProperties($templateData)->render();
254
255            case self::TYPE_BUTTON_FOLLOW:
256                if ($this->handle === null) {
257                    return $urlTemplate;
258                }
259                $templateData[Tag\FollowTag::HANDLE_ATTRIBUTE] = $this->handle;
260                return Template::create($urlTemplate)->setProperties($templateData)->render();
261            default:
262                // The type is mandatory and checked at creation,
263                // it should not happen, we don't throw an error
264                $message = "Button type ($this->type) is unknown";
265                LogUtility::msg($message, LogUtility::LVL_MSG_ERROR, self::CANONICAL);
266                return $message;
267        }
268
269    }
270
271    public function __toString()
272    {
273        return $this->brand->__toString();
274    }
275
276    public function getLabel(): string
277    {
278        $title = $this->title;
279        if ($title !== null && trim($title) !== "") {
280            return $title;
281        }
282        $title = $this->brand->getTitle($this->iconType);
283        if ($title !== null && trim($title) !== "") {
284            return $title;
285        }
286        $name = ucfirst($this->brand->getName());
287        switch ($this->type) {
288            case self::TYPE_BUTTON_SHARE:
289                return "Share this page via $name";
290            case self::TYPE_BUTTON_FOLLOW:
291                return "Follow us on $name";
292            case self::TYPE_BUTTON_BRAND:
293                return $name;
294            default:
295                return "Button type ($this->type) is unknown";
296        }
297    }
298
299    /**
300     * @throws ExceptionCompile
301     */
302    public
303    function getStyle(): string
304    {
305
306        /**
307         * Default colors
308         */
309        // make the button/link space square
310        $properties["padding"] = "0.375rem 0.375rem";
311        switch ($this->widget) {
312            case self::WIDGET_LINK_VALUE:
313                $properties["vertical-align"] = "middle";
314                $properties["display"] = "inline-block";
315                $primaryColor = $this->getPrimaryColor();
316                if ($primaryColor !== null) {
317                    // important because the nav-bar class takes over
318                    $properties["color"] = "$primaryColor!important";
319                }
320                break;
321            default:
322            case self::WIDGET_BUTTON_VALUE:
323
324                $primary = $this->getPrimaryColor();
325                if ($primary === null) {
326                    // custom brand default color
327                    $primary = ComboStrap::PRIMARY_COLOR;
328                }
329                $textColor = $this->getTextColor();
330                if ($textColor === null || $textColor === "") {
331                    $textColor = "#fff";
332                }
333                $properties["background-color"] = $primary;
334                $properties["border-color"] = $primary;
335                $properties["color"] = $textColor;
336                break;
337        }
338        switch ($this->iconType) {
339            case self::ICON_OUTLINE_VALUE:
340                // not for outline circle, it's cut otherwise, don't know why
341                $properties["stroke-width"] = "2px";
342                break;
343        }
344
345        $cssProperties = "\n";
346        foreach ($properties as $key => $value) {
347            $cssProperties .= "    $key:$value;\n";
348        }
349        $style = <<<EOF
350.{$this->getIdentifierClass()} {{$cssProperties}}
351EOF;
352
353        /**
354         * Hover Style
355         */
356        $secondary = $this->getSecondaryColor();
357        if ($secondary === null) {
358            return $style;
359        }
360        $hoverProperties = [];
361        switch ($this->widget) {
362            case self::WIDGET_LINK_VALUE:
363                $hoverProperties["color"] = $secondary;
364                break;
365            default:
366            case self::WIDGET_BUTTON_VALUE:
367                $textColor = $this->getTextColor();
368                $hoverProperties["background-color"] = $secondary;
369                $hoverProperties["border-color"] = $secondary;
370                $hoverProperties["color"] = $textColor;
371                break;
372        }
373        $hoverCssProperties = "\n";
374        foreach ($hoverProperties as $key => $value) {
375            $hoverCssProperties .= "    $key:$value;\n";
376        }
377        $hoverStyle = <<<EOF
378.{$this->getIdentifierClass()}:hover, .{$this->getIdentifierClass()}:active {{$hoverCssProperties}}
379EOF;
380
381        return <<<EOF
382$style
383$hoverStyle
384EOF;
385
386
387    }
388
389    public function getBrand(): Brand
390    {
391        return $this->brand;
392    }
393
394    /**
395     * The identifier of the {@link BrandButton::getStyle()} script
396     * used as script id in the {@link SnippetSystem}
397     * @return string
398     */
399    public
400    function getStyleScriptIdentifier(): string
401    {
402        return "{$this->getType()}-{$this->brand->getName()}-{$this->getWidget()}-{$this->getIcon()}";
403    }
404
405    /**
406     * @return string - the class identifier used in the {@link BrandButton::getStyle()} script
407     */
408    public
409    function getIdentifierClass(): string
410    {
411        return StyleAttribute::addComboStrapSuffix($this->getStyleScriptIdentifier());
412    }
413
414    /**
415     * @throws ExceptionNotFound
416     */
417    public
418    function getIconAttributes(): array
419    {
420
421        $iconName = $this->getResourceIconName();
422        $icon = $this->getResourceIconFile();
423        if (!FileSystems::exists($icon)) {
424            $iconName = $this->brand->getIconName($this->iconType);
425            $brandNames = Brand::getAllKnownBrandNames();
426            if ($iconName === null && in_array($this->getBrand(), $brandNames)) {
427                throw new ExceptionNotFound("No {$this->iconType} icon could be found for the known brand ($this)");
428            }
429        }
430        $attributes = [FetcherSvg::NAME_ATTRIBUTE => $iconName];
431        $textColor = $this->getTextColor();
432        if ($textColor !== null) {
433            $attributes[ColorRgb::COLOR] = $textColor;
434        }
435        $attributes[Dimension::WIDTH_KEY] = $this->getWidth();
436
437        return $attributes;
438    }
439
440    public
441    function getTextColor(): ?string
442    {
443
444        switch ($this->widget) {
445            case self::WIDGET_LINK_VALUE:
446                return $this->getPrimaryColor();
447            default:
448            case self::WIDGET_BUTTON_VALUE:
449                return "#fff";
450        }
451
452    }
453
454    /**
455     * Class added to the link
456     * This is just to be boostrap conformance
457     */
458    public
459    function getWidgetClass(): string
460    {
461        /**
462         * The btn bootstrap class:
463         * * makes a link a button
464         * * and normalize the button styling
465         */
466        return "btn";
467    }
468
469
470    public
471    function getWidget(): string
472    {
473        return $this->widget;
474    }
475
476    private
477    function getIcon()
478    {
479        return $this->iconType;
480    }
481
482    private
483    function getDefaultWidth(): int
484    {
485        switch ($this->widget) {
486            case self::WIDGET_LINK_VALUE:
487                return 36;
488            case self::WIDGET_BUTTON_VALUE:
489            default:
490                return 24;
491        }
492    }
493
494    private
495    function getWidth(): ?int
496    {
497        if ($this->width === null) {
498            return $this->getDefaultWidth();
499        }
500        return $this->width;
501    }
502
503    public function hasIcon(): bool
504    {
505        if ($this->iconType === self::ICON_NONE_VALUE) {
506            return false;
507        }
508
509        if ($this->brand->getIconName($this->iconType) !== null) {
510            return true;
511        }
512
513        if (!FileSystems::exists($this->getResourceIconFile())) {
514            return false;
515        }
516        return true;
517    }
518
519
520    /**
521     */
522    public
523    function getTextForPage(MarkupPath $requestedPage): string
524    {
525
526        try {
527            return "{$requestedPage->getTitleOrDefault()} > {$requestedPage->getDescription()}";
528        } catch (ExceptionNotFound $e) {
529            // no description, may be ?
530            return $requestedPage->getTitleOrDefault();
531        }
532
533    }
534
535    public
536    function getSharedUrlForPage(MarkupPath $requestedPage): string
537    {
538        return $requestedPage->getCanonicalUrl()->toAbsoluteUrlString();
539    }
540
541    /**
542     * Return the button HTML attributes
543     * @throws ExceptionCompile
544     */
545    public
546    function getHtmlAttributes(MarkupPath $requestedPage = null): TagAttributes
547    {
548
549
550        $logicalTag = $this->type;
551        $buttonAttributes = TagAttributes::createEmpty($logicalTag);
552        $buttonAttributes->addComponentAttributeValue(TagAttributes::TYPE_KEY, $logicalTag);
553        $buttonAttributes->addClassName("{$this->getWidgetClass()} {$this->getIdentifierClass()}");
554        $label = $this->getLabel();
555        switch ($this->type) {
556            case self::TYPE_BUTTON_SHARE:
557
558                if ($requestedPage === null) {
559                    throw new ExceptionCompile("The page requested should not be null for a share button");
560                }
561
562                switch ($this->getBrand()) {
563                    case "whatsapp":
564                        /**
565                         * Direct link
566                         * For whatsapp, the sharer link is not the good one
567                         */
568                        $buttonAttributes->addOutputAttributeValue("target", "_blank");
569                        $buttonAttributes->addOutputAttributeValue("href", $this->getBrandEndpointForPage($requestedPage));
570                        $buttonAttributes->addOutputAttributeValue("title", $label);
571                        break;
572                    default:
573                        /**
574                         * Sharer
575                         * https://ellisonleao.github.io/sharer.js/
576                         */
577                        /**
578                         * Opens in a popup
579                         */
580                        $buttonAttributes->addOutputAttributeValue("rel", "noopener");
581
582                        PluginUtility::getSnippetManager()->attachRemoteJavascriptLibrary(
583                            "sharer",
584                            "https://cdn.jsdelivr.net/npm/sharer.js@0.5.0/sharer.min.js",
585                            "sha256-AqqY/JJCWPQwZFY/mAhlvxjC5/880Q331aOmargQVLU="
586                        );
587                        $buttonAttributes->addOutputAttributeValue("aria-label", $label);
588                        $buttonAttributes->addOutputAttributeValue("data-sharer", $this->getBrand()); // the id
589                        $buttonAttributes->addOutputAttributeValue("data-link", "false");
590                        $buttonAttributes->addOutputAttributeValue("data-title", $this->getTextForPage($requestedPage));
591                        $urlToShare = $this->getSharedUrlForPage($requestedPage);
592                        $buttonAttributes->addOutputAttributeValue("data-url", $urlToShare);
593                        //$linkAttributes->addComponentAttributeValue("href", "#"); // with # we style navigate to the top
594                        $buttonAttributes->addStyleDeclarationIfNotSet("cursor", "pointer"); // show a pointer (without href, there is none)
595                }
596                return $buttonAttributes;
597            case self::TYPE_BUTTON_FOLLOW:
598
599                $buttonAttributes->addOutputAttributeValue("title", $label);
600                $buttonAttributes->addOutputAttributeValue("target", "_blank");
601                $buttonAttributes->addOutputAttributeValue("rel", "nofollow");
602                $href = $this->getBrandEndpointForPage();
603                if ($href !== null) {
604                    $buttonAttributes->addOutputAttributeValue("href", $href);
605                }
606                return $buttonAttributes;
607            case self::TYPE_BUTTON_BRAND:
608                if ($this->brand->getBrandUrl() !== null) {
609                    $buttonAttributes->addOutputAttributeValue("href", $this->brand->getBrandUrl());
610                }
611                $buttonAttributes->addOutputAttributeValue("title", $label);
612                return $buttonAttributes;
613            default:
614                return $buttonAttributes;
615
616        }
617
618
619    }
620
621
622    public
623    function getType(): string
624    {
625        return $this->type;
626    }
627
628    public function setHandle(string $handle): BrandButton
629    {
630        $this->handle = $handle;
631        return $this;
632    }
633
634    public function setLinkTitle(string $title): BrandButton
635    {
636        $this->title = $title;
637        return $this;
638    }
639
640    public function setPrimaryColor(string $color): BrandButton
641    {
642        $this->primaryColor = $color;
643        return $this;
644    }
645
646    private function getResourceIconFile(): WikiPath
647    {
648        $iconName = $this->getResourceIconName();
649        $iconPath = str_replace(IconDownloader::COMBO, "", $iconName) . ".svg";
650        return WikiPath::createComboResource($iconPath);
651    }
652
653    public function setSecondaryColor(string $secondaryColor): BrandButton
654    {
655        $this->secondaryColor = $secondaryColor;
656        return $this;
657    }
658
659    private function getResourceIconName(): string
660    {
661        $comboLibrary = IconDownloader::COMBO;
662        return "$comboLibrary:brand:{$this->getBrand()->getName()}:{$this->iconType}";
663    }
664
665
666    private function getPrimaryColor(): ?string
667    {
668        if ($this->primaryColor !== null) {
669            return $this->primaryColor;
670        }
671        return $this->brand->getPrimaryColor();
672    }
673
674    private function getSecondaryColor(): ?string
675    {
676        if ($this->secondaryColor !== null) {
677            return $this->secondaryColor;
678        }
679        return $this->brand->getSecondaryColor();
680    }
681
682    /**
683     * The button is sometimes:
684     * * a HTML button
685     * * and other times a HTML link
686     *
687     * It seems that the button is mostly for data-sharer (share button)
688     *
689     * A Link should have an href otherwise the SEO scan will not be happy
690     * A button should have a aria-label
691     *
692     * @param $tagAttributes
693     * @return string
694     */
695    public function getHtmlElement($tagAttributes): string
696    {
697        if ($tagAttributes->hasAttribute("href")) {
698            return "a";
699        } else {
700            return "button";
701        }
702    }
703
704
705}
706