├── .gitignore ├── .jshintrc ├── Gruntfile.js ├── LICENSE ├── README.md ├── assets ├── css │ ├── dist │ │ ├── modular-page-builder.css │ │ └── modular-page-builder.css.map │ └── src │ │ └── modular-page-builder.scss └── js │ ├── dist │ └── modular-page-builder.js │ └── src │ ├── collections │ ├── module-attributes.js │ └── modules.js │ ├── globals.js │ ├── models │ ├── builder.js │ ├── module-attribute.js │ └── module.js │ ├── modular-page-builder.js │ ├── utils │ ├── edit-views.js │ ├── field-views.js │ └── module-factory.js │ └── views │ ├── builder.js │ ├── fields │ ├── field-attachment.js │ ├── field-checkbox.js │ ├── field-content-editable.js │ ├── field-link.js │ ├── field-number.js │ ├── field-post-select.js │ ├── field-select.js │ ├── field-text.js │ ├── field-textarea.js │ ├── field-wysiwyg.js │ └── field.js │ ├── module-edit-blockquote.js │ ├── module-edit-default.js │ ├── module-edit-form-row.js │ ├── module-edit-tools.js │ └── module-edit.js ├── composer.json ├── inc ├── class-builder-post-meta.php ├── class-builder.php ├── class-plugin.php ├── class-wp-cli.php └── modules │ ├── class-blockquote.php │ ├── class-header.php │ ├── class-image.php │ ├── class-module.php │ └── class-text.php ├── modular-page-builder.php ├── package.json └── templates ├── builder.tpl.html ├── field-attachment.tpl.html ├── field-checkbox.tpl.html ├── field-content-editable.tpl.html ├── field-link.tpl.html ├── field-number.tpl.html ├── field-select.tpl.html ├── field-text.tpl.html ├── field-textarea.tpl.html ├── field-wysiwyg.tpl.html ├── form-row.tpl.html ├── module-edit-blockquote.tpl.html ├── module-edit-header.tpl.html ├── module-edit-image.tpl.html ├── module-edit-text.tpl.html └── module-edit-tools.tpl.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /vendor/ 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "eqnull": true, 5 | "es3": false, 6 | "expr": true, 7 | "noarg": true, 8 | "quotmark": "single", 9 | "trailing": true, 10 | "undef": true, 11 | "unused": true, 12 | "browser": true, 13 | "devel": true, 14 | "browserify": true, 15 | "globals": { 16 | "modularPageBuilderData": true, 17 | "_": true, 18 | "Backbone": true, 19 | "jQuery": true, 20 | "JSON": true, 21 | "wp": true, 22 | "tinyMCE": true, 23 | "tinyMCEPreInit": true, 24 | "QTags": true, 25 | "quicktags": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function( grunt ) { 2 | 3 | 'use strict'; 4 | 5 | var remapify = require('remapify'); 6 | 7 | grunt.initConfig( { 8 | 9 | pkg: grunt.file.readJSON( 'package.json' ), 10 | 11 | sass: { 12 | dist: { 13 | files: { 14 | 'assets/css/dist/modular-page-builder.css' : 'assets/css/src/modular-page-builder.scss', 15 | }, 16 | options: { 17 | sourceMap: true 18 | } 19 | } 20 | }, 21 | 22 | autoprefixer: { 23 | options: { 24 | browsers: ['last 2 versions', 'ie 8', 'ie 9'], 25 | map: true, 26 | }, 27 | your_target: { 28 | src: 'assets/css/dist/modular-page-builder.css', 29 | dest: 'assets/css/dist/modular-page-builder.css', 30 | }, 31 | }, 32 | 33 | watch: { 34 | 35 | styles: { 36 | files: ['assets/css/src/*.scss','assets/css/src/**/*.scss'], 37 | tasks: ['styles'], 38 | options: { 39 | debounceDelay: 500, 40 | livereload: true, 41 | sourceMap: true 42 | } 43 | }, 44 | 45 | scripts: { 46 | files: [ 'assets/js/src/*.js', 'assets/js/src/**/*.js' ], 47 | tasks: ['scripts'], 48 | options: { 49 | debounceDelay: 500, 50 | livereload: true, 51 | sourceMap: true 52 | } 53 | } 54 | 55 | }, 56 | 57 | browserify : { 58 | 59 | options: { 60 | 61 | browserifyOptions: { 62 | debug: true 63 | }, 64 | 65 | preBundleCB: function(b) { 66 | 67 | b.plugin(remapify, [ 68 | { 69 | cwd: 'assets/js/src/models', 70 | src: '**/*.js', 71 | expose: 'models' 72 | }, 73 | { 74 | cwd: 'assets/js/src/collections', 75 | src: '**/*.js', 76 | expose: 'collections' 77 | }, 78 | { 79 | cwd: 'assets/js/src/views', 80 | src: '**/*.js', 81 | expose: 'views' 82 | }, 83 | { 84 | cwd: 'assets/js/src/utils', 85 | src: '**/*.js', 86 | expose: 'utils' 87 | } 88 | ]); 89 | 90 | } 91 | }, 92 | 93 | dist: { 94 | files : { 95 | 'assets/js/dist/modular-page-builder.js' : ['assets/js/src/modular-page-builder.js'], 96 | }, 97 | options: { 98 | transform: ['browserify-shim'] 99 | } 100 | }, 101 | 102 | }, 103 | 104 | phpcs: { 105 | application: { 106 | src: ['./**/*.php', '!./node_modules/**/*.php'], 107 | }, 108 | options: { 109 | standard: 'WordPress' 110 | } 111 | }, 112 | 113 | jshint: { 114 | all: ['Gruntfile.js', 'assets/js/src/**/*.js'], 115 | options: { 116 | jshintrc: true, 117 | }, 118 | } 119 | 120 | } ); 121 | 122 | grunt.loadNpmTasks( 'grunt-sass' ); 123 | grunt.loadNpmTasks( 'grunt-contrib-watch' ); 124 | grunt.loadNpmTasks( 'grunt-browserify' ); 125 | grunt.loadNpmTasks( 'grunt-autoprefixer' ); 126 | grunt.loadNpmTasks( 'grunt-phpcs' ); 127 | grunt.loadNpmTasks( 'grunt-contrib-jshint' ); 128 | 129 | grunt.registerTask( 'scripts', ['browserify', 'jshint'] ); 130 | grunt.registerTask( 'styles', ['sass', 'autoprefixer'] ); 131 | grunt.registerTask( 'php', ['phpcs'] ); 132 | grunt.registerTask( 'default', ['scripts', 'styles', 'php'] ); 133 | 134 | grunt.util.linefeed = '\n'; 135 | 136 | }; 137 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modular Page Builder 2 | 3 | Modular page builder for WordPress 4 | 5 | ![image](https://cloud.githubusercontent.com/assets/494927/10787478/1d80dd16-7d69-11e5-829e-725995593538.png) 6 | 7 | ## Basic usage 8 | 9 | Out of the box, modules are available for header, text and image only. 10 | 11 | You must add post type support for the builder `add_post_type_support( 'page', 'modular-page-builder' );` 12 | 13 | You must handle the output of the page builder data manually. Here is an example of simply replacing the post content. 14 | 15 | ```php 16 | add_filter( 'the_content', function( $content, $id = null ) { 17 | 18 | $id = $id ?: get_the_ID(); 19 | 20 | if ( post_type_supports( get_post_type( $id ), 'modular-page-builder' ) ) { 21 | $plugin = ModularPageBuilder\Plugin::get_instance()->get_builder( 'modular-page-builder' ); 22 | $content = $plugin->get_rendered_data( $id ); 23 | } 24 | 25 | return $content; 26 | 27 | }); 28 | ``` 29 | 30 | ## Revisions 31 | 32 | By default, WordPress does NOT revision post meta. If you want to revision the page builder data we reccommend you use the [WP-Post-Meta-Revisions](https://wordpress.org/plugins/wp-post-meta-revisions/) plugin. You just need to install and activate it, we have handled registering of the revisioned meta keys. 33 | 34 | ## Custom Modules 35 | 36 | * Register module using `$plugin->register_module( 'module-name', 'ModuleClass' ); 37 | * Module Class should extend `ModularPageBuilder\Modules\Module`. 38 | * It should provide a `render` method. 39 | * Set `$name` property the same as `module-name` 40 | * Define all available attributes in `$attr` array. 41 | * Each attribute should have name, label and type where type is an available field type. 42 | 43 | ### Extra Customization 44 | 45 | * By default, your module will use the `edit-form-default.js` view. 46 | * You can provide your own view by adding it to the edit view map: `window.modularPageBuilder.editViewMap`. Where the property is your module name and the view is your view object. 47 | * You should probably extend `window.modularPageBuilder.views.ModuleEdit`. 48 | * You can still make use of the built in field view objects if you want. 49 | 50 | ## Available Field Types 51 | 52 | * `text` 53 | * `textarea` 54 | * `select` 55 | * `html` 56 | * `link` 57 | * `attachment` 58 | * `post_select` 59 | 60 | ### Text Field 61 | 62 | Example. 63 | 64 | ```php 65 | array( 66 | 'name' => 'caption', 67 | 'label' => __( 'Test Text Field', 'mpb' ), 68 | 'type' => 'text' 69 | ) 70 | ``` 71 | 72 | ### Select Field 73 | 74 | Example. 75 | 76 | ```php 77 | array( 78 | 'name' => 'select_test', 79 | 'label' => __( 'Select Test', 'mbp' ), 80 | 'type' => 'select', 81 | 'config' => array( 82 | 'options' => array( 83 | array( 'value' => 'a', 'text' => 'Option A' ), 84 | array( 'value' => 'b', 'text' => 'Option B' ) 85 | ) 86 | ) 87 | ) 88 | ``` 89 | 90 | ### Image Field 91 | 92 | Example 93 | 94 | ```php 95 | array( 96 | 'name' => 'image', 97 | 'label' => __( 'Test Image', 'mbp' ), 98 | 'type' => 'attachment', 99 | 'config' => array( 100 | 'button_text' => __( 'Custom Button Text', 'mbp' ), 101 | ) 102 | ) 103 | ``` 104 | -------------------------------------------------------------------------------- /assets/css/dist/modular-page-builder.css: -------------------------------------------------------------------------------- 1 | .modular-page-builder-container { 2 | margin-top: 10px; } 3 | 4 | .modular-page-builder { 5 | background: #FFFFFF; 6 | border: 1px solid #e5e5e5; 7 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04) inset; 8 | margin: 8px 0; } 9 | .modular-page-builder .add-new { 10 | padding: 16px; 11 | background: #F5F5F5; 12 | border-top: 1px solid #e5e5e5; } 13 | .modular-page-builder .selection:empty + .add-new { 14 | border-top: none; } 15 | .modular-page-builder .module-edit { 16 | padding: 16px; 17 | margin-bottom: 0; 18 | border-width: 1px 0; 19 | position: relative; } 20 | .modular-page-builder .module-edit:after { 21 | content: ""; 22 | display: table; 23 | clear: both; } 24 | .modular-page-builder .module-edit + .module-edit { 25 | border-top: 1px dashed #e5e5e5; } 26 | .modular-page-builder .ui-sortable-helper:first-child + .module-edit { 27 | border-top: none; } 28 | .modular-page-builder .module-edit-tools { 29 | background-color: #F5F5F5; 30 | margin: -17px -17px 15px -17px; 31 | padding: 8px 16px; 32 | border: 1px solid #e5e5e5; 33 | overflow: auto; } 34 | .modular-page-builder .module-edit-tools .module-edit-title { 35 | margin: 0 !important; 36 | float: left; 37 | font-size: 14px !important; 38 | line-height: 1.714285714 !important; 39 | padding: 0 !important; } 40 | .modular-page-builder .module-edit-tools .button-selection-item-remove { 41 | display: none; 42 | float: right; 43 | margin: 0; 44 | padding-left: 1px; 45 | padding-right: 1px; } 46 | .modular-page-builder .module-edit-tools.ui-sortable-handle { 47 | cursor: move; } 48 | .modular-page-builder .module-edit-tools.ui-sortable-handle .button-selection-item-remove { 49 | display: block; } 50 | .modular-page-builder .form-row { 51 | margin-bottom: 16px; 52 | clear: both; 53 | padding-left: 150px; } 54 | .modular-page-builder .form-row:last-child { 55 | margin-bottom: 0; } 56 | .modular-page-builder .form-row-label { 57 | float: left; 58 | margin-left: -150px; 59 | width: 140px; 60 | margin-top: 5px; } 61 | .modular-page-builder .form-row-inline { 62 | display: inline-block; 63 | vertical-align: top; 64 | margin-right: 10px; 65 | margin-bottom: 8px; 66 | padding-left: 0; } 67 | .modular-page-builder .form-row-inline label { 68 | display: inline-block; 69 | margin-left: 0; 70 | margin-right: 20px; 71 | margin-bottom: 8px; 72 | width: auto; } 73 | .modular-page-builder .form-row-inline .wp-color-result { 74 | margin-bottom: 0; 75 | position: relative; 76 | top: 3px; } 77 | .modular-page-builder .description { 78 | margin: 8px 0; 79 | display: inline-block; } 80 | .modular-page-builder .ui-sortable-placeholder { 81 | visibility: visible !important; 82 | border-style: solid; 83 | background: #F5F5F5; 84 | border-color: #F5F5F5; 85 | margin-bottom: -1px; } 86 | .modular-page-builder .ui-sortable-helper { 87 | background: #FFFFFF; 88 | opacity: 1; } 89 | .modular-page-builder .button-small .dashicons-no { 90 | margin-top: 1px; } 91 | .modular-page-builder textarea { 92 | max-width: 100%; 93 | min-width: 100%; 94 | vertical-align: top; } 95 | .modular-page-builder .image-field-controls { 96 | margin-bottom: 16px; } 97 | .modular-page-builder .image-placeholder { 98 | border: 1px dashed #e5e5e5; 99 | border-radius: 3px; 100 | width: 148px; 101 | height: 148px; 102 | line-height: 148px; 103 | text-align: center; 104 | position: relative; 105 | display: block; 106 | float: left; 107 | margin-right: 16px; 108 | margin-bottom: 16px; } 109 | .modular-page-builder .image-placeholder .button.add { 110 | vertical-align: middle; } 111 | .modular-page-builder .image-placeholder .button.remove { 112 | position: absolute; 113 | top: 5px; 114 | right: 5px; 115 | width: 24px; 116 | padding: 0; 117 | z-index: 1; 118 | box-shadow: inset 0 1px 0 #fff, 0 0 0 1px rgba(0, 0, 0, 0.1); } 119 | .modular-page-builder .image-placeholder .image { 120 | margin: -1px; 121 | display: block; 122 | position: relative; 123 | width: 100%; 124 | height: 100%; 125 | display: -ms-flexbox; 126 | display: flex; 127 | -ms-flex-align: center; 128 | align-items: center; } 129 | .modular-page-builder .image-placeholder .image:after { 130 | content: ' '; 131 | position: absolute; 132 | top: 0; 133 | left: 0; 134 | right: -2px; 135 | bottom: -2px; 136 | box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25) inset; 137 | pointer-events: none; } 138 | .modular-page-builder .image-placeholder img { 139 | display: block; } 140 | .modular-page-builder .image-placeholder img.icon { 141 | width: auto; 142 | height: auto; 143 | position: absolute; 144 | left: 50%; 145 | top: 50%; 146 | margin-top: -15px; 147 | -ms-transform: translate(-50%, -50%); 148 | transform: translate(-50%, -50%); } 149 | .modular-page-builder .image-placeholder .filename { 150 | position: absolute; 151 | bottom: 0; 152 | left: 0; 153 | right: -1px; 154 | border-top: 1px solid #CCC; 155 | padding: 3px; 156 | line-height: 1.2; 157 | font-size: 12px; 158 | background: #FAFAFA; } 159 | .modular-page-builder .image-placeholder .spinner { 160 | display: block; 161 | position: absolute; 162 | left: 50%; 163 | top: 50%; 164 | margin: 0; 165 | margin-left: -10px; 166 | margin-top: -10px; } 167 | .modular-page-builder .image-requirements { 168 | color: #666; 169 | font-style: italic; 170 | margin-top: 0; } 171 | .modular-page-builder .wp-picker-container { 172 | position: relative; 173 | width: 120px; } 174 | .modular-page-builder .wp-picker-container .wp-picker-open + .wp-picker-input-wrap { 175 | position: relative; 176 | top: 3px; } 177 | .modular-page-builder .wp-picker-container .wp-color-picker { 178 | height: 24px; 179 | padding-top: 0; 180 | padding-bottom: 0; 181 | border-left: none; 182 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08) inset; 183 | border-top-right-radius: 3px !important; 184 | border-bottom-right-radius: 3px !important; } 185 | .modular-page-builder .wp-picker-container .wp-color-result:active { 186 | box-shadow: 0 0 0 1px #5b9dd9, 0 0 2px 1px rgba(30, 140, 190, 0.8); } 187 | .modular-page-builder .wp-picker-container .wp-color-picker:focus { 188 | outline: none; 189 | box-shadow: none; 190 | border-color: #ccc; 191 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08) inset, 0 1px 0 rgba(0, 0, 0, 0.08); } 192 | .modular-page-builder .wp-picker-container .wp-picker-clear { 193 | display: none; } 194 | .modular-page-builder .wp-picker-container .wp-picker-open.wp-color-result { 195 | margin-right: 0; 196 | border-top-right-radius: 0 !important; 197 | border-bottom-right-radius: 0 !important; } 198 | .modular-page-builder .wp-picker-container .wp-picker-open.wp-color-result:after { 199 | display: none; } 200 | .modular-page-builder .wp-picker-container .wp-picker-holder { 201 | box-shadow: rgba(0, 0, 0, 0.1) 1px 1px 2px; 202 | position: absolute; 203 | z-index: 999; } 204 | .modular-page-builder .form-field-repeatable input { 205 | margin-bottom: 8px; } 206 | .modular-page-builder .select2-container { 207 | width: 25em; 208 | max-width: 90%; 209 | z-index: auto; } 210 | 211 | .select2-search-choice-close { 212 | transition: none; } 213 | 214 | .builder-grid .selection { 215 | margin: -1px -1px 0; 216 | display: -ms-flexbox; 217 | display: flex; 218 | -ms-flex-wrap: wrap; 219 | flex-wrap: wrap; } 220 | 221 | .builder-grid .selection:after { 222 | content: ""; 223 | display: table; 224 | clear: both; } 225 | 226 | .builder-grid .module-edit { 227 | display: inline-block; 228 | width: 33.333%; 229 | box-sizing: border-box; 230 | vertical-align: top; 231 | background: #FFFFFF; 232 | box-shadow: 1px 0 0 0 #e5e5e5, 0 1px 0 0 #e5e5e5, 1px 1px 0 0 #e5e5e5, 1px 0 0 0 #e5e5e5 inset, 0 1px 0 0 #e5e5e5 inset; } 233 | 234 | .builder-grid .module-edit + .module-edit { 235 | border-top: none; } 236 | 237 | .builder-grid .ui-sortable-placeholder { 238 | box-shadow: none; 239 | background: #F5F5F5; } 240 | 241 | .builder-grid .module-edit-tools { 242 | margin: -16px -17px 15px -16px; } 243 | 244 | .builder-grid .form-row { 245 | padding-left: 0; } 246 | 247 | .builder-grid .form-row-label { 248 | float: none; 249 | display: block; 250 | clear: both; 251 | margin-left: 0; 252 | margin-bottom: 10px; } 253 | 254 | .builder-grid input { 255 | max-width: 100%; } 256 | 257 | .builder-grid .modular-page-builder .add-new { 258 | z-index: 1; 259 | position: relative; } 260 | 261 | .filter-notice { 262 | display: inline-block; 263 | position: relative; 264 | margin: 13px 0 0 0; 265 | border: 1px solid #c83434; 266 | padding: 2px 9px; 267 | background-color: #f6d3d4; } 268 | .media-toolbar .filter-notice { 269 | float: left; 270 | margin-right: 20px; } 271 | .attachment-info .filter-notice { 272 | margin-top: 0; 273 | margin-bottom: 13px; } 274 | 275 | .number-text { 276 | width: 4em; } 277 | /*# sourceMappingURL=modular-page-builder.css.map */ -------------------------------------------------------------------------------- /assets/css/dist/modular-page-builder.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../src/modular-page-builder.scss"],"names":[],"mappings":"AAKA;EACC,iBAAiB,EACjB;;AAED;EAEC,oBAVoB;EAWpB,0BAZqB;EAarB,gDAA2C;EAC3C,cAAc,EAqSd;EA1SD;IAQE,cAAc;IACd,oBAhBmB;IAiBnB,8BAnBoB,EAoBpB;EAXF;IAcE,iBAAiB,EACjB;EAfF;IAkBE,cAAc;IACd,iBAAiB;IACjB,oBAAoB;IACpB,mBAAmB,EAOnB;IA5BF;MAwBG,YAAY;MACZ,eAAe;MACf,YAAY,EACZ;EA3BH;IA+BE,+BAxCoB,EAyCpB;EAhCF;IAmCE,iBAAiB,EACjB;EApCF;IAuCE,0BA9CmB;IA+CnB,+BAA+B;IAC/B,kBAAkB;IAClB,0BAnDoB;IAoDpB,eAAe,EAyBf;IApEF;MA8CG,qBAAqB;MACrB,YAAY;MACZ,2BAA2B;MAC3B,oCAAoC;MACpC,sBAAsB,EACtB;IAnDH;MAsDG,cAAc;MACd,aAAa;MACb,UAAU;MACV,kBAAkB;MAClB,mBAAmB,EACnB;IA3DH;MA8DG,aAAa,EAIb;MAlEH;QAgEI,eAAe,EACf;EAjEJ;IAuEE,oBAAoB;IACpB,YAAY;IACZ,oBAAoB,EAKpB;IA9EF;MA4EG,iBAAiB,EACjB;EA7EH;IAiFE,YAAY;IACZ,oBAAoB;IACpB,aAAa;IACb,gBAAgB,EAChB;EArFF;IAyFE,sBAAsB;IACtB,oBAAoB;IACpB,mBAAmB;IACnB,mBAAmB;IACnB,gBAAgB,EAehB;IA5GF;MAgGG,sBAAsB;MACtB,eAAe;MACf,mBAAmB;MACnB,mBAAmB;MACnB,YAAY,EACZ;IArGH;MAwGG,iBAAiB;MACjB,mBAAmB;MACnB,SAAS,EACT;EA3GH;IA+GE,cAAc;IACd,sBAAsB,EACtB;EAjHF;IAoHE,+BAA+B;IAC/B,oBAAoB;IACpB,oBA7HmB;IA8HnB,sBA9HmB;IA+HnB,oBAAoB,EACpB;EAzHF;IA4HE,oBApImB;IAqInB,WAAW,EACX;EA9HF;IAiIE,gBAAgB,EAChB;EAlIF;IAqIE,gBAAgB;IAChB,gBAAgB;IAChB,oBAAoB,EACpB;EAxIF;IA2IE,oBAAoB,EACpB;EA5IF;IAgJE,2BAzJoB;IA0JpB,mBAAmB;IACnB,aAAa;IACb,cAAc;IACd,mBAAmB;IACnB,mBAAmB;IACnB,mBAAmB;IACnB,eAAe;IACf,YAAY;IACZ,mBAAmB;IACnB,oBAAoB,EAyEpB;IAnOF;MA6JG,uBAAuB,EACvB;IA9JH;MAiKG,mBAAmB;MACnB,SAAS;MACT,WAAW;MACX,YAAY;MACZ,WAAW;MACX,WAAW;MACX,6DAA8C,EAC9C;IAxKH;MA2KG,aAAa;MACb,eAAe;MACf,mBAAmB;MACnB,YAAY;MACZ,aAAa;MACb,qBAAc;MAAd,cAAc;MACd,uBAAoB;UAApB,oBAAoB,EAYpB;MA7LH;QAoLI,aAAa;QACb,mBAAmB;QACnB,OAAO;QACP,QAAQ;QACR,YAAY;QACZ,aAAa;QACb,gDAA+C;QAC/C,qBAAqB,EACrB;IA5LJ;MAgMG,eAAe,EACf;IAjMH;MAoMG,YAAY;MACZ,aAAa;MACb,mBAAmB;MACnB,UAAU;MACV,SAAS;MACT,kBAAkB;MAClB,qCAAoB;UAApB,iCAAoB,EACpB;IA3MH;MA8MG,mBAAmB;MACnB,UAAU;MACV,QAAQ;MACR,YAAY;MACZ,2BAA2B;MAC3B,aAAa;MACb,iBAAiB;MACjB,gBAAgB;MAChB,oBAAoB,EACpB;IAvNH;MA0NG,eAAe;MACf,mBAAmB;MACnB,UAAU;MACV,SAAS;MACT,UAAU;MACV,mBAAmB;MACnB,kBAAkB,EAClB;EAjOH;IAsOE,YAAY;IACZ,mBAAmB;IACnB,cAAc,EACd;EAzOF;IA6OE,mBAAmB;IACnB,aAAa,EAiDb;IA/RF;MAiPG,mBAAmB;MACnB,SAAS,EACT;IAnPH;MAuPG,aAAa;MACb,eAAe;MACf,kBAAkB;MAClB,kBAAkB;MAClB,8CAAyC;MACzC,wCAAwC;MACxC,2CAA2C,EAC3C;IA9PH;MAkQG,mEAA8C,EAC9C;IAnQH;MAsQG,cAAc;MACd,iBAAiB;MACjB,mBAAmB;MACnB,2EAAuD,EACvD;IA1QH;MA6QG,cAAc,EACd;IA9QH;MAiRG,gBAAgB;MAChB,sCAAsC;MACtC,yCAAyC,EACzC;IApRH;MAuRG,cAAc,EACd;IAxRH;MA2RG,2CAA0C;MAC1C,mBAAmB;MACnB,aAAa,EACb;EA9RH;IAkSE,mBAAmB,EACnB;EAnSF;IAsSE,YAAY;IACZ,eAAe;IACf,cAAc,EACd;;AAIF;EACC,iBAAiB,EACjB;;AAED;EAGE,oBAAoB;EACpB,qBAAc;EAAd,cAAc;EACd,oBAAgB;MAAhB,gBAAgB,EAChB;;AANF;EASE,YAAY;EACZ,eAAe;EACf,YAAY,EACZ;;AAZF;EAeE,sBAAsB;EACtB,eAAe;EACf,uBAAuB;EACvB,oBAAoB;EACpB,oBA5UmB;EA6UnB,wHAAqJ,EACrJ;;AArBF;EAwBE,iBAAiB,EACjB;;AAzBF;EA4BE,iBAAiB;EACjB,oBApVmB,EAqVnB;;AA9BF;EAiCE,+BAA+B,EAC/B;;AAlCF;EAqCE,gBAAgB,EAChB;;AAtCF;EAyCE,YAAY;EACZ,eAAe;EACf,YAAY;EACZ,eAAe;EACf,oBAAoB,EACpB;;AA9CF;EAiDE,gBAAgB,EAChB;;AAlDF;EAqDE,WAAW;EACX,mBAAmB,EACnB;;AAKF;EAEC,sBAAsB;EACtB,mBAAmB;EACnB,mBAAmB;EACnB,0BAA0B;EAC1B,iBAAiB;EACjB,0BAA0B,EAY1B;EAnBD;IAUE,YAAY;IACZ,mBAAmB,EACnB;EAZF;IAeE,cAAc;IACd,oBAAoB,EACpB;;AAIF;EACC,WAAW,EACX","file":"modular-page-builder.css"} -------------------------------------------------------------------------------- /assets/css/src/modular-page-builder.scss: -------------------------------------------------------------------------------- 1 | $border-color: #e5e5e5; 2 | $bg-color-lt: #FFFFFF; 3 | $bg-color-md: #F5F5F5; 4 | $bg-color-dk: #F5F5F5; 5 | 6 | .modular-page-builder-container { 7 | margin-top: 10px; 8 | } 9 | 10 | .modular-page-builder { 11 | 12 | background: $bg-color-lt; 13 | border: 1px solid $border-color; 14 | box-shadow: 0 1px 1px rgba(0,0,0,.04) inset; 15 | margin: 8px 0; 16 | 17 | .add-new { 18 | padding: 16px; 19 | background: $bg-color-md; 20 | border-top: 1px solid $border-color; 21 | } 22 | 23 | .selection:empty + .add-new { 24 | border-top: none; 25 | } 26 | 27 | .module-edit { 28 | padding: 16px; 29 | margin-bottom: 0; 30 | border-width: 1px 0; 31 | position: relative; 32 | 33 | &:after { 34 | content: ""; 35 | display: table; 36 | clear: both; 37 | } 38 | } 39 | 40 | .module-edit + .module-edit { 41 | border-top: 1px dashed $border-color; 42 | } 43 | 44 | .ui-sortable-helper:first-child + .module-edit { 45 | border-top: none; 46 | } 47 | 48 | .module-edit-tools { 49 | background-color: $bg-color-md; 50 | margin: -17px -17px 15px -17px; 51 | padding: 8px 16px; 52 | border: 1px solid $border-color; 53 | overflow: auto; 54 | 55 | .module-edit-title { 56 | margin: 0 !important; 57 | float: left; 58 | font-size: 14px !important; 59 | line-height: 1.714285714 !important; 60 | padding: 0 !important; 61 | } 62 | 63 | .button-selection-item-remove { 64 | display: none; 65 | float: right; 66 | margin: 0; 67 | padding-left: 1px; 68 | padding-right: 1px; 69 | } 70 | 71 | &.ui-sortable-handle { 72 | cursor: move; 73 | .button-selection-item-remove { 74 | display: block; 75 | } 76 | } 77 | 78 | } 79 | 80 | .form-row { 81 | margin-bottom: 16px; 82 | clear: both; 83 | padding-left: 150px; 84 | 85 | &:last-child { 86 | margin-bottom: 0; 87 | } 88 | } 89 | 90 | .form-row-label { 91 | float: left; 92 | margin-left: -150px; 93 | width: 140px; 94 | margin-top: 5px; 95 | } 96 | 97 | .form-row-inline { 98 | 99 | display: inline-block; 100 | vertical-align: top; 101 | margin-right: 10px; 102 | margin-bottom: 8px; 103 | padding-left: 0; 104 | 105 | label { 106 | display: inline-block; 107 | margin-left: 0; 108 | margin-right: 20px; 109 | margin-bottom: 8px; 110 | width: auto; 111 | } 112 | 113 | .wp-color-result { 114 | margin-bottom: 0; 115 | position: relative; 116 | top: 3px; 117 | } 118 | } 119 | 120 | .description { 121 | margin: 8px 0; 122 | display: inline-block; 123 | } 124 | 125 | .ui-sortable-placeholder { 126 | visibility: visible !important; 127 | border-style: solid; 128 | background: $bg-color-md; 129 | border-color: $bg-color-md; 130 | margin-bottom: -1px; 131 | } 132 | 133 | .ui-sortable-helper { 134 | background: $bg-color-lt; 135 | opacity: 1; 136 | } 137 | 138 | .button-small .dashicons-no { 139 | margin-top: 1px; 140 | } 141 | 142 | textarea { 143 | max-width: 100%; 144 | min-width: 100%; 145 | vertical-align: top; 146 | } 147 | 148 | .image-field-controls { 149 | margin-bottom: 16px; 150 | } 151 | 152 | .image-placeholder { 153 | 154 | border: 1px dashed $border-color; 155 | border-radius: 3px; 156 | width: 148px; 157 | height: 148px; 158 | line-height: 148px; 159 | text-align: center; 160 | position: relative; 161 | display: block; 162 | float: left; 163 | margin-right: 16px; 164 | margin-bottom: 16px; 165 | 166 | .button.add { 167 | vertical-align: middle; 168 | } 169 | 170 | .button.remove { 171 | position: absolute; 172 | top: 5px; 173 | right: 5px; 174 | width: 24px; 175 | padding: 0; 176 | z-index: 1; 177 | box-shadow: inset 0 1px 0 #fff, 0 0 0 1px rgba(0,0,0,0.1); 178 | } 179 | 180 | .image { 181 | margin: -1px; 182 | display: block; 183 | position: relative; 184 | width: 100%; 185 | height: 100%; 186 | display: flex; 187 | align-items: center; 188 | 189 | &:after { 190 | content: ' '; 191 | position: absolute; 192 | top: 0; 193 | left: 0; 194 | right: -2px; 195 | bottom: -2px; 196 | box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25) inset; 197 | pointer-events: none; 198 | } 199 | } 200 | 201 | img { 202 | display: block; 203 | } 204 | 205 | img.icon { 206 | width: auto; 207 | height: auto; 208 | position: absolute; 209 | left: 50%; 210 | top: 50%; 211 | margin-top: -15px; 212 | transform: translate(-50%, -50%); 213 | } 214 | 215 | .filename { 216 | position: absolute; 217 | bottom: 0; 218 | left: 0; 219 | right: -1px; 220 | border-top: 1px solid #CCC; 221 | padding: 3px; 222 | line-height: 1.2; 223 | font-size: 12px; 224 | background: #FAFAFA; 225 | } 226 | 227 | .spinner { 228 | display: block; 229 | position: absolute; 230 | left: 50%; 231 | top: 50%; 232 | margin: 0; 233 | margin-left: -10px; 234 | margin-top: -10px; 235 | } 236 | 237 | } 238 | 239 | .image-requirements { 240 | color: #666; 241 | font-style: italic; 242 | margin-top: 0; 243 | } 244 | 245 | .wp-picker-container { 246 | 247 | position: relative; 248 | width: 120px; 249 | 250 | .wp-picker-open + .wp-picker-input-wrap { 251 | position: relative; 252 | top: 3px; 253 | } 254 | 255 | 256 | .wp-color-picker { 257 | height: 24px; 258 | padding-top: 0; 259 | padding-bottom: 0; 260 | border-left: none; 261 | box-shadow: 0 1px 0 rgba(0,0,0,.08) inset; 262 | border-top-right-radius: 3px !important; 263 | border-bottom-right-radius: 3px !important; 264 | } 265 | 266 | .wp-color-result:active { 267 | -webkit-box-shadow: 0 0 0 1px #5b9dd9,0 0 2px 1px rgba(30,140,190,.8); 268 | box-shadow: 0 0 0 1px #5b9dd9,0 0 2px 1px rgba(30,140,190,.8); 269 | } 270 | 271 | .wp-color-picker:focus { 272 | outline: none; 273 | box-shadow: none; 274 | border-color: #ccc; 275 | box-shadow: 0 1px 0 rgba(0,0,0,.08) inset, 0 1px 0 rgba(0,0,0,.08); 276 | } 277 | 278 | .wp-picker-clear { 279 | display: none; 280 | } 281 | 282 | .wp-picker-open.wp-color-result { 283 | margin-right: 0; 284 | border-top-right-radius: 0 !important; 285 | border-bottom-right-radius: 0 !important; 286 | } 287 | 288 | .wp-picker-open.wp-color-result:after { 289 | display: none; 290 | } 291 | 292 | .wp-picker-holder { 293 | box-shadow: rgba(0, 0, 0, 0.1) 1px 1px 2px; 294 | position: absolute; 295 | z-index: 999; 296 | } 297 | } 298 | 299 | .form-field-repeatable input { 300 | margin-bottom: 8px; 301 | } 302 | 303 | .select2-container { 304 | width: 25em; 305 | max-width: 90%; 306 | z-index: auto; 307 | } 308 | } 309 | 310 | 311 | .select2-search-choice-close { 312 | transition: none; 313 | } 314 | 315 | .builder-grid { 316 | 317 | .selection { 318 | margin: -1px -1px 0; 319 | display: flex; 320 | flex-wrap: wrap; 321 | } 322 | 323 | .selection:after { 324 | content: ""; 325 | display: table; 326 | clear: both; 327 | } 328 | 329 | .module-edit { 330 | display: inline-block; 331 | width: 33.333%; 332 | box-sizing: border-box; 333 | vertical-align: top; 334 | background: $bg-color-lt; 335 | box-shadow: 1px 0 0 0 $border-color, 0 1px 0 0 $border-color, 1px 1px 0 0 $border-color, 1px 0 0 0 $border-color inset, 0 1px 0 0 $border-color inset; 336 | } 337 | 338 | .module-edit + .module-edit { 339 | border-top: none; 340 | } 341 | 342 | .ui-sortable-placeholder { 343 | box-shadow: none; 344 | background: $bg-color-dk; 345 | } 346 | 347 | .module-edit-tools { 348 | margin: -16px -17px 15px -16px; 349 | } 350 | 351 | .form-row { 352 | padding-left: 0; 353 | } 354 | 355 | .form-row-label { 356 | float: none; 357 | display: block; 358 | clear: both; 359 | margin-left: 0; 360 | margin-bottom: 10px; 361 | } 362 | 363 | input { 364 | max-width: 100%; 365 | } 366 | 367 | .modular-page-builder .add-new { 368 | z-index: 1; 369 | position: relative; 370 | } 371 | 372 | } 373 | 374 | 375 | .filter-notice { 376 | 377 | display: inline-block; 378 | position: relative; 379 | margin: 13px 0 0 0; 380 | border: 1px solid #c83434; 381 | padding: 2px 9px; 382 | background-color: #f6d3d4; 383 | 384 | .media-toolbar & { 385 | float: left; 386 | margin-right: 20px; 387 | } 388 | 389 | .attachment-info & { 390 | margin-top: 0; 391 | margin-bottom: 13px; 392 | } 393 | 394 | } 395 | 396 | .number-text { 397 | width: 4em; 398 | } 399 | -------------------------------------------------------------------------------- /assets/js/src/collections/module-attributes.js: -------------------------------------------------------------------------------- 1 | var Backbone = require('backbone'); 2 | var ModuleAttribute = require('models/module-attribute'); 3 | 4 | /** 5 | * Shortcode Attributes collection. 6 | */ 7 | var ShortcodeAttributes = Backbone.Collection.extend({ 8 | 9 | model : ModuleAttribute, 10 | 11 | // Deep Clone. 12 | clone: function() { 13 | return new this.constructor( _.map( this.models, function(m) { 14 | return m.clone(); 15 | })); 16 | }, 17 | 18 | /** 19 | * Return only the data that needs to be saved. 20 | * 21 | * @return object 22 | */ 23 | toMicroJSON: function() { 24 | 25 | var json = {}; 26 | 27 | this.each( function( model ) { 28 | json[ model.get( 'name' ) ] = model.toMicroJSON(); 29 | } ); 30 | 31 | return json; 32 | }, 33 | 34 | 35 | }); 36 | 37 | module.exports = ShortcodeAttributes; 38 | -------------------------------------------------------------------------------- /assets/js/src/collections/modules.js: -------------------------------------------------------------------------------- 1 | var Backbone = require('backbone'); 2 | var Module = require('models/module'); 3 | 4 | // Shortcode Collection 5 | var Modules = Backbone.Collection.extend({ 6 | 7 | model : Module, 8 | 9 | // Deep Clone. 10 | clone : function() { 11 | return new this.constructor( _.map( this.models, function(m) { 12 | return m.clone(); 13 | })); 14 | }, 15 | 16 | /** 17 | * Return only the data that needs to be saved. 18 | * 19 | * @return object 20 | */ 21 | toMicroJSON: function( options ) { 22 | return this.map( function(model) { return model.toMicroJSON( options ); } ); 23 | }, 24 | }); 25 | 26 | module.exports = Modules; 27 | -------------------------------------------------------------------------------- /assets/js/src/globals.js: -------------------------------------------------------------------------------- 1 | // Expose some functionality globally. 2 | var globals = { 3 | Builder: require('models/builder'), 4 | ModuleFactory: require('utils/module-factory'), 5 | editViews: require('utils/edit-views'), 6 | fieldViews: require('utils/field-views'), 7 | views: { 8 | BuilderView: require('views/builder'), 9 | ModuleEdit: require('views/module-edit'), 10 | ModuleEditDefault: require('views/module-edit-default'), 11 | Field: require('views/fields/field'), 12 | FieldLink: require('views/fields/field-link'), 13 | FieldAttachment: require('views/fields/field-attachment'), 14 | FieldText: require('views/fields/field-text'), 15 | FieldTextarea: require('views/fields/field-textarea'), 16 | FieldWysiwyg: require('views/fields/field-wysiwyg'), 17 | FieldPostSelect: require('views/fields/field-post-select'), 18 | }, 19 | instance: {}, 20 | }; 21 | 22 | module.exports = globals; 23 | -------------------------------------------------------------------------------- /assets/js/src/models/builder.js: -------------------------------------------------------------------------------- 1 | var Backbone = require('backbone'); 2 | var Modules = require('collections/modules'); 3 | var ModuleFactory = require('utils/module-factory'); 4 | 5 | var Builder = Backbone.Model.extend({ 6 | 7 | defaults: { 8 | selectDefault: modularPageBuilderData.l10n.selectDefault, 9 | addNewButton: modularPageBuilderData.l10n.addNewButton, 10 | selection: [], // Instance of Modules. Can't use a default, otherwise they won't be unique. 11 | allowedModules: [], // Module names allowed for this builder. 12 | requiredModules: [], // Module names required for this builder. They will be required in this order, at these positions. 13 | }, 14 | 15 | initialize: function() { 16 | 17 | // Set default selection to ensure it isn't a reference. 18 | if ( ! ( this.get( 'selection' ) instanceof Modules ) ) { 19 | this.set( 'selection', new Modules() ); 20 | } 21 | 22 | this.get( 'selection' ).on( 'change reset add remove', this.setRequiredModules, this ); 23 | this.setRequiredModules(); 24 | }, 25 | 26 | setData: function( data ) { 27 | 28 | var _selection; 29 | 30 | if ( '' === data ) { 31 | return; 32 | } 33 | 34 | // Handle either JSON string or proper obhect. 35 | data = ( 'string' === typeof data ) ? JSON.parse( data ) : data; 36 | 37 | // Convert saved data to Module models. 38 | if ( data && Array.isArray( data ) ) { 39 | _selection = data.map( function( module ) { 40 | return ModuleFactory.create( module.name, module.attr ); 41 | } ); 42 | } 43 | 44 | // Reset selection using data from hidden input. 45 | if ( _selection && _selection.length ) { 46 | this.get('selection').reset( _selection ); 47 | } else { 48 | this.get('selection').reset( [] ); 49 | } 50 | 51 | }, 52 | 53 | saveData: function() { 54 | 55 | var data = []; 56 | 57 | this.get('selection').each( function( module ) { 58 | 59 | // Skip empty/broken modules. 60 | if ( ! module.get('name' ) ) { 61 | return; 62 | } 63 | 64 | data.push( module.toMicroJSON() ); 65 | 66 | } ); 67 | 68 | this.trigger( 'save', data ); 69 | 70 | }, 71 | 72 | /** 73 | * List all available modules for this builder. 74 | * All modules, filtered by this.allowedModules. 75 | */ 76 | getAvailableModules: function() { 77 | return _.filter( ModuleFactory.availableModules, function( module ) { 78 | return this.isModuleAllowed( module.name ); 79 | }.bind( this ) ); 80 | }, 81 | 82 | isModuleAllowed: function( moduleName ) { 83 | return this.get('allowedModules').indexOf( moduleName ) >= 0; 84 | }, 85 | 86 | setRequiredModules: function() { 87 | var selection = this.get( 'selection' ); 88 | var required = this.get( 'requiredModules' ); 89 | 90 | if ( ! selection || ! required || required.length < 1 ) { 91 | return; 92 | } 93 | 94 | for ( var i = 0; i < required.length; i++ ) { 95 | if ( 96 | ( ! selection.at( i ) || selection.at( i ).get( 'name' ) !== required[ i ] ) && 97 | this.isModuleAllowed( required[ i ] ) 98 | ) { 99 | var module = ModuleFactory.create( required[ i ], [], { sortable: false } ); 100 | selection.add( module, { at: i, silent: true } ); 101 | } else if ( selection.at( i ) && selection.at( i ).get( 'name' ) === required[ i ] ) { 102 | selection.at( i ).set( 'sortable', false ); 103 | } 104 | } 105 | } 106 | 107 | }); 108 | 109 | module.exports = Builder; 110 | -------------------------------------------------------------------------------- /assets/js/src/models/module-attribute.js: -------------------------------------------------------------------------------- 1 | var Backbone = require('backbone'); 2 | 3 | var ModuleAttribute = Backbone.Model.extend({ 4 | 5 | defaults: { 6 | name: '', 7 | label: '', 8 | value: '', 9 | type: 'text', 10 | description: '', 11 | defaultValue: '', 12 | config: {} 13 | }, 14 | 15 | /** 16 | * Return only the data that needs to be saved. 17 | * 18 | * @return object 19 | */ 20 | toMicroJSON: function() { 21 | 22 | var r = {}; 23 | var allowedAttrProperties = [ 'name', 'value', 'type' ]; 24 | 25 | _.each( allowedAttrProperties, function( prop ) { 26 | r[ prop ] = this.get( prop ); 27 | }.bind(this) ); 28 | 29 | return r; 30 | 31 | } 32 | 33 | }); 34 | 35 | module.exports = ModuleAttribute; 36 | -------------------------------------------------------------------------------- /assets/js/src/models/module.js: -------------------------------------------------------------------------------- 1 | var Backbone = require('backbone'); 2 | var ModuleAtts = require('collections/module-attributes'); 3 | 4 | var Module = Backbone.Model.extend({ 5 | 6 | defaults: { 7 | name: '', 8 | label: '', 9 | attr: [], 10 | sortable: true, 11 | }, 12 | 13 | initialize: function() { 14 | // Set default selection to ensure it isn't a reference. 15 | if ( ! ( this.get('attr') instanceof ModuleAtts ) ) { 16 | this.set( 'attr', new ModuleAtts() ); 17 | } 18 | }, 19 | 20 | /** 21 | * Helper for getting an attribute model by name. 22 | */ 23 | getAttr: function( attrName ) { 24 | return this.get('attr').findWhere( { name: attrName }); 25 | }, 26 | 27 | /** 28 | * Helper for setting an attribute value 29 | * 30 | * Note manual change event trigger to ensure everything is updated. 31 | * 32 | * @param string attribute 33 | * @param mixed value 34 | */ 35 | setAttrValue: function( attribute, value ) { 36 | 37 | var attr = this.getAttr( attribute ); 38 | 39 | if ( attr ) { 40 | attr.set( 'value', value ); 41 | this.trigger( 'change', this ); 42 | } 43 | 44 | }, 45 | 46 | /** 47 | * Helper for getting an attribute value. 48 | * 49 | * Defaults to null. 50 | * 51 | * @param string attribute 52 | */ 53 | getAttrValue: function( attribute ) { 54 | 55 | var attr = this.getAttr( attribute ); 56 | 57 | if ( attr ) { 58 | return attr.get( 'value' ); 59 | } 60 | 61 | }, 62 | 63 | /** 64 | * Custom Parse. 65 | * Ensures attributes is an instance of ModuleAtts 66 | */ 67 | parse: function( response ) { 68 | 69 | if ( 'attr' in response && ! ( response.attr instanceof ModuleAtts ) ) { 70 | response.attr = new ModuleAtts( response.attr ); 71 | } 72 | 73 | return response; 74 | 75 | }, 76 | 77 | toJSON: function() { 78 | 79 | var json = _.clone( this.attributes ); 80 | 81 | if ( 'attr' in json && ( json.attr instanceof ModuleAtts ) ) { 82 | json.attr = json.attr.toJSON(); 83 | } 84 | 85 | return json; 86 | 87 | }, 88 | 89 | /** 90 | * Return only the data that needs to be saved. 91 | * 92 | * @return object 93 | */ 94 | toMicroJSON: function() { 95 | return { 96 | name: this.get('name'), 97 | attr: this.get('attr').toMicroJSON() 98 | }; 99 | }, 100 | 101 | }); 102 | 103 | module.exports = Module; 104 | -------------------------------------------------------------------------------- /assets/js/src/modular-page-builder.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var Builder = require('models/builder'); 3 | var BuilderView = require('views/builder'); 4 | var ModuleFactory = require('utils/module-factory'); 5 | 6 | // Expose some functionality to global namespace. 7 | window.modularPageBuilder = require('./globals'); 8 | 9 | $(document).ready(function(){ 10 | 11 | ModuleFactory.init(); 12 | 13 | // A field for storing the builder data. 14 | var $field = $( '[name=modular-page-builder-data]' ); 15 | 16 | if ( ! $field.length ) { 17 | return; 18 | } 19 | 20 | // A container element for displaying the builder. 21 | var $container = $( '#modular-page-builder' ); 22 | var allowedModules = $( '[name=modular-page-builder-allowed-modules]' ).val().split(','); 23 | var requiredModules = $( '[name=modular-page-builder-required-modules]' ).val().split(','); 24 | 25 | // Strip empty values. 26 | allowedModules = allowedModules.filter( function( val ) { return val !== ''; } ); 27 | requiredModules = requiredModules.filter( function( val ) { return val !== ''; } ); 28 | 29 | // Create a new instance of Builder model. 30 | // Pass an array of module names that are allowed for this builder. 31 | var builder = new Builder({ 32 | allowedModules: allowedModules, 33 | requiredModules: requiredModules, 34 | }); 35 | 36 | // Set the data using the current field value 37 | builder.setData( JSON.parse( $field.val() ) ); 38 | 39 | // On save, update the field value. 40 | builder.on( 'save', function( data ) { 41 | $field.val( JSON.stringify( data ) ); 42 | } ); 43 | 44 | // Create builder view. 45 | var builderView = new BuilderView( { model: builder } ); 46 | 47 | // Render builder. 48 | builderView.render().$el.appendTo( $container ); 49 | 50 | // Store a reference on global modularPageBuilder for modification by plugins. 51 | window.modularPageBuilder.instance.primary = builderView; 52 | }); 53 | -------------------------------------------------------------------------------- /assets/js/src/utils/edit-views.js: -------------------------------------------------------------------------------- 1 | var ModuleEditDefault = require('views/module-edit-default'); 2 | 3 | /** 4 | * Map module type to views. 5 | */ 6 | var editViews = { 7 | 'default': ModuleEditDefault 8 | }; 9 | 10 | module.exports = editViews; 11 | -------------------------------------------------------------------------------- /assets/js/src/utils/field-views.js: -------------------------------------------------------------------------------- 1 | var FieldText = require('views/fields/field-text'); 2 | var FieldTextarea = require('views/fields/field-textarea'); 3 | var FieldWYSIWYG = require('views/fields/field-wysiwyg'); 4 | var FieldAttachment = require('views/fields/field-attachment'); 5 | var FieldLink = require('views/fields/field-link'); 6 | var FieldNumber = require('views/fields/field-number'); 7 | var FieldCheckbox = require('views/fields/field-checkbox'); 8 | var FieldSelect = require('views/fields/field-select'); 9 | var FieldPostSelect = require('views/fields/field-post-select'); 10 | 11 | var fieldViews = { 12 | text: FieldText, 13 | textarea: FieldTextarea, 14 | html: FieldWYSIWYG, 15 | number: FieldNumber, 16 | attachment: FieldAttachment, 17 | link: FieldLink, 18 | checkbox: FieldCheckbox, 19 | select: FieldSelect, 20 | post_select: FieldPostSelect, 21 | }; 22 | 23 | module.exports = fieldViews; 24 | -------------------------------------------------------------------------------- /assets/js/src/utils/module-factory.js: -------------------------------------------------------------------------------- 1 | var Module = require('models/module'); 2 | var ModuleAtts = require('collections/module-attributes'); 3 | var editViews = require('utils/edit-views'); 4 | var $ = require('jquery'); 5 | 6 | var ModuleFactory = { 7 | 8 | availableModules: [], 9 | 10 | init: function() { 11 | if ( modularPageBuilderData && 'available_modules' in modularPageBuilderData ) { 12 | _.each( modularPageBuilderData.available_modules, function( module ) { 13 | this.registerModule( module ); 14 | }.bind( this ) ); 15 | } 16 | }, 17 | 18 | registerModule: function( module ) { 19 | this.availableModules.push( module ); 20 | }, 21 | 22 | getModule: function( moduleName ) { 23 | return $.extend( true, {}, _.findWhere( this.availableModules, { name: moduleName } ) ); 24 | }, 25 | 26 | /** 27 | * Create Module Model. 28 | * Use data from config, plus saved data. 29 | * 30 | * @param string moduleName 31 | * @param object Saved attribute data. 32 | * @param object moduleProps. Module properties. 33 | * @return Module 34 | */ 35 | create: function( moduleName, attrData, moduleProps ) { 36 | var data = this.getModule( moduleName ); 37 | var attributes = new ModuleAtts(); 38 | 39 | if ( ! data ) { 40 | return null; 41 | } 42 | 43 | for ( var prop in moduleProps ) { 44 | data[ prop ] = moduleProps[ prop ]; 45 | } 46 | 47 | /** 48 | * Add all the module attributes. 49 | * Whitelisted to attributes documented in schema 50 | * Sets only value from attrData. 51 | */ 52 | _.each( data.attr, function( attr ) { 53 | var cloneAttr = $.extend( true, {}, attr ); 54 | var savedAttr = _.findWhere( attrData, { name: attr.name } ); 55 | 56 | // Add saved attribute values. 57 | if ( savedAttr && 'value' in savedAttr ) { 58 | cloneAttr.value = savedAttr.value; 59 | } 60 | 61 | attributes.add( cloneAttr ); 62 | } ); 63 | 64 | data.attr = attributes; 65 | 66 | return new Module( data ); 67 | }, 68 | 69 | createEditView: function( model ) { 70 | 71 | var editView, moduleName; 72 | 73 | moduleName = model.get('name'); 74 | editView = ( name in editViews ) ? editViews[ moduleName ] : editViews['default']; 75 | 76 | return new editView( { model: model } ); 77 | 78 | }, 79 | 80 | }; 81 | 82 | module.exports = ModuleFactory; 83 | -------------------------------------------------------------------------------- /assets/js/src/views/builder.js: -------------------------------------------------------------------------------- 1 | var wp = require('wp'); 2 | var ModuleFactory = require('utils/module-factory'); 3 | var $ = require('jquery'); 4 | 5 | module.exports = wp.Backbone.View.extend({ 6 | 7 | template: wp.template( 'mpb-builder' ), 8 | className: 'modular-page-builder', 9 | model: null, 10 | newModuleName: null, 11 | 12 | events: { 13 | 'change > .add-new .add-new-module-select': 'toggleButtonStatus', 14 | 'click > .add-new .add-new-module-button': 'addModule', 15 | }, 16 | 17 | initialize: function() { 18 | 19 | var selection = this.model.get('selection'); 20 | 21 | selection.on( 'add', this.addNewSelectionItemView, this ); 22 | selection.on( 'reset set', this.render, this ); 23 | selection.on( 'all', this.model.saveData, this.model ); 24 | 25 | this.on( 'mpb:rendered', this.rendered ); 26 | 27 | }, 28 | 29 | prepare: function() { 30 | var options = this.model.toJSON(); 31 | options.l10n = modularPageBuilderData.l10n; 32 | options.availableModules = this.model.getAvailableModules(); 33 | return options; 34 | }, 35 | 36 | render: function() { 37 | wp.Backbone.View.prototype.render.apply( this, arguments ); 38 | 39 | this.views.remove(); 40 | 41 | this.model.get('selection').each( function( module, i ) { 42 | this.addNewSelectionItemView( module, i ); 43 | }.bind(this) ); 44 | 45 | this.trigger( 'mpb:rendered' ); 46 | return this; 47 | }, 48 | 49 | rendered: function() { 50 | this.initSortable(); 51 | }, 52 | 53 | /** 54 | * Initialize Sortable. 55 | */ 56 | initSortable: function() { 57 | $( '> .selection', this.$el ).sortable({ 58 | handle: '.module-edit-tools', 59 | items: '> .module-edit.module-edit-sortable', 60 | stop: function( e, ui ) { 61 | this.updateSelectionOrder( ui ); 62 | this.triggerSortStop( ui.item.attr( 'data-cid') ); 63 | }.bind( this ) 64 | }); 65 | }, 66 | 67 | /** 68 | * Sortable end callback. 69 | * After reordering, update the selection order. 70 | * Note - uses direct manipulation of collection models property. 71 | * This is to avoid having to mess about with the views themselves. 72 | */ 73 | updateSelectionOrder: function( ui ) { 74 | 75 | var selection = this.model.get('selection'); 76 | var item = selection.get({ cid: ui.item.attr( 'data-cid') }); 77 | var newIndex = ui.item.index(); 78 | var oldIndex = selection.indexOf( item ); 79 | 80 | if ( newIndex !== oldIndex ) { 81 | var dropped = selection.models.splice( oldIndex, 1 ); 82 | selection.models.splice( newIndex, 0, dropped[0] ); 83 | this.model.saveData(); 84 | } 85 | 86 | }, 87 | 88 | /** 89 | * Trigger sort stop on subView (by model CID). 90 | */ 91 | triggerSortStop: function( cid ) { 92 | 93 | var views = this.views.get( '> .selection' ); 94 | 95 | if ( views && views.length ) { 96 | 97 | var view = _.find( views, function( view ) { 98 | return cid === view.model.cid; 99 | } ); 100 | 101 | if ( view && ( 'refresh' in view ) ) { 102 | view.refresh(); 103 | } 104 | 105 | } 106 | 107 | }, 108 | 109 | /** 110 | * Toggle button status. 111 | * Enable/Disable button depending on whether 112 | * placeholder or valid module is selected. 113 | */ 114 | toggleButtonStatus: function(e) { 115 | var value = $(e.target).val(); 116 | var defaultOption = $(e.target).children().first().attr('value'); 117 | $('.add-new-module-button', this.$el ).attr( 'disabled', value === defaultOption ); 118 | this.newModuleName = ( value !== defaultOption ) ? value : null; 119 | }, 120 | 121 | /** 122 | * Handle adding module. 123 | * 124 | * Find module model. Clone it. Add to selection. 125 | */ 126 | addModule: function(e) { 127 | 128 | e.preventDefault(); 129 | 130 | if ( this.newModuleName && this.model.isModuleAllowed( this.newModuleName ) ) { 131 | var model = ModuleFactory.create( this.newModuleName ); 132 | this.model.get('selection').add( model ); 133 | } 134 | 135 | }, 136 | 137 | /** 138 | * Append new selection item view. 139 | */ 140 | addNewSelectionItemView: function( item, index ) { 141 | 142 | if ( ! this.model.isModuleAllowed( item.get('name') ) ) { 143 | return; 144 | } 145 | 146 | var views = this.views.get( '> .selection' ); 147 | var view = ModuleFactory.createEditView( item ); 148 | var options = {}; 149 | 150 | // If the item at this index, is already representing this item, return. 151 | if ( views && views[ index ] && views[ index ].$el.data( 'cid' ) === item.cid ) { 152 | return; 153 | } 154 | 155 | // If the item exists at wrong index, remove it. 156 | if ( views ) { 157 | var matches = views.filter( function( itemView ) { 158 | return item.cid === itemView.$el.data( 'cid' ); 159 | } ); 160 | if ( matches.length > 0 ) { 161 | this.views.unset( matches ); 162 | } 163 | } 164 | 165 | if ( index ) { 166 | options.at = index; 167 | } 168 | 169 | this.views.add( '> .selection', view, options ); 170 | 171 | var $selection = $( '> .selection', this.$el ); 172 | if ( $selection.hasClass('ui-sortable') ) { 173 | $selection.sortable('refresh'); 174 | } 175 | 176 | }, 177 | 178 | }); 179 | -------------------------------------------------------------------------------- /assets/js/src/views/fields/field-attachment.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var wp = require('wp'); 3 | var Field = require('views/fields/field'); 4 | 5 | /** 6 | * Image Field 7 | * 8 | * Initialize and listen for the 'change' event to get updated data. 9 | * 10 | */ 11 | var FieldAttachment = Field.extend({ 12 | 13 | template: wp.template( 'mpb-field-attachment' ), 14 | frame: null, 15 | value: [], // Attachment IDs. 16 | selection: {}, // Attachments collection for this.value. 17 | 18 | config: {}, 19 | 20 | defaultConfig: { 21 | multiple: false, 22 | library: { type: 'image' }, 23 | button_text: 'Select Image', 24 | }, 25 | 26 | events: { 27 | 'click .button.add': 'editImage', 28 | 'click .image-placeholder .button.remove': 'removeImage', 29 | }, 30 | 31 | /** 32 | * Initialize. 33 | * 34 | * Pass value and config as properties on the options object. 35 | * Available options 36 | * - multiple: bool 37 | * - sizeReq: eg { width: 100, height: 100 } 38 | * 39 | * @param object options 40 | * @return null 41 | */ 42 | initialize: function( options ) { 43 | 44 | // Call default initialize. 45 | Field.prototype.initialize.apply( this, [ options ] ); 46 | 47 | _.bindAll( this, 'render', 'editImage', 'onSelectImage', 'removeImage', 'isAttachmentSizeOk' ); 48 | 49 | this.on( 'change', this.render ); 50 | this.on( 'mpb:rendered', this.rendered ); 51 | 52 | this.initSelection(); 53 | 54 | }, 55 | 56 | setValue: function( value ) { 57 | 58 | // Ensure value is array. 59 | if ( ! value || ! Array.isArray( value ) ) { 60 | value = []; 61 | } 62 | 63 | Field.prototype.setValue.apply( this, [ value ] ); 64 | 65 | }, 66 | 67 | /** 68 | * Initialize Selection. 69 | * 70 | * Selection is an Attachment collection containing full models for the current value. 71 | * 72 | * @return null 73 | */ 74 | initSelection: function() { 75 | 76 | this.selection = new wp.media.model.Attachments(); 77 | 78 | this.selection.comparator = 'menu-order'; 79 | 80 | // Initialize selection. 81 | _.each( this.getValue(), function( item, i ) { 82 | 83 | var model; 84 | 85 | // Legacy. Handle storing full objects. 86 | item = ( 'object' === typeof( item ) ) ? item.id : item; 87 | model = new wp.media.attachment( item ); 88 | 89 | model.set( 'menu-order', i ); 90 | 91 | this.selection.add( model ); 92 | 93 | // Re-render after attachments have synced. 94 | model.fetch(); 95 | model.on( 'sync', this.render ); 96 | 97 | }.bind(this) ); 98 | 99 | }, 100 | 101 | prepare: function() { 102 | return { 103 | id: this.cid, 104 | value: this.selection.toJSON(), 105 | config: this.config, 106 | }; 107 | }, 108 | 109 | rendered: function() { 110 | 111 | this.$el.sortable({ 112 | delay: 150, 113 | items: '> .image-placeholder', 114 | stop: function() { 115 | 116 | var selection = this.selection; 117 | 118 | this.$el.children( '.image-placeholder' ).each( function( i ) { 119 | 120 | var id = parseInt( this.getAttribute( 'data-id' ) ); 121 | var model = selection.findWhere( { id: id } ); 122 | 123 | if ( model ) { 124 | model.set( 'menu-order', i ); 125 | } 126 | 127 | } ); 128 | 129 | selection.sort(); 130 | this.setValue( selection.pluck('id') ); 131 | 132 | }.bind(this) 133 | }); 134 | 135 | }, 136 | 137 | /** 138 | * Handle the select event. 139 | * 140 | * Insert an image or multiple images. 141 | */ 142 | onSelectImage: function() { 143 | 144 | var frame = this.frame || null; 145 | 146 | if ( ! frame ) { 147 | return; 148 | } 149 | 150 | this.selection.reset([]); 151 | 152 | frame.state().get('selection').each( function( attachment ) { 153 | 154 | if ( this.isAttachmentSizeOk( attachment ) ) { 155 | this.selection.add( attachment ); 156 | } 157 | 158 | }.bind(this) ); 159 | 160 | this.setValue( this.selection.pluck('id') ); 161 | 162 | frame.close(); 163 | 164 | }, 165 | 166 | /** 167 | * Handle the edit action. 168 | */ 169 | editImage: function(e) { 170 | 171 | e.preventDefault(); 172 | 173 | var frame = this.frame; 174 | 175 | if ( ! frame ) { 176 | 177 | var frameArgs = { 178 | library: this.config.library, 179 | multiple: this.config.multiple, 180 | title: 'Select Image', 181 | frame: 'select', 182 | }; 183 | 184 | frame = this.frame = wp.media( frameArgs ); 185 | 186 | frame.on( 'content:create:browse', this.setupFilters, this ); 187 | frame.on( 'content:render:browse', this.sizeFilterNotice, this ); 188 | frame.on( 'select', this.onSelectImage, this ); 189 | 190 | } 191 | 192 | // When the frame opens, set the selection. 193 | frame.on( 'open', function() { 194 | 195 | var selection = frame.state().get('selection'); 196 | 197 | // Set the selection. 198 | // Note - expects array of objects, not a collection. 199 | selection.set( this.selection.models ); 200 | 201 | }.bind(this) ); 202 | 203 | frame.open(); 204 | 205 | }, 206 | 207 | /** 208 | * Add filters to the frame library collection. 209 | * 210 | * - filter to limit to required size. 211 | */ 212 | setupFilters: function() { 213 | 214 | var lib = this.frame.state().get('library'); 215 | 216 | if ( 'sizeReq' in this.config ) { 217 | lib.filters.size = this.isAttachmentSizeOk; 218 | } 219 | 220 | }, 221 | 222 | 223 | /** 224 | * Handle display of size filter notice. 225 | */ 226 | sizeFilterNotice: function() { 227 | 228 | var lib = this.frame.state().get('library'); 229 | 230 | if ( ! lib.filters.size ) { 231 | return; 232 | } 233 | 234 | // Wait to be sure the frame is rendered. 235 | window.setTimeout( function() { 236 | 237 | var req, $notice, template, $toolbar; 238 | 239 | req = _.extend( { 240 | width: 0, 241 | height: 0, 242 | }, this.config.sizeReq ); 243 | 244 | // Display notice on main grid view. 245 | template = '

Only showing images that meet size requirements: <%= width %>px × <%= height %>px

'; 246 | $notice = $( _.template( template )( req ) ); 247 | $toolbar = $( '.attachments-browser .media-toolbar', this.frame.$el ).first(); 248 | $toolbar.prepend( $notice ); 249 | 250 | var contentView = this.frame.views.get( '.media-frame-content' ); 251 | contentView = contentView[0]; 252 | 253 | $notice = $( '

Image does not meet size requirements.

' ); 254 | 255 | // Display additional notice when selecting an image. 256 | // Required to indicate a bad image has just been uploaded. 257 | contentView.options.selection.on( 'selection:single', function() { 258 | 259 | var attachment = contentView.options.selection.single(); 260 | 261 | var displayNotice = function() { 262 | 263 | // If still uploading, wait and try displaying notice again. 264 | if ( attachment.get( 'uploading' ) ) { 265 | window.setTimeout( function() { 266 | displayNotice(); 267 | }, 500 ); 268 | 269 | // OK. Display notice as required. 270 | } else { 271 | 272 | if ( ! this.isAttachmentSizeOk( attachment ) ) { 273 | $( '.attachments-browser .attachment-info' ).prepend( $notice ); 274 | } else { 275 | $notice.remove(); 276 | } 277 | 278 | } 279 | 280 | }.bind(this); 281 | 282 | displayNotice(); 283 | 284 | }.bind(this) ); 285 | 286 | }.bind(this), 100 ); 287 | 288 | }, 289 | 290 | removeImage: function(e) { 291 | 292 | e.preventDefault(); 293 | 294 | var $target, id; 295 | 296 | $target = $(e.target); 297 | $target = ( $target.prop('tagName') === 'BUTTON' ) ? $target : $target.closest('button.remove'); 298 | id = $target.data( 'image-id' ); 299 | 300 | if ( ! id ) { 301 | return; 302 | } 303 | 304 | this.selection.remove( this.selection.where( { id: id } ) ); 305 | this.setValue( this.selection.pluck('id') ); 306 | 307 | }, 308 | 309 | /** 310 | * Does attachment meet size requirements? 311 | * 312 | * @param Attachment 313 | * @return boolean 314 | */ 315 | isAttachmentSizeOk: function( attachment ) { 316 | 317 | if ( ! ( 'sizeReq' in this.config ) ) { 318 | return true; 319 | } 320 | 321 | this.config.sizeReq = _.extend( { 322 | width: 0, 323 | height: 0, 324 | }, this.config.sizeReq ); 325 | 326 | var widthReq = attachment.get('width') >= this.config.sizeReq.width; 327 | var heightReq = attachment.get('height') >= this.config.sizeReq.height; 328 | 329 | return widthReq && heightReq; 330 | 331 | } 332 | 333 | } ); 334 | 335 | module.exports = FieldAttachment; 336 | -------------------------------------------------------------------------------- /assets/js/src/views/fields/field-checkbox.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var wp = require('wp'); 3 | var Field = require('views/fields/field'); 4 | 5 | var FieldText = Field.extend({ 6 | 7 | template: wp.template( 'mpb-field-checkbox' ), 8 | 9 | defaultConfig: { 10 | label: 'Test Label', 11 | }, 12 | 13 | events: { 14 | 'change input': 'inputChanged', 15 | }, 16 | 17 | inputChanged: _.debounce( function() { 18 | this.setValue( $( 'input', this.$el ).prop( 'checked' ) ); 19 | } ), 20 | 21 | } ); 22 | 23 | module.exports = FieldText; 24 | -------------------------------------------------------------------------------- /assets/js/src/views/fields/field-content-editable.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var wp = require('wp'); 3 | var Field = require('views/fields/field'); 4 | 5 | var FieldContentEditable = Field.extend({ 6 | 7 | template: wp.template( 'mpb-field-content-editable' ), 8 | 9 | events: { 10 | 'keyup .content-editable-field': 'inputChanged', 11 | 'change .content-editable-field': 'inputChanged', 12 | }, 13 | 14 | inputChanged: _.debounce( function(e) { 15 | if ( e && e.target ) { 16 | this.setValue( $(e.target).html() ); 17 | } 18 | } ), 19 | 20 | } ); 21 | 22 | module.exports = FieldContentEditable; 23 | -------------------------------------------------------------------------------- /assets/js/src/views/fields/field-link.js: -------------------------------------------------------------------------------- 1 | var wp = require('wp'); 2 | var Field = require('views/fields/field'); 3 | 4 | var FieldLink = Field.extend({ 5 | 6 | template: wp.template( 'mpb-field-link' ), 7 | 8 | events: { 9 | 'keyup input.field-text': 'textInputChanged', 10 | 'change input.field-text': 'textInputChanged', 11 | 'keyup input.field-link': 'linkInputChanged', 12 | 'change input.field-link': 'linkInputChanged', 13 | }, 14 | 15 | initialize: function( options ) { 16 | 17 | Field.prototype.initialize.apply( this, [ options ] ); 18 | 19 | this.value = this.value || {}; 20 | this.value = _.defaults( this.value, { link: '', text: '' } ); 21 | 22 | }, 23 | 24 | textInputChanged: _.debounce( function(e) { 25 | if ( e && e.target ) { 26 | var value = this.getValue(); 27 | value.text = e.target.value; 28 | this.setValue( value ); 29 | } 30 | } ), 31 | 32 | linkInputChanged: _.debounce( function(e) { 33 | if ( e && e.target ) { 34 | var value = this.getValue(); 35 | value.link = e.target.value; 36 | this.setValue( value ); 37 | } 38 | } ), 39 | 40 | }); 41 | 42 | module.exports = FieldLink; 43 | -------------------------------------------------------------------------------- /assets/js/src/views/fields/field-number.js: -------------------------------------------------------------------------------- 1 | var wp = require('wp'); 2 | var FieldText = require('views/fields/field-text'); 3 | 4 | var FieldNumber = FieldText.extend({ 5 | 6 | template: wp.template( 'mpb-field-number' ), 7 | 8 | getValue: function() { 9 | return parseFloat( this.value ); 10 | }, 11 | 12 | setValue: function( value ) { 13 | this.value = parseFloat( value ); 14 | this.trigger( 'change', this.getValue() ); 15 | }, 16 | 17 | } ); 18 | 19 | module.exports = FieldNumber; 20 | -------------------------------------------------------------------------------- /assets/js/src/views/fields/field-post-select.js: -------------------------------------------------------------------------------- 1 | /* global ajaxurl */ 2 | 3 | var $ = require('jquery'); 4 | var wp = require('wp'); 5 | var Field = require('views/fields/field'); 6 | 7 | /** 8 | * Text Field View 9 | * 10 | * You can use this anywhere. 11 | * Just listen for 'change' event on the view. 12 | */ 13 | var FieldPostSelect = Field.extend({ 14 | 15 | template: wp.template( 'mpb-field-text' ), 16 | 17 | defaultConfig: { 18 | multiple: true, 19 | sortable: true, 20 | postType: 'post', 21 | }, 22 | 23 | events: { 24 | 'change input': 'inputChanged' 25 | }, 26 | 27 | initialize: function( options ) { 28 | Field.prototype.initialize.apply( this, [ options ] ); 29 | this.on( 'mpb:rendered', this.rendered ); 30 | }, 31 | 32 | setValue: function( value ) { 33 | 34 | if ( this.config.multiple && ! Array.isArray( value ) ) { 35 | value = [ value ]; 36 | } else if ( ! this.config.multiple && Array.isArray( value ) ) { 37 | value = value[0]; 38 | } 39 | 40 | Field.prototype.setValue.apply( this, [ value ] ); 41 | 42 | }, 43 | 44 | /** 45 | * Get Value. 46 | * 47 | * @param Return value as an array even if multiple is false. 48 | */ 49 | getValue: function() { 50 | 51 | var value = this.value; 52 | 53 | if ( this.config.multiple && ! Array.isArray( value ) ) { 54 | value = [ value ]; 55 | } else if ( ! this.config.multiple && Array.isArray( value ) ) { 56 | value = value[0]; 57 | } 58 | 59 | return value; 60 | 61 | }, 62 | 63 | prepare: function() { 64 | 65 | var value = this.getValue(); 66 | value = Array.isArray( value ) ? value.join( ',' ) : value; 67 | 68 | return { 69 | id: this.cid, 70 | value: value, 71 | config: {} 72 | }; 73 | 74 | }, 75 | 76 | rendered: function () { 77 | this.initSelect2(); 78 | if ( this.config.multiple && this.config.sortable ) { 79 | this.initSortable(); 80 | } 81 | }, 82 | 83 | initSelect2: function() { 84 | 85 | var $field = $( '#' + this.cid, this.$el ); 86 | var postType = this.config.postType; 87 | var multiple = this.config.multiple; 88 | 89 | var formatRequest =function ( term, page ) { 90 | return { 91 | action: 'mce_get_posts', 92 | s: term, 93 | page: page, 94 | post_type: postType 95 | }; 96 | }; 97 | 98 | var parseResults = function ( response ) { 99 | return { 100 | results: response.results, 101 | more: response.more 102 | }; 103 | }; 104 | 105 | var initSelection = function( el, callback ) { 106 | 107 | var value = this.getValue(); 108 | 109 | if ( Array.isArray( value ) ) { 110 | value = value.join(','); 111 | } 112 | 113 | if ( value ) { 114 | $.get( ajaxurl, { 115 | action: 'mce_get_posts', 116 | post__in: value, 117 | post_type: postType 118 | } ).done( function( data ) { 119 | if ( multiple ) { 120 | callback( parseResults( data ).results ); 121 | } else { 122 | callback( parseResults( data ).results[0] ); 123 | } 124 | } ); 125 | } 126 | 127 | }.bind(this); 128 | 129 | $field.select2({ 130 | minimumInputLength: 1, 131 | multiple: multiple, 132 | initSelection: initSelection, 133 | ajax: { 134 | url: ajaxurl, 135 | dataType: 'json', 136 | delay: 250, 137 | cache: false, 138 | data: formatRequest, 139 | results: parseResults, 140 | }, 141 | }); 142 | 143 | }, 144 | 145 | initSortable: function() { 146 | $( '.select2-choices', this.$el ).sortable({ 147 | items: '> .select2-search-choice', 148 | containment: 'parent', 149 | stop: function() { 150 | var sorted = [], 151 | $input = $( 'input#' + this.cid, this.$el ); 152 | 153 | $( '.select2-choices > .select2-search-choice', this.$el ).each( function() { 154 | sorted.push( $(this).data('select2Data').id ); 155 | }); 156 | 157 | $input.attr( 'value', sorted.join( ',' ) ); 158 | $input.val( sorted.join( ',' ) ); 159 | this.inputChanged(); 160 | }.bind( this ) 161 | }); 162 | }, 163 | 164 | inputChanged: function() { 165 | var value = $( 'input#' + this.cid, this.$el ).val(); 166 | value = value.split( ',' ).map( Number ); 167 | this.setValue( value ); 168 | }, 169 | 170 | remove: function() { 171 | try { 172 | $( '.select2-choices', this.$el ).sortable( 'destroy' ); 173 | } catch( e ) {} 174 | }, 175 | 176 | } ); 177 | 178 | module.exports = FieldPostSelect; 179 | -------------------------------------------------------------------------------- /assets/js/src/views/fields/field-select.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var wp = require('wp'); 3 | var Field = require('views/fields/field'); 4 | 5 | /** 6 | * Text Field View 7 | * 8 | * You can use this anywhere. 9 | * Just listen for 'change' event on the view. 10 | */ 11 | var FieldSelect = Field.extend({ 12 | 13 | template: wp.template( 'mpb-field-select' ), 14 | value: [], 15 | 16 | defaultConfig: { 17 | multiple: false, 18 | options: [], 19 | }, 20 | 21 | events: { 22 | 'change select': 'inputChanged' 23 | }, 24 | 25 | initialize: function( options ) { 26 | _.bindAll( this, 'parseOption' ); 27 | Field.prototype.initialize.apply( this, [ options ] ); 28 | this.options = options.config.options || []; 29 | }, 30 | 31 | inputChanged: function() { 32 | this.setValue( $( 'select', this.$el ).val() ); 33 | }, 34 | 35 | getOptions: function() { 36 | return this.options.map( this.parseOption ); 37 | }, 38 | 39 | parseOption: function( option ) { 40 | option = _.defaults( option, { value: '', text: '', selected: false } ); 41 | option.selected = this.isSelected( option.value ); 42 | return option; 43 | }, 44 | 45 | isSelected: function( value ) { 46 | if ( this.config.multiple ) { 47 | return this.getValue().indexOf( value ) >= 0; 48 | } else { 49 | return value === this.getValue(); 50 | } 51 | }, 52 | 53 | setValue: function( value ) { 54 | 55 | if ( this.config.multiple && ! Array.isArray( value ) ) { 56 | value = [ value ]; 57 | } else if ( ! this.config.multiple && Array.isArray( value ) ) { 58 | value = value[0]; 59 | } 60 | 61 | Field.prototype.setValue.apply( this, [ value ] ); 62 | 63 | }, 64 | 65 | /** 66 | * Get Value. 67 | * 68 | * @param Return value as an array even if multiple is false. 69 | */ 70 | getValue: function() { 71 | 72 | var value = this.value; 73 | 74 | if ( this.config.multiple && ! Array.isArray( value ) ) { 75 | value = [ value ]; 76 | } else if ( ! this.config.multiple && Array.isArray( value ) ) { 77 | value = value[0]; 78 | } 79 | 80 | return value; 81 | 82 | }, 83 | 84 | render: function () { 85 | 86 | var data = { 87 | id: this.cid, 88 | options: this.getOptions(), 89 | }; 90 | 91 | // Create element from template. 92 | this.$el.html( this.template( data ) ); 93 | 94 | return this; 95 | 96 | }, 97 | 98 | } ); 99 | 100 | module.exports = FieldSelect; 101 | -------------------------------------------------------------------------------- /assets/js/src/views/fields/field-text.js: -------------------------------------------------------------------------------- 1 | var wp = require('wp'); 2 | var Field = require('views/fields/field'); 3 | 4 | var FieldText = Field.extend({ 5 | 6 | template: wp.template( 'mpb-field-text' ), 7 | 8 | defaultConfig: { 9 | classes: 'regular-text', 10 | placeholder: null, 11 | }, 12 | 13 | events: { 14 | 'keyup input': 'inputChanged', 15 | 'change input': 'inputChanged', 16 | }, 17 | 18 | inputChanged: _.debounce( function(e) { 19 | if ( e && e.target ) { 20 | this.setValue( e.target.value ); 21 | } 22 | } ), 23 | 24 | } ); 25 | 26 | module.exports = FieldText; 27 | -------------------------------------------------------------------------------- /assets/js/src/views/fields/field-textarea.js: -------------------------------------------------------------------------------- 1 | var wp = require('wp'); 2 | var FieldText = require('views/fields/field-text'); 3 | 4 | var FieldTextarea = FieldText.extend({ 5 | 6 | template: wp.template( 'mpb-field-textarea' ), 7 | 8 | events: { 9 | 'keyup textarea': 'inputChanged', 10 | 'change textarea': 'inputChanged', 11 | }, 12 | 13 | } ); 14 | 15 | module.exports = FieldTextarea; 16 | -------------------------------------------------------------------------------- /assets/js/src/views/fields/field-wysiwyg.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var wp = require('wp'); 3 | var Field = require('views/fields/field'); 4 | 5 | /** 6 | * Text Field View 7 | * 8 | * You can use this anywhere. 9 | * Just listen for 'change' event on the view. 10 | */ 11 | var FieldWYSIWYG = Field.extend({ 12 | 13 | template: wp.template( 'mpb-field-wysiwyg' ), 14 | editor: null, 15 | value: null, 16 | 17 | /** 18 | * Init. 19 | * 20 | * options.value is used to pass initial value. 21 | */ 22 | initialize: function( options ) { 23 | 24 | Field.prototype.initialize.apply( this, [ options ] ); 25 | 26 | this.on( 'mpb:rendered', this.rendered ); 27 | 28 | }, 29 | 30 | rendered: function () { 31 | 32 | // Hide editor to prevent FOUC. Show again on init. See setup. 33 | $( '.wp-editor-wrap', this.$el ).css( 'display', 'none' ); 34 | 35 | // Init. Defferred to make sure container element has been rendered. 36 | _.defer( this.initTinyMCE.bind( this ) ); 37 | 38 | return this; 39 | 40 | }, 41 | 42 | /** 43 | * Initialize the TinyMCE editor. 44 | * 45 | * Bit hacky this. 46 | * 47 | * @return null. 48 | */ 49 | initTinyMCE: function() { 50 | 51 | var self = this, prop; 52 | 53 | var id = 'mpb-text-body-' + this.cid; 54 | var regex = new RegExp( 'mpb-placeholder-(id|name)', 'g' ); 55 | var ed = tinyMCE.get( id ); 56 | var $el = $( '#wp-mpb-text-body-' + this.cid + '-wrap', this.$el ); 57 | 58 | // If found. Remove so we can re-init. 59 | if ( ed ) { 60 | tinyMCE.execCommand( 'mceRemoveEditor', false, id ); 61 | } 62 | 63 | // Get settings for this field. 64 | // If no settings for this field. Clone from placeholder. 65 | if ( typeof( tinyMCEPreInit.mceInit[ id ] ) === 'undefined' ) { 66 | var newSettings = jQuery.extend( {}, tinyMCEPreInit.mceInit[ 'mpb-placeholder-id' ] ); 67 | for ( prop in newSettings ) { 68 | if ( 'string' === typeof( newSettings[prop] ) ) { 69 | newSettings[prop] = newSettings[prop].replace( regex, id ); 70 | } 71 | } 72 | 73 | tinyMCEPreInit.mceInit[ id ] = newSettings; 74 | } 75 | 76 | // Remove fullscreen plugin. 77 | tinyMCEPreInit.mceInit[ id ].plugins = tinyMCEPreInit.mceInit[ id ].plugins.replace( 'fullscreen,', '' ); 78 | 79 | // Get quicktag settings for this field. 80 | // If none exists for this field. Clone from placeholder. 81 | if ( typeof( tinyMCEPreInit.qtInit[ id ] ) === 'undefined' ) { 82 | var newQTS = jQuery.extend( {}, tinyMCEPreInit.qtInit[ 'mpb-placeholder-id' ] ); 83 | for ( prop in newQTS ) { 84 | if ( 'string' === typeof( newQTS[prop] ) ) { 85 | newQTS[prop] = newQTS[prop].replace( regex, id ); 86 | } 87 | } 88 | tinyMCEPreInit.qtInit[ id ] = newQTS; 89 | } 90 | 91 | // When editor inits, attach save callback to change event. 92 | tinyMCEPreInit.mceInit[id].setup = function() { 93 | 94 | // Listen for changes in the MCE editor. 95 | this.on( 'change', function( e ) { 96 | self.setValue( e.target.getContent() ); 97 | } ); 98 | 99 | // Prevent FOUC. Show element after init. 100 | this.on( 'init', function() { 101 | $el.css( 'display', 'block' ); 102 | }); 103 | 104 | }; 105 | 106 | // Listen for changes in the HTML editor. 107 | $('#' + id ).on( 'keydown change', function() { 108 | self.setValue( this.value ); 109 | } ); 110 | 111 | // Current mode determined by class on element. 112 | // If mode is visual, create the tinyMCE. 113 | if ( $el.hasClass('tmce-active') ) { 114 | tinyMCE.init( tinyMCEPreInit.mceInit[id] ); 115 | } else { 116 | $el.css( 'display', 'block' ); 117 | } 118 | 119 | // Init quicktags. 120 | quicktags( tinyMCEPreInit.qtInit[ id ] ); 121 | QTags._buttonsInit(); 122 | 123 | var $builder = this.$el.closest( '.ui-sortable' ); 124 | 125 | // Handle temporary removal of tinyMCE when sorting. 126 | $builder.on( 'sortstart', function( event, ui ) { 127 | 128 | if ( event.currentTarget !== $builder ) { 129 | return; 130 | } 131 | 132 | if ( ui.item[0].getAttribute('data-cid') === this.el.getAttribute('data-cid') ) { 133 | tinyMCE.execCommand( 'mceRemoveEditor', false, id ); 134 | } 135 | 136 | }.bind(this) ); 137 | 138 | // Handle re-init after sorting. 139 | $builder.on( 'sortstop', function( event, ui ) { 140 | 141 | if ( event.currentTarget !== $builder ) { 142 | return; 143 | } 144 | 145 | if ( ui.item[0].getAttribute('data-cid') === this.el.getAttribute('data-cid') ) { 146 | tinyMCE.execCommand('mceAddEditor', false, id); 147 | } 148 | 149 | }.bind(this) ); 150 | 151 | }, 152 | 153 | remove: function() { 154 | tinyMCE.execCommand( 'mceRemoveEditor', false, 'mpb-text-body-' + this.cid ); 155 | }, 156 | 157 | /** 158 | * Refresh view after sort/collapse etc. 159 | */ 160 | refresh: function() { 161 | this.render(); 162 | }, 163 | 164 | } ); 165 | 166 | module.exports = FieldWYSIWYG; 167 | -------------------------------------------------------------------------------- /assets/js/src/views/fields/field.js: -------------------------------------------------------------------------------- 1 | var wp = require('wp'); 2 | 3 | /** 4 | * Abstract Field Class. 5 | * 6 | * Handles setup as well as getting and setting values. 7 | * Provides a very generic render method - but probably be OK for most simple fields. 8 | */ 9 | var Field = wp.Backbone.View.extend({ 10 | 11 | template: null, 12 | value: null, 13 | config: {}, 14 | defaultConfig: {}, 15 | 16 | /** 17 | * Initialize. 18 | * If you extend this view - it is reccommeded to call this. 19 | * 20 | * Expects options.value and options.config. 21 | */ 22 | initialize: function( options ) { 23 | 24 | var config; 25 | 26 | _.bindAll( this, 'getValue', 'setValue' ); 27 | 28 | // If a change callback is provided, call this on change. 29 | if ( 'onChange' in options ) { 30 | this.on( 'change', options.onChange ); 31 | } 32 | 33 | config = ( 'config' in options ) ? options.config : {}; 34 | this.config = _.extend( {}, this.defaultConfig, config ); 35 | 36 | if ( 'value' in options ) { 37 | this.setValue( options.value ); 38 | } 39 | 40 | }, 41 | 42 | getValue: function() { 43 | return this.value; 44 | }, 45 | 46 | setValue: function( value ) { 47 | this.value = value; 48 | this.trigger( 'change', this.value ); 49 | }, 50 | 51 | prepare: function() { 52 | return { 53 | id: this.cid, 54 | value: this.value, 55 | config: this.config 56 | }; 57 | }, 58 | 59 | render: function() { 60 | wp.Backbone.View.prototype.render.apply( this, arguments ); 61 | this.trigger( 'mpb:rendered' ); 62 | return this; 63 | }, 64 | 65 | /** 66 | * Refresh view after sort/collapse etc. 67 | */ 68 | refresh: function() {}, 69 | 70 | } ); 71 | 72 | module.exports = Field; 73 | -------------------------------------------------------------------------------- /assets/js/src/views/module-edit-blockquote.js: -------------------------------------------------------------------------------- 1 | // var $ = require('jquery'); 2 | var ModuleEdit = require('views/module-edit'); 3 | var FieldText = require('views/fields/field-text'); 4 | var FieldContentEditable = require('views/fields/field-content-editable'); 5 | 6 | /** 7 | * Highlight Module. 8 | * Extends default moudule, 9 | * custom different template. 10 | */ 11 | module.exports = ModuleEdit.extend({ 12 | 13 | template: wp.template( 'mpb-module-edit-blockquote' ), 14 | 15 | fields: { 16 | text: null, 17 | source: null, 18 | }, 19 | 20 | initialize: function( attributes, options ) { 21 | 22 | ModuleEdit.prototype.initialize.apply( this, [ attributes, options ] ); 23 | 24 | var fieldText = new FieldContentEditable( { 25 | value: this.model.getAttr('text').get('value'), 26 | } ); 27 | 28 | var fieldSource = new FieldText( { 29 | value: this.model.getAttr('source').get('value'), 30 | } ); 31 | 32 | this.views.add( '.field-text', fieldText ); 33 | this.views.add( '.field-source', fieldSource ); 34 | 35 | fieldText.on( 'change', function( value ) { 36 | this.model.setAttrValue( 'text', value ); 37 | }.bind(this) ); 38 | 39 | fieldSource.on( 'change', function( value ) { 40 | this.model.setAttrValue( 'source', value ); 41 | }.bind(this) ); 42 | 43 | }, 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /assets/js/src/views/module-edit-default.js: -------------------------------------------------------------------------------- 1 | var ModuleEdit = require('views/module-edit'); 2 | var ModuleEditFormRow = require('views/module-edit-form-row'); 3 | var fieldViews = require('utils/field-views'); 4 | 5 | /** 6 | * Generic Edit Form. 7 | * 8 | * Handles a wide range of generic field types. 9 | * For each attribute, it creates a field based on the attribute 'type' 10 | * Also uses optional attribute 'config' property when initializing field. 11 | */ 12 | module.exports = ModuleEdit.extend({ 13 | 14 | initialize: function( attributes, options ) { 15 | 16 | ModuleEdit.prototype.initialize.apply( this, [ attributes, options ] ); 17 | 18 | _.bindAll( this, 'render' ); 19 | 20 | // this.fields is an easy reference for the field views. 21 | var fieldsViews = this.fields = []; 22 | var model = this.model; 23 | 24 | // For each attribute - 25 | // initialize a field for that attribute 'type' 26 | // Store in this.fields 27 | // Use config from the attribute 28 | this.model.get('attr').each( function( attr ) { 29 | 30 | var fieldView, type, name, config, view; 31 | 32 | type = attr.get('type'); 33 | 34 | if ( ! type || ! ( type in fieldViews ) ) { 35 | return; 36 | } 37 | 38 | fieldView = fieldViews[ type ]; 39 | name = attr.get('name'); 40 | config = attr.get('config') || {}; 41 | 42 | view = new fieldView( { 43 | value: model.getAttrValue( name ), 44 | config: config, 45 | onChange: function( value ) { 46 | model.setAttrValue( name, value ); 47 | }, 48 | }); 49 | 50 | this.views.add( '', new ModuleEditFormRow( { 51 | label: attr.get('label'), 52 | desc: attr.get('description' ), 53 | fieldView: view 54 | } ) ); 55 | 56 | fieldsViews.push( view ); 57 | 58 | }.bind( this ) ); 59 | 60 | // Cleanup. 61 | // Remove each field view when this model is destroyed. 62 | this.model.on( 'destroy', function() { 63 | _.each( this.fields, function( field ) { 64 | field.remove(); 65 | } ); 66 | } ); 67 | 68 | }, 69 | 70 | /** 71 | * Refresh view. 72 | * Required after sort/collapse etc. 73 | */ 74 | refresh: function() { 75 | _.each( this.fields, function( field ) { 76 | field.refresh(); 77 | } ); 78 | }, 79 | 80 | }); 81 | -------------------------------------------------------------------------------- /assets/js/src/views/module-edit-form-row.js: -------------------------------------------------------------------------------- 1 | var wp = require('wp'); 2 | 3 | module.exports = wp.Backbone.View.extend({ 4 | 5 | template: wp.template( 'mpb-form-row' ), 6 | className: 'form-row', 7 | 8 | initialize: function( options ) { 9 | if ( 'fieldView' in options ) { 10 | this.views.set( '.field', options.fieldView ); 11 | } 12 | }, 13 | 14 | }); 15 | -------------------------------------------------------------------------------- /assets/js/src/views/module-edit-tools.js: -------------------------------------------------------------------------------- 1 | var wp = require('wp'); 2 | 3 | module.exports = wp.Backbone.View.extend({ 4 | 5 | template: wp.template( 'mpb-module-edit-tools' ), 6 | className: 'module-edit-tools', 7 | 8 | events: { 9 | 'click .button-selection-item-remove': function(e) { 10 | e.preventDefault(); 11 | this.trigger( 'mpb:module-remove' ); 12 | }, 13 | }, 14 | 15 | }); 16 | -------------------------------------------------------------------------------- /assets/js/src/views/module-edit.js: -------------------------------------------------------------------------------- 1 | var wp = require('wp'); 2 | var ModuleEditTools = require('views/module-edit-tools'); 3 | 4 | /** 5 | * Very generic form view handler. 6 | * This does some basic magic based on data attributes to update simple text fields. 7 | */ 8 | module.exports = wp.Backbone.View.extend({ 9 | 10 | className: 'module-edit', 11 | 12 | initialize: function() { 13 | 14 | _.bindAll( this, 'render', 'removeModel' ); 15 | 16 | var tools = new ModuleEditTools( { 17 | label: this.model.get( 'label' ) 18 | } ); 19 | 20 | this.views.add( '', tools ); 21 | this.model.on( 'change:sortable', this.render ); 22 | tools.on( 'mpb:module-remove', this.removeModel ); 23 | 24 | }, 25 | 26 | render: function() { 27 | wp.Backbone.View.prototype.render.apply( this, arguments ); 28 | this.$el.attr( 'data-cid', this.model.cid ); 29 | this.$el.toggleClass( 'module-edit-sortable', this.model.get( 'sortable' ) ); 30 | return this; 31 | }, 32 | 33 | /** 34 | * Remove model handler. 35 | */ 36 | removeModel: function() { 37 | this.remove(); 38 | this.model.destroy(); 39 | }, 40 | 41 | /** 42 | * Refresh view. 43 | * Required after sort/collapse etc. 44 | */ 45 | refresh: function() {}, 46 | 47 | }); 48 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "humanmade/modular-page-builder", 3 | "description": "Modular page builder for WordPress", 4 | "type": "project", 5 | "license": "GPL-2.0", 6 | "authors": [ 7 | { 8 | "name": "Matthew Haines-Young", 9 | "email": "matthew@matth.eu" 10 | } 11 | ], 12 | "require": {} 13 | } 14 | -------------------------------------------------------------------------------- /inc/class-builder-post-meta.php: -------------------------------------------------------------------------------- 1 | register_rest_fields(); 15 | } 16 | 17 | add_action( 'edit_form_after_editor', array( $this, 'output' ) ); 18 | add_action( 'save_post', array( $this, 'save_post' ) ); 19 | add_action( 'wp_insert_post_data', array( $this, 'wp_insert_post_data' ), 10, 2 ); 20 | add_filter( 'wp_refresh_nonces', function ( $response, $data ) { 21 | if ( ! array_key_exists( 'wp-refresh-post-nonces', $response ) ) { 22 | return $response; 23 | } 24 | 25 | $response['wp-refresh-post-nonces']['replace'][ $this->id . '-nonce' ] = wp_create_nonce( $this->id ); 26 | 27 | return $response; 28 | }, 11, 2 ); 29 | 30 | add_filter( "wp_get_revision_ui_diff", array( $this, 'revision_ui_diff' ), 10, 3 ); 31 | 32 | add_filter( 'wp_post_revision_meta_keys', function ( $keys ) { 33 | $keys[] = $this->id . '-data'; 34 | return $keys; 35 | } ); 36 | 37 | add_action( 38 | 'admin_enqueue_scripts', 39 | function () { 40 | if ( $this->is_allowed_for_screen() ) { 41 | Plugin::get_instance()->enqueue_builder(); 42 | } 43 | } 44 | ); 45 | 46 | } 47 | 48 | public function output( $post ) { 49 | 50 | if ( ! $this->is_allowed_for_screen() ) { 51 | return; 52 | } 53 | 54 | $data[ $this->id . '-data' ] = json_encode( $this->get_raw_data( $post->ID, $this->id . '-data' ) ); 55 | $data[ $this->id . '-allowed-modules' ] = implode( ',', $this->get_allowed_modules_for_page( $post->ID ) ); 56 | $data[ $this->id . '-required-modules' ] = implode( ',', $this->get_required_modules_for_page( $post->ID ) ); 57 | $data[ $this->id . '-nonce' ] = wp_create_nonce( $this->id ); 58 | 59 | printf( '
', $this->id ); 60 | 61 | foreach ( $data as $name => $value ) { 62 | printf( 63 | '', 64 | esc_attr( $name ), 65 | esc_attr( $name ), 66 | esc_attr( $value ) 67 | ); 68 | } 69 | 70 | if ( $this->args['title'] ) { 71 | printf( '

%s

', esc_html( $this->args['title'] ) ); 72 | } 73 | 74 | echo '
'; 75 | 76 | } 77 | 78 | public function save_post( $post_id ) { 79 | 80 | $data = $this->get_post_data(); 81 | 82 | if ( $data ) { 83 | $this->save_data( $post_id, $data ); 84 | } 85 | 86 | } 87 | 88 | public function wp_insert_post_data( $post_data, $postarr ) { 89 | global $wpdb; 90 | 91 | $data = $this->get_post_data(); 92 | 93 | if ( $data && ! empty( $postarr['ID'] ) ) { 94 | $post_data['post_content'] = $this->get_rendered_data( $data ); 95 | $post_data['post_content'] = sanitize_post_field( 'post_content', $post_data['post_content'], $postarr['ID'], 'db' ); 96 | $post_data['post_content'] = wp_slash( $post_data['post_content'] ); 97 | 98 | $charset = $wpdb->get_col_charset( $wpdb->posts, 'post_content' ); 99 | if ( 'utf8' === $charset ) { 100 | $post_data['post_content'] = wp_encode_emoji( $post_data['post_content'] ); 101 | } 102 | } 103 | 104 | return $post_data; 105 | } 106 | 107 | public function get_allowed_modules_for_page( $post_id = null ) { 108 | return apply_filters( 'modular_page_builder_allowed_modules_for_page', $this->args['allowed_modules'], $post_id ); 109 | } 110 | 111 | public function get_required_modules_for_page( $post_id = null ) { 112 | return apply_filters( 'modular_page_builder_required_modules_for_page', $this->args['required_modules'], $post_id ); 113 | } 114 | 115 | /** 116 | * Build the revision UI diff in the case where we have data for revisions. 117 | * 118 | * This is only visible if you have revisioned meta data. 119 | * 120 | * @param array $return The data that will be returned for the diff. 121 | * @param \WP_Post $compare_from The post comparing from. 122 | * @param \WP_Post $compare_to The post comparing to. 123 | * @return array 124 | */ 125 | public function revision_ui_diff( $return, $compare_from, $compare_to ) { 126 | 127 | if ( ! is_a( $compare_from, 'WP_Post' ) || ! is_a( $compare_to, 'WP_Post' ) ) { 128 | return $return; 129 | } 130 | 131 | $from_data = $this->get_raw_data( $compare_from->ID ); 132 | $to_data = $this->get_raw_data( $compare_to->ID ); 133 | 134 | $return[] = array( 135 | 'id' => $this->id, 136 | 'name' => 'Page Builder', 137 | 'diff' => wp_text_diff( 138 | json_encode( $from_data ), 139 | json_encode( $to_data ), 140 | array( 'show_split_view' => true ) 141 | ), 142 | ); 143 | 144 | return $return; 145 | } 146 | 147 | public function register_rest_fields() { 148 | 149 | $schema = array( 150 | 'description' => 'Modular page builder data.', 151 | 'type' => 'array', 152 | 'context' => array( 'view' ), 153 | 'properties' => array( 154 | 'rendered' => array( 155 | 'type' => 'string', 156 | 'description' => 'HTML rendering of the page builder moduels', 157 | ), 158 | 'modules' => array( 159 | 'type' => 'array', 160 | 'description' => 'Data for all the modules', 161 | ), 162 | ), 163 | ); 164 | 165 | register_rest_field( 166 | $this->get_supported_post_types(), 167 | $this->args['api_prop'], 168 | array( 169 | 'schema' => $schema, 170 | 'get_callback' => function ( $object, $field_name, $request ) { 171 | 172 | if ( ! is_null( $request->get_param( 'ignore_page_builder' ) ) ) { 173 | return array(); 174 | } 175 | 176 | $raw_data = $this->get_raw_data( $object['id'] ); 177 | $html = $this->get_rendered_data( $raw_data ); 178 | $modules = array(); 179 | 180 | foreach ( $raw_data as $module_args ) { 181 | if ( $module = Plugin::get_instance()->init_module( $module_args['name'], $module_args ) ) { 182 | $modules[] = array( 183 | 'type' => $module_args['name'], 184 | 'data' => $module->get_json(), 185 | ); 186 | } 187 | } 188 | return array( 189 | 'rendered' => $html, 190 | 'modules' => $modules, 191 | ); 192 | }, 193 | ) 194 | ); 195 | 196 | } 197 | 198 | public function save_data( $object_id, $data = array() ) { 199 | 200 | if ( ! empty( $data ) ) { 201 | update_metadata( 'post', $object_id, $this->id . '-data', $data ); 202 | } else { 203 | delete_metadata( 'post', $object_id, $this->id . '-data' ); 204 | } 205 | 206 | } 207 | 208 | public function get_raw_data( $object_id ) { 209 | $data = (array) get_post_meta( $object_id, $this->id . '-data', true ); 210 | return $this->validate_data( $data ); 211 | } 212 | 213 | /** 214 | * Renders the page builder content from the data array 215 | * 216 | * @param array|int $data Data array or post ID 217 | * @return string 218 | */ 219 | public function get_rendered_data( $data ) { 220 | 221 | $content = ''; 222 | 223 | // Back compat 224 | if ( is_int( $data ) ) { 225 | $data = $this->get_raw_data( $data ); 226 | } 227 | 228 | foreach ( $data as $module_args ) { 229 | if ( $module = Plugin::get_instance()->init_module( $module_args['name'], $module_args ) ) { 230 | $content .= $module->get_rendered(); 231 | } 232 | } 233 | 234 | return $content; 235 | } 236 | 237 | /** 238 | * Is this builder allowed for the current admin screen? 239 | * 240 | * @return boolean 241 | */ 242 | public function is_allowed_for_screen() { 243 | 244 | // function won't be available when not in the admin. 245 | if ( ! function_exists( 'get_current_screen' ) ) { 246 | return false; 247 | } 248 | 249 | $screen = get_current_screen(); 250 | 251 | if ( ! $screen ) { 252 | return false; 253 | } 254 | 255 | $allowed_for_screen = false; 256 | $id = null; 257 | 258 | if ( isset( $_GET['post'] ) ) { 259 | $id = absint( $_GET['post'] ); 260 | } elseif ( isset( $_POST['post_ID'] ) ) { 261 | $id = absint( $_POST['post_ID'] ); 262 | } 263 | 264 | if ( $id ) { 265 | $allowed_for_screen = $this->is_enabled_for_post( $id ); 266 | } 267 | 268 | return $allowed_for_screen; 269 | } 270 | 271 | /** 272 | * Check if page builder is enabled for a single post. 273 | * @param mixed $post_id Post Id. 274 | * @return boolean 275 | */ 276 | public function is_enabled_for_post( $post_id ) { 277 | 278 | // Is enabled for post type. 279 | $allowed = in_array( get_post_type( $post_id ), $this->get_supported_post_types() ); 280 | 281 | // Allow filtering to enable per-post. 282 | return apply_filters( 'modular_page_builder_is_allowed_for_post', $allowed, $post_id, $this->id ); 283 | } 284 | 285 | /** 286 | * Get post types that this page builder instance supports. 287 | * 288 | * @return array $post_types 289 | */ 290 | public function get_supported_post_types() { 291 | return array_filter( get_post_types(), function ( $post_type ) { 292 | return post_type_supports( $post_type, $this->id ); 293 | } ); 294 | } 295 | 296 | /** 297 | * Gets the page builder json and returns it as a PHP array 298 | * or false on failure. 299 | * 300 | * @return array|bool 301 | */ 302 | protected function get_post_data() { 303 | 304 | if ( ! $this->is_allowed_for_screen() ) { 305 | return false; 306 | } 307 | 308 | $nonce = null; 309 | $data = null; 310 | 311 | if ( isset( $_POST[ $this->id . '-nonce' ] ) ) { 312 | $nonce = sanitize_text_field( $_POST[ $this->id . '-nonce' ] ); // Input var okay. 313 | } 314 | 315 | if ( isset( $_POST[ $this->id . '-data' ] ) ) { 316 | $json = $_POST[ $this->id . '-data' ]; // Input var okay. 317 | $data = json_decode( $json, true ); 318 | 319 | /** 320 | * Data is sometimes already slashed, see https://core.trac.wordpress.org/ticket/35408 321 | */ 322 | if ( json_last_error() ) { 323 | $data = json_decode( stripslashes( $json ), true ); 324 | } 325 | 326 | if ( ! json_last_error() && $nonce && wp_verify_nonce( $nonce, $this->id ) ) { 327 | return $data; 328 | } 329 | } 330 | 331 | return false; 332 | } 333 | 334 | } 335 | -------------------------------------------------------------------------------- /inc/class-builder.php: -------------------------------------------------------------------------------- 1 | id = $id; 20 | $this->plugin = Plugin::get_instance(); 21 | 22 | $this->args = wp_parse_args( $args, array( 23 | 'title' => null, 24 | 'api_prop' => $this->id, 25 | 'allowed_modules' => array(), 26 | 'required_modules' => array(), 27 | ) ); 28 | 29 | } 30 | 31 | abstract function get_raw_data( $object_id ); 32 | 33 | abstract function get_rendered_data( $data ); 34 | 35 | abstract function save_data( $object_id, $data = array() ); 36 | 37 | public function validate_data( $modules ) { 38 | 39 | $modules = array_map( array( $this, 'validate_module_data' ), $modules ); 40 | 41 | $modules = array_filter( $modules, function( $module ) { 42 | return ! empty( $module['name'] ); 43 | } ); 44 | 45 | return array_values( $modules ); 46 | 47 | } 48 | 49 | public function validate_module_data( $module ) { 50 | 51 | $module = $this->parse_args( (array) $module, array( 52 | 'name' => '', 53 | 'attr' => array(), 54 | ) ); 55 | 56 | $valid_attr = array(); 57 | foreach ( $module['attr'] as $attr ) { 58 | $attr = $this->validate_attribute_data( $attr ); 59 | $valid_attr[ sanitize_text_field( $attr['name'] ) ] = $attr; 60 | } 61 | $module['attr'] = $valid_attr; 62 | 63 | return $module; 64 | 65 | } 66 | 67 | public function validate_attribute_data( $attr ) { 68 | return $this->parse_args( (array) $attr, array( 69 | 'name' => '', 70 | 'value' => '', 71 | 'type' => '', 72 | ) ); 73 | } 74 | 75 | /** 76 | * Parse Args. 77 | * 78 | * Like wp_parse_args, but whitelisted to attributes with defaults. 79 | * 80 | * @param array $args 81 | * @param array $defaults 82 | * @return array $args 83 | */ 84 | public function parse_args( $args, $defaults ) { 85 | $args = wp_parse_args( $args, $defaults ); 86 | return array_intersect_key( $args, $defaults ); 87 | } 88 | 89 | /** 90 | * Is this builder allowed for the current admin screen? 91 | * 92 | * @return boolean 93 | */ 94 | abstract public function is_allowed_for_screen(); 95 | 96 | } 97 | -------------------------------------------------------------------------------- /inc/class-plugin.php: -------------------------------------------------------------------------------- 1 | instance 14 | * 15 | * @var array 16 | */ 17 | protected $builders; 18 | 19 | /** 20 | * A list of all available modules. 21 | * name => className. 22 | * 23 | * @var array 24 | */ 25 | public $available_modules = array(); 26 | 27 | public static function get_instance() { 28 | $class = get_called_class(); 29 | if ( ! isset( static::$instances[ $class ] ) ) { 30 | self::$instances[ $class ] = $instance = new $class; 31 | $instance->load(); 32 | } 33 | return self::$instances[ $class ]; 34 | } 35 | 36 | public function load() { 37 | 38 | add_action( 'admin_enqueue_scripts', array( $this, 'register_scripts' ), 5 ); 39 | 40 | add_filter( 'teeny_mce_plugins', array( $this, 'enable_autoresize_plugin' ) ); 41 | 42 | add_action( 'wp_ajax_mce_get_posts', array( $this, 'get_posts' ) ); 43 | 44 | } 45 | 46 | public function get_posts() { 47 | 48 | $query = array( 49 | 'post_type' => 'page', 50 | 'fields' => 'ids', 51 | 'posts_per_page' => 5, 52 | 'perm' => 'readable', 53 | 'paged' => 1, 54 | 'post_status' => 'publish', 55 | ); 56 | 57 | if ( isset( $_GET['post_type'] ) ) { 58 | if ( is_array( $_GET['post_type'] ) ) { 59 | $query['post_type'] = array_map( 'sanitize_text_field', wp_unslash( $_GET['post_type'] ) ); 60 | } else { 61 | $query['post_type'] = sanitize_text_field( $_GET['post_type'] ); 62 | } 63 | } 64 | 65 | if ( isset( $_GET['s'] ) ) { 66 | $query['s'] = sanitize_text_field( $_GET['s'] ); 67 | } 68 | 69 | if ( isset( $_GET['page'] ) ) { 70 | $query['paged'] = absint( $_GET['page'] ); 71 | } 72 | 73 | if ( isset( $_GET['post__in'] ) ) { 74 | $query['post__in'] = explode( ',', sanitize_text_field( $_GET['post__in'] ) ); 75 | $query['post__in'] = array_map( 'absint', $query['post__in'] ); 76 | $query['orderby'] = 'post__in'; 77 | } 78 | 79 | if ( isset( $_GET['post_status'] ) ) { 80 | $query['post_status'] = sanitize_text_field( $_GET['post_status'] ); 81 | } 82 | 83 | $query = new WP_Query( $query ); 84 | $data = array(); 85 | 86 | foreach ( $query->posts as $post_id ) { 87 | $data[] = array( 'id' => absint( $post_id ), 'text' => get_the_title( $post_id ) ); 88 | } 89 | 90 | wp_send_json( array( 91 | 'results' => $data, 92 | 'more' => $query->get( 'page' ) < $query->max_num_pages, 93 | ) ); 94 | 95 | } 96 | 97 | public function register_builder_post_meta( $id, $args ) { 98 | $this->builders[ $id ] = new Builder_Post_Meta( $id, $args ); 99 | $this->builders[ $id ]->init(); 100 | } 101 | 102 | public function register_builder_post_content( $id, $args ) { 103 | $this->builders[ $id ] = new Builder_Post_Content( $id, $args ); 104 | $this->builders[ $id ]->init(); 105 | } 106 | 107 | public function get_builder( $id ) { 108 | if ( isset( $this->builders[ $id ] ) ) { 109 | return $this->builders[ $id ]; 110 | } 111 | } 112 | 113 | function register_module( $module_name, $class_name ) { 114 | $this->available_modules[ $module_name ] = $class_name; 115 | } 116 | 117 | function init_module( $module_name, $args = array() ) { 118 | 119 | if ( ! array_key_exists( $module_name, $this->available_modules ) ) { 120 | return; 121 | } 122 | 123 | $class_name = $this->available_modules[ $module_name ]; 124 | 125 | if ( $class_name && class_exists( $class_name ) ) { 126 | return new $class_name( $args ); 127 | } 128 | 129 | throw new \Exception( 'Module not found' ); 130 | 131 | } 132 | 133 | public function register_scripts( $screen ) { 134 | 135 | wp_register_style( 'mpb-select2', '//cdnjs.cloudflare.com/ajax/libs/select2/3.5.2/select2.min.css' ); 136 | wp_register_script( 'mpb-select2', '//cdnjs.cloudflare.com/ajax/libs/select2/3.5.2/select2.min.js', array( 'jquery' ) ); 137 | 138 | wp_register_script( 'modular-page-builder', PLUGIN_URL . '/assets/js/dist/modular-page-builder.js', array( 'jquery', 'backbone', 'wp-backbone', 'wp-color-picker', 'jquery-ui-sortable', 'mpb-select2' ), null, true ); 139 | wp_register_style( 'modular-page-builder', PLUGIN_URL . '/assets/css/dist/modular-page-builder.css', array( 'wp-color-picker', 'mpb-select2' ) ); 140 | 141 | } 142 | 143 | public function enqueue_builder( $post_id = null ) { 144 | 145 | wp_enqueue_media(); 146 | wp_enqueue_script( 'modular-page-builder' ); 147 | wp_enqueue_style( 'modular-page-builder' ); 148 | 149 | $data = array( 150 | 'l10n' => array( 151 | 'addNewButton' => __( 'Add new module', 'modular-page-builder' ), 152 | 'selectDefault' => __( 'Select Module…', 'modular-page-builder' ), 153 | ), 154 | 'available_modules' => array(), 155 | ); 156 | 157 | foreach ( array_keys( $this->available_modules ) as $module_name ) { 158 | 159 | if ( $module = $this->init_module( $module_name ) ) { 160 | $data['available_modules'][] = array( 161 | 'name' => $module->name, 162 | 'label' => $module->label, 163 | 'attr' => $module->attr, 164 | ); 165 | } 166 | } 167 | 168 | wp_localize_script( 'modular-page-builder', 'modularPageBuilderData', $data ); 169 | 170 | add_action( 'admin_footer', array( $this, 'load_templates' ) ); 171 | 172 | } 173 | 174 | public function load_templates() { 175 | foreach ( glob( PLUGIN_DIR . '/templates/*.tpl.html' ) as $filepath ) { 176 | $id = str_replace( '.tpl.html', '', basename( $filepath ) ); 177 | echo ''; 180 | } 181 | } 182 | 183 | /** 184 | * Make sure wpautoresize mce plugin is available for 'teeny' versions. 185 | */ 186 | function enable_autoresize_plugin( $plugins ) { 187 | if ( ! in_array( 'wpautoresize', $plugins ) ) { 188 | $plugins[] = 'wpautoresize'; 189 | } 190 | return $plugins; 191 | } 192 | 193 | } 194 | -------------------------------------------------------------------------------- /inc/class-wp-cli.php: -------------------------------------------------------------------------------- 1 | ] [--dry_run] 19 | */ 20 | public function generate_from_content( $args, $assoc_args ) { 21 | 22 | $plugin = Plugin::get_instance(); 23 | $builder = $plugin->get_builder( 'modular-page-builder' ); 24 | 25 | $assoc_args = wp_parse_args( $assoc_args, array( 26 | 'post_type' => 'post', 27 | 'dry_run' => false, 28 | ) ); 29 | 30 | $query_args = array( 31 | 'post_type' => $assoc_args['post_type'], 32 | 'post_status' => 'any', 33 | 'posts_per_page' => 50, 34 | ); 35 | 36 | $page = 1; 37 | $more_posts = true; 38 | 39 | while ( $more_posts ) { 40 | 41 | $query_args['paged'] = $page; 42 | $query = new WP_Query( $query_args ); 43 | 44 | foreach ( $query->posts as $post ) { 45 | 46 | if ( empty( $post->post_content ) ) { 47 | continue; 48 | } 49 | 50 | WP_CLI::line( "Migrating data for $post->ID" ); 51 | 52 | $module = array( 53 | 'name' => 'text', 54 | 'attr' => array( 55 | array( 'name' => 'body', 'type' => 'wysiwyg', 'value' => $post->post_content ), 56 | ) 57 | ); 58 | 59 | $modules = $builder->get_raw_data( $post->ID ); 60 | $modules[] = $module; 61 | 62 | $modules = $builder->validate_data( $modules ); 63 | 64 | if ( ! $assoc_args['dry_run'] ) { 65 | $modules = $builder->save_data( $post->ID, $modules ); 66 | wp_update_post( array( 'ID' => $post->ID, 'post_content' => '' ) ); 67 | } 68 | } 69 | 70 | $more_posts = $page < absint( $query->max_num_pages ); 71 | $page++; 72 | 73 | } 74 | 75 | } 76 | 77 | /** 78 | * Validate all page builder data. 79 | * 80 | * @subcommand validate-data 81 | * @synopsis [--post_type=] [--dry_run] 82 | */ 83 | public function validate_data( $args, $assoc_args ) { 84 | 85 | $plugin = Plugin::get_instance(); 86 | $builder = $plugin->get_builder( 'modular-page-builder' ); 87 | 88 | $assoc_args = wp_parse_args( $assoc_args, array( 89 | 'post_type' => 'post', 90 | 'dry_run' => false, 91 | ) ); 92 | 93 | $query_args = array( 94 | 'post_type' => $assoc_args['post_type'], 95 | 'posts_per_page' => 50, 96 | 'post_status' => 'any', 97 | ); 98 | 99 | $page = 1; 100 | $more_posts = true; 101 | 102 | while ( $more_posts ) { 103 | 104 | $query_args['paged'] = $page; 105 | $query = new WP_Query( $query_args ); 106 | 107 | foreach ( $query->posts as $post ) { 108 | 109 | WP_CLI::line( "Validating data for $post->ID" ); 110 | 111 | $modules = $builder->get_raw_data( $post->ID ); 112 | 113 | if ( ! $assoc_args['dry_run'] ) { 114 | $builder->save_data( $post->ID, $modules ); 115 | } 116 | } 117 | 118 | $more_posts = $page < absint( $query->max_num_pages ); 119 | $page++; 120 | 121 | } 122 | 123 | } 124 | 125 | /** 126 | * Migrate legacy image data. 127 | * 128 | * We used to store the full image model in the DB. 129 | * Now - just store the ID and fetch the data on output. 130 | * This is leaner and more flexible to changes. 131 | * 132 | * @subcommand migrate-legacy-image-data 133 | * @synopsis [--builder_id] [--post_type=] [--dry_run] 134 | */ 135 | public function migrate_legacy_image_data( $args, $assoc_args ) { 136 | 137 | $assoc_args = wp_parse_args( $assoc_args, array( 138 | 'post_type' => 'post', 139 | 'dry_run' => false, 140 | 'builder_id' => 'modular-page-builder', 141 | ) ); 142 | 143 | $plugin = Plugin::get_instance(); 144 | $builder = $plugin->get_builder( $assoc_args['builder_id'] ); 145 | 146 | if ( ! $builder ) { 147 | return; 148 | } 149 | 150 | $query_args = array( 151 | 'post_type' => $assoc_args['post_type'], 152 | 'posts_per_page' => 50, 153 | 'post_status' => 'any', 154 | // @codingStandardsIgnoreStart 155 | 'meta_key' => sprintf( '%s-data', $assoc_args['builder_id'] ), 156 | // @codingStandardsIgnoreEnd 157 | 'meta_compare' => 'EXISTS', 158 | ); 159 | 160 | $page = 1; 161 | $more_posts = true; 162 | 163 | while ( $more_posts ) { 164 | 165 | $query_args['paged'] = $page; 166 | $query = new WP_Query( $query_args ); 167 | 168 | foreach ( $query->posts as $post ) { 169 | 170 | WP_CLI::line( "Updating data for $post->ID" ); 171 | 172 | $modules = $builder->get_raw_data( $post->ID ); 173 | 174 | foreach ( $modules as &$module ) { 175 | $module = $this->migrate_legacy_image_data_for_module( $module ); 176 | } 177 | 178 | $builder->save_data( $post->ID, $modules ); 179 | 180 | } 181 | 182 | $more_posts = $page < absint( $query->max_num_pages ); 183 | $page++; 184 | 185 | } 186 | 187 | } 188 | 189 | function migrate_legacy_image_data_for_module( $module ) { 190 | 191 | // Migrate data function. 192 | $migrate_callback = function( $val ) { 193 | if ( is_numeric( $val ) ) { 194 | return absint( $val ); 195 | } elseif ( is_object( $val ) && isset( $val->id ) ) { 196 | return absint( $val->id ); 197 | } 198 | }; 199 | 200 | foreach ( $module['attr'] as &$attr ) { 201 | 202 | $simple_image_fields = array( 'image', 'image_logo_headline' ); 203 | 204 | if ( in_array( $module['name'], $simple_image_fields ) && isset( $module['attr']['image'] ) ) { 205 | 206 | $module['attr']['image']['value'] = array_filter( array_map( $migrate_callback, (array) $module['attr']['image']['value'] ) ); 207 | 208 | } elseif ( 'grid' === $module['name'] ) { 209 | 210 | if ( isset( $module['attr']['grid_image'] ) ) { 211 | $module['attr']['grid_image']['value'] = array_filter( array_map( $migrate_callback, $module['attr']['grid_image']['value'] ) ); 212 | } 213 | 214 | if ( isset( $module['attr']['grid_cells'] ) ) { 215 | foreach ( $module['attr']['grid_cells']['value'] as &$cell ) { 216 | $cell->attr->image->value = array_filter( array_map( $migrate_callback, $cell->attr->image->value ) ); 217 | } 218 | } 219 | } 220 | 221 | return $module; 222 | 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /inc/modules/class-blockquote.php: -------------------------------------------------------------------------------- 1 | label = __( 'Large Quote', 'mpb' ); 12 | 13 | // Set all attribute data. 14 | $this->attr = array( 15 | array( 'name' => 'text', 'label' => __( 'Quote Text', 'mpb' ), 'type' => 'textarea', 'value' => '' ), 16 | array( 'name' => 'source', 'label' => __( 'Source', 'mpb' ), 'type' => 'text', 'value' => '' ), 17 | ); 18 | 19 | parent::__construct( $args ); 20 | 21 | } 22 | 23 | public function render() { 24 | 25 | echo '
'; 26 | 27 | if ( $val = $this->get_attr_value( 'text' ) ) { 28 | printf( '
%s
', esc_html( $val ) ); 29 | } 30 | 31 | if ( $val = $this->get_attr_value( 'source' ) ) { 32 | printf( '

%s', esc_html( $val ) ); 33 | } 34 | 35 | echo '

'; 36 | 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /inc/modules/class-header.php: -------------------------------------------------------------------------------- 1 | label = __( 'Header', 'mpb' ); 12 | 13 | // Set all attribute data. 14 | $this->attr = array( 15 | array( 'name' => 'heading', 'label' => __( 'Heading', 'mpb' ), 'type' => 'text' ), 16 | array( 'name' => 'subheading', 'label' => __( 'Subheading (optional)', 'mpb' ), 'type' => 'textarea' ), 17 | ); 18 | 19 | parent::__construct( $args ); 20 | 21 | } 22 | 23 | public function render() { 24 | 25 | $heading_tag = 'h2'; 26 | $subheading_tag = 'p'; 27 | 28 | echo '
'; 29 | 30 | if ( $val = $this->get_attr_value( 'heading' ) ) { 31 | printf( '<%s class="page-builder-heading-heading">%s', esc_attr( $heading_tag ), esc_html( $val ), esc_attr( $heading_tag ) ); 32 | } 33 | 34 | if ( $val = $this->get_attr_value( 'subheading' ) ) { 35 | printf( '<%s class="page-builder-heading-subheading">%s', esc_attr( $subheading_tag ), esc_html( $val ), esc_attr( $subheading_tag ) ); 36 | } 37 | 38 | echo '
'; 39 | 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /inc/modules/class-image.php: -------------------------------------------------------------------------------- 1 | label = __( 'Image', 'mpb' ); 12 | 13 | // Set all attribute data. 14 | $this->attr = array( 15 | array( 'name' => 'image', 'label' => __( 'Image / Gallery', 'mpb' ), 'type' => 'attachment', 'description' => 'Select one or more images.', 'config' => [ 'multiple' => true ] ), 16 | array( 'name' => 'caption', 'label' => __( 'Caption', 'mpb' ), 'type' => 'text' ), 17 | ); 18 | 19 | parent::__construct( $args ); 20 | 21 | } 22 | 23 | public function render() { 24 | 25 | $image_ids = (array) $this->get_attr_value( 'image' ); 26 | 27 | if ( empty( $image_ids ) ) { 28 | return; 29 | } 30 | 31 | echo '
'; 32 | 33 | if ( count( $image_ids ) > 1 ) { 34 | echo do_shortcode( sprintf( '[gallery ids="%s"]', implode( ',', $image_ids ) ) ); 35 | } else { 36 | echo wp_get_attachment_image( $image_ids[0], 'large' ); 37 | } 38 | 39 | if ( $caption = $this->get_attr_value( 'caption' ) ) { 40 | printf( '

%s

', esc_html( $caption ) ); 41 | } 42 | 43 | echo '
'; 44 | 45 | } 46 | 47 | public function get_json() { 48 | $data = parent::get_json(); 49 | $data['image'] = array_map( function( $value ) { 50 | return wp_get_attachment_image_src( $value, 'large' ); 51 | }, $data['image'] ); 52 | return $data; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /inc/modules/class-module.php: -------------------------------------------------------------------------------- 1 | update_all_attr_values( $args['attr'] ); 15 | } 16 | } 17 | 18 | public function render() { 19 | ?> 20 |

You must implement `render` in

21 | render(); 27 | return ob_get_clean(); 28 | } 29 | 30 | public function get_json() { 31 | 32 | $json = array(); 33 | foreach ( $this->attr as $attr ) { 34 | $json[ $attr['name'] ] = $this->get_attr_value( $attr['name'] ); 35 | } 36 | 37 | return $json; 38 | } 39 | 40 | protected function get_attr( $attr_name ) { 41 | foreach ( $this->attr as $key => $attr ) { 42 | if ( $attr['name'] === $attr_name ) { 43 | return $attr; 44 | } 45 | } 46 | } 47 | 48 | protected function get_attr_value( $attr_name ) { 49 | if ( $attr = $this->get_attr( $attr_name ) ) { 50 | return isset( $attr['value'] ) ? $attr['value'] : null; 51 | } 52 | } 53 | 54 | protected function update_all_attr_values( array $data ) { 55 | foreach ( $data as $attr ) { 56 | if ( isset( $attr['name'] ) && isset( $attr['value'] ) ) { 57 | $this->update_attr_value( $attr['name'], $attr['value'] ); 58 | } 59 | } 60 | } 61 | 62 | protected function update_attr_value( $attr_name, $value ) { 63 | foreach ( $this->attr as &$attr ) { 64 | if ( $attr_name === $attr['name'] ) { 65 | $attr['value'] = $value; 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /inc/modules/class-text.php: -------------------------------------------------------------------------------- 1 | label = __( 'Text', 'mpb' ); 12 | 13 | // Set all attribute data. 14 | $this->attr = array( 15 | array( 'name' => 'body', 'label' => __( 'Content', 'mpb' ), 'type' => 'html' ), 16 | ); 17 | 18 | parent::__construct( $args ); 19 | 20 | } 21 | 22 | public function render() { 23 | 24 | if ( $val = $this->get_attr_value( 'body' ) ) { 25 | printf( '
%s
', wpautop( wp_kses_post( $val ) ) ); 26 | } 27 | 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /modular-page-builder.php: -------------------------------------------------------------------------------- 1 | register_module( 'header', __NAMESPACE__ . '\Modules\Header' ); 45 | $plugin->register_module( 'text', __NAMESPACE__ . '\Modules\Text' ); 46 | $plugin->register_module( 'image', __NAMESPACE__ . '\Modules\Image' ); 47 | $plugin->register_module( 'blockquote', __NAMESPACE__ . '\Modules\Blockquote' ); 48 | 49 | $plugin->register_builder_post_meta( 'modular-page-builder', array( 50 | 'title' => __( 'Page Body Content' ), 51 | 'api_prop' => 'page_builder', 52 | 'allowed_modules' => array( 'header', 'text', 'image', 'video', 'blockquote', ), 53 | ) ); 54 | 55 | }, 100 ); 56 | 57 | if ( defined( 'WP_CLI' ) && WP_CLI ) { 58 | require __DIR__ . '/inc/class-wp-cli.php'; 59 | WP_CLI::add_command( 'modular-page-builder', __NAMESPACE__ . '\\CLI' ); 60 | } 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modular-page-builder", 3 | "version": "0.0.1", 4 | "main": "Gruntfile.js", 5 | "author": "Human Made Limited", 6 | "license": "GPL V2", 7 | "devDependencies": { 8 | "browserify": "^8.1.3", 9 | "browserify-shim": "^3.8.3", 10 | "grunt": "^0.4.5", 11 | "grunt-autoprefixer": "^3.0.3", 12 | "grunt-browserify": "^3.4.0", 13 | "grunt-contrib-jshint": "^0.11.3", 14 | "grunt-contrib-watch": "^0.6.1", 15 | "grunt-phpcs": "^0.4.0", 16 | "grunt-sass": "^1.0.0", 17 | "remapify": "1.4.3" 18 | }, 19 | "browserify": { 20 | "transform": [ 21 | "browserify-shim" 22 | ] 23 | }, 24 | "browserify-shim": { 25 | "jquery": "global:jQuery", 26 | "wp": "global:wp", 27 | "underscore": "global:_", 28 | "backbone": { 29 | "exports": "global:Backbone", 30 | "depends": [ 31 | "jquery", 32 | "underscore" 33 | ] 34 | }, 35 | "wp": { 36 | "exports": "global:wp", 37 | "depends": [ 38 | "backbone" 39 | ] 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /templates/builder.tpl.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 11 | 12 | 15 | 16 |
17 | -------------------------------------------------------------------------------- /templates/field-attachment.tpl.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | <# if ( Array.isArray( data.value ) && data.value.length ) { #> 6 | <# _.each( data.value, function( attachment ) { #> 7 |
8 | 9 | <# if ( 'image' === attachment.type && attachment.sizes ) { #> 10 | 15 | <# } else if ( 'image' !== attachment.type ) { #> 16 | 17 |
{{ attachment.filename }}
18 | <# } else { #> 19 | 20 | <# } #> 21 | 22 |
23 |
24 | <# } ); #> 25 | <# } #> 26 | 27 |
28 | 29 | <# if ( data.config.sizeReq ) { #> 30 |

Images must be {{ data.config.sizeReq.width }}px × {{ data.config.sizeReq.height }}px

31 | <# } #> 32 | -------------------------------------------------------------------------------- /templates/field-checkbox.tpl.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /templates/field-content-editable.tpl.html: -------------------------------------------------------------------------------- 1 |
{{ data.value }}
7 | -------------------------------------------------------------------------------- /templates/field-link.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /templates/field-number.tpl.html: -------------------------------------------------------------------------------- 1 | id="{{ data.id }}"<# } #> 4 | class="number-text" 5 | value="{{ data.value }}" 6 | /> 7 | -------------------------------------------------------------------------------- /templates/field-select.tpl.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /templates/field-text.tpl.html: -------------------------------------------------------------------------------- 1 | 7 | placeholder="{{ data.config.placeholder }}" 8 | <# } #> 9 | /> 10 | -------------------------------------------------------------------------------- /templates/field-textarea.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/field-wysiwyg.tpl.html: -------------------------------------------------------------------------------- 1 | 'mpb-placeholder-name', 7 | 'textarea_rows' => 3, 8 | 'teeny' => true, 9 | 'tinymce' => array( 10 | 'resize' => false, 11 | 'wp_autoresize_on' => true, 12 | ) 13 | ) ); 14 | 15 | wp_editor( 'mpb-placeholder-content', 'mpb-placeholder-id', $args ); 16 | 17 | $editor = ob_get_clean(); 18 | 19 | $editor = str_replace( 'mpb-placeholder-name', 'mpb-text-body-{{ data.id }}', $editor ); 20 | $editor = str_replace( 'mpb-placeholder-id', 'mpb-text-body-{{ data.id }}', $editor ); 21 | $editor = str_replace( 'mpb-placeholder-content', '{{{ data.value }}}', $editor ); 22 | 23 | echo $editor; 24 | 25 | ?> 26 | -------------------------------------------------------------------------------- /templates/form-row.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | <# if ( 'undefined' !== typeof data.desc && data.desc.length ) { #> 6 | {{ data.desc }} 7 | <# } #> 8 | -------------------------------------------------------------------------------- /templates/module-edit-blockquote.tpl.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /templates/module-edit-header.tpl.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 |
14 | -------------------------------------------------------------------------------- /templates/module-edit-image.tpl.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 | 8 |
9 | -------------------------------------------------------------------------------- /templates/module-edit-text.tpl.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | <# if ( 'heading' in data.attr ) { #> 5 |
6 | 7 | 8 |
9 | <# } #> 10 | 11 |
12 | 13 |
14 |
15 | 16 | <# if ( 'style' in attr ) { #> 17 |
18 | 19 | 29 |
30 | <# } #> 31 | 32 |
33 | -------------------------------------------------------------------------------- /templates/module-edit-tools.tpl.html: -------------------------------------------------------------------------------- 1 |

{{ data.label }}

2 | 3 | --------------------------------------------------------------------------------