PluginProbe
Gridable / 1.2.0
Gridable v1.2.0
1.2.12 1.2.11 trunk 0.1.0 0.5.0 1.0.0 1.1.0 1.2.0 1.2.1 1.2.10 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9
gridable / admin / js / gridable.js

gridable.js in Gridable 1.2.0, at admin/js/gridable.js

1,276 lines 37.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 (function ($, exports) {
2
3 $(document).ready(function () {
4
5 /**
6 * A TinyMCE plugin which handles the rendering of grid shortcodes
7 * Docs to consider:
8 * Manager: https://www.tinymce.com/docs/api/tinymce/tinymce.editormanager
9 * Events: https://www.tinymce.com/docs/api/tinymce/tinymce.editor/#events
10 */
11 tinymce.PluginManager.add('gridable', function (editor, url) {
12 var toolbar,
13 l10n = gridable_params.l10n,
14 gridable_resizing = false,
15 xStart,
16 xLast,
17 xEnd,
18 nextWidth,
19 prevWidth,
20 gridStyle,
21 gridWidth,
22 colWidth,
23 debug = true,
24 $next,
25 $prev;
26
27 // The bix X button that removes the entire row shortcode
28 editor.addButton('gridable_row_remove', {
29 tooltip: l10n.remove_row,
30 icon: 'dashicon dashicons-no',
31 onclick: function (event) {
32 // first get the current selected node and search for his "row" parent
33 var node = editor.selection.getNode(),
34 wrap = editor.$(node).closest('.row.gridable-mceItem');
35
36 // now if there is a parent row, also remove the surrounding <p> tags
37 if (wrap) {
38 if (wrap.nextSibling) {
39 editor.selection.select(wrap.nextSibling);
40 } else if (wrap.previousSibling) {
41 editor.selection.select(wrap.previousSibling);
42 } else {
43 editor.selection.select(wrap.parentNode);
44 }
45
46 editor.selection.collapse(true);
47 editor.dom.remove(wrap);
48 } else {
49 editor.dom.remove(node);
50 }
51 }
52 });
53
54 /**
55 * The Add Column button comes with a few rules:
56 *
57 * * A row suppports only 6 columns
58 * * When adding a new column take the space from the biggest one
59 *
60 */
61 editor.addButton('gridable_add_col', {
62 tooltip: l10n.add_column,
63 icon: 'dashicon dashicons-plus',
64 onclick: function (event) {
65 var node = editor.selection.getNode(),
66 wrap = editor.$(node).closest('.row.gridable-mceItem'),
67 columns = wrap.find('.col.gridable-mceItem'),
68 new_size = 0;
69
70 if (columns.length > 0) {
71 columns.each(function (i, el) {
72 var current_size = editor.$(el).attr('data-sh-column-attr-size');
73
74 if ( current_size > 2 ) {
75 editor.$(el).attr('data-sh-column-attr-size', current_size - 2);
76 new_size += 2;
77 return false;
78 }
79 });
80 }
81
82 if ( new_size === 0 ) {
83 new_size = 12; // asta e
84 }
85
86 /**
87 * Create a new html template with the new column and append it to the current editing row
88 */
89 var tmp = getColTemplate({
90 atts: {size: new_size.toString()},
91 size: new_size.toString(),
92 content: '<p>' + l10n.new_column_content + '</p>'
93 });
94
95 node = editor.dom.create('DIV', {}, tmp);
96
97 wrap[0].appendChild(node.children[0]);
98
99 editor.execCommand('gridableAddResizeHandlers');
100 }
101 });
102
103 editor.addButton('gridable_remove_col', {
104 tooltip: l10n.remove_column,
105 icon: 'dashicon dashicons-minus',
106 onclick: function (event) {
107 var node = editor.selection.getNode(),
108 column = editor.$(node).closest('.col.gridable-mceItem');
109
110 // if (window.confirm('Are you sure you want to remove this column?')) {
111 var column_size = editor.$(column).attr('data-sh-column-attr-size');
112
113 if (column[0].previousElementSibling !== null) {
114 increase_column_size_with(column_size, column[0].previousElementSibling);
115 } else if (column[0].nextElementSibling !== null) {
116 increase_column_size_with(column_size, column[0].nextElementSibling);
117 } else {
118 editor.$(node).closest('.row.gridable-mceItem').remove();
119 }
120 column.remove();
121 // }
122 }
123 });
124
125 editor.addButton('gridable_row_options', {
126 tooltip: l10n.edit_row,
127 icon: 'dashicon dashicons-edit',
128 onclick: function (event) {
129
130 var node = editor.selection.getNode(),
131 row = editor.$(node).closest('.row.gridable-mceItem');
132
133 GridableOptionsModal.open('row', editor, row[0]);
134 }
135 });
136
137 editor.addButton('gridable_col_options', {
138 tooltip: l10n.edit_column,
139 icon: 'dashicon dashicons-edit',
140 onclick: function (event) {
141
142 var node = editor.selection.getNode(),
143 column = editor.$(node).closest('.col.gridable-mceItem');
144
145 GridableOptionsModal.open('column', editor, column[0]);
146 }
147 });
148
149 editor.addButton('gridable_col_label', {
150 text: l10n.column + ':',
151 disabled: true,
152 role: 'separator'
153 });
154
155 editor.addButton('gridable_row_label', {
156 text: l10n.row + ':',
157 disabled: true,
158 role: 'separator'
159 });
160
161
162 // @TODO https://github.com/pixelgrade/gridable/issues/57
163 // editor.on('ExecCommand', function (args, e) {
164 // if ( 'SelectAll' !== args.command ) {
165 // return;
166 // }
167 // console.log( editor.selection.getRng() );
168 // });
169 //
170 // editor.on('BeforeExecCommand', function (args) {
171 // if ( 'SelectAll' !== args.command ) {
172 // return;
173 // }
174 // console.log( args );
175 // console.log( editor.selection.getRng() );
176 // });
177
178 /**
179 * Create the toolbar with the controls for row
180 */
181 editor.on('wptoolbar', function (args) {
182 var selected_row = editor.dom.$(args.element).parents('.row.gridable-mceItem');
183
184 // if a row is focused we display the toolbar and add a CSS class
185 if ( selected_row.length > 0 && ( ['P', 'h1', 'H2', 'H3', 'H4', 'H5', 'STRONG', 'SPAN', 'DIV', 'FONT', 'BR'].indexOf(args.element.tagName) !== -1 || args.element.className.indexOf('gridable-mceItem') !== -1 ) ) {
186 // if ( selected_row.length > 0 ) {
187 args.toolbar = toolbar;
188 args.selection = selected_row[0];
189 selected_row.addClass('is-focused');
190 } else { // we need to ensure that the focused class is removed
191 var $rows = editor.dom.$('.row.gridable-mceItem.is-focused');
192 if ($rows.length > 0) {
193 $rows = $rows.removeClass('is-focused');
194 }
195 }
196 });
197
198 /**
199 * Assign buttons for our toolbar
200 * When the editor is initialized, we need to bind the resize events for every resize handler that may appear
201 */
202 editor.once('preinit', function () {
203 if (editor.wp && editor.wp._createToolbar) {
204
205 // the fist two options must be the add / remove columns
206 var toolbar_buttons = [
207 'gridable_col_label',
208 'gridable_add_col',
209 'gridable_remove_col',
210 ];
211
212 if ( typeof gridable_column_options !== 'undefined' && Object.keys( gridable_column_options ).length > 1 ) {
213 toolbar_buttons.push( 'gridable_col_options' );
214 }
215
216 toolbar_buttons.push('|');
217
218 toolbar_buttons.push('gridable_row_label');
219
220 if ( typeof gridable_row_options !== 'undefined' && Object.keys( gridable_row_options ).length > 1 ) {
221 // just a separator
222 toolbar_buttons.push( 'gridable_row_options' );
223 }
224
225 // the remove row button must be the last
226 toolbar_buttons.push('gridable_row_remove');
227
228 toolbar = editor.wp._createToolbar( toolbar_buttons );
229 }
230 });
231
232 /**
233 * Whenever the cursor changes it's position the parent may be a grid column, then we need to add handlers
234 */
235 // editor.on('NodeChange', function (event) {
236 //
237 // if ('html' === window.getUserSetting('editor')) {
238 // return;
239 // }
240 //
241 // var el = editor.dom.$(event.element);
242 //
243 // });
244
245 editor.on('keydown', function (evt) {
246 if ('html' === window.getUserSetting('editor')) {
247 return;
248 }
249
250 /**
251 * While pressing enter in editor the cursor should not be allowed the leave the column
252 */
253 if (evt.keyCode == 13) { // if Enter is pressed
254 var dom = editor.dom,
255 selection = editor.selection,
256 settings = editor.settings,
257 rng = selection.getRng(true),
258 container = rng.startContainer,
259 parentBlock = dom.getParent(container, dom.isBlock), // Find parent block and setup empty block paddings
260 containerBlock = parentBlock ? dom.getParent(parentBlock.parentNode, dom.isBlock) : null;
261
262 // Handle enter in column item
263 if (typeof parentBlock !== "null"
264 && dom.isEmpty(parentBlock)
265 && containerBlock !== null
266 && typeof containerBlock.tagName !== "undefined"
267 && "DIV" === containerBlock.tagName
268 && containerBlock.className.indexOf("col gridable-mceItem") !== -1) {
269 editor.execCommand("InsertLineBreak", false, evt);
270 evt.preventDefault();
271 return false;
272 }
273 }
274 });
275
276 /**
277 * Event triggered when the content is set
278 * Here we replace the shortcodes like [row] with <section class="row">
279 */
280 editor.on('SetContent', function (event) {
281 // console.group('GetContent');
282 if (!event.content || 'raw' === event.format || 'savecontent' === event.type || event.selection === true) {
283 return;
284 }
285
286 editor.execCommand('gridableRender');
287 // console.groupEnd('GetContent');
288 });
289
290 /**
291 * After we save the content ensure that the shortcodes are rendered back
292 */
293 editor.on('PreProcess', function (event) {
294 if ('html' === window.getUserSetting('editor')) {
295 return false;
296 } else if ( editor.editorCommands.hasCustomCommand('gridableRestore' ) && event.save === true ) {
297 editor.editorCommands.execCommand('gridableRestore');
298 }
299 });
300
301 editor.on( 'pastePostProcess', function( event ) {
302 var bm = tinyMCE.activeEditor.selection.getBookmark();
303 var node = event.target.selection.getNode();
304
305 // mceInsertContent is messing our resize handlers, we stick with this simple replce for now
306 $(node).replaceWith( event.node.innerHTML );
307 // editor.execCommand('mceInsertContent', false, event.node.innerHTML);
308
309 // event.target.selection.setNode( node );
310 // event.target.selection.collapse(0);
311 tinyMCE.activeEditor.selection.moveToBookmark(bm);
312
313 event.preventDefault();
314 });
315
316 /**
317 * This function turns the grid shortcodes into HTML
318 *
319 * [row][/row] will turn into <section class="row"></section>
320 *
321 * @param content
322 * @returns {*}
323 */
324 editor.addCommand('gridableRender', function () {
325
326 var $save_btn = jQuery('#publishing-action .button');
327 $save_btn.attr('disabled', 'disabled');
328
329 // console.group('gridableRender');
330 var content = this.dom.doc.body.innerHTML;
331
332 if (typeof content === "undefined") {
333 return;
334 }
335 // first we need to strip grid shortcodes from p's
336 content = remove_p_around_shortcodes(content);
337
338 // same for cols
339 content = maybe_replace_columns(content);
340
341 // now replace row shortcodes with their HTML if there are any
342 content = maybe_replace_rows(content);
343
344 // event.content = content;
345 this.dom.doc.body.innerHTML = content;
346
347 // console.groupEnd('gridableRender');
348 // bind resize events
349 editor.execCommand('gridableAddResizeHandlers');
350
351 $save_btn.removeAttr('disabled');
352 });
353
354 /**
355 * This function must restore the shortcodes from the rendering state
356 *
357 * Since we are handling html we rather create a DOM element and use its innerHTML as parsing method
358 *
359 * <section class="row"></section> will turn into [row][/row]
360 *
361 * @param content
362 * @returns {*|string}
363 */
364 editor.addCommand('gridableRestore', function () {
365
366 var $save_btn = jQuery('#publishing-action .button');
367 $save_btn.attr('disabled', 'disabled');
368
369 // console.group('gridableRestore');
370
371 // hold all the content inside a HTML element.This way we keep it safe
372 // var content_process = this.dom.create('DIV', {}, event.content);
373 var content_process = this.dom.doc.body,
374 restore_needed = false;
375
376 content_process.innerHTML = content_process.innerHTML.replace(/(<p>&nbsp;<\/p>)/gi, '<br />');
377
378 // get all the columns inside the editor
379 var columns = content_process.querySelectorAll('.col.gridable-mceItem'),
380 columnReplacement = '';
381
382 for (var columnIndex = 0; columnIndex < columns.length; columnIndex++) {
383
384 // create a new shortcode string like [col size="6"]
385 var columnReplacement = wp.shortcode.string({
386 tag: 'col',
387 // attrs: {size: columns[columnIndex].getAttribute('data-sh-column-attr-size')},
388 attrs: get_valid_column_attrs(columns[columnIndex]),
389 content: columns[columnIndex].innerHTML.trim()
390 });
391
392 // now replace the column html with the [col] shortcode
393 content_process.innerHTML = content_process.innerHTML.replace(columns[columnIndex].outerHTML, columnReplacement);
394 restore_needed = true;
395 }
396
397 // first restore back the row shortcodes
398 var rows = content_process.querySelectorAll('.row.gridable-mceItem'),
399 rowReplacement = '';
400
401 for (var rowIndex = 0; rowIndex < rows.length; rowIndex++) {
402 // this is the shortcode representation of the row
403 var rowReplacement = wp.shortcode.string({
404 tag: 'row',
405 attrs: get_valid_row_attrs(rows[rowIndex]),
406 content: rows[rowIndex].innerHTML.trim()
407 });
408
409 // replace the row html with the shortcode
410 content_process.innerHTML = content_process.innerHTML.replace(rows[rowIndex].outerHTML, rowReplacement);
411 restore_needed = true;
412 }
413
414 if (restore_needed) {
415 // @TODO find a better way to save the restored content without rendering it
416 // this.setContent(content_process.innerHTML, { no_events: true});
417 content_process = this.dom.doc.body.innerHTML = content_process.innerHTML;
418 // console.debug( content_process );
419 }
420 // console.groupEnd('gridableRestore');
421
422 $save_btn.removeAttr('disabled');
423 });
424
425 editor.addCommand('gridableRemoveResize', function () {
426 var $grids = editor.dom.$('.gridable__handle');
427 var $cols = editor.dom.$('.col.gridable-mceItem');
428
429 $cols.each( function (count, column) {
430 var row = editor.dom.$(column).parents('.row.gridable-mceItem');
431
432 editor.dom.unbind( column, 'mousedown', onMouseDown );
433 editor.dom.unbind( row[0], 'mousemove', onMouseMove );
434 editor.dom.unbind( row[0], 'mouseup', onMouseUp );
435 editor.dom.unbind( row[0], 'mouseleave', onMouseUp );
436 });
437
438 $grids.remove();
439 });
440
441 /**
442 * Function to add Column Resize Handlers and bound events
443 */
444 editor.addCommand('gridableAddResizeHandlers', function () {
445 editor.execCommand('gridableRemoveResize');
446
447 var $cols = editor.dom.$('.col.gridable-mceItem');
448
449 $cols.each( function (count, column) {
450 var row = editor.dom.$(column).parents('.row.gridable-mceItem');
451
452 editor.dom.bind( column, 'mousedown', onMouseDown );
453 editor.dom.bind( row[0], 'mousemove', onMouseMove, column );
454 editor.dom.bind( row[0], 'mouseup', onMouseUp, column );
455 editor.dom.bind( row[0], 'mouseleave', onMouseUp, column );
456 });
457 });
458
459
460 function getGridStyle(grid) {
461 var gridStyle = getComputedStyle(grid),
462 gridWidth = grid.clientWidth - parseFloat(gridStyle.paddingLeft) - parseFloat(gridStyle.paddingRight),
463 colWidth = gridWidth / 12;
464
465 return {
466 gridStyle: gridStyle,
467 gridWidth: gridWidth,
468 colWidth: colWidth
469 }
470 }
471
472 /**
473 * Each column has a before pseudo element which acts as a resize handler
474 * Detect if the click event is made over this pseude elements and add the resize class if so
475 * @param e
476 * @returns {boolean}
477 */
478 function onMouseDown(e) {
479 // no class === no fun
480 if ( typeof e.target.className === "undefined" ) {
481 return true;
482 }
483
484 xStart = e.clientX;
485 xLast = xStart;
486
487 if ( e.target.className.indexOf('col gridable-mceItem') !== -1) {
488 var $el = editor.dom.$( e.target );
489
490 if ( ( e.clientX - $el.offset().left ) <= 25 ) {
491 e.preventDefault();
492 e.stopImmediatePropagation();
493
494 var grid = e.target.closest('.grid'),
495 $grid = editor.$(grid);
496
497 var gstyle = getGridStyle(grid);
498
499 gridStyle = gstyle.gridStyle;
500 gridWidth = gstyle.gridWidth;
501 colWidth = gstyle.colWidth;
502
503 $grid.addClass('grabbing');
504
505 $next = editor.dom.$(e.target);
506 $prev = $next.prev('.col');
507
508 gridable_resizing = true;
509 updateLoop();
510
511 var width = parseInt($next[0].offsetWidth, 10),
512 colNo = Math.round(width / colWidth);
513 }
514 }
515 }
516
517 function onMouseMove(e) {
518 if (gridable_resizing) {
519 // console.log('handler mousemove');
520 e.preventDefault();
521 e.stopImmediatePropagation();
522 xLast = e.clientX;
523 return false;
524 }
525 };
526
527 function onMouseUp(e) {
528 // console.log('handler mouse out');
529 var grid = editor.dom.$(e.target).closest('.grid'),
530 $grid = editor.dom.$(grid);
531
532 $grid.removeClass('grabbing');
533
534 xEnd = e.clientX;
535 gridable_resizing = false;
536 };
537
538 function updateLoop() {
539
540 if (!gridable_resizing || !xLast || !xStart) {
541 return false;
542 }
543
544 if ( $next.length && $prev.length && typeof xStart !== "unedfined" ) {
545
546 if ( xLast - xStart >= colWidth / 2 ) {
547 var nextSpan = parseInt($next[0].getAttribute('data-sh-column-attr-size'), 10),
548 prevSpan = parseInt($prev[0].getAttribute('data-sh-column-attr-size'), 10);
549
550 if (nextSpan != 1) {
551 $next[0].setAttribute('data-sh-column-attr-size', nextSpan - 1);
552 $prev[0].setAttribute('data-sh-column-attr-size', prevSpan + 1);
553
554 xStart += 1 * colWidth;
555 }
556 } else if (xStart - xLast >= colWidth / 2) {
557 var nextSpan = parseInt($next[0].getAttribute('data-sh-column-attr-size'), 10),
558 prevSpan = parseInt($prev[0].getAttribute('data-sh-column-attr-size'), 10);
559
560 if (prevSpan != 1) {
561 $next[0].setAttribute('data-sh-column-attr-size', nextSpan + 1);
562 $prev[0].setAttribute('data-sh-column-attr-size', prevSpan - 1);
563
564 xStart -= 1 * colWidth;
565 }
566 }
567 }
568
569 requestAnimationFrame(updateLoop);
570 }
571
572
573 /** === Helper functions ==== **/
574
575 /**
576 * Try to keep our shortcodes clear of wraping P tags
577 * This is very important since a [row] shortcode will turn into a <section class="row">
578 * In this case there is now way we can have a <p>[row]</p> turned into <p><section class="row"></p>
579 * The world will end then.
580 *
581 * @param content
582 * @returns {*}
583 */
584 var remove_p_around_shortcodes = function (content) {
585 /** Starting shortcodes **/
586
587 // This catches anything like <p>[row] [col]</p> or <p>[row]</p>
588 content = content.replace(/<p[^>]*>\s*(\[\s*row[^\]]*\])\s*(\[\s*col[^\]]*\])?\s*<\s*\/p\s*>/gmi, '$1$2');
589
590 // This catches anything like <p>[row] [col] some text </p> or <p>[row] some text</p> and replaces it with [row][col]<p>some text</p> or [row]<p>some text</p>
591 content = content.replace(/<p[^>]*>\s*(\[\s*row[^\]]*\])\s*(\[\s*col[^\]]*\])?(.*?)<\s*\/p\s*>/gmi, '$1$2<p>$3</p>');
592
593 // This catches anything like <p>[col] some text </p> and replaces it with [col]<p> some text</p>
594 content = content.replace(/<p[^>]*>\s*(\[\s*col[^\]]*\])(.*?)<\s*\/p\s*>/gmi, '$1<p>$2</p>');
595
596 /** Ending shortcodes or Ending and Opening **/
597
598 // This catches anything like <p>[/col] [/row]<p> or <p>[/col</p> or <p>[/row]</p>
599 content = content.replace(/<p[^>]*>\s*(\[\s*\/col[^\]]*\])?\s*(\[\s*\/row[^\]]*\])?\s*<\s*\/p\s*>/gmi, '$1$2');
600
601 // This catches anything like <p>[/col] [col]<p>
602 content = content.replace(/<p[^>]*>\s*(\[\s*\/col[^\]]*\])\s*(\[\s*col[^\]]*\])\s*<\s*\/p\s*>/gmi, '$1$2');
603
604 // This catches anything like <p>[/col] [col] some text<p> and replaces it with [/col][col]<p>some text</p>
605 content = content.replace(/<p[^>]*>\s*(\[\s*\/col[^\]]*\])\s*(\[\s*col[^\]]*\])(.*?)<\s*\/p\s*>/gmi, '$1$2<p>$3</p>');
606
607 // This catches anything like <p>[/row] [row]<p>
608 content = content.replace(/<p[^>]*>\s*(\[\s*\/row[^\]]*\])\s*(\[\s*row[^\]]*\])\s*<\s*\/p\s*>/gmi, '$1$2');
609
610 // This catches anything like <p>[/row] [row] some text<p> and replaces it with [/row] [row]<p>some text</p>
611 content = content.replace(/<p[^>]*>\s*(\[\s*\/row[^\]]*\])\s*(\[\s*row[^\]]*\])(.*?)<\s*\/p\s*>/gmi, '$1$2');
612
613 // This is a fail safe in case there is a stranded </p>
614 // This catches anything like [row]</p> or [row] [col]</p>
615 content = content.replace(/(\[\s*row[^\]]*\])\s*(\[\s*col[^\]]*\])?\s*<\s*\/p\s*>/gmi, '$1$2');
616 // This catches anything like [col]</p>
617 content = content.replace(/(\[\s*col[^\]]*\])\s*<\s*\/p\s*>/gmi, '$1');
618
619 // avoid casses like <p>[/col], you can never start a paragraf when you are just closing a column
620 content = content.replace( /<p[^>]*>\s*(\[\s*\/col[^\]]*\])/gmi, '$1');
621 return content;
622 };
623
624 /**
625 * Incresease the column size based on a given number
626 * @TODO maybe decrese the number of columns since we already know that 1 column will be deleted
627 * @param column_size
628 * @param node
629 */
630 function increase_column_size_with(column_size, node) {
631 var current_size = editor.$(node).attr('data-sh-column-attr-size');
632
633 editor.$(node).attr('data-sh-column-attr-size', (parseInt(current_size) + parseInt(column_size)));
634 }
635
636 /**
637 * Render [row] shortcodes
638 * @param content
639 * @returns {*}
640 */
641 function maybe_replace_rows(content) {
642 var next = wp.shortcode.next('row', content);
643
644 if (typeof next !== "undefined") {
645 var template_attrs = {
646 tag: "row",
647 content: next.shortcode.content,
648 atts: next.shortcode.attrs.named
649 };
650
651 if ( typeof next.shortcode.attrs.named.bg_color !== "undefined" ) {
652 template_attrs.atts.style = "background-color:" + next.shortcode.attrs.named.bg_color + ';';
653 }
654
655 var row = getRowTemplate(template_attrs);
656
657 var new_content = content.replace(next.content, row);
658
659 // for recursivity, try again
660 new_content = maybe_replace_rows(new_content);
661
662 return new_content;
663 }
664 return content;
665 }
666
667 /**
668 * Render columns shortcodes
669 * @param content
670 * @returns {*}
671 */
672 function maybe_replace_columns(content) {
673
674 //content = remove_p_around_shortcodes(content);
675
676 var next = wp.shortcode.next('col', content);
677
678 if (typeof next !== "undefined") {
679
680 var template_attrs = {
681 tag: "col",
682 content: next.shortcode.content,
683 atts: next.shortcode.attrs.named
684 };
685
686 if ( typeof next.shortcode.attrs.named.bg_color !== "undefined" ) {
687 template_attrs.atts.style = "background-color:" + next.shortcode.attrs.named.bg_color + ';';
688 }
689
690 // get the HTML template of a column
691 var col = getColTemplate(template_attrs);
692
693 var new_content = content.replace(next.content, col);
694
695 // in case of inner columns, try again
696 new_content = maybe_replace_columns(new_content);
697
698 return new_content;
699 }
700
701 return content;
702 }
703
704 function get_valid_row_attrs(el) {
705 var to_return = {};
706
707 var needle = 'data-sh-row-attr-';
708
709 Array.prototype.slice.call(el.attributes).forEach(function (item) {
710 var attr_name = item.name.replace(needle, '');
711
712 if (item.name.indexOf(needle) !== -1 && attr_name in gridable_row_options) {
713
714 if (item.value !== '') {
715 to_return[attr_name] = item.value;
716 } else if (typeof gridable_row_options[attr_name].default !== 'undefined') {
717 to_return[attr_name] = gridable_row_options[attr_name].default;
718 }
719 }
720 });
721
722 return to_return;
723 }
724
725 function get_valid_column_attrs(el) {
726
727 var to_return = {};
728 var needle = 'data-sh-column-attr-';
729
730 Array.prototype.slice.call(el.attributes).forEach(function (item) {
731
732 var attr_name = item.name.replace(needle, '');
733
734 if (item.name.indexOf(needle) !== -1 && attr_name in gridable_column_options) {
735
736 if (item.value !== '') {
737 to_return[attr_name] = item.value;
738 } else if (typeof gridable_column_options[attr_name].default !== 'undefined') {
739 to_return[attr_name] = gridable_column_options[attr_name].default;
740 }
741 }
742 });
743
744 return to_return;
745 }
746
747 /**
748 * Returns the html template of a [row] with `cols_nr` attribute
749 *
750 * @param args
751 * @returns {*}
752 */
753 function getRowTemplate(args) {
754 var rowSh = wp.template("gridable-grider-row"),
755 atts = get_attrs_string('row', args.atts);
756 return rowSh({
757 content: args.content, //wpAutoP(args.content),
758 classes: 'row gridable-mceItem',
759 atts: atts
760 });
761 }
762
763 /**
764 * Returns the html template of a [col] with `size` attribute
765 *
766 * @param args
767 * @returns {*}
768 */
769 function getColTemplate(args) {
770 var atts = get_attrs_string('column', args.atts),
771 colSh = wp.template("gridable-grider-col");
772
773 return colSh({
774 content: args.content,
775 classes: 'col gridable-mceItem ',
776 atts: atts
777 });
778 }
779
780 /**
781 * First get all the attributes and save them from cols_nr="4" into `data-attr-sh-cols_nr="4"`
782 *
783 * @param tag
784 * @param atts
785 * @returns {string}
786 */
787 function get_attrs_string(tag, atts) {
788 var atts_string = '';
789
790 if (typeof atts !== "undefined" && Object.keys(atts).length > 0) {
791 Object.keys(atts).forEach(function (key, index) {
792 if ( key === "style") {
793 atts_string += key + '="' + atts[key] + '" ';
794 } else {
795 atts_string += ' data-sh-' + tag + '-attr-' + key + '="' + atts[key] + '" ';
796 }
797 });
798 }
799
800 return atts_string;
801 }
802
803 /**
804 * Avoid this
805 *
806 * @param content
807 * @returns {*}
808 */
809 function wpAutoP(content) {
810 if (switchEditors && switchEditors.wpautop) {
811 content = switchEditors.wpautop(content);
812 }
813 return content;
814 };
815
816 /**
817 * Strip 'p' and 'br' tags, replace with line breaks.
818 *
819 * Reverses the effect of the WP editor autop functionality.
820 *
821 * @param {string} content Content with `<p>` and `<br>` tags inserted
822 * @return {string}
823 */
824 function removeAutoP(content) {
825 if (switchEditors && switchEditors.pre_wpautop) {
826 content = switchEditors.pre_wpautop(content);
827 }
828 return content;
829 };
830
831 /** Development functions, they can be removed in production **/
832
833 /**
834 * For the moment just switch editors and they will take care
835 */
836 var clearfix = function () {
837 switchEditors.go('content', 'html');
838 switchEditors.go('content', 'tmce');
839 };
840
841 /**
842 * WordPress modal logic
843 */
844 var GridableOptionsModal = (function () {
845
846 var postMediaFrame = wp.media.view.MediaFrame.Post;
847
848 var MediaController = wp.media.controller.State.extend({
849
850 initialize: function (opts) {
851 this.props = new Backbone.Model(opts.sh_atts);
852 this.props.on('change:action', this.refresh, this);
853 },
854
855 refresh: function () {
856 if (this.frame && this.frame.toolbar) {
857 this.frame.toolbar.get().refresh();
858 }
859 },
860
861 insert: function () {
862
863 if (typeof this.frame.options.$shortcode !== "undefined" && typeof this.props.changed !== {}) {
864
865 var $sh = this.frame.options.$shortcode;
866 var tag = this.frame.options.type;
867
868 _.each(this.props.attributes, function (value, key) {
869 $sh.setAttribute('data-sh-' + tag + '-attr-' + key, value);
870 });
871
872 // apply style changes by re-rendering
873 editor.execCommand('gridableRestore');
874 editor.execCommand('gridableRender');
875
876 this.frame.close();
877 }
878 },
879
880 reset: function () {
881 this.props.set('action', 'select');
882 this.props.set('currentShortcode', null);
883 },
884 });
885
886 var Toolbar = wp.media.view.Toolbar.extend({
887 initialize: function () {
888 _.defaults(this.options, {
889 requires: false
890 });
891 // Call 'initialize' directly on the parent class.
892 wp.media.view.Toolbar.prototype.initialize.apply(this, arguments);
893 },
894
895 refresh: function () {
896 var action = this.controller.state().props.get('action');
897 if (this.get('insert')) {
898 this.get('insert').model.set('disabled', action == 'select');
899 }
900 /**
901 * call 'refresh' directly on the parent class
902 */
903 wp.media.view.Toolbar.prototype.refresh.apply(this, arguments);
904 }
905 });
906
907 var editGridableAttributeField = wp.media.View.extend({
908
909 type: 'text',
910
911 config: {},
912
913 tagName: 'div',
914
915 className: 'gridable-attribute-field',
916
917 events: {
918 'input input': 'inputChanged',
919 'input textarea': 'inputChanged',
920 'change select': 'inputChanged',
921 'change input[type="radio"]': 'inputChanged',
922 'change input[type="checkbox"]': 'inputChanged',
923 'change input[type="text"].select2': 'inputChanged'
924 },
925
926 initialize: function () {
927 this.config = this.options.config;
928 this.type = this.options.config.type;
929 },
930
931 render: function () {
932 var tmpl_key = 'gridable-row-option-' + this.type,
933 template = wp.template(tmpl_key);
934
935 config = jQuery.extend({
936 id: 'gridable-ui-' + this.options.key,
937 label: 'Text'
938 }, this.config);
939
940 var template_config = {
941 key: this.options.key,
942 label: this.config.label,
943 value: this.config.default
944 };
945
946 if (typeof this.options.model.attributes[this.options.key] !== "undefined") {
947 template_config.value = this.options.model.attributes[this.options.key];
948 }
949
950 if (this.type === 'checkbox') {
951 template_config.checked = template_config.value === 'true' ? 'checked="checked"' : '';
952 }
953
954 var element = template(template_config);
955
956 this.$el.html(element);
957
958 var self = this;
959
960 // if there is a colorpicker left behind, init it now
961 if ('color' === this.type) {
962 this.$el.find('.colorpicker input:not(.wp-color-picker)').wpColorPicker({
963 hide: false,
964 change: function (event, ui) {
965 // event = standard jQuery event, produced by whichever control was changed.
966 // ui = standard jQuery UI object, with a color member containing a Color.js object
967 jQuery(this).parents('.media-frame-content').css('backgroundColor', ui.color.toString());
968 jQuery(this).val(ui.color.toString());
969 jQuery(this).trigger('input');
970 // change the bg color
971 },
972 clear: function( event ) {
973 // Clear button should make the field transparent
974 self.options.model.attributes[self.options.key] = 'transparent';
975 }
976 });
977 }
978
979 if ('select' === self.type) {
980 var options = [];
981
982 _.each(self.config.options, function (label, value) {
983 var opt_conf = {id: value, text: label};
984 if ( value === template_config.value ) {
985 opt_conf.selected = true;
986 }
987
988 options.push(opt_conf);
989 });
990
991 var $fieldSelect2 = self.$el.find('.selector select').select2({
992 placeholder: self.config.label || 'Search',
993 data: options,
994 // containerCssClass: 'gridable-select2',
995 theme: 'gridable',
996 minimumResultsForSearch: -1
997 });
998 }
999
1000 return self;
1001 },
1002
1003 /**
1004 * Input Changed Update Callback.
1005 *
1006 * If the input field that has changed is for content or a valid attribute,
1007 * then it should update the model. If a callback function is registered
1008 * for this attribute, it should be called as well.
1009 */
1010 inputChanged: function (e) {
1011 var $input = this.$el.find('.value_to_parse');
1012 if (this.type === 'checkbox') {
1013 this.setValue($input.attr('name'), $input[0].checked ? 'true' : 'false');
1014 } else {
1015 this.setValue($input.attr('name'), $input.val());
1016 }
1017 },
1018
1019 getValue: function () {
1020 return this.model.get('value');
1021 },
1022
1023 setValue: function (key, val) {
1024 this.model.set(key, val);
1025 },
1026 });
1027
1028 var Gridable_UI = wp.Backbone.View.extend({
1029
1030 initialize: function (options) {
1031 this.controller = options.controller.state();
1032 //toolbar model looks for controller.state()
1033 this.toolbar_controller = options.controller;
1034 },
1035
1036 createToolbar: function (options) {
1037 toolbarOptions = {
1038 controller: this.toolbar_controller
1039 };
1040 this.toolbar = new Toolbar(toolbarOptions);
1041 this.views.add(this.toolbar);
1042 },
1043
1044 render: function () {
1045
1046 switch (this.controller.frame.options.type) {
1047 case 'row' :
1048 this.renderRowOptions();
1049 break;
1050 case 'column' :
1051 this.renderColumnOptions();
1052 break;
1053 default:
1054 console.log('render what?');
1055 break;
1056 }
1057 },
1058
1059 renderRowOptions: function () {
1060 var atts = this.controller.frame.options.atts,
1061 $modal = this.$el,
1062 values = this.controller.props;
1063
1064 if (typeof gridable_row_options !== "undefined") {
1065
1066 _.each(gridable_row_options, function (config, key) {
1067
1068 if ('cols_nr' === key || 'size' === key) {
1069 return true;
1070 }
1071
1072 if (typeof config.type === 'undefined') {
1073 config.type = 'text';
1074 }
1075
1076 if (typeof config.default === 'undefined') {
1077 config.default = 'Default';
1078 }
1079
1080 // @TODO split this view object in multiple fields like select or ... etc
1081 var view = new editGridableAttributeField({key: key, config: config, model: values});
1082 $modal.append(view.render().el);
1083 });
1084 }
1085 },
1086
1087 renderColumnOptions: function () {
1088 var atts = this.controller.frame.options.atts,
1089 $modal = this.$el,
1090 values = this.controller.props;
1091
1092 if (typeof gridable_column_options !== "undefined") {
1093
1094 _.each(gridable_column_options, function (config, key) {
1095
1096 if ('cols_nr' === key || 'size' === key) {
1097 return true;
1098 }
1099
1100 if (typeof config.type === 'undefined') {
1101 config.type = 'text';
1102 }
1103
1104 if (typeof config.default === 'undefined') {
1105 config.default = 'Default';
1106 }
1107
1108 var view = new editGridableAttributeField({key: key, config: config, model: values});
1109 $modal.append(view.render().el);
1110 });
1111 }
1112 },
1113
1114 });
1115
1116 var mediaFrame = postMediaFrame.extend({
1117
1118 initialize: function () {
1119
1120 postMediaFrame.prototype.initialize.apply(this, arguments);
1121
1122 var id = 'gridable-ui',
1123 title = 'Update ' + this.options.type + ' options',
1124 sh_atts = {},
1125 needle = 'data-sh-column-attr-';
1126
1127 if (this.options.type === 'row') {
1128 needle = 'data-sh-row-attr-';
1129 }
1130
1131 Array.prototype.slice.call(this.options.$shortcode.attributes).forEach(function (item) {
1132 if (item.name.indexOf(needle) !== -1) {
1133 sh_atts[item.name.replace(needle, '')] = item.value;
1134 }
1135 });
1136
1137 var opts = {
1138 id: id,
1139 search: false,
1140 router: false,
1141 toolbar: id + '-toolbar',
1142 menu: 'default',
1143 title: title,
1144 priority: 66,
1145 content: id + '-content-update',
1146 sh_atts: sh_atts
1147 };
1148
1149 this.mediaController = new MediaController(opts);
1150 this.states.add([this.mediaController]);
1151
1152 this.on('content:render:' + id + '-content-update', _.bind(this.contentRender, this, 'gridable-ui', 'update'));
1153 this.on('toolbar:create:gridable-ui-toolbar', this.toolbarCreate, this);
1154 this.on('toolbar:render:gridable-ui-toolbar', this.toolbarRender, this);
1155 this.on('menu:render:default', this.renderShortcodeUIMenu);
1156 },
1157
1158 events: function () {
1159 return _.extend({}, postMediaFrame.prototype.events, {
1160 'click .media-menu-item': 'resetMediaController',
1161 });
1162 },
1163
1164 resetMediaController: function (event) {
1165 if (this.state() && 'undefined' !== typeof this.state().props && this.state().props.get('currentShortcode')) {
1166 //this.mediaController.reset();
1167 this.contentRender('gridable-ui', 'update');
1168 }
1169 },
1170
1171 contentRender: function (id, tab) {
1172
1173 var view = new Gridable_UI({
1174 controller: this,
1175 className: 'clearfix media-sidebar visible ' + id + '-content ' + id + '-content-' + tab
1176 });
1177
1178 this.content.set(view);
1179 },
1180
1181 toolbarRender: function (toolbar) {
1182 },
1183
1184 toolbarCreate: function (toolbar) {
1185 toolbar.view = new Toolbar({
1186 controller: this,
1187 items: {
1188 insert: {
1189 text: 'Update ' + this.options.type,
1190 style: 'primary',
1191 priority: 80,
1192 requires: false,
1193 click: this.insertAction,
1194 }
1195 }
1196 });
1197 },
1198
1199 renderShortcodeUIMenu: function (view) {
1200
1201 // Hide menu if editing.
1202 // @todo - fix this.
1203 // This is a hack.
1204 // I just can't work out how to do it properly...
1205 // if ( view.controller.state().props && view.controller.state().props.get( 'currentShortcode' ) ) {
1206 window.setTimeout(function () {
1207 view.controller.$el.addClass('hide-menu');
1208 });
1209 // }
1210
1211 },
1212
1213 insertAction: function () {
1214 /* Trigger render_destroy */
1215 /*
1216 * Action run before the shortcode overlay is destroyed.
1217 *
1218 * Called as `shortcode-ui.render_destroy`.
1219 *
1220 * @param shortcodeModel (object)
1221 * Reference to the shortcode model used in this overlay.
1222 */
1223 // var hookName = 'shortcode-ui.render_destroy';
1224 // var shortcodeModel = this.controller.state().props.get( 'currentShortcode' );
1225 // wp.shortcake.hooks.doAction( hookName, shortcodeModel );
1226
1227 this.controller.state().insert();
1228 },
1229 });
1230
1231 function open(type, editor, shortcode) {
1232
1233 wp.media.view.MediaFrame.Post = mediaFrame;
1234
1235 var atts = get_shortcode_atts(type, shortcode);
1236
1237 // @TODO process shortcode
1238 var options = {
1239 frame: 'post',
1240 state: 'gridable-ui',
1241 type: type,
1242 atts: atts,
1243 $shortcode: shortcode
1244 };
1245
1246 wp.media.editor.remove(editor);
1247 wp.media.editor.open(editor, options);
1248 }
1249
1250 function get_shortcode_atts(type, el) {
1251
1252 var all_atts = el.attributes,
1253 sh_atts = {};
1254
1255 var needle = 'data-sh-column-attr-';
1256
1257 if (type === 'row') {
1258 needle = 'data-sh-row-attr-';
1259 }
1260
1261 Array.prototype.slice.call(el.attributes).forEach(function (item) {
1262 if (item.name.indexOf(needle) !== -1) {
1263 sh_atts[item.name.replace(needle, '')] = item.value;
1264 }
1265 });
1266
1267 return sh_atts;
1268 }
1269
1270 return {
1271 open: open
1272 };
1273 })();
1274 });
1275 });
1276 })(jQuery, window);