@themarioga/grid-editor 4.1.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 (36) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +68 -8
  3. package/UPGRADING.md +55 -0
  4. package/dist/grideditor.css +461 -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 +634 -109
  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 +26 -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.float.js +56 -0
  15. package/dist/plugins/grideditor.float.min.js +2 -0
  16. package/dist/plugins/grideditor.float.min.js.map +1 -0
  17. package/dist/plugins/grideditor.sections.js +199 -0
  18. package/dist/plugins/grideditor.sections.min.js +2 -0
  19. package/dist/plugins/grideditor.sections.min.js.map +1 -0
  20. package/dist/plugins/grideditor.textalign.js +60 -0
  21. package/dist/plugins/grideditor.textalign.min.js +2 -0
  22. package/dist/plugins/grideditor.textalign.min.js.map +1 -0
  23. package/docs/events.md +13 -2
  24. package/docs/locale-keys.md +36 -1
  25. package/docs/plugins.md +35 -2
  26. package/example/autocols.html +137 -0
  27. package/example/breakpoints.html +7 -5
  28. package/example/index.html +16 -2
  29. package/example/utilities.html +22 -1
  30. package/package.json +1 -1
  31. package/src/js/jquery.grideditor.js +635 -110
  32. package/src/js/locales/grideditor.es.js +26 -0
  33. package/src/js/plugins/grideditor.float.js +56 -0
  34. package/src/js/plugins/grideditor.sections.js +199 -0
  35. package/src/js/plugins/grideditor.textalign.js +60 -0
  36. package/src/less/grideditor.less +132 -0
@@ -28,6 +28,7 @@ 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 },
@@ -62,8 +63,8 @@ var BREAKPOINTS = [
62
63
 
63
64
  /**
64
65
  * The view that edits every tier at once. It is the default, and the one most
65
- * pages want: a layout that needs no per-device tuning is written once and
66
- * 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.
67
68
  */
68
69
  var ALL_VIEW = 'all';
69
70
  var ALL_VIEW_LABEL_KEY = 'view.all';
@@ -77,6 +78,31 @@ var LEGACY_VIEW_INDEXES = ['lg', 'sm', 'xs'];
77
78
  var MAX_COL_SIZE = 12;
78
79
  var MAX_COL_OFFSET = 11;
79
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
+
80
106
  function breakpoint(key) {
81
107
  for (var i = 0; i < BREAKPOINTS.length; i++) {
82
108
  if (BREAKPOINTS[i].key === key) { return BREAKPOINTS[i]; }
@@ -84,9 +110,12 @@ function breakpoint(key) {
84
110
  return null;
85
111
  }
86
112
 
87
- /** 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
+ */
88
117
  function tiersFor(view) {
89
- if (view === ALL_VIEW) { return BREAKPOINTS.slice(); }
118
+ if (view === ALL_VIEW) { return [BREAKPOINTS[0]]; }
90
119
 
91
120
  var tier = breakpoint(view);
92
121
  return tier ? [tier] : [];
@@ -264,11 +293,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
264
293
  'tab_tools' : [], // Host tools on tab drawers
265
294
  'accordion_tools' : [], // Host tools on accordion item drawers
266
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-*
267
297
  'utilities' : {}, // Options for the utility plugins, by plugin name
268
298
  'elements' : NESTED_SETTINGS.elements, // Element level controls, below the column
269
299
  'custom_filter' : '',
270
300
  'content_types' : ['tinymce'],
271
- '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'],
272
302
  'valid_col_offsets' : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
273
303
  'add_column' : NESTED_SETTINGS.add_column, // The add column tool
274
304
  'layout_modes' : VIEW_KEYS.slice(), // Which views the dropdown offers
@@ -580,54 +610,91 @@ $.fn.gridEditor = function( optionsOrMethod ) {
580
610
  }
581
611
 
582
612
  /**
583
- * Resize a column through the events, writing the tiers the current
584
- * 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.
585
615
  *
586
- * The budget is checked per tier and the whole change is refused if
587
- * any tier has no room, so a resize never half lands.
588
- */
589
- /**
590
- * What resizing this column to `size` would write, or null when the
591
- * 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.
592
619
  *
593
620
  * Refused rather than quietly clamped: the offset is something the
594
- * user set, and a tool that rewrites it is a tool that lies. The plan
595
- * 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.
596
624
  */
597
- function planSize(col, size) {
598
- var tiers = tiersFor(curView);
599
- var wanted = tiers.map(function(tier) {
600
- 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;
601
639
  });
640
+ if (refused) { return null; }
641
+
642
+ var base = BREAKPOINTS[0];
643
+ var writes = [];
644
+ var cleared = [];
602
645
 
603
- 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; }
604
649
 
605
- var unchanged = tiers.every(function(tier, i) {
606
- return getSize(col, tier) === wanted[i].size;
650
+ writes.push({ tier: each, size: null });
651
+ cleared.push({ breakpoint: each.key, value: own });
607
652
  });
608
- if (unchanged) { return null; }
609
653
 
610
- 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;
611
657
  }
612
658
 
613
659
  function writeSize(col, plan) {
614
- 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); });
615
661
  stripPixelWidths(col);
616
662
  }
617
663
 
618
- /** Resize a column from a tool, announcing it either side. */
619
- 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
+
620
678
  var from = currentSize(col);
621
- var plan = planSize(col, size);
679
+ var plan = planSize(col, size, view);
622
680
 
623
681
  if (!plan) { return false; }
624
682
 
625
683
  return operate(function() {
626
- 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));
627
690
 
628
- if (!emit('before-resize', payload)) { return false; }
691
+ if (!emit('before-resize', payload)) {
692
+ refreshUtilities(col);
693
+ return false;
694
+ }
629
695
 
630
696
  writeSize(col, plan);
697
+ refreshUtilities(col);
631
698
  emit('after-resize', payload);
632
699
 
633
700
  return true;
@@ -637,41 +704,49 @@ $.fn.gridEditor = function( optionsOrMethod ) {
637
704
  /**
638
705
  * Indent a column, shrinking it when the budget needs it: the offset
639
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.
640
710
  */
641
711
  function indentColumn(col, offset, source) {
642
- var tiers = tiersFor(curView);
712
+ var everywhere = curView === ALL_VIEW;
713
+ var tier = everywhere ? BREAKPOINTS[0] : breakpoint(curView);
643
714
  var from = currentOffset(col);
644
715
 
645
716
  offset = Math.min(Math.max(offset, 0), MAX_COL_OFFSET);
646
717
 
647
- var unchanged = tiers.every(function(tier) {
648
- return (getOffset(col, tier) || 0) === offset;
649
- });
650
- 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; }
651
723
 
652
724
  return operate(function() {
653
- var payload = payloadFor('column', col, {
725
+ var payload = payloadFor('column', col, withCleared({
654
726
  source: source,
655
727
  from: from,
656
728
  to: offset,
657
- });
729
+ }, everywhere ? others.map(function(each) {
730
+ return { breakpoint: each.key, value: getOffset(col, each) };
731
+ }) : null));
658
732
 
659
733
  if (!emit('before-indent', payload)) { return false; }
660
734
 
661
- tiers.forEach(function(tier) {
662
- var was = getEffectiveSize(col, tier);
663
- var wanted = clamp({ size: was, offset: offset, leading: 'offset' });
735
+ setOffset(col, tier, offset);
736
+ others.forEach(function(each) { setOffset(col, each, 0); });
664
737
 
665
- 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; }
666
744
 
667
- // Only where the budget actually forced the column to give
668
- // way; otherwise an indent would write size classes for
669
- // tiers nobody asked it to touch
670
- if (wanted.size !== null && wanted.size !== was) {
671
- setSize(col, tier, wanted.size);
672
- }
745
+ var wanted = clamp({ size: was, offset: offset, leading: 'offset' });
746
+ if (wanted.size !== was) { setSize(col, each, wanted.size); }
673
747
  });
674
748
 
749
+ refreshUtilities(col);
675
750
  emit('after-indent', payload);
676
751
 
677
752
  return true;
@@ -770,17 +845,17 @@ $.fn.gridEditor = function( optionsOrMethod ) {
770
845
  addRowGroup = $('<div class="ge-addRowGroup btn-group" />').appendTo(wrapper);
771
846
  addContainerGroup = $('<div class="ge-addContainerGroup btn-group" />');
772
847
  $.each(settings.new_row_layouts, function(j, layout) {
848
+ var grouped = !Array.isArray(layout);
773
849
  var btn = $('<a class="btn btn-sm btn-primary" />')
774
- .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('-') }))
775
853
  // What this button makes, in the markup rather than in
776
854
  // jQuery data: a drag works on a clone of it
777
855
  .attr('data-ge-toolbar', 'row')
778
- .attr('data-ge-layout', layout.join(','))
856
+ .attr('data-ge-layout', grouped ? JSON.stringify(layout) : layout.join(','))
779
857
  .on('click', function() {
780
- var row = createRow();
781
- layout.forEach(function(i) {
782
- createColumn(i).appendTo(row);
783
- });
858
+ var row = rowFromLayoutValue(layout);
784
859
 
785
860
  var added = addNode('row', row, function() {
786
861
  row.appendTo(canvas);
@@ -795,10 +870,14 @@ $.fn.gridEditor = function( optionsOrMethod ) {
795
870
 
796
871
  btn.append('<i class="bi bi-plus"></i>');
797
872
 
798
- 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;
799
876
  var icon = '<div class="row ge-row-icon">';
800
- layout.forEach(function(i) {
801
- 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>';
802
881
  });
803
882
  icon += '</div>';
804
883
  btn.append(icon);
@@ -830,6 +909,29 @@ $.fn.gridEditor = function( optionsOrMethod ) {
830
909
  ;
831
910
  });
832
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
+
833
935
  // Buttons on right
834
936
  layoutDropdown = $('<div class="dropdown pull-right ge-layout-mode">' +
835
937
  '<button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-bs-toggle="dropdown"></button>' +
@@ -1005,14 +1107,14 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1005
1107
 
1006
1108
  if (!under) { return null; }
1007
1109
 
1008
- var region = $(under).closest('.column, .ge-canvas');
1110
+ var region = $(under).closest(['.column', '.ge-canvas'].concat(pluginHooks('regions')).join(', '));
1009
1111
  if (!region.length || (region[0] !== canvas[0] && !canvas[0].contains(region[0]))) {
1010
1112
  return null;
1011
1113
  }
1012
1114
 
1013
1115
  var before = null;
1014
1116
 
1015
- region.children('.row, .ge-content, [data-ge-container]').each(function() {
1117
+ region.children(blockSelector()).each(function() {
1016
1118
  if (before) { return; }
1017
1119
 
1018
1120
  var box = this.getBoundingClientRect();
@@ -1022,6 +1124,30 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1022
1124
  return { region: region, before: before };
1023
1125
  }
1024
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
+
1025
1151
  /** A line where the block would go, following the pointer. */
1026
1152
  function showDropMarker(pageX, pageY) {
1027
1153
  var where = dropPlaceAt(pageX, pageY);
@@ -1048,6 +1174,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1048
1174
  * the pointer left it.
1049
1175
  */
1050
1176
  function insertFromToolbar(button, where) {
1177
+ if (button.attr('data-ge-toolbar') === 'feature') {
1178
+ return insertFeatureFromToolbar(button, where);
1179
+ }
1180
+
1051
1181
  var container = button.attr('data-ge-toolbar') === 'container';
1052
1182
  var type = button.attr('data-ge-container-type');
1053
1183
  var made = container
@@ -1072,16 +1202,94 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1072
1202
  }, { parent: where.region, source: 'dragdrop' });
1073
1203
  }
1074
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. */
1075
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) {
1076
1259
  var row = createRow();
1077
1260
 
1078
- (layout || '').split(',').forEach(function(size) {
1079
- 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)); }
1080
1269
  });
1081
1270
 
1271
+ for (var i = 0; i < (layout.columns || 0); i++) { createColumn(null).appendTo(row); }
1272
+
1082
1273
  return row;
1083
1274
  }
1084
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
+
1085
1293
  function onScroll(e) {
1086
1294
  var $window = $(window);
1087
1295
 
@@ -1168,6 +1376,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1168
1376
  // one that rebuilt its area's DOM brought the preview styles back
1169
1377
  // with it, and the attribute recording them came back too
1170
1378
  clearPreviews(canvas);
1379
+ canvas.find('[data-ge-row-cols]').removeAttr('data-ge-row-cols');
1171
1380
  unmarkContainers();
1172
1381
  removeSortable();
1173
1382
  removeResizable();
@@ -1236,6 +1445,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1236
1445
  */
1237
1446
  function loadPlugins() {
1238
1447
  var api = pluginApi();
1448
+
1449
+ registerFamily('grid', {}, widthFamily());
1450
+ if (settings.row_cols !== false) { registerFamily('grid', {}, rowColsFamily()); }
1239
1451
  var wanted = function(name) {
1240
1452
  return !settings.plugins || settings.plugins.indexOf(name) !== -1;
1241
1453
  };
@@ -1321,6 +1533,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1321
1533
  getUtility: getUtility,
1322
1534
  setUtility: setUtility,
1323
1535
  utilityField: utilityField,
1536
+ bareStyle: bareStyle,
1537
+ rowFromLayout: rowFromLayoutValue,
1324
1538
  };
1325
1539
  }
1326
1540
 
@@ -1392,6 +1606,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1392
1606
  }
1393
1607
 
1394
1608
  function utilityClass(family, tier, value) {
1609
+ if (family.className) { return family.className(tier.key, value); }
1610
+
1395
1611
  return family.prefix + (tier.infix ? '-' + tier.infix : '') + '-' + value;
1396
1612
  }
1397
1613
 
@@ -1538,6 +1754,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1538
1754
  return false;
1539
1755
  }
1540
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
+
1541
1761
  var plan = planUtility(node, family, view, value);
1542
1762
  if (!plan) { return false; }
1543
1763
 
@@ -1568,6 +1788,29 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1568
1788
  });
1569
1789
  }
1570
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
+
1571
1814
  /** Bring a node's panel and preview up to date with its classes. */
1572
1815
  function refreshUtilities(node) {
1573
1816
  var details = node.children('.ge-tools-drawer').children('.ge-details');
@@ -1598,6 +1841,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1598
1841
 
1599
1842
  if (curView !== ALL_VIEW) { previewTier(scope, breakpoint(curView)); }
1600
1843
 
1844
+ markRowCols(scope);
1845
+
1601
1846
  // Whatever a plugin marks the canvas with to show its utilities
1602
1847
  // goes stale at the same moments the preview does
1603
1848
  plugins('onRefresh', scope);
@@ -1615,6 +1860,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1615
1860
  $.extend(styles, family.preview(effectiveUtility(node, family, tier), node, kind));
1616
1861
  });
1617
1862
 
1863
+ if (kind === 'column') { $.extend(styles, rowColsPreview(node, tier)); }
1864
+
1618
1865
  // A plugin whose families settle one property between them
1619
1866
  // previews the node as a whole
1620
1867
  $.each(UTILITIES, function(name, utility) {
@@ -1778,6 +2025,11 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1778
2025
  blank = inherited
1779
2026
  ? t('utility.inherit', { value: labelOf(family, inherited.value), breakpoint: inherited.tier.key })
1780
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; }
1781
2033
  }
1782
2034
 
1783
2035
  // A value the markup carries is shown even when the family
@@ -2129,9 +2381,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2129
2381
  settings.valid_col_sizes.forEach(function(size) {
2130
2382
  $('<a class="ge-size" />')
2131
2383
  .attr('data-ge-size', size)
2132
- .attr('title', t('tool.column_size', { size: size }))
2133
- .toggleClass('ge-size-tight', size > room)
2134
- .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))
2135
2388
  .on('click', function(e) {
2136
2389
  e.preventDefault();
2137
2390
  e.stopPropagation();
@@ -2152,6 +2405,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2152
2405
  }
2153
2406
 
2154
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
+
2155
2415
  function closeSizePicker() {
2156
2416
  if (!sizePicker) { return false; }
2157
2417
 
@@ -2183,7 +2443,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2183
2443
  createTool(drawer, t('tool.add_column'), 'ge-add-column', 'bi bi-plus-circle', function() {
2184
2444
  if (closeSizePicker()) { return; } // The picker was open: that was the answer
2185
2445
 
2186
- 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);
2187
2448
  });
2188
2449
 
2189
2450
  attachSizePicker(drawer.find('> .ge-add-column'), row);
@@ -2203,12 +2464,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2203
2464
  createTool(drawer, t('tool.column_narrower'), 'ge-decrease-col-width', 'bi bi-dash-lg', function(e) {
2204
2465
  resizeColumn(col, e.shiftKey
2205
2466
  ? smallest(settings.valid_col_sizes)
2206
- : stepThrough(settings.valid_col_sizes, currentSize(col), -1),
2467
+ : stepThrough(settings.valid_col_sizes.filter(isUnits), currentUnits(col), -1),
2207
2468
  'tool');
2208
2469
  });
2209
2470
 
2210
2471
  createTool(drawer, t('tool.column_wider'), 'ge-increase-col-width', 'bi bi-plus-lg', function(e) {
2211
- 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');
2212
2476
  });
2213
2477
 
2214
2478
  createTool(drawer, t('tool.indent_decrease'), 'ge-decrease-col-offset', 'bi bi-text-indent-right', function(e) {
@@ -2259,17 +2523,43 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2259
2523
  * view it is the widest tier, because the canvas is not constrained
2260
2524
  * there and the widest tier is what the user is looking at: clicking
2261
2525
  * "narrower" on a column authored as col-lg-6 should take it to 5,
2262
- * 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.
2263
2528
  */
2264
2529
  function leadingTier() {
2265
2530
  return curView === ALL_VIEW ? BREAKPOINTS[BREAKPOINTS.length - 1] : breakpoint(curView);
2266
2531
  }
2267
2532
 
2533
+ /** The size that applies in the view: units, `equal` or `auto`. */
2268
2534
  function currentSize(col) {
2269
- 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);
2270
2541
  return size === null ? MAX_COL_SIZE : size;
2271
2542
  }
2272
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
+
2273
2563
  function currentOffset(col) {
2274
2564
  return getEffectiveOffset(col, leadingTier()) || 0;
2275
2565
  }
@@ -2289,11 +2579,11 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2289
2579
  }
2290
2580
 
2291
2581
  function smallest(values) {
2292
- 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);
2293
2583
  }
2294
2584
 
2295
2585
  function largest(values) {
2296
- 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);
2297
2587
  }
2298
2588
 
2299
2589
  /**
@@ -2311,7 +2601,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2311
2601
  * itself left inside the row.
2312
2602
  */
2313
2603
  function deepestFor(col) {
2314
- var room = spare(col.parent(), leadingTier(), col) - currentSize(col);
2604
+ var room = spare(col.parent(), leadingTier(), col) - currentUnits(col);
2315
2605
 
2316
2606
  return Math.min(largest(settings.valid_col_offsets), Math.max(room, 0));
2317
2607
  }
@@ -2354,6 +2644,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2354
2644
  function isEditorClass(name) {
2355
2645
  if (name === 'row' || name === 'column') { return true; }
2356
2646
  if (/^(ge-|ui-)/.test(name)) { return true; }
2647
+ if (/^col(-(sm|md|lg|xl|xxl))?(-auto)?$/.test(name)) { return true; }
2357
2648
 
2358
2649
  return BREAKPOINTS.some(function(tier) {
2359
2650
  return new RegExp('^(' + tier.colPrefix + '|' + tier.offsetPrefix + ')\\d+$').test(name);
@@ -2452,15 +2743,137 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2452
2743
  * column that had none.
2453
2744
  */
2454
2745
  function addAllColClasses() {
2455
- canvas.find('.column, div[class*="col-"]').each(function() {
2746
+ canvas.find('.column, div[class*="col-"], div.col').each(function() {
2456
2747
  var col = $(this).addClass('column');
2457
2748
 
2458
- 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; }
2459
2751
 
2460
2752
  setSize(col, BREAKPOINTS[0], MAX_COL_SIZE);
2461
2753
  });
2462
2754
  }
2463
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
+
2464
2877
  /* --------------------------------------------------------------
2465
2878
  * The sizing core.
2466
2879
  *
@@ -2470,9 +2883,18 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2470
2883
  * names, the 12 unit budget and what an absent class means.
2471
2884
  * -------------------------------------------------------------- */
2472
2885
 
2473
- /** 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
+ */
2474
2890
  function getSize(col, tier) {
2475
- 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;
2476
2898
  }
2477
2899
 
2478
2900
  /** The units a column is indented by at one tier, or null. */
@@ -2522,8 +2944,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2522
2944
  col.attr('class', classes.join(' '));
2523
2945
  }
2524
2946
 
2525
- function setSize(col, tier, units) {
2526
- 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)); }
2527
2953
  }
2528
2954
 
2529
2955
  /** An offset of 0 is written as no class at all, which is what it means. */
@@ -2549,7 +2975,11 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2549
2975
  var sibling = $(this);
2550
2976
  if (ignore && sibling[0] === ignore[0]) { return; }
2551
2977
 
2552
- 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);
2553
2983
  });
2554
2984
 
2555
2985
  return MAX_COL_SIZE - used;
@@ -2570,7 +3000,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2570
3000
 
2571
3001
  offset = Math.min(Math.max(offset, 0), MAX_COL_OFFSET);
2572
3002
 
2573
- if (size === null) { return { size: null, offset: offset, refused: false }; }
3003
+ if (size === null || !isUnits(size)) { return { size: size, offset: offset, refused: false }; }
2574
3004
 
2575
3005
  size = Math.min(Math.max(size, 1), MAX_COL_SIZE);
2576
3006
 
@@ -2614,6 +3044,35 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2614
3044
  return 'ge-' + name + '-' + instanceId;
2615
3045
  }
2616
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
+
2617
3076
  /** What must never start a drag, whatever the handle is. */
2618
3077
  function dragCancelSelector() {
2619
3078
  return settings.drag_handle === 'drawer'
@@ -2651,6 +3110,16 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2651
3110
  ? { name: groupName(options.group) }
2652
3111
  : { name: groupName(options.draggable + '-' + (++soloGroup)), pull: false, put: false };
2653
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
+
2654
3123
  sortables.push(window.Sortable.create(list, $.extend({
2655
3124
  group: group,
2656
3125
  draggable: options.draggable,
@@ -2715,9 +3184,24 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2715
3184
  });
2716
3185
 
2717
3186
  sortable(canvas.add(canvas.find('.column')), {
2718
- draggable: '.row, .ge-content, [data-ge-container]',
3187
+ draggable: blockSelector(),
2719
3188
  group: 'block',
2720
- });
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
+ }
2721
3205
 
2722
3206
  // A plugin makes its own: only it knows which of its parts move
2723
3207
  plugins('onSortable', sortable);
@@ -2889,20 +3373,21 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2889
3373
  * divider between two columns looks like it should do.
2890
3374
  */
2891
3375
  function snapUnits(col, pixels) {
2892
- var row = col.parent();
2893
- var style = window.getComputedStyle(row[0]);
2894
- var content = row[0].clientWidth -
2895
- parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
2896
-
2897
- var units = Math.round(pixels / content * MAX_COL_SIZE);
3376
+ var units = Math.round(pixels / rowContentWidth(col.parent()) * MAX_COL_SIZE);
2898
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');
2899
3383
 
2900
3384
  // With a sibling to balance against, the drag may take that
2901
3385
  // column's units but not its last one. Without one, the row is
2902
3386
  // allowed to wrap - that is what balance false means - so the only
2903
3387
  // limit is the column's own budget against its indent.
2904
- var room = next
2905
- ? 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)
2906
3391
  : MAX_COL_SIZE - currentOffset(col);
2907
3392
 
2908
3393
  return Math.min(
@@ -2948,7 +3433,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2948
3433
  if (!allowed) { return false; }
2949
3434
 
2950
3435
  col.data('ge-resize-from', from);
2951
- resizeReadout(col, sizeLabel(from));
3436
+ col.data('ge-resize-units', currentUnits(col));
3437
+ resizeReadout(col, sizeLabel(col.data('ge-resize-units')));
2952
3438
 
2953
3439
  return true;
2954
3440
  }
@@ -2959,30 +3445,33 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2959
3445
 
2960
3446
  function resizeStop(col, width) {
2961
3447
  var from = col.data('ge-resize-from');
3448
+ var started = col.data('ge-resize-units');
2962
3449
  var units = snapUnits(col, width);
2963
3450
 
2964
- col.removeData('ge-resize-from');
3451
+ col.removeData('ge-resize-from').removeData('ge-resize-units');
2965
3452
  resizeReadout(col, '');
2966
3453
  stripPixelWidths(col);
2967
3454
 
2968
3455
  if (from === undefined) { return; }
2969
3456
 
2970
3457
  // A drag of a couple of pixels lands on the size it started from,
2971
- // and is not a resize
2972
- 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; }
2973
3461
 
2974
3462
  var plan = planSize(col, units);
2975
3463
  if (!plan) { return; }
2976
3464
 
2977
3465
  operate(function() {
2978
3466
  writeSize(col, plan);
2979
- balanceAfterResize(col, plan.size - from);
3467
+ balanceAfterResize(col, units - started);
3468
+ refreshUtilities(col);
2980
3469
 
2981
- emit('after-resize', payloadFor('column', col, {
3470
+ emit('after-resize', payloadFor('column', col, withCleared({
2982
3471
  source: 'dragdrop',
2983
3472
  from: from,
2984
3473
  to: plan.size,
2985
- }));
3474
+ }, plan.cleared)));
2986
3475
  });
2987
3476
  }
2988
3477
 
@@ -2990,12 +3479,22 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2990
3479
  * Move the delta into the following column, so a full row stays full.
2991
3480
  * It is part of the same gesture, so it is not announced separately.
2992
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
+ */
2993
3486
  function balanceAfterResize(col, delta) {
2994
3487
  var next = balanceSibling(col);
2995
3488
  if (!next || !delta) { return; }
2996
3489
 
2997
- var plan = planSize(next, currentSize(next) - delta);
2998
- 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
+ }
2999
3498
  }
3000
3499
 
3001
3500
  /**
@@ -3062,13 +3561,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3062
3561
  function apiCreateRow(layout, options) {
3063
3562
  var row = createRow();
3064
3563
 
3065
- if (layout !== undefined && !Array.isArray(layout)) {
3066
- warn('createRow: the layout is an array of column sizes, as in [8, 4]. ' +
3067
- '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.');
3068
3567
  layout = [];
3069
3568
  }
3070
3569
 
3071
- (layout || []).forEach(function(size) {
3570
+ if (layout && !Array.isArray(layout)) { row = rowFromLayoutValue(layout); }
3571
+
3572
+ (Array.isArray(layout) ? layout : []).forEach(function(size) {
3072
3573
  createColumn(size).appendTo(row);
3073
3574
  });
3074
3575
 
@@ -3081,7 +3582,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3081
3582
  function apiCreateColumn(size, options) {
3082
3583
  options = options || {};
3083
3584
 
3084
- 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) {
3085
3591
  warn('createColumn: no column size given, using ' + MAX_COL_SIZE);
3086
3592
  size = MAX_COL_SIZE;
3087
3593
  }
@@ -3152,8 +3658,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3152
3658
  }
3153
3659
 
3154
3660
  /**
3155
- * A column sized for the current view: one tier, or every tier in the
3156
- * 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.
3157
3663
  */
3158
3664
  function createColumn(size, offset) {
3159
3665
  var rte = getRTE(settings.content_types[0]);
@@ -3161,12 +3667,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3161
3667
  .append(createDefaultContentWrapper().html(rte ? rte.initialContent : ''))
3162
3668
  ;
3163
3669
 
3164
- tiersFor(curView).forEach(function(tier) {
3165
- 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' });
3166
3673
 
3167
- setSize(column, tier, wanted.size === null ? size : wanted.size);
3168
- setOffset(column, tier, wanted.offset);
3169
- });
3674
+ setSize(column, tier, wanted.size === null ? size : wanted.size);
3675
+ setOffset(column, tier, wanted.offset);
3170
3676
 
3171
3677
  return column;
3172
3678
  }
@@ -3324,6 +3830,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3324
3830
 
3325
3831
  return featureMethods.createElement(content, options);
3326
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
+ },
3327
3842
  createContainer: apiCreateContainer,
3328
3843
  addTab: function(container, options) { return addPaneTo(container, 'tabs', options); },
3329
3844
  addAccordionItem: function(container, options) {
@@ -3416,6 +3931,8 @@ $.fn.gridEditor.locales = {
3416
3931
  'tool.add_row': 'Add row',
3417
3932
  'tool.add_column': 'Add column\n(hold to choose the width)',
3418
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',
3419
3936
  'tool.delete_row': 'Remove row',
3420
3937
  'tool.delete_column': 'Remove col',
3421
3938
  'tool.delete_container': 'Remove container',
@@ -3433,6 +3950,7 @@ $.fn.gridEditor.locales = {
3433
3950
  'tool.classes_title': 'Css classes, separated by spaces',
3434
3951
  'tool.toggle_class': 'Toggle "{label}" styling',
3435
3952
  'row.add': 'Add row {layout}',
3953
+ 'row.add_row_cols': 'Add a row of {columns} columns, {counts} per row',
3436
3954
  'confirm.title': 'Confirm',
3437
3955
  'confirm.ok': 'Delete',
3438
3956
  'confirm.cancel': 'Cancel',
@@ -3447,6 +3965,13 @@ $.fn.gridEditor.locales = {
3447
3965
  'view.xl': 'Large desktop',
3448
3966
  'view.xxl': 'Widescreen',
3449
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',
3450
3975
  'utility.default': 'Default',
3451
3976
  'utility.inherit': 'Inherit: {value} (from {breakpoint})',
3452
3977
  'utility.varies': 'Changes at {breakpoints}; choosing here replaces that',