@themarioga/grid-editor 8.8.2 → 8.9.0

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.
@@ -38,6 +38,8 @@ var METHODS = {
38
38
  setUtility: { value: true },
39
39
  getActiveTarget: { value: true },
40
40
  setActiveTarget: {},
41
+ getSelected: { value: true },
42
+ setSelected: {},
41
43
  };
42
44
 
43
45
  /**
@@ -154,6 +156,9 @@ var NESTED_SETTINGS = {
154
156
  indent: {
155
157
  tools: true, // The indent tools in the column's drawer
156
158
  },
159
+ sidebar: {
160
+ empty: null, // function(ge) -> { title, body } shown while nothing is selected, or null for the message
161
+ },
157
162
  drag: {
158
163
  delay: 0, // Milliseconds to hold before a drag starts
159
164
  touch_delay: 100, // The same for touch, where 0 eats the page's scrolling
@@ -171,6 +176,13 @@ var editorCounter = 0;
171
176
  /** The editor on each canvas element, so there is never a second one. */
172
177
  var instances = new WeakMap();
173
178
 
179
+ /** The editor whose settings_panel is 'sidebar': a page has one sidebar, so one editor at most. */
180
+ var sidebarHolder = null;
181
+
182
+ /** The editor's own controls outside the canvas: a click on one is not a click on the page. */
183
+ var EDITOR_CONTROLS = '.ge-mainControls, .ge-settings-panel, .ge-sidebar-toggle, .ge-dialog, .ge-confirm, ' +
184
+ '.ge-size-picker, .modal, .modal-backdrop';
185
+
174
186
  /**
175
187
  * Translate one key.
176
188
  *
@@ -408,7 +420,8 @@ function build(instance, baseElem, optionsOrMethod) {
408
420
  'locale_strings' : {}, // Overrides for individual keys
409
421
  'callbacks' : {}, // before_*/after_* functions, the events by another route
410
422
  'confirm_delete' : true, // Ask before deleting a row or a column
411
- 'settings_panel' : 'offcanvas', // Where a node's settings open: 'offcanvas', 'popover', 'modal' or 'inline'
423
+ 'settings_panel' : 'offcanvas', // Where a node's settings open: 'offcanvas', 'popover', 'modal', 'inline' or 'sidebar'
424
+ 'sidebar' : NESTED_SETTINGS.sidebar, // The 'sidebar' panel: what it shows while nothing is selected
412
425
  'drag' : NESTED_SETTINGS.drag // How a drag behaves, whatever drives it
413
426
  }, optionsOrMethod);
414
427
 
@@ -444,6 +457,19 @@ function build(instance, baseElem, optionsOrMethod) {
444
457
  var sortables = []; // Every list made sortable, so deinit destroys exactly those
445
458
  var instanceId = ++editorCounter; // Scopes the sortable groups to this editor
446
459
  var destroyed = false;
460
+ var selected = null; // The node whose settings the sidebar shows, with settings_panel 'sidebar'
461
+ var sidebarCollapsed = null; // Folded away, once the sidebar has been shown: the window's width decides the first time
462
+ var pluginHandle = null; // What the plugins were handed, for the hooks that call them back
463
+
464
+ if (settings.settings_panel === 'sidebar') {
465
+ if (sidebarHolder) {
466
+ warnOnceHere('sidebar', 'settings_panel "sidebar" is in use by another editor on the page, which has one ' +
467
+ 'sidebar: using offcanvas');
468
+ settings.settings_panel = 'offcanvas';
469
+ } else {
470
+ sidebarHolder = instance;
471
+ }
472
+ }
447
473
 
448
474
  // What the editor remembers about the nodes it edits, which used to
449
475
  // be jQuery data: a node's settings panel, a drag or a resize in
@@ -1055,10 +1081,9 @@ function build(instance, baseElem, optionsOrMethod) {
1055
1081
  if (dom.hasClass(canvas, 'ge-editing')) { e.preventDefault(); }
1056
1082
  }, signal);
1057
1083
 
1058
- if (settings.active_target) {
1059
- // Whatever the click was for - a text, a tool - it also says
1060
- // where the toolbar adds next. A click on no column or region
1061
- // says nowhere, and the toolbar adds to the canvas again.
1084
+ var sidebarMode = panelMode() === 'sidebar';
1085
+
1086
+ if (settings.active_target || sidebarMode) {
1062
1087
  // The click a drag ends in - a resize, a move - is not one:
1063
1088
  // it comes where the pointer let go, not where it pressed
1064
1089
  var pressed = null;
@@ -1070,28 +1095,67 @@ function build(instance, baseElem, optionsOrMethod) {
1070
1095
  pressed = null;
1071
1096
  if (!dom.hasClass(canvas, 'ge-editing')) { return; }
1072
1097
  if (from && Math.abs(e.clientX - from.x) + Math.abs(e.clientY - from.y) > settings.drag.threshold) { return; }
1073
- var region = dom.closest(e.target, targetSelector());
1074
1098
 
1075
- // The target's own background, or its drawer's, clicked
1076
- // again takes it back. What is in it - a text, a tool, a
1077
- // nested column - keeps it, or makes that the target
1078
- if (region && region === activeTarget &&
1079
- (e.target === region || e.target === dom.child(region, '.ge-tools-drawer'))) {
1080
- changeTarget(null);
1081
- return;
1082
- }
1083
-
1084
- changeTarget(region && region !== canvas && canvas.contains(region) ? region : null);
1099
+ if (settings.active_target) { clickTarget(e); }
1100
+ // A tool that took its node off the canvas says nothing of where the click was
1101
+ if (sidebarMode && canvas.contains(e.target)) { changeSelection(settingsNodeAt(e.target)); }
1085
1102
  }, signal);
1086
1103
 
1087
1104
  // Escape is the panel's, the dialog's, and a field's or a
1088
- // text's while one has the focus; after those, it is this
1105
+ // text's while one has the focus; after those, it is the
1106
+ // selection's, and then the target's
1089
1107
  document.addEventListener('keydown', function(e) {
1090
- if (e.key !== 'Escape' || !activeTarget || openSettingsState || dialogState) { return; }
1108
+ if (e.key !== 'Escape' || openSettingsState || dialogState) { return; }
1109
+ if (confirmDialog && dom.hasClass(confirmDialog, 'show')) { return; }
1091
1110
  var focus = document.activeElement;
1092
1111
  if (focus && (dom.is(focus, 'input, textarea, select') || focus.isContentEditable)) { return; }
1093
- changeTarget(null);
1112
+
1113
+ if (selected && isEditing()) {
1114
+ changeSelection(null);
1115
+ } else if (activeTarget) {
1116
+ changeTarget(null);
1117
+ }
1118
+ }, signal);
1119
+ }
1120
+
1121
+ if (sidebarMode) {
1122
+ // The focus coming into a node selects it, as a click does
1123
+ canvas.addEventListener('focusin', function(e) {
1124
+ var node = isEditing() ? settingsNodeAt(e.target) : null;
1125
+ if (node) { changeSelection(node); }
1094
1126
  }, signal);
1127
+
1128
+ // A click on the page, away from the canvas and the editor's own
1129
+ // controls, puts the selection away - not while a text is open,
1130
+ // whose editor has controls of its own out there
1131
+ document.addEventListener('click', function(e) {
1132
+ var target = e.target;
1133
+ if (!selected || !isEditing() || !target || target.nodeType !== 1) { return; }
1134
+ if (!dom.attached(target) || canvas.contains(target)) { return; }
1135
+ if (dom.closest(target, EDITOR_CONTROLS) || canvas.querySelector('.ge-rte-active')) { return; }
1136
+
1137
+ changeSelection(null);
1138
+ }, signal);
1139
+
1140
+ window.addEventListener('resize', reserveSidebarRoom, signal);
1141
+ }
1142
+
1143
+ /* Whatever the click was for - a text, a tool - it also says
1144
+ where the toolbar adds next. A click on no column or region
1145
+ says nowhere, and the toolbar adds to the canvas again. */
1146
+ function clickTarget(e) {
1147
+ var region = dom.closest(e.target, targetSelector());
1148
+
1149
+ // The target's own background, or its drawer's, clicked
1150
+ // again takes it back. What is in it - a text, a tool, a
1151
+ // nested column - keeps it, or makes that the target
1152
+ if (region && region === activeTarget &&
1153
+ (e.target === region || e.target === dom.child(region, '.ge-tools-drawer'))) {
1154
+ changeTarget(null);
1155
+ return;
1156
+ }
1157
+
1158
+ changeTarget(region && region !== canvas && canvas.contains(region) ? region : null);
1095
1159
  }
1096
1160
  }
1097
1161
 
@@ -2074,6 +2138,7 @@ function build(instance, baseElem, optionsOrMethod) {
2074
2138
  deinit();
2075
2139
  // The canvas is made again from the html when it comes back
2076
2140
  changeTarget(null);
2141
+ changeSelection(null);
2077
2142
  htmlTextArea.style.height = (0.8 * document.documentElement.clientHeight) + 'px';
2078
2143
  htmlTextArea.value = canvas.innerHTML;
2079
2144
  dom.show(htmlTextArea);
@@ -2135,6 +2200,10 @@ function build(instance, baseElem, optionsOrMethod) {
2135
2200
  if (openSettingsState && !dom.attached(openSettingsState.node)) {
2136
2201
  closeSettings();
2137
2202
  }
2203
+ // The same for the selected node, which the sidebar stops showing
2204
+ if (selected && !canvas.contains(selected)) {
2205
+ changeSelection(null);
2206
+ }
2138
2207
  // And a dialog showing part of a panel that is gone
2139
2208
  if (dialogState && dialogState.home && !dom.attached(dialogState.home)) {
2140
2209
  closeDialog();
@@ -2163,11 +2232,14 @@ function build(instance, baseElem, optionsOrMethod) {
2163
2232
  } else if (activeTarget) {
2164
2233
  dom.addClass(activeTarget, 'ge-active-target');
2165
2234
  }
2235
+
2236
+ showSidebar();
2166
2237
  }
2167
2238
 
2168
2239
  function deinit() {
2169
2240
  // Its panel goes home to its drawer first, and both go together
2170
2241
  closeSettings();
2242
+ hideSidebar();
2171
2243
  dom.removeClass(canvas, 'ge-editing ge-drag-drawer ge-dropping');
2172
2244
  // The mark goes, the target stays: getHtml comes straight back
2173
2245
  if (activeTarget) { dom.removeClass(activeTarget, 'ge-active-target'); }
@@ -2376,6 +2448,8 @@ function build(instance, baseElem, optionsOrMethod) {
2376
2448
  if (sourceOpen) { closeSource(); }
2377
2449
  deinit();
2378
2450
  activeTarget = null;
2451
+ selected = null;
2452
+ if (sidebarHolder === instance) { sidebarHolder = null; }
2379
2453
  removeConfirmModal();
2380
2454
  removeSettingsPanels();
2381
2455
  removeDialog();
@@ -2396,7 +2470,7 @@ function build(instance, baseElem, optionsOrMethod) {
2396
2470
  * something it can see.
2397
2471
  */
2398
2472
  function loadPlugins() {
2399
- var api = pluginApi();
2473
+ var api = pluginHandle = pluginApi();
2400
2474
 
2401
2475
  registerFamily('grid', {}, widthFamily());
2402
2476
  if (settings.row_cols !== false) { registerFamily('grid', {}, rowColsFamily()); }
@@ -3885,9 +3959,13 @@ function build(instance, baseElem, optionsOrMethod) {
3885
3959
  var details = createDetails(node, presets || []);
3886
3960
  detailsFor.set(node, details);
3887
3961
 
3888
- createTool(drawer, t('tool.settings'), 'ge-settings', 'bi bi-gear-fill', function() {
3889
- toggleSettings(node, details, this);
3890
- });
3962
+ // The sidebar shows the panel of the node that is selected, so
3963
+ // there is no gear to open it with
3964
+ if (panelMode() !== 'sidebar') {
3965
+ createTool(drawer, t('tool.settings'), 'ge-settings', 'bi bi-gear-fill', function() {
3966
+ toggleSettings(node, details, this);
3967
+ });
3968
+ }
3891
3969
 
3892
3970
  // Beside the gear, because every node that has one is a node a
3893
3971
  // utility may apply to, whichever plugin built its drawer. A
@@ -3999,7 +4077,7 @@ function build(instance, baseElem, optionsOrMethod) {
3999
4077
 
4000
4078
  var holder = dom.element('div', { 'class': 'ge-panel-section', 'data-ge-plugin': name });
4001
4079
 
4002
- if (mode === 'offcanvas' || mode === 'modal') {
4080
+ if (mode === 'offcanvas' || mode === 'modal' || mode === 'sidebar') {
4003
4081
  holder.appendChild(sectionTitle(t(section.labelKey)));
4004
4082
  holder.appendChild(section.body);
4005
4083
  } else {
@@ -4039,7 +4117,7 @@ function build(instance, baseElem, optionsOrMethod) {
4039
4117
  * not at all. The modal is Bootstrap's own when Bootstrap is there.
4040
4118
  * -------------------------------------------------------------- */
4041
4119
 
4042
- var PANEL_MODES = ['offcanvas', 'popover', 'modal', 'inline'];
4120
+ var PANEL_MODES = ['offcanvas', 'popover', 'modal', 'inline', 'sidebar'];
4043
4121
  var settingsPanels = {}; // One of each, built on first use, outside the canvas
4044
4122
  var openSettingsState = null; // What is open: { mode, node, details, home, next, gear }
4045
4123
  var modalState = new WeakMap(); // modal panel -> { watched, wanted, busy, backdrop, dismissed }
@@ -4059,9 +4137,12 @@ function build(instance, baseElem, optionsOrMethod) {
4059
4137
 
4060
4138
  /** Where settings fields are found: the canvas, and the panel open outside it. */
4061
4139
  function settingsScope() {
4062
- return openSettingsState && openSettingsState.mode !== 'inline'
4063
- ? [canvas, openSettingsState.details]
4064
- : [canvas];
4140
+ var scope = [canvas];
4141
+
4142
+ if (openSettingsState && openSettingsState.mode !== 'inline') { scope.push(openSettingsState.details); }
4143
+ if (sidebarShown) { scope.push(sidebarShown.details); }
4144
+
4145
+ return scope;
4065
4146
  }
4066
4147
 
4067
4148
  /** The gear: its node's panel, opened or closed; another node's closes first. */
@@ -4155,6 +4236,11 @@ function build(instance, baseElem, optionsOrMethod) {
4155
4236
  hideModal(panel);
4156
4237
  }
4157
4238
 
4239
+ returnPanel(open);
4240
+ }
4241
+
4242
+ /** A node's panel back where it was in its drawer, and the node no longer marked. */
4243
+ function returnPanel(open) {
4158
4244
  dom.removeClass(open.node, 'ge-settings-target');
4159
4245
  open.details.style.removeProperty('display');
4160
4246
  dom.dropEmptyStyle(open.details);
@@ -4199,6 +4285,15 @@ function build(instance, baseElem, optionsOrMethod) {
4199
4285
  '</div>');
4200
4286
  dom.one(panel, '.popover-header').appendChild(closeButton());
4201
4287
  dom.hide(panel);
4288
+ } else if (mode === 'sidebar') {
4289
+ panel = dom.create('<div class="offcanvas offcanvas-end ge-settings-panel ge-settings-sidebar" tabindex="-1" role="complementary">' +
4290
+ '<div class="offcanvas-header"><h5 class="offcanvas-title ge-settings-title"></h5></div>' +
4291
+ '<div class="offcanvas-body ge-settings-body"></div>' +
4292
+ '</div>');
4293
+ panel.id = 'ge-settings-sidebar-' + instanceId;
4294
+ dom.one(panel, '.offcanvas-header').appendChild(closeButton());
4295
+ // Beside the panel rather than in it: it stays on the window's edge when the panel is folded away
4296
+ settingsPanels['sidebar-toggle'] = document.body.appendChild(sidebarToggle(panel));
4202
4297
  } else {
4203
4298
  panel = dom.create('<div class="modal fade ge-settings-panel ge-settings-modal" tabindex="-1" role="dialog" aria-hidden="true">' +
4204
4299
  '<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">' +
@@ -4218,7 +4313,7 @@ function build(instance, baseElem, optionsOrMethod) {
4218
4313
 
4219
4314
  dom.delegate(panel, 'click', '.ge-settings-close', function(e) {
4220
4315
  e.preventDefault();
4221
- closeSettings();
4316
+ if (mode === 'sidebar') { changeSelection(null); } else { closeSettings(); }
4222
4317
  });
4223
4318
 
4224
4319
  settingsPanels[mode] = document.body.appendChild(panel);
@@ -4227,6 +4322,8 @@ function build(instance, baseElem, optionsOrMethod) {
4227
4322
 
4228
4323
  function removeSettingsPanels() {
4229
4324
  closeSettings();
4325
+ unshowSidebar();
4326
+ dom.removeClass(document.documentElement, 'ge-sidebar-open');
4230
4327
 
4231
4328
  Object.keys(settingsPanels).forEach(function(mode) {
4232
4329
  if (mode === 'modal') {
@@ -4239,6 +4336,218 @@ function build(instance, baseElem, optionsOrMethod) {
4239
4336
  settingsPanels = {};
4240
4337
  }
4241
4338
 
4339
+ /* --------------------------------------------------------------
4340
+ * settings_panel: 'sidebar'. An offcanvas that stays: there while
4341
+ * the canvas is edited, making room for itself on the page, with the
4342
+ * settings of the node that is selected in it - or, with none, a
4343
+ * message, or what the host or a plugin put there instead.
4344
+ *
4345
+ * The panels are the same ones: the selected node's moves into the
4346
+ * sidebar and goes back to its drawer when another is selected, so
4347
+ * ge.detailsOf(node) finds it either way. Folded away, the panel
4348
+ * slides out and its button stays on the window's edge.
4349
+ * -------------------------------------------------------------- */
4350
+
4351
+ var SIDEBAR_MIN_WIDTH = 576; // Bootstrap's sm: narrower than that, the sidebar covers the page instead of making room
4352
+ var sidebarShown = null; // The selected node's panel while it is in the sidebar: { node, details, home, next }
4353
+ var sidebarEmpty = null; // What the sidebar shows while nothing is selected: { body }
4354
+
4355
+ function isEditing() {
4356
+ return dom.hasClass(canvas, 'ge-editing');
4357
+ }
4358
+
4359
+ function sidebarToggle(panel) {
4360
+ var button = dom.element('button', { type: 'button', 'class': 'ge-sidebar-toggle', 'aria-controls': panel.id });
4361
+ button.appendChild(dom.element('i'));
4362
+ button.addEventListener('click', function() { foldSidebar(!sidebarCollapsed); });
4363
+ return button;
4364
+ }
4365
+
4366
+ function foldSidebar(collapsed) {
4367
+ sidebarCollapsed = collapsed;
4368
+ applySidebar();
4369
+ }
4370
+
4371
+ /** The panel and its button as `sidebarCollapsed` says. */
4372
+ function applySidebar() {
4373
+ var panel = settingsPanels.sidebar;
4374
+ var toggle = settingsPanels['sidebar-toggle'];
4375
+ if (!panel) { return; }
4376
+
4377
+ var open = !sidebarCollapsed;
4378
+
4379
+ if (open && !dom.hasClass(panel, 'show')) {
4380
+ dom.removeClass(panel, 'hiding');
4381
+ panel.getBoundingClientRect(); // So the slide in is a transition, not a jump
4382
+ dom.addClass(panel, 'show');
4383
+ } else if (!open && dom.hasClass(panel, 'show')) {
4384
+ dom.removeClass(panel, 'show');
4385
+ dom.addClass(panel, 'hiding');
4386
+ window.setTimeout(function() { dom.removeClass(panel, 'hiding'); }, 300);
4387
+ }
4388
+
4389
+ var label = open ? t('panel.collapse') : t('panel.expand');
4390
+ toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
4391
+ toggle.setAttribute('aria-label', label);
4392
+ toggle.setAttribute('title', label);
4393
+ dom.one(toggle, 'i').className = 'bi ' + (open ? 'bi-chevron-right' : 'bi-chevron-left');
4394
+
4395
+ reserveSidebarRoom();
4396
+ }
4397
+
4398
+ /** The page makes room for the sidebar only while it is edited, unfolded, and the window is wide enough to have room. */
4399
+ function reserveSidebarRoom() {
4400
+ dom.toggleClass(document.documentElement, 'ge-sidebar-open',
4401
+ !!settingsPanels.sidebar && isEditing() && !sidebarCollapsed && window.innerWidth >= SIDEBAR_MIN_WIDTH);
4402
+ }
4403
+
4404
+ /** Editing starts, or starts again: the sidebar is there, showing what is selected. */
4405
+ function showSidebar() {
4406
+ if (panelMode() !== 'sidebar') { return; }
4407
+
4408
+ var panel = settingsPanel('sidebar');
4409
+ if (sidebarCollapsed === null) { sidebarCollapsed = window.innerWidth < SIDEBAR_MIN_WIDTH; }
4410
+
4411
+ panel.style.removeProperty('display');
4412
+ settingsPanels['sidebar-toggle'].style.removeProperty('display');
4413
+ renderSidebar();
4414
+ applySidebar();
4415
+ }
4416
+
4417
+ /** Editing stops: out of sight, with the selected node's panel back in its drawer. The selection stays. */
4418
+ function hideSidebar() {
4419
+ unshowDetails();
4420
+
4421
+ var panel = settingsPanels.sidebar;
4422
+ if (!panel) { return; }
4423
+
4424
+ panel.style.display = 'none';
4425
+ settingsPanels['sidebar-toggle'].style.display = 'none';
4426
+ dom.removeClass(document.documentElement, 'ge-sidebar-open');
4427
+ }
4428
+
4429
+ function setSidebarTitle(text) {
4430
+ var panel = settingsPanels.sidebar;
4431
+ dom.one(panel, '.ge-settings-title').textContent = text;
4432
+ panel.setAttribute('aria-label', text);
4433
+ }
4434
+
4435
+ function unshowDetails() {
4436
+ var shown = sidebarShown;
4437
+ sidebarShown = null;
4438
+ if (shown) { returnPanel(shown); }
4439
+ }
4440
+
4441
+ /** Whatever the sidebar holds out of it: the panel to its drawer, the empty content out of the body, not destroyed. */
4442
+ function unshowSidebar() {
4443
+ unshowDetails();
4444
+
4445
+ if (sidebarEmpty) {
4446
+ sidebarEmpty.body.remove();
4447
+ sidebarEmpty = null;
4448
+ }
4449
+ }
4450
+
4451
+ /** What the sidebar shows is what is selected; nothing to do when it already does. */
4452
+ function renderSidebar() {
4453
+ var panel = settingsPanels.sidebar;
4454
+ if (!panel || !isEditing()) { return; }
4455
+
4456
+ var details = selected ? detailsOf(selected) : null;
4457
+ if (details ? sidebarShown && sidebarShown.details === details : !sidebarShown && sidebarEmpty) { return; }
4458
+
4459
+ unshowSidebar();
4460
+ var holder = dom.one(panel, '.ge-settings-body');
4461
+
4462
+ if (!details) {
4463
+ var content = emptyContent();
4464
+ var body = content ? content.body : dom.element('p', { 'class': 'ge-sidebar-empty' }, t('panel.sidebar_empty'));
4465
+
4466
+ setSidebarTitle(content ? content.title : t('panel.sidebar_title'));
4467
+ holder.appendChild(body);
4468
+ sidebarEmpty = { body: body };
4469
+ return;
4470
+ }
4471
+
4472
+ sidebarShown = { node: selected, details: details, home: details.parentElement, next: details.nextElementSibling };
4473
+ setSidebarTitle(t('panel.title', { kind: kindLabel(selected) }));
4474
+ holder.appendChild(details);
4475
+ dom.show(details);
4476
+ dom.addClass(selected, 'ge-settings-target');
4477
+ }
4478
+
4479
+ /**
4480
+ * What the sidebar shows with nothing selected, other than its
4481
+ * message: the host's sidebar.empty, else the first plugin's
4482
+ * sidebarEmpty, as { title, body }. Null for the message.
4483
+ */
4484
+ function emptyContent() {
4485
+ var fromHost = settings.sidebar.empty;
4486
+ var found = null;
4487
+
4488
+ if (typeof fromHost === 'function') {
4489
+ found = emptyContentOf(fromHost(instance), function(result) { return result.title; });
4490
+ } else if (fromHost !== null && fromHost !== undefined) {
4491
+ warnOnceHere('sidebar.empty', 'sidebar.empty is a function(ge) returning { title, body }, not ' +
4492
+ typeof fromHost + ': ignored');
4493
+ }
4494
+
4495
+ [CONTAINERS, FEATURES, UTILITIES].forEach(function(registry) {
4496
+ Object.keys(registry).forEach(function(name) {
4497
+ var plugin = registry[name];
4498
+ if (found || !plugin.sidebarEmpty) { return; }
4499
+
4500
+ found = emptyContentOf(plugin.sidebarEmpty(pluginHandle), function(result) {
4501
+ return typeof result.titleKey === 'string' && result.titleKey !== '' ? t(result.titleKey) : '';
4502
+ });
4503
+ });
4504
+ });
4505
+
4506
+ return found;
4507
+ }
4508
+
4509
+ /** An answer of sidebar.empty or sidebarEmpty as { title, body }, or null when it has no body to show. */
4510
+ function emptyContentOf(result, titleOf) {
4511
+ if (!result || !result.body || result.body.nodeType !== 1) { return null; }
4512
+
4513
+ var title = titleOf(result);
4514
+ return { title: typeof title === 'string' && title !== '' ? title : t('panel.sidebar_title'), body: result.body };
4515
+ }
4516
+
4517
+ /**
4518
+ * The node at `target` whose settings the sidebar shows: the
4519
+ * innermost one, its drawer included. A text's drawer is beside it
4520
+ * rather than in it.
4521
+ */
4522
+ function settingsNodeAt(target) {
4523
+ var drawer = dom.closest(target, '.ge-tools-drawer', canvas);
4524
+
4525
+ if (drawer) {
4526
+ var home = drawer.parentElement;
4527
+ var owner = detailsFor.has(home) ? home : dom.children(home).filter(function(each) {
4528
+ return detailsFor.has(each);
4529
+ })[0];
4530
+ if (owner) { return owner; }
4531
+ }
4532
+
4533
+ for (var node = target; node && node !== canvas; node = node.parentElement) {
4534
+ if (detailsFor.has(node)) { return node; }
4535
+ }
4536
+
4537
+ return null;
4538
+ }
4539
+
4540
+ /** The one way the selection changes, told only when it does. */
4541
+ function changeSelection(node) {
4542
+ if (node === selected) { return; }
4543
+
4544
+ var from = selected;
4545
+ selected = node;
4546
+ renderSidebar();
4547
+
4548
+ emit('selection-change', { canvas: canvas, node: node, from: from });
4549
+ }
4550
+
4242
4551
  /**
4243
4552
  * Under the gear, or over it when there is more room there, inside
4244
4553
  * the window, and no taller than the room it has: past that its body
@@ -5881,6 +6190,23 @@ function build(instance, baseElem, optionsOrMethod) {
5881
6190
  changeTarget(node);
5882
6191
  }
5883
6192
 
6193
+ function apiSetSelected(value) {
6194
+ if (panelMode() !== 'sidebar') {
6195
+ warnOnceHere('sidebar-api', 'setSelected needs settings_panel "sidebar": without it, it does nothing');
6196
+ return;
6197
+ }
6198
+ if (value === null || value === undefined) { changeSelection(null); return; }
6199
+
6200
+ var node = nodeFrom(value);
6201
+ if (!node || node === canvas || !canvas.contains(node) || !detailsFor.has(node)) {
6202
+ var named = typeof value === 'string' ? value : (node ? '<' + node.tagName.toLowerCase() + '>' : String(value));
6203
+ warn('setSelected: ' + named + ' is not a node of the canvas with settings');
6204
+ return;
6205
+ }
6206
+
6207
+ changeSelection(node);
6208
+ }
6209
+
5884
6210
  var own = {
5885
6211
  getHtml: getHtml,
5886
6212
  getPlainHtml: getPlainHtml,
@@ -5914,6 +6240,8 @@ function build(instance, baseElem, optionsOrMethod) {
5914
6240
  setUtility: setUtility,
5915
6241
  getActiveTarget: function() { return settings.active_target ? activeTarget : null; },
5916
6242
  setActiveTarget: apiSetActiveTarget,
6243
+ getSelected: function() { return panelMode() === 'sidebar' ? selected : null; },
6244
+ setSelected: apiSetSelected,
5917
6245
  };
5918
6246
 
5919
6247
  Object.keys(own).forEach(function(name) {
@@ -6071,6 +6399,10 @@ GridEditor.locales = {
6071
6399
  'panel.title': '{kind} settings',
6072
6400
  'panel.close': 'Close',
6073
6401
  'panel.done': 'Done',
6402
+ 'panel.sidebar_title': 'Settings',
6403
+ 'panel.sidebar_empty': 'Click an element to see its settings',
6404
+ 'panel.collapse': 'Hide settings',
6405
+ 'panel.expand': 'Show settings',
6074
6406
  'panel.id': 'Id',
6075
6407
  'panel.classes': 'Classes',
6076
6408
  'panel.section_general': 'Id and classes',
@@ -59,6 +59,10 @@ GridEditor.locales.es = {
59
59
  'panel.title': 'Ajustes: {kind}',
60
60
  'panel.close': 'Cerrar',
61
61
  'panel.done': 'Hecho',
62
+ 'panel.sidebar_title': 'Ajustes',
63
+ 'panel.sidebar_empty': 'Haz clic en un elemento para ver sus ajustes',
64
+ 'panel.collapse': 'Ocultar ajustes',
65
+ 'panel.expand': 'Mostrar ajustes',
62
66
  'panel.id': 'Id',
63
67
  'panel.classes': 'Clases',
64
68
  'panel.section_general': 'Id y clases',
@@ -585,6 +585,65 @@
585
585
  --bs-offcanvas-height: 60vh;
586
586
  }
587
587
 
588
+ /* The sidebar: always there while editing, with the page making room for it
589
+ (html.ge-sidebar-open) where the window is wide enough. Narrower than
590
+ Bootstrap's sm it covers the page instead. */
591
+ .ge-settings-sidebar {
592
+ --bs-offcanvas-width: 340px;
593
+
594
+ .ge-sidebar-empty {
595
+ margin: 0;
596
+ color: var(--bs-secondary-color, #6c757d);
597
+ }
598
+ }
599
+
600
+ html.ge-sidebar-open {
601
+ padding-right: 340px;
602
+ }
603
+
604
+ /* On the panel's left edge, centred; folded away, on the window's right edge.
605
+ Its own element, so the panel can slide out from under it. */
606
+ .ge-sidebar-toggle {
607
+ position: fixed;
608
+ top: 50%;
609
+ right: 340px;
610
+ z-index: 1045;
611
+ display: flex;
612
+ align-items: center;
613
+ justify-content: center;
614
+ width: 1.5rem;
615
+ height: 3rem;
616
+ padding: 0;
617
+ transform: translateY(-50%);
618
+ border: 1px solid var(--bs-border-color, #dee2e6);
619
+ border-right: 0;
620
+ border-radius: 0.375rem 0 0 0.375rem;
621
+ background: var(--bs-body-bg, #fff);
622
+ color: var(--bs-body-color, #212529);
623
+ transition: right 0.3s ease-in-out;
624
+
625
+ &[aria-expanded="false"] {
626
+ right: 0;
627
+ }
628
+ }
629
+
630
+ .ge-sidebar-toggle.ge-under-dialog {
631
+ z-index: 1040;
632
+ }
633
+
634
+ @media (max-width: 575.98px) {
635
+ .ge-settings-sidebar {
636
+ --bs-offcanvas-width: 100vw;
637
+ }
638
+
639
+ .ge-sidebar-toggle[aria-expanded="true"] {
640
+ right: calc(100vw - 1.5rem);
641
+ border: 1px solid var(--bs-border-color, #dee2e6);
642
+ border-left: 0;
643
+ border-radius: 0 0.375rem 0.375rem 0;
644
+ }
645
+ }
646
+
588
647
  .ge-settings-popover {
589
648
  /* Popper would position it; without Popper that is the editor's job */
590
649
  position: absolute;