@themarioga/grid-editor 4.0.0 → 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 (51) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +53 -1
  3. package/dist/grideditor.css +148 -0
  4. package/dist/grideditor.min.css +1 -1
  5. package/dist/grideditor.min.css.map +1 -1
  6. package/dist/jquery.grideditor.bundle.min.js +2 -2
  7. package/dist/jquery.grideditor.js +555 -9
  8. package/dist/jquery.grideditor.min.js +1 -1
  9. package/dist/jquery.grideditor.min.js.map +1 -1
  10. package/dist/locales/grideditor.es.js +32 -0
  11. package/dist/locales/grideditor.es.min.js +1 -1
  12. package/dist/locales/grideditor.es.min.js.map +1 -1
  13. package/dist/plugins/grideditor.alignment.js +81 -0
  14. package/dist/plugins/grideditor.alignment.min.js +2 -0
  15. package/dist/plugins/grideditor.alignment.min.js.map +1 -0
  16. package/dist/plugins/grideditor.gutters.js +145 -0
  17. package/dist/plugins/grideditor.gutters.min.js +2 -0
  18. package/dist/plugins/grideditor.gutters.min.js.map +1 -0
  19. package/dist/plugins/grideditor.order.js +206 -0
  20. package/dist/plugins/grideditor.order.min.js +2 -0
  21. package/dist/plugins/grideditor.order.min.js.map +1 -0
  22. package/dist/plugins/grideditor.spacing.js +299 -0
  23. package/dist/plugins/grideditor.spacing.min.js +2 -0
  24. package/dist/plugins/grideditor.spacing.min.js.map +1 -0
  25. package/dist/plugins/grideditor.visibility.js +172 -0
  26. package/dist/plugins/grideditor.visibility.min.js +2 -0
  27. package/dist/plugins/grideditor.visibility.min.js.map +1 -0
  28. package/docs/events.md +20 -1
  29. package/docs/locale-keys.md +52 -0
  30. package/docs/plugins.md +108 -3
  31. package/example/autosave.html +5 -0
  32. package/example/basic.html +5 -0
  33. package/example/breakpoints.html +5 -0
  34. package/example/ckeditor.html +5 -0
  35. package/example/containers.html +4 -0
  36. package/example/elements.html +5 -0
  37. package/example/index.html +24 -1
  38. package/example/locale.html +5 -0
  39. package/example/plugins.html +5 -0
  40. package/example/summernote.html +5 -0
  41. package/example/utilities.html +145 -0
  42. package/example/wrap_content.html +5 -0
  43. package/package.json +1 -1
  44. package/src/js/jquery.grideditor.js +555 -9
  45. package/src/js/locales/grideditor.es.js +32 -0
  46. package/src/js/plugins/grideditor.alignment.js +81 -0
  47. package/src/js/plugins/grideditor.gutters.js +145 -0
  48. package/src/js/plugins/grideditor.order.js +206 -0
  49. package/src/js/plugins/grideditor.spacing.js +299 -0
  50. package/src/js/plugins/grideditor.visibility.js +172 -0
  51. package/src/less/grideditor.less +167 -0
@@ -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
  /**
@@ -258,7 +263,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
258
263
  'container_tools' : [], // Host tools on container drawers
259
264
  'tab_tools' : [], // Host tools on tab drawers
260
265
  'accordion_tools' : [], // Host tools on accordion item drawers
261
- '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
262
268
  'elements' : NESTED_SETTINGS.elements, // Element level controls, below the column
263
269
  'custom_filter' : '',
264
270
  'content_types' : ['tinymce'],
@@ -748,6 +754,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
748
754
  // when their editor is ready, and the drawers go back in.
749
755
  canvas.on('ge-rte-ready', '.ge-content', function() {
750
756
  plugins('onContentReady', $(this));
757
+ refreshPreviews($(this));
751
758
  });
752
759
  }
753
760
 
@@ -1136,6 +1143,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1136
1143
  makeSortable();
1137
1144
  makeResizable();
1138
1145
  switchLayout(curView);
1146
+ refreshPreviews(canvas);
1139
1147
  }
1140
1148
 
1141
1149
  function deinit() {
@@ -1156,6 +1164,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1156
1164
  hideDropMarker();
1157
1165
  canvas.find('.ge-tools-drawer').remove();
1158
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);
1159
1171
  unmarkContainers();
1160
1172
  removeSortable();
1161
1173
  removeResizable();
@@ -1236,6 +1248,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1236
1248
  if (wanted(name)) { FEATURES[name] = factory(api); }
1237
1249
  });
1238
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
+
1239
1260
  $.each(FEATURES, function(name, feature) {
1240
1261
  $.each(feature.methods || {}, function(method, implementation) {
1241
1262
  featureMethods[method] = implementation;
@@ -1243,7 +1264,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1243
1264
  });
1244
1265
 
1245
1266
  (settings.plugins || []).forEach(function(name) {
1246
- if (CONTAINERS[name] || FEATURES[name]) { return; }
1267
+ if (CONTAINERS[name] || FEATURES[name] || UTILITIES[name]) { return; }
1247
1268
 
1248
1269
  warnOnceHere('plugin:' + name, 'the "' + name + '" plugin is not loaded: ' +
1249
1270
  'include dist/plugins/grideditor.' + name + '.js after the editor');
@@ -1253,7 +1274,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1253
1274
  /**
1254
1275
  * A hook every loaded plugin may have. Containers first, since a
1255
1276
  * feature that looks at the canvas - elements, say - wants the
1256
- * containers already marked.
1277
+ * containers already marked, and utilities last, since they decorate
1278
+ * nodes the other two may have just made.
1257
1279
  */
1258
1280
  function plugins(hook, argument) {
1259
1281
  $.each(CONTAINERS, function(type, definition) {
@@ -1262,6 +1284,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1262
1284
  $.each(FEATURES, function(name, feature) {
1263
1285
  if (feature[hook]) { feature[hook](argument); }
1264
1286
  });
1287
+ $.each(UTILITIES, function(name, utility) {
1288
+ if (utility[hook]) { utility[hook](argument); }
1289
+ });
1265
1290
  }
1266
1291
 
1267
1292
  /** What a plugin is handed. See docs/plugins.md. */
@@ -1287,9 +1312,491 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1287
1312
  emit: emit,
1288
1313
  payloadFor: payloadFor,
1289
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,
1324
+ };
1325
+ }
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
+ }),
1290
1365
  };
1291
1366
  }
1292
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
+
1293
1800
  /* --------------------------------------------------------------
1294
1801
  * Containers: tabs, accordions and popups.
1295
1802
  *
@@ -1305,6 +1812,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1305
1812
 
1306
1813
  var CONTAINERS = {}; // The container plugins in use, by the type each builds
1307
1814
  var FEATURES = {}; // The feature plugins in use, by name
1815
+ var UTILITIES = {}; // The utility plugins in use, by name
1308
1816
  var featureMethods = {}; // The methods those features contribute
1309
1817
  var containerCounter = 0;
1310
1818
 
@@ -1873,6 +2381,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1873
2381
  details.toggle();
1874
2382
  });
1875
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
+
1876
2391
  return details.appendTo(drawer);
1877
2392
  }
1878
2393
 
@@ -1901,6 +2416,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1901
2416
  .appendTo(detailsDiv)
1902
2417
  .on('change', function() {
1903
2418
  setHostClasses(container, this.value);
2419
+ refreshUtilities(container);
1904
2420
  })
1905
2421
  ;
1906
2422
 
@@ -1913,11 +2429,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1913
2429
  .on('click', function() {
1914
2430
  btn.toggleClass('active btn-primary');
1915
2431
  container.toggleClass(rowClass.cssClass, btn.hasClass('active'));
2432
+ refreshUtilities(container);
1916
2433
  })
1917
2434
  .appendTo(classGroup)
1918
2435
  ;
1919
2436
  });
1920
2437
 
2438
+ var utilities = createUtilitiesSection(container);
2439
+ if (utilities) { utilities.appendTo(detailsDiv); }
2440
+
1921
2441
  return detailsDiv;
1922
2442
  }
1923
2443
 
@@ -2758,7 +3278,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2758
3278
  return;
2759
3279
  }
2760
3280
 
3281
+ var from = curView;
2761
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 });
2762
3290
  }
2763
3291
 
2764
3292
  function getView() {
@@ -2802,6 +3330,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2802
3330
  return addPaneTo(container, 'accordion', options);
2803
3331
  },
2804
3332
  setLocale: setLocale,
3333
+ getUtility: getUtility,
3334
+ setUtility: setUtility,
2805
3335
  canvas: canvas,
2806
3336
  settings: settingsCopy(),
2807
3337
  };
@@ -2853,6 +3383,18 @@ $.fn.gridEditor.containers = {};
2853
3383
  */
2854
3384
  $.fn.gridEditor.features = {};
2855
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
+
2856
3398
  /** Translator for the editor integrations, which get settings and no instance. */
2857
3399
  $.fn.gridEditor.t = translate;
2858
3400
 
@@ -2904,6 +3446,10 @@ $.fn.gridEditor.locales = {
2904
3446
  'view.lg': 'Desktop',
2905
3447
  'view.xl': 'Large desktop',
2906
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',
2907
3453
  'error.sortable_missing': 'SortableJS not available! Make sure you loaded the Sortable js file; dragging is off without it.',
2908
3454
  'warning.setting_removed': 'The {setting} setting was removed in 4.0. Use {replacement} instead.',
2909
3455
  'error.tinymce_missing': 'tinyMCE not available! Make sure you loaded the tinyMCE js file.',