@themarioga/grid-editor 3.1.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/AUTO_SAVE.md +30 -0
- package/BUILDING.md +68 -0
- package/CHANGELOG.md +362 -0
- package/LICENSE +23 -0
- package/README.md +521 -0
- package/UPGRADING.md +88 -0
- package/dist/grideditor.css +3875 -0
- package/dist/grideditor.min.css +2 -0
- package/dist/grideditor.min.css.map +1 -0
- package/dist/jquery.grideditor.js +2996 -0
- package/dist/jquery.grideditor.min.js +2 -0
- package/dist/jquery.grideditor.min.js.map +1 -0
- package/dist/locales/grideditor.es.js +74 -0
- package/dist/locales/grideditor.es.min.js +2 -0
- package/dist/locales/grideditor.es.min.js.map +1 -0
- package/dist/plugins/grideditor.accordion.js +185 -0
- package/dist/plugins/grideditor.accordion.min.js +2 -0
- package/dist/plugins/grideditor.accordion.min.js.map +1 -0
- package/dist/plugins/grideditor.elements.js +141 -0
- package/dist/plugins/grideditor.elements.min.js +2 -0
- package/dist/plugins/grideditor.elements.min.js.map +1 -0
- package/dist/plugins/grideditor.popup.js +143 -0
- package/dist/plugins/grideditor.popup.min.js +2 -0
- package/dist/plugins/grideditor.popup.min.js.map +1 -0
- package/dist/plugins/grideditor.tabs.js +140 -0
- package/dist/plugins/grideditor.tabs.min.js +2 -0
- package/dist/plugins/grideditor.tabs.min.js.map +1 -0
- package/docs/events.md +158 -0
- package/docs/locale-keys.md +140 -0
- package/docs/plugins.md +163 -0
- package/example/autosave.html +89 -0
- package/example/basic.html +93 -0
- package/example/breakpoints.html +94 -0
- package/example/ckeditor.html +104 -0
- package/example/containers.html +214 -0
- package/example/elements.html +147 -0
- package/example/index.html +119 -0
- package/example/locale.html +84 -0
- package/example/plugins.html +217 -0
- package/example/summernote.html +106 -0
- package/example/wrap_content.html +44 -0
- package/package.json +66 -0
- package/src/js/jquery.grideditor.ckeditor.js +77 -0
- package/src/js/jquery.grideditor.js +2711 -0
- package/src/js/jquery.grideditor.summernote.js +72 -0
- package/src/js/jquery.grideditor.tinymce.js +135 -0
- package/src/js/locales/grideditor.es.js +74 -0
- package/src/js/plugins/grideditor.accordion.js +185 -0
- package/src/js/plugins/grideditor.elements.js +141 -0
- package/src/js/plugins/grideditor.popup.js +143 -0
- package/src/js/plugins/grideditor.tabs.js +140 -0
- package/src/less/grideditor.less +594 -0
|
@@ -0,0 +1,2996 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid editor plugin.
|
|
3
|
+
*
|
|
4
|
+
* A fork of https://github.com/Friendly-Pixel/grid-editor by Simon Epskamp,
|
|
5
|
+
* maintained at https://github.com/themarioga/grid-editor.
|
|
6
|
+
*/
|
|
7
|
+
(function( $ ){
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Every method the plugin dispatches, and how to dispatch it.
|
|
11
|
+
*
|
|
12
|
+
* `value` marks a method that hands back something other than the jQuery set
|
|
13
|
+
* - html, a breakpoint key, a created node - so it runs against the first
|
|
14
|
+
* element of the set only and does not chain. `noInstance` is the answer for
|
|
15
|
+
* an element that carries no editor; every other method is a no-op returning
|
|
16
|
+
* the set. `unimplemented` registers a method a later phase fills in, so a
|
|
17
|
+
* host that calls it early gets told rather than ignored.
|
|
18
|
+
*/
|
|
19
|
+
var METHODS = {
|
|
20
|
+
getHtml: { value: true, noInstance: function(element) { return element.html(); } },
|
|
21
|
+
init: {},
|
|
22
|
+
deinit: {},
|
|
23
|
+
reset: {},
|
|
24
|
+
destroy: {},
|
|
25
|
+
remove: {},
|
|
26
|
+
changeView: {},
|
|
27
|
+
getView: { value: true },
|
|
28
|
+
createRow: { value: true },
|
|
29
|
+
createColumn: { value: true },
|
|
30
|
+
createElement: { value: true },
|
|
31
|
+
createContainer: { value: true },
|
|
32
|
+
addTab: { value: true },
|
|
33
|
+
addAccordionItem: { value: true },
|
|
34
|
+
setLocale: {},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Where a create* call may put the node it just made. The first one given
|
|
39
|
+
* wins, and giving none leaves the node detached for the host to place.
|
|
40
|
+
*/
|
|
41
|
+
var PLACEMENTS = ['appendTo', 'prependTo', 'insertAfter', 'insertBefore'];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Bootstrap 5's breakpoints, smallest first, which is the order the cascade
|
|
45
|
+
* runs in: a size written for a tier applies to every wider tier that does not
|
|
46
|
+
* override it. Every size and offset class grid-editor reads or writes comes
|
|
47
|
+
* from this table, so adding a tier is a row here and nothing else.
|
|
48
|
+
*/
|
|
49
|
+
var BREAKPOINTS = [
|
|
50
|
+
{ key: 'xs', colPrefix: 'col-', offsetPrefix: 'offset-', min: 0, preview: 400, labelKey: 'view.xs' },
|
|
51
|
+
{ key: 'sm', colPrefix: 'col-sm-', offsetPrefix: 'offset-sm-', min: 576, preview: 576, labelKey: 'view.sm' },
|
|
52
|
+
{ key: 'md', colPrefix: 'col-md-', offsetPrefix: 'offset-md-', min: 768, preview: 768, labelKey: 'view.md' },
|
|
53
|
+
{ key: 'lg', colPrefix: 'col-lg-', offsetPrefix: 'offset-lg-', min: 992, preview: 992, labelKey: 'view.lg' },
|
|
54
|
+
{ key: 'xl', colPrefix: 'col-xl-', offsetPrefix: 'offset-xl-', min: 1200, preview: 1200, labelKey: 'view.xl' },
|
|
55
|
+
{ key: 'xxl', colPrefix: 'col-xxl-', offsetPrefix: 'offset-xxl-', min: 1400, preview: null, labelKey: 'view.xxl' },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The view that edits every tier at once. It is the default, and the one most
|
|
60
|
+
* pages want: a layout that needs no per-device tuning is written once and
|
|
61
|
+
* lands on all six prefixes.
|
|
62
|
+
*/
|
|
63
|
+
var ALL_VIEW = 'all';
|
|
64
|
+
var ALL_VIEW_LABEL_KEY = 'view.all';
|
|
65
|
+
|
|
66
|
+
/** Every view key the dropdown can offer, in the order it offers them. */
|
|
67
|
+
var VIEW_KEYS = [ALL_VIEW].concat(BREAKPOINTS.map(function(tier) { return tier.key; }));
|
|
68
|
+
|
|
69
|
+
/** What the three layout mode indexes of 2.x meant. */
|
|
70
|
+
var LEGACY_VIEW_INDEXES = ['lg', 'sm', 'xs'];
|
|
71
|
+
|
|
72
|
+
var MAX_COL_SIZE = 12;
|
|
73
|
+
var MAX_COL_OFFSET = 11;
|
|
74
|
+
|
|
75
|
+
function breakpoint(key) {
|
|
76
|
+
for (var i = 0; i < BREAKPOINTS.length; i++) {
|
|
77
|
+
if (BREAKPOINTS[i].key === key) { return BREAKPOINTS[i]; }
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The tiers a view writes to: one, or all six in the all view. */
|
|
83
|
+
function tiersFor(view) {
|
|
84
|
+
if (view === ALL_VIEW) { return BREAKPOINTS.slice(); }
|
|
85
|
+
|
|
86
|
+
var tier = breakpoint(view);
|
|
87
|
+
return tier ? [tier] : [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function labelKeyFor(view) {
|
|
91
|
+
var tier = breakpoint(view);
|
|
92
|
+
return tier ? tier.labelKey : ALL_VIEW_LABEL_KEY;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Settings that are objects of grid-editor's own keys rather than something
|
|
97
|
+
* the host owns outright. A host naming one of their keys means "this one is
|
|
98
|
+
* different", not "forget the others", so these are filled in from their
|
|
99
|
+
* defaults instead of being replaced wholesale.
|
|
100
|
+
*/
|
|
101
|
+
var NESTED_SETTINGS = {
|
|
102
|
+
add_column: {
|
|
103
|
+
size: 12, // What a click on the add column tool adds
|
|
104
|
+
picker: true, // Holding it offers the sizes instead
|
|
105
|
+
delay: 600, // How long to hold, in milliseconds
|
|
106
|
+
},
|
|
107
|
+
elements: {
|
|
108
|
+
enabled: 'auto', // 'auto' turns them on when the page has any
|
|
109
|
+
selector: '[data-ge-element]', // What the host marks an element with
|
|
110
|
+
auto: false, // Treat every child of a content area as an element
|
|
111
|
+
},
|
|
112
|
+
resize: {
|
|
113
|
+
enabled: true,
|
|
114
|
+
handles: 'e', // Which edges carry a handle, as jQuery UI names them
|
|
115
|
+
balance: 'next', // 'next' takes the units out of the following column
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
var warned = {};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Translate one key.
|
|
123
|
+
*
|
|
124
|
+
* Lookup order is locale_strings, then the selected locale, then English,
|
|
125
|
+
* then the key itself, so a missing string is a visible key and never an
|
|
126
|
+
* empty tooltip. `params` fills {name} placeholders.
|
|
127
|
+
*
|
|
128
|
+
* Exposed as $.fn.gridEditor.t for the editor integrations in the other
|
|
129
|
+
* source files, which are handed the settings and have no instance of their
|
|
130
|
+
* own.
|
|
131
|
+
*/
|
|
132
|
+
function translate(settings, key, params) {
|
|
133
|
+
var locales = $.fn.gridEditor.locales;
|
|
134
|
+
var locale = locales[settings.locale] || {};
|
|
135
|
+
var overrides = settings.locale_strings || {};
|
|
136
|
+
var string = overrides[key];
|
|
137
|
+
|
|
138
|
+
if (string === undefined) { string = locale[key]; }
|
|
139
|
+
if (string === undefined) { string = locales.en[key]; }
|
|
140
|
+
|
|
141
|
+
if (string === undefined) {
|
|
142
|
+
warnOnce('locale:' + key, 'no string for "' + key + '" in any locale, showing the key');
|
|
143
|
+
string = key;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return string.replace(/\{(\w+)\}/g, function(placeholder, name) {
|
|
147
|
+
return params && params[name] !== undefined ? params[name] : placeholder;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function warn(message) {
|
|
152
|
+
if (window.console && window.console.warn) {
|
|
153
|
+
window.console.warn('grid-editor: ' + message);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Warn about something the host can only usefully be told about once. */
|
|
158
|
+
function warnOnce(key, message) {
|
|
159
|
+
if (warned[key]) { return; }
|
|
160
|
+
warned[key] = true;
|
|
161
|
+
warn(message);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Run a string method against a set of elements.
|
|
166
|
+
*
|
|
167
|
+
* An element with no editor on it is not an error: the method is a no-op and
|
|
168
|
+
* the set comes back for chaining, so host code does not have to check first.
|
|
169
|
+
* `getHtml` is the exception, because reading an element's html makes sense
|
|
170
|
+
* whether or not it is being edited.
|
|
171
|
+
*/
|
|
172
|
+
function dispatch(set, name, args) {
|
|
173
|
+
var descriptor = METHODS[name];
|
|
174
|
+
|
|
175
|
+
if (!descriptor) {
|
|
176
|
+
warnOnce('method:' + name, 'unknown method "' + name + '"');
|
|
177
|
+
return set;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (descriptor.value) {
|
|
181
|
+
var element = set.first();
|
|
182
|
+
if (!element.length) { return null; }
|
|
183
|
+
|
|
184
|
+
var instance = element.data('grideditor');
|
|
185
|
+
if (!instance) {
|
|
186
|
+
return descriptor.noInstance ? descriptor.noInstance(element) : null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return instance[name].apply(instance, args);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
set.each(function() {
|
|
193
|
+
var found = $(this).data('grideditor');
|
|
194
|
+
if (found) { found[name].apply(found, args); }
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return set;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
$.fn.gridEditor = function( optionsOrMethod ) {
|
|
201
|
+
|
|
202
|
+
var self = this;
|
|
203
|
+
|
|
204
|
+
/** Methods **/
|
|
205
|
+
|
|
206
|
+
if (typeof optionsOrMethod == 'string') {
|
|
207
|
+
return dispatch(self, optionsOrMethod, Array.prototype.slice.call(arguments, 1));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Initialize plugin */
|
|
211
|
+
|
|
212
|
+
self.each(function(baseIndex, baseElem) {
|
|
213
|
+
baseElem = $(baseElem);
|
|
214
|
+
|
|
215
|
+
var settings = $.extend({
|
|
216
|
+
'new_row_layouts' : [ // Column layouts for add row buttons
|
|
217
|
+
[12],
|
|
218
|
+
[6, 6],
|
|
219
|
+
[4, 4, 4],
|
|
220
|
+
[3, 3, 3, 3],
|
|
221
|
+
[2, 2, 2, 2, 2, 2],
|
|
222
|
+
[2, 8, 2],
|
|
223
|
+
[4, 8],
|
|
224
|
+
[8, 4]
|
|
225
|
+
],
|
|
226
|
+
'row_classes' : [], // Preset class toggles, on top of the classes field
|
|
227
|
+
'col_classes' : [],
|
|
228
|
+
'col_tools' : [], /* Example:
|
|
229
|
+
[ {
|
|
230
|
+
title: 'Set background image',
|
|
231
|
+
iconClass: 'glyphicon-picture',
|
|
232
|
+
on: { click: function() {} }
|
|
233
|
+
} ]
|
|
234
|
+
*/
|
|
235
|
+
'row_tools' : [],
|
|
236
|
+
'drag_handle' : 'tool', // 'tool' for the move tool, 'drawer' for the whole drawer
|
|
237
|
+
'toolbar_drag' : 'auto', // Drag the toolbar's buttons onto the canvas. 'auto' follows drag_handle
|
|
238
|
+
'element_tools' : [], // Host tools on element drawers, same shape as row_tools
|
|
239
|
+
'element_classes' : [], // Preset class toggles on an element's settings panel
|
|
240
|
+
'container_classes' : [], // The same, on a container's panel
|
|
241
|
+
'pane_classes' : [], // And on a tab's or an accordion item's
|
|
242
|
+
'container_tools' : [], // Host tools on container drawers
|
|
243
|
+
'tab_tools' : [], // Host tools on tab drawers
|
|
244
|
+
'accordion_tools' : [], // Host tools on accordion item drawers
|
|
245
|
+
'plugins' : null, // Container plugins to use; null means every one loaded
|
|
246
|
+
'elements' : NESTED_SETTINGS.elements, // Element level controls, below the column
|
|
247
|
+
'custom_filter' : '',
|
|
248
|
+
'content_types' : ['tinymce'],
|
|
249
|
+
'valid_col_sizes' : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
|
|
250
|
+
'valid_col_offsets' : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
|
|
251
|
+
'add_column' : NESTED_SETTINGS.add_column, // The add column tool
|
|
252
|
+
'layout_modes' : VIEW_KEYS.slice(), // Which views the dropdown offers
|
|
253
|
+
'default_view' : ALL_VIEW,
|
|
254
|
+
'resize' : NESTED_SETTINGS.resize, // Resizing a column by dragging its edge
|
|
255
|
+
'resizable_options' : {}, // Merged into every jQuery UI resizable
|
|
256
|
+
'source_textarea' : '',
|
|
257
|
+
'locale' : 'en', // Code of a locale in $.fn.gridEditor.locales
|
|
258
|
+
'locale_strings' : {}, // Overrides for individual keys
|
|
259
|
+
'callbacks' : {}, // before_*/after_* functions, the events by another route
|
|
260
|
+
'confirm_delete' : true, // Ask before deleting a row or a column
|
|
261
|
+
'sortable_options' : {} // Merged into every jQuery UI sortable
|
|
262
|
+
}, optionsOrMethod);
|
|
263
|
+
|
|
264
|
+
// Merged rather than replaced, so `elements: { auto: true }` keeps the
|
|
265
|
+
// default selector instead of losing it
|
|
266
|
+
$.each(NESTED_SETTINGS, function(name, defaults) {
|
|
267
|
+
settings[name] = $.extend({}, defaults, settings[name]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
// Elems
|
|
272
|
+
var canvas,
|
|
273
|
+
mainControls,
|
|
274
|
+
wrapper, // controls wrapper
|
|
275
|
+
addRowGroup,
|
|
276
|
+
addContainerGroup,
|
|
277
|
+
layoutDropdown,
|
|
278
|
+
htmlTextArea
|
|
279
|
+
;
|
|
280
|
+
var curView = settings.default_view; // Breakpoint key, or 'all'
|
|
281
|
+
var confirmDialog = null; // The delete confirmation, built when first needed
|
|
282
|
+
var sizePicker = null; // The open column size picker, if there is one
|
|
283
|
+
var dropMarker = null; // The line showing where a dragged toolbar button would land
|
|
284
|
+
var warnedHere = {}; // Deprecations are worth saying once per instance, not once per call
|
|
285
|
+
|
|
286
|
+
// Before anything else, because the instance handle hands the canvas
|
|
287
|
+
// to hosts and the rest of setup() runs at the end of this function
|
|
288
|
+
canvas = baseElem.addClass('ge-canvas');
|
|
289
|
+
|
|
290
|
+
function warnOnceHere(key, message) {
|
|
291
|
+
if (warnedHere[key]) { return; }
|
|
292
|
+
warnedHere[key] = true;
|
|
293
|
+
warn(message);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** This instance's strings, in the locale its settings asked for. */
|
|
297
|
+
function t(key, params) {
|
|
298
|
+
return translate(settings, key, params);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Swap language at runtime. The controls carry their strings in
|
|
303
|
+
* attributes, so they are rebuilt rather than patched.
|
|
304
|
+
*/
|
|
305
|
+
function setLocale(code) {
|
|
306
|
+
settings.locale = code;
|
|
307
|
+
handle.settings = settingsCopy();
|
|
308
|
+
|
|
309
|
+
removeConfirmModal();
|
|
310
|
+
mainControls.remove();
|
|
311
|
+
createMainControls();
|
|
312
|
+
reset();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
var operationDepth = 0; // Operations running right now
|
|
316
|
+
var deferredWork = []; // What handlers asked for while one was running
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* The payload every notification carries. Built here, and only here,
|
|
320
|
+
* so no caller assembles one by hand and gets a field wrong.
|
|
321
|
+
*
|
|
322
|
+
* `parent` is where the node is going, or where it is coming from on a
|
|
323
|
+
* delete, which is not the same as node.parent() while the node is
|
|
324
|
+
* still detached - so an add passes it in.
|
|
325
|
+
*/
|
|
326
|
+
function payloadFor(kind, node, extra) {
|
|
327
|
+
return $.extend({
|
|
328
|
+
kind: kind,
|
|
329
|
+
node: node,
|
|
330
|
+
parent: node.parent(),
|
|
331
|
+
canvas: canvas,
|
|
332
|
+
breakpoint: getView(),
|
|
333
|
+
source: 'api',
|
|
334
|
+
}, extra || {});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Deliver one notification twice: as a jQuery event on the canvas -
|
|
339
|
+
* the specific name first, then the generic one - and as the matching
|
|
340
|
+
* settings.callbacks entries. Everything is delivered whatever the
|
|
341
|
+
* first listener says, and the answer is whether any of them canceled,
|
|
342
|
+
* which only means something for a before-* notification.
|
|
343
|
+
*/
|
|
344
|
+
function emit(name, payload) {
|
|
345
|
+
var names = [name];
|
|
346
|
+
var generic = name.replace(/^(before|after)-add-.+$/, '$1-add');
|
|
347
|
+
if (generic !== name) { names.push(generic); }
|
|
348
|
+
|
|
349
|
+
var canceled = false;
|
|
350
|
+
|
|
351
|
+
names.forEach(function(eventName) {
|
|
352
|
+
var event = $.Event('grideditor:' + eventName);
|
|
353
|
+
canvas.trigger(event, [payload]);
|
|
354
|
+
if (event.isDefaultPrevented()) { canceled = true; }
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
names.forEach(function(eventName) {
|
|
358
|
+
var callback = settings.callbacks[eventName.replace(/-/g, '_')];
|
|
359
|
+
if (typeof callback == 'function' && callback(payload) === false) {
|
|
360
|
+
canceled = true;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
return !canceled;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* One operation, from its before-* notification to its after-* one.
|
|
369
|
+
*
|
|
370
|
+
* Anything a handler asks the editor to do while the operation runs is
|
|
371
|
+
* queued and played back once it finishes, so a handler cannot reset
|
|
372
|
+
* the canvas out from under the operation that called it.
|
|
373
|
+
*/
|
|
374
|
+
function operate(body) {
|
|
375
|
+
operationDepth++;
|
|
376
|
+
try {
|
|
377
|
+
return body();
|
|
378
|
+
} finally {
|
|
379
|
+
operationDepth--;
|
|
380
|
+
if (operationDepth === 0) {
|
|
381
|
+
while (deferredWork.length) {
|
|
382
|
+
deferredWork.shift()();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Run `work` now, or after the running operation if there is one. */
|
|
389
|
+
function defer(work) {
|
|
390
|
+
if (operationDepth === 0) {
|
|
391
|
+
work();
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
deferredWork.push(work);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Insert a node: ask, insert, bring the canvas up to date so the new
|
|
400
|
+
* markup has its controls, then announce it. Hands back the node, or
|
|
401
|
+
* null when a handler canceled.
|
|
402
|
+
*
|
|
403
|
+
* The update is init() rather than reset(): a reset deinitializes
|
|
404
|
+
* every rich text editor on the canvas, and adding a row somewhere
|
|
405
|
+
* else is no reason to close the editor the user is typing in.
|
|
406
|
+
*/
|
|
407
|
+
function addNode(kind, node, insert, extra) {
|
|
408
|
+
var name = addEventName(kind);
|
|
409
|
+
|
|
410
|
+
return operate(function() {
|
|
411
|
+
var payload = payloadFor(kind, node, extra);
|
|
412
|
+
|
|
413
|
+
if (!emit('before-add-' + name, payload)) { return null; }
|
|
414
|
+
|
|
415
|
+
insert();
|
|
416
|
+
init();
|
|
417
|
+
emit('after-add-' + name, payload);
|
|
418
|
+
|
|
419
|
+
return node;
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Every container type shares one pair of add events, with the
|
|
425
|
+
* payload's kind saying which type it was: a host that cares about
|
|
426
|
+
* containers binds one name, not three.
|
|
427
|
+
*/
|
|
428
|
+
function addEventName(kind) {
|
|
429
|
+
return $.fn.gridEditor.containers[kind] ? 'container' : kind;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Ask the user, in Bootstrap's own modal.
|
|
434
|
+
*
|
|
435
|
+
* Bootstrap is already a dependency of an editor for Bootstrap's grid,
|
|
436
|
+
* and window.confirm cannot be styled, cannot be translated by us and
|
|
437
|
+
* blocks the page while it is up. The modal lives outside the canvas,
|
|
438
|
+
* so it is never part of what getHtml returns.
|
|
439
|
+
*
|
|
440
|
+
* A page that loaded Bootstrap's css but not its javascript still gets
|
|
441
|
+
* asked - by the browser, as before.
|
|
442
|
+
*/
|
|
443
|
+
function askToDelete(message, whenConfirmed) {
|
|
444
|
+
if (!settings.confirm_delete) {
|
|
445
|
+
whenConfirmed();
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (!window.bootstrap || !window.bootstrap.Modal) {
|
|
450
|
+
if (window.confirm(message)) { whenConfirmed(); }
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
var modal = confirmModal();
|
|
455
|
+
var confirmed = false;
|
|
456
|
+
|
|
457
|
+
modal.find('.ge-confirm-message').text(message);
|
|
458
|
+
modal.find('.ge-confirm-ok').off('click').on('click', function() {
|
|
459
|
+
confirmed = true;
|
|
460
|
+
window.bootstrap.Modal.getInstance(modal[0]).hide();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
modal.off('hidden.bs.modal').on('hidden.bs.modal', function() {
|
|
464
|
+
// After the modal is out of the way, so the backdrop is not
|
|
465
|
+
// sitting over the animation the delete runs
|
|
466
|
+
if (confirmed) { whenConfirmed(); }
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
modal.off('shown.bs.modal').on('shown.bs.modal', function() {
|
|
470
|
+
modal.find('.ge-confirm-ok').trigger('focus');
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
window.bootstrap.Modal.getOrCreateInstance(modal[0]).show();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Built once per instance, and taken away again by destroy(). */
|
|
477
|
+
function confirmModal() {
|
|
478
|
+
if (confirmDialog) { return confirmDialog; }
|
|
479
|
+
|
|
480
|
+
confirmDialog = $(
|
|
481
|
+
'<div class="modal fade ge-confirm" tabindex="-1" aria-hidden="true">' +
|
|
482
|
+
'<div class="modal-dialog modal-dialog-centered">' +
|
|
483
|
+
'<div class="modal-content">' +
|
|
484
|
+
'<div class="modal-header">' +
|
|
485
|
+
'<h5 class="modal-title"></h5>' +
|
|
486
|
+
'<button type="button" class="btn-close" data-bs-dismiss="modal"></button>' +
|
|
487
|
+
'</div>' +
|
|
488
|
+
'<div class="modal-body"><p class="ge-confirm-message"></p></div>' +
|
|
489
|
+
'<div class="modal-footer">' +
|
|
490
|
+
'<button type="button" class="btn btn-secondary ge-confirm-cancel" data-bs-dismiss="modal"></button>' +
|
|
491
|
+
'<button type="button" class="btn btn-danger ge-confirm-ok"></button>' +
|
|
492
|
+
'</div>' +
|
|
493
|
+
'</div>' +
|
|
494
|
+
'</div>' +
|
|
495
|
+
'</div>'
|
|
496
|
+
).appendTo('body');
|
|
497
|
+
|
|
498
|
+
confirmDialog.find('.modal-title').text(t('confirm.title'));
|
|
499
|
+
confirmDialog.find('.btn-close').attr('aria-label', t('confirm.cancel'));
|
|
500
|
+
confirmDialog.find('.ge-confirm-cancel').text(t('confirm.cancel'));
|
|
501
|
+
confirmDialog.find('.ge-confirm-ok').text(t('confirm.ok'));
|
|
502
|
+
|
|
503
|
+
return confirmDialog;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** The confirm modal is rebuilt in the new language on setLocale. */
|
|
507
|
+
function removeConfirmModal() {
|
|
508
|
+
if (!confirmDialog) { return; }
|
|
509
|
+
|
|
510
|
+
if (window.bootstrap && window.bootstrap.Modal) {
|
|
511
|
+
var instance = window.bootstrap.Modal.getInstance(confirmDialog[0]);
|
|
512
|
+
if (instance) { instance.dispose(); }
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
confirmDialog.remove();
|
|
516
|
+
confirmDialog = null;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Remove a node: ask the host, then the user, then remove it, update
|
|
521
|
+
* the canvas and announce it once the animation has finished.
|
|
522
|
+
*
|
|
523
|
+
* The host's handler goes first on purpose. A host that cancels
|
|
524
|
+
* before-delete to ask in its own way does not want the built-in
|
|
525
|
+
* question to have been asked already.
|
|
526
|
+
*/
|
|
527
|
+
function deleteNode(kind, node, message, animate) {
|
|
528
|
+
operate(function() {
|
|
529
|
+
var payload = payloadFor(kind, node, { source: 'tool' });
|
|
530
|
+
|
|
531
|
+
if (!emit('before-delete', payload)) { return; }
|
|
532
|
+
|
|
533
|
+
askToDelete(message, function() {
|
|
534
|
+
operate(function() {
|
|
535
|
+
animate(function() {
|
|
536
|
+
node.remove();
|
|
537
|
+
operate(function() {
|
|
538
|
+
init();
|
|
539
|
+
emit('after-delete', payload);
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Resize a column through the events, writing the tiers the current
|
|
549
|
+
* view covers: one in a per-breakpoint view, all six in the all view.
|
|
550
|
+
*
|
|
551
|
+
* The budget is checked per tier and the whole change is refused if
|
|
552
|
+
* any tier has no room, so a resize never half lands.
|
|
553
|
+
*/
|
|
554
|
+
/**
|
|
555
|
+
* What resizing this column to `size` would write, or null when the
|
|
556
|
+
* budget refuses it or there is nothing to change.
|
|
557
|
+
*
|
|
558
|
+
* Refused rather than quietly clamped: the offset is something the
|
|
559
|
+
* user set, and a tool that rewrites it is a tool that lies. The plan
|
|
560
|
+
* covers every tier the view writes, so a resize never half lands.
|
|
561
|
+
*/
|
|
562
|
+
function planSize(col, size) {
|
|
563
|
+
var tiers = tiersFor(curView);
|
|
564
|
+
var wanted = tiers.map(function(tier) {
|
|
565
|
+
return clamp({ size: size, offset: getEffectiveOffset(col, tier) || 0 });
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
if (wanted.some(function(request) { return request.refused; })) { return null; }
|
|
569
|
+
|
|
570
|
+
var unchanged = tiers.every(function(tier, i) {
|
|
571
|
+
return getSize(col, tier) === wanted[i].size;
|
|
572
|
+
});
|
|
573
|
+
if (unchanged) { return null; }
|
|
574
|
+
|
|
575
|
+
return { tiers: tiers, sizes: wanted, size: wanted[0].size };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function writeSize(col, plan) {
|
|
579
|
+
plan.tiers.forEach(function(tier, i) { setSize(col, tier, plan.sizes[i].size); });
|
|
580
|
+
stripPixelWidths(col);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Resize a column from a tool, announcing it either side. */
|
|
584
|
+
function resizeColumn(col, size, source) {
|
|
585
|
+
var from = currentSize(col);
|
|
586
|
+
var plan = planSize(col, size);
|
|
587
|
+
|
|
588
|
+
if (!plan) { return false; }
|
|
589
|
+
|
|
590
|
+
return operate(function() {
|
|
591
|
+
var payload = payloadFor('column', col, { source: source, from: from, to: plan.size });
|
|
592
|
+
|
|
593
|
+
if (!emit('before-resize', payload)) { return false; }
|
|
594
|
+
|
|
595
|
+
writeSize(col, plan);
|
|
596
|
+
emit('after-resize', payload);
|
|
597
|
+
|
|
598
|
+
return true;
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Indent a column, shrinking it when the budget needs it: the offset
|
|
604
|
+
* is what the user asked for, so it is the one that gets its way.
|
|
605
|
+
*/
|
|
606
|
+
function indentColumn(col, offset, source) {
|
|
607
|
+
var tiers = tiersFor(curView);
|
|
608
|
+
var from = currentOffset(col);
|
|
609
|
+
|
|
610
|
+
offset = Math.min(Math.max(offset, 0), MAX_COL_OFFSET);
|
|
611
|
+
|
|
612
|
+
var unchanged = tiers.every(function(tier) {
|
|
613
|
+
return (getOffset(col, tier) || 0) === offset;
|
|
614
|
+
});
|
|
615
|
+
if (unchanged) { return false; }
|
|
616
|
+
|
|
617
|
+
return operate(function() {
|
|
618
|
+
var payload = payloadFor('column', col, {
|
|
619
|
+
source: source,
|
|
620
|
+
from: from,
|
|
621
|
+
to: offset,
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
if (!emit('before-indent', payload)) { return false; }
|
|
625
|
+
|
|
626
|
+
tiers.forEach(function(tier) {
|
|
627
|
+
var was = getEffectiveSize(col, tier);
|
|
628
|
+
var wanted = clamp({ size: was, offset: offset, leading: 'offset' });
|
|
629
|
+
|
|
630
|
+
setOffset(col, tier, wanted.offset);
|
|
631
|
+
|
|
632
|
+
// Only where the budget actually forced the column to give
|
|
633
|
+
// way; otherwise an indent would write size classes for
|
|
634
|
+
// tiers nobody asked it to touch
|
|
635
|
+
if (wanted.size !== null && wanted.size !== was) {
|
|
636
|
+
setSize(col, tier, wanted.size);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
emit('after-indent', payload);
|
|
641
|
+
|
|
642
|
+
return true;
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Where a node sits, for the from/to of a move. Tool drawers are not
|
|
648
|
+
* counted, so the index is the one a host would recognize.
|
|
649
|
+
*/
|
|
650
|
+
function positionOf(node) {
|
|
651
|
+
var parent = node.parent();
|
|
652
|
+
|
|
653
|
+
return {
|
|
654
|
+
parent: parent,
|
|
655
|
+
index: parent.children().not('.ge-tools-drawer').index(node),
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function kindOf(node) {
|
|
660
|
+
var fromPlugin = null;
|
|
661
|
+
|
|
662
|
+
$.each(FEATURES, function(name, feature) {
|
|
663
|
+
if (!fromPlugin && feature.kindOf) { fromPlugin = feature.kindOf(node); }
|
|
664
|
+
});
|
|
665
|
+
if (fromPlugin) { return fromPlugin; }
|
|
666
|
+
|
|
667
|
+
if (node.attr('data-ge-container')) { return node.attr('data-ge-container'); }
|
|
668
|
+
if (node.hasClass('ge-tab')) { return 'tab'; }
|
|
669
|
+
if (node.hasClass('ge-accordion-item')) { return 'accordion-item'; }
|
|
670
|
+
if (node.hasClass('row')) { return 'row'; }
|
|
671
|
+
if (node.hasClass('column')) { return 'column'; }
|
|
672
|
+
if (node.hasClass('ge-element')) { return 'element'; }
|
|
673
|
+
if (node.hasClass('ge-content')) { return 'content'; }
|
|
674
|
+
return 'node';
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Copy html to sourceElement if a source textarea is given
|
|
678
|
+
if (settings.source_textarea) {
|
|
679
|
+
var sourceHtml = $(settings.source_textarea).val();
|
|
680
|
+
if (sourceHtml.length > 0 && $('<div>' + sourceHtml + '</div>').find('.row').addBack('.row').length == 0) {
|
|
681
|
+
var sourceRow = createRow();
|
|
682
|
+
var sourceColumn = createColumn(12).appendTo(sourceRow);
|
|
683
|
+
sourceColumn.find('.ge-content').html(sourceHtml);
|
|
684
|
+
sourceHtml = sourceColumn.html();
|
|
685
|
+
}
|
|
686
|
+
baseElem.html(sourceHtml);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Wrap content if it is non-bootstrap
|
|
690
|
+
if (baseElem.children().length && !baseElem.find('div.row').length) {
|
|
691
|
+
var children = baseElem.children();
|
|
692
|
+
var newRow = $('<div class="row"><div class="col-lg-12"/></div>').appendTo(baseElem);
|
|
693
|
+
newRow.find('.col-lg-12').append(children);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// setup() and init() run at the end of this function, once every
|
|
697
|
+
// table and helper below has been assigned: the toolbar is built from
|
|
698
|
+
// the container registry, and a var declared later is not there yet.
|
|
699
|
+
|
|
700
|
+
function setup() {
|
|
701
|
+
htmlTextArea = $('<textarea class="ge-html-output"/>').insertBefore(canvas);
|
|
702
|
+
|
|
703
|
+
createMainControls();
|
|
704
|
+
|
|
705
|
+
// Make controls fixed on scroll
|
|
706
|
+
$(window).on('scroll', onScroll);
|
|
707
|
+
|
|
708
|
+
/* Init RTE on click */
|
|
709
|
+
canvas.on('click', '.ge-content', initRTE);
|
|
710
|
+
|
|
711
|
+
/* A trigger is often a link, and a link still navigates even
|
|
712
|
+
with its Bootstrap attributes suspended */
|
|
713
|
+
canvas.on('click', '.ge-popup-trigger, [data-ge-popup-target]', function(e) {
|
|
714
|
+
if (canvas.hasClass('ge-editing')) { e.preventDefault(); }
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// A rich text editor rewrites the content area as it takes over,
|
|
718
|
+
// which costs the element drawers inside it. The integrations say
|
|
719
|
+
// when their editor is ready, and the drawers go back in.
|
|
720
|
+
canvas.on('ge-rte-ready', '.ge-content', function() {
|
|
721
|
+
plugins('onContentReady', $(this));
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* The toolbar above the canvas. Separate from setup() because every
|
|
727
|
+
* string in it comes from the locale, so setLocale() rebuilds it.
|
|
728
|
+
*/
|
|
729
|
+
function createMainControls() {
|
|
730
|
+
mainControls = $('<div class="ge-mainControls" />').insertBefore(htmlTextArea);
|
|
731
|
+
wrapper = $('<div class="ge-wrapper ge-top" />').appendTo(mainControls);
|
|
732
|
+
|
|
733
|
+
// Add row
|
|
734
|
+
addRowGroup = $('<div class="ge-addRowGroup btn-group" />').appendTo(wrapper);
|
|
735
|
+
addContainerGroup = $('<div class="ge-addContainerGroup btn-group" />');
|
|
736
|
+
$.each(settings.new_row_layouts, function(j, layout) {
|
|
737
|
+
var btn = $('<a class="btn btn-sm btn-primary" />')
|
|
738
|
+
.attr('title', t('row.add', { layout: layout.join('-') }))
|
|
739
|
+
// What this button makes, in the markup rather than in
|
|
740
|
+
// jQuery data: a drag works on a clone of it
|
|
741
|
+
.attr('data-ge-toolbar', 'row')
|
|
742
|
+
.attr('data-ge-layout', layout.join(','))
|
|
743
|
+
.on('click', function() {
|
|
744
|
+
var row = createRow();
|
|
745
|
+
layout.forEach(function(i) {
|
|
746
|
+
createColumn(i).appendTo(row);
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
var added = addNode('row', row, function() {
|
|
750
|
+
row.appendTo(canvas);
|
|
751
|
+
}, { parent: canvas, source: 'tool' });
|
|
752
|
+
|
|
753
|
+
if (added && row[0].scrollIntoView) {
|
|
754
|
+
row[0].scrollIntoView({behavior: 'smooth'});
|
|
755
|
+
}
|
|
756
|
+
})
|
|
757
|
+
.appendTo(addRowGroup)
|
|
758
|
+
;
|
|
759
|
+
|
|
760
|
+
btn.append('<i class="bi bi-plus"></i>');
|
|
761
|
+
|
|
762
|
+
var layoutName = layout.join(' - ');
|
|
763
|
+
var icon = '<div class="row ge-row-icon">';
|
|
764
|
+
layout.forEach(function(i) {
|
|
765
|
+
icon += '<div class="column col-' + i + '"/>';
|
|
766
|
+
});
|
|
767
|
+
icon += '</div>';
|
|
768
|
+
btn.append(icon);
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
addContainerGroup.appendTo(wrapper);
|
|
772
|
+
|
|
773
|
+
// A container starts in a row of its own, the way the add row
|
|
774
|
+
// buttons next to these ones do
|
|
775
|
+
$.each(CONTAINERS, function(type, definition) {
|
|
776
|
+
$('<a class="btn btn-sm btn-primary ge-add-container" />')
|
|
777
|
+
.attr('title', t(definition.labelKey))
|
|
778
|
+
.attr('data-ge-toolbar', 'container')
|
|
779
|
+
.attr('data-ge-container-type', type)
|
|
780
|
+
.append('<i class="bi bi-plus"></i>')
|
|
781
|
+
.append($('<span />').text(t(definition.labelKey)))
|
|
782
|
+
.on('click', function() {
|
|
783
|
+
var row = createRow();
|
|
784
|
+
var column = createColumn(MAX_COL_SIZE).appendTo(row);
|
|
785
|
+
var container = definition.create({});
|
|
786
|
+
|
|
787
|
+
column.find('> .ge-content').replaceWith(container);
|
|
788
|
+
|
|
789
|
+
addNode(type, container, function() {
|
|
790
|
+
row.appendTo(canvas);
|
|
791
|
+
}, { parent: canvas, source: 'tool' });
|
|
792
|
+
})
|
|
793
|
+
.appendTo(addContainerGroup)
|
|
794
|
+
;
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
// Buttons on right
|
|
798
|
+
layoutDropdown = $('<div class="dropdown pull-right ge-layout-mode">' +
|
|
799
|
+
'<button type="button" class="btn btn-sm btn-primary dropdown-toggle" data-bs-toggle="dropdown"></button>' +
|
|
800
|
+
'<div class="dropdown-menu" role="menu"></div>' +
|
|
801
|
+
'</div>')
|
|
802
|
+
.on('click', 'a', function() {
|
|
803
|
+
// Through changeView, so the dropdown and the method are
|
|
804
|
+
// one path rather than two that have to agree
|
|
805
|
+
changeView($(this).attr('data-ge-view'));
|
|
806
|
+
})
|
|
807
|
+
.appendTo(wrapper)
|
|
808
|
+
;
|
|
809
|
+
settings.layout_modes.forEach(function(view) {
|
|
810
|
+
$('<a class="dropdown-item" />')
|
|
811
|
+
.attr('data-ge-view', view)
|
|
812
|
+
.attr('title', t(labelKeyFor(view)))
|
|
813
|
+
.text(t(labelKeyFor(view)))
|
|
814
|
+
.appendTo(layoutDropdown.find('.dropdown-menu'))
|
|
815
|
+
;
|
|
816
|
+
});
|
|
817
|
+
layoutDropdown.find('button').text(t(labelKeyFor(curView)));
|
|
818
|
+
|
|
819
|
+
var btnGroup = $('<div class="btn-group pull-right"/>')
|
|
820
|
+
.appendTo(wrapper)
|
|
821
|
+
;
|
|
822
|
+
var htmlButton = $('<button type="button" class="btn btn-sm btn-primary gm-edit-mode"><i class="bi bi-code-slash"></i></button>')
|
|
823
|
+
.attr('title', t('tool.edit_source'))
|
|
824
|
+
.on('click', function() {
|
|
825
|
+
if (htmlButton.hasClass('active')) {
|
|
826
|
+
canvas.empty().html(htmlTextArea.val()).show();
|
|
827
|
+
init();
|
|
828
|
+
htmlTextArea.hide();
|
|
829
|
+
} else {
|
|
830
|
+
deinit();
|
|
831
|
+
htmlTextArea
|
|
832
|
+
.height(0.8 * $(window).height())
|
|
833
|
+
.val(canvas.html())
|
|
834
|
+
.show()
|
|
835
|
+
;
|
|
836
|
+
canvas.hide();
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
htmlButton.toggleClass('active btn-danger');
|
|
840
|
+
})
|
|
841
|
+
.appendTo(btnGroup)
|
|
842
|
+
;
|
|
843
|
+
var previewButton = $('<button type="button" class="btn btn-sm btn-primary gm-preview"><i class="bi bi-eye-fill"></i></button>')
|
|
844
|
+
.attr('title', t('tool.preview'))
|
|
845
|
+
.on('mouseenter', function() {
|
|
846
|
+
canvas.removeClass('ge-editing');
|
|
847
|
+
})
|
|
848
|
+
.on('click', function() {
|
|
849
|
+
previewButton.toggleClass('active btn-danger').trigger('mouseleave');
|
|
850
|
+
})
|
|
851
|
+
.on('mouseleave', function() {
|
|
852
|
+
if (!previewButton.hasClass('active')) {
|
|
853
|
+
canvas.addClass('ge-editing');
|
|
854
|
+
}
|
|
855
|
+
})
|
|
856
|
+
.appendTo(btnGroup)
|
|
857
|
+
;
|
|
858
|
+
|
|
859
|
+
makeToolbarDraggable();
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* The toolbar's buttons as a palette: drag one onto the canvas and
|
|
864
|
+
* what it makes is created where it lands, rather than at the end.
|
|
865
|
+
*
|
|
866
|
+
* On by default in the mode where everything else is dragged by its
|
|
867
|
+
* body rather than by a handle, since that is the same idea applied to
|
|
868
|
+
* the toolbar; toolbar_drag: true or false decides it outright.
|
|
869
|
+
*/
|
|
870
|
+
function toolbarDrags() {
|
|
871
|
+
if (settings.toolbar_drag === 'auto') { return settings.drag_handle === 'drawer'; }
|
|
872
|
+
|
|
873
|
+
return !!settings.toolbar_drag;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function makeToolbarDraggable() {
|
|
877
|
+
if (!toolbarDrags()) { return; }
|
|
878
|
+
|
|
879
|
+
mainControls.find('[data-ge-toolbar]').draggable({
|
|
880
|
+
helper: function() { return $(this).clone().addClass('ge-toolbar-helper'); },
|
|
881
|
+
appendTo: 'body',
|
|
882
|
+
zIndex: 1000,
|
|
883
|
+
cursorAt: { top: 14, left: 14 },
|
|
884
|
+
|
|
885
|
+
start: function() { canvas.addClass('ge-dropping'); },
|
|
886
|
+
drag: function(e) { showDropMarker(e.pageX, e.pageY); },
|
|
887
|
+
|
|
888
|
+
stop: function(e) {
|
|
889
|
+
var where = dropPlaceAt(e.pageX, e.pageY);
|
|
890
|
+
|
|
891
|
+
canvas.removeClass('ge-dropping');
|
|
892
|
+
hideDropMarker();
|
|
893
|
+
|
|
894
|
+
if (where) { insertFromToolbar($(this), where); }
|
|
895
|
+
},
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Where a drop at this point would put things: which region it lands
|
|
901
|
+
* in, and which of that region's children it goes before.
|
|
902
|
+
*
|
|
903
|
+
* Worked out from the pointer rather than handed to a sortable. The
|
|
904
|
+
* canvas is a tree of regions that connected sortables fight over -
|
|
905
|
+
* a column grows as a placeholder is put in it, until it covers the
|
|
906
|
+
* pointer wherever the pointer goes - and a new block has one
|
|
907
|
+
* question to answer, which is where it lands.
|
|
908
|
+
*/
|
|
909
|
+
function dropPlaceAt(pageX, pageY) {
|
|
910
|
+
var x = pageX - window.scrollX;
|
|
911
|
+
var y = pageY - window.scrollY;
|
|
912
|
+
var under = document.elementFromPoint(x, y);
|
|
913
|
+
|
|
914
|
+
if (!under) { return null; }
|
|
915
|
+
|
|
916
|
+
var region = $(under).closest('.column, .ge-canvas');
|
|
917
|
+
if (!region.length || (region[0] !== canvas[0] && !canvas[0].contains(region[0]))) {
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
var before = null;
|
|
922
|
+
|
|
923
|
+
region.children('.row, .ge-content, [data-ge-container]').each(function() {
|
|
924
|
+
if (before) { return; }
|
|
925
|
+
|
|
926
|
+
var box = this.getBoundingClientRect();
|
|
927
|
+
if (y < box.top + box.height / 2) { before = $(this); }
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
return { region: region, before: before };
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/** A line where the block would go, following the pointer. */
|
|
934
|
+
function showDropMarker(pageX, pageY) {
|
|
935
|
+
var where = dropPlaceAt(pageX, pageY);
|
|
936
|
+
|
|
937
|
+
if (!where) { return hideDropMarker(); }
|
|
938
|
+
|
|
939
|
+
if (!dropMarker) { dropMarker = $('<div class="ge-drop-marker" />'); }
|
|
940
|
+
|
|
941
|
+
if (where.before) {
|
|
942
|
+
dropMarker.insertBefore(where.before);
|
|
943
|
+
} else {
|
|
944
|
+
dropMarker.appendTo(where.region);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
return undefined;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function hideDropMarker() {
|
|
951
|
+
if (dropMarker) { dropMarker.remove(); }
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* The row or container a toolbar button stands for, made and put where
|
|
956
|
+
* the pointer left it.
|
|
957
|
+
*/
|
|
958
|
+
function insertFromToolbar(button, where) {
|
|
959
|
+
var container = button.attr('data-ge-toolbar') === 'container';
|
|
960
|
+
var type = button.attr('data-ge-container-type');
|
|
961
|
+
var made = container
|
|
962
|
+
? CONTAINERS[type].create({})
|
|
963
|
+
: rowFromLayout(button.attr('data-ge-layout'));
|
|
964
|
+
|
|
965
|
+
// A container belongs in a column: dropped straight onto the
|
|
966
|
+
// canvas it brings a row and a column of its own
|
|
967
|
+
var placed = made;
|
|
968
|
+
if (container && !where.region.is('.column')) {
|
|
969
|
+
placed = createRow();
|
|
970
|
+
createColumn(MAX_COL_SIZE).appendTo(placed)
|
|
971
|
+
.find('> .ge-content').replaceWith(made);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
return addNode(container ? type : 'row', made, function() {
|
|
975
|
+
if (where.before) {
|
|
976
|
+
placed.insertBefore(where.before);
|
|
977
|
+
} else {
|
|
978
|
+
placed.appendTo(where.region);
|
|
979
|
+
}
|
|
980
|
+
}, { parent: where.region, source: 'dragdrop' });
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function rowFromLayout(layout) {
|
|
984
|
+
var row = createRow();
|
|
985
|
+
|
|
986
|
+
(layout || '').split(',').forEach(function(size) {
|
|
987
|
+
if (size !== '') { createColumn(parseInt(size, 10)).appendTo(row); }
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
return row;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function onScroll(e) {
|
|
994
|
+
var $window = $(window);
|
|
995
|
+
|
|
996
|
+
if (
|
|
997
|
+
$window.scrollTop() > mainControls.offset().top &&
|
|
998
|
+
$window.scrollTop() < canvas.offset().top + canvas.height()
|
|
999
|
+
) {
|
|
1000
|
+
if (wrapper.hasClass('ge-top')) {
|
|
1001
|
+
wrapper
|
|
1002
|
+
.css({
|
|
1003
|
+
left: wrapper.offset().left,
|
|
1004
|
+
width: wrapper.outerWidth(),
|
|
1005
|
+
})
|
|
1006
|
+
.removeClass('ge-top')
|
|
1007
|
+
.addClass('ge-fixed')
|
|
1008
|
+
;
|
|
1009
|
+
}
|
|
1010
|
+
} else {
|
|
1011
|
+
if (wrapper.hasClass('ge-fixed')) {
|
|
1012
|
+
wrapper
|
|
1013
|
+
.css({ left: '', width: '' })
|
|
1014
|
+
.removeClass('ge-fixed')
|
|
1015
|
+
.addClass('ge-top')
|
|
1016
|
+
;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function initRTE(e) {
|
|
1022
|
+
if ($(this).hasClass('ge-rte-active')) { return; }
|
|
1023
|
+
|
|
1024
|
+
// A content area nobody can see - a tab that is not the open one,
|
|
1025
|
+
// a closed accordion item - has no geometry for an editor to lay
|
|
1026
|
+
// its toolbar out against, and nothing anyone can type into
|
|
1027
|
+
if (!$(this).is(':visible')) { return; }
|
|
1028
|
+
|
|
1029
|
+
var rte = getRTE($(this).data('ge-content-type'));
|
|
1030
|
+
if (rte) {
|
|
1031
|
+
$(this).addClass('ge-rte-active', true);
|
|
1032
|
+
rte.init(settings, $(this));
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function reset() {
|
|
1037
|
+
deinit();
|
|
1038
|
+
init();
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function init() {
|
|
1042
|
+
runFilter(true);
|
|
1043
|
+
canvas.addClass('ge-editing');
|
|
1044
|
+
canvas.toggleClass('ge-drag-drawer', settings.drag_handle === 'drawer');
|
|
1045
|
+
addAllColClasses();
|
|
1046
|
+
wrapContent();
|
|
1047
|
+
createRowControls();
|
|
1048
|
+
createColControls();
|
|
1049
|
+
markContainers();
|
|
1050
|
+
plugins('onInit');
|
|
1051
|
+
makeSortable();
|
|
1052
|
+
makeResizable();
|
|
1053
|
+
switchLayout(curView);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function deinit() {
|
|
1057
|
+
canvas.removeClass('ge-editing ge-drag-drawer ge-dropping');
|
|
1058
|
+
var contents = canvas.find('.ge-content').each(function() {
|
|
1059
|
+
var content = $(this);
|
|
1060
|
+
var rte = getRTE(content.data('ge-content-type'));
|
|
1061
|
+
if (rte) {
|
|
1062
|
+
rte.deinit(settings, content);
|
|
1063
|
+
}
|
|
1064
|
+
// Cleared after rte.deinit, not before: an editor can restore the
|
|
1065
|
+
// class attribute it snapshotted when it was created, which would
|
|
1066
|
+
// leave ge-rte-active in place and make initRTE ignore every later
|
|
1067
|
+
// click on this content area.
|
|
1068
|
+
content.removeClass('ge-rte-active');
|
|
1069
|
+
});
|
|
1070
|
+
closeSizePicker();
|
|
1071
|
+
hideDropMarker();
|
|
1072
|
+
canvas.find('.ge-tools-drawer').remove();
|
|
1073
|
+
plugins('onDeinit');
|
|
1074
|
+
unmarkContainers();
|
|
1075
|
+
removeSortable();
|
|
1076
|
+
removeResizable();
|
|
1077
|
+
runFilter(false);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* The markup as a host would save it: no drawers, no editor, no
|
|
1082
|
+
* sortables. The canvas goes back to editing afterwards.
|
|
1083
|
+
*/
|
|
1084
|
+
function getHtml() {
|
|
1085
|
+
deinit();
|
|
1086
|
+
stripPixelWidths(canvas);
|
|
1087
|
+
var html = canvas.html();
|
|
1088
|
+
init();
|
|
1089
|
+
return html;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function destroy() {
|
|
1093
|
+
deinit();
|
|
1094
|
+
removeConfirmModal();
|
|
1095
|
+
mainControls.remove();
|
|
1096
|
+
htmlTextArea.remove();
|
|
1097
|
+
$(window).off('scroll', onScroll);
|
|
1098
|
+
canvas.off('click', '.ge-content', initRTE);
|
|
1099
|
+
canvas.off('ge-rte-ready', '.ge-content');
|
|
1100
|
+
canvas.removeData('grideditor');
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function deprecatedRemove() {
|
|
1104
|
+
warnOnceHere('remove', 'remove() is deprecated and will be removed in a later ' +
|
|
1105
|
+
'release. Use destroy(), which does the same thing.');
|
|
1106
|
+
destroy();
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
/** The elements of one content area: its marked children, or all of them. */
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* Give every element its class, its drawer and, while editing, the
|
|
1114
|
+
* contenteditable="false" that makes a rich text editor treat it as
|
|
1115
|
+
* one atomic thing rather than as text it may rewrite.
|
|
1116
|
+
*
|
|
1117
|
+
* The class is re-applied on every init rather than trusted to
|
|
1118
|
+
* survive: an editor that snapshots and restores the markup inside a
|
|
1119
|
+
* content area can drop it, and the marking that identifies an
|
|
1120
|
+
* element lives in a data attribute for exactly that reason.
|
|
1121
|
+
*/
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* What the info tool calls this element: its label, its type, or both.
|
|
1127
|
+
* An element found by elements.auto has neither, so it is named after
|
|
1128
|
+
* its tag, which is the only thing it has said about itself.
|
|
1129
|
+
*/
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* The container plugins this editor is using: the ones registered by
|
|
1133
|
+
* the files the page loaded, narrowed by the plugins setting.
|
|
1134
|
+
*
|
|
1135
|
+
* Each is a factory, called once here with the handle it works
|
|
1136
|
+
* through. Everything a plugin needs from the editor goes through
|
|
1137
|
+
* that handle, because the closure it runs outside of is not
|
|
1138
|
+
* something it can see.
|
|
1139
|
+
*/
|
|
1140
|
+
function loadPlugins() {
|
|
1141
|
+
var api = pluginApi();
|
|
1142
|
+
var wanted = function(name) {
|
|
1143
|
+
return !settings.plugins || settings.plugins.indexOf(name) !== -1;
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
$.each($.fn.gridEditor.containers, function(type, factory) {
|
|
1147
|
+
if (wanted(type)) { CONTAINERS[type] = factory(api); }
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
$.each($.fn.gridEditor.features, function(name, factory) {
|
|
1151
|
+
if (wanted(name)) { FEATURES[name] = factory(api); }
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1154
|
+
$.each(FEATURES, function(name, feature) {
|
|
1155
|
+
$.each(feature.methods || {}, function(method, implementation) {
|
|
1156
|
+
featureMethods[method] = implementation;
|
|
1157
|
+
});
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
(settings.plugins || []).forEach(function(name) {
|
|
1161
|
+
if (CONTAINERS[name] || FEATURES[name]) { return; }
|
|
1162
|
+
|
|
1163
|
+
warnOnceHere('plugin:' + name, 'the "' + name + '" plugin is not loaded: ' +
|
|
1164
|
+
'include dist/plugins/grideditor.' + name + '.js after the editor');
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* A hook every loaded plugin may have. Containers first, since a
|
|
1170
|
+
* feature that looks at the canvas - elements, say - wants the
|
|
1171
|
+
* containers already marked.
|
|
1172
|
+
*/
|
|
1173
|
+
function plugins(hook, argument) {
|
|
1174
|
+
$.each(CONTAINERS, function(type, definition) {
|
|
1175
|
+
if (definition[hook]) { definition[hook](argument); }
|
|
1176
|
+
});
|
|
1177
|
+
$.each(FEATURES, function(name, feature) {
|
|
1178
|
+
if (feature[hook]) { feature[hook](argument); }
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/** What a plugin is handed. See docs/plugins.md. */
|
|
1183
|
+
function pluginApi() {
|
|
1184
|
+
return {
|
|
1185
|
+
canvas: canvas,
|
|
1186
|
+
settings: settings,
|
|
1187
|
+
t: t,
|
|
1188
|
+
warn: warn,
|
|
1189
|
+
containerId: containerId,
|
|
1190
|
+
defaultRegion: defaultRegion,
|
|
1191
|
+
createTool: createTool,
|
|
1192
|
+
createMoveTool: createMoveTool,
|
|
1193
|
+
addSettingsTool: addSettingsTool,
|
|
1194
|
+
deleteNode: deleteNode,
|
|
1195
|
+
place: place,
|
|
1196
|
+
createPaneControls: createPaneControls,
|
|
1197
|
+
makeLabelEditable: makeLabelEditable,
|
|
1198
|
+
labelIn: labelIn,
|
|
1199
|
+
unwrapLabels: unwrapLabels,
|
|
1200
|
+
suspendToggles: suspendToggles,
|
|
1201
|
+
resumeToggles: resumeToggles,
|
|
1202
|
+
emit: emit,
|
|
1203
|
+
payloadFor: payloadFor,
|
|
1204
|
+
operate: operate,
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/* --------------------------------------------------------------
|
|
1209
|
+
* Containers: tabs, accordions and popups.
|
|
1210
|
+
*
|
|
1211
|
+
* A container holds panes, and a pane is an ordinary canvas region -
|
|
1212
|
+
* rows, columns, content areas and elements nest inside one exactly
|
|
1213
|
+
* as they do at the top level, because init() walks the whole canvas
|
|
1214
|
+
* and does not care how deep it is.
|
|
1215
|
+
*
|
|
1216
|
+
* What a container is, is said by data-ge-container. The ge-* classes
|
|
1217
|
+
* are editing furniture and come off with everything else, so a host
|
|
1218
|
+
* restyling the markup cannot break detection and getHtml stays clean.
|
|
1219
|
+
* -------------------------------------------------------------- */
|
|
1220
|
+
|
|
1221
|
+
var CONTAINERS = {}; // The container plugins in use, by the type each builds
|
|
1222
|
+
var FEATURES = {}; // The feature plugins in use, by name
|
|
1223
|
+
var featureMethods = {}; // The methods those features contribute
|
|
1224
|
+
var containerCounter = 0;
|
|
1225
|
+
|
|
1226
|
+
/**
|
|
1227
|
+
* Ids go into the markup rather than into jQuery data: Bootstrap's
|
|
1228
|
+
* toggles are written in terms of them, and the markup has to survive
|
|
1229
|
+
* getHtml with those toggles still pointing at the right panes.
|
|
1230
|
+
*/
|
|
1231
|
+
function containerId(type) {
|
|
1232
|
+
containerCounter++;
|
|
1233
|
+
|
|
1234
|
+
return 'ge-' + type + '-' + containerCounter + '-' +
|
|
1235
|
+
Math.random().toString(36).slice(2, 6);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/** A pane's starting content: one full width column, ready to edit. */
|
|
1239
|
+
function defaultRegion() {
|
|
1240
|
+
var row = createRow();
|
|
1241
|
+
createColumn(MAX_COL_SIZE).appendTo(row);
|
|
1242
|
+
return row;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function containerTypeOf(container) {
|
|
1246
|
+
return container.attr('data-ge-container');
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function markContainers() {
|
|
1250
|
+
canvas.find('[data-ge-container]').each(function() {
|
|
1251
|
+
var container = $(this);
|
|
1252
|
+
var type = containerTypeOf(container);
|
|
1253
|
+
var definition = CONTAINERS[type];
|
|
1254
|
+
|
|
1255
|
+
if (!definition) {
|
|
1256
|
+
warnOnceHere('container:' + type, 'unknown container type "' + type + '"');
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
container.addClass('ge-container ge-container-' + type);
|
|
1261
|
+
definition.mark(container);
|
|
1262
|
+
|
|
1263
|
+
if (!container.find('> .ge-tools-drawer').length) {
|
|
1264
|
+
createContainerControls(container, type, definition);
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function unmarkContainers() {
|
|
1270
|
+
canvas.find('[data-ge-container]').each(function() {
|
|
1271
|
+
var container = $(this);
|
|
1272
|
+
var definition = CONTAINERS[containerTypeOf(container)];
|
|
1273
|
+
|
|
1274
|
+
if (definition) { definition.unmark(container); }
|
|
1275
|
+
|
|
1276
|
+
container.removeClass('ge-container ge-container-' + containerTypeOf(container));
|
|
1277
|
+
if (!container.attr('class')) { container.removeAttr('class'); }
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function createContainerControls(container, type, definition) {
|
|
1282
|
+
var drawer = $('<div class="ge-tools-drawer ge-container-drawer" />').prependTo(container);
|
|
1283
|
+
|
|
1284
|
+
createMoveTool(drawer);
|
|
1285
|
+
addSettingsTool(drawer, container, settings.container_classes);
|
|
1286
|
+
if (definition.addPane) {
|
|
1287
|
+
createTool(drawer, t(definition.addPaneKey), 'ge-add-pane', 'bi bi-plus-circle', function() {
|
|
1288
|
+
var pane = definition.addPane(container, {});
|
|
1289
|
+
|
|
1290
|
+
addNode(definition.paneKind, pane, function() {}, {
|
|
1291
|
+
parent: container,
|
|
1292
|
+
source: 'tool',
|
|
1293
|
+
container: container,
|
|
1294
|
+
});
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
settings.container_tools.forEach(function(hostTool) {
|
|
1299
|
+
createTool(drawer, hostTool.title || '', hostTool.className || '',
|
|
1300
|
+
hostTool.iconClass || 'bi bi-wrench', hostTool.on);
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
if (definition.tools) { definition.tools(drawer, container); }
|
|
1304
|
+
|
|
1305
|
+
createTool(drawer, t('tool.delete_container'), 'ge-delete-container', 'bi bi-trash', function() {
|
|
1306
|
+
deleteNode(type, container, t('confirm.delete_container'), function(removed) {
|
|
1307
|
+
container.slideUp(removed);
|
|
1308
|
+
});
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* A pane drawer: small, inline, and made the same way for every
|
|
1314
|
+
* container type so a tab and an accordion item behave alike.
|
|
1315
|
+
*/
|
|
1316
|
+
function createPaneControls(pane, kind, hostTools, confirmText, remove) {
|
|
1317
|
+
var drawer = $('<div class="ge-tools-drawer ge-pane-drawer" />').prependTo(pane);
|
|
1318
|
+
|
|
1319
|
+
createMoveTool(drawer);
|
|
1320
|
+
addSettingsTool(drawer, pane, settings.pane_classes);
|
|
1321
|
+
|
|
1322
|
+
hostTools.forEach(function(hostTool) {
|
|
1323
|
+
createTool(drawer, hostTool.title || '', hostTool.className || '',
|
|
1324
|
+
hostTool.iconClass || 'bi bi-wrench', hostTool.on);
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
createTool(drawer, t('tool.delete_pane'), 'ge-delete-pane', 'bi bi-trash', function() {
|
|
1328
|
+
deleteNode(kind, pane, confirmText, function(removed) {
|
|
1329
|
+
remove(removed);
|
|
1330
|
+
});
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
return drawer;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* Take a Bootstrap toggle away from a node, and give it back later.
|
|
1338
|
+
*
|
|
1339
|
+
* Bootstrap binds its data-api handlers on the document in the
|
|
1340
|
+
* capture phase, so a listener on the node cannot stop one: the
|
|
1341
|
+
* document sees the click first. What does work is leaving nothing
|
|
1342
|
+
* for its selector to match, so the attribute is moved aside while
|
|
1343
|
+
* the editor needs the control not to react, and moved back on the
|
|
1344
|
+
* way out.
|
|
1345
|
+
*/
|
|
1346
|
+
function suspendToggles(scope) {
|
|
1347
|
+
scope.find('[data-bs-toggle], [data-bs-dismiss]').addBack('[data-bs-toggle], [data-bs-dismiss]')
|
|
1348
|
+
.each(function() {
|
|
1349
|
+
var node = $(this);
|
|
1350
|
+
|
|
1351
|
+
['toggle', 'dismiss'].forEach(function(name) {
|
|
1352
|
+
var value = node.attr('data-bs-' + name);
|
|
1353
|
+
if (value === undefined) { return; }
|
|
1354
|
+
|
|
1355
|
+
node.attr('data-ge-bs-' + name, value).removeAttr('data-bs-' + name);
|
|
1356
|
+
});
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function resumeToggles(scope) {
|
|
1361
|
+
scope.find('[data-ge-bs-toggle], [data-ge-bs-dismiss]').addBack('[data-ge-bs-toggle], [data-ge-bs-dismiss]')
|
|
1362
|
+
.each(function() {
|
|
1363
|
+
var node = $(this);
|
|
1364
|
+
|
|
1365
|
+
['toggle', 'dismiss'].forEach(function(name) {
|
|
1366
|
+
var value = node.attr('data-ge-bs-' + name);
|
|
1367
|
+
if (value === undefined) { return; }
|
|
1368
|
+
|
|
1369
|
+
node.attr('data-bs-' + name, value).removeAttr('data-ge-bs-' + name);
|
|
1370
|
+
});
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/**
|
|
1375
|
+
* Rename a label in place. The label sits inside the button Bootstrap
|
|
1376
|
+
* toggles from, so the toggle is suspended while the label is being
|
|
1377
|
+
* edited: typing in a tab's name must not switch tabs.
|
|
1378
|
+
*/
|
|
1379
|
+
function makeLabelEditable(label) {
|
|
1380
|
+
if (label.data('ge-editable')) { return; }
|
|
1381
|
+
|
|
1382
|
+
var toggle = label.closest('[data-bs-toggle], [data-ge-bs-toggle]');
|
|
1383
|
+
|
|
1384
|
+
label.data('ge-editable', true)
|
|
1385
|
+
.attr('title', t('tool.rename'))
|
|
1386
|
+
.on('dblclick', function(e) {
|
|
1387
|
+
e.preventDefault();
|
|
1388
|
+
e.stopPropagation();
|
|
1389
|
+
|
|
1390
|
+
suspendToggles(toggle);
|
|
1391
|
+
label.attr('contenteditable', 'true').trigger('focus');
|
|
1392
|
+
window.getSelection().selectAllChildren(label[0]);
|
|
1393
|
+
})
|
|
1394
|
+
.on('keydown', function(e) {
|
|
1395
|
+
if (label.attr('contenteditable') !== 'true') { return; }
|
|
1396
|
+
|
|
1397
|
+
if (e.key === 'Enter') {
|
|
1398
|
+
e.preventDefault();
|
|
1399
|
+
label.trigger('blur');
|
|
1400
|
+
}
|
|
1401
|
+
})
|
|
1402
|
+
.on('blur', function() {
|
|
1403
|
+
label.removeAttr('contenteditable');
|
|
1404
|
+
resumeToggles(toggle);
|
|
1405
|
+
})
|
|
1406
|
+
;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
/** Exactly one tab is the active one, in the strip and in the content. */
|
|
1411
|
+
|
|
1412
|
+
/**
|
|
1413
|
+
* An accordion whose items carry no data-bs-parent is one Bootstrap
|
|
1414
|
+
* lets you open several items in at once. The markup is the state:
|
|
1415
|
+
* there is nothing else to remember it in.
|
|
1416
|
+
*/
|
|
1417
|
+
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* An item dropped into another accordion collapses against the one it
|
|
1421
|
+
* landed in, and takes on that accordion's idea of whether several
|
|
1422
|
+
* items may be open at once.
|
|
1423
|
+
*/
|
|
1424
|
+
/**
|
|
1425
|
+
* Open or close an item while editing.
|
|
1426
|
+
*
|
|
1427
|
+
* The editor does this itself rather than letting Bootstrap's collapse
|
|
1428
|
+
* run over the canvas, and it writes what it does to data-ge-open, so
|
|
1429
|
+
* what is left open here is what the authored page opens with. An
|
|
1430
|
+
* accordion that closes its siblings - one without stay_open - closes
|
|
1431
|
+
* them here too, because the canvas is meant to look like the page.
|
|
1432
|
+
*/
|
|
1433
|
+
|
|
1434
|
+
/** One item's state, in the attribute and in Bootstrap's own classes. */
|
|
1435
|
+
|
|
1436
|
+
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* A trigger is any node the host marked with data-ge-popup-target,
|
|
1441
|
+
* plus the button the container makes for itself. Ids go stale - a
|
|
1442
|
+
* popup deleted, a trigger pasted from another page - so what can be
|
|
1443
|
+
* repaired is repaired, and the rest is reported rather than removed.
|
|
1444
|
+
* Grid-editor never deletes a node the host wrote.
|
|
1445
|
+
*/
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* On the way out, every trigger gets the attributes that make
|
|
1449
|
+
* Bootstrap open the modal in the authored page. An orphan gets
|
|
1450
|
+
* nothing, because there is nothing to point it at.
|
|
1451
|
+
*/
|
|
1452
|
+
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* What each container type is made of, and what has to happen to one
|
|
1456
|
+
* while it is being edited. Everything type specific lives here; the
|
|
1457
|
+
* core above treats them all the same.
|
|
1458
|
+
*/
|
|
1459
|
+
/** The label inside a pane's button, wrapped so it can be edited alone. */
|
|
1460
|
+
function labelIn(button) {
|
|
1461
|
+
var label = button.find('> .ge-pane-label');
|
|
1462
|
+
|
|
1463
|
+
if (!label.length) {
|
|
1464
|
+
label = $('<span class="ge-pane-label" />').text(button.text().trim());
|
|
1465
|
+
button.empty().append(label);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
return label;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function unwrapLabels(scope) {
|
|
1472
|
+
scope.find('.ge-pane-label').each(function() {
|
|
1473
|
+
$(this).removeData('ge-editable').contents().unwrap();
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* Add a column of `size` to a row, through the add events.
|
|
1479
|
+
*/
|
|
1480
|
+
function addColumnTo(row, size) {
|
|
1481
|
+
var column = createColumn(size);
|
|
1482
|
+
|
|
1483
|
+
return addNode('column', column, function() {
|
|
1484
|
+
row.append(column);
|
|
1485
|
+
}, { parent: row, source: 'tool' });
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* Holding the add column tool offers the sizes instead of taking the
|
|
1490
|
+
* default one.
|
|
1491
|
+
*
|
|
1492
|
+
* Held rather than hovered alone, because a hover is a gesture a touch
|
|
1493
|
+
* screen does not have, and the tooltip says so: a gesture nobody can
|
|
1494
|
+
* see is a gesture nobody finds.
|
|
1495
|
+
*/
|
|
1496
|
+
function attachSizePicker(tool, row) {
|
|
1497
|
+
if (!settings.add_column.picker) { return; }
|
|
1498
|
+
|
|
1499
|
+
var timer = null;
|
|
1500
|
+
|
|
1501
|
+
var cancel = function() {
|
|
1502
|
+
window.clearTimeout(timer);
|
|
1503
|
+
timer = null;
|
|
1504
|
+
};
|
|
1505
|
+
|
|
1506
|
+
tool.on('mouseenter mousedown', function() {
|
|
1507
|
+
if (timer || sizePicker) { return; }
|
|
1508
|
+
|
|
1509
|
+
timer = window.setTimeout(function() {
|
|
1510
|
+
timer = null;
|
|
1511
|
+
openSizePicker(tool, row);
|
|
1512
|
+
}, settings.add_column.delay);
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
tool.on('mouseleave', cancel);
|
|
1516
|
+
tool.on('mouseup', cancel);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* The sizes a column may be given, as a strip under the tool. Sizes
|
|
1521
|
+
* that do not fit what is left of the row are marked, not withheld:
|
|
1522
|
+
* a row is allowed to wrap, and that is the host's page to lay out.
|
|
1523
|
+
*/
|
|
1524
|
+
function openSizePicker(tool, row) {
|
|
1525
|
+
closeSizePicker();
|
|
1526
|
+
|
|
1527
|
+
var room = spare(row, leadingTier());
|
|
1528
|
+
|
|
1529
|
+
// The drawer it hangs off is raised while it is open: every
|
|
1530
|
+
// drawer sits above jQuery UI's handles, so without this the
|
|
1531
|
+
// drawer of the column below takes the clicks meant for the picker
|
|
1532
|
+
tool.closest('.ge-tools-drawer').addClass('ge-picker-open');
|
|
1533
|
+
|
|
1534
|
+
sizePicker = $('<div class="ge-size-picker" />').appendTo(tool);
|
|
1535
|
+
|
|
1536
|
+
settings.valid_col_sizes.forEach(function(size) {
|
|
1537
|
+
$('<a class="ge-size" />')
|
|
1538
|
+
.attr('data-ge-size', size)
|
|
1539
|
+
.attr('title', t('tool.column_size', { size: size }))
|
|
1540
|
+
.toggleClass('ge-size-tight', size > room)
|
|
1541
|
+
.text(size)
|
|
1542
|
+
.on('click', function(e) {
|
|
1543
|
+
e.preventDefault();
|
|
1544
|
+
e.stopPropagation();
|
|
1545
|
+
|
|
1546
|
+
closeSizePicker();
|
|
1547
|
+
addColumnTo(row, size);
|
|
1548
|
+
})
|
|
1549
|
+
.appendTo(sizePicker)
|
|
1550
|
+
;
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
// Anywhere else, and the question is withdrawn
|
|
1554
|
+
tool.one('mouseleave', function() {
|
|
1555
|
+
window.setTimeout(function() {
|
|
1556
|
+
if (sizePicker && !sizePicker.is(':hover')) { closeSizePicker(); }
|
|
1557
|
+
}, 400);
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
/** True when there was one to close, which is also a click's answer. */
|
|
1562
|
+
function closeSizePicker() {
|
|
1563
|
+
if (!sizePicker) { return false; }
|
|
1564
|
+
|
|
1565
|
+
sizePicker.closest('.ge-tools-drawer').removeClass('ge-picker-open');
|
|
1566
|
+
sizePicker.remove();
|
|
1567
|
+
sizePicker = null;
|
|
1568
|
+
|
|
1569
|
+
return true;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
function createRowControls() {
|
|
1573
|
+
canvas.find('.row').each(function() {
|
|
1574
|
+
var row = $(this);
|
|
1575
|
+
if (row.find('> .ge-tools-drawer').length) { return; }
|
|
1576
|
+
|
|
1577
|
+
var drawer = $('<div class="ge-tools-drawer" />').prependTo(row);
|
|
1578
|
+
createMoveTool(drawer);
|
|
1579
|
+
addSettingsTool(drawer, row, settings.row_classes);
|
|
1580
|
+
|
|
1581
|
+
settings.row_tools.forEach(function(hostTool) {
|
|
1582
|
+
createTool(drawer, hostTool.title || '', hostTool.className || '',
|
|
1583
|
+
hostTool.iconClass || 'bi bi-wrench', hostTool.on);
|
|
1584
|
+
});
|
|
1585
|
+
createTool(drawer, t('tool.delete_row'), 'ge-delete-row', 'bi bi-trash', function() {
|
|
1586
|
+
deleteNode('row', row, t('confirm.delete_row'), function(removed) {
|
|
1587
|
+
row.slideUp(removed);
|
|
1588
|
+
});
|
|
1589
|
+
});
|
|
1590
|
+
createTool(drawer, t('tool.add_column'), 'ge-add-column', 'bi bi-plus-circle', function() {
|
|
1591
|
+
if (closeSizePicker()) { return; } // The picker was open: that was the answer
|
|
1592
|
+
|
|
1593
|
+
addColumnTo(row, settings.add_column.size);
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
attachSizePicker(drawer.find('> .ge-add-column'), row);
|
|
1597
|
+
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
function createColControls() {
|
|
1602
|
+
canvas.find('.column').each(function() {
|
|
1603
|
+
var col = $(this);
|
|
1604
|
+
if (col.find('> .ge-tools-drawer').length) { return; }
|
|
1605
|
+
|
|
1606
|
+
var drawer = $('<div class="ge-tools-drawer" />').prependTo(col);
|
|
1607
|
+
|
|
1608
|
+
createMoveTool(drawer);
|
|
1609
|
+
|
|
1610
|
+
createTool(drawer, t('tool.column_narrower'), 'ge-decrease-col-width', 'bi bi-dash-lg', function(e) {
|
|
1611
|
+
resizeColumn(col, e.shiftKey
|
|
1612
|
+
? smallest(settings.valid_col_sizes)
|
|
1613
|
+
: stepThrough(settings.valid_col_sizes, currentSize(col), -1),
|
|
1614
|
+
'tool');
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
createTool(drawer, t('tool.column_wider'), 'ge-increase-col-width', 'bi bi-plus-lg', function(e) {
|
|
1618
|
+
resizeColumn(col, e.shiftKey ? widestFor(col) : stepThrough(settings.valid_col_sizes, currentSize(col), 1), 'tool');
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
createTool(drawer, t('tool.indent_decrease'), 'ge-decrease-col-offset', 'bi bi-text-indent-right', function(e) {
|
|
1622
|
+
indentColumn(col, e.shiftKey
|
|
1623
|
+
? smallest(settings.valid_col_offsets)
|
|
1624
|
+
: stepThrough(settings.valid_col_offsets, currentOffset(col), -1),
|
|
1625
|
+
'tool');
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
createTool(drawer, t('tool.indent_increase'), 'ge-increase-col-offset', 'bi bi-text-indent-left', function(e) {
|
|
1629
|
+
indentColumn(col, e.shiftKey ? deepestFor(col) : stepThrough(settings.valid_col_offsets, currentOffset(col), 1), 'tool');
|
|
1630
|
+
});
|
|
1631
|
+
|
|
1632
|
+
addSettingsTool(drawer, col, settings.col_classes);
|
|
1633
|
+
|
|
1634
|
+
settings.col_tools.forEach(function(hostTool) {
|
|
1635
|
+
createTool(drawer, hostTool.title || '', hostTool.className || '',
|
|
1636
|
+
hostTool.iconClass || 'bi bi-wrench', hostTool.on);
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
createTool(drawer, t('tool.delete_column'), 'ge-delete-column', 'bi bi-trash', function() {
|
|
1640
|
+
deleteNode('column', col, t('confirm.delete_column'), function(removed) {
|
|
1641
|
+
col.animate({
|
|
1642
|
+
opacity: 'hide',
|
|
1643
|
+
width: 'hide',
|
|
1644
|
+
height: 'hide'
|
|
1645
|
+
}, 400, removed);
|
|
1646
|
+
});
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
createTool(drawer, t('tool.add_row'), 'ge-add-row', 'bi bi-plus-circle', function() {
|
|
1650
|
+
// An empty row: the columns in it are the next decision,
|
|
1651
|
+
// and its drawer's add column tool is where that is made
|
|
1652
|
+
var row = createRow();
|
|
1653
|
+
|
|
1654
|
+
addNode('row', row, function() {
|
|
1655
|
+
col.append(row);
|
|
1656
|
+
}, { parent: col, source: 'tool' });
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
/**
|
|
1663
|
+
* The tier the tools read when they need one number.
|
|
1664
|
+
*
|
|
1665
|
+
* In a per-breakpoint view that is the tier being edited. In the all
|
|
1666
|
+
* view it is the widest tier, because the canvas is not constrained
|
|
1667
|
+
* there and the widest tier is what the user is looking at: clicking
|
|
1668
|
+
* "narrower" on a column authored as col-lg-6 should take it to 5,
|
|
1669
|
+
* not to 11 because no xs class was ever written.
|
|
1670
|
+
*/
|
|
1671
|
+
function leadingTier() {
|
|
1672
|
+
return curView === ALL_VIEW ? BREAKPOINTS[BREAKPOINTS.length - 1] : breakpoint(curView);
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
function currentSize(col) {
|
|
1676
|
+
var size = getEffectiveSize(col, leadingTier());
|
|
1677
|
+
return size === null ? MAX_COL_SIZE : size;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
function currentOffset(col) {
|
|
1681
|
+
return getEffectiveOffset(col, leadingTier()) || 0;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
/** The next value a tool moves to, one step along the allowed list. */
|
|
1685
|
+
function stepThrough(values, from, direction) {
|
|
1686
|
+
var index = values.indexOf(from);
|
|
1687
|
+
|
|
1688
|
+
if (index === -1) {
|
|
1689
|
+
// A value the host did not allow: step to the nearest one it did
|
|
1690
|
+
return values.reduce(function(best, value) {
|
|
1691
|
+
return Math.abs(value - from) < Math.abs(best - from) ? value : best;
|
|
1692
|
+
}, values[0]);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
return values[Math.min(Math.max(index + direction, 0), values.length - 1)];
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
function smallest(values) {
|
|
1699
|
+
return values.reduce(function(a, b) { return Math.min(a, b); }, MAX_COL_SIZE);
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function largest(values) {
|
|
1703
|
+
return values.reduce(function(a, b) { return Math.max(a, b); }, 0);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
/**
|
|
1707
|
+
* The widest this column can be: everything the row has left, minus
|
|
1708
|
+
* its own indent. What "hold shift for max" means.
|
|
1709
|
+
*/
|
|
1710
|
+
function widestFor(col) {
|
|
1711
|
+
var room = spare(col.parent(), leadingTier(), col) - currentOffset(col);
|
|
1712
|
+
|
|
1713
|
+
return Math.min(largest(settings.valid_col_sizes), Math.max(room, 1));
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/**
|
|
1717
|
+
* The deepest this column can be indented and still have a unit of
|
|
1718
|
+
* itself left inside the row.
|
|
1719
|
+
*/
|
|
1720
|
+
function deepestFor(col) {
|
|
1721
|
+
var room = spare(col.parent(), leadingTier(), col) - currentSize(col);
|
|
1722
|
+
|
|
1723
|
+
return Math.min(largest(settings.valid_col_offsets), Math.max(room, 0));
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
/**
|
|
1727
|
+
* The move tool, unless the whole drawer is the handle - in which case
|
|
1728
|
+
* a tool that only says "drag from here" is one tool too many.
|
|
1729
|
+
*/
|
|
1730
|
+
function createMoveTool(drawer) {
|
|
1731
|
+
if (settings.drag_handle === 'drawer') { return; }
|
|
1732
|
+
|
|
1733
|
+
createTool(drawer, t('tool.move'), 'ge-move', 'bi bi-arrows-move');
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
function createTool(drawer, title, className, iconClass, eventHandlers) {
|
|
1737
|
+
var tool = $('<a title="' + title + '" class="' + className + '"><i class="' + iconClass + '"></i></a>')
|
|
1738
|
+
.appendTo(drawer)
|
|
1739
|
+
;
|
|
1740
|
+
if (typeof eventHandlers == 'function') {
|
|
1741
|
+
tool.on('click', eventHandlers);
|
|
1742
|
+
}
|
|
1743
|
+
if (typeof eventHandlers == 'object') {
|
|
1744
|
+
$.each(eventHandlers, function(name, func) {
|
|
1745
|
+
tool.on(name, func);
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
/**
|
|
1751
|
+
* The classes on a node that are the host's own, rather than the ones
|
|
1752
|
+
* the grid and the editor put there. What the settings panel shows,
|
|
1753
|
+
* and the only ones it is allowed to take away.
|
|
1754
|
+
*/
|
|
1755
|
+
function hostClasses(node) {
|
|
1756
|
+
return (node.attr('class') || '').split(/\s+/).filter(function(name) {
|
|
1757
|
+
return name !== '' && !isEditorClass(name);
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
function isEditorClass(name) {
|
|
1762
|
+
if (name === 'row' || name === 'column') { return true; }
|
|
1763
|
+
if (/^(ge-|ui-)/.test(name)) { return true; }
|
|
1764
|
+
|
|
1765
|
+
return BREAKPOINTS.some(function(tier) {
|
|
1766
|
+
return new RegExp('^(' + tier.colPrefix + '|' + tier.offsetPrefix + ')\\d+$').test(name);
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function setHostClasses(node, value) {
|
|
1771
|
+
hostClasses(node).forEach(function(name) { node.removeClass(name); });
|
|
1772
|
+
|
|
1773
|
+
value.split(/\s+/).forEach(function(name) {
|
|
1774
|
+
if (name !== '') { node.addClass(name); }
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
if (!node.attr('class')) { node.removeAttr('class'); }
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
/**
|
|
1781
|
+
* The gear and the panel it opens: the node's id, its css classes, and
|
|
1782
|
+
* whatever preset toggles the host configured for that kind of node.
|
|
1783
|
+
*/
|
|
1784
|
+
function addSettingsTool(drawer, node, presets) {
|
|
1785
|
+
var details = createDetails(node, presets || []);
|
|
1786
|
+
|
|
1787
|
+
createTool(drawer, t('tool.settings'), 'ge-settings', 'bi bi-gear-fill', function() {
|
|
1788
|
+
details.toggle();
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
return details.appendTo(drawer);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
function createDetails(container, cssClasses) {
|
|
1795
|
+
var detailsDiv = $('<div class="ge-details" />');
|
|
1796
|
+
|
|
1797
|
+
$('<input class="ge-id" />')
|
|
1798
|
+
.attr('placeholder', t('tool.id_placeholder'))
|
|
1799
|
+
.val(container.attr('id'))
|
|
1800
|
+
.attr('title', t('tool.id_title'))
|
|
1801
|
+
.appendTo(detailsDiv)
|
|
1802
|
+
.on('change', function() {
|
|
1803
|
+
// An empty field means no id, not an empty one
|
|
1804
|
+
if (this.value === '') {
|
|
1805
|
+
container.removeAttr('id');
|
|
1806
|
+
} else {
|
|
1807
|
+
container.attr('id', this.value);
|
|
1808
|
+
}
|
|
1809
|
+
})
|
|
1810
|
+
;
|
|
1811
|
+
|
|
1812
|
+
$('<input class="ge-classes" />')
|
|
1813
|
+
.attr('placeholder', t('tool.classes_placeholder'))
|
|
1814
|
+
.attr('title', t('tool.classes_title'))
|
|
1815
|
+
.val(hostClasses(container).join(' '))
|
|
1816
|
+
.appendTo(detailsDiv)
|
|
1817
|
+
.on('change', function() {
|
|
1818
|
+
setHostClasses(container, this.value);
|
|
1819
|
+
})
|
|
1820
|
+
;
|
|
1821
|
+
|
|
1822
|
+
var classGroup = $('<div class="btn-group" />').appendTo(detailsDiv);
|
|
1823
|
+
cssClasses.forEach(function(rowClass) {
|
|
1824
|
+
var btn = $('<a class="btn btn-sm btn-default" />')
|
|
1825
|
+
.html(rowClass.label)
|
|
1826
|
+
.attr('title', rowClass.title ? rowClass.title : t('tool.toggle_class', { label: rowClass.label }))
|
|
1827
|
+
.toggleClass('active btn-primary', container.hasClass(rowClass.cssClass))
|
|
1828
|
+
.on('click', function() {
|
|
1829
|
+
btn.toggleClass('active btn-primary');
|
|
1830
|
+
container.toggleClass(rowClass.cssClass, btn.hasClass('active'));
|
|
1831
|
+
})
|
|
1832
|
+
.appendTo(classGroup)
|
|
1833
|
+
;
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
return detailsDiv;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
/**
|
|
1840
|
+
* Make sure every column is marked as one, and that a column with no
|
|
1841
|
+
* sizing at all gets some.
|
|
1842
|
+
*
|
|
1843
|
+
* Deliberately conservative: a column that carries any size class is
|
|
1844
|
+
* left exactly as authored. Seeding every tier would put six classes
|
|
1845
|
+
* on every column now that there are six tiers, and the smallest tier
|
|
1846
|
+
* already applies to the wider ones, so one class is enough for a
|
|
1847
|
+
* column that had none.
|
|
1848
|
+
*/
|
|
1849
|
+
function addAllColClasses() {
|
|
1850
|
+
canvas.find('.column, div[class*="col-"]').each(function() {
|
|
1851
|
+
var col = $(this).addClass('column');
|
|
1852
|
+
|
|
1853
|
+
if (sizedTiers(col).length) { return; }
|
|
1854
|
+
|
|
1855
|
+
setSize(col, BREAKPOINTS[0], MAX_COL_SIZE);
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
/* --------------------------------------------------------------
|
|
1860
|
+
* The sizing core.
|
|
1861
|
+
*
|
|
1862
|
+
* Everything that reads or writes a size or an offset class goes
|
|
1863
|
+
* through here: the width and indent tools, drag resize,
|
|
1864
|
+
* createColumn and the getHtml cleanup. One place owns the class
|
|
1865
|
+
* names, the 12 unit budget and what an absent class means.
|
|
1866
|
+
* -------------------------------------------------------------- */
|
|
1867
|
+
|
|
1868
|
+
/** The units a column is given at one tier, or null if that tier says nothing. */
|
|
1869
|
+
function getSize(col, tier) {
|
|
1870
|
+
return readUnits(col, tier.colPrefix);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
/** The units a column is indented by at one tier, or null. */
|
|
1874
|
+
function getOffset(col, tier) {
|
|
1875
|
+
return readUnits(col, tier.offsetPrefix);
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
/**
|
|
1879
|
+
* What actually applies at a tier: its own class, or the nearest
|
|
1880
|
+
* smaller tier that has one, because that is how Bootstrap cascades.
|
|
1881
|
+
* Null when no tier below it says anything either.
|
|
1882
|
+
*
|
|
1883
|
+
* Not a "return the first thing I found" fallback: it walks the tiers
|
|
1884
|
+
* downward from the one asked about, so an offset lookup can never
|
|
1885
|
+
* answer with a size, and a lookup for one tier can never answer with
|
|
1886
|
+
* a wider tier's value.
|
|
1887
|
+
*/
|
|
1888
|
+
function getEffectiveSize(col, tier) {
|
|
1889
|
+
return readEffective(col, tier, getSize);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
function getEffectiveOffset(col, tier) {
|
|
1893
|
+
return readEffective(col, tier, getOffset);
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
function readEffective(col, tier, read) {
|
|
1897
|
+
for (var i = BREAKPOINTS.indexOf(tier); i >= 0; i--) {
|
|
1898
|
+
var units = read(col, BREAKPOINTS[i]);
|
|
1899
|
+
if (units !== null) { return units; }
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
function readUnits(col, prefix) {
|
|
1906
|
+
var match = new RegExp('(?:^|\\s)' + prefix + '(\\d+)(?:\\s|$)').exec(col.attr('class') || '');
|
|
1907
|
+
return match ? parseInt(match[1], 10) : null;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
function writeUnits(col, prefix, units) {
|
|
1911
|
+
var classes = (col.attr('class') || '').split(/\s+/).filter(function(name) {
|
|
1912
|
+
return name !== '' && !new RegExp('^' + prefix + '\\d+$').test(name);
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1915
|
+
if (units !== null) { classes.push(prefix + units); }
|
|
1916
|
+
|
|
1917
|
+
col.attr('class', classes.join(' '));
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
function setSize(col, tier, units) {
|
|
1921
|
+
writeUnits(col, tier.colPrefix, units);
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
/** An offset of 0 is written as no class at all, which is what it means. */
|
|
1925
|
+
function setOffset(col, tier, units) {
|
|
1926
|
+
writeUnits(col, tier.offsetPrefix, units ? units : null);
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
/** The tiers a column carries an explicit size for. */
|
|
1930
|
+
function sizedTiers(col) {
|
|
1931
|
+
return BREAKPOINTS.filter(function(tier) {
|
|
1932
|
+
return getSize(col, tier) !== null;
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
/**
|
|
1937
|
+
* The units left in a row at one tier, counting every sibling's size
|
|
1938
|
+
* and offset. What "hold shift for max" grows into.
|
|
1939
|
+
*/
|
|
1940
|
+
function spare(row, tier, ignore) {
|
|
1941
|
+
var used = 0;
|
|
1942
|
+
|
|
1943
|
+
row.children('.column').each(function() {
|
|
1944
|
+
var sibling = $(this);
|
|
1945
|
+
if (ignore && sibling[0] === ignore[0]) { return; }
|
|
1946
|
+
|
|
1947
|
+
used += (getEffectiveSize(sibling, tier) || 0) + (getEffectiveOffset(sibling, tier) || 0);
|
|
1948
|
+
});
|
|
1949
|
+
|
|
1950
|
+
return MAX_COL_SIZE - used;
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
/**
|
|
1954
|
+
* The 12 unit budget, in one place: a column's size plus its offset
|
|
1955
|
+
* never exceeds 12 at any tier.
|
|
1956
|
+
*
|
|
1957
|
+
* Which of the two gives way is the caller's decision, expressed by
|
|
1958
|
+
* which one it marks as leading. Growing an offset shrinks the column;
|
|
1959
|
+
* growing a column with no room left is refused, so the tool visibly
|
|
1960
|
+
* does nothing rather than quietly rewriting an offset the user set.
|
|
1961
|
+
*/
|
|
1962
|
+
function clamp(request) {
|
|
1963
|
+
var size = request.size === null || request.size === undefined ? null : request.size;
|
|
1964
|
+
var offset = request.offset === null || request.offset === undefined ? 0 : request.offset;
|
|
1965
|
+
|
|
1966
|
+
offset = Math.min(Math.max(offset, 0), MAX_COL_OFFSET);
|
|
1967
|
+
|
|
1968
|
+
if (size === null) { return { size: null, offset: offset, refused: false }; }
|
|
1969
|
+
|
|
1970
|
+
size = Math.min(Math.max(size, 1), MAX_COL_SIZE);
|
|
1971
|
+
|
|
1972
|
+
if (size + offset <= MAX_COL_SIZE) {
|
|
1973
|
+
return { size: size, offset: offset, refused: false };
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
if (request.leading === 'offset') {
|
|
1977
|
+
return { size: MAX_COL_SIZE - offset, offset: offset, refused: false };
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
return { size: null, offset: offset, refused: true };
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
/**
|
|
1984
|
+
* Drag resize leaves an inline pixel width behind, and jQuery UI adds
|
|
1985
|
+
* its own. Neither belongs in the markup a host saves, or in the
|
|
1986
|
+
* canvas once the size class has been written.
|
|
1987
|
+
*/
|
|
1988
|
+
function stripPixelWidths(scope) {
|
|
1989
|
+
scope.find('.column').addBack('.column').each(function() {
|
|
1990
|
+
var col = $(this);
|
|
1991
|
+
|
|
1992
|
+
col.css({ width: '', height: '', left: '', top: '' });
|
|
1993
|
+
|
|
1994
|
+
// Clearing the last property leaves style="" behind, which is
|
|
1995
|
+
// an editor leftover like any other
|
|
1996
|
+
if (!col.attr('style')) { col.removeAttr('style'); }
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
function makeSortable() {
|
|
2001
|
+
var wholeDrawer = settings.drag_handle === 'drawer';
|
|
2002
|
+
|
|
2003
|
+
var shared = {
|
|
2004
|
+
handle: wholeDrawer ? '> .ge-tools-drawer' : '> .ge-tools-drawer .ge-move',
|
|
2005
|
+
|
|
2006
|
+
// With the whole drawer as the handle, the tools inside it are
|
|
2007
|
+
// still tools: a drag starting on one would swallow its click,
|
|
2008
|
+
// and the settings panel has fields to type in
|
|
2009
|
+
cancel: wholeDrawer
|
|
2010
|
+
? '.ge-tools-drawer > a, .ge-details, input, textarea, button, select, option'
|
|
2011
|
+
: 'input, textarea, button, select, option',
|
|
2012
|
+
|
|
2013
|
+
start: sortStart,
|
|
2014
|
+
stop: sortStop,
|
|
2015
|
+
helper: 'clone',
|
|
2016
|
+
};
|
|
2017
|
+
|
|
2018
|
+
canvas.find('.row').sortable($.extend({
|
|
2019
|
+
items: '> .column',
|
|
2020
|
+
connectWith: '.ge-canvas .row',
|
|
2021
|
+
tolerance: 'pointer',
|
|
2022
|
+
}, shared, settings.sortable_options));
|
|
2023
|
+
|
|
2024
|
+
canvas.add(canvas.find('.column')).sortable($.extend({
|
|
2025
|
+
items: '> .row, > .ge-content',
|
|
2026
|
+
connectWith: '.ge-canvas, .ge-canvas .column',
|
|
2027
|
+
}, shared, settings.sortable_options));
|
|
2028
|
+
|
|
2029
|
+
// A tab strip sorts its own tabs, and the panes follow them
|
|
2030
|
+
canvas.find('.ge-container-tabs > .nav-tabs').sortable($.extend({
|
|
2031
|
+
items: '> .ge-tab',
|
|
2032
|
+
}, shared, settings.sortable_options));
|
|
2033
|
+
|
|
2034
|
+
// Accordion items sort within their accordion and into any other
|
|
2035
|
+
canvas.find('.ge-container-accordion > .accordion').sortable($.extend({
|
|
2036
|
+
items: '> .ge-accordion-item',
|
|
2037
|
+
connectWith: '.ge-canvas .ge-container-accordion > .accordion',
|
|
2038
|
+
}, shared, settings.sortable_options));
|
|
2039
|
+
|
|
2040
|
+
// A plugin makes its own: only it knows which of its parts move
|
|
2041
|
+
plugins('onSortable', shared);
|
|
2042
|
+
|
|
2043
|
+
/**
|
|
2044
|
+
* jQuery UI cannot refuse a drag once it has started, so a
|
|
2045
|
+
* canceled before-move is remembered here and undone on drop
|
|
2046
|
+
* (spec 2.4). The node carries the mark, because with connected
|
|
2047
|
+
* lists the drop is not always reported by the list that started
|
|
2048
|
+
* the drag.
|
|
2049
|
+
*/
|
|
2050
|
+
function sortStart(e, ui) {
|
|
2051
|
+
ui.placeholder.css({ height: ui.item.outerHeight()});
|
|
2052
|
+
|
|
2053
|
+
var node = ui.item;
|
|
2054
|
+
var from = positionOf(node);
|
|
2055
|
+
|
|
2056
|
+
node.data('ge-move-from', from);
|
|
2057
|
+
node.removeData('ge-move-canceled');
|
|
2058
|
+
|
|
2059
|
+
operate(function() {
|
|
2060
|
+
var moving = emit('before-move', payloadFor(kindOf(node), node, {
|
|
2061
|
+
parent: from.parent,
|
|
2062
|
+
source: 'dragdrop',
|
|
2063
|
+
from: from,
|
|
2064
|
+
}));
|
|
2065
|
+
|
|
2066
|
+
if (!moving) { node.data('ge-move-canceled', true); }
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
function sortStop(e, ui) {
|
|
2071
|
+
var node = ui.item;
|
|
2072
|
+
var from = node.data('ge-move-from') || positionOf(node);
|
|
2073
|
+
|
|
2074
|
+
node.removeData('ge-move-from');
|
|
2075
|
+
|
|
2076
|
+
if (node.data('ge-move-canceled')) {
|
|
2077
|
+
node.removeData('ge-move-canceled');
|
|
2078
|
+
$(this).sortable('cancel');
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
var to = positionOf(node);
|
|
2083
|
+
if (to.parent[0] === from.parent[0] && to.index === from.index) {
|
|
2084
|
+
return; // A drag that went nowhere is not a move
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
var container = node.closest('[data-ge-container]');
|
|
2088
|
+
var definition = CONTAINERS[containerTypeOf(container)];
|
|
2089
|
+
if (definition && definition.afterPaneMove) {
|
|
2090
|
+
definition.afterPaneMove(container, node, from);
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
// No init() here, unlike an add: the node brought its drawer
|
|
2094
|
+
// with it, and jQuery UI is still finishing the drag, so this
|
|
2095
|
+
// is the wrong moment to rebuild the widgets it is using.
|
|
2096
|
+
operate(function() {
|
|
2097
|
+
emit('after-move', payloadFor(kindOf(node), node, {
|
|
2098
|
+
parent: to.parent,
|
|
2099
|
+
source: 'dragdrop',
|
|
2100
|
+
from: from,
|
|
2101
|
+
to: to,
|
|
2102
|
+
container: container.length ? container : undefined,
|
|
2103
|
+
}));
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
/**
|
|
2109
|
+
* Resizing a column by dragging its edge.
|
|
2110
|
+
*
|
|
2111
|
+
* The handle sits on the column's edge and the sort handle is the
|
|
2112
|
+
* drawer, so the two gestures never fight over the same pixels. The
|
|
2113
|
+
* column follows the pointer in pixels while dragging, the drawer says
|
|
2114
|
+
* which class it would land on, and the pixels are snapped to whole
|
|
2115
|
+
* units and thrown away on drop.
|
|
2116
|
+
*/
|
|
2117
|
+
function makeResizable() {
|
|
2118
|
+
if (!settings.resize.enabled) { return; }
|
|
2119
|
+
|
|
2120
|
+
canvas.find('.column').each(function() {
|
|
2121
|
+
var col = $(this);
|
|
2122
|
+
if (col.data('ui-resizable')) { return; }
|
|
2123
|
+
|
|
2124
|
+
$('<span class="ge-resize-size" />').appendTo(col.find('> .ge-tools-drawer'));
|
|
2125
|
+
|
|
2126
|
+
col.resizable($.extend({
|
|
2127
|
+
handles: settings.resize.handles,
|
|
2128
|
+
start: resizeStart,
|
|
2129
|
+
resize: resizeMove,
|
|
2130
|
+
stop: resizeStop,
|
|
2131
|
+
}, settings.resizable_options));
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
function removeResizable() {
|
|
2136
|
+
canvas.find('.column').each(function() {
|
|
2137
|
+
var col = $(this);
|
|
2138
|
+
if (col.data('ui-resizable')) { col.resizable('destroy'); }
|
|
2139
|
+
});
|
|
2140
|
+
|
|
2141
|
+
canvas.find('.ge-resize-size').remove();
|
|
2142
|
+
stripPixelWidths(canvas);
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
/**
|
|
2146
|
+
* The units a pixel width comes to, snapped to whole ones and held
|
|
2147
|
+
* inside the same budget the tools obey. With balance 'next' the
|
|
2148
|
+
* column may grow into its neighbour, which is what dragging the
|
|
2149
|
+
* divider between two columns looks like it should do.
|
|
2150
|
+
*/
|
|
2151
|
+
function snapUnits(col, pixels) {
|
|
2152
|
+
var row = col.parent();
|
|
2153
|
+
var style = window.getComputedStyle(row[0]);
|
|
2154
|
+
var content = row[0].clientWidth -
|
|
2155
|
+
parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
|
|
2156
|
+
|
|
2157
|
+
var units = Math.round(pixels / content * MAX_COL_SIZE);
|
|
2158
|
+
var next = balanceSibling(col);
|
|
2159
|
+
|
|
2160
|
+
// With a sibling to balance against, the drag may take that
|
|
2161
|
+
// column's units but not its last one. Without one, the row is
|
|
2162
|
+
// allowed to wrap - that is what balance false means - so the only
|
|
2163
|
+
// limit is the column's own budget against its indent.
|
|
2164
|
+
var room = next
|
|
2165
|
+
? currentSize(col) + currentSize(next) - smallest(settings.valid_col_sizes)
|
|
2166
|
+
: MAX_COL_SIZE - currentOffset(col);
|
|
2167
|
+
|
|
2168
|
+
return Math.min(
|
|
2169
|
+
Math.max(units, smallest(settings.valid_col_sizes)),
|
|
2170
|
+
Math.max(room, smallest(settings.valid_col_sizes)),
|
|
2171
|
+
largest(settings.valid_col_sizes)
|
|
2172
|
+
);
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
/** The column that absorbs the delta, when the host asked for that. */
|
|
2176
|
+
function balanceSibling(col) {
|
|
2177
|
+
if (settings.resize.balance !== 'next') { return null; }
|
|
2178
|
+
|
|
2179
|
+
var next = col.nextAll('.column').first();
|
|
2180
|
+
return next.length ? next : null;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
function resizeReadout(col, text) {
|
|
2184
|
+
col.find('> .ge-tools-drawer > .ge-resize-size').text(text);
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
function sizeLabel(units) {
|
|
2188
|
+
return (curView === ALL_VIEW ? BREAKPOINTS[0].colPrefix : leadingTier().colPrefix) + units;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
/**
|
|
2192
|
+
* A canceled before-resize refuses the drag.
|
|
2193
|
+
*
|
|
2194
|
+
* jQuery UI's resizable ignores false from its start handler - unlike
|
|
2195
|
+
* draggable, and unlike what the spec assumed - so the refusal is
|
|
2196
|
+
* carried on the column and every step of the drag returns false,
|
|
2197
|
+
* which the widget does honour. Nothing is written and the column ends
|
|
2198
|
+
* where it began.
|
|
2199
|
+
*/
|
|
2200
|
+
function resizeStart(e, ui) {
|
|
2201
|
+
var col = $(this);
|
|
2202
|
+
var from = currentSize(col);
|
|
2203
|
+
|
|
2204
|
+
var allowed = operate(function() {
|
|
2205
|
+
return emit('before-resize', payloadFor('column', col, {
|
|
2206
|
+
source: 'dragdrop',
|
|
2207
|
+
from: from,
|
|
2208
|
+
to: null, // Not known until the pointer stops
|
|
2209
|
+
}));
|
|
2210
|
+
});
|
|
2211
|
+
|
|
2212
|
+
if (!allowed) {
|
|
2213
|
+
col.data('ge-resize-refused', true);
|
|
2214
|
+
return false;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
col.data('ge-resize-from', from);
|
|
2218
|
+
resizeReadout(col, sizeLabel(from));
|
|
2219
|
+
|
|
2220
|
+
return undefined;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
function resizeMove(e, ui) {
|
|
2224
|
+
var col = $(this);
|
|
2225
|
+
|
|
2226
|
+
if (col.data('ge-resize-refused')) { return false; }
|
|
2227
|
+
|
|
2228
|
+
resizeReadout(col, sizeLabel(snapUnits(col, ui.size.width)));
|
|
2229
|
+
|
|
2230
|
+
return undefined;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
function resizeStop(e, ui) {
|
|
2234
|
+
var col = $(this);
|
|
2235
|
+
var from = col.data('ge-resize-from');
|
|
2236
|
+
var units = snapUnits(col, ui.size.width);
|
|
2237
|
+
|
|
2238
|
+
col.removeData('ge-resize-from');
|
|
2239
|
+
resizeReadout(col, '');
|
|
2240
|
+
stripPixelWidths(col);
|
|
2241
|
+
|
|
2242
|
+
if (col.data('ge-resize-refused') || from === undefined) {
|
|
2243
|
+
col.removeData('ge-resize-refused');
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
// A drag of a couple of pixels lands on the size it started from,
|
|
2248
|
+
// and is not a resize
|
|
2249
|
+
if (units === from) { return; }
|
|
2250
|
+
|
|
2251
|
+
var plan = planSize(col, units);
|
|
2252
|
+
if (!plan) { return; }
|
|
2253
|
+
|
|
2254
|
+
operate(function() {
|
|
2255
|
+
writeSize(col, plan);
|
|
2256
|
+
balanceAfterResize(col, plan.size - from);
|
|
2257
|
+
|
|
2258
|
+
emit('after-resize', payloadFor('column', col, {
|
|
2259
|
+
source: 'dragdrop',
|
|
2260
|
+
from: from,
|
|
2261
|
+
to: plan.size,
|
|
2262
|
+
}));
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
/**
|
|
2267
|
+
* Move the delta into the following column, so a full row stays full.
|
|
2268
|
+
* It is part of the same gesture, so it is not announced separately.
|
|
2269
|
+
*/
|
|
2270
|
+
function balanceAfterResize(col, delta) {
|
|
2271
|
+
var next = balanceSibling(col);
|
|
2272
|
+
if (!next || !delta) { return; }
|
|
2273
|
+
|
|
2274
|
+
var plan = planSize(next, currentSize(next) - delta);
|
|
2275
|
+
if (plan) { writeSize(next, plan); }
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
function removeSortable() {
|
|
2279
|
+
// Only where a sortable was actually made: deinit() is a public
|
|
2280
|
+
// method now, and jQuery UI throws when asked to destroy a widget
|
|
2281
|
+
// that is not there, so calling deinit() twice would fail.
|
|
2282
|
+
// jQuery UI marks what it made, so a plugin's sortables come
|
|
2283
|
+
// away with the editor's without the core knowing about them
|
|
2284
|
+
canvas.find('.ui-sortable').addBack('.ui-sortable').each(function() {
|
|
2285
|
+
var node = $(this);
|
|
2286
|
+
if (node.data('ui-sortable')) {
|
|
2287
|
+
node.sortable('destroy');
|
|
2288
|
+
}
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
function createRow() {
|
|
2293
|
+
return $('<div class="row" />');
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
/**
|
|
2297
|
+
* Put a freshly created node where the caller asked for it, through
|
|
2298
|
+
* the add events, and bring the canvas up to date so the new markup
|
|
2299
|
+
* gets its controls. With no placement option the node stays
|
|
2300
|
+
* detached, and placing it and calling reset() is the host's job
|
|
2301
|
+
* (spec 1.2).
|
|
2302
|
+
*
|
|
2303
|
+
* Returns the node, or null when a handler canceled the add. A call
|
|
2304
|
+
* made from inside an event handler is queued, and then returns the
|
|
2305
|
+
* node without knowing yet whether the add will be canceled.
|
|
2306
|
+
*/
|
|
2307
|
+
function place(node, kind, options) {
|
|
2308
|
+
var placement = null;
|
|
2309
|
+
|
|
2310
|
+
PLACEMENTS.forEach(function(name) {
|
|
2311
|
+
if (placement === null && options && options[name] !== undefined) {
|
|
2312
|
+
placement = name;
|
|
2313
|
+
}
|
|
2314
|
+
});
|
|
2315
|
+
|
|
2316
|
+
if (placement === null) { return node; }
|
|
2317
|
+
|
|
2318
|
+
var target = $(options[placement]);
|
|
2319
|
+
var parent = (placement === 'appendTo' || placement === 'prependTo')
|
|
2320
|
+
? target
|
|
2321
|
+
: target.parent();
|
|
2322
|
+
|
|
2323
|
+
var add = function() {
|
|
2324
|
+
return addNode(kind, node, function() {
|
|
2325
|
+
node[placement](options[placement]);
|
|
2326
|
+
}, { parent: parent, source: 'api' });
|
|
2327
|
+
};
|
|
2328
|
+
|
|
2329
|
+
if (operationDepth > 0) {
|
|
2330
|
+
defer(add);
|
|
2331
|
+
return node;
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
return add();
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
/**
|
|
2338
|
+
* A row, optionally with columns in it: createRow([8, 4]).
|
|
2339
|
+
*/
|
|
2340
|
+
function apiCreateRow(layout, options) {
|
|
2341
|
+
var row = createRow();
|
|
2342
|
+
|
|
2343
|
+
if (layout !== undefined && !Array.isArray(layout)) {
|
|
2344
|
+
warn('createRow: the layout is an array of column sizes, as in [8, 4]. ' +
|
|
2345
|
+
'Making an empty row instead.');
|
|
2346
|
+
layout = [];
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
(layout || []).forEach(function(size) {
|
|
2350
|
+
createColumn(size).appendTo(row);
|
|
2351
|
+
});
|
|
2352
|
+
|
|
2353
|
+
return place(row, 'row', options);
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
/**
|
|
2357
|
+
* A column of `size` units, optionally holding `options.content`.
|
|
2358
|
+
*/
|
|
2359
|
+
function apiCreateColumn(size, options) {
|
|
2360
|
+
options = options || {};
|
|
2361
|
+
|
|
2362
|
+
if (typeof size != 'number') {
|
|
2363
|
+
warn('createColumn: no column size given, using ' + MAX_COL_SIZE);
|
|
2364
|
+
size = MAX_COL_SIZE;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
var column = createColumn(size, options.offset);
|
|
2368
|
+
if (options.content !== undefined) {
|
|
2369
|
+
column.find('.ge-content').html(options.content);
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
return place(column, 'column', options);
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
/**
|
|
2376
|
+
* A container of the given type, with its panes already in it.
|
|
2377
|
+
*/
|
|
2378
|
+
function apiCreateContainer(type, options) {
|
|
2379
|
+
options = options || {};
|
|
2380
|
+
|
|
2381
|
+
var definition = CONTAINERS[type];
|
|
2382
|
+
if (!definition) {
|
|
2383
|
+
warn('createContainer: no such container type "' + type + '"');
|
|
2384
|
+
return null;
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
return place(definition.create(options), type, options);
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
/** A pane appended to a container, through the add events. */
|
|
2391
|
+
function addPaneTo(container, type, options) {
|
|
2392
|
+
container = $(container);
|
|
2393
|
+
options = options || {};
|
|
2394
|
+
|
|
2395
|
+
var definition = CONTAINERS[containerTypeOf(container)];
|
|
2396
|
+
|
|
2397
|
+
if (!definition || containerTypeOf(container) !== type) {
|
|
2398
|
+
warn('this is not a ' + type + ' container');
|
|
2399
|
+
return null;
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
var pane = definition.addPane(container, options);
|
|
2403
|
+
|
|
2404
|
+
return addNode(definition.paneKind, pane, function() {}, {
|
|
2405
|
+
parent: container,
|
|
2406
|
+
source: 'api',
|
|
2407
|
+
container: container,
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
/**
|
|
2412
|
+
* Host markup wrapped as a grid-editor element (spec 4.5). What is
|
|
2413
|
+
* inside stays the host's; grid-editor owns the wrapper only.
|
|
2414
|
+
*/
|
|
2415
|
+
|
|
2416
|
+
/**
|
|
2417
|
+
* A shallow frozen copy of the settings for the instance handle, so a
|
|
2418
|
+
* host can read what the editor is running with without changing it
|
|
2419
|
+
* behind the editor's back. Arrays are copied; the objects inside them
|
|
2420
|
+
* are the host's own and stay shared.
|
|
2421
|
+
*/
|
|
2422
|
+
function settingsCopy() {
|
|
2423
|
+
var copy = {};
|
|
2424
|
+
|
|
2425
|
+
$.each(settings, function(key, value) {
|
|
2426
|
+
copy[key] = Array.isArray(value) ? value.slice() : value;
|
|
2427
|
+
});
|
|
2428
|
+
|
|
2429
|
+
return Object.freeze(copy);
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
/**
|
|
2433
|
+
* A column sized for the current view: one tier, or every tier in the
|
|
2434
|
+
* all view. `offset` indents it, within the same 12 unit budget.
|
|
2435
|
+
*/
|
|
2436
|
+
function createColumn(size, offset) {
|
|
2437
|
+
var rte = getRTE(settings.content_types[0]);
|
|
2438
|
+
var column = $('<div class="column"/>')
|
|
2439
|
+
.append(createDefaultContentWrapper().html(rte ? rte.initialContent : ''))
|
|
2440
|
+
;
|
|
2441
|
+
|
|
2442
|
+
tiersFor(curView).forEach(function(tier) {
|
|
2443
|
+
var wanted = clamp({ size: size, offset: offset || 0, leading: 'offset' });
|
|
2444
|
+
|
|
2445
|
+
setSize(column, tier, wanted.size === null ? size : wanted.size);
|
|
2446
|
+
setOffset(column, tier, wanted.offset);
|
|
2447
|
+
});
|
|
2448
|
+
|
|
2449
|
+
return column;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
/**
|
|
2453
|
+
* Run custom content filter on init and deinit
|
|
2454
|
+
*/
|
|
2455
|
+
function runFilter(isInit) {
|
|
2456
|
+
if (settings.custom_filter.length) {
|
|
2457
|
+
$.each(settings.custom_filter, function(key, func) {
|
|
2458
|
+
if (typeof func == 'string') {
|
|
2459
|
+
func = window[func];
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
func(canvas, isInit);
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
/**
|
|
2468
|
+
* Wrap column content in <div class="ge-content"> where neccesary
|
|
2469
|
+
*/
|
|
2470
|
+
function wrapContent() {
|
|
2471
|
+
canvas.find('.column').each(function() {
|
|
2472
|
+
var col = $(this);
|
|
2473
|
+
var contents = $();
|
|
2474
|
+
|
|
2475
|
+
col.children().each(function() {
|
|
2476
|
+
var child = $(this);
|
|
2477
|
+
|
|
2478
|
+
// The editor's own furniture is not content and not a
|
|
2479
|
+
// boundary either. jQuery UI's resize handle used to be
|
|
2480
|
+
// treated as content and wrapped into a content area of
|
|
2481
|
+
// its own on the next init.
|
|
2482
|
+
if (child.is('.ge-tools-drawer, .ui-resizable-handle')) { return; }
|
|
2483
|
+
|
|
2484
|
+
// A container sits in the column beside the content
|
|
2485
|
+
// areas, not inside one, so it ends a run of loose
|
|
2486
|
+
// content rather than joining it
|
|
2487
|
+
if (child.is('.row, .ge-content, [data-ge-container]')) {
|
|
2488
|
+
contents = doWrap(contents);
|
|
2489
|
+
} else {
|
|
2490
|
+
contents = contents.add(child);
|
|
2491
|
+
}
|
|
2492
|
+
});
|
|
2493
|
+
|
|
2494
|
+
doWrap(contents);
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
/**
|
|
2499
|
+
* Wrap a run of loose column content in a content area, and hand back
|
|
2500
|
+
* an empty set: the caller has to forget what it just wrapped, or the
|
|
2501
|
+
* next boundary wraps the same nodes again and leaves the first
|
|
2502
|
+
* wrapper behind, empty.
|
|
2503
|
+
*/
|
|
2504
|
+
function doWrap(contents) {
|
|
2505
|
+
if (contents.length) {
|
|
2506
|
+
var contentArea = createDefaultContentWrapper().insertAfter(contents.last());
|
|
2507
|
+
contents.appendTo(contentArea);
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
return $();
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
function createDefaultContentWrapper() {
|
|
2514
|
+
return $('<div/>')
|
|
2515
|
+
.addClass('ge-content ge-content-type-' + settings.content_types[0])
|
|
2516
|
+
.attr('data-ge-content-type', settings.content_types[0])
|
|
2517
|
+
;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
/**
|
|
2521
|
+
* Constrain the canvas to the view's preview width and make that
|
|
2522
|
+
* tier's classes the effective ones. The all view constrains nothing:
|
|
2523
|
+
* every tier is live, which is how the page will really render.
|
|
2524
|
+
*/
|
|
2525
|
+
function switchLayout(view) {
|
|
2526
|
+
curView = view;
|
|
2527
|
+
|
|
2528
|
+
VIEW_KEYS.forEach(function(key) {
|
|
2529
|
+
canvas.toggleClass('ge-layout-' + key, key === view);
|
|
2530
|
+
});
|
|
2531
|
+
layoutDropdown.find('button').text(t(labelKeyFor(view)));
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
/**
|
|
2535
|
+
* The view key a caller asked for, or null. 2.x callers passed a
|
|
2536
|
+
* layout mode index, which still works and says so once.
|
|
2537
|
+
*/
|
|
2538
|
+
function viewKey(view) {
|
|
2539
|
+
if (typeof view == 'number') {
|
|
2540
|
+
warnOnceHere('changeView-index', 'changeView(' + view + '): layout modes are ' +
|
|
2541
|
+
'identified by breakpoint key now, so pass one of ' +
|
|
2542
|
+
JSON.stringify(VIEW_KEYS) + '. Numeric indexes still work, ' +
|
|
2543
|
+
'but they mean what they meant in 2.x (' +
|
|
2544
|
+
LEGACY_VIEW_INDEXES.join(', ') + ') and will be dropped.');
|
|
2545
|
+
return LEGACY_VIEW_INDEXES[view] || null;
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
return VIEW_KEYS.indexOf(view) === -1 ? null : view;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
function changeView(view) {
|
|
2552
|
+
var key = viewKey(view);
|
|
2553
|
+
|
|
2554
|
+
if (key === null) {
|
|
2555
|
+
warn('changeView(' + JSON.stringify(view) + '): no such layout mode');
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
switchLayout(key);
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
function getView() {
|
|
2563
|
+
return curView;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
function getRTE(type) {
|
|
2567
|
+
return $.fn.gridEditor.RTEs[type];
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
/**
|
|
2571
|
+
* The instance handle, documented API as of 3.0: the methods the
|
|
2572
|
+
* plugin dispatches, plus the settings and the canvas. A host holding
|
|
2573
|
+
* this can call several methods without dispatching each one.
|
|
2574
|
+
*/
|
|
2575
|
+
var handle = {
|
|
2576
|
+
getHtml: getHtml,
|
|
2577
|
+
// init and reset are deferred when a handler calls them, so an
|
|
2578
|
+
// operation in flight finishes before the canvas is rebuilt
|
|
2579
|
+
init: function() { defer(init); },
|
|
2580
|
+
reset: function() { defer(reset); },
|
|
2581
|
+
deinit: deinit,
|
|
2582
|
+
destroy: destroy,
|
|
2583
|
+
remove: deprecatedRemove,
|
|
2584
|
+
changeView: changeView,
|
|
2585
|
+
getView: getView,
|
|
2586
|
+
createRow: apiCreateRow,
|
|
2587
|
+
createColumn: apiCreateColumn,
|
|
2588
|
+
createElement: function(content, options) {
|
|
2589
|
+
if (!featureMethods.createElement) {
|
|
2590
|
+
warnOnceHere('plugin:elements', 'createElement needs the elements plugin: ' +
|
|
2591
|
+
'include dist/plugins/grideditor.elements.js after the editor');
|
|
2592
|
+
return null;
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
return featureMethods.createElement(content, options);
|
|
2596
|
+
},
|
|
2597
|
+
createContainer: apiCreateContainer,
|
|
2598
|
+
addTab: function(container, options) { return addPaneTo(container, 'tabs', options); },
|
|
2599
|
+
addAccordionItem: function(container, options) {
|
|
2600
|
+
return addPaneTo(container, 'accordion', options);
|
|
2601
|
+
},
|
|
2602
|
+
setLocale: setLocale,
|
|
2603
|
+
canvas: canvas,
|
|
2604
|
+
settings: settingsCopy(),
|
|
2605
|
+
};
|
|
2606
|
+
|
|
2607
|
+
// Methods a later phase fills in: registered, so calling one gets a
|
|
2608
|
+
// warning and null rather than silence.
|
|
2609
|
+
$.each(METHODS, function(name, descriptor) {
|
|
2610
|
+
if (!descriptor.unimplemented) { return; }
|
|
2611
|
+
|
|
2612
|
+
handle[name] = function() {
|
|
2613
|
+
warnOnceHere(name, name + ' is registered but not implemented in this build yet');
|
|
2614
|
+
return null;
|
|
2615
|
+
};
|
|
2616
|
+
});
|
|
2617
|
+
|
|
2618
|
+
baseElem.data('grideditor', handle);
|
|
2619
|
+
|
|
2620
|
+
loadPlugins();
|
|
2621
|
+
setup();
|
|
2622
|
+
init();
|
|
2623
|
+
|
|
2624
|
+
});
|
|
2625
|
+
|
|
2626
|
+
return self;
|
|
2627
|
+
|
|
2628
|
+
};
|
|
2629
|
+
|
|
2630
|
+
$.fn.gridEditor.RTEs = {};
|
|
2631
|
+
|
|
2632
|
+
/**
|
|
2633
|
+
* Container plugins: tabs, accordions, popups, and whatever a host writes.
|
|
2634
|
+
*
|
|
2635
|
+
* A plugin is a factory registered under the type it builds, called once per
|
|
2636
|
+
* editor with the handle described in docs/plugins.md. Loading its file is
|
|
2637
|
+
* what makes the type available; the `plugins` setting narrows that list.
|
|
2638
|
+
*
|
|
2639
|
+
* $.fn.gridEditor.containers.carousel = function(ge) {
|
|
2640
|
+
* return { labelKey: ..., create: ..., mark: ..., unmark: ... };
|
|
2641
|
+
* };
|
|
2642
|
+
*/
|
|
2643
|
+
$.fn.gridEditor.containers = {};
|
|
2644
|
+
|
|
2645
|
+
/**
|
|
2646
|
+
* Feature plugins: a piece of the editor that is not a container type, in a
|
|
2647
|
+
* file of its own. Element level controls are one. Same bargain as a
|
|
2648
|
+
* container plugin - a factory under its name, called once per editor with
|
|
2649
|
+
* the handle in docs/plugins.md - and the same `plugins` setting decides
|
|
2650
|
+
* which of the loaded ones are used.
|
|
2651
|
+
*/
|
|
2652
|
+
$.fn.gridEditor.features = {};
|
|
2653
|
+
|
|
2654
|
+
/** Translator for the editor integrations, which get settings and no instance. */
|
|
2655
|
+
$.fn.gridEditor.t = translate;
|
|
2656
|
+
|
|
2657
|
+
/**
|
|
2658
|
+
* Locale registry: code -> { key: string }.
|
|
2659
|
+
*
|
|
2660
|
+
* English is built in rather than shipped as a file, because it is where every
|
|
2661
|
+
* lookup ends: a page that loads no locale file still has a complete UI.
|
|
2662
|
+
* Removing a key from it is a breaking change. Locale files register
|
|
2663
|
+
* themselves here, see src/js/locales/.
|
|
2664
|
+
*
|
|
2665
|
+
* Every key is listed in docs/locale-keys.md, which test/locales.js holds to
|
|
2666
|
+
* this catalogue in both directions.
|
|
2667
|
+
*/
|
|
2668
|
+
$.fn.gridEditor.locales = {
|
|
2669
|
+
en: {
|
|
2670
|
+
'tool.move': 'Move',
|
|
2671
|
+
'tool.settings': 'Settings',
|
|
2672
|
+
'tool.add_row': 'Add row',
|
|
2673
|
+
'tool.add_column': 'Add column\n(hold to choose the width)',
|
|
2674
|
+
'tool.column_size': '{size} of 12',
|
|
2675
|
+
'tool.delete_row': 'Remove row',
|
|
2676
|
+
'tool.delete_column': 'Remove col',
|
|
2677
|
+
'tool.delete_container': 'Remove container',
|
|
2678
|
+
'tool.delete_pane': 'Remove pane',
|
|
2679
|
+
'tool.rename': 'Double click to rename',
|
|
2680
|
+
'tool.column_narrower': 'Make column narrower\n(hold shift for min)',
|
|
2681
|
+
'tool.column_wider': 'Make column wider\n(hold shift for max)',
|
|
2682
|
+
'tool.indent_decrease': 'Decrease indent\n(hold shift for none)',
|
|
2683
|
+
'tool.indent_increase': 'Increase indent\n(hold shift for max)',
|
|
2684
|
+
'tool.edit_source': 'Edit Source Code',
|
|
2685
|
+
'tool.preview': 'Preview',
|
|
2686
|
+
'tool.id_placeholder': 'id',
|
|
2687
|
+
'tool.id_title': 'Set a unique identifier',
|
|
2688
|
+
'tool.classes_placeholder': 'classes',
|
|
2689
|
+
'tool.classes_title': 'Css classes, separated by spaces',
|
|
2690
|
+
'tool.toggle_class': 'Toggle "{label}" styling',
|
|
2691
|
+
'row.add': 'Add row {layout}',
|
|
2692
|
+
'confirm.title': 'Confirm',
|
|
2693
|
+
'confirm.ok': 'Delete',
|
|
2694
|
+
'confirm.cancel': 'Cancel',
|
|
2695
|
+
'confirm.delete_row': 'Delete row?',
|
|
2696
|
+
'confirm.delete_column': 'Delete column?',
|
|
2697
|
+
'confirm.delete_container': 'Delete this container and everything in it?',
|
|
2698
|
+
'view.all': 'All sizes',
|
|
2699
|
+
'view.xs': 'Phone',
|
|
2700
|
+
'view.sm': 'Tablet',
|
|
2701
|
+
'view.md': 'Small desktop',
|
|
2702
|
+
'view.lg': 'Desktop',
|
|
2703
|
+
'view.xl': 'Large desktop',
|
|
2704
|
+
'view.xxl': 'Widescreen',
|
|
2705
|
+
'error.tinymce_missing': 'tinyMCE not available! Make sure you loaded the tinyMCE js file.',
|
|
2706
|
+
'error.ckeditor_missing': 'CKEditor not available! Make sure you loaded the ckeditor and jquery adapter js files.',
|
|
2707
|
+
'error.summernote_missing': 'Summernote not available! Make sure you loaded the Summernote js file.',
|
|
2708
|
+
},
|
|
2709
|
+
};
|
|
2710
|
+
|
|
2711
|
+
})( jQuery );
|
|
2712
|
+
(function($) {
|
|
2713
|
+
$.fn.gridEditor.RTEs.ckeditor = {
|
|
2714
|
+
|
|
2715
|
+
init: function(settings, contentAreas) {
|
|
2716
|
+
|
|
2717
|
+
if (!window.CKEDITOR) {
|
|
2718
|
+
console.error($.fn.gridEditor.t(settings, 'error.ckeditor_missing'));
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
var self = this;
|
|
2722
|
+
contentAreas.each(function() {
|
|
2723
|
+
var contentArea = $(this);
|
|
2724
|
+
if (!contentArea.hasClass('active')) {
|
|
2725
|
+
if (contentArea.html() == self.initialContent) {
|
|
2726
|
+
// CKEditor kills this ' ' creating a non usable box :/
|
|
2727
|
+
contentArea.html(' ');
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
// Add the .attr('contenteditable',''true') or CKEditor loads readonly
|
|
2731
|
+
contentArea.addClass('active').attr('contenteditable', 'true');
|
|
2732
|
+
|
|
2733
|
+
var configuration = $.extend(
|
|
2734
|
+
{},
|
|
2735
|
+
(settings.ckeditor && settings.ckeditor.config ? settings.ckeditor.config : {}),
|
|
2736
|
+
{
|
|
2737
|
+
// Focus editor on creation
|
|
2738
|
+
on: {
|
|
2739
|
+
instanceReady: function( evt ) {
|
|
2740
|
+
// Call original instanceReady function, if one was passed in the config
|
|
2741
|
+
var callback;
|
|
2742
|
+
try {
|
|
2743
|
+
callback = settings.ckeditor.config.on.instanceReady;
|
|
2744
|
+
} catch (err) {
|
|
2745
|
+
// No callback passed
|
|
2746
|
+
}
|
|
2747
|
+
if (callback) {
|
|
2748
|
+
callback.call(this, evt);
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
// The editor owns what is inside the
|
|
2752
|
+
// content area now, so the grid editor is
|
|
2753
|
+
// told to put its own furniture back
|
|
2754
|
+
contentArea.trigger('ge-rte-ready');
|
|
2755
|
+
|
|
2756
|
+
instance.focus();
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
);
|
|
2761
|
+
var instance = CKEDITOR.inline(contentArea.get(0), configuration);
|
|
2762
|
+
}
|
|
2763
|
+
});
|
|
2764
|
+
},
|
|
2765
|
+
|
|
2766
|
+
deinit: function(settings, contentAreas) {
|
|
2767
|
+
contentAreas.filter('.active').each(function() {
|
|
2768
|
+
var contentArea = $(this);
|
|
2769
|
+
|
|
2770
|
+
// Destroy all CKEditor instances
|
|
2771
|
+
$.each(CKEDITOR.instances, function(_, instance) {
|
|
2772
|
+
instance.destroy();
|
|
2773
|
+
});
|
|
2774
|
+
|
|
2775
|
+
// Cleanup
|
|
2776
|
+
contentArea
|
|
2777
|
+
.removeClass('active cke_focus')
|
|
2778
|
+
.removeAttr('id')
|
|
2779
|
+
.removeAttr('style')
|
|
2780
|
+
.removeAttr('spellcheck')
|
|
2781
|
+
.removeAttr('contenteditable')
|
|
2782
|
+
;
|
|
2783
|
+
});
|
|
2784
|
+
},
|
|
2785
|
+
|
|
2786
|
+
initialContent: '<p>Lorem initius... </p>',
|
|
2787
|
+
};
|
|
2788
|
+
})(jQuery);
|
|
2789
|
+
(function($) {
|
|
2790
|
+
|
|
2791
|
+
$.fn.gridEditor.RTEs.summernote = {
|
|
2792
|
+
|
|
2793
|
+
init: function(settings, contentAreas) {
|
|
2794
|
+
|
|
2795
|
+
if (!jQuery().summernote) {
|
|
2796
|
+
console.error($.fn.gridEditor.t(settings, 'error.summernote_missing'));
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
var self = this;
|
|
2800
|
+
contentAreas.each(function() {
|
|
2801
|
+
var contentArea = $(this);
|
|
2802
|
+
if (!contentArea.hasClass('active')) {
|
|
2803
|
+
if (contentArea.html() == self.initialContent) {
|
|
2804
|
+
contentArea.html('');
|
|
2805
|
+
}
|
|
2806
|
+
contentArea.addClass('active');
|
|
2807
|
+
|
|
2808
|
+
var configuration = $.extend(
|
|
2809
|
+
true, // deep copy
|
|
2810
|
+
{},
|
|
2811
|
+
(settings.summernote && settings.summernote.config ? settings.summernote.config : {}),
|
|
2812
|
+
{
|
|
2813
|
+
tabsize: 2,
|
|
2814
|
+
airMode: true,
|
|
2815
|
+
// Focus editor on creation
|
|
2816
|
+
callbacks: {
|
|
2817
|
+
onInit: function() {
|
|
2818
|
+
|
|
2819
|
+
// Call original oninit function, if one was passed in the config
|
|
2820
|
+
var callback;
|
|
2821
|
+
try {
|
|
2822
|
+
callback = settings.summernote.config.callbacks.onInit;
|
|
2823
|
+
} catch (err) {
|
|
2824
|
+
// No callback passed
|
|
2825
|
+
}
|
|
2826
|
+
if (callback) {
|
|
2827
|
+
callback.call(this);
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
// The editor owns what is inside the
|
|
2831
|
+
// content area now, so the grid editor is
|
|
2832
|
+
// told to put its own furniture back
|
|
2833
|
+
contentArea.trigger('ge-rte-ready');
|
|
2834
|
+
|
|
2835
|
+
contentArea.summernote('focus');
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
);
|
|
2840
|
+
contentArea.summernote(configuration);
|
|
2841
|
+
}
|
|
2842
|
+
});
|
|
2843
|
+
},
|
|
2844
|
+
|
|
2845
|
+
deinit: function(settings, contentAreas) {
|
|
2846
|
+
contentAreas.filter('.active').each(function() {
|
|
2847
|
+
var contentArea = $(this);
|
|
2848
|
+
contentArea.summernote('destroy');
|
|
2849
|
+
contentArea
|
|
2850
|
+
.removeClass('active')
|
|
2851
|
+
.removeAttr('id')
|
|
2852
|
+
.removeAttr('style')
|
|
2853
|
+
.removeAttr('spellcheck')
|
|
2854
|
+
;
|
|
2855
|
+
});
|
|
2856
|
+
},
|
|
2857
|
+
|
|
2858
|
+
initialContent: '<p>Lorem ipsum dolores</p>',
|
|
2859
|
+
};
|
|
2860
|
+
})(jQuery);
|
|
2861
|
+
|
|
2862
|
+
(function($) {
|
|
2863
|
+
|
|
2864
|
+
// tinyMCE snapshots the target element's attributes when an inline editor is
|
|
2865
|
+
// created and restores them on remove(), so this has to run *after* remove()
|
|
2866
|
+
// to keep the grid editor's own class and tinyMCE's leftovers off the element.
|
|
2867
|
+
function cleanUp(contentArea) {
|
|
2868
|
+
contentArea
|
|
2869
|
+
.removeClass('active')
|
|
2870
|
+
.removeClass('ge-rte-active')
|
|
2871
|
+
.removeAttr('id')
|
|
2872
|
+
.removeAttr('style')
|
|
2873
|
+
.removeAttr('spellcheck')
|
|
2874
|
+
.removeAttr('contenteditable')
|
|
2875
|
+
.removeAttr('data-mce-style')
|
|
2876
|
+
;
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
$.fn.gridEditor.RTEs.tinymce = {
|
|
2880
|
+
|
|
2881
|
+
init: function(settings, contentAreas) {
|
|
2882
|
+
|
|
2883
|
+
if (!window.tinymce) {
|
|
2884
|
+
console.error($.fn.gridEditor.t(settings, 'error.tinymce_missing'));
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
var self = this;
|
|
2889
|
+
var userConfig = (settings.tinymce && settings.tinymce.config) ? settings.tinymce.config : {};
|
|
2890
|
+
|
|
2891
|
+
contentAreas.each(function() {
|
|
2892
|
+
var contentArea = $(this);
|
|
2893
|
+
if (contentArea.hasClass('active')) { return; }
|
|
2894
|
+
|
|
2895
|
+
if (contentArea.html() == self.initialContent) {
|
|
2896
|
+
contentArea.html('');
|
|
2897
|
+
}
|
|
2898
|
+
contentArea.addClass('active');
|
|
2899
|
+
|
|
2900
|
+
var configuration = $.extend({
|
|
2901
|
+
// tinyMCE's own "Upgrade" badge in the menubar. Off by
|
|
2902
|
+
// default because an inline editor here is a column of
|
|
2903
|
+
// someone's page, not tinyMCE's own interface; a host that
|
|
2904
|
+
// wants it back passes promotion: true.
|
|
2905
|
+
promotion: false,
|
|
2906
|
+
}, userConfig, {
|
|
2907
|
+
target: this,
|
|
2908
|
+
inline: true,
|
|
2909
|
+
init_instance_callback: function(editor) {
|
|
2910
|
+
// deinit ran before this init finished, so tear it down again
|
|
2911
|
+
if (contentArea.data('ge-tinymce-pending-remove')) {
|
|
2912
|
+
contentArea.removeData('ge-tinymce-pending-remove');
|
|
2913
|
+
editor.remove();
|
|
2914
|
+
// deinit already ran and cannot clean up after this
|
|
2915
|
+
// late remove(), so do it here instead
|
|
2916
|
+
cleanUp(contentArea);
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
contentArea.data('ge-tinymce', editor);
|
|
2921
|
+
|
|
2922
|
+
// The editor rewrote what is inside this content area
|
|
2923
|
+
// while it took it over, so whatever the grid editor
|
|
2924
|
+
// had in there is gone. Saying so lets it put its own
|
|
2925
|
+
// furniture back (see RTE_READY in the core).
|
|
2926
|
+
contentArea.trigger('ge-rte-ready');
|
|
2927
|
+
|
|
2928
|
+
// The inline toolbar is laid out against the element's
|
|
2929
|
+
// geometry at the moment tinyMCE draws it, and an
|
|
2930
|
+
// element that has just appeared - a tab pane, an
|
|
2931
|
+
// accordion body, a column whose width is still
|
|
2932
|
+
// settling - may not have its own width yet. Asking
|
|
2933
|
+
// for the ui again once the browser has laid the frame
|
|
2934
|
+
// out measures it as it now is, instead of leaving a
|
|
2935
|
+
// toolbar wrapped into a narrow column.
|
|
2936
|
+
window.requestAnimationFrame(function() {
|
|
2937
|
+
if (editor.removed || !editor.ui || !editor.ui.show) { return; }
|
|
2938
|
+
|
|
2939
|
+
editor.ui.show();
|
|
2940
|
+
});
|
|
2941
|
+
|
|
2942
|
+
// And again whenever the user comes back to this
|
|
2943
|
+
// editor: by then the element may have been resized,
|
|
2944
|
+
// hidden and shown again - a tab switched away from
|
|
2945
|
+
// and back, a column made narrower - and the toolbar
|
|
2946
|
+
// is only ever as right as its last measurement.
|
|
2947
|
+
editor.on('focus', function() {
|
|
2948
|
+
window.requestAnimationFrame(function() {
|
|
2949
|
+
if (editor.removed || !editor.ui || !editor.ui.show) { return; }
|
|
2950
|
+
|
|
2951
|
+
editor.ui.show();
|
|
2952
|
+
});
|
|
2953
|
+
});
|
|
2954
|
+
|
|
2955
|
+
// Bring focus to text field
|
|
2956
|
+
editor.focus();
|
|
2957
|
+
|
|
2958
|
+
// Call the original callbacks, if any were passed in the config
|
|
2959
|
+
if (userConfig.init_instance_callback) {
|
|
2960
|
+
userConfig.init_instance_callback.call(this, editor);
|
|
2961
|
+
}
|
|
2962
|
+
// 'oninit' is the pre-6 name, honoured so existing configs keep working
|
|
2963
|
+
if (userConfig.oninit) {
|
|
2964
|
+
userConfig.oninit.call(this, editor);
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
});
|
|
2968
|
+
// We always edit the element we were handed, so a selector in the
|
|
2969
|
+
// user config would only fight with target
|
|
2970
|
+
delete configuration.selector;
|
|
2971
|
+
|
|
2972
|
+
window.tinymce.init(configuration);
|
|
2973
|
+
});
|
|
2974
|
+
},
|
|
2975
|
+
|
|
2976
|
+
deinit: function(settings, contentAreas) {
|
|
2977
|
+
contentAreas.filter('.active').each(function() {
|
|
2978
|
+
var contentArea = $(this);
|
|
2979
|
+
var editor = contentArea.data('ge-tinymce');
|
|
2980
|
+
|
|
2981
|
+
if (editor) {
|
|
2982
|
+
contentArea.removeData('ge-tinymce');
|
|
2983
|
+
editor.remove();
|
|
2984
|
+
} else {
|
|
2985
|
+
// tinymce.init() is asynchronous, so the editor may not exist
|
|
2986
|
+
// yet. Leave a note for init_instance_callback to remove it.
|
|
2987
|
+
contentArea.data('ge-tinymce-pending-remove', true);
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
cleanUp(contentArea);
|
|
2991
|
+
});
|
|
2992
|
+
},
|
|
2993
|
+
|
|
2994
|
+
initialContent: '<p>Lorem ipsum dolores</p>',
|
|
2995
|
+
};
|
|
2996
|
+
})(jQuery);
|