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 break; 571 default: 572 /** 573 * Sharer 574 * https://ellisonleao.github.io/sharer.js/ 575 */ 576 /** 577 * Opens in a popup 578 */ 579 $buttonAttributes->addOutputAttributeValue("rel", "noopener"); 580 581 PluginUtility::getSnippetManager()->attachRemoteJavascriptLibrary( 582 "sharer", 583 "https://cdn.jsdelivr.net/npm/sharer.js@0.5.0/sharer.min.js", 584 "sha256-AqqY/JJCWPQwZFY/mAhlvxjC5/880Q331aOmargQVLU=" 585 ); 586 $buttonAttributes->addOutputAttributeValue("aria-label", $label); 587 $buttonAttributes->addOutputAttributeValue("data-sharer", $this->getBrand()); // the id 588 $buttonAttributes->addOutputAttributeValue("data-link", "false"); 589 $buttonAttributes->addOutputAttributeValue("data-title", $this->getTextForPage($requestedPage)); 590 $urlToShare = $this->getSharedUrlForPage($requestedPage); 591 $buttonAttributes->addOutputAttributeValue("data-url", $urlToShare); 592 //$linkAttributes->addComponentAttributeValue("href", "#"); // with # we style navigate to the top 593 $buttonAttributes->addStyleDeclarationIfNotSet("cursor", "pointer"); // show a pointer (without href, there is none) 594 } 595 return $buttonAttributes; 596 case self::TYPE_BUTTON_FOLLOW: 597 598 $buttonAttributes->addOutputAttributeValue("title", $label); 599 $buttonAttributes->addOutputAttributeValue("target", "_blank"); 600 $buttonAttributes->addOutputAttributeValue("rel", "nofollow"); 601 $href = $this->getBrandEndpointForPage(); 602 if ($href !== null) { 603 $buttonAttributes->addOutputAttributeValue("href", $href); 604 } 605 return $buttonAttributes; 606 case self::TYPE_BUTTON_BRAND: 607 if ($this->brand->getBrandUrl() !== null) { 608 $buttonAttributes->addOutputAttributeValue("href", $this->brand->getBrandUrl()); 609 } 610 $buttonAttributes->addOutputAttributeValue("title", $label); 611 return $buttonAttributes; 612 default: 613 return $buttonAttributes; 614 615 } 616 617 618 } 619 620 621 public 622 function getType(): string 623 { 624 return $this->type; 625 } 626 627 public function setHandle(string $handle): BrandButton 628 { 629 $this->handle = $handle; 630 return $this; 631 } 632 633 public function setLinkTitle(string $title): BrandButton 634 { 635 $this->title = $title; 636 return $this; 637 } 638 639 public function setPrimaryColor(string $color): BrandButton 640 { 641 $this->primaryColor = $color; 642 return $this; 643 } 644 645 private function getResourceIconFile(): WikiPath 646 { 647 $iconName = $this->getResourceIconName(); 648 $iconPath = str_replace(IconDownloader::COMBO, "", $iconName) . ".svg"; 649 return WikiPath::createComboResource($iconPath); 650 } 651 652 public function setSecondaryColor(string $secondaryColor): BrandButton 653 { 654 $this->secondaryColor = $secondaryColor; 655 return $this; 656 } 657 658 private function getResourceIconName(): string 659 { 660 $comboLibrary = IconDownloader::COMBO; 661 return "$comboLibrary:brand:{$this->getBrand()->getName()}:{$this->iconType}"; 662 } 663 664 665 private function getPrimaryColor(): ?string 666 { 667 if ($this->primaryColor !== null) { 668 return $this->primaryColor; 669 } 670 return $this->brand->getPrimaryColor(); 671 } 672 673 private function getSecondaryColor(): ?string 674 { 675 if ($this->secondaryColor !== null) { 676 return $this->secondaryColor; 677 } 678 return $this->brand->getSecondaryColor(); 679 } 680 681 /** 682 * The button is sometimes: 683 * * a HTML button 684 * * and other times a HTML link 685 * 686 * It seems that the button is mostly for data-sharer (share button) 687 * 688 * A Link should have an href otherwise the SEO scan will not be happy 689 * A button should have a aria-label 690 * 691 * @param $tagAttributes 692 * @return string 693 */ 694 public function getHtmlElement($tagAttributes): string 695 { 696 if ($tagAttributes->hasAttribute("href")) { 697 return "a"; 698 } else { 699 return "button"; 700 } 701 } 702 703 704} 705