@themarioga/grid-editor 5.0.0 → 5.1.1

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,277 @@
1
+ /**
2
+ * Copy and paste for grid-editor.
3
+ *
4
+ * A feature plugin: load this file after the editor and every row, column,
5
+ * section, container and element gets a copy tool, and every place one of
6
+ * them can go gets a paste tool while there is something copied that fits.
7
+ * What it can ask the editor for is the handle its factory is called with,
8
+ * described in docs/plugins.md.
9
+ *
10
+ * <script src="dist/jquery.grideditor.min.js"></script>
11
+ * <script src="dist/plugins/grideditor.clipboard.min.js"></script>
12
+ *
13
+ * What is copied is kept in localStorage, so it survives a reload and can be
14
+ * pasted in another tab or another editor of the same site. A browser that
15
+ * will not give the page its storage keeps it in memory, for this page only.
16
+ *
17
+ * A paste is an add like any other - before-add-* and after-add-*, with
18
+ * `source: 'paste'` - so a host can turn one away.
19
+ */
20
+ (function($) {
21
+
22
+ $.extend($.fn.gridEditor.locales.en, {
23
+ 'tool.copy': 'Copy',
24
+ 'tool.paste': 'Paste',
25
+ 'clipboard.paste_row': 'Paste row',
26
+ 'clipboard.paste_section': 'Paste section',
27
+ });
28
+
29
+ var STORAGE_ITEM = 'grideditor.clipboard';
30
+ var CHANGE = 'grideditor-clipboard';
31
+ var VERSION = 1;
32
+
33
+ /** Where nothing can be stored, what was copied lives here, for this page. */
34
+ var memory = null;
35
+ var editors = 0;
36
+
37
+ /**
38
+ * What each drawer takes when something is pasted into it, by the kind of the
39
+ * node the drawer belongs to. A category is what was copied: a row, a column,
40
+ * a section, a container of any type, an element.
41
+ */
42
+ var TARGETS = {
43
+ column: ['row', 'container', 'element'],
44
+ row: ['column'],
45
+ section: ['row'],
46
+ };
47
+
48
+ /**
49
+ * The attributes that point at an id, as the id itself or as a #selector.
50
+ * A pasted copy that has to take new ids takes them here too, so its tabs,
51
+ * its accordion and its popups still point at their own panes.
52
+ */
53
+ var REFERENCES = ['data-bs-target', 'data-bs-parent', 'href', 'aria-controls', 'aria-labelledby',
54
+ 'aria-describedby', 'for', 'data-ge-popup-id', 'data-ge-popup-target'];
55
+
56
+ /** An id grid-editor generated: ge-{type}-{counter}-{random}. */
57
+ var GENERATED_ID = /^ge-([a-z][a-z-]*?)-\d+-[a-z0-9]+$/;
58
+
59
+ function read() {
60
+ var raw;
61
+
62
+ try {
63
+ raw = window.localStorage.getItem(STORAGE_ITEM);
64
+ } catch (error) {
65
+ return memory;
66
+ }
67
+ if (raw === null) { return memory; }
68
+
69
+ try {
70
+ var clip = JSON.parse(raw);
71
+ return clip && clip.version === VERSION && clip.html ? clip : null;
72
+ } catch (error) {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ function write(clip) {
78
+ try {
79
+ window.localStorage.setItem(STORAGE_ITEM, JSON.stringify(clip));
80
+ memory = null;
81
+ } catch (error) {
82
+ memory = clip;
83
+ }
84
+
85
+ $(document).trigger(CHANGE);
86
+ }
87
+
88
+ // Another tab copied something: every editor here looks again
89
+ $(window).on('storage', function(e) {
90
+ if (e.originalEvent && e.originalEvent.key === STORAGE_ITEM) { $(document).trigger(CHANGE); }
91
+ });
92
+
93
+ $.fn.gridEditor.features.clipboard = function(ge) {
94
+
95
+ var namespace = '.ge-clipboard-' + (++editors);
96
+ var shown = null; // What the toolbar's paste buttons stand for
97
+
98
+ function used(name) {
99
+ return !ge.settings.plugins || ge.settings.plugins.indexOf(name) !== -1;
100
+ }
101
+
102
+ /** A category this editor can take: the plugin that makes it is here. */
103
+ function available(clip) {
104
+ if (clip.category === 'container') {
105
+ return !!$.fn.gridEditor.containers[clip.kind] && used(clip.kind);
106
+ }
107
+ if (clip.category === 'section') { return !!$.fn.gridEditor.features.sections && used('sections'); }
108
+ if (clip.category === 'element') { return !!$.fn.gridEditor.features.elements && used('elements'); }
109
+
110
+ return true;
111
+ }
112
+
113
+ function fits(clip, categories) {
114
+ return !!clip && categories.indexOf(clip.category) !== -1 && available(clip);
115
+ }
116
+
117
+ function categoryOf(kind) {
118
+ if ($.fn.gridEditor.containers[kind]) { return 'container'; }
119
+
120
+ return ['row', 'column', 'section', 'element'].indexOf(kind) !== -1 ? kind : null;
121
+ }
122
+
123
+ function copy(node, kind) {
124
+ var html = ge.nodeHtml(node);
125
+
126
+ write({ version: VERSION, category: categoryOf(kind), kind: kind, html: html });
127
+ ge.emit('after-copy', ge.payloadFor(kind, node, { source: 'tool' }));
128
+
129
+ // The canvas came back with new drawers: the one to flash is the
130
+ // node's new copy tool
131
+ var tool = node.children('.ge-tools-drawer').find('> .ge-copy');
132
+ tool.addClass('ge-copied').find('i').attr('class', 'bi bi-check2');
133
+ setTimeout(function() {
134
+ tool.removeClass('ge-copied').find('i').attr('class', 'bi bi-copy');
135
+ }, 1200);
136
+ }
137
+
138
+ function paste(target, categories) {
139
+ var clip = read();
140
+ if (!fits(clip, categories)) { return; }
141
+
142
+ var node = fresh(clip.html);
143
+ var into = target;
144
+
145
+ // An element goes into the column's last content area. With none,
146
+ // the column's next init wraps it in one of its own
147
+ if (clip.category === 'element') {
148
+ var area = target.children('.ge-content').last();
149
+ if (area.length) { into = area; }
150
+ }
151
+
152
+ ge.place(node, clip.kind, { appendTo: into, source: 'paste' });
153
+ }
154
+
155
+ /**
156
+ * The copied markup as a node, with new ids where the page already has
157
+ * them: two copies of a tab strip pointing at one set of panes is a tab
158
+ * strip that opens the other copy's tabs. An id the page does not have
159
+ * is kept, so pasting into another page changes nothing.
160
+ */
161
+ function fresh(html) {
162
+ var node = $($.parseHTML(html.trim(), document, true)).first();
163
+ var renamed = {};
164
+ var taken = function(id) {
165
+ return !!document.getElementById(id) || Object.keys(renamed).some(function(old) {
166
+ return renamed[old] === id;
167
+ });
168
+ };
169
+
170
+ node.find('[id]').addBack('[id]').each(function() {
171
+ var id = this.id;
172
+ if (!document.getElementById(id)) { return; }
173
+
174
+ var generated = GENERATED_ID.exec(id);
175
+ var next;
176
+
177
+ if (generated) {
178
+ do { next = ge.containerId(generated[1]); } while (taken(next));
179
+ } else {
180
+ var n = 2;
181
+ while (taken(id + '-' + n)) { n++; }
182
+ next = id + '-' + n;
183
+ }
184
+
185
+ renamed[id] = next;
186
+ this.id = next;
187
+ });
188
+
189
+ if (!Object.keys(renamed).length) { return node; }
190
+
191
+ node.find('*').addBack().each(function() {
192
+ var element = this;
193
+
194
+ REFERENCES.forEach(function(name) {
195
+ var value = element.getAttribute(name);
196
+ if (value === null) { return; }
197
+ if (name === 'href' && value.charAt(0) !== '#') { return; }
198
+
199
+ var rewritten = value.split(/(\s+)/).map(function(token) {
200
+ if (renamed[token]) { return renamed[token]; }
201
+ if (token.charAt(0) === '#' && renamed[token.slice(1)]) { return '#' + renamed[token.slice(1)]; }
202
+
203
+ return token;
204
+ }).join('');
205
+
206
+ if (rewritten !== value) { element.setAttribute(name, rewritten); }
207
+ });
208
+ });
209
+
210
+ return node;
211
+ }
212
+
213
+ /** Show the paste tools, and the toolbar's paste buttons, that fit what is copied. */
214
+ function refresh() {
215
+ var clip = read();
216
+
217
+ ge.canvas.find('.ge-paste').each(function() {
218
+ $(this).toggle(fits(clip, $(this).attr('data-ge-paste').split(' ')));
219
+ });
220
+
221
+ ge.toolbarItems('clipboard').each(function() {
222
+ var item = TOOLBAR[parseInt($(this).attr('data-ge-item'), 10)];
223
+ $(this).toggle(fits(clip, [item.kind]));
224
+ });
225
+
226
+ shown = clip;
227
+ }
228
+
229
+ var TOOLBAR = [
230
+ { kind: 'row', labelKey: 'clipboard.paste_row' },
231
+ { kind: 'section', labelKey: 'clipboard.paste_section' },
232
+ ].map(function(item) {
233
+ return $.extend(item, {
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',
238
+ source: 'paste',
239
+ // What the button showed, even if another tab has copied
240
+ // something else since
241
+ create: function() { return fresh(shown.html); },
242
+ });
243
+ });
244
+
245
+ return {
246
+ drawerTools: function(drawer, node, kind) {
247
+ if (categoryOf(kind)) {
248
+ ge.createTool(drawer, ge.t('tool.copy'), 'ge-copy', 'bi bi-copy', function() {
249
+ copy(node, kind);
250
+ });
251
+ }
252
+
253
+ var categories = TARGETS[kind];
254
+ if (!categories) { return; }
255
+
256
+ ge.createTool(drawer, ge.t('tool.paste'), 'ge-paste', 'bi bi-clipboard-plus', function() {
257
+ paste(node, categories);
258
+ });
259
+ drawer.children('.ge-paste').last()
260
+ .attr('data-ge-paste', categories.join(' '))
261
+ .toggle(fits(read(), categories));
262
+ },
263
+
264
+ toolbar: TOOLBAR,
265
+
266
+ onInit: function() {
267
+ refresh();
268
+ $(document).off(CHANGE + namespace).on(CHANGE + namespace, refresh);
269
+ },
270
+
271
+ onDeinit: function() {
272
+ $(document).off(CHANGE + namespace);
273
+ },
274
+ };
275
+ };
276
+
277
+ })(jQuery);
@@ -124,15 +124,15 @@ $.fn.gridEditor.features.sections = function(ge) {
124
124
  ge.createMoveTool(drawer);
125
125
  ge.addSettingsTool(drawer, section, ge.settings.section_classes || []).append(widthField(section));
126
126
 
127
- ge.createTool(drawer, ge.t('tool.add_row_to_section'), 'ge-add-row', 'bi bi-plus-circle', function() {
128
- ge.place(ge.rowFromLayout([12]), 'row', { appendTo: section });
129
- });
130
-
131
127
  ge.createTool(drawer, ge.t('tool.delete_section'), 'ge-delete-section', 'bi bi-trash', function() {
132
128
  ge.deleteNode('section', section, ge.t('confirm.delete_section'), function(removed) {
133
129
  section.slideUp(removed);
134
130
  });
135
131
  });
132
+
133
+ ge.createTool(drawer, ge.t('tool.add_row_to_section'), 'ge-add-row', 'bi bi-plus-circle', function() {
134
+ ge.place(ge.rowFromLayout([12]), 'row', { appendTo: section });
135
+ });
136
136
  }
137
137
 
138
138
  function mark() {
@@ -64,6 +64,10 @@
64
64
  .ge-layout-mode {
65
65
  margin-left: 5px;
66
66
  }
67
+
68
+ .ge-toolbar-end {
69
+ margin-right: 5px;
70
+ }
67
71
 
68
72
  .btn:focus {
69
73
  box-shadow: none;
@@ -347,6 +351,12 @@
347
351
  position: relative;
348
352
  }
349
353
 
354
+ /* The copy tool says it copied, for a moment, with the clipboard
355
+ plugin */
356
+ .ge-tools-drawer > a.ge-copied {
357
+ color: #198754;
358
+ }
359
+
350
360
  .ge-size-picker {
351
361
  position: absolute;
352
362
  top: 100%;