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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { default as Options } from './Interfaces/Options.ts';
|
|
2
|
+
import { default as BaseMode } from './Modes/BaseMode.ts';
|
|
3
|
+
import { default as InlineMode } from './Modes/InlineMode.ts';
|
|
4
|
+
import { default as PopupMode } from './Modes/PopupMode.ts';
|
|
5
|
+
import { default as BaseType } from './Types/BaseType.ts';
|
|
6
|
+
import { default as DateTimeType } from './Types/DateTimeType.ts';
|
|
7
|
+
import { default as DateType } from './Types/DateType.ts';
|
|
8
|
+
import { default as InputType } from './Types/InputType.ts';
|
|
9
|
+
import { default as SelectType } from './Types/SelectType.ts';
|
|
10
|
+
import { default as TextAreaType } from './Types/TextAreaType.ts';
|
|
11
|
+
export type { Options };
|
|
12
|
+
export default class Editable {
|
|
13
|
+
static BaseType: typeof BaseType;
|
|
14
|
+
static BaseMode: typeof BaseMode;
|
|
15
|
+
static InputType: typeof InputType;
|
|
16
|
+
static TextAreaType: typeof TextAreaType;
|
|
17
|
+
static SelectType: typeof SelectType;
|
|
18
|
+
static DateType: typeof DateType;
|
|
19
|
+
static DateTimeType: typeof DateTimeType;
|
|
20
|
+
static PopupMode: typeof PopupMode;
|
|
21
|
+
static InlineMode: typeof InlineMode;
|
|
22
|
+
static types: Map<string, typeof BaseType>;
|
|
23
|
+
static modes: Map<string, typeof BaseMode>;
|
|
24
|
+
static registerType(name: string, type: typeof BaseType): void;
|
|
25
|
+
static registerMode(name: string, mode: typeof BaseMode): void;
|
|
26
|
+
element: HTMLElement;
|
|
27
|
+
options: Options;
|
|
28
|
+
typeElement: BaseType;
|
|
29
|
+
modeElement: BaseMode;
|
|
30
|
+
private accessibilityAbortController;
|
|
31
|
+
constructor(element: HTMLElement, options?: Options);
|
|
32
|
+
private readOption;
|
|
33
|
+
private writeOption;
|
|
34
|
+
get_opt(name: string, default_value: unknown): unknown;
|
|
35
|
+
get_opt_object(name: string, default_value: unknown): void;
|
|
36
|
+
get_opt_bool(name: string, default_value: boolean): void;
|
|
37
|
+
init_options(): void;
|
|
38
|
+
init_text(): void;
|
|
39
|
+
init_style(): void;
|
|
40
|
+
init_accessibility(): void;
|
|
41
|
+
route_mode(): BaseMode;
|
|
42
|
+
route_type(): BaseType;
|
|
43
|
+
success(response: Response, newValue: string): Promise<string | undefined>;
|
|
44
|
+
error(response: Response, newValue: string): Promise<string | undefined>;
|
|
45
|
+
enable(): void;
|
|
46
|
+
disable(): void;
|
|
47
|
+
setValue(value: string): void;
|
|
48
|
+
getValue(): string;
|
|
49
|
+
getOption(name: string): unknown;
|
|
50
|
+
destroy(): void;
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sbs-editable-bs5",
|
|
3
|
+
"description": "In-place editing with Bootstrap 5",
|
|
4
|
+
"author": "Konstantin Litvinov",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"src"
|
|
10
|
+
],
|
|
11
|
+
"main": "./dist/editable.js",
|
|
12
|
+
"types": "./dist/editable.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
"import": "./dist/editable.js",
|
|
15
|
+
"require": "./dist/editable.umd.cjs"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"bootstrap": "^5.3"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"dayjs": "^1.11.23"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@biomejs/biome": "^2.5.14",
|
|
25
|
+
"@types/bootstrap": "^5.2.11",
|
|
26
|
+
"@vitest/browser": "^5.0.1",
|
|
27
|
+
"@vitest/browser-playwright": "^5.0.1",
|
|
28
|
+
"playwright": "^1.63.0",
|
|
29
|
+
"typescript": "^5.9.3",
|
|
30
|
+
"vite": "^8.3.0",
|
|
31
|
+
"vite-plugin-dts": "^5.1.0",
|
|
32
|
+
"vite-plugin-lib-inject-css": "^2.2.2",
|
|
33
|
+
"vitest": "^5.0.1"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"dev": "vite",
|
|
40
|
+
"build": "tsc && vite build",
|
|
41
|
+
"prepublishOnly": "npm run build",
|
|
42
|
+
"preview": "vite preview",
|
|
43
|
+
"lint": "biome check .",
|
|
44
|
+
"lint:fix": "biome check --write .",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:watch": "vitest"
|
|
47
|
+
},
|
|
48
|
+
"version": "1.0.0",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/koftikes/sbs-editable-bs5.git"
|
|
52
|
+
},
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/koftikes/sbs-editable-bs5/issues"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/koftikes/sbs-editable-bs5#readme"
|
|
57
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Popover } from 'bootstrap';
|
|
2
|
+
import type Editable from '../editable.ts';
|
|
3
|
+
import type BaseType from '../Types/BaseType.ts';
|
|
4
|
+
|
|
5
|
+
export default interface Options {
|
|
6
|
+
value?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
type?: BaseType | string;
|
|
10
|
+
ajaxOptions?: RequestInit;
|
|
11
|
+
send?: boolean;
|
|
12
|
+
mode?: 'popup' | 'inline';
|
|
13
|
+
emptyText?: string;
|
|
14
|
+
url?: string | ((context: Editable, newValue: string) => string) | null;
|
|
15
|
+
requestBuilder?: (
|
|
16
|
+
context: Editable,
|
|
17
|
+
newValue: string,
|
|
18
|
+
resolvedUrl: string,
|
|
19
|
+
) => { url: string; init: RequestInit } | Promise<{ url: string; init: RequestInit }>;
|
|
20
|
+
required?: boolean;
|
|
21
|
+
showButtons?: boolean;
|
|
22
|
+
success?: (response: Response, newValue: string | number) => Promise<string | undefined>;
|
|
23
|
+
error?: (response: Response, newValue: string | number) => Promise<string | undefined>;
|
|
24
|
+
popoverOptions?: Popover.Options;
|
|
25
|
+
render?: (text: string, context: Editable) => string;
|
|
26
|
+
format?: string;
|
|
27
|
+
displayFormat?: string;
|
|
28
|
+
source?: Array<{ value: string | number; text: string }> | string;
|
|
29
|
+
attributes?: { [key: string]: string | number | boolean | undefined };
|
|
30
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type Editable from '../editable.ts';
|
|
2
|
+
|
|
3
|
+
export default class BaseMode {
|
|
4
|
+
context: Editable;
|
|
5
|
+
|
|
6
|
+
constructor(context: Editable) {
|
|
7
|
+
if (this.constructor === BaseMode) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
'BaseMode is abstract and cannot be instantiated directly — create a subclass that implements init()/enable()/disable()/hide()/destroy() (see PopupMode, InlineMode).',
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
this.context = context;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
event_show() {
|
|
16
|
+
this.context.typeElement.hideError();
|
|
17
|
+
if (!this.context.typeElement.element) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`${this.context.typeElement.constructor.name}.create() did not call createContainer() — the input element was never set.`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
this.context.typeElement.element.value = this.context.getValue();
|
|
23
|
+
this.context.element.dispatchEvent(new CustomEvent('show', { detail: { Editable: this.context } }));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
event_shown() {
|
|
27
|
+
this.context.element.dispatchEvent(new CustomEvent('shown', { detail: { Editable: this.context } }));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
event_hide() {
|
|
31
|
+
this.context.element.dispatchEvent(new CustomEvent('hide', { detail: { Editable: this.context } }));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
event_hidden() {
|
|
35
|
+
this.context.element.dispatchEvent(new CustomEvent('hidden', { detail: { Editable: this.context } }));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
init() {
|
|
39
|
+
throw new Error(`${this.constructor.name} must implement init().`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
enable() {
|
|
43
|
+
throw new Error(`${this.constructor.name} must implement enable().`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
disable() {
|
|
47
|
+
throw new Error(`${this.constructor.name} must implement disable().`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
hide() {
|
|
51
|
+
throw new Error(`${this.constructor.name} must implement hide().`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
destroy() {
|
|
55
|
+
throw new Error(`${this.constructor.name} must implement destroy().`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import BaseMode from './BaseMode.js';
|
|
2
|
+
|
|
3
|
+
export default class InlineMode extends BaseMode {
|
|
4
|
+
private openHandler: (() => void) | null = null;
|
|
5
|
+
private disabled = false;
|
|
6
|
+
|
|
7
|
+
init() {
|
|
8
|
+
this.openHandler = () => {
|
|
9
|
+
if (!this.disabled) {
|
|
10
|
+
const item = this.context.typeElement.create();
|
|
11
|
+
this.event_show();
|
|
12
|
+
this.removeOpenHandler();
|
|
13
|
+
this.context.element.replaceChildren(item);
|
|
14
|
+
this.event_shown();
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
this.context.element.addEventListener('click', this.openHandler);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private removeOpenHandler(): void {
|
|
21
|
+
if (this.openHandler) {
|
|
22
|
+
this.context.element.removeEventListener('click', this.openHandler);
|
|
23
|
+
this.openHandler = null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
enable() {
|
|
28
|
+
this.disabled = false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
disable() {
|
|
32
|
+
this.disabled = true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
hide() {
|
|
36
|
+
this.event_hide();
|
|
37
|
+
this.context.element.textContent = this.context.getValue();
|
|
38
|
+
setTimeout(() => {
|
|
39
|
+
this.init();
|
|
40
|
+
this.event_hidden();
|
|
41
|
+
}, 100);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
destroy(): void {
|
|
45
|
+
this.removeOpenHandler();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Popover } from 'bootstrap';
|
|
2
|
+
import BaseMode from './BaseMode.js';
|
|
3
|
+
|
|
4
|
+
export default class PopupMode extends BaseMode {
|
|
5
|
+
popover: Popover | null = null;
|
|
6
|
+
private abortController: AbortController = new AbortController();
|
|
7
|
+
|
|
8
|
+
init() {
|
|
9
|
+
const options = {
|
|
10
|
+
container: 'body',
|
|
11
|
+
content: this.context.typeElement.create(),
|
|
12
|
+
html: true,
|
|
13
|
+
customClass: 'editable',
|
|
14
|
+
title: this.context.options.title,
|
|
15
|
+
};
|
|
16
|
+
this.popover = new Popover(this.context.element, Object.assign(options, this.context.options.popoverOptions));
|
|
17
|
+
const { signal } = this.abortController;
|
|
18
|
+
this.context.element.addEventListener(
|
|
19
|
+
'show.bs.popover',
|
|
20
|
+
() => {
|
|
21
|
+
this.event_show();
|
|
22
|
+
},
|
|
23
|
+
{ signal },
|
|
24
|
+
);
|
|
25
|
+
this.context.element.addEventListener(
|
|
26
|
+
'inserted.bs.popover',
|
|
27
|
+
() => {
|
|
28
|
+
this.syncTheme();
|
|
29
|
+
},
|
|
30
|
+
{ signal },
|
|
31
|
+
);
|
|
32
|
+
this.context.element.addEventListener(
|
|
33
|
+
'shown.bs.popover',
|
|
34
|
+
() => {
|
|
35
|
+
this.event_shown();
|
|
36
|
+
},
|
|
37
|
+
{ signal },
|
|
38
|
+
);
|
|
39
|
+
this.context.element.addEventListener(
|
|
40
|
+
'hide.bs.popover',
|
|
41
|
+
() => {
|
|
42
|
+
this.event_hide();
|
|
43
|
+
},
|
|
44
|
+
{ signal },
|
|
45
|
+
);
|
|
46
|
+
this.context.element.addEventListener(
|
|
47
|
+
'hidden.bs.popover',
|
|
48
|
+
() => {
|
|
49
|
+
this.event_hidden();
|
|
50
|
+
},
|
|
51
|
+
{ signal },
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
document.addEventListener(
|
|
55
|
+
'click',
|
|
56
|
+
(e) => {
|
|
57
|
+
const target = <HTMLElement>e.target;
|
|
58
|
+
// @ts-expect-error — Popover.tip is an undocumented internal property
|
|
59
|
+
const tip = this.popover?.tip;
|
|
60
|
+
if ((this.popover && target === tip) || target === this.context.element) return;
|
|
61
|
+
let current = target.parentNode;
|
|
62
|
+
while (current) {
|
|
63
|
+
if (current === tip) return;
|
|
64
|
+
current = current.parentNode;
|
|
65
|
+
}
|
|
66
|
+
this.hide();
|
|
67
|
+
},
|
|
68
|
+
{ signal },
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The tip is appended to <body> by default, outside any scoped data-bs-theme
|
|
73
|
+
// wrapper around the trigger — copy the nearest theme onto it explicitly so
|
|
74
|
+
// Bootstrap's CSS variables still resolve to the trigger's theme, not the page default.
|
|
75
|
+
private syncTheme(): void {
|
|
76
|
+
const themedAncestor = this.context.element.closest('[data-bs-theme]');
|
|
77
|
+
// @ts-expect-error — Popover.tip is an undocumented internal property
|
|
78
|
+
const tip = this.popover?.tip as HTMLElement | undefined;
|
|
79
|
+
if (tip && themedAncestor) {
|
|
80
|
+
tip.setAttribute('data-bs-theme', themedAncestor.getAttribute('data-bs-theme') ?? '');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
enable(): void {
|
|
85
|
+
if (this.popover) {
|
|
86
|
+
this.popover.enable();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
disable(): void {
|
|
91
|
+
if (this.popover) {
|
|
92
|
+
this.popover.disable();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
hide(): void {
|
|
97
|
+
if (this.popover) {
|
|
98
|
+
this.popover.hide();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
destroy(): void {
|
|
103
|
+
this.abortController.abort();
|
|
104
|
+
this.popover?.dispose();
|
|
105
|
+
this.popover = null;
|
|
106
|
+
}
|
|
107
|
+
}
|