@api-client/ui 0.5.6 → 0.5.8
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/.cursor/rules/html-and-css-best-practices.mdc +63 -0
- package/.cursor/rules/lit-best-practices.mdc +78 -0
- package/.github/instructions/html-and-css-best-practices.instructions.md +70 -0
- package/.github/instructions/lit-best-practices.instructions.md +86 -0
- package/build/src/elements/currency/currency-picker.d.ts +10 -0
- package/build/src/elements/currency/currency-picker.d.ts.map +1 -0
- package/build/src/elements/currency/currency-picker.js +27 -0
- package/build/src/elements/currency/currency-picker.js.map +1 -0
- package/build/src/elements/currency/internals/Picker.d.ts +311 -0
- package/build/src/elements/currency/internals/Picker.d.ts.map +1 -0
- package/build/src/elements/currency/internals/Picker.js +857 -0
- package/build/src/elements/currency/internals/Picker.js.map +1 -0
- package/build/src/elements/currency/internals/Picker.styles.d.ts +3 -0
- package/build/src/elements/currency/internals/Picker.styles.d.ts.map +1 -0
- package/build/src/elements/currency/internals/Picker.styles.js +58 -0
- package/build/src/elements/currency/internals/Picker.styles.js.map +1 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.d.ts +216 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.d.ts.map +1 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.js +1037 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.js.map +1 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.styles.d.ts +3 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.styles.d.ts.map +1 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.styles.js +274 -0
- package/build/src/elements/mention-textarea/internals/MentionTextArea.styles.js.map +1 -0
- package/build/src/elements/mention-textarea/ui-mention-textarea.d.ts +13 -0
- package/build/src/elements/mention-textarea/ui-mention-textarea.d.ts.map +1 -0
- package/build/src/elements/mention-textarea/ui-mention-textarea.js +28 -0
- package/build/src/elements/mention-textarea/ui-mention-textarea.js.map +1 -0
- package/build/src/md/button/internals/base.d.ts +1 -0
- package/build/src/md/button/internals/base.d.ts.map +1 -1
- package/build/src/md/button/internals/base.js +7 -0
- package/build/src/md/button/internals/base.js.map +1 -1
- package/build/src/md/chip/internals/Chip.styles.d.ts.map +1 -1
- package/build/src/md/chip/internals/Chip.styles.js +2 -0
- package/build/src/md/chip/internals/Chip.styles.js.map +1 -1
- package/build/src/md/date-picker/internals/DatePicker.styles.d.ts.map +1 -1
- package/build/src/md/date-picker/internals/DatePicker.styles.js +73 -0
- package/build/src/md/date-picker/internals/DatePicker.styles.js.map +1 -1
- package/build/src/md/date-picker/internals/DatePickerCalendar.d.ts +164 -51
- package/build/src/md/date-picker/internals/DatePickerCalendar.d.ts.map +1 -1
- package/build/src/md/date-picker/internals/DatePickerCalendar.js +660 -368
- package/build/src/md/date-picker/internals/DatePickerCalendar.js.map +1 -1
- package/build/src/md/date-picker/ui-date-picker-input.d.ts +65 -13
- package/build/src/md/date-picker/ui-date-picker-input.d.ts.map +1 -1
- package/build/src/md/date-picker/ui-date-picker-input.js +143 -76
- package/build/src/md/date-picker/ui-date-picker-input.js.map +1 -1
- package/build/src/md/date-picker/ui-date-picker-modal-input.d.ts +76 -17
- package/build/src/md/date-picker/ui-date-picker-modal-input.d.ts.map +1 -1
- package/build/src/md/date-picker/ui-date-picker-modal-input.js +192 -127
- package/build/src/md/date-picker/ui-date-picker-modal-input.js.map +1 -1
- package/build/src/md/date-picker/ui-date-picker-modal.d.ts +63 -15
- package/build/src/md/date-picker/ui-date-picker-modal.d.ts.map +1 -1
- package/build/src/md/date-picker/ui-date-picker-modal.js +143 -64
- package/build/src/md/date-picker/ui-date-picker-modal.js.map +1 -1
- package/demo/elements/currency/index.html +91 -0
- package/demo/elements/currency/index.ts +272 -0
- package/demo/elements/index.html +6 -0
- package/demo/elements/mention-textarea/index.html +19 -0
- package/demo/elements/mention-textarea/index.ts +205 -0
- package/demo/md/date-picker/date-picker.ts +138 -103
- package/package.json +2 -2
- package/src/elements/currency/currency-picker.ts +14 -0
- package/src/elements/currency/internals/Picker.styles.ts +58 -0
- package/src/elements/currency/internals/Picker.ts +846 -0
- package/src/elements/mention-textarea/internals/MentionTextArea.styles.ts +274 -0
- package/src/elements/mention-textarea/internals/MentionTextArea.ts +1036 -0
- package/src/elements/mention-textarea/ui-mention-textarea.ts +18 -0
- package/src/md/button/internals/base.ts +7 -0
- package/src/md/chip/internals/Chip.styles.ts +2 -0
- package/src/md/date-picker/internals/DatePicker.styles.ts +73 -0
- package/src/md/date-picker/internals/DatePickerCalendar.ts +643 -309
- package/src/md/date-picker/ui-date-picker-input.ts +110 -49
- package/src/md/date-picker/ui-date-picker-modal-input.ts +168 -99
- package/src/md/date-picker/ui-date-picker-modal.ts +136 -53
- package/test/README.md +3 -2
- package/test/elements/currency/CurrencyPicker.accessibility.test.ts +328 -0
- package/test/elements/currency/CurrencyPicker.core.test.ts +318 -0
- package/test/elements/currency/CurrencyPicker.integration.test.ts +482 -0
- package/test/elements/currency/CurrencyPicker.test.ts +486 -0
- package/test/elements/mention-textarea/MentionTextArea.basic.test.ts +63 -0
- package/test/elements/mention-textarea/MentionTextArea.test.ts +321 -0
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,1037 @@
|
|
|
1
|
+
import { __esDecorate, __runInitializers } from "tslib";
|
|
2
|
+
import { html, LitElement, nothing } from 'lit';
|
|
3
|
+
import { property, query, state } from 'lit/decorators.js';
|
|
4
|
+
import { classMap } from 'lit/directives/class-map.js';
|
|
5
|
+
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
6
|
+
import reactive from '../../../decorators/reactive.js';
|
|
7
|
+
import '../../../md/chip/ui-chip.js';
|
|
8
|
+
/**
|
|
9
|
+
* A NodeFilter mask that includes text nodes and elements.
|
|
10
|
+
* It is used with the NodeWalker
|
|
11
|
+
*/
|
|
12
|
+
const ShowMask = NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT;
|
|
13
|
+
let MentionTextArea = (() => {
|
|
14
|
+
let _classSuper = LitElement;
|
|
15
|
+
let _instanceExtraInitializers = [];
|
|
16
|
+
let _label_decorators;
|
|
17
|
+
let _label_initializers = [];
|
|
18
|
+
let _label_extraInitializers = [];
|
|
19
|
+
let _supportingText_decorators;
|
|
20
|
+
let _supportingText_initializers = [];
|
|
21
|
+
let _supportingText_extraInitializers = [];
|
|
22
|
+
let _disabled_decorators;
|
|
23
|
+
let _disabled_initializers = [];
|
|
24
|
+
let _disabled_extraInitializers = [];
|
|
25
|
+
let _invalid_decorators;
|
|
26
|
+
let _invalid_initializers = [];
|
|
27
|
+
let _invalid_extraInitializers = [];
|
|
28
|
+
let _name_decorators;
|
|
29
|
+
let _name_initializers = [];
|
|
30
|
+
let _name_extraInitializers = [];
|
|
31
|
+
let _required_decorators;
|
|
32
|
+
let _required_initializers = [];
|
|
33
|
+
let _required_extraInitializers = [];
|
|
34
|
+
let _placeholder_decorators;
|
|
35
|
+
let _placeholder_initializers = [];
|
|
36
|
+
let _placeholder_extraInitializers = [];
|
|
37
|
+
let _set_value_decorators;
|
|
38
|
+
let _suggestions_decorators;
|
|
39
|
+
let _suggestions_initializers = [];
|
|
40
|
+
let _suggestions_extraInitializers = [];
|
|
41
|
+
let _mentionTrigger_decorators;
|
|
42
|
+
let _mentionTrigger_initializers = [];
|
|
43
|
+
let _mentionTrigger_extraInitializers = [];
|
|
44
|
+
let _minQueryLength_decorators;
|
|
45
|
+
let _minQueryLength_initializers = [];
|
|
46
|
+
let _minQueryLength_extraInitializers = [];
|
|
47
|
+
let _editorElement_decorators;
|
|
48
|
+
let _editorElement_initializers = [];
|
|
49
|
+
let _editorElement_extraInitializers = [];
|
|
50
|
+
let _suggestionsPopover_decorators;
|
|
51
|
+
let _suggestionsPopover_initializers = [];
|
|
52
|
+
let _suggestionsPopover_extraInitializers = [];
|
|
53
|
+
let _isShowingSuggestions_decorators;
|
|
54
|
+
let _isShowingSuggestions_initializers = [];
|
|
55
|
+
let _isShowingSuggestions_extraInitializers = [];
|
|
56
|
+
let _filteredSuggestions_decorators;
|
|
57
|
+
let _filteredSuggestions_initializers = [];
|
|
58
|
+
let _filteredSuggestions_extraInitializers = [];
|
|
59
|
+
let _selectedSuggestionIndex_decorators;
|
|
60
|
+
let _selectedSuggestionIndex_initializers = [];
|
|
61
|
+
let _selectedSuggestionIndex_extraInitializers = [];
|
|
62
|
+
let _hasContent_decorators;
|
|
63
|
+
let _hasContent_initializers = [];
|
|
64
|
+
let _hasContent_extraInitializers = [];
|
|
65
|
+
let _isLabelFloating_decorators;
|
|
66
|
+
let _isLabelFloating_initializers = [];
|
|
67
|
+
let _isLabelFloating_extraInitializers = [];
|
|
68
|
+
let _isEditorFocus_decorators;
|
|
69
|
+
let _isEditorFocus_initializers = [];
|
|
70
|
+
let _isEditorFocus_extraInitializers = [];
|
|
71
|
+
return class MentionTextArea extends _classSuper {
|
|
72
|
+
static {
|
|
73
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
74
|
+
_label_decorators = [property({ type: String })];
|
|
75
|
+
_supportingText_decorators = [property({ type: String, attribute: 'supporting-text' })];
|
|
76
|
+
_disabled_decorators = [property({ type: Boolean, reflect: true })];
|
|
77
|
+
_invalid_decorators = [property({ type: Boolean, reflect: true })];
|
|
78
|
+
_name_decorators = [property({ type: String })];
|
|
79
|
+
_required_decorators = [property({ type: Boolean })];
|
|
80
|
+
_placeholder_decorators = [property({ type: String })];
|
|
81
|
+
_set_value_decorators = [property({ type: String })];
|
|
82
|
+
_suggestions_decorators = [property({ type: Array, attribute: false })];
|
|
83
|
+
_mentionTrigger_decorators = [property({ type: String, attribute: 'mention-trigger' })];
|
|
84
|
+
_minQueryLength_decorators = [property({ type: Number, attribute: 'min-query-length' })];
|
|
85
|
+
_editorElement_decorators = [query('.editor')];
|
|
86
|
+
_suggestionsPopover_decorators = [query('.suggestions-popover')];
|
|
87
|
+
_isShowingSuggestions_decorators = [state(), reactive()];
|
|
88
|
+
_filteredSuggestions_decorators = [reactive()];
|
|
89
|
+
_selectedSuggestionIndex_decorators = [state()];
|
|
90
|
+
_hasContent_decorators = [state()];
|
|
91
|
+
_isLabelFloating_decorators = [state()];
|
|
92
|
+
_isEditorFocus_decorators = [state()];
|
|
93
|
+
__esDecorate(this, null, _label_decorators, { kind: "accessor", name: "label", static: false, private: false, access: { has: obj => "label" in obj, get: obj => obj.label, set: (obj, value) => { obj.label = value; } }, metadata: _metadata }, _label_initializers, _label_extraInitializers);
|
|
94
|
+
__esDecorate(this, null, _supportingText_decorators, { kind: "accessor", name: "supportingText", static: false, private: false, access: { has: obj => "supportingText" in obj, get: obj => obj.supportingText, set: (obj, value) => { obj.supportingText = value; } }, metadata: _metadata }, _supportingText_initializers, _supportingText_extraInitializers);
|
|
95
|
+
__esDecorate(this, null, _disabled_decorators, { kind: "accessor", name: "disabled", static: false, private: false, access: { has: obj => "disabled" in obj, get: obj => obj.disabled, set: (obj, value) => { obj.disabled = value; } }, metadata: _metadata }, _disabled_initializers, _disabled_extraInitializers);
|
|
96
|
+
__esDecorate(this, null, _invalid_decorators, { kind: "accessor", name: "invalid", static: false, private: false, access: { has: obj => "invalid" in obj, get: obj => obj.invalid, set: (obj, value) => { obj.invalid = value; } }, metadata: _metadata }, _invalid_initializers, _invalid_extraInitializers);
|
|
97
|
+
__esDecorate(this, null, _name_decorators, { kind: "accessor", name: "name", static: false, private: false, access: { has: obj => "name" in obj, get: obj => obj.name, set: (obj, value) => { obj.name = value; } }, metadata: _metadata }, _name_initializers, _name_extraInitializers);
|
|
98
|
+
__esDecorate(this, null, _required_decorators, { kind: "accessor", name: "required", static: false, private: false, access: { has: obj => "required" in obj, get: obj => obj.required, set: (obj, value) => { obj.required = value; } }, metadata: _metadata }, _required_initializers, _required_extraInitializers);
|
|
99
|
+
__esDecorate(this, null, _placeholder_decorators, { kind: "accessor", name: "placeholder", static: false, private: false, access: { has: obj => "placeholder" in obj, get: obj => obj.placeholder, set: (obj, value) => { obj.placeholder = value; } }, metadata: _metadata }, _placeholder_initializers, _placeholder_extraInitializers);
|
|
100
|
+
__esDecorate(this, null, _set_value_decorators, { kind: "setter", name: "value", static: false, private: false, access: { has: obj => "value" in obj, set: (obj, value) => { obj.value = value; } }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
101
|
+
__esDecorate(this, null, _suggestions_decorators, { kind: "accessor", name: "suggestions", static: false, private: false, access: { has: obj => "suggestions" in obj, get: obj => obj.suggestions, set: (obj, value) => { obj.suggestions = value; } }, metadata: _metadata }, _suggestions_initializers, _suggestions_extraInitializers);
|
|
102
|
+
__esDecorate(this, null, _mentionTrigger_decorators, { kind: "accessor", name: "mentionTrigger", static: false, private: false, access: { has: obj => "mentionTrigger" in obj, get: obj => obj.mentionTrigger, set: (obj, value) => { obj.mentionTrigger = value; } }, metadata: _metadata }, _mentionTrigger_initializers, _mentionTrigger_extraInitializers);
|
|
103
|
+
__esDecorate(this, null, _minQueryLength_decorators, { kind: "accessor", name: "minQueryLength", static: false, private: false, access: { has: obj => "minQueryLength" in obj, get: obj => obj.minQueryLength, set: (obj, value) => { obj.minQueryLength = value; } }, metadata: _metadata }, _minQueryLength_initializers, _minQueryLength_extraInitializers);
|
|
104
|
+
__esDecorate(this, null, _editorElement_decorators, { kind: "accessor", name: "editorElement", static: false, private: false, access: { has: obj => "editorElement" in obj, get: obj => obj.editorElement, set: (obj, value) => { obj.editorElement = value; } }, metadata: _metadata }, _editorElement_initializers, _editorElement_extraInitializers);
|
|
105
|
+
__esDecorate(this, null, _suggestionsPopover_decorators, { kind: "accessor", name: "suggestionsPopover", static: false, private: false, access: { has: obj => "suggestionsPopover" in obj, get: obj => obj.suggestionsPopover, set: (obj, value) => { obj.suggestionsPopover = value; } }, metadata: _metadata }, _suggestionsPopover_initializers, _suggestionsPopover_extraInitializers);
|
|
106
|
+
__esDecorate(this, null, _isShowingSuggestions_decorators, { kind: "accessor", name: "isShowingSuggestions", static: false, private: false, access: { has: obj => "isShowingSuggestions" in obj, get: obj => obj.isShowingSuggestions, set: (obj, value) => { obj.isShowingSuggestions = value; } }, metadata: _metadata }, _isShowingSuggestions_initializers, _isShowingSuggestions_extraInitializers);
|
|
107
|
+
__esDecorate(this, null, _filteredSuggestions_decorators, { kind: "accessor", name: "filteredSuggestions", static: false, private: false, access: { has: obj => "filteredSuggestions" in obj, get: obj => obj.filteredSuggestions, set: (obj, value) => { obj.filteredSuggestions = value; } }, metadata: _metadata }, _filteredSuggestions_initializers, _filteredSuggestions_extraInitializers);
|
|
108
|
+
__esDecorate(this, null, _selectedSuggestionIndex_decorators, { kind: "accessor", name: "selectedSuggestionIndex", static: false, private: false, access: { has: obj => "selectedSuggestionIndex" in obj, get: obj => obj.selectedSuggestionIndex, set: (obj, value) => { obj.selectedSuggestionIndex = value; } }, metadata: _metadata }, _selectedSuggestionIndex_initializers, _selectedSuggestionIndex_extraInitializers);
|
|
109
|
+
__esDecorate(this, null, _hasContent_decorators, { kind: "accessor", name: "hasContent", static: false, private: false, access: { has: obj => "hasContent" in obj, get: obj => obj.hasContent, set: (obj, value) => { obj.hasContent = value; } }, metadata: _metadata }, _hasContent_initializers, _hasContent_extraInitializers);
|
|
110
|
+
__esDecorate(this, null, _isLabelFloating_decorators, { kind: "accessor", name: "isLabelFloating", static: false, private: false, access: { has: obj => "isLabelFloating" in obj, get: obj => obj.isLabelFloating, set: (obj, value) => { obj.isLabelFloating = value; } }, metadata: _metadata }, _isLabelFloating_initializers, _isLabelFloating_extraInitializers);
|
|
111
|
+
__esDecorate(this, null, _isEditorFocus_decorators, { kind: "accessor", name: "isEditorFocus", static: false, private: false, access: { has: obj => "isEditorFocus" in obj, get: obj => obj.isEditorFocus, set: (obj, value) => { obj.isEditorFocus = value; } }, metadata: _metadata }, _isEditorFocus_initializers, _isEditorFocus_extraInitializers);
|
|
112
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Shadow root configuration for the component.
|
|
116
|
+
* Uses 'open' mode for accessibility and delegates focus to enable proper focus management.
|
|
117
|
+
*/
|
|
118
|
+
static shadowRootOptions = {
|
|
119
|
+
mode: 'open',
|
|
120
|
+
delegatesFocus: true,
|
|
121
|
+
};
|
|
122
|
+
#label_accessor_storage = (__runInitializers(this, _instanceExtraInitializers), __runInitializers(this, _label_initializers, ''
|
|
123
|
+
/**
|
|
124
|
+
* Supporting text displayed below the input
|
|
125
|
+
*/
|
|
126
|
+
));
|
|
127
|
+
/**
|
|
128
|
+
* The label text displayed as placeholder/floating label
|
|
129
|
+
*/
|
|
130
|
+
get label() { return this.#label_accessor_storage; }
|
|
131
|
+
set label(value) { this.#label_accessor_storage = value; }
|
|
132
|
+
#supportingText_accessor_storage = (__runInitializers(this, _label_extraInitializers), __runInitializers(this, _supportingText_initializers, ''
|
|
133
|
+
/**
|
|
134
|
+
* Whether the component is disabled
|
|
135
|
+
*/
|
|
136
|
+
));
|
|
137
|
+
/**
|
|
138
|
+
* Supporting text displayed below the input
|
|
139
|
+
*/
|
|
140
|
+
get supportingText() { return this.#supportingText_accessor_storage; }
|
|
141
|
+
set supportingText(value) { this.#supportingText_accessor_storage = value; }
|
|
142
|
+
#disabled_accessor_storage = (__runInitializers(this, _supportingText_extraInitializers), __runInitializers(this, _disabled_initializers, false
|
|
143
|
+
/**
|
|
144
|
+
* Whether the component is in an invalid state
|
|
145
|
+
*/
|
|
146
|
+
));
|
|
147
|
+
/**
|
|
148
|
+
* Whether the component is disabled
|
|
149
|
+
*/
|
|
150
|
+
get disabled() { return this.#disabled_accessor_storage; }
|
|
151
|
+
set disabled(value) { this.#disabled_accessor_storage = value; }
|
|
152
|
+
#invalid_accessor_storage = (__runInitializers(this, _disabled_extraInitializers), __runInitializers(this, _invalid_initializers, false
|
|
153
|
+
/**
|
|
154
|
+
* The name attribute for form integration
|
|
155
|
+
*/
|
|
156
|
+
));
|
|
157
|
+
/**
|
|
158
|
+
* Whether the component is in an invalid state
|
|
159
|
+
*/
|
|
160
|
+
get invalid() { return this.#invalid_accessor_storage; }
|
|
161
|
+
set invalid(value) { this.#invalid_accessor_storage = value; }
|
|
162
|
+
#name_accessor_storage = (__runInitializers(this, _invalid_extraInitializers), __runInitializers(this, _name_initializers, ''
|
|
163
|
+
/**
|
|
164
|
+
* Whether the input is required
|
|
165
|
+
*/
|
|
166
|
+
));
|
|
167
|
+
/**
|
|
168
|
+
* The name attribute for form integration
|
|
169
|
+
*/
|
|
170
|
+
get name() { return this.#name_accessor_storage; }
|
|
171
|
+
set name(value) { this.#name_accessor_storage = value; }
|
|
172
|
+
#required_accessor_storage = (__runInitializers(this, _name_extraInitializers), __runInitializers(this, _required_initializers, false
|
|
173
|
+
/**
|
|
174
|
+
* Placeholder text shown when input is empty
|
|
175
|
+
*/
|
|
176
|
+
));
|
|
177
|
+
/**
|
|
178
|
+
* Whether the input is required
|
|
179
|
+
*/
|
|
180
|
+
get required() { return this.#required_accessor_storage; }
|
|
181
|
+
set required(value) { this.#required_accessor_storage = value; }
|
|
182
|
+
#placeholder_accessor_storage = (__runInitializers(this, _required_extraInitializers), __runInitializers(this, _placeholder_initializers, ''));
|
|
183
|
+
/**
|
|
184
|
+
* Placeholder text shown when input is empty
|
|
185
|
+
*/
|
|
186
|
+
get placeholder() { return this.#placeholder_accessor_storage; }
|
|
187
|
+
set placeholder(value) { this.#placeholder_accessor_storage = value; }
|
|
188
|
+
get value() {
|
|
189
|
+
return this._value;
|
|
190
|
+
}
|
|
191
|
+
set value(newValue) {
|
|
192
|
+
const oldValue = this._value;
|
|
193
|
+
if (newValue === this._value)
|
|
194
|
+
return; // No change, skip update
|
|
195
|
+
this._value = newValue;
|
|
196
|
+
this.requestUpdate('value', oldValue);
|
|
197
|
+
// Only sync editor from value when set externally and element is ready
|
|
198
|
+
if (this.editorElement) {
|
|
199
|
+
this.syncEditorFromValue();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
#suggestions_accessor_storage = (__runInitializers(this, _placeholder_extraInitializers), __runInitializers(this, _suggestions_initializers, []
|
|
203
|
+
/**
|
|
204
|
+
* Character that triggers mention suggestions (default: '@')
|
|
205
|
+
*/
|
|
206
|
+
));
|
|
207
|
+
/**
|
|
208
|
+
* Available suggestions for mentions
|
|
209
|
+
*/
|
|
210
|
+
get suggestions() { return this.#suggestions_accessor_storage; }
|
|
211
|
+
set suggestions(value) { this.#suggestions_accessor_storage = value; }
|
|
212
|
+
#mentionTrigger_accessor_storage = (__runInitializers(this, _suggestions_extraInitializers), __runInitializers(this, _mentionTrigger_initializers, '@'
|
|
213
|
+
/**
|
|
214
|
+
* Minimum characters after trigger to show suggestions
|
|
215
|
+
*/
|
|
216
|
+
));
|
|
217
|
+
/**
|
|
218
|
+
* Character that triggers mention suggestions (default: '@')
|
|
219
|
+
*/
|
|
220
|
+
get mentionTrigger() { return this.#mentionTrigger_accessor_storage; }
|
|
221
|
+
set mentionTrigger(value) { this.#mentionTrigger_accessor_storage = value; }
|
|
222
|
+
#minQueryLength_accessor_storage = (__runInitializers(this, _mentionTrigger_extraInitializers), __runInitializers(this, _minQueryLength_initializers, 0));
|
|
223
|
+
/**
|
|
224
|
+
* Minimum characters after trigger to show suggestions
|
|
225
|
+
*/
|
|
226
|
+
get minQueryLength() { return this.#minQueryLength_accessor_storage; }
|
|
227
|
+
set minQueryLength(value) { this.#minQueryLength_accessor_storage = value; }
|
|
228
|
+
#editorElement_accessor_storage = (__runInitializers(this, _minQueryLength_extraInitializers), __runInitializers(this, _editorElement_initializers, void 0));
|
|
229
|
+
get editorElement() { return this.#editorElement_accessor_storage; }
|
|
230
|
+
set editorElement(value) { this.#editorElement_accessor_storage = value; }
|
|
231
|
+
#suggestionsPopover_accessor_storage = (__runInitializers(this, _editorElement_extraInitializers), __runInitializers(this, _suggestionsPopover_initializers, void 0));
|
|
232
|
+
get suggestionsPopover() { return this.#suggestionsPopover_accessor_storage; }
|
|
233
|
+
set suggestionsPopover(value) { this.#suggestionsPopover_accessor_storage = value; }
|
|
234
|
+
#isShowingSuggestions_accessor_storage = (__runInitializers(this, _suggestionsPopover_extraInitializers), __runInitializers(this, _isShowingSuggestions_initializers, false));
|
|
235
|
+
get isShowingSuggestions() { return this.#isShowingSuggestions_accessor_storage; }
|
|
236
|
+
set isShowingSuggestions(value) { this.#isShowingSuggestions_accessor_storage = value; }
|
|
237
|
+
#filteredSuggestions_accessor_storage = (__runInitializers(this, _isShowingSuggestions_extraInitializers), __runInitializers(this, _filteredSuggestions_initializers, []));
|
|
238
|
+
get filteredSuggestions() { return this.#filteredSuggestions_accessor_storage; }
|
|
239
|
+
set filteredSuggestions(value) { this.#filteredSuggestions_accessor_storage = value; }
|
|
240
|
+
#selectedSuggestionIndex_accessor_storage = (__runInitializers(this, _filteredSuggestions_extraInitializers), __runInitializers(this, _selectedSuggestionIndex_initializers, 0));
|
|
241
|
+
get selectedSuggestionIndex() { return this.#selectedSuggestionIndex_accessor_storage; }
|
|
242
|
+
set selectedSuggestionIndex(value) { this.#selectedSuggestionIndex_accessor_storage = value; }
|
|
243
|
+
#hasContent_accessor_storage = (__runInitializers(this, _selectedSuggestionIndex_extraInitializers), __runInitializers(this, _hasContent_initializers, false));
|
|
244
|
+
get hasContent() { return this.#hasContent_accessor_storage; }
|
|
245
|
+
set hasContent(value) { this.#hasContent_accessor_storage = value; }
|
|
246
|
+
#isLabelFloating_accessor_storage = (__runInitializers(this, _hasContent_extraInitializers), __runInitializers(this, _isLabelFloating_initializers, false));
|
|
247
|
+
get isLabelFloating() { return this.#isLabelFloating_accessor_storage; }
|
|
248
|
+
set isLabelFloating(value) { this.#isLabelFloating_accessor_storage = value; }
|
|
249
|
+
#isEditorFocus_accessor_storage = (__runInitializers(this, _isLabelFloating_extraInitializers), __runInitializers(this, _isEditorFocus_initializers, false));
|
|
250
|
+
get isEditorFocus() { return this.#isEditorFocus_accessor_storage; }
|
|
251
|
+
set isEditorFocus(value) { this.#isEditorFocus_accessor_storage = value; }
|
|
252
|
+
currentMentionQuery = (__runInitializers(this, _isEditorFocus_extraInitializers), '');
|
|
253
|
+
currentMentionStart = -1;
|
|
254
|
+
mentionMap = new Map();
|
|
255
|
+
mutationObserver;
|
|
256
|
+
_value = ''; // Internal value storage
|
|
257
|
+
connectedCallback() {
|
|
258
|
+
super.connectedCallback();
|
|
259
|
+
// Setup mutation observer to watch for changes in the editor
|
|
260
|
+
this.mutationObserver = new MutationObserver(() => {
|
|
261
|
+
this.syncValueFromEditor();
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
disconnectedCallback() {
|
|
265
|
+
super.disconnectedCallback();
|
|
266
|
+
this.mutationObserver?.disconnect();
|
|
267
|
+
}
|
|
268
|
+
firstUpdated() {
|
|
269
|
+
// Start observing mutations in the editor
|
|
270
|
+
if (this.editorElement && this.mutationObserver) {
|
|
271
|
+
this.mutationObserver.observe(this.editorElement, {
|
|
272
|
+
childList: true,
|
|
273
|
+
subtree: true,
|
|
274
|
+
characterData: true,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
// Setup initial content
|
|
278
|
+
this.updateComplete.then(() => {
|
|
279
|
+
this.syncEditorFromValue();
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
willUpdate(changedProperties) {
|
|
283
|
+
super.willUpdate(changedProperties);
|
|
284
|
+
// No need to sync editor from value here since setter handles it
|
|
285
|
+
if (changedProperties.has('suggestions')) {
|
|
286
|
+
this.updateFilteredSuggestions();
|
|
287
|
+
}
|
|
288
|
+
if (changedProperties.has('selectedSuggestionIndex')) {
|
|
289
|
+
const index = this.selectedSuggestionIndex;
|
|
290
|
+
if (index >= 0) {
|
|
291
|
+
this.suggestionsPopover?.querySelector(`.suggestion-item:nth-child(${index + 1})`)?.scrollIntoView({
|
|
292
|
+
behavior: 'smooth',
|
|
293
|
+
block: 'nearest',
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Sets the caret position at the beginning of a specific text node
|
|
300
|
+
*/
|
|
301
|
+
setCaretPositionAtTextNode(textNode, offset = 0) {
|
|
302
|
+
const selection = window.getSelection();
|
|
303
|
+
if (!selection)
|
|
304
|
+
return;
|
|
305
|
+
const range = document.createRange();
|
|
306
|
+
range.setStart(textNode, offset);
|
|
307
|
+
range.setEnd(textNode, offset);
|
|
308
|
+
selection.removeAllRanges();
|
|
309
|
+
selection.addRange(range);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Synchronizes the value property from the editor content
|
|
313
|
+
*/
|
|
314
|
+
syncValueFromEditor() {
|
|
315
|
+
const newValue = this.getValueFromEditor();
|
|
316
|
+
if (newValue !== this._value) {
|
|
317
|
+
// Update internal value directly to avoid triggering setter
|
|
318
|
+
this._value = newValue;
|
|
319
|
+
this.requestUpdate('value');
|
|
320
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
321
|
+
}
|
|
322
|
+
this.updateContentState();
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Gets the value from editor content, converting chips to @{mention} format
|
|
326
|
+
*/
|
|
327
|
+
getValueFromEditor() {
|
|
328
|
+
const childNodes = Array.from(this.editorElement.childNodes);
|
|
329
|
+
let result = '';
|
|
330
|
+
for (const node of childNodes) {
|
|
331
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
332
|
+
result += node.textContent || '';
|
|
333
|
+
}
|
|
334
|
+
else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
335
|
+
const element = node;
|
|
336
|
+
if (element.classList.contains('mention-chip')) {
|
|
337
|
+
const mentionId = element.getAttribute('data-mention-id');
|
|
338
|
+
if (mentionId) {
|
|
339
|
+
result += `@{${mentionId}}`;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
result += element.textContent || '';
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Synchronizes the editor content from the value property
|
|
351
|
+
*/
|
|
352
|
+
syncEditorFromValue() {
|
|
353
|
+
if (!this.editorElement)
|
|
354
|
+
return;
|
|
355
|
+
const fragments = this.parseValueToFragments(this._value);
|
|
356
|
+
this.editorElement.innerHTML = '';
|
|
357
|
+
for (const fragment of fragments) {
|
|
358
|
+
if (fragment.type === 'text') {
|
|
359
|
+
this.editorElement.appendChild(document.createTextNode(fragment.content));
|
|
360
|
+
}
|
|
361
|
+
else if (fragment.type === 'mention') {
|
|
362
|
+
if (fragment.mentionId) {
|
|
363
|
+
let mention = this.mentionMap.get(fragment.mentionId);
|
|
364
|
+
// If mention not in map, try to find it in suggestions
|
|
365
|
+
if (!mention) {
|
|
366
|
+
mention = this.suggestions.find((s) => s.id === fragment.mentionId);
|
|
367
|
+
if (mention) {
|
|
368
|
+
// Add to map for future use
|
|
369
|
+
this.mentionMap.set(mention.id, mention);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (mention) {
|
|
373
|
+
const chipElement = this.createMentionChip(mention);
|
|
374
|
+
this.editorElement.appendChild(chipElement);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
// Fallback to text if mention not found anywhere
|
|
378
|
+
this.editorElement.appendChild(document.createTextNode(`@{${fragment.mentionId}}`));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
this.updateContentState();
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Parses value string into text and mention fragments
|
|
387
|
+
*/
|
|
388
|
+
parseValueToFragments(value) {
|
|
389
|
+
const fragments = [];
|
|
390
|
+
const mentionRegex = /@\{([^}]+)\}/g;
|
|
391
|
+
let lastIndex = 0;
|
|
392
|
+
let match;
|
|
393
|
+
while ((match = mentionRegex.exec(value)) !== null) {
|
|
394
|
+
// Add text before mention
|
|
395
|
+
if (match.index > lastIndex) {
|
|
396
|
+
fragments.push({
|
|
397
|
+
type: 'text',
|
|
398
|
+
content: value.substring(lastIndex, match.index),
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
// Add mention
|
|
402
|
+
fragments.push({
|
|
403
|
+
type: 'mention',
|
|
404
|
+
content: match[0],
|
|
405
|
+
mentionId: match[1],
|
|
406
|
+
});
|
|
407
|
+
lastIndex = mentionRegex.lastIndex;
|
|
408
|
+
}
|
|
409
|
+
// Add remaining text
|
|
410
|
+
if (lastIndex < value.length) {
|
|
411
|
+
fragments.push({
|
|
412
|
+
type: 'text',
|
|
413
|
+
content: value.substring(lastIndex),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return fragments;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Creates a mention chip element
|
|
420
|
+
*/
|
|
421
|
+
createMentionChip(mention) {
|
|
422
|
+
const wrapper = document.createElement('span');
|
|
423
|
+
wrapper.className = 'mention-chip';
|
|
424
|
+
wrapper.setAttribute('data-mention-id', mention.id);
|
|
425
|
+
wrapper.setAttribute('contenteditable', 'false');
|
|
426
|
+
const chip = document.createElement('ui-chip');
|
|
427
|
+
chip.setAttribute('type', 'input');
|
|
428
|
+
chip.setAttribute('removable', 'true');
|
|
429
|
+
chip.textContent = mention.label;
|
|
430
|
+
chip.addEventListener('remove', () => {
|
|
431
|
+
this.removeMention(wrapper, mention);
|
|
432
|
+
});
|
|
433
|
+
wrapper.appendChild(chip);
|
|
434
|
+
return wrapper;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Updates content state flags
|
|
438
|
+
*/
|
|
439
|
+
updateContentState() {
|
|
440
|
+
const hasAnyContent = this.editorElement.childNodes.length > 0 && (this.editorElement.textContent?.trim().length || 0) > 0;
|
|
441
|
+
this.hasContent = hasAnyContent;
|
|
442
|
+
this.isLabelFloating = hasAnyContent || this.isShowingSuggestions || this.isEditorFocus;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Handles input events in the editor
|
|
446
|
+
*/
|
|
447
|
+
handleEditorInput(event) {
|
|
448
|
+
event.stopPropagation();
|
|
449
|
+
// Check for mention trigger
|
|
450
|
+
this.checkForMentionTrigger();
|
|
451
|
+
// Sync value
|
|
452
|
+
this.syncValueFromEditor();
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Handles keydown events in the editor
|
|
456
|
+
*/
|
|
457
|
+
handleEditorKeyDown(event) {
|
|
458
|
+
if (this.isShowingSuggestions) {
|
|
459
|
+
this.handleSuggestionKeyDown(event);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Handles keydown events when suggestions are visible
|
|
464
|
+
*/
|
|
465
|
+
handleSuggestionKeyDown(event) {
|
|
466
|
+
switch (event.key) {
|
|
467
|
+
case 'ArrowUp':
|
|
468
|
+
event.preventDefault();
|
|
469
|
+
this.selectedSuggestionIndex = Math.max(0, this.selectedSuggestionIndex - 1);
|
|
470
|
+
break;
|
|
471
|
+
case 'ArrowDown':
|
|
472
|
+
event.preventDefault();
|
|
473
|
+
this.selectedSuggestionIndex = Math.min(this.filteredSuggestions.length - 1, this.selectedSuggestionIndex + 1);
|
|
474
|
+
break;
|
|
475
|
+
case 'Enter':
|
|
476
|
+
case 'Tab':
|
|
477
|
+
event.preventDefault();
|
|
478
|
+
this.selectSuggestion(this.filteredSuggestions[this.selectedSuggestionIndex]);
|
|
479
|
+
break;
|
|
480
|
+
case 'Escape':
|
|
481
|
+
event.preventDefault();
|
|
482
|
+
this.hideSuggestions();
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Handles focus events
|
|
488
|
+
*/
|
|
489
|
+
handleEditorFocus() {
|
|
490
|
+
this.isEditorFocus = true;
|
|
491
|
+
this.updateContentState();
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Handles blur events
|
|
495
|
+
*/
|
|
496
|
+
handleEditorBlur(event) {
|
|
497
|
+
// Don't hide suggestions if focus moved to suggestions popover
|
|
498
|
+
if (event.relatedTarget && this.suggestionsPopover?.contains(event.relatedTarget)) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
this.isEditorFocus = false;
|
|
502
|
+
this.hideSuggestions();
|
|
503
|
+
this.updateContentState();
|
|
504
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
505
|
+
}
|
|
506
|
+
handleEditorPaste(event) {
|
|
507
|
+
// Prevent default paste behavior to handle mentions properly
|
|
508
|
+
event.preventDefault();
|
|
509
|
+
const pastedContent = event.clipboardData?.getData('text/plain');
|
|
510
|
+
if (!pastedContent)
|
|
511
|
+
return;
|
|
512
|
+
const selection = window.getSelection();
|
|
513
|
+
if (!selection || selection.rangeCount === 0)
|
|
514
|
+
return;
|
|
515
|
+
// Get the current range
|
|
516
|
+
let range;
|
|
517
|
+
if ('getComposedRanges' in selection && typeof selection.getComposedRanges === 'function' && this.shadowRoot) {
|
|
518
|
+
const composedRanges = selection.getComposedRanges({ shadowRoots: [this.shadowRoot] });
|
|
519
|
+
if (composedRanges.length === 0)
|
|
520
|
+
return;
|
|
521
|
+
range = composedRanges[0];
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
range = selection.getRangeAt(0);
|
|
525
|
+
}
|
|
526
|
+
// Convert StaticRange to Range if needed for deleteContents
|
|
527
|
+
let workingRange;
|
|
528
|
+
if (range instanceof StaticRange) {
|
|
529
|
+
workingRange = document.createRange();
|
|
530
|
+
workingRange.setStart(range.startContainer, range.startOffset);
|
|
531
|
+
workingRange.setEnd(range.endContainer, range.endOffset);
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
workingRange = range;
|
|
535
|
+
}
|
|
536
|
+
// Delete the current selection (if any)
|
|
537
|
+
workingRange.deleteContents();
|
|
538
|
+
// Insert the pasted text at the current position
|
|
539
|
+
const textNode = document.createTextNode(pastedContent);
|
|
540
|
+
workingRange.insertNode(textNode);
|
|
541
|
+
// Position cursor at the end of the inserted text
|
|
542
|
+
workingRange.setStartAfter(textNode);
|
|
543
|
+
workingRange.setEndAfter(textNode);
|
|
544
|
+
selection.removeAllRanges();
|
|
545
|
+
selection.addRange(workingRange);
|
|
546
|
+
// Hide suggestions if showing
|
|
547
|
+
this.hideSuggestions();
|
|
548
|
+
// Sync value and trigger events
|
|
549
|
+
this.syncValueFromEditor();
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Checks if the current caret position indicates a mention trigger
|
|
553
|
+
*/
|
|
554
|
+
checkForMentionTrigger() {
|
|
555
|
+
const selection = window.getSelection();
|
|
556
|
+
if (!selection || selection.rangeCount === 0) {
|
|
557
|
+
this.hideSuggestions();
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
// Use getComposedRanges() for shadow DOM support, fallback to getRangeAt()
|
|
561
|
+
let range;
|
|
562
|
+
if ('getComposedRanges' in selection && typeof selection.getComposedRanges === 'function' && this.shadowRoot) {
|
|
563
|
+
const composedRanges = selection.getComposedRanges({ shadowRoots: [this.shadowRoot] });
|
|
564
|
+
if (composedRanges.length === 0) {
|
|
565
|
+
this.hideSuggestions();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
range = composedRanges[0];
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
range = selection.getRangeAt(0);
|
|
572
|
+
}
|
|
573
|
+
const caretContainer = range.startContainer;
|
|
574
|
+
const caretOffset = range.startOffset;
|
|
575
|
+
// Ensure the caret is within our editor element
|
|
576
|
+
if (!this.editorElement.contains(caretContainer)) {
|
|
577
|
+
this.hideSuggestions();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
// Only look for triggers in text nodes
|
|
581
|
+
if (caretContainer.nodeType !== Node.TEXT_NODE) {
|
|
582
|
+
this.hideSuggestions();
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const textContent = caretContainer.textContent || '';
|
|
586
|
+
// Look backwards from caret to find mention trigger
|
|
587
|
+
let mentionStart = -1;
|
|
588
|
+
for (let i = caretOffset - 1; i >= 0; i--) {
|
|
589
|
+
const char = textContent[i];
|
|
590
|
+
if (char === this.mentionTrigger) {
|
|
591
|
+
mentionStart = i;
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
if (char === ' ' || char === '\n') {
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (mentionStart >= 0) {
|
|
599
|
+
const query = textContent.substring(mentionStart + 1, caretOffset);
|
|
600
|
+
if (query.length >= this.minQueryLength) {
|
|
601
|
+
this.currentMentionQuery = query;
|
|
602
|
+
this.currentMentionStart = this.getGlobalTextPosition(caretContainer, mentionStart);
|
|
603
|
+
this.showSuggestions();
|
|
604
|
+
this.updateFilteredSuggestions();
|
|
605
|
+
}
|
|
606
|
+
else {
|
|
607
|
+
this.hideSuggestions();
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
this.hideSuggestions();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
walkerNodeFilter(node) {
|
|
615
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
616
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
617
|
+
}
|
|
618
|
+
else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
619
|
+
const element = node;
|
|
620
|
+
if (element.classList.contains('mention-chip')) {
|
|
621
|
+
// Accept mention chips as single characters
|
|
622
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
623
|
+
}
|
|
624
|
+
else if (element.parentElement?.classList.contains('mention-chip')) {
|
|
625
|
+
// It's a chip, ignore the entire tree
|
|
626
|
+
return NodeFilter.FILTER_REJECT;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return NodeFilter.FILTER_SKIP;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Gets the global text position within the editor for a position within a text node
|
|
633
|
+
*/
|
|
634
|
+
getGlobalTextPosition(textNode, offsetInNode) {
|
|
635
|
+
let position = 0;
|
|
636
|
+
const walker = document.createTreeWalker(this.editorElement, ShowMask, this.walkerNodeFilter);
|
|
637
|
+
let node = walker.nextNode();
|
|
638
|
+
while (node && node !== textNode) {
|
|
639
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
640
|
+
position += node.textContent?.length || 0;
|
|
641
|
+
}
|
|
642
|
+
else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
643
|
+
const element = node;
|
|
644
|
+
if (element.classList?.contains('mention-chip')) {
|
|
645
|
+
position += 1; // Count chip as 1 character
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
node = walker.nextNode();
|
|
649
|
+
}
|
|
650
|
+
return position + offsetInNode;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Finds the DOM node and offset that corresponds to a global text position
|
|
654
|
+
*/
|
|
655
|
+
findNodeAtGlobalPosition(globalPosition) {
|
|
656
|
+
let currentPosition = 0;
|
|
657
|
+
const walker = document.createTreeWalker(this.editorElement, ShowMask, this.walkerNodeFilter);
|
|
658
|
+
let node = walker.nextNode();
|
|
659
|
+
while (node) {
|
|
660
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
661
|
+
const textLength = node.textContent?.length || 0;
|
|
662
|
+
if (currentPosition + textLength >= globalPosition) {
|
|
663
|
+
return {
|
|
664
|
+
node,
|
|
665
|
+
offset: globalPosition - currentPosition,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
currentPosition += textLength;
|
|
669
|
+
}
|
|
670
|
+
else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
671
|
+
const element = node;
|
|
672
|
+
if (element.classList?.contains('mention-chip')) {
|
|
673
|
+
if (currentPosition === globalPosition) {
|
|
674
|
+
// Position is right before the chip
|
|
675
|
+
const parentNode = element.parentNode;
|
|
676
|
+
if (parentNode) {
|
|
677
|
+
return {
|
|
678
|
+
node: parentNode,
|
|
679
|
+
offset: Array.from(parentNode.childNodes).indexOf(element),
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
currentPosition += 1; // Count chip as 1 character
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
node = walker.nextNode();
|
|
687
|
+
}
|
|
688
|
+
// Position is beyond the end, return the last position
|
|
689
|
+
const lastChild = this.editorElement.lastChild;
|
|
690
|
+
if (lastChild) {
|
|
691
|
+
if (lastChild.nodeType === Node.TEXT_NODE) {
|
|
692
|
+
return {
|
|
693
|
+
node: lastChild,
|
|
694
|
+
offset: lastChild.textContent?.length || 0,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
return {
|
|
699
|
+
node: this.editorElement,
|
|
700
|
+
offset: this.editorElement.childNodes.length,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Shows the suggestions popover
|
|
708
|
+
*/
|
|
709
|
+
showSuggestions() {
|
|
710
|
+
if (!this.isShowingSuggestions) {
|
|
711
|
+
this.isShowingSuggestions = true;
|
|
712
|
+
this.selectedSuggestionIndex = 0;
|
|
713
|
+
this.updateContentState();
|
|
714
|
+
// Use Popover API to show the popover
|
|
715
|
+
this.updateComplete.then(() => {
|
|
716
|
+
if (this.suggestionsPopover) {
|
|
717
|
+
this.suggestionsPopover.showPopover();
|
|
718
|
+
this.positionSuggestions();
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Hides the suggestions popover
|
|
725
|
+
*/
|
|
726
|
+
hideSuggestions() {
|
|
727
|
+
if (this.isShowingSuggestions) {
|
|
728
|
+
this.isShowingSuggestions = false;
|
|
729
|
+
this.currentMentionQuery = '';
|
|
730
|
+
this.currentMentionStart = -1;
|
|
731
|
+
this.updateContentState();
|
|
732
|
+
// Use Popover API to hide the popover
|
|
733
|
+
if (this.suggestionsPopover) {
|
|
734
|
+
this.suggestionsPopover.hidePopover();
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Updates filtered suggestions based on current query
|
|
740
|
+
*/
|
|
741
|
+
updateFilteredSuggestions() {
|
|
742
|
+
if (!this.currentMentionQuery) {
|
|
743
|
+
this.filteredSuggestions = this.suggestions.slice(0, 10); // Limit to 10
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const query = this.currentMentionQuery.toLowerCase();
|
|
747
|
+
this.filteredSuggestions = this.suggestions
|
|
748
|
+
.filter((suggestion) => suggestion.label.toLowerCase().includes(query) ||
|
|
749
|
+
suggestion.description?.toLowerCase().includes(query) ||
|
|
750
|
+
suggestion.suffix?.toLowerCase().includes(query))
|
|
751
|
+
.slice(0, 10); // Limit to 10
|
|
752
|
+
// Reset selection if needed
|
|
753
|
+
if (this.selectedSuggestionIndex >= this.filteredSuggestions.length) {
|
|
754
|
+
this.selectedSuggestionIndex = 0;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Positions the suggestions popover using the Popover API anchor positioning
|
|
759
|
+
*/
|
|
760
|
+
positionSuggestions() {
|
|
761
|
+
if (!this.suggestionsPopover)
|
|
762
|
+
return;
|
|
763
|
+
const selection = window.getSelection();
|
|
764
|
+
if (!selection || selection.rangeCount === 0)
|
|
765
|
+
return;
|
|
766
|
+
// Use getComposedRanges() for shadow DOM support, fallback to getRangeAt()
|
|
767
|
+
let range;
|
|
768
|
+
if ('getComposedRanges' in selection && typeof selection.getComposedRanges === 'function' && this.shadowRoot) {
|
|
769
|
+
const composedRanges = selection.getComposedRanges({ shadowRoots: [this.shadowRoot] });
|
|
770
|
+
if (composedRanges.length === 0)
|
|
771
|
+
return;
|
|
772
|
+
range = composedRanges[0];
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
range = selection.getRangeAt(0);
|
|
776
|
+
}
|
|
777
|
+
// Convert StaticRange to Range if needed for getBoundingClientRect
|
|
778
|
+
let workingRange;
|
|
779
|
+
if (range instanceof StaticRange) {
|
|
780
|
+
workingRange = document.createRange();
|
|
781
|
+
workingRange.setStart(range.startContainer, range.startOffset);
|
|
782
|
+
workingRange.setEnd(range.endContainer, range.endOffset);
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
workingRange = range;
|
|
786
|
+
}
|
|
787
|
+
const rect = workingRange.getBoundingClientRect();
|
|
788
|
+
// Position suggestions below the caret using manual positioning as fallback
|
|
789
|
+
// The Popover API will handle proper layering and overflow handling
|
|
790
|
+
this.suggestionsPopover.style.left = `${rect.left}px`;
|
|
791
|
+
this.suggestionsPopover.style.top = `${rect.bottom + 4}px`;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Selects a suggestion and inserts it as a mention
|
|
795
|
+
*/
|
|
796
|
+
selectSuggestion(suggestion) {
|
|
797
|
+
if (!suggestion)
|
|
798
|
+
return;
|
|
799
|
+
// Add to mention map
|
|
800
|
+
this.mentionMap.set(suggestion.id, suggestion);
|
|
801
|
+
// Find the position where the mention trigger starts
|
|
802
|
+
const mentionStart = this.currentMentionStart;
|
|
803
|
+
// Find the DOM node and offset for the mention start position
|
|
804
|
+
const mentionStartInfo = this.findNodeAtGlobalPosition(mentionStart);
|
|
805
|
+
if (!mentionStartInfo) {
|
|
806
|
+
// Fallback: insert at the end
|
|
807
|
+
const chip = this.createMentionChip(suggestion);
|
|
808
|
+
const afterTextNode = document.createTextNode('');
|
|
809
|
+
this.editorElement.appendChild(chip);
|
|
810
|
+
this.editorElement.appendChild(afterTextNode);
|
|
811
|
+
this.setCaretPositionAtTextNode(afterTextNode, 0);
|
|
812
|
+
this.hideSuggestions();
|
|
813
|
+
this.syncValueFromEditor();
|
|
814
|
+
this.dispatchEvent(new CustomEvent('mention-insert', {
|
|
815
|
+
detail: {
|
|
816
|
+
suggestion,
|
|
817
|
+
trigger: this.mentionTrigger + this.currentMentionQuery,
|
|
818
|
+
position: mentionStart,
|
|
819
|
+
},
|
|
820
|
+
bubbles: true,
|
|
821
|
+
}));
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
// If the target is not a text node, insert at the container level
|
|
825
|
+
if (mentionStartInfo.node.nodeType !== Node.TEXT_NODE) {
|
|
826
|
+
const chip = this.createMentionChip(suggestion);
|
|
827
|
+
const container = mentionStartInfo.node;
|
|
828
|
+
const beforeNode = container.childNodes[mentionStartInfo.offset];
|
|
829
|
+
// Create an empty text node for caret positioning
|
|
830
|
+
const afterTextNode = document.createTextNode('');
|
|
831
|
+
if (beforeNode) {
|
|
832
|
+
container.insertBefore(chip, beforeNode);
|
|
833
|
+
container.insertBefore(afterTextNode, beforeNode);
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
container.appendChild(chip);
|
|
837
|
+
container.appendChild(afterTextNode);
|
|
838
|
+
}
|
|
839
|
+
// Position caret at the beginning of the after text node
|
|
840
|
+
this.setCaretPositionAtTextNode(afterTextNode, 0);
|
|
841
|
+
this.hideSuggestions();
|
|
842
|
+
this.syncValueFromEditor();
|
|
843
|
+
this.dispatchEvent(new CustomEvent('mention-insert', {
|
|
844
|
+
detail: {
|
|
845
|
+
suggestion,
|
|
846
|
+
trigger: this.mentionTrigger + this.currentMentionQuery,
|
|
847
|
+
position: mentionStart,
|
|
848
|
+
},
|
|
849
|
+
bubbles: true,
|
|
850
|
+
}));
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
// We have a text node - split it and insert the chip
|
|
854
|
+
const textNode = mentionStartInfo.node;
|
|
855
|
+
const splitOffset = mentionStartInfo.offset;
|
|
856
|
+
// Create the mention chip
|
|
857
|
+
const chip = this.createMentionChip(suggestion);
|
|
858
|
+
// Split the text node at the mention start position
|
|
859
|
+
const beforeText = textNode.textContent?.substring(0, splitOffset) || '';
|
|
860
|
+
const afterText = textNode.textContent?.substring(splitOffset + 1) || ''; // +1 to skip the trigger
|
|
861
|
+
// Replace the original text node with the split content
|
|
862
|
+
const parentNode = textNode.parentNode;
|
|
863
|
+
if (parentNode) {
|
|
864
|
+
// Create new text nodes - always create afterTextNode even if empty
|
|
865
|
+
const beforeTextNode = beforeText ? document.createTextNode(beforeText) : null;
|
|
866
|
+
const afterTextNode = document.createTextNode(afterText); // Always create, even if empty
|
|
867
|
+
// Insert the nodes in order
|
|
868
|
+
if (beforeTextNode) {
|
|
869
|
+
parentNode.insertBefore(beforeTextNode, textNode);
|
|
870
|
+
}
|
|
871
|
+
parentNode.insertBefore(chip, textNode);
|
|
872
|
+
parentNode.insertBefore(afterTextNode, textNode);
|
|
873
|
+
// Remove the original text node
|
|
874
|
+
parentNode.removeChild(textNode);
|
|
875
|
+
// Position caret at the beginning of the after text node
|
|
876
|
+
this.setCaretPositionAtTextNode(afterTextNode, 0);
|
|
877
|
+
}
|
|
878
|
+
else {
|
|
879
|
+
// Fallback: append to editor and create a text node for caret positioning
|
|
880
|
+
const afterTextNode = document.createTextNode('');
|
|
881
|
+
this.editorElement.appendChild(chip);
|
|
882
|
+
this.editorElement.appendChild(afterTextNode);
|
|
883
|
+
this.setCaretPositionAtTextNode(afterTextNode, 0);
|
|
884
|
+
}
|
|
885
|
+
// Hide suggestions
|
|
886
|
+
this.hideSuggestions();
|
|
887
|
+
// Sync value and dispatch events
|
|
888
|
+
this.syncValueFromEditor();
|
|
889
|
+
this.dispatchEvent(new CustomEvent('mention-insert', {
|
|
890
|
+
detail: {
|
|
891
|
+
suggestion,
|
|
892
|
+
trigger: this.mentionTrigger + this.currentMentionQuery,
|
|
893
|
+
position: mentionStart,
|
|
894
|
+
},
|
|
895
|
+
bubbles: true,
|
|
896
|
+
}));
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Removes a mention chip
|
|
900
|
+
*/
|
|
901
|
+
removeMention(chipElement, mention) {
|
|
902
|
+
const position = this.getElementPosition(chipElement);
|
|
903
|
+
chipElement.remove();
|
|
904
|
+
this.syncValueFromEditor();
|
|
905
|
+
this.dispatchEvent(new CustomEvent('mention-remove', {
|
|
906
|
+
detail: {
|
|
907
|
+
suggestion: mention,
|
|
908
|
+
position,
|
|
909
|
+
},
|
|
910
|
+
bubbles: true,
|
|
911
|
+
}));
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Gets the text position of an element
|
|
915
|
+
*/
|
|
916
|
+
getElementPosition(element) {
|
|
917
|
+
let position = 0;
|
|
918
|
+
let currentNode = this.editorElement.firstChild;
|
|
919
|
+
while (currentNode && currentNode !== element) {
|
|
920
|
+
if (currentNode.nodeType === Node.TEXT_NODE) {
|
|
921
|
+
position += currentNode.textContent?.length || 0;
|
|
922
|
+
}
|
|
923
|
+
else if (currentNode.nodeType === Node.ELEMENT_NODE) {
|
|
924
|
+
const element = currentNode;
|
|
925
|
+
if (element.classList?.contains('mention-chip')) {
|
|
926
|
+
position += 1; // Count chip as 1 character
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
currentNode = currentNode.nextSibling;
|
|
930
|
+
}
|
|
931
|
+
return position;
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Handles clicking on a suggestion
|
|
935
|
+
*/
|
|
936
|
+
handleSuggestionClick(suggestion, event) {
|
|
937
|
+
event.preventDefault();
|
|
938
|
+
this.selectSuggestion(suggestion);
|
|
939
|
+
this.focus();
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Renders a suggestion item
|
|
943
|
+
*/
|
|
944
|
+
renderSuggestion(suggestion, index) {
|
|
945
|
+
const isSelected = index === this.selectedSuggestionIndex;
|
|
946
|
+
const classes = classMap({
|
|
947
|
+
'suggestion-item': true,
|
|
948
|
+
'selected': isSelected,
|
|
949
|
+
});
|
|
950
|
+
return html `
|
|
951
|
+
<button
|
|
952
|
+
class="${classes}"
|
|
953
|
+
@click="${(e) => this.handleSuggestionClick(suggestion, e)}"
|
|
954
|
+
@mouseenter="${() => (this.selectedSuggestionIndex = index)}"
|
|
955
|
+
>
|
|
956
|
+
<div class="suggestion-content">
|
|
957
|
+
<div class="suggestion-headline">${suggestion.label}</div>
|
|
958
|
+
${suggestion.description
|
|
959
|
+
? html ` <div class="suggestion-supporting-text">${suggestion.description}</div> `
|
|
960
|
+
: nothing}
|
|
961
|
+
</div>
|
|
962
|
+
${suggestion.suffix ? html ` <div class="suggestion-suffix">${suggestion.suffix}</div> ` : nothing}
|
|
963
|
+
</button>
|
|
964
|
+
`;
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Renders the suggestions popover
|
|
968
|
+
*/
|
|
969
|
+
renderSuggestions() {
|
|
970
|
+
return html `
|
|
971
|
+
<div class="suggestions-popover" popover="manual">
|
|
972
|
+
${this.isShowingSuggestions && this.filteredSuggestions.length > 0
|
|
973
|
+
? this.filteredSuggestions.map((suggestion, index) => this.renderSuggestion(suggestion, index))
|
|
974
|
+
: nothing}
|
|
975
|
+
</div>
|
|
976
|
+
`;
|
|
977
|
+
}
|
|
978
|
+
render() {
|
|
979
|
+
const surfaceClasses = classMap({
|
|
980
|
+
'surface': true,
|
|
981
|
+
'has-content': this.hasContent,
|
|
982
|
+
});
|
|
983
|
+
const labelClasses = classMap({
|
|
984
|
+
label: true,
|
|
985
|
+
floating: this.isLabelFloating,
|
|
986
|
+
});
|
|
987
|
+
return html `
|
|
988
|
+
<div class="${surfaceClasses}">
|
|
989
|
+
<div class="container"></div>
|
|
990
|
+
<div class="content">
|
|
991
|
+
<div class="body">
|
|
992
|
+
${this.label ? html `<div class="${labelClasses}">${this.label}</div>` : nothing}
|
|
993
|
+
<div
|
|
994
|
+
class="editor"
|
|
995
|
+
contenteditable="${!this.disabled}"
|
|
996
|
+
data-placeholder="${ifDefined(this.placeholder)}"
|
|
997
|
+
@input="${this.handleEditorInput}"
|
|
998
|
+
@keydown="${this.handleEditorKeyDown}"
|
|
999
|
+
@focus="${this.handleEditorFocus}"
|
|
1000
|
+
@blur="${this.handleEditorBlur}"
|
|
1001
|
+
@paste="${this.handleEditorPaste}"
|
|
1002
|
+
role="textbox"
|
|
1003
|
+
aria-label="${ifDefined(this.label)}"
|
|
1004
|
+
aria-multiline="true"
|
|
1005
|
+
aria-required="${this.required}"
|
|
1006
|
+
aria-invalid="${this.invalid}"
|
|
1007
|
+
tabindex="${this.disabled ? -1 : 0}"
|
|
1008
|
+
></div>
|
|
1009
|
+
</div>
|
|
1010
|
+
</div>
|
|
1011
|
+
${this.renderSuggestions()}
|
|
1012
|
+
</div>
|
|
1013
|
+
${this.supportingText ? html ` <div class="supporting-text">${this.supportingText}</div> ` : nothing}
|
|
1014
|
+
`;
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
})();
|
|
1018
|
+
/**
|
|
1019
|
+
* A material design textarea component that supports @mentions with suggestions.
|
|
1020
|
+
*
|
|
1021
|
+
* Features:
|
|
1022
|
+
* - Material Design styling
|
|
1023
|
+
* - @mention triggers with customizable suggestions
|
|
1024
|
+
* - Inline pill/chip rendering for mentions
|
|
1025
|
+
* - Proper caret management
|
|
1026
|
+
* - Keyboard navigation for suggestions
|
|
1027
|
+
* - Overflow container support
|
|
1028
|
+
* - Generic design for extensibility
|
|
1029
|
+
* - Accessibility support
|
|
1030
|
+
*
|
|
1031
|
+
* @fires mention-insert - When a mention is inserted
|
|
1032
|
+
* @fires mention-remove - When a mention is removed
|
|
1033
|
+
* @fires input - When the input value changes
|
|
1034
|
+
* @fires change - When the input loses focus and value has changed
|
|
1035
|
+
*/
|
|
1036
|
+
export default MentionTextArea;
|
|
1037
|
+
//# sourceMappingURL=MentionTextArea.js.map
|