├── README.md ├── assets └── js │ └── bb-custom-attributes.js ├── bb-custom-attributes.php ├── includes ├── BBCustomAttributes.php └── GithubUpdater.php └── license.txt /README.md: -------------------------------------------------------------------------------- 1 | # Beaver Builder Custom Attributes 2 | 3 | ## What does this do? 4 | It adds the ability to add custom attributes to modules, columns, and rows within Beaver Builder the same way you would add 5 | a class or ID. In fact, you'll find the new field right below those fields in the Advanced tab. 6 | 7 | ## How does it work? 8 | After installing, to add an attribute to modules, columns, and rows, you navigate to the advanced tab. There will now be a field below the Label input where you can add your attributes. 9 | When you click into a custom attribute, you will have inputs for Key, Value, Target Selector, and Override Attribute. 10 | - The **Key** and **Value** inputs are your attribute name/value pairs like: `key="value"` 11 | - The **Target Selector** is a way to add your custom attribute to an inner element of the current element. By default, all attributes are added to the outer wrapper of a module/column/row. For example, you could add an attribute like `id`, `class`, `title` or an `aria-` attribute to an `` inside a Button Module by typing in a Target Selector of `a` or `a.fl-button`. See below for more info about this feature. 12 | - The **Override Attribute** select input allows you to override the attribute if that attribute already exists from another source. Selecting 'No' is safer and will avoid conflicts. 13 | 14 | ![custom-attribute-form](https://github.com/user-attachments/assets/9f60dea4-c149-4533-8c52-10c1c6227fe5) 15 | 16 | ## Advanced use of the Target Selector 17 | The Target Selector allows you to apply custom attributes to inner elements within the current element. Here are some advanced usage tips: 18 | - If your Target Selector matches more than one inner element, all matches will receive the attribute. 19 | - These inner attributes are added with JavaScript, so if you are trying to access this data with your own JavaScript, you will need to make sure it runs after this JavaScript runs. 20 | ### Ensuring custom JavaScript runs after attributes are applied 21 | The JavaScript to add the inner element attributes runs on `DOMContentLoaded` in the footer. So if you need to run JavaScript on those attributes, to ensure it runs after the custom attributes have been applied, you can listen for the `customAttrsProcessed` event. Additionally, you should check if the processing has already completed by checking the `window.customAttrsProcessingComplete` flag. Here is an example: 22 | ``` 23 | function runCustomJs() { 24 | // Your custom JavaScript here 25 | } 26 | 27 | if (window.customAttrsProcessingComplete) { 28 | // If the custom attributes processing is already complete 29 | runCustomJs(); 30 | } else { 31 | // Otherwise, wait for the customAttrsProcessed event 32 | document.addEventListener('customAttrsProcessed', runCustomJs); 33 | } 34 | ``` 35 | 36 | ## How do I install it? What about updates? 37 | 1. Download the zip file of this plugin 38 | 2. Upload it to your WordPress install 39 | 3. Activate the plugin. 40 | 41 | ### How Updates Work 42 | 43 | Even though the plugin is not listed in the WordPress repository, you’ll still receive updates through the WordPress dashboard like any other plugin. The GitHub updater built into the plugin ensures that whenever there’s a new version available on GitHub, it will appear in your WordPress updates section. 44 | 45 | This means you don’t have to manually check GitHub for updates—the plugin will notify you of new releases and allow you to update in one click, just like with plugins from the WordPress plugin repository. 46 | 47 | ## Is this supported? 48 | This is intended to be a very simple and light-weight plugin. Ideally it shouldn't really require much support. 49 | That said, if you create an issue or pull request I'll get to it when I'm able. Please don't expect too much as this 50 | is just a freebie intended to help others. 51 | -------------------------------------------------------------------------------- /assets/js/bb-custom-attributes.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | const elsWithInnerCustomAttrs = document.querySelectorAll('[data-custom-attributes]'); 3 | 4 | elsWithInnerCustomAttrs.forEach(function(element) { 5 | const customAttributes = JSON.parse(element.getAttribute('data-custom-attributes')); 6 | 7 | customAttributes.forEach(function(attribute) { 8 | const targetElements = element.querySelectorAll(attribute.target); 9 | 10 | targetElements.forEach(function(targetElement) { 11 | if (attribute.override === 'yes' || !targetElement.hasAttribute(attribute.key)) { 12 | targetElement.setAttribute(attribute.key, attribute.value); 13 | } 14 | }); 15 | }); 16 | 17 | // Remove the data-custom-attributes attribute after processing 18 | element.removeAttribute('data-custom-attributes'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /bb-custom-attributes.php: -------------------------------------------------------------------------------- 1 | load(); 27 | 28 | // Initialize GitHub updater 29 | function init_updater() { 30 | $updater = new GithubUpdater( __FILE__ ); 31 | $updater->set_username( 'JasonTheAdams' ); 32 | $updater->set_repository( 'BBCustomAttributes' ); 33 | $updater->set_settings( array( 34 | 'requires' => '5.6', 35 | 'tested' => '6.6.1', 36 | 'rating' => '100.0', 37 | 'num_ratings' => '10', 38 | 'downloaded' => '10', 39 | 'added' => '2024-08-22', 40 | ) ); 41 | $updater->initialize(); 42 | } 43 | init_updater(); -------------------------------------------------------------------------------- /includes/BBCustomAttributes.php: -------------------------------------------------------------------------------- 1 | __('Custom Attributes'), 30 | 'tabs' => [ 31 | 'attributes' => [ 32 | 'title' => __('Attribute'), 33 | 'sections' => [ 34 | 'general' => [ 35 | 'title' => '', 36 | 'fields' => [ 37 | 'key' => [ 38 | 'type' => 'text', 39 | 'label' => __('Key'), 40 | 'help' => __('Attribute key'), 41 | 'preview' => ['type' => 'none'] 42 | ], 43 | 'value' => [ 44 | 'type' => 'text', 45 | 'label' => __('Value'), 46 | 'help' => __('Attribute value'), 47 | 'preview' => ['type' => 'none'] 48 | ], 49 | 'target' => [ 50 | 'type' => 'text', 51 | 'label' => __('Target Selector'), 52 | 'help' => __('CSS selector of the inner element to apply the attribute to (leave blank to add to wrapper)'), 53 | 'preview' => ['type' => 'none'] 54 | ], 55 | 'override' => [ 56 | 'type' => 'select', 57 | 'label' => __('Override Attribute'), 58 | 'help' => __('If the attribute already exists from another source, override or yield. Selecting no is safer and will avoid conflicts.'), 59 | 'default' => 'no', 60 | 'options' => [ 61 | 'no' => __('No'), 62 | 'yes' => __('Yes') 63 | ] 64 | ] 65 | ] 66 | ] 67 | ] 68 | ] 69 | ] 70 | ]); 71 | } 72 | 73 | /** 74 | * Adds the custom attributes field to the CSS section of the advanced section 75 | * 76 | * @param array $form 77 | * @param string $id 78 | * 79 | * @return array 80 | */ 81 | public function filterAdvancedTabAttr($form, $id) 82 | { 83 | if ('module_advanced' === $id) { 84 | $form['sections']['css_selectors']['fields']['custom_attributes'] = [ 85 | 'type' => 'form', 86 | 'form' => 'custom_attributes', 87 | 'label' => __('Attribute'), 88 | 'help' => __('Adds custom attributes to the module'), 89 | 'multiple' => true, 90 | 'preview_text' => 'key' 91 | ]; 92 | } 93 | 94 | if('col' === $id ) { 95 | $form['tabs']['advanced']['sections']['css_selectors']['fields']['custom_attributes'] = [ 96 | 'type' => 'form', 97 | 'form' => 'custom_attributes', 98 | 'label' => __('Attribute'), 99 | 'help' => __('Adds custom attributes to the column'), 100 | 'multiple' => true, 101 | 'preview_text' => 'key' 102 | ]; 103 | } 104 | 105 | if('row' === $id ) { 106 | $form['tabs']['advanced']['sections']['css_selectors']['fields']['custom_attributes'] = [ 107 | 'type' => 'form', 108 | 'form' => 'custom_attributes', 109 | 'label' => __('Attribute'), 110 | 'help' => __('Adds custom attributes to the row'), 111 | 'multiple' => true, 112 | 'preview_text' => 'key' 113 | ]; 114 | } 115 | 116 | return $form; 117 | } 118 | 119 | /** 120 | * Adds the custom attributes to the row/column/module being rendered 121 | * If there is a target value set, then add the attr to data-custom-attributes attr so js can add it to the inner element 122 | * 123 | * @param array $attributes 124 | * @param object $element 125 | * 126 | * @return array 127 | */ 128 | public function filterAttributes($attributes, $element) 129 | { 130 | if (isset($element->settings->custom_attributes)) { 131 | $innerElementAttributes = []; 132 | $wrapperAttributes = []; 133 | 134 | foreach ($element->settings->custom_attributes as $attribute) { 135 | if (!empty($attribute->key) && !empty($attribute->value)) { 136 | $attr = [ 137 | 'key' => esc_attr($attribute->key), 138 | 'value' => do_shortcode(esc_attr($attribute->value)), 139 | 'override' => esc_attr($attribute->override) 140 | ]; 141 | 142 | if (!empty($attribute->target)) { 143 | $attr['target'] = esc_attr($attribute->target); 144 | $innerElementAttributes[] = $attr; 145 | } else { 146 | $wrapperAttributes[$attr['key']] = $attr['value']; 147 | } 148 | } 149 | } 150 | 151 | // Add direct attributes to the wrapper 152 | foreach ($wrapperAttributes as $key => $value) { 153 | if (isset($attributes[$key]) && $attr['override'] === 'no') { 154 | continue; 155 | } 156 | $attributes[$key] = $value; 157 | } 158 | 159 | // Add inner element custom attrs to data-custom-attributes attribute on the wrapper in prep for js processing 160 | if (!empty($innerElementAttributes)) { 161 | $attributes['data-custom-attributes'] = esc_attr(json_encode($innerElementAttributes)); 162 | } 163 | } 164 | 165 | return $attributes; 166 | } 167 | 168 | /** 169 | * Enqueues the JavaScript for processing custom attributes for inner elements 170 | */ 171 | public function enqueueCustomAttributesScript() { 172 | wp_enqueue_script( 173 | 'bb-custom-attributes-script', 174 | plugin_dir_url( dirname( __FILE__ ) ) . 'assets/js/bb-custom-attributes.js', 175 | array(), 176 | BBCUSTOMATTRIBUTES_VERSION, 177 | true 178 | ); 179 | } 180 | 181 | } 182 | -------------------------------------------------------------------------------- /includes/GithubUpdater.php: -------------------------------------------------------------------------------- 1 | file = $file; 35 | add_action( 'admin_init', array( $this, 'set_plugin_properties' ) ); 36 | return $this; 37 | } 38 | 39 | /** 40 | * [set_plugin_properties description] 41 | */ 42 | public function set_plugin_properties() { 43 | $this->plugin = get_plugin_data( $this->file ); 44 | $this->basename = plugin_basename( $this->file ); 45 | $this->active = is_plugin_active( $this->basename ); 46 | } 47 | 48 | /** 49 | * [set_username description] 50 | * @param [type] $username [description] 51 | */ 52 | public function set_username( $username ) { 53 | $this->username = $username; 54 | } 55 | 56 | /** 57 | * [set_settings description] 58 | * @param [type] $settings [description] 59 | */ 60 | public function set_settings( $settings ) { 61 | 62 | // set some defaults in case someone forgets to set these 63 | $defaults = array( 64 | 'requires' => '5.4', 65 | 'tested' => '6.3', 66 | 'rating' => '100.0', 67 | 'num_ratings' => '10', 68 | 'downloaded' => '10', 69 | 'added' => '2023-08-20', 70 | 'banners' => false, 71 | ); 72 | 73 | $settings = wp_parse_args( $settings , $defaults ); 74 | 75 | $this->plugin_settings = $settings; 76 | } 77 | 78 | /** 79 | * [set_repository description] 80 | * @param [type] $repository [description] 81 | */ 82 | public function set_repository( $repository ) { 83 | $this->repository = $repository; 84 | } 85 | 86 | /** 87 | * [authorize description] 88 | * @param [type] $token [description] 89 | * @return [type] [description] 90 | */ 91 | public function authorize( $token ) { 92 | $this->authorize_token = $token; 93 | } 94 | 95 | /** 96 | * [get_repository_info description] 97 | * @return [type] [description] 98 | */ 99 | private function get_repository_info() { 100 | 101 | // Do we have a response? 102 | if ( is_null( $this->github_response ) ) { 103 | // Build URI 104 | $request_uri = sprintf( 'https://api.github.com/repos/%s/%s/releases', $this->username, $this->repository ); 105 | 106 | // Is there an access token? 107 | if( $this->authorize_token ) { 108 | // Append it 109 | $request_uri = add_query_arg( 'access_token', $this->authorize_token, $request_uri ); 110 | } 111 | 112 | // Get JSON and parse it 113 | $response = json_decode( wp_remote_retrieve_body( wp_remote_get( $request_uri ) ), true ); 114 | 115 | // If it is an array 116 | if( is_array( $response ) ) { 117 | // Get the first item 118 | $response = current( $response ); 119 | } 120 | // Is there an access token? 121 | if( $this->authorize_token ) { 122 | // Update our zip url with token 123 | $response['zipball_url'] = add_query_arg( 'access_token', $this->authorize_token, $response['zipball_url'] ); 124 | } 125 | 126 | // try to get metadata from the release body 127 | $metadata = $this->get_tmpfile_data( $response['body']); 128 | 129 | // merge the data with the response 130 | $response = array_merge( $response, $metadata); 131 | 132 | // Set it to our property 133 | $this->github_response = $response; 134 | return $response; 135 | } 136 | 137 | } 138 | 139 | /** 140 | * [initialize description] 141 | * @return [type] [description] 142 | */ 143 | public function initialize() { 144 | add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'modify_transient' ), 10, 1 ); 145 | add_filter( 'plugins_api', array( $this, 'plugin_popup' ), 10, 3); 146 | add_filter( 'upgrader_post_install', array( $this, 'after_install' ), 10, 3 ); 147 | } 148 | 149 | /** 150 | * [modify_transient description] 151 | * @param [type] $transient [description] 152 | * @return [type] [description] 153 | */ 154 | public function modify_transient( $transient ) { 155 | 156 | // Check if transient has a checked property 157 | if ( property_exists( $transient, 'checked') ) { 158 | 159 | // Did Wordpress check for updates? 160 | if ( $checked = $transient->checked ) { 161 | 162 | // return early if our plugin hasn't been checked 163 | if( !isset( $checked[ $this->basename ] ) ) return $transient; 164 | 165 | // Get the repo info 166 | $this->get_repository_info(); 167 | 168 | // Check if we're out of date 169 | $out_of_date = version_compare( $this->github_response['tag_name'], $checked[ $this->basename ], 'gt' ); 170 | if( $out_of_date ) { 171 | 172 | // Get the ZIP 173 | $new_files = $this->github_response['zipball_url']; 174 | 175 | // Create valid slug 176 | $slug = current( explode('/', $this->basename ) ); 177 | 178 | // setup our plugin info 179 | $plugin = array( 180 | 'url' => $this->plugin["PluginURI"], 181 | 'slug' => $slug, 182 | 'package' => $new_files, 183 | 'tested' => $this->github_response['tested'], 184 | 'icons' => $this->github_response['icons'], 185 | 'banners' => $this->github_response['banners'], 186 | 'banners_rtl' => [], 187 | 'requires_php' => $this->github_response['requires_php'], 188 | 'new_version' => $this->github_response['tag_name'], 189 | ); 190 | 191 | // Return it in response 192 | $transient->response[$this->basename] = (object) $plugin; 193 | } 194 | } 195 | } 196 | 197 | // Return filtered transient 198 | return $transient; 199 | } 200 | 201 | /** 202 | * get_tmpfile_data 203 | * 204 | * takes a string, creates a temp file and tries to get meta data from the tmp file 205 | * since I couldn't find a function that does what I wanted 206 | * 207 | * @param mixed $string 208 | * @return void 209 | */ 210 | private function get_tmpfile_data( $string ) { 211 | 212 | 213 | // create a wp temp file in the 214 | $temp_file = wp_tempnam(); 215 | $temp = fopen($temp_file, 'r+'); 216 | 217 | // make sure to also delete the file when done or even when scripts fail 218 | register_shutdown_function( function() use( $temp_file ) { 219 | @unlink( $temp_file ); 220 | } ); 221 | 222 | $tmpfilename = stream_get_meta_data($temp)['uri']; 223 | fwrite( $temp, $string); 224 | 225 | $file_headers = \get_file_data( 226 | $tmpfilename, 227 | [ 228 | 'tested' => 'Tested', 229 | 'icons' => 'Icons', 230 | 'banners' => 'Banners', 231 | 'requires_php' => 'RequiresPHP', 232 | ] 233 | ); 234 | 235 | $icons = $file_headers[ 'icons' ] ? array_map( 'trim', explode(',', $file_headers[ 'icons' ] ) ) : false; 236 | $banners = $file_headers[ 'banners' ] ? array_map( 'trim', explode(',', $file_headers[ 'banners' ] ) ) : false; 237 | 238 | $username = $this->username; 239 | $repository = $this->repository; 240 | 241 | // decompose the icons, if provided 242 | if (is_array($icons)) { 243 | $icons = array_reduce( $icons, function ($acc , $item) use ($username,$repository) { 244 | $ex_item = explode('|', $item); 245 | $acc[$ex_item[0]] = sprintf("https://github.com/%s/%s" , $username, $repository ) . $ex_item[1]; 246 | return $acc; 247 | } , []); 248 | } 249 | 250 | // decompose the banners, if provided 251 | if (is_array($banners)) { 252 | $banners = array_reduce( $banners, function ($acc , $item) use ($username,$repository) { 253 | $ex_item = explode('|', $item); 254 | $acc[$ex_item[0]] = sprintf("https://github.com/%s/%s" , $username, $repository ) . $ex_item[1]; 255 | return $acc; 256 | } , []); 257 | } 258 | 259 | // try to find the update_description delimiter 260 | $update_description = explode( '|||' , $string ); 261 | 262 | $updates = ( sizeof($update_description) == 2 ) ? $update_description[1] : ''; 263 | 264 | $data = [ 265 | 'tested' => $file_headers[ 'tested' ], 266 | 'requires_php' => $file_headers[ 'requires_php' ], 267 | 'icons' => $icons, 268 | 'banners' => $banners, 269 | 'updates' => $updates, 270 | ]; 271 | 272 | // the register_shutdown function will also make sure the temp-file 273 | // gets deleted whenever something fails 274 | 275 | return $data; 276 | 277 | 278 | 279 | 280 | } 281 | 282 | /** 283 | * [plugin_popup description] 284 | * @param [type] $result [description] 285 | * @param [type] $action [description] 286 | * @param [type] $args [description] 287 | * @return [type] [description] 288 | */ 289 | public function plugin_popup( $result, $action, $args ) { 290 | 291 | // If there is a slug 292 | if( ! empty( $args->slug ) ) { 293 | 294 | // And it's our slug 295 | if( $args->slug == current( explode( '/' , $this->basename ) ) ) { 296 | 297 | // Get our repo info 298 | $this->get_repository_info(); 299 | // Set it to an array 300 | $plugin = array( 301 | 'name' => $this->plugin["Name"], 302 | 'slug' => $this->basename, 303 | 'version' => $this->github_response['tag_name'], 304 | 'author' => $this->plugin["AuthorName"], 305 | 'author_profile' => $this->plugin["AuthorURI"], 306 | 'last_updated' => $this->github_response['published_at'], 307 | 'homepage' => $this->plugin["PluginURI"], 308 | 'short_description' => $this->plugin["Description"], 309 | 'sections' => array( 310 | 'Description' => $this->plugin["Description"], 311 | 'Updates' => $this->github_response['updates'], 312 | ), 313 | 'banners' => $this->github_response[ 'banners' ], 314 | 'download_link' => $this->github_response['zipball_url'] 315 | ); 316 | 317 | // merge with other settings that can be set 318 | $plugin = wp_parse_args( $plugin, $this->plugin_settings ); 319 | 320 | // Return the data 321 | return (object) $plugin; 322 | } 323 | } 324 | // Otherwise return default 325 | return $result; 326 | } 327 | 328 | /** 329 | * [after_install description] 330 | * @param [type] $response [description] 331 | * @param [type] $hook_extra [description] 332 | * @param [type] $result [description] 333 | * @return [type] [description] 334 | */ 335 | public function after_install( $response, $hook_extra, $result ) { 336 | 337 | // Get global FS object 338 | global $wp_filesystem; 339 | 340 | // Our plugin directory 341 | $install_directory = plugin_dir_path( $this->file ); 342 | 343 | // Move files to the plugin dir 344 | $wp_filesystem->move( $result['destination'], $install_directory ); 345 | 346 | // Set the destination for the rest of the stack 347 | $result['destination'] = $install_directory; 348 | 349 | // If it was active 350 | if ( $this->active ) { 351 | 352 | // Reactivate 353 | activate_plugin( $this->basename ); 354 | } 355 | 356 | return $result; 357 | } 358 | } -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright 2019 Jason Adams 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------------------------------