@themarioga/grid-editor 5.1.1 → 5.2.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.
Files changed (41) hide show
  1. package/BUILDING.md +6 -0
  2. package/CHANGELOG.md +45 -0
  3. package/README.md +18 -8
  4. package/UPGRADING.md +25 -0
  5. package/dist/grideditor.css +3 -0
  6. package/dist/grideditor.min.css +1 -1
  7. package/dist/grideditor.min.css.map +1 -1
  8. package/dist/jquery.grideditor.bundle.min.js +2 -2
  9. package/dist/jquery.grideditor.js +374 -218
  10. package/dist/jquery.grideditor.min.js +1 -1
  11. package/dist/jquery.grideditor.min.js.map +1 -1
  12. package/dist/plugins/grideditor.ckeditor.js +115 -0
  13. package/dist/plugins/grideditor.ckeditor.min.js +2 -0
  14. package/dist/plugins/grideditor.ckeditor.min.js.map +1 -0
  15. package/dist/plugins/grideditor.summernote.js +93 -0
  16. package/dist/plugins/grideditor.summernote.min.js +2 -0
  17. package/dist/plugins/grideditor.summernote.min.js.map +1 -0
  18. package/dist/plugins/grideditor.tinymce.js +157 -0
  19. package/dist/plugins/grideditor.tinymce.min.js +2 -0
  20. package/dist/plugins/grideditor.tinymce.min.js.map +1 -0
  21. package/docs/plugins.md +86 -3
  22. package/example/attributes.html +364 -0
  23. package/example/autosave.html +2 -0
  24. package/example/basic.html +2 -0
  25. package/example/ckeditor.html +2 -0
  26. package/example/clipboard.html +2 -0
  27. package/example/containers.html +2 -0
  28. package/example/elements.html +2 -0
  29. package/example/index.html +8 -0
  30. package/example/summernote.html +2 -0
  31. package/example/wrap_content.html +2 -0
  32. package/package.json +1 -1
  33. package/src/js/compat/bundled-texts.js +14 -0
  34. package/src/js/jquery.grideditor.js +73 -14
  35. package/src/js/plugins/grideditor.ckeditor.js +115 -0
  36. package/src/js/plugins/grideditor.summernote.js +93 -0
  37. package/src/js/plugins/grideditor.tinymce.js +157 -0
  38. package/src/less/grideditor.less +4 -0
  39. package/src/js/jquery.grideditor.ckeditor.js +0 -77
  40. package/src/js/jquery.grideditor.summernote.js +0 -72
  41. package/src/js/jquery.grideditor.tinymce.js +0 -135
@@ -0,0 +1,157 @@
1
+ /**
2
+ * tinyMCE for grid-editor's content areas.
3
+ *
4
+ * A text editor plugin: load this file after the editor, and tinyMCE 6
5
+ * after or before it, and content_types: ['tinymce'] edits each content area
6
+ * with an inline tinyMCE. What it can ask the editor for is the handle its
7
+ * factory is called with, described in docs/plugins.md.
8
+ *
9
+ * <script src="tinymce/tinymce.min.js"></script>
10
+ * <script src="dist/jquery.grideditor.min.js"></script>
11
+ * <script src="dist/plugins/grideditor.tinymce.min.js"></script>
12
+ *
13
+ * Up to 6.0 the main bundle carries a copy of this file too.
14
+ */
15
+ (function($) {
16
+
17
+ // tinyMCE snapshots the target element's attributes when an inline editor is
18
+ // created and restores them on remove(), so this has to run *after* remove()
19
+ // to keep the grid editor's own class and tinyMCE's leftovers off the element.
20
+ function cleanUp(contentArea) {
21
+ contentArea
22
+ .removeClass('active')
23
+ .removeClass('ge-rte-active')
24
+ .removeAttr('id')
25
+ .removeAttr('style')
26
+ .removeAttr('spellcheck')
27
+ .removeAttr('contenteditable')
28
+ .removeAttr('data-mce-style')
29
+ ;
30
+ }
31
+
32
+ var INITIAL_CONTENT = '<p>Lorem ipsum dolores</p>';
33
+
34
+ $.fn.gridEditor.texts.tinymce = function(ge) {
35
+ return {
36
+ initialContent: INITIAL_CONTENT,
37
+ missingKey: 'error.tinymce_missing',
38
+
39
+ available: function() { return !!window.tinymce; },
40
+
41
+ start: function(contentAreas) {
42
+ var settings = ge.settings;
43
+ var userConfig = (settings.tinymce && settings.tinymce.config) ? settings.tinymce.config : {};
44
+
45
+ contentAreas.each(function() {
46
+ var contentArea = $(this);
47
+ if (contentArea.hasClass('active')) { return; }
48
+
49
+ if (contentArea.html() == INITIAL_CONTENT) {
50
+ contentArea.html('');
51
+ }
52
+ contentArea.addClass('active');
53
+
54
+ var configuration = $.extend({
55
+ // tinyMCE's own "Upgrade" badge in the menubar. Off by
56
+ // default because an inline editor here is a column of
57
+ // someone's page, not tinyMCE's own interface; a host that
58
+ // wants it back passes promotion: true.
59
+ promotion: false,
60
+ }, userConfig, {
61
+ target: this,
62
+ inline: true,
63
+ init_instance_callback: function(editor) {
64
+ // deinit ran before this init finished, so tear it down again
65
+ if (contentArea.data('ge-tinymce-pending-remove')) {
66
+ contentArea.removeData('ge-tinymce-pending-remove');
67
+ editor.remove();
68
+ // deinit already ran and cannot clean up after this
69
+ // late remove(), so do it here instead
70
+ cleanUp(contentArea);
71
+ return;
72
+ }
73
+
74
+ contentArea.data('ge-tinymce', editor);
75
+
76
+ // The editor rewrote what is inside this content area
77
+ // while it took it over, so whatever the grid editor
78
+ // had in there is gone. Saying so lets it put its own
79
+ // furniture back.
80
+ ge.textReady(contentArea);
81
+
82
+ // Undo and redo rewrite it too, from snapshots that
83
+ // leave out whatever is marked data-mce-bogus - an
84
+ // element's drawer among them - so it goes back in
85
+ // after each of those as well
86
+ editor.on('Undo Redo', function() {
87
+ ge.textReady(contentArea);
88
+ });
89
+
90
+ // The inline toolbar is laid out against the element's
91
+ // geometry at the moment tinyMCE draws it, and an
92
+ // element that has just appeared - a tab pane, an
93
+ // accordion body, a column whose width is still
94
+ // settling - may not have its own width yet. Asking
95
+ // for the ui again once the browser has laid the frame
96
+ // out measures it as it now is, instead of leaving a
97
+ // toolbar wrapped into a narrow column.
98
+ window.requestAnimationFrame(function() {
99
+ if (editor.removed || !editor.ui || !editor.ui.show) { return; }
100
+
101
+ editor.ui.show();
102
+ });
103
+
104
+ // And again whenever the user comes back to this
105
+ // editor: by then the element may have been resized,
106
+ // hidden and shown again - a tab switched away from
107
+ // and back, a column made narrower - and the toolbar
108
+ // is only ever as right as its last measurement.
109
+ editor.on('focus', function() {
110
+ window.requestAnimationFrame(function() {
111
+ if (editor.removed || !editor.ui || !editor.ui.show) { return; }
112
+
113
+ editor.ui.show();
114
+ });
115
+ });
116
+
117
+ // Bring focus to text field
118
+ editor.focus();
119
+
120
+ // Call the original callbacks, if any were passed in the config
121
+ if (userConfig.init_instance_callback) {
122
+ userConfig.init_instance_callback.call(this, editor);
123
+ }
124
+ // 'oninit' is the pre-6 name, honoured so existing configs keep working
125
+ if (userConfig.oninit) {
126
+ userConfig.oninit.call(this, editor);
127
+ }
128
+ }
129
+ });
130
+ // We always edit the element we were handed, so a selector in the
131
+ // user config would only fight with target
132
+ delete configuration.selector;
133
+
134
+ window.tinymce.init(configuration);
135
+ });
136
+ },
137
+
138
+ stop: function(contentAreas) {
139
+ contentAreas.filter('.active').each(function() {
140
+ var contentArea = $(this);
141
+ var editor = contentArea.data('ge-tinymce');
142
+
143
+ if (editor) {
144
+ contentArea.removeData('ge-tinymce');
145
+ editor.remove();
146
+ } else {
147
+ // tinymce.init() is asynchronous, so the editor may not exist
148
+ // yet. Leave a note for init_instance_callback to remove it.
149
+ contentArea.data('ge-tinymce-pending-remove', true);
150
+ }
151
+
152
+ cleanUp(contentArea);
153
+ });
154
+ },
155
+ };
156
+ };
157
+ })(jQuery);
@@ -0,0 +1,2 @@
1
+ (o=>{function a(e){e.removeClass("active").removeClass("ge-rte-active").removeAttr("id").removeAttr("style").removeAttr("spellcheck").removeAttr("contenteditable").removeAttr("data-mce-style")}var c="<p>Lorem ipsum dolores</p>";o.fn.gridEditor.texts.tinymce=function(n){return{initialContent:c,missingKey:"error.tinymce_missing",available:function(){return!!window.tinymce},start:function(e){var t=n.settings,i=t.tinymce&&t.tinymce.config?t.tinymce.config:{};e.each(function(){var e,t=o(this);t.hasClass("active")||(t.html()==c&&t.html(""),t.addClass("active"),delete(e=o.extend({promotion:!1},i,{target:this,inline:!0,init_instance_callback:function(e){t.data("ge-tinymce-pending-remove")?(t.removeData("ge-tinymce-pending-remove"),e.remove(),a(t)):(t.data("ge-tinymce",e),n.textReady(t),e.on("Undo Redo",function(){n.textReady(t)}),window.requestAnimationFrame(function(){!e.removed&&e.ui&&e.ui.show&&e.ui.show()}),e.on("focus",function(){window.requestAnimationFrame(function(){!e.removed&&e.ui&&e.ui.show&&e.ui.show()})}),e.focus(),i.init_instance_callback&&i.init_instance_callback.call(this,e),i.oninit&&i.oninit.call(this,e))}})).selector,window.tinymce.init(e))})},stop:function(e){e.filter(".active").each(function(){var e=o(this),t=e.data("ge-tinymce");t?(e.removeData("ge-tinymce"),t.remove()):e.data("ge-tinymce-pending-remove",!0),a(e)})}}}})(jQuery);
2
+ //# sourceMappingURL=grideditor.tinymce.min.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grideditor.tinymce.min.js","sources":["../../src/js/plugins/grideditor.tinymce.js"],"names":["$","cleanUp","contentArea","removeClass","removeAttr","INITIAL_CONTENT","fn","gridEditor","texts","tinymce","ge","initialContent","missingKey","available","window","start","contentAreas","settings","userConfig","config","each","configuration","this","hasClass","html","addClass","extend","promotion","target","inline","init_instance_callback","editor","data","removeData","remove","textReady","on","requestAnimationFrame","removed","ui","show","focus","call","oninit","selector","init","stop","filter","jQuery"],"mappings":"CAcUA,IAKN,SAASC,EAAQC,GACbA,EACKC,YAAY,QAAQ,EACpBA,YAAY,eAAe,EAC3BC,WAAW,IAAI,EACfA,WAAW,OAAO,EAClBA,WAAW,YAAY,EACvBA,WAAW,iBAAiB,EAC5BA,WAAW,gBAAgB,CAEpC,CAEA,IAAIC,EAAkB,6BAEtBL,EAAEM,GAAGC,WAAWC,MAAMC,QAAU,SAASC,GACrC,MAAO,CACHC,eAAgBN,EAChBO,WAAY,wBAEZC,UAAW,WAAa,MAAO,CAAC,CAACC,OAAOL,OAAS,EAEjDM,MAAO,SAASC,GACZ,IAAIC,EAAWP,EAAGO,SACdC,EAAcD,EAASR,SAAWQ,EAASR,QAAQU,OAAUF,EAASR,QAAQU,OAAS,GAE3FH,EAAaI,KAAK,WACd,IAQIC,EARAnB,EAAcF,EAAEsB,IAAI,EACpBpB,EAAYqB,SAAS,QAAQ,IAE7BrB,EAAYsB,KAAK,GAAKnB,GACtBH,EAAYsB,KAAK,EAAE,EAEvBtB,EAAYuB,SAAS,QAAQ,EAgF7B,OA9EIJ,EAAgBrB,EAAE0B,OAAO,CAKzBC,UAAW,CAAA,CACf,EAAGT,EAAY,CACXU,OAAQN,KACRO,OAAQ,CAAA,EACRC,uBAAwB,SAASC,GAEzB7B,EAAY8B,KAAK,2BAA2B,GAC5C9B,EAAY+B,WAAW,2BAA2B,EAClDF,EAAOG,OAAO,EAGdjC,EAAQC,CAAW,IAIvBA,EAAY8B,KAAK,aAAcD,CAAM,EAMrCrB,EAAGyB,UAAUjC,CAAW,EAMxB6B,EAAOK,GAAG,YAAa,WACnB1B,EAAGyB,UAAUjC,CAAW,CAC5B,CAAC,EAUDY,OAAOuB,sBAAsB,WACrBN,CAAAA,EAAOO,SAAYP,EAAOQ,IAAOR,EAAOQ,GAAGC,MAE/CT,EAAOQ,GAAGC,KAAK,CACnB,CAAC,EAODT,EAAOK,GAAG,QAAS,WACftB,OAAOuB,sBAAsB,WACrBN,CAAAA,EAAOO,SAAYP,EAAOQ,IAAOR,EAAOQ,GAAGC,MAE/CT,EAAOQ,GAAGC,KAAK,CACnB,CAAC,CACL,CAAC,EAGDT,EAAOU,MAAM,EAGTvB,EAAWY,wBACXZ,EAAWY,uBAAuBY,KAAKpB,KAAMS,CAAM,EAGnDb,EAAWyB,QACXzB,EAAWyB,OAAOD,KAAKpB,KAAMS,CAAM,EAE3C,CACJ,CAAC,GAGoBa,SAErB9B,OAAOL,QAAQoC,KAAKxB,CAAa,EACrC,CAAC,CACL,EAEAyB,KAAM,SAAS9B,GACXA,EAAa+B,OAAO,SAAS,EAAE3B,KAAK,WAChC,IAAIlB,EAAcF,EAAEsB,IAAI,EACpBS,EAAS7B,EAAY8B,KAAK,YAAY,EAEtCD,GACA7B,EAAY+B,WAAW,YAAY,EACnCF,EAAOG,OAAO,GAIdhC,EAAY8B,KAAK,4BAA6B,CAAA,CAAI,EAGtD/B,EAAQC,CAAW,CACvB,CAAC,CACL,CACJ,CACJ,CACH,GAAE8C,MAAM"}
package/docs/plugins.md CHANGED
@@ -14,13 +14,15 @@ the one to read first if you are about to write your own.
14
14
  <script src="dist/plugins/grideditor.elements.min.js"></script>
15
15
  ```
16
16
 
17
- There are three kinds. A **container plugin** builds a type of container — it
17
+ There are four kinds. A **container plugin** builds a type of container — it
18
18
  registers under `$.fn.gridEditor.containers`, and the toolbar offers a button
19
19
  for it. A **utility plugin** declares families of Bootstrap's responsive
20
20
  utility classes — `$.fn.gridEditor.utilities` — and the editor edits them per
21
- breakpoint. A **feature plugin** is anything else the editor can do —
21
+ breakpoint. A **text editor plugin** edits content areas with a rich text
22
+ editor — `$.fn.gridEditor.texts`, one per editor: tinyMCE, CKEditor,
23
+ summernote. A **feature plugin** is anything else the editor can do —
22
24
  `$.fn.gridEditor.features`, hooks into the canvas, and may contribute methods.
23
- All three are factories called once per editor with the same handle.
25
+ All four are factories called once per editor with the same handle.
24
26
 
25
27
  The `plugins` setting names which of the loaded ones to use:
26
28
 
@@ -124,6 +126,7 @@ not change without a major version.
124
126
  | `ge.setUtility(node, family, value, options?)` | Write one through the events. `options` is a view key or `{ view, source }` |
125
127
  | `ge.utilityField(node, family)` | A panel field for one family, for a plugin that builds its own panel |
126
128
  | `ge.rowFromLayout(layout)` | A detached row from a layout: `[8, 4]`, `['auto', 'equal']` or `{ row_cols, columns }` |
129
+ | `ge.textReady(block)` | A text editor has rewritten a content area: whatever the editor and its plugins put in there goes back in |
127
130
  | `ge.nodeHtml(node)` | One node's markup as `getHtml` would give it. The canvas leaves editing to read it and comes back, as it does for `getHtml` |
128
131
  | `ge.toolbarItems(name)` | The toolbar buttons the feature plugin `name` declared, for showing and hiding them |
129
132
  | `ge.bareStyle(node, family, property)` | A css property's value on the node with none of the family's classes: what a preview shows when no class applies and that is not a constant |
@@ -174,6 +177,37 @@ needs to put a new kind of block on the canvas:
174
177
  - **`drawerTools(drawer, node, kind)`** runs for every drawer that has a gear,
175
178
  as a utility plugin's does, so a tool can go on any node. The clipboard
176
179
  plugin puts its copy and paste tools in with it.
180
+
181
+ ### Settings of your own, saved on the node
182
+
183
+ A tool that opens a dialog and saves what the user chose on the node is a
184
+ feature plugin with `drawerTools` and nothing else.
185
+ [example/attributes.html](../example/attributes.html) is one, in full: an
186
+ animation for rows, columns, containers and elements, kept in a
187
+ `data-animation` attribute. Three things to get right:
188
+
189
+ - **The attribute's name.** Anything but `data-ge-*`, which `getPlainHtml`
190
+ takes off. `getHtml` keeps every attribute that is not the editor's.
191
+ - **The dialog lives outside the canvas**, appended to `body`, so it is never
192
+ part of the markup.
193
+ - **An element sits inside text a rich text editor may be editing.** Written
194
+ behind its back, the attribute is lost the next time the user undoes
195
+ something: undo restores a snapshot from before it. With tinyMCE, write it
196
+ through the editor's undo manager, and it becomes a step of its own:
197
+
198
+ ```javascript
199
+ var area = node.closest('.ge-content')[0];
200
+ var editor = window.tinymce && tinymce.get().filter(function(e) { return e.getBody() === area; })[0];
201
+ var apply = function() { node.attr('data-animation', JSON.stringify(value)); };
202
+
203
+ if (editor) { editor.undoManager.transact(apply); } else { apply(); }
204
+ ```
205
+
206
+ Rows, columns, sections and containers are outside any content area, and a
207
+ plain `attr()` is all they need.
208
+
209
+ A node's drawer is built again on every `init`, so a tool reads the node
210
+ rather than remembering anything about it.
177
211
  - A **`toolbar`** item can also have an `iconClass`, `bi bi-plus` otherwise,
178
212
  and a `source` for the add events, `tool` or `dragdrop` otherwise. With
179
213
  `align: 'end'` it goes on the right of the toolbar, beside the source and
@@ -220,6 +254,55 @@ plugin knows which of its parts move: the tabs plugin makes its strip sortable
220
254
  and puts the panes back in strip order in `afterPaneMove`.
221
255
 
222
256
 
257
+ Text editor plugins
258
+ -------------------
259
+
260
+ A text editor plugin edits content areas with a rich text editor. It is
261
+ registered under the content type it edits - the name `content_types` uses -
262
+ and the three that ship, `grideditor.tinymce.js`, `grideditor.ckeditor.js` and
263
+ `grideditor.summernote.js`, are the ones to read:
264
+
265
+ ```javascript
266
+ $.fn.gridEditor.texts.mytext = function(ge) {
267
+ return {
268
+ initialContent: '<p>Write here</p>', // what a new column's content area holds
269
+ missingKey: 'error.mytext_missing', // the error when the library is not loaded, optional
270
+ available: function() { return !!window.MyText; }, // optional, true otherwise
271
+
272
+ start: function(block) { … }, // open the editor on this content area
273
+ stop: function(block) { … }, // close it, and leave the content area as it was
274
+ };
275
+ };
276
+ ```
277
+
278
+ - **`start(block)`** is called when the user clicks a content area of this
279
+ type. The editor marks it `ge-rte-active` first, and does not call `start`
280
+ again until it has been stopped. Once the library has taken the content
281
+ area over, call **`ge.textReady(block)`**: it rewrote what was inside, and
282
+ the editor and its plugins put their own furniture back - an element's
283
+ drawer, for one. Call it again whenever the library rewrites the content
284
+ area again, as tinyMCE's undo does.
285
+ - **`stop(block)`** is called for every content area of this type on each
286
+ `deinit`, which `getHtml` runs: a content area whose editor never started is
287
+ the plugin's to ignore. Whatever the library and `start` added - classes,
288
+ `contenteditable`, inline styles, ids - comes off here.
289
+ - **`available()`** false and the editor logs `missingKey`'s message and
290
+ starts nothing, without marking the content area, so a later click tries
291
+ again once the library is there.
292
+
293
+ A text editor is chosen by `content_types`, never by the `plugins` setting: a
294
+ page that names its containers there has named no editor.
295
+
296
+ Up to 6.0 the main bundle carries a copy of the three shipped ones, marked
297
+ `bundled`; editing with a copy warns once that 6.0 leaves them out. A plugin's
298
+ own file, loaded after, registers itself again and is the one used.
299
+
300
+ 5.x's `$.fn.gridEditor.RTEs.mytext = { init, deinit, initialContent }` still
301
+ works: `init(settings, contentAreas)` and `deinit(settings, contentAreas)` are
302
+ wrapped as `start` and `stop`, with a warning that `RTEs` is deprecated. It goes
303
+ in 7.0.
304
+
305
+
223
306
  Utility plugins
224
307
  ---------------
225
308
 
@@ -0,0 +1,364 @@
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
+ <!-- The text editor is a plugin of its own, loaded after the editor -->
21
+ <script src="../dist/plugins/grideditor.tinymce.min.js"></script>
22
+ <script src="../dist/plugins/grideditor.card.min.js"></script>
23
+ <script src="../dist/plugins/grideditor.elements.min.js"></script>
24
+
25
+ <style>
26
+ /* The drawer tool of a node that has an animation set. Under the
27
+ canvas's id, to outweigh the colour the editor gives each kind of
28
+ drawer's tools */
29
+ #myGrid .ge-tools-drawer > a.my-animation-tool.my-animation-set {
30
+ color: #d63384;
31
+ }
32
+ </style>
33
+
34
+ <!-- A plugin of this page's own: a drawer tool that opens a modal, and
35
+ what the user chooses there saved as an attribute on the node. -->
36
+ <script id="animation-plugin">
37
+ (function($) {
38
+
39
+ $.extend($.fn.gridEditor.locales.en, {
40
+ 'animation.tool': 'Animation',
41
+ 'animation.title': 'Animate on scroll',
42
+ 'animation.effect': 'Effect',
43
+ 'animation.none': 'None',
44
+ 'animation.fade': 'Fade in',
45
+ 'animation.slide': 'Slide up',
46
+ 'animation.zoom': 'Zoom in',
47
+ 'animation.duration': 'Duration (ms)',
48
+ 'animation.delay': 'Delay (ms)',
49
+ 'animation.once': 'Only the first time it scrolls into view',
50
+ 'animation.cancel': 'Cancel',
51
+ 'animation.save': 'Save',
52
+ });
53
+
54
+ // Not data-ge-*: getPlainHtml takes those off, and this has to
55
+ // reach the published page
56
+ var ATTRIBUTE = 'data-animation';
57
+ var EFFECTS = ['fade', 'slide', 'zoom'];
58
+
59
+ $.fn.gridEditor.features.animation = function(ge) {
60
+
61
+ var modal = null;
62
+
63
+ function read(node) {
64
+ try {
65
+ return JSON.parse(node.attr(ATTRIBUTE) || 'null');
66
+ } catch (error) {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * An element lives inside a content area, and while a rich
73
+ * text editor is open there, a change it does not know about
74
+ * is one its undo would take back: undoing the next thing the
75
+ * user types restores a snapshot from before the attribute.
76
+ * Made through tinyMCE's undo manager, it is a step of its
77
+ * own, which undo and redo treat like any other.
78
+ */
79
+ function write(node, value) {
80
+ var apply = function() {
81
+ if (value) {
82
+ node.attr(ATTRIBUTE, JSON.stringify(value));
83
+ } else {
84
+ node.removeAttr(ATTRIBUTE);
85
+ }
86
+ };
87
+ var editor = editorAround(node);
88
+
89
+ if (editor) {
90
+ editor.undoManager.transact(apply);
91
+ } else {
92
+ apply();
93
+ }
94
+ }
95
+
96
+ /** The tinyMCE editor open on the content area around node, if there is one. */
97
+ function editorAround(node) {
98
+ var area = node.closest('.ge-content');
99
+ if (!area.length || !window.tinymce) { return null; }
100
+
101
+ return window.tinymce.get().filter(function(editor) {
102
+ return editor.getBody && editor.getBody() === area[0];
103
+ })[0] || null;
104
+ }
105
+
106
+ /** Built once, outside the canvas, so it is never part of getHtml. */
107
+ function dialog() {
108
+ if (modal) { return modal; }
109
+
110
+ var effects = $('<select class="form-select" name="effect" />')
111
+ .append($('<option value="" />').text(ge.t('animation.none')));
112
+ EFFECTS.forEach(function(effect) {
113
+ effects.append($('<option />').val(effect).text(ge.t('animation.' + effect)));
114
+ });
115
+
116
+ var field = function(label, input) {
117
+ return $('<div class="mb-3" />')
118
+ .append($('<label class="form-label" />').text(ge.t(label)))
119
+ .append(input);
120
+ };
121
+
122
+ modal = $(
123
+ '<div class="modal fade my-animation-modal" tabindex="-1" aria-hidden="true">' +
124
+ '<div class="modal-dialog modal-dialog-centered"><form class="modal-content">' +
125
+ '<div class="modal-header">' +
126
+ '<h5 class="modal-title"></h5>' +
127
+ '<button type="button" class="btn-close" data-bs-dismiss="modal"></button>' +
128
+ '</div>' +
129
+ '<div class="modal-body"></div>' +
130
+ '<div class="modal-footer">' +
131
+ '<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"></button>' +
132
+ '<button type="submit" class="btn btn-primary"></button>' +
133
+ '</div>' +
134
+ '</form></div>' +
135
+ '</div>'
136
+ ).appendTo('body');
137
+
138
+ modal.find('.modal-title').text(ge.t('animation.title'));
139
+ modal.find('.modal-footer .btn-secondary').text(ge.t('animation.cancel'));
140
+ modal.find('.modal-footer .btn-primary').text(ge.t('animation.save'));
141
+ modal.find('.modal-body')
142
+ .append(field('animation.effect', effects))
143
+ .append(field('animation.duration', '<input class="form-control" type="number" name="duration" min="0" step="100">'))
144
+ .append(field('animation.delay', '<input class="form-control" type="number" name="delay" min="0" step="100">'))
145
+ .append($('<div class="form-check" />')
146
+ .append('<input class="form-check-input" type="checkbox" name="once" id="my-animation-once">')
147
+ .append($('<label class="form-check-label" for="my-animation-once" />').text(ge.t('animation.once'))));
148
+
149
+ return modal;
150
+ }
151
+
152
+ function open(node, kind, tool) {
153
+ var form = dialog().find('form');
154
+ var current = read(node) || {};
155
+
156
+ form.find('[name=effect]').val(current.effect || '');
157
+ form.find('[name=duration]').val(current.duration || 600);
158
+ form.find('[name=delay]').val(current.delay || 0);
159
+ form.find('[name=once]').prop('checked', current.once !== false);
160
+
161
+ form.off('submit').on('submit', function(e) {
162
+ e.preventDefault();
163
+
164
+ var effect = form.find('[name=effect]').val();
165
+ var value = effect ? {
166
+ effect: effect,
167
+ duration: parseInt(form.find('[name=duration]').val(), 10) || 0,
168
+ delay: parseInt(form.find('[name=delay]').val(), 10) || 0,
169
+ once: form.find('[name=once]').prop('checked'),
170
+ } : null;
171
+
172
+ var from = read(node);
173
+ write(node, value);
174
+ tool.toggleClass('my-animation-set', !!value);
175
+
176
+ // Announced like the editor's own changes, for a host
177
+ // that listens: grideditor:animation-change
178
+ ge.emit('animation-change', ge.payloadFor(kind, node, {
179
+ source: 'tool', from: from, to: value,
180
+ }));
181
+
182
+ window.bootstrap.Modal.getInstance(modal[0]).hide();
183
+ });
184
+
185
+ window.bootstrap.Modal.getOrCreateInstance(modal[0]).show();
186
+ }
187
+
188
+ return {
189
+ // Every drawer that has a gear, and the kind of its node:
190
+ // a row, a column, an element, a container's type, a pane
191
+ drawerTools: function(drawer, node, kind) {
192
+ var animated = ['row', 'column', 'section', 'element'].indexOf(kind) !== -1 ||
193
+ !!$.fn.gridEditor.containers[kind];
194
+ if (!animated) { return; }
195
+
196
+ ge.createTool(drawer, ge.t('animation.tool'), 'my-animation-tool', 'bi bi-magic', function() {
197
+ open(node, kind, tool);
198
+ });
199
+
200
+ // Drawers are built again on every init, so the tool
201
+ // reads the node rather than remembering anything
202
+ var tool = drawer.children('.my-animation-tool').last()
203
+ .toggleClass('my-animation-set', !!read(node));
204
+ },
205
+ };
206
+ };
207
+
208
+ })(jQuery);
209
+ </script>
210
+
211
+ <script>
212
+ $(function() {
213
+ $('#animation-source').text($('#animation-plugin').text().trim().replace(/^ {8}/gm, ''));
214
+
215
+ $('#myGrid').gridEditor({
216
+ new_row_layouts: [[12], [6, 6], [4, 4, 4]],
217
+ content_types: ['tinymce'],
218
+ });
219
+
220
+ $('#myGrid').on('grideditor:animation-change', function(e, payload) {
221
+ $('#log').text('The ' + payload.kind + ' now animates: ' +
222
+ (payload.to ? payload.to.effect : 'no'));
223
+ });
224
+
225
+ $('#output').on('click', function() {
226
+ $('#html').text($('#myGrid').gridEditor('getPlainHtml'));
227
+ });
228
+
229
+ // The published page: the plain html, and a few lines of the
230
+ // host's own that read data-animation. Nothing of grid-editor.
231
+ $('#published').on('click', function() {
232
+ var html = $('#myGrid').gridEditor('getPlainHtml');
233
+ var page = window.open('', '_blank');
234
+
235
+ page.document.write(
236
+ '<!DOCTYPE html><html><head>' +
237
+ '<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">' +
238
+ '<style>' +
239
+ '[data-animation] { transition-property: opacity, transform; }' +
240
+ '.animate-waiting { opacity: 0; }' +
241
+ '.animate-waiting.animate-slide { transform: translateY(40px); }' +
242
+ '.animate-waiting.animate-zoom { transform: scale(.85); }' +
243
+ 'body { padding-bottom: 60vh; }' +
244
+ '.scroll-hint { height: 100vh; }' +
245
+ '</style>' +
246
+ '</head><body>' +
247
+ '<div class="container scroll-hint d-flex align-items-center justify-content-center text-body-secondary">' +
248
+ 'Scroll down: each node animates as it comes into view' +
249
+ '</div>' +
250
+ '<div class="container">' + html + '</div>' +
251
+ '<script>' + $('#runtime').text() + '<\/script>' +
252
+ '</body></html>'
253
+ );
254
+ page.document.close();
255
+ });
256
+ });
257
+ </script>
258
+
259
+ <!-- What the published page runs: not grid-editor's, the host's -->
260
+ <script type="text/plain" id="runtime">
261
+ document.querySelectorAll('[data-animation]').forEach(function(node) {
262
+ var options = JSON.parse(node.getAttribute('data-animation'));
263
+ node.style.transitionDuration = options.duration + 'ms';
264
+ node.style.transitionDelay = options.delay + 'ms';
265
+ // Prefixed: Bootstrap has a .fade class of its own
266
+ node.classList.add('animate-waiting', 'animate-' + options.effect);
267
+
268
+ new IntersectionObserver(function(entries, observer) {
269
+ entries.forEach(function(entry) {
270
+ node.classList.toggle('animate-waiting', !entry.isIntersecting);
271
+ if (entry.isIntersecting && options.once) { observer.disconnect(); }
272
+ });
273
+ }, { threshold: 0.2 }).observe(node);
274
+ });
275
+ </script>
276
+
277
+ <title>Grid Editor example - a plugin with a modal</title>
278
+ </head>
279
+ <body>
280
+ <!-- Back to the demo index -->
281
+ <div class="container pt-3">
282
+ <a class="link-secondary" href="index.html"><i class="bi bi-arrow-left"></i> All demos</a>
283
+ </div>
284
+
285
+ <div class="container">
286
+
287
+ <div class="row my-3">
288
+ <div class="col-lg-9">
289
+ <h1 class="h4">A plugin with a modal</h1>
290
+ <p class="form-text">
291
+ The <i class="bi bi-magic"></i> tool in the drawer of a row,
292
+ a column, a card or an element is a plugin written on this
293
+ page. It opens a modal, and what you choose there is saved
294
+ on the node as a <code>data-animation</code> attribute, which
295
+ <code>getPlainHtml</code> keeps: only <code>data-ge-*</code>
296
+ attributes are the editor&rsquo;s to take off. A node with
297
+ an animation has its tool highlighted.
298
+ </p>
299
+ <p class="form-text">
300
+ The quote is an element, inside text tinyMCE edits. Open the
301
+ editor there, set an animation on the quote, keep typing
302
+ and press <kbd>Ctrl</kbd>+<kbd>Z</kbd>: the text goes, the
303
+ animation stays, because the plugin wrote it through
304
+ tinyMCE&rsquo;s undo manager. Written behind its back,
305
+ undoing the typing would undo the attribute with it.
306
+ </p>
307
+ <p class="form-text">
308
+ <em>Open as a published page</em> puts the plain html in a
309
+ page of its own, with a few lines that read the attribute
310
+ and animate each node as it scrolls into view.
311
+ <span id="log" class="fw-semibold"></span>
312
+ </p>
313
+ <button class="btn btn-sm btn-secondary" id="output" type="button">Show getPlainHtml</button>
314
+ <button class="btn btn-sm btn-primary" id="published" type="button">Open as a published page</button>
315
+ <pre id="html" class="form-text"></pre>
316
+ </div>
317
+ </div>
318
+
319
+ <div id="myGrid">
320
+ <div class="row" data-animation='{"effect":"fade","duration":800,"delay":0,"once":true}'>
321
+ <div class="col-lg-12">
322
+ <h2>This row fades in</h2>
323
+ <p>It came with its animation already set, in the markup the editor was started on.</p>
324
+ </div>
325
+ </div>
326
+ <div class="row">
327
+ <div class="col-lg-6">
328
+ <p>Ordinary text, edited by tinyMCE.</p>
329
+ <blockquote class="blockquote" data-ge-element="quote" data-ge-label="Quote">
330
+ <p>An element: give it an animation while you edit the text around it.</p>
331
+ </blockquote>
332
+ <p>More text, after the element.</p>
333
+ </div>
334
+ <div class="col-lg-6">
335
+ <div data-ge-container="card">
336
+ <div class="card">
337
+ <div class="card-header">A card</div>
338
+ <div class="card-body">
339
+ <div class="row"><div class="col-lg-12">
340
+ <p>Containers take the tool too.</p>
341
+ </div></div>
342
+ </div>
343
+ </div>
344
+ </div>
345
+ </div>
346
+ </div>
347
+ </div>
348
+
349
+ <div class="row my-4">
350
+ <div class="col-lg-10">
351
+ <h2 class="h5">The plugin, in full</h2>
352
+ <p class="form-text">
353
+ A feature plugin: <code>drawerTools</code> puts a tool in
354
+ every drawer that has a gear, and the kind says whose drawer
355
+ it is. See <a href="../docs/plugins.md">docs/plugins.md</a>.
356
+ </p>
357
+ <pre class="border rounded p-3 bg-light"><code id="animation-source"></code></pre>
358
+ </div>
359
+ </div>
360
+
361
+ </div>
362
+
363
+ </body>
364
+ </html>