@themarioga/grid-editor 6.0.0-beta.3 → 6.0.0-beta.5

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 (50) hide show
  1. package/BUILDING.md +3 -1
  2. package/CHANGELOG.md +74 -1
  3. package/README.md +71 -5
  4. package/UPGRADING.md +36 -7
  5. package/dist/grideditor.css +60 -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 +353 -402
  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 +7 -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.codemirror-inline.js +190 -0
  19. package/dist/plugins/grideditor.codemirror-inline.min.js +2 -0
  20. package/dist/plugins/grideditor.codemirror-inline.min.js.map +1 -0
  21. package/dist/plugins/grideditor.codemirror.js +79 -0
  22. package/dist/plugins/grideditor.codemirror.min.js +2 -0
  23. package/dist/plugins/grideditor.codemirror.min.js.map +1 -0
  24. package/dist/plugins/grideditor.summernote.js +397 -3
  25. package/dist/plugins/grideditor.summernote.min.js +1 -1
  26. package/dist/plugins/grideditor.summernote.min.js.map +1 -1
  27. package/dist/plugins/grideditor.tinymce.js +397 -3
  28. package/dist/plugins/grideditor.tinymce.min.js +1 -1
  29. package/dist/plugins/grideditor.tinymce.min.js.map +1 -1
  30. package/dist/plugins/grideditor.visibility.js +1 -1
  31. package/dist/plugins/grideditor.visibility.min.js +1 -1
  32. package/dist/plugins/grideditor.visibility.min.js.map +1 -1
  33. package/docs/events.md +17 -2
  34. package/docs/locale-keys.md +25 -3
  35. package/docs/plugins.md +124 -46
  36. package/example/codemirror.html +103 -0
  37. package/example/custom_editor.html +193 -0
  38. package/example/index.html +16 -0
  39. package/example/wrap_content.html +5 -3
  40. package/package.json +1 -1
  41. package/src/js/jquery.grideditor.js +353 -402
  42. package/src/js/locales/grideditor.es.js +7 -0
  43. package/src/js/plugins/grideditor.ckeditor.js +7 -3
  44. package/src/js/plugins/grideditor.codemirror-inline.js +190 -0
  45. package/src/js/plugins/grideditor.codemirror.js +79 -0
  46. package/src/js/plugins/grideditor.summernote.js +7 -3
  47. package/src/js/plugins/grideditor.tinymce.js +7 -3
  48. package/src/js/plugins/grideditor.visibility.js +1 -1
  49. package/src/js/text/grideditor.text.js +389 -0
  50. package/src/less/grideditor.less +71 -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
@@ -359,6 +358,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
359
358
  'default_view' : ALL_VIEW,
360
359
  'resize' : NESTED_SETTINGS.resize, // Resizing a column by dragging its edge
361
360
  'source_textarea' : '',
361
+ 'edit_source' : true, // The toolbar's button to edit the canvas as html
362
362
  'locale' : 'en', // Code of a locale in $.fn.gridEditor.locales
363
363
  'locale_strings' : {}, // Overrides for individual keys
364
364
  'callbacks' : {}, // before_*/after_* functions, the events by another route
@@ -398,6 +398,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
398
398
  var curView = settings.default_view; // Breakpoint key, or 'all'
399
399
  var confirmDialog = null; // The delete confirmation, built when first needed
400
400
  var sizePicker = null; // The open column size picker, if there is one
401
+ var sourceOpen = false; // Whether the canvas is being edited as html
401
402
  var dropMarker = null; // The line showing where a dragged toolbar button would land
402
403
  var warnedHere = {}; // Deprecations are worth saying once per instance, not once per call
403
404
  var sortables = []; // Every list made sortable, so deinit destroys exactly those
@@ -836,19 +837,21 @@ $.fn.gridEditor = function( optionsOrMethod ) {
836
837
  if (node.hasClass('row')) { return 'row'; }
837
838
  if (node.hasClass('column')) { return 'column'; }
838
839
  if (node.hasClass('ge-element')) { return 'element'; }
839
- if (node.hasClass('ge-content') || node.hasClass('ge-text-block')) { return 'text'; }
840
+ // A content area is a text when an editor's type is on it, and the
841
+ // host's plain content when there is none
842
+ if (node.hasClass('ge-text-block')) { node = node.children('.ge-content'); }
843
+ if (node.hasClass('ge-content')) { return node.attr('data-ge-content-type') ? 'text' : 'plain'; }
840
844
  return 'node';
841
845
  }
842
846
 
843
847
  // Copy html to sourceElement if a source textarea is given
844
848
  if (settings.source_textarea) {
845
849
  var sourceHtml = $(settings.source_textarea).val();
850
+ // Html with no grid in it goes into a row and a full width column
851
+ // of its own, where init wraps it as the host's plain content
846
852
  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
- }
853
+ sourceHtml = '<div class="row"><div class="col-lg-12">' + sourceHtml + '</div></div>';
854
+ }
852
855
  baseElem.html(sourceHtml);
853
856
  }
854
857
 
@@ -871,8 +874,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
871
874
  // Make controls fixed on scroll
872
875
  $(window).on('scroll', onScroll);
873
876
 
874
- /* Init RTE on click */
875
- canvas.on('click', '.ge-content', initRTE);
877
+ /* A click on the host's plain content offers to make it a text */
878
+ canvas.on('click', '.ge-content', onContentClick);
876
879
 
877
880
  /* A trigger is often a link, and a link still navigates even
878
881
  with its Bootstrap attributes suspended */
@@ -884,12 +887,6 @@ $.fn.gridEditor = function( optionsOrMethod ) {
884
887
  // which costs the element drawers inside it. The integrations say
885
888
  // when their editor is ready, and the drawers go back in.
886
889
  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
890
  plugins('onContentReady', $(this));
894
891
  refreshPreviews($(this));
895
892
  });
@@ -947,50 +944,16 @@ $.fn.gridEditor = function( optionsOrMethod ) {
947
944
 
948
945
  addContainerGroup.appendTo(wrapper);
949
946
 
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
947
  // A container starts in a row of its own, the way the add row
980
948
  // buttons next to these ones do
981
949
  $.each(CONTAINERS, function(type, definition) {
982
- $('<a class="btn btn-sm btn-primary ge-add-container" />')
983
- .attr('title', t(definition.labelKey))
950
+ labelButton($('<a class="btn btn-sm btn-primary ge-add-container" />'), t(definition.labelKey), definition.iconClass)
984
951
  .attr('data-ge-toolbar', 'container')
985
952
  .attr('data-ge-container-type', type)
986
- .append('<i class="bi bi-plus"></i>')
987
- .append($('<span />').text(t(definition.labelKey)))
988
953
  .on('click', function() {
989
954
  var row = createRow();
990
955
  var column = createColumn(MAX_COL_SIZE).appendTo(row);
991
- var container = definition.create({});
992
-
993
- column.find('> .ge-content').replaceWith(container);
956
+ var container = definition.create({}).appendTo(column);
994
957
 
995
958
  addNode(type, container, function() {
996
959
  row.appendTo(canvas);
@@ -1042,27 +1005,23 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1042
1005
  var btnGroup = $('<div class="btn-group pull-right"/>')
1043
1006
  .appendTo(wrapper)
1044
1007
  ;
1045
- var htmlButton = $('<button type="button" class="btn btn-sm btn-primary gm-edit-mode"><i class="bi bi-code-slash"></i></button>')
1046
- .attr('title', t('tool.edit_source'))
1047
- .on('click', function() {
1048
- if (htmlButton.hasClass('active')) {
1049
- canvas.empty().html(htmlTextArea.val()).show();
1050
- init();
1051
- htmlTextArea.hide();
1052
- } else {
1053
- deinit();
1054
- htmlTextArea
1055
- .height(0.8 * $(window).height())
1056
- .val(canvas.html())
1057
- .show()
1058
- ;
1059
- canvas.hide();
1060
- }
1008
+ if (settings.edit_source) {
1009
+ // Built again by setLocale, maybe with the source open
1010
+ var htmlButton = $('<button type="button" class="btn btn-sm btn-primary gm-edit-mode"><i class="bi bi-code-slash"></i></button>')
1011
+ .attr('title', t('tool.edit_source'))
1012
+ .toggleClass('active btn-danger', sourceOpen)
1013
+ .on('click', function() {
1014
+ if (sourceOpen) {
1015
+ closeSource();
1016
+ } else {
1017
+ openSource();
1018
+ }
1061
1019
 
1062
- htmlButton.toggleClass('active btn-danger');
1063
- })
1064
- .appendTo(btnGroup)
1065
- ;
1020
+ htmlButton.toggleClass('active btn-danger', sourceOpen);
1021
+ })
1022
+ .appendTo(btnGroup)
1023
+ ;
1024
+ }
1066
1025
  var previewButton = $('<button type="button" class="btn btn-sm btn-primary gm-preview"><i class="bi bi-eye-fill"></i></button>')
1067
1026
  .attr('title', t('tool.preview'))
1068
1027
  .on('mouseenter', function() {
@@ -1093,25 +1052,49 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1093
1052
  makeToolbarDraggable();
1094
1053
  }
1095
1054
 
1096
- /** A feature plugin's toolbar button: its label as text, or as the title alone at the end. */
1055
+ /**
1056
+ * A toolbar button's face: the plugin's icon alone when it has one,
1057
+ * with its label as the title, or a plus and the label otherwise.
1058
+ */
1059
+ function labelButton(button, label, iconClass) {
1060
+ button.attr('title', label);
1061
+
1062
+ if (iconClass) { return button.append($('<i />').addClass(iconClass)); }
1063
+
1064
+ return button
1065
+ .append('<i class="bi bi-plus"></i>')
1066
+ .append($('<span />').text(label))
1067
+ ;
1068
+ }
1069
+
1070
+ /** 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
1071
  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))
1072
+ var iconClass = item.iconClass || (item.align === 'end' ? 'bi bi-plus' : null);
1073
+ // A label of its own, worked out when the toolbar is built - and
1074
+ // built again by setLocale - or the string its labelKey names
1075
+ var label = typeof item.label === 'function' ? item.label() : (item.label || t(item.labelKey));
1076
+ var button = labelButton($('<a class="btn btn-sm btn-primary ge-add-container ge-add-feature" />'), label, iconClass)
1077
+ .addClass(item.className || '')
1100
1078
  .attr('data-ge-toolbar', 'feature')
1101
1079
  .attr('data-ge-feature', name)
1102
1080
  .attr('data-ge-item', index)
1103
- .append($('<i />').addClass(item.iconClass || 'bi bi-plus'))
1104
1081
  .on('click', function() {
1105
1082
  var made = item.create();
1106
1083
 
1107
- addNode(item.kind, made, function() {
1108
- made.appendTo(canvas);
1084
+ // One that belongs in a column brings a row and a column
1085
+ // of its own, the way a container does
1086
+ var placed = item.inColumn ? inRowOfItsOwn(made) : made;
1087
+
1088
+ var added = addNode(item.kind, made, function() {
1089
+ placed.appendTo(canvas);
1109
1090
  }, { parent: canvas, source: item.source || 'tool' });
1091
+
1092
+ if (added && item.inColumn && placed[0].scrollIntoView) {
1093
+ placed[0].scrollIntoView({behavior: 'smooth'});
1094
+ }
1110
1095
  })
1111
1096
  ;
1112
1097
 
1113
- if (item.align !== 'end') { button.append($('<span />').text(t(item.labelKey))); }
1114
-
1115
1098
  return button;
1116
1099
  }
1117
1100
 
@@ -1250,8 +1233,13 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1250
1233
  function insertFeatureFromToolbar(button, where) {
1251
1234
  var item = FEATURES[button.attr('data-ge-feature')].toolbar[parseInt(button.attr('data-ge-item'), 10)];
1252
1235
  var made = item.create();
1236
+ var placed = made;
1253
1237
 
1254
- if (!acceptsBlock(where.region, made)) {
1238
+ if (item.inColumn) {
1239
+ // Where it lands in a column, or in a row and a column of its
1240
+ // own anywhere else
1241
+ if (!where.region.is('.column')) { placed = inRowOfItsOwn(made); }
1242
+ } else if (!acceptsBlock(where.region, made)) {
1255
1243
  var top = where.region.parentsUntil(canvas).addBack().first();
1256
1244
  where = { region: canvas, before: top.length ? top.next() : null };
1257
1245
  if (where.before && !where.before.length) { where.before = null; }
@@ -1259,33 +1247,19 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1259
1247
 
1260
1248
  return addNode(item.kind, made, function() {
1261
1249
  if (where.before) {
1262
- made.insertBefore(where.before);
1250
+ placed.insertBefore(where.before);
1263
1251
  } else {
1264
- made.appendTo(where.region);
1252
+ placed.appendTo(where.region);
1265
1253
  }
1266
1254
  }, { parent: where.region, source: item.source || 'dragdrop' });
1267
1255
  }
1268
1256
 
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
- }
1257
+ /** A detached row with one full width column holding `node`. */
1258
+ function inRowOfItsOwn(node) {
1259
+ var row = createRow();
1260
+ createColumn(MAX_COL_SIZE).appendTo(row).append(node);
1281
1261
 
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' });
1262
+ return row;
1289
1263
  }
1290
1264
 
1291
1265
  /** A line where the block would go, following the pointer. */
@@ -1317,9 +1291,6 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1317
1291
  if (button.attr('data-ge-toolbar') === 'feature') {
1318
1292
  return insertFeatureFromToolbar(button, where);
1319
1293
  }
1320
- if (button.attr('data-ge-toolbar') === 'text') {
1321
- return insertTextFromToolbar(button, where);
1322
- }
1323
1294
 
1324
1295
  var container = button.attr('data-ge-toolbar') === 'container';
1325
1296
  var type = button.attr('data-ge-container-type');
@@ -1330,11 +1301,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1330
1301
  // A container belongs in a column: dropped straight onto the
1331
1302
  // canvas it brings a row and a column of its own
1332
1303
  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
- }
1304
+ if (container && !where.region.is('.column')) { placed = inRowOfItsOwn(made); }
1338
1305
 
1339
1306
  return addNode(container ? type : 'row', made, function() {
1340
1307
  if (where.before) {
@@ -1461,30 +1428,153 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1461
1428
  }
1462
1429
  }
1463
1430
 
1464
- function initRTE(e) {
1465
- if ($(this).hasClass('ge-rte-active')) { return; }
1431
+ /**
1432
+ * A click on the host's plain content makes it a text: of the one
1433
+ * editor offered, or of the one chosen when there are several. What
1434
+ * a click on a text does is its editor plugin's business.
1435
+ */
1436
+ function onContentClick() {
1437
+ var block = $(this);
1438
+ if (block.attr('data-ge-content-type')) { return; }
1439
+
1440
+ // A second click, with the choice still open, withdraws it
1441
+ if (closeSizePicker()) { return; }
1466
1442
 
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
1443
+ // Content nobody can see - a tab that is not the open one, a
1444
+ // closed accordion item - has no geometry for an editor to lay
1469
1445
  // 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;
1446
+ if (!block.is(':visible')) { return; }
1447
+
1448
+ var offers = textOffers();
1449
+ if (!offers.length) { return; }
1450
+
1451
+ if (offers.length === 1) {
1452
+ convertPlain(block, offers[0]);
1453
+ } else {
1454
+ openConvertPicker(block, offers);
1455
+ }
1456
+ }
1457
+
1458
+ /**
1459
+ * Plain content made a text of `offer`'s type, through the convert
1460
+ * events: the type goes on, and the plugin that declared it takes the
1461
+ * block over and opens its editor. Nothing else about the node
1462
+ * changes. False when the library is missing or a handler canceled.
1463
+ */
1464
+ function convertPlain(block, offer) {
1465
+ // Not converted, so the next click tries again: the library may
1466
+ // be loaded by then
1467
+ if (offer.available && !offer.available()) {
1468
+ if (offer.missingKey) { console.error(t(offer.missingKey)); }
1469
+ return false;
1483
1470
  }
1484
1471
 
1485
- $(this).data('ge-text-before', attributesOf(this)).removeData('ge-text-ready');
1486
- $(this).addClass('ge-rte-active');
1487
- text.start($(this));
1472
+ return operate(function() {
1473
+ var payload = payloadFor('plain', block, { source: 'tool', from: 'plain', to: offer.type });
1474
+
1475
+ if (!emit('before-convert', payload)) { return false; }
1476
+
1477
+ block.addClass('ge-content-type-' + offer.type).attr('data-ge-content-type', offer.type);
1478
+
1479
+ // Its plain drawer goes: the block is the plugin's now, and
1480
+ // the plugin gives it the drawer a text has
1481
+ block.parent('.ge-text-block').removeClass('ge-plain-block').children('.ge-tools-drawer').remove();
1482
+
1483
+ emit('after-convert', payload);
1484
+ offer.edit(block);
1485
+
1486
+ return true;
1487
+ });
1488
+ }
1489
+
1490
+ /**
1491
+ * The text types offered, as a strip under the plain content's
1492
+ * drawer, when there is more than one to choose from.
1493
+ */
1494
+ function openConvertPicker(block, offers) {
1495
+ var textBlock = block.parent('.ge-text-block');
1496
+
1497
+ openPicker(textBlock.children('.ge-tools-drawer'), offers.map(function(offer) {
1498
+ return {
1499
+ label: offer.label,
1500
+ title: t('tool.convert_type', { editor: offer.label }),
1501
+ attributes: { 'data-ge-content-type': offer.type },
1502
+ choose: function() { convertPlain(block, offer); },
1503
+ };
1504
+ }), 'ge-text-picker ge-convert-picker', textBlock);
1505
+ }
1506
+
1507
+ /**
1508
+ * A choice as a strip under `anchor` - a tool, or a drawer - one
1509
+ * button per `{ label, title, attributes, choose }`. Leaving `hover`
1510
+ * (the anchor by default) withdraws it, and so does closeSizePicker.
1511
+ */
1512
+ function openPicker(anchor, choices, className, hover) {
1513
+ closeSizePicker();
1514
+
1515
+ anchor.closest('.ge-tools-drawer').addClass('ge-picker-open');
1516
+ sizePicker = $('<div class="ge-size-picker" />').addClass(className || '').appendTo(anchor);
1517
+
1518
+ choices.forEach(function(choice) {
1519
+ $('<a class="ge-size ge-size-flex" />')
1520
+ .attr(choice.attributes || {})
1521
+ .attr('title', choice.title || choice.label)
1522
+ .text(choice.label)
1523
+ .on('click', function(e) {
1524
+ e.preventDefault();
1525
+ e.stopPropagation();
1526
+
1527
+ closeSizePicker();
1528
+ choice.choose();
1529
+ })
1530
+ .appendTo(sizePicker)
1531
+ ;
1532
+ });
1533
+
1534
+ (hover || anchor).one('mouseleave', function() {
1535
+ window.setTimeout(function() {
1536
+ if (sizePicker && !sizePicker.is(':hover')) { closeSizePicker(); }
1537
+ }, 400);
1538
+ });
1539
+ }
1540
+
1541
+ /**
1542
+ * Every text type the feature plugins declare, in the order they
1543
+ * were registered: the first plugin to declare a type owns it.
1544
+ */
1545
+ function textTypes() {
1546
+ var owners = {};
1547
+ var entries = [];
1548
+
1549
+ $.each(FEATURES, function(name, feature) {
1550
+ if (!feature.textTypes) { return; }
1551
+
1552
+ (feature.textTypes() || []).forEach(function(entry) {
1553
+ if (owners[entry.type]) {
1554
+ if (owners[entry.type] !== name) {
1555
+ warnOnceHere('text-type:' + entry.type, 'the "' + name + '" plugin declares the ' +
1556
+ 'text type "' + entry.type + '", which the "' + owners[entry.type] +
1557
+ '" plugin already declares: ignored');
1558
+ }
1559
+ return;
1560
+ }
1561
+
1562
+ owners[entry.type] = name;
1563
+ entries.push(entry);
1564
+ });
1565
+ });
1566
+
1567
+ return entries;
1568
+ }
1569
+
1570
+ /** The types plain content can be made, in order: the ones not marked `offered: false`. */
1571
+ function textOffers() {
1572
+ return textTypes().filter(function(entry) { return entry.offered !== false; });
1573
+ }
1574
+
1575
+ /** Whether a plugin declares this text type. */
1576
+ function hasTextOwner(type) {
1577
+ return textTypes().some(function(entry) { return entry.type === type; });
1488
1578
  }
1489
1579
 
1490
1580
  function reset() {
@@ -1492,6 +1582,32 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1492
1582
  init();
1493
1583
  }
1494
1584
 
1585
+ /**
1586
+ * The canvas as html, in the textarea in its place, to edit by hand.
1587
+ * A feature plugin's onSourceOpen may put a code editor over the
1588
+ * textarea; its onSourceClose puts what it holds back in it, which is
1589
+ * what the canvas is made of again.
1590
+ */
1591
+ function openSource() {
1592
+ deinit();
1593
+ htmlTextArea
1594
+ .height(0.8 * $(window).height())
1595
+ .val(canvas.html())
1596
+ .show()
1597
+ ;
1598
+ canvas.hide();
1599
+ sourceOpen = true;
1600
+ plugins('onSourceOpen', htmlTextArea);
1601
+ }
1602
+
1603
+ function closeSource() {
1604
+ plugins('onSourceClose', htmlTextArea);
1605
+ sourceOpen = false;
1606
+ canvas.empty().html(htmlTextArea.val()).show();
1607
+ init();
1608
+ htmlTextArea.hide();
1609
+ }
1610
+
1495
1611
  function init() {
1496
1612
  // The node whose settings were open is gone - deleted, say - and
1497
1613
  // its panel with it
@@ -1521,9 +1637,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1521
1637
  // Its panel goes home to its drawer first, and both go together
1522
1638
  closeSettings();
1523
1639
  canvas.removeClass('ge-editing ge-drag-drawer ge-dropping');
1524
- canvas.find('.ge-content').each(function() {
1525
- closeText($(this));
1526
- });
1640
+ // Before the drawers and the text blocks come off: an editor
1641
+ // plugin closes its editors here, and finds them where it left them
1642
+ plugins('onBeforeDeinit');
1527
1643
  closeSizePicker();
1528
1644
  hideDropMarker();
1529
1645
  canvas.find('.ge-tools-drawer').remove();
@@ -1570,13 +1686,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1570
1686
  }
1571
1687
 
1572
1688
  function destroy() {
1689
+ // The html being edited is what the canvas is left with
1690
+ if (sourceOpen) { closeSource(); }
1573
1691
  deinit();
1574
1692
  removeConfirmModal();
1575
1693
  removeSettingsPanels();
1576
1694
  mainControls.remove();
1577
1695
  htmlTextArea.remove();
1578
1696
  $(window).off('scroll', onScroll);
1579
- canvas.off('click', '.ge-content', initRTE);
1697
+ canvas.off('click', '.ge-content', onContentClick);
1580
1698
  canvas.off('ge-rte-ready', '.ge-content');
1581
1699
  canvas.removeData('grideditor');
1582
1700
  }
@@ -1627,30 +1745,27 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1627
1745
  return !settings.plugins || settings.plugins.indexOf(name) !== -1;
1628
1746
  };
1629
1747
 
1748
+ // What the plugins setting does not choose: the text editors,
1749
+ // which content_types chooses. A page that names its containers
1750
+ // there has named no editor, and still wants the one it uses.
1751
+ var featureWanted = function(name, factory) {
1752
+ return factory.always === true || wanted(name);
1753
+ };
1754
+
1630
1755
  $.each($.fn.gridEditor.containers, function(type, factory) {
1631
1756
  if (wanted(type)) { CONTAINERS[type] = factory(api); }
1632
1757
  });
1633
1758
 
1634
1759
  $.each($.fn.gridEditor.features, function(name, factory) {
1635
- if (wanted(name)) { FEATURES[name] = factory(api); }
1760
+ if (featureWanted(name, factory)) { FEATURES[name] = factory(api); }
1636
1761
  });
1637
1762
 
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
- });
1763
+ // 5.x's registry of text editors is gone: an editor of a host's
1764
+ // own is a plugin of its own since 6.0
1765
+ if (!$.isEmptyObject($.fn.gridEditor.RTEs || {})) {
1766
+ warnOnceHere('rtes', '$.fn.gridEditor.RTEs was removed in 6.0 and what is registered there ' +
1767
+ 'is ignored: write the editor as a plugin, see docs/plugins.md');
1768
+ }
1654
1769
 
1655
1770
  $.each($.fn.gridEditor.utilities, function(name, factory) {
1656
1771
  if (!wanted(name)) { return; }
@@ -1731,6 +1846,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1731
1846
  // A text editor has rewritten a content area, so whatever the
1732
1847
  // editor and its plugins had put in there goes back in
1733
1848
  textReady: function(block) { block.trigger('ge-rte-ready'); },
1849
+ // A choice under a tool, offered by holding it
1850
+ attachPicker: attachPicker,
1851
+ openPicker: openPicker,
1852
+ closePicker: closeSizePicker,
1734
1853
  // A node's settings panel, in its drawer or open outside it
1735
1854
  detailsOf: detailsOf,
1736
1855
  // A node's drawer: its first child, or for a content area the
@@ -1804,6 +1923,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1804
1923
  * name the ones it means.
1805
1924
  */
1806
1925
  function appliesTo(family, kind) {
1926
+ // The host's plain content reads and previews its utilities as a
1927
+ // text does; with no gear, nothing in its drawer writes them
1928
+ if (kind === 'plain') { kind = 'text'; }
1807
1929
  if (family.appliesTo.indexOf(kind) !== -1) { return true; }
1808
1930
 
1809
1931
  return !!CONTAINERS[kind] && family.appliesTo.indexOf('container') !== -1;
@@ -2273,7 +2395,6 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2273
2395
  * -------------------------------------------------------------- */
2274
2396
 
2275
2397
  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
2398
  var FEATURES = {}; // The feature plugins in use, by name
2278
2399
  var UTILITIES = {}; // The utility plugins in use, by name
2279
2400
  var featureMethods = {}; // The methods those features contribute
@@ -2724,22 +2845,6 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2724
2845
  });
2725
2846
  });
2726
2847
 
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
2848
  createTool(drawer, t('tool.add_row'), 'ge-add-row', 'bi bi-plus-circle', function() {
2744
2849
  // An empty row: the columns in it are the next decision,
2745
2850
  // and its drawer's add column tool is where that is made
@@ -4202,7 +4307,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4202
4307
 
4203
4308
  var column = createColumn(size, options.offset);
4204
4309
  if (options.content !== undefined) {
4205
- column.find('.ge-content').html(options.content);
4310
+ column.append(contentFor(options.content));
4206
4311
  }
4207
4312
 
4208
4313
  return place(column, 'column', options);
@@ -4270,10 +4375,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4270
4375
  * the all view. `offset` indents it, within the same 12 unit budget.
4271
4376
  */
4272
4377
  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
- ;
4378
+ // Empty: what goes in it is the next decision, and the tools of
4379
+ // the column and the toolbar are where it is made
4380
+ var column = $('<div class="column"/>');
4277
4381
 
4278
4382
  // The tier being edited, or the base class in the all view
4279
4383
  var tier = tiersFor(curView)[0];
@@ -4367,8 +4471,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4367
4471
  return;
4368
4472
  } else if (kept) {
4369
4473
  // Of the same type, or of none if it had none
4370
- placed = (type ? createDefaultContentWrapper(type) : $('<div class="ge-content" />'))
4371
- .append(piece.text);
4474
+ placed = createDefaultContentWrapper(type).append(piece.text);
4372
4475
  } else {
4373
4476
  // The content area itself, moved along if an
4374
4477
  // element came before the first text
@@ -4424,7 +4527,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4424
4527
  }
4425
4528
 
4426
4529
  /**
4427
- * Wrap a run of loose column content in a content area, and hand back
4530
+ * Wrap a run of loose column content in a content area of no type -
4531
+ * the host's plain content - and hand back
4428
4532
  * an empty set: the caller has to forget what it just wrapped, or the
4429
4533
  * next boundary wraps the same nodes again and leaves the first
4430
4534
  * wrapper behind, empty.
@@ -4439,12 +4543,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4439
4543
  }
4440
4544
 
4441
4545
  /**
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.
4546
+ * A content area of a text type, or with none the host's plain
4547
+ * content: a content area and nothing more.
4445
4548
  */
4446
4549
  function createDefaultContentWrapper(type) {
4447
- type = type || settings.content_types[0];
4448
4550
  var contentArea = $('<div class="ge-content"/>');
4449
4551
 
4450
4552
  if (type) {
@@ -4455,22 +4557,23 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4455
4557
  }
4456
4558
 
4457
4559
  /* --------------------------------------------------------------
4458
- * Text blocks: a content area as a block of its own, with a drawer.
4560
+ * Content blocks: a content area as a block of its own, with a drawer.
4459
4561
  *
4460
4562
  * 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.
4563
+ * block's does: what is inside may be a text editor's, which would
4564
+ * make the drawer editable text, copy it into its own document or
4565
+ * take it away on an undo. So while editing, each content area in a
4566
+ * column sits in a .ge-text-block beside its drawer, and that wrapper
4567
+ * is the block the column moves. deinit takes it off again: it is
4568
+ * never part of the markup.
4569
+ *
4570
+ * A content area with no type is the host's plain content, and its
4571
+ * drawer is the editor's: move and delete, nothing else. One whose
4572
+ * type a plugin declares is that plugin's text, and the plugin gives
4573
+ * it its drawer. One whose type nobody declares is a text whose
4574
+ * editor is not loaded: a block to move and delete, not to edit.
4467
4575
  * -------------------------------------------------------------- */
4468
4576
 
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
4577
  function wrapTexts() {
4475
4578
  canvas.find('.column > .ge-content').each(function() {
4476
4579
  $(this).wrap('<div class="ge-text-block" />');
@@ -4478,7 +4581,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4478
4581
 
4479
4582
  canvas.find('.ge-text-block').each(function() {
4480
4583
  var textBlock = $(this);
4481
- if (!textBlock.children('.ge-tools-drawer').length) { createTextControls(textBlock); }
4584
+ if (textBlock.children('.ge-tools-drawer').length) { return; }
4585
+
4586
+ var type = textBlock.children('.ge-content').attr('data-ge-content-type');
4587
+
4588
+ if (!type) {
4589
+ createPlainControls(textBlock);
4590
+ } else if (!hasTextOwner(type)) {
4591
+ createOrphanControls(textBlock, type);
4592
+ }
4482
4593
  });
4483
4594
  }
4484
4595
 
@@ -4489,41 +4600,22 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4489
4600
  });
4490
4601
  }
4491
4602
 
4492
- function createTextControls(textBlock) {
4603
+ /** The host's plain content: moved and deleted here, edited in the source. */
4604
+ function createPlainControls(textBlock) {
4493
4605
  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);
4606
+ var drawer = $('<div class="ge-tools-drawer ge-text-drawer ge-plain-drawer" />').prependTo(textBlock);
4607
+ textBlock.addClass('ge-plain-block');
4496
4608
 
4497
4609
  createMoveTool(drawer);
4498
4610
 
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);
4611
+ // The tools a feature plugin gives plain content, which is not
4612
+ // every plugin's drawerTools: no gear, no utilities, no copy
4613
+ $.each(FEATURES, function(name, feature) {
4614
+ if (feature.plainTools) { feature.plainTools(drawer, block); }
4520
4615
  });
4521
4616
 
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);
4617
+ createTool(drawer, t('tool.delete_plain'), 'ge-delete-plain', 'bi bi-trash', function() {
4618
+ deleteNode('plain', block, t('confirm.delete_plain'), function(removed) {
4527
4619
  textBlock.slideUp(function() {
4528
4620
  textBlock.remove();
4529
4621
  removed();
@@ -4533,162 +4625,34 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4533
4625
  }
4534
4626
 
4535
4627
  /**
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.
4628
+ * A text whose editor is not loaded: still a block to move and delete,
4629
+ * and its drawer says why it cannot be edited.
4548
4630
  */
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
- });
4631
+ function createOrphanControls(textBlock, type) {
4632
+ var block = textBlock.children('.ge-content');
4633
+ var drawer = $('<div class="ge-tools-drawer ge-text-drawer" />').prependTo(textBlock);
4599
4634
 
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); }
4635
+ createMoveTool(drawer);
4636
+ createTool(drawer, t('text.no_editor', { type: type }), 'ge-text-info ge-text-missing',
4637
+ 'bi bi-exclamation-triangle');
4638
+ createTool(drawer, t('tool.delete_text'), 'ge-delete-text', 'bi bi-trash', function() {
4639
+ deleteNode('text', block, t('confirm.delete_text'), function(removed) {
4640
+ textBlock.slideUp(function() {
4641
+ textBlock.remove();
4642
+ removed();
4643
+ });
4644
+ });
4607
4645
  });
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
4646
  }
4638
4647
 
4639
4648
  /**
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.
4649
+ * A detached content area holding `content`: a text of the first type
4650
+ * offered, or the host's plain content when no editor is offered.
4643
4651
  */
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
- }
4652
+ function contentFor(content) {
4653
+ var offers = textOffers();
4657
4654
 
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
- });
4686
-
4687
- tool.one('mouseleave', function() {
4688
- window.setTimeout(function() {
4689
- if (sizePicker && !sizePicker.is(':hover')) { closeSizePicker(); }
4690
- }, 400);
4691
- });
4655
+ return createDefaultContentWrapper(offers.length ? offers[0].type : null).html(content);
4692
4656
  }
4693
4657
 
4694
4658
  /**
@@ -4745,23 +4709,6 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4745
4709
  return curView;
4746
4710
  }
4747
4711
 
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
4712
  /**
4766
4713
  * The instance handle, documented API as of 3.0: the methods the
4767
4714
  * plugin dispatches, plus the settings and the canvas. A host holding
@@ -4781,7 +4728,15 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4781
4728
  getView: getView,
4782
4729
  createRow: apiCreateRow,
4783
4730
  createColumn: apiCreateColumn,
4784
- createText: apiCreateText,
4731
+ createText: function(type, options) {
4732
+ if (!featureMethods.createText) {
4733
+ warnOnceHere('plugin:text', 'createText needs a text editor plugin: include ' +
4734
+ 'dist/plugins/grideditor.tinymce.js, or another editor\'s, after the editor');
4735
+ return null;
4736
+ }
4737
+
4738
+ return featureMethods.createText(type, options);
4739
+ },
4785
4740
  createElement: function(content, options) {
4786
4741
  if (!featureMethods.createElement) {
4787
4742
  warnOnceHere('plugin:elements', 'createElement needs the elements plugin: ' +
@@ -4826,6 +4781,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4826
4781
  baseElem.data('grideditor', handle);
4827
4782
 
4828
4783
  loadPlugins();
4784
+ // Again, with what the plugins filled in: the text editors'
4785
+ // content_types when the host gave none
4786
+ handle.settings = settingsCopy();
4829
4787
  setup();
4830
4788
  init();
4831
4789
 
@@ -4836,14 +4794,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4836
4794
  };
4837
4795
 
4838
4796
  /**
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 }.
4797
+ * 5.x's registry of text editors, removed in 6.0: still there to register
4798
+ * into, so a page written for 5.x does not throw, but what it holds is
4799
+ * ignored with a warning. The text editors are plugins since 6.0.
4843
4800
  */
4844
- $.fn.gridEditor.texts = {};
4845
-
4846
- /** 5.x's registry of text editors, adapted with a warning. Removed in 7.0. */
4847
4801
  $.fn.gridEditor.RTEs = {};
4848
4802
 
4849
4803
  /**
@@ -4899,11 +4853,9 @@ $.fn.gridEditor.locales = {
4899
4853
  'tool.move': 'Move',
4900
4854
  'tool.settings': 'Settings',
4901
4855
  'tool.add_row': 'Add row',
4902
- 'tool.add_text': 'Add text',
4903
- 'tool.add_text_type': 'Add {editor} text',
4904
4856
  'tool.delete_text': 'Remove text',
4905
- 'text.add': 'Text',
4906
- 'text.add_type': 'Text ({editor})',
4857
+ 'tool.delete_plain': 'Remove content',
4858
+ 'tool.convert_type': 'Edit as {editor} text',
4907
4859
  'text.no_editor': 'No text editor "{type}" is loaded: this text can be moved and deleted, not edited',
4908
4860
  'tool.add_column': 'Add column\n(hold to choose the width)',
4909
4861
  'tool.column_size': '{size} of 12',
@@ -4925,10 +4877,8 @@ $.fn.gridEditor.locales = {
4925
4877
  'panel.done': 'Done',
4926
4878
  'panel.id': 'Id',
4927
4879
  'panel.classes': 'Classes',
4928
- 'panel.editor': 'Editor',
4929
4880
  'panel.kind_row': 'Row',
4930
4881
  'panel.kind_column': 'Column',
4931
- 'panel.kind_text': 'Text',
4932
4882
  'panel.kind_element': 'Element',
4933
4883
  'panel.kind_section': 'Section',
4934
4884
  'panel.kind_tab': 'Tab',
@@ -4946,6 +4896,7 @@ $.fn.gridEditor.locales = {
4946
4896
  'confirm.delete_row': 'Delete row?',
4947
4897
  'confirm.delete_column': 'Delete column?',
4948
4898
  'confirm.delete_text': 'Delete this text?',
4899
+ 'confirm.delete_plain': 'Delete this content?',
4949
4900
  'confirm.delete_container': 'Delete this container and everything in it?',
4950
4901
  'view.all': 'All sizes',
4951
4902
  'view.xs': 'Phone',