@themarioga/grid-editor 5.1.0 → 5.1.2

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.
@@ -0,0 +1,362 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
5
+
6
+ <!-- CSS dependencies -->
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.css">
9
+
10
+ <!-- Javascript dependencies -->
11
+ <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
12
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js" integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI" crossorigin="anonymous"></script>
13
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/6.8.6/tinymce.min.js"></script>
14
+
15
+ <!-- Grid editor CSS -->
16
+ <link rel="stylesheet" type="text/css" href="../dist/grideditor.css" />
17
+ <!-- Grid editor javascript -->
18
+ <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js"></script>
19
+ <script src="../dist/jquery.grideditor.min.js"></script>
20
+ <script src="../dist/plugins/grideditor.card.min.js"></script>
21
+ <script src="../dist/plugins/grideditor.elements.min.js"></script>
22
+
23
+ <style>
24
+ /* The drawer tool of a node that has an animation set. Under the
25
+ canvas's id, to outweigh the colour the editor gives each kind of
26
+ drawer's tools */
27
+ #myGrid .ge-tools-drawer > a.my-animation-tool.my-animation-set {
28
+ color: #d63384;
29
+ }
30
+ </style>
31
+
32
+ <!-- A plugin of this page's own: a drawer tool that opens a modal, and
33
+ what the user chooses there saved as an attribute on the node. -->
34
+ <script id="animation-plugin">
35
+ (function($) {
36
+
37
+ $.extend($.fn.gridEditor.locales.en, {
38
+ 'animation.tool': 'Animation',
39
+ 'animation.title': 'Animate on scroll',
40
+ 'animation.effect': 'Effect',
41
+ 'animation.none': 'None',
42
+ 'animation.fade': 'Fade in',
43
+ 'animation.slide': 'Slide up',
44
+ 'animation.zoom': 'Zoom in',
45
+ 'animation.duration': 'Duration (ms)',
46
+ 'animation.delay': 'Delay (ms)',
47
+ 'animation.once': 'Only the first time it scrolls into view',
48
+ 'animation.cancel': 'Cancel',
49
+ 'animation.save': 'Save',
50
+ });
51
+
52
+ // Not data-ge-*: getPlainHtml takes those off, and this has to
53
+ // reach the published page
54
+ var ATTRIBUTE = 'data-animation';
55
+ var EFFECTS = ['fade', 'slide', 'zoom'];
56
+
57
+ $.fn.gridEditor.features.animation = function(ge) {
58
+
59
+ var modal = null;
60
+
61
+ function read(node) {
62
+ try {
63
+ return JSON.parse(node.attr(ATTRIBUTE) || 'null');
64
+ } catch (error) {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * An element lives inside a content area, and while a rich
71
+ * text editor is open there, a change it does not know about
72
+ * is one its undo would take back: undoing the next thing the
73
+ * user types restores a snapshot from before the attribute.
74
+ * Made through tinyMCE's undo manager, it is a step of its
75
+ * own, which undo and redo treat like any other.
76
+ */
77
+ function write(node, value) {
78
+ var apply = function() {
79
+ if (value) {
80
+ node.attr(ATTRIBUTE, JSON.stringify(value));
81
+ } else {
82
+ node.removeAttr(ATTRIBUTE);
83
+ }
84
+ };
85
+ var editor = editorAround(node);
86
+
87
+ if (editor) {
88
+ editor.undoManager.transact(apply);
89
+ } else {
90
+ apply();
91
+ }
92
+ }
93
+
94
+ /** The tinyMCE editor open on the content area around node, if there is one. */
95
+ function editorAround(node) {
96
+ var area = node.closest('.ge-content');
97
+ if (!area.length || !window.tinymce) { return null; }
98
+
99
+ return window.tinymce.get().filter(function(editor) {
100
+ return editor.getBody && editor.getBody() === area[0];
101
+ })[0] || null;
102
+ }
103
+
104
+ /** Built once, outside the canvas, so it is never part of getHtml. */
105
+ function dialog() {
106
+ if (modal) { return modal; }
107
+
108
+ var effects = $('<select class="form-select" name="effect" />')
109
+ .append($('<option value="" />').text(ge.t('animation.none')));
110
+ EFFECTS.forEach(function(effect) {
111
+ effects.append($('<option />').val(effect).text(ge.t('animation.' + effect)));
112
+ });
113
+
114
+ var field = function(label, input) {
115
+ return $('<div class="mb-3" />')
116
+ .append($('<label class="form-label" />').text(ge.t(label)))
117
+ .append(input);
118
+ };
119
+
120
+ modal = $(
121
+ '<div class="modal fade my-animation-modal" tabindex="-1" aria-hidden="true">' +
122
+ '<div class="modal-dialog modal-dialog-centered"><form class="modal-content">' +
123
+ '<div class="modal-header">' +
124
+ '<h5 class="modal-title"></h5>' +
125
+ '<button type="button" class="btn-close" data-bs-dismiss="modal"></button>' +
126
+ '</div>' +
127
+ '<div class="modal-body"></div>' +
128
+ '<div class="modal-footer">' +
129
+ '<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"></button>' +
130
+ '<button type="submit" class="btn btn-primary"></button>' +
131
+ '</div>' +
132
+ '</form></div>' +
133
+ '</div>'
134
+ ).appendTo('body');
135
+
136
+ modal.find('.modal-title').text(ge.t('animation.title'));
137
+ modal.find('.modal-footer .btn-secondary').text(ge.t('animation.cancel'));
138
+ modal.find('.modal-footer .btn-primary').text(ge.t('animation.save'));
139
+ modal.find('.modal-body')
140
+ .append(field('animation.effect', effects))
141
+ .append(field('animation.duration', '<input class="form-control" type="number" name="duration" min="0" step="100">'))
142
+ .append(field('animation.delay', '<input class="form-control" type="number" name="delay" min="0" step="100">'))
143
+ .append($('<div class="form-check" />')
144
+ .append('<input class="form-check-input" type="checkbox" name="once" id="my-animation-once">')
145
+ .append($('<label class="form-check-label" for="my-animation-once" />').text(ge.t('animation.once'))));
146
+
147
+ return modal;
148
+ }
149
+
150
+ function open(node, kind, tool) {
151
+ var form = dialog().find('form');
152
+ var current = read(node) || {};
153
+
154
+ form.find('[name=effect]').val(current.effect || '');
155
+ form.find('[name=duration]').val(current.duration || 600);
156
+ form.find('[name=delay]').val(current.delay || 0);
157
+ form.find('[name=once]').prop('checked', current.once !== false);
158
+
159
+ form.off('submit').on('submit', function(e) {
160
+ e.preventDefault();
161
+
162
+ var effect = form.find('[name=effect]').val();
163
+ var value = effect ? {
164
+ effect: effect,
165
+ duration: parseInt(form.find('[name=duration]').val(), 10) || 0,
166
+ delay: parseInt(form.find('[name=delay]').val(), 10) || 0,
167
+ once: form.find('[name=once]').prop('checked'),
168
+ } : null;
169
+
170
+ var from = read(node);
171
+ write(node, value);
172
+ tool.toggleClass('my-animation-set', !!value);
173
+
174
+ // Announced like the editor's own changes, for a host
175
+ // that listens: grideditor:animation-change
176
+ ge.emit('animation-change', ge.payloadFor(kind, node, {
177
+ source: 'tool', from: from, to: value,
178
+ }));
179
+
180
+ window.bootstrap.Modal.getInstance(modal[0]).hide();
181
+ });
182
+
183
+ window.bootstrap.Modal.getOrCreateInstance(modal[0]).show();
184
+ }
185
+
186
+ return {
187
+ // Every drawer that has a gear, and the kind of its node:
188
+ // a row, a column, an element, a container's type, a pane
189
+ drawerTools: function(drawer, node, kind) {
190
+ var animated = ['row', 'column', 'section', 'element'].indexOf(kind) !== -1 ||
191
+ !!$.fn.gridEditor.containers[kind];
192
+ if (!animated) { return; }
193
+
194
+ ge.createTool(drawer, ge.t('animation.tool'), 'my-animation-tool', 'bi bi-magic', function() {
195
+ open(node, kind, tool);
196
+ });
197
+
198
+ // Drawers are built again on every init, so the tool
199
+ // reads the node rather than remembering anything
200
+ var tool = drawer.children('.my-animation-tool').last()
201
+ .toggleClass('my-animation-set', !!read(node));
202
+ },
203
+ };
204
+ };
205
+
206
+ })(jQuery);
207
+ </script>
208
+
209
+ <script>
210
+ $(function() {
211
+ $('#animation-source').text($('#animation-plugin').text().trim().replace(/^ {8}/gm, ''));
212
+
213
+ $('#myGrid').gridEditor({
214
+ new_row_layouts: [[12], [6, 6], [4, 4, 4]],
215
+ content_types: ['tinymce'],
216
+ });
217
+
218
+ $('#myGrid').on('grideditor:animation-change', function(e, payload) {
219
+ $('#log').text('The ' + payload.kind + ' now animates: ' +
220
+ (payload.to ? payload.to.effect : 'no'));
221
+ });
222
+
223
+ $('#output').on('click', function() {
224
+ $('#html').text($('#myGrid').gridEditor('getPlainHtml'));
225
+ });
226
+
227
+ // The published page: the plain html, and a few lines of the
228
+ // host's own that read data-animation. Nothing of grid-editor.
229
+ $('#published').on('click', function() {
230
+ var html = $('#myGrid').gridEditor('getPlainHtml');
231
+ var page = window.open('', '_blank');
232
+
233
+ page.document.write(
234
+ '<!DOCTYPE html><html><head>' +
235
+ '<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">' +
236
+ '<style>' +
237
+ '[data-animation] { transition-property: opacity, transform; }' +
238
+ '.animate-waiting { opacity: 0; }' +
239
+ '.animate-waiting.animate-slide { transform: translateY(40px); }' +
240
+ '.animate-waiting.animate-zoom { transform: scale(.85); }' +
241
+ 'body { padding-bottom: 60vh; }' +
242
+ '.scroll-hint { height: 100vh; }' +
243
+ '</style>' +
244
+ '</head><body>' +
245
+ '<div class="container scroll-hint d-flex align-items-center justify-content-center text-body-secondary">' +
246
+ 'Scroll down: each node animates as it comes into view' +
247
+ '</div>' +
248
+ '<div class="container">' + html + '</div>' +
249
+ '<script>' + $('#runtime').text() + '<\/script>' +
250
+ '</body></html>'
251
+ );
252
+ page.document.close();
253
+ });
254
+ });
255
+ </script>
256
+
257
+ <!-- What the published page runs: not grid-editor's, the host's -->
258
+ <script type="text/plain" id="runtime">
259
+ document.querySelectorAll('[data-animation]').forEach(function(node) {
260
+ var options = JSON.parse(node.getAttribute('data-animation'));
261
+ node.style.transitionDuration = options.duration + 'ms';
262
+ node.style.transitionDelay = options.delay + 'ms';
263
+ // Prefixed: Bootstrap has a .fade class of its own
264
+ node.classList.add('animate-waiting', 'animate-' + options.effect);
265
+
266
+ new IntersectionObserver(function(entries, observer) {
267
+ entries.forEach(function(entry) {
268
+ node.classList.toggle('animate-waiting', !entry.isIntersecting);
269
+ if (entry.isIntersecting && options.once) { observer.disconnect(); }
270
+ });
271
+ }, { threshold: 0.2 }).observe(node);
272
+ });
273
+ </script>
274
+
275
+ <title>Grid Editor example - a plugin with a modal</title>
276
+ </head>
277
+ <body>
278
+ <!-- Back to the demo index -->
279
+ <div class="container pt-3">
280
+ <a class="link-secondary" href="index.html"><i class="bi bi-arrow-left"></i> All demos</a>
281
+ </div>
282
+
283
+ <div class="container">
284
+
285
+ <div class="row my-3">
286
+ <div class="col-lg-9">
287
+ <h1 class="h4">A plugin with a modal</h1>
288
+ <p class="form-text">
289
+ The <i class="bi bi-magic"></i> tool in the drawer of a row,
290
+ a column, a card or an element is a plugin written on this
291
+ page. It opens a modal, and what you choose there is saved
292
+ on the node as a <code>data-animation</code> attribute, which
293
+ <code>getPlainHtml</code> keeps: only <code>data-ge-*</code>
294
+ attributes are the editor&rsquo;s to take off. A node with
295
+ an animation has its tool highlighted.
296
+ </p>
297
+ <p class="form-text">
298
+ The quote is an element, inside text tinyMCE edits. Open the
299
+ editor there, set an animation on the quote, keep typing
300
+ and press <kbd>Ctrl</kbd>+<kbd>Z</kbd>: the text goes, the
301
+ animation stays, because the plugin wrote it through
302
+ tinyMCE&rsquo;s undo manager. Written behind its back,
303
+ undoing the typing would undo the attribute with it.
304
+ </p>
305
+ <p class="form-text">
306
+ <em>Open as a published page</em> puts the plain html in a
307
+ page of its own, with a few lines that read the attribute
308
+ and animate each node as it scrolls into view.
309
+ <span id="log" class="fw-semibold"></span>
310
+ </p>
311
+ <button class="btn btn-sm btn-secondary" id="output" type="button">Show getPlainHtml</button>
312
+ <button class="btn btn-sm btn-primary" id="published" type="button">Open as a published page</button>
313
+ <pre id="html" class="form-text"></pre>
314
+ </div>
315
+ </div>
316
+
317
+ <div id="myGrid">
318
+ <div class="row" data-animation='{"effect":"fade","duration":800,"delay":0,"once":true}'>
319
+ <div class="col-lg-12">
320
+ <h2>This row fades in</h2>
321
+ <p>It came with its animation already set, in the markup the editor was started on.</p>
322
+ </div>
323
+ </div>
324
+ <div class="row">
325
+ <div class="col-lg-6">
326
+ <p>Ordinary text, edited by tinyMCE.</p>
327
+ <blockquote class="blockquote" data-ge-element="quote" data-ge-label="Quote">
328
+ <p>An element: give it an animation while you edit the text around it.</p>
329
+ </blockquote>
330
+ <p>More text, after the element.</p>
331
+ </div>
332
+ <div class="col-lg-6">
333
+ <div data-ge-container="card">
334
+ <div class="card">
335
+ <div class="card-header">A card</div>
336
+ <div class="card-body">
337
+ <div class="row"><div class="col-lg-12">
338
+ <p>Containers take the tool too.</p>
339
+ </div></div>
340
+ </div>
341
+ </div>
342
+ </div>
343
+ </div>
344
+ </div>
345
+ </div>
346
+
347
+ <div class="row my-4">
348
+ <div class="col-lg-10">
349
+ <h2 class="h5">The plugin, in full</h2>
350
+ <p class="form-text">
351
+ A feature plugin: <code>drawerTools</code> puts a tool in
352
+ every drawer that has a gear, and the kind says whose drawer
353
+ it is. See <a href="../docs/plugins.md">docs/plugins.md</a>.
354
+ </p>
355
+ <pre class="border rounded p-3 bg-light"><code id="animation-source"></code></pre>
356
+ </div>
357
+ </div>
358
+
359
+ </div>
360
+
361
+ </body>
362
+ </html>
@@ -59,9 +59,10 @@
59
59
  <i class="bi bi-copy"></i> copy tool in its drawer. Once something
60
60
  is copied, a <i class="bi bi-clipboard-plus"></i> paste tool shows
61
61
  wherever it can go: a row into a column or a section, a column into
62
- a row, a container or an element into a column. Rows and sections
63
- also get a <em>Paste</em> button in the toolbar, which can be
64
- clicked or dragged to where the copy should land.
62
+ a row, a container or an element into a column. A copied row or
63
+ section can also be pasted from the toolbar, with the
64
+ <i class="bi bi-clipboard-plus"></i> on the right: click it, or
65
+ drag it to where the copy should land.
65
66
  </p>
66
67
  <p class="text-body-secondary">
67
68
  What is copied is kept in the browser, so it can be pasted in the
@@ -61,6 +61,14 @@
61
61
  <span class="text-body-secondary">The plugin model, with a container plugin written in the page itself</span>
62
62
  </a>
63
63
 
64
+ <a class="list-group-item list-group-item-action" href="attributes.html">
65
+ <div class="d-flex justify-content-between">
66
+ <strong>A plugin with a modal</strong>
67
+ <code class="text-body-secondary">attributes.html</code>
68
+ </div>
69
+ <span class="text-body-secondary">A drawer tool that opens a modal and saves its settings on the node as an attribute, with tinyMCE&rsquo;s undo in mind</span>
70
+ </a>
71
+
64
72
  <a class="list-group-item list-group-item-action" href="containers.html">
65
73
  <div class="d-flex justify-content-between">
66
74
  <strong>Containers</strong>
package/package.json CHANGED
@@ -34,7 +34,7 @@
34
34
  "comment": "A scoped package publishes restricted unless it is told otherwise.",
35
35
  "access": "public"
36
36
  },
37
- "version": "5.1.0",
37
+ "version": "5.1.2",
38
38
  "main": "dist/jquery.grideditor.js",
39
39
  "files": [
40
40
  "dist",
@@ -894,7 +894,7 @@ $.fn.gridEditor = function( optionsOrMethod ) {
894
894
 
895
895
  // Add row
896
896
  addRowGroup = $('<div class="ge-addRowGroup btn-group" />').appendTo(wrapper);
897
- addContainerGroup = $('<div class="ge-addContainerGroup btn-group" />');
897
+ addContainerGroup = $('<div class="ge-addContainerGroup btn-group ms-1" />');
898
898
  $.each(settings.new_row_layouts, function(j, layout) {
899
899
  var grouped = !Array.isArray(layout);
900
900
  var btn = $('<a class="btn btn-sm btn-primary" />')
@@ -961,25 +961,19 @@ $.fn.gridEditor = function( optionsOrMethod ) {
961
961
  });
962
962
 
963
963
  // A feature plugin's own buttons, beside the containers': what
964
- // one makes goes onto the canvas, where the plugin says it belongs
964
+ // one makes goes onto the canvas, where the plugin says it belongs.
965
+ // One with align 'end' goes on the right instead, as an icon,
966
+ // beside the source and preview buttons.
967
+ var endItems = [];
965
968
  $.each(FEATURES, function(name, feature) {
966
969
  (feature.toolbar || []).forEach(function(item, index) {
967
- $('<a class="btn btn-sm btn-primary ge-add-container ge-add-feature" />')
968
- .attr('title', t(item.labelKey))
969
- .attr('data-ge-toolbar', 'feature')
970
- .attr('data-ge-feature', name)
971
- .attr('data-ge-item', index)
972
- .append($('<i />').addClass(item.iconClass || 'bi bi-plus'))
973
- .append($('<span />').text(t(item.labelKey)))
974
- .on('click', function() {
975
- var made = item.create();
976
-
977
- addNode(item.kind, made, function() {
978
- made.appendTo(canvas);
979
- }, { parent: canvas, source: item.source || 'tool' });
980
- })
981
- .appendTo(addContainerGroup)
982
- ;
970
+ var button = featureButton(name, item, index);
971
+
972
+ if (item.align === 'end') {
973
+ endItems.push(button);
974
+ } else {
975
+ button.appendTo(addContainerGroup);
976
+ }
983
977
  });
984
978
  });
985
979
 
@@ -1045,8 +1039,41 @@ $.fn.gridEditor = function( optionsOrMethod ) {
1045
1039
  .appendTo(btnGroup)
1046
1040
  ;
1047
1041
 
1042
+ // Floated right after the source and preview buttons, so it
1043
+ // stands to their left. Not a btn-group: its buttons show one at
1044
+ // a time, and a hidden one still counts as a neighbour to
1045
+ // Bootstrap, which squares off the corners they would share.
1046
+ if (endItems.length) {
1047
+ $('<div class="pull-right ge-toolbar-end" />')
1048
+ .append(endItems)
1049
+ .appendTo(wrapper)
1050
+ ;
1051
+ }
1052
+
1048
1053
  makeToolbarDraggable();
1049
1054
  }
1055
+
1056
+ /** A feature plugin's toolbar button: its label as text, or as the title alone at the end. */
1057
+ function featureButton(name, item, index) {
1058
+ var button = $('<a class="btn btn-sm btn-primary ge-add-container ge-add-feature" />')
1059
+ .attr('title', t(item.labelKey))
1060
+ .attr('data-ge-toolbar', 'feature')
1061
+ .attr('data-ge-feature', name)
1062
+ .attr('data-ge-item', index)
1063
+ .append($('<i />').addClass(item.iconClass || 'bi bi-plus'))
1064
+ .on('click', function() {
1065
+ var made = item.create();
1066
+
1067
+ addNode(item.kind, made, function() {
1068
+ made.appendTo(canvas);
1069
+ }, { parent: canvas, source: item.source || 'tool' });
1070
+ })
1071
+ ;
1072
+
1073
+ if (item.align !== 'end') { button.append($('<span />').text(t(item.labelKey))); }
1074
+
1075
+ return button;
1076
+ }
1050
1077
 
1051
1078
  /**
1052
1079
  * The toolbar's buttons as a palette: drag one onto the canvas and
@@ -64,6 +64,14 @@
64
64
  // furniture back (see RTE_READY in the core).
65
65
  contentArea.trigger('ge-rte-ready');
66
66
 
67
+ // Undo and redo rewrite it too, from snapshots that
68
+ // leave out whatever is marked data-mce-bogus - an
69
+ // element's drawer among them - so it goes back in
70
+ // after each of those as well
71
+ editor.on('Undo Redo', function() {
72
+ contentArea.trigger('ge-rte-ready');
73
+ });
74
+
67
75
  // The inline toolbar is laid out against the element's
68
76
  // geometry at the moment tinyMCE draws it, and an
69
77
  // element that has just appeared - a tab pane, an
@@ -232,6 +232,9 @@ $.fn.gridEditor.features.clipboard = function(ge) {
232
232
  ].map(function(item) {
233
233
  return $.extend(item, {
234
234
  iconClass: 'bi bi-clipboard-plus',
235
+ // On the right, as an icon: pasting is not one of the things
236
+ // the add buttons make
237
+ align: 'end',
235
238
  source: 'paste',
236
239
  // What the button showed, even if another tab has copied
237
240
  // something else since
@@ -64,6 +64,14 @@
64
64
  .ge-layout-mode {
65
65
  margin-left: 5px;
66
66
  }
67
+
68
+ .ge-toolbar-end {
69
+ margin-right: 5px;
70
+
71
+ > .btn + .btn {
72
+ margin-left: 5px;
73
+ }
74
+ }
67
75
 
68
76
  .btn:focus {
69
77
  box-shadow: none;