@themarioga/grid-editor 3.2.1 → 4.1.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/BUILDING.md +12 -3
  2. package/CHANGELOG.md +87 -0
  3. package/README.md +79 -7
  4. package/UPGRADING.md +79 -0
  5. package/dist/grideditor.css +188 -20
  6. package/dist/grideditor.min.css +1 -1
  7. package/dist/grideditor.min.css.map +1 -1
  8. package/dist/jquery.grideditor.bundle.min.js +10 -0
  9. package/dist/jquery.grideditor.js +924 -174
  10. package/dist/jquery.grideditor.min.js +1 -1
  11. package/dist/jquery.grideditor.min.js.map +1 -1
  12. package/dist/locales/grideditor.es.js +34 -0
  13. package/dist/locales/grideditor.es.min.js +1 -1
  14. package/dist/locales/grideditor.es.min.js.map +1 -1
  15. package/dist/plugins/grideditor.accordion.js +8 -0
  16. package/dist/plugins/grideditor.accordion.min.js +1 -1
  17. package/dist/plugins/grideditor.accordion.min.js.map +1 -1
  18. package/dist/plugins/grideditor.alignment.js +81 -0
  19. package/dist/plugins/grideditor.alignment.min.js +2 -0
  20. package/dist/plugins/grideditor.alignment.min.js.map +1 -0
  21. package/dist/plugins/grideditor.elements.js +5 -5
  22. package/dist/plugins/grideditor.elements.min.js +1 -1
  23. package/dist/plugins/grideditor.elements.min.js.map +1 -1
  24. package/dist/plugins/grideditor.gutters.js +145 -0
  25. package/dist/plugins/grideditor.gutters.min.js +2 -0
  26. package/dist/plugins/grideditor.gutters.min.js.map +1 -0
  27. package/dist/plugins/grideditor.order.js +206 -0
  28. package/dist/plugins/grideditor.order.min.js +2 -0
  29. package/dist/plugins/grideditor.order.min.js.map +1 -0
  30. package/dist/plugins/grideditor.spacing.js +299 -0
  31. package/dist/plugins/grideditor.spacing.min.js +2 -0
  32. package/dist/plugins/grideditor.spacing.min.js.map +1 -0
  33. package/dist/plugins/grideditor.tabs.js +8 -0
  34. package/dist/plugins/grideditor.tabs.min.js +1 -1
  35. package/dist/plugins/grideditor.tabs.min.js.map +1 -1
  36. package/dist/plugins/grideditor.visibility.js +172 -0
  37. package/dist/plugins/grideditor.visibility.min.js +2 -0
  38. package/dist/plugins/grideditor.visibility.min.js.map +1 -0
  39. package/docs/events.md +26 -8
  40. package/docs/locale-keys.md +54 -0
  41. package/docs/plugins.md +129 -10
  42. package/example/autosave.html +6 -1
  43. package/example/basic.html +8 -2
  44. package/example/breakpoints.html +6 -1
  45. package/example/ckeditor.html +6 -1
  46. package/example/containers.html +5 -1
  47. package/example/elements.html +6 -1
  48. package/example/index.html +24 -1
  49. package/example/locale.html +6 -1
  50. package/example/plugins.html +6 -1
  51. package/example/summernote.html +6 -1
  52. package/example/utilities.html +145 -0
  53. package/example/wrap_content.html +6 -1
  54. package/package.json +3 -2
  55. package/src/js/jquery.grideditor.js +924 -174
  56. package/src/js/locales/grideditor.es.js +34 -0
  57. package/src/js/plugins/grideditor.accordion.js +8 -0
  58. package/src/js/plugins/grideditor.alignment.js +81 -0
  59. package/src/js/plugins/grideditor.elements.js +5 -5
  60. package/src/js/plugins/grideditor.gutters.js +145 -0
  61. package/src/js/plugins/grideditor.order.js +206 -0
  62. package/src/js/plugins/grideditor.spacing.js +299 -0
  63. package/src/js/plugins/grideditor.tabs.js +8 -0
  64. package/src/js/plugins/grideditor.visibility.js +172 -0
  65. package/src/less/grideditor.less +210 -18
@@ -32,6 +32,8 @@ var METHODS = {
32
32
  addTab: { value: true },
33
33
  addAccordionItem: { value: true },
34
34
  setLocale: {},
35
+ getUtility: { value: true },
36
+ setUtility: { value: true },
35
37
  };
36
38
 
37
39
  /**
@@ -45,14 +47,17 @@ var PLACEMENTS = ['appendTo', 'prependTo', 'insertAfter', 'insertBefore'];
45
47
  * runs in: a size written for a tier applies to every wider tier that does not
46
48
  * override it. Every size and offset class grid-editor reads or writes comes
47
49
  * from this table, so adding a tier is a row here and nothing else.
50
+ *
51
+ * `infix` is what Bootstrap's utility classes put between the property and
52
+ * the value - `order-md-2`, `d-none` - and is empty for the smallest tier.
48
53
  */
49
54
  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' },
55
+ { key: 'xs', infix: '', colPrefix: 'col-', offsetPrefix: 'offset-', min: 0, preview: 400, labelKey: 'view.xs' },
56
+ { key: 'sm', infix: 'sm', colPrefix: 'col-sm-', offsetPrefix: 'offset-sm-', min: 576, preview: 576, labelKey: 'view.sm' },
57
+ { key: 'md', infix: 'md', colPrefix: 'col-md-', offsetPrefix: 'offset-md-', min: 768, preview: 768, labelKey: 'view.md' },
58
+ { key: 'lg', infix: 'lg', colPrefix: 'col-lg-', offsetPrefix: 'offset-lg-', min: 992, preview: 992, labelKey: 'view.lg' },
59
+ { key: 'xl', infix: 'xl', colPrefix: 'col-xl-', offsetPrefix: 'offset-xl-', min: 1200, preview: 1200, labelKey: 'view.xl' },
60
+ { key: 'xxl', infix: 'xxl', colPrefix: 'col-xxl-', offsetPrefix: 'offset-xxl-', min: 1400, preview: null, labelKey: 'view.xxl' },
56
61
  ];
57
62
 
58
63
  /**
@@ -111,13 +116,29 @@ var NESTED_SETTINGS = {
111
116
  },
112
117
  resize: {
113
118
  enabled: true,
114
- handles: 'e', // Which edges carry a handle, as jQuery UI names them
119
+ handles: 'e', // Which edges carry a handle: 'e', 'w', or 'e, w'
115
120
  balance: 'next', // 'next' takes the units out of the following column
116
121
  },
122
+ drag: {
123
+ delay: 0, // Milliseconds to hold before a drag starts
124
+ touch_delay: 100, // The same for touch, where 0 eats the page's scrolling
125
+ threshold: 3, // Pixels of movement before a gesture counts as a drag
126
+ animation: 150, // Milliseconds of reordering animation, 0 for none
127
+ scroll: true, // Scroll the page when a drag reaches its edge
128
+ },
129
+ };
130
+
131
+ /** Settings 4.0 took away, and what a host should reach for instead. */
132
+ var REMOVED_SETTINGS = {
133
+ sortable_options: 'drag',
134
+ resizable_options: 'resize',
117
135
  };
118
136
 
119
137
  var warned = {};
120
138
 
139
+ /** Editors on the page, counted so each one's sortable groups are its own. */
140
+ var editorCounter = 0;
141
+
121
142
  /**
122
143
  * Translate one key.
123
144
  *
@@ -242,7 +263,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
242
263
  'container_tools' : [], // Host tools on container drawers
243
264
  'tab_tools' : [], // Host tools on tab drawers
244
265
  'accordion_tools' : [], // Host tools on accordion item drawers
245
- 'plugins' : null, // Container plugins to use; null means every one loaded
266
+ 'plugins' : null, // Plugins to use, of any kind; null means every one loaded
267
+ 'utilities' : {}, // Options for the utility plugins, by plugin name
246
268
  'elements' : NESTED_SETTINGS.elements, // Element level controls, below the column
247
269
  'custom_filter' : '',
248
270
  'content_types' : ['tinymce'],
@@ -252,13 +274,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
252
274
  'layout_modes' : VIEW_KEYS.slice(), // Which views the dropdown offers
253
275
  'default_view' : ALL_VIEW,
254
276
  'resize' : NESTED_SETTINGS.resize, // Resizing a column by dragging its edge
255
- 'resizable_options' : {}, // Merged into every jQuery UI resizable
256
277
  'source_textarea' : '',
257
278
  'locale' : 'en', // Code of a locale in $.fn.gridEditor.locales
258
279
  'locale_strings' : {}, // Overrides for individual keys
259
280
  'callbacks' : {}, // before_*/after_* functions, the events by another route
260
281
  'confirm_delete' : true, // Ask before deleting a row or a column
261
- 'sortable_options' : {} // Merged into every jQuery UI sortable
282
+ 'drag' : NESTED_SETTINGS.drag // How a drag behaves, whatever drives it
262
283
  }, optionsOrMethod);
263
284
 
264
285
  // Merged rather than replaced, so `elements: { auto: true }` keeps the
@@ -267,6 +288,18 @@ $.fn.gridEditor = function( optionsOrMethod ) {
267
288
  settings[name] = $.extend({}, defaults, settings[name]);
268
289
  });
269
290
 
291
+ // Both handed out the drag toolkit's own options, which 4.0 stops
292
+ // promising: there is no widget underneath a host should be reaching
293
+ // for. What they were used for is a setting of the editor's now.
294
+ $.each(REMOVED_SETTINGS, function(name, replacement) {
295
+ if (optionsOrMethod && optionsOrMethod[name] !== undefined) {
296
+ warn($.fn.gridEditor.t(settings, 'warning.setting_removed', {
297
+ setting: name,
298
+ replacement: replacement,
299
+ }));
300
+ }
301
+ });
302
+
270
303
 
271
304
  // Elems
272
305
  var canvas,
@@ -282,6 +315,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
282
315
  var sizePicker = null; // The open column size picker, if there is one
283
316
  var dropMarker = null; // The line showing where a dragged toolbar button would land
284
317
  var warnedHere = {}; // Deprecations are worth saying once per instance, not once per call
318
+ var sortables = []; // Every list made sortable, so deinit destroys exactly those
319
+ var instanceId = ++editorCounter; // Scopes the sortable groups to this editor
285
320
 
286
321
  // Before anything else, because the instance handle hands the canvas
287
322
  // to hosts and the rest of setup() runs at the end of this function
@@ -719,6 +754,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
719
754
  // when their editor is ready, and the drawers go back in.
720
755
  canvas.on('ge-rte-ready', '.ge-content', function() {
721
756
  plugins('onContentReady', $(this));
757
+ refreshPreviews($(this));
722
758
  });
723
759
  }
724
760
 
@@ -874,26 +910,82 @@ $.fn.gridEditor = function( optionsOrMethod ) {
874
910
  }
875
911
 
876
912
  function makeToolbarDraggable() {
913
+ var buttons = mainControls.find('[data-ge-toolbar]')
914
+ .removeClass('ge-palette-button')
915
+ .off('pointerdown.ge-palette')
916
+ ;
917
+
877
918
  if (!toolbarDrags()) { return; }
878
919
 
879
- mainControls.find('[data-ge-toolbar]').draggable({
880
- helper: function() { return $(this).clone().addClass('ge-toolbar-helper'); },
881
- appendTo: 'body',
882
- zIndex: 1000,
883
- cursorAt: { top: 14, left: 14 },
920
+ buttons.addClass('ge-palette-button').on('pointerdown.ge-palette', startToolbarDrag);
921
+ }
884
922
 
885
- start: function() { canvas.addClass('ge-dropping'); },
886
- drag: function(e) { showDropMarker(e.pageX, e.pageY); },
923
+ /**
924
+ * A toolbar button carried onto the canvas.
925
+ *
926
+ * Nothing happens until the pointer has moved far enough to mean it:
927
+ * below that the gesture is a click, and the button's own handler
928
+ * adds the block at the end of the canvas as it always has - which is
929
+ * also what keeps the add column tool's hold-to-pick working.
930
+ */
931
+ function startToolbarDrag(e) {
932
+ var button = $(e.currentTarget);
933
+ var startX = e.pageX;
934
+ var startY = e.pageY;
935
+ var helper = null;
887
936
 
888
- stop: function(e) {
889
- var where = dropPlaceAt(e.pageX, e.pageY);
937
+ if (e.button) { return; }
890
938
 
891
- canvas.removeClass('ge-dropping');
892
- hideDropMarker();
939
+ function far(move) {
940
+ return Math.abs(move.pageX - startX) + Math.abs(move.pageY - startY) >
941
+ settings.drag.threshold;
942
+ }
893
943
 
894
- if (where) { insertFromToolbar($(this), where); }
895
- },
896
- });
944
+ function onMove(move) {
945
+ if (!helper) {
946
+ if (!far(move)) { return; }
947
+
948
+ helper = button.clone()
949
+ .addClass('ge-toolbar-helper')
950
+ .appendTo('body')
951
+ ;
952
+ canvas.addClass('ge-dropping');
953
+ }
954
+
955
+ helper.css({ left: move.pageX - 14, top: move.pageY - 14 });
956
+ showDropMarker(move.pageX, move.pageY);
957
+ }
958
+
959
+ function onUp(up) {
960
+ $(document)
961
+ .off('pointermove.ge-toolbar', onMove)
962
+ .off('pointerup.ge-toolbar pointercancel.ge-toolbar', onUp)
963
+ ;
964
+
965
+ if (!helper) { return; }
966
+
967
+ helper.remove();
968
+ canvas.removeClass('ge-dropping');
969
+ hideDropMarker();
970
+
971
+ // The click that follows a drag would add the block a second
972
+ // time, at the end of the canvas
973
+ button.one('click', function(click) {
974
+ click.preventDefault();
975
+ click.stopImmediatePropagation();
976
+ });
977
+
978
+ var where = dropPlaceAt(up.pageX, up.pageY);
979
+ if (where) { insertFromToolbar(button, where); }
980
+ }
981
+
982
+ // On the document, not on the button: until the gesture is far
983
+ // enough along to be a drag there is nothing to capture the
984
+ // pointer with, and the pointer has left the button by then
985
+ $(document)
986
+ .on('pointermove.ge-toolbar', onMove)
987
+ .on('pointerup.ge-toolbar pointercancel.ge-toolbar', onUp)
988
+ ;
897
989
  }
898
990
 
899
991
  /**
@@ -1051,6 +1143,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1051
1143
  makeSortable();
1052
1144
  makeResizable();
1053
1145
  switchLayout(curView);
1146
+ refreshPreviews(canvas);
1054
1147
  }
1055
1148
 
1056
1149
  function deinit() {
@@ -1071,6 +1164,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1071
1164
  hideDropMarker();
1072
1165
  canvas.find('.ge-tools-drawer').remove();
1073
1166
  plugins('onDeinit');
1167
+ // After the rich text editors have let go of their content areas:
1168
+ // one that rebuilt its area's DOM brought the preview styles back
1169
+ // with it, and the attribute recording them came back too
1170
+ clearPreviews(canvas);
1074
1171
  unmarkContainers();
1075
1172
  removeSortable();
1076
1173
  removeResizable();
@@ -1151,6 +1248,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1151
1248
  if (wanted(name)) { FEATURES[name] = factory(api); }
1152
1249
  });
1153
1250
 
1251
+ $.each($.fn.gridEditor.utilities, function(name, factory) {
1252
+ if (!wanted(name)) { return; }
1253
+
1254
+ UTILITIES[name] = factory(api);
1255
+ (UTILITIES[name].families || []).forEach(function(family) {
1256
+ registerFamily(name, UTILITIES[name], family);
1257
+ });
1258
+ });
1259
+
1154
1260
  $.each(FEATURES, function(name, feature) {
1155
1261
  $.each(feature.methods || {}, function(method, implementation) {
1156
1262
  featureMethods[method] = implementation;
@@ -1158,7 +1264,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1158
1264
  });
1159
1265
 
1160
1266
  (settings.plugins || []).forEach(function(name) {
1161
- if (CONTAINERS[name] || FEATURES[name]) { return; }
1267
+ if (CONTAINERS[name] || FEATURES[name] || UTILITIES[name]) { return; }
1162
1268
 
1163
1269
  warnOnceHere('plugin:' + name, 'the "' + name + '" plugin is not loaded: ' +
1164
1270
  'include dist/plugins/grideditor.' + name + '.js after the editor');
@@ -1168,7 +1274,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1168
1274
  /**
1169
1275
  * A hook every loaded plugin may have. Containers first, since a
1170
1276
  * feature that looks at the canvas - elements, say - wants the
1171
- * containers already marked.
1277
+ * containers already marked, and utilities last, since they decorate
1278
+ * nodes the other two may have just made.
1172
1279
  */
1173
1280
  function plugins(hook, argument) {
1174
1281
  $.each(CONTAINERS, function(type, definition) {
@@ -1177,6 +1284,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1177
1284
  $.each(FEATURES, function(name, feature) {
1178
1285
  if (feature[hook]) { feature[hook](argument); }
1179
1286
  });
1287
+ $.each(UTILITIES, function(name, utility) {
1288
+ if (utility[hook]) { utility[hook](argument); }
1289
+ });
1180
1290
  }
1181
1291
 
1182
1292
  /** What a plugin is handed. See docs/plugins.md. */
@@ -1202,9 +1312,491 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1202
1312
  emit: emit,
1203
1313
  payloadFor: payloadFor,
1204
1314
  operate: operate,
1315
+ kindOf: kindOf,
1316
+ view: getView,
1317
+ viewTiers: function() {
1318
+ return tiersFor(curView).map(function(tier) { return tier.key; });
1319
+ },
1320
+ breakpoints: BREAKPOINTS.map(function(tier) { return tier.key; }),
1321
+ getUtility: getUtility,
1322
+ setUtility: setUtility,
1323
+ utilityField: utilityField,
1205
1324
  };
1206
1325
  }
1207
1326
 
1327
+ /* --------------------------------------------------------------
1328
+ * Utilities: Bootstrap's responsive utility classes.
1329
+ *
1330
+ * A utility plugin declares families - order, d, justify-content -
1331
+ * and this is everything that reads or writes one. Every family is
1332
+ * spelled the way Bootstrap spells them all, {prefix}-{infix}-{value}
1333
+ * with no infix at the smallest tier, and cascades the way the size
1334
+ * classes do: a tier that says nothing takes the nearest smaller one.
1335
+ *
1336
+ * The canvas cannot show a breakpoint's utilities by itself. A
1337
+ * per-breakpoint view narrows the canvas, not the viewport, and
1338
+ * Bootstrap's utilities answer to the viewport with !important. So
1339
+ * each node gets what its classes mean at the view being edited,
1340
+ * as inline !important styles, recorded in an attribute so they come
1341
+ * off again - exactly those, leaving the host's own style alone.
1342
+ * -------------------------------------------------------------- */
1343
+
1344
+ var FAMILIES = {}; // Every family the utility plugins declare, by name
1345
+ var UTILITY_NODES = '.row, .column, .ge-element, [data-ge-container]';
1346
+ var PREVIEW_ATTR = 'data-ge-preview';
1347
+ var utilitiesOpen = false; // Whether the panels show their Responsive section unfolded
1348
+
1349
+ function registerFamily(pluginName, utility, family) {
1350
+ var name = family.name || family.prefix;
1351
+
1352
+ if (FAMILIES[name]) {
1353
+ warn('the "' + pluginName + '" plugin declares the utility "' + name +
1354
+ '", which the "' + FAMILIES[name].plugin + '" plugin already declared: ignored');
1355
+ return;
1356
+ }
1357
+
1358
+ FAMILIES[name] = {
1359
+ plugin: pluginName,
1360
+ family: $.extend({}, family, {
1361
+ name: name,
1362
+ values: family.values.map(String),
1363
+ appliesTo: family.appliesTo || utility.appliesTo || ['row', 'column'],
1364
+ }),
1365
+ };
1366
+ }
1367
+
1368
+ /** The family a caller named, or null and a warning when no plugin declares it. */
1369
+ function familyNamed(name, method) {
1370
+ if (FAMILIES[name]) { return FAMILIES[name].family; }
1371
+
1372
+ warnOnceHere('utility:' + name, method + '(' + JSON.stringify(name) + '): no loaded ' +
1373
+ 'plugin declares that utility');
1374
+ return null;
1375
+ }
1376
+
1377
+ /**
1378
+ * Whether a family applies to a kind of node. A container answers to
1379
+ * `container` and to its own type, so a plugin can take them all or
1380
+ * name the ones it means.
1381
+ */
1382
+ function appliesTo(family, kind) {
1383
+ if (family.appliesTo.indexOf(kind) !== -1) { return true; }
1384
+
1385
+ return !!CONTAINERS[kind] && family.appliesTo.indexOf('container') !== -1;
1386
+ }
1387
+
1388
+ function familiesFor(kind) {
1389
+ return $.map(FAMILIES, function(entry) {
1390
+ return appliesTo(entry.family, kind) ? entry.family : null;
1391
+ });
1392
+ }
1393
+
1394
+ function utilityClass(family, tier, value) {
1395
+ return family.prefix + (tier.infix ? '-' + tier.infix : '') + '-' + value;
1396
+ }
1397
+
1398
+ /** The value a node carries a class for at exactly this tier, or null. */
1399
+ function ownUtility(node, family, tier) {
1400
+ var classes = (node.attr('class') || '').split(/\s+/);
1401
+
1402
+ for (var i = 0; i < family.values.length; i++) {
1403
+ if (classes.indexOf(utilityClass(family, tier, family.values[i])) !== -1) {
1404
+ return family.values[i];
1405
+ }
1406
+ }
1407
+
1408
+ return null;
1409
+ }
1410
+
1411
+ /**
1412
+ * The tier below this one that decides the value here, and what it
1413
+ * says, when this tier says nothing itself. Null when nothing below
1414
+ * says anything either.
1415
+ */
1416
+ function inheritedUtility(node, family, tier) {
1417
+ for (var i = BREAKPOINTS.indexOf(tier) - 1; i >= 0; i--) {
1418
+ var value = ownUtility(node, family, BREAKPOINTS[i]);
1419
+ if (value !== null) { return { tier: BREAKPOINTS[i], value: value }; }
1420
+ }
1421
+
1422
+ return null;
1423
+ }
1424
+
1425
+ /** What applies at a tier: its own class, or what it inherits. */
1426
+ function effectiveUtility(node, family, tier) {
1427
+ var own = ownUtility(node, family, tier);
1428
+ if (own !== null) { return own; }
1429
+
1430
+ var inherited = inheritedUtility(node, family, tier);
1431
+ return inherited ? inherited.value : null;
1432
+ }
1433
+
1434
+ /** Every tier the node carries a class of this family for. */
1435
+ function utilityTiers(node, family) {
1436
+ return BREAKPOINTS.map(function(tier) {
1437
+ return { tier: tier, value: ownUtility(node, family, tier) };
1438
+ }).filter(function(entry) { return entry.value !== null; });
1439
+ }
1440
+
1441
+ function writeUtility(node, family, tier, value) {
1442
+ family.values.forEach(function(candidate) {
1443
+ node.removeClass(utilityClass(family, tier, candidate));
1444
+ });
1445
+
1446
+ if (value !== null) { node.addClass(utilityClass(family, tier, value)); }
1447
+ if (!node.attr('class')) { node.removeAttr('class'); }
1448
+ }
1449
+
1450
+ /**
1451
+ * What a view reads. A breakpoint reads what applies there. The all
1452
+ * view reads the class with no infix, since that is what it writes:
1453
+ * the one class that means "the same at every size".
1454
+ */
1455
+ function readUtility(node, family, view) {
1456
+ return view === ALL_VIEW
1457
+ ? ownUtility(node, family, BREAKPOINTS[0])
1458
+ : effectiveUtility(node, family, breakpoint(view));
1459
+ }
1460
+
1461
+ /**
1462
+ * What writing `value` in a view comes to, or null when it changes
1463
+ * nothing. A breakpoint writes its own tier and nothing else. The all
1464
+ * view writes the class with no infix and clears the family from
1465
+ * every other tier, and says what it cleared: choosing one value for
1466
+ * every size is choosing it over what the sizes said.
1467
+ */
1468
+ function planUtility(node, family, view, value) {
1469
+ if (view !== ALL_VIEW) {
1470
+ var tier = breakpoint(view);
1471
+ if (ownUtility(node, family, tier) === value) { return null; }
1472
+
1473
+ return { writes: [{ tier: tier, value: value }], cleared: [] };
1474
+ }
1475
+
1476
+ var writes = [];
1477
+ var cleared = [];
1478
+
1479
+ utilityTiers(node, family).forEach(function(entry) {
1480
+ if (entry.tier === BREAKPOINTS[0]) { return; }
1481
+
1482
+ writes.push({ tier: entry.tier, value: null });
1483
+ cleared.push({ breakpoint: entry.tier.key, value: entry.value });
1484
+ });
1485
+
1486
+ if (ownUtility(node, family, BREAKPOINTS[0]) !== value) {
1487
+ writes.unshift({ tier: BREAKPOINTS[0], value: value });
1488
+ }
1489
+
1490
+ return writes.length ? { writes: writes, cleared: cleared } : null;
1491
+ }
1492
+
1493
+ /** getUtility(node, family, view?): the value that applies, or null. */
1494
+ function getUtility(node, name, view) {
1495
+ node = $(node).first();
1496
+
1497
+ var family = familyNamed(name, 'getUtility');
1498
+ if (!family || !node.length) { return null; }
1499
+
1500
+ var key = view === undefined ? curView : viewKey(view);
1501
+ if (key === null) {
1502
+ warn('getUtility(' + JSON.stringify(view) + '): no such layout mode');
1503
+ return null;
1504
+ }
1505
+
1506
+ return readUtility(node, family, key);
1507
+ }
1508
+
1509
+ /**
1510
+ * setUtility(node, family, value, view?): write a value through the
1511
+ * events. Null is "inherit". The last argument is a view key, or
1512
+ * { view, source } from a plugin that wants its tool named in the
1513
+ * payload. False when a handler canceled or nothing changed.
1514
+ */
1515
+ function setUtility(node, name, value, options) {
1516
+ options = typeof options == 'string' ? { view: options } : (options || {});
1517
+ node = $(node).first();
1518
+
1519
+ var family = familyNamed(name, 'setUtility');
1520
+ if (!family || !node.length) { return false; }
1521
+
1522
+ var view = options.view === undefined ? curView : viewKey(options.view);
1523
+ if (view === null) {
1524
+ warn('setUtility(' + JSON.stringify(options.view) + '): no such layout mode');
1525
+ return false;
1526
+ }
1527
+
1528
+ value = value === null || value === undefined || value === '' ? null : String(value);
1529
+ if (value !== null && family.values.indexOf(value) === -1) {
1530
+ warn('setUtility: ' + JSON.stringify(value) + ' is not a value of "' + name +
1531
+ '", which takes ' + JSON.stringify(family.values));
1532
+ return false;
1533
+ }
1534
+
1535
+ var kind = kindOf(node);
1536
+ if (!appliesTo(family, kind)) {
1537
+ warn('setUtility: "' + name + '" does not apply to a ' + kind);
1538
+ return false;
1539
+ }
1540
+
1541
+ var plan = planUtility(node, family, view, value);
1542
+ if (!plan) { return false; }
1543
+
1544
+ return operate(function() {
1545
+ var payload = payloadFor(kind, node, {
1546
+ family: name,
1547
+ breakpoint: view,
1548
+ tiers: plan.writes.map(function(write) { return write.tier.key; }),
1549
+ from: readUtility(node, family, view),
1550
+ to: value,
1551
+ cleared: plan.cleared,
1552
+ source: options.source || 'api',
1553
+ });
1554
+
1555
+ if (!emit('before-utility', payload)) {
1556
+ // The panel already shows the value that was refused
1557
+ refreshUtilities(node);
1558
+ return false;
1559
+ }
1560
+
1561
+ plan.writes.forEach(function(write) {
1562
+ writeUtility(node, family, write.tier, write.value);
1563
+ });
1564
+ refreshUtilities(node);
1565
+ emit('after-utility', payload);
1566
+
1567
+ return true;
1568
+ });
1569
+ }
1570
+
1571
+ /** Bring a node's panel and preview up to date with its classes. */
1572
+ function refreshUtilities(node) {
1573
+ var details = node.children('.ge-tools-drawer').children('.ge-details');
1574
+
1575
+ details.children('.ge-classes').val(hostClasses(node).join(' '));
1576
+ details.children('.ge-utilities').each(function() { renderUtilities($(this)); });
1577
+ refreshPreviews(node);
1578
+ }
1579
+
1580
+ /* Preview */
1581
+
1582
+ function utilityNodes(scope) {
1583
+ return scope.find(UTILITY_NODES).addBack(UTILITY_NODES);
1584
+ }
1585
+
1586
+ /**
1587
+ * Put what each node's utilities mean at the view being edited on the
1588
+ * node itself. Only nodes that carry a class of a family get anything,
1589
+ * and they get the family's answer for the view even when that is
1590
+ * "nothing": a wider tier's class is still live in a wide window, and
1591
+ * has to be overruled.
1592
+ *
1593
+ * The all view gets nothing. Its canvas is not narrowed, every tier is
1594
+ * live, and what Bootstrap shows is the truth.
1595
+ */
1596
+ function refreshPreviews(scope) {
1597
+ clearPreviews(scope);
1598
+
1599
+ if (curView !== ALL_VIEW) { previewTier(scope, breakpoint(curView)); }
1600
+
1601
+ // Whatever a plugin marks the canvas with to show its utilities
1602
+ // goes stale at the same moments the preview does
1603
+ plugins('onRefresh', scope);
1604
+ }
1605
+
1606
+ function previewTier(scope, tier) {
1607
+ utilityNodes(scope).each(function() {
1608
+ var node = $(this);
1609
+ var kind = kindOf(node);
1610
+ var styles = {};
1611
+
1612
+ familiesFor(kind).forEach(function(family) {
1613
+ if (!family.preview || !utilityTiers(node, family).length) { return; }
1614
+
1615
+ $.extend(styles, family.preview(effectiveUtility(node, family, tier), node, kind));
1616
+ });
1617
+
1618
+ // A plugin whose families settle one property between them
1619
+ // previews the node as a whole
1620
+ $.each(UTILITIES, function(name, utility) {
1621
+ if (utility.preview) { $.extend(styles, utility.preview(node, kind, tier.key)); }
1622
+ });
1623
+
1624
+ if (!$.isEmptyObject(styles)) { applyPreview(node, styles); }
1625
+ });
1626
+ }
1627
+
1628
+ /**
1629
+ * Set inline !important styles, which is the one thing that beats
1630
+ * Bootstrap's own !important, and remember what each property was so
1631
+ * it can be put back. The record is an attribute rather than jQuery
1632
+ * data because a rich text editor rebuilds the DOM of the area it
1633
+ * edits, and the record has to come back with the node.
1634
+ */
1635
+ function applyPreview(node, styles) {
1636
+ var style = node[0].style;
1637
+ var was = {};
1638
+
1639
+ $.each(styles, function(property, value) {
1640
+ was[property] = [style.getPropertyValue(property), style.getPropertyPriority(property)];
1641
+ style.setProperty(property, String(value), 'important');
1642
+ });
1643
+
1644
+ node.attr(PREVIEW_ATTR, JSON.stringify(was));
1645
+ }
1646
+
1647
+ function clearPreviews(scope) {
1648
+ scope.find('[' + PREVIEW_ATTR + ']').addBack('[' + PREVIEW_ATTR + ']').each(function() {
1649
+ var node = $(this);
1650
+ var style = this.style;
1651
+ var was = {};
1652
+
1653
+ try { was = JSON.parse(node.attr(PREVIEW_ATTR)) || {}; } catch (error) { /* a mangled record: drop it */ }
1654
+
1655
+ $.each(was, function(property, before) {
1656
+ if (before && before[0]) {
1657
+ style.setProperty(property, before[0], before[1]);
1658
+ } else {
1659
+ style.removeProperty(property);
1660
+ }
1661
+ });
1662
+
1663
+ node.removeAttr(PREVIEW_ATTR);
1664
+ if (!node.attr('style')) { node.removeAttr('style'); }
1665
+ });
1666
+ }
1667
+
1668
+ /* The panel */
1669
+
1670
+ /**
1671
+ * The Responsive section of a node's settings panel: one field per
1672
+ * family that applies to the node, reading and writing the view being
1673
+ * edited, and whatever fields a plugin builds itself. Folded until the
1674
+ * user unfolds one, and then unfolded on every node, since whoever
1675
+ * wanted it on one wants it on the next.
1676
+ */
1677
+ function createUtilitiesSection(node) {
1678
+ var kind = kindOf(node);
1679
+ var fields = familiesFor(kind)
1680
+ .filter(function(family) { return family.panel !== false; })
1681
+ .map(function(family) { return createField(node, family); });
1682
+
1683
+ $.each(UTILITIES, function(name, utility) {
1684
+ var own = utility.panel ? utility.panel(node, kind) : null;
1685
+ if (own && own.length) { fields.push(own); }
1686
+ });
1687
+
1688
+ if (!fields.length) { return null; }
1689
+
1690
+ var section = $('<div class="ge-utilities" />')
1691
+ .toggleClass('ge-open', utilitiesOpen)
1692
+ .data('ge-node', node)
1693
+ ;
1694
+
1695
+ $('<a class="ge-utilities-toggle" />')
1696
+ .appendTo(section)
1697
+ .on('click', function() {
1698
+ utilitiesOpen = !section.hasClass('ge-open');
1699
+ canvas.find('.ge-utilities').toggleClass('ge-open', utilitiesOpen);
1700
+ })
1701
+ ;
1702
+
1703
+ var body = $('<div class="ge-utilities-body" />').appendTo(section);
1704
+ fields.forEach(function(field) { field.appendTo(body); });
1705
+
1706
+ renderUtilities(section);
1707
+
1708
+ return section;
1709
+ }
1710
+
1711
+ /**
1712
+ * One family's field: its label, a select that writes through
1713
+ * setUtility, and a note. Filled by renderField, again whenever the
1714
+ * view or the node's classes change.
1715
+ */
1716
+ function createField(node, family) {
1717
+ var field = $('<label class="ge-utility" />').attr('data-ge-family', family.name);
1718
+
1719
+ $('<span class="ge-utility-label" />')
1720
+ .text(family.labelKey ? t(family.labelKey) : family.name)
1721
+ .appendTo(field)
1722
+ ;
1723
+ $('<select />')
1724
+ .appendTo(field)
1725
+ .on('change', function() {
1726
+ setUtility(node, family.name, this.value, { source: 'panel' });
1727
+ })
1728
+ ;
1729
+ $('<small class="ge-utility-note" />').appendTo(field);
1730
+
1731
+ return field;
1732
+ }
1733
+
1734
+ /** ge.utilityField(node, family): a field a plugin places in a panel of its own. */
1735
+ function utilityField(node, name) {
1736
+ var family = familyNamed(name, 'utilityField');
1737
+ if (!family) { return $(); }
1738
+
1739
+ var field = createField(node, family);
1740
+ renderField(field, node);
1741
+
1742
+ return field;
1743
+ }
1744
+
1745
+ /** Fill a section's fields, the families' and the plugins' own, for the view being edited. */
1746
+ function renderUtilities(section) {
1747
+ var node = section.data('ge-node');
1748
+
1749
+ section.children('.ge-utilities-toggle')
1750
+ .text(t('utility.section', { view: t(labelKeyFor(curView)) }));
1751
+
1752
+ section.find('.ge-utility').each(function() { renderField($(this), node); });
1753
+ }
1754
+
1755
+ function renderField(field, node) {
1756
+ var family = FAMILIES[field.attr('data-ge-family')].family;
1757
+ var select = field.children('select').empty();
1758
+ var choices = family.choices ? family.choices(node, kindOf(node)) : family.values;
1759
+ var own, blank, note = '';
1760
+
1761
+ if (curView === ALL_VIEW) {
1762
+ own = ownUtility(node, family, BREAKPOINTS[0]);
1763
+ blank = t('utility.default');
1764
+
1765
+ var varies = utilityTiers(node, family).filter(function(entry) {
1766
+ return entry.tier !== BREAKPOINTS[0];
1767
+ });
1768
+ if (varies.length) {
1769
+ note = t('utility.varies', {
1770
+ breakpoints: varies.map(function(entry) { return entry.tier.key; }).join(', '),
1771
+ });
1772
+ }
1773
+ } else {
1774
+ var tier = breakpoint(curView);
1775
+ var inherited = inheritedUtility(node, family, tier);
1776
+
1777
+ own = ownUtility(node, family, tier);
1778
+ blank = inherited
1779
+ ? t('utility.inherit', { value: labelOf(family, inherited.value), breakpoint: inherited.tier.key })
1780
+ : t('utility.default');
1781
+ }
1782
+
1783
+ // A value the markup carries is shown even when the family
1784
+ // would not offer it here, rather than shown as something else
1785
+ if (own !== null && choices.indexOf(own) === -1) { choices = choices.concat([own]); }
1786
+
1787
+ $('<option value="" />').text(blank).appendTo(select);
1788
+ choices.forEach(function(value) {
1789
+ $('<option />').attr('value', value).text(labelOf(family, value)).appendTo(select);
1790
+ });
1791
+
1792
+ select.val(own === null ? '' : own);
1793
+ field.children('.ge-utility-note').text(note).toggle(note !== '');
1794
+ }
1795
+
1796
+ function labelOf(family, value) {
1797
+ return family.label ? family.label(value) : value;
1798
+ }
1799
+
1208
1800
  /* --------------------------------------------------------------
1209
1801
  * Containers: tabs, accordions and popups.
1210
1802
  *
@@ -1220,6 +1812,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1220
1812
 
1221
1813
  var CONTAINERS = {}; // The container plugins in use, by the type each builds
1222
1814
  var FEATURES = {}; // The feature plugins in use, by name
1815
+ var UTILITIES = {}; // The utility plugins in use, by name
1223
1816
  var featureMethods = {}; // The methods those features contribute
1224
1817
  var containerCounter = 0;
1225
1818
 
@@ -1526,9 +2119,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1526
2119
 
1527
2120
  var room = spare(row, leadingTier());
1528
2121
 
1529
- // The drawer it hangs off is raised while it is open: every
1530
- // drawer sits above jQuery UI's handles, so without this the
1531
- // drawer of the column below takes the clicks meant for the picker
2122
+ // The drawer it hangs off is raised while it is open: drawers sit
2123
+ // below the resize handles, so without this the drawer of the
2124
+ // column below takes the clicks meant for the picker
1532
2125
  tool.closest('.ge-tools-drawer').addClass('ge-picker-open');
1533
2126
 
1534
2127
  sizePicker = $('<div class="ge-size-picker" />').appendTo(tool);
@@ -1788,6 +2381,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1788
2381
  details.toggle();
1789
2382
  });
1790
2383
 
2384
+ // Beside the gear, because every node that has one is a node a
2385
+ // utility may apply to, whichever plugin built its drawer
2386
+ var kind = kindOf(node);
2387
+ $.each(UTILITIES, function(name, utility) {
2388
+ if (utility.drawerTools) { utility.drawerTools(drawer, node, kind); }
2389
+ });
2390
+
1791
2391
  return details.appendTo(drawer);
1792
2392
  }
1793
2393
 
@@ -1816,6 +2416,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1816
2416
  .appendTo(detailsDiv)
1817
2417
  .on('change', function() {
1818
2418
  setHostClasses(container, this.value);
2419
+ refreshUtilities(container);
1819
2420
  })
1820
2421
  ;
1821
2422
 
@@ -1828,11 +2429,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1828
2429
  .on('click', function() {
1829
2430
  btn.toggleClass('active btn-primary');
1830
2431
  container.toggleClass(rowClass.cssClass, btn.hasClass('active'));
2432
+ refreshUtilities(container);
1831
2433
  })
1832
2434
  .appendTo(classGroup)
1833
2435
  ;
1834
2436
  });
1835
2437
 
2438
+ var utilities = createUtilitiesSection(container);
2439
+ if (utilities) { utilities.appendTo(detailsDiv); }
2440
+
1836
2441
  return detailsDiv;
1837
2442
  }
1838
2443
 
@@ -1981,9 +2586,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1981
2586
  }
1982
2587
 
1983
2588
  /**
1984
- * Drag resize leaves an inline pixel width behind, and jQuery UI adds
1985
- * its own. Neither belongs in the markup a host saves, or in the
1986
- * canvas once the size class has been written.
2589
+ * Drag resize leaves an inline pixel width behind: the column follows
2590
+ * the pointer in pixels while the gesture lasts. It does not belong in
2591
+ * the markup a host saves, or in the canvas once the size class has
2592
+ * been written.
1987
2593
  */
1988
2594
  function stripPixelWidths(scope) {
1989
2595
  scope.find('.column').addBack('.column').each(function() {
@@ -1997,112 +2603,196 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1997
2603
  });
1998
2604
  }
1999
2605
 
2000
- function makeSortable() {
2001
- var wholeDrawer = settings.drag_handle === 'drawer';
2002
-
2003
- var shared = {
2004
- handle: wholeDrawer ? '> .ge-tools-drawer' : '> .ge-tools-drawer .ge-move',
2606
+ /**
2607
+ * A group name, scoped to this editor.
2608
+ *
2609
+ * Two editors on one page must not drag into each other, since a node
2610
+ * carries its drawer - and the events that go with it - from the
2611
+ * editor that made it.
2612
+ */
2613
+ function groupName(name) {
2614
+ return 'ge-' + name + '-' + instanceId;
2615
+ }
2005
2616
 
2617
+ /** What must never start a drag, whatever the handle is. */
2618
+ function dragCancelSelector() {
2619
+ return settings.drag_handle === 'drawer'
2006
2620
  // With the whole drawer as the handle, the tools inside it are
2007
2621
  // still tools: a drag starting on one would swallow its click,
2008
2622
  // and the settings panel has fields to type in
2009
- cancel: wholeDrawer
2010
- ? '.ge-tools-drawer > a, .ge-details, input, textarea, button, select, option'
2011
- : 'input, textarea, button, select, option',
2623
+ ? '.ge-tools-drawer > a, .ge-details, input, textarea, button, select, option'
2624
+ : 'input, textarea, button, select, option';
2625
+ }
2012
2626
 
2013
- start: sortStart,
2014
- stop: sortStop,
2015
- helper: 'clone',
2016
- };
2627
+ /**
2628
+ * Every sortable list the editor makes, core's and a plugin's alike,
2629
+ * is made here.
2630
+ *
2631
+ * A caller says what moves (`draggable`) and what the list connects to
2632
+ * (`group`, or none for a list that sorts only within itself); the
2633
+ * handle, the filter, the callbacks and the drag settings are this
2634
+ * function's business. That is the whole point of it: the toolkit
2635
+ * underneath is named in one place, so replacing it is one function
2636
+ * and not thirty call sites.
2637
+ */
2638
+ function sortable(lists, options) {
2639
+ if (!lists.length || !window.Sortable) { return; }
2017
2640
 
2018
- canvas.find('.row').sortable($.extend({
2019
- items: '> .column',
2020
- connectWith: '.ge-canvas .row',
2021
- tolerance: 'pointer',
2022
- }, shared, settings.sortable_options));
2641
+ var wholeDrawer = settings.drag_handle === 'drawer';
2642
+ var cancel = dragCancelSelector();
2643
+ var soloGroup = 0;
2644
+
2645
+ lists.each(function() {
2646
+ var list = this;
2647
+
2648
+ // A list with no group sorts only within itself, which is what
2649
+ // a tab strip wants: a name of its own, closed both ways
2650
+ var group = options.group
2651
+ ? { name: groupName(options.group) }
2652
+ : { name: groupName(options.draggable + '-' + (++soloGroup)), pull: false, put: false };
2653
+
2654
+ sortables.push(window.Sortable.create(list, $.extend({
2655
+ group: group,
2656
+ draggable: options.draggable,
2657
+ handle: wholeDrawer ? '.ge-tools-drawer' : '.ge-tools-drawer .ge-move',
2658
+
2659
+ /**
2660
+ * Two refusals in one, because SortableJS asks once.
2661
+ *
2662
+ * A tool or a form field never starts a drag, as the
2663
+ * cancel list said before. And only a direct child of
2664
+ * this list moves: the selector is matched against every
2665
+ * descendant, so without this the canvas would pick up a
2666
+ * row nested three columns down and drag that.
2667
+ */
2668
+ filter: function(e, item) {
2669
+ if ($(e.target).closest(cancel, list).length) { return true; }
2670
+
2671
+ return !item || item.parentNode !== list;
2672
+ },
2673
+
2674
+ // A filtered pointerdown is still a click on a tool
2675
+ preventOnFilter: false,
2676
+
2677
+ // The HTML5 drag and drop API cannot be driven by
2678
+ // synthetic events, so the tests could not exist without
2679
+ // this; it also gives one helper across browsers
2680
+ forceFallback: true,
2681
+ fallbackOnBody: true,
2682
+
2683
+ ghostClass: 'ge-drag-placeholder',
2684
+ chosenClass: 'ge-drag-chosen',
2685
+ dragClass: 'ge-drag-helper',
2686
+ fallbackClass: 'ge-drag-helper',
2687
+
2688
+ animation: settings.drag.animation,
2689
+
2690
+ // One delay, which applies to both gestures or to touch
2691
+ // alone: a touch drag that starts instantly takes the
2692
+ // page's scrolling with it, a mouse drag has no such
2693
+ // problem, and asking for `delay` means asking for both
2694
+ delay: settings.drag.delay || settings.drag.touch_delay,
2695
+ delayOnTouchOnly: !settings.drag.delay,
2696
+
2697
+ touchStartThreshold: settings.drag.threshold,
2698
+ scroll: settings.drag.scroll,
2699
+
2700
+ onStart: sortStart,
2701
+ onEnd: sortEnd,
2702
+ }, options.options || {})));
2703
+ });
2704
+ }
2023
2705
 
2024
- canvas.add(canvas.find('.column')).sortable($.extend({
2025
- items: '> .row, > .ge-content',
2026
- connectWith: '.ge-canvas, .ge-canvas .column',
2027
- }, shared, settings.sortable_options));
2706
+ function makeSortable() {
2707
+ if (!window.Sortable) {
2708
+ warnOnceHere('sortable_missing', t('error.sortable_missing'));
2709
+ return;
2710
+ }
2028
2711
 
2029
- // A tab strip sorts its own tabs, and the panes follow them
2030
- canvas.find('.ge-container-tabs > .nav-tabs').sortable($.extend({
2031
- items: '> .ge-tab',
2032
- }, shared, settings.sortable_options));
2712
+ sortable(canvas.find('.row'), {
2713
+ draggable: '.column',
2714
+ group: 'column',
2715
+ });
2033
2716
 
2034
- // Accordion items sort within their accordion and into any other
2035
- canvas.find('.ge-container-accordion > .accordion').sortable($.extend({
2036
- items: '> .ge-accordion-item',
2037
- connectWith: '.ge-canvas .ge-container-accordion > .accordion',
2038
- }, shared, settings.sortable_options));
2717
+ sortable(canvas.add(canvas.find('.column')), {
2718
+ draggable: '.row, .ge-content, [data-ge-container]',
2719
+ group: 'block',
2720
+ });
2039
2721
 
2040
2722
  // A plugin makes its own: only it knows which of its parts move
2041
- plugins('onSortable', shared);
2042
-
2043
- /**
2044
- * jQuery UI cannot refuse a drag once it has started, so a
2045
- * canceled before-move is remembered here and undone on drop
2046
- * (spec 2.4). The node carries the mark, because with connected
2047
- * lists the drop is not always reported by the list that started
2048
- * the drag.
2049
- */
2050
- function sortStart(e, ui) {
2051
- ui.placeholder.css({ height: ui.item.outerHeight()});
2052
-
2053
- var node = ui.item;
2054
- var from = positionOf(node);
2055
-
2056
- node.data('ge-move-from', from);
2057
- node.removeData('ge-move-canceled');
2723
+ plugins('onSortable', sortable);
2724
+ }
2058
2725
 
2059
- operate(function() {
2060
- var moving = emit('before-move', payloadFor(kindOf(node), node, {
2061
- parent: from.parent,
2062
- source: 'dragdrop',
2063
- from: from,
2064
- }));
2726
+ /**
2727
+ * A drag cannot be refused once it has started, so a canceled
2728
+ * before-move is remembered here and undone on drop (spec 2.4). The
2729
+ * node carries the mark, because with connected lists the drop is not
2730
+ * always reported by the list that started the drag.
2731
+ */
2732
+ function sortStart(e) {
2733
+ var node = $(e.item);
2734
+ var from = positionOf(node);
2065
2735
 
2066
- if (!moving) { node.data('ge-move-canceled', true); }
2067
- });
2068
- }
2736
+ node.data('ge-move-from', from);
2737
+ node.removeData('ge-move-canceled');
2069
2738
 
2070
- function sortStop(e, ui) {
2071
- var node = ui.item;
2072
- var from = node.data('ge-move-from') || positionOf(node);
2739
+ operate(function() {
2740
+ var moving = emit('before-move', payloadFor(kindOf(node), node, {
2741
+ parent: from.parent,
2742
+ source: 'dragdrop',
2743
+ from: from,
2744
+ }));
2073
2745
 
2074
- node.removeData('ge-move-from');
2746
+ if (!moving) { node.data('ge-move-canceled', true); }
2747
+ });
2748
+ }
2075
2749
 
2076
- if (node.data('ge-move-canceled')) {
2077
- node.removeData('ge-move-canceled');
2078
- $(this).sortable('cancel');
2079
- return;
2080
- }
2750
+ /** Put a node back where a refused drag found it. */
2751
+ function putBack(node, from) {
2752
+ var siblings = from.parent.children().not('.ge-tools-drawer').not(node);
2081
2753
 
2082
- var to = positionOf(node);
2083
- if (to.parent[0] === from.parent[0] && to.index === from.index) {
2084
- return; // A drag that went nowhere is not a move
2085
- }
2754
+ if (!siblings.length || from.index >= siblings.length) {
2755
+ node.appendTo(from.parent);
2756
+ } else {
2757
+ node.insertBefore(siblings.eq(from.index));
2758
+ }
2759
+ }
2086
2760
 
2087
- var container = node.closest('[data-ge-container]');
2088
- var definition = CONTAINERS[containerTypeOf(container)];
2089
- if (definition && definition.afterPaneMove) {
2090
- definition.afterPaneMove(container, node, from);
2091
- }
2761
+ function sortEnd(e) {
2762
+ var node = $(e.item);
2763
+ var from = node.data('ge-move-from') || positionOf(node);
2092
2764
 
2093
- // No init() here, unlike an add: the node brought its drawer
2094
- // with it, and jQuery UI is still finishing the drag, so this
2095
- // is the wrong moment to rebuild the widgets it is using.
2096
- operate(function() {
2097
- emit('after-move', payloadFor(kindOf(node), node, {
2098
- parent: to.parent,
2099
- source: 'dragdrop',
2100
- from: from,
2101
- to: to,
2102
- container: container.length ? container : undefined,
2103
- }));
2104
- });
2765
+ node.removeData('ge-move-from');
2766
+
2767
+ if (node.data('ge-move-canceled')) {
2768
+ node.removeData('ge-move-canceled');
2769
+ putBack(node, from);
2770
+ return;
2771
+ }
2772
+
2773
+ var to = positionOf(node);
2774
+ if (to.parent[0] === from.parent[0] && to.index === from.index) {
2775
+ return; // A drag that went nowhere is not a move
2105
2776
  }
2777
+
2778
+ var container = node.closest('[data-ge-container]');
2779
+ var definition = CONTAINERS[containerTypeOf(container)];
2780
+ if (definition && definition.afterPaneMove) {
2781
+ definition.afterPaneMove(container, node, from);
2782
+ }
2783
+
2784
+ // No init() here, unlike an add: the node brought its drawer
2785
+ // with it, and the drag is still being finished, so this is the
2786
+ // wrong moment to rebuild the lists it is using.
2787
+ operate(function() {
2788
+ emit('after-move', payloadFor(kindOf(node), node, {
2789
+ parent: to.parent,
2790
+ source: 'dragdrop',
2791
+ from: from,
2792
+ to: to,
2793
+ container: container.length ? container : undefined,
2794
+ }));
2795
+ });
2106
2796
  }
2107
2797
 
2108
2798
  /**
@@ -2119,24 +2809,74 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2119
2809
 
2120
2810
  canvas.find('.column').each(function() {
2121
2811
  var col = $(this);
2122
- if (col.data('ui-resizable')) { return; }
2812
+ if (col.find('> .ge-resize-handle').length) { return; }
2123
2813
 
2124
2814
  $('<span class="ge-resize-size" />').appendTo(col.find('> .ge-tools-drawer'));
2125
2815
 
2126
- col.resizable($.extend({
2127
- handles: settings.resize.handles,
2128
- start: resizeStart,
2129
- resize: resizeMove,
2130
- stop: resizeStop,
2131
- }, settings.resizable_options));
2816
+ $.each(resizeEdges(), function(_, edge) {
2817
+ $('<span class="ge-resize-handle" />')
2818
+ .addClass('ge-resize-' + edge)
2819
+ .attr('data-ge-edge', edge)
2820
+ .on('pointerdown', startResizeDrag)
2821
+ .appendTo(col)
2822
+ ;
2823
+ });
2132
2824
  });
2133
2825
  }
2134
2826
 
2827
+ /** Which edges carry a handle: 'e', 'w', or both. */
2828
+ function resizeEdges() {
2829
+ return String(settings.resize.handles).split(',')
2830
+ .map(function(edge) { return edge.trim(); })
2831
+ .filter(function(edge) { return edge === 'e' || edge === 'w'; });
2832
+ }
2833
+
2834
+ /**
2835
+ * One resize gesture, from the pointer going down on a handle to it
2836
+ * coming up again.
2837
+ *
2838
+ * The pointer is captured, so a fast drag that leaves the column
2839
+ * behind keeps resizing it, and a refused before-resize simply never
2840
+ * starts: nothing is written and the column does not move.
2841
+ */
2842
+ function startResizeDrag(e) {
2843
+ var handle = $(e.currentTarget);
2844
+ var col = handle.parent();
2845
+ var west = handle.attr('data-ge-edge') === 'w';
2846
+ var startX = e.pageX;
2847
+ var startWidth = col.outerWidth();
2848
+
2849
+ e.preventDefault();
2850
+
2851
+ if (!resizeStart(col)) { return; }
2852
+
2853
+ col.addClass('ge-resizing');
2854
+ if (e.pointerId !== undefined && handle[0].setPointerCapture) {
2855
+ handle[0].setPointerCapture(e.pointerId);
2856
+ }
2857
+
2858
+ function widthAt(move) {
2859
+ var delta = move.pageX - startX;
2860
+
2861
+ return Math.max(1, startWidth + (west ? -delta : delta));
2862
+ }
2863
+
2864
+ function onMove(move) {
2865
+ col.css('width', widthAt(move) + 'px');
2866
+ resizeMove(col, widthAt(move));
2867
+ }
2868
+
2869
+ function onUp(up) {
2870
+ handle.off('pointermove', onMove).off('pointerup pointercancel', onUp);
2871
+ col.removeClass('ge-resizing');
2872
+ resizeStop(col, widthAt(up));
2873
+ }
2874
+
2875
+ handle.on('pointermove', onMove).on('pointerup pointercancel', onUp);
2876
+ }
2877
+
2135
2878
  function removeResizable() {
2136
- canvas.find('.column').each(function() {
2137
- var col = $(this);
2138
- if (col.data('ui-resizable')) { col.resizable('destroy'); }
2139
- });
2879
+ canvas.find('.ge-resize-handle').remove();
2140
2880
 
2141
2881
  canvas.find('.ge-resize-size').remove();
2142
2882
  stripPixelWidths(canvas);
@@ -2189,16 +2929,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2189
2929
  }
2190
2930
 
2191
2931
  /**
2192
- * A canceled before-resize refuses the drag.
2193
- *
2194
- * jQuery UI's resizable ignores false from its start handler - unlike
2195
- * draggable, and unlike what the spec assumed - so the refusal is
2196
- * carried on the column and every step of the drag returns false,
2197
- * which the widget does honour. Nothing is written and the column ends
2198
- * where it began.
2199
- */
2200
- function resizeStart(e, ui) {
2201
- var col = $(this);
2932
+ * A canceled before-resize refuses the gesture before it begins, so
2933
+ * nothing is written and the column does not move. Under jQuery UI
2934
+ * this took a flag on the column and a refusal from every step of the
2935
+ * drag, because resizable ignores false from its start handler.
2936
+ */
2937
+ function resizeStart(col) {
2202
2938
  var from = currentSize(col);
2203
2939
 
2204
2940
  var allowed = operate(function() {
@@ -2209,40 +2945,27 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2209
2945
  }));
2210
2946
  });
2211
2947
 
2212
- if (!allowed) {
2213
- col.data('ge-resize-refused', true);
2214
- return false;
2215
- }
2948
+ if (!allowed) { return false; }
2216
2949
 
2217
2950
  col.data('ge-resize-from', from);
2218
2951
  resizeReadout(col, sizeLabel(from));
2219
2952
 
2220
- return undefined;
2953
+ return true;
2221
2954
  }
2222
2955
 
2223
- function resizeMove(e, ui) {
2224
- var col = $(this);
2225
-
2226
- if (col.data('ge-resize-refused')) { return false; }
2227
-
2228
- resizeReadout(col, sizeLabel(snapUnits(col, ui.size.width)));
2229
-
2230
- return undefined;
2956
+ function resizeMove(col, width) {
2957
+ resizeReadout(col, sizeLabel(snapUnits(col, width)));
2231
2958
  }
2232
2959
 
2233
- function resizeStop(e, ui) {
2234
- var col = $(this);
2960
+ function resizeStop(col, width) {
2235
2961
  var from = col.data('ge-resize-from');
2236
- var units = snapUnits(col, ui.size.width);
2962
+ var units = snapUnits(col, width);
2237
2963
 
2238
2964
  col.removeData('ge-resize-from');
2239
2965
  resizeReadout(col, '');
2240
2966
  stripPixelWidths(col);
2241
2967
 
2242
- if (col.data('ge-resize-refused') || from === undefined) {
2243
- col.removeData('ge-resize-refused');
2244
- return;
2245
- }
2968
+ if (from === undefined) { return; }
2246
2969
 
2247
2970
  // A drag of a couple of pixels lands on the size it started from,
2248
2971
  // and is not a resize
@@ -2275,18 +2998,17 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2275
2998
  if (plan) { writeSize(next, plan); }
2276
2999
  }
2277
3000
 
3001
+ /**
3002
+ * Undo every list `sortable()` made, and only those.
3003
+ *
3004
+ * The registry is what makes that exact: deinit() is a public method,
3005
+ * so it can be called twice, and a list the host made sortable itself
3006
+ * is none of the editor's business.
3007
+ */
2278
3008
  function removeSortable() {
2279
- // Only where a sortable was actually made: deinit() is a public
2280
- // method now, and jQuery UI throws when asked to destroy a widget
2281
- // that is not there, so calling deinit() twice would fail.
2282
- // jQuery UI marks what it made, so a plugin's sortables come
2283
- // away with the editor's without the core knowing about them
2284
- canvas.find('.ui-sortable').addBack('.ui-sortable').each(function() {
2285
- var node = $(this);
2286
- if (node.data('ui-sortable')) {
2287
- node.sortable('destroy');
2288
- }
2289
- });
3009
+ $.each(sortables, function(_, instance) { instance.destroy(); });
3010
+
3011
+ sortables = [];
2290
3012
  }
2291
3013
 
2292
3014
  function createRow() {
@@ -2476,10 +3198,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2476
3198
  var child = $(this);
2477
3199
 
2478
3200
  // The editor's own furniture is not content and not a
2479
- // boundary either. jQuery UI's resize handle used to be
2480
- // treated as content and wrapped into a content area of
2481
- // its own on the next init.
2482
- if (child.is('.ge-tools-drawer, .ui-resizable-handle')) { return; }
3201
+ // boundary either. The resize handle used to be treated as
3202
+ // content and wrapped into a content area of its own on
3203
+ // the next init.
3204
+ if (child.is('.ge-tools-drawer, .ge-resize-handle')) { return; }
2483
3205
 
2484
3206
  // A container sits in the column beside the content
2485
3207
  // areas, not inside one, so it ends a run of loose
@@ -2556,7 +3278,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2556
3278
  return;
2557
3279
  }
2558
3280
 
3281
+ var from = curView;
2559
3282
  switchLayout(key);
3283
+
3284
+ if (key === from) { return; }
3285
+
3286
+ canvas.find('.ge-utilities').each(function() { renderUtilities($(this)); });
3287
+ refreshPreviews(canvas);
3288
+ plugins('onViewChange', key);
3289
+ emit('view-change', { canvas: canvas, breakpoint: key, from: from, to: key });
2560
3290
  }
2561
3291
 
2562
3292
  function getView() {
@@ -2600,6 +3330,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2600
3330
  return addPaneTo(container, 'accordion', options);
2601
3331
  },
2602
3332
  setLocale: setLocale,
3333
+ getUtility: getUtility,
3334
+ setUtility: setUtility,
2603
3335
  canvas: canvas,
2604
3336
  settings: settingsCopy(),
2605
3337
  };
@@ -2651,6 +3383,18 @@ $.fn.gridEditor.containers = {};
2651
3383
  */
2652
3384
  $.fn.gridEditor.features = {};
2653
3385
 
3386
+ /**
3387
+ * Utility plugins: Bootstrap's responsive utility classes - order-md-2,
3388
+ * d-lg-none - edited per breakpoint. A plugin declares families of classes
3389
+ * and the editor reads them, writes them, puts them in the settings panel and
3390
+ * previews them in each view. Same factory, same `plugins` setting.
3391
+ *
3392
+ * $.fn.gridEditor.utilities.order = function(ge) {
3393
+ * return { families: [{ name: 'order', prefix: 'order', values: [...] }] };
3394
+ * };
3395
+ */
3396
+ $.fn.gridEditor.utilities = {};
3397
+
2654
3398
  /** Translator for the editor integrations, which get settings and no instance. */
2655
3399
  $.fn.gridEditor.t = translate;
2656
3400
 
@@ -2702,6 +3446,12 @@ $.fn.gridEditor.locales = {
2702
3446
  'view.lg': 'Desktop',
2703
3447
  'view.xl': 'Large desktop',
2704
3448
  'view.xxl': 'Widescreen',
3449
+ 'utility.section': 'Responsive: {view}',
3450
+ 'utility.default': 'Default',
3451
+ 'utility.inherit': 'Inherit: {value} (from {breakpoint})',
3452
+ 'utility.varies': 'Changes at {breakpoints}; choosing here replaces that',
3453
+ 'error.sortable_missing': 'SortableJS not available! Make sure you loaded the Sortable js file; dragging is off without it.',
3454
+ 'warning.setting_removed': 'The {setting} setting was removed in 4.0. Use {replacement} instead.',
2705
3455
  'error.tinymce_missing': 'tinyMCE not available! Make sure you loaded the tinyMCE js file.',
2706
3456
  'error.ckeditor_missing': 'CKEditor not available! Make sure you loaded the ckeditor and jquery adapter js files.',
2707
3457
  'error.summernote_missing': 'Summernote not available! Make sure you loaded the Summernote js file.',