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,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,4 @@
1
+ export default interface BaseTypeButtons {
2
+ success: HTMLElement | null;
3
+ cancel: HTMLElement | null;
4
+ }
@@ -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
+ }