├── LICENSE.md ├── README.md ├── composer.json ├── config └── share-buttons.php ├── resources ├── css │ └── share-buttons.css └── js │ ├── share-buttons.jquery.js │ └── share-buttons.js └── src ├── Exceptions ├── InvalidOptionValue.php ├── InvalidProcessedCallArgument.php └── InvalidTemplaterFactoryArgument.php ├── Facades └── ShareButtonsFacade.php ├── Presenters ├── Formatters │ ├── AttributesFormatter.php │ └── DefaultAttributesFormatter.php ├── ShareButtonsPresenter.php ├── TemplateBasedBlockPresenter.php ├── TemplateBasedElementPresenter.php ├── TemplateBasedPresenterMediator.php └── TemplateBasedUrlPresenter.php ├── Providers └── ShareButtonsServiceProvider.php ├── ShareButtons.php ├── Templaters ├── LaravelTemplater.php ├── SimpleColonTemplater.php └── Templater.php └── ValueObjects └── ProcessedCall.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Sergey Kudashev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Share Buttons 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/kudashevs/laravel-share-buttons.svg)](https://packagist.org/packages/kudashevs/laravel-share-buttons) 4 | [![Run Tests](https://github.com/kudashevs/laravel-share-buttons/actions/workflows/run-tests.yml/badge.svg)](https://github.com/kudashevs/laravel-share-buttons/actions/workflows/run-tests.yml) 5 | [![License MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE.md) 6 | 7 | This Laravel package provides the possibility to generate share links (social media share buttons) for your site in 8 | a flexible and convenient way within seconds. The package was originated from the Laravel Share. 9 | 10 | [//]: # (@todo don't forget to update these services) 11 | ### Available services 12 | 13 | * Reddit 14 | * LinkedIn 15 | * Facebook 16 | * X (formerly Twitter) 17 | * Bluesky 18 | * Mastodon 19 | * Hacker News 20 | * Tumblr 21 | * Telegram 22 | * WhatsApp 23 | * VKontakte 24 | * Pinterest 25 | * Pocket 26 | * Evernote 27 | * Skype 28 | * Xing 29 | * Copy the link 30 | * Mail the link 31 | 32 | 33 | ## Installation 34 | 35 | You can install the package via composer: 36 | ```bash 37 | composer require kudashevs/laravel-share-buttons 38 | ``` 39 | 40 | If you don't use auto-discovery, just add a ShareButtonsServiceProvider to the `config/app.php` 41 | ```php 42 | 'providers' => [ 43 | Kudashevs\ShareButtons\Providers\ShareButtonsServiceProvider::class, 44 | ], 45 | ``` 46 | By default, the `ShareButtons` class instance is bound to the `sharebuttons` alias. 47 | 48 | **Note**: Don't forget to publish the configuration file (required) and assets. 49 | ```bash 50 | php artisan vendor:publish --provider="Kudashevs\ShareButtons\Providers\ShareButtonsServiceProvider" 51 | ``` 52 | > In case of a major change, it is recommended to back up your config file and republish a new one from scratch. 53 | 54 | If you want to publish certain assets only, use the `--tag` option with one of the following assets tags: `config`, `css`, 55 | `js` (includes all possible js files), `vanilla`, `jquery`. 56 | 57 | 58 | ## Assets 59 | 60 | By default, this package relies on the `Font Awesome` icons. The buttons' interactivity is implemented in two 61 | different ways (via `Vanilla JS` and `jQuery`). However, you can use any custom fonts, icons, or JavaScript. 62 | 63 | ### Font Awesome and default styles 64 | 65 | To enable Font Awesome icons, you can load it from a CDN. For more information on how to use Font Awesome, please read the [introduction](https://fontawesome.com/docs/web/setup/get-started). 66 | ```html 67 | 68 | ``` 69 | 70 | To enable the default styles, [publish](#publish) the `css` asset (the command will create a `resources/css/share-buttons.css` file). 71 | After publishing, you can copy the file to the `public/css` folder and use it directly. Or you can integrate the css file into 72 | your assets building process. 73 | ```html 74 | 75 | ``` 76 | 77 | ### JavaScript 78 | 79 | To enable interaction on social media buttons with JavaScript, [publish](#publish) the `vanilla` asset (the command will create a `resources/js/share-buttons.js` file). 80 | After publishing, you can copy the file to the `public/js` folder and use it directly. Or you can integrate this file into 81 | your assets building process. 82 | ```html 83 | 84 | ``` 85 | 86 | ### jQuery 87 | 88 | To enable interaction on social media buttons with jQuery, [publish](#publish) the `jquery` asset (the command will create a `resources/js/share-buttons.jquery.js` file). 89 | After publishing, you can copy the file to the `public/js` folder and use it directly. Or you can integrate this file into 90 | your assets building process. 91 | ```html 92 | 93 | 94 | ``` 95 | 96 | 97 | ## Usage 98 | 99 | Let's take a look at a short usage example (you can find a detailed usage example in the [corresponding section](#a-detailed-usage-example)). 100 | ```php 101 | ShareButtons::page('https://site.com', 'Page title', [ 102 | 'title' => 'Page title', 103 | 'rel' => 'nofollow noopener noreferrer', 104 | ]) 105 | ->facebook() 106 | ->linkedin(['rel' => 'follow']) 107 | ->render(); 108 | ``` 109 | 110 | The code above will result into the following HTML code: 111 | ```html 112 |
113 | 114 | 115 |
116 | ``` 117 | 118 | ### Fluent interface 119 | 120 | The `ShareButtons` instance provides a fluent interface. The fluent interface is a pattern based on method chaining. 121 | To start a method chaining you just need to use one of the methods listed below (the starting point). 122 | ``` 123 | page($url, $title, $options) # Creates a chaining with a given URL and a given page title 124 | createForPage($url, $title, $options) # Does the same (an alias of the page() method) 125 | currentPage($title, $options) # Creates a chaining with the current page URL and a given page title 126 | createForCurrentPage($title, $options) # Does the same (an alias of the currentPage() method) 127 | ``` 128 | 129 | ### Add buttons 130 | 131 | To generate a single social media button, you just need to add one of the following methods to the [method chaining](#fluent-interface). 132 | Each method accepts an array of options (more information about these options in the [local options](#local-options) section). 133 | 134 | [//]: # (@todo don't forget to update these methods) 135 | ``` 136 | reddit($options) # Generates a Reddit share button 137 | linkedin($options) # Generates a LinkedIn share button 138 | facebook($options) # Generates a Facebook share button 139 | twitter($options) # Generates an X (former Twitter) share button 140 | bluesky($options) # Generates a Bluesky share button 141 | mastodon($options) # Generates a Mastodon share button 142 | hackernews($options) # Generates a Hacker News share button 143 | tumblr($options) # Generates a Tumblr share button 144 | telegram($options) # Generates a Telegram share button 145 | whatsapp($options) # Generates a WhatsApp share button 146 | vkontakte($options) # Generates a VKontakte share button 147 | pinterest($options) # Generates a Pinterest share button 148 | pocket($options) # Generates a Pocket share button 149 | evernote($options) # Generates an Evernote share button 150 | skype($options) # Generates a Skype share button 151 | xing($options) # Generates a Xing share button 152 | copylink($options) # Generates a copy to the clipboard share button 153 | mailto($options) # Generates a send by mail share button 154 | ``` 155 | 156 | These methods are a part of the fluent interface. Therefore, to create multiple social media share buttons you just need to chain them. 157 | 158 | ### Getting share buttons 159 | 160 | You can use a ShareButtons instance as a string or cast it to a string to get ready-to-use HTML code. However, this is not the best way. 161 | If you want to be clear in your intentions, use one of the methods that return generated HTML code. These methods are: 162 | ```php 163 | render() # Returns a generated share buttons HTML code 164 | getShareButtons() # Does the same (an alias of the render() method) 165 | ``` 166 | 167 | ### Getting raw links 168 | 169 | Sometimes, you may only want the raw links without any HTML. In such a case, just use the `getRawLinks` method. 170 | ```php 171 | getRawLinks() # Returns an array of generated links 172 | ``` 173 | 174 | ## Parameters 175 | 176 | There is the possibility of providing different options to style and decorate the resulting HTML code at different levels. 177 | 178 | ### Global options 179 | 180 | Every time a chaining method is called, it accepts several arguments, including a page URL (depending on the method), a page title, 181 | and an array of options. These options are global because they change the representation of all share buttons. These options are: 182 | ``` 183 | 'block_prefix' => 'tag' # Sets a share buttons block prefix (default is
) 184 | 'block_suffix' => 'tag' # Sets a share buttons block suffix (default is
) 185 | 'element_prefix' => 'tag' # Sets an element prefix (default is empty) 186 | 'element_suffix' => 'tag' # Sets an element suffix (default is empty) 187 | 'id' => 'value' # Adds an HTML id attribute to the output links 188 | 'class' => 'value' # Adds an HTML class attribute to the output links 189 | 'title' => 'value' # Adds an HTML title attribute to the output links 190 | 'rel' => 'value' # Adds an HTML rel attribute to the output links 191 | ``` 192 | 193 | ### Local options 194 | 195 | Any of the [share button methods](#add-buttons), that generates a button, accepts several arguments. These options are local 196 | because they will be applied to a specific element only. The local options have a **higher priority**. Therefore, they 197 | will overwrite the global options if there is any overlap. At the moment, the package supports the following local options: 198 | ``` 199 | 'text' => 'value' # Adds a link text to a generated URL (overrides global page title) 200 | 'id' => 'value' # Adds an HTML id attribute to the button link 201 | 'class' => 'value' # Adds an HTML class attribute to the button link 202 | 'title' => 'value' # Adds an HTML title attribute to the button link 203 | 'rel' => 'value' # Adds an HTML rel attribute to the button link 204 | 'summary' => 'value' # Adds a summary text to the URL (linkedin button only) 205 | ``` 206 | 207 | ## Configuration 208 | 209 | The configuration settings are located in the `config/share-buttons.php` file. 210 | 211 | ### Representation section 212 | 213 | This section contains settings related to the "container" in which the social media buttons will be displayed. 214 | ``` 215 | 'block_prefix' => 'tag' # Sets a block prefix (default is
) 216 | 'block_suffix' => 'tag' # Sets a block suffix (default is
) 217 | 'element_prefix' => 'tag' # Sets an element prefix (default is empty) 218 | 'element_suffix' => 'tag' # Sets an element suffix (default is empty) 219 | ``` 220 | 221 | ### Share buttons section 222 | 223 | Each social media share button has its own individual configuration settings. 224 | ``` 225 | 'url' => 'value' # A share button URL template (is used to generate a button's URL) 226 | 'text' => 'value' # A default text to be added to the url (is used when the page title is empty) 227 | 'extra' => [ # Extra options that are required by some specific buttons 228 | 'summary' => 'value' # A default summary to be added to the url (linkedin only) 229 | 'raw' => 'value' # A boolean defines whether to skip the URL-encoding of the url 230 | 'hash' => 'value' # A boolean defines whether to use a hash instead of the url 231 | ] 232 | ``` 233 | **Note**: a text value might contain a `url` element, which will be replaced by the page url while processing. 234 | 235 | ### Templates section 236 | 237 | Each share button has a corresponding link template. A template contains several elements that will be substituted with 238 | data from different arguments and options. The format of these elements depends on the `templater` setting. By default, 239 | these elements are: 240 | ``` 241 | :url # Will be replaced with a prepared URL 242 | :id # Will be replaced with an id attribute 243 | :class # Will be replaced with a class attribute 244 | :title # Will be replaced with a title attribute 245 | :rel # Will be replaced with a rel attribute 246 | ``` 247 | 248 | ### Templaters section 249 | 250 | For processing different templates and substitute elements in them, the package uses templaters (template engines). 251 | By default, these options are optional (if no value provided, the default templater will be used). 252 | ``` 253 | 'templater' # A template engine for processing link templates 254 | 'url_templater' # A template engine for processing share buttons URLs 255 | ``` 256 | 257 | 258 | ## A detailed usage example 259 | 260 | To summarize all of the information from above, let's take a look at a real-life example. We begin with one of the methods 261 | that start the fluent interface, and we provide some global options. Then, we add some specific methods that generate social 262 | media share buttons. At this step, we can provide any local options, as it is done in the `linkedin()` method. Finally, 263 | we finish the fluent interface chain with one of the methods that return the resulting HTML code. 264 | ```php 265 | ShareButtons::page('https://site.com', 'Page title', [ 266 | 'block_prefix' => '', 268 | 'element_prefix' => '
  • ', 269 | 'element_suffix' => '
  • ', 270 | 'class' => 'my-class', 271 | 'id' => 'my-id', 272 | 'title' => 'my-title', 273 | 'rel' => 'nofollow noopener noreferrer', 274 | ]) 275 | ->facebook() 276 | ->linkedin(['id' => 'linked', 'class' => 'hover', 'rel' => 'follow', 'summary' => 'cool summary']) 277 | ->render(); 278 | ``` 279 | 280 | The code above will result into the following HTML code: 281 | ```html 282 | 286 | ``` 287 | 288 | 289 | ## Testing 290 | 291 | ```bash 292 | composer test 293 | ``` 294 | 295 | ## Contributing 296 | 297 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 298 | 299 | ## License 300 | 301 | The MIT License (MIT). Please see the [License file](LICENSE.md) for more information. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kudashevs/laravel-share-buttons", 3 | "description": "A Laravel social media share buttons package.", 4 | "keywords": [ 5 | "laravel", 6 | "share", 7 | "share buttons", 8 | "social links" 9 | ], 10 | "homepage": "https://github.com/kudashevs/laravel-share-buttons", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Sergey Kudashev", 15 | "email": "kudashevs@gmail.com", 16 | "homepage": "https://kudashevs.com", 17 | "role": "developer" 18 | }, 19 | { 20 | "name": "Joren Van Hocht", 21 | "email": "joren@codeswitch.be", 22 | "homepage": "https://codeswitch.be", 23 | "role": "creator" 24 | } 25 | ], 26 | "require": { 27 | "php": "^8.1" 28 | }, 29 | "require-dev": { 30 | "orchestra/testbench": "^8.0|^9.0|^10.0", 31 | "phpstan/phpstan": "^1.10", 32 | "phpunit/phpunit": "^10.1|^11.0|^12.0" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Kudashevs\\ShareButtons\\": "src/" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Kudashevs\\ShareButtons\\Tests\\": "tests/" 42 | } 43 | }, 44 | "config": { 45 | "sort-packages": true 46 | }, 47 | "scripts": { 48 | "test": "vendor/bin/phpunit --no-coverage", 49 | "test-coverage": "vendor/bin/phpunit --coverage-html build/coverage", 50 | "analyze": "phpstan analyze --no-progress --memory-limit=1024M" 51 | }, 52 | "extra": { 53 | "laravel": { 54 | "providers": [ 55 | "Kudashevs\\ShareButtons\\Providers\\ShareButtonsServiceProvider" 56 | ], 57 | "aliases": { 58 | "ShareButtons": "Kudashevs\\ShareButtons\\Facades\\ShareButtonsFacade" 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /config/share-buttons.php: -------------------------------------------------------------------------------- 1 | '
    ', 22 | 'block_suffix' => '
    ', 23 | 'element_prefix' => '', 24 | 'element_suffix' => '', 25 | 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Share buttons 29 | |-------------------------------------------------------------------------- 30 | | 31 | | These values specify configuration settings for each social media button. 32 | | The settings include a sharing url, a default text in the url, some extras. 33 | | The format of substitution depends on a templater (see Templaters section). 34 | | Note: It is allowed to provide a site's url to the copylink button, because 35 | | some people might want to see it there, even though using a hash is enough. 36 | | 37 | */ 38 | 39 | 'buttons' => [ 40 | 'bluesky' => [ 41 | 'url' => 'https://bsky.app/intent/compose?text=:url%20:text', 42 | 'text' => 'Default share text', 43 | ], 44 | 'copylink' => [ 45 | 'url' => ':url', 46 | 'extra' => [ 47 | 'raw' => 'true', 48 | 'hash' => 'true', 49 | ], 50 | ], 51 | 'evernote' => [ 52 | 'url' => 'https://www.evernote.com/clip.action?url=:url&t=:text', 53 | 'text' => 'Default share text', 54 | ], 55 | 'facebook' => [ 56 | 'url' => 'https://www.facebook.com/sharer/sharer.php?u=:url"e=:text', 57 | 'text' => 'Default share text', 58 | ], 59 | 'hackernews' => [ 60 | 'url' => 'https://news.ycombinator.com/submitlink?t=:text&u=:url', 61 | 'text' => 'Default share text', 62 | ], 63 | 'linkedin' => [ 64 | 'url' => 'https://www.linkedin.com/sharing/share-offsite?mini=true&url=:url&title=:text&summary=:summary', 65 | 'text' => 'Default share text', 66 | 'extra' => [ 67 | 'summary' => '', 68 | ], 69 | ], 70 | 'mailto' => [ 71 | 'url' => 'mailto:?subject=:text&body=:url', 72 | 'text' => 'Default share text', 73 | ], 74 | 'mastodon' => [ 75 | 'url' => 'https://mastodon.social/share?text=:text&url=:url', 76 | 'text' => 'Default share text', 77 | ], 78 | 'pinterest' => [ 79 | 'url' => 'https://pinterest.com/pin/create/button/?url=:url', 80 | ], 81 | 'pocket' => [ 82 | 'url' => 'https://getpocket.com/edit?url=:url&title=:text', 83 | 'text' => 'Default share text', 84 | ], 85 | 'reddit' => [ 86 | 'url' => 'https://www.reddit.com/submit?title=:text&url=:url', 87 | 'text' => 'Default share text', 88 | ], 89 | 'skype' => [ 90 | 'url' => 'https://web.skype.com/share?url=:url&text=:text&source=button', 91 | 'text' => 'Default share text', 92 | ], 93 | 'telegram' => [ 94 | 'url' => 'https://telegram.me/share/url?url=:url&text=:text', 95 | 'text' => 'Default share text', 96 | ], 97 | 'tumblr' => [ 98 | 'url' => 'https://www.tumblr.com/share?v=3&u=:url&t=:text', 99 | 'text' => 'Default share text', 100 | ], 101 | 'twitter' => [ 102 | 'url' => 'https://twitter.com/intent/tweet?text=:text&url=:url', 103 | 'text' => 'Default share text', 104 | ], 105 | 'vkontakte' => [ 106 | 'url' => 'https://vk.com/share.php?url=:url&title=:text', 107 | 'text' => 'Default share text', 108 | ], 109 | 'whatsapp' => [ 110 | 'url' => 'https://wa.me/?text=:url%20:text', 111 | 'text' => 'Default share text', 112 | ], 113 | 'xing' => [ 114 | 'url' => 'https://www.xing.com/spi/shares/new?url=:url', 115 | ], 116 | ], 117 | 118 | /* 119 | |-------------------------------------------------------------------------- 120 | | Templates 121 | |-------------------------------------------------------------------------- 122 | | 123 | | These values specify link templates for each of the social media buttons. 124 | | The format of substitution depends on a templater (see Templaters section). 125 | | Note: Don't remove the social-button class from links because it's used in js. 126 | | 127 | */ 128 | 129 | 'templates' => [ 130 | 'bluesky' => '', 131 | 'copylink' => '', 132 | 'evernote' => '', 133 | 'facebook' => '', 134 | 'hackernews' => '', 135 | 'linkedin' => '', 136 | 'mailto' => '', 137 | 'mastodon' => '', 138 | 'pinterest' => '', 139 | 'pocket' => '', 140 | 'reddit' => '', 141 | 'skype' => '', 142 | 'telegram' => '', 143 | 'tumblr' => '', 144 | 'twitter' => '', 145 | 'vkontakte' => '', 146 | 'whatsapp' => '', 147 | 'xing' => '', 148 | ], 149 | 150 | /* 151 | |-------------------------------------------------------------------------- 152 | | Templaters 153 | |-------------------------------------------------------------------------- 154 | | 155 | | This package uses a simple template engine to substitute values in different 156 | | configuration settings and templates. If you want to change the substitution 157 | | format, feel free to use your favorite template engine (in this case it is 158 | | recommended to introduce an adapter that conforms to the Templater interface). 159 | | 160 | */ 161 | 162 | 'templater' => \Kudashevs\ShareButtons\Templaters\LaravelTemplater::class, 163 | 164 | 'url_templater' => \Kudashevs\ShareButtons\Templaters\LaravelTemplater::class, 165 | 166 | ]; 167 | -------------------------------------------------------------------------------- /resources/css/share-buttons.css: -------------------------------------------------------------------------------- 1 | #social-buttons { 2 | } 3 | 4 | #social-buttons a { 5 | display:inline-block; 6 | margin:0.125em; 7 | font-size:1.5em; 8 | list-style:none; 9 | } 10 | 11 | #social-buttons a:hover { 12 | -webkit-transform:scale(1.1); 13 | -moz-transform:scale(1.1); 14 | -ms-transform:scale(1.1); 15 | -o-transform:scale(1.1); 16 | transform:scale(1.1); 17 | } 18 | 19 | .fa-square-bluesky {color:#1185fe;} 20 | .fa-share {color:#1977d4;} 21 | .fa-evernote {color:#1fb655;} 22 | .fa-facebook, .fa-facebook-square {color:#3c599f;} 23 | .fa-hacker-news {color:#f37022;} 24 | .fa-linkedin {color:#0085ae;} 25 | .fa-envelope {color:#ea4445;} 26 | .fa-mastodon {color:#563acc;} 27 | .fa-pinterest {color:#cb2027;} 28 | .fa-get-pocket {color:#ee4056;} 29 | .fa-reddit {color:#ff4500;} 30 | .fa-skype {color:#01aef2;} 31 | .fa-telegram {color:#0088cc;} 32 | .fa-square-tumblr {color:#36465d;} 33 | .fa-twitter, .fa-square-twitter {color:#00aced;} 34 | .fa-x-twitter, .fa-square-x-twitter {color:#000;} 35 | .fa-vk {color:#375474;} 36 | .fa-whatsapp, .fa-square-whatsapp {color:#4fce5d;} 37 | .fa-xing, .fa-square-xing {color:#00555c;} 38 | -------------------------------------------------------------------------------- /resources/js/share-buttons.jquery.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | const popupWidth = 780; 3 | const popupHeight = 550; 4 | 5 | $(document).on('click', '.social-button', function (e) { 6 | if ((e.target.id || e.target.parentElement.id) === 'clip') { 7 | e.preventDefault(); 8 | if (window.clipboardData && window.clipboardData.setData) { 9 | clipboardData.setData("Text", this.href); 10 | } else { 11 | let textArea = document.createElement("textarea"); 12 | textArea.value = this.href; 13 | document.body.appendChild(textArea); 14 | textArea.select(); 15 | document.execCommand("copy"); // Security exception may be thrown by some browsers. 16 | textArea.remove(); 17 | } 18 | return; 19 | } 20 | 21 | let vPosition = Math.floor(($(window).width() - popupWidth) / 2), 22 | hPosition = Math.floor(($(window).height() - popupHeight) / 2); 23 | 24 | let popup = window.open($(this).prop('href'), 'social', 25 | 'width=' + popupWidth + ',height=' + popupHeight + 26 | ',left=' + vPosition + ',top=' + hPosition + 27 | ',location=0,menubar=0,toolbar=0,status=0,scrollbars=1,resizable=1'); 28 | 29 | if (popup) { 30 | popup.focus(); 31 | e.preventDefault(); 32 | } 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /resources/js/share-buttons.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", () => { 2 | let socialButtons = document.querySelectorAll('.social-button'); 3 | 4 | socialButtons.forEach((button) => { 5 | button.addEventListener('click', socialButtonClickHandler) 6 | }) 7 | }); 8 | 9 | function socialButtonClickHandler(e) { 10 | const popupWidth = 780; 11 | const popupHeight = 550; 12 | 13 | let el = identifyTargetElement(e, provideMissingTargetElementHandler(e)); 14 | if (el === undefined) return; 15 | 16 | if (el.id === 'clip') { 17 | e.preventDefault(); 18 | e.stopImmediatePropagation(); 19 | if (window.clipboardData && window.clipboardData.setData) { 20 | clipboardData.setData("Text", el.href); 21 | } else { 22 | let textArea = document.createElement("textarea"); 23 | textArea.value = el.href; 24 | document.body.appendChild(textArea); 25 | textArea.select(); 26 | document.execCommand("copy"); // Security exception may be thrown by some browsers. 27 | textArea.remove(); 28 | } 29 | return; 30 | } 31 | 32 | const windowWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; 33 | const windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; 34 | 35 | const vPosition = Math.floor((windowWidth - popupWidth) / 2), 36 | hPosition = Math.floor((windowHeight - popupHeight) / 2); 37 | 38 | const popup = window.open(el.href, 'social', 39 | 'width=' + popupWidth + ',height=' + popupHeight + 40 | ',left=' + vPosition + ',top=' + hPosition + 41 | ',location=0,menubar=0,toolbar=0,status=0,scrollbars=1,resizable=1'); 42 | 43 | if (popup) { 44 | e.preventDefault(); 45 | e.stopImmediatePropagation(); 46 | popup.focus(); 47 | } 48 | } 49 | 50 | function identifyTargetElement(e, cb) { 51 | let buttonClassName = 'social-button'; 52 | 53 | if ( 54 | e.target.parentElement && 55 | e.target.parentElement.className.indexOf(buttonClassName) !== -1 56 | ) { 57 | return e.target.parentElement; 58 | } 59 | 60 | if ( 61 | e.target.className.indexOf(buttonClassName) !== -1 62 | ) { 63 | return e.target; 64 | } 65 | 66 | typeof cb === 'function' && cb(buttonClassName) 67 | } 68 | 69 | // this function can be modified to handle a missing target element 70 | // don't remove it because it is used in the socialButtonClickHandler 71 | function provideMissingTargetElementHandler(e) { 72 | // returns a function with an enclosing e variable (contains a triggered event) 73 | // accepts a name argument (contains a classname used to identify a target element) 74 | return (name) => { 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidOptionValue.php: -------------------------------------------------------------------------------- 1 | ' %s', 11 | 'id' => ' id="%s"', 12 | 'title' => ' title="%s"', 13 | 'rel' => ' rel="%s"', 14 | ]; 15 | 16 | /** 17 | * @inheritDoc 18 | */ 19 | public function format(array $attributes): array 20 | { 21 | $formattedAttributes = []; 22 | 23 | // Since we need all the attributes to be present in the result, we iterate over 24 | // the attributes listed in the constant, rather than the provided attributes. 25 | foreach (self::KNOWN_ATTRIBUTE_FORMATS as $name => $format) { 26 | $formattedAttributes[$name] = isset($attributes[$name]) 27 | ? sprintf($format, trim($attributes[$name])) 28 | : ''; 29 | } 30 | 31 | return $formattedAttributes; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Presenters/ShareButtonsPresenter.php: -------------------------------------------------------------------------------- 1 | '', 16 | 'block_suffix' => '', 17 | ]; 18 | 19 | public function __construct() 20 | { 21 | $this->initRepresentation(); 22 | } 23 | 24 | /** 25 | * @param array{block_prefix?: string, block_suffix?: string} $options 26 | */ 27 | protected function initRepresentation(array $options = []): void 28 | { 29 | $applicable = $this->retrieveApplicableOptions($options); 30 | 31 | $this->initBlockRepresentation($applicable); 32 | } 33 | 34 | /** 35 | * @param array $options 36 | * @return array 37 | */ 38 | protected function retrieveApplicableOptions(array $options): array 39 | { 40 | return array_filter($options, 'is_string'); 41 | } 42 | 43 | /** 44 | * @param array{block_prefix?: string, block_suffix?: string} $options 45 | */ 46 | protected function initBlockRepresentation(array $options): void 47 | { 48 | $this->styling['block_prefix'] = $options['block_prefix'] ?? config('share-buttons.block_prefix', ''); 50 | } 51 | 52 | /** 53 | * Refresh styling (style of elements representation) of the share buttons. 54 | * 55 | * @param array{block_prefix?: string, block_suffix?: string} $options 56 | * @return void 57 | */ 58 | public function refresh(array $options = []): void 59 | { 60 | $this->initRepresentation($options); 61 | } 62 | 63 | /** 64 | * @see TemplateBasedPresenterMediator::getBlockPrefix() 65 | */ 66 | public function getBlockPrefix(): string 67 | { 68 | return $this->styling['block_prefix']; 69 | } 70 | 71 | /** 72 | * @see TemplateBasedPresenterMediator::getBlockSuffix() 73 | */ 74 | public function getBlockSuffix(): string 75 | { 76 | return $this->styling['block_suffix']; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Presenters/TemplateBasedElementPresenter.php: -------------------------------------------------------------------------------- 1 | '', 24 | 'element_suffix' => '', 25 | ]; 26 | 27 | /** 28 | * Contain attributes passed to the page() method (the global attributes). These attributes will be 29 | * automatically applied to all the elements in the case where no specific attributes are provided. 30 | * 31 | * @var array{class?: string, id?: string, title?: string, rel?: string} 32 | */ 33 | protected array $attributes = []; 34 | 35 | public function __construct(Templater $templater) 36 | { 37 | $this->templater = $templater; 38 | 39 | $this->initAttributesFormatter(); 40 | 41 | $this->initRepresentation(); 42 | } 43 | 44 | protected function initAttributesFormatter(): void 45 | { 46 | $this->formatter = new DefaultAttributesFormatter(); 47 | } 48 | 49 | /** 50 | * @param array{element_prefix?: string, element_suffix?: string, id?: string, class?: string, title?: string, rel?: string} $options 51 | */ 52 | protected function initRepresentation(array $options = []): void 53 | { 54 | $applicable = $this->retrieveApplicableOptions($options); 55 | 56 | $this->initElementRepresentation($applicable); 57 | $this->initElementAttributes($applicable); 58 | } 59 | 60 | /** 61 | * @param array $options 62 | * @return array 63 | */ 64 | protected function retrieveApplicableOptions(array $options): array 65 | { 66 | return array_filter($options, 'is_string'); 67 | } 68 | 69 | /** 70 | * @param array{element_prefix?: string, element_suffix?: string} $options 71 | */ 72 | protected function initElementRepresentation(array $options): void 73 | { 74 | $this->styling['element_prefix'] = $options['element_prefix'] ?? config('share-buttons.element_prefix', '
  • '); 75 | $this->styling['element_suffix'] = $options['element_suffix'] ?? config('share-buttons.element_suffix', '
  • '); 76 | } 77 | 78 | /** 79 | * @param array{id?: string, class?: string, title?: string, rel?: string} $options 80 | */ 81 | protected function initElementAttributes(array $options): void 82 | { 83 | $this->attributes = array_diff_key($options, $this->styling); 84 | } 85 | 86 | /** 87 | * Refresh styling (style of elements representation) of the share buttons. 88 | * 89 | * @param array{element_prefix?: string, element_suffix?: string, id?: string, class?: string, title?: string, rel?: string} $options 90 | * @return void 91 | */ 92 | public function refresh(array $options = []): void 93 | { 94 | $this->initRepresentation($options); 95 | } 96 | 97 | /** 98 | * @see TemplateBasedPresenterMediator::getElementPrefix() 99 | */ 100 | public function getElementPrefix(): string 101 | { 102 | return $this->styling['element_prefix']; 103 | } 104 | 105 | /** 106 | * @see TemplateBasedPresenterMediator::getElementSuffix() 107 | */ 108 | public function getElementSuffix(): string 109 | { 110 | return $this->styling['element_suffix']; 111 | } 112 | 113 | /** 114 | * @param array{url: string, text: string, id?: string, class?: string, title?: string, rel?: string, summary?: string} $arguments 115 | * @see TemplateBasedPresenterMediator::getElementBody() 116 | */ 117 | public function getElementBody(string $name, array $arguments): string 118 | { 119 | $template = $this->retrieveElementTemplate($name); 120 | $replacements = $this->retrieveReplacements($arguments); 121 | 122 | return $this->templater->process($template, $replacements); 123 | } 124 | 125 | protected function retrieveElementTemplate(string $name): string 126 | { 127 | return config('share-buttons.templates.' . $name, ''); 128 | } 129 | 130 | /** 131 | * @param array{url: string, text: string, id?: string, class?: string, title?: string, rel?: string, summary?: string} $arguments 132 | * @return array{url: string, id: string, class: string, title: string, rel: string} 133 | */ 134 | protected function retrieveReplacements(array $arguments): array 135 | { 136 | $elementAttributes = $this->prepareAttributes($arguments); 137 | 138 | return array_merge([ 139 | 'url' => $arguments['url'], 140 | ], $elementAttributes); 141 | } 142 | 143 | /** 144 | * Prepare element's attributes. The preparation process includes: 145 | * - format the attributes according to the formatter's rules 146 | * 147 | * @param array{url: string, text: string, id?: string, class?: string, title?: string, rel?: string, summary?: string} $arguments 148 | * @return array{class: string, id: string, title: string, rel: string} 149 | */ 150 | protected function prepareAttributes(array $arguments): array 151 | { 152 | $attributes = array_merge($this->attributes, $arguments); 153 | 154 | return $this->formatter->format($attributes); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Presenters/TemplateBasedPresenterMediator.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | const DEFAULT_TEMPLATER_CLASS = SimpleColonTemplater::class; 17 | 18 | protected Templater $templater; 19 | 20 | protected TemplateBasedBlockPresenter $blockPresenter; 21 | 22 | protected TemplateBasedElementPresenter $elementPresenter; 23 | 24 | protected TemplateBasedUrlPresenter $urlPresenter; 25 | 26 | /** 27 | * @param array{templater?: class-string, url_templater?: class-string} $options 28 | * 29 | * @throws InvalidOptionValue 30 | */ 31 | public function __construct(array $options = []) 32 | { 33 | $this->initMediator($options); 34 | } 35 | 36 | /** 37 | * @param array{templater?: class-string, url_templater?: class-string} $options 38 | * 39 | * @throws InvalidOptionValue 40 | */ 41 | protected function initMediator(array $options): void 42 | { 43 | $this->blockPresenter = new TemplateBasedBlockPresenter(); 44 | 45 | $urlTemplaterClass = $options['url_templater'] ?? self::DEFAULT_TEMPLATER_CLASS; 46 | $urlTemplaterInstance = $this->createTemplater($urlTemplaterClass); 47 | $this->urlPresenter = new TemplateBasedUrlPresenter($urlTemplaterInstance); 48 | 49 | $templaterClass = $options['templater'] ?? self::DEFAULT_TEMPLATER_CLASS; 50 | $templaterInstance = $this->createTemplater($templaterClass); 51 | $this->elementPresenter = new TemplateBasedElementPresenter($templaterInstance); 52 | } 53 | 54 | /** 55 | * @throws InvalidOptionValue 56 | */ 57 | protected function createTemplater(string $class): Templater 58 | { 59 | /** @var class-string $class */ 60 | if (!$this->isValidTemplater($class)) { 61 | throw new InvalidOptionValue( 62 | sprintf( 63 | '%s is not a valid templater class. Check if it implements the Templater interface.', 64 | $class 65 | ) 66 | ); 67 | } 68 | 69 | return new $class(); 70 | } 71 | 72 | private function isValidTemplater(string $class): bool 73 | { 74 | return class_exists($class) && is_a($class, Templater::class, true); 75 | } 76 | 77 | /** 78 | * @inheritDoc 79 | */ 80 | public function refresh(array $options): void 81 | { 82 | $this->blockPresenter->refresh($options); 83 | $this->elementPresenter->refresh($options); 84 | } 85 | 86 | /** 87 | * @inheritDoc 88 | */ 89 | public function getBlockPrefix(): string 90 | { 91 | return $this->blockPresenter->getBlockPrefix(); 92 | } 93 | 94 | /** 95 | * @inheritDoc 96 | */ 97 | public function getBlockSuffix(): string 98 | { 99 | return $this->blockPresenter->getBlockSuffix(); 100 | } 101 | 102 | /** 103 | * @inheritDoc 104 | */ 105 | public function getElementPrefix(): string 106 | { 107 | return $this->elementPresenter->getElementPrefix(); 108 | } 109 | 110 | /** 111 | * @inheritDoc 112 | */ 113 | public function getElementSuffix(): string 114 | { 115 | return $this->elementPresenter->getElementSuffix(); 116 | } 117 | 118 | /** 119 | * @inheritDoc 120 | */ 121 | public function getElementBody(string $name, array $arguments): string 122 | { 123 | $preparedArguments = $this->prepareElementArguments($name, $arguments); 124 | 125 | return $this->elementPresenter->getElementBody($name, $preparedArguments); 126 | } 127 | 128 | /** 129 | * Prepare element's arguments. The preparation process includes: 130 | * - convert a provided URL to a representation of the element's URL 131 | * 132 | * @param array{url: string, text: string, id?: string, class?: string, title?: string, rel?: string, summary?: string} $arguments 133 | * @return array{url: string, text: string, id?: string, class?: string, title?: string, rel?: string, summary?: string} 134 | */ 135 | protected function prepareElementArguments(string $name, array $arguments): array 136 | { 137 | return array_merge( 138 | $arguments, 139 | ['url' => $this->getElementUrl($name, $arguments)], 140 | ); 141 | } 142 | 143 | /** 144 | * @inheritDoc 145 | */ 146 | public function getElementUrl(string $name, array $arguments): string 147 | { 148 | return $this->urlPresenter->generateUrl($name, $arguments); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Presenters/TemplateBasedUrlPresenter.php: -------------------------------------------------------------------------------- 1 | templater = $templater; 21 | } 22 | 23 | /** 24 | * Return a share button's ready-to-use URL. 25 | * 26 | * @param string $name 27 | * @param array{url: string, text: string, summary?: string, ...} $arguments 28 | * @return string 29 | */ 30 | public function generateUrl(string $name, array $arguments): string 31 | { 32 | $this->initElementName($name); 33 | 34 | $template = $this->retrieveUrlTemplate(); 35 | $replacements = $this->retrieveUrlReplacements($arguments); 36 | 37 | $processedReplacements = $this->selfProcessReplacements($replacements); 38 | $encodedReplacements = $this->encodeReplacements($processedReplacements); 39 | 40 | return $this->templater->process($template, $encodedReplacements); 41 | } 42 | 43 | protected function initElementName(string $name): void 44 | { 45 | $this->name = $name; 46 | } 47 | 48 | protected function retrieveUrlTemplate(): string 49 | { 50 | $url = config('share-buttons.buttons.' . $this->name . '.url', ''); 51 | 52 | return $this->isHash() 53 | ? '#' 54 | : $url; 55 | } 56 | 57 | protected function isHash(): bool 58 | { 59 | return config()->has('share-buttons.buttons.' . $this->name . '.extra.hash') 60 | && config('share-buttons.buttons.' . $this->name . '.extra.hash') === true; 61 | } 62 | 63 | /** 64 | * @param array $arguments 65 | * @return array 66 | */ 67 | protected function retrieveUrlReplacements(array $arguments): array 68 | { 69 | $elementReplacements = $this->retrieveElementReplacements(); 70 | $applicableArguments = $this->retrieveApplicableArguments($arguments); 71 | 72 | // The arguments override replacements because they have a higher priority. 73 | return array_merge($elementReplacements, $applicableArguments); 74 | } 75 | 76 | /** 77 | * @return array 78 | */ 79 | protected function retrieveElementReplacements(): array 80 | { 81 | return array_merge([ 82 | 'text' => $this->retrieveText(), 83 | ], $this->retrieveExtras()); 84 | } 85 | 86 | /** 87 | * @param array $arguments 88 | * @return array 89 | */ 90 | protected function retrieveApplicableArguments(array $arguments): array 91 | { 92 | return array_filter($arguments, function ($value, $key) { 93 | return is_string($value) && ($value !== '' || $key === 'url'); 94 | }, ARRAY_FILTER_USE_BOTH); 95 | } 96 | 97 | protected function retrieveText(): string 98 | { 99 | return config('share-buttons.buttons.' . $this->name . '.text', ''); 100 | } 101 | 102 | /** 103 | * @return array 104 | */ 105 | protected function retrieveExtras(): array 106 | { 107 | $extras = config('share-buttons.buttons.' . $this->name . '.extra', []); 108 | 109 | return array_diff_key($extras, array_flip(self::EXTRA_EXCLUSIONS)); 110 | } 111 | 112 | /** 113 | * Process retrieved replacements. The self-processing includes: 114 | * - replace an url element with a provided page URL 115 | * 116 | * @param array{url: string, text: string, summary?: string, ...} $replacements 117 | * @return array{url: string, text: string, summary?: string, ...} 118 | */ 119 | protected function selfProcessReplacements(array $replacements): array 120 | { 121 | if (array_key_exists('text', $replacements)) { 122 | $replacements['text'] = $this->selfProcessText($replacements); 123 | } 124 | 125 | return $replacements; 126 | } 127 | 128 | /** 129 | * @param array{url: string, text: string, summary?: string, ...} $replacements 130 | * @return string 131 | */ 132 | protected function selfProcessText(array $replacements): string 133 | { 134 | return $this->templater->process( 135 | $replacements['text'], 136 | $replacements, 137 | ); 138 | } 139 | 140 | /** 141 | * @param array $replacements 142 | * @return array 143 | */ 144 | protected function encodeReplacements(array $replacements): array 145 | { 146 | return array_map(function (string $value): string { 147 | return $this->isRaw() 148 | ? (string)$value 149 | : urlencode($value); 150 | }, $replacements); 151 | } 152 | 153 | protected function isRaw(): bool 154 | { 155 | return config()->has('share-buttons.buttons.' . $this->name . '.extra.raw') 156 | || config('share-buttons.buttons.' . $this->name . '.extra.raw') === true; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Providers/ShareButtonsServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 19 | __DIR__ . '/../../config/share-buttons.php' => config_path('share-buttons.php'), 20 | ], 'config'); 21 | 22 | $this->publishes([ 23 | __DIR__ . '/../../resources/js/share-buttons.js' => resource_path('js/share-buttons.js'), 24 | ], ['js', 'vanilla']); 25 | 26 | $this->publishes([ 27 | __DIR__ . '/../../resources/js/share-buttons.jquery.js' => resource_path('js/share-buttons.jquery.js'), 28 | ], ['js', 'jquery']); 29 | 30 | $this->publishes([ 31 | __DIR__ . '/../../resources/css/share-buttons.css' => resource_path('css/share-buttons.css'), 32 | ], ['css']); 33 | } 34 | 35 | /** 36 | * Register the application services. 37 | */ 38 | public function register(): void 39 | { 40 | $this->app->bind(ShareButtons::class, fn() => new ShareButtons($this->retrieveOptions())); 41 | $this->app->alias(ShareButtons::class, 'sharebuttons'); 42 | 43 | $this->mergeConfigFrom(__DIR__ . '/../../config/share-buttons.php', 'share-buttons'); 44 | } 45 | 46 | /** 47 | * @return array{templater?: class-string, url_templater?: class-string} 48 | */ 49 | protected function retrieveOptions(): array 50 | { 51 | return [ 52 | 'templater' => config('share-buttons.templater'), 53 | 'url_templater' => config('share-buttons.url_templater'), 54 | ]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/ShareButtons.php: -------------------------------------------------------------------------------- 1 | 53 | */ 54 | protected array $calls = []; 55 | 56 | /** 57 | * @param array{templater?: class-string, url_templater?: class-string} $options 58 | * 59 | * @throws InvalidOptionValue 60 | */ 61 | public function __construct(array $options = []) 62 | { 63 | $this->initPresenter($options); 64 | } 65 | 66 | /** 67 | * @param array{templater?: class-string, url_templater?: class-string} $options 68 | * 69 | * @throws InvalidOptionValue 70 | */ 71 | protected function initPresenter(array $options): void 72 | { 73 | $this->presenter = $this->createPresenter($options); 74 | } 75 | 76 | /** 77 | * @param array{templater?: class-string, url_templater?: class-string} $options 78 | * 79 | * @throws InvalidOptionValue 80 | */ 81 | protected function createPresenter(array $options): ShareButtonsPresenter 82 | { 83 | return new TemplateBasedPresenterMediator($options); 84 | } 85 | 86 | /** 87 | * @param string $url 88 | * @param string $title 89 | * @param array{block_prefix?: string, block_suffix?: string, element_prefix?: string, element_suffix?: string, id?: string, class?: string, title?: string, rel?: string} $options 90 | * @return $this 91 | */ 92 | public function page(string $url, string $title = '', array $options = []): static 93 | { 94 | $this->refreshState($options); 95 | 96 | $this->pageUrl = $url; 97 | $this->pageTitle = $title; 98 | 99 | return $this; 100 | } 101 | 102 | /** 103 | * Refresh state and delete all previously remembered calls. 104 | * 105 | * @param array{block_prefix?: string, block_suffix?: string, element_prefix?: string, element_suffix?: string, id?: string, class?: string, title?: string, rel?: string} $options 106 | */ 107 | protected function refreshState(array $options): void 108 | { 109 | $this->presenter->refresh($options); 110 | $this->calls = []; 111 | } 112 | 113 | /** 114 | * @param string $title 115 | * @param array{block_prefix?: string, block_suffix?: string, element_prefix?: string, element_suffix?: string, id?: string, class?: string, title?: string, rel?: string} $options 116 | * @return $this 117 | */ 118 | public function currentPage(string $title = '', array $options = []): static 119 | { 120 | $url = app('request')->url(); 121 | 122 | return $this->page($url, $title, $options); 123 | } 124 | 125 | /** 126 | * @param string $url 127 | * @param string $title 128 | * @param array{block_prefix?: string, block_suffix?: string, element_prefix?: string, element_suffix?: string, id?: string, class?: string, title?: string, rel?: string} $options 129 | * @return $this 130 | */ 131 | public function createForPage(string $url, string $title = '', array $options = []): static 132 | { 133 | return $this->page($url, $title, $options); 134 | } 135 | 136 | /** 137 | * @param string $title 138 | * @param array{block_prefix?: string, block_suffix?: string, element_prefix?: string, element_suffix?: string, id?: string, class?: string, title?: string, rel?: string} $options 139 | * @return $this 140 | */ 141 | public function createForCurrentPage(string $title = '', array $options = []): static 142 | { 143 | return $this->currentPage($title, $options); 144 | } 145 | 146 | /** 147 | * @param string $name 148 | * @param array> $arguments 149 | * @return mixed|ShareButtons 150 | * 151 | * @throws BadMethodCallException 152 | */ 153 | public function __call(string $name, array $arguments): mixed 154 | { 155 | if ($this->isExpectedCall($name)) { 156 | $applicableArguments = $this->retrieveCallArguments($arguments); 157 | $prioritizedArguments = $this->prioritizeArguments($applicableArguments); 158 | 159 | $this->rememberProcessedCall($name, $prioritizedArguments); 160 | } else { 161 | $this->handleUnexpectedCall($name); 162 | } 163 | 164 | return $this; 165 | } 166 | 167 | protected function isExpectedCall(string $name): bool 168 | { 169 | /** @var \Illuminate\Config\Repository $config */ 170 | $config = config(); 171 | 172 | return $config->has('share-buttons.buttons.' . $name) 173 | && $config->has('share-buttons.templates.' . $name); 174 | } 175 | 176 | /** 177 | * @param array> $arguments 178 | * @return array{text?: string, id?: string, class?: string, title?: string, rel?: string, summary?: string} 179 | */ 180 | protected function retrieveCallArguments(array $arguments): array 181 | { 182 | if ($this->isAnyApplicableCallArguments($arguments)) { 183 | return array_filter($arguments[0], 'is_string'); 184 | } 185 | 186 | return []; 187 | } 188 | 189 | /** 190 | * @param array> $arguments 191 | */ 192 | protected function isAnyApplicableCallArguments(array $arguments): bool 193 | { 194 | return isset($arguments[0]) && is_array($arguments[0]); 195 | } 196 | 197 | /** 198 | * @param array{text?: string, id?: string, class?: string, title?: string, rel?: string, summary?: string} $arguments 199 | * @return array{url: string, text: string, id?: string, class?: string, title?: string, rel?: string, summary?: string} 200 | */ 201 | protected function prioritizeArguments(array $arguments): array 202 | { 203 | $lowPriorityArguments = [ 204 | 'text' => $this->pageTitle, 205 | ]; 206 | 207 | $highPriorityArguments = [ 208 | 'url' => $this->pageUrl, 209 | ]; 210 | 211 | return array_merge( 212 | $lowPriorityArguments, 213 | $arguments, 214 | $highPriorityArguments, 215 | ); 216 | } 217 | 218 | /** 219 | * @param array{url: string, text: string, id?: string, class?: string, title?: string, rel?: string, summary?: string} $arguments 220 | */ 221 | protected function rememberProcessedCall(string $name, array $arguments): void 222 | { 223 | // Since a share button can be displayed only once, there is no need to keep track and 224 | // make sure that the information about a previous button's call might be overwritten. 225 | $this->calls[$name] = new ProcessedCall($name, $arguments); 226 | } 227 | 228 | /** 229 | * @throws BadMethodCallException 230 | */ 231 | protected function handleUnexpectedCall(string $name): void 232 | { 233 | throw new BadMethodCallException( 234 | sprintf( 235 | 'Call an unexpected method "ShareButtons::%s()". Check if "%s" is registered in the `%s` file in the `buttons` and `templates` options.', 236 | $name, 237 | $name, 238 | 'config/share-buttons.php', 239 | ) 240 | ); 241 | } 242 | 243 | /** 244 | * Return a generated share buttons HTML code. 245 | * 246 | * @return string 247 | */ 248 | public function render(): string 249 | { 250 | return $this->generateShareButtons(); 251 | } 252 | 253 | /** 254 | * Return a generated share buttons HTML code. 255 | * 256 | * @return string 257 | */ 258 | public function getShareButtons(): string 259 | { 260 | return $this->generateShareButtons(); 261 | } 262 | 263 | /** 264 | * Return a generated share buttons HTML code. 265 | * 266 | * @return string 267 | */ 268 | public function __toString(): string 269 | { 270 | return $this->generateShareButtons(); 271 | } 272 | 273 | protected function generateShareButtons(): string 274 | { 275 | $representation = $this->presenter->getBlockPrefix(); 276 | 277 | /** @var ProcessedCall $call */ 278 | foreach ($this->calls as $call) { 279 | $representation .= $this->presenter->getElementPrefix(); 280 | $representation .= $this->presenter->getElementBody( 281 | $call->getName(), 282 | $call->getArguments(), 283 | ); 284 | $representation .= $this->presenter->getElementSuffix(); 285 | } 286 | 287 | $representation .= $this->presenter->getBlockSuffix(); 288 | 289 | return $representation; 290 | } 291 | 292 | /** 293 | * Return generated raw links. 294 | * 295 | * @return array 296 | */ 297 | public function getRawLinks(): array 298 | { 299 | return array_map(function ($call) { 300 | return $this->presenter->getElementUrl( 301 | $call->getName(), 302 | $call->getArguments(), 303 | ); 304 | }, $this->calls); 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/Templaters/LaravelTemplater.php: -------------------------------------------------------------------------------- 1 | prepareReplacements($replacements); 15 | 16 | return $this->applyReplacements($template, $prepared); 17 | } 18 | 19 | /** 20 | * @param array $replacements 21 | * @return array 22 | */ 23 | protected function prepareReplacements(array $replacements): array 24 | { 25 | $prepared = []; 26 | 27 | foreach ($replacements as $search => $replace) { 28 | $prepared[':' . mb_strtolower($search)] = $replace; 29 | $prepared[':' . mb_strtoupper($search)] = mb_strtoupper($replace); 30 | } 31 | 32 | return $prepared; 33 | } 34 | 35 | /** 36 | * @param array $replacements 37 | */ 38 | protected function applyReplacements(string $template, array $replacements): string 39 | { 40 | return strtr($template, $replacements); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Templaters/Templater.php: -------------------------------------------------------------------------------- 1 | $replacements 17 | * @return string 18 | */ 19 | public function process(string $template, array $replacements): string; 20 | } 21 | -------------------------------------------------------------------------------- /src/ValueObjects/ProcessedCall.php: -------------------------------------------------------------------------------- 1 | initName($name); 27 | $this->initArguments($arguments); 28 | } 29 | 30 | private function initName(string $name): void 31 | { 32 | if (trim($name) === '') { 33 | throw new InvalidProcessedCallArgument('A name argument cannot be empty.'); 34 | } 35 | 36 | $this->name = $name; 37 | } 38 | 39 | /** 40 | * @param array{url: string, text: string, id?: string, class?: string, title?: string, rel?: string, summary?: string} $arguments 41 | */ 42 | private function initArguments(array $arguments): void 43 | { 44 | $this->arguments = $arguments; 45 | } 46 | 47 | /** 48 | * @return string 49 | */ 50 | public function getName(): string 51 | { 52 | return $this->name; 53 | } 54 | 55 | /** 56 | * @return array{url: string, text: string, id?: string, class?: string, title?: string, rel?: string, summary?: string} 57 | */ 58 | public function getArguments(): array 59 | { 60 | return $this->arguments; 61 | } 62 | } 63 | --------------------------------------------------------------------------------