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

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 (51) hide show
  1. package/BUILDING.md +2 -4
  2. package/CHANGELOG.md +59 -0
  3. package/README.md +34 -8
  4. package/UPGRADING.md +57 -12
  5. package/dist/grideditor.css +179 -100
  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 +490 -432
  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 +12 -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 +2 -1
  16. package/dist/plugins/grideditor.ckeditor.min.js.map +1 -1
  17. package/dist/plugins/grideditor.clipboard.js +1 -11
  18. package/dist/plugins/grideditor.clipboard.min.js +1 -1
  19. package/dist/plugins/grideditor.clipboard.min.js.map +1 -1
  20. package/dist/plugins/grideditor.elements.js +66 -38
  21. package/dist/plugins/grideditor.elements.min.js +1 -1
  22. package/dist/plugins/grideditor.elements.min.js.map +1 -1
  23. package/dist/plugins/grideditor.sections.js +2 -2
  24. package/dist/plugins/grideditor.sections.min.js +1 -1
  25. package/dist/plugins/grideditor.sections.min.js.map +1 -1
  26. package/dist/plugins/grideditor.spacing.js +1 -1
  27. package/dist/plugins/grideditor.spacing.min.js +1 -1
  28. package/dist/plugins/grideditor.spacing.min.js.map +1 -1
  29. package/dist/plugins/grideditor.summernote.js +2 -1
  30. package/dist/plugins/grideditor.summernote.min.js.map +1 -1
  31. package/dist/plugins/grideditor.tinymce.js +2 -1
  32. package/dist/plugins/grideditor.tinymce.min.js.map +1 -1
  33. package/docs/events.md +4 -3
  34. package/docs/locale-keys.md +23 -0
  35. package/docs/plugins.md +18 -20
  36. package/example/attributes.html +21 -37
  37. package/example/elements.html +25 -20
  38. package/example/plugins.html +5 -5
  39. package/example/utilities.html +31 -8
  40. package/package.json +1 -1
  41. package/src/js/jquery.grideditor.js +489 -34
  42. package/src/js/locales/grideditor.es.js +12 -0
  43. package/src/js/plugins/grideditor.ckeditor.js +2 -1
  44. package/src/js/plugins/grideditor.clipboard.js +1 -11
  45. package/src/js/plugins/grideditor.elements.js +66 -38
  46. package/src/js/plugins/grideditor.sections.js +2 -2
  47. package/src/js/plugins/grideditor.spacing.js +1 -1
  48. package/src/js/plugins/grideditor.summernote.js +2 -1
  49. package/src/js/plugins/grideditor.tinymce.js +2 -1
  50. package/src/less/grideditor.less +203 -112
  51. package/src/js/compat/bundled-texts.js +0 -14
@@ -363,6 +363,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
363
363
  'locale_strings' : {}, // Overrides for individual keys
364
364
  'callbacks' : {}, // before_*/after_* functions, the events by another route
365
365
  'confirm_delete' : true, // Ask before deleting a row or a column
366
+ 'settings_panel' : 'offcanvas', // Where a node's settings open: 'offcanvas', 'popover', 'modal' or 'inline'
366
367
  'drag' : NESTED_SETTINGS.drag // How a drag behaves, whatever drives it
367
368
  }, optionsOrMethod);
368
369
 
@@ -426,6 +427,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
426
427
  handle.settings = settingsCopy();
427
428
 
428
429
  removeConfirmModal();
430
+ removeSettingsPanels();
429
431
  mainControls.remove();
430
432
  createMainControls();
431
433
  reset();
@@ -1480,12 +1482,6 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1480
1482
  return;
1481
1483
  }
1482
1484
 
1483
- if (text.bundled) {
1484
- warnOnceHere('text-bundled:' + type, 'the ' + type + ' text editor is the copy in the ' +
1485
- 'main bundle, which 6.0 will not include: load dist/plugins/grideditor.' + type +
1486
- '.js after the editor');
1487
- }
1488
-
1489
1485
  $(this).data('ge-text-before', attributesOf(this)).removeData('ge-text-ready');
1490
1486
  $(this).addClass('ge-rte-active');
1491
1487
  text.start($(this));
@@ -1497,11 +1493,19 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1497
1493
  }
1498
1494
 
1499
1495
  function init() {
1496
+ // The node whose settings were open is gone - deleted, say - and
1497
+ // its panel with it
1498
+ if (openSettingsState && !$.contains(document.documentElement, openSettingsState.node[0])) {
1499
+ closeSettings();
1500
+ }
1501
+
1500
1502
  runFilter(true);
1501
1503
  canvas.addClass('ge-editing');
1502
1504
  canvas.toggleClass('ge-drag-drawer', settings.drag_handle === 'drawer');
1503
1505
  addAllColClasses();
1504
- wrapContent();
1506
+ var cutter = textCutter();
1507
+ splitTexts(cutter);
1508
+ wrapContent(cutter);
1505
1509
  wrapTexts();
1506
1510
  createRowControls();
1507
1511
  createColControls();
@@ -1514,6 +1518,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1514
1518
  }
1515
1519
 
1516
1520
  function deinit() {
1521
+ // Its panel goes home to its drawer first, and both go together
1522
+ closeSettings();
1517
1523
  canvas.removeClass('ge-editing ge-drag-drawer ge-dropping');
1518
1524
  canvas.find('.ge-content').each(function() {
1519
1525
  closeText($(this));
@@ -1566,6 +1572,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1566
1572
  function destroy() {
1567
1573
  deinit();
1568
1574
  removeConfirmModal();
1575
+ removeSettingsPanels();
1569
1576
  mainControls.remove();
1570
1577
  htmlTextArea.remove();
1571
1578
  $(window).off('scroll', onScroll);
@@ -1632,7 +1639,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1632
1639
  // plugins setting: a page that names its containers there has
1633
1640
  // named no editor, and still wants the one it uses
1634
1641
  $.each($.fn.gridEditor.texts, function(type, factory) {
1635
- TEXTS[type] = $.extend({ bundled: !!factory.bundled }, factory(api));
1642
+ TEXTS[type] = factory(api);
1636
1643
  });
1637
1644
 
1638
1645
  // 5.x's registry, for an integration a host wrote itself. One the
@@ -1724,6 +1731,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1724
1731
  // A text editor has rewritten a content area, so whatever the
1725
1732
  // editor and its plugins had put in there goes back in
1726
1733
  textReady: function(block) { block.trigger('ge-rte-ready'); },
1734
+ // A node's settings panel, in its drawer or open outside it
1735
+ detailsOf: detailsOf,
1727
1736
  // A node's drawer: its first child, or for a content area the
1728
1737
  // one beside it in its text block
1729
1738
  drawerOf: function(node) {
@@ -2014,9 +2023,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2014
2023
 
2015
2024
  /** Bring a node's panel and preview up to date with its classes. */
2016
2025
  function refreshUtilities(node) {
2017
- var details = node.children('.ge-tools-drawer').children('.ge-details');
2026
+ var details = detailsOf(node);
2018
2027
 
2019
- details.children('.ge-classes').val(hostClasses(node).join(' '));
2028
+ details.find('.ge-classes').val(hostClasses(node).join(' '));
2020
2029
  details.children('.ge-utilities').each(function() { renderUtilities($(this)); });
2021
2030
  refreshPreviews(node);
2022
2031
  }
@@ -2144,7 +2153,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2144
2153
  .appendTo(section)
2145
2154
  .on('click', function() {
2146
2155
  utilitiesOpen = !section.hasClass('ge-open');
2147
- canvas.find('.ge-utilities').toggleClass('ge-open', utilitiesOpen);
2156
+ settingsScope().find('.ge-utilities').toggleClass('ge-open', utilitiesOpen);
2148
2157
  })
2149
2158
  ;
2150
2159
 
@@ -2168,7 +2177,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2168
2177
  .text(family.labelKey ? t(family.labelKey) : family.name)
2169
2178
  .appendTo(field)
2170
2179
  ;
2171
- $('<select />')
2180
+ $('<select class="form-select form-select-sm" />')
2172
2181
  .appendTo(field)
2173
2182
  .on('change', function() {
2174
2183
  setUtility(node, family.name, this.value, { source: 'panel' });
@@ -2901,9 +2910,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2901
2910
  */
2902
2911
  function addSettingsTool(drawer, node, presets) {
2903
2912
  var details = createDetails(node, presets || []);
2913
+ node.data('ge-details', details);
2904
2914
 
2905
2915
  createTool(drawer, t('tool.settings'), 'ge-settings', 'bi bi-gear-fill', function() {
2906
- details.toggle();
2916
+ toggleSettings(node, details, $(this));
2907
2917
  });
2908
2918
 
2909
2919
  // Beside the gear, because every node that has one is a node a
@@ -2923,12 +2933,18 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2923
2933
 
2924
2934
  function createDetails(container, cssClasses) {
2925
2935
  var detailsDiv = $('<div class="ge-details" />');
2936
+ var general = $('<div class="ge-details-general" />').appendTo(detailsDiv);
2937
+ var field = function(label) {
2938
+ return $('<label class="ge-field" />')
2939
+ .append($('<span class="ge-field-label" />').text(label))
2940
+ .appendTo(general);
2941
+ };
2926
2942
 
2927
- $('<input class="ge-id" />')
2943
+ $('<input class="ge-id form-control form-control-sm" />')
2928
2944
  .attr('placeholder', t('tool.id_placeholder'))
2929
2945
  .val(container.attr('id'))
2930
2946
  .attr('title', t('tool.id_title'))
2931
- .appendTo(detailsDiv)
2947
+ .appendTo(field(t('panel.id')))
2932
2948
  .on('change', function() {
2933
2949
  // An empty field means no id, not an empty one
2934
2950
  if (this.value === '') {
@@ -2939,25 +2955,28 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2939
2955
  })
2940
2956
  ;
2941
2957
 
2942
- $('<input class="ge-classes" />')
2958
+ $('<input class="ge-classes form-control form-control-sm" />')
2943
2959
  .attr('placeholder', t('tool.classes_placeholder'))
2944
2960
  .attr('title', t('tool.classes_title'))
2945
2961
  .val(hostClasses(container).join(' '))
2946
- .appendTo(detailsDiv)
2962
+ .appendTo(field(t('panel.classes')))
2947
2963
  .on('change', function() {
2948
2964
  setHostClasses(container, this.value);
2949
2965
  refreshUtilities(container);
2950
2966
  })
2951
2967
  ;
2952
2968
 
2953
- var classGroup = $('<div class="btn-group" />').appendTo(detailsDiv);
2969
+ // Bootstrap 5's outline buttons, filled while their class is on;
2970
+ // up to 5.x they carried Bootstrap 3's btn-default, which 5 lacks
2971
+ var classGroup = $('<div class="btn-group btn-group-sm ge-presets" role="group" />').appendTo(general);
2954
2972
  cssClasses.forEach(function(rowClass) {
2955
- var btn = $('<a class="btn btn-sm btn-default" />')
2973
+ var btn = $('<a role="button" class="btn btn-sm btn-outline-secondary" />')
2956
2974
  .html(rowClass.label)
2957
2975
  .attr('title', rowClass.title ? rowClass.title : t('tool.toggle_class', { label: rowClass.label }))
2958
- .toggleClass('active btn-primary', container.hasClass(rowClass.cssClass))
2976
+ .toggleClass('active', container.hasClass(rowClass.cssClass))
2977
+ .attr('aria-pressed', container.hasClass(rowClass.cssClass) ? 'true' : 'false')
2959
2978
  .on('click', function() {
2960
- btn.toggleClass('active btn-primary');
2979
+ btn.toggleClass('active').attr('aria-pressed', btn.hasClass('active') ? 'true' : 'false');
2961
2980
  container.toggleClass(rowClass.cssClass, btn.hasClass('active'));
2962
2981
  refreshUtilities(container);
2963
2982
  })
@@ -2971,6 +2990,330 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2971
2990
  return detailsDiv;
2972
2991
  }
2973
2992
 
2993
+ /* --------------------------------------------------------------
2994
+ * Where a node's settings open.
2995
+ *
2996
+ * Each node's panel - createDetails' fields, and what plugins add to
2997
+ * them - is built into its drawer, as ever. settings_panel says where
2998
+ * it shows: 'inline' unfolds it in the drawer; 'offcanvas', 'popover'
2999
+ * and 'modal' move it into one of Bootstrap's, outside the canvas,
3000
+ * while it is open, and back into its drawer when it closes, so
3001
+ * everything that finds a panel through its node keeps working.
3002
+ *
3003
+ * Bootstrap's markup and styles, but the editor's own opening and
3004
+ * placing: a page may load Bootstrap's javascript without Popper, or
3005
+ * not at all. The modal is Bootstrap's own when Bootstrap is there.
3006
+ * -------------------------------------------------------------- */
3007
+
3008
+ var PANEL_MODES = ['offcanvas', 'popover', 'modal', 'inline'];
3009
+ var settingsPanels = {}; // One of each, built on first use, outside the canvas
3010
+ var openSettingsState = null; // What is open: { mode, node, details, home, next, gear }
3011
+
3012
+ function panelMode() {
3013
+ if (PANEL_MODES.indexOf(settings.settings_panel) !== -1) { return settings.settings_panel; }
3014
+
3015
+ warnOnceHere('settings_panel', 'settings_panel "' + settings.settings_panel + '" is not one of ' +
3016
+ PANEL_MODES.join(', ') + ': using offcanvas');
3017
+ return 'offcanvas';
3018
+ }
3019
+
3020
+ /** A node's panel, wherever it is at the moment. */
3021
+ function detailsOf(node) {
3022
+ var details = node.data('ge-details');
3023
+ return details && details.length ? details : $();
3024
+ }
3025
+
3026
+ /** Where settings fields are found: the canvas, and the panel open outside it. */
3027
+ function settingsScope() {
3028
+ return openSettingsState && openSettingsState.mode !== 'inline'
3029
+ ? canvas.add(openSettingsState.details)
3030
+ : canvas;
3031
+ }
3032
+
3033
+ /** The gear: its node's panel, opened or closed; another node's closes first. */
3034
+ function toggleSettings(node, details, gear) {
3035
+ var mode = panelMode();
3036
+
3037
+ if (mode === 'inline') {
3038
+ details.toggle();
3039
+ return;
3040
+ }
3041
+
3042
+ var same = openSettingsState && openSettingsState.details[0] === details[0];
3043
+ closeSettings();
3044
+ if (!same) { openSettings(mode, node, details, gear); }
3045
+ }
3046
+
3047
+ function openSettings(mode, node, details, gear) {
3048
+ var panel = settingsPanel(mode);
3049
+
3050
+ openSettingsState = {
3051
+ mode: mode,
3052
+ node: node,
3053
+ details: details,
3054
+ home: details.parent(),
3055
+ next: details.next(),
3056
+ gear: gear,
3057
+ };
3058
+
3059
+ panel.find('.ge-settings-title').text(t('panel.title', { kind: kindLabel(node) }));
3060
+ details.appendTo(panel.find('.ge-settings-body')).show();
3061
+ node.addClass('ge-settings-target');
3062
+
3063
+ var ns = '.ge-settings-' + instanceId;
3064
+ $(document).on('keydown' + ns, function(e) {
3065
+ if (e.key === 'Escape') { closeSettings(); }
3066
+ });
3067
+
3068
+ if (mode === 'offcanvas') {
3069
+ // From the bottom on a phone, from the side anywhere wider
3070
+ var narrow = window.innerWidth < 576;
3071
+ panel.toggleClass('offcanvas-end', !narrow).toggleClass('offcanvas-bottom', narrow);
3072
+ panel.removeClass('hiding');
3073
+ panel[0].getBoundingClientRect(); // So the slide in is a transition, not a jump
3074
+ panel.addClass('show');
3075
+ } else if (mode === 'popover') {
3076
+ placePopover(panel, gear);
3077
+ $(window).on('scroll' + ns + ' resize' + ns, function() { placePopover(panel, gear); });
3078
+
3079
+ // And again whenever what is in it changes size: the
3080
+ // Responsive section unfolding, a plugin swapping a field
3081
+ if (window.ResizeObserver) {
3082
+ openSettingsState.observer = new window.ResizeObserver(function() { placePopover(panel, gear); });
3083
+ openSettingsState.observer.observe(details[0]);
3084
+ }
3085
+ // A press anywhere else puts it away, as a popover does
3086
+ $(document).on('mousedown' + ns + ' touchstart' + ns, function(e) {
3087
+ if (!$(e.target).closest(panel).length && !$(e.target).closest(gear).length) { closeSettings(); }
3088
+ });
3089
+ } else {
3090
+ showModal(panel);
3091
+ }
3092
+ }
3093
+
3094
+ /** Put what is open away, and its panel back in its drawer. */
3095
+ function closeSettings() {
3096
+ var open = openSettingsState;
3097
+ if (!open) { return; }
3098
+ openSettingsState = null;
3099
+
3100
+ var panel = settingsPanels[open.mode];
3101
+ $(document).off('.ge-settings-' + instanceId);
3102
+ $(window).off('.ge-settings-' + instanceId);
3103
+ if (open.observer) { open.observer.disconnect(); }
3104
+
3105
+ if (open.mode === 'offcanvas') {
3106
+ panel.removeClass('show').addClass('hiding');
3107
+ window.setTimeout(function() { panel.removeClass('hiding'); }, 300);
3108
+ } else if (open.mode === 'popover') {
3109
+ panel.hide();
3110
+ } else {
3111
+ hideModal(panel);
3112
+ }
3113
+
3114
+ open.node.removeClass('ge-settings-target');
3115
+ open.details.css('display', '');
3116
+
3117
+ // Where it was in its drawer, if the drawer is still there: a
3118
+ // node deleted while its settings were open took it with it
3119
+ if (open.home.length && $.contains(document.documentElement, open.home[0])) {
3120
+ if (open.next.length && open.next.parent()[0] === open.home[0]) {
3121
+ open.details.insertBefore(open.next);
3122
+ } else {
3123
+ open.details.appendTo(open.home);
3124
+ }
3125
+ } else {
3126
+ open.details.remove();
3127
+ }
3128
+ }
3129
+
3130
+ /** Built once per mode, on first use, and taken away by destroy() or setLocale(). */
3131
+ function settingsPanel(mode) {
3132
+ if (settingsPanels[mode]) { return settingsPanels[mode]; }
3133
+
3134
+ var closeButton = function(extra) {
3135
+ return $('<button type="button" class="btn-close ge-settings-close" />')
3136
+ .addClass(extra || '')
3137
+ .attr('aria-label', t('panel.close'));
3138
+ };
3139
+ var panel;
3140
+
3141
+ if (mode === 'offcanvas') {
3142
+ panel = $('<div class="offcanvas offcanvas-end ge-settings-panel ge-settings-offcanvas" tabindex="-1" role="dialog" />')
3143
+ .append($('<div class="offcanvas-header" />')
3144
+ .append('<h5 class="offcanvas-title ge-settings-title"></h5>')
3145
+ .append(closeButton()))
3146
+ .append('<div class="offcanvas-body ge-settings-body"></div>');
3147
+ } else if (mode === 'popover') {
3148
+ panel = $('<div class="popover bs-popover-bottom ge-settings-panel ge-settings-popover" role="dialog" />')
3149
+ .append('<div class="popover-arrow"></div>')
3150
+ .append($('<div class="popover-header" />')
3151
+ .append('<span class="ge-settings-title"></span>')
3152
+ .append(closeButton()))
3153
+ .append('<div class="popover-body ge-settings-body"></div>')
3154
+ .hide();
3155
+ } else {
3156
+ panel = $('<div class="modal fade ge-settings-panel ge-settings-modal" tabindex="-1" role="dialog" aria-hidden="true" />')
3157
+ .append($('<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" />')
3158
+ .append($('<div class="modal-content" />')
3159
+ .append($('<div class="modal-header" />')
3160
+ .append('<h5 class="modal-title ge-settings-title"></h5>')
3161
+ .append(closeButton()))
3162
+ .append('<div class="modal-body ge-settings-body"></div>')
3163
+ .append($('<div class="modal-footer" />')
3164
+ .append($('<button type="button" class="btn btn-primary ge-settings-close" />')
3165
+ .text(t('panel.done'))))));
3166
+ }
3167
+
3168
+ panel.on('click', '.ge-settings-close', function(e) {
3169
+ e.preventDefault();
3170
+ closeSettings();
3171
+ });
3172
+
3173
+ settingsPanels[mode] = panel.appendTo('body');
3174
+ return panel;
3175
+ }
3176
+
3177
+ function removeSettingsPanels() {
3178
+ closeSettings();
3179
+
3180
+ $.each(settingsPanels, function(mode, panel) {
3181
+ if (mode === 'modal' && window.bootstrap && window.bootstrap.Modal) {
3182
+ var instance = window.bootstrap.Modal.getInstance(panel[0]);
3183
+ if (instance) { instance.dispose(); }
3184
+ }
3185
+ panel.remove();
3186
+ });
3187
+ if (settingsBackdrop) { settingsBackdrop.remove(); settingsBackdrop = null; }
3188
+ settingsPanels = {};
3189
+ }
3190
+
3191
+ /**
3192
+ * Under the gear, or over it when there is more room there, inside
3193
+ * the window, and no taller than the room it has: past that its body
3194
+ * scrolls.
3195
+ */
3196
+ function placePopover(panel, gear) {
3197
+ if (!$.contains(document.documentElement, gear[0])) { return; }
3198
+
3199
+ panel.show();
3200
+
3201
+ var tool = gear[0].getBoundingClientRect();
3202
+ var gap = 8;
3203
+ var body = panel.children('.popover-body').css('max-height', '');
3204
+ var width = panel.outerWidth();
3205
+ var height = panel.outerHeight();
3206
+ var roomBelow = window.innerHeight - tool.bottom - 2 * gap;
3207
+ var roomAbove = tool.top - 2 * gap;
3208
+ var below = height <= roomBelow || roomBelow >= roomAbove;
3209
+ var room = below ? roomBelow : roomAbove;
3210
+
3211
+ if (height > room) {
3212
+ body.css('max-height', Math.max(120, room - (height - body.outerHeight())));
3213
+ height = panel.outerHeight();
3214
+ }
3215
+
3216
+ var top = below ? tool.bottom + gap : tool.top - gap - height;
3217
+ var left = Math.max(gap, Math.min(tool.left + tool.width / 2 - 24, window.innerWidth - width - gap));
3218
+
3219
+ panel
3220
+ .toggleClass('bs-popover-bottom', below)
3221
+ .toggleClass('bs-popover-top', !below)
3222
+ .css({ top: top + window.pageYOffset, left: left + window.pageXOffset })
3223
+ ;
3224
+
3225
+ // The arrow points at the gear, wherever the popover had to go
3226
+ panel.children('.popover-arrow').css('left',
3227
+ Math.max(gap, Math.min(tool.left + tool.width / 2 - left - 8, width - 24)));
3228
+ }
3229
+
3230
+ var settingsBackdrop = null; // The modal's backdrop, when Bootstrap's javascript is not there to make one
3231
+
3232
+ /**
3233
+ * Bootstrap's modal ignores a hide while it is still fading in, and a
3234
+ * show while it is fading out, so the editor asks for what it wants
3235
+ * and has it done once the running transition ends: a panel closed as
3236
+ * it opens - getHtml, a second click - would otherwise stay open, and
3237
+ * empty. A hide Bootstrap starts itself, from Escape or the backdrop,
3238
+ * closes the settings as the close button does.
3239
+ */
3240
+ function showModal(panel) {
3241
+ if (!window.bootstrap || !window.bootstrap.Modal) {
3242
+ settingsBackdrop = $('<div class="modal-backdrop fade show ge-settings-backdrop" />')
3243
+ .appendTo('body')
3244
+ .on('click', function() { closeSettings(); });
3245
+ panel.addClass('show').css('display', 'block').removeAttr('aria-hidden');
3246
+ return;
3247
+ }
3248
+
3249
+ var modal = window.bootstrap.Modal.getOrCreateInstance(panel[0]);
3250
+
3251
+ if (!panel.data('ge-watched')) {
3252
+ panel.data('ge-watched', true);
3253
+ panel.on('hide.bs.modal', function() {
3254
+ // Not asked for: Escape or the backdrop
3255
+ if (panel.data('ge-wanted') === 'open') {
3256
+ panel.data('ge-wanted', 'closed');
3257
+ panel.data('ge-busy', true);
3258
+ }
3259
+ });
3260
+ panel.on('shown.bs.modal', function() {
3261
+ panel.data('ge-busy', false);
3262
+ if (panel.data('ge-wanted') === 'closed') { hideModal(panel); }
3263
+ });
3264
+ panel.on('hidden.bs.modal', function() {
3265
+ panel.data('ge-busy', false);
3266
+ if (panel.data('ge-wanted') === 'open') {
3267
+ showModal(panel);
3268
+ } else if (openSettingsState && openSettingsState.mode === 'modal') {
3269
+ closeSettings();
3270
+ }
3271
+ });
3272
+ }
3273
+
3274
+ panel.data('ge-wanted', 'open');
3275
+ if (!panel.data('ge-busy')) {
3276
+ panel.data('ge-busy', true);
3277
+ modal.show();
3278
+ }
3279
+ }
3280
+
3281
+ function hideModal(panel) {
3282
+ if (!window.bootstrap || !window.bootstrap.Modal) {
3283
+ if (settingsBackdrop) { settingsBackdrop.remove(); settingsBackdrop = null; }
3284
+ panel.removeClass('show').css('display', '').attr('aria-hidden', 'true');
3285
+ return;
3286
+ }
3287
+
3288
+ panel.data('ge-wanted', 'closed');
3289
+
3290
+ // Already out of sight - Bootstrap hid it itself, and this is its
3291
+ // hidden event closing the settings - and nothing is under way:
3292
+ // a hide now would be ignored, and no event would come to say so
3293
+ if (!panel.data('ge-busy') && panel.hasClass('show')) {
3294
+ panel.data('ge-busy', true);
3295
+ window.bootstrap.Modal.getOrCreateInstance(panel[0]).hide();
3296
+ }
3297
+ }
3298
+
3299
+ /** What a panel's title calls a node. */
3300
+ function kindLabel(node) {
3301
+ var kind = kindOf(node);
3302
+
3303
+ if (CONTAINERS[kind] && CONTAINERS[kind].labelKey) { return t(CONTAINERS[kind].labelKey); }
3304
+
3305
+ switch (kind) {
3306
+ case 'row': return t('panel.kind_row');
3307
+ case 'column': return t('panel.kind_column');
3308
+ case 'text': return t('panel.kind_text');
3309
+ case 'element': return t('panel.kind_element');
3310
+ case 'section': return t('panel.kind_section');
3311
+ case 'tab': return t('panel.kind_tab');
3312
+ case 'accordion-item': return t('panel.kind_accordion_item');
3313
+ default: return kind;
3314
+ }
3315
+ }
3316
+
2974
3317
  /**
2975
3318
  * Make sure every column is marked as one, and that a column with no
2976
3319
  * sizing at all gets some.
@@ -3320,7 +3663,11 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3320
3663
  // With the whole drawer as the handle, the tools inside it are
3321
3664
  // still tools: a drag starting on one would swallow its click,
3322
3665
  // and the settings panel has fields to type in
3323
- ? '.ge-tools-drawer > a, .ge-details, input, textarea, button, select, option'
3666
+ // - the info tools excepted, which do nothing when clicked:
3667
+ // a text's drawer is small and made of little but tools, and
3668
+ // with those turned away as well there was nothing to hold
3669
+ ? '.ge-tools-drawer > a:not(.ge-text-info):not(.ge-element-info), .ge-details, ' +
3670
+ 'input, textarea, button, select, option'
3324
3671
  : 'input, textarea, button, select, option';
3325
3672
  }
3326
3673
 
@@ -3388,7 +3735,14 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3388
3735
  // synthetic events, so the tests could not exist without
3389
3736
  // this; it also gives one helper across browsers
3390
3737
  forceFallback: true,
3391
- fallbackOnBody: true,
3738
+
3739
+ // The copy that follows the pointer is appended to the
3740
+ // list the drag started in rather than to the body: it is
3741
+ // positioned fixed either way, and inside the canvas it
3742
+ // looks like what it is a copy of. On the body none of the
3743
+ // editing styles reached it - its drawers came out
3744
+ // unstyled, their settings panels open.
3745
+ fallbackOnBody: false,
3392
3746
 
3393
3747
  ghostClass: 'ge-drag-placeholder',
3394
3748
  chosenClass: 'ge-drag-chosen',
@@ -3475,15 +3829,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3475
3829
 
3476
3830
  /**
3477
3831
  * What a drag reports as moving. A text block is the editor's own
3478
- * wrapper, so what moved is the content area inside it, and until 6.0
3479
- * a content area moves as kind 'content', as it did in 5.x - adding
3480
- * and deleting one, which 5.x never announced, already say 'text'.
3832
+ * wrapper, so what moved is the content area inside it.
3481
3833
  */
3482
3834
  function moveSubject(item) {
3483
3835
  var node = item.hasClass('ge-text-block') ? item.children('.ge-content') : item;
3484
- var kind = kindOf(node);
3485
3836
 
3486
- return { kind: kind === 'text' ? 'content' : kind, node: node };
3837
+ return { kind: kindOf(node), node: node };
3487
3838
  }
3488
3839
 
3489
3840
  /** Put a node back where a refused drag found it. */
@@ -3949,10 +4300,102 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3949
4300
  }
3950
4301
  }
3951
4302
 
4303
+ /**
4304
+ * What a text is never made of: rows, containers, and whatever a
4305
+ * feature plugin says - the elements plugin's elements. Each is a
4306
+ * block of the column's, beside the texts.
4307
+ */
4308
+ function textCutter() {
4309
+ var cuts = ['.row', '[data-ge-container]'];
4310
+
4311
+ $.each(FEATURES, function(name, feature) {
4312
+ var cut = typeof feature.cuts === 'function' ? feature.cuts() : feature.cuts;
4313
+ if (cut) { cuts.push(cut); }
4314
+ });
4315
+
4316
+ return cuts.join(', ');
4317
+ }
4318
+
4319
+ /**
4320
+ * Take the blocks out of the text.
4321
+ *
4322
+ * Up to 5.x an element lived inside a content area, among the text
4323
+ * a rich text editor edited. From 6.0 it is a block of its own, so a
4324
+ * content area with one inside - markup 5.x saved - is cut there: the
4325
+ * text before stays in the content area, the element goes into the
4326
+ * column after it, and the text after goes into a new content area of
4327
+ * the same type. Only a content area's own children cut it, which is
4328
+ * where 5.x recognized elements; one inside a paragraph is text, as
4329
+ * it was. A part with nothing but whitespace is dropped, and the first
4330
+ * part that has something keeps the content area, id and classes and
4331
+ * all.
4332
+ *
4333
+ * On every init, and a no-op on markup 6.0 made: nothing to cut. A
4334
+ * content area its editor is open on is left for the next init.
4335
+ */
4336
+ function splitTexts(cutter) {
4337
+ canvas.find('.column > .ge-content, .column > .ge-text-block > .ge-content').each(function() {
4338
+ var area = $(this);
4339
+ if (area.hasClass('ge-rte-active') || !area.children().filter(cutter).length) { return; }
4340
+
4341
+ var type = area.attr('data-ge-content-type');
4342
+ var anchor = area.parent('.ge-text-block').length ? area.parent() : area;
4343
+ var pieces = [];
4344
+ var run = [];
4345
+
4346
+ $.each($.makeArray(area[0].childNodes), function(i, node) {
4347
+ if (node.nodeType === 1 && $(node).is(cutter)) {
4348
+ pieces.push({ text: run }, { block: node });
4349
+ run = [];
4350
+ } else {
4351
+ run.push(node);
4352
+ }
4353
+ });
4354
+ pieces.push({ text: run });
4355
+
4356
+ $(area[0].childNodes).detach();
4357
+
4358
+ var kept = false;
4359
+ var last = anchor;
4360
+
4361
+ pieces.forEach(function(piece) {
4362
+ var placed;
4363
+
4364
+ if (piece.block) {
4365
+ placed = $(piece.block);
4366
+ } else if (!hasContent(piece.text)) {
4367
+ return;
4368
+ } else if (kept) {
4369
+ // Of the same type, or of none if it had none
4370
+ placed = (type ? createDefaultContentWrapper(type) : $('<div class="ge-content" />'))
4371
+ .append(piece.text);
4372
+ } else {
4373
+ // The content area itself, moved along if an
4374
+ // element came before the first text
4375
+ area.append(piece.text);
4376
+ placed = anchor;
4377
+ kept = true;
4378
+ }
4379
+
4380
+ if (placed[0] !== last[0]) { placed.insertAfter(last); }
4381
+ last = placed;
4382
+ });
4383
+
4384
+ if (!kept) { anchor.remove(); }
4385
+ });
4386
+ }
4387
+
4388
+ /** Whether a run of nodes has anything in it but whitespace. */
4389
+ function hasContent(nodes) {
4390
+ return nodes.some(function(node) {
4391
+ return node.nodeType === 1 || (node.nodeType === 3 && /\S/.test(node.nodeValue));
4392
+ });
4393
+ }
4394
+
3952
4395
  /**
3953
4396
  * Wrap column content in <div class="ge-content"> where neccesary
3954
4397
  */
3955
- function wrapContent() {
4398
+ function wrapContent(cutter) {
3956
4399
  canvas.find('.column').each(function() {
3957
4400
  var col = $(this);
3958
4401
  var contents = $();
@@ -3966,10 +4409,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3966
4409
  // the next init.
3967
4410
  if (child.is('.ge-tools-drawer, .ge-resize-handle')) { return; }
3968
4411
 
3969
- // A container sits in the column beside the content
3970
- // areas, not inside one, so it ends a run of loose
3971
- // content rather than joining it
3972
- if (child.is('.row, .ge-content, .ge-text-block, [data-ge-container]')) {
4412
+ // A container, or an element, sits in the column beside
4413
+ // the content areas, not inside one, so it ends a run of
4414
+ // loose content rather than joining it
4415
+ if (child.is('.ge-content, .ge-text-block') || child.is(cutter)) {
3973
4416
  contents = doWrap(contents);
3974
4417
  } else {
3975
4418
  contents = contents.add(child);
@@ -4285,7 +4728,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4285
4728
 
4286
4729
  if (key === from) { return; }
4287
4730
 
4288
- canvas.find('.ge-utilities').each(function() { renderUtilities($(this)); });
4731
+ settingsScope().find('.ge-utilities').each(function() { renderUtilities($(this)); });
4289
4732
  refreshPreviews(canvas);
4290
4733
  plugins('onViewChange', key);
4291
4734
  emit('view-change', { canvas: canvas, breakpoint: key, from: from, to: key });
@@ -4471,6 +4914,18 @@ $.fn.gridEditor.locales = {
4471
4914
  'tool.indent_increase': 'Increase indent\n(hold shift for max)',
4472
4915
  'tool.edit_source': 'Edit Source Code',
4473
4916
  'tool.preview': 'Preview',
4917
+ 'panel.title': '{kind} settings',
4918
+ 'panel.close': 'Close',
4919
+ 'panel.done': 'Done',
4920
+ 'panel.id': 'Id',
4921
+ 'panel.classes': 'Classes',
4922
+ 'panel.kind_row': 'Row',
4923
+ 'panel.kind_column': 'Column',
4924
+ 'panel.kind_text': 'Text',
4925
+ 'panel.kind_element': 'Element',
4926
+ 'panel.kind_section': 'Section',
4927
+ 'panel.kind_tab': 'Tab',
4928
+ 'panel.kind_accordion_item': 'Accordion item',
4474
4929
  'tool.id_placeholder': 'id',
4475
4930
  'tool.id_title': 'Set a unique identifier',
4476
4931
  'tool.classes_placeholder': 'classes',