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

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.
@@ -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();
@@ -1491,6 +1493,12 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1491
1493
  }
1492
1494
 
1493
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
+
1494
1502
  runFilter(true);
1495
1503
  canvas.addClass('ge-editing');
1496
1504
  canvas.toggleClass('ge-drag-drawer', settings.drag_handle === 'drawer');
@@ -1510,6 +1518,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1510
1518
  }
1511
1519
 
1512
1520
  function deinit() {
1521
+ // Its panel goes home to its drawer first, and both go together
1522
+ closeSettings();
1513
1523
  canvas.removeClass('ge-editing ge-drag-drawer ge-dropping');
1514
1524
  canvas.find('.ge-content').each(function() {
1515
1525
  closeText($(this));
@@ -1562,6 +1572,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1562
1572
  function destroy() {
1563
1573
  deinit();
1564
1574
  removeConfirmModal();
1575
+ removeSettingsPanels();
1565
1576
  mainControls.remove();
1566
1577
  htmlTextArea.remove();
1567
1578
  $(window).off('scroll', onScroll);
@@ -1720,6 +1731,8 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1720
1731
  // A text editor has rewritten a content area, so whatever the
1721
1732
  // editor and its plugins had put in there goes back in
1722
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,
1723
1736
  // A node's drawer: its first child, or for a content area the
1724
1737
  // one beside it in its text block
1725
1738
  drawerOf: function(node) {
@@ -2010,9 +2023,9 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2010
2023
 
2011
2024
  /** Bring a node's panel and preview up to date with its classes. */
2012
2025
  function refreshUtilities(node) {
2013
- var details = node.children('.ge-tools-drawer').children('.ge-details');
2026
+ var details = detailsOf(node);
2014
2027
 
2015
- details.children('.ge-classes').val(hostClasses(node).join(' '));
2028
+ details.find('.ge-classes').val(hostClasses(node).join(' '));
2016
2029
  details.children('.ge-utilities').each(function() { renderUtilities($(this)); });
2017
2030
  refreshPreviews(node);
2018
2031
  }
@@ -2140,7 +2153,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2140
2153
  .appendTo(section)
2141
2154
  .on('click', function() {
2142
2155
  utilitiesOpen = !section.hasClass('ge-open');
2143
- canvas.find('.ge-utilities').toggleClass('ge-open', utilitiesOpen);
2156
+ settingsScope().find('.ge-utilities').toggleClass('ge-open', utilitiesOpen);
2144
2157
  })
2145
2158
  ;
2146
2159
 
@@ -2164,7 +2177,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2164
2177
  .text(family.labelKey ? t(family.labelKey) : family.name)
2165
2178
  .appendTo(field)
2166
2179
  ;
2167
- $('<select />')
2180
+ $('<select class="form-select form-select-sm" />')
2168
2181
  .appendTo(field)
2169
2182
  .on('change', function() {
2170
2183
  setUtility(node, family.name, this.value, { source: 'panel' });
@@ -2897,9 +2910,10 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2897
2910
  */
2898
2911
  function addSettingsTool(drawer, node, presets) {
2899
2912
  var details = createDetails(node, presets || []);
2913
+ node.data('ge-details', details);
2900
2914
 
2901
2915
  createTool(drawer, t('tool.settings'), 'ge-settings', 'bi bi-gear-fill', function() {
2902
- details.toggle();
2916
+ toggleSettings(node, details, $(this));
2903
2917
  });
2904
2918
 
2905
2919
  // Beside the gear, because every node that has one is a node a
@@ -2919,12 +2933,18 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2919
2933
 
2920
2934
  function createDetails(container, cssClasses) {
2921
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
+ };
2922
2942
 
2923
- $('<input class="ge-id" />')
2943
+ $('<input class="ge-id form-control form-control-sm" />')
2924
2944
  .attr('placeholder', t('tool.id_placeholder'))
2925
2945
  .val(container.attr('id'))
2926
2946
  .attr('title', t('tool.id_title'))
2927
- .appendTo(detailsDiv)
2947
+ .appendTo(field(t('panel.id')))
2928
2948
  .on('change', function() {
2929
2949
  // An empty field means no id, not an empty one
2930
2950
  if (this.value === '') {
@@ -2935,25 +2955,28 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2935
2955
  })
2936
2956
  ;
2937
2957
 
2938
- $('<input class="ge-classes" />')
2958
+ $('<input class="ge-classes form-control form-control-sm" />')
2939
2959
  .attr('placeholder', t('tool.classes_placeholder'))
2940
2960
  .attr('title', t('tool.classes_title'))
2941
2961
  .val(hostClasses(container).join(' '))
2942
- .appendTo(detailsDiv)
2962
+ .appendTo(field(t('panel.classes')))
2943
2963
  .on('change', function() {
2944
2964
  setHostClasses(container, this.value);
2945
2965
  refreshUtilities(container);
2946
2966
  })
2947
2967
  ;
2948
2968
 
2949
- 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);
2950
2972
  cssClasses.forEach(function(rowClass) {
2951
- var btn = $('<a class="btn btn-sm btn-default" />')
2973
+ var btn = $('<a role="button" class="btn btn-sm btn-outline-secondary" />')
2952
2974
  .html(rowClass.label)
2953
2975
  .attr('title', rowClass.title ? rowClass.title : t('tool.toggle_class', { label: rowClass.label }))
2954
- .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')
2955
2978
  .on('click', function() {
2956
- btn.toggleClass('active btn-primary');
2979
+ btn.toggleClass('active').attr('aria-pressed', btn.hasClass('active') ? 'true' : 'false');
2957
2980
  container.toggleClass(rowClass.cssClass, btn.hasClass('active'));
2958
2981
  refreshUtilities(container);
2959
2982
  })
@@ -2967,6 +2990,330 @@ $.fn.gridEditor = function( optionsOrMethod ) {
2967
2990
  return detailsDiv;
2968
2991
  }
2969
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
+
2970
3317
  /**
2971
3318
  * Make sure every column is marked as one, and that a column with no
2972
3319
  * sizing at all gets some.
@@ -3316,7 +3663,11 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3316
3663
  // With the whole drawer as the handle, the tools inside it are
3317
3664
  // still tools: a drag starting on one would swallow its click,
3318
3665
  // and the settings panel has fields to type in
3319
- ? '.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'
3320
3671
  : 'input, textarea, button, select, option';
3321
3672
  }
3322
3673
 
@@ -3384,7 +3735,14 @@ $.fn.gridEditor = function( optionsOrMethod ) {
3384
3735
  // synthetic events, so the tests could not exist without
3385
3736
  // this; it also gives one helper across browsers
3386
3737
  forceFallback: true,
3387
- 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,
3388
3746
 
3389
3747
  ghostClass: 'ge-drag-placeholder',
3390
3748
  chosenClass: 'ge-drag-chosen',
@@ -4138,16 +4496,23 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4138
4496
 
4139
4497
  createMoveTool(drawer);
4140
4498
 
4141
- // Which editor edits it, or that none can: a type whose plugin is
4142
- // not loaded is still a block to move and delete, but not to edit
4143
- if (type && TEXTS[type]) {
4144
- createTool(drawer, t('tool.text_info', { editor: textLabel(type) }), 'ge-text-info', 'bi bi-fonts');
4145
- } else if (type) {
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]) {
4146
4502
  createTool(drawer, t('text.no_editor', { type: type }), 'ge-text-info ge-text-missing',
4147
4503
  'bi bi-exclamation-triangle');
4148
4504
  }
4149
4505
 
4150
- addSettingsTool(drawer, block, settings.text_classes);
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
+ }
4151
4516
 
4152
4517
  settings.text_tools.forEach(function(hostTool) {
4153
4518
  createTool(drawer, hostTool.title || '', hostTool.className || '',
@@ -4370,7 +4735,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
4370
4735
 
4371
4736
  if (key === from) { return; }
4372
4737
 
4373
- canvas.find('.ge-utilities').each(function() { renderUtilities($(this)); });
4738
+ settingsScope().find('.ge-utilities').each(function() { renderUtilities($(this)); });
4374
4739
  refreshPreviews(canvas);
4375
4740
  plugins('onViewChange', key);
4376
4741
  emit('view-change', { canvas: canvas, breakpoint: key, from: from, to: key });
@@ -4536,7 +4901,6 @@ $.fn.gridEditor.locales = {
4536
4901
  'tool.add_row': 'Add row',
4537
4902
  'tool.add_text': 'Add text',
4538
4903
  'tool.add_text_type': 'Add {editor} text',
4539
- 'tool.text_info': 'Text: {editor}',
4540
4904
  'tool.delete_text': 'Remove text',
4541
4905
  'text.add': 'Text',
4542
4906
  'text.add_type': 'Text ({editor})',
@@ -4556,6 +4920,19 @@ $.fn.gridEditor.locales = {
4556
4920
  'tool.indent_increase': 'Increase indent\n(hold shift for max)',
4557
4921
  'tool.edit_source': 'Edit Source Code',
4558
4922
  'tool.preview': 'Preview',
4923
+ 'panel.title': '{kind} settings',
4924
+ 'panel.close': 'Close',
4925
+ 'panel.done': 'Done',
4926
+ 'panel.id': 'Id',
4927
+ 'panel.classes': 'Classes',
4928
+ 'panel.editor': 'Editor',
4929
+ 'panel.kind_row': 'Row',
4930
+ 'panel.kind_column': 'Column',
4931
+ 'panel.kind_text': 'Text',
4932
+ 'panel.kind_element': 'Element',
4933
+ 'panel.kind_section': 'Section',
4934
+ 'panel.kind_tab': 'Tab',
4935
+ 'panel.kind_accordion_item': 'Accordion item',
4559
4936
  'tool.id_placeholder': 'id',
4560
4937
  'tool.id_title': 'Set a unique identifier',
4561
4938
  'tool.classes_placeholder': 'classes',
@@ -21,7 +21,6 @@
21
21
  'tool.add_row': 'Añadir fila',
22
22
  'tool.add_text': 'Añadir texto',
23
23
  'tool.add_text_type': 'Añadir texto con {editor}',
24
- 'tool.text_info': 'Texto: {editor}',
25
24
  'tool.delete_text': 'Quitar texto',
26
25
  'text.add': 'Texto',
27
26
  'text.add_type': 'Texto ({editor})',
@@ -47,6 +46,19 @@
47
46
  'tool.indent_increase': 'Aumentar sangría\n(Mayús para el máximo)',
48
47
  'tool.edit_source': 'Editar el código fuente',
49
48
  'tool.preview': 'Vista previa',
49
+ 'panel.title': 'Ajustes: {kind}',
50
+ 'panel.close': 'Cerrar',
51
+ 'panel.done': 'Hecho',
52
+ 'panel.id': 'Id',
53
+ 'panel.classes': 'Clases',
54
+ 'panel.editor': 'Editor',
55
+ 'panel.kind_row': 'Fila',
56
+ 'panel.kind_column': 'Columna',
57
+ 'panel.kind_text': 'Texto',
58
+ 'panel.kind_element': 'Elemento',
59
+ 'panel.kind_section': 'Sección',
60
+ 'panel.kind_tab': 'Pestaña',
61
+ 'panel.kind_accordion_item': 'Elemento de acordeón',
50
62
  'tool.id_placeholder': 'id',
51
63
  'tool.id_title': 'Asignar un identificador único',
52
64
  'tool.classes_placeholder': 'clases',
@@ -90,7 +90,7 @@ $.fn.gridEditor.features.sections = function(ge) {
90
90
  });
91
91
 
92
92
  if (!ge.emit('before-utility', payload)) {
93
- section.find('> .ge-tools-drawer .ge-section-width select').val(from);
93
+ ge.detailsOf(section).find('.ge-section-width select').val(from);
94
94
  return false;
95
95
  }
96
96
 
@@ -102,7 +102,7 @@ $.fn.gridEditor.features.sections = function(ge) {
102
102
  }
103
103
 
104
104
  function widthField(section) {
105
- var select = $('<select />');
105
+ var select = $('<select class="form-select form-select-sm" />');
106
106
  var current = widthOf(section);
107
107
  var offered = options.widths.indexOf(current) === -1 ? options.widths.concat([current]) : options.widths;
108
108
 
@@ -183,7 +183,7 @@ $.fn.gridEditor.utilities.spacing = function(ge) {
183
183
  /** A padding or margin group: its name, the side, and the field of that side's family. */
184
184
  function group(node, key, labelText) {
185
185
  var box = $('<div class="ge-spacing-group" />').attr('data-ge-spacing', key);
186
- var side = $('<select class="ge-spacing-side" />');
186
+ var side = $('<select class="ge-spacing-side form-select form-select-sm" />');
187
187
 
188
188
  $('<span class="ge-utility-label" />').text(labelText).appendTo(box);
189
189