@themarioga/grid-editor 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/README.md +121 -9
  3. package/UPGRADING.md +55 -0
  4. package/dist/grideditor.css +609 -0
  5. package/dist/grideditor.min.css +1 -1
  6. package/dist/grideditor.min.css.map +1 -1
  7. package/dist/jquery.grideditor.bundle.min.js +2 -2
  8. package/dist/jquery.grideditor.js +1189 -118
  9. package/dist/jquery.grideditor.min.js +1 -1
  10. package/dist/jquery.grideditor.min.js.map +1 -1
  11. package/dist/locales/grideditor.es.js +58 -0
  12. package/dist/locales/grideditor.es.min.js +1 -1
  13. package/dist/locales/grideditor.es.min.js.map +1 -1
  14. package/dist/plugins/grideditor.alignment.js +81 -0
  15. package/dist/plugins/grideditor.alignment.min.js +2 -0
  16. package/dist/plugins/grideditor.alignment.min.js.map +1 -0
  17. package/dist/plugins/grideditor.float.js +56 -0
  18. package/dist/plugins/grideditor.float.min.js +2 -0
  19. package/dist/plugins/grideditor.float.min.js.map +1 -0
  20. package/dist/plugins/grideditor.gutters.js +145 -0
  21. package/dist/plugins/grideditor.gutters.min.js +2 -0
  22. package/dist/plugins/grideditor.gutters.min.js.map +1 -0
  23. package/dist/plugins/grideditor.order.js +206 -0
  24. package/dist/plugins/grideditor.order.min.js +2 -0
  25. package/dist/plugins/grideditor.order.min.js.map +1 -0
  26. package/dist/plugins/grideditor.sections.js +199 -0
  27. package/dist/plugins/grideditor.sections.min.js +2 -0
  28. package/dist/plugins/grideditor.sections.min.js.map +1 -0
  29. package/dist/plugins/grideditor.spacing.js +299 -0
  30. package/dist/plugins/grideditor.spacing.min.js +2 -0
  31. package/dist/plugins/grideditor.spacing.min.js.map +1 -0
  32. package/dist/plugins/grideditor.textalign.js +60 -0
  33. package/dist/plugins/grideditor.textalign.min.js +2 -0
  34. package/dist/plugins/grideditor.textalign.min.js.map +1 -0
  35. package/dist/plugins/grideditor.visibility.js +172 -0
  36. package/dist/plugins/grideditor.visibility.min.js +2 -0
  37. package/dist/plugins/grideditor.visibility.min.js.map +1 -0
  38. package/docs/events.md +32 -2
  39. package/docs/locale-keys.md +88 -1
  40. package/docs/plugins.md +141 -3
  41. package/example/autocols.html +137 -0
  42. package/example/autosave.html +5 -0
  43. package/example/basic.html +5 -0
  44. package/example/breakpoints.html +12 -5
  45. package/example/ckeditor.html +5 -0
  46. package/example/containers.html +4 -0
  47. package/example/elements.html +5 -0
  48. package/example/index.html +38 -1
  49. package/example/locale.html +5 -0
  50. package/example/plugins.html +5 -0
  51. package/example/summernote.html +5 -0
  52. package/example/utilities.html +166 -0
  53. package/example/wrap_content.html +5 -0
  54. package/package.json +1 -1
  55. package/src/js/jquery.grideditor.js +1190 -119
  56. package/src/js/locales/grideditor.es.js +58 -0
  57. package/src/js/plugins/grideditor.alignment.js +81 -0
  58. package/src/js/plugins/grideditor.float.js +56 -0
  59. package/src/js/plugins/grideditor.gutters.js +145 -0
  60. package/src/js/plugins/grideditor.order.js +206 -0
  61. package/src/js/plugins/grideditor.sections.js +199 -0
  62. package/src/js/plugins/grideditor.spacing.js +299 -0
  63. package/src/js/plugins/grideditor.textalign.js +60 -0
  64. package/src/js/plugins/grideditor.visibility.js +172 -0
  65. package/src/less/grideditor.less +299 -0
@@ -28,10 +28,13 @@ var METHODS = {
28
28
  createRow: { value: true },
29
29
  createColumn: { value: true },
30
30
  createElement: { value: true },
31
+ createSection: { value: true },
31
32
  createContainer: { value: true },
32
33
  addTab: { value: true },
33
34
  addAccordionItem: { value: true },
34
35
  setLocale: {},
36
+ getUtility: { value: true },
37
+ setUtility: { value: true },
35
38
  };
36
39
 
37
40
  /**
@@ -45,20 +48,23 @@ var PLACEMENTS = ['appendTo', 'prependTo', 'insertAfter', 'insertBefore'];
45
48
  * runs in: a size written for a tier applies to every wider tier that does not
46
49
  * override it. Every size and offset class grid-editor reads or writes comes
47
50
  * from this table, so adding a tier is a row here and nothing else.
51
+ *
52
+ * `infix` is what Bootstrap's utility classes put between the property and
53
+ * the value - `order-md-2`, `d-none` - and is empty for the smallest tier.
48
54
  */
49
55
  var BREAKPOINTS = [
50
- { key: 'xs', colPrefix: 'col-', offsetPrefix: 'offset-', min: 0, preview: 400, labelKey: 'view.xs' },
51
- { key: 'sm', colPrefix: 'col-sm-', offsetPrefix: 'offset-sm-', min: 576, preview: 576, labelKey: 'view.sm' },
52
- { key: 'md', colPrefix: 'col-md-', offsetPrefix: 'offset-md-', min: 768, preview: 768, labelKey: 'view.md' },
53
- { key: 'lg', colPrefix: 'col-lg-', offsetPrefix: 'offset-lg-', min: 992, preview: 992, labelKey: 'view.lg' },
54
- { key: 'xl', colPrefix: 'col-xl-', offsetPrefix: 'offset-xl-', min: 1200, preview: 1200, labelKey: 'view.xl' },
55
- { key: 'xxl', colPrefix: 'col-xxl-', offsetPrefix: 'offset-xxl-', min: 1400, preview: null, labelKey: 'view.xxl' },
56
+ { key: 'xs', infix: '', colPrefix: 'col-', offsetPrefix: 'offset-', min: 0, preview: 400, labelKey: 'view.xs' },
57
+ { key: 'sm', infix: 'sm', colPrefix: 'col-sm-', offsetPrefix: 'offset-sm-', min: 576, preview: 576, labelKey: 'view.sm' },
58
+ { key: 'md', infix: 'md', colPrefix: 'col-md-', offsetPrefix: 'offset-md-', min: 768, preview: 768, labelKey: 'view.md' },
59
+ { key: 'lg', infix: 'lg', colPrefix: 'col-lg-', offsetPrefix: 'offset-lg-', min: 992, preview: 992, labelKey: 'view.lg' },
60
+ { key: 'xl', infix: 'xl', colPrefix: 'col-xl-', offsetPrefix: 'offset-xl-', min: 1200, preview: 1200, labelKey: 'view.xl' },
61
+ { key: 'xxl', infix: 'xxl', colPrefix: 'col-xxl-', offsetPrefix: 'offset-xxl-', min: 1400, preview: null, labelKey: 'view.xxl' },
56
62
  ];
57
63
 
58
64
  /**
59
65
  * The view that edits every tier at once. It is the default, and the one most
60
- * pages want: a layout that needs no per-device tuning is written once and
61
- * lands on all six prefixes.
66
+ * pages want: a layout that needs no per-device tuning is written once, as the
67
+ * class with no breakpoint, and applies at every size.
62
68
  */
63
69
  var ALL_VIEW = 'all';
64
70
  var ALL_VIEW_LABEL_KEY = 'view.all';
@@ -72,6 +78,31 @@ var LEGACY_VIEW_INDEXES = ['lg', 'sm', 'xs'];
72
78
  var MAX_COL_SIZE = 12;
73
79
  var MAX_COL_OFFSET = 11;
74
80
 
81
+ /**
82
+ * The two sizes that are not a number of units: `equal` is Bootstrap's col,
83
+ * which shares what the row has left with the other equal columns, and
84
+ * `auto` is col-auto, as wide as its content.
85
+ */
86
+ var FLEX_SIZES = ['equal', 'auto'];
87
+
88
+ /** How many columns a row-cols class puts on a line. */
89
+ var ROW_COLS_VALUES = ['1', '2', '3', '4', '5', '6', 'auto'];
90
+
91
+ function rowColsClass(tier, value) {
92
+ return 'row-cols' + (tier.infix ? '-' + tier.infix : '') + '-' + value;
93
+ }
94
+
95
+ function isUnits(size) {
96
+ return typeof size === 'number';
97
+ }
98
+
99
+ /** The class that gives a column `size` at one tier: col-md-4, col-md, col-md-auto. */
100
+ function sizeClass(tier, size) {
101
+ if (size === 'equal') { return tier.infix ? 'col-' + tier.infix : 'col'; }
102
+
103
+ return tier.colPrefix + size;
104
+ }
105
+
75
106
  function breakpoint(key) {
76
107
  for (var i = 0; i < BREAKPOINTS.length; i++) {
77
108
  if (BREAKPOINTS[i].key === key) { return BREAKPOINTS[i]; }
@@ -79,9 +110,12 @@ function breakpoint(key) {
79
110
  return null;
80
111
  }
81
112
 
82
- /** The tiers a view writes to: one, or all six in the all view. */
113
+ /**
114
+ * The tier a view writes to. The all view writes the base class, the one
115
+ * with no breakpoint, and takes the others off.
116
+ */
83
117
  function tiersFor(view) {
84
- if (view === ALL_VIEW) { return BREAKPOINTS.slice(); }
118
+ if (view === ALL_VIEW) { return [BREAKPOINTS[0]]; }
85
119
 
86
120
  var tier = breakpoint(view);
87
121
  return tier ? [tier] : [];
@@ -258,11 +292,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
258
292
  'container_tools' : [], // Host tools on container drawers
259
293
  'tab_tools' : [], // Host tools on tab drawers
260
294
  'accordion_tools' : [], // Host tools on accordion item drawers
261
- 'plugins' : null, // Container plugins to use; null means every one loaded
295
+ 'plugins' : null, // Plugins to use, of any kind; null means every one loaded
296
+ 'row_cols' : true, // A row's "columns per row" field, row-cols-*
297
+ 'utilities' : {}, // Options for the utility plugins, by plugin name
262
298
  'elements' : NESTED_SETTINGS.elements, // Element level controls, below the column
263
299
  'custom_filter' : '',
264
300
  'content_types' : ['tinymce'],
265
- 'valid_col_sizes' : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
301
+ 'valid_col_sizes' : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 'equal', 'auto'],
266
302
  'valid_col_offsets' : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
267
303
  'add_column' : NESTED_SETTINGS.add_column, // The add column tool
268
304
  'layout_modes' : VIEW_KEYS.slice(), // Which views the dropdown offers
@@ -574,54 +610,91 @@ $.fn.gridEditor = function( optionsOrMethod ) {
574
610
  }
575
611
 
576
612
  /**
577
- * Resize a column through the events, writing the tiers the current
578
- * view covers: one in a per-breakpoint view, all six in the all view.
613
+ * What resizing this column to `size` in `view` would write, or null
614
+ * when the budget refuses it or there is nothing to change.
579
615
  *
580
- * The budget is checked per tier and the whole change is refused if
581
- * any tier has no room, so a resize never half lands.
582
- */
583
- /**
584
- * What resizing this column to `size` would write, or null when the
585
- * budget refuses it or there is nothing to change.
616
+ * A breakpoint view writes its own tier. The all view writes the base
617
+ * class and takes the size off every other tier, so one size is what
618
+ * applies everywhere; the plan says what it took off.
586
619
  *
587
620
  * Refused rather than quietly clamped: the offset is something the
588
- * user set, and a tool that rewrites it is a tool that lies. The plan
589
- * covers every tier the view writes, so a resize never half lands.
621
+ * user set, and a tool that rewrites it is a tool that lies. In the
622
+ * all view the base size has to fit every tier's offset, since it is
623
+ * the size at every tier once the others are gone.
590
624
  */
591
- function planSize(col, size) {
592
- var tiers = tiersFor(curView);
593
- var wanted = tiers.map(function(tier) {
594
- return clamp({ size: size, offset: getEffectiveOffset(col, tier) || 0 });
625
+ function planSize(col, size, view) {
626
+ view = view || curView;
627
+
628
+ if (view !== ALL_VIEW) {
629
+ var tier = breakpoint(view);
630
+ var wanted = clamp({ size: size, offset: getEffectiveOffset(col, tier) || 0 });
631
+
632
+ if (wanted.refused || getSize(col, tier) === wanted.size) { return null; }
633
+
634
+ return { writes: [{ tier: tier, size: wanted.size }], cleared: null, size: wanted.size };
635
+ }
636
+
637
+ var refused = BREAKPOINTS.some(function(each) {
638
+ return clamp({ size: size, offset: getEffectiveOffset(col, each) || 0 }).refused;
595
639
  });
640
+ if (refused) { return null; }
641
+
642
+ var base = BREAKPOINTS[0];
643
+ var writes = [];
644
+ var cleared = [];
596
645
 
597
- if (wanted.some(function(request) { return request.refused; })) { return null; }
646
+ BREAKPOINTS.slice(1).forEach(function(each) {
647
+ var own = getSize(col, each);
648
+ if (own === null) { return; }
598
649
 
599
- var unchanged = tiers.every(function(tier, i) {
600
- return getSize(col, tier) === wanted[i].size;
650
+ writes.push({ tier: each, size: null });
651
+ cleared.push({ breakpoint: each.key, value: own });
601
652
  });
602
- if (unchanged) { return null; }
603
653
 
604
- return { tiers: tiers, sizes: wanted, size: wanted[0].size };
654
+ if (getSize(col, base) !== size) { writes.unshift({ tier: base, size: size }); }
655
+
656
+ return writes.length ? { writes: writes, cleared: cleared, size: size } : null;
605
657
  }
606
658
 
607
659
  function writeSize(col, plan) {
608
- plan.tiers.forEach(function(tier, i) { setSize(col, tier, plan.sizes[i].size); });
660
+ plan.writes.forEach(function(write) { setSize(col, write.tier, write.size); });
609
661
  stripPixelWidths(col);
610
662
  }
611
663
 
612
- /** Resize a column from a tool, announcing it either side. */
613
- function resizeColumn(col, size, source) {
664
+ /** The all view's payloads say what the write took off the breakpoints. */
665
+ function withCleared(extra, cleared) {
666
+ if (cleared) { extra.cleared = cleared; }
667
+ return extra;
668
+ }
669
+
670
+ /**
671
+ * Resize a column, announcing it either side. `size` is units,
672
+ * `equal` or `auto`; `view` is the current one unless the size field
673
+ * of a plugin's panel says otherwise.
674
+ */
675
+ function resizeColumn(col, size, source, view) {
676
+ view = view || curView;
677
+
614
678
  var from = currentSize(col);
615
- var plan = planSize(col, size);
679
+ var plan = planSize(col, size, view);
616
680
 
617
681
  if (!plan) { return false; }
618
682
 
619
683
  return operate(function() {
620
- var payload = payloadFor('column', col, { source: source, from: from, to: plan.size });
684
+ var payload = payloadFor('column', col, withCleared({
685
+ source: source,
686
+ breakpoint: view,
687
+ from: from,
688
+ to: plan.size,
689
+ }, plan.cleared));
621
690
 
622
- if (!emit('before-resize', payload)) { return false; }
691
+ if (!emit('before-resize', payload)) {
692
+ refreshUtilities(col);
693
+ return false;
694
+ }
623
695
 
624
696
  writeSize(col, plan);
697
+ refreshUtilities(col);
625
698
  emit('after-resize', payload);
626
699
 
627
700
  return true;
@@ -631,41 +704,49 @@ $.fn.gridEditor = function( optionsOrMethod ) {
631
704
  /**
632
705
  * Indent a column, shrinking it when the budget needs it: the offset
633
706
  * is what the user asked for, so it is the one that gets its way.
707
+ *
708
+ * Like a size, an offset is written to the tier being edited, or in
709
+ * the all view as the base class with every other tier's taken off.
634
710
  */
635
711
  function indentColumn(col, offset, source) {
636
- var tiers = tiersFor(curView);
712
+ var everywhere = curView === ALL_VIEW;
713
+ var tier = everywhere ? BREAKPOINTS[0] : breakpoint(curView);
637
714
  var from = currentOffset(col);
638
715
 
639
716
  offset = Math.min(Math.max(offset, 0), MAX_COL_OFFSET);
640
717
 
641
- var unchanged = tiers.every(function(tier) {
642
- return (getOffset(col, tier) || 0) === offset;
643
- });
644
- if (unchanged) { return false; }
718
+ var others = everywhere ? BREAKPOINTS.slice(1).filter(function(each) {
719
+ return getOffset(col, each) !== null;
720
+ }) : [];
721
+
722
+ if ((getOffset(col, tier) || 0) === offset && !others.length) { return false; }
645
723
 
646
724
  return operate(function() {
647
- var payload = payloadFor('column', col, {
725
+ var payload = payloadFor('column', col, withCleared({
648
726
  source: source,
649
727
  from: from,
650
728
  to: offset,
651
- });
729
+ }, everywhere ? others.map(function(each) {
730
+ return { breakpoint: each.key, value: getOffset(col, each) };
731
+ }) : null));
652
732
 
653
733
  if (!emit('before-indent', payload)) { return false; }
654
734
 
655
- tiers.forEach(function(tier) {
656
- var was = getEffectiveSize(col, tier);
657
- var wanted = clamp({ size: was, offset: offset, leading: 'offset' });
735
+ setOffset(col, tier, offset);
736
+ others.forEach(function(each) { setOffset(col, each, 0); });
658
737
 
659
- setOffset(col, tier, wanted.offset);
738
+ // Wherever the column's size and the new offset no longer fit
739
+ // - one tier, or every tier in the all view - the size gives
740
+ // way, at that tier only. An equal or auto column fits anywhere.
741
+ (everywhere ? BREAKPOINTS : [tier]).forEach(function(each) {
742
+ var was = getEffectiveSize(col, each);
743
+ if (!isUnits(was)) { return; }
660
744
 
661
- // Only where the budget actually forced the column to give
662
- // way; otherwise an indent would write size classes for
663
- // tiers nobody asked it to touch
664
- if (wanted.size !== null && wanted.size !== was) {
665
- setSize(col, tier, wanted.size);
666
- }
745
+ var wanted = clamp({ size: was, offset: offset, leading: 'offset' });
746
+ if (wanted.size !== was) { setSize(col, each, wanted.size); }
667
747
  });
668
748
 
749
+ refreshUtilities(col);
669
750
  emit('after-indent', payload);
670
751
 
671
752
  return true;
@@ -748,6 +829,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
748
829
  // when their editor is ready, and the drawers go back in.
749
830
  canvas.on('ge-rte-ready', '.ge-content', function() {
750
831
  plugins('onContentReady', $(this));
832
+ refreshPreviews($(this));
751
833
  });
752
834
  }
753
835
 
@@ -763,17 +845,17 @@ $.fn.gridEditor = function( optionsOrMethod ) {
763
845
  addRowGroup = $('<div class="ge-addRowGroup btn-group" />').appendTo(wrapper);
764
846
  addContainerGroup = $('<div class="ge-addContainerGroup btn-group" />');
765
847
  $.each(settings.new_row_layouts, function(j, layout) {
848
+ var grouped = !Array.isArray(layout);
766
849
  var btn = $('<a class="btn btn-sm btn-primary" />')
767
- .attr('title', t('row.add', { layout: layout.join('-') }))
850
+ .attr('title', grouped
851
+ ? t('row.add_row_cols', { columns: layout.columns, counts: rowColsText(layout.row_cols) })
852
+ : t('row.add', { layout: layout.join('-') }))
768
853
  // What this button makes, in the markup rather than in
769
854
  // jQuery data: a drag works on a clone of it
770
855
  .attr('data-ge-toolbar', 'row')
771
- .attr('data-ge-layout', layout.join(','))
856
+ .attr('data-ge-layout', grouped ? JSON.stringify(layout) : layout.join(','))
772
857
  .on('click', function() {
773
- var row = createRow();
774
- layout.forEach(function(i) {
775
- createColumn(i).appendTo(row);
776
- });
858
+ var row = rowFromLayoutValue(layout);
777
859
 
778
860
  var added = addNode('row', row, function() {
779
861
  row.appendTo(canvas);
@@ -788,10 +870,14 @@ $.fn.gridEditor = function( optionsOrMethod ) {
788
870
 
789
871
  btn.append('<i class="bi bi-plus"></i>');
790
872
 
791
- var layoutName = layout.join(' - ');
873
+ // A row of columns shares out the icon as they share the row;
874
+ // a row with row-cols draws one line of its widest count
875
+ var sizes = grouped ? rowColsIcon(layout) : layout;
792
876
  var icon = '<div class="row ge-row-icon">';
793
- layout.forEach(function(i) {
794
- icon += '<div class="column col-' + i + '"/>';
877
+ sizes.forEach(function(size) {
878
+ // Closed explicitly: jQuery 4 no longer expands <div/>, and
879
+ // each column would be parsed inside the one before
880
+ icon += '<div class="column ' + sizeClass(BREAKPOINTS[0], size) + '"></div>';
795
881
  });
796
882
  icon += '</div>';
797
883
  btn.append(icon);
@@ -823,6 +909,29 @@ $.fn.gridEditor = function( optionsOrMethod ) {
823
909
  ;
824
910
  });
825
911
 
912
+ // A feature plugin's own buttons, beside the containers': what
913
+ // one makes goes onto the canvas, where the plugin says it belongs
914
+ $.each(FEATURES, function(name, feature) {
915
+ (feature.toolbar || []).forEach(function(item, index) {
916
+ $('<a class="btn btn-sm btn-primary ge-add-container ge-add-feature" />')
917
+ .attr('title', t(item.labelKey))
918
+ .attr('data-ge-toolbar', 'feature')
919
+ .attr('data-ge-feature', name)
920
+ .attr('data-ge-item', index)
921
+ .append('<i class="bi bi-plus"></i>')
922
+ .append($('<span />').text(t(item.labelKey)))
923
+ .on('click', function() {
924
+ var made = item.create();
925
+
926
+ addNode(item.kind, made, function() {
927
+ made.appendTo(canvas);
928
+ }, { parent: canvas, source: 'tool' });
929
+ })
930
+ .appendTo(addContainerGroup)
931
+ ;
932
+ });
933
+ });
934
+
826
935
  // Buttons on right
827
936
  layoutDropdown = $('<div class="dropdown pull-right ge-layout-mode">' +
828
937
  '<button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-bs-toggle="dropdown"></button>' +
@@ -998,14 +1107,14 @@ $.fn.gridEditor = function( optionsOrMethod ) {
998
1107
 
999
1108
  if (!under) { return null; }
1000
1109
 
1001
- var region = $(under).closest('.column, .ge-canvas');
1110
+ var region = $(under).closest(['.column', '.ge-canvas'].concat(pluginHooks('regions')).join(', '));
1002
1111
  if (!region.length || (region[0] !== canvas[0] && !canvas[0].contains(region[0]))) {
1003
1112
  return null;
1004
1113
  }
1005
1114
 
1006
1115
  var before = null;
1007
1116
 
1008
- region.children('.row, .ge-content, [data-ge-container]').each(function() {
1117
+ region.children(blockSelector()).each(function() {
1009
1118
  if (before) { return; }
1010
1119
 
1011
1120
  var box = this.getBoundingClientRect();
@@ -1015,6 +1124,30 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1015
1124
  return { region: region, before: before };
1016
1125
  }
1017
1126
 
1127
+ /**
1128
+ * A feature's toolbar button dropped on the canvas. A region that will
1129
+ * not have what it makes - a section dropped on a column - gives way
1130
+ * to the canvas, just after the top level block the pointer was in.
1131
+ */
1132
+ function insertFeatureFromToolbar(button, where) {
1133
+ var item = FEATURES[button.attr('data-ge-feature')].toolbar[parseInt(button.attr('data-ge-item'), 10)];
1134
+ var made = item.create();
1135
+
1136
+ if (!acceptsBlock(where.region, made)) {
1137
+ var top = where.region.parentsUntil(canvas).addBack().first();
1138
+ where = { region: canvas, before: top.length ? top.next() : null };
1139
+ if (where.before && !where.before.length) { where.before = null; }
1140
+ }
1141
+
1142
+ return addNode(item.kind, made, function() {
1143
+ if (where.before) {
1144
+ made.insertBefore(where.before);
1145
+ } else {
1146
+ made.appendTo(where.region);
1147
+ }
1148
+ }, { parent: where.region, source: 'dragdrop' });
1149
+ }
1150
+
1018
1151
  /** A line where the block would go, following the pointer. */
1019
1152
  function showDropMarker(pageX, pageY) {
1020
1153
  var where = dropPlaceAt(pageX, pageY);
@@ -1041,6 +1174,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1041
1174
  * the pointer left it.
1042
1175
  */
1043
1176
  function insertFromToolbar(button, where) {
1177
+ if (button.attr('data-ge-toolbar') === 'feature') {
1178
+ return insertFeatureFromToolbar(button, where);
1179
+ }
1180
+
1044
1181
  var container = button.attr('data-ge-toolbar') === 'container';
1045
1182
  var type = button.attr('data-ge-container-type');
1046
1183
  var made = container
@@ -1065,16 +1202,94 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1065
1202
  }, { parent: where.region, source: 'dragdrop' });
1066
1203
  }
1067
1204
 
1205
+ /**
1206
+ * The column width as a utility family, so the Responsive section of
1207
+ * a column's panel has a field for it: every size, units or not, in
1208
+ * the view being edited. A write is a resize, through the resize
1209
+ * events, not a utility change.
1210
+ */
1211
+ function widthFamily() {
1212
+ var values = [];
1213
+ for (var units = 1; units <= MAX_COL_SIZE; units++) { values.push(String(units)); }
1214
+
1215
+ return {
1216
+ name: 'col',
1217
+ values: values.concat(FLEX_SIZES),
1218
+ appliesTo: ['column'],
1219
+ labelKey: 'utility.col_width',
1220
+ className: function(key, value) { return sizeClass(breakpoint(key), parseSize(value)); },
1221
+ choices: function() { return settings.valid_col_sizes.map(String); },
1222
+ label: function(value) {
1223
+ if (value === 'equal') { return t('utility.col_equal'); }
1224
+ if (value === 'auto') { return t('utility.col_auto'); }
1225
+
1226
+ return value;
1227
+ },
1228
+ write: function(col, value, view, source) {
1229
+ return resizeColumn(col, value === null ? null : parseSize(value), source, view);
1230
+ },
1231
+ // With no size of its own, a column in a row with row-cols
1232
+ // takes its share from the row, which the field says
1233
+ blank: function(col, view) {
1234
+ var winner = view === ALL_VIEW ? null : rowColsWinner(col, breakpoint(view));
1235
+ return winner ? t('utility.col_from_row', { count: rowColsLabel(winner.value) }) : null;
1236
+ },
1237
+ };
1238
+ }
1239
+
1240
+ /** A size as markup or a select carries it: '4' is 4, 'equal' and 'auto' are themselves. */
1241
+ function parseSize(size) {
1242
+ return /^\d+$/.test(String(size)) ? parseInt(size, 10) : size;
1243
+ }
1244
+
1245
+ /** A row from a toolbar button's data-ge-layout: sizes, or a row-cols layout as JSON. */
1068
1246
  function rowFromLayout(layout) {
1247
+ if (/^\s*\{/.test(layout || '')) { return rowFromLayoutValue(JSON.parse(layout)); }
1248
+
1249
+ return rowFromLayoutValue((layout || '').split(',').filter(function(size) {
1250
+ return size !== '';
1251
+ }).map(parseSize));
1252
+ }
1253
+
1254
+ /**
1255
+ * A row from a layout: an array of sizes, or { row_cols, columns } -
1256
+ * row-cols per breakpoint, and that many columns sized by the row.
1257
+ */
1258
+ function rowFromLayoutValue(layout) {
1069
1259
  var row = createRow();
1070
1260
 
1071
- (layout || '').split(',').forEach(function(size) {
1072
- if (size !== '') { createColumn(parseInt(size, 10)).appendTo(row); }
1261
+ if (Array.isArray(layout)) {
1262
+ layout.forEach(function(size) { createColumn(size).appendTo(row); });
1263
+ return row;
1264
+ }
1265
+
1266
+ $.each(layout.row_cols || {}, function(key, value) {
1267
+ var tier = breakpoint(key);
1268
+ if (tier && ROW_COLS_VALUES.indexOf(String(value)) !== -1) { row.addClass(rowColsClass(tier, value)); }
1073
1269
  });
1074
1270
 
1271
+ for (var i = 0; i < (layout.columns || 0); i++) { createColumn(null).appendTo(row); }
1272
+
1075
1273
  return row;
1076
1274
  }
1077
1275
 
1276
+ /** "1, md: 3": a row-cols layout's counts, for its button's title. */
1277
+ function rowColsText(counts) {
1278
+ return BREAKPOINTS.filter(function(tier) { return counts && counts[tier.key] !== undefined; })
1279
+ .map(function(tier) { return (tier.infix ? tier.key + ': ' : '') + counts[tier.key]; })
1280
+ .join(', ');
1281
+ }
1282
+
1283
+ /** The icon of a row-cols layout: one line of equal columns, as many as its widest count. */
1284
+ function rowColsIcon(layout) {
1285
+ var widest = 1;
1286
+ $.each(layout.row_cols || {}, function(key, value) {
1287
+ if (value !== 'auto') { widest = Math.max(widest, parseInt(value, 10) || 1); }
1288
+ });
1289
+
1290
+ return Array.apply(null, Array(Math.min(widest, layout.columns || widest))).map(function() { return 'equal'; });
1291
+ }
1292
+
1078
1293
  function onScroll(e) {
1079
1294
  var $window = $(window);
1080
1295
 
@@ -1136,6 +1351,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1136
1351
  makeSortable();
1137
1352
  makeResizable();
1138
1353
  switchLayout(curView);
1354
+ refreshPreviews(canvas);
1139
1355
  }
1140
1356
 
1141
1357
  function deinit() {
@@ -1156,6 +1372,11 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1156
1372
  hideDropMarker();
1157
1373
  canvas.find('.ge-tools-drawer').remove();
1158
1374
  plugins('onDeinit');
1375
+ // After the rich text editors have let go of their content areas:
1376
+ // one that rebuilt its area's DOM brought the preview styles back
1377
+ // with it, and the attribute recording them came back too
1378
+ clearPreviews(canvas);
1379
+ canvas.find('[data-ge-row-cols]').removeAttr('data-ge-row-cols');
1159
1380
  unmarkContainers();
1160
1381
  removeSortable();
1161
1382
  removeResizable();
@@ -1224,6 +1445,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1224
1445
  */
1225
1446
  function loadPlugins() {
1226
1447
  var api = pluginApi();
1448
+
1449
+ registerFamily('grid', {}, widthFamily());
1450
+ if (settings.row_cols !== false) { registerFamily('grid', {}, rowColsFamily()); }
1227
1451
  var wanted = function(name) {
1228
1452
  return !settings.plugins || settings.plugins.indexOf(name) !== -1;
1229
1453
  };
@@ -1236,6 +1460,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1236
1460
  if (wanted(name)) { FEATURES[name] = factory(api); }
1237
1461
  });
1238
1462
 
1463
+ $.each($.fn.gridEditor.utilities, function(name, factory) {
1464
+ if (!wanted(name)) { return; }
1465
+
1466
+ UTILITIES[name] = factory(api);
1467
+ (UTILITIES[name].families || []).forEach(function(family) {
1468
+ registerFamily(name, UTILITIES[name], family);
1469
+ });
1470
+ });
1471
+
1239
1472
  $.each(FEATURES, function(name, feature) {
1240
1473
  $.each(feature.methods || {}, function(method, implementation) {
1241
1474
  featureMethods[method] = implementation;
@@ -1243,7 +1476,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1243
1476
  });
1244
1477
 
1245
1478
  (settings.plugins || []).forEach(function(name) {
1246
- if (CONTAINERS[name] || FEATURES[name]) { return; }
1479
+ if (CONTAINERS[name] || FEATURES[name] || UTILITIES[name]) { return; }
1247
1480
 
1248
1481
  warnOnceHere('plugin:' + name, 'the "' + name + '" plugin is not loaded: ' +
1249
1482
  'include dist/plugins/grideditor.' + name + '.js after the editor');
@@ -1253,7 +1486,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1253
1486
  /**
1254
1487
  * A hook every loaded plugin may have. Containers first, since a
1255
1488
  * feature that looks at the canvas - elements, say - wants the
1256
- * containers already marked.
1489
+ * containers already marked, and utilities last, since they decorate
1490
+ * nodes the other two may have just made.
1257
1491
  */
1258
1492
  function plugins(hook, argument) {
1259
1493
  $.each(CONTAINERS, function(type, definition) {
@@ -1262,6 +1496,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1262
1496
  $.each(FEATURES, function(name, feature) {
1263
1497
  if (feature[hook]) { feature[hook](argument); }
1264
1498
  });
1499
+ $.each(UTILITIES, function(name, utility) {
1500
+ if (utility[hook]) { utility[hook](argument); }
1501
+ });
1265
1502
  }
1266
1503
 
1267
1504
  /** What a plugin is handed. See docs/plugins.md. */
@@ -1287,9 +1524,531 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1287
1524
  emit: emit,
1288
1525
  payloadFor: payloadFor,
1289
1526
  operate: operate,
1527
+ kindOf: kindOf,
1528
+ view: getView,
1529
+ viewTiers: function() {
1530
+ return tiersFor(curView).map(function(tier) { return tier.key; });
1531
+ },
1532
+ breakpoints: BREAKPOINTS.map(function(tier) { return tier.key; }),
1533
+ getUtility: getUtility,
1534
+ setUtility: setUtility,
1535
+ utilityField: utilityField,
1536
+ bareStyle: bareStyle,
1537
+ rowFromLayout: rowFromLayoutValue,
1538
+ };
1539
+ }
1540
+
1541
+ /* --------------------------------------------------------------
1542
+ * Utilities: Bootstrap's responsive utility classes.
1543
+ *
1544
+ * A utility plugin declares families - order, d, justify-content -
1545
+ * and this is everything that reads or writes one. Every family is
1546
+ * spelled the way Bootstrap spells them all, {prefix}-{infix}-{value}
1547
+ * with no infix at the smallest tier, and cascades the way the size
1548
+ * classes do: a tier that says nothing takes the nearest smaller one.
1549
+ *
1550
+ * The canvas cannot show a breakpoint's utilities by itself. A
1551
+ * per-breakpoint view narrows the canvas, not the viewport, and
1552
+ * Bootstrap's utilities answer to the viewport with !important. So
1553
+ * each node gets what its classes mean at the view being edited,
1554
+ * as inline !important styles, recorded in an attribute so they come
1555
+ * off again - exactly those, leaving the host's own style alone.
1556
+ * -------------------------------------------------------------- */
1557
+
1558
+ var FAMILIES = {}; // Every family the utility plugins declare, by name
1559
+ var UTILITY_NODES = '.row, .column, .ge-element, [data-ge-container]';
1560
+ var PREVIEW_ATTR = 'data-ge-preview';
1561
+ var utilitiesOpen = false; // Whether the panels show their Responsive section unfolded
1562
+
1563
+ function registerFamily(pluginName, utility, family) {
1564
+ var name = family.name || family.prefix;
1565
+
1566
+ if (FAMILIES[name]) {
1567
+ warn('the "' + pluginName + '" plugin declares the utility "' + name +
1568
+ '", which the "' + FAMILIES[name].plugin + '" plugin already declared: ignored');
1569
+ return;
1570
+ }
1571
+
1572
+ FAMILIES[name] = {
1573
+ plugin: pluginName,
1574
+ family: $.extend({}, family, {
1575
+ name: name,
1576
+ values: family.values.map(String),
1577
+ appliesTo: family.appliesTo || utility.appliesTo || ['row', 'column'],
1578
+ }),
1290
1579
  };
1291
1580
  }
1292
1581
 
1582
+ /** The family a caller named, or null and a warning when no plugin declares it. */
1583
+ function familyNamed(name, method) {
1584
+ if (FAMILIES[name]) { return FAMILIES[name].family; }
1585
+
1586
+ warnOnceHere('utility:' + name, method + '(' + JSON.stringify(name) + '): no loaded ' +
1587
+ 'plugin declares that utility');
1588
+ return null;
1589
+ }
1590
+
1591
+ /**
1592
+ * Whether a family applies to a kind of node. A container answers to
1593
+ * `container` and to its own type, so a plugin can take them all or
1594
+ * name the ones it means.
1595
+ */
1596
+ function appliesTo(family, kind) {
1597
+ if (family.appliesTo.indexOf(kind) !== -1) { return true; }
1598
+
1599
+ return !!CONTAINERS[kind] && family.appliesTo.indexOf('container') !== -1;
1600
+ }
1601
+
1602
+ function familiesFor(kind) {
1603
+ return $.map(FAMILIES, function(entry) {
1604
+ return appliesTo(entry.family, kind) ? entry.family : null;
1605
+ });
1606
+ }
1607
+
1608
+ function utilityClass(family, tier, value) {
1609
+ if (family.className) { return family.className(tier.key, value); }
1610
+
1611
+ return family.prefix + (tier.infix ? '-' + tier.infix : '') + '-' + value;
1612
+ }
1613
+
1614
+ /** The value a node carries a class for at exactly this tier, or null. */
1615
+ function ownUtility(node, family, tier) {
1616
+ var classes = (node.attr('class') || '').split(/\s+/);
1617
+
1618
+ for (var i = 0; i < family.values.length; i++) {
1619
+ if (classes.indexOf(utilityClass(family, tier, family.values[i])) !== -1) {
1620
+ return family.values[i];
1621
+ }
1622
+ }
1623
+
1624
+ return null;
1625
+ }
1626
+
1627
+ /**
1628
+ * The tier below this one that decides the value here, and what it
1629
+ * says, when this tier says nothing itself. Null when nothing below
1630
+ * says anything either.
1631
+ */
1632
+ function inheritedUtility(node, family, tier) {
1633
+ for (var i = BREAKPOINTS.indexOf(tier) - 1; i >= 0; i--) {
1634
+ var value = ownUtility(node, family, BREAKPOINTS[i]);
1635
+ if (value !== null) { return { tier: BREAKPOINTS[i], value: value }; }
1636
+ }
1637
+
1638
+ return null;
1639
+ }
1640
+
1641
+ /** What applies at a tier: its own class, or what it inherits. */
1642
+ function effectiveUtility(node, family, tier) {
1643
+ var own = ownUtility(node, family, tier);
1644
+ if (own !== null) { return own; }
1645
+
1646
+ var inherited = inheritedUtility(node, family, tier);
1647
+ return inherited ? inherited.value : null;
1648
+ }
1649
+
1650
+ /** Every tier the node carries a class of this family for. */
1651
+ function utilityTiers(node, family) {
1652
+ return BREAKPOINTS.map(function(tier) {
1653
+ return { tier: tier, value: ownUtility(node, family, tier) };
1654
+ }).filter(function(entry) { return entry.value !== null; });
1655
+ }
1656
+
1657
+ function writeUtility(node, family, tier, value) {
1658
+ family.values.forEach(function(candidate) {
1659
+ node.removeClass(utilityClass(family, tier, candidate));
1660
+ });
1661
+
1662
+ if (value !== null) { node.addClass(utilityClass(family, tier, value)); }
1663
+ if (!node.attr('class')) { node.removeAttr('class'); }
1664
+ }
1665
+
1666
+ /**
1667
+ * What a view reads. A breakpoint reads what applies there. The all
1668
+ * view reads the class with no infix, since that is what it writes:
1669
+ * the one class that means "the same at every size".
1670
+ */
1671
+ function readUtility(node, family, view) {
1672
+ return view === ALL_VIEW
1673
+ ? ownUtility(node, family, BREAKPOINTS[0])
1674
+ : effectiveUtility(node, family, breakpoint(view));
1675
+ }
1676
+
1677
+ /**
1678
+ * What writing `value` in a view comes to, or null when it changes
1679
+ * nothing. A breakpoint writes its own tier and nothing else. The all
1680
+ * view writes the class with no infix and clears the family from
1681
+ * every other tier, and says what it cleared: choosing one value for
1682
+ * every size is choosing it over what the sizes said.
1683
+ */
1684
+ function planUtility(node, family, view, value) {
1685
+ if (view !== ALL_VIEW) {
1686
+ var tier = breakpoint(view);
1687
+ if (ownUtility(node, family, tier) === value) { return null; }
1688
+
1689
+ return { writes: [{ tier: tier, value: value }], cleared: [] };
1690
+ }
1691
+
1692
+ var writes = [];
1693
+ var cleared = [];
1694
+
1695
+ utilityTiers(node, family).forEach(function(entry) {
1696
+ if (entry.tier === BREAKPOINTS[0]) { return; }
1697
+
1698
+ writes.push({ tier: entry.tier, value: null });
1699
+ cleared.push({ breakpoint: entry.tier.key, value: entry.value });
1700
+ });
1701
+
1702
+ if (ownUtility(node, family, BREAKPOINTS[0]) !== value) {
1703
+ writes.unshift({ tier: BREAKPOINTS[0], value: value });
1704
+ }
1705
+
1706
+ return writes.length ? { writes: writes, cleared: cleared } : null;
1707
+ }
1708
+
1709
+ /** getUtility(node, family, view?): the value that applies, or null. */
1710
+ function getUtility(node, name, view) {
1711
+ node = $(node).first();
1712
+
1713
+ var family = familyNamed(name, 'getUtility');
1714
+ if (!family || !node.length) { return null; }
1715
+
1716
+ var key = view === undefined ? curView : viewKey(view);
1717
+ if (key === null) {
1718
+ warn('getUtility(' + JSON.stringify(view) + '): no such layout mode');
1719
+ return null;
1720
+ }
1721
+
1722
+ return readUtility(node, family, key);
1723
+ }
1724
+
1725
+ /**
1726
+ * setUtility(node, family, value, view?): write a value through the
1727
+ * events. Null is "inherit". The last argument is a view key, or
1728
+ * { view, source } from a plugin that wants its tool named in the
1729
+ * payload. False when a handler canceled or nothing changed.
1730
+ */
1731
+ function setUtility(node, name, value, options) {
1732
+ options = typeof options == 'string' ? { view: options } : (options || {});
1733
+ node = $(node).first();
1734
+
1735
+ var family = familyNamed(name, 'setUtility');
1736
+ if (!family || !node.length) { return false; }
1737
+
1738
+ var view = options.view === undefined ? curView : viewKey(options.view);
1739
+ if (view === null) {
1740
+ warn('setUtility(' + JSON.stringify(options.view) + '): no such layout mode');
1741
+ return false;
1742
+ }
1743
+
1744
+ value = value === null || value === undefined || value === '' ? null : String(value);
1745
+ if (value !== null && family.values.indexOf(value) === -1) {
1746
+ warn('setUtility: ' + JSON.stringify(value) + ' is not a value of "' + name +
1747
+ '", which takes ' + JSON.stringify(family.values));
1748
+ return false;
1749
+ }
1750
+
1751
+ var kind = kindOf(node);
1752
+ if (!appliesTo(family, kind)) {
1753
+ warn('setUtility: "' + name + '" does not apply to a ' + kind);
1754
+ return false;
1755
+ }
1756
+
1757
+ // A family whose writes are some other operation - the column
1758
+ // width, which is a resize - does them its own way
1759
+ if (family.write) { return family.write(node, value, view, options.source || 'api'); }
1760
+
1761
+ var plan = planUtility(node, family, view, value);
1762
+ if (!plan) { return false; }
1763
+
1764
+ return operate(function() {
1765
+ var payload = payloadFor(kind, node, {
1766
+ family: name,
1767
+ breakpoint: view,
1768
+ tiers: plan.writes.map(function(write) { return write.tier.key; }),
1769
+ from: readUtility(node, family, view),
1770
+ to: value,
1771
+ cleared: plan.cleared,
1772
+ source: options.source || 'api',
1773
+ });
1774
+
1775
+ if (!emit('before-utility', payload)) {
1776
+ // The panel already shows the value that was refused
1777
+ refreshUtilities(node);
1778
+ return false;
1779
+ }
1780
+
1781
+ plan.writes.forEach(function(write) {
1782
+ writeUtility(node, family, write.tier, write.value);
1783
+ });
1784
+ refreshUtilities(node);
1785
+ emit('after-utility', payload);
1786
+
1787
+ return true;
1788
+ });
1789
+ }
1790
+
1791
+ /**
1792
+ * ge.bareStyle(node, family, property): what a css property comes to
1793
+ * on a node with none of the family's classes, at any breakpoint. The
1794
+ * answer a preview needs for "no class applies here" when that is not
1795
+ * a constant: text-align inherits from the parent, and the host's own
1796
+ * css may float an element. The browser is the only one who knows, so
1797
+ * the classes come off for the moment it takes to ask.
1798
+ */
1799
+ function bareStyle(node, name, property) {
1800
+ var family = familyNamed(name, 'bareStyle');
1801
+ node = $(node).first();
1802
+ if (!family || !node.length) { return null; }
1803
+
1804
+ var original = node.attr('class');
1805
+ BREAKPOINTS.forEach(function(tier) { writeUtility(node, family, tier, null); });
1806
+
1807
+ var value = getComputedStyle(node[0]).getPropertyValue(property);
1808
+
1809
+ if (original === undefined) { node.removeAttr('class'); } else { node.attr('class', original); }
1810
+
1811
+ return value;
1812
+ }
1813
+
1814
+ /** Bring a node's panel and preview up to date with its classes. */
1815
+ function refreshUtilities(node) {
1816
+ var details = node.children('.ge-tools-drawer').children('.ge-details');
1817
+
1818
+ details.children('.ge-classes').val(hostClasses(node).join(' '));
1819
+ details.children('.ge-utilities').each(function() { renderUtilities($(this)); });
1820
+ refreshPreviews(node);
1821
+ }
1822
+
1823
+ /* Preview */
1824
+
1825
+ function utilityNodes(scope) {
1826
+ return scope.find(UTILITY_NODES).addBack(UTILITY_NODES);
1827
+ }
1828
+
1829
+ /**
1830
+ * Put what each node's utilities mean at the view being edited on the
1831
+ * node itself. Only nodes that carry a class of a family get anything,
1832
+ * and they get the family's answer for the view even when that is
1833
+ * "nothing": a wider tier's class is still live in a wide window, and
1834
+ * has to be overruled.
1835
+ *
1836
+ * The all view gets nothing. Its canvas is not narrowed, every tier is
1837
+ * live, and what Bootstrap shows is the truth.
1838
+ */
1839
+ function refreshPreviews(scope) {
1840
+ clearPreviews(scope);
1841
+
1842
+ if (curView !== ALL_VIEW) { previewTier(scope, breakpoint(curView)); }
1843
+
1844
+ markRowCols(scope);
1845
+
1846
+ // Whatever a plugin marks the canvas with to show its utilities
1847
+ // goes stale at the same moments the preview does
1848
+ plugins('onRefresh', scope);
1849
+ }
1850
+
1851
+ function previewTier(scope, tier) {
1852
+ utilityNodes(scope).each(function() {
1853
+ var node = $(this);
1854
+ var kind = kindOf(node);
1855
+ var styles = {};
1856
+
1857
+ familiesFor(kind).forEach(function(family) {
1858
+ if (!family.preview || !utilityTiers(node, family).length) { return; }
1859
+
1860
+ $.extend(styles, family.preview(effectiveUtility(node, family, tier), node, kind));
1861
+ });
1862
+
1863
+ if (kind === 'column') { $.extend(styles, rowColsPreview(node, tier)); }
1864
+
1865
+ // A plugin whose families settle one property between them
1866
+ // previews the node as a whole
1867
+ $.each(UTILITIES, function(name, utility) {
1868
+ if (utility.preview) { $.extend(styles, utility.preview(node, kind, tier.key)); }
1869
+ });
1870
+
1871
+ if (!$.isEmptyObject(styles)) { applyPreview(node, styles); }
1872
+ });
1873
+ }
1874
+
1875
+ /**
1876
+ * Set inline !important styles, which is the one thing that beats
1877
+ * Bootstrap's own !important, and remember what each property was so
1878
+ * it can be put back. The record is an attribute rather than jQuery
1879
+ * data because a rich text editor rebuilds the DOM of the area it
1880
+ * edits, and the record has to come back with the node.
1881
+ */
1882
+ function applyPreview(node, styles) {
1883
+ var style = node[0].style;
1884
+ var was = {};
1885
+
1886
+ $.each(styles, function(property, value) {
1887
+ was[property] = [style.getPropertyValue(property), style.getPropertyPriority(property)];
1888
+ style.setProperty(property, String(value), 'important');
1889
+ });
1890
+
1891
+ node.attr(PREVIEW_ATTR, JSON.stringify(was));
1892
+ }
1893
+
1894
+ function clearPreviews(scope) {
1895
+ scope.find('[' + PREVIEW_ATTR + ']').addBack('[' + PREVIEW_ATTR + ']').each(function() {
1896
+ var node = $(this);
1897
+ var style = this.style;
1898
+ var was = {};
1899
+
1900
+ try { was = JSON.parse(node.attr(PREVIEW_ATTR)) || {}; } catch (error) { /* a mangled record: drop it */ }
1901
+
1902
+ $.each(was, function(property, before) {
1903
+ if (before && before[0]) {
1904
+ style.setProperty(property, before[0], before[1]);
1905
+ } else {
1906
+ style.removeProperty(property);
1907
+ }
1908
+ });
1909
+
1910
+ node.removeAttr(PREVIEW_ATTR);
1911
+ if (!node.attr('style')) { node.removeAttr('style'); }
1912
+ });
1913
+ }
1914
+
1915
+ /* The panel */
1916
+
1917
+ /**
1918
+ * The Responsive section of a node's settings panel: one field per
1919
+ * family that applies to the node, reading and writing the view being
1920
+ * edited, and whatever fields a plugin builds itself. Folded until the
1921
+ * user unfolds one, and then unfolded on every node, since whoever
1922
+ * wanted it on one wants it on the next.
1923
+ */
1924
+ function createUtilitiesSection(node) {
1925
+ var kind = kindOf(node);
1926
+ var fields = familiesFor(kind)
1927
+ .filter(function(family) { return family.panel !== false; })
1928
+ .map(function(family) { return createField(node, family); });
1929
+
1930
+ $.each(UTILITIES, function(name, utility) {
1931
+ var own = utility.panel ? utility.panel(node, kind) : null;
1932
+ if (own && own.length) { fields.push(own); }
1933
+ });
1934
+
1935
+ if (!fields.length) { return null; }
1936
+
1937
+ var section = $('<div class="ge-utilities" />')
1938
+ .toggleClass('ge-open', utilitiesOpen)
1939
+ .data('ge-node', node)
1940
+ ;
1941
+
1942
+ $('<a class="ge-utilities-toggle" />')
1943
+ .appendTo(section)
1944
+ .on('click', function() {
1945
+ utilitiesOpen = !section.hasClass('ge-open');
1946
+ canvas.find('.ge-utilities').toggleClass('ge-open', utilitiesOpen);
1947
+ })
1948
+ ;
1949
+
1950
+ var body = $('<div class="ge-utilities-body" />').appendTo(section);
1951
+ fields.forEach(function(field) { field.appendTo(body); });
1952
+
1953
+ renderUtilities(section);
1954
+
1955
+ return section;
1956
+ }
1957
+
1958
+ /**
1959
+ * One family's field: its label, a select that writes through
1960
+ * setUtility, and a note. Filled by renderField, again whenever the
1961
+ * view or the node's classes change.
1962
+ */
1963
+ function createField(node, family) {
1964
+ var field = $('<label class="ge-utility" />').attr('data-ge-family', family.name);
1965
+
1966
+ $('<span class="ge-utility-label" />')
1967
+ .text(family.labelKey ? t(family.labelKey) : family.name)
1968
+ .appendTo(field)
1969
+ ;
1970
+ $('<select />')
1971
+ .appendTo(field)
1972
+ .on('change', function() {
1973
+ setUtility(node, family.name, this.value, { source: 'panel' });
1974
+ })
1975
+ ;
1976
+ $('<small class="ge-utility-note" />').appendTo(field);
1977
+
1978
+ return field;
1979
+ }
1980
+
1981
+ /** ge.utilityField(node, family): a field a plugin places in a panel of its own. */
1982
+ function utilityField(node, name) {
1983
+ var family = familyNamed(name, 'utilityField');
1984
+ if (!family) { return $(); }
1985
+
1986
+ var field = createField(node, family);
1987
+ renderField(field, node);
1988
+
1989
+ return field;
1990
+ }
1991
+
1992
+ /** Fill a section's fields, the families' and the plugins' own, for the view being edited. */
1993
+ function renderUtilities(section) {
1994
+ var node = section.data('ge-node');
1995
+
1996
+ section.children('.ge-utilities-toggle')
1997
+ .text(t('utility.section', { view: t(labelKeyFor(curView)) }));
1998
+
1999
+ section.find('.ge-utility').each(function() { renderField($(this), node); });
2000
+ }
2001
+
2002
+ function renderField(field, node) {
2003
+ var family = FAMILIES[field.attr('data-ge-family')].family;
2004
+ var select = field.children('select').empty();
2005
+ var choices = family.choices ? family.choices(node, kindOf(node)) : family.values;
2006
+ var own, blank, note = '';
2007
+
2008
+ if (curView === ALL_VIEW) {
2009
+ own = ownUtility(node, family, BREAKPOINTS[0]);
2010
+ blank = t('utility.default');
2011
+
2012
+ var varies = utilityTiers(node, family).filter(function(entry) {
2013
+ return entry.tier !== BREAKPOINTS[0];
2014
+ });
2015
+ if (varies.length) {
2016
+ note = t('utility.varies', {
2017
+ breakpoints: varies.map(function(entry) { return entry.tier.key; }).join(', '),
2018
+ });
2019
+ }
2020
+ } else {
2021
+ var tier = breakpoint(curView);
2022
+ var inherited = inheritedUtility(node, family, tier);
2023
+
2024
+ own = ownUtility(node, family, tier);
2025
+ blank = inherited
2026
+ ? t('utility.inherit', { value: labelOf(family, inherited.value), breakpoint: inherited.tier.key })
2027
+ : t('utility.default');
2028
+
2029
+ // What the empty choice means is sometimes not the family's
2030
+ // own business: a column's width can come from its row
2031
+ var custom = family.blank ? family.blank(node, curView) : null;
2032
+ if (custom && own === null) { blank = custom; }
2033
+ }
2034
+
2035
+ // A value the markup carries is shown even when the family
2036
+ // would not offer it here, rather than shown as something else
2037
+ if (own !== null && choices.indexOf(own) === -1) { choices = choices.concat([own]); }
2038
+
2039
+ $('<option value="" />').text(blank).appendTo(select);
2040
+ choices.forEach(function(value) {
2041
+ $('<option />').attr('value', value).text(labelOf(family, value)).appendTo(select);
2042
+ });
2043
+
2044
+ select.val(own === null ? '' : own);
2045
+ field.children('.ge-utility-note').text(note).toggle(note !== '');
2046
+ }
2047
+
2048
+ function labelOf(family, value) {
2049
+ return family.label ? family.label(value) : value;
2050
+ }
2051
+
1293
2052
  /* --------------------------------------------------------------
1294
2053
  * Containers: tabs, accordions and popups.
1295
2054
  *
@@ -1305,6 +2064,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1305
2064
 
1306
2065
  var CONTAINERS = {}; // The container plugins in use, by the type each builds
1307
2066
  var FEATURES = {}; // The feature plugins in use, by name
2067
+ var UTILITIES = {}; // The utility plugins in use, by name
1308
2068
  var featureMethods = {}; // The methods those features contribute
1309
2069
  var containerCounter = 0;
1310
2070
 
@@ -1621,9 +2381,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1621
2381
  settings.valid_col_sizes.forEach(function(size) {
1622
2382
  $('<a class="ge-size" />')
1623
2383
  .attr('data-ge-size', size)
1624
- .attr('title', t('tool.column_size', { size: size }))
1625
- .toggleClass('ge-size-tight', size > room)
1626
- .text(size)
2384
+ .attr('title', sizeTitle(size))
2385
+ .toggleClass('ge-size-tight', isUnits(size) && size > room)
2386
+ .toggleClass('ge-size-flex', !isUnits(size))
2387
+ .text(isUnits(size) ? size : sizeClass(BREAKPOINTS[0], size))
1627
2388
  .on('click', function(e) {
1628
2389
  e.preventDefault();
1629
2390
  e.stopPropagation();
@@ -1644,6 +2405,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1644
2405
  }
1645
2406
 
1646
2407
  /** True when there was one to close, which is also a click's answer. */
2408
+ function sizeTitle(size) {
2409
+ if (size === 'equal') { return t('tool.column_equal'); }
2410
+ if (size === 'auto') { return t('tool.column_auto'); }
2411
+
2412
+ return t('tool.column_size', { size: size });
2413
+ }
2414
+
1647
2415
  function closeSizePicker() {
1648
2416
  if (!sizePicker) { return false; }
1649
2417
 
@@ -1675,7 +2443,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1675
2443
  createTool(drawer, t('tool.add_column'), 'ge-add-column', 'bi bi-plus-circle', function() {
1676
2444
  if (closeSizePicker()) { return; } // The picker was open: that was the answer
1677
2445
 
1678
- addColumnTo(row, settings.add_column.size);
2446
+ // In a row with row-cols the new column takes its share
2447
+ addColumnTo(row, rowColsSource(row, leadingTier()) ? null : settings.add_column.size);
1679
2448
  });
1680
2449
 
1681
2450
  attachSizePicker(drawer.find('> .ge-add-column'), row);
@@ -1695,12 +2464,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1695
2464
  createTool(drawer, t('tool.column_narrower'), 'ge-decrease-col-width', 'bi bi-dash-lg', function(e) {
1696
2465
  resizeColumn(col, e.shiftKey
1697
2466
  ? smallest(settings.valid_col_sizes)
1698
- : stepThrough(settings.valid_col_sizes, currentSize(col), -1),
2467
+ : stepThrough(settings.valid_col_sizes.filter(isUnits), currentUnits(col), -1),
1699
2468
  'tool');
1700
2469
  });
1701
2470
 
1702
2471
  createTool(drawer, t('tool.column_wider'), 'ge-increase-col-width', 'bi bi-plus-lg', function(e) {
1703
- resizeColumn(col, e.shiftKey ? widestFor(col) : stepThrough(settings.valid_col_sizes, currentSize(col), 1), 'tool');
2472
+ resizeColumn(col, e.shiftKey
2473
+ ? widestFor(col)
2474
+ : stepThrough(settings.valid_col_sizes.filter(isUnits), currentUnits(col), 1),
2475
+ 'tool');
1704
2476
  });
1705
2477
 
1706
2478
  createTool(drawer, t('tool.indent_decrease'), 'ge-decrease-col-offset', 'bi bi-text-indent-right', function(e) {
@@ -1751,17 +2523,43 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1751
2523
  * view it is the widest tier, because the canvas is not constrained
1752
2524
  * there and the widest tier is what the user is looking at: clicking
1753
2525
  * "narrower" on a column authored as col-lg-6 should take it to 5,
1754
- * not to 11 because no xs class was ever written.
2526
+ * not to 11 because no base class was ever written. What the all view
2527
+ * writes is the base class all the same.
1755
2528
  */
1756
2529
  function leadingTier() {
1757
2530
  return curView === ALL_VIEW ? BREAKPOINTS[BREAKPOINTS.length - 1] : breakpoint(curView);
1758
2531
  }
1759
2532
 
2533
+ /** The size that applies in the view: units, `equal` or `auto`. */
1760
2534
  function currentSize(col) {
1761
- var size = getEffectiveSize(col, leadingTier());
2535
+ var tier = leadingTier();
2536
+
2537
+ // Sized by its row: it has no size of its own to report
2538
+ if (rowColsWinner(col, tier)) { return null; }
2539
+
2540
+ var size = getEffectiveSize(col, tier);
1762
2541
  return size === null ? MAX_COL_SIZE : size;
1763
2542
  }
1764
2543
 
2544
+ /**
2545
+ * The size in units, which is what the tools step through. An equal
2546
+ * or auto column has no number, so it is measured: the width it has
2547
+ * on the canvas, in twelfths of its row.
2548
+ */
2549
+ function currentUnits(col) {
2550
+ var size = currentSize(col);
2551
+ if (isUnits(size)) { return size; }
2552
+
2553
+ var units = Math.round(col.outerWidth() / rowContentWidth(col.parent()) * MAX_COL_SIZE);
2554
+ return Math.min(Math.max(units, 1), MAX_COL_SIZE);
2555
+ }
2556
+
2557
+ function rowContentWidth(row) {
2558
+ var style = window.getComputedStyle(row[0]);
2559
+
2560
+ return row[0].clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
2561
+ }
2562
+
1765
2563
  function currentOffset(col) {
1766
2564
  return getEffectiveOffset(col, leadingTier()) || 0;
1767
2565
  }
@@ -1781,11 +2579,11 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1781
2579
  }
1782
2580
 
1783
2581
  function smallest(values) {
1784
- return values.reduce(function(a, b) { return Math.min(a, b); }, MAX_COL_SIZE);
2582
+ return values.filter(isUnits).reduce(function(a, b) { return Math.min(a, b); }, MAX_COL_SIZE);
1785
2583
  }
1786
2584
 
1787
2585
  function largest(values) {
1788
- return values.reduce(function(a, b) { return Math.max(a, b); }, 0);
2586
+ return values.filter(isUnits).reduce(function(a, b) { return Math.max(a, b); }, 0);
1789
2587
  }
1790
2588
 
1791
2589
  /**
@@ -1803,7 +2601,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1803
2601
  * itself left inside the row.
1804
2602
  */
1805
2603
  function deepestFor(col) {
1806
- var room = spare(col.parent(), leadingTier(), col) - currentSize(col);
2604
+ var room = spare(col.parent(), leadingTier(), col) - currentUnits(col);
1807
2605
 
1808
2606
  return Math.min(largest(settings.valid_col_offsets), Math.max(room, 0));
1809
2607
  }
@@ -1846,6 +2644,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1846
2644
  function isEditorClass(name) {
1847
2645
  if (name === 'row' || name === 'column') { return true; }
1848
2646
  if (/^(ge-|ui-)/.test(name)) { return true; }
2647
+ if (/^col(-(sm|md|lg|xl|xxl))?(-auto)?$/.test(name)) { return true; }
1849
2648
 
1850
2649
  return BREAKPOINTS.some(function(tier) {
1851
2650
  return new RegExp('^(' + tier.colPrefix + '|' + tier.offsetPrefix + ')\\d+$').test(name);
@@ -1873,6 +2672,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1873
2672
  details.toggle();
1874
2673
  });
1875
2674
 
2675
+ // Beside the gear, because every node that has one is a node a
2676
+ // utility may apply to, whichever plugin built its drawer
2677
+ var kind = kindOf(node);
2678
+ $.each(UTILITIES, function(name, utility) {
2679
+ if (utility.drawerTools) { utility.drawerTools(drawer, node, kind); }
2680
+ });
2681
+
1876
2682
  return details.appendTo(drawer);
1877
2683
  }
1878
2684
 
@@ -1901,6 +2707,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1901
2707
  .appendTo(detailsDiv)
1902
2708
  .on('change', function() {
1903
2709
  setHostClasses(container, this.value);
2710
+ refreshUtilities(container);
1904
2711
  })
1905
2712
  ;
1906
2713
 
@@ -1913,11 +2720,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1913
2720
  .on('click', function() {
1914
2721
  btn.toggleClass('active btn-primary');
1915
2722
  container.toggleClass(rowClass.cssClass, btn.hasClass('active'));
2723
+ refreshUtilities(container);
1916
2724
  })
1917
2725
  .appendTo(classGroup)
1918
2726
  ;
1919
2727
  });
1920
2728
 
2729
+ var utilities = createUtilitiesSection(container);
2730
+ if (utilities) { utilities.appendTo(detailsDiv); }
2731
+
1921
2732
  return detailsDiv;
1922
2733
  }
1923
2734
 
@@ -1932,15 +2743,137 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1932
2743
  * column that had none.
1933
2744
  */
1934
2745
  function addAllColClasses() {
1935
- canvas.find('.column, div[class*="col-"]').each(function() {
2746
+ canvas.find('.column, div[class*="col-"], div.col').each(function() {
1936
2747
  var col = $(this).addClass('column');
1937
2748
 
1938
- if (sizedTiers(col).length) { return; }
2749
+ // A column in a row with row-cols is sized by its row
2750
+ if (sizedTiers(col).length || hasRowCols(col.parent())) { return; }
1939
2751
 
1940
2752
  setSize(col, BREAKPOINTS[0], MAX_COL_SIZE);
1941
2753
  });
1942
2754
  }
1943
2755
 
2756
+ /* --------------------------------------------------------------
2757
+ * Columns per row: row-cols-*.
2758
+ *
2759
+ * A row's row-cols class gives every child an equal share of a
2760
+ * line, and a column's own size class takes that share's place.
2761
+ * Which of the two wins at a breakpoint is settled the way
2762
+ * Bootstrap's css settles it: the class from the wider breakpoint,
2763
+ * and at one breakpoint col loses to row-cols, which loses to
2764
+ * col-auto and col-N - the order Bootstrap writes their rules in.
2765
+ * -------------------------------------------------------------- */
2766
+
2767
+ function hasRowCols(row) {
2768
+ return row.hasClass('row') && BREAKPOINTS.some(function(tier) {
2769
+ return ROW_COLS_VALUES.some(function(value) { return row.hasClass(rowColsClass(tier, value)); });
2770
+ });
2771
+ }
2772
+
2773
+ /** The row-cols class that applies to a row at a tier, as { tier, value }, or null. */
2774
+ function rowColsSource(row, tier) {
2775
+ if (!row.hasClass('row')) { return null; }
2776
+
2777
+ for (var i = BREAKPOINTS.indexOf(tier); i >= 0; i--) {
2778
+ for (var v = 0; v < ROW_COLS_VALUES.length; v++) {
2779
+ if (row.hasClass(rowColsClass(BREAKPOINTS[i], ROW_COLS_VALUES[v]))) {
2780
+ return { tier: i, value: ROW_COLS_VALUES[v] };
2781
+ }
2782
+ }
2783
+ }
2784
+
2785
+ return null;
2786
+ }
2787
+
2788
+ /** The column's own size that applies at a tier, as { tier, size }, or null. */
2789
+ function columnSource(col, tier) {
2790
+ for (var i = BREAKPOINTS.indexOf(tier); i >= 0; i--) {
2791
+ var size = getSize(col, BREAKPOINTS[i]);
2792
+ if (size !== null) { return { tier: i, size: size }; }
2793
+ }
2794
+
2795
+ return null;
2796
+ }
2797
+
2798
+ /**
2799
+ * The row-cols class that sizes this column at a tier, or null when
2800
+ * the column's own size does.
2801
+ */
2802
+ function rowColsWinner(col, tier) {
2803
+ var row = rowColsSource(col.parent(), tier);
2804
+ if (!row) { return null; }
2805
+
2806
+ var own = columnSource(col, tier);
2807
+ if (!own || row.tier > own.tier) { return row; }
2808
+
2809
+ return row.tier === own.tier && own.size === 'equal' ? row : null;
2810
+ }
2811
+
2812
+ /** A column's share of a line when its row sizes it, in units: auto has no number. */
2813
+ function rowColsUnits(winner) {
2814
+ return winner.value === 'auto' ? 0 : MAX_COL_SIZE / parseInt(winner.value, 10);
2815
+ }
2816
+
2817
+ /**
2818
+ * What a breakpoint view shows of the row-cols a column is under. The
2819
+ * view narrows the canvas, not the window, and Bootstrap's
2820
+ * .row-cols-md-3 > * answers to the window - and to nothing the
2821
+ * preview can put on the row, since it styles the row's children.
2822
+ */
2823
+ function rowColsPreview(col, tier) {
2824
+ var winner = rowColsWinner(col, tier);
2825
+
2826
+ if (winner) {
2827
+ return {
2828
+ flex: '0 0 auto',
2829
+ width: winner.value === 'auto' ? 'auto' : (100 / parseInt(winner.value, 10)) + '%',
2830
+ 'max-width': '100%',
2831
+ };
2832
+ }
2833
+
2834
+ // A wider breakpoint's row-cols is live in a wide window; a
2835
+ // column with no size of its own here is a full width one
2836
+ if (hasRowCols(col.parent()) && !columnSource(col, tier)) {
2837
+ return { flex: '0 0 auto', width: '100%', 'max-width': '100%' };
2838
+ }
2839
+
2840
+ return {};
2841
+ }
2842
+
2843
+ /**
2844
+ * The badge on a row whose columns are sized by row-cols in the view,
2845
+ * and in the all view by its row-cols with no breakpoint.
2846
+ */
2847
+ function markRowCols(scope) {
2848
+ scope.find('.row').addBack('.row').each(function() {
2849
+ var row = $(this);
2850
+ var source = settings.row_cols === false ? null : (curView === ALL_VIEW
2851
+ ? (rowColsSource(row, BREAKPOINTS[0]) || null)
2852
+ : rowColsSource(row, breakpoint(curView)));
2853
+
2854
+ if (!source) {
2855
+ row.removeAttr('data-ge-row-cols');
2856
+ } else {
2857
+ row.attr('data-ge-row-cols', rowColsLabel(source.value));
2858
+ }
2859
+ });
2860
+ }
2861
+
2862
+ function rowColsLabel(value) {
2863
+ return value === 'auto' ? t('badge.row_cols_auto') : t('badge.row_cols', { count: value });
2864
+ }
2865
+
2866
+ /** The core's family for the field in a row's panel. */
2867
+ function rowColsFamily() {
2868
+ return {
2869
+ name: 'row-cols',
2870
+ prefix: 'row-cols',
2871
+ values: ROW_COLS_VALUES,
2872
+ appliesTo: ['row'],
2873
+ labelKey: 'utility.row_cols',
2874
+ };
2875
+ }
2876
+
1944
2877
  /* --------------------------------------------------------------
1945
2878
  * The sizing core.
1946
2879
  *
@@ -1950,9 +2883,18 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1950
2883
  * names, the 12 unit budget and what an absent class means.
1951
2884
  * -------------------------------------------------------------- */
1952
2885
 
1953
- /** The units a column is given at one tier, or null if that tier says nothing. */
2886
+ /**
2887
+ * A column's size at one tier: a number of units, `equal`, `auto`, or
2888
+ * null when that tier says nothing.
2889
+ */
1954
2890
  function getSize(col, tier) {
1955
- return readUnits(col, tier.colPrefix);
2891
+ var units = readUnits(col, tier.colPrefix);
2892
+ if (units !== null) { return units; }
2893
+
2894
+ if (col.hasClass(sizeClass(tier, 'auto'))) { return 'auto'; }
2895
+ if (col.hasClass(sizeClass(tier, 'equal'))) { return 'equal'; }
2896
+
2897
+ return null;
1956
2898
  }
1957
2899
 
1958
2900
  /** The units a column is indented by at one tier, or null. */
@@ -2002,8 +2944,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2002
2944
  col.attr('class', classes.join(' '));
2003
2945
  }
2004
2946
 
2005
- function setSize(col, tier, units) {
2006
- writeUnits(col, tier.colPrefix, units);
2947
+ /** One size per tier: writing a size takes whatever size the tier had off. */
2948
+ function setSize(col, tier, size) {
2949
+ writeUnits(col, tier.colPrefix, null);
2950
+ FLEX_SIZES.forEach(function(flex) { col.removeClass(sizeClass(tier, flex)); });
2951
+
2952
+ if (size !== null && size !== undefined) { col.addClass(sizeClass(tier, size)); }
2007
2953
  }
2008
2954
 
2009
2955
  /** An offset of 0 is written as no class at all, which is what it means. */
@@ -2029,7 +2975,11 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2029
2975
  var sibling = $(this);
2030
2976
  if (ignore && sibling[0] === ignore[0]) { return; }
2031
2977
 
2032
- used += (getEffectiveSize(sibling, tier) || 0) + (getEffectiveOffset(sibling, tier) || 0);
2978
+ // An equal or auto column takes what is left, so it uses none;
2979
+ // one its row sizes uses its share of the line
2980
+ var winner = rowColsWinner(sibling, tier);
2981
+ var size = winner ? rowColsUnits(winner) : getEffectiveSize(sibling, tier);
2982
+ used += (isUnits(size) ? size : 0) + (getEffectiveOffset(sibling, tier) || 0);
2033
2983
  });
2034
2984
 
2035
2985
  return MAX_COL_SIZE - used;
@@ -2050,7 +3000,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2050
3000
 
2051
3001
  offset = Math.min(Math.max(offset, 0), MAX_COL_OFFSET);
2052
3002
 
2053
- if (size === null) { return { size: null, offset: offset, refused: false }; }
3003
+ if (size === null || !isUnits(size)) { return { size: size, offset: offset, refused: false }; }
2054
3004
 
2055
3005
  size = Math.min(Math.max(size, 1), MAX_COL_SIZE);
2056
3006
 
@@ -2094,6 +3044,35 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2094
3044
  return 'ge-' + name + '-' + instanceId;
2095
3045
  }
2096
3046
 
3047
+ /**
3048
+ * What the canvas, the columns and the plugins' regions move: rows,
3049
+ * content areas and containers, and whatever blocks a feature adds -
3050
+ * the sections plugin's sections.
3051
+ */
3052
+ function blockSelector() {
3053
+ return ['.row', '.ge-content', '[data-ge-container]'].concat(pluginHooks('blocks')).join(', ');
3054
+ }
3055
+
3056
+ /** A selector every feature plugin may contribute to, under one name. */
3057
+ function pluginHooks(name) {
3058
+ return $.map(FEATURES, function(feature) { return feature[name] || null; });
3059
+ }
3060
+
3061
+ /**
3062
+ * Whether a region will have a block dropped in it. Every block goes
3063
+ * everywhere unless a feature says otherwise: a section only on the
3064
+ * canvas, and only rows in a section.
3065
+ */
3066
+ function acceptsBlock(region, node) {
3067
+ var accepted = true;
3068
+
3069
+ $.each(FEATURES, function(name, feature) {
3070
+ if (accepted && feature.accepts && feature.accepts(region, node) === false) { accepted = false; }
3071
+ });
3072
+
3073
+ return accepted;
3074
+ }
3075
+
2097
3076
  /** What must never start a drag, whatever the handle is. */
2098
3077
  function dragCancelSelector() {
2099
3078
  return settings.drag_handle === 'drawer'
@@ -2131,6 +3110,16 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2131
3110
  ? { name: groupName(options.group) }
2132
3111
  : { name: groupName(options.draggable + '-' + (++soloGroup)), pull: false, put: false };
2133
3112
 
3113
+ // A list may turn some of its group's items away. A put
3114
+ // function replaces SortableJS's own test that the item comes
3115
+ // from the same group, so it makes that test too: without it
3116
+ // a column would drop into the canvas, or into another editor
3117
+ if (options.group && options.accepts) {
3118
+ group.put = function(to, from, dragged) {
3119
+ return from.options.group.name === group.name && options.accepts($(to.el), $(dragged));
3120
+ };
3121
+ }
3122
+
2134
3123
  sortables.push(window.Sortable.create(list, $.extend({
2135
3124
  group: group,
2136
3125
  draggable: options.draggable,
@@ -2195,9 +3184,24 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2195
3184
  });
2196
3185
 
2197
3186
  sortable(canvas.add(canvas.find('.column')), {
2198
- draggable: '.row, .ge-content, [data-ge-container]',
3187
+ draggable: blockSelector(),
2199
3188
  group: 'block',
2200
- });
3189
+ accepts: acceptsBlock,
3190
+ });
3191
+
3192
+ // A plugin's region can be a block itself - a section is both -
3193
+ // and SortableJS tests a list's own element against the draggable
3194
+ // selector when it counts the list's children, so every child of
3195
+ // a section would count as a block, its drawer first. '>' is
3196
+ // SortableJS for "direct children only", which the list is not.
3197
+ var regions = pluginHooks('regions');
3198
+ if (regions.length) {
3199
+ sortable(canvas.find(regions.join(', ')), {
3200
+ draggable: '>' + blockSelector(),
3201
+ group: 'block',
3202
+ accepts: acceptsBlock,
3203
+ });
3204
+ }
2201
3205
 
2202
3206
  // A plugin makes its own: only it knows which of its parts move
2203
3207
  plugins('onSortable', sortable);
@@ -2369,20 +3373,21 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2369
3373
  * divider between two columns looks like it should do.
2370
3374
  */
2371
3375
  function snapUnits(col, pixels) {
2372
- var row = col.parent();
2373
- var style = window.getComputedStyle(row[0]);
2374
- var content = row[0].clientWidth -
2375
- parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
2376
-
2377
- var units = Math.round(pixels / content * MAX_COL_SIZE);
3376
+ var units = Math.round(pixels / rowContentWidth(col.parent()) * MAX_COL_SIZE);
2378
3377
  var next = balanceSibling(col);
3378
+ var nextSize = next ? currentSize(next) : null;
3379
+
3380
+ // Measured when the drag started: the column is the width of the
3381
+ // pointer now, which is not what it was
3382
+ var own = col.data('ge-resize-units');
2379
3383
 
2380
3384
  // With a sibling to balance against, the drag may take that
2381
3385
  // column's units but not its last one. Without one, the row is
2382
3386
  // allowed to wrap - that is what balance false means - so the only
2383
3387
  // limit is the column's own budget against its indent.
2384
- var room = next
2385
- ? currentSize(col) + currentSize(next) - smallest(settings.valid_col_sizes)
3388
+ // An equal or auto sibling gives way by itself, so it sets no limit
3389
+ var room = next && isUnits(nextSize)
3390
+ ? own + nextSize - smallest(settings.valid_col_sizes)
2386
3391
  : MAX_COL_SIZE - currentOffset(col);
2387
3392
 
2388
3393
  return Math.min(
@@ -2428,7 +3433,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2428
3433
  if (!allowed) { return false; }
2429
3434
 
2430
3435
  col.data('ge-resize-from', from);
2431
- resizeReadout(col, sizeLabel(from));
3436
+ col.data('ge-resize-units', currentUnits(col));
3437
+ resizeReadout(col, sizeLabel(col.data('ge-resize-units')));
2432
3438
 
2433
3439
  return true;
2434
3440
  }
@@ -2439,30 +3445,33 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2439
3445
 
2440
3446
  function resizeStop(col, width) {
2441
3447
  var from = col.data('ge-resize-from');
3448
+ var started = col.data('ge-resize-units');
2442
3449
  var units = snapUnits(col, width);
2443
3450
 
2444
- col.removeData('ge-resize-from');
3451
+ col.removeData('ge-resize-from').removeData('ge-resize-units');
2445
3452
  resizeReadout(col, '');
2446
3453
  stripPixelWidths(col);
2447
3454
 
2448
3455
  if (from === undefined) { return; }
2449
3456
 
2450
3457
  // A drag of a couple of pixels lands on the size it started from,
2451
- // and is not a resize
2452
- if (units === from) { return; }
3458
+ // and is not a resize - not even of an equal or auto column,
3459
+ // which it would otherwise turn into a number
3460
+ if (units === started) { return; }
2453
3461
 
2454
3462
  var plan = planSize(col, units);
2455
3463
  if (!plan) { return; }
2456
3464
 
2457
3465
  operate(function() {
2458
3466
  writeSize(col, plan);
2459
- balanceAfterResize(col, plan.size - from);
3467
+ balanceAfterResize(col, units - started);
3468
+ refreshUtilities(col);
2460
3469
 
2461
- emit('after-resize', payloadFor('column', col, {
3470
+ emit('after-resize', payloadFor('column', col, withCleared({
2462
3471
  source: 'dragdrop',
2463
3472
  from: from,
2464
3473
  to: plan.size,
2465
- }));
3474
+ }, plan.cleared)));
2466
3475
  });
2467
3476
  }
2468
3477
 
@@ -2470,12 +3479,22 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2470
3479
  * Move the delta into the following column, so a full row stays full.
2471
3480
  * It is part of the same gesture, so it is not announced separately.
2472
3481
  */
3482
+ /**
3483
+ * The column after takes what the resized one gave or took, unless it
3484
+ * is equal or auto: those take what is left by themselves.
3485
+ */
2473
3486
  function balanceAfterResize(col, delta) {
2474
3487
  var next = balanceSibling(col);
2475
3488
  if (!next || !delta) { return; }
2476
3489
 
2477
- var plan = planSize(next, currentSize(next) - delta);
2478
- if (plan) { writeSize(next, plan); }
3490
+ var size = currentSize(next);
3491
+ if (!isUnits(size)) { return; }
3492
+
3493
+ var plan = planSize(next, size - delta);
3494
+ if (plan) {
3495
+ writeSize(next, plan);
3496
+ refreshUtilities(next);
3497
+ }
2479
3498
  }
2480
3499
 
2481
3500
  /**
@@ -2542,13 +3561,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2542
3561
  function apiCreateRow(layout, options) {
2543
3562
  var row = createRow();
2544
3563
 
2545
- if (layout !== undefined && !Array.isArray(layout)) {
2546
- warn('createRow: the layout is an array of column sizes, as in [8, 4]. ' +
2547
- 'Making an empty row instead.');
3564
+ if (layout !== undefined && !Array.isArray(layout) && !(layout && layout.row_cols)) {
3565
+ warn('createRow: the layout is an array of column sizes, as in [8, 4], or ' +
3566
+ '{ row_cols, columns }. Making an empty row instead.');
2548
3567
  layout = [];
2549
3568
  }
2550
3569
 
2551
- (layout || []).forEach(function(size) {
3570
+ if (layout && !Array.isArray(layout)) { row = rowFromLayoutValue(layout); }
3571
+
3572
+ (Array.isArray(layout) ? layout : []).forEach(function(size) {
2552
3573
  createColumn(size).appendTo(row);
2553
3574
  });
2554
3575
 
@@ -2561,7 +3582,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2561
3582
  function apiCreateColumn(size, options) {
2562
3583
  options = options || {};
2563
3584
 
2564
- if (typeof size != 'number') {
3585
+ // Going into a row with row-cols, no size is a size: the row's share
3586
+ var into = $(options.appendTo || options.prependTo || []).first();
3587
+
3588
+ if (size === undefined && into.length && rowColsSource(into, leadingTier())) {
3589
+ size = null;
3590
+ } else if (!isUnits(size) && FLEX_SIZES.indexOf(size) === -1) {
2565
3591
  warn('createColumn: no column size given, using ' + MAX_COL_SIZE);
2566
3592
  size = MAX_COL_SIZE;
2567
3593
  }
@@ -2632,8 +3658,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2632
3658
  }
2633
3659
 
2634
3660
  /**
2635
- * A column sized for the current view: one tier, or every tier in the
2636
- * all view. `offset` indents it, within the same 12 unit budget.
3661
+ * A column sized for the current view: its tier, or the base class in
3662
+ * the all view. `offset` indents it, within the same 12 unit budget.
2637
3663
  */
2638
3664
  function createColumn(size, offset) {
2639
3665
  var rte = getRTE(settings.content_types[0]);
@@ -2641,12 +3667,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2641
3667
  .append(createDefaultContentWrapper().html(rte ? rte.initialContent : ''))
2642
3668
  ;
2643
3669
 
2644
- tiersFor(curView).forEach(function(tier) {
2645
- var wanted = clamp({ size: size, offset: offset || 0, leading: 'offset' });
3670
+ // The tier being edited, or the base class in the all view
3671
+ var tier = tiersFor(curView)[0];
3672
+ var wanted = clamp({ size: size, offset: offset || 0, leading: 'offset' });
2646
3673
 
2647
- setSize(column, tier, wanted.size === null ? size : wanted.size);
2648
- setOffset(column, tier, wanted.offset);
2649
- });
3674
+ setSize(column, tier, wanted.size === null ? size : wanted.size);
3675
+ setOffset(column, tier, wanted.offset);
2650
3676
 
2651
3677
  return column;
2652
3678
  }
@@ -2758,7 +3784,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2758
3784
  return;
2759
3785
  }
2760
3786
 
3787
+ var from = curView;
2761
3788
  switchLayout(key);
3789
+
3790
+ if (key === from) { return; }
3791
+
3792
+ canvas.find('.ge-utilities').each(function() { renderUtilities($(this)); });
3793
+ refreshPreviews(canvas);
3794
+ plugins('onViewChange', key);
3795
+ emit('view-change', { canvas: canvas, breakpoint: key, from: from, to: key });
2762
3796
  }
2763
3797
 
2764
3798
  function getView() {
@@ -2796,12 +3830,23 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2796
3830
 
2797
3831
  return featureMethods.createElement(content, options);
2798
3832
  },
3833
+ createSection: function(options) {
3834
+ if (!featureMethods.createSection) {
3835
+ warnOnceHere('plugin:sections', 'createSection needs the sections plugin: ' +
3836
+ 'include dist/plugins/grideditor.sections.js after the editor');
3837
+ return null;
3838
+ }
3839
+
3840
+ return featureMethods.createSection(options);
3841
+ },
2799
3842
  createContainer: apiCreateContainer,
2800
3843
  addTab: function(container, options) { return addPaneTo(container, 'tabs', options); },
2801
3844
  addAccordionItem: function(container, options) {
2802
3845
  return addPaneTo(container, 'accordion', options);
2803
3846
  },
2804
3847
  setLocale: setLocale,
3848
+ getUtility: getUtility,
3849
+ setUtility: setUtility,
2805
3850
  canvas: canvas,
2806
3851
  settings: settingsCopy(),
2807
3852
  };
@@ -2853,6 +3898,18 @@ $.fn.gridEditor.containers = {};
2853
3898
  */
2854
3899
  $.fn.gridEditor.features = {};
2855
3900
 
3901
+ /**
3902
+ * Utility plugins: Bootstrap's responsive utility classes - order-md-2,
3903
+ * d-lg-none - edited per breakpoint. A plugin declares families of classes
3904
+ * and the editor reads them, writes them, puts them in the settings panel and
3905
+ * previews them in each view. Same factory, same `plugins` setting.
3906
+ *
3907
+ * $.fn.gridEditor.utilities.order = function(ge) {
3908
+ * return { families: [{ name: 'order', prefix: 'order', values: [...] }] };
3909
+ * };
3910
+ */
3911
+ $.fn.gridEditor.utilities = {};
3912
+
2856
3913
  /** Translator for the editor integrations, which get settings and no instance. */
2857
3914
  $.fn.gridEditor.t = translate;
2858
3915
 
@@ -2874,6 +3931,8 @@ $.fn.gridEditor.locales = {
2874
3931
  'tool.add_row': 'Add row',
2875
3932
  'tool.add_column': 'Add column\n(hold to choose the width)',
2876
3933
  'tool.column_size': '{size} of 12',
3934
+ 'tool.column_equal': 'Equal: shares what the row has left',
3935
+ 'tool.column_auto': 'Auto: as wide as its content',
2877
3936
  'tool.delete_row': 'Remove row',
2878
3937
  'tool.delete_column': 'Remove col',
2879
3938
  'tool.delete_container': 'Remove container',
@@ -2891,6 +3950,7 @@ $.fn.gridEditor.locales = {
2891
3950
  'tool.classes_title': 'Css classes, separated by spaces',
2892
3951
  'tool.toggle_class': 'Toggle "{label}" styling',
2893
3952
  'row.add': 'Add row {layout}',
3953
+ 'row.add_row_cols': 'Add a row of {columns} columns, {counts} per row',
2894
3954
  'confirm.title': 'Confirm',
2895
3955
  'confirm.ok': 'Delete',
2896
3956
  'confirm.cancel': 'Cancel',
@@ -2904,6 +3964,17 @@ $.fn.gridEditor.locales = {
2904
3964
  'view.lg': 'Desktop',
2905
3965
  'view.xl': 'Large desktop',
2906
3966
  'view.xxl': 'Widescreen',
3967
+ 'utility.section': 'Responsive: {view}',
3968
+ 'utility.col_width': 'Width',
3969
+ 'utility.col_equal': 'Equal',
3970
+ 'utility.col_auto': 'Auto',
3971
+ 'utility.col_from_row': 'From the row: {count}',
3972
+ 'utility.row_cols': 'Columns per row',
3973
+ 'badge.row_cols': '{count} per row',
3974
+ 'badge.row_cols_auto': 'As wide as their content',
3975
+ 'utility.default': 'Default',
3976
+ 'utility.inherit': 'Inherit: {value} (from {breakpoint})',
3977
+ 'utility.varies': 'Changes at {breakpoints}; choosing here replaces that',
2907
3978
  'error.sortable_missing': 'SortableJS not available! Make sure you loaded the Sortable js file; dragging is off without it.',
2908
3979
  'warning.setting_removed': 'The {setting} setting was removed in 4.0. Use {replacement} instead.',
2909
3980
  'error.tinymce_missing': 'tinyMCE not available! Make sure you loaded the tinyMCE js file.',