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