├── .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 | 
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%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%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 |
7 |
8 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/templates/module-edit-header.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/templates/module-edit-image.tpl.html:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/templates/module-edit-text.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <# if ( 'heading' in data.attr ) { #>
5 |
6 |
7 |
8 |
9 | <# } #>
10 |
11 |
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 |
--------------------------------------------------------------------------------