@themarioga/grid-editor 6.0.0-beta.3 → 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 +50 -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 -385
  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 +3 -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 +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 +12 -3
  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 -385
  35. package/src/js/locales/grideditor.es.js +3 -0
  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);
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;
1483
1472
  }
1484
1473
 
1485
- $(this).data('ge-text-before', attributesOf(this)).removeData('ge-text-ready');
1486
- $(this).addClass('ge-rte-active');
1487
- text.start($(this));
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
+ }
1571
+
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); }
1734
+ if (featureWanted(name, factory)) { FEATURES[name] = factory(api); }
1636
1735
  });
1637
1736
 
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);
1643
- });
1644
-
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,41 +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
- // That no editor can edit it: a type whose plugin is not loaded is
4500
- // still a block to move and delete, but not to edit
4501
- if (type && !TEXTS[type]) {
4502
- createTool(drawer, t('text.no_editor', { type: type }), 'ge-text-info ge-text-missing',
4503
- 'bi bi-exclamation-triangle');
4504
- }
4505
-
4506
- var details = addSettingsTool(drawer, block, settings.text_classes);
4507
-
4508
- // Which editor edits it, first in its settings: something to
4509
- // know about it, not something to do with it
4510
- if (type && TEXTS[type]) {
4511
- $('<div class="ge-field ge-text-editor" />')
4512
- .append($('<span class="ge-field-label" />').text(t('panel.editor')))
4513
- .append($('<span class="ge-field-value" />').text(textLabel(type)))
4514
- .prependTo(details.children('.ge-details-general'));
4515
- }
4516
-
4517
- settings.text_tools.forEach(function(hostTool) {
4518
- createTool(drawer, hostTool.title || '', hostTool.className || '',
4519
- hostTool.iconClass || 'bi bi-wrench', hostTool.on);
4520
- });
4521
-
4522
- createTool(drawer, t('tool.delete_text'), 'ge-delete-text', 'bi bi-trash', function() {
4523
- deleteNode('text', block, t('confirm.delete_text'), function(removed) {
4524
- // Its editor lets go first: removed while open, it would
4525
- // be left behind, attached to nothing
4526
- 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) {
4527
4586
  textBlock.slideUp(function() {
4528
4587
  textBlock.remove();
4529
4588
  removed();
@@ -4533,162 +4592,34 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4533
4592
  }
4534
4593
 
4535
4594
  /**
4536
- * Close the text editor on one content area, and leave the content
4537
- * area's own attributes as the host left them.
4538
- *
4539
- * An editor adds attributes of its own while it is open, and takes
4540
- * them off again, more or less: CKEditor leaves aria-readonly behind.
4541
- * tinyMCE does worse, and puts back every attribute as it was when it
4542
- * opened, so an id, a class or a plugin's attribute given while it was
4543
- * open is lost. So the attributes are read three times - before the
4544
- * editor opens, once it has opened, and before it closes - and what
4545
- * changed between the second and the third, the host's doing, is
4546
- * played back onto the first, the host's markup. What the editor did
4547
- * 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.
4548
4597
  */
4549
- function closeText(block) {
4550
- var text = textFor(block.attr('data-ge-content-type'));
4551
- var before = block.data('ge-text-before');
4552
- var open = before ? attributesOf(block[0]) : null;
4553
-
4554
- // Every content area is told, as in 5.x: one whose editor never
4555
- // started is the plugin's to ignore
4556
- if (text) { text.stop(block); }
4557
-
4558
- // After stop, not before: an editor that restores the class
4559
- // attribute it snapshotted would put ge-rte-active back
4560
- block.removeClass('ge-rte-active');
4561
-
4562
- if (before) {
4563
- restoreAttributes(block, before, block.data('ge-text-ready') || before, open);
4564
- block.removeData('ge-text-before').removeData('ge-text-ready');
4565
- }
4566
- }
4567
-
4568
- /** What a text editor puts on a content area while it is open, whenever it likes. */
4569
- var EDITOR_CLASS = /^(mce-|cke|note-)|^(active|ge-rte-active)$/;
4570
- var EDITOR_ATTRIBUTE = /^(data-mce-|contenteditable$|spellcheck$)/;
4571
-
4572
- function attributesOf(element) {
4573
- var found = {};
4574
- $.each($.makeArray(element.attributes), function(i, attribute) {
4575
- found[attribute.name] = attribute.value;
4576
- });
4577
- return found;
4578
- }
4579
-
4580
- function classesIn(value) {
4581
- return (value || '').split(/\s+/).filter(function(name) {
4582
- return name !== '' && !EDITOR_CLASS.test(name);
4583
- });
4584
- }
4585
-
4586
- /** The host's markup, with what changed from `ready` to `open` played back onto it. */
4587
- function restoreAttributes(block, before, ready, open) {
4588
- var result = $.extend({}, before);
4589
-
4590
- $.each($.extend({}, ready, open), function(name) {
4591
- if (name === 'class' || EDITOR_ATTRIBUTE.test(name) || ready[name] === open[name]) { return; }
4592
-
4593
- if (open[name] === undefined) {
4594
- delete result[name];
4595
- } else {
4596
- result[name] = open[name];
4597
- }
4598
- });
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);
4599
4601
 
4600
- var readyClasses = classesIn(ready['class']);
4601
- var openClasses = classesIn(open['class']);
4602
- var classes = classesIn(before['class']).filter(function(name) {
4603
- return openClasses.indexOf(name) !== -1 || readyClasses.indexOf(name) === -1;
4604
- });
4605
- openClasses.forEach(function(name) {
4606
- 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
+ });
4607
4612
  });
4608
-
4609
- // In the host's order, class where it was: getHtml should not
4610
- // shuffle a node's attributes because it was edited
4611
- if (classes.length) { result['class'] = classes.join(' '); } else { delete result['class']; }
4612
-
4613
- $.each(attributesOf(block[0]), function(name) { block.removeAttr(name); });
4614
- $.each(result, function(name, value) { block.attr(name, value); });
4615
- }
4616
-
4617
- /** What a text editor is called: its plugin's label, or its type. */
4618
- function textLabel(type) {
4619
- var text = TEXTS[type];
4620
- return text && text.labelKey ? t(text.labelKey) : type;
4621
- }
4622
-
4623
- /** A detached content area of `type`, holding `content` or the editor's initial content. */
4624
- function makeText(type, content) {
4625
- var text = TEXTS[type];
4626
- return createDefaultContentWrapper(type)
4627
- .html(content !== undefined ? content : (text && text.initialContent) || '');
4628
- }
4629
-
4630
- /** A text block at the end of a column, through the add events. */
4631
- function addTextTo(col, type) {
4632
- var block = makeText(type);
4633
-
4634
- return addNode('text', block, function() {
4635
- col.append(block);
4636
- }, { parent: col, source: 'tool' });
4637
4613
  }
4638
4614
 
4639
4615
  /**
4640
- * createText(type?, options?): a content area of a text editor's,
4641
- * detached unless a placement is given. The type is the first one
4642
- * 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.
4643
4618
  */
4644
- function apiCreateText(type, options) {
4645
- if (type && typeof type === 'object') {
4646
- options = type;
4647
- type = undefined;
4648
- }
4649
- options = options || {};
4650
- type = type || offeredTexts()[0];
4651
-
4652
- if (!type || !TEXTS[type]) {
4653
- warnOnceHere('createText:' + type, 'createText: no text editor "' + type + '" is loaded; ' +
4654
- 'load its plugin and name it in content_types');
4655
- return null;
4656
- }
4657
-
4658
- return place(makeText(type, options.content), 'text', options);
4659
- }
4660
-
4661
- /**
4662
- * The text editors offered, as a strip under the add text tool, when
4663
- * there is more than one to choose from.
4664
- */
4665
- function openTextPicker(tool, col) {
4666
- closeSizePicker();
4667
-
4668
- tool.closest('.ge-tools-drawer').addClass('ge-picker-open');
4669
- sizePicker = $('<div class="ge-size-picker ge-text-picker" />').appendTo(tool);
4670
-
4671
- offeredTexts().forEach(function(type) {
4672
- $('<a class="ge-size ge-size-flex" />')
4673
- .attr('data-ge-content-type', type)
4674
- .attr('title', t('tool.add_text_type', { editor: textLabel(type) }))
4675
- .text(textLabel(type))
4676
- .on('click', function(e) {
4677
- e.preventDefault();
4678
- e.stopPropagation();
4679
-
4680
- closeSizePicker();
4681
- addTextTo(col, type);
4682
- })
4683
- .appendTo(sizePicker)
4684
- ;
4685
- });
4619
+ function contentFor(content) {
4620
+ var offers = textOffers();
4686
4621
 
4687
- tool.one('mouseleave', function() {
4688
- window.setTimeout(function() {
4689
- if (sizePicker && !sizePicker.is(':hover')) { closeSizePicker(); }
4690
- }, 400);
4691
- });
4622
+ return createDefaultContentWrapper(offers.length ? offers[0].type : null).html(content);
4692
4623
  }
4693
4624
 
4694
4625
  /**
@@ -4745,23 +4676,6 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4745
4676
  return curView;
4746
4677
  }
4747
4678
 
4748
- /** The text editor plugin for a content type, or null. */
4749
- function textFor(type) {
4750
- return TEXTS[type] || null;
4751
- }
4752
-
4753
- /**
4754
- * An integration written to 5.x's contract - init and deinit over a
4755
- * set of content areas - as a text editor plugin.
4756
- */
4757
- function legacyText(rte) {
4758
- return {
4759
- initialContent: rte.initialContent,
4760
- start: function(block) { rte.init(settings, block); },
4761
- stop: function(block) { rte.deinit(settings, block); },
4762
- };
4763
- }
4764
-
4765
4679
  /**
4766
4680
  * The instance handle, documented API as of 3.0: the methods the
4767
4681
  * plugin dispatches, plus the settings and the canvas. A host holding
@@ -4781,7 +4695,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4781
4695
  getView: getView,
4782
4696
  createRow: apiCreateRow,
4783
4697
  createColumn: apiCreateColumn,
4784
- 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
+ },
4785
4707
  createElement: function(content, options) {
4786
4708
  if (!featureMethods.createElement) {
4787
4709
  warnOnceHere('plugin:elements', 'createElement needs the elements plugin: ' +
@@ -4826,6 +4748,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4826
4748
  baseElem.data('grideditor', handle);
4827
4749
 
4828
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();
4829
4754
  setup();
4830
4755
  init();
4831
4756
 
@@ -4836,14 +4761,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4836
4761
  };
4837
4762
 
4838
4763
  /**
4839
- * Text editor plugins: tinyMCE, CKEditor, summernote, and whatever a host
4840
- * writes. A factory registered under the content type it edits, called once
4841
- * per editor with the handle described in docs/plugins.md, returning
4842
- * { 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.
4843
4767
  */
4844
- $.fn.gridEditor.texts = {};
4845
-
4846
- /** 5.x's registry of text editors, adapted with a warning. Removed in 7.0. */
4847
4768
  $.fn.gridEditor.RTEs = {};
4848
4769
 
4849
4770
  /**
@@ -4899,11 +4820,9 @@ $.fn.gridEditor.locales = {
4899
4820
  'tool.move': 'Move',
4900
4821
  'tool.settings': 'Settings',
4901
4822
  'tool.add_row': 'Add row',
4902
- 'tool.add_text': 'Add text',
4903
- 'tool.add_text_type': 'Add {editor} text',
4904
4823
  'tool.delete_text': 'Remove text',
4905
- 'text.add': 'Text',
4906
- 'text.add_type': 'Text ({editor})',
4824
+ 'tool.delete_plain': 'Remove content',
4825
+ 'tool.convert_type': 'Edit as {editor} text',
4907
4826
  'text.no_editor': 'No text editor "{type}" is loaded: this text can be moved and deleted, not edited',
4908
4827
  'tool.add_column': 'Add column\n(hold to choose the width)',
4909
4828
  'tool.column_size': '{size} of 12',
@@ -4925,10 +4844,8 @@ $.fn.gridEditor.locales = {
4925
4844
  'panel.done': 'Done',
4926
4845
  'panel.id': 'Id',
4927
4846
  'panel.classes': 'Classes',
4928
- 'panel.editor': 'Editor',
4929
4847
  'panel.kind_row': 'Row',
4930
4848
  'panel.kind_column': 'Column',
4931
- 'panel.kind_text': 'Text',
4932
4849
  'panel.kind_element': 'Element',
4933
4850
  'panel.kind_section': 'Section',
4934
4851
  'panel.kind_tab': 'Tab',
@@ -4946,6 +4863,7 @@ $.fn.gridEditor.locales = {
4946
4863
  'confirm.delete_row': 'Delete row?',
4947
4864
  'confirm.delete_column': 'Delete column?',
4948
4865
  'confirm.delete_text': 'Delete this text?',
4866
+ 'confirm.delete_plain': 'Delete this content?',
4949
4867
  'confirm.delete_container': 'Delete this container and everything in it?',
4950
4868
  'view.all': 'All sizes',
4951
4869
  'view.xs': 'Phone',