@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.
- package/CHANGELOG.md +24 -0
- package/README.md +18 -0
- package/dist/grideditor.bundle.min.js +3 -3
- package/dist/grideditor.css +51 -0
- package/dist/grideditor.d.ts +28 -1
- package/dist/grideditor.esm.js +381 -20
- package/dist/grideditor.jquery.js +3 -1
- package/dist/grideditor.jquery.min.js +1 -1
- package/dist/grideditor.jquery.min.js.map +2 -2
- package/dist/grideditor.js +381 -20
- package/dist/grideditor.min.css +1 -1
- package/dist/grideditor.min.css.map +1 -1
- package/dist/grideditor.min.js +2 -2
- package/dist/grideditor.min.js.map +3 -3
- package/dist/locales/grideditor.es.esm.js +4 -0
- package/dist/locales/grideditor.es.js +4 -0
- package/dist/locales/grideditor.es.min.js +1 -1
- package/dist/locales/grideditor.es.min.js.map +2 -2
- package/dist/plugins/grideditor.clipboard.esm.js +2 -95
- package/dist/plugins/grideditor.clipboard.js +2 -95
- package/dist/plugins/grideditor.clipboard.min.js +1 -1
- package/dist/plugins/grideditor.clipboard.min.js.map +3 -3
- package/docs/events.md +6 -0
- package/docs/locale-keys.md +4 -0
- package/docs/plugins.md +16 -1
- package/example/utilities.html +3 -1
- package/package.json +1 -1
- package/src/js/grideditor.jquery.js +2 -0
- package/src/js/grideditor.js +433 -29
- package/src/js/locales/grideditor.es.js +4 -0
- package/src/js/plugins/grideditor.clipboard.js +2 -72
- package/src/less/grideditor.less +59 -0
- package/types/grideditor.d.ts +28 -1
package/src/js/grideditor.js
CHANGED
|
@@ -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 '
|
|
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
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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
|
-
|
|
1076
|
-
//
|
|
1077
|
-
|
|
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
|
|
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' ||
|
|
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
|
-
|
|
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
|
-
|
|
3889
|
-
|
|
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
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
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',
|