@themarioga/grid-editor 7.2.0 → 7.3.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 (61) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +83 -14
  3. package/UPGRADING.md +31 -0
  4. package/dist/grideditor.bundle.min.js +2 -2
  5. package/dist/grideditor.css +102 -0
  6. package/dist/grideditor.d.ts +18 -0
  7. package/dist/grideditor.esm.js +291 -31
  8. package/dist/grideditor.js +291 -31
  9. package/dist/grideditor.min.css +1 -1
  10. package/dist/grideditor.min.css.map +1 -1
  11. package/dist/grideditor.min.js +1 -1
  12. package/dist/grideditor.min.js.map +3 -3
  13. package/dist/locales/grideditor.es.esm.js +67 -0
  14. package/dist/locales/grideditor.es.js +67 -0
  15. package/dist/locales/grideditor.es.min.js +1 -1
  16. package/dist/locales/grideditor.es.min.js.map +2 -2
  17. package/dist/plugins/grideditor.float.esm.js +10 -1
  18. package/dist/plugins/grideditor.float.js +8 -2
  19. package/dist/plugins/grideditor.float.min.js +1 -1
  20. package/dist/plugins/grideditor.float.min.js.map +4 -4
  21. package/dist/plugins/grideditor.spacing.esm.js +12 -3
  22. package/dist/plugins/grideditor.spacing.js +9 -3
  23. package/dist/plugins/grideditor.spacing.min.js +1 -1
  24. package/dist/plugins/grideditor.spacing.min.js.map +4 -4
  25. package/dist/plugins/grideditor.style.esm.js +1593 -0
  26. package/dist/plugins/grideditor.style.js +1592 -0
  27. package/dist/plugins/grideditor.style.min.js +2 -0
  28. package/dist/plugins/grideditor.style.min.js.map +7 -0
  29. package/dist/plugins/grideditor.textalign.esm.js +10 -1
  30. package/dist/plugins/grideditor.textalign.js +8 -2
  31. package/dist/plugins/grideditor.textalign.min.js +1 -1
  32. package/dist/plugins/grideditor.textalign.min.js.map +4 -4
  33. package/dist/plugins/grideditor.visibility.esm.js +12 -3
  34. package/dist/plugins/grideditor.visibility.js +9 -3
  35. package/dist/plugins/grideditor.visibility.min.js +1 -1
  36. package/dist/plugins/grideditor.visibility.min.js.map +4 -4
  37. package/docs/locale-keys.md +86 -7
  38. package/docs/plugins.md +49 -5
  39. package/example/index.html +14 -0
  40. package/example/style.html +151 -0
  41. package/example/utilities.html +15 -11
  42. package/package.json +1 -1
  43. package/src/js/grideditor.js +365 -27
  44. package/src/js/locales/grideditor.es.js +67 -0
  45. package/src/js/plugins/grideditor.float.js +8 -33
  46. package/src/js/plugins/grideditor.spacing.js +8 -273
  47. package/src/js/plugins/grideditor.style.js +179 -0
  48. package/src/js/plugins/grideditor.textalign.js +8 -39
  49. package/src/js/plugins/grideditor.visibility.js +8 -147
  50. package/src/js/style/accordion.js +157 -0
  51. package/src/js/style/catalog.js +76 -0
  52. package/src/js/style/custom.js +84 -0
  53. package/src/js/style/fields.js +382 -0
  54. package/src/js/style/float.js +46 -0
  55. package/src/js/style/options.js +75 -0
  56. package/src/js/style/sections.js +271 -0
  57. package/src/js/style/spacing.js +286 -0
  58. package/src/js/style/textalign.js +52 -0
  59. package/src/js/style/visibility.js +160 -0
  60. package/src/less/grideditor.less +111 -0
  61. package/types/grideditor.d.ts +18 -0
@@ -0,0 +1,382 @@
1
+ /**
2
+ * The style plugin's inline fields: one per css property, each writing the
3
+ * node's own style through ge.setHostStyle, never by hand, so the preview
4
+ * of a breakpoint view neither shows in them nor eats what they write.
5
+ *
6
+ * A field is filled from the node by renderStyleField, again whenever the
7
+ * plugin asks: after a write elsewhere, after the classes change. A value
8
+ * the browser refused stays in its field, marked invalid, until it is put
9
+ * right or emptied; nothing is written meanwhile.
10
+ */
11
+ import * as dom from '../dom.js';
12
+ import { overriding } from './sections.js';
13
+
14
+ var renderers = new WeakMap(); // field -> what fills it from the node
15
+ var fieldCounter = 0;
16
+
17
+ function classesOf(node) {
18
+ return (node.getAttribute('class') || '').split(/\s+/).filter(Boolean);
19
+ }
20
+
21
+ /**
22
+ * Write what the user typed: trimmed, with a trailing !important taken as
23
+ * the priority. Without one, the property keeps the priority it had.
24
+ */
25
+ function write(ge, node, property, raw) {
26
+ var text = String(raw === null || raw === undefined ? '' : raw).trim();
27
+ var important = /\s*!\s*important\s*$/i.exec(text);
28
+ var priority;
29
+
30
+ if (important) {
31
+ text = text.slice(0, important.index).trim();
32
+ priority = 'important';
33
+ }
34
+
35
+ return ge.setHostStyle(node, property, text, priority);
36
+ }
37
+
38
+ function mark(input, ok, ge) {
39
+ dom.toggleClass(input, 'is-invalid', !ok);
40
+ if (ok) {
41
+ input.removeAttribute('title');
42
+ } else {
43
+ input.setAttribute('title', ge.t('style.invalid'));
44
+ }
45
+ }
46
+
47
+ /** Put a value in an input, unless it holds something refused that is still being put right. */
48
+ function fill(input, value) {
49
+ if (!dom.hasClass(input, 'is-invalid')) { input.value = value; }
50
+ }
51
+
52
+ function textInput(className) {
53
+ return dom.element('input', { type: 'text', 'class': 'form-control form-control-sm ' + (className || '') });
54
+ }
55
+
56
+ /* Colors */
57
+
58
+ var HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
59
+
60
+ /** What a color picker can show of a value: a six digit hex, or nothing. */
61
+ function pickable(value) {
62
+ var match = HEX.exec(value.trim());
63
+ if (!match) { return null; }
64
+
65
+ var hex = match[1];
66
+ if (hex.length === 3) { hex = hex.split('').map(function(c) { return c + c; }).join(''); }
67
+
68
+ return '#' + hex.toLowerCase();
69
+ }
70
+
71
+ /** A picker and a text beside it, which takes what a picker cannot: rgba, var(), transparent. */
72
+ function colorControl(onValue) {
73
+ var box = dom.element('div', { 'class': 'input-group input-group-sm ge-style-color' });
74
+ var picker = box.appendChild(dom.element('input', { type: 'color', 'class': 'form-control form-control-color' }));
75
+ var text = box.appendChild(textInput());
76
+
77
+ picker.addEventListener('input', function() {
78
+ dom.removeClass(text, 'is-invalid');
79
+ text.value = picker.value;
80
+ onValue(picker.value, text);
81
+ });
82
+ text.addEventListener('change', function() {
83
+ var hex = pickable(text.value);
84
+ if (hex) { picker.value = hex; }
85
+ onValue(text.value, text);
86
+ });
87
+
88
+ return {
89
+ element: box,
90
+ set: function(value) {
91
+ fill(text, value);
92
+ var hex = pickable(value);
93
+ if (hex) { picker.value = hex; }
94
+ },
95
+ };
96
+ }
97
+
98
+ /* Background images */
99
+
100
+ var URL_VALUE = /^url\(\s*(["']?)(.*)\1\s*\)$/i;
101
+
102
+ /** What the field shows of a background-image: the url itself, or the value as it is. */
103
+ function shownUrl(value) {
104
+ var match = URL_VALUE.exec(value.trim());
105
+ if (!match) { return value; }
106
+
107
+ return match[1] ? match[2].replace(/\\(["\\])/g, '$1') : match[2];
108
+ }
109
+
110
+ /** A url becomes url("…"); a gradient, none or any other css function goes as it is. */
111
+ function urlValue(text) {
112
+ text = text.trim();
113
+ if (text === '' || /^(?:none|inherit|initial|unset|revert)$/i.test(text) || /^[a-z-]+\(/i.test(text)) { return text; }
114
+
115
+ return 'url("' + text.replace(/(["\\])/g, '\\$1') + '")';
116
+ }
117
+
118
+ /* Shadows */
119
+
120
+ /** Split at spaces and commas outside parentheses. */
121
+ function tokens(value) {
122
+ var list = [];
123
+ var depth = 0;
124
+ var current = '';
125
+
126
+ for (var i = 0; i < value.length; i++) {
127
+ var c = value.charAt(i);
128
+
129
+ if (c === '(') { depth++; }
130
+ if (c === ')') { depth--; }
131
+
132
+ if (depth === 0 && (c === ' ' || c === ',')) {
133
+ if (current) { list.push(current); }
134
+ if (c === ',') { list.push(','); }
135
+ current = '';
136
+ } else {
137
+ current += c;
138
+ }
139
+ }
140
+ if (current) { list.push(current); }
141
+
142
+ return list;
143
+ }
144
+
145
+ var LENGTH = /^-?(?:\d*\.)?\d+(?:[a-z%]+)?$/i;
146
+
147
+ /** The lengths of a shadow, in the order css writes them; a text shadow has no spread. */
148
+ var SHADOW_PARTS = [
149
+ { name: 'x', labelKey: 'style.shadow_x' },
150
+ { name: 'y', labelKey: 'style.shadow_y' },
151
+ { name: 'blur', labelKey: 'style.shadow_blur' },
152
+ { name: 'spread', labelKey: 'style.shadow_spread' },
153
+ ];
154
+
155
+ /**
156
+ * One shadow as the builder's parts, or null when the value is something
157
+ * the builder cannot hold: several shadows, var(), a keyword.
158
+ */
159
+ function parseShadow(value, box) {
160
+ value = value.trim();
161
+ if (value === '') { return { inset: false, lengths: [], color: '' }; }
162
+ if (/var\(/i.test(value)) { return null; }
163
+
164
+ var list = tokens(value);
165
+ if (list.indexOf(',') !== -1) { return null; }
166
+
167
+ var inset = false;
168
+ var lengths = [];
169
+ var colors = [];
170
+
171
+ list.forEach(function(token) {
172
+ if (box && token.toLowerCase() === 'inset') {
173
+ inset = true;
174
+ } else if (LENGTH.test(token)) {
175
+ lengths.push(token);
176
+ } else {
177
+ colors.push(token);
178
+ }
179
+ });
180
+
181
+ if (colors.length > 1 || lengths.length < 2 || lengths.length > (box ? 4 : 3)) { return null; }
182
+ if (colors.length && /^(?:none|inherit|initial|unset|revert)$/i.test(colors[0])) { return null; }
183
+
184
+ return { inset: inset, lengths: lengths, color: colors[0] || '' };
185
+ }
186
+
187
+ function composeShadow(parts, box) {
188
+ var lengths = parts.lengths.slice();
189
+
190
+ // A later length needs the earlier ones: a spread needs a blur
191
+ for (var i = lengths.length - 1; i >= 0; i--) {
192
+ if (lengths[i] === '' && lengths.slice(i + 1).some(Boolean)) { lengths[i] = '0'; }
193
+ }
194
+ lengths = lengths.filter(Boolean);
195
+
196
+ if (!lengths.length && !parts.color && !parts.inset) { return ''; }
197
+ while (lengths.length < 2) { lengths.push('0'); }
198
+
199
+ return (box && parts.inset ? 'inset ' : '') + lengths.join(' ') + (parts.color ? ' ' + parts.color : '');
200
+ }
201
+
202
+ /**
203
+ * The shadow field: a builder for one shadow - x, y, blur, the spread and
204
+ * inset for a box - and the value as text when it is more than the
205
+ * builder holds, or when the user asks for the text.
206
+ */
207
+ function shadowControl(ge, node, property, changed) {
208
+ var box = property === 'box-shadow';
209
+ var holder = dom.element('div', { 'class': 'ge-style-shadow' });
210
+ var textMode = false;
211
+
212
+ function writeValue(value, input) {
213
+ var ok = write(ge, node, property, value);
214
+ mark(input, ok, ge);
215
+ if (ok) { changed(); }
216
+ return ok;
217
+ }
218
+
219
+ function builder(parts) {
220
+ holder.innerHTML = '';
221
+ var grid = holder.appendChild(dom.element('div', { 'class': 'ge-style-shadow-builder' }));
222
+ var names = box ? SHADOW_PARTS : SHADOW_PARTS.slice(0, 3);
223
+ var inputs = names.map(function(part, i) {
224
+ var cell = grid.appendChild(dom.element('label', { 'class': 'ge-style-shadow-part' }));
225
+ cell.appendChild(dom.element('span', { 'class': 'ge-style-shadow-label' }, ge.t(part.labelKey)));
226
+ var input = cell.appendChild(textInput('ge-style-shadow-' + part.name));
227
+ input.value = parts.lengths[i] || '';
228
+ return input;
229
+ });
230
+
231
+ var colorCell = grid.appendChild(dom.element('label', { 'class': 'ge-style-shadow-part ge-style-shadow-color' }));
232
+ colorCell.appendChild(dom.element('span', { 'class': 'ge-style-shadow-label' }, ge.t('style.shadow_color')));
233
+ var color = colorControl(function() { update(); });
234
+ colorCell.appendChild(color.element);
235
+ color.set(parts.color);
236
+
237
+ var inset = null;
238
+ if (box) {
239
+ var insetCell = grid.appendChild(dom.element('label', { 'class': 'form-check ge-style-shadow-inset' }));
240
+ inset = insetCell.appendChild(dom.element('input', { type: 'checkbox', 'class': 'form-check-input' }));
241
+ insetCell.appendChild(dom.element('span', { 'class': 'form-check-label' }, ge.t('style.shadow_inset')));
242
+ inset.checked = parts.inset;
243
+ inset.addEventListener('change', function() { update(); });
244
+ }
245
+
246
+ inputs.forEach(function(input) { input.addEventListener('change', function() { update(); }); });
247
+
248
+ var toText = holder.appendChild(dom.element('a', { href: '#', 'class': 'ge-style-shadow-mode' }, ge.t('style.shadow_text_mode')));
249
+ toText.addEventListener('click', function(e) {
250
+ e.preventDefault();
251
+ textMode = true;
252
+ render();
253
+ });
254
+
255
+ function update() {
256
+ var value = composeShadow({
257
+ inset: inset ? inset.checked : false,
258
+ lengths: inputs.map(function(input) { return input.value.trim(); }),
259
+ color: dom.one(color.element, 'input[type="text"]').value.trim(),
260
+ }, box);
261
+ writeValue(value, inputs[0]);
262
+ }
263
+ }
264
+
265
+ function text(value) {
266
+ holder.innerHTML = '';
267
+ var input = holder.appendChild(textInput('ge-style-shadow-text'));
268
+ input.value = value;
269
+ input.addEventListener('change', function() {
270
+ if (input.value.trim() === '') {
271
+ if (writeValue('', input)) {
272
+ textMode = false;
273
+ render();
274
+ }
275
+ return;
276
+ }
277
+ writeValue(input.value, input);
278
+ });
279
+ }
280
+
281
+ var shown = null; // The value the builder or the text was last built for
282
+
283
+ function render() {
284
+ if (holder.querySelector('.is-invalid')) { return; }
285
+
286
+ var value = ge.hostStyle(node, property).value;
287
+ var parts = parseShadow(value, box);
288
+
289
+ // Built again only when the value changed under it: building takes
290
+ // the focus from whichever of its inputs the user moved on to
291
+ if (value === shown && holder.firstChild && textMode === !!holder.querySelector('.ge-style-shadow-text')) { return; }
292
+ shown = value;
293
+
294
+ if (textMode || !parts) {
295
+ textMode = !!value || textMode;
296
+ text(value);
297
+ } else {
298
+ builder(parts);
299
+ }
300
+ }
301
+
302
+ return { element: holder, render: render, value: function() { return ge.hostStyle(node, property).value; } };
303
+ }
304
+
305
+ /**
306
+ * The field for one property of a node. `changed` is called after every
307
+ * write that went through, so the plugin can bring the rest of its
308
+ * sections up to date.
309
+ */
310
+ export function createStyleField(ge, node, property, changed) {
311
+ var field = dom.element('div', { 'class': 'ge-style-field', 'data-ge-style-property': property.name });
312
+ var label = field.appendChild(dom.element('label', { 'class': 'ge-style-label' }, ge.t(property.labelKey)));
313
+ var render;
314
+
315
+ function current() {
316
+ return ge.hostStyle(node, property.name).value;
317
+ }
318
+
319
+ function commit(value, input) {
320
+ var ok = write(ge, node, property.name, value);
321
+ mark(input, ok, ge);
322
+ if (ok) { changed(); }
323
+ }
324
+
325
+ if (property.type === 'select') {
326
+ var select = field.appendChild(dom.element('select', { 'class': 'form-select form-select-sm' }));
327
+ select.addEventListener('change', function() { commit(select.value, select); });
328
+ render = function() {
329
+ var value = current();
330
+ var values = property.values.slice();
331
+ if (value && values.indexOf(value) === -1) { values.push(value); }
332
+
333
+ select.innerHTML = '';
334
+ select.appendChild(dom.element('option', { value: '' }, ''));
335
+ values.forEach(function(each) { select.appendChild(dom.element('option', { value: each }, each)); });
336
+ select.value = value;
337
+ };
338
+ } else if (property.type === 'color') {
339
+ var color = colorControl(function(value, input) { commit(value, input); });
340
+ field.appendChild(color.element);
341
+ render = function() { color.set(current()); };
342
+ } else if (property.type === 'url') {
343
+ var url = field.appendChild(textInput());
344
+ url.addEventListener('change', function() { commit(urlValue(url.value), url); });
345
+ render = function() { fill(url, shownUrl(current())); };
346
+ } else if (property.type === 'shadow') {
347
+ var shadow = shadowControl(ge, node, property.name, changed);
348
+ field.appendChild(shadow.element);
349
+ render = shadow.render;
350
+ } else {
351
+ var input = field.appendChild(textInput());
352
+ input.addEventListener('change', function() { commit(input.value, input); });
353
+ render = function() { fill(input, current()); };
354
+ }
355
+
356
+ var control = field.querySelector('input, select');
357
+ if (control) {
358
+ control.id = 'ge-style-field-' + (++fieldCounter);
359
+ label.setAttribute('for', control.id);
360
+ }
361
+
362
+ field.appendChild(dom.element('small', { 'class': 'ge-style-note ge-style-overridden' }));
363
+
364
+ renderers.set(field, function() {
365
+ render();
366
+
367
+ // A Bootstrap utility on the node wins over the value: every one is !important
368
+ var winner = current() ? overriding(property.name, classesOf(node)) : null;
369
+ var note = dom.child(field, '.ge-style-overridden');
370
+ note.textContent = winner ? ge.t('style.overridden', { 'class': winner }) : '';
371
+ dom.toggle(note, !!winner);
372
+ });
373
+ renderStyleField(field);
374
+
375
+ return field;
376
+ }
377
+
378
+ /** Fill a field from its node again. */
379
+ export function renderStyleField(field) {
380
+ var render = renderers.get(field);
381
+ if (render) { render(); }
382
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Float, a part of the style plugin: floating an element per breakpoint, with Bootstrap's
3
+ * float-{breakpoint}-start, -end and -none classes.
4
+ *
5
+ * The part is what the float plugin was up to 7.2 - its families, and
6
+ * whatever it did on the canvas - as a function of the handle.
7
+ * grideditor.style.js puts it in its sections; the deprecated
8
+ * grideditor.float.js still registers it on its own, as it always did.
9
+ */
10
+ import { GridEditor } from '../grideditor.js';
11
+
12
+ Object.assign(GridEditor.locales.en, {
13
+ 'utility.float': 'Float',
14
+ 'utility.float_start': 'Start',
15
+ 'utility.float_end': 'End',
16
+ 'utility.float_none': 'None',
17
+ });
18
+
19
+ /** Start and end are left and right: Bootstrap's css is left to right. */
20
+ var CSS = { start: 'left', end: 'right', none: 'none' };
21
+
22
+ export function floatPart(ge) {
23
+
24
+ function label(value) {
25
+ if (value === 'start') { return ge.t('utility.float_start'); }
26
+ if (value === 'end') { return ge.t('utility.float_end'); }
27
+
28
+ return ge.t('utility.float_none');
29
+ }
30
+
31
+ return {
32
+ families: [{
33
+ name: 'float',
34
+ prefix: 'float',
35
+ values: ['start', 'end', 'none'],
36
+ appliesTo: ['element'],
37
+ labelKey: 'utility.float',
38
+ label: label,
39
+
40
+ /** With no class applying here, whatever the host's css floats it as. */
41
+ preview: function(value, node) {
42
+ return { float: value === null ? ge.bareStyle(node, 'float', 'float') : CSS[value] };
43
+ },
44
+ }],
45
+ };
46
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The style plugin's settings, `style` in the editor's options:
3
+ *
4
+ * style: {
5
+ * sections: { border: { properties: [...], catalog: false }, position: false, … },
6
+ * spacing: { values, scale }, // what utilities.spacing was
7
+ * visibility: { drawer }, // what utilities.visibility was
8
+ * }
9
+ *
10
+ * Every section is on unless it is turned off. utilities.spacing and
11
+ * utilities.visibility are still read, for 7.x, when the new key is not
12
+ * there, and said to be deprecated.
13
+ */
14
+ import { SECTIONS, section } from './sections.js';
15
+
16
+ /**
17
+ * What each section shows: { key: { properties, catalog } } for the ones
18
+ * that are on, in the sections' own order.
19
+ */
20
+ function resolveSections(ge, given) {
21
+ var resolved = {};
22
+
23
+ Object.keys(given || {}).forEach(function(key) {
24
+ if (!section(key)) { ge.warn('style.sections: there is no "' + key + '" section: ignored'); }
25
+ });
26
+
27
+ SECTIONS.forEach(function(each) {
28
+ var option = given && given[each.key] !== undefined ? given[each.key] : true;
29
+ if (option === false) { return; }
30
+
31
+ var own = each.properties.map(function(property) { return property.name; });
32
+ var properties = each.properties;
33
+ var catalog = true;
34
+
35
+ if (option && typeof option === 'object') {
36
+ if (Array.isArray(option.properties)) {
37
+ option.properties.forEach(function(name) {
38
+ if (own.indexOf(name) === -1) {
39
+ ge.warn('style.sections.' + each.key + ': "' + name + '" is not one of its properties: ignored');
40
+ }
41
+ });
42
+ properties = option.properties.filter(function(name) { return own.indexOf(name) !== -1; })
43
+ .map(function(name) { return each.properties[own.indexOf(name)]; });
44
+ }
45
+ if (option.catalog === false) { catalog = false; }
46
+ }
47
+
48
+ resolved[each.key] = { section: each, properties: properties, catalog: catalog };
49
+ });
50
+
51
+ return resolved;
52
+ }
53
+
54
+ /** An option of a merged part: the new key, or the old one, deprecated. */
55
+ function partOption(ge, style, name) {
56
+ if (style[name] !== undefined) { return style[name]; }
57
+
58
+ var old = (ge.settings.utilities || {})[name];
59
+ if (old !== undefined) {
60
+ ge.warn('utilities.' + name + ' is deprecated and will be removed in 8.0: use style.' + name);
61
+ return old;
62
+ }
63
+
64
+ return undefined;
65
+ }
66
+
67
+ export function resolveOptions(ge) {
68
+ var style = ge.settings.style || {};
69
+
70
+ return {
71
+ sections: resolveSections(ge, style.sections),
72
+ spacing: partOption(ge, style, 'spacing'),
73
+ visibility: partOption(ge, style, 'visibility'),
74
+ };
75
+ }