sbs-editable-bs5 1.0.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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +325 -0
  3. package/dist/editable.css +1 -0
  4. package/dist/editable.iife.js +3 -0
  5. package/dist/editable.iife.js.map +1 -0
  6. package/dist/editable.js +906 -0
  7. package/dist/editable.js.map +1 -0
  8. package/dist/editable.umd.cjs +3 -0
  9. package/dist/editable.umd.cjs.map +1 -0
  10. package/dist/src/Interfaces/BaseTypeButtons.d.ts +4 -0
  11. package/dist/src/Interfaces/Options.d.ts +36 -0
  12. package/dist/src/Modes/BaseMode.d.ts +14 -0
  13. package/dist/src/Modes/InlineMode.d.ts +11 -0
  14. package/dist/src/Modes/PopupMode.d.ts +12 -0
  15. package/dist/src/Types/BaseType.d.ts +35 -0
  16. package/dist/src/Types/DateTimeType.d.ts +5 -0
  17. package/dist/src/Types/DateType.d.ts +7 -0
  18. package/dist/src/Types/InputType.d.ts +4 -0
  19. package/dist/src/Types/SelectType.d.ts +7 -0
  20. package/dist/src/Types/TextAreaType.d.ts +4 -0
  21. package/dist/src/editable.d.ts +51 -0
  22. package/dist/tests/accessibility.test.d.ts +1 -0
  23. package/dist/tests/disabled.test.d.ts +1 -0
  24. package/dist/tests/errors.test.d.ts +1 -0
  25. package/dist/tests/events.test.d.ts +1 -0
  26. package/dist/tests/lifecycle.test.d.ts +1 -0
  27. package/dist/tests/public-api.test.d.ts +1 -0
  28. package/dist/tests/render.test.d.ts +1 -0
  29. package/dist/tests/request.test.d.ts +1 -0
  30. package/dist/tests/save-flow.test.d.ts +1 -0
  31. package/dist/tests/theme.test.d.ts +1 -0
  32. package/dist/tests/types/attributes.test.d.ts +1 -0
  33. package/dist/tests/types/date.test.d.ts +1 -0
  34. package/dist/tests/types/select.test.d.ts +1 -0
  35. package/package.json +57 -0
  36. package/src/Interfaces/BaseTypeButtons.ts +4 -0
  37. package/src/Interfaces/Options.ts +30 -0
  38. package/src/Modes/BaseMode.ts +57 -0
  39. package/src/Modes/InlineMode.ts +47 -0
  40. package/src/Modes/PopupMode.ts +107 -0
  41. package/src/Types/BaseType.ts +301 -0
  42. package/src/Types/DateTimeType.ts +18 -0
  43. package/src/Types/DateType.ts +43 -0
  44. package/src/Types/InputType.ts +12 -0
  45. package/src/Types/SelectType.ts +70 -0
  46. package/src/Types/TextAreaType.ts +9 -0
  47. package/src/editable.css +136 -0
  48. package/src/editable.ts +244 -0
@@ -0,0 +1,301 @@
1
+ import type Editable from '../editable.ts';
2
+ import type BaseTypeButtons from '../Interfaces/BaseTypeButtons.ts';
3
+
4
+ export default class BaseType {
5
+ context: Editable;
6
+ element: HTMLInputElement | null = null;
7
+ error: HTMLElement | null = null;
8
+ form: HTMLElement | null = null;
9
+ load: HTMLElement | null = null;
10
+ buttons: BaseTypeButtons = { success: null, cancel: null };
11
+
12
+ constructor(context: Editable) {
13
+ if (this.constructor === BaseType) {
14
+ throw new Error(
15
+ 'BaseType is abstract and cannot be instantiated directly — create a subclass that implements create() (see InputType, SelectType, etc.).',
16
+ );
17
+ }
18
+ this.context = context;
19
+ }
20
+
21
+ create(): HTMLElement {
22
+ throw new Error(`${this.constructor.name} must implement create().`);
23
+ }
24
+
25
+ // Flags options that only make sense for a different type (e.g. `format` on a `text` field) —
26
+ // they're silently ignored otherwise, which is easy to miss. Override per-type as needed.
27
+ checkUnsupportedOptions(): void {
28
+ if (this.context.options.format !== undefined) {
29
+ console.error(
30
+ `${this.constructor.name} does not support the "format" option — it only applies to type: 'date'/'datetime'. It will be ignored.`,
31
+ );
32
+ }
33
+ if (this.context.options.displayFormat !== undefined) {
34
+ console.error(
35
+ `${this.constructor.name} does not support the "displayFormat" option — it only applies to type: 'date'/'datetime'. It will be ignored.`,
36
+ );
37
+ }
38
+ if (this.context.options.source !== undefined) {
39
+ console.error(
40
+ `${this.constructor.name} does not support the "source" option — it only applies to type: 'select'. It will be ignored.`,
41
+ );
42
+ }
43
+ }
44
+
45
+ createContainer(element: HTMLInputElement): HTMLDivElement {
46
+ const div = document.createElement(`div`);
47
+ this.element = element;
48
+ this.error = this.createContainerError();
49
+ this.form = this.createContainerForm();
50
+ this.load = this.createContainerLoad();
51
+ this.form.append(element, this.load);
52
+ this.buttons.success = null;
53
+ this.buttons.cancel = null;
54
+ if (this.context.options.showButtons) {
55
+ this.buttons.success = this.createButtonSuccess();
56
+ this.buttons.cancel = this.createButtonCancel();
57
+ this.form.append(this.buttons.success, this.buttons.cancel);
58
+ }
59
+
60
+ div.append(this.error, this.form);
61
+ return div;
62
+ }
63
+
64
+ createContainerError(): HTMLDivElement {
65
+ const div = document.createElement(`div`);
66
+ div.classList.add('editable-error', 'text-danger', 'fst-italic', 'mb-2', 'fw-bold');
67
+ div.setAttribute('role', 'alert');
68
+ div.hidden = true;
69
+ return div;
70
+ }
71
+
72
+ // Closes the widget and returns keyboard focus to the trigger — used for every
73
+ // explicit exit (cancel, Escape, successful save), but not for an outside-click
74
+ // dismissal, where focus should stay wherever the user actually clicked.
75
+ private closeAndFocus(): void {
76
+ this.context.modeElement.hide();
77
+ this.context.element.focus();
78
+ }
79
+
80
+ createContainerForm(): HTMLFormElement {
81
+ const form = document.createElement(`form`);
82
+ form.classList.add('editable-form', 'd-flex', 'align-items-start');
83
+ form.addEventListener('keydown', (e) => {
84
+ if (e.key === 'Escape') {
85
+ e.preventDefault();
86
+ this.closeAndFocus();
87
+ }
88
+ });
89
+ form.addEventListener('submit', async (e) => {
90
+ e.preventDefault();
91
+ const newValue = this.getValue();
92
+ if (this.context.options.send && this.context.options.url && this.context.getValue() !== newValue) {
93
+ this.showLoad();
94
+ let msg: string | undefined;
95
+ try {
96
+ const response = await this.ajax(newValue);
97
+ if (response.ok) {
98
+ msg = await this.context.success(response, newValue);
99
+ } else {
100
+ msg =
101
+ (await this.context.error(response, newValue)) ||
102
+ `${response.status} ${response.statusText}`;
103
+ }
104
+ } catch (error) {
105
+ console.error(error);
106
+ if (!(error instanceof TypeError)) {
107
+ throw error;
108
+ }
109
+ msg = error.message;
110
+ }
111
+
112
+ if (msg) {
113
+ this.showError();
114
+ this.setError(msg);
115
+ } else {
116
+ this.setError('');
117
+ this.hideError();
118
+ this.context.setValue(this.getValue());
119
+ this.closeAndFocus();
120
+ this.initText();
121
+ }
122
+ this.hideLoad();
123
+ } else {
124
+ this.context.setValue(this.getValue());
125
+ this.closeAndFocus();
126
+ this.initText();
127
+ }
128
+ this.context.element.dispatchEvent(new CustomEvent('save', { detail: { Editable: this.context } }));
129
+ });
130
+ return form;
131
+ }
132
+
133
+ createContainerLoad(): HTMLDivElement {
134
+ const div = document.createElement(`div`);
135
+ div.classList.add('editable-load-overlay');
136
+ div.hidden = true;
137
+ const loader = document.createElement(`div`);
138
+ loader.classList.add('editable-loader');
139
+ div.append(loader);
140
+ return div;
141
+ }
142
+
143
+ createButton(): HTMLButtonElement {
144
+ const button = document.createElement('button');
145
+ button.type = 'button';
146
+ button.classList.add('btn', 'btn-sm');
147
+ return button;
148
+ }
149
+
150
+ createButtonSuccess(): HTMLButtonElement {
151
+ const btn_success = this.createButton();
152
+ btn_success.type = 'submit';
153
+ btn_success.classList.add('btn-success');
154
+ btn_success.setAttribute('aria-label', 'Save');
155
+ btn_success.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M13.485 1.929a1 1 0 0 1 .057 1.414l-7.5 8a1 1 0 0 1-1.45.036l-3.5-3.5a1 1 0 1 1 1.414-1.415L5.5 9.379l6.571-7.007a1 1 0 0 1 1.414-.043z"/></svg>`;
156
+ return btn_success;
157
+ }
158
+
159
+ createButtonCancel(): HTMLButtonElement {
160
+ const btn_cancel = this.createButton();
161
+ btn_cancel.classList.add('btn-danger');
162
+ btn_cancel.setAttribute('aria-label', 'Cancel');
163
+ btn_cancel.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854z"/></svg>`;
164
+ btn_cancel.addEventListener('click', () => {
165
+ this.closeAndFocus();
166
+ });
167
+ return btn_cancel;
168
+ }
169
+
170
+ hideLoad(): void {
171
+ if (this.load) {
172
+ this.load.hidden = true;
173
+ }
174
+ }
175
+
176
+ showLoad(): void {
177
+ if (this.load) {
178
+ this.load.hidden = false;
179
+ }
180
+ }
181
+
182
+ async ajax(new_value: string): Promise<Response> {
183
+ const urlOption = this.context.options.url;
184
+ if (!urlOption) {
185
+ const err = new Error(
186
+ 'ajax() was called without a `url` option configured. Set `url` (string or function) before calling save.',
187
+ );
188
+ console.error(err);
189
+ throw err;
190
+ }
191
+ const url = typeof urlOption === 'function' ? urlOption(this.context, new_value) : urlOption;
192
+
193
+ if (this.context.options.requestBuilder) {
194
+ const built = await this.context.options.requestBuilder(this.context, new_value, url);
195
+ return fetch(built.url, built.init);
196
+ }
197
+
198
+ const form = new FormData();
199
+ form.append(this.context.options.name ?? 'value', new_value);
200
+
201
+ const ajaxOptions = { ...this.context.options.ajaxOptions };
202
+ ajaxOptions.body = form;
203
+ return fetch(url, ajaxOptions);
204
+ }
205
+
206
+ async successResponse(_response: Response, _newValue: string): Promise<string | undefined> {
207
+ return undefined;
208
+ }
209
+
210
+ async errorResponse(_response: Response, _newValue: string): Promise<string | undefined> {
211
+ return undefined;
212
+ }
213
+
214
+ setError(errorMsg: string): void {
215
+ if (this.error) {
216
+ this.error.textContent = errorMsg;
217
+ }
218
+ }
219
+
220
+ showError(): void {
221
+ if (this.error) {
222
+ this.error.hidden = false;
223
+ }
224
+ }
225
+
226
+ hideError(): void {
227
+ if (this.error) {
228
+ this.error.hidden = true;
229
+ }
230
+ }
231
+
232
+ createElement(name: string): HTMLInputElement {
233
+ const element = <HTMLInputElement>document.createElement(name);
234
+ element.classList.add('form-control');
235
+ if (this.context.options.required) {
236
+ element.required = this.context.options.required;
237
+ }
238
+ this.applyAttributes(element);
239
+ if (!this.context.options.showButtons) {
240
+ element.addEventListener('change', () => {
241
+ if (this.form) {
242
+ this.form.dispatchEvent(new Event('submit'));
243
+ }
244
+ });
245
+ }
246
+ this.add_focus(element);
247
+ return element;
248
+ }
249
+
250
+ private applyAttributes(element: HTMLInputElement): void {
251
+ const attrs = this.context.options.attributes || {};
252
+ const allowedAttributes = [
253
+ 'step',
254
+ 'min',
255
+ 'max',
256
+ 'minlength',
257
+ 'maxlength',
258
+ 'pattern',
259
+ 'placeholder',
260
+ 'required',
261
+ 'readonly',
262
+ 'disabled',
263
+ 'autocomplete',
264
+ 'autofocus',
265
+ ];
266
+ for (const [key, value] of Object.entries(attrs)) {
267
+ if (allowedAttributes.includes(key) && value !== undefined) {
268
+ element.setAttribute(key, String(value));
269
+ }
270
+ }
271
+ }
272
+
273
+ add_focus(element: HTMLInputElement): void {
274
+ this.context.element.addEventListener(
275
+ 'shown',
276
+ () => {
277
+ element.focus();
278
+ },
279
+ { once: true },
280
+ );
281
+ }
282
+
283
+ initText(): boolean {
284
+ if (this.context.getValue() === '') {
285
+ this.context.element.textContent = this.context.options.emptyText || '';
286
+ return true;
287
+ } else {
288
+ const text = this.context.getValue();
289
+ this.context.element.textContent = this.context.options.render
290
+ ? this.context.options.render(text, this.context)
291
+ : text;
292
+ return false;
293
+ }
294
+ }
295
+
296
+ initOptions(): void {}
297
+
298
+ getValue(): string {
299
+ return this.element ? this.element.value : '';
300
+ }
301
+ }
@@ -0,0 +1,18 @@
1
+ import dayjs from 'dayjs';
2
+ import DateType from './DateType.js';
3
+
4
+ export default class DateTimeType extends DateType {
5
+ create() {
6
+ const input = this.createElement(`input`);
7
+ input.type = 'datetime-local';
8
+
9
+ return this.createContainer(input);
10
+ }
11
+
12
+ initOptions(): void {
13
+ const default_format = 'YYYY-MM-DDTHH:mm';
14
+ const format = this.context.get_opt('format', default_format) as string;
15
+ const displayFormat = this.context.get_opt('displayFormat', default_format) as string;
16
+ this.context.setValue(dayjs(this.context.getValue(), displayFormat).format(format));
17
+ }
18
+ }
@@ -0,0 +1,43 @@
1
+ import dayjs from 'dayjs';
2
+ import customParseFormat from 'dayjs/plugin/customParseFormat';
3
+ import BaseType from './BaseType.js';
4
+
5
+ dayjs.extend(customParseFormat);
6
+
7
+ export default class DateType extends BaseType {
8
+ checkUnsupportedOptions(): void {
9
+ if (this.context.options.source !== undefined) {
10
+ console.error(
11
+ `${this.constructor.name} does not support the "source" option — it only applies to type: 'select'. It will be ignored.`,
12
+ );
13
+ }
14
+ }
15
+
16
+ create() {
17
+ const input = this.createElement(`input`);
18
+ input.type = 'date';
19
+
20
+ return this.createContainer(input);
21
+ }
22
+
23
+ initText(): boolean {
24
+ const value = this.context.getValue();
25
+ if (value === '') {
26
+ this.context.element.textContent = this.context.options.emptyText || '';
27
+ return true;
28
+ } else {
29
+ const text = dayjs(value, this.context.options.format).format(this.context.options.displayFormat);
30
+ this.context.element.textContent = this.context.options.render
31
+ ? this.context.options.render(text, this.context)
32
+ : text;
33
+ return false;
34
+ }
35
+ }
36
+
37
+ initOptions(): void {
38
+ const default_format = 'YYYY-MM-DD';
39
+ const format = this.context.get_opt('format', default_format) as string;
40
+ const displayFormat = this.context.get_opt('displayFormat', default_format) as string;
41
+ this.context.setValue(dayjs(this.context.getValue(), displayFormat).format(format));
42
+ }
43
+ }
@@ -0,0 +1,12 @@
1
+ import BaseType from './BaseType.js';
2
+
3
+ export default class InputType extends BaseType {
4
+ create() {
5
+ const input = this.createElement(`input`);
6
+ const { options = {} } = this.context;
7
+
8
+ input.type = typeof options.type === 'string' ? options.type : 'text';
9
+
10
+ return this.createContainer(input);
11
+ }
12
+ }
@@ -0,0 +1,70 @@
1
+ import BaseType from './BaseType.js';
2
+
3
+ export default class SelectType extends BaseType {
4
+ checkUnsupportedOptions(): void {
5
+ if (this.context.options.format !== undefined) {
6
+ console.error(
7
+ `${this.constructor.name} does not support the "format" option — it only applies to type: 'date'/'datetime'. It will be ignored.`,
8
+ );
9
+ }
10
+ if (this.context.options.displayFormat !== undefined) {
11
+ console.error(
12
+ `${this.constructor.name} does not support the "displayFormat" option — it only applies to type: 'date'/'datetime'. It will be ignored.`,
13
+ );
14
+ }
15
+ }
16
+
17
+ create() {
18
+ const select = this.createElement(`select`);
19
+ if (this.context.options.source && Array.isArray(this.context.options.source)) {
20
+ this.context.options.source.forEach((item) => {
21
+ const opt = document.createElement(`option`);
22
+ opt.value = String(item.value);
23
+ opt.textContent = item.text;
24
+ select.append(opt);
25
+ });
26
+ }
27
+
28
+ return this.createContainer(select);
29
+ }
30
+
31
+ initText() {
32
+ this.context.element.textContent = this.context.options.emptyText || '';
33
+ if (
34
+ this.context.getValue() !== '' &&
35
+ this.context.options.source &&
36
+ Array.isArray(this.context.options.source) &&
37
+ this.context.options.source.length > 0
38
+ ) {
39
+ for (let i = 0; i < this.context.options.source.length; i++) {
40
+ const item = this.context.options.source[i];
41
+ if (String(item.value) === this.context.getValue()) {
42
+ this.context.element.textContent = this.context.options.render
43
+ ? this.context.options.render(item.text, this.context)
44
+ : item.text;
45
+ return false;
46
+ }
47
+ }
48
+ }
49
+ return true;
50
+ }
51
+
52
+ initOptions() {
53
+ this.context.get_opt('source', []);
54
+ if (
55
+ this.context.options &&
56
+ typeof this.context.options.source === 'string' &&
57
+ this.context.options.source !== ''
58
+ ) {
59
+ try {
60
+ this.context.options.source = JSON.parse(this.context.options.source);
61
+ } catch (e) {
62
+ const el = this.context.element;
63
+ const identifier = el.id ? `#${el.id}` : `<${el.tagName.toLowerCase()}>`;
64
+ throw new Error(
65
+ `Invalid JSON in "source" option/data-source attribute on ${identifier}: ${(e as Error).message}`,
66
+ );
67
+ }
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,9 @@
1
+ import BaseType from './BaseType.js';
2
+
3
+ export default class TextAreaType extends BaseType {
4
+ create() {
5
+ const textarea = this.createElement(`textarea`);
6
+
7
+ return this.createContainer(textarea);
8
+ }
9
+ }
@@ -0,0 +1,136 @@
1
+ .editable-element {
2
+ border-bottom: dashed 1px var(--bs-primary, #08c);
3
+ text-decoration: none;
4
+ cursor: pointer;
5
+ }
6
+
7
+ .editable-element-disabled {
8
+ border-bottom: none;
9
+ cursor: default;
10
+ }
11
+
12
+ .editable-element-empty {
13
+ font-style: italic;
14
+ color: var(--bs-danger, #d14);
15
+ }
16
+
17
+ .editable {
18
+ max-width: none;
19
+ }
20
+
21
+ .editable-form {
22
+ gap: 20px;
23
+ }
24
+
25
+ .editable-load-overlay {
26
+ position: absolute;
27
+ inset: 0;
28
+ display: flex;
29
+ align-items: center;
30
+ justify-content: center;
31
+ background: var(--bs-body-bg, #fff);
32
+ }
33
+
34
+ .editable-loader {
35
+ --editable-loader-color: var(--bs-body-color, #000);
36
+ font-size: 5px;
37
+ width: 1em;
38
+ height: 1em;
39
+ border-radius: 50%;
40
+ position: relative;
41
+ text-indent: -9999em;
42
+ animation: load5 1.1s infinite ease;
43
+ transform: translateZ(0);
44
+ }
45
+
46
+ @keyframes load5 {
47
+ 0%,
48
+ 100% {
49
+ box-shadow:
50
+ 0em -2.6em 0em 0em var(--editable-loader-color),
51
+ 1.8em -1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
52
+ 2.5em 0em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
53
+ 1.75em 1.75em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
54
+ 0em 2.5em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
55
+ -1.8em 1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
56
+ -2.6em 0em 0 0em color-mix(in srgb, var(--editable-loader-color) 50%, transparent),
57
+ -1.8em -1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 70%, transparent);
58
+ }
59
+ 12.5% {
60
+ box-shadow:
61
+ 0em -2.6em 0em 0em color-mix(in srgb, var(--editable-loader-color) 70%, transparent),
62
+ 1.8em -1.8em 0 0em var(--editable-loader-color),
63
+ 2.5em 0em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
64
+ 1.75em 1.75em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
65
+ 0em 2.5em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
66
+ -1.8em 1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
67
+ -2.6em 0em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
68
+ -1.8em -1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 50%, transparent);
69
+ }
70
+ 25% {
71
+ box-shadow:
72
+ 0em -2.6em 0em 0em color-mix(in srgb, var(--editable-loader-color) 50%, transparent),
73
+ 1.8em -1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 70%, transparent),
74
+ 2.5em 0em 0 0em var(--editable-loader-color),
75
+ 1.75em 1.75em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
76
+ 0em 2.5em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
77
+ -1.8em 1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
78
+ -2.6em 0em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
79
+ -1.8em -1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent);
80
+ }
81
+ 37.5% {
82
+ box-shadow:
83
+ 0em -2.6em 0em 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
84
+ 1.8em -1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 50%, transparent),
85
+ 2.5em 0em 0 0em color-mix(in srgb, var(--editable-loader-color) 70%, transparent),
86
+ 1.75em 1.75em 0 0em var(--editable-loader-color),
87
+ 0em 2.5em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
88
+ -1.8em 1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
89
+ -2.6em 0em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
90
+ -1.8em -1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent);
91
+ }
92
+ 50% {
93
+ box-shadow:
94
+ 0em -2.6em 0em 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
95
+ 1.8em -1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
96
+ 2.5em 0em 0 0em color-mix(in srgb, var(--editable-loader-color) 50%, transparent),
97
+ 1.75em 1.75em 0 0em color-mix(in srgb, var(--editable-loader-color) 70%, transparent),
98
+ 0em 2.5em 0 0em var(--editable-loader-color),
99
+ -1.8em 1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
100
+ -2.6em 0em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
101
+ -1.8em -1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent);
102
+ }
103
+ 62.5% {
104
+ box-shadow:
105
+ 0em -2.6em 0em 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
106
+ 1.8em -1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
107
+ 2.5em 0em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
108
+ 1.75em 1.75em 0 0em color-mix(in srgb, var(--editable-loader-color) 50%, transparent),
109
+ 0em 2.5em 0 0em color-mix(in srgb, var(--editable-loader-color) 70%, transparent),
110
+ -1.8em 1.8em 0 0em var(--editable-loader-color),
111
+ -2.6em 0em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
112
+ -1.8em -1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent);
113
+ }
114
+ 75% {
115
+ box-shadow:
116
+ 0em -2.6em 0em 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
117
+ 1.8em -1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
118
+ 2.5em 0em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
119
+ 1.75em 1.75em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
120
+ 0em 2.5em 0 0em color-mix(in srgb, var(--editable-loader-color) 50%, transparent),
121
+ -1.8em 1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 70%, transparent),
122
+ -2.6em 0em 0 0em var(--editable-loader-color),
123
+ -1.8em -1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent);
124
+ }
125
+ 87.5% {
126
+ box-shadow:
127
+ 0em -2.6em 0em 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
128
+ 1.8em -1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
129
+ 2.5em 0em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
130
+ 1.75em 1.75em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
131
+ 0em 2.5em 0 0em color-mix(in srgb, var(--editable-loader-color) 20%, transparent),
132
+ -1.8em 1.8em 0 0em color-mix(in srgb, var(--editable-loader-color) 50%, transparent),
133
+ -2.6em 0em 0 0em color-mix(in srgb, var(--editable-loader-color) 70%, transparent),
134
+ -1.8em -1.8em 0 0em var(--editable-loader-color);
135
+ }
136
+ }