PluginProbe
Gridable / trunk
Gridable vtrunk
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 trunk, at admin/js/gridable.js

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