@themarioga/grid-editor 6.0.0-beta.2 → 6.0.0-beta.4

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 (41) hide show
  1. package/BUILDING.md +3 -1
  2. package/CHANGELOG.md +58 -1
  3. package/README.md +16 -5
  4. package/UPGRADING.md +36 -7
  5. package/dist/grideditor.css +14 -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 +303 -378
  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 +4 -1
  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 +397 -3
  16. package/dist/plugins/grideditor.ckeditor.min.js +1 -1
  17. package/dist/plugins/grideditor.ckeditor.min.js.map +1 -1
  18. package/dist/plugins/grideditor.summernote.js +397 -3
  19. package/dist/plugins/grideditor.summernote.min.js +1 -1
  20. package/dist/plugins/grideditor.summernote.min.js.map +1 -1
  21. package/dist/plugins/grideditor.tinymce.js +397 -3
  22. package/dist/plugins/grideditor.tinymce.min.js +1 -1
  23. package/dist/plugins/grideditor.tinymce.min.js.map +1 -1
  24. package/dist/plugins/grideditor.visibility.js +1 -1
  25. package/dist/plugins/grideditor.visibility.min.js +1 -1
  26. package/dist/plugins/grideditor.visibility.min.js.map +1 -1
  27. package/docs/events.md +11 -2
  28. package/docs/locale-keys.md +13 -4
  29. package/docs/plugins.md +109 -46
  30. package/example/custom_editor.html +193 -0
  31. package/example/index.html +8 -0
  32. package/example/wrap_content.html +5 -3
  33. package/package.json +1 -1
  34. package/src/js/jquery.grideditor.js +303 -378
  35. package/src/js/locales/grideditor.es.js +4 -1
  36. package/src/js/plugins/grideditor.ckeditor.js +7 -3
  37. package/src/js/plugins/grideditor.summernote.js +7 -3
  38. package/src/js/plugins/grideditor.tinymce.js +7 -3
  39. package/src/js/plugins/grideditor.visibility.js +1 -1
  40. package/src/js/text/grideditor.text.js +389 -0
  41. package/src/less/grideditor.less +17 -0
@@ -339,8 +339,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
339
339
  'toolbar_drag' : 'auto', // Drag the toolbar's buttons onto the canvas. 'auto' follows drag_handle
340
340
  'element_tools' : [], // Host tools on element drawers, same shape as row_tools
341
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
342
+ // content_types, text_tools and text_classes are the text editor
343
+ // plugins' settings since 6.0: see grideditor.text.js
344
344
  'container_classes' : [], // The same, on a container's panel
345
345
  'pane_classes' : [], // And on a tab's or an accordion item's
346
346
  'container_tools' : [], // Host tools on container drawers
@@ -351,7 +351,6 @@ $.fn.gridEditor = function( optionsOrMethod ) {
351
351
  'utilities' : {}, // Options for the utility plugins, by plugin name
352
352
  'elements' : NESTED_SETTINGS.elements, // Element level controls, below the column
353
353
  'custom_filter' : '',
354
- 'content_types' : ['tinymce'],
355
354
  'valid_col_sizes' : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 'equal', 'auto'],
356
355
  'valid_col_offsets' : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
357
356
  'add_column' : NESTED_SETTINGS.add_column, // The add column tool
@@ -836,19 +835,21 @@ $.fn.gridEditor = function( optionsOrMethod ) {
836
835
  if (node.hasClass('row')) { return 'row'; }
837
836
  if (node.hasClass('column')) { return 'column'; }
838
837
  if (node.hasClass('ge-element')) { return 'element'; }
839
- if (node.hasClass('ge-content') || node.hasClass('ge-text-block')) { return 'text'; }
838
+ // A content area is a text when an editor's type is on it, and the
839
+ // host's plain content when there is none
840
+ if (node.hasClass('ge-text-block')) { node = node.children('.ge-content'); }
841
+ if (node.hasClass('ge-content')) { return node.attr('data-ge-content-type') ? 'text' : 'plain'; }
840
842
  return 'node';
841
843
  }
842
844
 
843
845
  // Copy html to sourceElement if a source textarea is given
844
846
  if (settings.source_textarea) {
845
847
  var sourceHtml = $(settings.source_textarea).val();
848
+ // Html with no grid in it goes into a row and a full width column
849
+ // of its own, where init wraps it as the host's plain content
846
850
  if (sourceHtml.length > 0 && $('<div>' + sourceHtml + '</div>').find('.row').addBack('.row').length == 0) {
847
- var sourceRow = createRow();
848
- var sourceColumn = createColumn(12).appendTo(sourceRow);
849
- sourceColumn.find('.ge-content').html(sourceHtml);
850
- sourceHtml = sourceColumn.html();
851
- }
851
+ sourceHtml = '<div class="row"><div class="col-lg-12">' + sourceHtml + '</div></div>';
852
+ }
852
853
  baseElem.html(sourceHtml);
853
854
  }
854
855
 
@@ -871,8 +872,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
871
872
  // Make controls fixed on scroll
872
873
  $(window).on('scroll', onScroll);
873
874
 
874
- /* Init RTE on click */
875
- canvas.on('click', '.ge-content', initRTE);
875
+ /* A click on the host's plain content offers to make it a text */
876
+ canvas.on('click', '.ge-content', onContentClick);
876
877
 
877
878
  /* A trigger is often a link, and a link still navigates even
878
879
  with its Bootstrap attributes suspended */
@@ -884,12 +885,6 @@ $.fn.gridEditor = function( optionsOrMethod ) {
884
885
  // which costs the element drawers inside it. The integrations say
885
886
  // when their editor is ready, and the drawers go back in.
886
887
  canvas.on('ge-rte-ready', '.ge-content', function() {
887
- // The first time only: an undo says ready again, and by then
888
- // the host may have changed the content area itself
889
- if ($(this).data('ge-text-before') && !$(this).data('ge-text-ready')) {
890
- $(this).data('ge-text-ready', attributesOf(this));
891
- }
892
-
893
888
  plugins('onContentReady', $(this));
894
889
  refreshPreviews($(this));
895
890
  });
@@ -947,50 +942,16 @@ $.fn.gridEditor = function( optionsOrMethod ) {
947
942
 
948
943
  addContainerGroup.appendTo(wrapper);
949
944
 
950
- // A text block of each editor offered. Like a container, it goes
951
- // into a row of its own when clicked, or where it is dropped
952
- var texts = offeredTexts();
953
- texts.forEach(function(type) {
954
- var label = texts.length > 1 ? t('text.add_type', { editor: textLabel(type) }) : t('text.add');
955
-
956
- $('<a class="btn btn-sm btn-primary ge-add-container ge-add-text-button" />')
957
- .attr('title', label)
958
- .attr('data-ge-toolbar', 'text')
959
- .attr('data-ge-text', type)
960
- .append('<i class="bi bi-plus"></i>')
961
- .append($('<span />').text(label))
962
- .on('click', function() {
963
- var row = createRow();
964
- var block = makeText(type);
965
- createColumn(MAX_COL_SIZE).appendTo(row).find('> .ge-content').replaceWith(block);
966
-
967
- var added = addNode('text', block, function() {
968
- row.appendTo(canvas);
969
- }, { parent: canvas, source: 'tool' });
970
-
971
- if (added && row[0].scrollIntoView) {
972
- row[0].scrollIntoView({behavior: 'smooth'});
973
- }
974
- })
975
- .appendTo(addContainerGroup)
976
- ;
977
- });
978
-
979
945
  // A container starts in a row of its own, the way the add row
980
946
  // buttons next to these ones do
981
947
  $.each(CONTAINERS, function(type, definition) {
982
- $('<a class="btn btn-sm btn-primary ge-add-container" />')
983
- .attr('title', t(definition.labelKey))
948
+ labelButton($('<a class="btn btn-sm btn-primary ge-add-container" />'), t(definition.labelKey), definition.iconClass)
984
949
  .attr('data-ge-toolbar', 'container')
985
950
  .attr('data-ge-container-type', type)
986
- .append('<i class="bi bi-plus"></i>')
987
- .append($('<span />').text(t(definition.labelKey)))
988
951
  .on('click', function() {
989
952
  var row = createRow();
990
953
  var column = createColumn(MAX_COL_SIZE).appendTo(row);
991
- var container = definition.create({});
992
-
993
- column.find('> .ge-content').replaceWith(container);
954
+ var container = definition.create({}).appendTo(column);
994
955
 
995
956
  addNode(type, container, function() {
996
957
  row.appendTo(canvas);
@@ -1093,25 +1054,49 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1093
1054
  makeToolbarDraggable();
1094
1055
  }
1095
1056
 
1096
- /** A feature plugin's toolbar button: its label as text, or as the title alone at the end. */
1057
+ /**
1058
+ * A toolbar button's face: the plugin's icon alone when it has one,
1059
+ * with its label as the title, or a plus and the label otherwise.
1060
+ */
1061
+ function labelButton(button, label, iconClass) {
1062
+ button.attr('title', label);
1063
+
1064
+ if (iconClass) { return button.append($('<i />').addClass(iconClass)); }
1065
+
1066
+ return button
1067
+ .append('<i class="bi bi-plus"></i>')
1068
+ .append($('<span />').text(label))
1069
+ ;
1070
+ }
1071
+
1072
+ /** A feature plugin's toolbar button. At the end it is always an icon, `bi bi-plus` if it has none of its own. */
1097
1073
  function featureButton(name, item, index) {
1098
- var button = $('<a class="btn btn-sm btn-primary ge-add-container ge-add-feature" />')
1099
- .attr('title', t(item.labelKey))
1074
+ var iconClass = item.iconClass || (item.align === 'end' ? 'bi bi-plus' : null);
1075
+ // A label of its own, worked out when the toolbar is built - and
1076
+ // built again by setLocale - or the string its labelKey names
1077
+ var label = typeof item.label === 'function' ? item.label() : (item.label || t(item.labelKey));
1078
+ var button = labelButton($('<a class="btn btn-sm btn-primary ge-add-container ge-add-feature" />'), label, iconClass)
1079
+ .addClass(item.className || '')
1100
1080
  .attr('data-ge-toolbar', 'feature')
1101
1081
  .attr('data-ge-feature', name)
1102
1082
  .attr('data-ge-item', index)
1103
- .append($('<i />').addClass(item.iconClass || 'bi bi-plus'))
1104
1083
  .on('click', function() {
1105
1084
  var made = item.create();
1106
1085
 
1107
- addNode(item.kind, made, function() {
1108
- made.appendTo(canvas);
1086
+ // One that belongs in a column brings a row and a column
1087
+ // of its own, the way a container does
1088
+ var placed = item.inColumn ? inRowOfItsOwn(made) : made;
1089
+
1090
+ var added = addNode(item.kind, made, function() {
1091
+ placed.appendTo(canvas);
1109
1092
  }, { parent: canvas, source: item.source || 'tool' });
1093
+
1094
+ if (added && item.inColumn && placed[0].scrollIntoView) {
1095
+ placed[0].scrollIntoView({behavior: 'smooth'});
1096
+ }
1110
1097
  })
1111
1098
  ;
1112
1099
 
1113
- if (item.align !== 'end') { button.append($('<span />').text(t(item.labelKey))); }
1114
-
1115
1100
  return button;
1116
1101
  }
1117
1102
 
@@ -1250,8 +1235,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1250
1235
  function insertFeatureFromToolbar(button, where) {
1251
1236
  var item = FEATURES[button.attr('data-ge-feature')].toolbar[parseInt(button.attr('data-ge-item'), 10)];
1252
1237
  var made = item.create();
1238
+ var placed = made;
1253
1239
 
1254
- if (!acceptsBlock(where.region, made)) {
1240
+ if (item.inColumn) {
1241
+ // Where it lands in a column, or in a row and a column of its
1242
+ // own anywhere else
1243
+ if (!where.region.is('.column')) { placed = inRowOfItsOwn(made); }
1244
+ } else if (!acceptsBlock(where.region, made)) {
1255
1245
  var top = where.region.parentsUntil(canvas).addBack().first();
1256
1246
  where = { region: canvas, before: top.length ? top.next() : null };
1257
1247
  if (where.before && !where.before.length) { where.before = null; }
@@ -1259,33 +1249,19 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1259
1249
 
1260
1250
  return addNode(item.kind, made, function() {
1261
1251
  if (where.before) {
1262
- made.insertBefore(where.before);
1252
+ placed.insertBefore(where.before);
1263
1253
  } else {
1264
- made.appendTo(where.region);
1254
+ placed.appendTo(where.region);
1265
1255
  }
1266
1256
  }, { parent: where.region, source: item.source || 'dragdrop' });
1267
1257
  }
1268
1258
 
1269
- /**
1270
- * A text block dropped from the toolbar: where it lands in a column,
1271
- * or in a row and a column of its own anywhere else.
1272
- */
1273
- function insertTextFromToolbar(button, where) {
1274
- var made = makeText(button.attr('data-ge-text'));
1275
- var placed = made;
1276
-
1277
- if (!where.region.is('.column')) {
1278
- placed = createRow();
1279
- createColumn(MAX_COL_SIZE).appendTo(placed).find('> .ge-content').replaceWith(made);
1280
- }
1259
+ /** A detached row with one full width column holding `node`. */
1260
+ function inRowOfItsOwn(node) {
1261
+ var row = createRow();
1262
+ createColumn(MAX_COL_SIZE).appendTo(row).append(node);
1281
1263
 
1282
- return addNode('text', made, function() {
1283
- if (where.before) {
1284
- placed.insertBefore(where.before);
1285
- } else {
1286
- placed.appendTo(where.region);
1287
- }
1288
- }, { parent: where.region, source: 'dragdrop' });
1264
+ return row;
1289
1265
  }
1290
1266
 
1291
1267
  /** A line where the block would go, following the pointer. */
@@ -1317,9 +1293,6 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1317
1293
  if (button.attr('data-ge-toolbar') === 'feature') {
1318
1294
  return insertFeatureFromToolbar(button, where);
1319
1295
  }
1320
- if (button.attr('data-ge-toolbar') === 'text') {
1321
- return insertTextFromToolbar(button, where);
1322
- }
1323
1296
 
1324
1297
  var container = button.attr('data-ge-toolbar') === 'container';
1325
1298
  var type = button.attr('data-ge-container-type');
@@ -1330,11 +1303,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1330
1303
  // A container belongs in a column: dropped straight onto the
1331
1304
  // canvas it brings a row and a column of its own
1332
1305
  var placed = made;
1333
- if (container && !where.region.is('.column')) {
1334
- placed = createRow();
1335
- createColumn(MAX_COL_SIZE).appendTo(placed)
1336
- .find('> .ge-content').replaceWith(made);
1337
- }
1306
+ if (container && !where.region.is('.column')) { placed = inRowOfItsOwn(made); }
1338
1307
 
1339
1308
  return addNode(container ? type : 'row', made, function() {
1340
1309
  if (where.before) {
@@ -1461,30 +1430,153 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1461
1430
  }
1462
1431
  }
1463
1432
 
1464
- function initRTE(e) {
1465
- if ($(this).hasClass('ge-rte-active')) { return; }
1433
+ /**
1434
+ * A click on the host's plain content makes it a text: of the one
1435
+ * editor offered, or of the one chosen when there are several. What
1436
+ * a click on a text does is its editor plugin's business.
1437
+ */
1438
+ function onContentClick() {
1439
+ var block = $(this);
1440
+ if (block.attr('data-ge-content-type')) { return; }
1441
+
1442
+ // A second click, with the choice still open, withdraws it
1443
+ if (closeSizePicker()) { return; }
1466
1444
 
1467
- // A content area nobody can see - a tab that is not the open one,
1468
- // a closed accordion item - has no geometry for an editor to lay
1445
+ // Content nobody can see - a tab that is not the open one, a
1446
+ // closed accordion item - has no geometry for an editor to lay
1469
1447
  // its toolbar out against, and nothing anyone can type into
1470
- if (!$(this).is(':visible')) { return; }
1471
-
1472
- // The attribute, not jQuery's cached copy of it: it is markup, and
1473
- // the drawer reads it the same way
1474
- var type = $(this).attr('data-ge-content-type');
1475
- var text = textFor(type);
1476
- if (!text) { return; }
1477
-
1478
- // Not marked active, so the next click tries again: the library
1479
- // may be loaded by then
1480
- if (text.available && !text.available()) {
1481
- if (text.missingKey) { console.error(t(text.missingKey)); }
1482
- return;
1448
+ if (!block.is(':visible')) { return; }
1449
+
1450
+ var offers = textOffers();
1451
+ if (!offers.length) { return; }
1452
+
1453
+ if (offers.length === 1) {
1454
+ convertPlain(block, offers[0]);
1455
+ } else {
1456
+ openConvertPicker(block, offers);
1483
1457
  }
1458
+ }
1459
+
1460
+ /**
1461
+ * Plain content made a text of `offer`'s type, through the convert
1462
+ * events: the type goes on, and the plugin that declared it takes the
1463
+ * block over and opens its editor. Nothing else about the node
1464
+ * changes. False when the library is missing or a handler canceled.
1465
+ */
1466
+ function convertPlain(block, offer) {
1467
+ // Not converted, so the next click tries again: the library may
1468
+ // be loaded by then
1469
+ if (offer.available && !offer.available()) {
1470
+ if (offer.missingKey) { console.error(t(offer.missingKey)); }
1471
+ return false;
1472
+ }
1473
+
1474
+ return operate(function() {
1475
+ var payload = payloadFor('plain', block, { source: 'tool', from: 'plain', to: offer.type });
1476
+
1477
+ if (!emit('before-convert', payload)) { return false; }
1478
+
1479
+ block.addClass('ge-content-type-' + offer.type).attr('data-ge-content-type', offer.type);
1480
+
1481
+ // Its plain drawer goes: the block is the plugin's now, and
1482
+ // the plugin gives it the drawer a text has
1483
+ block.parent('.ge-text-block').removeClass('ge-plain-block').children('.ge-tools-drawer').remove();
1484
+
1485
+ emit('after-convert', payload);
1486
+ offer.edit(block);
1487
+
1488
+ return true;
1489
+ });
1490
+ }
1491
+
1492
+ /**
1493
+ * The text types offered, as a strip under the plain content's
1494
+ * drawer, when there is more than one to choose from.
1495
+ */
1496
+ function openConvertPicker(block, offers) {
1497
+ var textBlock = block.parent('.ge-text-block');
1498
+
1499
+ openPicker(textBlock.children('.ge-tools-drawer'), offers.map(function(offer) {
1500
+ return {
1501
+ label: offer.label,
1502
+ title: t('tool.convert_type', { editor: offer.label }),
1503
+ attributes: { 'data-ge-content-type': offer.type },
1504
+ choose: function() { convertPlain(block, offer); },
1505
+ };
1506
+ }), 'ge-text-picker ge-convert-picker', textBlock);
1507
+ }
1508
+
1509
+ /**
1510
+ * A choice as a strip under `anchor` - a tool, or a drawer - one
1511
+ * button per `{ label, title, attributes, choose }`. Leaving `hover`
1512
+ * (the anchor by default) withdraws it, and so does closeSizePicker.
1513
+ */
1514
+ function openPicker(anchor, choices, className, hover) {
1515
+ closeSizePicker();
1516
+
1517
+ anchor.closest('.ge-tools-drawer').addClass('ge-picker-open');
1518
+ sizePicker = $('<div class="ge-size-picker" />').addClass(className || '').appendTo(anchor);
1519
+
1520
+ choices.forEach(function(choice) {
1521
+ $('<a class="ge-size ge-size-flex" />')
1522
+ .attr(choice.attributes || {})
1523
+ .attr('title', choice.title || choice.label)
1524
+ .text(choice.label)
1525
+ .on('click', function(e) {
1526
+ e.preventDefault();
1527
+ e.stopPropagation();
1528
+
1529
+ closeSizePicker();
1530
+ choice.choose();
1531
+ })
1532
+ .appendTo(sizePicker)
1533
+ ;
1534
+ });
1535
+
1536
+ (hover || anchor).one('mouseleave', function() {
1537
+ window.setTimeout(function() {
1538
+ if (sizePicker && !sizePicker.is(':hover')) { closeSizePicker(); }
1539
+ }, 400);
1540
+ });
1541
+ }
1542
+
1543
+ /**
1544
+ * Every text type the feature plugins declare, in the order they
1545
+ * were registered: the first plugin to declare a type owns it.
1546
+ */
1547
+ function textTypes() {
1548
+ var owners = {};
1549
+ var entries = [];
1550
+
1551
+ $.each(FEATURES, function(name, feature) {
1552
+ if (!feature.textTypes) { return; }
1553
+
1554
+ (feature.textTypes() || []).forEach(function(entry) {
1555
+ if (owners[entry.type]) {
1556
+ if (owners[entry.type] !== name) {
1557
+ warnOnceHere('text-type:' + entry.type, 'the "' + name + '" plugin declares the ' +
1558
+ 'text type "' + entry.type + '", which the "' + owners[entry.type] +
1559
+ '" plugin already declares: ignored');
1560
+ }
1561
+ return;
1562
+ }
1563
+
1564
+ owners[entry.type] = name;
1565
+ entries.push(entry);
1566
+ });
1567
+ });
1568
+
1569
+ return entries;
1570
+ }
1484
1571
 
1485
- $(this).data('ge-text-before', attributesOf(this)).removeData('ge-text-ready');
1486
- $(this).addClass('ge-rte-active');
1487
- text.start($(this));
1572
+ /** The types plain content can be made, in order: the ones not marked `offered: false`. */
1573
+ function textOffers() {
1574
+ return textTypes().filter(function(entry) { return entry.offered !== false; });
1575
+ }
1576
+
1577
+ /** Whether a plugin declares this text type. */
1578
+ function hasTextOwner(type) {
1579
+ return textTypes().some(function(entry) { return entry.type === type; });
1488
1580
  }
1489
1581
 
1490
1582
  function reset() {
@@ -1521,9 +1613,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1521
1613
  // Its panel goes home to its drawer first, and both go together
1522
1614
  closeSettings();
1523
1615
  canvas.removeClass('ge-editing ge-drag-drawer ge-dropping');
1524
- canvas.find('.ge-content').each(function() {
1525
- closeText($(this));
1526
- });
1616
+ // Before the drawers and the text blocks come off: an editor
1617
+ // plugin closes its editors here, and finds them where it left them
1618
+ plugins('onBeforeDeinit');
1527
1619
  closeSizePicker();
1528
1620
  hideDropMarker();
1529
1621
  canvas.find('.ge-tools-drawer').remove();
@@ -1576,7 +1668,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1576
1668
  mainControls.remove();
1577
1669
  htmlTextArea.remove();
1578
1670
  $(window).off('scroll', onScroll);
1579
- canvas.off('click', '.ge-content', initRTE);
1671
+ canvas.off('click', '.ge-content', onContentClick);
1580
1672
  canvas.off('ge-rte-ready', '.ge-content');
1581
1673
  canvas.removeData('grideditor');
1582
1674
  }
@@ -1627,30 +1719,27 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1627
1719
  return !settings.plugins || settings.plugins.indexOf(name) !== -1;
1628
1720
  };
1629
1721
 
1722
+ // What the plugins setting does not choose: the text editors,
1723
+ // which content_types chooses. A page that names its containers
1724
+ // there has named no editor, and still wants the one it uses.
1725
+ var featureWanted = function(name, factory) {
1726
+ return factory.always === true || wanted(name);
1727
+ };
1728
+
1630
1729
  $.each($.fn.gridEditor.containers, function(type, factory) {
1631
1730
  if (wanted(type)) { CONTAINERS[type] = factory(api); }
1632
1731
  });
1633
1732
 
1634
1733
  $.each($.fn.gridEditor.features, function(name, factory) {
1635
- if (wanted(name)) { FEATURES[name] = factory(api); }
1636
- });
1637
-
1638
- // Text editors are chosen by content_types rather than by the
1639
- // plugins setting: a page that names its containers there has
1640
- // named no editor, and still wants the one it uses
1641
- $.each($.fn.gridEditor.texts, function(type, factory) {
1642
- TEXTS[type] = factory(api);
1734
+ if (featureWanted(name, factory)) { FEATURES[name] = factory(api); }
1643
1735
  });
1644
1736
 
1645
- // 5.x's registry, for an integration a host wrote itself. One the
1646
- // plugins above already provide is theirs.
1647
- $.each($.fn.gridEditor.RTEs, function(type, rte) {
1648
- if (TEXTS[type]) { return; }
1649
-
1650
- warnOnceHere('rtes:' + type, '$.fn.gridEditor.RTEs is deprecated and will be removed ' +
1651
- 'in 7.0: register "' + type + '" under $.fn.gridEditor.texts, see docs/plugins.md');
1652
- TEXTS[type] = legacyText(rte);
1653
- });
1737
+ // 5.x's registry of text editors is gone: an editor of a host's
1738
+ // own is a plugin of its own since 6.0
1739
+ if (!$.isEmptyObject($.fn.gridEditor.RTEs || {})) {
1740
+ warnOnceHere('rtes', '$.fn.gridEditor.RTEs was removed in 6.0 and what is registered there ' +
1741
+ 'is ignored: write the editor as a plugin, see docs/plugins.md');
1742
+ }
1654
1743
 
1655
1744
  $.each($.fn.gridEditor.utilities, function(name, factory) {
1656
1745
  if (!wanted(name)) { return; }
@@ -1731,6 +1820,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1731
1820
  // A text editor has rewritten a content area, so whatever the
1732
1821
  // editor and its plugins had put in there goes back in
1733
1822
  textReady: function(block) { block.trigger('ge-rte-ready'); },
1823
+ // A choice under a tool, offered by holding it
1824
+ attachPicker: attachPicker,
1825
+ openPicker: openPicker,
1826
+ closePicker: closeSizePicker,
1734
1827
  // A node's settings panel, in its drawer or open outside it
1735
1828
  detailsOf: detailsOf,
1736
1829
  // A node's drawer: its first child, or for a content area the
@@ -1804,6 +1897,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1804
1897
  * name the ones it means.
1805
1898
  */
1806
1899
  function appliesTo(family, kind) {
1900
+ // The host's plain content reads and previews its utilities as a
1901
+ // text does; with no gear, nothing in its drawer writes them
1902
+ if (kind === 'plain') { kind = 'text'; }
1807
1903
  if (family.appliesTo.indexOf(kind) !== -1) { return true; }
1808
1904
 
1809
1905
  return !!CONTAINERS[kind] && family.appliesTo.indexOf('container') !== -1;
@@ -2273,7 +2369,6 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2273
2369
  * -------------------------------------------------------------- */
2274
2370
 
2275
2371
  var CONTAINERS = {}; // The container plugins in use, by the type each builds
2276
- var TEXTS = {}; // The text editor plugins, by the content type each edits
2277
2372
  var FEATURES = {}; // The feature plugins in use, by name
2278
2373
  var UTILITIES = {}; // The utility plugins in use, by name
2279
2374
  var featureMethods = {}; // The methods those features contribute
@@ -2724,22 +2819,6 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2724
2819
  });
2725
2820
  });
2726
2821
 
2727
- // A text block of the first editor offered; held, the tool
2728
- // offers each of them when there is more than one
2729
- var texts = offeredTexts();
2730
- if (texts.length) {
2731
- createTool(drawer, t('tool.add_text'), 'ge-add-text', 'bi bi-type', function() {
2732
- if (closeSizePicker()) { return; } // The picker was open: that was the answer
2733
-
2734
- addTextTo(col, texts[0]);
2735
- });
2736
-
2737
- if (texts.length > 1) {
2738
- var textTool = drawer.find('> .ge-add-text');
2739
- attachPicker(textTool, function() { openTextPicker(textTool, col); });
2740
- }
2741
- }
2742
-
2743
2822
  createTool(drawer, t('tool.add_row'), 'ge-add-row', 'bi bi-plus-circle', function() {
2744
2823
  // An empty row: the columns in it are the next decision,
2745
2824
  // and its drawer's add column tool is where that is made
@@ -4202,7 +4281,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4202
4281
 
4203
4282
  var column = createColumn(size, options.offset);
4204
4283
  if (options.content !== undefined) {
4205
- column.find('.ge-content').html(options.content);
4284
+ column.append(contentFor(options.content));
4206
4285
  }
4207
4286
 
4208
4287
  return place(column, 'column', options);
@@ -4270,10 +4349,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4270
4349
  * the all view. `offset` indents it, within the same 12 unit budget.
4271
4350
  */
4272
4351
  function createColumn(size, offset) {
4273
- var text = textFor(settings.content_types[0]);
4274
- var column = $('<div class="column"/>')
4275
- .append(createDefaultContentWrapper().html(text ? text.initialContent : ''))
4276
- ;
4352
+ // Empty: what goes in it is the next decision, and the tools of
4353
+ // the column and the toolbar are where it is made
4354
+ var column = $('<div class="column"/>');
4277
4355
 
4278
4356
  // The tier being edited, or the base class in the all view
4279
4357
  var tier = tiersFor(curView)[0];
@@ -4367,8 +4445,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4367
4445
  return;
4368
4446
  } else if (kept) {
4369
4447
  // Of the same type, or of none if it had none
4370
- placed = (type ? createDefaultContentWrapper(type) : $('<div class="ge-content" />'))
4371
- .append(piece.text);
4448
+ placed = createDefaultContentWrapper(type).append(piece.text);
4372
4449
  } else {
4373
4450
  // The content area itself, moved along if an
4374
4451
  // element came before the first text
@@ -4424,7 +4501,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4424
4501
  }
4425
4502
 
4426
4503
  /**
4427
- * Wrap a run of loose column content in a content area, and hand back
4504
+ * Wrap a run of loose column content in a content area of no type -
4505
+ * the host's plain content - and hand back
4428
4506
  * an empty set: the caller has to forget what it just wrapped, or the
4429
4507
  * next boundary wraps the same nodes again and leaves the first
4430
4508
  * wrapper behind, empty.
@@ -4439,12 +4517,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4439
4517
  }
4440
4518
 
4441
4519
  /**
4442
- * A content area for a content type, the first one by default. With
4443
- * no content types it is a content area and nothing more: there is no
4444
- * type to name.
4520
+ * A content area of a text type, or with none the host's plain
4521
+ * content: a content area and nothing more.
4445
4522
  */
4446
4523
  function createDefaultContentWrapper(type) {
4447
- type = type || settings.content_types[0];
4448
4524
  var contentArea = $('<div class="ge-content"/>');
4449
4525
 
4450
4526
  if (type) {
@@ -4455,22 +4531,23 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4455
4531
  }
4456
4532
 
4457
4533
  /* --------------------------------------------------------------
4458
- * Text blocks: a content area as a block of its own, with a drawer.
4534
+ * Content blocks: a content area as a block of its own, with a drawer.
4459
4535
  *
4460
4536
  * The drawer cannot go inside the content area, as every other
4461
- * block's does: what is inside is the text editor's, which would make
4462
- * the drawer editable text, copy it into its own document or take it
4463
- * away on an undo. So while editing, each content area in a column
4464
- * sits in a .ge-text-block beside its drawer, and that wrapper is the
4465
- * block the column moves. deinit takes it off again: it is never part
4466
- * of the markup.
4537
+ * block's does: what is inside may be a text editor's, which would
4538
+ * make the drawer editable text, copy it into its own document or
4539
+ * take it away on an undo. So while editing, each content area in a
4540
+ * column sits in a .ge-text-block beside its drawer, and that wrapper
4541
+ * is the block the column moves. deinit takes it off again: it is
4542
+ * never part of the markup.
4543
+ *
4544
+ * A content area with no type is the host's plain content, and its
4545
+ * drawer is the editor's: move and delete, nothing else. One whose
4546
+ * type a plugin declares is that plugin's text, and the plugin gives
4547
+ * it its drawer. One whose type nobody declares is a text whose
4548
+ * editor is not loaded: a block to move and delete, not to edit.
4467
4549
  * -------------------------------------------------------------- */
4468
4550
 
4469
- /** The text editors this editor offers, in content_types order: the ones loaded. */
4470
- function offeredTexts() {
4471
- return settings.content_types.filter(function(type) { return !!TEXTS[type]; });
4472
- }
4473
-
4474
4551
  function wrapTexts() {
4475
4552
  canvas.find('.column > .ge-content').each(function() {
4476
4553
  $(this).wrap('<div class="ge-text-block" />');
@@ -4478,7 +4555,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4478
4555
 
4479
4556
  canvas.find('.ge-text-block').each(function() {
4480
4557
  var textBlock = $(this);
4481
- if (!textBlock.children('.ge-tools-drawer').length) { createTextControls(textBlock); }
4558
+ if (textBlock.children('.ge-tools-drawer').length) { return; }
4559
+
4560
+ var type = textBlock.children('.ge-content').attr('data-ge-content-type');
4561
+
4562
+ if (!type) {
4563
+ createPlainControls(textBlock);
4564
+ } else if (!hasTextOwner(type)) {
4565
+ createOrphanControls(textBlock, type);
4566
+ }
4482
4567
  });
4483
4568
  }
4484
4569
 
@@ -4489,34 +4574,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4489
4574
  });
4490
4575
  }
4491
4576
 
4492
- function createTextControls(textBlock) {
4577
+ /** The host's plain content: moved and deleted here, edited in the source. */
4578
+ function createPlainControls(textBlock) {
4493
4579
  var block = textBlock.children('.ge-content');
4494
- var type = block.attr('data-ge-content-type');
4495
- var drawer = $('<div class="ge-tools-drawer ge-text-drawer" />').prependTo(textBlock);
4580
+ var drawer = $('<div class="ge-tools-drawer ge-text-drawer ge-plain-drawer" />').prependTo(textBlock);
4581
+ textBlock.addClass('ge-plain-block');
4496
4582
 
4497
4583
  createMoveTool(drawer);
4498
-
4499
- // Which editor edits it, or that none can: a type whose plugin is
4500
- // not loaded is still a block to move and delete, but not to edit
4501
- if (type && TEXTS[type]) {
4502
- createTool(drawer, t('tool.text_info', { editor: textLabel(type) }), 'ge-text-info', 'bi bi-fonts');
4503
- } else if (type) {
4504
- createTool(drawer, t('text.no_editor', { type: type }), 'ge-text-info ge-text-missing',
4505
- 'bi bi-exclamation-triangle');
4506
- }
4507
-
4508
- addSettingsTool(drawer, block, settings.text_classes);
4509
-
4510
- settings.text_tools.forEach(function(hostTool) {
4511
- createTool(drawer, hostTool.title || '', hostTool.className || '',
4512
- hostTool.iconClass || 'bi bi-wrench', hostTool.on);
4513
- });
4514
-
4515
- createTool(drawer, t('tool.delete_text'), 'ge-delete-text', 'bi bi-trash', function() {
4516
- deleteNode('text', block, t('confirm.delete_text'), function(removed) {
4517
- // Its editor lets go first: removed while open, it would
4518
- // be left behind, attached to nothing
4519
- closeText(block);
4584
+ createTool(drawer, t('tool.delete_plain'), 'ge-delete-plain', 'bi bi-trash', function() {
4585
+ deleteNode('plain', block, t('confirm.delete_plain'), function(removed) {
4520
4586
  textBlock.slideUp(function() {
4521
4587
  textBlock.remove();
4522
4588
  removed();
@@ -4526,162 +4592,34 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4526
4592
  }
4527
4593
 
4528
4594
  /**
4529
- * Close the text editor on one content area, and leave the content
4530
- * area's own attributes as the host left them.
4531
- *
4532
- * An editor adds attributes of its own while it is open, and takes
4533
- * them off again, more or less: CKEditor leaves aria-readonly behind.
4534
- * tinyMCE does worse, and puts back every attribute as it was when it
4535
- * opened, so an id, a class or a plugin's attribute given while it was
4536
- * open is lost. So the attributes are read three times - before the
4537
- * editor opens, once it has opened, and before it closes - and what
4538
- * changed between the second and the third, the host's doing, is
4539
- * played back onto the first, the host's markup. What the editor did
4540
- * is left out either way.
4595
+ * A text whose editor is not loaded: still a block to move and delete,
4596
+ * and its drawer says why it cannot be edited.
4541
4597
  */
4542
- function closeText(block) {
4543
- var text = textFor(block.attr('data-ge-content-type'));
4544
- var before = block.data('ge-text-before');
4545
- var open = before ? attributesOf(block[0]) : null;
4546
-
4547
- // Every content area is told, as in 5.x: one whose editor never
4548
- // started is the plugin's to ignore
4549
- if (text) { text.stop(block); }
4550
-
4551
- // After stop, not before: an editor that restores the class
4552
- // attribute it snapshotted would put ge-rte-active back
4553
- block.removeClass('ge-rte-active');
4554
-
4555
- if (before) {
4556
- restoreAttributes(block, before, block.data('ge-text-ready') || before, open);
4557
- block.removeData('ge-text-before').removeData('ge-text-ready');
4558
- }
4559
- }
4560
-
4561
- /** What a text editor puts on a content area while it is open, whenever it likes. */
4562
- var EDITOR_CLASS = /^(mce-|cke|note-)|^(active|ge-rte-active)$/;
4563
- var EDITOR_ATTRIBUTE = /^(data-mce-|contenteditable$|spellcheck$)/;
4564
-
4565
- function attributesOf(element) {
4566
- var found = {};
4567
- $.each($.makeArray(element.attributes), function(i, attribute) {
4568
- found[attribute.name] = attribute.value;
4569
- });
4570
- return found;
4571
- }
4572
-
4573
- function classesIn(value) {
4574
- return (value || '').split(/\s+/).filter(function(name) {
4575
- return name !== '' && !EDITOR_CLASS.test(name);
4576
- });
4577
- }
4578
-
4579
- /** The host's markup, with what changed from `ready` to `open` played back onto it. */
4580
- function restoreAttributes(block, before, ready, open) {
4581
- var result = $.extend({}, before);
4582
-
4583
- $.each($.extend({}, ready, open), function(name) {
4584
- if (name === 'class' || EDITOR_ATTRIBUTE.test(name) || ready[name] === open[name]) { return; }
4585
-
4586
- if (open[name] === undefined) {
4587
- delete result[name];
4588
- } else {
4589
- result[name] = open[name];
4590
- }
4591
- });
4598
+ function createOrphanControls(textBlock, type) {
4599
+ var block = textBlock.children('.ge-content');
4600
+ var drawer = $('<div class="ge-tools-drawer ge-text-drawer" />').prependTo(textBlock);
4592
4601
 
4593
- var readyClasses = classesIn(ready['class']);
4594
- var openClasses = classesIn(open['class']);
4595
- var classes = classesIn(before['class']).filter(function(name) {
4596
- return openClasses.indexOf(name) !== -1 || readyClasses.indexOf(name) === -1;
4597
- });
4598
- openClasses.forEach(function(name) {
4599
- if (readyClasses.indexOf(name) === -1 && classes.indexOf(name) === -1) { classes.push(name); }
4602
+ createMoveTool(drawer);
4603
+ createTool(drawer, t('text.no_editor', { type: type }), 'ge-text-info ge-text-missing',
4604
+ 'bi bi-exclamation-triangle');
4605
+ createTool(drawer, t('tool.delete_text'), 'ge-delete-text', 'bi bi-trash', function() {
4606
+ deleteNode('text', block, t('confirm.delete_text'), function(removed) {
4607
+ textBlock.slideUp(function() {
4608
+ textBlock.remove();
4609
+ removed();
4610
+ });
4611
+ });
4600
4612
  });
4601
-
4602
- // In the host's order, class where it was: getHtml should not
4603
- // shuffle a node's attributes because it was edited
4604
- if (classes.length) { result['class'] = classes.join(' '); } else { delete result['class']; }
4605
-
4606
- $.each(attributesOf(block[0]), function(name) { block.removeAttr(name); });
4607
- $.each(result, function(name, value) { block.attr(name, value); });
4608
- }
4609
-
4610
- /** What a text editor is called: its plugin's label, or its type. */
4611
- function textLabel(type) {
4612
- var text = TEXTS[type];
4613
- return text && text.labelKey ? t(text.labelKey) : type;
4614
- }
4615
-
4616
- /** A detached content area of `type`, holding `content` or the editor's initial content. */
4617
- function makeText(type, content) {
4618
- var text = TEXTS[type];
4619
- return createDefaultContentWrapper(type)
4620
- .html(content !== undefined ? content : (text && text.initialContent) || '');
4621
- }
4622
-
4623
- /** A text block at the end of a column, through the add events. */
4624
- function addTextTo(col, type) {
4625
- var block = makeText(type);
4626
-
4627
- return addNode('text', block, function() {
4628
- col.append(block);
4629
- }, { parent: col, source: 'tool' });
4630
4613
  }
4631
4614
 
4632
4615
  /**
4633
- * createText(type?, options?): a content area of a text editor's,
4634
- * detached unless a placement is given. The type is the first one
4635
- * offered by default. `options.content` is its html.
4616
+ * A detached content area holding `content`: a text of the first type
4617
+ * offered, or the host's plain content when no editor is offered.
4636
4618
  */
4637
- function apiCreateText(type, options) {
4638
- if (type && typeof type === 'object') {
4639
- options = type;
4640
- type = undefined;
4641
- }
4642
- options = options || {};
4643
- type = type || offeredTexts()[0];
4644
-
4645
- if (!type || !TEXTS[type]) {
4646
- warnOnceHere('createText:' + type, 'createText: no text editor "' + type + '" is loaded; ' +
4647
- 'load its plugin and name it in content_types');
4648
- return null;
4649
- }
4650
-
4651
- return place(makeText(type, options.content), 'text', options);
4652
- }
4653
-
4654
- /**
4655
- * The text editors offered, as a strip under the add text tool, when
4656
- * there is more than one to choose from.
4657
- */
4658
- function openTextPicker(tool, col) {
4659
- closeSizePicker();
4660
-
4661
- tool.closest('.ge-tools-drawer').addClass('ge-picker-open');
4662
- sizePicker = $('<div class="ge-size-picker ge-text-picker" />').appendTo(tool);
4663
-
4664
- offeredTexts().forEach(function(type) {
4665
- $('<a class="ge-size ge-size-flex" />')
4666
- .attr('data-ge-content-type', type)
4667
- .attr('title', t('tool.add_text_type', { editor: textLabel(type) }))
4668
- .text(textLabel(type))
4669
- .on('click', function(e) {
4670
- e.preventDefault();
4671
- e.stopPropagation();
4672
-
4673
- closeSizePicker();
4674
- addTextTo(col, type);
4675
- })
4676
- .appendTo(sizePicker)
4677
- ;
4678
- });
4619
+ function contentFor(content) {
4620
+ var offers = textOffers();
4679
4621
 
4680
- tool.one('mouseleave', function() {
4681
- window.setTimeout(function() {
4682
- if (sizePicker && !sizePicker.is(':hover')) { closeSizePicker(); }
4683
- }, 400);
4684
- });
4622
+ return createDefaultContentWrapper(offers.length ? offers[0].type : null).html(content);
4685
4623
  }
4686
4624
 
4687
4625
  /**
@@ -4738,23 +4676,6 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4738
4676
  return curView;
4739
4677
  }
4740
4678
 
4741
- /** The text editor plugin for a content type, or null. */
4742
- function textFor(type) {
4743
- return TEXTS[type] || null;
4744
- }
4745
-
4746
- /**
4747
- * An integration written to 5.x's contract - init and deinit over a
4748
- * set of content areas - as a text editor plugin.
4749
- */
4750
- function legacyText(rte) {
4751
- return {
4752
- initialContent: rte.initialContent,
4753
- start: function(block) { rte.init(settings, block); },
4754
- stop: function(block) { rte.deinit(settings, block); },
4755
- };
4756
- }
4757
-
4758
4679
  /**
4759
4680
  * The instance handle, documented API as of 3.0: the methods the
4760
4681
  * plugin dispatches, plus the settings and the canvas. A host holding
@@ -4774,7 +4695,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4774
4695
  getView: getView,
4775
4696
  createRow: apiCreateRow,
4776
4697
  createColumn: apiCreateColumn,
4777
- createText: apiCreateText,
4698
+ createText: function(type, options) {
4699
+ if (!featureMethods.createText) {
4700
+ warnOnceHere('plugin:text', 'createText needs a text editor plugin: include ' +
4701
+ 'dist/plugins/grideditor.tinymce.js, or another editor\'s, after the editor');
4702
+ return null;
4703
+ }
4704
+
4705
+ return featureMethods.createText(type, options);
4706
+ },
4778
4707
  createElement: function(content, options) {
4779
4708
  if (!featureMethods.createElement) {
4780
4709
  warnOnceHere('plugin:elements', 'createElement needs the elements plugin: ' +
@@ -4819,6 +4748,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4819
4748
  baseElem.data('grideditor', handle);
4820
4749
 
4821
4750
  loadPlugins();
4751
+ // Again, with what the plugins filled in: the text editors'
4752
+ // content_types when the host gave none
4753
+ handle.settings = settingsCopy();
4822
4754
  setup();
4823
4755
  init();
4824
4756
 
@@ -4829,14 +4761,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4829
4761
  };
4830
4762
 
4831
4763
  /**
4832
- * Text editor plugins: tinyMCE, CKEditor, summernote, and whatever a host
4833
- * writes. A factory registered under the content type it edits, called once
4834
- * per editor with the handle described in docs/plugins.md, returning
4835
- * { labelKey, initialContent, available, missingKey, start, stop }.
4764
+ * 5.x's registry of text editors, removed in 6.0: still there to register
4765
+ * into, so a page written for 5.x does not throw, but what it holds is
4766
+ * ignored with a warning. The text editors are plugins since 6.0.
4836
4767
  */
4837
- $.fn.gridEditor.texts = {};
4838
-
4839
- /** 5.x's registry of text editors, adapted with a warning. Removed in 7.0. */
4840
4768
  $.fn.gridEditor.RTEs = {};
4841
4769
 
4842
4770
  /**
@@ -4892,12 +4820,9 @@ $.fn.gridEditor.locales = {
4892
4820
  'tool.move': 'Move',
4893
4821
  'tool.settings': 'Settings',
4894
4822
  'tool.add_row': 'Add row',
4895
- 'tool.add_text': 'Add text',
4896
- 'tool.add_text_type': 'Add {editor} text',
4897
- 'tool.text_info': 'Text: {editor}',
4898
4823
  'tool.delete_text': 'Remove text',
4899
- 'text.add': 'Text',
4900
- 'text.add_type': 'Text ({editor})',
4824
+ 'tool.delete_plain': 'Remove content',
4825
+ 'tool.convert_type': 'Edit as {editor} text',
4901
4826
  'text.no_editor': 'No text editor "{type}" is loaded: this text can be moved and deleted, not edited',
4902
4827
  'tool.add_column': 'Add column\n(hold to choose the width)',
4903
4828
  'tool.column_size': '{size} of 12',
@@ -4921,7 +4846,6 @@ $.fn.gridEditor.locales = {
4921
4846
  'panel.classes': 'Classes',
4922
4847
  'panel.kind_row': 'Row',
4923
4848
  'panel.kind_column': 'Column',
4924
- 'panel.kind_text': 'Text',
4925
4849
  'panel.kind_element': 'Element',
4926
4850
  'panel.kind_section': 'Section',
4927
4851
  'panel.kind_tab': 'Tab',
@@ -4939,6 +4863,7 @@ $.fn.gridEditor.locales = {
4939
4863
  'confirm.delete_row': 'Delete row?',
4940
4864
  'confirm.delete_column': 'Delete column?',
4941
4865
  'confirm.delete_text': 'Delete this text?',
4866
+ 'confirm.delete_plain': 'Delete this content?',
4942
4867
  'confirm.delete_container': 'Delete this container and everything in it?',
4943
4868
  'view.all': 'All sizes',
4944
4869
  'view.xs': 'Phone',