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