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.
- package/LICENSE +21 -0
- package/README.md +325 -0
- package/dist/editable.css +1 -0
- package/dist/editable.iife.js +3 -0
- package/dist/editable.iife.js.map +1 -0
- package/dist/editable.js +906 -0
- package/dist/editable.js.map +1 -0
- package/dist/editable.umd.cjs +3 -0
- package/dist/editable.umd.cjs.map +1 -0
- package/dist/src/Interfaces/BaseTypeButtons.d.ts +4 -0
- package/dist/src/Interfaces/Options.d.ts +36 -0
- package/dist/src/Modes/BaseMode.d.ts +14 -0
- package/dist/src/Modes/InlineMode.d.ts +11 -0
- package/dist/src/Modes/PopupMode.d.ts +12 -0
- package/dist/src/Types/BaseType.d.ts +35 -0
- package/dist/src/Types/DateTimeType.d.ts +5 -0
- package/dist/src/Types/DateType.d.ts +7 -0
- package/dist/src/Types/InputType.d.ts +4 -0
- package/dist/src/Types/SelectType.d.ts +7 -0
- package/dist/src/Types/TextAreaType.d.ts +4 -0
- package/dist/src/editable.d.ts +51 -0
- package/dist/tests/accessibility.test.d.ts +1 -0
- package/dist/tests/disabled.test.d.ts +1 -0
- package/dist/tests/errors.test.d.ts +1 -0
- package/dist/tests/events.test.d.ts +1 -0
- package/dist/tests/lifecycle.test.d.ts +1 -0
- package/dist/tests/public-api.test.d.ts +1 -0
- package/dist/tests/render.test.d.ts +1 -0
- package/dist/tests/request.test.d.ts +1 -0
- package/dist/tests/save-flow.test.d.ts +1 -0
- package/dist/tests/theme.test.d.ts +1 -0
- package/dist/tests/types/attributes.test.d.ts +1 -0
- package/dist/tests/types/date.test.d.ts +1 -0
- package/dist/tests/types/select.test.d.ts +1 -0
- package/package.json +57 -0
- package/src/Interfaces/BaseTypeButtons.ts +4 -0
- package/src/Interfaces/Options.ts +30 -0
- package/src/Modes/BaseMode.ts +57 -0
- package/src/Modes/InlineMode.ts +47 -0
- package/src/Modes/PopupMode.ts +107 -0
- package/src/Types/BaseType.ts +301 -0
- package/src/Types/DateTimeType.ts +18 -0
- package/src/Types/DateType.ts +43 -0
- package/src/Types/InputType.ts +12 -0
- package/src/Types/SelectType.ts +70 -0
- package/src/Types/TextAreaType.ts +9 -0
- package/src/editable.css +136 -0
- package/src/editable.ts +244 -0
package/src/editable.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import './editable.css';
|
|
2
|
+
import type Options from './Interfaces/Options.ts';
|
|
3
|
+
import BaseMode from './Modes/BaseMode.ts';
|
|
4
|
+
import InlineMode from './Modes/InlineMode.ts';
|
|
5
|
+
import PopupMode from './Modes/PopupMode.ts';
|
|
6
|
+
import BaseType from './Types/BaseType.ts';
|
|
7
|
+
import DateTimeType from './Types/DateTimeType.ts';
|
|
8
|
+
import DateType from './Types/DateType.ts';
|
|
9
|
+
import InputType from './Types/InputType.ts';
|
|
10
|
+
import SelectType from './Types/SelectType.ts';
|
|
11
|
+
import TextAreaType from './Types/TextAreaType.ts';
|
|
12
|
+
|
|
13
|
+
export type { Options };
|
|
14
|
+
|
|
15
|
+
export default class Editable {
|
|
16
|
+
static BaseType = BaseType;
|
|
17
|
+
static BaseMode = BaseMode;
|
|
18
|
+
static InputType = InputType;
|
|
19
|
+
static TextAreaType = TextAreaType;
|
|
20
|
+
static SelectType = SelectType;
|
|
21
|
+
static DateType = DateType;
|
|
22
|
+
static DateTimeType = DateTimeType;
|
|
23
|
+
static PopupMode = PopupMode;
|
|
24
|
+
static InlineMode = InlineMode;
|
|
25
|
+
|
|
26
|
+
static types: Map<string, typeof BaseType> = new Map<string, typeof BaseType>([
|
|
27
|
+
['text', InputType],
|
|
28
|
+
['password', InputType],
|
|
29
|
+
['email', InputType],
|
|
30
|
+
['url', InputType],
|
|
31
|
+
['tel', InputType],
|
|
32
|
+
['number', InputType],
|
|
33
|
+
['range', InputType],
|
|
34
|
+
['time', InputType],
|
|
35
|
+
['textarea', TextAreaType],
|
|
36
|
+
['select', SelectType],
|
|
37
|
+
['date', DateType],
|
|
38
|
+
['datetime', DateTimeType],
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
static modes: Map<string, typeof BaseMode> = new Map<string, typeof BaseMode>([
|
|
42
|
+
['popup', PopupMode],
|
|
43
|
+
['inline', InlineMode],
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
static registerType(name: string, type: typeof BaseType): void {
|
|
47
|
+
Editable.types.set(name, type);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static registerMode(name: string, mode: typeof BaseMode): void {
|
|
51
|
+
Editable.modes.set(name, mode);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
element: HTMLElement;
|
|
55
|
+
options: Options;
|
|
56
|
+
|
|
57
|
+
typeElement: BaseType;
|
|
58
|
+
modeElement: BaseMode;
|
|
59
|
+
|
|
60
|
+
private accessibilityAbortController = new AbortController();
|
|
61
|
+
|
|
62
|
+
constructor(element: HTMLElement, options: Options = {}) {
|
|
63
|
+
this.element = element;
|
|
64
|
+
this.options = { ...options };
|
|
65
|
+
this.init_options();
|
|
66
|
+
this.typeElement = this.route_type();
|
|
67
|
+
this.typeElement.checkUnsupportedOptions();
|
|
68
|
+
this.typeElement.initOptions();
|
|
69
|
+
this.modeElement = this.route_mode();
|
|
70
|
+
this.modeElement.init();
|
|
71
|
+
this.init_text();
|
|
72
|
+
this.init_style();
|
|
73
|
+
this.init_accessibility();
|
|
74
|
+
this.element.dispatchEvent(new CustomEvent('init', { detail: { Editable: this } }));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Options has no index signature by design (keeps the public shape autocomplete-friendly),
|
|
78
|
+
// so these two helpers are the only place that reads/writes it dynamically by name.
|
|
79
|
+
private readOption(name: string): unknown {
|
|
80
|
+
// @ts-expect-error
|
|
81
|
+
return this.options[name];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private writeOption(name: string, value: unknown): void {
|
|
85
|
+
// @ts-expect-error
|
|
86
|
+
this.options[name] = value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get_opt(name: string, default_value: unknown): unknown {
|
|
90
|
+
const value = this.element.dataset?.[name] ?? this.readOption(name) ?? default_value;
|
|
91
|
+
this.writeOption(name, value);
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get_opt_object(name: string, default_value: unknown): void {
|
|
96
|
+
// Object-valued options are JS-only — dataset is deliberately not read here
|
|
97
|
+
this.writeOption(name, this.readOption(name) ?? default_value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get_opt_bool(name: string, default_value: boolean): void {
|
|
101
|
+
this.get_opt(name, default_value);
|
|
102
|
+
if (typeof this.readOption(name) !== 'boolean') {
|
|
103
|
+
if (this.readOption(name) === 'true') {
|
|
104
|
+
this.writeOption(name, true);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (this.readOption(name) === 'false') {
|
|
108
|
+
this.writeOption(name, false);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.writeOption(name, default_value);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
init_options(): void {
|
|
116
|
+
//priority date elements
|
|
117
|
+
this.get_opt('value', this.element.textContent ?? '');
|
|
118
|
+
this.get_opt('name', this.element.id || 'value');
|
|
119
|
+
this.get_opt('title', '');
|
|
120
|
+
this.get_opt('type', 'text');
|
|
121
|
+
this.get_opt('emptyText', 'Empty');
|
|
122
|
+
this.get_opt('mode', 'popup');
|
|
123
|
+
this.get_opt('url', null);
|
|
124
|
+
this.get_opt_object('ajaxOptions', {});
|
|
125
|
+
this.options.ajaxOptions = Object.assign({ method: 'POST' }, this.options.ajaxOptions);
|
|
126
|
+
this.get_opt_bool('send', true);
|
|
127
|
+
this.get_opt_bool('required', false);
|
|
128
|
+
this.get_opt_bool('showButtons', true);
|
|
129
|
+
if (this.options?.success && typeof this.options?.success === 'function') {
|
|
130
|
+
this.success = this.options.success;
|
|
131
|
+
}
|
|
132
|
+
if (this.options?.error && typeof this.options?.error === 'function') {
|
|
133
|
+
this.error = this.options.error;
|
|
134
|
+
}
|
|
135
|
+
this.get_opt_object('attributes', {});
|
|
136
|
+
this.get_opt_object('popoverOptions', {});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
init_text() {
|
|
140
|
+
const empty_class = 'editable-element-empty';
|
|
141
|
+
this.element.classList.remove(empty_class);
|
|
142
|
+
if (this.typeElement.initText()) {
|
|
143
|
+
this.element.classList.add(empty_class);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
init_style() {
|
|
148
|
+
this.element.classList.add('editable-element');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// The trigger has no native keyboard semantics unless the host already made it one
|
|
152
|
+
// (a real <button> or <a href>) — give it button-like tabbing and Enter/Space activation
|
|
153
|
+
// otherwise. Guarded to the element itself so keys typed into an open inline-mode input,
|
|
154
|
+
// which lives inside this same element, don't re-trigger it.
|
|
155
|
+
init_accessibility(): void {
|
|
156
|
+
const el = this.element;
|
|
157
|
+
const isNativelyInteractive =
|
|
158
|
+
el.tagName === 'BUTTON' ||
|
|
159
|
+
el.tagName === 'INPUT' ||
|
|
160
|
+
el.tagName === 'SELECT' ||
|
|
161
|
+
el.tagName === 'TEXTAREA' ||
|
|
162
|
+
(el.tagName === 'A' && el.hasAttribute('href'));
|
|
163
|
+
if (isNativelyInteractive) return;
|
|
164
|
+
|
|
165
|
+
if (!el.hasAttribute('tabindex')) {
|
|
166
|
+
el.tabIndex = 0;
|
|
167
|
+
}
|
|
168
|
+
if (!el.hasAttribute('role')) {
|
|
169
|
+
el.setAttribute('role', 'button');
|
|
170
|
+
}
|
|
171
|
+
el.addEventListener(
|
|
172
|
+
'keydown',
|
|
173
|
+
(e) => {
|
|
174
|
+
if (e.target !== el) return;
|
|
175
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
el.click();
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
{ signal: this.accessibilityAbortController.signal },
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
route_mode(): BaseMode {
|
|
185
|
+
const ModeClass = Editable.modes.get(this.options.mode as string);
|
|
186
|
+
if (!ModeClass) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Mode "${this.options.mode}" is not registered. Available modes: ${[...Editable.modes.keys()].join(', ')}. Register custom modes via Editable.registerMode(name, ModeClass).`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return new ModeClass(this);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
route_type(): BaseType {
|
|
195
|
+
if (this.options.type && typeof this.options.type !== 'string') {
|
|
196
|
+
// @ts-expect-error
|
|
197
|
+
return new this.options.type(this);
|
|
198
|
+
}
|
|
199
|
+
const TypeClass = Editable.types.get(this.options.type as string);
|
|
200
|
+
if (!TypeClass) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`Type "${this.options.type}" is not registered. Available types: ${[...Editable.types.keys()].join(', ')}. Register custom types via Editable.registerType(name, TypeClass).`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return new TypeClass(this);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async success(response: Response, newValue: string): Promise<string | undefined> {
|
|
209
|
+
return await this.typeElement.successResponse(response, newValue);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async error(response: Response, newValue: string): Promise<string | undefined> {
|
|
213
|
+
return await this.typeElement.errorResponse(response, newValue);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
enable(): void {
|
|
217
|
+
this.element.classList.remove('editable-element-disabled');
|
|
218
|
+
this.modeElement.enable();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
disable(): void {
|
|
222
|
+
this.element.classList.add('editable-element-disabled');
|
|
223
|
+
this.modeElement.disable();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
setValue(value: string): void {
|
|
227
|
+
this.options.value = value;
|
|
228
|
+
this.init_text();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
getValue(): string {
|
|
232
|
+
return this.options.value ?? '';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
getOption(name: string): unknown {
|
|
236
|
+
return this.readOption(name) ?? null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
destroy(): void {
|
|
240
|
+
this.accessibilityAbortController.abort();
|
|
241
|
+
this.modeElement.destroy();
|
|
242
|
+
this.element.classList.remove('editable-element', 'editable-element-disabled', 'editable-element-empty');
|
|
243
|
+
}
|
|
244
|
+
}
|