@themarioga/grid-editor 5.2.0 → 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 (48) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +29 -1
  3. package/dist/grideditor.css +28 -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 +395 -26
  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 +11 -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.ckeditor.js +5 -0
  14. package/dist/plugins/grideditor.ckeditor.min.js +1 -1
  15. package/dist/plugins/grideditor.ckeditor.min.js.map +1 -1
  16. package/dist/plugins/grideditor.clipboard.js +8 -7
  17. package/dist/plugins/grideditor.clipboard.min.js +1 -1
  18. package/dist/plugins/grideditor.clipboard.min.js.map +1 -1
  19. package/dist/plugins/grideditor.spacing.js +1 -1
  20. package/dist/plugins/grideditor.spacing.min.js +1 -1
  21. package/dist/plugins/grideditor.spacing.min.js.map +1 -1
  22. package/dist/plugins/grideditor.summernote.js +5 -0
  23. package/dist/plugins/grideditor.summernote.min.js +1 -1
  24. package/dist/plugins/grideditor.summernote.min.js.map +1 -1
  25. package/dist/plugins/grideditor.textalign.js +1 -1
  26. package/dist/plugins/grideditor.textalign.min.js +1 -1
  27. package/dist/plugins/grideditor.textalign.min.js.map +1 -1
  28. package/dist/plugins/grideditor.tinymce.js +5 -0
  29. package/dist/plugins/grideditor.tinymce.min.js +1 -1
  30. package/dist/plugins/grideditor.tinymce.min.js.map +1 -1
  31. package/dist/plugins/grideditor.visibility.js +3 -3
  32. package/dist/plugins/grideditor.visibility.min.js +1 -1
  33. package/dist/plugins/grideditor.visibility.min.js.map +1 -1
  34. package/docs/events.md +5 -2
  35. package/docs/locale-keys.md +23 -1
  36. package/docs/plugins.md +7 -2
  37. package/example/attributes.html +2 -2
  38. package/package.json +1 -1
  39. package/src/js/jquery.grideditor.js +380 -26
  40. package/src/js/locales/grideditor.es.js +11 -0
  41. package/src/js/plugins/grideditor.ckeditor.js +5 -0
  42. package/src/js/plugins/grideditor.clipboard.js +8 -7
  43. package/src/js/plugins/grideditor.spacing.js +1 -1
  44. package/src/js/plugins/grideditor.summernote.js +5 -0
  45. package/src/js/plugins/grideditor.textalign.js +1 -1
  46. package/src/js/plugins/grideditor.tinymce.js +5 -0
  47. package/src/js/plugins/grideditor.visibility.js +3 -3
  48. package/src/less/grideditor.less +34 -0
@@ -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,7 +1467,9 @@ $.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 type = $(this).data('ge-content-type');
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');
1408
1473
  var text = textFor(type);
1409
1474
  if (!text) { return; }
1410
1475
 
@@ -1421,6 +1486,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1421
1486
  '.js after the editor');
1422
1487
  }
1423
1488
 
1489
+ $(this).data('ge-text-before', attributesOf(this)).removeData('ge-text-ready');
1424
1490
  $(this).addClass('ge-rte-active');
1425
1491
  text.start($(this));
1426
1492
  }
@@ -1436,6 +1502,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1436
1502
  canvas.toggleClass('ge-drag-drawer', settings.drag_handle === 'drawer');
1437
1503
  addAllColClasses();
1438
1504
  wrapContent();
1505
+ wrapTexts();
1439
1506
  createRowControls();
1440
1507
  createColControls();
1441
1508
  markContainers();
@@ -1448,21 +1515,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1448
1515
 
1449
1516
  function deinit() {
1450
1517
  canvas.removeClass('ge-editing ge-drag-drawer ge-dropping');
1451
- var contents = canvas.find('.ge-content').each(function() {
1452
- var content = $(this);
1453
- var text = textFor(content.data('ge-content-type'));
1454
- if (text) {
1455
- text.stop(content);
1456
- }
1457
- // Cleared after rte.deinit, not before: an editor can restore the
1458
- // class attribute it snapshotted when it was created, which would
1459
- // leave ge-rte-active in place and make initRTE ignore every later
1460
- // click on this content area.
1461
- content.removeClass('ge-rte-active');
1518
+ canvas.find('.ge-content').each(function() {
1519
+ closeText($(this));
1462
1520
  });
1463
1521
  closeSizePicker();
1464
1522
  hideDropMarker();
1465
1523
  canvas.find('.ge-tools-drawer').remove();
1524
+ unwrapTexts();
1466
1525
  plugins('onDeinit');
1467
1526
  // After the rich text editors have let go of their content areas:
1468
1527
  // one that rebuilt its area's DOM brought the preview styles back
@@ -1665,6 +1724,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1665
1724
  // A text editor has rewritten a content area, so whatever the
1666
1725
  // editor and its plugins had put in there goes back in
1667
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
+ },
1668
1734
  toolbarItems: function(name) {
1669
1735
  return mainControls
1670
1736
  ? mainControls.find('[data-ge-toolbar="feature"][data-ge-feature="' + name + '"]')
@@ -1691,7 +1757,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1691
1757
  * -------------------------------------------------------------- */
1692
1758
 
1693
1759
  var FAMILIES = {}; // Every family the utility plugins declare, by name
1694
- var UTILITY_NODES = '.row, .column, .ge-element, [data-ge-container]';
1760
+ var UTILITY_NODES = '.row, .column, .ge-content, .ge-element, [data-ge-container]';
1695
1761
  var PREVIEW_ATTR = 'data-ge-preview';
1696
1762
  var utilitiesOpen = false; // Whether the panels show their Responsive section unfolded
1697
1763
 
@@ -2479,6 +2545,14 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2479
2545
  function attachSizePicker(tool, row) {
2480
2546
  if (!settings.add_column.picker) { return; }
2481
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) {
2482
2556
  var timer = null;
2483
2557
 
2484
2558
  var cancel = function() {
@@ -2491,7 +2565,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2491
2565
 
2492
2566
  timer = window.setTimeout(function() {
2493
2567
  timer = null;
2494
- openSizePicker(tool, row);
2568
+ open();
2495
2569
  }, settings.add_column.delay);
2496
2570
  });
2497
2571
 
@@ -2641,6 +2715,22 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2641
2715
  });
2642
2716
  });
2643
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
+
2644
2734
  createTool(drawer, t('tool.add_row'), 'ge-add-row', 'bi bi-plus-circle', function() {
2645
2735
  // An empty row: the columns in it are the next decision,
2646
2736
  // and its drawer's add column tool is where that is made
@@ -2755,7 +2845,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2755
2845
  }
2756
2846
 
2757
2847
  function createTool(drawer, title, className, iconClass, eventHandlers) {
2758
- 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))
2759
2855
  .appendTo(drawer)
2760
2856
  ;
2761
2857
  if (typeof eventHandlers == 'function') {
@@ -3189,11 +3285,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3189
3285
 
3190
3286
  /**
3191
3287
  * What the canvas, the columns and the plugins' regions move: rows,
3192
- * content areas and containers, and whatever blocks a feature adds -
3193
- * 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.
3194
3291
  */
3195
3292
  function blockSelector() {
3196
- 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(', ');
3197
3295
  }
3198
3296
 
3199
3297
  /** A selector every feature plugin may contribute to, under one name. */
@@ -3359,12 +3457,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3359
3457
  function sortStart(e) {
3360
3458
  var node = $(e.item);
3361
3459
  var from = positionOf(node);
3460
+ var subject = moveSubject(node);
3362
3461
 
3363
3462
  node.data('ge-move-from', from);
3364
3463
  node.removeData('ge-move-canceled');
3365
3464
 
3366
3465
  operate(function() {
3367
- var moving = emit('before-move', payloadFor(kindOf(node), node, {
3466
+ var moving = emit('before-move', payloadFor(subject.kind, subject.node, {
3368
3467
  parent: from.parent,
3369
3468
  source: 'dragdrop',
3370
3469
  from: from,
@@ -3374,6 +3473,19 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3374
3473
  });
3375
3474
  }
3376
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
+
3377
3489
  /** Put a node back where a refused drag found it. */
3378
3490
  function putBack(node, from) {
3379
3491
  var siblings = from.parent.children().not('.ge-tools-drawer').not(node);
@@ -3411,8 +3523,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3411
3523
  // No init() here, unlike an add: the node brought its drawer
3412
3524
  // with it, and the drag is still being finished, so this is the
3413
3525
  // wrong moment to rebuild the lists it is using.
3526
+ var subject = moveSubject(node);
3527
+
3414
3528
  operate(function() {
3415
- emit('after-move', payloadFor(kindOf(node), node, {
3529
+ emit('after-move', payloadFor(subject.kind, subject.node, {
3416
3530
  parent: to.parent,
3417
3531
  source: 'dragdrop',
3418
3532
  from: from,
@@ -3855,7 +3969,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3855
3969
  // A container sits in the column beside the content
3856
3970
  // areas, not inside one, so it ends a run of loose
3857
3971
  // content rather than joining it
3858
- if (child.is('.row, .ge-content, [data-ge-container]')) {
3972
+ if (child.is('.row, .ge-content, .ge-text-block, [data-ge-container]')) {
3859
3973
  contents = doWrap(contents);
3860
3974
  } else {
3861
3975
  contents = contents.add(child);
@@ -3882,11 +3996,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3882
3996
  }
3883
3997
 
3884
3998
  /**
3885
- * A content area for the first content type. With no content types
3886
- * 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.
3887
4002
  */
3888
- function createDefaultContentWrapper() {
3889
- var type = settings.content_types[0];
4003
+ function createDefaultContentWrapper(type) {
4004
+ type = type || settings.content_types[0];
3890
4005
  var contentArea = $('<div class="ge-content"/>');
3891
4006
 
3892
4007
  if (type) {
@@ -3896,6 +4011,236 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3896
4011
  return contentArea;
3897
4012
  }
3898
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
+
3899
4244
  /**
3900
4245
  * Constrain the canvas to the view's preview width and make that
3901
4246
  * tier's classes the effective ones. The all view constrains nothing:
@@ -3986,6 +4331,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3986
4331
  getView: getView,
3987
4332
  createRow: apiCreateRow,
3988
4333
  createColumn: apiCreateColumn,
4334
+ createText: apiCreateText,
3989
4335
  createElement: function(content, options) {
3990
4336
  if (!featureMethods.createElement) {
3991
4337
  warnOnceHere('plugin:elements', 'createElement needs the elements plugin: ' +
@@ -4103,6 +4449,13 @@ $.fn.gridEditor.locales = {
4103
4449
  'tool.move': 'Move',
4104
4450
  'tool.settings': 'Settings',
4105
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',
4106
4459
  'tool.add_column': 'Add column\n(hold to choose the width)',
4107
4460
  'tool.column_size': '{size} of 12',
4108
4461
  'tool.column_equal': 'Equal: shares what the row has left',
@@ -4130,6 +4483,7 @@ $.fn.gridEditor.locales = {
4130
4483
  'confirm.cancel': 'Cancel',
4131
4484
  'confirm.delete_row': 'Delete row?',
4132
4485
  'confirm.delete_column': 'Delete column?',
4486
+ 'confirm.delete_text': 'Delete this text?',
4133
4487
  'confirm.delete_container': 'Delete this container and everything in it?',
4134
4488
  'view.all': 'All sizes',
4135
4489
  'view.xs': 'Phone',
@@ -4174,6 +4528,10 @@ $.fn.gridEditor.locales = {
4174
4528
  */
4175
4529
  (function($) {
4176
4530
 
4531
+ $.extend($.fn.gridEditor.locales.en, {
4532
+ 'text.tinymce': 'tinyMCE',
4533
+ });
4534
+
4177
4535
  // tinyMCE snapshots the target element's attributes when an inline editor is
4178
4536
  // created and restores them on remove(), so this has to run *after* remove()
4179
4537
  // to keep the grid editor's own class and tinyMCE's leftovers off the element.
@@ -4193,6 +4551,7 @@ $.fn.gridEditor.locales = {
4193
4551
 
4194
4552
  $.fn.gridEditor.texts.tinymce = function(ge) {
4195
4553
  return {
4554
+ labelKey: 'text.tinymce',
4196
4555
  initialContent: INITIAL_CONTENT,
4197
4556
  missingKey: 'error.tinymce_missing',
4198
4557
 
@@ -4332,6 +4691,10 @@ $.fn.gridEditor.locales = {
4332
4691
  */
4333
4692
  (function($) {
4334
4693
 
4694
+ $.extend($.fn.gridEditor.locales.en, {
4695
+ 'text.ckeditor': 'CKEditor',
4696
+ });
4697
+
4335
4698
  var INITIAL_CONTENT = '<p>Lorem initius... </p>';
4336
4699
 
4337
4700
  $.fn.gridEditor.texts.ckeditor = function(ge) {
@@ -4354,6 +4717,7 @@ $.fn.gridEditor.locales = {
4354
4717
  }
4355
4718
 
4356
4719
  return {
4720
+ labelKey: 'text.ckeditor',
4357
4721
  initialContent: INITIAL_CONTENT,
4358
4722
  missingKey: 'error.ckeditor_missing',
4359
4723
 
@@ -4448,10 +4812,15 @@ $.fn.gridEditor.locales = {
4448
4812
  */
4449
4813
  (function($) {
4450
4814
 
4815
+ $.extend($.fn.gridEditor.locales.en, {
4816
+ 'text.summernote': 'Summernote',
4817
+ });
4818
+
4451
4819
  var INITIAL_CONTENT = '<p>Lorem ipsum dolores</p>';
4452
4820
 
4453
4821
  $.fn.gridEditor.texts.summernote = function(ge) {
4454
4822
  return {
4823
+ labelKey: 'text.summernote',
4455
4824
  initialContent: INITIAL_CONTENT,
4456
4825
  missingKey: 'error.summernote_missing',
4457
4826