asljs-ui-text-input 0.0.0-stage → 0.1.1
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.md +51 -0
- package/README.md +75 -2
- package/dist/bootstrap-text-input.tpl.d.ts +6 -0
- package/dist/bootstrap-text-input.tpl.js +28 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/text-input.d.ts +61 -0
- package/dist/text-input.js +676 -0
- package/dist/text-input.tpl.d.ts +6 -0
- package/dist/text-input.tpl.js +26 -0
- package/dist/theme.d.ts +13 -0
- package/dist/theme.js +14 -0
- package/docs/Text Input.md +112 -0
- package/package.json +64 -4
package/LICENSE.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Permissive No-Derivatives License 1.0
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexandrite Software Ltd.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software (the "Software") to:
|
|
7
|
+
|
|
8
|
+
1. Use the Software for any purpose, including commercial purposes.
|
|
9
|
+
2. Copy and redistribute the Software, including as part of another application,
|
|
10
|
+
library, or package.
|
|
11
|
+
3. Modify the Software for private or internal use.
|
|
12
|
+
|
|
13
|
+
Subject to the following conditions:
|
|
14
|
+
|
|
15
|
+
## 1. No Distribution of Modified Versions
|
|
16
|
+
|
|
17
|
+
Modified versions of the Software, or substantial portions thereof, must not be
|
|
18
|
+
distributed without prior written permission from the copyright holder.
|
|
19
|
+
|
|
20
|
+
Routine build transformations, including minification, transpilation, and
|
|
21
|
+
bundling, are permitted for distribution, provided they do not intentionally
|
|
22
|
+
alter the Software's functionality.
|
|
23
|
+
|
|
24
|
+
## 2. Licence and Copyright Preservation
|
|
25
|
+
|
|
26
|
+
All distributed copies of the Software must retain this copyright notice and
|
|
27
|
+
licence.
|
|
28
|
+
|
|
29
|
+
The Software itself, including copies incorporated into other applications,
|
|
30
|
+
libraries, or packages, remains subject to this licence.
|
|
31
|
+
|
|
32
|
+
The Software must not be sublicensed or redistributed under a different licence.
|
|
33
|
+
|
|
34
|
+
## 3. Use in Other Software
|
|
35
|
+
|
|
36
|
+
Applications and libraries that merely use, import, link to, or bundle the
|
|
37
|
+
Software are not considered modified versions of the Software and may be
|
|
38
|
+
distributed under any licence.
|
|
39
|
+
|
|
40
|
+
Such use, incorporation, or bundling does not grant permission to relicense the
|
|
41
|
+
Software or distribute modified versions of it.
|
|
42
|
+
|
|
43
|
+
### 4. No Warranty
|
|
44
|
+
|
|
45
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
46
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
47
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
48
|
+
|
|
49
|
+
IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR
|
|
50
|
+
OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR ITS
|
|
51
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,76 @@
|
|
|
1
|
-
#
|
|
1
|
+
# text-input
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Part of [Alexandrite Software Library][#1] - a set of high-quality, performant
|
|
4
|
+
> JavaScript libraries and tools for everyday use.
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
`asljs-ui-text-input` provides `asljs-text-input`, a labelled text field or
|
|
9
|
+
textarea with validation. `value` sets or resets the field; the user's draft,
|
|
10
|
+
its validity and dirty state are reported through `input` and `change` events.
|
|
11
|
+
|
|
12
|
+
## Scope
|
|
13
|
+
|
|
14
|
+
- Single-line input or auto-extending textarea.
|
|
15
|
+
- Validation through a `validator` function returning an error message.
|
|
16
|
+
- Layout and control markup from templates: a local slot template, the
|
|
17
|
+
`textInput` theme section, or the Bootstrap part,
|
|
18
|
+
`createBootstrapTextInputTheme()`.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install asljs-ui-text-input
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
NPM package: [asljs-ui-text-input][NPM]
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
Import the package once to register the element:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import 'asljs-ui-text-input';
|
|
34
|
+
|
|
35
|
+
const input =
|
|
36
|
+
document.createElement('asljs-text-input') as HTMLElement & {
|
|
37
|
+
label: string | null;
|
|
38
|
+
validator: ((value: string) => string | null) | null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
input.label = 'Name';
|
|
42
|
+
input.validator =
|
|
43
|
+
value => value.trim() === ''
|
|
44
|
+
? 'Name is required.'
|
|
45
|
+
: null;
|
|
46
|
+
|
|
47
|
+
input.addEventListener(
|
|
48
|
+
'change',
|
|
49
|
+
event => console.log((event as CustomEvent<{ value: string }>).detail.value));
|
|
50
|
+
|
|
51
|
+
document.body.appendChild(input);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Further reading
|
|
55
|
+
|
|
56
|
+
- [Text Input][DOC] - properties, events and template overrides.
|
|
57
|
+
- [Theming][THM] - templates, themes and how they combine.
|
|
58
|
+
|
|
59
|
+
Questions and bugs: [asljs/issues][ISS].
|
|
60
|
+
|
|
61
|
+
## Related packages
|
|
62
|
+
|
|
63
|
+
- [ui][UI] - the shared theme and the other UI packages.
|
|
64
|
+
- `asljs-data-binding` for the bindings the templates use.
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT License. See [LICENSE][LIC] for details.
|
|
69
|
+
|
|
70
|
+
[#1]: https://github.com/AlexandriteSoftware/asljs
|
|
71
|
+
[DOC]: <docs/Text Input.md>
|
|
72
|
+
[ISS]: https://github.com/AlexandriteSoftware/asljs/issues
|
|
73
|
+
[LIC]: LICENSE.md
|
|
74
|
+
[NPM]: https://www.npmjs.com/package/asljs-ui-text-input
|
|
75
|
+
[THM]: ../core/docs/Theming.md
|
|
76
|
+
[UI]: ../core/README.md
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type BindDataModelOptions } from "asljs-data-binding";
|
|
2
|
+
import type { TextInputTemplateModel as TemplateModel } from "./text-input.js";
|
|
3
|
+
/** The markup inside the template's <template> element. */
|
|
4
|
+
export declare const html = "\n <div class=\"mb-3\"\n data-bind-class-asljs-text-input-empty=\"isEmpty\"\n data-bind-class-asljs-text-input-invalid=\"hasError\">\n <label class=\"form-label\"\n data-bind-text=\"label\"\n data-bind-prop-hidden=\"hideLabel\"\n data-bind-for=\"inputId\"></label>\n <div data-role=\"control-host\"></div>\n <div class=\"invalid-feedback\"\n data-bind-text=\"errorMessage\"\n data-bind-prop-hidden=\"hideError\"\n data-bind-prop-id=\"errorId\"></div>\n <div class=\"form-text\"\n data-bind-text=\"description\"\n data-bind-prop-hidden=\"hideDescription\"\n data-bind-prop-id=\"descriptionId\"></div>\n </div>\n";
|
|
5
|
+
export declare function createTemplate(doc?: Document): HTMLTemplateElement;
|
|
6
|
+
export declare function bindTemplate(root: ParentNode, model: TemplateModel, options?: BindDataModelOptions): () => boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Generated by data-bind-compile from bootstrap-text-input.tpl.html. Do not edit.
|
|
2
|
+
import { bindDataModel } from "asljs-data-binding";
|
|
3
|
+
/** The markup inside the template's <template> element. */
|
|
4
|
+
export const html = "\n <div class=\"mb-3\"\n data-bind-class-asljs-text-input-empty=\"isEmpty\"\n data-bind-class-asljs-text-input-invalid=\"hasError\">\n <label class=\"form-label\"\n data-bind-text=\"label\"\n data-bind-prop-hidden=\"hideLabel\"\n data-bind-for=\"inputId\"></label>\n <div data-role=\"control-host\"></div>\n <div class=\"invalid-feedback\"\n data-bind-text=\"errorMessage\"\n data-bind-prop-hidden=\"hideError\"\n data-bind-prop-id=\"errorId\"></div>\n <div class=\"form-text\"\n data-bind-text=\"description\"\n data-bind-prop-hidden=\"hideDescription\"\n data-bind-prop-id=\"descriptionId\"></div>\n </div>\n";
|
|
5
|
+
export function createTemplate(doc = document) {
|
|
6
|
+
const template = doc.createElement('template');
|
|
7
|
+
template.innerHTML = html;
|
|
8
|
+
return template;
|
|
9
|
+
}
|
|
10
|
+
export function bindTemplate(root, model, options) {
|
|
11
|
+
return bindDataModel(root, model, options);
|
|
12
|
+
}
|
|
13
|
+
// Never called: each statement checks one binding against the model and
|
|
14
|
+
// element types, and names the template line it comes from.
|
|
15
|
+
function check(model) {
|
|
16
|
+
void model.isEmpty; // bootstrap-text-input.tpl.html:3:8 data-bind-class-asljs-text-input-empty
|
|
17
|
+
void model.hasError; // bootstrap-text-input.tpl.html:4:8 data-bind-class-asljs-text-input-invalid
|
|
18
|
+
void model.label; // bootstrap-text-input.tpl.html:6:12 data-bind-text
|
|
19
|
+
void model.hideLabel; // bootstrap-text-input.tpl.html:7:12 data-bind-prop-hidden
|
|
20
|
+
void model.inputId; // bootstrap-text-input.tpl.html:8:12 data-bind-for
|
|
21
|
+
void model.errorMessage; // bootstrap-text-input.tpl.html:11:10 data-bind-text
|
|
22
|
+
void model.hideError; // bootstrap-text-input.tpl.html:12:10 data-bind-prop-hidden
|
|
23
|
+
void model.errorId; // bootstrap-text-input.tpl.html:13:10 data-bind-prop-id
|
|
24
|
+
void model.description; // bootstrap-text-input.tpl.html:15:10 data-bind-text
|
|
25
|
+
void model.hideDescription; // bootstrap-text-input.tpl.html:16:10 data-bind-prop-hidden
|
|
26
|
+
void model.descriptionId; // bootstrap-text-input.tpl.html:17:10 data-bind-prop-id
|
|
27
|
+
}
|
|
28
|
+
void check;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type Converted } from 'asljs-observable';
|
|
2
|
+
import { ComponentModelDefinition, ComponentsTheme } from 'asljs-ui';
|
|
3
|
+
import { LitElement } from 'lit';
|
|
4
|
+
export type TextInputEnterKeyBehavior = 'finish' | 'newline';
|
|
5
|
+
export type TextInputValidator = (value: string) => string | null;
|
|
6
|
+
export interface TextInputChangeDetail {
|
|
7
|
+
value: string;
|
|
8
|
+
isEmpty: boolean;
|
|
9
|
+
isValid: boolean;
|
|
10
|
+
errorMessage: string | null;
|
|
11
|
+
dirty: boolean;
|
|
12
|
+
}
|
|
13
|
+
export type TextInputStatus = Converted<{
|
|
14
|
+
draftValue: string;
|
|
15
|
+
isEmpty: boolean;
|
|
16
|
+
isValid: boolean;
|
|
17
|
+
errorMessage: string | null;
|
|
18
|
+
dirty: boolean;
|
|
19
|
+
}>;
|
|
20
|
+
export type TextInputTemplateModel = Converted<{
|
|
21
|
+
label: string;
|
|
22
|
+
description: string;
|
|
23
|
+
errorMessage: string;
|
|
24
|
+
hideLabel: boolean;
|
|
25
|
+
hideDescription: boolean;
|
|
26
|
+
hideError: boolean;
|
|
27
|
+
hasError: boolean;
|
|
28
|
+
isEmpty: boolean;
|
|
29
|
+
multiline: boolean;
|
|
30
|
+
inputId: string;
|
|
31
|
+
descriptionId: string;
|
|
32
|
+
errorId: string;
|
|
33
|
+
}>;
|
|
34
|
+
export declare const TextInputModelDefinition: ComponentModelDefinition;
|
|
35
|
+
export declare class TextInput extends LitElement {
|
|
36
|
+
#private;
|
|
37
|
+
readonly status: TextInputStatus;
|
|
38
|
+
accessor label: string | null;
|
|
39
|
+
accessor description: string | null;
|
|
40
|
+
accessor validator: TextInputValidator | null;
|
|
41
|
+
accessor theme: ComponentsTheme | null;
|
|
42
|
+
accessor value: string | null;
|
|
43
|
+
accessor placeholder: string | null;
|
|
44
|
+
accessor inputType: string;
|
|
45
|
+
accessor controlClassName: string;
|
|
46
|
+
accessor multiline: boolean;
|
|
47
|
+
accessor autoExtend: boolean;
|
|
48
|
+
accessor autoExtendMaxRows: number | null;
|
|
49
|
+
accessor enterKeyBehavior: TextInputEnterKeyBehavior;
|
|
50
|
+
accessor disabled: boolean;
|
|
51
|
+
accessor rows: number;
|
|
52
|
+
get draftValue(): string;
|
|
53
|
+
get isEmpty(): boolean;
|
|
54
|
+
get isValid(): boolean;
|
|
55
|
+
get errorMessage(): string | null;
|
|
56
|
+
createRenderRoot(): this;
|
|
57
|
+
connectedCallback(): void;
|
|
58
|
+
disconnectedCallback(): void;
|
|
59
|
+
render(): ReturnType<LitElement['render']>;
|
|
60
|
+
updated(changedProperties: Map<PropertyKey, unknown>): void;
|
|
61
|
+
}
|
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { bindDataModel } from 'asljs-data-binding';
|
|
8
|
+
import { observable } from 'asljs-observable';
|
|
9
|
+
import { defineComponentModel, findThemeProvider, getDefaultTheme, resolveThemeTemplate, THEME_CHANGED_EVENT_NAME } from 'asljs-ui';
|
|
10
|
+
import { html, LitElement } from 'lit';
|
|
11
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
12
|
+
import { createTemplate } from './text-input.tpl.js';
|
|
13
|
+
export const TextInputModelDefinition = { name: 'TextInputModelDefinition',
|
|
14
|
+
title: 'Text Input',
|
|
15
|
+
properties: [{ name: 'label',
|
|
16
|
+
title: 'Label',
|
|
17
|
+
type: 'string' },
|
|
18
|
+
{ name: 'description',
|
|
19
|
+
title: 'Description',
|
|
20
|
+
type: 'string' },
|
|
21
|
+
{ name: 'validator',
|
|
22
|
+
title: 'Validator',
|
|
23
|
+
type: 'function',
|
|
24
|
+
description: 'Validation function that returns an error message or null.' },
|
|
25
|
+
{ name: 'theme',
|
|
26
|
+
title: 'Theme',
|
|
27
|
+
type: 'object',
|
|
28
|
+
description: 'Per-instance components theme override.' },
|
|
29
|
+
{ name: 'value',
|
|
30
|
+
title: 'Value',
|
|
31
|
+
type: 'string' },
|
|
32
|
+
{ name: 'placeholder',
|
|
33
|
+
title: 'Placeholder',
|
|
34
|
+
type: 'string' },
|
|
35
|
+
{ name: 'inputType',
|
|
36
|
+
title: 'Input type',
|
|
37
|
+
type: 'string' },
|
|
38
|
+
{ name: 'controlClassName',
|
|
39
|
+
title: 'Control class name',
|
|
40
|
+
type: 'string' },
|
|
41
|
+
{ name: 'multiline',
|
|
42
|
+
title: 'Multiline',
|
|
43
|
+
type: 'boolean' },
|
|
44
|
+
{ name: 'autoExtend',
|
|
45
|
+
title: 'Auto extend',
|
|
46
|
+
type: 'boolean' },
|
|
47
|
+
{ name: 'autoExtendMaxRows',
|
|
48
|
+
title: 'Auto extend max rows',
|
|
49
|
+
type: 'number',
|
|
50
|
+
description: 'Maximum rows when auto extend is enabled.' },
|
|
51
|
+
{ name: 'enterKeyBehavior',
|
|
52
|
+
title: 'Enter key behavior',
|
|
53
|
+
type: 'string' },
|
|
54
|
+
{ name: 'disabled',
|
|
55
|
+
title: 'Disabled',
|
|
56
|
+
type: 'boolean' },
|
|
57
|
+
{ name: 'rows',
|
|
58
|
+
title: 'Rows',
|
|
59
|
+
type: 'number' },
|
|
60
|
+
{ name: 'status',
|
|
61
|
+
title: 'Status',
|
|
62
|
+
type: 'object',
|
|
63
|
+
description: 'Observable draft status object.',
|
|
64
|
+
editable: false },
|
|
65
|
+
{ name: 'draftValue',
|
|
66
|
+
title: 'Draft value',
|
|
67
|
+
type: 'string',
|
|
68
|
+
description: 'Current in-progress text.',
|
|
69
|
+
editable: false },
|
|
70
|
+
{ name: 'isEmpty',
|
|
71
|
+
title: 'Is empty',
|
|
72
|
+
type: 'boolean',
|
|
73
|
+
editable: false },
|
|
74
|
+
{ name: 'isValid',
|
|
75
|
+
title: 'Is valid',
|
|
76
|
+
type: 'boolean',
|
|
77
|
+
editable: false },
|
|
78
|
+
{ name: 'errorMessage',
|
|
79
|
+
title: 'Error message',
|
|
80
|
+
type: 'string',
|
|
81
|
+
description: 'Current validation message or null.',
|
|
82
|
+
editable: false }] };
|
|
83
|
+
defineComponentModel('asljs-text-input', TextInputModelDefinition);
|
|
84
|
+
let TextInput = class TextInput extends LitElement {
|
|
85
|
+
constructor() {
|
|
86
|
+
super(...arguments);
|
|
87
|
+
this.#templateElement = null;
|
|
88
|
+
this.#inputTemplateElement = null;
|
|
89
|
+
this.#textareaTemplateElement = null;
|
|
90
|
+
this.#templateDispose = null;
|
|
91
|
+
this.#controlTemplateDispose = null;
|
|
92
|
+
this.#themeProvider = null;
|
|
93
|
+
this.#control = null;
|
|
94
|
+
this.#controlBaseClassName = '';
|
|
95
|
+
this.#controlInvalidClassName = null;
|
|
96
|
+
this.#controlInputListener = null;
|
|
97
|
+
this.#controlBlurListener = null;
|
|
98
|
+
this.#controlKeydownListener = null;
|
|
99
|
+
this.#skipNextBlur = false;
|
|
100
|
+
this.#idBase = `asljs-text-input-${nextTextInputId++}`;
|
|
101
|
+
this.#model = observable({ label: '',
|
|
102
|
+
description: '',
|
|
103
|
+
errorMessage: '',
|
|
104
|
+
hideLabel: true,
|
|
105
|
+
hideDescription: true,
|
|
106
|
+
hideError: true,
|
|
107
|
+
hasError: false,
|
|
108
|
+
isEmpty: true,
|
|
109
|
+
multiline: false,
|
|
110
|
+
inputId: `${this.#idBase}-control`,
|
|
111
|
+
descriptionId: `${this.#idBase}-description`,
|
|
112
|
+
errorId: `${this.#idBase}-error` });
|
|
113
|
+
this.status = observable({ draftValue: '',
|
|
114
|
+
isEmpty: true,
|
|
115
|
+
isValid: true,
|
|
116
|
+
errorMessage: null,
|
|
117
|
+
dirty: false });
|
|
118
|
+
this.#label_accessor_storage = null;
|
|
119
|
+
this.#description_accessor_storage = null;
|
|
120
|
+
this.#validator_accessor_storage = null;
|
|
121
|
+
this.#theme_accessor_storage = null;
|
|
122
|
+
this.#value_accessor_storage = '';
|
|
123
|
+
this.#placeholder_accessor_storage = null;
|
|
124
|
+
this.#inputType_accessor_storage = 'text';
|
|
125
|
+
this.#controlClassName_accessor_storage = '';
|
|
126
|
+
this.#multiline_accessor_storage = false;
|
|
127
|
+
this.#autoExtend_accessor_storage = false;
|
|
128
|
+
this.#autoExtendMaxRows_accessor_storage = null;
|
|
129
|
+
this.#enterKeyBehavior_accessor_storage = 'finish';
|
|
130
|
+
this.#disabled_accessor_storage = false;
|
|
131
|
+
this.#rows_accessor_storage = 3;
|
|
132
|
+
this.#handleThemeChanged = () => {
|
|
133
|
+
this.#renderTemplate();
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
#templateElement;
|
|
137
|
+
#inputTemplateElement;
|
|
138
|
+
#textareaTemplateElement;
|
|
139
|
+
#templateDispose;
|
|
140
|
+
#controlTemplateDispose;
|
|
141
|
+
#themeProvider;
|
|
142
|
+
#control;
|
|
143
|
+
#controlBaseClassName;
|
|
144
|
+
#controlInvalidClassName;
|
|
145
|
+
#controlInputListener;
|
|
146
|
+
#controlBlurListener;
|
|
147
|
+
#controlKeydownListener;
|
|
148
|
+
#skipNextBlur;
|
|
149
|
+
#idBase;
|
|
150
|
+
#model;
|
|
151
|
+
#label_accessor_storage;
|
|
152
|
+
get label() { return this.#label_accessor_storage; }
|
|
153
|
+
set label(value) { this.#label_accessor_storage = value; }
|
|
154
|
+
#description_accessor_storage;
|
|
155
|
+
get description() { return this.#description_accessor_storage; }
|
|
156
|
+
set description(value) { this.#description_accessor_storage = value; }
|
|
157
|
+
#validator_accessor_storage;
|
|
158
|
+
get validator() { return this.#validator_accessor_storage; }
|
|
159
|
+
set validator(value) { this.#validator_accessor_storage = value; }
|
|
160
|
+
#theme_accessor_storage;
|
|
161
|
+
get theme() { return this.#theme_accessor_storage; }
|
|
162
|
+
set theme(value) { this.#theme_accessor_storage = value; }
|
|
163
|
+
#value_accessor_storage;
|
|
164
|
+
get value() { return this.#value_accessor_storage; }
|
|
165
|
+
set value(value) { this.#value_accessor_storage = value; }
|
|
166
|
+
#placeholder_accessor_storage;
|
|
167
|
+
get placeholder() { return this.#placeholder_accessor_storage; }
|
|
168
|
+
set placeholder(value) { this.#placeholder_accessor_storage = value; }
|
|
169
|
+
#inputType_accessor_storage;
|
|
170
|
+
get inputType() { return this.#inputType_accessor_storage; }
|
|
171
|
+
set inputType(value) { this.#inputType_accessor_storage = value; }
|
|
172
|
+
#controlClassName_accessor_storage;
|
|
173
|
+
get controlClassName() { return this.#controlClassName_accessor_storage; }
|
|
174
|
+
set controlClassName(value) { this.#controlClassName_accessor_storage = value; }
|
|
175
|
+
#multiline_accessor_storage;
|
|
176
|
+
get multiline() { return this.#multiline_accessor_storage; }
|
|
177
|
+
set multiline(value) { this.#multiline_accessor_storage = value; }
|
|
178
|
+
#autoExtend_accessor_storage;
|
|
179
|
+
get autoExtend() { return this.#autoExtend_accessor_storage; }
|
|
180
|
+
set autoExtend(value) { this.#autoExtend_accessor_storage = value; }
|
|
181
|
+
#autoExtendMaxRows_accessor_storage;
|
|
182
|
+
get autoExtendMaxRows() { return this.#autoExtendMaxRows_accessor_storage; }
|
|
183
|
+
set autoExtendMaxRows(value) { this.#autoExtendMaxRows_accessor_storage = value; }
|
|
184
|
+
#enterKeyBehavior_accessor_storage;
|
|
185
|
+
get enterKeyBehavior() { return this.#enterKeyBehavior_accessor_storage; }
|
|
186
|
+
set enterKeyBehavior(value) { this.#enterKeyBehavior_accessor_storage = value; }
|
|
187
|
+
#disabled_accessor_storage;
|
|
188
|
+
get disabled() { return this.#disabled_accessor_storage; }
|
|
189
|
+
set disabled(value) { this.#disabled_accessor_storage = value; }
|
|
190
|
+
#rows_accessor_storage;
|
|
191
|
+
get rows() { return this.#rows_accessor_storage; }
|
|
192
|
+
set rows(value) { this.#rows_accessor_storage = value; }
|
|
193
|
+
get draftValue() {
|
|
194
|
+
return this.status.draftValue;
|
|
195
|
+
}
|
|
196
|
+
get isEmpty() {
|
|
197
|
+
return this.status.isEmpty;
|
|
198
|
+
}
|
|
199
|
+
get isValid() {
|
|
200
|
+
return this.status.isValid;
|
|
201
|
+
}
|
|
202
|
+
get errorMessage() {
|
|
203
|
+
return this.status.errorMessage;
|
|
204
|
+
}
|
|
205
|
+
createRenderRoot() {
|
|
206
|
+
return this;
|
|
207
|
+
}
|
|
208
|
+
connectedCallback() {
|
|
209
|
+
super.connectedCallback();
|
|
210
|
+
this.#captureTemplates();
|
|
211
|
+
this.#syncThemeProvider();
|
|
212
|
+
this.#applyExternalValue();
|
|
213
|
+
}
|
|
214
|
+
disconnectedCallback() {
|
|
215
|
+
this.#disposeControlBindings();
|
|
216
|
+
this.#templateDispose?.();
|
|
217
|
+
this.#templateDispose = null;
|
|
218
|
+
this.#controlTemplateDispose?.();
|
|
219
|
+
this.#controlTemplateDispose = null;
|
|
220
|
+
this.#disposeThemeProvider();
|
|
221
|
+
super.disconnectedCallback();
|
|
222
|
+
}
|
|
223
|
+
render() {
|
|
224
|
+
return html `
|
|
225
|
+
<div data-role="template-host"></div>
|
|
226
|
+
`;
|
|
227
|
+
}
|
|
228
|
+
updated(changedProperties) {
|
|
229
|
+
if (changedProperties.has('theme')) {
|
|
230
|
+
this.#syncThemeProvider();
|
|
231
|
+
}
|
|
232
|
+
if (changedProperties.has('value')) {
|
|
233
|
+
this.#applyExternalValue();
|
|
234
|
+
}
|
|
235
|
+
if (changedProperties.has('label')
|
|
236
|
+
|| changedProperties.has('description')
|
|
237
|
+
|| changedProperties.has('multiline')
|
|
238
|
+
|| changedProperties.has('theme')) {
|
|
239
|
+
this.#renderTemplate();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (changedProperties.has('placeholder')
|
|
243
|
+
|| changedProperties.has('inputType')
|
|
244
|
+
|| changedProperties.has('controlClassName')
|
|
245
|
+
|| changedProperties.has('disabled')
|
|
246
|
+
|| changedProperties.has('rows')
|
|
247
|
+
|| changedProperties.has('autoExtend')
|
|
248
|
+
|| changedProperties.has('autoExtendMaxRows')
|
|
249
|
+
|| changedProperties.has('enterKeyBehavior')) {
|
|
250
|
+
this.#syncControlState();
|
|
251
|
+
}
|
|
252
|
+
this.#syncModelState();
|
|
253
|
+
}
|
|
254
|
+
#captureTemplates() {
|
|
255
|
+
this.#templateElement = null;
|
|
256
|
+
this.#inputTemplateElement = null;
|
|
257
|
+
this.#textareaTemplateElement = null;
|
|
258
|
+
this.#templateElement =
|
|
259
|
+
cloneNamedTemplate(this, 'template');
|
|
260
|
+
this.#inputTemplateElement =
|
|
261
|
+
cloneNamedTemplate(this, 'input');
|
|
262
|
+
this.#textareaTemplateElement =
|
|
263
|
+
cloneNamedTemplate(this, 'textarea');
|
|
264
|
+
}
|
|
265
|
+
#syncThemeProvider() {
|
|
266
|
+
this.#disposeThemeProvider();
|
|
267
|
+
this.#themeProvider =
|
|
268
|
+
findThemeProvider(this);
|
|
269
|
+
if (this.#themeProvider === null) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
this.#themeProvider.addEventListener(THEME_CHANGED_EVENT_NAME, this.#handleThemeChanged);
|
|
273
|
+
}
|
|
274
|
+
#disposeThemeProvider() {
|
|
275
|
+
this.#themeProvider?.removeEventListener(THEME_CHANGED_EVENT_NAME, this.#handleThemeChanged);
|
|
276
|
+
this.#themeProvider = null;
|
|
277
|
+
}
|
|
278
|
+
#handleThemeChanged;
|
|
279
|
+
#applyExternalValue() {
|
|
280
|
+
const nextValue = normalizeText(this.value);
|
|
281
|
+
this.status.draftValue = nextValue;
|
|
282
|
+
this.#syncModelState();
|
|
283
|
+
this.#syncControlState();
|
|
284
|
+
}
|
|
285
|
+
#syncModelState() {
|
|
286
|
+
const label = normalizeOptionalText(this.label);
|
|
287
|
+
const description = normalizeOptionalText(this.description);
|
|
288
|
+
let errorMessage = null;
|
|
289
|
+
const validator = this.validator;
|
|
290
|
+
if (validator !== null) {
|
|
291
|
+
errorMessage =
|
|
292
|
+
validator(this.status.draftValue);
|
|
293
|
+
}
|
|
294
|
+
const isEmpty = this.status.draftValue === '';
|
|
295
|
+
const dirty = this.status.draftValue !== normalizeText(this.value);
|
|
296
|
+
this.status.isEmpty = isEmpty;
|
|
297
|
+
this.status.errorMessage = errorMessage;
|
|
298
|
+
this.status.isValid = errorMessage === null;
|
|
299
|
+
this.status.dirty = dirty;
|
|
300
|
+
this.#model.label = label ?? '';
|
|
301
|
+
this.#model.description = description ?? '';
|
|
302
|
+
this.#model.errorMessage = errorMessage ?? '';
|
|
303
|
+
this.#model.hideLabel = label === null;
|
|
304
|
+
this.#model.hideDescription = description === null;
|
|
305
|
+
this.#model.hideError = errorMessage === null;
|
|
306
|
+
this.#model.hasError = errorMessage !== null;
|
|
307
|
+
this.#model.isEmpty = isEmpty;
|
|
308
|
+
this.#model.multiline = this.multiline;
|
|
309
|
+
this.#syncControlState();
|
|
310
|
+
}
|
|
311
|
+
#renderTemplate() {
|
|
312
|
+
const templateHost = this.querySelector('[data-role="template-host"]');
|
|
313
|
+
if (templateHost === null) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
this.#templateDispose?.();
|
|
317
|
+
this.#templateDispose = null;
|
|
318
|
+
this.#controlTemplateDispose?.();
|
|
319
|
+
this.#controlTemplateDispose = null;
|
|
320
|
+
this.#disposeControlBindings();
|
|
321
|
+
const template = this.#resolveTemplate('template');
|
|
322
|
+
if (template === null) {
|
|
323
|
+
templateHost.replaceChildren();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const fragment = template.content.cloneNode(true);
|
|
327
|
+
this.#templateDispose =
|
|
328
|
+
bindDataModel(fragment, this.#model);
|
|
329
|
+
templateHost.replaceChildren(fragment);
|
|
330
|
+
this.#mountControl();
|
|
331
|
+
this.#syncModelState();
|
|
332
|
+
}
|
|
333
|
+
#resolveTemplate(slotName) {
|
|
334
|
+
return this.#getLocalTemplate(slotName)
|
|
335
|
+
?? this.#getThemeTemplate(slotName)
|
|
336
|
+
?? createDefaultTextInputTemplate();
|
|
337
|
+
}
|
|
338
|
+
#getLocalTemplate(slotName) {
|
|
339
|
+
switch (slotName) {
|
|
340
|
+
case 'template':
|
|
341
|
+
return this.#templateElement;
|
|
342
|
+
case 'input':
|
|
343
|
+
return this.#inputTemplateElement;
|
|
344
|
+
case 'textarea':
|
|
345
|
+
return this.#textareaTemplateElement;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
#getThemeTemplate(slotName) {
|
|
349
|
+
const theme = this.theme
|
|
350
|
+
?? this.#themeProvider?.theme
|
|
351
|
+
?? getDefaultTheme();
|
|
352
|
+
return resolveThemeTemplate(slotName === 'template'
|
|
353
|
+
? theme.textInput?.template
|
|
354
|
+
: slotName === 'input'
|
|
355
|
+
? theme.textInput?.input
|
|
356
|
+
: theme.textInput?.textarea, this);
|
|
357
|
+
}
|
|
358
|
+
#mountControl() {
|
|
359
|
+
const controlHost = this.querySelector('[data-role="control-host"]');
|
|
360
|
+
if (controlHost === null) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const mountedControl = this.#createMountedControl(controlHost);
|
|
364
|
+
const control = mountedControl.control;
|
|
365
|
+
this.#controlTemplateDispose =
|
|
366
|
+
bindDataModel(mountedControl.fragment, this.#model);
|
|
367
|
+
controlHost.replaceChildren(mountedControl.fragment);
|
|
368
|
+
this.#control = control;
|
|
369
|
+
this.#controlBaseClassName =
|
|
370
|
+
mountedControl.className;
|
|
371
|
+
this.#controlInvalidClassName =
|
|
372
|
+
mountedControl.invalidClassName;
|
|
373
|
+
const inputListener = () => {
|
|
374
|
+
this.status.draftValue = control.value;
|
|
375
|
+
this.#syncModelState();
|
|
376
|
+
this.#dispatchValueEvent('input');
|
|
377
|
+
};
|
|
378
|
+
const blurListener = () => {
|
|
379
|
+
if (this.#skipNextBlur) {
|
|
380
|
+
this.#skipNextBlur = false;
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
this.#dispatchValueEvent('change');
|
|
384
|
+
};
|
|
385
|
+
const keydownListener = (event) => {
|
|
386
|
+
const keyboardEvent = event;
|
|
387
|
+
if (keyboardEvent.key !== 'Enter') {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (this.multiline
|
|
391
|
+
&& !keyboardEvent.ctrlKey
|
|
392
|
+
&& !keyboardEvent.metaKey
|
|
393
|
+
&& this.enterKeyBehavior
|
|
394
|
+
=== 'newline') {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
keyboardEvent.preventDefault();
|
|
398
|
+
this.#skipNextBlur = true;
|
|
399
|
+
this.#dispatchValueEvent('change');
|
|
400
|
+
control.blur();
|
|
401
|
+
};
|
|
402
|
+
control.addEventListener('input', inputListener);
|
|
403
|
+
control.addEventListener('blur', blurListener);
|
|
404
|
+
control.addEventListener('keydown', keydownListener);
|
|
405
|
+
this.#controlInputListener =
|
|
406
|
+
() => {
|
|
407
|
+
control.removeEventListener('input', inputListener);
|
|
408
|
+
};
|
|
409
|
+
this.#controlBlurListener =
|
|
410
|
+
() => {
|
|
411
|
+
control.removeEventListener('blur', blurListener);
|
|
412
|
+
};
|
|
413
|
+
this.#controlKeydownListener =
|
|
414
|
+
() => {
|
|
415
|
+
control.removeEventListener('keydown', keydownListener);
|
|
416
|
+
};
|
|
417
|
+
this.#syncControlState();
|
|
418
|
+
}
|
|
419
|
+
#syncControlState() {
|
|
420
|
+
const control = this.#control;
|
|
421
|
+
if (control === null) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (control.value
|
|
425
|
+
!== this.status.draftValue) {
|
|
426
|
+
control.value =
|
|
427
|
+
this.status.draftValue;
|
|
428
|
+
}
|
|
429
|
+
control.id = this.#model.inputId;
|
|
430
|
+
control.placeholder =
|
|
431
|
+
normalizeOptionalText(this.placeholder) ?? '';
|
|
432
|
+
control.disabled = this.disabled;
|
|
433
|
+
control.className =
|
|
434
|
+
joinClassNames(this.#controlBaseClassName, this.controlClassName);
|
|
435
|
+
const invalidClassName = this.#controlInvalidClassName;
|
|
436
|
+
if (control instanceof HTMLInputElement) {
|
|
437
|
+
control.type = this.inputType;
|
|
438
|
+
}
|
|
439
|
+
if (invalidClassName !== null
|
|
440
|
+
&& !this.status.isValid) {
|
|
441
|
+
control.classList.add(invalidClassName);
|
|
442
|
+
}
|
|
443
|
+
control.toggleAttribute('aria-invalid', !this.status.isValid);
|
|
444
|
+
control.setAttribute('aria-describedby', resolveAriaDescribedBy(this.#model));
|
|
445
|
+
if (this.multiline
|
|
446
|
+
&& control instanceof HTMLTextAreaElement) {
|
|
447
|
+
control.rows =
|
|
448
|
+
Math.max(1, this.rows);
|
|
449
|
+
this.#syncAutoExtend(control);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
control.style.height = '';
|
|
453
|
+
control.style.overflowY = '';
|
|
454
|
+
}
|
|
455
|
+
#createMountedControl(controlHost) {
|
|
456
|
+
const slotName = this.multiline
|
|
457
|
+
? 'textarea'
|
|
458
|
+
: 'input';
|
|
459
|
+
const template = this.#resolveTemplate(slotName);
|
|
460
|
+
if (template === null) {
|
|
461
|
+
return createFallbackMountedControl(this.multiline, controlHost);
|
|
462
|
+
}
|
|
463
|
+
const fragment = template.content.cloneNode(true);
|
|
464
|
+
const control = this.multiline
|
|
465
|
+
? fragment.querySelector('textarea')
|
|
466
|
+
: fragment.querySelector('input');
|
|
467
|
+
if (control === null) {
|
|
468
|
+
return createFallbackMountedControl(this.multiline, controlHost);
|
|
469
|
+
}
|
|
470
|
+
return { fragment,
|
|
471
|
+
control,
|
|
472
|
+
className: resolveInitialControlClassName(control, controlHost),
|
|
473
|
+
invalidClassName: resolveInitialControlInvalidClassName(control, controlHost) };
|
|
474
|
+
}
|
|
475
|
+
#syncAutoExtend(control) {
|
|
476
|
+
if (!this.autoExtend) {
|
|
477
|
+
control.style.height = '';
|
|
478
|
+
control.style.overflowY = '';
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
control.style.height = 'auto';
|
|
482
|
+
const maxHeight = resolveMaxHeight(control, this.autoExtendMaxRows);
|
|
483
|
+
const nextHeight = maxHeight === null
|
|
484
|
+
? control.scrollHeight
|
|
485
|
+
: Math.min(control.scrollHeight, maxHeight);
|
|
486
|
+
control.style.height = `${nextHeight}px`;
|
|
487
|
+
control.style.overflowY =
|
|
488
|
+
maxHeight !== null && control.scrollHeight > maxHeight
|
|
489
|
+
? 'auto'
|
|
490
|
+
: 'hidden';
|
|
491
|
+
}
|
|
492
|
+
#dispatchValueEvent(name) {
|
|
493
|
+
const detail = { value: this.status.draftValue,
|
|
494
|
+
isEmpty: this.status.isEmpty,
|
|
495
|
+
isValid: this.status.isValid,
|
|
496
|
+
errorMessage: this.status.errorMessage,
|
|
497
|
+
dirty: this.status.dirty };
|
|
498
|
+
this.dispatchEvent(new CustomEvent(name, { detail,
|
|
499
|
+
bubbles: true,
|
|
500
|
+
composed: true }));
|
|
501
|
+
}
|
|
502
|
+
#disposeControlBindings() {
|
|
503
|
+
this.#controlInputListener?.();
|
|
504
|
+
this.#controlBlurListener?.();
|
|
505
|
+
this.#controlKeydownListener?.();
|
|
506
|
+
this.#controlInputListener = null;
|
|
507
|
+
this.#controlBlurListener = null;
|
|
508
|
+
this.#controlKeydownListener = null;
|
|
509
|
+
this.#control = null;
|
|
510
|
+
this.#controlBaseClassName = '';
|
|
511
|
+
this.#controlInvalidClassName = null;
|
|
512
|
+
this.#controlTemplateDispose?.();
|
|
513
|
+
this.#controlTemplateDispose = null;
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
__decorate([
|
|
517
|
+
property({ attribute: false })
|
|
518
|
+
], TextInput.prototype, "label", null);
|
|
519
|
+
__decorate([
|
|
520
|
+
property({ attribute: false })
|
|
521
|
+
], TextInput.prototype, "description", null);
|
|
522
|
+
__decorate([
|
|
523
|
+
property({ attribute: false })
|
|
524
|
+
], TextInput.prototype, "validator", null);
|
|
525
|
+
__decorate([
|
|
526
|
+
property({ attribute: false })
|
|
527
|
+
], TextInput.prototype, "theme", null);
|
|
528
|
+
__decorate([
|
|
529
|
+
property({ attribute: false })
|
|
530
|
+
], TextInput.prototype, "value", null);
|
|
531
|
+
__decorate([
|
|
532
|
+
property({ attribute: false })
|
|
533
|
+
], TextInput.prototype, "placeholder", null);
|
|
534
|
+
__decorate([
|
|
535
|
+
property({ attribute: false })
|
|
536
|
+
], TextInput.prototype, "inputType", null);
|
|
537
|
+
__decorate([
|
|
538
|
+
property({ attribute: false })
|
|
539
|
+
], TextInput.prototype, "controlClassName", null);
|
|
540
|
+
__decorate([
|
|
541
|
+
property({ attribute: false })
|
|
542
|
+
], TextInput.prototype, "multiline", null);
|
|
543
|
+
__decorate([
|
|
544
|
+
property({ attribute: false })
|
|
545
|
+
], TextInput.prototype, "autoExtend", null);
|
|
546
|
+
__decorate([
|
|
547
|
+
property({ attribute: false })
|
|
548
|
+
], TextInput.prototype, "autoExtendMaxRows", null);
|
|
549
|
+
__decorate([
|
|
550
|
+
property({ attribute: false })
|
|
551
|
+
], TextInput.prototype, "enterKeyBehavior", null);
|
|
552
|
+
__decorate([
|
|
553
|
+
property({ attribute: false })
|
|
554
|
+
], TextInput.prototype, "disabled", null);
|
|
555
|
+
__decorate([
|
|
556
|
+
property({ attribute: false })
|
|
557
|
+
], TextInput.prototype, "rows", null);
|
|
558
|
+
TextInput = __decorate([
|
|
559
|
+
customElement('asljs-text-input')
|
|
560
|
+
], TextInput);
|
|
561
|
+
export { TextInput };
|
|
562
|
+
let nextTextInputId = 1;
|
|
563
|
+
function normalizeText(value) {
|
|
564
|
+
return value ?? '';
|
|
565
|
+
}
|
|
566
|
+
function normalizeOptionalText(value) {
|
|
567
|
+
if (value === null
|
|
568
|
+
|| value === undefined
|
|
569
|
+
|| value === '') {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
return value;
|
|
573
|
+
}
|
|
574
|
+
function resolveAriaDescribedBy(model) {
|
|
575
|
+
const ids = [];
|
|
576
|
+
if (!model.hideDescription) {
|
|
577
|
+
ids.push(model.descriptionId);
|
|
578
|
+
}
|
|
579
|
+
if (!model.hideError) {
|
|
580
|
+
ids.push(model.errorId);
|
|
581
|
+
}
|
|
582
|
+
return ids.join(' ');
|
|
583
|
+
}
|
|
584
|
+
function resolveMaxHeight(control, maxRows) {
|
|
585
|
+
if (maxRows === null
|
|
586
|
+
|| maxRows <= 0) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
const computed = getComputedStyle(control);
|
|
590
|
+
const lineHeight = parseFloat(computed.lineHeight);
|
|
591
|
+
if (!Number.isFinite(lineHeight)
|
|
592
|
+
|| lineHeight <= 0) {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
const borderTop = parseFloat(computed.borderTopWidth) || 0;
|
|
596
|
+
const borderBottom = parseFloat(computed.borderBottomWidth) || 0;
|
|
597
|
+
const paddingTop = parseFloat(computed.paddingTop) || 0;
|
|
598
|
+
const paddingBottom = parseFloat(computed.paddingBottom) || 0;
|
|
599
|
+
return lineHeight * maxRows
|
|
600
|
+
+ paddingTop
|
|
601
|
+
+ paddingBottom
|
|
602
|
+
+ borderTop
|
|
603
|
+
+ borderBottom;
|
|
604
|
+
}
|
|
605
|
+
function cloneNamedTemplate(host, slotName) {
|
|
606
|
+
const element = host.querySelector(`template[data-slot="${slotName}"]`);
|
|
607
|
+
if (element === null) {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
const templateElement = element;
|
|
611
|
+
const clonedTemplate = document.createElement('template');
|
|
612
|
+
clonedTemplate.content.append(templateElement.content.cloneNode(true));
|
|
613
|
+
return clonedTemplate;
|
|
614
|
+
}
|
|
615
|
+
function resolveInitialControlClassName(control, controlHost) {
|
|
616
|
+
return control.className
|
|
617
|
+
|| controlHost.getAttribute('data-control-class')
|
|
618
|
+
|| '';
|
|
619
|
+
}
|
|
620
|
+
function resolveInitialControlInvalidClassName(control, controlHost) {
|
|
621
|
+
return control.getAttribute('data-control-invalid-class')
|
|
622
|
+
?? controlHost.getAttribute('data-control-invalid-class')
|
|
623
|
+
?? null;
|
|
624
|
+
}
|
|
625
|
+
function createDefaultTextInputTemplate() {
|
|
626
|
+
return createTemplate();
|
|
627
|
+
}
|
|
628
|
+
function createSingleLineInput() {
|
|
629
|
+
const input = document.createElement('input');
|
|
630
|
+
input.type = 'text';
|
|
631
|
+
return input;
|
|
632
|
+
}
|
|
633
|
+
function createDefaultInputTemplate() {
|
|
634
|
+
const template = document.createElement('template');
|
|
635
|
+
template.innerHTML =
|
|
636
|
+
'<input type="text">';
|
|
637
|
+
return template;
|
|
638
|
+
}
|
|
639
|
+
function createDefaultTextareaTemplate() {
|
|
640
|
+
const template = document.createElement('template');
|
|
641
|
+
template.innerHTML =
|
|
642
|
+
'<textarea></textarea>';
|
|
643
|
+
return template;
|
|
644
|
+
}
|
|
645
|
+
function createFallbackMountedControl(multiline, controlHost) {
|
|
646
|
+
const fragment = document.createDocumentFragment();
|
|
647
|
+
const template = multiline
|
|
648
|
+
? createDefaultTextareaTemplate()
|
|
649
|
+
: createDefaultInputTemplate();
|
|
650
|
+
const templateFragment = template.content.cloneNode(true);
|
|
651
|
+
const control = multiline
|
|
652
|
+
? templateFragment.querySelector('textarea')
|
|
653
|
+
: templateFragment.querySelector('input');
|
|
654
|
+
if (control === null) {
|
|
655
|
+
const fallbackControl = multiline
|
|
656
|
+
? document.createElement('textarea')
|
|
657
|
+
: createSingleLineInput();
|
|
658
|
+
fragment.append(fallbackControl);
|
|
659
|
+
return { fragment,
|
|
660
|
+
control: fallbackControl,
|
|
661
|
+
className: resolveInitialControlClassName(fallbackControl, controlHost),
|
|
662
|
+
invalidClassName: resolveInitialControlInvalidClassName(fallbackControl, controlHost) };
|
|
663
|
+
}
|
|
664
|
+
fragment.append(templateFragment);
|
|
665
|
+
return { fragment,
|
|
666
|
+
control,
|
|
667
|
+
className: resolveInitialControlClassName(control, controlHost),
|
|
668
|
+
invalidClassName: resolveInitialControlInvalidClassName(control, controlHost) };
|
|
669
|
+
}
|
|
670
|
+
function joinClassNames(...classNames) {
|
|
671
|
+
return classNames
|
|
672
|
+
.flatMap(className => className.split(/\s+/))
|
|
673
|
+
.map(className => className.trim())
|
|
674
|
+
.filter(className => className !== '')
|
|
675
|
+
.join(' ');
|
|
676
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type BindDataModelOptions } from "asljs-data-binding";
|
|
2
|
+
import type { TextInputTemplateModel as TemplateModel } from "./text-input.js";
|
|
3
|
+
/** The markup inside the template's <template> element. */
|
|
4
|
+
export declare const html = "\n <div>\n <label\n data-bind-text=\"label\"\n data-bind-prop-hidden=\"hideLabel\"\n data-bind-for=\"inputId\"></label>\n <div data-role=\"control-host\"></div>\n <div\n data-bind-text=\"description\"\n data-bind-prop-hidden=\"hideDescription\"\n data-bind-prop-id=\"descriptionId\"></div>\n <div\n data-bind-text=\"errorMessage\"\n data-bind-prop-hidden=\"hideError\"\n data-bind-prop-id=\"errorId\"></div>\n </div>\n";
|
|
5
|
+
export declare function createTemplate(doc?: Document): HTMLTemplateElement;
|
|
6
|
+
export declare function bindTemplate(root: ParentNode, model: TemplateModel, options?: BindDataModelOptions): () => boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Generated by data-bind-compile from text-input.tpl.html. Do not edit.
|
|
2
|
+
import { bindDataModel } from "asljs-data-binding";
|
|
3
|
+
/** The markup inside the template's <template> element. */
|
|
4
|
+
export const html = "\n <div>\n <label\n data-bind-text=\"label\"\n data-bind-prop-hidden=\"hideLabel\"\n data-bind-for=\"inputId\"></label>\n <div data-role=\"control-host\"></div>\n <div\n data-bind-text=\"description\"\n data-bind-prop-hidden=\"hideDescription\"\n data-bind-prop-id=\"descriptionId\"></div>\n <div\n data-bind-text=\"errorMessage\"\n data-bind-prop-hidden=\"hideError\"\n data-bind-prop-id=\"errorId\"></div>\n </div>\n";
|
|
5
|
+
export function createTemplate(doc = document) {
|
|
6
|
+
const template = doc.createElement('template');
|
|
7
|
+
template.innerHTML = html;
|
|
8
|
+
return template;
|
|
9
|
+
}
|
|
10
|
+
export function bindTemplate(root, model, options) {
|
|
11
|
+
return bindDataModel(root, model, options);
|
|
12
|
+
}
|
|
13
|
+
// Never called: each statement checks one binding against the model and
|
|
14
|
+
// element types, and names the template line it comes from.
|
|
15
|
+
function check(model) {
|
|
16
|
+
void model.label; // text-input.tpl.html:4:12 data-bind-text
|
|
17
|
+
void model.hideLabel; // text-input.tpl.html:5:12 data-bind-prop-hidden
|
|
18
|
+
void model.inputId; // text-input.tpl.html:6:12 data-bind-for
|
|
19
|
+
void model.description; // text-input.tpl.html:9:10 data-bind-text
|
|
20
|
+
void model.hideDescription; // text-input.tpl.html:10:10 data-bind-prop-hidden
|
|
21
|
+
void model.descriptionId; // text-input.tpl.html:11:10 data-bind-prop-id
|
|
22
|
+
void model.errorMessage; // text-input.tpl.html:13:10 data-bind-text
|
|
23
|
+
void model.hideError; // text-input.tpl.html:14:10 data-bind-prop-hidden
|
|
24
|
+
void model.errorId; // text-input.tpl.html:15:10 data-bind-prop-id
|
|
25
|
+
}
|
|
26
|
+
void check;
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ComponentsTheme, type ThemeTemplateValue } from 'asljs-ui';
|
|
2
|
+
export interface TextInputThemeDefinition {
|
|
3
|
+
template?: ThemeTemplateValue;
|
|
4
|
+
input?: ThemeTemplateValue;
|
|
5
|
+
textarea?: ThemeTemplateValue;
|
|
6
|
+
}
|
|
7
|
+
declare module 'asljs-ui' {
|
|
8
|
+
interface ComponentsTheme {
|
|
9
|
+
textInput?: TextInputThemeDefinition;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** The Bootstrap text input section. */
|
|
13
|
+
export declare function createBootstrapTextInputTheme(): ComponentsTheme;
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { html as textInputTemplate } from './bootstrap-text-input.tpl.js';
|
|
2
|
+
/** The Bootstrap text input section. */
|
|
3
|
+
export function createBootstrapTextInputTheme() {
|
|
4
|
+
return { textInput: { template: textInputTemplate,
|
|
5
|
+
input: `
|
|
6
|
+
<input type="text"
|
|
7
|
+
class="form-control"
|
|
8
|
+
data-control-invalid-class="is-invalid">
|
|
9
|
+
`,
|
|
10
|
+
textarea: `
|
|
11
|
+
<textarea class="form-control"
|
|
12
|
+
data-control-invalid-class="is-invalid"></textarea>
|
|
13
|
+
` } };
|
|
14
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Text Input
|
|
2
|
+
|
|
3
|
+
`asljs-text-input` is a labelled text field or textarea with validation.
|
|
4
|
+
|
|
5
|
+
## Properties
|
|
6
|
+
|
|
7
|
+
- `value` — the external set/reset value
|
|
8
|
+
- `label`
|
|
9
|
+
- `description`
|
|
10
|
+
- `placeholder`
|
|
11
|
+
- `validator` — returns an error message or `null`
|
|
12
|
+
- `multiline` — renders a textarea instead of an input
|
|
13
|
+
- `enterKeyBehavior` — `finish` or `newline`
|
|
14
|
+
- `autoExtend` and `autoExtendMaxRows` — textarea growth
|
|
15
|
+
- `theme`
|
|
16
|
+
- local `template[data-slot="template"]`, `template[data-slot="input"]` and
|
|
17
|
+
`template[data-slot="textarea"]`
|
|
18
|
+
|
|
19
|
+
`value` is a set/reset input, not the live draft. User edits are kept in
|
|
20
|
+
`draftValue`, reflected in the observable `status` object, and emitted through
|
|
21
|
+
`input` and `change` events whose detail reports the draft value, validity and
|
|
22
|
+
dirty state. Listen for those events and decide whether to persist or reset.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import 'asljs-ui-text-input';
|
|
28
|
+
|
|
29
|
+
const textInput =
|
|
30
|
+
document.createElement('asljs-text-input') as HTMLElement & {
|
|
31
|
+
label: string | null;
|
|
32
|
+
description: string | null;
|
|
33
|
+
value: string | null;
|
|
34
|
+
placeholder: string | null;
|
|
35
|
+
validator: ((value: string) => string | null) | null;
|
|
36
|
+
multiline: boolean;
|
|
37
|
+
enterKeyBehavior: 'finish' | 'newline';
|
|
38
|
+
autoExtend: boolean;
|
|
39
|
+
autoExtendMaxRows: number | null;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
textInput.label = 'Summary';
|
|
43
|
+
textInput.description = 'Ctrl+Enter always finishes editing.';
|
|
44
|
+
textInput.placeholder = 'Write a short summary';
|
|
45
|
+
textInput.multiline = true;
|
|
46
|
+
textInput.enterKeyBehavior = 'newline';
|
|
47
|
+
textInput.autoExtend = true;
|
|
48
|
+
textInput.autoExtendMaxRows = 8;
|
|
49
|
+
textInput.validator =
|
|
50
|
+
value => value.trim() === ''
|
|
51
|
+
? 'Summary is required.'
|
|
52
|
+
: null;
|
|
53
|
+
textInput.value = 'Initial text';
|
|
54
|
+
|
|
55
|
+
textInput.addEventListener(
|
|
56
|
+
'change',
|
|
57
|
+
event => {
|
|
58
|
+
const detail =
|
|
59
|
+
(event as CustomEvent<{
|
|
60
|
+
value: string;
|
|
61
|
+
isValid: boolean;
|
|
62
|
+
}>).detail;
|
|
63
|
+
|
|
64
|
+
console.log(detail.value, detail.isValid);
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Template overrides
|
|
69
|
+
|
|
70
|
+
Provide a local `template[data-slot="template"]` when the layout must differ
|
|
71
|
+
from the Bootstrap-oriented default. The template can use any supported
|
|
72
|
+
`asljs-data-binding` attributes, but it must include
|
|
73
|
+
`[data-role="control-host"]` so the component can mount the actual `input` or
|
|
74
|
+
`textarea`.
|
|
75
|
+
|
|
76
|
+
```html
|
|
77
|
+
<asljs-text-input>
|
|
78
|
+
<template data-slot="template">
|
|
79
|
+
<div class="editor-field">
|
|
80
|
+
<div data-role="control-host"></div>
|
|
81
|
+
<small data-bind-text="description"
|
|
82
|
+
data-bind-prop-hidden="hideDescription"></small>
|
|
83
|
+
<div class="error"
|
|
84
|
+
data-bind-text="errorMessage"
|
|
85
|
+
data-bind-prop-hidden="hideError"></div>
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
88
|
+
</asljs-text-input>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Provide `template[data-slot="input"]` or `template[data-slot="textarea"]` when
|
|
92
|
+
the native control markup itself must come from the local component or theme.
|
|
93
|
+
The slot must include the matching native control element, but wrapper markup
|
|
94
|
+
around it is allowed. Control templates can also use `asljs-data-binding`
|
|
95
|
+
attributes, for example to place validation feedback next to the real control.
|
|
96
|
+
|
|
97
|
+
```html
|
|
98
|
+
<asljs-text-input>
|
|
99
|
+
<template data-slot="input">
|
|
100
|
+
<div class="field-shell">
|
|
101
|
+
<input class="field-control"
|
|
102
|
+
data-control-invalid-class="field-control-invalid">
|
|
103
|
+
</div>
|
|
104
|
+
</template>
|
|
105
|
+
</asljs-text-input>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Themes can supply the same templates, in the `textInput` section;
|
|
109
|
+
`createBootstrapTextInputTheme()` supplies Bootstrap form markup. See
|
|
110
|
+
[Theming][THM].
|
|
111
|
+
|
|
112
|
+
[THM]: ../../core/docs/Theming.md
|
package/package.json
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "asljs-ui-text-input",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "A text field or textarea web component with validation.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"asljs": {
|
|
7
|
+
"type": "lib"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist/**",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE.md",
|
|
21
|
+
"docs/**"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"flint": "toolkit flint",
|
|
25
|
+
"clean": "toolkit clean",
|
|
26
|
+
"build": "npm run codegen --if-present && tsc -p tsconfig.build.json",
|
|
27
|
+
"build:dist": "npm run codegen --if-present && tsc -p tsconfig.dist.json",
|
|
28
|
+
"typecheck": "npm run codegen --if-present && tsc -p tsconfig.build.json --noEmit",
|
|
29
|
+
"guard:clean-git": "toolkit ensure-clean-working-directory",
|
|
30
|
+
"prepack": "npm run clean && npm run build:dist && npm run build && npm run test && npm run guard:clean-git",
|
|
31
|
+
"postpublish": "toolkit tag-release-revision",
|
|
32
|
+
"test": "node --test --enable-source-maps \"build/**/*.test.js\"",
|
|
33
|
+
"test:watch": "node --watch --test \"build/**/*.test.js\"",
|
|
34
|
+
"coverage": "npm run build && node --test --experimental-test-coverage --enable-source-maps --test-coverage-exclude=\"**/*.test.js\" \"build/**/*.test.js\"",
|
|
35
|
+
"release:patch": "toolkit release-patch",
|
|
36
|
+
"all": "npm run clean && npm run build:dist && npm run flint && npm run typecheck && npm run build && npm run test",
|
|
37
|
+
"codegen": "data-bind-compile"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"web-components",
|
|
41
|
+
"lit",
|
|
42
|
+
"ui",
|
|
43
|
+
"input",
|
|
44
|
+
"form"
|
|
45
|
+
],
|
|
46
|
+
"author": "\"Alex Netkachov\" <alex.netkachov@gmail.com>",
|
|
47
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
48
|
+
"homepage": "https://github.com/AlexandriteSoftware/asljs#readme",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/AlexandriteSoftware/asljs/issues"
|
|
51
|
+
},
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/AlexandriteSoftware/asljs.git"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"asljs-data-binding": "^0.2.5",
|
|
58
|
+
"asljs-observable": "^0.5.5",
|
|
59
|
+
"asljs-ui": "^0.1.1",
|
|
60
|
+
"lit": "^3.3.1"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"asljs-testing": "^0.1.1",
|
|
64
|
+
"asljs-toolkit": "^0.1.0"
|
|
65
|
+
}
|
|
66
|
+
}
|