@themarioga/grid-editor 8.8.2 → 8.10.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
  /**
@@ -46,6 +48,17 @@ var METHODS = {
46
48
  */
47
49
  var PLACEMENTS = ['appendTo', 'prependTo', 'insertAfter', 'insertBefore'];
48
50
 
51
+ /**
52
+ * The attributes that point at an id, as the id itself or as a #selector.
53
+ * A copy that has to take new ids (see freshNode) takes them here too, so
54
+ * its tabs, its accordion and its popups still point at their own panes.
55
+ */
56
+ var REFERENCES = ['data-bs-target', 'data-bs-parent', 'href', 'aria-controls', 'aria-labelledby',
57
+ 'aria-describedby', 'for', 'data-ge-popup-id', 'data-ge-popup-target'];
58
+
59
+ /** An id grid-editor generated: ge-{type}-{counter}-{random}. */
60
+ var GENERATED_ID = /^ge-([a-z][a-z-]*?)-\d+-[a-z0-9]+$/;
61
+
49
62
  /**
50
63
  * Bootstrap 5's breakpoints, smallest first, which is the order the cascade
51
64
  * runs in: a size written for a tier applies to every wider tier that does not
@@ -154,6 +167,9 @@ var NESTED_SETTINGS = {
154
167
  indent: {
155
168
  tools: true, // The indent tools in the column's drawer
156
169
  },
170
+ sidebar: {
171
+ empty: null, // function(ge) -> { title, body } shown while nothing is selected, or null for the message
172
+ },
157
173
  drag: {
158
174
  delay: 0, // Milliseconds to hold before a drag starts
159
175
  touch_delay: 100, // The same for touch, where 0 eats the page's scrolling
@@ -171,6 +187,13 @@ var editorCounter = 0;
171
187
  /** The editor on each canvas element, so there is never a second one. */
172
188
  var instances = new WeakMap();
173
189
 
190
+ /** The editor whose settings_panel is 'sidebar': a page has one sidebar, so one editor at most. */
191
+ var sidebarHolder = null;
192
+
193
+ /** The editor's own controls outside the canvas: a click on one is not a click on the page. */
194
+ var EDITOR_CONTROLS = '.ge-mainControls, .ge-settings-panel, .ge-sidebar-toggle, .ge-dialog, .ge-confirm, ' +
195
+ '.ge-size-picker, .modal, .modal-backdrop';
196
+
174
197
  /**
175
198
  * Translate one key.
176
199
  *
@@ -408,7 +431,8 @@ function build(instance, baseElem, optionsOrMethod) {
408
431
  'locale_strings' : {}, // Overrides for individual keys
409
432
  'callbacks' : {}, // before_*/after_* functions, the events by another route
410
433
  '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'
434
+ 'settings_panel' : 'offcanvas', // Where a node's settings open: 'offcanvas', 'popover', 'modal', 'inline' or 'sidebar'
435
+ 'sidebar' : NESTED_SETTINGS.sidebar, // The 'sidebar' panel: what it shows while nothing is selected
412
436
  'drag' : NESTED_SETTINGS.drag // How a drag behaves, whatever drives it
413
437
  }, optionsOrMethod);
414
438
 
@@ -444,6 +468,19 @@ function build(instance, baseElem, optionsOrMethod) {
444
468
  var sortables = []; // Every list made sortable, so deinit destroys exactly those
445
469
  var instanceId = ++editorCounter; // Scopes the sortable groups to this editor
446
470
  var destroyed = false;
471
+ var selected = null; // The node whose settings the sidebar shows, with settings_panel 'sidebar'
472
+ var sidebarCollapsed = null; // Folded away, once the sidebar has been shown: the window's width decides the first time
473
+ var pluginHandle = null; // What the plugins were handed, for the hooks that call them back
474
+
475
+ if (settings.settings_panel === 'sidebar') {
476
+ if (sidebarHolder) {
477
+ warnOnceHere('sidebar', 'settings_panel "sidebar" is in use by another editor on the page, which has one ' +
478
+ 'sidebar: using offcanvas');
479
+ settings.settings_panel = 'offcanvas';
480
+ } else {
481
+ sidebarHolder = instance;
482
+ }
483
+ }
447
484
 
448
485
  // What the editor remembers about the nodes it edits, which used to
449
486
  // be jQuery data: a node's settings panel, a drag or a resize in
@@ -1055,10 +1092,9 @@ function build(instance, baseElem, optionsOrMethod) {
1055
1092
  if (dom.hasClass(canvas, 'ge-editing')) { e.preventDefault(); }
1056
1093
  }, signal);
1057
1094
 
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.
1095
+ var sidebarMode = panelMode() === 'sidebar';
1096
+
1097
+ if (settings.active_target || sidebarMode) {
1062
1098
  // The click a drag ends in - a resize, a move - is not one:
1063
1099
  // it comes where the pointer let go, not where it pressed
1064
1100
  var pressed = null;
@@ -1070,29 +1106,68 @@ function build(instance, baseElem, optionsOrMethod) {
1070
1106
  pressed = null;
1071
1107
  if (!dom.hasClass(canvas, 'ge-editing')) { return; }
1072
1108
  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
1109
 
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);
1110
+ if (settings.active_target) { clickTarget(e); }
1111
+ // A tool that took its node off the canvas says nothing of where the click was
1112
+ if (sidebarMode && canvas.contains(e.target)) { changeSelection(settingsNodeAt(e.target)); }
1085
1113
  }, signal);
1086
1114
 
1087
1115
  // 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
1116
+ // text's while one has the focus; after those, it is the
1117
+ // selection's, and then the target's
1089
1118
  document.addEventListener('keydown', function(e) {
1090
- if (e.key !== 'Escape' || !activeTarget || openSettingsState || dialogState) { return; }
1119
+ if (e.key !== 'Escape' || openSettingsState || dialogState) { return; }
1120
+ if (confirmDialog && dom.hasClass(confirmDialog, 'show')) { return; }
1091
1121
  var focus = document.activeElement;
1092
1122
  if (focus && (dom.is(focus, 'input, textarea, select') || focus.isContentEditable)) { return; }
1093
- changeTarget(null);
1123
+
1124
+ if (selected && isEditing()) {
1125
+ changeSelection(null);
1126
+ } else if (activeTarget) {
1127
+ changeTarget(null);
1128
+ }
1094
1129
  }, signal);
1095
1130
  }
1131
+
1132
+ if (sidebarMode) {
1133
+ // The focus coming into a node selects it, as a click does
1134
+ canvas.addEventListener('focusin', function(e) {
1135
+ var node = isEditing() ? settingsNodeAt(e.target) : null;
1136
+ if (node) { changeSelection(node); }
1137
+ }, signal);
1138
+
1139
+ // A click on the page, away from the canvas and the editor's own
1140
+ // controls, puts the selection away - not while a text is open,
1141
+ // whose editor has controls of its own out there
1142
+ document.addEventListener('click', function(e) {
1143
+ var target = e.target;
1144
+ if (!selected || !isEditing() || !target || target.nodeType !== 1) { return; }
1145
+ if (!dom.attached(target) || canvas.contains(target)) { return; }
1146
+ if (dom.closest(target, EDITOR_CONTROLS) || canvas.querySelector('.ge-rte-active')) { return; }
1147
+
1148
+ changeSelection(null);
1149
+ }, signal);
1150
+
1151
+ window.addEventListener('resize', reserveSidebarRoom, signal);
1152
+ }
1153
+
1154
+ /* Whatever the click was for - a text, a tool - it also says
1155
+ where the toolbar adds next. A click on no column or region
1156
+ says nowhere, and the toolbar adds to the canvas again. */
1157
+ function clickTarget(e) {
1158
+ var region = dom.closest(e.target, targetSelector());
1159
+
1160
+ // The target's own background, or its drawer's, clicked
1161
+ // again takes it back. What is in it - a text, a tool, a
1162
+ // nested column - keeps it, or makes that the target
1163
+ if (region && region === activeTarget &&
1164
+ (e.target === region || e.target === dom.child(region, '.ge-tools-drawer'))) {
1165
+ changeTarget(null);
1166
+ return;
1167
+ }
1168
+
1169
+ changeTarget(region && region !== canvas && canvas.contains(region) ? region : null);
1170
+ }
1096
1171
  }
1097
1172
 
1098
1173
  /**
@@ -2074,6 +2149,7 @@ function build(instance, baseElem, optionsOrMethod) {
2074
2149
  deinit();
2075
2150
  // The canvas is made again from the html when it comes back
2076
2151
  changeTarget(null);
2152
+ changeSelection(null);
2077
2153
  htmlTextArea.style.height = (0.8 * document.documentElement.clientHeight) + 'px';
2078
2154
  htmlTextArea.value = canvas.innerHTML;
2079
2155
  dom.show(htmlTextArea);
@@ -2135,6 +2211,10 @@ function build(instance, baseElem, optionsOrMethod) {
2135
2211
  if (openSettingsState && !dom.attached(openSettingsState.node)) {
2136
2212
  closeSettings();
2137
2213
  }
2214
+ // The same for the selected node, which the sidebar stops showing
2215
+ if (selected && !canvas.contains(selected)) {
2216
+ changeSelection(null);
2217
+ }
2138
2218
  // And a dialog showing part of a panel that is gone
2139
2219
  if (dialogState && dialogState.home && !dom.attached(dialogState.home)) {
2140
2220
  closeDialog();
@@ -2163,11 +2243,14 @@ function build(instance, baseElem, optionsOrMethod) {
2163
2243
  } else if (activeTarget) {
2164
2244
  dom.addClass(activeTarget, 'ge-active-target');
2165
2245
  }
2246
+
2247
+ showSidebar();
2166
2248
  }
2167
2249
 
2168
2250
  function deinit() {
2169
2251
  // Its panel goes home to its drawer first, and both go together
2170
2252
  closeSettings();
2253
+ hideSidebar();
2171
2254
  dom.removeClass(canvas, 'ge-editing ge-drag-drawer ge-dropping');
2172
2255
  // The mark goes, the target stays: getHtml comes straight back
2173
2256
  if (activeTarget) { dom.removeClass(activeTarget, 'ge-active-target'); }
@@ -2376,6 +2459,8 @@ function build(instance, baseElem, optionsOrMethod) {
2376
2459
  if (sourceOpen) { closeSource(); }
2377
2460
  deinit();
2378
2461
  activeTarget = null;
2462
+ selected = null;
2463
+ if (sidebarHolder === instance) { sidebarHolder = null; }
2379
2464
  removeConfirmModal();
2380
2465
  removeSettingsPanels();
2381
2466
  removeDialog();
@@ -2396,7 +2481,7 @@ function build(instance, baseElem, optionsOrMethod) {
2396
2481
  * something it can see.
2397
2482
  */
2398
2483
  function loadPlugins() {
2399
- var api = pluginApi();
2484
+ var api = pluginHandle = pluginApi();
2400
2485
 
2401
2486
  registerFamily('grid', {}, widthFamily());
2402
2487
  if (settings.row_cols !== false) { registerFamily('grid', {}, rowColsFamily()); }
@@ -2466,6 +2551,7 @@ function build(instance, baseElem, optionsOrMethod) {
2466
2551
  t: t,
2467
2552
  warn: warn,
2468
2553
  containerId: containerId,
2554
+ freshNode: freshNode,
2469
2555
  defaultRegion: defaultRegion,
2470
2556
  createTool: createTool,
2471
2557
  createMoveTool: createMoveTool,
@@ -3192,6 +3278,66 @@ function build(instance, baseElem, optionsOrMethod) {
3192
3278
  Math.random().toString(36).slice(2, 6);
3193
3279
  }
3194
3280
 
3281
+ /**
3282
+ * Markup as a node, with new ids where the page already has them: two
3283
+ * copies of a tab strip pointing at one set of panes is a tab strip that
3284
+ * opens the other copy's tabs. An id the page does not have is kept, so
3285
+ * markup that goes into another page changes nothing. The clipboard
3286
+ * pastes through it, and a host inserting saved markup should too.
3287
+ *
3288
+ * Parsed the way innerHTML parses, so a <script> in the markup is markup,
3289
+ * placed like the rest, and does not run in the editor.
3290
+ */
3291
+ function freshNode(html) {
3292
+ var node = dom.create(html);
3293
+ var renamed = {};
3294
+ var taken = function(id) {
3295
+ return !!document.getElementById(id) || Object.keys(renamed).some(function(old) {
3296
+ return renamed[old] === id;
3297
+ });
3298
+ };
3299
+
3300
+ dom.selfAndAll(node, '[id]').forEach(function(element) {
3301
+ var id = element.id;
3302
+ if (!document.getElementById(id)) { return; }
3303
+
3304
+ var generated = GENERATED_ID.exec(id);
3305
+ var next;
3306
+
3307
+ if (generated) {
3308
+ do { next = containerId(generated[1]); } while (taken(next));
3309
+ } else {
3310
+ var n = 2;
3311
+ while (taken(id + '-' + n)) { n++; }
3312
+ next = id + '-' + n;
3313
+ }
3314
+
3315
+ renamed[id] = next;
3316
+ element.id = next;
3317
+ });
3318
+
3319
+ if (!Object.keys(renamed).length) { return node; }
3320
+
3321
+ dom.selfAndAll(node, '*').forEach(function(element) {
3322
+ REFERENCES.forEach(function(name) {
3323
+ var value = element.getAttribute(name);
3324
+ if (value === null) { return; }
3325
+ if (name === 'href' && value.charAt(0) !== '#') { return; }
3326
+
3327
+ var rewritten = value.split(/(\s+)/).map(function(token) {
3328
+ if (renamed[token]) { return renamed[token]; }
3329
+ if (token.charAt(0) === '#' && renamed[token.slice(1)]) { return '#' + renamed[token.slice(1)]; }
3330
+
3331
+ return token;
3332
+ }).join('');
3333
+
3334
+ if (rewritten !== value) { element.setAttribute(name, rewritten); }
3335
+ });
3336
+ });
3337
+
3338
+ return node;
3339
+ }
3340
+
3195
3341
  /** A pane's starting content: one full width column, ready to edit. */
3196
3342
  function defaultRegion() {
3197
3343
  var row = createRow();
@@ -3885,9 +4031,13 @@ function build(instance, baseElem, optionsOrMethod) {
3885
4031
  var details = createDetails(node, presets || []);
3886
4032
  detailsFor.set(node, details);
3887
4033
 
3888
- createTool(drawer, t('tool.settings'), 'ge-settings', 'bi bi-gear-fill', function() {
3889
- toggleSettings(node, details, this);
3890
- });
4034
+ // The sidebar shows the panel of the node that is selected, so
4035
+ // there is no gear to open it with
4036
+ if (panelMode() !== 'sidebar') {
4037
+ createTool(drawer, t('tool.settings'), 'ge-settings', 'bi bi-gear-fill', function() {
4038
+ toggleSettings(node, details, this);
4039
+ });
4040
+ }
3891
4041
 
3892
4042
  // Beside the gear, because every node that has one is a node a
3893
4043
  // utility may apply to, whichever plugin built its drawer. A
@@ -3999,7 +4149,7 @@ function build(instance, baseElem, optionsOrMethod) {
3999
4149
 
4000
4150
  var holder = dom.element('div', { 'class': 'ge-panel-section', 'data-ge-plugin': name });
4001
4151
 
4002
- if (mode === 'offcanvas' || mode === 'modal') {
4152
+ if (mode === 'offcanvas' || mode === 'modal' || mode === 'sidebar') {
4003
4153
  holder.appendChild(sectionTitle(t(section.labelKey)));
4004
4154
  holder.appendChild(section.body);
4005
4155
  } else {
@@ -4039,7 +4189,7 @@ function build(instance, baseElem, optionsOrMethod) {
4039
4189
  * not at all. The modal is Bootstrap's own when Bootstrap is there.
4040
4190
  * -------------------------------------------------------------- */
4041
4191
 
4042
- var PANEL_MODES = ['offcanvas', 'popover', 'modal', 'inline'];
4192
+ var PANEL_MODES = ['offcanvas', 'popover', 'modal', 'inline', 'sidebar'];
4043
4193
  var settingsPanels = {}; // One of each, built on first use, outside the canvas
4044
4194
  var openSettingsState = null; // What is open: { mode, node, details, home, next, gear }
4045
4195
  var modalState = new WeakMap(); // modal panel -> { watched, wanted, busy, backdrop, dismissed }
@@ -4059,9 +4209,12 @@ function build(instance, baseElem, optionsOrMethod) {
4059
4209
 
4060
4210
  /** Where settings fields are found: the canvas, and the panel open outside it. */
4061
4211
  function settingsScope() {
4062
- return openSettingsState && openSettingsState.mode !== 'inline'
4063
- ? [canvas, openSettingsState.details]
4064
- : [canvas];
4212
+ var scope = [canvas];
4213
+
4214
+ if (openSettingsState && openSettingsState.mode !== 'inline') { scope.push(openSettingsState.details); }
4215
+ if (sidebarShown) { scope.push(sidebarShown.details); }
4216
+
4217
+ return scope;
4065
4218
  }
4066
4219
 
4067
4220
  /** The gear: its node's panel, opened or closed; another node's closes first. */
@@ -4155,6 +4308,11 @@ function build(instance, baseElem, optionsOrMethod) {
4155
4308
  hideModal(panel);
4156
4309
  }
4157
4310
 
4311
+ returnPanel(open);
4312
+ }
4313
+
4314
+ /** A node's panel back where it was in its drawer, and the node no longer marked. */
4315
+ function returnPanel(open) {
4158
4316
  dom.removeClass(open.node, 'ge-settings-target');
4159
4317
  open.details.style.removeProperty('display');
4160
4318
  dom.dropEmptyStyle(open.details);
@@ -4199,6 +4357,15 @@ function build(instance, baseElem, optionsOrMethod) {
4199
4357
  '</div>');
4200
4358
  dom.one(panel, '.popover-header').appendChild(closeButton());
4201
4359
  dom.hide(panel);
4360
+ } else if (mode === 'sidebar') {
4361
+ panel = dom.create('<div class="offcanvas offcanvas-end ge-settings-panel ge-settings-sidebar" tabindex="-1" role="complementary">' +
4362
+ '<div class="offcanvas-header"><h5 class="offcanvas-title ge-settings-title"></h5></div>' +
4363
+ '<div class="offcanvas-body ge-settings-body"></div>' +
4364
+ '</div>');
4365
+ panel.id = 'ge-settings-sidebar-' + instanceId;
4366
+ dom.one(panel, '.offcanvas-header').appendChild(closeButton());
4367
+ // Beside the panel rather than in it: it stays on the window's edge when the panel is folded away
4368
+ settingsPanels['sidebar-toggle'] = document.body.appendChild(sidebarToggle(panel));
4202
4369
  } else {
4203
4370
  panel = dom.create('<div class="modal fade ge-settings-panel ge-settings-modal" tabindex="-1" role="dialog" aria-hidden="true">' +
4204
4371
  '<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">' +
@@ -4218,7 +4385,7 @@ function build(instance, baseElem, optionsOrMethod) {
4218
4385
 
4219
4386
  dom.delegate(panel, 'click', '.ge-settings-close', function(e) {
4220
4387
  e.preventDefault();
4221
- closeSettings();
4388
+ if (mode === 'sidebar') { changeSelection(null); } else { closeSettings(); }
4222
4389
  });
4223
4390
 
4224
4391
  settingsPanels[mode] = document.body.appendChild(panel);
@@ -4227,6 +4394,8 @@ function build(instance, baseElem, optionsOrMethod) {
4227
4394
 
4228
4395
  function removeSettingsPanels() {
4229
4396
  closeSettings();
4397
+ unshowSidebar();
4398
+ dom.removeClass(document.documentElement, 'ge-sidebar-open');
4230
4399
 
4231
4400
  Object.keys(settingsPanels).forEach(function(mode) {
4232
4401
  if (mode === 'modal') {
@@ -4239,6 +4408,218 @@ function build(instance, baseElem, optionsOrMethod) {
4239
4408
  settingsPanels = {};
4240
4409
  }
4241
4410
 
4411
+ /* --------------------------------------------------------------
4412
+ * settings_panel: 'sidebar'. An offcanvas that stays: there while
4413
+ * the canvas is edited, making room for itself on the page, with the
4414
+ * settings of the node that is selected in it - or, with none, a
4415
+ * message, or what the host or a plugin put there instead.
4416
+ *
4417
+ * The panels are the same ones: the selected node's moves into the
4418
+ * sidebar and goes back to its drawer when another is selected, so
4419
+ * ge.detailsOf(node) finds it either way. Folded away, the panel
4420
+ * slides out and its button stays on the window's edge.
4421
+ * -------------------------------------------------------------- */
4422
+
4423
+ var SIDEBAR_MIN_WIDTH = 576; // Bootstrap's sm: narrower than that, the sidebar covers the page instead of making room
4424
+ var sidebarShown = null; // The selected node's panel while it is in the sidebar: { node, details, home, next }
4425
+ var sidebarEmpty = null; // What the sidebar shows while nothing is selected: { body }
4426
+
4427
+ function isEditing() {
4428
+ return dom.hasClass(canvas, 'ge-editing');
4429
+ }
4430
+
4431
+ function sidebarToggle(panel) {
4432
+ var button = dom.element('button', { type: 'button', 'class': 'ge-sidebar-toggle', 'aria-controls': panel.id });
4433
+ button.appendChild(dom.element('i'));
4434
+ button.addEventListener('click', function() { foldSidebar(!sidebarCollapsed); });
4435
+ return button;
4436
+ }
4437
+
4438
+ function foldSidebar(collapsed) {
4439
+ sidebarCollapsed = collapsed;
4440
+ applySidebar();
4441
+ }
4442
+
4443
+ /** The panel and its button as `sidebarCollapsed` says. */
4444
+ function applySidebar() {
4445
+ var panel = settingsPanels.sidebar;
4446
+ var toggle = settingsPanels['sidebar-toggle'];
4447
+ if (!panel) { return; }
4448
+
4449
+ var open = !sidebarCollapsed;
4450
+
4451
+ if (open && !dom.hasClass(panel, 'show')) {
4452
+ dom.removeClass(panel, 'hiding');
4453
+ panel.getBoundingClientRect(); // So the slide in is a transition, not a jump
4454
+ dom.addClass(panel, 'show');
4455
+ } else if (!open && dom.hasClass(panel, 'show')) {
4456
+ dom.removeClass(panel, 'show');
4457
+ dom.addClass(panel, 'hiding');
4458
+ window.setTimeout(function() { dom.removeClass(panel, 'hiding'); }, 300);
4459
+ }
4460
+
4461
+ var label = open ? t('panel.collapse') : t('panel.expand');
4462
+ toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
4463
+ toggle.setAttribute('aria-label', label);
4464
+ toggle.setAttribute('title', label);
4465
+ dom.one(toggle, 'i').className = 'bi ' + (open ? 'bi-chevron-right' : 'bi-chevron-left');
4466
+
4467
+ reserveSidebarRoom();
4468
+ }
4469
+
4470
+ /** The page makes room for the sidebar only while it is edited, unfolded, and the window is wide enough to have room. */
4471
+ function reserveSidebarRoom() {
4472
+ dom.toggleClass(document.documentElement, 'ge-sidebar-open',
4473
+ !!settingsPanels.sidebar && isEditing() && !sidebarCollapsed && window.innerWidth >= SIDEBAR_MIN_WIDTH);
4474
+ }
4475
+
4476
+ /** Editing starts, or starts again: the sidebar is there, showing what is selected. */
4477
+ function showSidebar() {
4478
+ if (panelMode() !== 'sidebar') { return; }
4479
+
4480
+ var panel = settingsPanel('sidebar');
4481
+ if (sidebarCollapsed === null) { sidebarCollapsed = window.innerWidth < SIDEBAR_MIN_WIDTH; }
4482
+
4483
+ panel.style.removeProperty('display');
4484
+ settingsPanels['sidebar-toggle'].style.removeProperty('display');
4485
+ renderSidebar();
4486
+ applySidebar();
4487
+ }
4488
+
4489
+ /** Editing stops: out of sight, with the selected node's panel back in its drawer. The selection stays. */
4490
+ function hideSidebar() {
4491
+ unshowDetails();
4492
+
4493
+ var panel = settingsPanels.sidebar;
4494
+ if (!panel) { return; }
4495
+
4496
+ panel.style.display = 'none';
4497
+ settingsPanels['sidebar-toggle'].style.display = 'none';
4498
+ dom.removeClass(document.documentElement, 'ge-sidebar-open');
4499
+ }
4500
+
4501
+ function setSidebarTitle(text) {
4502
+ var panel = settingsPanels.sidebar;
4503
+ dom.one(panel, '.ge-settings-title').textContent = text;
4504
+ panel.setAttribute('aria-label', text);
4505
+ }
4506
+
4507
+ function unshowDetails() {
4508
+ var shown = sidebarShown;
4509
+ sidebarShown = null;
4510
+ if (shown) { returnPanel(shown); }
4511
+ }
4512
+
4513
+ /** Whatever the sidebar holds out of it: the panel to its drawer, the empty content out of the body, not destroyed. */
4514
+ function unshowSidebar() {
4515
+ unshowDetails();
4516
+
4517
+ if (sidebarEmpty) {
4518
+ sidebarEmpty.body.remove();
4519
+ sidebarEmpty = null;
4520
+ }
4521
+ }
4522
+
4523
+ /** What the sidebar shows is what is selected; nothing to do when it already does. */
4524
+ function renderSidebar() {
4525
+ var panel = settingsPanels.sidebar;
4526
+ if (!panel || !isEditing()) { return; }
4527
+
4528
+ var details = selected ? detailsOf(selected) : null;
4529
+ if (details ? sidebarShown && sidebarShown.details === details : !sidebarShown && sidebarEmpty) { return; }
4530
+
4531
+ unshowSidebar();
4532
+ var holder = dom.one(panel, '.ge-settings-body');
4533
+
4534
+ if (!details) {
4535
+ var content = emptyContent();
4536
+ var body = content ? content.body : dom.element('p', { 'class': 'ge-sidebar-empty' }, t('panel.sidebar_empty'));
4537
+
4538
+ setSidebarTitle(content ? content.title : t('panel.sidebar_title'));
4539
+ holder.appendChild(body);
4540
+ sidebarEmpty = { body: body };
4541
+ return;
4542
+ }
4543
+
4544
+ sidebarShown = { node: selected, details: details, home: details.parentElement, next: details.nextElementSibling };
4545
+ setSidebarTitle(t('panel.title', { kind: kindLabel(selected) }));
4546
+ holder.appendChild(details);
4547
+ dom.show(details);
4548
+ dom.addClass(selected, 'ge-settings-target');
4549
+ }
4550
+
4551
+ /**
4552
+ * What the sidebar shows with nothing selected, other than its
4553
+ * message: the host's sidebar.empty, else the first plugin's
4554
+ * sidebarEmpty, as { title, body }. Null for the message.
4555
+ */
4556
+ function emptyContent() {
4557
+ var fromHost = settings.sidebar.empty;
4558
+ var found = null;
4559
+
4560
+ if (typeof fromHost === 'function') {
4561
+ found = emptyContentOf(fromHost(instance), function(result) { return result.title; });
4562
+ } else if (fromHost !== null && fromHost !== undefined) {
4563
+ warnOnceHere('sidebar.empty', 'sidebar.empty is a function(ge) returning { title, body }, not ' +
4564
+ typeof fromHost + ': ignored');
4565
+ }
4566
+
4567
+ [CONTAINERS, FEATURES, UTILITIES].forEach(function(registry) {
4568
+ Object.keys(registry).forEach(function(name) {
4569
+ var plugin = registry[name];
4570
+ if (found || !plugin.sidebarEmpty) { return; }
4571
+
4572
+ found = emptyContentOf(plugin.sidebarEmpty(pluginHandle), function(result) {
4573
+ return typeof result.titleKey === 'string' && result.titleKey !== '' ? t(result.titleKey) : '';
4574
+ });
4575
+ });
4576
+ });
4577
+
4578
+ return found;
4579
+ }
4580
+
4581
+ /** An answer of sidebar.empty or sidebarEmpty as { title, body }, or null when it has no body to show. */
4582
+ function emptyContentOf(result, titleOf) {
4583
+ if (!result || !result.body || result.body.nodeType !== 1) { return null; }
4584
+
4585
+ var title = titleOf(result);
4586
+ return { title: typeof title === 'string' && title !== '' ? title : t('panel.sidebar_title'), body: result.body };
4587
+ }
4588
+
4589
+ /**
4590
+ * The node at `target` whose settings the sidebar shows: the
4591
+ * innermost one, its drawer included. A text's drawer is beside it
4592
+ * rather than in it.
4593
+ */
4594
+ function settingsNodeAt(target) {
4595
+ var drawer = dom.closest(target, '.ge-tools-drawer', canvas);
4596
+
4597
+ if (drawer) {
4598
+ var home = drawer.parentElement;
4599
+ var owner = detailsFor.has(home) ? home : dom.children(home).filter(function(each) {
4600
+ return detailsFor.has(each);
4601
+ })[0];
4602
+ if (owner) { return owner; }
4603
+ }
4604
+
4605
+ for (var node = target; node && node !== canvas; node = node.parentElement) {
4606
+ if (detailsFor.has(node)) { return node; }
4607
+ }
4608
+
4609
+ return null;
4610
+ }
4611
+
4612
+ /** The one way the selection changes, told only when it does. */
4613
+ function changeSelection(node) {
4614
+ if (node === selected) { return; }
4615
+
4616
+ var from = selected;
4617
+ selected = node;
4618
+ renderSidebar();
4619
+
4620
+ emit('selection-change', { canvas: canvas, node: node, from: from });
4621
+ }
4622
+
4242
4623
  /**
4243
4624
  * Under the gear, or over it when there is more room there, inside
4244
4625
  * the window, and no taller than the room it has: past that its body
@@ -5881,6 +6262,23 @@ function build(instance, baseElem, optionsOrMethod) {
5881
6262
  changeTarget(node);
5882
6263
  }
5883
6264
 
6265
+ function apiSetSelected(value) {
6266
+ if (panelMode() !== 'sidebar') {
6267
+ warnOnceHere('sidebar-api', 'setSelected needs settings_panel "sidebar": without it, it does nothing');
6268
+ return;
6269
+ }
6270
+ if (value === null || value === undefined) { changeSelection(null); return; }
6271
+
6272
+ var node = nodeFrom(value);
6273
+ if (!node || node === canvas || !canvas.contains(node) || !detailsFor.has(node)) {
6274
+ var named = typeof value === 'string' ? value : (node ? '<' + node.tagName.toLowerCase() + '>' : String(value));
6275
+ warn('setSelected: ' + named + ' is not a node of the canvas with settings');
6276
+ return;
6277
+ }
6278
+
6279
+ changeSelection(node);
6280
+ }
6281
+
5884
6282
  var own = {
5885
6283
  getHtml: getHtml,
5886
6284
  getPlainHtml: getPlainHtml,
@@ -5914,6 +6312,8 @@ function build(instance, baseElem, optionsOrMethod) {
5914
6312
  setUtility: setUtility,
5915
6313
  getActiveTarget: function() { return settings.active_target ? activeTarget : null; },
5916
6314
  setActiveTarget: apiSetActiveTarget,
6315
+ getSelected: function() { return panelMode() === 'sidebar' ? selected : null; },
6316
+ setSelected: apiSetSelected,
5917
6317
  };
5918
6318
 
5919
6319
  Object.keys(own).forEach(function(name) {
@@ -6071,6 +6471,10 @@ GridEditor.locales = {
6071
6471
  'panel.title': '{kind} settings',
6072
6472
  'panel.close': 'Close',
6073
6473
  'panel.done': 'Done',
6474
+ 'panel.sidebar_title': 'Settings',
6475
+ 'panel.sidebar_empty': 'Click an element to see its settings',
6476
+ 'panel.collapse': 'Hide settings',
6477
+ 'panel.expand': 'Show settings',
6074
6478
  'panel.id': 'Id',
6075
6479
  'panel.classes': 'Classes',
6076
6480
  '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',