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