@themarioga/grid-editor 5.1.2 → 5.3.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 (62) hide show
  1. package/BUILDING.md +6 -0
  2. package/CHANGELOG.md +58 -0
  3. package/README.md +46 -9
  4. package/UPGRADING.md +25 -0
  5. package/dist/grideditor.css +28 -0
  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 +2 -2
  9. package/dist/jquery.grideditor.js +760 -245
  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 +11 -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.ckeditor.js +120 -0
  16. package/dist/plugins/grideditor.ckeditor.min.js +2 -0
  17. package/dist/plugins/grideditor.ckeditor.min.js.map +1 -0
  18. package/dist/plugins/grideditor.clipboard.js +8 -7
  19. package/dist/plugins/grideditor.clipboard.min.js +1 -1
  20. package/dist/plugins/grideditor.clipboard.min.js.map +1 -1
  21. package/dist/plugins/grideditor.spacing.js +1 -1
  22. package/dist/plugins/grideditor.spacing.min.js +1 -1
  23. package/dist/plugins/grideditor.spacing.min.js.map +1 -1
  24. package/dist/plugins/grideditor.summernote.js +98 -0
  25. package/dist/plugins/grideditor.summernote.min.js +2 -0
  26. package/dist/plugins/grideditor.summernote.min.js.map +1 -0
  27. package/dist/plugins/grideditor.textalign.js +1 -1
  28. package/dist/plugins/grideditor.textalign.min.js +1 -1
  29. package/dist/plugins/grideditor.textalign.min.js.map +1 -1
  30. package/dist/plugins/grideditor.tinymce.js +162 -0
  31. package/dist/plugins/grideditor.tinymce.min.js +2 -0
  32. package/dist/plugins/grideditor.tinymce.min.js.map +1 -0
  33. package/dist/plugins/grideditor.visibility.js +3 -3
  34. package/dist/plugins/grideditor.visibility.min.js +1 -1
  35. package/dist/plugins/grideditor.visibility.min.js.map +1 -1
  36. package/docs/events.md +5 -2
  37. package/docs/locale-keys.md +23 -1
  38. package/docs/plugins.md +61 -4
  39. package/example/attributes.html +4 -2
  40. package/example/autosave.html +2 -0
  41. package/example/basic.html +2 -0
  42. package/example/ckeditor.html +2 -0
  43. package/example/clipboard.html +2 -0
  44. package/example/containers.html +2 -0
  45. package/example/elements.html +2 -0
  46. package/example/summernote.html +2 -0
  47. package/example/wrap_content.html +2 -0
  48. package/package.json +1 -1
  49. package/src/js/compat/bundled-texts.js +14 -0
  50. package/src/js/jquery.grideditor.js +444 -33
  51. package/src/js/locales/grideditor.es.js +11 -0
  52. package/src/js/plugins/grideditor.ckeditor.js +120 -0
  53. package/src/js/plugins/grideditor.clipboard.js +8 -7
  54. package/src/js/plugins/grideditor.spacing.js +1 -1
  55. package/src/js/plugins/grideditor.summernote.js +98 -0
  56. package/src/js/plugins/grideditor.textalign.js +1 -1
  57. package/src/js/plugins/grideditor.tinymce.js +162 -0
  58. package/src/js/plugins/grideditor.visibility.js +3 -3
  59. package/src/less/grideditor.less +34 -0
  60. package/src/js/jquery.grideditor.ckeditor.js +0 -77
  61. package/src/js/jquery.grideditor.summernote.js +0 -72
  62. package/src/js/jquery.grideditor.tinymce.js +0 -143
@@ -29,6 +29,7 @@ var METHODS = {
29
29
  createRow: { value: true },
30
30
  createColumn: { value: true },
31
31
  createElement: { value: true },
32
+ createText: { value: true },
32
33
  createSection: { value: true },
33
34
  createContainer: { value: true },
34
35
  addTab: { value: true },
@@ -338,6 +339,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
338
339
  'toolbar_drag' : 'auto', // Drag the toolbar's buttons onto the canvas. 'auto' follows drag_handle
339
340
  'element_tools' : [], // Host tools on element drawers, same shape as row_tools
340
341
  'element_classes' : [], // Preset class toggles on an element's settings panel
342
+ 'text_tools' : [], // Host tools on text block drawers
343
+ 'text_classes' : [], // Preset class toggles on a text block's settings panel
341
344
  'container_classes' : [], // The same, on a container's panel
342
345
  'pane_classes' : [], // And on a tab's or an accordion item's
343
346
  'container_tools' : [], // Host tools on container drawers
@@ -831,7 +834,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
831
834
  if (node.hasClass('row')) { return 'row'; }
832
835
  if (node.hasClass('column')) { return 'column'; }
833
836
  if (node.hasClass('ge-element')) { return 'element'; }
834
- if (node.hasClass('ge-content')) { return 'content'; }
837
+ if (node.hasClass('ge-content') || node.hasClass('ge-text-block')) { return 'text'; }
835
838
  return 'node';
836
839
  }
837
840
 
@@ -879,6 +882,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
879
882
  // which costs the element drawers inside it. The integrations say
880
883
  // when their editor is ready, and the drawers go back in.
881
884
  canvas.on('ge-rte-ready', '.ge-content', function() {
885
+ // The first time only: an undo says ready again, and by then
886
+ // the host may have changed the content area itself
887
+ if ($(this).data('ge-text-before') && !$(this).data('ge-text-ready')) {
888
+ $(this).data('ge-text-ready', attributesOf(this));
889
+ }
890
+
882
891
  plugins('onContentReady', $(this));
883
892
  refreshPreviews($(this));
884
893
  });
@@ -936,6 +945,35 @@ $.fn.gridEditor = function( optionsOrMethod ) {
936
945
 
937
946
  addContainerGroup.appendTo(wrapper);
938
947
 
948
+ // A text block of each editor offered. Like a container, it goes
949
+ // into a row of its own when clicked, or where it is dropped
950
+ var texts = offeredTexts();
951
+ texts.forEach(function(type) {
952
+ var label = texts.length > 1 ? t('text.add_type', { editor: textLabel(type) }) : t('text.add');
953
+
954
+ $('<a class="btn btn-sm btn-primary ge-add-container ge-add-text-button" />')
955
+ .attr('title', label)
956
+ .attr('data-ge-toolbar', 'text')
957
+ .attr('data-ge-text', type)
958
+ .append('<i class="bi bi-plus"></i>')
959
+ .append($('<span />').text(label))
960
+ .on('click', function() {
961
+ var row = createRow();
962
+ var block = makeText(type);
963
+ createColumn(MAX_COL_SIZE).appendTo(row).find('> .ge-content').replaceWith(block);
964
+
965
+ var added = addNode('text', block, function() {
966
+ row.appendTo(canvas);
967
+ }, { parent: canvas, source: 'tool' });
968
+
969
+ if (added && row[0].scrollIntoView) {
970
+ row[0].scrollIntoView({behavior: 'smooth'});
971
+ }
972
+ })
973
+ .appendTo(addContainerGroup)
974
+ ;
975
+ });
976
+
939
977
  // A container starts in a row of its own, the way the add row
940
978
  // buttons next to these ones do
941
979
  $.each(CONTAINERS, function(type, definition) {
@@ -1226,6 +1264,28 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1226
1264
  }, { parent: where.region, source: item.source || 'dragdrop' });
1227
1265
  }
1228
1266
 
1267
+ /**
1268
+ * A text block dropped from the toolbar: where it lands in a column,
1269
+ * or in a row and a column of its own anywhere else.
1270
+ */
1271
+ function insertTextFromToolbar(button, where) {
1272
+ var made = makeText(button.attr('data-ge-text'));
1273
+ var placed = made;
1274
+
1275
+ if (!where.region.is('.column')) {
1276
+ placed = createRow();
1277
+ createColumn(MAX_COL_SIZE).appendTo(placed).find('> .ge-content').replaceWith(made);
1278
+ }
1279
+
1280
+ return addNode('text', made, function() {
1281
+ if (where.before) {
1282
+ placed.insertBefore(where.before);
1283
+ } else {
1284
+ placed.appendTo(where.region);
1285
+ }
1286
+ }, { parent: where.region, source: 'dragdrop' });
1287
+ }
1288
+
1229
1289
  /** A line where the block would go, following the pointer. */
1230
1290
  function showDropMarker(pageX, pageY) {
1231
1291
  var where = dropPlaceAt(pageX, pageY);
@@ -1255,6 +1315,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1255
1315
  if (button.attr('data-ge-toolbar') === 'feature') {
1256
1316
  return insertFeatureFromToolbar(button, where);
1257
1317
  }
1318
+ if (button.attr('data-ge-toolbar') === 'text') {
1319
+ return insertTextFromToolbar(button, where);
1320
+ }
1258
1321
 
1259
1322
  var container = button.attr('data-ge-toolbar') === 'container';
1260
1323
  var type = button.attr('data-ge-container-type');
@@ -1404,11 +1467,28 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1404
1467
  // its toolbar out against, and nothing anyone can type into
1405
1468
  if (!$(this).is(':visible')) { return; }
1406
1469
 
1407
- var rte = getRTE($(this).data('ge-content-type'));
1408
- if (rte) {
1409
- $(this).addClass('ge-rte-active', true);
1410
- rte.init(settings, $(this));
1470
+ // The attribute, not jQuery's cached copy of it: it is markup, and
1471
+ // the drawer reads it the same way
1472
+ var type = $(this).attr('data-ge-content-type');
1473
+ var text = textFor(type);
1474
+ if (!text) { return; }
1475
+
1476
+ // Not marked active, so the next click tries again: the library
1477
+ // may be loaded by then
1478
+ if (text.available && !text.available()) {
1479
+ if (text.missingKey) { console.error(t(text.missingKey)); }
1480
+ return;
1481
+ }
1482
+
1483
+ if (text.bundled) {
1484
+ warnOnceHere('text-bundled:' + type, 'the ' + type + ' text editor is the copy in the ' +
1485
+ 'main bundle, which 6.0 will not include: load dist/plugins/grideditor.' + type +
1486
+ '.js after the editor');
1411
1487
  }
1488
+
1489
+ $(this).data('ge-text-before', attributesOf(this)).removeData('ge-text-ready');
1490
+ $(this).addClass('ge-rte-active');
1491
+ text.start($(this));
1412
1492
  }
1413
1493
 
1414
1494
  function reset() {
@@ -1422,6 +1502,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1422
1502
  canvas.toggleClass('ge-drag-drawer', settings.drag_handle === 'drawer');
1423
1503
  addAllColClasses();
1424
1504
  wrapContent();
1505
+ wrapTexts();
1425
1506
  createRowControls();
1426
1507
  createColControls();
1427
1508
  markContainers();
@@ -1434,21 +1515,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1434
1515
 
1435
1516
  function deinit() {
1436
1517
  canvas.removeClass('ge-editing ge-drag-drawer ge-dropping');
1437
- var contents = canvas.find('.ge-content').each(function() {
1438
- var content = $(this);
1439
- var rte = getRTE(content.data('ge-content-type'));
1440
- if (rte) {
1441
- rte.deinit(settings, content);
1442
- }
1443
- // Cleared after rte.deinit, not before: an editor can restore the
1444
- // class attribute it snapshotted when it was created, which would
1445
- // leave ge-rte-active in place and make initRTE ignore every later
1446
- // click on this content area.
1447
- content.removeClass('ge-rte-active');
1518
+ canvas.find('.ge-content').each(function() {
1519
+ closeText($(this));
1448
1520
  });
1449
1521
  closeSizePicker();
1450
1522
  hideDropMarker();
1451
1523
  canvas.find('.ge-tools-drawer').remove();
1524
+ unwrapTexts();
1452
1525
  plugins('onDeinit');
1453
1526
  // After the rich text editors have let go of their content areas:
1454
1527
  // one that rebuilt its area's DOM brought the preview styles back
@@ -1555,6 +1628,23 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1555
1628
  if (wanted(name)) { FEATURES[name] = factory(api); }
1556
1629
  });
1557
1630
 
1631
+ // Text editors are chosen by content_types rather than by the
1632
+ // plugins setting: a page that names its containers there has
1633
+ // named no editor, and still wants the one it uses
1634
+ $.each($.fn.gridEditor.texts, function(type, factory) {
1635
+ TEXTS[type] = $.extend({ bundled: !!factory.bundled }, factory(api));
1636
+ });
1637
+
1638
+ // 5.x's registry, for an integration a host wrote itself. One the
1639
+ // plugins above already provide is theirs.
1640
+ $.each($.fn.gridEditor.RTEs, function(type, rte) {
1641
+ if (TEXTS[type]) { return; }
1642
+
1643
+ warnOnceHere('rtes:' + type, '$.fn.gridEditor.RTEs is deprecated and will be removed ' +
1644
+ 'in 7.0: register "' + type + '" under $.fn.gridEditor.texts, see docs/plugins.md');
1645
+ TEXTS[type] = legacyText(rte);
1646
+ });
1647
+
1558
1648
  $.each($.fn.gridEditor.utilities, function(name, factory) {
1559
1649
  if (!wanted(name)) { return; }
1560
1650
 
@@ -1631,6 +1721,16 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1631
1721
  bareStyle: bareStyle,
1632
1722
  rowFromLayout: rowFromLayoutValue,
1633
1723
  nodeHtml: nodeHtml,
1724
+ // A text editor has rewritten a content area, so whatever the
1725
+ // editor and its plugins had put in there goes back in
1726
+ textReady: function(block) { block.trigger('ge-rte-ready'); },
1727
+ // A node's drawer: its first child, or for a content area the
1728
+ // one beside it in its text block
1729
+ drawerOf: function(node) {
1730
+ return node.hasClass('ge-content')
1731
+ ? node.parent('.ge-text-block').children('.ge-tools-drawer')
1732
+ : node.children('.ge-tools-drawer');
1733
+ },
1634
1734
  toolbarItems: function(name) {
1635
1735
  return mainControls
1636
1736
  ? mainControls.find('[data-ge-toolbar="feature"][data-ge-feature="' + name + '"]')
@@ -1657,7 +1757,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1657
1757
  * -------------------------------------------------------------- */
1658
1758
 
1659
1759
  var FAMILIES = {}; // Every family the utility plugins declare, by name
1660
- var UTILITY_NODES = '.row, .column, .ge-element, [data-ge-container]';
1760
+ var UTILITY_NODES = '.row, .column, .ge-content, .ge-element, [data-ge-container]';
1661
1761
  var PREVIEW_ATTR = 'data-ge-preview';
1662
1762
  var utilitiesOpen = false; // Whether the panels show their Responsive section unfolded
1663
1763
 
@@ -2164,6 +2264,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2164
2264
  * -------------------------------------------------------------- */
2165
2265
 
2166
2266
  var CONTAINERS = {}; // The container plugins in use, by the type each builds
2267
+ var TEXTS = {}; // The text editor plugins, by the content type each edits
2167
2268
  var FEATURES = {}; // The feature plugins in use, by name
2168
2269
  var UTILITIES = {}; // The utility plugins in use, by name
2169
2270
  var featureMethods = {}; // The methods those features contribute
@@ -2444,6 +2545,14 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2444
2545
  function attachSizePicker(tool, row) {
2445
2546
  if (!settings.add_column.picker) { return; }
2446
2547
 
2548
+ attachPicker(tool, function() { openSizePicker(tool, row); });
2549
+ }
2550
+
2551
+ /**
2552
+ * Hold a tool - or rest the pointer on it - and `open` offers a
2553
+ * choice under it. The add text tool offers its editors the same way.
2554
+ */
2555
+ function attachPicker(tool, open) {
2447
2556
  var timer = null;
2448
2557
 
2449
2558
  var cancel = function() {
@@ -2456,7 +2565,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2456
2565
 
2457
2566
  timer = window.setTimeout(function() {
2458
2567
  timer = null;
2459
- openSizePicker(tool, row);
2568
+ open();
2460
2569
  }, settings.add_column.delay);
2461
2570
  });
2462
2571
 
@@ -2606,6 +2715,22 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2606
2715
  });
2607
2716
  });
2608
2717
 
2718
+ // A text block of the first editor offered; held, the tool
2719
+ // offers each of them when there is more than one
2720
+ var texts = offeredTexts();
2721
+ if (texts.length) {
2722
+ createTool(drawer, t('tool.add_text'), 'ge-add-text', 'bi bi-type', function() {
2723
+ if (closeSizePicker()) { return; } // The picker was open: that was the answer
2724
+
2725
+ addTextTo(col, texts[0]);
2726
+ });
2727
+
2728
+ if (texts.length > 1) {
2729
+ var textTool = drawer.find('> .ge-add-text');
2730
+ attachPicker(textTool, function() { openTextPicker(textTool, col); });
2731
+ }
2732
+ }
2733
+
2609
2734
  createTool(drawer, t('tool.add_row'), 'ge-add-row', 'bi bi-plus-circle', function() {
2610
2735
  // An empty row: the columns in it are the next decision,
2611
2736
  // and its drawer's add column tool is where that is made
@@ -2720,7 +2845,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2720
2845
  }
2721
2846
 
2722
2847
  function createTool(drawer, title, className, iconClass, eventHandlers) {
2723
- var tool = $('<a title="' + title + '" class="' + className + '"><i class="' + iconClass + '"></i></a>')
2848
+ // Through attr rather than spliced into markup: a title is text,
2849
+ // and a quote in it - a type name, a host's own title - would
2850
+ // end the attribute early
2851
+ var tool = $('<a />')
2852
+ .attr('title', title)
2853
+ .attr('class', className)
2854
+ .append($('<i />').attr('class', iconClass))
2724
2855
  .appendTo(drawer)
2725
2856
  ;
2726
2857
  if (typeof eventHandlers == 'function') {
@@ -3154,11 +3285,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3154
3285
 
3155
3286
  /**
3156
3287
  * What the canvas, the columns and the plugins' regions move: rows,
3157
- * content areas and containers, and whatever blocks a feature adds -
3158
- * the sections plugin's sections.
3288
+ * text blocks and containers, and whatever blocks a feature adds -
3289
+ * the sections plugin's sections. A content area is a block through
3290
+ * the text block around it while editing, and on its own before.
3159
3291
  */
3160
3292
  function blockSelector() {
3161
- return ['.row', '.ge-content', '[data-ge-container]'].concat(pluginHooks('blocks')).join(', ');
3293
+ return ['.row', '.ge-text-block', '.ge-content', '[data-ge-container]']
3294
+ .concat(pluginHooks('blocks')).join(', ');
3162
3295
  }
3163
3296
 
3164
3297
  /** A selector every feature plugin may contribute to, under one name. */
@@ -3324,12 +3457,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3324
3457
  function sortStart(e) {
3325
3458
  var node = $(e.item);
3326
3459
  var from = positionOf(node);
3460
+ var subject = moveSubject(node);
3327
3461
 
3328
3462
  node.data('ge-move-from', from);
3329
3463
  node.removeData('ge-move-canceled');
3330
3464
 
3331
3465
  operate(function() {
3332
- var moving = emit('before-move', payloadFor(kindOf(node), node, {
3466
+ var moving = emit('before-move', payloadFor(subject.kind, subject.node, {
3333
3467
  parent: from.parent,
3334
3468
  source: 'dragdrop',
3335
3469
  from: from,
@@ -3339,6 +3473,19 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3339
3473
  });
3340
3474
  }
3341
3475
 
3476
+ /**
3477
+ * What a drag reports as moving. A text block is the editor's own
3478
+ * wrapper, so what moved is the content area inside it, and until 6.0
3479
+ * a content area moves as kind 'content', as it did in 5.x - adding
3480
+ * and deleting one, which 5.x never announced, already say 'text'.
3481
+ */
3482
+ function moveSubject(item) {
3483
+ var node = item.hasClass('ge-text-block') ? item.children('.ge-content') : item;
3484
+ var kind = kindOf(node);
3485
+
3486
+ return { kind: kind === 'text' ? 'content' : kind, node: node };
3487
+ }
3488
+
3342
3489
  /** Put a node back where a refused drag found it. */
3343
3490
  function putBack(node, from) {
3344
3491
  var siblings = from.parent.children().not('.ge-tools-drawer').not(node);
@@ -3376,8 +3523,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3376
3523
  // No init() here, unlike an add: the node brought its drawer
3377
3524
  // with it, and the drag is still being finished, so this is the
3378
3525
  // wrong moment to rebuild the lists it is using.
3526
+ var subject = moveSubject(node);
3527
+
3379
3528
  operate(function() {
3380
- emit('after-move', payloadFor(kindOf(node), node, {
3529
+ emit('after-move', payloadFor(subject.kind, subject.node, {
3381
3530
  parent: to.parent,
3382
3531
  source: 'dragdrop',
3383
3532
  from: from,
@@ -3770,9 +3919,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3770
3919
  * the all view. `offset` indents it, within the same 12 unit budget.
3771
3920
  */
3772
3921
  function createColumn(size, offset) {
3773
- var rte = getRTE(settings.content_types[0]);
3922
+ var text = textFor(settings.content_types[0]);
3774
3923
  var column = $('<div class="column"/>')
3775
- .append(createDefaultContentWrapper().html(rte ? rte.initialContent : ''))
3924
+ .append(createDefaultContentWrapper().html(text ? text.initialContent : ''))
3776
3925
  ;
3777
3926
 
3778
3927
  // The tier being edited, or the base class in the all view
@@ -3820,7 +3969,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3820
3969
  // A container sits in the column beside the content
3821
3970
  // areas, not inside one, so it ends a run of loose
3822
3971
  // content rather than joining it
3823
- if (child.is('.row, .ge-content, [data-ge-container]')) {
3972
+ if (child.is('.row, .ge-content, .ge-text-block, [data-ge-container]')) {
3824
3973
  contents = doWrap(contents);
3825
3974
  } else {
3826
3975
  contents = contents.add(child);
@@ -3847,11 +3996,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3847
3996
  }
3848
3997
 
3849
3998
  /**
3850
- * A content area for the first content type. With no content types
3851
- * it is a content area and nothing more: there is no type to name.
3999
+ * A content area for a content type, the first one by default. With
4000
+ * no content types it is a content area and nothing more: there is no
4001
+ * type to name.
3852
4002
  */
3853
- function createDefaultContentWrapper() {
3854
- var type = settings.content_types[0];
4003
+ function createDefaultContentWrapper(type) {
4004
+ type = type || settings.content_types[0];
3855
4005
  var contentArea = $('<div class="ge-content"/>');
3856
4006
 
3857
4007
  if (type) {
@@ -3861,6 +4011,236 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3861
4011
  return contentArea;
3862
4012
  }
3863
4013
 
4014
+ /* --------------------------------------------------------------
4015
+ * Text blocks: a content area as a block of its own, with a drawer.
4016
+ *
4017
+ * The drawer cannot go inside the content area, as every other
4018
+ * block's does: what is inside is the text editor's, which would make
4019
+ * the drawer editable text, copy it into its own document or take it
4020
+ * away on an undo. So while editing, each content area in a column
4021
+ * sits in a .ge-text-block beside its drawer, and that wrapper is the
4022
+ * block the column moves. deinit takes it off again: it is never part
4023
+ * of the markup.
4024
+ * -------------------------------------------------------------- */
4025
+
4026
+ /** The text editors this editor offers, in content_types order: the ones loaded. */
4027
+ function offeredTexts() {
4028
+ return settings.content_types.filter(function(type) { return !!TEXTS[type]; });
4029
+ }
4030
+
4031
+ function wrapTexts() {
4032
+ canvas.find('.column > .ge-content').each(function() {
4033
+ $(this).wrap('<div class="ge-text-block" />');
4034
+ });
4035
+
4036
+ canvas.find('.ge-text-block').each(function() {
4037
+ var textBlock = $(this);
4038
+ if (!textBlock.children('.ge-tools-drawer').length) { createTextControls(textBlock); }
4039
+ });
4040
+ }
4041
+
4042
+ /** After the drawers are gone, so a wrapper holds its content area and nothing else. */
4043
+ function unwrapTexts() {
4044
+ canvas.find('.ge-text-block').each(function() {
4045
+ $(this).replaceWith($(this).contents());
4046
+ });
4047
+ }
4048
+
4049
+ function createTextControls(textBlock) {
4050
+ var block = textBlock.children('.ge-content');
4051
+ var type = block.attr('data-ge-content-type');
4052
+ var drawer = $('<div class="ge-tools-drawer ge-text-drawer" />').prependTo(textBlock);
4053
+
4054
+ createMoveTool(drawer);
4055
+
4056
+ // Which editor edits it, or that none can: a type whose plugin is
4057
+ // not loaded is still a block to move and delete, but not to edit
4058
+ if (type && TEXTS[type]) {
4059
+ createTool(drawer, t('tool.text_info', { editor: textLabel(type) }), 'ge-text-info', 'bi bi-fonts');
4060
+ } else if (type) {
4061
+ createTool(drawer, t('text.no_editor', { type: type }), 'ge-text-info ge-text-missing',
4062
+ 'bi bi-exclamation-triangle');
4063
+ }
4064
+
4065
+ addSettingsTool(drawer, block, settings.text_classes);
4066
+
4067
+ settings.text_tools.forEach(function(hostTool) {
4068
+ createTool(drawer, hostTool.title || '', hostTool.className || '',
4069
+ hostTool.iconClass || 'bi bi-wrench', hostTool.on);
4070
+ });
4071
+
4072
+ createTool(drawer, t('tool.delete_text'), 'ge-delete-text', 'bi bi-trash', function() {
4073
+ deleteNode('text', block, t('confirm.delete_text'), function(removed) {
4074
+ // Its editor lets go first: removed while open, it would
4075
+ // be left behind, attached to nothing
4076
+ closeText(block);
4077
+ textBlock.slideUp(function() {
4078
+ textBlock.remove();
4079
+ removed();
4080
+ });
4081
+ });
4082
+ });
4083
+ }
4084
+
4085
+ /**
4086
+ * Close the text editor on one content area, and leave the content
4087
+ * area's own attributes as the host left them.
4088
+ *
4089
+ * An editor adds attributes of its own while it is open, and takes
4090
+ * them off again, more or less: CKEditor leaves aria-readonly behind.
4091
+ * tinyMCE does worse, and puts back every attribute as it was when it
4092
+ * opened, so an id, a class or a plugin's attribute given while it was
4093
+ * open is lost. So the attributes are read three times - before the
4094
+ * editor opens, once it has opened, and before it closes - and what
4095
+ * changed between the second and the third, the host's doing, is
4096
+ * played back onto the first, the host's markup. What the editor did
4097
+ * is left out either way.
4098
+ */
4099
+ function closeText(block) {
4100
+ var text = textFor(block.attr('data-ge-content-type'));
4101
+ var before = block.data('ge-text-before');
4102
+ var open = before ? attributesOf(block[0]) : null;
4103
+
4104
+ // Every content area is told, as in 5.x: one whose editor never
4105
+ // started is the plugin's to ignore
4106
+ if (text) { text.stop(block); }
4107
+
4108
+ // After stop, not before: an editor that restores the class
4109
+ // attribute it snapshotted would put ge-rte-active back
4110
+ block.removeClass('ge-rte-active');
4111
+
4112
+ if (before) {
4113
+ restoreAttributes(block, before, block.data('ge-text-ready') || before, open);
4114
+ block.removeData('ge-text-before').removeData('ge-text-ready');
4115
+ }
4116
+ }
4117
+
4118
+ /** What a text editor puts on a content area while it is open, whenever it likes. */
4119
+ var EDITOR_CLASS = /^(mce-|cke|note-)|^(active|ge-rte-active)$/;
4120
+ var EDITOR_ATTRIBUTE = /^(data-mce-|contenteditable$|spellcheck$)/;
4121
+
4122
+ function attributesOf(element) {
4123
+ var found = {};
4124
+ $.each($.makeArray(element.attributes), function(i, attribute) {
4125
+ found[attribute.name] = attribute.value;
4126
+ });
4127
+ return found;
4128
+ }
4129
+
4130
+ function classesIn(value) {
4131
+ return (value || '').split(/\s+/).filter(function(name) {
4132
+ return name !== '' && !EDITOR_CLASS.test(name);
4133
+ });
4134
+ }
4135
+
4136
+ /** The host's markup, with what changed from `ready` to `open` played back onto it. */
4137
+ function restoreAttributes(block, before, ready, open) {
4138
+ var result = $.extend({}, before);
4139
+
4140
+ $.each($.extend({}, ready, open), function(name) {
4141
+ if (name === 'class' || EDITOR_ATTRIBUTE.test(name) || ready[name] === open[name]) { return; }
4142
+
4143
+ if (open[name] === undefined) {
4144
+ delete result[name];
4145
+ } else {
4146
+ result[name] = open[name];
4147
+ }
4148
+ });
4149
+
4150
+ var readyClasses = classesIn(ready['class']);
4151
+ var openClasses = classesIn(open['class']);
4152
+ var classes = classesIn(before['class']).filter(function(name) {
4153
+ return openClasses.indexOf(name) !== -1 || readyClasses.indexOf(name) === -1;
4154
+ });
4155
+ openClasses.forEach(function(name) {
4156
+ if (readyClasses.indexOf(name) === -1 && classes.indexOf(name) === -1) { classes.push(name); }
4157
+ });
4158
+
4159
+ // In the host's order, class where it was: getHtml should not
4160
+ // shuffle a node's attributes because it was edited
4161
+ if (classes.length) { result['class'] = classes.join(' '); } else { delete result['class']; }
4162
+
4163
+ $.each(attributesOf(block[0]), function(name) { block.removeAttr(name); });
4164
+ $.each(result, function(name, value) { block.attr(name, value); });
4165
+ }
4166
+
4167
+ /** What a text editor is called: its plugin's label, or its type. */
4168
+ function textLabel(type) {
4169
+ var text = TEXTS[type];
4170
+ return text && text.labelKey ? t(text.labelKey) : type;
4171
+ }
4172
+
4173
+ /** A detached content area of `type`, holding `content` or the editor's initial content. */
4174
+ function makeText(type, content) {
4175
+ var text = TEXTS[type];
4176
+ return createDefaultContentWrapper(type)
4177
+ .html(content !== undefined ? content : (text && text.initialContent) || '');
4178
+ }
4179
+
4180
+ /** A text block at the end of a column, through the add events. */
4181
+ function addTextTo(col, type) {
4182
+ var block = makeText(type);
4183
+
4184
+ return addNode('text', block, function() {
4185
+ col.append(block);
4186
+ }, { parent: col, source: 'tool' });
4187
+ }
4188
+
4189
+ /**
4190
+ * createText(type?, options?): a content area of a text editor's,
4191
+ * detached unless a placement is given. The type is the first one
4192
+ * offered by default. `options.content` is its html.
4193
+ */
4194
+ function apiCreateText(type, options) {
4195
+ if (type && typeof type === 'object') {
4196
+ options = type;
4197
+ type = undefined;
4198
+ }
4199
+ options = options || {};
4200
+ type = type || offeredTexts()[0];
4201
+
4202
+ if (!type || !TEXTS[type]) {
4203
+ warnOnceHere('createText:' + type, 'createText: no text editor "' + type + '" is loaded; ' +
4204
+ 'load its plugin and name it in content_types');
4205
+ return null;
4206
+ }
4207
+
4208
+ return place(makeText(type, options.content), 'text', options);
4209
+ }
4210
+
4211
+ /**
4212
+ * The text editors offered, as a strip under the add text tool, when
4213
+ * there is more than one to choose from.
4214
+ */
4215
+ function openTextPicker(tool, col) {
4216
+ closeSizePicker();
4217
+
4218
+ tool.closest('.ge-tools-drawer').addClass('ge-picker-open');
4219
+ sizePicker = $('<div class="ge-size-picker ge-text-picker" />').appendTo(tool);
4220
+
4221
+ offeredTexts().forEach(function(type) {
4222
+ $('<a class="ge-size ge-size-flex" />')
4223
+ .attr('data-ge-content-type', type)
4224
+ .attr('title', t('tool.add_text_type', { editor: textLabel(type) }))
4225
+ .text(textLabel(type))
4226
+ .on('click', function(e) {
4227
+ e.preventDefault();
4228
+ e.stopPropagation();
4229
+
4230
+ closeSizePicker();
4231
+ addTextTo(col, type);
4232
+ })
4233
+ .appendTo(sizePicker)
4234
+ ;
4235
+ });
4236
+
4237
+ tool.one('mouseleave', function() {
4238
+ window.setTimeout(function() {
4239
+ if (sizePicker && !sizePicker.is(':hover')) { closeSizePicker(); }
4240
+ }, 400);
4241
+ });
4242
+ }
4243
+
3864
4244
  /**
3865
4245
  * Constrain the canvas to the view's preview width and make that
3866
4246
  * tier's classes the effective ones. The all view constrains nothing:
@@ -3915,8 +4295,21 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3915
4295
  return curView;
3916
4296
  }
3917
4297
 
3918
- function getRTE(type) {
3919
- return $.fn.gridEditor.RTEs[type];
4298
+ /** The text editor plugin for a content type, or null. */
4299
+ function textFor(type) {
4300
+ return TEXTS[type] || null;
4301
+ }
4302
+
4303
+ /**
4304
+ * An integration written to 5.x's contract - init and deinit over a
4305
+ * set of content areas - as a text editor plugin.
4306
+ */
4307
+ function legacyText(rte) {
4308
+ return {
4309
+ initialContent: rte.initialContent,
4310
+ start: function(block) { rte.init(settings, block); },
4311
+ stop: function(block) { rte.deinit(settings, block); },
4312
+ };
3920
4313
  }
3921
4314
 
3922
4315
  /**
@@ -3938,6 +4331,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3938
4331
  getView: getView,
3939
4332
  createRow: apiCreateRow,
3940
4333
  createColumn: apiCreateColumn,
4334
+ createText: apiCreateText,
3941
4335
  createElement: function(content, options) {
3942
4336
  if (!featureMethods.createElement) {
3943
4337
  warnOnceHere('plugin:elements', 'createElement needs the elements plugin: ' +
@@ -3991,6 +4385,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3991
4385
 
3992
4386
  };
3993
4387
 
4388
+ /**
4389
+ * Text editor plugins: tinyMCE, CKEditor, summernote, and whatever a host
4390
+ * writes. A factory registered under the content type it edits, called once
4391
+ * per editor with the handle described in docs/plugins.md, returning
4392
+ * { labelKey, initialContent, available, missingKey, start, stop }.
4393
+ */
4394
+ $.fn.gridEditor.texts = {};
4395
+
4396
+ /** 5.x's registry of text editors, adapted with a warning. Removed in 7.0. */
3994
4397
  $.fn.gridEditor.RTEs = {};
3995
4398
 
3996
4399
  /**
@@ -4046,6 +4449,13 @@ $.fn.gridEditor.locales = {
4046
4449
  'tool.move': 'Move',
4047
4450
  'tool.settings': 'Settings',
4048
4451
  'tool.add_row': 'Add row',
4452
+ 'tool.add_text': 'Add text',
4453
+ 'tool.add_text_type': 'Add {editor} text',
4454
+ 'tool.text_info': 'Text: {editor}',
4455
+ 'tool.delete_text': 'Remove text',
4456
+ 'text.add': 'Text',
4457
+ 'text.add_type': 'Text ({editor})',
4458
+ 'text.no_editor': 'No text editor "{type}" is loaded: this text can be moved and deleted, not edited',
4049
4459
  'tool.add_column': 'Add column\n(hold to choose the width)',
4050
4460
  'tool.column_size': '{size} of 12',
4051
4461
  'tool.column_equal': 'Equal: shares what the row has left',
@@ -4073,6 +4483,7 @@ $.fn.gridEditor.locales = {
4073
4483
  'confirm.cancel': 'Cancel',
4074
4484
  'confirm.delete_row': 'Delete row?',
4075
4485
  'confirm.delete_column': 'Delete column?',
4486
+ 'confirm.delete_text': 'Delete this text?',
4076
4487
  'confirm.delete_container': 'Delete this container and everything in it?',
4077
4488
  'view.all': 'All sizes',
4078
4489
  'view.xs': 'Phone',
@@ -4101,30 +4512,235 @@ $.fn.gridEditor.locales = {
4101
4512
  };
4102
4513
 
4103
4514
  })( jQuery );
4515
+ /**
4516
+ * tinyMCE for grid-editor's content areas.
4517
+ *
4518
+ * A text editor plugin: load this file after the editor, and tinyMCE 6
4519
+ * after or before it, and content_types: ['tinymce'] edits each content area
4520
+ * with an inline tinyMCE. What it can ask the editor for is the handle its
4521
+ * factory is called with, described in docs/plugins.md.
4522
+ *
4523
+ * <script src="tinymce/tinymce.min.js"></script>
4524
+ * <script src="dist/jquery.grideditor.min.js"></script>
4525
+ * <script src="dist/plugins/grideditor.tinymce.min.js"></script>
4526
+ *
4527
+ * Up to 6.0 the main bundle carries a copy of this file too.
4528
+ */
4104
4529
  (function($) {
4105
- $.fn.gridEditor.RTEs.ckeditor = {
4106
4530
 
4107
- init: function(settings, contentAreas) {
4531
+ $.extend($.fn.gridEditor.locales.en, {
4532
+ 'text.tinymce': 'tinyMCE',
4533
+ });
4108
4534
 
4109
- if (!window.CKEDITOR) {
4110
- console.error($.fn.gridEditor.t(settings, 'error.ckeditor_missing'));
4111
- }
4535
+ // tinyMCE snapshots the target element's attributes when an inline editor is
4536
+ // created and restores them on remove(), so this has to run *after* remove()
4537
+ // to keep the grid editor's own class and tinyMCE's leftovers off the element.
4538
+ function cleanUp(contentArea) {
4539
+ contentArea
4540
+ .removeClass('active')
4541
+ .removeClass('ge-rte-active')
4542
+ .removeAttr('id')
4543
+ .removeAttr('style')
4544
+ .removeAttr('spellcheck')
4545
+ .removeAttr('contenteditable')
4546
+ .removeAttr('data-mce-style')
4547
+ ;
4548
+ }
4549
+
4550
+ var INITIAL_CONTENT = '<p>Lorem ipsum dolores</p>';
4551
+
4552
+ $.fn.gridEditor.texts.tinymce = function(ge) {
4553
+ return {
4554
+ labelKey: 'text.tinymce',
4555
+ initialContent: INITIAL_CONTENT,
4556
+ missingKey: 'error.tinymce_missing',
4557
+
4558
+ available: function() { return !!window.tinymce; },
4559
+
4560
+ start: function(contentAreas) {
4561
+ var settings = ge.settings;
4562
+ var userConfig = (settings.tinymce && settings.tinymce.config) ? settings.tinymce.config : {};
4112
4563
 
4113
- var self = this;
4114
- contentAreas.each(function() {
4115
- var contentArea = $(this);
4116
- if (!contentArea.hasClass('active')) {
4117
- if (contentArea.html() == self.initialContent) {
4118
- // CKEditor kills this '&nbsp' creating a non usable box :/
4119
- contentArea.html('&nbsp;');
4564
+ contentAreas.each(function() {
4565
+ var contentArea = $(this);
4566
+ if (contentArea.hasClass('active')) { return; }
4567
+
4568
+ if (contentArea.html() == INITIAL_CONTENT) {
4569
+ contentArea.html('');
4120
4570
  }
4121
-
4571
+ contentArea.addClass('active');
4572
+
4573
+ var configuration = $.extend({
4574
+ // tinyMCE's own "Upgrade" badge in the menubar. Off by
4575
+ // default because an inline editor here is a column of
4576
+ // someone's page, not tinyMCE's own interface; a host that
4577
+ // wants it back passes promotion: true.
4578
+ promotion: false,
4579
+ }, userConfig, {
4580
+ target: this,
4581
+ inline: true,
4582
+ init_instance_callback: function(editor) {
4583
+ // deinit ran before this init finished, so tear it down again
4584
+ if (contentArea.data('ge-tinymce-pending-remove')) {
4585
+ contentArea.removeData('ge-tinymce-pending-remove');
4586
+ editor.remove();
4587
+ // deinit already ran and cannot clean up after this
4588
+ // late remove(), so do it here instead
4589
+ cleanUp(contentArea);
4590
+ return;
4591
+ }
4592
+
4593
+ contentArea.data('ge-tinymce', editor);
4594
+
4595
+ // The editor rewrote what is inside this content area
4596
+ // while it took it over, so whatever the grid editor
4597
+ // had in there is gone. Saying so lets it put its own
4598
+ // furniture back.
4599
+ ge.textReady(contentArea);
4600
+
4601
+ // Undo and redo rewrite it too, from snapshots that
4602
+ // leave out whatever is marked data-mce-bogus - an
4603
+ // element's drawer among them - so it goes back in
4604
+ // after each of those as well
4605
+ editor.on('Undo Redo', function() {
4606
+ ge.textReady(contentArea);
4607
+ });
4608
+
4609
+ // The inline toolbar is laid out against the element's
4610
+ // geometry at the moment tinyMCE draws it, and an
4611
+ // element that has just appeared - a tab pane, an
4612
+ // accordion body, a column whose width is still
4613
+ // settling - may not have its own width yet. Asking
4614
+ // for the ui again once the browser has laid the frame
4615
+ // out measures it as it now is, instead of leaving a
4616
+ // toolbar wrapped into a narrow column.
4617
+ window.requestAnimationFrame(function() {
4618
+ if (editor.removed || !editor.ui || !editor.ui.show) { return; }
4619
+
4620
+ editor.ui.show();
4621
+ });
4622
+
4623
+ // And again whenever the user comes back to this
4624
+ // editor: by then the element may have been resized,
4625
+ // hidden and shown again - a tab switched away from
4626
+ // and back, a column made narrower - and the toolbar
4627
+ // is only ever as right as its last measurement.
4628
+ editor.on('focus', function() {
4629
+ window.requestAnimationFrame(function() {
4630
+ if (editor.removed || !editor.ui || !editor.ui.show) { return; }
4631
+
4632
+ editor.ui.show();
4633
+ });
4634
+ });
4635
+
4636
+ // Bring focus to text field
4637
+ editor.focus();
4638
+
4639
+ // Call the original callbacks, if any were passed in the config
4640
+ if (userConfig.init_instance_callback) {
4641
+ userConfig.init_instance_callback.call(this, editor);
4642
+ }
4643
+ // 'oninit' is the pre-6 name, honoured so existing configs keep working
4644
+ if (userConfig.oninit) {
4645
+ userConfig.oninit.call(this, editor);
4646
+ }
4647
+ }
4648
+ });
4649
+ // We always edit the element we were handed, so a selector in the
4650
+ // user config would only fight with target
4651
+ delete configuration.selector;
4652
+
4653
+ window.tinymce.init(configuration);
4654
+ });
4655
+ },
4656
+
4657
+ stop: function(contentAreas) {
4658
+ contentAreas.filter('.active').each(function() {
4659
+ var contentArea = $(this);
4660
+ var editor = contentArea.data('ge-tinymce');
4661
+
4662
+ if (editor) {
4663
+ contentArea.removeData('ge-tinymce');
4664
+ editor.remove();
4665
+ } else {
4666
+ // tinymce.init() is asynchronous, so the editor may not exist
4667
+ // yet. Leave a note for init_instance_callback to remove it.
4668
+ contentArea.data('ge-tinymce-pending-remove', true);
4669
+ }
4670
+
4671
+ cleanUp(contentArea);
4672
+ });
4673
+ },
4674
+ };
4675
+ };
4676
+ })(jQuery);
4677
+
4678
+ /**
4679
+ * CKEditor for grid-editor's content areas.
4680
+ *
4681
+ * A text editor plugin: load this file after the editor, and CKEditor 4
4682
+ * after or before it, and content_types: ['ckeditor'] edits each content area
4683
+ * with an inline CKEditor. What it can ask the editor for is the handle its
4684
+ * factory is called with, described in docs/plugins.md.
4685
+ *
4686
+ * <script src="ckeditor/ckeditor.js"></script>
4687
+ * <script src="dist/jquery.grideditor.min.js"></script>
4688
+ * <script src="dist/plugins/grideditor.ckeditor.min.js"></script>
4689
+ *
4690
+ * Up to 6.0 the main bundle carries a copy of this file too.
4691
+ */
4692
+ (function($) {
4693
+
4694
+ $.extend($.fn.gridEditor.locales.en, {
4695
+ 'text.ckeditor': 'CKEditor',
4696
+ });
4697
+
4698
+ var INITIAL_CONTENT = '<p>Lorem initius... </p>';
4699
+
4700
+ $.fn.gridEditor.texts.ckeditor = function(ge) {
4701
+
4702
+ /**
4703
+ * The instance editing this content area. Kept in jQuery data when it
4704
+ * is made, and looked for among CKEditor's own as a fallback, since
4705
+ * an instance made by another version of this file has no data.
4706
+ */
4707
+ function instanceOf(contentArea) {
4708
+ var kept = contentArea.data('ge-ckeditor');
4709
+ if (kept) { return kept; }
4710
+
4711
+ var found = null;
4712
+ $.each(window.CKEDITOR.instances, function(name, instance) {
4713
+ if (instance.element && instance.element.$ === contentArea[0]) { found = instance; }
4714
+ });
4715
+
4716
+ return found;
4717
+ }
4718
+
4719
+ return {
4720
+ labelKey: 'text.ckeditor',
4721
+ initialContent: INITIAL_CONTENT,
4722
+ missingKey: 'error.ckeditor_missing',
4723
+
4724
+ available: function() { return !!window.CKEDITOR; },
4725
+
4726
+ start: function(contentAreas) {
4727
+ var settings = ge.settings;
4728
+
4729
+ contentAreas.each(function() {
4730
+ var contentArea = $(this);
4731
+ if (contentArea.hasClass('active')) { return; }
4732
+
4733
+ if (contentArea.html() == INITIAL_CONTENT) {
4734
+ // CKEditor kills this '&nbsp' creating a non usable box :/
4735
+ contentArea.html('&nbsp;');
4736
+ }
4737
+
4122
4738
  // Add the .attr('contenteditable',''true') or CKEditor loads readonly
4123
4739
  contentArea.addClass('active').attr('contenteditable', 'true');
4124
-
4740
+
4125
4741
  var configuration = $.extend(
4126
4742
  {},
4127
- (settings.ckeditor && settings.ckeditor.config ? settings.ckeditor.config : {}),
4743
+ (settings.ckeditor && settings.ckeditor.config ? settings.ckeditor.config : {}),
4128
4744
  {
4129
4745
  // Focus editor on creation
4130
4746
  on: {
@@ -4143,56 +4759,86 @@ $.fn.gridEditor.locales = {
4143
4759
  // The editor owns what is inside the
4144
4760
  // content area now, so the grid editor is
4145
4761
  // told to put its own furniture back
4146
- contentArea.trigger('ge-rte-ready');
4762
+ ge.textReady(contentArea);
4147
4763
 
4148
4764
  instance.focus();
4149
4765
  }
4150
4766
  }
4151
4767
  }
4152
4768
  );
4153
- var instance = CKEDITOR.inline(contentArea.get(0), configuration);
4154
- }
4155
- });
4156
- },
4157
-
4158
- deinit: function(settings, contentAreas) {
4159
- contentAreas.filter('.active').each(function() {
4160
- var contentArea = $(this);
4161
-
4162
- // Destroy all CKEditor instances
4163
- $.each(CKEDITOR.instances, function(_, instance) {
4164
- instance.destroy();
4769
+ var instance = window.CKEDITOR.inline(contentArea.get(0), configuration);
4770
+ contentArea.data('ge-ckeditor', instance);
4165
4771
  });
4772
+ },
4166
4773
 
4167
- // Cleanup
4168
- contentArea
4169
- .removeClass('active cke_focus')
4170
- .removeAttr('id')
4171
- .removeAttr('style')
4172
- .removeAttr('spellcheck')
4173
- .removeAttr('contenteditable')
4174
- ;
4175
- });
4176
- },
4177
-
4178
- initialContent: '<p>Lorem initius... </p>',
4774
+ stop: function(contentAreas) {
4775
+ contentAreas.filter('.active').each(function() {
4776
+ var contentArea = $(this);
4777
+
4778
+ // This content area's instance, and no other: up to 5.x
4779
+ // closing one content area destroyed every CKEditor on
4780
+ // the page, other editors' and the host's own included
4781
+ var instance = window.CKEDITOR ? instanceOf(contentArea) : null;
4782
+ if (instance) { instance.destroy(); }
4783
+ contentArea.removeData('ge-ckeditor');
4784
+
4785
+ // Cleanup
4786
+ contentArea
4787
+ .removeClass('active cke_focus')
4788
+ .removeAttr('id')
4789
+ .removeAttr('style')
4790
+ .removeAttr('spellcheck')
4791
+ .removeAttr('contenteditable')
4792
+ ;
4793
+ });
4794
+ },
4795
+ };
4179
4796
  };
4180
4797
  })(jQuery);
4798
+
4799
+ /**
4800
+ * Summernote for grid-editor's content areas.
4801
+ *
4802
+ * A text editor plugin: load this file after the editor, and summernote
4803
+ * after or before it, and content_types: ['summernote'] edits each content
4804
+ * area with summernote in air mode. What it can ask the editor for is the
4805
+ * handle its factory is called with, described in docs/plugins.md.
4806
+ *
4807
+ * <script src="summernote/summernote-bs5.min.js"></script>
4808
+ * <script src="dist/jquery.grideditor.min.js"></script>
4809
+ * <script src="dist/plugins/grideditor.summernote.min.js"></script>
4810
+ *
4811
+ * Up to 6.0 the main bundle carries a copy of this file too.
4812
+ */
4181
4813
  (function($) {
4182
4814
 
4183
- $.fn.gridEditor.RTEs.summernote = {
4815
+ $.extend($.fn.gridEditor.locales.en, {
4816
+ 'text.summernote': 'Summernote',
4817
+ });
4184
4818
 
4185
- init: function(settings, contentAreas) {
4186
-
4187
- if (!jQuery().summernote) {
4188
- console.error($.fn.gridEditor.t(settings, 'error.summernote_missing'));
4189
- }
4819
+ var INITIAL_CONTENT = '<p>Lorem ipsum dolores</p>';
4190
4820
 
4191
- var self = this;
4192
- contentAreas.each(function() {
4193
- var contentArea = $(this);
4194
- if (!contentArea.hasClass('active')) {
4195
- if (contentArea.html() == self.initialContent) {
4821
+ $.fn.gridEditor.texts.summernote = function(ge) {
4822
+ return {
4823
+ labelKey: 'text.summernote',
4824
+ initialContent: INITIAL_CONTENT,
4825
+ missingKey: 'error.summernote_missing',
4826
+
4827
+ available: function() { return !!$.fn.summernote; },
4828
+
4829
+ start: function(contentAreas) {
4830
+ var settings = ge.settings;
4831
+
4832
+ // Summernote 0.9.1 calls $.now(), which jQuery 4 removed, and
4833
+ // cannot open without it. Given back only where it is missing,
4834
+ // and only once summernote is actually used.
4835
+ if (!$.now) { $.now = Date.now; }
4836
+
4837
+ contentAreas.each(function() {
4838
+ var contentArea = $(this);
4839
+ if (contentArea.hasClass('active')) { return; }
4840
+
4841
+ if (contentArea.html() == INITIAL_CONTENT) {
4196
4842
  contentArea.html('');
4197
4843
  }
4198
4844
  contentArea.addClass('active');
@@ -4207,7 +4853,7 @@ $.fn.gridEditor.locales = {
4207
4853
  // Focus editor on creation
4208
4854
  callbacks: {
4209
4855
  onInit: function() {
4210
-
4856
+
4211
4857
  // Call original oninit function, if one was passed in the config
4212
4858
  var callback;
4213
4859
  try {
@@ -4222,175 +4868,44 @@ $.fn.gridEditor.locales = {
4222
4868
  // The editor owns what is inside the
4223
4869
  // content area now, so the grid editor is
4224
4870
  // told to put its own furniture back
4225
- contentArea.trigger('ge-rte-ready');
4226
-
4871
+ ge.textReady(contentArea);
4872
+
4227
4873
  contentArea.summernote('focus');
4228
4874
  }
4229
4875
  }
4230
4876
  }
4231
4877
  );
4232
4878
  contentArea.summernote(configuration);
4233
- }
4234
- });
4235
- },
4236
-
4237
- deinit: function(settings, contentAreas) {
4238
- contentAreas.filter('.active').each(function() {
4239
- var contentArea = $(this);
4240
- contentArea.summernote('destroy');
4241
- contentArea
4242
- .removeClass('active')
4243
- .removeAttr('id')
4244
- .removeAttr('style')
4245
- .removeAttr('spellcheck')
4246
- ;
4247
- });
4248
- },
4879
+ });
4880
+ },
4249
4881
 
4250
- initialContent: '<p>Lorem ipsum dolores</p>',
4882
+ stop: function(contentAreas) {
4883
+ contentAreas.filter('.active').each(function() {
4884
+ var contentArea = $(this);
4885
+ contentArea.summernote('destroy');
4886
+ contentArea
4887
+ .removeClass('active')
4888
+ .removeAttr('id')
4889
+ .removeAttr('style')
4890
+ .removeAttr('spellcheck')
4891
+ ;
4892
+ });
4893
+ },
4894
+ };
4251
4895
  };
4252
4896
  })(jQuery);
4253
4897
 
4898
+ /**
4899
+ * Up to 6.0 the main bundle carries a copy of each text editor plugin, so a
4900
+ * page that loaded only the editor keeps the editor it had. This marks those
4901
+ * copies, and the editor warns once when a content area is edited with one:
4902
+ * 6.0 leaves them out. Loaded on its own, a plugin's file registers itself
4903
+ * again, unmarked, and is the one used.
4904
+ *
4905
+ * Only ever concatenated after the plugins it marks, never built on its own.
4906
+ */
4254
4907
  (function($) {
4255
-
4256
- // tinyMCE snapshots the target element's attributes when an inline editor is
4257
- // created and restores them on remove(), so this has to run *after* remove()
4258
- // to keep the grid editor's own class and tinyMCE's leftovers off the element.
4259
- function cleanUp(contentArea) {
4260
- contentArea
4261
- .removeClass('active')
4262
- .removeClass('ge-rte-active')
4263
- .removeAttr('id')
4264
- .removeAttr('style')
4265
- .removeAttr('spellcheck')
4266
- .removeAttr('contenteditable')
4267
- .removeAttr('data-mce-style')
4268
- ;
4269
- }
4270
-
4271
- $.fn.gridEditor.RTEs.tinymce = {
4272
-
4273
- init: function(settings, contentAreas) {
4274
-
4275
- if (!window.tinymce) {
4276
- console.error($.fn.gridEditor.t(settings, 'error.tinymce_missing'));
4277
- return;
4278
- }
4279
-
4280
- var self = this;
4281
- var userConfig = (settings.tinymce && settings.tinymce.config) ? settings.tinymce.config : {};
4282
-
4283
- contentAreas.each(function() {
4284
- var contentArea = $(this);
4285
- if (contentArea.hasClass('active')) { return; }
4286
-
4287
- if (contentArea.html() == self.initialContent) {
4288
- contentArea.html('');
4289
- }
4290
- contentArea.addClass('active');
4291
-
4292
- var configuration = $.extend({
4293
- // tinyMCE's own "Upgrade" badge in the menubar. Off by
4294
- // default because an inline editor here is a column of
4295
- // someone's page, not tinyMCE's own interface; a host that
4296
- // wants it back passes promotion: true.
4297
- promotion: false,
4298
- }, userConfig, {
4299
- target: this,
4300
- inline: true,
4301
- init_instance_callback: function(editor) {
4302
- // deinit ran before this init finished, so tear it down again
4303
- if (contentArea.data('ge-tinymce-pending-remove')) {
4304
- contentArea.removeData('ge-tinymce-pending-remove');
4305
- editor.remove();
4306
- // deinit already ran and cannot clean up after this
4307
- // late remove(), so do it here instead
4308
- cleanUp(contentArea);
4309
- return;
4310
- }
4311
-
4312
- contentArea.data('ge-tinymce', editor);
4313
-
4314
- // The editor rewrote what is inside this content area
4315
- // while it took it over, so whatever the grid editor
4316
- // had in there is gone. Saying so lets it put its own
4317
- // furniture back (see RTE_READY in the core).
4318
- contentArea.trigger('ge-rte-ready');
4319
-
4320
- // Undo and redo rewrite it too, from snapshots that
4321
- // leave out whatever is marked data-mce-bogus - an
4322
- // element's drawer among them - so it goes back in
4323
- // after each of those as well
4324
- editor.on('Undo Redo', function() {
4325
- contentArea.trigger('ge-rte-ready');
4326
- });
4327
-
4328
- // The inline toolbar is laid out against the element's
4329
- // geometry at the moment tinyMCE draws it, and an
4330
- // element that has just appeared - a tab pane, an
4331
- // accordion body, a column whose width is still
4332
- // settling - may not have its own width yet. Asking
4333
- // for the ui again once the browser has laid the frame
4334
- // out measures it as it now is, instead of leaving a
4335
- // toolbar wrapped into a narrow column.
4336
- window.requestAnimationFrame(function() {
4337
- if (editor.removed || !editor.ui || !editor.ui.show) { return; }
4338
-
4339
- editor.ui.show();
4340
- });
4341
-
4342
- // And again whenever the user comes back to this
4343
- // editor: by then the element may have been resized,
4344
- // hidden and shown again - a tab switched away from
4345
- // and back, a column made narrower - and the toolbar
4346
- // is only ever as right as its last measurement.
4347
- editor.on('focus', function() {
4348
- window.requestAnimationFrame(function() {
4349
- if (editor.removed || !editor.ui || !editor.ui.show) { return; }
4350
-
4351
- editor.ui.show();
4352
- });
4353
- });
4354
-
4355
- // Bring focus to text field
4356
- editor.focus();
4357
-
4358
- // Call the original callbacks, if any were passed in the config
4359
- if (userConfig.init_instance_callback) {
4360
- userConfig.init_instance_callback.call(this, editor);
4361
- }
4362
- // 'oninit' is the pre-6 name, honoured so existing configs keep working
4363
- if (userConfig.oninit) {
4364
- userConfig.oninit.call(this, editor);
4365
- }
4366
- }
4367
- });
4368
- // We always edit the element we were handed, so a selector in the
4369
- // user config would only fight with target
4370
- delete configuration.selector;
4371
-
4372
- window.tinymce.init(configuration);
4373
- });
4374
- },
4375
-
4376
- deinit: function(settings, contentAreas) {
4377
- contentAreas.filter('.active').each(function() {
4378
- var contentArea = $(this);
4379
- var editor = contentArea.data('ge-tinymce');
4380
-
4381
- if (editor) {
4382
- contentArea.removeData('ge-tinymce');
4383
- editor.remove();
4384
- } else {
4385
- // tinymce.init() is asynchronous, so the editor may not exist
4386
- // yet. Leave a note for init_instance_callback to remove it.
4387
- contentArea.data('ge-tinymce-pending-remove', true);
4388
- }
4389
-
4390
- cleanUp(contentArea);
4391
- });
4392
- },
4393
-
4394
- initialContent: '<p>Lorem ipsum dolores</p>',
4395
- };
4908
+ ['tinymce', 'ckeditor', 'summernote'].forEach(function(type) {
4909
+ if ($.fn.gridEditor.texts[type]) { $.fn.gridEditor.texts[type].bundled = true; }
4910
+ });
4396
4911
  })(jQuery);